1
0
mirror of https://github.com/actions/checkout.git synced 2026-07-02 18:33:48 +08:00

Compare commits

..

4 Commits

Author SHA1 Message Date
eric sciple
8e8c483db8 Clarify v6 README (#2328) 2025-12-01 20:08:49 -06:00
eric sciple
033fa0dc0b Add worktree support for persist-credentials includeIf (#2327) 2025-12-01 19:53:23 -06:00
eric sciple
c2d88d3ecc Update all references from v5 and v4 to v6 (#2314)
- Updated README.md examples to reference @v6
- Updated all workflow files to use actions/checkout@v6
2025-11-23 19:32:55 -06:00
eric sciple
1af3b93b68 update readme/changelog for v6 (#2311) 2025-11-20 10:20:04 -06:00
21 changed files with 158 additions and 483 deletions

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v6
- name: Set Node.js 24.x
uses: actions/setup-node@v4

View File

@@ -39,7 +39,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.6
uses: actions/checkout@v6
- name: Initialize CodeQL
uses: github/codeql-action/init@v3

View File

@@ -9,6 +9,6 @@ jobs:
runs-on: ubuntu-latest
name: Check licenses
steps:
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v6
- run: npm ci
- run: npm run licensed-check

View File

@@ -14,7 +14,7 @@ jobs:
steps:
- name: Checking out
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Publish
id: publish
uses: actions/publish-immutable-action@0.0.3

View File

@@ -19,7 +19,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 24.x
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v6
- run: npm ci
- run: npm run build
- run: npm run format-check
@@ -37,7 +37,7 @@ jobs:
steps:
# Clone this repo
- name: Checkout
uses: actions/checkout@v4.1.6
uses: actions/checkout@v6
# Basic checkout
- name: Checkout basic
@@ -165,6 +165,22 @@ jobs:
- name: Verify submodules recursive
run: __test__/verify-submodules-recursive.sh
# Worktree credentials
- name: Checkout for worktree test
uses: ./
with:
path: worktree-test
- name: Verify worktree credentials
shell: bash
run: __test__/verify-worktree.sh worktree-test worktree-branch
# Worktree credentials in container step
- name: Verify worktree credentials in container step
if: runner.os == 'Linux'
uses: docker://bitnami/git:latest
with:
args: bash __test__/verify-worktree.sh worktree-test container-worktree-branch
# Basic checkout using REST API
- name: Remove basic
if: runner.os != 'windows'
@@ -202,7 +218,7 @@ jobs:
steps:
# Clone this repo
- name: Checkout
uses: actions/checkout@v4.1.6
uses: actions/checkout@v6
# Basic checkout using git
- name: Checkout basic
@@ -234,7 +250,7 @@ jobs:
steps:
# Clone this repo
- name: Checkout
uses: actions/checkout@v4.1.6
uses: actions/checkout@v6
# Basic checkout using git
- name: Checkout basic
@@ -264,7 +280,7 @@ jobs:
steps:
# Clone this repo
- name: Checkout
uses: actions/checkout@v4.1.6
uses: actions/checkout@v6
with:
path: localClone
@@ -291,8 +307,8 @@ jobs:
git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main
# needed to make checkout post cleanup succeed
- name: Fix Checkout v4
uses: actions/checkout@v4.1.6
- name: Fix Checkout v6
uses: actions/checkout@v6
with:
path: localClone
@@ -301,7 +317,7 @@ jobs:
steps:
# Clone this repo
- name: Checkout
uses: actions/checkout@v4.1.6
uses: actions/checkout@v6
with:
path: actions-checkout

View File

@@ -23,7 +23,7 @@ jobs:
# Note this update workflow can also be used as a rollback tool.
# For that reason, it's best to pin `actions/checkout` to a known, stable version
# (typically, about two releases back).
- uses: actions/checkout@v4.1.6
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Git config

View File

@@ -26,7 +26,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
# Use `docker/login-action` to log in to GHCR.io.
# Once published, the packages are scoped to the account defined here.

View File

@@ -1,10 +1,19 @@
# Changelog
## V5.0.0
## v6.0.0
* Persist creds to a separate file by @ericsciple in https://github.com/actions/checkout/pull/2286
* Update README to include Node.js 24 support details and requirements by @salmanmkc in https://github.com/actions/checkout/pull/2248
## v5.0.1
* Port v6 cleanup to v5 by @ericsciple in https://github.com/actions/checkout/pull/2301
## v5.0.0
* Update actions checkout to use node 24 by @salmanmkc in https://github.com/actions/checkout/pull/2226
## v4.3.1
* Port v6 cleanup to v4 by @ericsciple in https://github.com/actions/checkout/pull/2305
## V4.3.0
## v4.3.0
* 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
* Documentation update - add recommended permissions to Readme by @benwells in https://github.com/actions/checkout/pull/2043

View File

@@ -1,11 +1,12 @@
[![Build and Test](https://github.com/actions/checkout/actions/workflows/test.yml/badge.svg)](https://github.com/actions/checkout/actions/workflows/test.yml)
# Checkout v6-beta
# Checkout v6
## What's new
- Updated `persist-credentials` to store the credentials under `$RUNNER_TEMP` instead of directly in the local git config.
- This requires a minimum Actions Runner version of [v2.329.0](https://github.com/actions/runner/releases/tag/v2.329.0) to access the persisted credentials for [Docker container action](https://docs.github.com/en/actions/tutorials/use-containerized-services/create-a-docker-container-action) scenarios.
- Improved credential security: `persist-credentials` now stores credentials in a separate file under `$RUNNER_TEMP` instead of directly in `.git/config`
- No workflow changes required — `git fetch`, `git push`, etc. continue to work automatically
- Running authenticated git commands from a [Docker container action](https://docs.github.com/actions/sharing-automations/creating-actions/creating-a-docker-container-action) requires Actions Runner [v2.329.0](https://github.com/actions/runner/releases/tag/v2.329.0) or later
# Checkout v5
@@ -51,7 +52,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
<!-- start usage -->
```yaml
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
# Repository name with owner. For example, actions/checkout
# Default: ${{ github.repository }}
@@ -111,12 +112,6 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
# Default: true
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.
# Default: null
filter: ''
@@ -196,7 +191,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
## Fetch only the root files
```yaml
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
sparse-checkout: .
```
@@ -204,7 +199,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
## Fetch only the root files and `.github` and `src` folder
```yaml
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
sparse-checkout: |
.github
@@ -214,7 +209,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
## Fetch only a single file
```yaml
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
sparse-checkout: |
README.md
@@ -224,7 +219,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
## Fetch all history for all tags and branches
```yaml
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 0
```
@@ -232,7 +227,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
## Checkout a different branch
```yaml
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: my-branch
```
@@ -240,7 +235,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
## Checkout HEAD^
```yaml
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
fetch-depth: 2
- run: git checkout HEAD^
@@ -250,12 +245,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
```yaml
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
path: main
- name: Checkout tools repo
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: my-org/my-tools
path: my-tools
@@ -266,10 +261,10 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
```yaml
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Checkout tools repo
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: my-org/my-tools
path: my-tools
@@ -280,12 +275,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
```yaml
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
path: main
- name: Checkout private tools
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: my-org/my-private-tools
token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT
@@ -298,7 +293,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
## Checkout pull request HEAD commit instead of merge commit
```yaml
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
```
@@ -314,7 +309,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
```
## Push a commit using the built-in token
@@ -325,7 +320,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- run: |
date > generated.txt
# Note: the following account information will not work on GHES
@@ -347,7 +342,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ github.head_ref }}
- run: |
@@ -362,21 +357,6 @@ 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
## 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
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:

View File

@@ -1163,7 +1163,6 @@ async function setup(testName: string): Promise<void> {
submodules: false,
nestedSubmodules: false,
persistCredentials: true,
preserveLocalChanges: false,
ref: 'refs/heads/main',
repositoryName: 'my-repo',
repositoryOwner: 'my-org',

View File

@@ -143,41 +143,12 @@ describe('git-directory-helper tests', () => {
repositoryPath,
repositoryUrl,
clean,
ref,
false // preserveLocalChanges = false
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
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(files).toHaveLength(0)
expect(git.tryClean).toHaveBeenCalled()
expect(core.warning).toHaveBeenCalled()
expect(git.tryReset).not.toHaveBeenCalled()
@@ -199,50 +170,16 @@ describe('git-directory-helper tests', () => {
repositoryPath,
differentRepositoryUrl,
clean,
ref,
false // preserveLocalChanges = false
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
expect(files).toEqual(['.git']) // Expect just the .git directory to remain
expect(files).toHaveLength(0)
expect(core.warning).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 =
'removes contents when no git directory'
it(removesContentsWhenNoGitDirectory, async () => {
@@ -284,41 +221,12 @@ describe('git-directory-helper tests', () => {
repositoryPath,
repositoryUrl,
clean,
ref,
false // preserveLocalChanges = false
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
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(files).toHaveLength(0)
expect(git.tryClean).toHaveBeenCalled()
expect(git.tryReset).toHaveBeenCalled()
expect(core.warning).toHaveBeenCalled()
@@ -338,13 +246,12 @@ describe('git-directory-helper tests', () => {
repositoryPath,
repositoryUrl,
clean,
ref,
false // preserveLocalChanges = false
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
expect(files).toEqual(['.git']) // Expect just the .git directory to remain
expect(files).toHaveLength(0)
expect(core.warning).not.toHaveBeenCalled()
})
@@ -395,13 +302,12 @@ describe('git-directory-helper tests', () => {
repositoryPath,
repositoryUrl,
clean,
ref,
false // preserveLocalChanges = false
ref
)
// Assert
const files = await fs.promises.readdir(repositoryPath)
expect(files).toEqual(['.git']) // Expect just the .git directory to remain
expect(files).toHaveLength(0)
expect(git.tryClean).toHaveBeenCalled()
})

51
__test__/verify-worktree.sh Executable file
View File

@@ -0,0 +1,51 @@
#!/bin/bash
set -e
# Verify worktree credentials
# This test verifies that git credentials work in worktrees created after checkout
# Usage: verify-worktree.sh <checkout-path> <worktree-name>
CHECKOUT_PATH="$1"
WORKTREE_NAME="$2"
if [ -z "$CHECKOUT_PATH" ] || [ -z "$WORKTREE_NAME" ]; then
echo "Usage: verify-worktree.sh <checkout-path> <worktree-name>"
exit 1
fi
cd "$CHECKOUT_PATH"
# Add safe directory for container environments
git config --global --add safe.directory "*" 2>/dev/null || true
# Show the includeIf configuration
echo "Git config includeIf entries:"
git config --list --show-origin | grep -i include || true
# Create the worktree
echo "Creating worktree..."
git worktree add "../$WORKTREE_NAME" HEAD --detach
# Change to worktree directory
cd "../$WORKTREE_NAME"
# Verify we're in a worktree
echo "Verifying worktree gitdir:"
cat .git
# Verify credentials are available in worktree by checking extraheader is configured
echo "Checking credentials in worktree..."
if git config --list --show-origin | grep -q "extraheader"; then
echo "Credentials are configured in worktree"
else
echo "ERROR: Credentials are NOT configured in worktree"
echo "Full git config:"
git config --list --show-origin
exit 1
fi
# Verify fetch works in the worktree
echo "Fetching in worktree..."
git fetch origin
echo "Worktree credentials test passed!"

View File

@@ -56,10 +56,7 @@ inputs:
description: 'Relative path under $GITHUB_WORKSPACE to place the repository'
clean:
description: 'Whether to execute `git clean -ffdx && git reset --hard HEAD` before fetching'
default: 'true'
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'
default: true
filter:
description: >
Partially clone against a given filter.

150
dist/index.js vendored
View File

@@ -412,6 +412,9 @@ class GitAuthHelper {
// Configure host includeIf
const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`;
yield this.git.config(hostIncludeKey, credentialsConfigPath);
// Configure host includeIf for worktrees
const hostWorktreeIncludeKey = `includeIf.gitdir:${gitDir}/worktrees/*.path`;
yield this.git.config(hostWorktreeIncludeKey, credentialsConfigPath);
// Container git directory
const workingDirectory = this.git.getWorkingDirectory();
const githubWorkspace = process.env['GITHUB_WORKSPACE'];
@@ -424,6 +427,9 @@ class GitAuthHelper {
// Configure container includeIf
const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`;
yield this.git.config(containerIncludeKey, containerCredentialsPath);
// Configure container includeIf for worktrees
const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path`;
yield this.git.config(containerWorktreeIncludeKey, containerCredentialsPath);
}
});
}
@@ -775,17 +781,9 @@ class GitCommandManager {
yield fs.promises.appendFile(sparseCheckoutPath, `\n${sparseCheckout.join('\n')}\n`);
});
}
checkout(ref_1, startPoint_1) {
return __awaiter(this, arguments, void 0, function* (ref, startPoint, options = []) {
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');
}
checkout(ref, startPoint) {
return __awaiter(this, void 0, void 0, function* () {
const args = ['checkout', '--progress', '--force'];
if (startPoint) {
args.push('-B', ref, startPoint);
}
@@ -1269,17 +1267,13 @@ const fs = __importStar(__nccwpck_require__(7147));
const fsHelper = __importStar(__nccwpck_require__(7219));
const io = __importStar(__nccwpck_require__(7436));
const path = __importStar(__nccwpck_require__(1017));
function prepareExistingDirectory(git_1, repositoryPath_1, repositoryUrl_1, clean_1, ref_1) {
return __awaiter(this, arguments, void 0, function* (git, repositoryPath, repositoryUrl, clean, ref, preserveLocalChanges = false) {
function prepareExistingDirectory(git, repositoryPath, repositoryUrl, clean, ref) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
assert.ok(repositoryPath, 'Expected repositoryPath to be defined');
assert.ok(repositoryUrl, 'Expected repositoryUrl to be defined');
// Indicates whether to delete the directory contents
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
if (!git) {
remove = true;
@@ -1360,28 +1354,14 @@ function prepareExistingDirectory(git_1, repositoryPath_1, repositoryUrl_1, clea
remove = true;
}
}
if (remove && !preserveLocalChanges) {
if (remove) {
// Delete the contents of the directory. Don't delete the directory itself
// since it might be the current working directory.
core.info(`Deleting the contents of '${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));
}
}
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
});
}
}
});
}
@@ -1478,7 +1458,7 @@ function getSource(settings) {
}
// Prepare existing directory, otherwise recreate
if (isExisting) {
yield gitDirectoryHelper.prepareExistingDirectory(git, settings.repositoryPath, repositoryUrl, settings.clean, settings.ref, settings.preserveLocalChanges);
yield gitDirectoryHelper.prepareExistingDirectory(git, settings.repositoryPath, repositoryUrl, settings.clean, settings.ref);
}
if (!git) {
// Downloading using REST API
@@ -1591,104 +1571,7 @@ function getSource(settings) {
}
// Checkout
core.startGroup('Checking out the ref');
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);
}
yield git.checkout(checkoutInfo.ref, checkoutInfo.startPoint);
core.endGroup();
// Submodules
if (settings.submodules) {
@@ -2125,11 +2008,6 @@ function getInputs() {
// Clean
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE';
core.debug(`clean = ${result.clean}`);
// Preserve local changes
result.preserveLocalChanges =
(core.getInput('preserve-local-changes') || 'false').toUpperCase() ===
'TRUE';
core.debug(`preserveLocalChanges = ${result.preserveLocalChanges}`);
// Filter
const filter = core.getInput('filter');
if (filter) {

View File

@@ -374,6 +374,10 @@ class GitAuthHelper {
const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`
await this.git.config(hostIncludeKey, credentialsConfigPath)
// Configure host includeIf for worktrees
const hostWorktreeIncludeKey = `includeIf.gitdir:${gitDir}/worktrees/*.path`
await this.git.config(hostWorktreeIncludeKey, credentialsConfigPath)
// Container git directory
const workingDirectory = this.git.getWorkingDirectory()
const githubWorkspace = process.env['GITHUB_WORKSPACE']
@@ -395,6 +399,13 @@ class GitAuthHelper {
// Configure container includeIf
const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`
await this.git.config(containerIncludeKey, containerCredentialsPath)
// Configure container includeIf for worktrees
const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path`
await this.git.config(
containerWorktreeIncludeKey,
containerCredentialsPath
)
}
}

View File

@@ -22,7 +22,7 @@ export interface IGitCommandManager {
disableSparseCheckout(): Promise<void>
sparseCheckout(sparseCheckout: string[]): Promise<void>
sparseCheckoutNonConeMode(sparseCheckout: string[]): Promise<void>
checkout(ref: string, startPoint: string, options?: string[]): Promise<void>
checkout(ref: string, startPoint: string): Promise<void>
checkoutDetach(): Promise<void>
config(
configKey: string,
@@ -221,21 +221,8 @@ class GitCommandManager {
)
}
async checkout(
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')
}
async checkout(ref: string, startPoint: string): Promise<void> {
const args = ['checkout', '--progress', '--force']
if (startPoint) {
args.push('-B', ref, startPoint)
} else {

View File

@@ -11,8 +11,7 @@ export async function prepareExistingDirectory(
repositoryPath: string,
repositoryUrl: string,
clean: boolean,
ref: string,
preserveLocalChanges: boolean = false
ref: string
): Promise<void> {
assert.ok(repositoryPath, 'Expected repositoryPath to be defined')
assert.ok(repositoryUrl, 'Expected repositoryUrl to be defined')
@@ -20,13 +19,6 @@ export async function prepareExistingDirectory(
// Indicates whether to delete the directory contents
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
if (!git) {
remove = true
@@ -122,43 +114,12 @@ export async function prepareExistingDirectory(
}
}
// 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) {
if (remove) {
// Delete the contents of the directory. Don't delete the directory itself
// since it might be the current working directory.
core.info(`Deleting the contents of '${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))
}
} 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
})
}
}
}

View File

@@ -70,8 +70,7 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
settings.repositoryPath,
repositoryUrl,
settings.clean,
settings.ref,
settings.preserveLocalChanges
settings.ref
)
}
@@ -230,115 +229,7 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
// Checkout
core.startGroup('Checking out the ref')
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)
}
await git.checkout(checkoutInfo.ref, checkoutInfo.startPoint)
core.endGroup()
// Submodules

View File

@@ -25,15 +25,10 @@ export interface IGitSourceSettings {
commit: string
/**
* Whether to execute git clean and git reset before fetching
* Indicates whether to clean the repository
*/
clean: boolean
/**
* Whether to preserve local changes during checkout
*/
preserveLocalChanges: boolean
/**
* The filter determining which objects to include
*/

View File

@@ -82,12 +82,6 @@ export async function getInputs(): Promise<IGitSourceSettings> {
result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'
core.debug(`clean = ${result.clean}`)
// Preserve local changes
result.preserveLocalChanges =
(core.getInput('preserve-local-changes') || 'false').toUpperCase() ===
'TRUE'
core.debug(`preserveLocalChanges = ${result.preserveLocalChanges}`)
// Filter
const filter = core.getInput('filter')
if (filter) {

View File

@@ -120,7 +120,7 @@ function updateUsage(
}
updateUsage(
'actions/checkout@v5',
'actions/checkout@v6',
path.join(__dirname, '..', '..', 'action.yml'),
path.join(__dirname, '..', '..', 'README.md')
)