Skip to content

Commit

Permalink
mhutchie#462 Pipeline/Workflow status for GitLab/GitHub
Browse files Browse the repository at this point in the history
  • Loading branch information
keydepth committed Mar 6, 2021
1 parent ac69033 commit 9b5aa94
Show file tree
Hide file tree
Showing 11 changed files with 445 additions and 25 deletions.
29 changes: 28 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,31 @@
"default": [],
"markdownDescription": "An array of custom Pull Request providers that can be used in the \"Pull Request Creation\" Integration. For information on how to configure this setting, see the documentation [here](https://github.com/mhutchie/vscode-git-graph/wiki/Configuring-a-custom-Pull-Request-Provider)."
},
"git-graph.customPipelineProviders": {
"type": "array",
"items": {
"type": "object",
"title": "Pipeline Request Provider",
"required": [
"name",
"templateUrl"
],
"properties": {
"name": {
"type": "string",
"title": "Name of the Provider",
"description": "A unique, identifying, display name for the provider."
},
"templateUrl": {
"type": "string",
"title": "Template URL",
"markdownDescription": "A template URL that can be used to create a Pipeline, after the $1 - $8 variables have been substituted to construct the final URL. For information on how to configure this setting, see the documentation [here](https://github.com/mhutchie/vscode-git-graph/wiki/Configuring-a-custom-Pull-Request-Provider)."
}
}
},
"default": [],
"markdownDescription": "An array of custom Pipeline providers that can be used in the \"Pipeline Status\" Integration. For information on how to configure this setting, see the documentation [here](https://github.com/mhutchie/vscode-git-graph/wiki/Configuring-a-custom-Pull-Request-Provider)."
},
"git-graph.date.format": {
"type": "string",
"enum": [
Expand Down Expand Up @@ -1451,7 +1476,8 @@
"test-and-report-coverage": "jest --verbose --coverage"
},
"dependencies": {
"iconv-lite": "0.5.0"
"iconv-lite": "0.5.0",
"request-promise": "^4.2.5"
},
"devDependencies": {
"@types/jest": "26.0.19",
Expand All @@ -1463,6 +1489,7 @@
"jest": "26.6.3",
"ts-jest": "26.4.4",
"typescript": "4.0.2",
"@types/request-promise": "^4.1.46",
"uglify-js": "3.10.0"
}
}
19 changes: 16 additions & 3 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ContextMenuActionsVisibility,
CustomBranchGlobPattern,
CustomEmojiShortcodeMapping,
CustomPipelineProvider,
CustomPullRequestProvider,
DateFormat,
DateFormatType,
Expand Down Expand Up @@ -132,6 +133,18 @@ class Config {
: [];
}

/**
* Get the value of the `git-graph.customPipelineProviders` Extension Setting.
*/
get customPipelineProviders(): CustomPipelineProvider[] {
let providers = this.config.get('customPipelineProviders', <any[]>[]);
return Array.isArray(providers)
? providers
.filter((provider) => typeof provider.name === 'string' && typeof provider.templateUrl === 'string')
.map((provider) => ({ name: provider.name, templateUrl: provider.templateUrl }))
: [];
}

/**
* Get the value of the `git-graph.date.format` Extension Setting.
*/
Expand Down Expand Up @@ -160,10 +173,10 @@ class Config {
*/
get defaultColumnVisibility(): DefaultColumnVisibility {
let obj: any = this.config.get('defaultColumnVisibility', {});
if (typeof obj === 'object' && obj !== null && typeof obj['Date'] === 'boolean' && typeof obj['Author'] === 'boolean' && typeof obj['Commit'] === 'boolean') {
return { author: obj['Author'], commit: obj['Commit'], date: obj['Date'] };
if (typeof obj === 'object' && obj !== null && typeof obj['Date'] === 'boolean' && typeof obj['Author'] === 'boolean' && typeof obj['Commit'] === 'boolean' && typeof obj['Pipeline'] === 'boolean') {
return { author: obj['Author'], commit: obj['Commit'], date: obj['Date'], pipeline: obj['Pileline']};
} else {
return { author: true, commit: true, date: true };
return { author: true, commit: true, date: true, pipeline: true };
}
}

Expand Down
195 changes: 191 additions & 4 deletions src/dataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import * as vscode from 'vscode';
import { AskpassEnvironment, AskpassManager } from './askpass/askpassManager';
import { getConfig } from './config';
import { Logger } from './logger';
import { CommitOrdering, DateType, DeepWriteable, ErrorInfo, GitCommit, GitCommitDetails, GitCommitStash, GitConfigLocation, GitFileChange, GitFileStatus, GitPushBranchMode, GitRepoConfig, GitRepoConfigBranches, GitResetMode, GitSignatureStatus, GitStash, MergeActionOn, RebaseActionOn, SquashMessageFormat, TagType, Writeable } from './types';
import { CommitOrdering, DateType, DeepWriteable, ErrorInfo, GitCommit, GitCommitDetails, GitCommitStash, GitConfigLocation, GitFileChange, GitFileStatus, GitPipelinesData, GitPushBranchMode, GitRepoConfig, GitRepoConfigBranches, GitResetMode, GitSignatureStatus, GitStash, MergeActionOn, PipelineConfig, PipelineProvider, RebaseActionOn, SquashMessageFormat, TagType, Writeable } from './types';
import { GitExecutable, UNABLE_TO_FIND_GIT_MSG, UNCOMMITTED, abbrevCommit, constructIncompatibleGitVersionMessage, getPathFromStr, getPathFromUri, isGitAtLeastVersion, openGitTerminal, pathWithTrailingSlash, realpath, resolveSpawnOutput, showErrorMessage } from './utils';
import { Disposable } from './utils/disposable';
import { Event } from './utils/event';
import * as request from 'request-promise';

const DRIVE_LETTER_PATH_REGEX = /^[a-z]:\//;
const EOL_REGEX = /\r\n|\r|\n/g;
Expand Down Expand Up @@ -158,13 +159,15 @@ export class DataSource extends Disposable {
* @param stashes An array of all stashes in the repository.
* @returns The commits in the repository.
*/
public getCommits(repo: string, branches: ReadonlyArray<string> | null, maxCommits: number, showTags: boolean, showRemoteBranches: boolean, includeCommitsMentionedByReflogs: boolean, onlyFollowFirstParent: boolean, commitOrdering: CommitOrdering, remotes: ReadonlyArray<string>, hideRemotes: ReadonlyArray<string>, stashes: ReadonlyArray<GitStash>): Promise<GitCommitData> {
public getCommits(repo: string, branches: ReadonlyArray<string> | null, maxCommits: number, showTags: boolean, showRemoteBranches: boolean, includeCommitsMentionedByReflogs: boolean, onlyFollowFirstParent: boolean, commitOrdering: CommitOrdering, remotes: ReadonlyArray<string>, hideRemotes: ReadonlyArray<string>, stashes: ReadonlyArray<GitStash>, pipelineConfigs: PipelineConfig[] | null): Promise<GitCommitData> {
const config = getConfig();
return Promise.all([
this.getLog(repo, branches, maxCommits + 1, showTags && config.showCommitsOnlyReferencedByTags, showRemoteBranches, includeCommitsMentionedByReflogs, onlyFollowFirstParent, commitOrdering, remotes, hideRemotes, stashes),
this.getRefs(repo, showRemoteBranches, config.showRemoteHeads, hideRemotes).then((refData: GitRefData) => refData, (errorMessage: string) => errorMessage)
this.getRefs(repo, showRemoteBranches, config.showRemoteHeads, hideRemotes).then((refData: GitRefData) => refData, (errorMessage: string) => errorMessage),
this.getPipelines(pipelineConfigs).then((refData: GitPipelinesData[] | string | undefined) => refData, (errorMessage: string) => errorMessage)
]).then(async (results) => {
let commits: GitCommitRecord[] = results[0], refData: GitRefData | string = results[1], i;
let pipelines: GitPipelinesData[] | string | undefined = results[2];
let moreCommitsAvailable = commits.length === maxCommits + 1;
if (moreCommitsAvailable) commits.pop();

Expand Down Expand Up @@ -197,7 +200,7 @@ export class DataSource extends Disposable {

for (i = 0; i < commits.length; i++) {
commitLookup[commits[i].hash] = i;
commitNodes.push({ ...commits[i], heads: [], tags: [], remotes: [], stash: null });
commitNodes.push({ ...commits[i], heads: [], tags: [], remotes: [], stash: null, pipeline: null });
}

/* Insert Stashes */
Expand All @@ -217,6 +220,7 @@ export class DataSource extends Disposable {
for (i = toAdd.length - 1; i >= 0; i--) {
let stash = toAdd[i].data;
commitNodes.splice(toAdd[i].index, 0, {
pipeline: null,
hash: stash.hash,
parents: [stash.baseHash],
author: stash.author,
Expand Down Expand Up @@ -257,6 +261,16 @@ export class DataSource extends Disposable {
}
}

if (typeof pipelines === 'string' || typeof pipelines === 'undefined') {
pipelines = [];
}
/* Annotate Pipelines */
for (i = 0; i < pipelines.length; i++) {
if (typeof commitLookup[pipelines[i].sha] === 'number') {
commitNodes[commitLookup[pipelines[i].sha]].pipeline = pipelines[i];
}
}

return {
commits: commitNodes,
head: refData.head,
Expand Down Expand Up @@ -1497,6 +1511,179 @@ export class DataSource extends Disposable {
});
}

/**
* Get the result in a Pipelines.
* @param pipelineConfigs pipeline configuration.
* @returns The references data.
*/
private async getPipelines(pipelineConfigs: PipelineConfig[] | null) {
if (pipelineConfigs === null) {
return '';
}

return await Promise.all(
pipelineConfigs.map(async pipelineConfig => {
if (pipelineConfig.provider === PipelineProvider.GitHubV3) {

const match1 = pipelineConfig.gitUrl.match(/^(https?:\/\/|git@)((?=[^/]+@)[^@]+@|(?![^/]+@))([^/:]+)/);
let hostRootUrl = match1 !== null ? 'https://api.' + match1[3] : '';

const match2 = pipelineConfig.gitUrl.match(/^(https?:\/\/|git@)[^/:]+[/:]([^/]+)\/([^/]*?)(.git|)$/);
let sourceOwner = match2 !== null ? match2[2] : '';
let sourceRepo = match2 !== null ? match2[3] : '';

const apiRoot = `${hostRootUrl}`;
const pipelinesRootPath = `/repos/${sourceOwner}/${sourceRepo.replace(/\//g, '%2F')}/actions/runs?per_page=100`;

const config: request.RequestPromiseOptions = {
method: 'GET',
headers: {
'Authorization': `token ${pipelineConfig.glToken}`,
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'vscode-git-graph'
}
};

config.transform = (body, response) => {
try {
let res: any = JSON.parse(body);
let last = 1;
let next = 2;
if (typeof response.headers['link'] === 'string') {
next = 3;
const DELIM_LINKS = ',';
const DELIM_LINK_PARAM = ';';
let links = response.headers['link'].split(DELIM_LINKS);
links.forEach(link => {
let segments = link.split(DELIM_LINK_PARAM);

let linkPart = segments[0].trim();
if (!linkPart.startsWith('<') || !linkPart.endsWith('>')) {
return true;
}
linkPart = linkPart.substring(1, linkPart.length - 1);
let match3 = linkPart.match(/&page=(\d+).*$/);
let linkPage = match3 !== null ? match3[1] : '0';

for (let i = 1; i < segments.length; i++) {
let rel = segments[i].trim().split('=');
if (rel.length < 2) {
continue;
}

let relValue = rel[1];
if (relValue.startsWith('"') && relValue.endsWith('"')) {
relValue = relValue.substring(1, relValue.length - 1);
}

if (relValue === 'last') {
last = parseInt(linkPage);
} else if (relValue === 'next') {
next = parseInt(linkPage);
}
}
});
}
if (typeof res['workflow_runs'] !== 'undefined' && res['workflow_runs'].length >= 1) { // url found
let ret: GitPipelinesData[] = res['workflow_runs'].map( (elm: { [x: string]: any; }) => {
return {
id: elm['id'],
status: elm['conclusion'],
ref: elm['name'],
sha: elm['head_sha'],
web_url: elm['html_url'],
created_at: elm['created_at'],
updated_at: elm['updated_at']
};
});
if (next === 2) {
return { x_total_pages: last, ret: ret };
}
return ret;
}
return { x_total_pages: 0, ret: 'error' };
} catch (e) {
return { x_total_pages: 0, ret: e };
}
};
return request(`${apiRoot}${pipelinesRootPath}`, config).then(async (result1st) => {
let promises = [];
promises.push(result1st.ret);
for (let i = 1; i < result1st.x_total_pages; i++) {
promises.push(request(`${apiRoot}${pipelinesRootPath}&page=${i + 1}`, config));
}
return await Promise.all(promises);
}).then((resultAll) => {
let retAll: GitPipelinesData[] = [];
for (let i = 0; i < resultAll.length; i++) {
retAll = retAll.concat(resultAll[i]);
}
return retAll;
});
}
if (pipelineConfig.provider === PipelineProvider.GitLabV4) {

const match1 = pipelineConfig.gitUrl.match(/^(https?:\/\/|git@)((?=[^/]+@)[^@]+@|(?![^/]+@))([^/:]+)/);
let hostRootUrl = match1 !== null ? 'https://' + match1[3] : '';

const match2 = pipelineConfig.gitUrl.match(/^(https?:\/\/|git@)[^/:]+[/:]([^/]+)\/([^/]*?)(.git|)$/);
let sourceOwner = match2 !== null ? match2[2] : '';
let sourceRepo = match2 !== null ? match2[3] : '';

const apiRoot = `${hostRootUrl}/api/v4`;
const pipelinesRootPath = `/projects/${sourceOwner}%2F${sourceRepo.replace(/\//g, '%2F')}/pipelines?per_page=100`;

const config: request.RequestPromiseOptions = {
method: 'GET',
headers: {
'PRIVATE-TOKEN': pipelineConfig.glToken,
'User-Agent': 'vscode-git-graph'
}
};

config.transform = (body, response) => {
try {
if (typeof response.headers['x-page'] === 'string' && typeof response.headers['x-total-pages'] === 'string' && typeof response.headers['x-total'] === 'string') {
let res: any = JSON.parse(body);
if (parseInt(response.headers['x-total']) !== 0 && res.length && res[0].id) { // url found
let ret: GitPipelinesData[] = res;
if (parseInt(response.headers['x-page']) === 1) {
return { x_total_pages: parseInt(response.headers['x-total-pages']), ret: ret };
}
return ret;
}
}
return { x_total_pages: 0, ret: 'error' };
} catch (e) {
return { x_total_pages: 0, ret: e };
}
};
return request(`${apiRoot}${pipelinesRootPath}`, config).then(async (result1st) => {
let promises = [];
promises.push(result1st.ret);
for (let i = 1; i < result1st.x_total_pages; i++) {
promises.push(request(`${apiRoot}${pipelinesRootPath}&page=${i + 1}`, config));
}
return await Promise.all(promises);
}).then((resultAll) => {
let retAll: GitPipelinesData[] = [];
for (let i = 0; i < resultAll.length; i++) {
retAll = retAll.concat(resultAll[i]);
}
return retAll;
});
}
})
).then((resultAll2) => {
let retAll: GitPipelinesData[] = [];
resultAll2.forEach(resultList => {
resultList?.forEach(result => {
retAll = retAll.concat(result);
});
});
return retAll;
});
}
/**
* Get the references in a repository.
* @param repo The path of the repository.
Expand Down
1 change: 1 addition & 0 deletions src/extensionState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const DEFAULT_REPO_STATE: GitRepoState = {
onRepoLoadShowCheckedOutBranch: BooleanOverride.Default,
onRepoLoadShowSpecificBranches: null,
pullRequestConfig: null,
pipelineConfigs: null,
showRemoteBranches: true,
showRemoteBranchesV2: BooleanOverride.Default,
showStashes: BooleanOverride.Default,
Expand Down
3 changes: 2 additions & 1 deletion src/gitGraphView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ export class GitGraphView extends Disposable {
command: 'loadCommits',
refreshId: msg.refreshId,
onlyFollowFirstParent: msg.onlyFollowFirstParent,
...await this.dataSource.getCommits(msg.repo, msg.branches, msg.maxCommits, msg.showTags, msg.showRemoteBranches, msg.includeCommitsMentionedByReflogs, msg.onlyFollowFirstParent, msg.commitOrdering, msg.remotes, msg.hideRemotes, msg.stashes)
...await this.dataSource.getCommits(msg.repo, msg.branches, msg.maxCommits, msg.showTags, msg.showRemoteBranches, msg.includeCommitsMentionedByReflogs, msg.onlyFollowFirstParent, msg.commitOrdering, msg.remotes, msg.hideRemotes, msg.stashes, msg.pipelineConfigs)
});
break;
case 'loadConfig':
Expand Down Expand Up @@ -628,6 +628,7 @@ export class GitGraphView extends Disposable {
customBranchGlobPatterns: config.customBranchGlobPatterns,
customEmojiShortcodeMappings: config.customEmojiShortcodeMappings,
customPullRequestProviders: config.customPullRequestProviders,
customPipelineProviders: config.customPipelineProviders,
dateFormat: config.dateFormat,
defaultColumnVisibility: config.defaultColumnVisibility,
dialogDefaults: config.dialogDefaults,
Expand Down
Loading

0 comments on commit 9b5aa94

Please sign in to comment.