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)',
4bb999b322 readOnly: this.connection.readOnly ?? false,
b69ab31323 },
b69ab31324 });
b69ab31325 break;
b69ab31326 }
b69ab31327 case 'fileBugReport': {
b69ab31328 const maybeRepo = this.currentState.type === 'repo' ? this.currentState.repo : undefined;
b69ab31329 const ctx: RepositoryContext =
b69ab31330 this.currentState.type === 'repo'
b69ab31331 ? this.currentState.ctx
b69ab31332 : {
b69ab31333 // cwd is only needed to run graphql query, here it's just best-effort
b69ab31334 cwd: maybeRepo?.initialConnectionContext.cwd ?? process.cwd(),
b69ab31335 cmd: this.connection.command ?? 'sl',
b69ab31336 logger: this.logger,
b69ab31337 tracker: this.tracker,
b69ab31338 };
b69ab31339 Internal.fileABug?.(
b69ab31340 ctx,
b69ab31341 this.platform.platformName,
b69ab31342 data.data,
b69ab31343 data.uiState,
b69ab31344 // Use repo for rage, if available.
b69ab31345 maybeRepo,
b69ab31346 data.collectRage,
b69ab31347 (progress: FileABugProgress) => {
b69ab31348 this.connection.logger?.info('file a bug progress: ', JSON.stringify(progress));
b69ab31349 this.postMessage({type: 'fileBugReportProgress', ...progress});
b69ab31350 },
b69ab31351 );
b69ab31352 break;
b69ab31353 }
6c9fcae354 case 'fetchGroveOwners': {
6c9fcae355 const groveConfig = readGroveConfig(this.logger);
6c9fcae356 if (!groveConfig.hub || !groveConfig.token) {
6c9fcae357 this.postMessage({type: 'fetchedGroveOwners', owners: []});
6c9fcae358 break;
6c9fcae359 }
6c9fcae360 (async () => {
6c9fcae361 try {
6c9fcae362 const owners: Array<{name: string; type: 'user' | 'org'}> = [];
6c9fcae363 if (groveConfig.username) {
6c9fcae364 owners.push({name: groveConfig.username, type: 'user'});
6c9fcae365 }
6c9fcae366 const res = await fetch(`${groveConfig.hub}/api/orgs`, {
6c9fcae367 headers: {Authorization: `Bearer ${groveConfig.token}`},
6c9fcae368 });
6c9fcae369 if (res.ok) {
6c9fcae370 const data = (await res.json()) as {orgs: Array<{name: string}>};
6c9fcae371 for (const org of data.orgs) {
6c9fcae372 owners.push({name: org.name, type: 'org'});
6c9fcae373 }
6c9fcae374 }
6c9fcae375 this.postMessage({type: 'fetchedGroveOwners', owners});
6c9fcae376 } catch (err) {
6c9fcae377 this.logger.error('Failed to fetch Grove owners:', err);
6c9fcae378 this.postMessage({type: 'fetchedGroveOwners', owners: []});
6c9fcae379 }
6c9fcae380 })();
6c9fcae381 break;
6c9fcae382 }
6c9fcae383 case 'createGroveRepo': {
6c9fcae384 const groveConfig = readGroveConfig(this.logger);
6c9fcae385 if (!groveConfig.hub || !groveConfig.token) {
6c9fcae386 this.postMessage({
6c9fcae387 type: 'createdGroveRepo',
6c9fcae388 result: {error: new Error('Not logged in to Grove. Run `grove auth login` first.')},
6c9fcae389 });
6c9fcae390 break;
6c9fcae391 }
6c9fcae392 const owner = data.owner ?? groveConfig.username;
6c9fcae393 if (!owner) {
6c9fcae394 this.postMessage({
6c9fcae395 type: 'createdGroveRepo',
6c9fcae396 result: {error: new Error('No owner specified and no username found in Grove config.')},
6c9fcae397 });
6c9fcae398 break;
6c9fcae399 }
6c9fcae400 const repoName = data.name;
6c9fcae401 (async () => {
6c9fcae402 try {
6c9fcae403 // Create the repo on Grove
6c9fcae404 const res = await fetch(`${groveConfig.hub}/api/repos`, {
6c9fcae405 method: 'POST',
6c9fcae406 headers: {
6c9fcae407 Authorization: `Bearer ${groveConfig.token}`,
6c9fcae408 'Content-Type': 'application/json',
6c9fcae409 },
6c9fcae410 body: JSON.stringify({name: repoName, owner}),
6c9fcae411 });
6c9fcae412 if (!res.ok) {
6c9fcae413 const body = await res.text();
6c9fcae414 throw new Error(`Failed to create repository: ${res.status} ${body}`);
6c9fcae415 }
6c9fcae416
6c9fcae417 const cwd = this.connection.cwd;
6c9fcae418 const cmd = this.connection.command ?? 'sl';
6c9fcae419 const homedir = process.env.HOME ?? process.env.USERPROFILE ?? '';
0542c45420
0542c45421 // If sl init created a git-backed repo, re-init with correct format
0542c45422 const fs = await import('fs');
0542c45423 const storeRequires = path.join(cwd, '.sl', 'store', 'requires');
0542c45424 try {
0542c45425 const requires = fs.readFileSync(storeRequires, 'utf8');
0542c45426 if (requires.includes('git')) {
0542c45427 this.logger.info(
0542c45428 'Re-initializing repo: current store is git-backed, need native format for Grove',
0542c45429 );
0542c45430 fs.rmSync(path.join(cwd, '.sl'), {recursive: true, force: true});
0542c45431 await ejeca(
0542c45432 cmd,
0542c45433 [
0542c45434 'init',
0542c45435 '--config',
0542c45436 'init.prefer-git=false',
0542c45437 '--config',
0542c45438 'format.use-remotefilelog=true',
0542c45439 cwd,
0542c45440 ],
0542c45441 {cwd},
0542c45442 );
0542c45443 }
0542c45444 } catch {
0542c45445 // No .sl/store/requires means no repo yet — init fresh
0542c45446 if (!fs.existsSync(path.join(cwd, '.sl'))) {
0542c45447 await ejeca(
0542c45448 cmd,
0542c45449 [
0542c45450 'init',
0542c45451 '--config',
0542c45452 'init.prefer-git=false',
0542c45453 '--config',
0542c45454 'format.use-remotefilelog=true',
0542c45455 cwd,
0542c45456 ],
0542c45457 {cwd},
0542c45458 );
0542c45459 }
0542c45460 }
0542c45461
0542c45462 // Configure sapling remote
6c9fcae463 const configPairs = [
6c9fcae464 ['paths.default', `mononoke://grove.host:8443/${repoName}`],
6c9fcae465 ['remotefilelog.reponame', repoName],
6c9fcae466 ['grove.owner', owner],
6c9fcae467 ['edenapi.url', 'https://grove.host:8443/edenapi/'],
6c9fcae468 ['web.cacerts', `${homedir}/.grove/tls/ca.crt`],
6c9fcae469 ['auth.grove.prefix', 'https://grove.host'],
6c9fcae470 ['auth.grove.cert', `${homedir}/.grove/tls/client.crt`],
6c9fcae471 ['auth.grove.key', `${homedir}/.grove/tls/client.key`],
6c9fcae472 ['auth.grove.cacerts', `${homedir}/.grove/tls/ca.crt`],
6c9fcae473 ['auth.grove-mononoke.prefix', 'mononoke://grove.host'],
6c9fcae474 ['auth.grove-mononoke.cert', `${homedir}/.grove/tls/client.crt`],
6c9fcae475 ['auth.grove-mononoke.key', `${homedir}/.grove/tls/client.key`],
6c9fcae476 ['auth.grove-mononoke.cacerts', `${homedir}/.grove/tls/ca.crt`],
6c9fcae477 ['clone.use-commit-graph', 'true'],
6c9fcae478 ['remotenames.selectivepulldefault', 'main'],
6c9fcae479 ['push.edenapi', 'true'],
6c9fcae480 ['push.to', 'main'],
6c9fcae481 ];
6c9fcae482 for (const [key, value] of configPairs) {
6c9fcae483 await ejeca(cmd, ['config', '--local', key, value], {cwd});
6c9fcae484 }
6c9fcae485
0542c45486 // Pull the seed commit and check out main
0542c45487 try {
0542c45488 await ejeca(cmd, ['pull'], {cwd});
0542c45489 await ejeca(cmd, ['goto', 'main'], {cwd});
0542c45490 } catch (pullErr) {
0542c45491 this.logger.info('Pull after repo creation failed (may not be seeded yet):', pullErr);
0542c45492 }
0542c45493
6c9fcae494 // Re-detect repo info and send it to the client
6c9fcae495 const ctx = {
6c9fcae496 cwd,
6c9fcae497 cmd,
6c9fcae498 logger: this.logger,
6c9fcae499 tracker: this.tracker,
6c9fcae500 };
6c9fcae501 const newInfo = await Repository.getRepoInfo(ctx);
6c9fcae502 this.postMessage({type: 'repoInfo', info: newInfo, cwd});
6c9fcae503
6c9fcae504 // Also re-initialize the repo in the cache so subscriptions work
6c9fcae505 this.setActiveRepoForCwd(cwd);
6c9fcae506
6c9fcae507 this.postMessage({
6c9fcae508 type: 'createdGroveRepo',
6c9fcae509 result: {value: {owner, repo: repoName}},
6c9fcae510 });
6c9fcae511 } catch (err) {
6c9fcae512 this.logger.error('Failed to create Grove repo:', err);
6c9fcae513 this.postMessage({
6c9fcae514 type: 'createdGroveRepo',
6c9fcae515 result: {error: err instanceof Error ? err : new Error(String(err))},
6c9fcae516 });
6c9fcae517 }
6c9fcae518 })();
6c9fcae519 break;
6c9fcae520 }
b69ab31521 }
b69ab31522 }
b69ab31523
b69ab31524 private handleMaybeForgotOperation(operationId: string, repo: Repository) {
b69ab31525 if (repo.getRunningOperation()?.id !== operationId) {
b69ab31526 this.postMessage({type: 'operationProgress', id: operationId, kind: 'forgot'});
b69ab31527 }
b69ab31528 }
b69ab31529
b69ab31530 /**
b69ab31531 * Handle messages which require a repository to have been successfully set up to run
b69ab31532 */
b69ab31533 private handleIncomingMessageWithRepo(
b69ab31534 data: WithRepoMessage,
b69ab31535 repo: Repository,
b69ab31536 ctx: RepositoryContext,
b69ab31537 ) {
b69ab31538 const {cwd, logger} = ctx;
b69ab31539 switch (data.type) {
b69ab31540 case 'subscribe': {
b69ab31541 const {subscriptionID, kind} = data;
b69ab31542 switch (kind) {
b69ab31543 case 'uncommittedChanges': {
b69ab31544 const postUncommittedChanges = (result: FetchedUncommittedChanges) => {
b69ab31545 this.postMessage({
b69ab31546 type: 'subscriptionResult',
b69ab31547 kind: 'uncommittedChanges',
b69ab31548 subscriptionID,
b69ab31549 data: result,
b69ab31550 });
b69ab31551 };
b69ab31552
b69ab31553 const uncommittedChanges = repo.getUncommittedChanges();
b69ab31554 if (uncommittedChanges != null) {
b69ab31555 postUncommittedChanges(uncommittedChanges);
b69ab31556 }
b69ab31557 const disposables: Array<Disposable> = [];
b69ab31558
b69ab31559 // send changes as they come in from watchman
b69ab31560 disposables.push(repo.subscribeToUncommittedChanges(postUncommittedChanges));
b69ab31561 // trigger a fetch on startup
b69ab31562 repo.fetchUncommittedChanges();
b69ab31563
b69ab31564 disposables.push(
b69ab31565 repo.subscribeToUncommittedChangesBeginFetching(() =>
b69ab31566 this.postMessage({type: 'beganFetchingUncommittedChangesEvent'}),
b69ab31567 ),
b69ab31568 );
b69ab31569 this.subscriptions.set(subscriptionID, {
b69ab31570 dispose: () => {
b69ab31571 disposables.forEach(d => d.dispose());
b69ab31572 },
b69ab31573 });
b69ab31574 break;
b69ab31575 }
b69ab31576 case 'smartlogCommits': {
b69ab31577 const postSmartlogCommits = (result: FetchedCommits) => {
b69ab31578 this.postMessage({
b69ab31579 type: 'subscriptionResult',
b69ab31580 kind: 'smartlogCommits',
b69ab31581 subscriptionID,
b69ab31582 data: result,
b69ab31583 });
b69ab31584 };
b69ab31585
b69ab31586 const smartlogCommits = repo.getSmartlogCommits();
b69ab31587 if (smartlogCommits != null) {
b69ab31588 postSmartlogCommits(smartlogCommits);
b69ab31589 }
b69ab31590 const disposables: Array<Disposable> = [];
b69ab31591 // send changes as they come from file watcher
b69ab31592 disposables.push(repo.subscribeToSmartlogCommitsChanges(postSmartlogCommits));
b69ab31593
b69ab31594 // trigger fetch on startup
b69ab31595 repo.fetchSmartlogCommits();
b69ab31596
b69ab31597 disposables.push(
b69ab31598 repo.subscribeToSmartlogCommitsBeginFetching(() =>
b69ab31599 this.postMessage({type: 'beganFetchingSmartlogCommitsEvent'}),
b69ab31600 ),
b69ab31601 );
b69ab31602
b69ab31603 this.subscriptions.set(subscriptionID, {
b69ab31604 dispose: () => {
b69ab31605 disposables.forEach(d => d.dispose());
b69ab31606 },
b69ab31607 });
b69ab31608 break;
b69ab31609 }
b69ab31610 case 'mergeConflicts': {
b69ab31611 const postMergeConflicts = (conflicts: MergeConflicts | undefined) => {
b69ab31612 this.postMessage({
b69ab31613 type: 'subscriptionResult',
b69ab31614 kind: 'mergeConflicts',
b69ab31615 subscriptionID,
b69ab31616 data: conflicts,
b69ab31617 });
b69ab31618 };
b69ab31619
b69ab31620 const mergeConflicts = repo.getMergeConflicts();
b69ab31621 if (mergeConflicts != null) {
b69ab31622 postMergeConflicts(mergeConflicts);
b69ab31623 }
b69ab31624
b69ab31625 this.subscriptions.set(subscriptionID, repo.onChangeConflictState(postMergeConflicts));
b69ab31626 break;
b69ab31627 }
b69ab31628 case 'submodules': {
b69ab31629 const postSubmodules = (submodulesByRoot: SubmodulesByRoot) => {
b69ab31630 this.postMessage({
b69ab31631 type: 'subscriptionResult',
b69ab31632 kind: 'submodules',
b69ab31633 subscriptionID,
b69ab31634 data: submodulesByRoot,
b69ab31635 });
b69ab31636 };
b69ab31637 const submoduleMap = repo.getSubmoduleMap();
b69ab31638 if (submoduleMap !== undefined) {
b69ab31639 postSubmodules(submoduleMap);
b69ab31640 }
b69ab31641 repo.fetchSubmoduleMap();
b69ab31642
b69ab31643 const disposable = repo.subscribeToSubmodulesChanges(postSubmodules);
b69ab31644 this.subscriptions.set(subscriptionID, {
b69ab31645 dispose: () => {
b69ab31646 disposable.dispose();
b69ab31647 },
b69ab31648 });
b69ab31649 break;
b69ab31650 }
b69ab31651 case 'subscribedFullRepoBranches': {
b69ab31652 const fullRepoBranchModule = repo.fullRepoBranchModule;
b69ab31653 if (fullRepoBranchModule == null) {
b69ab31654 return;
b69ab31655 }
b69ab31656
b69ab31657 const postSubscribedFullRepoBranches = (
b69ab31658 result: Array<InternalTypes['FullRepoBranch']>,
b69ab31659 ) => {
b69ab31660 this.postMessage({
b69ab31661 type: 'subscriptionResult',
b69ab31662 kind: 'subscribedFullRepoBranches',
b69ab31663 subscriptionID,
b69ab31664 data: result,
b69ab31665 });
b69ab31666 };
b69ab31667
b69ab31668 const subscribedFullRepoBranches = fullRepoBranchModule.getSubscribedFullRepoBranches();
b69ab31669 if (subscribedFullRepoBranches != null) {
b69ab31670 postSubscribedFullRepoBranches(subscribedFullRepoBranches);
b69ab31671 }
b69ab31672 const disposables: Array<Disposable> = [];
b69ab31673 // send changes as they come from file watcher
b69ab31674 disposables.push(
b69ab31675 fullRepoBranchModule.subscribeToSubscribedFullRepoBranchesChanges(
b69ab31676 postSubscribedFullRepoBranches,
b69ab31677 ),
b69ab31678 );
b69ab31679 // trigger a fetch on startup
b69ab31680 fullRepoBranchModule.pullSubscribedFullRepoBranches();
b69ab31681
b69ab31682 disposables.push(
b69ab31683 fullRepoBranchModule.subscribeToSubscribedFullRepoBranchesBeginFetching(() =>
b69ab31684 this.postMessage({type: 'beganFetchingSubscribedFullRepoBranchesEvent'}),
b69ab31685 ),
b69ab31686 );
b69ab31687
b69ab31688 this.subscriptions.set(subscriptionID, {
b69ab31689 dispose: () => {
b69ab31690 disposables.forEach(d => d.dispose());
b69ab31691 },
b69ab31692 });
b69ab31693 break;
b69ab31694 }
b69ab31695 }
b69ab31696 break;
b69ab31697 }
b69ab31698 case 'unsubscribe': {
b69ab31699 const subscription = this.subscriptions.get(data.subscriptionID);
b69ab31700 subscription?.dispose();
b69ab31701 this.subscriptions.delete(data.subscriptionID);
b69ab31702 break;
b69ab31703 }
b69ab31704 case 'runOperation': {
4bb999b705 if (this.connection.readOnly) {
4bb999b706 const {operation} = data;
4bb999b707 this.postMessage({
4bb999b708 type: 'operationProgress',
4bb999b709 kind: 'exit',
4bb999b710 exitCode: 1,
4bb999b711 id: operation.id,
4bb999b712 timestamp: Date.now() / 1000,
4bb999b713 });
4bb999b714 break;
4bb999b715 }
b69ab31716 const {operation} = data;
b69ab31717 repo.runOrQueueOperation(ctx, operation, progress => {
b69ab31718 this.postMessage({type: 'operationProgress', ...progress});
b69ab31719 if (progress.kind === 'queue') {
b69ab31720 this.tracker.track('QueueOperation', {extras: {operation: operation.trackEventName}});
b69ab31721 }
b69ab31722 });
b69ab31723 break;
b69ab31724 }
b69ab31725 case 'abortRunningOperation': {
4bb999b726 if (this.connection.readOnly) {
4bb999b727 break;
4bb999b728 }
b69ab31729 const {operationId} = data;
b69ab31730 repo.abortRunningOperation(operationId);
b69ab31731 this.handleMaybeForgotOperation(operationId, repo);
b69ab31732 break;
b69ab31733 }
b69ab31734 case 'getConfig': {
b69ab31735 repo
b69ab31736 .getConfig(ctx, data.name)
b69ab31737 .catch(() => undefined)
b69ab31738 .then(value => {
b69ab31739 logger.info('got config', data.name, value);
b69ab31740 this.postMessage({type: 'gotConfig', name: data.name, value});
b69ab31741 });
b69ab31742 break;
b69ab31743 }
b69ab31744 case 'setConfig': {
b69ab31745 logger.info('set config', data.name, data.value);
b69ab31746 repo.setConfig(ctx, 'user', data.name, data.value).catch(err => {
b69ab31747 logger.error('error setting config', data.name, data.value, err);
b69ab31748 });
b69ab31749 break;
b69ab31750 }
b69ab31751 case 'setDebugLogging': {
b69ab31752 logger.info('set debug', data.name, data.enabled);
b69ab31753 if (data.name === 'debug' || data.name === 'verbose') {
b69ab31754 ctx[data.name] = !!data.enabled;
b69ab31755 }
b69ab31756 break;
b69ab31757 }
b69ab31758 case 'requestComparison': {
b69ab31759 const {comparison} = data;
b69ab31760 const diff: Promise<Result<string>> = repo
b69ab31761 .runDiff(ctx, comparison)
b69ab31762 .then(value => ({value}))
b69ab31763 .catch(error => {
b69ab31764 logger?.error('error running diff', error.toString());
b69ab31765 return {error};
b69ab31766 });
b69ab31767 diff.then(data =>
b69ab31768 this.postMessage({
b69ab31769 type: 'comparison',
b69ab31770 comparison,
b69ab31771 data: {diff: data},
b69ab31772 }),
b69ab31773 );
b69ab31774 break;
b69ab31775 }
b69ab31776 case 'requestComparisonContextLines': {
b69ab31777 const {
b69ab31778 id: {path: relativePath, comparison},
b69ab31779 // This is the line number in the "before" side of the comparison
b69ab31780 start,
b69ab31781 // This is the number of context lines to fetch
b69ab31782 numLines,
b69ab31783 } = data;
b69ab31784
b69ab31785 const absolutePath = path.join(repo.info.repoRoot, relativePath);
b69ab31786
b69ab31787 // TODO: For context lines, before/after sides of the comparison
b69ab31788 // are identical... except for line numbers.
b69ab31789 // Typical comparisons with '.' would be much faster (nearly instant)
b69ab31790 // by reading from the filesystem rather than using cat,
b69ab31791 // we just need the caller to ask with "after" line numbers instead of "before".
b69ab31792 // Note: we would still need to fall back to cat for comparisons that do not involve
b69ab31793 // the working copy.
b69ab31794 const cat: Promise<string> = repo.cat(
b69ab31795 ctx,
b69ab31796 absolutePath,
b69ab31797 beforeRevsetForComparison(comparison),
b69ab31798 );
b69ab31799
b69ab31800 cat
b69ab31801 .then(content =>
b69ab31802 this.postMessage({
b69ab31803 type: 'comparisonContextLines',
b69ab31804 lines: {value: content.split('\n').slice(start - 1, start - 1 + numLines)},
b69ab31805 path: relativePath,
b69ab31806 }),
b69ab31807 )
b69ab31808 .catch((error: Error) =>
b69ab31809 this.postMessage({
b69ab31810 type: 'comparisonContextLines',
b69ab31811 lines: {error},
b69ab31812 path: relativePath,
b69ab31813 }),
b69ab31814 );
b69ab31815 break;
b69ab31816 }
b69ab31817 case 'requestMissedOperationProgress': {
b69ab31818 const {operationId} = data;
b69ab31819 this.handleMaybeForgotOperation(operationId, repo);
b69ab31820 break;
b69ab31821 }
b69ab31822 case 'refresh': {
b69ab31823 logger?.log('refresh requested');
b69ab31824 repo.fetchAndSetRecommendedBookmarks(bookmarks => {
b69ab31825 this.postMessage({type: 'fetchedRecommendedBookmarks', bookmarks});
b69ab31826 });
b69ab31827 repo.fetchSmartlogCommits();
b69ab31828 repo.fetchUncommittedChanges();
b69ab31829 repo.fetchSubmoduleMap();
b69ab31830 repo.checkForMergeConflicts();
b69ab31831 repo.fullRepoBranchModule?.pullSubscribedFullRepoBranches();
b69ab31832 repo.codeReviewProvider?.triggerDiffSummariesFetch(repo.getAllDiffIds());
b69ab31833 repo.initialConnectionContext.tracker.track('DiffFetchSource', {
b69ab31834 extras: {source: 'manual_refresh'},
b69ab31835 });
b69ab31836 generatedFilesDetector.clear(); // allow generated files to be rechecked
b69ab31837 break;
b69ab31838 }
b69ab31839 case 'pageVisibility': {
b69ab31840 repo.setPageFocus(this.pageId, data.state);
b69ab31841 break;
b69ab31842 }
b69ab31843 case 'uploadFile': {
b69ab31844 const {id, filename, b64Content} = data;
b69ab31845 const payload = base64Decode(b64Content);
b69ab31846 const uploadFile = Internal.uploadFile;
b69ab31847 if (uploadFile == null) {
b69ab31848 return;
b69ab31849 }
b69ab31850 this.tracker
b69ab31851 .operation('UploadImage', 'UploadImageError', {}, () =>
b69ab31852 uploadFile(this.logger, {filename, data: payload}),
b69ab31853 )
b69ab31854 .then((result: string) => {
b69ab31855 this.logger.info('successfully uploaded file', filename, result);
b69ab31856 this.postMessage({type: 'uploadFileResult', id, result: {value: result}});
b69ab31857 })
b69ab31858 .catch((error: Error) => {
b69ab31859 this.logger.info('error uploading file', filename, error);
b69ab31860 this.postMessage({type: 'uploadFileResult', id, result: {error}});
b69ab31861 });
b69ab31862 break;
b69ab31863 }
b69ab31864 case 'fetchCommitMessageTemplate': {
b69ab31865 this.handleFetchCommitMessageTemplate(repo, ctx);
b69ab31866 break;
b69ab31867 }
b69ab31868 case 'fetchShelvedChanges': {
b69ab31869 repo
b69ab31870 .getShelvedChanges(ctx)
b69ab31871 .then(shelvedChanges => {
b69ab31872 this.postMessage({
b69ab31873 type: 'fetchedShelvedChanges',
b69ab31874 shelvedChanges: {value: shelvedChanges},
b69ab31875 });
b69ab31876 })
b69ab31877 .catch(err => {
b69ab31878 logger?.error('Could not fetch shelved changes', err);
b69ab31879 this.postMessage({type: 'fetchedShelvedChanges', shelvedChanges: {error: err}});
b69ab31880 });
b69ab31881 break;
b69ab31882 }
b69ab31883 case 'fetchLatestCommit': {
b69ab31884 repo
b69ab31885 .lookupCommits(ctx, [data.revset])
b69ab31886 .then(commits => {
b69ab31887 const commit = firstOfIterable(commits.values());
b69ab31888 if (commit == null) {
b69ab31889 throw new Error(`No commit found for revset ${data.revset}`);
b69ab31890 }
b69ab31891 this.postMessage({
b69ab31892 type: 'fetchedLatestCommit',
b69ab31893 revset: data.revset,
b69ab31894 info: {value: commit},
b69ab31895 });
b69ab31896 })
b69ab31897 .catch(err => {
b69ab31898 this.postMessage({
b69ab31899 type: 'fetchedLatestCommit',
b69ab31900 revset: data.revset,
b69ab31901 info: {error: err as Error},
b69ab31902 });
b69ab31903 });
b69ab31904 break;
b69ab31905 }
b69ab31906 case 'fetchPendingSignificantLinesOfCode':
b69ab31907 {
b69ab31908 repo
b69ab31909 .fetchPendingSignificantLinesOfCode(ctx, data.hash, data.includedFiles)
b69ab31910 .then(value => {
b69ab31911 this.postMessage({
b69ab31912 type: 'fetchedPendingSignificantLinesOfCode',
b69ab31913 requestId: data.requestId,
b69ab31914 hash: data.hash,
b69ab31915 result: {value: value ?? 0},
b69ab31916 });
b69ab31917 })
b69ab31918 .catch(err => {
b69ab31919 this.postMessage({
b69ab31920 type: 'fetchedPendingSignificantLinesOfCode',
b69ab31921 hash: data.hash,
b69ab31922 requestId: data.requestId,
b69ab31923 result: {error: err as Error},
b69ab31924 });
b69ab31925 });
b69ab31926 }
b69ab31927 break;
b69ab31928 case 'fetchSignificantLinesOfCode':
b69ab31929 {
b69ab31930 repo
b69ab31931 .fetchSignificantLinesOfCode(ctx, data.hash, data.excludedFiles)
b69ab31932 .then(value => {
b69ab31933 this.postMessage({
b69ab31934 type: 'fetchedSignificantLinesOfCode',
b69ab31935 hash: data.hash,
b69ab31936 result: {value: value ?? 0},
b69ab31937 });
b69ab31938 })
b69ab31939 .catch(err => {
b69ab31940 this.postMessage({
b69ab31941 type: 'fetchedSignificantLinesOfCode',
b69ab31942 hash: data.hash,
b69ab31943 result: {error: err as Error},
b69ab31944 });
b69ab31945 });
b69ab31946 }
b69ab31947 break;
b69ab31948 case 'fetchPendingAmendSignificantLinesOfCode':
b69ab31949 {
b69ab31950 repo
b69ab31951 .fetchPendingAmendSignificantLinesOfCode(ctx, data.hash, data.includedFiles)
b69ab31952 .then(value => {
b69ab31953 this.postMessage({
b69ab31954 type: 'fetchedPendingAmendSignificantLinesOfCode',
b69ab31955 requestId: data.requestId,
b69ab31956 hash: data.hash,
b69ab31957 result: {value: value ?? 0},
b69ab31958 });
b69ab31959 })
b69ab31960 .catch(err => {
b69ab31961 this.postMessage({
b69ab31962 type: 'fetchedPendingAmendSignificantLinesOfCode',
b69ab31963 hash: data.hash,
b69ab31964 requestId: data.requestId,
b69ab31965 result: {error: err as Error},
b69ab31966 });
b69ab31967 });
b69ab31968 }
b69ab31969 break;
b69ab31970 case 'fetchCommitChangedFiles': {
b69ab31971 repo
b69ab31972 .getAllChangedFiles(ctx, data.hash)
b69ab31973 .then(files => {
b69ab31974 this.postMessage({
b69ab31975 type: 'fetchedCommitChangedFiles',
b69ab31976 hash: data.hash,
b69ab31977 result: {
b69ab31978 value: {
b69ab31979 filesSample: data.limit != null ? files.slice(0, data.limit) : files,
b69ab31980 totalFileCount: files.length,
b69ab31981 },
b69ab31982 },
b69ab31983 });
b69ab31984 })
b69ab31985 .catch(err => {
b69ab31986 this.postMessage({
b69ab31987 type: 'fetchedCommitChangedFiles',
b69ab31988 hash: data.hash,
b69ab31989 result: {error: err as Error},
b69ab31990 });
b69ab31991 });
b69ab31992 break;
b69ab31993 }
b69ab31994 case 'fetchCommitCloudState': {
b69ab31995 repo.getCommitCloudState(ctx).then(state => {
b69ab31996 this.postMessage({
b69ab31997 type: 'fetchedCommitCloudState',
b69ab31998 state: {value: state},
b69ab31999 });
b69ab311000 });
b69ab311001 break;
b69ab311002 }
b69ab311003 case 'fetchGeneratedStatuses': {
b69ab311004 generatedFilesDetector
b69ab311005 .queryFilesGenerated(repo, ctx, repo.info.repoRoot, data.paths)
b69ab311006 .then(results => {
b69ab311007 this.postMessage({type: 'fetchedGeneratedStatuses', results});
b69ab311008 });
b69ab311009 break;
b69ab311010 }
b69ab311011 case 'typeahead': {
b69ab311012 // Current repo's code review provider should be able to handle all
b69ab311013 // TypeaheadKinds for the fields in its defined schema.
b69ab311014 repo.codeReviewProvider?.typeahead?.(data.kind, data.query, cwd)?.then(result =>
b69ab311015 this.postMessage({
b69ab311016 type: 'typeaheadResult',
b69ab311017 id: data.id,
b69ab311018 result,
b69ab311019 }),
b69ab311020 );
b69ab311021 break;
b69ab311022 }
b69ab311023 case 'fetchDiffSummaries': {
b69ab311024 repo.codeReviewProvider?.triggerDiffSummariesFetch(data.diffIds ?? repo.getAllDiffIds());
b69ab311025 break;
b69ab311026 }
e7069e11027 case 'fetchCanopySignals': {
e7069e11028 repo.codeReviewProvider?.triggerCanopySignalsFetch?.();
e7069e11029 break;
e7069e11030 }
b69ab311031 case 'fetchLandInfo': {
b69ab311032 repo.codeReviewProvider
b69ab311033 ?.fetchLandInfo?.(data.topOfStack)
b69ab311034 ?.then((landInfo: LandInfo) => {
b69ab311035 this.postMessage({
b69ab311036 type: 'fetchedLandInfo',
b69ab311037 topOfStack: data.topOfStack,
b69ab311038 landInfo: {value: landInfo},
b69ab311039 });
b69ab311040 })
b69ab311041 .catch(err => {
b69ab311042 this.postMessage({
b69ab311043 type: 'fetchedLandInfo',
b69ab311044 topOfStack: data.topOfStack,
b69ab311045 landInfo: {error: err as Error},
b69ab311046 });
b69ab311047 });
b69ab311048
b69ab311049 break;
b69ab311050 }
b69ab311051 case 'confirmLand': {
b69ab311052 if (data.landConfirmationInfo == null) {
b69ab311053 break;
b69ab311054 }
b69ab311055 repo.codeReviewProvider
b69ab311056 ?.confirmLand?.(data.landConfirmationInfo)
b69ab311057 ?.then((result: Result<undefined>) => {
b69ab311058 this.postMessage({
b69ab311059 type: 'confirmedLand',
b69ab311060 result,
b69ab311061 });
b69ab311062 });
b69ab311063 break;
b69ab311064 }
b69ab311065 case 'fetchAvatars': {
b69ab311066 repo.codeReviewProvider?.fetchAvatars?.(data.authors)?.then(avatars => {
b69ab311067 this.postMessage({
b69ab311068 type: 'fetchedAvatars',
b69ab311069 avatars,
b69ab311070 authors: data.authors,
b69ab311071 });
b69ab311072 });
b69ab311073 break;
b69ab311074 }
b69ab311075 case 'fetchDiffComments': {
b69ab311076 repo.codeReviewProvider
b69ab311077 ?.fetchComments?.(data.diffId)
b69ab311078 ?.then(comments => {
b69ab311079 this.postMessage({
b69ab311080 type: 'fetchedDiffComments',
b69ab311081 diffId: data.diffId,
b69ab311082 comments: {value: comments},
b69ab311083 });
b69ab311084 })
b69ab311085 .catch(error => {
b69ab311086 this.postMessage({
b69ab311087 type: 'fetchedDiffComments',
b69ab311088 diffId: data.diffId,
b69ab311089 comments: {error},
b69ab311090 });
b69ab311091 });
b69ab311092 break;
b69ab311093 }
b69ab311094 case 'renderMarkup': {
b69ab311095 repo.codeReviewProvider
b69ab311096 ?.renderMarkup?.(data.markup)
b69ab311097 ?.then(html => {
b69ab311098 this.postMessage({
b69ab311099 type: 'renderedMarkup',
b69ab311100 id: data.id,
b69ab311101 html,
b69ab311102 });
b69ab311103 })
b69ab311104 ?.catch(err => {
b69ab311105 this.logger.error('Error rendering markup:', err);
b69ab311106 });
b69ab311107 break;
b69ab311108 }
b69ab311109 case 'getSuggestedReviewers': {
b69ab311110 repo.codeReviewProvider?.getSuggestedReviewers?.(data.context).then(reviewers => {
b69ab311111 this.postMessage({
b69ab311112 type: 'gotSuggestedReviewers',
b69ab311113 reviewers,
b69ab311114 key: data.key,
b69ab311115 });
b69ab311116 });
b69ab311117 break;
b69ab311118 }
b69ab311119 case 'updateRemoteDiffMessage': {
b69ab311120 repo.codeReviewProvider
b69ab311121 ?.updateDiffMessage?.(data.diffId, data.title, data.description)
b69ab311122 ?.catch(err => err)
b69ab311123 ?.then((error: string | undefined) => {
b69ab311124 if (error != null) {
b69ab311125 this.logger.error('Error updating remote diff message:', error);
b69ab311126 }
b69ab311127 this.postMessage({type: 'updatedRemoteDiffMessage', diffId: data.diffId, error});
b69ab311128 });
b69ab311129 break;
b69ab311130 }
b69ab311131 case 'loadMoreCommits': {
b69ab311132 const rangeInDays = repo.nextVisibleCommitRangeInDays();
b69ab311133 this.postMessage({type: 'commitsShownRange', rangeInDays});
b69ab311134 this.postMessage({type: 'beganLoadingMoreCommits'});
b69ab311135 repo.fetchSmartlogCommits();
b69ab311136 this.tracker.track('LoadMoreCommits', {extras: {daysToFetch: rangeInDays ?? 'Infinity'}});
b69ab311137 return;
b69ab311138 }
b69ab311139 case 'exportStack': {
b69ab311140 const {revs, assumeTracked} = data;
b69ab311141 const assumeTrackedArgs = (assumeTracked ?? []).map(path => `--assume-tracked=${path}`);
b69ab311142 const exec = repo.runCommand(
b69ab311143 ['debugexportstack', '-r', revs, ...assumeTrackedArgs],
b69ab311144 'ExportStackCommand',
b69ab311145 ctx,
b69ab311146 undefined,
b69ab311147 /* don't timeout */ 0,
b69ab311148 );
b69ab311149 const reply = (stack?: ExportStack, error?: string) => {
b69ab311150 this.postMessage({
b69ab311151 type: 'exportedStack',
b69ab311152 assumeTracked: assumeTracked ?? [],
b69ab311153 revs,
b69ab311154 stack: stack ?? [],
b69ab311155 error,
b69ab311156 });
b69ab311157 };
b69ab311158 parseExecJson(exec, reply);
b69ab311159 break;
b69ab311160 }
b69ab311161 case 'importStack': {
b69ab311162 const stdinStream = Readable.from(JSON.stringify(data.stack));
b69ab311163 const exec = repo.runCommand(
b69ab311164 ['debugimportstack'],
b69ab311165 'ImportStackCommand',
b69ab311166 ctx,
b69ab311167 {stdin: stdinStream},
b69ab311168 /* don't timeout */ 0,
b69ab311169 );
b69ab311170 const reply = (imported?: ImportedStack, error?: string) => {
b69ab311171 this.postMessage({type: 'importedStack', imported: imported ?? [], error});
b69ab311172 };
b69ab311173 parseExecJson(exec, reply);
b69ab311174 break;
b69ab311175 }
b69ab311176 case 'fetchQeFlag': {
b69ab311177 Internal.fetchQeFlag?.(repo.initialConnectionContext, data.name).then((passes: boolean) => {
b69ab311178 this.logger.info(`qe flag ${data.name} ${passes ? 'PASSES' : 'FAILS'}`);
b69ab311179 this.postMessage({type: 'fetchedQeFlag', name: data.name, passes});
b69ab311180 });
b69ab311181 break;
b69ab311182 }
b69ab311183 case 'fetchFeatureFlag': {
b69ab311184 Internal.fetchFeatureFlag?.(repo.initialConnectionContext, data.name).then(
b69ab311185 (passes: boolean) => {
b69ab311186 this.logger.info(`feature flag ${data.name} ${passes ? 'PASSES' : 'FAILS'}`);
b69ab311187 this.postMessage({type: 'fetchedFeatureFlag', name: data.name, passes});
b69ab311188 },
b69ab311189 );
b69ab311190 break;
b69ab311191 }
b69ab311192 case 'bulkFetchFeatureFlags': {
b69ab311193 Internal.bulkFetchFeatureFlags?.(repo.initialConnectionContext, data.names).then(
b69ab311194 (result: Record<string, boolean>) => {
b69ab311195 this.logger.info(`feature flags ${JSON.stringify(result, null, 2)}`);
b69ab311196 this.postMessage({type: 'bulkFetchedFeatureFlags', id: data.id, result});
b69ab311197 },
b69ab311198 );
b69ab311199 break;
b69ab311200 }
b69ab311201 case 'fetchInternalUserInfo': {
b69ab311202 Internal.fetchUserInfo?.(repo.initialConnectionContext).then((info: Serializable) => {
b69ab311203 this.logger.info('user info:', info);
b69ab311204 this.postMessage({type: 'fetchedInternalUserInfo', info});
b69ab311205 });
b69ab311206 break;
b69ab311207 }
b69ab311208 case 'fetchAndSetStables': {
b69ab311209 Internal.fetchStableLocations?.(ctx, data.additionalStables).then(
b69ab311210 (stables: StableLocationData | undefined) => {
b69ab311211 this.logger.info('fetched stable locations', stables);
b69ab311212 if (stables == null) {
b69ab311213 return;
b69ab311214 }
b69ab311215 this.postMessage({type: 'fetchedStables', stables});
b69ab311216 repo.stableLocations = [
b69ab311217 ...stables.stables,
b69ab311218 ...stables.special,
b69ab311219 ...Object.values(stables.manual),
b69ab311220 ]
b69ab311221 .map(stable => stable?.value)
b69ab311222 .filter(notEmpty);
b69ab311223 repo.fetchSmartlogCommits();
b69ab311224 },
b69ab311225 );
b69ab311226 break;
b69ab311227 }
b69ab311228 case 'fetchStableLocationAutocompleteOptions': {
b69ab311229 Internal.fetchStableLocationAutocompleteOptions?.(ctx).then(
b69ab311230 (result: Result<Array<TypeaheadResult>>) => {
b69ab311231 this.postMessage({type: 'fetchedStableLocationAutocompleteOptions', result});
b69ab311232 },
b69ab311233 );
b69ab311234 break;
b69ab311235 }
b69ab311236 case 'fetchDevEnvType': {
b69ab311237 if (Internal.getDevEnvType == null) {
b69ab311238 break;
b69ab311239 }
b69ab311240
b69ab311241 Internal.getDevEnvType()
b69ab311242 .catch((error: Error) => {
b69ab311243 this.logger.error('Error getting dev env type:', error);
b69ab311244 return 'error';
b69ab311245 })
b69ab311246 .then((result: string) => {
b69ab311247 this.postMessage({
b69ab311248 type: 'fetchedDevEnvType',
b69ab311249 envType: result,
b69ab311250 id: data.id,
b69ab311251 });
b69ab311252 });
b69ab311253 break;
b69ab311254 }
b69ab311255 case 'splitCommitWithAI': {
b69ab311256 Internal.splitCommitWithAI?.(ctx, data.diffCommit, data.args).then(
b69ab311257 (result: Result<ReadonlyArray<PartiallySelectedDiffCommit>>) => {
b69ab311258 this.postMessage({
b69ab311259 type: 'splitCommitWithAI',
b69ab311260 id: data.id,
b69ab311261 result,
b69ab311262 });
b69ab311263 },
b69ab311264 );
b69ab311265 break;
b69ab311266 }
b69ab311267 case 'fetchActiveAlerts': {
b69ab311268 repo
b69ab311269 .getActiveAlerts(ctx)
b69ab311270 .then(alerts => {
b69ab311271 if (alerts.length === 0) {
b69ab311272 return;
b69ab311273 }
b69ab311274 this.postMessage({
b69ab311275 type: 'fetchedActiveAlerts',
b69ab311276 alerts,
b69ab311277 });
b69ab311278 })
b69ab311279 .catch(err => {
b69ab311280 this.logger.error('Failed to fetch active alerts:', err);
b69ab311281 });
b69ab311282 break;
b69ab311283 }
b69ab311284 case 'gotUiState': {
b69ab311285 break;
b69ab311286 }
b69ab311287 case 'getConfiguredMergeTool': {
b69ab311288 repo.getMergeTool(ctx).then((tool: string | null) => {
b69ab311289 this.postMessage({
b69ab311290 type: 'gotConfiguredMergeTool',
b69ab311291 tool: tool ?? undefined,
b69ab311292 });
b69ab311293 });
b69ab311294 break;
b69ab311295 }
b69ab311296 case 'fetchGkDetails': {
b69ab311297 Internal.fetchGkDetails?.(ctx, data.name)
b69ab311298 .then((gk: InternalTypes['InternalGatekeeper']) => {
b69ab311299 this.postMessage({type: 'fetchedGkDetails', id: data.id, result: {value: gk}});
b69ab311300 })
b69ab311301 .catch((err: unknown) => {
b69ab311302 logger?.error('Could not fetch GK details', err);
b69ab311303 this.postMessage({
b69ab311304 type: 'fetchedGkDetails',
b69ab311305 id: data.id,
b69ab311306 result: {error: err as Error},
b69ab311307 });
b69ab311308 });
b69ab311309 break;
b69ab311310 }
b69ab311311 case 'fetchJkDetails': {
b69ab311312 Internal.fetchJustKnobsByNames?.(ctx, data.names)
b69ab311313 .then((jk: InternalTypes['InternalJustknob']) => {
b69ab311314 this.postMessage({type: 'fetchedJkDetails', id: data.id, result: {value: jk}});
b69ab311315 })
b69ab311316 .catch((err: unknown) => {
b69ab311317 logger?.error('Could not fetch JK details', err);
b69ab311318 this.postMessage({
b69ab311319 type: 'fetchedJkDetails',
b69ab311320 id: data.id,
b69ab311321 result: {error: err as Error},
b69ab311322 });
b69ab311323 });
b69ab311324 break;
b69ab311325 }
b69ab311326 case 'fetchKnobsetDetails': {
b69ab311327 Internal.fetchKnobset?.(ctx, data.configPath)
b69ab311328 .then((knobset: InternalTypes['InternalKnobset']) => {
b69ab311329 this.postMessage({
b69ab311330 type: 'fetchedKnobsetDetails',
b69ab311331 id: data.id,
b69ab311332 result: {value: knobset},
b69ab311333 });
b69ab311334 })
b69ab311335 .catch((err: unknown) => {
b69ab311336 logger?.error('Could not fetch knobset details', err);
b69ab311337 this.postMessage({
b69ab311338 type: 'fetchedKnobsetDetails',
b69ab311339 id: data.id,
b69ab311340 result: {error: err as Error},
b69ab311341 });
b69ab311342 });
b69ab311343 break;
b69ab311344 }
b69ab311345 case 'fetchQeDetails': {
b69ab311346 Internal.fetchQeMetadata?.(ctx, data.name)
b69ab311347 .then((qe: InternalTypes['InternalQuickExperiment']) => {
b69ab311348 this.postMessage({
b69ab311349 type: 'fetchedQeDetails',
b69ab311350 id: data.id,
b69ab311351 result: {value: qe},
b69ab311352 });
b69ab311353 })
b69ab311354 .catch((err: unknown) => {
b69ab311355 logger?.error('Could not fetch QE details', err);
b69ab311356 this.postMessage({
b69ab311357 type: 'fetchedQeDetails',
b69ab311358 id: data.id,
b69ab311359 result: {error: err as Error},
b69ab311360 });
b69ab311361 });
b69ab311362 break;
b69ab311363 }
b69ab311364 case 'fetchABPropDetails': {
b69ab311365 Internal.fetchABPropMetadata?.(ctx, data.name)
b69ab311366 .then((abprop: InternalTypes['InternalMetaConfig']) => {
b69ab311367 this.postMessage({
b69ab311368 type: 'fetchedABPropDetails',
b69ab311369 id: data.id,
b69ab311370 result: {value: abprop},
b69ab311371 });
b69ab311372 })
b69ab311373 .catch((err: unknown) => {
b69ab311374 logger?.error('Could not fetch ABProp details', err);
b69ab311375 this.postMessage({
b69ab311376 type: 'fetchedABPropDetails',
b69ab311377 id: data.id,
b69ab311378 result: {error: err as Error},
b69ab311379 });
b69ab311380 });
b69ab311381 break;
b69ab311382 }
b69ab311383 case 'getRepoUrlAtHash': {
b69ab311384 const args = ['url', '--rev', data.revset];
b69ab311385 // validate that the path is a valid file in repo
b69ab311386 if (data.path != null && absolutePathForFileInRepo(data.path, repo) != null) {
b69ab311387 args.push(`path:${data.path}`);
b69ab311388 }
b69ab311389 repo
b69ab311390 .runCommand(args, 'RepoUrlCommand', ctx)
b69ab311391 .then(result => {
b69ab311392 this.postMessage({
b69ab311393 type: 'gotRepoUrlAtHash',
b69ab311394 url: {value: result.stdout},
b69ab311395 });
b69ab311396 })
b69ab311397 .catch((err: EjecaError) => {
b69ab311398 this.logger.error('Failed to get repo url at hash:', err);
b69ab311399 this.postMessage({
b69ab311400 type: 'gotRepoUrlAtHash',
b69ab311401 url: {error: err},
b69ab311402 });
b69ab311403 });
b69ab311404 break;
b69ab311405 }
b69ab311406 case 'fetchTaskDetails': {
b69ab311407 Internal.getTask?.(ctx, data.taskNumber).then(
b69ab311408 (task: InternalTypes['InternalTaskDetails']) => {
b69ab311409 this.postMessage({type: 'fetchedTaskDetails', id: data.id, result: {value: task}});
b69ab311410 },
b69ab311411 );
b69ab311412 break;
b69ab311413 }
b69ab311414 case 'runDevmateCommand': {
b69ab311415 Internal.runDevmateCommand?.(data.args, data.cwd)
b69ab311416 .then((result: EjecaReturn) => {
b69ab311417 this.postMessage({
b69ab311418 type: 'devmateCommandResult',
b69ab311419 result: {type: 'value', stdout: result.stdout, requestId: data.requestId},
b69ab311420 });
b69ab311421 })
b69ab311422 .catch((error: EjecaError) => {
b69ab311423 this.postMessage({
b69ab311424 type: 'devmateCommandResult',
b69ab311425 result: {type: 'error', stderr: error.stderr, requestId: data.requestId},
b69ab311426 });
b69ab311427 });
b69ab311428 break;
b69ab311429 }
b69ab311430 case 'fetchSubscribedFullRepoBranches': {
b69ab311431 Internal.fetchSubscribedFullRepoBranches?.(ctx, repo)
b69ab311432 .then((branches: Array<InternalTypes['FullRepoBranch']>) => {
b69ab311433 this.postMessage({
b69ab311434 type: 'fetchedSubscribedFullRepoBranches',
b69ab311435 result: {value: branches},
b69ab311436 });
b69ab311437 })
b69ab311438 .catch((error: EjecaError) => {
b69ab311439 this.postMessage({
b69ab311440 type: 'fetchedSubscribedFullRepoBranches',
b69ab311441 result: {error},
b69ab311442 });
b69ab311443 });
b69ab311444 break;
b69ab311445 }
b69ab311446 case 'fetchFullRepoBranchAllChangedFiles': {
b69ab311447 Internal.getFullRepoBranchAllChangedFiles?.(ctx, data.fullRepoBranch)
b69ab311448 .then((paths: Array<ChangedFile>) => {
b69ab311449 this.postMessage({
b69ab311450 type: 'fetchedFullRepoBranchAllChangedFiles',
b69ab311451 id: data.id,
b69ab311452 result: {value: paths},
b69ab311453 });
b69ab311454 })
b69ab311455 .catch((error: EjecaError) => {
b69ab311456 this.postMessage({
b69ab311457 type: 'fetchedFullRepoBranchAllChangedFiles',
b69ab311458 id: data.id,
b69ab311459 result: {error},
b69ab311460 });
b69ab311461 });
b69ab311462 break;
b69ab311463 }
b69ab311464 case 'fetchFullRepoBranchMergeSubtreePaths': {
b69ab311465 Internal.getFullRepoBranchMergeSubtreePaths?.(ctx, data.fullRepoBranch, data.paths)
b69ab311466 .then((paths: Array<string>) => {
b69ab311467 this.postMessage({
b69ab311468 type: 'fetchedFullRepoBranchMergeSubtreePaths',
b69ab311469 id: data.id,
b69ab311470 result: {value: paths},
b69ab311471 });
b69ab311472 })
b69ab311473 .catch((error: EjecaError) => {
b69ab311474 this.postMessage({
b69ab311475 type: 'fetchedFullRepoBranchMergeSubtreePaths',
b69ab311476 id: data.id,
b69ab311477 result: {error},
b69ab311478 });
b69ab311479 });
b69ab311480 break;
b69ab311481 }
b69ab311482 case 'subscribeToFullRepoBranch': {
b69ab311483 Internal.subscribeToFullRepoBranch?.(ctx, repo, data.fullRepoBranch);
b69ab311484 break;
b69ab311485 }
b69ab311486 case 'unsubscribeToFullRepoBranch': {
b69ab311487 Internal.unsubscribeToFullRepoBranch?.(ctx, repo, data.fullRepoBranch);
b69ab311488 break;
b69ab311489 }
b69ab311490 default: {
b69ab311491 if (
b69ab311492 repo.codeReviewProvider?.handleClientToServerMessage?.(data, message =>
b69ab311493 this.postMessage(message),
b69ab311494 ) === true
b69ab311495 ) {
b69ab311496 break;
b69ab311497 }
b69ab311498 this.platform.handleMessageFromClient(
b69ab311499 repo,
b69ab311500 ctx,
b69ab311501 data as Exclude<typeof data, CodeReviewProviderSpecificClientToServerMessages>,
b69ab311502 message => this.postMessage(message),
b69ab311503 (dispose: () => unknown) => {
b69ab311504 this.repoDisposables.push({dispose});
b69ab311505 },
b69ab311506 );
b69ab311507 break;
b69ab311508 }
b69ab311509 }
b69ab311510
b69ab311511 this.notifyListeners(data);
b69ab311512 }
b69ab311513
b69ab311514 private notifyListeners(data: IncomingMessage): void {
b69ab311515 const listeners = this.listenersByType.get(data.type);
b69ab311516 if (listeners) {
b69ab311517 listeners.forEach(handle => handle(data));
b69ab311518 }
b69ab311519 }
b69ab311520
b69ab311521 private async handleFetchCommitMessageTemplate(repo: Repository, ctx: RepositoryContext) {
b69ab311522 const {logger} = ctx;
b69ab311523 try {
b69ab311524 const [result, customTemplate] = await Promise.all([
b69ab311525 repo.runCommand(['debugcommitmessage', 'isl'], 'FetchCommitTemplateCommand', ctx),
b69ab311526 Internal.getCustomDefaultCommitTemplate?.(repo.initialConnectionContext),
b69ab311527 ]);
b69ab311528
b69ab311529 let template = result.stdout
b69ab311530 .replace(repo.IGNORE_COMMIT_MESSAGE_LINES_REGEX, '')
b69ab311531 .replace(/^<Replace this line with a title. Use 1 line only, 67 chars or less>/, '');
b69ab311532
b69ab311533 if (customTemplate && customTemplate?.trim() !== '') {
b69ab311534 template = customTemplate as string;
b69ab311535
b69ab311536 this.tracker.track('UseCustomCommitMessageTemplate');
b69ab311537 }
b69ab311538
b69ab311539 this.postMessage({
b69ab311540 type: 'fetchedCommitMessageTemplate',
b69ab311541 template,
b69ab311542 });
b69ab311543 } catch (err) {
b69ab311544 logger?.error('Could not fetch commit message template', err);
b69ab311545 }
b69ab311546 }
b69ab311547}