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

GH-3736: Use the system Git in electron. #3747

Merged
merged 1 commit into from
Dec 18, 2018
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
5 changes: 5 additions & 0 deletions packages/git/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@types/p-queue": "^2.3.1",
"diff": "^3.4.0",
"dugite-extra": "0.1.9",
"find-git-exec": "^0.0.1-alpha.2",
"find-git-repositories": "^0.1.0",
"fs-extra": "^4.0.2",
"moment": "^2.21.0",
Expand All @@ -33,6 +34,10 @@
"backend": "lib/node/env/git-env-module",
"backendElectron": "lib/electron-node/env/electron-git-env-module"
},
{
"backend": "lib/node/init/git-init-module",
"backendElectron": "lib/electron-node/init/electron-git-init-module"
},
{
"frontend": "lib/browser/prompt/git-prompt-module",
"frontendElectron": "lib/electron-browser/prompt/electron-git-prompt-module"
Expand Down
24 changes: 24 additions & 0 deletions packages/git/src/electron-node/init/electron-git-init-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/********************************************************************************
* Copyright (C) 2018 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { ContainerModule } from 'inversify';
import { GitInit } from '../../node/init/git-init';
import { ElectronGitInit } from './electron-git-init';

export default new ContainerModule(bind => {
bind(ElectronGitInit).toSelf();
bind(GitInit).toService(ElectronGitInit);
});
67 changes: 67 additions & 0 deletions packages/git/src/electron-node/init/electron-git-init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/********************************************************************************
* Copyright (C) 2018 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { inject, injectable } from 'inversify';
import findGit from 'find-git-exec';
import { dirname } from 'path';
import { pathExists } from 'fs-extra';
import { ILogger } from '@theia/core/lib/common/logger';
import { DefaultGitInit } from '../../node/init/git-init';

/**
* The Git initializer for electron. If Git can be found on the `PATH`, it will be used instead of the embedded Git shipped with `dugite`.
*/
@injectable()
export class ElectronGitInit extends DefaultGitInit {

@inject(ILogger)
protected readonly logger: ILogger;

async init(): Promise<void> {
const { env } = process;
if (typeof env.LOCAL_GIT_DIRECTORY !== 'undefined' || typeof env.GIT_EXEC_PATH !== 'undefined') {
await this.handleExternalNotFound('Cannot use Git from the PATH when the LOCAL_GIT_DIRECTORY or the GIT_EXEC_PATH environment variables are set.');
} else {
try {
const { execPath, path, version } = await findGit();
if (!!execPath && !!path && !!version) {
// https:/desktop/dugite/issues/111#issuecomment-323222834
// Instead of the executable path, we need the root directory of Git.
const dir = dirname(dirname(path));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if path is e.g. /usr/local/bin/git, dir is supposed to be /usr/local?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☝️ Yes, there is the comment above the code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! ;-)

const [execPathOk, pathOk, dirOk] = await Promise.all([pathExists(execPath), pathExists(path), pathExists(dir)]);
if (execPathOk && pathOk && dirOk) {
process.env.LOCAL_GIT_DIRECTORY = dir;
process.env.GIT_EXEC_PATH = execPath;
this.logger.info(`Using Git [${version}] from the PATH. (${path})`);
return;
}
}
await this.handleExternalNotFound();
} catch (err) {
await this.handleExternalNotFound(err);
}
}
}

// tslint:disable-next-line:no-any
protected async handleExternalNotFound(err?: any): Promise<void> {
if (err) {
this.logger.error(err);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we notify users in such cases?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Oh sure. I did not want to log the stack trace; the user cannot do much with that, but I wanted to "log" Could not find Git on the PATH. Falling back to the embedded Git. via the messageService.

}
this.logger.info('Could not find Git on the PATH. Falling back to the embedded Git.');
}

}
35 changes: 35 additions & 0 deletions packages/git/src/node/dugite-git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { GitRepositoryManager } from './git-repository-manager';
import { GitLocator } from './git-locator/git-locator-protocol';
import { GitExecProvider } from './git-exec-provider';
import { GitEnvProvider } from './env/git-env-provider';
import { GitInit } from './init/git-init';

/**
* Parsing and converting raw Git output into Git model instances.
Expand Down Expand Up @@ -308,26 +309,39 @@ export class DugiteGit implements Git {
@inject(GitEnvProvider)
protected readonly envProvider: GitEnvProvider;

@inject(GitInit)
protected readonly gitInit: GitInit;

protected ready: Deferred<void> = new Deferred();
protected gitEnv: Deferred<Object> = new Deferred();

@postConstruct()
protected init(): void {
this.envProvider.getEnv().then(env => this.gitEnv.resolve(env));
this.gitInit.init()
.catch(err => {
this.logger.error('An error occurred during the Git initialization.', err);
this.ready.resolve();
})
.then(() => this.ready.resolve());
}

dispose(): void {
this.locator.dispose();
this.execProvider.dispose();
this.gitInit.dispose();
}

async clone(remoteUrl: string, options: Git.Options.Clone): Promise<Repository> {
await this.ready.promise;
const { localUri, branch } = options;
const [exec, env] = await Promise.all([this.execProvider.exec(), this.gitEnv.promise]);
await clone(remoteUrl, this.getFsPath(localUri), { branch }, { exec, env });
return { localUri };
}

async repositories(workspaceRootUri: string, options: Git.Options.Repositories): Promise<Repository[]> {
await this.ready.promise;
const workspaceRootPath = this.getFsPath(workspaceRootUri);
const repositories: Repository[] = [];
const containingPath = await this.resolveContainingPath(workspaceRootPath);
Expand All @@ -353,13 +367,15 @@ export class DugiteGit implements Git {
}

async status(repository: Repository): Promise<WorkingDirectoryStatus> {
await this.ready.promise;
const repositoryPath = this.getFsPath(repository);
const [exec, env] = await Promise.all([this.execProvider.exec(), this.gitEnv.promise]);
const dugiteStatus = await getStatus(repositoryPath, true, this.limit, { exec, env });
return this.mapStatus(dugiteStatus, repository);
}

async add(repository: Repository, uri: string | string[]): Promise<void> {
await this.ready.promise;
const paths = (Array.isArray(uri) ? uri : [uri]).map(FileUri.fsPath);
const [exec, env] = await Promise.all([this.execProvider.exec(), this.gitEnv.promise]);
return this.manager.run(repository, () =>
Expand All @@ -368,6 +384,7 @@ export class DugiteGit implements Git {
}

async unstage(repository: Repository, uri: string | string[], options?: Git.Options.Unstage): Promise<void> {
await this.ready.promise;
const paths = (Array.isArray(uri) ? uri : [uri]).map(FileUri.fsPath);
const treeish = options && options.treeish ? options.treeish : undefined;
const where = options && options.reset ? options.reset : undefined;
Expand All @@ -382,6 +399,7 @@ export class DugiteGit implements Git {
async branch(repository: Repository, options: Git.Options.BranchCommand.Create | Git.Options.BranchCommand.Rename | Git.Options.BranchCommand.Delete): Promise<void>;
// tslint:disable-next-line:no-any
async branch(repository: any, options: any): Promise<void | undefined | Branch | Branch[]> {
await this.ready.promise;
const [exec, env] = await Promise.all([this.execProvider.exec(), this.gitEnv.promise]);
const repositoryPath = this.getFsPath(repository);
if (GitUtils.isBranchList(options)) {
Expand All @@ -407,6 +425,7 @@ export class DugiteGit implements Git {
}

async checkout(repository: Repository, options: Git.Options.Checkout.CheckoutBranch | Git.Options.Checkout.WorkingTreeFile): Promise<void> {
await this.ready.promise;
const [exec, env] = await Promise.all([this.execProvider.exec(), this.gitEnv.promise]);
return this.manager.run(repository, () => {
const repositoryPath = this.getFsPath(repository);
Expand All @@ -422,6 +441,7 @@ export class DugiteGit implements Git {
}

async commit(repository: Repository, message?: string, options?: Git.Options.Commit): Promise<void> {
await this.ready.promise;
const signOff = options && options.signOff;
const amend = options && options.amend;
const [exec, env] = await Promise.all([this.execProvider.exec(), this.gitEnv.promise]);
Expand All @@ -431,6 +451,7 @@ export class DugiteGit implements Git {
}

async fetch(repository: Repository, options?: Git.Options.Fetch): Promise<void> {
await this.ready.promise;
const repositoryPath = this.getFsPath(repository);
const r = await this.getDefaultRemote(repositoryPath, options ? options.remote : undefined);
if (r) {
Expand All @@ -443,6 +464,7 @@ export class DugiteGit implements Git {
}

async push(repository: Repository, { remote, localBranch, remoteBranch, setUpstream, force }: Git.Options.Push = {}): Promise<void> {
await this.ready.promise;
const repositoryPath = this.getFsPath(repository);
const currentRemote = await this.getDefaultRemote(repositoryPath, remote);
if (currentRemote === undefined) {
Expand Down Expand Up @@ -472,6 +494,7 @@ export class DugiteGit implements Git {
}

async pull(repository: Repository, { remote, branch, rebase }: Git.Options.Pull = {}): Promise<void> {
await this.ready.promise;
const repositoryPath = this.getFsPath(repository);
const currentRemote = await this.getDefaultRemote(repositoryPath, remote);
if (currentRemote === undefined) {
Expand All @@ -496,6 +519,7 @@ export class DugiteGit implements Git {
}

async reset(repository: Repository, options: Git.Options.Reset): Promise<void> {
await this.ready.promise;
const repositoryPath = this.getFsPath(repository);
const mode = this.getResetMode(options.mode);
const [exec, env] = await Promise.all([this.execProvider.exec(), this.gitEnv.promise]);
Expand All @@ -505,6 +529,7 @@ export class DugiteGit implements Git {
}

async merge(repository: Repository, options: Git.Options.Merge): Promise<void> {
await this.ready.promise;
const repositoryPath = this.getFsPath(repository);
const [exec, env] = await Promise.all([this.execProvider.exec(), this.gitEnv.promise]);
return this.manager.run(repository, () =>
Expand All @@ -513,6 +538,7 @@ export class DugiteGit implements Git {
}

async show(repository: Repository, uri: string, options?: Git.Options.Show): Promise<string> {
await this.ready.promise;
const encoding = options ? options.encoding || 'utf8' : 'utf8';
const commitish = this.getCommitish(options);
const repositoryPath = this.getFsPath(repository);
Expand All @@ -525,11 +551,13 @@ export class DugiteGit implements Git {
}

async remote(repository: Repository): Promise<string[]> {
await this.ready.promise;
const repositoryPath = this.getFsPath(repository);
return this.getRemotes(repositoryPath);
}

async exec(repository: Repository, args: string[], options?: Git.Options.Execution): Promise<GitResult> {
await this.ready.promise;
const repositoryPath = this.getFsPath(repository);
return this.manager.run(repository, async () => {
const name = options && options.name ? options.name : '';
Expand All @@ -550,6 +578,7 @@ export class DugiteGit implements Git {
}

async diff(repository: Repository, options?: Git.Options.Diff): Promise<GitFileChange[]> {
await this.ready.promise;
const args = ['diff', '--name-status', '-C', '-M', '-z'];
args.push(this.mapRange((options || {}).range));
if (options && options.uri) {
Expand All @@ -561,6 +590,7 @@ export class DugiteGit implements Git {
}

async log(repository: Repository, options?: Git.Options.Log): Promise<CommitWithChanges[]> {
await this.ready.promise;
// If remaining commits should be calculated by the backend, then run `git rev-list --count ${fromRevision | HEAD~fromRevision}`.
// How to use `mailmap` to map authors: https://www.kernel.org/pub/software/scm/git/docs/git-shortlog.html.
const args = ['log'];
Expand Down Expand Up @@ -589,6 +619,7 @@ export class DugiteGit implements Git {
}

async blame(repository: Repository, uri: string, options?: Git.Options.Blame): Promise<GitFileBlame | undefined> {
await this.ready.promise;
const args = ['blame', '--root', '--incremental'];
const file = Path.relative(this.getFsPath(repository), this.getFsPath(uri));
const repositoryPath = this.getFsPath(repository);
Expand Down Expand Up @@ -623,6 +654,7 @@ export class DugiteGit implements Git {

// tslint:disable-next-line:no-any
async lsFiles(repository: Repository, uri: string, options?: Git.Options.LsFiles): Promise<any> {
await this.ready.promise;
const args = ['ls-files'];
const file = Path.relative(this.getFsPath(repository), this.getFsPath(uri));
if (options && options.errorUnmatch) {
Expand All @@ -645,6 +677,7 @@ export class DugiteGit implements Git {
// TODO: akitta what about symlinks? What if the workspace root is a symlink?
// Maybe, we should use `--show-cdup` here instead of `--show-toplevel` because `show-toplevel` dereferences symlinks.
private async resolveContainingPath(repositoryPath: string): Promise<string | undefined> {
await this.ready.promise;
// Do not log an error if we are not contained in a Git repository. Treat exit code 128 as a success too.
const [exec, env] = await Promise.all([this.execProvider.exec(), this.gitEnv.promise]);
const options = { successExitCodes: new Set([0, 128]), exec, env };
Expand All @@ -662,6 +695,7 @@ export class DugiteGit implements Git {
}

private async getRemotes(repositoryPath: string): Promise<string[]> {
await this.ready.promise;
const [exec, env] = await Promise.all([this.execProvider.exec(), this.gitEnv.promise]);
const result = await git(['remote'], repositoryPath, 'remote', { exec, env });
const out = result.stdout || '';
Expand All @@ -677,6 +711,7 @@ export class DugiteGit implements Git {
}

private async getCurrentBranch(repositoryPath: string, localBranch?: string): Promise<Branch | string> {
await this.ready.promise;
if (localBranch !== undefined) {
return localBranch;
}
Expand Down
23 changes: 23 additions & 0 deletions packages/git/src/node/init/git-init-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/********************************************************************************
* Copyright (C) 2018 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { ContainerModule } from 'inversify';
import { GitInit, DefaultGitInit } from './git-init';

export default new ContainerModule(bind => {
bind(DefaultGitInit).toSelf();
bind(GitInit).toService(DefaultGitInit);
});
Loading