@gitgov/core 1.8.0 → 1.8.3
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 +1 -1
- package/dist/src/index.d.ts +56 -8
- package/dist/src/index.js +757 -112
- package/dist/src/index.js.map +1 -1
- package/package.json +1 -1
package/dist/src/index.js
CHANGED
|
@@ -3000,12 +3000,61 @@ var ConfigManager = class _ConfigManager {
|
|
|
3000
3000
|
}
|
|
3001
3001
|
/**
|
|
3002
3002
|
* Load GitGovernance session state
|
|
3003
|
+
* [EARS-53] Auto-detects actor from .key files if no session or no actorId exists
|
|
3003
3004
|
*/
|
|
3004
3005
|
async loadSession() {
|
|
3005
3006
|
try {
|
|
3006
3007
|
const sessionContent = await promises.readFile(this.sessionPath, "utf-8");
|
|
3007
|
-
|
|
3008
|
+
const session = JSON.parse(sessionContent);
|
|
3009
|
+
if (!session.lastSession?.actorId) {
|
|
3010
|
+
const detectedActorId = await this.detectActorFromKeyFiles();
|
|
3011
|
+
if (detectedActorId) {
|
|
3012
|
+
session.lastSession = {
|
|
3013
|
+
actorId: detectedActorId,
|
|
3014
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3015
|
+
};
|
|
3016
|
+
await promises.writeFile(this.sessionPath, JSON.stringify(session, null, 2), "utf-8");
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
return session;
|
|
3008
3020
|
} catch (error) {
|
|
3021
|
+
const detectedActorId = await this.detectActorFromKeyFiles();
|
|
3022
|
+
if (detectedActorId) {
|
|
3023
|
+
const newSession = {
|
|
3024
|
+
lastSession: {
|
|
3025
|
+
actorId: detectedActorId,
|
|
3026
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3027
|
+
},
|
|
3028
|
+
actorState: {}
|
|
3029
|
+
};
|
|
3030
|
+
try {
|
|
3031
|
+
await promises.writeFile(this.sessionPath, JSON.stringify(newSession, null, 2), "utf-8");
|
|
3032
|
+
return newSession;
|
|
3033
|
+
} catch {
|
|
3034
|
+
return newSession;
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
return null;
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
/**
|
|
3041
|
+
* [EARS-53] Detect actor from .key files in .gitgov/actors/
|
|
3042
|
+
* Returns the actor ID if exactly one .key file exists, or the first one if multiple exist.
|
|
3043
|
+
* Private keys (.key files) indicate which actors can sign on this machine.
|
|
3044
|
+
*/
|
|
3045
|
+
async detectActorFromKeyFiles() {
|
|
3046
|
+
try {
|
|
3047
|
+
const gitgovDir = path6.dirname(this.sessionPath);
|
|
3048
|
+
const actorsDir = path6.join(gitgovDir, "actors");
|
|
3049
|
+
const files = await promises.readdir(actorsDir);
|
|
3050
|
+
const keyFiles = files.filter((f) => f.endsWith(".key"));
|
|
3051
|
+
const firstKeyFile = keyFiles[0];
|
|
3052
|
+
if (!firstKeyFile) {
|
|
3053
|
+
return null;
|
|
3054
|
+
}
|
|
3055
|
+
const actorId = firstKeyFile.replace(".key", "");
|
|
3056
|
+
return actorId;
|
|
3057
|
+
} catch {
|
|
3009
3058
|
return null;
|
|
3010
3059
|
}
|
|
3011
3060
|
}
|
|
@@ -3598,7 +3647,7 @@ var IdentityAdapter = class {
|
|
|
3598
3647
|
}
|
|
3599
3648
|
return actors;
|
|
3600
3649
|
}
|
|
3601
|
-
async signRecord(record, actorId, role) {
|
|
3650
|
+
async signRecord(record, actorId, role, notes) {
|
|
3602
3651
|
const actor = await this.getActor(actorId);
|
|
3603
3652
|
if (!actor) {
|
|
3604
3653
|
throw new Error(`Actor not found: ${actorId}`);
|
|
@@ -3617,12 +3666,12 @@ var IdentityAdapter = class {
|
|
|
3617
3666
|
}
|
|
3618
3667
|
let signature;
|
|
3619
3668
|
if (privateKey) {
|
|
3620
|
-
signature = signPayload(record.payload, privateKey, actorId, role,
|
|
3669
|
+
signature = signPayload(record.payload, privateKey, actorId, role, notes);
|
|
3621
3670
|
} else {
|
|
3622
3671
|
signature = {
|
|
3623
3672
|
keyId: actorId,
|
|
3624
3673
|
role,
|
|
3625
|
-
notes
|
|
3674
|
+
notes,
|
|
3626
3675
|
signature: `mock-signature-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
3627
3676
|
timestamp: Math.floor(Date.now() / 1e3)
|
|
3628
3677
|
};
|
|
@@ -4069,7 +4118,7 @@ var FeedbackAdapter = class {
|
|
|
4069
4118
|
},
|
|
4070
4119
|
payload: validatedPayload
|
|
4071
4120
|
};
|
|
4072
|
-
const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author");
|
|
4121
|
+
const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author", "Feedback record created");
|
|
4073
4122
|
await this.feedbackStore.write(signedRecord);
|
|
4074
4123
|
this.eventBus.publish({
|
|
4075
4124
|
type: "feedback.created",
|
|
@@ -4336,7 +4385,7 @@ var ExecutionAdapter = class {
|
|
|
4336
4385
|
},
|
|
4337
4386
|
payload: validatedPayload
|
|
4338
4387
|
};
|
|
4339
|
-
const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author");
|
|
4388
|
+
const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author", "Execution record created");
|
|
4340
4389
|
await this.executionStore.write(signedRecord);
|
|
4341
4390
|
this.eventBus.publish({
|
|
4342
4391
|
type: "execution.created",
|
|
@@ -4568,7 +4617,7 @@ var ChangelogAdapter = class {
|
|
|
4568
4617
|
},
|
|
4569
4618
|
payload: validatedPayload
|
|
4570
4619
|
};
|
|
4571
|
-
const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author");
|
|
4620
|
+
const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author", "Changelog record created");
|
|
4572
4621
|
await this.changelogStore.write(signedRecord);
|
|
4573
4622
|
this.eventBus.publish({
|
|
4574
4623
|
type: "changelog.created",
|
|
@@ -5264,7 +5313,7 @@ var BacklogAdapter = class {
|
|
|
5264
5313
|
},
|
|
5265
5314
|
payload: validatedPayload
|
|
5266
5315
|
};
|
|
5267
|
-
const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author");
|
|
5316
|
+
const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author", "Task created");
|
|
5268
5317
|
await this.taskStore.write(signedRecord);
|
|
5269
5318
|
this.eventBus.publish({
|
|
5270
5319
|
type: "task.created",
|
|
@@ -5326,7 +5375,7 @@ var BacklogAdapter = class {
|
|
|
5326
5375
|
}
|
|
5327
5376
|
const updatedPayload = { ...task, status: "review" };
|
|
5328
5377
|
const updatedRecord = { ...taskRecord, payload: updatedPayload };
|
|
5329
|
-
const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "submitter");
|
|
5378
|
+
const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "submitter", "Task submitted for review");
|
|
5330
5379
|
await this.taskStore.write(signedRecord);
|
|
5331
5380
|
this.eventBus.publish({
|
|
5332
5381
|
type: "task.status.changed",
|
|
@@ -5378,7 +5427,7 @@ var BacklogAdapter = class {
|
|
|
5378
5427
|
}
|
|
5379
5428
|
const updatedPayload = { ...task, status: targetState };
|
|
5380
5429
|
const updatedRecord = { ...taskRecord, payload: updatedPayload };
|
|
5381
|
-
const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "approver");
|
|
5430
|
+
const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "approver", `Task approved: ${task.status} \u2192 ${targetState}`);
|
|
5382
5431
|
await this.taskStore.write(signedRecord);
|
|
5383
5432
|
this.eventBus.publish({
|
|
5384
5433
|
type: "task.status.changed",
|
|
@@ -5418,7 +5467,7 @@ var BacklogAdapter = class {
|
|
|
5418
5467
|
}
|
|
5419
5468
|
const updatedPayload = { ...task, status: "active" };
|
|
5420
5469
|
const updatedRecord = { ...taskRecord, payload: updatedPayload };
|
|
5421
|
-
const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "executor");
|
|
5470
|
+
const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "executor", "Task activated");
|
|
5422
5471
|
await this.taskStore.write(signedRecord);
|
|
5423
5472
|
await this.configManager.updateActorState(actorId, {
|
|
5424
5473
|
activeTaskId: taskId
|
|
@@ -5469,7 +5518,7 @@ var BacklogAdapter = class {
|
|
|
5469
5518
|
}
|
|
5470
5519
|
};
|
|
5471
5520
|
const updatedRecord = { ...taskRecord, payload: updatedPayload };
|
|
5472
|
-
const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "pauser");
|
|
5521
|
+
const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "pauser", `Task paused: ${reason || "No reason provided"}`);
|
|
5473
5522
|
await this.taskStore.write(signedRecord);
|
|
5474
5523
|
await this.configManager.updateActorState(actorId, {
|
|
5475
5524
|
activeTaskId: void 0
|
|
@@ -5519,7 +5568,7 @@ var BacklogAdapter = class {
|
|
|
5519
5568
|
}
|
|
5520
5569
|
const updatedPayload = { ...task, status: "active" };
|
|
5521
5570
|
const updatedRecord = { ...taskRecord, payload: updatedPayload };
|
|
5522
|
-
const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "resumer");
|
|
5571
|
+
const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "resumer", "Task resumed");
|
|
5523
5572
|
await this.taskStore.write(signedRecord);
|
|
5524
5573
|
await this.configManager.updateActorState(actorId, {
|
|
5525
5574
|
activeTaskId: taskId
|
|
@@ -5562,7 +5611,7 @@ var BacklogAdapter = class {
|
|
|
5562
5611
|
}
|
|
5563
5612
|
const updatedPayload = { ...task, status: "done" };
|
|
5564
5613
|
const updatedRecord = { ...taskRecord, payload: updatedPayload };
|
|
5565
|
-
const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "approver");
|
|
5614
|
+
const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "approver", "Task completed");
|
|
5566
5615
|
await this.taskStore.write(signedRecord);
|
|
5567
5616
|
await this.configManager.updateActorState(actorId, {
|
|
5568
5617
|
activeTaskId: void 0
|
|
@@ -5617,7 +5666,7 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
|
|
|
5617
5666
|
}
|
|
5618
5667
|
};
|
|
5619
5668
|
const updatedRecord = { ...taskRecord, payload: updatedPayload };
|
|
5620
|
-
const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "canceller");
|
|
5669
|
+
const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "canceller", `Task discarded: ${reason || "No reason provided"}`);
|
|
5621
5670
|
await this.taskStore.write(signedRecord);
|
|
5622
5671
|
await this.configManager.updateActorState(actorId, {
|
|
5623
5672
|
activeTaskId: void 0
|
|
@@ -5671,8 +5720,9 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
|
|
|
5671
5720
|
}
|
|
5672
5721
|
/**
|
|
5673
5722
|
* Updates a task with new payload
|
|
5723
|
+
* [EARS-28] Signs the updated record with the editor's signature
|
|
5674
5724
|
*/
|
|
5675
|
-
async updateTask(taskId, payload) {
|
|
5725
|
+
async updateTask(taskId, payload, actorId) {
|
|
5676
5726
|
const taskRecord = await this.taskStore.read(taskId);
|
|
5677
5727
|
if (!taskRecord) {
|
|
5678
5728
|
throw new Error(`RecordNotFoundError: Task not found: ${taskId}`);
|
|
@@ -5682,7 +5732,8 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
|
|
|
5682
5732
|
}
|
|
5683
5733
|
const updatedPayload = createTaskRecord({ ...taskRecord.payload, ...payload });
|
|
5684
5734
|
const updatedRecord = { ...taskRecord, payload: updatedPayload };
|
|
5685
|
-
await this.
|
|
5735
|
+
const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "editor", "Task updated");
|
|
5736
|
+
await this.taskStore.write(signedRecord);
|
|
5686
5737
|
return updatedPayload;
|
|
5687
5738
|
}
|
|
5688
5739
|
// ===== PHASE 2: AGENT NAVIGATION (IMPLEMENTED) =====
|
|
@@ -5993,7 +6044,7 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
|
|
|
5993
6044
|
},
|
|
5994
6045
|
payload: validatedPayload
|
|
5995
6046
|
};
|
|
5996
|
-
const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author");
|
|
6047
|
+
const signedRecord = await this.identity.signRecord(unsignedRecord, actorId, "author", "Cycle created");
|
|
5997
6048
|
await this.cycleStore.write(signedRecord);
|
|
5998
6049
|
this.eventBus.publish({
|
|
5999
6050
|
type: "cycle.created",
|
|
@@ -6096,12 +6147,14 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
|
|
|
6096
6147
|
const signedCycleRecord = await this.identity.signRecord(
|
|
6097
6148
|
{ ...cycleRecord, payload: updatedCycle },
|
|
6098
6149
|
currentActor.id,
|
|
6099
|
-
"author"
|
|
6150
|
+
"author",
|
|
6151
|
+
`Task ${taskId} added to cycle`
|
|
6100
6152
|
);
|
|
6101
6153
|
const signedTaskRecord = await this.identity.signRecord(
|
|
6102
6154
|
{ ...taskRecord, payload: updatedTask },
|
|
6103
6155
|
currentActor.id,
|
|
6104
|
-
"author"
|
|
6156
|
+
"author",
|
|
6157
|
+
`Task linked to cycle ${cycleId}`
|
|
6105
6158
|
);
|
|
6106
6159
|
await Promise.all([
|
|
6107
6160
|
this.cycleStore.write(signedCycleRecord),
|
|
@@ -6145,7 +6198,8 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
|
|
|
6145
6198
|
const signedCycleRecord = await this.identity.signRecord(
|
|
6146
6199
|
{ ...cycleRecord, payload: updatedCycle },
|
|
6147
6200
|
currentActor.id,
|
|
6148
|
-
"author"
|
|
6201
|
+
"author",
|
|
6202
|
+
`Tasks removed from cycle: ${taskIds.join(", ")}`
|
|
6149
6203
|
);
|
|
6150
6204
|
const signedTaskRecords = await Promise.all(
|
|
6151
6205
|
taskRecords.map(async ({ record }) => {
|
|
@@ -6157,7 +6211,8 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
|
|
|
6157
6211
|
return await this.identity.signRecord(
|
|
6158
6212
|
{ ...record, payload: updatedTask },
|
|
6159
6213
|
currentActor.id,
|
|
6160
|
-
"author"
|
|
6214
|
+
"author",
|
|
6215
|
+
"Task removed from deleted cycle"
|
|
6161
6216
|
);
|
|
6162
6217
|
})
|
|
6163
6218
|
);
|
|
@@ -6223,12 +6278,14 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
|
|
|
6223
6278
|
this.identity.signRecord(
|
|
6224
6279
|
{ ...sourceCycleRecord, payload: updatedSourceCycle },
|
|
6225
6280
|
currentActor.id,
|
|
6226
|
-
"author"
|
|
6281
|
+
"author",
|
|
6282
|
+
"Tasks moved from cycle"
|
|
6227
6283
|
),
|
|
6228
6284
|
this.identity.signRecord(
|
|
6229
6285
|
{ ...targetCycleRecord, payload: updatedTargetCycle },
|
|
6230
6286
|
currentActor.id,
|
|
6231
|
-
"author"
|
|
6287
|
+
"author",
|
|
6288
|
+
"Tasks moved to cycle"
|
|
6232
6289
|
)
|
|
6233
6290
|
]);
|
|
6234
6291
|
const signedTaskRecords = await Promise.all(
|
|
@@ -6242,7 +6299,8 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
|
|
|
6242
6299
|
return await this.identity.signRecord(
|
|
6243
6300
|
{ ...record, payload: updatedTask },
|
|
6244
6301
|
currentActor.id,
|
|
6245
|
-
"author"
|
|
6302
|
+
"author",
|
|
6303
|
+
"Task cycle updated"
|
|
6246
6304
|
);
|
|
6247
6305
|
})
|
|
6248
6306
|
);
|
|
@@ -7254,7 +7312,7 @@ var ProjectAdapter = class {
|
|
|
7254
7312
|
const projectRoot2 = process.env["GITGOV_ORIGINAL_DIR"] || process.cwd();
|
|
7255
7313
|
const gitgovPath = path6.join(projectRoot2, ".gitgov");
|
|
7256
7314
|
await this.createDirectoryStructure(gitgovPath);
|
|
7257
|
-
await this.copyAgentPrompt(
|
|
7315
|
+
await this.copyAgentPrompt(projectRoot2);
|
|
7258
7316
|
const actor = await this.identityAdapter.createActor(
|
|
7259
7317
|
{
|
|
7260
7318
|
type: "human",
|
|
@@ -7333,7 +7391,8 @@ var ProjectAdapter = class {
|
|
|
7333
7391
|
}
|
|
7334
7392
|
console.warn(`\u26A0\uFE0F ${warnings.join(", ")}.`);
|
|
7335
7393
|
console.warn(` State sync will be available after 'git remote add origin <url>' and first commit.`);
|
|
7336
|
-
console.warn(` Run 'gitgov sync push' when ready to enable multi-machine collaboration
|
|
7394
|
+
console.warn(` Run 'gitgov sync push' when ready to enable multi-machine collaboration.
|
|
7395
|
+
`);
|
|
7337
7396
|
}
|
|
7338
7397
|
await this.initializeSession(actor.id, gitgovPath);
|
|
7339
7398
|
await this.setupGitIntegration(projectRoot2);
|
|
@@ -7552,8 +7611,8 @@ var ProjectAdapter = class {
|
|
|
7552
7611
|
await promises.mkdir(path6.join(gitgovPath, dir), { recursive: true });
|
|
7553
7612
|
}
|
|
7554
7613
|
}
|
|
7555
|
-
async copyAgentPrompt(
|
|
7556
|
-
const targetPrompt = path6.join(
|
|
7614
|
+
async copyAgentPrompt(projectRoot2) {
|
|
7615
|
+
const targetPrompt = path6.join(projectRoot2, "gitgov");
|
|
7557
7616
|
const potentialSources = [];
|
|
7558
7617
|
potentialSources.push(
|
|
7559
7618
|
path6.join(process.cwd(), "prompts/gitgov_agent_prompt.md")
|
|
@@ -7583,7 +7642,7 @@ var ProjectAdapter = class {
|
|
|
7583
7642
|
try {
|
|
7584
7643
|
await promises.access(source);
|
|
7585
7644
|
await promises.copyFile(source, targetPrompt);
|
|
7586
|
-
logger3.debug(`\u{1F4CB} @gitgov agent prompt copied to
|
|
7645
|
+
logger3.debug(`\u{1F4CB} @gitgov agent prompt copied to project root (./gitgov)
|
|
7587
7646
|
`);
|
|
7588
7647
|
return;
|
|
7589
7648
|
} catch {
|
|
@@ -7625,8 +7684,8 @@ var ProjectAdapter = class {
|
|
|
7625
7684
|
# Ignore entire .gitgov/ directory (state lives in gitgov-state branch)
|
|
7626
7685
|
.gitgov/
|
|
7627
7686
|
|
|
7628
|
-
#
|
|
7629
|
-
|
|
7687
|
+
# Ignore agent prompt file (project-specific, created by gitgov init)
|
|
7688
|
+
gitgov
|
|
7630
7689
|
`;
|
|
7631
7690
|
try {
|
|
7632
7691
|
let existingContent = "";
|
|
@@ -11355,7 +11414,31 @@ function shouldSyncFile(filePath) {
|
|
|
11355
11414
|
if (LOCAL_ONLY_FILES.includes(fileName)) {
|
|
11356
11415
|
return false;
|
|
11357
11416
|
}
|
|
11358
|
-
|
|
11417
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
11418
|
+
const parts = normalizedPath.split("/");
|
|
11419
|
+
const gitgovIndex = parts.findIndex((p) => p === ".gitgov");
|
|
11420
|
+
let relativeParts;
|
|
11421
|
+
if (gitgovIndex !== -1) {
|
|
11422
|
+
relativeParts = parts.slice(gitgovIndex + 1);
|
|
11423
|
+
} else {
|
|
11424
|
+
const syncDirIndex = parts.findIndex(
|
|
11425
|
+
(p) => SYNC_DIRECTORIES.includes(p)
|
|
11426
|
+
);
|
|
11427
|
+
if (syncDirIndex !== -1) {
|
|
11428
|
+
relativeParts = parts.slice(syncDirIndex);
|
|
11429
|
+
} else if (SYNC_ROOT_FILES.includes(fileName)) {
|
|
11430
|
+
return true;
|
|
11431
|
+
} else {
|
|
11432
|
+
return false;
|
|
11433
|
+
}
|
|
11434
|
+
}
|
|
11435
|
+
if (relativeParts.length === 1) {
|
|
11436
|
+
return SYNC_ROOT_FILES.includes(relativeParts[0]);
|
|
11437
|
+
} else if (relativeParts.length >= 2) {
|
|
11438
|
+
const dirName = relativeParts[0];
|
|
11439
|
+
return SYNC_DIRECTORIES.includes(dirName);
|
|
11440
|
+
}
|
|
11441
|
+
return false;
|
|
11359
11442
|
}
|
|
11360
11443
|
async function getAllFiles(dir, baseDir = dir) {
|
|
11361
11444
|
const files = [];
|
|
@@ -11374,7 +11457,7 @@ async function getAllFiles(dir, baseDir = dir) {
|
|
|
11374
11457
|
}
|
|
11375
11458
|
return files;
|
|
11376
11459
|
}
|
|
11377
|
-
async function copySyncableFiles(sourceDir, destDir, log) {
|
|
11460
|
+
async function copySyncableFiles(sourceDir, destDir, log, excludeFiles = /* @__PURE__ */ new Set()) {
|
|
11378
11461
|
let copiedCount = 0;
|
|
11379
11462
|
for (const dirName of SYNC_DIRECTORIES) {
|
|
11380
11463
|
const sourcePath = path6__default.join(sourceDir, dirName);
|
|
@@ -11386,6 +11469,11 @@ async function copySyncableFiles(sourceDir, destDir, log) {
|
|
|
11386
11469
|
for (const relativePath of allFiles) {
|
|
11387
11470
|
const fullSourcePath = path6__default.join(sourcePath, relativePath);
|
|
11388
11471
|
const fullDestPath = path6__default.join(destPath, relativePath);
|
|
11472
|
+
const gitgovRelativePath = `.gitgov/${dirName}/${relativePath}`;
|
|
11473
|
+
if (excludeFiles.has(gitgovRelativePath)) {
|
|
11474
|
+
log(`[EARS-60] Skipped (remote-only change preserved): ${gitgovRelativePath}`);
|
|
11475
|
+
continue;
|
|
11476
|
+
}
|
|
11389
11477
|
if (shouldSyncFile(fullSourcePath)) {
|
|
11390
11478
|
await promises.mkdir(path6__default.dirname(fullDestPath), { recursive: true });
|
|
11391
11479
|
await promises.copyFile(fullSourcePath, fullDestPath);
|
|
@@ -11405,6 +11493,11 @@ async function copySyncableFiles(sourceDir, destDir, log) {
|
|
|
11405
11493
|
for (const fileName of SYNC_ROOT_FILES) {
|
|
11406
11494
|
const sourcePath = path6__default.join(sourceDir, fileName);
|
|
11407
11495
|
const destPath = path6__default.join(destDir, fileName);
|
|
11496
|
+
const gitgovRelativePath = `.gitgov/${fileName}`;
|
|
11497
|
+
if (excludeFiles.has(gitgovRelativePath)) {
|
|
11498
|
+
log(`[EARS-60] Skipped (remote-only change preserved): ${gitgovRelativePath}`);
|
|
11499
|
+
continue;
|
|
11500
|
+
}
|
|
11408
11501
|
try {
|
|
11409
11502
|
await promises.copyFile(sourcePath, destPath);
|
|
11410
11503
|
log(`Copied root file: ${fileName}`);
|
|
@@ -11628,7 +11721,28 @@ var SyncModule = class {
|
|
|
11628
11721
|
await this.git.checkoutOrphanBranch(stateBranch);
|
|
11629
11722
|
try {
|
|
11630
11723
|
await execAsync("git rm -rf . 2>/dev/null || true", { cwd: repoRoot });
|
|
11631
|
-
|
|
11724
|
+
const gitignoreContent = `# GitGovernance State Branch .gitignore
|
|
11725
|
+
# This file is auto-generated during gitgov init
|
|
11726
|
+
# These files are machine-specific and should NOT be synced
|
|
11727
|
+
|
|
11728
|
+
# Local-only files (regenerated/machine-specific)
|
|
11729
|
+
index.json
|
|
11730
|
+
.session.json
|
|
11731
|
+
gitgov
|
|
11732
|
+
|
|
11733
|
+
# Private keys (never synced for security)
|
|
11734
|
+
*.key
|
|
11735
|
+
|
|
11736
|
+
# Backup and temporary files
|
|
11737
|
+
*.backup
|
|
11738
|
+
*.backup-*
|
|
11739
|
+
*.tmp
|
|
11740
|
+
*.bak
|
|
11741
|
+
`;
|
|
11742
|
+
const gitignorePath = path6__default.join(repoRoot, ".gitignore");
|
|
11743
|
+
await promises.writeFile(gitignorePath, gitignoreContent, "utf-8");
|
|
11744
|
+
await execAsync("git add .gitignore", { cwd: repoRoot });
|
|
11745
|
+
await execAsync('git commit -m "Initialize state branch with .gitignore"', { cwd: repoRoot });
|
|
11632
11746
|
} catch (commitError) {
|
|
11633
11747
|
const error = commitError;
|
|
11634
11748
|
throw new Error(`Failed to create initial commit on orphan branch: ${error.stderr || error.message}`);
|
|
@@ -11683,6 +11797,17 @@ var SyncModule = class {
|
|
|
11683
11797
|
);
|
|
11684
11798
|
}
|
|
11685
11799
|
}
|
|
11800
|
+
/**
|
|
11801
|
+
* [EARS-60] Detect file-level conflicts and identify remote-only changes.
|
|
11802
|
+
*
|
|
11803
|
+
* A conflict exists when:
|
|
11804
|
+
* 1. A file was modified by the remote during implicit pull
|
|
11805
|
+
* 2. AND the LOCAL USER also modified that same file (content in tempDir differs from what was in git before pull)
|
|
11806
|
+
*
|
|
11807
|
+
* This catches conflicts that git rebase can't detect because we copy files AFTER the pull.
|
|
11808
|
+
*
|
|
11809
|
+
* @param tempDir - Directory containing local .gitgov/ files (preserved before checkout)
|
|
11810
|
+
* @param repoRoot - Repository root path
|
|
11686
11811
|
/**
|
|
11687
11812
|
* Checks if a rebase is in progress.
|
|
11688
11813
|
*
|
|
@@ -11817,8 +11942,14 @@ var SyncModule = class {
|
|
|
11817
11942
|
const commit = commits[i];
|
|
11818
11943
|
if (!commit) continue;
|
|
11819
11944
|
const message = commit.message.toLowerCase();
|
|
11820
|
-
|
|
11821
|
-
|
|
11945
|
+
if (message.startsWith("resolution:")) {
|
|
11946
|
+
continue;
|
|
11947
|
+
}
|
|
11948
|
+
if (message.startsWith("sync:")) {
|
|
11949
|
+
continue;
|
|
11950
|
+
}
|
|
11951
|
+
const isExplicitRebaseCommit = message.includes("rebase") || message.includes("pick ");
|
|
11952
|
+
if (isExplicitRebaseCommit) {
|
|
11822
11953
|
const nextCommit = commits[i + 1];
|
|
11823
11954
|
const isResolutionNext = nextCommit && nextCommit.message.toLowerCase().startsWith("resolution:");
|
|
11824
11955
|
if (!isResolutionNext) {
|
|
@@ -11963,14 +12094,34 @@ Then push your changes: git push -u origin ${sourceBranch}`
|
|
|
11963
12094
|
log(`Audit result: ${auditReport.passed ? "PASSED" : "FAILED"}`);
|
|
11964
12095
|
if (!auditReport.passed) {
|
|
11965
12096
|
log(`Audit violations: ${auditReport.summary}`);
|
|
12097
|
+
const affectedFiles = [];
|
|
12098
|
+
const detailedErrors = [];
|
|
12099
|
+
if (auditReport.lintReport?.results) {
|
|
12100
|
+
for (const r of auditReport.lintReport.results) {
|
|
12101
|
+
if (r.level === "error") {
|
|
12102
|
+
if (!affectedFiles.includes(r.filePath)) {
|
|
12103
|
+
affectedFiles.push(r.filePath);
|
|
12104
|
+
}
|
|
12105
|
+
detailedErrors.push(` \u2022 ${r.filePath}: [${r.validator}] ${r.message}`);
|
|
12106
|
+
}
|
|
12107
|
+
}
|
|
12108
|
+
}
|
|
12109
|
+
for (const v of auditReport.integrityViolations) {
|
|
12110
|
+
detailedErrors.push(` \u2022 Commit ${v.rebaseCommitHash.slice(0, 8)}: ${v.commitMessage} (by ${v.author})`);
|
|
12111
|
+
}
|
|
12112
|
+
const detailSection = detailedErrors.length > 0 ? `
|
|
12113
|
+
|
|
12114
|
+
Details:
|
|
12115
|
+
${detailedErrors.join("\n")}` : "";
|
|
11966
12116
|
result.conflictDetected = true;
|
|
11967
12117
|
result.conflictInfo = {
|
|
11968
12118
|
type: "integrity_violation",
|
|
11969
|
-
affectedFiles
|
|
11970
|
-
message: auditReport.summary,
|
|
12119
|
+
affectedFiles,
|
|
12120
|
+
message: auditReport.summary + detailSection,
|
|
11971
12121
|
resolutionSteps: [
|
|
11972
|
-
"
|
|
11973
|
-
"
|
|
12122
|
+
"Run 'gitgov lint --fix' to auto-fix signature/checksum issues",
|
|
12123
|
+
"If issues persist, manually review the affected files",
|
|
12124
|
+
"Then retry: gitgov sync push"
|
|
11974
12125
|
]
|
|
11975
12126
|
};
|
|
11976
12127
|
result.error = "Integrity violations detected. Cannot push.";
|
|
@@ -12009,11 +12160,53 @@ Then push your changes: git push -u origin ${sourceBranch}`
|
|
|
12009
12160
|
await this.git.checkoutBranch(savedBranch);
|
|
12010
12161
|
if (tempDir) {
|
|
12011
12162
|
try {
|
|
12012
|
-
log("[EARS-47] Restoring .gitgov/ from temp directory (early return)...");
|
|
12013
12163
|
const repoRoot2 = await this.git.getRepoRoot();
|
|
12014
12164
|
const gitgovDir = path6__default.join(repoRoot2, ".gitgov");
|
|
12015
|
-
|
|
12016
|
-
|
|
12165
|
+
if (returnResult.implicitPull?.hasChanges) {
|
|
12166
|
+
log("[EARS-58] Implicit pull detected in early return - preserving new files from gitgov-state...");
|
|
12167
|
+
try {
|
|
12168
|
+
await this.git.checkoutFilesFromBranch(stateBranch, [".gitgov/"]);
|
|
12169
|
+
await execAsync("git reset HEAD .gitgov/ 2>/dev/null || true", { cwd: repoRoot2 });
|
|
12170
|
+
log("[EARS-58] Synced files copied from gitgov-state (unstaged)");
|
|
12171
|
+
} catch (checkoutError) {
|
|
12172
|
+
log(`[EARS-58] Warning: Failed to checkout from gitgov-state: ${checkoutError}`);
|
|
12173
|
+
}
|
|
12174
|
+
for (const fileName of LOCAL_ONLY_FILES) {
|
|
12175
|
+
const tempFilePath = path6__default.join(tempDir, fileName);
|
|
12176
|
+
const destFilePath = path6__default.join(gitgovDir, fileName);
|
|
12177
|
+
try {
|
|
12178
|
+
await promises.access(tempFilePath);
|
|
12179
|
+
await promises.cp(tempFilePath, destFilePath, { force: true });
|
|
12180
|
+
log(`[EARS-58] Restored LOCAL_ONLY_FILE: ${fileName}`);
|
|
12181
|
+
} catch {
|
|
12182
|
+
}
|
|
12183
|
+
}
|
|
12184
|
+
const restoreExcludedFilesEarly = async (dir, destDir) => {
|
|
12185
|
+
try {
|
|
12186
|
+
const entries = await promises.readdir(dir, { withFileTypes: true });
|
|
12187
|
+
for (const entry of entries) {
|
|
12188
|
+
const srcPath = path6__default.join(dir, entry.name);
|
|
12189
|
+
const dstPath = path6__default.join(destDir, entry.name);
|
|
12190
|
+
if (entry.isDirectory()) {
|
|
12191
|
+
await restoreExcludedFilesEarly(srcPath, dstPath);
|
|
12192
|
+
} else {
|
|
12193
|
+
const isExcluded = SYNC_EXCLUDED_PATTERNS.some((pattern) => pattern.test(entry.name));
|
|
12194
|
+
if (isExcluded) {
|
|
12195
|
+
await promises.mkdir(path6__default.dirname(dstPath), { recursive: true });
|
|
12196
|
+
await promises.copyFile(srcPath, dstPath);
|
|
12197
|
+
log(`[EARS-59] Restored excluded file (early return): ${entry.name}`);
|
|
12198
|
+
}
|
|
12199
|
+
}
|
|
12200
|
+
}
|
|
12201
|
+
} catch {
|
|
12202
|
+
}
|
|
12203
|
+
};
|
|
12204
|
+
await restoreExcludedFilesEarly(tempDir, gitgovDir);
|
|
12205
|
+
} else {
|
|
12206
|
+
log("[EARS-47] Restoring .gitgov/ from temp directory (early return)...");
|
|
12207
|
+
await promises.cp(tempDir, gitgovDir, { recursive: true, force: true });
|
|
12208
|
+
log("[EARS-47] .gitgov/ restored from temp (early return)");
|
|
12209
|
+
}
|
|
12017
12210
|
await promises.rm(tempDir, { recursive: true, force: true });
|
|
12018
12211
|
log("[EARS-47] Temp directory cleaned up (early return)");
|
|
12019
12212
|
} catch (tempRestoreError) {
|
|
@@ -12030,42 +12223,32 @@ Then push your changes: git push -u origin ${sourceBranch}`
|
|
|
12030
12223
|
returnResult.error = returnResult.error ? `${returnResult.error}. Failed to restore stashed changes.` : "Failed to restore stashed changes. Run 'git stash pop' manually.";
|
|
12031
12224
|
}
|
|
12032
12225
|
}
|
|
12226
|
+
if (returnResult.implicitPull?.hasChanges) {
|
|
12227
|
+
log("[EARS-58] Regenerating index after implicit pull (early return)...");
|
|
12228
|
+
try {
|
|
12229
|
+
await this.indexer.generateIndex();
|
|
12230
|
+
returnResult.implicitPull.reindexed = true;
|
|
12231
|
+
log("[EARS-58] Index regenerated successfully");
|
|
12232
|
+
} catch (indexError) {
|
|
12233
|
+
log(`[EARS-58] Warning: Failed to regenerate index: ${indexError}`);
|
|
12234
|
+
returnResult.implicitPull.reindexed = false;
|
|
12235
|
+
}
|
|
12236
|
+
}
|
|
12033
12237
|
return returnResult;
|
|
12034
12238
|
};
|
|
12035
12239
|
log(`Checking out to ${stateBranch}...`);
|
|
12036
12240
|
await this.git.checkoutBranch(stateBranch);
|
|
12037
12241
|
log(`Now on branch: ${await this.git.getCurrentBranch()}`);
|
|
12038
|
-
|
|
12242
|
+
let filesBeforeChanges = /* @__PURE__ */ new Set();
|
|
12039
12243
|
try {
|
|
12040
|
-
await this.git.
|
|
12041
|
-
|
|
12042
|
-
|
|
12043
|
-
|
|
12044
|
-
|
|
12045
|
-
|
|
12046
|
-
|
|
12047
|
-
|
|
12048
|
-
log("Pull failed but continuing (already up-to-date or no remote)");
|
|
12049
|
-
} else {
|
|
12050
|
-
const conflictedFiles = await this.git.getConflictedFiles();
|
|
12051
|
-
if (conflictedFiles.length > 0) {
|
|
12052
|
-
await this.git.rebaseAbort();
|
|
12053
|
-
result.conflictDetected = true;
|
|
12054
|
-
result.conflictInfo = {
|
|
12055
|
-
type: "rebase_conflict",
|
|
12056
|
-
affectedFiles: conflictedFiles,
|
|
12057
|
-
message: "Conflict detected during automatic reconciliation",
|
|
12058
|
-
resolutionSteps: [
|
|
12059
|
-
"Pull remote changes first: gitgov sync pull",
|
|
12060
|
-
"Resolve any conflicts",
|
|
12061
|
-
"Then push your changes"
|
|
12062
|
-
]
|
|
12063
|
-
};
|
|
12064
|
-
result.error = "Conflict detected during reconciliation";
|
|
12065
|
-
return await restoreStashAndReturn(result);
|
|
12066
|
-
}
|
|
12067
|
-
throw error;
|
|
12068
|
-
}
|
|
12244
|
+
const repoRoot2 = await this.git.getRepoRoot();
|
|
12245
|
+
const { stdout: filesOutput } = await execAsync(
|
|
12246
|
+
`git ls-files ".gitgov" 2>/dev/null || true`,
|
|
12247
|
+
{ cwd: repoRoot2 }
|
|
12248
|
+
);
|
|
12249
|
+
filesBeforeChanges = new Set(filesOutput.trim().split("\n").filter((f) => f && shouldSyncFile(f)));
|
|
12250
|
+
log(`[EARS-57] Files in gitgov-state before changes: ${filesBeforeChanges.size}`);
|
|
12251
|
+
} catch {
|
|
12069
12252
|
}
|
|
12070
12253
|
log("=== Phase 2: Publication ===");
|
|
12071
12254
|
log("Checking if .gitgov/ exists in gitgov-state...");
|
|
@@ -12213,6 +12396,43 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
|
|
|
12213
12396
|
} catch {
|
|
12214
12397
|
}
|
|
12215
12398
|
log("[EARS-47] Non-syncable files cleanup complete");
|
|
12399
|
+
log("[EARS-57] Checking for deleted files to sync...");
|
|
12400
|
+
try {
|
|
12401
|
+
const sourceFiles = /* @__PURE__ */ new Set();
|
|
12402
|
+
const findSourceFiles = async (dir, prefix = ".gitgov") => {
|
|
12403
|
+
try {
|
|
12404
|
+
const entries = await promises.readdir(dir, { withFileTypes: true });
|
|
12405
|
+
for (const entry of entries) {
|
|
12406
|
+
const fullPath = path6__default.join(dir, entry.name);
|
|
12407
|
+
const relativePath = `${prefix}/${entry.name}`;
|
|
12408
|
+
if (entry.isDirectory()) {
|
|
12409
|
+
await findSourceFiles(fullPath, relativePath);
|
|
12410
|
+
} else if (shouldSyncFile(relativePath)) {
|
|
12411
|
+
sourceFiles.add(relativePath);
|
|
12412
|
+
}
|
|
12413
|
+
}
|
|
12414
|
+
} catch {
|
|
12415
|
+
}
|
|
12416
|
+
};
|
|
12417
|
+
const sourceDir = tempDir || path6__default.join(repoRoot, ".gitgov");
|
|
12418
|
+
await findSourceFiles(sourceDir);
|
|
12419
|
+
log(`[EARS-57] Found ${sourceFiles.size} syncable files in source (user's local state)`);
|
|
12420
|
+
log(`[EARS-57] Files that existed before changes: ${filesBeforeChanges.size}`);
|
|
12421
|
+
let deletedCount = 0;
|
|
12422
|
+
for (const fileBeforeChange of filesBeforeChanges) {
|
|
12423
|
+
if (!sourceFiles.has(fileBeforeChange)) {
|
|
12424
|
+
try {
|
|
12425
|
+
await execAsync(`git rm -f "${fileBeforeChange}"`, { cwd: repoRoot });
|
|
12426
|
+
log(`[EARS-57] Deleted (user removed): ${fileBeforeChange}`);
|
|
12427
|
+
deletedCount++;
|
|
12428
|
+
} catch {
|
|
12429
|
+
}
|
|
12430
|
+
}
|
|
12431
|
+
}
|
|
12432
|
+
log(`[EARS-57] Deleted ${deletedCount} files that user removed locally`);
|
|
12433
|
+
} catch (err) {
|
|
12434
|
+
log(`[EARS-57] Warning: Failed to sync deleted files: ${err}`);
|
|
12435
|
+
}
|
|
12216
12436
|
const hasStaged = await this.git.hasUncommittedChanges();
|
|
12217
12437
|
log(`Has staged changes: ${hasStaged}`);
|
|
12218
12438
|
if (!hasStaged) {
|
|
@@ -12221,10 +12441,10 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
|
|
|
12221
12441
|
result.filesSynced = 0;
|
|
12222
12442
|
return await restoreStashAndReturn(result);
|
|
12223
12443
|
}
|
|
12224
|
-
log("Creating commit...");
|
|
12444
|
+
log("Creating local commit...");
|
|
12225
12445
|
try {
|
|
12226
12446
|
const commitHash = await this.git.commit(commitMessage);
|
|
12227
|
-
log(`
|
|
12447
|
+
log(`Local commit created: ${commitHash}`);
|
|
12228
12448
|
result.commitHash = commitHash;
|
|
12229
12449
|
} catch (commitError) {
|
|
12230
12450
|
const errorMsg = commitError instanceof Error ? commitError.message : String(commitError);
|
|
@@ -12242,6 +12462,116 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
|
|
|
12242
12462
|
log(`ERROR: Git stderr: ${stderr}`);
|
|
12243
12463
|
throw new Error(`Failed to create commit: ${errorMsg} | stderr: ${stderr}`);
|
|
12244
12464
|
}
|
|
12465
|
+
log("=== Phase 3: Reconcile with Remote (Git-Native) ===");
|
|
12466
|
+
let hashBeforePull = null;
|
|
12467
|
+
try {
|
|
12468
|
+
const { stdout: beforeHash } = await execAsync(`git rev-parse HEAD`, { cwd: repoRoot });
|
|
12469
|
+
hashBeforePull = beforeHash.trim();
|
|
12470
|
+
log(`Hash before pull: ${hashBeforePull}`);
|
|
12471
|
+
} catch {
|
|
12472
|
+
}
|
|
12473
|
+
log("Attempting git pull --rebase origin gitgov-state...");
|
|
12474
|
+
try {
|
|
12475
|
+
await this.git.pullRebase("origin", stateBranch);
|
|
12476
|
+
log("Pull rebase successful - no conflicts");
|
|
12477
|
+
if (hashBeforePull) {
|
|
12478
|
+
try {
|
|
12479
|
+
const { stdout: afterHash } = await execAsync(`git rev-parse HEAD`, { cwd: repoRoot });
|
|
12480
|
+
const hashAfterPull = afterHash.trim();
|
|
12481
|
+
if (hashAfterPull !== hashBeforePull) {
|
|
12482
|
+
const pulledChangedFiles = await this.git.getChangedFiles(hashBeforePull, hashAfterPull, ".gitgov/");
|
|
12483
|
+
result.implicitPull = {
|
|
12484
|
+
hasChanges: true,
|
|
12485
|
+
filesUpdated: pulledChangedFiles.length,
|
|
12486
|
+
reindexed: false
|
|
12487
|
+
// Will be set to true after actual reindex
|
|
12488
|
+
};
|
|
12489
|
+
log(`[EARS-54] Implicit pull: ${pulledChangedFiles.length} files from remote were rebased`);
|
|
12490
|
+
}
|
|
12491
|
+
} catch (e) {
|
|
12492
|
+
log(`[EARS-54] Could not capture implicit pull details: ${e}`);
|
|
12493
|
+
}
|
|
12494
|
+
}
|
|
12495
|
+
} catch (pullError) {
|
|
12496
|
+
const errorMsg = pullError instanceof Error ? pullError.message : String(pullError);
|
|
12497
|
+
log(`Pull rebase result: ${errorMsg}`);
|
|
12498
|
+
const isAlreadyUpToDate = errorMsg.includes("up to date") || errorMsg.includes("up-to-date");
|
|
12499
|
+
const isNoRemote = errorMsg.includes("does not appear to be") || errorMsg.includes("Could not read from remote");
|
|
12500
|
+
const isNoUpstream = errorMsg.includes("no tracking information") || errorMsg.includes("There is no tracking information");
|
|
12501
|
+
if (isAlreadyUpToDate || isNoRemote || isNoUpstream) {
|
|
12502
|
+
log("Pull not needed or no remote - continuing to push");
|
|
12503
|
+
} else {
|
|
12504
|
+
const isRebaseInProgress = await this.isRebaseInProgress();
|
|
12505
|
+
const conflictedFiles = await this.git.getConflictedFiles();
|
|
12506
|
+
if (isRebaseInProgress || conflictedFiles.length > 0) {
|
|
12507
|
+
log(`[GIT-NATIVE] Conflict detected! Files: ${conflictedFiles.join(", ")}`);
|
|
12508
|
+
result.conflictDetected = true;
|
|
12509
|
+
const fileWord = conflictedFiles.length === 1 ? "file" : "files";
|
|
12510
|
+
const stageCommand = conflictedFiles.length === 1 ? `git add ${conflictedFiles[0]}` : "git add .gitgov/";
|
|
12511
|
+
result.conflictInfo = {
|
|
12512
|
+
type: "rebase_conflict",
|
|
12513
|
+
affectedFiles: conflictedFiles,
|
|
12514
|
+
message: "Conflict detected during sync - Git has paused the rebase for manual resolution",
|
|
12515
|
+
resolutionSteps: [
|
|
12516
|
+
`1. Edit the conflicted ${fileWord} to resolve conflicts (remove <<<<<<, ======, >>>>>> markers)`,
|
|
12517
|
+
`2. Stage resolved ${fileWord}: ${stageCommand}`,
|
|
12518
|
+
"3. Complete sync: gitgov sync resolve --reason 'your reason'",
|
|
12519
|
+
"(This will continue the rebase, re-sign the record, and return you to your original branch)"
|
|
12520
|
+
]
|
|
12521
|
+
};
|
|
12522
|
+
result.error = `Conflict detected: ${conflictedFiles.length} file(s) need manual resolution. Use 'git status' to see details.`;
|
|
12523
|
+
if (stashHash) {
|
|
12524
|
+
try {
|
|
12525
|
+
await this.git.checkoutBranch(sourceBranch);
|
|
12526
|
+
await execAsync("git stash pop", { cwd: repoRoot });
|
|
12527
|
+
await this.git.checkoutBranch(stateBranch);
|
|
12528
|
+
log("Restored stash to original branch during conflict");
|
|
12529
|
+
} catch (stashErr) {
|
|
12530
|
+
log(`Warning: Could not restore stash: ${stashErr}`);
|
|
12531
|
+
}
|
|
12532
|
+
}
|
|
12533
|
+
if (tempDir) {
|
|
12534
|
+
log("Restoring local files (.key, .session.json, etc.) for conflict resolution...");
|
|
12535
|
+
const gitgovInState = path6__default.join(repoRoot, ".gitgov");
|
|
12536
|
+
for (const fileName of LOCAL_ONLY_FILES) {
|
|
12537
|
+
const srcPath = path6__default.join(tempDir, fileName);
|
|
12538
|
+
const destPath = path6__default.join(gitgovInState, fileName);
|
|
12539
|
+
try {
|
|
12540
|
+
await promises.access(srcPath);
|
|
12541
|
+
await promises.cp(srcPath, destPath, { force: true });
|
|
12542
|
+
log(`Restored LOCAL_ONLY_FILE for conflict resolution: ${fileName}`);
|
|
12543
|
+
} catch {
|
|
12544
|
+
}
|
|
12545
|
+
}
|
|
12546
|
+
const restoreExcluded = async (srcDir, destDir) => {
|
|
12547
|
+
try {
|
|
12548
|
+
const entries = await promises.readdir(srcDir, { withFileTypes: true });
|
|
12549
|
+
for (const entry of entries) {
|
|
12550
|
+
const srcPath = path6__default.join(srcDir, entry.name);
|
|
12551
|
+
const dstPath = path6__default.join(destDir, entry.name);
|
|
12552
|
+
if (entry.isDirectory()) {
|
|
12553
|
+
await restoreExcluded(srcPath, dstPath);
|
|
12554
|
+
} else {
|
|
12555
|
+
const isExcluded = SYNC_EXCLUDED_PATTERNS.some((pattern) => pattern.test(entry.name));
|
|
12556
|
+
if (isExcluded) {
|
|
12557
|
+
await promises.mkdir(path6__default.dirname(dstPath), { recursive: true });
|
|
12558
|
+
await promises.copyFile(srcPath, dstPath);
|
|
12559
|
+
log(`Restored EXCLUDED file for conflict resolution: ${entry.name}`);
|
|
12560
|
+
}
|
|
12561
|
+
}
|
|
12562
|
+
}
|
|
12563
|
+
} catch {
|
|
12564
|
+
}
|
|
12565
|
+
};
|
|
12566
|
+
await restoreExcluded(tempDir, gitgovInState);
|
|
12567
|
+
log("Local files restored for conflict resolution");
|
|
12568
|
+
}
|
|
12569
|
+
return result;
|
|
12570
|
+
}
|
|
12571
|
+
throw pullError;
|
|
12572
|
+
}
|
|
12573
|
+
}
|
|
12574
|
+
log("=== Phase 4: Push to Remote ===");
|
|
12245
12575
|
log("Pushing to remote...");
|
|
12246
12576
|
try {
|
|
12247
12577
|
await this.git.push("origin", stateBranch);
|
|
@@ -12271,11 +12601,57 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
|
|
|
12271
12601
|
}
|
|
12272
12602
|
}
|
|
12273
12603
|
if (tempDir) {
|
|
12274
|
-
log("[EARS-43] Restoring ENTIRE .gitgov/ from temp directory to working tree...");
|
|
12275
12604
|
const repoRoot2 = await this.git.getRepoRoot();
|
|
12276
12605
|
const gitgovDir = path6__default.join(repoRoot2, ".gitgov");
|
|
12277
|
-
|
|
12278
|
-
|
|
12606
|
+
if (result.implicitPull?.hasChanges) {
|
|
12607
|
+
log("[EARS-56] Implicit pull detected - copying synced files from gitgov-state first...");
|
|
12608
|
+
try {
|
|
12609
|
+
await this.git.checkoutFilesFromBranch(stateBranch, [".gitgov/"]);
|
|
12610
|
+
await execAsync("git reset HEAD .gitgov/ 2>/dev/null || true", { cwd: repoRoot2 });
|
|
12611
|
+
log("[EARS-56] Synced files copied from gitgov-state to work branch (unstaged)");
|
|
12612
|
+
} catch (checkoutError) {
|
|
12613
|
+
log(`[EARS-56] Warning: Failed to checkout from gitgov-state: ${checkoutError}`);
|
|
12614
|
+
await promises.cp(tempDir, gitgovDir, { recursive: true, force: true });
|
|
12615
|
+
log("[EARS-56] Fallback: Entire .gitgov/ restored from temp");
|
|
12616
|
+
}
|
|
12617
|
+
log("[EARS-56] Restoring local-only files from temp directory...");
|
|
12618
|
+
for (const fileName of LOCAL_ONLY_FILES) {
|
|
12619
|
+
const tempFilePath = path6__default.join(tempDir, fileName);
|
|
12620
|
+
const destFilePath = path6__default.join(gitgovDir, fileName);
|
|
12621
|
+
try {
|
|
12622
|
+
await promises.access(tempFilePath);
|
|
12623
|
+
await promises.cp(tempFilePath, destFilePath, { force: true });
|
|
12624
|
+
log(`[EARS-56] Restored LOCAL_ONLY_FILE: ${fileName}`);
|
|
12625
|
+
} catch {
|
|
12626
|
+
}
|
|
12627
|
+
}
|
|
12628
|
+
const restoreExcludedFiles = async (dir, destDir) => {
|
|
12629
|
+
try {
|
|
12630
|
+
const entries = await promises.readdir(dir, { withFileTypes: true });
|
|
12631
|
+
for (const entry of entries) {
|
|
12632
|
+
const srcPath = path6__default.join(dir, entry.name);
|
|
12633
|
+
const dstPath = path6__default.join(destDir, entry.name);
|
|
12634
|
+
if (entry.isDirectory()) {
|
|
12635
|
+
await restoreExcludedFiles(srcPath, dstPath);
|
|
12636
|
+
} else {
|
|
12637
|
+
const isExcluded = SYNC_EXCLUDED_PATTERNS.some((pattern) => pattern.test(entry.name));
|
|
12638
|
+
if (isExcluded) {
|
|
12639
|
+
await promises.mkdir(path6__default.dirname(dstPath), { recursive: true });
|
|
12640
|
+
await promises.copyFile(srcPath, dstPath);
|
|
12641
|
+
log(`[EARS-59] Restored excluded file: ${entry.name}`);
|
|
12642
|
+
}
|
|
12643
|
+
}
|
|
12644
|
+
}
|
|
12645
|
+
} catch {
|
|
12646
|
+
}
|
|
12647
|
+
};
|
|
12648
|
+
await restoreExcludedFiles(tempDir, gitgovDir);
|
|
12649
|
+
log("[EARS-59] Local-only and excluded files restored from temp");
|
|
12650
|
+
} else {
|
|
12651
|
+
log("[EARS-43] Restoring ENTIRE .gitgov/ from temp directory to working tree...");
|
|
12652
|
+
await promises.cp(tempDir, gitgovDir, { recursive: true, force: true });
|
|
12653
|
+
log("[EARS-43] Entire .gitgov/ restored from temp");
|
|
12654
|
+
}
|
|
12279
12655
|
log("[EARS-43] Cleaning up temp directory...");
|
|
12280
12656
|
try {
|
|
12281
12657
|
await promises.rm(tempDir, { recursive: true, force: true });
|
|
@@ -12284,6 +12660,17 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
|
|
|
12284
12660
|
log(`[EARS-43] Warning: Failed to cleanup temp directory: ${cleanupError}`);
|
|
12285
12661
|
}
|
|
12286
12662
|
}
|
|
12663
|
+
if (result.implicitPull?.hasChanges) {
|
|
12664
|
+
log("[EARS-54] Regenerating index after implicit pull...");
|
|
12665
|
+
try {
|
|
12666
|
+
await this.indexer.generateIndex();
|
|
12667
|
+
result.implicitPull.reindexed = true;
|
|
12668
|
+
log("[EARS-54] Index regenerated successfully after implicit pull");
|
|
12669
|
+
} catch (indexError) {
|
|
12670
|
+
log(`[EARS-54] Warning: Failed to regenerate index after implicit pull: ${indexError}`);
|
|
12671
|
+
result.implicitPull.reindexed = false;
|
|
12672
|
+
}
|
|
12673
|
+
}
|
|
12287
12674
|
result.success = true;
|
|
12288
12675
|
result.filesSynced = delta.length;
|
|
12289
12676
|
log(`=== pushState COMPLETED SUCCESSFULLY: ${delta.length} files synced ===`);
|
|
@@ -12326,7 +12713,7 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
|
|
|
12326
12713
|
* [EARS-44] Requires remote to be configured (pull without remote makes no sense)
|
|
12327
12714
|
*/
|
|
12328
12715
|
async pullState(options = {}) {
|
|
12329
|
-
const { forceReindex = false } = options;
|
|
12716
|
+
const { forceReindex = false, force = false } = options;
|
|
12330
12717
|
const stateBranch = await this.getStateBranchName();
|
|
12331
12718
|
if (!stateBranch) {
|
|
12332
12719
|
throw new SyncError("Failed to get state branch name");
|
|
@@ -12393,6 +12780,37 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
|
|
|
12393
12780
|
} catch (error) {
|
|
12394
12781
|
log(`[EARS-51] Warning: Could not save local files: ${error.message}`);
|
|
12395
12782
|
}
|
|
12783
|
+
const savedSyncableFiles = /* @__PURE__ */ new Map();
|
|
12784
|
+
try {
|
|
12785
|
+
const gitgovPath2 = path6__default.join(pullRepoRoot, ".gitgov");
|
|
12786
|
+
const gitgovExists2 = await promises.access(gitgovPath2).then(() => true).catch(() => false);
|
|
12787
|
+
if (gitgovExists2) {
|
|
12788
|
+
const readSyncableFilesRecursive = async (dir, baseDir) => {
|
|
12789
|
+
try {
|
|
12790
|
+
const entries = await promises.readdir(dir, { withFileTypes: true });
|
|
12791
|
+
for (const entry of entries) {
|
|
12792
|
+
const fullPath = path6__default.join(dir, entry.name);
|
|
12793
|
+
const relativePath = path6__default.relative(baseDir, fullPath);
|
|
12794
|
+
const gitgovRelativePath = `.gitgov/${relativePath}`;
|
|
12795
|
+
if (entry.isDirectory()) {
|
|
12796
|
+
await readSyncableFilesRecursive(fullPath, baseDir);
|
|
12797
|
+
} else if (shouldSyncFile(gitgovRelativePath)) {
|
|
12798
|
+
try {
|
|
12799
|
+
const content = await promises.readFile(fullPath, "utf-8");
|
|
12800
|
+
savedSyncableFiles.set(gitgovRelativePath, content);
|
|
12801
|
+
} catch {
|
|
12802
|
+
}
|
|
12803
|
+
}
|
|
12804
|
+
}
|
|
12805
|
+
} catch {
|
|
12806
|
+
}
|
|
12807
|
+
};
|
|
12808
|
+
await readSyncableFilesRecursive(gitgovPath2, gitgovPath2);
|
|
12809
|
+
log(`[EARS-61] Saved ${savedSyncableFiles.size} syncable files before checkout for conflict detection`);
|
|
12810
|
+
}
|
|
12811
|
+
} catch (error) {
|
|
12812
|
+
log(`[EARS-61] Warning: Could not save syncable files: ${error.message}`);
|
|
12813
|
+
}
|
|
12396
12814
|
try {
|
|
12397
12815
|
await this.git.checkoutBranch(stateBranch);
|
|
12398
12816
|
} catch (checkoutError) {
|
|
@@ -12427,6 +12845,90 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
|
|
|
12427
12845
|
});
|
|
12428
12846
|
const hashBefore = commitBefore[0]?.hash;
|
|
12429
12847
|
await this.git.fetch("origin");
|
|
12848
|
+
log("[EARS-61] Checking for local changes that would be overwritten...");
|
|
12849
|
+
let remoteChangedFiles = [];
|
|
12850
|
+
try {
|
|
12851
|
+
const { stdout: remoteChanges } = await execAsync(
|
|
12852
|
+
`git diff --name-only ${stateBranch} origin/${stateBranch} -- .gitgov/ 2>/dev/null || true`,
|
|
12853
|
+
{ cwd: pullRepoRoot }
|
|
12854
|
+
);
|
|
12855
|
+
remoteChangedFiles = remoteChanges.trim().split("\n").filter((f) => f && shouldSyncFile(f));
|
|
12856
|
+
log(`[EARS-61] Remote changed files: ${remoteChangedFiles.length} - ${remoteChangedFiles.join(", ")}`);
|
|
12857
|
+
} catch {
|
|
12858
|
+
log("[EARS-61] Could not determine remote changes, continuing...");
|
|
12859
|
+
}
|
|
12860
|
+
let localModifiedFiles = [];
|
|
12861
|
+
if (remoteChangedFiles.length > 0 && savedSyncableFiles.size > 0) {
|
|
12862
|
+
try {
|
|
12863
|
+
for (const remoteFile of remoteChangedFiles) {
|
|
12864
|
+
const savedContent = savedSyncableFiles.get(remoteFile);
|
|
12865
|
+
if (savedContent !== void 0) {
|
|
12866
|
+
try {
|
|
12867
|
+
const { stdout: gitStateContent } = await execAsync(
|
|
12868
|
+
`git show HEAD:${remoteFile} 2>/dev/null`,
|
|
12869
|
+
{ cwd: pullRepoRoot }
|
|
12870
|
+
);
|
|
12871
|
+
if (savedContent !== gitStateContent) {
|
|
12872
|
+
localModifiedFiles.push(remoteFile);
|
|
12873
|
+
log(`[EARS-61] Local file was modified since last sync: ${remoteFile}`);
|
|
12874
|
+
}
|
|
12875
|
+
} catch {
|
|
12876
|
+
localModifiedFiles.push(remoteFile);
|
|
12877
|
+
log(`[EARS-61] Local file is new (not in gitgov-state): ${remoteFile}`);
|
|
12878
|
+
}
|
|
12879
|
+
}
|
|
12880
|
+
}
|
|
12881
|
+
log(`[EARS-61] Local modified files that overlap with remote: ${localModifiedFiles.length}`);
|
|
12882
|
+
} catch (error) {
|
|
12883
|
+
log(`[EARS-61] Warning: Could not check local modifications: ${error.message}`);
|
|
12884
|
+
}
|
|
12885
|
+
}
|
|
12886
|
+
if (localModifiedFiles.length > 0) {
|
|
12887
|
+
if (force) {
|
|
12888
|
+
log(`[EARS-62] Force flag set - will overwrite ${localModifiedFiles.length} local file(s)`);
|
|
12889
|
+
logger6.warn(`[pullState] Force pull: overwriting local changes to ${localModifiedFiles.length} file(s)`);
|
|
12890
|
+
result.forcedOverwrites = localModifiedFiles;
|
|
12891
|
+
} else {
|
|
12892
|
+
log(`[EARS-61] CONFLICT: Local changes would be overwritten by pull`);
|
|
12893
|
+
await this.git.checkoutBranch(savedBranch);
|
|
12894
|
+
for (const [filePath, content] of savedSyncableFiles) {
|
|
12895
|
+
const fullPath = path6__default.join(pullRepoRoot, filePath);
|
|
12896
|
+
try {
|
|
12897
|
+
await promises.mkdir(path6__default.dirname(fullPath), { recursive: true });
|
|
12898
|
+
await promises.writeFile(fullPath, content, "utf-8");
|
|
12899
|
+
log(`[EARS-61] Restored syncable file: ${filePath}`);
|
|
12900
|
+
} catch {
|
|
12901
|
+
}
|
|
12902
|
+
}
|
|
12903
|
+
for (const [fileName, content] of savedLocalFiles) {
|
|
12904
|
+
const filePath = path6__default.join(pullRepoRoot, ".gitgov", fileName);
|
|
12905
|
+
try {
|
|
12906
|
+
await promises.mkdir(path6__default.dirname(filePath), { recursive: true });
|
|
12907
|
+
await promises.writeFile(filePath, content, "utf-8");
|
|
12908
|
+
log(`[EARS-61] Restored local-only file: ${fileName}`);
|
|
12909
|
+
} catch {
|
|
12910
|
+
}
|
|
12911
|
+
}
|
|
12912
|
+
result.success = false;
|
|
12913
|
+
result.conflictDetected = true;
|
|
12914
|
+
result.conflictInfo = {
|
|
12915
|
+
type: "local_changes_conflict",
|
|
12916
|
+
affectedFiles: localModifiedFiles,
|
|
12917
|
+
message: `Your local changes to the following files would be overwritten by pull.
|
|
12918
|
+
You have modified these files locally, and they were also modified remotely.
|
|
12919
|
+
To avoid losing your changes, push first or use --force to overwrite.`,
|
|
12920
|
+
resolutionSteps: [
|
|
12921
|
+
"1. Run 'gitgov sync push' to push your local changes first",
|
|
12922
|
+
" \u2192 This will trigger a rebase and let you resolve conflicts properly",
|
|
12923
|
+
"2. Or run 'gitgov sync pull --force' to discard your local changes"
|
|
12924
|
+
]
|
|
12925
|
+
};
|
|
12926
|
+
result.error = "Aborting pull: local changes would be overwritten by remote changes";
|
|
12927
|
+
logger6.warn(`[pullState] Aborting: local changes to ${localModifiedFiles.length} file(s) would be overwritten by pull`);
|
|
12928
|
+
return result;
|
|
12929
|
+
}
|
|
12930
|
+
}
|
|
12931
|
+
log("[EARS-61] No conflicting local changes (or force enabled), proceeding with pull...");
|
|
12430
12932
|
try {
|
|
12431
12933
|
await this.git.pullRebase("origin", stateBranch);
|
|
12432
12934
|
} catch (error) {
|
|
@@ -12457,7 +12959,10 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
|
|
|
12457
12959
|
const hashAfter = commitAfter[0]?.hash;
|
|
12458
12960
|
const hasNewChanges = hashBefore !== hashAfter;
|
|
12459
12961
|
result.hasChanges = hasNewChanges;
|
|
12460
|
-
|
|
12962
|
+
const indexPath = path6__default.join(pullRepoRoot, ".gitgov", "index.json");
|
|
12963
|
+
const indexExists = await promises.access(indexPath).then(() => true).catch(() => false);
|
|
12964
|
+
const shouldReindex = hasNewChanges || forceReindex || !indexExists;
|
|
12965
|
+
if (shouldReindex) {
|
|
12461
12966
|
result.reindexed = true;
|
|
12462
12967
|
if (hasNewChanges && hashBefore && hashAfter) {
|
|
12463
12968
|
const changedFiles = await this.git.getChangedFiles(
|
|
@@ -12467,13 +12972,6 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
|
|
|
12467
12972
|
);
|
|
12468
12973
|
result.filesUpdated = changedFiles.length;
|
|
12469
12974
|
}
|
|
12470
|
-
logger6.info("Invoking IndexerAdapter.generateIndex() after pull...");
|
|
12471
|
-
try {
|
|
12472
|
-
await this.indexer.generateIndex();
|
|
12473
|
-
logger6.info("Index regenerated successfully");
|
|
12474
|
-
} catch (error) {
|
|
12475
|
-
logger6.warn(`Failed to regenerate index: ${error.message}`);
|
|
12476
|
-
}
|
|
12477
12975
|
}
|
|
12478
12976
|
const gitgovPath = path6__default.join(pullRepoRoot, ".gitgov");
|
|
12479
12977
|
const gitgovExists = await promises.access(gitgovPath).then(() => true).catch(() => false);
|
|
@@ -12518,6 +13016,15 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
|
|
|
12518
13016
|
logger6.warn(`[pullState] Failed to restore .gitgov/ to filesystem: ${error.message}`);
|
|
12519
13017
|
}
|
|
12520
13018
|
}
|
|
13019
|
+
if (shouldReindex) {
|
|
13020
|
+
logger6.info("Invoking IndexerAdapter.generateIndex() after pull...");
|
|
13021
|
+
try {
|
|
13022
|
+
await this.indexer.generateIndex();
|
|
13023
|
+
logger6.info("Index regenerated successfully");
|
|
13024
|
+
} catch (error) {
|
|
13025
|
+
logger6.warn(`Failed to regenerate index: ${error.message}`);
|
|
13026
|
+
}
|
|
13027
|
+
}
|
|
12521
13028
|
result.success = true;
|
|
12522
13029
|
log(`=== pullState COMPLETED: ${hasNewChanges ? "new changes pulled" : "no changes"}, reindexed: ${result.reindexed} ===`);
|
|
12523
13030
|
return result;
|
|
@@ -12531,25 +13038,37 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
|
|
|
12531
13038
|
}
|
|
12532
13039
|
}
|
|
12533
13040
|
/**
|
|
12534
|
-
* Resolves state conflicts in a governed manner.
|
|
12535
|
-
*
|
|
12536
|
-
*
|
|
13041
|
+
* Resolves state conflicts in a governed manner (Git-Native).
|
|
13042
|
+
*
|
|
13043
|
+
* Git-Native Flow:
|
|
13044
|
+
* 1. User resolves conflicts using standard Git tools (edit files, remove markers)
|
|
13045
|
+
* 2. User stages resolved files: git add .gitgov/
|
|
13046
|
+
* 3. User runs: gitgov sync resolve --reason "reason"
|
|
13047
|
+
*
|
|
13048
|
+
* This method:
|
|
13049
|
+
* - Verifies that a rebase is in progress
|
|
13050
|
+
* - Checks that no conflict markers remain in staged files
|
|
13051
|
+
* - Updates resolved Records with new checksums and signatures
|
|
13052
|
+
* - Continues the git rebase (git rebase --continue)
|
|
13053
|
+
* - Creates a signed resolution commit
|
|
13054
|
+
* - Regenerates the index
|
|
12537
13055
|
*
|
|
12538
13056
|
* [EARS-17 through EARS-23]
|
|
12539
13057
|
*/
|
|
12540
13058
|
async resolveConflict(options) {
|
|
12541
13059
|
const { reason, actorId } = options;
|
|
12542
13060
|
const log = (msg) => logger6.debug(`[resolveConflict] ${msg}`);
|
|
12543
|
-
log("=== STARTING resolveConflict ===");
|
|
12544
|
-
log("Phase 0: Verifying rebase
|
|
13061
|
+
log("=== STARTING resolveConflict (Git-Native) ===");
|
|
13062
|
+
log("Phase 0: Verifying rebase in progress...");
|
|
12545
13063
|
const rebaseInProgress = await this.isRebaseInProgress();
|
|
12546
13064
|
if (!rebaseInProgress) {
|
|
12547
13065
|
throw new NoRebaseInProgressError();
|
|
12548
13066
|
}
|
|
13067
|
+
log("Conflict mode: rebase_conflict (Git-Native)");
|
|
12549
13068
|
console.log("[resolveConflict] Getting staged files...");
|
|
12550
13069
|
const allStagedFiles = await this.git.getStagedFiles();
|
|
12551
13070
|
console.log("[resolveConflict] All staged files:", allStagedFiles);
|
|
12552
|
-
|
|
13071
|
+
let resolvedRecords = allStagedFiles.filter(
|
|
12553
13072
|
(f) => f.startsWith(".gitgov/") && f.endsWith(".json")
|
|
12554
13073
|
);
|
|
12555
13074
|
console.log("[resolveConflict] Resolved Records (staged .gitgov/*.json):", resolvedRecords);
|
|
@@ -12559,7 +13078,33 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
|
|
|
12559
13078
|
if (filesWithMarkers.length > 0) {
|
|
12560
13079
|
throw new ConflictMarkersPresentError(filesWithMarkers);
|
|
12561
13080
|
}
|
|
12562
|
-
|
|
13081
|
+
let rebaseCommitHash = "";
|
|
13082
|
+
console.log("[resolveConflict] Step 4: Calling git.rebaseContinue()...");
|
|
13083
|
+
await this.git.rebaseContinue();
|
|
13084
|
+
console.log("[resolveConflict] rebaseContinue completed successfully");
|
|
13085
|
+
const currentBranch = await this.git.getCurrentBranch();
|
|
13086
|
+
const rebaseCommit = await this.git.getCommitHistory(currentBranch, {
|
|
13087
|
+
maxCount: 1
|
|
13088
|
+
});
|
|
13089
|
+
rebaseCommitHash = rebaseCommit[0]?.hash ?? "";
|
|
13090
|
+
if (resolvedRecords.length === 0 && rebaseCommitHash) {
|
|
13091
|
+
console.log("[resolveConflict] No staged files detected, getting files from rebase commit...");
|
|
13092
|
+
const repoRoot2 = await this.git.getRepoRoot();
|
|
13093
|
+
try {
|
|
13094
|
+
const { stdout } = await execAsync(
|
|
13095
|
+
`git diff-tree --no-commit-id --name-only -r ${rebaseCommitHash}`,
|
|
13096
|
+
{ cwd: repoRoot2 }
|
|
13097
|
+
);
|
|
13098
|
+
const commitFiles = stdout.trim().split("\n").filter((f) => f);
|
|
13099
|
+
resolvedRecords = commitFiles.filter(
|
|
13100
|
+
(f) => f.startsWith(".gitgov/") && f.endsWith(".json")
|
|
13101
|
+
);
|
|
13102
|
+
console.log("[resolveConflict] Files from rebase commit:", resolvedRecords);
|
|
13103
|
+
} catch (e) {
|
|
13104
|
+
console.log("[resolveConflict] Could not get files from rebase commit:", e);
|
|
13105
|
+
}
|
|
13106
|
+
}
|
|
13107
|
+
console.log("[resolveConflict] Updating resolved Records with signatures...");
|
|
12563
13108
|
if (resolvedRecords.length > 0) {
|
|
12564
13109
|
const currentActor = await this.identity.getCurrentActor();
|
|
12565
13110
|
console.log("[resolveConflict] Current actor:", currentActor);
|
|
@@ -12567,8 +13112,8 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
|
|
|
12567
13112
|
for (const filePath of resolvedRecords) {
|
|
12568
13113
|
console.log("[resolveConflict] Processing Record:", filePath);
|
|
12569
13114
|
try {
|
|
12570
|
-
const
|
|
12571
|
-
const fullPath = join(
|
|
13115
|
+
const repoRoot2 = await this.git.getRepoRoot();
|
|
13116
|
+
const fullPath = join(repoRoot2, filePath);
|
|
12572
13117
|
const content = readFileSync(fullPath, "utf-8");
|
|
12573
13118
|
const record = JSON.parse(content);
|
|
12574
13119
|
if (!record.header || !record.payload) {
|
|
@@ -12577,7 +13122,8 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
|
|
|
12577
13122
|
const signedRecord = await this.identity.signRecord(
|
|
12578
13123
|
record,
|
|
12579
13124
|
currentActor.id,
|
|
12580
|
-
"resolver"
|
|
13125
|
+
"resolver",
|
|
13126
|
+
`Conflict resolved: ${reason}`
|
|
12581
13127
|
);
|
|
12582
13128
|
writeFileSync(fullPath, JSON.stringify(signedRecord, null, 2) + "\n", "utf-8");
|
|
12583
13129
|
logger6.info(`Updated Record: ${filePath} (new checksum + resolver signature)`);
|
|
@@ -12587,20 +13133,12 @@ Files: ${delta.length} file(s) ${isFirstPush ? "synced (initial)" : "changed"}
|
|
|
12587
13133
|
console.log("[resolveConflict] Error updating Record:", filePath, error);
|
|
12588
13134
|
}
|
|
12589
13135
|
}
|
|
12590
|
-
console.log("[resolveConflict] All Records updated,
|
|
13136
|
+
console.log("[resolveConflict] All Records updated, staging...");
|
|
12591
13137
|
}
|
|
12592
|
-
console.log("[resolveConflict]
|
|
13138
|
+
console.log("[resolveConflict] Staging .gitgov/ with updated metadata...");
|
|
12593
13139
|
await this.git.add([".gitgov"], { force: true });
|
|
12594
|
-
console.log("[resolveConflict] Step 6: Calling git.rebaseContinue() (THIS MAY HANG)...");
|
|
12595
|
-
await this.git.rebaseContinue();
|
|
12596
|
-
console.log("[resolveConflict] rebaseContinue completed successfully");
|
|
12597
|
-
const currentBranch = await this.git.getCurrentBranch();
|
|
12598
|
-
const rebaseCommit = await this.git.getCommitHistory(currentBranch, {
|
|
12599
|
-
maxCount: 1
|
|
12600
|
-
});
|
|
12601
|
-
const rebaseCommitHash = rebaseCommit[0]?.hash ?? "";
|
|
12602
13140
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
12603
|
-
const resolutionMessage = `resolution:
|
|
13141
|
+
const resolutionMessage = `resolution: conflict resolved by ${actorId}
|
|
12604
13142
|
|
|
12605
13143
|
Actor: ${actorId}
|
|
12606
13144
|
Timestamp: ${timestamp}
|
|
@@ -12608,9 +13146,116 @@ Reason: ${reason}
|
|
|
12608
13146
|
Files: ${resolvedRecords.length} file(s) resolved
|
|
12609
13147
|
|
|
12610
13148
|
Signed-off-by: ${actorId}`;
|
|
12611
|
-
|
|
12612
|
-
|
|
12613
|
-
|
|
13149
|
+
let resolutionCommitHash = "";
|
|
13150
|
+
try {
|
|
13151
|
+
resolutionCommitHash = await this.git.commit(resolutionMessage);
|
|
13152
|
+
} catch (commitError) {
|
|
13153
|
+
const stdout = commitError.stdout || "";
|
|
13154
|
+
const stderr = commitError.stderr || "";
|
|
13155
|
+
const isNothingToCommit = stdout.includes("nothing to commit") || stderr.includes("nothing to commit") || stdout.includes("nothing added to commit") || stderr.includes("nothing added to commit");
|
|
13156
|
+
if (isNothingToCommit) {
|
|
13157
|
+
log("No additional changes to commit (no records needed re-signing)");
|
|
13158
|
+
resolutionCommitHash = rebaseCommitHash;
|
|
13159
|
+
} else {
|
|
13160
|
+
throw commitError;
|
|
13161
|
+
}
|
|
13162
|
+
}
|
|
13163
|
+
log("Pushing resolved state to remote...");
|
|
13164
|
+
try {
|
|
13165
|
+
await this.git.push("origin", "gitgov-state");
|
|
13166
|
+
log("Push successful");
|
|
13167
|
+
} catch (pushError) {
|
|
13168
|
+
const pushErrorMsg = pushError instanceof Error ? pushError.message : String(pushError);
|
|
13169
|
+
log(`Push failed (non-fatal): ${pushErrorMsg}`);
|
|
13170
|
+
}
|
|
13171
|
+
log("Returning to original branch and restoring .gitgov/ files...");
|
|
13172
|
+
const repoRoot = await this.git.getRepoRoot();
|
|
13173
|
+
const gitgovDir = path6__default.join(repoRoot, ".gitgov");
|
|
13174
|
+
const tempDir = path6__default.join(os.tmpdir(), `gitgov-resolve-${Date.now()}`);
|
|
13175
|
+
await promises.mkdir(tempDir, { recursive: true });
|
|
13176
|
+
log(`Created temp directory for local files: ${tempDir}`);
|
|
13177
|
+
for (const fileName of LOCAL_ONLY_FILES) {
|
|
13178
|
+
const srcPath = path6__default.join(gitgovDir, fileName);
|
|
13179
|
+
const destPath = path6__default.join(tempDir, fileName);
|
|
13180
|
+
try {
|
|
13181
|
+
await promises.access(srcPath);
|
|
13182
|
+
await promises.cp(srcPath, destPath, { force: true });
|
|
13183
|
+
log(`Saved LOCAL_ONLY_FILE to temp: ${fileName}`);
|
|
13184
|
+
} catch {
|
|
13185
|
+
}
|
|
13186
|
+
}
|
|
13187
|
+
const saveExcludedFiles = async (srcDir, destDir) => {
|
|
13188
|
+
try {
|
|
13189
|
+
const entries = await promises.readdir(srcDir, { withFileTypes: true });
|
|
13190
|
+
for (const entry of entries) {
|
|
13191
|
+
const srcPath = path6__default.join(srcDir, entry.name);
|
|
13192
|
+
const dstPath = path6__default.join(destDir, entry.name);
|
|
13193
|
+
if (entry.isDirectory()) {
|
|
13194
|
+
await promises.mkdir(dstPath, { recursive: true });
|
|
13195
|
+
await saveExcludedFiles(srcPath, dstPath);
|
|
13196
|
+
} else {
|
|
13197
|
+
const isExcluded = SYNC_EXCLUDED_PATTERNS.some((pattern) => pattern.test(entry.name));
|
|
13198
|
+
if (isExcluded) {
|
|
13199
|
+
await promises.mkdir(path6__default.dirname(dstPath), { recursive: true });
|
|
13200
|
+
await promises.copyFile(srcPath, dstPath);
|
|
13201
|
+
log(`Saved EXCLUDED file to temp: ${entry.name}`);
|
|
13202
|
+
}
|
|
13203
|
+
}
|
|
13204
|
+
}
|
|
13205
|
+
} catch {
|
|
13206
|
+
}
|
|
13207
|
+
};
|
|
13208
|
+
await saveExcludedFiles(gitgovDir, tempDir);
|
|
13209
|
+
try {
|
|
13210
|
+
await execAsync("git checkout -", { cwd: repoRoot });
|
|
13211
|
+
log("Returned to original branch");
|
|
13212
|
+
} catch (checkoutError) {
|
|
13213
|
+
log(`Warning: Could not return to original branch: ${checkoutError}`);
|
|
13214
|
+
}
|
|
13215
|
+
log("Restoring .gitgov/ from gitgov-state...");
|
|
13216
|
+
try {
|
|
13217
|
+
await this.git.checkoutFilesFromBranch("gitgov-state", [".gitgov/"]);
|
|
13218
|
+
await execAsync("git reset HEAD .gitgov/ 2>/dev/null || true", { cwd: repoRoot });
|
|
13219
|
+
log("Restored .gitgov/ from gitgov-state (unstaged)");
|
|
13220
|
+
} catch (checkoutFilesError) {
|
|
13221
|
+
log(`Warning: Could not restore .gitgov/ from gitgov-state: ${checkoutFilesError}`);
|
|
13222
|
+
}
|
|
13223
|
+
for (const fileName of LOCAL_ONLY_FILES) {
|
|
13224
|
+
const srcPath = path6__default.join(tempDir, fileName);
|
|
13225
|
+
const destPath = path6__default.join(gitgovDir, fileName);
|
|
13226
|
+
try {
|
|
13227
|
+
await promises.access(srcPath);
|
|
13228
|
+
await promises.cp(srcPath, destPath, { force: true });
|
|
13229
|
+
log(`Restored LOCAL_ONLY_FILE from temp: ${fileName}`);
|
|
13230
|
+
} catch {
|
|
13231
|
+
}
|
|
13232
|
+
}
|
|
13233
|
+
const restoreExcludedFiles = async (srcDir, destDir) => {
|
|
13234
|
+
try {
|
|
13235
|
+
const entries = await promises.readdir(srcDir, { withFileTypes: true });
|
|
13236
|
+
for (const entry of entries) {
|
|
13237
|
+
const srcPath = path6__default.join(srcDir, entry.name);
|
|
13238
|
+
const dstPath = path6__default.join(destDir, entry.name);
|
|
13239
|
+
if (entry.isDirectory()) {
|
|
13240
|
+
await restoreExcludedFiles(srcPath, dstPath);
|
|
13241
|
+
} else {
|
|
13242
|
+
const isExcluded = SYNC_EXCLUDED_PATTERNS.some((pattern) => pattern.test(entry.name));
|
|
13243
|
+
if (isExcluded) {
|
|
13244
|
+
await promises.mkdir(path6__default.dirname(dstPath), { recursive: true });
|
|
13245
|
+
await promises.copyFile(srcPath, dstPath);
|
|
13246
|
+
log(`Restored EXCLUDED file from temp: ${entry.name}`);
|
|
13247
|
+
}
|
|
13248
|
+
}
|
|
13249
|
+
}
|
|
13250
|
+
} catch {
|
|
13251
|
+
}
|
|
13252
|
+
};
|
|
13253
|
+
await restoreExcludedFiles(tempDir, gitgovDir);
|
|
13254
|
+
try {
|
|
13255
|
+
await promises.rm(tempDir, { recursive: true, force: true });
|
|
13256
|
+
log("Temp directory cleaned up");
|
|
13257
|
+
} catch {
|
|
13258
|
+
}
|
|
12614
13259
|
logger6.info("Invoking IndexerAdapter.generateIndex() after conflict resolution...");
|
|
12615
13260
|
try {
|
|
12616
13261
|
await this.indexer.generateIndex();
|