Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve support for empty Git repository in Git view #5484

Merged
merged 1 commit into from
Jul 9, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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