@mbluemer_2/gittyup 0.1.2 → 0.1.8
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 +3 -2
- package/package.json +1 -1
- package/dist/aliases.js +0 -32
- package/dist/cli.js +0 -5
- package/dist/config.js +0 -127
- package/dist/errors.js +0 -19
- package/dist/git.js +0 -293
- package/dist/interactive.js +0 -206
- package/dist/metadata.js +0 -83
- package/dist/output.js +0 -211
- package/dist/process.js +0 -46
- package/dist/program.js +0 -293
- package/dist/projects.js +0 -308
- package/dist/tmux.js +0 -146
- package/dist/types.js +0 -2
package/README.md
CHANGED
|
@@ -223,11 +223,12 @@ Both `pnpm dev project list` and `pnpm dev -- project list` are supported.
|
|
|
223
223
|
Updating `package.json` to a new version and merging that change to `main`
|
|
224
224
|
automatically runs the release workflow.
|
|
225
225
|
|
|
226
|
-
- CI runs
|
|
226
|
+
- CI runs first on the `main` push
|
|
227
|
+
- Release only runs after that CI run succeeds
|
|
227
228
|
- The workflow creates and pushes the matching `vX.Y.Z` tag
|
|
228
229
|
- `pnpm pack` creates an installable tarball
|
|
229
230
|
- GitHub Releases gets the tarball and `SHA256SUMS.txt` attached automatically
|
|
230
|
-
- The same workflow publishes `@mbluemer_2/gittyup` to npm
|
|
231
|
+
- The same workflow publishes `@mbluemer_2/gittyup` to npm using the `NPM_TOKEN` repo secret
|
|
231
232
|
|
|
232
233
|
You can still create a release manually by pushing a tag like `v0.1.0` yourself.
|
|
233
234
|
|
package/package.json
CHANGED
package/dist/aliases.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizeArgv = normalizeArgv;
|
|
4
|
-
const COMMAND_ALIASES = {
|
|
5
|
-
p: ['project'],
|
|
6
|
-
pl: ['project', 'list'],
|
|
7
|
-
pd: ['project', 'doctor'],
|
|
8
|
-
pg: ['project', 'init', 'get'],
|
|
9
|
-
ps: ['project', 'init', 'set'],
|
|
10
|
-
px: ['project', 'init', 'clear'],
|
|
11
|
-
pc: ['project', 'create'],
|
|
12
|
-
pcl: ['project', 'clone'],
|
|
13
|
-
pi: ['project', 'import'],
|
|
14
|
-
pr: ['project', 'rename'],
|
|
15
|
-
w: ['worktree'],
|
|
16
|
-
wl: ['worktree', 'list'],
|
|
17
|
-
wa: ['worktree', 'add'],
|
|
18
|
-
wr: ['worktree', 'remove'],
|
|
19
|
-
s: ['status'],
|
|
20
|
-
st: ['status'],
|
|
21
|
-
t: ['tmux'],
|
|
22
|
-
tl: ['tmux', 'launch'],
|
|
23
|
-
ts: ['tmux', 'switch'],
|
|
24
|
-
};
|
|
25
|
-
function normalizeArgv(argv) {
|
|
26
|
-
const normalized = argv[2] === '--' ? [argv[0], argv[1], ...argv.slice(3)] : [...argv];
|
|
27
|
-
const alias = normalized[2] ? COMMAND_ALIASES[normalized[2]] : undefined;
|
|
28
|
-
if (!alias) {
|
|
29
|
-
return normalized;
|
|
30
|
-
}
|
|
31
|
-
return [normalized[0], normalized[1], ...alias, ...normalized.slice(3)];
|
|
32
|
-
}
|
package/dist/cli.js
DELETED
package/dist/config.js
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
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.PROJECT_METADATA_FILENAME = exports.MAIN_WORKTREE_ALIAS = exports.BARE_REPO_DIRNAME = exports.DEFAULT_ROOT_DIR = void 0;
|
|
7
|
-
exports.expandHome = expandHome;
|
|
8
|
-
exports.resolveRootDir = resolveRootDir;
|
|
9
|
-
exports.validateProjectName = validateProjectName;
|
|
10
|
-
exports.validateAlias = validateAlias;
|
|
11
|
-
exports.deriveAlias = deriveAlias;
|
|
12
|
-
exports.inferProjectName = inferProjectName;
|
|
13
|
-
exports.validateBranchName = validateBranchName;
|
|
14
|
-
exports.validateRemoteUrl = validateRemoteUrl;
|
|
15
|
-
exports.buildProjectPaths = buildProjectPaths;
|
|
16
|
-
const node_os_1 = __importDefault(require("node:os"));
|
|
17
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
18
|
-
const errors_1 = require("./errors");
|
|
19
|
-
exports.DEFAULT_ROOT_DIR = node_path_1.default.join(node_os_1.default.homedir(), 'src');
|
|
20
|
-
exports.BARE_REPO_DIRNAME = 'repo.git';
|
|
21
|
-
exports.MAIN_WORKTREE_ALIAS = 'main';
|
|
22
|
-
exports.PROJECT_METADATA_FILENAME = '.gittyup.json';
|
|
23
|
-
function expandHome(value) {
|
|
24
|
-
if (value === '~') {
|
|
25
|
-
return node_os_1.default.homedir();
|
|
26
|
-
}
|
|
27
|
-
if (value.startsWith('~/')) {
|
|
28
|
-
return node_path_1.default.join(node_os_1.default.homedir(), value.slice(2));
|
|
29
|
-
}
|
|
30
|
-
if (value.startsWith('$HOME/')) {
|
|
31
|
-
return node_path_1.default.join(node_os_1.default.homedir(), value.slice(6));
|
|
32
|
-
}
|
|
33
|
-
return value;
|
|
34
|
-
}
|
|
35
|
-
function resolveRootDir(input) {
|
|
36
|
-
return node_path_1.default.resolve(expandHome(input ?? process.env.GITTYUP_ROOT ?? exports.DEFAULT_ROOT_DIR));
|
|
37
|
-
}
|
|
38
|
-
function validateProjectName(name) {
|
|
39
|
-
if (name.trim().length === 0) {
|
|
40
|
-
throw new errors_1.CliError('Project name cannot be empty.');
|
|
41
|
-
}
|
|
42
|
-
if (name !== name.trim()) {
|
|
43
|
-
throw new errors_1.CliError('Project name cannot start or end with whitespace.');
|
|
44
|
-
}
|
|
45
|
-
if (/[/\\]/.test(name)) {
|
|
46
|
-
throw new errors_1.CliError('Project name must be a single directory name.');
|
|
47
|
-
}
|
|
48
|
-
if (name === '.' || name === '..') {
|
|
49
|
-
throw new errors_1.CliError("Project name must not be '.' or '..'.");
|
|
50
|
-
}
|
|
51
|
-
return name;
|
|
52
|
-
}
|
|
53
|
-
function validateAlias(alias) {
|
|
54
|
-
if (alias.trim().length === 0) {
|
|
55
|
-
throw new errors_1.CliError('Worktree alias cannot be empty.');
|
|
56
|
-
}
|
|
57
|
-
if (alias !== alias.trim()) {
|
|
58
|
-
throw new errors_1.CliError('Worktree alias cannot start or end with whitespace.');
|
|
59
|
-
}
|
|
60
|
-
if (/[/\\]/.test(alias)) {
|
|
61
|
-
throw new errors_1.CliError('Worktree alias must be a single directory name.');
|
|
62
|
-
}
|
|
63
|
-
if (alias === '.' || alias === '..') {
|
|
64
|
-
throw new errors_1.CliError("Worktree alias must not be '.' or '..'.");
|
|
65
|
-
}
|
|
66
|
-
return alias;
|
|
67
|
-
}
|
|
68
|
-
function deriveAlias(branch) {
|
|
69
|
-
const alias = branch
|
|
70
|
-
.replace(/[/\\]+/g, '-')
|
|
71
|
-
.replace(/[^A-Za-z0-9._-]+/g, '-')
|
|
72
|
-
.replace(/-+/g, '-')
|
|
73
|
-
.replace(/^-|-$/g, '');
|
|
74
|
-
return validateAlias(alias || 'worktree');
|
|
75
|
-
}
|
|
76
|
-
function inferProjectName(remote) {
|
|
77
|
-
const trimmed = remote.trim().replace(/\/$/, '');
|
|
78
|
-
const scpTail = trimmed.includes(':') && !trimmed.includes('://')
|
|
79
|
-
? trimmed.slice(trimmed.lastIndexOf(':') + 1)
|
|
80
|
-
: trimmed;
|
|
81
|
-
const tail = scpTail.split('/').pop() ?? scpTail;
|
|
82
|
-
const name = tail.endsWith('.git') ? tail.slice(0, -4) : tail;
|
|
83
|
-
return validateProjectName(name);
|
|
84
|
-
}
|
|
85
|
-
// eslint-disable-next-line no-control-regex
|
|
86
|
-
const INVALID_BRANCH_CHARS = /[\x00-\x1f\x7f~^:?*[\\]/;
|
|
87
|
-
const INVALID_BRANCH_START = /^[./]/;
|
|
88
|
-
const INVALID_BRANCH_END = /[./]$/;
|
|
89
|
-
const INVALID_BRANCH_SEQUENCE = /(\.\.)|(\/\/)|(@\{)/;
|
|
90
|
-
function validateBranchName(branch) {
|
|
91
|
-
const trimmed = branch.trim();
|
|
92
|
-
if (trimmed.length === 0) {
|
|
93
|
-
throw new errors_1.CliError('Branch name cannot be empty.');
|
|
94
|
-
}
|
|
95
|
-
if (INVALID_BRANCH_CHARS.test(trimmed)) {
|
|
96
|
-
throw new errors_1.CliError('Branch name contains invalid characters (control chars, ~, ^, :, ?, *, [, \\).');
|
|
97
|
-
}
|
|
98
|
-
if (INVALID_BRANCH_START.test(trimmed)) {
|
|
99
|
-
throw new errors_1.CliError('Branch name cannot start with . or /.');
|
|
100
|
-
}
|
|
101
|
-
if (INVALID_BRANCH_END.test(trimmed)) {
|
|
102
|
-
throw new errors_1.CliError('Branch name cannot end with . or /.');
|
|
103
|
-
}
|
|
104
|
-
if (INVALID_BRANCH_SEQUENCE.test(trimmed)) {
|
|
105
|
-
throw new errors_1.CliError('Branch name cannot contain "..", "//", or "@{".');
|
|
106
|
-
}
|
|
107
|
-
return trimmed;
|
|
108
|
-
}
|
|
109
|
-
function validateRemoteUrl(remote) {
|
|
110
|
-
const trimmed = remote.trim();
|
|
111
|
-
if (trimmed.length === 0) {
|
|
112
|
-
throw new errors_1.CliError('Remote URL cannot be empty.');
|
|
113
|
-
}
|
|
114
|
-
return trimmed;
|
|
115
|
-
}
|
|
116
|
-
function buildProjectPaths(rootDir, projectName) {
|
|
117
|
-
const name = validateProjectName(projectName);
|
|
118
|
-
const rootPath = node_path_1.default.join(rootDir, name);
|
|
119
|
-
const gitDir = node_path_1.default.join(rootPath, exports.BARE_REPO_DIRNAME);
|
|
120
|
-
const metadataPath = node_path_1.default.join(rootPath, exports.PROJECT_METADATA_FILENAME);
|
|
121
|
-
return {
|
|
122
|
-
name,
|
|
123
|
-
rootPath,
|
|
124
|
-
gitDir,
|
|
125
|
-
metadataPath,
|
|
126
|
-
};
|
|
127
|
-
}
|
package/dist/errors.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.CliError = void 0;
|
|
4
|
-
exports.toErrorMessage = toErrorMessage;
|
|
5
|
-
class CliError extends Error {
|
|
6
|
-
exitCode;
|
|
7
|
-
constructor(message, exitCode = 1) {
|
|
8
|
-
super(message);
|
|
9
|
-
this.name = 'CliError';
|
|
10
|
-
this.exitCode = exitCode;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
exports.CliError = CliError;
|
|
14
|
-
function toErrorMessage(error) {
|
|
15
|
-
if (error instanceof Error) {
|
|
16
|
-
return error.message;
|
|
17
|
-
}
|
|
18
|
-
return String(error);
|
|
19
|
-
}
|
package/dist/git.js
DELETED
|
@@ -1,293 +0,0 @@
|
|
|
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.checkGitAvailable = checkGitAvailable;
|
|
7
|
-
exports.runGit = runGit;
|
|
8
|
-
exports.initBareRepo = initBareRepo;
|
|
9
|
-
exports.cloneBareRepo = cloneBareRepo;
|
|
10
|
-
exports.getRepoTopLevel = getRepoTopLevel;
|
|
11
|
-
exports.isBareRepository = isBareRepository;
|
|
12
|
-
exports.bootstrapBareRepo = bootstrapBareRepo;
|
|
13
|
-
exports.repoHasCommits = repoHasCommits;
|
|
14
|
-
exports.getDefaultBranch = getDefaultBranch;
|
|
15
|
-
exports.localBranchExists = localBranchExists;
|
|
16
|
-
exports.parseWorktreeList = parseWorktreeList;
|
|
17
|
-
exports.listWorktrees = listWorktrees;
|
|
18
|
-
exports.listLinkedWorktrees = listLinkedWorktrees;
|
|
19
|
-
exports.addWorktree = addWorktree;
|
|
20
|
-
exports.removeWorktree = removeWorktree;
|
|
21
|
-
exports.repairWorktrees = repairWorktrees;
|
|
22
|
-
exports.getCurrentBranch = getCurrentBranch;
|
|
23
|
-
exports.getHeadShortSha = getHeadShortSha;
|
|
24
|
-
exports.getWorktreeStatus = getWorktreeStatus;
|
|
25
|
-
const promises_1 = require("node:fs/promises");
|
|
26
|
-
const node_os_1 = __importDefault(require("node:os"));
|
|
27
|
-
const node_path_1 = __importDefault(require("node:path"));
|
|
28
|
-
const errors_1 = require("./errors");
|
|
29
|
-
const process_1 = require("./process");
|
|
30
|
-
const MINIMUM_GIT_VERSION = '2.17.0';
|
|
31
|
-
function compareVersions(a, b) {
|
|
32
|
-
const aParts = a.split('.').map(Number);
|
|
33
|
-
const bParts = b.split('.').map(Number);
|
|
34
|
-
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
35
|
-
const aVal = aParts[i] ?? 0;
|
|
36
|
-
const bVal = bParts[i] ?? 0;
|
|
37
|
-
if (aVal > bVal)
|
|
38
|
-
return 1;
|
|
39
|
-
if (aVal < bVal)
|
|
40
|
-
return -1;
|
|
41
|
-
}
|
|
42
|
-
return 0;
|
|
43
|
-
}
|
|
44
|
-
async function checkGitAvailable() {
|
|
45
|
-
try {
|
|
46
|
-
const versionOutput = await runGit(['--version']);
|
|
47
|
-
const match = versionOutput.match(/git version (\d+\.\d+\.\d+)/);
|
|
48
|
-
if (!match) {
|
|
49
|
-
throw new errors_1.CliError('Unable to determine git version. Please ensure git is installed.');
|
|
50
|
-
}
|
|
51
|
-
const version = match[1];
|
|
52
|
-
if (compareVersions(version, MINIMUM_GIT_VERSION) < 0) {
|
|
53
|
-
throw new errors_1.CliError(`Git version ${MINIMUM_GIT_VERSION} or higher is required. Found version ${version}.`);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
if (error instanceof errors_1.CliError) {
|
|
58
|
-
throw error;
|
|
59
|
-
}
|
|
60
|
-
throw new errors_1.CliError('Git is not installed or not available in PATH. Please install git to use gittyup.');
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
async function runGit(args, options = {}) {
|
|
64
|
-
return (0, process_1.runCommand)('git', args, options);
|
|
65
|
-
}
|
|
66
|
-
async function initBareRepo(gitDir) {
|
|
67
|
-
await runGit(['init', '--bare', '--initial-branch=main', gitDir]);
|
|
68
|
-
}
|
|
69
|
-
async function cloneBareRepo(remote, gitDir) {
|
|
70
|
-
await runGit(['clone', '--bare', remote, gitDir]);
|
|
71
|
-
}
|
|
72
|
-
async function getRepoTopLevel(repoPath) {
|
|
73
|
-
return runGit(['-C', repoPath, 'rev-parse', '--show-toplevel']);
|
|
74
|
-
}
|
|
75
|
-
async function isBareRepository(repoPath) {
|
|
76
|
-
const output = await runGit([
|
|
77
|
-
'-C',
|
|
78
|
-
repoPath,
|
|
79
|
-
'rev-parse',
|
|
80
|
-
'--is-bare-repository',
|
|
81
|
-
]);
|
|
82
|
-
return output === 'true';
|
|
83
|
-
}
|
|
84
|
-
async function bootstrapBareRepo(gitDir, branch) {
|
|
85
|
-
const tempDir = await (0, promises_1.mkdtemp)(node_path_1.default.join(node_os_1.default.tmpdir(), 'gittyup-bootstrap-'));
|
|
86
|
-
try {
|
|
87
|
-
await runGit(['init', '--initial-branch', branch, tempDir]);
|
|
88
|
-
await runGit(['-C', tempDir, 'remote', 'add', 'origin', gitDir]);
|
|
89
|
-
await runGit([
|
|
90
|
-
'-C',
|
|
91
|
-
tempDir,
|
|
92
|
-
'-c',
|
|
93
|
-
'user.name=gittyup',
|
|
94
|
-
'-c',
|
|
95
|
-
'user.email=gittyup@local',
|
|
96
|
-
'commit',
|
|
97
|
-
'--allow-empty',
|
|
98
|
-
'-m',
|
|
99
|
-
'Initial commit',
|
|
100
|
-
]);
|
|
101
|
-
await runGit(['-C', tempDir, 'push', 'origin', `${branch}:${branch}`]);
|
|
102
|
-
}
|
|
103
|
-
finally {
|
|
104
|
-
await (0, promises_1.rm)(tempDir, { recursive: true, force: true });
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
async function repoHasCommits(gitDir) {
|
|
108
|
-
try {
|
|
109
|
-
await runGit([
|
|
110
|
-
'--git-dir',
|
|
111
|
-
gitDir,
|
|
112
|
-
'rev-parse',
|
|
113
|
-
'--verify',
|
|
114
|
-
'HEAD^{commit}',
|
|
115
|
-
]);
|
|
116
|
-
return true;
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
return false;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
async function getDefaultBranch(gitDir) {
|
|
123
|
-
try {
|
|
124
|
-
return await runGit([
|
|
125
|
-
'--git-dir',
|
|
126
|
-
gitDir,
|
|
127
|
-
'symbolic-ref',
|
|
128
|
-
'--quiet',
|
|
129
|
-
'--short',
|
|
130
|
-
'HEAD',
|
|
131
|
-
]);
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
async function localBranchExists(gitDir, branch) {
|
|
138
|
-
try {
|
|
139
|
-
await runGit([
|
|
140
|
-
'--git-dir',
|
|
141
|
-
gitDir,
|
|
142
|
-
'show-ref',
|
|
143
|
-
'--verify',
|
|
144
|
-
'--quiet',
|
|
145
|
-
`refs/heads/${branch}`,
|
|
146
|
-
]);
|
|
147
|
-
return true;
|
|
148
|
-
}
|
|
149
|
-
catch {
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
function parseWorktreeList(output) {
|
|
154
|
-
const normalized = output.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
155
|
-
const trimmed = normalized.trim();
|
|
156
|
-
if (!trimmed) {
|
|
157
|
-
return [];
|
|
158
|
-
}
|
|
159
|
-
return trimmed.split(/\n\s*\n/).map((block) => {
|
|
160
|
-
let worktreePath = '';
|
|
161
|
-
let head = null;
|
|
162
|
-
let branch = null;
|
|
163
|
-
let bare = false;
|
|
164
|
-
let locked = false;
|
|
165
|
-
let prunable = false;
|
|
166
|
-
let detached = false;
|
|
167
|
-
for (const line of block.split('\n')) {
|
|
168
|
-
if (line.startsWith('worktree ')) {
|
|
169
|
-
worktreePath = line.slice('worktree '.length);
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
if (line.startsWith('HEAD ')) {
|
|
173
|
-
head = line.slice('HEAD '.length);
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
if (line.startsWith('branch ')) {
|
|
177
|
-
branch = line.slice('branch '.length).replace(/^refs\/heads\//, '');
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
if (line === 'bare') {
|
|
181
|
-
bare = true;
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
if (line === 'detached') {
|
|
185
|
-
detached = true;
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
if (line.startsWith('locked')) {
|
|
189
|
-
locked = true;
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (line.startsWith('prunable')) {
|
|
193
|
-
prunable = true;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return {
|
|
197
|
-
path: worktreePath,
|
|
198
|
-
alias: node_path_1.default.basename(worktreePath),
|
|
199
|
-
branch,
|
|
200
|
-
head,
|
|
201
|
-
bare,
|
|
202
|
-
locked,
|
|
203
|
-
prunable,
|
|
204
|
-
detached,
|
|
205
|
-
};
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
async function listWorktrees(gitDir) {
|
|
209
|
-
const output = await runGit([
|
|
210
|
-
'--git-dir',
|
|
211
|
-
gitDir,
|
|
212
|
-
'worktree',
|
|
213
|
-
'list',
|
|
214
|
-
'--porcelain',
|
|
215
|
-
]);
|
|
216
|
-
return parseWorktreeList(output);
|
|
217
|
-
}
|
|
218
|
-
async function listLinkedWorktrees(gitDir) {
|
|
219
|
-
const worktrees = await listWorktrees(gitDir);
|
|
220
|
-
return worktrees.filter((entry) => !entry.bare);
|
|
221
|
-
}
|
|
222
|
-
async function addWorktree(gitDir, worktreePath, branch, startPoint) {
|
|
223
|
-
if (!(await repoHasCommits(gitDir))) {
|
|
224
|
-
throw new errors_1.CliError('Repository has no commits yet. Git cannot create a linked worktree for an empty bare repository.');
|
|
225
|
-
}
|
|
226
|
-
const args = ['--git-dir', gitDir, 'worktree', 'add'];
|
|
227
|
-
if (await localBranchExists(gitDir, branch)) {
|
|
228
|
-
args.push(worktreePath, branch);
|
|
229
|
-
}
|
|
230
|
-
else {
|
|
231
|
-
const resolvedStartPoint = startPoint ?? (await getDefaultBranch(gitDir));
|
|
232
|
-
if (!resolvedStartPoint) {
|
|
233
|
-
throw new errors_1.CliError(`Could not determine a start point for branch '${branch}'.`);
|
|
234
|
-
}
|
|
235
|
-
args.push('-b', branch, worktreePath, resolvedStartPoint);
|
|
236
|
-
}
|
|
237
|
-
await runGit(args);
|
|
238
|
-
}
|
|
239
|
-
async function removeWorktree(gitDir, worktreePath, force) {
|
|
240
|
-
const args = ['--git-dir', gitDir, 'worktree', 'remove'];
|
|
241
|
-
if (force) {
|
|
242
|
-
args.push('--force');
|
|
243
|
-
}
|
|
244
|
-
args.push(worktreePath);
|
|
245
|
-
await runGit(args);
|
|
246
|
-
}
|
|
247
|
-
async function repairWorktrees(gitDir, worktreePaths) {
|
|
248
|
-
const args = ['--git-dir', gitDir, 'worktree', 'repair', ...worktreePaths];
|
|
249
|
-
await runGit(args);
|
|
250
|
-
}
|
|
251
|
-
async function getCurrentBranch(worktreePath) {
|
|
252
|
-
try {
|
|
253
|
-
return await runGit([
|
|
254
|
-
'-C',
|
|
255
|
-
worktreePath,
|
|
256
|
-
'symbolic-ref',
|
|
257
|
-
'--quiet',
|
|
258
|
-
'--short',
|
|
259
|
-
'HEAD',
|
|
260
|
-
]);
|
|
261
|
-
}
|
|
262
|
-
catch {
|
|
263
|
-
return null;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
async function getHeadShortSha(worktreePath) {
|
|
267
|
-
return runGit(['-C', worktreePath, 'rev-parse', '--short', 'HEAD']);
|
|
268
|
-
}
|
|
269
|
-
async function getWorktreeStatus(worktree) {
|
|
270
|
-
const statusOutput = await runGit([
|
|
271
|
-
'-C',
|
|
272
|
-
worktree.path,
|
|
273
|
-
'status',
|
|
274
|
-
'--porcelain',
|
|
275
|
-
]);
|
|
276
|
-
const changes = statusOutput
|
|
277
|
-
? statusOutput.split('\n').filter(Boolean).length
|
|
278
|
-
: 0;
|
|
279
|
-
let branch = worktree.branch;
|
|
280
|
-
let detached = worktree.detached;
|
|
281
|
-
if (!branch) {
|
|
282
|
-
branch = await getCurrentBranch(worktree.path);
|
|
283
|
-
detached = branch === null;
|
|
284
|
-
}
|
|
285
|
-
return {
|
|
286
|
-
...worktree,
|
|
287
|
-
branch,
|
|
288
|
-
detached,
|
|
289
|
-
head: worktree.head ?? (await getHeadShortSha(worktree.path)),
|
|
290
|
-
state: changes === 0 ? 'clean' : 'dirty',
|
|
291
|
-
changes,
|
|
292
|
-
};
|
|
293
|
-
}
|