1use crate::style::{BOLD, BRANCH, DIM, GREEN, LINK, RED, YELLOW, link, paint, sanitize};
4use gw_parse::{PullLine, PullResult, RefKind, RefUpdate, classify_pull_line};
5use unicode_width::UnicodeWidthStr;
6
7fn short(sha: &str) -> &str {
8 &sha[..sha.len().min(7)]
9}
10
11pub fn render_target(target: &str, color: bool) -> String {
13 format!("{} {}\n", paint("->", BOLD, color), target_text(target, color))
14}
15
16fn target_text(target: &str, color: bool) -> String {
17 let target = sanitize(target);
18 if target.contains("://") { link(&target, &paint(&target, LINK, color)) } else { target }
19}
20
21pub fn all_up_to_date(refs: &[RefUpdate]) -> bool {
22 refs.iter().all(|r| r.kind == RefKind::UpToDate)
23}
24
25pub fn render_up_to_date(color: bool) -> String {
26 format!("{}\n", paint("Everything up-to-date", DIM, color))
27}
28
29pub fn render_ref(r: &RefUpdate, color: bool) -> String {
30 let (marker, sgr) = match r.kind {
31 RefKind::FastForward => ("✓", GREEN),
32 RefKind::Forced => ("± forced", YELLOW),
33 RefKind::New => ("+ new", GREEN),
34 RefKind::Deleted => ("- deleted", RED),
35 RefKind::Rejected => ("✗ rejected", RED),
36 RefKind::UpToDate => ("=", DIM),
37 };
38 let mut s = format!(" {} ", paint(marker, sgr, color));
39 if let (Some(old), Some(new)) = (&r.old, &r.new) {
40 let dots = if r.kind == RefKind::Forced { "..." } else { ".." };
41 s += &format!("{}{dots}{} ", paint(short(old), DIM, color), paint(short(new), BOLD, color));
42 }
43 if r.kind == RefKind::UpToDate {
44 let refs = if r.src.is_empty() { r.dst.clone() } else { format!("{} -> {}", r.src, r.dst) };
46 s += &paint(&refs, DIM, color);
47 } else {
48 if !r.src.is_empty() && r.src != "(none)" {
49 s += &format!("{} -> ", paint(&r.src, BRANCH, color));
50 }
51 s += &paint(&r.dst, BRANCH, color);
52 }
53 if let Some(reason) = &r.reason {
54 s += &format!(" {}", paint(&format!("({reason})"), sgr, color));
55 }
56 s.push('\n');
57 s
58}
59
60pub fn render_refs(target: Option<&str>, refs: &[RefUpdate], color: bool) -> String {
62 let mut out = target.map(|t| render_target(t, color)).unwrap_or_default();
63 for r in refs {
64 out += &render_ref(r, color);
65 }
66 out
67}
68
69#[derive(Debug, Clone, Default)]
71pub struct RefGroup {
72 pub submodule: Option<String>,
73 pub target: Option<String>,
74 pub refs: Vec<RefUpdate>,
75}
76
77pub fn render_ref_groups(groups: &[RefGroup], color: bool) -> String {
79 let mut out = String::new();
80 for g in groups.iter().filter(|g| !g.refs.is_empty()) {
81 match &g.submodule {
82 Some(sub) => {
83 let shown = g.target.as_deref().map(|t| format!(" {}", target_text(t, color))).unwrap_or_default();
84 out += &format!(
85 "{} {} {}{shown}\n",
86 paint("->", BOLD, color),
87 paint("submodule", DIM, color),
88 paint(&sanitize(sub), BRANCH, color)
89 );
90 out += &render_refs(None, &g.refs, color);
91 }
92 None => out += &render_refs(g.target.as_deref(), &g.refs, color),
93 }
94 }
95 out
96}
97
98fn bars(plus: usize, minus: usize, color: bool) -> String {
99 format!("{}{}", paint(&"+".repeat(plus), GREEN, color), paint(&"-".repeat(minus), RED, color))
100}
101
102pub fn render_pull_summary(lines: &[String], color: bool) -> String {
105 let mut out = String::new();
106 let mut subs: Vec<(String, String, String)> = Vec::new();
107 for line in lines {
108 let p = |s: &str, sgr: &str| paint(&sanitize(s), sgr, color);
109 let l = match classify_pull_line(line) {
110 PullLine::Submodule { path, verb, sha } => {
111 subs.push((sanitize(&path), verb, sanitize(&sha)));
112 continue;
113 }
114 PullLine::SubmoduleNoise => continue,
115 _ if !color => sanitize(line),
116 PullLine::Updating { old, new } => format!("Updating {}..{}", p(&old, DIM), p(&new, BOLD)),
117 PullLine::Result(r) => {
118 let sgr = match r {
119 PullResult::FastForward | PullResult::Merge | PullResult::Rebased => GREEN,
120 PullResult::UpToDate => DIM,
121 PullResult::Conflict | PullResult::Failed => RED,
122 };
123 p(line, sgr)
124 }
125 PullLine::Diffstat { path, count, plus, minus } if plus + minus == 0 => format!("{} | {}", sanitize(&path), p(&count, DIM)),
126 PullLine::Diffstat { path, count, plus, minus } => {
127 format!("{} | {} {}", sanitize(&path), p(&count, DIM), bars(plus, minus, color))
128 }
129 PullLine::BinStat { path, rest } => format!("{} | {}", sanitize(&path), p(&format!("Bin {rest}"), DIM)),
130 PullLine::Summary { files, ins, del } => {
131 let mut parts = vec![sanitize(&files)];
132 parts.extend(ins.map(|i| p(&i, GREEN)));
133 parts.extend(del.map(|d| p(&d, RED)));
134 parts.join(", ")
135 }
136 PullLine::Mode { verb, mode, rest } => {
137 let sgr = match verb.as_str() {
138 "create mode" => GREEN,
139 "delete mode" => RED,
140 _ => YELLOW,
141 };
142 let mode = if mode.is_empty() { String::new() } else { format!("{} ", p(&mode, DIM)) };
143 format!(" {} {mode}{}", p(&verb, sgr), sanitize(&rest))
144 }
145 PullLine::Other => sanitize(line),
146 };
147 out += &l;
148 out.push('\n');
149 }
150 if !subs.is_empty() {
151 out += &format!("{}\n", paint("Submodules", BRANCH, color));
152 let w = subs.iter().map(|s| s.0.width()).max().unwrap_or(0);
153 for (path, verb, sha) in subs {
154 out += &format!(
155 " {}{} {} {}\n",
156 paint(&path, BRANCH, color),
157 " ".repeat(w - path.width()),
158 paint(&verb, GREEN, color),
159 paint(short(&sha), YELLOW, color)
160 );
161 }
162 }
163 out
164}