mirror of https://github.com/actions/checkout
Compare commits
10 Commits
c3c8a0719d
...
9d20889f09
| Author | SHA1 | Date |
|---|---|---|
|
|
9d20889f09 | |
|
|
1cce3390c2 | |
|
|
45abae3e9f | |
|
|
215f9562a1 | |
|
|
caa5717450 | |
|
|
630cdb3874 | |
|
|
6503dcd44c | |
|
|
ebd82bae91 | |
|
|
f04b821901 | |
|
|
45fe6460ed |
21
README.md
21
README.md
|
|
@ -112,6 +112,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: ''
|
||||||
|
|
@ -357,6 +363,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:
|
||||||
|
|
|
||||||
|
|
@ -1163,6 +1163,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',
|
||||||
|
|
|
||||||
|
|
@ -378,6 +378,59 @@ describe('Test fetchDepth and fetchTags options', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('repository initialization object format', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
|
||||||
|
jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('initializes SHA-256 repositories with the matching object format', async () => {
|
||||||
|
mockExec.mockImplementation((path, args, options) => {
|
||||||
|
if (args.includes('version')) {
|
||||||
|
options.listeners.stdout(Buffer.from('git version 2.50.1'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
||||||
|
|
||||||
|
git = await commandManager.createCommandManager('test', false, false)
|
||||||
|
|
||||||
|
await git.init('sha256')
|
||||||
|
|
||||||
|
expect(mockExec).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
['init', '--object-format=sha256', 'test'],
|
||||||
|
expect.any(Object)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('initializes SHA-1 repositories with existing default arguments', async () => {
|
||||||
|
mockExec.mockImplementation((path, args, options) => {
|
||||||
|
if (args.includes('version')) {
|
||||||
|
options.listeners.stdout(Buffer.from('git version 2.50.1'))
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
jest.spyOn(exec, 'exec').mockImplementation(mockExec)
|
||||||
|
|
||||||
|
git = await commandManager.createCommandManager('test', false, false)
|
||||||
|
|
||||||
|
await git.init('sha1')
|
||||||
|
|
||||||
|
expect(mockExec).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
['init', 'test'],
|
||||||
|
expect.any(Object)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('git user-agent with orchestration ID', () => {
|
describe('git user-agent with orchestration ID', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
|
jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as github from '@actions/github'
|
||||||
|
import * as githubApiHelper from '../lib/github-api-helper'
|
||||||
|
|
||||||
|
describe('github-api-helper object format', () => {
|
||||||
|
let getOctokitSpy: jest.SpyInstance
|
||||||
|
let debugSpy: jest.SpyInstance
|
||||||
|
let request: jest.Mock
|
||||||
|
|
||||||
|
function mockHashAlgorithmApi(hashAlgorithm: string): void {
|
||||||
|
request = jest.fn(async () => ({
|
||||||
|
data: {
|
||||||
|
hash_algorithm: hashAlgorithm
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
getOctokitSpy = jest.spyOn(github, 'getOctokit').mockReturnValue({
|
||||||
|
request
|
||||||
|
} as any)
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
debugSpy = jest.spyOn(core, 'debug').mockImplementation(jest.fn())
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detects SHA-256 from the repository hash algorithm endpoint', async () => {
|
||||||
|
mockHashAlgorithmApi('sha256')
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
|
||||||
|
).resolves.toEqual({format: 'sha256', succeeded: true})
|
||||||
|
|
||||||
|
expect(getOctokitSpy).toHaveBeenCalledWith(
|
||||||
|
'token',
|
||||||
|
expect.objectContaining({baseUrl: 'https://api.github.com'})
|
||||||
|
)
|
||||||
|
expect(request).toHaveBeenCalledWith(
|
||||||
|
'GET /repos/{owner}/{repo}/hash-algorithm',
|
||||||
|
{owner: 'owner', repo: 'repo'}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detects SHA-1 from the repository hash algorithm endpoint', async () => {
|
||||||
|
mockHashAlgorithmApi('sha1')
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
|
||||||
|
).resolves.toEqual({format: 'sha1', succeeded: true})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detects object format from an existing commit without API calls', async () => {
|
||||||
|
const commitSha =
|
||||||
|
'9422233ca7ee1b17f1e905d0e141faf0c401556c41cdc6acd71c6bd685da2e92'
|
||||||
|
getOctokitSpy = jest.spyOn(github, 'getOctokit')
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
githubApiHelper.tryGetRepositoryObjectFormat(
|
||||||
|
'token',
|
||||||
|
'owner',
|
||||||
|
'repo',
|
||||||
|
undefined,
|
||||||
|
commitSha
|
||||||
|
)
|
||||||
|
).resolves.toEqual({format: 'sha256', succeeded: true})
|
||||||
|
|
||||||
|
expect(getOctokitSpy).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns unsuccessful when the hash algorithm endpoint value is not recognized', async () => {
|
||||||
|
mockHashAlgorithmApi('unknown')
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
|
||||||
|
).resolves.toEqual({format: '', succeeded: false})
|
||||||
|
expect(debugSpy).toHaveBeenCalledWith(
|
||||||
|
'Unable to determine repository object format from hash-algorithm endpoint'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns unsuccessful when the hash algorithm API lookup fails', async () => {
|
||||||
|
request = jest.fn(async () => {
|
||||||
|
throw new Error('not found')
|
||||||
|
})
|
||||||
|
jest.spyOn(github, 'getOctokit').mockReturnValue({
|
||||||
|
request
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
githubApiHelper.tryGetRepositoryObjectFormat('token', 'owner', 'repo')
|
||||||
|
).resolves.toEqual({format: '', succeeded: false})
|
||||||
|
expect(debugSpy).toHaveBeenCalledWith(
|
||||||
|
'Unable to determine repository object format from hash-algorithm endpoint: not found'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -780,9 +780,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);
|
||||||
}
|
}
|
||||||
|
|
@ -896,9 +904,14 @@ class GitCommandManager {
|
||||||
getWorkingDirectory() {
|
getWorkingDirectory() {
|
||||||
return this.workingDirectory;
|
return this.workingDirectory;
|
||||||
}
|
}
|
||||||
init() {
|
init(objectFormat) {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
yield this.execGit(['init', this.workingDirectory]);
|
const args = ['init'];
|
||||||
|
if (objectFormat === 'sha256') {
|
||||||
|
args.push('--object-format=sha256');
|
||||||
|
}
|
||||||
|
args.push(this.workingDirectory);
|
||||||
|
yield this.execGit(args);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
isDetached() {
|
isDetached() {
|
||||||
|
|
@ -1276,13 +1289,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;
|
||||||
|
|
@ -1363,14 +1380,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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1467,7 +1498,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
|
||||||
|
|
@ -1486,8 +1517,17 @@ function getSource(settings) {
|
||||||
stateHelper.setRepositoryPath(settings.repositoryPath);
|
stateHelper.setRepositoryPath(settings.repositoryPath);
|
||||||
// Initialize the repository
|
// Initialize the repository
|
||||||
if (!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))) {
|
if (!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))) {
|
||||||
|
core.startGroup('Determining repository object format');
|
||||||
|
const objectFormatResult = yield githubApiHelper.tryGetRepositoryObjectFormat(settings.authToken, settings.repositoryOwner, settings.repositoryName, settings.githubServerUrl, settings.commit);
|
||||||
|
const objectFormat = objectFormatResult.succeeded
|
||||||
|
? objectFormatResult.format
|
||||||
|
: '';
|
||||||
|
if (objectFormat === 'sha256') {
|
||||||
|
core.info('Detected SHA-256 repository object format');
|
||||||
|
}
|
||||||
|
core.endGroup();
|
||||||
core.startGroup('Initializing the repository');
|
core.startGroup('Initializing the repository');
|
||||||
yield git.init();
|
yield git.init(objectFormat);
|
||||||
yield git.remoteAdd('origin', repositoryUrl);
|
yield git.remoteAdd('origin', repositoryUrl);
|
||||||
core.endGroup();
|
core.endGroup();
|
||||||
}
|
}
|
||||||
|
|
@ -1593,7 +1633,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) {
|
||||||
|
|
@ -1810,6 +1947,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
||||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||||
exports.downloadRepository = downloadRepository;
|
exports.downloadRepository = downloadRepository;
|
||||||
exports.getDefaultBranch = getDefaultBranch;
|
exports.getDefaultBranch = getDefaultBranch;
|
||||||
|
exports.tryGetRepositoryObjectFormat = tryGetRepositoryObjectFormat;
|
||||||
const assert = __importStar(__nccwpck_require__(9491));
|
const assert = __importStar(__nccwpck_require__(9491));
|
||||||
const core = __importStar(__nccwpck_require__(2186));
|
const core = __importStar(__nccwpck_require__(2186));
|
||||||
const fs = __importStar(__nccwpck_require__(7147));
|
const fs = __importStar(__nccwpck_require__(7147));
|
||||||
|
|
@ -1911,6 +2049,40 @@ function getDefaultBranch(authToken, owner, repo, baseUrl) {
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function tryGetRepositoryObjectFormat(authToken, owner, repo, baseUrl, commit) {
|
||||||
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
|
var _a;
|
||||||
|
const commitFormat = getObjectFormat(commit);
|
||||||
|
if (commitFormat) {
|
||||||
|
return { format: commitFormat, succeeded: true };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const octokit = github.getOctokit(authToken, {
|
||||||
|
baseUrl: (0, url_helper_1.getServerApiUrl)(baseUrl)
|
||||||
|
});
|
||||||
|
const response = yield octokit.request('GET /repos/{owner}/{repo}/hash-algorithm', { owner, repo });
|
||||||
|
const hashAlgorithm = response.data.hash_algorithm;
|
||||||
|
if (hashAlgorithm === 'sha256' || hashAlgorithm === 'sha1') {
|
||||||
|
return { format: hashAlgorithm, succeeded: true };
|
||||||
|
}
|
||||||
|
core.debug('Unable to determine repository object format from hash-algorithm endpoint');
|
||||||
|
return { format: '', succeeded: false };
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
core.debug(`Unable to determine repository object format from hash-algorithm endpoint: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
||||||
|
return { format: '', succeeded: false };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function getObjectFormat(sha) {
|
||||||
|
if (/^[0-9a-fA-F]{64}$/.test(sha || '')) {
|
||||||
|
return 'sha256';
|
||||||
|
}
|
||||||
|
if (/^[0-9a-fA-F]{40}$/.test(sha || '')) {
|
||||||
|
return 'sha1';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
function downloadArchive(authToken, owner, repo, ref, commit, baseUrl) {
|
function downloadArchive(authToken, owner, repo, ref, commit, baseUrl) {
|
||||||
return __awaiter(this, void 0, void 0, function* () {
|
return __awaiter(this, void 0, void 0, function* () {
|
||||||
const octokit = github.getOctokit(authToken, {
|
const octokit = github.getOctokit(authToken, {
|
||||||
|
|
@ -2030,6 +2202,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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -43,7 +43,7 @@ export interface IGitCommandManager {
|
||||||
getDefaultBranch(repositoryUrl: string): Promise<string>
|
getDefaultBranch(repositoryUrl: string): Promise<string>
|
||||||
getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
|
getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
|
||||||
getWorkingDirectory(): string
|
getWorkingDirectory(): string
|
||||||
init(): Promise<void>
|
init(objectFormat?: string): Promise<void>
|
||||||
isDetached(): Promise<boolean>
|
isDetached(): Promise<boolean>
|
||||||
lfsFetch(ref: string): Promise<void>
|
lfsFetch(ref: string): Promise<void>
|
||||||
lfsInstall(): Promise<void>
|
lfsInstall(): Promise<void>
|
||||||
|
|
@ -220,8 +220,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 {
|
||||||
|
|
@ -364,8 +377,14 @@ class GitCommandManager {
|
||||||
return this.workingDirectory
|
return this.workingDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(objectFormat?: string): Promise<void> {
|
||||||
await this.execGit(['init', this.workingDirectory])
|
const args = ['init']
|
||||||
|
if (objectFormat === 'sha256') {
|
||||||
|
args.push('--object-format=sha256')
|
||||||
|
}
|
||||||
|
args.push(this.workingDirectory)
|
||||||
|
|
||||||
|
await this.execGit(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
async isDetached(): Promise<boolean> {
|
async isDetached(): Promise<boolean> {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,8 +110,25 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
|
||||||
if (
|
if (
|
||||||
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
!fsHelper.directoryExistsSync(path.join(settings.repositoryPath, '.git'))
|
||||||
) {
|
) {
|
||||||
|
core.startGroup('Determining repository object format')
|
||||||
|
const objectFormatResult =
|
||||||
|
await githubApiHelper.tryGetRepositoryObjectFormat(
|
||||||
|
settings.authToken,
|
||||||
|
settings.repositoryOwner,
|
||||||
|
settings.repositoryName,
|
||||||
|
settings.githubServerUrl,
|
||||||
|
settings.commit
|
||||||
|
)
|
||||||
|
const objectFormat = objectFormatResult.succeeded
|
||||||
|
? objectFormatResult.format
|
||||||
|
: ''
|
||||||
|
if (objectFormat === 'sha256') {
|
||||||
|
core.info('Detected SHA-256 repository object format')
|
||||||
|
}
|
||||||
|
core.endGroup()
|
||||||
|
|
||||||
core.startGroup('Initializing the repository')
|
core.startGroup('Initializing the repository')
|
||||||
await git.init()
|
await git.init(objectFormat)
|
||||||
await git.remoteAdd('origin', repositoryUrl)
|
await git.remoteAdd('origin', repositoryUrl)
|
||||||
core.endGroup()
|
core.endGroup()
|
||||||
}
|
}
|
||||||
|
|
@ -251,7 +269,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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,11 @@ import {getServerApiUrl} from './url-helper'
|
||||||
|
|
||||||
const IS_WINDOWS = process.platform === 'win32'
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
|
|
||||||
|
export interface RepositoryObjectFormatResult {
|
||||||
|
format: string
|
||||||
|
succeeded: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export async function downloadRepository(
|
export async function downloadRepository(
|
||||||
authToken: string,
|
authToken: string,
|
||||||
owner: string,
|
owner: string,
|
||||||
|
|
@ -122,6 +127,53 @@ export async function getDefaultBranch(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function tryGetRepositoryObjectFormat(
|
||||||
|
authToken: string,
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
baseUrl?: string,
|
||||||
|
commit?: string
|
||||||
|
): Promise<RepositoryObjectFormatResult> {
|
||||||
|
const commitFormat = getObjectFormat(commit)
|
||||||
|
if (commitFormat) {
|
||||||
|
return {format: commitFormat, succeeded: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const octokit = github.getOctokit(authToken, {
|
||||||
|
baseUrl: getServerApiUrl(baseUrl)
|
||||||
|
})
|
||||||
|
const response = await octokit.request(
|
||||||
|
'GET /repos/{owner}/{repo}/hash-algorithm',
|
||||||
|
{owner, repo}
|
||||||
|
)
|
||||||
|
const hashAlgorithm = response.data.hash_algorithm
|
||||||
|
if (hashAlgorithm === 'sha256' || hashAlgorithm === 'sha1') {
|
||||||
|
return {format: hashAlgorithm, succeeded: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
core.debug(
|
||||||
|
'Unable to determine repository object format from hash-algorithm endpoint'
|
||||||
|
)
|
||||||
|
return {format: '', succeeded: false}
|
||||||
|
} catch (err) {
|
||||||
|
core.debug(
|
||||||
|
`Unable to determine repository object format from hash-algorithm endpoint: ${(err as any)?.message ?? err}`
|
||||||
|
)
|
||||||
|
return {format: '', succeeded: false}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getObjectFormat(sha?: string): string {
|
||||||
|
if (/^[0-9a-fA-F]{64}$/.test(sha || '')) {
|
||||||
|
return 'sha256'
|
||||||
|
}
|
||||||
|
if (/^[0-9a-fA-F]{40}$/.test(sha || '')) {
|
||||||
|
return 'sha1'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
async function downloadArchive(
|
async function downloadArchive(
|
||||||
authToken: string,
|
authToken: string,
|
||||||
owner: string,
|
owner: string,
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue