提交 03d43b1f 编写于 作者: T Tomas Vik

Merge branch '228-prepare-for-detached-pipeline' into 'main'

refactor(status bar): extract status_bar module into a class

See merge request gitlab-org/gitlab-vscode-extension!181
......@@ -24,14 +24,17 @@ module.exports = {
},
window: {
showErrorMessage: jest.fn(),
createStatusBarItem: jest.fn(),
},
commands: {
executeCommand: jest.fn(),
registerCommand: jest.fn(),
},
workspace: {
getConfiguration: jest.fn().mockReturnValue({}),
},
CommentMode: { Preview: 1 },
StatusBarAlignment: { Left: 0 },
CommentThreadCollapsibleState: { Expanded: 1 },
Position: function Position(x, y) {
return { x, y };
......
......@@ -92,10 +92,10 @@ describe('git_service', () => {
});
});
describe('fetchGitRemotePipeline', () => {
describe('fetchPipelineGitRemote', () => {
it('returns default remote when the pipelineGitRemoteName setting is missing', async () => {
await git.addRemote(SECOND_REMOTE, 'git@test.another.com:gitlab-org/gitlab.git');
const remoteUrl = await gitService.fetchGitRemotePipeline();
const remoteUrl = await gitService.fetchPipelineGitRemote();
expect(remoteUrl?.host).toEqual('test.gitlab.com');
});
......@@ -105,7 +105,7 @@ describe('git_service', () => {
const options = { ...getDefaultOptions(), pipelineGitRemoteName: SECOND_REMOTE };
gitService = new GitService(options);
const remoteUrl = await gitService.fetchGitRemotePipeline();
const remoteUrl = await gitService.fetchPipelineGitRemote();
expect(remoteUrl?.host).toEqual('test.another.com');
});
......@@ -166,8 +166,8 @@ describe('git_service', () => {
expect(gitService.fetchLastCommitId()).rejects.toBeInstanceOf(Error);
});
it('fetchGitRemotePipeline returns null', async () => {
expect(gitService.fetchGitRemotePipeline()).rejects.toBeInstanceOf(Error);
it('fetchPipelineGitRemote returns null', async () => {
expect(gitService.fetchPipelineGitRemote()).rejects.toBeInstanceOf(Error);
});
it('fetchTrackingBranchName returns null', async () => {
......
......@@ -67,7 +67,7 @@ export class GitService {
return this.fetch('git log --format=%H -n 1');
}
async fetchGitRemotePipeline(): Promise<GitRemote | null> {
async fetchPipelineGitRemote(): Promise<GitRemote | null> {
return this.fetchRemoteUrl(this.pipelineGitRemoteName);
}
......
......@@ -138,7 +138,7 @@ export async function fetchCurrentProjectSwallowError(workspaceFolder: string) {
export async function fetchCurrentPipelineProject(workspaceFolder: string) {
try {
const remote = await createGitService(workspaceFolder).fetchGitRemotePipeline();
const remote = await createGitService(workspaceFolder).fetchPipelineGitRemote();
return await fetchProjectData(remote, workspaceFolder);
} catch (e) {
......
const vscode = require('vscode');
const gitLabService = require('./gitlab_service');
const openers = require('./openers');
const statusBar = require('./status_bar');
const { instance: statusBar } = require('./status_bar');
const { getCurrentWorkspaceFolderOrSelectOne } = require('./services/workspace_service');
async function showPicker() {
......
......@@ -6,15 +6,8 @@ const { UserFriendlyError } = require('./errors/user_friendly_error');
const { handleError, logError } = require('./log');
const { USER_COMMANDS } = require('./command_names');
let context = null;
let pipelineStatusBarItem = null;
let pipelinesStatusTimer = null;
let mrStatusBarItem = null;
let mrIssueStatusBarItem = null;
let mrStatusTimer = null;
let issue = null;
let mr = null;
let firstRun = true;
const MAXIMUM_DISPLAYED_JOBS = 4;
const {
showStatusBarLinks,
showIssueLinkOnStatusBar,
......@@ -22,9 +15,33 @@ const {
showPipelineUpdateNotifications,
} = vscode.workspace.getConfiguration('gitlab');
const iconForStatus = {
running: { icon: 'pulse' },
pending: { icon: 'clock' },
success: { icon: 'check', text: 'passed' },
failed: { icon: 'x' },
canceled: { icon: 'circle-slash' },
skipped: { icon: 'diff-renamed' },
};
const getStatusText = status => iconForStatus[status].text || status;
const createStatusTextFromJobs = (jobs, status) => {
let statusText = getStatusText(status);
const jobNames = jobs.filter(job => job.status === status).map(job => job.name);
if (jobNames.length > MAXIMUM_DISPLAYED_JOBS) {
statusText += ' (';
statusText += jobNames.slice(0, MAXIMUM_DISPLAYED_JOBS).join(', ');
statusText += `, +${jobNames.length - MAXIMUM_DISPLAYED_JOBS} jobs`;
statusText += ')';
} else if (jobNames.length > 0) {
statusText += ` (${jobNames.join(', ')})`;
}
return statusText;
};
const createStatusBarItem = (text, command) => {
const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
context.subscriptions.push(statusBarItem);
statusBarItem.text = text;
statusBarItem.show();
......@@ -39,19 +56,22 @@ const commandRegisterHelper = (cmdName, callback) => {
vscode.commands.registerCommand(cmdName, callback);
};
async function refreshPipeline() {
class StatusBar {
constructor() {
this.pipelineStatusBarItem = null;
this.pipelinesStatusTimer = null;
this.mrStatusBarItem = null;
this.mrIssueStatusBarItem = null;
this.mrStatusTimer = null;
this.issue = null;
this.mr = null;
this.firstRun = true;
}
async refreshPipeline() {
let workspaceFolder = null;
let project = null;
let pipeline = null;
const maxJobs = 4;
const statuses = {
running: { icon: 'pulse' },
pending: { icon: 'clock' },
success: { icon: 'check', text: 'passed' },
failed: { icon: 'x' },
canceled: { icon: 'circle-slash' },
skipped: { icon: 'diff-renamed' },
};
try {
workspaceFolder = await getCurrentWorkspaceFolder();
......@@ -59,42 +79,38 @@ async function refreshPipeline() {
if (project != null) {
pipeline = await gitLabService.fetchLastPipelineForCurrentBranch(workspaceFolder);
} else {
pipelineStatusBarItem.hide();
this.pipelineStatusBarItem.hide();
}
} catch (e) {
logError(e);
if (!project) {
pipelineStatusBarItem.hide();
this.pipelineStatusBarItem.hide();
return;
}
}
if (pipeline) {
const { status } = pipeline;
let statusText = statuses[status].text || status;
let statusText = getStatusText(status);
if (status === 'running' || status === 'failed') {
try {
const jobs = await gitLabService.fetchLastJobsForCurrentBranch(pipeline, workspaceFolder);
if (jobs) {
const jobNames = jobs.filter(job => job.status === status).map(job => job.name);
if (jobNames.length > maxJobs) {
statusText += ' (';
statusText += jobNames.slice(0, maxJobs).join(', ');
statusText += `, +${jobNames.length - maxJobs} jobs`;
statusText += ')';
} else if (jobNames.length > 0) {
statusText += ` (${jobNames.join(', ')})`;
}
statusText = createStatusTextFromJobs(jobs, status);
}
} catch (e) {
handleError(new UserFriendlyError('Failed to fetch jobs for pipeline.', e));
}
}
const msg = `$(${statuses[status].icon}) GitLab: Pipeline ${statusText}`;
const msg = `$(${iconForStatus[status].icon}) GitLab: Pipeline ${statusText}`;
if (showPipelineUpdateNotifications && pipelineStatusBarItem.text !== msg && !firstRun) {
if (
showPipelineUpdateNotifications &&
this.pipelineStatusBarItem.text !== msg &&
!this.firstRun
) {
const message = `Pipeline ${statusText}.`;
vscode.window
......@@ -106,40 +122,40 @@ async function refreshPipeline() {
});
}
pipelineStatusBarItem.text = msg;
pipelineStatusBarItem.show();
this.pipelineStatusBarItem.text = msg;
this.pipelineStatusBarItem.show();
} else {
pipelineStatusBarItem.text = 'GitLab: No pipeline.';
this.pipelineStatusBarItem.text = 'GitLab: No pipeline.';
}
this.firstRun = false;
}
firstRun = false;
}
const initPipelineStatus = async () => {
pipelineStatusBarItem = createStatusBarItem(
async initPipelineStatus() {
this.pipelineStatusBarItem = createStatusBarItem(
'$(info) GitLab: Fetching pipeline...',
USER_COMMANDS.PIPELINE_ACTIONS,
);
pipelinesStatusTimer = setInterval(() => {
refreshPipeline();
this.pipelinesStatusTimer = setInterval(() => {
this.refreshPipeline();
}, 30000);
await refreshPipeline();
};
await this.refreshPipeline();
}
async function fetchMRIssues(workspaceFolder) {
const issues = await gitLabService.fetchMRIssues(mr.iid, workspaceFolder);
async fetchMRIssues(workspaceFolder) {
const issues = await gitLabService.fetchMRIssues(this.mr.iid, workspaceFolder);
let text = `$(code) GitLab: No issue.`;
if (issues[0]) {
[issue] = issues;
text = `$(code) GitLab: Issue #${issue.iid}`;
[this.issue] = issues;
text = `$(code) GitLab: Issue #${this.issue.iid}`;
}
mrIssueStatusBarItem.text = text;
}
this.mrIssueStatusBarItem.text = text;
}
async function fetchBranchMR() {
async fetchBranchMR() {
let text = '$(git-pull-request) GitLab: Create MR.';
let workspaceFolder = null;
let project = null;
......@@ -148,94 +164,107 @@ async function fetchBranchMR() {
workspaceFolder = await getCurrentWorkspaceFolder();
project = await gitLabService.fetchCurrentProject(workspaceFolder);
if (project != null) {
mr = await gitLabService.fetchOpenMergeRequestForCurrentBranch(workspaceFolder);
mrStatusBarItem.show();
this.mr = await gitLabService.fetchOpenMergeRequestForCurrentBranch(workspaceFolder);
this.mrStatusBarItem.show();
} else {
mrStatusBarItem.hide();
this.mrStatusBarItem.hide();
}
} catch (e) {
logError(e);
mrStatusBarItem.hide();
this.mrStatusBarItem.hide();
}
if (project && mr) {
text = `$(git-pull-request) GitLab: MR !${mr.iid}`;
await fetchMRIssues(workspaceFolder);
mrIssueStatusBarItem.show();
if (project && this.mr) {
text = `$(git-pull-request) GitLab: MR !${this.mr.iid}`;
await this.fetchMRIssues(workspaceFolder);
this.mrIssueStatusBarItem.show();
} else if (project) {
mrIssueStatusBarItem.text = `$(code) GitLab: No issue.`;
mrIssueStatusBarItem.show();
this.mrIssueStatusBarItem.text = `$(code) GitLab: No issue.`;
this.mrIssueStatusBarItem.show();
} else {
mrIssueStatusBarItem.hide();
this.mrIssueStatusBarItem.hide();
}
mrStatusBarItem.text = text;
}
this.mrStatusBarItem.text = text;
}
const initMrStatus = async () => {
async initMrStatus() {
const cmdName = `gl.mrOpener${Date.now()}`;
commandRegisterHelper(cmdName, () => {
if (mr) {
openers.openUrl(mr.web_url);
if (this.mr) {
openers.openUrl(this.mr.web_url);
} else {
openers.openCreateNewMr();
}
});
mrStatusBarItem = createStatusBarItem('$(info) GitLab: Finding MR...', cmdName);
mrStatusTimer = setInterval(() => {
fetchBranchMR();
this.mrStatusBarItem = createStatusBarItem('$(info) GitLab: Finding MR...', cmdName);
this.mrStatusTimer = setInterval(() => {
this.fetchBranchMR();
}, 60000);
await fetchBranchMR();
};
await this.fetchBranchMR();
}
const initMrIssueStatus = () => {
initMrIssueStatus() {
const cmdName = `gl.mrIssueOpener${Date.now()}`;
commandRegisterHelper(cmdName, () => {
if (issue) {
openers.openUrl(issue.web_url);
if (this.issue) {
openers.openUrl(this.issue.web_url);
} else {
vscode.window.showInformationMessage('GitLab Workflow: No closing issue found for this MR.');
vscode.window.showInformationMessage(
'GitLab Workflow: No closing issue found for this MR.',
);
}
});
mrIssueStatusBarItem = createStatusBarItem('$(info) GitLab: Fetching closing issue...', cmdName);
};
const init = async ctx => {
context = ctx;
this.mrIssueStatusBarItem = createStatusBarItem(
'$(info) GitLab: Fetching closing issue...',
cmdName,
);
}
async init() {
if (showStatusBarLinks) {
await initPipelineStatus();
await this.initPipelineStatus();
// FIXME: add showMrStatusOnStatusBar to the condition
// because the initMrStatus() method does all the fetching and initMrIssueStatus
// only introduces a placeholder item
if (showIssueLinkOnStatusBar) {
initMrIssueStatus();
this.initMrIssueStatus();
}
if (showMrStatusOnStatusBar) {
await initMrStatus();
await this.initMrStatus();
}
}
};
}
dispose() {
if (showStatusBarLinks) {
this.pipelineStatusBarItem.dispose();
const dispose = () => {
mrStatusBarItem.dispose();
pipelineStatusBarItem.dispose();
if (showIssueLinkOnStatusBar) {
mrIssueStatusBarItem.dispose();
this.mrIssueStatusBarItem.dispose();
}
if (showMrStatusOnStatusBar) {
this.mrStatusBarItem.dispose();
}
}
if (pipelinesStatusTimer) {
clearInterval(pipelinesStatusTimer);
pipelinesStatusTimer = null;
if (this.pipelinesStatusTimer) {
clearInterval(this.pipelinesStatusTimer);
this.pipelinesStatusTimer = null;
}
if (mrStatusTimer) {
clearInterval(mrStatusTimer);
mrStatusTimer = null;
if (this.mrStatusTimer) {
clearInterval(this.mrStatusTimer);
this.mrStatusTimer = null;
}
};
}
}
exports.init = init;
exports.dispose = dispose;
exports.refreshPipeline = refreshPipeline;
module.exports = {
StatusBar,
instance: new StatusBar(),
};
const vscode = require('vscode');
jest.mock('./gitlab_service');
vscode.workspace.getConfiguration.mockReturnValue({
showStatusBarLinks: true,
showIssueLinkOnStatusBar: true,
showMrStatusOnStatusBar: true,
});
const { StatusBar } = require('./status_bar');
const gitLabService = require('./gitlab_service');
const { project, pipeline, mr, issue } = require('./test_utils/entities');
const createFakeItem = () => ({
show: jest.fn(),
hide: jest.fn(),
dispose: jest.fn(),
});
describe('status_bar', () => {
let fakeItems;
let statusBar;
const getPipelineItem = () => fakeItems[0];
const getClosingIssueItem = () => fakeItems[1];
const getMrItem = () => fakeItems[2];
beforeEach(() => {
fakeItems = [];
statusBar = new StatusBar();
vscode.window.createStatusBarItem.mockImplementation(() => {
const fakeItem = createFakeItem();
fakeItems.push(fakeItem);
return fakeItem;
});
});
describe('pipeline item', () => {
beforeEach(() => {
gitLabService.fetchCurrentPipelineProject.mockReturnValue(project);
gitLabService.fetchLastJobsForCurrentBranch.mockReset();
});
afterEach(() => {
statusBar.dispose();
});
it('initializes the pipeline item with success', async () => {
gitLabService.fetchLastPipelineForCurrentBranch.mockReturnValue(pipeline);
await statusBar.init();
expect(getPipelineItem().show).toHaveBeenCalled();
expect(getPipelineItem().hide).not.toHaveBeenCalled();
expect(getPipelineItem().text).toBe('$(check) GitLab: Pipeline passed');
});
it('prints jobs for running pipeline', async () => {
gitLabService.fetchLastPipelineForCurrentBranch.mockReturnValue({
...pipeline,
status: 'running',
});
gitLabService.fetchLastJobsForCurrentBranch.mockReturnValue([
{
status: 'running',
name: 'Unit Tests',
},
{
status: 'running',
name: 'Integration Tests',
},
{
status: 'success',
name: 'Lint',
},
]);
await statusBar.init();
expect(getPipelineItem().text).toBe(
'$(pulse) GitLab: Pipeline running (Unit Tests, Integration Tests)',
);
});
it('shows no pipeline text when there is no pipeline', async () => {
gitLabService.fetchLastPipelineForCurrentBranch.mockReturnValue(null);
await statusBar.init();
expect(getPipelineItem().text).toBe('GitLab: No pipeline.');
});
it('hides the item when there is no project', async () => {
gitLabService.fetchCurrentPipelineProject.mockReturnValue(null);
await statusBar.init();
expect(getPipelineItem().hide).toHaveBeenCalled();
});
it.each`
status | itemText
${'running'} | ${'$(pulse) GitLab: Pipeline running'}
${'success'} | ${'$(check) GitLab: Pipeline passed'}
${'pending'} | ${'$(clock) GitLab: Pipeline pending'}
${'failed'} | ${'$(x) GitLab: Pipeline failed'}
${'canceled'} | ${'$(circle-slash) GitLab: Pipeline canceled'}
${'skipped'} | ${'$(diff-renamed) GitLab: Pipeline skipped'}
`('shows $itemText for pipeline with status $status', async ({ status, itemText }) => {
gitLabService.fetchLastPipelineForCurrentBranch.mockReturnValue({
...pipeline,
status,
});
await statusBar.init();
expect(getPipelineItem().text).toBe(itemText);
});
});
describe('MR closing issue item', () => {
beforeEach(() => {
gitLabService.fetchCurrentPipelineProject.mockReturnValue(project);
// FIXME: why is closing issue fetched from normal remote and pipeline result from pipeline remote?
gitLabService.fetchCurrentProject.mockReturnValue(project);
gitLabService.fetchLastPipelineForCurrentBranch.mockReturnValue(null);
});
afterEach(() => {
statusBar.dispose();
});
it('shows closing issue for an MR', async () => {
gitLabService.fetchOpenMergeRequestForCurrentBranch.mockReturnValue(mr);
gitLabService.fetchMRIssues.mockReturnValue([issue]);
await statusBar.init();
expect(getClosingIssueItem().show).toHaveBeenCalled();
expect(getClosingIssueItem().hide).not.toHaveBeenCalled();
expect(getClosingIssueItem().text).toBe('$(code) GitLab: Issue #1000');
});
it('shows no issue when there is not a closing issue', async () => {
gitLabService.fetchOpenMergeRequestForCurrentBranch.mockReturnValue(mr);
gitLabService.fetchMRIssues.mockReturnValue([]);
await statusBar.init();
expect(getClosingIssueItem().text).toBe('$(code) GitLab: No issue.');
});
it('shows no issue when there is no MR', async () => {
gitLabService.fetchOpenMergeRequestForCurrentBranch.mockReturnValue(null);
await statusBar.init();
expect(getClosingIssueItem().text).toBe('$(code) GitLab: No issue.');
});
it('hides the item when there is no project', async () => {
gitLabService.fetchCurrentProject.mockReturnValue(null);
await statusBar.init();
expect(getClosingIssueItem().hide).toHaveBeenCalled();
});
});
describe('MR item', () => {
beforeEach(() => {
gitLabService.fetchCurrentPipelineProject.mockReturnValue(project);
// FIXME: why is closing issue fetched from normal remote and pipeline result from pipeline remote?
gitLabService.fetchCurrentProject.mockReturnValue(project);
gitLabService.fetchLastPipelineForCurrentBranch.mockReturnValue(null);
});
afterEach(() => {
statusBar.dispose();
});
it('shows MR item', async () => {
gitLabService.fetchOpenMergeRequestForCurrentBranch.mockReturnValue(mr);
await statusBar.init();
expect(getMrItem().show).toHaveBeenCalled();
expect(getMrItem().hide).not.toHaveBeenCalled();
expect(getMrItem().text).toBe('$(git-pull-request) GitLab: MR !2000');
});
it('shows create MR text when there is no MR', async () => {
gitLabService.fetchOpenMergeRequestForCurrentBranch.mockReturnValue(null);
await statusBar.init();
expect(getMrItem().text).toBe('$(git-pull-request) GitLab: Create MR.');
});
it('hides the MR item when there is no project', async () => {
gitLabService.fetchCurrentProject.mockReturnValue(null);
await statusBar.init();
expect(getMrItem().hide).toHaveBeenCalled();
});
});
});
......@@ -73,3 +73,9 @@ export const createReviewUri = ({
scheme: 'gl-review',
} as vscode.Uri;
};
export const pipeline: RestPipeline = {
status: 'success',
updated_at: '2021-02-12T12:06:17Z',
id: 123456,
};
const vscode = require('vscode');
const { GITLAB_COM_URL } = require('./constants');
const openers = require('./openers');
const statusBar = require('./status_bar');
const { instance: statusBar } = require('./status_bar');
const { tokenService } = require('./services/token_service');
const { USER_COMMANDS } = require('./command_names');
......@@ -17,7 +17,8 @@ const updateExtensionStatus = () => {
const tokenExists = !!getToken();
if (!active && tokenExists) {
statusBar.init(context);
statusBar.init();
context.subscriptions.push(statusBar);
active = true;
} else if (active && !tokenExists) {
statusBar.dispose();
......
const assert = require('assert');
const sinon = require('sinon');
const vscode = require('vscode');
const statusBar = require('../../src/status_bar');
const { StatusBar } = require('../../src/status_bar');
const { tokenService } = require('../../src/services/token_service');
const pipelinesResponse = require('./fixtures/rest/pipelines.json');
const pipelineResponse = require('./fixtures/rest/pipeline.json');
const { getServer, createJsonEndpoint } = require('./test_infrastructure/mock_server');
const {
getServer,
createJsonEndpoint,
createQueryJsonEndpoint,
} = require('./test_infrastructure/mock_server');
const { GITLAB_URL } = require('./test_infrastructure/constants');
const { USER_COMMANDS } = require('../../src/command_names');
describe('GitLab status bar', () => {
let server;
let statusBar;
let returnedItems = [];
const sandbox = sinon.createSandbox();
......@@ -22,13 +27,14 @@ describe('GitLab status bar', () => {
before(async () => {
server = getServer([
createJsonEndpoint('/projects/278964/pipelines?ref=master', pipelinesResponse),
createQueryJsonEndpoint('/projects/278964/pipelines', { '?ref=master': pipelinesResponse }),
createJsonEndpoint('/projects/278964/pipelines/47', pipelineResponse),
]);
await tokenService.setToken(GITLAB_URL, 'abcd-secret');
});
beforeEach(() => {
statusBar = new StatusBar();
server.resetHandlers();
sandbox.stub(vscode.window, 'createStatusBarItem').callsFake(createFakeStatusBarItem);
});
......@@ -45,13 +51,13 @@ describe('GitLab status bar', () => {
});
it('shows the correct pipeline item', async () => {
await statusBar.init({ subscriptions: [] });
await statusBar.init();
assert.strictEqual(
vscode.window.createStatusBarItem.firstCall.firstArg,
vscode.StatusBarAlignment.Left,
);
const pipelineItem = returnedItems[0];
const pipelineItem = statusBar.pipelineStatusBarItem;
assert.strictEqual(pipelineItem.text, '$(check) GitLab: Pipeline passed');
assert.strictEqual(pipelineItem.show.called, true);
assert.strictEqual(pipelineItem.hide.called, false);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册