51.6 KB1548 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {TypeaheadResult} from 'isl-components/Types';
9import type {Serializable} from 'isl/src/serialize';
10import type {
11 ChangedFile,
12 ClientToServerMessage,
13 CodeReviewProviderSpecificClientToServerMessages,
14 Disposable,
15 FetchedCommits,
16 FetchedUncommittedChanges,
17 FileABugProgress,
18 LandInfo,
19 MergeConflicts,
20 PlatformSpecificClientToServerMessages,
21 RepositoryError,
22 Result,
23 ServerToClientMessage,
24 StableLocationData,
25 SubmodulesByRoot,
26} from 'isl/src/types';
27import type {EjecaError, EjecaReturn} from 'shared/ejeca';
28import {ejeca} from 'shared/ejeca';
29import type {ExportStack, ImportedStack} from 'shared/types/stack';
30import type {ClientConnection} from '.';
31import type {RepositoryReference} from './RepositoryCache';
32import type {ServerSideTracker} from './analytics/serverSideTracker';
33import type {Logger} from './logger';
34import type {ServerPlatform} from './serverPlatform';
35import type {RepositoryContext} from './serverTypes';
36
37import type {InternalTypes} from 'isl/src/InternalTypes';
38import {deserializeFromString, serializeToString} from 'isl/src/serialize';
39import type {PartiallySelectedDiffCommit} from 'isl/src/stackEdit/diffSplitTypes';
40import {Readable} from 'node:stream';
41import path from 'path';
42import {beforeRevsetForComparison} from 'shared/Comparison';
43import {base64Decode, notEmpty, randomId} from 'shared/utils';
44import {generatedFilesDetector} from './GeneratedFiles';
45import {Internal} from './Internal';
46import {Repository, absolutePathForFileInRepo, readGroveConfig} from './Repository';
47import {repositoryCache} from './RepositoryCache';
48import {firstOfIterable, parseExecJson} from './utils';
49
50export type IncomingMessage = ClientToServerMessage;
51export type OutgoingMessage = ServerToClientMessage;
52
53type GeneralMessage = IncomingMessage &
54 (
55 | {type: 'heartbeat'}
56 | {type: 'stress'}
57 | {type: 'changeCwd'}
58 | {type: 'requestRepoInfo'}
59 | {type: 'requestApplicationInfo'}
60 | {type: 'fileBugReport'}
61 | {type: 'track'}
62 | {type: 'clientReady'}
63 | {type: 'fetchGroveOwners'}
64 | {type: 'createGroveRepo'}
65 );
66type WithRepoMessage = Exclude<IncomingMessage, GeneralMessage>;
67
68/**
69 * Message passing channel built on top of ClientConnection.
70 * Use to send and listen for well-typed events with the client
71 *
72 * Note: you must set the current repository to start sending data back to the client.
73 */
74export default class ServerToClientAPI {
75 private listenersByType = new Map<
76 string,
77 Set<(message: IncomingMessage) => void | Promise<void>>
78 >();
79 private incomingListener: Disposable;
80
81 /** Disposables that must be disposed whenever the current repo is changed */
82 private repoDisposables: Array<Disposable> = [];
83 private subscriptions = new Map<string, Disposable>();
84 private activeRepoRef: RepositoryReference | undefined;
85
86 private queuedMessages: Array<IncomingMessage> = [];
87 private currentState:
88 | {type: 'loading'}
89 | {type: 'repo'; repo: Repository; ctx: RepositoryContext}
90 | {type: 'error'; error: RepositoryError} = {type: 'loading'};
91
92 private pageId = randomId();
93
94 constructor(
95 private platform: ServerPlatform,
96 private connection: ClientConnection,
97 private tracker: ServerSideTracker,
98 private logger: Logger,
99 ) {
100 this.incomingListener = this.connection.onDidReceiveMessage(buf => {
101 const message = buf.toString('utf-8');
102 const data = deserializeFromString(message) as IncomingMessage;
103
104 // When the client is connected, we want to immediately start listening to messages.
105 // However, we can't properly respond to these messages until we have a repository set up.
106 // Queue up messages until a repository is set.
107 if (this.currentState.type === 'loading') {
108 this.queuedMessages.push(data);
109 } else {
110 try {
111 this.handleIncomingMessage(data);
112 } catch (err) {
113 connection.logger?.error('error handling incoming message: ', data, err);
114 }
115 }
116 });
117 }
118
119 private setRepoError(error: RepositoryError) {
120 this.disposeRepoDisposables();
121
122 this.currentState = {type: 'error', error};
123
124 this.tracker.context.setRepo(undefined);
125
126 this.processQueuedMessages();
127 }
128
129 private setCurrentRepo(repo: Repository, ctx: RepositoryContext) {
130 this.disposeRepoDisposables();
131
132 this.currentState = {type: 'repo', repo, ctx};
133
134 this.tracker.context.setRepo(repo);
135
136 if (repo.codeReviewProvider != null) {
137 this.repoDisposables.push(
138 repo.codeReviewProvider.onChangeDiffSummaries(value => {
139 this.postMessage({type: 'fetchedDiffSummaries', summaries: value});
140 }),
141 );
142 if (repo.codeReviewProvider.onChangeCanopySignals) {
143 this.repoDisposables.push(
144 repo.codeReviewProvider.onChangeCanopySignals(runs => {
145 this.postMessage({type: 'fetchedCanopySignals', runs});
146 }),
147 );
148 }
149 }
150
151 repo.ref();
152 this.repoDisposables.push({dispose: () => repo.unref()});
153
154 // Send initial watchman status and subscribe to changes
155 this.postMessage({type: 'watchmanStatus', status: repo.watchForChanges.watchman.status});
156 repo.watchForChanges.onWatchmanStatusChange = status => {
157 this.postMessage({type: 'watchmanStatus', status});
158 };
159 this.repoDisposables.push({dispose: () => {
160 repo.watchForChanges.onWatchmanStatusChange = undefined;
161 }});
162
163 repo.fetchAndSetRecommendedBookmarks(async bookmarks => {
164 await this.connection.readySignal?.promise;
165 this.postMessage({type: 'fetchedRecommendedBookmarks', bookmarks});
166 });
167
168 repo.fetchAndSetHiddenMasterConfig(async (config, odType) => {
169 await this.connection.readySignal?.promise;
170 this.postMessage({
171 type: 'fetchedHiddenMasterBranchConfig',
172 config,
173 odType,
174 cwd: ctx.cwd,
175 });
176 });
177
178 this.processQueuedMessages();
179 }
180
181 postMessage(message: OutgoingMessage) {
182 this.connection.postMessage(serializeToString(message));
183 }
184
185 /** Get a repository reference for a given cwd, and set that as the active repo. */
186 setActiveRepoForCwd(newCwd: string) {
187 if (this.activeRepoRef !== undefined) {
188 this.activeRepoRef.unref();
189 }
190 this.logger.info(`Setting active repo cwd to ${newCwd}`);
191 // Set as loading right away while we determine the new cwd's repo
192 // This ensures new messages coming in will be queued and handled only with the new repository
193 this.currentState = {type: 'loading'};
194 const command = this.connection.command ?? 'sl';
195 const ctx: RepositoryContext = {
196 cwd: newCwd,
197 cmd: command,
198 logger: this.logger,
199 tracker: this.tracker,
200 };
201 this.activeRepoRef = repositoryCache.getOrCreate(ctx);
202 this.activeRepoRef.promise.then(repoOrError => {
203 if (repoOrError instanceof Repository) {
204 this.setCurrentRepo(repoOrError, ctx);
205 } else {
206 this.setRepoError(repoOrError);
207 }
208 });
209 }
210
211 dispose() {
212 this.incomingListener.dispose();
213 this.disposeRepoDisposables();
214
215 if (this.activeRepoRef !== undefined) {
216 this.activeRepoRef.unref();
217 }
218 }
219
220 private disposeRepoDisposables() {
221 this.repoDisposables.forEach(disposable => disposable.dispose());
222 this.repoDisposables = [];
223
224 this.subscriptions.forEach(sub => sub.dispose());
225 this.subscriptions.clear();
226 }
227
228 private processQueuedMessages() {
229 for (const message of this.queuedMessages) {
230 try {
231 this.handleIncomingMessage(message);
232 } catch (err) {
233 this.connection.logger?.error('error handling queued message: ', message, err);
234 }
235 }
236 this.queuedMessages = [];
237 }
238
239 private handleIncomingMessage(data: IncomingMessage) {
240 this.handleIncomingGeneralMessage(data as GeneralMessage);
241 const {currentState} = this;
242 switch (currentState.type) {
243 case 'repo': {
244 const {repo, ctx} = currentState;
245 this.handleIncomingMessageWithRepo(data as WithRepoMessage, repo, ctx);
246 break;
247 }
248
249 // If the repo is in the loading or error state, the client may still send
250 // platform messages such as `platform/openExternal` that should be processed.
251 case 'loading':
252 case 'error':
253 if (data.type.startsWith('platform/')) {
254 this.platform.handleMessageFromClient(
255 /*repo=*/ undefined,
256 // even if we don't have a repo, we can still make a RepositoryContext to execute commands
257 {
258 cwd: this.connection.cwd,
259 cmd: this.connection.command ?? 'sl',
260 logger: this.logger,
261 tracker: this.tracker,
262 },
263 data as PlatformSpecificClientToServerMessages,
264 message => this.postMessage(message),
265 (dispose: () => unknown) => {
266 this.repoDisposables.push({dispose});
267 },
268 );
269 this.notifyListeners(data);
270 }
271 break;
272 }
273 }
274
275 /**
276 * Handle messages which can be handled regardless of if a repo was successfully created or not
277 */
278 private handleIncomingGeneralMessage(data: GeneralMessage) {
279 switch (data.type) {
280 case 'heartbeat': {
281 this.postMessage({type: 'heartbeat', id: data.id});
282 break;
283 }
284 case 'stress': {
285 this.postMessage(data);
286 break;
287 }
288 case 'track': {
289 this.tracker.trackData(data.data);
290 break;
291 }
292 case 'clientReady': {
293 this.connection.readySignal?.resolve();
294 break;
295 }
296 case 'changeCwd': {
297 this.setActiveRepoForCwd(data.cwd);
298 break;
299 }
300 case 'requestRepoInfo': {
301 switch (this.currentState.type) {
302 case 'repo':
303 this.postMessage({
304 type: 'repoInfo',
305 info: this.currentState.repo.info,
306 cwd: this.currentState.ctx.cwd,
307 });
308 break;
309 case 'error':
310 this.postMessage({type: 'repoInfo', info: this.currentState.error});
311 break;
312 }
313 break;
314 }
315 case 'requestApplicationInfo': {
316 this.postMessage({
317 type: 'applicationInfo',
318 info: {
319 platformName: this.platform.platformName,
320 version: this.connection.version,
321 logFilePath: this.connection.logFileLocation ?? '(no log file, logging to stdout)',
322 readOnly: this.connection.readOnly ?? false,
323 },
324 });
325 break;
326 }
327 case 'fileBugReport': {
328 const maybeRepo = this.currentState.type === 'repo' ? this.currentState.repo : undefined;
329 const ctx: RepositoryContext =
330 this.currentState.type === 'repo'
331 ? this.currentState.ctx
332 : {
333 // cwd is only needed to run graphql query, here it's just best-effort
334 cwd: maybeRepo?.initialConnectionContext.cwd ?? process.cwd(),
335 cmd: this.connection.command ?? 'sl',
336 logger: this.logger,
337 tracker: this.tracker,
338 };
339 Internal.fileABug?.(
340 ctx,
341 this.platform.platformName,
342 data.data,
343 data.uiState,
344 // Use repo for rage, if available.
345 maybeRepo,
346 data.collectRage,
347 (progress: FileABugProgress) => {
348 this.connection.logger?.info('file a bug progress: ', JSON.stringify(progress));
349 this.postMessage({type: 'fileBugReportProgress', ...progress});
350 },
351 );
352 break;
353 }
354 case 'fetchGroveOwners': {
355 const groveConfig = readGroveConfig(this.logger);
356 if (!groveConfig.hub || !groveConfig.token) {
357 this.postMessage({type: 'fetchedGroveOwners', owners: []});
358 break;
359 }
360 (async () => {
361 try {
362 const owners: Array<{name: string; type: 'user' | 'org'}> = [];
363 if (groveConfig.username) {
364 owners.push({name: groveConfig.username, type: 'user'});
365 }
366 const res = await fetch(`${groveConfig.hub}/api/orgs`, {
367 headers: {Authorization: `Bearer ${groveConfig.token}`},
368 });
369 if (res.ok) {
370 const data = (await res.json()) as {orgs: Array<{name: string}>};
371 for (const org of data.orgs) {
372 owners.push({name: org.name, type: 'org'});
373 }
374 }
375 this.postMessage({type: 'fetchedGroveOwners', owners});
376 } catch (err) {
377 this.logger.error('Failed to fetch Grove owners:', err);
378 this.postMessage({type: 'fetchedGroveOwners', owners: []});
379 }
380 })();
381 break;
382 }
383 case 'createGroveRepo': {
384 const groveConfig = readGroveConfig(this.logger);
385 if (!groveConfig.hub || !groveConfig.token) {
386 this.postMessage({
387 type: 'createdGroveRepo',
388 result: {error: new Error('Not logged in to Grove. Run `grove auth login` first.')},
389 });
390 break;
391 }
392 const owner = data.owner ?? groveConfig.username;
393 if (!owner) {
394 this.postMessage({
395 type: 'createdGroveRepo',
396 result: {error: new Error('No owner specified and no username found in Grove config.')},
397 });
398 break;
399 }
400 const repoName = data.name;
401 (async () => {
402 try {
403 // Create the repo on Grove
404 const res = await fetch(`${groveConfig.hub}/api/repos`, {
405 method: 'POST',
406 headers: {
407 Authorization: `Bearer ${groveConfig.token}`,
408 'Content-Type': 'application/json',
409 },
410 body: JSON.stringify({name: repoName, owner}),
411 });
412 if (!res.ok) {
413 const body = await res.text();
414 throw new Error(`Failed to create repository: ${res.status} ${body}`);
415 }
416
417 const cwd = this.connection.cwd;
418 const cmd = this.connection.command ?? 'sl';
419 const homedir = process.env.HOME ?? process.env.USERPROFILE ?? '';
420
421 // If sl init created a git-backed repo, re-init with correct format
422 const fs = await import('fs');
423 const storeRequires = path.join(cwd, '.sl', 'store', 'requires');
424 try {
425 const requires = fs.readFileSync(storeRequires, 'utf8');
426 if (requires.includes('git')) {
427 this.logger.info(
428 'Re-initializing repo: current store is git-backed, need native format for Grove',
429 );
430 fs.rmSync(path.join(cwd, '.sl'), {recursive: true, force: true});
431 await ejeca(
432 cmd,
433 [
434 'init',
435 '--config',
436 'init.prefer-git=false',
437 '--config',
438 'format.use-remotefilelog=true',
439 cwd,
440 ],
441 {cwd},
442 );
443 }
444 } catch {
445 // No .sl/store/requires means no repo yet — init fresh
446 if (!fs.existsSync(path.join(cwd, '.sl'))) {
447 await ejeca(
448 cmd,
449 [
450 'init',
451 '--config',
452 'init.prefer-git=false',
453 '--config',
454 'format.use-remotefilelog=true',
455 cwd,
456 ],
457 {cwd},
458 );
459 }
460 }
461
462 // Configure sapling remote
463 const configPairs = [
464 ['paths.default', `mononoke://grove.host:8443/${repoName}`],
465 ['remotefilelog.reponame', repoName],
466 ['grove.owner', owner],
467 ['edenapi.url', 'https://grove.host:8443/edenapi/'],
468 ['web.cacerts', `${homedir}/.grove/tls/ca.crt`],
469 ['auth.grove.prefix', 'https://grove.host'],
470 ['auth.grove.cert', `${homedir}/.grove/tls/client.crt`],
471 ['auth.grove.key', `${homedir}/.grove/tls/client.key`],
472 ['auth.grove.cacerts', `${homedir}/.grove/tls/ca.crt`],
473 ['auth.grove-mononoke.prefix', 'mononoke://grove.host'],
474 ['auth.grove-mononoke.cert', `${homedir}/.grove/tls/client.crt`],
475 ['auth.grove-mononoke.key', `${homedir}/.grove/tls/client.key`],
476 ['auth.grove-mononoke.cacerts', `${homedir}/.grove/tls/ca.crt`],
477 ['clone.use-commit-graph', 'true'],
478 ['remotenames.selectivepulldefault', 'main'],
479 ['push.edenapi', 'true'],
480 ['push.to', 'main'],
481 ];
482 for (const [key, value] of configPairs) {
483 await ejeca(cmd, ['config', '--local', key, value], {cwd});
484 }
485
486 // Pull the seed commit and check out main
487 try {
488 await ejeca(cmd, ['pull'], {cwd});
489 await ejeca(cmd, ['goto', 'main'], {cwd});
490 } catch (pullErr) {
491 this.logger.info('Pull after repo creation failed (may not be seeded yet):', pullErr);
492 }
493
494 // Re-detect repo info and send it to the client
495 const ctx = {
496 cwd,
497 cmd,
498 logger: this.logger,
499 tracker: this.tracker,
500 };
501 const newInfo = await Repository.getRepoInfo(ctx);
502 this.postMessage({type: 'repoInfo', info: newInfo, cwd});
503
504 // Also re-initialize the repo in the cache so subscriptions work
505 this.setActiveRepoForCwd(cwd);
506
507 this.postMessage({
508 type: 'createdGroveRepo',
509 result: {value: {owner, repo: repoName}},
510 });
511 } catch (err) {
512 this.logger.error('Failed to create Grove repo:', err);
513 this.postMessage({
514 type: 'createdGroveRepo',
515 result: {error: err instanceof Error ? err : new Error(String(err))},
516 });
517 }
518 })();
519 break;
520 }
521 }
522 }
523
524 private handleMaybeForgotOperation(operationId: string, repo: Repository) {
525 if (repo.getRunningOperation()?.id !== operationId) {
526 this.postMessage({type: 'operationProgress', id: operationId, kind: 'forgot'});
527 }
528 }
529
530 /**
531 * Handle messages which require a repository to have been successfully set up to run
532 */
533 private handleIncomingMessageWithRepo(
534 data: WithRepoMessage,
535 repo: Repository,
536 ctx: RepositoryContext,
537 ) {
538 const {cwd, logger} = ctx;
539 switch (data.type) {
540 case 'subscribe': {
541 const {subscriptionID, kind} = data;
542 switch (kind) {
543 case 'uncommittedChanges': {
544 const postUncommittedChanges = (result: FetchedUncommittedChanges) => {
545 this.postMessage({
546 type: 'subscriptionResult',
547 kind: 'uncommittedChanges',
548 subscriptionID,
549 data: result,
550 });
551 };
552
553 const uncommittedChanges = repo.getUncommittedChanges();
554 if (uncommittedChanges != null) {
555 postUncommittedChanges(uncommittedChanges);
556 }
557 const disposables: Array<Disposable> = [];
558
559 // send changes as they come in from watchman
560 disposables.push(repo.subscribeToUncommittedChanges(postUncommittedChanges));
561 // trigger a fetch on startup
562 repo.fetchUncommittedChanges();
563
564 disposables.push(
565 repo.subscribeToUncommittedChangesBeginFetching(() =>
566 this.postMessage({type: 'beganFetchingUncommittedChangesEvent'}),
567 ),
568 );
569 this.subscriptions.set(subscriptionID, {
570 dispose: () => {
571 disposables.forEach(d => d.dispose());
572 },
573 });
574 break;
575 }
576 case 'smartlogCommits': {
577 const postSmartlogCommits = (result: FetchedCommits) => {
578 this.postMessage({
579 type: 'subscriptionResult',
580 kind: 'smartlogCommits',
581 subscriptionID,
582 data: result,
583 });
584 };
585
586 const smartlogCommits = repo.getSmartlogCommits();
587 if (smartlogCommits != null) {
588 postSmartlogCommits(smartlogCommits);
589 }
590 const disposables: Array<Disposable> = [];
591 // send changes as they come from file watcher
592 disposables.push(repo.subscribeToSmartlogCommitsChanges(postSmartlogCommits));
593
594 // trigger fetch on startup
595 repo.fetchSmartlogCommits();
596
597 disposables.push(
598 repo.subscribeToSmartlogCommitsBeginFetching(() =>
599 this.postMessage({type: 'beganFetchingSmartlogCommitsEvent'}),
600 ),
601 );
602
603 this.subscriptions.set(subscriptionID, {
604 dispose: () => {
605 disposables.forEach(d => d.dispose());
606 },
607 });
608 break;
609 }
610 case 'mergeConflicts': {
611 const postMergeConflicts = (conflicts: MergeConflicts | undefined) => {
612 this.postMessage({
613 type: 'subscriptionResult',
614 kind: 'mergeConflicts',
615 subscriptionID,
616 data: conflicts,
617 });
618 };
619
620 const mergeConflicts = repo.getMergeConflicts();
621 if (mergeConflicts != null) {
622 postMergeConflicts(mergeConflicts);
623 }
624
625 this.subscriptions.set(subscriptionID, repo.onChangeConflictState(postMergeConflicts));
626 break;
627 }
628 case 'submodules': {
629 const postSubmodules = (submodulesByRoot: SubmodulesByRoot) => {
630 this.postMessage({
631 type: 'subscriptionResult',
632 kind: 'submodules',
633 subscriptionID,
634 data: submodulesByRoot,
635 });
636 };
637 const submoduleMap = repo.getSubmoduleMap();
638 if (submoduleMap !== undefined) {
639 postSubmodules(submoduleMap);
640 }
641 repo.fetchSubmoduleMap();
642
643 const disposable = repo.subscribeToSubmodulesChanges(postSubmodules);
644 this.subscriptions.set(subscriptionID, {
645 dispose: () => {
646 disposable.dispose();
647 },
648 });
649 break;
650 }
651 case 'subscribedFullRepoBranches': {
652 const fullRepoBranchModule = repo.fullRepoBranchModule;
653 if (fullRepoBranchModule == null) {
654 return;
655 }
656
657 const postSubscribedFullRepoBranches = (
658 result: Array<InternalTypes['FullRepoBranch']>,
659 ) => {
660 this.postMessage({
661 type: 'subscriptionResult',
662 kind: 'subscribedFullRepoBranches',
663 subscriptionID,
664 data: result,
665 });
666 };
667
668 const subscribedFullRepoBranches = fullRepoBranchModule.getSubscribedFullRepoBranches();
669 if (subscribedFullRepoBranches != null) {
670 postSubscribedFullRepoBranches(subscribedFullRepoBranches);
671 }
672 const disposables: Array<Disposable> = [];
673 // send changes as they come from file watcher
674 disposables.push(
675 fullRepoBranchModule.subscribeToSubscribedFullRepoBranchesChanges(
676 postSubscribedFullRepoBranches,
677 ),
678 );
679 // trigger a fetch on startup
680 fullRepoBranchModule.pullSubscribedFullRepoBranches();
681
682 disposables.push(
683 fullRepoBranchModule.subscribeToSubscribedFullRepoBranchesBeginFetching(() =>
684 this.postMessage({type: 'beganFetchingSubscribedFullRepoBranchesEvent'}),
685 ),
686 );
687
688 this.subscriptions.set(subscriptionID, {
689 dispose: () => {
690 disposables.forEach(d => d.dispose());
691 },
692 });
693 break;
694 }
695 }
696 break;
697 }
698 case 'unsubscribe': {
699 const subscription = this.subscriptions.get(data.subscriptionID);
700 subscription?.dispose();
701 this.subscriptions.delete(data.subscriptionID);
702 break;
703 }
704 case 'runOperation': {
705 if (this.connection.readOnly) {
706 const {operation} = data;
707 this.postMessage({
708 type: 'operationProgress',
709 kind: 'exit',
710 exitCode: 1,
711 id: operation.id,
712 timestamp: Date.now() / 1000,
713 });
714 break;
715 }
716 const {operation} = data;
717 repo.runOrQueueOperation(ctx, operation, progress => {
718 this.postMessage({type: 'operationProgress', ...progress});
719 if (progress.kind === 'queue') {
720 this.tracker.track('QueueOperation', {extras: {operation: operation.trackEventName}});
721 }
722 });
723 break;
724 }
725 case 'abortRunningOperation': {
726 if (this.connection.readOnly) {
727 break;
728 }
729 const {operationId} = data;
730 repo.abortRunningOperation(operationId);
731 this.handleMaybeForgotOperation(operationId, repo);
732 break;
733 }
734 case 'getConfig': {
735 repo
736 .getConfig(ctx, data.name)
737 .catch(() => undefined)
738 .then(value => {
739 logger.info('got config', data.name, value);
740 this.postMessage({type: 'gotConfig', name: data.name, value});
741 });
742 break;
743 }
744 case 'setConfig': {
745 logger.info('set config', data.name, data.value);
746 repo.setConfig(ctx, 'user', data.name, data.value).catch(err => {
747 logger.error('error setting config', data.name, data.value, err);
748 });
749 break;
750 }
751 case 'setDebugLogging': {
752 logger.info('set debug', data.name, data.enabled);
753 if (data.name === 'debug' || data.name === 'verbose') {
754 ctx[data.name] = !!data.enabled;
755 }
756 break;
757 }
758 case 'requestComparison': {
759 const {comparison} = data;
760 const diff: Promise<Result<string>> = repo
761 .runDiff(ctx, comparison)
762 .then(value => ({value}))
763 .catch(error => {
764 logger?.error('error running diff', error.toString());
765 return {error};
766 });
767 diff.then(data =>
768 this.postMessage({
769 type: 'comparison',
770 comparison,
771 data: {diff: data},
772 }),
773 );
774 break;
775 }
776 case 'requestComparisonContextLines': {
777 const {
778 id: {path: relativePath, comparison},
779 // This is the line number in the "before" side of the comparison
780 start,
781 // This is the number of context lines to fetch
782 numLines,
783 } = data;
784
785 const absolutePath = path.join(repo.info.repoRoot, relativePath);
786
787 // TODO: For context lines, before/after sides of the comparison
788 // are identical... except for line numbers.
789 // Typical comparisons with '.' would be much faster (nearly instant)
790 // by reading from the filesystem rather than using cat,
791 // we just need the caller to ask with "after" line numbers instead of "before".
792 // Note: we would still need to fall back to cat for comparisons that do not involve
793 // the working copy.
794 const cat: Promise<string> = repo.cat(
795 ctx,
796 absolutePath,
797 beforeRevsetForComparison(comparison),
798 );
799
800 cat
801 .then(content =>
802 this.postMessage({
803 type: 'comparisonContextLines',
804 lines: {value: content.split('\n').slice(start - 1, start - 1 + numLines)},
805 path: relativePath,
806 }),
807 )
808 .catch((error: Error) =>
809 this.postMessage({
810 type: 'comparisonContextLines',
811 lines: {error},
812 path: relativePath,
813 }),
814 );
815 break;
816 }
817 case 'requestMissedOperationProgress': {
818 const {operationId} = data;
819 this.handleMaybeForgotOperation(operationId, repo);
820 break;
821 }
822 case 'refresh': {
823 logger?.log('refresh requested');
824 repo.fetchAndSetRecommendedBookmarks(bookmarks => {
825 this.postMessage({type: 'fetchedRecommendedBookmarks', bookmarks});
826 });
827 repo.fetchSmartlogCommits();
828 repo.fetchUncommittedChanges();
829 repo.fetchSubmoduleMap();
830 repo.checkForMergeConflicts();
831 repo.fullRepoBranchModule?.pullSubscribedFullRepoBranches();
832 repo.codeReviewProvider?.triggerDiffSummariesFetch(repo.getAllDiffIds());
833 repo.initialConnectionContext.tracker.track('DiffFetchSource', {
834 extras: {source: 'manual_refresh'},
835 });
836 generatedFilesDetector.clear(); // allow generated files to be rechecked
837 break;
838 }
839 case 'pageVisibility': {
840 repo.setPageFocus(this.pageId, data.state);
841 break;
842 }
843 case 'uploadFile': {
844 const {id, filename, b64Content} = data;
845 const payload = base64Decode(b64Content);
846 const uploadFile = Internal.uploadFile;
847 if (uploadFile == null) {
848 return;
849 }
850 this.tracker
851 .operation('UploadImage', 'UploadImageError', {}, () =>
852 uploadFile(this.logger, {filename, data: payload}),
853 )
854 .then((result: string) => {
855 this.logger.info('successfully uploaded file', filename, result);
856 this.postMessage({type: 'uploadFileResult', id, result: {value: result}});
857 })
858 .catch((error: Error) => {
859 this.logger.info('error uploading file', filename, error);
860 this.postMessage({type: 'uploadFileResult', id, result: {error}});
861 });
862 break;
863 }
864 case 'fetchCommitMessageTemplate': {
865 this.handleFetchCommitMessageTemplate(repo, ctx);
866 break;
867 }
868 case 'fetchShelvedChanges': {
869 repo
870 .getShelvedChanges(ctx)
871 .then(shelvedChanges => {
872 this.postMessage({
873 type: 'fetchedShelvedChanges',
874 shelvedChanges: {value: shelvedChanges},
875 });
876 })
877 .catch(err => {
878 logger?.error('Could not fetch shelved changes', err);
879 this.postMessage({type: 'fetchedShelvedChanges', shelvedChanges: {error: err}});
880 });
881 break;
882 }
883 case 'fetchLatestCommit': {
884 repo
885 .lookupCommits(ctx, [data.revset])
886 .then(commits => {
887 const commit = firstOfIterable(commits.values());
888 if (commit == null) {
889 throw new Error(`No commit found for revset ${data.revset}`);
890 }
891 this.postMessage({
892 type: 'fetchedLatestCommit',
893 revset: data.revset,
894 info: {value: commit},
895 });
896 })
897 .catch(err => {
898 this.postMessage({
899 type: 'fetchedLatestCommit',
900 revset: data.revset,
901 info: {error: err as Error},
902 });
903 });
904 break;
905 }
906 case 'fetchPendingSignificantLinesOfCode':
907 {
908 repo
909 .fetchPendingSignificantLinesOfCode(ctx, data.hash, data.includedFiles)
910 .then(value => {
911 this.postMessage({
912 type: 'fetchedPendingSignificantLinesOfCode',
913 requestId: data.requestId,
914 hash: data.hash,
915 result: {value: value ?? 0},
916 });
917 })
918 .catch(err => {
919 this.postMessage({
920 type: 'fetchedPendingSignificantLinesOfCode',
921 hash: data.hash,
922 requestId: data.requestId,
923 result: {error: err as Error},
924 });
925 });
926 }
927 break;
928 case 'fetchSignificantLinesOfCode':
929 {
930 repo
931 .fetchSignificantLinesOfCode(ctx, data.hash, data.excludedFiles)
932 .then(value => {
933 this.postMessage({
934 type: 'fetchedSignificantLinesOfCode',
935 hash: data.hash,
936 result: {value: value ?? 0},
937 });
938 })
939 .catch(err => {
940 this.postMessage({
941 type: 'fetchedSignificantLinesOfCode',
942 hash: data.hash,
943 result: {error: err as Error},
944 });
945 });
946 }
947 break;
948 case 'fetchPendingAmendSignificantLinesOfCode':
949 {
950 repo
951 .fetchPendingAmendSignificantLinesOfCode(ctx, data.hash, data.includedFiles)
952 .then(value => {
953 this.postMessage({
954 type: 'fetchedPendingAmendSignificantLinesOfCode',
955 requestId: data.requestId,
956 hash: data.hash,
957 result: {value: value ?? 0},
958 });
959 })
960 .catch(err => {
961 this.postMessage({
962 type: 'fetchedPendingAmendSignificantLinesOfCode',
963 hash: data.hash,
964 requestId: data.requestId,
965 result: {error: err as Error},
966 });
967 });
968 }
969 break;
970 case 'fetchCommitChangedFiles': {
971 repo
972 .getAllChangedFiles(ctx, data.hash)
973 .then(files => {
974 this.postMessage({
975 type: 'fetchedCommitChangedFiles',
976 hash: data.hash,
977 result: {
978 value: {
979 filesSample: data.limit != null ? files.slice(0, data.limit) : files,
980 totalFileCount: files.length,
981 },
982 },
983 });
984 })
985 .catch(err => {
986 this.postMessage({
987 type: 'fetchedCommitChangedFiles',
988 hash: data.hash,
989 result: {error: err as Error},
990 });
991 });
992 break;
993 }
994 case 'fetchCommitCloudState': {
995 repo.getCommitCloudState(ctx).then(state => {
996 this.postMessage({
997 type: 'fetchedCommitCloudState',
998 state: {value: state},
999 });
1000 });
1001 break;
1002 }
1003 case 'fetchGeneratedStatuses': {
1004 generatedFilesDetector
1005 .queryFilesGenerated(repo, ctx, repo.info.repoRoot, data.paths)
1006 .then(results => {
1007 this.postMessage({type: 'fetchedGeneratedStatuses', results});
1008 });
1009 break;
1010 }
1011 case 'typeahead': {
1012 // Current repo's code review provider should be able to handle all
1013 // TypeaheadKinds for the fields in its defined schema.
1014 repo.codeReviewProvider?.typeahead?.(data.kind, data.query, cwd)?.then(result =>
1015 this.postMessage({
1016 type: 'typeaheadResult',
1017 id: data.id,
1018 result,
1019 }),
1020 );
1021 break;
1022 }
1023 case 'fetchDiffSummaries': {
1024 repo.codeReviewProvider?.triggerDiffSummariesFetch(data.diffIds ?? repo.getAllDiffIds());
1025 break;
1026 }
1027 case 'fetchCanopySignals': {
1028 repo.codeReviewProvider?.triggerCanopySignalsFetch?.();
1029 break;
1030 }
1031 case 'fetchLandInfo': {
1032 repo.codeReviewProvider
1033 ?.fetchLandInfo?.(data.topOfStack)
1034 ?.then((landInfo: LandInfo) => {
1035 this.postMessage({
1036 type: 'fetchedLandInfo',
1037 topOfStack: data.topOfStack,
1038 landInfo: {value: landInfo},
1039 });
1040 })
1041 .catch(err => {
1042 this.postMessage({
1043 type: 'fetchedLandInfo',
1044 topOfStack: data.topOfStack,
1045 landInfo: {error: err as Error},
1046 });
1047 });
1048
1049 break;
1050 }
1051 case 'confirmLand': {
1052 if (data.landConfirmationInfo == null) {
1053 break;
1054 }
1055 repo.codeReviewProvider
1056 ?.confirmLand?.(data.landConfirmationInfo)
1057 ?.then((result: Result<undefined>) => {
1058 this.postMessage({
1059 type: 'confirmedLand',
1060 result,
1061 });
1062 });
1063 break;
1064 }
1065 case 'fetchAvatars': {
1066 repo.codeReviewProvider?.fetchAvatars?.(data.authors)?.then(avatars => {
1067 this.postMessage({
1068 type: 'fetchedAvatars',
1069 avatars,
1070 authors: data.authors,
1071 });
1072 });
1073 break;
1074 }
1075 case 'fetchDiffComments': {
1076 repo.codeReviewProvider
1077 ?.fetchComments?.(data.diffId)
1078 ?.then(comments => {
1079 this.postMessage({
1080 type: 'fetchedDiffComments',
1081 diffId: data.diffId,
1082 comments: {value: comments},
1083 });
1084 })
1085 .catch(error => {
1086 this.postMessage({
1087 type: 'fetchedDiffComments',
1088 diffId: data.diffId,
1089 comments: {error},
1090 });
1091 });
1092 break;
1093 }
1094 case 'renderMarkup': {
1095 repo.codeReviewProvider
1096 ?.renderMarkup?.(data.markup)
1097 ?.then(html => {
1098 this.postMessage({
1099 type: 'renderedMarkup',
1100 id: data.id,
1101 html,
1102 });
1103 })
1104 ?.catch(err => {
1105 this.logger.error('Error rendering markup:', err);
1106 });
1107 break;
1108 }
1109 case 'getSuggestedReviewers': {
1110 repo.codeReviewProvider?.getSuggestedReviewers?.(data.context).then(reviewers => {
1111 this.postMessage({
1112 type: 'gotSuggestedReviewers',
1113 reviewers,
1114 key: data.key,
1115 });
1116 });
1117 break;
1118 }
1119 case 'updateRemoteDiffMessage': {
1120 repo.codeReviewProvider
1121 ?.updateDiffMessage?.(data.diffId, data.title, data.description)
1122 ?.catch(err => err)
1123 ?.then((error: string | undefined) => {
1124 if (error != null) {
1125 this.logger.error('Error updating remote diff message:', error);
1126 }
1127 this.postMessage({type: 'updatedRemoteDiffMessage', diffId: data.diffId, error});
1128 });
1129 break;
1130 }
1131 case 'loadMoreCommits': {
1132 const rangeInDays = repo.nextVisibleCommitRangeInDays();
1133 this.postMessage({type: 'commitsShownRange', rangeInDays});
1134 this.postMessage({type: 'beganLoadingMoreCommits'});
1135 repo.fetchSmartlogCommits();
1136 this.tracker.track('LoadMoreCommits', {extras: {daysToFetch: rangeInDays ?? 'Infinity'}});
1137 return;
1138 }
1139 case 'exportStack': {
1140 const {revs, assumeTracked} = data;
1141 const assumeTrackedArgs = (assumeTracked ?? []).map(path => `--assume-tracked=${path}`);
1142 const exec = repo.runCommand(
1143 ['debugexportstack', '-r', revs, ...assumeTrackedArgs],
1144 'ExportStackCommand',
1145 ctx,
1146 undefined,
1147 /* don't timeout */ 0,
1148 );
1149 const reply = (stack?: ExportStack, error?: string) => {
1150 this.postMessage({
1151 type: 'exportedStack',
1152 assumeTracked: assumeTracked ?? [],
1153 revs,
1154 stack: stack ?? [],
1155 error,
1156 });
1157 };
1158 parseExecJson(exec, reply);
1159 break;
1160 }
1161 case 'importStack': {
1162 const stdinStream = Readable.from(JSON.stringify(data.stack));
1163 const exec = repo.runCommand(
1164 ['debugimportstack'],
1165 'ImportStackCommand',
1166 ctx,
1167 {stdin: stdinStream},
1168 /* don't timeout */ 0,
1169 );
1170 const reply = (imported?: ImportedStack, error?: string) => {
1171 this.postMessage({type: 'importedStack', imported: imported ?? [], error});
1172 };
1173 parseExecJson(exec, reply);
1174 break;
1175 }
1176 case 'fetchQeFlag': {
1177 Internal.fetchQeFlag?.(repo.initialConnectionContext, data.name).then((passes: boolean) => {
1178 this.logger.info(`qe flag ${data.name} ${passes ? 'PASSES' : 'FAILS'}`);
1179 this.postMessage({type: 'fetchedQeFlag', name: data.name, passes});
1180 });
1181 break;
1182 }
1183 case 'fetchFeatureFlag': {
1184 Internal.fetchFeatureFlag?.(repo.initialConnectionContext, data.name).then(
1185 (passes: boolean) => {
1186 this.logger.info(`feature flag ${data.name} ${passes ? 'PASSES' : 'FAILS'}`);
1187 this.postMessage({type: 'fetchedFeatureFlag', name: data.name, passes});
1188 },
1189 );
1190 break;
1191 }
1192 case 'bulkFetchFeatureFlags': {
1193 Internal.bulkFetchFeatureFlags?.(repo.initialConnectionContext, data.names).then(
1194 (result: Record<string, boolean>) => {
1195 this.logger.info(`feature flags ${JSON.stringify(result, null, 2)}`);
1196 this.postMessage({type: 'bulkFetchedFeatureFlags', id: data.id, result});
1197 },
1198 );
1199 break;
1200 }
1201 case 'fetchInternalUserInfo': {
1202 Internal.fetchUserInfo?.(repo.initialConnectionContext).then((info: Serializable) => {
1203 this.logger.info('user info:', info);
1204 this.postMessage({type: 'fetchedInternalUserInfo', info});
1205 });
1206 break;
1207 }
1208 case 'fetchAndSetStables': {
1209 Internal.fetchStableLocations?.(ctx, data.additionalStables).then(
1210 (stables: StableLocationData | undefined) => {
1211 this.logger.info('fetched stable locations', stables);
1212 if (stables == null) {
1213 return;
1214 }
1215 this.postMessage({type: 'fetchedStables', stables});
1216 repo.stableLocations = [
1217 ...stables.stables,
1218 ...stables.special,
1219 ...Object.values(stables.manual),
1220 ]
1221 .map(stable => stable?.value)
1222 .filter(notEmpty);
1223 repo.fetchSmartlogCommits();
1224 },
1225 );
1226 break;
1227 }
1228 case 'fetchStableLocationAutocompleteOptions': {
1229 Internal.fetchStableLocationAutocompleteOptions?.(ctx).then(
1230 (result: Result<Array<TypeaheadResult>>) => {
1231 this.postMessage({type: 'fetchedStableLocationAutocompleteOptions', result});
1232 },
1233 );
1234 break;
1235 }
1236 case 'fetchDevEnvType': {
1237 if (Internal.getDevEnvType == null) {
1238 break;
1239 }
1240
1241 Internal.getDevEnvType()
1242 .catch((error: Error) => {
1243 this.logger.error('Error getting dev env type:', error);
1244 return 'error';
1245 })
1246 .then((result: string) => {
1247 this.postMessage({
1248 type: 'fetchedDevEnvType',
1249 envType: result,
1250 id: data.id,
1251 });
1252 });
1253 break;
1254 }
1255 case 'splitCommitWithAI': {
1256 Internal.splitCommitWithAI?.(ctx, data.diffCommit, data.args).then(
1257 (result: Result<ReadonlyArray<PartiallySelectedDiffCommit>>) => {
1258 this.postMessage({
1259 type: 'splitCommitWithAI',
1260 id: data.id,
1261 result,
1262 });
1263 },
1264 );
1265 break;
1266 }
1267 case 'fetchActiveAlerts': {
1268 repo
1269 .getActiveAlerts(ctx)
1270 .then(alerts => {
1271 if (alerts.length === 0) {
1272 return;
1273 }
1274 this.postMessage({
1275 type: 'fetchedActiveAlerts',
1276 alerts,
1277 });
1278 })
1279 .catch(err => {
1280 this.logger.error('Failed to fetch active alerts:', err);
1281 });
1282 break;
1283 }
1284 case 'gotUiState': {
1285 break;
1286 }
1287 case 'getConfiguredMergeTool': {
1288 repo.getMergeTool(ctx).then((tool: string | null) => {
1289 this.postMessage({
1290 type: 'gotConfiguredMergeTool',
1291 tool: tool ?? undefined,
1292 });
1293 });
1294 break;
1295 }
1296 case 'fetchGkDetails': {
1297 Internal.fetchGkDetails?.(ctx, data.name)
1298 .then((gk: InternalTypes['InternalGatekeeper']) => {
1299 this.postMessage({type: 'fetchedGkDetails', id: data.id, result: {value: gk}});
1300 })
1301 .catch((err: unknown) => {
1302 logger?.error('Could not fetch GK details', err);
1303 this.postMessage({
1304 type: 'fetchedGkDetails',
1305 id: data.id,
1306 result: {error: err as Error},
1307 });
1308 });
1309 break;
1310 }
1311 case 'fetchJkDetails': {
1312 Internal.fetchJustKnobsByNames?.(ctx, data.names)
1313 .then((jk: InternalTypes['InternalJustknob']) => {
1314 this.postMessage({type: 'fetchedJkDetails', id: data.id, result: {value: jk}});
1315 })
1316 .catch((err: unknown) => {
1317 logger?.error('Could not fetch JK details', err);
1318 this.postMessage({
1319 type: 'fetchedJkDetails',
1320 id: data.id,
1321 result: {error: err as Error},
1322 });
1323 });
1324 break;
1325 }
1326 case 'fetchKnobsetDetails': {
1327 Internal.fetchKnobset?.(ctx, data.configPath)
1328 .then((knobset: InternalTypes['InternalKnobset']) => {
1329 this.postMessage({
1330 type: 'fetchedKnobsetDetails',
1331 id: data.id,
1332 result: {value: knobset},
1333 });
1334 })
1335 .catch((err: unknown) => {
1336 logger?.error('Could not fetch knobset details', err);
1337 this.postMessage({
1338 type: 'fetchedKnobsetDetails',
1339 id: data.id,
1340 result: {error: err as Error},
1341 });
1342 });
1343 break;
1344 }
1345 case 'fetchQeDetails': {
1346 Internal.fetchQeMetadata?.(ctx, data.name)
1347 .then((qe: InternalTypes['InternalQuickExperiment']) => {
1348 this.postMessage({
1349 type: 'fetchedQeDetails',
1350 id: data.id,
1351 result: {value: qe},
1352 });
1353 })
1354 .catch((err: unknown) => {
1355 logger?.error('Could not fetch QE details', err);
1356 this.postMessage({
1357 type: 'fetchedQeDetails',
1358 id: data.id,
1359 result: {error: err as Error},
1360 });
1361 });
1362 break;
1363 }
1364 case 'fetchABPropDetails': {
1365 Internal.fetchABPropMetadata?.(ctx, data.name)
1366 .then((abprop: InternalTypes['InternalMetaConfig']) => {
1367 this.postMessage({
1368 type: 'fetchedABPropDetails',
1369 id: data.id,
1370 result: {value: abprop},
1371 });
1372 })
1373 .catch((err: unknown) => {
1374 logger?.error('Could not fetch ABProp details', err);
1375 this.postMessage({
1376 type: 'fetchedABPropDetails',
1377 id: data.id,
1378 result: {error: err as Error},
1379 });
1380 });
1381 break;
1382 }
1383 case 'getRepoUrlAtHash': {
1384 const args = ['url', '--rev', data.revset];
1385 // validate that the path is a valid file in repo
1386 if (data.path != null && absolutePathForFileInRepo(data.path, repo) != null) {
1387 args.push(`path:${data.path}`);
1388 }
1389 repo
1390 .runCommand(args, 'RepoUrlCommand', ctx)
1391 .then(result => {
1392 this.postMessage({
1393 type: 'gotRepoUrlAtHash',
1394 url: {value: result.stdout},
1395 });
1396 })
1397 .catch((err: EjecaError) => {
1398 this.logger.error('Failed to get repo url at hash:', err);
1399 this.postMessage({
1400 type: 'gotRepoUrlAtHash',
1401 url: {error: err},
1402 });
1403 });
1404 break;
1405 }
1406 case 'fetchTaskDetails': {
1407 Internal.getTask?.(ctx, data.taskNumber).then(
1408 (task: InternalTypes['InternalTaskDetails']) => {
1409 this.postMessage({type: 'fetchedTaskDetails', id: data.id, result: {value: task}});
1410 },
1411 );
1412 break;
1413 }
1414 case 'runDevmateCommand': {
1415 Internal.runDevmateCommand?.(data.args, data.cwd)
1416 .then((result: EjecaReturn) => {
1417 this.postMessage({
1418 type: 'devmateCommandResult',
1419 result: {type: 'value', stdout: result.stdout, requestId: data.requestId},
1420 });
1421 })
1422 .catch((error: EjecaError) => {
1423 this.postMessage({
1424 type: 'devmateCommandResult',
1425 result: {type: 'error', stderr: error.stderr, requestId: data.requestId},
1426 });
1427 });
1428 break;
1429 }
1430 case 'fetchSubscribedFullRepoBranches': {
1431 Internal.fetchSubscribedFullRepoBranches?.(ctx, repo)
1432 .then((branches: Array<InternalTypes['FullRepoBranch']>) => {
1433 this.postMessage({
1434 type: 'fetchedSubscribedFullRepoBranches',
1435 result: {value: branches},
1436 });
1437 })
1438 .catch((error: EjecaError) => {
1439 this.postMessage({
1440 type: 'fetchedSubscribedFullRepoBranches',
1441 result: {error},
1442 });
1443 });
1444 break;
1445 }
1446 case 'fetchFullRepoBranchAllChangedFiles': {
1447 Internal.getFullRepoBranchAllChangedFiles?.(ctx, data.fullRepoBranch)
1448 .then((paths: Array<ChangedFile>) => {
1449 this.postMessage({
1450 type: 'fetchedFullRepoBranchAllChangedFiles',
1451 id: data.id,
1452 result: {value: paths},
1453 });
1454 })
1455 .catch((error: EjecaError) => {
1456 this.postMessage({
1457 type: 'fetchedFullRepoBranchAllChangedFiles',
1458 id: data.id,
1459 result: {error},
1460 });
1461 });
1462 break;
1463 }
1464 case 'fetchFullRepoBranchMergeSubtreePaths': {
1465 Internal.getFullRepoBranchMergeSubtreePaths?.(ctx, data.fullRepoBranch, data.paths)
1466 .then((paths: Array<string>) => {
1467 this.postMessage({
1468 type: 'fetchedFullRepoBranchMergeSubtreePaths',
1469 id: data.id,
1470 result: {value: paths},
1471 });
1472 })
1473 .catch((error: EjecaError) => {
1474 this.postMessage({
1475 type: 'fetchedFullRepoBranchMergeSubtreePaths',
1476 id: data.id,
1477 result: {error},
1478 });
1479 });
1480 break;
1481 }
1482 case 'subscribeToFullRepoBranch': {
1483 Internal.subscribeToFullRepoBranch?.(ctx, repo, data.fullRepoBranch);
1484 break;
1485 }
1486 case 'unsubscribeToFullRepoBranch': {
1487 Internal.unsubscribeToFullRepoBranch?.(ctx, repo, data.fullRepoBranch);
1488 break;
1489 }
1490 default: {
1491 if (
1492 repo.codeReviewProvider?.handleClientToServerMessage?.(data, message =>
1493 this.postMessage(message),
1494 ) === true
1495 ) {
1496 break;
1497 }
1498 this.platform.handleMessageFromClient(
1499 repo,
1500 ctx,
1501 data as Exclude<typeof data, CodeReviewProviderSpecificClientToServerMessages>,
1502 message => this.postMessage(message),
1503 (dispose: () => unknown) => {
1504 this.repoDisposables.push({dispose});
1505 },
1506 );
1507 break;
1508 }
1509 }
1510
1511 this.notifyListeners(data);
1512 }
1513
1514 private notifyListeners(data: IncomingMessage): void {
1515 const listeners = this.listenersByType.get(data.type);
1516 if (listeners) {
1517 listeners.forEach(handle => handle(data));
1518 }
1519 }
1520
1521 private async handleFetchCommitMessageTemplate(repo: Repository, ctx: RepositoryContext) {
1522 const {logger} = ctx;
1523 try {
1524 const [result, customTemplate] = await Promise.all([
1525 repo.runCommand(['debugcommitmessage', 'isl'], 'FetchCommitTemplateCommand', ctx),
1526 Internal.getCustomDefaultCommitTemplate?.(repo.initialConnectionContext),
1527 ]);
1528
1529 let template = result.stdout
1530 .replace(repo.IGNORE_COMMIT_MESSAGE_LINES_REGEX, '')
1531 .replace(/^<Replace this line with a title. Use 1 line only, 67 chars or less>/, '');
1532
1533 if (customTemplate && customTemplate?.trim() !== '') {
1534 template = customTemplate as string;
1535
1536 this.tracker.track('UseCustomCommitMessageTemplate');
1537 }
1538
1539 this.postMessage({
1540 type: 'fetchedCommitMessageTemplate',
1541 template,
1542 });
1543 } catch (err) {
1544 logger?.error('Could not fetch commit message template', err);
1545 }
1546 }
1547}
1548