import * as assert from 'assert' import * as core from '@actions/core' import * as fs from 'fs' import * as fsHelper from './fs-helper' import * as io from '@actions/io' import * as path from 'path' import {IGitCommandManager} from './git-command-manager' export async function prepareExistingDirectory( git: IGitCommandManager | undefined, repositoryPath: string, repositoryUrl: string, clean: boolean, ref: string, preserveLocalChanges: boolean = false ): Promise { assert.ok(repositoryPath, 'Expected repositoryPath to be defined') assert.ok(repositoryUrl, 'Expected repositoryUrl to be defined') // Indicates whether to delete the directory contents let remove = false // If preserveLocalChanges is true, log it if (preserveLocalChanges) { core.info( `Preserve local changes is enabled, will attempt to keep local files` ) } // Check whether using git or REST API if (!git) { remove = true } // Fetch URL does not match else if ( !fsHelper.directoryExistsSync(path.join(repositoryPath, '.git')) || repositoryUrl !== (await git.tryGetFetchUrl()) ) { remove = true } else { // Delete any index.lock and shallow.lock left by a previously canceled run or crashed git process const lockPaths = [ path.join(repositoryPath, '.git', 'index.lock'), path.join(repositoryPath, '.git', 'shallow.lock') ] for (const lockPath of lockPaths) { try { await io.rmRF(lockPath) } catch (error) { core.debug( `Unable to delete '${lockPath}'. ${(error as any)?.message ?? error}` ) } } try { core.startGroup('Removing previously created refs, to avoid conflicts') // Checkout detached HEAD if (!(await git.isDetached())) { await git.checkoutDetach() } // Remove all refs/heads/* let branches = await git.branchList(false) for (const branch of branches) { await git.branchDelete(false, branch) } // Remove any conflicting refs/remotes/origin/* // Example 1: Consider ref is refs/heads/foo and previously fetched refs/remotes/origin/foo/bar // Example 2: Consider ref is refs/heads/foo/bar and previously fetched refs/remotes/origin/foo if (ref) { ref = ref.startsWith('refs/') ? ref : `refs/heads/${ref}` if (ref.startsWith('refs/heads/')) { const upperName1 = ref.toUpperCase().substr('REFS/HEADS/'.length) const upperName1Slash = `${upperName1}/` branches = await git.branchList(true) for (const branch of branches) { const upperName2 = branch.substr('origin/'.length).toUpperCase() const upperName2Slash = `${upperName2}/` if ( upperName1.startsWith(upperName2Slash) || upperName2.startsWith(upperName1Slash) ) { await git.branchDelete(true, branch) } } } } core.endGroup() // Check for submodules and delete any existing files if submodules are present if (!(await git.submoduleStatus())) { remove = true core.info('Bad Submodules found, removing existing files') } // Clean if (clean) { core.startGroup('Cleaning the repository') if (!(await git.tryClean())) { core.debug( `The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For further investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.` ) remove = true } else if (!(await git.tryReset())) { remove = true } core.endGroup() if (remove) { core.warning( `Unable to clean or reset the repository. The repository will be recreated instead.` ) } } } catch (error) { core.warning( `Unable to prepare the existing repository. The repository will be recreated instead.` ) remove = true } } // Check repository conditions let isLocalGitRepo = git && fsHelper.directoryExistsSync(path.join(repositoryPath, '.git')); let repoUrl = isLocalGitRepo ? await git?.tryGetFetchUrl() : ''; let isSameRepository = repositoryUrl === repoUrl; let differentRepoUrl = !isSameRepository; // Repository URL has changed if (differentRepoUrl) { if (preserveLocalChanges) { core.warning(`Repository URL has changed from '${repoUrl}' to '${repositoryUrl}'. Local changes will be preserved as requested.`); } remove = true; // Mark for removal, but actual removal will respect preserveLocalChanges } if (remove && !preserveLocalChanges) { // Delete the contents of the directory. Don't delete the directory itself // since it might be the current working directory. core.info(`Deleting the contents of '${repositoryPath}'`) for (const file of await fs.promises.readdir(repositoryPath)) { // Skip .git directory as we need it to determine if a file is tracked if (file === '.git') { continue } await io.rmRF(path.join(repositoryPath, file)) } } else if (remove && preserveLocalChanges) { core.info( `Skipping deletion of directory contents due to preserve-local-changes setting` ) // We still need to make sure we have a git repository to work with if (!git) { core.info( `Initializing git repository to prepare for checkout with preserved changes` ) await fs.promises.mkdir(path.join(repositoryPath, '.git'), { recursive: true }) } } }