diff --git a/CHANGELOG.md b/CHANGELOG.md index 2760cf6d..6d5165db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +**compare commits** + +![compare](assets/compare.gif) + **options** ![options](assets/options.gif) @@ -20,7 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ![name-validation](assets/branch-validation.gif) ## Added -- allow opening top commit of a branch +- allow inspecting top commit of a branch from list +- compare commits in revlog and head against branch ([#852](https://github.com/extrawurst/gitui/issues/852)) - new options popup (show untracked files, diff settings) ([#849](https://github.com/extrawurst/gitui/issues/849)) - mark and drop multiple stashes ([#854](https://github.com/extrawurst/gitui/issues/854)) - check branch name validity while typing ([#559](https://github.com/extrawurst/gitui/issues/559)) diff --git a/assets/compare.gif b/assets/compare.gif new file mode 100644 index 00000000..40150339 Binary files /dev/null and b/assets/compare.gif differ diff --git a/asyncgit/src/commit_files.rs b/asyncgit/src/commit_files.rs index 1bc61725..23235924 100644 --- a/asyncgit/src/commit_files.rs +++ b/asyncgit/src/commit_files.rs @@ -12,9 +12,34 @@ use std::sync::{ type ResultType = Vec; struct Request(R, A); +/// +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct CommitFilesParams { + /// + pub id: CommitId, + /// + pub other: Option, +} + +impl From for CommitFilesParams { + fn from(id: CommitId) -> Self { + Self { id, other: None } + } +} + +impl From<(CommitId, CommitId)> for CommitFilesParams { + fn from((id, other): (CommitId, CommitId)) -> Self { + Self { + id, + other: Some(other), + } + } +} + /// pub struct AsyncCommitFiles { - current: Arc>>>, + current: + Arc>>>, sender: Sender, pending: Arc, } @@ -32,7 +57,7 @@ impl AsyncCommitFiles { /// pub fn current( &mut self, - ) -> Result> { + ) -> Result> { let c = self.current.lock()?; c.as_ref() @@ -45,17 +70,17 @@ impl AsyncCommitFiles { } /// - pub fn fetch(&mut self, id: CommitId) -> Result<()> { + pub fn fetch(&mut self, params: CommitFilesParams) -> Result<()> { if self.is_pending() { return Ok(()); } - log::trace!("request: {}", id.to_string()); + log::trace!("request: {:?}", params); { let current = self.current.lock()?; if let Some(c) = &*current { - if c.0 == id { + if c.0 == params { return Ok(()); } } @@ -68,7 +93,7 @@ impl AsyncCommitFiles { self.pending.fetch_add(1, Ordering::Relaxed); rayon_core::spawn(move || { - Self::fetch_helper(id, &arc_current) + Self::fetch_helper(params, &arc_current) .expect("failed to fetch"); arc_pending.fetch_sub(1, Ordering::Relaxed); @@ -82,22 +107,19 @@ impl AsyncCommitFiles { } fn fetch_helper( - id: CommitId, + params: CommitFilesParams, arc_current: &Arc< - Mutex>>, + Mutex>>, >, ) -> Result<()> { - let res = sync::get_commit_files(CWD, id)?; + let res = + sync::get_commit_files(CWD, params.id, params.other)?; - log::trace!( - "get_commit_files: {} ({})", - id.to_string(), - res.len() - ); + log::trace!("get_commit_files: {:?} ({})", params, res.len()); { let mut current = arc_current.lock()?; - *current = Some(Request(id, res)); + *current = Some(Request(params, res)); } Ok(()) diff --git a/asyncgit/src/diff.rs b/asyncgit/src/diff.rs index 58a506e8..45ec3408 100644 --- a/asyncgit/src/diff.rs +++ b/asyncgit/src/diff.rs @@ -16,6 +16,8 @@ use std::{ /// #[derive(Debug, Hash, Clone, PartialEq)] pub enum DiffType { + /// diff two commits + Commits((CommitId, CommitId)), /// diff in a given commit Commit(CommitId), /// diff against staged file @@ -167,6 +169,11 @@ impl AsyncDiff { id, params.path.clone(), )?, + DiffType::Commits(ids) => sync::diff::get_diff_commits( + CWD, + ids, + params.path.clone(), + )?, }; let mut notify = false; diff --git a/asyncgit/src/lib.rs b/asyncgit/src/lib.rs index c77e9548..6c356e2f 100644 --- a/asyncgit/src/lib.rs +++ b/asyncgit/src/lib.rs @@ -41,7 +41,7 @@ mod tags; pub use crate::{ blame::{AsyncBlame, BlameParams}, - commit_files::AsyncCommitFiles, + commit_files::{AsyncCommitFiles, CommitFilesParams}, diff::{AsyncDiff, DiffParams, DiffType}, fetch::{AsyncFetch, FetchRequest}, push::{AsyncPush, PushRequest}, diff --git a/asyncgit/src/sync/commit.rs b/asyncgit/src/sync/commit.rs index 0d5f7847..86a9b529 100644 --- a/asyncgit/src/sync/commit.rs +++ b/asyncgit/src/sync/commit.rs @@ -205,7 +205,7 @@ mod tests { let details = get_commit_details(repo_path, new_id)?; assert_eq!(details.message.unwrap().subject, "amended"); - let files = get_commit_files(repo_path, new_id)?; + let files = get_commit_files(repo_path, new_id, None)?; assert_eq!(files.len(), 2); diff --git a/asyncgit/src/sync/commit_files.rs b/asyncgit/src/sync/commit_files.rs index ee2bb062..6015d843 100644 --- a/asyncgit/src/sync/commit_files.rs +++ b/asyncgit/src/sync/commit_files.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + use super::{stash::is_stash_commit, utils::repo, CommitId}; use crate::{ error::Error, error::Result, StatusItem, StatusItemType, @@ -9,12 +11,17 @@ use scopetime::scope_time; pub fn get_commit_files( repo_path: &str, id: CommitId, + other: Option, ) -> Result> { scope_time!("get_commit_files"); let repo = repo(repo_path)?; - let diff = get_commit_diff(&repo, id, None)?; + let diff = if let Some(other) = other { + get_compare_commits_diff(&repo, (id, other), None)? + } else { + get_commit_diff(&repo, id, None)? + }; let mut res = Vec::new(); @@ -38,6 +45,44 @@ pub fn get_commit_files( Ok(res) } +#[allow(clippy::needless_pass_by_value)] +pub fn get_compare_commits_diff( + repo: &Repository, + ids: (CommitId, CommitId), + pathspec: Option, +) -> Result> { + // scope_time!("get_compare_commits_diff"); + + let commits = ( + repo.find_commit(ids.0.into())?, + repo.find_commit(ids.1.into())?, + ); + + let commits = if commits.0.time().cmp(&commits.1.time()) + == Ordering::Greater + { + (commits.1, commits.0) + } else { + commits + }; + + let trees = (commits.0.tree()?, commits.1.tree()?); + + let mut opts = DiffOptions::new(); + if let Some(p) = &pathspec { + opts.pathspec(p.clone()); + } + opts.show_binary(true); + + let diff = repo.diff_tree_to_tree( + Some(&trees.0), + Some(&trees.1), + Some(&mut opts), + )?; + + Ok(diff) +} + #[allow(clippy::redundant_pub_crate)] pub(crate) fn get_commit_diff( repo: &Repository, @@ -48,6 +93,7 @@ pub(crate) fn get_commit_diff( let commit = repo.find_commit(id.into())?; let commit_tree = commit.tree()?; + let parent = if commit.parent_count() > 0 { repo.find_commit(commit.parent_id(0)?) .ok() @@ -116,7 +162,7 @@ mod tests { let id = commit(repo_path, "commit msg")?; - let diff = get_commit_files(repo_path, id)?; + let diff = get_commit_files(repo_path, id, None)?; assert_eq!(diff.len(), 1); assert_eq!(diff[0].status, StatusItemType::New); @@ -136,7 +182,7 @@ mod tests { let id = stash_save(repo_path, None, true, false)?; - let diff = get_commit_files(repo_path, id)?; + let diff = get_commit_files(repo_path, id, None)?; assert_eq!(diff.len(), 1); assert_eq!(diff[0].status, StatusItemType::New); @@ -164,7 +210,7 @@ mod tests { let id = stash_save(repo_path, None, true, false)?; - let diff = get_commit_files(repo_path, id)?; + let diff = get_commit_files(repo_path, id, None)?; assert_eq!(diff.len(), 2); assert_eq!(diff[0].status, StatusItemType::Modified); diff --git a/asyncgit/src/sync/diff.rs b/asyncgit/src/sync/diff.rs index 4b9b5952..17333727 100644 --- a/asyncgit/src/sync/diff.rs +++ b/asyncgit/src/sync/diff.rs @@ -1,7 +1,7 @@ //! sync git api for fetching a diff use super::{ - commit_files::get_commit_diff, + commit_files::{get_commit_diff, get_compare_commits_diff}, utils::{self, get_head_repo, work_dir}, CommitId, }; @@ -222,6 +222,22 @@ pub fn get_diff_commit( raw_diff_to_file_diff(&diff, work_dir) } +/// get file changes of a diff between two commits +pub fn get_diff_commits( + repo_path: &str, + ids: (CommitId, CommitId), + p: String, +) -> Result { + scope_time!("get_diff_commits"); + + let repo = utils::repo(repo_path)?; + let work_dir = work_dir(&repo)?; + let diff = + get_compare_commits_diff(&repo, (ids.0, ids.1), Some(p))?; + + raw_diff_to_file_diff(&diff, work_dir) +} + /// //TODO: refactor into helper type with the inline closures as dedicated functions #[allow(clippy::too_many_lines)] diff --git a/asyncgit/src/sync/remotes/push.rs b/asyncgit/src/sync/remotes/push.rs index 6cef918e..cb661a03 100644 --- a/asyncgit/src/sync/remotes/push.rs +++ b/asyncgit/src/sync/remotes/push.rs @@ -284,7 +284,8 @@ mod tests { assert_eq!( sync::get_commit_files( tmp_repo_dir.path().to_str().unwrap(), - repo_1_commit + repo_1_commit, + None ) .unwrap()[0] .path, diff --git a/asyncgit/src/sync/stash.rs b/asyncgit/src/sync/stash.rs index f98679a2..7c8ca43b 100644 --- a/asyncgit/src/sync/stash.rs +++ b/asyncgit/src/sync/stash.rs @@ -231,7 +231,7 @@ mod tests { let stash = get_stashes(repo_path)?[0]; - let diff = get_commit_files(repo_path, stash)?; + let diff = get_commit_files(repo_path, stash, None)?; assert_eq!(diff.len(), 1); diff --git a/src/app.rs b/src/app.rs index d17c0853..da46daf5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -4,8 +4,8 @@ use crate::{ components::{ event_pump, AppOption, BlameFileComponent, BranchListComponent, CommandBlocking, CommandInfo, - CommitComponent, Component, ConfirmComponent, - CreateBranchComponent, DrawableComponent, + CommitComponent, CompareCommitsComponent, Component, + ConfirmComponent, CreateBranchComponent, DrawableComponent, ExternalEditorComponent, HelpComponent, InspectCommitComponent, MsgComponent, OptionsPopupComponent, PullComponent, PushComponent, PushTagsComponent, @@ -48,6 +48,7 @@ pub struct App { blame_file_popup: BlameFileComponent, stashmsg_popup: StashMsgComponent, inspect_commit_popup: InspectCommitComponent, + compare_commits_popup: CompareCommitsComponent, external_editor_popup: ExternalEditorComponent, revision_files_popup: RevisionFilesPopup, push_popup: PushComponent, @@ -128,6 +129,12 @@ impl App { theme.clone(), key_config.clone(), ), + compare_commits_popup: CompareCommitsComponent::new( + &queue, + sender, + theme.clone(), + key_config.clone(), + ), external_editor_popup: ExternalEditorComponent::new( theme.clone(), key_config.clone(), @@ -369,6 +376,7 @@ impl App { self.revlog.update_git(ev)?; self.blame_file_popup.update_git(ev)?; self.inspect_commit_popup.update_git(ev)?; + self.compare_commits_popup.update_git(ev)?; self.push_popup.update_git(ev)?; self.push_tags_popup.update_git(ev)?; self.pull_popup.update_git(ev)?; @@ -399,6 +407,7 @@ impl App { || self.files_tab.anything_pending() || self.blame_file_popup.any_work_pending() || self.inspect_commit_popup.any_work_pending() + || self.compare_commits_popup.any_work_pending() || self.input.is_state_changing() || self.push_popup.any_work_pending() || self.push_tags_popup.any_work_pending() @@ -429,6 +438,7 @@ impl App { blame_file_popup, stashmsg_popup, inspect_commit_popup, + compare_commits_popup, external_editor_popup, push_popup, push_tags_popup, @@ -456,6 +466,7 @@ impl App { stashmsg_popup, help, inspect_commit_popup, + compare_commits_popup, blame_file_popup, external_editor_popup, tag_commit_popup, @@ -566,6 +577,7 @@ impl App { if flags.contains(NeedsUpdate::DIFF) { self.status_tab.update_diff()?; self.inspect_commit_popup.update_diff()?; + self.compare_commits_popup.update_diff()?; } if flags.contains(NeedsUpdate::COMMANDS) { self.update_commands(); @@ -593,6 +605,7 @@ impl App { Ok(flags) } + #[allow(clippy::too_many_lines)] fn process_internal_event( &mut self, ev: InternalEvent, @@ -694,6 +707,11 @@ impl App { flags.insert(NeedsUpdate::ALL); } + InternalEvent::CompareCommits(id, other) => { + self.compare_commits_popup.open(id, other)?; + flags + .insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS); + } }; Ok(flags) diff --git a/src/components/branchlist.rs b/src/components/branchlist.rs index 9f6e6a35..1922f4d3 100644 --- a/src/components/branchlist.rs +++ b/src/components/branchlist.rs @@ -130,6 +130,14 @@ impl Component for BranchListComponent { true, )); + out.push(CommandInfo::new( + strings::commands::compare_with_head( + &self.key_config, + ), + !self.selection_is_cur_branch(), + true, + )); + out.push(CommandInfo::new( strings::commands::toggle_branch_popup( &self.key_config, @@ -261,6 +269,15 @@ impl Component for BranchListComponent { InternalEvent::InspectCommit(b, None), ); } + } else if e == self.key_config.compare_commits + && self.valid_selection() + { + self.hide(); + if let Some(b) = self.get_selected() { + self.queue.push( + InternalEvent::CompareCommits(b, None), + ); + } } } diff --git a/src/components/commit_details/compare_details.rs b/src/components/commit_details/compare_details.rs new file mode 100644 index 00000000..26605b9f --- /dev/null +++ b/src/components/commit_details/compare_details.rs @@ -0,0 +1,206 @@ +use std::borrow::Cow; + +use crate::{ + components::{ + commit_details::style::{style_detail, Detail}, + dialog_paragraph, + utils::time_to_string, + CommandBlocking, CommandInfo, Component, DrawableComponent, + EventState, + }, + keys::SharedKeyConfig, + strings::{self}, + ui::style::SharedTheme, +}; +use anyhow::Result; +use asyncgit::{ + sync::{self, CommitDetails, CommitId}, + CWD, +}; +use crossterm::event::Event; +use tui::{ + backend::Backend, + layout::{Constraint, Direction, Layout, Rect}, + text::{Span, Spans, Text}, + Frame, +}; + +pub struct CompareDetailsComponent { + data: Option<(CommitDetails, CommitDetails)>, + theme: SharedTheme, + focused: bool, + key_config: SharedKeyConfig, +} + +impl CompareDetailsComponent { + /// + pub const fn new( + theme: SharedTheme, + key_config: SharedKeyConfig, + focused: bool, + ) -> Self { + Self { + data: None, + theme, + focused, + key_config, + } + } + + pub fn set_commits(&mut self, ids: Option<(CommitId, CommitId)>) { + self.data = ids.and_then(|ids| { + let c1 = sync::get_commit_details(CWD, ids.0).ok(); + let c2 = sync::get_commit_details(CWD, ids.1).ok(); + + c1.and_then(|c1| c2.map(|c2| (c1, c2))) + }); + } + + #[allow(unstable_name_collisions)] + fn get_commit_text(&self, data: &CommitDetails) -> Vec { + let mut res = vec![ + Spans::from(vec![ + style_detail( + &self.theme, + &self.key_config, + &Detail::Author, + ), + Span::styled( + Cow::from(format!( + "{} <{}>", + data.author.name, data.author.email + )), + self.theme.text(true, false), + ), + ]), + Spans::from(vec![ + style_detail( + &self.theme, + &self.key_config, + &Detail::Date, + ), + Span::styled( + Cow::from(time_to_string( + data.author.time, + false, + )), + self.theme.text(true, false), + ), + ]), + ]; + + if let Some(ref committer) = data.committer { + res.extend(vec![ + Spans::from(vec![ + style_detail( + &self.theme, + &self.key_config, + &Detail::Commiter, + ), + Span::styled( + Cow::from(format!( + "{} <{}>", + committer.name, committer.email + )), + self.theme.text(true, false), + ), + ]), + Spans::from(vec![ + style_detail( + &self.theme, + &self.key_config, + &Detail::Date, + ), + Span::styled( + Cow::from(time_to_string( + committer.time, + false, + )), + self.theme.text(true, false), + ), + ]), + ]); + } + + res.push(Spans::from(vec![ + Span::styled( + Cow::from(strings::commit::details_sha( + &self.key_config, + )), + self.theme.text(false, false), + ), + Span::styled( + Cow::from(data.hash.clone()), + self.theme.text(true, false), + ), + ])); + + res + } +} + +impl DrawableComponent for CompareDetailsComponent { + fn draw( + &self, + f: &mut Frame, + rect: Rect, + ) -> Result<()> { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints( + [Constraint::Length(5), Constraint::Length(5)] + .as_ref(), + ) + .split(rect); + + if let Some(data) = &self.data { + f.render_widget( + dialog_paragraph( + &strings::commit::compare_details_info_title( + true, + ), + Text::from(self.get_commit_text(&data.0)), + &self.theme, + false, + ), + chunks[0], + ); + + f.render_widget( + dialog_paragraph( + &strings::commit::compare_details_info_title( + false, + ), + Text::from(self.get_commit_text(&data.1)), + &self.theme, + false, + ), + chunks[1], + ); + } + + Ok(()) + } +} + +impl Component for CompareDetailsComponent { + fn commands( + &self, + _out: &mut Vec, + _force_all: bool, + ) -> CommandBlocking { + CommandBlocking::PassingOn + } + + fn event(&mut self, _event: Event) -> Result { + Ok(EventState::NotConsumed) + } + + fn focused(&self) -> bool { + self.focused + } + + fn focus(&mut self, focus: bool) { + self.focused = focus; + } +} diff --git a/src/components/commit_details/details.rs b/src/components/commit_details/details.rs index 2e8b69b3..6dc69926 100644 --- a/src/components/commit_details/details.rs +++ b/src/components/commit_details/details.rs @@ -1,5 +1,6 @@ use crate::{ components::{ + commit_details::style::style_detail, dialog_paragraph, utils::{scroll_vertical::VerticalScroll, time_to_string}, CommandBlocking, CommandInfo, Component, DrawableComponent, @@ -26,12 +27,8 @@ use tui::{ text::{Span, Spans, Text}, Frame, }; -enum Detail { - Author, - Date, - Commiter, - Sha, -} + +use super::style::Detail; pub struct DetailsComponent { data: Option, @@ -153,41 +150,16 @@ impl DetailsComponent { .collect() } - fn style_detail(&self, field: &Detail) -> Span { - match field { - Detail::Author => Span::styled( - Cow::from(strings::commit::details_author( - &self.key_config, - )), - self.theme.text(false, false), - ), - Detail::Date => Span::styled( - Cow::from(strings::commit::details_date( - &self.key_config, - )), - self.theme.text(false, false), - ), - Detail::Commiter => Span::styled( - Cow::from(strings::commit::details_committer( - &self.key_config, - )), - self.theme.text(false, false), - ), - Detail::Sha => Span::styled( - Cow::from(strings::commit::details_tags( - &self.key_config, - )), - self.theme.text(false, false), - ), - } - } - - #[allow(unstable_name_collisions)] + #[allow(unstable_name_collisions, clippy::too_many_lines)] fn get_text_info(&self) -> Vec { if let Some(ref data) = self.data { let mut res = vec![ Spans::from(vec![ - self.style_detail(&Detail::Author), + style_detail( + &self.theme, + &self.key_config, + &Detail::Author, + ), Span::styled( Cow::from(format!( "{} <{}>", @@ -197,7 +169,11 @@ impl DetailsComponent { ), ]), Spans::from(vec![ - self.style_detail(&Detail::Date), + style_detail( + &self.theme, + &self.key_config, + &Detail::Date, + ), Span::styled( Cow::from(time_to_string( data.author.time, @@ -211,7 +187,11 @@ impl DetailsComponent { if let Some(ref committer) = data.committer { res.extend(vec![ Spans::from(vec![ - self.style_detail(&Detail::Commiter), + style_detail( + &self.theme, + &self.key_config, + &Detail::Commiter, + ), Span::styled( Cow::from(format!( "{} <{}>", @@ -221,7 +201,11 @@ impl DetailsComponent { ), ]), Spans::from(vec![ - self.style_detail(&Detail::Date), + style_detail( + &self.theme, + &self.key_config, + &Detail::Date, + ), Span::styled( Cow::from(time_to_string( committer.time, @@ -247,9 +231,11 @@ impl DetailsComponent { ])); if !self.tags.is_empty() { - res.push(Spans::from( - self.style_detail(&Detail::Sha), - )); + res.push(Spans::from(style_detail( + &self.theme, + &self.key_config, + &Detail::Sha, + ))); res.push(Spans::from( self.tags diff --git a/src/components/commit_details/mod.rs b/src/components/commit_details/mod.rs index 3afd9926..5e52e0e5 100644 --- a/src/components/commit_details/mod.rs +++ b/src/components/commit_details/mod.rs @@ -1,4 +1,6 @@ +mod compare_details; mod details; +mod style; use super::{ command_pump, event_pump, CommandBlocking, CommandInfo, @@ -10,9 +12,10 @@ use crate::{ }; use anyhow::Result; use asyncgit::{ - sync::{CommitId, CommitTags}, - AsyncCommitFiles, AsyncGitNotification, + sync::CommitTags, AsyncCommitFiles, AsyncGitNotification, + CommitFilesParams, }; +use compare_details::CompareDetailsComponent; use crossbeam_channel::Sender; use crossterm::event::Event; use details::DetailsComponent; @@ -23,7 +26,9 @@ use tui::{ }; pub struct CommitDetailsComponent { - details: DetailsComponent, + commit: Option, + single_details: DetailsComponent, + compare_details: CompareDetailsComponent, file_tree: FileTreeComponent, git_commit_files: AsyncCommitFiles, visible: bool, @@ -31,7 +36,7 @@ pub struct CommitDetailsComponent { } impl CommitDetailsComponent { - accessors!(self, [details, file_tree]); + accessors!(self, [single_details, compare_details, file_tree]); /// pub fn new( @@ -41,7 +46,12 @@ impl CommitDetailsComponent { key_config: SharedKeyConfig, ) -> Self { Self { - details: DetailsComponent::new( + single_details: DetailsComponent::new( + theme.clone(), + key_config.clone(), + false, + ), + compare_details: CompareDetailsComponent::new( theme.clone(), key_config.clone(), false, @@ -55,6 +65,7 @@ impl CommitDetailsComponent { key_config.clone(), ), visible: false, + commit: None, key_config, } } @@ -70,14 +81,26 @@ impl CommitDetailsComponent { } /// - pub fn set_commit( + pub fn set_commits( &mut self, - id: Option, + params: Option, tags: Option, ) -> Result<()> { - self.details.set_commit(id, tags); + if params.is_none() { + self.single_details.set_commit(None, None); + self.compare_details.set_commits(None); + } + + self.commit = params; + + if let Some(id) = params { + if let Some(other) = id.other { + self.compare_details + .set_commits(Some((id.id, other))); + } else { + self.single_details.set_commit(Some(id.id), tags); + } - if let Some(id) = id { if let Some((fetched_id, res)) = self.git_commit_files.current()? { @@ -107,6 +130,23 @@ impl CommitDetailsComponent { pub const fn files(&self) -> &FileTreeComponent { &self.file_tree } + + fn details_focused(&self) -> bool { + self.single_details.focused() + || self.compare_details.focused() + } + + fn set_details_focus(&mut self, focus: bool) { + if self.is_compare() { + self.compare_details.focus(focus); + } else { + self.single_details.focus(focus); + } + } + + fn is_compare(&self) -> bool { + self.commit.map(|p| p.other.is_some()).unwrap_or_default() + } } impl DrawableComponent for CommitDetailsComponent { @@ -115,26 +155,34 @@ impl DrawableComponent for CommitDetailsComponent { f: &mut Frame, rect: Rect, ) -> Result<()> { - let percentages = if self.file_tree.focused() { - (40, 60) - } else if self.details.focused() { - (60, 40) + let constraints = if self.is_compare() { + [Constraint::Length(10), Constraint::Min(0)] } else { - (40, 60) + let details_focused = self.details_focused(); + let percentages = if self.file_tree.focused() { + (40, 60) + } else if details_focused { + (60, 40) + } else { + (40, 60) + }; + + [ + Constraint::Percentage(percentages.0), + Constraint::Percentage(percentages.1), + ] }; let chunks = Layout::default() .direction(Direction::Vertical) - .constraints( - [ - Constraint::Percentage(percentages.0), - Constraint::Percentage(percentages.1), - ] - .as_ref(), - ) + .constraints(constraints.as_ref()) .split(rect); - self.details.draw(f, chunks[0])?; + if self.is_compare() { + self.compare_details.draw(f, chunks[0])?; + } else { + self.single_details.draw(f, chunks[0])?; + } self.file_tree.draw(f, chunks[1])?; Ok(()) @@ -168,16 +216,17 @@ impl Component for CommitDetailsComponent { if self.focused() { if let Event::Key(e) = ev { return if e == self.key_config.focus_below - && self.details.focused() + && self.details_focused() { - self.details.focus(false); + self.set_details_focus(false); self.file_tree.focus(true); Ok(EventState::Consumed) } else if e == self.key_config.focus_above && self.file_tree.focused() + && !self.is_compare() { self.file_tree.focus(false); - self.details.focus(true); + self.set_details_focus(true); Ok(EventState::Consumed) } else { Ok(EventState::NotConsumed) @@ -200,10 +249,12 @@ impl Component for CommitDetailsComponent { } fn focused(&self) -> bool { - self.details.focused() || self.file_tree.focused() + self.details_focused() || self.file_tree.focused() } + fn focus(&mut self, focus: bool) { - self.details.focus(false); + self.single_details.focus(false); + self.compare_details.focus(false); self.file_tree.focus(focus); self.file_tree.show_selection(true); } diff --git a/src/components/commit_details/style.rs b/src/components/commit_details/style.rs new file mode 100644 index 00000000..2f0ce72f --- /dev/null +++ b/src/components/commit_details/style.rs @@ -0,0 +1,35 @@ +use crate::{keys::SharedKeyConfig, strings, ui::style::SharedTheme}; +use std::borrow::Cow; +use tui::text::Span; + +pub enum Detail { + Author, + Date, + Commiter, + Sha, +} + +pub fn style_detail<'a>( + theme: &'a SharedTheme, + keys: &'a SharedKeyConfig, + field: &Detail, +) -> Span<'a> { + match field { + Detail::Author => Span::styled( + Cow::from(strings::commit::details_author(keys)), + theme.text(false, false), + ), + Detail::Date => Span::styled( + Cow::from(strings::commit::details_date(keys)), + theme.text(false, false), + ), + Detail::Commiter => Span::styled( + Cow::from(strings::commit::details_committer(keys)), + theme.text(false, false), + ), + Detail::Sha => Span::styled( + Cow::from(strings::commit::details_tags(keys)), + theme.text(false, false), + ), + } +} diff --git a/src/components/compare_commits.rs b/src/components/compare_commits.rs new file mode 100644 index 00000000..fdc0b89b --- /dev/null +++ b/src/components/compare_commits.rs @@ -0,0 +1,268 @@ +use super::{ + command_pump, event_pump, visibility_blocking, CommandBlocking, + CommandInfo, CommitDetailsComponent, Component, DiffComponent, + DrawableComponent, EventState, +}; +use crate::{ + accessors, keys::SharedKeyConfig, queue::Queue, strings, + ui::style::SharedTheme, +}; +use anyhow::Result; +use asyncgit::{ + sync::{self, diff::DiffOptions, CommitId}, + AsyncDiff, AsyncGitNotification, CommitFilesParams, DiffParams, + DiffType, CWD, +}; +use crossbeam_channel::Sender; +use crossterm::event::Event; +use tui::{ + backend::Backend, + layout::{Constraint, Direction, Layout, Rect}, + widgets::Clear, + Frame, +}; + +pub struct CompareCommitsComponent { + commit_ids: Option<(CommitId, CommitId)>, + diff: DiffComponent, + details: CommitDetailsComponent, + git_diff: AsyncDiff, + visible: bool, + key_config: SharedKeyConfig, +} + +impl DrawableComponent for CompareCommitsComponent { + fn draw( + &self, + f: &mut Frame, + rect: Rect, + ) -> Result<()> { + if self.is_visible() { + let percentages = if self.diff.focused() { + (30, 70) + } else { + (50, 50) + }; + + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints( + [ + Constraint::Percentage(percentages.0), + Constraint::Percentage(percentages.1), + ] + .as_ref(), + ) + .split(rect); + + f.render_widget(Clear, rect); + + self.details.draw(f, chunks[0])?; + self.diff.draw(f, chunks[1])?; + } + + Ok(()) + } +} + +impl Component for CompareCommitsComponent { + fn commands( + &self, + out: &mut Vec, + force_all: bool, + ) -> CommandBlocking { + if self.is_visible() || force_all { + command_pump( + out, + force_all, + self.components().as_slice(), + ); + + out.push( + CommandInfo::new( + strings::commands::close_popup(&self.key_config), + true, + true, + ) + .order(1), + ); + + out.push(CommandInfo::new( + strings::commands::diff_focus_right(&self.key_config), + self.can_focus_diff(), + !self.diff.focused() || force_all, + )); + + out.push(CommandInfo::new( + strings::commands::diff_focus_left(&self.key_config), + true, + self.diff.focused() || force_all, + )); + } + + visibility_blocking(self) + } + + fn event(&mut self, ev: Event) -> Result { + if self.is_visible() { + if event_pump(ev, self.components_mut().as_mut_slice())? + .is_consumed() + { + return Ok(EventState::Consumed); + } + + if let Event::Key(e) = ev { + if e == self.key_config.exit_popup { + self.hide(); + } else if e == self.key_config.focus_right + && self.can_focus_diff() + { + self.details.focus(false); + self.diff.focus(true); + } else if e == self.key_config.focus_left + && self.diff.focused() + { + self.details.focus(true); + self.diff.focus(false); + } else if e == self.key_config.focus_left { + self.hide(); + } + + return Ok(EventState::Consumed); + } + } + + Ok(EventState::NotConsumed) + } + + fn is_visible(&self) -> bool { + self.visible + } + fn hide(&mut self) { + self.visible = false; + } + fn show(&mut self) -> Result<()> { + self.visible = true; + self.details.show()?; + self.details.focus(true); + self.diff.focus(false); + self.update()?; + Ok(()) + } +} + +impl CompareCommitsComponent { + accessors!(self, [diff, details]); + + /// + pub fn new( + queue: &Queue, + sender: &Sender, + theme: SharedTheme, + key_config: SharedKeyConfig, + ) -> Self { + Self { + details: CommitDetailsComponent::new( + queue, + sender, + theme.clone(), + key_config.clone(), + ), + diff: DiffComponent::new( + queue.clone(), + theme, + key_config.clone(), + true, + ), + commit_ids: None, + git_diff: AsyncDiff::new(sender), + visible: false, + key_config, + } + } + + /// + pub fn open( + &mut self, + id: CommitId, + other: Option, + ) -> Result<()> { + let other = if let Some(other) = other { + other + } else { + sync::get_head_tuple(CWD)?.id + }; + self.commit_ids = Some((id, other)); + self.show()?; + + Ok(()) + } + + /// + pub fn any_work_pending(&self) -> bool { + self.git_diff.is_pending() || self.details.any_work_pending() + } + + /// + pub fn update_git( + &mut self, + ev: AsyncGitNotification, + ) -> Result<()> { + if self.is_visible() { + if let AsyncGitNotification::CommitFiles = ev { + self.update()?; + } else if let AsyncGitNotification::Diff = ev { + self.update_diff()?; + } + } + + Ok(()) + } + + /// called when any tree component changed selection + pub fn update_diff(&mut self) -> Result<()> { + if self.is_visible() { + if let Some(ids) = self.commit_ids { + if let Some(f) = self.details.files().selection_file() + { + let diff_params = DiffParams { + path: f.path.clone(), + diff_type: DiffType::Commits(ids), + options: DiffOptions::default(), + }; + + if let Some((params, last)) = + self.git_diff.last()? + { + if params == diff_params { + self.diff.update(f.path, false, last); + return Ok(()); + } + } + + self.git_diff.request(diff_params)?; + self.diff.clear(true); + return Ok(()); + } + } + + self.diff.clear(false); + } + + Ok(()) + } + + fn update(&mut self) -> Result<()> { + self.details.set_commits( + self.commit_ids.map(CommitFilesParams::from), + None, + )?; + self.update_diff()?; + + Ok(()) + } + + fn can_focus_diff(&self) -> bool { + self.details.files().selection_file().is_some() + } +} diff --git a/src/components/inspect_commit.rs b/src/components/inspect_commit.rs index 1e09bb7d..44bafa42 100644 --- a/src/components/inspect_commit.rs +++ b/src/components/inspect_commit.rs @@ -13,7 +13,8 @@ use crate::{ use anyhow::Result; use asyncgit::{ sync::{diff::DiffOptions, CommitId, CommitTags}, - AsyncDiff, AsyncGitNotification, DiffParams, DiffType, + AsyncDiff, AsyncGitNotification, CommitFilesParams, DiffParams, + DiffType, }; use crossbeam_channel::Sender; use crossterm::event::Event; @@ -270,7 +271,10 @@ impl InspectCommitComponent { } fn update(&mut self) -> Result<()> { - self.details.set_commit(self.commit_id, self.tags.clone())?; + self.details.set_commits( + self.commit_id.map(CommitFilesParams::from), + self.tags.clone(), + )?; self.update_diff()?; Ok(()) diff --git a/src/components/mod.rs b/src/components/mod.rs index 36a75bdc..aca90a14 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -5,6 +5,7 @@ mod command; mod commit; mod commit_details; mod commitlist; +mod compare_commits; mod create_branch; mod cred; mod diff; @@ -36,6 +37,7 @@ pub use command::{CommandInfo, CommandText}; pub use commit::CommitComponent; pub use commit_details::CommitDetailsComponent; pub use commitlist::CommitList; +pub use compare_commits::CompareCommitsComponent; pub use create_branch::CreateBranchComponent; pub use diff::DiffComponent; pub use externaleditor::ExternalEditorComponent; diff --git a/src/keys.rs b/src/keys.rs index c52d2436..4a86655f 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -76,6 +76,7 @@ pub struct KeyConfig { pub select_branch: KeyEvent, pub delete_branch: KeyEvent, pub merge_branch: KeyEvent, + pub compare_commits: KeyEvent, pub tags: KeyEvent, pub delete_tag: KeyEvent, pub select_tag: KeyEvent, @@ -89,16 +90,16 @@ pub struct KeyConfig { #[rustfmt::skip] impl Default for KeyConfig { - fn default() -> Self { - Self { + fn default() -> Self { + Self { tab_status: KeyEvent { code: KeyCode::Char('1'), modifiers: KeyModifiers::empty()}, tab_log: KeyEvent { code: KeyCode::Char('2'), modifiers: KeyModifiers::empty()}, - tab_files: KeyEvent { code: KeyCode::Char('3'), modifiers: KeyModifiers::empty()}, + tab_files: KeyEvent { code: KeyCode::Char('3'), modifiers: KeyModifiers::empty()}, tab_stashing: KeyEvent { code: KeyCode::Char('4'), modifiers: KeyModifiers::empty()}, tab_stashes: KeyEvent { code: KeyCode::Char('5'), modifiers: KeyModifiers::empty()}, tab_toggle: KeyEvent { code: KeyCode::Tab, modifiers: KeyModifiers::empty()}, tab_toggle_reverse: KeyEvent { code: KeyCode::BackTab, modifiers: KeyModifiers::SHIFT}, - toggle_workarea: KeyEvent { code: KeyCode::Char('w'), modifiers: KeyModifiers::empty()}, + toggle_workarea: KeyEvent { code: KeyCode::Char('w'), modifiers: KeyModifiers::empty()}, focus_right: KeyEvent { code: KeyCode::Right, modifiers: KeyModifiers::empty()}, focus_left: KeyEvent { code: KeyCode::Left, modifiers: KeyModifiers::empty()}, focus_above: KeyEvent { code: KeyCode::Up, modifiers: KeyModifiers::empty()}, @@ -112,8 +113,8 @@ impl Default for KeyConfig { open_options: KeyEvent { code: KeyCode::Char('o'), modifiers: KeyModifiers::empty()}, move_left: KeyEvent { code: KeyCode::Left, modifiers: KeyModifiers::empty()}, move_right: KeyEvent { code: KeyCode::Right, modifiers: KeyModifiers::empty()}, - tree_collapse_recursive: KeyEvent { code: KeyCode::Left, modifiers: KeyModifiers::SHIFT}, - tree_expand_recursive: KeyEvent { code: KeyCode::Right, modifiers: KeyModifiers::SHIFT}, + tree_collapse_recursive: KeyEvent { code: KeyCode::Left, modifiers: KeyModifiers::SHIFT}, + tree_expand_recursive: KeyEvent { code: KeyCode::Right, modifiers: KeyModifiers::SHIFT}, home: KeyEvent { code: KeyCode::Home, modifiers: KeyModifiers::empty()}, end: KeyEvent { code: KeyCode::End, modifiers: KeyModifiers::empty()}, move_up: KeyEvent { code: KeyCode::Up, modifiers: KeyModifiers::empty()}, @@ -127,9 +128,9 @@ impl Default for KeyConfig { edit_file: KeyEvent { code: KeyCode::Char('e'), modifiers: KeyModifiers::empty()}, status_stage_all: KeyEvent { code: KeyCode::Char('a'), modifiers: KeyModifiers::empty()}, status_reset_item: KeyEvent { code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT}, - diff_reset_lines: KeyEvent { code: KeyCode::Char('d'), modifiers: KeyModifiers::empty()}, + diff_reset_lines: KeyEvent { code: KeyCode::Char('d'), modifiers: KeyModifiers::empty()}, status_ignore_file: KeyEvent { code: KeyCode::Char('i'), modifiers: KeyModifiers::empty()}, - diff_stage_lines: KeyEvent { code: KeyCode::Char('s'), modifiers: KeyModifiers::empty()}, + diff_stage_lines: KeyEvent { code: KeyCode::Char('s'), modifiers: KeyModifiers::empty()}, stashing_save: KeyEvent { code: KeyCode::Char('s'), modifiers: KeyModifiers::empty()}, stashing_toggle_untracked: KeyEvent { code: KeyCode::Char('u'), modifiers: KeyModifiers::empty()}, stashing_toggle_index: KeyEvent { code: KeyCode::Char('i'), modifiers: KeyModifiers::empty()}, @@ -138,25 +139,26 @@ impl Default for KeyConfig { stash_drop: KeyEvent { code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT}, cmd_bar_toggle: KeyEvent { code: KeyCode::Char('.'), modifiers: KeyModifiers::empty()}, log_tag_commit: KeyEvent { code: KeyCode::Char('t'), modifiers: KeyModifiers::empty()}, - log_mark_commit: KeyEvent { code: KeyCode::Char(' '), modifiers: KeyModifiers::empty()}, + log_mark_commit: KeyEvent { code: KeyCode::Char(' '), modifiers: KeyModifiers::empty()}, commit_amend: KeyEvent { code: KeyCode::Char('a'), modifiers: KeyModifiers::CONTROL}, - copy: KeyEvent { code: KeyCode::Char('y'), modifiers: KeyModifiers::empty()}, - create_branch: KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::empty()}, - rename_branch: KeyEvent { code: KeyCode::Char('r'), modifiers: KeyModifiers::empty()}, - select_branch: KeyEvent { code: KeyCode::Char('b'), modifiers: KeyModifiers::empty()}, - delete_branch: KeyEvent { code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT}, - merge_branch: KeyEvent { code: KeyCode::Char('m'), modifiers: KeyModifiers::empty()}, - tags: KeyEvent { code: KeyCode::Char('T'), modifiers: KeyModifiers::SHIFT}, - delete_tag: KeyEvent { code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT}, - select_tag: KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::empty()}, - push: KeyEvent { code: KeyCode::Char('p'), modifiers: KeyModifiers::empty()}, - force_push: KeyEvent { code: KeyCode::Char('P'), modifiers: KeyModifiers::SHIFT}, - undo_commit: KeyEvent { code: KeyCode::Char('U'), modifiers: KeyModifiers::SHIFT}, - pull: KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::empty()}, - abort_merge: KeyEvent { code: KeyCode::Char('M'), modifiers: KeyModifiers::SHIFT}, - open_file_tree: KeyEvent { code: KeyCode::Char('F'), modifiers: KeyModifiers::SHIFT}, - } - } + copy: KeyEvent { code: KeyCode::Char('y'), modifiers: KeyModifiers::empty()}, + create_branch: KeyEvent { code: KeyCode::Char('c'), modifiers: KeyModifiers::empty()}, + rename_branch: KeyEvent { code: KeyCode::Char('r'), modifiers: KeyModifiers::empty()}, + select_branch: KeyEvent { code: KeyCode::Char('b'), modifiers: KeyModifiers::empty()}, + delete_branch: KeyEvent { code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT}, + merge_branch: KeyEvent { code: KeyCode::Char('m'), modifiers: KeyModifiers::empty()}, + compare_commits: KeyEvent { code: KeyCode::Char('C'), modifiers: KeyModifiers::SHIFT}, + tags: KeyEvent { code: KeyCode::Char('T'), modifiers: KeyModifiers::SHIFT}, + delete_tag: KeyEvent { code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT}, + select_tag: KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::empty()}, + push: KeyEvent { code: KeyCode::Char('p'), modifiers: KeyModifiers::empty()}, + force_push: KeyEvent { code: KeyCode::Char('P'), modifiers: KeyModifiers::SHIFT}, + undo_commit: KeyEvent { code: KeyCode::Char('U'), modifiers: KeyModifiers::SHIFT}, + pull: KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::empty()}, + abort_merge: KeyEvent { code: KeyCode::Char('M'), modifiers: KeyModifiers::SHIFT}, + open_file_tree: KeyEvent { code: KeyCode::Char('F'), modifiers: KeyModifiers::SHIFT}, + } + } } impl KeyConfig { @@ -194,7 +196,7 @@ impl KeyConfig { Self::default().save(file)?; Err(anyhow::anyhow!("{}\n Old file was renamed to {:?}.\n Defaults loaded and saved as {:?}", - e,config_path_old,config_path.to_string_lossy())) + e,config_path_old,config_path.to_string_lossy())) } Ok(res) => Ok(res), } diff --git a/src/queue.rs b/src/queue.rs index 411e16e2..d9bd740c 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -60,6 +60,8 @@ pub enum InternalEvent { /// InspectCommit(CommitId, Option), /// + CompareCommits(CommitId, Option), + /// SelectCommitInRevlog(CommitId), /// TagCommit(CommitId), diff --git a/src/strings.rs b/src/strings.rs index af3305bd..d8e6e5af 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -322,6 +322,13 @@ pub mod commit { ) -> String { "Info".to_string() } + pub fn compare_details_info_title(old: bool) -> String { + if old { + "Old".to_string() + } else { + "New".to_string() + } + } pub fn details_message_title( _key_config: &SharedKeyConfig, ) -> String { @@ -1051,6 +1058,33 @@ pub mod commands { CMD_GROUP_BRANCHES, ) } + + pub fn compare_with_head( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Compare [{}]", + key_config.get_hint(key_config.compare_commits), + ), + "compare with head", + CMD_GROUP_BRANCHES, + ) + } + + pub fn compare_commits( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Compare Commits [{}]", + key_config.get_hint(key_config.compare_commits), + ), + "compare two marked commits", + CMD_GROUP_LOG, + ) + } + pub fn select_branch_popup( key_config: &SharedKeyConfig, ) -> CommandText { diff --git a/src/tabs/revlog.rs b/src/tabs/revlog.rs index ddcf74af..4a37d391 100644 --- a/src/tabs/revlog.rs +++ b/src/tabs/revlog.rs @@ -13,7 +13,8 @@ use anyhow::Result; use asyncgit::{ cached, sync::{self, CommitId}, - AsyncGitNotification, AsyncLog, AsyncTags, FetchStatus, CWD, + AsyncGitNotification, AsyncLog, AsyncTags, CommitFilesParams, + FetchStatus, CWD, }; use crossbeam_channel::Sender; use crossterm::event::Event; @@ -101,7 +102,10 @@ impl Revlog { let commit = self.selected_commit(); let tags = self.selected_commit_tags(&commit); - self.commit_details.set_commit(commit, tags)?; + self.commit_details.set_commits( + commit.map(CommitFilesParams::from), + tags, + )?; } } @@ -269,6 +273,29 @@ impl Component for Revlog { } else if k == self.key_config.tags { self.queue.push(InternalEvent::Tags); return Ok(EventState::Consumed); + } else if k == self.key_config.compare_commits + && self.list.marked_count() > 0 + { + if self.list.marked_count() == 1 { + // compare against head + self.queue.push( + InternalEvent::CompareCommits( + self.list.marked()[0], + None, + ), + ); + return Ok(EventState::Consumed); + } else if self.list.marked_count() == 2 { + //compare two marked commits + let marked = self.list.marked(); + self.queue.push( + InternalEvent::CompareCommits( + marked[0], + Some(marked[1]), + ), + ); + return Ok(EventState::Consumed); + } } } } @@ -298,12 +325,6 @@ impl Component for Revlog { || force_all, )); - out.push(CommandInfo::new( - strings::commands::log_tag_commit(&self.key_config), - self.selected_commit().is_some(), - self.visible || force_all, - )); - out.push(CommandInfo::new( strings::commands::open_branch_select_popup( &self.key_config, @@ -313,9 +334,17 @@ impl Component for Revlog { )); out.push(CommandInfo::new( - strings::commands::open_tags_popup(&self.key_config), + strings::commands::compare_with_head(&self.key_config), + self.list.marked_count() == 1, + (self.visible && self.list.marked_count() <= 1) + || force_all, + )); + + out.push(CommandInfo::new( + strings::commands::compare_commits(&self.key_config), true, - self.visible || force_all, + (self.visible && self.list.marked_count() == 2) + || force_all, )); out.push(CommandInfo::new( @@ -324,6 +353,18 @@ impl Component for Revlog { self.visible || force_all, )); + out.push(CommandInfo::new( + strings::commands::log_tag_commit(&self.key_config), + self.selected_commit().is_some(), + self.visible || force_all, + )); + + out.push(CommandInfo::new( + strings::commands::open_tags_popup(&self.key_config), + true, + self.visible || force_all, + )); + out.push(CommandInfo::new( strings::commands::push_tags(&self.key_config), true, diff --git a/vim_style_key_config.ron b/vim_style_key_config.ron index 10cf9f10..a945ef7a 100644 --- a/vim_style_key_config.ron +++ b/vim_style_key_config.ron @@ -86,6 +86,8 @@ merge_branch: ( code: Char('m'), modifiers: ( bits: 0,),), abort_merge: ( code: Char('M'), modifiers: ( bits: 1,),), + compare_commits: ( code: Char('C'), modifiers: ( bits: 1,),), + tags: ( code: Char('T'), modifiers: ( bits: 1,),), delete_tag: ( code: Char('D'), modifiers: ( bits: 1,),), select_tag: ( code: Enter, modifiers: ( bits: 0,),),