@aaronshaf/ger 2.0.1 → 2.0.2
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/README.md +16 -0
- package/docs/prd/commands.md +71 -0
- package/llms.txt +217 -0
- package/package.json +1 -1
- package/src/api/gerrit.ts +63 -60
- package/src/cli/commands/comment.ts +18 -0
- package/src/cli/commands/install-hook.ts +59 -0
- package/src/cli/commands/show.ts +9 -0
- package/src/cli/commands/topic.ts +108 -0
- package/src/cli/register-commands.ts +55 -33
- package/src/schemas/gerrit.ts +2 -0
- package/tests/topic.test.ts +443 -0
- package/tests/unit/commands/install-hook.test.ts +258 -0
package/src/cli/commands/show.ts
CHANGED
|
@@ -48,6 +48,7 @@ interface ChangeDetails {
|
|
|
48
48
|
created?: string
|
|
49
49
|
updated?: string
|
|
50
50
|
commitMessage: string
|
|
51
|
+
topic?: string
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
const getChangeDetails = (
|
|
@@ -71,6 +72,7 @@ const getChangeDetails = (
|
|
|
71
72
|
created: change.created,
|
|
72
73
|
updated: change.updated,
|
|
73
74
|
commitMessage: change.subject, // For now, using subject as commit message
|
|
75
|
+
topic: change.topic,
|
|
74
76
|
}
|
|
75
77
|
})
|
|
76
78
|
|
|
@@ -142,6 +144,9 @@ const formatShowPretty = (
|
|
|
142
144
|
console.log(` Project: ${changeDetails.project}`)
|
|
143
145
|
console.log(` Branch: ${changeDetails.branch}`)
|
|
144
146
|
console.log(` Status: ${changeDetails.status}`)
|
|
147
|
+
if (changeDetails.topic) {
|
|
148
|
+
console.log(` Topic: ${changeDetails.topic}`)
|
|
149
|
+
}
|
|
145
150
|
console.log(` Owner: ${changeDetails.owner.name || changeDetails.owner.email || 'Unknown'}`)
|
|
146
151
|
console.log(
|
|
147
152
|
` Created: ${changeDetails.created ? formatDate(changeDetails.created) : 'Unknown'}`,
|
|
@@ -220,6 +225,7 @@ const formatShowJson = async (
|
|
|
220
225
|
status: changeDetails.status,
|
|
221
226
|
project: changeDetails.project,
|
|
222
227
|
branch: changeDetails.branch,
|
|
228
|
+
topic: changeDetails.topic,
|
|
223
229
|
owner: removeUndefined(changeDetails.owner),
|
|
224
230
|
created: changeDetails.created,
|
|
225
231
|
updated: changeDetails.updated,
|
|
@@ -301,6 +307,9 @@ const formatShowXml = async (
|
|
|
301
307
|
xmlParts.push(` <status>${escapeXML(changeDetails.status)}</status>`)
|
|
302
308
|
xmlParts.push(` <project>${escapeXML(changeDetails.project)}</project>`)
|
|
303
309
|
xmlParts.push(` <branch>${escapeXML(changeDetails.branch)}</branch>`)
|
|
310
|
+
if (changeDetails.topic) {
|
|
311
|
+
xmlParts.push(` <topic><![CDATA[${sanitizeCDATA(changeDetails.topic)}]]></topic>`)
|
|
312
|
+
}
|
|
304
313
|
xmlParts.push(` <owner>`)
|
|
305
314
|
if (changeDetails.owner.name) {
|
|
306
315
|
xmlParts.push(` <name><![CDATA[${sanitizeCDATA(changeDetails.owner.name)}]]></name>`)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { Effect } from 'effect'
|
|
2
|
+
import { type ApiError, GerritApiService } from '@/api/gerrit'
|
|
3
|
+
import { sanitizeCDATA } from '@/utils/shell-safety'
|
|
4
|
+
import { getChangeIdFromHead, GitError, NoChangeIdError } from '@/utils/git-commit'
|
|
5
|
+
|
|
6
|
+
export const TOPIC_HELP_TEXT = `
|
|
7
|
+
Examples:
|
|
8
|
+
# View current topic (auto-detect from HEAD)
|
|
9
|
+
$ ger topic
|
|
10
|
+
|
|
11
|
+
# View topic for specific change
|
|
12
|
+
$ ger topic 12345
|
|
13
|
+
|
|
14
|
+
# Set topic on a change
|
|
15
|
+
$ ger topic 12345 my-feature
|
|
16
|
+
|
|
17
|
+
# Remove topic from a change
|
|
18
|
+
$ ger topic 12345 --delete
|
|
19
|
+
$ ger topic --delete # auto-detect from HEAD
|
|
20
|
+
|
|
21
|
+
Note: When no change-id is provided, it will be auto-detected from the HEAD commit.`
|
|
22
|
+
|
|
23
|
+
interface TopicOptions {
|
|
24
|
+
xml?: boolean
|
|
25
|
+
delete?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Manages topic for a Gerrit change.
|
|
30
|
+
*
|
|
31
|
+
* - No topic argument: get current topic
|
|
32
|
+
* - With topic argument: set topic
|
|
33
|
+
* - With --delete flag: remove topic
|
|
34
|
+
*
|
|
35
|
+
* @param changeId - Change number or Change-ID (auto-detects from HEAD if not provided)
|
|
36
|
+
* @param topic - Optional topic to set
|
|
37
|
+
* @param options - Configuration options
|
|
38
|
+
* @returns Effect that completes when the operation finishes
|
|
39
|
+
*/
|
|
40
|
+
export const topicCommand = (
|
|
41
|
+
changeId: string | undefined,
|
|
42
|
+
topic: string | undefined,
|
|
43
|
+
options: TopicOptions = {},
|
|
44
|
+
): Effect.Effect<void, ApiError | GitError | NoChangeIdError, GerritApiService> =>
|
|
45
|
+
Effect.gen(function* () {
|
|
46
|
+
const gerritApi = yield* GerritApiService
|
|
47
|
+
|
|
48
|
+
// Auto-detect Change-ID from HEAD commit if not provided
|
|
49
|
+
const resolvedChangeId = changeId?.trim() || (yield* getChangeIdFromHead())
|
|
50
|
+
|
|
51
|
+
// Handle delete operation
|
|
52
|
+
if (options.delete) {
|
|
53
|
+
yield* gerritApi.deleteTopic(resolvedChangeId)
|
|
54
|
+
|
|
55
|
+
if (options.xml) {
|
|
56
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
57
|
+
console.log(`<topic_result>`)
|
|
58
|
+
console.log(` <status>success</status>`)
|
|
59
|
+
console.log(` <action>deleted</action>`)
|
|
60
|
+
console.log(` <change_id><![CDATA[${sanitizeCDATA(resolvedChangeId)}]]></change_id>`)
|
|
61
|
+
console.log(`</topic_result>`)
|
|
62
|
+
} else {
|
|
63
|
+
console.log(`✓ Removed topic from change ${resolvedChangeId}`)
|
|
64
|
+
}
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Handle set operation
|
|
69
|
+
if (topic !== undefined && topic.trim() !== '') {
|
|
70
|
+
const newTopic = yield* gerritApi.setTopic(resolvedChangeId, topic)
|
|
71
|
+
|
|
72
|
+
if (options.xml) {
|
|
73
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
74
|
+
console.log(`<topic_result>`)
|
|
75
|
+
console.log(` <status>success</status>`)
|
|
76
|
+
console.log(` <action>set</action>`)
|
|
77
|
+
console.log(` <change_id><![CDATA[${sanitizeCDATA(resolvedChangeId)}]]></change_id>`)
|
|
78
|
+
console.log(` <topic><![CDATA[${sanitizeCDATA(newTopic)}]]></topic>`)
|
|
79
|
+
console.log(`</topic_result>`)
|
|
80
|
+
} else {
|
|
81
|
+
console.log(`✓ Set topic on change ${resolvedChangeId}: ${newTopic}`)
|
|
82
|
+
}
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Handle get operation (default)
|
|
87
|
+
const currentTopic = yield* gerritApi.getTopic(resolvedChangeId)
|
|
88
|
+
|
|
89
|
+
if (options.xml) {
|
|
90
|
+
console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
|
|
91
|
+
console.log(`<topic_result>`)
|
|
92
|
+
console.log(` <status>success</status>`)
|
|
93
|
+
console.log(` <action>get</action>`)
|
|
94
|
+
console.log(` <change_id><![CDATA[${sanitizeCDATA(resolvedChangeId)}]]></change_id>`)
|
|
95
|
+
if (currentTopic) {
|
|
96
|
+
console.log(` <topic><![CDATA[${sanitizeCDATA(currentTopic)}]]></topic>`)
|
|
97
|
+
} else {
|
|
98
|
+
console.log(` <topic />`)
|
|
99
|
+
}
|
|
100
|
+
console.log(`</topic_result>`)
|
|
101
|
+
} else {
|
|
102
|
+
if (currentTopic) {
|
|
103
|
+
console.log(currentTopic)
|
|
104
|
+
} else {
|
|
105
|
+
console.log(`No topic set for change ${resolvedChangeId}`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
})
|
|
@@ -9,15 +9,17 @@ import { abandonCommand } from './commands/abandon'
|
|
|
9
9
|
import { restoreCommand } from './commands/restore'
|
|
10
10
|
import { rebaseCommand } from './commands/rebase'
|
|
11
11
|
import { submitCommand } from './commands/submit'
|
|
12
|
+
import { topicCommand, TOPIC_HELP_TEXT } from './commands/topic'
|
|
12
13
|
import { voteCommand } from './commands/vote'
|
|
13
14
|
import { projectsCommand } from './commands/projects'
|
|
14
15
|
import { buildStatusCommand, BUILD_STATUS_HELP_TEXT } from './commands/build-status'
|
|
15
16
|
import { checkoutCommand, CHECKOUT_HELP_TEXT } from './commands/checkout'
|
|
16
|
-
import { commentCommand } from './commands/comment'
|
|
17
|
+
import { commentCommand, COMMENT_HELP_TEXT } from './commands/comment'
|
|
17
18
|
import { commentsCommand } from './commands/comments'
|
|
18
19
|
import { diffCommand } from './commands/diff'
|
|
19
20
|
import { extractUrlCommand } from './commands/extract-url'
|
|
20
21
|
import { incomingCommand } from './commands/incoming'
|
|
22
|
+
import { installHookCommand } from './commands/install-hook'
|
|
21
23
|
import { mineCommand } from './commands/mine'
|
|
22
24
|
import { openCommand } from './commands/open'
|
|
23
25
|
import { pushCommand, PUSH_HELP_TEXT } from './commands/push'
|
|
@@ -106,38 +108,7 @@ export function registerCommands(program: Command): void {
|
|
|
106
108
|
.option('--unresolved', 'Mark comment as unresolved (requires human attention)')
|
|
107
109
|
.option('--batch', 'Read batch comments from stdin as JSON (see examples below)')
|
|
108
110
|
.option('--xml', 'XML output for LLM consumption')
|
|
109
|
-
.addHelpText(
|
|
110
|
-
'after',
|
|
111
|
-
`
|
|
112
|
-
Examples:
|
|
113
|
-
# Post a general comment on a change (using change number)
|
|
114
|
-
$ ger comment 12345 -m "Looks good to me!"
|
|
115
|
-
|
|
116
|
-
# Post a comment using Change-ID
|
|
117
|
-
$ ger comment If5a3ae8cb5a107e187447802358417f311d0c4b1 -m "LGTM"
|
|
118
|
-
|
|
119
|
-
# Post a comment using piped input (useful for multi-line comments or scripts)
|
|
120
|
-
$ echo "This is a comment from stdin!" | ger comment 12345
|
|
121
|
-
$ cat review-notes.txt | ger comment 12345
|
|
122
|
-
|
|
123
|
-
# Post a line-specific comment (line number from NEW file version)
|
|
124
|
-
$ ger comment 12345 --file src/main.js --line 42 -m "Consider using const here"
|
|
125
|
-
|
|
126
|
-
# Post an unresolved comment requiring human attention
|
|
127
|
-
$ ger comment 12345 --file src/api.js --line 15 -m "Security concern" --unresolved
|
|
128
|
-
|
|
129
|
-
# Post multiple comments using batch mode
|
|
130
|
-
$ echo '{"message": "Review complete", "comments": [
|
|
131
|
-
{"file": "src/main.js", "line": 10, "message": "Good refactor"},
|
|
132
|
-
{"file": "src/api.js", "line": 25, "message": "Check error handling", "unresolved": true}
|
|
133
|
-
]}' | ger comment 12345 --batch
|
|
134
|
-
|
|
135
|
-
Note:
|
|
136
|
-
- Both change number (e.g., 12345) and Change-ID (e.g., If5a3ae8...) formats are accepted
|
|
137
|
-
- Line numbers refer to the actual line numbers in the NEW version of the file,
|
|
138
|
-
NOT the line numbers shown in the diff view. To find the correct line number,
|
|
139
|
-
look at the file after all changes have been applied.`,
|
|
140
|
-
)
|
|
111
|
+
.addHelpText('after', COMMENT_HELP_TEXT)
|
|
141
112
|
.action(async (changeId, options) => {
|
|
142
113
|
await executeEffect(
|
|
143
114
|
commentCommand(changeId, options).pipe(
|
|
@@ -316,6 +287,24 @@ Note:
|
|
|
316
287
|
)
|
|
317
288
|
})
|
|
318
289
|
|
|
290
|
+
// topic command
|
|
291
|
+
program
|
|
292
|
+
.command('topic [change-id] [topic]')
|
|
293
|
+
.description('Get, set, or remove topic for a change (auto-detects from HEAD if not specified)')
|
|
294
|
+
.option('--delete', 'Remove the topic from the change')
|
|
295
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
296
|
+
.addHelpText('after', TOPIC_HELP_TEXT)
|
|
297
|
+
.action(async (changeId, topic, options) => {
|
|
298
|
+
await executeEffect(
|
|
299
|
+
topicCommand(changeId, topic, options).pipe(
|
|
300
|
+
Effect.provide(GerritApiServiceLive),
|
|
301
|
+
Effect.provide(ConfigServiceLive),
|
|
302
|
+
),
|
|
303
|
+
options,
|
|
304
|
+
'topic_result',
|
|
305
|
+
)
|
|
306
|
+
})
|
|
307
|
+
|
|
319
308
|
// vote command
|
|
320
309
|
program
|
|
321
310
|
.command('vote <change-id>')
|
|
@@ -528,6 +517,39 @@ Note:
|
|
|
528
517
|
}
|
|
529
518
|
})
|
|
530
519
|
|
|
520
|
+
// install-hook command
|
|
521
|
+
program
|
|
522
|
+
.command('install-hook')
|
|
523
|
+
.description('Install the Gerrit commit-msg hook for automatic Change-Id generation')
|
|
524
|
+
.option('--force', 'Overwrite existing hook')
|
|
525
|
+
.option('--xml', 'XML output for LLM consumption')
|
|
526
|
+
.addHelpText(
|
|
527
|
+
'after',
|
|
528
|
+
`
|
|
529
|
+
Examples:
|
|
530
|
+
# Install the commit-msg hook
|
|
531
|
+
$ ger install-hook
|
|
532
|
+
|
|
533
|
+
# Force reinstall (overwrite existing)
|
|
534
|
+
$ ger install-hook --force
|
|
535
|
+
|
|
536
|
+
Note:
|
|
537
|
+
- Downloads hook from your configured Gerrit server
|
|
538
|
+
- Installs to .git/hooks/commit-msg
|
|
539
|
+
- Makes hook executable (chmod +x)
|
|
540
|
+
- Required for commits to have Change-Id footers`,
|
|
541
|
+
)
|
|
542
|
+
.action(async (options) => {
|
|
543
|
+
await executeEffect(
|
|
544
|
+
installHookCommand(options).pipe(
|
|
545
|
+
Effect.provide(CommitHookServiceLive),
|
|
546
|
+
Effect.provide(ConfigServiceLive),
|
|
547
|
+
),
|
|
548
|
+
options,
|
|
549
|
+
'install_hook_result',
|
|
550
|
+
)
|
|
551
|
+
})
|
|
552
|
+
|
|
531
553
|
// push command
|
|
532
554
|
program
|
|
533
555
|
.command('push')
|
package/src/schemas/gerrit.ts
CHANGED
|
@@ -118,6 +118,7 @@ export const ChangeInfo: Schema.Schema<{
|
|
|
118
118
|
readonly work_in_progress?: boolean
|
|
119
119
|
readonly current_revision?: string
|
|
120
120
|
readonly revisions?: Record<string, RevisionInfoType>
|
|
121
|
+
readonly topic?: string
|
|
121
122
|
}> = Schema.Struct({
|
|
122
123
|
id: Schema.String,
|
|
123
124
|
project: Schema.String,
|
|
@@ -182,6 +183,7 @@ export const ChangeInfo: Schema.Schema<{
|
|
|
182
183
|
work_in_progress: Schema.optional(Schema.Boolean),
|
|
183
184
|
current_revision: Schema.optional(Schema.String),
|
|
184
185
|
revisions: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Any })),
|
|
186
|
+
topic: Schema.optional(Schema.String),
|
|
185
187
|
})
|
|
186
188
|
export type ChangeInfo = Schema.Schema.Type<typeof ChangeInfo>
|
|
187
189
|
|