@aaronshaf/ger 1.2.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc.js ADDED
@@ -0,0 +1,12 @@
1
+ export default {
2
+ env: {
3
+ browser: true,
4
+ es2022: true,
5
+ },
6
+ extends: "eslint:recommended",
7
+ parserOptions: {
8
+ ecmaVersion: "latest",
9
+ sourceType: "module",
10
+ },
11
+ rules: {},
12
+ };
package/README.md CHANGED
@@ -1,2 +1,9 @@
1
- # ger
2
- gerrit cli
1
+ ## Usage
2
+
3
+ ```bash
4
+ # list branches with gerrit info
5
+ ger branch
6
+
7
+ # list branches with expanded gerrit info
8
+ ger branch -v
9
+ ```
package/bin/ger ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-undef */
3
+
4
+ const path = require("path");
5
+ const { spawnSync } = require("child_process");
6
+
7
+ const projectDir = path.resolve(__dirname, "..");
8
+ const tsNodePath = path.resolve(projectDir, "node_modules", ".bin", "ts-node");
9
+ const gerTsPath = path.resolve(projectDir, "src", "ger.ts");
10
+
11
+ const args = process.argv.slice(2);
12
+
13
+ const result = spawnSync(tsNodePath, [gerTsPath, ...args], {
14
+ stdio: "inherit",
15
+ shell: true,
16
+ });
17
+
18
+ process.exit(result.status);
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@aaronshaf/ger",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Gerrit CLI",
5
- "main": "ger.js",
5
+ "main": "src/ger.ts",
6
6
  "bin": {
7
- "ger": "./bin/ger.sh"
7
+ "ger": "./bin/ger"
8
8
  },
9
9
  "repository": {
10
10
  "type": "git",
@@ -18,5 +18,16 @@
18
18
  "bugs": {
19
19
  "url": "https://github.com/aaronshaf/ger/issues"
20
20
  },
21
- "homepage": "https://github.com/aaronshaf/ger#readme"
21
+ "homepage": "https://github.com/aaronshaf/ger#readme",
22
+ "devDependencies": {
23
+ "@commander-js/extra-typings": "^10.0.3",
24
+ "@types/node": "^18.16.1",
25
+ "cli-table3": "^0.6.3",
26
+ "eslint": "^8.39.0",
27
+ "ts-node": "^10.9.1",
28
+ "typescript": "^5.0.4"
29
+ },
30
+ "dependencies": {
31
+ "commander": "^10.0.1"
32
+ }
22
33
  }
@@ -0,0 +1,176 @@
1
+ import { execSync } from "child_process";
2
+ import { createInterface } from "readline";
3
+ import {
4
+ abbreviateApproverDescription,
5
+ bold,
6
+ darkBrown,
7
+ getGerritData,
8
+ getLocalBranchData,
9
+ getLocalBranchNames,
10
+ green,
11
+ grey,
12
+ normalizeApproverName,
13
+ normalizeValue,
14
+ yellow,
15
+ } from "../utils";
16
+ import type {
17
+ LocalBranchDataMap,
18
+ LocalBranchData,
19
+ GerritDataMap,
20
+ } from "../types.d";
21
+
22
+ const Table = require("cli-table3");
23
+
24
+ export function branch({
25
+ v: isVerbose,
26
+ d: shouldPromptForMergedBranchDeletion,
27
+ }: {
28
+ v?: boolean;
29
+ d?: boolean;
30
+ }) {
31
+ const localBranchNameList = getLocalBranchNames();
32
+ const localBranchDataMap = getLocalBranchData(localBranchNameList);
33
+ const changeIds: string[] = Object.values<LocalBranchData>(localBranchDataMap)
34
+ .map((b) => b.changeId)
35
+ .filter(Boolean) as string[];
36
+ const gerritOutputByChangeId = getGerritData(changeIds);
37
+
38
+ const mergedBranches = localBranchNameList.filter((name) => {
39
+ const changeId = localBranchDataMap[name].changeId;
40
+ if (changeId) {
41
+ const gerritData = gerritOutputByChangeId[changeId];
42
+ return gerritData?.status === "MERGED";
43
+ }
44
+ return false;
45
+ });
46
+
47
+ output(
48
+ localBranchNameList,
49
+ localBranchDataMap,
50
+ gerritOutputByChangeId,
51
+ Boolean(isVerbose)
52
+ );
53
+
54
+ if (shouldPromptForMergedBranchDeletion) {
55
+ promptForMergedBranchDeletion(mergedBranches);
56
+ }
57
+ }
58
+
59
+ function output(
60
+ localBranchNameList: string[],
61
+ localBranchDataMap: LocalBranchDataMap,
62
+ gerritOutputMap: GerritDataMap,
63
+ isVerbose: boolean
64
+ ) {
65
+ const tableOptions = {
66
+ chars: {
67
+ top: "",
68
+ "top-mid": "",
69
+ "top-left": "",
70
+ "top-right": "",
71
+ bottom: "",
72
+ "bottom-mid": "",
73
+ "bottom-left": "",
74
+ "bottom-right": "",
75
+ left: "",
76
+ "left-mid": "",
77
+ mid: isVerbose ? "─" : "",
78
+ "mid-mid": "",
79
+ right: "",
80
+ "right-mid": "",
81
+ middle: " ",
82
+ },
83
+ style: { "padding-left": 0, "padding-right": 0 },
84
+ };
85
+ const table = new Table(tableOptions);
86
+
87
+ for (const localBranch of localBranchNameList) {
88
+ const changeId = localBranchDataMap[localBranch].changeId;
89
+ if (changeId && gerritOutputMap[changeId]) {
90
+ const gerritData = gerritOutputMap[changeId];
91
+ const subject = gerritData.subject;
92
+ const isWip = gerritData.wip || subject.toLowerCase().startsWith("WIP");
93
+ const isMerged = gerritData.status === "MERGED";
94
+ const isAbandoned = gerritData.status === "ABANDONED";
95
+ const labels = [
96
+ isWip && darkBrown("WIP"),
97
+ isAbandoned && grey("Abandoned"),
98
+ isMerged && green("Merged"),
99
+ ]
100
+ .filter(Boolean)
101
+ .map((str) => `[${str}]`) as string[];
102
+
103
+ const approvals = gerritData.currentPatchSet.approvals.map((a) => {
104
+ let description = a.description;
105
+ if (a.type === "SUBM" && !description) {
106
+ description = "Submitted";
107
+ }
108
+ return isVerbose
109
+ ? `${description || a.type}: ${normalizeValue(
110
+ a.value
111
+ )} (${normalizeApproverName(a.by.name)})`
112
+ : `${abbreviateApproverDescription(
113
+ description || a.type
114
+ )}${normalizeValue(a.value)}`;
115
+ });
116
+
117
+ const approvalText = approvals.join(isVerbose ? "\n" : ", ");
118
+
119
+ if (isVerbose) {
120
+ table.push([
121
+ `${bold(localBranch)}\n${localBranchDataMap[localBranch].shortHash}`,
122
+ labels.join(" "),
123
+ `${subject}\n${yellow(gerritData.url.replace("https://", ""))}`,
124
+ approvalText,
125
+ ]);
126
+ } else {
127
+ table.push([
128
+ localBranch,
129
+ labels.join(" "),
130
+ yellow(String(gerritData.number)),
131
+ subject,
132
+ approvalText,
133
+ ]);
134
+ }
135
+ } else {
136
+ if (isVerbose) {
137
+ table.push([
138
+ `${bold(localBranch)}\n${localBranchDataMap[localBranch].shortHash}`,
139
+ "",
140
+ localBranchDataMap[localBranch].subject,
141
+ "",
142
+ ]);
143
+ } else {
144
+ table.push([
145
+ localBranch,
146
+ "",
147
+ "",
148
+ localBranchDataMap[localBranch].subject,
149
+ "",
150
+ ]);
151
+ }
152
+ }
153
+ }
154
+ console.log(table.toString());
155
+ }
156
+
157
+ async function promptForMergedBranchDeletion(mergedBranches: string[]) {
158
+ const readline = createInterface({
159
+ input: process.stdin,
160
+ output: process.stdout,
161
+ });
162
+
163
+ for (const mergedLocalBranch of mergedBranches) {
164
+ const answer: string = await new Promise((resolve) => {
165
+ readline.question(
166
+ `Delete merged local branch ${bold(mergedLocalBranch)}? (y/n) `,
167
+ resolve
168
+ );
169
+ });
170
+ if (answer.toLowerCase() === "y") {
171
+ execSync(`git branch -D ${mergedLocalBranch}`);
172
+ console.log(` Deleted ${bold(mergedLocalBranch)}`);
173
+ }
174
+ }
175
+ process.exit(0);
176
+ }
package/src/ger.ts ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ts-node
2
+
3
+ import { Command } from '@commander-js/extra-typings';
4
+ import { branch } from "./commands/branch";
5
+
6
+ const version = require("../package.json").version;
7
+
8
+ const program = new Command();
9
+ program.name("ger").description("Gerrit CLI").version(version);
10
+
11
+ program
12
+ .command("branch")
13
+ .description("Get a list of local branches with Gerrit info")
14
+ .option("-v", "Show extra information, i.e. approvals")
15
+ .action((
16
+ _program: any,
17
+ ) => {
18
+ branch(_program);
19
+ });
20
+
21
+ program.parse();
package/src/types.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ export type LocalBranchData = {
2
+ changeId: string | null;
3
+ shortHash: string
4
+ subject: string;
5
+ }
6
+
7
+ export type LocalBranchDataMap = {
8
+ [branchName: string]: LocalBranchData;
9
+ }
10
+
11
+ export type GerritApproval = {
12
+ type: 'SUBM';
13
+ description: string;
14
+ value: string;
15
+ by: {
16
+ name: string;
17
+ }
18
+ }
19
+
20
+ export type GerritData = {
21
+ currentPatchSet: {
22
+ approvals: GerritApproval[]
23
+ }
24
+ id: string;
25
+ status: 'MERGED' | 'ABANDONED';
26
+ subject: string;
27
+ url: string;
28
+ wip: boolean;
29
+ number: number
30
+ }
31
+
32
+ export type GerritDataMap = {
33
+ [changeId: string]: GerritData;
34
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,133 @@
1
+ import { execSync } from "child_process";
2
+ import {
3
+ GerritData,
4
+ GerritDataMap,
5
+ LocalBranchData,
6
+ LocalBranchDataMap,
7
+ } from "./types.d";
8
+
9
+ type Row = string[];
10
+
11
+ export function bold(text: string) {
12
+ return `\x1b[1m${text}\x1b[0m`;
13
+ }
14
+
15
+ // grey background
16
+ export function grey(text: string) {
17
+ return `\x1b[47m\x1b[30m${text}\x1b[0m`;
18
+ }
19
+
20
+ // brown with transparent background
21
+ export function darkBrown(text: string) {
22
+ // return `\x1b[43m\x1b[30m${text}\x1b[0m`;
23
+ return `\x1b[33m${text}\x1b[0m`;
24
+ }
25
+
26
+ export function green(text: string) {
27
+ return `\x1b[42m\x1b[30m${text}\x1b[0m`;
28
+ }
29
+
30
+ // yellow text with transparent background
31
+ export function yellow(text: string) {
32
+ // return `\x1b[33m\x1b[40m${text}\x1b[0m`;
33
+ return `\x1b[33m${text}\x1b[0m`;
34
+ }
35
+
36
+ export function getChangeIdFromCommitMessage(message: string) {
37
+ // Extract the Change-Id
38
+ const regex = /Change-Id: .{41}/;
39
+ const change_id_match = message.match(regex);
40
+
41
+ if (change_id_match) {
42
+ const change_id = change_id_match[0].substring(11);
43
+ return change_id;
44
+ }
45
+ return null;
46
+ }
47
+
48
+ export function getLocalBranchNames(): string[] {
49
+ // Extract local branch names
50
+ const stdout = execSync(
51
+ `git for-each-ref --sort=committerdate refs/heads/ --format='%(refname:lstrip=2)' | grep -v '^master$'`
52
+ );
53
+ return stdout.toString().trim().split("\n");
54
+ }
55
+
56
+ export function getGerritData(changeIds: string[]) {
57
+ const query = changeIds.map((cid) => `change:${cid}`).join(" OR ");
58
+
59
+ // if query is empty
60
+ if (!query) {
61
+ console.log("No change IDs found");
62
+ process.exit(0);
63
+ }
64
+
65
+ // Run the gerrit query command
66
+ const gerrit_output = execSync(
67
+ `ssh gerrit "gerrit query --current-patch-set --format=JSON '${query}'"`
68
+ )
69
+ .toString()
70
+ .trim()
71
+ .split("\n")
72
+ .map((line) => JSON.parse(line) as GerritData);
73
+
74
+ return gerrit_output.reduce<GerritDataMap>((acc, change) => {
75
+ return {
76
+ ...acc,
77
+ [change.id]: change,
78
+ };
79
+ }, {});
80
+ }
81
+
82
+ export function getLocalBranchData(branchNames: string[]) {
83
+ return branchNames.reduce<LocalBranchDataMap>((acc, branchName) => {
84
+ // Get the latest commit SHA and message
85
+ const gitLogOutput = execSync(`git log -1 --pretty=format:%h%n%B ${branchName}`)
86
+ .toString()
87
+ .trim();
88
+
89
+ // Split the output into commit SHA and message
90
+ const [shortHash, ...commitMessageLines] = gitLogOutput.split("\n");
91
+
92
+ const localBranchData: LocalBranchData = {
93
+ changeId: getChangeIdFromCommitMessage(commitMessageLines.join("\n")) || null,
94
+ subject: commitMessageLines[0],
95
+ shortHash,
96
+ };
97
+
98
+ return {
99
+ ...acc,
100
+ [branchName]: localBranchData,
101
+ };
102
+ }, {});
103
+ }
104
+
105
+ export const normalizeValue = (value: string) => {
106
+ if (value === "1" || value === "2") {
107
+ return `+${value}`;
108
+ }
109
+ return value;
110
+ };
111
+
112
+ export function normalizeApproverName(name: string) {
113
+ return name
114
+ .replace(" (Bot)", "")
115
+ .replace("Service Cloud Jenkins", "Jenkins")
116
+ .replace("Larry Gergich", "Gergich");
117
+ }
118
+
119
+ export function abbreviateApproverDescription(name: string) {
120
+ if (name === "Lint-Review") {
121
+ return `Lint `;
122
+ }
123
+ if (name === "Code-Review") {
124
+ return `CR `;
125
+ }
126
+ if (name === "Product-Review") {
127
+ return `PR `;
128
+ }
129
+ if (name === "QA-Review") {
130
+ return `QA `;
131
+ }
132
+ return `${name} `;
133
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "module": "commonjs",
5
+ "noEmit": true,
6
+ "strict": true,
7
+ },
8
+ "include": ["src/**/*"],
9
+ "exclude": ["node_modules"]
10
+ }
package/bin/ger.sh DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- require('../ger.js')
@@ -1,132 +0,0 @@
1
- const {execSync} = require('child_process')
2
-
3
- // Extract local branch names
4
- const stdout = execSync(
5
- `git for-each-ref --sort=committerdate refs/heads/ --format='%(refname:lstrip=2)' | grep -v '^master$'`
6
- )
7
- const localBranches = stdout.toString().trim().split('\n')
8
-
9
- const localBranchToChangeId = {}
10
-
11
- // Initialize an empty query string
12
- let query = ''
13
-
14
- localBranches.forEach(branch => {
15
- // Get the latest commit message
16
- const commit_message = execSync(`git log -1 --pretty=%B ${branch}`).toString().trim()
17
-
18
- // Extract the Change-Id
19
- const regex = /Change-Id: .{41}/
20
- const change_id_match = commit_message.match(regex)
21
-
22
- if (change_id_match) {
23
- const change_id = change_id_match[0].substring(11)
24
- localBranchToChangeId[branch] = change_id
25
-
26
- // Append the Change-Id to the query string
27
- query += `change:${change_id} OR `
28
- } else {
29
- localBranchToChangeId[branch] = null
30
- }
31
- })
32
-
33
- // if query is empty
34
- if (!query) {
35
- console.log('No change IDs found')
36
- process.exit(0)
37
- }
38
-
39
- // Remove the trailing " OR "
40
- query = query.replace(/ OR $/, '')
41
-
42
- // Run the gerrit query command
43
- const gerrit_output = execSync(
44
- `ssh gerrit "gerrit query --current-patch-set --format=JSON '${query}'"`
45
- )
46
- .toString()
47
- .trim()
48
- .split('\n')
49
- .map(line => JSON.parse(line))
50
-
51
- const gerritOutputByChangeId = {}
52
- for (const change of gerrit_output) {
53
- gerritOutputByChangeId[change.id] = change
54
- }
55
-
56
- function bold(text) {
57
- return `\x1b[1m${text}\x1b[0m`
58
- }
59
- // grey background
60
- function grey(text) {
61
- return `\x1b[47m\x1b[30m${text}\x1b[0m`
62
- }
63
- function darkBrown(text) {
64
- return `\x1b[43m\x1b[30m${text}\x1b[0m`
65
- }
66
- function green(text) {
67
- return `\x1b[42m\x1b[30m${text}\x1b[0m`
68
- }
69
- // function red(text) {
70
- // return `\x1b[41m\x1b[30m${text}\x1b[0m`
71
- // }
72
-
73
- const mergedBranches = localBranches.filter(localBranch => {
74
- const changeId = localBranchToChangeId[localBranch]
75
- const gerritData = gerritOutputByChangeId[changeId]
76
- return gerritData?.status === 'MERGED'
77
- })
78
-
79
- for (const localBranch of localBranches) {
80
- const changeId = localBranchToChangeId[localBranch]
81
- if (changeId && gerritOutputByChangeId[changeId]) {
82
- const gerritData = gerritOutputByChangeId[changeId]
83
- const isWip = gerritData.wip || gerritData.subject.toLowerCase().startsWith('WIP')
84
- const isMerged = gerritData.status === 'MERGED'
85
- const isAbandoned = gerritData.status === 'ABANDONED'
86
- const labels = [
87
- isWip && darkBrown('WIP'),
88
- isAbandoned && grey('Abandoned'),
89
- isMerged && green('Merged'),
90
- ].filter(Boolean)
91
- console.log(`${bold(localBranch)}: ${gerritData.subject} ${labels.join(' ')}`)
92
- console.log(` ${gerritData.url.replace('https://', '')}`)
93
- for (const approval of gerritData.currentPatchSet.approvals) {
94
- let description = approval.description
95
- if (approval.type === 'SUBM' && !description) {
96
- description = 'Submitted'
97
- }
98
- console.log(
99
- ` ${description || approval.type}: ${approval.value} (${approval.by.name.replace(
100
- ' (Bot)',
101
- ''
102
- )})`
103
- )
104
- }
105
- } else {
106
- console.log(`${localBranch}`)
107
- }
108
- // blank line
109
- console.log('')
110
- }
111
-
112
- // if -d flag is passed
113
- if (process.argv[3] === '-d') {
114
- async function promptForMergedBranchDeletion() {
115
- const readline = require('readline').createInterface({
116
- input: process.stdin,
117
- output: process.stdout,
118
- })
119
-
120
- for (const mergedLocalBranch of mergedBranches) {
121
- const answer = await new Promise(resolve => {
122
- readline.question(`Delete merged local branch ${bold(mergedLocalBranch)}? (y/n) `, resolve)
123
- })
124
- if (answer.toLowerCase() === 'y') {
125
- execSync(`git branch -D ${mergedLocalBranch}`)
126
- console.log(` Deleted ${bold(mergedLocalBranch)}`)
127
- }
128
- }
129
- process.exit(0)
130
- }
131
- promptForMergedBranchDeletion()
132
- }
package/ger.js DELETED
@@ -1,5 +0,0 @@
1
- const branch = require('./commands/branch.js')
2
-
3
- if (process.argv[2] === 'branch') {
4
- branch()
5
- }