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
161 if !args.should_edit
163 && !workspace_command
164 .repo()
165 .view()
166 .heads()
167 .contains(working_commit_id)
168 {
169 return Err(user_error("The working copy must not have any children")
170 .hinted("Create a new commit on top of this one or use `--edit`"));
171 }
172
173 let start_revset = if args.should_edit {
176 wc_revset.clone()
177 } else {
178 wc_revset.parents()
179 };
180
181 let target_revset = direction.build_target_revset(&wc_revset, &start_revset, args)?;
182
183 let targets: Vec<Commit> = target_revset
184 .evaluate(workspace_command.repo().as_ref())?
185 .iter()
186 .commits(workspace_command.repo().store())
187 .try_collect()?;
188
189 let target = match targets.as_slice() {
190 [target] => target,
191 [] => {
192 let start_commits: Vec<Commit> = start_revset
194 .evaluate(workspace_command.repo().as_ref())?
195 .iter()
196 .commits(workspace_command.repo().store())
197 .try_collect()?;
198 return Err(direction.target_not_found_error(workspace_command, args, &start_commits));
199 }
200 commits => choose_commit(ui, workspace_command, direction, commits)?,
201 };
202
203 Ok(target.clone())
204}
205
206fn choose_commit<'a>(
207 ui: &Ui,
208 workspace_command: &WorkspaceCommandHelper,
209 direction: Direction,
210 commits: &'a [Commit],
211) -> Result<&'a Commit, CommandError> {
212 writeln!(
213 ui.stderr(),
214 "ambiguous {} commit, choose one to target:",
215 direction.cmd()
216 )?;
217 let mut formatter = ui.stderr_formatter();
218 let template = workspace_command.commit_summary_template();
219 let mut choices: Vec<String> = Default::default();
220 for (i, commit) in commits.iter().enumerate() {
221 write!(formatter, "{}: ", i + 1)?;
222 template.format(commit, formatter.as_mut())?;
223 writeln!(formatter)?;
224 choices.push(format!("{}", i + 1));
225 }
226 writeln!(formatter, "q: quit the prompt")?;
227 choices.push("q".to_string());
228 drop(formatter);
229
230 let index = ui.prompt_choice(
231 "enter the index of the commit you want to target",
232 &choices,
233 None,
234 )?;
235 commits
236 .get(index)
237 .ok_or_else(|| user_error("ambiguous target commit"))
238}
239
240pub(crate) fn move_to_commit(
241 ui: &mut Ui,
242 command: &CommandHelper,
243 direction: Direction,
244 args: &MovementArgs,
245) -> Result<(), CommandError> {
246 let mut workspace_command = command.workspace_helper(ui)?;
247
248 let current_wc_id = workspace_command
249 .get_wc_commit_id()
250 .ok_or_else(|| user_error("This command requires a working copy"))?;
251
252 let config_edit_flag = workspace_command.settings().get_bool("ui.movement.edit")?;
253 let args = MovementArgsInternal {
254 should_edit: args.edit || (!args.no_edit && config_edit_flag),
255 offset: args.offset,
256 conflict: args.conflict,
257 };
258
259 let target = get_target_commit(ui, &workspace_command, direction, current_wc_id, &args)?;
260 let current_short = short_commit_hash(current_wc_id);
261 let target_short = short_commit_hash(target.id());
262 let cmd = direction.cmd();
263 if args.should_edit {
265 workspace_command.check_rewritable([target.id()])?;
267 let mut tx = workspace_command.start_transaction();
268 tx.edit(&target)?;
269 tx.finish(
270 ui,
271 format!("{cmd}: {current_short} -> editing {target_short}"),
272 )?;
273 return Ok(());
274 }
275 let mut tx = workspace_command.start_transaction();
276 tx.check_out(&target)?;
278 tx.finish(ui, format!("{cmd}: {current_short} -> {target_short}"))?;
279 Ok(())
280}