temp for tree extraction
This commit is contained in:
296
internal/commands/push.go
Normal file
296
internal/commands/push.go
Normal file
@ -0,0 +1,296 @@
|
||||
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
|
||||
}
|
Reference in New Issue
Block a user