Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions pkg/cli/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -533,3 +533,116 @@ func checkWorkflowFileStatus(workflowPath string) (*WorkflowFileStatus, error) {

return status, nil
}

// hasChangesToCommit checks if there are any changes in the working directory
func hasChangesToCommit() (bool, error) {
gitLog.Print("Checking for modified files")
cmd := exec.Command("git", "status", "--porcelain")
output, err := cmd.Output()
if err != nil {
gitLog.Printf("Failed to check git status: %v", err)
return false, fmt.Errorf("failed to check git status: %w", err)
}

hasChanges := len(strings.TrimSpace(string(output))) > 0
gitLog.Printf("Has changes to commit: %v", hasChanges)
return hasChanges, nil
}

// hasRemote checks if a remote repository named 'origin' is configured
func hasRemote() bool {
gitLog.Print("Checking for remote repository")
cmd := exec.Command("git", "remote", "get-url", "origin")
err := cmd.Run()
hasRemoteRepo := err == nil
gitLog.Printf("Has remote repository: %v", hasRemoteRepo)
return hasRemoteRepo
}

// pullFromRemote pulls the latest changes from the remote repository using rebase
func pullFromRemote(verbose bool) error {
gitLog.Print("Pulling latest changes from remote")
pullCmd := exec.Command("git", "pull", "--rebase")
if output, err := pullCmd.CombinedOutput(); err != nil {
gitLog.Printf("Failed to pull changes: %v, output: %s", err, string(output))
return fmt.Errorf("failed to pull changes: %w", err)
}
gitLog.Print("Successfully pulled latest changes")
return nil
}

// stageAllChanges stages all modified files using git add -A
func stageAllChanges(verbose bool) error {
gitLog.Print("Staging all changes")
addCmd := exec.Command("git", "add", "-A")
if output, err := addCmd.CombinedOutput(); err != nil {
gitLog.Printf("Failed to stage changes: %v, output: %s", err, string(output))
return fmt.Errorf("failed to stage changes: %w", err)
}
gitLog.Print("Successfully staged all changes")
return nil
}

// pushToRemote pushes the current branch to the remote repository
func pushToRemote(verbose bool) error {
gitLog.Print("Pushing changes to remote")
pushCmd := exec.Command("git", "push")
if output, err := pushCmd.CombinedOutput(); err != nil {
gitLog.Printf("Failed to push changes: %v, output: %s", err, string(output))
return fmt.Errorf("failed to push changes: %w\nOutput: %s", err, string(output))
}
gitLog.Print("Successfully pushed changes to remote")
return nil
}

// commitAndPushChanges is a helper that orchestrates the full commit and push workflow
// It checks for changes, pulls from remote (if exists), stages all changes, commits, and pushes (if remote exists)
func commitAndPushChanges(commitMessage string, verbose bool) error {
gitLog.Printf("Starting commit and push workflow with message: %s", commitMessage)

// Check if there are any changes to commit
hasChanges, err := hasChangesToCommit()
if err != nil {
return err
}

if !hasChanges {
gitLog.Print("No changes to commit")
return nil
}

// Pull latest changes from remote before committing (if remote exists)
if hasRemote() {
gitLog.Print("Remote repository exists, pulling latest changes")
if err := pullFromRemote(verbose); err != nil {
return err
}
} else {
gitLog.Print("No remote repository configured, skipping pull")
}

// Stage all modified files
if err := stageAllChanges(verbose); err != nil {
return err
}

// Commit the changes
gitLog.Printf("Committing changes with message: %s", commitMessage)
if err := commitChanges(commitMessage, verbose); err != nil {
gitLog.Printf("Failed to commit changes: %v", err)
return fmt.Errorf("failed to commit changes: %w", err)
}

// Push the changes (only if remote exists)
if hasRemote() {
gitLog.Print("Remote repository exists, pushing changes")
if err := pushToRemote(verbose); err != nil {
return err
}
} else {
gitLog.Print("No remote repository configured, skipping push")
}

gitLog.Print("Commit and push workflow completed successfully")
return nil
}
39 changes: 38 additions & 1 deletion pkg/cli/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,9 +444,18 @@ func attemptSetSecret(secretName, repoSlug string, verbose bool) error {
}

// InitRepository initializes the repository for agentic workflows
func InitRepository(verbose bool, mcp bool, campaign bool, tokens bool, engine string, codespaceRepos []string, codespaceEnabled bool, completions bool, rootCmd CommandProvider) error {
func InitRepository(verbose bool, mcp bool, campaign bool, tokens bool, engine string, codespaceRepos []string, codespaceEnabled bool, completions bool, push bool, rootCmd CommandProvider) error {
initLog.Print("Starting repository initialization for agentic workflows")

// If --push is enabled, ensure git status is clean before starting
if push {
initLog.Print("Checking for clean working directory (--push enabled)")
if err := checkCleanWorkingDirectory(verbose); err != nil {
initLog.Printf("Git status check failed: %v", err)
return fmt.Errorf("--push requires a clean working directory: %w", err)
}
}

// Ensure we're in a git repository
if !isGitRepo() {
initLog.Print("Not in a git repository, initialization failed")
Expand Down Expand Up @@ -680,6 +689,34 @@ func InitRepository(verbose bool, mcp bool, campaign bool, tokens bool, engine s

initLog.Print("Repository initialization completed successfully")

// If --push is enabled, commit and push changes
if push {
initLog.Print("Push enabled - preparing to commit and push changes")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Preparing to commit and push changes..."))

// Use the helper function to orchestrate the full workflow
commitMessage := "chore: initialize agentic workflows"
if err := commitAndPushChanges(commitMessage, verbose); err != nil {
// Check if it's the "no changes" case
hasChanges, checkErr := hasChangesToCommit()
if checkErr == nil && !hasChanges {
initLog.Print("No changes to commit")
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("No changes to commit"))
} else {
return err
}
} else {
// Print success messages based on whether remote exists
fmt.Fprintln(os.Stderr, "")
if hasRemote() {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("✓ Changes pushed to remote"))
} else {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("✓ Changes committed locally (no remote configured)"))
}
}
}

// Display success message with next steps
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Repository initialized for agentic workflows!"))
Expand Down
11 changes: 7 additions & 4 deletions pkg/cli/init_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ Examples:
` + string(constants.CLIExtensionPrefix) + ` init --tokens --engine copilot # Check Copilot tokens
` + string(constants.CLIExtensionPrefix) + ` init --codespaces # Configure Codespaces
` + string(constants.CLIExtensionPrefix) + ` init --codespaces repo1,repo2 # Codespaces with additional repos
` + string(constants.CLIExtensionPrefix) + ` init --completions # Install shell completions`,
` + string(constants.CLIExtensionPrefix) + ` init --completions # Install shell completions
` + string(constants.CLIExtensionPrefix) + ` init --push # Initialize and automatically commit/push`,
RunE: func(cmd *cobra.Command, args []string) error {
verbose, _ := cmd.Flags().GetBool("verbose")
mcpFlag, _ := cmd.Flags().GetBool("mcp")
Expand All @@ -95,6 +96,7 @@ Examples:
codespaceReposStr, _ := cmd.Flags().GetString("codespaces")
codespaceEnabled := cmd.Flags().Changed("codespaces")
completions, _ := cmd.Flags().GetBool("completions")
push, _ := cmd.Flags().GetBool("push")

// Determine MCP state: default true, unless --no-mcp is specified
// --mcp flag is kept for backward compatibility (hidden from help)
Expand Down Expand Up @@ -122,15 +124,15 @@ Examples:
if !cmd.Flags().Changed("mcp") && !cmd.Flags().Changed("no-mcp") &&
!cmd.Flags().Changed("campaign") && !cmd.Flags().Changed("tokens") &&
!cmd.Flags().Changed("engine") && !cmd.Flags().Changed("codespaces") &&
!cmd.Flags().Changed("completions") {
!cmd.Flags().Changed("completions") && !cmd.Flags().Changed("push") {

// Enter interactive mode
initCommandLog.Print("Entering interactive mode")
return InitRepositoryInteractive(verbose, cmd.Root())
}

initCommandLog.Printf("Executing init command: verbose=%v, mcp=%v, campaign=%v, tokens=%v, engine=%v, codespaces=%v, codespaceEnabled=%v, completions=%v", verbose, mcp, campaign, tokens, engine, codespaceRepos, codespaceEnabled, completions)
if err := InitRepository(verbose, mcp, campaign, tokens, engine, codespaceRepos, codespaceEnabled, completions, cmd.Root()); err != nil {
initCommandLog.Printf("Executing init command: verbose=%v, mcp=%v, campaign=%v, tokens=%v, engine=%v, codespaces=%v, codespaceEnabled=%v, completions=%v, push=%v", verbose, mcp, campaign, tokens, engine, codespaceRepos, codespaceEnabled, completions, push)
if err := InitRepository(verbose, mcp, campaign, tokens, engine, codespaceRepos, codespaceEnabled, completions, push, cmd.Root()); err != nil {
initCommandLog.Printf("Init command failed: %v", err)
return err
}
Expand All @@ -148,6 +150,7 @@ Examples:
// NoOptDefVal allows using --codespaces without a value (returns empty string when no value provided)
cmd.Flags().Lookup("codespaces").NoOptDefVal = " "
cmd.Flags().Bool("completions", false, "Install shell completion for the detected shell (bash, zsh, fish, or PowerShell)")
cmd.Flags().Bool("push", false, "Automatically commit and push changes after successful initialization")

// Hide the deprecated --mcp flag from help (kept for backward compatibility)
_ = cmd.Flags().MarkHidden("mcp")
Expand Down
58 changes: 29 additions & 29 deletions pkg/cli/init_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,9 @@ func TestInitRepositoryBasic(t *testing.T) {
exec.Command("git", "config", "user.email", "test@example.com").Run()

// Test basic init with MCP enabled by default (mcp=true, noMcp=false behavior)
err = InitRepository(false, true, false, false, "", []string{}, false, false, nil)
err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil)
if err != nil {
t.Fatalf("InitRepository(, false, nil) failed: %v", err)
t.Fatalf("InitRepository(, false, false, nil) failed: %v", err)
}

// Verify .gitattributes was created/updated
Expand Down Expand Up @@ -245,9 +245,9 @@ func TestInitRepositoryWithMCP(t *testing.T) {
exec.Command("git", "config", "user.email", "test@example.com").Run()

// Test init with MCP explicitly enabled (same as default)
err = InitRepository(false, true, false, false, "", []string{}, false, false, nil)
err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil)
if err != nil {
t.Fatalf("InitRepository(, false, nil) with MCP failed: %v", err)
t.Fatalf("InitRepository(, false, false, nil) with MCP failed: %v", err)
}

// Verify .vscode/mcp.json was created
Expand Down Expand Up @@ -288,9 +288,9 @@ func TestInitRepositoryWithNoMCP(t *testing.T) {
exec.Command("git", "config", "user.email", "test@example.com").Run()

// Test init with --no-mcp flag (mcp=false)
err = InitRepository(false, false, false, false, "", []string{}, false, false, nil)
err = InitRepository(false, false, false, false, "", []string{}, false, false, false, nil)
if err != nil {
t.Fatalf("InitRepository(, false, nil) with --no-mcp failed: %v", err)
t.Fatalf("InitRepository(, false, false, nil) with --no-mcp failed: %v", err)
}

// Verify .vscode/mcp.json was NOT created
Expand Down Expand Up @@ -336,9 +336,9 @@ func TestInitRepositoryWithMCPBackwardCompatibility(t *testing.T) {
exec.Command("git", "config", "user.email", "test@example.com").Run()

// Test init with deprecated --mcp flag for backward compatibility (mcp=true)
err = InitRepository(false, true, false, false, "", []string{}, false, false, nil)
err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil)
if err != nil {
t.Fatalf("InitRepository(, false, nil) with deprecated --mcp flag failed: %v", err)
t.Fatalf("InitRepository(, false, false, nil) with deprecated --mcp flag failed: %v", err)
}

// Verify .vscode/mcp.json was created
Expand Down Expand Up @@ -379,9 +379,9 @@ func TestInitRepositoryVerbose(t *testing.T) {
exec.Command("git", "config", "user.email", "test@example.com").Run()

// Test verbose mode with MCP enabled by default (should not error, just produce more output)
err = InitRepository(true, true, false, false, "", []string{}, false, false, nil)
err = InitRepository(true, true, false, false, "", []string{}, false, false, false, nil)
if err != nil {
t.Fatalf("InitRepository(, false, nil) in verbose mode failed: %v", err)
t.Fatalf("InitRepository(, false, false, nil) in verbose mode failed: %v", err)
}

// Verify basic files were still created
Expand All @@ -406,12 +406,12 @@ func TestInitRepositoryNotInGitRepo(t *testing.T) {
}

// Don't initialize git repo - should fail for some operations
err = InitRepository(false, true, false, false, "", []string{}, false, false, nil)
err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil)

// The function should handle this gracefully or return an error
// Based on the implementation, ensureGitAttributes requires git
if err == nil {
t.Log("InitRepository(, false, nil) succeeded despite not being in a git repo")
t.Log("InitRepository(, false, false, nil) succeeded despite not being in a git repo")
}
}

Expand Down Expand Up @@ -440,15 +440,15 @@ func TestInitRepositoryIdempotent(t *testing.T) {
exec.Command("git", "config", "user.email", "test@example.com").Run()

// Run init twice with MCP enabled by default
err = InitRepository(false, true, false, false, "", []string{}, false, false, nil)
err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil)
if err != nil {
t.Fatalf("First InitRepository(, false, nil) failed: %v", err)
t.Fatalf("First InitRepository(, false, false, nil) failed: %v", err)
}

// Second run should be idempotent
err = InitRepository(false, true, false, false, "", []string{}, false, false, nil)
err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil)
if err != nil {
t.Fatalf("Second InitRepository(, false, nil) failed: %v", err)
t.Fatalf("Second InitRepository(, false, false, nil) failed: %v", err)
}

// Verify .gitattributes still correct
Expand Down Expand Up @@ -491,14 +491,14 @@ func TestInitRepositoryWithMCPIdempotent(t *testing.T) {
exec.Command("git", "config", "user.email", "test@example.com").Run()

// Run init with MCP twice
err = InitRepository(false, true, false, false, "", []string{}, false, false, nil)
err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil)
if err != nil {
t.Fatalf("First InitRepository(, false, nil) with MCP failed: %v", err)
t.Fatalf("First InitRepository(, false, false, nil) with MCP failed: %v", err)
}

err = InitRepository(false, true, false, false, "", []string{}, false, false, nil)
err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil)
if err != nil {
t.Fatalf("Second InitRepository(, false, nil) with MCP failed: %v", err)
t.Fatalf("Second InitRepository(, false, false, nil) with MCP failed: %v", err)
}

// Verify files still exist and are correct
Expand Down Expand Up @@ -538,9 +538,9 @@ func TestInitRepositoryCreatesDirectories(t *testing.T) {
exec.Command("git", "config", "user.email", "test@example.com").Run()

// Run init with MCP
err = InitRepository(false, true, false, false, "", []string{}, false, false, nil)
err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil)
if err != nil {
t.Fatalf("InitRepository(, false, nil) failed: %v", err)
t.Fatalf("InitRepository(, false, false, nil) failed: %v", err)
}

// Verify directory structure
Expand Down Expand Up @@ -606,7 +606,7 @@ func TestInitRepositoryErrorHandling(t *testing.T) {
}

// Test init without git repo (with MCP enabled by default)
err = InitRepository(false, true, false, false, "", []string{}, false, false, nil)
err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil)

// Should handle error gracefully or return error
// The actual behavior depends on implementation
Expand Down Expand Up @@ -649,9 +649,9 @@ func TestInitRepositoryWithExistingFiles(t *testing.T) {
}

// Run init with MCP enabled by default
err = InitRepository(false, true, false, false, "", []string{}, false, false, nil)
err = InitRepository(false, true, false, false, "", []string{}, false, false, false, nil)
if err != nil {
t.Fatalf("InitRepository(, false, nil) failed: %v", err)
t.Fatalf("InitRepository(, false, false, nil) failed: %v", err)
}

// Verify existing content is preserved and new entry is added
Expand Down Expand Up @@ -699,9 +699,9 @@ func TestInitRepositoryWithCodespace(t *testing.T) {

// Test init with --codespaces flag (with MCP enabled by default and additional repos)
additionalRepos := []string{"org/repo1", "owner/repo2"}
err = InitRepository(false, true, false, false, "", additionalRepos, true, false, nil)
err = InitRepository(false, true, false, false, "", additionalRepos, true, false, false, nil)
if err != nil {
t.Fatalf("InitRepository(, false, nil) with codespaces failed: %v", err)
t.Fatalf("InitRepository(, false, false, nil) with codespaces failed: %v", err)
}

// Verify .devcontainer/devcontainer.json was created at default location
Expand Down Expand Up @@ -764,9 +764,9 @@ func TestInitCommandWithCodespacesNoArgs(t *testing.T) {
exec.Command("git", "config", "user.email", "test@example.com").Run()

// Test init with --codespaces flag (no additional repos, MCP enabled by default)
err = InitRepository(false, true, false, false, "", []string{}, true, false, nil)
err = InitRepository(false, true, false, false, "", []string{}, true, false, false, nil)
if err != nil {
t.Fatalf("InitRepository(, false, nil) with codespaces (no args) failed: %v", err)
t.Fatalf("InitRepository(, false, false, nil) with codespaces (no args) failed: %v", err)
}

// Verify .devcontainer/devcontainer.json was created at default location
Expand Down
Loading
Loading