Files
onyx/internal/commands/sync.go

223 lines
6.5 KiB
Go

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
}