From 30015bee0500c6d705c23d8e1c0de27a32627b9c Mon Sep 17 00:00:00 2001 From: Brian McGee Date: Fri, 21 Mar 2025 09:41:06 +0000 Subject: [PATCH] wip: add a git-mergetool sub command Partial implementation of #523. Signed-off-by: Brian McGee --- cmd/git_mergetool.go | 72 ++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 14 ++++++-- format/formatter_test.go | 10 +++--- stats/stats.go | 4 +-- walk/filesystem_test.go | 2 +- walk/git_test.go | 4 +-- 6 files changed, 94 insertions(+), 12 deletions(-) create mode 100644 cmd/git_mergetool.go diff --git a/cmd/git_mergetool.go b/cmd/git_mergetool.go new file mode 100644 index 00000000..6bfb51bf --- /dev/null +++ b/cmd/git_mergetool.go @@ -0,0 +1,72 @@ +package cmd + +import ( + "fmt" + "os" + "os/exec" + + "github.com/numtide/treefmt/v2/cmd/format" + "github.com/numtide/treefmt/v2/stats" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// gitMergetool handles a 3-way merge using `git merge-file` and formats the resulting merged file. +// It expects 4 arguments: current, base, other, and merged filenames. +// Returns an error if the process fails or if arguments are invalid. +func gitMergetool( + v *viper.Viper, + statz *stats.Stats, + cmd *cobra.Command, + args []string, +) error { + if len(args) != 4 { + return fmt.Errorf("expected 4 arguments, got %d", len(args)) + } + + current := args[0] + base := args[1] + other := args[2] + merged := args[3] + + // run treefmt on the first three arguments: current, base and other + _, _ = fmt.Fprintf(os.Stderr, "formatting: %s, %s, %s\n\n", current, base, other) + + //nolint:wrapcheck + if err := format.Run(v, statz, cmd, args[:3]); err != nil { + return err + } + + // open merge file + mergeFile, err := os.OpenFile(merged, os.O_WRONLY|os.O_CREATE, 0o600) + if err != nil { + return fmt.Errorf("failed to open merge file: %w", err) + } + + // merge current base and other + merge := exec.Command("git", "merge-file", "--stdout", current, base, other) + _, _ = fmt.Fprintf(os.Stderr, "\n%s\n", merge.String()) + + // redirect stdout to the merge file + merge.Stdout = mergeFile + // capture stderr + merge.Stderr = os.Stderr + + if err = merge.Run(); err != nil { + return fmt.Errorf("failed to run git merge-file: %w", err) + } + + // close the merge file + if err = mergeFile.Close(); err != nil { + return fmt.Errorf("failed to close temporary merge file: %w", err) + } + + // format the merge file + _, _ = fmt.Fprintf(os.Stderr, "formatting: %s\n\n", mergeFile.Name()) + + if err = format.Run(v, stats.New(), cmd, []string{mergeFile.Name()}); err != nil { + return fmt.Errorf("failed to format merged file: %w", err) + } + + return nil +} diff --git a/cmd/root.go b/cmd/root.go index b9b711dc..3dec78fa 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -34,7 +34,7 @@ func NewRoot() (*cobra.Command, *stats.Stats) { DisableDefaultCmd: true, }, RunE: func(cmd *cobra.Command, args []string) error { - return runE(v, &statz, cmd, args) + return runE(v, statz, cmd, args) }, } @@ -70,6 +70,9 @@ func NewRoot() (*cobra.Command, *stats.Stats) { "[bash|zsh|fish] Generate shell completion scripts for the specified shell.", ) + // add a flag for git merge tool sub command + fs.Bool("git-mergetool", false, "Use treefmt as a git merge tool. Accepts four arguments: current base other merged.") + // bind our command's flags to viper if err := v.BindPFlags(fs); err != nil { cobra.CheckErr(fmt.Errorf("failed to bind global config to viper: %w", err)) @@ -79,7 +82,7 @@ func NewRoot() (*cobra.Command, *stats.Stats) { // conforms with https://github.com/numtide/prj-spec/blob/main/PRJ_SPEC.md cobra.CheckErr(v.BindPFlag("prj_root", fs.Lookup("tree-root"))) - return cmd, &statz + return cmd, statz } func runE(v *viper.Viper, statz *stats.Stats, cmd *cobra.Command, args []string) error { @@ -175,6 +178,13 @@ func runE(v *viper.Viper, statz *stats.Stats, cmd *cobra.Command, args []string) } } + // git mergetool + if merge, err := flags.GetBool("git-mergetool"); err != nil { + cobra.CheckErr(fmt.Errorf("failed to read git-mergetool flag: %w", err)) + } else if merge { + return gitMergetool(v, statz, cmd, args) + } + // format return format.Run(v, statz, cmd, args) //nolint:wrapcheck } diff --git a/format/formatter_test.go b/format/formatter_test.go index 273689bf..40c1b85f 100644 --- a/format/formatter_test.go +++ b/format/formatter_test.go @@ -24,7 +24,7 @@ func TestInvalidFormatterName(t *testing.T) { statz := stats.New() // simple "empty" config - _, err := NewCompositeFormatter(cfg, &statz, batchSize) + _, err := NewCompositeFormatter(cfg, statz, batchSize) as.NoError(err) // valid name using all the acceptable characters @@ -35,7 +35,7 @@ func TestInvalidFormatterName(t *testing.T) { }, } - _, err = NewCompositeFormatter(cfg, &statz, batchSize) + _, err = NewCompositeFormatter(cfg, statz, batchSize) as.NoError(err) // test with some bad examples @@ -48,7 +48,7 @@ func TestInvalidFormatterName(t *testing.T) { }, } - _, err = NewCompositeFormatter(cfg, &statz, batchSize) + _, err = NewCompositeFormatter(cfg, statz, batchSize) as.ErrorIs(err, ErrInvalidName) } } @@ -108,7 +108,7 @@ func TestFormatSignature(t *testing.T) { }) t.Run("modify formatter options", func(_ *testing.T) { - f, err := NewCompositeFormatter(cfg, &statz, batchSize) + f, err := NewCompositeFormatter(cfg, statz, batchSize) as.NoError(err) oldSignature = assertSignatureChangedAndStable(t, as, cfg, nil) @@ -169,7 +169,7 @@ func assertSignatureChangedAndStable( t.Helper() statz := stats.New() - f, err := NewCompositeFormatter(cfg, &statz, 1024) + f, err := NewCompositeFormatter(cfg, statz, 1024) as.NoError(err) newHash, err := f.signature() diff --git a/stats/stats.go b/stats/stats.go index d39a228b..845e8ec1 100644 --- a/stats/stats.go +++ b/stats/stats.go @@ -55,14 +55,14 @@ func (s *Stats) PrintToStderr() { ) } -func New() Stats { +func New() *Stats { counters := make(map[Type]*atomic.Int64) counters[Traversed] = &atomic.Int64{} counters[Matched] = &atomic.Int64{} counters[Formatted] = &atomic.Int64{} counters[Changed] = &atomic.Int64{} - return Stats{ + return &Stats{ start: time.Now(), counters: counters, } diff --git a/walk/filesystem_test.go b/walk/filesystem_test.go index d7e6f1a5..60699942 100644 --- a/walk/filesystem_test.go +++ b/walk/filesystem_test.go @@ -54,7 +54,7 @@ func TestFilesystemReader(t *testing.T) { tempDir := test.TempExamples(t) statz := stats.New() - r := walk.NewFilesystemReader(tempDir, "", &statz, 1024) + r := walk.NewFilesystemReader(tempDir, "", statz, 1024) count := 0 diff --git a/walk/git_test.go b/walk/git_test.go index 8731da2d..25480e09 100644 --- a/walk/git_test.go +++ b/walk/git_test.go @@ -26,7 +26,7 @@ func TestGitReader(t *testing.T) { // read empty worktree statz := stats.New() - reader, err := walk.NewGitReader(tempDir, "", &statz) + reader, err := walk.NewGitReader(tempDir, "", statz) as.NoError(err) files := make([]*walk.File, 8) @@ -42,7 +42,7 @@ func TestGitReader(t *testing.T) { cmd.Dir = tempDir as.NoError(cmd.Run(), "failed to add everything to the index") - reader, err = walk.NewGitReader(tempDir, "", &statz) + reader, err = walk.NewGitReader(tempDir, "", statz) as.NoError(err) count := 0