mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-20 08:33:11 +00:00
feat: use mustache instead of tangular
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2018 StApps
|
||||
* Copyright (C) 2018, 2019 StApps
|
||||
* This program is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the Free
|
||||
* Software Foundation, version 3.
|
||||
@@ -15,15 +15,61 @@
|
||||
import {Api} from '@openstapps/gitlab-api';
|
||||
import {Issue, IssueState, MembershipScope, MergeRequestState, Project, User} from '@openstapps/gitlab-api/lib/types';
|
||||
import {asyncPool} from 'async-pool-native/dist/async-pool';
|
||||
import {readFileSync, writeFile} from 'fs';
|
||||
import * as moment from 'moment';
|
||||
import {join} from 'path';
|
||||
import {render} from 'mustache';
|
||||
import {join, resolve} from 'path';
|
||||
import {cwd} from 'process';
|
||||
import {promisify} from 'util';
|
||||
import {flatten2dArray, getProjects, getSubGroups, logger} from '../common';
|
||||
import {flatten2dArray, getProjects, logger, readFilePromisified, writeFilePromisified} from '../common';
|
||||
import {BOLD_LABELS, GROUPS, LABEL_WEIGHTS} from '../configuration';
|
||||
|
||||
const asyncWriteFile = promisify(writeFile);
|
||||
/**
|
||||
* A structure for template compilation
|
||||
*/
|
||||
export interface StructureForTemplate {
|
||||
issuesByAssignee: AssigneeWithIssues[];
|
||||
meetingDay: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A map
|
||||
*/
|
||||
export interface IssuesGroupedByAssigneeId {
|
||||
[assigneeId: number]: AssigneeWithIssues;
|
||||
}
|
||||
|
||||
/**
|
||||
* An assignee with assigned issues
|
||||
*/
|
||||
export interface AssigneeWithIssues {
|
||||
assignee: User;
|
||||
issueCounts: {
|
||||
closed: number;
|
||||
opened: number;
|
||||
};
|
||||
issues: IssueWithMeta[];
|
||||
quota: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue with meta information
|
||||
*/
|
||||
export interface IssueWithMeta extends Issue {
|
||||
$branchExists: boolean;
|
||||
$labels: string;
|
||||
$mergeRequestUrl: string;
|
||||
$project: string;
|
||||
$weeksOpen: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge request data
|
||||
*/
|
||||
export interface MergeRequestsForProjects {
|
||||
[projectId: string]: Array<{
|
||||
issue_iid: number;
|
||||
web_url: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if issue state is opened or closed
|
||||
@@ -41,8 +87,7 @@ export function issueStateIsOpenedOrClosed(state: IssueState): state is IssueSta
|
||||
* @param projectId Project ID to get data about merge requests for
|
||||
* @param issueIid Issue IID in certain project (relative ID, and not issue's GitLab API ID)
|
||||
*/
|
||||
|
||||
export function getMergeRequestUrls(projectMergeRequests: MergeRequestData,
|
||||
export function getMergeRequestUrls(projectMergeRequests: MergeRequestsForProjects,
|
||||
projectId: number,
|
||||
issueIid: number): string[] {
|
||||
if (typeof projectMergeRequests[projectId] === 'undefined' || projectMergeRequests[projectId].length === 0) {
|
||||
@@ -56,72 +101,15 @@ export function getMergeRequestUrls(projectMergeRequests: MergeRequestData,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* An assignee with his/her issues
|
||||
*/
|
||||
export interface AssigneeWithIssues {
|
||||
assignee: User;
|
||||
issueCounts: {
|
||||
closed: number;
|
||||
opened: number;
|
||||
};
|
||||
issues: Issue[];
|
||||
quota: number;
|
||||
}
|
||||
|
||||
export interface MergeRequestData {
|
||||
[k: string]: Array<{
|
||||
issue_iid: number;
|
||||
web_url: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tangular compiled template
|
||||
*/
|
||||
export type TangularCompiled = (model?: any, $?: any) => string;
|
||||
|
||||
/**
|
||||
* Convert an issue to a markdown table row
|
||||
*
|
||||
* @param issue Issue to convert
|
||||
* @param projectBranches Map of branches for project IDs
|
||||
* @param projectMergeRequests Map of merge requests for project IDs
|
||||
* @param template Template to render this issue with
|
||||
*/
|
||||
function issueToString(issue: any,
|
||||
projectBranches: any,
|
||||
projectMergeRequests: any,
|
||||
template: any): string {
|
||||
issue._labels = issue.labels.map((label: string) => {
|
||||
// print specific labels bold
|
||||
if (BOLD_LABELS.indexOf(label) >= 0) {
|
||||
label = '__' + label + '__';
|
||||
}
|
||||
|
||||
return label;
|
||||
}).join(', ');
|
||||
|
||||
/* tslint:disable: max-line-length */
|
||||
return template({
|
||||
branchExists: typeof projectBranches[issue.project_id] !== 'undefined' && projectBranches[issue.project_id].indexOf(issue.iid) >= 0,
|
||||
issue: issue,
|
||||
// take the first URL from the merge request urls array (usually if there are URLs, then there is that only one)
|
||||
mergeRequestUrl: getMergeRequestUrls(projectMergeRequests, issue.project_id, issue.iid)[0],
|
||||
project: issue.web_url.replace('https://gitlab.com/', '').split('/issues/')[0],
|
||||
weeksOpen: moment().diff(moment(issue.created_at), 'weeks'),
|
||||
});
|
||||
/* tslint:enable */
|
||||
}
|
||||
|
||||
/**
|
||||
* Get issues from all groups with a specific milestone
|
||||
*
|
||||
* @param api GitLab API to make requests with
|
||||
* @param label Label to filter by
|
||||
* @param groups List of groups to get issues for
|
||||
*/
|
||||
export async function getIssues(api: Api, label: string): Promise<Issue[]> {
|
||||
const issueResults = await asyncPool(2, GROUPS, (groupId) => {
|
||||
export async function getIssues(api: Api, label: string, groups: number[]): Promise<Issue[]> {
|
||||
const issueResults = await asyncPool(2, groups, (groupId) => {
|
||||
return api.getIssues({
|
||||
groupId: groupId,
|
||||
});
|
||||
@@ -142,7 +130,7 @@ export async function getIssues(api: Api, label: string): Promise<Issue[]> {
|
||||
* @param api GitLab API To make requests with
|
||||
* @param projects List of projects
|
||||
*/
|
||||
export async function getIdsOfIssuesWithBranchesForProjects(
|
||||
export async function getIssueBranches(
|
||||
api: Api,
|
||||
projects: Project[]): Promise<{ [k: string]: number[] }> {
|
||||
const projectBranches: { [k: string]: number[] } = {};
|
||||
@@ -164,14 +152,28 @@ export async function getIdsOfIssuesWithBranchesForProjects(
|
||||
}
|
||||
|
||||
/**
|
||||
* Group a list of issues by their assignees
|
||||
* Get issues grouped by assignees
|
||||
*
|
||||
* @param issues List of issues to group
|
||||
* @param api GitLab API to make requests with
|
||||
* @param label Label to generate report for
|
||||
*/
|
||||
export function groupIssuesByAssignee(issues: Issue[]): { [k: number]: AssigneeWithIssues } {
|
||||
const issuesByAssignee: {
|
||||
[k: number]: AssigneeWithIssues;
|
||||
} = {};
|
||||
export async function getIssuesGroupedByAssignees(api: Api, label: string): Promise<AssigneeWithIssues[]> {
|
||||
const issuesByAssignee: IssuesGroupedByAssigneeId = {};
|
||||
|
||||
const groups = flatten2dArray(await asyncPool(2, GROUPS, async (groupId) => {
|
||||
return (await api.getSubGroupsForGroup(groupId)).map((group) => {
|
||||
return group.id;
|
||||
});
|
||||
}));
|
||||
groups.push.apply(groups, GROUPS);
|
||||
|
||||
const [issues, projects] = await Promise.all([
|
||||
getIssues(api, label, groups),
|
||||
getProjects(api, groups),
|
||||
]);
|
||||
|
||||
const issueBranches = await getIssueBranches(api, projects);
|
||||
const mergeRequests = await getMergeRequests(api, projects);
|
||||
|
||||
issues.forEach((issue) => {
|
||||
if (issue.assignee === null) {
|
||||
@@ -195,12 +197,33 @@ export function groupIssuesByAssignee(issues: Issue[]): { [k: number]: AssigneeW
|
||||
issue.state = IssueState.OPENED;
|
||||
}
|
||||
|
||||
const issueWithMeta: IssueWithMeta = {
|
||||
...issue,
|
||||
...{
|
||||
$branchExists: typeof issueBranches[issue.project_id] !== 'undefined'
|
||||
&& issueBranches[issue.project_id].indexOf(issue.iid) >= 0,
|
||||
$issue: issue,
|
||||
$labels: issue.labels.map((issueLabel: string) => {
|
||||
// print specific labels bold
|
||||
if (BOLD_LABELS.indexOf(issueLabel) >= 0) {
|
||||
issueLabel = '__' + issueLabel + '__';
|
||||
}
|
||||
|
||||
return issueLabel;
|
||||
}).join(', '),
|
||||
$mergeRequestUrl: getMergeRequestUrls(mergeRequests, issue.project_id, issue.iid)[0],
|
||||
$project: issue.web_url.replace('https://gitlab.com/', '').split('/issues/')[0],
|
||||
$weeksOpen: moment().diff(moment(issue.created_at), 'weeks'),
|
||||
},
|
||||
};
|
||||
|
||||
if (issueStateIsOpenedOrClosed(issue.state)) {
|
||||
issuesByAssignee[issue.assignee.id].issueCounts[issue.state]++;
|
||||
issuesByAssignee[issue.assignee.id].issues.push(issue);
|
||||
issuesByAssignee[issue.assignee.id].issues.push(issueWithMeta);
|
||||
}
|
||||
});
|
||||
|
||||
// calculate quota
|
||||
Object.keys(issuesByAssignee).forEach((_assigneeId) => {
|
||||
const assigneeId = parseInt(_assigneeId, 10);
|
||||
|
||||
@@ -211,25 +234,37 @@ export function groupIssuesByAssignee(issues: Issue[]): { [k: number]: AssigneeW
|
||||
);
|
||||
});
|
||||
|
||||
return issuesByAssignee;
|
||||
}
|
||||
// sort issues by weight of labels and status
|
||||
Object.keys(issuesByAssignee).forEach((_assigneeId) => {
|
||||
const assigneeId = parseInt(_assigneeId, 10);
|
||||
|
||||
/**
|
||||
* Compile templates
|
||||
*/
|
||||
export function compileTemplates(): { [k: string]: TangularCompiled } {
|
||||
const tangular: { compile: (template: string) => TangularCompiled } = require('tangular');
|
||||
issuesByAssignee[assigneeId].issues.sort((a, b) => {
|
||||
let weightA = 0;
|
||||
let weightB = 0;
|
||||
|
||||
const templates = {
|
||||
assigneeFooter: tangular.compile(readFileSync('templates/md/assigneeFooter.md').toString()),
|
||||
assigneeHeader: tangular.compile(readFileSync('templates/md/assigneeHeader.md').toString()),
|
||||
header: tangular.compile(readFileSync('templates/md/header.md').toString()),
|
||||
issue: tangular.compile(readFileSync('templates/md/issue.md').toString()),
|
||||
};
|
||||
Object.keys(LABEL_WEIGHTS).forEach((issueLabel) => {
|
||||
if (a.labels.indexOf(issueLabel) >= 0) {
|
||||
weightA += LABEL_WEIGHTS[issueLabel];
|
||||
}
|
||||
|
||||
logger.log('Compiled templates.');
|
||||
if (b.labels.indexOf(issueLabel) >= 0) {
|
||||
weightB += LABEL_WEIGHTS[issueLabel];
|
||||
}
|
||||
});
|
||||
|
||||
return templates;
|
||||
if (a.state === IssueState.CLOSED) {
|
||||
weightA -= 10;
|
||||
}
|
||||
|
||||
if (b.state === IssueState.CLOSED) {
|
||||
weightB -= 10;
|
||||
}
|
||||
|
||||
return weightB - weightA;
|
||||
});
|
||||
});
|
||||
|
||||
return Object.values(issuesByAssignee);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,9 +296,9 @@ export function getNextMeetingDay() {
|
||||
* @param api GitLab API to make requests with
|
||||
* @param projects List of projects
|
||||
*/
|
||||
export async function getMergeRequestsForProjects(api: Api,
|
||||
projects: Project[]): Promise<MergeRequestData> {
|
||||
const projectMergeRequests: MergeRequestData = {};
|
||||
export async function getMergeRequests(api: Api,
|
||||
projects: Project[]): Promise<MergeRequestsForProjects> {
|
||||
const projectMergeRequests: MergeRequestsForProjects = {};
|
||||
|
||||
// iterate over projects
|
||||
await asyncPool(2, projects, async (project) => {
|
||||
@@ -292,72 +327,41 @@ export async function getMergeRequestsForProjects(api: Api,
|
||||
return projectMergeRequests;
|
||||
}
|
||||
|
||||
export async function generateReport(api: Api, label: string): Promise<string> {
|
||||
const templates = compileTemplates();
|
||||
/**
|
||||
* Generate a report
|
||||
*
|
||||
* @param api GitLab API to make requests with
|
||||
* @param label Label to generate report for
|
||||
* @param template Template to generate report with
|
||||
*/
|
||||
export async function generateReport(api: Api, label: string, template: string): Promise<string> {
|
||||
const issuesGroupedByAssignee = await getIssuesGroupedByAssignees(api, label);
|
||||
|
||||
const groupIds: number[] = [];
|
||||
groupIds.push.apply(groupIds, GROUPS);
|
||||
groupIds.push.apply(groupIds, (await getSubGroups(api, GROUPS)).map((group) => group.id));
|
||||
const structureForTemplate: StructureForTemplate = {
|
||||
issuesByAssignee: issuesGroupedByAssignee,
|
||||
meetingDay: getNextMeetingDay(),
|
||||
};
|
||||
|
||||
logger.log(`Getting data for ${groupIds.length} group(s).`);
|
||||
|
||||
const projects = await getProjects(api, groupIds);
|
||||
const issues = await getIssues(api, label);
|
||||
|
||||
const issuesGroupedByAssignee = groupIssuesByAssignee(issues);
|
||||
|
||||
const issueBranches = await getIdsOfIssuesWithBranchesForProjects(api, projects);
|
||||
const mergeRequests = await getMergeRequestsForProjects(api, projects);
|
||||
|
||||
let markdown = templates.header();
|
||||
|
||||
Object.keys(issuesGroupedByAssignee).forEach((_assigneeId) => {
|
||||
const assigneeId = parseInt(_assigneeId, 10);
|
||||
|
||||
markdown += templates.assigneeHeader(issuesGroupedByAssignee[assigneeId]);
|
||||
|
||||
issuesGroupedByAssignee[assigneeId].issues.sort((a, b) => {
|
||||
let weightA = 0;
|
||||
let weightB = 0;
|
||||
|
||||
Object.keys(LABEL_WEIGHTS).forEach((issueLabel) => {
|
||||
if (a.labels.indexOf(issueLabel) >= 0) {
|
||||
weightA += LABEL_WEIGHTS[issueLabel];
|
||||
}
|
||||
|
||||
if (b.labels.indexOf(issueLabel) >= 0) {
|
||||
weightB += LABEL_WEIGHTS[issueLabel];
|
||||
}
|
||||
});
|
||||
|
||||
if (a.state === IssueState.CLOSED) {
|
||||
weightA -= 10;
|
||||
}
|
||||
|
||||
if (b.state === IssueState.CLOSED) {
|
||||
weightB -= 10;
|
||||
}
|
||||
|
||||
return weightB - weightA;
|
||||
}).forEach((issue) => {
|
||||
markdown += issueToString(
|
||||
issue,
|
||||
issueBranches,
|
||||
mergeRequests,
|
||||
templates.issue,
|
||||
);
|
||||
});
|
||||
|
||||
markdown += templates.assigneeFooter(issuesGroupedByAssignee[assigneeId]);
|
||||
});
|
||||
|
||||
return markdown;
|
||||
return render(
|
||||
template,
|
||||
structureForTemplate,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a markdown report
|
||||
*
|
||||
* @param api GitLab API to make requests with
|
||||
* @param label Label to generate report for
|
||||
*/
|
||||
export async function report(api: Api, label: string) {
|
||||
const meetingDay = getNextMeetingDay();
|
||||
|
||||
const markdown = generateReport(api, label);
|
||||
const markdown = await generateReport(
|
||||
api,
|
||||
label,
|
||||
(await readFilePromisified(resolve(__dirname, '..', '..', 'templates', 'report.md.mustache'))).toString(),
|
||||
);
|
||||
|
||||
let filename = join(cwd(), 'reports', meetingDay + '.md');
|
||||
|
||||
@@ -365,7 +369,7 @@ export async function report(api: Api, label: string) {
|
||||
filename = join(cwd(), 'reports', `${label}.md`);
|
||||
}
|
||||
|
||||
await asyncWriteFile(filename, markdown);
|
||||
await writeFilePromisified(filename, markdown);
|
||||
|
||||
logger.ok(`Wrote file '${filename}'.`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user