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.
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) {
@@ -2993,6 +2998,13 @@ var WorktreeStatusService = class {
2993
2998
  };
2994
2999
 
2995
3000
  // src/services/git.service.ts
3001
+ function sanitizeGitEnv(env) {
3002
+ const sanitized = { ...env };
3003
+ delete sanitized.EDITOR;
3004
+ delete sanitized.GIT_EDITOR;
3005
+ delete sanitized.GIT_SEQUENCE_EDITOR;
3006
+ return sanitized;
3007
+ }
2996
3008
  var GitService = class {
2997
3009
  constructor(config, logger) {
2998
3010
  this.config = config;
@@ -3017,11 +3029,21 @@ var GitService = class {
3017
3029
  getSparseCheckoutService() {
3018
3030
  return this.sparseCheckoutService;
3019
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
+ }
3020
3040
  getCachedGit(dirPath, useLfsSkip = false) {
3021
3041
  const key = `${path5.resolve(dirPath)}::${useLfsSkip ? "1" : "0"}`;
3022
3042
  let git = this.gitInstances.get(key);
3023
3043
  if (!git) {
3024
- 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;
3025
3047
  this.gitInstances.set(key, git);
3026
3048
  }
3027
3049
  return git;
@@ -3037,7 +3059,9 @@ var GitService = class {
3037
3059
  } catch {
3038
3060
  this.logger.info(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
3039
3061
  await fs4.mkdir(path5.dirname(this.bareRepoPath), { recursive: true });
3040
- 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;
3041
3065
  await cloneGit.clone(repoUrl, this.bareRepoPath, ["--bare"]);
3042
3066
  this.logger.info("\u2705 Clone successful.");
3043
3067
  }
@@ -3123,6 +3147,9 @@ var GitService = class {
3123
3147
  getDefaultBranch() {
3124
3148
  return this.defaultBranch;
3125
3149
  }
3150
+ getBareRepoPath() {
3151
+ return this.bareRepoPath;
3152
+ }
3126
3153
  async fetchAll() {
3127
3154
  this.assertInitialized();
3128
3155
  this.logger.info("Fetching latest data from remote...");
@@ -3169,7 +3196,7 @@ var GitService = class {
3169
3196
  return branches;
3170
3197
  }
3171
3198
  async verifyLfsFilesDownloaded(worktreePath, branchName) {
3172
- const worktreeGit = this.config.sparseCheckout ? simpleGit4(worktreePath).env({ ...process.env, [ENV_CONSTANTS.GIT_ATTR_SOURCE]: "HEAD" }) : this.getCachedGit(worktreePath);
3199
+ const worktreeGit = this.config.sparseCheckout ? simpleGit4(worktreePath).env({ ...sanitizeGitEnv(process.env), [ENV_CONSTANTS.GIT_ATTR_SOURCE]: "HEAD" }) : this.getCachedGit(worktreePath);
3173
3200
  try {
3174
3201
  const lfsFiles = await worktreeGit.raw(["lfs", "ls-files", "--name-only"]);
3175
3202
  let lfsFileList = lfsFiles.trim().split("\n").filter((f) => f.length > 0);
@@ -3832,6 +3859,11 @@ var WorktreeSyncService = class {
3832
3859
  this.logger.warn("\u26A0\uFE0F Sync already in progress, skipping...");
3833
3860
  return { started: false, reason: "in_progress" };
3834
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
+ }
3835
3867
  this.syncInProgress = true;
3836
3868
  this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Starting worktree synchronization...`);
3837
3869
  const totalTimer = new Timer();
@@ -3848,6 +3880,11 @@ var WorktreeSyncService = class {
3848
3880
  this.gitService.setLfsSkipEnabled(false);
3849
3881
  }
3850
3882
  this.syncInProgress = false;
3883
+ try {
3884
+ await release();
3885
+ } catch (releaseError) {
3886
+ this.logger.warn(`Failed to release sync lock: ${getErrorMessage(releaseError)}`);
3887
+ }
3851
3888
  this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Synchronization finished.
3852
3889
  `);
3853
3890
  if (this.config.debug) {
@@ -3859,6 +3896,39 @@ var WorktreeSyncService = class {
3859
3896
  }
3860
3897
  return { started: true };
3861
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
+ }
3862
3932
  createRetryOptions(syncContext) {
3863
3933
  return {
3864
3934
  maxAttempts: this.config.retry?.maxAttempts ?? 3,