git: Use correct file mode when staging (#41900)

Closes #28667

Release Notes:

- Fixed git not preserving file mode when committing. Now if an input file is executable it will be preserved when committed with Zed.

---------

Signed-off-by: 11happy <soni5happy@gmail.com>
Signed-off-by: 11happy <bhuminjaysoni@gmail.com>
Co-authored-by: Jakub Konka <kubkon@jakubkonka.com>
This commit is contained in:
Bhuminjay Soni
2025-11-26 09:01:20 +00:00
committed by GitHub
co-authored by Jakub Konka
parent 00e93bfa11
commit 425d4c73f3
7 changed files with 126 additions and 3 deletions
+13 -2
View File
@@ -301,6 +301,7 @@ impl std::ops::Deref for Repository {
#[derive(Clone)]
pub enum RepositoryState {
Local {
fs: Arc<dyn Fs>,
backend: Arc<dyn GitRepository>,
environment: Arc<HashMap<String, String>>,
},
@@ -4288,6 +4289,7 @@ impl Repository {
RepositoryState::Local {
backend,
environment,
..
} => backend.run_hook(hook, environment.clone()).await,
RepositoryState::Remote { project_id, client } => {
client
@@ -4580,6 +4582,7 @@ impl Repository {
let id = self.id;
let this = cx.weak_entity();
let git_store = self.git_store.clone();
let abs_path = self.snapshot.repo_path_to_abs_path(&path);
self.send_keyed_job(
Some(GitJobKey::WriteIndex(vec![path.clone()])),
None,
@@ -4588,14 +4591,21 @@ impl Repository {
"start updating index text for buffer {}",
path.as_unix_str()
);
match git_repo {
RepositoryState::Local {
fs,
backend,
environment,
..
} => {
let executable = match fs.metadata(&abs_path).await {
Ok(Some(meta)) => meta.is_executable,
Ok(None) => false,
Err(_err) => false,
};
backend
.set_index_text(path.clone(), content, environment.clone())
.set_index_text(path.clone(), content, environment.clone(), executable)
.await?;
}
RepositoryState::Remote { project_id, client } => {
@@ -5164,6 +5174,7 @@ impl Repository {
cx: &mut Context<Self>,
) -> mpsc::UnboundedSender<GitJob> {
let (job_tx, mut job_rx) = mpsc::unbounded::<GitJob>();
let fs_cloned = fs.clone();
cx.spawn(async move |_, cx| {
let environment = project_environment
@@ -5195,8 +5206,8 @@ impl Repository {
backend.clone(),
);
}
let state = RepositoryState::Local {
fs: fs_cloned,
backend,
environment: Arc::new(environment),
};
+85
View File
@@ -8174,6 +8174,91 @@ async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
});
}
// TODO: Should we test this on Windows also?
#[gpui::test]
#[cfg(not(windows))]
async fn test_staging_hunk_preserve_executable_permission(cx: &mut gpui::TestAppContext) {
use std::os::unix::fs::PermissionsExt;
init_test(cx);
cx.executor().allow_parking();
let committed_contents = "bar\n";
let file_contents = "baz\n";
let root = TempTree::new(json!({
"project": {
"foo": committed_contents
},
}));
let work_dir = root.path().join("project");
let file_path = work_dir.join("foo");
let repo = git_init(work_dir.as_path());
let mut perms = std::fs::metadata(&file_path).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&file_path, perms).unwrap();
git_add("foo", &repo);
git_commit("Initial commit", &repo);
std::fs::write(&file_path, file_contents).unwrap();
let project = Project::test(
Arc::new(RealFs::new(None, cx.executor())),
[root.path()],
cx,
)
.await;
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer(file_path.as_path(), cx)
})
.await
.unwrap();
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
let uncommitted_diff = project
.update(cx, |project, cx| {
project.open_uncommitted_diff(buffer.clone(), cx)
})
.await
.unwrap();
uncommitted_diff.update(cx, |diff, cx| {
let hunks = diff.hunks(&snapshot, cx).collect::<Vec<_>>();
diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx);
});
cx.run_until_parked();
let output = smol::process::Command::new("git")
.current_dir(&work_dir)
.args(["diff", "--staged"])
.output()
.await
.unwrap();
let staged_diff = String::from_utf8_lossy(&output.stdout);
assert!(
!staged_diff.contains("new mode 100644"),
"Staging should not change file mode from 755 to 644.\ngit diff --staged:\n{}",
staged_diff
);
let output = smol::process::Command::new("git")
.current_dir(&work_dir)
.args(["ls-files", "-s"])
.output()
.await
.unwrap();
let index_contents = String::from_utf8_lossy(&output.stdout);
assert!(
index_contents.contains("100755"),
"Index should show file as executable (100755).\ngit ls-files -s:\n{}",
index_contents
);
}
#[gpui::test]
async fn test_repository_and_path_for_project_path(
background_executor: BackgroundExecutor,