package commands import ( "fmt" "os" "strings" "git.dws.rip/DWS/onyx/internal/core" "git.dws.rip/DWS/onyx/internal/git" gogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/spf13/cobra" ) // NewPushCmd creates the push command func NewPushCmd() *cobra.Command { var remoteName string var force bool var stacked bool cmd := &cobra.Command{ Use: "push", Short: "Push the current workstream to the remote repository", Long: `Push the current workstream to the remote repository. By default, pushes as a single branch (clean, traditional workflow). Use --stacked to push each commit as a separate branch (advanced stacked diffs). Single-branch mode (default): - Pushes workstream as one branch with all commits - Clean remote UI (1 branch per workstream) - Perfect for traditional PR workflows - Example: 'milestone-4' branch Stacked mode (--stacked): - Pushes each commit as a separate branch - Enables stacked diff workflow (Meta/Google style) - Each commit can have its own PR - Example: 'onyx/workstreams/milestone-4/commit-1', 'commit-2', etc.`, RunE: func(cmd *cobra.Command, args []string) error { return runPush(remoteName, force, stacked) }, } cmd.Flags().StringVarP(&remoteName, "remote", "r", "origin", "Remote to push to") cmd.Flags().BoolVarP(&force, "force", "f", false, "Force push (use with caution)") cmd.Flags().BoolVar(&stacked, "stacked", false, "Push each commit as separate branch (stacked diffs)") return cmd } // runPush executes the push command func runPush(remoteName string, force, stacked bool) 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, "push", "Pushed to remote", func() error { if stacked { return executePushStacked(repo, remoteName, force) } return executePushSingleBranch(repo, remoteName, force) }) if err != nil { return err } fmt.Println("✓ Push completed successfully") return nil } // executePushSingleBranch pushes the workstream as a single branch (default behavior) func executePushSingleBranch(repo *core.OnyxRepository, remoteName string, force bool) error { gitRepo := repo.GetGitRepo() // 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 push") } // 3. Get the remote remote, err := remoteHelper.GetRemote(remoteName) if err != nil { return fmt.Errorf("failed to get remote: %w", err) } // 4. Get the latest commit in the workstream latestCommit, err := currentWorkstream.GetLatestCommit() if err != nil { return fmt.Errorf("failed to get latest commit: %w", err) } // 5. Build refspec to push the latest commit to a branch named after the workstream branchName := currentWorkstream.Name localRef := latestCommit.BranchRef remoteRef := fmt.Sprintf("refs/heads/%s", branchName) // Also create/update the local branch to point to the latest commit // This ensures `refs/heads/master` (or whatever the workstream is) exists locally gitBackend := git.NewGitBackend(gitRepo) localBranchRef := fmt.Sprintf("refs/heads/%s", branchName) if err := gitBackend.UpdateRef(localBranchRef, latestCommit.SHA); err != nil { // Warn but don't fail - the push can still succeed fmt.Fprintf(os.Stderr, "Warning: failed to update local branch %s: %v\n", branchName, err) } refSpec := config.RefSpec(fmt.Sprintf("%s:%s", localRef, remoteRef)) if force { refSpec = config.RefSpec(fmt.Sprintf("+%s:%s", localRef, remoteRef)) } // 6. 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 push without authentication...\n") } // 7. Push to remote fmt.Printf("Pushing workstream '%s' to %s...\n", branchName, remoteName) err = remote.Push(&gogit.PushOptions{ Auth: authMethod, RefSpecs: []config.RefSpec{refSpec}, Progress: os.Stdout, }) if err != nil { if err == gogit.NoErrAlreadyUpToDate { fmt.Println("Already up to date") return nil } return fmt.Errorf("failed to push: %w", err) } fmt.Printf("✓ Pushed branch '%s' with %d commit(s)\n", branchName, len(currentWorkstream.Commits)) fmt.Printf("\nTo create a pull request:\n") fmt.Printf(" gh pr create --base %s --head %s\n", currentWorkstream.BaseBranch, branchName) return nil } // executePushStacked pushes each commit as a separate branch (stacked diffs) func executePushStacked(repo *core.OnyxRepository, remoteName string, force bool) error { gitRepo := repo.GetGitRepo() // 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 push") } // 3. Get the remote remote, err := remoteHelper.GetRemote(remoteName) if err != nil { return fmt.Errorf("failed to get remote: %w", err) } // 4. Build list of refspecs to push (all branches in the workstream) refspecs := []config.RefSpec{} // Also push the base branch if it exists locally baseBranch := currentWorkstream.BaseBranch if baseBranch != "" { // Check if base branch exists locally gitBackend := git.NewGitBackend(gitRepo) baseRef := fmt.Sprintf("refs/heads/%s", baseBranch) if _, err := gitBackend.GetRef(baseRef); err == nil { refSpec := config.RefSpec(fmt.Sprintf("refs/heads/%s:refs/heads/%s", baseBranch, baseBranch)) if force { refSpec = config.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/heads/%s", baseBranch, baseBranch)) } refspecs = append(refspecs, refSpec) } } // Push each commit's branch ref for i, commit := range currentWorkstream.Commits { branchRef := commit.BranchRef if branchRef == "" { continue } // Extract branch name from ref (e.g., refs/onyx/workstreams/foo/commit-1 -> foo/commit-1) // We'll push to refs/heads/onyx/workstreams/[workstream]/commit-[n] remoteBranch := fmt.Sprintf("onyx/workstreams/%s/commit-%d", currentWorkstream.Name, i+1) refSpec := config.RefSpec(fmt.Sprintf("%s:refs/heads/%s", branchRef, remoteBranch)) if force { refSpec = config.RefSpec(fmt.Sprintf("+%s:refs/heads/%s", branchRef, remoteBranch)) } refspecs = append(refspecs, refSpec) } if len(refspecs) == 0 { return fmt.Errorf("no branches to push") } // 5. 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 push without authentication...\n") } // 6. Push to remote fmt.Printf("Pushing %d branch(es) to %s...\n", len(refspecs), remoteName) err = remote.Push(&gogit.PushOptions{ Auth: authMethod, RefSpecs: refspecs, Progress: os.Stdout, }) if err != nil { if err == gogit.NoErrAlreadyUpToDate { fmt.Println("Already up to date") return nil } return fmt.Errorf("failed to push: %w", err) } fmt.Printf("✓ Pushed %d branch(es) successfully\n", len(refspecs)) // 7. Print summary of pushed branches fmt.Println("\nPushed branches (stacked diffs):") if baseBranch != "" { fmt.Printf(" - %s (base branch)\n", baseBranch) } for i, commit := range currentWorkstream.Commits { remoteBranch := fmt.Sprintf("onyx/workstreams/%s/commit-%d", currentWorkstream.Name, i+1) commitTitle := strings.Split(commit.Message, "\n")[0] if len(commitTitle) > 60 { commitTitle = commitTitle[:57] + "..." } fmt.Printf(" - %s: %s\n", remoteBranch, commitTitle) } fmt.Printf("\nTip: Each branch can have its own PR for incremental review\n") return nil }