@dleangen/cage-git 0.0.1
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/README.md +360 -0
- package/bin/dev.js +7 -0
- package/bin/run.js +7 -0
- package/dist/commands/land.d.ts +22 -0
- package/dist/commands/land.js +250 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/oclif.manifest.json +113 -0
- package/package.json +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# @dleangen/cage-git
|
|
2
|
+
|
|
3
|
+
Git workflow mechanics for CAGE projects.
|
|
4
|
+
|
|
5
|
+
<!-- commands -->
|
|
6
|
+
* [`cage-git help [COMMAND]`](#cage-git-help-command)
|
|
7
|
+
* [`cage-git land`](#cage-git-land)
|
|
8
|
+
* [`cage-git plugins`](#cage-git-plugins)
|
|
9
|
+
* [`cage-git plugins add PLUGIN`](#cage-git-plugins-add-plugin)
|
|
10
|
+
* [`cage-git plugins:inspect PLUGIN...`](#cage-git-pluginsinspect-plugin)
|
|
11
|
+
* [`cage-git plugins install PLUGIN`](#cage-git-plugins-install-plugin)
|
|
12
|
+
* [`cage-git plugins link PATH`](#cage-git-plugins-link-path)
|
|
13
|
+
* [`cage-git plugins remove [PLUGIN]`](#cage-git-plugins-remove-plugin)
|
|
14
|
+
* [`cage-git plugins reset`](#cage-git-plugins-reset)
|
|
15
|
+
* [`cage-git plugins uninstall [PLUGIN]`](#cage-git-plugins-uninstall-plugin)
|
|
16
|
+
* [`cage-git plugins unlink [PLUGIN]`](#cage-git-plugins-unlink-plugin)
|
|
17
|
+
* [`cage-git plugins update`](#cage-git-plugins-update)
|
|
18
|
+
|
|
19
|
+
## `cage-git help [COMMAND]`
|
|
20
|
+
|
|
21
|
+
Display help for cage-git.
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
USAGE
|
|
25
|
+
$ cage-git help [COMMAND...] [-n]
|
|
26
|
+
|
|
27
|
+
ARGUMENTS
|
|
28
|
+
[COMMAND...] Command to show help for.
|
|
29
|
+
|
|
30
|
+
FLAGS
|
|
31
|
+
-n, --nested-commands Include all nested commands in the output.
|
|
32
|
+
|
|
33
|
+
DESCRIPTION
|
|
34
|
+
Display help for cage-git.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/6.2.50/src/commands/help.ts)_
|
|
38
|
+
|
|
39
|
+
## `cage-git land`
|
|
40
|
+
|
|
41
|
+
Safe, inspect-before-merge branch landing
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
USAGE
|
|
45
|
+
$ cage-git land --from <value> --into <value> [--accept | --analyze | --preview | --reject | --cleanup]
|
|
46
|
+
[--force]
|
|
47
|
+
|
|
48
|
+
FLAGS
|
|
49
|
+
--accept Merge landing branch into target
|
|
50
|
+
--analyze Preflight analysis (read-only)
|
|
51
|
+
--cleanup Delete backup branch if present
|
|
52
|
+
--force Bypass pushed-branch guard (--preview only)
|
|
53
|
+
--from=<value> (required) Branch to land
|
|
54
|
+
--into=<value> (required) Target branch to land into
|
|
55
|
+
--preview Create landing branch with cherry-picked commits
|
|
56
|
+
--reject Discard landing branch; leave original untouched
|
|
57
|
+
|
|
58
|
+
DESCRIPTION
|
|
59
|
+
Safe, inspect-before-merge branch landing
|
|
60
|
+
|
|
61
|
+
EXAMPLES
|
|
62
|
+
$ cage-git land --from feat/foo --into main --analyze
|
|
63
|
+
|
|
64
|
+
$ cage-git land --from feat/foo --into main --preview
|
|
65
|
+
|
|
66
|
+
$ cage-git land --from feat/foo --into main --accept
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
_See code: [src/commands/land.ts](https://github.com/dleangen/cage-git/blob/v0.0.1/src/commands/land.ts)_
|
|
70
|
+
|
|
71
|
+
## `cage-git plugins`
|
|
72
|
+
|
|
73
|
+
List installed plugins.
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
USAGE
|
|
77
|
+
$ cage-git plugins [--json] [--core]
|
|
78
|
+
|
|
79
|
+
FLAGS
|
|
80
|
+
--core Show core plugins.
|
|
81
|
+
|
|
82
|
+
GLOBAL FLAGS
|
|
83
|
+
--json Format output as json.
|
|
84
|
+
|
|
85
|
+
DESCRIPTION
|
|
86
|
+
List installed plugins.
|
|
87
|
+
|
|
88
|
+
EXAMPLES
|
|
89
|
+
$ cage-git plugins
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.4.74/src/commands/plugins/index.ts)_
|
|
93
|
+
|
|
94
|
+
## `cage-git plugins add PLUGIN`
|
|
95
|
+
|
|
96
|
+
Installs a plugin into cage-git.
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
USAGE
|
|
100
|
+
$ cage-git plugins add PLUGIN... [--json] [-f] [-h] [-s | -v]
|
|
101
|
+
|
|
102
|
+
ARGUMENTS
|
|
103
|
+
PLUGIN... Plugin to install.
|
|
104
|
+
|
|
105
|
+
FLAGS
|
|
106
|
+
-f, --force Force npm to fetch remote resources even if a local copy exists on disk.
|
|
107
|
+
-h, --help Show CLI help.
|
|
108
|
+
-s, --silent Silences npm output.
|
|
109
|
+
-v, --verbose Show verbose npm output.
|
|
110
|
+
|
|
111
|
+
GLOBAL FLAGS
|
|
112
|
+
--json Format output as json.
|
|
113
|
+
|
|
114
|
+
DESCRIPTION
|
|
115
|
+
Installs a plugin into cage-git.
|
|
116
|
+
|
|
117
|
+
Uses npm to install plugins.
|
|
118
|
+
|
|
119
|
+
Installation of a user-installed plugin will override a core plugin.
|
|
120
|
+
|
|
121
|
+
Use the CAGE_GIT_NPM_LOG_LEVEL environment variable to set the npm loglevel.
|
|
122
|
+
Use the CAGE_GIT_NPM_REGISTRY environment variable to set the npm registry.
|
|
123
|
+
|
|
124
|
+
ALIASES
|
|
125
|
+
$ cage-git plugins add
|
|
126
|
+
|
|
127
|
+
EXAMPLES
|
|
128
|
+
Install a plugin from npm registry.
|
|
129
|
+
|
|
130
|
+
$ cage-git plugins add myplugin
|
|
131
|
+
|
|
132
|
+
Install a plugin from a github url.
|
|
133
|
+
|
|
134
|
+
$ cage-git plugins add https://github.com/someuser/someplugin
|
|
135
|
+
|
|
136
|
+
Install a plugin from a github slug.
|
|
137
|
+
|
|
138
|
+
$ cage-git plugins add someuser/someplugin
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## `cage-git plugins:inspect PLUGIN...`
|
|
142
|
+
|
|
143
|
+
Displays installation properties of a plugin.
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
USAGE
|
|
147
|
+
$ cage-git plugins inspect PLUGIN...
|
|
148
|
+
|
|
149
|
+
ARGUMENTS
|
|
150
|
+
PLUGIN... [default: .] Plugin to inspect.
|
|
151
|
+
|
|
152
|
+
FLAGS
|
|
153
|
+
-h, --help Show CLI help.
|
|
154
|
+
-v, --verbose
|
|
155
|
+
|
|
156
|
+
GLOBAL FLAGS
|
|
157
|
+
--json Format output as json.
|
|
158
|
+
|
|
159
|
+
DESCRIPTION
|
|
160
|
+
Displays installation properties of a plugin.
|
|
161
|
+
|
|
162
|
+
EXAMPLES
|
|
163
|
+
$ cage-git plugins inspect myplugin
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.4.74/src/commands/plugins/inspect.ts)_
|
|
167
|
+
|
|
168
|
+
## `cage-git plugins install PLUGIN`
|
|
169
|
+
|
|
170
|
+
Installs a plugin into cage-git.
|
|
171
|
+
|
|
172
|
+
```
|
|
173
|
+
USAGE
|
|
174
|
+
$ cage-git plugins install PLUGIN... [--json] [-f] [-h] [-s | -v]
|
|
175
|
+
|
|
176
|
+
ARGUMENTS
|
|
177
|
+
PLUGIN... Plugin to install.
|
|
178
|
+
|
|
179
|
+
FLAGS
|
|
180
|
+
-f, --force Force npm to fetch remote resources even if a local copy exists on disk.
|
|
181
|
+
-h, --help Show CLI help.
|
|
182
|
+
-s, --silent Silences npm output.
|
|
183
|
+
-v, --verbose Show verbose npm output.
|
|
184
|
+
|
|
185
|
+
GLOBAL FLAGS
|
|
186
|
+
--json Format output as json.
|
|
187
|
+
|
|
188
|
+
DESCRIPTION
|
|
189
|
+
Installs a plugin into cage-git.
|
|
190
|
+
|
|
191
|
+
Uses npm to install plugins.
|
|
192
|
+
|
|
193
|
+
Installation of a user-installed plugin will override a core plugin.
|
|
194
|
+
|
|
195
|
+
Use the CAGE_GIT_NPM_LOG_LEVEL environment variable to set the npm loglevel.
|
|
196
|
+
Use the CAGE_GIT_NPM_REGISTRY environment variable to set the npm registry.
|
|
197
|
+
|
|
198
|
+
ALIASES
|
|
199
|
+
$ cage-git plugins add
|
|
200
|
+
|
|
201
|
+
EXAMPLES
|
|
202
|
+
Install a plugin from npm registry.
|
|
203
|
+
|
|
204
|
+
$ cage-git plugins install myplugin
|
|
205
|
+
|
|
206
|
+
Install a plugin from a github url.
|
|
207
|
+
|
|
208
|
+
$ cage-git plugins install https://github.com/someuser/someplugin
|
|
209
|
+
|
|
210
|
+
Install a plugin from a github slug.
|
|
211
|
+
|
|
212
|
+
$ cage-git plugins install someuser/someplugin
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.4.74/src/commands/plugins/install.ts)_
|
|
216
|
+
|
|
217
|
+
## `cage-git plugins link PATH`
|
|
218
|
+
|
|
219
|
+
Links a plugin into the CLI for development.
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
USAGE
|
|
223
|
+
$ cage-git plugins link PATH [-h] [--install] [-v]
|
|
224
|
+
|
|
225
|
+
ARGUMENTS
|
|
226
|
+
PATH [default: .] path to plugin
|
|
227
|
+
|
|
228
|
+
FLAGS
|
|
229
|
+
-h, --help Show CLI help.
|
|
230
|
+
-v, --verbose
|
|
231
|
+
--[no-]install Install dependencies after linking the plugin.
|
|
232
|
+
|
|
233
|
+
DESCRIPTION
|
|
234
|
+
Links a plugin into the CLI for development.
|
|
235
|
+
|
|
236
|
+
Installation of a linked plugin will override a user-installed or core plugin.
|
|
237
|
+
|
|
238
|
+
e.g. If you have a user-installed or core plugin that has a 'hello' command, installing a linked plugin with a 'hello'
|
|
239
|
+
command will override the user-installed or core plugin implementation. This is useful for development work.
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
EXAMPLES
|
|
243
|
+
$ cage-git plugins link myplugin
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.4.74/src/commands/plugins/link.ts)_
|
|
247
|
+
|
|
248
|
+
## `cage-git plugins remove [PLUGIN]`
|
|
249
|
+
|
|
250
|
+
Removes a plugin from the CLI.
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
USAGE
|
|
254
|
+
$ cage-git plugins remove [PLUGIN...] [-h] [-v]
|
|
255
|
+
|
|
256
|
+
ARGUMENTS
|
|
257
|
+
[PLUGIN...] plugin to uninstall
|
|
258
|
+
|
|
259
|
+
FLAGS
|
|
260
|
+
-h, --help Show CLI help.
|
|
261
|
+
-v, --verbose
|
|
262
|
+
|
|
263
|
+
DESCRIPTION
|
|
264
|
+
Removes a plugin from the CLI.
|
|
265
|
+
|
|
266
|
+
ALIASES
|
|
267
|
+
$ cage-git plugins unlink
|
|
268
|
+
$ cage-git plugins remove
|
|
269
|
+
|
|
270
|
+
EXAMPLES
|
|
271
|
+
$ cage-git plugins remove myplugin
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## `cage-git plugins reset`
|
|
275
|
+
|
|
276
|
+
Remove all user-installed and linked plugins.
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
USAGE
|
|
280
|
+
$ cage-git plugins reset [--hard] [--reinstall]
|
|
281
|
+
|
|
282
|
+
FLAGS
|
|
283
|
+
--hard Delete node_modules and package manager related files in addition to uninstalling plugins.
|
|
284
|
+
--reinstall Reinstall all plugins after uninstalling.
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.4.74/src/commands/plugins/reset.ts)_
|
|
288
|
+
|
|
289
|
+
## `cage-git plugins uninstall [PLUGIN]`
|
|
290
|
+
|
|
291
|
+
Removes a plugin from the CLI.
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
USAGE
|
|
295
|
+
$ cage-git plugins uninstall [PLUGIN...] [-h] [-v]
|
|
296
|
+
|
|
297
|
+
ARGUMENTS
|
|
298
|
+
[PLUGIN...] plugin to uninstall
|
|
299
|
+
|
|
300
|
+
FLAGS
|
|
301
|
+
-h, --help Show CLI help.
|
|
302
|
+
-v, --verbose
|
|
303
|
+
|
|
304
|
+
DESCRIPTION
|
|
305
|
+
Removes a plugin from the CLI.
|
|
306
|
+
|
|
307
|
+
ALIASES
|
|
308
|
+
$ cage-git plugins unlink
|
|
309
|
+
$ cage-git plugins remove
|
|
310
|
+
|
|
311
|
+
EXAMPLES
|
|
312
|
+
$ cage-git plugins uninstall myplugin
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.4.74/src/commands/plugins/uninstall.ts)_
|
|
316
|
+
|
|
317
|
+
## `cage-git plugins unlink [PLUGIN]`
|
|
318
|
+
|
|
319
|
+
Removes a plugin from the CLI.
|
|
320
|
+
|
|
321
|
+
```
|
|
322
|
+
USAGE
|
|
323
|
+
$ cage-git plugins unlink [PLUGIN...] [-h] [-v]
|
|
324
|
+
|
|
325
|
+
ARGUMENTS
|
|
326
|
+
[PLUGIN...] plugin to uninstall
|
|
327
|
+
|
|
328
|
+
FLAGS
|
|
329
|
+
-h, --help Show CLI help.
|
|
330
|
+
-v, --verbose
|
|
331
|
+
|
|
332
|
+
DESCRIPTION
|
|
333
|
+
Removes a plugin from the CLI.
|
|
334
|
+
|
|
335
|
+
ALIASES
|
|
336
|
+
$ cage-git plugins unlink
|
|
337
|
+
$ cage-git plugins remove
|
|
338
|
+
|
|
339
|
+
EXAMPLES
|
|
340
|
+
$ cage-git plugins unlink myplugin
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## `cage-git plugins update`
|
|
344
|
+
|
|
345
|
+
Update installed plugins.
|
|
346
|
+
|
|
347
|
+
```
|
|
348
|
+
USAGE
|
|
349
|
+
$ cage-git plugins update [-h] [-v]
|
|
350
|
+
|
|
351
|
+
FLAGS
|
|
352
|
+
-h, --help Show CLI help.
|
|
353
|
+
-v, --verbose
|
|
354
|
+
|
|
355
|
+
DESCRIPTION
|
|
356
|
+
Update installed plugins.
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/5.4.74/src/commands/plugins/update.ts)_
|
|
360
|
+
<!-- commandsstop -->
|
package/bin/dev.js
ADDED
package/bin/run.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
export default class Land extends Command {
|
|
3
|
+
static args: {};
|
|
4
|
+
static description: string;
|
|
5
|
+
static examples: string[];
|
|
6
|
+
static flags: {
|
|
7
|
+
accept: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
8
|
+
analyze: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
cleanup: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
10
|
+
force: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
from: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
12
|
+
into: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
13
|
+
preview: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
reject: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
15
|
+
};
|
|
16
|
+
run(): Promise<void>;
|
|
17
|
+
private runAccept;
|
|
18
|
+
private runAnalyze;
|
|
19
|
+
private runCleanup;
|
|
20
|
+
private runPreview;
|
|
21
|
+
private runReject;
|
|
22
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const core_1 = require("@oclif/core");
|
|
40
|
+
const node_child_process_1 = require("node:child_process");
|
|
41
|
+
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
43
|
+
function git(args, cwd = process.cwd()) {
|
|
44
|
+
return (0, node_child_process_1.execSync)(`git ${args}`, { cwd, encoding: 'utf8' }).trim();
|
|
45
|
+
}
|
|
46
|
+
function gitSafe(args, cwd = process.cwd()) {
|
|
47
|
+
try {
|
|
48
|
+
return git(args, cwd);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function isDirty(cwd = process.cwd()) {
|
|
55
|
+
return git('status --porcelain --untracked-files=no', cwd) !== '';
|
|
56
|
+
}
|
|
57
|
+
function branchExists(branch, cwd = process.cwd()) {
|
|
58
|
+
return gitSafe(`rev-parse --verify ${branch}`, cwd) !== null;
|
|
59
|
+
}
|
|
60
|
+
function mergeBase(from, into, cwd = process.cwd()) {
|
|
61
|
+
return git(`merge-base ${from} ${into}`, cwd);
|
|
62
|
+
}
|
|
63
|
+
function anchorDir(cwd = process.cwd()) {
|
|
64
|
+
return node_path_1.default.join(cwd, '.cage', 'land');
|
|
65
|
+
}
|
|
66
|
+
function anchorPath(from, cwd = process.cwd()) {
|
|
67
|
+
return node_path_1.default.join(anchorDir(cwd), `${from}.preview-base`);
|
|
68
|
+
}
|
|
69
|
+
function ensureAnchorDir(cwd = process.cwd()) {
|
|
70
|
+
fs.mkdirSync(anchorDir(cwd), { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
class Land extends core_1.Command {
|
|
73
|
+
static args = {};
|
|
74
|
+
static description = 'Safe, inspect-before-merge branch landing';
|
|
75
|
+
static examples = [
|
|
76
|
+
'<%= config.bin %> <%= command.id %> --from feat/foo --into main --analyze',
|
|
77
|
+
'<%= config.bin %> <%= command.id %> --from feat/foo --into main --preview',
|
|
78
|
+
'<%= config.bin %> <%= command.id %> --from feat/foo --into main --accept',
|
|
79
|
+
];
|
|
80
|
+
static flags = {
|
|
81
|
+
accept: core_1.Flags.boolean({ description: 'Merge landing branch into target', exclusive: ['analyze', 'preview', 'reject', 'cleanup'] }),
|
|
82
|
+
analyze: core_1.Flags.boolean({ description: 'Preflight analysis (read-only)', exclusive: ['preview', 'accept', 'reject', 'cleanup'] }),
|
|
83
|
+
cleanup: core_1.Flags.boolean({ description: 'Delete backup branch if present', exclusive: ['analyze', 'preview', 'accept', 'reject'] }),
|
|
84
|
+
force: core_1.Flags.boolean({ description: 'Bypass pushed-branch guard (--preview only)' }),
|
|
85
|
+
from: core_1.Flags.string({ description: 'Branch to land', required: true }),
|
|
86
|
+
into: core_1.Flags.string({ description: 'Target branch to land into', required: true }),
|
|
87
|
+
preview: core_1.Flags.boolean({ description: 'Create landing branch with cherry-picked commits', exclusive: ['analyze', 'accept', 'reject', 'cleanup'] }),
|
|
88
|
+
reject: core_1.Flags.boolean({ description: 'Discard landing branch; leave original untouched', exclusive: ['analyze', 'preview', 'accept', 'cleanup'] }),
|
|
89
|
+
};
|
|
90
|
+
async run() {
|
|
91
|
+
const { flags } = await this.parse(Land);
|
|
92
|
+
const { force, from, into } = flags;
|
|
93
|
+
const modeCount = [flags.analyze, flags.preview, flags.accept, flags.reject, flags.cleanup].filter(Boolean).length;
|
|
94
|
+
if (modeCount === 0) {
|
|
95
|
+
this.error('Exactly one mode is required: --analyze, --preview, --accept, --reject, or --cleanup');
|
|
96
|
+
}
|
|
97
|
+
const cwd = process.cwd();
|
|
98
|
+
if (flags.analyze)
|
|
99
|
+
return this.runAnalyze(from, into, cwd);
|
|
100
|
+
if (flags.preview)
|
|
101
|
+
return this.runPreview(from, into, force, cwd);
|
|
102
|
+
if (flags.accept)
|
|
103
|
+
return this.runAccept(from, into, cwd);
|
|
104
|
+
if (flags.reject)
|
|
105
|
+
return this.runReject(from, cwd);
|
|
106
|
+
if (flags.cleanup)
|
|
107
|
+
return this.runCleanup(from, cwd);
|
|
108
|
+
}
|
|
109
|
+
runAccept(from, into, cwd) {
|
|
110
|
+
// Dirty-tree gate
|
|
111
|
+
if (isDirty(cwd)) {
|
|
112
|
+
this.error('Working tree is dirty. Commit or stash changes before running --accept.');
|
|
113
|
+
}
|
|
114
|
+
const landingBranch = `${from}-landing`;
|
|
115
|
+
// Landing branch must exist
|
|
116
|
+
if (!branchExists(landingBranch, cwd)) {
|
|
117
|
+
this.error(`Landing branch '${landingBranch}' does not exist. Run --preview first.`);
|
|
118
|
+
}
|
|
119
|
+
// Drift guard
|
|
120
|
+
const anchor = anchorPath(from, cwd);
|
|
121
|
+
if (fs.existsSync(anchor)) {
|
|
122
|
+
const previewBase = fs.readFileSync(anchor, 'utf8').trim();
|
|
123
|
+
const currentTip = git(`rev-parse ${into}`, cwd);
|
|
124
|
+
if (previewBase !== currentTip) {
|
|
125
|
+
this.error(`Target '${into}' has moved since --preview. Run --reject then --preview again.`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Merge
|
|
129
|
+
git(`checkout ${into}`, cwd);
|
|
130
|
+
git(`merge --no-ff ${landingBranch} -m "Merge branch '${from}' into ${into}"`, cwd);
|
|
131
|
+
// Clean up
|
|
132
|
+
git(`branch -D ${landingBranch}`, cwd);
|
|
133
|
+
if (fs.existsSync(anchor))
|
|
134
|
+
fs.unlinkSync(anchor);
|
|
135
|
+
const tip = git(`rev-parse --short HEAD`, cwd);
|
|
136
|
+
this.log(`Branch '${from}' landed into '${into}'. Tip: ${tip}`);
|
|
137
|
+
}
|
|
138
|
+
runAnalyze(from, into, cwd) {
|
|
139
|
+
const base = mergeBase(from, into, cwd);
|
|
140
|
+
// Commits to land
|
|
141
|
+
const commits = gitSafe(`log --oneline ${into}..${from}`, cwd) ?? '';
|
|
142
|
+
this.log('\nCommits to land:');
|
|
143
|
+
this.log(commits || ' (none)');
|
|
144
|
+
// Drift distance
|
|
145
|
+
const driftCount = Number(git(`rev-list --count ${base}..${into}`, cwd));
|
|
146
|
+
this.log(`\nDrift distance: ${driftCount} commit(s) on ${into} since fork`);
|
|
147
|
+
if (driftCount > 0) {
|
|
148
|
+
this.log(` Warning: ${into} has advanced ${driftCount} commit(s) since the fork point.`);
|
|
149
|
+
}
|
|
150
|
+
// File overlap
|
|
151
|
+
const targetFiles = new Set(git(`diff --name-only ${base} ${into}`, cwd).split('\n').filter(Boolean));
|
|
152
|
+
const branchFiles = new Set(git(`diff --name-only ${base} ${from}`, cwd).split('\n').filter(Boolean));
|
|
153
|
+
const overlap = [...targetFiles].filter((f) => branchFiles.has(f));
|
|
154
|
+
if (overlap.length > 0) {
|
|
155
|
+
this.log('\nFile overlap (potential conflicts):');
|
|
156
|
+
for (const f of overlap)
|
|
157
|
+
this.log(` ${f}`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
this.log('\nNo file overlap detected.');
|
|
161
|
+
}
|
|
162
|
+
// Remote status
|
|
163
|
+
const fromRemote = gitSafe(`rev-parse --verify origin/${from}`, cwd) !== null;
|
|
164
|
+
const intoRemote = gitSafe(`rev-parse --verify origin/${into}`, cwd) !== null;
|
|
165
|
+
this.log(`\nRemote tracking: ${from}=${fromRemote ? 'yes' : 'no'}, ${into}=${intoRemote ? 'yes' : 'no'}`);
|
|
166
|
+
}
|
|
167
|
+
runCleanup(from, cwd) {
|
|
168
|
+
const backupBranch = `${from}-backup`;
|
|
169
|
+
let cleaned = false;
|
|
170
|
+
if (branchExists(backupBranch, cwd)) {
|
|
171
|
+
git(`branch -D ${backupBranch}`, cwd);
|
|
172
|
+
this.log(`Deleted backup branch '${backupBranch}'.`);
|
|
173
|
+
cleaned = true;
|
|
174
|
+
}
|
|
175
|
+
const anchor = anchorPath(from, cwd);
|
|
176
|
+
if (fs.existsSync(anchor)) {
|
|
177
|
+
fs.unlinkSync(anchor);
|
|
178
|
+
this.log(`Removed stale drift anchor for '${from}'.`);
|
|
179
|
+
cleaned = true;
|
|
180
|
+
}
|
|
181
|
+
if (!cleaned) {
|
|
182
|
+
this.log('Nothing to clean up.');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
runPreview(from, into, force, cwd) {
|
|
186
|
+
// Dirty-tree gate
|
|
187
|
+
if (isDirty(cwd)) {
|
|
188
|
+
this.error('Working tree is dirty. Commit or stash changes before running --preview.');
|
|
189
|
+
}
|
|
190
|
+
// Pushed-branch guard
|
|
191
|
+
if (!force && gitSafe(`rev-parse --verify origin/${from}`, cwd) !== null) {
|
|
192
|
+
this.error(`Branch '${from}' has a remote-tracking ref (origin/${from}). ` +
|
|
193
|
+
`Use --force to bypass this guard.`);
|
|
194
|
+
}
|
|
195
|
+
const landingBranch = `${from}-landing`;
|
|
196
|
+
// Abort if landing branch already exists
|
|
197
|
+
if (branchExists(landingBranch, cwd)) {
|
|
198
|
+
this.error(`Landing branch '${landingBranch}' already exists. Run --reject first.`);
|
|
199
|
+
}
|
|
200
|
+
const base = mergeBase(from, into, cwd);
|
|
201
|
+
// Record drift anchor
|
|
202
|
+
ensureAnchorDir(cwd);
|
|
203
|
+
const gitignorePath = node_path_1.default.join(cwd, '.gitignore');
|
|
204
|
+
const gitignored = fs.existsSync(gitignorePath) &&
|
|
205
|
+
fs.readFileSync(gitignorePath, 'utf8').split('\n').some((line) => line.trim() === '.cage' || line.trim() === '.cage/');
|
|
206
|
+
if (!gitignored) {
|
|
207
|
+
this.warn('`.cage/` is not in .gitignore — drift anchors may be accidentally committed.');
|
|
208
|
+
}
|
|
209
|
+
const targetTip = git(`rev-parse ${into}`, cwd);
|
|
210
|
+
fs.writeFileSync(anchorPath(from, cwd), targetTip);
|
|
211
|
+
// Create landing branch
|
|
212
|
+
git(`checkout -b ${landingBranch} ${into}`, cwd);
|
|
213
|
+
// Cherry-pick
|
|
214
|
+
try {
|
|
215
|
+
git(`cherry-pick ${base}..${from}`, cwd);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
this.log(`Cherry-pick conflict on '${landingBranch}'.\n` +
|
|
219
|
+
`Resolve conflicts, then:\n` +
|
|
220
|
+
` git cherry-pick --continue\n` +
|
|
221
|
+
`Or abort with:\n` +
|
|
222
|
+
` git cherry-pick --abort && cage land --from ${from} --into ${into} --reject`);
|
|
223
|
+
this.exit(1);
|
|
224
|
+
}
|
|
225
|
+
// Return to target branch — leaves repo in a predictable state for --reject/--accept
|
|
226
|
+
git(`checkout ${into}`, cwd);
|
|
227
|
+
this.log(`Landing branch '${landingBranch}' created.`);
|
|
228
|
+
this.log(`Inspect with: git diff ${into}..${landingBranch}`);
|
|
229
|
+
}
|
|
230
|
+
runReject(from, cwd) {
|
|
231
|
+
const landingBranch = `${from}-landing`;
|
|
232
|
+
let cleaned = false;
|
|
233
|
+
if (branchExists(landingBranch, cwd)) {
|
|
234
|
+
git(`branch -D ${landingBranch}`, cwd);
|
|
235
|
+
cleaned = true;
|
|
236
|
+
}
|
|
237
|
+
const anchor = anchorPath(from, cwd);
|
|
238
|
+
if (fs.existsSync(anchor)) {
|
|
239
|
+
fs.unlinkSync(anchor);
|
|
240
|
+
cleaned = true;
|
|
241
|
+
}
|
|
242
|
+
if (cleaned) {
|
|
243
|
+
this.log(`Landing branch '${landingBranch}' discarded. '${from}' is untouched.`);
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
this.log(`Nothing to reject — '${landingBranch}' did not exist.`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
exports.default = Land;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { run } from '@oclif/core';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
{
|
|
2
|
+
"commands": {
|
|
3
|
+
"land": {
|
|
4
|
+
"aliases": [],
|
|
5
|
+
"args": {},
|
|
6
|
+
"description": "Safe, inspect-before-merge branch landing",
|
|
7
|
+
"examples": [
|
|
8
|
+
"<%= config.bin %> <%= command.id %> --from feat/foo --into main --analyze",
|
|
9
|
+
"<%= config.bin %> <%= command.id %> --from feat/foo --into main --preview",
|
|
10
|
+
"<%= config.bin %> <%= command.id %> --from feat/foo --into main --accept"
|
|
11
|
+
],
|
|
12
|
+
"flags": {
|
|
13
|
+
"accept": {
|
|
14
|
+
"description": "Merge landing branch into target",
|
|
15
|
+
"exclusive": [
|
|
16
|
+
"analyze",
|
|
17
|
+
"preview",
|
|
18
|
+
"reject",
|
|
19
|
+
"cleanup"
|
|
20
|
+
],
|
|
21
|
+
"name": "accept",
|
|
22
|
+
"allowNo": false,
|
|
23
|
+
"type": "boolean"
|
|
24
|
+
},
|
|
25
|
+
"analyze": {
|
|
26
|
+
"description": "Preflight analysis (read-only)",
|
|
27
|
+
"exclusive": [
|
|
28
|
+
"preview",
|
|
29
|
+
"accept",
|
|
30
|
+
"reject",
|
|
31
|
+
"cleanup"
|
|
32
|
+
],
|
|
33
|
+
"name": "analyze",
|
|
34
|
+
"allowNo": false,
|
|
35
|
+
"type": "boolean"
|
|
36
|
+
},
|
|
37
|
+
"cleanup": {
|
|
38
|
+
"description": "Delete backup branch if present",
|
|
39
|
+
"exclusive": [
|
|
40
|
+
"analyze",
|
|
41
|
+
"preview",
|
|
42
|
+
"accept",
|
|
43
|
+
"reject"
|
|
44
|
+
],
|
|
45
|
+
"name": "cleanup",
|
|
46
|
+
"allowNo": false,
|
|
47
|
+
"type": "boolean"
|
|
48
|
+
},
|
|
49
|
+
"force": {
|
|
50
|
+
"description": "Bypass pushed-branch guard (--preview only)",
|
|
51
|
+
"name": "force",
|
|
52
|
+
"allowNo": false,
|
|
53
|
+
"type": "boolean"
|
|
54
|
+
},
|
|
55
|
+
"from": {
|
|
56
|
+
"description": "Branch to land",
|
|
57
|
+
"name": "from",
|
|
58
|
+
"required": true,
|
|
59
|
+
"hasDynamicHelp": false,
|
|
60
|
+
"multiple": false,
|
|
61
|
+
"type": "option"
|
|
62
|
+
},
|
|
63
|
+
"into": {
|
|
64
|
+
"description": "Target branch to land into",
|
|
65
|
+
"name": "into",
|
|
66
|
+
"required": true,
|
|
67
|
+
"hasDynamicHelp": false,
|
|
68
|
+
"multiple": false,
|
|
69
|
+
"type": "option"
|
|
70
|
+
},
|
|
71
|
+
"preview": {
|
|
72
|
+
"description": "Create landing branch with cherry-picked commits",
|
|
73
|
+
"exclusive": [
|
|
74
|
+
"analyze",
|
|
75
|
+
"accept",
|
|
76
|
+
"reject",
|
|
77
|
+
"cleanup"
|
|
78
|
+
],
|
|
79
|
+
"name": "preview",
|
|
80
|
+
"allowNo": false,
|
|
81
|
+
"type": "boolean"
|
|
82
|
+
},
|
|
83
|
+
"reject": {
|
|
84
|
+
"description": "Discard landing branch; leave original untouched",
|
|
85
|
+
"exclusive": [
|
|
86
|
+
"analyze",
|
|
87
|
+
"preview",
|
|
88
|
+
"accept",
|
|
89
|
+
"cleanup"
|
|
90
|
+
],
|
|
91
|
+
"name": "reject",
|
|
92
|
+
"allowNo": false,
|
|
93
|
+
"type": "boolean"
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"hasDynamicHelp": false,
|
|
97
|
+
"hiddenAliases": [],
|
|
98
|
+
"id": "land",
|
|
99
|
+
"pluginAlias": "@dleangen/cage-git",
|
|
100
|
+
"pluginName": "@dleangen/cage-git",
|
|
101
|
+
"pluginType": "core",
|
|
102
|
+
"strict": true,
|
|
103
|
+
"enableJsonFlag": false,
|
|
104
|
+
"isESM": false,
|
|
105
|
+
"relativePath": [
|
|
106
|
+
"dist",
|
|
107
|
+
"commands",
|
|
108
|
+
"land.js"
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"version": "0.0.1"
|
|
113
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dleangen/cage-git",
|
|
3
|
+
"description": "Git workflow mechanics for CAGE projects.",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"author": "dleangen",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cage-git": "bin/run.js"
|
|
8
|
+
},
|
|
9
|
+
"bugs": "https://github.com/dleangen/cage-git/issues",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@oclif/core": "^4",
|
|
12
|
+
"@oclif/plugin-help": "^6",
|
|
13
|
+
"@oclif/plugin-plugins": "^5"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@eslint/compat": "^1",
|
|
17
|
+
"@oclif/prettier-config": "^0.2.1",
|
|
18
|
+
"@oclif/test": "^4",
|
|
19
|
+
"@types/chai": "^4",
|
|
20
|
+
"@types/mocha": "^10",
|
|
21
|
+
"@types/node": "^18",
|
|
22
|
+
"chai": "^4",
|
|
23
|
+
"eslint": "^9",
|
|
24
|
+
"eslint-config-oclif": "^6",
|
|
25
|
+
"eslint-config-prettier": "^10",
|
|
26
|
+
"mocha": "^11",
|
|
27
|
+
"oclif": "^4",
|
|
28
|
+
"shx": "^0.3.3",
|
|
29
|
+
"ts-node": "^10",
|
|
30
|
+
"typescript": "^5"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18.0.0"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"./bin",
|
|
37
|
+
"./dist",
|
|
38
|
+
"./oclif.manifest.json"
|
|
39
|
+
],
|
|
40
|
+
"homepage": "https://github.com/dleangen/cage-git",
|
|
41
|
+
"keywords": [
|
|
42
|
+
"oclif",
|
|
43
|
+
"cage",
|
|
44
|
+
"git"
|
|
45
|
+
],
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"main": "dist/index.js",
|
|
48
|
+
"oclif": {
|
|
49
|
+
"bin": "cage-git",
|
|
50
|
+
"dirname": "cage-git",
|
|
51
|
+
"commands": "./dist/commands",
|
|
52
|
+
"plugins": [
|
|
53
|
+
"@oclif/plugin-help",
|
|
54
|
+
"@oclif/plugin-plugins"
|
|
55
|
+
],
|
|
56
|
+
"topicSeparator": " "
|
|
57
|
+
},
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "git+https://github.com/dleangen/cage-git.git"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "shx rm -rf dist && tsc -b",
|
|
64
|
+
"lint": "eslint",
|
|
65
|
+
"postpack": "shx rm -f oclif.manifest.json",
|
|
66
|
+
"posttest": "npm run lint",
|
|
67
|
+
"prepack": "oclif manifest && oclif readme",
|
|
68
|
+
"test": "mocha --forbid-only \"test/**/*.test.ts\"",
|
|
69
|
+
"version": "oclif readme && git add README.md"
|
|
70
|
+
},
|
|
71
|
+
"types": "dist/index.d.ts"
|
|
72
|
+
}
|