20 changes: 0 additions & 20 deletions .licenses/npm/@octokit/plugin-request-log.dep.yml

This file was deleted.

4 changes: 2 additions & 2 deletions .licenses/npm/@octokit/plugin-rest-endpoint-methods.dep.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions .licenses/npm/@octokit/request.dep.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 0 additions & 35 deletions .licenses/npm/@octokit/rest.dep.yml

This file was deleted.

4 changes: 2 additions & 2 deletions .licenses/npm/@octokit/types.dep.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 0 additions & 26 deletions .licenses/npm/@types/node.dep.yml

This file was deleted.

32 changes: 0 additions & 32 deletions .licenses/npm/atob-lite.dep.yml

This file was deleted.

4 changes: 2 additions & 2 deletions .licenses/npm/before-after-hook.dep.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 0 additions & 32 deletions .licenses/npm/btoa-lite.dep.yml

This file was deleted.

34 changes: 0 additions & 34 deletions .licenses/npm/cross-spawn.dep.yml

This file was deleted.

34 changes: 0 additions & 34 deletions .licenses/npm/end-of-stream.dep.yml

This file was deleted.

22 changes: 0 additions & 22 deletions .licenses/npm/get-stream.dep.yml

This file was deleted.

4 changes: 2 additions & 2 deletions .licenses/npm/is-plain-object.dep.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 0 additions & 34 deletions .licenses/npm/is-stream.dep.yml

This file was deleted.

26 changes: 0 additions & 26 deletions .licenses/npm/isexe.dep.yml

This file was deleted.

40 changes: 0 additions & 40 deletions .licenses/npm/isobject.dep.yml

This file was deleted.

58 changes: 0 additions & 58 deletions .licenses/npm/lodash.get.dep.yml

This file was deleted.

58 changes: 0 additions & 58 deletions .licenses/npm/lodash.set.dep.yml

This file was deleted.

58 changes: 0 additions & 58 deletions .licenses/npm/lodash.uniq.dep.yml

This file was deleted.

22 changes: 0 additions & 22 deletions .licenses/npm/macos-release.dep.yml

This file was deleted.

32 changes: 0 additions & 32 deletions .licenses/npm/nice-try.dep.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .licenses/npm/node-fetch.dep.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 0 additions & 34 deletions .licenses/npm/npm-run-path.dep.yml

This file was deleted.

35 changes: 0 additions & 35 deletions .licenses/npm/octokit-pagination-methods.dep.yml

This file was deleted.

22 changes: 0 additions & 22 deletions .licenses/npm/os-name.dep.yml

This file was deleted.

35 changes: 0 additions & 35 deletions .licenses/npm/p-finally.dep.yml

This file was deleted.

34 changes: 0 additions & 34 deletions .licenses/npm/path-key.dep.yml

This file was deleted.

34 changes: 0 additions & 34 deletions .licenses/npm/pump.dep.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .licenses/npm/qs.dep.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 0 additions & 26 deletions .licenses/npm/semver-5.7.1.dep.yml

This file was deleted.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 0 additions & 34 deletions .licenses/npm/shebang-command.dep.yml

This file was deleted.

34 changes: 0 additions & 34 deletions .licenses/npm/shebang-regex.dep.yml

This file was deleted.

27 changes: 0 additions & 27 deletions .licenses/npm/signal-exit.dep.yml

This file was deleted.

34 changes: 0 additions & 34 deletions .licenses/npm/strip-eof.dep.yml

This file was deleted.

20 changes: 0 additions & 20 deletions .licenses/npm/universal-user-agent-5.0.0.dep.yml

This file was deleted.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

File renamed without changes.
16 changes: 7 additions & 9 deletions .licenses/npm/execa.dep.yml → .licenses/npm/uuid-8.3.2.dep.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 0 additions & 27 deletions .licenses/npm/which.dep.yml

This file was deleted.

52 changes: 51 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,60 @@
# Changelog

## v3.6.0
- [Fix: Mark test scripts with Bash'isms to be run via Bash](https://github.com/actions/checkout/pull/1377)
- [Add option to fetch tags even if fetch-depth > 0](https://github.com/actions/checkout/pull/579)

## v3.5.3
- [Fix: Checkout fail in self-hosted runners when faulty submodule are checked-in](https://github.com/actions/checkout/pull/1196)
- [Fix typos found by codespell](https://github.com/actions/checkout/pull/1287)
- [Add support for sparse checkouts](https://github.com/actions/checkout/pull/1369)

## v3.5.2
- [Fix api endpoint for GHES](https://github.com/actions/checkout/pull/1289)

## v3.5.1
- [Fix slow checkout on Windows](https://github.com/actions/checkout/pull/1246)

## v3.5.0
* [Add new public key for known_hosts](https://github.com/actions/checkout/pull/1237)

## v3.4.0
- [Upgrade codeql actions to v2](https://github.com/actions/checkout/pull/1209)
- [Upgrade dependencies](https://github.com/actions/checkout/pull/1210)
- [Upgrade @actions/io](https://github.com/actions/checkout/pull/1225)

## v3.3.0
- [Implement branch list using callbacks from exec function](https://github.com/actions/checkout/pull/1045)
- [Add in explicit reference to private checkout options](https://github.com/actions/checkout/pull/1050)
- [Fix comment typos (that got added in #770)](https://github.com/actions/checkout/pull/1057)

## v3.2.0
- [Add GitHub Action to perform release](https://github.com/actions/checkout/pull/942)
- [Fix status badge](https://github.com/actions/checkout/pull/967)
- [Replace datadog/squid with ubuntu/squid Docker image](https://github.com/actions/checkout/pull/1002)
- [Wrap pipeline commands for submoduleForeach in quotes](https://github.com/actions/checkout/pull/964)
- [Update @actions/io to 1.1.2](https://github.com/actions/checkout/pull/1029)
- [Upgrading version to 3.2.0](https://github.com/actions/checkout/pull/1039)

## v3.1.0
- [Use @actions/core `saveState` and `getState`](https://github.com/actions/checkout/pull/939)
- [Add `github-server-url` input](https://github.com/actions/checkout/pull/922)

## v3.0.2
- [Add input `set-safe-directory`](https://github.com/actions/checkout/pull/770)

## v3.0.1
- [Fixed an issue where checkout failed to run in container jobs due to the new git setting `safe.directory`](https://github.com/actions/checkout/pull/762)
- [Bumped various npm package versions](https://github.com/actions/checkout/pull/744)

## v3.0.0

- [Update to node 16](https://github.com/actions/checkout/pull/689)

## 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)
Expand Down
28 changes: 28 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Contributing

## Submitting a pull request

1. Fork and clone the repository
1. Configure and install the dependencies: `npm install`
1. Create a new branch: `git checkout -b my-branch-name`
1. Make your change, add tests, and make sure the tests still pass: `npm run test`
1. Make sure your code is correctly formatted: `npm run format`
1. Update `dist/index.js` using `npm run build`. This creates a single javascript file that is used as an entrypoint for the action
1. Push to your fork and submit a pull request
1. Pat yourself on the back and wait for your pull request to be reviewed and merged

Here are a few things you can do that will increase the likelihood of your pull request being accepted:

- Write tests.
- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.

## Resources

- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
- [Using Pull Requests](https://help.github.com/articles/about-pull-requests/)
- [GitHub Help](https://help.github.com)
- [Writing good commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)

Thanks! :heart: :heart: :heart:

GitHub Actions Team :octocat:
106 changes: 74 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
<p align="center">
<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>
[![Build and Test](https://github.com/actions/checkout/actions/workflows/test.yml/badge.svg)](https://github.com/actions/checkout/actions/workflows/test.yml)

# Checkout V2
# Checkout V3

This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it.

Expand All @@ -14,27 +12,14 @@ When Git 2.18 or higher is not in your PATH, falls back to the REST API to downl

# What's new

- Improved performance
- Fetches only a single commit by default
- Script authenticated git commands
- Auth token persisted in the local git config
- Supports SSH
- Creates a local branch
- No longer detached HEAD when checking out a branch
- Improved layout
- The input `path` is always relative to $GITHUB_WORKSPACE
- Aligns better with container actions, where $GITHUB_WORKSPACE gets mapped in
- Fallback to REST API download
- When Git 2.18 or higher is not in the PATH, the REST API will be used to download the files
- When using a job container, the container's PATH is used

Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous versions.
- Updated to the node16 runtime by default
- This requires a minimum [Actions Runner](https://github.com/actions/runner/releases/tag/v2.285.0) version of v2.285.0 to run, which is by default available in GHES 3.4 or later.

# Usage

<!-- start usage -->
```yaml
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
# Repository name with owner. For example, actions/checkout
# Default: ${{ github.repository }}
Expand Down Expand Up @@ -89,10 +74,23 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
# Default: true
clean: ''

# Do a sparse checkout on given patterns. Each pattern should be separated with
# new lines
# Default: null
sparse-checkout: ''

# Specifies whether to use cone-mode when doing a sparse checkout.
# Default: true
sparse-checkout-cone-mode: ''

# Number of commits to fetch. 0 indicates all history for all branches and tags.
# Default: 1
fetch-depth: ''

# Whether to fetch tags, even if fetch-depth > 0.
# Default: false
fetch-tags: ''

# Whether to download Git-LFS files
# Default: false
lfs: ''
Expand All @@ -105,11 +103,25 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
#
# Default: false
submodules: ''

# Add repository path as safe.directory for Git global config by running `git
# config --global --add safe.directory <path>`
# Default: true
set-safe-directory: ''

# The base URL for the GitHub instance that you are trying to clone from, will use
# environment defaults to fetch from the same instance that the workflow is
# running from unless specified. Example URLs are https://github.com or
# https://my-ghes-server.example.com
github-server-url: ''
```
<!-- end usage -->
# Scenarios
- [Fetch only the root files](#Fetch-only-the-root-files)
- [Fetch only the root files and `.github` and `src` folder](#Fetch-only-the-root-files-and-github-and-src-folder)
- [Fetch only a single file](#Fetch-only-a-single-file)
- [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)
Expand All @@ -120,26 +132,54 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
- [Checkout pull request on closed event](#Checkout-pull-request-on-closed-event)
- [Push a commit using the built-in token](#Push-a-commit-using-the-built-in-token)

## Fetch only the root files

```yaml
- uses: actions/checkout@v3
with:
sparse-checkout: .
```

## Fetch only the root files and `.github` and `src` folder

```yaml
- uses: actions/checkout@v3
with:
sparse-checkout: |
.github
src
```

## Fetch only a single file

```yaml
- uses: actions/checkout@v3
with:
sparse-checkout: |
README.md
sparse-checkout-cone-mode: false
```

## Fetch all history for all tags and branches

```yaml
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0
```

## Checkout a different branch

```yaml
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
ref: my-branch
```

## Checkout HEAD^

```yaml
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 2
- run: git checkout HEAD^
Expand All @@ -149,40 +189,42 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous

```yaml
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
path: main
- name: Checkout tools repo
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: my-org/my-tools
path: my-tools
```
> - If your secondary repository is private you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private)

## Checkout multiple repos (nested)

```yaml
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Checkout tools repo
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: my-org/my-tools
path: my-tools
```
> - If your secondary repository is private you will need to add the option noted in [Checkout multiple repos (private)](#Checkout-multiple-repos-private)

## Checkout multiple repos (private)

```yaml
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
path: main
- name: Checkout private tools
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: my-org/my-private-tools
token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT
Expand All @@ -195,7 +237,7 @@ Refer [here](https://github.com/actions/checkout/blob/v1/README.md) for previous
## Checkout pull request HEAD commit instead of merge commit

```yaml
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.sha }}
```
Expand All @@ -211,7 +253,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
```

## Push a commit using the built-in token
Expand All @@ -222,7 +264,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- run: |
date > generated.txt
git config user.name github-actions
Expand Down
63 changes: 53 additions & 10 deletions __test__/git-auth-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ let tempHomedir: string
let git: IGitCommandManager & {env: {[key: string]: string}}
let settings: IGitSourceSettings
let sshPath: string
let githubServerUrl: string

describe('git-auth-helper tests', () => {
beforeAll(async () => {
Expand Down Expand Up @@ -67,11 +68,18 @@ describe('git-auth-helper tests', () => {
}
})

const configureAuth_configuresAuthHeader =
'configureAuth configures auth header'
it(configureAuth_configuresAuthHeader, async () => {
async function testAuthHeader(
testName: string,
serverUrl: string | undefined = undefined
) {
// Arrange
await setup(configureAuth_configuresAuthHeader)
let expectedServerUrl = 'https://github.com'
if (serverUrl) {
githubServerUrl = serverUrl
expectedServerUrl = githubServerUrl
}

await setup(testName)
expect(settings.authToken).toBeTruthy() // sanity check
const authHelper = gitAuthHelper.createAuthHelper(git, settings)

Expand All @@ -88,9 +96,33 @@ describe('git-auth-helper tests', () => {
).toString('base64')
expect(
configContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
`http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
)
).toBeGreaterThanOrEqual(0)
}

const configureAuth_configuresAuthHeader =
'configureAuth configures auth header'
it(configureAuth_configuresAuthHeader, async () => {
await testAuthHeader(configureAuth_configuresAuthHeader)
})

const configureAuth_AcceptsGitHubServerUrl =
'inject https://my-ghes-server.com as github server url'
it(configureAuth_AcceptsGitHubServerUrl, async () => {
await testAuthHeader(
configureAuth_AcceptsGitHubServerUrl,
'https://my-ghes-server.com'
)
})

const configureAuth_AcceptsGitHubServerUrlSetToGHEC =
'inject https://github.com as github server url'
it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => {
await testAuthHeader(
configureAuth_AcceptsGitHubServerUrl,
'https://github.com'
)
})

const configureAuth_configuresAuthHeaderEvenWhenPersistCredentialsFalse =
Expand Down Expand Up @@ -643,10 +675,11 @@ describe('git-auth-helper tests', () => {
expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
})

const removeGlobalAuth_removesOverride = 'removeGlobalAuth removes override'
it(removeGlobalAuth_removesOverride, async () => {
const removeGlobalConfig_removesOverride =
'removeGlobalConfig removes override'
it(removeGlobalConfig_removesOverride, async () => {
// Arrange
await setup(removeGlobalAuth_removesOverride)
await setup(removeGlobalConfig_removesOverride)
const authHelper = gitAuthHelper.createAuthHelper(git, settings)
await authHelper.configureAuth()
await authHelper.configureGlobalAuth()
Expand All @@ -655,7 +688,7 @@ describe('git-auth-helper tests', () => {
await fs.promises.stat(path.join(git.env['HOME'], '.gitconfig'))

// Act
await authHelper.removeGlobalAuth()
await authHelper.removeGlobalConfig()

// Assert
expect(git.env['HOME']).toBeUndefined()
Expand Down Expand Up @@ -694,6 +727,8 @@ async function setup(testName: string): Promise<void> {
branchDelete: jest.fn(),
branchExists: jest.fn(),
branchList: jest.fn(),
sparseCheckout: jest.fn(),
sparseCheckoutNonConeMode: jest.fn(),
checkout: jest.fn(),
checkoutDetach: jest.fn(),
config: jest.fn(
Expand Down Expand Up @@ -737,6 +772,9 @@ async function setup(testName: string): Promise<void> {
return ''
}),
submoduleSync: jest.fn(),
submoduleStatus: jest.fn(async () => {
return true
}),
submoduleUpdate: jest.fn(),
tagExists: jest.fn(),
tryClean: jest.fn(),
Expand Down Expand Up @@ -764,7 +802,10 @@ async function setup(testName: string): Promise<void> {
authToken: 'some auth token',
clean: true,
commit: '',
sparseCheckout: [],
sparseCheckoutConeMode: true,
fetchDepth: 1,
fetchTags: false,
lfs: false,
submodules: false,
nestedSubmodules: false,
Expand All @@ -776,7 +817,9 @@ async function setup(testName: string): Promise<void> {
sshKey: sshPath ? 'some ssh private key' : '',
sshKnownHosts: '',
sshStrict: true,
workflowOrganizationId: 123456
workflowOrganizationId: 123456,
setSafeDirectory: true,
githubServerUrl: githubServerUrl
}
}

Expand Down
266 changes: 266 additions & 0 deletions __test__/git-command-manager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
import * as exec from '@actions/exec'
import * as fshelper from '../lib/fs-helper'
import * as commandManager from '../lib/git-command-manager'

let git: commandManager.IGitCommandManager
let mockExec = jest.fn()

describe('git-auth-helper tests', () => {
beforeAll(async () => {})

beforeEach(async () => {
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
})

afterEach(() => {
jest.restoreAllMocks()
})

afterAll(() => {})

it('branch list matches', async () => {
mockExec.mockImplementation((path, args, options) => {
console.log(args, options.listeners.stdout)

if (args.includes('version')) {
options.listeners.stdout(Buffer.from('2.18'))
return 0
}

if (args.includes('rev-parse')) {
options.listeners.stdline(Buffer.from('refs/heads/foo'))
options.listeners.stdline(Buffer.from('refs/heads/bar'))
return 0
}

return 1
})
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)

let branches = await git.branchList(false)

expect(branches).toHaveLength(2)
expect(branches.sort()).toEqual(['foo', 'bar'].sort())
})

it('ambiguous ref name output is captured', async () => {
mockExec.mockImplementation((path, args, options) => {
console.log(args, options.listeners.stdout)

if (args.includes('version')) {
options.listeners.stdout(Buffer.from('2.18'))
return 0
}

if (args.includes('rev-parse')) {
options.listeners.stdline(Buffer.from('refs/heads/foo'))
// If refs/tags/v1 and refs/heads/tags/v1 existed on this repository
options.listeners.errline(
Buffer.from("error: refname 'tags/v1' is ambiguous")
)
return 0
}

return 1
})
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)

let branches = await git.branchList(false)

expect(branches).toHaveLength(1)
expect(branches.sort()).toEqual(['foo'].sort())
})
})

describe('Test fetchDepth and fetchTags options', () => {
beforeEach(async () => {
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
mockExec.mockImplementation((path, args, options) => {
console.log(args, options.listeners.stdout)

if (args.includes('version')) {
options.listeners.stdout(Buffer.from('2.18'))
}

return 0
})
})

afterEach(() => {
jest.restoreAllMocks()
})

it('should call execGit with the correct arguments when fetchDepth is 0 and fetchTags is true', async () => {
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)

const refSpec = ['refspec1', 'refspec2']
const options = {
filter: 'filterValue',
fetchDepth: 0,
fetchTags: true
}

await git.fetch(refSpec, options)

expect(mockExec).toHaveBeenCalledWith(
expect.any(String),
[
'-c',
'protocol.version=2',
'fetch',
'--prune',
'--progress',
'--no-recurse-submodules',
'--filter=filterValue',
'origin',
'refspec1',
'refspec2'
],
expect.any(Object)
)
})

it('should call execGit with the correct arguments when fetchDepth is 0 and fetchTags is false', async () => {
jest.spyOn(exec, 'exec').mockImplementation(mockExec)

const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
const refSpec = ['refspec1', 'refspec2']
const options = {
filter: 'filterValue',
fetchDepth: 0,
fetchTags: false
}

await git.fetch(refSpec, options)

expect(mockExec).toHaveBeenCalledWith(
expect.any(String),
[
'-c',
'protocol.version=2',
'fetch',
'--no-tags',
'--prune',
'--progress',
'--no-recurse-submodules',
'--filter=filterValue',
'origin',
'refspec1',
'refspec2'
],
expect.any(Object)
)
})

it('should call execGit with the correct arguments when fetchDepth is 1 and fetchTags is false', async () => {
jest.spyOn(exec, 'exec').mockImplementation(mockExec)

const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
const refSpec = ['refspec1', 'refspec2']
const options = {
filter: 'filterValue',
fetchDepth: 1,
fetchTags: false
}

await git.fetch(refSpec, options)

expect(mockExec).toHaveBeenCalledWith(
expect.any(String),
[
'-c',
'protocol.version=2',
'fetch',
'--no-tags',
'--prune',
'--progress',
'--no-recurse-submodules',
'--filter=filterValue',
'--depth=1',
'origin',
'refspec1',
'refspec2'
],
expect.any(Object)
)
})

it('should call execGit with the correct arguments when fetchDepth is 1 and fetchTags is true', async () => {
jest.spyOn(exec, 'exec').mockImplementation(mockExec)

const workingDirectory = 'test'
const lfs = false
const doSparseCheckout = false
git = await commandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
const refSpec = ['refspec1', 'refspec2']
const options = {
filter: 'filterValue',
fetchDepth: 1,
fetchTags: true
}

await git.fetch(refSpec, options)

expect(mockExec).toHaveBeenCalledWith(
expect.any(String),
[
'-c',
'protocol.version=2',
'fetch',
'--prune',
'--progress',
'--no-recurse-submodules',
'--filter=filterValue',
'--depth=1',
'origin',
'refspec1',
'refspec2'
],
expect.any(Object)
)
})
})
64 changes: 64 additions & 0 deletions __test__/git-directory-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,65 @@ describe('git-directory-helper tests', () => {
expect(git.branchDelete).toHaveBeenCalledWith(false, 'local-branch-2')
})

const cleanWhenSubmoduleStatusIsFalse =
'cleans when submodule status is false'

it(cleanWhenSubmoduleStatusIsFalse, async () => {
// Arrange
await setup(cleanWhenSubmoduleStatusIsFalse)
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')

//mock bad submodule

const submoduleStatus = git.submoduleStatus as jest.Mock<any, any>
submoduleStatus.mockImplementation(async (remote: boolean) => {
return false
})

// Act
await gitDirectoryHelper.prepareExistingDirectory(
git,
repositoryPath,
repositoryUrl,
clean,
ref
)

// Assert
const files = await fs.promises.readdir(repositoryPath)
expect(files).toHaveLength(0)
expect(git.tryClean).toHaveBeenCalled()
})

const doesNotCleanWhenSubmoduleStatusIsTrue =
'does not clean when submodule status is true'

it(doesNotCleanWhenSubmoduleStatusIsTrue, async () => {
// Arrange
await setup(doesNotCleanWhenSubmoduleStatusIsTrue)
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')

const submoduleStatus = git.submoduleStatus as jest.Mock<any, any>
submoduleStatus.mockImplementation(async (remote: boolean) => {
return true
})

// 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.tryClean).toHaveBeenCalled()
})

const removesLockFiles = 'removes lock files'
it(removesLockFiles, async () => {
// Arrange
Expand Down Expand Up @@ -403,6 +462,8 @@ async function setup(testName: string): Promise<void> {
branchList: jest.fn(async () => {
return []
}),
sparseCheckout: jest.fn(),
sparseCheckoutNonConeMode: jest.fn(),
checkout: jest.fn(),
checkoutDetach: jest.fn(),
config: jest.fn(),
Expand All @@ -423,6 +484,9 @@ async function setup(testName: string): Promise<void> {
submoduleForeach: jest.fn(),
submoduleSync: jest.fn(),
submoduleUpdate: jest.fn(),
submoduleStatus: jest.fn(async () => {
return true
}),
tagExists: jest.fn(),
tryClean: jest.fn(async () => {
return true
Expand Down
4 changes: 4 additions & 0 deletions __test__/input-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,16 @@ describe('input-helper tests', () => {
expect(settings.clean).toBe(true)
expect(settings.commit).toBeTruthy()
expect(settings.commit).toBe('1234567890123456789012345678901234567890')
expect(settings.sparseCheckout).toBe(undefined)
expect(settings.sparseCheckoutConeMode).toBe(true)
expect(settings.fetchDepth).toBe(1)
expect(settings.fetchTags).toBe(false)
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)
expect(settings.setSafeDirectory).toBe(true)
})

it('qualifies ref', async () => {
Expand Down
51 changes: 51 additions & 0 deletions __test__/verify-sparse-checkout-non-cone-mode.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/bin/bash

# Verify .git folder
if [ ! -d "./sparse-checkout-non-cone-mode/.git" ]; then
echo "Expected ./sparse-checkout-non-cone-mode/.git folder to exist"
exit 1
fi

# Verify sparse-checkout (non-cone-mode)
cd sparse-checkout-non-cone-mode

ENABLED=$(git config --local --get-all core.sparseCheckout)

if [ "$?" != "0" ]; then
echo "Failed to verify that sparse-checkout is enabled"
exit 1
fi

# Check that sparse-checkout is enabled
if [ "$ENABLED" != "true" ]; then
echo "Expected sparse-checkout to be enabled (is: $ENABLED)"
exit 1
fi

SPARSE_CHECKOUT_FILE=$(git rev-parse --git-path info/sparse-checkout)

if [ "$?" != "0" ]; then
echo "Failed to validate sparse-checkout"
exit 1
fi

# Check that sparse-checkout list is not empty
if [ ! -f "$SPARSE_CHECKOUT_FILE" ]; then
echo "Expected sparse-checkout file to exist"
exit 1
fi

# Check that all folders from sparse-checkout exists
for pattern in $(cat "$SPARSE_CHECKOUT_FILE")
do
if [ ! -d "${pattern#/}" ]; then
echo "Expected directory '${pattern#/}' to exist"
exit 1
fi
done

# Verify that the root directory is not checked out
if [ -f README.md ]; then
echo "Expected top-level files not to exist"
exit 1
fi
63 changes: 63 additions & 0 deletions __test__/verify-sparse-checkout.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/bin/bash

# Verify .git folder
if [ ! -d "./sparse-checkout/.git" ]; then
echo "Expected ./sparse-checkout/.git folder to exist"
exit 1
fi

# Verify sparse-checkout
cd sparse-checkout

SPARSE=$(git sparse-checkout list)

if [ "$?" != "0" ]; then
echo "Failed to validate sparse-checkout"
exit 1
fi

# Check that sparse-checkout list is not empty
if [ -z "$SPARSE" ]; then
echo "Expected sparse-checkout list to not be empty"
exit 1
fi

# Check that all folders of the sparse checkout exist
for pattern in $SPARSE
do
if [ ! -d "$pattern" ]; then
echo "Expected directory '$pattern' to exist"
exit 1
fi
done

checkSparse () {
if [ ! -d "./$1" ]; then
echo "Expected directory '$1' to exist"
exit 1
fi

for file in $(git ls-tree -r --name-only HEAD $1)
do
if [ ! -f "$file" ]; then
echo "Expected file '$file' to exist"
exit 1
fi
done
}

# Check that all folders and their children have been checked out
checkSparse __test__
checkSparse .github
checkSparse dist

# Check that only sparse-checkout folders have been checked out
for pattern in $(git ls-tree --name-only HEAD)
do
if [ -d "$pattern" ]; then
if [[ "$pattern" != "__test__" && "$pattern" != ".github" && "$pattern" != "dist" ]]; then
echo "Expected directory '$pattern' to not exist"
exit 1
fi
fi
done
20 changes: 19 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,21 @@ inputs:
clean:
description: 'Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching'
default: true
sparse-checkout:
description: >
Do a sparse checkout on given patterns.
Each pattern should be separated with new lines
default: null
sparse-checkout-cone-mode:
description: >
Specifies whether to use cone-mode when doing a sparse checkout.
default: true
fetch-depth:
description: 'Number of commits to fetch. 0 indicates all history for all branches and tags.'
default: 1
fetch-tags:
description: 'Whether to fetch tags, even if fetch-depth > 0.'
default: false
lfs:
description: 'Whether to download Git-LFS files'
default: false
Expand All @@ -68,7 +80,13 @@ inputs:
When the `ssh-key` input is not provided, SSH URLs beginning with `git@github.com:` are
converted to HTTPS.
default: false
set-safe-directory:
description: Add repository path as safe.directory for Git global config by running `git config --global --add safe.directory <path>`
default: true
github-server-url:
description: The base URL for the GitHub instance that you are trying to clone from, will use environment defaults to fetch from the same instance that the workflow is running from unless specified. Example URLs are https://github.com or https://my-ghes-server.example.com
required: false
runs:
using: node12
using: node16
main: dist/index.js
post: dist/index.js
4 changes: 2 additions & 2 deletions adrs/0153-checkout-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ GITHUB_WORKSPACE=/home/runner/work/foo/foo
RUNNER_WORKSPACE=/home/runner/work/foo
```

V2 introduces a new contraint on the checkout path. The location must now be under `github.workspace`. Whereas the checkout@v1 constraint was one level up, under `runner.workspace`.
V2 introduces a new constraint on the checkout path. The location must now be under `github.workspace`. Whereas the checkout@v1 constraint was one level up, under `runner.workspace`.

V2 no longer changes `github.workspace` to follow wherever the self repo is checked-out.

Expand Down Expand Up @@ -287,4 +287,4 @@ Note:
- Update samples to consume `actions/checkout@v2`
- Job containers now require git in the PATH for checkout, otherwise fallback to REST API
- Minimum git version 2.18
- Update problem matcher logic regarding source file verification (runner)
- Update problem matcher logic regarding source file verification (runner)
47,912 changes: 15,004 additions & 32,908 deletions dist/index.js

Large diffs are not rendered by default.

22,975 changes: 18,871 additions & 4,104 deletions package-lock.json

Large diffs are not rendered by default.

17 changes: 9 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "checkout",
"version": "2.0.2",
"version": "3.6.0",
"description": "checkout action",
"main": "lib/main.js",
"scripts": {
Expand Down Expand Up @@ -28,27 +28,28 @@
},
"homepage": "https://github.com/actions/checkout#readme",
"dependencies": {
"@actions/core": "^1.2.6",
"@actions/core": "^1.10.0",
"@actions/exec": "^1.0.1",
"@actions/github": "^2.2.0",
"@actions/io": "^1.0.1",
"@actions/github": "^5.0.0",
"@actions/io": "^1.1.3",
"@actions/tool-cache": "^1.1.2",
"uuid": "^3.3.3"
},
"devDependencies": {
"@types/jest": "^27.0.2",
"@types/node": "^12.7.12",
"@types/uuid": "^3.4.6",
"@typescript-eslint/parser": "^5.1.0",
"@zeit/ncc": "^0.20.5",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"@vercel/ncc": "^0.36.1",
"eslint": "^7.32.0",
"eslint-plugin-github": "^4.3.2",
"eslint-plugin-jest": "^25.2.2",
"eslint-plugin-jest": "^25.7.0",
"jest": "^27.3.0",
"jest-circus": "^27.3.0",
"js-yaml": "^3.13.1",
"prettier": "^1.19.1",
"ts-jest": "^27.0.7",
"typescript": "^4.4.4"
}
}
}
47 changes: 31 additions & 16 deletions src/git-auth-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ export interface IGitAuthHelper {
configureAuth(): Promise<void>
configureGlobalAuth(): Promise<void>
configureSubmoduleAuth(): Promise<void>
configureTempGlobalConfig(): Promise<string>
removeAuth(): Promise<void>
removeGlobalAuth(): Promise<void>
removeGlobalConfig(): Promise<void>
}

export function createAuthHelper(
Expand Down Expand Up @@ -51,7 +52,7 @@ class GitAuthHelper {
this.settings = gitSourceSettings || (({} as unknown) as IGitSourceSettings)

// Token auth header
const serverUrl = urlHelper.getServerUrl()
const serverUrl = urlHelper.getServerUrl(this.settings.githubServerUrl)
this.tokenConfigKey = `http.${serverUrl.origin}/.extraheader` // "origin" is SCHEME://HOSTNAME[:PORT]
const basicCredential = Buffer.from(
`x-access-token:${this.settings.authToken}`,
Expand Down Expand Up @@ -80,7 +81,11 @@ class GitAuthHelper {
await this.configureToken()
}

async configureGlobalAuth(): Promise<void> {
async configureTempGlobalConfig(): Promise<string> {
// Already setup global config
if (this.temporaryHomePath?.length > 0) {
return path.join(this.temporaryHomePath, '.gitconfig')
}
// Create a temp home directory
const runnerTemp = process.env['RUNNER_TEMP'] || ''
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
Expand Down Expand Up @@ -110,13 +115,19 @@ class GitAuthHelper {
await fs.promises.writeFile(newGitConfigPath, '')
}

try {
// Override HOME
core.info(
`Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`
)
this.git.setEnvironmentVariable('HOME', this.temporaryHomePath)
// Override HOME
core.info(
`Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`
)
this.git.setEnvironmentVariable('HOME', this.temporaryHomePath)

return newGitConfigPath
}

async configureGlobalAuth(): Promise<void> {
// 'configureTempGlobalConfig' noops if already set, just returns the path
const newGitConfigPath = await this.configureTempGlobalConfig()
try {
// Configure the token
await this.configureToken(newGitConfigPath, true)

Expand Down Expand Up @@ -146,7 +157,8 @@ class GitAuthHelper {
// by process creation audit events, which are commonly logged. For more information,
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
const output = await this.git.submoduleForeach(
`git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url`,
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
`sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`,
this.settings.nestedSubmodules
)

Expand Down Expand Up @@ -181,10 +193,12 @@ class GitAuthHelper {
await this.removeToken()
}

async removeGlobalAuth(): Promise<void> {
core.debug(`Unsetting HOME override`)
this.git.removeEnvironmentVariable('HOME')
await io.rmRF(this.temporaryHomePath)
async removeGlobalConfig(): Promise<void> {
if (this.temporaryHomePath?.length > 0) {
core.debug(`Unsetting HOME override`)
this.git.removeEnvironmentVariable('HOME')
await io.rmRF(this.temporaryHomePath)
}
}

private async configureSsh(): Promise<void> {
Expand Down Expand Up @@ -233,7 +247,7 @@ class GitAuthHelper {
if (this.settings.sshKnownHosts) {
knownHosts += `# Begin from input known hosts\n${this.settings.sshKnownHosts}\n# end from input known hosts\n`
}
knownHosts += `# Begin implicitly added github.com\ngithub.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==\n# End implicitly added github.com\n`
knownHosts += `# Begin implicitly added github.com\ngithub.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=\n# End implicitly added github.com\n`
this.sshKnownHostsPath = path.join(runnerTemp, `${uniqueId}_known_hosts`)
stateHelper.setSshKnownHostsPath(this.sshKnownHostsPath)
await fs.promises.writeFile(this.sshKnownHostsPath, knownHosts)
Expand Down Expand Up @@ -352,7 +366,8 @@ class GitAuthHelper {

const pattern = regexpHelper.escape(configKey)
await this.git.submoduleForeach(
`git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :`,
// wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
`sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`,
true
)
}
Expand Down
167 changes: 137 additions & 30 deletions src/git-command-manager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as core from '@actions/core'
import * as exec from '@actions/exec'
import * as fs from 'fs'
import * as fshelper from './fs-helper'
import * as io from '@actions/io'
import * as path from 'path'
Expand All @@ -16,6 +17,8 @@ export interface IGitCommandManager {
branchDelete(remote: boolean, branch: string): Promise<void>
branchExists(remote: boolean, pattern: string): Promise<boolean>
branchList(remote: boolean): Promise<string[]>
sparseCheckout(sparseCheckout: string[]): Promise<void>
sparseCheckoutNonConeMode(sparseCheckout: string[]): Promise<void>
checkout(ref: string, startPoint: string): Promise<void>
checkoutDetach(): Promise<void>
config(
Expand All @@ -25,7 +28,14 @@ export interface IGitCommandManager {
add?: boolean
): Promise<void>
configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
fetch(refSpec: string[], fetchDepth?: number): Promise<void>
fetch(
refSpec: string[],
options: {
filter?: string
fetchDepth?: number
fetchTags?: boolean
}
): Promise<void>
getDefaultBranch(repositoryUrl: string): Promise<string>
getWorkingDirectory(): string
init(): Promise<void>
Expand All @@ -41,6 +51,7 @@ export interface IGitCommandManager {
submoduleForeach(command: string, recursive: boolean): Promise<string>
submoduleSync(recursive: boolean): Promise<void>
submoduleUpdate(fetchDepth: number, recursive: boolean): Promise<void>
submoduleStatus(): Promise<boolean>
tagExists(pattern: string): Promise<boolean>
tryClean(): Promise<boolean>
tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
Expand All @@ -51,9 +62,14 @@ export interface IGitCommandManager {

export async function createCommandManager(
workingDirectory: string,
lfs: boolean
lfs: boolean,
doSparseCheckout: boolean
): Promise<IGitCommandManager> {
return await GitCommandManager.createCommandManager(workingDirectory, lfs)
return await GitCommandManager.createCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
}

class GitCommandManager {
Expand All @@ -63,6 +79,7 @@ class GitCommandManager {
}
private gitPath = ''
private lfs = false
private doSparseCheckout = false
private workingDirectory = ''

// Private constructor; use createCommandManager()
Expand Down Expand Up @@ -94,8 +111,11 @@ class GitCommandManager {

// Note, this implementation uses "rev-parse --symbolic-full-name" because the output from
// "branch --list" is more difficult when in a detached HEAD state.
// Note, this implementation uses "rev-parse --symbolic-full-name" because there is a bug
// in Git 2.18 that causes "rev-parse --symbolic" to output symbolic full names.

// TODO(https://github.com/actions/checkout/issues/786): this implementation uses
// "rev-parse --symbolic-full-name" because there is a bug
// in Git 2.18 that causes "rev-parse --symbolic" to output symbolic full names. When
// 2.18 is no longer supported, we can switch back to --symbolic.

const args = ['rev-parse', '--symbolic-full-name']
if (remote) {
Expand All @@ -104,24 +124,73 @@ class GitCommandManager {
args.push('--branches')
}

const output = await this.execGit(args)
const stderr: string[] = []
const errline: string[] = []
const stdout: string[] = []
const stdline: string[] = []

const listeners = {
stderr: (data: Buffer) => {
stderr.push(data.toString())
},
errline: (data: Buffer) => {
errline.push(data.toString())
},
stdout: (data: Buffer) => {
stdout.push(data.toString())
},
stdline: (data: Buffer) => {
stdline.push(data.toString())
}
}

// Suppress the output in order to avoid flooding annotations with innocuous errors.
await this.execGit(args, false, true, listeners)

for (let branch of output.stdout.trim().split('\n')) {
core.debug(`stderr callback is: ${stderr}`)
core.debug(`errline callback is: ${errline}`)
core.debug(`stdout callback is: ${stdout}`)
core.debug(`stdline callback is: ${stdline}`)

for (let branch of stdline) {
branch = branch.trim()
if (branch) {
if (branch.startsWith('refs/heads/')) {
branch = branch.substr('refs/heads/'.length)
} else if (branch.startsWith('refs/remotes/')) {
branch = branch.substr('refs/remotes/'.length)
}
if (!branch) {
continue
}

result.push(branch)
if (branch.startsWith('refs/heads/')) {
branch = branch.substring('refs/heads/'.length)
} else if (branch.startsWith('refs/remotes/')) {
branch = branch.substring('refs/remotes/'.length)
}

result.push(branch)
}

return result
}

async sparseCheckout(sparseCheckout: string[]): Promise<void> {
await this.execGit(['sparse-checkout', 'set', ...sparseCheckout])
}

async sparseCheckoutNonConeMode(sparseCheckout: string[]): Promise<void> {
await this.execGit(['config', 'core.sparseCheckout', 'true'])
const output = await this.execGit([
'rev-parse',
'--git-path',
'info/sparse-checkout'
])
const sparseCheckoutPath = path.join(
this.workingDirectory,
output.stdout.trimRight()
)
await fs.promises.appendFile(
sparseCheckoutPath,
`\n${sparseCheckout.join('\n')}\n`
)
}

async checkout(ref: string, startPoint: string): Promise<void> {
const args = ['checkout', '--progress', '--force']
if (startPoint) {
Expand Down Expand Up @@ -170,15 +239,23 @@ class GitCommandManager {
return output.exitCode === 0
}

async fetch(refSpec: string[], fetchDepth?: number): Promise<void> {
async fetch(
refSpec: string[],
options: {filter?: string; fetchDepth?: number; fetchTags?: boolean}
): Promise<void> {
const args = ['-c', 'protocol.version=2', 'fetch']
if (!refSpec.some(x => x === refHelper.tagsRefSpec)) {
if (!refSpec.some(x => x === refHelper.tagsRefSpec) && !options.fetchTags) {
args.push('--no-tags')
}

args.push('--prune', '--progress', '--no-recurse-submodules')
if (fetchDepth && fetchDepth > 0) {
args.push(`--depth=${fetchDepth}`)

if (options.filter) {
args.push(`--filter=${options.filter}`)
}

if (options.fetchDepth && options.fetchDepth > 0) {
args.push(`--depth=${options.fetchDepth}`)
} else if (
fshelper.fileExistsSync(
path.join(this.workingDirectory, '.git', 'shallow')
Expand Down Expand Up @@ -257,8 +334,8 @@ class GitCommandManager {
}

async log1(format?: string): Promise<string> {
var args = format ? ['log', '-1', format] : ['log', '-1']
var silent = format ? false : true
const args = format ? ['log', '-1', format] : ['log', '-1']
const silent = format ? false : true
const output = await this.execGit(args, false, silent)
return output.stdout
}
Expand Down Expand Up @@ -326,6 +403,12 @@ class GitCommandManager {
await this.execGit(args)
}

async submoduleStatus(): Promise<boolean> {
const output = await this.execGit(['submodule', 'status'], true)
core.debug(output.stdout)
return output.exitCode === 0
}

async tagExists(pattern: string): Promise<boolean> {
const output = await this.execGit(['tag', '--list', pattern])
return !!output.stdout.trim()
Expand Down Expand Up @@ -385,17 +468,23 @@ class GitCommandManager {

static async createCommandManager(
workingDirectory: string,
lfs: boolean
lfs: boolean,
doSparseCheckout: boolean
): Promise<GitCommandManager> {
const result = new GitCommandManager()
await result.initializeCommandManager(workingDirectory, lfs)
await result.initializeCommandManager(
workingDirectory,
lfs,
doSparseCheckout
)
return result
}

private async execGit(
args: string[],
allowAllExitCodes = false,
silent = false
silent = false,
customListeners = {}
): Promise<GitOutput> {
fshelper.directoryExistsSync(this.workingDirectory, true)

Expand All @@ -409,28 +498,36 @@ class GitCommandManager {
env[key] = this.gitEnv[key]
}

const stdout: string[] = []
const defaultListener = {
stdout: (data: Buffer) => {
stdout.push(data.toString())
}
}

const mergedListeners = {...defaultListener, ...customListeners}

const stdout: string[] = []
const options = {
cwd: this.workingDirectory,
env,
silent,
ignoreReturnCode: allowAllExitCodes,
listeners: {
stdout: (data: Buffer) => {
stdout.push(data.toString())
}
}
listeners: mergedListeners
}

result.exitCode = await exec.exec(`"${this.gitPath}"`, args, options)
result.stdout = stdout.join('')

core.debug(result.exitCode.toString())
core.debug(result.stdout)

return result
}

private async initializeCommandManager(
workingDirectory: string,
lfs: boolean
lfs: boolean,
doSparseCheckout: boolean
): Promise<void> {
this.workingDirectory = workingDirectory

Expand Down Expand Up @@ -493,6 +590,16 @@ class GitCommandManager {
}
}

this.doSparseCheckout = doSparseCheckout
if (this.doSparseCheckout) {
// The `git sparse-checkout` command was introduced in Git v2.25.0
const minimumGitSparseCheckoutVersion = new GitVersion('2.25')
if (!gitVersion.checkMinimum(minimumGitSparseCheckoutVersion)) {
throw new Error(
`Minimum Git version required for sparse checkout is ${minimumGitSparseCheckoutVersion}. Your git ('${this.gitPath}') is ${gitVersion}`
)
}
}
// Set the user agent
const gitHttpUserAgent = `git/${gitVersion} (github-actions-checkout)`
core.debug(`Set git useragent to: ${gitHttpUserAgent}`)
Expand Down
8 changes: 7 additions & 1 deletion src/git-directory-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,18 @@ export async function prepareExistingDirectory(
}
core.endGroup()

// Check for submodules and delete any existing files if submodules are present
if (!(await git.submoduleStatus())) {
remove = true
core.info('Bad Submodules found, removing existing files')
}

// Clean
if (clean) {
core.startGroup('Cleaning the repository')
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}'.`
`The clean command failed. This might be caused by: 1) path too long, 2) permission issue, or 3) file in use. For further investigation, manually run 'git clean -ffdx' on the directory '${repositoryPath}'.`
)
remove = true
} else if (!(await git.tryReset())) {
Expand Down
Loading