[Code] replace nodegit with native git (#45491) (#47920)

* [Code]  use native git to iterate git files
* [Code] use native git to clone/update repository
* [Code] git history using native git
* [Code] use native git to read file tree and file content
* [Code] fix the 'bad file' warning from status api
* [Code] use native git to handle worktree
* [Code] use native git to resolve references
* [Code] use native git to handle blame / diff
* [Code] patch git binaries in kibana build script
* [Code] migrate unit tests to use native git
This commit is contained in:
Yulong 2019-10-11 12:50:06 +08:00 committed by GitHub
parent 936e018172
commit bb92b8b8b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 732 additions and 1669 deletions

View file

@ -16,62 +16,21 @@
* specific language governing permissions and limitations
* under the License.
*/
import install from '@elastic/simple-git/scripts/install';
import { deleteAll } from '../lib';
import path from 'path';
import { scanCopy, untar, deleteAll } from '../lib';
import { createWriteStream, mkdirSync } from 'fs';
import { binaryInfo } from '../../../../x-pack/legacy/plugins/code/tasks/nodegit_info';
import wreck from '@hapi/wreck';
import { dirname, join, basename } from 'path';
import { createPromiseFromStreams } from '../../../legacy/utils/streams';
async function download(url, destination, log) {
const response = await wreck.request('GET', url);
if (response.statusCode !== 200) {
throw new Error(`Unexpected status code ${response.statusCode} when downloading ${url}`);
}
mkdirSync(dirname(destination), { recursive: true });
await createPromiseFromStreams([response, createWriteStream(destination)]);
log.debug('Downloaded ', url);
}
async function downloadAndExtractTarball(url, dest, log, retry) {
try {
await download(url, dest, log);
const extractDir = join(dirname(dest), basename(dest, '.tar.gz'));
await untar(dest, extractDir, {
strip: 1,
});
return extractDir;
} catch (e) {
if (retry > 0) {
await downloadAndExtractTarball(url, dest, log, retry - 1);
} else {
throw e;
}
}
}
async function patchNodeGit(config, log, build, platform) {
const plat = platform.isWindows() ? 'win32' : platform.getName();
const arch = platform.getNodeArch().split('-')[1];
const { downloadUrl, packageName } = binaryInfo(plat, arch);
const downloadPath = build.resolvePathForPlatform(platform, '.nodegit_binaries', packageName);
const extractDir = await downloadAndExtractTarball(downloadUrl, downloadPath, log, 3);
async function patchGit(config, log, build, platform) {
const downloadPath = build.resolvePathForPlatform(platform, '.git_binaries', 'git.tar.gz');
const destination = build.resolvePathForPlatform(
platform,
'node_modules/@elastic/nodegit/build/Release'
'node_modules/@elastic/simple-git/native/git'
);
log.debug('Replacing nodegit binaries from ', extractDir);
await deleteAll([destination], log);
await scanCopy({
source: extractDir,
destination: destination,
time: new Date(),
});
await deleteAll([extractDir, downloadPath], log);
log.debug('Replacing git binaries from ' + downloadPath + ' to ' + destination);
const p = platform.isWindows() ? 'win32' : platform.getName();
await deleteAll([destination]);
await install(p, downloadPath, destination);
await deleteAll([path.dirname(downloadPath)], log);
}
export const PatchNativeModulesTask = {
@ -80,7 +39,7 @@ export const PatchNativeModulesTask = {
await Promise.all(
config.getTargetPlatforms().map(async platform => {
if (!build.isOss()) {
await patchNodeGit(config, log, build, platform);
await patchGit(config, log, build, platform);
}
})
);

View file

@ -8,7 +8,9 @@ export interface CommitInfo {
updated: Date;
message: string;
committer: string;
committerEmail?: string;
author: string;
authorEmail?: string;
id: string;
parents: string[];
treeId: string;
@ -17,7 +19,7 @@ export interface CommitInfo {
export interface ReferenceInfo {
name: string;
reference: string;
commit: CommitInfo;
commit?: CommitInfo;
type: ReferenceType;
}

View file

@ -4,14 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Git from '@elastic/nodegit';
import assert from 'assert';
import { delay } from 'bluebird';
import fs from 'fs';
import path from 'path';
import rimraf from 'rimraf';
import sinon from 'sinon';
import { prepareProjectByCloning as prepareProject } from '../test_utils';
import { CloneWorkerResult, Repository } from '../../model';
import { DiskWatermarkService } from '../disk_watermark';
import { GitOperations } from '../git_operations';
@ -30,27 +28,6 @@ const esQueue = {};
const serverOptions = createTestServerOption();
const gitOps = new GitOperations(serverOptions.repoPath);
function prepareProject(url: string, p: string) {
return new Promise(resolve => {
if (!fs.existsSync(p)) {
rimraf(p, error => {
Git.Clone.clone(url, p, {
fetchOpts: {
callbacks: {
certificateCheck: () => 0,
},
},
bare: 1,
}).then(repo => {
resolve(repo);
});
});
} else {
resolve();
}
});
}
function cleanWorkspace() {
return new Promise(resolve => {
rimraf(serverOptions.workspacePath, resolve);

View file

@ -4,13 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Git, { CloneOptions } from '@elastic/nodegit';
import assert from 'assert';
import fs from 'fs';
import path from 'path';
import rimraf from 'rimraf';
import sinon from 'sinon';
import { prepareProjectByCloning as prepareProject } from '../test_utils';
import { GitOperations } from '../git_operations';
import { CommitIndexRequest, WorkerReservedProgress } from '../../model';
import { CommitIndexer } from '../indexer/commit_indexer';
@ -33,29 +31,6 @@ const esClient = {
},
};
function prepareProject(url: string, p: string) {
const opts: CloneOptions = {
bare: 1,
fetchOpts: {
callbacks: {
certificateCheck: () => 0,
},
},
};
return new Promise(resolve => {
if (!fs.existsSync(p)) {
rimraf(p, error => {
Git.Clone.clone(url, p, opts).then(repo => {
resolve(repo);
});
});
} else {
resolve();
}
});
}
const repoUri = 'github.com/elastic/TypeScript-Node-Starter';
const serverOptions = createTestServerOption();

View file

@ -5,7 +5,6 @@
*/
// @ts-ignore
import Git from '@elastic/nodegit';
import assert from 'assert';
import { execSync } from 'child_process';
import fs from 'fs';
@ -13,6 +12,7 @@ import path from 'path';
import rimraf from 'rimraf';
import { GitOperations } from '../git_operations';
import { createTestServerOption } from '../test_utils';
import { prepareProjectByCloning as cloneProject, prepareProjectByInit } from '../test_utils';
describe('git_operations', () => {
it('get default branch from a non master repo', async () => {
@ -34,11 +34,10 @@ describe('git_operations', () => {
try {
const g = new GitOperations(serverOptions.repoPath);
const uri = path.join(repoUri, '.git');
const defaultBranch = await g.getDefaultBranch(uri);
const defaultBranch = await g.getDefaultBranch(repoUri);
assert.strictEqual(defaultBranch, 'trunk');
const headRevision = await g.getHeadRevision(uri);
const headCommit = await g.getCommitInfo(uri, 'HEAD');
const headRevision = await g.getHeadRevision(repoUri);
const headCommit = await g.getCommitInfo(repoUri, 'HEAD');
assert.strictEqual(headRevision, headCommit!.id);
} finally {
rimraf.sync(repoDir);
@ -48,33 +47,16 @@ describe('git_operations', () => {
async function prepareProject(repoPath: string) {
fs.mkdirSync(repoPath, { recursive: true });
const workDir = path.join(serverOptions.workspacePath, repoUri);
const repo = await Git.Repository.init(workDir, 0);
const content = '';
fs.writeFileSync(path.join(workDir, '1'), content, 'utf8');
const subFolder = 'src';
fs.mkdirSync(path.join(workDir, subFolder));
fs.writeFileSync(path.join(workDir, 'src/2'), content, 'utf8');
fs.writeFileSync(path.join(workDir, 'src/3'), content, 'utf8');
const index = await repo.refreshIndex();
await index.addByPath('1');
await index.addByPath('src/2');
await index.addByPath('src/3');
index.write();
const treeId = await index.writeTree();
const committer = Git.Signature.create('tester', 'test@test.com', Date.now() / 1000, 60);
const commit = await repo.createCommit(
'HEAD',
committer,
committer,
'commit for test',
treeId,
[]
);
const { git, commits } = await prepareProjectByInit(workDir, {
'commit for test': {
'1': '',
'src/2': '',
'src/3': '',
},
});
// eslint-disable-next-line no-console
console.log(`created commit ${commit.tostrS()}`);
await Git.Clone.clone(workDir, repoPath, { bare: 1 });
return Git.Repository.openBare(repoPath);
console.log(`created commit ${commits[0]}`);
await git.clone(workDir, repoPath, ['--bare']);
}
// @ts-ignore
@ -97,13 +79,10 @@ describe('git_operations', () => {
const iterator = await g.iterateRepo(repoUri, 'HEAD');
for await (const value of iterator) {
if (count === 0) {
assert.strictEqual('1', value.name);
assert.strictEqual('1', value.path);
} else if (count === 1) {
assert.strictEqual('2', value.name);
assert.strictEqual('src/2', value.path);
} else if (count === 2) {
assert.strictEqual('3', value.name);
assert.strictEqual('src/3', value.path);
} else {
assert.fail('this repo should contains exactly 3 files');
@ -114,26 +93,6 @@ describe('git_operations', () => {
assert.strictEqual(count, 3, 'this repo should contains exactly 3 files');
assert.strictEqual(totalFiles, 3, 'this repo should contains exactly 3 files');
});
function cloneProject(url: string, p: string) {
return new Promise(resolve => {
if (!fs.existsSync(p)) {
rimraf(p, error => {
Git.Clone.clone(url, p, {
bare: 1,
fetchOpts: {
callbacks: {
certificateCheck: () => 0,
},
},
}).then(repo => {
resolve(repo);
});
});
} else {
resolve();
}
});
}
it('can resolve branches', async () => {
const g = new GitOperations(serverOptions.repoPath);

View file

@ -4,9 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Git, { CloneOptions } from '@elastic/nodegit';
import assert from 'assert';
import fs from 'fs';
import path from 'path';
import rimraf from 'rimraf';
import sinon from 'sinon';
@ -23,6 +21,7 @@ import { LspService } from '../lsp/lsp_service';
import { RepositoryConfigController } from '../repository_config_controller';
import { createTestServerOption, emptyAsyncFunc, createTestHapiServer } from '../test_utils';
import { ConsoleLoggerFactory } from '../utils/console_logger_factory';
import { prepareProjectByCloning as prepareProject } from '../test_utils';
const log: Logger = new ConsoleLoggerFactory().getLogger(['test']);
@ -37,29 +36,6 @@ const esClient = {
},
};
function prepareProject(url: string, p: string) {
const opts: CloneOptions = {
bare: 1,
fetchOpts: {
callbacks: {
certificateCheck: () => 0,
},
},
};
return new Promise(resolve => {
if (!fs.existsSync(p)) {
rimraf(p, error => {
Git.Clone.clone(url, p, opts).then(repo => {
resolve(repo);
});
});
} else {
resolve();
}
});
}
const repoUri = 'github.com/elastic/TypeScript-Node-Starter';
const serverOptions = createTestServerOption();
@ -196,7 +172,7 @@ describe('LSP incremental indexer unit tests', () => {
// DeletebyQuery is called 10 times (1 file + 1 symbol reuqests per diff item)
// for 5 MODIFIED items
assert.strictEqual(deleteByQuerySpy.callCount, 10);
assert.strictEqual(deleteByQuerySpy.callCount, 5);
// There are 5 MODIFIED items and 1 ADDED item. Only 1 file is in supported
// language. Each file with supported language has 1 file + 1 symbol + 1 reference.
@ -207,7 +183,7 @@ describe('LSP incremental indexer unit tests', () => {
for (let i = 0; i < bulkSpy.callCount; i++) {
total += bulkSpy.getCall(i).args[0].body.length;
}
assert.strictEqual(total, 16);
assert.strictEqual(total, 8);
// @ts-ignore
}).timeout(20000);
@ -313,8 +289,8 @@ describe('LSP incremental indexer unit tests', () => {
for (let i = 0; i < bulkSpy.callCount; i++) {
total += bulkSpy.getCall(i).args[0].body.length;
}
assert.strictEqual(total, 5 * 2);
assert.strictEqual(deleteByQuerySpy.callCount, 4);
assert.strictEqual(total, 4 * 2);
assert.strictEqual(deleteByQuerySpy.callCount, 1);
// @ts-ignore
}).timeout(20000);
// @ts-ignore

View file

@ -4,12 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Git, { CloneOptions } from '@elastic/nodegit';
import assert from 'assert';
import fs from 'fs';
import path from 'path';
import rimraf from 'rimraf';
import sinon from 'sinon';
import { prepareProjectByCloning as prepareProject } from '../test_utils';
import { GitOperations } from '../git_operations';
import { WorkerReservedProgress } from '../../model';
@ -36,29 +35,6 @@ const esClient = {
},
};
function prepareProject(url: string, p: string) {
const opts: CloneOptions = {
bare: 1,
fetchOpts: {
callbacks: {
certificateCheck: () => 0,
},
},
};
return new Promise(resolve => {
if (!fs.existsSync(p)) {
rimraf(p, error => {
Git.Clone.clone(url, p, opts).then(repo => {
resolve(repo);
});
});
} else {
resolve();
}
});
}
const repoUri = 'github.com/elastic/TypeScript-Node-Starter';
const serverOptions = createTestServerOption();

View file

@ -4,20 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Git from '@elastic/nodegit';
import fs from 'fs';
import path from 'path';
import rimraf from 'rimraf';
import sinon from 'sinon';
import assert from 'assert';
import { simplegit } from '@elastic/simple-git/dist';
import { GitOperations } from '../git_operations';
import { RepositoryConfigReservedField, RepositoryGitStatusReservedField } from '../indexer/schema';
import { InstallManager } from '../lsp/install_manager';
import { LspService } from '../lsp/lsp_service';
import { RepositoryConfigController } from '../repository_config_controller';
import { createTestHapiServer, createTestServerOption } from '../test_utils';
import {
createTestHapiServer,
createTestServerOption,
prepareProjectByCloning,
prepareProjectByInit,
} from '../test_utils';
import { ConsoleLoggerFactory } from '../utils/console_logger_factory';
const filename = 'hello.ts';
@ -42,41 +46,17 @@ describe('lsp_service tests', () => {
let firstCommitSha = '';
let secondCommitSha = '';
async function prepareProject(repoPath: string) {
fs.mkdirSync(repoPath, { recursive: true });
const repo = await Git.Repository.init(repoPath, 0);
const helloContent = "console.log('hello world');";
fs.writeFileSync(path.join(repo.workdir(), filename), helloContent, 'utf8');
const { commits } = await prepareProjectByInit(repoPath, {
'commit for test': {
[filename]: "console.log('hello world');",
},
'commit2 for test': {
'package.json': packagejson,
},
});
let index = await repo.refreshIndex();
await index.addByPath(filename);
index.write();
const treeId = await index.writeTree();
const committer = Git.Signature.create('tester', 'test@test.com', Date.now() / 1000, 60);
const commit = await repo.createCommit(
'HEAD',
committer,
committer,
'commit for test',
treeId,
[]
);
firstCommitSha = commit.tostrS();
fs.writeFileSync(path.join(repo.workdir(), 'package.json'), packagejson, 'utf8');
index = await repo.refreshIndex();
await index.addByPath('package.json');
index.write();
const treeId2 = await index.writeTree();
const commit2 = await repo.createCommit(
'HEAD',
committer,
committer,
'commit2 for test',
treeId2,
[commit]
);
secondCommitSha = commit2.tostrS();
return repo;
firstCommitSha = commits[0];
secondCommitSha = commits[1];
}
const serverOptions = createTestServerOption();
@ -110,14 +90,7 @@ describe('lsp_service tests', () => {
before(async () => {
const tmpRepo = path.join(serverOptions.repoPath, 'tmp');
await prepareProject(tmpRepo);
await Git.Clone.clone(`file://${tmpRepo}`, path.join(serverOptions.repoPath, repoUri), {
bare: 1,
fetchOpts: {
callbacks: {
certificateCheck: () => 0,
},
},
});
await prepareProjectByCloning(`file://${tmpRepo}`, path.join(serverOptions.repoPath, repoUri));
});
function comparePath(pathA: string, pathB: string) {
@ -195,7 +168,7 @@ describe('lsp_service tests', () => {
await lspservice.shutdown();
}
// @ts-ignore
}).timeout(10000);
}).timeout(30000);
it('unload a workspace', async () => {
const lspservice = mockLspService();
@ -234,40 +207,7 @@ describe('lsp_service tests', () => {
await lspservice.shutdown();
}
// @ts-ignore
}).timeout(10000);
it('should work if a worktree exists', async () => {
const lspservice = mockLspService();
try {
const revision = 'master';
// send a dummy request to open a workspace;
const response = await sendHoverRequest(lspservice, revision);
assert.ok(response);
const workspacePath = path.resolve(serverOptions.workspacePath, repoUri, revision);
const workspaceFolderExists = fs.existsSync(workspacePath);
// workspace is opened
assert.ok(workspaceFolderExists);
const bareRepoWorktree = path.join(
serverOptions.repoPath,
repoUri,
'worktrees',
`workspace-${revision}`
);
// worktree is exists
const bareRepoWorktreeExists = fs.existsSync(bareRepoWorktree);
assert.ok(bareRepoWorktreeExists);
// delete the workspace folder but leave worktree
rimraf.sync(workspacePath);
// send a dummy request to open it again
await sendHoverRequest(lspservice, revision);
assert.ok(fs.existsSync(workspacePath));
return;
} finally {
await lspservice.shutdown();
}
// @ts-ignore
}).timeout(20000);
}).timeout(30000);
it('should update if a worktree is not the newest', async () => {
const lspservice = mockLspService();
@ -277,23 +217,22 @@ describe('lsp_service tests', () => {
const response = await sendHoverRequest(lspservice, revision);
assert.ok(response);
const workspacePath = path.resolve(serverOptions.workspacePath, repoUri, revision);
const workspaceRepo = await Git.Repository.open(workspacePath);
const git = simplegit(workspacePath);
const workspaceCommit = await git.revparse(['HEAD']);
// workspace is newest now
assert.strictEqual((await workspaceRepo.getHeadCommit()).sha(), secondCommitSha);
const firstCommit = await workspaceRepo.getCommit(firstCommitSha);
assert.strictEqual(workspaceCommit, secondCommitSha);
// reset workspace to an older one
// @ts-ignore
await Git.Reset.reset(workspaceRepo, firstCommit, Git.Reset.TYPE.HARD, {});
assert.strictEqual((await workspaceRepo.getHeadCommit()).sha(), firstCommitSha);
await git.reset([firstCommitSha, '--hard']);
assert.strictEqual(await git.revparse(['HEAD']), firstCommitSha);
// send a request again;
await sendHoverRequest(lspservice, revision);
// workspace_handler should update workspace to the newest one
assert.strictEqual((await workspaceRepo.getHeadCommit()).sha(), secondCommitSha);
assert.strictEqual(await git.revparse(['HEAD']), secondCommitSha);
return;
} finally {
await lspservice.shutdown();
}
// @ts-ignore
}).timeout(10000);
}).timeout(30000);
});

View file

@ -28,7 +28,7 @@ describe('repository service test', () => {
after(() => {
return rimraf.sync(baseDir);
});
const service = new RepositoryService(repoDir, credsDir, log, true);
const service = new RepositoryService(repoDir, credsDir, log);
it('can not clone a repo by ssh without a key', async () => {
const repo = RepositoryUtils.buildRepository(

View file

@ -6,7 +6,6 @@
import fs from 'fs';
import path from 'path';
import Git from '@elastic/nodegit';
import assert from 'assert';
import * as os from 'os';
import rimraf from 'rimraf';
@ -16,6 +15,7 @@ import { LspRequest } from '../../model';
import { GitOperations } from '../git_operations';
import { WorkspaceHandler } from '../lsp/workspace_handler';
import { ConsoleLoggerFactory } from '../utils/console_logger_factory';
import { prepareProjectByInit } from '../test_utils';
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'code_test'));
const workspaceDir = path.join(baseDir, 'workspace');
@ -119,26 +119,11 @@ describe('workspace_handler tests', () => {
async function prepareProject(repoPath: string) {
fs.mkdirSync(repoPath, { recursive: true });
const repo = await Git.Repository.init(repoPath, 0);
const content = 'console.log("test")';
const subFolder = 'src';
fs.mkdirSync(path.join(repo.workdir(), subFolder));
fs.writeFileSync(path.join(repo.workdir(), 'src/app.ts'), content, 'utf8');
const index = await repo.refreshIndex();
await index.addByPath('src/app.ts');
index.write();
const treeId = await index.writeTree();
const committer = Git.Signature.create('tester', 'test@test.com', Date.now() / 1000, 60);
const commit = await repo.createCommit(
'HEAD',
committer,
committer,
'commit for test',
treeId,
[]
);
return { repo, commit };
await prepareProjectByInit(repoPath, {
'commit for test': {
'src/app.ts': 'console.log("test")',
},
});
}
it('should throw a error if file path is external', async () => {

View file

@ -6,8 +6,7 @@
import fileType from 'file-type';
import Boom from 'boom';
import { Commit, Oid, Revwalk } from '@elastic/nodegit';
import { commitInfo, GitOperations } from '../../git_operations';
import { GitOperations } from '../../git_operations';
import { FileTree } from '../../../model';
import { RequestContext, ServiceHandlerFor } from '../service_definition';
import { extractLines } from '../../utils/buffer';
@ -144,29 +143,15 @@ export const getGitServiceHandler = (
};
},
async history({ uri, path, revision, count, after }) {
const repository = await gitOps.openRepo(uri);
const commit = await gitOps.getCommitInfo(uri, revision);
if (commit === null) {
throw Boom.notFound(`commit ${revision} not found in repo ${uri}`);
}
const walk = repository.createRevWalk();
walk.sorting(Revwalk.SORT.TIME);
const commitId = Oid.fromString(commit!.id);
walk.push(commitId);
let commits: Commit[];
if (path) {
// magic number 10000: how many commits at the most to iterate in order to find the commits contains the path
const results = await walk.fileHistoryWalk(path, count, 10000);
commits = results.map(result => result.commit);
} else {
commits = await walk.getCommits(count);
}
let commits = await gitOps.log(uri, commit.id, after ? count + 1 : count, path);
if (after && commits.length > 0) {
if (commits[0].id().equal(commitId)) {
commits = commits.slice(1);
}
commits = commits.slice(1);
}
return commits.map(commitInfo);
return commits;
},
async branchesAndTags({ uri }) {
return await gitOps.getBranchAndTags(uri);

View file

@ -6,188 +6,123 @@
/* eslint-disable @typescript-eslint/camelcase */
import {
Blame,
Commit as NodeGitCommit,
Diff as NodeGitDiff,
Error as NodeGitError,
Oid,
Repository,
Revwalk,
} from '@elastic/nodegit';
import { FileItem, LsTreeSummary, simplegit, SimpleGit } from '@elastic/simple-git/dist';
import Boom from 'boom';
import LruCache from 'lru-cache';
import * as Path from 'path';
import * as fs from 'fs';
import { isBinaryFileSync } from 'isbinaryfile';
import { BlameSummary, DiffResultTextFile } from '@elastic/simple-git/dist/response';
import moment from 'moment';
import { GitBlame } from '../common/git_blame';
import { CommitDiff, Diff, DiffKind } from '../common/git_diff';
import { Commit, FileTree, FileTreeItemType, RepositoryUri } from '../model';
import { FileTree, FileTreeItemType, RepositoryUri } from '../model';
import { CommitInfo, ReferenceInfo, ReferenceType } from '../model/commit';
import { detectLanguage } from './utils/detect_language';
import {
GitPrime,
TreeEntry,
CommitDescription,
TreeDescription,
TagDescription,
GitObjectDescription,
BlobDescription,
} from './utils/git_prime';
import { FormatParser } from './utils/format_parser';
export const HEAD = 'HEAD';
const REFS_HEADS = 'refs/heads/';
export const DEFAULT_TREE_CHILDREN_LIMIT = 50;
/**
* do a nodegit operation and check the results. If it throws a not found error or returns null,
* rethrow a Boom.notFound error.
* @param func the nodegit operation
* @param message the message pass to Boom.notFound error
*/
async function checkExists<R>(func: () => Promise<R>, message: string): Promise<R> {
let result: R;
try {
result = await func();
} catch (e) {
if (e.errno === NodeGitError.CODE.ENOTFOUND) {
throw Boom.notFound(message);
} else {
throw e;
}
}
if (result == null) {
throw Boom.notFound(message);
}
return result;
export interface Blob {
isBinary(): boolean;
content(): Buffer;
rawsize(): number;
}
function entry2Tree(entry: TreeEntry, prefixPath: string = ''): FileTree {
function entry2Tree(entry: FileItem, prefixPath: string = ''): FileTree {
const type: FileTreeItemType = GitOperations.mode2type(entry.mode);
const { path, oid } = entry;
const { path, id } = entry;
return {
name: path,
path: prefixPath ? prefixPath + '/' + path : path,
sha1: oid,
sha1: id,
type,
};
}
interface Tree {
entries: TreeEntry[];
gitdir: string;
entries: FileItem[];
oid: string;
}
export class GitOperations {
private REPO_LRU_CACHE_SIZE = 16;
private REPO_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour;
private repoRoot: string;
private repoCache: LruCache<RepositoryUri, Repository>;
constructor(repoRoot: string) {
this.repoRoot = repoRoot;
const options = {
max: this.REPO_LRU_CACHE_SIZE,
maxAge: this.REPO_MAX_AGE_MS,
dispose: (repoUri: RepositoryUri, repo: Repository) => {
// Clean up the repository before disposing this repo out of the cache.
repo.cleanup();
},
};
this.repoCache = new LruCache(options);
}
public cleanRepo(uri: RepositoryUri) {
if (this.repoCache.has(uri)) {
this.repoCache.del(uri);
}
}
public async cleanAllRepo() {
this.repoCache.reset();
}
public async fileContent(uri: RepositoryUri, path: string, revision: string = 'master') {
const gitdir = this.repoDir(uri);
const git = await this.openGit(uri);
const commit: CommitInfo = await this.getCommitOr404(uri, revision);
const file = await GitPrime.readObject({
gitdir,
oid: commit.id,
filepath: path,
format: 'content',
});
if (file && file.type === 'blob') {
return file.object as BlobDescription;
}
throw Boom.unsupportedMediaType(`${uri}/${path} is not a file.`);
}
const p = `${commit.id}:${path}`;
public async getCommit(uri: RepositoryUri, revision: string): Promise<NodeGitCommit> {
const info = await this.getCommitOr404(uri, revision);
const repo = await this.openRepo(uri);
return (await checkExists(
() => this.findCommit(repo, info.id),
`revision or branch ${revision} not found in ${uri}`
)) as NodeGitCommit;
const type = await git.catFile(['-t', p]);
if (type.trim() === 'blob') {
const buffer: Buffer = await git.binaryCatFile(['blob', p]);
return {
content(): Buffer {
return buffer;
},
rawsize(): number {
return buffer.length;
},
isBinary(): boolean {
return isBinaryFileSync(buffer);
},
} as Blob;
} else {
throw Boom.unsupportedMediaType(`${uri}/${path} is not a file.`);
}
}
public async getDefaultBranch(uri: RepositoryUri): Promise<string> {
const gitdir = this.repoDir(uri);
const ref = await GitPrime.resolveRef({ gitdir, ref: HEAD, depth: 2 });
if (ref.startsWith(REFS_HEADS)) {
return ref.substr(REFS_HEADS.length);
}
return ref;
const git = await this.openGit(uri);
return (await git.raw(['symbolic-ref', HEAD, '--short'])).trim();
}
public async getHeadRevision(uri: RepositoryUri): Promise<string> {
const gitdir = this.repoDir(uri);
return await GitPrime.resolveRef({ gitdir, ref: HEAD, depth: 10 });
return await this.getRevision(uri, HEAD);
}
public async getRevision(uri: RepositoryUri, ref: string): Promise<string> {
const git = await this.openGit(uri);
return await git.revparse([ref]);
}
public async blame(uri: RepositoryUri, revision: string, path: string): Promise<GitBlame[]> {
const repo = await this.openRepo(uri);
const newestCommit = (await this.getCommit(uri, revision)).id();
const blame = await Blame.file(repo, path, { newestCommit });
const git = await this.openGit(uri);
const blameSummary: BlameSummary = await git.blame(revision, path);
const results: GitBlame[] = [];
for (let i = 0; i < blame.getHunkCount(); i++) {
const hunk = blame.getHunkByIndex(i);
// @ts-ignore wrong definition in nodegit
const commit = await repo.getCommit(hunk.finalCommitId());
for (const blame of blameSummary.blames) {
results.push({
committer: {
// @ts-ignore wrong definition in nodegit
name: hunk.finalSignature().name(),
// @ts-ignore wrong definition in nodegit
email: hunk.finalSignature().email(),
name: blame.commit.author!.name,
email: blame.commit.author!.email,
},
// @ts-ignore wrong definition in nodegit
startLine: hunk.finalStartLineNumber(),
// @ts-ignore wrong definition in nodegit
lines: hunk.linesInHunk(),
startLine: blame.resultLine,
lines: blame.lines,
commit: {
id: commit.sha(),
message: commit.message(),
date: commit.date().toISOString(),
id: blame.commit.id!,
message: blame.commit.message!,
date: moment
.unix(blame.commit.author!.time)
.utcOffset(blame.commit.author!.tz)
.toISOString(true),
},
});
}
return results;
}
public async openRepo(uri: RepositoryUri): Promise<Repository> {
if (this.repoCache.has(uri)) {
const repo = this.repoCache.get(uri) as Repository;
return Promise.resolve(repo);
}
public async openGit(uri: RepositoryUri, check: boolean = true): Promise<SimpleGit> {
const repoDir = this.repoDir(uri);
const repo = await checkExists<Repository>(
() => Repository.open(repoDir),
`repo ${uri} not found`
);
this.repoCache.set(uri, repo);
return Promise.resolve(repo);
const git = simplegit(repoDir);
if (!check) return git;
if (await git.checkIsRepo()) {
return git;
} else {
throw Boom.notFound(`repo ${uri} not found`);
}
}
private repoDir(uri: RepositoryUri) {
@ -202,85 +137,33 @@ export class GitOperations {
}
}
private static async isTextFile(gitdir: string, entry: TreeEntry) {
if (entry.type === 'blob') {
const type = GitOperations.mode2type(entry.mode);
if (type === FileTreeItemType.File) {
return await GitPrime.isTextFile({ gitdir, oid: entry.oid });
}
}
return false;
}
public async countRepoFiles(uri: RepositoryUri, revision: string): Promise<number> {
let count = 0;
const commit = await this.getCommitOr404(uri, revision);
const gitdir = this.repoDir(uri);
const commitObject = await GitPrime.readObject({ gitdir, oid: commit.id });
const treeId = (commitObject.object as CommitDescription).tree;
const git = await this.openGit(uri);
const ls = new LsTreeSummary(git, revision, '.', { recursive: true });
return (await ls.allFiles()).length;
}
async function walk(oid: string) {
const tree = await GitOperations.readTree(gitdir, oid);
for (const entry of tree.entries) {
if (entry.type === 'tree') {
await walk(entry.oid);
} else if (await GitOperations.isTextFile(gitdir, entry)) {
count++;
}
public async *iterateRepo(uri: RepositoryUri, revision: string) {
const git = await this.openGit(uri);
const ls = new LsTreeSummary(git, revision, '.', { showSize: true, recursive: true });
for await (const file of ls.iterator()) {
const type = GitOperations.mode2type(file.mode);
if (type === FileTreeItemType.File) {
yield file;
}
}
await walk(treeId);
return count;
}
public async iterateRepo(
uri: RepositoryUri,
revision: string
): Promise<AsyncIterableIterator<FileTree>> {
const commit = await this.getCommitOr404(uri, revision);
const gitdir = this.repoDir(uri);
const commitObject = await GitPrime.readObject({ gitdir, oid: commit.id });
const treeId = (commitObject.object as CommitDescription).tree;
async function* walk(oid: string, prefix: string = ''): AsyncIterableIterator<FileTree> {
const tree = await GitOperations.readTree(gitdir, oid);
for (const entry of tree.entries) {
const path = prefix ? `${prefix}/${entry.path}` : entry.path;
if (entry.type === 'tree') {
yield* walk(entry.oid, path);
} else if (await GitOperations.isTextFile(gitdir, entry)) {
const type = GitOperations.mode2type(entry.mode);
yield {
name: entry.path,
type,
path,
repoUri: uri,
sha1: entry.oid,
} as FileTree;
}
}
}
return walk(treeId);
public async readTree(git: SimpleGit, oid: string, path: string = '.'): Promise<Tree> {
const lsTree = new LsTreeSummary(git, oid, path, {});
const entries = await lsTree.allFiles();
return {
entries,
oid,
} as Tree;
}
private static async readTree(gitdir: string, oid: string): Promise<Tree> {
const { type, object } = await GitPrime.readObject({ gitdir, oid });
if (type === 'commit') {
return await this.readTree(gitdir, (object as CommitDescription).tree);
} else if (type === 'tree') {
const tree = object as TreeDescription;
return {
entries: tree.entries,
gitdir,
oid,
} as Tree;
} else {
throw new Error(`${oid} is not a tree`);
}
}
static mode2type(mode: string): FileTreeItemType {
public static mode2type(mode: string): FileTreeItemType {
switch (mode) {
case '100755':
case '100644':
@ -297,44 +180,6 @@ export class GitOperations {
}
}
public async iterateCommits(
uri: RepositoryUri,
startRevision: string,
untilRevision?: string
): Promise<Commit[]> {
const repository = await this.openRepo(uri);
const commit = await this.getCommit(uri, startRevision);
const revWalk = repository.createRevWalk();
revWalk.sorting(Revwalk.SORT.TOPOLOGICAL);
revWalk.push(commit.id());
const commits: NodeGitCommit[] = await revWalk.getCommitsUntil((c: NodeGitCommit) => {
// Iterate commits all the way to the oldest one.
return true;
});
const res: Commit[] = commits.map(c => {
return {
repoUri: uri,
id: c.sha(),
message: c.message(),
body: c.body(),
date: c.date(),
parents: c.parents().map(i => i.tostrS()),
author: {
name: c.author().name(),
email: c.author().email(),
},
committer: {
name: c.committer().name(),
email: c.committer().email(),
},
} as Commit;
});
return res;
}
/**
* Return a fileTree structure by walking the repo file tree.
* @param uri the repo uri
@ -355,7 +200,7 @@ export class GitOperations {
flatten: boolean = false
): Promise<FileTree> {
const commit = await this.getCommitOr404(uri, revision);
const gitdir = this.repoDir(uri);
const git = await this.openGit(uri);
if (path.startsWith('/')) {
path = path.slice(1);
}
@ -382,38 +227,46 @@ export class GitOperations {
path: '',
type: FileTreeItemType.Directory,
};
const tree = await GitOperations.readTree(gitdir, commit.treeId);
await this.fillChildren(root, tree, { skip, limit, flatten });
const tree = await this.readTree(git, commit.treeId);
await this.fillChildren(git, root, tree, { skip, limit, flatten });
if (path) {
await this.resolvePath(root, tree, path.split('/'), { skip, limit, flatten });
await this.resolvePath(git, root, tree, path.split('/'), { skip, limit, flatten });
}
return root;
} else {
const obj = await GitPrime.readObject({ gitdir, oid: commit.id, filepath: path });
const result: FileTree = {
name: path.split('/').pop() || '',
path,
type: type2item(obj.type!),
sha1: obj.oid,
};
if (result.type === FileTreeItemType.Directory) {
await this.fillChildren(
result,
{
gitdir,
entries: (obj.object as TreeDescription).entries,
oid: obj.oid,
},
{ skip, limit, flatten }
);
if (path) {
const file = (await this.readTree(git, commit.id, path)).entries[0];
const result: FileTree = {
name: path.split('/').pop() || '',
path,
type: type2item(file.type!),
sha1: file.id,
};
if (file.type === 'tree') {
await this.fillChildren(git, result, await this.readTree(git, file.id), {
skip,
limit,
flatten,
});
}
return result;
} else {
const root: FileTree = {
name: '',
path: '',
type: FileTreeItemType.Directory,
};
const tree = await this.readTree(git, commit.id, '.');
await this.fillChildren(git, root, tree, { skip, limit, flatten });
return root;
}
return result;
}
}
private async fillChildren(
git: SimpleGit,
result: FileTree,
{ entries, gitdir }: Tree,
{ entries }: Tree,
{ skip, limit, flatten }: { skip: number; limit: number; flatten: boolean }
) {
result.childrenCount = entries.length;
@ -422,15 +275,16 @@ export class GitOperations {
const child = entry2Tree(e, result.path);
result.children.push(child);
if (flatten && child.type === FileTreeItemType.Directory) {
const tree = await GitOperations.readTree(gitdir, e.oid);
const tree = await this.readTree(git, e.id);
if (tree.entries.length === 1) {
await this.fillChildren(child, tree, { skip, limit, flatten });
await this.fillChildren(git, child, tree, { skip, limit, flatten });
}
}
}
}
private async resolvePath(
git: SimpleGit,
result: FileTree,
tree: Tree,
paths: string[],
@ -443,17 +297,17 @@ export class GitOperations {
result.children = [];
}
const child = entry2Tree(entry, result.path);
const idx = result.children.findIndex(i => i.sha1 === entry.oid);
const idx = result.children.findIndex(i => i.sha1 === entry.id);
if (idx < 0) {
result.children.push(child);
} else {
result.children[idx] = child;
}
if (entry.type === 'tree') {
const subTree = await GitOperations.readTree(tree.gitdir, entry.oid);
await this.fillChildren(child, subTree, opt);
const subTree = await this.readTree(git, entry.id);
await this.fillChildren(git, child, subTree, opt);
if (rest.length > 0) {
await this.resolvePath(child, subTree, rest, opt);
await this.resolvePath(git, child, subTree, rest, opt);
}
}
}
@ -461,71 +315,82 @@ export class GitOperations {
}
public async getCommitDiff(uri: string, revision: string): Promise<CommitDiff> {
const repo = await this.openRepo(uri);
const commit = await this.getCommit(uri, revision);
const diffs = await commit.getDiff();
const git = await this.openGit(uri);
const commit = await this.getCommitOr404(uri, revision);
const diffs = await git.diffSummary([revision]);
const commitDiff: CommitDiff = {
commit: commitInfo(commit),
additions: 0,
deletions: 0,
commit,
additions: diffs.insertions,
deletions: diffs.deletions,
files: [],
};
for (const diff of diffs) {
const patches = await diff.patches();
for (const patch of patches) {
const { total_deletions, total_additions } = patch.lineStats();
commitDiff.additions += total_additions;
commitDiff.deletions += total_deletions;
if (patch.isAdded()) {
const path = patch.newFile().path();
const modifiedCode = await this.getModifiedCode(commit, path);
const language = await detectLanguage(path, modifiedCode);
commitDiff.files.push({
language,
path,
modifiedCode,
additions: total_additions,
deletions: total_deletions,
kind: DiffKind.ADDED,
});
} else if (patch.isDeleted()) {
const path = patch.oldFile().path();
const originCode = await this.getOriginCode(commit, repo, path);
const language = await detectLanguage(path, originCode);
commitDiff.files.push({
language,
path,
originCode,
kind: DiffKind.DELETED,
additions: total_additions,
deletions: total_deletions,
});
} else if (patch.isModified()) {
const path = patch.newFile().path();
const modifiedCode = await this.getModifiedCode(commit, path);
const originPath = patch.oldFile().path();
const originCode = await this.getOriginCode(commit, repo, originPath);
const language = await detectLanguage(patch.newFile().path(), modifiedCode);
commitDiff.files.push({
language,
path,
originPath,
originCode,
modifiedCode,
kind: DiffKind.MODIFIED,
additions: total_additions,
deletions: total_deletions,
});
} else if (patch.isRenamed()) {
const path = patch.newFile().path();
commitDiff.files.push({
path,
originPath: patch.oldFile().path(),
kind: DiffKind.RENAMED,
additions: total_additions,
deletions: total_deletions,
});
for (const d of diffs.files) {
if (!d.binary) {
const diff = d as DiffResultTextFile;
const kind = this.diffKind(diff);
switch (kind) {
case DiffKind.ADDED:
{
const path = diff.file;
const modifiedCode = await this.getModifiedCode(git, commit, path);
const language = await detectLanguage(path, modifiedCode);
commitDiff.files.push({
language,
path,
modifiedCode,
additions: diff.insertions,
deletions: diff.deletions,
kind,
});
}
break;
case DiffKind.DELETED:
{
const path = diff.file;
const originCode = await this.getOriginCode(git, commit, path);
const language = await detectLanguage(path, originCode);
commitDiff.files.push({
language,
path,
originCode,
kind,
additions: diff.insertions,
deletions: diff.deletions,
});
}
break;
case DiffKind.MODIFIED:
{
const path = diff.rename || diff.file;
const modifiedCode = await this.getModifiedCode(git, commit, path);
const originPath = diff.file;
const originCode = await this.getOriginCode(git, commit, originPath);
const language = await detectLanguage(path, modifiedCode);
commitDiff.files.push({
language,
path,
originPath,
originCode,
modifiedCode,
kind,
additions: diff.insertions,
deletions: diff.deletions,
});
}
break;
case DiffKind.RENAMED:
{
const path = diff.rename || diff.file;
commitDiff.files.push({
path,
originPath: diff.file,
kind,
additions: diff.insertions,
deletions: diff.deletions,
});
}
break;
}
}
}
@ -533,120 +398,91 @@ export class GitOperations {
}
public async getDiff(uri: string, oldRevision: string, newRevision: string): Promise<Diff> {
const repo = await this.openRepo(uri);
const oldCommit = await this.getCommit(uri, oldRevision);
const newCommit = await this.getCommit(uri, newRevision);
const oldTree = await oldCommit.getTree();
const newTree = await newCommit.getTree();
const diff = await NodeGitDiff.treeToTree(repo, oldTree, newTree);
const git = await this.openGit(uri);
const diff = await git.diffSummary([oldRevision, newRevision]);
const res: Diff = {
additions: 0,
deletions: 0,
additions: diff.insertions,
deletions: diff.deletions,
files: [],
};
const patches = await diff.patches();
for (const patch of patches) {
const { total_deletions, total_additions } = patch.lineStats();
res.additions += total_additions;
res.deletions += total_deletions;
if (patch.isAdded()) {
const path = patch.newFile().path();
diff.files.forEach(d => {
if (!d.binary) {
const td = d as DiffResultTextFile;
const kind = this.diffKind(td);
res.files.push({
path,
additions: total_additions,
deletions: total_deletions,
kind: DiffKind.ADDED,
});
} else if (patch.isDeleted()) {
const path = patch.oldFile().path();
res.files.push({
path,
kind: DiffKind.DELETED,
additions: total_additions,
deletions: total_deletions,
});
} else if (patch.isModified()) {
const path = patch.newFile().path();
const originPath = patch.oldFile().path();
res.files.push({
path,
originPath,
kind: DiffKind.MODIFIED,
additions: total_additions,
deletions: total_deletions,
});
} else if (patch.isRenamed()) {
const path = patch.newFile().path();
res.files.push({
path,
originPath: patch.oldFile().path(),
kind: DiffKind.RENAMED,
additions: total_additions,
deletions: total_deletions,
path: d.file,
additions: td.insertions,
deletions: td.deletions,
kind,
});
}
}
});
return res;
}
private async getOriginCode(commit: NodeGitCommit, repo: Repository, path: string) {
for (const oid of commit.parents()) {
const parentCommit = await repo.getCommit(oid);
if (parentCommit) {
const entry = await parentCommit.getEntry(path);
if (entry) {
return (await entry.getBlob()).content().toString('utf8');
}
}
private diffKind(diff: DiffResultTextFile) {
let kind: DiffKind = DiffKind.MODIFIED;
if (diff.changes === diff.insertions) {
kind = DiffKind.ADDED;
} else if (diff.changes === diff.deletions) {
kind = DiffKind.DELETED;
} else if (diff.rename) {
kind = DiffKind.RENAMED;
}
return '';
return kind;
}
private async getModifiedCode(commit: NodeGitCommit, path: string) {
const entry = await commit.getEntry(path);
return (await entry.getBlob()).content().toString('utf8');
private async getOriginCode(git: SimpleGit, commit: CommitInfo, path: string) {
const buffer: Buffer = await git.binaryCatFile(['blob', `${commit.id}~1:${path}`]);
return buffer.toString('utf8');
}
private async findCommit(repo: Repository, oid: string): Promise<NodeGitCommit | null> {
try {
return repo.getCommit(Oid.fromString(oid));
} catch (e) {
return null;
}
private async getModifiedCode(git: SimpleGit, commit: CommitInfo, path: string) {
const buffer: Buffer = await git.binaryCatFile(['blob', `${commit.id}:${path}`]);
return buffer.toString('utf8');
}
public async getBranchAndTags(repoUri: string): Promise<ReferenceInfo[]> {
const gitdir = this.repoDir(repoUri);
const remoteBranches = await GitPrime.listBranches({ gitdir, remote: 'origin' });
const results: ReferenceInfo[] = [];
for (const name of remoteBranches) {
const reference = `refs/remotes/origin/${name}`;
const commit = await this.getCommitInfo(repoUri, reference);
if (commit) {
results.push({
name,
reference,
type: ReferenceType.REMOTE_BRANCH,
commit,
});
const format = {
name: '%(refname:short)',
reference: '%(refname)',
type: '%(objecttype)',
commit: {
updated: '%(*authordate)',
message: '%(*contents)',
committer: '%(*committername)',
author: '%(*authorname)',
id: '%(*objectname)',
parents: '%(*parent)',
treeId: '%(*tree)',
},
};
const parser = new FormatParser(format);
const git = await this.openGit(repoUri);
const result = await git.raw([
'for-each-ref',
'--format=' + parser.toFormatStr(),
'refs/tags/*',
'refs/remotes/origin/*',
]);
const results = parser.parseResult(result);
return results.map(r => {
const ref: ReferenceInfo = {
name: r.name.startsWith('origin/') ? r.name.slice(7) : r.name,
reference: r.reference,
type: r.type === 'tag' ? ReferenceType.TAG : ReferenceType.REMOTE_BRANCH,
};
if (r.commit && r.commit.id) {
const commit = {
...r.commit,
};
commit.parents = r.commit.parents ? r.commit.parents.split(' ') : [];
commit.updated = new Date(r.commit.updated);
ref.commit = commit;
}
}
const tags = await GitPrime.listTags({ gitdir });
for (const name of tags) {
const reference = `refs/tags/${name}`;
const commit = await this.getCommitInfo(repoUri, reference);
if (commit) {
results.push({
name,
reference,
type: ReferenceType.TAG,
commit,
});
}
}
return results;
return ref;
});
}
public async getCommitOr404(repoUri: string, ref: string): Promise<CommitInfo> {
@ -657,71 +493,63 @@ export class GitOperations {
return commit;
}
public async log(
repoUri: string,
revision: string,
count: number,
path?: string
): Promise<CommitInfo[]> {
const git = await this.openGit(repoUri);
const options: any = {
n: count,
format: {
updated: '%ai',
message: '%B',
author: '%an',
authorEmail: '%ae',
committer: '%cn',
committerEmail: '%ce',
id: '%H',
parents: '%p',
treeId: '%T',
},
from: revision,
};
if (path) {
options.file = path;
}
const result = await git.log(options);
return (result.all as unknown) as CommitInfo[];
}
public async resolveRef(repoUri: string, ref: string): Promise<string | null> {
const git = await this.openGit(repoUri);
let oid = '';
try {
// try local branches or tags
oid = (await git.revparse(['-q', '--verify', ref])).trim();
} catch (e) {
// try remote branches
}
if (!oid) {
try {
oid = (await git.revparse(['-q', '--verify', `origin/${ref}`])).trim();
} catch (e1) {
// no match
}
}
return oid || null;
}
public async getCommitInfo(repoUri: string, ref: string): Promise<CommitInfo | null> {
const gitdir = this.repoDir(repoUri);
// depth: avoid infinite loop
let obj: GitObjectDescription | null = null;
let oid: string = '';
if (/^[0-9a-f]{5,40}$/.test(ref)) {
// it's possible ref is sha-1 object id
try {
oid = ref;
if (oid.length < 40) {
oid = await GitPrime.expandOid({ gitdir, oid });
}
obj = await GitPrime.readObject({ gitdir, oid, format: 'parsed' });
} catch (e) {
// expandOid or readObject failed
}
}
// test if it is a reference
if (!obj) {
try {
// try local branches or tags
oid = await GitPrime.resolveRef({ gitdir, ref, depth: 10 });
} catch (e) {
// try remote branches
try {
oid = await GitPrime.resolveRef({ gitdir, ref: `origin/${ref}`, depth: 10 });
} catch (e1) {
// no match
}
}
if (oid) {
obj = await GitPrime.readObject({ gitdir, oid, format: 'parsed' });
}
}
if (obj) {
if (obj.type === 'commit') {
const commit = obj.object as CommitDescription;
return {
id: obj.oid,
author: commit.author.name,
committer: commit.committer.name,
message: commit.message,
updated: new Date(commit.committer.timestamp * 1000),
parents: commit.parent,
treeId: commit.tree,
} as CommitInfo;
} else if (obj.type === 'tag') {
const tag = obj.object as TagDescription;
if (tag.type === 'commit') {
return await this.getCommitInfo(repoUri, tag.object);
}
const oid = await this.resolveRef(repoUri, ref);
if (oid) {
const commits = await this.log(repoUri, oid, 1);
if (commits.length > 0) {
return commits[0];
}
}
return null;
}
}
export function commitInfo(commit: NodeGitCommit): CommitInfo {
return {
updated: commit.date(),
message: commit.message(),
author: commit.author().name(),
committer: commit.committer().name(),
id: commit.sha().substr(0, 7),
parents: commit.parents().map(oid => oid.toString().substring(0, 7)),
treeId: commit.treeId().tostrS(),
};
}

View file

@ -101,7 +101,24 @@ export class CommitIndexer extends AbstractIndexer {
private commits: Commit[] | null = null;
protected async getIndexRequestCount(): Promise<number> {
try {
this.commits = await this.gitOps.iterateCommits(this.repoUri, HEAD);
this.commits = (await this.gitOps.log(this.repoUri, HEAD, Number.MAX_SAFE_INTEGER)).map(c => {
const [message, ...body] = c.message.split('\n');
return {
author: {
name: c.author,
email: c.authorEmail,
},
committer: {
name: c.committer,
email: c.committer,
},
message,
parents: c.parents,
date: c.updated,
id: c.id,
body: body.join('\n'),
} as Commit;
});
return this.commits.length;
} catch (error) {
if (this.isCancelled()) {

View file

@ -123,6 +123,7 @@ export class LspIncrementalIndexer extends LspIndexer {
protected async *getIndexRequestIterator(): AsyncIterableIterator<LspIncIndexRequest> {
try {
if (this.diff) {
await this.prepareWorkspace();
for (const f of this.diff.files) {
yield {
repoUri: this.repoUri,

View file

@ -5,7 +5,10 @@
*/
import { ResponseError } from 'vscode-jsonrpc';
import fs from 'fs';
import path from 'path';
import { promisify } from 'util';
import { isBinaryFile } from 'isbinaryfile';
import { ProgressReporter } from '.';
import { TEXT_FILE_LIMIT } from '../../common/file';
import {
@ -36,6 +39,9 @@ import {
} from './index_creation_request';
import { ALL_RESERVED, DocumentIndexName, ReferenceIndexName, SymbolIndexName } from './schema';
const state = promisify(fs.stat);
const readFile = promisify(fs.readFile);
export class LspIndexer extends AbstractIndexer {
public type: IndexerType = IndexerType.LSP;
// Batch index helper for symbols/references
@ -45,6 +51,7 @@ export class LspIndexer extends AbstractIndexer {
private LSP_BATCH_INDEX_SIZE = 50;
private DOC_BATCH_INDEX_SIZE = 50;
private workspaceDir: string = '';
constructor(
protected readonly repoUri: RepositoryUri,
@ -105,6 +112,8 @@ export class LspIndexer extends AbstractIndexer {
protected async *getIndexRequestIterator(): AsyncIterableIterator<LspIndexRequest> {
try {
await this.prepareWorkspace();
const fileIterator = await this.gitOps.iterateRepo(this.repoUri, HEAD);
for await (const file of fileIterator) {
const filePath = file.path!;
@ -126,6 +135,14 @@ export class LspIndexer extends AbstractIndexer {
}
}
protected async prepareWorkspace() {
const { workspaceDir } = await this.lspService.workspaceHandler.openWorkspace(
this.repoUri,
HEAD
);
this.workspaceDir = workspaceDir;
}
protected async getIndexRequestCount(): Promise<number> {
try {
return await this.gitOps.countRepoFiles(this.repoUri, HEAD);
@ -198,14 +215,20 @@ export class LspIndexer extends AbstractIndexer {
}
protected FILE_OVERSIZE_ERROR_MSG = 'File size exceeds limit. Skip index.';
protected BINARY_FILE_ERROR_MSG = 'Binary file detected. Skip index.';
protected async getFileSource(request: LspIndexRequest): Promise<string> {
const { revision, filePath } = request;
// Always read file content from the original bare repo
const blob = await this.gitOps.fileContent(this.repoUri, filePath, revision);
if (blob.rawsize() > TEXT_FILE_LIMIT) {
const { filePath } = request;
const fullPath = path.join(this.workspaceDir, filePath);
const fileStat = await state(fullPath);
const fileSize = fileStat.size;
if (fileSize > TEXT_FILE_LIMIT) {
throw new Error(this.FILE_OVERSIZE_ERROR_MSG);
}
return blob.content().toString();
const bin = await isBinaryFile(fullPath);
if (bin) {
throw new Error(this.BINARY_FILE_ERROR_MSG);
}
return readFile(fullPath, { encoding: 'utf8' }) as Promise<string>;
}
protected async execLspIndexing(
@ -278,7 +301,10 @@ export class LspIndexer extends AbstractIndexer {
try {
content = await this.getFileSource(request);
} catch (error) {
if ((error as Error).message === this.FILE_OVERSIZE_ERROR_MSG) {
if ((error as Error).message === this.BINARY_FILE_ERROR_MSG) {
this.log.debug(this.BINARY_FILE_ERROR_MSG);
return stats;
} else if ((error as Error).message === this.FILE_OVERSIZE_ERROR_MSG) {
// Skip this index request if the file is oversized
this.log.debug(this.FILE_OVERSIZE_ERROR_MSG);
return stats;

View file

@ -58,7 +58,6 @@ export function initLocalService(
server.events.on('stop', async () => {
loggerFactory.get().debug('shutdown lsp process');
await lspService.shutdown();
await gitOps.cleanAllRepo();
});
codeServices.registerHandler(
LspServiceDefinition,

View file

@ -6,10 +6,10 @@
/* eslint-disable no-console */
import fs from 'fs';
import Git from '@elastic/nodegit';
import rimraf from 'rimraf';
import { TestConfig, Repo } from '../../model/test_config';
import { prepareProjectByCloning } from '../test_utils';
export class TestRepoManager {
private repos: Repo[];
@ -20,32 +20,10 @@ export class TestRepoManager {
public async importAllRepos() {
for (const repo of this.repos) {
await this.importRepo(repo.url, repo.path);
await prepareProjectByCloning(repo.url, repo.path);
}
}
public importRepo(url: string, path: string) {
return new Promise(resolve => {
if (!fs.existsSync(path)) {
rimraf(path, error => {
console.log(`begin to import ${url} to ${path}`);
Git.Clone.clone(url, path, {
fetchOpts: {
callbacks: {
certificateCheck: () => 0,
},
},
}).then(repo => {
console.log(`import ${url} done`);
resolve(repo);
});
});
} else {
resolve();
}
});
}
public async cleanAllRepos() {
this.repos.forEach(repo => {
this.cleanRepo(repo.path);

View file

@ -4,38 +4,34 @@
* you may not use this file except in compliance with the Elastic License.
*/
import {
Commit,
Error as GitError,
Repository,
Reset,
TreeEntry,
// @ts-ignore
Worktree,
} from '@elastic/nodegit';
import Boom from 'boom';
import del from 'del';
import fs from 'fs';
import { delay } from 'lodash';
import path from 'path';
import { promisify } from 'util';
import { ResponseMessage } from 'vscode-jsonrpc/lib/messages';
import { Hover, Location, TextDocumentPositionParams } from 'vscode-languageserver';
import { DetailSymbolInformation, Full } from '@elastic/lsp-extension';
import { SimpleGit } from '@elastic/simple-git/dist/promise';
import { simplegit } from '@elastic/simple-git/dist';
import { RepositoryUtils } from '../../common/repository_utils';
import { parseLspUrl } from '../../common/uri_util';
import { LspRequest, WorkerReservedProgress } from '../../model';
import { GitOperations } from '../git_operations';
import { FileTreeItemType, LspRequest, WorkerReservedProgress } from '../../model';
import { GitOperations, HEAD } from '../git_operations';
import { EsClient } from '../lib/esqueue';
import { Logger } from '../log';
import { RepositoryObjectClient } from '../search';
import { LoggerFactory } from '../utils/log_factory';
export const MAX_RESULT_COUNT = 20;
interface Worktree {
path: string;
revision: string;
branch: string;
}
const mkdirAsync = promisify(fs.mkdir);
export const MAX_RESULT_COUNT = 20;
export class WorkspaceHandler {
private revisionMap: { [uri: string]: string } = {};
@ -58,9 +54,9 @@ export class WorkspaceHandler {
/**
* open workspace for repositoryUri, update it from bare repository if necessary.
* @param repositoryUri the uri of bare repository.
* @param revision
* @param ref
*/
public async openWorkspace(repositoryUri: string, revision: string) {
public async openWorkspace(repositoryUri: string, ref: string) {
// Try get repository clone status with 3 retries at maximum.
const tryGetGitStatus = async (retryCount: number) => {
let gitStatus;
@ -85,48 +81,85 @@ export class WorkspaceHandler {
if (this.objectClient) {
await tryGetGitStatus(0);
}
const bareRepo = await this.gitOps.openRepo(repositoryUri);
const targetCommit = await this.gitOps.getCommit(repositoryUri, revision);
const git = await this.gitOps.openGit(repositoryUri);
const defaultBranch = await this.gitOps.getDefaultBranch(repositoryUri);
if (revision !== defaultBranch) {
await this.checkCommit(bareRepo, targetCommit);
revision = defaultBranch;
const targetRevision = await this.gitOps.getRevision(repositoryUri, ref);
if (ref !== defaultBranch) {
await this.checkCommit(git, targetRevision);
ref = defaultBranch;
}
let workspace;
if (await this.workspaceExists(bareRepo, repositoryUri, revision)) {
workspace = await this.updateWorkspace(repositoryUri, revision, targetCommit);
const workspaceBranch = this.workspaceWorktreeBranchName(ref);
const worktrees = await this.listWorktrees(git);
let wt: Worktree;
if (worktrees.has(workspaceBranch)) {
wt = worktrees.get(workspaceBranch)!;
} else {
workspace = await this.cloneWorkspace(bareRepo, repositoryUri, revision, targetCommit);
wt = await this.openWorktree(
git,
workspaceBranch,
await this.revisionDir(repositoryUri, ref),
targetRevision
);
}
if (!targetRevision.startsWith(wt.revision)) {
await this.setWorkspaceRevision(wt.path, targetRevision);
}
const { workspaceDir, workspaceHeadCommit } = workspace;
this.setWorkspaceRevision(workspaceDir, workspace.workspaceHeadCommit);
return {
workspaceDir,
workspaceRevision: workspaceHeadCommit.sha().substring(0, 7),
workspaceDir: wt.path,
workspaceRevision: targetRevision,
};
}
public async openWorktree(
git: SimpleGit,
workspaceBranch: string,
dir: string,
revision: string
) {
await git.raw(['worktree', 'add', '-b', workspaceBranch, dir, revision]);
return {
revision,
path: dir,
branch: workspaceBranch,
} as Worktree;
}
public async listWorkspaceFolders(repoUri: string) {
const workspaceDir = await this.workspaceDir(repoUri);
const git = await this.gitOps.openGit(repoUri);
const worktrees = await this.listWorktrees(git);
const isDir = (source: string) => fs.lstatSync(source).isDirectory();
try {
return fs
.readdirSync(workspaceDir)
.map(name => path.join(workspaceDir, name))
.filter(isDir);
} catch (error) {
if (error.code === 'ENOENT') {
this.log.debug('Cannot find workspace dirs');
return [];
} else {
throw error;
return [...worktrees.values()]
.filter(wt => wt.branch.startsWith('workspace'))
.map(wt => wt.path)
.filter(isDir);
}
public async listWorktrees(git: SimpleGit): Promise<Map<string, Worktree>> {
const str = await git.raw(['worktree', 'list']);
const regex = /(.*?)\s+([a-h0-9]+)\s+\[(.+)\]/gm;
let m;
const result: Map<string, Worktree> = new Map();
while ((m = regex.exec(str)) !== null) {
if (m.index === regex.lastIndex) {
regex.lastIndex++;
}
const [, p, revision, branch] = m;
result.set(branch, {
path: p,
revision,
branch,
});
}
return result;
}
public async clearWorkspace(repoUri: string) {
const git = await this.gitOps.openGit(repoUri);
const worktrees = await this.listWorktrees(git);
for (const wt of worktrees.values()) {
await git.raw(['worktree', 'remove', wt.path, '--force']);
await git.deleteLocalBranch(wt.branch);
}
const workspaceDir = await this.workspaceDir(repoUri);
await del([workspaceDir], { force: true });
}
@ -147,8 +180,8 @@ export class WorkspaceHandler {
if (filePath) {
request.documentUri = payload.textDocument.uri;
payload.textDocument.uri = request.resolvedFilePath = filePath;
request.workspacePath = workspacePath;
request.workspaceRevision = workspaceRevision;
request.workspacePath = workspacePath!;
request.workspaceRevision = workspaceRevision!;
}
break;
}
@ -337,26 +370,16 @@ export class WorkspaceHandler {
}
}
private async checkCommit(repository: Repository, commit: Commit) {
private async checkCommit(git: SimpleGit, targetRevision: string) {
// we only support headCommit now.
const headCommit = await repository.getHeadCommit();
if (headCommit.sha() !== commit.sha()) {
const headRevision = await git.revparse([HEAD]);
if (headRevision !== targetRevision) {
throw Boom.badRequest(`revision must be master.`);
}
}
private async workspaceExists(bareRepo: Repository, repositoryUri: string, revision: string) {
const workTreeName = this.workspaceWorktreeBranchName(revision);
const wt = this.getWorktree(bareRepo, workTreeName);
if (wt) {
const workspaceDir = await this.revisionDir(repositoryUri, revision);
return fs.existsSync(workspaceDir);
}
return false;
}
public async revisionDir(repositoryUri: string, revision: string) {
return path.join(await this.workspaceDir(repositoryUri), revision);
public async revisionDir(repositoryUri: string, ref: string) {
return path.join(await this.workspaceDir(repositoryUri), ref);
}
private async workspaceDir(repoUri: string) {
@ -374,81 +397,9 @@ export class WorkspaceHandler {
return `workspace-${branch}`;
}
private async updateWorkspace(repositoryUri: string, revision: string, targetCommit: Commit) {
const workspaceDir = await this.revisionDir(repositoryUri, revision);
const workspaceRepo = await Repository.open(workspaceDir);
const workspaceHeadCommit = await workspaceRepo.getHeadCommit();
if (workspaceHeadCommit.sha() !== targetCommit.sha()) {
const commit = await workspaceRepo.getCommit(targetCommit.sha());
this.log.info(`Checkout workspace ${workspaceDir} to ${targetCommit.sha()}`);
// @ts-ignore
const result = await Reset.reset(workspaceRepo, commit, Reset.TYPE.HARD, {});
if (result !== undefined && result !== GitError.CODE.OK) {
throw Boom.internal(`Reset workspace to commit ${targetCommit.sha()} failed.`);
}
}
workspaceRepo.cleanup();
return { workspaceHeadCommit, workspaceDir };
}
private async cloneWorkspace(
bareRepo: Repository,
repositoryUri: string,
revision: string,
targetCommit: Commit
) {
const workspaceDir = await this.revisionDir(repositoryUri, revision);
this.log.info(`Create workspace ${workspaceDir} from url ${bareRepo.path()}`);
const parentDir = path.dirname(workspaceDir);
// on windows, git clone will failed if parent folder is not exists;
await mkdirAsync(parentDir, { recursive: true });
const workTreeName = this.workspaceWorktreeBranchName(revision);
await this.pruneWorktree(bareRepo, workTreeName);
// Create the worktree and open it as Repository.
const wt = await Worktree.add(bareRepo, workTreeName, workspaceDir, { lock: 0, version: 1 });
// @ts-ignore
const workspaceRepo = await Repository.openFromWorktree(wt);
const workspaceHeadCommit = await workspaceRepo.getHeadCommit();
// when we start supporting multi-revision, targetCommit may not be head
if (workspaceHeadCommit.sha() !== targetCommit.sha()) {
const commit = await workspaceRepo.getCommit(targetCommit.sha());
this.log.info(`checkout ${workspaceRepo.workdir()} to commit ${targetCommit.sha()}`);
// @ts-ignore
const result = await Reset.reset(workspaceRepo, commit, Reset.TYPE.HARD, {});
if (result !== undefined && result !== GitError.CODE.OK) {
throw Boom.internal(`checkout workspace to commit ${targetCommit.sha()} failed.`);
}
}
workspaceRepo.cleanup();
return { workspaceHeadCommit, workspaceDir };
}
private async getWorktree(bareRepo: Repository, workTreeName: string) {
try {
const wt: Worktree = await Worktree.lookup(bareRepo, workTreeName);
return wt;
} catch (e) {
return null;
}
}
private async pruneWorktree(bareRepo: Repository, workTreeName: string) {
const wt = await this.getWorktree(bareRepo, workTreeName);
if (wt) {
wt.prune({ flags: 1 });
try {
// try delete the worktree branch
const ref = await bareRepo.getReference(`refs/heads/${workTreeName}`);
ref.delete();
} catch (e) {
// it doesn't matter if branch is not exists
}
}
}
private setWorkspaceRevision(workspaceDir: string, headCommit: Commit) {
const workspaceRelativePath = path.relative(this.workspacePath, workspaceDir);
this.revisionMap[workspaceRelativePath] = headCommit.sha().substring(0, 7);
private async setWorkspaceRevision(workspaceDir: string, revision: string) {
const git = simplegit(workspaceDir);
await git.reset(['--hard', revision]);
}
/**
@ -462,16 +413,15 @@ export class WorkspaceHandler {
*/
private async checkFile(repoUri: string, revision: string, filePath: string) {
try {
const commit = await this.gitOps.getCommit(repoUri, revision);
const entry = await commit.getEntry(filePath);
switch (entry.filemode()) {
case TreeEntry.FILEMODE.TREE:
case TreeEntry.FILEMODE.BLOB:
case TreeEntry.FILEMODE.EXECUTABLE:
return true;
default:
return false;
const git = await this.gitOps.openGit(repoUri);
const p = filePath.endsWith('/') ? filePath.slice(0, -1) : filePath;
const tree = await this.gitOps.readTree(git, revision, p);
if (tree.entries.length !== 1) {
return false;
}
const entry = tree.entries[0];
const type = GitOperations.mode2type(entry.mode);
return type === FileTreeItemType.File || type === FileTreeItemType.Directory;
} catch (e) {
// filePath may not exists
return false;

View file

@ -8,7 +8,6 @@ import crypto from 'crypto';
import * as _ from 'lodash';
import { CoreSetup } from 'src/core/server';
import { GitOperations } from './git_operations';
import { RepositoryIndexInitializerFactory, tryMigrateIndices } from './indexer';
import { Esqueue } from './lib/esqueue';
import { Logger } from './log';
@ -59,7 +58,6 @@ import { PluginSetupContract } from '../../../../plugins/code/server/index';
export class CodePlugin {
private isCodeNode = false;
private gitOps: GitOperations | null = null;
private queue: Esqueue | null = null;
private log: Logger;
private serverOptions: ServerOptions;
@ -140,7 +138,6 @@ export class CodePlugin {
repoConfigController
);
this.lspService = lspService;
this.gitOps = gitOps;
const { indexScheduler, updateScheduler, cloneWorker } = initWorkers(
server,
this.log,
@ -188,7 +185,6 @@ export class CodePlugin {
repoConfigController
);
this.lspService = lspService;
this.gitOps = gitOps;
const { indexScheduler, updateScheduler } = initWorkers(
server,
this.log,
@ -215,7 +211,6 @@ export class CodePlugin {
public async stop() {
if (this.isCodeNode) {
if (this.gitOps) await this.gitOps.cleanAllRepo();
if (this.indexScheduler) this.indexScheduler.stop();
if (this.updateScheduler) this.updateScheduler.stop();
if (this.queue) this.queue.destroy();

View file

@ -68,8 +68,7 @@ export class CloneWorker extends AbstractGitWorker {
const repoService = this.repoServiceFactory.newInstance(
this.serverOptions.repoPath,
this.serverOptions.credsPath,
this.log,
this.serverOptions.security.enableGitCertCheck
this.log
);
const repo = RepositoryUtils.buildRepository(url);

View file

@ -79,11 +79,7 @@ test('Execute delete job.', async () => {
esQueue as Esqueue,
log,
esClient as EsClient,
{
security: {
enableGitCertCheck: true,
},
} as ServerOptions,
{} as ServerOptions,
(gitOps as any) as GitOperations,
(cancellationService as any) as CancellationSerivce,
(lspService as any) as LspService,

View file

@ -77,10 +77,8 @@ export class DeleteWorker extends AbstractWorker {
const repoService = this.repoServiceFactory.newInstance(
this.serverOptions.repoPath,
this.serverOptions.credsPath,
this.log,
this.serverOptions.security.enableGitCertCheck
this.log
);
this.gitOps.cleanRepo(uri);
await this.deletePromiseWrapper(repoService.remove(uri), 'git data', uri);
// 4. Delete the document index and alias where the repository document and all status reside,

View file

@ -66,9 +66,6 @@ test('Execute update job', async () => {
log,
esClient as EsClient,
{
security: {
enableGitCertCheck: true,
},
disk: {
thresholdEnabled: true,
watermarkLow: '80%',
@ -112,9 +109,6 @@ test('On update job completed because of cancellation ', async () => {
log,
esClient as EsClient,
{
security: {
enableGitCertCheck: true,
},
disk: {
thresholdEnabled: true,
watermarkLow: '80%',
@ -187,9 +181,6 @@ test('Execute update job failed because of low available disk space', async () =
log,
esClient as EsClient,
{
security: {
enableGitCertCheck: true,
},
disk: {
thresholdEnabled: true,
watermarkLow: '80%',
@ -266,9 +257,6 @@ test('On update job error or timeout will not persist as error', async () => {
log,
esClient as EsClient,
{
security: {
enableGitCertCheck: true,
},
disk: {
thresholdEnabled: true,
watermarkLow: '80%',

View file

@ -43,8 +43,7 @@ export class UpdateWorker extends AbstractGitWorker {
const repoService = this.repoServiceFactory.newInstance(
this.serverOptions.repoPath,
this.serverOptions.credsPath,
this.log,
this.serverOptions.security.enableGitCertCheck
this.log
);
// Try to cancel any existing update job for this repository.

View file

@ -4,22 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Git, { RemoteCallbacks } from '@elastic/nodegit';
import { Progress } from '@elastic/simple-git/dist';
import del from 'del';
import fs from 'fs';
import { promisify } from 'util';
import moment from 'moment';
import path from 'path';
import { SimpleGit } from '@elastic/simple-git/dist/promise';
import { RepositoryUtils } from '../common/repository_utils';
import {
CloneProgress,
CloneWorkerResult,
DeleteWorkerResult,
Repository,
UpdateWorkerResult,
} from '../model';
import { CloneProgress, CloneWorkerResult, DeleteWorkerResult, Repository } from '../model';
import { Logger } from './log';
import { GitOperations } from './git_operations';
// Return false to stop the clone progress. Return true to keep going;
export type CloneProgressHandler = (
@ -46,12 +42,14 @@ function isCancelled(error: any) {
// This is the service for any kind of repository handling, e.g. clone, update, delete, etc.
export class RepositoryService {
private readonly gitOps: GitOperations;
constructor(
private readonly repoVolPath: string,
private readonly credsPath: string,
private readonly log: Logger,
private readonly enableGitCertCheck: boolean
) {}
private readonly log: Logger
) {
this.gitOps = new GitOperations(repoVolPath);
}
public async clone(repo: Repository, handler?: CloneProgressHandler): Promise<CloneWorkerResult> {
if (!repo) {
@ -62,30 +60,23 @@ export class RepositoryService {
this.log.info(`Repository exist in local path. Do update instead of clone.`);
try {
// Do update instead of clone if the local repo exists.
const updateRes = await this.update(repo);
return {
uri: repo.uri,
repo: {
...repo,
defaultBranch: updateRes.branch,
revision: updateRes.revision,
},
};
return await this.update(repo);
} catch (error) {
// If failed to update the current git repo living in the disk, clean up the local git repo and
// move on with the clone.
await this.remove(repo.uri);
}
} else {
const parentDir = path.dirname(localPath);
// on windows, git clone will failed if parent folder is not exists;
await mkdirAsync(parentDir, { recursive: true });
await mkdirAsync(localPath, { recursive: true });
}
// Go head with the actual clone.
const git = await this.gitOps.openGit(repo.uri, false);
await git.init(true);
await git.addRemote('origin', repo.url);
if (repo.protocol === 'ssh') {
return this.tryWithKeys(key => this.doClone(repo, localPath, handler, key));
return this.tryWithKeys(git, () => this.doFetch(git, repo, handler));
} else {
return await this.doClone(repo, localPath, handler);
return await this.doFetch(git, repo, handler);
}
}
}
@ -105,103 +96,21 @@ export class RepositoryService {
throw error;
}
}
public async update(
repo: Repository,
handler?: UpdateProgressHandler
): Promise<UpdateWorkerResult> {
public async update(repo: Repository, handler?: UpdateProgressHandler) {
const git = await this.gitOps.openGit(repo.uri);
if (repo.protocol === 'ssh') {
return await this.tryWithKeys(key => this.doUpdate(repo.uri, key, handler));
return await this.tryWithKeys(git, () => this.doFetch(git, repo, handler));
} else {
return await this.doUpdate(repo.uri, /* key */ undefined, handler);
}
}
public async doUpdate(
uri: string,
key?: string,
handler?: UpdateProgressHandler
): Promise<UpdateWorkerResult> {
const localPath = RepositoryUtils.repositoryLocalPath(this.repoVolPath, uri);
let repo: Git.Repository | undefined;
try {
repo = await Git.Repository.open(localPath);
let lastProgressUpdate = moment();
const cbs: RemoteCallbacks = {
transferProgress: async (_: any) => {
// Update progress update throttling.
const now = moment();
if (now.diff(lastProgressUpdate) < this.PROGRESS_UPDATE_THROTTLING_FREQ_MS) {
return 0;
}
lastProgressUpdate = now;
if (handler) {
const resumeUpdate = await handler();
if (!resumeUpdate) {
return GIT_FETCH_PROGRESS_CANCEL;
}
}
return 0;
},
credentials: this.credentialFunc(key),
};
// Ignore cert check on testing environment.
if (!this.enableGitCertCheck) {
cbs.certificateCheck = () => {
// Ignore cert check failures.
return 0;
};
}
await repo.fetchAll({
callbacks: cbs,
});
// TODO(mengwei): deal with the case when the default branch has changed.
const currentBranch = await repo.getCurrentBranch();
const currentBranchName = currentBranch.shorthand();
const originBranchName = `origin/${currentBranchName}`;
const originRef = await repo.getReference(originBranchName);
const headRef = await repo.getReference(currentBranchName);
if (!originRef.target().equal(headRef.target())) {
await headRef.setTarget(originRef.target(), 'update');
}
const headCommit = await repo.getHeadCommit();
this.log.debug(`Update repository to revision ${headCommit.sha()}`);
return {
uri,
branch: currentBranchName,
revision: headCommit.sha(),
};
} catch (error) {
if (isCancelled(error)) {
// Update job was cancelled intentionally. Do not throw this error.
this.log.info(`Update repository job for ${uri} was cancelled.`);
this.log.debug(
`Update repository job cancellation error: ${JSON.stringify(error, null, 2)}`
);
return {
uri,
branch: '',
revision: '',
cancelled: true,
};
} else if (error.message && error.message.startsWith(SSH_AUTH_ERROR.message)) {
throw SSH_AUTH_ERROR;
} else {
const msg = `update repository ${uri} error: ${error}`;
this.log.error(msg);
throw new Error(msg);
}
} finally {
if (repo) {
repo.cleanup();
}
return await this.doFetch(git, repo, handler);
}
}
/**
* read credentials dir, try using each privateKey until action is successful
* @param git
* @param action
*/
private async tryWithKeys<R>(action: (key: string) => Promise<R>): Promise<R> {
private async tryWithKeys<R>(git: SimpleGit, action: () => Promise<R>): Promise<R> {
const files = fs.existsSync(this.credsPath)
? new Set(fs.readdirSync(this.credsPath))
: new Set([]);
@ -211,7 +120,11 @@ export class RepositoryService {
if (files.has(privateKey)) {
try {
this.log.debug(`try with key ${privateKey}`);
return await action(privateKey);
await git.addConfig(
'core.sshCommand',
`ssh -i ${path.join(this.credsPath, privateKey)}`
);
return await action();
} catch (e) {
if (e !== SSH_AUTH_ERROR) {
throw e;
@ -225,82 +138,42 @@ export class RepositoryService {
}
private PROGRESS_UPDATE_THROTTLING_FREQ_MS = 1000;
private async doClone(
repo: Repository,
localPath: string,
handler?: CloneProgressHandler,
keyFile?: string
) {
private async doFetch(git: SimpleGit, repo: Repository, handler?: CloneProgressHandler) {
try {
let lastProgressUpdate = moment();
const cbs: RemoteCallbacks = {
transferProgress: async (stats: any) => {
// Clone progress update throttling.
const now = moment();
if (now.diff(lastProgressUpdate) < this.PROGRESS_UPDATE_THROTTLING_FREQ_MS) {
return 0;
}
lastProgressUpdate = now;
if (handler) {
const progress =
(100 * (stats.receivedObjects() + stats.indexedObjects())) /
(stats.totalObjects() * 2);
const cloneProgress = {
isCloned: false,
receivedObjects: stats.receivedObjects(),
indexedObjects: stats.indexedObjects(),
totalObjects: stats.totalObjects(),
localObjects: stats.localObjects(),
totalDeltas: stats.totalDeltas(),
indexedDeltas: stats.indexedDeltas(),
receivedBytes: stats.receivedBytes(),
};
const resumeClone = await handler(progress, cloneProgress);
if (!resumeClone) {
return GIT_FETCH_PROGRESS_CANCEL;
}
}
const progressCallback = async (progress: Progress) => {
const now = moment();
if (now.diff(lastProgressUpdate) < this.PROGRESS_UPDATE_THROTTLING_FREQ_MS) {
return 0;
},
credentials: this.credentialFunc(keyFile),
};
// Ignore cert check on testing environment.
if (!this.enableGitCertCheck) {
cbs.certificateCheck = () => {
// Ignore cert check failures.
return 0;
};
}
let gitRepo: Git.Repository | undefined;
try {
gitRepo = await Git.Clone.clone(repo.url, localPath, {
bare: 1,
fetchOpts: {
callbacks: cbs,
},
});
const headCommit = await gitRepo.getHeadCommit();
const headRevision = headCommit.sha();
const currentBranch = await gitRepo.getCurrentBranch();
const currentBranchName = currentBranch.shorthand();
this.log.info(
`Clone repository from ${repo.url} done with head revision ${headRevision} and default branch ${currentBranchName}`
);
return {
uri: repo.uri,
repo: {
...repo,
defaultBranch: currentBranchName,
revision: headRevision,
},
};
} finally {
if (gitRepo) {
gitRepo.cleanup();
}
}
lastProgressUpdate = now;
if (handler) {
const resumeClone = await handler(progress.percentage);
if (!resumeClone) {
return GIT_FETCH_PROGRESS_CANCEL;
}
}
};
await git.fetch(['origin'], undefined, {
progressCallback,
});
const currentBranchName = (await git.raw(['symbolic-ref', 'HEAD', '--short'])).trim();
const headRevision = await git.revparse([`origin/${currentBranchName}`]);
// Update master to match origin/master
await git.raw(['update-ref', `refs/heads/${currentBranchName}`, headRevision]);
this.log.info(
`Clone repository from ${repo.url} done with head revision ${headRevision} and default branch ${currentBranchName}`
);
return {
uri: repo.uri,
repo: {
...repo,
defaultBranch: currentBranchName,
revision: headRevision,
},
};
} catch (error) {
if (isCancelled(error)) {
// Clone job was cancelled intentionally. Do not throw this error.
@ -323,20 +196,4 @@ export class RepositoryService {
}
}
}
private credentialFunc(keyFile: string | undefined) {
return (url: string, userName: string) => {
if (keyFile) {
this.log.debug(`try with key ${path.join(this.credsPath, keyFile)}`);
return Git.Cred.sshKeyNew(
userName,
path.join(this.credsPath, `${keyFile}.pub`),
path.join(this.credsPath, keyFile),
''
);
} else {
return Git.Cred.defaultNew();
}
};
}
}

View file

@ -8,12 +8,7 @@ import { Logger } from './log';
import { RepositoryService } from './repository_service';
export class RepositoryServiceFactory {
public newInstance(
repoPath: string,
credsPath: string,
log: Logger,
enableGitCertCheck: boolean
): RepositoryService {
return new RepositoryService(repoPath, credsPath, log, enableGitCertCheck);
public newInstance(repoPath: string, credsPath: string, log: Logger): RepositoryService {
return new RepositoryService(repoPath, credsPath, log);
}
}

View file

@ -21,7 +21,6 @@ export interface SecurityOptions {
installNodeDependency: boolean;
gitHostWhitelist: string[];
gitProtocolWhitelist: string[];
enableGitCertCheck: boolean;
}
export interface DiskOptions {

View file

@ -9,11 +9,54 @@ import { Server } from 'hapi';
import * as os from 'os';
import path from 'path';
import { simplegit } from '@elastic/simple-git/dist';
import rimraf from 'rimraf';
import { AnyObject } from './lib/esqueue';
import { ServerOptions } from './server_options';
import { ServerFacade } from '..';
// TODO migrate other duplicate classes, functions
export function prepareProjectByCloning(url: string, p: string) {
return new Promise(resolve => {
if (!fs.existsSync(p)) {
rimraf(p, error => {
fs.mkdirSync(p, { recursive: true });
const git = simplegit(p);
git.clone(url, p, ['--bare']).then(resolve);
});
} else {
resolve();
}
});
}
export async function prepareProjectByInit(
repoPath: string,
commits: { [commitMessage: string]: { [path: string]: string } }
) {
if (!fs.existsSync(repoPath)) fs.mkdirSync(repoPath, { recursive: true });
const git = simplegit(repoPath);
await git.init();
await git.addConfig('user.email', 'test@test.com');
await git.addConfig('user.name', 'tester');
const results: string[] = [];
for (const [message, commit] of Object.entries(commits)) {
const files = [];
for (const [file, content] of Object.entries(commit)) {
const filePath = path.join(repoPath, file);
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(filePath, content, 'utf8');
files.push(file);
await git.add(file);
}
await git.commit(message, files);
const c = await git.revparse(['HEAD']);
results.push(c);
}
return { git, commits: results };
}
export const emptyAsyncFunc = async (_: AnyObject): Promise<any> => {
Promise.resolve({});

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { set } from 'lodash';
export interface Format {
[field: string]: string | Format;
}
export interface Field {
name: string;
path: string;
format: string;
}
const BOUNDARY = 'ª––––––––º';
const SPLITTER = '≤∞≥';
export class FormatParser {
private fields: Field[];
constructor(readonly format: Format) {
this.fields = [];
this.toFields(this.fields, format);
}
private toFields(fields: Field[], format: Format, prefix: string = '') {
Object.entries(format).forEach(entry => {
const [key, value] = entry;
if (typeof value === 'string') {
fields.push({
name: key,
path: `${prefix}${key}`,
format: value,
});
} else {
this.toFields(fields, value, `${prefix}${key}.`);
}
});
}
public toFormatStr(): string {
return this.fields.map(f => f.format).join(SPLITTER) + BOUNDARY;
}
public parseResult(result: string): any[] {
return result
.split(BOUNDARY)
.map(item => item.trim().split(SPLITTER))
.filter(items => items.length > 0)
.map(items => this.toObject(items));
}
private toObject(items: string[]) {
const result = {};
this.fields.forEach((f, i) => {
set(result, f.path, items[i]);
});
return result;
}
}

View file

@ -1,215 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// eslint-disable-next-line max-classes-per-file
import { Object, OdbObject, Oid, Repository, Signature, TreeEntry as TE } from '@elastic/nodegit';
import * as isogit from 'isomorphic-git';
import { CommitDescription, TagDescription, TreeDescription, TreeEntry } from 'isomorphic-git';
import * as fs from 'fs';
interface GitObjectParam {
gitdir: string;
oid: string;
format?: string | undefined;
filepath?: string | undefined;
}
interface GitRefParam {
gitdir: string;
ref: string;
depth: number;
}
isogit.plugins.set('fs', fs);
export interface BlobDescription {
isBinary(): boolean;
content(): Buffer;
rawsize(): number;
}
export interface GitObjectDescription {
oid: string;
type: 'blob' | 'tree' | 'commit' | 'tag';
format: 'content' | 'parsed';
object: BlobDescription | CommitDescription | TreeDescription | TagDescription | null;
}
export class GitPrime {
public static async readObject({
format = 'parsed',
gitdir,
oid,
filepath,
}: GitObjectParam): Promise<GitObjectDescription> {
const repo = await Repository.openBare(gitdir);
const odb = await repo.odb();
const o = await odb.read(Oid.fromString(oid));
const obj = new WrappedObj(o, repo);
if (obj.type === 'commit' && filepath) {
const commit = await repo.getCommit(o.id());
const entry = await commit.getEntry(filepath);
return GitPrime.readObject({ oid: entry.oid(), gitdir, format });
}
if (format === 'parsed' || format === 'content') {
await obj.parse();
}
return obj as GitObjectDescription;
}
public static async resolveRef({ gitdir, ref, depth }: GitRefParam) {
return await isogit.resolveRef({ gitdir, ref, depth });
}
public static async expandOid({ gitdir, oid }: { gitdir: string; oid: string }) {
const repo = await Repository.openBare(gitdir);
const o = await Object.lookupPrefix(repo, Oid.fromString(oid), oid.length, Object.TYPE.COMMIT);
return o.id().tostrS();
}
static async listBranches(param: { gitdir: string; remote: string }) {
return await isogit.listBranches(param);
}
static async listTags(param: { gitdir: string }) {
return await isogit.listTags(param);
}
static async isTextFile({ gitdir, oid }: { gitdir: string; oid: string }) {
const repo = await Repository.openBare(gitdir);
const blob = await repo.getBlob(oid);
return blob.isBinary() === 0;
}
}
class WrappedObj implements GitObjectDescription {
_format: 'content' | 'parsed' = 'content';
_object: CommitDescription | TreeDescription | TagDescription | BlobDescription | null;
constructor(private readonly o: OdbObject, private readonly repo: Repository) {
this._object = null;
}
public get object() {
return this._object;
}
public get format(): 'content' | 'parsed' {
return this._format;
}
public get oid(): string {
return this.o.id().tostrS();
}
public get type(): 'blob' | 'tree' | 'commit' | 'tag' {
return type2str(this.o.type());
}
async parse() {
function fromSignature(sign: Signature) {
return {
name: sign.name(),
email: sign.email(),
timestamp: sign.when().time,
timezoneOffset: sign.when().offset,
};
}
switch (this.o.type()) {
case 1:
const commit = await this.repo.getCommit(this.o.id());
this._object = {
tree: commit.treeId().tostrS(),
author: fromSignature(commit.author()),
message: commit.message(),
committer: fromSignature(commit.committer()),
parent: commit.parents().map(o => o.tostrS()),
} as CommitDescription;
break;
case 2:
const tree = await this.repo.getTree(this.o.id());
const entries = tree.entries().map(convertEntry);
this._object = {
entries,
} as TreeDescription;
break;
case 3:
const blob = await this.repo.getBlob(this.o.id());
this._object = {
content() {
return blob.content();
},
isBinary() {
return blob.isBinary() === 1;
},
rawsize() {
return blob.rawsize();
},
} as BlobDescription;
break;
case 4:
const tag = await this.repo.getTag(this.o.id());
this._object = {
message: tag.message(),
object: tag.targetId().tostrS(),
tagger: fromSignature(tag.tagger()),
tag: tag.name(),
type: type2str(tag.targetType()),
} as TagDescription;
break;
default:
throw new Error('invalid object type ' + this.o.type());
}
this._format = 'parsed';
}
}
function convertEntry(t: TE): TreeEntry {
let mode: string;
switch (t.filemode()) {
case TE.FILEMODE.EXECUTABLE:
mode = '100755';
break;
case TE.FILEMODE.BLOB:
mode = '100644';
break;
case TE.FILEMODE.COMMIT:
mode = '160000';
break;
case TE.FILEMODE.TREE:
mode = '040000';
break;
case TE.FILEMODE.LINK:
mode = '120000';
break;
default:
throw new Error('invalid file mode.');
}
return {
mode,
path: t.path(),
oid: t.sha(),
type: type2str(t.type()),
} as TreeEntry;
}
function type2str(t: number) {
switch (t) {
case 1:
return 'commit';
case 2:
return 'tree';
case 3:
return 'blob';
case 4:
return 'tag';
default:
throw new Error('invalid object type ' + t);
}
}
export { TreeEntry, CommitDescription, TreeDescription, TagDescription };

View file

@ -1,20 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
/* eslint-disable @typescript-eslint/camelcase */
// @ts-ignore
import binary_info from '@elastic/nodegit/dist/utils/binary_info';
export function binaryInfo(platform: string, arch: string) {
const info = binary_info(platform, arch);
const downloadUrl = info.hosted_tarball;
const packageName = info.package_name;
return {
downloadUrl,
packageName,
};
}

View file

@ -195,9 +195,9 @@
"@elastic/lsp-extension": "^0.1.2",
"@elastic/maki": "6.1.0",
"@elastic/node-crypto": "^1.0.0",
"@elastic/nodegit": "0.25.0-alpha.23",
"@elastic/numeral": "2.3.3",
"@elastic/request-crypto": "^1.0.2",
"@elastic/simple-git": "1.124.0-elastic.15",
"@kbn/babel-preset": "1.0.0",
"@kbn/config-schema": "1.0.0",
"@kbn/elastic-idx": "1.0.0",
@ -272,7 +272,6 @@
"intl": "^1.2.5",
"io-ts": "^2.0.1",
"isbinaryfile": "4.0.2",
"isomorphic-git": "0.55.5",
"joi": "^13.5.2",
"jquery": "^3.4.1",
"js-yaml": "3.13.1",

214
yarn.lock
View file

@ -1268,22 +1268,6 @@
resolved "https://registry.yarnpkg.com/@elastic/node-ctags/-/node-ctags-1.0.2.tgz#447d7694a5598f9413fe2b6f356d56f64f612dfd"
integrity sha512-EHhJ0NPlCvYy+gbzBMU4/Z/55hftfdwlAG8JwOy7g0ITmH6rFPanEnzg1WL3/L+pp8OlYHyvDLwmyg0+06y8LQ==
"@elastic/nodegit@0.25.0-alpha.23":
version "0.25.0-alpha.23"
resolved "https://registry.yarnpkg.com/@elastic/nodegit/-/nodegit-0.25.0-alpha.23.tgz#34d06497030a45a3b8090c3fcb9504d722b9d0c9"
integrity sha512-2zSLGigYW58g+DbcRtbCwwj6boTGNhyCG85IDK8yx/jlycLS6gnmxEbZ6FFfgdS2qKWi911lmKQ/3l0LpUaDRw==
dependencies:
fs-extra "^7.0.0"
json5 "^2.1.0"
lodash "^4.17.11"
nan "^2.11.1"
node-gyp "^3.8.0"
node-pre-gyp "^0.11.0"
promisify-node "~0.3.0"
ramda "^0.25.0"
request-promise-native "^1.0.5"
tar-fs "^1.16.3"
"@elastic/numeral@2.3.3":
version "2.3.3"
resolved "https://registry.yarnpkg.com/@elastic/numeral/-/numeral-2.3.3.tgz#94d38a35bd315efa7a6918b22695128fc40a885e"
@ -1298,6 +1282,15 @@
"@types/node-jose" "1.1.0"
node-jose "1.1.0"
"@elastic/simple-git@1.124.0-elastic.15":
version "1.124.0-elastic.15"
resolved "https://registry.yarnpkg.com/@elastic/simple-git/-/simple-git-1.124.0-elastic.15.tgz#b7975e9d1aeb424c87c817e1bd0549d25ec1d2c5"
integrity sha512-Nb+WwJI9I2PN72Ue6stMUDVlqrNKuskpfKJwd+74YtyyfuV7dfHvt4FksqrjnOLbvFxJi214DZ1dHoROg3iQyQ==
dependencies:
debug "^4.0.1"
request "^2.88.0"
targz "^1.0.1"
"@elastic/ui-ace@0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@elastic/ui-ace/-/ui-ace-0.2.3.tgz#5281aed47a79b7216c55542b0675e435692f20cd"
@ -5686,11 +5679,6 @@ async-limiter@~1.0.0:
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==
async-lock@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.2.0.tgz#cd6a53cb1ec3f86af25eafdeb6bc7c6e317258b8"
integrity sha512-81HzTQm4+qMj6PwNlnR+y9g7pDdGGzd/YBUrQnHk+BhR28ja2qv497NkQQc1KcKEqh/RShm07di2b0cIWVFrNQ==
async-retry@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.2.3.tgz#a6521f338358d322b1a0012b79030c6f411d1ce0"
@ -6462,11 +6450,6 @@ base64-arraybuffer@0.1.5:
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg=
base64-js@0.0.2, base64-js@^1.2.1, base64-js@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
base64-js@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-0.0.8.tgz#1101e9544f4a76b1bc3b26d452ca96d7a35e7978"
@ -6477,6 +6460,11 @@ base64-js@^1.0.2, base64-js@^1.1.2:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886"
integrity sha512-dwVUVIXsBZXwTuwnXI9RK8sBmgq09NDHzyR9SAph9eqk76gKK2JSQmZARC2zRC81JC2QTtxD0ARU5qTS25gIGw==
base64-js@^1.2.1, base64-js@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
base64-js@^1.2.3:
version "1.3.0"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
@ -6767,14 +6755,6 @@ boom@7.x.x, boom@^7.1.0, boom@^7.2.0:
dependencies:
hoek "6.x.x"
bops@~0.0.6:
version "0.0.7"
resolved "https://registry.yarnpkg.com/bops/-/bops-0.0.7.tgz#b4a0a5a839a406454af0fe05a8b91a7a766a54e2"
integrity sha1-tKClqDmkBkVK8P4FqLkaenZqVOI=
dependencies:
base64-js "0.0.2"
to-utf8 "0.0.1"
bottleneck@^2.15.3:
version "2.18.0"
resolved "https://registry.yarnpkg.com/bottleneck/-/bottleneck-2.18.0.tgz#41fa63ae185b65435d789d1700334bc48222dacf"
@ -7877,11 +7857,6 @@ clean-css@4.2.x, clean-css@^4.1.11:
dependencies:
source-map "~0.6.0"
clean-git-ref@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/clean-git-ref/-/clean-git-ref-1.0.3.tgz#5325dc839eab01c974ae0e97f5734782750f88ec"
integrity sha1-UyXcg56rAcl0rg6X9XNHgnUPiOw=
clean-stack@^1.0.0, clean-stack@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-1.3.0.tgz#9e821501ae979986c46b1d66d2d432db2fd4ae31"
@ -8817,14 +8792,6 @@ cpy@^7.3.0:
globby "^9.2.0"
nested-error-stacks "^2.1.0"
crc-32@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208"
integrity sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==
dependencies:
exit-on-epipe "~1.0.1"
printj "~1.1.0"
crc32-stream@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-3.0.1.tgz#cae6eeed003b0e44d739d279de5ae63b171b4e85"
@ -10186,13 +10153,6 @@ diagnostics@^1.1.1:
enabled "1.0.x"
kuler "1.0.x"
diff-lines@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/diff-lines/-/diff-lines-1.1.1.tgz#4f10a709b6ce2af1d6b412ada5a90cf01d782571"
integrity sha512-Oo5JzEEriF/+T0usOeRP5yOzr6SWvni2rrxvIgijMZSxPcEvf8JOvCO5GpnWwkte7fcOgnue/f5ECg1H9lMPCw==
dependencies:
diff "^3.5.0"
diff-sequences@^24.0.0:
version "24.0.0"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.0.0.tgz#cdf8e27ed20d8b8d3caccb4e0c0d8fe31a173013"
@ -11753,11 +11713,6 @@ exit-hook@^2.2.0:
resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-2.2.0.tgz#f5502f92179018e867f2d8ee4428392da7f3894e"
integrity sha512-YFH+2oGdldRH5GqGpnaiKbBxWHMmuXHmKTMtUC58kWSOrnTf95rKITVSFTTtas14DWvWpih429+ffAvFetPwNA==
exit-on-epipe@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692"
integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==
exit@^0.1.2, exit@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
@ -13288,14 +13243,6 @@ gh-got@^5.0.0:
got "^6.2.0"
is-plain-obj "^1.1.0"
git-apply-delta@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/git-apply-delta/-/git-apply-delta-0.0.7.tgz#fb76ae144540d79440b52b31de03e63c993c7219"
integrity sha1-+3auFEVA15RAtSsx3gPmPJk8chk=
dependencies:
bops "~0.0.6"
varint "0.0.3"
git-clone@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/git-clone/-/git-clone-0.1.0.tgz#0d76163778093aef7f1c30238f2a9ef3f07a2eb9"
@ -13544,11 +13491,6 @@ globals@^9.18.0, globals@^9.2.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==
globalyzer@^0.1.0:
version "0.1.4"
resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.4.tgz#bc8e273afe1ac7c24eea8def5b802340c5cc534f"
integrity sha512-LeguVWaxgHN0MNbWC6YljNMzHkrCny9fzjmEUdnF1kQ7wATFD1RHFRqA1qxaX2tgxGENlcxjOflopBwj3YZiXA==
globby@8.0.2:
version "8.0.2"
resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.2.tgz#5697619ccd95c5275dbb2d6faa42087c1a941d8d"
@ -13650,11 +13592,6 @@ globby@^9.1.0, globby@^9.2.0:
pify "^4.0.1"
slash "^2.0.0"
globrex@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098"
integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==
globule@^1.0.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.1.tgz#5dffb1b191f22d20797a9369b49eab4e9839696d"
@ -15140,7 +15077,7 @@ ignore@^4.0.3, ignore@^4.0.6:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
ignore@^5.0.4, ignore@^5.1.1:
ignore@^5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.2.tgz#e28e584d43ad7e92f96995019cc43b9e1ac49558"
integrity sha512-vdqWBp7MyzdmHkkRWV5nY+PfGRbYbahfuvsBCh277tq+w9zyNi7h5CYJCK0kmzti9kU+O/cB7sE8HvKv6aXAKQ==
@ -16349,27 +16286,6 @@ isomorphic-fetch@^2.1.1:
node-fetch "^1.0.1"
whatwg-fetch ">=0.10.0"
isomorphic-git@0.55.5:
version "0.55.5"
resolved "https://registry.yarnpkg.com/isomorphic-git/-/isomorphic-git-0.55.5.tgz#d232dddefa9d271470b9b496c14aa65aa98f2519"
integrity sha512-qqFID4DYGjnMkxDowXbp5GUdId6SJCbOEZFRdodMgrk26hjeOiNiOpGlYOX4rBe2mJAsE7Bdj30EQAQJtE72DA==
dependencies:
async-lock "^1.1.0"
clean-git-ref "1.0.3"
crc-32 "^1.2.0"
diff-lines "^1.1.1"
git-apply-delta "0.0.7"
globalyzer "^0.1.0"
globrex "^0.1.2"
ignore "^5.0.4"
marky "^1.2.1"
minimisted "^2.0.0"
pako "^1.0.10"
pify "^4.0.1"
readable-stream "^3.1.1"
sha.js "^2.4.9"
simple-get "^3.0.2"
isstream@0.1.x, isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
@ -18819,11 +18735,6 @@ marksy@^7.0.0:
he "^1.1.1"
marked "^0.6.2"
marky@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.1.tgz#a3fcf82ffd357756b8b8affec9fdbf3a30dc1b02"
integrity sha512-md9k+Gxa3qLH6sUKpeC2CNkJK/Ld+bEz5X96nYwloqphQE0CKCVEKco/6jxEZixinqNdz5RFi/KaCyfbMDMAXQ==
matchdep@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-2.0.0.tgz#c6f34834a0d8dbc3b37c27ee8bbcb27c7775582e"
@ -19282,13 +19193,6 @@ minimist@~0.0.1:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=
minimisted@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/minimisted/-/minimisted-2.0.0.tgz#5e3295e74ed701b1cbeaa863a888181d6efbe8ce"
integrity sha512-oP88Dw3LK/pdrKyMdlbmg3W50969UNr4ctISzJfPl+YPYHTAOrS+dihXnsgRNKSRIzDsrnV3eE2CCVlZbpOKdQ==
dependencies:
minimist "^1.2.0"
minimost@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/minimost/-/minimost-1.0.0.tgz#1d07954aa0268873408b95552fbffc5977dfc78b"
@ -19732,11 +19636,6 @@ nan@^2.10.0, nan@^2.9.2:
resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==
nan@^2.11.1:
version "2.12.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552"
integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==
nanomatch@^1.2.5:
version "1.2.7"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.7.tgz#53cd4aa109ff68b7f869591fdc9d10daeeea3e79"
@ -20102,22 +20001,6 @@ node-pre-gyp@^0.10.0:
semver "^5.3.0"
tar "^4"
node-pre-gyp@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz#db1f33215272f692cd38f03238e3e9b47c5dd054"
integrity sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==
dependencies:
detect-libc "^1.0.2"
mkdirp "^0.5.1"
needle "^2.2.1"
nopt "^4.0.1"
npm-packlist "^1.1.6"
npmlog "^4.0.2"
rc "^1.2.7"
rimraf "^2.6.1"
semver "^5.3.0"
tar "^4"
node-releases@^1.1.25:
version "1.1.25"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.25.tgz#0c2d7dbc7fed30fbe02a9ee3007b8c90bf0133d3"
@ -20162,13 +20045,6 @@ node-status-codes@^1.0.0:
resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f"
integrity sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=
nodegit-promise@~4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/nodegit-promise/-/nodegit-promise-4.0.0.tgz#5722b184f2df7327161064a791d2e842c9167b34"
integrity sha1-VyKxhPLfcycWEGSnkdLoQskWezQ=
dependencies:
asap "~2.0.3"
nodemailer@^4.7.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-4.7.0.tgz#4420e06abfffd77d0618f184ea49047db84f4ad8"
@ -21115,7 +20991,7 @@ pako@^0.2.5:
resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
integrity sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=
pako@^1.0.10, pako@^1.0.5, pako@~1.0.2:
pako@^1.0.5, pako@~1.0.2:
version "1.0.10"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732"
integrity sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==
@ -22022,11 +21898,6 @@ pretty-ms@^4.0.0:
dependencies:
parse-ms "^2.0.0"
printj@~1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==
prismjs@^1.8.4, prismjs@~1.16.0:
version "1.16.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.16.0.tgz#406eb2c8aacb0f5f0f1167930cb83835d10a4308"
@ -22095,13 +21966,6 @@ promise@^7.0.1, promise@^7.1.1:
dependencies:
asap "~2.0.3"
promisify-node@~0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/promisify-node/-/promisify-node-0.3.0.tgz#b4b55acf90faa7d2b8b90ca396899086c03060cf"
integrity sha1-tLVaz5D6p9K4uQyjlomQhsAwYM8=
dependencies:
nodegit-promise "~4.0.0"
prompts@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.0.3.tgz#c5ccb324010b2e8f74752aadceeb57134c1d2522"
@ -22575,11 +22439,6 @@ ramda@^0.21.0:
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.21.0.tgz#a001abedb3ff61077d4ff1d577d44de77e8d0a35"
integrity sha1-oAGr7bP/YQd9T/HVd9RN536NCjU=
ramda@^0.25.0:
version "0.25.0"
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.25.0.tgz#8fdf68231cffa90bc2f9460390a0cb74a29b29a9"
integrity sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ==
ramda@^0.26, ramda@^0.26.1:
version "0.26.1"
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06"
@ -25313,14 +25172,6 @@ sha.js@^2.4.0, sha.js@^2.4.8:
inherits "^2.0.1"
safe-buffer "^5.0.1"
sha.js@^2.4.9:
version "2.4.11"
resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
dependencies:
inherits "^2.0.1"
safe-buffer "^5.0.1"
shallow-clone@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060"
@ -25428,20 +25279,6 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
simple-concat@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6"
integrity sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=
simple-get@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.0.3.tgz#924528ac3f9d7718ce5e9ec1b1a69c0be4d62efa"
integrity sha512-Wvre/Jq5vgoz31Z9stYWPLn0PqRqmBDpFSdypAnHu5AvRVCYPRYGnvryNLiXu8GOBNDH82J2FRHUGMjjHUpXFw==
dependencies:
decompress-response "^3.3.0"
once "^1.3.1"
simple-concat "^1.0.0"
simple-git@1.116.0:
version "1.116.0"
resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-1.116.0.tgz#ea6e533466f1e0152186e306e004d4eefa6e3e00"
@ -26795,7 +26632,7 @@ tape@^4.5.1:
string.prototype.trim "~1.1.2"
through "~2.3.8"
tar-fs@^1.16.3:
tar-fs@^1.16.3, tar-fs@^1.8.1:
version "1.16.3"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.3.tgz#966a628841da2c4010406a82167cbd5e0c72d509"
integrity sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==
@ -26874,6 +26711,13 @@ tar@^4:
safe-buffer "^5.1.2"
yallist "^3.0.2"
targz@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/targz/-/targz-1.0.1.tgz#8f76a523694cdedfbb5d60a4076ff6eeecc5398f"
integrity sha1-j3alI2lM3t+7XWCkB2/27uzFOY8=
dependencies:
tar-fs "^1.8.1"
tcomb-validation@^3.3.0:
version "3.4.1"
resolved "https://registry.yarnpkg.com/tcomb-validation/-/tcomb-validation-3.4.1.tgz#a7696ec176ce56a081d9e019f8b732a5a8894b65"
@ -27330,11 +27174,6 @@ to-through@^2.0.0:
dependencies:
through2 "^2.0.3"
to-utf8@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/to-utf8/-/to-utf8-0.0.1.tgz#d17aea72ff2fba39b9e43601be7b3ff72e089852"
integrity sha1-0Xrqcv8vujm55DYBvns/9y4ImFI=
toggle-selection@^1.0.3:
version "1.0.6"
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
@ -28750,11 +28589,6 @@ value-or-function@^3.0.0:
resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813"
integrity sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM=
varint@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/varint/-/varint-0.0.3.tgz#b821de9b04b38b3cd22f72c18d94a9fb72ab3518"
integrity sha1-uCHemwSzizzSL3LBjZSp+3KrNRg=
vary@^1, vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"