Skip to content

Commit e8b53ec

Browse files
authored
fix: materialize Git symlinks for consistent hashes (#4276)
1 parent 7b9a831 commit e8b53ec

2 files changed

Lines changed: 51 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
1616

1717
- Assertion failure on expanding URLs with variables containing absolute paths ([#4256])
1818
- Correct open rule matching for trashed directories ([#4268])
19+
- Materialize Git symlinks for consistent hashes ([#4276])
1920
- Wait for terminal probe echo back before stopping instance ([#4271])
2021
- Avoid flicker caused by screen clear on final response from terminal ([#4250])
2122
- Fall back when `vergen` cannot determine Git SHA ([#4252])
@@ -1838,3 +1839,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
18381839
[#4265]: https://github.com/sxyazi/yazi/pull/4265
18391840
[#4268]: https://github.com/sxyazi/yazi/pull/4268
18401841
[#4271]: https://github.com/sxyazi/yazi/pull/4271
1842+
[#4276]: https://github.com/sxyazi/yazi/pull/4276

yazi-cli/src/package/git.rs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
1-
use std::path::Path;
1+
use std::path::{Path, PathBuf};
22

33
use anyhow::{Context, Result, bail};
4-
use tokio::process::Command;
4+
use tokio::{fs, process::Command};
55
use yazi_shared::strip_trailing_newline;
6+
use yazi_shim::wtf8::{FromWtf8, FromWtf8Vec};
67

78
pub(super) struct Git;
89

910
impl Git {
1011
pub(super) async fn clone(url: &str, path: &Path) -> Result<()> {
11-
Self::exec(|c| c.args(["clone", url]).arg(path)).await
12+
Self::exec(|c| c.args(["clone", url]).arg(path)).await?;
13+
Self::materialize(path).await
1214
}
1315

1416
pub(super) async fn fetch(path: &Path) -> Result<()> {
1517
Self::exec(|c| c.arg("fetch").current_dir(path)).await
1618
}
1719

1820
pub(super) async fn checkout(path: &Path, rev: &str) -> Result<()> {
19-
Self::exec(|c| c.args(["checkout", rev, "--force"]).current_dir(path)).await
21+
Self::exec(|c| c.args(["checkout", rev, "--force"]).current_dir(path)).await?;
22+
Self::materialize(path).await
2023
}
2124

2225
pub(super) async fn pull(path: &Path) -> Result<()> {
@@ -42,13 +45,55 @@ impl Git {
4245
))
4346
}
4447

48+
async fn materialize(path: &Path) -> Result<()> {
49+
let path = fs::canonicalize(path).await.context("Failed to resolve Git repository")?;
50+
let output = Command::new("git")
51+
.args(["ls-files", "--stage", "-z"])
52+
.current_dir(&path)
53+
.output()
54+
.await
55+
.context("Failed to list Git files")?;
56+
if !output.status.success() {
57+
bail!("Listing Git files failed: {}", output.status);
58+
}
59+
60+
for ent in output.stdout.split(|&c| c == 0).filter(|b| !b.is_empty()) {
61+
if !ent.starts_with(b"120000 ") {
62+
continue;
63+
}
64+
65+
let Some(tab) = ent.iter().position(|&b| b == b'\t') else { continue };
66+
let link = path.join(
67+
Path::from_wtf8(&ent[tab + 1..]).context("Git path cannot be represented by the OS")?,
68+
);
69+
70+
let original = PathBuf::from_wtf8_vec(fs::read(&link).await?)
71+
.context("Git symlink origin cannot be represented by the OS")?;
72+
let original = fs::canonicalize(link.parent().unwrap_or(&path).join(original))
73+
.await
74+
.with_context(|| format!("failed to resolve Git symlink target `{}`", link.display()))?;
75+
76+
if !original.starts_with(&path) {
77+
bail!("Git symlink target escapes repository: `{}`", link.display());
78+
}
79+
80+
fs::copy(original, &link)
81+
.await
82+
.with_context(|| format!("failed to materialize `{}`", link.display()))?;
83+
}
84+
85+
Ok(())
86+
}
87+
4588
async fn exec(f: impl FnOnce(&mut Command) -> &mut Command) -> Result<()> {
4689
let status = f(Command::new("git").args([
4790
"-c",
4891
"core.eol=lf",
4992
"-c",
5093
"core.autocrlf=false",
5194
"-c",
95+
"core.symlinks=false",
96+
"-c",
5297
"clone.defaultRemoteName=origin",
5398
"-c",
5499
"checkout.defaultRemote=origin",

0 commit comments

Comments
 (0)