gps/ps/public/
list.rs

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    // We do know what branch we are currently checked out on when running this command. It seems
302    // like we should use that as the base branch.
303
304    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                    // Decided that we need to make the request review branches tracking branches
407                    // because when we do an rr or other similar commands we have to find the
408                    // associated branch(es). In the case where it is a single branch we can assume
409                    // that branch and then use it's associated tracking branch. In the case where
410                    // multiple branches exist with the patch the user will have to select a
411                    // branch somehow and then once they select a branch then we can use the
412                    // tracking branch of that branch to know where to push changes.
413
414                    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}