@bstopp/git-who 0.1.0 → 0.2.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/package.json +1 -1
- package/dist/src/cli.js +19 -6
- package/dist/src/commands/list.js +5 -1
- package/dist/src/commands/show.js +35 -11
- package/dist/src/commands/use.js +45 -25
- package/dist/src/utils/log.js +6 -0
- package/package.json +1 -1
package/dist/package.json
CHANGED
package/dist/src/cli.js
CHANGED
|
@@ -19,14 +19,18 @@ program
|
|
|
19
19
|
program
|
|
20
20
|
.command('show [name]')
|
|
21
21
|
.description('Show the current git profile for this repo')
|
|
22
|
+
.option('--json', 'Print the result as JSON')
|
|
22
23
|
.action(show);
|
|
23
24
|
program
|
|
24
25
|
.command('list')
|
|
25
26
|
.description('List all available profiles')
|
|
27
|
+
.option('--json', 'Print the result as JSON')
|
|
26
28
|
.action(list);
|
|
27
29
|
program
|
|
28
30
|
.command('use [name]')
|
|
29
31
|
.description('Set the profile for this repo.')
|
|
32
|
+
.option('--json', 'Print the result as JSON, without prompting (a profile name is required)')
|
|
33
|
+
.option('--update-origin', 'Update the origin remote to match the profile, without asking')
|
|
30
34
|
.action(use);
|
|
31
35
|
program
|
|
32
36
|
.command('add')
|
|
@@ -45,16 +49,19 @@ program
|
|
|
45
49
|
.description('Clone a remote repo using a specific profile.')
|
|
46
50
|
.action(clone);
|
|
47
51
|
/**
|
|
48
|
-
*
|
|
52
|
+
* The message of an error followed by those of its `cause` chain, so wrapped failures show the real reason.
|
|
49
53
|
*/
|
|
50
|
-
function
|
|
51
|
-
const
|
|
54
|
+
function errorMessages(err) {
|
|
55
|
+
const messages = [];
|
|
52
56
|
let current = err;
|
|
53
57
|
while (current instanceof Error) {
|
|
54
|
-
|
|
58
|
+
messages.push(current.message);
|
|
55
59
|
current = current.cause;
|
|
56
60
|
}
|
|
57
|
-
return
|
|
61
|
+
return messages.length > 0 ? messages : [String(err)];
|
|
62
|
+
}
|
|
63
|
+
function describeError(err) {
|
|
64
|
+
return errorMessages(err).map((message, i) => i === 0 ? message : `Caused by: ${message}`).join('\n');
|
|
58
65
|
}
|
|
59
66
|
/**
|
|
60
67
|
* Run the cli, printing any failure (with its cause chain) and setting a non-zero exit code. A cancelled prompt
|
|
@@ -69,7 +76,13 @@ export async function run(argv) {
|
|
|
69
76
|
process.exitCode = SIGINT_EXIT_CODE;
|
|
70
77
|
return;
|
|
71
78
|
}
|
|
72
|
-
|
|
79
|
+
if (argv.includes('--json')) {
|
|
80
|
+
const [error, ...causes] = errorMessages(err);
|
|
81
|
+
log.json({ error, causes });
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
log.error(describeError(err));
|
|
85
|
+
}
|
|
73
86
|
process.exitCode = 1;
|
|
74
87
|
}
|
|
75
88
|
}
|
|
@@ -2,9 +2,13 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { simpleGit } from 'simple-git';
|
|
3
3
|
import log from "../utils/log.js";
|
|
4
4
|
import { loadProfiles, getActiveProfileName } from "../utils/profiles.js";
|
|
5
|
-
export default async function list() {
|
|
5
|
+
export default async function list(options = {}) {
|
|
6
6
|
const profiles = await loadProfiles();
|
|
7
7
|
const activeName = await getActiveProfileName(simpleGit());
|
|
8
|
+
if (options.json) {
|
|
9
|
+
log.json(profiles.map((p) => ({ ...p, active: p.name === activeName })));
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
8
12
|
const lines = profiles.map((p) => {
|
|
9
13
|
if (p.name === activeName) {
|
|
10
14
|
return chalk.bold(` - ${p.name} (${p.host})`);
|
|
@@ -128,14 +128,43 @@ async function resolveProfile(name, profiles) {
|
|
|
128
128
|
return await resolveFromGit(profiles);
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* The result as a plain object for `--json`. The warnings that are printed as text become fields, so nothing is lost
|
|
133
|
+
* from stdout. A mismatched field git has no value for is `null`, which JSON would otherwise drop from the object.
|
|
134
|
+
*/
|
|
135
|
+
function toJson(resolved, signingKeyStatus) {
|
|
136
|
+
const { profile, derived, mismatches } = resolved;
|
|
137
|
+
return {
|
|
138
|
+
profile,
|
|
139
|
+
derived,
|
|
140
|
+
signingKeyStatus,
|
|
141
|
+
mismatches: mismatches && Object.fromEntries(Object.entries(mismatches).map(([field, value]) => [field, value ?? null])),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The extras for `printProfile`, or `undefined` when there are none, so a plain profile is printed without them.
|
|
146
|
+
*/
|
|
147
|
+
function displayOptions(resolved, signingKeyStatus) {
|
|
148
|
+
let opts = undefined;
|
|
149
|
+
if (resolved.mismatches && Object.keys(resolved.mismatches).length > 0) {
|
|
150
|
+
opts ||= {};
|
|
151
|
+
opts.mismatches = resolved.mismatches;
|
|
152
|
+
}
|
|
153
|
+
if (signingKeyStatus) {
|
|
154
|
+
opts ||= {};
|
|
155
|
+
opts.signingKeyStatus = signingKeyStatus;
|
|
156
|
+
}
|
|
157
|
+
return opts;
|
|
158
|
+
}
|
|
131
159
|
/**
|
|
132
160
|
* Print a profile, either the one given by name or the one resolved from the current repo. A profile resolved from the
|
|
133
|
-
* repo is also checked against its git config, and any signing key is looked up to confirm it exists.
|
|
161
|
+
* repo is also checked against its git config, and any signing key is looked up to confirm it exists. With `json` the
|
|
162
|
+
* result is printed as JSON instead.
|
|
134
163
|
*
|
|
135
164
|
* @throws {Error} If no name is given outside a git repo, the named profile does not exist, or no profile can be
|
|
136
165
|
* resolved from the repo. Errors from the ssh and gpg key lookups are not caught.
|
|
137
166
|
*/
|
|
138
|
-
export default async function show(name) {
|
|
167
|
+
export default async function show(name, options = {}) {
|
|
139
168
|
const git = simpleGit();
|
|
140
169
|
if (!name && !(await git.checkIsRepo())) {
|
|
141
170
|
throw new Error('You must provide a profile name, or invoke the show command from a git repo.');
|
|
@@ -143,16 +172,11 @@ export default async function show(name) {
|
|
|
143
172
|
const profiles = await loadProfiles();
|
|
144
173
|
const resolved = await resolveProfile(name, profiles);
|
|
145
174
|
const signingKeyStatus = await buildStatus(resolved);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
opts.mismatches = resolved.mismatches;
|
|
150
|
-
}
|
|
151
|
-
if (signingKeyStatus) {
|
|
152
|
-
opts ||= {};
|
|
153
|
-
opts.signingKeyStatus = signingKeyStatus;
|
|
175
|
+
if (options.json) {
|
|
176
|
+
log.json(toJson(resolved, signingKeyStatus));
|
|
177
|
+
return;
|
|
154
178
|
}
|
|
155
|
-
printProfile(resolved.profile,
|
|
179
|
+
printProfile(resolved.profile, displayOptions(resolved, signingKeyStatus));
|
|
156
180
|
if (resolved.derived) {
|
|
157
181
|
log.warn(`Profile derived from git config — not explicitly set via gitwho.profile`);
|
|
158
182
|
}
|
package/dist/src/commands/use.js
CHANGED
|
@@ -11,54 +11,74 @@ function extractRepoPath(url) {
|
|
|
11
11
|
return parseSshUrl(url)?.path ?? url.match(HTTPS_URL_PATTERN)?.[1];
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
* whether to rewrite it to `git@<host>:<path>`.
|
|
14
|
+
* Find where the origin remote differs from the url the profile's ssh host would give it, `git@<host>:<path>`.
|
|
16
15
|
*
|
|
17
|
-
* @returns The
|
|
16
|
+
* @returns The current and expected urls, or `undefined` when there is no origin or it already matches.
|
|
18
17
|
* @throws {Error} If the origin is neither an ssh nor an http(s) url, so its repo path cannot be determined.
|
|
19
18
|
*/
|
|
20
|
-
async function
|
|
21
|
-
const { value:
|
|
22
|
-
if (!
|
|
19
|
+
async function checkOrigin(git, profile) {
|
|
20
|
+
const { value: current } = await git.getConfig('remote.origin.url', 'local');
|
|
21
|
+
if (!current) {
|
|
23
22
|
return undefined;
|
|
24
23
|
}
|
|
25
|
-
const path = extractRepoPath(
|
|
24
|
+
const path = extractRepoPath(current);
|
|
26
25
|
if (!path) {
|
|
27
|
-
throw new Error(`Unable to determine the repo path from origin remote '${
|
|
28
|
-
}
|
|
29
|
-
const desiredUrl = `git@${profile.host}:${path}`;
|
|
30
|
-
if (desiredUrl === currentUrl) {
|
|
31
|
-
return undefined;
|
|
26
|
+
throw new Error(`Unable to determine the repo path from origin remote '${current}'.`);
|
|
32
27
|
}
|
|
28
|
+
const expected = `git@${profile.host}:${path}`;
|
|
29
|
+
return expected === current ? undefined : { current, expected };
|
|
30
|
+
}
|
|
31
|
+
function confirmOriginUpdate(profile, { current, expected }) {
|
|
33
32
|
const { style } = makeTheme();
|
|
34
|
-
|
|
35
|
-
message: `Origin remote is ${style.highlight(
|
|
33
|
+
return confirm({
|
|
34
|
+
message: `Origin remote is ${style.highlight(current)}, but profile '${profile.name}' expects ${style.highlight(expected)}. Update origin to match?`,
|
|
36
35
|
default: true,
|
|
37
36
|
});
|
|
38
|
-
return updateOrigin ? desiredUrl : undefined;
|
|
39
37
|
}
|
|
40
38
|
/**
|
|
41
|
-
*
|
|
39
|
+
* Look a profile up by name without falling back to a prompt, for output that nobody is there to answer.
|
|
40
|
+
*/
|
|
41
|
+
function findProfile(profiles, name) {
|
|
42
|
+
if (!name) {
|
|
43
|
+
throw new Error('A profile name is required with --json.');
|
|
44
|
+
}
|
|
45
|
+
const profile = profiles.find((p) => p.name === name);
|
|
46
|
+
if (!profile) {
|
|
47
|
+
throw new Error(`Profile ${name} not found.`);
|
|
48
|
+
}
|
|
49
|
+
return profile;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Apply a profile to the current repo, either the one given by name or one picked from a prompt. A mismatched origin is
|
|
53
|
+
* rewritten when `updateOrigin` is set, otherwise the user is asked, except with `json`, which never prompts: it needs
|
|
54
|
+
* a name and leaves the origin alone unless `updateOrigin` is set.
|
|
42
55
|
*
|
|
43
|
-
* @throws {Error} If the current directory is not a git repo,
|
|
44
|
-
* up, the origin remote url cannot be parsed, or applying the profile fails.
|
|
45
|
-
* failure is thrown as is, because nothing has been written by then. A failure
|
|
46
|
-
* since the git config may be left partly updated.
|
|
56
|
+
* @throws {Error} If the current directory is not a git repo, `json` has no name or the name is unknown, the profile's
|
|
57
|
+
* signing key is missing or cannot be looked up, the origin remote url cannot be parsed, or applying the profile fails.
|
|
58
|
+
* The signing key is checked first and its failure is thrown as is, because nothing has been written by then. A failure
|
|
59
|
+
* to apply is wrapped with its `cause`, since the git config may be left partly updated.
|
|
47
60
|
*/
|
|
48
|
-
export default async function use(name) {
|
|
61
|
+
export default async function use(name, options = {}) {
|
|
49
62
|
const git = simpleGit();
|
|
50
63
|
if (!await git.checkIsRepo()) {
|
|
51
64
|
throw new Error('The current directory must have a git repository to use a profile.');
|
|
52
65
|
}
|
|
53
66
|
const profiles = await loadProfiles();
|
|
54
|
-
const profile = await selectProfile(profiles, name);
|
|
67
|
+
const profile = options.json ? findProfile(profiles, name) : await selectProfile(profiles, name);
|
|
55
68
|
await verifySigningKey(profile);
|
|
56
|
-
const
|
|
69
|
+
const origin = await checkOrigin(git, profile);
|
|
70
|
+
const updateOrigin = origin !== undefined
|
|
71
|
+
&& (options.updateOrigin || (!options.json && await confirmOriginUpdate(profile, origin)));
|
|
57
72
|
try {
|
|
58
|
-
await applyProfile(git, profile,
|
|
73
|
+
await applyProfile(git, profile, updateOrigin ? origin.expected : undefined);
|
|
59
74
|
}
|
|
60
75
|
catch (error) {
|
|
61
76
|
throw new Error(`Failed to apply profile '${profile.name}'. Git config may be partly updated; re-run to retry.`, { cause: error });
|
|
62
77
|
}
|
|
63
|
-
|
|
78
|
+
if (options.json) {
|
|
79
|
+
log.json({ profile, originUpdated: updateOrigin, origin });
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
log.info(`Now using profile '${chalk.bold(profile.name)}'.`);
|
|
83
|
+
}
|
|
64
84
|
}
|
package/dist/src/utils/log.js
CHANGED
|
@@ -18,5 +18,11 @@ const log = {
|
|
|
18
18
|
info(msg) {
|
|
19
19
|
console.log(`\n${msg}\n`);
|
|
20
20
|
},
|
|
21
|
+
/**
|
|
22
|
+
* Writes a value to stdout as indented JSON, with no blank lines or colors so the output can be parsed as is.
|
|
23
|
+
*/
|
|
24
|
+
json(value) {
|
|
25
|
+
console.log(JSON.stringify(value, null, 2));
|
|
26
|
+
},
|
|
21
27
|
};
|
|
22
28
|
export default log;
|