@aaronshaf/ger 0.3.2 → 0.3.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/EXAMPLES.md CHANGED
@@ -117,6 +117,54 @@ const program = pipe(
117
117
  Effect.runPromise(program).catch(console.error)
118
118
  ```
119
119
 
120
+ ### 2b. Search Changes with Query Syntax
121
+
122
+ ```typescript
123
+ import { Effect, pipe } from 'effect'
124
+ import {
125
+ GerritApiService,
126
+ GerritApiServiceLive,
127
+ ConfigServiceLive,
128
+ } from '@aaronshaf/ger'
129
+
130
+ const searchChanges = (query: string, limit = 25) =>
131
+ Effect.gen(function* () {
132
+ const api = yield* GerritApiService
133
+
134
+ // Use Gerrit query syntax to search changes
135
+ const fullQuery = query.includes('limit:') ? query : `${query} limit:${limit}`
136
+ const changes = yield* api.listChanges(fullQuery)
137
+
138
+ // Group by project for organized output
139
+ const byProject = new Map<string, typeof changes>()
140
+ for (const change of changes) {
141
+ const existing = byProject.get(change.project) ?? []
142
+ existing.push(change)
143
+ byProject.set(change.project, existing)
144
+ }
145
+
146
+ console.log(`Found ${changes.length} changes:`)
147
+ for (const [project, projectChanges] of byProject) {
148
+ console.log(`\n${project}:`)
149
+ for (const change of projectChanges) {
150
+ console.log(` #${change._number} - ${change.subject} (${change.status})`)
151
+ }
152
+ }
153
+
154
+ return changes
155
+ })
156
+
157
+ // Example queries
158
+ const program = pipe(
159
+ // Search for merged changes in the last week
160
+ searchChanges('status:merged age:7d', 10),
161
+ Effect.provide(GerritApiServiceLive),
162
+ Effect.provide(ConfigServiceLive)
163
+ )
164
+
165
+ Effect.runPromise(program).catch(console.error)
166
+ ```
167
+
120
168
  ### 3. Post a Comment
121
169
 
122
170
  ```typescript
package/README.md CHANGED
@@ -49,9 +49,16 @@ ger show 12345
49
49
  # Add a comment
50
50
  ger comment 12345 -m "LGTM"
51
51
 
52
+ # Add reviewers to a change
53
+ ger add-reviewer user@example.com -c 12345
54
+
52
55
  # Get diff for review
53
56
  ger diff 12345
54
57
 
58
+ # Search for changes using Gerrit query syntax
59
+ ger search "owner:self status:open"
60
+ ger search "project:my-project" -n 10
61
+
55
62
  # Extract URLs from messages (e.g., Jenkins build links)
56
63
  ger extract-url "build-summary-report" | tail -1
57
64
 
@@ -98,6 +105,54 @@ ger workspace
98
105
  ger workspace --pretty
99
106
  ```
100
107
 
108
+ ### Search Changes
109
+
110
+ Search for changes across your Gerrit instance using native query syntax:
111
+
112
+ ```bash
113
+ # Search for all open changes (default)
114
+ ger search
115
+
116
+ # Search for your open changes
117
+ ger search "owner:self status:open"
118
+
119
+ # Search for changes by a specific user
120
+ ger search "owner:john@example.com"
121
+
122
+ # Search by project
123
+ ger search "project:my-project status:open"
124
+
125
+ # Search with date filters
126
+ ger search "owner:self after:2025-01-01"
127
+ ger search "status:merged age:7d"
128
+
129
+ # Combine filters
130
+ ger search "owner:self status:merged before:2025-06-01"
131
+
132
+ # Limit results (default: 25)
133
+ ger search "project:my-project" -n 10
134
+
135
+ # XML output for automation
136
+ ger search "owner:self" --xml
137
+ ```
138
+
139
+ #### Common query operators:
140
+ | Operator | Description |
141
+ |----------|-------------|
142
+ | `owner:USER` | Changes owned by USER (use 'self' for yourself) |
143
+ | `status:STATE` | open, merged, abandoned, closed |
144
+ | `project:NAME` | Changes in a specific project |
145
+ | `branch:NAME` | Changes targeting a branch |
146
+ | `age:TIME` | Time since last update (e.g., 1d, 2w, 1mon) |
147
+ | `before:DATE` | Changes modified before date (YYYY-MM-DD) |
148
+ | `after:DATE` | Changes modified after date (YYYY-MM-DD) |
149
+ | `is:wip` | Work-in-progress changes |
150
+ | `is:submittable` | Changes ready to submit |
151
+ | `reviewer:USER` | Changes where USER is a reviewer |
152
+ | `label:NAME=VALUE` | Filter by label (e.g., label:Code-Review+2) |
153
+
154
+ See the [full query syntax documentation](https://gerrit-review.googlesource.com/Documentation/user-search.html).
155
+
101
156
  ### Comments
102
157
 
103
158
  #### Overall Comments
@@ -359,6 +414,38 @@ ger abandon 12345
359
414
  ger abandon 12345 -m "Reason"
360
415
  ```
361
416
 
417
+ ### Add Reviewers
418
+
419
+ Add reviewers or CCs to a change:
420
+
421
+ ```bash
422
+ # Add a single reviewer
423
+ ger add-reviewer user@example.com -c 12345
424
+
425
+ # Add multiple reviewers
426
+ ger add-reviewer user1@example.com user2@example.com -c 12345
427
+
428
+ # Add as CC instead of reviewer
429
+ ger add-reviewer --cc user@example.com -c 12345
430
+
431
+ # Suppress email notifications
432
+ ger add-reviewer --notify none user@example.com -c 12345
433
+
434
+ # XML output for automation
435
+ ger add-reviewer user@example.com -c 12345 --xml
436
+ ```
437
+
438
+ #### Options:
439
+ - `-c, --change <id>` - Change ID (required)
440
+ - `--cc` - Add as CC instead of reviewer
441
+ - `--notify <level>` - Notification level: `none`, `owner`, `owner_reviewers`, `all` (default: `all`)
442
+ - `--xml` - XML output for LLM/automation consumption
443
+
444
+ #### Notes:
445
+ - Both email addresses and usernames are accepted
446
+ - Multiple reviewers can be added in a single command
447
+ - Use `--cc` for carbon copy (notified but not required to review)
448
+
362
449
  ### AI-Powered Review
363
450
 
364
451
  The `ger review` command provides automated code review using AI tools (claude, llm, or opencode CLI).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aaronshaf/ger",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Gerrit CLI and SDK - A modern CLI tool and TypeScript SDK for Gerrit Code Review, built with Effect-TS",
5
5
  "keywords": [
6
6
  "gerrit",
package/src/api/gerrit.ts CHANGED
@@ -9,6 +9,8 @@ import {
9
9
  FileInfo,
10
10
  type GerritCredentials,
11
11
  type ReviewInput,
12
+ type ReviewerInput,
13
+ ReviewerResult,
12
14
  RevisionInfo,
13
15
  } from '@/schemas/gerrit'
14
16
  import { filterMeaningfulMessages } from '@/utils/message-filters'
@@ -50,6 +52,11 @@ export interface GerritApiServiceImpl {
50
52
  revisionId?: string,
51
53
  ) => Effect.Effect<Record<string, readonly CommentInfo[]>, ApiError>
52
54
  readonly getMessages: (changeId: string) => Effect.Effect<readonly MessageInfo[], ApiError>
55
+ readonly addReviewer: (
56
+ changeId: string,
57
+ reviewer: string,
58
+ options?: { state?: 'REVIEWER' | 'CC'; notify?: 'NONE' | 'OWNER' | 'OWNER_REVIEWERS' | 'ALL' },
59
+ ) => Effect.Effect<ReviewerResult, ApiError>
53
60
  }
54
61
 
55
62
  // Export both the tag value and the type for use in Effect requirements
@@ -485,6 +492,26 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
485
492
  return changeResponse.messages || []
486
493
  }).pipe(Effect.map(filterMeaningfulMessages))
487
494
 
495
+ const addReviewer = (
496
+ changeId: string,
497
+ reviewer: string,
498
+ options?: {
499
+ state?: 'REVIEWER' | 'CC'
500
+ notify?: 'NONE' | 'OWNER' | 'OWNER_REVIEWERS' | 'ALL'
501
+ },
502
+ ) =>
503
+ Effect.gen(function* () {
504
+ const { credentials, authHeader } = yield* getCredentialsAndAuth
505
+ const normalized = yield* normalizeAndValidate(changeId)
506
+ const url = `${credentials.host}/a/changes/${encodeURIComponent(normalized)}/reviewers`
507
+ const body: ReviewerInput = {
508
+ reviewer,
509
+ ...(options?.state && { state: options.state }),
510
+ ...(options?.notify && { notify: options.notify }),
511
+ }
512
+ return yield* makeRequest(url, authHeader, 'POST', body, ReviewerResult)
513
+ })
514
+
488
515
  return {
489
516
  getChange,
490
517
  listChanges,
@@ -499,6 +526,7 @@ export const GerritApiServiceLive: Layer.Layer<GerritApiService, never, ConfigSe
499
526
  getDiff,
500
527
  getComments,
501
528
  getMessages,
529
+ addReviewer,
502
530
  }
503
531
  }),
504
532
  )
@@ -0,0 +1,135 @@
1
+ import { Effect } from 'effect'
2
+ import { type ApiError, GerritApiService } from '@/api/gerrit'
3
+
4
+ interface AddReviewerOptions {
5
+ change?: string
6
+ cc?: boolean
7
+ notify?: string
8
+ xml?: boolean
9
+ }
10
+
11
+ type NotifyLevel = 'NONE' | 'OWNER' | 'OWNER_REVIEWERS' | 'ALL'
12
+
13
+ const VALID_NOTIFY_LEVELS: ReadonlyArray<NotifyLevel> = ['NONE', 'OWNER', 'OWNER_REVIEWERS', 'ALL']
14
+
15
+ const escapeXml = (str: string): string =>
16
+ str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
17
+
18
+ const outputXmlError = (message: string): void => {
19
+ console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
20
+ console.log(`<add_reviewer_result>`)
21
+ console.log(` <status>error</status>`)
22
+ console.log(` <error><![CDATA[${message}]]></error>`)
23
+ console.log(`</add_reviewer_result>`)
24
+ }
25
+
26
+ class ValidationError extends Error {
27
+ readonly _tag = 'ValidationError'
28
+ }
29
+
30
+ export const addReviewerCommand = (
31
+ reviewers: string[],
32
+ options: AddReviewerOptions = {},
33
+ ): Effect.Effect<void, ApiError | ValidationError, GerritApiService> =>
34
+ Effect.gen(function* () {
35
+ const gerritApi = yield* GerritApiService
36
+
37
+ const changeId = options.change
38
+
39
+ if (!changeId) {
40
+ const message =
41
+ 'Change ID is required. Use -c <change-id> or run from a branch with an active change.'
42
+ if (options.xml) {
43
+ outputXmlError(message)
44
+ } else {
45
+ console.error(`✗ ${message}`)
46
+ }
47
+ return yield* Effect.fail(new ValidationError(message))
48
+ }
49
+
50
+ if (reviewers.length === 0) {
51
+ const message = 'At least one reviewer is required.'
52
+ if (options.xml) {
53
+ outputXmlError(message)
54
+ } else {
55
+ console.error(`✗ ${message}`)
56
+ }
57
+ return yield* Effect.fail(new ValidationError(message))
58
+ }
59
+
60
+ const state: 'REVIEWER' | 'CC' = options.cc ? 'CC' : 'REVIEWER'
61
+ const stateLabel = options.cc ? 'cc' : 'reviewer'
62
+
63
+ let notify: NotifyLevel | undefined
64
+ if (options.notify) {
65
+ const upperNotify = options.notify.toUpperCase()
66
+ if (!VALID_NOTIFY_LEVELS.includes(upperNotify as NotifyLevel)) {
67
+ const message = `Invalid notify level: ${options.notify}. Valid values: none, owner, owner_reviewers, all`
68
+ if (options.xml) {
69
+ outputXmlError(message)
70
+ } else {
71
+ console.error(`✗ ${message}`)
72
+ }
73
+ return yield* Effect.fail(new ValidationError(message))
74
+ }
75
+ notify = upperNotify as NotifyLevel
76
+ }
77
+
78
+ const results: Array<{ reviewer: string; success: boolean; name?: string; error?: string }> = []
79
+
80
+ for (const reviewer of reviewers) {
81
+ const result = yield* Effect.either(
82
+ gerritApi.addReviewer(changeId, reviewer, { state, notify }),
83
+ )
84
+
85
+ if (result._tag === 'Left') {
86
+ const error = result.left
87
+ const message = 'message' in error ? String(error.message) : String(error)
88
+ results.push({ reviewer, success: false, error: message })
89
+ continue
90
+ }
91
+
92
+ const apiResult = result.right
93
+
94
+ if (apiResult.error) {
95
+ results.push({ reviewer, success: false, error: apiResult.error })
96
+ } else {
97
+ const added = apiResult.reviewers?.[0] || apiResult.ccs?.[0]
98
+ const name = added?.name || added?.email || reviewer
99
+ results.push({ reviewer, success: true, name })
100
+ }
101
+ }
102
+
103
+ if (options.xml) {
104
+ console.log(`<?xml version="1.0" encoding="UTF-8"?>`)
105
+ console.log(`<add_reviewer_result>`)
106
+ console.log(` <change_id>${escapeXml(changeId)}</change_id>`)
107
+ console.log(` <state>${escapeXml(stateLabel)}</state>`)
108
+ console.log(` <reviewers>`)
109
+ for (const r of results) {
110
+ if (r.success) {
111
+ console.log(` <reviewer status="added">`)
112
+ console.log(` <input>${escapeXml(r.reviewer)}</input>`)
113
+ console.log(` <name><![CDATA[${r.name}]]></name>`)
114
+ console.log(` </reviewer>`)
115
+ } else {
116
+ console.log(` <reviewer status="failed">`)
117
+ console.log(` <input>${escapeXml(r.reviewer)}</input>`)
118
+ console.log(` <error><![CDATA[${r.error}]]></error>`)
119
+ console.log(` </reviewer>`)
120
+ }
121
+ }
122
+ console.log(` </reviewers>`)
123
+ const allSuccess = results.every((r) => r.success)
124
+ console.log(` <status>${allSuccess ? 'success' : 'partial_failure'}</status>`)
125
+ console.log(`</add_reviewer_result>`)
126
+ } else {
127
+ for (const r of results) {
128
+ if (r.success) {
129
+ console.log(`✓ Added ${r.name} as ${stateLabel}`)
130
+ } else {
131
+ console.error(`✗ Failed to add ${r.reviewer}: ${r.error}`)
132
+ }
133
+ }
134
+ }
135
+ })
@@ -3,6 +3,55 @@ import { type ApiError, GerritApiService } from '@/api/gerrit'
3
3
  import type { MessageInfo } from '@/schemas/gerrit'
4
4
  import { getChangeIdFromHead, GitError, NoChangeIdError } from '@/utils/git-commit'
5
5
 
6
+ /** Help text for build-status command - exported to keep index.ts under line limit */
7
+ export const BUILD_STATUS_HELP_TEXT = `
8
+ This command parses Gerrit change messages to determine build status.
9
+ It looks for "Build Started" messages and subsequent verification labels.
10
+
11
+ Output is JSON with a "state" field that can be:
12
+ - pending: No build has started yet
13
+ - running: Build started but no verification yet
14
+ - success: Build completed with Verified+1
15
+ - failure: Build completed with Verified-1
16
+ - not_found: Change does not exist
17
+
18
+ Exit codes:
19
+ - 0: Default for all states (like gh run watch)
20
+ - 1: Only when --exit-status is used AND build fails
21
+ - 2: Timeout reached in watch mode
22
+ - 3: API/network errors
23
+
24
+ Examples:
25
+ # Single check (current behavior)
26
+ $ ger build-status 392385
27
+ {"state":"success"}
28
+
29
+ # Watch until completion (outputs JSON on each poll)
30
+ $ ger build-status 392385 --watch
31
+ {"state":"pending"}
32
+ {"state":"running"}
33
+ {"state":"running"}
34
+ {"state":"success"}
35
+
36
+ # Watch with custom interval (check every 5 seconds)
37
+ $ ger build-status --watch --interval 5
38
+
39
+ # Watch with custom timeout (60 minutes)
40
+ $ ger build-status --watch --timeout 3600
41
+
42
+ # Exit with code 1 on failure (for CI/CD pipelines)
43
+ $ ger build-status --watch --exit-status && deploy.sh
44
+
45
+ # Trigger notification when done (like gh run watch pattern)
46
+ $ ger build-status --watch && notify-send 'Build is done!'
47
+
48
+ # Parse final state in scripts
49
+ $ ger build-status --watch | tail -1 | jq -r '.state'
50
+ success
51
+
52
+ Note: When no change-id is provided, it will be automatically extracted from the
53
+ Change-ID footer in your HEAD commit.`
54
+
6
55
  // Export types for external use
7
56
  export type BuildState = 'pending' | 'running' | 'success' | 'failure' | 'not_found'
8
57