Skip to main content

gw_render/
refs.rs

1//! Target header, ref update list, and pull summary.
2
3use 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
11/// `-> <target>` line; URL forms become links.
12pub 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        // Whole line dim.
45        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
60/// Target (if any) then each ref.
61pub 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/// One fetched repository's ref updates. `submodule` names the submodule for a recursive fetch.
70#[derive(Debug, Clone, Default)]
71pub struct RefGroup {
72    pub submodule: Option<String>,
73    pub target: Option<String>,
74    pub refs: Vec<RefUpdate>,
75}
76
77/// Each group with updates under its own header; groups without updates print nothing.
78pub 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
102/// Pull's stdout after the fetch (also clone's), colored line by line. Unknown lines are printed
103/// unchanged; `Submodule path` lines are gathered into a "Submodules" section at the end.
104pub 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::Action { verb, path } => {
146                format!("{} {}", p(&verb, if verb == "Removing" { RED } else { DIM }), sanitize(&path))
147            }
148            PullLine::Other => sanitize(line),
149        };
150        out += &l;
151        out.push('\n');
152    }
153    if !subs.is_empty() {
154        out += &format!("{}\n", paint("Submodules", BRANCH, color));
155        let w = subs.iter().map(|s| s.0.width()).max().unwrap_or(0);
156        for (path, verb, sha) in subs {
157            out += &format!(
158                "  {}{}  {} {}\n",
159                paint(&path, BRANCH, color),
160                " ".repeat(w - path.width()),
161                paint(&verb, GREEN, color),
162                paint(short(&sha), YELLOW, color)
163            );
164        }
165    }
166    out
167}
168
169/// git's leftover stderr lines: `error:`/`fatal:` prefix red, `warning:` yellow, `hint:` lines dim.
170pub fn render_messages(lines: &[String], color: bool) -> String {
171    let mut out = String::new();
172    for line in lines {
173        let l = sanitize(line);
174        let prefix = ["error:", "fatal:", "warning:", "hint:"].into_iter().find(|p| l.starts_with(p));
175        out += &match prefix {
176            _ if !color => l,
177            Some("hint:") => paint(&l, DIM, color),
178            Some(p) => format!("{}{}", paint(p, if p == "warning:" { YELLOW } else { RED }, color), &l[p.len()..]),
179            None => l,
180        };
181        out.push('\n');
182    }
183    out
184}