// 输出层:把解析结果渲染成文本。
//
// 和解析层一样是纯函数,不碰 stdin、不碰文件,所以全部可以单测。
///|
/// 一行在 diff 里的行首符号。
pub fn LineKind::prefix(self : LineKind) -> String {
match self {
Context => " "
Added => "+"
Removed => "-"
}
}
///|
/// 人话版的状态名,用在详细视图的标题行上。
pub fn FileStatus::label(self : FileStatus) -> String {
match self {
Modified => "modified"
NewFile => "new file"
Deleted => "deleted"
Renamed => "renamed"
Copied => "copied"
ModeOnly => "mode change"
}
}
///|
fn bar(plus : Int, minus : Int) -> String {
// 条太长会挤掉文件名,两侧各截到 20 个字符就够了
let p = if plus > 20 { 20 } else { plus }
let m = if minus > 20 { 20 } else { minus }
"+".repeat(p) + "-".repeat(m)
}
///|
fn plural(n : Int, word : String) -> String {
if n == 1 {
"\{n} \{word}"
} else {
"\{n} \{word}s"
}
}
///|
/// git diffstat 风格:每个文件一行,末尾跟一条总计。
///
/// 二进制文件和只改权限的文件没有 hunk,计数自然是 0,照样占一行。
pub fn Diff::format_stat(self : Diff) -> String {
let files = self.files
let n = files.length()
if n == 0 {
return "no changes"
}
// 文件名对齐到最长的那个,竖线才能对上
let width = files.fold(init=0, fn(acc, f) {
let len = f.display_path().length()
if len > acc {
len
} else {
acc
}
})
let out : Array[String] = []
for f in files {
let added = f.added_count()
let removed = f.removed_count()
let b = bar(added, removed)
let tail = if b == "" { "" } else { " \{b}" }
out.push(
"\{f.display_path().pad_start(width + 1, ' ')} | \{(added + removed).to_string().pad_start(4, ' ')}\{tail}",
)
}
out.push(
"\{plural(n, "file")} changed, \{self.added_count()} insertion(+), \{self.removed_count()} deletion(-)",
)
out.join("\n")
}
///|
/// 按文件、按 hunk 展开的视图,保留 diff 自己的行首符号。
pub fn Diff::format_detail(self : Diff) -> String {
let out : Array[String] = []
for f in self.files {
out.push("\{f.status.label()} \{f.display_path()}")
if f.binary {
out.push(" (binary)")
}
if f.status == ModeOnly {
let old = f.old_mode.unwrap_or("?")
let new = f.new_mode.unwrap_or("?")
out.push(" mode \{old} -> \{new}")
}
for h in f.hunks {
let hint = if h.heading == "" { "" } else { " \{h.heading}" }
out.push(
"@@ -\{h.old_start},\{h.old_count} +\{h.new_start},\{h.new_count} @@\{hint}",
)
for l in h.lines {
out.push("\{l.kind.prefix()}\{l.content}")
}
if h.no_newline_old || h.no_newline_new {
out.push("\\ No newline at end of file")
}
}
}
out.join("\n")
}