Compare commits
8 Commits
revert-56-
...
users/eric
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9fadf668a | ||
|
|
4817b449b0 | ||
|
|
689bf84be4 | ||
|
|
cc70598ce8 | ||
|
|
8461dbfed3 | ||
|
|
e347bba93b | ||
|
|
50fbc622fc | ||
|
|
e8bd1dffb6 |
3
.eslintignore
Normal file
3
.eslintignore
Normal file
@@ -0,0 +1,3 @@
|
||||
dist/
|
||||
lib/
|
||||
node_modules/
|
||||
58
.eslintrc.json
Normal file
58
.eslintrc.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"plugins": ["jest", "@typescript-eslint"],
|
||||
"extends": ["plugin:github/es6"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 9,
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"rules": {
|
||||
"eslint-comments/no-use": "off",
|
||||
"import/no-namespace": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
|
||||
"@typescript-eslint/no-require-imports": "error",
|
||||
"@typescript-eslint/array-type": "error",
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
"@typescript-eslint/ban-ts-ignore": "error",
|
||||
"camelcase": "off",
|
||||
"@typescript-eslint/camelcase": "error",
|
||||
"@typescript-eslint/class-name-casing": "error",
|
||||
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
|
||||
"@typescript-eslint/func-call-spacing": ["error", "never"],
|
||||
"@typescript-eslint/generic-type-naming": ["error", "^[A-Z][A-Za-z]*$"],
|
||||
"@typescript-eslint/no-array-constructor": "error",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-extraneous-class": "error",
|
||||
"@typescript-eslint/no-for-in-array": "error",
|
||||
"@typescript-eslint/no-inferrable-types": "error",
|
||||
"@typescript-eslint/no-misused-new": "error",
|
||||
"@typescript-eslint/no-namespace": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||
"@typescript-eslint/no-object-literal-type-assertion": "error",
|
||||
"@typescript-eslint/no-unnecessary-qualifier": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
"@typescript-eslint/no-useless-constructor": "error",
|
||||
"@typescript-eslint/no-var-requires": "error",
|
||||
"@typescript-eslint/prefer-for-of": "warn",
|
||||
"@typescript-eslint/prefer-function-type": "warn",
|
||||
"@typescript-eslint/prefer-includes": "error",
|
||||
"@typescript-eslint/prefer-interface": "error",
|
||||
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
||||
"@typescript-eslint/promise-function-async": "error",
|
||||
"@typescript-eslint/require-array-sort-compare": "error",
|
||||
"@typescript-eslint/restrict-plus-operands": "error",
|
||||
"semi": "off",
|
||||
"@typescript-eslint/semi": ["error", "never"],
|
||||
"@typescript-eslint/type-annotation-spacing": "error",
|
||||
"@typescript-eslint/unbound-method": "error"
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true,
|
||||
"jest/globals": true
|
||||
}
|
||||
}
|
||||
79
.github/workflows/test.yml
vendored
79
.github/workflows/test.yml
vendored
@@ -1,18 +1,83 @@
|
||||
name: "test-local"
|
||||
name: Build and Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- 'releases/*'
|
||||
- releases/*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1 # todo: switch to v2
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npm run format-check
|
||||
- run: npm run lint
|
||||
- run: npm run pack
|
||||
- run: npm run gendocs
|
||||
- name: Verify no unstaged changes
|
||||
run: __test__/verify-no-unstaged-changes.sh
|
||||
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest, macOS-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
runs-on: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: ./
|
||||
- run: git ls-remote --tags origin
|
||||
# Clone this repo
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1 # todo: switch to V2
|
||||
|
||||
# Basic checkout
|
||||
- name: Basic checkout
|
||||
uses: ./
|
||||
with:
|
||||
ref: test-data/v2/basic
|
||||
path: basic
|
||||
- name: Verify basic
|
||||
shell: bash
|
||||
run: __test__/verify-basic.sh
|
||||
|
||||
# Clean
|
||||
- name: Modify work tree
|
||||
shell: bash
|
||||
run: __test__/modify-work-tree.sh
|
||||
- name: Clean checkout
|
||||
uses: ./
|
||||
with:
|
||||
ref: test-data/v2/basic
|
||||
path: basic
|
||||
- name: Verify clean
|
||||
shell: bash
|
||||
run: __test__/verify-clean.sh
|
||||
|
||||
# Side by side
|
||||
- name: Side by side checkout 1
|
||||
uses: ./
|
||||
with:
|
||||
ref: test-data/v2/side-by-side-1
|
||||
path: side-by-side-1
|
||||
- name: Side by side checkout 2
|
||||
uses: ./
|
||||
with:
|
||||
ref: test-data/v2/side-by-side-2
|
||||
path: side-by-side-2
|
||||
- name: Verify side by side
|
||||
shell: bash
|
||||
run: __test__/verify-side-by-side.sh
|
||||
|
||||
# LFS
|
||||
- name: LFS checkout
|
||||
uses: ./
|
||||
with:
|
||||
repository: actions/checkout # hardcoded, otherwise doesn't work from a fork
|
||||
ref: test-data/v2/lfs
|
||||
path: lfs
|
||||
lfs: true
|
||||
- name: Verify LFS
|
||||
shell: bash
|
||||
run: __test__/verify-lfs.sh
|
||||
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
lib/
|
||||
node_modules/
|
||||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
||||
dist/
|
||||
lib/
|
||||
node_modules/
|
||||
11
.prettierrc.json
Normal file
11
.prettierrc.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": false,
|
||||
"arrowParens": "avoid",
|
||||
"parser": "typescript"
|
||||
}
|
||||
23
CHANGELOG.md
Normal file
23
CHANGELOG.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Changelog
|
||||
|
||||
## v2 (beta)
|
||||
|
||||
- Improved fetch performance
|
||||
- The default behavior now fetches only the SHA being checked-out
|
||||
- Script authenticated git commands
|
||||
- Persists `with.token` in the local git config
|
||||
- Enables your scripts to run authenticated git commands
|
||||
- Post-job cleanup removes the token
|
||||
- Coming soon: Opt out by setting `with.persist-credentials` to `false`
|
||||
- Creates a local branch
|
||||
- No longer detached HEAD when checking out a branch
|
||||
- A local branch is created with the corresponding upstream branch set
|
||||
- Improved layout
|
||||
- `with.path` is always relative to `github.workspace`
|
||||
- Aligns better with container actions, where `github.workspace` gets mapped in
|
||||
- Removed input `submodules`
|
||||
|
||||
|
||||
## v1
|
||||
|
||||
Refer [here](https://github.com/actions/checkout/blob/v1/CHANGELOG.md) for the V1 changelog
|
||||
98
README.md
98
README.md
@@ -2,63 +2,93 @@
|
||||
<a href="https://github.com/actions/checkout"><img alt="GitHub Actions status" src="https://github.com/actions/checkout/workflows/test-local/badge.svg"></a>
|
||||
</p>
|
||||
|
||||
# Checkout
|
||||
# Checkout V2 beta
|
||||
|
||||
This action checks out your repository to `$GITHUB_WORKSPACE`, so that your workflow can access the contents of your repository.
|
||||
This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it.
|
||||
|
||||
By default, this is equivalent to running `git fetch` and `git checkout $GITHUB_SHA`, so that you'll always have your repo contents at the version that triggered the workflow.
|
||||
See [here](https://help.github.com/en/articles/events-that-trigger-workflows) to learn what `$GITHUB_SHA` is for different kinds of events.
|
||||
By default, the repository that triggered the workflow is checked-out, for the ref/SHA that triggered the event.
|
||||
|
||||
Refer [here](https://help.github.com/en/articles/events-that-trigger-workflows) to learn which commit `$GITHUB_SHA` points to for different events.
|
||||
|
||||
Changes in V2:
|
||||
- Improved fetch performance
|
||||
- The default behavior now fetches only the SHA being checked-out
|
||||
- Script authenticated git commands
|
||||
- Persists `with.token` in the local git config
|
||||
- Enables your scripts to run authenticated git commands
|
||||
- Post-job cleanup removes the token
|
||||
- Coming soon: Opt out by setting `with.persist-credentials` to `false`
|
||||
- Creates a local branch
|
||||
- No longer detached HEAD when checking out a branch
|
||||
- A local branch is created with the corresponding upstream branch set
|
||||
- Improved layout
|
||||
- `with.path` is always relative to `github.workspace`
|
||||
- Aligns better with container actions, where `github.workspace` gets mapped in
|
||||
- Removed input `submodules`
|
||||
|
||||
Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous versions.
|
||||
|
||||
# Usage
|
||||
|
||||
See [action.yml](action.yml)
|
||||
|
||||
Basic:
|
||||
|
||||
<!-- start usage -->
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/checkout@v2-beta
|
||||
with:
|
||||
node-version: 10.x
|
||||
- run: npm install
|
||||
- run: npm test
|
||||
# Repository name with owner. For example, actions/checkout
|
||||
# Default: ${{ github.repository }}
|
||||
repository: ''
|
||||
|
||||
# Ref to checkout (SHA, branch, tag). For the repository that triggered the
|
||||
# workflow, defaults to the ref/SHA for the event. Otherwise defaults to master.
|
||||
ref: ''
|
||||
|
||||
# Access token for clone repository
|
||||
# Default: ${{ github.token }}
|
||||
token: ''
|
||||
|
||||
# Relative path under $GITHUB_WORKSPACE to place the repository
|
||||
path: ''
|
||||
|
||||
# Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching
|
||||
# Default: true
|
||||
clean: ''
|
||||
|
||||
# Number of commits to fetch. 0 indicates all history.
|
||||
# Default: 1
|
||||
fetch-depth: ''
|
||||
|
||||
# Whether to download Git-LFS files
|
||||
# Default: false
|
||||
lfs: ''
|
||||
```
|
||||
<!-- end usage -->
|
||||
|
||||
By default, the branch or tag ref that triggered the workflow will be checked out, `${{ github.token }}` will be used for any Git server authentication. If you wish to check out a different branch, a different repository or use different token to checkout, specify that using `with.ref`, `with.repository` and `with.token`:
|
||||
## Checkout a different branch
|
||||
|
||||
Checkout different branch from the workflow repository:
|
||||
```yaml
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2-beta
|
||||
with:
|
||||
ref: some-branch
|
||||
```
|
||||
|
||||
Checkout different private repository:
|
||||
## Checkout a different, private repository
|
||||
|
||||
```yaml
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2-beta
|
||||
with:
|
||||
repository: myAccount/myRepository
|
||||
ref: refs/heads/release
|
||||
token: ${{ secrets.GitHub_PAT }} // `GitHub_PAT` is a secret contains your PAT.
|
||||
ref: refs/heads/master
|
||||
token: ${{ secrets.GitHub_PAT }} # `GitHub_PAT` is a secret that contains your PAT
|
||||
```
|
||||
> - `${{ github.token }}` is scoped to the current repository, so if you want to checkout another repository that is private you will need to provide your own [PAT](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line).
|
||||
|
||||
## Checkout the HEAD commit of a PR, rather than the merge commit
|
||||
|
||||
Checkout private submodules:
|
||||
```yaml
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2-beta
|
||||
with:
|
||||
submodules: recursive
|
||||
token: ${{ secrets.GitHub_PAT }} // `GitHub_PAT` is a secret contains your PAT.
|
||||
ref: refs/pull/${{ github.event.number }}/head
|
||||
```
|
||||
> - `with.token` will be used as `Basic` authentication header for https requests talk to https://github.com from `git(.exe)`, ensure those private submodules are configured via `https` not `ssh`.
|
||||
> - `${{ github.token }}` only has permission to the workflow triggering repository. If the repository contains any submodules that comes from private repository, you will have to add your PAT as secret and use the secret in `with.token` to make `checkout` action work.
|
||||
|
||||
For more details, see [Contexts and expression syntax for GitHub Actions](https://help.github.com/en/articles/contexts-and-expression-syntax-for-github-actions) and [Creating and using secrets (encrypted variables)](https://help.github.com/en/articles/virtual-environments-for-github-actions#creating-and-using-secrets-encrypted-variables)
|
||||
|
||||
# Changelog
|
||||
|
||||
## v1.1.0 (unreleased)
|
||||
- Persist `with.token` or `${{ github.token }}` into checkout repository's git config as `http.https://github.com/.extraheader=AUTHORIZATION: basic ***` to better support scripting git
|
||||
|
||||
# License
|
||||
|
||||
|
||||
45
__test__/git-version.test.ts
Normal file
45
__test__/git-version.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {GitVersion} from '../lib/git-version'
|
||||
|
||||
describe('git-version tests', () => {
|
||||
it('basics', async () => {
|
||||
let version = new GitVersion('')
|
||||
expect(version.isValid()).toBeFalsy()
|
||||
|
||||
version = new GitVersion('asdf')
|
||||
expect(version.isValid()).toBeFalsy()
|
||||
|
||||
version = new GitVersion('1.2')
|
||||
expect(version.isValid()).toBeTruthy()
|
||||
expect(version.toString()).toBe('1.2')
|
||||
|
||||
version = new GitVersion('1.2.3')
|
||||
expect(version.isValid()).toBeTruthy()
|
||||
expect(version.toString()).toBe('1.2.3')
|
||||
})
|
||||
|
||||
it('check minimum', async () => {
|
||||
let version = new GitVersion('4.5')
|
||||
expect(version.checkMinimum(new GitVersion('3.6'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('3.6.7'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('4.4'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('4.5'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('4.5.0'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('4.6'))).toBeFalsy()
|
||||
expect(version.checkMinimum(new GitVersion('4.6.0'))).toBeFalsy()
|
||||
expect(version.checkMinimum(new GitVersion('5.1'))).toBeFalsy()
|
||||
expect(version.checkMinimum(new GitVersion('5.1.2'))).toBeFalsy()
|
||||
|
||||
version = new GitVersion('4.5.6')
|
||||
expect(version.checkMinimum(new GitVersion('3.6'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('3.6.7'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('4.4'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('4.5'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('4.5.5'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('4.5.6'))).toBeTruthy()
|
||||
expect(version.checkMinimum(new GitVersion('4.5.7'))).toBeFalsy()
|
||||
expect(version.checkMinimum(new GitVersion('4.6'))).toBeFalsy()
|
||||
expect(version.checkMinimum(new GitVersion('4.6.0'))).toBeFalsy()
|
||||
expect(version.checkMinimum(new GitVersion('5.1'))).toBeFalsy()
|
||||
expect(version.checkMinimum(new GitVersion('5.1.2'))).toBeFalsy()
|
||||
})
|
||||
})
|
||||
120
__test__/input-helper.test.ts
Normal file
120
__test__/input-helper.test.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import * as assert from 'assert'
|
||||
import * as path from 'path'
|
||||
import {ISourceSettings} from '../lib/git-source-provider'
|
||||
|
||||
const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE']
|
||||
const gitHubWorkspace = path.resolve('/checkout-tests/workspace')
|
||||
|
||||
// Late bind
|
||||
let inputHelper: any
|
||||
|
||||
// Mock @actions/core
|
||||
let inputs = {} as any
|
||||
const mockCore = jest.genMockFromModule('@actions/core') as any
|
||||
mockCore.getInput = (name: string) => {
|
||||
return inputs[name]
|
||||
}
|
||||
|
||||
// Mock @actions/github
|
||||
const mockGitHub = jest.genMockFromModule('@actions/github') as any
|
||||
mockGitHub.context = {
|
||||
repo: {
|
||||
owner: 'some-owner',
|
||||
repo: 'some-repo'
|
||||
},
|
||||
ref: 'refs/heads/some-ref',
|
||||
sha: '1234567890123456789012345678901234567890'
|
||||
}
|
||||
|
||||
// Mock ./fs-helper
|
||||
const mockFSHelper = jest.genMockFromModule('../lib/fs-helper') as any
|
||||
mockFSHelper.directoryExistsSync = (path: string) => path == gitHubWorkspace
|
||||
|
||||
describe('input-helper tests', () => {
|
||||
beforeAll(() => {
|
||||
// GitHub workspace
|
||||
process.env['GITHUB_WORKSPACE'] = gitHubWorkspace
|
||||
|
||||
// Mocks
|
||||
jest.setMock('@actions/core', mockCore)
|
||||
jest.setMock('@actions/github', mockGitHub)
|
||||
jest.setMock('../lib/fs-helper', mockFSHelper)
|
||||
|
||||
// Now import
|
||||
inputHelper = require('../lib/input-helper')
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset inputs
|
||||
inputs = {}
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
// Reset GitHub workspace
|
||||
delete process.env['GITHUB_WORKSPACE']
|
||||
if (originalGitHubWorkspace) {
|
||||
process.env['GITHUB_WORKSPACE'] = originalGitHubWorkspace
|
||||
}
|
||||
|
||||
// Reset modules
|
||||
jest.resetModules()
|
||||
})
|
||||
|
||||
it('sets defaults', () => {
|
||||
const settings: ISourceSettings = inputHelper.getInputs()
|
||||
expect(settings).toBeTruthy()
|
||||
expect(settings.accessToken).toBeFalsy()
|
||||
expect(settings.clean).toBe(true)
|
||||
expect(settings.commit).toBeTruthy()
|
||||
expect(settings.commit).toBe('1234567890123456789012345678901234567890')
|
||||
expect(settings.fetchDepth).toBe(1)
|
||||
expect(settings.lfs).toBe(false)
|
||||
expect(settings.ref).toBe('refs/heads/some-ref')
|
||||
expect(settings.repositoryName).toBe('some-repo')
|
||||
expect(settings.repositoryOwner).toBe('some-owner')
|
||||
expect(settings.repositoryPath).toBe(gitHubWorkspace)
|
||||
})
|
||||
|
||||
it('requires qualified repo', () => {
|
||||
inputs.repository = 'some-unqualified-repo'
|
||||
assert.throws(() => {
|
||||
inputHelper.getInputs()
|
||||
}, /Invalid repository 'some-unqualified-repo'/)
|
||||
})
|
||||
|
||||
it('roots path', () => {
|
||||
inputs.path = 'some-directory/some-subdirectory'
|
||||
const settings: ISourceSettings = inputHelper.getInputs()
|
||||
expect(settings.repositoryPath).toBe(
|
||||
path.join(gitHubWorkspace, 'some-directory', 'some-subdirectory')
|
||||
)
|
||||
})
|
||||
|
||||
it('sets correct default ref/sha for other repo', () => {
|
||||
inputs.repository = 'some-owner/some-other-repo'
|
||||
const settings: ISourceSettings = 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: ISourceSettings = inputHelper.getInputs()
|
||||
expect(settings.ref).toBeFalsy()
|
||||
expect(settings.commit).toBe('1111111111222222222233333333334444444444')
|
||||
})
|
||||
|
||||
it('sets sha to empty when explicit ref', () => {
|
||||
inputs.ref = 'refs/heads/some-other-ref'
|
||||
const settings: ISourceSettings = inputHelper.getInputs()
|
||||
expect(settings.ref).toBe('refs/heads/some-other-ref')
|
||||
expect(settings.commit).toBeFalsy()
|
||||
})
|
||||
|
||||
it('gives good error message for submodules input', () => {
|
||||
inputs.submodules = 'true'
|
||||
assert.throws(() => {
|
||||
inputHelper.getInputs()
|
||||
}, /The input 'submodules' is not supported/)
|
||||
})
|
||||
})
|
||||
10
__test__/modify-work-tree.sh
Executable file
10
__test__/modify-work-tree.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! -f "./basic/basic-file.txt" ]; then
|
||||
echo "Expected basic file does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo hello >> ./basic/basic-file.txt
|
||||
echo hello >> ./basic/new-file.txt
|
||||
git -C ./basic status
|
||||
168
__test__/ref-helper.test.ts
Normal file
168
__test__/ref-helper.test.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import * as assert from 'assert'
|
||||
import * as refHelper from '../lib/ref-helper'
|
||||
import {IGitCommandManager} from '../lib/git-command-manager'
|
||||
|
||||
const commit = '1234567890123456789012345678901234567890'
|
||||
let git: IGitCommandManager
|
||||
|
||||
describe('ref-helper tests', () => {
|
||||
beforeEach(() => {
|
||||
git = ({} as unknown) as IGitCommandManager
|
||||
})
|
||||
|
||||
it('getCheckoutInfo requires git', async () => {
|
||||
const git = (null as unknown) as IGitCommandManager
|
||||
try {
|
||||
await refHelper.getCheckoutInfo(git, 'refs/heads/my/branch', commit)
|
||||
throw new Error('Should not reach here')
|
||||
} catch (err) {
|
||||
expect(err.message).toBe('Arg git cannot be empty')
|
||||
}
|
||||
})
|
||||
|
||||
it('getCheckoutInfo requires ref or commit', async () => {
|
||||
try {
|
||||
await refHelper.getCheckoutInfo(git, '', '')
|
||||
throw new Error('Should not reach here')
|
||||
} catch (err) {
|
||||
expect(err.message).toBe('Args ref and commit cannot both be empty')
|
||||
}
|
||||
})
|
||||
|
||||
it('getCheckoutInfo sha only', async () => {
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(git, '', commit)
|
||||
expect(checkoutInfo.ref).toBe(commit)
|
||||
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||
})
|
||||
|
||||
it('getCheckoutInfo refs/heads/', async () => {
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(
|
||||
git,
|
||||
'refs/heads/my/branch',
|
||||
commit
|
||||
)
|
||||
expect(checkoutInfo.ref).toBe('my/branch')
|
||||
expect(checkoutInfo.startPoint).toBe('refs/remotes/origin/my/branch')
|
||||
})
|
||||
|
||||
it('getCheckoutInfo refs/pull/', async () => {
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(
|
||||
git,
|
||||
'refs/pull/123/merge',
|
||||
commit
|
||||
)
|
||||
expect(checkoutInfo.ref).toBe('refs/remotes/pull/123/merge')
|
||||
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||
})
|
||||
|
||||
it('getCheckoutInfo refs/tags/', async () => {
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(
|
||||
git,
|
||||
'refs/tags/my-tag',
|
||||
commit
|
||||
)
|
||||
expect(checkoutInfo.ref).toBe('refs/tags/my-tag')
|
||||
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||
})
|
||||
|
||||
it('getCheckoutInfo unqualified branch only', async () => {
|
||||
git.branchExists = jest.fn(async (remote: boolean, pattern: string) => {
|
||||
return true
|
||||
})
|
||||
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(git, 'my/branch', '')
|
||||
|
||||
expect(checkoutInfo.ref).toBe('my/branch')
|
||||
expect(checkoutInfo.startPoint).toBe('refs/remotes/origin/my/branch')
|
||||
})
|
||||
|
||||
it('getCheckoutInfo unqualified tag only', async () => {
|
||||
git.branchExists = jest.fn(async (remote: boolean, pattern: string) => {
|
||||
return false
|
||||
})
|
||||
git.tagExists = jest.fn(async (pattern: string) => {
|
||||
return true
|
||||
})
|
||||
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(git, 'my-tag', '')
|
||||
|
||||
expect(checkoutInfo.ref).toBe('refs/tags/my-tag')
|
||||
expect(checkoutInfo.startPoint).toBeFalsy()
|
||||
})
|
||||
|
||||
it('getCheckoutInfo unqualified ref only, not a branch or tag', async () => {
|
||||
git.branchExists = jest.fn(async (remote: boolean, pattern: string) => {
|
||||
return false
|
||||
})
|
||||
git.tagExists = jest.fn(async (pattern: string) => {
|
||||
return false
|
||||
})
|
||||
|
||||
try {
|
||||
await refHelper.getCheckoutInfo(git, 'my-ref', '')
|
||||
throw new Error('Should not reach here')
|
||||
} catch (err) {
|
||||
expect(err.message).toBe(
|
||||
"A branch or tag with the name 'my-ref' could not be found"
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('getRefSpec requires ref or commit', async () => {
|
||||
assert.throws(
|
||||
() => refHelper.getRefSpec('', ''),
|
||||
/Args ref and commit cannot both be empty/
|
||||
)
|
||||
})
|
||||
|
||||
it('getRefSpec sha + refs/heads/', async () => {
|
||||
const refSpec = refHelper.getRefSpec('refs/heads/my/branch', commit)
|
||||
expect(refSpec.length).toBe(1)
|
||||
expect(refSpec[0]).toBe(`+${commit}:refs/remotes/origin/my/branch`)
|
||||
})
|
||||
|
||||
it('getRefSpec sha + refs/pull/', async () => {
|
||||
const refSpec = refHelper.getRefSpec('refs/pull/123/merge', commit)
|
||||
expect(refSpec.length).toBe(1)
|
||||
expect(refSpec[0]).toBe(`+${commit}:refs/remotes/pull/123/merge`)
|
||||
})
|
||||
|
||||
it('getRefSpec sha + refs/tags/', async () => {
|
||||
const refSpec = refHelper.getRefSpec('refs/tags/my-tag', commit)
|
||||
expect(refSpec.length).toBe(1)
|
||||
expect(refSpec[0]).toBe(`+${commit}:refs/tags/my-tag`)
|
||||
})
|
||||
|
||||
it('getRefSpec sha only', async () => {
|
||||
const refSpec = refHelper.getRefSpec('', commit)
|
||||
expect(refSpec.length).toBe(1)
|
||||
expect(refSpec[0]).toBe(commit)
|
||||
})
|
||||
|
||||
it('getRefSpec unqualified ref only', async () => {
|
||||
const refSpec = refHelper.getRefSpec('my-ref', '')
|
||||
expect(refSpec.length).toBe(2)
|
||||
expect(refSpec[0]).toBe('+refs/heads/my-ref*:refs/remotes/origin/my-ref*')
|
||||
expect(refSpec[1]).toBe('+refs/tags/my-ref*:refs/tags/my-ref*')
|
||||
})
|
||||
|
||||
it('getRefSpec refs/heads/ only', async () => {
|
||||
const refSpec = refHelper.getRefSpec('refs/heads/my/branch', '')
|
||||
expect(refSpec.length).toBe(1)
|
||||
expect(refSpec[0]).toBe(
|
||||
'+refs/heads/my/branch:refs/remotes/origin/my/branch'
|
||||
)
|
||||
})
|
||||
|
||||
it('getRefSpec refs/pull/ only', async () => {
|
||||
const refSpec = refHelper.getRefSpec('refs/pull/123/merge', '')
|
||||
expect(refSpec.length).toBe(1)
|
||||
expect(refSpec[0]).toBe('+refs/pull/123/merge:refs/remotes/pull/123/merge')
|
||||
})
|
||||
|
||||
it('getRefSpec refs/tags/ only', async () => {
|
||||
const refSpec = refHelper.getRefSpec('refs/tags/my-tag', '')
|
||||
expect(refSpec.length).toBe(1)
|
||||
expect(refSpec[0]).toBe('+refs/tags/my-tag:refs/tags/my-tag')
|
||||
})
|
||||
})
|
||||
10
__test__/verify-basic.sh
Executable file
10
__test__/verify-basic.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! -f "./basic/basic-file.txt" ]; then
|
||||
echo "Expected basic file does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify auth token
|
||||
cd basic
|
||||
git fetch
|
||||
13
__test__/verify-clean.sh
Executable file
13
__test__/verify-clean.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$(git -C ./basic status --porcelain)" != "" ]]; then
|
||||
echo ----------------------------------------
|
||||
echo git status
|
||||
echo ----------------------------------------
|
||||
git status
|
||||
echo ----------------------------------------
|
||||
echo git diff
|
||||
echo ----------------------------------------
|
||||
git diff
|
||||
exit 1
|
||||
fi
|
||||
11
__test__/verify-lfs.sh
Executable file
11
__test__/verify-lfs.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! -f "./lfs/regular-file.txt" ]; then
|
||||
echo "Expected regular file does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "./lfs/lfs-file.bin" ]; then
|
||||
echo "Expected lfs file does not exist"
|
||||
exit 1
|
||||
fi
|
||||
17
__test__/verify-no-unstaged-changes.sh
Executable file
17
__test__/verify-no-unstaged-changes.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$(git status --porcelain)" != "" ]]; then
|
||||
echo ----------------------------------------
|
||||
echo git status
|
||||
echo ----------------------------------------
|
||||
git status
|
||||
echo ----------------------------------------
|
||||
echo git diff
|
||||
echo ----------------------------------------
|
||||
git diff
|
||||
echo ----------------------------------------
|
||||
echo Troubleshooting
|
||||
echo ----------------------------------------
|
||||
echo "::error::Unstaged changes detected. Locally try running: git clean -ffdx && npm ci && npm run all"
|
||||
exit 1
|
||||
fi
|
||||
11
__test__/verify-side-by-side.sh
Executable file
11
__test__/verify-side-by-side.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! -f "./side-by-side-1/side-by-side-test-file-1.txt" ]; then
|
||||
echo "Expected file 1 does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "./side-by-side-2/side-by-side-test-file-2.txt" ]; then
|
||||
echo "Expected file 2 does not exist"
|
||||
exit 1
|
||||
fi
|
||||
11
__test__/verify-submodules-not-checked-out.sh
Executable file
11
__test__/verify-submodules-not-checked-out.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ ! -f "./submodules-not-checked-out/regular-file.txt" ]; then
|
||||
echo "Expected regular file does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "./submodules-not-checked-out/submodule-level-1/submodule-file.txt" ]; then
|
||||
echo "Unexpected submodule file exists"
|
||||
exit 1
|
||||
fi
|
||||
35
action.yml
35
action.yml
@@ -1,23 +1,28 @@
|
||||
name: 'Checkout'
|
||||
description: 'Checkout a Git repository.'
|
||||
description: 'Checkout a Git repository'
|
||||
inputs:
|
||||
repository:
|
||||
description: 'Repository name'
|
||||
description: 'Repository name with owner. For example, actions/checkout'
|
||||
default: ${{ github.repository }}
|
||||
ref:
|
||||
description: 'Ref to checkout (SHA, branch, tag)'
|
||||
description: >
|
||||
Ref to checkout (SHA, branch, tag). For the repository that triggered the
|
||||
workflow, defaults to the ref/SHA for the event. Otherwise defaults to master.
|
||||
token:
|
||||
description: 'Access token for clone repository'
|
||||
clean:
|
||||
description: 'If true, execute `execute git clean -ffdx && git reset --hard HEAD` before fetching'
|
||||
default: true
|
||||
submodules:
|
||||
description: 'Whether to include submodules: false to exclude submodules, true to include only one level of submodules, or recursive to recursively clone submodules; defaults to false'
|
||||
lfs:
|
||||
description: 'Whether to download Git-LFS files; defaults to false'
|
||||
fetch-depth:
|
||||
description: 'The depth of commits to ask Git to fetch; defaults to no limit'
|
||||
default: ${{ github.token }}
|
||||
path:
|
||||
description: 'Optional path to check out source code'
|
||||
description: 'Relative path under $GITHUB_WORKSPACE to place the repository'
|
||||
clean:
|
||||
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.'
|
||||
default: 1
|
||||
lfs:
|
||||
description: 'Whether to download Git-LFS files'
|
||||
default: false
|
||||
runs:
|
||||
# Plugins live on the runner and are only available to a certain set of first party actions.
|
||||
plugin: 'checkoutV1_1'
|
||||
using: node12
|
||||
main: dist/index.js
|
||||
post: dist/index.js
|
||||
13338
dist/index.js
vendored
Normal file
13338
dist/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
13
dist/problem-matcher.json
vendored
Normal file
13
dist/problem-matcher.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "checkout-git",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(fatal|error): (.*)$",
|
||||
"message": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
11
jest.config.js
Normal file
11
jest.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
clearMocks: true,
|
||||
moduleFileExtensions: ['js', 'ts'],
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.ts'],
|
||||
testRunner: 'jest-circus/runner',
|
||||
transform: {
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
},
|
||||
verbose: true
|
||||
}
|
||||
7025
package-lock.json
generated
Normal file
7025
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
package.json
Normal file
52
package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "checkout",
|
||||
"version": "2.0.0",
|
||||
"description": "checkout action",
|
||||
"main": "lib/main.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"format": "prettier --write **/*.ts",
|
||||
"format-check": "prettier --check **/*.ts",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"pack": "ncc build",
|
||||
"gendocs": "node lib/misc/generate-docs.js",
|
||||
"test": "jest",
|
||||
"all": "npm run build && npm run format && npm run lint && npm run pack && npm run gendocs && npm test"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/actions/checkout.git"
|
||||
},
|
||||
"keywords": [
|
||||
"github",
|
||||
"actions",
|
||||
"checkout"
|
||||
],
|
||||
"author": "GitHub",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/actions/checkout/issues"
|
||||
},
|
||||
"homepage": "https://github.com/actions/checkout#readme",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.1.3",
|
||||
"@actions/exec": "^1.0.1",
|
||||
"@actions/github": "^1.1.0",
|
||||
"@actions/io": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^24.0.23",
|
||||
"@types/node": "^12.7.12",
|
||||
"@typescript-eslint/parser": "^2.8.0",
|
||||
"@zeit/ncc": "^0.20.5",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-github": "^2.0.0",
|
||||
"eslint-plugin-jest": "^22.21.0",
|
||||
"jest": "^24.9.0",
|
||||
"jest-circus": "^24.9.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"prettier": "^1.19.1",
|
||||
"ts-jest": "^24.2.0",
|
||||
"typescript": "^3.6.4"
|
||||
}
|
||||
}
|
||||
77
src/fs-helper.ts
Normal file
77
src/fs-helper.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import * as fs from 'fs'
|
||||
|
||||
export function directoryExistsSync(path: string, required?: boolean): boolean {
|
||||
if (!path) {
|
||||
throw new Error("Arg 'path' must not be empty")
|
||||
}
|
||||
|
||||
let stats: fs.Stats
|
||||
try {
|
||||
stats = fs.statSync(path)
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
if (!required) {
|
||||
return false
|
||||
}
|
||||
|
||||
throw new Error(`Directory '${path}' does not exist`)
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
|
||||
)
|
||||
}
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
return true
|
||||
} else if (!required) {
|
||||
return false
|
||||
}
|
||||
|
||||
throw new Error(`Directory '${path}' does not exist`)
|
||||
}
|
||||
|
||||
export function existsSync(path: string): boolean {
|
||||
if (!path) {
|
||||
throw new Error("Arg 'path' must not be empty")
|
||||
}
|
||||
|
||||
try {
|
||||
fs.statSync(path)
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return false
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function fileExistsSync(path: string): boolean {
|
||||
if (!path) {
|
||||
throw new Error("Arg 'path' must not be empty")
|
||||
}
|
||||
|
||||
let stats: fs.Stats
|
||||
try {
|
||||
stats = fs.statSync(path)
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return false
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Encountered an error when checking whether path '${path}' exists: ${error.message}`
|
||||
)
|
||||
}
|
||||
|
||||
if (!stats.isDirectory()) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
399
src/git-command-manager.ts
Normal file
399
src/git-command-manager.ts
Normal file
File diff suppressed because it is too large
Load Diff
250
src/git-source-provider.ts
Normal file
250
src/git-source-provider.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as coreCommand from '@actions/core/lib/command'
|
||||
import * as fs from 'fs'
|
||||
import * as fsHelper from './fs-helper'
|
||||
import * as gitCommandManager from './git-command-manager'
|
||||
import * as io from '@actions/io'
|
||||
import * as path from 'path'
|
||||
import * as refHelper from './ref-helper'
|
||||
import {IGitCommandManager} from './git-command-manager'
|
||||
|
||||
const authConfigKey = `http.https://github.com/.extraheader`
|
||||
|
||||
export interface ISourceSettings {
|
||||
repositoryPath: string
|
||||
repositoryOwner: string
|
||||
repositoryName: string
|
||||
ref: string
|
||||
commit: string
|
||||
clean: boolean
|
||||
fetchDepth: number
|
||||
lfs: boolean
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
export async function getSource(settings: ISourceSettings): Promise<void> {
|
||||
core.info(
|
||||
`Syncing repository: ${settings.repositoryOwner}/${settings.repositoryName}`
|
||||
)
|
||||
const repositoryUrl = `https://github.com/${encodeURIComponent(
|
||||
settings.repositoryOwner
|
||||
)}/${encodeURIComponent(settings.repositoryName)}`
|
||||
|
||||
// Remove conflicting file path
|
||||
if (fsHelper.fileExistsSync(settings.repositoryPath)) {
|
||||
await io.rmRF(settings.repositoryPath)
|
||||
}
|
||||
|
||||
// Create directory
|
||||
let isExisting = true
|
||||
if (!fsHelper.directoryExistsSync(settings.repositoryPath)) {
|
||||
isExisting = false
|
||||
await io.mkdirP(settings.repositoryPath)
|
||||
}
|
||||
|
||||
// Git command manager
|
||||
core.info(`Working directory is '${settings.repositoryPath}'`)
|
||||
const git = await gitCommandManager.CreateCommandManager(
|
||||
settings.repositoryPath,
|
||||
settings.lfs
|
||||
)
|
||||
|
||||
// Try prepare existing directory, otherwise recreate
|
||||
if (
|
||||
isExisting &&
|
||||
!(await tryPrepareExistingDirectory(
|
||||
git,
|
||||
settings.repositoryPath,
|
||||
repositoryUrl,
|
||||
settings.clean
|
||||
))
|
||||
) {
|
||||
// Delete the contents of the directory. Don't delete the directory itself
|
||||
// since it may be the current working directory.
|
||||
core.info(`Deleting the contents of '${settings.repositoryPath}'`)
|
||||
for (const file of await fs.promises.readdir(settings.repositoryPath)) {
|
||||
await io.rmRF(path.join(settings.repositoryPath, file))
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the repository
|
||||
if (
|
||||
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
||||
) {
|
||||
await git.init()
|
||||
await git.remoteAdd('origin', repositoryUrl)
|
||||
}
|
||||
|
||||
// Disable automatic garbage collection
|
||||
if (!(await git.tryDisableAutomaticGarbageCollection())) {
|
||||
core.warning(
|
||||
`Unable to turn off git automatic garbage collection. The git fetch operation may trigger garbage collection and cause a delay.`
|
||||
)
|
||||
}
|
||||
|
||||
// Remove possible previous extraheader
|
||||
await removeGitConfig(git, authConfigKey)
|
||||
|
||||
// Add extraheader (auth)
|
||||
const base64Credentials = Buffer.from(
|
||||
`x-access-token:${settings.accessToken}`,
|
||||
'utf8'
|
||||
).toString('base64')
|
||||
core.setSecret(base64Credentials)
|
||||
const authConfigValue = `AUTHORIZATION: basic ${base64Credentials}`
|
||||
await git.config(authConfigKey, authConfigValue)
|
||||
|
||||
// LFS install
|
||||
if (settings.lfs) {
|
||||
await git.lfsInstall()
|
||||
}
|
||||
|
||||
// Fetch
|
||||
const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
|
||||
await git.fetch(settings.fetchDepth, refSpec)
|
||||
|
||||
// Checkout info
|
||||
const checkoutInfo = await refHelper.getCheckoutInfo(
|
||||
git,
|
||||
settings.ref,
|
||||
settings.commit
|
||||
)
|
||||
|
||||
// LFS fetch
|
||||
// Explicit lfs-fetch to avoid slow checkout (fetches one lfs object at a time).
|
||||
// Explicit lfs fetch will fetch lfs objects in parallel.
|
||||
if (settings.lfs) {
|
||||
await git.lfsFetch(checkoutInfo.startPoint || checkoutInfo.ref)
|
||||
}
|
||||
|
||||
// Checkout
|
||||
await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
|
||||
|
||||
// Dump some info about the checked out commit
|
||||
await git.log1()
|
||||
|
||||
// Set intra-task state for cleanup
|
||||
coreCommand.issueCommand(
|
||||
'save-state',
|
||||
{name: 'repositoryPath'},
|
||||
settings.repositoryPath
|
||||
)
|
||||
}
|
||||
|
||||
export async function cleanup(repositoryPath: string): Promise<void> {
|
||||
// Repo exists?
|
||||
if (!fsHelper.fileExistsSync(path.join(repositoryPath, '.git', 'config'))) {
|
||||
return
|
||||
}
|
||||
fsHelper.directoryExistsSync(repositoryPath, true)
|
||||
|
||||
// Remove the config key
|
||||
const git = await gitCommandManager.CreateCommandManager(
|
||||
repositoryPath,
|
||||
false
|
||||
)
|
||||
await removeGitConfig(git, authConfigKey)
|
||||
}
|
||||
|
||||
async function tryPrepareExistingDirectory(
|
||||
git: IGitCommandManager,
|
||||
repositoryPath: string,
|
||||
repositoryUrl: string,
|
||||
clean: boolean
|
||||
): Promise<boolean> {
|
||||
// Fetch URL does not match
|
||||
if (
|
||||
!fsHelper.directoryExistsSync(path.join(repositoryPath, '.git')) ||
|
||||
repositoryUrl !== (await git.tryGetFetchUrl())
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Delete any index.lock and shallow.lock left by a previously canceled run or crashed git process
|
||||
const lockPaths = [
|
||||
path.join(repositoryPath, '.git', 'index.lock'),
|
||||
path.join(repositoryPath, '.git', 'shallow.lock')
|
||||
]
|
||||
for (const lockPath of lockPaths) {
|
||||
try {
|
||||
await io.rmRF(lockPath)
|
||||
} catch (error) {
|
||||
core.debug(`Unable to delete '${lockPath}'. ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Checkout detached HEAD
|
||||
if (!(await git.isDetached())) {
|
||||
await git.checkoutDetach()
|
||||
}
|
||||
|
||||
// Remove all refs/heads/*
|
||||
let branches = await git.branchList(false)
|
||||
for (const branch of branches) {
|
||||
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)
|
||||
}
|
||||
} catch (error) {
|
||||
core.warning(
|
||||
`Unable to prepare the existing repository. The repository will be recreated instead.`
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
// Clean
|
||||
if (clean) {
|
||||
let succeeded = true
|
||||
if (!(await git.tryClean())) {
|
||||
core.debug(
|
||||
`The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For futher investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.`
|
||||
)
|
||||
succeeded = false
|
||||
} else if (!(await git.tryReset())) {
|
||||
succeeded = false
|
||||
}
|
||||
|
||||
if (!succeeded) {
|
||||
core.warning(
|
||||
`Unable to clean or reset the repository. The repository will be recreated instead.`
|
||||
)
|
||||
}
|
||||
|
||||
return succeeded
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async function removeGitConfig(
|
||||
git: IGitCommandManager,
|
||||
configKey: string
|
||||
): Promise<void> {
|
||||
if (
|
||||
(await git.configExists(configKey)) &&
|
||||
!(await git.tryConfigUnset(configKey))
|
||||
) {
|
||||
// Load the config contents
|
||||
core.warning(
|
||||
`Failed to remove '${configKey}' from the git config. Attempting to remove the config value by editing the file directly.`
|
||||
)
|
||||
const configPath = path.join(git.getWorkingDirectory(), '.git', 'config')
|
||||
fsHelper.fileExistsSync(configPath)
|
||||
let contents = fs.readFileSync(configPath).toString() || ''
|
||||
|
||||
// Filter - only includes lines that do not contain the config key
|
||||
const upperConfigKey = configKey.toUpperCase()
|
||||
const split = contents
|
||||
.split('\n')
|
||||
.filter(x => !x.toUpperCase().includes(upperConfigKey))
|
||||
contents = split.join('\n')
|
||||
|
||||
// Rewrite the config file
|
||||
fs.writeFileSync(configPath, contents)
|
||||
}
|
||||
}
|
||||
77
src/git-version.ts
Normal file
77
src/git-version.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
export class GitVersion {
|
||||
private readonly major: number = NaN
|
||||
private readonly minor: number = NaN
|
||||
private readonly patch: number = NaN
|
||||
|
||||
/**
|
||||
* Used for comparing the version of git and git-lfs against the minimum required version
|
||||
* @param version the version string, e.g. 1.2 or 1.2.3
|
||||
*/
|
||||
constructor(version?: string) {
|
||||
if (version) {
|
||||
const match = version.match(/^(\d+)\.(\d+)(\.(\d+))?$/)
|
||||
if (match) {
|
||||
this.major = Number(match[1])
|
||||
this.minor = Number(match[2])
|
||||
if (match[4]) {
|
||||
this.patch = Number(match[4])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the instance against a minimum required version
|
||||
* @param minimum Minimum version
|
||||
*/
|
||||
checkMinimum(minimum: GitVersion): boolean {
|
||||
if (!minimum.isValid()) {
|
||||
throw new Error('Arg minimum is not a valid version')
|
||||
}
|
||||
|
||||
// Major is insufficient
|
||||
if (this.major < minimum.major) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Major is equal
|
||||
if (this.major === minimum.major) {
|
||||
// Minor is insufficient
|
||||
if (this.minor < minimum.minor) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Minor is equal
|
||||
if (this.minor === minimum.minor) {
|
||||
// Patch is insufficient
|
||||
if (this.patch && this.patch < (minimum.patch || 0)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the instance was constructed from a valid version string
|
||||
*/
|
||||
isValid(): boolean {
|
||||
return !isNaN(this.major)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version as a string, e.g. 1.2 or 1.2.3
|
||||
*/
|
||||
toString(): string {
|
||||
let result = ''
|
||||
if (this.isValid()) {
|
||||
result = `${this.major}.${this.minor}`
|
||||
if (!isNaN(this.patch)) {
|
||||
result += `.${this.patch}`
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
104
src/input-helper.ts
Normal file
104
src/input-helper.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as fsHelper from './fs-helper'
|
||||
import * as github from '@actions/github'
|
||||
import * as path from 'path'
|
||||
import {ISourceSettings} from './git-source-provider'
|
||||
|
||||
export function getInputs(): ISourceSettings {
|
||||
const result = ({} as unknown) as ISourceSettings
|
||||
|
||||
// GitHub workspace
|
||||
let githubWorkspacePath = process.env['GITHUB_WORKSPACE']
|
||||
if (!githubWorkspacePath) {
|
||||
throw new Error('GITHUB_WORKSPACE not defined')
|
||||
}
|
||||
githubWorkspacePath = path.resolve(githubWorkspacePath)
|
||||
core.debug(`GITHUB_WORKSPACE = '${githubWorkspacePath}'`)
|
||||
fsHelper.directoryExistsSync(githubWorkspacePath, true)
|
||||
|
||||
// Qualified repository
|
||||
const qualifiedRepository =
|
||||
core.getInput('repository') ||
|
||||
`${github.context.repo.owner}/${github.context.repo.repo}`
|
||||
core.debug(`qualified repository = '${qualifiedRepository}'`)
|
||||
const splitRepository = qualifiedRepository.split('/')
|
||||
if (
|
||||
splitRepository.length !== 2 ||
|
||||
!splitRepository[0] ||
|
||||
!splitRepository[1]
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid repository '${qualifiedRepository}'. Expected format {owner}/{repo}.`
|
||||
)
|
||||
}
|
||||
result.repositoryOwner = splitRepository[0]
|
||||
result.repositoryName = splitRepository[1]
|
||||
|
||||
// Repository path
|
||||
result.repositoryPath = core.getInput('path') || '.'
|
||||
result.repositoryPath = path.resolve(
|
||||
githubWorkspacePath,
|
||||
result.repositoryPath
|
||||
)
|
||||
if (
|
||||
!(result.repositoryPath + path.sep).startsWith(
|
||||
githubWorkspacePath + path.sep
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`Repository path '${result.repositoryPath}' is not under '${githubWorkspacePath}'`
|
||||
)
|
||||
}
|
||||
|
||||
// Workflow repository?
|
||||
const isWorkflowRepository =
|
||||
qualifiedRepository.toUpperCase() ===
|
||||
`${github.context.repo.owner}/${github.context.repo.repo}`.toUpperCase()
|
||||
|
||||
// Source branch, source version
|
||||
result.ref = core.getInput('ref')
|
||||
if (!result.ref) {
|
||||
if (isWorkflowRepository) {
|
||||
result.ref = github.context.ref
|
||||
result.commit = github.context.sha
|
||||
}
|
||||
|
||||
if (!result.ref && !result.commit) {
|
||||
result.ref = 'refs/heads/master'
|
||||
}
|
||||
}
|
||||
// SHA?
|
||||
else if (result.ref.match(/^[0-9a-fA-F]{40}$/)) {
|
||||
result.commit = result.ref
|
||||
result.ref = ''
|
||||
}
|
||||
core.debug(`ref = '${result.ref}'`)
|
||||
core.debug(`commit = '${result.commit}'`)
|
||||
|
||||
// Clean
|
||||
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'
|
||||
core.debug(`clean = ${result.clean}`)
|
||||
|
||||
// Submodules
|
||||
if (core.getInput('submodules')) {
|
||||
throw new Error(
|
||||
"The input 'submodules' is not supported in actions/checkout@v2"
|
||||
)
|
||||
}
|
||||
|
||||
// Fetch depth
|
||||
result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1'))
|
||||
if (isNaN(result.fetchDepth) || result.fetchDepth < 0) {
|
||||
result.fetchDepth = 0
|
||||
}
|
||||
core.debug(`fetch depth = ${result.fetchDepth}`)
|
||||
|
||||
// LFS
|
||||
result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE'
|
||||
core.debug(`lfs = ${result.lfs}`)
|
||||
|
||||
// Access token
|
||||
result.accessToken = core.getInput('token')
|
||||
|
||||
return result
|
||||
}
|
||||
47
src/main.ts
Normal file
47
src/main.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as core from '@actions/core'
|
||||
import * as coreCommand from '@actions/core/lib/command'
|
||||
import * as gitSourceProvider from './git-source-provider'
|
||||
import * as inputHelper from './input-helper'
|
||||
import * as path from 'path'
|
||||
|
||||
const cleanupRepositoryPath = process.env['STATE_repositoryPath'] as string
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
const sourceSettings = inputHelper.getInputs()
|
||||
|
||||
try {
|
||||
// Register problem matcher
|
||||
coreCommand.issueCommand(
|
||||
'add-matcher',
|
||||
{},
|
||||
path.join(__dirname, 'problem-matcher.json')
|
||||
)
|
||||
|
||||
// Get sources
|
||||
await gitSourceProvider.getSource(sourceSettings)
|
||||
} finally {
|
||||
// Unregister problem matcher
|
||||
coreCommand.issueCommand('remove-matcher', {owner: 'checkout-git'}, '')
|
||||
}
|
||||
} catch (error) {
|
||||
core.setFailed(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanup(): Promise<void> {
|
||||
try {
|
||||
await gitSourceProvider.cleanup(cleanupRepositoryPath)
|
||||
} catch (error) {
|
||||
core.warning(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// Main
|
||||
if (!cleanupRepositoryPath) {
|
||||
run()
|
||||
}
|
||||
// Post
|
||||
else {
|
||||
cleanup()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user