1use super::super::super::ps;
2use super::super::private::config;
3use super::super::private::config::list::ColorSelector;
4use super::super::private::git;
5use super::super::private::git::RebaseTodoCommand;
6use super::super::private::list;
7use super::super::private::paths;
8use super::super::private::state_computation;
9use ansi_term::Color;
10use std::cmp::Ordering;
11
12#[derive(Debug)]
13pub enum ListError {
14 RepositoryNotFound,
15 GetPatchStackFailed(Box<dyn std::error::Error>),
16 GetPatchListFailed(Box<dyn std::error::Error>),
17 GetRepoRootPathFailed(Box<dyn std::error::Error>),
18 PathNotUtf8,
19 GetConfigFailed(Box<dyn std::error::Error>),
20 GetCommitDiffPatchIdFailed(Box<dyn std::error::Error>),
21 GetHookOutputError(Box<dyn std::error::Error>),
22 CurrentBranchNameMissing,
23 GetUpstreamBranchNameFailed,
24}
25
26impl std::fmt::Display for ListError {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 match self {
29 Self::RepositoryNotFound => write!(f, "repository not found"),
30 Self::GetPatchStackFailed(e) => write!(f, "get patch stack failed, {}", e),
31 Self::GetPatchListFailed(e) => {
32 write!(f, "get patch stack list of patches failed, {}", e)
33 }
34 Self::GetRepoRootPathFailed(e) => write!(f, "get repository root path failed, {}", e),
35 Self::PathNotUtf8 => write!(f, "path not utf-8"),
36 Self::GetConfigFailed(e) => write!(f, "get config failed, {}", e),
37 Self::GetCommitDiffPatchIdFailed(e) => {
38 write!(f, "get commit diff patch id failed, {}", e)
39 }
40 Self::GetHookOutputError(e) => write!(f, "get hook output failed, {}", e),
41 Self::CurrentBranchNameMissing => write!(f, "current branch name missing"),
42 Self::GetUpstreamBranchNameFailed => write!(f, "get upstream branch name failed"),
43 }
44 }
45}
46
47impl std::error::Error for ListError {
48 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
49 match self {
50 Self::RepositoryNotFound => None,
51 Self::GetPatchStackFailed(e) => Some(e.as_ref()),
52 Self::GetPatchListFailed(e) => Some(e.as_ref()),
53 Self::GetRepoRootPathFailed(e) => Some(e.as_ref()),
54 Self::PathNotUtf8 => None,
55 Self::GetConfigFailed(e) => Some(e.as_ref()),
56 Self::GetCommitDiffPatchIdFailed(e) => Some(e.as_ref()),
57 Self::GetHookOutputError(e) => Some(e.as_ref()),
58 Self::CurrentBranchNameMissing => None,
59 Self::GetUpstreamBranchNameFailed => None,
60 }
61 }
62}
63
64fn is_connected_to_prev_row(prev_patch_branches: &[String], cur_patch_branches: &[String]) -> bool {
65 cur_patch_branches
66 .iter()
67 .map(|cb| prev_patch_branches.contains(cb))
68 .reduce(|acc, v| acc || v)
69 .unwrap()
70}
71
72fn rebase_todo_command_to_row(
73 todo: &RebaseTodoCommand,
74 color: bool,
75 patch_series_index_color: Option<Color>,
76 patch_series_sha_color: Option<Color>,
77) -> list::ListRow {
78 let mut row = list::ListRow::new(color);
79 match todo {
80 RebaseTodoCommand::Pick {
81 line: _,
82 key,
83 sha,
84 rest,
85 }
86 | RebaseTodoCommand::Revert {
87 line: _,
88 key,
89 sha,
90 rest,
91 }
92 | RebaseTodoCommand::Edit {
93 line: _,
94 key,
95 sha,
96 rest,
97 }
98 | RebaseTodoCommand::Reword {
99 line: _,
100 key,
101 sha,
102 rest,
103 }
104 | RebaseTodoCommand::Squash {
105 line: _,
106 key,
107 sha,
108 rest,
109 }
110 | RebaseTodoCommand::Drop {
111 line: _,
112 key,
113 sha,
114 rest,
115 }
116 | RebaseTodoCommand::Fixup {
117 line: _,
118 key,
119 sha,
120 rest,
121 keep_only_this_commits_message: _,
122 open_editor: _,
123 } => {
124 row.add_cell(
125 Some(11),
126 patch_series_index_color,
127 None,
128 format!("{} ", key.clone()),
129 );
130 row.add_cell(
131 Some(8),
132 patch_series_sha_color,
133 None,
134 format!("{:.7} ", sha.clone()),
135 );
136 row.add_cell(Some(51), None, None, format!("{:.50} ", rest.clone()));
137 row
138 }
139 RebaseTodoCommand::Merge {
140 line: _,
141 key,
142 sha,
143 label,
144 oneline,
145 reword: _,
146 } => {
147 row.add_cell(
148 Some(11),
149 patch_series_index_color,
150 None,
151 format!("{} ", key.clone()),
152 );
153 row.add_cell(
154 Some(8),
155 patch_series_sha_color,
156 None,
157 format!("{:.7} ", sha.clone().unwrap_or("".to_string())),
158 );
159 row.add_cell(
160 Some(51),
161 None,
162 None,
163 format!("{:.50} ", format!("{} {}", label, oneline).to_string()),
164 );
165 row
166 }
167 RebaseTodoCommand::Exec { line: _, key, rest }
168 | RebaseTodoCommand::Break { line: _, key, rest }
169 | RebaseTodoCommand::Label { line: _, key, rest }
170 | RebaseTodoCommand::Reset { line: _, key, rest }
171 | RebaseTodoCommand::UpdateRef { line: _, key, rest }
172 | RebaseTodoCommand::Noop { line: _, key, rest } => {
173 row.add_cell(
174 Some(11),
175 patch_series_index_color,
176 None,
177 format!("{} ", key.clone()),
178 );
179 row.add_cell(Some(51), None, None, format!("{:.50} ", rest.clone()));
180 row
181 }
182 RebaseTodoCommand::Comment {
183 line: _,
184 key,
185 message,
186 } => {
187 row.add_cell(
188 Some(11),
189 patch_series_index_color,
190 None,
191 format!("{} ", key.clone()),
192 );
193 row.add_cell(Some(51), None, None, format!("{:.50} ", message.clone()));
194 row
195 }
196 }
197}
198
199fn get_behind_count(
200 repo: &git2::Repository,
201 patch_stack: &ps::PatchStack,
202 patch_stack_upstream_tracking_branch_name: &str,
203) -> usize {
204 let patch_stack_branch_upstream = repo
205 .find_branch(
206 patch_stack_upstream_tracking_branch_name,
207 git2::BranchType::Remote,
208 )
209 .expect("cur patch stack branch upstream to exist");
210
211 let patch_stack_branch_upstream_oid = patch_stack_branch_upstream
212 .into_reference()
213 .target()
214 .expect("cur patch stack branch upstream to have a target");
215
216 let behind_com_anc = git::common_ancestor(
217 repo,
218 patch_stack.head.target().expect("HEAD to have an oid"),
219 patch_stack_branch_upstream_oid,
220 )
221 .expect("common ancestor between HEAD and upstream tracking branch to exist");
222
223 git::count_commits(repo, patch_stack_branch_upstream_oid, behind_com_anc)
224 .expect("to be able to count commits from remote tracking branch to common ancestor")
225}
226
227pub fn list(color: bool) -> Result<(), ListError> {
228 let repo = git::create_cwd_repo().map_err(|_| ListError::RepositoryNotFound)?;
229
230 let repo_root_path =
231 paths::repo_root_path(&repo).map_err(|e| ListError::GetRepoRootPathFailed(e.into()))?;
232 let repo_root_str = repo_root_path.to_str().ok_or(ListError::PathNotUtf8)?;
233 let repo_gitdir_path = repo.path();
234 let repo_gitdir_str = repo_gitdir_path.to_str().ok_or(ListError::PathNotUtf8)?;
235 let config = config::get_config(repo_root_str, repo_gitdir_str)
236 .map_err(|e| ListError::GetConfigFailed(e.into()))?;
237
238 if git::in_rebase(repo_gitdir_path) {
239 let rebase_head_name = git::in_rebase_head_name(repo_gitdir_path)
240 .unwrap()
241 .trim()
242 .replace("refs/heads/", "");
243
244 let rebase_onto = git::in_rebase_onto(repo_gitdir_path)
245 .unwrap()
246 .trim()
247 .to_string();
248 if color {
249 print!(
250 "{}",
251 Color::Red.paint(format!(
252 "rebase of '{}' in progress; onto",
253 rebase_head_name
254 ))
255 );
256 println!(" {}", Color::Yellow.paint(format!("{:.7} ", rebase_onto)));
257 } else {
258 println!(
259 "rebase of '{}' in progress; onto {:.7}",
260 rebase_head_name, rebase_onto
261 );
262 }
263
264 if !config.list.reverse_order {
265 let todos_vec = git::in_rebase_todos(repo_gitdir_path).unwrap();
266 println!(
267 "Next commands to do ({} remaining commands)",
268 todos_vec.len()
269 );
270 for todo in todos_vec.iter().rev() {
271 println!(
272 "{}",
273 rebase_todo_command_to_row(
274 todo,
275 color,
276 config.list.patch_index.select_color(false),
277 config.list.patch_sha.select_color(false)
278 )
279 );
280 }
281 println!("(use \"git rebase --edit-todo\" to view and edit)");
282 println!("(use \"git rebase --continue\" once you are satisfied with your changes)");
283 println!();
284 }
285 }
286
287 let cur_patch_stack_branch_ref = match git::in_rebase(repo_gitdir_path) {
288 true => git::in_rebase_head_name(repo_gitdir_path)
289 .unwrap()
290 .trim()
291 .to_string(),
292 false => git::get_current_branch(&repo).ok_or(ListError::CurrentBranchNameMissing)?,
293 };
294 let cur_patch_stack_branch_upstream_ref =
295 git::branch_upstream_name(&repo, &cur_patch_stack_branch_ref)
296 .map_err(|_| ListError::GetUpstreamBranchNameFailed)?;
297 let cur_patch_stack_branch_name = str::replace(&cur_patch_stack_branch_ref, "refs/heads/", "");
298 let cur_patch_stack_branch_upstream_name =
299 str::replace(&cur_patch_stack_branch_upstream_ref, "refs/remotes/", "");
300
301 let patch_stack =
305 ps::get_patch_stack(&repo).map_err(|e| ListError::GetPatchStackFailed(e.into()))?;
306
307 let list_of_patches = ps::get_patch_list(&repo, &patch_stack)
308 .map_err(|e| ListError::GetPatchListFailed(e.into()))?;
309
310 let base_oid = patch_stack.base.target().unwrap();
311
312 let patch_info_collection =
313 state_computation::get_list_patch_info(&repo, base_oid, &cur_patch_stack_branch_name)
314 .unwrap();
315
316 let behind_count = get_behind_count(&repo, &patch_stack, &cur_patch_stack_branch_upstream_name);
317
318 println!(
319 "{} tracking {} [ahead {}, behind {}]",
320 &cur_patch_stack_branch_name,
321 &cur_patch_stack_branch_upstream_name,
322 list_of_patches.len(),
323 behind_count,
324 );
325
326 let list_of_patches_iter: Box<dyn Iterator<Item = _>> = if config.list.reverse_order {
327 Box::new(list_of_patches.into_iter())
328 } else {
329 Box::new(list_of_patches.into_iter().rev())
330 };
331
332 let mut prev_patch_branches: Vec<String> = vec![];
333 let mut connected_to_prev_row: bool;
334 let mut prev_row_had_alternate_colors: bool = true;
335
336 for patch in list_of_patches_iter {
337 let mut row = list::ListRow::new(color);
338
339 let commit = repo.find_commit(patch.oid).unwrap();
340
341 let commit_diff_id: Option<git2::Oid> = match git::commit_diff_patch_id(&repo, &commit) {
342 Ok(id) => Some(id),
343 Err(git::CommitDiffPatchIdError::GetDiffFailed(git::CommitDiffError::MergeCommit)) => {
344 None
345 }
346 Err(e) => return Err(ListError::GetCommitDiffPatchIdFailed(e.into())),
347 };
348
349 if let Some(ps_id) = ps::commit_ps_id(&commit) {
350 if let Some(patch_info) = patch_info_collection.get(&ps_id) {
351 let cur_row_branches: Vec<String> =
352 patch_info.branches.iter().map(|b| b.name.clone()).collect();
353 connected_to_prev_row =
354 is_connected_to_prev_row(&prev_patch_branches, &cur_row_branches);
355 prev_patch_branches = cur_row_branches.to_vec();
356 } else {
357 connected_to_prev_row = false;
358 prev_patch_branches = vec![];
359 }
360 } else {
361 connected_to_prev_row = false;
362 prev_patch_branches = vec![];
363 }
364
365 let is_alternate = config.list.alternate_patch_series_colors
366 && connected_to_prev_row == prev_row_had_alternate_colors;
367 let bg_color = config.list.patch_background.select_color(is_alternate);
368 prev_row_had_alternate_colors = is_alternate;
369 let fg_color = config.list.patch_foreground.select_color(is_alternate);
370 let sha_color = config.list.patch_sha.select_color(is_alternate);
371 let index_color = config.list.patch_index.select_color(is_alternate);
372 let summary_color = config.list.patch_summary.select_color(is_alternate);
373 let extra_patch_info_color = config.list.patch_extra_info.select_color(is_alternate);
374
375 row.add_cell(Some(5), index_color, bg_color, format!("{} ", patch.index));
376 row.add_cell(Some(8), sha_color, bg_color, format!("{:.7} ", patch.oid));
377 row.add_cell(
378 Some(51),
379 summary_color,
380 bg_color,
381 format!("{:.50} ", patch.summary.clone()),
382 );
383
384 if let Some(ps_id) = ps::commit_ps_id(&commit) {
385 if let Some(patch_info) = patch_info_collection.get(&ps_id) {
386 row.add_cell(Some(2), fg_color, bg_color, "( ");
387 for b in patch_info.branches.iter() {
388 match patch_info.branches.len().cmp(&1) {
389 Ordering::Greater => {
390 row.add_cell(None, fg_color, bg_color, format!("{} ", b.name.clone()));
391 }
392 Ordering::Less => {}
393 Ordering::Equal => {
394 let branch_info = patch_info.branches.first().unwrap();
395 if !branch_info.name.starts_with("ps/rr/") {
396 row.add_cell(
397 None,
398 fg_color,
399 bg_color,
400 format!("{} ", b.name.clone()),
401 );
402 }
403 }
404 }
405
406 let mut state_string = String::new();
415
416 let branch_patch: state_computation::PatchInfo = b
417 .patches
418 .iter()
419 .filter(|p| p.patch_id == ps_id)
420 .map(|p| p.to_owned())
421 .collect::<Vec<state_computation::PatchInfo>>()
422 .first()
423 .unwrap()
424 .clone();
425 state_string.push('l');
426
427 match commit_diff_id {
428 Some(id) => {
429 if branch_patch.commit_diff_id != id {
430 state_string.push('*');
431 }
432 }
433 None => state_string.push('*'),
434 }
435
436 let upstream_opt = b.upstream.clone();
437 if let Some(upstream) = upstream_opt {
438 let upstream_branch_patch_opt: Option<state_computation::PatchInfo> =
439 upstream
440 .patches
441 .iter()
442 .filter(|p| p.patch_id == ps_id)
443 .map(|p| p.to_owned())
444 .collect::<Vec<state_computation::PatchInfo>>()
445 .first()
446 .cloned();
447
448 if upstream_branch_patch_opt.is_some() {
449 state_string.push('r');
450 match commit_diff_id {
451 Some(id) => {
452 if let Some(upstream_branch_patch) = upstream_branch_patch_opt {
453 if upstream_branch_patch.commit_diff_id != id {
454 state_string.push('*');
455 }
456 }
457 }
458 None => state_string.push('*'),
459 }
460
461 if upstream.patches.len() < upstream.commit_count {
462 state_string.push('!');
463 }
464 }
465 }
466 row.add_cell(
467 None,
468 extra_patch_info_color,
469 bg_color,
470 format!("{} ", &state_string),
471 );
472
473 if config.list.add_extra_patch_info {
474 let hook_stdout = list::execute_list_additional_info_hook(
475 repo_root_str,
476 repo_gitdir_str,
477 &[
478 &patch.index.to_string(),
479 &state_string,
480 &patch.oid.to_string(),
481 &patch.summary,
482 ],
483 )
484 .map_err(|e| ListError::GetHookOutputError(e.into()))?;
485 let hook_stdout_len = config.list.extra_patch_info_length;
486 row.add_cell(
487 Some(hook_stdout_len + 1),
488 extra_patch_info_color,
489 bg_color,
490 format!("{} ", hook_stdout),
491 );
492 }
493 }
494 row.add_cell(Some(2), fg_color, bg_color, ")");
495 } else {
496 row.add_cell(None, fg_color, bg_color, "()")
497 }
498 } else {
499 row.add_cell(None, fg_color, bg_color, "()")
500 }
501
502 println!("{}", row);
503 }
504
505 if git::in_rebase(repo_gitdir_path) && config.list.reverse_order {
506 let todos_vec = git::in_rebase_todos(repo_gitdir_path).unwrap();
507 println!();
508 println!(
509 "Next commands to do ({} remaining commands)",
510 todos_vec.len()
511 );
512 for todo in todos_vec.iter() {
513 println!(
514 "{}",
515 rebase_todo_command_to_row(
516 todo,
517 color,
518 config.list.patch_index.select_color(false),
519 config.list.patch_sha.select_color(false)
520 )
521 );
522 }
523 println!("(use \"git rebase --edit-todo\" to view and edit)");
524 println!("(use \"git rebase --continue\" once you are satisfied with your changes)");
525 println!();
526 }
527
528 Ok(())
529}