Skip to content

Commit

Permalink
Improve support for empty Git repository in Git and Git History view
Browse files Browse the repository at this point in the history
Signed-off-by: Nigel Westbury <nigelipse@miegel.org>
  • Loading branch information
westbury committed Jun 21, 2019
1 parent 6b8e776 commit 15628e9
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 61 deletions.
106 changes: 63 additions & 43 deletions packages/git/src/browser/history/git-history-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,21 @@ export type GitHistoryListNode = (GitCommitNode | GitFileChangeNode);
@injectable()
export class GitHistoryWidget extends GitNavigableListWidget<GitHistoryListNode> implements StatefulWidget {
protected options: Git.Options.Log;
protected commits: GitCommitNode[];
protected ready: boolean;
protected singleFileMode: boolean;
private cancelIndicator: CancellationTokenSource;
protected listView: GitHistoryList | undefined;
protected hasMoreCommits: boolean;
protected allowScrollToSelected: boolean;
protected errorMessage: React.ReactNode;

protected status: {
state: 'loading',
} | {
state: 'ready',
commits: GitCommitNode[];
} | {
state: 'error',
errorMessage: React.ReactNode
};

constructor(
@inject(OpenerService) protected readonly openerService: OpenerService,
Expand Down Expand Up @@ -103,7 +110,6 @@ export class GitHistoryWidget extends GitNavigableListWidget<GitHistoryListNode>

async setContent(options?: Git.Options.Log) {
this.resetState(options);
this.ready = false;
if (options && options.uri) {
const fileStat = await this.fileSystem.getFileStat(options.uri);
this.singleFileMode = !!fileStat && !fileStat.isDirectory;
Expand All @@ -117,7 +123,7 @@ export class GitHistoryWidget extends GitNavigableListWidget<GitHistoryListNode>

protected resetState(options?: Git.Options.Log) {
this.options = options || {};
this.commits = [];
this.status = { state: 'loading' };
this.gitNodes = [];
this.hasMoreCommits = true;
this.allowScrollToSelected = true;
Expand All @@ -127,21 +133,22 @@ export class GitHistoryWidget extends GitNavigableListWidget<GitHistoryListNode>
let repository: Repository | undefined;
repository = this.repositoryProvider.findRepositoryOrSelected(options);

this.errorMessage = undefined;
this.cancelIndicator.cancel();
this.cancelIndicator = new CancellationTokenSource();
const token = this.cancelIndicator.token;

if (repository) {
try {
const currentCommits = this.status.state === 'ready' ? this.status.commits : [];

let changes = await this.git.log(repository, options);
if (token.isCancellationRequested || !this.hasMoreCommits) {
return;
}
if (options && ((options.maxCount && changes.length < options.maxCount) || (!options.maxCount && this.commits))) {
if (options && ((options.maxCount && changes.length < options.maxCount) || (!options.maxCount && currentCommits))) {
this.hasMoreCommits = false;
}
if (this.commits.length > 0) {
if (currentCommits.length > 0) {
changes = changes.slice(1);
}
if (changes.length > 0) {
Expand All @@ -164,21 +171,23 @@ export class GitHistoryWidget extends GitNavigableListWidget<GitHistoryListNode>
selected: false
});
}
this.commits.push(...commits);
currentCommits.push(...commits);
this.status = { state: 'ready', commits: currentCommits };
} else if (options && options.uri && repository) {
const pathIsUnderVersionControl = await this.git.lsFiles(repository, options.uri, { errorUnmatch: true });
if (!pathIsUnderVersionControl) {
this.errorMessage = <React.Fragment>It is not under version control.</React.Fragment>;
this.status = { state: 'error', errorMessage: <React.Fragment> It is not under version control.</React.Fragment> };
} else {
this.status = { state: 'error', errorMessage: <React.Fragment> No commits have been committed.</React.Fragment> };
}
}

} catch (error) {
this.errorMessage = error.message;
this.status = { state: 'error', errorMessage: error.message };
}

} else {
this.commits = [];
this.errorMessage = <React.Fragment>There is no repository selected in this workspace.</React.Fragment>;
this.status = { state: 'error', errorMessage: <React.Fragment>There is no repository selected in this workspace.</React.Fragment> };
}
}

Expand Down Expand Up @@ -232,37 +241,48 @@ export class GitHistoryWidget extends GitNavigableListWidget<GitHistoryListNode>
}

protected onDataReady(): void {
this.ready = true;
this.gitNodes = this.commits;
if (this.status.state === 'ready') {
this.gitNodes = this.status.commits;
}
this.update();
}

protected render(): React.ReactNode {
let content: React.ReactNode;
if (this.ready && this.gitNodes.length > 0) {
content = < React.Fragment >
{this.renderHistoryHeader()}
{this.renderCommitList()}
</React.Fragment>;
} else if (this.errorMessage) {
let path: React.ReactNode = '';
let reason: React.ReactNode;
reason = this.errorMessage;
if (this.options.uri) {
const relPath = this.relativePath(this.options.uri);
const repo = this.repositoryProvider.findRepository(new URI(this.options.uri));
const repoName = repo ? ` in ${new URI(repo.localUri).displayName}` : '';
path = ` for ${decodeURIComponent(relPath)}${repoName}`;
}
content = <AlertMessage
type='WARNING'
header={`There is no Git history available${path}`}>
{reason}
</AlertMessage>;
} else {
content = <div className='spinnerContainer'>
<span className='fa fa-spinner fa-pulse fa-3x fa-fw'></span>
</div>;
switch (this.status.state) {
case 'ready':
content = < React.Fragment >
{this.renderHistoryHeader()}
{this.renderCommitList()}
</React.Fragment>;
break;

case 'error':
let path: React.ReactNode = '';
let reason: React.ReactNode;
reason = this.status.errorMessage;
if (this.options.uri) {
const relPathEncoded = this.relativePath(this.options.uri);
const relPath = relPathEncoded ? `${decodeURIComponent(relPathEncoded)}` : '';

const repo = this.repositoryProvider.findRepository(new URI(this.options.uri));
const repoName = repo ? `${new URI(repo.localUri).displayName}` : '';

const relPathAndRepo = [relPath, repoName].filter(Boolean).join(' in ');
path = ` for ${relPathAndRepo}`;
}
content = <AlertMessage
type='WARNING'
header={`There is no Git history available${path}.`}>
{reason}
</AlertMessage>;
break;

case 'loading':
content = <div className='spinnerContainer'>
<span className='fa fa-spinner fa-pulse fa-3x fa-fw'></span>
</div>;
break;
}
return <div className='git-diff-container'>
{content}
Expand Down Expand Up @@ -425,18 +445,18 @@ export class GitHistoryWidget extends GitNavigableListWidget<GitHistoryListNode>

protected navigateLeft(): void {
const selected = this.getSelected();
if (selected) {
const idx = this.commits.findIndex(c => c.commitSha === selected.commitSha);
if (selected && this.status.state === 'ready') {
const idx = this.status.commits.findIndex(c => c.commitSha === selected.commitSha);
if (GitCommitNode.is(selected)) {
if (selected.expanded) {
this.addOrRemoveFileChangeNodes(selected);
} else {
if (idx > 0) {
this.selectNode(this.commits[idx - 1]);
this.selectNode(this.status.commits[idx - 1]);
}
}
} else if (GitFileChangeNode.is(selected)) {
this.selectNode(this.commits[idx]);
this.selectNode(this.status.commits[idx]);
}
}
this.update();
Expand Down
21 changes: 21 additions & 0 deletions packages/git/src/common/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,18 @@ export namespace Git {

}

/**
* Options for the `git rev-parse` command.
*/
export interface RevParse {

/**
* The reference to parse.
*/
readonly ref: string;

}

}
}

Expand Down Expand Up @@ -797,6 +809,15 @@ export interface Git extends Disposable {
*/
log(repository: Repository, options?: Git.Options.Log): Promise<CommitWithChanges[]>;

/**
* Returns the commit SHA of the given ref if the ref exists, or returns 'undefined' if the
* given ref does not exist.
*
* @param repository the repository where the ref may be found.
* @param options configuration containing the ref and optionally other properties for further refining the `git rev-parse` command execution.
*/
revParse(repository: Repository, options: Git.Options.RevParse): Promise<string | undefined>;

/**
* Returns the annotations of each line in the given file.
*
Expand Down
12 changes: 10 additions & 2 deletions packages/git/src/node/dugite-git.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -772,10 +772,18 @@ describe('log', function () {
const repository = { localUri };
const git = await createGit();
const result = await git.log(repository, { uri: localUri });
expect(result.length === 1).to.be.true;
expect(result[0].author.email === 'jon@doe.com').to.be.true;
expect(result.length).to.be.equal(1);
expect(result[0].author.email).to.be.equal('jon@doe.com');
});

it('should not fail when executed against an empty repository', async () => {
const root = await initRepository(track.mkdirSync('empty-log-test'));
const localUri = FileUri.create(root).toString();
const repository = { localUri };
const git = await createGit();
const result = await git.log(repository, { uri: localUri });
expect(result.length).to.be.equal(0);
});
});

function toPathSegment(repository: Repository, uri: string): string {
Expand Down
28 changes: 27 additions & 1 deletion packages/git/src/node/dugite-git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -657,13 +657,39 @@ export class DugiteGit implements Git {
const file = Path.relative(this.getFsPath(repository), this.getFsPath(options.uri)) || '.';
args.push(...[file]);
}
const result = await this.exec(repository, args);

const successExitCodes = new Set([0, 128]);
let result = await this.exec(repository, args, { successExitCodes });
if (result.exitCode !== 0) {
// Note that if no range specified then the 'to revision' defaults to HEAD
const rangeInvolvesHead = !options || !options.range || options.range.toRevision === 'HEAD';
const repositoryHasNoHead = !await this.revParse(repository, { ref: 'HEAD' });
// The 'log' command could potentially be valid when no HEAD if the revision range does not involve HEAD */
if (rangeInvolvesHead && repositoryHasNoHead) {
// The range involves HEAD but there is no HEAD. 'no head' most likely means a newly created repository with
// no commits, but could potentially have commits with no HEAD. This is effectively an empty repository.
return [];
}
// Either the range did not involve HEAD or HEAD exists. The error must be something else,
// so re-run but this time we don't ignore the error.
result = await this.exec(repository, args);
}

return this.commitDetailsParser.parse(
repository.localUri, result.stdout.trim()
.split(CommitDetailsParser.COMMIT_CHUNK_DELIMITER)
.filter(item => item && item.length > 0));
}

async revParse(repository: Repository, options: Git.Options.RevParse): Promise<string | undefined> {
const ref = options.ref;
const successExitCodes = new Set([0, 128]);
const result = await this.exec(repository, ['rev-parse', ref], { successExitCodes });
if (result.exitCode === 0) {
return result.stdout; // sha
}
}

async blame(repository: Repository, uri: string, options?: Git.Options.Blame): Promise<GitFileBlame | undefined> {
await this.ready.promise;
const args = ['blame', '--root', '--incremental'];
Expand Down
29 changes: 14 additions & 15 deletions packages/scm/src/browser/scm-amend-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,21 +104,20 @@ export class ScmAmendComponent extends React.Component<ScmAmendComponentProps, S
} else if (nextCommit === undefined && this.state.lastCommit === undefined) {
// No change here
} else if (this.transitionHint === 'none') {
if (this.state.lastCommit) {
// If the 'last' commit changes, but we are not expecting an 'amend'
// or 'unamend' to occur, then we clear out the list of amended commits.
// This is because an unexpected change has happened to the repoistory,
// perhaps the user commited, merged, or something. The amended commits
// will no longer be valid.
await this.clearAmendingCommits();
// There is a change to the last commit, but no transition hint so
// the view just updates without transition.
this.setState({ amendingCommits: [], lastCommit: nextCommit });
} else {
// There should always be a previous 'last commit'
throw new Error('unexpected state');
// this.setState({ amendingCommits: await this.buildAmendingList(), lastCommit: nextCommit });
}
// If the 'last' commit changes, but we are not expecting an 'amend'
// or 'unamend' to occur, then we clear out the list of amended commits.
// This is because an unexpected change has happened to the repoistory,
// perhaps the user commited, merged, or something. The amended commits
// will no longer be valid.

// Note that there may or may not have been a previous lastCommit (if the
// repository was previously empty with no initial commit then lastCommit
// will be undefined). Either way we clear the amending commits.
await this.clearAmendingCommits();

// There is a change to the last commit, but no transition hint so
// the view just updates without transition.
this.setState({ amendingCommits: [], lastCommit: nextCommit });
} else {
if (this.state.lastCommit && nextCommit) {
const direction: 'up' | 'down' = this.transitionHint === 'amend' ? 'up' : 'down';
Expand Down

0 comments on commit 15628e9

Please sign in to comment.