6.4 KB198 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {Repository} from 'isl-server/src/Repository';
9import type {ServerPlatform} from 'isl-server/src/serverPlatform';
10import type {RepositoryContext} from 'isl-server/src/serverTypes';
11import type {RepoInfo, ValidatedRepoInfo} from 'isl/src/types';
12import type {EnabledSCMApiFeature} from '../types';
13
14import {repositoryCache} from 'isl-server/src/RepositoryCache';
15import {makeServerSideTracker} from 'isl-server/src/analytics/serverSideTracker';
16import {Logger} from 'isl-server/src/logger';
17import {TypedEventEmitter} from 'shared/TypedEventEmitter';
18import {nextTick} from 'shared/testUtils';
19import * as vscode from 'vscode';
20import {VSCodeReposList} from '../VSCodeRepo';
21
22export class MockLogger extends Logger {
23 write() {
24 // noop
25 }
26}
27export const mockLogger = new MockLogger();
28
29const mockTracker = makeServerSideTracker(
30 mockLogger,
31 {platformName: 'test'} as ServerPlatform,
32 '0.1',
33 jest.fn(),
34);
35
36jest.mock('isl-server/src/Repository', () => {
37 class MockRepository implements Partial<Repository> {
38 static getRepoInfo = jest.fn((ctx: RepositoryContext): Promise<RepoInfo> => {
39 let root: string;
40 // resolve cwd into shared mock locations
41 if (ctx.cwd.includes('/path/to/repo1')) {
42 root = '/path/to/repo1';
43 } else if (ctx.cwd.includes('/path/to/repo2')) {
44 root = '/path/to/repo2';
45 } else {
46 return Promise.resolve({type: 'cwdNotARepository', cwd: ctx.cwd});
47 }
48 return Promise.resolve({
49 type: 'success',
50 repoRoot: root,
51 dotdir: root + '/.sl',
52 command: 'sl',
53 preferredSubmitCommand: 'pr',
54 codeReviewSystem: {type: 'unknown', path: ''},
55 pullRequestDomain: undefined,
56 isEdenFs: false,
57 });
58 });
59 constructor(
60 public info: ValidatedRepoInfo,
61 public logger?: Logger,
62 ) {}
63
64 public disposables: Array<() => void> = [];
65 public dispose() {
66 this.disposables.forEach(d => d());
67 }
68 public onDidDispose = (cb: () => void) => this.disposables.push(cb);
69 public subscribeToUncommittedChanges = jest.fn();
70 public onChangeConflictState = jest.fn();
71 public getUncommittedChanges = jest.fn();
72 public getMergeConflicts = jest.fn();
73 }
74 return {
75 Repository: MockRepository as unknown as Repository,
76 };
77});
78
79describe('adding and removing repositories', () => {
80 let foldersEmitter: TypedEventEmitter<'value', vscode.WorkspaceFoldersChangeEvent>;
81 beforeEach(() => {
82 foldersEmitter = new TypedEventEmitter();
83 (vscode.workspace.onDidChangeWorkspaceFolders as jest.Mock).mockImplementation(cb => {
84 foldersEmitter.on('value', cb);
85 return {dispose: () => foldersEmitter.off('value', cb)};
86 });
87 });
88
89 afterEach(() => {
90 jest.clearAllMocks();
91 repositoryCache.clearCache();
92 foldersEmitter.removeAllListeners();
93 });
94
95 const ENABLED = new Set<EnabledSCMApiFeature>(['blame', 'sidebar']);
96
97 it('creates repositories for workspace folders', async () => {
98 const repos = new VSCodeReposList(mockLogger, mockTracker, ENABLED);
99 foldersEmitter.emit('value', {
100 added: [{name: 'my folder', index: 0, uri: vscode.Uri.file('/path/to/repo1')}],
101 removed: [],
102 });
103 await nextTick();
104
105 expect(vscode.scm.createSourceControl).toHaveBeenCalledTimes(1);
106 repos.dispose();
107 });
108
109 it('deduplicates among shared repos', async () => {
110 const repos = new VSCodeReposList(mockLogger, mockTracker, ENABLED);
111 foldersEmitter.emit('value', {
112 added: [{name: 'my folder', index: 0, uri: vscode.Uri.file('/path/to/repo1/foo')}],
113 removed: [],
114 });
115 await nextTick();
116 foldersEmitter.emit('value', {
117 added: [{name: 'my folder', index: 1, uri: vscode.Uri.file('/path/to/repo1/bar')}],
118 removed: [],
119 });
120 await nextTick();
121
122 expect(vscode.scm.createSourceControl).toHaveBeenCalledTimes(1);
123
124 foldersEmitter.emit('value', {
125 added: [{name: 'my folder', index: 1, uri: vscode.Uri.file('/path/to/repo2/foobar')}],
126 removed: [],
127 });
128 await nextTick();
129 expect(vscode.scm.createSourceControl).toHaveBeenCalledTimes(2);
130
131 repos.dispose();
132 });
133
134 it('deletes repositories for workspace folders', async () => {
135 const repos = new VSCodeReposList(mockLogger, mockTracker, ENABLED);
136
137 // add repo twice, only creates 1 repo
138 foldersEmitter.emit('value', {
139 added: [{name: 'my folder', index: 0, uri: vscode.Uri.file('/path/to/repo1/foo')}],
140 removed: [],
141 });
142 await nextTick();
143 foldersEmitter.emit('value', {
144 added: [{name: 'my folder', index: 0, uri: vscode.Uri.file('/path/to/repo1/bar')}],
145 removed: [],
146 });
147 await nextTick();
148 expect(vscode.scm.createSourceControl).toHaveBeenCalledTimes(1);
149
150 // remove that repo
151 foldersEmitter.emit('value', {
152 added: [],
153 removed: [{name: 'my folder', index: 1, uri: vscode.Uri.file('/path/to/repo1/foo')}],
154 });
155 await nextTick();
156 foldersEmitter.emit('value', {
157 added: [],
158 removed: [{name: 'my folder', index: 1, uri: vscode.Uri.file('/path/to/repo1/bar')}],
159 });
160 await nextTick();
161
162 // adding the same repo again must create it again
163 foldersEmitter.emit('value', {
164 added: [{name: 'my folder', index: 0, uri: vscode.Uri.file('/path/to/repo1/foo')}],
165 removed: [],
166 });
167 await nextTick();
168 expect(vscode.scm.createSourceControl).toHaveBeenCalledTimes(2);
169
170 repos.dispose();
171 });
172
173 it('looks up repos by prefix', async () => {
174 const repos = new VSCodeReposList(mockLogger, mockTracker, ENABLED);
175
176 foldersEmitter.emit('value', {
177 added: [{name: 'my folder', index: 0, uri: vscode.Uri.file('/path/to/repo1/foo')}],
178 removed: [],
179 });
180 await nextTick();
181
182 expect(repos.repoForPath('/path/to/repo1/foo')).not.toBeUndefined();
183 expect(repos.repoForPath('/path/to/repo1/foo/myFile.txt')).not.toBeUndefined();
184 expect(repos.repoForPath('/path/to/repo2/foo')).toBeUndefined();
185
186 foldersEmitter.emit('value', {
187 added: [],
188 removed: [{name: 'my folder', index: 1, uri: vscode.Uri.file('/path/to/repo1/foo')}],
189 });
190 await nextTick();
191
192 expect(repos.repoForPath('/path/to/repo1/foo')).toBeUndefined();
193 expect(repos.repoForPath('/path/to/repo1/foo/myFile.txt')).toBeUndefined();
194
195 repos.dispose();
196 });
197});
198