use std::collections::HashSet;
use std::rc::Rc;
use clap_complete::ArgValueCandidates;
use futures::TryStreamExt as _;
use jj_lib::repo::Repo as _;
use jj_lib::revset::RevsetExpression;
use jj_lib::str_util::StringExpression;
use super::warn_unmatched_local_or_remote_bookmarks;
use crate::cli_util::CommandHelper;
use crate::cli_util::RevisionArg;
use crate::cli_util::default_ignored_remote_name;
use crate::command_error::CommandError;
use crate::commit_ref_list;
use crate::commit_ref_list::RefFilterPredicates;
use crate::commit_ref_list::SortKey;
use crate::commit_templater::CommitRef;
use crate::complete;
use crate::revset_util::parse_union_name_patterns;
use crate::templater::TemplateRenderer;
use crate::ui::Ui;
#[derive(clap::Args, Clone, Debug)]
pub struct BookmarkListArgs {
#[arg(long, short, alias = "all")]
all_remotes: bool,
#[arg(long = "remote", value_name = "REMOTE", conflicts_with = "all_remotes")]
#[arg(add = ArgValueCandidates::new(complete::git_remotes))]
remotes: Option<Vec<String>>,
#[arg(long, short, conflicts_with = "all_remotes")]
tracked: bool,
#[arg(long, short, conflicts_with = "all_remotes")]
conflicted: bool,
#[arg(add = ArgValueCandidates::new(complete::bookmarks))]
names: Option<Vec<String>>,
#[arg(long = "revision", short, value_name = "REVSETS", alias = "revisions")]
revisions: Option<Vec<RevisionArg>>,
#[arg(long, short = 'T')]
#[arg(add = ArgValueCandidates::new(complete::template_aliases))]
template: Option<String>,
#[arg(long, value_name = "SORT_KEY", value_enum, value_delimiter = ',')]
sort: Vec<SortKey>,
}
pub async fn cmd_bookmark_list(
ui: &mut Ui,
command: &CommandHelper,
args: &BookmarkListArgs,
) -> Result<(), CommandError> {
let workspace_command = command.workspace_helper(ui)?;
let repo = workspace_command.repo();
let view = repo.view();
let name_expr = match (&args.names, &args.revisions) {
(Some(texts), _) => parse_union_name_patterns(ui, texts)?,
(None, Some(_)) => StringExpression::none(),
(None, None) => StringExpression::all(),
};
let matched_local_targets: HashSet<_> = if let Some(revisions) = &args.revisions {
let mut expression = workspace_command.parse_union_revsets(ui, revisions)?;
expression.intersect_with(&RevsetExpression::bookmarks(StringExpression::all()));
expression.evaluate_to_commit_ids()?.try_collect().await?
} else {
HashSet::new()
};
let template: TemplateRenderer<Rc<CommitRef>> = {
let language = workspace_command.commit_template_language();
let text = match &args.template {
Some(value) => value.to_owned(),
None => workspace_command
.settings()
.get("templates.bookmark_list")?,
};
workspace_command
.parse_template(ui, &language, &text)?
.labeled(["bookmark_list"])
};
let ignored_tracked_remote = default_ignored_remote_name(repo.store());
let remote_expr = match (
&args.remotes,
args.tracked.then_some(ignored_tracked_remote).flatten(),
) {
(Some(texts), _) => parse_union_name_patterns(ui, texts)?,
(None, Some(ignored)) => StringExpression::exact(ignored).negated(),
(None, None) => StringExpression::all(),
};
let predicates = RefFilterPredicates {
name_matcher: name_expr.to_matcher(),
remote_matcher: remote_expr.to_matcher(),
matched_local_targets,
conflicted: args.conflicted,
include_local_only: !args.tracked && args.remotes.is_none(),
include_synced_remotes: args.tracked || args.all_remotes || args.remotes.is_some(),
include_untracked_remotes: !args.tracked && (args.all_remotes || args.remotes.is_some()),
};
let mut bookmark_list_items = commit_ref_list::collect_items(view.bookmarks(), &predicates);
let sort_keys = if args.sort.is_empty() {
workspace_command.settings().get_value_with(
"ui.bookmark-list-sort-keys",
commit_ref_list::parse_sort_keys,
)?
} else {
args.sort.clone()
};
commit_ref_list::sort(repo.store(), &mut bookmark_list_items, &sort_keys)?;
ui.request_pager();
let mut formatter = ui.stdout_formatter();
bookmark_list_items
.iter()
.flat_map(|item| itertools::chain([&item.primary], &item.tracked))
.try_for_each(|commit_ref| template.format(commit_ref, formatter.as_mut()))?;
drop(formatter);
warn_unmatched_local_or_remote_bookmarks(ui, view, &name_expr)?;
if bookmark_list_items
.iter()
.any(|item| item.primary.is_local() && item.primary.has_conflict())
{
writeln!(
ui.hint_default(),
"Some bookmarks have conflicts. Use `jj bookmark set <name> -r <rev>` to resolve."
)?;
}
#[cfg(feature = "git")]
if jj_lib::git::get_git_backend(repo.store()).is_ok() {
let deleted_tracking = bookmark_list_items
.iter()
.filter(|item| item.primary.is_local() && item.primary.is_absent())
.map(|item| {
item.tracked.iter().any(|r| {
let remote = r.remote_name().expect("tracked ref should be remote");
ignored_tracked_remote.is_none_or(|ignored| remote != ignored)
})
})
.max();
match deleted_tracking {
Some(true) => {
writeln!(
ui.hint_default(),
"Bookmarks marked as deleted can be *deleted permanently* on the remote by \
running `jj git push --deleted`. Use `jj bookmark forget` if you don't want \
that."
)?;
}
Some(false) => {
writeln!(
ui.hint_default(),
"Bookmarks marked as deleted will be deleted from the underlying Git repo on \
the next `jj git export`."
)?;
}
None => {}
}
}
Ok(())
}