This repository has no description
1use knot_git::{Commit, FilePatch, Hunk, LineCount, LineNumber, LineOp, PatchStatus};
2
3use crate::wire::{entry_mode_octal, fold_subject, message_body, rfc2822};
4
5const GRAPH_WIDTH: usize = 60;
6
7fn span(start: LineNumber, lines: LineCount) -> String {
8 match lines.get() {
9 1 => format!("{}", start.get()),
10 _ => format!("{},{}", start.get(), lines.get()),
11 }
12}
13
14fn render_hunk(out: &mut String, hunk: &Hunk) {
15 out.push_str(&format!(
16 "@@ -{} +{} @@\n",
17 span(hunk.old_start, hunk.old_lines),
18 span(hunk.new_start, hunk.new_lines)
19 ));
20 hunk.lines.iter().for_each(|line| {
21 out.push(match line.op {
22 LineOp::Context => ' ',
23 LineOp::Delete => '-',
24 LineOp::Add => '+',
25 });
26 out.push_str(&String::from_utf8_lossy(&line.text));
27 if !line.text.ends_with(b"\n") {
28 out.push_str("\n\\ No newline at end of file\n");
29 }
30 });
31}
32
33fn render_file(out: &mut String, patch: &FilePatch) {
34 let (a, b) = (&patch.path, &patch.path);
35 out.push_str(&format!("diff --git a/{a} b/{b}\n"));
36 match patch.status {
37 PatchStatus::Added => {
38 let mode = patch.new_kind.map(entry_mode_octal).unwrap_or_default();
39 out.push_str(&format!("new file mode {mode}\n"));
40 out.push_str(&format!(
41 "index {}..{}\n",
42 patch.old_oid.to_hex(),
43 patch.new_oid.to_hex()
44 ));
45 }
46 PatchStatus::Deleted => {
47 let mode = patch.old_kind.map(entry_mode_octal).unwrap_or_default();
48 out.push_str(&format!("deleted file mode {mode}\n"));
49 out.push_str(&format!(
50 "index {}..{}\n",
51 patch.old_oid.to_hex(),
52 patch.new_oid.to_hex()
53 ));
54 }
55 PatchStatus::Modified => {
56 if patch.old_kind == patch.new_kind {
57 let mode = patch.old_kind.map(entry_mode_octal).unwrap_or_default();
58 out.push_str(&format!(
59 "index {}..{} {mode}\n",
60 patch.old_oid.to_hex(),
61 patch.new_oid.to_hex()
62 ));
63 } else {
64 let old = patch.old_kind.map(entry_mode_octal).unwrap_or_default();
65 let new = patch.new_kind.map(entry_mode_octal).unwrap_or_default();
66 out.push_str(&format!("old mode {old}\nnew mode {new}\n"));
67 out.push_str(&format!(
68 "index {}..{}\n",
69 patch.old_oid.to_hex(),
70 patch.new_oid.to_hex()
71 ));
72 }
73 }
74 }
75 let old_label = match patch.status {
76 PatchStatus::Added => "/dev/null".to_string(),
77 _ => format!("a/{a}"),
78 };
79 let new_label = match patch.status {
80 PatchStatus::Deleted => "/dev/null".to_string(),
81 _ => format!("b/{b}"),
82 };
83 if patch.is_binary {
84 out.push_str(&format!(
85 "Binary files {old_label} and {new_label} differ\n"
86 ));
87 return;
88 }
89 if patch.hunks.is_empty() {
90 return;
91 }
92 out.push_str(&format!("--- {old_label}\n+++ {new_label}\n"));
93 patch.hunks.iter().for_each(|hunk| render_hunk(out, hunk));
94}
95
96pub(crate) fn render_patches(patches: &[FilePatch]) -> String {
97 patches.iter().fold(String::new(), |mut out, patch| {
98 render_file(&mut out, patch);
99 out
100 })
101}
102
103fn stat_counts(patch: &FilePatch) -> (usize, usize) {
104 patch.hunks.iter().fold((0, 0), |(added, deleted), hunk| {
105 (
106 added + hunk.added().get() as usize,
107 deleted + hunk.deleted().get() as usize,
108 )
109 })
110}
111
112fn graph(added: usize, deleted: usize) -> String {
113 let total = added + deleted;
114 let (added, deleted) = if total > GRAPH_WIDTH {
115 (added * GRAPH_WIDTH / total, deleted * GRAPH_WIDTH / total)
116 } else {
117 (added, deleted)
118 };
119 format!("{}{}", "+".repeat(added), "-".repeat(deleted))
120}
121
122fn diffstat(patches: &[FilePatch]) -> String {
123 let width = patches
124 .iter()
125 .map(|patch| patch.path.as_str().len())
126 .max()
127 .unwrap_or(0);
128 let rows: String = patches
129 .iter()
130 .map(|patch| {
131 if patch.is_binary {
132 format!(" {:<width$} | Bin\n", patch.path)
133 } else {
134 let (added, deleted) = stat_counts(patch);
135 format!(
136 " {:<width$} | {} {}\n",
137 patch.path,
138 added + deleted,
139 graph(added, deleted)
140 )
141 }
142 })
143 .collect();
144 let (added, deleted) = patches.iter().fold((0, 0), |(a, d), patch| {
145 let (pa, pd) = stat_counts(patch);
146 (a + pa, d + pd)
147 });
148 let files = patches.len();
149 let mut summary = format!(" {files} file{} changed", if files == 1 { "" } else { "s" });
150 if added > 0 {
151 summary.push_str(&format!(
152 ", {added} insertion{}(+)",
153 if added == 1 { "" } else { "s" }
154 ));
155 }
156 if deleted > 0 {
157 summary.push_str(&format!(
158 ", {deleted} deletion{}(-)",
159 if deleted == 1 { "" } else { "s" }
160 ));
161 }
162 summary.push('\n');
163 let created: String = patches
164 .iter()
165 .filter(|patch| patch.status == PatchStatus::Added)
166 .map(|patch| {
167 format!(
168 " create mode {} {}\n",
169 patch.new_kind.map(entry_mode_octal).unwrap_or_default(),
170 patch.path
171 )
172 })
173 .collect();
174 let deleted_rows: String = patches
175 .iter()
176 .filter(|patch| patch.status == PatchStatus::Deleted)
177 .map(|patch| {
178 format!(
179 " delete mode {} {}\n",
180 patch.old_kind.map(entry_mode_octal).unwrap_or_default(),
181 patch.path
182 )
183 })
184 .collect();
185 format!("{rows}{summary}{created}{deleted_rows}")
186}
187
188pub(crate) fn render_format_patch(commit: &Commit, patches: &[FilePatch]) -> String {
189 let subject = fold_subject(&commit.message);
190 let body = message_body(&commit.message);
191 let mut out = format!("From {} Mon Sep 17 00:00:00 2001\n", commit.id.to_hex());
192 out.push_str(&format!(
193 "From: {} <{}>\n",
194 commit.author.name, commit.author.email
195 ));
196 out.push_str(&format!(
197 "Date: {}\n",
198 rfc2822(commit.author.time.get(), commit.author.offset_seconds)
199 ));
200 out.push_str(&format!("Subject: [PATCH] {subject}\n"));
201 if let Some(change_id) = commit.change_id() {
202 out.push_str(&format!("Change-Id: {change_id}\n"));
203 }
204 out.push('\n');
205 if !body.is_empty() {
206 out.push_str(&body);
207 out.push('\n');
208 }
209 out.push_str("---\n");
210 out.push_str(&diffstat(patches));
211 out.push('\n');
212 out.push_str(&render_patches(patches));
213 out.push_str("-- \nknot\n\n");
214 out
215}