Skip to content

wip: add a git-mergetool sub command #561

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions cmd/git_mergetool.go
Original file line number Diff line number Diff line change
@@ -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
}
14 changes: 12 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -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
}
10 changes: 5 additions & 5 deletions format/formatter_test.go
Original file line number Diff line number Diff line change
@@ -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()
4 changes: 2 additions & 2 deletions stats/stats.go
Original file line number Diff line number Diff line change
@@ -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,
}
2 changes: 1 addition & 1 deletion walk/filesystem_test.go
Original file line number Diff line number Diff line change
@@ -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

4 changes: 2 additions & 2 deletions walk/git_test.go
Original file line number Diff line number Diff line change
@@ -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