-
-
Notifications
You must be signed in to change notification settings - Fork 37
/
get-git-blame.ts
126 lines (119 loc) · 3.6 KB
/
get-git-blame.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/**
* Spawns git blame and parses results into JSON, via stream (so, no problem on huge files)
*/
import { camelCase, readLines } from "./deps.ts";
interface BlameOptions {
/**
* Annotate only the given line range. May be specified multiple times. Overlapping ranges are allowed.
* @see {@link https://git-scm.com/docs/git-blame#_specifying_ranges}
*/
range: string;
ignoreWhitespace: boolean;
workTree: string;
gitDir: string;
rev: string;
}
interface LineInfo {
sourceLine: number;
resultLine: number;
hash: string;
numberOfLines: number;
author: string;
authorMail: string;
authorTime: number;
authorTz: string;
commiter: string;
commiterMail: string;
commiterTime: number;
commiterTz: string;
summary: string;
previous: string;
filename: string;
[k: string]: string | number;
}
export default async function getGitBlame(
filename: string,
options: Partial<BlameOptions> = {},
gitPath = "git",
): Promise<Map<number, LineInfo>> {
/**
* @see {@link https://git-scm.com/docs/git-blame#_options}
*/
const args = ["--no-pager", "blame", "--line-porcelain"];
if (typeof options.workTree === "string") {
args.unshift(`--work-tree=${options.workTree}`);
}
if (typeof options.gitDir === "string") {
args.unshift(`--git-dir=${options.gitDir}`);
}
if (typeof options.ignoreWhitespace === "boolean") {
args.push("-w");
}
if (typeof options.range === "string") {
args.push(`-L${options.range}`);
}
if (typeof options.rev === "string") {
args.push(options.rev);
}
const cmd = [gitPath, ...args, "--", filename];
const process = Deno.run({
cmd,
cwd: options.workTree,
stdin: "piped",
stdout: "piped",
});
let currentLine: Partial<LineInfo>;
const linesMap: Map<number, Partial<LineInfo>> = new Map();
// return linesMap;
for await (const line of readLines(process.stdout)) {
// https://git-scm.com/docs/git-blame#_the_porcelain_format
// Each blame entry always starts with a line of:
// <40-byte hex sha1> <sourceline> <resultline> <num_lines>
// like: 49790775624c422f67057f7bb936f35df920e391 94 120 3
const parsedLine =
/^(?<hash>[a-f0-9]{40,40})\s(?<sourceline>\d+)\s(?<resultLine>\d+)\s(?<numLines>\d+)$/
.exec(
line,
);
if (parsedLine?.groups) {
// this is a new line info
const sourceLine = parseInt(parsedLine.groups.sourceline, 10);
const resultLine = parseInt(parsedLine?.groups.resultLine, 10);
const numberOfLines = parseInt(parsedLine?.groups.numLines, 10);
currentLine = {
hash: parsedLine.groups.hash,
sourceLine,
resultLine,
numberOfLines,
};
// set for all lines
for (let i = resultLine; i < resultLine + numberOfLines; i++) {
linesMap.set(i, currentLine);
}
} else {
if (currentLine!) {
const commitInfo =
/^(?<token>[a-z]+(-(?<subtoken>[a-z]+))?)\s(?<data>.+)$/.exec(
line,
);
if (commitInfo?.groups) {
const property = camelCase(commitInfo.groups.token);
let value: string | number = commitInfo.groups.data;
switch (commitInfo.groups.subtoken) {
case "mail":
// remove <> from email
value = value.slice(1, -1);
break;
case "time":
// parse datestamp into number
value = parseInt(value, 10);
break;
}
currentLine![property] = value;
}
}
}
}
return linesMap as Map<number, LineInfo>;
}