mirror of
				https://github.com/actions/checkout.git
				synced 2025-10-31 19:33:35 +00:00 
			
		
		
		
	Merge 45abae3e9f3df5893d661cb11371ae3124e5bfe1 into ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
This commit is contained in:
		
						commit
						6452e1970f
					
				
							
								
								
									
										21
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								README.md
									
									
									
									
									
								
							| @ -104,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: '' | ||||||
| @ -349,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. | ||||||
|  | |||||||
							
								
								
									
										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) { | ||||||
|  | |||||||
| @ -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) { | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user