Skip to content

Commit 56904ba

Browse files
committed
feat(walk): add jujutsu walker
- added jujutsu module similar to the git module, which provides the `IsInsideWorktree` function - added jujutsu walker, with the following differences to the git walker - the list command does not update the index. thus, new files are not listed, the user has to add them to the index first by running a `jj` command - added jujutsu walker test - added jujutsu walker root test - adapted config and docs
1 parent 7799c74 commit 56904ba

File tree

10 files changed

+544
-12
lines changed

10 files changed

+544
-12
lines changed

cmd/init/init.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
# verbose = 2
4949

5050
# The method used to traverse the files within the tree root
51-
# Currently, we support 'auto', 'git' or 'filesystem'
51+
# Currently, we support 'auto', 'git', 'jujutsu', or 'filesystem'
5252
# Env $TREEFMT_WALK
5353
# walk = "filesystem"
5454

@@ -64,4 +64,4 @@ excludes = []
6464
# Controls the order of application when multiple formatters match the same file
6565
# Lower the number, the higher the precedence
6666
# Default is 0
67-
priority = 0
67+
priority = 0

cmd/root_test.go

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1536,6 +1536,231 @@ func TestGit(t *testing.T) {
15361536
)
15371537
}
15381538

1539+
func TestJujutsu(t *testing.T) {
1540+
as := require.New(t)
1541+
1542+
tempDir := test.TempExamples(t)
1543+
configPath := filepath.Join(tempDir, "/treefmt.toml")
1544+
1545+
test.ChangeWorkDir(t, tempDir)
1546+
1547+
// basic config
1548+
cfg := &config.Config{
1549+
FormatterConfigs: map[string]*config.Formatter{
1550+
"echo": {
1551+
Command: "echo", // will not generate any underlying changes in the file
1552+
Includes: []string{"*"},
1553+
},
1554+
},
1555+
}
1556+
1557+
test.WriteConfig(t, configPath, cfg)
1558+
1559+
// init a jujutsu repo (disable signing for testing)
1560+
jjCmd := exec.Command("jj", "git", "init", "--config", "signing.backend=none")
1561+
as.NoError(jjCmd.Run(), "failed to init jujutsu repository")
1562+
1563+
// run before adding anything to the index
1564+
// we shouldn't pick up untracked files, because here the jujutsu walker differs from the git walker
1565+
treefmt(t,
1566+
withConfig(configPath, cfg),
1567+
withNoError(t),
1568+
withStats(t, map[stats.Type]int{
1569+
stats.Traversed: 0,
1570+
stats.Matched: 0,
1571+
stats.Formatted: 0,
1572+
stats.Changed: 0,
1573+
}),
1574+
)
1575+
1576+
// add everything to the index (disable signing for testing)
1577+
jjCmd = exec.Command("jj", "file", "track", "--config", "signing.backend=none", ".")
1578+
as.NoError(jjCmd.Run(), "failed to add everything to the index")
1579+
1580+
// update jujutsu's index (disable signing for testing)
1581+
jjCmd = exec.Command("jj", "--config", "signing.backend=none")
1582+
as.NoError(jjCmd.Run(), "failed to update the index")
1583+
1584+
// This is our first pass, since previously the files were not in the index. This should format all files.
1585+
treefmt(t,
1586+
withConfig(configPath, cfg),
1587+
withNoError(t),
1588+
withStats(t, map[stats.Type]int{
1589+
stats.Traversed: 32,
1590+
stats.Matched: 32,
1591+
stats.Formatted: 32,
1592+
stats.Changed: 0,
1593+
}),
1594+
)
1595+
1596+
// create a file which should be in .gitignore
1597+
f, err := os.CreateTemp(tempDir, "test-*.txt")
1598+
as.NoError(err, "failed to create temp file")
1599+
1600+
// add everything to the index (disable signing for testing), this command shoud ignore files in .gitignore
1601+
jjCmd = exec.Command("jj", "file", "track", "--config", "signing.backend=none", ".")
1602+
as.NoError(jjCmd.Run(), "failed to add everything to the index")
1603+
1604+
// update jujutsu's index (disable signing for testing)
1605+
jjCmd = exec.Command("jj", "--config", "signing.backend=none")
1606+
as.NoError(jjCmd.Run(), "failed to update the index")
1607+
1608+
t.Cleanup(func() {
1609+
_ = f.Close()
1610+
})
1611+
1612+
treefmt(t,
1613+
withConfig(configPath, cfg),
1614+
withNoError(t),
1615+
withStats(t, map[stats.Type]int{
1616+
stats.Traversed: 32,
1617+
stats.Matched: 32,
1618+
stats.Formatted: 0,
1619+
stats.Changed: 0,
1620+
}),
1621+
)
1622+
1623+
// remove python directory
1624+
as.NoError(os.RemoveAll(filepath.Join(tempDir, "python")), "failed to remove python directory")
1625+
1626+
// update jujutsu's index (disable signing for testing)
1627+
jjCmd = exec.Command("jj", "--config", "signing.backend=none")
1628+
as.NoError(jjCmd.Run(), "failed to update the index")
1629+
1630+
// we should traverse and match against fewer files, but no formatting should occur as no formatting signatures
1631+
// are impacted
1632+
treefmt(t,
1633+
withConfig(configPath, cfg),
1634+
withNoError(t),
1635+
withStats(t, map[stats.Type]int{
1636+
stats.Traversed: 29,
1637+
stats.Matched: 29,
1638+
stats.Formatted: 0,
1639+
stats.Changed: 0,
1640+
}),
1641+
)
1642+
1643+
// remove nixpkgs.toml from the filesystem but leave it in the index
1644+
as.NoError(os.Remove(filepath.Join(tempDir, "nixpkgs.toml")))
1645+
1646+
// update jujutsu's index (disable signing for testing)
1647+
jjCmd = exec.Command("jj", "--config", "signing.backend=none")
1648+
as.NoError(jjCmd.Run(), "failed to update the index")
1649+
1650+
// walk with filesystem instead of with jujutsu
1651+
// the .jj folder contains 104 additional files
1652+
// when added to the 30 we started with (34 minus nixpkgs.toml which we removed from the filesystem), we should
1653+
// traverse 134 files.
1654+
treefmt(t,
1655+
withArgs("--walk", "filesystem"),
1656+
withConfig(configPath, cfg),
1657+
withNoError(t),
1658+
withStats(t, map[stats.Type]int{
1659+
stats.Traversed: 134,
1660+
stats.Matched: 134,
1661+
stats.Formatted: 106, // the echo formatter should only be applied to the new files
1662+
stats.Changed: 0,
1663+
}),
1664+
)
1665+
1666+
// format specific sub paths
1667+
// we should traverse and match against those files, but without any underlying change to their files or their
1668+
// formatting config, we will not format them
1669+
1670+
treefmt(t,
1671+
withArgs("go"),
1672+
withConfig(configPath, cfg),
1673+
withNoError(t),
1674+
withStats(t, map[stats.Type]int{
1675+
stats.Traversed: 2,
1676+
stats.Matched: 2,
1677+
stats.Formatted: 0,
1678+
stats.Changed: 0,
1679+
}),
1680+
)
1681+
1682+
treefmt(t,
1683+
withArgs("go", "haskell"),
1684+
withConfig(configPath, cfg),
1685+
withNoError(t),
1686+
withStats(t, map[stats.Type]int{
1687+
stats.Traversed: 9,
1688+
stats.Matched: 9,
1689+
stats.Formatted: 0,
1690+
stats.Changed: 0,
1691+
}),
1692+
)
1693+
1694+
treefmt(t,
1695+
withArgs("-C", tempDir, "go", "haskell", "ruby"),
1696+
withConfig(configPath, cfg),
1697+
withNoError(t),
1698+
withStats(t, map[stats.Type]int{
1699+
stats.Traversed: 10,
1700+
stats.Matched: 10,
1701+
stats.Formatted: 0,
1702+
stats.Changed: 0,
1703+
}),
1704+
)
1705+
1706+
// try with a bad path
1707+
treefmt(t,
1708+
withArgs("-C", tempDir, "haskell", "foo"),
1709+
withConfig(configPath, cfg),
1710+
withError(func(as *require.Assertions, err error) {
1711+
as.ErrorContains(err, "foo not found")
1712+
}),
1713+
)
1714+
1715+
// try with a path not in the git index
1716+
_, err = os.Create(filepath.Join(tempDir, "foo.txt"))
1717+
as.NoError(err)
1718+
1719+
// add everything to the index (disable signing for testing), this command shoud ignore files in .gitignore
1720+
jjCmd = exec.Command("jj", "file", "track", "--config", "signing.backend=none", ".")
1721+
as.NoError(jjCmd.Run(), "failed to add everything to the index")
1722+
1723+
// update jujutsu's index (disable signing for testing)
1724+
jjCmd = exec.Command("jj", "--config", "signing.backend=none")
1725+
as.NoError(jjCmd.Run(), "failed to update the index")
1726+
1727+
treefmt(t,
1728+
withArgs("haskell", "foo.txt", "-vv"),
1729+
withConfig(configPath, cfg),
1730+
withNoError(t),
1731+
withStats(t, map[stats.Type]int{
1732+
stats.Traversed: 8,
1733+
stats.Matched: 8,
1734+
stats.Formatted: 1, // we only format foo.txt, which is new to the cache
1735+
stats.Changed: 0,
1736+
}),
1737+
)
1738+
1739+
treefmt(t,
1740+
withArgs("go", "foo.txt"),
1741+
withConfig(configPath, cfg),
1742+
withNoError(t),
1743+
withStats(t, map[stats.Type]int{
1744+
stats.Traversed: 3,
1745+
stats.Matched: 3,
1746+
stats.Formatted: 0,
1747+
stats.Changed: 0,
1748+
}),
1749+
)
1750+
1751+
treefmt(t,
1752+
withArgs("foo.txt"),
1753+
withConfig(configPath, cfg),
1754+
withNoError(t),
1755+
withStats(t, map[stats.Type]int{
1756+
stats.Traversed: 1,
1757+
stats.Matched: 1,
1758+
stats.Formatted: 0,
1759+
stats.Changed: 0,
1760+
}),
1761+
)
1762+
}
1763+
15391764
func TestTreeRootCmd(t *testing.T) {
15401765
as := require.New(t)
15411766

config/config.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/charmbracelet/log"
1818
"github.com/google/shlex"
1919
"github.com/numtide/treefmt/v2/git"
20+
"github.com/numtide/treefmt/v2/jujutsu"
2021
"github.com/numtide/treefmt/v2/walk"
2122
"github.com/spf13/pflag"
2223
"github.com/spf13/viper"
@@ -113,8 +114,8 @@ func SetFlags(fs *pflag.FlagSet) {
113114
fs.String(
114115
"tree-root", "",
115116
"The root directory from which treefmt will start walking the filesystem. "+
116-
"Defaults to the root of the current git worktree. If not in a git repo, defaults to the directory "+
117-
"containing the config file. (env $TREEFMT_TREE_ROOT)",
117+
"Defaults to the root of the current git or jujutsu worktree. If not in a git or jujutsu repo, defaults to the "+
118+
"directory containing the config file. (env $TREEFMT_TREE_ROOT)",
118119
)
119120
fs.String(
120121
"tree-root-cmd", "",
@@ -136,7 +137,7 @@ func SetFlags(fs *pflag.FlagSet) {
136137
fs.String(
137138
"walk", "auto",
138139
"The method used to traverse the files within the tree root. Currently supports "+
139-
"<auto|git|filesystem>. (env $TREEFMT_WALK)",
140+
"<auto|git|jujutsu|filesystem>. (env $TREEFMT_WALK)",
140141
)
141142
fs.StringP(
142143
"working-dir", "C", ".",
@@ -329,6 +330,21 @@ func determineTreeRoot(v *viper.Viper, cfg *Config, logger *log.Logger) error {
329330
}
330331
}
331332

333+
// attempt to resolve with jujutsu
334+
if cfg.Walk == walk.Auto.String() || cfg.Walk == walk.Jujutsu.String() {
335+
logger.Infof("attempting to resolve tree root using jujutsu: %s", jujutsu.TreeRootCmd)
336+
337+
// attempt to resolve the tree root with jujutsu
338+
cfg.TreeRoot, err = execTreeRootCmd(jujutsu.TreeRootCmd, cfg.WorkingDirectory)
339+
if err != nil && cfg.Walk == walk.Git.String() {
340+
return fmt.Errorf("failed to resolve tree root with jujutsu: %w", err)
341+
}
342+
343+
if err != nil {
344+
logger.Infof("failed to resolve tree root with jujutsu: %v", err)
345+
}
346+
}
347+
332348
if cfg.TreeRoot == "" {
333349
// fallback to the directory containing the config file
334350
logger.Infof(

docs/site/getting-started/configure.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,8 +317,13 @@ If you wish to pass arguments containing quotes, you should use nested quotes e.
317317
If [walk](#walk) is set to `git` and no tree root option has been defined, `tree-root-cmd` will be defaulted to
318318
`git rev-parse --show-toplevel`.
319319

320+
If [walk](#walk) is set to `jujutsu` and no tree root option has been defined, `tree-root-cmd` will be defaulted to
321+
`jj workspace root`.
322+
320323
if [walk](#walk) is set to `auto` (the default), `treefmt` will check if the [working directory](#working-dir) is
321-
inside a git worktree. If it is, `tree-root-cmd` will be defaulted as described above for `git`.
324+
inside a git worktree. If it is, `tree-root-cmd` will be defaulted as described above for `git`. If the [working
325+
directory](#working-dir) is inside a jujutsu worktree the `tree-root-cmd` will be defaulted as described above for
326+
`jujutsu`.
322327

323328
=== "Flag"
324329

@@ -391,7 +396,7 @@ Set the verbosity level of logs:
391396
### `walk`
392397

393398
The method used to traverse the files within the tree root.
394-
Currently, we support 'auto', 'git' or 'filesystem'
399+
Currently, we support 'auto', 'git', 'jujutsu' or 'filesystem'
395400

396401
=== "Flag"
397402

jujutsu/jujutsu.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package jujutsu
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os/exec"
7+
"strings"
8+
)
9+
10+
const TreeRootCmd = "jj workspace root"
11+
12+
func IsInsideWorktree(path string) (bool, error) {
13+
// check if the root is a jujutsu repository
14+
cmd := exec.Command("jj", "workspace", "root")
15+
cmd.Dir = path
16+
17+
err := cmd.Run()
18+
if err != nil {
19+
var exitErr *exec.ExitError
20+
if errors.As(err, &exitErr) && strings.Contains(string(exitErr.Stderr), "There is no jj repo in \".\"") {
21+
return false, nil
22+
}
23+
24+
return false, fmt.Errorf("failed to check if %s is a jujutsu repository: %w", path, err)
25+
}
26+
// is a jujutsu repo
27+
return true, nil
28+
}

nix/packages/treefmt/default.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ in
4646
nativeBuildInputs =
4747
(with pkgs; [
4848
git
49+
jujutsu
4950
installShellFiles
5051
])
5152
++

0 commit comments

Comments
 (0)