From 8a69d6cb013f0338e8e64dc995ae6ace699fc96a Mon Sep 17 00:00:00 2001 From: CrazyMax Date: Sat, 25 Feb 2023 05:41:28 +0100 Subject: [PATCH] git: alternative github context and additional methods Signed-off-by: CrazyMax --- __tests__/git.test.ts | 129 +++++++++++++++++++++++++++++++++++++++--- src/buildx/install.ts | 2 +- src/git.ts | 78 ++++++++++++++++++++++--- src/types/git.ts | 19 +++++++ 4 files changed, 210 insertions(+), 18 deletions(-) create mode 100644 src/types/git.ts diff --git a/__tests__/git.test.ts b/__tests__/git.test.ts index c3d8712..9db8f2e 100644 --- a/__tests__/git.test.ts +++ b/__tests__/git.test.ts @@ -17,18 +17,131 @@ import {beforeEach, describe, expect, it, jest} from '@jest/globals'; import {Git} from '../src/git'; +import {Exec} from '../src/exec'; +import {ExecOutput} from '@actions/exec'; beforeEach(() => { jest.clearAllMocks(); + jest.restoreAllMocks(); }); -describe('git', () => { - it('returns git remote ref', async () => { - try { - expect(await Git.getRemoteSha('https://github.com/docker/buildx.git', 'refs/pull/648/head')).toEqual('f11797113e5a9b86bd976329c5dbb8a8bfdfadfa'); - } catch (e) { - // eslint-disable-next-line jest/no-conditional-expect - expect(e).toEqual(null); - } +describe('context', () => { + it('returns mocked ref and sha', async () => { + jest.spyOn(Exec, 'getExecOutput').mockImplementation((cmd, args): Promise => { + const fullCmd = `${cmd} ${args?.join(' ')}`; + let result = ''; + switch (fullCmd) { + case 'git show --format=%H HEAD --quiet --': + result = 'test-sha'; + break; + case 'git symbolic-ref HEAD': + result = 'refs/heads/test'; + break; + } + return Promise.resolve({ + stdout: result, + stderr: '', + exitCode: 0 + }); + }); + const ctx = await Git.context(); + expect(ctx.ref).toEqual('refs/heads/test'); + expect(ctx.sha).toEqual('test-sha'); + }); +}); + +describe('isInsideWorkTree', () => { + it('have been called', async () => { + const execSpy = jest.spyOn(Exec, 'getExecOutput'); + try { + await Git.isInsideWorkTree(); + } catch (err) { + // noop + } + expect(execSpy).toHaveBeenCalledWith(`git`, ['rev-parse', '--is-inside-work-tree'], { + silent: true, + ignoreReturnCode: true + }); + }); +}); + +describe('remoteSha', () => { + it('returns git remote sha', async () => { + expect(await Git.remoteSha('https://github.com/docker/buildx.git', 'refs/pull/648/head')).toEqual('f11797113e5a9b86bd976329c5dbb8a8bfdfadfa'); + }); +}); + +describe('remoteURL', () => { + it('have been called', async () => { + const execSpy = jest.spyOn(Exec, 'getExecOutput'); + try { + await Git.remoteURL(); + } catch (err) { + // noop + } + expect(execSpy).toHaveBeenCalledWith(`git`, ['remote', 'get-url', 'origin'], { + silent: true, + ignoreReturnCode: true + }); + }); +}); + +describe('ref', () => { + it('have been called', async () => { + const execSpy = jest.spyOn(Exec, 'getExecOutput'); + try { + await Git.ref(); + } catch (err) { + // noop + } + expect(execSpy).toHaveBeenCalledWith(`git`, ['symbolic-ref', 'HEAD'], { + silent: true, + ignoreReturnCode: true + }); + }); +}); + +describe('fullCommit', () => { + it('have been called', async () => { + const execSpy = jest.spyOn(Exec, 'getExecOutput'); + try { + await Git.fullCommit(); + } catch (err) { + // noop + } + expect(execSpy).toHaveBeenCalledWith(`git`, ['show', '--format=%H', 'HEAD', '--quiet', '--'], { + silent: true, + ignoreReturnCode: true + }); + }); +}); + +describe('shortCommit', () => { + it('have been called', async () => { + const execSpy = jest.spyOn(Exec, 'getExecOutput'); + try { + await Git.shortCommit(); + } catch (err) { + // noop + } + expect(execSpy).toHaveBeenCalledWith(`git`, ['show', '--format=%h', 'HEAD', '--quiet', '--'], { + silent: true, + ignoreReturnCode: true + }); + }); +}); + +describe('tag', () => { + it('have been called', async () => { + const execSpy = jest.spyOn(Exec, 'getExecOutput'); + try { + await Git.tag(); + } catch (err) { + // noop + } + expect(execSpy).toHaveBeenCalledWith(`git`, ['tag', '--points-at', 'HEAD', '--sort', '-version:creatordate'], { + silent: true, + ignoreReturnCode: true + }); }); }); diff --git a/src/buildx/install.ts b/src/buildx/install.ts index 30425ab..ea7d1bf 100644 --- a/src/buildx/install.ts +++ b/src/buildx/install.ts @@ -73,7 +73,7 @@ export class Install { if (ref.match(/^[0-9a-fA-F]{40}$/)) { vspec = ref; } else { - vspec = await Git.getRemoteSha(repo, ref); + vspec = await Git.remoteSha(repo, ref); } core.debug(`Install.build: tool version spec ${vspec}`); diff --git a/src/git.ts b/src/git.ts index dcb3f43..44f61fe 100644 --- a/src/git.ts +++ b/src/git.ts @@ -15,21 +15,81 @@ */ import {Exec} from './exec'; +import {Context} from '@actions/github/lib/context'; +import {Context as GitContext} from './types/git'; export class Git { - public static async getRemoteSha(repo: string, ref: string): Promise { - return await Exec.getExecOutput(`git`, ['ls-remote', repo, ref], { - ignoreReturnCode: true, - silent: true - }).then(res => { - if (res.stderr.length > 0 && res.exitCode != 0) { - throw new Error(res.stderr); - } - const [rsha] = res.stdout.trim().split(/[\s\t]/); + public static async context(): Promise { + const ctx = new Context(); + ctx.ref = await Git.ref(); + ctx.sha = await Git.fullCommit(); + return ctx; + } + + public static async isInsideWorkTree(): Promise { + return await Git.exec(['rev-parse', '--is-inside-work-tree']) + .then(out => { + return out === 'true'; + }) + .catch(() => { + return false; + }); + } + + public static async remoteSha(repo: string, ref: string): Promise { + return await Git.exec(['ls-remote', repo, ref]).then(out => { + const [rsha] = out.split(/[\s\t]/); if (rsha.length == 0) { throw new Error(`Cannot find remote ref for ${repo}#${ref}`); } return rsha; }); } + + public static async remoteURL(): Promise { + return await Git.exec(['remote', 'get-url', 'origin']).then(rurl => { + if (rurl.length == 0) { + return Git.exec(['remote', 'get-url', 'upstream']).then(rurl => { + if (rurl.length == 0) { + throw new Error(`Cannot find remote URL for origin or upstream`); + } + return rurl; + }); + } + return rurl; + }); + } + + public static async ref(): Promise { + return await Git.exec(['symbolic-ref', 'HEAD']); + } + + public static async fullCommit(): Promise { + return await Git.exec(['show', '--format=%H', 'HEAD', '--quiet', '--']); + } + + public static async shortCommit(): Promise { + return await Git.exec(['show', '--format=%h', 'HEAD', '--quiet', '--']); + } + + public static async tag(): Promise { + return await Git.exec(['tag', '--points-at', 'HEAD', '--sort', '-version:creatordate']).then(tags => { + if (tags.length == 0) { + return Git.exec(['describe', '--tags', '--abbrev=0']); + } + return tags.split('\n')[0]; + }); + } + + private static async exec(args: string[] = []): Promise { + return await Exec.getExecOutput(`git`, args, { + ignoreReturnCode: true, + silent: true + }).then(res => { + if (res.stderr.length > 0 && res.exitCode != 0) { + throw new Error(res.stderr); + } + return res.stdout.trim(); + }); + } } diff --git a/src/types/git.ts b/src/types/git.ts new file mode 100644 index 0000000..47cd3d0 --- /dev/null +++ b/src/types/git.ts @@ -0,0 +1,19 @@ +/** + * Copyright 2023 actions-toolkit authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Context as GitHubContext} from '@actions/github/lib/context'; + +export type Context = GitHubContext;