mirror of
https://github.com/actions/checkout.git
synced 2026-07-08 19:03:47 +08:00
Compare commits
12 Commits
v4
...
6452e1970f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6452e1970f | ||
|
|
ff7abcd0c3 | ||
|
|
45abae3e9f | ||
|
|
08c6903cd8 | ||
|
|
215f9562a1 | ||
|
|
caa5717450 | ||
|
|
630cdb3874 | ||
|
|
6503dcd44c | ||
|
|
ebd82bae91 | ||
|
|
f04b821901 | ||
|
|
45fe6460ed | ||
|
|
9f265659d3 |
4
.github/workflows/check-dist.yml
vendored
4
.github/workflows/check-dist.yml
vendored
@@ -24,10 +24,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4.1.6
|
- uses: actions/checkout@v4.1.6
|
||||||
|
|
||||||
- name: Set Node.js 20.x
|
- name: Set Node.js 24.x
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 24.x
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|||||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 24.x
|
||||||
- uses: actions/checkout@v4.1.6
|
- uses: actions/checkout@v4.1.6
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
|
|||||||
1
.github/workflows/update-main-version.yml
vendored
1
.github/workflows/update-main-version.yml
vendored
@@ -11,6 +11,7 @@ on:
|
|||||||
type: choice
|
type: choice
|
||||||
description: The major version to update
|
description: The major version to update
|
||||||
options:
|
options:
|
||||||
|
- v5
|
||||||
- v4
|
- v4
|
||||||
- v3
|
- v3
|
||||||
- v2
|
- v2
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## V5.0.0
|
||||||
|
* Update actions checkout to use node 24 by @salmanmkc in https://github.com/actions/checkout/pull/2226
|
||||||
|
|
||||||
|
|
||||||
## V4.3.0
|
## V4.3.0
|
||||||
* docs: update README.md by @motss in https://github.com/actions/checkout/pull/1971
|
* docs: update README.md by @motss in https://github.com/actions/checkout/pull/1971
|
||||||
* Add internal repos for checking out multiple repositories by @mouismail in https://github.com/actions/checkout/pull/1977
|
* Add internal repos for checking out multiple repositories by @mouismail in https://github.com/actions/checkout/pull/1977
|
||||||
|
|||||||
98
README.md
98
README.md
@@ -1,5 +1,13 @@
|
|||||||
[](https://github.com/actions/checkout/actions/workflows/test.yml)
|
[](https://github.com/actions/checkout/actions/workflows/test.yml)
|
||||||
|
|
||||||
|
# Checkout V5
|
||||||
|
|
||||||
|
## What's new
|
||||||
|
|
||||||
|
- Updated to the node24 runtime
|
||||||
|
- This requires a minimum Actions Runner version of [v2.327.1](https://github.com/actions/runner/releases/tag/v2.327.1) to run.
|
||||||
|
|
||||||
|
|
||||||
# Checkout V4
|
# Checkout V4
|
||||||
|
|
||||||
This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it.
|
This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it.
|
||||||
@@ -36,7 +44,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
|
|
||||||
<!-- start usage -->
|
<!-- start usage -->
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
# Repository name with owner. For example, actions/checkout
|
# Repository name with owner. For example, actions/checkout
|
||||||
# Default: ${{ github.repository }}
|
# Default: ${{ github.repository }}
|
||||||
@@ -96,6 +104,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
# Default: true
|
# Default: true
|
||||||
clean: ''
|
clean: ''
|
||||||
|
|
||||||
|
# Whether to preserve local changes during checkout. If true, tries to preserve
|
||||||
|
# local files that are not tracked by Git. By default, all files will be
|
||||||
|
# overwritten.
|
||||||
|
# Default: false
|
||||||
|
preserve-local-changes: ''
|
||||||
|
|
||||||
# Partially clone against a given filter. Overrides sparse-checkout if set.
|
# Partially clone against a given filter. Overrides sparse-checkout if set.
|
||||||
# Default: null
|
# Default: null
|
||||||
filter: ''
|
filter: ''
|
||||||
@@ -149,24 +163,33 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
|
|
||||||
# Scenarios
|
# Scenarios
|
||||||
|
|
||||||
- [Fetch only the root files](#Fetch-only-the-root-files)
|
- [Checkout V5](#checkout-v5)
|
||||||
- [Fetch only the root files and `.github` and `src` folder](#Fetch-only-the-root-files-and-github-and-src-folder)
|
- [What's new](#whats-new)
|
||||||
- [Fetch only a single file](#Fetch-only-a-single-file)
|
- [Checkout V4](#checkout-v4)
|
||||||
- [Fetch all history for all tags and branches](#Fetch-all-history-for-all-tags-and-branches)
|
- [Note](#note)
|
||||||
- [Checkout a different branch](#Checkout-a-different-branch)
|
- [What's new](#whats-new-1)
|
||||||
- [Checkout HEAD^](#Checkout-HEAD)
|
- [Usage](#usage)
|
||||||
- [Checkout multiple repos (side by side)](#Checkout-multiple-repos-side-by-side)
|
- [Scenarios](#scenarios)
|
||||||
- [Checkout multiple repos (nested)](#Checkout-multiple-repos-nested)
|
- [Fetch only the root files](#fetch-only-the-root-files)
|
||||||
- [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
|
- [Fetch only the root files and `.github` and `src` folder](#fetch-only-the-root-files-and-github-and-src-folder)
|
||||||
- [Checkout pull request HEAD commit instead of merge commit](#Checkout-pull-request-HEAD-commit-instead-of-merge-commit)
|
- [Fetch only a single file](#fetch-only-a-single-file)
|
||||||
- [Checkout pull request on closed event](#Checkout-pull-request-on-closed-event)
|
- [Fetch all history for all tags and branches](#fetch-all-history-for-all-tags-and-branches)
|
||||||
- [Push a commit using the built-in token](#Push-a-commit-using-the-built-in-token)
|
- [Checkout a different branch](#checkout-a-different-branch)
|
||||||
- [Push a commit to a PR using the built-in token](#Push-a-commit-to-a-PR-using-the-built-in-token)
|
- [Checkout HEAD^](#checkout-head)
|
||||||
|
- [Checkout multiple repos (side by side)](#checkout-multiple-repos-side-by-side)
|
||||||
|
- [Checkout multiple repos (nested)](#checkout-multiple-repos-nested)
|
||||||
|
- [Checkout multiple repos (private)](#checkout-multiple-repos-private)
|
||||||
|
- [Checkout pull request HEAD commit instead of merge commit](#checkout-pull-request-head-commit-instead-of-merge-commit)
|
||||||
|
- [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)
|
||||||
|
- [Push a commit to a PR using the built-in token](#push-a-commit-to-a-pr-using-the-built-in-token)
|
||||||
|
- [Recommended permissions](#recommended-permissions)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
## Fetch only the root files
|
## Fetch only the root files
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
sparse-checkout: .
|
sparse-checkout: .
|
||||||
```
|
```
|
||||||
@@ -174,7 +197,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
## Fetch only the root files and `.github` and `src` folder
|
## Fetch only the root files and `.github` and `src` folder
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
sparse-checkout: |
|
sparse-checkout: |
|
||||||
.github
|
.github
|
||||||
@@ -184,7 +207,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
## Fetch only a single file
|
## Fetch only a single file
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
sparse-checkout: |
|
sparse-checkout: |
|
||||||
README.md
|
README.md
|
||||||
@@ -194,7 +217,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
## Fetch all history for all tags and branches
|
## Fetch all history for all tags and branches
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
```
|
```
|
||||||
@@ -202,7 +225,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
## Checkout a different branch
|
## Checkout a different branch
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: my-branch
|
ref: my-branch
|
||||||
```
|
```
|
||||||
@@ -210,7 +233,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
## Checkout HEAD^
|
## Checkout HEAD^
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
- run: git checkout HEAD^
|
- run: git checkout HEAD^
|
||||||
@@ -220,12 +243,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
path: main
|
path: main
|
||||||
|
|
||||||
- name: Checkout tools repo
|
- name: Checkout tools repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
repository: my-org/my-tools
|
repository: my-org/my-tools
|
||||||
path: my-tools
|
path: my-tools
|
||||||
@@ -236,10 +259,10 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Checkout tools repo
|
- name: Checkout tools repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
repository: my-org/my-tools
|
repository: my-org/my-tools
|
||||||
path: my-tools
|
path: my-tools
|
||||||
@@ -250,12 +273,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
path: main
|
path: main
|
||||||
|
|
||||||
- name: Checkout private tools
|
- name: Checkout private tools
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
repository: my-org/my-private-tools
|
repository: my-org/my-private-tools
|
||||||
token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT
|
token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT
|
||||||
@@ -268,7 +291,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
|
|||||||
## Checkout pull request HEAD commit instead of merge commit
|
## Checkout pull request HEAD commit instead of merge commit
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
```
|
```
|
||||||
@@ -284,7 +307,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
```
|
```
|
||||||
|
|
||||||
## Push a commit using the built-in token
|
## Push a commit using the built-in token
|
||||||
@@ -295,7 +318,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
- run: |
|
- run: |
|
||||||
date > generated.txt
|
date > generated.txt
|
||||||
# Note: the following account information will not work on GHES
|
# Note: the following account information will not work on GHES
|
||||||
@@ -317,7 +340,7 @@ jobs:
|
|||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.head_ref }}
|
ref: ${{ github.head_ref }}
|
||||||
- run: |
|
- run: |
|
||||||
@@ -332,6 +355,21 @@ jobs:
|
|||||||
|
|
||||||
*NOTE:* The user email is `{user.id}+{user.login}@users.noreply.github.com`. See users API: https://api.github.com/users/github-actions%5Bbot%5D
|
*NOTE:* The user email is `{user.id}+{user.login}@users.noreply.github.com`. See users API: https://api.github.com/users/github-actions%5Bbot%5D
|
||||||
|
|
||||||
|
## Preserve local changes during checkout
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
steps:
|
||||||
|
- name: Create file before checkout
|
||||||
|
shell: pwsh
|
||||||
|
run: New-Item -Path . -Name "example.txt" -ItemType "File"
|
||||||
|
|
||||||
|
- name: Checkout with preserving local changes
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
clean: false
|
||||||
|
preserve-local-changes: true
|
||||||
|
```
|
||||||
|
|
||||||
# Recommended permissions
|
# Recommended permissions
|
||||||
|
|
||||||
When using the `checkout` action in your GitHub Actions workflow, it is recommended to set the following `GITHUB_TOKEN` permissions to ensure proper functionality, unless alternative auth is provided via the `token` or `ssh-key` inputs:
|
When using the `checkout` action in your GitHub Actions workflow, it is recommended to set the following `GITHUB_TOKEN` permissions to ensure proper functionality, unless alternative auth is provided via the `token` or `ssh-key` inputs:
|
||||||
|
|||||||
@@ -814,6 +814,7 @@ async function setup(testName: string): Promise<void> {
|
|||||||
submodules: false,
|
submodules: false,
|
||||||
nestedSubmodules: false,
|
nestedSubmodules: false,
|
||||||
persistCredentials: true,
|
persistCredentials: true,
|
||||||
|
preserveLocalChanges: false,
|
||||||
ref: 'refs/heads/main',
|
ref: 'refs/heads/main',
|
||||||
repositoryName: 'my-repo',
|
repositoryName: 'my-repo',
|
||||||
repositoryOwner: 'my-org',
|
repositoryOwner: 'my-org',
|
||||||
|
|||||||
@@ -143,12 +143,41 @@ describe('git-directory-helper tests', () => {
|
|||||||
repositoryPath,
|
repositoryPath,
|
||||||
repositoryUrl,
|
repositoryUrl,
|
||||||
clean,
|
clean,
|
||||||
ref
|
ref,
|
||||||
|
false // preserveLocalChanges = false
|
||||||
)
|
)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
const files = await fs.promises.readdir(repositoryPath)
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
expect(files).toHaveLength(0)
|
expect(files).toEqual(['.git']) // Expect just the .git directory to remain
|
||||||
|
expect(git.tryClean).toHaveBeenCalled()
|
||||||
|
expect(core.warning).toHaveBeenCalled()
|
||||||
|
expect(git.tryReset).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
const preservesContentsWhenCleanFailsAndPreserveLocalChanges = 'preserves contents when clean fails and preserve-local-changes is true'
|
||||||
|
it(preservesContentsWhenCleanFailsAndPreserveLocalChanges, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(preservesContentsWhenCleanFailsAndPreserveLocalChanges)
|
||||||
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
|
let mockTryClean = git.tryClean as jest.Mock<any, any>
|
||||||
|
mockTryClean.mockImplementation(async () => {
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await gitDirectoryHelper.prepareExistingDirectory(
|
||||||
|
git,
|
||||||
|
repositoryPath,
|
||||||
|
repositoryUrl,
|
||||||
|
clean,
|
||||||
|
ref,
|
||||||
|
true // preserveLocalChanges = true
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
|
expect(files.sort()).toEqual(['.git', 'my-file']) // Expect both .git and user files to remain
|
||||||
expect(git.tryClean).toHaveBeenCalled()
|
expect(git.tryClean).toHaveBeenCalled()
|
||||||
expect(core.warning).toHaveBeenCalled()
|
expect(core.warning).toHaveBeenCalled()
|
||||||
expect(git.tryReset).not.toHaveBeenCalled()
|
expect(git.tryReset).not.toHaveBeenCalled()
|
||||||
@@ -170,16 +199,50 @@ describe('git-directory-helper tests', () => {
|
|||||||
repositoryPath,
|
repositoryPath,
|
||||||
differentRepositoryUrl,
|
differentRepositoryUrl,
|
||||||
clean,
|
clean,
|
||||||
ref
|
ref,
|
||||||
|
false // preserveLocalChanges = false
|
||||||
)
|
)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
const files = await fs.promises.readdir(repositoryPath)
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
expect(files).toHaveLength(0)
|
expect(files).toEqual(['.git']) // Expect just the .git directory to remain
|
||||||
expect(core.warning).not.toHaveBeenCalled()
|
expect(core.warning).not.toHaveBeenCalled()
|
||||||
expect(git.isDetached).not.toHaveBeenCalled()
|
expect(git.isDetached).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const keepsContentsWhenDifferentRepositoryUrlAndPreserveLocalChanges =
|
||||||
|
'keeps contents when different repository url and preserve-local-changes is true'
|
||||||
|
it(keepsContentsWhenDifferentRepositoryUrlAndPreserveLocalChanges, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(keepsContentsWhenDifferentRepositoryUrlAndPreserveLocalChanges)
|
||||||
|
clean = false
|
||||||
|
|
||||||
|
// Create a file that we expect to be preserved
|
||||||
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
|
|
||||||
|
// Simulate a different repository by simply removing the .git directory
|
||||||
|
await io.rmRF(path.join(repositoryPath, '.git'))
|
||||||
|
await fs.promises.mkdir(path.join(repositoryPath, '.git'))
|
||||||
|
|
||||||
|
const differentRepositoryUrl = 'https://github.com/my-different-org/my-different-repo'
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await gitDirectoryHelper.prepareExistingDirectory(
|
||||||
|
git,
|
||||||
|
repositoryPath,
|
||||||
|
differentRepositoryUrl, // Use a different URL
|
||||||
|
clean,
|
||||||
|
ref,
|
||||||
|
true // preserveLocalChanges = true
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
|
console.log('Files after operation:', files)
|
||||||
|
// When preserveLocalChanges is true, files should be preserved even with different repo URL
|
||||||
|
expect(files.sort()).toEqual(['.git', 'my-file'].sort())
|
||||||
|
})
|
||||||
|
|
||||||
const removesContentsWhenNoGitDirectory =
|
const removesContentsWhenNoGitDirectory =
|
||||||
'removes contents when no git directory'
|
'removes contents when no git directory'
|
||||||
it(removesContentsWhenNoGitDirectory, async () => {
|
it(removesContentsWhenNoGitDirectory, async () => {
|
||||||
@@ -221,12 +284,41 @@ describe('git-directory-helper tests', () => {
|
|||||||
repositoryPath,
|
repositoryPath,
|
||||||
repositoryUrl,
|
repositoryUrl,
|
||||||
clean,
|
clean,
|
||||||
ref
|
ref,
|
||||||
|
false // preserveLocalChanges = false
|
||||||
)
|
)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
const files = await fs.promises.readdir(repositoryPath)
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
expect(files).toHaveLength(0)
|
expect(files).toEqual(['.git']) // Expect just the .git directory to remain
|
||||||
|
expect(git.tryClean).toHaveBeenCalled()
|
||||||
|
expect(git.tryReset).toHaveBeenCalled()
|
||||||
|
expect(core.warning).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
const preservesContentsWhenResetFailsAndPreserveLocalChanges = 'preserves contents when reset fails and preserve-local-changes is true'
|
||||||
|
it(preservesContentsWhenResetFailsAndPreserveLocalChanges, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setup(preservesContentsWhenResetFailsAndPreserveLocalChanges)
|
||||||
|
await fs.promises.writeFile(path.join(repositoryPath, 'my-file'), '')
|
||||||
|
let mockTryReset = git.tryReset as jest.Mock<any, any>
|
||||||
|
mockTryReset.mockImplementation(async () => {
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await gitDirectoryHelper.prepareExistingDirectory(
|
||||||
|
git,
|
||||||
|
repositoryPath,
|
||||||
|
repositoryUrl,
|
||||||
|
clean,
|
||||||
|
ref,
|
||||||
|
true // preserveLocalChanges = true
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
|
expect(files.sort()).toEqual(['.git', 'my-file']) // Expect both .git and user files to remain
|
||||||
expect(git.tryClean).toHaveBeenCalled()
|
expect(git.tryClean).toHaveBeenCalled()
|
||||||
expect(git.tryReset).toHaveBeenCalled()
|
expect(git.tryReset).toHaveBeenCalled()
|
||||||
expect(core.warning).toHaveBeenCalled()
|
expect(core.warning).toHaveBeenCalled()
|
||||||
@@ -246,12 +338,13 @@ describe('git-directory-helper tests', () => {
|
|||||||
repositoryPath,
|
repositoryPath,
|
||||||
repositoryUrl,
|
repositoryUrl,
|
||||||
clean,
|
clean,
|
||||||
ref
|
ref,
|
||||||
|
false // preserveLocalChanges = false
|
||||||
)
|
)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
const files = await fs.promises.readdir(repositoryPath)
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
expect(files).toHaveLength(0)
|
expect(files).toEqual(['.git']) // Expect just the .git directory to remain
|
||||||
expect(core.warning).not.toHaveBeenCalled()
|
expect(core.warning).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -302,12 +395,13 @@ describe('git-directory-helper tests', () => {
|
|||||||
repositoryPath,
|
repositoryPath,
|
||||||
repositoryUrl,
|
repositoryUrl,
|
||||||
clean,
|
clean,
|
||||||
ref
|
ref,
|
||||||
|
false // preserveLocalChanges = false
|
||||||
)
|
)
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
const files = await fs.promises.readdir(repositoryPath)
|
const files = await fs.promises.readdir(repositoryPath)
|
||||||
expect(files).toHaveLength(0)
|
expect(files).toEqual(['.git']) // Expect just the .git directory to remain
|
||||||
expect(git.tryClean).toHaveBeenCalled()
|
expect(git.tryClean).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,10 @@ inputs:
|
|||||||
description: 'Relative path under $GITHUB_WORKSPACE to place the repository'
|
description: 'Relative path under $GITHUB_WORKSPACE to place the repository'
|
||||||
clean:
|
clean:
|
||||||
description: 'Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching'
|
description: 'Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching'
|
||||||
default: true
|
default: 'true'
|
||||||
|
preserve-local-changes:
|
||||||
|
description: 'Whether to preserve local changes during checkout. If true, tries to preserve local files that are not tracked by Git. By default, all files will be overwritten.'
|
||||||
|
default: 'false'
|
||||||
filter:
|
filter:
|
||||||
description: >
|
description: >
|
||||||
Partially clone against a given filter.
|
Partially clone against a given filter.
|
||||||
@@ -104,6 +107,6 @@ outputs:
|
|||||||
commit:
|
commit:
|
||||||
description: 'The commit SHA that was checked out'
|
description: 'The commit SHA that was checked out'
|
||||||
runs:
|
runs:
|
||||||
using: node20
|
using: node24
|
||||||
main: dist/index.js
|
main: dist/index.js
|
||||||
post: dist/index.js
|
post: dist/index.js
|
||||||
|
|||||||
144
dist/index.js
vendored
144
dist/index.js
vendored
@@ -609,9 +609,17 @@ class GitCommandManager {
|
|||||||
yield fs.promises.appendFile(sparseCheckoutPath, `\n${sparseCheckout.join('\n')}\n`);
|
yield fs.promises.appendFile(sparseCheckoutPath, `\n${sparseCheckout.join('\n')}\n`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
checkout(ref, startPoint) {
|
checkout(ref_1, startPoint_1) {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, arguments, void 0, function* (ref, startPoint, options = []) {
|
||||||
const args = ['checkout', '--progress', '--force'];
|
const args = ['checkout', '--progress'];
|
||||||
|
// Add custom options (like --merge) if provided
|
||||||
|
if (options.length > 0) {
|
||||||
|
args.push(...options);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Default behavior - use force
|
||||||
|
args.push('--force');
|
||||||
|
}
|
||||||
if (startPoint) {
|
if (startPoint) {
|
||||||
args.push('-B', ref, startPoint);
|
args.push('-B', ref, startPoint);
|
||||||
}
|
}
|
||||||
@@ -1025,13 +1033,17 @@ const fs = __importStar(__nccwpck_require__(7147));
|
|||||||
const fsHelper = __importStar(__nccwpck_require__(7219));
|
const fsHelper = __importStar(__nccwpck_require__(7219));
|
||||||
const io = __importStar(__nccwpck_require__(7436));
|
const io = __importStar(__nccwpck_require__(7436));
|
||||||
const path = __importStar(__nccwpck_require__(1017));
|
const path = __importStar(__nccwpck_require__(1017));
|
||||||
function prepareExistingDirectory(git, repositoryPath, repositoryUrl, clean, ref) {
|
function prepareExistingDirectory(git_1, repositoryPath_1, repositoryUrl_1, clean_1, ref_1) {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, arguments, void 0, function* (git, repositoryPath, repositoryUrl, clean, ref, preserveLocalChanges = false) {
|
||||||
var _a;
|
var _a;
|
||||||
assert.ok(repositoryPath, 'Expected repositoryPath to be defined');
|
assert.ok(repositoryPath, 'Expected repositoryPath to be defined');
|
||||||
assert.ok(repositoryUrl, 'Expected repositoryUrl to be defined');
|
assert.ok(repositoryUrl, 'Expected repositoryUrl to be defined');
|
||||||
// Indicates whether to delete the directory contents
|
// Indicates whether to delete the directory contents
|
||||||
let remove = false;
|
let remove = false;
|
||||||
|
// If preserveLocalChanges is true, log it
|
||||||
|
if (preserveLocalChanges) {
|
||||||
|
core.info(`Preserve local changes is enabled, will attempt to keep local files`);
|
||||||
|
}
|
||||||
// Check whether using git or REST API
|
// Check whether using git or REST API
|
||||||
if (!git) {
|
if (!git) {
|
||||||
remove = true;
|
remove = true;
|
||||||
@@ -1112,14 +1124,28 @@ function prepareExistingDirectory(git, repositoryPath, repositoryUrl, clean, ref
|
|||||||
remove = true;
|
remove = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (remove) {
|
if (remove && !preserveLocalChanges) {
|
||||||
// Delete the contents of the directory. Don't delete the directory itself
|
// Delete the contents of the directory. Don't delete the directory itself
|
||||||
// since it might be the current working directory.
|
// since it might be the current working directory.
|
||||||
core.info(`Deleting the contents of '${repositoryPath}'`);
|
core.info(`Deleting the contents of '${repositoryPath}'`);
|
||||||
for (const file of yield fs.promises.readdir(repositoryPath)) {
|
for (const file of yield fs.promises.readdir(repositoryPath)) {
|
||||||
|
// Skip .git directory as we need it to determine if a file is tracked
|
||||||
|
if (file === '.git') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
yield io.rmRF(path.join(repositoryPath, file));
|
yield io.rmRF(path.join(repositoryPath, file));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (remove && preserveLocalChanges) {
|
||||||
|
core.info(`Skipping deletion of directory contents due to preserve-local-changes setting`);
|
||||||
|
// We still need to make sure we have a git repository to work with
|
||||||
|
if (!git) {
|
||||||
|
core.info(`Initializing git repository to prepare for checkout with preserved changes`);
|
||||||
|
yield fs.promises.mkdir(path.join(repositoryPath, '.git'), {
|
||||||
|
recursive: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1216,7 +1242,7 @@ function getSource(settings) {
|
|||||||
}
|
}
|
||||||
// Prepare existing directory, otherwise recreate
|
// Prepare existing directory, otherwise recreate
|
||||||
if (isExisting) {
|
if (isExisting) {
|
||||||
yield gitDirectoryHelper.prepareExistingDirectory(git, settings.repositoryPath, repositoryUrl, settings.clean, settings.ref);
|
yield gitDirectoryHelper.prepareExistingDirectory(git, settings.repositoryPath, repositoryUrl, settings.clean, settings.ref, settings.preserveLocalChanges);
|
||||||
}
|
}
|
||||||
if (!git) {
|
if (!git) {
|
||||||
// Downloading using REST API
|
// Downloading using REST API
|
||||||
@@ -1329,7 +1355,104 @@ function getSource(settings) {
|
|||||||
}
|
}
|
||||||
// Checkout
|
// Checkout
|
||||||
core.startGroup('Checking out the ref');
|
core.startGroup('Checking out the ref');
|
||||||
yield git.checkout(checkoutInfo.ref, checkoutInfo.startPoint);
|
if (settings.preserveLocalChanges) {
|
||||||
|
core.info('Attempting to preserve local changes during checkout');
|
||||||
|
// List and store local files before checkout
|
||||||
|
const fs = __nccwpck_require__(7147);
|
||||||
|
const path = __nccwpck_require__(1017);
|
||||||
|
const localFiles = new Map();
|
||||||
|
try {
|
||||||
|
// Get all files in the workspace that aren't in the .git directory
|
||||||
|
const workspacePath = process.cwd();
|
||||||
|
core.info(`Current workspace path: ${workspacePath}`);
|
||||||
|
// List all files in the current directory using fs
|
||||||
|
const listFilesRecursively = (dir) => {
|
||||||
|
let results = [];
|
||||||
|
const list = fs.readdirSync(dir);
|
||||||
|
list.forEach((file) => {
|
||||||
|
const fullPath = path.join(dir, file);
|
||||||
|
const relativePath = path.relative(workspacePath, fullPath);
|
||||||
|
// Skip .git directory
|
||||||
|
if (relativePath.startsWith('.git'))
|
||||||
|
return;
|
||||||
|
const stat = fs.statSync(fullPath);
|
||||||
|
if (stat && stat.isDirectory()) {
|
||||||
|
// Recursively explore subdirectories
|
||||||
|
results = results.concat(listFilesRecursively(fullPath));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Store file content in memory
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(fullPath);
|
||||||
|
localFiles.set(relativePath, content);
|
||||||
|
results.push(relativePath);
|
||||||
|
}
|
||||||
|
catch (readErr) {
|
||||||
|
core.warning(`Failed to read file ${relativePath}: ${readErr}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
const localFilesList = listFilesRecursively(workspacePath);
|
||||||
|
core.info(`Found ${localFilesList.length} local files to preserve:`);
|
||||||
|
localFilesList.forEach(file => core.info(` - ${file}`));
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
core.warning(`Failed to list local files: ${error}`);
|
||||||
|
}
|
||||||
|
// Perform normal checkout
|
||||||
|
yield git.checkout(checkoutInfo.ref, checkoutInfo.startPoint);
|
||||||
|
// Restore local files that were not tracked by git
|
||||||
|
core.info('Restoring local files after checkout');
|
||||||
|
try {
|
||||||
|
let restoredCount = 0;
|
||||||
|
const execOptions = {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
silent: true,
|
||||||
|
ignoreReturnCode: true
|
||||||
|
};
|
||||||
|
for (const [filePath, content] of localFiles.entries()) {
|
||||||
|
// Check if file exists in git using a child process instead of git.execGit
|
||||||
|
const { exec } = __nccwpck_require__(1514);
|
||||||
|
let exitCode = 0;
|
||||||
|
const output = {
|
||||||
|
stdout: '',
|
||||||
|
stderr: ''
|
||||||
|
};
|
||||||
|
// Capture output
|
||||||
|
const options = Object.assign(Object.assign({}, execOptions), { listeners: {
|
||||||
|
stdout: (data) => {
|
||||||
|
output.stdout += data.toString();
|
||||||
|
},
|
||||||
|
stderr: (data) => {
|
||||||
|
output.stderr += data.toString();
|
||||||
|
}
|
||||||
|
} });
|
||||||
|
exitCode = yield exec('git', ['ls-files', '--error-unmatch', filePath], options);
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
// File is not tracked by git, safe to restore
|
||||||
|
const fullPath = path.join(process.cwd(), filePath);
|
||||||
|
// Ensure directory exists
|
||||||
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||||
|
fs.writeFileSync(fullPath, content);
|
||||||
|
core.info(`Restored local file: ${filePath}`);
|
||||||
|
restoredCount++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
core.info(`Skipping ${filePath} as it's tracked by git`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
core.info(`Successfully restored ${restoredCount} local files`);
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
core.warning(`Failed to restore local files: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Use the default behavior with --force
|
||||||
|
yield git.checkout(checkoutInfo.ref, checkoutInfo.startPoint);
|
||||||
|
}
|
||||||
core.endGroup();
|
core.endGroup();
|
||||||
// Submodules
|
// Submodules
|
||||||
if (settings.submodules) {
|
if (settings.submodules) {
|
||||||
@@ -1766,6 +1889,11 @@ function getInputs() {
|
|||||||
// Clean
|
// Clean
|
||||||
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE';
|
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE';
|
||||||
core.debug(`clean = ${result.clean}`);
|
core.debug(`clean = ${result.clean}`);
|
||||||
|
// Preserve local changes
|
||||||
|
result.preserveLocalChanges =
|
||||||
|
(core.getInput('preserve-local-changes') || 'false').toUpperCase() ===
|
||||||
|
'TRUE';
|
||||||
|
core.debug(`preserveLocalChanges = ${result.preserveLocalChanges}`);
|
||||||
// Filter
|
// Filter
|
||||||
const filter = core.getInput('filter');
|
const filter = core.getInput('filter');
|
||||||
if (filter) {
|
if (filter) {
|
||||||
|
|||||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "checkout",
|
"name": "checkout",
|
||||||
"version": "4.3.0",
|
"version": "5.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "checkout",
|
"name": "checkout",
|
||||||
"version": "4.3.0",
|
"version": "5.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "^20.12.12",
|
"@types/node": "^24.1.0",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.9.0",
|
"@typescript-eslint/eslint-plugin": "^7.9.0",
|
||||||
"@typescript-eslint/parser": "^7.9.0",
|
"@typescript-eslint/parser": "^7.9.0",
|
||||||
@@ -1515,12 +1515,12 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.12.12",
|
"version": "24.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
|
||||||
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
|
"integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~5.26.4"
|
"undici-types": "~7.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/stack-utils": {
|
"node_modules/@types/stack-utils": {
|
||||||
@@ -6865,9 +6865,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "5.26.5",
|
"version": "7.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
"integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/universal-user-agent": {
|
"node_modules/universal-user-agent": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "checkout",
|
"name": "checkout",
|
||||||
"version": "4.3.0",
|
"version": "5.0.0",
|
||||||
"description": "checkout action",
|
"description": "checkout action",
|
||||||
"main": "lib/main.js",
|
"main": "lib/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "^20.12.12",
|
"@types/node": "^24.1.0",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.9.0",
|
"@typescript-eslint/eslint-plugin": "^7.9.0",
|
||||||
"@typescript-eslint/parser": "^7.9.0",
|
"@typescript-eslint/parser": "^7.9.0",
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export interface IGitCommandManager {
|
|||||||
disableSparseCheckout(): Promise<void>
|
disableSparseCheckout(): Promise<void>
|
||||||
sparseCheckout(sparseCheckout: string[]): Promise<void>
|
sparseCheckout(sparseCheckout: string[]): Promise<void>
|
||||||
sparseCheckoutNonConeMode(sparseCheckout: string[]): Promise<void>
|
sparseCheckoutNonConeMode(sparseCheckout: string[]): Promise<void>
|
||||||
checkout(ref: string, startPoint: string): Promise<void>
|
checkout(ref: string, startPoint: string, options?: string[]): Promise<void>
|
||||||
checkoutDetach(): Promise<void>
|
checkoutDetach(): Promise<void>
|
||||||
config(
|
config(
|
||||||
configKey: string,
|
configKey: string,
|
||||||
@@ -203,8 +203,21 @@ class GitCommandManager {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkout(ref: string, startPoint: string): Promise<void> {
|
async checkout(
|
||||||
const args = ['checkout', '--progress', '--force']
|
ref: string,
|
||||||
|
startPoint: string,
|
||||||
|
options: string[] = []
|
||||||
|
): Promise<void> {
|
||||||
|
const args = ['checkout', '--progress']
|
||||||
|
|
||||||
|
// Add custom options (like --merge) if provided
|
||||||
|
if (options.length > 0) {
|
||||||
|
args.push(...options)
|
||||||
|
} else {
|
||||||
|
// Default behavior - use force
|
||||||
|
args.push('--force')
|
||||||
|
}
|
||||||
|
|
||||||
if (startPoint) {
|
if (startPoint) {
|
||||||
args.push('-B', ref, startPoint)
|
args.push('-B', ref, startPoint)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ export async function prepareExistingDirectory(
|
|||||||
repositoryPath: string,
|
repositoryPath: string,
|
||||||
repositoryUrl: string,
|
repositoryUrl: string,
|
||||||
clean: boolean,
|
clean: boolean,
|
||||||
ref: string
|
ref: string,
|
||||||
|
preserveLocalChanges: boolean = false
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
assert.ok(repositoryPath, 'Expected repositoryPath to be defined')
|
assert.ok(repositoryPath, 'Expected repositoryPath to be defined')
|
||||||
assert.ok(repositoryUrl, 'Expected repositoryUrl to be defined')
|
assert.ok(repositoryUrl, 'Expected repositoryUrl to be defined')
|
||||||
@@ -19,6 +20,13 @@ export async function prepareExistingDirectory(
|
|||||||
// Indicates whether to delete the directory contents
|
// Indicates whether to delete the directory contents
|
||||||
let remove = false
|
let remove = false
|
||||||
|
|
||||||
|
// If preserveLocalChanges is true, log it
|
||||||
|
if (preserveLocalChanges) {
|
||||||
|
core.info(
|
||||||
|
`Preserve local changes is enabled, will attempt to keep local files`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Check whether using git or REST API
|
// Check whether using git or REST API
|
||||||
if (!git) {
|
if (!git) {
|
||||||
remove = true
|
remove = true
|
||||||
@@ -114,12 +122,43 @@ export async function prepareExistingDirectory(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (remove) {
|
// Check repository conditions
|
||||||
|
let isLocalGitRepo = git && fsHelper.directoryExistsSync(path.join(repositoryPath, '.git'));
|
||||||
|
let repoUrl = isLocalGitRepo ? await git?.tryGetFetchUrl() : '';
|
||||||
|
let isSameRepository = repositoryUrl === repoUrl;
|
||||||
|
let differentRepoUrl = !isSameRepository;
|
||||||
|
|
||||||
|
// Repository URL has changed
|
||||||
|
if (differentRepoUrl) {
|
||||||
|
if (preserveLocalChanges) {
|
||||||
|
core.warning(`Repository URL has changed from '${repoUrl}' to '${repositoryUrl}'. Local changes will be preserved as requested.`);
|
||||||
|
}
|
||||||
|
remove = true; // Mark for removal, but actual removal will respect preserveLocalChanges
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remove && !preserveLocalChanges) {
|
||||||
// Delete the contents of the directory. Don't delete the directory itself
|
// Delete the contents of the directory. Don't delete the directory itself
|
||||||
// since it might be the current working directory.
|
// since it might be the current working directory.
|
||||||
core.info(`Deleting the contents of '${repositoryPath}'`)
|
core.info(`Deleting the contents of '${repositoryPath}'`)
|
||||||
for (const file of await fs.promises.readdir(repositoryPath)) {
|
for (const file of await fs.promises.readdir(repositoryPath)) {
|
||||||
|
// Skip .git directory as we need it to determine if a file is tracked
|
||||||
|
if (file === '.git') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
await io.rmRF(path.join(repositoryPath, file))
|
await io.rmRF(path.join(repositoryPath, file))
|
||||||
}
|
}
|
||||||
|
} else if (remove && preserveLocalChanges) {
|
||||||
|
core.info(
|
||||||
|
`Skipping deletion of directory contents due to preserve-local-changes setting`
|
||||||
|
)
|
||||||
|
// We still need to make sure we have a git repository to work with
|
||||||
|
if (!git) {
|
||||||
|
core.info(
|
||||||
|
`Initializing git repository to prepare for checkout with preserved changes`
|
||||||
|
)
|
||||||
|
await fs.promises.mkdir(path.join(repositoryPath, '.git'), {
|
||||||
|
recursive: true
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
|||||||
settings.repositoryPath,
|
settings.repositoryPath,
|
||||||
repositoryUrl,
|
repositoryUrl,
|
||||||
settings.clean,
|
settings.clean,
|
||||||
settings.ref
|
settings.ref,
|
||||||
|
settings.preserveLocalChanges
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +230,115 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
|||||||
|
|
||||||
// Checkout
|
// Checkout
|
||||||
core.startGroup('Checking out the ref')
|
core.startGroup('Checking out the ref')
|
||||||
await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
|
if (settings.preserveLocalChanges) {
|
||||||
|
core.info('Attempting to preserve local changes during checkout')
|
||||||
|
|
||||||
|
// List and store local files before checkout
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const localFiles = new Map()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all files in the workspace that aren't in the .git directory
|
||||||
|
const workspacePath = process.cwd()
|
||||||
|
core.info(`Current workspace path: ${workspacePath}`)
|
||||||
|
|
||||||
|
// List all files in the current directory using fs
|
||||||
|
const listFilesRecursively = (dir: string): string[] => {
|
||||||
|
let results: string[] = []
|
||||||
|
const list = fs.readdirSync(dir)
|
||||||
|
list.forEach((file: string) => {
|
||||||
|
const fullPath = path.join(dir, file)
|
||||||
|
const relativePath = path.relative(workspacePath, fullPath)
|
||||||
|
// Skip .git directory
|
||||||
|
if (relativePath.startsWith('.git')) return
|
||||||
|
|
||||||
|
const stat = fs.statSync(fullPath)
|
||||||
|
if (stat && stat.isDirectory()) {
|
||||||
|
// Recursively explore subdirectories
|
||||||
|
results = results.concat(listFilesRecursively(fullPath))
|
||||||
|
} else {
|
||||||
|
// Store file content in memory
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(fullPath)
|
||||||
|
localFiles.set(relativePath, content)
|
||||||
|
results.push(relativePath)
|
||||||
|
} catch (readErr) {
|
||||||
|
core.warning(`Failed to read file ${relativePath}: ${readErr}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
const localFilesList = listFilesRecursively(workspacePath)
|
||||||
|
core.info(`Found ${localFilesList.length} local files to preserve:`)
|
||||||
|
localFilesList.forEach(file => core.info(` - ${file}`))
|
||||||
|
} catch (error) {
|
||||||
|
core.warning(`Failed to list local files: ${error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform normal checkout
|
||||||
|
await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
|
||||||
|
|
||||||
|
// Restore local files that were not tracked by git
|
||||||
|
core.info('Restoring local files after checkout')
|
||||||
|
try {
|
||||||
|
let restoredCount = 0
|
||||||
|
const execOptions = {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
silent: true,
|
||||||
|
ignoreReturnCode: true
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [filePath, content] of localFiles.entries()) {
|
||||||
|
// Check if file exists in git using a child process instead of git.execGit
|
||||||
|
const {exec} = require('@actions/exec')
|
||||||
|
let exitCode = 0
|
||||||
|
const output = {
|
||||||
|
stdout: '',
|
||||||
|
stderr: ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture output
|
||||||
|
const options = {
|
||||||
|
...execOptions,
|
||||||
|
listeners: {
|
||||||
|
stdout: (data: Buffer) => {
|
||||||
|
output.stdout += data.toString()
|
||||||
|
},
|
||||||
|
stderr: (data: Buffer) => {
|
||||||
|
output.stderr += data.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exitCode = await exec(
|
||||||
|
'git',
|
||||||
|
['ls-files', '--error-unmatch', filePath],
|
||||||
|
options
|
||||||
|
)
|
||||||
|
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
// File is not tracked by git, safe to restore
|
||||||
|
const fullPath = path.join(process.cwd(), filePath)
|
||||||
|
// Ensure directory exists
|
||||||
|
fs.mkdirSync(path.dirname(fullPath), {recursive: true})
|
||||||
|
fs.writeFileSync(fullPath, content)
|
||||||
|
core.info(`Restored local file: ${filePath}`)
|
||||||
|
restoredCount++
|
||||||
|
} else {
|
||||||
|
core.info(`Skipping ${filePath} as it's tracked by git`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
core.info(`Successfully restored ${restoredCount} local files`)
|
||||||
|
} catch (error) {
|
||||||
|
core.warning(`Failed to restore local files: ${error}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use the default behavior with --force
|
||||||
|
await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
|
||||||
|
}
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
|
|
||||||
// Submodules
|
// Submodules
|
||||||
|
|||||||
@@ -25,10 +25,15 @@ export interface IGitSourceSettings {
|
|||||||
commit: string
|
commit: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates whether to clean the repository
|
* Whether to execute git clean and git reset before fetching
|
||||||
*/
|
*/
|
||||||
clean: boolean
|
clean: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to preserve local changes during checkout
|
||||||
|
*/
|
||||||
|
preserveLocalChanges: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The filter determining which objects to include
|
* The filter determining which objects to include
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ export async function getInputs(): Promise<IGitSourceSettings> {
|
|||||||
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'
|
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'
|
||||||
core.debug(`clean = ${result.clean}`)
|
core.debug(`clean = ${result.clean}`)
|
||||||
|
|
||||||
|
// Preserve local changes
|
||||||
|
result.preserveLocalChanges =
|
||||||
|
(core.getInput('preserve-local-changes') || 'false').toUpperCase() ===
|
||||||
|
'TRUE'
|
||||||
|
core.debug(`preserveLocalChanges = ${result.preserveLocalChanges}`)
|
||||||
|
|
||||||
// Filter
|
// Filter
|
||||||
const filter = core.getInput('filter')
|
const filter = core.getInput('filter')
|
||||||
if (filter) {
|
if (filter) {
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ function updateUsage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateUsage(
|
updateUsage(
|
||||||
'actions/checkout@v4',
|
'actions/checkout@v5',
|
||||||
path.join(__dirname, '..', '..', 'action.yml'),
|
path.join(__dirname, '..', '..', 'action.yml'),
|
||||||
path.join(__dirname, '..', '..', 'README.md')
|
path.join(__dirname, '..', '..', 'README.md')
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user