diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index beb1ef61ef9886..ef15a27c6a5cd5 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -1979,6 +1979,7 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA delay_ms: None, min_column: None, show_commit_summary: false, + author_display: None, }); cx_a.update(|cx| { SettingsStore::update_global(cx, |store, cx| { diff --git a/crates/git/src/blame.rs b/crates/git/src/blame.rs index 8f87a8ca546ee8..0f11a7e51f65ea 100644 --- a/crates/git/src/blame.rs +++ b/crates/git/src/blame.rs @@ -31,11 +31,22 @@ impl Blame { content: &Rope, remote_url: Option, provider_registry: Arc, + author_display_replace: Option<&str>, ) -> Result { let output = run_git_blame(git_binary, working_directory, path, content)?; let mut entries = parse_git_blame(&output)?; entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start)); + if let Some(replacement) = author_display_replace { + if let Some(username) = get_git_user_name(git_binary, working_directory)? { + let new_author = replacement.replace("{}", &username); + entries + .iter_mut() + .filter(|e| e.author.as_deref() == Some(&username)) + .for_each(|e| e.author = Some(new_author.clone())); + } + } + let mut permalinks = HashMap::default(); let mut unique_shas = HashSet::default(); let parsed_remote_url = remote_url @@ -119,6 +130,21 @@ fn run_git_blame( Ok(String::from_utf8(output.stdout)?) } +fn get_git_user_name(git_binary: &Path, working_directory: &Path) -> Result> { + let output = std::process::Command::new(git_binary) + .current_dir(working_directory) + .arg("config") + .arg("user.name") + .output()?; + + if output.status.success() { + let name = String::from_utf8(output.stdout)?; + Ok(Some(name.trim().to_string())) + } else { + Ok(None) + } +} + #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] pub struct BlameEntry { pub sha: Oid, diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 78f6ece508b937..6d1d3bc59c9c87 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -44,7 +44,12 @@ pub trait GitRepository: Send + Sync { fn create_branch(&self, _: &str) -> Result<()>; fn branch_exits(&self, _: &str) -> Result; - fn blame(&self, path: &Path, content: Rope) -> Result; + fn blame( + &self, + path: &Path, + content: Rope, + author_display_replace: Option<&str>, + ) -> Result; fn path(&self) -> PathBuf; } @@ -204,7 +209,12 @@ impl GitRepository for RealGitRepository { Ok(()) } - fn blame(&self, path: &Path, content: Rope) -> Result { + fn blame( + &self, + path: &Path, + content: Rope, + author_display_replace: Option<&str>, + ) -> Result { let working_directory = self .repository .lock() @@ -222,6 +232,7 @@ impl GitRepository for RealGitRepository { &content, remote_url, self.hosting_provider_registry.clone(), + author_display_replace, ) } } @@ -349,7 +360,12 @@ impl GitRepository for FakeGitRepository { Ok(()) } - fn blame(&self, path: &Path, _content: Rope) -> Result { + fn blame( + &self, + path: &Path, + _content: Rope, + _author_replace_display: Option<&str>, + ) -> Result { let state = self.state.lock(); state .blames diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 7a54f7cc4711ff..2100b0e6e17b57 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -1,7 +1,8 @@ use crate::{ + project_settings::GitAuthorDisplaySetting, search::SearchQuery, worktree_store::{WorktreeStore, WorktreeStoreEvent}, - ProjectItem as _, ProjectPath, + ProjectItem as _, ProjectPath, ProjectSettings, }; use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry}; use anyhow::{anyhow, Context as _, Result}; @@ -23,6 +24,7 @@ use language::{ Buffer, BufferEvent, Capability, DiskState, File as _, Language, Operation, }; use rpc::{proto, AnyProtoClient, ErrorExt as _, TypedEnvelope}; +use settings::Settings; use smol::channel::Receiver; use std::{io, ops::Range, path::Path, str::FromStr as _, sync::Arc, time::Instant}; use text::BufferId; @@ -1063,11 +1065,25 @@ impl BufferStore { anyhow::Ok(Some((repo, relative_path, content))) }); + let author_display_setting = ProjectSettings::get_global(cx) + .git + .inline_blame + .unwrap_or_default() + .author_display + .unwrap_or_default(); + cx.background_executor().spawn(async move { let Some((repo, relative_path, content)) = blame_params? else { return Ok(None); }; - repo.blame(&relative_path, content) + + let author_display_replace = match author_display_setting { + GitAuthorDisplaySetting::Author => None, + GitAuthorDisplaySetting::You => Some("You"), + GitAuthorDisplaySetting::AuthorYou => Some("{} (You)"), + }; + + repo.blame(&relative_path, content, author_display_replace) .with_context(|| format!("Failed to blame {:?}", relative_path.0)) .map(Some) }) diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index bd0d7cc884f95e..f5579ad9016941 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -156,6 +156,22 @@ pub struct InlineBlameSettings { /// Default: false #[serde(default = "false_value")] pub show_commit_summary: bool, + /// Author display format for git blame. + /// + /// Default: author + pub author_display: Option, +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum GitAuthorDisplaySetting { + /// Only Author name. + #[default] + Author, + /// You + You, + /// Author (You) + AuthorYou, } const fn true_value() -> bool {