Skip to content

fix(sources): resolve symlinks when computing repo-relative lock paths - #365

Merged
Nan Liu (liunan-ms) merged 6 commits into
microsoft:mainfrom
IzanVil:fix/render-symlinked-repo-path
Nov 11, 2026
Merged

Nan Liu (liunan-ms) merged 6 commits into
microsoft:mainfrom
IzanVil:fix/render-symlinked-repo-path

Conversation

@IzanVil

Copy link
Copy Markdown
Contributor

Fixes #188

Problem

When the project is reached through a symlinked directory, azldev component render silently drops the synthetic changelog history and regresses release_number.

go-git reports the worktree root with symlinks resolved (go-billy's osfs calls filepath.EvalSymlinks on its base directory), while paths derived from the project config (LockDir, SourceConfigFile) keep the invocation path. filepath.Rel between the two produced ../../../home/user/repos/azurelinux/locks/rust.lock, readLockFileAtHEAD treated the lock as not committed, and synthetic history was skipped with only a debug log.

Changes

  • Add git.RepoRelPath, which resolves symlinks in both the repository root and the target path (tolerating trailing elements that do not exist yet) before relativizing, and rejects results that escape the repository root.
  • Use it for the lock file path in buildSyntheticCommits and the upstream-commit TOML path in buildUpstreamCommitSyntheticCommits.
  • Replace the local repoRelPath in cmds/component with the shared helper. component changed failed with "escapes repository root" and component history silently dropped TOML/lock metrics through a symlinked path for the same reason.

Behavior change: a lock path that resolves outside the project repository is now an error in synthetic history instead of a silent skip, as suggested under "Additional hardening" in the issue.

Testing

  • TestBuildSyntheticCommits_SymlinkedProjectPath: loads a real project config through a symlinked directory against an on-disk go-git repository, with git log stubbed through the test command factory. It fails before this change (the lock is never found, no changes are returned) and passes after it.
  • TestRepoRelPath / TestRepoRelPath_EscapesRoot: table tests for the helper (symlinked path, symlinked root, missing file under a symlink, escape via an in-repo symlink).
  • Both use t.TempDir() because symlink resolution cannot be replicated in memory.
  • mage unit and mage check lint pass.

go-git reports the project worktree root with symlinks resolved, while
paths derived from the project config keep the invocation directory. When
the project is reached through a symlink, the repo-relative lock file path
became "../../..", the committed lock was not found at HEAD, and
'component render' silently dropped the synthetic changelog history and
regressed the release number.

Add git.RepoRelPath, which resolves symlinks on both sides before
relativizing and rejects paths that escape the repository root, and use it
for the lock file and upstream-commit TOML paths in synthetic history.

Fixes microsoft#188

Signed-off-by: IzanVil <soyoquienmas@outlook.com>
'component changed' failed with "escapes repository root" and
'component history' silently dropped TOML and lock metrics when the project
was reached through a symlinked path, for the same reason as microsoft#188. Replace
the local repoRelPath helper with git.RepoRelPath.

Signed-off-by: IzanVil <soyoquienmas@outlook.com>
Copilot AI lite review requested due to automatic review settings September 26, 2026 21:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Address the dangling-symlink escape and inconsistent TOML sharing-key handling.

Review effort: Lite
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Fixes symlink-related repository path resolution for synthetic history and component metrics.

Changes:

  • Adds shared symlink-aware git.RepoRelPath with escape detection.
  • Applies it to synthetic history and component commands.
  • Adds filesystem-backed regression tests.

Review findings:

  • Critical (4 votes): Dangling symlinks may still be treated as inside the repository.
  • Moderate (2 votes): Canonical TOML caching is inconsistent with tomlSharing, breaking --shared=omit for symlink aliases.
File Description
internal/​utils/​git/​gogit.go Adds repository-relative path resolution.
internal/​utils/​git/​gogit_test.go Tests symlink and escape cases.
internal/​app/​azldev/​core/​sources/​synthistory.go Uses the helper for lock paths.
internal/​app/​azldev/​core/​sources/​synthistory_upstreamcommit.go Uses the helper for TOML paths.
internal/​app/​azldev/​core/​sources/​synthistory_internal_test.go Tests synthetic history through symlinked paths.
internal/​app/​azldev/​cmds/​component/​history_gitmetrics.go Uses shared path resolution for metrics.
internal/​app/​azldev/​cmds/​component/​changed.go Replaces the local path helper.
internal/​app/​azldev/​cmds/​component/​changed_upstreamcommit.go Uses shared path resolution.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/utils/git/gogit.go Resolved
A symlink whose target does not exist made EvalSymlinks fail, and the
fallback appended the link name to its resolved parent without following
it. An in-repo link pointing outside the repository was then reported as
inside it. Read the link target and keep resolving it instead.

Signed-off-by: IzanVil <soyoquienmas@outlook.com>
Copilot AI review requested due to automatic review settings September 26, 2026 22:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

Only a minor documentation nit remains; no blocking issues were identified.

Review effort: Lite
Findings: None

Resolved since last review (2)

@IzanVil

Copy link
Copy Markdown
Contributor Author

Friendly ping: this one has been open for a few weeks with no human review yet. Copilot's two findings were addressed (the dangling-symlink case is fixed in d3b16cf, with tests), and it still merges cleanly with current main, builds, and passes lint. Happy to rebase, split it, or adjust the approach if you would prefer something different. Thanks!

Copilot AI lite review requested due to automatic review settings November 10, 2026 21:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The change still has an unused import that prevents compilation and lacks the required documentation for the new exported function, causing lint failure.

2 open issues

Review effort: Lite

}

lockRelDir, err := repoRelPath(repoRoot, env.Config().Project.LockDir)
lockRelDir, err := git.RepoRelPath(repoRoot, env.Config().Project.LockDir)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the unused strings import.

})
}

func RepoRelPath(repoRoot, absPath string) (string, error) {

@liunan-ms Nan Liu (liunan-ms) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for contributing! I rebased this PR to main and ran the CI checks. Would you mind taking another look and fixing them?

Comment thread internal/utils/git/gogit.go Outdated
target = filepath.Join(resolvedParent, target)
}

return resolveSymlinks(target)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a cycle guard here? A link pointing to missing/../link can hit the missing-file fallback, then filepath.Join cleans the target back to the same link. That keeps calling resolveSymlinks until it crashes with a stack overflow. A visited-path check or traversal limit, plus a test for this case, would help.

Signed-off-by: IzanVil <soyoquienmas@outlook.com>
…lic links

Signed-off-by: IzanVil <soyoquienmas@outlook.com>
Copilot AI lite review requested due to automatic review settings November 11, 2026 16:34
@IzanVil

Copy link
Copy Markdown
Contributor Author

Done Nan Liu (@liunan-ms)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread internal/utils/git/gogit.go Resolved
Comment on lines +17 to +25
func TestRepoRelPath(t *testing.T) {
tempDir := t.TempDir()

realRepoDir := filepath.Join(tempDir, "real", "repo")
linkedRepoDir := filepath.Join(tempDir, "link", "repo")

require.NoError(t, os.MkdirAll(filepath.Join(realRepoDir, "locks"), fileperms.PrivateDir))
require.NoError(t, os.WriteFile(filepath.Join(realRepoDir, "locks", "curl.lock"), nil, fileperms.PrivateFile))
require.NoError(t, os.Symlink(filepath.Join(tempDir, "real"), filepath.Join(tempDir, "link")))
Comment thread internal/utils/git/gogit.go Resolved
@liunan-ms
Nan Liu (liunan-ms) merged commit b361e31 into microsoft:main Nov 11, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

component render silently drops synthetic history when the project repo path traverses a symlink

3 participants