@gitgov/core 2.7.1 → 2.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/fs.d.ts CHANGED
@@ -1248,13 +1248,19 @@ declare class FsWorktreeSyncStateModule implements ISyncStateModule {
1248
1248
  getWorktreePath(): string;
1249
1249
  /** [WTSYNC-A1..A6] Ensures worktree exists and is healthy */
1250
1250
  ensureWorktree(): Promise<void>;
1251
+ /**
1252
+ * [WTSYNC-A7] Remove .gitignore from state branch if it exists.
1253
+ * The worktree module filters files in code (shouldSyncFile()), not via .gitignore.
1254
+ * Legacy state branches initialized by FsSyncState may have a .gitignore — remove it.
1255
+ */
1256
+ private removeLegacyGitignore;
1251
1257
  /** Check worktree health */
1252
1258
  private checkWorktreeHealth;
1253
1259
  /** Remove worktree cleanly */
1254
1260
  private removeWorktree;
1255
- /** [WTSYNC-B1..B14] Push local state to remote */
1261
+ /** [WTSYNC-B1..B16] Push local state to remote */
1256
1262
  pushState(options: SyncStatePushOptions): Promise<SyncStatePushResult>;
1257
- /** [WTSYNC-C1..C8] Pull remote state */
1263
+ /** [WTSYNC-C1..C9] Pull remote state */
1258
1264
  pullState(options?: SyncStatePullOptions): Promise<SyncStatePullResult>;
1259
1265
  /** [WTSYNC-D1..D7] Resolve rebase conflict */
1260
1266
  resolveConflict(options: SyncStateResolveOptions): Promise<SyncStateResolveResult>;
@@ -1280,6 +1286,12 @@ declare class FsWorktreeSyncStateModule implements ISyncStateModule {
1280
1286
  private execGit;
1281
1287
  /** Execute git command in worktree context (throws on non-zero exit) */
1282
1288
  private execInWorktree;
1289
+ /**
1290
+ * [WTSYNC-B16] Check if local gitgov-state has commits not present on remote.
1291
+ * Returns { ahead: true } if local has unpushed commits or remote branch doesn't exist.
1292
+ * Also returns { remoteExists } to let caller decide whether reconciliation is needed.
1293
+ */
1294
+ private isLocalAheadOfRemote;
1283
1295
  /** Calculate file delta (uncommitted changes in worktree) */
1284
1296
  private calculateFileDelta;
1285
1297
  /** [WTSYNC-B4/B9/B10/B11] Stage only syncable files from delta (adds, mods, and deletions) */
package/dist/src/fs.js CHANGED
@@ -7646,7 +7646,7 @@ var FsWorktreeSyncStateModule = class {
7646
7646
  this.gitgovPath = path9__default.join(this.worktreePath, ".gitgov");
7647
7647
  }
7648
7648
  // ═══════════════════════════════════════════════
7649
- // Section A: Worktree Management (WTSYNC-A1..A6)
7649
+ // Section A: Worktree Management (WTSYNC-A1..A7)
7650
7650
  // ═══════════════════════════════════════════════
7651
7651
  /** [WTSYNC-A4] Returns the worktree path */
7652
7652
  getWorktreePath() {
@@ -7657,6 +7657,7 @@ var FsWorktreeSyncStateModule = class {
7657
7657
  const health = await this.checkWorktreeHealth();
7658
7658
  if (health.healthy) {
7659
7659
  logger7.debug("Worktree is healthy");
7660
+ await this.removeLegacyGitignore();
7660
7661
  return;
7661
7662
  }
7662
7663
  if (health.exists && !health.healthy) {
@@ -7674,6 +7675,26 @@ var FsWorktreeSyncStateModule = class {
7674
7675
  error instanceof Error ? error : void 0
7675
7676
  );
7676
7677
  }
7678
+ await this.removeLegacyGitignore();
7679
+ }
7680
+ /**
7681
+ * [WTSYNC-A7] Remove .gitignore from state branch if it exists.
7682
+ * The worktree module filters files in code (shouldSyncFile()), not via .gitignore.
7683
+ * Legacy state branches initialized by FsSyncState may have a .gitignore — remove it.
7684
+ */
7685
+ async removeLegacyGitignore() {
7686
+ const gitignorePath = path9__default.join(this.worktreePath, ".gitignore");
7687
+ if (!existsSync(gitignorePath)) return;
7688
+ logger7.info("Removing legacy .gitignore from state branch");
7689
+ try {
7690
+ await this.execInWorktree(["rm", ".gitignore"]);
7691
+ await this.execInWorktree(["commit", "-m", "gitgov: remove legacy .gitignore (filtering is in code)"]);
7692
+ } catch {
7693
+ try {
7694
+ await promises.unlink(gitignorePath);
7695
+ } catch {
7696
+ }
7697
+ }
7677
7698
  }
7678
7699
  /** Check worktree health */
7679
7700
  async checkWorktreeHealth() {
@@ -7719,9 +7740,9 @@ var FsWorktreeSyncStateModule = class {
7719
7740
  }
7720
7741
  }
7721
7742
  // ═══════════════════════════════════════════════
7722
- // Section B: Push Operations (WTSYNC-B1..B14)
7743
+ // Section B: Push Operations (WTSYNC-B1..B16)
7723
7744
  // ═══════════════════════════════════════════════
7724
- /** [WTSYNC-B1..B14] Push local state to remote */
7745
+ /** [WTSYNC-B1..B16] Push local state to remote */
7725
7746
  async pushState(options) {
7726
7747
  const { actorId, dryRun = false, force = false } = options;
7727
7748
  const log = (msg) => logger7.debug(`[pushState] ${msg}`);
@@ -7749,11 +7770,53 @@ var FsWorktreeSyncStateModule = class {
7749
7770
  const delta = rawDelta.filter((f) => shouldSyncFile2(f.file));
7750
7771
  log(`Delta: ${delta.length} syncable files (${rawDelta.length} total)`);
7751
7772
  if (delta.length === 0) {
7773
+ const { ahead: aheadOfRemote, remoteExists } = await this.isLocalAheadOfRemote();
7774
+ if (!aheadOfRemote) {
7775
+ return {
7776
+ success: true,
7777
+ filesSynced: 0,
7778
+ sourceBranch: options.sourceBranch ?? "current",
7779
+ commitHash: null,
7780
+ commitMessage: null,
7781
+ conflictDetected: false
7782
+ };
7783
+ }
7784
+ log("No uncommitted changes but local is ahead of remote \u2014 pushing existing commits");
7785
+ if (remoteExists && !force) {
7786
+ try {
7787
+ await this.execInWorktree(["pull", "--rebase", "origin", this.stateBranchName]);
7788
+ } catch (err) {
7789
+ if (await this.isRebaseInProgress()) {
7790
+ const affectedFiles = await this.getConflictedFiles();
7791
+ return {
7792
+ success: false,
7793
+ filesSynced: 0,
7794
+ sourceBranch: options.sourceBranch ?? "current",
7795
+ commitHash: null,
7796
+ commitMessage: null,
7797
+ conflictDetected: true,
7798
+ conflictInfo: {
7799
+ type: "rebase_conflict",
7800
+ affectedFiles,
7801
+ message: "Rebase conflict during push reconciliation (local ahead, no uncommitted changes)",
7802
+ resolutionSteps: [
7803
+ `Edit conflicted files in ${this.worktreePath}/.gitgov/`,
7804
+ 'Run `gitgov sync resolve --reason "..."` to finalize'
7805
+ ]
7806
+ },
7807
+ error: "Rebase conflict during push reconciliation"
7808
+ };
7809
+ }
7810
+ throw err;
7811
+ }
7812
+ }
7813
+ await this.execInWorktree(["push", "origin", this.stateBranchName]);
7814
+ const currentHead = (await this.execInWorktree(["rev-parse", "HEAD"])).trim();
7752
7815
  return {
7753
7816
  success: true,
7754
7817
  filesSynced: 0,
7755
7818
  sourceBranch: options.sourceBranch ?? "current",
7756
- commitHash: null,
7819
+ commitHash: currentHead,
7757
7820
  commitMessage: null,
7758
7821
  conflictDetected: false
7759
7822
  };
@@ -7848,9 +7911,9 @@ var FsWorktreeSyncStateModule = class {
7848
7911
  return result;
7849
7912
  }
7850
7913
  // ═══════════════════════════════════════════════
7851
- // Section C: Pull Operations (WTSYNC-C1..C8)
7914
+ // Section C: Pull Operations (WTSYNC-C1..C9)
7852
7915
  // ═══════════════════════════════════════════════
7853
- /** [WTSYNC-C1..C8] Pull remote state */
7916
+ /** [WTSYNC-C1..C9] Pull remote state */
7854
7917
  async pullState(options) {
7855
7918
  const { forceReindex = false, force = false } = options ?? {};
7856
7919
  const log = (msg) => logger7.debug(`[pullState] ${msg}`);
@@ -8276,6 +8339,33 @@ var FsWorktreeSyncStateModule = class {
8276
8339
  async execInWorktree(args, options) {
8277
8340
  return this.execGit(["-C", this.worktreePath, ...args], options);
8278
8341
  }
8342
+ /**
8343
+ * [WTSYNC-B16] Check if local gitgov-state has commits not present on remote.
8344
+ * Returns { ahead: true } if local has unpushed commits or remote branch doesn't exist.
8345
+ * Also returns { remoteExists } to let caller decide whether reconciliation is needed.
8346
+ */
8347
+ async isLocalAheadOfRemote() {
8348
+ try {
8349
+ await this.execGit(["ls-remote", "--exit-code", "origin", this.stateBranchName]);
8350
+ } catch {
8351
+ return { ahead: true, remoteExists: false };
8352
+ }
8353
+ try {
8354
+ await this.execInWorktree(["fetch", "origin", this.stateBranchName]);
8355
+ } catch {
8356
+ return { ahead: false, remoteExists: true };
8357
+ }
8358
+ try {
8359
+ const count = (await this.execInWorktree([
8360
+ "rev-list",
8361
+ `origin/${this.stateBranchName}..HEAD`,
8362
+ "--count"
8363
+ ])).trim();
8364
+ return { ahead: parseInt(count, 10) > 0, remoteExists: true };
8365
+ } catch {
8366
+ return { ahead: true, remoteExists: true };
8367
+ }
8368
+ }
8279
8369
  /** Calculate file delta (uncommitted changes in worktree) */
8280
8370
  async calculateFileDelta() {
8281
8371
  try {