Skip to content

Commit 60b2c48

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 60b2c48

File tree

10 files changed

+522
-11
lines changed

10 files changed

+522
-11
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: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1536,6 +1536,218 @@ 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+
treefmt(t,
1585+
withConfig(configPath, cfg),
1586+
withNoError(t),
1587+
withStats(t, map[stats.Type]int{
1588+
stats.Traversed: 32,
1589+
stats.Matched: 32,
1590+
stats.Formatted: 0,
1591+
stats.Changed: 0,
1592+
}),
1593+
)
1594+
1595+
// create a file which should be in .gitignore
1596+
f, err := os.CreateTemp(tempDir, "test-*.txt")
1597+
as.NoError(err, "failed to create temp file")
1598+
1599+
// add everything to the index (disable signing for testing), this command shoud ignore files in .gitignore
1600+
jjCmd = exec.Command("jj", "file", "track", "--config", "signing.backend=none", ".")
1601+
as.NoError(jjCmd.Run(), "failed to add everything to the index")
1602+
1603+
// update jujutsu's index (disable signing for testing)
1604+
jjCmd = exec.Command("jj", "--config", "signing.backend=none")
1605+
as.NoError(jjCmd.Run(), "failed to update the index")
1606+
1607+
t.Cleanup(func() {
1608+
_ = f.Close()
1609+
})
1610+
1611+
treefmt(t,
1612+
withConfig(configPath, cfg),
1613+
withNoError(t),
1614+
withStats(t, map[stats.Type]int{
1615+
stats.Traversed: 32,
1616+
stats.Matched: 32,
1617+
stats.Formatted: 0,
1618+
stats.Changed: 0,
1619+
}),
1620+
)
1621+
1622+
// remove python directory
1623+
as.NoError(os.RemoveAll(filepath.Join(tempDir, "python")), "failed to remove python directory")
1624+
1625+
// update jujutsu's index (disable signing for testing)
1626+
jjCmd = exec.Command("jj", "--config", "signing.backend=none")
1627+
as.NoError(jjCmd.Run(), "failed to update the index")
1628+
1629+
// we should traverse and match against fewer files, but no formatting should occur as no formatting signatures
1630+
// are impacted
1631+
treefmt(t,
1632+
withConfig(configPath, cfg),
1633+
withNoError(t),
1634+
withStats(t, map[stats.Type]int{
1635+
stats.Traversed: 29,
1636+
stats.Matched: 29,
1637+
stats.Formatted: 0,
1638+
stats.Changed: 0,
1639+
}),
1640+
)
1641+
1642+
// remove nixpkgs.toml from the filesystem but leave it in the index
1643+
as.NoError(os.Remove(filepath.Join(tempDir, "nixpkgs.toml")))
1644+
1645+
// walk with filesystem instead of with jujutsu
1646+
// the .jj folder contains 104 additional files
1647+
// when added to the 30 we started with (34 minus nixpkgs.toml which we removed from the filesystem), we should
1648+
// traverse 134 files.
1649+
treefmt(t,
1650+
withArgs("--walk", "filesystem"),
1651+
withConfig(configPath, cfg),
1652+
withNoError(t),
1653+
withStats(t, map[stats.Type]int{
1654+
stats.Traversed: 134,
1655+
stats.Matched: 134,
1656+
stats.Formatted: 106, // the echo formatter should only be applied to the new files
1657+
stats.Changed: 0,
1658+
}),
1659+
)
1660+
1661+
// format specific sub paths
1662+
// we should traverse and match against those files, but without any underlying change to their files or their
1663+
// formatting config, we will not format them
1664+
1665+
treefmt(t,
1666+
withArgs("go"),
1667+
withConfig(configPath, cfg),
1668+
withNoError(t),
1669+
withStats(t, map[stats.Type]int{
1670+
stats.Traversed: 2,
1671+
stats.Matched: 2,
1672+
stats.Formatted: 0,
1673+
stats.Changed: 0,
1674+
}),
1675+
)
1676+
1677+
treefmt(t,
1678+
withArgs("go", "haskell"),
1679+
withConfig(configPath, cfg),
1680+
withNoError(t),
1681+
withStats(t, map[stats.Type]int{
1682+
stats.Traversed: 9,
1683+
stats.Matched: 9,
1684+
stats.Formatted: 0,
1685+
stats.Changed: 0,
1686+
}),
1687+
)
1688+
1689+
treefmt(t,
1690+
withArgs("-C", tempDir, "go", "haskell", "ruby"),
1691+
withConfig(configPath, cfg),
1692+
withNoError(t),
1693+
withStats(t, map[stats.Type]int{
1694+
stats.Traversed: 10,
1695+
stats.Matched: 10,
1696+
stats.Formatted: 0,
1697+
stats.Changed: 0,
1698+
}),
1699+
)
1700+
1701+
// try with a bad path
1702+
treefmt(t,
1703+
withArgs("-C", tempDir, "haskell", "foo"),
1704+
withConfig(configPath, cfg),
1705+
withError(func(as *require.Assertions, err error) {
1706+
as.ErrorContains(err, "foo not found")
1707+
}),
1708+
)
1709+
1710+
// try with a path not in the git index
1711+
_, err = os.Create(filepath.Join(tempDir, "foo.txt"))
1712+
as.NoError(err)
1713+
1714+
treefmt(t,
1715+
withArgs("haskell", "foo.txt", "-vv"),
1716+
withConfig(configPath, cfg),
1717+
withNoError(t),
1718+
withStats(t, map[stats.Type]int{
1719+
stats.Traversed: 8,
1720+
stats.Matched: 8,
1721+
stats.Formatted: 1, // we only format foo.txt, which is new to the cache
1722+
stats.Changed: 0,
1723+
}),
1724+
)
1725+
1726+
treefmt(t,
1727+
withArgs("go", "foo.txt"),
1728+
withConfig(configPath, cfg),
1729+
withNoError(t),
1730+
withStats(t, map[stats.Type]int{
1731+
stats.Traversed: 3,
1732+
stats.Matched: 3,
1733+
stats.Formatted: 0,
1734+
stats.Changed: 0,
1735+
}),
1736+
)
1737+
1738+
treefmt(t,
1739+
withArgs("foo.txt"),
1740+
withConfig(configPath, cfg),
1741+
withNoError(t),
1742+
withStats(t, map[stats.Type]int{
1743+
stats.Traversed: 1,
1744+
stats.Matched: 1,
1745+
stats.Formatted: 0,
1746+
stats.Changed: 0,
1747+
}),
1748+
)
1749+
}
1750+
15391751
func TestTreeRootCmd(t *testing.T) {
15401752
as := require.New(t)
15411753

config/config.go

Lines changed: 18 additions & 2 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,7 +114,7 @@ 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+
"Defaults to the root of the current git or jujutsu worktree. If not in a git or jujutsu repo, defaults to the directory "+
117118
"containing the config file. (env $TREEFMT_TREE_ROOT)",
118119
)
119120
fs.String(
@@ -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)