@idea1/cli 1.7.7 → 1.8.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 +126 -60
- package/dist/cli/named-lists.d.ts.map +1 -1
- package/dist/cli/named-lists.js +5 -0
- package/dist/cli/named-lists.js.map +1 -1
- package/dist/commands/approve-pr.d.ts.map +1 -1
- package/dist/commands/approve-pr.js +94 -8
- package/dist/commands/approve-pr.js.map +1 -1
- package/dist/commands/attach-project-item.d.ts.map +1 -1
- package/dist/commands/attach-project-item.js +28 -41
- package/dist/commands/attach-project-item.js.map +1 -1
- package/dist/commands/close-worktree.d.ts.map +1 -1
- package/dist/commands/close-worktree.js +50 -84
- package/dist/commands/close-worktree.js.map +1 -1
- package/dist/commands/create-worktree.d.ts.map +1 -1
- package/dist/commands/create-worktree.js +1 -4
- package/dist/commands/create-worktree.js.map +1 -1
- package/dist/commands/get-project-item-url.d.ts.map +1 -1
- package/dist/commands/get-project-item-url.js +3 -1
- package/dist/commands/get-project-item-url.js.map +1 -1
- package/dist/commands/help.js +13 -13
- package/dist/commands/help.js.map +1 -1
- package/dist/commands/index.d.ts +1 -2
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +1 -3
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/join-project-item.d.ts.map +1 -1
- package/dist/commands/join-project-item.js +18 -60
- package/dist/commands/join-project-item.js.map +1 -1
- package/dist/commands/next-workflow-step.d.ts.map +1 -1
- package/dist/commands/next-workflow-step.js +16 -22
- package/dist/commands/next-workflow-step.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +4 -3
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/ready-for-approval.d.ts.map +1 -1
- package/dist/commands/ready-for-approval.js +94 -8
- package/dist/commands/ready-for-approval.js.map +1 -1
- package/dist/commands/release.d.ts.map +1 -1
- package/dist/commands/release.js +4 -15
- package/dist/commands/release.js.map +1 -1
- package/dist/commands/start-agent-implementation.d.ts.map +1 -1
- package/dist/commands/start-agent-implementation.js +10 -13
- package/dist/commands/start-agent-implementation.js.map +1 -1
- package/dist/commands/start-dev-implementation.d.ts.map +1 -1
- package/dist/commands/start-dev-implementation.js +7 -8
- package/dist/commands/start-dev-implementation.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +21 -15
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/update-branch.d.ts.map +1 -1
- package/dist/commands/update-branch.js +28 -75
- package/dist/commands/update-branch.js.map +1 -1
- package/dist/interactive.d.ts.map +1 -1
- package/dist/interactive.js +47 -34
- package/dist/interactive.js.map +1 -1
- package/dist/lib/claude-options.d.ts +11 -10
- package/dist/lib/claude-options.d.ts.map +1 -1
- package/dist/lib/claude-options.js +38 -39
- package/dist/lib/claude-options.js.map +1 -1
- package/dist/lib/settings.d.ts +23 -27
- package/dist/lib/settings.d.ts.map +1 -1
- package/dist/lib/settings.js +35 -50
- package/dist/lib/settings.js.map +1 -1
- package/dist/prompts/platform-prompts.d.ts +30 -0
- package/dist/prompts/platform-prompts.d.ts.map +1 -0
- package/dist/prompts/platform-prompts.js +38 -0
- package/dist/prompts/platform-prompts.js.map +1 -0
- package/dist/prompts/project-item-prompts.d.ts +44 -0
- package/dist/prompts/project-item-prompts.d.ts.map +1 -0
- package/dist/prompts/project-item-prompts.js +41 -0
- package/dist/prompts/project-item-prompts.js.map +1 -0
- package/dist/services/claude.d.ts +27 -33
- package/dist/services/claude.d.ts.map +1 -1
- package/dist/services/claude.js +40 -140
- package/dist/services/claude.js.map +1 -1
- package/dist/services/git.d.ts +24 -121
- package/dist/services/git.d.ts.map +1 -1
- package/dist/services/git.js +73 -470
- package/dist/services/git.js.map +1 -1
- package/dist/services/github.d.ts +20 -4
- package/dist/services/github.d.ts.map +1 -1
- package/dist/services/github.js +77 -6
- package/dist/services/github.js.map +1 -1
- package/dist/services/platform.d.ts +75 -0
- package/dist/services/platform.d.ts.map +1 -0
- package/dist/services/platform.js +237 -0
- package/dist/services/platform.js.map +1 -0
- package/dist/services/setup.d.ts +1 -1
- package/dist/services/setup.js +1 -1
- package/dist/services/template-renderer.d.ts +10 -0
- package/dist/services/template-renderer.d.ts.map +1 -0
- package/dist/services/template-renderer.js +20 -0
- package/dist/services/template-renderer.js.map +1 -0
- package/dist/services/worktree.d.ts +17 -16
- package/dist/services/worktree.d.ts.map +1 -1
- package/dist/services/worktree.js +41 -65
- package/dist/services/worktree.js.map +1 -1
- package/dist/types/index.d.ts +14 -58
- package/dist/types/index.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workflow/data/project-item.d.ts +21 -0
- package/dist/workflow/data/project-item.d.ts.map +1 -0
- package/dist/workflow/data/project-item.js +2 -0
- package/dist/workflow/data/project-item.js.map +1 -0
- package/dist/workflow/data/workflow-context.d.ts +46 -0
- package/dist/workflow/data/workflow-context.d.ts.map +1 -0
- package/dist/workflow/data/workflow-context.js +14 -0
- package/dist/workflow/data/workflow-context.js.map +1 -0
- package/dist/{lib → workflow/data}/workflow-states.d.ts +1 -1
- package/dist/workflow/data/workflow-states.d.ts.map +1 -0
- package/dist/{lib → workflow/data}/workflow-states.generated.d.ts +1 -1
- package/dist/workflow/data/workflow-states.generated.d.ts.map +1 -0
- package/dist/{lib → workflow/data}/workflow-states.generated.js +1 -1
- package/dist/workflow/data/workflow-states.generated.js.map +1 -0
- package/dist/{lib → workflow/data}/workflow-states.js +1 -1
- package/dist/workflow/data/workflow-states.js.map +1 -0
- package/dist/workflow/engine/approval-flow.d.ts +53 -0
- package/dist/workflow/engine/approval-flow.d.ts.map +1 -0
- package/dist/workflow/engine/approval-flow.js +111 -0
- package/dist/workflow/engine/approval-flow.js.map +1 -0
- package/dist/{services → workflow/engine}/branch-setup.d.ts +2 -1
- package/dist/workflow/engine/branch-setup.d.ts.map +1 -0
- package/dist/{services → workflow/engine}/branch-setup.js +6 -5
- package/dist/workflow/engine/branch-setup.js.map +1 -0
- package/dist/workflow/engine/handlers/agent-implementation.d.ts +10 -0
- package/dist/workflow/engine/handlers/agent-implementation.d.ts.map +1 -0
- package/dist/workflow/engine/handlers/agent-implementation.js +42 -0
- package/dist/workflow/engine/handlers/agent-implementation.js.map +1 -0
- package/dist/{services/workflow-handlers → workflow/engine/handlers}/approve-pr.d.ts +7 -4
- package/dist/workflow/engine/handlers/approve-pr.d.ts.map +1 -0
- package/dist/workflow/engine/handlers/approve-pr.js +63 -0
- package/dist/workflow/engine/handlers/approve-pr.js.map +1 -0
- package/dist/{services/workflow-handlers → workflow/engine/handlers}/dev-implementation.d.ts +3 -2
- package/dist/workflow/engine/handlers/dev-implementation.d.ts.map +1 -0
- package/dist/workflow/engine/handlers/dev-implementation.js +36 -0
- package/dist/workflow/engine/handlers/dev-implementation.js.map +1 -0
- package/dist/workflow/engine/handlers/index.d.ts +21 -0
- package/dist/workflow/engine/handlers/index.d.ts.map +1 -0
- package/dist/{services/workflow-handlers → workflow/engine/handlers}/index.js +10 -6
- package/dist/workflow/engine/handlers/index.js.map +1 -0
- package/dist/workflow/engine/handlers/ready-for-approval.d.ts +11 -0
- package/dist/workflow/engine/handlers/ready-for-approval.d.ts.map +1 -0
- package/dist/workflow/engine/handlers/ready-for-approval.js +56 -0
- package/dist/workflow/engine/handlers/ready-for-approval.js.map +1 -0
- package/dist/{services → workflow/engine}/release-flow.d.ts +19 -10
- package/dist/workflow/engine/release-flow.d.ts.map +1 -0
- package/dist/{services → workflow/engine}/release-flow.js +33 -16
- package/dist/workflow/engine/release-flow.js.map +1 -0
- package/dist/workflow/engine/workflow-engine.d.ts +162 -0
- package/dist/workflow/engine/workflow-engine.d.ts.map +1 -0
- package/dist/workflow/engine/workflow-engine.js +405 -0
- package/dist/workflow/engine/workflow-engine.js.map +1 -0
- package/package.json +2 -1
- package/dist/commands/integrate.d.ts +0 -13
- package/dist/commands/integrate.d.ts.map +0 -1
- package/dist/commands/integrate.js +0 -128
- package/dist/commands/integrate.js.map +0 -1
- package/dist/lib/workflow-states.d.ts.map +0 -1
- package/dist/lib/workflow-states.generated.d.ts.map +0 -1
- package/dist/lib/workflow-states.generated.js.map +0 -1
- package/dist/lib/workflow-states.js.map +0 -1
- package/dist/services/branch-setup.d.ts.map +0 -1
- package/dist/services/branch-setup.js.map +0 -1
- package/dist/services/context.d.ts +0 -26
- package/dist/services/context.d.ts.map +0 -1
- package/dist/services/context.js +0 -304
- package/dist/services/context.js.map +0 -1
- package/dist/services/release-flow.d.ts.map +0 -1
- package/dist/services/release-flow.js.map +0 -1
- package/dist/services/workflow-engine.d.ts +0 -67
- package/dist/services/workflow-engine.d.ts.map +0 -1
- package/dist/services/workflow-engine.js +0 -371
- package/dist/services/workflow-engine.js.map +0 -1
- package/dist/services/workflow-handlers/agent-implementation.d.ts +0 -9
- package/dist/services/workflow-handlers/agent-implementation.d.ts.map +0 -1
- package/dist/services/workflow-handlers/agent-implementation.js +0 -38
- package/dist/services/workflow-handlers/agent-implementation.js.map +0 -1
- package/dist/services/workflow-handlers/approve-pr.d.ts.map +0 -1
- package/dist/services/workflow-handlers/approve-pr.js +0 -122
- package/dist/services/workflow-handlers/approve-pr.js.map +0 -1
- package/dist/services/workflow-handlers/dev-implementation.d.ts.map +0 -1
- package/dist/services/workflow-handlers/dev-implementation.js +0 -34
- package/dist/services/workflow-handlers/dev-implementation.js.map +0 -1
- package/dist/services/workflow-handlers/index.d.ts +0 -18
- package/dist/services/workflow-handlers/index.d.ts.map +0 -1
- package/dist/services/workflow-handlers/index.js.map +0 -1
- package/dist/services/workflow-handlers/ready-for-approval.d.ts +0 -10
- package/dist/services/workflow-handlers/ready-for-approval.d.ts.map +0 -1
- package/dist/services/workflow-handlers/ready-for-approval.js +0 -59
- package/dist/services/workflow-handlers/ready-for-approval.js.map +0 -1
- package/dist/services/workflow.d.ts +0 -14
- package/dist/services/workflow.d.ts.map +0 -1
- package/dist/services/workflow.js +0 -37
- package/dist/services/workflow.js.map +0 -1
package/dist/services/git.js
CHANGED
|
@@ -2,9 +2,8 @@ import { exec, execOrThrow } from '../lib/shell.js';
|
|
|
2
2
|
import { logger } from '../lib/logger.js';
|
|
3
3
|
import { dirname, basename, join, resolve } from 'path';
|
|
4
4
|
import { tmpdir } from 'os';
|
|
5
|
-
import {
|
|
5
|
+
import { mkdtemp, writeFile, rm } from 'fs/promises';
|
|
6
6
|
import { access } from 'fs/promises';
|
|
7
|
-
import { DEFAULT_BRANCH_SETTINGS, SETTINGS_FILES, getBranchPrefix, getIntegrationBranchPrefix } from '../lib/settings.js';
|
|
8
7
|
/**
|
|
9
8
|
* Check if a directory exists
|
|
10
9
|
*/
|
|
@@ -17,100 +16,6 @@ async function directoryExists(path) {
|
|
|
17
16
|
return false;
|
|
18
17
|
}
|
|
19
18
|
}
|
|
20
|
-
/**
|
|
21
|
-
* Sanitize a branch name for use as a filename by replacing `/` with `--`
|
|
22
|
-
*/
|
|
23
|
-
function sanitizeBranchNameForFilename(branchName) {
|
|
24
|
-
return branchName.replace(/\//g, '--');
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Reverse of sanitizeBranchNameForFilename: `--` back to `/`
|
|
28
|
-
*/
|
|
29
|
-
function branchNameFromFilename(filename) {
|
|
30
|
-
return filename.replace(/\.json$/, '').replace(/--/g, '/');
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Path of a branch's metadata JSON file relative to the repository root
|
|
34
|
-
*/
|
|
35
|
-
export function getBranchMetadataRelativePath(branchName) {
|
|
36
|
-
return `.idea1/${sanitizeBranchNameForFilename(branchName)}.json`;
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Get the path to a branch's metadata JSON file inside `.idea1/`
|
|
40
|
-
*/
|
|
41
|
-
function getBranchMetadataPath(worktreePath, branchName) {
|
|
42
|
-
return join(worktreePath, getBranchMetadataRelativePath(branchName));
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* The on-disk form of a branch metadata file. The web client writes the same
|
|
46
|
-
* bytes through the GitHub API, so keep this exact (2-space indent, trailing newline).
|
|
47
|
-
*/
|
|
48
|
-
export function serializeBranchMetadata(metadata) {
|
|
49
|
-
return JSON.stringify(metadata, null, 2) + '\n';
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Read branch metadata from the JSON file in the current working tree.
|
|
53
|
-
* Returns null if the file doesn't exist.
|
|
54
|
-
*/
|
|
55
|
-
export async function readBranchMetadata(branchName) {
|
|
56
|
-
const branch = branchName ?? await getCurrentBranch();
|
|
57
|
-
const repoRoot = await getRepoRoot();
|
|
58
|
-
const metadataPath = getBranchMetadataPath(repoRoot, branch);
|
|
59
|
-
try {
|
|
60
|
-
const content = await readFile(metadataPath, 'utf-8');
|
|
61
|
-
return JSON.parse(content);
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Read a branch's committed metadata directly from a remote ref via `git show`,
|
|
69
|
-
* without checking the branch out. Returns null if the file is missing or unparseable.
|
|
70
|
-
* Lets callers recover durable per-branch state from a fresh clone.
|
|
71
|
-
*/
|
|
72
|
-
export async function readBranchMetadataFromRef(branchName, remote = 'origin') {
|
|
73
|
-
const filename = `${sanitizeBranchNameForFilename(branchName)}.json`;
|
|
74
|
-
const result = await exec('git', ['show', `${remote}/${branchName}:.idea1/${filename}`]);
|
|
75
|
-
if (result.exitCode !== 0)
|
|
76
|
-
return null;
|
|
77
|
-
try {
|
|
78
|
-
return JSON.parse(result.stdout);
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Write branch metadata to the JSON file.
|
|
86
|
-
* Returns false if `.idea1/` doesn't exist (e.g., main repo before worktree creation).
|
|
87
|
-
*/
|
|
88
|
-
export async function writeBranchMetadata(metadata, worktreePath) {
|
|
89
|
-
const root = worktreePath ?? await getRepoRoot();
|
|
90
|
-
const idea1Dir = join(root, '.idea1');
|
|
91
|
-
if (!await directoryExists(idea1Dir))
|
|
92
|
-
return false;
|
|
93
|
-
const metadataPath = getBranchMetadataPath(root, metadata.branchName);
|
|
94
|
-
await writeFile(metadataPath, serializeBranchMetadata(metadata));
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Partially update an existing branch metadata JSON file.
|
|
99
|
-
* Silently skips if the file doesn't exist (e.g., store* called before worktree exists).
|
|
100
|
-
*/
|
|
101
|
-
async function updateBranchMetadataFile(branchName, updates) {
|
|
102
|
-
const repoRoot = await getRepoRoot();
|
|
103
|
-
const metadataPath = getBranchMetadataPath(repoRoot, branchName);
|
|
104
|
-
try {
|
|
105
|
-
const content = await readFile(metadataPath, 'utf-8');
|
|
106
|
-
const metadata = JSON.parse(content);
|
|
107
|
-
Object.assign(metadata, updates);
|
|
108
|
-
await writeFile(metadataPath, serializeBranchMetadata(metadata));
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
// File doesn't exist yet — silently skip
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
19
|
/**
|
|
115
20
|
* Get the repository root path from git
|
|
116
21
|
*/
|
|
@@ -224,7 +129,7 @@ export async function updateBranchToLatest(branchName) {
|
|
|
224
129
|
const worktreePath = await getWorktreePathForBranch(branchName);
|
|
225
130
|
if (worktreePath) {
|
|
226
131
|
logger.step(`Branch ${branchName} is in a worktree at ${worktreePath}, pulling there...`);
|
|
227
|
-
await
|
|
132
|
+
await pullBranch(branchName, worktreePath);
|
|
228
133
|
}
|
|
229
134
|
else {
|
|
230
135
|
await checkoutAndPullBranch(branchName);
|
|
@@ -237,7 +142,7 @@ export async function checkoutAndPullBranch(branchName) {
|
|
|
237
142
|
logger.step(`Checking out ${branchName}...`);
|
|
238
143
|
await execOrThrow('git', ['checkout', branchName]);
|
|
239
144
|
logger.step(`Pulling latest from ${branchName}...`);
|
|
240
|
-
await
|
|
145
|
+
await pullBranch(branchName);
|
|
241
146
|
}
|
|
242
147
|
/**
|
|
243
148
|
* Create a new branch from a base branch and push it to origin
|
|
@@ -261,13 +166,13 @@ export async function prepareRepoForWorktree(fullBranchName, destBranchName, fal
|
|
|
261
166
|
const destWorktreePath = await getWorktreePathForBranch(destBranchName);
|
|
262
167
|
if (destWorktreePath) {
|
|
263
168
|
logger.step(`Branch ${destBranchName} is in a worktree at ${destWorktreePath}, pulling there...`);
|
|
264
|
-
await
|
|
169
|
+
await pullBranch(destBranchName, destWorktreePath);
|
|
265
170
|
}
|
|
266
171
|
else {
|
|
267
172
|
await checkoutAndPullBranch(destBranchName);
|
|
268
173
|
}
|
|
269
174
|
await execOrThrow('git', ['checkout', fullBranchName]);
|
|
270
|
-
await
|
|
175
|
+
await pullBranch(fullBranchName);
|
|
271
176
|
logger.success(`Branch ${fullBranchName} is ready for development`);
|
|
272
177
|
// Must leave main working tree NOT on fullBranchName for worktree add to work
|
|
273
178
|
if (destWorktreePath) {
|
|
@@ -282,7 +187,7 @@ export async function prepareRepoForWorktree(fullBranchName, destBranchName, fal
|
|
|
282
187
|
}
|
|
283
188
|
else {
|
|
284
189
|
await execOrThrow('git', ['checkout', destBranchName]);
|
|
285
|
-
await
|
|
190
|
+
await pullBranch(destBranchName);
|
|
286
191
|
}
|
|
287
192
|
}
|
|
288
193
|
/**
|
|
@@ -359,7 +264,7 @@ export async function checkoutBranchInMainRepo(branchName) {
|
|
|
359
264
|
await execOrThrow('git', ['checkout', '--track', `origin/${branchName}`]);
|
|
360
265
|
}
|
|
361
266
|
}
|
|
362
|
-
await
|
|
267
|
+
await pullBranch(branchName);
|
|
363
268
|
logger.success(`Branch ${branchName} is checked out in ${mainRepoRoot}`);
|
|
364
269
|
return mainRepoRoot;
|
|
365
270
|
}
|
|
@@ -402,228 +307,50 @@ export async function enterWorktreeAndSetup(worktreePath, fullBranchName) {
|
|
|
402
307
|
await exec('git', ['push', '-u', 'origin', fullBranchName], { cwd: worktreePath });
|
|
403
308
|
}
|
|
404
309
|
/**
|
|
405
|
-
*
|
|
310
|
+
* Remember which project item a branch implements in this clone's git config
|
|
311
|
+
* (`branch.<name>.idea1ProjectItemUrl`). A local convenience only: the platform's record of the
|
|
312
|
+
* branch is the truth, and this is what it said last time the branch was resolved here.
|
|
406
313
|
*/
|
|
407
314
|
export async function storeProjectItemUrl(projectItemUrl, branchName) {
|
|
408
|
-
const
|
|
409
|
-
if (
|
|
410
|
-
await execOrThrow('git', ['config', '--local', `branch.${
|
|
411
|
-
await updateBranchMetadataFile(branch, { projectItemUrl });
|
|
412
|
-
logger.dim(`Project item URL stored in git config for ${branch}`);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
/**
|
|
416
|
-
* Commit a `.idea1/<sanitized-branch>.json` metadata file on the branch so all
|
|
417
|
-
* per-branch metadata is discoverable from any clone via `git show`.
|
|
418
|
-
* Reads current values from git config and writes them into the JSON file.
|
|
419
|
-
*/
|
|
420
|
-
export async function commitBranchMetadata(worktreePath, branchName, projectItemUrl) {
|
|
421
|
-
const metadata = { branchName };
|
|
422
|
-
// Gather all metadata from git config
|
|
423
|
-
if (projectItemUrl)
|
|
424
|
-
metadata.projectItemUrl = projectItemUrl;
|
|
425
|
-
const storedUrl = await getProjectItemUrl(branchName);
|
|
426
|
-
if (storedUrl && !metadata.projectItemUrl)
|
|
427
|
-
metadata.projectItemUrl = storedUrl;
|
|
428
|
-
const baseBranch = await getBaseBranch(branchName);
|
|
429
|
-
if (baseBranch)
|
|
430
|
-
metadata.baseBranch = baseBranch;
|
|
431
|
-
const workflowState = await getWorkflowState(branchName);
|
|
432
|
-
if (workflowState)
|
|
433
|
-
metadata.workflowState = workflowState;
|
|
434
|
-
const devWorkflowType = await getDevWorkflowType(branchName);
|
|
435
|
-
if (devWorkflowType)
|
|
436
|
-
metadata.devWorkflowType = devWorkflowType;
|
|
437
|
-
const stackBranch = await getStackBranch(branchName);
|
|
438
|
-
if (stackBranch)
|
|
439
|
-
metadata.stackBranch = stackBranch;
|
|
440
|
-
const integrationBranch = await getIntegrationBranch(branchName);
|
|
441
|
-
if (integrationBranch)
|
|
442
|
-
metadata.integrationBranch = integrationBranch;
|
|
443
|
-
// Write JSON file
|
|
444
|
-
const idea1Dir = join(worktreePath, '.idea1');
|
|
445
|
-
await mkdir(idea1Dir, { recursive: true });
|
|
446
|
-
const filename = `${sanitizeBranchNameForFilename(branchName)}.json`;
|
|
447
|
-
await writeFile(join(idea1Dir, filename), serializeBranchMetadata(metadata));
|
|
448
|
-
await execOrThrow('git', ['add', `.idea1/${filename}`], { cwd: worktreePath });
|
|
449
|
-
await execOrThrow('git', ['commit', '-m', `idea1: link to ${metadata.projectItemUrl ?? branchName}`], { cwd: worktreePath });
|
|
450
|
-
logger.dim('Committed branch metadata to branch');
|
|
451
|
-
}
|
|
452
|
-
/**
|
|
453
|
-
* Remove the branch's `.idea1/<sanitized-branch>.json` file, commit, and push.
|
|
454
|
-
* Must be called before the PR is merged so the metadata file doesn't leak into the target branch.
|
|
455
|
-
*/
|
|
456
|
-
export async function removeBranchMetadataAndPush(branchName) {
|
|
457
|
-
const repoRoot = await getRepoRoot();
|
|
458
|
-
const filename = `${sanitizeBranchNameForFilename(branchName)}.json`;
|
|
459
|
-
const filePath = join(repoRoot, '.idea1', filename);
|
|
460
|
-
try {
|
|
461
|
-
await access(filePath);
|
|
462
|
-
}
|
|
463
|
-
catch {
|
|
464
|
-
// File doesn't exist — nothing to remove
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
await unlink(filePath);
|
|
468
|
-
await execOrThrow('git', ['add', `.idea1/${filename}`]);
|
|
469
|
-
await execOrThrow('git', ['commit', '-m', `idea1: remove branch metadata for ${branchName}`]);
|
|
470
|
-
await execOrThrow('git', ['push']);
|
|
471
|
-
logger.dim('Removed branch metadata file before merge');
|
|
472
|
-
}
|
|
473
|
-
/**
|
|
474
|
-
* Stage and commit the branch metadata file if it has changed.
|
|
475
|
-
* Returns true if a commit was made.
|
|
476
|
-
*/
|
|
477
|
-
export async function commitBranchMetadataIfChanged(branchName) {
|
|
478
|
-
const repoRoot = await getRepoRoot();
|
|
479
|
-
const filename = `${sanitizeBranchNameForFilename(branchName)}.json`;
|
|
480
|
-
const filePath = join(repoRoot, '.idea1', filename);
|
|
481
|
-
try {
|
|
482
|
-
await access(filePath);
|
|
483
|
-
}
|
|
484
|
-
catch {
|
|
485
|
-
return false;
|
|
486
|
-
}
|
|
487
|
-
await execOrThrow('git', ['add', `.idea1/${filename}`]);
|
|
488
|
-
const diffResult = await exec('git', ['diff', '--cached', '--quiet']);
|
|
489
|
-
if (diffResult.exitCode === 0) {
|
|
490
|
-
return false;
|
|
315
|
+
const current = await getProjectItemUrl(branchName);
|
|
316
|
+
if (current !== projectItemUrl) {
|
|
317
|
+
await execOrThrow('git', ['config', '--local', `branch.${branchName}.idea1ProjectItemUrl`, projectItemUrl]);
|
|
491
318
|
}
|
|
492
|
-
const url = await getProjectItemUrl(branchName);
|
|
493
|
-
const commitRef = url ?? branchName;
|
|
494
|
-
await execOrThrow('git', ['commit', '-m', `idea1: update metadata for ${commitRef}`]);
|
|
495
|
-
logger.dim('Committed branch metadata update');
|
|
496
|
-
return true;
|
|
497
319
|
}
|
|
498
320
|
/**
|
|
499
|
-
* The
|
|
500
|
-
* branch prefix and, for integration workflows, the integration prefix. Derived from
|
|
501
|
-
* settings rather than assuming `working/`, so a repo that names its branches
|
|
502
|
-
* differently still resolves an item to its branch.
|
|
321
|
+
* The project item URL this clone remembers for a branch, or null for a branch it has yet to resolve.
|
|
503
322
|
*/
|
|
504
|
-
async function
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
const repoRoot = await getRepoRoot();
|
|
508
|
-
const [branchPrefix, integrationPrefix] = await Promise.all([
|
|
509
|
-
getBranchPrefix(repoRoot),
|
|
510
|
-
getIntegrationBranchPrefix(repoRoot)
|
|
511
|
-
]);
|
|
512
|
-
if (branchPrefix)
|
|
513
|
-
prefixes.add(branchPrefix);
|
|
514
|
-
if (integrationPrefix)
|
|
515
|
-
prefixes.add(integrationPrefix);
|
|
516
|
-
}
|
|
517
|
-
catch {
|
|
518
|
-
// Not in a repo; the built-in prefixes are the best available guess.
|
|
519
|
-
}
|
|
520
|
-
return [...prefixes].map(prefix => `origin/${prefix}/*`);
|
|
323
|
+
export async function getProjectItemUrl(branchName) {
|
|
324
|
+
const result = await exec('git', ['config', '--local', '--get', `branch.${branchName}.idea1ProjectItemUrl`]);
|
|
325
|
+
return result.exitCode === 0 && result.stdout.trim() ? result.stdout.trim() : null;
|
|
521
326
|
}
|
|
522
327
|
/**
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
* Falls back to checking the legacy `.idea1/project-item-url` for pre-migration branches.
|
|
328
|
+
* The branch this clone remembers as implementing a project item, or null for an item it has yet
|
|
329
|
+
* to resolve.
|
|
526
330
|
*/
|
|
527
|
-
export async function
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
if (result.exitCode !== 0 || !result.stdout.trim())
|
|
331
|
+
export async function findBranchWithProjectItemUrl(projectItemUrl) {
|
|
332
|
+
const result = await exec('git', ['config', '--local', '--list']);
|
|
333
|
+
if (result.exitCode !== 0) {
|
|
531
334
|
return null;
|
|
532
|
-
const branches = result.stdout
|
|
533
|
-
.split('\n')
|
|
534
|
-
.map(line => line.trim())
|
|
535
|
-
.filter(name => name.length > 0 && !name.includes('HEAD'));
|
|
536
|
-
for (const remoteBranch of branches) {
|
|
537
|
-
const localName = remoteBranch.replace('origin/', '');
|
|
538
|
-
const jsonFilename = `${sanitizeBranchNameForFilename(localName)}.json`;
|
|
539
|
-
// Check new per-branch JSON file
|
|
540
|
-
const jsonResult = await exec('git', ['show', `${remoteBranch}:.idea1/${jsonFilename}`]);
|
|
541
|
-
if (jsonResult.exitCode === 0) {
|
|
542
|
-
try {
|
|
543
|
-
const metadata = JSON.parse(jsonResult.stdout);
|
|
544
|
-
if (metadata.projectItemUrl === projectItemUrl) {
|
|
545
|
-
return localName;
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
catch {
|
|
549
|
-
// Malformed JSON — skip
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
// Backward-compat: check legacy .idea1/project-item-url
|
|
553
|
-
const legacyResult = await exec('git', ['show', `${remoteBranch}:.idea1/project-item-url`]);
|
|
554
|
-
if (legacyResult.exitCode === 0 && legacyResult.stdout.trim() === projectItemUrl) {
|
|
555
|
-
return localName;
|
|
556
|
-
}
|
|
557
335
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
* Fetches latest, then checks local git config (fast path) before falling back
|
|
563
|
-
* to searching remote branches for the committed `.idea1/<branch>.json` metadata.
|
|
564
|
-
* Returns null if no branch is found.
|
|
565
|
-
*/
|
|
566
|
-
export async function resolveBranchFromProjectItemUrl(projectItemUrl) {
|
|
567
|
-
// Fetch latest so remote refs are up-to-date
|
|
568
|
-
await fetchLatest();
|
|
569
|
-
// Fast path: search local git config
|
|
570
|
-
const result = await exec('git', ['config', '--local', '--list']);
|
|
571
|
-
if (result.exitCode === 0) {
|
|
572
|
-
const lines = result.stdout.split('\n');
|
|
573
|
-
for (const line of lines) {
|
|
574
|
-
const match = line.match(/^branch\.(.+)\.idea1projectitemurl=(.+)$/i);
|
|
575
|
-
if (match && match[2] === projectItemUrl) {
|
|
576
|
-
return match[1];
|
|
577
|
-
}
|
|
336
|
+
for (const line of result.stdout.split('\n')) {
|
|
337
|
+
const match = line.match(/^branch\.(.+)\.idea1projectitemurl=(.+)$/i);
|
|
338
|
+
if (match && match[2] === projectItemUrl) {
|
|
339
|
+
return match[1];
|
|
578
340
|
}
|
|
579
341
|
}
|
|
580
|
-
|
|
581
|
-
return findBranchByProjectItemFile(projectItemUrl);
|
|
582
|
-
}
|
|
583
|
-
/**
|
|
584
|
-
* Read a per-branch value from git config, falling back to the committed
|
|
585
|
-
* `.idea1/<branch>.json` metadata file (the durable, remote-persisted copy)
|
|
586
|
-
* when no config value is set.
|
|
587
|
-
*
|
|
588
|
-
* @param branchName Branch to read for (defaults to the current branch).
|
|
589
|
-
* @param configKey The `branch.<branch>.<configKey>` git config key.
|
|
590
|
-
* @param fromMetadata Pulls the corresponding field out of the committed metadata.
|
|
591
|
-
*/
|
|
592
|
-
async function readBranchValue(branchName, configKey, fromMetadata) {
|
|
593
|
-
const branch = branchName ?? await getCurrentBranch();
|
|
594
|
-
const result = await exec('git', ['config', '--local', '--get', `branch.${branch}.${configKey}`]);
|
|
595
|
-
if (result.exitCode === 0 && result.stdout.trim()) {
|
|
596
|
-
return result.stdout.trim();
|
|
597
|
-
}
|
|
598
|
-
const metadata = await readBranchMetadata(branch);
|
|
599
|
-
return (metadata ? fromMetadata(metadata) : undefined) ?? null;
|
|
600
|
-
}
|
|
601
|
-
/**
|
|
602
|
-
* Retrieve project item URL from git config, falling back to the committed
|
|
603
|
-
* `.idea1/<branch>.json` metadata file (the durable, remote-persisted copy).
|
|
604
|
-
*/
|
|
605
|
-
export async function getProjectItemUrl(branchName) {
|
|
606
|
-
return readBranchValue(branchName, 'idea1ProjectItemUrl', m => m.projectItemUrl);
|
|
342
|
+
return null;
|
|
607
343
|
}
|
|
608
344
|
/**
|
|
609
|
-
*
|
|
345
|
+
* The GitHub repository `origin` points at, as `owner/name`.
|
|
610
346
|
*/
|
|
611
|
-
export async function
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
logger.dim(`Base branch stored in git config for ${branch}: ${baseBranch}`);
|
|
347
|
+
export async function getRepositoryFullName() {
|
|
348
|
+
const url = (await execOrThrow('git', ['remote', 'get-url', 'origin'])).trim();
|
|
349
|
+
const match = url.match(/[:/]([^/:]+\/[^/:]+?)(?:\.git)?\/?$/);
|
|
350
|
+
if (!match) {
|
|
351
|
+
throw new Error(`Could not read the repository name from the origin URL: ${url}`);
|
|
617
352
|
}
|
|
618
|
-
|
|
619
|
-
/**
|
|
620
|
-
* Retrieve base branch for a branch (with hierarchical resolution)
|
|
621
|
-
*/
|
|
622
|
-
export async function getBaseBranch(branchName) {
|
|
623
|
-
// The authoritative source (an existing PR) is queried in the command layer.
|
|
624
|
-
// Here we resolve branch-specific config, then the committed metadata file,
|
|
625
|
-
// then null (caller decides the default).
|
|
626
|
-
return readBranchValue(branchName, 'baseBranch', m => m.baseBranch);
|
|
353
|
+
return match[1];
|
|
627
354
|
}
|
|
628
355
|
/**
|
|
629
356
|
* Validate that a branch exists
|
|
@@ -657,13 +384,14 @@ export async function commitAllChanges(message) {
|
|
|
657
384
|
/**
|
|
658
385
|
* Push to remote branch
|
|
659
386
|
*/
|
|
660
|
-
export async function pushToRemote(branchName) {
|
|
387
|
+
export async function pushToRemote(branchName, cwd) {
|
|
661
388
|
logger.step(`Pushing changes to remote branch ${branchName}...`);
|
|
662
|
-
await execOrThrow('git', ['push', '--set-upstream', 'origin', branchName]);
|
|
389
|
+
await execOrThrow('git', ['push', '--set-upstream', 'origin', branchName], { cwd });
|
|
663
390
|
logger.success('Changes pushed successfully');
|
|
664
391
|
}
|
|
665
392
|
/**
|
|
666
|
-
*
|
|
393
|
+
* Forget the remembered item URLs (and the per-branch config older CLI versions kept) of branches
|
|
394
|
+
* that no longer exist.
|
|
667
395
|
*/
|
|
668
396
|
export async function gcOrphanedBranchConfigs() {
|
|
669
397
|
const result = await exec('git', ['config', '--local', '--list']);
|
|
@@ -688,103 +416,6 @@ export async function gcOrphanedBranchConfigs() {
|
|
|
688
416
|
if (removedCount > 0) {
|
|
689
417
|
logger.dim(`Removed ${removedCount} orphaned branch config entries`);
|
|
690
418
|
}
|
|
691
|
-
// Also clean up orphaned .idea1/*.json files
|
|
692
|
-
await gcOrphanedBranchMetadataFiles();
|
|
693
|
-
}
|
|
694
|
-
/**
|
|
695
|
-
* Remove `.idea1/*.json` files for branches that no longer exist (excluding the settings files)
|
|
696
|
-
*/
|
|
697
|
-
async function gcOrphanedBranchMetadataFiles() {
|
|
698
|
-
try {
|
|
699
|
-
const repoRoot = await getRepoRoot();
|
|
700
|
-
const idea1Dir = join(repoRoot, '.idea1');
|
|
701
|
-
const files = await readdir(idea1Dir);
|
|
702
|
-
let removedCount = 0;
|
|
703
|
-
for (const file of files) {
|
|
704
|
-
if (!file.endsWith('.json') || SETTINGS_FILES.includes(file))
|
|
705
|
-
continue;
|
|
706
|
-
const branchName = branchNameFromFilename(file);
|
|
707
|
-
const exists = await branchExists(branchName);
|
|
708
|
-
if (!exists) {
|
|
709
|
-
await unlink(join(idea1Dir, file));
|
|
710
|
-
removedCount++;
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
if (removedCount > 0) {
|
|
714
|
-
logger.dim(`Removed ${removedCount} orphaned branch metadata files`);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
catch {
|
|
718
|
-
// .idea1/ directory doesn't exist or isn't readable — skip
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
/**
|
|
722
|
-
* Store workflow state in git config for later retrieval
|
|
723
|
-
*/
|
|
724
|
-
export async function storeWorkflowState(state, branchName) {
|
|
725
|
-
const branch = branchName ?? await getCurrentBranch();
|
|
726
|
-
if (state && branch) {
|
|
727
|
-
await execOrThrow('git', ['config', '--local', `branch.${branch}.idea1WorkflowState`, state]);
|
|
728
|
-
await updateBranchMetadataFile(branch, { workflowState: state });
|
|
729
|
-
logger.dim(`Workflow state stored in git config for ${branch}`);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
/**
|
|
733
|
-
* Retrieve workflow state from git config
|
|
734
|
-
*/
|
|
735
|
-
export async function getWorkflowState(branchName) {
|
|
736
|
-
return readBranchValue(branchName, 'idea1WorkflowState', m => m.workflowState);
|
|
737
|
-
}
|
|
738
|
-
/**
|
|
739
|
-
* Store the development workflow type in git config
|
|
740
|
-
*/
|
|
741
|
-
export async function storeDevWorkflowType(workflowType, branchName) {
|
|
742
|
-
const branch = branchName ?? await getCurrentBranch();
|
|
743
|
-
if (workflowType && branch) {
|
|
744
|
-
await execOrThrow('git', ['config', '--local', `branch.${branch}.idea1DevWorkflowType`, workflowType]);
|
|
745
|
-
await updateBranchMetadataFile(branch, { devWorkflowType: workflowType });
|
|
746
|
-
logger.dim(`Dev workflow type stored in git config for ${branch}: ${workflowType}`);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
/**
|
|
750
|
-
* Retrieve the development workflow type from git config
|
|
751
|
-
*/
|
|
752
|
-
export async function getDevWorkflowType(branchName) {
|
|
753
|
-
return readBranchValue(branchName, 'idea1DevWorkflowType', m => m.devWorkflowType);
|
|
754
|
-
}
|
|
755
|
-
/**
|
|
756
|
-
* Store the stack branch in git config (for stacked workflows)
|
|
757
|
-
*/
|
|
758
|
-
export async function storeStackBranch(stackBranch, branchName) {
|
|
759
|
-
const branch = branchName ?? await getCurrentBranch();
|
|
760
|
-
if (stackBranch && branch) {
|
|
761
|
-
await execOrThrow('git', ['config', '--local', `branch.${branch}.idea1StackBranch`, stackBranch]);
|
|
762
|
-
await updateBranchMetadataFile(branch, { stackBranch });
|
|
763
|
-
logger.dim(`Stack branch stored in git config for ${branch}: ${stackBranch}`);
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
/**
|
|
767
|
-
* Retrieve the stack branch from git config
|
|
768
|
-
*/
|
|
769
|
-
export async function getStackBranch(branchName) {
|
|
770
|
-
return readBranchValue(branchName, 'idea1StackBranch', m => m.stackBranch);
|
|
771
|
-
}
|
|
772
|
-
/**
|
|
773
|
-
* Store the integration branch name in git config
|
|
774
|
-
*/
|
|
775
|
-
export async function storeIntegrationBranch(integrationBranch, branchName) {
|
|
776
|
-
const branch = branchName ?? await getCurrentBranch();
|
|
777
|
-
if (integrationBranch && branch) {
|
|
778
|
-
await execOrThrow('git', ['config', '--local', `branch.${branch}.idea1IntegrationBranch`, integrationBranch]);
|
|
779
|
-
await updateBranchMetadataFile(branch, { integrationBranch });
|
|
780
|
-
logger.dim(`Integration branch stored in git config for ${branch}: ${integrationBranch}`);
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* Retrieve the integration branch name from git config
|
|
785
|
-
*/
|
|
786
|
-
export async function getIntegrationBranch(branchName) {
|
|
787
|
-
return readBranchValue(branchName, 'idea1IntegrationBranch', m => m.integrationBranch);
|
|
788
419
|
}
|
|
789
420
|
/**
|
|
790
421
|
* Fetch and pull a specific branch from origin, ensuring it's up to date
|
|
@@ -798,16 +429,7 @@ export async function fetchAndPullBranch(branchName) {
|
|
|
798
429
|
// Update local branch to match remote without checking it out
|
|
799
430
|
const currentBranch = await getCurrentBranch();
|
|
800
431
|
if (currentBranch === branchName) {
|
|
801
|
-
|
|
802
|
-
const ffResult = await exec('git', ['pull', '--ff-only', 'origin', branchName]);
|
|
803
|
-
if (ffResult.exitCode !== 0) {
|
|
804
|
-
const rebaseResult = await exec('git', ['pull', '--rebase', 'origin', branchName]);
|
|
805
|
-
if (rebaseResult.exitCode !== 0) {
|
|
806
|
-
await exec('git', ['rebase', '--abort']);
|
|
807
|
-
logger.warning('Rebase had conflicts, falling back to merge commit');
|
|
808
|
-
await execOrThrow('git', ['pull', '--no-rebase', 'origin', branchName]);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
432
|
+
await pullBranch(branchName);
|
|
811
433
|
}
|
|
812
434
|
else {
|
|
813
435
|
// Check if branch is in a secondary worktree — git fetch origin branch:branch
|
|
@@ -815,7 +437,7 @@ export async function fetchAndPullBranch(branchName) {
|
|
|
815
437
|
const worktreePath = await getWorktreePathForBranch(branchName);
|
|
816
438
|
if (worktreePath) {
|
|
817
439
|
logger.step(`Branch ${branchName} is in a worktree at ${worktreePath}, pulling there...`);
|
|
818
|
-
await
|
|
440
|
+
await pullBranch(branchName, worktreePath);
|
|
819
441
|
}
|
|
820
442
|
else {
|
|
821
443
|
await execOrThrow('git', ['fetch', 'origin', `${branchName}:${branchName}`]);
|
|
@@ -824,12 +446,39 @@ export async function fetchAndPullBranch(branchName) {
|
|
|
824
446
|
}
|
|
825
447
|
}
|
|
826
448
|
/**
|
|
827
|
-
*
|
|
449
|
+
* Run a git command that merges into `targetBranch`, checked out at `cwd` (the current directory
|
|
450
|
+
* when omitted). A merge that stops on conflicts is left in progress for the developer to resolve
|
|
451
|
+
* and reported as an error saying so; nothing is aborted, retried, or rebased.
|
|
452
|
+
*/
|
|
453
|
+
async function runMerge(args, sourceRef, targetBranch, cwd) {
|
|
454
|
+
const result = await exec('git', args, { cwd });
|
|
455
|
+
if (result.exitCode === 0) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const mergeInProgress = await exec('git', ['rev-parse', '-q', '--verify', 'MERGE_HEAD'], { cwd });
|
|
459
|
+
if (mergeInProgress.exitCode === 0) {
|
|
460
|
+
const location = cwd ?? (await getRepoRoot()).trim();
|
|
461
|
+
throw new Error(`Merging ${sourceRef} into ${targetBranch} has conflicts to resolve before the merge can complete. ` +
|
|
462
|
+
`Resolve them in ${location} (on ${targetBranch}), commit the merge, and push ${targetBranch}.`);
|
|
463
|
+
}
|
|
464
|
+
throw new Error(result.stderr || `Command failed: git ${args.join(' ')}`);
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Pull `origin/<branch>` into the local branch checked out at `cwd` (the current directory when
|
|
468
|
+
* omitted). Pulls never rebase, whatever the clone's `pull.rebase` or `branch.<name>.rebase`
|
|
469
|
+
* config says: a branch that is behind fast-forwards, and one that has diverged gets a merge commit.
|
|
470
|
+
*/
|
|
471
|
+
export async function pullBranch(branchName, cwd) {
|
|
472
|
+
await runMerge(['pull', '--no-rebase', '--no-edit', 'origin', branchName], `origin/${branchName}`, branchName, cwd);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Merge a source branch into `targetBranch`, checked out at `cwd` (the current directory when
|
|
476
|
+
* omitted), always with a merge commit.
|
|
828
477
|
*/
|
|
829
|
-
export async function mergeBranch(sourceBranch) {
|
|
830
|
-
logger.step(`Merging ${sourceBranch} into
|
|
831
|
-
await
|
|
832
|
-
logger.success(`
|
|
478
|
+
export async function mergeBranch(sourceBranch, targetBranch, cwd) {
|
|
479
|
+
logger.step(`Merging ${sourceBranch} into ${targetBranch}...`);
|
|
480
|
+
await runMerge(['merge', '--no-ff', '--no-edit', sourceBranch], sourceBranch, targetBranch, cwd);
|
|
481
|
+
logger.success(`Merged ${sourceBranch} into ${targetBranch}`);
|
|
833
482
|
}
|
|
834
483
|
/**
|
|
835
484
|
* Check if merging source into target would produce conflicts,
|
|
@@ -849,12 +498,6 @@ export async function checkMergeConflicts(source, target) {
|
|
|
849
498
|
}
|
|
850
499
|
return { hasConflicts: false };
|
|
851
500
|
}
|
|
852
|
-
/**
|
|
853
|
-
* Abort an in-progress merge
|
|
854
|
-
*/
|
|
855
|
-
export async function abortMerge() {
|
|
856
|
-
await exec('git', ['merge', '--abort']);
|
|
857
|
-
}
|
|
858
501
|
/**
|
|
859
502
|
* List remote branches matching a prefix pattern
|
|
860
503
|
*/
|
|
@@ -967,8 +610,7 @@ async function getGitCommonDir() {
|
|
|
967
610
|
/**
|
|
968
611
|
* Commit file changes on top of a ref without checking anything out, using a
|
|
969
612
|
* temporary index. Returns the new commit sha (not yet on any branch). Used for
|
|
970
|
-
* branches that have no local checkout
|
|
971
|
-
* metadata before anyone works on it.
|
|
613
|
+
* branches that have no local checkout.
|
|
972
614
|
*/
|
|
973
615
|
export async function commitFilesDetached(parentRef, changes, message) {
|
|
974
616
|
const parentSha = (await execOrThrow('git', ['rev-parse', '--verify', `${parentRef}^{commit}`])).trim();
|
|
@@ -1008,32 +650,6 @@ export async function pushCommitToBranch(branchName, commitSha) {
|
|
|
1008
650
|
await execOrThrow('git', ['update-ref', `refs/heads/${branchName}`, commitSha]);
|
|
1009
651
|
}
|
|
1010
652
|
}
|
|
1011
|
-
/**
|
|
1012
|
-
* Create a branch on origin from a source ref (e.g. `origin/working/release`) with
|
|
1013
|
-
* its `.idea1/<branch>.json` metadata as the first commit. No checkout is needed.
|
|
1014
|
-
*/
|
|
1015
|
-
export async function createBranchWithMetadataDetached(branchName, sourceRef, metadata) {
|
|
1016
|
-
logger.step(`Creating branch ${branchName} from ${sourceRef}...`);
|
|
1017
|
-
const commitSha = await commitFilesDetached(sourceRef, [{ path: getBranchMetadataRelativePath(branchName), content: serializeBranchMetadata(metadata) }], `idea1: link to ${metadata.projectItemUrl ?? branchName}`);
|
|
1018
|
-
await pushCommitToBranch(branchName, commitSha);
|
|
1019
|
-
logger.success(`Branch ${branchName} created on origin with its workflow metadata`);
|
|
1020
|
-
}
|
|
1021
|
-
/**
|
|
1022
|
-
* Update fields of the committed `.idea1/<branch>.json` on origin for a branch
|
|
1023
|
-
* that has no local checkout. Returns true when a commit was pushed.
|
|
1024
|
-
*/
|
|
1025
|
-
export async function updateBranchMetadataDetached(branchName, updates) {
|
|
1026
|
-
const remote = await readBranchMetadataFromRef(branchName);
|
|
1027
|
-
const merged = { ...(remote ?? { branchName }), ...updates, branchName };
|
|
1028
|
-
const unchanged = remote !== null && Object.keys(updates).every(key => remote[key] === merged[key]);
|
|
1029
|
-
if (unchanged) {
|
|
1030
|
-
return false;
|
|
1031
|
-
}
|
|
1032
|
-
const commitSha = await commitFilesDetached(`origin/${branchName}`, [{ path: getBranchMetadataRelativePath(branchName), content: serializeBranchMetadata(merged) }], `idea1: update metadata for ${merged.projectItemUrl ?? branchName}`);
|
|
1033
|
-
await pushCommitToBranch(branchName, commitSha);
|
|
1034
|
-
logger.dim('Committed branch metadata update on origin');
|
|
1035
|
-
return true;
|
|
1036
|
-
}
|
|
1037
653
|
/**
|
|
1038
654
|
* Create a branch on origin pointing at a source ref, without any local checkout.
|
|
1039
655
|
*/
|
|
@@ -1073,17 +689,4 @@ export async function fetchRemoteBranch(branchName) {
|
|
|
1073
689
|
const result = await exec('git', ['fetch', 'origin', branchName]);
|
|
1074
690
|
return result.exitCode === 0;
|
|
1075
691
|
}
|
|
1076
|
-
/**
|
|
1077
|
-
* Remove the committed `.idea1/<branch>.json` from a branch that has no local
|
|
1078
|
-
* checkout, so the file does not leak into the target branch when the PR merges.
|
|
1079
|
-
*/
|
|
1080
|
-
export async function removeBranchMetadataDetached(branchName) {
|
|
1081
|
-
const remote = await readBranchMetadataFromRef(branchName);
|
|
1082
|
-
if (!remote) {
|
|
1083
|
-
return;
|
|
1084
|
-
}
|
|
1085
|
-
const commitSha = await commitFilesDetached(`origin/${branchName}`, [{ path: getBranchMetadataRelativePath(branchName), content: null }], `idea1: remove branch metadata for ${branchName}`);
|
|
1086
|
-
await pushCommitToBranch(branchName, commitSha);
|
|
1087
|
-
logger.dim('Removed branch metadata file before merge');
|
|
1088
|
-
}
|
|
1089
692
|
//# sourceMappingURL=git.js.map
|