addons/isl-server/src/ServerToClientAPI.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import type {TypeaheadResult} from 'isl-components/Types';
b69ab319import type {Serializable} from 'isl/src/serialize';
b69ab3110import type {
b69ab3111 ChangedFile,
b69ab3112 ClientToServerMessage,
b69ab3113 CodeReviewProviderSpecificClientToServerMessages,
b69ab3114 Disposable,
b69ab3115 FetchedCommits,
b69ab3116 FetchedUncommittedChanges,
b69ab3117 FileABugProgress,
b69ab3118 LandInfo,
b69ab3119 MergeConflicts,
b69ab3120 PlatformSpecificClientToServerMessages,
b69ab3121 RepositoryError,
b69ab3122 Result,
b69ab3123 ServerToClientMessage,
b69ab3124 StableLocationData,
b69ab3125 SubmodulesByRoot,
b69ab3126} from 'isl/src/types';
b69ab3127import type {EjecaError, EjecaReturn} from 'shared/ejeca';
6c9fcae28import {ejeca} from 'shared/ejeca';
b69ab3129import type {ExportStack, ImportedStack} from 'shared/types/stack';
b69ab3130import type {ClientConnection} from '.';
b69ab3131import type {RepositoryReference} from './RepositoryCache';
b69ab3132import type {ServerSideTracker} from './analytics/serverSideTracker';
b69ab3133import type {Logger} from './logger';
b69ab3134import type {ServerPlatform} from './serverPlatform';
b69ab3135import type {RepositoryContext} from './serverTypes';
b69ab3136
b69ab3137import type {InternalTypes} from 'isl/src/InternalTypes';
b69ab3138import {deserializeFromString, serializeToString} from 'isl/src/serialize';
b69ab3139import type {PartiallySelectedDiffCommit} from 'isl/src/stackEdit/diffSplitTypes';
b69ab3140import {Readable} from 'node:stream';
b69ab3141import path from 'path';
b69ab3142import {beforeRevsetForComparison} from 'shared/Comparison';
b69ab3143import {base64Decode, notEmpty, randomId} from 'shared/utils';
b69ab3144import {generatedFilesDetector} from './GeneratedFiles';
b69ab3145import {Internal} from './Internal';
6c9fcae46import {Repository, absolutePathForFileInRepo, readGroveConfig} from './Repository';
b69ab3147import {repositoryCache} from './RepositoryCache';
b69ab3148import {firstOfIterable, parseExecJson} from './utils';
b69ab3149
b69ab3150export type IncomingMessage = ClientToServerMessage;
b69ab3151export type OutgoingMessage = ServerToClientMessage;
b69ab3152
b69ab3153type GeneralMessage = IncomingMessage &
b69ab3154 (
b69ab3155 | {type: 'heartbeat'}
b69ab3156 | {type: 'stress'}
b69ab3157 | {type: 'changeCwd'}
b69ab3158 | {type: 'requestRepoInfo'}
b69ab3159 | {type: 'requestApplicationInfo'}
b69ab3160 | {type: 'fileBugReport'}
b69ab3161 | {type: 'track'}
b69ab3162 | {type: 'clientReady'}
6c9fcae63 | {type: 'fetchGroveOwners'}
6c9fcae64 | {type: 'createGroveRepo'}
b69ab3165 );
b69ab3166type WithRepoMessage = Exclude<IncomingMessage, GeneralMessage>;
b69ab3167
b69ab3168/**
b69ab3169 * Message passing channel built on top of ClientConnection.
b69ab3170 * Use to send and listen for well-typed events with the client
b69ab3171 *
b69ab3172 * Note: you must set the current repository to start sending data back to the client.
b69ab3173 */
b69ab3174export default class ServerToClientAPI {
b69ab3175 private listenersByType = new Map<
b69ab3176 string,
b69ab3177 Set<(message: IncomingMessage) => void | Promise<void>>
b69ab3178 >();
b69ab3179 private incomingListener: Disposable;
b69ab3180
b69ab3181 /** Disposables that must be disposed whenever the current repo is changed */
b69ab3182 private repoDisposables: Array<Disposable> = [];
b69ab3183 private subscriptions = new Map<string, Disposable>();
b69ab3184 private activeRepoRef: RepositoryReference | undefined;
b69ab3185
b69ab3186 private queuedMessages: Array<IncomingMessage> = [];
b69ab3187 private currentState:
b69ab3188 | {type: 'loading'}
b69ab3189 | {type: 'repo'; repo: Repository; ctx: RepositoryContext}
b69ab3190 | {type: 'error'; error: RepositoryError} = {type: 'loading'};
b69ab3191
b69ab3192 private pageId = randomId();
b69ab3193
b69ab3194 constructor(
b69ab3195 private platform: ServerPlatform,
b69ab3196 private connection: ClientConnection,
b69ab3197 private tracker: ServerSideTracker,
b69ab3198 private logger: Logger,
b69ab3199 ) {
b69ab31100 this.incomingListener = this.connection.onDidReceiveMessage(buf => {
b69ab31101 const message = buf.toString('utf-8');
b69ab31102 const data = deserializeFromString(message) as IncomingMessage;
b69ab31103
b69ab31104 // When the client is connected, we want to immediately start listening to messages.
b69ab31105 // However, we can't properly respond to these messages until we have a repository set up.
b69ab31106 // Queue up messages until a repository is set.
b69ab31107 if (this.currentState.type === 'loading') {
b69ab31108 this.queuedMessages.push(data);
b69ab31109 } else {
b69ab31110 try {
b69ab31111 this.handleIncomingMessage(data);
b69ab31112 } catch (err) {
b69ab31113 connection.logger?.error('error handling incoming message: ', data, err);
b69ab31114 }
b69ab31115 }
b69ab31116 });
b69ab31117 }
b69ab31118
b69ab31119 private setRepoError(error: RepositoryError) {
b69ab31120 this.disposeRepoDisposables();
b69ab31121
b69ab31122 this.currentState = {type: 'error', error};
b69ab31123
b69ab31124 this.tracker.context.setRepo(undefined);
b69ab31125
b69ab31126 this.processQueuedMessages();
b69ab31127 }
b69ab31128
b69ab31129 private setCurrentRepo(repo: Repository, ctx: RepositoryContext) {
b69ab31130 this.disposeRepoDisposables();
b69ab31131
b69ab31132 this.currentState = {type: 'repo', repo, ctx};
b69ab31133
b69ab31134 this.tracker.context.setRepo(repo);
b69ab31135
b69ab31136 if (repo.codeReviewProvider != null) {
b69ab31137 this.repoDisposables.push(
b69ab31138 repo.codeReviewProvider.onChangeDiffSummaries(value => {
b69ab31139 this.postMessage({type: 'fetchedDiffSummaries', summaries: value});
b69ab31140 }),
b69ab31141 );
083fd5f142 if (repo.codeReviewProvider.onChangeCanopySignals) {
083fd5f143 this.repoDisposables.push(
083fd5f144 repo.codeReviewProvider.onChangeCanopySignals(runs => {
083fd5f145 this.postMessage({type: 'fetchedCanopySignals', runs});
083fd5f146 }),
083fd5f147 );
083fd5f148 }
b69ab31149 }
b69ab31150
b69ab31151 repo.ref();
b69ab31152 this.repoDisposables.push({dispose: () => repo.unref()});
b69ab31153
4fe1f34154 // Send initial watchman status and subscribe to changes
4fe1f34155 this.postMessage({type: 'watchmanStatus', status: repo.watchForChanges.watchman.status});
4fe1f34156 repo.watchForChanges.onWatchmanStatusChange = status => {
4fe1f34157 this.postMessage({type: 'watchmanStatus', status});
4fe1f34158 };
4fe1f34159 this.repoDisposables.push({dispose: () => {
4fe1f34160 repo.watchForChanges.onWatchmanStatusChange = undefined;
4fe1f34161 }});
4fe1f34162
b69ab31163 repo.fetchAndSetRecommendedBookmarks(async bookmarks => {
b69ab31164 await this.connection.readySignal?.promise;
b69ab31165 this.postMessage({type: 'fetchedRecommendedBookmarks', bookmarks});
b69ab31166 });
b69ab31167
b69ab31168 repo.fetchAndSetHiddenMasterConfig(async (config, odType) => {
b69ab31169 await this.connection.readySignal?.promise;
b69ab31170 this.postMessage({
b69ab31171 type: 'fetchedHiddenMasterBranchConfig',
b69ab31172 config,
b69ab31173 odType,
b69ab31174 cwd: ctx.cwd,
b69ab31175 });
b69ab31176 });
b69ab31177
b69ab31178 this.processQueuedMessages();
b69ab31179 }
b69ab31180
b69ab31181 postMessage(message: OutgoingMessage) {
b69ab31182 this.connection.postMessage(serializeToString(message));
b69ab31183 }
b69ab31184
b69ab31185 /** Get a repository reference for a given cwd, and set that as the active repo. */
b69ab31186 setActiveRepoForCwd(newCwd: string) {
b69ab31187 if (this.activeRepoRef !== undefined) {
b69ab31188 this.activeRepoRef.unref();
b69ab31189 }
b69ab31190 this.logger.info(`Setting active repo cwd to ${newCwd}`);
b69ab31191 // Set as loading right away while we determine the new cwd's repo
b69ab31192 // This ensures new messages coming in will be queued and handled only with the new repository
b69ab31193 this.currentState = {type: 'loading'};
b69ab31194 const command = this.connection.command ?? 'sl';
b69ab31195 const ctx: RepositoryContext = {
b69ab31196 cwd: newCwd,
b69ab31197 cmd: command,
b69ab31198 logger: this.logger,
b69ab31199 tracker: this.tracker,
b69ab31200 };
b69ab31201 this.activeRepoRef = repositoryCache.getOrCreate(ctx);
b69ab31202 this.activeRepoRef.promise.then(repoOrError => {
b69ab31203 if (repoOrError instanceof Repository) {
b69ab31204 this.setCurrentRepo(repoOrError, ctx);
b69ab31205 } else {
b69ab31206 this.setRepoError(repoOrError);
b69ab31207 }
b69ab31208 });
b69ab31209 }
b69ab31210
b69ab31211 dispose() {
b69ab31212 this.incomingListener.dispose();
b69ab31213 this.disposeRepoDisposables();
b69ab31214
b69ab31215 if (this.activeRepoRef !== undefined) {
b69ab31216 this.activeRepoRef.unref();
b69ab31217 }
b69ab31218 }
b69ab31219
b69ab31220 private disposeRepoDisposables() {
b69ab31221 this.repoDisposables.forEach(disposable => disposable.dispose());
b69ab31222 this.repoDisposables = [];
b69ab31223
b69ab31224 this.subscriptions.forEach(sub => sub.dispose());
b69ab31225 this.subscriptions.clear();
b69ab31226 }
b69ab31227
b69ab31228 private processQueuedMessages() {
b69ab31229 for (const message of this.queuedMessages) {
b69ab31230 try {
b69ab31231 this.handleIncomingMessage(message);
b69ab31232 } catch (err) {
b69ab31233 this.connection.logger?.error('error handling queued message: ', message, err);
b69ab31234 }
b69ab31235 }
b69ab31236 this.queuedMessages = [];
b69ab31237 }
b69ab31238
b69ab31239 private handleIncomingMessage(data: IncomingMessage) {
b69ab31240 this.handleIncomingGeneralMessage(data as GeneralMessage);
b69ab31241 const {currentState} = this;
b69ab31242 switch (currentState.type) {
b69ab31243 case 'repo': {
b69ab31244 const {repo, ctx} = currentState;
b69ab31245 this.handleIncomingMessageWithRepo(data as WithRepoMessage, repo, ctx);
b69ab31246 break;
b69ab31247 }
b69ab31248
b69ab31249 // If the repo is in the loading or error state, the client may still send
b69ab31250 // platform messages such as `platform/openExternal` that should be processed.
b69ab31251 case 'loading':
b69ab31252 case 'error':
b69ab31253 if (data.type.startsWith('platform/')) {
b69ab31254 this.platform.handleMessageFromClient(
b69ab31255 /*repo=*/ undefined,
b69ab31256 // even if we don't have a repo, we can still make a RepositoryContext to execute commands
b69ab31257 {
b69ab31258 cwd: this.connection.cwd,
b69ab31259 cmd: this.connection.command ?? 'sl',
b69ab31260 logger: this.logger,
b69ab31261 tracker: this.tracker,
b69ab31262 },
b69ab31263 data as PlatformSpecificClientToServerMessages,
b69ab31264 message => this.postMessage(message),
b69ab31265 (dispose: () => unknown) => {
b69ab31266 this.repoDisposables.push({dispose});
b69ab31267 },
b69ab31268 );
b69ab31269 this.notifyListeners(data);
b69ab31270 }
b69ab31271 break;
b69ab31272 }
b69ab31273 }
b69ab31274
b69ab31275 /**
b69ab31276 * Handle messages which can be handled regardless of if a repo was successfully created or not
b69ab31277 */
b69ab31278 private handleIncomingGeneralMessage(data: GeneralMessage) {
b69ab31279 switch (data.type) {
b69ab31280 case 'heartbeat': {
b69ab31281 this.postMessage({type: 'heartbeat', id: data.id});
b69ab31282 break;
b69ab31283 }
b69ab31284 case 'stress': {
b69ab31285 this.postMessage(data);
b69ab31286 break;
b69ab31287 }
b69ab31288 case 'track': {
b69ab31289 this.tracker.trackData(data.data);
b69ab31290 break;
b69ab31291 }
b69ab31292 case 'clientReady': {
b69ab31293 this.connection.readySignal?.resolve();
b69ab31294 break;
b69ab31295 }
b69ab31296 case 'changeCwd': {
b69ab31297 this.setActiveRepoForCwd(data.cwd);
b69ab31298 break;
b69ab31299 }
b69ab31300 case 'requestRepoInfo': {
b69ab31301 switch (this.currentState.type) {
b69ab31302 case 'repo':
b69ab31303 this.postMessage({
b69ab31304 type: 'repoInfo',
b69ab31305 info: this.currentState.repo.info,
b69ab31306 cwd: this.currentState.ctx.cwd,
b69ab31307 });
b69ab31308 break;
b69ab31309 case 'error':
b69ab31310 this.postMessage({type: 'repoInfo', info: this.currentState.error});
b69ab31311 break;
b69ab31312 }
b69ab31313 break;
b69ab31314 }
b69ab31315 case 'requestApplicationInfo': {
b69ab31316 this.postMessage({
b69ab31317 type: 'applicationInfo',
b69ab31318 info: {
b69ab31319 platformName: this.platform.platformName,
b69ab31320 version: this.connection.version,
b69ab31321 logFilePath: this.connection.logFileLocation ?? '(no log file, logging to stdout)',
b69ab31322 },
b69ab31323 });
b69ab31324 break;
b69ab31325 }
b69ab31326 case 'fileBugReport': {
b69ab31327 const maybeRepo = this.currentState.type === 'repo' ? this.currentState.repo : undefined;
b69ab31328 const ctx: RepositoryContext =
b69ab31329 this.currentState.type === 'repo'
b69ab31330 ? this.currentState.ctx
b69ab31331 : {
b69ab31332 // cwd is only needed to run graphql query, here it's just best-effort
b69ab31333 cwd: maybeRepo?.initialConnectionContext.cwd ?? process.cwd(),
b69ab31334 cmd: this.connection.command ?? 'sl',
b69ab31335 logger: this.logger,
b69ab31336 tracker: this.tracker,
b69ab31337 };
b69ab31338 Internal.fileABug?.(
b69ab31339 ctx,
b69ab31340 this.platform.platformName,
b69ab31341 data.data,
b69ab31342 data.uiState,
b69ab31343 // Use repo for rage, if available.
b69ab31344 maybeRepo,
b69ab31345 data.collectRage,
b69ab31346 (progress: FileABugProgress) => {
b69ab31347 this.connection.logger?.info('file a bug progress: ', JSON.stringify(progress));
b69ab31348 this.postMessage({type: 'fileBugReportProgress', ...progress});
b69ab31349 },
b69ab31350 );
b69ab31351 break;
b69ab31352 }
6c9fcae353 case 'fetchGroveOwners': {
6c9fcae354 const groveConfig = readGroveConfig(this.logger);
6c9fcae355 if (!groveConfig.hub || !groveConfig.token) {
6c9fcae356 this.postMessage({type: 'fetchedGroveOwners', owners: []});
6c9fcae357 break;
6c9fcae358 }
6c9fcae359 (async () => {
6c9fcae360 try {
6c9fcae361 const owners: Array<{name: string; type: 'user' | 'org'}> = [];
6c9fcae362 if (groveConfig.username) {
6c9fcae363 owners.push({name: groveConfig.username, type: 'user'});
6c9fcae364 }
6c9fcae365 const res = await fetch(`${groveConfig.hub}/api/orgs`, {
6c9fcae366 headers: {Authorization: `Bearer ${groveConfig.token}`},
6c9fcae367 });
6c9fcae368 if (res.ok) {
6c9fcae369 const data = (await res.json()) as {orgs: Array<{name: string}>};
6c9fcae370 for (const org of data.orgs) {
6c9fcae371 owners.push({name: org.name, type: 'org'});
6c9fcae372 }
6c9fcae373 }
6c9fcae374 this.postMessage({type: 'fetchedGroveOwners', owners});
6c9fcae375 } catch (err) {
6c9fcae376 this.logger.error('Failed to fetch Grove owners:', err);
6c9fcae377 this.postMessage({type: 'fetchedGroveOwners', owners: []});
6c9fcae378 }
6c9fcae379 })();
6c9fcae380 break;
6c9fcae381 }
6c9fcae382 case 'createGroveRepo': {
6c9fcae383 const groveConfig = readGroveConfig(this.logger);
6c9fcae384 if (!groveConfig.hub || !groveConfig.token) {
6c9fcae385 this.postMessage({
6c9fcae386 type: 'createdGroveRepo',
6c9fcae387 result: {error: new Error('Not logged in to Grove. Run `grove auth login` first.')},
6c9fcae388 });
6c9fcae389 break;
6c9fcae390 }
6c9fcae391 const owner = data.owner ?? groveConfig.username;
6c9fcae392 if (!owner) {
6c9fcae393 this.postMessage({
6c9fcae394 type: 'createdGroveRepo',
6c9fcae395 result: {error: new Error('No owner specified and no username found in Grove config.')},
6c9fcae396 });
6c9fcae397 break;
6c9fcae398 }
6c9fcae399 const repoName = data.name;
6c9fcae400 (async () => {
6c9fcae401 try {
6c9fcae402 // Create the repo on Grove
6c9fcae403 const res = await fetch(`${groveConfig.hub}/api/repos`, {
6c9fcae404 method: 'POST',
6c9fcae405 headers: {
6c9fcae406 Authorization: `Bearer ${groveConfig.token}`,
6c9fcae407 'Content-Type': 'application/json',
6c9fcae408 },
6c9fcae409 body: JSON.stringify({name: repoName, owner}),
6c9fcae410 });
6c9fcae411 if (!res.ok) {
6c9fcae412 const body = await res.text();
6c9fcae413 throw new Error(`Failed to create repository: ${res.status} ${body}`);
6c9fcae414 }
6c9fcae415
6c9fcae416 const cwd = this.connection.cwd;
6c9fcae417 const cmd = this.connection.command ?? 'sl';
6c9fcae418 const homedir = process.env.HOME ?? process.env.USERPROFILE ?? '';
0542c45419
0542c45420 // If sl init created a git-backed repo, re-init with correct format
0542c45421 const fs = await import('fs');
0542c45422 const storeRequires = path.join(cwd, '.sl', 'store', 'requires');
0542c45423 try {
0542c45424 const requires = fs.readFileSync(storeRequires, 'utf8');
0542c45425 if (requires.includes('git')) {
0542c45426 this.logger.info(
0542c45427 'Re-initializing repo: current store is git-backed, need native format for Grove',
0542c45428 );
0542c45429 fs.rmSync(path.join(cwd, '.sl'), {recursive: true, force: true});
0542c45430 await ejeca(
0542c45431 cmd,
0542c45432 [
0542c45433 'init',
0542c45434 '--config',
0542c45435 'init.prefer-git=false',
0542c45436 '--config',
0542c45437 'format.use-remotefilelog=true',
0542c45438 cwd,
0542c45439 ],
0542c45440 {cwd},
0542c45441 );
0542c45442 }
0542c45443 } catch {
0542c45444 // No .sl/store/requires means no repo yet — init fresh
0542c45445 if (!fs.existsSync(path.join(cwd, '.sl'))) {
0542c45446 await ejeca(
0542c45447 cmd,
0542c45448 [
0542c45449 'init',
0542c45450 '--config',
0542c45451 'init.prefer-git=false',
0542c45452 '--config',
0542c45453 'format.use-remotefilelog=true',
0542c45454 cwd,
0542c45455 ],
0542c45456 {cwd},
0542c45457 );
0542c45458 }
0542c45459 }
0542c45460
0542c45461 // Configure sapling remote
6c9fcae462 const configPairs = [
6c9fcae463 ['paths.default', `mononoke://grove.host:8443/${repoName}`],
6c9fcae464 ['remotefilelog.reponame', repoName],
6c9fcae465 ['grove.owner', owner],
6c9fcae466 ['edenapi.url', 'https://grove.host:8443/edenapi/'],
6c9fcae467 ['web.cacerts', `${homedir}/.grove/tls/ca.crt`],
6c9fcae468 ['auth.grove.prefix', 'https://grove.host'],
6c9fcae469 ['auth.grove.cert', `${homedir}/.grove/tls/client.crt`],
6c9fcae470 ['auth.grove.key', `${homedir}/.grove/tls/client.key`],
6c9fcae471 ['auth.grove.cacerts', `${homedir}/.grove/tls/ca.crt`],
6c9fcae472 ['auth.grove-mononoke.prefix', 'mononoke://grove.host'],
6c9fcae473 ['auth.grove-mononoke.cert', `${homedir}/.grove/tls/client.crt`],
6c9fcae474 ['auth.grove-mononoke.key', `${homedir}/.grove/tls/client.key`],
6c9fcae475 ['auth.grove-mononoke.cacerts', `${homedir}/.grove/tls/ca.crt`],
6c9fcae476 ['clone.use-commit-graph', 'true'],
6c9fcae477 ['remotenames.selectivepulldefault', 'main'],
6c9fcae478 ['push.edenapi', 'true'],
6c9fcae479 ['push.to', 'main'],
6c9fcae480 ];
6c9fcae481 for (const [key, value] of configPairs) {
6c9fcae482 await ejeca(cmd, ['config', '--local', key, value], {cwd});
6c9fcae483 }
6c9fcae484
0542c45485 // Pull the seed commit and check out main
0542c45486 try {
0542c45487 await ejeca(cmd, ['pull'], {cwd});
0542c45488 await ejeca(cmd, ['goto', 'main'], {cwd});
0542c45489 } catch (pullErr) {
0542c45490 this.logger.info('Pull after repo creation failed (may not be seeded yet):', pullErr);
0542c45491 }
0542c45492
6c9fcae493 // Re-detect repo info and send it to the client
6c9fcae494 const ctx = {
6c9fcae495 cwd,
6c9fcae496 cmd,
6c9fcae497 logger: this.logger,
6c9fcae498 tracker: this.tracker,
6c9fcae499 };
6c9fcae500 const newInfo = await Repository.getRepoInfo(ctx);
6c9fcae501 this.postMessage({type: 'repoInfo', info: newInfo, cwd});
6c9fcae502
6c9fcae503 // Also re-initialize the repo in the cache so subscriptions work
6c9fcae504 this.setActiveRepoForCwd(cwd);
6c9fcae505
6c9fcae506 this.postMessage({
6c9fcae507 type: 'createdGroveRepo',
6c9fcae508 result: {value: {owner, repo: repoName}},
6c9fcae509 });
6c9fcae510 } catch (err) {
6c9fcae511 this.logger.error('Failed to create Grove repo:', err);
6c9fcae512 this.postMessage({
6c9fcae513 type: 'createdGroveRepo',
6c9fcae514 result: {error: err instanceof Error ? err : new Error(String(err))},
6c9fcae515 });
6c9fcae516 }
6c9fcae517 })();
6c9fcae518 break;
6c9fcae519 }
b69ab31520 }
b69ab31521 }
b69ab31522
b69ab31523 private handleMaybeForgotOperation(operationId: string, repo: Repository) {
b69ab31524 if (repo.getRunningOperation()?.id !== operationId) {
b69ab31525 this.postMessage({type: 'operationProgress', id: operationId, kind: 'forgot'});
b69ab31526 }
b69ab31527 }
b69ab31528
b69ab31529 /**
b69ab31530 * Handle messages which require a repository to have been successfully set up to run
b69ab31531 */
b69ab31532 private handleIncomingMessageWithRepo(
b69ab31533 data: WithRepoMessage,
b69ab31534 repo: Repository,
b69ab31535 ctx: RepositoryContext,
b69ab31536 ) {
b69ab31537 const {cwd, logger} = ctx;
b69ab31538 switch (data.type) {
b69ab31539 case 'subscribe': {
b69ab31540 const {subscriptionID, kind} = data;
b69ab31541 switch (kind) {
b69ab31542 case 'uncommittedChanges': {
b69ab31543 const postUncommittedChanges = (result: FetchedUncommittedChanges) => {
b69ab31544 this.postMessage({
b69ab31545 type: 'subscriptionResult',
b69ab31546 kind: 'uncommittedChanges',
b69ab31547 subscriptionID,
b69ab31548 data: result,
b69ab31549 });
b69ab31550 };
b69ab31551
b69ab31552 const uncommittedChanges = repo.getUncommittedChanges();
b69ab31553 if (uncommittedChanges != null) {
b69ab31554 postUncommittedChanges(uncommittedChanges);
b69ab31555 }
b69ab31556 const disposables: Array<Disposable> = [];
b69ab31557
b69ab31558 // send changes as they come in from watchman
b69ab31559 disposables.push(repo.subscribeToUncommittedChanges(postUncommittedChanges));
b69ab31560 // trigger a fetch on startup
b69ab31561 repo.fetchUncommittedChanges();
b69ab31562
b69ab31563 disposables.push(
b69ab31564 repo.subscribeToUncommittedChangesBeginFetching(() =>
b69ab31565 this.postMessage({type: 'beganFetchingUncommittedChangesEvent'}),
b69ab31566 ),
b69ab31567 );
b69ab31568 this.subscriptions.set(subscriptionID, {
b69ab31569 dispose: () => {
b69ab31570 disposables.forEach(d => d.dispose());
b69ab31571 },
b69ab31572 });
b69ab31573 break;
b69ab31574 }
b69ab31575 case 'smartlogCommits': {
b69ab31576 const postSmartlogCommits = (result: FetchedCommits) => {
b69ab31577 this.postMessage({
b69ab31578 type: 'subscriptionResult',
b69ab31579 kind: 'smartlogCommits',
b69ab31580 subscriptionID,
b69ab31581 data: result,
b69ab31582 });
b69ab31583 };
b69ab31584
b69ab31585 const smartlogCommits = repo.getSmartlogCommits();
b69ab31586 if (smartlogCommits != null) {
b69ab31587 postSmartlogCommits(smartlogCommits);
b69ab31588 }
b69ab31589 const disposables: Array<Disposable> = [];
b69ab31590 // send changes as they come from file watcher
b69ab31591 disposables.push(repo.subscribeToSmartlogCommitsChanges(postSmartlogCommits));
b69ab31592
b69ab31593 // trigger fetch on startup
b69ab31594 repo.fetchSmartlogCommits();
b69ab31595
b69ab31596 disposables.push(
b69ab31597 repo.subscribeToSmartlogCommitsBeginFetching(() =>
b69ab31598 this.postMessage({type: 'beganFetchingSmartlogCommitsEvent'}),
b69ab31599 ),
b69ab31600 );
b69ab31601
b69ab31602 this.subscriptions.set(subscriptionID, {
b69ab31603 dispose: () => {
b69ab31604 disposables.forEach(d => d.dispose());
b69ab31605 },
b69ab31606 });
b69ab31607 break;
b69ab31608 }
b69ab31609 case 'mergeConflicts': {
b69ab31610 const postMergeConflicts = (conflicts: MergeConflicts | undefined) => {
b69ab31611 this.postMessage({
b69ab31612 type: 'subscriptionResult',
b69ab31613 kind: 'mergeConflicts',
b69ab31614 subscriptionID,
b69ab31615 data: conflicts,
b69ab31616 });
b69ab31617 };
b69ab31618
b69ab31619 const mergeConflicts = repo.getMergeConflicts();
b69ab31620 if (mergeConflicts != null) {
b69ab31621 postMergeConflicts(mergeConflicts);
b69ab31622 }
b69ab31623
b69ab31624 this.subscriptions.set(subscriptionID, repo.onChangeConflictState(postMergeConflicts));
b69ab31625 break;
b69ab31626 }
b69ab31627 case 'submodules': {
b69ab31628 const postSubmodules = (submodulesByRoot: SubmodulesByRoot) => {
b69ab31629 this.postMessage({
b69ab31630 type: 'subscriptionResult',
b69ab31631 kind: 'submodules',
b69ab31632 subscriptionID,
b69ab31633 data: submodulesByRoot,
b69ab31634 });
b69ab31635 };
b69ab31636 const submoduleMap = repo.getSubmoduleMap();
b69ab31637 if (submoduleMap !== undefined) {
b69ab31638 postSubmodules(submoduleMap);
b69ab31639 }
b69ab31640 repo.fetchSubmoduleMap();
b69ab31641
b69ab31642 const disposable = repo.subscribeToSubmodulesChanges(postSubmodules);
b69ab31643 this.subscriptions.set(subscriptionID, {
b69ab31644 dispose: () => {
b69ab31645 disposable.dispose();
b69ab31646 },
b69ab31647 });
b69ab31648 break;
b69ab31649 }
b69ab31650 case 'subscribedFullRepoBranches': {
b69ab31651 const fullRepoBranchModule = repo.fullRepoBranchModule;
b69ab31652 if (fullRepoBranchModule == null) {
b69ab31653 return;
b69ab31654 }
b69ab31655
b69ab31656 const postSubscribedFullRepoBranches = (
b69ab31657 result: Array<InternalTypes['FullRepoBranch']>,
b69ab31658 ) => {
b69ab31659 this.postMessage({
b69ab31660 type: 'subscriptionResult',
b69ab31661 kind: 'subscribedFullRepoBranches',
b69ab31662 subscriptionID,
b69ab31663 data: result,
b69ab31664 });
b69ab31665 };
b69ab31666
b69ab31667 const subscribedFullRepoBranches = fullRepoBranchModule.getSubscribedFullRepoBranches();
b69ab31668 if (subscribedFullRepoBranches != null) {
b69ab31669 postSubscribedFullRepoBranches(subscribedFullRepoBranches);
b69ab31670 }
b69ab31671 const disposables: Array<Disposable> = [];
b69ab31672 // send changes as they come from file watcher
b69ab31673 disposables.push(
b69ab31674 fullRepoBranchModule.subscribeToSubscribedFullRepoBranchesChanges(
b69ab31675 postSubscribedFullRepoBranches,
b69ab31676 ),
b69ab31677 );
b69ab31678 // trigger a fetch on startup
b69ab31679 fullRepoBranchModule.pullSubscribedFullRepoBranches();
b69ab31680
b69ab31681 disposables.push(
b69ab31682 fullRepoBranchModule.subscribeToSubscribedFullRepoBranchesBeginFetching(() =>
b69ab31683 this.postMessage({type: 'beganFetchingSubscribedFullRepoBranchesEvent'}),
b69ab31684 ),
b69ab31685 );
b69ab31686
b69ab31687 this.subscriptions.set(subscriptionID, {
b69ab31688 dispose: () => {
b69ab31689 disposables.forEach(d => d.dispose());
b69ab31690 },
b69ab31691 });
b69ab31692 break;
b69ab31693 }
b69ab31694 }
b69ab31695 break;
b69ab31696 }
b69ab31697 case 'unsubscribe': {
b69ab31698 const subscription = this.subscriptions.get(data.subscriptionID);
b69ab31699 subscription?.dispose();
b69ab31700 this.subscriptions.delete(data.subscriptionID);
b69ab31701 break;
b69ab31702 }
b69ab31703 case 'runOperation': {
b69ab31704 const {operation} = data;
b69ab31705 repo.runOrQueueOperation(ctx, operation, progress => {
b69ab31706 this.postMessage({type: 'operationProgress', ...progress});
b69ab31707 if (progress.kind === 'queue') {
b69ab31708 this.tracker.track('QueueOperation', {extras: {operation: operation.trackEventName}});
b69ab31709 }
b69ab31710 });
b69ab31711 break;
b69ab31712 }
b69ab31713 case 'abortRunningOperation': {
b69ab31714 const {operationId} = data;
b69ab31715 repo.abortRunningOperation(operationId);
b69ab31716 this.handleMaybeForgotOperation(operationId, repo);
b69ab31717 break;
b69ab31718 }
b69ab31719 case 'getConfig': {
b69ab31720 repo
b69ab31721 .getConfig(ctx, data.name)
b69ab31722 .catch(() => undefined)
b69ab31723 .then(value => {
b69ab31724 logger.info('got config', data.name, value);
b69ab31725 this.postMessage({type: 'gotConfig', name: data.name, value});
b69ab31726 });
b69ab31727 break;
b69ab31728 }
b69ab31729 case 'setConfig': {
b69ab31730 logger.info('set config', data.name, data.value);
b69ab31731 repo.setConfig(ctx, 'user', data.name, data.value).catch(err => {
b69ab31732 logger.error('error setting config', data.name, data.value, err);
b69ab31733 });
b69ab31734 break;
b69ab31735 }
b69ab31736 case 'setDebugLogging': {
b69ab31737 logger.info('set debug', data.name, data.enabled);
b69ab31738 if (data.name === 'debug' || data.name === 'verbose') {
b69ab31739 ctx[data.name] = !!data.enabled;
b69ab31740 }
b69ab31741 break;
b69ab31742 }
b69ab31743 case 'requestComparison': {
b69ab31744 const {comparison} = data;
b69ab31745 const diff: Promise<Result<string>> = repo
b69ab31746 .runDiff(ctx, comparison)
b69ab31747 .then(value => ({value}))
b69ab31748 .catch(error => {
b69ab31749 logger?.error('error running diff', error.toString());
b69ab31750 return {error};
b69ab31751 });
b69ab31752 diff.then(data =>
b69ab31753 this.postMessage({
b69ab31754 type: 'comparison',
b69ab31755 comparison,
b69ab31756 data: {diff: data},
b69ab31757 }),
b69ab31758 );
b69ab31759 break;
b69ab31760 }
b69ab31761 case 'requestComparisonContextLines': {
b69ab31762 const {
b69ab31763 id: {path: relativePath, comparison},
b69ab31764 // This is the line number in the "before" side of the comparison
b69ab31765 start,
b69ab31766 // This is the number of context lines to fetch
b69ab31767 numLines,
b69ab31768 } = data;
b69ab31769
b69ab31770 const absolutePath = path.join(repo.info.repoRoot, relativePath);
b69ab31771
b69ab31772 // TODO: For context lines, before/after sides of the comparison
b69ab31773 // are identical... except for line numbers.
b69ab31774 // Typical comparisons with '.' would be much faster (nearly instant)
b69ab31775 // by reading from the filesystem rather than using cat,
b69ab31776 // we just need the caller to ask with "after" line numbers instead of "before".
b69ab31777 // Note: we would still need to fall back to cat for comparisons that do not involve
b69ab31778 // the working copy.
b69ab31779 const cat: Promise<string> = repo.cat(
b69ab31780 ctx,
b69ab31781 absolutePath,
b69ab31782 beforeRevsetForComparison(comparison),
b69ab31783 );
b69ab31784
b69ab31785 cat
b69ab31786 .then(content =>
b69ab31787 this.postMessage({
b69ab31788 type: 'comparisonContextLines',
b69ab31789 lines: {value: content.split('\n').slice(start - 1, start - 1 + numLines)},
b69ab31790 path: relativePath,
b69ab31791 }),
b69ab31792 )
b69ab31793 .catch((error: Error) =>
b69ab31794 this.postMessage({
b69ab31795 type: 'comparisonContextLines',
b69ab31796 lines: {error},
b69ab31797 path: relativePath,
b69ab31798 }),
b69ab31799 );
b69ab31800 break;
b69ab31801 }
b69ab31802 case 'requestMissedOperationProgress': {
b69ab31803 const {operationId} = data;
b69ab31804 this.handleMaybeForgotOperation(operationId, repo);
b69ab31805 break;
b69ab31806 }
b69ab31807 case 'refresh': {
b69ab31808 logger?.log('refresh requested');
b69ab31809 repo.fetchAndSetRecommendedBookmarks(bookmarks => {
b69ab31810 this.postMessage({type: 'fetchedRecommendedBookmarks', bookmarks});
b69ab31811 });
b69ab31812 repo.fetchSmartlogCommits();
b69ab31813 repo.fetchUncommittedChanges();
b69ab31814 repo.fetchSubmoduleMap();
b69ab31815 repo.checkForMergeConflicts();
b69ab31816 repo.fullRepoBranchModule?.pullSubscribedFullRepoBranches();
b69ab31817 repo.codeReviewProvider?.triggerDiffSummariesFetch(repo.getAllDiffIds());
b69ab31818 repo.initialConnectionContext.tracker.track('DiffFetchSource', {
b69ab31819 extras: {source: 'manual_refresh'},
b69ab31820 });
b69ab31821 generatedFilesDetector.clear(); // allow generated files to be rechecked
b69ab31822 break;
b69ab31823 }
b69ab31824 case 'pageVisibility': {
b69ab31825 repo.setPageFocus(this.pageId, data.state);
b69ab31826 break;
b69ab31827 }
b69ab31828 case 'uploadFile': {
b69ab31829 const {id, filename, b64Content} = data;
b69ab31830 const payload = base64Decode(b64Content);
b69ab31831 const uploadFile = Internal.uploadFile;
b69ab31832 if (uploadFile == null) {
b69ab31833 return;
b69ab31834 }
b69ab31835 this.tracker
b69ab31836 .operation('UploadImage', 'UploadImageError', {}, () =>
b69ab31837 uploadFile(this.logger, {filename, data: payload}),
b69ab31838 )
b69ab31839 .then((result: string) => {
b69ab31840 this.logger.info('successfully uploaded file', filename, result);
b69ab31841 this.postMessage({type: 'uploadFileResult', id, result: {value: result}});
b69ab31842 })
b69ab31843 .catch((error: Error) => {
b69ab31844 this.logger.info('error uploading file', filename, error);
b69ab31845 this.postMessage({type: 'uploadFileResult', id, result: {error}});
b69ab31846 });
b69ab31847 break;
b69ab31848 }
b69ab31849 case 'fetchCommitMessageTemplate': {
b69ab31850 this.handleFetchCommitMessageTemplate(repo, ctx);
b69ab31851 break;
b69ab31852 }
b69ab31853 case 'fetchShelvedChanges': {
b69ab31854 repo
b69ab31855 .getShelvedChanges(ctx)
b69ab31856 .then(shelvedChanges => {
b69ab31857 this.postMessage({
b69ab31858 type: 'fetchedShelvedChanges',
b69ab31859 shelvedChanges: {value: shelvedChanges},
b69ab31860 });
b69ab31861 })
b69ab31862 .catch(err => {
b69ab31863 logger?.error('Could not fetch shelved changes', err);
b69ab31864 this.postMessage({type: 'fetchedShelvedChanges', shelvedChanges: {error: err}});
b69ab31865 });
b69ab31866 break;
b69ab31867 }
b69ab31868 case 'fetchLatestCommit': {
b69ab31869 repo
b69ab31870 .lookupCommits(ctx, [data.revset])
b69ab31871 .then(commits => {
b69ab31872 const commit = firstOfIterable(commits.values());
b69ab31873 if (commit == null) {
b69ab31874 throw new Error(`No commit found for revset ${data.revset}`);
b69ab31875 }
b69ab31876 this.postMessage({
b69ab31877 type: 'fetchedLatestCommit',
b69ab31878 revset: data.revset,
b69ab31879 info: {value: commit},
b69ab31880 });
b69ab31881 })
b69ab31882 .catch(err => {
b69ab31883 this.postMessage({
b69ab31884 type: 'fetchedLatestCommit',
b69ab31885 revset: data.revset,
b69ab31886 info: {error: err as Error},
b69ab31887 });
b69ab31888 });
b69ab31889 break;
b69ab31890 }
b69ab31891 case 'fetchPendingSignificantLinesOfCode':
b69ab31892 {
b69ab31893 repo
b69ab31894 .fetchPendingSignificantLinesOfCode(ctx, data.hash, data.includedFiles)
b69ab31895 .then(value => {
b69ab31896 this.postMessage({
b69ab31897 type: 'fetchedPendingSignificantLinesOfCode',
b69ab31898 requestId: data.requestId,
b69ab31899 hash: data.hash,
b69ab31900 result: {value: value ?? 0},
b69ab31901 });
b69ab31902 })
b69ab31903 .catch(err => {
b69ab31904 this.postMessage({
b69ab31905 type: 'fetchedPendingSignificantLinesOfCode',
b69ab31906 hash: data.hash,
b69ab31907 requestId: data.requestId,
b69ab31908 result: {error: err as Error},
b69ab31909 });
b69ab31910 });
b69ab31911 }
b69ab31912 break;
b69ab31913 case 'fetchSignificantLinesOfCode':
b69ab31914 {
b69ab31915 repo
b69ab31916 .fetchSignificantLinesOfCode(ctx, data.hash, data.excludedFiles)
b69ab31917 .then(value => {
b69ab31918 this.postMessage({
b69ab31919 type: 'fetchedSignificantLinesOfCode',
b69ab31920 hash: data.hash,
b69ab31921 result: {value: value ?? 0},
b69ab31922 });
b69ab31923 })
b69ab31924 .catch(err => {
b69ab31925 this.postMessage({
b69ab31926 type: 'fetchedSignificantLinesOfCode',
b69ab31927 hash: data.hash,
b69ab31928 result: {error: err as Error},
b69ab31929 });
b69ab31930 });
b69ab31931 }
b69ab31932 break;
b69ab31933 case 'fetchPendingAmendSignificantLinesOfCode':
b69ab31934 {
b69ab31935 repo
b69ab31936 .fetchPendingAmendSignificantLinesOfCode(ctx, data.hash, data.includedFiles)
b69ab31937 .then(value => {
b69ab31938 this.postMessage({
b69ab31939 type: 'fetchedPendingAmendSignificantLinesOfCode',
b69ab31940 requestId: data.requestId,
b69ab31941 hash: data.hash,
b69ab31942 result: {value: value ?? 0},
b69ab31943 });
b69ab31944 })
b69ab31945 .catch(err => {
b69ab31946 this.postMessage({
b69ab31947 type: 'fetchedPendingAmendSignificantLinesOfCode',
b69ab31948 hash: data.hash,
b69ab31949 requestId: data.requestId,
b69ab31950 result: {error: err as Error},
b69ab31951 });
b69ab31952 });
b69ab31953 }
b69ab31954 break;
b69ab31955 case 'fetchCommitChangedFiles': {
b69ab31956 repo
b69ab31957 .getAllChangedFiles(ctx, data.hash)
b69ab31958 .then(files => {
b69ab31959 this.postMessage({
b69ab31960 type: 'fetchedCommitChangedFiles',
b69ab31961 hash: data.hash,
b69ab31962 result: {
b69ab31963 value: {
b69ab31964 filesSample: data.limit != null ? files.slice(0, data.limit) : files,
b69ab31965 totalFileCount: files.length,
b69ab31966 },
b69ab31967 },
b69ab31968 });
b69ab31969 })
b69ab31970 .catch(err => {
b69ab31971 this.postMessage({
b69ab31972 type: 'fetchedCommitChangedFiles',
b69ab31973 hash: data.hash,
b69ab31974 result: {error: err as Error},
b69ab31975 });
b69ab31976 });
b69ab31977 break;
b69ab31978 }
b69ab31979 case 'fetchCommitCloudState': {
b69ab31980 repo.getCommitCloudState(ctx).then(state => {
b69ab31981 this.postMessage({
b69ab31982 type: 'fetchedCommitCloudState',
b69ab31983 state: {value: state},
b69ab31984 });
b69ab31985 });
b69ab31986 break;
b69ab31987 }
b69ab31988 case 'fetchGeneratedStatuses': {
b69ab31989 generatedFilesDetector
b69ab31990 .queryFilesGenerated(repo, ctx, repo.info.repoRoot, data.paths)
b69ab31991 .then(results => {
b69ab31992 this.postMessage({type: 'fetchedGeneratedStatuses', results});
b69ab31993 });
b69ab31994 break;
b69ab31995 }
b69ab31996 case 'typeahead': {
b69ab31997 // Current repo's code review provider should be able to handle all
b69ab31998 // TypeaheadKinds for the fields in its defined schema.
b69ab31999 repo.codeReviewProvider?.typeahead?.(data.kind, data.query, cwd)?.then(result =>
b69ab311000 this.postMessage({
b69ab311001 type: 'typeaheadResult',
b69ab311002 id: data.id,
b69ab311003 result,
b69ab311004 }),
b69ab311005 );
b69ab311006 break;
b69ab311007 }
b69ab311008 case 'fetchDiffSummaries': {
b69ab311009 repo.codeReviewProvider?.triggerDiffSummariesFetch(data.diffIds ?? repo.getAllDiffIds());
b69ab311010 break;
b69ab311011 }
e7069e11012 case 'fetchCanopySignals': {
e7069e11013 repo.codeReviewProvider?.triggerCanopySignalsFetch?.();
e7069e11014 break;
e7069e11015 }
b69ab311016 case 'fetchLandInfo': {
b69ab311017 repo.codeReviewProvider
b69ab311018 ?.fetchLandInfo?.(data.topOfStack)
b69ab311019 ?.then((landInfo: LandInfo) => {
b69ab311020 this.postMessage({
b69ab311021 type: 'fetchedLandInfo',
b69ab311022 topOfStack: data.topOfStack,
b69ab311023 landInfo: {value: landInfo},
b69ab311024 });
b69ab311025 })
b69ab311026 .catch(err => {
b69ab311027 this.postMessage({
b69ab311028 type: 'fetchedLandInfo',
b69ab311029 topOfStack: data.topOfStack,
b69ab311030 landInfo: {error: err as Error},
b69ab311031 });
b69ab311032 });
b69ab311033
b69ab311034 break;
b69ab311035 }
b69ab311036 case 'confirmLand': {
b69ab311037 if (data.landConfirmationInfo == null) {
b69ab311038 break;
b69ab311039 }
b69ab311040 repo.codeReviewProvider
b69ab311041 ?.confirmLand?.(data.landConfirmationInfo)
b69ab311042 ?.then((result: Result<undefined>) => {
b69ab311043 this.postMessage({
b69ab311044 type: 'confirmedLand',
b69ab311045 result,
b69ab311046 });
b69ab311047 });
b69ab311048 break;
b69ab311049 }
b69ab311050 case 'fetchAvatars': {
b69ab311051 repo.codeReviewProvider?.fetchAvatars?.(data.authors)?.then(avatars => {
b69ab311052 this.postMessage({
b69ab311053 type: 'fetchedAvatars',
b69ab311054 avatars,
b69ab311055 authors: data.authors,
b69ab311056 });
b69ab311057 });
b69ab311058 break;
b69ab311059 }
b69ab311060 case 'fetchDiffComments': {
b69ab311061 repo.codeReviewProvider
b69ab311062 ?.fetchComments?.(data.diffId)
b69ab311063 ?.then(comments => {
b69ab311064 this.postMessage({
b69ab311065 type: 'fetchedDiffComments',
b69ab311066 diffId: data.diffId,
b69ab311067 comments: {value: comments},
b69ab311068 });
b69ab311069 })
b69ab311070 .catch(error => {
b69ab311071 this.postMessage({
b69ab311072 type: 'fetchedDiffComments',
b69ab311073 diffId: data.diffId,
b69ab311074 comments: {error},
b69ab311075 });
b69ab311076 });
b69ab311077 break;
b69ab311078 }
b69ab311079 case 'renderMarkup': {
b69ab311080 repo.codeReviewProvider
b69ab311081 ?.renderMarkup?.(data.markup)
b69ab311082 ?.then(html => {
b69ab311083 this.postMessage({
b69ab311084 type: 'renderedMarkup',
b69ab311085 id: data.id,
b69ab311086 html,
b69ab311087 });
b69ab311088 })
b69ab311089 ?.catch(err => {
b69ab311090 this.logger.error('Error rendering markup:', err);
b69ab311091 });
b69ab311092 break;
b69ab311093 }
b69ab311094 case 'getSuggestedReviewers': {
b69ab311095 repo.codeReviewProvider?.getSuggestedReviewers?.(data.context).then(reviewers => {
b69ab311096 this.postMessage({
b69ab311097 type: 'gotSuggestedReviewers',
b69ab311098 reviewers,
b69ab311099 key: data.key,
b69ab311100 });
b69ab311101 });
b69ab311102 break;
b69ab311103 }
b69ab311104 case 'updateRemoteDiffMessage': {
b69ab311105 repo.codeReviewProvider
b69ab311106 ?.updateDiffMessage?.(data.diffId, data.title, data.description)
b69ab311107 ?.catch(err => err)
b69ab311108 ?.then((error: string | undefined) => {
b69ab311109 if (error != null) {
b69ab311110 this.logger.error('Error updating remote diff message:', error);
b69ab311111 }
b69ab311112 this.postMessage({type: 'updatedRemoteDiffMessage', diffId: data.diffId, error});
b69ab311113 });
b69ab311114 break;
b69ab311115 }
b69ab311116 case 'loadMoreCommits': {
b69ab311117 const rangeInDays = repo.nextVisibleCommitRangeInDays();
b69ab311118 this.postMessage({type: 'commitsShownRange', rangeInDays});
b69ab311119 this.postMessage({type: 'beganLoadingMoreCommits'});
b69ab311120 repo.fetchSmartlogCommits();
b69ab311121 this.tracker.track('LoadMoreCommits', {extras: {daysToFetch: rangeInDays ?? 'Infinity'}});
b69ab311122 return;
b69ab311123 }
b69ab311124 case 'exportStack': {
b69ab311125 const {revs, assumeTracked} = data;
b69ab311126 const assumeTrackedArgs = (assumeTracked ?? []).map(path => `--assume-tracked=${path}`);
b69ab311127 const exec = repo.runCommand(
b69ab311128 ['debugexportstack', '-r', revs, ...assumeTrackedArgs],
b69ab311129 'ExportStackCommand',
b69ab311130 ctx,
b69ab311131 undefined,
b69ab311132 /* don't timeout */ 0,
b69ab311133 );
b69ab311134 const reply = (stack?: ExportStack, error?: string) => {
b69ab311135 this.postMessage({
b69ab311136 type: 'exportedStack',
b69ab311137 assumeTracked: assumeTracked ?? [],
b69ab311138 revs,
b69ab311139 stack: stack ?? [],
b69ab311140 error,
b69ab311141 });
b69ab311142 };
b69ab311143 parseExecJson(exec, reply);
b69ab311144 break;
b69ab311145 }
b69ab311146 case 'importStack': {
b69ab311147 const stdinStream = Readable.from(JSON.stringify(data.stack));
b69ab311148 const exec = repo.runCommand(
b69ab311149 ['debugimportstack'],
b69ab311150 'ImportStackCommand',
b69ab311151 ctx,
b69ab311152 {stdin: stdinStream},
b69ab311153 /* don't timeout */ 0,
b69ab311154 );
b69ab311155 const reply = (imported?: ImportedStack, error?: string) => {
b69ab311156 this.postMessage({type: 'importedStack', imported: imported ?? [], error});
b69ab311157 };
b69ab311158 parseExecJson(exec, reply);
b69ab311159 break;
b69ab311160 }
b69ab311161 case 'fetchQeFlag': {
b69ab311162 Internal.fetchQeFlag?.(repo.initialConnectionContext, data.name).then((passes: boolean) => {
b69ab311163 this.logger.info(`qe flag ${data.name} ${passes ? 'PASSES' : 'FAILS'}`);
b69ab311164 this.postMessage({type: 'fetchedQeFlag', name: data.name, passes});
b69ab311165 });
b69ab311166 break;
b69ab311167 }
b69ab311168 case 'fetchFeatureFlag': {
b69ab311169 Internal.fetchFeatureFlag?.(repo.initialConnectionContext, data.name).then(
b69ab311170 (passes: boolean) => {
b69ab311171 this.logger.info(`feature flag ${data.name} ${passes ? 'PASSES' : 'FAILS'}`);
b69ab311172 this.postMessage({type: 'fetchedFeatureFlag', name: data.name, passes});
b69ab311173 },
b69ab311174 );
b69ab311175 break;
b69ab311176 }
b69ab311177 case 'bulkFetchFeatureFlags': {
b69ab311178 Internal.bulkFetchFeatureFlags?.(repo.initialConnectionContext, data.names).then(
b69ab311179 (result: Record<string, boolean>) => {
b69ab311180 this.logger.info(`feature flags ${JSON.stringify(result, null, 2)}`);
b69ab311181 this.postMessage({type: 'bulkFetchedFeatureFlags', id: data.id, result});
b69ab311182 },
b69ab311183 );
b69ab311184 break;
b69ab311185 }
b69ab311186 case 'fetchInternalUserInfo': {
b69ab311187 Internal.fetchUserInfo?.(repo.initialConnectionContext).then((info: Serializable) => {
b69ab311188 this.logger.info('user info:', info);
b69ab311189 this.postMessage({type: 'fetchedInternalUserInfo', info});
b69ab311190 });
b69ab311191 break;
b69ab311192 }
b69ab311193 case 'fetchAndSetStables': {
b69ab311194 Internal.fetchStableLocations?.(ctx, data.additionalStables).then(
b69ab311195 (stables: StableLocationData | undefined) => {
b69ab311196 this.logger.info('fetched stable locations', stables);
b69ab311197 if (stables == null) {
b69ab311198 return;
b69ab311199 }
b69ab311200 this.postMessage({type: 'fetchedStables', stables});
b69ab311201 repo.stableLocations = [
b69ab311202 ...stables.stables,
b69ab311203 ...stables.special,
b69ab311204 ...Object.values(stables.manual),
b69ab311205 ]
b69ab311206 .map(stable => stable?.value)
b69ab311207 .filter(notEmpty);
b69ab311208 repo.fetchSmartlogCommits();
b69ab311209 },
b69ab311210 );
b69ab311211 break;
b69ab311212 }
b69ab311213 case 'fetchStableLocationAutocompleteOptions': {
b69ab311214 Internal.fetchStableLocationAutocompleteOptions?.(ctx).then(
b69ab311215 (result: Result<Array<TypeaheadResult>>) => {
b69ab311216 this.postMessage({type: 'fetchedStableLocationAutocompleteOptions', result});
b69ab311217 },
b69ab311218 );
b69ab311219 break;
b69ab311220 }
b69ab311221 case 'fetchDevEnvType': {
b69ab311222 if (Internal.getDevEnvType == null) {
b69ab311223 break;
b69ab311224 }
b69ab311225
b69ab311226 Internal.getDevEnvType()
b69ab311227 .catch((error: Error) => {
b69ab311228 this.logger.error('Error getting dev env type:', error);
b69ab311229 return 'error';
b69ab311230 })
b69ab311231 .then((result: string) => {
b69ab311232 this.postMessage({
b69ab311233 type: 'fetchedDevEnvType',
b69ab311234 envType: result,
b69ab311235 id: data.id,
b69ab311236 });
b69ab311237 });
b69ab311238 break;
b69ab311239 }
b69ab311240 case 'splitCommitWithAI': {
b69ab311241 Internal.splitCommitWithAI?.(ctx, data.diffCommit, data.args).then(
b69ab311242 (result: Result<ReadonlyArray<PartiallySelectedDiffCommit>>) => {
b69ab311243 this.postMessage({
b69ab311244 type: 'splitCommitWithAI',
b69ab311245 id: data.id,
b69ab311246 result,
b69ab311247 });
b69ab311248 },
b69ab311249 );
b69ab311250 break;
b69ab311251 }
b69ab311252 case 'fetchActiveAlerts': {
b69ab311253 repo
b69ab311254 .getActiveAlerts(ctx)
b69ab311255 .then(alerts => {
b69ab311256 if (alerts.length === 0) {
b69ab311257 return;
b69ab311258 }
b69ab311259 this.postMessage({
b69ab311260 type: 'fetchedActiveAlerts',
b69ab311261 alerts,
b69ab311262 });
b69ab311263 })
b69ab311264 .catch(err => {
b69ab311265 this.logger.error('Failed to fetch active alerts:', err);
b69ab311266 });
b69ab311267 break;
b69ab311268 }
b69ab311269 case 'gotUiState': {
b69ab311270 break;
b69ab311271 }
b69ab311272 case 'getConfiguredMergeTool': {
b69ab311273 repo.getMergeTool(ctx).then((tool: string | null) => {
b69ab311274 this.postMessage({
b69ab311275 type: 'gotConfiguredMergeTool',
b69ab311276 tool: tool ?? undefined,
b69ab311277 });
b69ab311278 });
b69ab311279 break;
b69ab311280 }
b69ab311281 case 'fetchGkDetails': {
b69ab311282 Internal.fetchGkDetails?.(ctx, data.name)
b69ab311283 .then((gk: InternalTypes['InternalGatekeeper']) => {
b69ab311284 this.postMessage({type: 'fetchedGkDetails', id: data.id, result: {value: gk}});
b69ab311285 })
b69ab311286 .catch((err: unknown) => {
b69ab311287 logger?.error('Could not fetch GK details', err);
b69ab311288 this.postMessage({
b69ab311289 type: 'fetchedGkDetails',
b69ab311290 id: data.id,
b69ab311291 result: {error: err as Error},
b69ab311292 });
b69ab311293 });
b69ab311294 break;
b69ab311295 }
b69ab311296 case 'fetchJkDetails': {
b69ab311297 Internal.fetchJustKnobsByNames?.(ctx, data.names)
b69ab311298 .then((jk: InternalTypes['InternalJustknob']) => {
b69ab311299 this.postMessage({type: 'fetchedJkDetails', id: data.id, result: {value: jk}});
b69ab311300 })
b69ab311301 .catch((err: unknown) => {
b69ab311302 logger?.error('Could not fetch JK details', err);
b69ab311303 this.postMessage({
b69ab311304 type: 'fetchedJkDetails',
b69ab311305 id: data.id,
b69ab311306 result: {error: err as Error},
b69ab311307 });
b69ab311308 });
b69ab311309 break;
b69ab311310 }
b69ab311311 case 'fetchKnobsetDetails': {
b69ab311312 Internal.fetchKnobset?.(ctx, data.configPath)
b69ab311313 .then((knobset: InternalTypes['InternalKnobset']) => {
b69ab311314 this.postMessage({
b69ab311315 type: 'fetchedKnobsetDetails',
b69ab311316 id: data.id,
b69ab311317 result: {value: knobset},
b69ab311318 });
b69ab311319 })
b69ab311320 .catch((err: unknown) => {
b69ab311321 logger?.error('Could not fetch knobset details', err);
b69ab311322 this.postMessage({
b69ab311323 type: 'fetchedKnobsetDetails',
b69ab311324 id: data.id,
b69ab311325 result: {error: err as Error},
b69ab311326 });
b69ab311327 });
b69ab311328 break;
b69ab311329 }
b69ab311330 case 'fetchQeDetails': {
b69ab311331 Internal.fetchQeMetadata?.(ctx, data.name)
b69ab311332 .then((qe: InternalTypes['InternalQuickExperiment']) => {
b69ab311333 this.postMessage({
b69ab311334 type: 'fetchedQeDetails',
b69ab311335 id: data.id,
b69ab311336 result: {value: qe},
b69ab311337 });
b69ab311338 })
b69ab311339 .catch((err: unknown) => {
b69ab311340 logger?.error('Could not fetch QE details', err);
b69ab311341 this.postMessage({
b69ab311342 type: 'fetchedQeDetails',
b69ab311343 id: data.id,
b69ab311344 result: {error: err as Error},
b69ab311345 });
b69ab311346 });
b69ab311347 break;
b69ab311348 }
b69ab311349 case 'fetchABPropDetails': {
b69ab311350 Internal.fetchABPropMetadata?.(ctx, data.name)
b69ab311351 .then((abprop: InternalTypes['InternalMetaConfig']) => {
b69ab311352 this.postMessage({
b69ab311353 type: 'fetchedABPropDetails',
b69ab311354 id: data.id,
b69ab311355 result: {value: abprop},
b69ab311356 });
b69ab311357 })
b69ab311358 .catch((err: unknown) => {
b69ab311359 logger?.error('Could not fetch ABProp details', err);
b69ab311360 this.postMessage({
b69ab311361 type: 'fetchedABPropDetails',
b69ab311362 id: data.id,
b69ab311363 result: {error: err as Error},
b69ab311364 });
b69ab311365 });
b69ab311366 break;
b69ab311367 }
b69ab311368 case 'getRepoUrlAtHash': {
b69ab311369 const args = ['url', '--rev', data.revset];
b69ab311370 // validate that the path is a valid file in repo
b69ab311371 if (data.path != null && absolutePathForFileInRepo(data.path, repo) != null) {
b69ab311372 args.push(`path:${data.path}`);
b69ab311373 }
b69ab311374 repo
b69ab311375 .runCommand(args, 'RepoUrlCommand', ctx)
b69ab311376 .then(result => {
b69ab311377 this.postMessage({
b69ab311378 type: 'gotRepoUrlAtHash',
b69ab311379 url: {value: result.stdout},
b69ab311380 });
b69ab311381 })
b69ab311382 .catch((err: EjecaError) => {
b69ab311383 this.logger.error('Failed to get repo url at hash:', err);
b69ab311384 this.postMessage({
b69ab311385 type: 'gotRepoUrlAtHash',
b69ab311386 url: {error: err},
b69ab311387 });
b69ab311388 });
b69ab311389 break;
b69ab311390 }
b69ab311391 case 'fetchTaskDetails': {
b69ab311392 Internal.getTask?.(ctx, data.taskNumber).then(
b69ab311393 (task: InternalTypes['InternalTaskDetails']) => {
b69ab311394 this.postMessage({type: 'fetchedTaskDetails', id: data.id, result: {value: task}});
b69ab311395 },
b69ab311396 );
b69ab311397 break;
b69ab311398 }
b69ab311399 case 'runDevmateCommand': {
b69ab311400 Internal.runDevmateCommand?.(data.args, data.cwd)
b69ab311401 .then((result: EjecaReturn) => {
b69ab311402 this.postMessage({
b69ab311403 type: 'devmateCommandResult',
b69ab311404 result: {type: 'value', stdout: result.stdout, requestId: data.requestId},
b69ab311405 });
b69ab311406 })
b69ab311407 .catch((error: EjecaError) => {
b69ab311408 this.postMessage({
b69ab311409 type: 'devmateCommandResult',
b69ab311410 result: {type: 'error', stderr: error.stderr, requestId: data.requestId},
b69ab311411 });
b69ab311412 });
b69ab311413 break;
b69ab311414 }
b69ab311415 case 'fetchSubscribedFullRepoBranches': {
b69ab311416 Internal.fetchSubscribedFullRepoBranches?.(ctx, repo)
b69ab311417 .then((branches: Array<InternalTypes['FullRepoBranch']>) => {
b69ab311418 this.postMessage({
b69ab311419 type: 'fetchedSubscribedFullRepoBranches',
b69ab311420 result: {value: branches},
b69ab311421 });
b69ab311422 })
b69ab311423 .catch((error: EjecaError) => {
b69ab311424 this.postMessage({
b69ab311425 type: 'fetchedSubscribedFullRepoBranches',
b69ab311426 result: {error},
b69ab311427 });
b69ab311428 });
b69ab311429 break;
b69ab311430 }
b69ab311431 case 'fetchFullRepoBranchAllChangedFiles': {
b69ab311432 Internal.getFullRepoBranchAllChangedFiles?.(ctx, data.fullRepoBranch)
b69ab311433 .then((paths: Array<ChangedFile>) => {
b69ab311434 this.postMessage({
b69ab311435 type: 'fetchedFullRepoBranchAllChangedFiles',
b69ab311436 id: data.id,
b69ab311437 result: {value: paths},
b69ab311438 });
b69ab311439 })
b69ab311440 .catch((error: EjecaError) => {
b69ab311441 this.postMessage({
b69ab311442 type: 'fetchedFullRepoBranchAllChangedFiles',
b69ab311443 id: data.id,
b69ab311444 result: {error},
b69ab311445 });
b69ab311446 });
b69ab311447 break;
b69ab311448 }
b69ab311449 case 'fetchFullRepoBranchMergeSubtreePaths': {
b69ab311450 Internal.getFullRepoBranchMergeSubtreePaths?.(ctx, data.fullRepoBranch, data.paths)
b69ab311451 .then((paths: Array<string>) => {
b69ab311452 this.postMessage({
b69ab311453 type: 'fetchedFullRepoBranchMergeSubtreePaths',
b69ab311454 id: data.id,
b69ab311455 result: {value: paths},
b69ab311456 });
b69ab311457 })
b69ab311458 .catch((error: EjecaError) => {
b69ab311459 this.postMessage({
b69ab311460 type: 'fetchedFullRepoBranchMergeSubtreePaths',
b69ab311461 id: data.id,
b69ab311462 result: {error},
b69ab311463 });
b69ab311464 });
b69ab311465 break;
b69ab311466 }
b69ab311467 case 'subscribeToFullRepoBranch': {
b69ab311468 Internal.subscribeToFullRepoBranch?.(ctx, repo, data.fullRepoBranch);
b69ab311469 break;
b69ab311470 }
b69ab311471 case 'unsubscribeToFullRepoBranch': {
b69ab311472 Internal.unsubscribeToFullRepoBranch?.(ctx, repo, data.fullRepoBranch);
b69ab311473 break;
b69ab311474 }
b69ab311475 default: {
b69ab311476 if (
b69ab311477 repo.codeReviewProvider?.handleClientToServerMessage?.(data, message =>
b69ab311478 this.postMessage(message),
b69ab311479 ) === true
b69ab311480 ) {
b69ab311481 break;
b69ab311482 }
b69ab311483 this.platform.handleMessageFromClient(
b69ab311484 repo,
b69ab311485 ctx,
b69ab311486 data as Exclude<typeof data, CodeReviewProviderSpecificClientToServerMessages>,
b69ab311487 message => this.postMessage(message),
b69ab311488 (dispose: () => unknown) => {
b69ab311489 this.repoDisposables.push({dispose});
b69ab311490 },
b69ab311491 );
b69ab311492 break;
b69ab311493 }
b69ab311494 }
b69ab311495
b69ab311496 this.notifyListeners(data);
b69ab311497 }
b69ab311498
b69ab311499 private notifyListeners(data: IncomingMessage): void {
b69ab311500 const listeners = this.listenersByType.get(data.type);
b69ab311501 if (listeners) {
b69ab311502 listeners.forEach(handle => handle(data));
b69ab311503 }
b69ab311504 }
b69ab311505
b69ab311506 private async handleFetchCommitMessageTemplate(repo: Repository, ctx: RepositoryContext) {
b69ab311507 const {logger} = ctx;
b69ab311508 try {
b69ab311509 const [result, customTemplate] = await Promise.all([
b69ab311510 repo.runCommand(['debugcommitmessage', 'isl'], 'FetchCommitTemplateCommand', ctx),
b69ab311511 Internal.getCustomDefaultCommitTemplate?.(repo.initialConnectionContext),
b69ab311512 ]);
b69ab311513
b69ab311514 let template = result.stdout
b69ab311515 .replace(repo.IGNORE_COMMIT_MESSAGE_LINES_REGEX, '')
b69ab311516 .replace(/^<Replace this line with a title. Use 1 line only, 67 chars or less>/, '');
b69ab311517
b69ab311518 if (customTemplate && customTemplate?.trim() !== '') {
b69ab311519 template = customTemplate as string;
b69ab311520
b69ab311521 this.tracker.track('UseCustomCommitMessageTemplate');
b69ab311522 }
b69ab311523
b69ab311524 this.postMessage({
b69ab311525 type: 'fetchedCommitMessageTemplate',
b69ab311526 template,
b69ab311527 });
b69ab311528 } catch (err) {
b69ab311529 logger?.error('Could not fetch commit message template', err);
b69ab311530 }
b69ab311531 }
b69ab311532}