1use crate::Event;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum RefKind {
5 FastForward,
6 Forced,
7 New,
8 Deleted,
9 Rejected,
10 UpToDate,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct RefUpdate {
15 pub kind: RefKind,
16 pub old: Option<String>,
17 pub new: Option<String>,
18 pub src: String,
20 pub dst: String,
21 pub reason: Option<String>,
22}
23
24fn short_ref(r: &str) -> String {
25 ["refs/heads/", "refs/tags/", "refs/remotes/"]
26 .iter()
27 .find_map(|p| r.strip_prefix(p))
28 .unwrap_or(r)
29 .to_string()
30}
31
32fn kind_from_flag(flag: char) -> Option<RefKind> {
33 Some(match flag {
34 ' ' => RefKind::FastForward,
35 '+' | 't' => RefKind::Forced,
36 '*' => RefKind::New,
37 '-' => RefKind::Deleted,
38 '!' => RefKind::Rejected,
39 '=' => RefKind::UpToDate,
40 _ => return None,
41 })
42}
43
44fn shas(summary: &str) -> (Option<String>, Option<String>) {
46 match summary.split_once("...").or_else(|| summary.split_once("..")) {
47 Some((a, b)) => (Some(a.to_string()), Some(b.to_string())),
48 None => (None, None),
49 }
50}
51
52pub fn parse_push_porcelain(text: &str) -> Vec<Event> {
54 let mut out = Vec::new();
55 for line in text.lines() {
56 if let Some(url) = line.strip_prefix("To ") {
57 out.push(Event::Target(url.to_string()));
58 continue;
59 }
60 let mut f = line.splitn(3, '\t');
61 let (Some(flag), Some(refs), Some(summary)) = (f.next(), f.next(), f.next()) else { continue };
62 let Some(kind) = flag.chars().next().and_then(kind_from_flag) else { continue };
63 let (src, dst) = refs.split_once(':').unwrap_or(("", refs));
64 let (head, reason) = match summary.split_once(" (") {
66 Some((h, r)) => (h, Some(r.trim_end_matches(')').to_string())),
67 None => (summary, None),
68 };
69 let (old, new) = shas(head.trim_start_matches('+'));
70 let reason = reason.filter(|r| kind == RefKind::Rejected || r != "forced update");
71 out.push(Event::RefUpdate(RefUpdate { kind, old, new, src: short_ref(src), dst: short_ref(dst), reason }));
72 }
73 out
74}
75
76pub fn parse_fetch_porcelain(text: &str) -> Vec<RefUpdate> {
78 text.lines()
79 .filter_map(|line| {
80 let mut chars = line.chars();
81 let kind = kind_from_flag(chars.next()?)?;
82 let mut f = chars.as_str().split_whitespace();
83 let (old, new, dst) = (f.next()?, f.next()?, f.next()?);
84 let zero = |s: &str| s.bytes().all(|b| b == b'0');
85 Some(RefUpdate {
86 kind,
87 old: (!zero(old)).then(|| old.to_string()),
88 new: (!zero(new)).then(|| new.to_string()),
89 src: String::new(),
90 dst: short_ref(dst),
91 reason: None,
92 })
93 })
94 .collect()
95}
96
97pub fn parse_human_ref_line(line: &str) -> Option<RefUpdate> {
100 let rest = line.strip_prefix(' ')?;
101 let mut chars = rest.chars();
102 let flag = chars.next()?;
103 let mut kind = kind_from_flag(flag)?;
104 let rest = chars.as_str().strip_prefix(' ')?.trim_start();
105 let (summary, rest) = if rest.starts_with('[') {
106 let end = rest.find(']')?;
107 (&rest[..=end], &rest[end + 1..])
108 } else {
109 rest.split_once(' ').unwrap_or((rest, ""))
110 };
111 let (refs, reason) = match rest.rfind(" (") {
112 Some(i) if rest.trim_end().ends_with(')') => (&rest[..i], Some(rest[i + 2..].trim_end().trim_end_matches(')'))),
113 _ => (rest, None),
114 };
115 let (src, dst) = refs.split_once(" -> ")?;
116 let (old, new) = shas(summary);
117 if summary == "branch" || summary == "tag" {
119 kind = RefKind::UpToDate;
120 }
121 let reason = reason.filter(|r| *r != "forced update").map(str::to_string);
122 Some(RefUpdate { kind, old, new, src: src.trim().to_string(), dst: dst.trim().to_string(), reason })
123}