From 55275396ead2bfce89aff81593525707878d2769 Mon Sep 17 00:00:00 2001 From: Stephan Dilly Date: Tue, 11 Jan 2022 10:28:55 +0100 Subject: [PATCH 1/4] support annotated tags --- asyncgit/src/sync/commit.rs | 61 +++++++++++++++++++----- asyncgit/src/sync/mod.rs | 4 +- asyncgit/src/sync/remotes/tags.rs | 10 ++-- asyncgit/src/sync/tags.rs | 59 +++++++++++++++++++---- src/components/commit_details/details.rs | 6 +-- src/components/commitlist.rs | 10 ++-- src/components/tag_commit.rs | 3 +- 7 files changed, 116 insertions(+), 37 deletions(-) diff --git a/asyncgit/src/sync/commit.rs b/asyncgit/src/sync/commit.rs index 558df51685..b8754581fb 100644 --- a/asyncgit/src/sync/commit.rs +++ b/asyncgit/src/sync/commit.rs @@ -95,27 +95,35 @@ pub fn commit(repo_path: &RepoPath, msg: &str) -> Result { /// /// 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( +pub fn tag_commit( repo_path: &RepoPath, commit_id: &CommitId, tag: &str, + message: Option<&str>, ) -> Result { - scope_time!("tag"); + scope_time!("tag_commit"); 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()) + let c = if let Some(message) = message { + let signature = signature_allow_undefined_name(&repo)?; + repo.tag(tag, &target, &signature, message, false)?.into() + } else { + repo.tag_lightweight(tag, &target, false)?.into() + }; + + Ok(c) } #[cfg(test)] mod tests { use crate::error::Result; + use crate::sync::tags::Tag; use crate::sync::RepoPath; use crate::sync::{ commit, get_commit_details, get_commit_files, stage_add_file, @@ -124,7 +132,7 @@ mod tests { utils::get_head, LogWalker, }; - use commit::{amend, tag}; + use commit::{amend, tag_commit}; use git2::Repository; use std::{fs::File, io::Write, path::Path}; @@ -238,25 +246,56 @@ mod tests { let new_id = commit(repo_path, "commit msg")?; - tag(repo_path, &new_id, "tag")?; + tag_commit(repo_path, &new_id, "tag", None)?; assert_eq!( get_tags(repo_path).unwrap()[&new_id], - vec!["tag"] + vec![Tag::new("tag")] ); - assert!(matches!(tag(repo_path, &new_id, "tag"), Err(_))); + assert!(matches!( + tag_commit(repo_path, &new_id, "tag", None), + Err(_) + )); assert_eq!( get_tags(repo_path).unwrap()[&new_id], - vec!["tag"] + vec![Tag::new("tag")] ); - tag(repo_path, &new_id, "second-tag")?; + tag_commit(repo_path, &new_id, "second-tag", None)?; assert_eq!( get_tags(repo_path).unwrap()[&new_id], - vec!["second-tag", "tag"] + vec![Tag::new("second-tag"), Tag::new("tag")] + ); + + Ok(()) + } + + #[test] + fn test_tag_with_message() -> Result<()> { + let file_path = Path::new("foo"); + let (_td, repo) = repo_init_empty().unwrap(); + let root = repo.path().parent().unwrap(); + let repo_path: &RepoPath = + &root.as_os_str().to_str().unwrap().into(); + + 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_commit(repo_path, &new_id, "tag", Some("tag-message"))?; + + assert_eq!( + get_tags(repo_path).unwrap()[&new_id][0] + .annotation + .as_ref() + .unwrap(), + "tag-message" ); Ok(()) diff --git a/asyncgit/src/sync/mod.rs b/asyncgit/src/sync/mod.rs index 09a43bf6e9..08f6041164 100644 --- a/asyncgit/src/sync/mod.rs +++ b/asyncgit/src/sync/mod.rs @@ -40,7 +40,7 @@ pub use branch::{ merge_rebase::merge_upstream_rebase, rename::rename_branch, validate_branch_name, BranchCompare, BranchInfo, }; -pub use commit::{amend, commit, tag}; +pub use commit::{amend, commit, tag_commit}; pub use commit_details::{ get_commit_details, CommitDetails, CommitMessage, CommitSignature, }; @@ -80,7 +80,7 @@ pub use stash::{ }; pub use state::{repo_state, RepoState}; pub use tags::{ - delete_tag, get_tags, get_tags_with_metadata, CommitTags, + delete_tag, get_tags, get_tags_with_metadata, CommitTags, Tag, TagWithMetadata, Tags, }; pub use tree::{tree_file_content, tree_files, TreeFile}; diff --git a/asyncgit/src/sync/remotes/tags.rs b/asyncgit/src/sync/remotes/tags.rs index 0c7a045f35..b712bdfffd 100644 --- a/asyncgit/src/sync/remotes/tags.rs +++ b/asyncgit/src/sync/remotes/tags.rs @@ -181,7 +181,7 @@ mod tests { let commit1 = write_commit_file(&clone1, "test.txt", "test", "commit1"); - sync::tag(clone1_dir, &commit1, "tag1").unwrap(); + sync::tag_commit(clone1_dir, &commit1, "tag1", None).unwrap(); push( clone1_dir, "origin", "master", false, false, None, None, @@ -229,7 +229,7 @@ mod tests { let commit1 = write_commit_file(&clone1, "test.txt", "test", "commit1"); - sync::tag(clone1_dir, &commit1, "tag1").unwrap(); + sync::tag_commit(clone1_dir, &commit1, "tag1", None).unwrap(); push( clone1_dir, "origin", "master", false, false, None, None, @@ -263,7 +263,7 @@ mod tests { let commit1 = write_commit_file(&clone1, "test.txt", "test", "commit1"); - sync::tag(clone1_dir, &commit1, "tag1").unwrap(); + sync::tag_commit(clone1_dir, &commit1, "tag1", None).unwrap(); push( clone1_dir, "origin", "master", false, false, None, None, @@ -305,7 +305,7 @@ mod tests { // clone1 - creates tag - sync::tag(clone1_dir, &commit1, "tag1").unwrap(); + sync::tag_commit(clone1_dir, &commit1, "tag1", None).unwrap(); let tags1 = sync::get_tags(clone1_dir).unwrap(); @@ -345,7 +345,7 @@ mod tests { // clone1 - creates tag - sync::tag(clone1_dir, &commit1, "tag1").unwrap(); + sync::tag_commit(clone1_dir, &commit1, "tag1", None).unwrap(); let tags1 = sync::get_tags(clone1_dir).unwrap(); diff --git a/asyncgit/src/sync/tags.rs b/asyncgit/src/sync/tags.rs index 2d2753beec..531505d12e 100644 --- a/asyncgit/src/sync/tags.rs +++ b/asyncgit/src/sync/tags.rs @@ -1,10 +1,32 @@ use super::{get_commits_info, CommitId, RepoPath}; -use crate::{error::Result, sync::repository::repo}; +use crate::{ + error::Result, + sync::{repository::repo, utils::bytes2string}, +}; use scopetime::scope_time; use std::collections::{BTreeMap, HashMap, HashSet}; +/// +#[derive(Clone, Hash, PartialEq, Debug)] +pub struct Tag { + /// tag name + pub name: String, + /// tag annotation + pub annotation: Option, +} + +impl Tag { + /// + pub fn new(name: &str) -> Self { + Self { + name: name.into(), + annotation: None, + } + } +} + /// all tags pointing to a single commit -pub type CommitTags = Vec; +pub type CommitTags = Vec; /// hashmap of tag target commit hash to tag names pub type Tags = BTreeMap; @@ -29,7 +51,7 @@ pub fn get_tags(repo_path: &RepoPath) -> Result { scope_time!("get_tags"); let mut res = Tags::new(); - let mut adder = |key, value: String| { + let mut adder = |key, value: Tag| { if let Some(key) = res.get_mut(&key) { key.push(value); } else { @@ -44,17 +66,31 @@ pub fn get_tags(repo_path: &RepoPath) -> Result { // skip the `refs/tags/` part String::from_utf8(name[10..name.len()].into()) { - //NOTE: find_tag (git_tag_lookup) only works on annotated tags - // lightweight tags `id` already points to the target commit + //NOTE: find_tag (using underlying git_tag_lookup) only + // works on annotated tags lightweight tags `id` already + // points to the target commit // see https://github.com/libgit2/libgit2/issues/5586 - if let Ok(commit) = repo + let commit = if let Ok(commit) = repo .find_tag(id) .and_then(|tag| tag.target()) .and_then(|target| target.peel_to_commit()) { - adder(CommitId::new(commit.id()), name); + Some(CommitId::new(commit.id())) } else if repo.find_commit(id).is_ok() { - adder(CommitId::new(id), name); + Some(CommitId::new(id)) + } else { + None + }; + + let annotation = repo + .find_tag(id) + .ok() + .as_ref() + .and_then(git2::Tag::message_bytes) + .and_then(|msg| bytes2string(msg).ok()); + + if let Some(commit) = commit { + adder(commit, Tag { name, annotation }); } return true; @@ -78,7 +114,7 @@ pub fn get_tags_with_metadata( .iter() .flat_map(|(commit_id, tags)| { tags.iter() - .map(|tag| (tag.as_ref(), commit_id)) + .map(|tag| (tag.name.as_ref(), commit_id)) .collect::>() }) .collect(); @@ -167,7 +203,10 @@ mod tests { repo.tag("b", &target, &sig, "", false).unwrap(); assert_eq!( - get_tags(repo_path).unwrap()[&CommitId::new(head_id)], + get_tags(repo_path).unwrap()[&CommitId::new(head_id)] + .iter() + .map(|t| &t.name) + .collect::>(), vec!["a", "b"] ); diff --git a/src/components/commit_details/details.rs b/src/components/commit_details/details.rs index cf219b5996..0b0e99c383 100644 --- a/src/components/commit_details/details.rs +++ b/src/components/commit_details/details.rs @@ -12,7 +12,7 @@ use crate::{ }; use anyhow::Result; use asyncgit::sync::{ - self, CommitDetails, CommitId, CommitMessage, RepoPathRef, + self, CommitDetails, CommitId, CommitMessage, RepoPathRef, Tag, }; use crossterm::event::Event; use std::clone::Clone; @@ -31,7 +31,7 @@ use super::style::Detail; pub struct DetailsComponent { repo: RepoPathRef, data: Option, - tags: Vec, + tags: Vec, theme: SharedTheme, focused: bool, current_width: Cell, @@ -224,7 +224,7 @@ impl DetailsComponent { itertools::Itertools::intersperse( self.tags.iter().map(|tag| { Span::styled( - Cow::from(tag), + Cow::from(&tag.name), self.theme.text(true, false), ) }), diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs index f88497b756..00f264167b 100644 --- a/src/components/commitlist.rs +++ b/src/components/commitlist.rs @@ -13,6 +13,7 @@ use anyhow::Result; use asyncgit::sync::{CommitId, Tags}; use chrono::{DateTime, Local}; use crossterm::event::Event; +use itertools::Itertools; use std::{ borrow::Cow, cell::Cell, cmp, convert::TryFrom, time::Instant, }; @@ -320,11 +321,10 @@ impl CommitList { .take(height) .enumerate() { - let tags = self - .tags - .as_ref() - .and_then(|t| t.get(&e.id)) - .map(|tags| tags.join(" ")); + let tags = + self.tags.as_ref().and_then(|t| t.get(&e.id)).map( + |tags| tags.iter().map(|t| &t.name).join(" "), + ); let marked = if any_marked { self.is_marked(&e.id) diff --git a/src/components/tag_commit.rs b/src/components/tag_commit.rs index 5d478105e3..9aa6286fb3 100644 --- a/src/components/tag_commit.rs +++ b/src/components/tag_commit.rs @@ -121,10 +121,11 @@ impl TagCommitComponent { /// pub fn tag(&mut self) { if let Some(commit_id) = self.commit_id { - let result = sync::tag( + let result = sync::tag_commit( &self.repo.borrow(), &commit_id, self.input.get_text(), + None, ); match result { Ok(_) => { From 0a607961715f88af4e21f7beee4e34b5cba4d9f8 Mon Sep 17 00:00:00 2001 From: Stephan Dilly Date: Tue, 11 Jan 2022 10:34:58 +0100 Subject: [PATCH 2/4] update --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index cb8f50903c..80d9c26f05 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ These are the high level goals before calling out `1.0`: * notify-based change detection ([#1](https://github.com/extrawurst/gitui/issues/1)) * interactive rebase ([#32](https://github.com/extrawurst/gitui/issues/32)) * popup history and back button ([#846](https://github.com/extrawurst/gitui/issues/846)) +* delete tag on remote ## 5. Known Limitations [Top ▲](#table-of-contents) From 186d65b87f406659559b178e473b4eeab16a7fc9 Mon Sep 17 00:00:00 2001 From: Stephan Dilly Date: Tue, 11 Jan 2022 10:41:34 +0100 Subject: [PATCH 3/4] annotation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 80d9c26f05..99c9ad2bbb 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ These are the high level goals before calling out `1.0`: * notify-based change detection ([#1](https://github.com/extrawurst/gitui/issues/1)) * interactive rebase ([#32](https://github.com/extrawurst/gitui/issues/32)) * popup history and back button ([#846](https://github.com/extrawurst/gitui/issues/846)) -* delete tag on remote +* delete tag on remote ([#1074](https://github.com/extrawurst/gitui/issues/1074)) ## 5. Known Limitations [Top ▲](#table-of-contents) From 28d7d4a03109d955e1751b213b201eae9d9e761a Mon Sep 17 00:00:00 2001 From: Stephan Dilly Date: Wed, 12 Jan 2022 12:24:02 +0100 Subject: [PATCH 4/4] allow setting tag annotation via input --- src/components/tag_commit.rs | 64 ++++++++++++++++++++++++++++++++---- src/components/textinput.rs | 5 +++ src/keys/key_list.rs | 2 ++ src/keys/key_list_file.rs | 2 ++ src/strings.rs | 28 +++++++++++++--- 5 files changed, 90 insertions(+), 11 deletions(-) diff --git a/src/components/tag_commit.rs b/src/components/tag_commit.rs index 9aa6286fb3..0af1daeea2 100644 --- a/src/components/tag_commit.rs +++ b/src/components/tag_commit.rs @@ -14,8 +14,14 @@ use asyncgit::sync::{self, CommitId, RepoPathRef}; use crossterm::event::Event; use tui::{backend::Backend, layout::Rect, Frame}; +enum Mode { + Name, + Annotation { tag_name: String }, +} + pub struct TagCommitComponent { repo: RepoPathRef, + mode: Mode, input: TextInputComponent, commit_id: Option, queue: Queue, @@ -47,8 +53,14 @@ impl Component for TagCommitComponent { strings::commands::tag_commit_confirm_msg( &self.key_config, ), + self.is_valid_tag(), true, - true, + )); + + out.push(CommandInfo::new( + strings::commands::tag_annotate_msg(&self.key_config), + self.is_valid_tag(), + matches!(self.mode, Mode::Name), )); } @@ -62,8 +74,26 @@ impl Component for TagCommitComponent { } if let Event::Key(e) = ev { - if e == self.key_config.keys.enter { + if e == self.key_config.keys.enter + && self.is_valid_tag() + { self.tag(); + } else if e == self.key_config.keys.tag_annotate + && self.is_valid_tag() + { + let tag_name: String = + self.input.get_text().into(); + + self.input.clear(); + self.input.set_title( + strings::tag_popup_annotation_title( + &tag_name, + ), + ); + self.input.set_default_msg( + strings::tag_popup_annotation_msg(), + ); + self.mode = Mode::Annotation { tag_name }; } return Ok(EventState::Consumed); @@ -81,6 +111,9 @@ impl Component for TagCommitComponent { } fn show(&mut self) -> Result<()> { + self.mode = Mode::Name; + self.input.set_title(strings::tag_popup_name_title()); + self.input.set_default_msg(strings::tag_popup_name_msg()); self.input.show()?; Ok(()) @@ -100,13 +133,14 @@ impl TagCommitComponent { input: TextInputComponent::new( theme, key_config.clone(), - &strings::tag_commit_popup_title(&key_config), - &strings::tag_commit_popup_msg(&key_config), + &strings::tag_popup_name_title(), + &strings::tag_popup_name_msg(), true, ), commit_id: None, key_config, repo, + mode: Mode::Name, } } @@ -118,14 +152,29 @@ impl TagCommitComponent { Ok(()) } + fn is_valid_tag(&self) -> bool { + !self.input.get_text().is_empty() + } + + fn tag_info(&self) -> (String, Option) { + match &self.mode { + Mode::Name => (self.input.get_text().into(), None), + Mode::Annotation { tag_name } => { + (tag_name.clone(), Some(self.input.get_text().into())) + } + } + } + /// pub fn tag(&mut self) { + let (tag_name, tag_annotation) = self.tag_info(); + if let Some(commit_id) = self.commit_id { let result = sync::tag_commit( &self.repo.borrow(), &commit_id, - self.input.get_text(), - None, + &tag_name, + tag_annotation.as_deref(), ); match result { Ok(_) => { @@ -137,7 +186,10 @@ impl TagCommitComponent { )); } Err(e) => { + // go back to tag name if something goes wrong + self.input.set_text(tag_name); self.hide(); + log::error!("e: {}", e,); self.queue.push(InternalEvent::ShowErrorMsg( format!("tag error:\n{}", e,), diff --git a/src/components/textinput.rs b/src/components/textinput.rs index f137244c12..69e098c7ac 100644 --- a/src/components/textinput.rs +++ b/src/components/textinput.rs @@ -147,6 +147,11 @@ impl TextInputComponent { self.title = t; } + /// + pub fn set_default_msg(&mut self, v: String) { + self.default_msg = v; + } + fn get_draw_text(&self) -> Text { let style = self.theme.text(true, false); diff --git a/src/keys/key_list.rs b/src/keys/key_list.rs index f028053753..fb055323b6 100644 --- a/src/keys/key_list.rs +++ b/src/keys/key_list.rs @@ -74,6 +74,7 @@ pub struct KeysList { pub abort_merge: KeyEvent, pub undo_commit: KeyEvent, pub stage_unstage_item: KeyEvent, + pub tag_annotate: KeyEvent, } #[rustfmt::skip] @@ -150,6 +151,7 @@ impl Default for KeysList { open_file_tree: KeyEvent { code: KeyCode::Char('F'), modifiers: KeyModifiers::SHIFT}, file_find: KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::empty()}, stage_unstage_item: KeyEvent { code: KeyCode::Enter, modifiers: KeyModifiers::empty()}, + tag_annotate: KeyEvent { code: KeyCode::Char('a'), modifiers: KeyModifiers::CONTROL}, } } } diff --git a/src/keys/key_list_file.rs b/src/keys/key_list_file.rs index 451c8c82ac..564bffaefb 100644 --- a/src/keys/key_list_file.rs +++ b/src/keys/key_list_file.rs @@ -78,6 +78,7 @@ pub struct KeysListFile { pub abort_merge: Option, pub undo_commit: Option, pub stage_unstage_item: Option, + pub tag_annotate: Option, } impl KeysListFile { @@ -163,6 +164,7 @@ impl KeysListFile { abort_merge: self.abort_merge.unwrap_or(default.abort_merge), undo_commit: self.undo_commit.unwrap_or(default.undo_commit), stage_unstage_item: self.stage_unstage_item.unwrap_or(default.stage_unstage_item), + tag_annotate: self.tag_annotate.unwrap_or(default.tag_annotate), } } } diff --git a/src/strings.rs b/src/strings.rs index 571d678970..a143795cb4 100644 --- a/src/strings.rs +++ b/src/strings.rs @@ -263,13 +263,17 @@ pub fn log_title(_key_config: &SharedKeyConfig) -> String { pub fn blame_title(_key_config: &SharedKeyConfig) -> String { "Blame".to_string() } -pub fn tag_commit_popup_title( - _key_config: &SharedKeyConfig, -) -> String { +pub fn tag_popup_name_title() -> String { "Tag".to_string() } -pub fn tag_commit_popup_msg(_key_config: &SharedKeyConfig) -> String { - "type tag".to_string() +pub fn tag_popup_name_msg() -> String { + "type tag name".to_string() +} +pub fn tag_popup_annotation_title(name: &str) -> String { + format!("Tag Annotation ({})", name) +} +pub fn tag_popup_annotation_msg() -> String { + "type tag annotation".to_string() } pub fn stashlist_title(_key_config: &SharedKeyConfig) -> String { "Stashes".to_string() @@ -1078,6 +1082,20 @@ pub mod commands { CMD_GROUP_LOG, ) } + + pub fn tag_annotate_msg( + key_config: &SharedKeyConfig, + ) -> CommandText { + CommandText::new( + format!( + "Annotate [{}]", + key_config.get_hint(key_config.keys.tag_annotate), + ), + "annotate tag", + CMD_GROUP_LOG, + ) + } + pub fn create_branch_confirm_msg( key_config: &SharedKeyConfig, ) -> CommandText {