@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.
@@ -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')
@@ -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