Skip to content

Commit ae941c1

Browse files
committed
feat(format): add support for personalised reports
1 parent 3b46a34 commit ae941c1

File tree

5 files changed

+227
-66
lines changed

5 files changed

+227
-66
lines changed

README.md

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@
1414
[![Follow JamieMason on GitHub](https://img.shields.io/github/followers/JamieMason.svg?style=social&label=Follow)](https://github.com/JamieMason)
1515
[![Follow fold_left on Twitter](https://img.shields.io/twitter/follow/fold_left.svg?style=social&label=Follow)](https://twitter.com/fold_left)
1616

17-
## 🎨 Screenshot
18-
19-
![screenshot](static/screenshot.png)
20-
2117
## ☁️ Installation
2218

2319
```
@@ -26,49 +22,105 @@ npm install --save-dev eslint eslint-formatter-git-log
2622

2723
## 🕹 Usage
2824

25+
To use the default configuration, set ESLint's
26+
[`--format`](https://eslint.org/docs/user-guide/command-line-interface#-f---format)
27+
option to `git-log` as follows:
28+
2929
```
30-
eslint --format 'git-log' file.js
30+
eslint --format git-log './src/**/*.js'
3131
```
3232

33-
## ⚖️ Configuration
33+
## Examples
34+
35+
### Full Report
36+
37+
By default the formatter will display a report of every error or warning in the
38+
codebase:
39+
40+
![screenshot](static/screenshot.png)
41+
42+
### Personalised Reports
43+
44+
If you work with a lot of Developers in a large Organisation or Codebase —
45+
introducing new rules is a great way to codify conventions and ensure quality at
46+
scale. However, when a new rule applies to a particularly prevalent coding
47+
pattern, it can result in every Developer being presented with a huge list of
48+
warnings.
49+
50+
Rather than disabling these rules completely, an alternative can be to only
51+
present each Developer with Errors and Warnings that relate to changes they
52+
themselves have made.
53+
54+
1. Create a file in your project which follows the structure below.
55+
56+
```js
57+
const gitLogFormatter = require('eslint-formatter-git-log');
58+
59+
module.exports = gitLogFormatter.withConfig({
60+
emailRegExp: new RegExp(process.env.GIT_COMMITTER_EMAIL),
61+
});
62+
```
63+
64+
In this example we require that the
65+
[Git Environment Variable](https://git-scm.com/book/en/v2/Git-Internals-Environment-Variables)
66+
`GIT_COMMITTER_EMAIL` is exported and reachable but, since this is an
67+
ordinary Node.js Script, the Email Address can be retrieved from the current
68+
Contributor's Machine in any way you prefer.
69+
70+
2. Set ESLint's
71+
[`--format`](https://eslint.org/docs/user-guide/command-line-interface#-f---format)
72+
option to your customised version instead of `git-log`:
73+
74+
```
75+
eslint --format ./path/to/your/custom-formatter.js './src/**/*.js'
76+
```
77+
78+
![screenshot](static/screenshot-when-filtered.png)
3479

35-
This formatter is written to be as customisable as possible. To create a
36-
customised version of the formatter you can create a file somewhere in your
37-
project which follows the structure below.
80+
## ⚖️ Configuration
3881

39-
For this example I am using the default values. You do not need to provide a
40-
value for every configuration item, only those you want to change.
82+
This example lists every available option with its corresponding default value.
83+
You don't need to provide a value for every configuration item, just the ones
84+
you want to change.
4185

4286
```js
43-
const gitLogFormatter = require('eslint-formatter-git-log');
4487
const chalk = require('chalk');
88+
const gitLogFormatter = require('eslint-formatter-git-log');
4589

4690
module.exports = gitLogFormatter.withConfig({
91+
// If set, only show result when Author Email matches this pattern
92+
emailRegExp: undefined,
93+
// Whitespace to insert between items when formatting
94+
gutter: ' ',
95+
// Translations for plain text used when formatting
96+
label: {
97+
error: 'error',
98+
warning: 'warning',
99+
},
100+
// Increase if you have files with 1000s of lines
101+
locationColumnWidth: 8,
102+
// Which methods of https://github.com/chalk/chalk to use when formatting
47103
style: {
104+
// eg. "error"
48105
error: chalk.red,
106+
// eg. "/Users/guybrush/Dev/grogrates/src/index.js"
49107
filePath: chalk.underline,
108+
// eg. "warning"
50109
warning: chalk.yellow,
110+
// eg. "161:12"
51111
location: chalk.dim,
112+
// eg. "no-process-exit"
52113
rule: chalk.dim,
114+
// eg. "bda304e570"
53115
commit: chalk.magenta,
116+
// eg. "(1 year, 2 months ago)"
54117
date: chalk.greenBright,
118+
// eg. "<guybrush@threepwood.grog>"
55119
email: chalk.blueBright,
56120
},
57-
gutter: ' ',
58-
label: {
59-
error: 'error',
60-
warning: 'warning',
61-
},
62-
locationColumnWidth: 8,
63121
});
64122
```
65123

66-
Then point at your custom formatter instead of the default like so:
67-
68-
```
69-
eslint --format './path-to-your-custom-formatter.js' file.js
70-
```
71-
72124
## ❓ Getting Help
73125

74126
- Get help with issues by creating a

src/formatters/git-log.ts

Lines changed: 148 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,17 @@ interface EslintResult {
3333
source?: string;
3434
}
3535

36+
interface ResultItem {
37+
commit: string;
38+
date: string;
39+
email: string;
40+
message: EslintMessage;
41+
result: EslintResult;
42+
}
43+
3644
interface FinalConfig {
45+
/** If set, show only results for Emails matching this pattern */
46+
emailRegExp?: RegExp;
3747
/** Whitespace to insert between items when formatting */
3848
gutter: string;
3949
/** Translations for plain text used when formatting */
@@ -77,6 +87,7 @@ export interface GitLogFormatter {
7787
export type CreateGitLogFormatter = (config: Config) => GitLogFormatter;
7888

7989
export const defaultConfig: FinalConfig = Object.freeze({
90+
emailRegExp: undefined,
8091
gutter: ' ',
8192
label: {
8293
error: 'error',
@@ -98,53 +109,151 @@ export const defaultConfig: FinalConfig = Object.freeze({
98109
/** Create an instance of the Formatter with your own alternative config */
99110
export const createGitLogFormatter: CreateGitLogFormatter = (config) => {
100111
const formatter = (results: EslintResult[]) => {
101-
const getConfig = (path: string) =>
102-
getIn(path, config) || getIn(path, defaultConfig);
103-
104-
const gutter = getConfig('gutter');
105-
const locationColumnWidth = getConfig('locationColumnWidth');
106-
const errorLabel = getConfig('label.error');
107-
const warningLabel = getConfig('label.warning');
108-
const styledError = getConfig('style.error');
109-
const styledFilePath = getConfig('style.filePath');
110-
const styledWarning = getConfig('style.warning');
111-
const styledLocation = getConfig('style.location');
112-
const styledRule = getConfig('style.rule');
113-
const styledCommit = getConfig('style.commit');
114-
const styledDate = getConfig('style.date');
115-
const styledEmail = getConfig('style.email');
112+
const getConfig = <T>(path: string) =>
113+
getIn<T>(path, config) || (getIn<T>(path, defaultConfig) as T);
114+
115+
const emailRegExp = getIn<RegExp | undefined>('emailRegExp', config);
116+
const gutter = getConfig<string>('gutter');
117+
const locationColumnWidth = getConfig<number>('locationColumnWidth');
118+
const errorLabel = getConfig<string>('label.error');
119+
const warningLabel = getConfig<string>('label.warning');
120+
const styledError = getConfig<typeof chalk>('style.error');
121+
const styledFilePath = getConfig<typeof chalk>('style.filePath');
122+
const styledWarning = getConfig<typeof chalk>('style.warning');
123+
const styledLocation = getConfig<typeof chalk>('style.location');
124+
const styledRule = getConfig<typeof chalk>('style.rule');
125+
const styledCommit = getConfig<typeof chalk>('style.commit');
126+
const styledDate = getConfig<typeof chalk>('style.date');
127+
const styledEmail = getConfig<typeof chalk>('style.email');
116128
const WARNING = styledWarning(warningLabel);
117129
const ERROR = styledError(errorLabel);
118130

119-
return results.reduce((output, { filePath, messages }) => {
120-
if (messages.length > 0) {
121-
output += `\n${styledFilePath(filePath)}\n`;
122-
messages.forEach(({ ruleId, severity, message, line, column }) => {
123-
const command = `git blame --date=relative --show-email -L ${line},${line} -- "${filePath}"`;
124-
const blame = execSync(command, { encoding: 'utf8' });
125-
const rawLocation = `${line}:${column}`;
126-
const status = severity === 1 ? WARNING : ERROR;
127-
const commitMatch = blame.match(/^[^ ]+/) || [''];
128-
const dateMatch = blame.match(/> (.+ ago)/) || ['', ''];
129-
const emailMatch = blame.match(/<([^>]+)>/) || ['', ''];
130-
const rightAlignLocations = ' '.repeat(
131-
locationColumnWidth - rawLocation.length,
132-
);
133-
const leftAlignCommitsWithStatuses = ' '.repeat(
134-
rightAlignLocations.length + rawLocation.length + gutter.length,
131+
const mergeMessageWith = (result: EslintResult) => (
132+
message: EslintMessage,
133+
): ResultItem => {
134+
const { filePath } = result;
135+
const { line } = message;
136+
const command = `git blame --date=relative --show-email -L ${line},${line} -- "${filePath}"`;
137+
const blame = execSync(command, { encoding: 'utf8' });
138+
const commitMatch = blame.match(/^[^ ]+/) || [''];
139+
const dateMatch = blame.match(/> (.+ ago)/) || ['', ''];
140+
const emailMatch = blame.match(/<([^>]+)>/) || ['', ''];
141+
const commit = commitMatch[0];
142+
const date = dateMatch[1].trim();
143+
const email = emailMatch[1];
144+
return {
145+
commit,
146+
date,
147+
email,
148+
message,
149+
result,
150+
};
151+
};
152+
153+
const rightAlignToCol1 = (col1Contents: string) =>
154+
' '.repeat(locationColumnWidth - col1Contents.length);
155+
156+
const leftAlignToCol2 = (col1Contents: string) =>
157+
' '.repeat(
158+
rightAlignToCol1(col1Contents).length +
159+
col1Contents.length +
160+
gutter.length,
161+
);
162+
163+
const cols = (col1: string | number, col2: string | number) =>
164+
`${rightAlignToCol1(`${col1}`)}${col1}${gutter}${col2}`;
165+
166+
const formatMessage = ({
167+
commit: rawCommit,
168+
date: rawDate,
169+
email: rawEmail,
170+
message: { ruleId, severity, message, line, column },
171+
}: ResultItem) => {
172+
const rawLocation = `${line}:${column}`;
173+
const status = severity === 1 ? WARNING : ERROR;
174+
const headIndent = rightAlignToCol1(rawLocation);
175+
const footIndent = leftAlignToCol2(rawLocation);
176+
const location = styledLocation(rawLocation);
177+
const rule = ruleId ? styledRule(ruleId) : '';
178+
const commit = styledCommit(`${rawCommit}`);
179+
const date = styledDate(`(${rawDate})`);
180+
const email = styledEmail(`<${rawEmail}>`);
181+
let output = '';
182+
output += `${headIndent}${location}${gutter}${status}${gutter}${message}${gutter}${rule}\n`;
183+
output += `${footIndent}${commit} ${email} ${date}\n`;
184+
return output;
185+
};
186+
187+
const isIncluded = ({ email }: ResultItem) =>
188+
emailRegExp ? emailRegExp.test(email) : true;
189+
190+
const authors = new Set();
191+
const total = {
192+
all: 0,
193+
errors: 0,
194+
userErrors: 0,
195+
userWarnings: 0,
196+
visible: 0,
197+
warnings: 0,
198+
};
199+
200+
const body = results.reduce((output, result) => {
201+
if (result.messages.length > 0) {
202+
const items = result.messages
203+
.map(mergeMessageWith(result))
204+
.map((item) => {
205+
authors.add(item.email);
206+
return item;
207+
})
208+
.filter(isIncluded)
209+
.map((item) => {
210+
if (item.message.severity === 2) {
211+
total.userErrors++;
212+
} else if (item.message.severity === 1) {
213+
total.userWarnings++;
214+
}
215+
return item;
216+
});
217+
218+
total.errors += result.errorCount;
219+
total.warnings += result.warningCount;
220+
total.all += result.messages.length;
221+
total.visible += items.length;
222+
223+
if (items.length > 0) {
224+
output += `\n${styledFilePath(result.filePath)}\n`;
225+
output += items.reduce<string>(
226+
(str: string, item: ResultItem) => `${str}${formatMessage(item)}`,
227+
'',
135228
);
136-
const location = styledLocation(rawLocation);
137-
const rule = ruleId ? styledRule(ruleId) : '';
138-
const commit = styledCommit(`${commitMatch[0]}`);
139-
const date = styledDate(`(${dateMatch[1].trim()})`);
140-
const email = styledEmail(`<${emailMatch[1]}>`);
141-
output += `${rightAlignLocations}${location}${gutter}${status}${gutter}${message}${gutter}${rule}\n`;
142-
output += `${leftAlignCommitsWithStatuses}${commit} ${email} ${date}\n`;
143-
});
229+
}
144230
}
145231
return output;
146232
}, '');
233+
234+
const banner = chalk.inverse(' REPORT COMPLETE ');
235+
236+
let footer = '';
237+
238+
if (emailRegExp) {
239+
const totalWarnings = `${total.userWarnings}/${total.warnings}`;
240+
const totalErrors = `${total.userErrors}/${total.errors}`;
241+
const totalWarningsLabel = `Warnings assigned to ${emailRegExp}`;
242+
const totalErrorsLabel = `Errors assigned to ${emailRegExp}`;
243+
footer += `${cols(results.length, 'Files')}\n`;
244+
footer += `${cols(totalWarnings, totalWarningsLabel)}\n`;
245+
footer += `${cols(totalErrors, totalErrorsLabel)}\n`;
246+
footer += `${cols(authors.size, 'Assignees')}\n`;
247+
} else {
248+
footer += `${cols(results.length, 'Files')}\n`;
249+
footer += `${cols(total.warnings, 'Warnings')}\n`;
250+
footer += `${cols(total.errors, 'Errors')}\n`;
251+
footer += `${cols(authors.size, 'Assignees')}\n`;
252+
}
253+
254+
return `${body}\n${banner}\n\n${footer}`;
147255
};
256+
148257
formatter.defaultConfig = defaultConfig;
149258
formatter.withConfig = createGitLogFormatter;
150259
return formatter;

src/formatters/lib/get-in.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ const isWalkable = (value: any) =>
44
const getChild = (parent: any, child: any): any =>
55
isWalkable(parent) ? parent[child] : undefined;
66

7-
export const getIn = (
7+
export const getIn = <T>(
88
pathToValue: string | number,
99
owner?: any,
10-
defaultValue?: any,
10+
defaultValue?: T,
1111
) => {
1212
const value = `${pathToValue}`.split('.').reduce(getChild, owner);
13-
return value !== undefined ? value : defaultValue;
13+
return value !== undefined ? (value as T) : defaultValue;
1414
};

static/screenshot-when-filtered.png

39.5 KB
Loading

static/screenshot.png

-43.3 KB
Loading

0 commit comments

Comments
 (0)