@gitkraken/core-gitlens 0.10.6 → 0.11.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.
Files changed (106) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/dist/git/cache.d.ts +34 -2
  3. package/dist/git/cache.js +53 -7
  4. package/dist/git/cache.js.map +1 -1
  5. package/dist/git/context.d.ts +1 -1
  6. package/dist/git/models/repository.d.ts +3 -2
  7. package/dist/git/models/repository.js +5 -4
  8. package/dist/git/models/repository.js.map +1 -1
  9. package/dist/git/providers/branches.d.ts +22 -0
  10. package/dist/git/providers/config.d.ts +3 -1
  11. package/dist/git/providers/diff.d.ts +1 -0
  12. package/dist/git/providers/provider.d.ts +13 -1
  13. package/dist/git/providers/staging.d.ts +1 -1
  14. package/dist/git/service.d.ts +17 -1
  15. package/dist/git/service.js +59 -8
  16. package/dist/git/service.js.map +1 -1
  17. package/dist/git/watching/changeEvent.d.ts +48 -5
  18. package/dist/git/watching/changeEvent.js +82 -7
  19. package/dist/git/watching/changeEvent.js.map +1 -1
  20. package/dist/git/watching/classifyChange.d.ts +13 -2
  21. package/dist/git/watching/classifyChange.js +40 -5
  22. package/dist/git/watching/classifyChange.js.map +1 -1
  23. package/dist/git/watching/gitIgnoreFilter.d.ts +57 -10
  24. package/dist/git/watching/gitIgnoreFilter.js +186 -29
  25. package/dist/git/watching/gitIgnoreFilter.js.map +1 -1
  26. package/dist/git/watching/initWatcher.d.ts +5 -1
  27. package/dist/git/watching/initWatcher.js +1 -1
  28. package/dist/git/watching/initWatcher.js.map +1 -1
  29. package/dist/git/watching/provider.d.ts +33 -8
  30. package/dist/git/watching/watchGroup.d.ts +13 -2
  31. package/dist/git/watching/watchGroup.js +60 -14
  32. package/dist/git/watching/watchGroup.js.map +1 -1
  33. package/dist/git/watching/watchService.d.ts +42 -1
  34. package/dist/git/watching/watchService.js +172 -20
  35. package/dist/git/watching/watchService.js.map +1 -1
  36. package/dist/git/watching/watchSession.d.ts +50 -17
  37. package/dist/git/watching/watchSession.js +274 -83
  38. package/dist/git/watching/watchSession.js.map +1 -1
  39. package/dist/git/watching/watcherPatterns.d.ts +13 -2
  40. package/dist/git/watching/watcherPatterns.js +22 -2
  41. package/dist/git/watching/watcherPatterns.js.map +1 -1
  42. package/dist/git/watching/worktreeSummary.d.ts +121 -0
  43. package/dist/git/watching/worktreeSummary.js +427 -0
  44. package/dist/git/watching/worktreeSummary.js.map +1 -0
  45. package/dist/git-cli/cliGitProvider.d.ts +6 -1
  46. package/dist/git-cli/cliGitProvider.js +87 -2
  47. package/dist/git-cli/cliGitProvider.js.map +1 -1
  48. package/dist/git-cli/exec/git.js +1 -1
  49. package/dist/git-cli/providers/blame.js +1 -1
  50. package/dist/git-cli/providers/branches.d.ts +32 -1
  51. package/dist/git-cli/providers/branches.js +195 -0
  52. package/dist/git-cli/providers/branches.js.map +1 -1
  53. package/dist/git-cli/providers/commits.js +1 -1
  54. package/dist/git-cli/providers/config.d.ts +3 -0
  55. package/dist/git-cli/providers/config.js +14 -8
  56. package/dist/git-cli/providers/config.js.map +1 -1
  57. package/dist/git-cli/providers/contributors.js +1 -1
  58. package/dist/git-cli/providers/diff.d.ts +3 -0
  59. package/dist/git-cli/providers/diff.js +3 -2
  60. package/dist/git-cli/providers/diff.js.map +1 -1
  61. package/dist/git-cli/providers/graph.js +1 -1
  62. package/dist/git-cli/providers/maintenance.js +1 -1
  63. package/dist/git-cli/providers/pausedOperations.js +2 -2
  64. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  65. package/dist/git-cli/providers/refs.js +1 -1
  66. package/dist/git-cli/providers/staging.js +2 -2
  67. package/dist/git-cli/providers/status.js +1 -1
  68. package/dist/git-cli/service.js +1 -0
  69. package/dist/git-cli/service.js.map +1 -1
  70. package/dist/ipc/discovery.js +1 -1
  71. package/dist/ipc/ipcServer.js +1 -1
  72. package/dist/plus/agents/providers/claudeCodeTranscript.js +6 -6
  73. package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
  74. package/dist/plus/agents/providers/gkAgentProvider.js +2 -2
  75. package/dist/plus/ai/providers/openAICompatibleProviderBase.js +1 -1
  76. package/dist/plus/git-github/api/github.d.ts +1 -1
  77. package/dist/plus/git-github/api/github.js +2 -1
  78. package/dist/plus/git-github/api/github.js.map +1 -1
  79. package/dist/plus/git-github/providers/github/blame.js +1 -1
  80. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +1 -1
  81. package/dist/plus/integrations/context.d.ts +1 -1
  82. package/dist/plus/integrations/integrationService.js +1 -1
  83. package/dist/plus/integrations/models/integration.d.ts +1 -1
  84. package/dist/plus/integrations/models/integration.js +2 -2
  85. package/dist/plus/integrations/models/integration.js.map +1 -1
  86. package/dist/plus/integrations/providers/azure/azure.js +1 -1
  87. package/dist/plus/integrations/providers/bitbucket-server.d.ts +1 -1
  88. package/dist/plus/integrations/providers/jira.d.ts +2 -0
  89. package/dist/plus/integrations/providers/jira.js +5 -0
  90. package/dist/plus/integrations/providers/jira.js.map +1 -1
  91. package/dist/plus/integrations/providers/linear.d.ts +7 -0
  92. package/dist/plus/integrations/providers/linear.js +19 -0
  93. package/dist/plus/integrations/providers/linear.js.map +1 -1
  94. package/dist/plus/integrations/providers/trello.d.ts +8 -0
  95. package/dist/plus/integrations/providers/trello.js +22 -0
  96. package/dist/plus/integrations/providers/trello.js.map +1 -1
  97. package/dist/plus/integrations/reads/currentAccount.d.ts +1 -1
  98. package/dist/plus/integrations/reads/currentAccount.js +1 -1
  99. package/dist/plus/integrations/reads/currentAccount.js.map +1 -1
  100. package/dist/plus/integrations/reads/issueTracker.js +1 -0
  101. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  102. package/dist/utils/env/node/exec.js +2 -2
  103. package/dist/utils/event.d.ts +1 -1
  104. package/docs/integrations.md +6 -6
  105. package/docs/kepler-read-api-parity.md +2 -2
  106. package/package.json +1 -1
@@ -30,6 +30,13 @@ export interface WatchSessionOptions {
30
30
  readonly defaultRepoDelayMs?: number;
31
31
  /** Default debounce for working tree changes. Default: 2500ms */
32
32
  readonly defaultWorkingTreeDelayMs?: number;
33
+ /**
34
+ * Default cap on how long repo changes can be held back by continuous activity — a flush happens at most this long
35
+ * after the first pending change, however long the burst lasts. `Infinity` disables the cap. Default: 2000ms
36
+ */
37
+ readonly defaultRepoMaxWaitMs?: number;
38
+ /** Default cap on how long working tree changes can be held back by continuous activity. `Infinity` disables the cap. Default: 10000ms */
39
+ readonly defaultWorkingTreeMaxWaitMs?: number;
33
40
  readonly lifecycle?: WatchSessionLifecycle;
34
41
  /** Initial suspended state — new sessions inherit the service-global suspend state. Default: false */
35
42
  readonly suspended?: boolean;
@@ -51,25 +58,33 @@ export interface WatchSessionOptions {
51
58
  * pipeline for both repo (.git) and working-tree changes.
52
59
  *
53
60
  * Multiple subscribers share the same pipeline — the shortest
54
- * requested debounce delay wins.
61
+ * requested debounce delay wins, and so does the shortest max-wait.
55
62
  */
56
63
  export declare class RepositoryWatchSession implements UnifiedDisposable {
57
64
  readonly repoPath: string;
58
65
  private readonly repoEmitter;
59
- private readonly repoDelays;
66
+ private readonly repoSubscribers;
60
67
  private repoNextId;
61
- private pendingRepoEvent;
62
- private repoTimer;
68
+ private readonly pendingRepoChanges;
69
+ /** Full ref names, ref directories and pseudo-refs whose files changed, flushed with {@link pendingRepoChanges} */
70
+ private readonly pendingRepoRefs;
71
+ /** Ref kinds that changed without a name, flushed with {@link pendingRepoChanges} */
72
+ private readonly pendingRepoUnnamedRefKinds;
73
+ private readonly repoClock;
63
74
  private effectiveRepoMs;
75
+ private effectiveRepoMaxWaitMs;
64
76
  private readonly defaultRepoMs;
77
+ private readonly defaultRepoMaxWaitMs;
65
78
  private readonly workingTreeEmitter;
66
- private readonly workingTreeDelays;
79
+ private readonly workingTreeSubscribers;
67
80
  private workingTreeNextId;
68
81
  private pendingWorkingTreePaths;
69
82
  private pendingWorkingTreeIncomplete;
70
- private workingTreeTimer;
83
+ private readonly workingTreeClock;
71
84
  private effectiveWorkingTreeMs;
85
+ private effectiveWorkingTreeMaxWaitMs;
72
86
  private readonly defaultWorkingTreeMs;
87
+ private readonly defaultWorkingTreeMaxWaitMs;
73
88
  private _etagWorkingTree;
74
89
  private _suspended;
75
90
  private _disposed;
@@ -100,36 +115,54 @@ export declare class RepositoryWatchSession implements UnifiedDisposable {
100
115
  * fires `onLastRepoSubscriber`.
101
116
  *
102
117
  * When multiple subscribers exist, the shortest debounce wins —
103
- * all subscribers fire on the same (fastest) cadence.
118
+ * all subscribers fire on the same (fastest) cadence. `maxWaitMs` bounds how long continuous
119
+ * activity can defer a flush (the shortest wins too); `Infinity` means no cap.
120
+ *
121
+ * `relevantRefs` narrows THIS subscription's events to the refs it cares about, given as full ref names
122
+ * (`refs/heads/main`, `refs/remotes/origin/main`, `refs/tags/v1`, `refs/stash`). It is read at delivery, so it can
123
+ * return the subscriber's current list. A `heads`/`remotes`/`tags`/`stash` change is delivered only when a listed
124
+ * ref of that kind may have moved, when the change isn't attributable to a name (e.g. a `packed-refs` rewrite),
125
+ * or, for `heads`, when a pseudo-ref (`HEAD`, `ORIG_HEAD`) moved — so `stash` and `tags` changes are dropped unless
126
+ * a ref of that kind is listed. Other change kinds always pass, and an event left with no changes isn't delivered.
127
+ * The filter never affects other subscribers, the debounce, or the global `onDidFireRepoChange` forwarding.
104
128
  */
105
129
  subscribe(opts?: {
106
130
  delayMs?: number;
131
+ maxWaitMs?: number;
132
+ relevantRefs?: () => Iterable<string>;
107
133
  }): RepositorySubscription;
134
+ /** Subscribe to working tree changes; `delayMs` and `maxWaitMs` behave as in {@link subscribe}. */
108
135
  subscribeToWorkingTree(opts?: {
109
136
  delayMs?: number;
137
+ maxWaitMs?: number;
110
138
  }): WorkingTreeSubscription;
111
139
  suspend(): void;
112
140
  resume(delayMs?: number): void;
113
- /** Inject changes (e.g., after a git operation the caller performed). Goes through debounce + suspend. */
141
+ /**
142
+ * Inject changes (e.g., after a git operation the caller performed). Goes through debounce + suspend. Ref kinds
143
+ * among them are unnamed — any ref of that kind may have moved.
144
+ */
114
145
  fireChange(...changes: RepositoryChange[]): void;
115
- /** Immediate fire, bypassing debounce + suspend (for Closed/Opened). */
146
+ /** Immediate fire, bypassing debounce + suspend (for Closed/Opened). Ref kinds among them are unnamed. */
116
147
  fireChangeImmediate(...changes: RepositoryChange[]): void;
117
- /** Push interpreted repo changes into the pipeline */
118
- pushRepoChanges(changes: RepositoryChange[]): void;
148
+ /**
149
+ * Push interpreted repo changes into the pipeline. `ref` is the full ref name (or ref directory, or pseudo-ref) whose
150
+ * file changed; a ref kind in `changes` is attributed to it when the kinds match, and is otherwise unnamed.
151
+ */
152
+ pushRepoChanges(changes: RepositoryChange[], ref?: string): void;
119
153
  /** Push working tree file changes into the pipeline */
120
154
  /** `incomplete` marks the batch as partial when the caller already dropped paths of its own. */
121
155
  pushWorkingTreeChanges(paths: Iterable<string>, incomplete?: boolean): void;
122
156
  private static minDelay;
123
- private recalcRepoDelay;
124
- private recalcWorkingTreeDelay;
157
+ /** Unlike the delay, the default isn't a ceiling — a subscriber can opt out of the cap with `Infinity` */
158
+ private static minMaxWait;
159
+ private recalcRepoTiming;
160
+ private recalcWorkingTreeTiming;
125
161
  private flushRepo;
126
- private scheduleRepoFlush;
127
162
  private coalesceRepo;
163
+ private clearPendingRepoChanges;
128
164
  private flushWorkingTree;
129
- private scheduleWorkingTreeFlush;
130
165
  private coalesceWorkingTrees;
131
- private cancelRepoTimer;
132
- private cancelWorkingTreeTimer;
133
166
  }
134
167
  //#endregion
135
168
  //# sourceMappingURL=watchSession.d.ts.map
@@ -1,9 +1,113 @@
1
1
  import { createDisposable } from "../../utils/disposable.js";
2
2
  import { Emitter } from "../../utils/event.js";
3
- import { WatcherRepoChangeEvent } from "./changeEvent.js";
3
+ import { WatcherRepoChangeEvent, findCoveringRefName, getRefChangeKind, isRefChangeKind } from "./changeEvent.js";
4
4
  //#region ../git/src/watching/watchSession.ts
5
5
  const defaultRepoDelay = 250;
6
6
  const defaultWorkingTreeDelay = 2500;
7
+ const defaultRepoMaxWait = 2e3;
8
+ const defaultWorkingTreeMaxWait = 1e4;
9
+ /** Largest delay `setTimeout` honors (a signed 32-bit int); anything longer fires immediately */
10
+ const maxTimerDelay = 2147483647;
11
+ /**
12
+ * Trailing-deadline debounce with a max-wait, shared by the repo and working-tree pipelines.
13
+ *
14
+ * Recording an event costs a `performance.now()` and, only when no timer is armed, one `setTimeout` — never a
15
+ * `clearTimeout`/`setTimeout` pair per event, which is what made a busy repo allocate (and starve) on every raw
16
+ * watcher event. The armed timer is a deadline, not a countdown: when it fires it recomputes the due time from the
17
+ * latest timestamps and either flushes or re-arms for the remainder. The due time is
18
+ * `min(lastEventAt + delay, firstPendingAt + maxWait)`, so continuous activity is flushed at the max-wait instead of
19
+ * never. Monotonic time, so a wall-clock change can't push a deadline out.
20
+ */
21
+ var DebounceClock = class {
22
+ getDelayMs;
23
+ getMaxWaitMs;
24
+ onDue;
25
+ active = false;
26
+ paused = false;
27
+ firstAt = 0;
28
+ lastAt = 0;
29
+ timer;
30
+ timerDueAt = 0;
31
+ /** Absolute flush time set by {@link resume}; dropped by the next event, which returns to normal debounce */
32
+ resumeDueAt;
33
+ constructor(getDelayMs, getMaxWaitMs, onDue) {
34
+ this.getDelayMs = getDelayMs;
35
+ this.getMaxWaitMs = getMaxWaitMs;
36
+ this.onDue = onDue;
37
+ }
38
+ /** Records an event; arms the timer only if none is armed. Cheap enough to call per raw event. */
39
+ note() {
40
+ const now = performance.now();
41
+ if (!this.active) {
42
+ this.active = true;
43
+ this.firstAt = now;
44
+ }
45
+ this.lastAt = now;
46
+ if (this.paused) return;
47
+ if (this.resumeDueAt != null) {
48
+ this.resumeDueAt = void 0;
49
+ this.rearmIfEarlier();
50
+ }
51
+ if (this.timer == null) this.arm(this.getDueAt(), now);
52
+ }
53
+ /** Moves the armed timer earlier when the due time shrank (a subscriber joined with a shorter delay or max-wait) */
54
+ rearmIfEarlier() {
55
+ if (!this.active || this.paused || this.timer == null) return;
56
+ const dueAt = this.getDueAt();
57
+ if (dueAt >= this.timerDueAt) return;
58
+ clearTimeout(this.timer);
59
+ this.arm(dueAt, performance.now());
60
+ }
61
+ /** Cancels the timer; events keep accumulating but nothing is due until {@link resume} */
62
+ suspend() {
63
+ this.paused = true;
64
+ this.clearTimer();
65
+ }
66
+ /**
67
+ * Restarts the max-wait window from now and schedules the flush for `delayMs` from now (the effective delay when
68
+ * omitted). Does nothing if no events are pending.
69
+ */
70
+ resume(delayMs) {
71
+ this.paused = false;
72
+ if (!this.active) return;
73
+ const now = performance.now();
74
+ this.firstAt = now;
75
+ this.resumeDueAt = now + (delayMs ?? this.getDelayMs());
76
+ this.clearTimer();
77
+ this.arm(this.resumeDueAt, now);
78
+ }
79
+ /** Drops the pending window and the timer */
80
+ cancel() {
81
+ this.active = false;
82
+ this.resumeDueAt = void 0;
83
+ this.clearTimer();
84
+ }
85
+ getDueAt() {
86
+ return this.resumeDueAt ?? Math.min(this.lastAt + this.getDelayMs(), this.firstAt + this.getMaxWaitMs());
87
+ }
88
+ arm(dueAt, now) {
89
+ this.timerDueAt = dueAt;
90
+ this.timer = setTimeout(() => this.onTimer(), Math.min(Math.max(0, dueAt - now), maxTimerDelay));
91
+ }
92
+ onTimer() {
93
+ this.timer = void 0;
94
+ if (!this.active || this.paused) return;
95
+ const now = performance.now();
96
+ const dueAt = this.getDueAt();
97
+ if (dueAt > now) {
98
+ this.arm(dueAt, now);
99
+ return;
100
+ }
101
+ this.cancel();
102
+ this.onDue();
103
+ }
104
+ clearTimer() {
105
+ if (this.timer != null) {
106
+ clearTimeout(this.timer);
107
+ this.timer = void 0;
108
+ }
109
+ }
110
+ };
7
111
  /**
8
112
  * Cap on retained working-tree paths per flush. The set otherwise grows without bound — while the window
9
113
  * is unfocused the session is suspended, so a branch switch or build accumulates every touched path until
@@ -13,29 +117,42 @@ const defaultWorkingTreeDelay = 2500;
13
117
  */
14
118
  const maxWorkingTreePaths = 1e3;
15
119
  /**
120
+ * Cap on retained ref names per flush, for the same reason (a fetch of many refs while suspended). Past it, a ref kind
121
+ * is recorded unnamed — any ref of that kind may have moved — which only makes `mayHaveChangedRef` more conservative.
122
+ */
123
+ const maxPendingRefNames = 1e3;
124
+ /**
16
125
  * Per-repository session that owns the debounce/coalesce/suspend
17
126
  * pipeline for both repo (.git) and working-tree changes.
18
127
  *
19
128
  * Multiple subscribers share the same pipeline — the shortest
20
- * requested debounce delay wins.
129
+ * requested debounce delay wins, and so does the shortest max-wait.
21
130
  */
22
131
  var RepositoryWatchSession = class RepositoryWatchSession {
23
132
  repoPath;
24
133
  repoEmitter = new Emitter();
25
- repoDelays = /* @__PURE__ */ new Map();
134
+ repoSubscribers = /* @__PURE__ */ new Map();
26
135
  repoNextId = 0;
27
- pendingRepoEvent;
28
- repoTimer;
136
+ pendingRepoChanges = /* @__PURE__ */ new Set();
137
+ /** Full ref names, ref directories and pseudo-refs whose files changed, flushed with {@link pendingRepoChanges} */
138
+ pendingRepoRefs = /* @__PURE__ */ new Set();
139
+ /** Ref kinds that changed without a name, flushed with {@link pendingRepoChanges} */
140
+ pendingRepoUnnamedRefKinds = /* @__PURE__ */ new Set();
141
+ repoClock;
29
142
  effectiveRepoMs;
143
+ effectiveRepoMaxWaitMs;
30
144
  defaultRepoMs;
145
+ defaultRepoMaxWaitMs;
31
146
  workingTreeEmitter = new Emitter();
32
- workingTreeDelays = /* @__PURE__ */ new Map();
147
+ workingTreeSubscribers = /* @__PURE__ */ new Map();
33
148
  workingTreeNextId = 0;
34
149
  pendingWorkingTreePaths = /* @__PURE__ */ new Set();
35
150
  pendingWorkingTreeIncomplete = false;
36
- workingTreeTimer;
151
+ workingTreeClock;
37
152
  effectiveWorkingTreeMs;
153
+ effectiveWorkingTreeMaxWaitMs;
38
154
  defaultWorkingTreeMs;
155
+ defaultWorkingTreeMaxWaitMs;
39
156
  _etagWorkingTree = 0;
40
157
  _suspended;
41
158
  _disposed = false;
@@ -46,12 +163,22 @@ var RepositoryWatchSession = class RepositoryWatchSession {
46
163
  this.repoPath = options.repoPath;
47
164
  this.defaultRepoMs = options.defaultRepoDelayMs ?? defaultRepoDelay;
48
165
  this.defaultWorkingTreeMs = options.defaultWorkingTreeDelayMs ?? defaultWorkingTreeDelay;
166
+ this.defaultRepoMaxWaitMs = options.defaultRepoMaxWaitMs ?? defaultRepoMaxWait;
167
+ this.defaultWorkingTreeMaxWaitMs = options.defaultWorkingTreeMaxWaitMs ?? defaultWorkingTreeMaxWait;
49
168
  this.effectiveRepoMs = this.defaultRepoMs;
169
+ this.effectiveRepoMaxWaitMs = this.defaultRepoMaxWaitMs;
50
170
  this.effectiveWorkingTreeMs = this.defaultWorkingTreeMs;
171
+ this.effectiveWorkingTreeMaxWaitMs = this.defaultWorkingTreeMaxWaitMs;
172
+ this.repoClock = new DebounceClock(() => this.effectiveRepoMs, () => this.effectiveRepoMaxWaitMs, () => this.flushRepo());
173
+ this.workingTreeClock = new DebounceClock(() => this.effectiveWorkingTreeMs, () => this.effectiveWorkingTreeMaxWaitMs, () => this.flushWorkingTree());
51
174
  this.lifecycle = options.lifecycle;
52
175
  this.onDidFireRepoChangeCallback = options.onDidFireRepoChange;
53
176
  this.onDidFireWorkingTreeChangeCallback = options.onDidFireWorkingTreeChange;
54
177
  this._suspended = options.suspended ?? false;
178
+ if (this._suspended) {
179
+ this.repoClock.suspend();
180
+ this.workingTreeClock.suspend();
181
+ }
55
182
  }
56
183
  [Symbol.dispose]() {
57
184
  this.dispose();
@@ -59,13 +186,13 @@ var RepositoryWatchSession = class RepositoryWatchSession {
59
186
  dispose() {
60
187
  if (this._disposed) return;
61
188
  this._disposed = true;
62
- this.cancelRepoTimer();
63
- this.cancelWorkingTreeTimer();
64
- this.pendingRepoEvent = void 0;
189
+ this.repoClock.cancel();
190
+ this.workingTreeClock.cancel();
191
+ this.clearPendingRepoChanges();
65
192
  this.pendingWorkingTreePaths.clear();
66
193
  this.pendingWorkingTreeIncomplete = false;
67
- this.repoDelays.clear();
68
- this.workingTreeDelays.clear();
194
+ this.repoSubscribers.clear();
195
+ this.workingTreeSubscribers.clear();
69
196
  this.repoEmitter.dispose();
70
197
  this.workingTreeEmitter.dispose();
71
198
  }
@@ -73,7 +200,7 @@ var RepositoryWatchSession = class RepositoryWatchSession {
73
200
  return this._suspended;
74
201
  }
75
202
  get hasPendingChanges() {
76
- return this.pendingRepoEvent != null || this.hasPendingWorkingTreeChanges;
203
+ return this.pendingRepoChanges.size > 0 || this.hasPendingWorkingTreeChanges;
77
204
  }
78
205
  /**
79
206
  * An incomplete batch with no surviving paths still has to fire: the caller dropped paths we never saw, so
@@ -83,10 +210,10 @@ var RepositoryWatchSession = class RepositoryWatchSession {
83
210
  return this.pendingWorkingTreePaths.size > 0 || this.pendingWorkingTreeIncomplete;
84
211
  }
85
212
  get repoSubscriberCount() {
86
- return this.repoDelays.size;
213
+ return this.repoSubscribers.size;
87
214
  }
88
215
  get workingTreeSubscriberCount() {
89
- return this.workingTreeDelays.size;
216
+ return this.workingTreeSubscribers.size;
90
217
  }
91
218
  /**
92
219
  * Monotonic counter incremented immediately when working tree changes
@@ -102,49 +229,61 @@ var RepositoryWatchSession = class RepositoryWatchSession {
102
229
  * fires `onLastRepoSubscriber`.
103
230
  *
104
231
  * When multiple subscribers exist, the shortest debounce wins —
105
- * all subscribers fire on the same (fastest) cadence.
232
+ * all subscribers fire on the same (fastest) cadence. `maxWaitMs` bounds how long continuous
233
+ * activity can defer a flush (the shortest wins too); `Infinity` means no cap.
234
+ *
235
+ * `relevantRefs` narrows THIS subscription's events to the refs it cares about, given as full ref names
236
+ * (`refs/heads/main`, `refs/remotes/origin/main`, `refs/tags/v1`, `refs/stash`). It is read at delivery, so it can
237
+ * return the subscriber's current list. A `heads`/`remotes`/`tags`/`stash` change is delivered only when a listed
238
+ * ref of that kind may have moved, when the change isn't attributable to a name (e.g. a `packed-refs` rewrite),
239
+ * or, for `heads`, when a pseudo-ref (`HEAD`, `ORIG_HEAD`) moved — so `stash` and `tags` changes are dropped unless
240
+ * a ref of that kind is listed. Other change kinds always pass, and an event left with no changes isn't delivered.
241
+ * The filter never affects other subscribers, the debounce, or the global `onDidFireRepoChange` forwarding.
106
242
  */
107
243
  subscribe(opts) {
108
244
  if (this._disposed) return Object.assign(createDisposable(() => {}), { onDidChange: this.repoEmitter.event });
109
245
  const id = this.repoNextId++;
110
- const delay = opts?.delayMs ?? this.defaultRepoMs;
111
- const wasEmpty = this.repoDelays.size === 0;
112
- const prevDelay = this.effectiveRepoMs;
113
- this.repoDelays.set(id, delay);
114
- this.recalcRepoDelay();
246
+ const wasEmpty = this.repoSubscribers.size === 0;
247
+ this.repoSubscribers.set(id, {
248
+ delayMs: opts?.delayMs ?? this.defaultRepoMs,
249
+ maxWaitMs: opts?.maxWaitMs ?? this.defaultRepoMaxWaitMs
250
+ });
251
+ this.recalcRepoTiming();
115
252
  if (wasEmpty) this.lifecycle?.onFirstRepoSubscriber?.();
116
- if (this.effectiveRepoMs < prevDelay && this.repoTimer != null) this.scheduleRepoFlush();
253
+ this.repoClock.rearmIfEarlier();
117
254
  let subDisposed = false;
118
255
  return Object.assign(createDisposable(() => {
119
256
  if (subDisposed) return;
120
257
  subDisposed = true;
121
- this.repoDelays.delete(id);
122
- this.recalcRepoDelay();
123
- if (this.repoDelays.size === 0) {
124
- this.cancelRepoTimer();
125
- this.pendingRepoEvent = void 0;
258
+ this.repoSubscribers.delete(id);
259
+ this.recalcRepoTiming();
260
+ if (this.repoSubscribers.size === 0) {
261
+ this.repoClock.cancel();
262
+ this.clearPendingRepoChanges();
126
263
  this.lifecycle?.onLastRepoSubscriber?.();
127
264
  }
128
- }), { onDidChange: this.repoEmitter.event });
265
+ }), { onDidChange: opts?.relevantRefs != null ? createRelevantRefsEvent(this.repoEmitter.event, opts.relevantRefs) : this.repoEmitter.event });
129
266
  }
267
+ /** Subscribe to working tree changes; `delayMs` and `maxWaitMs` behave as in {@link subscribe}. */
130
268
  subscribeToWorkingTree(opts) {
131
269
  if (this._disposed) return Object.assign(createDisposable(() => {}), { onDidChangeWorkingTree: this.workingTreeEmitter.event });
132
270
  const id = this.workingTreeNextId++;
133
- const delay = opts?.delayMs ?? this.defaultWorkingTreeMs;
134
- const wasEmpty = this.workingTreeDelays.size === 0;
135
- const prevDelay = this.effectiveWorkingTreeMs;
136
- this.workingTreeDelays.set(id, delay);
137
- this.recalcWorkingTreeDelay();
271
+ const wasEmpty = this.workingTreeSubscribers.size === 0;
272
+ this.workingTreeSubscribers.set(id, {
273
+ delayMs: opts?.delayMs ?? this.defaultWorkingTreeMs,
274
+ maxWaitMs: opts?.maxWaitMs ?? this.defaultWorkingTreeMaxWaitMs
275
+ });
276
+ this.recalcWorkingTreeTiming();
138
277
  if (wasEmpty) this.lifecycle?.onFirstWorkingTreeSubscriber?.();
139
- if (this.effectiveWorkingTreeMs < prevDelay && this.workingTreeTimer != null) this.scheduleWorkingTreeFlush();
278
+ this.workingTreeClock.rearmIfEarlier();
140
279
  let subDisposed = false;
141
280
  return Object.assign(createDisposable(() => {
142
281
  if (subDisposed) return;
143
282
  subDisposed = true;
144
- this.workingTreeDelays.delete(id);
145
- this.recalcWorkingTreeDelay();
146
- if (this.workingTreeDelays.size === 0) {
147
- this.cancelWorkingTreeTimer();
283
+ this.workingTreeSubscribers.delete(id);
284
+ this.recalcWorkingTreeTiming();
285
+ if (this.workingTreeSubscribers.size === 0) {
286
+ this.workingTreeClock.cancel();
148
287
  this.pendingWorkingTreePaths.clear();
149
288
  this.pendingWorkingTreeIncomplete = false;
150
289
  this.lifecycle?.onLastWorkingTreeSubscriber?.();
@@ -153,31 +292,37 @@ var RepositoryWatchSession = class RepositoryWatchSession {
153
292
  }
154
293
  suspend() {
155
294
  this._suspended = true;
156
- this.cancelRepoTimer();
157
- this.cancelWorkingTreeTimer();
295
+ this.repoClock.suspend();
296
+ this.workingTreeClock.suspend();
158
297
  }
159
298
  resume(delayMs) {
160
299
  if (!this._suspended) return;
161
300
  this._suspended = false;
162
- if (this.pendingRepoEvent != null) this.scheduleRepoFlush(delayMs);
163
- if (this.hasPendingWorkingTreeChanges) this.scheduleWorkingTreeFlush(delayMs);
301
+ this.repoClock.resume(delayMs);
302
+ this.workingTreeClock.resume(delayMs);
164
303
  }
165
- /** Inject changes (e.g., after a git operation the caller performed). Goes through debounce + suspend. */
304
+ /**
305
+ * Inject changes (e.g., after a git operation the caller performed). Goes through debounce + suspend. Ref kinds
306
+ * among them are unnamed — any ref of that kind may have moved.
307
+ */
166
308
  fireChange(...changes) {
167
309
  if (this._disposed || changes.length === 0) return;
168
310
  this.coalesceRepo(changes);
169
311
  }
170
- /** Immediate fire, bypassing debounce + suspend (for Closed/Opened). */
312
+ /** Immediate fire, bypassing debounce + suspend (for Closed/Opened). Ref kinds among them are unnamed. */
171
313
  fireChangeImmediate(...changes) {
172
314
  if (this._disposed || changes.length === 0) return;
173
315
  const event = new WatcherRepoChangeEvent(this.repoPath, changes);
174
316
  this.onDidFireRepoChangeCallback?.(event);
175
317
  this.repoEmitter.fire(event);
176
318
  }
177
- /** Push interpreted repo changes into the pipeline */
178
- pushRepoChanges(changes) {
319
+ /**
320
+ * Push interpreted repo changes into the pipeline. `ref` is the full ref name (or ref directory, or pseudo-ref) whose
321
+ * file changed; a ref kind in `changes` is attributed to it when the kinds match, and is otherwise unnamed.
322
+ */
323
+ pushRepoChanges(changes, ref) {
179
324
  if (this._disposed) return;
180
- this.coalesceRepo(changes);
325
+ this.coalesceRepo(changes, ref);
181
326
  }
182
327
  /** Push working tree file changes into the pipeline */
183
328
  /** `incomplete` marks the batch as partial when the caller already dropped paths of its own. */
@@ -187,38 +332,53 @@ var RepositoryWatchSession = class RepositoryWatchSession {
187
332
  if (incomplete) this.pendingWorkingTreeIncomplete = true;
188
333
  this.coalesceWorkingTrees(paths);
189
334
  }
190
- static minDelay(delays, fallback) {
191
- if (delays.size === 0) return fallback;
335
+ static minDelay(subscribers, fallback) {
192
336
  let min = fallback;
193
- for (const d of delays.values()) if (d < min) min = d;
337
+ for (const { delayMs } of subscribers.values()) if (delayMs < min) min = delayMs;
338
+ return min;
339
+ }
340
+ /** Unlike the delay, the default isn't a ceiling — a subscriber can opt out of the cap with `Infinity` */
341
+ static minMaxWait(subscribers, fallback) {
342
+ if (subscribers.size === 0) return fallback;
343
+ let min = Infinity;
344
+ for (const { maxWaitMs } of subscribers.values()) if (maxWaitMs < min) min = maxWaitMs;
194
345
  return min;
195
346
  }
196
- recalcRepoDelay() {
197
- this.effectiveRepoMs = RepositoryWatchSession.minDelay(this.repoDelays, this.defaultRepoMs);
347
+ recalcRepoTiming() {
348
+ this.effectiveRepoMs = RepositoryWatchSession.minDelay(this.repoSubscribers, this.defaultRepoMs);
349
+ this.effectiveRepoMaxWaitMs = RepositoryWatchSession.minMaxWait(this.repoSubscribers, this.defaultRepoMaxWaitMs);
198
350
  }
199
- recalcWorkingTreeDelay() {
200
- this.effectiveWorkingTreeMs = RepositoryWatchSession.minDelay(this.workingTreeDelays, this.defaultWorkingTreeMs);
351
+ recalcWorkingTreeTiming() {
352
+ this.effectiveWorkingTreeMs = RepositoryWatchSession.minDelay(this.workingTreeSubscribers, this.defaultWorkingTreeMs);
353
+ this.effectiveWorkingTreeMaxWaitMs = RepositoryWatchSession.minMaxWait(this.workingTreeSubscribers, this.defaultWorkingTreeMaxWaitMs);
201
354
  }
202
355
  flushRepo() {
203
- this.repoTimer = void 0;
204
- const event = this.pendingRepoEvent;
205
- if (event == null) return;
206
- this.pendingRepoEvent = void 0;
356
+ if (this.pendingRepoChanges.size === 0) return;
357
+ const event = new WatcherRepoChangeEvent(this.repoPath, [...this.pendingRepoChanges], {
358
+ names: this.pendingRepoRefs,
359
+ unnamed: this.pendingRepoUnnamedRefKinds
360
+ });
361
+ this.clearPendingRepoChanges();
207
362
  this.onDidFireRepoChangeCallback?.(event);
208
363
  this.repoEmitter.fire(event);
209
364
  }
210
- scheduleRepoFlush(delayOverride) {
211
- if (this._suspended || this.pendingRepoEvent == null) return;
212
- if (this.repoTimer != null) clearTimeout(this.repoTimer);
213
- this.repoTimer = setTimeout(() => this.flushRepo(), delayOverride ?? this.effectiveRepoMs);
214
- }
215
- coalesceRepo(changes) {
365
+ coalesceRepo(changes, ref) {
216
366
  if (changes.length === 0) return;
217
- this.pendingRepoEvent = this.pendingRepoEvent == null ? new WatcherRepoChangeEvent(this.repoPath, changes) : this.pendingRepoEvent.with(changes);
218
- this.scheduleRepoFlush();
367
+ const refKind = ref != null ? getRefChangeKind(ref) : void 0;
368
+ for (const change of changes) {
369
+ this.pendingRepoChanges.add(change);
370
+ if (!isRefChangeKind(change)) continue;
371
+ if (ref != null && refKind === change && (this.pendingRepoRefs.size < maxPendingRefNames || this.pendingRepoRefs.has(ref))) this.pendingRepoRefs.add(ref);
372
+ else this.pendingRepoUnnamedRefKinds.add(change);
373
+ }
374
+ this.repoClock.note();
375
+ }
376
+ clearPendingRepoChanges() {
377
+ this.pendingRepoChanges.clear();
378
+ this.pendingRepoRefs.clear();
379
+ this.pendingRepoUnnamedRefKinds.clear();
219
380
  }
220
381
  flushWorkingTree() {
221
- this.workingTreeTimer = void 0;
222
382
  if (!this.hasPendingWorkingTreeChanges) return;
223
383
  const paths = this.pendingWorkingTreePaths;
224
384
  const incomplete = this.pendingWorkingTreeIncomplete;
@@ -231,11 +391,6 @@ var RepositoryWatchSession = class RepositoryWatchSession {
231
391
  incomplete
232
392
  });
233
393
  }
234
- scheduleWorkingTreeFlush(delayOverride) {
235
- if (this._suspended || !this.hasPendingWorkingTreeChanges) return;
236
- if (this.workingTreeTimer != null) clearTimeout(this.workingTreeTimer);
237
- this.workingTreeTimer = setTimeout(() => this.flushWorkingTree(), delayOverride ?? this.effectiveWorkingTreeMs);
238
- }
239
394
  coalesceWorkingTrees(paths) {
240
395
  for (const p of paths) {
241
396
  if (this.pendingWorkingTreePaths.size >= maxWorkingTreePaths && !this.pendingWorkingTreePaths.has(p)) {
@@ -244,21 +399,57 @@ var RepositoryWatchSession = class RepositoryWatchSession {
244
399
  }
245
400
  this.pendingWorkingTreePaths.add(p);
246
401
  }
247
- this.scheduleWorkingTreeFlush();
402
+ if (this.hasPendingWorkingTreeChanges) this.workingTreeClock.note();
248
403
  }
249
- cancelRepoTimer() {
250
- if (this.repoTimer != null) {
251
- clearTimeout(this.repoTimer);
252
- this.repoTimer = void 0;
253
- }
404
+ };
405
+ /** Wraps a session's repo event so each delivery is narrowed to the subscriber's relevant refs (see `subscribe`) */
406
+ function createRelevantRefsEvent(event, relevantRefs) {
407
+ return (listener, thisArgs, disposables) => event((e) => {
408
+ const narrowed = narrowToRelevantRefs(e, relevantRefs);
409
+ if (narrowed != null) listener.call(thisArgs, narrowed);
410
+ }, void 0, disposables);
411
+ }
412
+ /**
413
+ * Narrows an event to the refs a subscriber cares about. Returns the event itself when no change kind is removed (the
414
+ * kept names then stay a superset, which only makes `mayHaveChangedRef` more conservative), `undefined` when every kind
415
+ * is removed, and otherwise a new event with the kept kinds, names and unnamed kinds.
416
+ */
417
+ function narrowToRelevantRefs(e, relevantRefs) {
418
+ let unresolved;
419
+ for (const change of e.changes) {
420
+ if (!isRefChangeKind(change) || e.unnamedRefKinds.has(change)) continue;
421
+ (unresolved ??= /* @__PURE__ */ new Set()).add(change);
254
422
  }
255
- cancelWorkingTreeTimer() {
256
- if (this.workingTreeTimer != null) {
257
- clearTimeout(this.workingTreeTimer);
258
- this.workingTreeTimer = void 0;
423
+ if (unresolved == null) return e;
424
+ const pseudoRefs = [];
425
+ for (const name of e.refs) if (!name.startsWith("refs/")) pseudoRefs.push(name);
426
+ if (pseudoRefs.length !== 0) {
427
+ unresolved.delete("heads");
428
+ if (unresolved.size === 0) return e;
429
+ }
430
+ const keptNames = new Set(pseudoRefs);
431
+ for (const ref of relevantRefs()) {
432
+ const kind = getRefChangeKind(ref);
433
+ if (kind == null || !e.changes.has(kind)) continue;
434
+ if (ref === "HEAD") {
435
+ for (const name of e.refs) if (getRefChangeKind(name) === "heads") keptNames.add(name);
436
+ unresolved.delete(kind);
437
+ continue;
259
438
  }
439
+ const covering = findCoveringRefName(e.refs, ref);
440
+ if (covering == null) continue;
441
+ keptNames.add(covering);
442
+ unresolved.delete(kind);
260
443
  }
261
- };
444
+ if (unresolved.size === 0) return e;
445
+ const keptChanges = [];
446
+ for (const change of e.changes) if (!isRefChangeKind(change) || !unresolved.has(change)) keptChanges.push(change);
447
+ if (keptChanges.length === 0) return void 0;
448
+ return new WatcherRepoChangeEvent(e.repoPath, keptChanges, {
449
+ names: keptNames,
450
+ unnamed: e.unnamedRefKinds
451
+ });
452
+ }
262
453
  //#endregion
263
454
  export { RepositoryWatchSession };
264
455