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.
package/dist/index.js CHANGED
@@ -55,7 +55,11 @@ var DEFAULT_CONFIG = {
55
55
  MAX_SAFE_TOTAL_CONCURRENT_OPS: 100
56
56
  },
57
57
  UPDATE_EXISTING_WORKTREES: true,
58
- HOOK_TIMEOUT_MS: 6e4
58
+ HOOK_TIMEOUT_MS: 6e4,
59
+ FETCH_TIMEOUT_MS: 3e5,
60
+ CLONE_TIMEOUT_MS: 9e5,
61
+ LOCK_STALE_MS: 6e5,
62
+ LOCK_UPDATE_MS: 3e4
59
63
  };
60
64
  var ERROR_MESSAGES = {
61
65
  GIT_NOT_INITIALIZED: "Git service not initialized. Call initialize() first.",
@@ -1947,6 +1951,7 @@ var App_default = App;
1947
1951
  import * as fs6 from "fs/promises";
1948
1952
  import * as path7 from "path";
1949
1953
  import pLimit from "p-limit";
1954
+ import * as lockfile from "proper-lockfile";
1950
1955
 
1951
1956
  // src/utils/date-filter.ts
1952
1957
  function parseDuration(durationStr) {
@@ -3024,11 +3029,21 @@ var GitService = class {
3024
3029
  getSparseCheckoutService() {
3025
3030
  return this.sparseCheckoutService;
3026
3031
  }
3032
+ getFetchTimeoutMs() {
3033
+ if (process.env.NODE_ENV === ENV_CONSTANTS.NODE_ENV_TEST) return 0;
3034
+ return this.config.fetchTimeoutMs ?? DEFAULT_CONFIG.FETCH_TIMEOUT_MS;
3035
+ }
3036
+ getCloneTimeoutMs() {
3037
+ if (process.env.NODE_ENV === ENV_CONSTANTS.NODE_ENV_TEST) return 0;
3038
+ return this.config.cloneTimeoutMs ?? DEFAULT_CONFIG.CLONE_TIMEOUT_MS;
3039
+ }
3027
3040
  getCachedGit(dirPath, useLfsSkip = false) {
3028
3041
  const key = `${path5.resolve(dirPath)}::${useLfsSkip ? "1" : "0"}`;
3029
3042
  let git = this.gitInstances.get(key);
3030
3043
  if (!git) {
3031
- git = useLfsSkip ? simpleGit4(dirPath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit4(dirPath);
3044
+ const block = this.getFetchTimeoutMs();
3045
+ const base = block > 0 ? simpleGit4(dirPath, { timeout: { block } }) : simpleGit4(dirPath);
3046
+ git = useLfsSkip ? base.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : base;
3032
3047
  this.gitInstances.set(key, git);
3033
3048
  }
3034
3049
  return git;
@@ -3044,7 +3059,9 @@ var GitService = class {
3044
3059
  } catch {
3045
3060
  this.logger.info(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
3046
3061
  await fs4.mkdir(path5.dirname(this.bareRepoPath), { recursive: true });
3047
- const cloneGit = this.isLfsSkipEnabled() ? simpleGit4().env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit4();
3062
+ const cloneBlock = this.getCloneTimeoutMs();
3063
+ const cloneBase = cloneBlock > 0 ? simpleGit4({ timeout: { block: cloneBlock } }) : simpleGit4();
3064
+ const cloneGit = this.isLfsSkipEnabled() ? cloneBase.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : cloneBase;
3048
3065
  await cloneGit.clone(repoUrl, this.bareRepoPath, ["--bare"]);
3049
3066
  this.logger.info("\u2705 Clone successful.");
3050
3067
  }
@@ -3130,6 +3147,9 @@ var GitService = class {
3130
3147
  getDefaultBranch() {
3131
3148
  return this.defaultBranch;
3132
3149
  }
3150
+ getBareRepoPath() {
3151
+ return this.bareRepoPath;
3152
+ }
3133
3153
  async fetchAll() {
3134
3154
  this.assertInitialized();
3135
3155
  this.logger.info("Fetching latest data from remote...");
@@ -3839,6 +3859,11 @@ var WorktreeSyncService = class {
3839
3859
  this.logger.warn("\u26A0\uFE0F Sync already in progress, skipping...");
3840
3860
  return { started: false, reason: "in_progress" };
3841
3861
  }
3862
+ const release = await this.acquireBareLock();
3863
+ if (release === null) {
3864
+ this.logger.warn("\u26A0\uFE0F Another process holds the sync lock for this repo, skipping...");
3865
+ return { started: false, reason: "locked" };
3866
+ }
3842
3867
  this.syncInProgress = true;
3843
3868
  this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Starting worktree synchronization...`);
3844
3869
  const totalTimer = new Timer();
@@ -3855,6 +3880,11 @@ var WorktreeSyncService = class {
3855
3880
  this.gitService.setLfsSkipEnabled(false);
3856
3881
  }
3857
3882
  this.syncInProgress = false;
3883
+ try {
3884
+ await release();
3885
+ } catch (releaseError) {
3886
+ this.logger.warn(`Failed to release sync lock: ${getErrorMessage(releaseError)}`);
3887
+ }
3858
3888
  this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Synchronization finished.
3859
3889
  `);
3860
3890
  if (this.config.debug) {
@@ -3866,6 +3896,39 @@ var WorktreeSyncService = class {
3866
3896
  }
3867
3897
  return { started: true };
3868
3898
  }
3899
+ async acquireBareLock() {
3900
+ if (process.env.NODE_ENV === ENV_CONSTANTS.NODE_ENV_TEST) {
3901
+ return async () => {
3902
+ };
3903
+ }
3904
+ if (typeof this.gitService.getBareRepoPath !== "function") {
3905
+ return async () => {
3906
+ };
3907
+ }
3908
+ const barePath = this.gitService.getBareRepoPath();
3909
+ const lockTarget = path7.join(barePath, "HEAD");
3910
+ try {
3911
+ await fs6.access(lockTarget);
3912
+ } catch {
3913
+ return async () => {
3914
+ };
3915
+ }
3916
+ try {
3917
+ const release = await lockfile.lock(lockTarget, {
3918
+ stale: DEFAULT_CONFIG.LOCK_STALE_MS,
3919
+ update: DEFAULT_CONFIG.LOCK_UPDATE_MS,
3920
+ retries: 0,
3921
+ realpath: false
3922
+ });
3923
+ return release;
3924
+ } catch (error) {
3925
+ const code = error.code;
3926
+ if (code === "ELOCKED") {
3927
+ return null;
3928
+ }
3929
+ throw error;
3930
+ }
3931
+ }
3869
3932
  createRetryOptions(syncContext) {
3870
3933
  return {
3871
3934
  maxAttempts: this.config.retry?.maxAttempts ?? 3,