diff --git a/packages/git/package.json b/packages/git/package.json index a874ce21f6e1c..c26dcf5529a6f 100644 --- a/packages/git/package.json +++ b/packages/git/package.json @@ -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", @@ -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" diff --git a/packages/git/src/electron-node/init/electron-git-init-module.ts b/packages/git/src/electron-node/init/electron-git-init-module.ts new file mode 100644 index 0000000000000..25a2ea79eae93 --- /dev/null +++ b/packages/git/src/electron-node/init/electron-git-init-module.ts @@ -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); +}); diff --git a/packages/git/src/electron-node/init/electron-git-init.ts b/packages/git/src/electron-node/init/electron-git-init.ts new file mode 100644 index 0000000000000..739e02c0961c2 --- /dev/null +++ b/packages/git/src/electron-node/init/electron-git-init.ts @@ -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 { + 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://github.com/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 { + if (err) { + this.logger.error(err); + } + this.logger.info('Could not find Git on the PATH. Falling back to the embedded Git.'); + } + +} diff --git a/packages/git/src/node/dugite-git.ts b/packages/git/src/node/dugite-git.ts index 067797ad21d9a..8a2c10cfe11f3 100644 --- a/packages/git/src/node/dugite-git.ts +++ b/packages/git/src/node/dugite-git.ts @@ -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. @@ -308,19 +309,31 @@ export class DugiteGit implements Git { @inject(GitEnvProvider) protected readonly envProvider: GitEnvProvider; + @inject(GitInit) + protected readonly gitInit: GitInit; + + protected ready: Deferred = new Deferred(); protected gitEnv: Deferred = 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 { + 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 }); @@ -328,6 +341,7 @@ export class DugiteGit implements Git { } async repositories(workspaceRootUri: string, options: Git.Options.Repositories): Promise { + await this.ready.promise; const workspaceRootPath = this.getFsPath(workspaceRootUri); const repositories: Repository[] = []; const containingPath = await this.resolveContainingPath(workspaceRootPath); @@ -353,6 +367,7 @@ export class DugiteGit implements Git { } async status(repository: Repository): Promise { + 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 }); @@ -360,6 +375,7 @@ export class DugiteGit implements Git { } async add(repository: Repository, uri: string | string[]): Promise { + 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, () => @@ -368,6 +384,7 @@ export class DugiteGit implements Git { } async unstage(repository: Repository, uri: string | string[], options?: Git.Options.Unstage): Promise { + 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; @@ -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; // tslint:disable-next-line:no-any async branch(repository: any, options: any): Promise { + 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)) { @@ -407,6 +425,7 @@ export class DugiteGit implements Git { } async checkout(repository: Repository, options: Git.Options.Checkout.CheckoutBranch | Git.Options.Checkout.WorkingTreeFile): Promise { + 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); @@ -422,6 +441,7 @@ export class DugiteGit implements Git { } async commit(repository: Repository, message?: string, options?: Git.Options.Commit): Promise { + 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]); @@ -431,6 +451,7 @@ export class DugiteGit implements Git { } async fetch(repository: Repository, options?: Git.Options.Fetch): Promise { + await this.ready.promise; const repositoryPath = this.getFsPath(repository); const r = await this.getDefaultRemote(repositoryPath, options ? options.remote : undefined); if (r) { @@ -443,6 +464,7 @@ export class DugiteGit implements Git { } async push(repository: Repository, { remote, localBranch, remoteBranch, setUpstream, force }: Git.Options.Push = {}): Promise { + await this.ready.promise; const repositoryPath = this.getFsPath(repository); const currentRemote = await this.getDefaultRemote(repositoryPath, remote); if (currentRemote === undefined) { @@ -472,6 +494,7 @@ export class DugiteGit implements Git { } async pull(repository: Repository, { remote, branch, rebase }: Git.Options.Pull = {}): Promise { + await this.ready.promise; const repositoryPath = this.getFsPath(repository); const currentRemote = await this.getDefaultRemote(repositoryPath, remote); if (currentRemote === undefined) { @@ -496,6 +519,7 @@ export class DugiteGit implements Git { } async reset(repository: Repository, options: Git.Options.Reset): Promise { + 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]); @@ -505,6 +529,7 @@ export class DugiteGit implements Git { } async merge(repository: Repository, options: Git.Options.Merge): Promise { + 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, () => @@ -513,6 +538,7 @@ export class DugiteGit implements Git { } async show(repository: Repository, uri: string, options?: Git.Options.Show): Promise { + await this.ready.promise; const encoding = options ? options.encoding || 'utf8' : 'utf8'; const commitish = this.getCommitish(options); const repositoryPath = this.getFsPath(repository); @@ -525,11 +551,13 @@ export class DugiteGit implements Git { } async remote(repository: Repository): Promise { + await this.ready.promise; const repositoryPath = this.getFsPath(repository); return this.getRemotes(repositoryPath); } async exec(repository: Repository, args: string[], options?: Git.Options.Execution): Promise { + await this.ready.promise; const repositoryPath = this.getFsPath(repository); return this.manager.run(repository, async () => { const name = options && options.name ? options.name : ''; @@ -550,6 +578,7 @@ export class DugiteGit implements Git { } async diff(repository: Repository, options?: Git.Options.Diff): Promise { + await this.ready.promise; const args = ['diff', '--name-status', '-C', '-M', '-z']; args.push(this.mapRange((options || {}).range)); if (options && options.uri) { @@ -561,6 +590,7 @@ export class DugiteGit implements Git { } async log(repository: Repository, options?: Git.Options.Log): Promise { + 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']; @@ -589,6 +619,7 @@ export class DugiteGit implements Git { } async blame(repository: Repository, uri: string, options?: Git.Options.Blame): Promise { + await this.ready.promise; const args = ['blame', '--root', '--incremental']; const file = Path.relative(this.getFsPath(repository), this.getFsPath(uri)); const repositoryPath = this.getFsPath(repository); @@ -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 { + await this.ready.promise; const args = ['ls-files']; const file = Path.relative(this.getFsPath(repository), this.getFsPath(uri)); if (options && options.errorUnmatch) { @@ -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 { + 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 }; @@ -662,6 +695,7 @@ export class DugiteGit implements Git { } private async getRemotes(repositoryPath: string): Promise { + 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 || ''; @@ -677,6 +711,7 @@ export class DugiteGit implements Git { } private async getCurrentBranch(repositoryPath: string, localBranch?: string): Promise { + await this.ready.promise; if (localBranch !== undefined) { return localBranch; } diff --git a/packages/git/src/node/init/git-init-module.ts b/packages/git/src/node/init/git-init-module.ts new file mode 100644 index 0000000000000..c006d53cb6785 --- /dev/null +++ b/packages/git/src/node/init/git-init-module.ts @@ -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); +}); diff --git a/packages/git/src/node/init/git-init.ts b/packages/git/src/node/init/git-init.ts new file mode 100644 index 0000000000000..89cb9c128a4be --- /dev/null +++ b/packages/git/src/node/init/git-init.ts @@ -0,0 +1,51 @@ +/******************************************************************************** + * 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 { injectable } from 'inversify'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; + +/** + * Initializer hook for Git. + */ +export const GitInit = Symbol('GitInit'); +export interface GitInit extends Disposable { + + /** + * Called before `Git` is ready to be used in Theia. Git operations cannot be executed before the returning promise is not resolved or rejected. + * + * This implementation does nothing at all. + */ + init(): Promise; + +} + +/** + * The default initializer. It is used in the browser. Does nothing at all. + */ +@injectable() +export class DefaultGitInit implements GitInit { + + protected readonly toDispose = new DisposableCollection(); + + async init(): Promise { + // NOOP + } + + dispose(): void { + this.toDispose.dispose(); + } + +} diff --git a/packages/git/src/node/test/binding-helper.ts b/packages/git/src/node/test/binding-helper.ts index 944915ac08ce0..d3f04aec066ff 100644 --- a/packages/git/src/node/test/binding-helper.ts +++ b/packages/git/src/node/test/binding-helper.ts @@ -21,6 +21,7 @@ import { bindGit, GitBindingOptions } from '../git-backend-module'; import { bindLogger } from '@theia/core/lib/node/logger-backend-module'; import { NoSyncRepositoryManager } from '.././test/no-sync-repository-manager'; import { GitEnvProvider, DefaultGitEnvProvider } from '../env/git-env-provider'; +import { GitInit, DefaultGitInit } from '../init/git-init'; // tslint:disable-next-line:no-any export function initializeBindings(): { container: Container, bind: interfaces.Bind } { @@ -28,6 +29,8 @@ export function initializeBindings(): { container: Container, bind: interfaces.B const bind = container.bind.bind(container); bind(DefaultGitEnvProvider).toSelf().inRequestScope(); bind(GitEnvProvider).toService(DefaultGitEnvProvider); + bind(DefaultGitInit).toSelf(); + bind(GitInit).toService(DefaultGitInit); bindLogger(bind); return { container, bind }; } diff --git a/yarn.lock b/yarn.lock index 349750c7c018d..d7582d20e6c8f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4025,7 +4025,7 @@ find-cache-dir@^1.0.0: make-dir "^1.0.0" pkg-dir "^2.0.0" -find-git-exec@0.0.1-alpha.2: +find-git-exec@0.0.1-alpha.2, find-git-exec@^0.0.1-alpha.2: version "0.0.1-alpha.2" resolved "https://registry.yarnpkg.com/find-git-exec/-/find-git-exec-0.0.1-alpha.2.tgz#02c266b3be6e411c19aa5fd6f813c96a73286fac" dependencies: