223 lines
6.5 KiB
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
|
|
}
|