git: Branch diff (#40188)

Release Notes:

- git: Adds the ability to view the diff of the current branch since
main

---------

Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
Conrad Irwin
2025-10-23 22:38:40 -06:00
committed by GitHub
co-authored by Cole Miller
parent f11a3dcc97
commit f45a9b351d
18 changed files with 1362 additions and 777 deletions
+74 -7
View File
@@ -1,6 +1,6 @@
use crate::commit::parse_git_diff_name_status;
use crate::stash::GitStash;
use crate::status::{GitStatus, StatusCode};
use crate::status::{DiffTreeType, GitStatus, StatusCode, TreeDiff};
use crate::{Oid, SHORT_SHA_LENGTH};
use anyhow::{Context as _, Result, anyhow, bail};
use collections::HashMap;
@@ -350,6 +350,7 @@ pub trait GitRepository: Send + Sync {
///
/// Also returns `None` for symlinks.
fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>>;
fn load_blob_content(&self, oid: Oid) -> BoxFuture<'_, Result<String>>;
fn set_index_text(
&self,
@@ -379,6 +380,7 @@ pub trait GitRepository: Send + Sync {
fn merge_message(&self) -> BoxFuture<'_, Option<String>>;
fn status(&self, path_prefixes: &[RepoPath]) -> Task<Result<GitStatus>>;
fn diff_tree(&self, request: DiffTreeType) -> BoxFuture<'_, Result<TreeDiff>>;
fn stash_entries(&self) -> BoxFuture<'_, Result<GitStash>>;
@@ -908,6 +910,17 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn load_blob_content(&self, oid: Oid) -> BoxFuture<'_, Result<String>> {
let repo = self.repository.clone();
self.executor
.spawn(async move {
let repo = repo.lock();
let content = repo.find_blob(oid.0)?.content().to_owned();
Ok(String::from_utf8(content)?)
})
.boxed()
}
fn set_index_text(
&self,
path: RepoPath,
@@ -1060,6 +1073,50 @@ impl GitRepository for RealGitRepository {
})
}
fn diff_tree(&self, request: DiffTreeType) -> BoxFuture<'_, Result<TreeDiff>> {
let git_binary_path = self.any_git_binary_path.clone();
let working_directory = match self.working_directory() {
Ok(working_directory) => working_directory,
Err(e) => return Task::ready(Err(e)).boxed(),
};
let mut args = vec![
OsString::from("--no-optional-locks"),
OsString::from("diff-tree"),
OsString::from("-r"),
OsString::from("-z"),
OsString::from("--no-renames"),
];
match request {
DiffTreeType::MergeBase { base, head } => {
args.push("--merge-base".into());
args.push(OsString::from(base.as_str()));
args.push(OsString::from(head.as_str()));
}
DiffTreeType::Since { base, head } => {
args.push(OsString::from(base.as_str()));
args.push(OsString::from(head.as_str()));
}
}
self.executor
.spawn(async move {
let output = new_smol_command(&git_binary_path)
.current_dir(working_directory)
.args(args)
.output()
.await?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
stdout.parse()
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("git status failed: {stderr}");
}
})
.boxed()
}
fn stash_entries(&self) -> BoxFuture<'_, Result<GitStash>> {
let git_binary_path = self.any_git_binary_path.clone();
let working_directory = self.working_directory();
@@ -1827,13 +1884,23 @@ impl GitRepository for RealGitRepository {
return Ok(output);
}
let output = git
.run(&["symbolic-ref", "refs/remotes/origin/HEAD"])
.await?;
if let Ok(output) = git.run(&["symbolic-ref", "refs/remotes/origin/HEAD"]).await {
return Ok(output
.strip_prefix("refs/remotes/origin/")
.map(|s| SharedString::from(s.to_owned())));
}
Ok(output
.strip_prefix("refs/remotes/origin/")
.map(|s| SharedString::from(s.to_owned())))
if let Ok(default_branch) = git.run(&["config", "init.defaultBranch"]).await {
if git.run(&["rev-parse", &default_branch]).await.is_ok() {
return Ok(Some(default_branch.into()));
}
}
if git.run(&["rev-parse", "master"]).await.is_ok() {
return Ok(Some("master".into()));
}
Ok(None)
})
.boxed()
}
+134 -3
View File
@@ -1,5 +1,7 @@
use crate::repository::RepoPath;
use anyhow::Result;
use crate::{Oid, repository::RepoPath};
use anyhow::{Result, anyhow};
use collections::HashMap;
use gpui::SharedString;
use serde::{Deserialize, Serialize};
use std::{str::FromStr, sync::Arc};
use util::{ResultExt, rel_path::RelPath};
@@ -190,7 +192,11 @@ impl FileStatus {
}
pub fn is_deleted(self) -> bool {
matches!(self, FileStatus::Tracked(tracked) if matches!((tracked.index_status, tracked.worktree_status), (StatusCode::Deleted, _) | (_, StatusCode::Deleted)))
let FileStatus::Tracked(tracked) = self else {
return false;
};
tracked.index_status == StatusCode::Deleted && tracked.worktree_status != StatusCode::Added
|| tracked.worktree_status == StatusCode::Deleted
}
pub fn is_untracked(self) -> bool {
@@ -486,3 +492,128 @@ impl Default for GitStatus {
}
}
}
pub enum DiffTreeType {
MergeBase {
base: SharedString,
head: SharedString,
},
Since {
base: SharedString,
head: SharedString,
},
}
impl DiffTreeType {
pub fn base(&self) -> &SharedString {
match self {
DiffTreeType::MergeBase { base, .. } => base,
DiffTreeType::Since { base, .. } => base,
}
}
pub fn head(&self) -> &SharedString {
match self {
DiffTreeType::MergeBase { head, .. } => head,
DiffTreeType::Since { head, .. } => head,
}
}
}
#[derive(Debug, PartialEq)]
pub struct TreeDiff {
pub entries: HashMap<RepoPath, TreeDiffStatus>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TreeDiffStatus {
Added,
Modified { old: Oid },
Deleted { old: Oid },
}
impl FromStr for TreeDiff {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
let mut fields = s.split('\0');
let mut parsed = HashMap::default();
while let Some((status, path)) = fields.next().zip(fields.next()) {
let path = RepoPath(RelPath::unix(path)?.into());
let mut fields = status.split(" ").skip(2);
let old_sha = fields
.next()
.ok_or_else(|| anyhow!("expected to find old_sha"))?
.to_owned()
.parse()?;
let _new_sha = fields
.next()
.ok_or_else(|| anyhow!("expected to find new_sha"))?;
let status = fields
.next()
.and_then(|s| {
if s.len() == 1 {
s.as_bytes().first()
} else {
None
}
})
.ok_or_else(|| anyhow!("expected to find status"))?;
let result = match StatusCode::from_byte(*status)? {
StatusCode::Modified => TreeDiffStatus::Modified { old: old_sha },
StatusCode::Added => TreeDiffStatus::Added,
StatusCode::Deleted => TreeDiffStatus::Deleted { old: old_sha },
_status => continue,
};
parsed.insert(path, result);
}
Ok(Self { entries: parsed })
}
}
#[cfg(test)]
mod tests {
use crate::{
repository::RepoPath,
status::{TreeDiff, TreeDiffStatus},
};
#[test]
fn test_tree_diff_parsing() {
let input = ":000000 100644 0000000000000000000000000000000000000000 0062c311b8727c3a2e3cd7a41bc9904feacf8f98 A\x00.zed/settings.json\x00".to_owned() +
":100644 000000 bb3e9ed2e97a8c02545bae243264d342c069afb3 0000000000000000000000000000000000000000 D\x00README.md\x00" +
":100644 100644 42f097005a1f21eb2260fad02ec8c991282beee8 a437d85f63bb8c62bd78f83f40c506631fabf005 M\x00parallel.go\x00";
let output: TreeDiff = input.parse().unwrap();
assert_eq!(
output,
TreeDiff {
entries: [
(
RepoPath::new(".zed/settings.json").unwrap(),
TreeDiffStatus::Added,
),
(
RepoPath::new("README.md").unwrap(),
TreeDiffStatus::Deleted {
old: "bb3e9ed2e97a8c02545bae243264d342c069afb3".parse().unwrap()
}
),
(
RepoPath::new("parallel.go").unwrap(),
TreeDiffStatus::Modified {
old: "42f097005a1f21eb2260fad02ec8c991282beee8".parse().unwrap(),
}
),
]
.into_iter()
.collect()
}
)
}
}