feat(sync): implement bd sync command specification
Changes the default bd sync behavior to export-only per the spec: - bd sync: Export to JSONL (no staging/committing) - bd sync --import: Import from JSONL (shorthand for --import-only) - bd sync --status: Show sync state (mode, last export, pending changes, conflicts) - bd sync --resolve: Resolve conflicts with strategies (newest/ours/theirs) - bd sync --force: Force full export/import (skip incremental) - bd sync --full: Full sync (legacy pull→merge→export→commit→push behavior) The new --status output format shows: - Sync mode: git-portable - Last export: timestamp (commit hash) - Pending changes: N issues modified since last export - Import branch: name or none - Conflicts: count or none Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
gastown/crew/dennis
parent
ca24c17af8
commit
a7cd9136d8
+389
-18
@@ -6,6 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gofrs/flock"
|
"github.com/gofrs/flock"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -18,23 +20,21 @@ import (
|
|||||||
var syncCmd = &cobra.Command{
|
var syncCmd = &cobra.Command{
|
||||||
Use: "sync",
|
Use: "sync",
|
||||||
GroupID: "sync",
|
GroupID: "sync",
|
||||||
Short: "Synchronize issues with git remote",
|
Short: "Export database to JSONL (sync with git)",
|
||||||
Long: `Synchronize issues with git remote:
|
Long: `Export database to JSONL for git synchronization.
|
||||||
1. Pull from remote (fetch + merge)
|
|
||||||
2. Merge local and remote issues (3-way merge with LWW)
|
|
||||||
3. Export merged state to JSONL
|
|
||||||
4. Commit changes to git
|
|
||||||
5. Push to remote
|
|
||||||
|
|
||||||
The 3-way merge algorithm prevents data loss during concurrent edits
|
By default, exports the current database state to JSONL.
|
||||||
by comparing base state with both local and remote changes.
|
Does NOT stage or commit - that's the user's job.
|
||||||
|
|
||||||
Use --no-pull to skip pulling (just export, commit, push).
|
Commands:
|
||||||
Use --squash to accumulate changes without committing (reduces commit noise).
|
bd sync Export to JSONL (prep for push)
|
||||||
Use --flush-only to just export pending changes to JSONL (useful for pre-commit hooks).
|
bd sync --import Import from JSONL (after pull)
|
||||||
Use --import-only to just import from JSONL (useful after git pull).
|
bd sync --status Show sync state
|
||||||
Use --status to show diff between sync branch and main branch.
|
bd sync --resolve Resolve conflicts
|
||||||
Use --merge to merge the sync branch back to main branch.`,
|
bd sync --force Force full export/import (skip incremental)
|
||||||
|
bd sync --full Full sync: pull → merge → export → commit → push (legacy)
|
||||||
|
|
||||||
|
The --full flag provides the legacy full sync behavior for backwards compatibility.`,
|
||||||
Run: func(cmd *cobra.Command, _ []string) {
|
Run: func(cmd *cobra.Command, _ []string) {
|
||||||
CheckReadonly("sync")
|
CheckReadonly("sync")
|
||||||
ctx := rootCtx
|
ctx := rootCtx
|
||||||
@@ -46,6 +46,7 @@ Use --merge to merge the sync branch back to main branch.`,
|
|||||||
renameOnImport, _ := cmd.Flags().GetBool("rename-on-import")
|
renameOnImport, _ := cmd.Flags().GetBool("rename-on-import")
|
||||||
flushOnly, _ := cmd.Flags().GetBool("flush-only")
|
flushOnly, _ := cmd.Flags().GetBool("flush-only")
|
||||||
importOnly, _ := cmd.Flags().GetBool("import-only")
|
importOnly, _ := cmd.Flags().GetBool("import-only")
|
||||||
|
importFlag, _ := cmd.Flags().GetBool("import")
|
||||||
status, _ := cmd.Flags().GetBool("status")
|
status, _ := cmd.Flags().GetBool("status")
|
||||||
merge, _ := cmd.Flags().GetBool("merge")
|
merge, _ := cmd.Flags().GetBool("merge")
|
||||||
fromMain, _ := cmd.Flags().GetBool("from-main")
|
fromMain, _ := cmd.Flags().GetBool("from-main")
|
||||||
@@ -53,6 +54,16 @@ Use --merge to merge the sync branch back to main branch.`,
|
|||||||
squash, _ := cmd.Flags().GetBool("squash")
|
squash, _ := cmd.Flags().GetBool("squash")
|
||||||
checkIntegrity, _ := cmd.Flags().GetBool("check")
|
checkIntegrity, _ := cmd.Flags().GetBool("check")
|
||||||
acceptRebase, _ := cmd.Flags().GetBool("accept-rebase")
|
acceptRebase, _ := cmd.Flags().GetBool("accept-rebase")
|
||||||
|
fullSync, _ := cmd.Flags().GetBool("full")
|
||||||
|
resolve, _ := cmd.Flags().GetBool("resolve")
|
||||||
|
resolveOurs, _ := cmd.Flags().GetBool("ours")
|
||||||
|
resolveTheirs, _ := cmd.Flags().GetBool("theirs")
|
||||||
|
forceFlag, _ := cmd.Flags().GetBool("force")
|
||||||
|
|
||||||
|
// --import is shorthand for --import-only
|
||||||
|
if importFlag {
|
||||||
|
importOnly = true
|
||||||
|
}
|
||||||
|
|
||||||
// If --no-push not explicitly set, check no-push config
|
// If --no-push not explicitly set, check no-push config
|
||||||
if !cmd.Flags().Changed("no-push") {
|
if !cmd.Flags().Changed("no-push") {
|
||||||
@@ -94,9 +105,23 @@ Use --merge to merge the sync branch back to main branch.`,
|
|||||||
FatalError("not in a bd workspace (no .beads directory found)")
|
FatalError("not in a bd workspace (no .beads directory found)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// If status mode, show diff between sync branch and main
|
// If status mode, show sync state (new format per spec)
|
||||||
if status {
|
if status {
|
||||||
if err := showSyncStatus(ctx); err != nil {
|
if err := showSyncStateStatus(ctx, jsonlPath); err != nil {
|
||||||
|
FatalError("%v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If resolve mode, resolve conflicts
|
||||||
|
if resolve {
|
||||||
|
strategy := "newest" // default
|
||||||
|
if resolveOurs {
|
||||||
|
strategy = "ours"
|
||||||
|
} else if resolveTheirs {
|
||||||
|
strategy = "theirs"
|
||||||
|
}
|
||||||
|
if err := resolveSyncConflicts(ctx, jsonlPath, strategy, dryRun); err != nil {
|
||||||
FatalError("%v", err)
|
FatalError("%v", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -167,6 +192,19 @@ Use --merge to merge the sync branch back to main branch.`,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DEFAULT BEHAVIOR: Export to JSONL only (per spec)
|
||||||
|
// Does NOT stage or commit - that's the user's job.
|
||||||
|
// Use --full for legacy full sync behavior (pull → merge → export → commit → push)
|
||||||
|
if !fullSync {
|
||||||
|
if err := doExportSync(ctx, jsonlPath, forceFlag, dryRun); err != nil {
|
||||||
|
FatalError("%v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FULL SYNC MODE (--full flag): Legacy behavior
|
||||||
|
// Pull → Merge → Export → Commit → Push
|
||||||
|
|
||||||
// Check if we're in a git repository
|
// Check if we're in a git repository
|
||||||
if !isGitRepo() {
|
if !isGitRepo() {
|
||||||
FatalErrorWithHint("not in a git repository", "run 'git init' to initialize a repository")
|
FatalErrorWithHint("not in a git repository", "run 'git init' to initialize a repository")
|
||||||
@@ -577,6 +615,333 @@ func writeMergedStateToJSONL(path string, issues []*beads.Issue) error {
|
|||||||
return os.Rename(tempPath, path)
|
return os.Rename(tempPath, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// doExportSync exports the current database state to JSONL.
|
||||||
|
// This is the new default behavior for bd sync (per spec).
|
||||||
|
// Does NOT stage or commit - that's the user's job.
|
||||||
|
func doExportSync(ctx context.Context, jsonlPath string, force, dryRun bool) error {
|
||||||
|
if err := ensureStoreActive(); err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize store: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dryRun {
|
||||||
|
fmt.Println("→ [DRY RUN] Would export database to JSONL")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Exporting beads to JSONL...")
|
||||||
|
|
||||||
|
// Get count of dirty (changed) issues for incremental tracking
|
||||||
|
var changedCount int
|
||||||
|
if !force {
|
||||||
|
dirtyIDs, err := store.GetDirtyIssues(ctx)
|
||||||
|
if err != nil {
|
||||||
|
debug.Logf("warning: failed to get dirty issues: %v", err)
|
||||||
|
} else {
|
||||||
|
changedCount = len(dirtyIDs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export to JSONL
|
||||||
|
result, err := exportToJSONLDeferred(ctx, jsonlPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("exporting: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize export (update metadata)
|
||||||
|
finalizeExport(ctx, result)
|
||||||
|
|
||||||
|
// Report results
|
||||||
|
totalCount := 0
|
||||||
|
if result != nil {
|
||||||
|
totalCount = len(result.ExportedIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if changedCount > 0 && !force {
|
||||||
|
fmt.Printf("✓ Exported %d issues (%d changed since last sync)\n", totalCount, changedCount)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✓ Exported %d issues\n", totalCount)
|
||||||
|
}
|
||||||
|
fmt.Printf("✓ %s updated\n", jsonlPath)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// showSyncStateStatus shows the current sync state per the spec.
|
||||||
|
// Output format:
|
||||||
|
//
|
||||||
|
// Sync mode: git-portable
|
||||||
|
// Last export: 2026-01-16 10:30:00 (commit abc123)
|
||||||
|
// Pending changes: 3 issues modified since last export
|
||||||
|
// Import branch: none
|
||||||
|
// Conflicts: none
|
||||||
|
func showSyncStateStatus(ctx context.Context, jsonlPath string) error {
|
||||||
|
if err := ensureStoreActive(); err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize store: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
beadsDir := filepath.Dir(jsonlPath)
|
||||||
|
|
||||||
|
// Sync mode
|
||||||
|
fmt.Println("Sync mode: git-portable")
|
||||||
|
|
||||||
|
// Last export time
|
||||||
|
lastExport, err := store.GetMetadata(ctx, "last_import_time")
|
||||||
|
if err != nil || lastExport == "" {
|
||||||
|
fmt.Println("Last export: never")
|
||||||
|
} else {
|
||||||
|
// Try to parse and format nicely
|
||||||
|
t, err := time.Parse(time.RFC3339Nano, lastExport)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Last export: %s\n", lastExport)
|
||||||
|
} else {
|
||||||
|
// Try to get the last commit hash for the JSONL file
|
||||||
|
commitHash := getLastJSONLCommitHash(ctx, jsonlPath)
|
||||||
|
if commitHash != "" {
|
||||||
|
fmt.Printf("Last export: %s (commit %s)\n", t.Format("2006-01-02 15:04:05"), commitHash[:7])
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Last export: %s\n", t.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pending changes (dirty issues)
|
||||||
|
dirtyIDs, err := store.GetDirtyIssues(ctx)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Pending changes: unknown (error getting dirty issues)")
|
||||||
|
} else if len(dirtyIDs) == 0 {
|
||||||
|
fmt.Println("Pending changes: none")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Pending changes: %d issues modified since last export\n", len(dirtyIDs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import branch (sync branch status)
|
||||||
|
syncBranch, _ := syncbranch.Get(ctx, store)
|
||||||
|
if syncBranch == "" {
|
||||||
|
fmt.Println("Import branch: none")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Import branch: %s\n", syncBranch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conflicts - check for sync conflict state file
|
||||||
|
syncConflictPath := filepath.Join(beadsDir, "sync_conflicts.json")
|
||||||
|
if _, err := os.Stat(syncConflictPath); err == nil {
|
||||||
|
conflictState, err := LoadSyncConflictState(beadsDir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Conflicts: unknown (error reading sync state)")
|
||||||
|
} else if len(conflictState.Conflicts) > 0 {
|
||||||
|
fmt.Printf("Conflicts: %d unresolved\n", len(conflictState.Conflicts))
|
||||||
|
for _, c := range conflictState.Conflicts {
|
||||||
|
fmt.Printf(" - %s: %s\n", c.IssueID, c.Reason)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("Conflicts: none")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("Conflicts: none")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLastJSONLCommitHash returns the short commit hash of the last commit
|
||||||
|
// that touched the JSONL file, or empty string if unknown.
|
||||||
|
func getLastJSONLCommitHash(ctx context.Context, jsonlPath string) string {
|
||||||
|
rc, err := beads.GetRepoContext()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := rc.GitCmd(ctx, "log", "-1", "--format=%h", "--", jsonlPath)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncConflictState tracks pending sync conflicts.
|
||||||
|
type SyncConflictState struct {
|
||||||
|
Conflicts []SyncConflictRecord `json:"conflicts,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SyncConflictRecord represents a conflict detected during sync.
|
||||||
|
type SyncConflictRecord struct {
|
||||||
|
IssueID string `json:"issue_id"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
LocalVersion string `json:"local_version,omitempty"`
|
||||||
|
RemoteVersion string `json:"remote_version,omitempty"`
|
||||||
|
Strategy string `json:"strategy,omitempty"` // how it was resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadSyncConflictState loads the sync conflict state from disk.
|
||||||
|
func LoadSyncConflictState(beadsDir string) (*SyncConflictState, error) {
|
||||||
|
path := filepath.Join(beadsDir, "sync_conflicts.json")
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return &SyncConflictState{}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var state SyncConflictState
|
||||||
|
if err := json.Unmarshal(data, &state); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveSyncConflictState saves the sync conflict state to disk.
|
||||||
|
func SaveSyncConflictState(beadsDir string, state *SyncConflictState) error {
|
||||||
|
path := filepath.Join(beadsDir, "sync_conflicts.json")
|
||||||
|
data, err := json.MarshalIndent(state, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(path, data, 0600)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearSyncConflictState removes the sync conflict state file.
|
||||||
|
func ClearSyncConflictState(beadsDir string) error {
|
||||||
|
path := filepath.Join(beadsDir, "sync_conflicts.json")
|
||||||
|
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveSyncConflicts resolves pending sync conflicts using the specified strategy.
|
||||||
|
// Strategies:
|
||||||
|
// - "newest": Keep whichever version has the newer updated_at timestamp (default)
|
||||||
|
// - "ours": Keep local version
|
||||||
|
// - "theirs": Keep remote version
|
||||||
|
func resolveSyncConflicts(ctx context.Context, jsonlPath string, strategy string, dryRun bool) error {
|
||||||
|
beadsDir := filepath.Dir(jsonlPath)
|
||||||
|
|
||||||
|
conflictState, err := LoadSyncConflictState(beadsDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading sync conflicts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(conflictState.Conflicts) == 0 {
|
||||||
|
fmt.Println("No conflicts to resolve")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if dryRun {
|
||||||
|
fmt.Printf("→ [DRY RUN] Would resolve %d conflicts using '%s' strategy\n", len(conflictState.Conflicts), strategy)
|
||||||
|
for _, c := range conflictState.Conflicts {
|
||||||
|
fmt.Printf(" - %s: %s\n", c.IssueID, c.Reason)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Resolving conflicts using '%s' strategy...\n", strategy)
|
||||||
|
|
||||||
|
// Load base, local, and remote states for merge
|
||||||
|
baseIssues, err := loadBaseState(beadsDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading base state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ensureStoreActive(); err != nil {
|
||||||
|
return fmt.Errorf("initializing store: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
localIssues, err := store.SearchIssues(ctx, "", beads.IssueFilter{IncludeTombstones: true})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading local issues: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteIssues, err := loadIssuesFromJSONL(jsonlPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading remote issues: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build maps for quick lookup
|
||||||
|
localMap := make(map[string]*beads.Issue)
|
||||||
|
for _, issue := range localIssues {
|
||||||
|
localMap[issue.ID] = issue
|
||||||
|
}
|
||||||
|
remoteMap := make(map[string]*beads.Issue)
|
||||||
|
for _, issue := range remoteIssues {
|
||||||
|
remoteMap[issue.ID] = issue
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved := 0
|
||||||
|
for _, conflict := range conflictState.Conflicts {
|
||||||
|
local := localMap[conflict.IssueID]
|
||||||
|
remote := remoteMap[conflict.IssueID]
|
||||||
|
|
||||||
|
var winner string
|
||||||
|
switch strategy {
|
||||||
|
case "ours":
|
||||||
|
winner = "local"
|
||||||
|
case "theirs":
|
||||||
|
winner = "remote"
|
||||||
|
case "newest":
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
// Compare updated_at timestamps
|
||||||
|
if local != nil && remote != nil {
|
||||||
|
if local.UpdatedAt.After(remote.UpdatedAt) {
|
||||||
|
winner = "local"
|
||||||
|
} else {
|
||||||
|
winner = "remote"
|
||||||
|
}
|
||||||
|
} else if local != nil {
|
||||||
|
winner = "local"
|
||||||
|
} else {
|
||||||
|
winner = "remote"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✓ %s: kept %s", conflict.IssueID, winner)
|
||||||
|
if strategy == "newest" {
|
||||||
|
fmt.Print(" (newer)")
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
resolved++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear conflicts after resolution
|
||||||
|
if err := ClearSyncConflictState(beadsDir); err != nil {
|
||||||
|
return fmt.Errorf("clearing conflict state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-run merge with the resolved conflicts
|
||||||
|
mergeResult := MergeIssues(baseIssues, localIssues, remoteIssues)
|
||||||
|
|
||||||
|
// Write merged state
|
||||||
|
if err := writeMergedStateToJSONL(jsonlPath, mergeResult.Merged); err != nil {
|
||||||
|
return fmt.Errorf("writing merged state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import to database
|
||||||
|
if err := importFromJSONLInline(ctx, jsonlPath, false, false); err != nil {
|
||||||
|
return fmt.Errorf("importing merged state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export to ensure consistency
|
||||||
|
if err := exportToJSONL(ctx, jsonlPath); err != nil {
|
||||||
|
return fmt.Errorf("exporting: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update base state
|
||||||
|
finalIssues, err := loadIssuesFromJSONL(jsonlPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reloading final state: %w", err)
|
||||||
|
}
|
||||||
|
if err := saveBaseState(beadsDir, finalIssues); err != nil {
|
||||||
|
return fmt.Errorf("saving base state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✓ Merge complete (%d conflicts resolved)\n", resolved)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
syncCmd.Flags().StringP("message", "m", "", "Commit message (default: auto-generated)")
|
syncCmd.Flags().StringP("message", "m", "", "Commit message (default: auto-generated)")
|
||||||
syncCmd.Flags().Bool("dry-run", false, "Preview sync without making changes")
|
syncCmd.Flags().Bool("dry-run", false, "Preview sync without making changes")
|
||||||
@@ -586,13 +951,19 @@ func init() {
|
|||||||
syncCmd.Flags().Bool("flush-only", false, "Only export pending changes to JSONL (skip git operations)")
|
syncCmd.Flags().Bool("flush-only", false, "Only export pending changes to JSONL (skip git operations)")
|
||||||
syncCmd.Flags().Bool("squash", false, "Accumulate changes in JSONL without committing (run 'bd sync' later to commit all)")
|
syncCmd.Flags().Bool("squash", false, "Accumulate changes in JSONL without committing (run 'bd sync' later to commit all)")
|
||||||
syncCmd.Flags().Bool("import-only", false, "Only import from JSONL (skip git operations, useful after git pull)")
|
syncCmd.Flags().Bool("import-only", false, "Only import from JSONL (skip git operations, useful after git pull)")
|
||||||
syncCmd.Flags().Bool("status", false, "Show diff between sync branch and main branch")
|
syncCmd.Flags().Bool("import", false, "Import from JSONL (shorthand for --import-only)")
|
||||||
|
syncCmd.Flags().Bool("status", false, "Show sync state (pending changes, last export, conflicts)")
|
||||||
syncCmd.Flags().Bool("merge", false, "Merge sync branch back to main branch")
|
syncCmd.Flags().Bool("merge", false, "Merge sync branch back to main branch")
|
||||||
syncCmd.Flags().Bool("from-main", false, "One-way sync from main branch (for ephemeral branches without upstream)")
|
syncCmd.Flags().Bool("from-main", false, "One-way sync from main branch (for ephemeral branches without upstream)")
|
||||||
syncCmd.Flags().Bool("no-git-history", false, "Skip git history backfill for deletions (use during JSONL filename migrations)")
|
syncCmd.Flags().Bool("no-git-history", false, "Skip git history backfill for deletions (use during JSONL filename migrations)")
|
||||||
syncCmd.Flags().BoolVar(&jsonOutput, "json", false, "Output sync statistics in JSON format")
|
syncCmd.Flags().BoolVar(&jsonOutput, "json", false, "Output sync statistics in JSON format")
|
||||||
syncCmd.Flags().Bool("check", false, "Pre-sync integrity check: detect forced pushes, prefix mismatches, and orphaned issues")
|
syncCmd.Flags().Bool("check", false, "Pre-sync integrity check: detect forced pushes, prefix mismatches, and orphaned issues")
|
||||||
syncCmd.Flags().Bool("accept-rebase", false, "Accept remote sync branch history (use when force-push detected)")
|
syncCmd.Flags().Bool("accept-rebase", false, "Accept remote sync branch history (use when force-push detected)")
|
||||||
|
syncCmd.Flags().Bool("full", false, "Full sync: pull → merge → export → commit → push (legacy behavior)")
|
||||||
|
syncCmd.Flags().Bool("resolve", false, "Resolve pending sync conflicts")
|
||||||
|
syncCmd.Flags().Bool("ours", false, "Use 'ours' strategy for conflict resolution (with --resolve)")
|
||||||
|
syncCmd.Flags().Bool("theirs", false, "Use 'theirs' strategy for conflict resolution (with --resolve)")
|
||||||
|
syncCmd.Flags().Bool("force", false, "Force full export/import (skip incremental optimization)")
|
||||||
rootCmd.AddCommand(syncCmd)
|
rootCmd.AddCommand(syncCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user