1 | var la = require('lazy-ass')
|
2 | var check = require('check-more-types')
|
3 | var exec = require('./exec')
|
4 | var path = require('path')
|
5 | var fs = require('fs')
|
6 | var quote = require('quote')
|
7 | var R = require('ramda')
|
8 |
|
9 | function isFileNameLine (line) {
|
10 | return /^filename/.test(line)
|
11 | }
|
12 | var findFilenameLine = R.find(isFileNameLine)
|
13 |
|
14 | function linesToBlameInfo (lines) {
|
15 | la(check.array(lines), 'expected lines', lines)
|
16 | la(lines.length > 3, 'few lines in output', lines)
|
17 | la(/^author/.test(lines[1]), 'cannot find author line in', lines)
|
18 |
|
19 | var info = {
|
20 | commit: lines[0].split(' ')[0],
|
21 | author: lines[1].replace('author ', ''),
|
22 | committer: lines[5].replace('committer ', ''),
|
23 | summary: lines[9].replace('summary ', '')
|
24 | }
|
25 |
|
26 | var filename = findFilenameLine(lines)
|
27 | la(
|
28 | /^filename/.test(filename),
|
29 | 'could not find filename line from',
|
30 | quote(filename),
|
31 | 'from',
|
32 | lines.length,
|
33 | 'lines\n---\n' + lines.join('\n') + '\n---'
|
34 | )
|
35 | info.filename = filename.replace('filename ', '')
|
36 |
|
37 | var line = lines[lines.length - 1]
|
38 | info.line = line
|
39 | return info
|
40 | }
|
41 |
|
42 |
|
43 | function toBlameInfo (blamePorcelainOutput) {
|
44 | la(
|
45 | check.unemptyString(blamePorcelainOutput),
|
46 | 'expected string output',
|
47 | blamePorcelainOutput
|
48 | )
|
49 | |
50 |
|
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 | var lines = blamePorcelainOutput.trim().split('\n')
|
64 | return linesToBlameInfo(lines)
|
65 | }
|
66 |
|
67 | function isUncommittedLine (line) {
|
68 | la(check.unemptyString(line), 'expected commit id line', line)
|
69 | return /^00000/.test(line)
|
70 | }
|
71 |
|
72 | function hasPreviousCommitId (lines) {
|
73 | la(check.array(lines), 'expected string lines', lines)
|
74 | return /^previous/.test(lines[10])
|
75 | }
|
76 |
|
77 | function hasBoundaryLine (lines) {
|
78 | la(check.array(lines), 'expected string lines', lines)
|
79 | return /^boundary/.test(lines[10])
|
80 | }
|
81 |
|
82 | function linesPerCommit (lines) {
|
83 | return isUncommittedLine(lines[0]) ||
|
84 | hasPreviousCommitId(lines) ||
|
85 | hasBoundaryLine(lines)
|
86 | ? 13
|
87 | : 12
|
88 | }
|
89 |
|
90 | function toBlameInfoFile (blamePorcelainOutput) {
|
91 | la(
|
92 | check.unemptyString(blamePorcelainOutput),
|
93 | 'expected string output',
|
94 | blamePorcelainOutput
|
95 | )
|
96 | |
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 | var lines = blamePorcelainOutput.trim().split('\n')
|
113 | la(lines.length > 3, 'few lines in output', blamePorcelainOutput)
|
114 |
|
115 | var perLineInfo = []
|
116 | var lineInfo
|
117 | while (lines.length > 0) {
|
118 | var linesPerSourceLine = linesPerCommit(lines)
|
119 | la(check.positiveNumber(linesPerSourceLine))
|
120 | lineInfo = lines.splice(0, linesPerSourceLine)
|
121 |
|
122 | perLineInfo.push(linesToBlameInfo(lineInfo))
|
123 | }
|
124 |
|
125 | return perLineInfo
|
126 | }
|
127 |
|
128 | function blameOneLine (filename, lineNumber) {
|
129 | la(check.unemptyString(filename), 'missing filename')
|
130 | la(
|
131 | check.positiveNumber(lineNumber),
|
132 | 'file',
|
133 | filename,
|
134 | 'missing line number (starting with 1)',
|
135 | lineNumber
|
136 | )
|
137 | var fullFilename = path.resolve(filename)
|
138 | la(
|
139 | fs.existsSync(filename),
|
140 | 'file',
|
141 | fullFilename,
|
142 | 'not found, based on',
|
143 | filename
|
144 | )
|
145 |
|
146 | console.log('who to blame for', fullFilename, lineNumber)
|
147 |
|
148 | var cmd =
|
149 | 'git blame --porcelain -L ' +
|
150 | lineNumber +
|
151 | ',' +
|
152 | lineNumber +
|
153 | ' ' +
|
154 | fullFilename
|
155 | return exec(cmd).then(toBlameInfo)
|
156 | }
|
157 |
|
158 | function blame (filename, lineNumber) {
|
159 | la(check.unemptyString(filename), 'missing filename')
|
160 |
|
161 | if (lineNumber) {
|
162 | return blameOneLine(filename, lineNumber)
|
163 | }
|
164 |
|
165 | var fullFilename = path.resolve(filename)
|
166 | la(
|
167 | fs.existsSync(filename),
|
168 | 'file',
|
169 | fullFilename,
|
170 | 'not found, based on',
|
171 | filename
|
172 | )
|
173 |
|
174 | console.log('who to blame for', fullFilename)
|
175 |
|
176 | var cmd = 'git blame --porcelain --line-porcelain ' + fullFilename
|
177 | return exec(cmd).then(toBlameInfoFile)
|
178 | }
|
179 |
|
180 | module.exports = blame
|