Skip to content

Commit 54e16db

Browse files
committed
git: block concurrent commands, sync
1 parent 6e93f26 commit 54e16db

2 files changed

Lines changed: 259 additions & 120 deletions

File tree

extensions/git/src/model.ts

Lines changed: 193 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
import { Uri, EventEmitter, Event, SCMResource, SCMResourceDecorations, SCMResourceGroup } from 'vscode';
99
import { Repository, IRef, IBranch, IRemote, IPushOptions } from './git';
10-
import { throttle } from './util';
11-
import { decorate } from 'core-decorators';
10+
import { throttle, anyEvent } from './util';
11+
import { decorate, memoize } from 'core-decorators';
1212
import * as path from 'path';
1313

1414
const iconsRootPath = path.join(path.dirname(__dirname), 'resources', 'icons');
@@ -150,11 +150,58 @@ export class WorkingTreeGroup extends ResourceGroup {
150150
}
151151
}
152152

153+
export enum Operation {
154+
Status = 0o1,
155+
Stage = 0o2,
156+
Unstage = 0o4,
157+
Commit = 0o10,
158+
Clean = 0o20,
159+
Branch = 0o40,
160+
Checkout = 0o100,
161+
Fetch = 0o200,
162+
Sync = 0o400,
163+
Push = 0o1000
164+
}
165+
166+
export interface Operations {
167+
isRunning(operation: Operation): boolean;
168+
}
169+
170+
class OperationsImpl implements Operations {
171+
172+
constructor(private readonly operations: number = 0) {
173+
// noop
174+
}
175+
176+
start(operation: Operation): OperationsImpl {
177+
return new OperationsImpl(this.operations | operation);
178+
}
179+
180+
end(operation: Operation): OperationsImpl {
181+
return new OperationsImpl(this.operations & ~operation);
182+
}
183+
184+
isRunning(operation: Operation): boolean {
185+
return (this.operations & operation) !== 0;
186+
}
187+
}
188+
153189
export class Model {
154190

155191
private _onDidChange = new EventEmitter<SCMResourceGroup[]>();
156192
readonly onDidChange: Event<SCMResourceGroup[]> = this._onDidChange.event;
157193

194+
private _onRunOperation = new EventEmitter<Operation>();
195+
readonly onRunOperation: Event<Operation> = this._onRunOperation.event;
196+
197+
private _onDidRunOperation = new EventEmitter<Operation>();
198+
readonly onDidRunOperation: Event<Operation> = this._onDidRunOperation.event;
199+
200+
@memoize
201+
get onDidChangeOperations(): Event<void> {
202+
return anyEvent(this.onRunOperation as Event<any>, this.onDidRunOperation as Event<any>);
203+
}
204+
158205
private _mergeGroup = new MergeGroup([]);
159206
get mergeGroup(): MergeGroup { return this._mergeGroup; }
160207

@@ -180,6 +227,9 @@ export class Model {
180227
return result;
181228
}
182229

230+
private _operations = new OperationsImpl();
231+
get operations(): Operations { return this._operations; }
232+
183233
constructor(private _repositoryRoot: string, private repository: Repository) {
184234

185235
}
@@ -205,145 +255,186 @@ export class Model {
205255

206256
@decorate(throttle)
207257
async update(): Promise<void> {
208-
const status = await this.repository.getStatus();
209-
let HEAD: IBranch | undefined;
210-
211-
try {
212-
HEAD = await this.repository.getHEAD();
213-
214-
if (HEAD.name) {
215-
try {
216-
HEAD = await this.repository.getBranch(HEAD.name);
217-
} catch (err) {
218-
// noop
258+
await this.run(Operation.Status, async () => {
259+
const status = await this.repository.getStatus();
260+
let HEAD: IBranch | undefined;
261+
262+
try {
263+
HEAD = await this.repository.getHEAD();
264+
265+
if (HEAD.name) {
266+
try {
267+
HEAD = await this.repository.getBranch(HEAD.name);
268+
} catch (err) {
269+
// noop
270+
}
219271
}
272+
} catch (err) {
273+
// noop
220274
}
221-
} catch (err) {
222-
// noop
223-
}
224275

225-
const [refs, remotes] = await Promise.all([this.repository.getRefs(), this.repository.getRemotes()]);
226-
227-
this._HEAD = HEAD;
228-
this._refs = refs;
229-
this._remotes = remotes;
230-
231-
const index: Resource[] = [];
232-
const workingTree: Resource[] = [];
233-
const merge: Resource[] = [];
234-
235-
status.forEach(raw => {
236-
const uri = Uri.file(path.join(this.repositoryRoot, raw.path));
237-
238-
switch (raw.x + raw.y) {
239-
case '??': return workingTree.push(new Resource(uri, Status.UNTRACKED));
240-
case '!!': return workingTree.push(new Resource(uri, Status.IGNORED));
241-
case 'DD': return merge.push(new Resource(uri, Status.BOTH_DELETED));
242-
case 'AU': return merge.push(new Resource(uri, Status.ADDED_BY_US));
243-
case 'UD': return merge.push(new Resource(uri, Status.DELETED_BY_THEM));
244-
case 'UA': return merge.push(new Resource(uri, Status.ADDED_BY_THEM));
245-
case 'DU': return merge.push(new Resource(uri, Status.DELETED_BY_US));
246-
case 'AA': return merge.push(new Resource(uri, Status.BOTH_ADDED));
247-
case 'UU': return merge.push(new Resource(uri, Status.BOTH_MODIFIED));
248-
}
276+
const [refs, remotes] = await Promise.all([this.repository.getRefs(), this.repository.getRemotes()]);
277+
278+
this._HEAD = HEAD;
279+
this._refs = refs;
280+
this._remotes = remotes;
281+
282+
const index: Resource[] = [];
283+
const workingTree: Resource[] = [];
284+
const merge: Resource[] = [];
285+
286+
status.forEach(raw => {
287+
const uri = Uri.file(path.join(this.repositoryRoot, raw.path));
288+
289+
switch (raw.x + raw.y) {
290+
case '??': return workingTree.push(new Resource(uri, Status.UNTRACKED));
291+
case '!!': return workingTree.push(new Resource(uri, Status.IGNORED));
292+
case 'DD': return merge.push(new Resource(uri, Status.BOTH_DELETED));
293+
case 'AU': return merge.push(new Resource(uri, Status.ADDED_BY_US));
294+
case 'UD': return merge.push(new Resource(uri, Status.DELETED_BY_THEM));
295+
case 'UA': return merge.push(new Resource(uri, Status.ADDED_BY_THEM));
296+
case 'DU': return merge.push(new Resource(uri, Status.DELETED_BY_US));
297+
case 'AA': return merge.push(new Resource(uri, Status.BOTH_ADDED));
298+
case 'UU': return merge.push(new Resource(uri, Status.BOTH_MODIFIED));
299+
}
249300

250-
let isModifiedInIndex = false;
301+
let isModifiedInIndex = false;
251302

252-
switch (raw.x) {
253-
case 'M': index.push(new Resource(uri, Status.INDEX_MODIFIED)); isModifiedInIndex = true; break;
254-
case 'A': index.push(new Resource(uri, Status.INDEX_ADDED)); break;
255-
case 'D': index.push(new Resource(uri, Status.INDEX_DELETED)); break;
256-
case 'R': index.push(new Resource(uri, Status.INDEX_RENAMED/*, raw.rename*/)); break;
257-
case 'C': index.push(new Resource(uri, Status.INDEX_COPIED)); break;
258-
}
303+
switch (raw.x) {
304+
case 'M': index.push(new Resource(uri, Status.INDEX_MODIFIED)); isModifiedInIndex = true; break;
305+
case 'A': index.push(new Resource(uri, Status.INDEX_ADDED)); break;
306+
case 'D': index.push(new Resource(uri, Status.INDEX_DELETED)); break;
307+
case 'R': index.push(new Resource(uri, Status.INDEX_RENAMED/*, raw.rename*/)); break;
308+
case 'C': index.push(new Resource(uri, Status.INDEX_COPIED)); break;
309+
}
259310

260-
switch (raw.y) {
261-
case 'M': workingTree.push(new Resource(uri, Status.MODIFIED/*, raw.rename*/)); break;
262-
case 'D': workingTree.push(new Resource(uri, Status.DELETED/*, raw.rename*/)); break;
263-
}
264-
});
311+
switch (raw.y) {
312+
case 'M': workingTree.push(new Resource(uri, Status.MODIFIED/*, raw.rename*/)); break;
313+
case 'D': workingTree.push(new Resource(uri, Status.DELETED/*, raw.rename*/)); break;
314+
}
315+
});
265316

266-
this._mergeGroup = new MergeGroup(merge);
267-
this._indexGroup = new IndexGroup(index);
268-
this._workingTreeGroup = new WorkingTreeGroup(workingTree);
317+
this._mergeGroup = new MergeGroup(merge);
318+
this._indexGroup = new IndexGroup(index);
319+
this._workingTreeGroup = new WorkingTreeGroup(workingTree);
269320

270-
this._onDidChange.fire(this.resources);
321+
this._onDidChange.fire(this.resources);
322+
});
271323
}
272324

325+
@decorate(throttle)
273326
async stage(...resources: Resource[]): Promise<void> {
274-
const paths = resources.map(r => r.uri.fsPath);
275-
await this.repository.add(paths);
276-
await this.update();
327+
await this.run(Operation.Stage, async () => {
328+
const paths = resources.map(r => r.uri.fsPath);
329+
await this.repository.add(paths);
330+
await this.update();
331+
});
277332
}
278333

334+
@decorate(throttle)
279335
async unstage(...resources: Resource[]): Promise<void> {
280-
const paths = resources.map(r => r.uri.fsPath);
281-
await this.repository.revertFiles('HEAD', paths);
282-
await this.update();
336+
await this.run(Operation.Unstage, async () => {
337+
const paths = resources.map(r => r.uri.fsPath);
338+
await this.repository.revertFiles('HEAD', paths);
339+
await this.update();
340+
});
283341
}
284342

343+
@decorate(throttle)
285344
async commit(message: string, opts: { all?: boolean, amend?: boolean, signoff?: boolean } = Object.create(null)): Promise<void> {
286-
if (opts.all) {
287-
await this.repository.add([]);
288-
}
345+
await this.run(Operation.Commit, async () => {
346+
if (opts.all) {
347+
await this.repository.add([]);
348+
}
289349

290-
await this.repository.commit(message, opts);
291-
await this.update();
350+
await this.repository.commit(message, opts);
351+
await this.update();
352+
});
292353
}
293354

355+
@decorate(throttle)
294356
async clean(...resources: Resource[]): Promise<void> {
295-
const toClean: string[] = [];
296-
const toCheckout: string[] = [];
297-
298-
resources.forEach(r => {
299-
switch (r.type) {
300-
case Status.UNTRACKED:
301-
case Status.IGNORED:
302-
toClean.push(r.uri.fsPath);
303-
break;
304-
305-
default:
306-
toCheckout.push(r.uri.fsPath);
307-
break;
308-
}
309-
});
357+
await this.run(Operation.Clean, async () => {
358+
const toClean: string[] = [];
359+
const toCheckout: string[] = [];
360+
361+
resources.forEach(r => {
362+
switch (r.type) {
363+
case Status.UNTRACKED:
364+
case Status.IGNORED:
365+
toClean.push(r.uri.fsPath);
366+
break;
367+
368+
default:
369+
toCheckout.push(r.uri.fsPath);
370+
break;
371+
}
372+
});
310373

311-
const promises: Promise<void>[] = [];
374+
const promises: Promise<void>[] = [];
312375

313-
if (toClean.length > 0) {
314-
promises.push(this.repository.clean(toClean));
315-
}
376+
if (toClean.length > 0) {
377+
promises.push(this.repository.clean(toClean));
378+
}
316379

317-
if (toCheckout.length > 0) {
318-
promises.push(this.repository.checkout('', toCheckout));
319-
}
380+
if (toCheckout.length > 0) {
381+
promises.push(this.repository.checkout('', toCheckout));
382+
}
320383

321-
await Promise.all(promises);
322-
await this.update();
384+
await Promise.all(promises);
385+
await this.update();
386+
});
323387
}
324388

389+
@decorate(throttle)
325390
async branch(name: string): Promise<void> {
326-
await this.repository.branch(name, true);
327-
await this.update();
391+
await this.run(Operation.Branch, async () => {
392+
await this.repository.branch(name, true);
393+
await this.update();
394+
});
328395
}
329396

397+
@decorate(throttle)
330398
async checkout(treeish: string): Promise<void> {
331-
await this.repository.checkout(treeish, []);
332-
await this.update();
399+
await this.run(Operation.Checkout, async () => {
400+
await this.repository.checkout(treeish, []);
401+
await this.update();
402+
});
333403
}
334404

405+
@decorate(throttle)
335406
async fetch(): Promise<void> {
336-
await this.repository.fetch();
337-
await this.update();
407+
await this.run(Operation.Fetch, async () => {
408+
await this.repository.fetch();
409+
await this.update();
410+
});
338411
}
339412

413+
@decorate(throttle)
340414
async sync(): Promise<void> {
341-
await this.repository.sync();
342-
await this.update();
415+
await this.run(Operation.Sync, async () => {
416+
await this.repository.sync();
417+
await this.update();
418+
});
343419
}
344420

421+
@decorate(throttle)
345422
async push(remote?: string, name?: string, options?: IPushOptions): Promise<void> {
346-
await this.repository.push(remote, name, options);
347-
await this.update();
423+
await this.run(Operation.Push, async () => {
424+
await this.repository.push(remote, name, options);
425+
await this.update();
426+
});
427+
}
428+
429+
private async run(operation: Operation, fn: () => Promise<void>): Promise<void> {
430+
this._operations = this._operations.start(operation);
431+
this._onRunOperation.fire(operation);
432+
433+
try {
434+
await fn();
435+
} finally {
436+
this._operations = this._operations.end(operation);
437+
this._onDidRunOperation.fire(operation);
438+
}
348439
}
349440
}

0 commit comments

Comments
 (0)