From 3bd9c511cbe7687f0cfe7883be5b70a6724092a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Wed, 8 Jul 2020 20:54:36 +0200 Subject: [PATCH] Add command for tagging commit --- asyncgit/src/sync/commit.rs | 64 ++++++++++++++++- asyncgit/src/sync/mod.rs | 2 +- src/app.rs | 13 +++- src/components/mod.rs | 3 + src/components/tag_commit.rs | 134 +++++++++++++++++++++++++++++++++++ src/keys.rs | 1 + src/queue.rs | 2 + src/strings.rs | 10 +++ src/tabs/revlog.rs | 19 +++++ 9 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 src/components/tag_commit.rs diff --git a/asyncgit/src/sync/commit.rs b/asyncgit/src/sync/commit.rs index 80f0b5dbc4..b24e7ce5ed 100644 --- a/asyncgit/src/sync/commit.rs +++ b/asyncgit/src/sync/commit.rs @@ -1,6 +1,6 @@ use super::{get_head, utils::repo, CommitId}; use crate::error::Result; -use git2::{ErrorCode, Repository, Signature}; +use git2::{ErrorCode, ObjectType, Repository, Signature}; use scopetime::scope_time; /// @@ -80,17 +80,39 @@ pub fn commit(repo_path: &str, msg: &str) -> Result { .into()) } +/// Tag a commit. +/// +/// This function will return an `Err(…)` variant if the tag’s name is refused +/// by git or if the tag already exists. +pub fn tag( + repo_path: &str, + commit_id: &CommitId, + tag: &str, +) -> Result { + scope_time!("tag"); + + let repo = repo(repo_path)?; + + let signature = signature_allow_undefined_name(&repo)?; + let object_id = commit_id.get_oid(); + let target = + repo.find_object(object_id, Some(ObjectType::Commit))?; + + Ok(repo.tag(tag, &target, &signature, "", false)?.into()) +} + #[cfg(test)] mod tests { use crate::error::Result; use crate::sync::{ commit, get_commit_details, get_commit_files, stage_add_file, + tags::get_tags, tests::{get_statuses, repo_init, repo_init_empty}, utils::get_head, LogWalker, }; - use commit::amend; + use commit::{amend, tag}; use git2::Repository; use std::{fs::File, io::Write, path::Path}; @@ -185,4 +207,42 @@ mod tests { Ok(()) } + + #[test] + fn test_tag() -> Result<()> { + let file_path = Path::new("foo"); + let (_td, repo) = repo_init_empty().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path = root.as_os_str().to_str().unwrap(); + + File::create(&root.join(file_path))? + .write_all(b"test\nfoo")?; + + stage_add_file(repo_path, file_path)?; + + let new_id = commit(repo_path, "commit msg")?; + + tag(repo_path, &new_id, "tag")?; + + assert_eq!( + get_tags(repo_path).unwrap()[&new_id], + vec!["tag"] + ); + + assert!(matches!(tag(repo_path, &new_id, "tag"), Err(_))); + + assert_eq!( + get_tags(repo_path).unwrap()[&new_id], + vec!["tag"] + ); + + tag(repo_path, &new_id, "second-tag")?; + + assert_eq!( + get_tags(repo_path).unwrap()[&new_id], + vec!["second-tag", "tag"] + ); + + Ok(()) + } } diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 2c0e782fc1..382905b9f8 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -18,7 +18,7 @@ pub mod utils; pub(crate) use branch::get_branch_name; -pub use commit::{amend, commit}; +pub use commit::{amend, commit, tag}; pub use commit_details::{get_commit_details, CommitDetails}; pub use commit_files::get_commit_files; pub use commits_info::{get_commits_info, CommitId, CommitInfo}; diff --git a/src/app.rs b/src/app.rs index fa02846767..76b8b808a1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,7 +5,7 @@ use crate::{ event_pump, CommandBlocking, CommandInfo, CommitComponent, Component, DrawableComponent, ExternalEditorComponent, HelpComponent, InspectCommitComponent, MsgComponent, - ResetComponent, StashMsgComponent, + ResetComponent, StashMsgComponent, TagCommitComponent, }, input::{Input, InputEvent, InputState}, keys, @@ -40,6 +40,7 @@ pub struct App { stashmsg_popup: StashMsgComponent, inspect_commit_popup: InspectCommitComponent, external_editor_popup: ExternalEditorComponent, + tag_commit_popup: TagCommitComponent, cmdbar: RefCell, tab: usize, revlog: Revlog, @@ -85,6 +86,10 @@ impl App { external_editor_popup: ExternalEditorComponent::new( theme.clone(), ), + tag_commit_popup: TagCommitComponent::new( + queue.clone(), + theme.clone(), + ), do_quit: false, cmdbar: RefCell::new(CommandBar::new(theme.clone())), help: HelpComponent::new(theme.clone()), @@ -296,6 +301,7 @@ impl App { stashmsg_popup, inspect_commit_popup, external_editor_popup, + tag_commit_popup, help, revlog, status_tab, @@ -419,6 +425,9 @@ impl App { self.stashmsg_popup.options(opts); self.stashmsg_popup.show()? } + InternalEvent::TagCommit(id) => { + self.tag_commit_popup.open(id)?; + } InternalEvent::TabSwitch => self.set_tab(0)?, InternalEvent::InspectCommit(id, tags) => { self.inspect_commit_popup.open(id, tags)?; @@ -484,6 +493,7 @@ impl App { || self.stashmsg_popup.is_visible() || self.inspect_commit_popup.is_visible() || self.external_editor_popup.is_visible() + || self.tag_commit_popup.is_visible() } fn draw_popups( @@ -508,6 +518,7 @@ impl App { self.msg.draw(f, size)?; self.inspect_commit_popup.draw(f, size)?; self.external_editor_popup.draw(f, size)?; + self.tag_commit_popup.draw(f, size)?; Ok(()) } diff --git a/src/components/mod.rs b/src/components/mod.rs index 386e59225f..13ca85d03f 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -11,6 +11,7 @@ mod inspect_commit; mod msg; mod reset; mod stashmsg; +mod tag_commit; mod textinput; mod utils; @@ -30,6 +31,8 @@ pub use inspect_commit::InspectCommitComponent; pub use msg::MsgComponent; pub use reset::ResetComponent; pub use stashmsg::StashMsgComponent; +pub use tag_commit::TagCommitComponent; +pub use textinput::TextInputComponent; pub use utils::filetree::FileTreeItemKind; use crate::ui::style::Theme; diff --git a/src/components/tag_commit.rs b/src/components/tag_commit.rs new file mode 100644 index 0000000000..e956726f58 --- /dev/null +++ b/src/components/tag_commit.rs @@ -0,0 +1,134 @@ +use super::{ + textinput::TextInputComponent, visibility_blocking, + CommandBlocking, CommandInfo, Component, DrawableComponent, +}; +use crate::{ + queue::{InternalEvent, NeedsUpdate, Queue}, + strings::{self, commands}, + ui::style::SharedTheme, +}; +use anyhow::Result; +use asyncgit::{ + sync::{self, CommitId}, + CWD, +}; +use crossterm::event::{Event, KeyCode}; +use tui::{backend::Backend, layout::Rect, Frame}; + +pub struct TagCommitComponent { + input: TextInputComponent, + commit_id: Option, + queue: Queue, +} + +impl DrawableComponent for TagCommitComponent { + fn draw( + &self, + f: &mut Frame, + rect: Rect, + ) -> Result<()> { + self.input.draw(f, rect)?; + + Ok(()) + } +} + +impl Component for TagCommitComponent { + fn commands( + &self, + out: &mut Vec, + force_all: bool, + ) -> CommandBlocking { + if self.is_visible() || force_all { + self.input.commands(out, force_all); + + out.push(CommandInfo::new( + commands::TAG_COMMIT_CONFIRM_MSG, + true, + true, + )); + } + + visibility_blocking(self) + } + + fn event(&mut self, ev: Event) -> Result { + if self.is_visible() { + if self.input.event(ev)? { + return Ok(true); + } + + if let Event::Key(e) = ev { + if let KeyCode::Enter = e.code { + self.tag() + } + + return Ok(true); + } + } + Ok(false) + } + + fn is_visible(&self) -> bool { + self.input.is_visible() + } + + fn hide(&mut self) { + self.input.hide() + } + + fn show(&mut self) -> Result<()> { + self.input.show()?; + + Ok(()) + } +} + +impl TagCommitComponent { + /// + pub fn new(queue: Queue, theme: SharedTheme) -> Self { + Self { + queue, + input: TextInputComponent::new( + theme, + strings::TAG_COMMIT_POPUP_TITLE, + strings::TAG_COMMIT_POPUP_MSG, + ), + commit_id: None, + } + } + + /// + pub fn open(&mut self, id: CommitId) -> Result<()> { + self.commit_id = Some(id); + self.show()?; + + Ok(()) + } + + /// + pub fn tag(&mut self) { + if let Some(commit_id) = self.commit_id { + match sync::tag(CWD, &commit_id, self.input.get_text()) { + Ok(_) => { + self.input.clear(); + self.hide(); + + self.queue.borrow_mut().push_back( + InternalEvent::Update(NeedsUpdate::ALL), + ); + } + Err(e) => { + self.hide(); + log::error!("e: {}", e,); + self.queue.borrow_mut().push_back( + InternalEvent::ShowErrorMsg(format!( + "tag error:\n{}", + e, + )), + ); + } + } + } + } +} diff --git a/src/keys.rs b/src/keys.rs index dda890ad73..b6f3ef479b 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -66,5 +66,6 @@ pub const STASH_DROP: KeyEvent = with_mod(KeyCode::Char('D'), KeyModifiers::SHIFT); pub const CMD_BAR_TOGGLE: KeyEvent = no_mod(KeyCode::Char('.')); pub const LOG_COMMIT_DETAILS: KeyEvent = no_mod(KeyCode::Enter); +pub const LOG_TAG_COMMIT: KeyEvent = no_mod(KeyCode::Char('t')); pub const COMMIT_AMEND: KeyEvent = with_mod(KeyCode::Char('a'), KeyModifiers::CONTROL); diff --git a/src/queue.rs b/src/queue.rs index bed396ae53..f519b37d81 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -49,6 +49,8 @@ pub enum InternalEvent { /// InspectCommit(CommitId, Option), /// + TagCommit(CommitId), + /// OpenExternalEditor(Option), } diff --git a/src/strings.rs b/src/strings.rs index cd4fbd91b5..218a530845 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -27,6 +27,10 @@ pub static CONFIRM_MSG_STASHDROP: &str = "confirm stash drop?"; pub static CONFIRM_MSG_RESETHUNK: &str = "confirm reset hunk?"; pub static LOG_TITLE: &str = "Commit"; + +pub static TAG_COMMIT_POPUP_TITLE: &str = "Tag"; +pub static TAG_COMMIT_POPUP_MSG: &str = "type tag"; + pub static STASHLIST_TITLE: &str = "Stashes"; pub static HELP_TITLE: &str = "Help: all commands"; @@ -295,4 +299,10 @@ pub mod commands { "inspect selected commit in detail", CMD_GROUP_LOG, ); + /// + pub static LOG_TAG_COMMIT: CommandText = + CommandText::new("Tag [t]", "tag commit", CMD_GROUP_LOG); + /// + pub static TAG_COMMIT_CONFIRM_MSG: CommandText = + CommandText::new("Tag [enter]", "tag commit", CMD_GROUP_LOG); } diff --git a/src/tabs/revlog.rs b/src/tabs/revlog.rs index fbe0cc9968..f69a22c650 100644 --- a/src/tabs/revlog.rs +++ b/src/tabs/revlog.rs @@ -207,6 +207,19 @@ impl Component for Revlog { return Ok(true); } + Event::Key(keys::LOG_TAG_COMMIT) => { + return if let Some(id) = + self.selected_commit() + { + self.queue.borrow_mut().push_back( + InternalEvent::TagCommit(id), + ); + Ok(true) + } else { + Ok(false) + }; + } + Event::Key(keys::FOCUS_RIGHT) if self.commit_details.is_visible() => { @@ -257,6 +270,12 @@ impl Component for Revlog { || force_all, )); + out.push(CommandInfo::new( + commands::LOG_TAG_COMMIT, + true, + self.visible || force_all, + )); + visibility_blocking(self) }