Skip to content

Commit 752f0a8

Browse files
committed
Add blame view
This closes gitui-org#484.
1 parent 524add8 commit 752f0a8

File tree

10 files changed

+520
-47
lines changed

10 files changed

+520
-47
lines changed

asyncgit/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ pub enum AsyncNotification {
7777
Fetch,
7878
}
7979

80-
/// current working director `./`
80+
/// current working directory `./`
8181
pub static CWD: &str = "./";
8282

8383
/// helper function to calculate the hash of an arbitrary type that implements the `Hash` trait

asyncgit/src/sync/blame.rs

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
//! Sync git API for fetching a file blame
2+
3+
use super::{utils, CommitId};
4+
use crate::{error::Result, sync::get_commit_info, CWD};
5+
use std::io::{BufRead, BufReader};
6+
use std::path::Path;
7+
8+
/// A `BlameHunk` contains all the information that will be shown to the user.
9+
#[derive(Clone, Hash, Debug, PartialEq, Eq)]
10+
pub struct BlameHunk {
11+
///
12+
pub commit_id: CommitId,
13+
///
14+
pub author: String,
15+
///
16+
pub time: i64,
17+
/// `git2::BlameHunk::final_start_line` returns 1-based indices, but
18+
/// `start_line` is 0-based because the `Vec` storing the lines starts at
19+
/// index 0.
20+
pub start_line: usize,
21+
///
22+
pub end_line: usize,
23+
}
24+
25+
/// A `BlameFile` represents as a collection of hunks. This resembles `git2`’s
26+
/// API.
27+
#[derive(Default, Clone, Debug)]
28+
pub struct FileBlame {
29+
///
30+
pub path: String,
31+
///
32+
pub lines: Vec<(Option<BlameHunk>, String)>,
33+
}
34+
35+
///
36+
pub fn blame_file(file_path: &str) -> Result<FileBlame> {
37+
let repo_path = CWD;
38+
let repo = utils::repo(repo_path)?;
39+
40+
// https://github.com/rust-lang/git2-rs/blob/7912c90991444abb00f9d0476939d48bc368516b/examples/blame.rs
41+
let commit_id = "HEAD";
42+
let spec = format!("{}:{}", commit_id, file_path);
43+
let blame = repo.blame_file(Path::new(file_path), None)?;
44+
let object = repo.revparse_single(&spec[..])?;
45+
let blob = repo.find_blob(object.id())?;
46+
let reader = BufReader::new(blob.content());
47+
48+
let lines: Vec<(Option<BlameHunk>, String)> = reader
49+
.lines()
50+
.enumerate()
51+
.map(|(i, line)| {
52+
// Line indices in a `FileBlame` are 1-based.
53+
let corresponding_hunk = blame.get_line(i + 1);
54+
55+
if let Some(hunk) = corresponding_hunk {
56+
let commit_id = CommitId::new(hunk.final_commit_id());
57+
// Line indices in a `BlameHunk` are 1-based.
58+
let start_line = hunk.final_start_line() - 1;
59+
let end_line = start_line + hunk.lines_in_hunk();
60+
61+
if let Ok(commit_info) =
62+
get_commit_info(repo_path, &commit_id)
63+
{
64+
let hunk = BlameHunk {
65+
commit_id,
66+
author: commit_info.author.clone(),
67+
time: commit_info.time,
68+
start_line,
69+
end_line,
70+
};
71+
72+
return (
73+
Some(hunk),
74+
line.unwrap_or_else(|_| "".into()),
75+
);
76+
}
77+
}
78+
79+
(None, line.unwrap_or_else(|_| "".into()))
80+
})
81+
.collect();
82+
83+
let file_blame = FileBlame {
84+
path: file_path.into(),
85+
lines,
86+
};
87+
88+
Ok(file_blame)
89+
}

asyncgit/src/sync/commits_info.rs

+20
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,26 @@ pub fn get_commits_info(
9595
Ok(res)
9696
}
9797

98+
///
99+
pub fn get_commit_info(
100+
repo_path: &str,
101+
commit_id: &CommitId,
102+
) -> Result<CommitInfo> {
103+
scope_time!("get_commit_info");
104+
105+
let repo = repo(repo_path)?;
106+
107+
let commit = repo.find_commit((*commit_id).into())?;
108+
let author = commit.author();
109+
110+
Ok(CommitInfo {
111+
message: commit.message().unwrap_or("").into(),
112+
author: author.name().unwrap_or("<unknown>").into(),
113+
time: commit.time().seconds(),
114+
id: CommitId(commit.id()),
115+
})
116+
}
117+
98118
///
99119
pub fn get_message(
100120
c: &Commit,

asyncgit/src/sync/mod.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//TODO: remove once we have this activated on the toplevel
44
#![deny(clippy::expect_used)]
55

6+
pub mod blame;
67
pub mod branch;
78
mod commit;
89
mod commit_details;
@@ -24,6 +25,7 @@ pub mod status;
2425
mod tags;
2526
pub mod utils;
2627

28+
pub use blame::{blame_file, BlameHunk, FileBlame};
2729
pub use branch::{
2830
branch_compare_upstream, checkout_branch, config_is_pull_rebase,
2931
create_branch, delete_branch, get_branch_remote,
@@ -37,7 +39,9 @@ pub use commit_details::{
3739
get_commit_details, CommitDetails, CommitMessage,
3840
};
3941
pub use commit_files::get_commit_files;
40-
pub use commits_info::{get_commits_info, CommitId, CommitInfo};
42+
pub use commits_info::{
43+
get_commit_info, get_commits_info, CommitId, CommitInfo,
44+
};
4145
pub use diff::get_diff_commit;
4246
pub use hooks::{
4347
hooks_commit_msg, hooks_post_commit, hooks_pre_commit, HookResult,

src/app.rs

+74-44
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use crate::{
22
accessors,
33
cmdbar::CommandBar,
44
components::{
5-
event_pump, BranchListComponent, CommandBlocking,
6-
CommandInfo, CommitComponent, Component,
5+
event_pump, BlameFileComponent, BranchListComponent,
6+
CommandBlocking, CommandInfo, CommitComponent, Component,
77
CreateBranchComponent, DrawableComponent,
88
ExternalEditorComponent, HelpComponent,
99
InspectCommitComponent, MsgComponent, PullComponent,
@@ -41,6 +41,7 @@ pub struct App {
4141
msg: MsgComponent,
4242
reset: ResetComponent,
4343
commit: CommitComponent,
44+
blame_file_popup: BlameFileComponent,
4445
stashmsg_popup: StashMsgComponent,
4546
inspect_commit_popup: InspectCommitComponent,
4647
external_editor_popup: ExternalEditorComponent,
@@ -93,6 +94,11 @@ impl App {
9394
theme.clone(),
9495
key_config.clone(),
9596
),
97+
blame_file_popup: BlameFileComponent::new(
98+
&strings::blame_title(&key_config),
99+
theme.clone(),
100+
key_config.clone(),
101+
),
96102
stashmsg_popup: StashMsgComponent::new(
97103
queue.clone(),
98104
theme.clone(),
@@ -363,6 +369,7 @@ impl App {
363369
msg,
364370
reset,
365371
commit,
372+
blame_file_popup,
366373
stashmsg_popup,
367374
inspect_commit_popup,
368375
external_editor_popup,
@@ -488,48 +495,9 @@ impl App {
488495
) -> Result<NeedsUpdate> {
489496
let mut flags = NeedsUpdate::empty();
490497
match ev {
491-
InternalEvent::ConfirmedAction(action) => match action {
492-
Action::Reset(r) => {
493-
if self.status_tab.reset(&r) {
494-
flags.insert(NeedsUpdate::ALL);
495-
}
496-
}
497-
Action::StashDrop(_) | Action::StashPop(_) => {
498-
if self.stashlist_tab.action_confirmed(&action) {
499-
flags.insert(NeedsUpdate::ALL);
500-
}
501-
}
502-
Action::ResetHunk(path, hash) => {
503-
sync::reset_hunk(CWD, &path, hash)?;
504-
flags.insert(NeedsUpdate::ALL);
505-
}
506-
Action::ResetLines(path, lines) => {
507-
sync::discard_lines(CWD, &path, &lines)?;
508-
flags.insert(NeedsUpdate::ALL);
509-
}
510-
Action::DeleteBranch(branch_ref) => {
511-
if let Err(e) =
512-
sync::delete_branch(CWD, &branch_ref)
513-
{
514-
self.queue.borrow_mut().push_back(
515-
InternalEvent::ShowErrorMsg(
516-
e.to_string(),
517-
),
518-
)
519-
} else {
520-
flags.insert(NeedsUpdate::ALL);
521-
self.select_branch_popup.update_branches()?;
522-
}
523-
}
524-
Action::ForcePush(branch, force) => self
525-
.queue
526-
.borrow_mut()
527-
.push_back(InternalEvent::Push(branch, force)),
528-
Action::PullMerge { rebase, .. } => {
529-
self.pull_popup.try_conflict_free_merge(rebase);
530-
flags.insert(NeedsUpdate::ALL);
531-
}
532-
},
498+
InternalEvent::ConfirmedAction(action) => {
499+
self.process_confirmed_action(action, &mut flags)?;
500+
}
533501
InternalEvent::ConfirmAction(action) => {
534502
self.reset.open(action)?;
535503
flags.insert(NeedsUpdate::COMMANDS);
@@ -548,6 +516,14 @@ impl App {
548516
InternalEvent::TagCommit(id) => {
549517
self.tag_commit_popup.open(id)?;
550518
}
519+
InternalEvent::BlameFile(path) => {
520+
// Having to explicitly close another popup before showing a new
521+
// one is not an optimal solution because this constraint can’t
522+
// be automatically enforced.
523+
self.inspect_commit_popup.hide();
524+
self.blame_file_popup.open(&path)?;
525+
flags.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS)
526+
}
551527
InternalEvent::CreateBranch => {
552528
self.create_branch_popup.open()?;
553529
}
@@ -560,6 +536,10 @@ impl App {
560536
}
561537
InternalEvent::TabSwitch => self.set_tab(0)?,
562538
InternalEvent::InspectCommit(id, tags) => {
539+
// Having to explicitly close another popup before showing a new
540+
// one is not an optimal solution because this constraint can’t
541+
// be automatically enforced.
542+
self.blame_file_popup.hide();
563543
self.inspect_commit_popup.open(id, tags)?;
564544
flags.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS)
565545
}
@@ -586,6 +566,54 @@ impl App {
586566
Ok(flags)
587567
}
588568

569+
fn process_confirmed_action(
570+
&mut self,
571+
action: Action,
572+
flags: &mut NeedsUpdate,
573+
) -> Result<()> {
574+
match action {
575+
Action::Reset(r) => {
576+
if self.status_tab.reset(&r) {
577+
flags.insert(NeedsUpdate::ALL);
578+
}
579+
}
580+
Action::StashDrop(_) | Action::StashPop(_) => {
581+
if self.stashlist_tab.action_confirmed(&action) {
582+
flags.insert(NeedsUpdate::ALL);
583+
}
584+
}
585+
Action::ResetHunk(path, hash) => {
586+
sync::reset_hunk(CWD, &path, hash)?;
587+
flags.insert(NeedsUpdate::ALL);
588+
}
589+
Action::ResetLines(path, lines) => {
590+
sync::discard_lines(CWD, &path, &lines)?;
591+
flags.insert(NeedsUpdate::ALL);
592+
}
593+
Action::DeleteBranch(branch_ref) => {
594+
if let Err(e) = sync::delete_branch(CWD, &branch_ref)
595+
{
596+
self.queue.borrow_mut().push_back(
597+
InternalEvent::ShowErrorMsg(e.to_string()),
598+
)
599+
} else {
600+
flags.insert(NeedsUpdate::ALL);
601+
self.select_branch_popup.update_branches()?;
602+
}
603+
}
604+
Action::ForcePush(branch, force) => self
605+
.queue
606+
.borrow_mut()
607+
.push_back(InternalEvent::Push(branch, force)),
608+
Action::PullMerge { rebase, .. } => {
609+
self.pull_popup.try_conflict_free_merge(rebase);
610+
flags.insert(NeedsUpdate::ALL);
611+
}
612+
};
613+
614+
Ok(())
615+
}
616+
589617
fn commands(&self, force_all: bool) -> Vec<CommandInfo> {
590618
let mut res = Vec::new();
591619

@@ -637,6 +665,7 @@ impl App {
637665
|| self.msg.is_visible()
638666
|| self.stashmsg_popup.is_visible()
639667
|| self.inspect_commit_popup.is_visible()
668+
|| self.blame_file_popup.is_visible()
640669
|| self.external_editor_popup.is_visible()
641670
|| self.tag_commit_popup.is_visible()
642671
|| self.create_branch_popup.is_visible()
@@ -666,6 +695,7 @@ impl App {
666695
self.stashmsg_popup.draw(f, size)?;
667696
self.help.draw(f, size)?;
668697
self.inspect_commit_popup.draw(f, size)?;
698+
self.blame_file_popup.draw(f, size)?;
669699
self.external_editor_popup.draw(f, size)?;
670700
self.tag_commit_popup.draw(f, size)?;
671701
self.select_branch_popup.draw(f, size)?;

0 commit comments

Comments
 (0)