From 9ce696e624dd52c2ebc1c494887643dd5e27178c Mon Sep 17 00:00:00 2001 From: AlexeySafronov Date: Mon, 4 Oct 2021 17:08:03 +0300 Subject: [PATCH 1/8] Web: Studio: Init Debug Info + Added About version from package.json + Fix DocumentServer version --- .gitignore | 3 +- config/nginx/onlyoffice.conf | 2 +- lerna.json | 1 + package.json | 4 +- packages/asc-web-common/api/settings/index.js | 8 + .../asc-web-common/store/SettingsStore.js | 23 +- packages/debug-info/package.json | 32 ++ packages/debug-info/src/commits.js | 167 ++++++ packages/debug-info/src/index.js | 9 + packages/debug-info/src/releases.js | 157 ++++++ packages/debug-info/src/remote.js | 96 ++++ packages/debug-info/src/run.js | 203 ++++++++ packages/debug-info/src/tags.js | 111 ++++ packages/debug-info/src/template.js | 98 ++++ packages/debug-info/src/utils.js | 132 +++++ packages/debug-info/templates/compact.hbs | 30 ++ packages/debug-info/templates/debuginfo.hbs | 12 + packages/debug-info/templates/json.hbs | 1 + .../debug-info/templates/keepachangelog.hbs | 39 ++ web/ASC.Web.Client/package.json | 1 + .../NavMenu/sub-components/header-nav.js | 43 +- .../components/pages/About/AboutContent.js | 7 +- .../src/components/pages/About/AboutDialog.js | 6 +- .../src/components/pages/About/index.js | 17 +- .../src/components/pages/DebugInfo/index.js | 65 +++ web/ASC.Web.Client/webpack.config.js | 11 + yarn.lock | 477 +++++++++++++++++- 27 files changed, 1723 insertions(+), 32 deletions(-) create mode 100644 packages/debug-info/package.json create mode 100644 packages/debug-info/src/commits.js create mode 100644 packages/debug-info/src/index.js create mode 100644 packages/debug-info/src/releases.js create mode 100644 packages/debug-info/src/remote.js create mode 100644 packages/debug-info/src/run.js create mode 100644 packages/debug-info/src/tags.js create mode 100644 packages/debug-info/src/template.js create mode 100644 packages/debug-info/src/utils.js create mode 100644 packages/debug-info/templates/compact.hbs create mode 100644 packages/debug-info/templates/debuginfo.hbs create mode 100644 packages/debug-info/templates/json.hbs create mode 100644 packages/debug-info/templates/keepachangelog.hbs create mode 100644 web/ASC.Web.Client/src/components/pages/DebugInfo/index.js diff --git a/.gitignore b/.gitignore index f656810b2d4..f3cef92b8a1 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,5 @@ Data/ Logs/ **/.DS_Store .eslintcache -build/deploy/ \ No newline at end of file +build/deploy/ +/public/debuginfo.md diff --git a/config/nginx/onlyoffice.conf b/config/nginx/onlyoffice.conf index 315a5e79660..ce0983dc9f3 100644 --- a/config/nginx/onlyoffice.conf +++ b/config/nginx/onlyoffice.conf @@ -83,7 +83,7 @@ server { location / { proxy_pass http://localhost:5001; - location ~* /(manifest.json|sw.js|appIcon.png|bg-error.png|favicon.ico) { + location ~* /(manifest.json|sw.js|appIcon.png|bg-error.png|favicon.ico|debuginfo.md) { root $public_root; try_files /$basename /index.html =404; } diff --git a/lerna.json b/lerna.json index c44f1b23d6a..5ef065cbbf2 100644 --- a/lerna.json +++ b/lerna.json @@ -5,6 +5,7 @@ "packages/asc-web-components", "packages/asc-web-common", "packages/browserslist-config-asc", + "packages/debug-info", "web/ASC.Web.Login", "web/ASC.Web.Client", "web/ASC.Web.Editor", diff --git a/package.json b/package.json index 3f34216e2a3..4f57dc406ff 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "packages/asc-web-components", "packages/asc-web-common", "packages/browserslist-config-asc", + "packages/debug-info", "web/ASC.Web.Login", "web/ASC.Web.Client", "web/ASC.Web.Editor", @@ -44,7 +45,8 @@ "sw-studio-login-replace": "replace-in-files --string='studio/login/' --replacement='login/' build/deploy/public/sw.js", "sw-studio-replace": "replace-in-files --string='studio/client/' --replacement='/' build/deploy/public/sw.js", "test": "yarn workspace @appserver/components test", - "wipe": "shx rm -rf node_modules yarn.lock web/**/node_modules products/**/node_modules" + "wipe": "shx rm -rf node_modules yarn.lock web/**/node_modules products/**/node_modules", + "debug-info": "node packages/debug-info/src/index.js --unreleased-only --template debuginfo --output public/debuginfo.md" }, "devDependencies": { "browserslist": "^4.17.1", diff --git a/packages/asc-web-common/api/settings/index.js b/packages/asc-web-common/api/settings/index.js index 3b290779878..686db38ccef 100644 --- a/packages/asc-web-common/api/settings/index.js +++ b/packages/asc-web-common/api/settings/index.js @@ -323,3 +323,11 @@ export function getCommonThirdPartyList() { }; return request(options); } + +export function getBuildVersion() { + const options = { + method: "get", + url: "/settings/version/build.json", + }; + return request(options); +} diff --git a/packages/asc-web-common/store/SettingsStore.js b/packages/asc-web-common/store/SettingsStore.js index ae8c02fd776..069e6a5d23b 100644 --- a/packages/asc-web-common/store/SettingsStore.js +++ b/packages/asc-web-common/store/SettingsStore.js @@ -4,6 +4,7 @@ import { ARTICLE_PINNED_KEY, LANGUAGE } from "../constants"; import { combineUrl } from "../utils"; import FirebaseHelper from "../utils/firebase"; import { AppServerConfig } from "../constants"; +import { version } from "../package.json"; const { proxyURL } = AppServerConfig; class SettingsStore { @@ -85,6 +86,10 @@ class SettingsStore { measurementId: "", }; version = ""; + buildVersionInfo = { + appServer: version, + documentServer: "6.4.1", + }; constructor() { makeAutoObservable(this); @@ -183,7 +188,7 @@ class SettingsStore { init = async () => { this.setIsLoading(true); - await this.getPortalSettings(); + await Promise.all([this.getPortalSettings(), this.getBuildVersionInfo()]); this.setIsLoading(false); this.setIsLoaded(true); @@ -329,6 +334,22 @@ class SettingsStore { window.firebaseHelper = new FirebaseHelper(this.firebase); return window.firebaseHelper; } + + getBuildVersionInfo = async () => { + const versionInfo = await api.settings.getBuildVersion(); + this.setBuildVersionInfo(versionInfo); + }; + + setBuildVersionInfo = (versionInfo) => { + this.buildVersionInfo = { + ...this.buildVersionInfo, + appServer: version, + ...versionInfo, + }; + + if (!this.buildVersionInfo.documentServer) + this.buildVersionInfo.documentServer = "6.4.1"; + }; } export default SettingsStore; diff --git a/packages/debug-info/package.json b/packages/debug-info/package.json new file mode 100644 index 00000000000..415e0a3d3c0 --- /dev/null +++ b/packages/debug-info/package.json @@ -0,0 +1,32 @@ +{ + "name": "@appserver/debuginfo", + "version": "0.0.1", + "description": "Command line tool for generating debug info by commit history", + "main": "./src/index.js", + "engines": { + "node": ">=8.3" + }, + "dependencies": { + "commander": "^7.2.0", + "handlebars": "^4.7.7", + "node-fetch": "^2.6.5", + "parse-github-url": "^1.0.2", + "semver": "^7.3.5" + }, + "nyc": { + "all": true, + "include": "src", + "exclude": "src/index.js", + "sourceMap": false, + "instrument": false, + "report-dir": "./coverage", + "temp-dir": "./coverage/.nyc_output", + "require": [ + "@babel/register" + ], + "reporter": [ + "text", + "html" + ] + } +} diff --git a/packages/debug-info/src/commits.js b/packages/debug-info/src/commits.js new file mode 100644 index 00000000000..a7bace9d254 --- /dev/null +++ b/packages/debug-info/src/commits.js @@ -0,0 +1,167 @@ +const semver = require("semver"); +const { + cmd, + isLink, + encodeHTML, + niceDate, + replaceText, + getGitVersion, +} = require("./utils"); + +const COMMIT_SEPARATOR = "__AUTO_CHANGELOG_COMMIT_SEPARATOR__"; +const MESSAGE_SEPARATOR = "__AUTO_CHANGELOG_MESSAGE_SEPARATOR__"; +const MATCH_COMMIT = /(.*)\n(.*)\n(.*)\n(.*)\n([\S\s]+)/; +const MATCH_STATS = /(\d+) files? changed(?:, (\d+) insertions?...)?(?:, (\d+) deletions?...)?/; +const BODY_FORMAT = "%B"; +const FALLBACK_BODY_FORMAT = "%s%n%n%b"; + +// https://help.github.com/articles/closing-issues-via-commit-messages +const DEFAULT_FIX_PATTERN = /(?:close[sd]?|fixe?[sd]?|resolve[sd]?)\s(?:#(\d+)|(https?:\/\/.+?\/(?:issues|pull|pull-requests|merge_requests)\/(\d+)))/gi; + +const MERGE_PATTERNS = [ + /^Merge pull request #(\d+) from .+\n\n(.+)/, // Regular GitHub merge + /^(.+) \(#(\d+)\)(?:$|\n\n)/, // Github squash merge + /^Merged in .+ \(pull request #(\d+)\)\n\n(.+)/, // BitBucket merge + /^Merge branch .+ into .+\n\n(.+)[\S\s]+See merge request [^!]*!(\d+)/, // GitLab merge +]; + +const fetchCommits = async (diff, options = {}) => { + const format = await getLogFormat(); + const log = await cmd( + `git log ${diff} --shortstat --pretty=format:${format} ${options.appendGitLog}` + ); + return parseCommits(log, options); +}; + +const getLogFormat = async () => { + const gitVersion = await getGitVersion(); + const bodyFormat = + gitVersion && semver.gte(gitVersion, "1.7.2") + ? BODY_FORMAT + : FALLBACK_BODY_FORMAT; + return `${COMMIT_SEPARATOR}%H%n%ai%n%an%n%ae%n${bodyFormat}${MESSAGE_SEPARATOR}`; +}; + +const parseCommits = (string, options = {}) => { + return string + .split(COMMIT_SEPARATOR) + .slice(1) + .map((commit) => parseCommit(commit, options)) + .filter((commit) => filterCommit(commit, options)); +}; + +const parseCommit = (commit, options = {}) => { + const [, hash, date, author, email, tail] = commit.match(MATCH_COMMIT); + const [body, stats] = tail.split(MESSAGE_SEPARATOR); + const message = encodeHTML(body); + const parsed = { + hash, + shorthash: hash.slice(0, 7), + author, + email, + date: new Date(date).toISOString(), + niceDate: niceDate(new Date(date)), + subject: replaceText(getSubject(message), options), + message: message.trim(), + fixes: getFixes(message, author, options), + href: options.getCommitLink(hash), + breaking: + !!options.breakingPattern && + new RegExp(options.breakingPattern).test(message), + ...getStats(stats), + }; + return { + ...parsed, + merge: getMerge(parsed, message, options), + }; +}; + +const getSubject = (message) => { + if (!message.trim()) { + return "_No commit message_"; + } + return message.match(/[^\n]+/)[0]; +}; + +const getStats = (stats) => { + if (!stats.trim()) return {}; + const [, files, insertions, deletions] = stats.match(MATCH_STATS); + return { + files: parseInt(files || 0), + insertions: parseInt(insertions || 0), + deletions: parseInt(deletions || 0), + }; +}; + +const getFixes = (message, author, options = {}) => { + const pattern = getFixPattern(options); + const fixes = []; + let match = pattern.exec(message); + if (!match) return null; + while (match) { + const id = getFixID(match); + const href = isLink(match[2]) ? match[2] : options.getIssueLink(id); + fixes.push({ id, href, author }); + match = pattern.exec(message); + } + return fixes; +}; + +const getFixID = (match) => { + // Get the last non-falsey value in the match array + for (let i = match.length; i >= 0; i--) { + if (match[i]) { + return match[i]; + } + } +}; + +const getFixPattern = (options) => { + if (options.issuePattern) { + return new RegExp(options.issuePattern, "g"); + } + return DEFAULT_FIX_PATTERN; +}; + +const getMergePatterns = (options) => { + if (options.mergePattern) { + return MERGE_PATTERNS.concat(new RegExp(options.mergePattern, "g")); + } + return MERGE_PATTERNS; +}; + +const getMerge = (commit, message, options = {}) => { + const patterns = getMergePatterns(options); + for (const pattern of patterns) { + const match = pattern.exec(message); + if (match) { + const id = /^\d+$/.test(match[1]) ? match[1] : match[2]; + const message = /^\d+$/.test(match[1]) ? match[2] : match[1]; + return { + id, + message: replaceText(message, options), + href: options.getMergeLink(id), + author: commit.author, + commit, + }; + } + } + return null; +}; + +const filterCommit = (commit, { ignoreCommitPattern }) => { + if ( + ignoreCommitPattern && + new RegExp(ignoreCommitPattern).test(commit.subject) + ) { + return false; + } + return true; +}; + +module.exports = { + COMMIT_SEPARATOR, + MESSAGE_SEPARATOR, + fetchCommits, + parseCommit, +}; diff --git a/packages/debug-info/src/index.js b/packages/debug-info/src/index.js new file mode 100644 index 00000000000..31a9319135c --- /dev/null +++ b/packages/debug-info/src/index.js @@ -0,0 +1,9 @@ +#!/usr/bin/env node + +const { run } = require('./run') + +run(process.argv).catch(error => { + console.log('\n') + console.error(error) + process.exit(1) +}) diff --git a/packages/debug-info/src/releases.js b/packages/debug-info/src/releases.js new file mode 100644 index 00000000000..5ebc9fd852b --- /dev/null +++ b/packages/debug-info/src/releases.js @@ -0,0 +1,157 @@ +const semver = require("semver"); +const { fetchCommits } = require("./commits"); + +const MERGE_COMMIT_PATTERN = /^Merge (remote-tracking )?branch '.+'/; +const COMMIT_MESSAGE_PATTERN = /\n+([\S\s]+)/; + +const parseReleases = async (tags, options, onParsed) => { + const releases = await Promise.all( + tags.map(async (tag) => { + const commits = await fetchCommits(tag.diff, options); + const merges = commits + .filter((commit) => commit.merge) + .map((commit) => commit.merge); + const fixes = commits + .filter((commit) => commit.fixes) + .map((commit) => ({ fixes: commit.fixes, commit })); + const emptyRelease = merges.length === 0 && fixes.length === 0; + const { message } = commits[0] || { message: null }; + const breakingCount = commits.filter((c) => c.breaking).length; + const filteredCommits = commits + .filter(filterCommits(merges)) + .sort(sortCommits(options)) + .slice(0, getCommitLimit(options, emptyRelease, breakingCount)); + + if (onParsed) onParsed(tag); + + const origins = commits.sort(sortCommits(options)); + + return { + ...tag, + summary: getSummary(message, options), + commits: filteredCommits, + origins, + groups: getGroups(commits), + merges, + fixes, + }; + }) + ); + return releases.filter(filterReleases(options)); +}; + +const getGroups = (commits) => { + const grouped = commits.reduce((groups, commit) => { + const niceDate = commit.niceDate; + if (!groups[niceDate]) { + groups[niceDate] = []; + } + if (!commit.merge && !MERGE_COMMIT_PATTERN.test(commit.subject)) + groups[niceDate].push(commit); + + return groups; + }, {}); + + // Edit: to add it in the array format instead + const groupArrays = Object.keys(grouped) + .map((niceDate) => { + return { + niceDate, + authors: getGroupedByAuthor(grouped[niceDate]), + }; + }) + .filter((g) => g.authors.length > 0) + .slice(0, 30); // last 30 days only + + return groupArrays; +}; + +const getGroupedByAuthor = (commits) => { + const grouped = commits.reduce((groups, commit) => { + const { author } = commit; + if (!groups[author]) { + groups[author] = []; + } + groups[author].push(commit); + + return groups; + }, {}); + + const groupArrays = Object.keys(grouped).map((author) => { + return { + author, + commits: grouped[author], + }; + }); + + return groupArrays; +}; + +const filterCommits = (merges) => (commit) => { + if (commit.fixes || commit.merge) { + // Filter out commits that already appear in fix or merge lists + return false; + } + if (commit.breaking) { + return true; + } + if (semver.valid(commit.subject)) { + // Filter out version commits + return false; + } + if (MERGE_COMMIT_PATTERN.test(commit.subject)) { + // Filter out merge commits + return false; + } + if (merges.findIndex((m) => m.message === commit.subject) !== -1) { + // Filter out commits with the same message as an existing merge + return false; + } + return true; +}; + +const sortCommits = ({ sortCommits }) => (a, b) => { + if (!a.breaking && b.breaking) return 1; + if (a.breaking && !b.breaking) return -1; + if (sortCommits === "date") return new Date(a.date) - new Date(b.date); + if (sortCommits === "date-desc") return new Date(b.date) - new Date(a.date); + if (sortCommits === "subject") return a.subject.localeCompare(b.subject); + if (sortCommits === "subject-desc") return b.subject.localeCompare(a.subject); + return b.insertions + b.deletions - (a.insertions + a.deletions); +}; + +const getCommitLimit = ( + { commitLimit, backfillLimit }, + emptyRelease, + breakingCount +) => { + if (commitLimit === false) { + return undefined; // Return all commits + } + const limit = emptyRelease ? backfillLimit : commitLimit; + return Math.max(breakingCount, limit); +}; + +const getSummary = (message, { releaseSummary }) => { + if (!message || !releaseSummary) { + return null; + } + if (COMMIT_MESSAGE_PATTERN.test(message)) { + return message.match(COMMIT_MESSAGE_PATTERN)[1]; + } + return null; +}; + +const filterReleases = (options) => ({ merges, fixes, commits }) => { + if ( + options.hideEmptyReleases && + merges.length + fixes.length + commits.length === 0 + ) { + return false; + } + return true; +}; + +module.exports = { + parseReleases, +}; diff --git a/packages/debug-info/src/remote.js b/packages/debug-info/src/remote.js new file mode 100644 index 00000000000..667fd57a018 --- /dev/null +++ b/packages/debug-info/src/remote.js @@ -0,0 +1,96 @@ +const parseRepoURL = require('parse-github-url') +const { cmd } = require('./utils') + +const fetchRemote = async options => { + const remoteURL = await cmd(`git config --get remote.${options.remote}.url`) + return getRemote(remoteURL, options) +} + +const getRemote = (remoteURL, options = {}) => { + const overrides = getOverrides(options) + if (!remoteURL) { + // No point warning if everything is overridden + if (Object.keys(overrides).length !== 4) { + console.warn(`Warning: Git remote ${options.remote} was not found`) + } + return { + getCommitLink: () => null, + getIssueLink: () => null, + getMergeLink: () => null, + getCompareLink: () => null, + ...overrides + } + } + const remote = parseRepoURL(remoteURL) + const protocol = remote.protocol === 'http:' ? 'http:' : 'https:' + const hostname = remote.hostname || remote.host + + const IS_BITBUCKET = /bitbucket/.test(hostname) + const IS_GITLAB = /gitlab/.test(hostname) + const IS_GITLAB_SUBGROUP = /\.git$/.test(remote.branch) + const IS_AZURE = /dev\.azure/.test(hostname) + const IS_VISUAL_STUDIO = /visualstudio/.test(hostname) + + if (IS_BITBUCKET) { + const url = `${protocol}//${hostname}/${remote.repo}` + return { + getCommitLink: id => `${url}/commits/${id}`, + getIssueLink: id => `${url}/issues/${id}`, + getMergeLink: id => `${url}/pull-requests/${id}`, + getCompareLink: (from, to) => `${url}/compare/${to}..${from}`, + ...overrides + } + } + + if (IS_GITLAB) { + const url = IS_GITLAB_SUBGROUP + ? `${protocol}//${hostname}/${remote.repo}/${remote.branch.replace(/\.git$/, '')}` + : `${protocol}//${hostname}/${remote.repo}` + return { + getCommitLink: id => `${url}/commit/${id}`, + getIssueLink: id => `${url}/issues/${id}`, + getMergeLink: id => `${url}/merge_requests/${id}`, + getCompareLink: (from, to) => `${url}/compare/${from}...${to}`, + ...overrides + } + } + + if (IS_AZURE || IS_VISUAL_STUDIO) { + const url = IS_AZURE + ? `${protocol}//${hostname}/${remote.path}` + : `${protocol}//${hostname}/${remote.repo}/${remote.branch}` + const project = IS_AZURE + ? `${protocol}//${hostname}/${remote.repo}` + : `${protocol}//${hostname}/${remote.owner}` + return { + getCommitLink: id => `${url}/commit/${id}`, + getIssueLink: id => `${project}/_workitems/edit/${id}`, + getMergeLink: id => `${url}/pullrequest/${id}`, + getCompareLink: (from, to) => `${url}/branches?baseVersion=GT${to}&targetVersion=GT${from}&_a=commits`, + ...overrides + } + } + + const url = `${protocol}//${hostname}/${remote.repo}` + return { + getCommitLink: id => `${url}/commit/${id}`, + getIssueLink: id => `${url}/issues/${id}`, + getMergeLink: id => `${url}/pull/${id}`, + getCompareLink: (from, to) => `${url}/compare/${from}...${to}`, + ...overrides + } +} + +const getOverrides = ({ commitUrl, issueUrl, mergeUrl, compareUrl }) => { + const overrides = {} + if (commitUrl) overrides.getCommitLink = id => commitUrl.replace('{id}', id) + if (issueUrl) overrides.getIssueLink = id => issueUrl.replace('{id}', id) + if (mergeUrl) overrides.getMergeLink = id => mergeUrl.replace('{id}', id) + if (compareUrl) overrides.getCompareLink = (from, to) => compareUrl.replace('{from}', from).replace('{to}', to) + return overrides +} + +module.exports = { + fetchRemote, + getRemote +} diff --git a/packages/debug-info/src/run.js b/packages/debug-info/src/run.js new file mode 100644 index 00000000000..1e1840a9e1e --- /dev/null +++ b/packages/debug-info/src/run.js @@ -0,0 +1,203 @@ +const { Command } = require("commander"); +const { version } = require("../package.json"); +const { fetchRemote } = require("./remote"); +const { fetchTags } = require("./tags"); +const { parseReleases } = require("./releases"); +const { compileTemplate } = require("./template"); +const { + parseLimit, + readFile, + readJson, + writeFile, + fileExists, + updateLog, + formatBytes, +} = require("./utils"); + +const DEFAULT_OPTIONS = { + output: "CHANGELOG.md", + template: "compact", + remote: "origin", + commitLimit: 3, + backfillLimit: 3, + tagPrefix: "", + sortCommits: "relevance", + appendGitLog: "", + appendGitTag: "", + config: ".debug-info", +}; + +const PACKAGE_FILE = "package.json"; +const PACKAGE_OPTIONS_KEY = "debug-info"; +const PREPEND_TOKEN = ""; + +const getOptions = async (argv) => { + const commandOptions = new Command() + .option( + "-o, --output ", + `output file, default: ${DEFAULT_OPTIONS.output}` + ) + .option( + "-c, --config ", + `config file location, default: ${DEFAULT_OPTIONS.config}` + ) + .option( + "-t, --template