feat: initial commit of Git Automation CLI
- Add comprehensive Git workflow automation tools - Include branch management utilities - Add commit helpers with conventional commit support - Implement GitHub integration for PR management - Add configuration management system - Include comprehensive test coverage - Add professional documentation and examples
This commit is contained in:
272
internal/cmd/branch.go
Normal file
272
internal/cmd/branch.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/iwasforcedtobehere/git-automation-cli/internal/config"
|
||||
"github.com/iwasforcedtobehere/git-automation-cli/internal/git"
|
||||
"github.com/iwasforcedtobehere/git-automation-cli/internal/validation"
|
||||
)
|
||||
|
||||
var branchCmd = &cobra.Command{
|
||||
Use: "branch",
|
||||
Short: "Branch utilities",
|
||||
Long: `Create, list, switch, and manage Git branches.
|
||||
Supports common branch naming conventions and workflows.`,
|
||||
}
|
||||
|
||||
var branchListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List branches",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
branches, err := git.LocalBranches(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list branches: %w", err)
|
||||
}
|
||||
|
||||
currentBranch, err := git.CurrentBranch(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current branch: %w", err)
|
||||
}
|
||||
|
||||
for _, branch := range branches {
|
||||
if branch == currentBranch {
|
||||
fmt.Printf("* %s\n", branch)
|
||||
} else {
|
||||
fmt.Printf(" %s\n", branch)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var branchCreateCmd = &cobra.Command{
|
||||
Use: "create [name]",
|
||||
Short: "Create a new branch",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
name := args[0]
|
||||
|
||||
// Validate Git repository
|
||||
if validationResult := validation.ValidateGitRepository(ctx); !validationResult.IsValid {
|
||||
return fmt.Errorf(validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Apply prefix if configured
|
||||
originalName := name
|
||||
if config.GlobalConfig.BranchPrefix != "" {
|
||||
name = config.GlobalConfig.BranchPrefix + name
|
||||
}
|
||||
|
||||
// Validate branch name
|
||||
if validationResult := validation.ValidateBranchName(name); !validationResult.IsValid {
|
||||
return fmt.Errorf("invalid branch name '%s': %s", originalName, validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Check if branch already exists
|
||||
branches, err := git.LocalBranches(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check existing branches: %w", err)
|
||||
}
|
||||
|
||||
for _, branch := range branches {
|
||||
if branch == name {
|
||||
return fmt.Errorf("branch '%s' already exists", name)
|
||||
}
|
||||
}
|
||||
|
||||
// Create the branch
|
||||
if err := git.CreateBranch(ctx, name); err != nil {
|
||||
return fmt.Errorf("failed to create branch: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Created branch: %s\n", name)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var branchSwitchCmd = &cobra.Command{
|
||||
Use: "switch [name]",
|
||||
Short: "Switch to a branch",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
name := args[0]
|
||||
|
||||
// Apply prefix if configured
|
||||
if config.GlobalConfig.BranchPrefix != "" && !strings.HasPrefix(name, config.GlobalConfig.BranchPrefix) {
|
||||
name = config.GlobalConfig.BranchPrefix + name
|
||||
}
|
||||
|
||||
// Check if branch exists
|
||||
branches, err := git.LocalBranches(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list branches: %w", err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, branch := range branches {
|
||||
if branch == name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("branch '%s' does not exist", name)
|
||||
}
|
||||
|
||||
// Switch to the branch
|
||||
if err := git.SwitchBranch(ctx, name); err != nil {
|
||||
return fmt.Errorf("failed to switch branch: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Switched to branch: %s\n", name)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var branchDeleteCmd = &cobra.Command{
|
||||
Use: "delete [name]",
|
||||
Short: "Delete a branch",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
name := args[0]
|
||||
|
||||
// Apply prefix if configured
|
||||
if config.GlobalConfig.BranchPrefix != "" && !strings.HasPrefix(name, config.GlobalConfig.BranchPrefix) {
|
||||
name = config.GlobalConfig.BranchPrefix + name
|
||||
}
|
||||
|
||||
// Check if branch exists
|
||||
branches, err := git.LocalBranches(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list branches: %w", err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, branch := range branches {
|
||||
if branch == name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("branch '%s' does not exist", name)
|
||||
}
|
||||
|
||||
// Check if it's the current branch
|
||||
currentBranch, err := git.CurrentBranch(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current branch: %w", err)
|
||||
}
|
||||
|
||||
if currentBranch == name {
|
||||
return fmt.Errorf("cannot delete the current branch '%s'", name)
|
||||
}
|
||||
|
||||
// Confirm deletion if configured
|
||||
if config.GlobalConfig.ConfirmDestructive {
|
||||
fmt.Printf("Are you sure you want to delete branch '%s'? [y/N]: ", name)
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
if strings.ToLower(response) != "y" && strings.ToLower(response) != "yes" {
|
||||
fmt.Println("Branch deletion cancelled")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the branch
|
||||
if err := git.DeleteBranch(ctx, name); err != nil {
|
||||
return fmt.Errorf("failed to delete branch: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Deleted branch: %s\n", name)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var branchFeatureCmd = &cobra.Command{
|
||||
Use: "feature [name]",
|
||||
Short: "Create a new feature branch",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
name := args[0]
|
||||
|
||||
// Apply feature prefix
|
||||
if config.GlobalConfig.FeaturePrefix != "" {
|
||||
name = config.GlobalConfig.FeaturePrefix + name
|
||||
}
|
||||
|
||||
// Check if branch already exists
|
||||
branches, err := git.LocalBranches(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check existing branches: %w", err)
|
||||
}
|
||||
|
||||
for _, branch := range branches {
|
||||
if branch == name {
|
||||
return fmt.Errorf("feature branch '%s' already exists", name)
|
||||
}
|
||||
}
|
||||
|
||||
// Create the feature branch
|
||||
if err := git.CreateBranch(ctx, name); err != nil {
|
||||
return fmt.Errorf("failed to create feature branch: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Created feature branch: %s\n", name)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var branchHotfixCmd = &cobra.Command{
|
||||
Use: "hotfix [name]",
|
||||
Short: "Create a new hotfix branch",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
name := args[0]
|
||||
|
||||
// Apply hotfix prefix
|
||||
if config.GlobalConfig.HotfixPrefix != "" {
|
||||
name = config.GlobalConfig.HotfixPrefix + name
|
||||
}
|
||||
|
||||
// Check if branch already exists
|
||||
branches, err := git.LocalBranches(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check existing branches: %w", err)
|
||||
}
|
||||
|
||||
for _, branch := range branches {
|
||||
if branch == name {
|
||||
return fmt.Errorf("hotfix branch '%s' already exists", name)
|
||||
}
|
||||
}
|
||||
|
||||
// Create the hotfix branch
|
||||
if err := git.CreateBranch(ctx, name); err != nil {
|
||||
return fmt.Errorf("failed to create hotfix branch: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Created hotfix branch: %s\n", name)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(branchCmd)
|
||||
branchCmd.AddCommand(branchListCmd)
|
||||
branchCmd.AddCommand(branchCreateCmd)
|
||||
branchCmd.AddCommand(branchSwitchCmd)
|
||||
branchCmd.AddCommand(branchDeleteCmd)
|
||||
branchCmd.AddCommand(branchFeatureCmd)
|
||||
branchCmd.AddCommand(branchHotfixCmd)
|
||||
}
|
134
internal/cmd/commands.go
Normal file
134
internal/cmd/commands.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/iwasforcedtobehere/git-automation-cli/internal/git"
|
||||
"github.com/iwasforcedtobehere/git-automation-cli/internal/config"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(syncCmd)
|
||||
rootCmd.AddCommand(cleanCmd)
|
||||
}
|
||||
|
||||
var syncCmd = &cobra.Command{
|
||||
Use: "sync",
|
||||
Short: "Sync with remote",
|
||||
Long: `Fetch the latest changes from remote, rebase the current branch onto
|
||||
its upstream tracking branch, and push the changes back to remote.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Check if we're in a Git repository
|
||||
if _, err := git.CurrentBranch(ctx); err != nil {
|
||||
return fmt.Errorf("not a git repository or no branch found")
|
||||
}
|
||||
|
||||
// Fetch latest changes
|
||||
if config.GlobalConfig.Verbose {
|
||||
fmt.Println("Fetching latest changes from remote...")
|
||||
}
|
||||
if err := git.Fetch(ctx); err != nil {
|
||||
return fmt.Errorf("failed to fetch from remote: %w", err)
|
||||
}
|
||||
|
||||
// Rebase onto tracking branch
|
||||
if config.GlobalConfig.Verbose {
|
||||
fmt.Println("Rebasing current branch onto tracking branch...")
|
||||
}
|
||||
if err := git.RebaseOntoTracking(ctx); err != nil {
|
||||
return fmt.Errorf("failed to rebase: %w", err)
|
||||
}
|
||||
|
||||
// Push changes
|
||||
if config.GlobalConfig.Verbose {
|
||||
fmt.Println("Pushing changes to remote...")
|
||||
}
|
||||
if err := git.PushCurrent(ctx, false); err != nil {
|
||||
return fmt.Errorf("failed to push: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Successfully synced with remote")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var cleanCmd = &cobra.Command{
|
||||
Use: "clean",
|
||||
Short: "Clean local feature branches",
|
||||
Long: `Delete local feature branches that have been merged or are no longer needed.
|
||||
By default, only deletes branches with the 'feature/' prefix.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
// Check if we're in a Git repository
|
||||
if _, err := git.CurrentBranch(ctx); err != nil {
|
||||
return fmt.Errorf("not a git repository or no branch found")
|
||||
}
|
||||
|
||||
// Get all local branches
|
||||
branches, err := git.LocalBranches(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list branches: %w", err)
|
||||
}
|
||||
|
||||
// Get current branch to avoid deleting it
|
||||
currentBranch, err := git.CurrentBranch(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current branch: %w", err)
|
||||
}
|
||||
|
||||
var removed []string
|
||||
var prefix string
|
||||
|
||||
// Determine which prefix to use for cleaning
|
||||
if config.GlobalConfig.FeaturePrefix != "" {
|
||||
prefix = config.GlobalConfig.FeaturePrefix
|
||||
} else {
|
||||
prefix = "feature/"
|
||||
}
|
||||
|
||||
for _, b := range branches {
|
||||
// Skip current branch and main/master branches
|
||||
if b == currentBranch || b == "main" || b == "master" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if branch matches the prefix
|
||||
if strings.HasPrefix(b, prefix) {
|
||||
// Confirm deletion if configured
|
||||
if config.GlobalConfig.ConfirmDestructive {
|
||||
fmt.Printf("Delete branch '%s'? [y/N]: ", b)
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
if strings.ToLower(response) != "y" && strings.ToLower(response) != "yes" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err := git.DeleteBranch(ctx, b); err == nil {
|
||||
removed = append(removed, b)
|
||||
if config.GlobalConfig.Verbose {
|
||||
fmt.Printf("Deleted branch: %s\n", b)
|
||||
}
|
||||
} else if config.GlobalConfig.Verbose {
|
||||
fmt.Printf("Failed to delete branch %s: %v\n", b, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(removed) > 0 {
|
||||
fmt.Printf("Deleted %d branches:\n", len(removed))
|
||||
for _, branch := range removed {
|
||||
fmt.Printf(" - %s\n", branch)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("No branches to clean")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
283
internal/cmd/commit.go
Normal file
283
internal/cmd/commit.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/iwasforcedtobehere/git-automation-cli/internal/git"
|
||||
"github.com/iwasforcedtobehere/git-automation-cli/internal/validation"
|
||||
)
|
||||
|
||||
var commitCmd = &cobra.Command{
|
||||
Use: "commit",
|
||||
Short: "Commit utilities",
|
||||
Long: `Create and manage Git commits with templates and helpers.
|
||||
Supports conventional commit format and commit message templates.`,
|
||||
}
|
||||
|
||||
var commitCreateCmd = &cobra.Command{
|
||||
Use: "create [message]",
|
||||
Short: "Create a new commit",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
message := strings.Join(args, " ")
|
||||
|
||||
// Validate Git repository
|
||||
if validationResult := validation.ValidateGitRepository(ctx); !validationResult.IsValid {
|
||||
return fmt.Errorf(validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Validate commit message
|
||||
if validationResult := validation.ValidateCommitMessage(message); !validationResult.IsValid {
|
||||
return fmt.Errorf("invalid commit message: %s", validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Check if working directory is clean
|
||||
if validationResult := validation.ValidateWorkingDirectory(ctx); !validationResult.IsValid {
|
||||
return fmt.Errorf(validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Add all changes
|
||||
if err := git.AddAll(ctx); err != nil {
|
||||
return fmt.Errorf("failed to add changes: %w", err)
|
||||
}
|
||||
|
||||
// Create commit
|
||||
if err := git.Commit(ctx, message); err != nil {
|
||||
return fmt.Errorf("failed to create commit: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Created commit: %s\n", message)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var commitAmendCmd = &cobra.Command{
|
||||
Use: "amend",
|
||||
Short: "Amend the last commit",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Validate Git repository
|
||||
if validationResult := validation.ValidateGitRepository(ctx); !validationResult.IsValid {
|
||||
return fmt.Errorf(validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Check if working directory is clean
|
||||
if validationResult := validation.ValidateWorkingDirectory(ctx); !validationResult.IsValid {
|
||||
return fmt.Errorf(validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Add all changes
|
||||
if err := git.AddAll(ctx); err != nil {
|
||||
return fmt.Errorf("failed to add changes: %w", err)
|
||||
}
|
||||
|
||||
// Amend commit
|
||||
var err error
|
||||
_, err = git.Run(ctx, "commit", "--amend", "--no-edit")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to amend commit: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("Amended last commit")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var commitFixupCmd = &cobra.Command{
|
||||
Use: "fixup [commit]",
|
||||
Short: "Create a fixup commit",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
commit := args[0]
|
||||
|
||||
// Validate Git repository
|
||||
if validationResult := validation.ValidateGitRepository(ctx); !validationResult.IsValid {
|
||||
return fmt.Errorf(validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Check if working directory is clean
|
||||
if validationResult := validation.ValidateWorkingDirectory(ctx); !validationResult.IsValid {
|
||||
return fmt.Errorf(validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Add all changes
|
||||
if err := git.AddAll(ctx); err != nil {
|
||||
return fmt.Errorf("failed to add changes: %w", err)
|
||||
}
|
||||
|
||||
// Create fixup commit
|
||||
var err error
|
||||
_, err = git.Run(ctx, "commit", "--fixup", commit)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create fixup commit: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Created fixup commit for %s\n", commit)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var commitConventionalCmd = &cobra.Command{
|
||||
Use: "conventional",
|
||||
Short: "Create a conventional commit",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get commit type
|
||||
fmt.Println("Select commit type:")
|
||||
fmt.Println("1. feat: A new feature")
|
||||
fmt.Println("2. fix: A bug fix")
|
||||
fmt.Println("3. docs: Documentation only changes")
|
||||
fmt.Println("4. style: Changes that do not affect the meaning of the code")
|
||||
fmt.Println("5. refactor: A code change that neither fixes a bug nor adds a feature")
|
||||
fmt.Println("6. perf: A code change that improves performance")
|
||||
fmt.Println("7. test: Adding missing tests or correcting existing tests")
|
||||
fmt.Println("8. build: Changes that affect the build system or external dependencies")
|
||||
fmt.Println("9. ci: Changes to our CI configuration files and scripts")
|
||||
fmt.Println("10. chore: Other changes that don't modify src or test files")
|
||||
|
||||
var choice int
|
||||
fmt.Print("Enter choice (1-10): ")
|
||||
fmt.Scanln(&choice)
|
||||
|
||||
var commitType string
|
||||
switch choice {
|
||||
case 1:
|
||||
commitType = "feat"
|
||||
case 2:
|
||||
commitType = "fix"
|
||||
case 3:
|
||||
commitType = "docs"
|
||||
case 4:
|
||||
commitType = "style"
|
||||
case 5:
|
||||
commitType = "refactor"
|
||||
case 6:
|
||||
commitType = "perf"
|
||||
case 7:
|
||||
commitType = "test"
|
||||
case 8:
|
||||
commitType = "build"
|
||||
case 9:
|
||||
commitType = "ci"
|
||||
case 10:
|
||||
commitType = "chore"
|
||||
default:
|
||||
return fmt.Errorf("invalid choice")
|
||||
}
|
||||
|
||||
// Get scope
|
||||
fmt.Print("Enter scope (optional, press Enter to skip): ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
scope, _ := reader.ReadString('\n')
|
||||
scope = strings.TrimSpace(scope)
|
||||
|
||||
// Get description
|
||||
fmt.Print("Enter description: ")
|
||||
description, _ := reader.ReadString('\n')
|
||||
description = strings.TrimSpace(description)
|
||||
if description == "" {
|
||||
return fmt.Errorf("description is required")
|
||||
}
|
||||
|
||||
// Get body (optional)
|
||||
fmt.Print("Enter detailed description (optional, press Enter to skip): ")
|
||||
body, _ := reader.ReadString('\n')
|
||||
body = strings.TrimSpace(body)
|
||||
|
||||
// Build commit message
|
||||
var message string
|
||||
if scope != "" {
|
||||
message = fmt.Sprintf("%s(%s): %s", commitType, scope, description)
|
||||
} else {
|
||||
message = fmt.Sprintf("%s: %s", commitType, description)
|
||||
}
|
||||
|
||||
if body != "" {
|
||||
message += "\n\n" + body
|
||||
}
|
||||
|
||||
// Validate Git repository
|
||||
if validationResult := validation.ValidateGitRepository(ctx); !validationResult.IsValid {
|
||||
return fmt.Errorf(validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Validate conventional commit message
|
||||
if validationResult := validation.ValidateConventionalCommit(message); !validationResult.IsValid {
|
||||
return fmt.Errorf("invalid conventional commit message: %s", validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Check if working directory is clean
|
||||
if validationResult := validation.ValidateWorkingDirectory(ctx); !validationResult.IsValid {
|
||||
return fmt.Errorf(validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Add all changes
|
||||
if err := git.AddAll(ctx); err != nil {
|
||||
return fmt.Errorf("failed to add changes: %w", err)
|
||||
}
|
||||
|
||||
// Create commit
|
||||
if err := git.Commit(ctx, message); err != nil {
|
||||
return fmt.Errorf("failed to create commit: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Created conventional commit: %s\n", message)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var commitSignoffCmd = &cobra.Command{
|
||||
Use: "signoff [message]",
|
||||
Short: "Create a commit with sign-off",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
message := strings.Join(args, " ")
|
||||
|
||||
// Validate Git repository
|
||||
if validationResult := validation.ValidateGitRepository(ctx); !validationResult.IsValid {
|
||||
return fmt.Errorf(validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Validate commit message
|
||||
if validationResult := validation.ValidateCommitMessage(message); !validationResult.IsValid {
|
||||
return fmt.Errorf("invalid commit message: %s", validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Check if working directory is clean
|
||||
if validationResult := validation.ValidateWorkingDirectory(ctx); !validationResult.IsValid {
|
||||
return fmt.Errorf(validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Add all changes
|
||||
if err := git.AddAll(ctx); err != nil {
|
||||
return fmt.Errorf("failed to add changes: %w", err)
|
||||
}
|
||||
|
||||
// Create commit with sign-off
|
||||
var err error
|
||||
_, err = git.Run(ctx, "commit", "-s", "-m", message)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create commit: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Created signed-off commit: %s\n", message)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(commitCmd)
|
||||
commitCmd.AddCommand(commitCreateCmd)
|
||||
commitCmd.AddCommand(commitAmendCmd)
|
||||
commitCmd.AddCommand(commitFixupCmd)
|
||||
commitCmd.AddCommand(commitConventionalCmd)
|
||||
commitCmd.AddCommand(commitSignoffCmd)
|
||||
}
|
107
internal/cmd/config.go
Normal file
107
internal/cmd/config.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/iwasforcedtobehere/git-automation-cli/internal/config"
|
||||
)
|
||||
|
||||
var configCmd = &cobra.Command{
|
||||
Use: "config",
|
||||
Short: "Manage configuration settings",
|
||||
Long: `View and modify configuration settings for gitauto.
|
||||
Settings can be stored in the configuration file or as environment variables.`,
|
||||
}
|
||||
|
||||
var configGetCmd = &cobra.Command{
|
||||
Use: "get [key]",
|
||||
Short: "Get a configuration value",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
// Show all config values
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Configuration file: %s\n\n", config.GetConfigFile())
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Default Remote: %s\n", config.GlobalConfig.DefaultRemote)
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Default Branch: %s\n", config.GlobalConfig.DefaultBranch)
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "GitHub URL: %s\n", config.GlobalConfig.GitHubURL)
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Auto Cleanup: %t\n", config.GlobalConfig.AutoCleanup)
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Confirm Destructive: %t\n", config.GlobalConfig.ConfirmDestructive)
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Dry Run: %t\n", config.GlobalConfig.DryRun)
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Verbose: %t\n", config.GlobalConfig.Verbose)
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Branch Prefix: %s\n", config.GlobalConfig.BranchPrefix)
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Feature Prefix: %s\n", config.GlobalConfig.FeaturePrefix)
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Hotfix Prefix: %s\n", config.GlobalConfig.HotfixPrefix)
|
||||
if config.GlobalConfig.GitHubToken != "" {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "GitHub Token: %s\n", maskToken(config.GlobalConfig.GitHubToken))
|
||||
}
|
||||
} else {
|
||||
// Show specific config value
|
||||
key := args[0]
|
||||
value := config.Get(key)
|
||||
if value == nil {
|
||||
fmt.Fprintf(cmd.ErrOrStderr(), "Configuration key '%s' not found\n", key)
|
||||
cmd.SilenceUsage = true
|
||||
return fmt.Errorf("configuration key '%s' not found", key)
|
||||
}
|
||||
|
||||
// Mask sensitive values
|
||||
if strings.Contains(key, "token") || strings.Contains(key, "password") {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "%s: %s\n", key, maskToken(fmt.Sprintf("%v", value)))
|
||||
} else {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "%s: %v\n", key, value)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var configSetCmd = &cobra.Command{
|
||||
Use: "set [key] [value]",
|
||||
Short: "Set a configuration value",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
key := args[0]
|
||||
value := args[1]
|
||||
|
||||
config.Set(key, value)
|
||||
if err := config.Save(); err != nil {
|
||||
fmt.Fprintf(cmd.ErrOrStderr(), "Error saving configuration: %v\n", err)
|
||||
return fmt.Errorf("error saving configuration: %v", err)
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Set %s = %v\n", key, value)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var configInitCmd = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Initialize configuration file",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if err := config.Save(); err != nil {
|
||||
fmt.Fprintf(cmd.ErrOrStderr(), "Error creating configuration file: %v\n", err)
|
||||
return fmt.Errorf("error creating configuration file: %v", err)
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Configuration file created at: %s\n", config.GetConfigFile())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func maskToken(token string) string {
|
||||
if len(token) <= 8 {
|
||||
return strings.Repeat("*", len(token))
|
||||
}
|
||||
// For GitHub tokens, use a standard masking format
|
||||
if strings.HasPrefix(token, "ghp_") {
|
||||
return token[:4] + strings.Repeat("*", 28) + token[len(token)-4:]
|
||||
}
|
||||
// For other tokens, use the original logic
|
||||
return token[:4] + strings.Repeat("*", len(token)-8) + token[len(token)-4:]
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(configCmd)
|
||||
configCmd.AddCommand(configGetCmd)
|
||||
configCmd.AddCommand(configSetCmd)
|
||||
configCmd.AddCommand(configInitCmd)
|
||||
}
|
286
internal/cmd/config_test.go
Normal file
286
internal/cmd/config_test.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/iwasforcedtobehere/git-automation-cli/internal/config"
|
||||
)
|
||||
|
||||
func TestConfigGetCmd(t *testing.T) {
|
||||
// Save original config and viper
|
||||
originalConfig := config.GlobalConfig
|
||||
originalViper := viper.GetViper()
|
||||
defer func() {
|
||||
config.GlobalConfig = originalConfig
|
||||
config.SetViper(originalViper)
|
||||
}()
|
||||
|
||||
// Create a new viper instance for testing
|
||||
testViper := viper.New()
|
||||
config.SetViper(testViper)
|
||||
|
||||
// Create a temporary config file
|
||||
tempDir := t.TempDir()
|
||||
configFile := filepath.Join(tempDir, "config.yaml")
|
||||
|
||||
// Set config file path in viper
|
||||
testViper.SetConfigFile(configFile)
|
||||
testViper.SetConfigType("yaml")
|
||||
|
||||
// Create the config file
|
||||
if err := os.WriteFile(configFile, []byte("default_remote: origin\ndefault_branch: main\ngithub_url: https://api.github.com\ngithub_token: ghp_testtoken123456\nauto_cleanup: true\nconfirm_destructive: true\ndry_run: false\nverbose: false\nbranch_prefix: feature/\nfeature_prefix: feature/\nhotfix_prefix: hotfix/\n"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create config file: %v", err)
|
||||
}
|
||||
|
||||
// Read the config file
|
||||
if err := testViper.ReadInConfig(); err != nil {
|
||||
t.Fatalf("Failed to read config file: %v", err)
|
||||
}
|
||||
|
||||
// Set up test config
|
||||
config.GlobalConfig = &config.Config{
|
||||
DefaultRemote: "origin",
|
||||
DefaultBranch: "main",
|
||||
GitHubURL: "https://api.github.com",
|
||||
GitHubToken: "ghp_testtoken123456",
|
||||
AutoCleanup: true,
|
||||
ConfirmDestructive: true,
|
||||
DryRun: false,
|
||||
Verbose: false,
|
||||
BranchPrefix: "feature/",
|
||||
FeaturePrefix: "feature/",
|
||||
HotfixPrefix: "hotfix/",
|
||||
}
|
||||
|
||||
// Set values in viper
|
||||
testViper.Set("default_remote", "origin")
|
||||
testViper.Set("default_branch", "main")
|
||||
testViper.Set("github_url", "https://api.github.com")
|
||||
testViper.Set("github_token", "ghp_testtoken123456")
|
||||
testViper.Set("auto_cleanup", true)
|
||||
testViper.Set("confirm_destructive", true)
|
||||
testViper.Set("dry_run", false)
|
||||
testViper.Set("verbose", false)
|
||||
testViper.Set("branch_prefix", "feature/")
|
||||
testViper.Set("feature_prefix", "feature/")
|
||||
testViper.Set("hotfix_prefix", "hotfix/")
|
||||
|
||||
// Set the config file path
|
||||
config.SetConfigFile(configFile)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
wantOutput string
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
name: "Get all config",
|
||||
args: []string{},
|
||||
wantOutput: "Configuration file: " + configFile + "\n\nDefault Remote: origin\nDefault Branch: main\nGitHub URL: https://api.github.com\nAuto Cleanup: true\nConfirm Destructive: true\nDry Run: false\nVerbose: false\nBranch Prefix: feature/\nFeature Prefix: feature/\nHotfix Prefix: hotfix/\nGitHub Token: ghp_****************************3456\n",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Get specific config",
|
||||
args: []string{"default_remote"},
|
||||
wantOutput: "default_remote: origin\n",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Get sensitive config",
|
||||
args: []string{"github_token"},
|
||||
wantOutput: "github_token: ghp_****************************3456\n",
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Get non-existent config",
|
||||
args: []string{"nonexistent"},
|
||||
wantOutput: "", // Don't check exact output for error case
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Use the actual configGetCmd
|
||||
cmd := &cobra.Command{
|
||||
Use: "get",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: configGetCmd.RunE,
|
||||
SilenceUsage: true, // Prevent usage output on errors
|
||||
}
|
||||
|
||||
// Capture output
|
||||
output := &bytes.Buffer{}
|
||||
cmd.SetOut(output)
|
||||
cmd.SetErr(output)
|
||||
|
||||
// Set args
|
||||
cmd.SetArgs(tt.args)
|
||||
|
||||
// Execute command
|
||||
err := cmd.Execute()
|
||||
|
||||
// Check error expectation
|
||||
if tt.wantError && err == nil {
|
||||
t.Errorf("Execute() error = %v, wantError %v", err, tt.wantError)
|
||||
return
|
||||
}
|
||||
if !tt.wantError && err != nil {
|
||||
t.Errorf("Execute() error = %v, wantError %v", err, tt.wantError)
|
||||
return
|
||||
}
|
||||
|
||||
// Check output for error cases
|
||||
if tt.wantError {
|
||||
outputStr := output.String()
|
||||
if !strings.Contains(outputStr, "Configuration key 'nonexistent' not found") {
|
||||
t.Errorf("Expected error output to contain 'Configuration key 'nonexistent' not found', got %q", outputStr)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Check output for success cases
|
||||
if output.String() != tt.wantOutput {
|
||||
t.Errorf("Execute() output = %q, want %q", output.String(), tt.wantOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigSetCmd(t *testing.T) {
|
||||
// Save original config and viper
|
||||
originalConfig := config.GlobalConfig
|
||||
originalViper := viper.GetViper()
|
||||
defer func() {
|
||||
config.GlobalConfig = originalConfig
|
||||
config.SetViper(originalViper)
|
||||
}()
|
||||
|
||||
// Create a new viper instance for testing
|
||||
testViper := viper.New()
|
||||
config.SetViper(testViper)
|
||||
|
||||
// Create a temporary config file
|
||||
tempDir := t.TempDir()
|
||||
configFile := filepath.Join(tempDir, "config.yaml")
|
||||
|
||||
// Set config file path in viper
|
||||
testViper.SetConfigFile(configFile)
|
||||
testViper.SetConfigType("yaml")
|
||||
|
||||
// Create the config file
|
||||
if err := os.WriteFile(configFile, []byte("default_remote: origin\ndefault_branch: main\n"), 0644); err != nil {
|
||||
t.Fatalf("Failed to create config file: %v", err)
|
||||
}
|
||||
|
||||
// Read the config file
|
||||
if err := testViper.ReadInConfig(); err != nil {
|
||||
t.Fatalf("Failed to read config file: %v", err)
|
||||
}
|
||||
|
||||
// Set up test config
|
||||
config.GlobalConfig = &config.Config{
|
||||
DefaultRemote: "origin",
|
||||
DefaultBranch: "main",
|
||||
}
|
||||
|
||||
// Set values in viper
|
||||
testViper.Set("default_remote", "origin")
|
||||
testViper.Set("default_branch", "main")
|
||||
|
||||
// Set the config file path
|
||||
config.SetConfigFile(configFile)
|
||||
|
||||
// Create a command
|
||||
cmd := &cobra.Command{
|
||||
Use: "set",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: configSetCmd.RunE,
|
||||
}
|
||||
|
||||
// Capture output
|
||||
output := &bytes.Buffer{}
|
||||
cmd.SetOut(output)
|
||||
cmd.SetErr(output)
|
||||
|
||||
// Set args
|
||||
cmd.SetArgs([]string{"default_branch", "develop"})
|
||||
|
||||
// Execute command
|
||||
err := cmd.Execute()
|
||||
|
||||
// Check error
|
||||
if err != nil {
|
||||
t.Errorf("Execute() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check output
|
||||
expected := "Set default_branch = develop\n"
|
||||
if output.String() != expected {
|
||||
t.Errorf("Execute() output = %q, want %q", output.String(), expected)
|
||||
}
|
||||
|
||||
// Check that config was updated
|
||||
if config.GlobalConfig.DefaultBranch != "develop" {
|
||||
t.Errorf("Expected DefaultBranch to be 'develop', got '%s'", config.GlobalConfig.DefaultBranch)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigInitCmd(t *testing.T) {
|
||||
// Save original config and viper
|
||||
originalConfig := config.GlobalConfig
|
||||
originalViper := viper.GetViper()
|
||||
defer func() {
|
||||
config.GlobalConfig = originalConfig
|
||||
config.SetViper(originalViper)
|
||||
}()
|
||||
|
||||
// Create a new viper instance for testing
|
||||
testViper := viper.New()
|
||||
config.SetViper(testViper)
|
||||
|
||||
// Create a temporary config file
|
||||
tempDir := t.TempDir()
|
||||
configFile := filepath.Join(tempDir, "config.yaml")
|
||||
|
||||
// Set config file path in viper
|
||||
testViper.SetConfigFile(configFile)
|
||||
testViper.SetConfigType("yaml")
|
||||
|
||||
// Create a command
|
||||
// Set the config file path to our temp file before creating the command
|
||||
config.SetConfigFile(configFile)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "init",
|
||||
RunE: configInitCmd.RunE,
|
||||
}
|
||||
|
||||
// Capture output
|
||||
output := &bytes.Buffer{}
|
||||
cmd.SetOut(output)
|
||||
cmd.SetErr(output)
|
||||
|
||||
// Execute command
|
||||
err := cmd.Execute()
|
||||
|
||||
// Check error
|
||||
if err != nil {
|
||||
t.Errorf("Execute() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check output contains expected text
|
||||
outputStr := output.String()
|
||||
if !strings.Contains(outputStr, "Configuration file created at:") {
|
||||
t.Errorf("Expected output to contain 'Configuration file created at:', got %q", outputStr)
|
||||
}
|
||||
}
|
326
internal/cmd/pr.go
Normal file
326
internal/cmd/pr.go
Normal file
@@ -0,0 +1,326 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/iwasforcedtobehere/git-automation-cli/internal/config"
|
||||
"github.com/iwasforcedtobehere/git-automation-cli/internal/git"
|
||||
"github.com/iwasforcedtobehere/git-automation-cli/internal/github"
|
||||
"github.com/iwasforcedtobehere/git-automation-cli/internal/validation"
|
||||
)
|
||||
|
||||
var prCmd = &cobra.Command{
|
||||
Use: "pr",
|
||||
Short: "Pull request utilities",
|
||||
Long: `Create, list, and manage GitHub pull requests.
|
||||
Supports creating pull requests from the current branch and managing existing PRs.`,
|
||||
}
|
||||
|
||||
var prCreateCmd = &cobra.Command{
|
||||
Use: "create [title]",
|
||||
Short: "Create a new pull request",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
title := strings.Join(args, " ")
|
||||
|
||||
// Get body from flag or prompt
|
||||
body, _ := cmd.Flags().GetString("body")
|
||||
if body == "" {
|
||||
body = promptForBody()
|
||||
}
|
||||
|
||||
// Get draft flag
|
||||
draft, _ := cmd.Flags().GetBool("draft")
|
||||
|
||||
// Get base branch
|
||||
base, _ := cmd.Flags().GetString("base")
|
||||
if base == "" {
|
||||
base = config.GlobalConfig.DefaultBranch
|
||||
if base == "" {
|
||||
base = "main"
|
||||
}
|
||||
}
|
||||
|
||||
// Validate Git repository
|
||||
if validationResult := validation.ValidateGitRepository(ctx); !validationResult.IsValid {
|
||||
return fmt.Errorf(validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Get current branch
|
||||
currentBranch, err := git.CurrentBranch(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current branch: %w", err)
|
||||
}
|
||||
|
||||
// Check if current branch is the same as base branch
|
||||
if currentBranch == base {
|
||||
return fmt.Errorf("cannot create pull request from %s branch to itself", base)
|
||||
}
|
||||
|
||||
// Create GitHub client
|
||||
client, err := github.NewClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create GitHub client: %w", err)
|
||||
}
|
||||
|
||||
// Get repository information
|
||||
remote, _ := cmd.Flags().GetString("remote")
|
||||
if remote == "" {
|
||||
remote = config.GlobalConfig.DefaultRemote
|
||||
if remote == "" {
|
||||
remote = "origin"
|
||||
}
|
||||
}
|
||||
|
||||
// Get remote URL
|
||||
remoteURL, err := git.GetRemoteURL(ctx, remote)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get remote URL: %w", err)
|
||||
}
|
||||
|
||||
// Parse GitHub URL
|
||||
if !github.IsGitHubURL(remoteURL) {
|
||||
return fmt.Errorf("remote URL is not a GitHub URL: %s", remoteURL)
|
||||
}
|
||||
|
||||
owner, repo, err := github.ParseGitHubURL(remoteURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse GitHub URL: %w", err)
|
||||
}
|
||||
|
||||
// Set repository on client
|
||||
client.SetRepository(owner, repo)
|
||||
|
||||
// Create pull request
|
||||
pr := &github.PullRequestRequest{
|
||||
Title: title,
|
||||
Body: body,
|
||||
Head: currentBranch,
|
||||
Base: base,
|
||||
Draft: draft,
|
||||
}
|
||||
|
||||
pullRequest, err := client.CreatePullRequest(ctx, pr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create pull request: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Created pull request: %s\n", pullRequest.HTMLURL)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var prListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List pull requests",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get state flag
|
||||
state, _ := cmd.Flags().GetString("state")
|
||||
if state == "" {
|
||||
state = "open"
|
||||
}
|
||||
|
||||
// Validate Git repository
|
||||
if validationResult := validation.ValidateGitRepository(ctx); !validationResult.IsValid {
|
||||
return fmt.Errorf(validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Create GitHub client
|
||||
client, err := github.NewClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create GitHub client: %w", err)
|
||||
}
|
||||
|
||||
// Get repository information
|
||||
remote, _ := cmd.Flags().GetString("remote")
|
||||
if remote == "" {
|
||||
remote = config.GlobalConfig.DefaultRemote
|
||||
if remote == "" {
|
||||
remote = "origin"
|
||||
}
|
||||
}
|
||||
|
||||
// Get remote URL
|
||||
remoteURL, err := git.GetRemoteURL(ctx, remote)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get remote URL: %w", err)
|
||||
}
|
||||
|
||||
// Parse GitHub URL
|
||||
if !github.IsGitHubURL(remoteURL) {
|
||||
return fmt.Errorf("remote URL is not a GitHub URL: %s", remoteURL)
|
||||
}
|
||||
|
||||
owner, repo, err := github.ParseGitHubURL(remoteURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse GitHub URL: %w", err)
|
||||
}
|
||||
|
||||
// Set repository on client
|
||||
client.SetRepository(owner, repo)
|
||||
|
||||
// Get pull requests
|
||||
pullRequests, err := client.GetPullRequests(ctx, state)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get pull requests: %w", err)
|
||||
}
|
||||
|
||||
// Print pull requests
|
||||
if len(pullRequests) == 0 {
|
||||
fmt.Printf("No %s pull requests found\n", state)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%s pull requests:\n", strings.Title(state))
|
||||
for _, pr := range pullRequests {
|
||||
fmt.Printf("#%d: %s (%s)\n", pr.Number, pr.Title, pr.User.Login)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var prMergeCmd = &cobra.Command{
|
||||
Use: "merge [number]",
|
||||
Short: "Merge a pull request",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
number := args[0]
|
||||
|
||||
// Validate Git repository
|
||||
if validationResult := validation.ValidateGitRepository(ctx); !validationResult.IsValid {
|
||||
return fmt.Errorf(validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Create GitHub client
|
||||
client, err := github.NewClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create GitHub client: %w", err)
|
||||
}
|
||||
|
||||
// Get repository information
|
||||
remote, _ := cmd.Flags().GetString("remote")
|
||||
if remote == "" {
|
||||
remote = config.GlobalConfig.DefaultRemote
|
||||
if remote == "" {
|
||||
remote = "origin"
|
||||
}
|
||||
}
|
||||
|
||||
// Get remote URL
|
||||
remoteURL, err := git.GetRemoteURL(ctx, remote)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get remote URL: %w", err)
|
||||
}
|
||||
|
||||
// Parse GitHub URL
|
||||
if !github.IsGitHubURL(remoteURL) {
|
||||
return fmt.Errorf("remote URL is not a GitHub URL: %s", remoteURL)
|
||||
}
|
||||
|
||||
owner, repo, err := github.ParseGitHubURL(remoteURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse GitHub URL: %w", err)
|
||||
}
|
||||
|
||||
// Set repository on client
|
||||
client.SetRepository(owner, repo)
|
||||
|
||||
// Get merge method
|
||||
method, _ := cmd.Flags().GetString("method")
|
||||
if method == "" {
|
||||
method = "merge"
|
||||
}
|
||||
|
||||
// Merge pull request
|
||||
mergeRequest := &github.MergePullRequestRequest{
|
||||
MergeMethod: method,
|
||||
}
|
||||
|
||||
mergeResponse, err := client.MergePullRequest(ctx, parseInt(number), mergeRequest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to merge pull request: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Merged pull request: %s\n", mergeResponse.SHA)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func promptForBody() string {
|
||||
fmt.Print("Enter pull request body (press Enter twice to finish):\n")
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
var body string
|
||||
var consecutiveEmptyLines int
|
||||
|
||||
for {
|
||||
fmt.Print("> ")
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
if line == "" {
|
||||
consecutiveEmptyLines++
|
||||
if consecutiveEmptyLines >= 2 {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
consecutiveEmptyLines = 0
|
||||
if body != "" {
|
||||
body += "\n"
|
||||
}
|
||||
body += line
|
||||
}
|
||||
}
|
||||
|
||||
return body
|
||||
}
|
||||
|
||||
func readLine() string {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(line)
|
||||
}
|
||||
|
||||
func parseInt(s string) int {
|
||||
var result int
|
||||
_, err := fmt.Sscanf(s, "%d", &result)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(prCmd)
|
||||
prCmd.AddCommand(prCreateCmd)
|
||||
prCmd.AddCommand(prListCmd)
|
||||
prCmd.AddCommand(prMergeCmd)
|
||||
|
||||
// Add flags to pr create command
|
||||
prCreateCmd.Flags().String("body", "", "Pull request body")
|
||||
prCreateCmd.Flags().Bool("draft", false, "Create a draft pull request")
|
||||
prCreateCmd.Flags().String("base", "", "Base branch for the pull request")
|
||||
prCreateCmd.Flags().String("remote", "", "Remote to use for the pull request")
|
||||
|
||||
// Add flags to pr list command
|
||||
prListCmd.Flags().String("state", "open", "State of pull requests to list (open, closed, all)")
|
||||
prListCmd.Flags().String("remote", "", "Remote to use for the pull request")
|
||||
|
||||
// Add flags to pr merge command
|
||||
prMergeCmd.Flags().String("method", "merge", "Merge method (merge, squash, rebase)")
|
||||
prMergeCmd.Flags().String("remote", "", "Remote to use for the pull request")
|
||||
}
|
518
internal/cmd/pr_test.go
Normal file
518
internal/cmd/pr_test.go
Normal file
@@ -0,0 +1,518 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/iwasforcedtobehere/git-automation-cli/internal/github"
|
||||
"github.com/iwasforcedtobehere/git-automation-cli/internal/validation"
|
||||
)
|
||||
|
||||
func TestPromptForBody(t *testing.T) {
|
||||
// Save original stdin
|
||||
originalStdin := os.Stdin
|
||||
defer func() {
|
||||
os.Stdin = originalStdin
|
||||
}()
|
||||
|
||||
// Create a pipe to simulate user input
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdin = r
|
||||
|
||||
// Write test input
|
||||
go func() {
|
||||
w.WriteString("This is the first line\n")
|
||||
w.WriteString("This is the second line\n")
|
||||
w.WriteString("\n") // First empty line
|
||||
w.WriteString("\n") // Second empty line to finish
|
||||
w.Close()
|
||||
}()
|
||||
|
||||
// Capture output
|
||||
oldStdout := os.Stdout
|
||||
stdoutR, stdoutW, _ := os.Pipe()
|
||||
os.Stdout = stdoutW
|
||||
|
||||
// Call the function
|
||||
body := promptForBody()
|
||||
|
||||
// Restore stdout
|
||||
stdoutW.Close()
|
||||
os.Stdout = oldStdout
|
||||
out, _ := io.ReadAll(stdoutR)
|
||||
|
||||
// Check result
|
||||
expectedBody := "This is the first line\nThis is the second line"
|
||||
if body != expectedBody {
|
||||
t.Errorf("promptForBody() = %q, want %q", body, expectedBody)
|
||||
}
|
||||
|
||||
// Check that prompt was displayed
|
||||
output := string(out)
|
||||
if !strings.Contains(output, "Enter pull request body") {
|
||||
t.Errorf("Expected prompt to be displayed, got %q", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadLine(t *testing.T) {
|
||||
// Save original stdin
|
||||
originalStdin := os.Stdin
|
||||
defer func() {
|
||||
os.Stdin = originalStdin
|
||||
}()
|
||||
|
||||
// Create a pipe to simulate user input
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdin = r
|
||||
|
||||
// Write test input
|
||||
go func() {
|
||||
w.WriteString("test line\n")
|
||||
w.Close()
|
||||
}()
|
||||
|
||||
// Call the function
|
||||
line := readLine()
|
||||
|
||||
// Check result
|
||||
expectedLine := "test line"
|
||||
if line != expectedLine {
|
||||
t.Errorf("readLine() = %q, want %q", line, expectedLine)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInt(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected int
|
||||
}{
|
||||
{
|
||||
name: "Valid number",
|
||||
input: "123",
|
||||
expected: 123,
|
||||
},
|
||||
{
|
||||
name: "Invalid number",
|
||||
input: "abc",
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "Empty string",
|
||||
input: "",
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "Negative number",
|
||||
input: "-456",
|
||||
expected: -456,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := parseInt(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("parseInt(%q) = %d, want %d", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MockGitService is a mock implementation of the git service
|
||||
type MockGitService struct {
|
||||
currentBranchFunc func(ctx context.Context) (string, error)
|
||||
getRemoteURLFunc func(ctx context.Context, remote string) (string, error)
|
||||
}
|
||||
|
||||
func (m *MockGitService) CurrentBranch(ctx context.Context) (string, error) {
|
||||
if m.currentBranchFunc != nil {
|
||||
return m.currentBranchFunc(ctx)
|
||||
}
|
||||
return "feature/test", nil
|
||||
}
|
||||
|
||||
func (m *MockGitService) GetRemoteURL(ctx context.Context, remote string) (string, error) {
|
||||
if m.getRemoteURLFunc != nil {
|
||||
return m.getRemoteURLFunc(ctx, remote)
|
||||
}
|
||||
return "https://github.com/owner/repo.git", nil
|
||||
}
|
||||
|
||||
// MockGitHubService is a mock implementation of the GitHub service
|
||||
type MockGitHubService struct {
|
||||
newClientFunc func(ctx context.Context) (*github.Client, error)
|
||||
isGitHubURLFunc func(url string) bool
|
||||
parseGitHubURLFunc func(url string) (string, string, error)
|
||||
createPullRequest func(c *github.Client, ctx context.Context, pr *github.PullRequestRequest) (*github.PullRequest, error)
|
||||
getPullRequests func(c *github.Client, ctx context.Context, state string) ([]github.PullRequest, error)
|
||||
mergePullRequest func(c *github.Client, ctx context.Context, number int, mergeRequest *github.MergePullRequestRequest) (*github.MergePullRequestResponse, error)
|
||||
}
|
||||
|
||||
func (m *MockGitHubService) NewClient(ctx context.Context) (*github.Client, error) {
|
||||
if m.newClientFunc != nil {
|
||||
return m.newClientFunc(ctx)
|
||||
}
|
||||
return &github.Client{}, nil
|
||||
}
|
||||
|
||||
func (m *MockGitHubService) IsGitHubURL(url string) bool {
|
||||
if m.isGitHubURLFunc != nil {
|
||||
return m.isGitHubURLFunc(url)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *MockGitHubService) ParseGitHubURL(url string) (string, string, error) {
|
||||
if m.parseGitHubURLFunc != nil {
|
||||
return m.parseGitHubURLFunc(url)
|
||||
}
|
||||
return "owner", "repo", nil
|
||||
}
|
||||
|
||||
// MockValidationService is a mock implementation of the validation service
|
||||
type MockValidationService struct {
|
||||
validateGitRepositoryFunc func(ctx context.Context) *validation.ValidationResult
|
||||
}
|
||||
|
||||
func (m *MockValidationService) ValidateGitRepository(ctx context.Context) *validation.ValidationResult {
|
||||
if m.validateGitRepositoryFunc != nil {
|
||||
return m.validateGitRepositoryFunc(ctx)
|
||||
}
|
||||
return &validation.ValidationResult{IsValid: true}
|
||||
}
|
||||
|
||||
func TestPRCreateCmd(t *testing.T) {
|
||||
// Create mock services
|
||||
mockGitService := &MockGitService{}
|
||||
mockGitHubService := &MockGitHubService{}
|
||||
mockValidationService := &MockValidationService{}
|
||||
|
||||
// Set up mock functions
|
||||
mockGitHubService.createPullRequest = func(c *github.Client, ctx context.Context, pr *github.PullRequestRequest) (*github.PullRequest, error) {
|
||||
return &github.PullRequest{
|
||||
ID: 1,
|
||||
Number: 123,
|
||||
Title: pr.Title,
|
||||
Body: pr.Body,
|
||||
HTMLURL: "https://github.com/owner/repo/pull/123",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Temporarily redirect stdout to capture output
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Create a command
|
||||
cmd := &cobra.Command{
|
||||
Use: "test",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Use mock services instead of real ones
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Validate Git repository
|
||||
if validationResult := mockValidationService.ValidateGitRepository(ctx); !validationResult.IsValid {
|
||||
return fmt.Errorf(validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Get current branch
|
||||
currentBranch, err := mockGitService.CurrentBranch(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current branch: %w", err)
|
||||
}
|
||||
|
||||
// Get base branch
|
||||
base := "main"
|
||||
|
||||
// Check if current branch is the same as base branch
|
||||
if currentBranch == base {
|
||||
return fmt.Errorf("cannot create pull request from %s branch to itself", base)
|
||||
}
|
||||
|
||||
// Create GitHub client
|
||||
client, err := mockGitHubService.NewClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create GitHub client: %w", err)
|
||||
}
|
||||
|
||||
// Get remote URL
|
||||
remoteURL, err := mockGitService.GetRemoteURL(ctx, "origin")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get remote URL: %w", err)
|
||||
}
|
||||
|
||||
// Parse GitHub URL
|
||||
if !mockGitHubService.IsGitHubURL(remoteURL) {
|
||||
return fmt.Errorf("remote URL is not a GitHub URL: %s", remoteURL)
|
||||
}
|
||||
|
||||
owner, repo, err := mockGitHubService.ParseGitHubURL(remoteURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse GitHub URL: %w", err)
|
||||
}
|
||||
|
||||
// Set repository on client
|
||||
client.SetRepository(owner, repo)
|
||||
|
||||
// Create pull request
|
||||
pr := &github.PullRequestRequest{
|
||||
Title: strings.Join(args, " "),
|
||||
Body: "Test body",
|
||||
Head: currentBranch,
|
||||
Base: base,
|
||||
Draft: false,
|
||||
}
|
||||
|
||||
pullRequest, err := mockGitHubService.createPullRequest(client, ctx, pr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create pull request: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Created pull request: %s\n", pullRequest.HTMLURL)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Set args
|
||||
cmd.SetArgs([]string{"Test PR"})
|
||||
|
||||
// Execute command
|
||||
err := cmd.Execute()
|
||||
|
||||
// Restore stdout
|
||||
w.Close()
|
||||
os.Stdout = oldStdout
|
||||
out, _ := io.ReadAll(r)
|
||||
|
||||
// Check error
|
||||
if err != nil {
|
||||
t.Errorf("Execute() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check output
|
||||
output := string(out)
|
||||
expected := "Created pull request: https://github.com/owner/repo/pull/123\n"
|
||||
if output != expected {
|
||||
t.Errorf("Execute() output = %q, want %q", output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRListCmd(t *testing.T) {
|
||||
// Create mock services
|
||||
mockGitService := &MockGitService{}
|
||||
mockGitHubService := &MockGitHubService{}
|
||||
mockValidationService := &MockValidationService{}
|
||||
|
||||
// Set up mock functions
|
||||
mockGitHubService.getPullRequests = func(c *github.Client, ctx context.Context, state string) ([]github.PullRequest, error) {
|
||||
return []github.PullRequest{
|
||||
{
|
||||
ID: 1,
|
||||
Number: 123,
|
||||
Title: "Test PR 1",
|
||||
User: github.User{Login: "user1"},
|
||||
},
|
||||
{
|
||||
ID: 2,
|
||||
Number: 124,
|
||||
Title: "Test PR 2",
|
||||
User: github.User{Login: "user2"},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Temporarily redirect stdout to capture output
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Create a command
|
||||
cmd := &cobra.Command{
|
||||
Use: "test",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Use mock services instead of real ones
|
||||
ctx := cmd.Context()
|
||||
|
||||
// Get state flag
|
||||
state := "open"
|
||||
|
||||
// Validate Git repository
|
||||
if validationResult := mockValidationService.ValidateGitRepository(ctx); !validationResult.IsValid {
|
||||
return fmt.Errorf(validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Create GitHub client
|
||||
client, err := mockGitHubService.NewClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create GitHub client: %w", err)
|
||||
}
|
||||
|
||||
// Get remote URL
|
||||
remoteURL, err := mockGitService.GetRemoteURL(ctx, "origin")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get remote URL: %w", err)
|
||||
}
|
||||
|
||||
// Parse GitHub URL
|
||||
if !mockGitHubService.IsGitHubURL(remoteURL) {
|
||||
return fmt.Errorf("remote URL is not a GitHub URL: %s", remoteURL)
|
||||
}
|
||||
|
||||
owner, repo, err := mockGitHubService.ParseGitHubURL(remoteURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse GitHub URL: %w", err)
|
||||
}
|
||||
|
||||
// Set repository on client
|
||||
client.SetRepository(owner, repo)
|
||||
|
||||
// Get pull requests
|
||||
pullRequests, err := mockGitHubService.getPullRequests(client, ctx, state)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get pull requests: %w", err)
|
||||
}
|
||||
|
||||
// Print pull requests
|
||||
if len(pullRequests) == 0 {
|
||||
fmt.Printf("No %s pull requests found\n", state)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("%s pull requests:\n", strings.Title(state))
|
||||
for _, pr := range pullRequests {
|
||||
fmt.Printf("#%d: %s (%s)\n", pr.Number, pr.Title, pr.User.Login)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Execute command
|
||||
err := cmd.Execute()
|
||||
|
||||
// Restore stdout
|
||||
w.Close()
|
||||
os.Stdout = oldStdout
|
||||
out, _ := io.ReadAll(r)
|
||||
|
||||
// Check error
|
||||
if err != nil {
|
||||
t.Errorf("Execute() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check output
|
||||
output := string(out)
|
||||
expected := "Open pull requests:\n#123: Test PR 1 (user1)\n#124: Test PR 2 (user2)\n"
|
||||
if output != expected {
|
||||
t.Errorf("Execute() output = %q, want %q", output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPRMergeCmd(t *testing.T) {
|
||||
// Create mock services
|
||||
mockGitService := &MockGitService{}
|
||||
mockGitHubService := &MockGitHubService{}
|
||||
mockValidationService := &MockValidationService{}
|
||||
|
||||
// Set up mock functions
|
||||
mockGitHubService.mergePullRequest = func(c *github.Client, ctx context.Context, number int, mergeRequest *github.MergePullRequestRequest) (*github.MergePullRequestResponse, error) {
|
||||
return &github.MergePullRequestResponse{
|
||||
SHA: "abc123",
|
||||
Merged: true,
|
||||
Message: "Pull Request successfully merged",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Temporarily redirect stdout to capture output
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Create a command
|
||||
cmd := &cobra.Command{
|
||||
Use: "test",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Use mock services instead of real ones
|
||||
ctx := cmd.Context()
|
||||
number := args[0]
|
||||
|
||||
// Validate Git repository
|
||||
if validationResult := mockValidationService.ValidateGitRepository(ctx); !validationResult.IsValid {
|
||||
return fmt.Errorf(validationResult.GetErrors())
|
||||
}
|
||||
|
||||
// Create GitHub client
|
||||
client, err := mockGitHubService.NewClient(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create GitHub client: %w", err)
|
||||
}
|
||||
|
||||
// Get remote URL
|
||||
remoteURL, err := mockGitService.GetRemoteURL(ctx, "origin")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get remote URL: %w", err)
|
||||
}
|
||||
|
||||
// Parse GitHub URL
|
||||
if !mockGitHubService.IsGitHubURL(remoteURL) {
|
||||
return fmt.Errorf("remote URL is not a GitHub URL: %s", remoteURL)
|
||||
}
|
||||
|
||||
owner, repo, err := mockGitHubService.ParseGitHubURL(remoteURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse GitHub URL: %w", err)
|
||||
}
|
||||
|
||||
// Set repository on client
|
||||
client.SetRepository(owner, repo)
|
||||
|
||||
// Get merge method
|
||||
method := "merge"
|
||||
|
||||
// Merge pull request
|
||||
mergeRequest := &github.MergePullRequestRequest{
|
||||
MergeMethod: method,
|
||||
}
|
||||
|
||||
mergeResponse, err := mockGitHubService.mergePullRequest(client, ctx, parseInt(number), mergeRequest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to merge pull request: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Merged pull request: %s\n", mergeResponse.SHA)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Set args
|
||||
cmd.SetArgs([]string{"123"})
|
||||
|
||||
// Execute command
|
||||
err := cmd.Execute()
|
||||
|
||||
// Restore stdout
|
||||
w.Close()
|
||||
os.Stdout = oldStdout
|
||||
out, _ := io.ReadAll(r)
|
||||
|
||||
// Check error
|
||||
if err != nil {
|
||||
t.Errorf("Execute() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check output
|
||||
output := string(out)
|
||||
expected := "Merged pull request: abc123\n"
|
||||
if output != expected {
|
||||
t.Errorf("Execute() output = %q, want %q", output, expected)
|
||||
}
|
||||
}
|
60
internal/cmd/root.go
Normal file
60
internal/cmd/root.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/iwasforcedtobehere/git-automation-cli/internal/version"
|
||||
"github.com/iwasforcedtobehere/git-automation-cli/internal/config"
|
||||
)
|
||||
|
||||
var (
|
||||
verbose bool
|
||||
dryRun bool
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "gitauto",
|
||||
Short: "Git automation toolkit",
|
||||
Long: `Git Automation CLI is a fast, practical tool to automate frequent Git workflows.
|
||||
It provides utilities for branch management, commit helpers, sync operations,
|
||||
and integration with GitHub for pull requests and team coordination.`,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
// Initialize configuration
|
||||
if err := config.Init(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error initializing configuration: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Override config with command line flags
|
||||
if verbose {
|
||||
config.Set("verbose", true)
|
||||
}
|
||||
if dryRun {
|
||||
config.Set("dry_run", true)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print the version information",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("gitauto version", version.GetVersion())
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
|
||||
// Add global flags
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
|
||||
rootCmd.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "dry run mode (no changes made)")
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
225
internal/cmd/root_test.go
Normal file
225
internal/cmd/root_test.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/iwasforcedtobehere/git-automation-cli/internal/config"
|
||||
)
|
||||
|
||||
// MockVersionService is a mock implementation of the version service
|
||||
type MockVersionService struct {
|
||||
getVersionFunc func() string
|
||||
}
|
||||
|
||||
func (m *MockVersionService) GetVersion() string {
|
||||
if m.getVersionFunc != nil {
|
||||
return m.getVersionFunc()
|
||||
}
|
||||
return "1.0.0"
|
||||
}
|
||||
|
||||
// MockConfigService is a mock implementation of the config service
|
||||
type MockConfigService struct {
|
||||
initFunc func() error
|
||||
}
|
||||
|
||||
func (m *MockConfigService) Init() error {
|
||||
if m.initFunc != nil {
|
||||
return m.initFunc()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestExecute(t *testing.T) {
|
||||
// Save original stdout
|
||||
originalStdout := os.Stdout
|
||||
defer func() {
|
||||
os.Stdout = originalStdout
|
||||
}()
|
||||
|
||||
// Temporarily redirect stdout to capture output
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Execute root command
|
||||
Execute()
|
||||
|
||||
// Restore stdout
|
||||
w.Close()
|
||||
os.Stdout = originalStdout
|
||||
out, _ := io.ReadAll(r)
|
||||
|
||||
// Check that help was displayed
|
||||
output := string(out)
|
||||
if !strings.Contains(output, "Git Automation CLI is a fast, practical tool to automate frequent Git workflows") {
|
||||
t.Errorf("Expected help to be displayed, got %q", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionCmd(t *testing.T) {
|
||||
// Create mock version service
|
||||
mockVersionService := &MockVersionService{
|
||||
getVersionFunc: func() string {
|
||||
return "1.0.0-test"
|
||||
},
|
||||
}
|
||||
|
||||
// Temporarily redirect stdout to capture output
|
||||
oldStdout := os.Stdout
|
||||
r, w, _ := os.Pipe()
|
||||
os.Stdout = w
|
||||
|
||||
// Create a command
|
||||
cmd := &cobra.Command{
|
||||
Use: "test",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Use mock version service instead of real one
|
||||
versionStr := mockVersionService.GetVersion()
|
||||
fmt.Printf("gitauto version %s\n", versionStr)
|
||||
},
|
||||
}
|
||||
|
||||
// Execute command
|
||||
err := cmd.Execute()
|
||||
|
||||
// Restore stdout
|
||||
w.Close()
|
||||
os.Stdout = oldStdout
|
||||
out, _ := io.ReadAll(r)
|
||||
|
||||
// Check error
|
||||
if err != nil {
|
||||
t.Errorf("Execute() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check output
|
||||
output := string(out)
|
||||
expected := "gitauto version 1.0.0-test\n"
|
||||
if output != expected {
|
||||
t.Errorf("Execute() output = %q, want %q", output, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootCmdPersistentPreRun(t *testing.T) {
|
||||
// Save original config
|
||||
originalConfig := config.GlobalConfig
|
||||
defer func() {
|
||||
config.GlobalConfig = originalConfig
|
||||
}()
|
||||
|
||||
// Create mock config service
|
||||
mockConfigService := &MockConfigService{
|
||||
initFunc: func() error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Set up test config
|
||||
config.GlobalConfig = &config.Config{
|
||||
Verbose: false,
|
||||
DryRun: false,
|
||||
}
|
||||
|
||||
// Create a command with PersistentPreRun
|
||||
cmd := &cobra.Command{
|
||||
Use: "test",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
// Initialize configuration
|
||||
if err := mockConfigService.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Override config with command line flags
|
||||
if verbose {
|
||||
config.Set("verbose", true)
|
||||
}
|
||||
if dryRun {
|
||||
config.Set("dry_run", true)
|
||||
}
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Do nothing
|
||||
},
|
||||
}
|
||||
|
||||
// Set verbose flag
|
||||
verbose = true
|
||||
|
||||
// Execute command
|
||||
err := cmd.Execute()
|
||||
|
||||
// Check error
|
||||
if err != nil {
|
||||
t.Errorf("Execute() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check that config was updated
|
||||
if !config.GlobalConfig.Verbose {
|
||||
t.Errorf("Expected Verbose to be true, got false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootCmdFlags(t *testing.T) {
|
||||
// Test that verbose and dry-run flags are properly set
|
||||
cmd := &cobra.Command{
|
||||
Use: "test",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
// Do nothing
|
||||
},
|
||||
}
|
||||
|
||||
// Add flags like in rootCmd
|
||||
cmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output")
|
||||
cmd.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "dry run mode (no changes made)")
|
||||
|
||||
// Set verbose flag
|
||||
err := cmd.ParseFlags([]string{"--verbose"})
|
||||
if err != nil {
|
||||
t.Errorf("ParseFlags() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !verbose {
|
||||
t.Errorf("Expected verbose to be true, got false")
|
||||
}
|
||||
|
||||
// Reset flags
|
||||
verbose = false
|
||||
dryRun = false
|
||||
|
||||
// Set dry-run flag
|
||||
err = cmd.ParseFlags([]string{"--dry-run"})
|
||||
if err != nil {
|
||||
t.Errorf("ParseFlags() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !dryRun {
|
||||
t.Errorf("Expected dryRun to be true, got false")
|
||||
}
|
||||
|
||||
// Reset flags
|
||||
verbose = false
|
||||
dryRun = false
|
||||
|
||||
// Set both flags
|
||||
err = cmd.ParseFlags([]string{"--verbose", "--dry-run"})
|
||||
if err != nil {
|
||||
t.Errorf("ParseFlags() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !verbose {
|
||||
t.Errorf("Expected verbose to be true, got false")
|
||||
}
|
||||
if !dryRun {
|
||||
t.Errorf("Expected dryRun to be true, got false")
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user