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:
co-authored by
Cole Miller
parent
f11a3dcc97
commit
f45a9b351d
@@ -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
@@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user