diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f40def..2914f60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,6 @@ # 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) - -## 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 diff --git a/README.md b/README.md index c2bd069..646065d 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: 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. +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. 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. @@ -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, uses the default branch. + # Otherwise, defaults to `master`. ref: '' # Personal access token (PAT) used to fetch the repository. The PAT is configured @@ -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 for all branches and tags. + # Number of commits to fetch. 0 indicates all history. # Default: 1 fetch-depth: '' @@ -110,7 +110,6 @@ 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) @@ -118,14 +117,9 @@ 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 history for all tags and branches - -```yaml -- uses: actions/checkout@v2 - with: - fetch-depth: 0 -``` +- [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) ## Checkout a different branch @@ -213,6 +207,29 @@ 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 +``` + # 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 e4e640c..1d5c3d5 100644 --- a/__test__/git-auth-helper.test.ts +++ b/__test__/git-auth-helper.test.ts @@ -714,7 +714,6 @@ async function setup(testName: string): Promise { ), env: {}, fetch: jest.fn(), - getDefaultBranch: jest.fn(), getWorkingDirectory: jest.fn(() => workspace), init: jest.fn(), isDetached: jest.fn(), @@ -723,11 +722,9 @@ 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 70849b5..c39a2a5 100644 --- a/__test__/git-directory-helper.test.ts +++ b/__test__/git-directory-helper.test.ts @@ -9,7 +9,6 @@ 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', () => { @@ -42,8 +41,7 @@ describe('git-directory-helper tests', () => { git, repositoryPath, repositoryUrl, - clean, - ref + clean ) // Assert @@ -65,8 +63,7 @@ describe('git-directory-helper tests', () => { git, repositoryPath, repositoryUrl, - clean, - ref + clean ) // Assert @@ -91,8 +88,7 @@ describe('git-directory-helper tests', () => { git, repositoryPath, repositoryUrl, - clean, - ref + clean ) // Assert @@ -113,8 +109,7 @@ describe('git-directory-helper tests', () => { git, repositoryPath, repositoryUrl, - clean, - ref + clean ) // Assert @@ -142,8 +137,7 @@ describe('git-directory-helper tests', () => { git, repositoryPath, repositoryUrl, - clean, - ref + clean ) // Assert @@ -169,8 +163,7 @@ describe('git-directory-helper tests', () => { git, repositoryPath, differentRepositoryUrl, - clean, - ref + clean ) // Assert @@ -194,8 +187,7 @@ describe('git-directory-helper tests', () => { git, repositoryPath, repositoryUrl, - clean, - ref + clean ) // Assert @@ -220,8 +212,7 @@ describe('git-directory-helper tests', () => { git, repositoryPath, repositoryUrl, - clean, - ref + clean ) // Assert @@ -245,8 +236,7 @@ describe('git-directory-helper tests', () => { undefined, repositoryPath, repositoryUrl, - clean, - ref + clean ) // Assert @@ -270,8 +260,7 @@ describe('git-directory-helper tests', () => { git, repositoryPath, repositoryUrl, - clean, - ref + clean ) // Assert @@ -301,8 +290,7 @@ describe('git-directory-helper tests', () => { git, repositoryPath, repositoryUrl, - clean, - ref + clean ) // Assert @@ -317,66 +305,29 @@ describe('git-directory-helper tests', () => { expect(git.tryReset).not.toHaveBeenCalled() }) - const removesAncestorRemoteBranch = 'removes ancestor remote branch' - it(removesAncestorRemoteBranch, async () => { + const removesRemoteBranches = 'removes local branches' + it(removesRemoteBranches, async () => { // Arrange - await setup(removesAncestorRemoteBranch) + await setup(removesRemoteBranches) 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', 'origin/remote-branch-2'] : [] + return remote ? ['remote-branch-1', 'remote-branch-2'] : [] }) - ref = 'remote-branch-1/conflict' // Act await gitDirectoryHelper.prepareExistingDirectory( git, repositoryPath, repositoryUrl, - clean, - ref + clean ) // 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' - ) - }) - - 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' - ) + expect(git.branchDelete).toHaveBeenCalledWith(true, 'remote-branch-1') + expect(git.branchDelete).toHaveBeenCalledWith(true, 'remote-branch-2') }) }) @@ -393,9 +344,6 @@ async function setup(testName: string): Promise { // Clean clean = true - // Ref - ref = '' - // Git command manager git = { branchDelete: jest.fn(), @@ -408,7 +356,6 @@ 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(), @@ -417,9 +364,7 @@ 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/__test__/input-helper.test.ts b/__test__/input-helper.test.ts index 920bc8e..00732ef 100644 --- a/__test__/input-helper.test.ts +++ b/__test__/input-helper.test.ts @@ -110,6 +110,13 @@ 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/__test__/verify-no-unstaged-changes.sh b/__test__/verify-no-unstaged-changes.sh index 9b30471..9fe6173 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 format && npm run build" + echo "::error::Unstaged changes detected. Locally try running: git clean -ffdx && npm ci && npm run all" exit 1 fi diff --git a/action.yml b/action.yml index 91d3982..58e11b7 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, uses the default branch. + event. Otherwise, defaults to `master`. token: description: > Personal access token (PAT) used to fetch the repository. The PAT is configured @@ -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 for all branches and tags.' + description: 'Number of commits to fetch. 0 indicates all history.' default: 1 lfs: description: 'Whether to download Git-LFS files' diff --git a/adrs/0153-checkout-v2.md b/adrs/0153-checkout-v2.md index f174b1a..b9536a5 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 for all tags and branches.' + description: 'Number of commits to fetch. 0 indicates all history.' default: 1 lfs: description: 'Whether to download Git-LFS files' diff --git a/dist/index.js b/dist/index.js index e0d0238..0074e0a 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, __webpack_require__) { +/***/ (function(__unusedmodule, exports) { "use strict"; @@ -3372,18 +3372,7 @@ 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)); -exports.tagsRefSpec = '+refs/tags/*:refs/tags/*'; function getCheckoutInfo(git, ref, commit) { return __awaiter(this, void 0, void 0, function* () { if (!git) { @@ -3430,15 +3419,6 @@ 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'); @@ -3488,129 +3468,6 @@ 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 { - // 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'; -} /***/ }), @@ -5688,7 +5545,6 @@ 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); @@ -5804,14 +5660,18 @@ class GitCommandManager { return output.exitCode === 0; }); } - fetch(refSpec, fetchDepth) { + fetch(fetchDepth, refSpec) { return __awaiter(this, void 0, void 0, function* () { - 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) { + const args = [ + '-c', + 'protocol.version=2', + 'fetch', + '--no-tags', + '--prune', + '--progress', + '--no-recurse-submodules' + ]; + if (fetchDepth > 0) { args.push(`--depth=${fetchDepth}`); } else if (fshelper.fileExistsSync(path.join(this.workingDirectory, '.git', 'shallow'))) { @@ -5827,33 +5687,6 @@ 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; } @@ -5885,8 +5718,7 @@ class GitCommandManager { } log1() { return __awaiter(this, void 0, void 0, function* () { - const output = yield this.execGit(['log', '-1']); - return output.stdout; + yield this.execGit(['log', '-1']); }); } remoteAdd(remoteName, remoteUrl) { @@ -5897,28 +5729,9 @@ 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']; @@ -6157,7 +5970,7 @@ function getSource(settings) { core.endGroup(); // Prepare existing directory, otherwise recreate if (isExisting) { - yield gitDirectoryHelper.prepareExistingDirectory(git, settings.repositoryPath, repositoryUrl, settings.clean, settings.ref); + yield gitDirectoryHelper.prepareExistingDirectory(git, settings.repositoryPath, repositoryUrl, settings.clean); } if (!git) { // Downloading using REST API @@ -6193,38 +6006,14 @@ 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(); } // Fetch core.startGroup('Fetching the repository'); - 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); - } + const refSpec = refHelper.getRefSpec(settings.ref, settings.commit); + yield git.fetch(settings.fetchDepth, refSpec); core.endGroup(); // Checkout info core.startGroup('Determining the checkout info'); @@ -6268,9 +6057,7 @@ function getSource(settings) { } } // Dump some info about the checked out commit - 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); + yield git.log1(); } finally { // Remove auth @@ -7575,7 +7362,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, ref) { +function prepareExistingDirectory(git, repositoryPath, repositoryUrl, clean) { return __awaiter(this, void 0, void 0, function* () { assert.ok(repositoryPath, 'Expected repositoryPath to be defined'); assert.ok(repositoryUrl, 'Expected repositoryUrl to be defined'); @@ -7615,24 +7402,10 @@ function prepareExistingDirectory(git, repositoryPath, repositoryUrl, clean, ref for (const branch of branches) { yield 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 = 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); - } - } - } + // Remove all refs/remotes/origin/* to avoid conflicts + branches = yield git.branchList(true); + for (const branch of branches) { + yield git.branchDelete(true, branch); } core.endGroup(); // Clean @@ -9563,11 +9336,6 @@ 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'); @@ -9612,42 +9380,6 @@ 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); - 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 - core.info(`Default branch '${result}'`); - // 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); @@ -14550,6 +14282,9 @@ 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}$/)) { @@ -14584,7 +14319,7 @@ function getInputs() { core.debug(`submodules = ${result.submodules}`); core.debug(`recursive submodules = ${result.nestedSubmodules}`); // Auth token - result.authToken = core.getInput('token', { required: true }); + result.authToken = core.getInput('token'); // SSH result.sshKey = core.getInput('ssh-key'); result.sshKnownHosts = core.getInput('ssh-known-hosts'); diff --git a/package.json b/package.json index f413218..7b943ad 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" }, diff --git a/src/git-command-manager.ts b/src/git-command-manager.ts index 8bf3aa1..4cbfe4a 100644 --- a/src/git-command-manager.ts +++ b/src/git-command-manager.ts @@ -3,7 +3,6 @@ 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' @@ -24,19 +23,16 @@ export interface IGitCommandManager { globalConfig?: boolean ): Promise configExists(configKey: string, globalConfig?: boolean): Promise - fetch(refSpec: string[], fetchDepth?: number): Promise - getDefaultBranch(repositoryUrl: string): Promise + fetch(fetchDepth: number, refSpec: string[]): Promise getWorkingDirectory(): string init(): Promise isDetached(): Promise lfsFetch(ref: string): Promise lfsInstall(): Promise - log1(): Promise + 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 @@ -168,14 +164,17 @@ class GitCommandManager { return output.exitCode === 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) { + async fetch(fetchDepth: number, refSpec: string[]): Promise { + const args = [ + '-c', + 'protocol.version=2', + 'fetch', + '--no-tags', + '--prune', + '--progress', + '--no-recurse-submodules' + ] + if (fetchDepth > 0) { args.push(`--depth=${fetchDepth}`) } else if ( fshelper.fileExistsSync( @@ -196,34 +195,6 @@ 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 } @@ -254,9 +225,8 @@ class GitCommandManager { await this.execGit(['lfs', 'install', '--local']) } - async log1(): Promise { - const output = await this.execGit(['log', '-1']) - return output.stdout + async log1(): Promise { + await this.execGit(['log', '-1']) } async remoteAdd(remoteName: string, remoteUrl: string): Promise { @@ -267,27 +237,10 @@ 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 e792190..3866866 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, - ref: string + clean: boolean ): Promise { assert.ok(repositoryPath, 'Expected repositoryPath to be defined') assert.ok(repositoryUrl, 'Expected repositoryUrl to be defined') @@ -56,26 +56,10 @@ export async function prepareExistingDirectory( 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) - } - } - } + // Remove all refs/remotes/origin/* to avoid conflicts + branches = await git.branchList(true) + for (const branch of branches) { + await git.branchDelete(true, branch) } core.endGroup() diff --git a/src/git-source-provider.ts b/src/git-source-provider.ts index 366ff33..6855288 100644 --- a/src/git-source-provider.ts +++ b/src/git-source-provider.ts @@ -42,8 +42,7 @@ export async function getSource(settings: IGitSourceSettings): Promise { git, settings.repositoryPath, repositoryUrl, - settings.clean, - settings.ref + settings.clean ) } @@ -103,21 +102,6 @@ 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() @@ -125,24 +109,8 @@ export async function getSource(settings: IGitSourceSettings): Promise { // Fetch core.startGroup('Fetching the repository') - 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) - } + const refSpec = refHelper.getRefSpec(settings.ref, settings.commit) + await git.fetch(settings.fetchDepth, refSpec) core.endGroup() // Checkout info @@ -202,17 +170,7 @@ export async function getSource(settings: IGitSourceSettings): Promise { } // Dump some info about the checked out commit - 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 - ) + await git.log1() } finally { // Remove auth if (!settings.persistCredentials) { diff --git a/src/github-api-helper.ts b/src/github-api-helper.ts index 8bbcf2d..e559c45 100644 --- a/src/github-api-helper.ts +++ b/src/github-api-helper.ts @@ -19,12 +19,6 @@ 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') @@ -73,46 +67,6 @@ 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) - 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 - core.info(`Default branch '${result}'`) - - // 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 eabb9e0..11a1ab6 100644 --- a/src/input-helper.ts +++ b/src/input-helper.ts @@ -68,6 +68,10 @@ 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}$/)) { @@ -106,7 +110,7 @@ export function getInputs(): IGitSourceSettings { core.debug(`recursive submodules = ${result.nestedSubmodules}`) // Auth token - result.authToken = core.getInput('token', {required: true}) + result.authToken = core.getInput('token') // SSH result.sshKey = core.getInput('ssh-key') diff --git a/src/ref-helper.ts b/src/ref-helper.ts index 381fa60..ff256af 100644 --- a/src/ref-helper.ts +++ b/src/ref-helper.ts @@ -1,9 +1,4 @@ -import {URL} from 'url' 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 @@ -62,16 +57,6 @@ 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') @@ -122,162 +107,3 @@ export function getRefSpec(ref: string, commit: string): string[] { return [`+${ref}:${ref}`] } } - -/** - * 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, - 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' -}