feat: configure sync.remote for contributor fork workflows
When bd init --contributor detects a fork setup (upstream remote exists), it now configures sync.remote = upstream. This ensures bd sync pulls beads from the source repo (upstream/main) rather than the fork's potentially outdated origin/main. Changes: - Add sync.remote config in contributor wizard when fork detected - Modify doSyncFromMain() to use configured sync.remote - Add getDefaultBranchForRemote() to support any remote name - Verify configured remote exists before fetching Fixes bd-bx9 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -30,7 +30,7 @@
|
|||||||
{"id":"bd-bhd","title":"Git history fallback assumes .beads is direct child of repo root","description":"## Problem\n\n`checkGitHistoryForDeletions` assumes the repo structure:\n\n```go\nrepoRoot := filepath.Dir(beadsDir) // Assumes .beads is in repo root\njsonlPath := filepath.Join(\".beads\", \"beads.jsonl\")\n```\n\nBut `.beads` could be in a subdirectory (monorepo, nested project), and the actual JSONL filename could be different (configured via `metadata.json`).\n\n## Location\n`internal/importer/importer.go:865-869`\n\n## Impact\n- Git search will fail silently for repos with non-standard structure\n- Monorepo users won't get deletion propagation\n\n## Fix\n1. Use `git rev-parse --show-toplevel` to find actual repo root\n2. Compute relative path from repo root to JSONL\n3. Or use `git -C \u003cdir\u003e` to run from beadsDir directly","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-11-25T12:51:03.46856-08:00","updated_at":"2025-11-25T15:05:40.754716-08:00","closed_at":"2025-11-25T15:05:40.754716-08:00"}
|
{"id":"bd-bhd","title":"Git history fallback assumes .beads is direct child of repo root","description":"## Problem\n\n`checkGitHistoryForDeletions` assumes the repo structure:\n\n```go\nrepoRoot := filepath.Dir(beadsDir) // Assumes .beads is in repo root\njsonlPath := filepath.Join(\".beads\", \"beads.jsonl\")\n```\n\nBut `.beads` could be in a subdirectory (monorepo, nested project), and the actual JSONL filename could be different (configured via `metadata.json`).\n\n## Location\n`internal/importer/importer.go:865-869`\n\n## Impact\n- Git search will fail silently for repos with non-standard structure\n- Monorepo users won't get deletion propagation\n\n## Fix\n1. Use `git rev-parse --show-toplevel` to find actual repo root\n2. Compute relative path from repo root to JSONL\n3. Or use `git -C \u003cdir\u003e` to run from beadsDir directly","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-11-25T12:51:03.46856-08:00","updated_at":"2025-11-25T15:05:40.754716-08:00","closed_at":"2025-11-25T15:05:40.754716-08:00"}
|
||||||
{"id":"bd-bok","title":"bd doctor --fix needs non-interactive mode (-y/--yes flag)","description":"When running `bd doctor --fix` in non-interactive mode (scripts, CI, Claude Code), it prompts 'Continue? (Y/n):' and fails with EOF.\n\n**Expected**: A `-y` or `--yes` flag to auto-confirm fixes.\n\n**Workaround**: Currently have to run `bd init` instead, but that's not discoverable from the doctor output.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-11-27T20:21:10.290649-08:00","updated_at":"2025-11-28T22:17:12.607642-08:00","closed_at":"2025-11-28T21:56:14.708313-08:00"}
|
{"id":"bd-bok","title":"bd doctor --fix needs non-interactive mode (-y/--yes flag)","description":"When running `bd doctor --fix` in non-interactive mode (scripts, CI, Claude Code), it prompts 'Continue? (Y/n):' and fails with EOF.\n\n**Expected**: A `-y` or `--yes` flag to auto-confirm fixes.\n\n**Workaround**: Currently have to run `bd init` instead, but that's not discoverable from the doctor output.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-11-27T20:21:10.290649-08:00","updated_at":"2025-11-28T22:17:12.607642-08:00","closed_at":"2025-11-28T21:56:14.708313-08:00"}
|
||||||
{"id":"bd-bt6y","title":"Improve compact/daemon/merge documentation and UX","description":"Multiple documentation and UX issues encountered:\n1. \"bd compact --analyze\" fails with misleading \"requires SQLite storage\" error when daemon is running. Needs --no-daemon or better error.\n2. \"bd merge\" help text is outdated (refers to 3-way merge instead of issue merging).\n3. Daemon mode purpose isn't clear to local-only users.\n4. Compact/cleanup commands are hard to discover.\n\nProposed fixes:\n- Fix compact+daemon interaction or error message.\n- Update \"bd merge\" help text.\n- Add \"when to use daemon\" section to docs.\n- Add maintenance section to quickstart.\n","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-20T18:55:43.637047-05:00","updated_at":"2025-11-29T22:06:06.330457-08:00","closed_at":"2025-11-28T23:10:43.884784-08:00"}
|
{"id":"bd-bt6y","title":"Improve compact/daemon/merge documentation and UX","description":"Multiple documentation and UX issues encountered:\n1. \"bd compact --analyze\" fails with misleading \"requires SQLite storage\" error when daemon is running. Needs --no-daemon or better error.\n2. \"bd merge\" help text is outdated (refers to 3-way merge instead of issue merging).\n3. Daemon mode purpose isn't clear to local-only users.\n4. Compact/cleanup commands are hard to discover.\n\nProposed fixes:\n- Fix compact+daemon interaction or error message.\n- Update \"bd merge\" help text.\n- Add \"when to use daemon\" section to docs.\n- Add maintenance section to quickstart.\n","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-20T18:55:43.637047-05:00","updated_at":"2025-11-29T22:06:06.330457-08:00","closed_at":"2025-11-28T23:10:43.884784-08:00"}
|
||||||
{"id":"bd-bx9","title":"bd init --contributor should configure sync.remote=upstream for fork workflows","description":"When running `bd init --contributor` in a fork workflow (where `upstream` remote points to the original repo), the wizard should configure beads to sync from `upstream/main` rather than `origin/main`.\n\n**Current behavior:**\n- Contributor mode detects the fork setup (upstream remote exists)\n- Sets up planning repo and auto-routing\n- Does NOT configure sync remote\n- `bd sync` on feature branches shows \"No upstream configured, using --from-main mode\" and syncs from `origin/main`\n\n**Expected behavior:**\n- Contributor mode should also set `sync.remote = upstream` (or similar config)\n- `bd sync` should pull beads from `upstream/main` (source of truth)\n\n**Why this matters:**\n- The fork's `origin/main` may be behind `upstream/main`\n- Contributors want the latest issues from the source repo\n- Code PRs go: local -\u003e origin -\u003e upstream, but beads should come FROM upstream\n\n**Suggested fix:**\nAdd to `runContributorWizard()` after detecting fork:\n```go\nif isFork {\n store.SetConfig(ctx, \"sync.remote\", \"upstream\")\n}\n```","status":"open","priority":2,"issue_type":"feature","created_at":"2025-11-29T00:39:05.137488727-05:00","updated_at":"2025-11-29T00:39:05.137488727-05:00","labels":["contributor","sync"]}
|
{"id":"bd-bx9","title":"bd init --contributor should configure sync.remote=upstream for fork workflows","description":"When running `bd init --contributor` in a fork workflow (where `upstream` remote points to the original repo), the wizard should configure beads to sync from `upstream/main` rather than `origin/main`.\n\n**Current behavior:**\n- Contributor mode detects the fork setup (upstream remote exists)\n- Sets up planning repo and auto-routing\n- Does NOT configure sync remote\n- `bd sync` on feature branches shows \"No upstream configured, using --from-main mode\" and syncs from `origin/main`\n\n**Expected behavior:**\n- Contributor mode should also set `sync.remote = upstream` (or similar config)\n- `bd sync` should pull beads from `upstream/main` (source of truth)\n\n**Why this matters:**\n- The fork's `origin/main` may be behind `upstream/main`\n- Contributors want the latest issues from the source repo\n- Code PRs go: local -\u003e origin -\u003e upstream, but beads should come FROM upstream\n\n**Suggested fix:**\nAdd to `runContributorWizard()` after detecting fork:\n```go\nif isFork {\n store.SetConfig(ctx, \"sync.remote\", \"upstream\")\n}\n```","status":"in_progress","priority":2,"issue_type":"feature","created_at":"2025-11-29T00:39:05.137488727-05:00","updated_at":"2025-11-29T23:22:53.036271-08:00","labels":["contributor","sync"]}
|
||||||
{"id":"bd-c362","title":"Extract database search logic into helper function","description":"The logic for finding a database in a beads directory is duplicated:\n- FindDatabasePath() BEADS_DIR section (beads.go:141-169)\n- findDatabaseInTree() (beads.go:248-280)\n\nBoth implement the same search order:\n1. Check config.json first (single source of truth)\n2. Fall back to canonical beads.db\n3. Search for *.db files, filtering backups and vc.db\n\nRefactoring suggestion:\nExtract to a helper function like:\n func findDatabaseInBeadsDir(beadsDir string) string\n\nBenefits:\n- Single source of truth for database search logic\n- Easier to maintain and update search order\n- Reduces code duplication\n\nRelated to [deleted:bd-e16b] implementation.","status":"closed","priority":3,"issue_type":"chore","created_at":"2025-11-02T18:34:02.831543-08:00","updated_at":"2025-11-25T22:27:33.794656-08:00","closed_at":"2025-11-25T22:27:33.794656-08:00"}
|
{"id":"bd-c362","title":"Extract database search logic into helper function","description":"The logic for finding a database in a beads directory is duplicated:\n- FindDatabasePath() BEADS_DIR section (beads.go:141-169)\n- findDatabaseInTree() (beads.go:248-280)\n\nBoth implement the same search order:\n1. Check config.json first (single source of truth)\n2. Fall back to canonical beads.db\n3. Search for *.db files, filtering backups and vc.db\n\nRefactoring suggestion:\nExtract to a helper function like:\n func findDatabaseInBeadsDir(beadsDir string) string\n\nBenefits:\n- Single source of truth for database search logic\n- Easier to maintain and update search order\n- Reduces code duplication\n\nRelated to [deleted:bd-e16b] implementation.","status":"closed","priority":3,"issue_type":"chore","created_at":"2025-11-02T18:34:02.831543-08:00","updated_at":"2025-11-25T22:27:33.794656-08:00","closed_at":"2025-11-25T22:27:33.794656-08:00"}
|
||||||
{"id":"bd-c4rq","title":"Refactor: Move staleness check inside daemon branch","description":"## Problem\n\nCurrently ensureDatabaseFresh() is called before the daemon mode check, but it checks daemonClient != nil internally and returns early. This is redundant.\n\n**Location:** All read commands (list.go:196, show.go:27, ready.go:102, status.go:80, etc.)\n\n## Current Pattern\n\nCall happens before daemon check, function checks daemonClient internally.\n\n## Better Pattern\n\nMove staleness check to direct mode branch only, after daemon check.\n\n## Impact\nLow - minor performance improvement (avoids one function call per command in daemon mode)\n\n## Effort\nMedium - requires refactoring 8 command files\n\n## Priority\nLow - can defer to future cleanup PR","status":"closed","priority":3,"issue_type":"chore","created_at":"2025-11-20T20:17:45.119583-05:00","updated_at":"2025-11-29T22:06:06.330716-08:00","closed_at":"2025-11-28T23:37:52.276192-08:00"}
|
{"id":"bd-c4rq","title":"Refactor: Move staleness check inside daemon branch","description":"## Problem\n\nCurrently ensureDatabaseFresh() is called before the daemon mode check, but it checks daemonClient != nil internally and returns early. This is redundant.\n\n**Location:** All read commands (list.go:196, show.go:27, ready.go:102, status.go:80, etc.)\n\n## Current Pattern\n\nCall happens before daemon check, function checks daemonClient internally.\n\n## Better Pattern\n\nMove staleness check to direct mode branch only, after daemon check.\n\n## Impact\nLow - minor performance improvement (avoids one function call per command in daemon mode)\n\n## Effort\nMedium - requires refactoring 8 command files\n\n## Priority\nLow - can defer to future cleanup PR","status":"closed","priority":3,"issue_type":"chore","created_at":"2025-11-20T20:17:45.119583-05:00","updated_at":"2025-11-29T22:06:06.330716-08:00","closed_at":"2025-11-28T23:37:52.276192-08:00"}
|
||||||
{"id":"bd-c8x","title":"Don't search parent directories for .beads databases","description":"bd currently walks up the directory tree looking for .beads directories, which can find unrelated databases (e.g., ~/.beads). This causes confusing warnings and potential data pollution.\n\nShould either:\n1. Stop at git root (don't search above it)\n2. Only use explicit BEADS_DB env var or local .beads\n3. At minimum, don't search in home directory","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-11-27T22:10:41.992686-08:00","updated_at":"2025-11-28T22:17:12.607956-08:00","closed_at":"2025-11-28T22:15:55.878353-08:00"}
|
{"id":"bd-c8x","title":"Don't search parent directories for .beads databases","description":"bd currently walks up the directory tree looking for .beads directories, which can find unrelated databases (e.g., ~/.beads). This causes confusing warnings and potential data pollution.\n\nShould either:\n1. Stop at git root (don't search above it)\n2. Only use explicit BEADS_DB env var or local .beads\n3. At minimum, don't search in home directory","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-11-27T22:10:41.992686-08:00","updated_at":"2025-11-28T22:17:12.607956-08:00","closed_at":"2025-11-28T22:15:55.878353-08:00"}
|
||||||
|
|||||||
@@ -179,6 +179,16 @@ Created by: bd init --contributor
|
|||||||
|
|
||||||
fmt.Printf("%s Auto-routing enabled\n", green("✓"))
|
fmt.Printf("%s Auto-routing enabled\n", green("✓"))
|
||||||
|
|
||||||
|
// If this is a fork, configure sync to pull beads from upstream (bd-bx9)
|
||||||
|
// This ensures `bd sync` gets the latest issues from the source repo,
|
||||||
|
// not from the fork's potentially outdated origin/main
|
||||||
|
if isFork {
|
||||||
|
if err := store.SetConfig(ctx, "sync.remote", "upstream"); err != nil {
|
||||||
|
return fmt.Errorf("failed to set sync remote: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s Sync configured to pull from upstream (source repo)\n", green("✓"))
|
||||||
|
}
|
||||||
|
|
||||||
// Step 5: Summary
|
// Step 5: Summary
|
||||||
fmt.Printf("\n%s %s\n\n", green("✓"), bold("Contributor setup complete!"))
|
fmt.Printf("\n%s %s\n\n", green("✓"), bold("Contributor setup complete!"))
|
||||||
|
|
||||||
|
|||||||
@@ -853,27 +853,34 @@ func gitPush(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDefaultBranch returns the default branch name (main or master)
|
// getDefaultBranch returns the default branch name (main or master) for origin remote
|
||||||
// Checks remote HEAD first, then falls back to checking if main/master exist
|
// Checks remote HEAD first, then falls back to checking if main/master exist
|
||||||
func getDefaultBranch(ctx context.Context) string {
|
func getDefaultBranch(ctx context.Context) string {
|
||||||
|
return getDefaultBranchForRemote(ctx, "origin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultBranchForRemote returns the default branch name for a specific remote
|
||||||
|
// Checks remote HEAD first, then falls back to checking if main/master exist
|
||||||
|
func getDefaultBranchForRemote(ctx context.Context, remote string) string {
|
||||||
// Try to get default branch from remote
|
// Try to get default branch from remote
|
||||||
cmd := exec.CommandContext(ctx, "git", "symbolic-ref", "refs/remotes/origin/HEAD")
|
cmd := exec.CommandContext(ctx, "git", "symbolic-ref", fmt.Sprintf("refs/remotes/%s/HEAD", remote))
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
ref := strings.TrimSpace(string(output))
|
ref := strings.TrimSpace(string(output))
|
||||||
// Extract branch name from refs/remotes/origin/main
|
// Extract branch name from refs/remotes/<remote>/main
|
||||||
if strings.HasPrefix(ref, "refs/remotes/origin/") {
|
prefix := fmt.Sprintf("refs/remotes/%s/", remote)
|
||||||
return strings.TrimPrefix(ref, "refs/remotes/origin/")
|
if strings.HasPrefix(ref, prefix) {
|
||||||
|
return strings.TrimPrefix(ref, prefix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: check if origin/main exists
|
// Fallback: check if <remote>/main exists
|
||||||
if exec.CommandContext(ctx, "git", "rev-parse", "--verify", "origin/main").Run() == nil {
|
if exec.CommandContext(ctx, "git", "rev-parse", "--verify", fmt.Sprintf("%s/main", remote)).Run() == nil {
|
||||||
return "main"
|
return "main"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: check if origin/master exists
|
// Fallback: check if <remote>/master exists
|
||||||
if exec.CommandContext(ctx, "git", "rev-parse", "--verify", "origin/master").Run() == nil {
|
if exec.CommandContext(ctx, "git", "rev-parse", "--verify", fmt.Sprintf("%s/master", remote)).Run() == nil {
|
||||||
return "master"
|
return "master"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -884,11 +891,21 @@ func getDefaultBranch(ctx context.Context) string {
|
|||||||
// doSyncFromMain performs a one-way sync from the default branch (main/master)
|
// doSyncFromMain performs a one-way sync from the default branch (main/master)
|
||||||
// Used for ephemeral branches without upstream tracking (gt-ick9)
|
// Used for ephemeral branches without upstream tracking (gt-ick9)
|
||||||
// This fetches beads from main and imports them, discarding local beads changes.
|
// This fetches beads from main and imports them, discarding local beads changes.
|
||||||
|
// If sync.remote is configured (e.g., "upstream" for fork workflows), uses that remote
|
||||||
|
// instead of "origin" (bd-bx9).
|
||||||
func doSyncFromMain(ctx context.Context, jsonlPath string, renameOnImport bool, dryRun bool, noGitHistory bool) error {
|
func doSyncFromMain(ctx context.Context, jsonlPath string, renameOnImport bool, dryRun bool, noGitHistory bool) error {
|
||||||
|
// Determine which remote to use (default: origin, but can be configured via sync.remote)
|
||||||
|
remote := "origin"
|
||||||
|
if err := ensureStoreActive(); err == nil && store != nil {
|
||||||
|
if configuredRemote, err := store.GetConfig(ctx, "sync.remote"); err == nil && configuredRemote != "" {
|
||||||
|
remote = configuredRemote
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if dryRun {
|
if dryRun {
|
||||||
fmt.Println("→ [DRY RUN] Would sync beads from main branch")
|
fmt.Println("→ [DRY RUN] Would sync beads from main branch")
|
||||||
fmt.Println(" 1. Fetch origin main")
|
fmt.Printf(" 1. Fetch %s main\n", remote)
|
||||||
fmt.Println(" 2. Checkout .beads/ from origin/main")
|
fmt.Printf(" 2. Checkout .beads/ from %s/main\n", remote)
|
||||||
fmt.Println(" 3. Import JSONL into database")
|
fmt.Println(" 3. Import JSONL into database")
|
||||||
fmt.Println("\n✓ Dry run complete (no changes made)")
|
fmt.Println("\n✓ Dry run complete (no changes made)")
|
||||||
return nil
|
return nil
|
||||||
@@ -904,20 +921,26 @@ func doSyncFromMain(ctx context.Context, jsonlPath string, renameOnImport bool,
|
|||||||
return fmt.Errorf("no git remote configured")
|
return fmt.Errorf("no git remote configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultBranch := getDefaultBranch(ctx)
|
// Verify the configured remote exists
|
||||||
|
checkRemoteCmd := exec.CommandContext(ctx, "git", "remote", "get-url", remote)
|
||||||
|
if err := checkRemoteCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("configured sync.remote '%s' does not exist (run 'git remote add %s <url>')", remote, remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultBranch := getDefaultBranchForRemote(ctx, remote)
|
||||||
|
|
||||||
// Step 1: Fetch from main
|
// Step 1: Fetch from main
|
||||||
fmt.Printf("→ Fetching from origin/%s...\n", defaultBranch)
|
fmt.Printf("→ Fetching from %s/%s...\n", remote, defaultBranch)
|
||||||
fetchCmd := exec.CommandContext(ctx, "git", "fetch", "origin", defaultBranch)
|
fetchCmd := exec.CommandContext(ctx, "git", "fetch", remote, defaultBranch)
|
||||||
if output, err := fetchCmd.CombinedOutput(); err != nil {
|
if output, err := fetchCmd.CombinedOutput(); err != nil {
|
||||||
return fmt.Errorf("git fetch origin %s failed: %w\n%s", defaultBranch, err, output)
|
return fmt.Errorf("git fetch %s %s failed: %w\n%s", remote, defaultBranch, err, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 2: Checkout .beads/ directory from main
|
// Step 2: Checkout .beads/ directory from main
|
||||||
fmt.Printf("→ Checking out beads from origin/%s...\n", defaultBranch)
|
fmt.Printf("→ Checking out beads from %s/%s...\n", remote, defaultBranch)
|
||||||
checkoutCmd := exec.CommandContext(ctx, "git", "checkout", fmt.Sprintf("origin/%s", defaultBranch), "--", ".beads/")
|
checkoutCmd := exec.CommandContext(ctx, "git", "checkout", fmt.Sprintf("%s/%s", remote, defaultBranch), "--", ".beads/")
|
||||||
if output, err := checkoutCmd.CombinedOutput(); err != nil {
|
if output, err := checkoutCmd.CombinedOutput(); err != nil {
|
||||||
return fmt.Errorf("git checkout .beads/ from origin/%s failed: %w\n%s", defaultBranch, err, output)
|
return fmt.Errorf("git checkout .beads/ from %s/%s failed: %w\n%s", remote, defaultBranch, err, output)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Import JSONL
|
// Step 3: Import JSONL
|
||||||
|
|||||||
Reference in New Issue
Block a user