From 97b30c411cc8e273e8f90d632b8e53d2604a90ca Mon Sep 17 00:00:00 2001 From: Peter Evans Date: Wed, 20 May 2020 01:34:05 +0900 Subject: [PATCH 01/12] fix prettier glob pattern (#247) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7b943ad..f413218 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "main": "lib/main.js", "scripts": { "build": "tsc && ncc build && node lib/misc/generate-docs.js", - "format": "prettier --write **/*.ts", - "format-check": "prettier --check **/*.ts", + "format": "prettier --write '**/*.ts'", + "format-check": "prettier --check '**/*.ts'", "lint": "eslint src/**/*.ts", "test": "jest" }, From df86c829ebbc4e80aa9885a4762d84e11e0eeacb Mon Sep 17 00:00:00 2001 From: eric sciple Date: Wed, 20 May 2020 10:20:52 -0400 Subject: [PATCH 02/12] fix readme (#251) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 646065d..745ee8a 100644 --- a/README.md +++ b/README.md @@ -227,7 +227,7 @@ jobs: ```yaml - uses: actions/checkout@v2 - run: | - git fetch --prune --unshallow + git fetch --prune --unshallow --tags ``` # License From 2ff2fbdea48a8f5da77a31e7dd5ecb46c017ffc3 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Thu, 21 May 2020 11:09:16 -0400 Subject: [PATCH 03/12] telemetry for incorrect merge commit (#253) --- dist/index.js | 98 +++++++++++++++++++++++++++++++-- src/git-command-manager.ts | 7 +-- src/git-source-provider.ts | 12 ++++- src/ref-helper.ts | 108 +++++++++++++++++++++++++++++++++++++ 4 files changed, 218 insertions(+), 7 deletions(-) diff --git a/dist/index.js b/dist/index.js index 0074e0a..86601d1 100644 --- a/dist/index.js +++ b/dist/index.js @@ -3359,7 +3359,7 @@ module.exports = {"name":"@octokit/rest","version":"16.43.1","publishConfig":{"a /***/ }), /***/ 227: -/***/ (function(__unusedmodule, exports) { +/***/ (function(__unusedmodule, exports, __webpack_require__) { "use strict"; @@ -3372,7 +3372,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; Object.defineProperty(exports, "__esModule", { value: true }); +const url_1 = __webpack_require__(835); +const core = __importStar(__webpack_require__(470)); +const github = __importStar(__webpack_require__(469)); function getCheckoutInfo(git, ref, commit) { return __awaiter(this, void 0, void 0, function* () { if (!git) { @@ -3468,6 +3478,85 @@ function getRefSpec(ref, commit) { } } exports.getRefSpec = getRefSpec; +function checkCommitInfo(token, commitInfo, repositoryOwner, repositoryName, ref, commit) { + return __awaiter(this, void 0, void 0, function* () { + try { + // GHES? + if (isGhes()) { + return; + } + // Auth token? + if (!token) { + return; + } + // Public PR synchronize, for workflow repo? + if (fromPayload('repository.private') !== false || + github.context.eventName !== 'pull_request' || + fromPayload('action') !== 'synchronize' || + repositoryOwner !== github.context.repo.owner || + repositoryName !== github.context.repo.repo || + ref !== github.context.ref || + !ref.startsWith('refs/pull/') || + commit !== github.context.sha) { + return; + } + // Head SHA + const expectedHeadSha = fromPayload('after'); + if (!expectedHeadSha) { + core.debug('Unable to determine head sha'); + return; + } + // Base SHA + const expectedBaseSha = fromPayload('pull_request.base.sha'); + if (!expectedBaseSha) { + core.debug('Unable to determine base sha'); + return; + } + // Expected message? + const expectedMessage = `Merge ${expectedHeadSha} into ${expectedBaseSha}`; + if (commitInfo.indexOf(expectedMessage) >= 0) { + return; + } + // Extract details from message + const match = commitInfo.match(/Merge ([0-9a-f]{40}) into ([0-9a-f]{40})/); + if (!match) { + core.debug('Unexpected message format'); + return; + } + // Post telemetry + const actualHeadSha = match[1]; + if (actualHeadSha !== expectedHeadSha) { + core.debug(`Expected head sha ${expectedHeadSha}; actual head sha ${actualHeadSha}`); + const octokit = new github.GitHub(token, { + userAgent: `actions-checkout-tracepoint/1.0 (code=STALE_MERGE;owner=${repositoryOwner};repo=${repositoryName};pr=${fromPayload('number')};run_id=${process.env['GITHUB_RUN_ID']};expected_head_sha=${expectedHeadSha};actual_head_sha=${actualHeadSha})` + }); + yield octokit.repos.get({ owner: repositoryOwner, repo: repositoryName }); + } + } + catch (err) { + core.debug(`Error when validating commit info: ${err.stack}`); + } + }); +} +exports.checkCommitInfo = checkCommitInfo; +function fromPayload(path) { + return select(github.context.payload, path); +} +function select(obj, path) { + if (!obj) { + return undefined; + } + const i = path.indexOf('.'); + if (i < 0) { + return obj[path]; + } + const key = path.substr(0, i); + return select(obj[key], path.substr(i + 1)); +} +function isGhes() { + const ghUrl = new url_1.URL(process.env['GITHUB_SERVER_URL'] || 'https://github.com'); + return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'; +} /***/ }), @@ -5718,7 +5807,8 @@ class GitCommandManager { } log1() { return __awaiter(this, void 0, void 0, function* () { - yield this.execGit(['log', '-1']); + const output = yield this.execGit(['log', '-1']); + return output.stdout; }); } remoteAdd(remoteName, remoteUrl) { @@ -6057,7 +6147,9 @@ function getSource(settings) { } } // Dump some info about the checked out commit - yield git.log1(); + const commitInfo = yield git.log1(); + // Check for incorrect pull request merge commit + yield refHelper.checkCommitInfo(settings.authToken, commitInfo, settings.repositoryOwner, settings.repositoryName, settings.ref, settings.commit); } finally { // Remove auth diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts index 4cbfe4a..495075a 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -29,7 +29,7 @@ export interface IGitCommandManager { isDetached(): Promise lfsFetch(ref: string): Promise lfsInstall(): Promise - log1(): Promise + log1(): Promise remoteAdd(remoteName: string, remoteUrl: string): Promise removeEnvironmentVariable(name: string): void setEnvironmentVariable(name: string, value: string): void @@ -225,8 +225,9 @@ class GitCommandManager { await this.execGit(['lfs', 'install', '--local']) } - async log1(): Promise { - await this.execGit(['log', '-1']) + async log1(): Promise { + const output = await this.execGit(['log', '-1']) + return output.stdout } async remoteAdd(remoteName: string, remoteUrl: string): Promise { diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts index 6855288..bde53bb 100644 --- a/src/git-source-provider.ts +++ b/src/git-source-provider.ts @@ -170,7 +170,17 @@ export async function getSource(settings: IGitSourceSettings): Promise { } // Dump some info about the checked out commit - await git.log1() + const commitInfo = await git.log1() + + // Check for incorrect pull request merge commit + await refHelper.checkCommitInfo( + settings.authToken, + commitInfo, + settings.repositoryOwner, + settings.repositoryName, + settings.ref, + settings.commit + ) } finally { // Remove auth if (!settings.persistCredentials) { diff --git a/src/ref-helper.ts b/src/ref-helper.ts index ff256af..f600179 100644 --- a/src/ref-helper.ts +++ b/src/ref-helper.ts @@ -1,4 +1,7 @@ +import {URL} from 'url' import {IGitCommandManager} from './git-command-manager' +import * as core from '@actions/core' +import * as github from '@actions/github' export interface ICheckoutInfo { ref: string @@ -107,3 +110,108 @@ export function getRefSpec(ref: string, commit: string): string[] { return [`+${ref}:${ref}`] } } + +export async function checkCommitInfo( + token: string, + commitInfo: string, + repositoryOwner: string, + repositoryName: string, + ref: string, + commit: string +): Promise { + try { + // GHES? + if (isGhes()) { + return + } + + // Auth token? + if (!token) { + return + } + + // Public PR synchronize, for workflow repo? + if ( + fromPayload('repository.private') !== false || + github.context.eventName !== 'pull_request' || + fromPayload('action') !== 'synchronize' || + repositoryOwner !== github.context.repo.owner || + repositoryName !== github.context.repo.repo || + ref !== github.context.ref || + !ref.startsWith('refs/pull/') || + commit !== github.context.sha + ) { + return + } + + // Head SHA + const expectedHeadSha = fromPayload('after') + if (!expectedHeadSha) { + core.debug('Unable to determine head sha') + return + } + + // Base SHA + const expectedBaseSha = fromPayload('pull_request.base.sha') + if (!expectedBaseSha) { + core.debug('Unable to determine base sha') + return + } + + // Expected message? + const expectedMessage = `Merge ${expectedHeadSha} into ${expectedBaseSha}` + if (commitInfo.indexOf(expectedMessage) >= 0) { + return + } + + // Extract details from message + const match = commitInfo.match(/Merge ([0-9a-f]{40}) into ([0-9a-f]{40})/) + if (!match) { + core.debug('Unexpected message format') + return + } + + // Post telemetry + const actualHeadSha = match[1] + if (actualHeadSha !== expectedHeadSha) { + core.debug( + `Expected head sha ${expectedHeadSha}; actual head sha ${actualHeadSha}` + ) + const octokit = new github.GitHub(token, { + userAgent: `actions-checkout-tracepoint/1.0 (code=STALE_MERGE;owner=${repositoryOwner};repo=${repositoryName};pr=${fromPayload( + 'number' + )};run_id=${ + process.env['GITHUB_RUN_ID'] + };expected_head_sha=${expectedHeadSha};actual_head_sha=${actualHeadSha})` + }) + await octokit.repos.get({owner: repositoryOwner, repo: repositoryName}) + } + } catch (err) { + core.debug(`Error when validating commit info: ${err.stack}`) + } +} + +function fromPayload(path: string): any { + return select(github.context.payload, path) +} + +function select(obj: any, path: string): any { + if (!obj) { + return undefined + } + + const i = path.indexOf('.') + if (i < 0) { + return obj[path] + } + + const key = path.substr(0, i) + return select(obj[key], path.substr(i + 1)) +} + +function isGhes(): boolean { + const ghUrl = new URL( + process.env['GITHUB_SERVER_URL'] || 'https://github.com' + ) + return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM' +} From e52d022eb52c224e5f2201beb687a66849e3b200 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Wed, 27 May 2020 09:54:28 -0400 Subject: [PATCH 04/12] Fetch all history for all tags and branches when fetch-depth=0 (#258) --- README.md | 37 ++----- __test__/git-auth-helper.test.ts | 2 + __test__/git-directory-helper.test.ts | 90 +++++++++++++---- adrs/0153-checkout-v2.md | 2 +- dist/index.js | 135 ++++++++++++++++++++++---- src/git-command-manager.ts | 41 +++++--- src/git-directory-helper.ts | 28 ++++-- src/git-source-provider.ts | 23 ++++- src/ref-helper.ts | 66 +++++++++++++ 9 files changed, 338 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 745ee8a..9104f8b 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 1d5c3d5..92a462a 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 c39a2a5..7283102 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 b9536a5..f174b1a 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 86601d1..0c78d25 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 495075a..9d2d45f 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 3866866..e792190 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 bde53bb..89c16b5 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 f600179..381fa60 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, From aabbfeb2ce60b5bd82389903509092c4648a9713 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Wed, 27 May 2020 12:37:40 -0400 Subject: [PATCH 05/12] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2914f60..d3d2627 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## v2.2.0 +- [Fetch all history for all tags and branches when fetch-depth=0](https://github.com/actions/checkout/pull/258) + ## v2.1.1 - Changes to support GHES ([here](https://github.com/actions/checkout/pull/236) and [here](https://github.com/actions/checkout/pull/248)) From 65865e15a14a3de9378a18a026ce6548b17a39ed Mon Sep 17 00:00:00 2001 From: Daniel Hwang Date: Sun, 31 May 2020 14:46:53 -0700 Subject: [PATCH 06/12] build because all is no more (#264) --- __test__/verify-no-unstaged-changes.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__test__/verify-no-unstaged-changes.sh b/__test__/verify-no-unstaged-changes.sh index 9fe6173..a7af194 100755 --- a/__test__/verify-no-unstaged-changes.sh +++ b/__test__/verify-no-unstaged-changes.sh @@ -12,6 +12,6 @@ if [[ "$(git status --porcelain)" != "" ]]; then echo ---------------------------------------- echo Troubleshooting echo ---------------------------------------- - echo "::error::Unstaged changes detected. Locally try running: git clean -ffdx && npm ci && npm run all" + echo "::error::Unstaged changes detected. Locally try running: git clean -ffdx && npm ci && npm run build" exit 1 fi From 453ee27fca95fa9e03a24c1969a92c82e1a9b15e Mon Sep 17 00:00:00 2001 From: eric sciple Date: Sun, 31 May 2020 17:48:32 -0400 Subject: [PATCH 07/12] update troubleshooting instructions to include 'npm run format' --- __test__/verify-no-unstaged-changes.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__test__/verify-no-unstaged-changes.sh b/__test__/verify-no-unstaged-changes.sh index a7af194..9b30471 100755 --- a/__test__/verify-no-unstaged-changes.sh +++ b/__test__/verify-no-unstaged-changes.sh @@ -12,6 +12,6 @@ if [[ "$(git status --porcelain)" != "" ]]; then echo ---------------------------------------- echo Troubleshooting echo ---------------------------------------- - echo "::error::Unstaged changes detected. Locally try running: git clean -ffdx && npm ci && npm run build" + echo "::error::Unstaged changes detected. Locally try running: git clean -ffdx && npm ci && npm run format && npm run build" exit 1 fi From 00a3be89340a3ce8d704f82f44a5e7f9e3a84dfe Mon Sep 17 00:00:00 2001 From: eric sciple Date: Tue, 16 Jun 2020 13:41:01 -0400 Subject: [PATCH 08/12] determine default branch (#278) --- README.md | 2 +- __test__/input-helper.test.ts | 7 ------- action.yml | 2 +- dist/index.js | 36 +++++++++++++++++++++++++++++++---- src/git-source-provider.ts | 11 +++++++++++ src/github-api-helper.ts | 32 +++++++++++++++++++++++++++++++ src/input-helper.ts | 6 +----- 7 files changed, 78 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 9104f8b..f647b6e 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous # The branch, tag or SHA to checkout. When checking out the repository that # triggered a workflow, this defaults to the reference or SHA for that event. - # Otherwise, defaults to `master`. + # Otherwise, uses the default branch. ref: '' # Personal access token (PAT) used to fetch the repository. The PAT is configured diff --git a/__test__/input-helper.test.ts b/__test__/input-helper.test.ts index 00732ef..920bc8e 100644 --- a/__test__/input-helper.test.ts +++ b/__test__/input-helper.test.ts @@ -110,13 +110,6 @@ describe('input-helper tests', () => { ) }) - it('sets correct default ref/sha for other repo', () => { - inputs.repository = 'some-owner/some-other-repo' - const settings: IGitSourceSettings = inputHelper.getInputs() - expect(settings.ref).toBe('refs/heads/master') - expect(settings.commit).toBeFalsy() - }) - it('sets ref to empty when explicit sha', () => { inputs.ref = '1111111111222222222233333333334444444444' const settings: IGitSourceSettings = inputHelper.getInputs() diff --git a/action.yml b/action.yml index 58e11b7..71655da 100644 --- a/action.yml +++ b/action.yml @@ -8,7 +8,7 @@ inputs: description: > The branch, tag or SHA to checkout. When checking out the repository that triggered a workflow, this defaults to the reference or SHA for that - event. Otherwise, defaults to `master`. + event. Otherwise, uses the default branch. token: description: > Personal access token (PAT) used to fetch the repository. The PAT is configured diff --git a/dist/index.js b/dist/index.js index 0c78d25..4ade91c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -6114,6 +6114,12 @@ function getSource(settings) { // Repository URL core.info(`Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}`); const repositoryUrl = urlHelper.getFetchUrl(settings); + // Determine the default branch + if (!settings.ref && !settings.commit) { + core.startGroup('Determining the default branch'); + settings.ref = yield githubApiHelper.getDefaultBranch(settings.authToken, settings.repositoryOwner, settings.repositoryName); + core.endGroup(); + } // Remove conflicting file path if (fsHelper.fileExistsSync(settings.repositoryPath)) { yield io.rmRF(settings.repositoryPath); @@ -9569,6 +9575,31 @@ function downloadRepository(authToken, owner, repo, ref, commit, repositoryPath) }); } exports.downloadRepository = downloadRepository; +/** + * Looks up the default branch name + */ +function getDefaultBranch(authToken, owner, repo) { + return __awaiter(this, void 0, void 0, function* () { + return yield retryHelper.execute(() => __awaiter(this, void 0, void 0, function* () { + core.info('Retrieving the default branch name'); + const octokit = new github.GitHub(authToken); + const response = yield octokit.repos.get({ owner, repo }); + if (response.status != 200) { + throw new Error(`Unexpected response from GitHub API. Status: ${response.status}, Data: ${response.data}`); + } + // Print the default branch + let result = response.data.default_branch; + core.info(`Default branch '${result}'`); + assert.ok(result, 'default_branch cannot be empty'); + // Prefix with 'refs/heads' + if (!result.startsWith('refs/')) { + result = `refs/heads/${result}`; + } + return result; + })); + }); +} +exports.getDefaultBranch = getDefaultBranch; function downloadArchive(authToken, owner, repo, ref, commit) { return __awaiter(this, void 0, void 0, function* () { const octokit = new github.GitHub(authToken); @@ -14471,9 +14502,6 @@ function getInputs() { result.ref = `refs/heads/${result.ref}`; } } - if (!result.ref && !result.commit) { - result.ref = 'refs/heads/master'; - } } // SHA? else if (result.ref.match(/^[0-9a-fA-F]{40}$/)) { @@ -14508,7 +14536,7 @@ function getInputs() { core.debug(`submodules = ${result.submodules}`); core.debug(`recursive submodules = ${result.nestedSubmodules}`); // Auth token - result.authToken = core.getInput('token'); + result.authToken = core.getInput('token', { required: true }); // SSH result.sshKey = core.getInput('ssh-key'); result.sshKnownHosts = core.getInput('ssh-known-hosts'); diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts index 89c16b5..25fba04 100644 --- a/src/git-source-provider.ts +++ b/src/git-source-provider.ts @@ -19,6 +19,17 @@ export async function getSource(settings: IGitSourceSettings): Promise { ) const repositoryUrl = urlHelper.getFetchUrl(settings) + // Determine the default branch + if (!settings.ref && !settings.commit) { + core.startGroup('Determining the default branch') + settings.ref = await githubApiHelper.getDefaultBranch( + settings.authToken, + settings.repositoryOwner, + settings.repositoryName + ) + core.endGroup() + } + // Remove conflicting file path if (fsHelper.fileExistsSync(settings.repositoryPath)) { await io.rmRF(settings.repositoryPath) diff --git a/src/github-api-helper.ts b/src/github-api-helper.ts index e559c45..7a09638 100644 --- a/src/github-api-helper.ts +++ b/src/github-api-helper.ts @@ -67,6 +67,38 @@ export async function downloadRepository( io.rmRF(extractPath) } +/** + * Looks up the default branch name + */ +export async function getDefaultBranch( + authToken: string, + owner: string, + repo: string +): Promise { + return await retryHelper.execute(async () => { + core.info('Retrieving the default branch name') + const octokit = new github.GitHub(authToken) + const response = await octokit.repos.get({owner, repo}) + if (response.status != 200) { + throw new Error( + `Unexpected response from GitHub API. Status: ${response.status}, Data: ${response.data}` + ) + } + + // Print the default branch + let result = response.data.default_branch + core.info(`Default branch '${result}'`) + assert.ok(result, 'default_branch cannot be empty') + + // Prefix with 'refs/heads' + if (!result.startsWith('refs/')) { + result = `refs/heads/${result}` + } + + return result + }) +} + async function downloadArchive( authToken: string, owner: string, diff --git a/src/input-helper.ts b/src/input-helper.ts index 11a1ab6..eabb9e0 100644 --- a/src/input-helper.ts +++ b/src/input-helper.ts @@ -68,10 +68,6 @@ export function getInputs(): IGitSourceSettings { result.ref = `refs/heads/${result.ref}` } } - - if (!result.ref && !result.commit) { - result.ref = 'refs/heads/master' - } } // SHA? else if (result.ref.match(/^[0-9a-fA-F]{40}$/)) { @@ -110,7 +106,7 @@ export function getInputs(): IGitSourceSettings { core.debug(`recursive submodules = ${result.nestedSubmodules}`) // Auth token - result.authToken = core.getInput('token') + result.authToken = core.getInput('token', {required: true}) // SSH result.sshKey = core.getInput('ssh-key') From b4483adec309c0d01a5435c5e24eb40de5773ad9 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Tue, 16 Jun 2020 13:48:53 -0400 Subject: [PATCH 09/12] changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3d2627..e9892cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,15 @@ # Changelog +## v2.3.0 + +- [Fallback to the default branch](https://github.com/actions/checkout/pull/278) + ## v2.2.0 + - [Fetch all history for all tags and branches when fetch-depth=0](https://github.com/actions/checkout/pull/258) ## v2.1.1 + - Changes to support GHES ([here](https://github.com/actions/checkout/pull/236) and [here](https://github.com/actions/checkout/pull/248)) ## v2.1.0 From fb6f360df236bd2026c7963cf88c8ddf20b4f0e2 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Thu, 18 Jun 2020 10:20:33 -0400 Subject: [PATCH 10/12] fix default branch for .wiki and when using ssh (#284) --- __test__/git-auth-helper.test.ts | 1 + __test__/git-directory-helper.test.ts | 1 + dist/index.js | 70 ++++++++++++++++++++++----- src/git-command-manager.ts | 29 +++++++++++ src/git-source-provider.ts | 26 +++++----- src/github-api-helper.ts | 28 ++++++++--- 6 files changed, 126 insertions(+), 29 deletions(-) diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts index 92a462a..e4e640c 100644 --- a/__test__/git-auth-helper.test.ts +++ b/__test__/git-auth-helper.test.ts @@ -714,6 +714,7 @@ async function setup(testName: string): Promise { ), env: {}, fetch: jest.fn(), + getDefaultBranch: jest.fn(), getWorkingDirectory: jest.fn(() => workspace), init: jest.fn(), isDetached: jest.fn(), diff --git a/__test__/git-directory-helper.test.ts b/__test__/git-directory-helper.test.ts index 7283102..70849b5 100644 --- a/__test__/git-directory-helper.test.ts +++ b/__test__/git-directory-helper.test.ts @@ -408,6 +408,7 @@ async function setup(testName: string): Promise { config: jest.fn(), configExists: jest.fn(), fetch: jest.fn(), + getDefaultBranch: jest.fn(), getWorkingDirectory: jest.fn(() => repositoryPath), init: jest.fn(), isDetached: jest.fn(), diff --git a/dist/index.js b/dist/index.js index 4ade91c..e0d0238 100644 --- a/dist/index.js +++ b/dist/index.js @@ -5827,6 +5827,33 @@ class GitCommandManager { })); }); } + getDefaultBranch(repositoryUrl) { + return __awaiter(this, void 0, void 0, function* () { + let output; + yield retryHelper.execute(() => __awaiter(this, void 0, void 0, function* () { + output = yield this.execGit([ + 'ls-remote', + '--quiet', + '--exit-code', + '--symref', + repositoryUrl, + 'HEAD' + ]); + })); + if (output) { + // Satisfy compiler, will always be set + for (let line of output.stdout.trim().split('\n')) { + line = line.trim(); + if (line.startsWith('ref:') || line.endsWith('HEAD')) { + return line + .substr('ref:'.length, line.length - 'ref:'.length - 'HEAD'.length) + .trim(); + } + } + } + throw new Error('Unexpected output when retrieving default branch'); + }); + } getWorkingDirectory() { return this.workingDirectory; } @@ -6114,12 +6141,6 @@ function getSource(settings) { // Repository URL core.info(`Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}`); const repositoryUrl = urlHelper.getFetchUrl(settings); - // Determine the default branch - if (!settings.ref && !settings.commit) { - core.startGroup('Determining the default branch'); - settings.ref = yield githubApiHelper.getDefaultBranch(settings.authToken, settings.repositoryOwner, settings.repositoryName); - core.endGroup(); - } // Remove conflicting file path if (fsHelper.fileExistsSync(settings.repositoryPath)) { yield io.rmRF(settings.repositoryPath); @@ -6172,6 +6193,17 @@ function getSource(settings) { core.startGroup('Setting up auth'); yield authHelper.configureAuth(); core.endGroup(); + // Determine the default branch + if (!settings.ref && !settings.commit) { + core.startGroup('Determining the default branch'); + if (settings.sshKey) { + settings.ref = yield git.getDefaultBranch(repositoryUrl); + } + else { + settings.ref = yield githubApiHelper.getDefaultBranch(settings.authToken, settings.repositoryOwner, settings.repositoryName); + } + core.endGroup(); + } // LFS install if (settings.lfs) { yield git.lfsInstall(); @@ -9531,6 +9563,11 @@ const v4_1 = __importDefault(__webpack_require__(826)); const IS_WINDOWS = process.platform === 'win32'; function downloadRepository(authToken, owner, repo, ref, commit, repositoryPath) { return __awaiter(this, void 0, void 0, function* () { + // Determine the default branch + if (!ref && !commit) { + core.info('Determining the default branch'); + ref = yield getDefaultBranch(authToken, owner, repo); + } // Download the archive let archiveData = yield retryHelper.execute(() => __awaiter(this, void 0, void 0, function* () { core.info('Downloading the archive'); @@ -9583,14 +9620,25 @@ function getDefaultBranch(authToken, owner, repo) { return yield retryHelper.execute(() => __awaiter(this, void 0, void 0, function* () { core.info('Retrieving the default branch name'); const octokit = new github.GitHub(authToken); - const response = yield octokit.repos.get({ owner, repo }); - if (response.status != 200) { - throw new Error(`Unexpected response from GitHub API. Status: ${response.status}, Data: ${response.data}`); + let result; + try { + // Get the default branch from the repo info + const response = yield octokit.repos.get({ owner, repo }); + result = response.data.default_branch; + assert.ok(result, 'default_branch cannot be empty'); + } + catch (err) { + // Handle .wiki repo + if (err['status'] === 404 && repo.toUpperCase().endsWith('.WIKI')) { + result = 'master'; + } + // Otherwise error + else { + throw err; + } } // Print the default branch - let result = response.data.default_branch; core.info(`Default branch '${result}'`); - assert.ok(result, 'default_branch cannot be empty'); // Prefix with 'refs/heads' if (!result.startsWith('refs/')) { result = `refs/heads/${result}`; diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts index 9d2d45f..8bf3aa1 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -25,6 +25,7 @@ export interface IGitCommandManager { ): Promise configExists(configKey: string, globalConfig?: boolean): Promise fetch(refSpec: string[], fetchDepth?: number): Promise + getDefaultBranch(repositoryUrl: string): Promise getWorkingDirectory(): string init(): Promise isDetached(): Promise @@ -195,6 +196,34 @@ class GitCommandManager { }) } + async getDefaultBranch(repositoryUrl: string): Promise { + let output: GitOutput | undefined + await retryHelper.execute(async () => { + output = await this.execGit([ + 'ls-remote', + '--quiet', + '--exit-code', + '--symref', + repositoryUrl, + 'HEAD' + ]) + }) + + if (output) { + // Satisfy compiler, will always be set + for (let line of output.stdout.trim().split('\n')) { + line = line.trim() + if (line.startsWith('ref:') || line.endsWith('HEAD')) { + return line + .substr('ref:'.length, line.length - 'ref:'.length - 'HEAD'.length) + .trim() + } + } + } + + throw new Error('Unexpected output when retrieving default branch') + } + getWorkingDirectory(): string { return this.workingDirectory } diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts index 25fba04..366ff33 100644 --- a/src/git-source-provider.ts +++ b/src/git-source-provider.ts @@ -19,17 +19,6 @@ export async function getSource(settings: IGitSourceSettings): Promise { ) const repositoryUrl = urlHelper.getFetchUrl(settings) - // Determine the default branch - if (!settings.ref && !settings.commit) { - core.startGroup('Determining the default branch') - settings.ref = await githubApiHelper.getDefaultBranch( - settings.authToken, - settings.repositoryOwner, - settings.repositoryName - ) - core.endGroup() - } - // Remove conflicting file path if (fsHelper.fileExistsSync(settings.repositoryPath)) { await io.rmRF(settings.repositoryPath) @@ -114,6 +103,21 @@ export async function getSource(settings: IGitSourceSettings): Promise { await authHelper.configureAuth() core.endGroup() + // Determine the default branch + if (!settings.ref && !settings.commit) { + core.startGroup('Determining the default branch') + if (settings.sshKey) { + settings.ref = await git.getDefaultBranch(repositoryUrl) + } else { + settings.ref = await githubApiHelper.getDefaultBranch( + settings.authToken, + settings.repositoryOwner, + settings.repositoryName + ) + } + core.endGroup() + } + // LFS install if (settings.lfs) { await git.lfsInstall() diff --git a/src/github-api-helper.ts b/src/github-api-helper.ts index 7a09638..8bbcf2d 100644 --- a/src/github-api-helper.ts +++ b/src/github-api-helper.ts @@ -19,6 +19,12 @@ export async function downloadRepository( commit: string, repositoryPath: string ): Promise { + // Determine the default branch + if (!ref && !commit) { + core.info('Determining the default branch') + ref = await getDefaultBranch(authToken, owner, repo) + } + // Download the archive let archiveData = await retryHelper.execute(async () => { core.info('Downloading the archive') @@ -78,17 +84,25 @@ export async function getDefaultBranch( return await retryHelper.execute(async () => { core.info('Retrieving the default branch name') const octokit = new github.GitHub(authToken) - const response = await octokit.repos.get({owner, repo}) - if (response.status != 200) { - throw new Error( - `Unexpected response from GitHub API. Status: ${response.status}, Data: ${response.data}` - ) + let result: string + try { + // Get the default branch from the repo info + const response = await octokit.repos.get({owner, repo}) + result = response.data.default_branch + assert.ok(result, 'default_branch cannot be empty') + } catch (err) { + // Handle .wiki repo + if (err['status'] === 404 && repo.toUpperCase().endsWith('.WIKI')) { + result = 'master' + } + // Otherwise error + else { + throw err + } } // Print the default branch - let result = response.data.default_branch core.info(`Default branch '${result}'`) - assert.ok(result, 'default_branch cannot be empty') // Prefix with 'refs/heads' if (!result.startsWith('refs/')) { From 28c7f3d2b5162b5ddd3dfd9a45aa55eaf396478b Mon Sep 17 00:00:00 2001 From: eric sciple Date: Thu, 18 Jun 2020 10:27:39 -0400 Subject: [PATCH 11/12] changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9892cb..6f40def 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v2.3.1 + +- [Fix default branch resolution for .wiki and when using SSH](https://github.com/actions/checkout/pull/284) + + ## v2.3.0 - [Fallback to the default branch](https://github.com/actions/checkout/pull/278) From 61b9e3751b92087fd0b06925ba6dd6314e06f089 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Sun, 12 Jul 2020 21:02:24 -0400 Subject: [PATCH 12/12] improve description for fetch-depth (#301) --- README.md | 2 +- action.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f647b6e..c2bd069 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous # Default: true clean: '' - # Number of commits to fetch. 0 indicates all history. + # Number of commits to fetch. 0 indicates all history for all branches and tags. # Default: 1 fetch-depth: '' diff --git a/action.yml b/action.yml index 71655da..91d3982 100644 --- a/action.yml +++ b/action.yml @@ -54,7 +54,7 @@ inputs: 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 branches and tags.' default: 1 lfs: description: 'Whether to download Git-LFS files'