sync-worktrees 3.3.0 → 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) {
@@ -1722,6 +1727,13 @@ var WorktreeStatusService = class {
1722
1727
  };
1723
1728
 
1724
1729
  // src/services/git.service.ts
1730
+ function sanitizeGitEnv(env) {
1731
+ const sanitized = { ...env };
1732
+ delete sanitized.EDITOR;
1733
+ delete sanitized.GIT_EDITOR;
1734
+ delete sanitized.GIT_SEQUENCE_EDITOR;
1735
+ return sanitized;
1736
+ }
1725
1737
  var GitService = class {
1726
1738
  constructor(config, logger) {
1727
1739
  this.config = config;
@@ -1746,11 +1758,21 @@ var GitService = class {
1746
1758
  getSparseCheckoutService() {
1747
1759
  return this.sparseCheckoutService;
1748
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
+ }
1749
1769
  getCachedGit(dirPath, useLfsSkip = false) {
1750
1770
  const key = `${path5.resolve(dirPath)}::${useLfsSkip ? "1" : "0"}`;
1751
1771
  let git = this.gitInstances.get(key);
1752
1772
  if (!git) {
1753
- 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;
1754
1776
  this.gitInstances.set(key, git);
1755
1777
  }
1756
1778
  return git;
@@ -1766,7 +1788,9 @@ var GitService = class {
1766
1788
  } catch {
1767
1789
  this.logger.info(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
1768
1790
  await fs4.mkdir(path5.dirname(this.bareRepoPath), { recursive: true });
1769
- 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;
1770
1794
  await cloneGit.clone(repoUrl, this.bareRepoPath, ["--bare"]);
1771
1795
  this.logger.info("\u2705 Clone successful.");
1772
1796
  }
@@ -1852,6 +1876,9 @@ var GitService = class {
1852
1876
  getDefaultBranch() {
1853
1877
  return this.defaultBranch;
1854
1878
  }
1879
+ getBareRepoPath() {
1880
+ return this.bareRepoPath;
1881
+ }
1855
1882
  async fetchAll() {
1856
1883
  this.assertInitialized();
1857
1884
  this.logger.info("Fetching latest data from remote...");
@@ -1898,7 +1925,7 @@ var GitService = class {
1898
1925
  return branches;
1899
1926
  }
1900
1927
  async verifyLfsFilesDownloaded(worktreePath, branchName) {
1901
- const worktreeGit = this.config.sparseCheckout ? simpleGit4(worktreePath).env({ ...process.env, [ENV_CONSTANTS.GIT_ATTR_SOURCE]: "HEAD" }) : this.getCachedGit(worktreePath);
1928
+ const worktreeGit = this.config.sparseCheckout ? simpleGit4(worktreePath).env({ ...sanitizeGitEnv(process.env), [ENV_CONSTANTS.GIT_ATTR_SOURCE]: "HEAD" }) : this.getCachedGit(worktreePath);
1902
1929
  try {
1903
1930
  const lfsFiles = await worktreeGit.raw(["lfs", "ls-files", "--name-only"]);
1904
1931
  let lfsFileList = lfsFiles.trim().split("\n").filter((f) => f.length > 0);
@@ -2561,6 +2588,11 @@ var WorktreeSyncService = class {
2561
2588
  this.logger.warn("\u26A0\uFE0F Sync already in progress, skipping...");
2562
2589
  return { started: false, reason: "in_progress" };
2563
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
+ }
2564
2596
  this.syncInProgress = true;
2565
2597
  this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Starting worktree synchronization...`);
2566
2598
  const totalTimer = new Timer();
@@ -2577,6 +2609,11 @@ var WorktreeSyncService = class {
2577
2609
  this.gitService.setLfsSkipEnabled(false);
2578
2610
  }
2579
2611
  this.syncInProgress = false;
2612
+ try {
2613
+ await release();
2614
+ } catch (releaseError) {
2615
+ this.logger.warn(`Failed to release sync lock: ${getErrorMessage(releaseError)}`);
2616
+ }
2580
2617
  this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Synchronization finished.
2581
2618
  `);
2582
2619
  if (this.config.debug) {
@@ -2588,6 +2625,39 @@ var WorktreeSyncService = class {
2588
2625
  }
2589
2626
  return { started: true };
2590
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
+ }
2591
2661
  createRetryOptions(syncContext) {
2592
2662
  return {
2593
2663
  maxAttempts: this.config.retry?.maxAttempts ?? 3,