1pub const US: char = '\x1f';
5
6pub const LOG_FORMAT: &str = "--format=%x1f%H%x1f%h%x1f%at%x1f%an%x1f%D%x1f%s";
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Commit {
11 pub hash: String,
12 pub short: String,
13 pub time: i64,
15 pub author: String,
16 pub refs: Vec<String>,
18 pub subject: String,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct LogRow {
24 pub graph: String,
25 pub commit: Option<Commit>,
26}
27
28pub fn parse_graph_line(line: &str) -> LogRow {
29 let Some((graph, rest)) = line.split_once(US) else {
30 return LogRow { graph: line.trim_end().to_string(), commit: None };
31 };
32 let f: Vec<&str> = rest.split(US).collect();
33 let get = |i: usize| f.get(i).copied().unwrap_or("");
34 let refs = get(4).split(", ").filter(|r| !r.is_empty()).map(str::to_string).collect();
35 LogRow {
36 graph: graph.trim_end().to_string(),
37 commit: Some(Commit {
38 hash: get(0).into(),
39 short: get(1).into(),
40 time: get(2).parse().unwrap_or(0),
41 author: get(3).into(),
42 refs,
43 subject: f.get(5..).map(|s| s.join("\u{1f}")).unwrap_or_default(),
45 }),
46 }
47}
48
49pub const BRANCH_FORMAT: &str = "--format=%(HEAD)%1f%(refname:short)%1f%(upstream:short)%1f%(upstream:track,nobracket)%1f%(committerdate:relative)%1f%(subject)";
51
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct Branch {
54 pub name: String,
55 pub current: bool,
56 pub upstream: Option<String>,
57 pub ahead: u32,
58 pub behind: u32,
59 pub gone: bool,
61 pub age: String,
62 pub subject: String,
63}
64
65pub fn parse_branches(text: &str) -> Vec<Branch> {
66 text.lines()
67 .filter_map(|l| {
68 let f: Vec<&str> = l.split(US).collect();
69 if f.len() < 6 {
70 return None;
71 }
72 let track = f[3];
73 let num = |key: &str| {
74 track.split(", ").find_map(|p| p.strip_prefix(key)).and_then(|n| n.trim().parse().ok()).unwrap_or(0)
75 };
76 Some(Branch {
77 current: f[0] == "*",
78 name: f[1].into(),
79 upstream: (!f[2].is_empty()).then(|| f[2].to_string()),
80 ahead: num("ahead "),
81 behind: num("behind "),
82 gone: track == "gone",
83 age: f[4].into(),
84 subject: f[5..].join("\u{1f}"),
85 })
86 })
87 .collect()
88}
89
90pub const STASH_FORMAT: &str = "--format=%gd%x1f%cr%x1f%gs";
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct Stash {
95 pub reference: String,
97 pub index: usize,
98 pub age: String,
99 pub branch: Option<String>,
100 pub message: String,
101}
102
103pub fn parse_stash_list(text: &str) -> Vec<Stash> {
104 text.lines()
105 .filter_map(|l| {
106 let mut f = l.splitn(3, US);
107 let (reference, age, gs) = (f.next()?, f.next()?, f.next()?);
108 let index = reference.strip_prefix("stash@{")?.strip_suffix('}')?.parse().ok()?;
109 let body = gs.strip_prefix("WIP on ").or_else(|| gs.strip_prefix("On "));
111 let (branch, message) = match body.and_then(|b| b.split_once(": ")) {
112 Some((b, m)) => (Some(b.to_string()), m.to_string()),
113 None => (None, gs.to_string()),
114 };
115 Some(Stash { reference: reference.into(), index, age: age.into(), branch, message })
116 })
117 .collect()
118}
119
120pub fn parse_shortlog(text: &str) -> Vec<(u32, String)> {
122 text.lines()
123 .filter_map(|l| {
124 let (n, who) = l.trim_start().split_once('\t')?;
125 Some((n.parse().ok()?, who.to_string()))
126 })
127 .collect()
128}
129
130pub fn parse_name_status_z(bytes: &[u8]) -> Vec<(char, String, Option<String>)> {
132 let text = String::from_utf8_lossy(bytes);
133 let mut it = text.split('\0').filter(|s| !s.is_empty());
134 let mut out = Vec::new();
135 while let Some(status) = it.next() {
136 let code = status.chars().next().unwrap_or('M');
137 if matches!(code, 'R' | 'C') {
138 let (Some(from), Some(to)) = (it.next(), it.next()) else { break };
139 out.push((code, to.to_string(), Some(from.to_string())));
140 } else if let Some(p) = it.next() {
141 out.push((code, p.to_string(), None));
142 }
143 }
144 out
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn connector_and_commit() {
153 assert_eq!(parse_graph_line("|\\ "), LogRow { graph: "|\\".into(), commit: None });
154 let r = parse_graph_line("* \x1fabc\x1fa\x1f5\x1fN\x1fHEAD -> main, tag: v1\x1fs: x");
155 let c = r.commit.unwrap();
156 assert_eq!((r.graph.as_str(), c.refs.len(), c.subject.as_str()), ("*", 2, "s: x"));
157 }
158
159 #[test]
160 fn name_status() {
161 let v = parse_name_status_z(b"M\0a.rs\0R087\0old\0new\0D\0gone\0");
162 assert_eq!(v[1], ('R', "new".into(), Some("old".into())));
163 assert_eq!(v.len(), 3);
164 }
165}