Skip to content

Commit 30015be

Browse files
committed
wip: add a git-mergetool sub command
Partial implementation of #523. Signed-off-by: Brian McGee <brian@bmcgee.ie>
1 parent dd35fce commit 30015be

File tree

6 files changed

+94
-12
lines changed

6 files changed

+94
-12
lines changed

cmd/git_mergetool.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
8+
"github.com/numtide/treefmt/v2/cmd/format"
9+
"github.com/numtide/treefmt/v2/stats"
10+
"github.com/spf13/cobra"
11+
"github.com/spf13/viper"
12+
)
13+
14+
// gitMergetool handles a 3-way merge using `git merge-file` and formats the resulting merged file.
15+
// It expects 4 arguments: current, base, other, and merged filenames.
16+
// Returns an error if the process fails or if arguments are invalid.
17+
func gitMergetool(
18+
v *viper.Viper,
19+
statz *stats.Stats,
20+
cmd *cobra.Command,
21+
args []string,
22+
) error {
23+
if len(args) != 4 {
24+
return fmt.Errorf("expected 4 arguments, got %d", len(args))
25+
}
26+
27+
current := args[0]
28+
base := args[1]
29+
other := args[2]
30+
merged := args[3]
31+
32+
// run treefmt on the first three arguments: current, base and other
33+
_, _ = fmt.Fprintf(os.Stderr, "formatting: %s, %s, %s\n\n", current, base, other)
34+
35+
//nolint:wrapcheck
36+
if err := format.Run(v, statz, cmd, args[:3]); err != nil {
37+
return err
38+
}
39+
40+
// open merge file
41+
mergeFile, err := os.OpenFile(merged, os.O_WRONLY|os.O_CREATE, 0o600)
42+
if err != nil {
43+
return fmt.Errorf("failed to open merge file: %w", err)
44+
}
45+
46+
// merge current base and other
47+
merge := exec.Command("git", "merge-file", "--stdout", current, base, other)
48+
_, _ = fmt.Fprintf(os.Stderr, "\n%s\n", merge.String())
49+
50+
// redirect stdout to the merge file
51+
merge.Stdout = mergeFile
52+
// capture stderr
53+
merge.Stderr = os.Stderr
54+
55+
if err = merge.Run(); err != nil {
56+
return fmt.Errorf("failed to run git merge-file: %w", err)
57+
}
58+
59+
// close the merge file
60+
if err = mergeFile.Close(); err != nil {
61+
return fmt.Errorf("failed to close temporary merge file: %w", err)
62+
}
63+
64+
// format the merge file
65+
_, _ = fmt.Fprintf(os.Stderr, "formatting: %s\n\n", mergeFile.Name())
66+
67+
if err = format.Run(v, stats.New(), cmd, []string{mergeFile.Name()}); err != nil {
68+
return fmt.Errorf("failed to format merged file: %w", err)
69+
}
70+
71+
return nil
72+
}

cmd/root.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func NewRoot() (*cobra.Command, *stats.Stats) {
3434
DisableDefaultCmd: true,
3535
},
3636
RunE: func(cmd *cobra.Command, args []string) error {
37-
return runE(v, &statz, cmd, args)
37+
return runE(v, statz, cmd, args)
3838
},
3939
}
4040

@@ -70,6 +70,9 @@ func NewRoot() (*cobra.Command, *stats.Stats) {
7070
"[bash|zsh|fish] Generate shell completion scripts for the specified shell.",
7171
)
7272

73+
// add a flag for git merge tool sub command
74+
fs.Bool("git-mergetool", false, "Use treefmt as a git merge tool. Accepts four arguments: current base other merged.")
75+
7376
// bind our command's flags to viper
7477
if err := v.BindPFlags(fs); err != nil {
7578
cobra.CheckErr(fmt.Errorf("failed to bind global config to viper: %w", err))
@@ -79,7 +82,7 @@ func NewRoot() (*cobra.Command, *stats.Stats) {
7982
// conforms with https://github.com/numtide/prj-spec/blob/main/PRJ_SPEC.md
8083
cobra.CheckErr(v.BindPFlag("prj_root", fs.Lookup("tree-root")))
8184

82-
return cmd, &statz
85+
return cmd, statz
8386
}
8487

8588
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)
175178
}
176179
}
177180

181+
// git mergetool
182+
if merge, err := flags.GetBool("git-mergetool"); err != nil {
183+
cobra.CheckErr(fmt.Errorf("failed to read git-mergetool flag: %w", err))
184+
} else if merge {
185+
return gitMergetool(v, statz, cmd, args)
186+
}
187+
178188
// format
179189
return format.Run(v, statz, cmd, args) //nolint:wrapcheck
180190
}

format/formatter_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func TestInvalidFormatterName(t *testing.T) {
2424
statz := stats.New()
2525

2626
// simple "empty" config
27-
_, err := NewCompositeFormatter(cfg, &statz, batchSize)
27+
_, err := NewCompositeFormatter(cfg, statz, batchSize)
2828
as.NoError(err)
2929

3030
// valid name using all the acceptable characters
@@ -35,7 +35,7 @@ func TestInvalidFormatterName(t *testing.T) {
3535
},
3636
}
3737

38-
_, err = NewCompositeFormatter(cfg, &statz, batchSize)
38+
_, err = NewCompositeFormatter(cfg, statz, batchSize)
3939
as.NoError(err)
4040

4141
// test with some bad examples
@@ -48,7 +48,7 @@ func TestInvalidFormatterName(t *testing.T) {
4848
},
4949
}
5050

51-
_, err = NewCompositeFormatter(cfg, &statz, batchSize)
51+
_, err = NewCompositeFormatter(cfg, statz, batchSize)
5252
as.ErrorIs(err, ErrInvalidName)
5353
}
5454
}
@@ -108,7 +108,7 @@ func TestFormatSignature(t *testing.T) {
108108
})
109109

110110
t.Run("modify formatter options", func(_ *testing.T) {
111-
f, err := NewCompositeFormatter(cfg, &statz, batchSize)
111+
f, err := NewCompositeFormatter(cfg, statz, batchSize)
112112
as.NoError(err)
113113

114114
oldSignature = assertSignatureChangedAndStable(t, as, cfg, nil)
@@ -169,7 +169,7 @@ func assertSignatureChangedAndStable(
169169
t.Helper()
170170

171171
statz := stats.New()
172-
f, err := NewCompositeFormatter(cfg, &statz, 1024)
172+
f, err := NewCompositeFormatter(cfg, statz, 1024)
173173
as.NoError(err)
174174

175175
newHash, err := f.signature()

stats/stats.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,14 @@ func (s *Stats) PrintToStderr() {
5555
)
5656
}
5757

58-
func New() Stats {
58+
func New() *Stats {
5959
counters := make(map[Type]*atomic.Int64)
6060
counters[Traversed] = &atomic.Int64{}
6161
counters[Matched] = &atomic.Int64{}
6262
counters[Formatted] = &atomic.Int64{}
6363
counters[Changed] = &atomic.Int64{}
6464

65-
return Stats{
65+
return &Stats{
6666
start: time.Now(),
6767
counters: counters,
6868
}

walk/filesystem_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func TestFilesystemReader(t *testing.T) {
5454
tempDir := test.TempExamples(t)
5555
statz := stats.New()
5656

57-
r := walk.NewFilesystemReader(tempDir, "", &statz, 1024)
57+
r := walk.NewFilesystemReader(tempDir, "", statz, 1024)
5858

5959
count := 0
6060

walk/git_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func TestGitReader(t *testing.T) {
2626

2727
// read empty worktree
2828
statz := stats.New()
29-
reader, err := walk.NewGitReader(tempDir, "", &statz)
29+
reader, err := walk.NewGitReader(tempDir, "", statz)
3030
as.NoError(err)
3131

3232
files := make([]*walk.File, 8)
@@ -42,7 +42,7 @@ func TestGitReader(t *testing.T) {
4242
cmd.Dir = tempDir
4343
as.NoError(cmd.Run(), "failed to add everything to the index")
4444

45-
reader, err = walk.NewGitReader(tempDir, "", &statz)
45+
reader, err = walk.NewGitReader(tempDir, "", statz)
4646
as.NoError(err)
4747

4848
count := 0

0 commit comments

Comments
 (0)