提交 3ea3e932 编写于 作者: T Tomas Vik

Merge branch '212-convert-status-bar-to-ts' into 'main'

refactor: convert status_bar to TypeScript

See merge request gitlab-org/gitlab-vscode-extension!224
......@@ -21,10 +21,11 @@ interface GitLabPipeline {
id: number;
}
interface GitLabJob {
export interface RestJob {
name: string;
// eslint-disable-next-line camelcase
created_at: string;
status: string;
}
const normalizeAvatarUrl = (instanceUrl: string) => (issuable: RestIssuable): RestIssuable => {
......@@ -102,6 +103,7 @@ async function fetch(path: string, method = 'GET', data?: Record<string, unknown
}
async function fetchProjectData(remote: GitRemote | null, workspaceFolder: string) {
// TODO require remote so we can guarantee that we return a value or error
if (remote) {
if (!(`${remote.namespace}_${remote.project}` in projectCache)) {
const { namespace, project } = remote;
......@@ -376,7 +378,7 @@ export async function fetchLastJobsForCurrentBranch(
const project = await fetchCurrentPipelineProject(workspaceFolder);
if (project) {
const { response } = await fetch(`/projects/${project.restId}/pipelines/${pipeline.id}/jobs`);
let jobs: GitLabJob[] = response;
let jobs: RestJob[] = response;
// Gitlab return multiple jobs if you retry the pipeline we filter to keep only the last
const alreadyProcessedJob = new Set();
......
const path = require('path');
const vscode = require('vscode');
const gitLabService = require('./gitlab_service');
const {
import * as path from 'path';
import * as vscode from 'vscode';
import * as assert from 'assert';
import * as gitLabService from './gitlab_service';
import {
getCurrentWorkspaceFolderOrSelectOne,
getCurrentWorkspaceFolder,
} = require('./services/workspace_service');
const { createGitService } = require('./service_factory');
const { handleError } = require('./log');
const { VS_COMMANDS } = require('./command_names');
} from './services/workspace_service';
import { createGitService } from './service_factory';
import { handleError } from './log';
import { VS_COMMANDS } from './command_names';
const openUrl = url => vscode.commands.executeCommand(VS_COMMANDS.OPEN, vscode.Uri.parse(url));
export const openUrl = async (url: string): Promise<void> =>
vscode.commands.executeCommand(VS_COMMANDS.OPEN, vscode.Uri.parse(url));
/**
* Fetches user and project before opening a link.
......@@ -24,24 +26,29 @@ const openUrl = url => vscode.commands.executeCommand(VS_COMMANDS.OPEN, vscode.U
*
* @param {string} linkTemplate
*/
async function getLink(linkTemplate, workspaceFolder) {
async function getLink(linkTemplate: string, workspaceFolder: string) {
const user = await gitLabService.fetchCurrentUser();
const project = await gitLabService.fetchCurrentProject(workspaceFolder);
assert(project, 'Failed to fetch project');
return linkTemplate.replace('$userId', user.id).replace('$projectUrl', project.webUrl);
}
async function openLink(linkTemplate, workspaceFolder) {
async function openLink(linkTemplate: string, workspaceFolder: string) {
await openUrl(await getLink(linkTemplate, workspaceFolder));
}
async function showIssues() {
export async function showIssues(): Promise<void> {
const workspaceFolder = await getCurrentWorkspaceFolderOrSelectOne();
if (!workspaceFolder) return;
await openLink('$projectUrl/issues?assignee_id=$userId', workspaceFolder);
}
async function showMergeRequests() {
export async function showMergeRequests(): Promise<void> {
const workspaceFolder = await getCurrentWorkspaceFolderOrSelectOne();
if (!workspaceFolder) return;
await openLink('$projectUrl/merge_requests?assignee_id=$userId', workspaceFolder);
}
......@@ -73,7 +80,7 @@ async function getActiveFile() {
await gitService.getRepositoryRootFolder(),
editor.document.uri.fsPath,
);
const fileUrl = `${currentProject.webUrl}/blob/${branchName}/${filePath}`;
const fileUrl = `${currentProject!.webUrl}/blob/${branchName}/${filePath}`;
let anchor = '';
if (editor.selection) {
......@@ -88,11 +95,11 @@ async function getActiveFile() {
return `${fileUrl}${anchor}`;
}
async function openActiveFile() {
await openUrl(await getActiveFile());
export async function openActiveFile(): Promise<void> {
await openUrl((await getActiveFile())!);
}
async function copyLinkToActiveFile() {
export async function copyLinkToActiveFile(): Promise<void> {
const fileUrl = await getActiveFile();
if (fileUrl) {
......@@ -100,8 +107,10 @@ async function copyLinkToActiveFile() {
}
}
async function openCurrentMergeRequest() {
export async function openCurrentMergeRequest(): Promise<void> {
const workspaceFolder = await getCurrentWorkspaceFolderOrSelectOne();
if (!workspaceFolder) return;
const mr = await gitLabService.fetchOpenMergeRequestForCurrentBranch(workspaceFolder);
if (mr) {
......@@ -109,25 +118,29 @@ async function openCurrentMergeRequest() {
}
}
async function openCreateNewIssue() {
export async function openCreateNewIssue(): Promise<void> {
const workspaceFolder = await getCurrentWorkspaceFolderOrSelectOne();
if (!workspaceFolder) return;
openLink('$projectUrl/issues/new', workspaceFolder);
}
async function openCreateNewMr() {
export async function openCreateNewMr(): Promise<void> {
const workspaceFolder = await getCurrentWorkspaceFolderOrSelectOne();
if (!workspaceFolder) return;
const project = await gitLabService.fetchCurrentProject(workspaceFolder);
const branchName = await createGitService(workspaceFolder).fetchTrackingBranchName();
openUrl(`${project.webUrl}/merge_requests/new?merge_request%5Bsource_branch%5D=${branchName}`);
openUrl(`${project!.webUrl}/merge_requests/new?merge_request%5Bsource_branch%5D=${branchName}`);
}
async function openProjectPage() {
export async function openProjectPage(): Promise<void> {
const workspaceFolder = await getCurrentWorkspaceFolderOrSelectOne();
if (!workspaceFolder) return;
openLink('$projectUrl', workspaceFolder);
}
async function openCurrentPipeline(workspaceFolder) {
export async function openCurrentPipeline(workspaceFolder: string): Promise<void> {
const { pipeline } = await gitLabService.fetchPipelineAndMrForCurrentBranch(workspaceFolder);
if (pipeline) {
......@@ -135,10 +148,11 @@ async function openCurrentPipeline(workspaceFolder) {
}
}
async function compareCurrentBranch() {
export async function compareCurrentBranch(): Promise<void> {
let project = null;
let lastCommitId = null;
const workspaceFolder = await getCurrentWorkspaceFolderOrSelectOne();
if (!workspaceFolder) return;
project = await gitLabService.fetchCurrentProject(workspaceFolder);
lastCommitId = await createGitService(workspaceFolder).fetchLastCommitId();
......@@ -147,15 +161,3 @@ async function compareCurrentBranch() {
openUrl(`${project.webUrl}/compare/master...${lastCommitId}`);
}
}
exports.openUrl = openUrl;
exports.showIssues = showIssues;
exports.showMergeRequests = showMergeRequests;
exports.openActiveFile = openActiveFile;
exports.copyLinkToActiveFile = copyLinkToActiveFile;
exports.openCurrentMergeRequest = openCurrentMergeRequest;
exports.openCreateNewIssue = openCreateNewIssue;
exports.openCreateNewMr = openCreateNewMr;
exports.openProjectPage = openProjectPage;
exports.openCurrentPipeline = openCurrentPipeline;
exports.compareCurrentBranch = compareCurrentBranch;
const vscode = require('vscode');
import * as vscode from 'vscode';
import * as gitLabService from './gitlab_service';
import { pipeline, mr, issue } from './test_utils/entities';
jest.mock('./gitlab_service');
vscode.workspace.getConfiguration.mockReturnValue({
const asMock = (mockFn: unknown) => mockFn as jest.Mock;
asMock(vscode.workspace.getConfiguration).mockReturnValue({
showStatusBarLinks: true,
showIssueLinkOnStatusBar: true,
showMrStatusOnStatusBar: true,
});
const { StatusBar } = require('./status_bar');
const gitLabService = require('./gitlab_service');
const { pipeline, mr, issue } = require('./test_utils/entities');
const createFakeItem = () => ({
show: jest.fn(),
hide: jest.fn(),
dispose: jest.fn(),
});
// StatusBar needs to be imported after we mock the configuration because it uses the configuration
// during module initialization
// eslint-disable-next-line import/first
import { StatusBar } from './status_bar';
const createFakeItem = (): vscode.StatusBarItem =>
(({
show: jest.fn(),
hide: jest.fn(),
dispose: jest.fn(),
} as unknown) as vscode.StatusBarItem);
// StatusBar is only interested in whether the project exists or not
const mockedGitLabProject = {};
describe('status_bar', () => {
let fakeItems;
let statusBar;
let fakeItems: vscode.StatusBarItem[];
let statusBar: StatusBar;
const getPipelineItem = () => fakeItems[0];
const getClosingIssueItem = () => fakeItems[1];
const getMrItem = () => fakeItems[2];
......@@ -29,7 +37,7 @@ describe('status_bar', () => {
beforeEach(() => {
fakeItems = [];
statusBar = new StatusBar();
vscode.window.createStatusBarItem.mockImplementation(() => {
asMock(vscode.window.createStatusBarItem).mockImplementation(() => {
const fakeItem = createFakeItem();
fakeItems.push(fakeItem);
return fakeItem;
......@@ -37,7 +45,7 @@ describe('status_bar', () => {
});
describe('pipeline item', () => {
beforeEach(() => {
gitLabService.fetchLastJobsForCurrentBranch.mockReset();
asMock(gitLabService.fetchLastJobsForCurrentBranch).mockReset();
});
afterEach(() => {
......@@ -45,7 +53,7 @@ describe('status_bar', () => {
});
it('initializes the pipeline item with success', async () => {
gitLabService.fetchPipelineAndMrForCurrentBranch.mockResolvedValue({ pipeline });
asMock(gitLabService.fetchPipelineAndMrForCurrentBranch).mockResolvedValue({ pipeline });
await statusBar.init();
expect(getPipelineItem().show).toHaveBeenCalled();
expect(getPipelineItem().hide).not.toHaveBeenCalled();
......@@ -53,13 +61,13 @@ describe('status_bar', () => {
});
it('prints jobs for running pipeline', async () => {
gitLabService.fetchPipelineAndMrForCurrentBranch.mockResolvedValue({
asMock(gitLabService.fetchPipelineAndMrForCurrentBranch).mockResolvedValue({
pipeline: {
...pipeline,
status: 'running',
},
});
gitLabService.fetchLastJobsForCurrentBranch.mockReturnValue([
asMock(gitLabService.fetchLastJobsForCurrentBranch).mockReturnValue([
{
status: 'running',
name: 'Unit Tests',
......@@ -80,13 +88,15 @@ describe('status_bar', () => {
});
it('shows no pipeline text when there is no pipeline', async () => {
gitLabService.fetchPipelineAndMrForCurrentBranch.mockResolvedValue({ pipeline: null });
asMock(gitLabService.fetchPipelineAndMrForCurrentBranch).mockResolvedValue({
pipeline: null,
});
await statusBar.init();
expect(getPipelineItem().text).toBe('GitLab: No pipeline.');
});
it('hides the item when there is no project', async () => {
gitLabService.fetchPipelineAndMrForCurrentBranch.mockRejectedValue(new Error());
asMock(gitLabService.fetchPipelineAndMrForCurrentBranch).mockRejectedValue(new Error());
await statusBar.init();
expect(getPipelineItem().hide).toHaveBeenCalled();
});
......@@ -100,7 +110,7 @@ describe('status_bar', () => {
${'canceled'} | ${'$(circle-slash) GitLab: Pipeline canceled'}
${'skipped'} | ${'$(diff-renamed) GitLab: Pipeline skipped'}
`('shows $itemText for pipeline with status $status', async ({ status, itemText }) => {
gitLabService.fetchPipelineAndMrForCurrentBranch.mockResolvedValue({
asMock(gitLabService.fetchPipelineAndMrForCurrentBranch).mockResolvedValue({
pipeline: {
...pipeline,
status,
......@@ -113,9 +123,9 @@ describe('status_bar', () => {
describe('MR closing issue item', () => {
beforeEach(() => {
gitLabService.fetchCurrentPipelineProject.mockReturnValue(mockedGitLabProject);
asMock(gitLabService.fetchCurrentPipelineProject).mockReturnValue(mockedGitLabProject);
// FIXME: why is closing issue fetched from normal remote and pipeline result from pipeline remote?
gitLabService.fetchCurrentProject.mockReturnValue(mockedGitLabProject);
asMock(gitLabService.fetchCurrentProject).mockReturnValue(mockedGitLabProject);
});
afterEach(() => {
......@@ -123,8 +133,8 @@ describe('status_bar', () => {
});
it('shows closing issue for an MR', async () => {
gitLabService.fetchOpenMergeRequestForCurrentBranch.mockReturnValue(mr);
gitLabService.fetchMRIssues.mockReturnValue([issue]);
asMock(gitLabService.fetchOpenMergeRequestForCurrentBranch).mockReturnValue(mr);
asMock(gitLabService.fetchMRIssues).mockReturnValue([issue]);
await statusBar.init();
expect(getClosingIssueItem().show).toHaveBeenCalled();
expect(getClosingIssueItem().hide).not.toHaveBeenCalled();
......@@ -132,20 +142,20 @@ describe('status_bar', () => {
});
it('shows no issue when there is not a closing issue', async () => {
gitLabService.fetchOpenMergeRequestForCurrentBranch.mockReturnValue(mr);
gitLabService.fetchMRIssues.mockReturnValue([]);
asMock(gitLabService.fetchOpenMergeRequestForCurrentBranch).mockReturnValue(mr);
asMock(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);
asMock(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);
asMock(gitLabService.fetchCurrentProject).mockReturnValue(null);
await statusBar.init();
expect(getClosingIssueItem().hide).toHaveBeenCalled();
});
......@@ -153,9 +163,9 @@ describe('status_bar', () => {
describe('MR item', () => {
beforeEach(() => {
gitLabService.fetchCurrentPipelineProject.mockReturnValue(mockedGitLabProject);
asMock(gitLabService.fetchCurrentPipelineProject).mockReturnValue(mockedGitLabProject);
// FIXME: why is closing issue fetched from normal remote and pipeline result from pipeline remote?
gitLabService.fetchCurrentProject.mockReturnValue(mockedGitLabProject);
asMock(gitLabService.fetchCurrentProject).mockReturnValue(mockedGitLabProject);
});
afterEach(() => {
......@@ -163,7 +173,7 @@ describe('status_bar', () => {
});
it('shows MR item', async () => {
gitLabService.fetchOpenMergeRequestForCurrentBranch.mockReturnValue(mr);
asMock(gitLabService.fetchOpenMergeRequestForCurrentBranch).mockReturnValue(mr);
await statusBar.init();
expect(getMrItem().show).toHaveBeenCalled();
expect(getMrItem().hide).not.toHaveBeenCalled();
......@@ -171,13 +181,13 @@ describe('status_bar', () => {
});
it('shows create MR text when there is no MR', async () => {
gitLabService.fetchOpenMergeRequestForCurrentBranch.mockReturnValue(null);
asMock(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);
asMock(gitLabService.fetchCurrentProject).mockReturnValue(null);
await statusBar.init();
expect(getMrItem().hide).toHaveBeenCalled();
});
......
const vscode = require('vscode');
const openers = require('./openers');
const gitLabService = require('./gitlab_service');
const { getCurrentWorkspaceFolder } = require('./services/workspace_service');
const { UserFriendlyError } = require('./errors/user_friendly_error');
const { handleError, logError } = require('./log');
const { USER_COMMANDS } = require('./command_names');
/* eslint-disable no-unused-expressions */
import * as vscode from 'vscode';
import * as openers from './openers';
import * as gitLabService from './gitlab_service';
import { getCurrentWorkspaceFolder } from './services/workspace_service';
import { UserFriendlyError } from './errors/user_friendly_error';
import { handleError, logError } from './log';
import { USER_COMMANDS } from './command_names';
const MAXIMUM_DISPLAYED_JOBS = 4;
......@@ -16,7 +17,7 @@ const {
showPipelineUpdateNotifications,
} = vscode.workspace.getConfiguration('gitlab');
const iconForStatus = {
const iconForStatus: Record<string, { icon: string; text?: string } | undefined> = {
running: { icon: 'pulse' },
pending: { icon: 'clock' },
success: { icon: 'check', text: 'passed' },
......@@ -25,9 +26,9 @@ const iconForStatus = {
skipped: { icon: 'diff-renamed' },
};
const getStatusText = status => iconForStatus[status].text || status;
const getStatusText = (status: string) => iconForStatus[status]?.text || status;
const createStatusTextFromJobs = (jobs, status) => {
const createStatusTextFromJobs = (jobs: gitLabService.RestJob[], status: string) => {
let statusText = getStatusText(status);
const jobNames = jobs.filter(job => job.status === status).map(job => job.name);
if (jobNames.length > MAXIMUM_DISPLAYED_JOBS) {
......@@ -41,7 +42,7 @@ const createStatusTextFromJobs = (jobs, status) => {
return statusText;
};
const createStatusBarItem = (text, command) => {
const createStatusBarItem = (text: string, command: string) => {
const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
statusBarItem.text = text;
statusBarItem.show();
......@@ -53,29 +54,35 @@ const createStatusBarItem = (text, command) => {
return statusBarItem;
};
const commandRegisterHelper = (cmdName, callback) => {
const commandRegisterHelper = (cmdName: string, callback: (...args: any[]) => any) => {
vscode.commands.registerCommand(cmdName, callback);
};
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;
}
export class StatusBar {
pipelineStatusBarItem?: vscode.StatusBarItem;
pipelinesStatusTimer?: NodeJS.Timeout;
mrStatusBarItem?: vscode.StatusBarItem;
mrIssueStatusBarItem?: vscode.StatusBarItem;
mrStatusTimer?: NodeJS.Timeout;
issue?: RestIssuable;
mr?: RestIssuable;
firstRun = true;
async refreshPipeline() {
let workspaceFolder = null;
if (!this.pipelineStatusBarItem) return;
let workspaceFolder: string | undefined;
let pipeline = null;
try {
workspaceFolder = await getCurrentWorkspaceFolder();
const result = await gitLabService.fetchPipelineAndMrForCurrentBranch(workspaceFolder);
const result = await gitLabService.fetchPipelineAndMrForCurrentBranch(workspaceFolder!);
// TODO: the result contains the MR for this branch as well, we can refactor status_bar
// to use this response instead of making a separate request.
pipeline = result.pipeline;
......@@ -95,7 +102,7 @@ class StatusBar {
if (status === 'running' || status === 'failed') {
try {
const jobs = await gitLabService.fetchLastJobsForCurrentBranch(pipeline, workspaceFolder);
const jobs = await gitLabService.fetchLastJobsForCurrentBranch(pipeline, workspaceFolder!);
if (jobs) {
statusText = createStatusTextFromJobs(jobs, status);
}
......@@ -104,7 +111,7 @@ class StatusBar {
}
}
const msg = `$(${iconForStatus[status].icon}) GitLab: Pipeline ${statusText}`;
const msg = `$(${iconForStatus[status]?.icon}) GitLab: Pipeline ${statusText}`;
if (
showPipelineUpdateNotifications &&
......@@ -117,7 +124,7 @@ class StatusBar {
.showInformationMessage(message, { modal: false }, 'View in Gitlab')
.then(selection => {
if (selection === 'View in Gitlab') {
openers.openCurrentPipeline(workspaceFolder);
openers.openCurrentPipeline(workspaceFolder!);
}
});
}
......@@ -140,7 +147,8 @@ class StatusBar {
await this.refreshPipeline();
}
async fetchMRIssues(workspaceFolder) {
async fetchMRIssues(workspaceFolder: string) {
if (!this.mrIssueStatusBarItem || !this.mr) return;
const issues = await gitLabService.fetchMRIssues(this.mr.iid, workspaceFolder);
let text = `$(code) GitLab: No issue.`;
......@@ -153,15 +161,18 @@ class StatusBar {
}
async fetchBranchMR() {
if (!this.mrIssueStatusBarItem || !this.mrStatusBarItem) return;
let text = '$(git-pull-request) GitLab: Create MR.';
let workspaceFolder = null;
let project = null;
try {
workspaceFolder = await getCurrentWorkspaceFolder();
project = await gitLabService.fetchCurrentProject(workspaceFolder);
project = await gitLabService.fetchCurrentProject(workspaceFolder!);
if (project != null) {
this.mr = await gitLabService.fetchOpenMergeRequestForCurrentBranch(workspaceFolder);
this.mr =
(await gitLabService.fetchOpenMergeRequestForCurrentBranch(workspaceFolder!)) ??
undefined;
this.mrStatusBarItem.show();
} else {
this.mrStatusBarItem.hide();
......@@ -173,7 +184,7 @@ class StatusBar {
if (project && this.mr) {
text = `$(git-pull-request) GitLab: MR !${this.mr.iid}`;
await this.fetchMRIssues(workspaceFolder);
await this.fetchMRIssues(workspaceFolder!);
this.mrIssueStatusBarItem.show();
} else if (project) {
this.mrIssueStatusBarItem.text = `$(code) GitLab: No issue.`;
......@@ -239,29 +250,26 @@ class StatusBar {
dispose() {
if (showStatusBarLinks) {
this.pipelineStatusBarItem.dispose();
this.pipelineStatusBarItem?.dispose();
if (showIssueLinkOnStatusBar) {
this.mrIssueStatusBarItem.dispose();
this.mrIssueStatusBarItem?.dispose();
}
if (showMrStatusOnStatusBar) {
this.mrStatusBarItem.dispose();
this.mrStatusBarItem?.dispose();
}
}
if (this.pipelinesStatusTimer) {
clearInterval(this.pipelinesStatusTimer);
this.pipelinesStatusTimer = null;
this.pipelinesStatusTimer = undefined;
}
if (this.mrStatusTimer) {
clearInterval(this.mrStatusTimer);
this.mrStatusTimer = null;
this.mrStatusTimer = undefined;
}
}
}
module.exports = {
StatusBar,
instance: new StatusBar(),
};
export const instance = new StatusBar();
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册