fix(context): complete RepoContext migration for remaining sync files (#1114)
Follow-up to #1102 - migrates remaining git command locations to use RepoContext API for correct repo resolution when BEADS_DIR is set. Files migrated: - sync_branch.go: getCurrentBranch, showSyncStatus, mergeSyncBranch - sync_check.go: checkForcedPush - sync_import.go: doSyncFromMain - autoimport.go: readFromGitRef, checkGitForIssues - status.go: getGitActivity - import.go: attemptAutoMerge (gitRoot lookup) - reinit_test.go: add ResetCaches for test isolation Pattern used throughout: - Try RepoContext first: rc.GitCmd() runs in beads repo - Fallback to CWD for tests or repos without beads - Graceful degradation maintains backwards compatibility
This commit is contained in:
committed by
GitHub
parent
fabf857b2a
commit
e110632afc
@@ -21,13 +21,20 @@ import (
|
|||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// readFromGitRef reads file content from a git ref (branch or commit).
|
// readFromGitRef reads file content from a git ref (branch or commit) in the beads repo.
|
||||||
// Returns the raw bytes from git show <ref>:<path>.
|
// Returns the raw bytes from git show <ref>:<path>.
|
||||||
// The filePath is automatically converted to forward slashes for Windows compatibility.
|
// The filePath is automatically converted to forward slashes for Windows compatibility.
|
||||||
// Returns nil, err if the git command fails (e.g., file not found in ref).
|
// Returns nil, err if the git command fails (e.g., file not found in ref).
|
||||||
|
// GH#1110: Now uses RepoContext to ensure git commands run in beads repo.
|
||||||
func readFromGitRef(filePath, gitRef string) ([]byte, error) {
|
func readFromGitRef(filePath, gitRef string) ([]byte, error) {
|
||||||
gitPath := filepath.ToSlash(filePath)
|
gitPath := filepath.ToSlash(filePath)
|
||||||
cmd := exec.Command("git", "show", fmt.Sprintf("%s:%s", gitRef, gitPath)) // #nosec G204 - git command with safe args
|
var cmd *exec.Cmd
|
||||||
|
if rc, err := beads.GetRepoContext(); err == nil {
|
||||||
|
cmd = rc.GitCmd(context.Background(), "show", fmt.Sprintf("%s:%s", gitRef, gitPath))
|
||||||
|
} else {
|
||||||
|
// Fallback to CWD for tests or repos without beads
|
||||||
|
cmd = exec.Command("git", "show", fmt.Sprintf("%s:%s", gitRef, gitPath)) // #nosec G204 - git command with safe args
|
||||||
|
}
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read from git: %w", err)
|
return nil, fmt.Errorf("failed to read from git: %w", err)
|
||||||
@@ -135,8 +142,14 @@ func checkGitForIssues() (int, string, string) {
|
|||||||
// Check if the sync branch exists (locally or on remote)
|
// Check if the sync branch exists (locally or on remote)
|
||||||
// Try origin/<branch> first (more likely to exist in fresh clones),
|
// Try origin/<branch> first (more likely to exist in fresh clones),
|
||||||
// then local <branch>
|
// then local <branch>
|
||||||
|
// GH#1110: Use RepoContext to ensure we check the beads repo
|
||||||
for _, ref := range []string{"origin/" + syncBranch, syncBranch} {
|
for _, ref := range []string{"origin/" + syncBranch, syncBranch} {
|
||||||
cmd := exec.Command("git", "rev-parse", "--verify", "--quiet", ref) // #nosec G204
|
var cmd *exec.Cmd
|
||||||
|
if rc, err := beads.GetRepoContext(); err == nil {
|
||||||
|
cmd = rc.GitCmd(context.Background(), "rev-parse", "--verify", "--quiet", ref)
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command("git", "rev-parse", "--verify", "--quiet", ref) // #nosec G204
|
||||||
|
}
|
||||||
if err := cmd.Run(); err == nil {
|
if err := cmd.Run(); err == nil {
|
||||||
gitRef = ref
|
gitRef = ref
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/steveyegge/beads/internal/beads"
|
||||||
"github.com/steveyegge/beads/internal/debug"
|
"github.com/steveyegge/beads/internal/debug"
|
||||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||||
"github.com/steveyegge/beads/internal/types"
|
"github.com/steveyegge/beads/internal/types"
|
||||||
@@ -635,20 +636,27 @@ func countLinesInGitHEAD(filePath string, workDir string) int {
|
|||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
|
|
||||||
// attemptAutoMerge attempts to resolve git conflicts using bd merge 3-way merge
|
// attemptAutoMerge attempts to resolve git conflicts using bd merge 3-way merge.
|
||||||
|
// GH#1110: Now uses RepoContext to ensure we operate on the beads repo.
|
||||||
func attemptAutoMerge(conflictedPath string) error {
|
func attemptAutoMerge(conflictedPath string) error {
|
||||||
// Validate inputs
|
// Validate inputs
|
||||||
if conflictedPath == "" {
|
if conflictedPath == "" {
|
||||||
return fmt.Errorf("no file path provided for merge")
|
return fmt.Errorf("no file path provided for merge")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get git repository root
|
// Get git repository root from RepoContext
|
||||||
gitRootCmd := exec.Command("git", "rev-parse", "--show-toplevel") // #nosec G204 -- fixed git invocation for repo root discovery
|
var gitRoot string
|
||||||
gitRootOutput, err := gitRootCmd.Output()
|
if rc, err := beads.GetRepoContext(); err == nil {
|
||||||
if err != nil {
|
gitRoot = rc.RepoRoot
|
||||||
return fmt.Errorf("not in a git repository: %w", err)
|
} else {
|
||||||
|
// Fallback to CWD-based lookup
|
||||||
|
gitRootCmd := exec.Command("git", "rev-parse", "--show-toplevel") // #nosec G204 -- fixed git invocation for repo root discovery
|
||||||
|
gitRootOutput, err := gitRootCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("not in a git repository: %w", err)
|
||||||
|
}
|
||||||
|
gitRoot = strings.TrimSpace(string(gitRootOutput))
|
||||||
}
|
}
|
||||||
gitRoot := strings.TrimSpace(string(gitRootOutput))
|
|
||||||
|
|
||||||
// Convert conflicted path to absolute path relative to git root
|
// Convert conflicted path to absolute path relative to git root
|
||||||
absConflictedPath := conflictedPath
|
absConflictedPath := conflictedPath
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/steveyegge/beads/internal/beads"
|
||||||
"github.com/steveyegge/beads/internal/git"
|
"github.com/steveyegge/beads/internal/git"
|
||||||
"github.com/steveyegge/beads/internal/storage/sqlite"
|
"github.com/steveyegge/beads/internal/storage/sqlite"
|
||||||
"github.com/steveyegge/beads/internal/types"
|
"github.com/steveyegge/beads/internal/types"
|
||||||
@@ -41,6 +42,7 @@ func TestDatabaseReinitialization(t *testing.T) {
|
|||||||
|
|
||||||
// testFreshCloneAutoImport verifies auto-import works on fresh clone
|
// testFreshCloneAutoImport verifies auto-import works on fresh clone
|
||||||
func testFreshCloneAutoImport(t *testing.T) {
|
func testFreshCloneAutoImport(t *testing.T) {
|
||||||
|
beads.ResetCaches() // Reset cached RepoContext between subtests
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|
||||||
// Initialize git repo
|
// Initialize git repo
|
||||||
@@ -124,6 +126,7 @@ func testFreshCloneAutoImport(t *testing.T) {
|
|||||||
|
|
||||||
// testDatabaseRemovalScenario tests the primary bug scenario
|
// testDatabaseRemovalScenario tests the primary bug scenario
|
||||||
func testDatabaseRemovalScenario(t *testing.T) {
|
func testDatabaseRemovalScenario(t *testing.T) {
|
||||||
|
beads.ResetCaches() // Reset cached RepoContext between subtests
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|
||||||
// Initialize git repo
|
// Initialize git repo
|
||||||
@@ -225,6 +228,7 @@ func testDatabaseRemovalScenario(t *testing.T) {
|
|||||||
|
|
||||||
// testLegacyFilenameSupport tests issues.jsonl fallback
|
// testLegacyFilenameSupport tests issues.jsonl fallback
|
||||||
func testLegacyFilenameSupport(t *testing.T) {
|
func testLegacyFilenameSupport(t *testing.T) {
|
||||||
|
beads.ResetCaches() // Reset cached RepoContext between subtests
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|
||||||
// Initialize git repo
|
// Initialize git repo
|
||||||
@@ -303,6 +307,7 @@ func testLegacyFilenameSupport(t *testing.T) {
|
|||||||
|
|
||||||
// testPrecedenceTest verifies issues.jsonl is preferred over beads.jsonl
|
// testPrecedenceTest verifies issues.jsonl is preferred over beads.jsonl
|
||||||
func testPrecedenceTest(t *testing.T) {
|
func testPrecedenceTest(t *testing.T) {
|
||||||
|
beads.ResetCaches() // Reset cached RepoContext between subtests
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|
||||||
// Initialize git repo
|
// Initialize git repo
|
||||||
@@ -358,6 +363,7 @@ func testPrecedenceTest(t *testing.T) {
|
|||||||
|
|
||||||
// testInitSafetyCheck tests the safety check that prevents silent data loss
|
// testInitSafetyCheck tests the safety check that prevents silent data loss
|
||||||
func testInitSafetyCheck(t *testing.T) {
|
func testInitSafetyCheck(t *testing.T) {
|
||||||
|
beads.ResetCaches() // Reset cached RepoContext between subtests
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
|
|
||||||
// Initialize git repo
|
// Initialize git repo
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -10,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/steveyegge/beads/internal/beads"
|
||||||
"github.com/steveyegge/beads/internal/types"
|
"github.com/steveyegge/beads/internal/types"
|
||||||
"github.com/steveyegge/beads/internal/ui"
|
"github.com/steveyegge/beads/internal/ui"
|
||||||
)
|
)
|
||||||
@@ -178,7 +180,8 @@ Examples:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// getGitActivity calculates activity stats from git log of issues.jsonl
|
// getGitActivity calculates activity stats from git log of issues.jsonl.
|
||||||
|
// GH#1110: Now uses RepoContext to ensure git commands run in beads repo.
|
||||||
func getGitActivity(hours int) *RecentActivitySummary {
|
func getGitActivity(hours int) *RecentActivitySummary {
|
||||||
activity := &RecentActivitySummary{
|
activity := &RecentActivitySummary{
|
||||||
HoursTracked: hours,
|
HoursTracked: hours,
|
||||||
@@ -186,7 +189,12 @@ func getGitActivity(hours int) *RecentActivitySummary {
|
|||||||
|
|
||||||
// Run git log to get patches for the last N hours
|
// Run git log to get patches for the last N hours
|
||||||
since := fmt.Sprintf("%d hours ago", hours)
|
since := fmt.Sprintf("%d hours ago", hours)
|
||||||
cmd := exec.Command("git", "log", "--since="+since, "--numstat", "--pretty=format:%H", ".beads/issues.jsonl") // #nosec G204 -- bounded arguments for local git history inspection
|
var cmd *exec.Cmd
|
||||||
|
if rc, err := beads.GetRepoContext(); err == nil {
|
||||||
|
cmd = rc.GitCmd(context.Background(), "log", "--since="+since, "--numstat", "--pretty=format:%H", ".beads/issues.jsonl")
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command("git", "log", "--since="+since, "--numstat", "--pretty=format:%H", ".beads/issues.jsonl") // #nosec G204 -- bounded arguments for local git history inspection
|
||||||
|
}
|
||||||
|
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -222,7 +230,11 @@ func getGitActivity(hours int) *RecentActivitySummary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get detailed diff to analyze changes
|
// Get detailed diff to analyze changes
|
||||||
cmd = exec.Command("git", "log", "--since="+since, "-p", ".beads/issues.jsonl") // #nosec G204 -- bounded arguments for local git history inspection
|
if rc, err := beads.GetRepoContext(); err == nil {
|
||||||
|
cmd = rc.GitCmd(context.Background(), "log", "--since="+since, "-p", ".beads/issues.jsonl")
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command("git", "log", "--since="+since, "-p", ".beads/issues.jsonl") // #nosec G204 -- bounded arguments for local git history inspection
|
||||||
|
}
|
||||||
output, err = cmd.Output()
|
output, err = cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -8,13 +8,22 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/steveyegge/beads/internal/beads"
|
||||||
"github.com/steveyegge/beads/internal/syncbranch"
|
"github.com/steveyegge/beads/internal/syncbranch"
|
||||||
)
|
)
|
||||||
|
|
||||||
// getCurrentBranch returns the name of the current git branch
|
// getCurrentBranch returns the name of the current git branch in the beads repo.
|
||||||
// Uses symbolic-ref instead of rev-parse to work in fresh repos without commits (bd-flil)
|
// Uses symbolic-ref instead of rev-parse to work in fresh repos without commits (bd-flil)
|
||||||
|
// GH#1110: Now uses RepoContext to ensure we query the beads repo, not CWD.
|
||||||
|
// Falls back to CWD-based query if no beads context found (for tests/standalone git).
|
||||||
func getCurrentBranch(ctx context.Context) (string, error) {
|
func getCurrentBranch(ctx context.Context) (string, error) {
|
||||||
cmd := exec.CommandContext(ctx, "git", "symbolic-ref", "--short", "HEAD")
|
var cmd *exec.Cmd
|
||||||
|
if rc, err := beads.GetRepoContext(); err == nil {
|
||||||
|
cmd = rc.GitCmd(ctx, "symbolic-ref", "--short", "HEAD")
|
||||||
|
} else {
|
||||||
|
// Fallback to CWD for tests or repos without beads
|
||||||
|
cmd = exec.CommandContext(ctx, "git", "symbolic-ref", "--short", "HEAD")
|
||||||
|
}
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get current branch: %w", err)
|
return "", fmt.Errorf("failed to get current branch: %w", err)
|
||||||
@@ -53,12 +62,18 @@ func getSyncBranch(ctx context.Context) (string, error) {
|
|||||||
return syncBranch, nil
|
return syncBranch, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// showSyncStatus shows the diff between sync branch and main branch
|
// showSyncStatus shows the diff between sync branch and main branch.
|
||||||
|
// GH#1110: Now uses RepoContext to ensure git commands run in beads repo.
|
||||||
func showSyncStatus(ctx context.Context) error {
|
func showSyncStatus(ctx context.Context) error {
|
||||||
if !isGitRepo() {
|
if !isGitRepo() {
|
||||||
return fmt.Errorf("not in a git repository")
|
return fmt.Errorf("not in a git repository")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rc, err := beads.GetRepoContext()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get repo context: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
currentBranch := getCurrentBranchOrHEAD(ctx)
|
currentBranch := getCurrentBranchOrHEAD(ctx)
|
||||||
|
|
||||||
syncBranch, err := getSyncBranch(ctx)
|
syncBranch, err := getSyncBranch(ctx)
|
||||||
@@ -67,7 +82,7 @@ func showSyncStatus(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if sync branch exists
|
// Check if sync branch exists
|
||||||
checkCmd := exec.CommandContext(ctx, "git", "show-ref", "--verify", "--quiet", "refs/heads/"+syncBranch) //nolint:gosec // syncBranch from config
|
checkCmd := rc.GitCmd(ctx, "show-ref", "--verify", "--quiet", "refs/heads/"+syncBranch)
|
||||||
if err := checkCmd.Run(); err != nil {
|
if err := checkCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("sync branch '%s' does not exist", syncBranch)
|
return fmt.Errorf("sync branch '%s' does not exist", syncBranch)
|
||||||
}
|
}
|
||||||
@@ -77,7 +92,7 @@ func showSyncStatus(ctx context.Context) error {
|
|||||||
|
|
||||||
// Show commit diff
|
// Show commit diff
|
||||||
fmt.Println("Commits in sync branch not in main:")
|
fmt.Println("Commits in sync branch not in main:")
|
||||||
logCmd := exec.CommandContext(ctx, "git", "log", "--oneline", currentBranch+".."+syncBranch) //nolint:gosec // branch names from git
|
logCmd := rc.GitCmd(ctx, "log", "--oneline", currentBranch+".."+syncBranch)
|
||||||
logOutput, err := logCmd.CombinedOutput()
|
logOutput, err := logCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get commit log: %w\n%s", err, logOutput)
|
return fmt.Errorf("failed to get commit log: %w\n%s", err, logOutput)
|
||||||
@@ -90,7 +105,7 @@ func showSyncStatus(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nCommits in main not in sync branch:")
|
fmt.Println("\nCommits in main not in sync branch:")
|
||||||
logCmd = exec.CommandContext(ctx, "git", "log", "--oneline", syncBranch+".."+currentBranch) //nolint:gosec // branch names from git
|
logCmd = rc.GitCmd(ctx, "log", "--oneline", syncBranch+".."+currentBranch)
|
||||||
logOutput, err = logCmd.CombinedOutput()
|
logOutput, err = logCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get commit log: %w\n%s", err, logOutput)
|
return fmt.Errorf("failed to get commit log: %w\n%s", err, logOutput)
|
||||||
@@ -104,7 +119,7 @@ func showSyncStatus(ctx context.Context) error {
|
|||||||
|
|
||||||
// Show file diff for .beads/issues.jsonl
|
// Show file diff for .beads/issues.jsonl
|
||||||
fmt.Println("\nFile differences in .beads/issues.jsonl:")
|
fmt.Println("\nFile differences in .beads/issues.jsonl:")
|
||||||
diffCmd := exec.CommandContext(ctx, "git", "diff", currentBranch+"..."+syncBranch, "--", ".beads/issues.jsonl") //nolint:gosec // branch names from git
|
diffCmd := rc.GitCmd(ctx, "diff", currentBranch+"..."+syncBranch, "--", ".beads/issues.jsonl")
|
||||||
diffOutput, err := diffCmd.CombinedOutput()
|
diffOutput, err := diffCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// diff returns non-zero when there are differences, which is fine
|
// diff returns non-zero when there are differences, which is fine
|
||||||
@@ -122,12 +137,18 @@ func showSyncStatus(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// mergeSyncBranch merges the sync branch back to the main branch
|
// mergeSyncBranch merges the sync branch back to the main branch.
|
||||||
|
// GH#1110: Now uses RepoContext to ensure git commands run in beads repo.
|
||||||
func mergeSyncBranch(ctx context.Context, dryRun bool) error {
|
func mergeSyncBranch(ctx context.Context, dryRun bool) error {
|
||||||
if !isGitRepo() {
|
if !isGitRepo() {
|
||||||
return fmt.Errorf("not in a git repository")
|
return fmt.Errorf("not in a git repository")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rc, err := beads.GetRepoContext()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get repo context: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
currentBranch, err := getCurrentBranch(ctx)
|
currentBranch, err := getCurrentBranch(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -139,13 +160,13 @@ func mergeSyncBranch(ctx context.Context, dryRun bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if sync branch exists
|
// Check if sync branch exists
|
||||||
checkCmd := exec.CommandContext(ctx, "git", "show-ref", "--verify", "--quiet", "refs/heads/"+syncBranch) //nolint:gosec // syncBranch from config
|
checkCmd := rc.GitCmd(ctx, "show-ref", "--verify", "--quiet", "refs/heads/"+syncBranch)
|
||||||
if err := checkCmd.Run(); err != nil {
|
if err := checkCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("sync branch '%s' does not exist", syncBranch)
|
return fmt.Errorf("sync branch '%s' does not exist", syncBranch)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there are uncommitted changes
|
// Check if there are uncommitted changes
|
||||||
statusCmd := exec.CommandContext(ctx, "git", "status", "--porcelain")
|
statusCmd := rc.GitCmd(ctx, "status", "--porcelain")
|
||||||
statusOutput, err := statusCmd.Output()
|
statusOutput, err := statusCmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to check git status: %w", err)
|
return fmt.Errorf("failed to check git status: %w", err)
|
||||||
@@ -159,7 +180,7 @@ func mergeSyncBranch(ctx context.Context, dryRun bool) error {
|
|||||||
if dryRun {
|
if dryRun {
|
||||||
fmt.Println("→ [DRY RUN] Would merge sync branch")
|
fmt.Println("→ [DRY RUN] Would merge sync branch")
|
||||||
// Show what would be merged
|
// Show what would be merged
|
||||||
logCmd := exec.CommandContext(ctx, "git", "log", "--oneline", currentBranch+".."+syncBranch) //nolint:gosec // branch names from git
|
logCmd := rc.GitCmd(ctx, "log", "--oneline", currentBranch+".."+syncBranch)
|
||||||
logOutput, _ := logCmd.CombinedOutput()
|
logOutput, _ := logCmd.CombinedOutput()
|
||||||
if len(strings.TrimSpace(string(logOutput))) > 0 {
|
if len(strings.TrimSpace(string(logOutput))) > 0 {
|
||||||
fmt.Println("\nCommits that would be merged:")
|
fmt.Println("\nCommits that would be merged:")
|
||||||
@@ -171,7 +192,7 @@ func mergeSyncBranch(ctx context.Context, dryRun bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Perform the merge
|
// Perform the merge
|
||||||
mergeCmd := exec.CommandContext(ctx, "git", "merge", syncBranch, "-m", fmt.Sprintf("Merge sync branch '%s'", syncBranch)) //nolint:gosec // syncBranch from config
|
mergeCmd := rc.GitCmd(ctx, "merge", syncBranch, "-m", fmt.Sprintf("Merge sync branch '%s'", syncBranch))
|
||||||
mergeOutput, err := mergeCmd.CombinedOutput()
|
mergeOutput, err := mergeCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("merge failed: %w\n%s", err, mergeOutput)
|
return fmt.Errorf("merge failed: %w\n%s", err, mergeOutput)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/steveyegge/beads/internal/beads"
|
||||||
"github.com/steveyegge/beads/internal/syncbranch"
|
"github.com/steveyegge/beads/internal/syncbranch"
|
||||||
"github.com/steveyegge/beads/internal/types"
|
"github.com/steveyegge/beads/internal/types"
|
||||||
)
|
)
|
||||||
@@ -112,12 +113,20 @@ func showSyncIntegrityCheck(ctx context.Context, jsonlPath string) {
|
|||||||
|
|
||||||
// checkForcedPush detects if the sync branch has diverged from remote.
|
// checkForcedPush detects if the sync branch has diverged from remote.
|
||||||
// This can happen when someone force-pushes to the sync branch.
|
// This can happen when someone force-pushes to the sync branch.
|
||||||
|
// GH#1110: Now uses RepoContext to ensure git commands run in beads repo.
|
||||||
func checkForcedPush(ctx context.Context) *ForcedPushCheck {
|
func checkForcedPush(ctx context.Context) *ForcedPushCheck {
|
||||||
result := &ForcedPushCheck{
|
result := &ForcedPushCheck{
|
||||||
Detected: false,
|
Detected: false,
|
||||||
Message: "No sync branch configured or no remote",
|
Message: "No sync branch configured or no remote",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get RepoContext for beads repo
|
||||||
|
rc, err := beads.GetRepoContext()
|
||||||
|
if err != nil {
|
||||||
|
result.Message = fmt.Sprintf("Failed to get repo context: %v", err)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// Get sync branch name
|
// Get sync branch name
|
||||||
if err := ensureStoreActive(); err != nil {
|
if err := ensureStoreActive(); err != nil {
|
||||||
return result
|
return result
|
||||||
@@ -129,14 +138,14 @@ func checkForcedPush(ctx context.Context) *ForcedPushCheck {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if sync branch exists locally
|
// Check if sync branch exists locally
|
||||||
checkLocalCmd := exec.CommandContext(ctx, "git", "show-ref", "--verify", "--quiet", "refs/heads/"+syncBranch) //nolint:gosec // syncBranch from config
|
checkLocalCmd := rc.GitCmd(ctx, "show-ref", "--verify", "--quiet", "refs/heads/"+syncBranch)
|
||||||
if checkLocalCmd.Run() != nil {
|
if checkLocalCmd.Run() != nil {
|
||||||
result.Message = fmt.Sprintf("Sync branch '%s' does not exist locally", syncBranch)
|
result.Message = fmt.Sprintf("Sync branch '%s' does not exist locally", syncBranch)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get local ref
|
// Get local ref
|
||||||
localRefCmd := exec.CommandContext(ctx, "git", "rev-parse", syncBranch) //nolint:gosec // syncBranch from config
|
localRefCmd := rc.GitCmd(ctx, "rev-parse", syncBranch)
|
||||||
localRefOutput, err := localRefCmd.Output()
|
localRefOutput, err := localRefCmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Message = "Failed to get local sync branch ref"
|
result.Message = "Failed to get local sync branch ref"
|
||||||
@@ -152,7 +161,7 @@ func checkForcedPush(ctx context.Context) *ForcedPushCheck {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get remote ref
|
// Get remote ref
|
||||||
remoteRefCmd := exec.CommandContext(ctx, "git", "rev-parse", remote+"/"+syncBranch) //nolint:gosec // remote and syncBranch from config
|
remoteRefCmd := rc.GitCmd(ctx, "rev-parse", remote+"/"+syncBranch)
|
||||||
remoteRefOutput, err := remoteRefCmd.Output()
|
remoteRefOutput, err := remoteRefCmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Message = fmt.Sprintf("Remote tracking branch '%s/%s' does not exist", remote, syncBranch)
|
result.Message = fmt.Sprintf("Remote tracking branch '%s/%s' does not exist", remote, syncBranch)
|
||||||
@@ -168,14 +177,14 @@ func checkForcedPush(ctx context.Context) *ForcedPushCheck {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if local is ahead of remote (normal case)
|
// Check if local is ahead of remote (normal case)
|
||||||
aheadCmd := exec.CommandContext(ctx, "git", "merge-base", "--is-ancestor", remoteRef, localRef) //nolint:gosec // refs from git rev-parse
|
aheadCmd := rc.GitCmd(ctx, "merge-base", "--is-ancestor", remoteRef, localRef)
|
||||||
if aheadCmd.Run() == nil {
|
if aheadCmd.Run() == nil {
|
||||||
result.Message = "Local sync branch is ahead of remote (normal)"
|
result.Message = "Local sync branch is ahead of remote (normal)"
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if remote is ahead of local (behind, needs pull)
|
// Check if remote is ahead of local (behind, needs pull)
|
||||||
behindCmd := exec.CommandContext(ctx, "git", "merge-base", "--is-ancestor", localRef, remoteRef) //nolint:gosec // refs from git rev-parse
|
behindCmd := rc.GitCmd(ctx, "merge-base", "--is-ancestor", localRef, remoteRef)
|
||||||
if behindCmd.Run() == nil {
|
if behindCmd.Run() == nil {
|
||||||
result.Message = "Local sync branch is behind remote (needs pull)"
|
result.Message = "Local sync branch is behind remote (needs pull)"
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/steveyegge/beads/internal/beads"
|
||||||
"github.com/steveyegge/beads/internal/debug"
|
"github.com/steveyegge/beads/internal/debug"
|
||||||
"github.com/steveyegge/beads/internal/types"
|
"github.com/steveyegge/beads/internal/types"
|
||||||
)
|
)
|
||||||
@@ -162,6 +163,7 @@ func resolveNoGitHistoryForFromMain(fromMain, noGitHistory bool) bool {
|
|||||||
// 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
|
// If sync.remote is configured (e.g., "upstream" for fork workflows), uses that remote
|
||||||
// instead of "origin".
|
// instead of "origin".
|
||||||
|
// GH#1110: Now uses RepoContext to ensure git commands run in beads repo.
|
||||||
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)
|
// Determine which remote to use (default: origin, but can be configured via sync.remote)
|
||||||
remote := "origin"
|
remote := "origin"
|
||||||
@@ -190,8 +192,14 @@ func doSyncFromMain(ctx context.Context, jsonlPath string, renameOnImport bool,
|
|||||||
return fmt.Errorf("no git remote configured")
|
return fmt.Errorf("no git remote configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get RepoContext for beads repo
|
||||||
|
rc, err := beads.GetRepoContext()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get repo context: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Verify the configured remote exists
|
// Verify the configured remote exists
|
||||||
checkRemoteCmd := exec.CommandContext(ctx, "git", "remote", "get-url", remote)
|
checkRemoteCmd := rc.GitCmd(ctx, "remote", "get-url", remote)
|
||||||
if err := checkRemoteCmd.Run(); err != nil {
|
if err := checkRemoteCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("configured sync.remote '%s' does not exist (run 'git remote add %s <url>')", remote, remote)
|
return fmt.Errorf("configured sync.remote '%s' does not exist (run 'git remote add %s <url>')", remote, remote)
|
||||||
}
|
}
|
||||||
@@ -200,14 +208,14 @@ func doSyncFromMain(ctx context.Context, jsonlPath string, renameOnImport bool,
|
|||||||
|
|
||||||
// Step 1: Fetch from main
|
// Step 1: Fetch from main
|
||||||
fmt.Printf("→ Fetching from %s/%s...\n", remote, defaultBranch)
|
fmt.Printf("→ Fetching from %s/%s...\n", remote, defaultBranch)
|
||||||
fetchCmd := exec.CommandContext(ctx, "git", "fetch", remote, defaultBranch) //nolint:gosec // remote and defaultBranch from config
|
fetchCmd := rc.GitCmd(ctx, "fetch", remote, defaultBranch)
|
||||||
if output, err := fetchCmd.CombinedOutput(); err != nil {
|
if output, err := fetchCmd.CombinedOutput(); err != nil {
|
||||||
return fmt.Errorf("git fetch %s %s failed: %w\n%s", remote, 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 %s/%s...\n", remote, defaultBranch)
|
fmt.Printf("→ Checking out beads from %s/%s...\n", remote, defaultBranch)
|
||||||
checkoutCmd := exec.CommandContext(ctx, "git", "checkout", fmt.Sprintf("%s/%s", remote, defaultBranch), "--", ".beads/") //nolint:gosec // remote and defaultBranch from config
|
checkoutCmd := rc.GitCmd(ctx, "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 %s/%s failed: %w\n%s", remote, defaultBranch, err, output)
|
return fmt.Errorf("git checkout .beads/ from %s/%s failed: %w\n%s", remote, defaultBranch, err, output)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user