Skip to content

Commit 8633913

Browse files
feat: find tree root via a user-specified command
Signed-off-by: Brian McGee <[email protected]> Co-authored-by: Matt Sturgeon <[email protected]> Signed-off-by: Brian McGee <[email protected]>
1 parent ea56597 commit 8633913

File tree

5 files changed

+246
-36
lines changed

5 files changed

+246
-36
lines changed

cmd/init/init.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,20 @@
2828
# on-unmatched = "info"
2929

3030
# The root directory from which treefmt will start walking the filesystem
31-
# Defaults to the directory containing the config file
31+
# Defaults to the root of the current git worktree.
32+
# If not in a git repo, defaults to the directory containing the config file.
3233
# Env $TREEFMT_TREE_ROOT
3334
# tree-root = "/tmp/foo"
3435

3536
# File to search for to find the tree root (if tree-root is not set)
3637
# Env $TREEFMT_TREE_ROOT_FILE
3738
# tree-root-file = ".git/config"
3839

40+
# Command to run to find the tree root. It is parsed using shlex, to allow quoting arguments that contain whitespace.
41+
# If you wish to pass arguments containing quotes, you should use nested quotes e.g. "'" or '"'
42+
# Env $TREEFMT_TREE_ROOT_CMD
43+
# tree-root-cmd = "git rev-parse --show-toplevel"
44+
3945
# Set the verbosity of logs
4046
# 0 = warn, 1 = info, 2 = debug
4147
# Env $TREEFMT_VERBOSE

cmd/root_test.go

Lines changed: 119 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1531,6 +1531,118 @@ func TestGit(t *testing.T) {
15311531
)
15321532
}
15331533

1534+
func TestTreeRootCmd(t *testing.T) {
1535+
as := require.New(t)
1536+
1537+
tempDir := test.TempExamples(t)
1538+
configPath := filepath.Join(tempDir, "/treefmt.toml")
1539+
1540+
test.ChangeWorkDir(t, tempDir)
1541+
1542+
// basic config
1543+
cfg := &config.Config{
1544+
FormatterConfigs: map[string]*config.Formatter{
1545+
"echo": {
1546+
Command: "echo", // will not generate any underlying changes in the file
1547+
Includes: []string{"*"},
1548+
},
1549+
},
1550+
}
1551+
1552+
test.WriteConfig(t, configPath, cfg)
1553+
1554+
// construct a tree root command with some error logging and dumping output on stdout
1555+
treeRootCmd := func(output string) string {
1556+
return fmt.Sprintf("bash -c '>&2 echo -e \"some error text\nsome more error text\" && echo %s'", output)
1557+
}
1558+
1559+
// helper for checking the contents of stderr matches our expected debug output
1560+
checkStderr := func(buf []byte) {
1561+
output := string(buf)
1562+
as.Contains(output, "DEBU tree-root-cmd | stderr: some error text\n")
1563+
as.Contains(output, "DEBU tree-root-cmd | stderr: some more error text\n")
1564+
}
1565+
1566+
// run treefmt with DEBUG logging enabled and with tree root cmd being the root of the temp directory
1567+
treefmt(t,
1568+
withArgs("-vv", "--tree-root-cmd", treeRootCmd(tempDir)),
1569+
withNoError(t),
1570+
withStderr(checkStderr),
1571+
withConfig(configPath, cfg),
1572+
withStats(t, map[stats.Type]int{
1573+
stats.Traversed: 32,
1574+
stats.Matched: 32,
1575+
stats.Formatted: 32,
1576+
stats.Changed: 0,
1577+
}),
1578+
)
1579+
1580+
// run from a subdirectory, mixing things up by specifying the command via an env variable
1581+
treefmt(t,
1582+
withArgs("-vv"),
1583+
withEnv(map[string]string{
1584+
"TREEFMT_TREE_ROOT_CMD": treeRootCmd(filepath.Join(tempDir, "go")),
1585+
}),
1586+
withNoError(t),
1587+
withStderr(checkStderr),
1588+
withConfig(configPath, cfg),
1589+
withStats(t, map[stats.Type]int{
1590+
stats.Traversed: 2,
1591+
stats.Matched: 2,
1592+
stats.Formatted: 2,
1593+
stats.Changed: 0,
1594+
}),
1595+
)
1596+
1597+
// run from a subdirectory, mixing things up by specifying the command via config
1598+
cfg.TreeRootCmd = treeRootCmd(filepath.Join(tempDir, "haskell"))
1599+
1600+
treefmt(t,
1601+
withArgs("-vv"),
1602+
withNoError(t),
1603+
withStderr(checkStderr),
1604+
withConfig(configPath, cfg),
1605+
withStats(t, map[stats.Type]int{
1606+
stats.Traversed: 7,
1607+
stats.Matched: 7,
1608+
stats.Formatted: 7,
1609+
stats.Changed: 0,
1610+
}),
1611+
)
1612+
1613+
// run with a long-running command (2 seconds or more)
1614+
treefmt(t,
1615+
withArgs(
1616+
"-vv",
1617+
"--tree-root-cmd", fmt.Sprintf(
1618+
"bash -c 'sleep 2 && echo %s'",
1619+
tempDir,
1620+
),
1621+
),
1622+
withError(func(as *require.Assertions, err error) {
1623+
as.ErrorContains(err, "tree-root-cmd was killed after taking more than 2s to execute")
1624+
}),
1625+
withConfig(configPath, cfg),
1626+
)
1627+
1628+
// run with a command that outputs multiple lines
1629+
treefmt(t,
1630+
withArgs(
1631+
"--tree-root-cmd", fmt.Sprintf(
1632+
"bash -c 'echo %s && echo %s'",
1633+
tempDir, tempDir,
1634+
),
1635+
),
1636+
withStderr(func(buf []byte) {
1637+
as.Contains(string(buf), fmt.Sprintf("ERRO tree-root-cmd | stdout: \n%s\n%s\n", tempDir, tempDir))
1638+
}),
1639+
withError(func(as *require.Assertions, err error) {
1640+
as.ErrorContains(err, "tree-root-cmd cannot output multiple lines")
1641+
}),
1642+
withConfig(configPath, cfg),
1643+
)
1644+
}
1645+
15341646
func TestTreeRootExclusivity(t *testing.T) {
15351647
tempDir := test.TempExamples(t)
15361648
configPath := filepath.Join(tempDir, "/treefmt.toml")
@@ -1552,7 +1664,7 @@ func TestTreeRootExclusivity(t *testing.T) {
15521664

15531665
assertExclusiveConfig := func(as *require.Assertions, err error) {
15541666
as.ErrorContains(err,
1555-
"only one of tree-root, tree-root-cmd or tree-root-file can be specified",
1667+
"at most one of tree-root, tree-root-cmd or tree-root-file can be specified",
15561668
)
15571669
}
15581670

@@ -1580,7 +1692,7 @@ func TestTreeRootExclusivity(t *testing.T) {
15801692
},
15811693
}
15821694

1583-
permutations := [][]string{
1695+
invalidCombinations := [][]string{
15841696
{"tree-root", "tree-root-cmd"},
15851697
{"tree-root", "tree-root-file"},
15861698
{"tree-root-cmd", "tree-root-file"},
@@ -1591,11 +1703,11 @@ func TestTreeRootExclusivity(t *testing.T) {
15911703
// Given that ultimately everything is being reduced into the config object after parsing from viper, I'm fairly
15921704
// confident if these tests all pass then the mixed methods should yield the same result.
15931705

1594-
// test permutations of the same type
1595-
for _, perm := range permutations {
1706+
// for each set of invalid args, test them with flags, environment variables, and config entries.
1707+
for _, combination := range invalidCombinations {
15961708
// test flags
15971709
var args []string
1598-
for _, key := range perm {
1710+
for _, key := range combination {
15991711
args = append(args, flagValues[key]...)
16001712
}
16011713

@@ -1607,7 +1719,7 @@ func TestTreeRootExclusivity(t *testing.T) {
16071719
// test env variables
16081720
env := make(map[string]string)
16091721

1610-
for _, key := range perm {
1722+
for _, key := range combination {
16111723
entry := envValues[key]
16121724
env[entry[0]] = entry[1]
16131725
}
@@ -1622,7 +1734,7 @@ func TestTreeRootExclusivity(t *testing.T) {
16221734
FormatterConfigs: formatterConfigs,
16231735
}
16241736

1625-
for _, key := range perm {
1737+
for _, key := range combination {
16261738
entry := configValues[key]
16271739
entry(cfg)
16281740
}

0 commit comments

Comments
 (0)