Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
id: BACK-695
title: Skip fetch when the requested Git remote is absent
status: In Progress
assignee:
- '@codex-pr-remote'
created_date: '2026-09-27 22:14'
updated_date: '2026-09-27 22:15'
labels: []
dependencies: []
references:
- 'https://github.com/MrLesk/Backlog.md/pull/1023'
- 'https://github.com/MrLesk/Backlog.md/issues/1020'
priority: medium
type: bug
ordinal: 325000
---

## Description

<!-- SECTION:DESCRIPTION:BEGIN -->
Repositories with only a differently named remote can produce repeated Git errors during task reads because the preflight checks for any remote, then attempts to fetch origin. Contributor PR #1023 fixes the requested-remote preflight through the existing Git primitive and adds a real upstream-only repository regression for issue #1020.
<!-- SECTION:DESCRIPTION:END -->

## Acceptance Criteria
<!-- AC:BEGIN -->
- [ ] #1 A fetch request for an absent remote returns without attempting that fetch, including a repository whose only remote is upstream.
- [ ] #2 Existing fetch behavior for a configured requested remote and disabled remote operations stays intact.
- [ ] #3 The contributor fix and its Git regressions pass review and remain attributed in the existing PR, with this task identity and matching title.
<!-- AC:END -->

## Definition of Done
<!-- DOD:BEGIN -->
- [ ] #1 bunx tsc --noEmit passes when TypeScript touched
- [ ] #2 bun run check . passes when formatting/linting touched
- [ ] #3 bun test (or scoped test) passes
<!-- DOD:END -->

## Implementation Plan

<!-- SECTION:PLAN:BEGIN -->
1. Review contributor PR #1023 at cc187d43b37b475fe1ff905c39d2b25b7f988af3, including the existing hasRemote helper, fetch configuration gate, and Git regressions.
2. Preserve the contributor implementation and commit. Add this CLI-managed task record as a metadata-only child commit on the existing PR branch, with no production or test changes.
3. Verify that the added commit changes only this task record, retains the original head as its direct parent, and can be pushed as a normal fast-forward after rechecking the remote head.
4. Keep this task In Progress with acceptance criteria unchecked until runtime checks and CI for the PR head provide verification evidence; complete the review in the existing PR.
<!-- SECTION:PLAN:END -->

## Implementation Notes

<!-- SECTION:NOTES:BEGIN -->
Original implementation and regression tests are by Lingikaushikreddy (Kaushikreddy Lingi) in PR #1023, commit cc187d43b37b475fe1ff905c39d2b25b7f988af3. The contributor commit and all production/test blobs are preserved.

Read-only review: fetchRemote now asks the existing hasRemote(remote) helper whether the requested name exists. The helper reads configured remote names and compares the exact requested name. The remoteOperations=false gate remains before this check. The fetch command, timeout, network-error handling, and concurrent-fetch behavior are unchanged. The added regression creates a repository whose only remote is upstream and expects the default-origin fetch to return successfully. Existing fetch and offline-mode mocks are adjusted to the helper that is now called. No concrete source defect or source repair was identified in this review.

This record is the only addition prepared here. No runtime or CI result is claimed by this review; the PR head still requires exact-head CI evidence. Acceptance criteria and Definition of Done remain unchecked.
<!-- SECTION:NOTES:END -->
10 changes: 6 additions & 4 deletions src/git/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -597,10 +597,12 @@ export class GitOperations {
}

private async fetchRemote(remote: string): Promise<void> {
// Preflight: skip if repository has no remotes configured
const hasRemotes = await this.hasAnyRemote();
if (!hasRemotes) {
// No remotes configured; silently skip fetch. A consolidated warning is shown during init if applicable.
// Preflight: skip if the requested remote is not configured. Checking for any remote is not
// enough: a repository whose only remote is named e.g. "upstream" would still run
// `git fetch origin` and fail with exit code 128 on every task read.
const hasRequestedRemote = await this.hasRemote(remote);
if (!hasRequestedRemote) {
// Silently skip fetch. A consolidated warning is shown during init if applicable.
return;
}

Expand Down
16 changes: 8 additions & 8 deletions src/test/git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,13 +238,13 @@ wait "$child_pid"
}
| undefined;
const internals = git as unknown as {
hasAnyRemote: () => Promise<boolean>;
hasRemote: (remote?: string) => Promise<boolean>;
execGit: (
args: string[],
options?: { env?: Record<string, string>; timeoutMs?: number },
) => Promise<{ stdout: string; stderr: string }>;
};
internals.hasAnyRemote = async () => {
internals.hasRemote = async () => {
remoteChecks += 1;
return true;
};
Expand Down Expand Up @@ -291,9 +291,9 @@ wait "$child_pid"
};
});
const internals = git as unknown as {
hasAnyRemote: () => Promise<boolean>;
hasRemote: (remote?: string) => Promise<boolean>;
};
internals.hasAnyRemote = async () => {
internals.hasRemote = async () => {
remoteChecks += 1;
return true;
};
Expand All @@ -316,10 +316,10 @@ wait "$child_pid"
return { ...TEST_CONFIG, remoteOperations: true };
});
const internals = git as unknown as {
hasAnyRemote: () => Promise<boolean>;
hasRemote: (remote?: string) => Promise<boolean>;
execGit: () => Promise<{ stdout: string; stderr: string }>;
};
internals.hasAnyRemote = async () => true;
internals.hasRemote = async () => true;
internals.execGit = async () => {
fetchCalls += 1;
return { stdout: "", stderr: "" };
Expand All @@ -341,10 +341,10 @@ wait "$child_pid"
remoteOperations: true,
});
const internals = git as unknown as {
hasAnyRemote: () => Promise<boolean>;
hasRemote: (remote?: string) => Promise<boolean>;
execGit: () => Promise<{ stdout: string; stderr: string }>;
};
internals.hasAnyRemote = async () => true;
internals.hasRemote = async () => true;
internals.execGit = async () => {
throw new Error("Git command timeout after 10000ms");
};
Expand Down
15 changes: 15 additions & 0 deletions src/test/no-remote-preflight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ describe("Missing git remote preflight", () => {
console.warn = originalWarn;
});

it("GitOperations.fetch() silently skips when only a differently named remote exists", async () => {
await $`git remote add upstream https://example.invalid/backlog.git`.cwd(tempDir).quiet();
const gitOps = new GitOperations(tempDir, {
projectName: "Test",
statuses: ["To Do", "Done"],
labels: [],
milestones: [],
dateFormat: "YYYY-MM-DD",
remoteOperations: true,
} as BacklogConfig);

// Without the per-remote preflight this runs `git fetch origin` and rejects with exit code 128.
await expect(gitOps.fetch()).resolves.toBeUndefined();
});

it("Core cross-branch loading handles remoteOperations=true without a remote", async () => {
const config: BacklogConfig = {
projectName: "Test",
Expand Down
8 changes: 4 additions & 4 deletions src/test/offline-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ describe("Offline Mode Configuration", () => {

let capturedArgs: string[] = [];
const internals = gitOps as unknown as {
hasAnyRemote: () => Promise<boolean>;
hasRemote: (remote?: string) => Promise<boolean>;
execGit: (args: string[]) => Promise<{ stdout: string; stderr: string }>;
};
const originalHasAnyRemote = internals.hasAnyRemote;
const originalHasRemote = internals.hasRemote;
const originalExecGit = internals.execGit;
internals.hasAnyRemote = async () => true;
internals.hasRemote = async () => true;
internals.execGit = async (args: string[]) => {
capturedArgs = args;
return { stdout: "", stderr: "" };
Expand All @@ -84,7 +84,7 @@ describe("Offline Mode Configuration", () => {
await gitOps.fetch();
expect(capturedArgs).toEqual(["fetch", "origin", "--prune", "--quiet"]);
} finally {
internals.hasAnyRemote = originalHasAnyRemote;
internals.hasRemote = originalHasRemote;
internals.execGit = originalExecGit;
}
});
Expand Down
Loading