package commands import ( "fmt" "os" "path/filepath" "git.dws.rip/DWS/onyx/internal/core" "git.dws.rip/DWS/onyx/internal/git" "git.dws.rip/DWS/onyx/internal/models" "git.dws.rip/DWS/onyx/internal/storage" gogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/spf13/cobra" ) // NewSyncCmd creates the sync command func NewSyncCmd() *cobra.Command { var remoteName string cmd := &cobra.Command{ Use: "sync", Short: "Sync the current workstream with the remote base branch", Long: `Synchronize the current workstream with the remote base branch. This command will: 1. Fetch the latest changes from the remote 2. Rebase the workstream commits onto the updated base branch 3. Use rerere to automatically resolve known conflicts 4. Update all branch references in the workstream If conflicts occur during the rebase, you will need to resolve them manually and then continue the sync operation.`, RunE: func(cmd *cobra.Command, args []string) error { return runSync(remoteName) }, } cmd.Flags().StringVarP(&remoteName, "remote", "r", "origin", "Remote to sync with") return cmd } // runSync executes the sync command func runSync(remoteName string) error { // Get current directory cwd, err := os.Getwd() if err != nil { return fmt.Errorf("failed to get current directory: %w", err) } // Check if this is an Onyx repository if !core.IsOnyxRepo(cwd) { return fmt.Errorf("not an Onyx repository. Run 'onx init' first") } // Open the repository repo, err := core.Open(cwd) if err != nil { return fmt.Errorf("failed to open repository: %w", err) } defer repo.Close() // Use ExecuteWithTransaction to capture state err = core.ExecuteWithTransaction(repo, "sync", "Synced with remote", func() error { return executeSync(repo, cwd, remoteName) }) if err != nil { return err } fmt.Println("✓ Sync completed successfully") return nil } // executeSync performs the actual sync operation func executeSync(repo *core.OnyxRepository, repoPath, remoteName string) error { gitRepo := repo.GetGitRepo() onyxPath := repo.GetOnyxPath() // 1. Validate remote exists remoteHelper := git.NewRemoteHelper(gitRepo) if err := remoteHelper.ValidateRemote(remoteName); err != nil { return fmt.Errorf("remote validation failed: %w", err) } // 2. Get current workstream wsManager := core.NewWorkstreamManager(repo) currentWorkstream, err := wsManager.GetCurrentWorkstream() if err != nil { return fmt.Errorf("no active workstream: %w", err) } if currentWorkstream.IsEmpty() { return fmt.Errorf("workstream has no commits to sync") } // 3. Get authentication for the remote remoteURL, err := remoteHelper.GetRemoteURL(remoteName) if err != nil { return fmt.Errorf("failed to get remote URL: %w", err) } authProvider := git.NewAuthProvider() authMethod, err := authProvider.GetAuthMethod(remoteURL) if err != nil { // Log the error but continue - some remotes might not need auth fmt.Fprintf(os.Stderr, "Warning: authentication not available: %v\n", err) fmt.Fprintf(os.Stderr, "Attempting fetch without authentication...\n") } // 4. Fetch from remote fmt.Printf("Fetching from %s...\n", remoteName) remote, err := remoteHelper.GetRemote(remoteName) if err != nil { return fmt.Errorf("failed to get remote: %w", err) } err = remote.Fetch(&gogit.FetchOptions{ Auth: authMethod, RefSpecs: []config.RefSpec{ config.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/remotes/%s/%s", currentWorkstream.BaseBranch, remoteName, currentWorkstream.BaseBranch)), }, Progress: os.Stdout, }) if err != nil && err != gogit.NoErrAlreadyUpToDate { return fmt.Errorf("failed to fetch: %w", err) } if err == gogit.NoErrAlreadyUpToDate { fmt.Println("Already up to date") } // 5. Get the updated base branch HEAD gitBackend := git.NewGitBackend(gitRepo) remoteRef := fmt.Sprintf("refs/remotes/%s/%s", remoteName, currentWorkstream.BaseBranch) newBaseSHA, err := gitBackend.GetRef(remoteRef) if err != nil { return fmt.Errorf("failed to get remote base branch: %w", err) } // 6. Build the commit stack from the workstream stack := []string{} for _, commit := range currentWorkstream.Commits { stack = append(stack, commit.SHA) } // 7. Create rebase engine with rerere support rebaseEngine := git.NewRebaseEngine(gitRepo, onyxPath, repoPath) fmt.Printf("Rebasing %d commit(s) onto %s...\n", len(stack), newBaseSHA[:8]) // 8. Perform the rebase result, err := rebaseEngine.RebaseStack(stack, newBaseSHA) if err != nil { return fmt.Errorf("rebase failed: %w", err) } // 9. Handle rebase result if !result.Success { if len(result.ConflictingFiles) > 0 { // Present conflicts to user conflictResolver := rebaseEngine.GetConflictResolver() conflictMsg := conflictResolver.PresentConflicts(result.ConflictingFiles) fmt.Println(conflictMsg) return fmt.Errorf("sync paused due to conflicts") } return fmt.Errorf("rebase failed: %s", result.Message) } // 10. Update workstream commits with new SHAs if err := updateWorkstreamCommits(repo, currentWorkstream, result.RebasedCommits); err != nil { return fmt.Errorf("failed to update workstream: %w", err) } // 11. Update the base commit metadata currentWorkstream.Metadata["base_commit"] = newBaseSHA wsCollection, err := storage.LoadWorkstreams(filepath.Join(onyxPath, "workstreams.json")) if err != nil { return fmt.Errorf("failed to load workstreams: %w", err) } if err := storage.SaveWorkstreams(filepath.Join(onyxPath, "workstreams.json"), wsCollection); err != nil { return fmt.Errorf("failed to save workstreams: %w", err) } fmt.Printf("✓ Rebased %d commit(s) successfully\n", len(result.RebasedCommits)) return nil } // updateWorkstreamCommits updates the workstream with new rebased commit SHAs func updateWorkstreamCommits(repo *core.OnyxRepository, ws *models.Workstream, newSHAs []string) error { if len(ws.Commits) != len(newSHAs) { return fmt.Errorf("mismatch between old and new commit counts") } gitBackend := git.NewGitBackend(repo.GetGitRepo()) // Update each commit SHA and its branch ref for i := range ws.Commits { oldSHA := ws.Commits[i].SHA newSHA := newSHAs[i] // Update the commit SHA ws.Commits[i].SHA = newSHA // Update the branch ref to point to the new commit branchRef := ws.Commits[i].BranchRef if branchRef != "" { if err := gitBackend.UpdateRef(branchRef, newSHA); err != nil { return fmt.Errorf("failed to update ref %s: %w", branchRef, err) } } fmt.Printf(" %s -> %s\n", oldSHA[:8], newSHA[:8]) } return nil }