Repository: numtide/treefmt (go)

Issue:
Fails when project root is a symlink
**Describe the bug**

When any element in the project root path is a symlink, formatting fails.

**To Reproduce**

Steps to reproduce the behavior:

```
$ # On macOS, $TMPDIR begins with a symlink:

$ echo $TMPDIR
/var/folders/z5/fclwwdms3r1gq4k4p3pkvvc00000gn/T/

$ readlink /var
private/var

$ cd "$(mktemp -d)"

$ cat .treefmt.toml
[formatter.echo]
command = "echo"
includes = ["*.txt"]

$ touch puppy.txt

$ treefmt puppy.txt
Error: path puppy.txt not inside the tree root /var/folders/z5/fclwwdms3r1gq4k4p3pkvvc00000gn/T/tmp.OmlZjRNdbX
```

**Expected behavior**

The symlink is followed and files are formatted correctly.

**System information**

macOS Sequoia 15.4.1 (24E263), M4 Max (aarch64-darwin)

```
$ treefmt --version
treefmt v2.2.1
```

**Additional context**

Possibly related:
- #528
- #508 
- #221



Pull request description:
Fix when tree root is symlink
Fixes #578

Patch:
diff --git a/cmd/format/format.go b/cmd/format/format.go
index ca27290..9a7b9a5 100644
--- a/cmd/format/format.go
+++ b/cmd/format/format.go
@@ -221,23 +221,22 @@ func Run(v *viper.Viper, statz *stats.Stats, cmd *cobra.Command, paths []string)
 // Symlinks are allowed in `paths` and we resolve them here, since
 // the readers will ignore symlinks.
 func resolvePaths(paths []string, walkType walk.Type, treeRoot string) error {
-	for i, path := range paths {
-		log.Debugf("Resolving path '%s': %v", path, walkType)
+	// Note: `treeRoot` may itself be or contain a symlink (e.g. it is in
+	// `$TMPDIR` on macOS or a user has set a symlink to shorten the repository
+	// path for path length restrictions), so we resolve it here first.
+	//
+	// See: https://github.com/numtide/treefmt/issues/578
+	treeRoot, err := resolvePath(walkType, treeRoot)
+	if err != nil {
+		return fmt.Errorf("error computing absolute path of %s: %w", treeRoot, err)
+	}
 
-		absolutePath, err := filepath.Abs(path)
+	for i, path := range paths {
+		absolutePath, err := resolvePath(walkType, path)
 		if err != nil {
 			return fmt.Errorf("error computing absolute path of %s: %w", path, err)
 		}
 
-		if walkType != walk.Stdin {
-			realPath, err := filepath.EvalSymlinks(absolutePath)
-			if err != nil {
-				return fmt.Errorf("path %s not found: %w", absolutePath, err)
-			}
-
-			absolutePath = realPath
-		}
-
 		relativePath, err := filepath.Rel(treeRoot, absolutePath)
 		if err != nil {
 			return fmt.Errorf("error computing relative path from %s to %s: %w", treeRoot, absolutePath, err)
@@ -252,3 +251,24 @@ func resolvePaths(paths []string, walkType walk.Type, treeRoot string) error {
 
 	return nil
 }
+
+// Resolve a path to an absolute path, resolving symlinks if necessary.
+func resolvePath(walkType walk.Type, path string) (string, error) {
+	log.Debugf("Resolving path '%s': %v", path, walkType)
+
+	absolutePath, err := filepath.Abs(path)
+	if err != nil {
+		return "", fmt.Errorf("error computing absolute path of %s: %w", path, err)
+	}
+
+	if walkType != walk.Stdin {
+		realPath, err := filepath.EvalSymlinks(absolutePath)
+		if err != nil {
+			return "", fmt.Errorf("path %s not found: %w", absolutePath, err)
+		}
+
+		absolutePath = realPath
+	}
+
+	return absolutePath, nil
+}


Test patch:
diff --git a/cmd/root_test.go b/cmd/root_test.go
index 429c2cd..f5a2da7 100644
--- a/cmd/root_test.go
+++ b/cmd/root_test.go
@@ -2099,6 +2099,44 @@ func TestRunInSubdir(t *testing.T) {
 	}
 }
 
+// Check that supplying paths on the command-line works when an element of the
+// project root is a symlink.
+//
+// Regression test for #578.
+//
+// See: https://github.com/numtide/treefmt/issues/578
+func TestProjectRootIsSymlink(t *testing.T) {
+	as := require.New(t)
+
+	tempDir := t.TempDir()
+	realRoot := filepath.Join(tempDir, "/real-root")
+	test.TempExamplesInDir(t, realRoot)
+
+	symlinkRoot := filepath.Join(tempDir, "/project-root")
+	err := os.Symlink(realRoot, symlinkRoot)
+	as.NoError(err)
+
+	test.ChangeWorkDir(t, symlinkRoot)
+
+	// basic config
+	cfg := &config.Config{
+		FormatterConfigs: map[string]*config.Formatter{
+			"echo": {
+				Command:  "echo",
+				Includes: []string{"*"},
+			},
+		},
+	}
+
+	configPath := filepath.Join(symlinkRoot, "/treefmt.toml")
+	test.WriteConfig(t, configPath, cfg)
+
+	treefmt(t,
+		withArgs("-c", "go/main.go"),
+		withNoError(t),
+	)
+}
+
 type options struct {
 	args []string
 	env  map[string]string