Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/git/src/cloneManager.ts
4772 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as path from 'path';
7
import * as fs from 'fs';
8
import * as os from 'os';
9
import { pickRemoteSource } from './remoteSource';
10
import { l10n, workspace, window, Uri, ProgressLocation, commands } from 'vscode';
11
import { RepositoryCache, RepositoryCacheInfo } from './repositoryCache';
12
import TelemetryReporter from '@vscode/extension-telemetry';
13
import { Model } from './model';
14
15
type ApiPostCloneAction = 'none';
16
enum PostCloneAction { Open, OpenNewWindow, AddToWorkspace, None }
17
18
export interface CloneOptions {
19
parentPath?: string;
20
ref?: string;
21
recursive?: boolean;
22
postCloneAction?: ApiPostCloneAction;
23
}
24
25
export class CloneManager {
26
constructor(private readonly model: Model,
27
private readonly telemetryReporter: TelemetryReporter,
28
private readonly repositoryCache: RepositoryCache) { }
29
30
async clone(url?: string, options: CloneOptions = {}) {
31
if (!url || typeof url !== 'string') {
32
url = await pickRemoteSource({
33
providerLabel: provider => l10n.t('Clone from {0}', provider.name),
34
urlLabel: l10n.t('Clone from URL')
35
});
36
}
37
38
if (!url) {
39
/* __GDPR__
40
"clone" : {
41
"owner": "lszomoru",
42
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" }
43
}
44
*/
45
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_URL' });
46
return;
47
}
48
49
url = url.trim().replace(/^git\s+clone\s+/, '');
50
51
const cachedRepository = this.repositoryCache.get(url);
52
if (cachedRepository && (cachedRepository.length > 0)) {
53
return this.tryOpenExistingRepository(cachedRepository, url, options.postCloneAction, options.parentPath, options.ref);
54
}
55
return this.cloneRepository(url, options.parentPath, options);
56
}
57
58
private async cloneRepository(url: string, parentPath?: string, options: { recursive?: boolean; ref?: string; postCloneAction?: ApiPostCloneAction } = {}): Promise<string | undefined> {
59
if (!parentPath) {
60
const config = workspace.getConfiguration('git');
61
let defaultCloneDirectory = config.get<string>('defaultCloneDirectory') || os.homedir();
62
defaultCloneDirectory = defaultCloneDirectory.replace(/^~/, os.homedir());
63
64
const uris = await window.showOpenDialog({
65
canSelectFiles: false,
66
canSelectFolders: true,
67
canSelectMany: false,
68
defaultUri: Uri.file(defaultCloneDirectory),
69
title: l10n.t('Choose a folder to clone {0} into', url),
70
openLabel: l10n.t('Select as Repository Destination')
71
});
72
73
if (!uris || uris.length === 0) {
74
/* __GDPR__
75
"clone" : {
76
"owner": "lszomoru",
77
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" }
78
}
79
*/
80
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_directory' });
81
return;
82
}
83
84
const uri = uris[0];
85
parentPath = uri.fsPath;
86
}
87
88
try {
89
const opts = {
90
location: ProgressLocation.Notification,
91
title: l10n.t('Cloning git repository "{0}"...', url),
92
cancellable: true
93
};
94
95
const repositoryPath = await window.withProgress(
96
opts,
97
(progress, token) => this.model.git.clone(url!, { parentPath: parentPath!, progress, recursive: options.recursive, ref: options.ref }, token)
98
);
99
100
await this.doPostCloneAction(repositoryPath, options.postCloneAction);
101
102
return repositoryPath;
103
} catch (err) {
104
if (/already exists and is not an empty directory/.test(err && err.stderr || '')) {
105
/* __GDPR__
106
"clone" : {
107
"owner": "lszomoru",
108
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" }
109
}
110
*/
111
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'directory_not_empty' });
112
} else if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
113
return;
114
} else {
115
/* __GDPR__
116
"clone" : {
117
"owner": "lszomoru",
118
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" }
119
}
120
*/
121
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'error' });
122
}
123
124
throw err;
125
}
126
}
127
128
private async doPostCloneAction(target: string, postCloneAction?: ApiPostCloneAction): Promise<void> {
129
const config = workspace.getConfiguration('git');
130
const openAfterClone = config.get<'always' | 'alwaysNewWindow' | 'whenNoFolderOpen' | 'prompt'>('openAfterClone');
131
132
let action: PostCloneAction | undefined = undefined;
133
134
if (postCloneAction && postCloneAction === 'none') {
135
action = PostCloneAction.None;
136
} else {
137
if (openAfterClone === 'always') {
138
action = PostCloneAction.Open;
139
} else if (openAfterClone === 'alwaysNewWindow') {
140
action = PostCloneAction.OpenNewWindow;
141
} else if (openAfterClone === 'whenNoFolderOpen' && !workspace.workspaceFolders) {
142
action = PostCloneAction.Open;
143
}
144
}
145
146
if (action === undefined) {
147
let message = l10n.t('Would you like to open the repository?');
148
const open = l10n.t('Open');
149
const openNewWindow = l10n.t('Open in New Window');
150
const choices = [open, openNewWindow];
151
152
const addToWorkspace = l10n.t('Add to Workspace');
153
if (workspace.workspaceFolders) {
154
message = l10n.t('Would you like to open the repository, or add it to the current workspace?');
155
choices.push(addToWorkspace);
156
}
157
158
const result = await window.showInformationMessage(message, { modal: true }, ...choices);
159
160
action = result === open ? PostCloneAction.Open
161
: result === openNewWindow ? PostCloneAction.OpenNewWindow
162
: result === addToWorkspace ? PostCloneAction.AddToWorkspace : undefined;
163
}
164
165
/* __GDPR__
166
"clone" : {
167
"owner": "lszomoru",
168
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" },
169
"openFolder": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "Indicates whether the folder is opened following the clone operation" }
170
}
171
*/
172
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'success' }, { openFolder: action === PostCloneAction.Open || action === PostCloneAction.OpenNewWindow ? 1 : 0 });
173
174
const uri = Uri.file(target);
175
176
if (action === PostCloneAction.Open) {
177
commands.executeCommand('vscode.openFolder', uri, { forceReuseWindow: true });
178
} else if (action === PostCloneAction.AddToWorkspace) {
179
workspace.updateWorkspaceFolders(workspace.workspaceFolders!.length, 0, { uri });
180
} else if (action === PostCloneAction.OpenNewWindow) {
181
commands.executeCommand('vscode.openFolder', uri, { forceNewWindow: true });
182
}
183
}
184
185
private async chooseExistingRepository(url: string, existingCachedRepositories: RepositoryCacheInfo[], ref: string | undefined, parentPath?: string, postCloneAction?: ApiPostCloneAction): Promise<string | undefined> {
186
try {
187
const items: { label: string; description?: string; item?: RepositoryCacheInfo }[] = existingCachedRepositories.map(knownFolder => {
188
const isWorkspace = knownFolder.workspacePath.endsWith('.code-workspace');
189
const label = isWorkspace ? l10n.t('Workspace: {0}', path.basename(knownFolder.workspacePath, '.code-workspace')) : path.basename(knownFolder.workspacePath);
190
return { label, description: knownFolder.workspacePath, item: knownFolder };
191
});
192
const cloneAgain = { label: l10n.t('Clone again') };
193
items.push(cloneAgain);
194
const placeHolder = l10n.t('Open Existing Repository Clone');
195
const pick = await window.showQuickPick(items, { placeHolder, canPickMany: false });
196
if (pick === cloneAgain) {
197
return (await this.cloneRepository(url, parentPath, { ref, postCloneAction })) ?? undefined;
198
}
199
if (!pick?.item) {
200
return undefined;
201
}
202
return pick.item.workspacePath;
203
} catch {
204
return undefined;
205
}
206
}
207
208
private async tryOpenExistingRepository(cachedRepository: RepositoryCacheInfo[], url: string, postCloneAction?: ApiPostCloneAction, parentPath?: string, ref?: string): Promise<string | undefined> {
209
// Gather existing folders/workspace files (ignore ones that no longer exist)
210
const existingCachedRepositories: RepositoryCacheInfo[] = (await Promise.all<RepositoryCacheInfo | undefined>(cachedRepository.map(async folder => {
211
const stat = await fs.promises.stat(folder.workspacePath).catch(() => undefined);
212
if (stat) {
213
return folder;
214
}
215
return undefined;
216
}
217
))).filter<RepositoryCacheInfo>((folder): folder is RepositoryCacheInfo => folder !== undefined);
218
219
if (!existingCachedRepositories.length) {
220
// fallback to clone
221
return (await this.cloneRepository(url, parentPath, { ref, postCloneAction }) ?? undefined);
222
}
223
224
// First, find the cached repo that exists in the current workspace
225
const matchingInCurrentWorkspace = existingCachedRepositories?.find(cachedRepo => {
226
return workspace.workspaceFolders?.some(workspaceFolder => workspaceFolder.uri.fsPath === cachedRepo.workspacePath);
227
});
228
229
if (matchingInCurrentWorkspace) {
230
return matchingInCurrentWorkspace.workspacePath;
231
}
232
233
let repoForWorkspace: string | undefined = (existingCachedRepositories.length === 1 ? existingCachedRepositories[0].workspacePath : undefined);
234
if (!repoForWorkspace) {
235
repoForWorkspace = await this.chooseExistingRepository(url, existingCachedRepositories, ref, parentPath, postCloneAction);
236
}
237
if (repoForWorkspace) {
238
await this.doPostCloneAction(repoForWorkspace, postCloneAction);
239
return repoForWorkspace;
240
}
241
return;
242
}
243
}
244
245