mirror of
https://github.com/Microsoft/vscode
synced 2024-09-18 01:58:27 +00:00
git: wrap up nogit state
This commit is contained in:
parent
ac703e0d92
commit
3d6b1b71c5
|
@ -65,7 +65,8 @@ async function init(disposables: Disposable[]): Promise<void> {
|
|||
checkoutStatusBar,
|
||||
syncStatusBar,
|
||||
autoFetcher,
|
||||
mergeDecorator
|
||||
mergeDecorator,
|
||||
model
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { Uri, EventEmitter, Event, SCMResource, SCMResourceDecorations, SCMResourceGroup, Disposable, window, workspace } from 'vscode';
|
||||
import { Repository, Ref, Branch, Remote, PushOptions, Commit, GitErrorCodes } from './git';
|
||||
import { anyEvent, eventToPromise, filterEvent, mapEvent } from './util';
|
||||
import { anyEvent, eventToPromise, filterEvent, mapEvent, EmptyDisposable, combinedDisposable } from './util';
|
||||
import { memoize, throttle, debounce } from './decorators';
|
||||
import { watch } from './watch';
|
||||
import * as path from 'path';
|
||||
|
@ -20,6 +20,12 @@ function getIconUri(iconName: string, theme: string): Uri {
|
|||
return Uri.file(path.join(iconsRootPath, theme, `${iconName}.svg`));
|
||||
}
|
||||
|
||||
export enum State {
|
||||
Uninitialized,
|
||||
Idle,
|
||||
NotAGitRepository
|
||||
}
|
||||
|
||||
export enum Status {
|
||||
INDEX_MODIFIED,
|
||||
INDEX_ADDED,
|
||||
|
@ -139,7 +145,7 @@ export class MergeGroup extends ResourceGroup {
|
|||
|
||||
static readonly ID = 'merge';
|
||||
|
||||
constructor(resources: Resource[]) {
|
||||
constructor(resources: Resource[] = []) {
|
||||
super(MergeGroup.ID, localize('merge changes', "Merge Changes"), resources);
|
||||
}
|
||||
}
|
||||
|
@ -148,7 +154,7 @@ export class IndexGroup extends ResourceGroup {
|
|||
|
||||
static readonly ID = 'index';
|
||||
|
||||
constructor(resources: Resource[]) {
|
||||
constructor(resources: Resource[] = []) {
|
||||
super(IndexGroup.ID, localize('staged changes', "Staged Changes"), resources);
|
||||
}
|
||||
}
|
||||
|
@ -157,7 +163,7 @@ export class WorkingTreeGroup extends ResourceGroup {
|
|||
|
||||
static readonly ID = 'workingTree';
|
||||
|
||||
constructor(resources: Resource[]) {
|
||||
constructor(resources: Resource[] = []) {
|
||||
super(WorkingTreeGroup.ID, localize('changes', "Changes"), resources);
|
||||
}
|
||||
}
|
||||
|
@ -176,6 +182,7 @@ export enum Operation {
|
|||
Push = 1 << 10,
|
||||
Sync = 1 << 11,
|
||||
Init = 1 << 12,
|
||||
UpdateModel = 1 << 13
|
||||
}
|
||||
|
||||
export interface Operations {
|
||||
|
@ -212,16 +219,18 @@ export interface CommitOptions {
|
|||
signoff?: boolean;
|
||||
}
|
||||
|
||||
export enum State {
|
||||
Uninitialized,
|
||||
Idle,
|
||||
NotAGitRepository
|
||||
}
|
||||
export class Model implements Disposable {
|
||||
|
||||
export class Model {
|
||||
private _onDidChangeState = new EventEmitter<State>();
|
||||
readonly onDidChangeState: Event<State> = this._onDidChangeState.event;
|
||||
|
||||
private _onDidChange = new EventEmitter<SCMResourceGroup[]>();
|
||||
readonly onDidChange: Event<SCMResourceGroup[]> = this._onDidChange.event;
|
||||
private _onDidChangeResources = new EventEmitter<SCMResourceGroup[]>();
|
||||
readonly onDidChangeResources: Event<SCMResourceGroup[]> = this._onDidChangeResources.event;
|
||||
|
||||
@memoize
|
||||
get onDidChange(): Event<void> {
|
||||
return anyEvent<any>(this.onDidChangeState, this.onDidChangeResources);
|
||||
}
|
||||
|
||||
private _onRunOperation = new EventEmitter<Operation>();
|
||||
readonly onRunOperation: Event<Operation> = this._onRunOperation.event;
|
||||
|
@ -229,9 +238,6 @@ export class Model {
|
|||
private _onDidRunOperation = new EventEmitter<Operation>();
|
||||
readonly onDidRunOperation: Event<Operation> = this._onDidRunOperation.event;
|
||||
|
||||
private _onDidChangeState = new EventEmitter<State>();
|
||||
readonly onDidChangeState: Event<State> = this._onDidChangeState.event;
|
||||
|
||||
@memoize
|
||||
get onDidChangeOperations(): Event<void> {
|
||||
return anyEvent(this.onRunOperation as Event<any>, this.onDidRunOperation as Event<any>);
|
||||
|
@ -262,57 +268,6 @@ export class Model {
|
|||
return result;
|
||||
}
|
||||
|
||||
private _operations = new OperationsImpl();
|
||||
get operations(): Operations { return this._operations; }
|
||||
|
||||
private repositoryRoot: string;
|
||||
|
||||
private _state = State.Uninitialized;
|
||||
get state(): State { return this._state; }
|
||||
set state(state: State) {
|
||||
this._state = state;
|
||||
this._onDidChangeState.fire(state);
|
||||
}
|
||||
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(
|
||||
private repository: Repository,
|
||||
private onWorkspaceChange: Event<Uri>
|
||||
) {
|
||||
this.initialize().catch(err => console.error(err));
|
||||
}
|
||||
|
||||
private async initialize(): Promise<void> {
|
||||
try {
|
||||
this.repositoryRoot = await this.repository.getRoot();
|
||||
this.state = State.Idle;
|
||||
|
||||
/* We use the native Node `watch` for faster, non debounced events.
|
||||
* That way we hopefully get the events during the operations we're
|
||||
* performing, thus sparing useless `git status` calls to refresh
|
||||
* the model's state.
|
||||
*/
|
||||
const gitPath = path.join(this.repositoryRoot, '.git');
|
||||
const { event, disposable } = watch(gitPath);
|
||||
const onGitChange = mapEvent(event, ({ filename }) => Uri.file(path.join(gitPath, filename)));
|
||||
const onRelevantGitChange = filterEvent(onGitChange, uri => !/\/\.git\/index\.lock$/.test(uri.fsPath));
|
||||
onRelevantGitChange(this.onFSChange, this, this.disposables);
|
||||
this.disposables.push(disposable);
|
||||
|
||||
const onNonGitChange = filterEvent(this.onWorkspaceChange, uri => !/\/\.git\//.test(uri.fsPath));
|
||||
onNonGitChange(this.onFSChange, this, this.disposables);
|
||||
|
||||
this.status();
|
||||
} catch (err) {
|
||||
if (err.gitErrorCode === GitErrorCodes.NotAGitRepository) {
|
||||
this.state = State.NotAGitRepository;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _HEAD: Branch | undefined;
|
||||
get HEAD(): Branch | undefined {
|
||||
return this._HEAD;
|
||||
|
@ -328,9 +283,43 @@ export class Model {
|
|||
return this._remotes;
|
||||
}
|
||||
|
||||
private _operations = new OperationsImpl();
|
||||
get operations(): Operations { return this._operations; }
|
||||
|
||||
private repositoryRoot: string;
|
||||
|
||||
private _state = State.Uninitialized;
|
||||
get state(): State { return this._state; }
|
||||
set state(state: State) {
|
||||
this._state = state;
|
||||
this._onDidChangeState.fire(state);
|
||||
|
||||
this._HEAD = undefined;
|
||||
this._refs = [];
|
||||
this._remotes = [];
|
||||
this._mergeGroup = new MergeGroup();
|
||||
this._indexGroup = new IndexGroup();
|
||||
this._workingTreeGroup = new WorkingTreeGroup();
|
||||
this._onDidChangeResources.fire(this.resources);
|
||||
}
|
||||
|
||||
private repositoryDisposable: Disposable = EmptyDisposable;
|
||||
|
||||
constructor(
|
||||
private repository: Repository,
|
||||
private onWorkspaceChange: Event<Uri>
|
||||
) {
|
||||
this.status();
|
||||
}
|
||||
|
||||
@throttle
|
||||
async init(): Promise<void> {
|
||||
await this.run(Operation.Init, () => this.repository.init());
|
||||
if (this.state !== State.NotAGitRepository) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.repository.init();
|
||||
await this.status();
|
||||
}
|
||||
|
||||
@throttle
|
||||
|
@ -432,14 +421,23 @@ export class Model {
|
|||
await this.run(Operation.Sync, () => this.repository.sync());
|
||||
}
|
||||
|
||||
private async run(operation: Operation, fn: () => Promise<void> = () => Promise.resolve()): Promise<void> {
|
||||
private async run(operation: Operation, runOperation: () => Promise<void> = () => Promise.resolve()): Promise<void> {
|
||||
return window.withScmProgress(async () => {
|
||||
this._operations = this._operations.start(operation);
|
||||
this._onRunOperation.fire(operation);
|
||||
|
||||
try {
|
||||
await fn();
|
||||
await this.assertIdleState();
|
||||
await runOperation();
|
||||
await this.update();
|
||||
} catch (err) {
|
||||
if (err.gitErrorCode === GitErrorCodes.NotAGitRepository) {
|
||||
// TODO@Joao!
|
||||
this.repositoryDisposable.dispose();
|
||||
this.state = State.NotAGitRepository;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
this._operations = this._operations.end(operation);
|
||||
this._onDidRunOperation.fire(operation);
|
||||
|
@ -447,6 +445,37 @@ export class Model {
|
|||
});
|
||||
}
|
||||
|
||||
/* We use the native Node `watch` for faster, non debounced events.
|
||||
* That way we hopefully get the events during the operations we're
|
||||
* performing, thus sparing useless `git status` calls to refresh
|
||||
* the model's state.
|
||||
*/
|
||||
private async assertIdleState(): Promise<void> {
|
||||
if (this.state === State.Idle) {
|
||||
return;
|
||||
}
|
||||
|
||||
const repositoryRoot = await this.repository.getRoot();
|
||||
|
||||
this.repositoryDisposable.dispose();
|
||||
this.repositoryRoot = repositoryRoot;
|
||||
|
||||
const disposables: Disposable[] = [];
|
||||
const gitPath = path.join(repositoryRoot, '.git');
|
||||
const { event, disposable: watcher } = watch(gitPath);
|
||||
disposables.push(watcher);
|
||||
|
||||
const onGitChange = mapEvent(event, ({ filename }) => Uri.file(path.join(gitPath, filename)));
|
||||
const onRelevantGitChange = filterEvent(onGitChange, uri => !/\/\.git\/index\.lock$/.test(uri.fsPath));
|
||||
onRelevantGitChange(this.onFSChange, this, disposables);
|
||||
|
||||
const onNonGitChange = filterEvent(this.onWorkspaceChange, uri => !/\/\.git\//.test(uri.fsPath));
|
||||
onNonGitChange(this.onFSChange, this, disposables);
|
||||
|
||||
this.repositoryDisposable = combinedDisposable(disposables);
|
||||
this.state = State.Idle;
|
||||
}
|
||||
|
||||
@throttle
|
||||
private async update(): Promise<void> {
|
||||
const status = await this.repository.getStatus();
|
||||
|
@ -511,8 +540,7 @@ export class Model {
|
|||
this._mergeGroup = new MergeGroup(merge);
|
||||
this._indexGroup = new IndexGroup(index);
|
||||
this._workingTreeGroup = new WorkingTreeGroup(workingTree);
|
||||
|
||||
this._onDidChange.fire(this.resources);
|
||||
this._onDidChangeResources.fire(this.resources);
|
||||
}
|
||||
|
||||
private onFSChange(uri: Uri): void {
|
||||
|
@ -547,4 +575,8 @@ export class Model {
|
|||
await eventToPromise(this.onDidRunOperation);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.repositoryDisposable.dispose();
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@
|
|||
import { scm, Uri, Disposable, SCMProvider, SCMResourceGroup, Event, ProviderResult, workspace } from 'vscode';
|
||||
import { Model, Resource, ResourceGroup, State } from './model';
|
||||
import { CommandCenter } from './commands';
|
||||
import { anyEvent, mapEvent } from './util';
|
||||
import { mapEvent } from './util';
|
||||
|
||||
export class GitSCMProvider implements SCMProvider {
|
||||
|
||||
|
@ -17,7 +17,7 @@ export class GitSCMProvider implements SCMProvider {
|
|||
get resources(): SCMResourceGroup[] { return this.model.resources; }
|
||||
|
||||
get onDidChange(): Event<SCMResourceGroup[]> {
|
||||
return mapEvent(anyEvent<any>(this.model.onDidChange, this.model.onDidChangeState), () => this.model.resources);
|
||||
return mapEvent(this.model.onDidChange, () => this.model.resources);
|
||||
}
|
||||
|
||||
get label(): string { return 'Git'; }
|
||||
|
|
|
@ -30,9 +30,7 @@ export class CheckoutStatusBar {
|
|||
const HEAD = this.model.HEAD;
|
||||
|
||||
if (!HEAD) {
|
||||
this.raw.command = '';
|
||||
this.raw.color = 'rgb(100, 100, 100)';
|
||||
this.raw.text = 'unknown';
|
||||
this.raw.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -48,6 +46,7 @@ export class CheckoutStatusBar {
|
|||
(this.model.workingTreeGroup.resources.length > 0 ? '*' : '') +
|
||||
(this.model.indexGroup.resources.length > 0 ? '+' : '') +
|
||||
(this.model.mergeGroup.resources.length > 0 ? '!' : '');
|
||||
this.raw.show();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
|
|
|
@ -28,6 +28,8 @@ export function combinedDisposable(disposables: IDisposable[]): IDisposable {
|
|||
return toDisposable(() => dispose(disposables));
|
||||
}
|
||||
|
||||
export const EmptyDisposable = toDisposable(() => null);
|
||||
|
||||
export function mapEvent<I, O>(event: Event<I>, map: (i: I) => O): Event<O> {
|
||||
return (listener, thisArgs = null, disposables?) => event(i => listener.call(thisArgs, map(i)), null, disposables);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue