Add git blame
(#8889)
This adds a new action to the editor: `editor: toggle git blame`. When used it turns on a sidebar containing `git blame` information for the currently open buffer. The git blame information is updated when the buffer changes. It handles additions, deletions, modifications, changes to the underlying git data (new commits, changed commits, ...), file saves. It also handles folding and wrapping lines correctly. When the user hovers over a commit, a tooltip displays information for the commit that introduced the line. If the repository has a remote with the name `origin` configured, then clicking on a blame entry opens the permalink to the commit on the code host. Users can right-click on a blame entry to get a context menu which allows them to copy the SHA of the commit. The feature also works on shared projects, e.g. when collaborating a peer can request `git blame` data. As of this PR, Zed now comes bundled with a `git` binary so that users don't have to have `git` installed locally to use this feature. ### Screenshots    ### TODOs - [x] Bundling `git` binary ### Release Notes Release Notes: - Added `editor: toggle git blame` command that toggles a sidebar with git blame information for the current buffer. --------- Co-authored-by: Antonio <antonio@zed.dev> Co-authored-by: Piotr <piotr@zed.dev> Co-authored-by: Bennet <bennetbo@gmx.de> Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
parent
e2d6b0deba
commit
7f54935324
39 changed files with 3760 additions and 157 deletions
|
@ -366,6 +366,7 @@ impl Server {
|
|||
.add_request_handler(forward_mutating_project_request::<proto::ExpandProjectEntry>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::OnTypeFormatting>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::SaveBuffer>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::BlameBuffer>)
|
||||
.add_message_handler(create_buffer_for_peer)
|
||||
.add_request_handler(update_buffer)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::RefreshInlayHints>)
|
||||
|
|
|
@ -23,6 +23,7 @@ use rpc::RECEIVE_TIMEOUT;
|
|||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
ops::Range,
|
||||
path::Path,
|
||||
sync::{
|
||||
atomic::{self, AtomicBool, AtomicUsize},
|
||||
|
@ -1986,6 +1987,187 @@ struct Row10;"#};
|
|||
struct Row1220;"#});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let mut server = TestServer::start(cx_a.executor()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/my-repo",
|
||||
json!({
|
||||
".git": {},
|
||||
"file.txt": "line1\nline2\nline3\nline\n",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let blame = git::blame::Blame {
|
||||
entries: vec![
|
||||
blame_entry("1b1b1b", 0..1),
|
||||
blame_entry("0d0d0d", 1..2),
|
||||
blame_entry("3a3a3a", 2..3),
|
||||
blame_entry("4c4c4c", 3..4),
|
||||
],
|
||||
permalinks: [
|
||||
("1b1b1b", "http://example.com/codehost/idx-0"),
|
||||
("0d0d0d", "http://example.com/codehost/idx-1"),
|
||||
("3a3a3a", "http://example.com/codehost/idx-2"),
|
||||
("4c4c4c", "http://example.com/codehost/idx-3"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(sha, url)| (sha.parse().unwrap(), url.parse().unwrap()))
|
||||
.collect(),
|
||||
messages: [
|
||||
("1b1b1b", "message for idx-0"),
|
||||
("0d0d0d", "message for idx-1"),
|
||||
("3a3a3a", "message for idx-2"),
|
||||
("4c4c4c", "message for idx-3"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(sha, message)| (sha.parse().unwrap(), message.into()))
|
||||
.collect(),
|
||||
};
|
||||
client_a.fs().set_blame_for_repo(
|
||||
Path::new("/my-repo/.git"),
|
||||
vec![(Path::new("file.txt"), blame)],
|
||||
);
|
||||
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/my-repo", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create editor_a
|
||||
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
let editor_a = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "file.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
// Join the project as client B.
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "file.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
// client_b now requests git blame for the open buffer
|
||||
editor_b.update(cx_b, |editor_b, cx| {
|
||||
assert!(editor_b.blame().is_none());
|
||||
editor_b.toggle_git_blame(&editor::actions::ToggleGitBlame {}, cx);
|
||||
});
|
||||
|
||||
cx_a.executor().run_until_parked();
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
editor_b.update(cx_b, |editor_b, cx| {
|
||||
let blame = editor_b.blame().expect("editor_b should have blame now");
|
||||
let entries = blame.update(cx, |blame, cx| {
|
||||
blame
|
||||
.blame_for_rows((0..4).map(Some), cx)
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
entries,
|
||||
vec![
|
||||
Some(blame_entry("1b1b1b", 0..1)),
|
||||
Some(blame_entry("0d0d0d", 1..2)),
|
||||
Some(blame_entry("3a3a3a", 2..3)),
|
||||
Some(blame_entry("4c4c4c", 3..4)),
|
||||
]
|
||||
);
|
||||
|
||||
blame.update(cx, |blame, _| {
|
||||
for (idx, entry) in entries.iter().flatten().enumerate() {
|
||||
assert_eq!(
|
||||
blame.permalink_for_entry(entry).unwrap().to_string(),
|
||||
format!("http://example.com/codehost/idx-{}", idx)
|
||||
);
|
||||
assert_eq!(
|
||||
blame.message_for_entry(entry).unwrap(),
|
||||
format!("message for idx-{}", idx)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// editor_b updates the file, which gets sent to client_a, which updates git blame,
|
||||
// which gets back to client_b.
|
||||
editor_b.update(cx_b, |editor_b, cx| {
|
||||
editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
|
||||
});
|
||||
|
||||
cx_a.executor().run_until_parked();
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
editor_b.update(cx_b, |editor_b, cx| {
|
||||
let blame = editor_b.blame().expect("editor_b should have blame now");
|
||||
let entries = blame.update(cx, |blame, cx| {
|
||||
blame
|
||||
.blame_for_rows((0..4).map(Some), cx)
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
entries,
|
||||
vec![
|
||||
None,
|
||||
Some(blame_entry("0d0d0d", 1..2)),
|
||||
Some(blame_entry("3a3a3a", 2..3)),
|
||||
Some(blame_entry("4c4c4c", 3..4)),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Now editor_a also updates the file
|
||||
editor_a.update(cx_a, |editor_a, cx| {
|
||||
editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
|
||||
});
|
||||
|
||||
cx_a.executor().run_until_parked();
|
||||
cx_b.executor().run_until_parked();
|
||||
|
||||
editor_b.update(cx_b, |editor_b, cx| {
|
||||
let blame = editor_b.blame().expect("editor_b should have blame now");
|
||||
let entries = blame.update(cx, |blame, cx| {
|
||||
blame
|
||||
.blame_for_rows((0..4).map(Some), cx)
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
entries,
|
||||
vec![
|
||||
None,
|
||||
None,
|
||||
Some(blame_entry("3a3a3a", 2..3)),
|
||||
Some(blame_entry("4c4c4c", 3..4)),
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn extract_hint_labels(editor: &Editor) -> Vec<String> {
|
||||
let mut labels = Vec::new();
|
||||
for hint in editor.inlay_hint_cache().hints() {
|
||||
|
@ -1996,3 +2178,11 @@ fn extract_hint_labels(editor: &Editor) -> Vec<String> {
|
|||
}
|
||||
labels
|
||||
}
|
||||
|
||||
fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
|
||||
git::blame::BlameEntry {
|
||||
sha: sha.parse().unwrap(),
|
||||
range,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue