From 91b3c4bfa1eab021eb5e406f54dff29d0882f479 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:12:52 +0000 Subject: [PATCH 01/21] Initial plan From 8a6be9adc2c5884560f22f1a23892194941070a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:16:59 +0000 Subject: [PATCH 02/21] Add automatic git sync before commit when local is behind remote Co-authored-by: pierzchala-m <162727606+pierzchala-m@users.noreply.github.com> --- src/containers/job/JobManagerContainer.ts | 41 +++++++++++++++++++++++ src/git/GitScanner.ts | 22 ++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/containers/job/JobManagerContainer.ts b/src/containers/job/JobManagerContainer.ts index ff0d9f11..82dbd636 100644 --- a/src/containers/job/JobManagerContainer.ts +++ b/src/containers/job/JobManagerContainer.ts @@ -689,6 +689,47 @@ export class JobManagerContainer extends Container { const userConfigService = new UserConfigService(googleFileSystem); const userConfig = await userConfigService.load(); + // Check if local branch is behind remote and sync if needed + if (userConfig.remote_branch) { + try { + await gitScanner.fetch({ + privateKeyFile: await userConfigService.getDeployPrivateKeyPath() + }); + + const { ahead, behind } = await gitScanner.countAheadBehind(userConfig.remote_branch); + + if (behind > 0) { + logger.info(`Local branch is ${behind} commit(s) behind remote. Syncing before commit...`); + + // Stash local changes + await gitScanner.stashChanges(); + + try { + // Pull with rebase + await gitScanner.pullBranch(userConfig.remote_branch, { + privateKeyFile: await userConfigService.getDeployPrivateKeyPath() + }); + + // Apply stashed changes + await gitScanner.stashPop(); + } catch (err) { + // If pull or stash pop fails, try to restore stash + try { + await gitScanner.stashPop(); + } catch (stashErr) { + logger.error('Failed to restore stashed changes: ' + stashErr.message); + } + throw err; + } + } + } catch (err) { + if (err.message.indexOf('Failed to retrieve list of SSH authentication methods') > -1) { + throw new Error('Failed to authenticate with remote repository'); + } + throw err; + } + } + const contentFileService = await getContentFileService(transformedFileSystem, userConfigService); const markdownTreeProcessor = new MarkdownTreeProcessor(contentFileService); await markdownTreeProcessor.load(); diff --git a/src/git/GitScanner.ts b/src/git/GitScanner.ts index 6dadb272..2c04da1e 100644 --- a/src/git/GitScanner.ts +++ b/src/git/GitScanner.ts @@ -1013,4 +1013,26 @@ export class GitScanner { this.companionFileResolver = resolver; } + async stashChanges(): Promise { + try { + await this.exec('git stash push -u -m "WikiGDrive auto-stash before sync"', { skipLogger: !this.debug }); + } catch (err) { + // If there's nothing to stash, git stash will return "No local changes to save" + if (err.message.indexOf('No local changes to save') === -1) { + throw err; + } + } + } + + async stashPop(): Promise { + try { + await this.exec('git stash pop', { skipLogger: !this.debug }); + } catch (err) { + // If there's no stash to pop, handle gracefully + if (err.message.indexOf('No stash entries found') === -1) { + throw err; + } + } + } + } From e49a947b21ef2cfa2c50a681b795a4094aa36d69 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:18:18 +0000 Subject: [PATCH 03/21] Add tests for stash functionality and commit-behind-remote scenario Co-authored-by: pierzchala-m <162727606+pierzchala-m@users.noreply.github.com> --- test/git/GitTest.ts | 104 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/test/git/GitTest.ts b/test/git/GitTest.ts index d7f069b9..c44ed46f 100644 --- a/test/git/GitTest.ts +++ b/test/git/GitTest.ts @@ -807,3 +807,107 @@ Deno.test('test remove assets not file', async () => { fs.rmSync(localRepoDir, { recursive: true, force: true }); } }); + +Deno.test('test stash and pop', async () => { + const localRepoDir: string = createTmpDir(); + + try { + const scannerLocal = new GitScanner(logger, localRepoDir, COMMITER1.email); + await scannerLocal.initialize(); + + fs.writeFileSync(path.join(localRepoDir, 'file1.md'), 'Initial content'); + await scannerLocal.commit('First commit', ['.gitignore', 'file1.md'], COMMITER1); + + // Create a local change + fs.writeFileSync(path.join(localRepoDir, 'file2.md'), 'New file'); + + { + const changes = await scannerLocal.changes(); + assertStrictEquals(changes.length, 1); + assertStrictEquals(changes[0].path, 'file2.md'); + } + + // Stash changes + await scannerLocal.stashChanges(); + + { + const changes = await scannerLocal.changes(); + assertStrictEquals(changes.length, 0); + } + + // Pop stashed changes + await scannerLocal.stashPop(); + + { + const changes = await scannerLocal.changes(); + assertStrictEquals(changes.length, 1); + assertStrictEquals(changes[0].path, 'file2.md'); + } + + } finally { + fs.rmSync(localRepoDir, { recursive: true, force: true }); + } +}); + +Deno.test('test commit with local behind remote', async () => { + const localRepoDir: string = createTmpDir(); + const githubRepoDir: string = createTmpDir(); + const secondRepoDir: string = createTmpDir(); + + try { + execSync(`git init -b main --bare ${githubRepoDir}`); + + // Setup first repo + const scannerLocal = new GitScanner(logger, localRepoDir, COMMITER1.email); + await scannerLocal.initialize(); + + fs.writeFileSync(path.join(localRepoDir, 'file1.md'), 'Initial content'); + await scannerLocal.commit('First commit', ['.gitignore', 'file1.md'], COMMITER1); + + await scannerLocal.setRemoteUrl(githubRepoDir); + await scannerLocal.pushBranch('main'); + + // Setup second repo and make a commit + const scannerSecond = new GitScanner(logger, secondRepoDir, COMMITER2.email); + await scannerSecond.initialize(); + fs.unlinkSync(secondRepoDir + '/.gitignore'); + await scannerSecond.setRemoteUrl(githubRepoDir); + await scannerSecond.pullBranch('main'); + + fs.writeFileSync(path.join(secondRepoDir, 'file2.md'), 'Second repo change'); + await scannerSecond.commit('Second commit', ['file2.md'], COMMITER2); + await scannerSecond.pushBranch('main'); + + // First repo now has local changes but is behind remote + fs.writeFileSync(path.join(localRepoDir, 'file3.md'), 'Local change'); + + // Fetch to make remote refs available + await scannerLocal.fetch(); + + const { ahead, behind } = await scannerLocal.countAheadBehind('main'); + assertStrictEquals(ahead, 0); + assertStrictEquals(behind, 1); + + // Test stash, pull, and pop workflow + await scannerLocal.stashChanges(); + await scannerLocal.pullBranch('main'); + await scannerLocal.stashPop(); + + // Verify we now have both files + assertStrictEquals(fs.existsSync(path.join(localRepoDir, 'file2.md')), true); + assertStrictEquals(fs.existsSync(path.join(localRepoDir, 'file3.md')), true); + + // Verify we're now up to date + const { ahead: newAhead, behind: newBehind } = await scannerLocal.countAheadBehind('main'); + assertStrictEquals(newAhead, 0); + assertStrictEquals(newBehind, 0); + + // Now we can commit successfully + await scannerLocal.commit('Third commit', ['file3.md'], COMMITER1); + + } finally { + fs.rmSync(localRepoDir, { recursive: true, force: true }); + fs.rmSync(githubRepoDir, { recursive: true, force: true }); + fs.rmSync(secondRepoDir, { recursive: true, force: true }); + } +}); From 43a782b17d3dc5e97367c6ae698c510d4b1ba6f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:19:10 +0000 Subject: [PATCH 04/21] Address code review feedback: improve error handling Co-authored-by: pierzchala-m <162727606+pierzchala-m@users.noreply.github.com> --- src/containers/job/JobManagerContainer.ts | 16 ++++++++++------ src/git/GitScanner.ts | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/containers/job/JobManagerContainer.ts b/src/containers/job/JobManagerContainer.ts index 82dbd636..9f91e3ac 100644 --- a/src/containers/job/JobManagerContainer.ts +++ b/src/containers/job/JobManagerContainer.ts @@ -703,6 +703,7 @@ export class JobManagerContainer extends Container { // Stash local changes await gitScanner.stashChanges(); + let stashed = true; try { // Pull with rebase @@ -712,18 +713,21 @@ export class JobManagerContainer extends Container { // Apply stashed changes await gitScanner.stashPop(); + stashed = false; } catch (err) { - // If pull or stash pop fails, try to restore stash - try { - await gitScanner.stashPop(); - } catch (stashErr) { - logger.error('Failed to restore stashed changes: ' + stashErr.message); + // If pull or stash pop fails, try to restore stash if it's still there + if (stashed) { + try { + await gitScanner.stashPop(); + } catch (stashErr) { + logger.error('Failed to restore stashed changes: ' + (stashErr?.message || stashErr)); + } } throw err; } } } catch (err) { - if (err.message.indexOf('Failed to retrieve list of SSH authentication methods') > -1) { + if (err?.message && err.message.indexOf('Failed to retrieve list of SSH authentication methods') > -1) { throw new Error('Failed to authenticate with remote repository'); } throw err; diff --git a/src/git/GitScanner.ts b/src/git/GitScanner.ts index 2c04da1e..36270d06 100644 --- a/src/git/GitScanner.ts +++ b/src/git/GitScanner.ts @@ -1018,7 +1018,7 @@ export class GitScanner { await this.exec('git stash push -u -m "WikiGDrive auto-stash before sync"', { skipLogger: !this.debug }); } catch (err) { // If there's nothing to stash, git stash will return "No local changes to save" - if (err.message.indexOf('No local changes to save') === -1) { + if (err?.message && err.message.indexOf('No local changes to save') === -1) { throw err; } } @@ -1029,7 +1029,7 @@ export class GitScanner { await this.exec('git stash pop', { skipLogger: !this.debug }); } catch (err) { // If there's no stash to pop, handle gracefully - if (err.message.indexOf('No stash entries found') === -1) { + if (err?.message && err.message.indexOf('No stash entries found') === -1) { throw err; } } From e44dc414ba7a09daa04b5523639d723a42c63895 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:19:58 +0000 Subject: [PATCH 05/21] Return stash status and improve error messages Co-authored-by: pierzchala-m <162727606+pierzchala-m@users.noreply.github.com> --- src/containers/job/JobManagerContainer.ts | 16 ++++++++-------- src/git/GitScanner.ts | 8 +++++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/containers/job/JobManagerContainer.ts b/src/containers/job/JobManagerContainer.ts index 9f91e3ac..10ee92a0 100644 --- a/src/containers/job/JobManagerContainer.ts +++ b/src/containers/job/JobManagerContainer.ts @@ -701,9 +701,8 @@ export class JobManagerContainer extends Container { if (behind > 0) { logger.info(`Local branch is ${behind} commit(s) behind remote. Syncing before commit...`); - // Stash local changes - await gitScanner.stashChanges(); - let stashed = true; + // Stash local changes - returns true if something was stashed + const stashed = await gitScanner.stashChanges(); try { // Pull with rebase @@ -711,16 +710,17 @@ export class JobManagerContainer extends Container { privateKeyFile: await userConfigService.getDeployPrivateKeyPath() }); - // Apply stashed changes - await gitScanner.stashPop(); - stashed = false; + // Apply stashed changes if we stashed something + if (stashed) { + await gitScanner.stashPop(); + } } catch (err) { - // If pull or stash pop fails, try to restore stash if it's still there + // If pull or stash pop fails, try to restore stash if we created one if (stashed) { try { await gitScanner.stashPop(); } catch (stashErr) { - logger.error('Failed to restore stashed changes: ' + (stashErr?.message || stashErr)); + logger.error('Failed to restore stashed changes after sync error: ' + (stashErr?.message || stashErr)); } } throw err; diff --git a/src/git/GitScanner.ts b/src/git/GitScanner.ts index 36270d06..7733061c 100644 --- a/src/git/GitScanner.ts +++ b/src/git/GitScanner.ts @@ -1013,14 +1013,16 @@ export class GitScanner { this.companionFileResolver = resolver; } - async stashChanges(): Promise { + async stashChanges(): Promise { try { await this.exec('git stash push -u -m "WikiGDrive auto-stash before sync"', { skipLogger: !this.debug }); + return true; } catch (err) { // If there's nothing to stash, git stash will return "No local changes to save" - if (err?.message && err.message.indexOf('No local changes to save') === -1) { - throw err; + if (err?.message && err.message.indexOf('No local changes to save') > -1) { + return false; } + throw err; } } From 42260b283292eae86dbce930252fd31c454d0098 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:20:56 +0000 Subject: [PATCH 06/21] Fix stashPop logic and verify stash return value in tests Co-authored-by: pierzchala-m <162727606+pierzchala-m@users.noreply.github.com> --- src/git/GitScanner.ts | 5 +++-- test/git/GitTest.ts | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/git/GitScanner.ts b/src/git/GitScanner.ts index 7733061c..769ea68d 100644 --- a/src/git/GitScanner.ts +++ b/src/git/GitScanner.ts @@ -1031,9 +1031,10 @@ export class GitScanner { await this.exec('git stash pop', { skipLogger: !this.debug }); } catch (err) { // If there's no stash to pop, handle gracefully - if (err?.message && err.message.indexOf('No stash entries found') === -1) { - throw err; + if (err?.message && err.message.indexOf('No stash entries found') > -1) { + return; } + throw err; } } diff --git a/test/git/GitTest.ts b/test/git/GitTest.ts index c44ed46f..79331297 100644 --- a/test/git/GitTest.ts +++ b/test/git/GitTest.ts @@ -828,7 +828,8 @@ Deno.test('test stash and pop', async () => { } // Stash changes - await scannerLocal.stashChanges(); + const stashed = await scannerLocal.stashChanges(); + assertStrictEquals(stashed, true); { const changes = await scannerLocal.changes(); @@ -889,7 +890,8 @@ Deno.test('test commit with local behind remote', async () => { assertStrictEquals(behind, 1); // Test stash, pull, and pop workflow - await scannerLocal.stashChanges(); + const stashed = await scannerLocal.stashChanges(); + assertStrictEquals(stashed, true); await scannerLocal.pullBranch('main'); await scannerLocal.stashPop(); From 6e69daaf4ba724ae4e8f43c5e8d83a94fc0affd3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:21:47 +0000 Subject: [PATCH 07/21] Use includes() for better readability and improve error recovery Co-authored-by: pierzchala-m <162727606+pierzchala-m@users.noreply.github.com> --- src/containers/job/JobManagerContainer.ts | 11 ++++------- src/git/GitScanner.ts | 4 ++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/containers/job/JobManagerContainer.ts b/src/containers/job/JobManagerContainer.ts index 10ee92a0..84a5dd68 100644 --- a/src/containers/job/JobManagerContainer.ts +++ b/src/containers/job/JobManagerContainer.ts @@ -715,19 +715,16 @@ export class JobManagerContainer extends Container { await gitScanner.stashPop(); } } catch (err) { - // If pull or stash pop fails, try to restore stash if we created one + // If pull fails, leave stash intact for manual recovery + // The user can use "Reset and Pull" to clean up if (stashed) { - try { - await gitScanner.stashPop(); - } catch (stashErr) { - logger.error('Failed to restore stashed changes after sync error: ' + (stashErr?.message || stashErr)); - } + logger.warn('Pull failed. Stashed changes remain saved for manual recovery. Use "Reset and Pull" to clean up.'); } throw err; } } } catch (err) { - if (err?.message && err.message.indexOf('Failed to retrieve list of SSH authentication methods') > -1) { + if (err?.message && err.message.includes('Failed to retrieve list of SSH authentication methods')) { throw new Error('Failed to authenticate with remote repository'); } throw err; diff --git a/src/git/GitScanner.ts b/src/git/GitScanner.ts index 769ea68d..10a10dee 100644 --- a/src/git/GitScanner.ts +++ b/src/git/GitScanner.ts @@ -1019,7 +1019,7 @@ export class GitScanner { return true; } catch (err) { // If there's nothing to stash, git stash will return "No local changes to save" - if (err?.message && err.message.indexOf('No local changes to save') > -1) { + if (err?.message && err.message.includes('No local changes to save')) { return false; } throw err; @@ -1031,7 +1031,7 @@ export class GitScanner { await this.exec('git stash pop', { skipLogger: !this.debug }); } catch (err) { // If there's no stash to pop, handle gracefully - if (err?.message && err.message.indexOf('No stash entries found') > -1) { + if (err?.message && err.message.includes('No stash entries found')) { return; } throw err; From 0288a9e4a45fb29dc1290736498c5fd4ace820b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:22:30 +0000 Subject: [PATCH 08/21] Remove redundant null checks with optional chaining Co-authored-by: pierzchala-m <162727606+pierzchala-m@users.noreply.github.com> --- src/containers/job/JobManagerContainer.ts | 2 +- src/git/GitScanner.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/containers/job/JobManagerContainer.ts b/src/containers/job/JobManagerContainer.ts index 84a5dd68..d9c39507 100644 --- a/src/containers/job/JobManagerContainer.ts +++ b/src/containers/job/JobManagerContainer.ts @@ -724,7 +724,7 @@ export class JobManagerContainer extends Container { } } } catch (err) { - if (err?.message && err.message.includes('Failed to retrieve list of SSH authentication methods')) { + if (err?.message?.includes('Failed to retrieve list of SSH authentication methods')) { throw new Error('Failed to authenticate with remote repository'); } throw err; diff --git a/src/git/GitScanner.ts b/src/git/GitScanner.ts index 10a10dee..9b97658e 100644 --- a/src/git/GitScanner.ts +++ b/src/git/GitScanner.ts @@ -1019,7 +1019,7 @@ export class GitScanner { return true; } catch (err) { // If there's nothing to stash, git stash will return "No local changes to save" - if (err?.message && err.message.includes('No local changes to save')) { + if (err?.message?.includes('No local changes to save')) { return false; } throw err; @@ -1031,7 +1031,7 @@ export class GitScanner { await this.exec('git stash pop', { skipLogger: !this.debug }); } catch (err) { // If there's no stash to pop, handle gracefully - if (err?.message && err.message.includes('No stash entries found')) { + if (err?.message?.includes('No stash entries found')) { return; } throw err; From 733f8efb72a158258c5149208b4d9d4ade4b2aa8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:23:07 +0000 Subject: [PATCH 09/21] Improve error messages with better context and guidance Co-authored-by: pierzchala-m <162727606+pierzchala-m@users.noreply.github.com> --- src/containers/job/JobManagerContainer.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/containers/job/JobManagerContainer.ts b/src/containers/job/JobManagerContainer.ts index d9c39507..ef0ec1b3 100644 --- a/src/containers/job/JobManagerContainer.ts +++ b/src/containers/job/JobManagerContainer.ts @@ -716,16 +716,17 @@ export class JobManagerContainer extends Container { } } catch (err) { // If pull fails, leave stash intact for manual recovery - // The user can use "Reset and Pull" to clean up + // The user can use "Reset and Pull" to clean up or `git stash list` to view saved changes if (stashed) { - logger.warn('Pull failed. Stashed changes remain saved for manual recovery. Use "Reset and Pull" to clean up.'); + logger.warn('Pull failed. Stashed changes remain saved for manual recovery. ' + + 'Use `git stash list` to view stashed changes or "Reset and Pull" to clean up.'); } throw err; } } } catch (err) { if (err?.message?.includes('Failed to retrieve list of SSH authentication methods')) { - throw new Error('Failed to authenticate with remote repository'); + throw new Error('Failed to authenticate with remote repository: ' + err.message); } throw err; } From b903ed8c3b71e96dd129d0f27ed9de145244dab6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:29:58 +0000 Subject: [PATCH 10/21] Address code review: check diverged branches, improve comments, add logging Co-authored-by: pierzchala-m <162727606+pierzchala-m@users.noreply.github.com> --- src/containers/job/JobManagerContainer.ts | 8 ++++++-- src/git/GitScanner.ts | 5 +++-- test/git/GitTest.ts | 13 ++++++++++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/containers/job/JobManagerContainer.ts b/src/containers/job/JobManagerContainer.ts index ef0ec1b3..860caa65 100644 --- a/src/containers/job/JobManagerContainer.ts +++ b/src/containers/job/JobManagerContainer.ts @@ -698,6 +698,10 @@ export class JobManagerContainer extends Container { const { ahead, behind } = await gitScanner.countAheadBehind(userConfig.remote_branch); + if (ahead > 0 && behind > 0) { + throw new Error('Local and remote branches have diverged. Please manually sync your repository before committing.'); + } + if (behind > 0) { logger.info(`Local branch is ${behind} commit(s) behind remote. Syncing before commit...`); @@ -705,7 +709,7 @@ export class JobManagerContainer extends Container { const stashed = await gitScanner.stashChanges(); try { - // Pull with rebase + // Pull with rebase to integrate remote changes (uses git pull --rebase internally) await gitScanner.pullBranch(userConfig.remote_branch, { privateKeyFile: await userConfigService.getDeployPrivateKeyPath() }); @@ -725,7 +729,7 @@ export class JobManagerContainer extends Container { } } } catch (err) { - if (err?.message?.includes('Failed to retrieve list of SSH authentication methods')) { + if (err.message && err.message.includes('Failed to retrieve list of SSH authentication methods')) { throw new Error('Failed to authenticate with remote repository: ' + err.message); } throw err; diff --git a/src/git/GitScanner.ts b/src/git/GitScanner.ts index 9b97658e..f9996534 100644 --- a/src/git/GitScanner.ts +++ b/src/git/GitScanner.ts @@ -902,8 +902,9 @@ export class GitScanner { return { ahead, behind }; - // deno-lint-ignore no-empty - } catch (ignore) {} + } catch (err) { + this.logger.warn(`Failed to count ahead/behind commits for branch ${remoteBranch}: ${err.message}`, { filename: __filename }); + } return { ahead: 0, behind: 0 }; } diff --git a/test/git/GitTest.ts b/test/git/GitTest.ts index 79331297..d51a662f 100644 --- a/test/git/GitTest.ts +++ b/test/git/GitTest.ts @@ -851,6 +851,13 @@ Deno.test('test stash and pop', async () => { }); Deno.test('test commit with local behind remote', async () => { + // This test simulates a scenario where WikiGDrive is out of sync with the remote repository: + // - localRepoDir: Represents the WikiGDrive local repository + // - githubRepoDir: Represents the remote GitHub repository (bare repo) + // - secondRepoDir: Represents another contributor who commits directly to GitHub + // The test verifies that the stash/pull/pop workflow correctly syncs local changes + // when the local repository is behind the remote. + const localRepoDir: string = createTmpDir(); const githubRepoDir: string = createTmpDir(); const secondRepoDir: string = createTmpDir(); @@ -858,7 +865,7 @@ Deno.test('test commit with local behind remote', async () => { try { execSync(`git init -b main --bare ${githubRepoDir}`); - // Setup first repo + // Setup first repo (WikiGDrive local repository) const scannerLocal = new GitScanner(logger, localRepoDir, COMMITER1.email); await scannerLocal.initialize(); @@ -868,7 +875,7 @@ Deno.test('test commit with local behind remote', async () => { await scannerLocal.setRemoteUrl(githubRepoDir); await scannerLocal.pushBranch('main'); - // Setup second repo and make a commit + // Setup second repo (simulates another contributor pushing to GitHub) const scannerSecond = new GitScanner(logger, secondRepoDir, COMMITER2.email); await scannerSecond.initialize(); fs.unlinkSync(secondRepoDir + '/.gitignore'); @@ -879,7 +886,7 @@ Deno.test('test commit with local behind remote', async () => { await scannerSecond.commit('Second commit', ['file2.md'], COMMITER2); await scannerSecond.pushBranch('main'); - // First repo now has local changes but is behind remote + // WikiGDrive repository now has local changes but is behind remote fs.writeFileSync(path.join(localRepoDir, 'file3.md'), 'Local change'); // Fetch to make remote refs available From 9b4386a1011854923463db2b04166286349e0052 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:31:24 +0000 Subject: [PATCH 11/21] Add conflict detection and proper error message handling Co-authored-by: pierzchala-m <162727606+pierzchala-m@users.noreply.github.com> --- src/containers/job/JobManagerContainer.ts | 6 ++++ src/git/GitScanner.ts | 25 ++++++++++++++-- test/git/GitTest.ts | 35 +++++++++++++++++++++++ 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/containers/job/JobManagerContainer.ts b/src/containers/job/JobManagerContainer.ts index 860caa65..b3aaa5b9 100644 --- a/src/containers/job/JobManagerContainer.ts +++ b/src/containers/job/JobManagerContainer.ts @@ -717,6 +717,12 @@ export class JobManagerContainer extends Container { // Apply stashed changes if we stashed something if (stashed) { await gitScanner.stashPop(); + + // Check for conflicts after stash pop + if (await gitScanner.hasConflicts()) { + throw new Error('Stash pop resulted in merge conflicts. Cannot proceed with commit. ' + + 'Please resolve conflicts manually using "Reset and Pull" or by running git commands directly.'); + } } } catch (err) { // If pull fails, leave stash intact for manual recovery diff --git a/src/git/GitScanner.ts b/src/git/GitScanner.ts index f9996534..df25087c 100644 --- a/src/git/GitScanner.ts +++ b/src/git/GitScanner.ts @@ -1020,7 +1020,8 @@ export class GitScanner { return true; } catch (err) { // If there's nothing to stash, git stash will return "No local changes to save" - if (err?.message?.includes('No local changes to save')) { + // Error message format: "Process exited with status: X\n" + stderr + if (err.message && err.message.includes('No local changes to save')) { return false; } throw err; @@ -1032,11 +1033,31 @@ export class GitScanner { await this.exec('git stash pop', { skipLogger: !this.debug }); } catch (err) { // If there's no stash to pop, handle gracefully - if (err?.message?.includes('No stash entries found')) { + // Error message format: "Process exited with status: X\n" + stderr + if (err.message && err.message.includes('No stash entries found')) { return; } + // Check if the error is due to merge conflicts + if (err.message && err.message.includes('CONFLICT')) { + throw new Error('Stash pop encountered merge conflicts. Please resolve conflicts manually.'); + } throw err; } } + async hasConflicts(): Promise { + try { + const result = await this.exec('git status --porcelain', { skipLogger: !this.debug }); + // Check for unmerged files (status codes starting with U) + const lines = result.stdout.split('\n'); + return lines.some(line => line.startsWith('UU ') || line.startsWith('AA ') || + line.startsWith('DD ') || line.startsWith('AU ') || + line.startsWith('UA ') || line.startsWith('DU ') || + line.startsWith('UD ')); + } catch (err) { + this.logger.warn('Failed to check for conflicts: ' + err.message, { filename: __filename }); + return false; + } + } + } diff --git a/test/git/GitTest.ts b/test/git/GitTest.ts index d51a662f..7b6aaef3 100644 --- a/test/git/GitTest.ts +++ b/test/git/GitTest.ts @@ -920,3 +920,38 @@ Deno.test('test commit with local behind remote', async () => { fs.rmSync(secondRepoDir, { recursive: true, force: true }); } }); + +Deno.test('test hasConflicts detection', async () => { + const localRepoDir: string = createTmpDir(); + + try { + const scannerLocal = new GitScanner(logger, localRepoDir, COMMITER1.email); + await scannerLocal.initialize(); + + fs.writeFileSync(path.join(localRepoDir, 'file1.md'), 'line1\n'); + await scannerLocal.commit('First commit', ['.gitignore', 'file1.md'], COMMITER1); + + // Create a conflict situation + fs.writeFileSync(path.join(localRepoDir, 'file1.md'), 'line1\nline2\n'); + await scannerLocal.commit('Second commit', ['file1.md'], COMMITER1); + + // Reset to previous commit and make conflicting change + await scannerLocal.exec('git reset --hard HEAD~1', {}); + fs.writeFileSync(path.join(localRepoDir, 'file1.md'), 'line1\ndifferent line\n'); + await scannerLocal.commit('Conflicting commit', ['file1.md'], COMMITER1); + + // Try to merge - this will create a conflict + try { + await scannerLocal.exec('git merge HEAD@{1}', {}); + } catch (err) { + // Expected to fail with conflict + } + + // Should detect conflicts + const hasConflicts = await scannerLocal.hasConflicts(); + assertStrictEquals(hasConflicts, true); + + } finally { + fs.rmSync(localRepoDir, { recursive: true, force: true }); + } +}); From ae75141f1aace30b189522a2978adc935c153e3f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:39:11 +0000 Subject: [PATCH 12/21] Fix test to use execSync, use indexOf for consistency, update comment Co-authored-by: pierzchala-m <162727606+pierzchala-m@users.noreply.github.com> --- src/containers/job/JobManagerContainer.ts | 2 +- src/git/GitScanner.ts | 2 +- test/git/GitTest.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/containers/job/JobManagerContainer.ts b/src/containers/job/JobManagerContainer.ts index b3aaa5b9..d6e24b69 100644 --- a/src/containers/job/JobManagerContainer.ts +++ b/src/containers/job/JobManagerContainer.ts @@ -735,7 +735,7 @@ export class JobManagerContainer extends Container { } } } catch (err) { - if (err.message && err.message.includes('Failed to retrieve list of SSH authentication methods')) { + if (err.message.indexOf('Failed to retrieve list of SSH authentication methods') > -1) { throw new Error('Failed to authenticate with remote repository: ' + err.message); } throw err; diff --git a/src/git/GitScanner.ts b/src/git/GitScanner.ts index df25087c..c901f9b1 100644 --- a/src/git/GitScanner.ts +++ b/src/git/GitScanner.ts @@ -1048,7 +1048,7 @@ export class GitScanner { async hasConflicts(): Promise { try { const result = await this.exec('git status --porcelain', { skipLogger: !this.debug }); - // Check for unmerged files (status codes starting with U) + // Check for unmerged files (any merge-conflict porcelain status codes) const lines = result.stdout.split('\n'); return lines.some(line => line.startsWith('UU ') || line.startsWith('AA ') || line.startsWith('DD ') || line.startsWith('AU ') || diff --git a/test/git/GitTest.ts b/test/git/GitTest.ts index 7b6aaef3..4460874d 100644 --- a/test/git/GitTest.ts +++ b/test/git/GitTest.ts @@ -936,13 +936,13 @@ Deno.test('test hasConflicts detection', async () => { await scannerLocal.commit('Second commit', ['file1.md'], COMMITER1); // Reset to previous commit and make conflicting change - await scannerLocal.exec('git reset --hard HEAD~1', {}); + execSync('git reset --hard HEAD~1', { cwd: localRepoDir }); fs.writeFileSync(path.join(localRepoDir, 'file1.md'), 'line1\ndifferent line\n'); await scannerLocal.commit('Conflicting commit', ['file1.md'], COMMITER1); // Try to merge - this will create a conflict try { - await scannerLocal.exec('git merge HEAD@{1}', {}); + execSync('git merge HEAD@{1}', { cwd: localRepoDir }); } catch (err) { // Expected to fail with conflict } From 34e6b2ab5c1a5d504309aece5bfe2256a0e8deb8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:42:38 +0000 Subject: [PATCH 13/21] Simplify hasConflicts using git diff --diff-filter=U Co-authored-by: pierzchala-m <162727606+pierzchala-m@users.noreply.github.com> --- src/git/GitScanner.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/git/GitScanner.ts b/src/git/GitScanner.ts index c901f9b1..2720995a 100644 --- a/src/git/GitScanner.ts +++ b/src/git/GitScanner.ts @@ -1047,13 +1047,9 @@ export class GitScanner { async hasConflicts(): Promise { try { - const result = await this.exec('git status --porcelain', { skipLogger: !this.debug }); - // Check for unmerged files (any merge-conflict porcelain status codes) - const lines = result.stdout.split('\n'); - return lines.some(line => line.startsWith('UU ') || line.startsWith('AA ') || - line.startsWith('DD ') || line.startsWith('AU ') || - line.startsWith('UA ') || line.startsWith('DU ') || - line.startsWith('UD ')); + const result = await this.exec('git diff --name-only --diff-filter=U', { skipLogger: !this.debug }); + // If any file names are returned, there are unmerged files (conflicts) + return result.stdout.trim().length > 0; } catch (err) { this.logger.warn('Failed to check for conflicts: ' + err.message, { filename: __filename }); return false; From ff34a29716c77a3b68c264f5cd856a000b3c1f62 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:49:29 +0000 Subject: [PATCH 14/21] Replace execSync with spawn for git commands in conflict test Co-authored-by: pierzchala-m <162727606+pierzchala-m@users.noreply.github.com> --- test/git/GitTest.ts | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/test/git/GitTest.ts b/test/git/GitTest.ts index 4460874d..5a332e33 100644 --- a/test/git/GitTest.ts +++ b/test/git/GitTest.ts @@ -1,6 +1,6 @@ import fs, {rmSync, unlinkSync} from 'node:fs'; import path from 'node:path'; -import {execSync} from 'node:child_process'; +import {execSync, spawn} from 'node:child_process'; import winston from 'winston'; // eslint-disable-next-line import/no-unresolved @@ -28,6 +28,30 @@ const logger = winston.createLogger({ }); instrumentLogger(logger); +// Helper function to run git commands using spawn +function runGitCommand(command: string, args: string[], cwd: string): Promise { + return new Promise((resolve, reject) => { + const proc = spawn(command, args, { cwd }); + let stderr = ''; + + proc.stderr?.on('data', (data) => { + stderr += data.toString(); + }); + + proc.on('close', (code) => { + if (code !== 0) { + reject(new Error(`Command failed with code ${code}: ${stderr}`)); + } else { + resolve(); + } + }); + + proc.on('error', (err) => { + reject(err); + }); + }); +} + Deno.test('test initialize', async () => { // t.timeout(5000); const localRepoDir: string = createTmpDir(); @@ -936,13 +960,13 @@ Deno.test('test hasConflicts detection', async () => { await scannerLocal.commit('Second commit', ['file1.md'], COMMITER1); // Reset to previous commit and make conflicting change - execSync('git reset --hard HEAD~1', { cwd: localRepoDir }); + await runGitCommand('git', ['reset', '--hard', 'HEAD~1'], localRepoDir); fs.writeFileSync(path.join(localRepoDir, 'file1.md'), 'line1\ndifferent line\n'); await scannerLocal.commit('Conflicting commit', ['file1.md'], COMMITER1); // Try to merge - this will create a conflict try { - execSync('git merge HEAD@{1}', { cwd: localRepoDir }); + await runGitCommand('git', ['merge', 'HEAD@{1}'], localRepoDir); } catch (err) { // Expected to fail with conflict } From 1d9aef4801b352e4b6cd03a6863c26654b99e555 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 14:59:29 +0000 Subject: [PATCH 15/21] Fix test and improve stash reliability and conflict detection Co-authored-by: pierzchala-m <162727606+pierzchala-m@users.noreply.github.com> --- src/git/GitScanner.ts | 30 ++++++++++++++++++++++++------ test/git/GitTest.ts | 8 ++++---- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/git/GitScanner.ts b/src/git/GitScanner.ts index 2720995a..ce24fcb2 100644 --- a/src/git/GitScanner.ts +++ b/src/git/GitScanner.ts @@ -1015,30 +1015,48 @@ export class GitScanner { } async stashChanges(): Promise { + // Capture the number of existing stash entries before attempting to stash + const beforeResult = await this.exec('git stash list --format=%gd', { skipLogger: !this.debug }); + const beforeCount = beforeResult.stdout.trim().length === 0 + ? 0 + : beforeResult.stdout.trim().split('\n').length; + try { await this.exec('git stash push -u -m "WikiGDrive auto-stash before sync"', { skipLogger: !this.debug }); - return true; } catch (err) { - // If there's nothing to stash, git stash will return "No local changes to save" - // Error message format: "Process exited with status: X\n" + stderr + // If there's nothing to stash, git stash may report "No local changes to save" + // in the error output. In that case, report that no stash was created. if (err.message && err.message.includes('No local changes to save')) { return false; } throw err; } + + // Re-count stash entries after the push to determine if a new stash was created + const afterResult = await this.exec('git stash list --format=%gd', { skipLogger: !this.debug }); + const afterCount = afterResult.stdout.trim().length === 0 + ? 0 + : afterResult.stdout.trim().split('\n').length; + + return afterCount > beforeCount; } async stashPop(): Promise { try { await this.exec('git stash pop', { skipLogger: !this.debug }); } catch (err) { + const message = err instanceof Error ? err.message : String(err); // If there's no stash to pop, handle gracefully // Error message format: "Process exited with status: X\n" + stderr - if (err.message && err.message.includes('No stash entries found')) { + if (message.includes('No stash entries found')) { return; } - // Check if the error is due to merge conflicts - if (err.message && err.message.includes('CONFLICT')) { + // Check if the error is due to merge conflicts based on the error message + if (message.includes('CONFLICT')) { + throw new Error('Stash pop encountered merge conflicts. Please resolve conflicts manually.'); + } + // If the message did not explicitly mention conflicts, fall back to checking for unmerged files + if (await this.hasConflicts()) { throw new Error('Stash pop encountered merge conflicts. Please resolve conflicts manually.'); } throw err; diff --git a/test/git/GitTest.ts b/test/git/GitTest.ts index 5a332e33..90783348 100644 --- a/test/git/GitTest.ts +++ b/test/git/GitTest.ts @@ -955,18 +955,18 @@ Deno.test('test hasConflicts detection', async () => { fs.writeFileSync(path.join(localRepoDir, 'file1.md'), 'line1\n'); await scannerLocal.commit('First commit', ['.gitignore', 'file1.md'], COMMITER1); - // Create a conflict situation + // Create a conflict situation - capture the SHA before resetting fs.writeFileSync(path.join(localRepoDir, 'file1.md'), 'line1\nline2\n'); - await scannerLocal.commit('Second commit', ['file1.md'], COMMITER1); + const secondCommitSha = await scannerLocal.commit('Second commit', ['file1.md'], COMMITER1); // Reset to previous commit and make conflicting change await runGitCommand('git', ['reset', '--hard', 'HEAD~1'], localRepoDir); fs.writeFileSync(path.join(localRepoDir, 'file1.md'), 'line1\ndifferent line\n'); await scannerLocal.commit('Conflicting commit', ['file1.md'], COMMITER1); - // Try to merge - this will create a conflict + // Try to merge the second commit - this will create a conflict try { - await runGitCommand('git', ['merge', 'HEAD@{1}'], localRepoDir); + await runGitCommand('git', ['merge', secondCommitSha], localRepoDir); } catch (err) { // Expected to fail with conflict } From a1173ee585dc4e33079a4c07a81be09e0161bce1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:01:10 +0000 Subject: [PATCH 16/21] Extract getStashCount helper and fix empty line handling Co-authored-by: pierzchala-m <162727606+pierzchala-m@users.noreply.github.com> --- src/git/GitScanner.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/git/GitScanner.ts b/src/git/GitScanner.ts index ce24fcb2..3e8e0bb6 100644 --- a/src/git/GitScanner.ts +++ b/src/git/GitScanner.ts @@ -1014,12 +1014,15 @@ export class GitScanner { this.companionFileResolver = resolver; } + private async getStashCount(): Promise { + const result = await this.exec('git stash list --format=%gd', { skipLogger: !this.debug }); + const lines = result.stdout.trim().split('\n').filter(line => line.length > 0); + return lines.length; + } + async stashChanges(): Promise { // Capture the number of existing stash entries before attempting to stash - const beforeResult = await this.exec('git stash list --format=%gd', { skipLogger: !this.debug }); - const beforeCount = beforeResult.stdout.trim().length === 0 - ? 0 - : beforeResult.stdout.trim().split('\n').length; + const beforeCount = await this.getStashCount(); try { await this.exec('git stash push -u -m "WikiGDrive auto-stash before sync"', { skipLogger: !this.debug }); @@ -1033,10 +1036,7 @@ export class GitScanner { } // Re-count stash entries after the push to determine if a new stash was created - const afterResult = await this.exec('git stash list --format=%gd', { skipLogger: !this.debug }); - const afterCount = afterResult.stdout.trim().length === 0 - ? 0 - : afterResult.stdout.trim().split('\n').length; + const afterCount = await this.getStashCount(); return afterCount > beforeCount; } From 5b3e0df083fb0bab1167f4aa9827112df8ab80a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:15:12 +0000 Subject: [PATCH 17/21] Improve pushBranch to handle rejections with stash and better conflict detection Co-authored-by: pierzchala-m <162727606+pierzchala-m@users.noreply.github.com> --- src/git/GitScanner.ts | 68 +++++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/src/git/GitScanner.ts b/src/git/GitScanner.ts index 3e8e0bb6..4a2f80f0 100644 --- a/src/git/GitScanner.ts +++ b/src/git/GitScanner.ts @@ -346,41 +346,65 @@ export class GitScanner { GIT_SSH_COMMAND: sshParams?.privateKeyFile ? `ssh -i ${sanitize(sshParams.privateKeyFile)} -o StrictHostKeyChecking=no -o IdentitiesOnly=yes` : '' } }); - } catch (err) { + catch (err) { if (err.message.indexOf('Updates were rejected because the remote contains work') > -1 || err.message.indexOf('Updates were rejected because a pushed branch tip is behind its remote') > -1) { - await this.exec(`git fetch origin ${remoteBranch}`, { - env: { - GIT_SSH_COMMAND: sshParams?.privateKeyFile ? `ssh -i ${sanitize(sshParams.privateKeyFile)} -o StrictHostKeyChecking=no -o IdentitiesOnly=yes` : '' + // Stash any local changes before fetching and rebasing + const stashed = await this.stashChanges(); + + try { + await this.exec(`git fetch origin ${remoteBranch}`, { + env: { + GIT_SSH_COMMAND: sshParams?.privateKeyFile ? `ssh -i ${sanitize(sshParams.privateKeyFile)} -o StrictHostKeyChecking=no -o IdentitiesOnly=yes` : '' + } + }); + + try { + await this.exec(`git rebase origin/${remoteBranch}`, { + env: { + GIT_AUTHOR_NAME: committer.name, + GIT_AUTHOR_EMAIL: committer.email, + GIT_COMMITTER_NAME: committer.name, + GIT_COMMITTER_EMAIL: committer.email + } + }); + + // Restore stashed changes if any + if (stashed) { + await this.stashPop(); + + // Check for conflicts after restoring stash + if (await this.hasConflicts()) { + await this.exec('git rebase --abort', { ignoreError: true }); + throw new Error('Stash pop resulted in merge conflicts after rebase. Cannot proceed with push. ' + + 'Please resolve conflicts manually.'); + } + } + } catch (err) { + await this.exec('git rebase --abort', { ignoreError: true }); + if (err.message.indexOf('Resolve all conflicts manually') > -1 || err.message.indexOf('merge conflicts') > -1) { + this.logger.error('Conflict detected during rebase', { filename: __filename }); + throw new Error('Rebase conflicts detected. Please resolve conflicts manually and retry.'); + } + throw err; } - }); - try { - await this.exec(`git rebase origin/${remoteBranch}`, { + await this.exec(`git push origin main:${remoteBranch}`, { env: { - GIT_AUTHOR_NAME: committer.name, - GIT_AUTHOR_EMAIL: committer.email, - GIT_COMMITTER_NAME: committer.name, - GIT_COMMITTER_EMAIL: committer.email + GIT_SSH_COMMAND: sshParams?.privateKeyFile ? `ssh -i ${sanitize(sshParams.privateKeyFile)} -o StrictHostKeyChecking=no -o IdentitiesOnly=yes` : '' } }); + return; } catch (err) { - await this.exec('git rebase --abort', { ignoreError: true }); - if (err.message.indexOf('Resolve all conflicts manually') > -1) { - this.logger.error('Conflict', { filename: __filename }); + // If we stashed something and the operation failed, warn about it + if (stashed) { + this.logger.warn('Push/rebase failed. Stashed changes remain saved. Use `git stash list` to view.', { filename: __filename }); } throw err; } - - await this.exec(`git push origin main:${remoteBranch}`, { - env: { - GIT_SSH_COMMAND: sshParams?.privateKeyFile ? `ssh -i ${sanitize(sshParams.privateKeyFile)} -o StrictHostKeyChecking=no -o IdentitiesOnly=yes` : '' - } - }); - return; } - return; + throw err; } } From 47153bf821f59f73224d1668ba069c5fab1f326f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:17:18 +0000 Subject: [PATCH 18/21] Fix syntax error in pushBranch try-catch block Co-authored-by: pierzchala-m <162727606+pierzchala-m@users.noreply.github.com> --- src/git/GitScanner.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/git/GitScanner.ts b/src/git/GitScanner.ts index 4a2f80f0..adbae0e7 100644 --- a/src/git/GitScanner.ts +++ b/src/git/GitScanner.ts @@ -346,7 +346,7 @@ export class GitScanner { GIT_SSH_COMMAND: sshParams?.privateKeyFile ? `ssh -i ${sanitize(sshParams.privateKeyFile)} -o StrictHostKeyChecking=no -o IdentitiesOnly=yes` : '' } }); - catch (err) { + } catch (err) { if (err.message.indexOf('Updates were rejected because the remote contains work') > -1 || err.message.indexOf('Updates were rejected because a pushed branch tip is behind its remote') > -1) { // Stash any local changes before fetching and rebasing @@ -404,6 +404,7 @@ export class GitScanner { } } + // For other errors, just throw them throw err; } } From c022affac9f29d426492276ef19e4078fc44a56a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:27:36 +0000 Subject: [PATCH 19/21] Improve hasConflicts test to verify merge conflict actually occurs Co-authored-by: pierzchala-m <162727606+pierzchala-m@users.noreply.github.com> --- test/git/GitTest.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test/git/GitTest.ts b/test/git/GitTest.ts index 90783348..6fdad212 100644 --- a/test/git/GitTest.ts +++ b/test/git/GitTest.ts @@ -965,15 +965,27 @@ Deno.test('test hasConflicts detection', async () => { await scannerLocal.commit('Conflicting commit', ['file1.md'], COMMITER1); // Try to merge the second commit - this will create a conflict + let mergeSucceeded = false; try { await runGitCommand('git', ['merge', secondCommitSha], localRepoDir); + mergeSucceeded = true; } catch (err) { - // Expected to fail with conflict + // Expected to fail with conflict - verify it's a merge conflict + const message = err instanceof Error ? err.message : String(err); + if (!message.includes('CONFLICT') && !message.includes('Merge conflict')) { + // If merge failed for a different reason, we need to know + console.error('Merge failed but not due to conflict:', message); + } + } + + // If merge succeeded without conflict, the test setup is wrong + if (mergeSucceeded) { + throw new Error('Merge should have created a conflict but succeeded'); } // Should detect conflicts const hasConflicts = await scannerLocal.hasConflicts(); - assertStrictEquals(hasConflicts, true); + assertStrictEquals(hasConflicts, true, 'hasConflicts() should return true after merge conflict'); } finally { fs.rmSync(localRepoDir, { recursive: true, force: true }); From d0236d77bfcad4574c1c117ff8fbf7582c68ab25 Mon Sep 17 00:00:00 2001 From: Monika <162727606+pierzchala-m@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:32:51 -0500 Subject: [PATCH 20/21] Remove hasConflicts detection test case --- test/git/GitTest.ts | 47 --------------------------------------------- 1 file changed, 47 deletions(-) diff --git a/test/git/GitTest.ts b/test/git/GitTest.ts index 6fdad212..55e06823 100644 --- a/test/git/GitTest.ts +++ b/test/git/GitTest.ts @@ -944,50 +944,3 @@ Deno.test('test commit with local behind remote', async () => { fs.rmSync(secondRepoDir, { recursive: true, force: true }); } }); - -Deno.test('test hasConflicts detection', async () => { - const localRepoDir: string = createTmpDir(); - - try { - const scannerLocal = new GitScanner(logger, localRepoDir, COMMITER1.email); - await scannerLocal.initialize(); - - fs.writeFileSync(path.join(localRepoDir, 'file1.md'), 'line1\n'); - await scannerLocal.commit('First commit', ['.gitignore', 'file1.md'], COMMITER1); - - // Create a conflict situation - capture the SHA before resetting - fs.writeFileSync(path.join(localRepoDir, 'file1.md'), 'line1\nline2\n'); - const secondCommitSha = await scannerLocal.commit('Second commit', ['file1.md'], COMMITER1); - - // Reset to previous commit and make conflicting change - await runGitCommand('git', ['reset', '--hard', 'HEAD~1'], localRepoDir); - fs.writeFileSync(path.join(localRepoDir, 'file1.md'), 'line1\ndifferent line\n'); - await scannerLocal.commit('Conflicting commit', ['file1.md'], COMMITER1); - - // Try to merge the second commit - this will create a conflict - let mergeSucceeded = false; - try { - await runGitCommand('git', ['merge', secondCommitSha], localRepoDir); - mergeSucceeded = true; - } catch (err) { - // Expected to fail with conflict - verify it's a merge conflict - const message = err instanceof Error ? err.message : String(err); - if (!message.includes('CONFLICT') && !message.includes('Merge conflict')) { - // If merge failed for a different reason, we need to know - console.error('Merge failed but not due to conflict:', message); - } - } - - // If merge succeeded without conflict, the test setup is wrong - if (mergeSucceeded) { - throw new Error('Merge should have created a conflict but succeeded'); - } - - // Should detect conflicts - const hasConflicts = await scannerLocal.hasConflicts(); - assertStrictEquals(hasConflicts, true, 'hasConflicts() should return true after merge conflict'); - - } finally { - fs.rmSync(localRepoDir, { recursive: true, force: true }); - } -}); From 90372d70186619ceabaac8e6b6367047289ede22 Mon Sep 17 00:00:00 2001 From: Monika <162727606+pierzchala-m@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:28:50 -0500 Subject: [PATCH 21/21] Potential fix for code scanning alert no. 291: Unused variable, import, function or class Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- test/git/GitTest.ts | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/test/git/GitTest.ts b/test/git/GitTest.ts index 55e06823..80873080 100644 --- a/test/git/GitTest.ts +++ b/test/git/GitTest.ts @@ -28,30 +28,6 @@ const logger = winston.createLogger({ }); instrumentLogger(logger); -// Helper function to run git commands using spawn -function runGitCommand(command: string, args: string[], cwd: string): Promise { - return new Promise((resolve, reject) => { - const proc = spawn(command, args, { cwd }); - let stderr = ''; - - proc.stderr?.on('data', (data) => { - stderr += data.toString(); - }); - - proc.on('close', (code) => { - if (code !== 0) { - reject(new Error(`Command failed with code ${code}: ${stderr}`)); - } else { - resolve(); - } - }); - - proc.on('error', (err) => { - reject(err); - }); - }); -} - Deno.test('test initialize', async () => { // t.timeout(5000); const localRepoDir: string = createTmpDir();