diff --git a/ci/build_test_release.go b/ci/build_test_release.go index 6bf7408..1e4d36d 100644 --- a/ci/build_test_release.go +++ b/ci/build_test_release.go @@ -24,7 +24,7 @@ func main() { // mount the source code directory on the host // at /src in the container source := client.Container(). - From("golang:1.23"). + From("golang:1.24"). WithDirectory("/src_d", client.Host().Directory(".", dagger.HostDirectoryOpts{ Exclude: []string{}, })).WithMountedCache("/src_d/dagger_dep_cache/go_dep", goCache) diff --git a/ci/go.mod b/ci/go.mod index dfa1893..0c3440b 100644 --- a/ci/go.mod +++ b/ci/go.mod @@ -1,6 +1,6 @@ module ci -go 1.23.2 +go 1.24.6 require dagger.io/dagger v0.13.6 diff --git a/ci/test_on_push.go b/ci/test_on_push.go index 17edfd5..2509e61 100644 --- a/ci/test_on_push.go +++ b/ci/test_on_push.go @@ -24,7 +24,7 @@ func main() { // mount the source code directory on the host // at /src in the container source := client.Container(). - From("golang:1.23"). + From("golang:1.24"). WithDirectory("/src_d", client.Host().Directory(".", dagger.HostDirectoryOpts{ Exclude: []string{"build/"}, })).WithMountedCache("/src_d/dagger_dep_cache/go_dep", goCache) @@ -41,4 +41,4 @@ func main() { panic(err) } fmt.Println(out) -} \ No newline at end of file +} diff --git a/src/cmd/amend.go b/src/cmd/amend.go index 315c695..0510a7e 100644 --- a/src/cmd/amend.go +++ b/src/cmd/amend.go @@ -24,7 +24,7 @@ var amendCmd = &cobra.Command{ pflag, _ := cmd.Flags().GetBool("print-output") tflag, _ := cmd.Flags().GetBool("test_print") git_flags, _ := cmd.Flags().GetString("git-flags") - edit, _ := cmd.Flags().GetBool("edit") + no_edit, _ := cmd.Flags().GetBool("no-edit") hash, _ := cmd.Flags().GetString("hash") @@ -33,10 +33,6 @@ var amendCmd = &cobra.Command{ hash = "" return } - - if edit { - - } var authors string if len(args) == 0 { @@ -56,7 +52,7 @@ var amendCmd = &cobra.Command{ git_flags_split = strings.Split(git_flags, " ") } - err, _ := utils.GitCommitAppender(authors, hash, git_flags_split, tflag, pflag) + err, _ := utils.GitCommitAppender(authors, hash, git_flags_split, tflag, pflag, no_edit) if err != nil { println("Error amending commit:", err.Error()) os.Exit(1) @@ -70,6 +66,6 @@ func init() { amendCmd.Flags().StringP("git-flags", "g", "", "Git flags to add to the commit command") amendCmd.Flags().BoolP("print-output", "p", false, "Print the commit message to stdout") amendCmd.Flags().BoolP("test_print", "t", false, "Print the commit message to stdout without amending") - amendCmd.Flags().BoolP("edit", "e", false, "Edit the commit message in the editor") + amendCmd.Flags().BoolP("no-edit", "n", false, "Do not edit the commit message in the editor") amendCmd.Flags().StringP("hash", "s", "", "Hash of the commit to amend") } diff --git a/src/cmd/config.go b/src/cmd/config.go new file mode 100644 index 0000000..bf48e89 --- /dev/null +++ b/src/cmd/config.go @@ -0,0 +1,68 @@ +package cmd + +import ( + "fmt" + + "github.com/Slug-Boi/cocommit/src/cmd/utils" + "github.com/spf13/cobra" +) + +// configCmd represents the config command +var configCmd = &cobra.Command{ + Use: "config", + Short: "This command will create or edit the configuration file for cocommit", + Long: `This command will create or edit the configuration file for cocommit. +You can set various settings like the author file, starting scope, and which editor to use. +A flag can be used to print the current configuration, as well as its location. +To see what options are available to use in the config file, please refer to the wiki page on the GitHub repository: +COMING SOON`, + Run: func(cmd *cobra.Command, args []string) { + printConfig, _ := cmd.Flags().GetBool("print") + editConfig, _ := cmd.Flags().GetBool("edit") + configLocation, _ := cmd.Flags().GetBool("location") + removeConfig, _ := cmd.Flags().GetBool("remove") + + if printConfig { + if !utils.CheckConfig() { + fmt.Println("No configuration file found. Default is being used.") + fmt.Println("Default configuration:") + + } else { + fmt.Println("Current configuration:") + } + fmt.Println(utils.ConfigVar.String()) + } + + // Check if the config file exists + if !utils.CheckConfig() { + err := utils.HandleMissingConfig() + if err != nil { + panic(fmt.Sprintf("Error handling missing configuration file: %v", err)) + } + return + } + if printConfig { + return + } + + if editConfig { + utils.LaunchEditor("default",utils.GetConfigFilePath()) + return + } else if configLocation { + fmt.Println("Configuration file location:", utils.GetConfigFilePath()) + return + } else if removeConfig { + utils.RemoveConfig() + return + } + fmt.Println("No action specified. Use flags to specify an action, use -h for help.") + }, +} + +func init() { + rootCmd.AddCommand(configCmd) + configCmd.Flags().BoolP("print", "p", false, "Print the current configuration") + configCmd.Flags().BoolP("edit", "e", false, "Edit the configuration file in your default editor") + configCmd.Flags().BoolP("location", "l", false, "Print the location of the configuration file") + configCmd.Flags().BoolP("remove", "r", false, "Remove the configuration file") +} diff --git a/src/cmd/root.go b/src/cmd/root.go index 12b09f4..9d2f59f 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -9,8 +9,8 @@ import ( "github.com/Slug-Boi/cocommit/src/cmd/tui" "github.com/Slug-Boi/cocommit/src/cmd/utils" - "github.com/inancgumus/screen" "github.com/charmbracelet/lipgloss" + "github.com/inancgumus/screen" "github.com/spf13/cobra" ) @@ -155,7 +155,16 @@ func Execute() { func call_tui(args []string) []string { // append commit message to args - args = append(args, tui.Entry_CM()) + //args = append(args, tui.Entry_CM()) + message, err := utils.LaunchEditor(utils.ConfigVar.Settings.Editor,"") + if err != nil { + panic(fmt.Sprintf("Error launching editor: %v", err)) + } + if message == "" { + message = tui.Entry_CM() + } + + args = append(args, message) // clear the screen screen.Clear() diff --git a/src/cmd/utils/author_file_utils.go b/src/cmd/utils/author_file_utils.go index e8ebe05..a020dec 100644 --- a/src/cmd/utils/author_file_utils.go +++ b/src/cmd/utils/author_file_utils.go @@ -14,37 +14,33 @@ import ( // An example of the author file can be found in the examples folder of the repo func Find_authorfile() string { var file string - - if os.Getenv("author_file") == "" { - if ConfigVar == nil { - cfg, _ := LoadConfig() - if cfg == nil { - // mimic the default config structure - cfg = &Config{ - Settings: struct { - AuthorFile string `mapstructure:"author_file"` - StartingScope string `mapstructure:"starting_scope"` - Editor string `mapstructure:"editor"` - }{ - AuthorFile: "", - StartingScope: "git", - Editor: "built-in", - }, - } - cfg.SetGlobalConfig() + if ConfigVar == nil { + cfg, _ := LoadConfig() + if cfg == nil { + // mimic the default config structure + cfg = &Config{ + Settings: struct { + AuthorFile string `mapstructure:"author_file"` + StartingScope string `mapstructure:"starting_scope"` + Editor string `mapstructure:"editor"` + }{ + AuthorFile: "", + StartingScope: "git", + Editor: "built-in", + }, } } + cfg.SetGlobalConfig() + } + if os.Getenv("author_file") == "" { if ConfigVar.Settings.AuthorFile != "" { file = ConfigVar.Settings.AuthorFile - } else if os.Getenv("author_file") != "" { - file = os.Getenv("author_file") } else { userconf, err :=os.UserConfigDir() if err != nil { panic(fmt.Sprintf("Error getting user config dir: %v", err)) - } - + } if _, err := os.Stat(userconf+"/cocommit/authors.json"); os.IsNotExist(err) { panic(fmt.Sprintf("No author file set, please set the author_file environment variable or create a config file using the command: cocommit config -c")) } else { diff --git a/src/cmd/utils/commit.go b/src/cmd/utils/commit.go index be7cbac..72ffcf4 100644 --- a/src/cmd/utils/commit.go +++ b/src/cmd/utils/commit.go @@ -2,6 +2,7 @@ package utils import ( "fmt" + "os" "os/exec" "regexp" "slices" @@ -129,7 +130,7 @@ func group_selection(group []User, excludeMode []string) []string { return excludeMode } -func GitCommitAppender(authors string, hash string, flags []string, t,p bool) (error, string) { +func GitCommitAppender(authors string, hash string, flags []string, t,p,n bool) (error, string) { // Get old commit message var cmd *exec.Cmd @@ -153,9 +154,36 @@ func GitCommitAppender(authors string, hash string, flags []string, t,p bool) (e // commit shell command // specify git command1 input := []string{"commit"} + + // Edit the old message input = append(input, flags...) old_commit = strings.TrimSpace(old_commit) - input = append(input, "--amend", "-m", old_commit+"\n"+authors) + + // Edit old commit message + var edited_commit string + if !n { + // Create tempfile for the commit message + file, err := os.CreateTemp("", "cocommit_editor_*.txt") + if err != nil { + return fmt.Errorf("Could not create tempfile: %s", err.Error()), "" + } + defer os.Remove(file.Name()) + + // Write the old commit message to the file + _, err = file.WriteString(old_commit + "\n" + authors) + if err != nil { + return fmt.Errorf("Could not write to tempfile: %s", err.Error()), "" + } + file.Close() + edited_commit, err = LaunchEditor(ConfigVar.Settings.Editor, file.Name()) + if err != nil { + return fmt.Errorf("Could not launch editor: %s", err.Error()), "" + } + } else { + edited_commit = old_commit + "\n" + authors + } + + input = append(input, "--amend", "-m", edited_commit) if p { println(old_commit + "\n" + authors) diff --git a/src/cmd/utils/config.go b/src/cmd/utils/config.go index d151e2d..35efdf4 100644 --- a/src/cmd/utils/config.go +++ b/src/cmd/utils/config.go @@ -33,6 +33,13 @@ type Config struct { } `mapstructure:"settings"` } +func (c *Config) String() string { + return fmt.Sprintf("Author File: %s\nStarting Scope: %s\nEditor: %s", + c.Settings.AuthorFile, + c.Settings.StartingScope, + c.Settings.Editor) +} + func init() { configDir, err := os.UserConfigDir() if err == nil { @@ -40,10 +47,11 @@ func init() { } } +var v *viper.Viper + func LoadConfig() (*Config, error) { // TODO: create if and give param as default config location - - v := viper.New() + v = viper.New() v.SetConfigName(configName) v.SetConfigType(configType) @@ -89,10 +97,10 @@ func (c *Config) SetGlobalConfig() { ConfigVar = c // This doesnt really do much right now but might be useful later viper.WatchConfig() - } + } } -func handleMissingConfig(v *viper.Viper) error { +func HandleMissingConfig() error { fmt.Println("Config file not found. Would you like to create one? (y/n)") var response string if _, err := fmt.Scanln(&response); err != nil { @@ -104,10 +112,48 @@ func handleMissingConfig(v *viper.Viper) error { return fmt.Errorf("config file not found") } - return createConfig(v) + if v == nil { + v = viper.New() + + v.SetConfigName(configName) + v.SetConfigType(configType) + } + + return CreateConfig() } -func createConfig(v *viper.Viper) error { +func CheckConfig() bool { + if v == nil { + return false + } else if v.ConfigFileUsed() == "" { + return false + } else { + return true + } +} + +func GetConfigFilePath() string { + if v == nil || v.ConfigFileUsed() == "" { + return "" + } + return v.ConfigFileUsed() +} + +func RemoveConfig() error { + if v == nil || v.ConfigFileUsed() == "" { + return fmt.Errorf("no config file to remove") + } + + configPath := v.ConfigFileUsed() + if err := os.Remove(configPath); err != nil { + return fmt.Errorf("failed to remove config file: %w", err) + } + + fmt.Printf("Config file removed: %s\n", configPath) + return nil +} + +func CreateConfig() error { fmt.Println("Where would you like to create the config file?") for i, path := range defaultConfigLocations { fmt.Printf("%d. %s\n", i, path) @@ -145,38 +191,38 @@ func createConfig(v *viper.Viper) error { } func (c *Config) Save() error { - v := viper.New() - - // Set all configuration values from the struct - v.Set("settings.author_file", c.Settings.AuthorFile) - v.Set("settings.starting_scope", c.Settings.StartingScope) - v.Set("settings.editor", c.Settings.Editor) - - v.SetConfigName(configName) - v.SetConfigType(configType) - - // Try to determine the original config file location - if viper.ConfigFileUsed() != "" { - v.SetConfigFile(viper.ConfigFileUsed()) - } else { - // Fall back to first default location if no existing config - if len(defaultConfigLocations) > 0 && defaultConfigLocations[0] != "" { - v.SetConfigFile(filepath.Join(defaultConfigLocations[0], fmt.Sprintf("%s.%s", configName, configType))) - } else { - return fmt.Errorf("no config file location available") - } - } - - // Ensure the directory exists - configDir := filepath.Dir(v.ConfigFileUsed()) - if err := os.MkdirAll(configDir, 0755); err != nil { - return fmt.Errorf("failed to create config directory: %w", err) - } - - // Write the config file - if err := v.WriteConfig(); err != nil { - return fmt.Errorf("failed to save config: %w", err) - } - - return nil -} \ No newline at end of file + v := viper.New() + + // Set all configuration values from the struct + v.Set("settings.author_file", c.Settings.AuthorFile) + v.Set("settings.starting_scope", c.Settings.StartingScope) + v.Set("settings.editor", c.Settings.Editor) + + v.SetConfigName(configName) + v.SetConfigType(configType) + + // Try to determine the original config file location + if viper.ConfigFileUsed() != "" { + v.SetConfigFile(viper.ConfigFileUsed()) + } else { + // Fall back to first default location if no existing config + if len(defaultConfigLocations) > 0 && defaultConfigLocations[0] != "" { + v.SetConfigFile(filepath.Join(defaultConfigLocations[0], fmt.Sprintf("%s.%s", configName, configType))) + } else { + return fmt.Errorf("no config file location available") + } + } + + // Ensure the directory exists + configDir := filepath.Dir(v.ConfigFileUsed()) + if err := os.MkdirAll(configDir, 0755); err != nil { + return fmt.Errorf("failed to create config directory: %w", err) + } + + // Write the config file + if err := v.WriteConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + return nil +} diff --git a/src/cmd/utils/editor.go b/src/cmd/utils/editor.go new file mode 100644 index 0000000..42dc882 --- /dev/null +++ b/src/cmd/utils/editor.go @@ -0,0 +1,104 @@ +package utils + +import ( + "fmt" + "os" + "os/exec" + "strings" +) + +func HandleEditor() (string, error) { + editor := ConfigVar.Settings.Editor + if editor == "built-in" { + return "", nil + } + + if editor == "" || editor == "default" { + editor = os.Getenv("EDITOR") + if editor == "" { + editor = "vim" // default to vim if no editor is set + } + } + + if _, err := exec.LookPath(editor); err != nil { + return "", fmt.Errorf("editor %s not found in PATH", editor) + } + + output, err := LaunchEditor(editor, "") + if err != nil { + return "", fmt.Errorf("failed to launch editor %s: %v", editor, err) + } + return output, nil +} + +func LaunchEditor(editor string, filepath string, ) (string, error) { + // Create a temp file or use an existing file + var tempFile *os.File + var err error + + switch strings.ToLower(editor) { + case "default", "": + editor = os.Getenv("EDITOR") + if editor == "" { + editor = "vim" // default to vim if no editor is set + } + case "built-in": + // fallback to built-in editor + return "", nil + default: + if _, err := exec.LookPath(editor); err != nil { + return "", fmt.Errorf("editor %s not found in PATH", editor) + } + } + + if filepath == "" { + tempFile, err = os.CreateTemp("", "cocommit_editor_*.txt") + defer os.Remove(tempFile.Name()) + } else { + tempFile, err = os.OpenFile(filepath, os.O_RDWR, 0666) + } + if err != nil { + return "", fmt.Errorf("Could not create or open tempfile: %s", err.Error()) + } + + cmd := exec.Command(editor, tempFile.Name()) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("error running editor command: %v", err) + } + + data, err := os.ReadFile(tempFile.Name()) + if err != nil { + return "", fmt.Errorf("error reading temp file: %v", err) + } + + message := string(data) + if message == "" { + fmt.Printf("Error: Commit message is empty. Please provide a commit message.\n") + os.Exit(0) + } + + // Clean up the temp file + if strings.HasSuffix(message, "\n") { + message = strings.TrimSuffix(message, "\n") + } + if strings.HasSuffix(message, "\r") { + message = strings.TrimSuffix(message, "\r") + } + if strings.TrimSpace(message) == "" { + fmt.Printf("Error: Commit message is empty. Please provide a commit message.\n") + os.Exit(0) + } + // If the message is too long, truncate it + // if len(message) > 72 { + // fmt.Printf("Warning: Commit message is too long (%d characters). It will be truncated to 72 characters.\n", len(message)) + // //TODO: Maybe add the rest to the description of the message? + // //description := message[72:] + // message = message[:72] + // } + + return message, nil +} diff --git a/src/cmd/utils/util_test.go b/src/cmd/utils/util_test.go index d8a8db7..7eb0376 100644 --- a/src/cmd/utils/util_test.go +++ b/src/cmd/utils/util_test.go @@ -7,11 +7,12 @@ import ( "io" "net/http" "os" + "os/exec" "strings" "testing" - "os/exec" "github.com/Slug-Boi/cocommit/src/cmd/utils" + "github.com/spf13/viper" ) const author_data = ` @@ -166,6 +167,7 @@ func Test_FindAuthorFilePanic(t *testing.T) { os.Setenv("author_file", "") os.Setenv("HOME", "") os.Setenv("XDG_CONFIG_HOME", "") + utils.ConfigVar.Settings.AuthorFile = "" utils.Find_authorfile() } @@ -178,14 +180,10 @@ func Test_FindAuthorFileEnv(t *testing.T) { defer func() { os.Setenv("author_file", originalAuthorFile) - - if r := recover(); r == nil { - t.Errorf("Find_authorfile() did not panic") - } }() - // Set an invalid environment variable to trigger panic - os.Setenv("author_file", "") + os.Setenv("author_file", "author_file_test") + utils.ConfigVar.Settings.AuthorFile = "" utils.Find_authorfile() } @@ -652,7 +650,7 @@ func Test_CommitAppender(t *testing.T) { message := strings.TrimSpace(string(out)) commit := utils.Commit("", authors) - err, appendedMessage := utils.GitCommitAppender(commit, "", nil, true, true) + err, appendedMessage := utils.GitCommitAppender(commit, "", nil, true, true, true) if err != nil { t.Errorf("GitCommitAppender() returned error: %v", err) } @@ -665,7 +663,7 @@ func Test_CommitAppender(t *testing.T) { // check inverted commit authors = []string{"^te"} commit = utils.Commit("", authors) - err, appendedMessage = utils.GitCommitAppender(commit, "", nil, true, true) + err, appendedMessage = utils.GitCommitAppender(commit, "", nil, true, true, true) if err != nil { t.Errorf("GitCommitAppender() returned error: %v", err) } @@ -678,7 +676,7 @@ func Test_CommitAppender(t *testing.T) { // Test CommitAppender with multiple authors authors = []string{"te", "testtest"} commit = utils.Commit("", authors) - err, appendedMessage = utils.GitCommitAppender(commit, "", nil, true, true) + err, appendedMessage = utils.GitCommitAppender(commit, "", nil, true, true, true) if err != nil { t.Errorf("GitCommitAppender() returned error: %v", err) } @@ -690,7 +688,7 @@ func Test_CommitAppender(t *testing.T) { // Test CommitAppender with all authors authors = []string{"all"} commit = utils.Commit("", authors) - err, appendedMessage = utils.GitCommitAppender(commit, "", nil, true, true) + err, appendedMessage = utils.GitCommitAppender(commit, "", nil, true, true, true) if err != nil { t.Errorf("GitCommitAppender() returned error: %v", err) } @@ -704,7 +702,7 @@ func Test_CommitAppender(t *testing.T) { // Test CommitAppender with group authors authors = []string{"gr1"} commit = utils.Commit("", authors) - err, appendedMessage = utils.GitCommitAppender(commit, "", nil, true, true) + err, appendedMessage = utils.GitCommitAppender(commit, "", nil, true, true, true) if err != nil { t.Errorf("GitCommitAppender() returned error: %v", err) } @@ -842,6 +840,65 @@ func Test_FetchGHProfileHTTP(t *testing.T) { } } - - // Github tests END + +// Config tests BEGIN + +func Test_Save(t *testing.T) { + setup() + defer teardown() + + filename := "test_save_config.toml" + + initial_config_data := `[settings] +author_file = "test_authors.json" +starting_scope = "git" +editor = "built-in"` + + os.Create(filename) + defer os.Remove(filename) + // Write some test data to the file + os.WriteFile(filename, []byte(initial_config_data), 0644) + + override_cfg := &utils.Config{ + Settings: struct { + AuthorFile string `mapstructure:"author_file"` + StartingScope string `mapstructure:"starting_scope"` + Editor string `mapstructure:"editor"` + }{ + AuthorFile: "test_authors.json", + StartingScope: "git", + Editor: "built-in", + }} + + + + // Set viper config file to be cfg + viper.SetConfigFile(filename) + // Set the config type to toml + viper.SetConfigType("toml") + + + // Change some values in the config + override_cfg.Settings.AuthorFile = "test" + override_cfg.Settings.StartingScope = "not_git" + override_cfg.Settings.Editor = "test_editor" + + // Save the config + err := override_cfg.Save() + if err != nil { + t.Errorf("Save() returned error: %v", err) + } + + // Check if file exists and contains expected content + data, err := os.ReadFile(filename) + if err != nil { + t.Errorf("Save() did not create file: %v", data) + } + if string(initial_config_data) == string(data) { + t.Errorf("Save() did not write expected content:\nNew:\n%s\n\nOld:\n%s", string(data), string(initial_config_data)) + } +} + + +