mirror of
https://github.com/gitui-org/gitui
synced 2026-05-24 09:28:21 +00:00
parent
bea7edf90e
commit
2ed6f53dcf
13 changed files with 599 additions and 7 deletions
|
|
@ -68,6 +68,14 @@ impl AsyncLog {
|
||||||
Ok(list[min..max].to_vec())
|
Ok(list[min..max].to_vec())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
pub fn position(&self, id: CommitId) -> Result<Option<usize>> {
|
||||||
|
let list = self.current.lock()?;
|
||||||
|
let position = list.iter().position(|&x| x == id);
|
||||||
|
|
||||||
|
Ok(position)
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
pub fn is_pending(&self) -> bool {
|
pub fn is_pending(&self) -> bool {
|
||||||
self.pending.load(Ordering::Relaxed)
|
self.pending.load(Ordering::Relaxed)
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,10 @@ pub use stash::{
|
||||||
get_stashes, stash_apply, stash_drop, stash_pop, stash_save,
|
get_stashes, stash_apply, stash_drop, stash_pop, stash_save,
|
||||||
};
|
};
|
||||||
pub use state::{repo_state, RepoState};
|
pub use state::{repo_state, RepoState};
|
||||||
pub use tags::{get_tags, CommitTags, Tags};
|
pub use tags::{
|
||||||
|
delete_tag, get_tags, get_tags_with_metadata, CommitTags,
|
||||||
|
TagWithMetadata, Tags,
|
||||||
|
};
|
||||||
pub use tree::{tree_file_content, tree_files, TreeFile};
|
pub use tree::{tree_file_content, tree_files, TreeFile};
|
||||||
pub use utils::{
|
pub use utils::{
|
||||||
get_head, get_head_tuple, is_bare_repo, is_repo, repo_dir,
|
get_head, get_head_tuple, is_bare_repo, is_repo, repo_dir,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,29 @@
|
||||||
use super::{utils::repo, CommitId};
|
use super::{get_commits_info, utils::repo, CommitId};
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use scopetime::scope_time;
|
use scopetime::scope_time;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
|
|
||||||
/// all tags pointing to a single commit
|
/// all tags pointing to a single commit
|
||||||
pub type CommitTags = Vec<String>;
|
pub type CommitTags = Vec<String>;
|
||||||
/// hashmap of tag target commit hash to tag names
|
/// hashmap of tag target commit hash to tag names
|
||||||
pub type Tags = BTreeMap<CommitId, CommitTags>;
|
pub type Tags = BTreeMap<CommitId, CommitTags>;
|
||||||
|
|
||||||
|
///
|
||||||
|
pub struct TagWithMetadata {
|
||||||
|
///
|
||||||
|
pub name: String,
|
||||||
|
///
|
||||||
|
pub author: String,
|
||||||
|
///
|
||||||
|
pub time: i64,
|
||||||
|
///
|
||||||
|
pub message: String,
|
||||||
|
///
|
||||||
|
pub commit_id: CommitId,
|
||||||
|
}
|
||||||
|
|
||||||
|
static MAX_MESSAGE_WIDTH: usize = 100;
|
||||||
|
|
||||||
/// returns `Tags` type filled with all tags found in repo
|
/// returns `Tags` type filled with all tags found in repo
|
||||||
pub fn get_tags(repo_path: &str) -> Result<Tags> {
|
pub fn get_tags(repo_path: &str) -> Result<Tags> {
|
||||||
scope_time!("get_tags");
|
scope_time!("get_tags");
|
||||||
|
|
@ -31,8 +47,12 @@ pub fn get_tags(repo_path: &str) -> Result<Tags> {
|
||||||
//NOTE: find_tag (git_tag_lookup) only works on annotated tags
|
//NOTE: find_tag (git_tag_lookup) only works on annotated tags
|
||||||
// lightweight tags `id` already points to the target commit
|
// lightweight tags `id` already points to the target commit
|
||||||
// see https://github.com/libgit2/libgit2/issues/5586
|
// see https://github.com/libgit2/libgit2/issues/5586
|
||||||
if let Ok(tag) = repo.find_tag(id) {
|
if let Ok(commit) = repo
|
||||||
adder(CommitId::new(tag.target_id()), name);
|
.find_tag(id)
|
||||||
|
.and_then(|tag| tag.target())
|
||||||
|
.and_then(|target| target.peel_to_commit())
|
||||||
|
{
|
||||||
|
adder(CommitId::new(commit.id()), name);
|
||||||
} else if repo.find_commit(id).is_ok() {
|
} else if repo.find_commit(id).is_ok() {
|
||||||
adder(CommitId::new(id), name);
|
adder(CommitId::new(id), name);
|
||||||
}
|
}
|
||||||
|
|
@ -45,6 +65,69 @@ pub fn get_tags(repo_path: &str) -> Result<Tags> {
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
pub fn get_tags_with_metadata(
|
||||||
|
repo_path: &str,
|
||||||
|
) -> Result<Vec<TagWithMetadata>> {
|
||||||
|
scope_time!("get_tags_with_metadata");
|
||||||
|
|
||||||
|
let tags_grouped_by_commit_id = get_tags(repo_path)?;
|
||||||
|
|
||||||
|
let tags_with_commit_id: Vec<(&str, &CommitId)> =
|
||||||
|
tags_grouped_by_commit_id
|
||||||
|
.iter()
|
||||||
|
.flat_map(|(commit_id, tags)| {
|
||||||
|
tags.iter()
|
||||||
|
.map(|tag| (tag.as_ref(), commit_id))
|
||||||
|
.collect::<Vec<(&str, &CommitId)>>()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let unique_commit_ids: HashSet<_> = tags_with_commit_id
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(|(_, &commit_id)| commit_id)
|
||||||
|
.collect();
|
||||||
|
let mut commit_ids = Vec::with_capacity(unique_commit_ids.len());
|
||||||
|
commit_ids.extend(unique_commit_ids);
|
||||||
|
|
||||||
|
let commit_infos =
|
||||||
|
get_commits_info(repo_path, &commit_ids, MAX_MESSAGE_WIDTH)?;
|
||||||
|
let unique_commit_infos: HashMap<_, _> = commit_infos
|
||||||
|
.iter()
|
||||||
|
.map(|commit_info| (commit_info.id, commit_info))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut tags: Vec<TagWithMetadata> = tags_with_commit_id
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(tag, commit_id)| {
|
||||||
|
unique_commit_infos.get(commit_id).map(|commit_info| {
|
||||||
|
TagWithMetadata {
|
||||||
|
name: String::from(tag),
|
||||||
|
author: commit_info.author.clone(),
|
||||||
|
time: commit_info.time,
|
||||||
|
message: commit_info.message.clone(),
|
||||||
|
commit_id: *commit_id,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
tags.sort_unstable_by(|a, b| b.time.cmp(&a.time));
|
||||||
|
|
||||||
|
Ok(tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
pub fn delete_tag(repo_path: &str, tag_name: &str) -> Result<()> {
|
||||||
|
scope_time!("delete_tag");
|
||||||
|
|
||||||
|
let repo = repo(repo_path)?;
|
||||||
|
repo.tag_delete(tag_name)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -82,5 +165,26 @@ mod tests {
|
||||||
get_tags(repo_path).unwrap()[&CommitId::new(head_id)],
|
get_tags(repo_path).unwrap()[&CommitId::new(head_id)],
|
||||||
vec!["a", "b"]
|
vec!["a", "b"]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let tags = get_tags_with_metadata(repo_path).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(tags.len(), 2);
|
||||||
|
assert_eq!(tags[0].name, "a");
|
||||||
|
assert_eq!(tags[0].message, "initial");
|
||||||
|
assert_eq!(tags[1].name, "b");
|
||||||
|
assert_eq!(tags[1].message, "initial");
|
||||||
|
assert_eq!(tags[0].commit_id, tags[1].commit_id);
|
||||||
|
|
||||||
|
delete_tag(repo_path, "a").unwrap();
|
||||||
|
|
||||||
|
let tags = get_tags(repo_path).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(tags.len(), 1);
|
||||||
|
|
||||||
|
delete_tag(repo_path, "b").unwrap();
|
||||||
|
|
||||||
|
let tags = get_tags(repo_path).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(tags.len(), 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
38
src/app.rs
38
src/app.rs
|
|
@ -9,7 +9,7 @@ use crate::{
|
||||||
InspectCommitComponent, MsgComponent, PullComponent,
|
InspectCommitComponent, MsgComponent, PullComponent,
|
||||||
PushComponent, PushTagsComponent, RenameBranchComponent,
|
PushComponent, PushTagsComponent, RenameBranchComponent,
|
||||||
ResetComponent, RevisionFilesComponent, StashMsgComponent,
|
ResetComponent, RevisionFilesComponent, StashMsgComponent,
|
||||||
TagCommitComponent,
|
TagCommitComponent, TagListComponent,
|
||||||
},
|
},
|
||||||
input::{Input, InputEvent, InputState},
|
input::{Input, InputEvent, InputState},
|
||||||
keys::{KeyConfig, SharedKeyConfig},
|
keys::{KeyConfig, SharedKeyConfig},
|
||||||
|
|
@ -54,6 +54,7 @@ pub struct App {
|
||||||
create_branch_popup: CreateBranchComponent,
|
create_branch_popup: CreateBranchComponent,
|
||||||
rename_branch_popup: RenameBranchComponent,
|
rename_branch_popup: RenameBranchComponent,
|
||||||
select_branch_popup: BranchListComponent,
|
select_branch_popup: BranchListComponent,
|
||||||
|
tags_popup: TagListComponent,
|
||||||
cmdbar: RefCell<CommandBar>,
|
cmdbar: RefCell<CommandBar>,
|
||||||
tab: usize,
|
tab: usize,
|
||||||
revlog: Revlog,
|
revlog: Revlog,
|
||||||
|
|
@ -162,6 +163,11 @@ impl App {
|
||||||
theme.clone(),
|
theme.clone(),
|
||||||
key_config.clone(),
|
key_config.clone(),
|
||||||
),
|
),
|
||||||
|
tags_popup: TagListComponent::new(
|
||||||
|
&queue,
|
||||||
|
theme.clone(),
|
||||||
|
key_config.clone(),
|
||||||
|
),
|
||||||
do_quit: false,
|
do_quit: false,
|
||||||
cmdbar: RefCell::new(CommandBar::new(
|
cmdbar: RefCell::new(CommandBar::new(
|
||||||
theme.clone(),
|
theme.clone(),
|
||||||
|
|
@ -397,6 +403,7 @@ impl App {
|
||||||
rename_branch_popup,
|
rename_branch_popup,
|
||||||
select_branch_popup,
|
select_branch_popup,
|
||||||
revision_files_popup,
|
revision_files_popup,
|
||||||
|
tags_popup,
|
||||||
help,
|
help,
|
||||||
revlog,
|
revlog,
|
||||||
status_tab,
|
status_tab,
|
||||||
|
|
@ -550,11 +557,26 @@ impl App {
|
||||||
InternalEvent::SelectBranch => {
|
InternalEvent::SelectBranch => {
|
||||||
self.select_branch_popup.open()?;
|
self.select_branch_popup.open()?;
|
||||||
}
|
}
|
||||||
|
InternalEvent::Tags => {
|
||||||
|
self.tags_popup.open()?;
|
||||||
|
}
|
||||||
InternalEvent::TabSwitch => self.set_tab(0)?,
|
InternalEvent::TabSwitch => self.set_tab(0)?,
|
||||||
InternalEvent::InspectCommit(id, tags) => {
|
InternalEvent::InspectCommit(id, tags) => {
|
||||||
self.inspect_commit_popup.open(id, tags)?;
|
self.inspect_commit_popup.open(id, tags)?;
|
||||||
flags.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS)
|
flags.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS)
|
||||||
}
|
}
|
||||||
|
InternalEvent::SelectCommitInRevlog(id) => {
|
||||||
|
if let Err(error) = self.revlog.select_commit(id) {
|
||||||
|
self.queue.borrow_mut().push_back(
|
||||||
|
InternalEvent::ShowErrorMsg(
|
||||||
|
error.to_string(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
self.tags_popup.hide();
|
||||||
|
flags.insert(NeedsUpdate::ALL)
|
||||||
|
}
|
||||||
|
}
|
||||||
InternalEvent::OpenExternalEditor(path) => {
|
InternalEvent::OpenExternalEditor(path) => {
|
||||||
self.input.set_polling(false);
|
self.input.set_polling(false);
|
||||||
self.external_editor_popup.show()?;
|
self.external_editor_popup.show()?;
|
||||||
|
|
@ -620,6 +642,18 @@ impl App {
|
||||||
self.select_branch_popup.update_branches()?;
|
self.select_branch_popup.update_branches()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Action::DeleteTag(tag_name) => {
|
||||||
|
if let Err(error) = sync::delete_tag(CWD, &tag_name) {
|
||||||
|
self.queue.borrow_mut().push_back(
|
||||||
|
InternalEvent::ShowErrorMsg(
|
||||||
|
error.to_string(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
flags.insert(NeedsUpdate::ALL);
|
||||||
|
self.tags_popup.update_tags()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Action::ForcePush(branch, force) => self
|
Action::ForcePush(branch, force) => self
|
||||||
.queue
|
.queue
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
|
|
@ -696,6 +730,7 @@ impl App {
|
||||||
|| self.push_tags_popup.is_visible()
|
|| self.push_tags_popup.is_visible()
|
||||||
|| self.pull_popup.is_visible()
|
|| self.pull_popup.is_visible()
|
||||||
|| self.select_branch_popup.is_visible()
|
|| self.select_branch_popup.is_visible()
|
||||||
|
|| self.tags_popup.is_visible()
|
||||||
|| self.rename_branch_popup.is_visible()
|
|| self.rename_branch_popup.is_visible()
|
||||||
|| self.revision_files_popup.is_visible()
|
|| self.revision_files_popup.is_visible()
|
||||||
}
|
}
|
||||||
|
|
@ -723,6 +758,7 @@ impl App {
|
||||||
self.external_editor_popup.draw(f, size)?;
|
self.external_editor_popup.draw(f, size)?;
|
||||||
self.tag_commit_popup.draw(f, size)?;
|
self.tag_commit_popup.draw(f, size)?;
|
||||||
self.select_branch_popup.draw(f, size)?;
|
self.select_branch_popup.draw(f, size)?;
|
||||||
|
self.tags_popup.draw(f, size)?;
|
||||||
self.create_branch_popup.draw(f, size)?;
|
self.create_branch_popup.draw(f, size)?;
|
||||||
self.rename_branch_popup.draw(f, size)?;
|
self.rename_branch_popup.draw(f, size)?;
|
||||||
self.revision_files_popup.draw(f, size)?;
|
self.revision_files_popup.draw(f, size)?;
|
||||||
|
|
|
||||||
|
|
@ -287,6 +287,10 @@ impl CommitList {
|
||||||
fn relative_selection(&self) -> usize {
|
fn relative_selection(&self) -> usize {
|
||||||
self.selection.saturating_sub(self.items.index_offset())
|
self.selection.saturating_sub(self.items.index_offset())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn select_entry(&mut self, position: usize) {
|
||||||
|
self.selection = position;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DrawableComponent for CommitList {
|
impl DrawableComponent for CommitList {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ mod revision_files;
|
||||||
mod stashmsg;
|
mod stashmsg;
|
||||||
mod syntax_text;
|
mod syntax_text;
|
||||||
mod tag_commit;
|
mod tag_commit;
|
||||||
|
mod taglist;
|
||||||
mod textinput;
|
mod textinput;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
|
|
@ -48,6 +49,7 @@ pub use revision_files::RevisionFilesComponent;
|
||||||
pub use stashmsg::StashMsgComponent;
|
pub use stashmsg::StashMsgComponent;
|
||||||
pub use syntax_text::SyntaxTextComponent;
|
pub use syntax_text::SyntaxTextComponent;
|
||||||
pub use tag_commit::TagCommitComponent;
|
pub use tag_commit::TagCommitComponent;
|
||||||
|
pub use taglist::TagListComponent;
|
||||||
pub use textinput::{InputType, TextInputComponent};
|
pub use textinput::{InputType, TextInputComponent};
|
||||||
pub use utils::filetree::FileTreeItemKind;
|
pub use utils::filetree::FileTreeItemKind;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,15 @@ impl ResetComponent {
|
||||||
branch_ref,
|
branch_ref,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Action::DeleteTag(tag_name) => (
|
||||||
|
strings::confirm_title_delete_tag(
|
||||||
|
&self.key_config,
|
||||||
|
),
|
||||||
|
strings::confirm_msg_delete_tag(
|
||||||
|
&self.key_config,
|
||||||
|
tag_name,
|
||||||
|
),
|
||||||
|
),
|
||||||
Action::ForcePush(branch, _force) => (
|
Action::ForcePush(branch, _force) => (
|
||||||
strings::confirm_title_force_push(
|
strings::confirm_title_force_push(
|
||||||
&self.key_config,
|
&self.key_config,
|
||||||
|
|
|
||||||
339
src/components/taglist.rs
Normal file
339
src/components/taglist.rs
Normal file
|
|
@ -0,0 +1,339 @@
|
||||||
|
use super::{
|
||||||
|
utils, visibility_blocking, CommandBlocking, CommandInfo,
|
||||||
|
Component, DrawableComponent, EventState,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
components::ScrollType,
|
||||||
|
keys::SharedKeyConfig,
|
||||||
|
queue::{Action, InternalEvent, Queue},
|
||||||
|
strings,
|
||||||
|
ui::{self, Size},
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use asyncgit::{
|
||||||
|
sync::{get_tags_with_metadata, TagWithMetadata},
|
||||||
|
CWD,
|
||||||
|
};
|
||||||
|
use crossterm::event::Event;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use tui::{
|
||||||
|
backend::Backend,
|
||||||
|
layout::{Constraint, Margin, Rect},
|
||||||
|
text::Span,
|
||||||
|
widgets::{
|
||||||
|
Block, BorderType, Borders, Cell, Clear, Row, Table,
|
||||||
|
TableState,
|
||||||
|
},
|
||||||
|
Frame,
|
||||||
|
};
|
||||||
|
use ui::style::SharedTheme;
|
||||||
|
|
||||||
|
///
|
||||||
|
pub struct TagListComponent {
|
||||||
|
theme: SharedTheme,
|
||||||
|
queue: Queue,
|
||||||
|
tags: Option<Vec<TagWithMetadata>>,
|
||||||
|
visible: bool,
|
||||||
|
table_state: std::cell::Cell<TableState>,
|
||||||
|
current_height: std::cell::Cell<usize>,
|
||||||
|
key_config: SharedKeyConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DrawableComponent for TagListComponent {
|
||||||
|
fn draw<B: Backend>(
|
||||||
|
&self,
|
||||||
|
f: &mut Frame<B>,
|
||||||
|
rect: Rect,
|
||||||
|
) -> Result<()> {
|
||||||
|
if self.visible {
|
||||||
|
const PERCENT_SIZE: Size = Size::new(80, 50);
|
||||||
|
const MIN_SIZE: Size = Size::new(60, 20);
|
||||||
|
|
||||||
|
let area = ui::centered_rect(
|
||||||
|
PERCENT_SIZE.width,
|
||||||
|
PERCENT_SIZE.height,
|
||||||
|
f.size(),
|
||||||
|
);
|
||||||
|
let area =
|
||||||
|
ui::rect_inside(MIN_SIZE, f.size().into(), area);
|
||||||
|
let area = area.intersection(rect);
|
||||||
|
|
||||||
|
let tag_name_width =
|
||||||
|
self.tags.as_ref().map_or(0, |tags| {
|
||||||
|
tags.iter()
|
||||||
|
.fold(0, |acc, tag| acc.max(tag.name.len()))
|
||||||
|
});
|
||||||
|
|
||||||
|
let constraints = [
|
||||||
|
// tag name
|
||||||
|
Constraint::Length(tag_name_width.try_into()?),
|
||||||
|
// commit date
|
||||||
|
Constraint::Length(10),
|
||||||
|
// author width
|
||||||
|
Constraint::Length(19),
|
||||||
|
// commit id
|
||||||
|
Constraint::Min(0),
|
||||||
|
];
|
||||||
|
|
||||||
|
let rows = self.get_rows();
|
||||||
|
let number_of_rows = rows.len();
|
||||||
|
|
||||||
|
let table = Table::new(rows)
|
||||||
|
.widths(&constraints)
|
||||||
|
.column_spacing(1)
|
||||||
|
.highlight_style(self.theme.text(true, true))
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.title(Span::styled(
|
||||||
|
strings::title_tags(),
|
||||||
|
self.theme.title(true),
|
||||||
|
))
|
||||||
|
.border_style(self.theme.block(true))
|
||||||
|
.border_type(BorderType::Thick),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut table_state = self.table_state.take();
|
||||||
|
|
||||||
|
f.render_widget(Clear, area);
|
||||||
|
f.render_stateful_widget(table, area, &mut table_state);
|
||||||
|
|
||||||
|
let area = area.inner(&Margin {
|
||||||
|
vertical: 1,
|
||||||
|
horizontal: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
ui::draw_scrollbar(
|
||||||
|
f,
|
||||||
|
area,
|
||||||
|
&self.theme,
|
||||||
|
number_of_rows,
|
||||||
|
table_state.selected().unwrap_or(0),
|
||||||
|
);
|
||||||
|
|
||||||
|
self.table_state.set(table_state);
|
||||||
|
self.current_height.set(area.height.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for TagListComponent {
|
||||||
|
fn commands(
|
||||||
|
&self,
|
||||||
|
out: &mut Vec<CommandInfo>,
|
||||||
|
force_all: bool,
|
||||||
|
) -> CommandBlocking {
|
||||||
|
if self.visible || force_all {
|
||||||
|
out.clear();
|
||||||
|
|
||||||
|
out.push(CommandInfo::new(
|
||||||
|
strings::commands::scroll(&self.key_config),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
));
|
||||||
|
|
||||||
|
out.push(CommandInfo::new(
|
||||||
|
strings::commands::close_popup(&self.key_config),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
));
|
||||||
|
|
||||||
|
out.push(CommandInfo::new(
|
||||||
|
strings::commands::delete_tag_popup(&self.key_config),
|
||||||
|
self.valid_selection(),
|
||||||
|
true,
|
||||||
|
));
|
||||||
|
out.push(CommandInfo::new(
|
||||||
|
strings::commands::select_tag(&self.key_config),
|
||||||
|
self.valid_selection(),
|
||||||
|
true,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
visibility_blocking(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event(&mut self, event: Event) -> Result<EventState> {
|
||||||
|
if self.visible {
|
||||||
|
if let Event::Key(key) = event {
|
||||||
|
if key == self.key_config.exit_popup {
|
||||||
|
self.hide()
|
||||||
|
} else if key == self.key_config.move_up {
|
||||||
|
self.move_selection(ScrollType::Up);
|
||||||
|
} else if key == self.key_config.move_down {
|
||||||
|
self.move_selection(ScrollType::Down);
|
||||||
|
} else if key == self.key_config.shift_up
|
||||||
|
|| key == self.key_config.home
|
||||||
|
{
|
||||||
|
self.move_selection(ScrollType::Home);
|
||||||
|
} else if key == self.key_config.shift_down
|
||||||
|
|| key == self.key_config.end
|
||||||
|
{
|
||||||
|
self.move_selection(ScrollType::End);
|
||||||
|
} else if key == self.key_config.page_down {
|
||||||
|
self.move_selection(ScrollType::PageDown);
|
||||||
|
} else if key == self.key_config.page_up {
|
||||||
|
self.move_selection(ScrollType::PageUp);
|
||||||
|
} else if key == self.key_config.delete_tag {
|
||||||
|
return self.selected_tag().map_or(
|
||||||
|
Ok(EventState::NotConsumed),
|
||||||
|
|tag| {
|
||||||
|
self.queue.borrow_mut().push_back(
|
||||||
|
InternalEvent::ConfirmAction(
|
||||||
|
Action::DeleteTag(
|
||||||
|
tag.name.clone(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Ok(EventState::Consumed)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if key == self.key_config.select_tag {
|
||||||
|
return self.selected_tag().map_or(
|
||||||
|
Ok(EventState::NotConsumed),
|
||||||
|
|tag| {
|
||||||
|
self.queue.borrow_mut().push_back(
|
||||||
|
InternalEvent::SelectCommitInRevlog(
|
||||||
|
tag.commit_id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Ok(EventState::Consumed)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(EventState::Consumed)
|
||||||
|
} else {
|
||||||
|
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;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TagListComponent {
|
||||||
|
pub fn new(
|
||||||
|
queue: &Queue,
|
||||||
|
theme: SharedTheme,
|
||||||
|
key_config: SharedKeyConfig,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
theme,
|
||||||
|
queue: queue.clone(),
|
||||||
|
tags: None,
|
||||||
|
visible: false,
|
||||||
|
table_state: std::cell::Cell::new(TableState::default()),
|
||||||
|
current_height: std::cell::Cell::new(0),
|
||||||
|
key_config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
pub fn open(&mut self) -> Result<()> {
|
||||||
|
self.table_state.get_mut().select(Some(0));
|
||||||
|
self.show()?;
|
||||||
|
|
||||||
|
self.update_tags()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// fetch list of tags
|
||||||
|
pub fn update_tags(&mut self) -> Result<()> {
|
||||||
|
let tags = get_tags_with_metadata(CWD)?;
|
||||||
|
|
||||||
|
self.tags = Some(tags);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
fn move_selection(&mut self, scroll_type: ScrollType) -> bool {
|
||||||
|
let mut table_state = self.table_state.take();
|
||||||
|
|
||||||
|
let old_selection = table_state.selected().unwrap_or(0);
|
||||||
|
let max_selection =
|
||||||
|
self.tags.as_ref().map_or(0, |tags| tags.len() - 1);
|
||||||
|
|
||||||
|
let new_selection = match scroll_type {
|
||||||
|
ScrollType::Up => old_selection.saturating_sub(1),
|
||||||
|
ScrollType::Down => {
|
||||||
|
old_selection.saturating_add(1).min(max_selection)
|
||||||
|
}
|
||||||
|
ScrollType::Home => 0,
|
||||||
|
ScrollType::End => max_selection,
|
||||||
|
ScrollType::PageUp => old_selection.saturating_sub(
|
||||||
|
self.current_height.get().saturating_sub(1),
|
||||||
|
),
|
||||||
|
ScrollType::PageDown => old_selection
|
||||||
|
.saturating_add(
|
||||||
|
self.current_height.get().saturating_sub(1),
|
||||||
|
)
|
||||||
|
.min(max_selection),
|
||||||
|
};
|
||||||
|
|
||||||
|
let needs_update = new_selection != old_selection;
|
||||||
|
|
||||||
|
table_state.select(Some(new_selection));
|
||||||
|
self.table_state.set(table_state);
|
||||||
|
|
||||||
|
needs_update
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
fn get_rows(&self) -> Vec<Row> {
|
||||||
|
if let Some(ref tags) = self.tags {
|
||||||
|
tags.iter().map(|tag| self.get_row(tag)).collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
fn get_row(&self, tag: &TagWithMetadata) -> Row {
|
||||||
|
let cells: Vec<Cell> = vec![
|
||||||
|
Cell::from(tag.name.clone())
|
||||||
|
.style(self.theme.text(true, false)),
|
||||||
|
Cell::from(utils::time_to_string(tag.time, true))
|
||||||
|
.style(self.theme.commit_time(false)),
|
||||||
|
Cell::from(tag.author.clone())
|
||||||
|
.style(self.theme.commit_author(false)),
|
||||||
|
Cell::from(tag.message.clone())
|
||||||
|
.style(self.theme.text(true, false)),
|
||||||
|
];
|
||||||
|
|
||||||
|
Row::new(cells)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_selection(&self) -> bool {
|
||||||
|
self.selected_tag().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_tag(&self) -> Option<&TagWithMetadata> {
|
||||||
|
self.tags.as_ref().and_then(|tags| {
|
||||||
|
let table_state = self.table_state.take();
|
||||||
|
|
||||||
|
let tag = table_state
|
||||||
|
.selected()
|
||||||
|
.and_then(|selected| tags.get(selected));
|
||||||
|
|
||||||
|
self.table_state.set(table_state);
|
||||||
|
|
||||||
|
tag
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -72,6 +72,9 @@ pub struct KeyConfig {
|
||||||
pub select_branch: KeyEvent,
|
pub select_branch: KeyEvent,
|
||||||
pub delete_branch: KeyEvent,
|
pub delete_branch: KeyEvent,
|
||||||
pub merge_branch: KeyEvent,
|
pub merge_branch: KeyEvent,
|
||||||
|
pub tags: KeyEvent,
|
||||||
|
pub delete_tag: KeyEvent,
|
||||||
|
pub select_tag: KeyEvent,
|
||||||
pub push: KeyEvent,
|
pub push: KeyEvent,
|
||||||
pub open_file_tree: KeyEvent,
|
pub open_file_tree: KeyEvent,
|
||||||
pub force_push: KeyEvent,
|
pub force_push: KeyEvent,
|
||||||
|
|
@ -134,6 +137,9 @@ impl Default for KeyConfig {
|
||||||
select_branch: KeyEvent { code: KeyCode::Char('b'), modifiers: KeyModifiers::empty()},
|
select_branch: KeyEvent { code: KeyCode::Char('b'), modifiers: KeyModifiers::empty()},
|
||||||
delete_branch: KeyEvent { code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT},
|
delete_branch: KeyEvent { code: KeyCode::Char('D'), modifiers: KeyModifiers::SHIFT},
|
||||||
merge_branch: KeyEvent { code: KeyCode::Char('m'), modifiers: KeyModifiers::empty()},
|
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()},
|
push: KeyEvent { code: KeyCode::Char('p'), modifiers: KeyModifiers::empty()},
|
||||||
force_push: KeyEvent { code: KeyCode::Char('P'), modifiers: KeyModifiers::SHIFT},
|
force_push: KeyEvent { code: KeyCode::Char('P'), modifiers: KeyModifiers::SHIFT},
|
||||||
pull: KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::empty()},
|
pull: KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::empty()},
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ pub enum Action {
|
||||||
StashDrop(CommitId),
|
StashDrop(CommitId),
|
||||||
StashPop(CommitId),
|
StashPop(CommitId),
|
||||||
DeleteBranch(String),
|
DeleteBranch(String),
|
||||||
|
DeleteTag(String),
|
||||||
ForcePush(String, bool),
|
ForcePush(String, bool),
|
||||||
PullMerge { incoming: usize, rebase: bool },
|
PullMerge { incoming: usize, rebase: bool },
|
||||||
AbortMerge,
|
AbortMerge,
|
||||||
|
|
@ -59,8 +60,12 @@ pub enum InternalEvent {
|
||||||
///
|
///
|
||||||
InspectCommit(CommitId, Option<CommitTags>),
|
InspectCommit(CommitId, Option<CommitTags>),
|
||||||
///
|
///
|
||||||
|
SelectCommitInRevlog(CommitId),
|
||||||
|
///
|
||||||
TagCommit(CommitId),
|
TagCommit(CommitId),
|
||||||
///
|
///
|
||||||
|
Tags,
|
||||||
|
///
|
||||||
BlameFile(String),
|
BlameFile(String),
|
||||||
///
|
///
|
||||||
CreateBranch,
|
CreateBranch,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@ pub static PUSH_TAGS_STATES_DONE: &str = "done";
|
||||||
pub fn title_branches() -> String {
|
pub fn title_branches() -> String {
|
||||||
"Branches".to_string()
|
"Branches".to_string()
|
||||||
}
|
}
|
||||||
|
pub fn title_tags() -> String {
|
||||||
|
"Tags".to_string()
|
||||||
|
}
|
||||||
pub fn title_status(_key_config: &SharedKeyConfig) -> String {
|
pub fn title_status(_key_config: &SharedKeyConfig) -> String {
|
||||||
"Unstaged Changes".to_string()
|
"Unstaged Changes".to_string()
|
||||||
}
|
}
|
||||||
|
|
@ -165,6 +168,17 @@ pub fn confirm_msg_delete_branch(
|
||||||
) -> String {
|
) -> String {
|
||||||
format!("Confirm deleting branch: '{}' ?", branch_ref)
|
format!("Confirm deleting branch: '{}' ?", branch_ref)
|
||||||
}
|
}
|
||||||
|
pub fn confirm_title_delete_tag(
|
||||||
|
_key_config: &SharedKeyConfig,
|
||||||
|
) -> String {
|
||||||
|
"Delete Tag".to_string()
|
||||||
|
}
|
||||||
|
pub fn confirm_msg_delete_tag(
|
||||||
|
_key_config: &SharedKeyConfig,
|
||||||
|
tag_name: &str,
|
||||||
|
) -> String {
|
||||||
|
format!("Confirm deleting Tag: '{}' ?", tag_name)
|
||||||
|
}
|
||||||
pub fn confirm_title_force_push(
|
pub fn confirm_title_force_push(
|
||||||
_key_config: &SharedKeyConfig,
|
_key_config: &SharedKeyConfig,
|
||||||
) -> String {
|
) -> String {
|
||||||
|
|
@ -999,6 +1013,41 @@ pub mod commands {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn open_tags_popup(
|
||||||
|
key_config: &SharedKeyConfig,
|
||||||
|
) -> CommandText {
|
||||||
|
CommandText::new(
|
||||||
|
format!(
|
||||||
|
"Tags [{}]",
|
||||||
|
key_config.get_hint(key_config.tags),
|
||||||
|
),
|
||||||
|
"open tags popup",
|
||||||
|
CMD_GROUP_GENERAL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pub fn delete_tag_popup(
|
||||||
|
key_config: &SharedKeyConfig,
|
||||||
|
) -> CommandText {
|
||||||
|
CommandText::new(
|
||||||
|
format!(
|
||||||
|
"Delete [{}]",
|
||||||
|
key_config.get_hint(key_config.delete_tag),
|
||||||
|
),
|
||||||
|
"delete a tag",
|
||||||
|
CMD_GROUP_GENERAL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
pub fn select_tag(key_config: &SharedKeyConfig) -> CommandText {
|
||||||
|
CommandText::new(
|
||||||
|
format!(
|
||||||
|
"Select commit [{}]",
|
||||||
|
key_config.get_hint(key_config.select_tag),
|
||||||
|
),
|
||||||
|
"Select commit in revlog",
|
||||||
|
CMD_GROUP_GENERAL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn status_push(key_config: &SharedKeyConfig) -> CommandText {
|
pub fn status_push(key_config: &SharedKeyConfig) -> CommandText {
|
||||||
CommandText::new(
|
CommandText::new(
|
||||||
format!(
|
format!(
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,18 @@ impl Revlog {
|
||||||
tags.and_then(|tags| tags.get(&commit).cloned())
|
tags.and_then(|tags| tags.get(&commit).cloned())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn select_commit(&mut self, id: CommitId) -> Result<()> {
|
||||||
|
let position = self.git_log.position(id)?;
|
||||||
|
|
||||||
|
if let Some(position) = position {
|
||||||
|
self.list.select_entry(position);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("Could not select commit in revlog. It might not be loaded yet or it might be on a different branch.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DrawableComponent for Revlog {
|
impl DrawableComponent for Revlog {
|
||||||
|
|
@ -259,6 +271,11 @@ impl Component for Revlog {
|
||||||
Ok(EventState::Consumed)
|
Ok(EventState::Consumed)
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
} else if k == self.key_config.tags {
|
||||||
|
self.queue
|
||||||
|
.borrow_mut()
|
||||||
|
.push_back(InternalEvent::Tags);
|
||||||
|
return Ok(EventState::Consumed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -302,6 +319,12 @@ impl Component for Revlog {
|
||||||
self.visible || force_all,
|
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(
|
out.push(CommandInfo::new(
|
||||||
strings::commands::copy_hash(&self.key_config),
|
strings::commands::copy_hash(&self.key_config),
|
||||||
self.selected_commit().is_some(),
|
self.selected_commit().is_some(),
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@
|
||||||
status_stage_all: ( code: Char('a'), modifiers: ( bits: 0,),),
|
status_stage_all: ( code: Char('a'), modifiers: ( bits: 0,),),
|
||||||
status_reset_item: ( code: Char('U'), modifiers: ( bits: 1,),),
|
status_reset_item: ( code: Char('U'), modifiers: ( bits: 1,),),
|
||||||
status_ignore_file: ( code: Char('i'), modifiers: ( bits: 0,),),
|
status_ignore_file: ( code: Char('i'), modifiers: ( bits: 0,),),
|
||||||
|
|
||||||
diff_reset_lines: ( code: Char('u'), modifiers: ( bits: 0,),),
|
diff_reset_lines: ( code: Char('u'), modifiers: ( bits: 0,),),
|
||||||
diff_stage_lines: ( code: Char('s'), modifiers: ( bits: 0,),),
|
diff_stage_lines: ( code: Char('s'), modifiers: ( bits: 0,),),
|
||||||
|
|
||||||
|
|
@ -79,6 +79,10 @@
|
||||||
merge_branch: ( code: Char('m'), modifiers: ( bits: 0,),),
|
merge_branch: ( code: Char('m'), modifiers: ( bits: 0,),),
|
||||||
abort_merge: ( code: Char('M'), modifiers: ( bits: 1,),),
|
abort_merge: ( code: Char('M'), 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,),),
|
||||||
|
|
||||||
push: ( code: Char('p'), modifiers: ( bits: 0,),),
|
push: ( code: Char('p'), modifiers: ( bits: 0,),),
|
||||||
force_push: ( code: Char('P'), modifiers: ( bits: 1,),),
|
force_push: ( code: Char('P'), modifiers: ( bits: 1,),),
|
||||||
pull: ( code: Char('f'), modifiers: ( bits: 0,),),
|
pull: ( code: Char('f'), modifiers: ( bits: 0,),),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue