1use std::io::Write as _;
16use std::sync::Arc;
17
18use itertools::Itertools as _;
19use jj_lib::backend::CommitId;
20use jj_lib::commit::Commit;
21use jj_lib::repo::Repo as _;
22use jj_lib::revset::ResolvedRevsetExpression;
23use jj_lib::revset::RevsetExpression;
24use jj_lib::revset::RevsetFilterPredicate;
25use jj_lib::revset::RevsetIteratorExt as _;
26
27use crate::cli_util::CommandHelper;
28use crate::cli_util::WorkspaceCommandHelper;
29use crate::cli_util::short_commit_hash;
30use crate::command_error::CommandError;
31use crate::command_error::user_error;
32use crate::ui::Ui;
33
34#[derive(Clone, Debug, Eq, PartialEq)]
35pub(crate) struct MovementArgs {
36 pub offset: u64,
37 pub edit: bool,
38 pub no_edit: bool,
39 pub conflict: bool,
40}
41
42#[derive(Clone, Debug, Eq, PartialEq)]
43struct MovementArgsInternal {
44 offset: u64,
45 should_edit: bool,
46 conflict: bool,
47}
48
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub(crate) enum Direction {
51 Next,
52 Prev,
53}
54
55impl Direction {
56 fn cmd(&self) -> &'static str {
57 match self {
58 Self::Next => "next",
59 Self::Prev => "prev",
60 }
61 }
62
63 fn target_not_found_error(
64 &self,
65 workspace_command: &WorkspaceCommandHelper,
66 args: &MovementArgsInternal,
67 commits: &[Commit],
68 ) -> CommandError {
69 let offset = args.offset;
70 let err_msg = match (self, args.should_edit, args.conflict) {
71 (Self::Next, true, true) => {
73 String::from("The working copy has no descendants with conflicts")
74 }
75 (Self::Next, true, false) => {
76 format!("No descendant found {offset} commit(s) forward from the working copy",)
77 }
78 (Self::Next, false, true) => {
81 String::from("The working copy parent(s) have no other descendants with conflicts")
82 }
83 (Self::Next, false, false) => format!(
84 "No other descendant found {offset} commit(s) forward from the working copy \
85 parent(s)",
86 ),
87 (Self::Prev, true, true) => {
90 String::from("The working copy has no ancestors with conflicts")
91 }
92 (Self::Prev, true, false) => {
93 format!("No ancestor found {offset} commit(s) back from the working copy",)
94 }
95 (Self::Prev, false, true) => {
96 String::from("The working copy parent(s) have no ancestors with conflicts")
97 }
98 (Self::Prev, false, false) => format!(
99 "No ancestor found {offset} commit(s) back from the working copy parents(s)",
100 ),
101 };
102
103 let template = workspace_command.commit_summary_template();
104 let mut cmd_err = user_error(err_msg);
105 for commit in commits {
106 cmd_err.add_formatted_hint_with(|formatter| {
107 if args.should_edit {
108 write!(formatter, "Working copy: ")?;
109 } else {
110 write!(formatter, "Working copy parent: ")?;
111 }
112 template.format(commit, formatter)
113 });
114 }
115
116 cmd_err
117 }
118
119 fn build_target_revset(
120 &self,
121 working_revset: &Arc<ResolvedRevsetExpression>,
122 start_revset: &Arc<ResolvedRevsetExpression>,
123 args: &MovementArgsInternal,
124 ) -> Result<Arc<ResolvedRevsetExpression>, CommandError> {
125 let nth = match (self, args.should_edit) {
126 (Self::Next, true) => start_revset.descendants_at(args.offset),
127 (Self::Next, false) => start_revset
128 .children()
129 .minus(working_revset)
130 .descendants_at(args.offset - 1),
131 (Self::Prev, _) => start_revset.ancestors_at(args.offset),
132 };
133
134 let target_revset = match (self, args.conflict) {
135 (_, false) => nth,
136 (Self::Next, true) => nth
137 .descendants()
138 .filtered(RevsetFilterPredicate::HasConflict)
139 .roots(),
140 (Self::Prev, true) => nth
143 .ancestors()
144 .filtered(RevsetFilterPredicate::HasConflict)
145 .heads(),
146 };
147
148 Ok(target_revset)
149 }
150}
151
152fn get_target_commit(
153 ui: &mut Ui,
154 workspace_command: &WorkspaceCommandHelper,
155 direction: Direction,
156 working_commit_id: &CommitId,
157 args: &MovementArgsInternal,
158) -> Result<Commit, CommandError> {
159 let wc_revset = RevsetExpression::commit(working_commit_id.clone());
160 let start_revset = if args.should_edit {
163 wc_revset.clone()
164 } else {
165 wc_revset.parents()
166 };
167
168 let target_revset = direction.build_target_revset(&wc_revset, &start_revset, args)?;
169
170 let targets: Vec<Commit> = target_revset
171 .evaluate(workspace_command.repo().as_ref())?
172 .iter()
173 .commits(workspace_command.repo().store())
174 .try_collect()?;
175
176 let target = match targets.as_slice() {
177 [target] => target,
178 [] => {
179 let start_commits: Vec<Commit> = start_revset
181 .evaluate(workspace_command.repo().as_ref())?
182 .iter()
183 .commits(workspace_command.repo().store())
184 .try_collect()?;
185 return Err(direction.target_not_found_error(workspace_command, args, &start_commits));
186 }
187 commits => choose_commit(ui, workspace_command, direction, commits)?,
188 };
189
190 Ok(target.clone())
191}
192
193fn choose_commit<'a>(
194 ui: &Ui,
195 workspace_command: &WorkspaceCommandHelper,
196 direction: Direction,
197 commits: &'a [Commit],
198) -> Result<&'a Commit, CommandError> {
199 writeln!(
200 ui.stderr(),
201 "ambiguous {} commit, choose one to target:",
202 direction.cmd()
203 )?;
204 let mut formatter = ui.stderr_formatter();
205 let template = workspace_command.commit_summary_template();
206 let mut choices: Vec<String> = Default::default();
207 for (i, commit) in commits.iter().enumerate() {
208 write!(formatter, "{}: ", i + 1)?;
209 template.format(commit, formatter.as_mut())?;
210 writeln!(formatter)?;
211 choices.push(format!("{}", i + 1));
212 }
213 writeln!(formatter, "q: quit the prompt")?;
214 choices.push("q".to_string());
215 drop(formatter);
216
217 let index = ui.prompt_choice(
218 "enter the index of the commit you want to target",
219 &choices,
220 None,
221 )?;
222 commits
223 .get(index)
224 .ok_or_else(|| user_error("ambiguous target commit"))
225}
226
227pub(crate) fn move_to_commit(
228 ui: &mut Ui,
229 command: &CommandHelper,
230 direction: Direction,
231 args: &MovementArgs,
232) -> Result<(), CommandError> {
233 let mut workspace_command = command.workspace_helper(ui)?;
234
235 let current_wc_id = workspace_command
236 .get_wc_commit_id()
237 .ok_or_else(|| user_error("This command requires a working copy"))?;
238
239 let config_edit_flag = workspace_command.settings().get_bool("ui.movement.edit")?;
240 let args = MovementArgsInternal {
241 should_edit: args.edit || (!args.no_edit && config_edit_flag),
242 offset: args.offset,
243 conflict: args.conflict,
244 };
245
246 let target = get_target_commit(ui, &workspace_command, direction, current_wc_id, &args)?;
247 let current_short = short_commit_hash(current_wc_id);
248 let target_short = short_commit_hash(target.id());
249 let cmd = direction.cmd();
250 if args.should_edit {
252 workspace_command.check_rewritable([target.id()])?;
254 let mut tx = workspace_command.start_transaction();
255 tx.edit(&target)?;
256 tx.finish(
257 ui,
258 format!("{cmd}: {current_short} -> editing {target_short}"),
259 )?;
260 return Ok(());
261 }
262 let mut tx = workspace_command.start_transaction();
263 tx.check_out(&target)?;
265 tx.finish(ui, format!("{cmd}: {current_short} -> {target_short}"))?;
266 Ok(())
267}