From 57a5322fa76baf8c5bf66db533419b13297d0057 Mon Sep 17 00:00:00 2001 From: Andrey Krupskiy Date: Sat, 4 Feb 2023 07:00:19 +0100 Subject: [PATCH] Checkout commit (#1499) * Add keybind to checkout commit in log view * Extract commit checkout into method * add quckbar hint for checkout commit * add a smoke test * update changelog * show an error in popup --------- Co-authored-by: Omnikar Co-authored-by: extrawurst <776816+extrawurst@users.noreply.github.com> --- CHANGELOG.md | 1 + asyncgit/src/sync/branch/mod.rs | 57 +++++++++++++++++++++++++++++++++ asyncgit/src/sync/mod.rs | 7 ++-- src/components/commitlist.rs | 26 ++++++++++++++- src/keys/key_list.rs | 2 ++ src/keys/key_list_file.rs | 2 ++ src/strings.rs | 13 ++++++++ src/tabs/revlog.rs | 7 ++++ src/tabs/stashlist.rs | 1 + 9 files changed, 112 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff5d7329..523ba413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * changes in commit message inside external editor [[@bc-universe]](https://github.com/bc-universe) ([#1420](https://github.com/extrawurst/gitui/issues/1420)) +* allow detaching HEAD and checking out specific commit from log view ([#1499](https://github.com/extrawurst/gitui/pull/1499)) * add no-verify option on commits to not run hooks [[@dam5h]](https://github.com/dam5h) ([#1374](https://github.com/extrawurst/gitui/issues/1374)) * allow `fetch` on status tab [[@alensiljak]](https://github.com/alensiljak) ([#1471](https://github.com/extrawurst/gitui/issues/1471)) diff --git a/asyncgit/src/sync/branch/mod.rs b/asyncgit/src/sync/branch/mod.rs index c72a4845..89056ecf 100644 --- a/asyncgit/src/sync/branch/mod.rs +++ b/asyncgit/src/sync/branch/mod.rs @@ -301,6 +301,36 @@ pub fn checkout_branch( } } +/// Detach HEAD to point to a commit then checkout HEAD, does not work if there are uncommitted changes +pub fn checkout_commit( + repo_path: &RepoPath, + commit_hash: CommitId, +) -> Result<()> { + scope_time!("checkout_commit"); + + let repo = repo(repo_path)?; + let cur_ref = repo.head()?; + let statuses = repo.statuses(Some( + git2::StatusOptions::new().include_ignored(false), + ))?; + + if statuses.is_empty() { + repo.set_head_detached(commit_hash.into())?; + + if let Err(e) = repo.checkout_head(Some( + git2::build::CheckoutBuilder::new().force(), + )) { + repo.set_head( + bytes2string(cur_ref.name_bytes())?.as_str(), + )?; + return Err(Error::Git(e)); + } + Ok(()) + } else { + Err(Error::UncommittedChanges) + } +} + /// pub fn checkout_remote_branch( repo_path: &RepoPath, @@ -665,6 +695,33 @@ mod tests_checkout { } } +#[cfg(test)] +mod tests_checkout_commit { + use super::*; + use crate::sync::tests::{repo_init, write_commit_file}; + use crate::sync::RepoPath; + + #[test] + fn test_smoke() { + let (_td, repo) = repo_init().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path: &RepoPath = + &root.as_os_str().to_str().unwrap().into(); + + let commit = + write_commit_file(&repo, "test_1.txt", "test", "commit1"); + write_commit_file(&repo, "test_2.txt", "test", "commit2"); + + checkout_commit(repo_path, commit).unwrap(); + + assert!(repo.head_detached().unwrap()); + assert_eq!( + repo.head().unwrap().target().unwrap(), + commit.get_oid() + ); + } +} + #[cfg(test)] mod test_delete_branch { use super::*; diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index ed305b4a..9194bf03 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -34,9 +34,10 @@ pub mod utils; pub use blame::{blame_file, BlameHunk, FileBlame}; pub use branch::{ - branch_compare_upstream, checkout_branch, config_is_pull_rebase, - create_branch, delete_branch, get_branch_remote, - get_branches_info, merge_commit::merge_upstream_commit, + branch_compare_upstream, checkout_branch, checkout_commit, + config_is_pull_rebase, create_branch, delete_branch, + get_branch_remote, get_branches_info, + merge_commit::merge_upstream_commit, merge_ff::branch_merge_upstream_fastforward, merge_rebase::merge_upstream_rebase, rename::rename_branch, validate_branch_name, BranchCompare, BranchInfo, diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs index 2d2a42b3..603c5332 100644 --- a/src/components/commitlist.rs +++ b/src/components/commitlist.rs @@ -7,11 +7,14 @@ use crate::{ keys::{key_match, SharedKeyConfig}, queue::{InternalEvent, Queue}, strings::{self, symbol}, + try_or_popup, ui::style::{SharedTheme, Theme}, ui::{calc_scroll_top, draw_scrollbar, Orientation}, }; use anyhow::Result; -use asyncgit::sync::{BranchInfo, CommitId, Tags}; +use asyncgit::sync::{ + checkout_commit, BranchInfo, CommitId, RepoPathRef, Tags, +}; use chrono::{DateTime, Local}; use crossterm::event::Event; use itertools::Itertools; @@ -31,6 +34,7 @@ const ELEMENTS_PER_LINE: usize = 9; /// pub struct CommitList { + repo: RepoPathRef, title: Box, selection: usize, count_total: usize, @@ -49,12 +53,14 @@ pub struct CommitList { impl CommitList { /// pub fn new( + repo: RepoPathRef, title: &str, theme: SharedTheme, queue: Queue, key_config: SharedKeyConfig, ) -> Self { Self { + repo, items: ItemBatch::default(), marked: Vec::with_capacity(2), selection: 0, @@ -435,6 +441,18 @@ impl CommitList { self.selection = position; } + pub fn checkout(&mut self) { + if let Some(commit_hash) = + self.selected_entry().map(|entry| entry.id) + { + try_or_popup!( + self, + "failed to checkout commit:", + checkout_commit(&self.repo.borrow(), commit_hash) + ); + } + } + pub fn set_branches(&mut self, branches: Vec) { self.branches.clear(); @@ -538,6 +556,12 @@ impl Component for CommitList { ) { self.mark(); true + } else if key_match( + k, + self.key_config.keys.log_checkout_commit, + ) { + self.checkout(); + true } else { false }; diff --git a/src/keys/key_list.rs b/src/keys/key_list.rs index d1120549..96fbaeeb 100644 --- a/src/keys/key_list.rs +++ b/src/keys/key_list.rs @@ -87,6 +87,7 @@ pub struct KeysList { pub cmd_bar_toggle: GituiKeyEvent, pub log_tag_commit: GituiKeyEvent, pub log_mark_commit: GituiKeyEvent, + pub log_checkout_commit: GituiKeyEvent, pub commit_amend: GituiKeyEvent, pub toggle_verify: GituiKeyEvent, pub copy: GituiKeyEvent, @@ -171,6 +172,7 @@ impl Default for KeysList { cmd_bar_toggle: GituiKeyEvent::new(KeyCode::Char('.'), KeyModifiers::empty()), log_tag_commit: GituiKeyEvent::new(KeyCode::Char('t'), KeyModifiers::empty()), log_mark_commit: GituiKeyEvent::new(KeyCode::Char(' '), KeyModifiers::empty()), + log_checkout_commit: GituiKeyEvent { code: KeyCode::Char('S'), modifiers: KeyModifiers::SHIFT }, commit_amend: GituiKeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL), toggle_verify: GituiKeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL), copy: GituiKeyEvent::new(KeyCode::Char('y'), KeyModifiers::empty()), diff --git a/src/keys/key_list_file.rs b/src/keys/key_list_file.rs index b73f87bf..ae8b4f2f 100644 --- a/src/keys/key_list_file.rs +++ b/src/keys/key_list_file.rs @@ -58,6 +58,7 @@ pub struct KeysListFile { pub cmd_bar_toggle: Option, pub log_tag_commit: Option, pub log_mark_commit: Option, + pub log_checkout_commit: Option, pub commit_amend: Option, pub toggle_verify: Option, pub copy: Option, @@ -151,6 +152,7 @@ impl KeysListFile { cmd_bar_toggle: self.cmd_bar_toggle.unwrap_or(default.cmd_bar_toggle), log_tag_commit: self.log_tag_commit.unwrap_or(default.log_tag_commit), log_mark_commit: self.log_mark_commit.unwrap_or(default.log_mark_commit), + log_checkout_commit: self.log_checkout_commit.unwrap_or(default.log_checkout_commit), commit_amend: self.commit_amend.unwrap_or(default.commit_amend), toggle_verify: self.toggle_verify.unwrap_or(default.toggle_verify), copy: self.copy.unwrap_or(default.copy), diff --git a/src/strings.rs b/src/strings.rs index 561a853b..37609499 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -1195,6 +1195,19 @@ pub mod commands { CMD_GROUP_LOG, ) } + pub fn log_checkout_commit( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Checkout [{}]", + key_config + .get_hint(key_config.keys.log_checkout_commit), + ), + "checkout commit", + CMD_GROUP_LOG, + ) + } pub fn inspect_file_tree( key_config: &SharedKeyConfig, ) -> CommandText { diff --git a/src/tabs/revlog.rs b/src/tabs/revlog.rs index fd124025..0b0ad7d4 100644 --- a/src/tabs/revlog.rs +++ b/src/tabs/revlog.rs @@ -62,6 +62,7 @@ impl Revlog { key_config.clone(), ), list: CommitList::new( + repo.clone(), &strings::log_title(&key_config), theme, queue.clone(), @@ -418,6 +419,12 @@ impl Component for Revlog { self.visible || force_all, )); + out.push(CommandInfo::new( + strings::commands::log_checkout_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, diff --git a/src/tabs/stashlist.rs b/src/tabs/stashlist.rs index 89176e93..4a9e1449 100644 --- a/src/tabs/stashlist.rs +++ b/src/tabs/stashlist.rs @@ -32,6 +32,7 @@ impl StashList { Self { visible: false, list: CommitList::new( + repo.clone(), &strings::stashlist_title(&key_config), theme, queue.clone(),