diff --git a/README.ja.md b/README.ja.md index 9a4d3e6d..4c0532bb 100644 --- a/README.ja.md +++ b/README.ja.md @@ -10,9 +10,12 @@ GitHub Actions 用のヘルパー +## Table of Contents + -**Table of Contents** +
+Details - [使用方法](#%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95) - [Logger](#logger) @@ -23,6 +26,7 @@ GitHub Actions 用のヘルパー - [ContextHelper](#contexthelper) - [Author](#author) +
## 使用方法 diff --git a/README.md b/README.md index 6cbca706..ac6d9182 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,12 @@ Helper for GitHub Actions. +## Table of Contents + -**Table of Contents** +
+Details - [Usage](#usage) - [Logger](#logger) @@ -23,6 +26,7 @@ Helper for GitHub Actions. - [ContextHelper](#contexthelper) - [Author](#author) +
## Usage diff --git a/__tests__/git-helper.test.ts b/__tests__/git-helper.test.ts index 6c231812..eb282bea 100644 --- a/__tests__/git-helper.test.ts +++ b/__tests__/git-helper.test.ts @@ -70,13 +70,13 @@ describe('GitHelper', () => { ]); }); - it('should run git checkout', async() => { + it('should run git clone PR', async() => { setExists(false); const mockExec = spyOnExec(); - expect(await helper.clone(workDir, context({ + await helper.clone(workDir, context({ ref: 'refs/pull/123/merge', - }))); + })); execCalledWith(mockExec, [ 'git clone \'--depth=3\' \'https://octocat:token1@github.com/hello/world.git\' \'.\' > /dev/null 2>&1 || :', @@ -85,48 +85,64 @@ describe('GitHelper', () => { ]); }); - it('should throw error', async() => { + it('should run checkout', async() => { setExists(false); + const mockExec = spyOnExec(); + + await helper.clone(workDir, context({ + ref: 'refs/tags/v1.2.3', + sha: '1234567890', + })); - await expect(helper.clone(workDir, context({ - ref: '', - sha: '', - }))).rejects.toThrow('Invalid context.'); + execCalledWith(mockExec, [ + 'git init \'.\'', + 'git remote add origin \'https://octocat:token1@github.com/hello/world.git\' > /dev/null 2>&1 || :', + 'git fetch --no-tags origin \'refs/tags/v1.2.3:refs/tags/v1.2.3\' || :', + 'git checkout -qf 1234567890', + ]); }); }); describe('checkout', () => { - it('should run checkout 1', async() => { + it('should run checkout branch', async() => { const mockExec = spyOnExec(); await helper.checkout(workDir, context()); execCalledWith(mockExec, [ - 'git clone \'--depth=3\' \'https://octocat:token1@github.com/hello/world.git\' \'.\' > /dev/null 2>&1', - 'git fetch \'https://octocat:token1@github.com/hello/world.git\' refs/heads/test-ref > /dev/null 2>&1', + 'rm -rdf \'.work\'', + 'git init \'.\'', + 'git remote add origin \'https://octocat:token1@github.com/hello/world.git\' > /dev/null 2>&1 || :', + 'git fetch --no-tags origin \'refs/heads/test-ref:refs/remotes/origin/test-ref\' || :', 'git checkout -qf test-sha', ]); }); - it('should run checkout 2', async() => { + it('should run checkout merge ref', async() => { const mockExec = spyOnExec(); - await helper.checkout(workDir, context({sha: ''})); + await helper.checkout(workDir, context({ref: 'refs/pull/123/merge'})); execCalledWith(mockExec, [ - 'git clone \'https://octocat:token1@github.com/hello/world.git\' \'.\' > /dev/null 2>&1', - 'git checkout -qf test-ref', + 'rm -rdf \'.work\'', + 'git init \'.\'', + 'git remote add origin \'https://octocat:token1@github.com/hello/world.git\' > /dev/null 2>&1 || :', + 'git fetch --no-tags origin \'refs/pull/123/merge:refs/pull/123/merge\' || :', + 'git checkout -qf test-sha', ]); }); - it('should run checkout 3', async() => { + it('should run checkout tag', async() => { const mockExec = spyOnExec(); - await helper.checkout(workDir, context({sha: '', ref: 'refs/tags/test-tag'})); + await helper.checkout(workDir, context({ref: 'refs/tags/v1.2.3'})); execCalledWith(mockExec, [ - 'git clone \'https://octocat:token1@github.com/hello/world.git\' \'.\' > /dev/null 2>&1', - 'git checkout -qf refs/tags/test-tag', + 'rm -rdf \'.work\'', + 'git init \'.\'', + 'git remote add origin \'https://octocat:token1@github.com/hello/world.git\' > /dev/null 2>&1 || :', + 'git fetch --no-tags origin \'refs/tags/v1.2.3:refs/tags/v1.2.3\' || :', + 'git checkout -qf test-sha', ]); }); }); @@ -451,6 +467,24 @@ describe('GitHelper', () => { 'git show \'--stat-count=10\' HEAD', ]); }); + + it('should run git commit with options', async() => { + setChildProcessParams({stdout: 'M file1\n\nM file2\n'}); + const mockExec = spyOnExec(); + + expect(await helper.commit(workDir, 'test', { + count: 20, + allowEmpty: true, + args: ['--dry-run'], + })).toBeTruthy(); + + execCalledWith(mockExec, [ + 'git add --all', + 'git status --short -uno', + 'git commit --allow-empty --dry-run -qm test', + 'git show \'--stat-count=20\' HEAD', + ]); + }); }); describe('fetchTags', () => { diff --git a/__tests__/utils.test.ts b/__tests__/utils.test.ts index 59c033a1..82798ae5 100644 --- a/__tests__/utils.test.ts +++ b/__tests__/utils.test.ts @@ -6,6 +6,7 @@ import { Utils } from '../src'; const {getWorkspace, getActor, escapeRegExp, getRegExp, getPrefixRegExp, getSuffixRegExp, useNpm, versionCompare, getOctokit} = Utils; const {isSemanticVersioningTagName, isPrRef, getPrMergeRef, getBoolValue, replaceAll, getPrHeadRef, arrayChunk, sleep} = Utils; const {getBranch, getRefForUpdate, uniqueArray, getBuildInfo, split, getArrayInput, generateNewPatchVersion, getPrBranch} = Utils; +const {isBranch, isTagRef, normalizeRef, trimRef, getTag, getRefspec} = Utils; jest.useFakeTimers(); @@ -482,3 +483,63 @@ describe('getOctokit', () => { expect(() => getOctokit()).toThrow(); }); }); + +describe('isBranch', () => { + it('should return true', () => { + expect(isBranch('refs/heads/master')).toBe(true); + expect(isBranch('heads/master')).toBe(true); + }); + + it('should return false', () => { + expect(isBranch('test')).toBe(false); + expect(isBranch('heads')).toBe(false); + }); +}); + +describe('isTagRef', () => { + it('should return true', () => { + expect(isTagRef('refs/tags/v1.2.3')).toBe(true); + }); + + it('should return false', () => { + expect(isTagRef('refs/heads/master')).toBe(false); + expect(isTagRef('heads/master')).toBe(false); + }); +}); + +describe('normalizeRef', () => { + it('should normalize ref', () => { + expect(normalizeRef('master')).toBe('refs/heads/master'); + expect(normalizeRef('refs/heads/master')).toBe('refs/heads/master'); + expect(normalizeRef('refs/tags/v1.2.3')).toBe('refs/tags/v1.2.3'); + expect(normalizeRef('refs/pull/123/merge')).toBe('refs/pull/123/merge'); + }); +}); + +describe('trimRef', () => { + it('should trim ref', () => { + expect(trimRef('master')).toBe('master'); + expect(trimRef('refs/heads/master')).toBe('master'); + expect(trimRef('refs/tags/v1.2.3')).toBe('v1.2.3'); + expect(trimRef('refs/pull/123/merge')).toBe('123/merge'); + }); +}); + +describe('getTag', () => { + it('should get tag', () => { + expect(getTag('master')).toBe(''); + expect(getTag('heads/master')).toBe(''); + expect(getTag('refs/heads/master')).toBe(''); + expect(getTag('refs/tags/v1.2.3')).toBe('v1.2.3'); + expect(getTag('refs/pull/123/merge')).toBe(''); + }); +}); + +describe('getRefspec', () => { + it('should get refspec', () => { + expect(getRefspec('master')).toBe('refs/heads/master:refs/remotes/origin/master'); + expect(getRefspec('refs/heads/master', 'test')).toBe('refs/heads/master:refs/remotes/test/master'); + expect(getRefspec('refs/tags/v1.2.3')).toBe('refs/tags/v1.2.3:refs/tags/v1.2.3'); + expect(getRefspec('refs/pull/123/merge')).toBe('refs/pull/123/merge:refs/pull/123/merge'); + }); +}); diff --git a/jest.config.js b/jest.config.js index 0098a6ea..92a6974a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,9 +1,9 @@ module.exports = { clearMocks: true, - moduleFileExtensions: [ 'js', 'ts' ], - setupFiles: [ '/jest.setup.ts' ], + moduleFileExtensions: ['js', 'ts'], + setupFiles: ['/jest.setup.ts'], testEnvironment: 'node', - testMatch: [ '**/*.test.ts' ], + testMatch: ['**/*.test.ts'], testRunner: 'jest-circus/runner', transform: { '^.+\\.ts$': 'ts-jest', diff --git a/package.json b/package.json index c79dc88e..2c1fc3ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@technote-space/github-action-helper", - "version": "0.8.4", + "version": "0.8.5", "description": "Helper to filter GitHub Action.", "author": { "name": "Technote", @@ -32,7 +32,7 @@ "sprintf-js": "^1.1.2" }, "devDependencies": { - "@technote-space/github-action-test-helper": "^0.1.5", + "@technote-space/github-action-test-helper": "^0.2.0", "@types/jest": "^25.1.1", "@types/node": "^13.7.0", "@typescript-eslint/eslint-plugin": "^2.19.0", diff --git a/src/git-helper.ts b/src/git-helper.ts index 67bf5165..df029f38 100644 --- a/src/git-helper.ts +++ b/src/git-helper.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import { Context } from '@actions/github/lib/context'; import { Command, Logger } from './index'; -import { getBranch, isBranch, isPrRef, isCloned, split, generateNewPatchVersion, arrayChunk, versionCompare, getAccessToken } from './utils'; +import { getBranch, isBranch, isPrRef, getRefspec, isCloned, split, generateNewPatchVersion, arrayChunk, versionCompare, getAccessToken } from './utils'; import { getGitUrlWithToken } from './context-helper'; type CommandType = string | { @@ -82,17 +82,34 @@ export default class GitHelper { } }; + /** + * @param {string} workDir work dir + * @return {Promise} void + */ + private initialize = async(workDir: string): Promise => { + if (fs.existsSync(workDir)) { + await this.runCommand(workDir, {command: 'rm', args: ['-rdf', workDir]}); + } + fs.mkdirSync(workDir, {recursive: true}); + await this.runCommand(workDir, {command: 'git init', args: ['.']}); + }; + + /** + * @param {Context} context context + * @return {string} origin + */ + private getOrigin = (context: Context): string => this.origin ?? getGitUrlWithToken(context, this.token); + /** * @param {string} workDir work dir * @param {Context} context context * @return {Promise} void */ public addOrigin = async(workDir: string, context: Context): Promise => { - const url = this.getOrigin(context); await this.initialize(workDir); await this.runCommand(workDir, { command: 'git remote add', - args: ['origin', url], + args: ['origin', getGitUrlWithToken(context, this.token)], quiet: this.isQuiet(), altCommand: 'git remote add origin', suppressError: true, @@ -115,12 +132,6 @@ export default class GitHelper { */ private isQuiet = (): boolean => !this.origin || this.quietIfNotOrigin; - /** - * @param {Context} context context - * @return {string} origin - */ - private getOrigin = (context: Context): string => this.origin ?? getGitUrlWithToken(context, this.token); - /** * @param {string} workDir work dir * @return {Promise} branch name @@ -199,64 +210,6 @@ export default class GitHelper { } }; - /** - * @param {string} workDir work dir - * @param {Context} context context - * @return {Promise} void - */ - public checkout = async(workDir: string, context: Context): Promise => { - const url = this.getOrigin(context); - if (this.cloneDepth && context.sha) { - await this.runCommand(workDir, [ - { - command: 'git clone', - args: [this.cloneDepth, url, '.'], - quiet: this.isQuiet(), - altCommand: 'git clone', - }, - { - command: 'git fetch', - args: [url, context.ref], - quiet: this.isQuiet(), - altCommand: `git fetch origin ${context.ref}`, - }, - { - command: 'git checkout', - args: ['-qf', context.sha], - }, - ]); - } else { - const checkout = context.sha || getBranch(context) || context.ref; - if (!checkout) { - throw new Error('Invalid context.'); - } - await this.runCommand(workDir, [ - { - command: 'git clone', - args: [url, '.'], - quiet: this.isQuiet(), - altCommand: 'git clone', - }, - { - command: 'git checkout', - args: ['-qf', checkout], - }, - ]); - } - }; - - /** - * @param {string} workDir work dir - * @return {Promise} void - */ - private initialize = async(workDir: string): Promise => { - if (fs.existsSync(workDir)) { - await this.runCommand(workDir, {command: 'rm', args: ['-rdf', workDir]}); - } - fs.mkdirSync(workDir, {recursive: true}); - await this.runCommand(workDir, {command: 'git init', args: ['.']}); - }; - /** * @param {string} workDir work dir * @param {string} branch branch @@ -288,6 +241,22 @@ export default class GitHelper { }); }; + /** + * @param {string} workDir work dir + * @param {Context} context context + * @return {Promise} void + */ + public checkout = async(workDir: string, context: Context): Promise => { + await this.fetchOrigin(workDir, context, ['--no-tags'], [getRefspec(context)]); + await this.runCommand(workDir, [ + { + command: 'git checkout', + args: ['-qf', context.sha], + stderrToStdout: true, + }, + ]); + }; + /** * @param {string} workDir work dir * @param {string} branch branch @@ -387,8 +356,9 @@ export default class GitHelper { /** * @param {string} workDir work dir * @param {string} message message + * @param {object} options options */ - public commit = async(workDir: string, message: string): Promise => { + public commit = async(workDir: string, message: string, options?: { count?: number; allowEmpty?: boolean; args?: Array }): Promise => { await this.runCommand(workDir, {command: 'git add', args: ['--all']}); if (!await this.checkDiff(workDir)) { @@ -396,20 +366,24 @@ export default class GitHelper { return false; } - await this.makeCommit(workDir, message); + await this.makeCommit(workDir, message, options); return true; }; /** * @param {string} workDir work dir * @param {string} message message - * @param {number} count stat count + * @param {object} options options */ - public makeCommit = async(workDir: string, message: string, count = 10): Promise => { // eslint-disable-line no-magic-numbers + public makeCommit = async(workDir: string, message: string, options?: { count?: number; allowEmpty?: boolean; args?: Array }): Promise => { + const count = options?.count ?? 10; // eslint-disable-line no-magic-numbers + const allowEmpty = options?.allowEmpty ?? false; + const args = options?.args ?? []; + await this.runCommand(workDir, [ { command: 'git commit', - args: ['-qm', message], + args: [allowEmpty ? '--allow-empty' : '', ...args, '-qm', message], }, { command: 'git show', diff --git a/src/utils.ts b/src/utils.ts index c1728427..d8d1e132 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -32,7 +32,9 @@ export const isCloned = (workDir: string): boolean => fs.existsSync(path.resolve export const isSemanticVersioningTagName = (tagName: string): boolean => /^v?\d+(\.\d+)*$/i.test(tagName); -export const isBranch = (ref: string | Context): boolean => /^(refs\/)?heads/.test(getRef(ref)); +export const isBranch = (ref: string | Context): boolean => /^(refs\/)?heads\//.test(getRef(ref)); + +export const isTagRef = (ref: string | Context): boolean => /^refs\/?tags\//.test(getRef(ref)); export const isRemoteBranch = (ref: string | Context): boolean => /^(refs\/)?remotes\/origin\//.test(getRef(ref)); @@ -56,6 +58,16 @@ export const getBranch = (ref: string | Context, defaultIsEmpty = true): string export const getPrBranch = (context: Context): string => context.payload.pull_request?.head.ref ?? ''; +export const normalizeRef = (ref: string | Context): string => /^refs\//.test(getRef(ref)) ? getRef(ref) : `refs/heads/${getRef(ref)}`; + +export const trimRef = (ref: string | Context): string => getRef(ref).replace(/^refs\/(heads|tags|pull)\//, ''); + +export const getTag = (ref: string | Context): string => isTagRef(ref) ? trimRef(ref) : ''; + +const saveTarget = (ref: string | Context, origin: string): string => isTagRef(ref) ? 'tags' : isPrRef(ref) ? 'pull' : `remotes/${origin}`; + +export const getRefspec = (ref: string | Context, origin = 'origin'): string => `${normalizeRef(ref)}:refs/${saveTarget(ref, origin)}/${trimRef(ref)}`; + export const getAccessToken = (required: boolean): string => getInput('GITHUB_TOKEN', {required}); // eslint-disable-next-line @typescript-eslint/ban-ts-ignore diff --git a/yarn.lock b/yarn.lock index 247d5c29..464d112c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -450,10 +450,10 @@ dependencies: type-detect "4.0.8" -"@technote-space/github-action-test-helper@^0.1.5": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@technote-space/github-action-test-helper/-/github-action-test-helper-0.1.5.tgz#2847edf76c0e905f0c3a757eb27e89199442408c" - integrity sha512-v0twcYEBGyeRZRsBrh54chvsiqduTl1NBQMweXmPbNRPJRChCEYUinIPSSYRN4BGsCk2naCeudx5Zjfxv4obew== +"@technote-space/github-action-test-helper@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@technote-space/github-action-test-helper/-/github-action-test-helper-0.2.0.tgz#6a5dc8599fac75c19c3b7562a278427d44b17d7f" + integrity sha512-xxQEh4MiJJg/x4nSknEJVoLPYx2tdkCGaAGrhdg/SmHfWgipDdcP+HxC8FQLRzJHJdDfTOS7YV/+hRaDkStOTw== dependencies: "@actions/github" "^2.1.0" js-yaml "^3.13.1" @@ -1284,9 +1284,9 @@ escape-string-regexp@^1.0.5: integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= escodegen@^1.11.1: - version "1.13.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.13.0.tgz#c7adf9bd3f3cc675bb752f202f79a720189cab29" - integrity sha512-eYk2dCkxR07DsHA/X2hRBj0CFAZeri/LyDMc0C8JT1Hqi6JnVpMhJ7XFITbb0+yZS3lVkaPL2oCkZ3AVmeVbMw== + version "1.14.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457" + integrity sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ== dependencies: esprima "^4.0.1" estraverse "^4.2.0" @@ -3297,9 +3297,9 @@ resolve@1.1.7: integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= resolve@1.x, resolve@^1.3.2: - version "1.15.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.0.tgz#1b7ca96073ebb52e741ffd799f6b39ea462c67f5" - integrity sha512-+hTmAldEGE80U2wJJDC1lebb5jWqvTYAfm3YZ1ckk1gBr0MnCqUKlwK1e+anaFljIl+F5tR5IoZcm4ZDA1zMQw== + version "1.15.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" + integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== dependencies: path-parse "^1.0.6"