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