List a directory named through a symlink in gen and freshen (closes #185) #186

Merged
clawbot merged 1 commits from issue-185-symlink-root into next 2026-10-08 05:59:03 +02:00
4 changed files with 113 additions and 1 deletions
+45
View File
@@ -1369,6 +1369,51 @@ func TestGenerateSeveralPathsDefaultOutput(t *testing.T) {
} }
} }
// testLink is the name of the symlink to a tree that the
// DirectoryNamedThroughSymlink tests make in a temp dir.
const testLink = "link"
// TestGenerateDirectoryNamedThroughSymlink runs gen on a directory named
// through a symlink, given as the argument or as the working directory: the
// manifest lists the files in the directory the symlink points to, and
// leaves out a symlink inside it, as gen does without --follow-symlinks.
//
//nolint:paralleltest // changes the process-global working directory
func TestGenerateDirectoryNamedThroughSymlink(t *testing.T) {
// Paths are relative to a temp dir holding data and link, a symlink to
// data.
for name, tc := range map[string]struct {
workDir string
args []string
}{
"argument": {".", []string{testLink}},
"working directory": {testLink, nil},
} {
t.Run(name, func(t *testing.T) {
root := t.TempDir()
data := filepath.Join(root, "data")
fs := afero.NewOsFs()
writeTestTree(t, fs, data)
require.NoError(t,
os.Symlink(testFileTxt, filepath.Join(data, "alias.txt")))
require.NoError(t, os.Symlink(data, filepath.Join(root, testLink)))
// t.Chdir sets PWD to the path it is given, as a shell does, and
// os.Getwd returns PWD when it names the working directory.
t.Chdir(filepath.Join(root, tc.workDir))
opts := testOpts(slices.Concat(
[]string{testApp, cmdGenerate, "-q"}, tc.args,
), fs)
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
assert.ElementsMatch(t, []string{testFileTxt, "sub/nested.txt"},
manifestPaths(t, fs, filepath.Join(data, defaultManifestName)))
})
}
}
// TestGenerateBytesDoNotDependOnOutput runs gen --seed on one tree, each // TestGenerateBytesDoNotDependOnOutput runs gen --seed on one tree, each
// time writing to another file, over a file already there and beside an // time writing to another file, over a file already there and beside an
// earlier run's temp file: neither is listed, and what is listed depends // earlier run's temp file: neither is listed, and what is listed depends
+9 -1
View File
@@ -379,6 +379,14 @@ func (mfa *CLIApp) freshenScan(
} }
} }
// The walk does not follow a symlink at its top, so a base directory
// named through one is resolved first. If that fails, the base is
// walked as named and the walk reports the problem.
resolved, err := filepath.EvalSymlinks(absBase)
if err == nil {
absBase = resolved
}
scanner := &freshenScanner{ scanner := &freshenScanner{
fs: mfa.Fs, fs: mfa.Fs,
absBase: absBase, absBase: absBase,
@@ -390,7 +398,7 @@ func (mfa *CLIApp) freshenScan(
existingByPath: existingByPath, existingByPath: existingByPath,
} }
err := afero.Walk(mfa.Fs, absBase, scanner.walk) err = afero.Walk(mfa.Fs, absBase, scanner.walk)
if showProgress { if showProgress {
log.ProgressDone() log.ProgressDone()
+43
View File
@@ -336,6 +336,49 @@ func TestFreshenResolvesEntriesAgainstManifestDirectory(t *testing.T) {
} }
} }
// TestFreshenDirectoryNamedThroughSymlink adds a file to a tree after gen
// made its manifest, then freshens it with the tree named through a symlink,
// given as the argument or as the working directory: the manifest lists the
// files in the tree, and leaves out a symlink inside it, as freshen does
// without --follow-symlinks.
//
//nolint:paralleltest // changes the process-global working directory
func TestFreshenDirectoryNamedThroughSymlink(t *testing.T) {
// Paths are relative to a temp dir holding link, a symlink to the tree.
for name, tc := range map[string]struct {
workDir string
args []string
}{
"argument": {".", []string{testLink}},
"working directory": {testLink, nil},
} {
t.Run(name, func(t *testing.T) {
fs := afero.NewOsFs()
tree, manifestPath := setupFreshenDir(t, fs,
map[string]string{testFileTxt: "in the tree"})
writeTestFile(t, fs, filepath.Join(tree, "later.txt"), "added later")
require.NoError(t,
os.Symlink(testFileTxt, filepath.Join(tree, "alias.txt")))
root := t.TempDir()
require.NoError(t, os.Symlink(tree, filepath.Join(root, testLink)))
// t.Chdir sets PWD to the path it is given, as a shell does, and
// os.Getwd returns PWD when it names the working directory.
t.Chdir(filepath.Join(root, tc.workDir))
opts := testOpts(slices.Concat(
[]string{testApp, cmdFreshen, "-q"}, tc.args,
), fs)
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
assertManifestLists(t, fs, tree, manifestPath, map[string]string{
testFileTxt: "in the tree", "later.txt": "added later",
})
})
}
}
// TestFreshenRecordEntryMtimePresence pins the behavior of recordEntry // TestFreshenRecordEntryMtimePresence pins the behavior of recordEntry
// with respect to MFFilePath.Mtime, which is a message pointer with // with respect to MFFilePath.Mtime, which is a message pointer with
// proto3 field presence and may legitimately be absent. // proto3 field presence and may legitimately be absent.
+16
View File
@@ -155,6 +155,14 @@ func (s *Scanner) EnumeratePath(
return err return err
} }
// The walk does not follow a symlink at its top, so a directory named
// through one is resolved first. If that fails, the directory is walked
// as named and the walk reports the problem.
resolved, err := filepath.EvalSymlinks(abs)
if err == nil {
abs = resolved
}
afs := afero.NewReadOnlyFs(afero.NewBasePathFs(s.fs, abs)) afs := afero.NewReadOnlyFs(afero.NewBasePathFs(s.fs, abs))
return s.enumerateFS(afs, abs, progress) return s.enumerateFS(afs, abs, progress)
@@ -185,6 +193,14 @@ func (s *Scanner) EnumeratePaths(
} }
if info.IsDir() { if info.IsDir() {
// The walk does not follow a symlink at its top, so a directory
// named through one is resolved first. If that fails, the
// directory is walked as named and the walk reports the problem.
resolved, evalErr := filepath.EvalSymlinks(abs)
if evalErr == nil {
abs = resolved
}
afs := afero.NewReadOnlyFs(afero.NewBasePathFs(s.fs, abs)) afs := afero.NewReadOnlyFs(afero.NewBasePathFs(s.fs, abs))
err = s.enumerateFS(afs, abs, progress) err = s.enumerateFS(afs, abs, progress)
} else { } else {