addons/isl-server/src/__tests__/Repository.test.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 {AbsolutePath, RunnableOperation, Submodule} from 'isl/src/types';
b69ab319import type {ResolveCommandConflictOutput} from '../commands';
b69ab3110import type {ServerPlatform} from '../serverPlatform';
b69ab3111import type {RepositoryContext} from '../serverTypes';
b69ab3112
b69ab3113import {CommandRunner, type MergeConflicts, type ValidatedRepoInfo} from 'isl/src/types';
b69ab3114import fs from 'node:fs';
b69ab3115import os from 'node:os';
b69ab3116import path from 'node:path';
b69ab3117import * as ejeca from 'shared/ejeca';
b69ab3118import * as fsUtils from 'shared/fs';
b69ab3119import {clone, mockLogger, nextTick} from 'shared/testUtils';
b69ab3120import {absolutePathForFileInRepo, Repository} from '../Repository';
b69ab3121import {makeServerSideTracker} from '../analytics/serverSideTracker';
b69ab3122import {extractRepoInfoFromUrl, setConfigOverrideForTests} from '../commands';
b69ab3123
b69ab3124/* eslint-disable require-await */
b69ab3125
b69ab3126jest.mock('../WatchForChanges', () => {
b69ab3127 class MockWatchForChanges {
b69ab3128 dispose = jest.fn();
b69ab3129 poll = jest.fn();
b69ab3130 }
b69ab3131 return {WatchForChanges: MockWatchForChanges};
b69ab3132});
b69ab3133
b69ab3134const mockTracker = makeServerSideTracker(
b69ab3135 mockLogger,
b69ab3136 {platformName: 'test'} as ServerPlatform,
b69ab3137 '0.1',
b69ab3138 jest.fn(),
b69ab3139);
b69ab3140
b69ab3141function mockEjeca(
b69ab3142 cmds: Array<[RegExp, (() => {stdout: string} | Error) | {stdout: string} | Error]>,
b69ab3143) {
b69ab3144 return jest.spyOn(ejeca, 'ejeca').mockImplementation(((cmd: string, args: Array<string>) => {
b69ab3145 const argStr = cmd + ' ' + args?.join(' ');
b69ab3146 const ejecaOther = {
b69ab3147 kill: jest.fn(),
b69ab3148 on: jest.fn((event, cb) => {
b69ab3149 // immediately call exit cb to teardown timeout
b69ab3150 if (event === 'exit') {
b69ab3151 cb();
b69ab3152 }
b69ab3153 }),
b69ab3154 };
b69ab3155 for (const [regex, output] of cmds) {
b69ab3156 if (regex.test(argStr)) {
b69ab3157 let value = output;
b69ab3158 if (typeof output === 'function') {
b69ab3159 value = output();
b69ab3160 }
b69ab3161 if (value instanceof Error) {
b69ab3162 throw value;
b69ab3163 }
b69ab3164 return {...ejecaOther, ...value};
b69ab3165 }
b69ab3166 }
b69ab3167 return {...ejecaOther, stdout: ''};
b69ab3168 }) as unknown as typeof ejeca.ejeca);
b69ab3169}
b69ab3170
b69ab3171function processExitError(code: number, message: string): ejeca.EjecaError {
b69ab3172 const err = new Error(message) as ejeca.EjecaError;
b69ab3173 err.exitCode = code;
b69ab3174 return err;
b69ab3175}
b69ab3176
b69ab3177function setPathsDefault(path: string) {
b69ab3178 setConfigOverrideForTests([['paths.default', path]], false);
b69ab3179}
b69ab3180
b69ab3181describe('Repository', () => {
b69ab3182 let ctx: RepositoryContext;
b69ab3183 beforeEach(() => {
b69ab3184 ctx = {
b69ab3185 cmd: 'sl',
b69ab3186 cwd: '/path/to/cwd',
b69ab3187 logger: mockLogger,
b69ab3188 tracker: mockTracker,
b69ab3189 };
b69ab3190 });
b69ab3191
b69ab3192 it('setting command name', async () => {
b69ab3193 const ejecaSpy = mockEjeca([]);
b69ab3194 await Repository.getRepoInfo({...ctx, cmd: 'slb'});
b69ab3195 expect(ejecaSpy).toHaveBeenCalledWith(
b69ab3196 'slb',
b69ab3197 expect.arrayContaining(['root']),
b69ab3198 expect.anything(),
b69ab3199 );
b69ab31100 });
b69ab31101
b69ab31102 describe('extracting github repo info', () => {
b69ab31103 beforeEach(() => {
b69ab31104 setConfigOverrideForTests([['github.pull_request_domain', 'github.com']]);
b69ab31105 mockEjeca([
b69ab31106 [/^sl root --dotdir/, {stdout: '/path/to/myRepo/.sl'}],
b69ab31107 [/^sl root/, {stdout: '/path/to/myRepo'}],
b69ab31108 [/^sl debugroots/, {stdout: '/path/to/myRepo'}],
b69ab31109 [
b69ab31110 /^gh auth status --hostname gitlab.myCompany.com/,
b69ab31111 new Error('not authenticated on this hostname'),
b69ab31112 ],
b69ab31113 [/^gh auth status --hostname ghe.myCompany.com/, {stdout: ''}],
b69ab31114 ]);
b69ab31115 });
b69ab31116
b69ab31117 it('extracting github repo info', async () => {
b69ab31118 setPathsDefault('https://github.com/myUsername/myRepo.git');
b69ab31119 const info = (await Repository.getRepoInfo(ctx)) as ValidatedRepoInfo;
b69ab31120 const repo = new Repository(info, ctx);
b69ab31121 expect(repo.info).toEqual({
b69ab31122 type: 'success',
b69ab31123 command: 'sl',
b69ab31124 repoRoot: '/path/to/myRepo',
b69ab31125 repoRoots: ['/path/to/myRepo'],
b69ab31126 dotdir: '/path/to/myRepo/.sl',
b69ab31127 codeReviewSystem: {
b69ab31128 type: 'github',
b69ab31129 owner: 'myUsername',
b69ab31130 repo: 'myRepo',
b69ab31131 hostname: 'github.com',
b69ab31132 },
b69ab31133 pullRequestDomain: 'github.com',
b69ab31134 isEdenFs: false,
b69ab31135 });
b69ab31136 });
b69ab31137
b69ab31138 it('extracting github enterprise repo info', async () => {
b69ab31139 setPathsDefault('https://ghe.myCompany.com/myUsername/myRepo.git');
b69ab31140 const info = (await Repository.getRepoInfo(ctx)) as ValidatedRepoInfo;
b69ab31141 const repo = new Repository(info, ctx);
b69ab31142 expect(repo.info).toEqual({
b69ab31143 type: 'success',
b69ab31144 command: 'sl',
b69ab31145 repoRoot: '/path/to/myRepo',
b69ab31146 repoRoots: ['/path/to/myRepo'],
b69ab31147 dotdir: '/path/to/myRepo/.sl',
b69ab31148 codeReviewSystem: {
b69ab31149 type: 'github',
b69ab31150 owner: 'myUsername',
b69ab31151 repo: 'myRepo',
b69ab31152 hostname: 'ghe.myCompany.com',
b69ab31153 },
b69ab31154 pullRequestDomain: 'github.com',
b69ab31155 isEdenFs: false,
b69ab31156 });
b69ab31157 });
b69ab31158
b69ab31159 it('handles non-github-enterprise unknown code review providers', async () => {
b69ab31160 setPathsDefault('https://gitlab.myCompany.com/myUsername/myRepo.git');
b69ab31161 const info = (await Repository.getRepoInfo(ctx)) as ValidatedRepoInfo;
b69ab31162 const repo = new Repository(info, ctx);
b69ab31163 expect(repo.info).toEqual({
b69ab31164 type: 'success',
b69ab31165 command: 'sl',
b69ab31166 repoRoot: '/path/to/myRepo',
b69ab31167 repoRoots: ['/path/to/myRepo'],
b69ab31168 dotdir: '/path/to/myRepo/.sl',
b69ab31169 codeReviewSystem: {
b69ab31170 type: 'unknown',
b69ab31171 path: 'https://gitlab.myCompany.com/myUsername/myRepo.git',
b69ab31172 },
b69ab31173 pullRequestDomain: 'github.com',
b69ab31174 isEdenFs: false,
b69ab31175 });
b69ab31176 });
b69ab31177 });
b69ab31178
b69ab31179 it('applies isl.hold-off-refresh-ms config', async () => {
b69ab31180 setConfigOverrideForTests([['isl.hold-off-refresh-ms', '12345']], false);
b69ab31181 const info = (await Repository.getRepoInfo(ctx)) as ValidatedRepoInfo;
b69ab31182 const repo = new Repository(info, ctx);
b69ab31183 await new Promise(process.nextTick);
b69ab31184 expect(repo.configHoldOffRefreshMs).toBe(12345);
b69ab31185 });
b69ab31186
b69ab31187 it('extracting repo info', async () => {
b69ab31188 setConfigOverrideForTests([]);
b69ab31189 setPathsDefault('mononoke://0.0.0.0/fbsource');
b69ab31190 mockEjeca([
b69ab31191 [/^sl root --dotdir/, {stdout: '/path/to/myRepo/.sl'}],
b69ab31192 [/^sl root/, {stdout: '/path/to/myRepo'}],
b69ab31193 [/^sl debugroots/, {stdout: '/path/to/myRepo/submodule\n/path/to/myRepo'}],
b69ab31194 ]);
b69ab31195 const info = (await Repository.getRepoInfo(ctx)) as ValidatedRepoInfo;
b69ab31196 const repo = new Repository(info, ctx);
b69ab31197 expect(repo.info).toEqual({
b69ab31198 type: 'success',
b69ab31199 command: 'sl',
b69ab31200 repoRoot: '/path/to/myRepo',
b69ab31201 repoRoots: ['/path/to/myRepo', '/path/to/myRepo/submodule'],
b69ab31202 dotdir: '/path/to/myRepo/.sl',
b69ab31203 codeReviewSystem: expect.anything(),
b69ab31204 pullRequestDomain: undefined,
b69ab31205 isEdenFs: false,
b69ab31206 });
b69ab31207 });
b69ab31208
b69ab31209 it('handles cwd not exists', async () => {
b69ab31210 const err = new Error('cwd does not exist') as Error & {code: string};
b69ab31211 err.code = 'ENOENT';
b69ab31212 mockEjeca([[/^sl root/, err]]);
b69ab31213 const info = (await Repository.getRepoInfo(ctx)) as ValidatedRepoInfo;
b69ab31214 expect(info).toEqual({
b69ab31215 type: 'cwdDoesNotExist',
b69ab31216 cwd: '/path/to/cwd',
b69ab31217 });
b69ab31218 });
b69ab31219
b69ab31220 it('handles missing executables on windows', async () => {
b69ab31221 const osSpy = jest.spyOn(os, 'platform').mockImplementation(() => 'win32');
b69ab31222 mockEjeca([
b69ab31223 [
b69ab31224 /^sl root/,
b69ab31225 processExitError(
b69ab31226 /* code */ 1,
b69ab31227 `'sl' is not recognized as an internal or external command, operable program or batch file.`,
b69ab31228 ),
b69ab31229 ],
b69ab31230 ]);
b69ab31231 jest.spyOn(fsUtils, 'exists').mockImplementation(async () => true);
b69ab31232 const info = (await Repository.getRepoInfo(ctx)) as ValidatedRepoInfo;
b69ab31233 expect(info).toEqual({
b69ab31234 type: 'invalidCommand',
b69ab31235 command: 'sl',
b69ab31236 path: expect.anything(),
b69ab31237 });
b69ab31238 osSpy.mockRestore();
b69ab31239 });
b69ab31240
b69ab31241 it('prevents setting configs not in the allowlist', async () => {
b69ab31242 setConfigOverrideForTests([]);
b69ab31243 setPathsDefault('mononoke://0.0.0.0/fbsource');
b69ab31244 mockEjeca([
b69ab31245 [/^sl root --dotdir/, {stdout: '/path/to/myRepo/.sl'}],
b69ab31246 [/^sl root/, {stdout: '/path/to/myRepo'}],
b69ab31247 ]);
b69ab31248 const info = (await Repository.getRepoInfo(ctx)) as ValidatedRepoInfo;
b69ab31249 const repo = new Repository(info, ctx);
b69ab31250 // @ts-expect-error We expect a type error in addition to runtime validation
b69ab31251 await expect(repo.setConfig(ctx, 'user', 'some-random-config', 'hi')).rejects.toEqual(
b69ab31252 new Error('config some-random-config not in allowlist for settable configs'),
b69ab31253 );
b69ab31254 });
b69ab31255
b69ab31256 describe('running operations', () => {
b69ab31257 const repoInfo: ValidatedRepoInfo = {
b69ab31258 type: 'success',
b69ab31259 command: 'sl',
b69ab31260 dotdir: '/path/to/repo/.sl',
b69ab31261 repoRoot: '/path/to/repo',
b69ab31262 codeReviewSystem: {type: 'unknown'},
b69ab31263 pullRequestDomain: undefined,
b69ab31264 isEdenFs: false,
b69ab31265 };
b69ab31266
b69ab31267 let ejecaSpy: ReturnType<typeof mockEjeca>;
b69ab31268 beforeEach(() => {
b69ab31269 ejecaSpy = mockEjeca([]);
b69ab31270 });
b69ab31271
b69ab31272 async function runOperation(op: Partial<RunnableOperation>) {
b69ab31273 const repo = new Repository(repoInfo, ctx);
b69ab31274 const progressSpy = jest.fn();
b69ab31275
b69ab31276 await repo.runOrQueueOperation(
b69ab31277 ctx,
b69ab31278 {
b69ab31279 id: '1',
b69ab31280 trackEventName: 'CommitOperation',
b69ab31281 args: [],
b69ab31282 runner: CommandRunner.Sapling,
b69ab31283 ...op,
b69ab31284 },
b69ab31285 progressSpy,
b69ab31286 );
b69ab31287 }
b69ab31288
b69ab31289 it('runs operations', async () => {
b69ab31290 await runOperation({
b69ab31291 args: ['commit', '--message', 'hi'],
b69ab31292 });
b69ab31293
b69ab31294 expect(ejecaSpy).toHaveBeenCalledWith(
b69ab31295 'sl',
b69ab31296 ['commit', '--message', 'hi', '--noninteractive'],
b69ab31297 expect.anything(),
b69ab31298 );
b69ab31299 });
b69ab31300
b69ab31301 it('handles succeedable revsets', async () => {
b69ab31302 await runOperation({
b69ab31303 args: ['rebase', '--rev', {type: 'succeedable-revset', revset: 'aaa'}],
b69ab31304 });
b69ab31305
b69ab31306 expect(ejecaSpy).toHaveBeenCalledWith(
b69ab31307 'sl',
b69ab31308 ['rebase', '--rev', 'max(successors(aaa))', '--noninteractive'],
b69ab31309 expect.anything(),
b69ab31310 );
b69ab31311 });
b69ab31312
b69ab31313 it('handles exact revsets', async () => {
b69ab31314 await runOperation({
b69ab31315 args: ['rebase', '--rev', {type: 'exact-revset', revset: 'aaa'}],
b69ab31316 });
b69ab31317
b69ab31318 expect(ejecaSpy).toHaveBeenCalledWith(
b69ab31319 'sl',
b69ab31320 ['rebase', '--rev', 'aaa', '--noninteractive'],
b69ab31321 expect.anything(),
b69ab31322 );
b69ab31323 });
b69ab31324
b69ab31325 it('handles repo-relative files', async () => {
b69ab31326 await runOperation({
b69ab31327 args: ['add', {type: 'repo-relative-file', path: 'path/to/file.txt'}],
b69ab31328 });
b69ab31329
b69ab31330 expect(ejecaSpy).toHaveBeenCalledWith(
b69ab31331 'sl',
b69ab31332 ['add', '../repo/path/to/file.txt', '--noninteractive'],
b69ab31333 expect.anything(),
b69ab31334 );
b69ab31335 });
b69ab31336
b69ab31337 it('handles allowed configs', async () => {
b69ab31338 await runOperation({
b69ab31339 args: ['commit', {type: 'config', key: 'ui.allowemptycommit', value: 'True'}],
b69ab31340 });
b69ab31341
b69ab31342 expect(ejecaSpy).toHaveBeenCalledWith(
b69ab31343 'sl',
b69ab31344 ['commit', '--config', 'ui.allowemptycommit=True', '--noninteractive'],
b69ab31345 expect.anything(),
b69ab31346 );
b69ab31347 });
b69ab31348
b69ab31349 it('disallows some commands', async () => {
b69ab31350 await runOperation({
b69ab31351 args: ['debugsh'],
b69ab31352 });
b69ab31353
b69ab31354 expect(ejecaSpy).not.toHaveBeenCalledWith(
b69ab31355 'sl',
b69ab31356 ['debugsh', '--noninteractive'],
b69ab31357 expect.anything(),
b69ab31358 );
b69ab31359 });
b69ab31360
b69ab31361 it('disallows unknown configs', async () => {
b69ab31362 await runOperation({
b69ab31363 args: ['commit', {type: 'config', key: 'foo.bar', value: '1'}],
b69ab31364 });
b69ab31365
b69ab31366 expect(ejecaSpy).not.toHaveBeenCalledWith(
b69ab31367 'sl',
b69ab31368 expect.arrayContaining(['commit', '--config', 'foo.bar=1']),
b69ab31369 expect.anything(),
b69ab31370 );
b69ab31371 });
b69ab31372
b69ab31373 it('disallows unstructured --config flag', async () => {
b69ab31374 await runOperation({
b69ab31375 args: ['commit', '--config', 'foo.bar=1'],
b69ab31376 });
b69ab31377
b69ab31378 expect(ejecaSpy).not.toHaveBeenCalledWith(
b69ab31379 'sl',
b69ab31380 expect.arrayContaining(['commit', '--config', 'foo.bar=1']),
b69ab31381 expect.anything(),
b69ab31382 );
b69ab31383 });
b69ab31384 });
b69ab31385
b69ab31386 describe('fetchSloc', () => {
b69ab31387 const repoInfo: ValidatedRepoInfo = {
b69ab31388 type: 'success',
b69ab31389 command: 'sl',
b69ab31390 dotdir: '/path/to/repo/.sl',
b69ab31391 repoRoot: '/path/to/repo',
b69ab31392 codeReviewSystem: {type: 'unknown'},
b69ab31393 pullRequestDomain: undefined,
b69ab31394 isEdenFs: false,
b69ab31395 };
b69ab31396
b69ab31397 const EXAMPLE_DIFFSTAT = `
b69ab31398| 34 ++++++++++
b69ab31399www/flib/intern/entity/diff/EntPhabricatorDiffSchema.php | 11 +++
b69ab314002 files changed, 45 insertions(+), 0 deletions(-)\n`;
b69ab31401
b69ab31402 it('parses sloc', async () => {
b69ab31403 const repo = new Repository(repoInfo, ctx);
b69ab31404
b69ab31405 const ejecaSpy = mockEjeca([[/^sl diff/, () => ({stdout: EXAMPLE_DIFFSTAT})]]);
b69ab31406 const results = repo.fetchSignificantLinesOfCode(ctx, 'abcdef', ['generated.file']);
b69ab31407 await expect(results).resolves.toEqual(45);
b69ab31408 expect(ejecaSpy).toHaveBeenCalledWith(
b69ab31409 'sl',
b69ab31410 expect.arrayContaining([
b69ab31411 'diff',
b69ab31412 '-B',
b69ab31413 '-X',
b69ab31414 '**__generated__**',
b69ab31415 '-X',
b69ab31416 '/path/to/repo/generated.file',
b69ab31417 '-c',
b69ab31418 'abcdef',
b69ab31419 ]),
b69ab31420 expect.anything(),
b69ab31421 );
b69ab31422 });
b69ab31423
b69ab31424 it('handles empty generated list', async () => {
b69ab31425 const repo = new Repository(repoInfo, ctx);
b69ab31426 const ejecaSpy = mockEjeca([[/^sl diff/, () => ({stdout: EXAMPLE_DIFFSTAT})]]);
b69ab31427 repo.fetchSignificantLinesOfCode(ctx, 'abcdef', []);
b69ab31428 expect(ejecaSpy).toHaveBeenCalledWith(
b69ab31429 'sl',
b69ab31430 expect.arrayContaining(['diff', '-B', '-X', '**__generated__**', '-c', 'abcdef']),
b69ab31431 expect.anything(),
b69ab31432 );
b69ab31433 });
b69ab31434
b69ab31435 it('handles multiple generated files', async () => {
b69ab31436 const repo = new Repository(repoInfo, ctx);
b69ab31437 const ejecaSpy = mockEjeca([[/^sl diff/, () => ({stdout: EXAMPLE_DIFFSTAT})]]);
b69ab31438 const generatedFiles = ['generated1.file', 'generated2.file'];
b69ab31439 repo.fetchSignificantLinesOfCode(ctx, 'abcdef', generatedFiles);
b69ab31440 await nextTick();
b69ab31441 expect(ejecaSpy).toHaveBeenCalledWith(
b69ab31442 'sl',
b69ab31443 expect.arrayContaining([
b69ab31444 'diff',
b69ab31445 '-B',
b69ab31446 '-X',
b69ab31447 '**__generated__**',
b69ab31448 '-X',
b69ab31449 '/path/to/repo/generated1.file',
b69ab31450 '-X',
b69ab31451 '/path/to/repo/generated2.file',
b69ab31452 '-c',
b69ab31453 'abcdef',
b69ab31454 ]),
b69ab31455 expect.anything(),
b69ab31456 );
b69ab31457 });
b69ab31458 });
b69ab31459
b69ab31460 describe('fetchSmartlogCommits', () => {
b69ab31461 const repoInfo: ValidatedRepoInfo = {
b69ab31462 type: 'success',
b69ab31463 command: 'sl',
b69ab31464 dotdir: '/path/to/repo/.sl',
b69ab31465 repoRoot: '/path/to/repo',
b69ab31466 codeReviewSystem: {type: 'unknown'},
b69ab31467 pullRequestDomain: undefined,
b69ab31468 isEdenFs: false,
b69ab31469 };
b69ab31470
b69ab31471 const expectCalledWithRevset = (spy: jest.SpyInstance<unknown>, revset: string) => {
b69ab31472 expect(spy).toHaveBeenCalledWith(
b69ab31473 'sl',
b69ab31474 expect.arrayContaining(['log', '--rev', revset]),
b69ab31475 expect.anything(),
b69ab31476 );
b69ab31477 };
b69ab31478
b69ab31479 it('uses correct revset in normal case', async () => {
b69ab31480 const repo = new Repository(repoInfo, ctx);
b69ab31481
b69ab31482 const ejecaSpy = mockEjeca([]);
b69ab31483
b69ab31484 await repo.fetchSmartlogCommits();
b69ab31485 expectCalledWithRevset(
b69ab31486 ejecaSpy,
b69ab31487 'smartlog(((interestingbookmarks() + heads(draft())) & date(-14)) + .)',
b69ab31488 );
b69ab31489 });
b69ab31490
b69ab31491 it('updates revset when changing date range', async () => {
b69ab31492 const ejecaSpy = mockEjeca([]);
b69ab31493 const repo = new Repository(repoInfo, ctx);
b69ab31494
b69ab31495 repo.nextVisibleCommitRangeInDays();
b69ab31496 await repo.fetchSmartlogCommits();
b69ab31497 expectCalledWithRevset(
b69ab31498 ejecaSpy,
b69ab31499 'smartlog(((interestingbookmarks() + heads(draft())) & date(-60)) + .)',
b69ab31500 );
b69ab31501
b69ab31502 repo.nextVisibleCommitRangeInDays();
b69ab31503 await repo.fetchSmartlogCommits();
b69ab31504 expectCalledWithRevset(ejecaSpy, 'smartlog((interestingbookmarks() + heads(draft())) + .)');
b69ab31505 });
b69ab31506
b69ab31507 it('fetches additional revsets', async () => {
b69ab31508 const ejecaSpy = mockEjeca([]);
b69ab31509 const repo = new Repository(repoInfo, ctx);
b69ab31510
b69ab31511 repo.stableLocations = [
b69ab31512 {name: 'mystable', hash: 'aaa', info: 'this is the stable for aaa', date: new Date(0)},
b69ab31513 ];
b69ab31514 await repo.fetchSmartlogCommits();
b69ab31515 expectCalledWithRevset(
b69ab31516 ejecaSpy,
b69ab31517 'smartlog(((interestingbookmarks() + heads(draft())) & date(-14)) + . + present(aaa))',
b69ab31518 );
b69ab31519
b69ab31520 repo.stableLocations = [
b69ab31521 {name: 'mystable', hash: 'aaa', info: 'this is the stable for aaa', date: new Date(0)},
b69ab31522 {name: '2', hash: 'bbb', info: '2', date: new Date(0)},
b69ab31523 ];
b69ab31524 await repo.fetchSmartlogCommits();
b69ab31525 expectCalledWithRevset(
b69ab31526 ejecaSpy,
b69ab31527 'smartlog(((interestingbookmarks() + heads(draft())) & date(-14)) + . + present(aaa) + present(bbb))',
b69ab31528 );
b69ab31529
b69ab31530 repo.nextVisibleCommitRangeInDays();
b69ab31531 repo.nextVisibleCommitRangeInDays();
b69ab31532 await repo.fetchSmartlogCommits();
b69ab31533 expectCalledWithRevset(
b69ab31534 ejecaSpy,
b69ab31535 'smartlog((interestingbookmarks() + heads(draft())) + . + present(aaa) + present(bbb))',
b69ab31536 );
b69ab31537 });
b69ab31538 });
b69ab31539
b69ab31540 describe('merge conflicts', () => {
b69ab31541 const repoInfo: ValidatedRepoInfo = {
b69ab31542 type: 'success',
b69ab31543 command: 'sl',
b69ab31544 dotdir: '/path/to/repo/.sl',
b69ab31545 repoRoot: '/path/to/repo',
b69ab31546 codeReviewSystem: {type: 'unknown'},
b69ab31547 pullRequestDomain: undefined,
b69ab31548 isEdenFs: false,
b69ab31549 };
b69ab31550 const NOT_IN_CONFLICT: ResolveCommandConflictOutput = [
b69ab31551 {
b69ab31552 command: null,
b69ab31553 conflicts: [],
b69ab31554 pathconflicts: [],
b69ab31555 },
b69ab31556 ];
b69ab31557
b69ab31558 const conflictFileData = (contents: string) => ({
b69ab31559 contents,
b69ab31560 exists: true,
b69ab31561 isexec: false,
b69ab31562 issymlink: false,
b69ab31563 });
b69ab31564 const MARK_IN = '<'.repeat(7) + ` dest: aaaaaaaaaaaa - unixname: Commit A`;
b69ab31565 const MARK_OUT = '>'.repeat(7) + ` source: bbbbbbbbbbbb - unixname: Commit B`;
b69ab31566 const MARK_BASE_START = `||||||| base`;
b69ab31567 const MARK_BASE_END = `=======`;
b69ab31568
b69ab31569 const MOCK_CONFLICT: ResolveCommandConflictOutput = [
b69ab31570 {
b69ab31571 command: 'rebase',
b69ab31572 command_details: {
b69ab31573 cmd: 'rebase',
b69ab31574 to_abort: 'rebase --abort',
b69ab31575 to_continue: 'rebase --continue',
b69ab31576 },
b69ab31577 conflicts: [
b69ab31578 {
b69ab31579 base: conflictFileData('hello\nworld\n'),
b69ab31580 local: conflictFileData('hello\nworld - modified 1\n'),
b69ab31581 other: conflictFileData('hello\nworld - modified 2\n'),
b69ab31582 output: conflictFileData(
b69ab31583 `\
b69ab31584hello
b69ab31585${MARK_IN}
b69ab31586world - modified 1
b69ab31587${MARK_BASE_START}
b69ab31588world
b69ab31589${MARK_BASE_END}
b69ab31590modified 2
b69ab31591${MARK_OUT}
b69ab31592`,
b69ab31593 ),
b69ab31594 path: 'file1.txt',
b69ab31595 },
b69ab31596 {
b69ab31597 base: conflictFileData('hello\nworld\n'),
b69ab31598 local: conflictFileData('hello\nworld - modified 1\n'),
b69ab31599 other: conflictFileData('hello\nworld - modified 2\n'),
b69ab31600 output: conflictFileData(
b69ab31601 `\
b69ab31602hello
b69ab31603${MARK_IN}
b69ab31604world - modified 1
b69ab31605${MARK_BASE_START}
b69ab31606world
b69ab31607${MARK_BASE_END}
b69ab31608modified 2
b69ab31609${MARK_OUT}
b69ab31610`,
b69ab31611 ),
b69ab31612 path: 'file2.txt',
b69ab31613 },
b69ab31614 ],
b69ab31615 pathconflicts: [],
b69ab31616 },
b69ab31617 ];
b69ab31618
b69ab31619 // same as MOCK_CONFLICT, but without any data for file1.txt
b69ab31620 const MOCK_CONFLICT_WITH_FILE1_RESOLVED: ResolveCommandConflictOutput = clone(MOCK_CONFLICT);
b69ab31621 MOCK_CONFLICT_WITH_FILE1_RESOLVED[0].conflicts.splice(0, 1);
b69ab31622
b69ab31623 // these mock values are returned by ejeca / fs mocks
b69ab31624 // default: start in a not-in-conflict state
b69ab31625 let slMergeDirExists = false;
b69ab31626 let conflictData: ResolveCommandConflictOutput = NOT_IN_CONFLICT;
b69ab31627
b69ab31628 /**
b69ab31629 * the next time repo.checkForMergeConflicts is called, this new conflict data will be used
b69ab31630 */
b69ab31631 function enterMergeConflict(conflict: ResolveCommandConflictOutput) {
b69ab31632 slMergeDirExists = true;
b69ab31633 conflictData = conflict;
b69ab31634 }
b69ab31635
b69ab31636 beforeEach(() => {
b69ab31637 slMergeDirExists = false;
b69ab31638 conflictData = NOT_IN_CONFLICT;
b69ab31639
b69ab31640 jest.spyOn(fsUtils, 'exists').mockImplementation(() => Promise.resolve(slMergeDirExists));
b69ab31641
b69ab31642 mockEjeca([
b69ab31643 [
b69ab31644 /^sl resolve --tool internal:dumpjson --all/,
b69ab31645 () => ({stdout: JSON.stringify(conflictData)}),
b69ab31646 ],
b69ab31647 ]);
b69ab31648 });
b69ab31649
b69ab31650 it('checks for merge conflicts', async () => {
b69ab31651 const repo = new Repository(repoInfo, ctx);
b69ab31652
b69ab31653 const onChange = jest.fn();
b69ab31654 repo.onChangeConflictState(onChange);
b69ab31655
b69ab31656 await repo.checkForMergeConflicts();
b69ab31657 expect(onChange).toHaveBeenCalledTimes(0);
b69ab31658
b69ab31659 enterMergeConflict(MOCK_CONFLICT);
b69ab31660
b69ab31661 await repo.checkForMergeConflicts();
b69ab31662
b69ab31663 expect(onChange).toHaveBeenCalledWith({state: 'loading'});
b69ab31664 expect(onChange).toHaveBeenCalledWith({
b69ab31665 state: 'loaded',
b69ab31666 command: 'rebase',
b69ab31667 toContinue: 'rebase --continue',
b69ab31668 toAbort: 'rebase --abort',
b69ab31669 files: [
b69ab31670 {path: 'file1.txt', status: 'U', conflictType: 'both_changed'},
b69ab31671 {path: 'file2.txt', status: 'U', conflictType: 'both_changed'},
b69ab31672 ],
b69ab31673 fetchStartTimestamp: expect.anything(),
b69ab31674 fetchCompletedTimestamp: expect.anything(),
b69ab31675 } as MergeConflicts);
b69ab31676 });
b69ab31677
b69ab31678 it('shows deleted file conflicts', async () => {
b69ab31679 const repo = new Repository(repoInfo, ctx);
b69ab31680
b69ab31681 const onChange = jest.fn();
b69ab31682 repo.onChangeConflictState(onChange);
b69ab31683
b69ab31684 await repo.checkForMergeConflicts();
b69ab31685 expect(onChange).toHaveBeenCalledTimes(0);
b69ab31686
b69ab31687 const MOCK_DELETED_CONFLICT: ResolveCommandConflictOutput = [
b69ab31688 {
b69ab31689 command: 'rebase',
b69ab31690 command_details: {
b69ab31691 cmd: 'rebase',
b69ab31692 to_abort: 'rebase --abort',
b69ab31693 to_continue: 'rebase --continue',
b69ab31694 },
b69ab31695 conflicts: [
b69ab31696 {
b69ab31697 base: conflictFileData('hello\nworld\n'),
b69ab31698 local: conflictFileData('hello\nworld - modified 1\n'),
b69ab31699 other: {
b69ab31700 contents: null,
b69ab31701 exists: false,
b69ab31702 isexec: false,
b69ab31703 issymlink: false,
b69ab31704 },
b69ab31705 output: {
b69ab31706 contents: null,
b69ab31707 exists: false,
b69ab31708 isexec: false,
b69ab31709 issymlink: false,
b69ab31710 },
b69ab31711 path: 'file_del1.txt',
b69ab31712 },
b69ab31713 {
b69ab31714 base: conflictFileData('hello\nworld\n'),
b69ab31715 local: {
b69ab31716 contents: null,
b69ab31717 exists: false,
b69ab31718 isexec: false,
b69ab31719 issymlink: false,
b69ab31720 },
b69ab31721 other: conflictFileData('hello\nworld - modified 2\n'),
b69ab31722 output: conflictFileData('hello\nworld - modified 2\n'),
b69ab31723 path: 'file_del2.txt',
b69ab31724 },
b69ab31725 ],
b69ab31726 pathconflicts: [],
b69ab31727 },
b69ab31728 ];
b69ab31729
b69ab31730 enterMergeConflict(MOCK_DELETED_CONFLICT);
b69ab31731
b69ab31732 await repo.checkForMergeConflicts();
b69ab31733
b69ab31734 expect(onChange).toHaveBeenCalledWith({state: 'loading'});
b69ab31735 expect(onChange).toHaveBeenCalledWith({
b69ab31736 state: 'loaded',
b69ab31737 command: 'rebase',
b69ab31738 toContinue: 'rebase --continue',
b69ab31739 toAbort: 'rebase --abort',
b69ab31740 files: [
b69ab31741 {path: 'file_del1.txt', status: 'U', conflictType: 'source_deleted'},
b69ab31742 {path: 'file_del2.txt', status: 'U', conflictType: 'dest_deleted'},
b69ab31743 ],
b69ab31744 fetchStartTimestamp: expect.anything(),
b69ab31745 fetchCompletedTimestamp: expect.anything(),
b69ab31746 } as MergeConflicts);
b69ab31747 });
b69ab31748
b69ab31749 it('disposes conflict change subscriptions', async () => {
b69ab31750 const repo = new Repository(repoInfo, ctx);
b69ab31751
b69ab31752 const onChange = jest.fn();
b69ab31753 const subscription = repo.onChangeConflictState(onChange);
b69ab31754 subscription.dispose();
b69ab31755
b69ab31756 enterMergeConflict(MOCK_CONFLICT);
b69ab31757 await repo.checkForMergeConflicts();
b69ab31758 expect(onChange).toHaveBeenCalledTimes(0);
b69ab31759 });
b69ab31760
b69ab31761 it('sends conflicts right away on subscription if already in conflicts', async () => {
b69ab31762 enterMergeConflict(MOCK_CONFLICT);
b69ab31763
b69ab31764 const repo = new Repository(repoInfo, ctx);
b69ab31765
b69ab31766 const onChange = jest.fn();
b69ab31767 repo.onChangeConflictState(onChange);
b69ab31768 await nextTick(); // allow message to get sent
b69ab31769
b69ab31770 expect(onChange).toHaveBeenCalledWith({state: 'loading'});
b69ab31771 expect(onChange).toHaveBeenCalledWith({
b69ab31772 state: 'loaded',
b69ab31773 command: 'rebase',
b69ab31774 toContinue: 'rebase --continue',
b69ab31775 toAbort: 'rebase --abort',
b69ab31776 files: [
b69ab31777 {path: 'file1.txt', status: 'U', conflictType: 'both_changed'},
b69ab31778 {path: 'file2.txt', status: 'U', conflictType: 'both_changed'},
b69ab31779 ],
b69ab31780 fetchStartTimestamp: expect.anything(),
b69ab31781 fetchCompletedTimestamp: expect.anything(),
b69ab31782 });
b69ab31783 });
b69ab31784
b69ab31785 it('preserves previous conflicts as resolved', async () => {
b69ab31786 const repo = new Repository(repoInfo, ctx);
b69ab31787 const onChange = jest.fn();
b69ab31788 repo.onChangeConflictState(onChange);
b69ab31789
b69ab31790 enterMergeConflict(MOCK_CONFLICT);
b69ab31791 await repo.checkForMergeConflicts();
b69ab31792 expect(onChange).toHaveBeenCalledWith({
b69ab31793 state: 'loaded',
b69ab31794 command: 'rebase',
b69ab31795 toContinue: 'rebase --continue',
b69ab31796 toAbort: 'rebase --abort',
b69ab31797 files: [
b69ab31798 {path: 'file1.txt', status: 'U', conflictType: 'both_changed'},
b69ab31799 {path: 'file2.txt', status: 'U', conflictType: 'both_changed'},
b69ab31800 ],
b69ab31801 fetchStartTimestamp: expect.anything(),
b69ab31802 fetchCompletedTimestamp: expect.anything(),
b69ab31803 });
b69ab31804
b69ab31805 enterMergeConflict(MOCK_CONFLICT_WITH_FILE1_RESOLVED);
b69ab31806 await repo.checkForMergeConflicts();
b69ab31807 expect(onChange).toHaveBeenCalledWith({
b69ab31808 state: 'loaded',
b69ab31809 command: 'rebase',
b69ab31810 toContinue: 'rebase --continue',
b69ab31811 toAbort: 'rebase --abort',
b69ab31812 files: [
b69ab31813 // even though file1 is no longer in the output, we remember it from before.
b69ab31814 {path: 'file1.txt', status: 'Resolved', conflictType: 'both_changed'},
b69ab31815 {path: 'file2.txt', status: 'U', conflictType: 'both_changed'},
b69ab31816 ],
b69ab31817 fetchStartTimestamp: expect.anything(),
b69ab31818 fetchCompletedTimestamp: expect.anything(),
b69ab31819 });
b69ab31820 });
b69ab31821
b69ab31822 it('handles errors from `sl resolve`', async () => {
b69ab31823 mockEjeca([
b69ab31824 [/^sl resolve --tool internal:dumpjson --all/, new Error('failed to do the thing')],
b69ab31825 ]);
b69ab31826
b69ab31827 const repo = new Repository(repoInfo, ctx);
b69ab31828 const onChange = jest.fn();
b69ab31829 repo.onChangeConflictState(onChange);
b69ab31830
b69ab31831 enterMergeConflict(MOCK_CONFLICT);
b69ab31832 await expect(repo.checkForMergeConflicts()).resolves.toEqual(undefined);
b69ab31833
b69ab31834 expect(onChange).toHaveBeenCalledWith({state: 'loading'});
b69ab31835 expect(onChange).toHaveBeenCalledWith(undefined);
b69ab31836 });
b69ab31837 });
b69ab31838});
b69ab31839
b69ab31840describe('extractRepoInfoFromUrl', () => {
b69ab31841 describe('github.com', () => {
b69ab31842 it('handles http', () => {
b69ab31843 expect(extractRepoInfoFromUrl('https://github.com/myUsername/myRepo.git')).toEqual({
b69ab31844 owner: 'myUsername',
b69ab31845 repo: 'myRepo',
b69ab31846 hostname: 'github.com',
b69ab31847 });
b69ab31848 });
b69ab31849 it('handles plain github.com', () => {
b69ab31850 expect(extractRepoInfoFromUrl('github.com/myUsername/myRepo.git')).toEqual({
b69ab31851 owner: 'myUsername',
b69ab31852 repo: 'myRepo',
b69ab31853 hostname: 'github.com',
b69ab31854 });
b69ab31855 });
b69ab31856 it('handles git@github', () => {
b69ab31857 expect(extractRepoInfoFromUrl('git@github.com:myUsername/myRepo.git')).toEqual({
b69ab31858 owner: 'myUsername',
b69ab31859 repo: 'myRepo',
b69ab31860 hostname: 'github.com',
b69ab31861 });
b69ab31862 });
b69ab31863 it('handles ssh with slashes', () => {
b69ab31864 expect(extractRepoInfoFromUrl('ssh://git@github.com/myUsername/my-repo.git')).toEqual({
b69ab31865 owner: 'myUsername',
b69ab31866 repo: 'my-repo',
b69ab31867 hostname: 'github.com',
b69ab31868 });
b69ab31869 });
b69ab31870 it('handles git+ssh', () => {
b69ab31871 expect(extractRepoInfoFromUrl('git+ssh://git@github.com:myUsername/myRepo.git')).toEqual({
b69ab31872 owner: 'myUsername',
b69ab31873 repo: 'myRepo',
b69ab31874 hostname: 'github.com',
b69ab31875 });
b69ab31876 });
b69ab31877 it('handles dotted http', () => {
b69ab31878 expect(extractRepoInfoFromUrl('https://github.com/myUsername/my.dotted.repo.git')).toEqual({
b69ab31879 owner: 'myUsername',
b69ab31880 repo: 'my.dotted.repo',
b69ab31881 hostname: 'github.com',
b69ab31882 });
b69ab31883 });
b69ab31884 it('handles dotted ssh', () => {
b69ab31885 expect(extractRepoInfoFromUrl('git@github.com:myUsername/my.dotted.repo.git')).toEqual({
b69ab31886 owner: 'myUsername',
b69ab31887 repo: 'my.dotted.repo',
b69ab31888 hostname: 'github.com',
b69ab31889 });
b69ab31890 });
b69ab31891 });
b69ab31892
b69ab31893 describe('github enterprise', () => {
b69ab31894 it('handles http', () => {
b69ab31895 expect(extractRepoInfoFromUrl('https://ghe.company.com/myUsername/myRepo.git')).toEqual({
b69ab31896 owner: 'myUsername',
b69ab31897 repo: 'myRepo',
b69ab31898 hostname: 'ghe.company.com',
b69ab31899 });
b69ab31900 });
b69ab31901 it('handles plain github.com', () => {
b69ab31902 expect(extractRepoInfoFromUrl('ghe.company.com/myUsername/myRepo.git')).toEqual({
b69ab31903 owner: 'myUsername',
b69ab31904 repo: 'myRepo',
b69ab31905 hostname: 'ghe.company.com',
b69ab31906 });
b69ab31907 });
b69ab31908 it('handles git@github', () => {
b69ab31909 expect(extractRepoInfoFromUrl('git@ghe.company.com:myUsername/myRepo.git')).toEqual({
b69ab31910 owner: 'myUsername',
b69ab31911 repo: 'myRepo',
b69ab31912 hostname: 'ghe.company.com',
b69ab31913 });
b69ab31914 });
b69ab31915 it('handles ssh with slashes', () => {
b69ab31916 expect(extractRepoInfoFromUrl('ssh://git@ghe.company.com/myUsername/my-repo.git')).toEqual({
b69ab31917 owner: 'myUsername',
b69ab31918 repo: 'my-repo',
b69ab31919 hostname: 'ghe.company.com',
b69ab31920 });
b69ab31921 });
b69ab31922 it('handles git+ssh', () => {
b69ab31923 expect(extractRepoInfoFromUrl('git+ssh://git@ghe.company.com:myUsername/myRepo.git')).toEqual(
b69ab31924 {
b69ab31925 owner: 'myUsername',
b69ab31926 repo: 'myRepo',
b69ab31927 hostname: 'ghe.company.com',
b69ab31928 },
b69ab31929 );
b69ab31930 });
b69ab31931 it('handles dotted http', () => {
b69ab31932 expect(
b69ab31933 extractRepoInfoFromUrl('https://ghe.company.com/myUsername/my.dotted.repo.git'),
b69ab31934 ).toEqual({
b69ab31935 owner: 'myUsername',
b69ab31936 repo: 'my.dotted.repo',
b69ab31937 hostname: 'ghe.company.com',
b69ab31938 });
b69ab31939 });
b69ab31940 it('handles dotted ssh', () => {
b69ab31941 expect(extractRepoInfoFromUrl('git@ghe.company.com:myUsername/my.dotted.repo.git')).toEqual({
b69ab31942 owner: 'myUsername',
b69ab31943 repo: 'my.dotted.repo',
b69ab31944 hostname: 'ghe.company.com',
b69ab31945 });
b69ab31946 });
b69ab31947 });
b69ab31948});
b69ab31949
b69ab31950describe('absolutePathForFileInRepo', () => {
b69ab31951 let ctx: RepositoryContext;
b69ab31952 beforeEach(() => {
b69ab31953 ctx = {
b69ab31954 cmd: 'sl',
b69ab31955 cwd: '/path/to/cwd',
b69ab31956 logger: mockLogger,
b69ab31957 tracker: mockTracker,
b69ab31958 };
b69ab31959 });
b69ab31960
b69ab31961 it('rejects .. in paths that escape the repo', () => {
b69ab31962 const repoInfo: ValidatedRepoInfo = {
b69ab31963 type: 'success',
b69ab31964 command: 'sl',
b69ab31965 dotdir: '/path/to/repo/.sl',
b69ab31966 repoRoot: '/path/to/repo',
b69ab31967 codeReviewSystem: {type: 'unknown'},
b69ab31968 pullRequestDomain: undefined,
b69ab31969 isEdenFs: false,
b69ab31970 };
b69ab31971 const repo = new Repository(repoInfo, ctx);
b69ab31972
b69ab31973 expect(absolutePathForFileInRepo('foo/bar/file.txt', repo)).toEqual(
b69ab31974 '/path/to/repo/foo/bar/file.txt',
b69ab31975 );
b69ab31976 expect(absolutePathForFileInRepo('foo/../bar/file.txt', repo)).toEqual(
b69ab31977 '/path/to/repo/bar/file.txt',
b69ab31978 );
b69ab31979 expect(absolutePathForFileInRepo('file.txt', repo)).toEqual('/path/to/repo/file.txt');
b69ab31980
b69ab31981 expect(absolutePathForFileInRepo('/file.txt', repo)).toEqual(null);
b69ab31982 expect(absolutePathForFileInRepo('', repo)).toEqual(null);
b69ab31983 expect(absolutePathForFileInRepo('foo/../../file.txt', repo)).toEqual(null);
b69ab31984 expect(absolutePathForFileInRepo('../file.txt', repo)).toEqual(null);
b69ab31985 expect(absolutePathForFileInRepo('/../file.txt', repo)).toEqual(null);
b69ab31986 });
b69ab31987
b69ab31988 it('works on windows', () => {
b69ab31989 const repoInfo: ValidatedRepoInfo = {
b69ab31990 type: 'success',
b69ab31991 command: 'sl',
b69ab31992 dotdir: 'C:\\path\\to\\repo\\.sl',
b69ab31993 repoRoot: 'C:\\path\\to\\repo',
b69ab31994 codeReviewSystem: {type: 'unknown'},
b69ab31995 pullRequestDomain: undefined,
b69ab31996 isEdenFs: false,
b69ab31997 };
b69ab31998 const repo = new Repository(repoInfo, ctx);
b69ab31999
b69ab311000 expect(absolutePathForFileInRepo('foo\\bar\\file.txt', repo, path.win32)).toEqual(
b69ab311001 'C:\\path\\to\\repo\\foo\\bar\\file.txt',
b69ab311002 );
b69ab311003
b69ab311004 expect(absolutePathForFileInRepo('foo\\..\\..\\file.txt', repo, path.win32)).toEqual(null);
b69ab311005 });
b69ab311006});
b69ab311007
b69ab311008describe('getCwdInfo', () => {
b69ab311009 it('computes cwd path and labels', async () => {
b69ab311010 mockEjeca([[/^sl root/, {stdout: '/path/to/myRepo'}]]);
b69ab311011 jest.spyOn(fs.promises, 'realpath').mockImplementation(async (path, _opts) => {
b69ab311012 return path as string;
b69ab311013 });
b69ab311014 await expect(
b69ab311015 Repository.getCwdInfo({
b69ab311016 cmd: 'sl',
b69ab311017 cwd: '/path/to/myRepo/some/subdir',
b69ab311018 logger: mockLogger,
b69ab311019 tracker: mockTracker,
b69ab311020 }),
b69ab311021 ).resolves.toEqual({
b69ab311022 cwd: '/path/to/myRepo/some/subdir',
b69ab311023 repoRoot: '/path/to/myRepo',
b69ab311024 repoRelativeCwdLabel: 'myRepo/some/subdir',
b69ab311025 });
b69ab311026 });
b69ab311027
b69ab311028 it('uses realpath', async () => {
b69ab311029 mockEjeca([[/^sl root/, {stdout: '/data/users/name/myRepo'}]]);
b69ab311030 jest.spyOn(fs.promises, 'realpath').mockImplementation(async (path, _opts) => {
b69ab311031 return (path as string).replace(/^\/home\/name\//, '/data/users/name/');
b69ab311032 });
b69ab311033 await expect(
b69ab311034 Repository.getCwdInfo({
b69ab311035 cmd: 'sl',
b69ab311036 cwd: '/home/name/myRepo/some/subdir',
b69ab311037 logger: mockLogger,
b69ab311038 tracker: mockTracker,
b69ab311039 }),
b69ab311040 ).resolves.toEqual({
b69ab311041 cwd: '/home/name/myRepo/some/subdir', // cwd is not realpath'd
b69ab311042 repoRoot: '/data/users/name/myRepo', // repo root is realpath'd
b69ab311043 repoRelativeCwdLabel: 'myRepo/some/subdir',
b69ab311044 });
b69ab311045 });
b69ab311046
b69ab311047 it('returns null for non-repos', async () => {
b69ab311048 mockEjeca([[/^sl root/, new Error('not a repository')]]);
b69ab311049 await expect(
b69ab311050 Repository.getCwdInfo({
b69ab311051 cmd: 'sl',
b69ab311052 cwd: '/path/ro/myRepo/some/subdir',
b69ab311053 logger: mockLogger,
b69ab311054 tracker: mockTracker,
b69ab311055 }),
b69ab311056 ).resolves.toEqual({
b69ab311057 cwd: '/path/ro/myRepo/some/subdir',
b69ab311058 });
b69ab311059 });
b69ab311060});
b69ab311061
b69ab311062describe('fetchSubmoduleMap', () => {
b69ab311063 let myRepoRoot: AbsolutePath;
b69ab311064 let ctx: RepositoryContext;
b69ab311065 beforeEach(() => {
b69ab311066 myRepoRoot = '/data/users/name/myRepo';
b69ab311067 ctx = {
b69ab311068 cmd: 'sl',
b69ab311069 cwd: myRepoRoot,
b69ab311070 logger: mockLogger,
b69ab311071 tracker: mockTracker,
b69ab311072 };
b69ab311073 });
b69ab311074
b69ab311075 it('simple', async () => {
b69ab311076 const submodules: Submodule[] = [
b69ab311077 {
b69ab311078 name: 'submoduleA',
b69ab311079 url: 'https://ghe.myCompany.com/myUsername/myRepo/submoduleA',
b69ab311080 path: 'submoduleA',
b69ab311081 active: true,
b69ab311082 },
b69ab311083 {
b69ab311084 name: 'submoduleB',
b69ab311085 url: 'https://ghe.myCompany.com/myUsername/myRepo/submoduleB',
b69ab311086 path: 'submoduleB',
b69ab311087 active: false,
b69ab311088 },
b69ab311089 ];
b69ab311090 const submodulesJson = JSON.stringify(submodules);
b69ab311091 mockEjeca([
b69ab311092 [/^sl root/, {stdout: myRepoRoot}],
b69ab311093 [/^sl debugroots/, {stdout: myRepoRoot}],
b69ab311094 [/^sl debuggitmodules/, {stdout: submodulesJson}],
b69ab311095 ]);
b69ab311096 const info = (await Repository.getRepoInfo(ctx)) as ValidatedRepoInfo;
b69ab311097 const repo = new Repository(info, ctx);
b69ab311098 await repo.fetchSubmoduleMap();
b69ab311099 const fetchedSubmoduleMap = repo.getSubmoduleMap();
b69ab311100 expect(fetchedSubmoduleMap).not.toBeUndefined();
b69ab311101 expect(fetchedSubmoduleMap?.get(myRepoRoot)?.value).toEqual(submodules);
b69ab311102 });
b69ab311103
b69ab311104 it('no submodules', async () => {
b69ab311105 const submodules: Submodule[] = [];
b69ab311106 const submodulesJson = JSON.stringify(submodules);
b69ab311107 mockEjeca([
b69ab311108 [/^sl root/, {stdout: myRepoRoot}],
b69ab311109 [/^sl debugroots/, {stdout: myRepoRoot}],
b69ab311110 [/^sl debuggitmodules/, {stdout: submodulesJson}],
b69ab311111 ]);
b69ab311112 const info = (await Repository.getRepoInfo(ctx)) as ValidatedRepoInfo;
b69ab311113 const repo = new Repository(info, ctx);
b69ab311114 await repo.fetchSubmoduleMap();
b69ab311115 const fetchedSubmoduleMap = repo.getSubmoduleMap();
b69ab311116 expect(fetchedSubmoduleMap).not.toBeUndefined();
b69ab311117 expect(fetchedSubmoduleMap?.get(myRepoRoot)?.value).toBeUndefined();
b69ab311118 });
b69ab311119
b69ab311120 it('nested', async () => {
b69ab311121 const submodulesOfMyRepo: Submodule[] = [
b69ab311122 {
b69ab311123 name: 'submoduleA',
b69ab311124 url: 'https://ghe.myCompany.com/myUsername/myRepo/submoduleA',
b69ab311125 path: 'submoduleA',
b69ab311126 active: true,
b69ab311127 },
b69ab311128 ];
b69ab311129 const submoduleARoot = myRepoRoot + '/submoduleA';
b69ab311130 const submodulesOfA: Submodule[] = [
b69ab311131 {
b69ab311132 name: 'submoduleB',
b69ab311133 url: 'https://ghe.myCompany.com/myUsername/myRepo/submoduleA/submoduleB',
b69ab311134 path: 'submoduleB',
b69ab311135 active: true,
b69ab311136 },
b69ab311137 ];
b69ab311138 const submoduleBRoot = submoduleARoot + '/submoduleB';
b69ab311139 mockEjeca([
b69ab311140 [
b69ab311141 new RegExp(`^sl debuggitmodules --json --repo ${submoduleARoot}`),
b69ab311142 {stdout: JSON.stringify(submodulesOfA)},
b69ab311143 ],
b69ab311144 [
b69ab311145 new RegExp(`^sl debuggitmodules --json --repo ${myRepoRoot}`),
b69ab311146 {stdout: JSON.stringify(submodulesOfMyRepo)},
b69ab311147 ],
b69ab311148 [/^sl root/, {stdout: submoduleBRoot}],
b69ab311149 [/^sl debugroots/, {stdout: myRepoRoot + '\n' + submoduleARoot + '\n' + submoduleBRoot}],
b69ab311150 ]);
b69ab311151 const updatedCtx = {...ctx, cwd: submoduleBRoot};
b69ab311152 const info = (await Repository.getRepoInfo(updatedCtx)) as ValidatedRepoInfo;
b69ab311153 const repo = new Repository(info, updatedCtx);
b69ab311154 await repo.fetchSubmoduleMap();
b69ab311155 const fetchedSubmoduleMap = repo.getSubmoduleMap();
b69ab311156
b69ab311157 expect(fetchedSubmoduleMap).not.toBeUndefined();
b69ab311158 expect(fetchedSubmoduleMap?.get(myRepoRoot)?.value).toEqual(submodulesOfMyRepo);
b69ab311159 expect(fetchedSubmoduleMap?.get(submoduleARoot)?.value).toEqual(submodulesOfA);
b69ab311160 });
b69ab311161
b69ab311162 it('error', async () => {
b69ab311163 const msg = 'mock sapling error';
b69ab311164 mockEjeca([
b69ab311165 [/^sl root/, {stdout: myRepoRoot}],
b69ab311166 [/^sl debugroots/, {stdout: myRepoRoot}],
b69ab311167 [/^sl debuggitmodules/, new Error(msg)],
b69ab311168 ]);
b69ab311169 const info = (await Repository.getRepoInfo(ctx)) as ValidatedRepoInfo;
b69ab311170 const repo = new Repository(info, ctx);
b69ab311171 await repo.fetchSubmoduleMap();
b69ab311172 const fetchedSubmoduleMap = repo.getSubmoduleMap();
b69ab311173
b69ab311174 expect(fetchedSubmoduleMap).not.toBeUndefined();
b69ab311175 expect(fetchedSubmoduleMap?.get(myRepoRoot)?.value).toBeUndefined();
b69ab311176 expect(fetchedSubmoduleMap?.get(myRepoRoot)?.error?.message).toMatch(msg);
b69ab311177 });
b69ab311178});