Skip to content

Commit

Permalink
GH-3736: Use the system Git in electron.
Browse files Browse the repository at this point in the history
If Git does not exisit on the `PATH`,
fall back to the embedded Git.

Closes #3736.

Signed-off-by: Akos Kitta <[email protected]>
  • Loading branch information
Akos Kitta committed Dec 6, 2018
1 parent f73263b commit 98161c0
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 1 deletion.
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);
});
73 changes: 73 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,73 @@
/********************************************************************************
* 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 { MessageService } from '@theia/core/lib/common/message-service';
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;

@inject(MessageService)
protected readonly messageService: MessageService;

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));
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);
}
this.logger.info('Could not find Git on the PATH. Falling back to the embedded Git.');
}
// LOCAL_GIT_DIRECTORY
// GIT_EXEC_PATH

}
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

0 comments on commit 98161c0

Please sign in to comment.