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:
149
internal/config/config.go
Normal file
149
internal/config/config.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Config represents the application configuration
|
||||
type Config struct {
|
||||
// Git settings
|
||||
DefaultRemote string `mapstructure:"default_remote"`
|
||||
DefaultBranch string `mapstructure:"default_branch"`
|
||||
|
||||
// GitHub settings
|
||||
GitHubToken string `mapstructure:"github_token"`
|
||||
GitHubURL string `mapstructure:"github_url"`
|
||||
|
||||
// Behavior settings
|
||||
AutoCleanup bool `mapstructure:"auto_cleanup"`
|
||||
ConfirmDestructive bool `mapstructure:"confirm_destructive"`
|
||||
DryRun bool `mapstructure:"dry_run"`
|
||||
Verbose bool `mapstructure:"verbose"`
|
||||
|
||||
// Branch settings
|
||||
BranchPrefix string `mapstructure:"branch_prefix"`
|
||||
FeaturePrefix string `mapstructure:"feature_prefix"`
|
||||
HotfixPrefix string `mapstructure:"hotfix_prefix"`
|
||||
}
|
||||
|
||||
var (
|
||||
// GlobalConfig holds the application configuration
|
||||
GlobalConfig *Config
|
||||
// configFile is the path to the configuration file
|
||||
configFile string
|
||||
// v is the viper instance
|
||||
v *viper.Viper
|
||||
)
|
||||
|
||||
// initViper initializes the viper instance
|
||||
func initViper() {
|
||||
if v == nil {
|
||||
v = viper.New()
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes the configuration
|
||||
func Init() error {
|
||||
initViper()
|
||||
|
||||
// Set default values
|
||||
v.SetDefault("default_remote", "origin")
|
||||
v.SetDefault("default_branch", "main")
|
||||
v.SetDefault("github_url", "https://api.github.com")
|
||||
v.SetDefault("auto_cleanup", false)
|
||||
v.SetDefault("confirm_destructive", true)
|
||||
v.SetDefault("dry_run", false)
|
||||
v.SetDefault("verbose", false)
|
||||
v.SetDefault("branch_prefix", "")
|
||||
v.SetDefault("feature_prefix", "feature/")
|
||||
v.SetDefault("hotfix_prefix", "hotfix/")
|
||||
|
||||
// Set configuration file paths
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get user home directory: %w", err)
|
||||
}
|
||||
|
||||
configDir := filepath.Join(home, ".gitauto")
|
||||
configFile = filepath.Join(configDir, "config.yaml")
|
||||
|
||||
// Ensure config directory exists
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return fmt.Errorf("could not create config directory: %w", err)
|
||||
}
|
||||
|
||||
// Set configuration file
|
||||
v.SetConfigName("config")
|
||||
v.SetConfigType("yaml")
|
||||
v.AddConfigPath(configDir)
|
||||
v.AddConfigPath(".")
|
||||
|
||||
// Read environment variables
|
||||
v.SetEnvPrefix("GITAUTO")
|
||||
v.AutomaticEnv()
|
||||
|
||||
// Read configuration file
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
// It's okay if config file doesn't exist
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
return fmt.Errorf("could not read config file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Unmarshal configuration
|
||||
GlobalConfig = &Config{}
|
||||
if err := v.Unmarshal(GlobalConfig); err != nil {
|
||||
return fmt.Errorf("could not unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save saves the current configuration to file
|
||||
func Save() error {
|
||||
initViper()
|
||||
|
||||
if err := v.WriteConfigAs(configFile); err != nil {
|
||||
return fmt.Errorf("could not save config file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetConfigFile returns the path to the configuration file
|
||||
func GetConfigFile() string {
|
||||
return configFile
|
||||
}
|
||||
|
||||
// SetConfigFile sets the path to the configuration file (for testing)
|
||||
func SetConfigFile(path string) {
|
||||
configFile = path
|
||||
}
|
||||
|
||||
// Set sets a configuration value
|
||||
func Set(key string, value interface{}) {
|
||||
initViper()
|
||||
|
||||
v.Set(key, value)
|
||||
// Update the global config
|
||||
v.Unmarshal(GlobalConfig)
|
||||
}
|
||||
|
||||
// Get gets a configuration value
|
||||
func Get(key string) interface{} {
|
||||
initViper()
|
||||
|
||||
return v.Get(key)
|
||||
}
|
||||
|
||||
// SetViper sets the viper instance (for testing)
|
||||
func SetViper(viperInstance *viper.Viper) {
|
||||
v = viperInstance
|
||||
}
|
||||
|
||||
// ResetViper resets the viper instance (for testing)
|
||||
func ResetViper() {
|
||||
v = nil
|
||||
}
|
267
internal/config/config_test.go
Normal file
267
internal/config/config_test.go
Normal file
@@ -0,0 +1,267 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func setupTestConfig(t *testing.T) (string, func()) {
|
||||
t.Helper()
|
||||
|
||||
// Create a temporary directory for the test config
|
||||
tempDir, err := os.MkdirTemp("", "config-test-")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
|
||||
// Get the original config file path
|
||||
originalConfigFile := configFile
|
||||
|
||||
// Set the config file path to the temp directory
|
||||
configFile = filepath.Join(tempDir, "config.yaml")
|
||||
|
||||
// Create a cleanup function
|
||||
cleanup := func() {
|
||||
configFile = originalConfigFile
|
||||
os.RemoveAll(tempDir)
|
||||
}
|
||||
|
||||
return tempDir, cleanup
|
||||
}
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
// Save original values
|
||||
originalConfigFile := configFile
|
||||
originalGlobalConfig := GlobalConfig
|
||||
|
||||
// Reset viper to ensure a clean state
|
||||
ResetViper()
|
||||
|
||||
// Setup test config
|
||||
_, cleanup := setupTestConfig(t)
|
||||
defer cleanup()
|
||||
|
||||
// Reset global config
|
||||
GlobalConfig = &Config{}
|
||||
|
||||
// Initialize the config
|
||||
err := Init()
|
||||
if err != nil {
|
||||
t.Fatalf("Init failed: %v", err)
|
||||
}
|
||||
|
||||
// Check default values
|
||||
if GlobalConfig.DefaultRemote != "origin" {
|
||||
t.Errorf("Expected default remote to be 'origin', got '%s'", GlobalConfig.DefaultRemote)
|
||||
}
|
||||
|
||||
if GlobalConfig.DefaultBranch != "main" {
|
||||
t.Errorf("Expected default branch to be 'main', got '%s'", GlobalConfig.DefaultBranch)
|
||||
}
|
||||
|
||||
if GlobalConfig.GitHubURL != "https://api.github.com" {
|
||||
t.Errorf("Expected GitHub URL to be 'https://api.github.com', got '%s'", GlobalConfig.GitHubURL)
|
||||
}
|
||||
|
||||
if GlobalConfig.GitHubToken != "" {
|
||||
t.Errorf("Expected GitHub token to be empty, got '%s'", GlobalConfig.GitHubToken)
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
configFile = originalConfigFile
|
||||
GlobalConfig = originalGlobalConfig
|
||||
}
|
||||
|
||||
func TestSave(t *testing.T) {
|
||||
// Save original values
|
||||
originalConfigFile := configFile
|
||||
originalGlobalConfig := GlobalConfig
|
||||
|
||||
// Reset viper to ensure a clean state
|
||||
ResetViper()
|
||||
|
||||
// Setup test config
|
||||
_, cleanup := setupTestConfig(t)
|
||||
defer cleanup()
|
||||
|
||||
// Reset global config
|
||||
GlobalConfig = &Config{}
|
||||
|
||||
// Initialize the config
|
||||
if err := Init(); err != nil {
|
||||
t.Fatalf("Init failed: %v", err)
|
||||
}
|
||||
|
||||
// Set some values
|
||||
Set("github_token", "test-token")
|
||||
Set("default_branch", "develop")
|
||||
Set("default_remote", "upstream")
|
||||
|
||||
// Save the config
|
||||
err := Save()
|
||||
if err != nil {
|
||||
t.Fatalf("Save failed: %v", err)
|
||||
}
|
||||
|
||||
// Check that the config file exists
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
t.Error("Config file does not exist")
|
||||
}
|
||||
|
||||
// Reset viper again
|
||||
ResetViper()
|
||||
|
||||
// Reset global config
|
||||
GlobalConfig = &Config{}
|
||||
|
||||
// Re-initialize the config to load from file
|
||||
if err := Init(); err != nil {
|
||||
t.Fatalf("Init failed: %v", err)
|
||||
}
|
||||
|
||||
// Check values
|
||||
if GlobalConfig.GitHubToken != "test-token" {
|
||||
t.Errorf("Expected GitHub token to be 'test-token', got '%s'", GlobalConfig.GitHubToken)
|
||||
}
|
||||
|
||||
if GlobalConfig.DefaultBranch != "develop" {
|
||||
t.Errorf("Expected default branch to be 'develop', got '%s'", GlobalConfig.DefaultBranch)
|
||||
}
|
||||
|
||||
if GlobalConfig.DefaultRemote != "upstream" {
|
||||
t.Errorf("Expected default remote to be 'upstream', got '%s'", GlobalConfig.DefaultRemote)
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
configFile = originalConfigFile
|
||||
GlobalConfig = originalGlobalConfig
|
||||
}
|
||||
|
||||
func TestSet(t *testing.T) {
|
||||
// Save original values
|
||||
originalConfigFile := configFile
|
||||
originalGlobalConfig := GlobalConfig
|
||||
|
||||
// Reset viper to ensure a clean state
|
||||
ResetViper()
|
||||
|
||||
// Setup test config
|
||||
_, cleanup := setupTestConfig(t)
|
||||
defer cleanup()
|
||||
|
||||
// Reset global config
|
||||
GlobalConfig = &Config{}
|
||||
|
||||
// Initialize the config
|
||||
if err := Init(); err != nil {
|
||||
t.Fatalf("Init failed: %v", err)
|
||||
}
|
||||
|
||||
// Set a GitHub token
|
||||
Set("github_token", "test-token")
|
||||
|
||||
// Check the value
|
||||
if GlobalConfig.GitHubToken != "test-token" {
|
||||
t.Errorf("Expected GitHub token to be 'test-token', got '%s'", GlobalConfig.GitHubToken)
|
||||
}
|
||||
|
||||
// Set a default branch
|
||||
Set("default_branch", "develop")
|
||||
|
||||
// Check the value
|
||||
if GlobalConfig.DefaultBranch != "develop" {
|
||||
t.Errorf("Expected default branch to be 'develop', got '%s'", GlobalConfig.DefaultBranch)
|
||||
}
|
||||
|
||||
// Set a default remote
|
||||
Set("default_remote", "upstream")
|
||||
|
||||
// Check the value
|
||||
if GlobalConfig.DefaultRemote != "upstream" {
|
||||
t.Errorf("Expected default remote to be 'upstream', got '%s'", GlobalConfig.DefaultRemote)
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
configFile = originalConfigFile
|
||||
GlobalConfig = originalGlobalConfig
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
// Save original values
|
||||
originalConfigFile := configFile
|
||||
originalGlobalConfig := GlobalConfig
|
||||
|
||||
// Reset viper to ensure a clean state
|
||||
ResetViper()
|
||||
|
||||
// Setup test config
|
||||
_, cleanup := setupTestConfig(t)
|
||||
defer cleanup()
|
||||
|
||||
// Reset global config
|
||||
GlobalConfig = &Config{}
|
||||
|
||||
// Initialize the config
|
||||
if err := Init(); err != nil {
|
||||
t.Fatalf("Init failed: %v", err)
|
||||
}
|
||||
|
||||
// Set some values
|
||||
Set("github_token", "test-token")
|
||||
Set("default_branch", "develop")
|
||||
Set("default_remote", "upstream")
|
||||
|
||||
// Get the values
|
||||
token := Get("github_token")
|
||||
if token != "test-token" {
|
||||
t.Errorf("Expected GitHub token to be 'test-token', got '%v'", token)
|
||||
}
|
||||
|
||||
branch := Get("default_branch")
|
||||
if branch != "develop" {
|
||||
t.Errorf("Expected default branch to be 'develop', got '%v'", branch)
|
||||
}
|
||||
|
||||
remote := Get("default_remote")
|
||||
if remote != "upstream" {
|
||||
t.Errorf("Expected default remote to be 'upstream', got '%v'", remote)
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
configFile = originalConfigFile
|
||||
GlobalConfig = originalGlobalConfig
|
||||
}
|
||||
|
||||
func TestGetConfigFile(t *testing.T) {
|
||||
// Save original values
|
||||
originalConfigFile := configFile
|
||||
originalGlobalConfig := GlobalConfig
|
||||
|
||||
// Reset viper to ensure a clean state
|
||||
ResetViper()
|
||||
|
||||
// Setup test config
|
||||
_, cleanup := setupTestConfig(t)
|
||||
defer cleanup()
|
||||
|
||||
// Reset global config
|
||||
GlobalConfig = &Config{}
|
||||
|
||||
// Initialize the config
|
||||
if err := Init(); err != nil {
|
||||
t.Fatalf("Init failed: %v", err)
|
||||
}
|
||||
|
||||
// Get the config file path
|
||||
configFilePath := GetConfigFile()
|
||||
|
||||
// Check that it matches the expected path
|
||||
if configFilePath != configFile {
|
||||
t.Errorf("Expected config file path to be '%s', got '%s'", configFile, configFilePath)
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
configFile = originalConfigFile
|
||||
GlobalConfig = originalGlobalConfig
|
||||
}
|
Reference in New Issue
Block a user