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 <omnikar5@gmail.com>
Co-authored-by: extrawurst <776816+extrawurst@users.noreply.github.com>
This commit is contained in:
Andrey Krupskiy 2023-02-04 07:00:19 +01:00 committed by GitHub
parent 5411397f9a
commit 57a5322fa7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 112 additions and 4 deletions

View File

@ -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))

View File

@ -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::*;

View File

@ -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,

View File

@ -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<str>,
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<BranchInfo>) {
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
};

View File

@ -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()),

View File

@ -58,6 +58,7 @@ pub struct KeysListFile {
pub cmd_bar_toggle: Option<GituiKeyEvent>,
pub log_tag_commit: Option<GituiKeyEvent>,
pub log_mark_commit: Option<GituiKeyEvent>,
pub log_checkout_commit: Option<GituiKeyEvent>,
pub commit_amend: Option<GituiKeyEvent>,
pub toggle_verify: Option<GituiKeyEvent>,
pub copy: Option<GituiKeyEvent>,
@ -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),

View File

@ -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 {

View File

@ -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,

View File

@ -32,6 +32,7 @@ impl StashList {
Self {
visible: false,
list: CommitList::new(
repo.clone(),
&strings::stashlist_title(&key_config),
theme,
queue.clone(),