From 92e70ca7c0c99171f15258dadaaa799d34c246d1 Mon Sep 17 00:00:00 2001 From: Yulong Date: Fri, 10 May 2019 22:07:09 +0800 Subject: [PATCH] [Code] try prune a worktree if it already exists (#36338) (#36422) --- x-pack/package.json | 2 +- .../code/server/__tests__/lsp_service.ts | 151 +++++++++++++----- .../code/server/lsp/workspace_handler.ts | 73 ++++++--- yarn.lock | 8 +- 4 files changed, 166 insertions(+), 68 deletions(-) diff --git a/x-pack/package.json b/x-pack/package.json index f82b80362000e..24dd4283c3ca0 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -169,7 +169,7 @@ "@elastic/javascript-typescript-langserver": "^0.1.23", "@elastic/lsp-extension": "^0.1.1", "@elastic/node-crypto": "^1.0.0", - "@elastic/nodegit": "0.25.0-alpha.19", + "@elastic/nodegit": "0.25.0-alpha.20", "@elastic/numeral": "2.3.3", "@kbn/babel-preset": "1.0.0", "@kbn/elastic-idx": "1.0.0", diff --git a/x-pack/plugins/code/server/__tests__/lsp_service.ts b/x-pack/plugins/code/server/__tests__/lsp_service.ts index c73107b4f5d1c..8819c429ede85 100644 --- a/x-pack/plugins/code/server/__tests__/lsp_service.ts +++ b/x-pack/plugins/code/server/__tests__/lsp_service.ts @@ -8,7 +8,7 @@ import Git from '@elastic/nodegit'; import fs from 'fs'; import mkdirp from 'mkdirp'; import path from 'path'; -// import rimraf from 'rimraf'; +import rimraf from 'rimraf'; import sinon from 'sinon'; import assert from 'assert'; @@ -39,15 +39,16 @@ const packagejson = ` } `; describe('lsp_service tests', () => { + let firstCommitSha = ''; + let secondCommitSha = ''; async function prepareProject(repoPath: string) { mkdirp.sync(repoPath); const repo = await Git.Repository.init(repoPath, 0); const helloContent = "console.log('hello world');"; fs.writeFileSync(path.join(repo.workdir(), filename), helloContent, 'utf8'); - fs.writeFileSync(path.join(repo.workdir(), 'package.json'), packagejson, 'utf8'); - const index = await repo.refreshIndex(); + + let index = await repo.refreshIndex(); await index.addByPath(filename); - await index.addByPath('package.json'); index.write(); const treeId = await index.writeTree(); const committer = Git.Signature.create('tester', 'test@test.com', Date.now() / 1000, 60); @@ -59,8 +60,22 @@ describe('lsp_service tests', () => { treeId, [] ); - // eslint-disable-next-line no-console - console.log(`created commit ${commit.tostrS()}`); + 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; } @@ -109,11 +124,9 @@ describe('lsp_service tests', () => { return path.resolve(pa) === path.resolve(pb); } - it('process a hover request', async () => { + function mockLspService() { const esClient = mockEsClient(); - const revision = 'master'; - - const lspservice = new LspService( + return new LspService( '127.0.0.1', serverOptions, esClient, @@ -121,24 +134,34 @@ describe('lsp_service tests', () => { new ConsoleLoggerFactory(), new RepositoryConfigController(esClient) ); + } + + async function sendHoverRequest(lspservice: LspService, revision: string) { + const method = 'textDocument/hover'; + + const params = { + textDocument: { + uri: `git://${repoUri}/blob/${revision}/${filename}`, + }, + position: { + line: 0, + character: 1, + }, + }; + return await lspservice.sendRequest(method, params); + } + + it('process a hover request', async () => { + const lspservice = mockLspService(); try { - const params = { - textDocument: { - uri: `git://${repoUri}/blob/${revision}/${filename}`, - }, - position: { - line: 0, - character: 1, - }, - }; const workspaceHandler = lspservice.workspaceHandler; const wsSpy = sinon.spy(workspaceHandler, 'handleRequest'); const controller = lspservice.controller; const ctrlSpy = sinon.spy(controller, 'handleRequest'); - const method = 'textDocument/hover'; + const revision = 'master'; - const response = await lspservice.sendRequest(method, params); + const response = await sendHoverRequest(lspservice, revision); assert.ok(response); assert.ok(response.result.contents); @@ -172,30 +195,11 @@ describe('lsp_service tests', () => { }).timeout(10000); it('unload a workspace', async () => { - const esClient = mockEsClient(); - const revision = 'master'; - const lspservice = new LspService( - '127.0.0.1', - serverOptions, - esClient, - installManager, - new ConsoleLoggerFactory(), - new RepositoryConfigController(esClient) - ); + const lspservice = mockLspService(); try { - const params = { - textDocument: { - uri: `git://${repoUri}/blob/${revision}/${filename}`, - }, - position: { - line: 0, - character: 1, - }, - }; - - const method = 'textDocument/hover'; + const revision = 'master'; // send a dummy request to open a workspace; - const response = await lspservice.sendRequest(method, params); + const response = await sendHoverRequest(lspservice, revision); assert.ok(response); const workspacePath = path.resolve(serverOptions.workspacePath, repoUri, revision); const workspaceFolderExists = fs.existsSync(workspacePath); @@ -228,4 +232,65 @@ describe('lsp_service tests', () => { } // @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(10000); + + it('should update if a worktree is not the newest', 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 workspaceRepo = await Git.Repository.open(workspacePath); + // workspace is newest now + assert.strictEqual((await workspaceRepo.getHeadCommit()).sha(), secondCommitSha); + const firstCommit = await workspaceRepo.getCommit(firstCommitSha); + // 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); + + // send a request again; + await sendHoverRequest(lspservice, revision); + // workspace_handler should update workspace to the newest one + assert.strictEqual((await workspaceRepo.getHeadCommit()).sha(), secondCommitSha); + return; + } finally { + await lspservice.shutdown(); + } + // @ts-ignore + }).timeout(10000); }); diff --git a/x-pack/plugins/code/server/lsp/workspace_handler.ts b/x-pack/plugins/code/server/lsp/workspace_handler.ts index 05084a81f5120..4003860cfa931 100644 --- a/x-pack/plugins/code/server/lsp/workspace_handler.ts +++ b/x-pack/plugins/code/server/lsp/workspace_handler.ts @@ -93,10 +93,10 @@ export class WorkspaceHandler { revision = defaultBranch; } let workspaceRepo: Repository; - if (await this.workspaceExists(repositoryUri, revision)) { + if (await this.workspaceExists(bareRepo, repositoryUri, revision)) { workspaceRepo = await this.updateWorkspace(repositoryUri, revision, targetCommit); } else { - workspaceRepo = await this.cloneWorkspace(bareRepo, repositoryUri, revision); + workspaceRepo = await this.cloneWorkspace(bareRepo, repositoryUri, revision, targetCommit); } const workspaceHeadCommit = await workspaceRepo.getHeadCommit(); @@ -122,13 +122,9 @@ export class WorkspaceHandler { .filter(isDir); } - public async clearWorkspace(repoUri: string, revision?: string) { + public async clearWorkspace(repoUri: string) { const workspaceDir = await this.workspaceDir(repoUri); - if (revision) { - await del([await this.revisionDir(repoUri, revision)], { force: true }); - } else { - await del([workspaceDir], { force: true }); - } + await del([workspaceDir], { force: true }); } public async handleRequest(request: LspRequest): Promise { @@ -341,9 +337,14 @@ export class WorkspaceHandler { } } - private async workspaceExists(repositoryUri: string, revision: string) { - const workspaceDir = await this.revisionDir(repositoryUri, revision); - return fs.existsSync(workspaceDir); + 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; } private async revisionDir(repositoryUri: string, revision: string) { @@ -388,12 +389,12 @@ export class WorkspaceHandler { private async cloneWorkspace( bareRepo: Repository, repositoryUri: string, - revision: string + revision: string, + targetCommit: Commit ): Promise { const workspaceDir = await this.revisionDir(repositoryUri, revision); this.log.info(`Create workspace ${workspaceDir} from url ${bareRepo.path()}`); const parentDir = path.dirname(workspaceDir); - const mainBranchName = path.basename(workspaceDir); // on windows, git clone will failed if parent folder is not exists; await new Promise((resolve, reject) => mkdirp(parentDir, err => { @@ -404,15 +405,47 @@ export class WorkspaceHandler { } }) ); + const workTreeName = this.workspaceWorktreeBranchName(revision); + await this.pruneWorktree(bareRepo, workTreeName); // Create the worktree and open it as Repository. - const wt = await Worktree.add( - bareRepo, - this.workspaceWorktreeBranchName(mainBranchName), - workspaceDir, - {} - ); + const wt = await Worktree.add(bareRepo, workTreeName, workspaceDir, { lock: 0, version: 1 }); // @ts-ignore - return await Repository.openFromWorktree(wt); + 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.`); + } + } + return workspaceRepo; + } + + 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(workspaceRepo: Repository, headCommit: Commit) { diff --git a/yarn.lock b/yarn.lock index 764595160f51f..74434e031ad1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1478,10 +1478,10 @@ resolved "https://registry.yarnpkg.com/@elastic/node-crypto/-/node-crypto-1.0.0.tgz#4d325df333fe1319556bb4d54214098ada1171d4" integrity sha512-bbjbEyILPRTRt0xnda18OttLtlkJBPuXx3CjISUSn9jhWqHoFMzfOaZ73D5jxZE2SaFZUrJYfPpqXP6qqPufAQ== -"@elastic/nodegit@0.25.0-alpha.19": - version "0.25.0-alpha.19" - resolved "https://registry.yarnpkg.com/@elastic/nodegit/-/nodegit-0.25.0-alpha.19.tgz#a05a712dedbdbd7fe649cb970eb5677c5ec4ada8" - integrity sha512-fOG7tXkf8wmZEVMld+tkZtS3BRsUlrh95sRNUglHDd0G3g+ow9YDjV3dFHlLST0pTWirKyJuxrow2KgpLoWplA== +"@elastic/nodegit@0.25.0-alpha.20": + version "0.25.0-alpha.20" + resolved "https://registry.yarnpkg.com/@elastic/nodegit/-/nodegit-0.25.0-alpha.20.tgz#74f36cb8c137386aeebacded574d1d6a4e6d9a01" + integrity sha512-wvBTfKVAhFkTMh/N6oO/5NOa+m+cRiZLrIPHpC3ITPBQiDAUP+g2/NzKVGh/5vx5D/Y9ucy7VsbP13zU9rSi0g== dependencies: fs-extra "^7.0.0" json5 "^2.1.0"