@evanpurkhiser/tooling-personal 1.43.0 → 1.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assignees.js +1 -0
- package/dist/blame.js +178 -0
- package/dist/cherry-pick.js +53 -0
- package/dist/cmd/pr-create.js +127 -0
- package/dist/cmd/pr.js +31 -53
- package/dist/cmd/suggest-assignees.js +87 -0
- package/dist/fzf.js +7 -4
- package/dist/identity.js +151 -0
- package/dist/index.js +60 -0
- package/package.json +2 -2
package/dist/assignees.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.selectAssignee = exports.AssigneeType = void 0;
|
|
7
|
+
exports.getAssignees = getAssignees;
|
|
7
8
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
9
|
const fast_merge_async_iterators_1 = __importDefault(require("fast-merge-async-iterators"));
|
|
9
10
|
const graphql_request_1 = require("graphql-request");
|
package/dist/blame.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseDiff = parseDiff;
|
|
7
|
+
exports.blameFile = blameFile;
|
|
8
|
+
exports.aggregateBlame = aggregateBlame;
|
|
9
|
+
const simple_git_1 = __importDefault(require("simple-git"));
|
|
10
|
+
function hunkFromHeader(line, contextWindow) {
|
|
11
|
+
const match = line.match(/^@@ -(\d+)(?:,(\d+))?/);
|
|
12
|
+
if (!match) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const oldStart = Number(match[1]);
|
|
16
|
+
const oldCount = match[2] === undefined ? 1 : Number(match[2]);
|
|
17
|
+
if (oldCount === 0) {
|
|
18
|
+
return {
|
|
19
|
+
start: Math.max(1, oldStart - contextWindow + 1),
|
|
20
|
+
end: oldStart + contextWindow,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return { start: oldStart, end: oldStart + oldCount - 1 };
|
|
24
|
+
}
|
|
25
|
+
function parseFileSection(section, contextWindow) {
|
|
26
|
+
const lines = section.split('\n');
|
|
27
|
+
const fromLine = lines.find(l => l.startsWith('--- '));
|
|
28
|
+
const toLine = lines.find(l => l.startsWith('+++ '));
|
|
29
|
+
if (!fromLine || !toLine) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const fromPath = fromLine.slice(4);
|
|
33
|
+
const toPath = toLine.slice(4);
|
|
34
|
+
// Skip added files (no prior history) and deleted files.
|
|
35
|
+
if (fromPath === '/dev/null' || toPath === '/dev/null') {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
// Blame the old path at `rev` so rename-with-edits hits the right file.
|
|
39
|
+
const file = fromPath.startsWith('a/') ? fromPath.slice(2) : fromPath;
|
|
40
|
+
const hunks = lines
|
|
41
|
+
.filter(l => l.startsWith('@@'))
|
|
42
|
+
.map(l => hunkFromHeader(l, contextWindow))
|
|
43
|
+
.filter((r) => r !== null);
|
|
44
|
+
return hunks.length > 0 ? { file, hunks } : null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Parse a unified diff into per-file old-side line ranges. Pure insertions
|
|
48
|
+
* expand to a small context window so nearby lines still contribute. Added
|
|
49
|
+
* files (from /dev/null) are skipped.
|
|
50
|
+
*/
|
|
51
|
+
function parseDiff(diff, contextWindow = 3) {
|
|
52
|
+
return diff
|
|
53
|
+
.split(/^diff --git /m)
|
|
54
|
+
.slice(1)
|
|
55
|
+
.map(section => parseFileSection(section, contextWindow))
|
|
56
|
+
.filter((f) => f !== null);
|
|
57
|
+
}
|
|
58
|
+
function mergeRanges(ranges) {
|
|
59
|
+
if (ranges.length === 0) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const sorted = [...ranges].sort((a, b) => a.start - b.start);
|
|
63
|
+
const merged = [{ ...sorted[0] }];
|
|
64
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
65
|
+
const last = merged[merged.length - 1];
|
|
66
|
+
const curr = sorted[i];
|
|
67
|
+
if (curr.start <= last.end + 1) {
|
|
68
|
+
last.end = Math.max(last.end, curr.end);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
merged.push({ ...curr });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return merged;
|
|
75
|
+
}
|
|
76
|
+
function lineInRanges(line, ranges) {
|
|
77
|
+
return ranges.some(r => line >= r.start && line <= r.end);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Blame the full file at `rev`, tallying per-author totals over the whole
|
|
81
|
+
* file and the subset of lines inside the given hunk ranges.
|
|
82
|
+
*/
|
|
83
|
+
async function blameFile(file, hunks, rev) {
|
|
84
|
+
const empty = {
|
|
85
|
+
fileLines: new Map(),
|
|
86
|
+
hunkLines: new Map(),
|
|
87
|
+
authorNames: new Map(),
|
|
88
|
+
fileTotal: 0,
|
|
89
|
+
hunkTotal: 0,
|
|
90
|
+
};
|
|
91
|
+
let output;
|
|
92
|
+
try {
|
|
93
|
+
output = await (0, simple_git_1.default)().raw(['blame', '--porcelain', rev, '--', file]);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return empty;
|
|
97
|
+
}
|
|
98
|
+
const merged = mergeRanges(hunks);
|
|
99
|
+
const shaToEmail = new Map();
|
|
100
|
+
const shaToName = new Map();
|
|
101
|
+
const entries = [];
|
|
102
|
+
// 01. Parse porcelain into per-line blame entries + per-sha author headers
|
|
103
|
+
for (const line of output.split('\n')) {
|
|
104
|
+
const shaMatch = line.match(/^([0-9a-f]{40}) \d+ (\d+)(?: \d+)?$/);
|
|
105
|
+
if (shaMatch) {
|
|
106
|
+
entries.push({ sha: shaMatch[1], finalLine: Number(shaMatch[2]) });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const last = entries[entries.length - 1];
|
|
110
|
+
if (!last) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (line.startsWith('author-mail ')) {
|
|
114
|
+
const email = line.slice('author-mail '.length).replace(/^<|>$/g, '');
|
|
115
|
+
if (!shaToEmail.has(last.sha)) {
|
|
116
|
+
shaToEmail.set(last.sha, email);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if (line.startsWith('author ')) {
|
|
120
|
+
const name = line.slice('author '.length);
|
|
121
|
+
if (!shaToName.has(last.sha)) {
|
|
122
|
+
shaToName.set(last.sha, name);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// 02. Tally per-author counts, splitting hunk vs file totals
|
|
127
|
+
const fileLines = new Map();
|
|
128
|
+
const hunkLines = new Map();
|
|
129
|
+
const authorNames = new Map();
|
|
130
|
+
let fileTotal = 0;
|
|
131
|
+
let hunkTotal = 0;
|
|
132
|
+
for (const { sha, finalLine } of entries) {
|
|
133
|
+
const email = shaToEmail.get(sha);
|
|
134
|
+
if (!email) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
fileLines.set(email, (fileLines.get(email) ?? 0) + 1);
|
|
138
|
+
fileTotal++;
|
|
139
|
+
if (lineInRanges(finalLine, merged)) {
|
|
140
|
+
hunkLines.set(email, (hunkLines.get(email) ?? 0) + 1);
|
|
141
|
+
hunkTotal++;
|
|
142
|
+
}
|
|
143
|
+
const name = shaToName.get(sha);
|
|
144
|
+
if (name && !authorNames.has(email)) {
|
|
145
|
+
authorNames.set(email, name);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { fileLines, hunkLines, authorNames, fileTotal, hunkTotal };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Blame every file in parallel and fold the per-file results together.
|
|
152
|
+
*/
|
|
153
|
+
async function aggregateBlame(fileHunks, rev) {
|
|
154
|
+
const results = await Promise.all(fileHunks.map(({ file, hunks }) => blameFile(file, hunks, rev)));
|
|
155
|
+
const agg = {
|
|
156
|
+
fileLines: new Map(),
|
|
157
|
+
hunkLines: new Map(),
|
|
158
|
+
authorNames: new Map(),
|
|
159
|
+
fileTotal: 0,
|
|
160
|
+
hunkTotal: 0,
|
|
161
|
+
};
|
|
162
|
+
for (const r of results) {
|
|
163
|
+
agg.fileTotal += r.fileTotal;
|
|
164
|
+
agg.hunkTotal += r.hunkTotal;
|
|
165
|
+
for (const [email, n] of r.fileLines) {
|
|
166
|
+
agg.fileLines.set(email, (agg.fileLines.get(email) ?? 0) + n);
|
|
167
|
+
}
|
|
168
|
+
for (const [email, n] of r.hunkLines) {
|
|
169
|
+
agg.hunkLines.set(email, (agg.hunkLines.get(email) ?? 0) + n);
|
|
170
|
+
}
|
|
171
|
+
for (const [email, name] of r.authorNames) {
|
|
172
|
+
if (!agg.authorNames.has(email)) {
|
|
173
|
+
agg.authorNames.set(email, name);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return agg;
|
|
178
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.cherryPickOnto = cherryPickOnto;
|
|
7
|
+
const simple_git_1 = __importDefault(require("simple-git"));
|
|
8
|
+
/**
|
|
9
|
+
* Build a commit object equivalent to cherry-picking `sha` onto `base`, without
|
|
10
|
+
* touching the working tree or any ref.
|
|
11
|
+
*
|
|
12
|
+
* Uses `git merge-tree --write-tree` (git >= 2.38) to produce the merged tree
|
|
13
|
+
* in the object database, then `git commit-tree` to wrap it in a commit whose
|
|
14
|
+
* parent is `base`. Author info and the full commit message are preserved from
|
|
15
|
+
* the original commit; the committer is the current user (mirrors the way
|
|
16
|
+
* `git rebase` / `git cherry-pick` work).
|
|
17
|
+
*
|
|
18
|
+
* Throws if the three-way merge has conflicts.
|
|
19
|
+
*/
|
|
20
|
+
async function cherryPickOnto(sha, base) {
|
|
21
|
+
const git = (0, simple_git_1.default)();
|
|
22
|
+
const parentSha = (await git.revparse([`${sha}^`])).trim();
|
|
23
|
+
const baseSha = (await git.revparse([base])).trim();
|
|
24
|
+
let treeOid;
|
|
25
|
+
try {
|
|
26
|
+
treeOid = (await git.raw([
|
|
27
|
+
'merge-tree',
|
|
28
|
+
'--write-tree',
|
|
29
|
+
'--no-messages',
|
|
30
|
+
`--merge-base=${parentSha}`,
|
|
31
|
+
baseSha,
|
|
32
|
+
sha,
|
|
33
|
+
])).trim();
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new Error(`Conflicts applying ${sha.slice(0, 8)} onto ${base}. Rebase locally and retry.\n${error}`);
|
|
37
|
+
}
|
|
38
|
+
const info = await git.raw(['show', '-s', '--format=%an%n%ae%n%aI%n%B', sha]);
|
|
39
|
+
const lines = info.split('\n');
|
|
40
|
+
const authorName = lines[0];
|
|
41
|
+
const authorEmail = lines[1];
|
|
42
|
+
const authorDate = lines[2];
|
|
43
|
+
const message = lines.slice(3).join('\n').replace(/\n+$/, '');
|
|
44
|
+
const newCommit = await git
|
|
45
|
+
.env({
|
|
46
|
+
...process.env,
|
|
47
|
+
GIT_AUTHOR_NAME: authorName,
|
|
48
|
+
GIT_AUTHOR_EMAIL: authorEmail,
|
|
49
|
+
GIT_AUTHOR_DATE: authorDate,
|
|
50
|
+
})
|
|
51
|
+
.raw(['commit-tree', treeOid, '-p', baseSha, '-m', message]);
|
|
52
|
+
return newCommit.trim();
|
|
53
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.prCreate = prCreate;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const open_1 = __importDefault(require("open"));
|
|
9
|
+
const simple_git_1 = __importDefault(require("simple-git"));
|
|
10
|
+
const assignees_1 = require("../assignees");
|
|
11
|
+
const cherry_pick_1 = require("../cherry-pick");
|
|
12
|
+
const pulls_1 = require("../pulls");
|
|
13
|
+
const utils_1 = require("../utils");
|
|
14
|
+
async function readStdin() {
|
|
15
|
+
if (process.stdin.isTTY) {
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
process.stdin.setEncoding('utf8');
|
|
19
|
+
let data = '';
|
|
20
|
+
for await (const chunk of process.stdin) {
|
|
21
|
+
data += chunk;
|
|
22
|
+
}
|
|
23
|
+
return data.trim();
|
|
24
|
+
}
|
|
25
|
+
function parseSlugs(input) {
|
|
26
|
+
return (input ?? '')
|
|
27
|
+
.split(',')
|
|
28
|
+
.map(s => s.trim().replace(/^@/, ''))
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
async function resolveReviewers(repo, slugs) {
|
|
32
|
+
if (slugs.length === 0) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const want = new Set(slugs.map(s => s.toLowerCase()));
|
|
36
|
+
const found = [];
|
|
37
|
+
for await (const assignee of (0, assignees_1.getAssignees)(repo)) {
|
|
38
|
+
const key = assignee.slug.toLowerCase();
|
|
39
|
+
if (want.has(key)) {
|
|
40
|
+
found.push(assignee);
|
|
41
|
+
want.delete(key);
|
|
42
|
+
if (want.size === 0) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (want.size > 0) {
|
|
48
|
+
throw new Error(`Could not resolve reviewer(s): ${[...want].join(', ')}`);
|
|
49
|
+
}
|
|
50
|
+
return found;
|
|
51
|
+
}
|
|
52
|
+
async function prCreate(argv) {
|
|
53
|
+
if (!argv.title) {
|
|
54
|
+
throw new Error('--title is required');
|
|
55
|
+
}
|
|
56
|
+
const username = await (0, utils_1.getEmailUsername)();
|
|
57
|
+
const repo = await (0, utils_1.getRepoKey)();
|
|
58
|
+
const { head, origin } = await (0, utils_1.getBranchNames)();
|
|
59
|
+
if (head === null) {
|
|
60
|
+
throw new Error('Cannot determine HEAD branch name');
|
|
61
|
+
}
|
|
62
|
+
if (origin === null) {
|
|
63
|
+
throw new Error('Cannot determine upstream HEAD branch name');
|
|
64
|
+
}
|
|
65
|
+
const body = await readStdin();
|
|
66
|
+
const unpublished = (0, simple_git_1.default)().log({ from: 'HEAD', to: origin });
|
|
67
|
+
const [repoDetails, commits, prs] = await Promise.all([
|
|
68
|
+
(0, pulls_1.getRepoInfo)(repo),
|
|
69
|
+
unpublished,
|
|
70
|
+
(0, pulls_1.getPulls)(repo),
|
|
71
|
+
]);
|
|
72
|
+
if (repoDetails === null) {
|
|
73
|
+
throw new Error('Failed to get repository ID');
|
|
74
|
+
}
|
|
75
|
+
const commit = commits.all.find(c => c.hash.startsWith(argv.sha));
|
|
76
|
+
if (!commit) {
|
|
77
|
+
throw new Error(`Commit ${argv.sha} not found in unpublished commits`);
|
|
78
|
+
}
|
|
79
|
+
const branchName = (0, utils_1.branchFromMessage)(username, commit.message);
|
|
80
|
+
const existingPr = prs.find(p => p.headRefName === branchName);
|
|
81
|
+
if (argv.updateOnly && !existingPr) {
|
|
82
|
+
throw new Error(`No existing PR for branch ${branchName}`);
|
|
83
|
+
}
|
|
84
|
+
const reviewerSlugs = parseSlugs(argv.reviewer);
|
|
85
|
+
// Reviewers only apply on PR creation (matches `pt pr` behavior).
|
|
86
|
+
const reviewers = existingPr ? [] : await resolveReviewers(repo, reviewerSlugs);
|
|
87
|
+
console.error(chalk_1.default.gray(`Cherry-picking ${commit.hash.slice(0, 8)} onto ${origin}`));
|
|
88
|
+
const newSha = await (0, cherry_pick_1.cherryPickOnto)(commit.hash, origin);
|
|
89
|
+
console.error(chalk_1.default.gray(`Pushing ${branchName}`));
|
|
90
|
+
await (0, simple_git_1.default)().push(['--force', 'origin', `${newSha}:refs/heads/${branchName}`]);
|
|
91
|
+
if (existingPr) {
|
|
92
|
+
const url = `https://github.com/${repo.fullName}/pull/${existingPr.number}`;
|
|
93
|
+
console.error(chalk_1.default.green(`Updated existing PR #${existingPr.number}: ${existingPr.title}`));
|
|
94
|
+
console.log(url);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
console.error(chalk_1.default.gray('Creating Pull Request'));
|
|
98
|
+
const { createPullRequest } = await (0, pulls_1.createPull)({
|
|
99
|
+
baseRefName: repoDetails.defaultBranch,
|
|
100
|
+
headRefName: branchName,
|
|
101
|
+
repositoryId: repoDetails.repoId,
|
|
102
|
+
draft: argv.draft,
|
|
103
|
+
title: argv.title,
|
|
104
|
+
body,
|
|
105
|
+
});
|
|
106
|
+
const pr = createPullRequest.pullRequest;
|
|
107
|
+
if (argv.autoMerge) {
|
|
108
|
+
try {
|
|
109
|
+
await (0, pulls_1.enableAutoMerge)({ pullRequestId: pr.id, mergeMethod: 'SQUASH' });
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
console.error(chalk_1.default.yellow('Auto merge not available, skipping'));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (reviewers.length > 0) {
|
|
116
|
+
await (0, pulls_1.requestReview)({
|
|
117
|
+
pullRequestId: pr.id,
|
|
118
|
+
userIds: reviewers.filter(r => r.type === assignees_1.AssigneeType.User).map(r => r.id),
|
|
119
|
+
teamIds: reviewers.filter(r => r.type === assignees_1.AssigneeType.Team).map(r => r.id),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
console.error(chalk_1.default.green(`Created PR: ${pr.url}`));
|
|
123
|
+
console.log(pr.url);
|
|
124
|
+
if (!argv.noOpen) {
|
|
125
|
+
await (0, open_1.default)(pr.url);
|
|
126
|
+
}
|
|
127
|
+
}
|
package/dist/cmd/pr.js
CHANGED
|
@@ -9,6 +9,7 @@ const listr2_1 = require("listr2");
|
|
|
9
9
|
const open_1 = __importDefault(require("open"));
|
|
10
10
|
const simple_git_1 = __importDefault(require("simple-git"));
|
|
11
11
|
const assignees_1 = require("../assignees");
|
|
12
|
+
const cherry_pick_1 = require("../cherry-pick");
|
|
12
13
|
const editor_1 = require("../editor");
|
|
13
14
|
const fzf_1 = require("../fzf");
|
|
14
15
|
const pulls_1 = require("../pulls");
|
|
@@ -62,8 +63,9 @@ async function pr(argv) {
|
|
|
62
63
|
},
|
|
63
64
|
});
|
|
64
65
|
const { repoId, defaultBranch, commits, prs } = await collectInfoTask.run();
|
|
65
|
-
const
|
|
66
|
-
prompt: 'Select commit
|
|
66
|
+
const pickCommit = () => (0, fzf_1.fzfSelect)({
|
|
67
|
+
prompt: 'Select commit for PR:',
|
|
68
|
+
multi: false,
|
|
67
69
|
genValues: addOption => commits.all.forEach(commit => {
|
|
68
70
|
const branchName = (0, utils_1.branchFromMessage)(username, commit.message);
|
|
69
71
|
const pr = prs.find(pr => pr.headRefName === branchName);
|
|
@@ -73,68 +75,44 @@ async function pr(argv) {
|
|
|
73
75
|
addOption({ label, id: commit.hash, ...commit });
|
|
74
76
|
}),
|
|
75
77
|
});
|
|
76
|
-
// 01. Select which
|
|
77
|
-
const
|
|
78
|
-
?
|
|
79
|
-
: await
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
...selectedShas,
|
|
86
|
-
...commits.all
|
|
87
|
-
.filter(c => !selectedShas.includes(c.hash))
|
|
88
|
-
.map(c => c.hash)
|
|
89
|
-
.reverse(),
|
|
90
|
-
]
|
|
91
|
-
.map(sha => `pick ${sha}`)
|
|
92
|
-
.join('\n');
|
|
93
|
-
const targetCommit = selectedCommits[selectedCommits.length - 1];
|
|
94
|
-
const branchName = (0, utils_1.branchFromMessage)(username, targetCommit.message);
|
|
78
|
+
// 01. Select which commit to turn into a PR
|
|
79
|
+
const selectedCommit = commits.total === 1
|
|
80
|
+
? { id: commits.all[0].hash, ...commits.all[0] }
|
|
81
|
+
: (await pickCommit())[0];
|
|
82
|
+
if (!selectedCommit) {
|
|
83
|
+
console.log(chalk_1.default.red `No commit selected, aborting`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
const branchName = (0, utils_1.branchFromMessage)(username, selectedCommit.message);
|
|
95
87
|
const willOpenPr = prs.some(pr => pr.headRefName === branchName);
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
.env({ ...process.env, GIT_SEQUENCE_EDITOR: `echo "${rebaseContents}" >` })
|
|
100
|
-
.rebase(['--interactive', '--autostash', origin]);
|
|
101
|
-
try {
|
|
102
|
-
await rebase;
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
// Abort a failed rebase
|
|
106
|
-
await (0, simple_git_1.default)().rebase(['--abort']);
|
|
107
|
-
throw new Error(`Failed to rebase\n${error}`);
|
|
108
|
-
}
|
|
109
|
-
};
|
|
88
|
+
// 02. Build a branch-tip commit by cherry-picking the selected commit onto
|
|
89
|
+
// origin/<default> via plumbing, then push that commit. Local main is
|
|
90
|
+
// never touched.
|
|
110
91
|
const doPush = async () => {
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
const rebaseTargetCommit = newCommits.all[commitIdx];
|
|
114
|
-
const refSpec = `${rebaseTargetCommit.hash}:refs/heads/${branchName}`;
|
|
92
|
+
const newSha = await (0, cherry_pick_1.cherryPickOnto)(selectedCommit.hash, origin);
|
|
93
|
+
const refSpec = `${newSha}:refs/heads/${branchName}`;
|
|
115
94
|
await (0, simple_git_1.default)().push(['--force', 'origin', refSpec]);
|
|
116
95
|
};
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// 04. Nothing left to do if we just updated an existing
|
|
96
|
+
const pushTask = new listr2_1.Listr([], { rendererOptions });
|
|
97
|
+
pushTask.add({ title: 'Building and pushing commit', task: doPush });
|
|
98
|
+
// 03. Nothing left to do if we just updated an existing
|
|
121
99
|
// pull request.
|
|
122
100
|
if (willOpenPr) {
|
|
123
|
-
await
|
|
101
|
+
await pushTask.run();
|
|
124
102
|
return;
|
|
125
103
|
}
|
|
126
104
|
// Cork stdout to avoid listr output polluting vim. We'll uncork after we
|
|
127
105
|
// close vim
|
|
128
106
|
process.stdout.cork();
|
|
129
|
-
//
|
|
130
|
-
const { editor, editorResult } = await (0, editor_1.editPullRequest)(
|
|
131
|
-
const
|
|
132
|
-
|
|
107
|
+
// 04. Open an editor to write the pull request
|
|
108
|
+
const { editor, editorResult } = await (0, editor_1.editPullRequest)(selectedCommit);
|
|
109
|
+
const pushPromise = pushTask.run();
|
|
110
|
+
pushPromise.catch(() => editor.kill());
|
|
133
111
|
const { title, body } = await editorResult;
|
|
134
112
|
// XXX: We cork stdout here to avoid listr from corrupting vims output
|
|
135
113
|
process.stdout.uncork();
|
|
136
114
|
try {
|
|
137
|
-
await
|
|
115
|
+
await pushPromise;
|
|
138
116
|
}
|
|
139
117
|
catch {
|
|
140
118
|
process.exit(1);
|
|
@@ -164,12 +142,12 @@ async function pr(argv) {
|
|
|
164
142
|
}
|
|
165
143
|
};
|
|
166
144
|
const prTasks = new listr2_1.Listr([], { rendererOptions });
|
|
167
|
-
//
|
|
145
|
+
// 05-a. Create a Pull Request
|
|
168
146
|
prTasks.add({
|
|
169
147
|
title: 'Creating Pull Request',
|
|
170
148
|
task: createPrTask,
|
|
171
149
|
});
|
|
172
|
-
//
|
|
150
|
+
// 05-b. Enable auto merge
|
|
173
151
|
prTasks.add({
|
|
174
152
|
enabled: !!argv.autoMerge,
|
|
175
153
|
title: 'Enabling auto merge',
|
|
@@ -181,7 +159,7 @@ async function pr(argv) {
|
|
|
181
159
|
const reviewers = await (0, assignees_1.selectAssignee)(repo);
|
|
182
160
|
const { pr } = await asyncPrTasks;
|
|
183
161
|
process.stdout.uncork();
|
|
184
|
-
//
|
|
162
|
+
// 06. Request reviews
|
|
185
163
|
const reviewRequestTask = new listr2_1.Listr([], { rendererOptions });
|
|
186
164
|
reviewRequestTask.add({
|
|
187
165
|
enabled: () => reviewers.length > 0,
|
|
@@ -193,6 +171,6 @@ async function pr(argv) {
|
|
|
193
171
|
}),
|
|
194
172
|
});
|
|
195
173
|
await reviewRequestTask.run();
|
|
196
|
-
//
|
|
174
|
+
// 07. Open in browser
|
|
197
175
|
(0, open_1.default)(pr.url);
|
|
198
176
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.suggestAssignees = suggestAssignees;
|
|
7
|
+
const simple_git_1 = __importDefault(require("simple-git"));
|
|
8
|
+
const blame_1 = require("../blame");
|
|
9
|
+
const config_1 = require("../config");
|
|
10
|
+
const identity_1 = require("../identity");
|
|
11
|
+
const utils_1 = require("../utils");
|
|
12
|
+
async function getDiffSource(commit) {
|
|
13
|
+
const git = (0, simple_git_1.default)();
|
|
14
|
+
if (commit) {
|
|
15
|
+
const diff = await git.raw(['show', '-U0', '--format=', commit]);
|
|
16
|
+
const rev = (await git.revparse([`${commit}^`])).trim();
|
|
17
|
+
return { diff, rev, label: `commit ${commit}` };
|
|
18
|
+
}
|
|
19
|
+
const staged = await git.diff(['-U0', '--cached']);
|
|
20
|
+
if (staged.trim()) {
|
|
21
|
+
return { diff: staged, rev: 'HEAD', label: 'staged changes' };
|
|
22
|
+
}
|
|
23
|
+
const unstaged = await git.diff(['-U0']);
|
|
24
|
+
if (unstaged.trim()) {
|
|
25
|
+
return { diff: unstaged, rev: 'HEAD', label: 'unstaged changes' };
|
|
26
|
+
}
|
|
27
|
+
throw new Error('No staged or unstaged changes to suggest assignees for');
|
|
28
|
+
}
|
|
29
|
+
async function suggestAssignees(argv) {
|
|
30
|
+
const repo = await (0, utils_1.getRepoKey)();
|
|
31
|
+
const selfEmail = (await (0, simple_git_1.default)().raw('config', '--get', 'user.email')).trim();
|
|
32
|
+
// 01. Figure out what we're scoring against (staged, unstaged, or a commit)
|
|
33
|
+
const { diff, rev, label } = await getDiffSource(argv.commit);
|
|
34
|
+
const fileHunks = (0, blame_1.parseDiff)(diff);
|
|
35
|
+
if (fileHunks.length === 0) {
|
|
36
|
+
throw new Error(`No files with prior history in ${label}`);
|
|
37
|
+
}
|
|
38
|
+
// 02. Blame every file + resolve the repo's assignable user directory
|
|
39
|
+
const [blame, resolver] = await Promise.all([
|
|
40
|
+
(0, blame_1.aggregateBlame)(fileHunks, rev),
|
|
41
|
+
(0, identity_1.buildResolver)(repo),
|
|
42
|
+
]);
|
|
43
|
+
// 03. Score each author: weighted combination of hunk and file ownership
|
|
44
|
+
const ignoreRegexes = config_1.config.get('ignoreAssignees').map(value => new RegExp(value));
|
|
45
|
+
const emails = new Set([...blame.fileLines.keys(), ...blame.hunkLines.keys()]);
|
|
46
|
+
const scored = [];
|
|
47
|
+
for (const email of emails) {
|
|
48
|
+
if (email.toLowerCase() === selfEmail.toLowerCase()) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const login = resolver.resolve(email, blame.authorNames.get(email));
|
|
52
|
+
if (login && ignoreRegexes.some(r => r.test(login))) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const fileLines = blame.fileLines.get(email) ?? 0;
|
|
56
|
+
const hunkLines = blame.hunkLines.get(email) ?? 0;
|
|
57
|
+
const fileShare = blame.fileTotal > 0 ? fileLines / blame.fileTotal : 0;
|
|
58
|
+
const hunkShare = blame.hunkTotal > 0 ? hunkLines / blame.hunkTotal : 0;
|
|
59
|
+
const score = hunkShare * argv.hunkWeight + fileShare * (1 - argv.hunkWeight);
|
|
60
|
+
scored.push({
|
|
61
|
+
email,
|
|
62
|
+
name: blame.authorNames.get(email),
|
|
63
|
+
login,
|
|
64
|
+
fileLines,
|
|
65
|
+
hunkLines,
|
|
66
|
+
fileShare,
|
|
67
|
+
hunkShare,
|
|
68
|
+
score,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
scored.sort((a, b) => b.score - a.score);
|
|
72
|
+
const resolved = scored.filter(s => s.login !== null);
|
|
73
|
+
const unresolved = scored.filter(s => s.login === null);
|
|
74
|
+
const top = resolved.slice(0, argv.limit);
|
|
75
|
+
// 04. Render
|
|
76
|
+
if (argv.format === 'slugs') {
|
|
77
|
+
console.log(top.map(s => s.login).join(','));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
console.log(JSON.stringify({
|
|
81
|
+
source: label,
|
|
82
|
+
fileTotal: blame.fileTotal,
|
|
83
|
+
hunkTotal: blame.hunkTotal,
|
|
84
|
+
suggestions: top,
|
|
85
|
+
unresolved,
|
|
86
|
+
}, null, 2));
|
|
87
|
+
}
|
package/dist/fzf.js
CHANGED
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.fzfSelect = fzfSelect;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
|
-
async function fzfSelect({ prompt, genValues, }) {
|
|
6
|
-
const
|
|
5
|
+
async function fzfSelect({ prompt, multi = true, genValues, }) {
|
|
6
|
+
const fzfArgs = [
|
|
7
7
|
'--ansi',
|
|
8
8
|
'--height=40%',
|
|
9
9
|
'--reverse',
|
|
10
10
|
`--header="${prompt}"`,
|
|
11
11
|
'--with-nth=2..',
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
];
|
|
13
|
+
if (multi) {
|
|
14
|
+
fzfArgs.push('-m');
|
|
15
|
+
}
|
|
16
|
+
const fzf = (0, child_process_1.spawn)('fzf', fzfArgs, { shell: true, stdio: ['pipe', 'pipe', 'inherit'] });
|
|
14
17
|
fzf.stdin.setDefaultEncoding('utf-8');
|
|
15
18
|
const options = {};
|
|
16
19
|
const valuesDone = genValues(option => {
|
package/dist/identity.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.loadAssignableUsers = loadAssignableUsers;
|
|
7
|
+
exports.buildResolver = buildResolver;
|
|
8
|
+
const graphql_request_1 = require("graphql-request");
|
|
9
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
10
|
+
const path_1 = require("path");
|
|
11
|
+
const graphql_1 = require("./graphql");
|
|
12
|
+
function cachePath(repo) {
|
|
13
|
+
const base = process.env.XDG_CACHE_HOME || (0, path_1.join)(process.env.HOME ?? '', '.cache');
|
|
14
|
+
return (0, path_1.join)(base, 'pt', `assignees-${repo.owner}-${repo.repo}.json`);
|
|
15
|
+
}
|
|
16
|
+
async function readCache(repo) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = await promises_1.default.readFile(cachePath(repo), 'utf8');
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function writeCache(repo, users) {
|
|
26
|
+
const path = cachePath(repo);
|
|
27
|
+
await promises_1.default.mkdir((0, path_1.join)(path, '..'), { recursive: true });
|
|
28
|
+
await promises_1.default.writeFile(path, JSON.stringify(users));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Fetch every assignable user for a repo. Cached to disk indefinitely; pass
|
|
32
|
+
* `refresh` to force a refetch.
|
|
33
|
+
*/
|
|
34
|
+
async function loadAssignableUsers(repo, opts = {}) {
|
|
35
|
+
if (!opts.refresh) {
|
|
36
|
+
const cached = await readCache(repo);
|
|
37
|
+
if (cached) {
|
|
38
|
+
return cached;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// We don't request the `email` field here — it requires the
|
|
42
|
+
// `user:email` scope, which `gh auth token` doesn't grant by default.
|
|
43
|
+
const query = (0, graphql_request_1.gql) `
|
|
44
|
+
query userAssignees($owner: String!, $repo: String!, $cursor: String) {
|
|
45
|
+
repository(owner: $owner, name: $repo) {
|
|
46
|
+
assignableUsers(first: 100, after: $cursor) {
|
|
47
|
+
nodes {
|
|
48
|
+
login
|
|
49
|
+
name
|
|
50
|
+
}
|
|
51
|
+
pageInfo {
|
|
52
|
+
endCursor
|
|
53
|
+
hasNextPage
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
const pages = (0, graphql_1.paginatedRequest)(query, { ...repo }, (obj) => obj.repository.assignableUsers.pageInfo);
|
|
60
|
+
const users = [];
|
|
61
|
+
for await (const page of pages) {
|
|
62
|
+
for (const node of page.repository.assignableUsers.nodes ?? []) {
|
|
63
|
+
if (!node) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
users.push({ login: node.login, name: node.name ?? null });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
await writeCache(repo, users);
|
|
70
|
+
return users;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Extract the login from a GitHub noreply email.
|
|
74
|
+
*
|
|
75
|
+
* `<id>+<login>@users.noreply.github.com`
|
|
76
|
+
* `<login>@users.noreply.github.com`
|
|
77
|
+
*/
|
|
78
|
+
function parseNoreplyLogin(email) {
|
|
79
|
+
const m = email.match(/^(?:\d+\+)?([^@]+)@users\.noreply\.github\.com$/);
|
|
80
|
+
return m ? m[1] : null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Strip emoji / punctuation, collapse whitespace, lowercase. Keeps letters
|
|
84
|
+
* from any script plus spaces and hyphens.
|
|
85
|
+
*/
|
|
86
|
+
function normalizeName(name) {
|
|
87
|
+
return name
|
|
88
|
+
.replace(/[^\p{L}\p{N}\s-]/gu, ' ')
|
|
89
|
+
.replace(/\s+/g, ' ')
|
|
90
|
+
.trim()
|
|
91
|
+
.toLowerCase();
|
|
92
|
+
}
|
|
93
|
+
function nameTokens(name) {
|
|
94
|
+
return new Set(normalizeName(name)
|
|
95
|
+
.split(/[\s-]+/)
|
|
96
|
+
.filter(t => t.length >= 2));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Build a resolver mapping git author emails to GitHub logins. Tries in
|
|
100
|
+
* order: noreply parse, exact name-to-login, normalized name match,
|
|
101
|
+
* unambiguous token-subset match.
|
|
102
|
+
*/
|
|
103
|
+
async function buildResolver(repo) {
|
|
104
|
+
const users = await loadAssignableUsers(repo);
|
|
105
|
+
const loginIndex = new Map();
|
|
106
|
+
const nameIndex = new Map();
|
|
107
|
+
const tokenizedUsers = [];
|
|
108
|
+
for (const u of users) {
|
|
109
|
+
loginIndex.set(u.login.toLowerCase(), u.login);
|
|
110
|
+
if (!u.name) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const key = normalizeName(u.name);
|
|
114
|
+
if (key) {
|
|
115
|
+
// Mark as ambiguous on collision so we never guess the wrong user.
|
|
116
|
+
nameIndex.set(key, nameIndex.has(key) ? null : u.login);
|
|
117
|
+
}
|
|
118
|
+
const tokens = nameTokens(u.name);
|
|
119
|
+
if (tokens.size > 0) {
|
|
120
|
+
tokenizedUsers.push({ login: u.login, tokens });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function resolveByTokens(authorName) {
|
|
124
|
+
const authorTokens = nameTokens(authorName);
|
|
125
|
+
if (authorTokens.size < 2) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const matches = tokenizedUsers.filter(u => [...authorTokens].every(t => u.tokens.has(t)));
|
|
129
|
+
return matches.length === 1 ? matches[0].login : null;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
resolve(email, authorName) {
|
|
133
|
+
const noreply = parseNoreplyLogin(email);
|
|
134
|
+
if (noreply) {
|
|
135
|
+
return noreply;
|
|
136
|
+
}
|
|
137
|
+
if (!authorName) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const byLogin = loginIndex.get(authorName.toLowerCase().trim());
|
|
141
|
+
if (byLogin) {
|
|
142
|
+
return byLogin;
|
|
143
|
+
}
|
|
144
|
+
const byName = nameIndex.get(normalizeName(authorName));
|
|
145
|
+
if (byName) {
|
|
146
|
+
return byName;
|
|
147
|
+
}
|
|
148
|
+
return resolveByTokens(authorName);
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
8
|
const yargs_1 = __importDefault(require("yargs"));
|
|
9
9
|
const pr_1 = require("./cmd/pr");
|
|
10
|
+
const pr_create_1 = require("./cmd/pr-create");
|
|
10
11
|
const select_commit_1 = require("./cmd/select-commit");
|
|
12
|
+
const suggest_assignees_1 = require("./cmd/suggest-assignees");
|
|
11
13
|
(0, yargs_1.default)(process.argv.slice(2))
|
|
12
14
|
.option('color', {
|
|
13
15
|
boolean: true,
|
|
@@ -29,6 +31,64 @@ const select_commit_1 = require("./cmd/select-commit");
|
|
|
29
31
|
boolean: true,
|
|
30
32
|
desc: 'Enable auto merge for the PR',
|
|
31
33
|
}), pr_1.pr)
|
|
34
|
+
.command('pr-create <sha>', 'Non-interactively create or update a PR for a single commit. Body is read from stdin.', y => y
|
|
35
|
+
.positional('sha', {
|
|
36
|
+
type: 'string',
|
|
37
|
+
demandOption: true,
|
|
38
|
+
desc: 'Commit SHA (full or prefix) to publish as a PR',
|
|
39
|
+
})
|
|
40
|
+
.option('title', {
|
|
41
|
+
type: 'string',
|
|
42
|
+
demandOption: true,
|
|
43
|
+
desc: 'PR title',
|
|
44
|
+
})
|
|
45
|
+
.option('reviewer', {
|
|
46
|
+
type: 'string',
|
|
47
|
+
desc: 'Comma-separated list of reviewers (user logins or org/team slugs)',
|
|
48
|
+
})
|
|
49
|
+
.option('draft', {
|
|
50
|
+
alias: 'd',
|
|
51
|
+
boolean: true,
|
|
52
|
+
desc: 'Create PR as a draft',
|
|
53
|
+
})
|
|
54
|
+
.option('autoMerge', {
|
|
55
|
+
alias: 'm',
|
|
56
|
+
boolean: true,
|
|
57
|
+
desc: 'Enable auto merge for the PR',
|
|
58
|
+
})
|
|
59
|
+
.option('updateOnly', {
|
|
60
|
+
boolean: true,
|
|
61
|
+
desc: 'Only update an existing PR; fail if none exists for the generated branch',
|
|
62
|
+
})
|
|
63
|
+
.option('noOpen', {
|
|
64
|
+
boolean: true,
|
|
65
|
+
desc: 'Do not open the PR in the browser after creation',
|
|
66
|
+
}), pr_create_1.prCreate)
|
|
67
|
+
.command('suggest-assignees', 'Suggest reviewers for current diff based on blame ownership', y => y
|
|
68
|
+
.option('commit', {
|
|
69
|
+
type: 'string',
|
|
70
|
+
desc: 'Analyze the files changed in this commit instead of staged changes',
|
|
71
|
+
})
|
|
72
|
+
.option('limit', {
|
|
73
|
+
type: 'number',
|
|
74
|
+
default: 3,
|
|
75
|
+
desc: 'Max number of suggestions',
|
|
76
|
+
})
|
|
77
|
+
.option('format', {
|
|
78
|
+
type: 'string',
|
|
79
|
+
choices: ['slugs', 'json'],
|
|
80
|
+
default: 'slugs',
|
|
81
|
+
desc: 'Output format',
|
|
82
|
+
})
|
|
83
|
+
.option('hunkWeight', {
|
|
84
|
+
type: 'number',
|
|
85
|
+
default: 0.7,
|
|
86
|
+
desc: 'Weight of hunk-level ownership vs whole-file ownership (0..1)',
|
|
87
|
+
})
|
|
88
|
+
.option('refresh', {
|
|
89
|
+
boolean: true,
|
|
90
|
+
desc: 'Ignore the cached assignable-users list and refetch',
|
|
91
|
+
}), suggest_assignees_1.suggestAssignees)
|
|
32
92
|
.command('select-commit', 'Select a commit hash', select_commit_1.selectCommit)
|
|
33
93
|
.demandCommand(1, '')
|
|
34
94
|
.parse();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@evanpurkhiser/tooling-personal",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.45.0",
|
|
5
5
|
"description": "Evan Purkhiser's personal tooling",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Evan Purkhiser",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsc && chmod +x ./dist/index.js",
|
|
18
18
|
"start": "ts-node ./src/index.ts",
|
|
19
|
-
"lint": "oxlint src
|
|
19
|
+
"lint": "oxlint src",
|
|
20
20
|
"format": "oxfmt .",
|
|
21
21
|
"format:check": "oxfmt --check ."
|
|
22
22
|
},
|