diff --git a/README.md b/README.md index 745ee8a36..9104f8b3b 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it. -Only a single commit is fetched by default, for the ref/SHA that triggered the workflow. Set `fetch-depth` to fetch more history. Refer [here](https://help.github.com/en/articles/events-that-trigger-workflows) to learn which commit `$GITHUB_SHA` points to for different events. +Only a single commit is fetched by default, for the ref/SHA that triggered the workflow. Set `fetch-depth: 0` to fetch all history for all branches and tags. Refer [here](https://help.github.com/en/articles/events-that-trigger-workflows) to learn which commit `$GITHUB_SHA` points to for different events. The auth token is persisted in the local git config. This enables your scripts to run authenticated git commands. The token is removed during post-job cleanup. Set `persist-credentials: false` to opt-out. @@ -110,6 +110,7 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous # Scenarios +- [Fetch all history for all tags and branches](#Fetch-all-history-for-all-tags-and-branches) - [Checkout a different branch](#Checkout-a-different-branch) - [Checkout HEAD^](#Checkout-HEAD) - [Checkout multiple repos (side by side)](#Checkout-multiple-repos-side-by-side) @@ -117,9 +118,14 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous - [Checkout multiple repos (private)](#Checkout-multiple-repos-private) - [Checkout pull request HEAD commit instead of merge commit](#Checkout-pull-request-HEAD-commit-instead-of-merge-commit) - [Checkout pull request on closed event](#Checkout-pull-request-on-closed-event) -- [Fetch all tags](#Fetch-all-tags) -- [Fetch all branches](#Fetch-all-branches) -- [Fetch all history for all tags and branches](#Fetch-all-history-for-all-tags-and-branches) + +## Fetch all history for all tags and branches + +```yaml +- uses: actions/checkout@v2 + with: + fetch-depth: 0 +``` ## Checkout a different branch @@ -207,29 +213,6 @@ jobs: - uses: actions/checkout@v2 ``` -## Fetch all tags - -```yaml -- uses: actions/checkout@v2 -- run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* -``` - -## Fetch all branches - -```yaml -- uses: actions/checkout@v2 -- run: | - git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/* -``` - -## Fetch all history for all tags and branches - -```yaml -- uses: actions/checkout@v2 -- run: | - git fetch --prune --unshallow --tags -``` - # License The scripts and documentation in this project are released under the [MIT License](LICENSE) diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts index 1d5c3d5df..92a462a2e 100644 --- a/__test__/git-auth-helper.test.ts +++ b/__test__/git-auth-helper.test.ts @@ -722,9 +722,11 @@ async function setup(testName: string): Promise { log1: jest.fn(), remoteAdd: jest.fn(), removeEnvironmentVariable: jest.fn((name: string) => delete git.env[name]), + revParse: jest.fn(), setEnvironmentVariable: jest.fn((name: string, value: string) => { git.env[name] = value }), + shaExists: jest.fn(), submoduleForeach: jest.fn(async () => { return '' }), diff --git a/__test__/git-directory-helper.test.ts b/__test__/git-directory-helper.test.ts index c39a2a54c..7283102d3 100644 --- a/__test__/git-directory-helper.test.ts +++ b/__test__/git-directory-helper.test.ts @@ -9,6 +9,7 @@ const testWorkspace = path.join(__dirname, '_temp', 'git-directory-helper') let repositoryPath: string let repositoryUrl: string let clean: boolean +let ref: string let git: IGitCommandManager describe('git-directory-helper tests', () => { @@ -41,7 +42,8 @@ describe('git-directory-helper tests', () => { git, repositoryPath, repositoryUrl, - clean + clean, + ref ) // Assert @@ -63,7 +65,8 @@ describe('git-directory-helper tests', () => { git, repositoryPath, repositoryUrl, - clean + clean, + ref ) // Assert @@ -88,7 +91,8 @@ describe('git-directory-helper tests', () => { git, repositoryPath, repositoryUrl, - clean + clean, + ref ) // Assert @@ -109,7 +113,8 @@ describe('git-directory-helper tests', () => { git, repositoryPath, repositoryUrl, - clean + clean, + ref ) // Assert @@ -137,7 +142,8 @@ describe('git-directory-helper tests', () => { git, repositoryPath, repositoryUrl, - clean + clean, + ref ) // Assert @@ -163,7 +169,8 @@ describe('git-directory-helper tests', () => { git, repositoryPath, differentRepositoryUrl, - clean + clean, + ref ) // Assert @@ -187,7 +194,8 @@ describe('git-directory-helper tests', () => { git, repositoryPath, repositoryUrl, - clean + clean, + ref ) // Assert @@ -212,7 +220,8 @@ describe('git-directory-helper tests', () => { git, repositoryPath, repositoryUrl, - clean + clean, + ref ) // Assert @@ -236,7 +245,8 @@ describe('git-directory-helper tests', () => { undefined, repositoryPath, repositoryUrl, - clean + clean, + ref ) // Assert @@ -260,7 +270,8 @@ describe('git-directory-helper tests', () => { git, repositoryPath, repositoryUrl, - clean + clean, + ref ) // Assert @@ -290,7 +301,8 @@ describe('git-directory-helper tests', () => { git, repositoryPath, repositoryUrl, - clean + clean, + ref ) // Assert @@ -305,29 +317,66 @@ describe('git-directory-helper tests', () => { expect(git.tryReset).not.toHaveBeenCalled() }) - const removesRemoteBranches = 'removes local branches' - it(removesRemoteBranches, async () => { + const removesAncestorRemoteBranch = 'removes ancestor remote branch' + it(removesAncestorRemoteBranch, async () => { // Arrange - await setup(removesRemoteBranches) + await setup(removesAncestorRemoteBranch) await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '') const mockBranchList = git.branchList as jest.Mock mockBranchList.mockImplementation(async (remote: boolean) => { - return remote ? ['remote-branch-1', 'remote-branch-2'] : [] + return remote ? ['origin/remote-branch-1', 'origin/remote-branch-2'] : [] }) + ref = 'remote-branch-1/conflict' // Act await gitDirectoryHelper.prepareExistingDirectory( git, repositoryPath, repositoryUrl, - clean + clean, + ref ) // Assert const files = await fs.promises.readdir(repositoryPath) expect(files.sort()).toEqual(['.git', 'my-file']) - expect(git.branchDelete).toHaveBeenCalledWith(true, 'remote-branch-1') - expect(git.branchDelete).toHaveBeenCalledWith(true, 'remote-branch-2') + expect(git.branchDelete).toHaveBeenCalledTimes(1) + expect(git.branchDelete).toHaveBeenCalledWith( + true, + 'origin/remote-branch-1' + ) + }) + + const removesDescendantRemoteBranches = 'removes descendant remote branch' + it(removesDescendantRemoteBranches, async () => { + // Arrange + await setup(removesDescendantRemoteBranches) + await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '') + const mockBranchList = git.branchList as jest.Mock + mockBranchList.mockImplementation(async (remote: boolean) => { + return remote + ? ['origin/remote-branch-1/conflict', 'origin/remote-branch-2'] + : [] + }) + ref = 'remote-branch-1' + + // Act + await gitDirectoryHelper.prepareExistingDirectory( + git, + repositoryPath, + repositoryUrl, + clean, + ref + ) + + // Assert + const files = await fs.promises.readdir(repositoryPath) + expect(files.sort()).toEqual(['.git', 'my-file']) + expect(git.branchDelete).toHaveBeenCalledTimes(1) + expect(git.branchDelete).toHaveBeenCalledWith( + true, + 'origin/remote-branch-1/conflict' + ) }) }) @@ -344,6 +393,9 @@ async function setup(testName: string): Promise { // Clean clean = true + // Ref + ref = '' + // Git command manager git = { branchDelete: jest.fn(), @@ -364,7 +416,9 @@ async function setup(testName: string): Promise { log1: jest.fn(), remoteAdd: jest.fn(), removeEnvironmentVariable: jest.fn(), + revParse: jest.fn(), setEnvironmentVariable: jest.fn(), + shaExists: jest.fn(), submoduleForeach: jest.fn(), submoduleSync: jest.fn(), submoduleUpdate: jest.fn(), diff --git a/adrs/0153-checkout-v2.md b/adrs/0153-checkout-v2.md index b9536a54b..f174b1a2d 100644 --- a/adrs/0153-checkout-v2.md +++ b/adrs/0153-checkout-v2.md @@ -70,7 +70,7 @@ We want to take this opportunity to make behavioral changes, from v1. This docum description: 'Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching' default: true fetch-depth: - description: 'Number of commits to fetch. 0 indicates all history.' + description: 'Number of commits to fetch. 0 indicates all history for all tags and branches.' default: 1 lfs: description: 'Whether to download Git-LFS files' diff --git a/dist/index.js b/dist/index.js index 86601d1f6..0c78d254c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3383,6 +3383,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const url_1 = __webpack_require__(835); const core = __importStar(__webpack_require__(470)); const github = __importStar(__webpack_require__(469)); +exports.tagsRefSpec = '+refs/tags/*:refs/tags/*'; function getCheckoutInfo(git, ref, commit) { return __awaiter(this, void 0, void 0, function* () { if (!git) { @@ -3429,6 +3430,15 @@ function getCheckoutInfo(git, ref, commit) { }); } exports.getCheckoutInfo = getCheckoutInfo; +function getRefSpecForAllHistory(ref, commit) { + const result = ['+refs/heads/*:refs/remotes/origin/*', exports.tagsRefSpec]; + if (ref && ref.toUpperCase().startsWith('REFS/PULL/')) { + const branch = ref.substring('refs/pull/'.length); + result.push(`+${commit || ref}:refs/remotes/pull/${branch}`); + } + return result; +} +exports.getRefSpecForAllHistory = getRefSpecForAllHistory; function getRefSpec(ref, commit) { if (!ref && !commit) { throw new Error('Args ref and commit cannot both be empty'); @@ -3478,6 +3488,50 @@ function getRefSpec(ref, commit) { } } exports.getRefSpec = getRefSpec; +/** + * Tests whether the initial fetch created the ref at the expected commit + */ +function testRef(git, ref, commit) { + return __awaiter(this, void 0, void 0, function* () { + if (!git) { + throw new Error('Arg git cannot be empty'); + } + if (!ref && !commit) { + throw new Error('Args ref and commit cannot both be empty'); + } + // No SHA? Nothing to test + if (!commit) { + return true; + } + // SHA only? + else if (!ref) { + return yield git.shaExists(commit); + } + const upperRef = ref.toUpperCase(); + // refs/heads/ + if (upperRef.startsWith('REFS/HEADS/')) { + const branch = ref.substring('refs/heads/'.length); + return ((yield git.branchExists(true, `origin/${branch}`)) && + commit === (yield git.revParse(`refs/remotes/origin/${branch}`))); + } + // refs/pull/ + else if (upperRef.startsWith('REFS/PULL/')) { + // Assume matches because fetched using the commit + return true; + } + // refs/tags/ + else if (upperRef.startsWith('REFS/TAGS/')) { + const tagName = ref.substring('refs/tags/'.length); + return ((yield git.tagExists(tagName)) && commit === (yield git.revParse(ref))); + } + // Unexpected + else { + core.debug(`Unexpected ref format '${ref}' when testing ref info`); + return true; + } + }); +} +exports.testRef = testRef; function checkCommitInfo(token, commitInfo, repositoryOwner, repositoryName, ref, commit) { return __awaiter(this, void 0, void 0, function* () { try { @@ -5634,6 +5688,7 @@ const exec = __importStar(__webpack_require__(986)); const fshelper = __importStar(__webpack_require__(618)); const io = __importStar(__webpack_require__(1)); const path = __importStar(__webpack_require__(622)); +const refHelper = __importStar(__webpack_require__(227)); const regexpHelper = __importStar(__webpack_require__(528)); const retryHelper = __importStar(__webpack_require__(587)); const git_version_1 = __webpack_require__(559); @@ -5749,18 +5804,14 @@ class GitCommandManager { return output.exitCode === 0; }); } - fetch(fetchDepth, refSpec) { + fetch(refSpec, fetchDepth) { return __awaiter(this, void 0, void 0, function* () { - const args = [ - '-c', - 'protocol.version=2', - 'fetch', - '--no-tags', - '--prune', - '--progress', - '--no-recurse-submodules' - ]; - if (fetchDepth > 0) { + const args = ['-c', 'protocol.version=2', 'fetch']; + if (!refSpec.some(x => x === refHelper.tagsRefSpec)) { + args.push('--no-tags'); + } + args.push('--prune', '--progress', '--no-recurse-submodules'); + if (fetchDepth && fetchDepth > 0) { args.push(`--depth=${fetchDepth}`); } else if (fshelper.fileExistsSync(path.join(this.workingDirectory, '.git', 'shallow'))) { @@ -5819,9 +5870,28 @@ class GitCommandManager { removeEnvironmentVariable(name) { delete this.gitEnv[name]; } + /** + * Resolves a ref to a SHA. For a branch or lightweight tag, the commit SHA is returned. + * For an annotated tag, the tag SHA is returned. + * @param {string} ref For example: 'refs/heads/master' or '/refs/tags/v1' + * @returns {Promise} + */ + revParse(ref) { + return __awaiter(this, void 0, void 0, function* () { + const output = yield this.execGit(['rev-parse', ref]); + return output.stdout.trim(); + }); + } setEnvironmentVariable(name, value) { this.gitEnv[name] = value; } + shaExists(sha) { + return __awaiter(this, void 0, void 0, function* () { + const args = ['rev-parse', '--verify', '--quiet', `${sha}^{object}`]; + const output = yield this.execGit(args, true); + return output.exitCode === 0; + }); + } submoduleForeach(command, recursive) { return __awaiter(this, void 0, void 0, function* () { const args = ['submodule', 'foreach']; @@ -6060,7 +6130,7 @@ function getSource(settings) { core.endGroup(); // Prepare existing directory, otherwise recreate if (isExisting) { - yield gitDirectoryHelper.prepareExistingDirectory(git, settings.repositoryPath, repositoryUrl, settings.clean); + yield gitDirectoryHelper.prepareExistingDirectory(git, settings.repositoryPath, repositoryUrl, settings.clean, settings.ref); } if (!git) { // Downloading using REST API @@ -6102,8 +6172,21 @@ function getSource(settings) { } // Fetch core.startGroup('Fetching the repository'); - const refSpec = refHelper.getRefSpec(settings.ref, settings.commit); - yield git.fetch(settings.fetchDepth, refSpec); + if (settings.fetchDepth <= 0) { + // Fetch all branches and tags + let refSpec = refHelper.getRefSpecForAllHistory(settings.ref, settings.commit); + yield git.fetch(refSpec); + // When all history is fetched, the ref we're interested in may have moved to a different + // commit (push or force push). If so, fetch again with a targeted refspec. + if (!(yield refHelper.testRef(git, settings.ref, settings.commit))) { + refSpec = refHelper.getRefSpec(settings.ref, settings.commit); + yield git.fetch(refSpec); + } + } + else { + const refSpec = refHelper.getRefSpec(settings.ref, settings.commit); + yield git.fetch(refSpec, settings.fetchDepth); + } core.endGroup(); // Checkout info core.startGroup('Determining the checkout info'); @@ -7454,7 +7537,7 @@ const fs = __importStar(__webpack_require__(747)); const fsHelper = __importStar(__webpack_require__(618)); const io = __importStar(__webpack_require__(1)); const path = __importStar(__webpack_require__(622)); -function prepareExistingDirectory(git, repositoryPath, repositoryUrl, clean) { +function prepareExistingDirectory(git, repositoryPath, repositoryUrl, clean, ref) { return __awaiter(this, void 0, void 0, function* () { assert.ok(repositoryPath, 'Expected repositoryPath to be defined'); assert.ok(repositoryUrl, 'Expected repositoryUrl to be defined'); @@ -7494,10 +7577,24 @@ function prepareExistingDirectory(git, repositoryPath, repositoryUrl, clean) { for (const branch of branches) { yield git.branchDelete(false, branch); } - // Remove all refs/remotes/origin/* to avoid conflicts - branches = yield git.branchList(true); - for (const branch of branches) { - yield git.branchDelete(true, 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 = yield 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)) { + yield git.branchDelete(true, branch); + } + } + } } core.endGroup(); // Clean diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts index 495075a3e..9d2d45fdf 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -3,6 +3,7 @@ import * as exec from '@actions/exec' import * as fshelper from './fs-helper' import * as io from '@actions/io' import * as path from 'path' +import * as refHelper from './ref-helper' import * as regexpHelper from './regexp-helper' import * as retryHelper from './retry-helper' import {GitVersion} from './git-version' @@ -23,7 +24,7 @@ export interface IGitCommandManager { globalConfig?: boolean ): Promise configExists(configKey: string, globalConfig?: boolean): Promise - fetch(fetchDepth: number, refSpec: string[]): Promise + fetch(refSpec: string[], fetchDepth?: number): Promise getWorkingDirectory(): string init(): Promise isDetached(): Promise @@ -32,7 +33,9 @@ export interface IGitCommandManager { log1(): Promise remoteAdd(remoteName: string, remoteUrl: string): Promise removeEnvironmentVariable(name: string): void + revParse(ref: string): Promise setEnvironmentVariable(name: string, value: string): void + shaExists(sha: string): Promise submoduleForeach(command: string, recursive: boolean): Promise submoduleSync(recursive: boolean): Promise submoduleUpdate(fetchDepth: number, recursive: boolean): Promise @@ -164,17 +167,14 @@ class GitCommandManager { return output.exitCode === 0 } - async fetch(fetchDepth: number, refSpec: string[]): Promise { - const args = [ - '-c', - 'protocol.version=2', - 'fetch', - '--no-tags', - '--prune', - '--progress', - '--no-recurse-submodules' - ] - if (fetchDepth > 0) { + async fetch(refSpec: string[], fetchDepth?: number): Promise { + const args = ['-c', 'protocol.version=2', 'fetch'] + if (!refSpec.some(x => x === refHelper.tagsRefSpec)) { + args.push('--no-tags') + } + + args.push('--prune', '--progress', '--no-recurse-submodules') + if (fetchDepth && fetchDepth > 0) { args.push(`--depth=${fetchDepth}`) } else if ( fshelper.fileExistsSync( @@ -238,10 +238,27 @@ class GitCommandManager { delete this.gitEnv[name] } + /** + * Resolves a ref to a SHA. For a branch or lightweight tag, the commit SHA is returned. + * For an annotated tag, the tag SHA is returned. + * @param {string} ref For example: 'refs/heads/master' or '/refs/tags/v1' + * @returns {Promise} + */ + async revParse(ref: string): Promise { + const output = await this.execGit(['rev-parse', ref]) + return output.stdout.trim() + } + setEnvironmentVariable(name: string, value: string): void { this.gitEnv[name] = value } + async shaExists(sha: string): Promise { + const args = ['rev-parse', '--verify', '--quiet', `${sha}^{object}`] + const output = await this.execGit(args, true) + return output.exitCode === 0 + } + async submoduleForeach(command: string, recursive: boolean): Promise { const args = ['submodule', 'foreach'] if (recursive) { diff --git a/src/git-directory-helper.ts b/src/git-directory-helper.ts index 386686604..e7921903b 100644 --- a/src/git-directory-helper.ts +++ b/src/git-directory-helper.ts @@ -5,13 +5,13 @@ import * as fsHelper from './fs-helper' import * as io from '@actions/io' import * as path from 'path' import {IGitCommandManager} from './git-command-manager' -import {IGitSourceSettings} from './git-source-settings' export async function prepareExistingDirectory( git: IGitCommandManager | undefined, repositoryPath: string, repositoryUrl: string, - clean: boolean + clean: boolean, + ref: string ): Promise { assert.ok(repositoryPath, 'Expected repositoryPath to be defined') assert.ok(repositoryUrl, 'Expected repositoryUrl to be defined') @@ -56,10 +56,26 @@ export async function prepareExistingDirectory( await git.branchDelete(false, branch) } - // Remove all refs/remotes/origin/* to avoid conflicts - branches = await git.branchList(true) - for (const branch of branches) { - await git.branchDelete(true, 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() diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts index bde53bb6b..89c16b587 100644 --- a/src/git-source-provider.ts +++ b/src/git-source-provider.ts @@ -42,7 +42,8 @@ export async function getSource(settings: IGitSourceSettings): Promise { git, settings.repositoryPath, repositoryUrl, - settings.clean + settings.clean, + settings.ref ) } @@ -109,8 +110,24 @@ export async function getSource(settings: IGitSourceSettings): Promise { // Fetch core.startGroup('Fetching the repository') - const refSpec = refHelper.getRefSpec(settings.ref, settings.commit) - await git.fetch(settings.fetchDepth, refSpec) + if (settings.fetchDepth <= 0) { + // Fetch all branches and tags + let refSpec = refHelper.getRefSpecForAllHistory( + settings.ref, + settings.commit + ) + await git.fetch(refSpec) + + // When all history is fetched, the ref we're interested in may have moved to a different + // commit (push or force push). If so, fetch again with a targeted refspec. + if (!(await refHelper.testRef(git, settings.ref, settings.commit))) { + refSpec = refHelper.getRefSpec(settings.ref, settings.commit) + await git.fetch(refSpec) + } + } else { + const refSpec = refHelper.getRefSpec(settings.ref, settings.commit) + await git.fetch(refSpec, settings.fetchDepth) + } core.endGroup() // Checkout info diff --git a/src/ref-helper.ts b/src/ref-helper.ts index f6001792b..381fa60ba 100644 --- a/src/ref-helper.ts +++ b/src/ref-helper.ts @@ -3,6 +3,8 @@ import {IGitCommandManager} from './git-command-manager' import * as core from '@actions/core' import * as github from '@actions/github' +export const tagsRefSpec = '+refs/tags/*:refs/tags/*' + export interface ICheckoutInfo { ref: string startPoint: string @@ -60,6 +62,16 @@ export async function getCheckoutInfo( return result } +export function getRefSpecForAllHistory(ref: string, commit: string): string[] { + const result = ['+refs/heads/*:refs/remotes/origin/*', tagsRefSpec] + if (ref && ref.toUpperCase().startsWith('REFS/PULL/')) { + const branch = ref.substring('refs/pull/'.length) + result.push(`+${commit || ref}:refs/remotes/pull/${branch}`) + } + + return result +} + export function getRefSpec(ref: string, commit: string): string[] { if (!ref && !commit) { throw new Error('Args ref and commit cannot both be empty') @@ -111,6 +123,60 @@ export function getRefSpec(ref: string, commit: string): string[] { } } +/** + * Tests whether the initial fetch created the ref at the expected commit + */ +export async function testRef( + git: IGitCommandManager, + ref: string, + commit: string +): Promise { + if (!git) { + throw new Error('Arg git cannot be empty') + } + + if (!ref && !commit) { + throw new Error('Args ref and commit cannot both be empty') + } + + // No SHA? Nothing to test + if (!commit) { + return true + } + // SHA only? + else if (!ref) { + return await git.shaExists(commit) + } + + const upperRef = ref.toUpperCase() + + // refs/heads/ + if (upperRef.startsWith('REFS/HEADS/')) { + const branch = ref.substring('refs/heads/'.length) + return ( + (await git.branchExists(true, `origin/${branch}`)) && + commit === (await git.revParse(`refs/remotes/origin/${branch}`)) + ) + } + // refs/pull/ + else if (upperRef.startsWith('REFS/PULL/')) { + // Assume matches because fetched using the commit + return true + } + // refs/tags/ + else if (upperRef.startsWith('REFS/TAGS/')) { + const tagName = ref.substring('refs/tags/'.length) + return ( + (await git.tagExists(tagName)) && commit === (await git.revParse(ref)) + ) + } + // Unexpected + else { + core.debug(`Unexpected ref format '${ref}' when testing ref info`) + return true + } +} + export async function checkCommitInfo( token: string, commitInfo: string,