sync-worktrees 3.3.1 → 3.4.0

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.
@@ -57,7 +57,11 @@ var DEFAULT_CONFIG = {
57
57
  MAX_SAFE_TOTAL_CONCURRENT_OPS: 100
58
58
  },
59
59
  UPDATE_EXISTING_WORKTREES: true,
60
- HOOK_TIMEOUT_MS: 6e4
60
+ HOOK_TIMEOUT_MS: 6e4,
61
+ FETCH_TIMEOUT_MS: 3e5,
62
+ CLONE_TIMEOUT_MS: 9e5,
63
+ LOCK_STALE_MS: 6e5,
64
+ LOCK_UPDATE_MS: 3e4
61
65
  };
62
66
  var ERROR_MESSAGES = {
63
67
  GIT_NOT_INITIALIZED: "Git service not initialized. Call initialize() first.",
@@ -753,6 +757,7 @@ function defaultConsoleOutput(msg, level) {
753
757
  import * as fs6 from "fs/promises";
754
758
  import * as path7 from "path";
755
759
  import pLimit from "p-limit";
760
+ import * as lockfile from "proper-lockfile";
756
761
 
757
762
  // src/utils/date-filter.ts
758
763
  function parseDuration(durationStr) {
@@ -1753,11 +1758,21 @@ var GitService = class {
1753
1758
  getSparseCheckoutService() {
1754
1759
  return this.sparseCheckoutService;
1755
1760
  }
1761
+ getFetchTimeoutMs() {
1762
+ if (process.env.NODE_ENV === ENV_CONSTANTS.NODE_ENV_TEST) return 0;
1763
+ return this.config.fetchTimeoutMs ?? DEFAULT_CONFIG.FETCH_TIMEOUT_MS;
1764
+ }
1765
+ getCloneTimeoutMs() {
1766
+ if (process.env.NODE_ENV === ENV_CONSTANTS.NODE_ENV_TEST) return 0;
1767
+ return this.config.cloneTimeoutMs ?? DEFAULT_CONFIG.CLONE_TIMEOUT_MS;
1768
+ }
1756
1769
  getCachedGit(dirPath, useLfsSkip = false) {
1757
1770
  const key = `${path5.resolve(dirPath)}::${useLfsSkip ? "1" : "0"}`;
1758
1771
  let git = this.gitInstances.get(key);
1759
1772
  if (!git) {
1760
- git = useLfsSkip ? simpleGit4(dirPath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit4(dirPath);
1773
+ const block = this.getFetchTimeoutMs();
1774
+ const base = block > 0 ? simpleGit4(dirPath, { timeout: { block } }) : simpleGit4(dirPath);
1775
+ git = useLfsSkip ? base.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : base;
1761
1776
  this.gitInstances.set(key, git);
1762
1777
  }
1763
1778
  return git;
@@ -1773,7 +1788,9 @@ var GitService = class {
1773
1788
  } catch {
1774
1789
  this.logger.info(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
1775
1790
  await fs4.mkdir(path5.dirname(this.bareRepoPath), { recursive: true });
1776
- const cloneGit = this.isLfsSkipEnabled() ? simpleGit4().env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit4();
1791
+ const cloneBlock = this.getCloneTimeoutMs();
1792
+ const cloneBase = cloneBlock > 0 ? simpleGit4({ timeout: { block: cloneBlock } }) : simpleGit4();
1793
+ const cloneGit = this.isLfsSkipEnabled() ? cloneBase.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : cloneBase;
1777
1794
  await cloneGit.clone(repoUrl, this.bareRepoPath, ["--bare"]);
1778
1795
  this.logger.info("\u2705 Clone successful.");
1779
1796
  }
@@ -1859,6 +1876,9 @@ var GitService = class {
1859
1876
  getDefaultBranch() {
1860
1877
  return this.defaultBranch;
1861
1878
  }
1879
+ getBareRepoPath() {
1880
+ return this.bareRepoPath;
1881
+ }
1862
1882
  async fetchAll() {
1863
1883
  this.assertInitialized();
1864
1884
  this.logger.info("Fetching latest data from remote...");
@@ -2568,6 +2588,11 @@ var WorktreeSyncService = class {
2568
2588
  this.logger.warn("\u26A0\uFE0F Sync already in progress, skipping...");
2569
2589
  return { started: false, reason: "in_progress" };
2570
2590
  }
2591
+ const release = await this.acquireBareLock();
2592
+ if (release === null) {
2593
+ this.logger.warn("\u26A0\uFE0F Another process holds the sync lock for this repo, skipping...");
2594
+ return { started: false, reason: "locked" };
2595
+ }
2571
2596
  this.syncInProgress = true;
2572
2597
  this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Starting worktree synchronization...`);
2573
2598
  const totalTimer = new Timer();
@@ -2584,6 +2609,11 @@ var WorktreeSyncService = class {
2584
2609
  this.gitService.setLfsSkipEnabled(false);
2585
2610
  }
2586
2611
  this.syncInProgress = false;
2612
+ try {
2613
+ await release();
2614
+ } catch (releaseError) {
2615
+ this.logger.warn(`Failed to release sync lock: ${getErrorMessage(releaseError)}`);
2616
+ }
2587
2617
  this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Synchronization finished.
2588
2618
  `);
2589
2619
  if (this.config.debug) {
@@ -2595,6 +2625,39 @@ var WorktreeSyncService = class {
2595
2625
  }
2596
2626
  return { started: true };
2597
2627
  }
2628
+ async acquireBareLock() {
2629
+ if (process.env.NODE_ENV === ENV_CONSTANTS.NODE_ENV_TEST) {
2630
+ return async () => {
2631
+ };
2632
+ }
2633
+ if (typeof this.gitService.getBareRepoPath !== "function") {
2634
+ return async () => {
2635
+ };
2636
+ }
2637
+ const barePath = this.gitService.getBareRepoPath();
2638
+ const lockTarget = path7.join(barePath, "HEAD");
2639
+ try {
2640
+ await fs6.access(lockTarget);
2641
+ } catch {
2642
+ return async () => {
2643
+ };
2644
+ }
2645
+ try {
2646
+ const release = await lockfile.lock(lockTarget, {
2647
+ stale: DEFAULT_CONFIG.LOCK_STALE_MS,
2648
+ update: DEFAULT_CONFIG.LOCK_UPDATE_MS,
2649
+ retries: 0,
2650
+ realpath: false
2651
+ });
2652
+ return release;
2653
+ } catch (error) {
2654
+ const code = error.code;
2655
+ if (code === "ELOCKED") {
2656
+ return null;
2657
+ }
2658
+ throw error;
2659
+ }
2660
+ }
2598
2661
  createRetryOptions(syncContext) {
2599
2662
  return {
2600
2663
  maxAttempts: this.config.retry?.maxAttempts ?? 3,