create-pull-request/src/git-command-manager.ts

383 lines
8.8 KiB
TypeScript
Raw Normal View History

2020-07-16 08:57:13 +00:00
import * as exec from '@actions/exec'
import * as io from '@actions/io'
2020-07-18 08:55:42 +00:00
import * as utils from './utils'
2020-07-18 07:04:36 +00:00
import * as path from 'path'
2020-07-16 08:57:13 +00:00
const tagsRefSpec = '+refs/tags/*:refs/tags/*'
feat: signed commits (v7) (#3057) * Add support for signed commits (#3055) * formatting * fix eslint and lint errors * shift setting the base to before the push * sign commits by default for testing * add debug lines * read to buffer not string and use non-legacy method to base64 * debug payload without contents * disable linter for debug code * fix filepath when using path input * try to fix head repo * remove commented code * Try refactor of file changes * add tests for building file changes * add build file changes test for binary files * refactor graphql code into github helper class * build file changes even when there is no diff * add function to get commit detail * fix format * build branch commits * use source mode for deleted files * try rest api route * fix check for branch existence * force push * try fix base tree * debug commit verification * debug commit verification * fix format and cleanup * add executable mode file to test * limit blob creation concurrency * only build commits when feature enabled * remove unused code * update readme link * update docs for commit signing * fix capital letter * update docs * add throttling * set default back to false * output head sha and verified status * log outputs * fix head sha output * default the operation output to none * output retryafter for secondary rate limit * use separate client for branch and pull operations * add maintainer-can-modify input * rename git-token to branch-token * fix branch token input * remove deprecated env output * update docs * fix doc * update docs * build branch commits when there is a diff with the base * check verification status of head commit when not known * fix verified output when no commit signing is being used * draft always-true * convert to draft on branch updates when there is a diff with base * update docs with blob size limit * catch errors during blob creation for debugging * parse empty commits * pass base commit to push signed commits * use parent commit details in create commit * use parent tree for base_tree * multipart tree creation * update docs * update readme about the permissions of the default token * fix edge case where changes are partially merged * add updating documentation * fix typo * update major version --------- Co-authored-by: Ravi <1299606+rustycl0ck@users.noreply.github.com>
2024-09-03 07:54:12 +00:00
export type Commit = {
sha: string
tree: string
parents: string[]
signed: boolean
subject: string
body: string
changes: {
mode: string
status: 'A' | 'M' | 'D'
path: string
}[]
unparsedChanges: string[]
}
2020-07-16 08:57:13 +00:00
export class GitCommandManager {
private gitPath: string
private workingDirectory: string
// Git options used when commands require an identity
private identityGitOptions?: string[]
private constructor(workingDirectory: string, gitPath: string) {
this.workingDirectory = workingDirectory
this.gitPath = gitPath
}
static async create(workingDirectory: string): Promise<GitCommandManager> {
const gitPath = await io.which('git', true)
return new GitCommandManager(workingDirectory, gitPath)
}
setIdentityGitOptions(identityGitOptions: string[]): void {
this.identityGitOptions = identityGitOptions
}
async checkout(ref: string, startPoint?: string): Promise<void> {
const args = ['checkout', '--progress']
if (startPoint) {
args.push('-B', ref, startPoint)
} else {
args.push(ref)
}
// https://github.com/git/git/commit/a047fafc7866cc4087201e284dc1f53e8f9a32d5
args.push('--')
2020-07-16 08:57:13 +00:00
await this.exec(args)
}
async cherryPick(
options?: string[],
allowAllExitCodes = false
): Promise<GitOutput> {
const args = ['cherry-pick']
if (this.identityGitOptions) {
args.unshift(...this.identityGitOptions)
}
if (options) {
args.push(...options)
}
return await this.exec(args, allowAllExitCodes)
}
async commit(
options?: string[],
allowAllExitCodes = false
): Promise<GitOutput> {
2020-07-16 08:57:13 +00:00
const args = ['commit']
if (this.identityGitOptions) {
args.unshift(...this.identityGitOptions)
}
if (options) {
args.push(...options)
}
return await this.exec(args, allowAllExitCodes)
2020-07-16 08:57:13 +00:00
}
2020-07-17 11:54:39 +00:00
async config(
configKey: string,
configValue: string,
globalConfig?: boolean,
add?: boolean
2020-07-17 11:54:39 +00:00
): Promise<void> {
const args: string[] = ['config', globalConfig ? '--global' : '--local']
if (add) {
args.push('--add')
}
args.push(...[configKey, configValue])
await this.exec(args)
2020-07-17 11:54:39 +00:00
}
async configExists(
configKey: string,
configValue = '.',
globalConfig?: boolean
): Promise<boolean> {
const output = await this.exec(
[
'config',
globalConfig ? '--global' : '--local',
'--name-only',
'--get-regexp',
configKey,
configValue
],
true
)
return output.exitCode === 0
}
2020-07-16 08:57:13 +00:00
async fetch(
refSpec: string[],
remoteName?: string,
options?: string[],
unshallow = false
2020-07-16 08:57:13 +00:00
): Promise<void> {
2020-07-17 11:54:39 +00:00
const args = ['-c', 'protocol.version=2', 'fetch']
2020-07-16 08:57:13 +00:00
if (!refSpec.some(x => x === tagsRefSpec)) {
args.push('--no-tags')
}
args.push('--progress', '--no-recurse-submodules')
2020-07-18 07:04:36 +00:00
if (
unshallow &&
2020-07-18 08:55:42 +00:00
utils.fileExistsSync(path.join(this.workingDirectory, '.git', 'shallow'))
2020-07-18 07:04:36 +00:00
) {
args.push('--unshallow')
}
2020-07-16 08:57:13 +00:00
if (options) {
args.push(...options)
}
if (remoteName) {
args.push(remoteName)
} else {
args.push('origin')
}
for (const arg of refSpec) {
args.push(arg)
}
await this.exec(args)
}
feat: signed commits (v7) (#3057) * Add support for signed commits (#3055) * formatting * fix eslint and lint errors * shift setting the base to before the push * sign commits by default for testing * add debug lines * read to buffer not string and use non-legacy method to base64 * debug payload without contents * disable linter for debug code * fix filepath when using path input * try to fix head repo * remove commented code * Try refactor of file changes * add tests for building file changes * add build file changes test for binary files * refactor graphql code into github helper class * build file changes even when there is no diff * add function to get commit detail * fix format * build branch commits * use source mode for deleted files * try rest api route * fix check for branch existence * force push * try fix base tree * debug commit verification * debug commit verification * fix format and cleanup * add executable mode file to test * limit blob creation concurrency * only build commits when feature enabled * remove unused code * update readme link * update docs for commit signing * fix capital letter * update docs * add throttling * set default back to false * output head sha and verified status * log outputs * fix head sha output * default the operation output to none * output retryafter for secondary rate limit * use separate client for branch and pull operations * add maintainer-can-modify input * rename git-token to branch-token * fix branch token input * remove deprecated env output * update docs * fix doc * update docs * build branch commits when there is a diff with the base * check verification status of head commit when not known * fix verified output when no commit signing is being used * draft always-true * convert to draft on branch updates when there is a diff with base * update docs with blob size limit * catch errors during blob creation for debugging * parse empty commits * pass base commit to push signed commits * use parent commit details in create commit * use parent tree for base_tree * multipart tree creation * update docs * update readme about the permissions of the default token * fix edge case where changes are partially merged * add updating documentation * fix typo * update major version --------- Co-authored-by: Ravi <1299606+rustycl0ck@users.noreply.github.com>
2024-09-03 07:54:12 +00:00
async getCommit(ref: string): Promise<Commit> {
const endOfBody = '###EOB###'
const output = await this.exec([
'show',
'--raw',
'--cc',
`--format=%H%n%T%n%P%n%G?%n%s%n%b%n${endOfBody}`,
ref
])
const lines = output.stdout.split('\n')
const endOfBodyIndex = lines.lastIndexOf(endOfBody)
const detailLines = lines.slice(0, endOfBodyIndex)
const unparsedChanges: string[] = []
return <Commit>{
sha: detailLines[0],
tree: detailLines[1],
parents: detailLines[2].split(' '),
signed: detailLines[3] !== 'N',
subject: detailLines[4],
body: detailLines.slice(5, endOfBodyIndex).join('\n'),
changes: lines.slice(endOfBodyIndex + 2, -1).map(line => {
const change = line.match(
/^:(\d{6}) (\d{6}) \w{7} \w{7} ([AMD])\s+(.*)$/
)
if (change) {
return {
mode: change[3] === 'D' ? change[1] : change[2],
status: change[3],
path: change[4]
}
} else {
unparsedChanges.push(line)
}
}),
unparsedChanges: unparsedChanges
}
}
2020-07-17 11:54:39 +00:00
async getConfigValue(configKey: string, configValue = '.'): Promise<string> {
const output = await this.exec([
'config',
'--local',
'--get-regexp',
configKey,
configValue
])
return output.stdout.trim().split(`${configKey} `)[1]
}
getGitDirectory(): Promise<string> {
return this.revParse('--git-dir')
}
2020-07-16 08:57:13 +00:00
getWorkingDirectory(): string {
return this.workingDirectory
}
async hasDiff(options?: string[]): Promise<boolean> {
const args = ['diff', '--quiet']
if (options) {
args.push(...options)
}
const output = await this.exec(args, true)
return output.exitCode === 1
}
async isDirty(untracked: boolean, pathspec?: string[]): Promise<boolean> {
const pathspecArgs = pathspec ? ['--', ...pathspec] : []
// Check untracked changes
const sargs = ['--porcelain', '-unormal']
sargs.push(...pathspecArgs)
if (untracked && (await this.status(sargs))) {
2020-07-16 08:57:13 +00:00
return true
}
// Check working index changes
if (await this.hasDiff(pathspecArgs)) {
2020-07-16 08:57:13 +00:00
return true
}
// Check staged changes
const dargs = ['--staged']
dargs.push(...pathspecArgs)
if (await this.hasDiff(dargs)) {
2020-07-16 08:57:13 +00:00
return true
}
return false
}
async push(options?: string[]): Promise<void> {
const args = ['push']
if (options) {
args.push(...options)
}
await this.exec(args)
}
async revList(
commitExpression: string[],
options?: string[]
): Promise<string> {
const args = ['rev-list']
if (options) {
args.push(...options)
}
args.push(...commitExpression)
const output = await this.exec(args)
return output.stdout.trim()
}
async revParse(ref: string, options?: string[]): Promise<string> {
const args = ['rev-parse']
if (options) {
args.push(...options)
}
args.push(ref)
const output = await this.exec(args)
return output.stdout.trim()
}
async stashPush(options?: string[]): Promise<boolean> {
const args = ['stash', 'push']
if (options) {
args.push(...options)
}
const output = await this.exec(args)
return output.stdout.trim() !== 'No local changes to save'
}
async stashPop(options?: string[]): Promise<void> {
const args = ['stash', 'pop']
if (options) {
args.push(...options)
}
await this.exec(args)
}
2020-07-16 08:57:13 +00:00
async status(options?: string[]): Promise<string> {
const args = ['status']
if (options) {
args.push(...options)
}
const output = await this.exec(args)
return output.stdout.trim()
}
async symbolicRef(ref: string, options?: string[]): Promise<string> {
const args = ['symbolic-ref', ref]
if (options) {
args.push(...options)
}
const output = await this.exec(args)
return output.stdout.trim()
}
async tryConfigUnset(
configKey: string,
2020-07-17 11:54:39 +00:00
configValue = '.',
2020-07-16 08:57:13 +00:00
globalConfig?: boolean
): Promise<boolean> {
const output = await this.exec(
[
'config',
globalConfig ? '--global' : '--local',
2020-07-17 11:54:39 +00:00
'--unset',
configKey,
configValue
2020-07-16 08:57:13 +00:00
],
true
)
return output.exitCode === 0
}
2020-07-17 11:54:39 +00:00
async tryGetRemoteUrl(): Promise<string> {
2020-07-16 08:57:13 +00:00
const output = await this.exec(
['config', '--local', '--get', 'remote.origin.url'],
true
)
if (output.exitCode !== 0) {
return ''
}
const stdout = output.stdout.trim()
if (stdout.includes('\n')) {
return ''
}
return stdout
}
async exec(args: string[], allowAllExitCodes = false): Promise<GitOutput> {
const result = new GitOutput()
const env = {}
for (const key of Object.keys(process.env)) {
env[key] = process.env[key]
}
const stdout: string[] = []
const stderr: string[] = []
const options = {
cwd: this.workingDirectory,
env,
ignoreReturnCode: allowAllExitCodes,
listeners: {
stdout: (data: Buffer) => {
stdout.push(data.toString())
},
stderr: (data: Buffer) => {
stderr.push(data.toString())
}
}
}
result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options)
result.stdout = stdout.join('')
result.stderr = stderr.join('')
return result
}
}
class GitOutput {
stdout = ''
stderr = ''
exitCode = 0
}