@minakoto00/skills 0.1.0
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/LICENSE +21 -0
- package/README.md +50 -0
- package/bin/skills.js +58 -0
- package/install-skill.sh +213 -0
- package/package.json +16 -0
- package/skills/review-pr/README.md +163 -0
- package/skills/review-pr/SKILL.md +180 -0
- package/skills/review-pr/docs/examples.md +106 -0
- package/skills/review-pr/scripts/cluster_review_issues.sh +110 -0
- package/skills/review-pr/scripts/common.sh +101 -0
- package/skills/review-pr/scripts/detect_platform.sh +55 -0
- package/skills/review-pr/scripts/fetch_review_comments.sh +301 -0
- package/skills/review-pr/scripts/plan_review_validation_dispatch.sh +74 -0
- package/skills/review-pr/scripts/post_review_comment.sh +89 -0
- package/skills/review-pr/scripts/repo_policy.sh +132 -0
- package/skills/review-pr/scripts/resolve_review_target.sh +160 -0
- package/skills/review-pr/scripts/worktree_sync.sh +118 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Review PR Examples
|
|
2
|
+
|
|
3
|
+
## Latest Open GitLab MR
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bash scripts/resolve_review_target.sh --repo /path/to/repo --latest
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Expected shape:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
platform=gitlab
|
|
13
|
+
number=42
|
|
14
|
+
source_branch=feat/example
|
|
15
|
+
head_sha=<sha>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Specific GitHub PR
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bash scripts/resolve_review_target.sh --repo /path/to/repo --number 128
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Fetch Review Comments
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bash scripts/fetch_review_comments.sh --repo /path/to/repo --number 128 --platform github --json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
If `code-review comments` are present, ask the user whether to include code-review comments in scope.
|
|
31
|
+
|
|
32
|
+
If `discussion comments` are present, ask the user whether to include discussion comments in scope.
|
|
33
|
+
|
|
34
|
+
Only the approved comment categories move forward.
|
|
35
|
+
|
|
36
|
+
Resolved code-review feedback is excluded by default.
|
|
37
|
+
|
|
38
|
+
Outdated threads are validated separately from unresolved threads.
|
|
39
|
+
|
|
40
|
+
Group approved unresolved and outdated code-review feedback into issue clusters before validation.
|
|
41
|
+
|
|
42
|
+
For approved comments, dispatch several subagents in parallel to validate whether the comments still make sense.
|
|
43
|
+
|
|
44
|
+
During that validation, subagents search only within changed files for same-pattern candidates.
|
|
45
|
+
|
|
46
|
+
Those same-pattern candidates are reported separately from the original issues.
|
|
47
|
+
|
|
48
|
+
Then confirm the verification report with the user before planning fixes.
|
|
49
|
+
|
|
50
|
+
## Inspect Repo Policy
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
bash scripts/repo_policy.sh --repo /path/to/repo
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This inspects both `AGENTS.md` and `CLAUDE.md` if they exist. If neither file defines a worktree rule, the fallback is `<repo>/.worktrees`.
|
|
57
|
+
|
|
58
|
+
## Reuse Existing Worktree
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
bash scripts/worktree_sync.sh \
|
|
62
|
+
--repo /path/to/repo \
|
|
63
|
+
--source-branch feat/example \
|
|
64
|
+
--head-sha abcdef1234567890
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If the worktree already exists and is clean, the script fetches and fast-forwards the source branch, then aligns it to the requested MR/PR head SHA.
|
|
68
|
+
|
|
69
|
+
## Create Missing Worktree
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
bash scripts/worktree_sync.sh \
|
|
73
|
+
--repo /path/to/repo \
|
|
74
|
+
--source-branch feat/example \
|
|
75
|
+
--head-sha abcdef1234567890
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
If the worktree does not exist, the script creates it under the repo-approved worktree root and checks out the remote source branch.
|
|
79
|
+
|
|
80
|
+
## Comment-Only Proposal
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bash scripts/post_review_comment.sh \
|
|
84
|
+
--repo /path/to/repo \
|
|
85
|
+
--number 42 \
|
|
86
|
+
--body-file /tmp/review-plan.md
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Suggested comment body structure:
|
|
90
|
+
|
|
91
|
+
```md
|
|
92
|
+
## Change Plan
|
|
93
|
+
|
|
94
|
+
- Summary of review findings
|
|
95
|
+
- Files or subsystems likely to change
|
|
96
|
+
- Patch outline
|
|
97
|
+
- Verification steps
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Installer Dry Run
|
|
101
|
+
|
|
102
|
+
Advanced manual installer:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
bash install-skill.sh --agent codex --scope user-global --method symlink --dry-run
|
|
106
|
+
```
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
|
5
|
+
# shellcheck source=./common.sh
|
|
6
|
+
source "$SCRIPT_DIR/common.sh"
|
|
7
|
+
|
|
8
|
+
input_path=
|
|
9
|
+
|
|
10
|
+
usage() {
|
|
11
|
+
cat <<'USAGE'
|
|
12
|
+
Usage: cluster_review_issues.sh [--input <path>]
|
|
13
|
+
|
|
14
|
+
Read normalized review-comment JSON and group unresolved or outdated code-review comments into issue clusters.
|
|
15
|
+
USAGE
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
while [[ $# -gt 0 ]]; do
|
|
19
|
+
case "$1" in
|
|
20
|
+
--input)
|
|
21
|
+
input_path=$2
|
|
22
|
+
shift 2
|
|
23
|
+
;;
|
|
24
|
+
--help|-h)
|
|
25
|
+
usage
|
|
26
|
+
exit 0
|
|
27
|
+
;;
|
|
28
|
+
*)
|
|
29
|
+
die "unknown argument: $1"
|
|
30
|
+
;;
|
|
31
|
+
esac
|
|
32
|
+
done
|
|
33
|
+
|
|
34
|
+
require_cmd jq
|
|
35
|
+
|
|
36
|
+
if [[ -n "$input_path" ]]; then
|
|
37
|
+
input_json=$(cat "$input_path")
|
|
38
|
+
else
|
|
39
|
+
input_json=$(cat)
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
jq -cn --argjson payload "$input_json" '
|
|
43
|
+
def review_comments:
|
|
44
|
+
$payload.code_review_comments.items // [];
|
|
45
|
+
|
|
46
|
+
def comment_state($comment):
|
|
47
|
+
$comment.thread_state
|
|
48
|
+
// (if ($comment.thread_resolved // false) then "resolved" else "unresolved" end);
|
|
49
|
+
|
|
50
|
+
def cluster_key($comment):
|
|
51
|
+
$comment.cluster_key
|
|
52
|
+
// $comment.issue_key
|
|
53
|
+
// (
|
|
54
|
+
(($comment.path // "") + "::" + ($comment.body // ""))
|
|
55
|
+
| gsub("[[:space:]]+"; " ")
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
def resolved_comments:
|
|
59
|
+
review_comments
|
|
60
|
+
| map(select(comment_state(.) == "resolved"));
|
|
61
|
+
|
|
62
|
+
def active_comments:
|
|
63
|
+
review_comments
|
|
64
|
+
| map(select(comment_state(.) != "resolved"));
|
|
65
|
+
|
|
66
|
+
def issue_clusters:
|
|
67
|
+
active_comments
|
|
68
|
+
| sort_by(cluster_key(.))
|
|
69
|
+
| group_by(cluster_key(.))
|
|
70
|
+
| map(
|
|
71
|
+
. as $cluster_comments
|
|
72
|
+
| {
|
|
73
|
+
cluster_id: cluster_key($cluster_comments[0]),
|
|
74
|
+
comment_count: ($cluster_comments | length),
|
|
75
|
+
comment_ids: ($cluster_comments | map(.id | tostring)),
|
|
76
|
+
thread_ids: ($cluster_comments | map(.thread_id // null) | unique),
|
|
77
|
+
thread_states: ($cluster_comments | map(comment_state(.)) | unique | sort),
|
|
78
|
+
paths: ($cluster_comments | map(.path // "") | unique | sort),
|
|
79
|
+
comments: (
|
|
80
|
+
$cluster_comments
|
|
81
|
+
| map({
|
|
82
|
+
id: (.id | tostring),
|
|
83
|
+
thread_id: (.thread_id // null),
|
|
84
|
+
thread_state: comment_state(.),
|
|
85
|
+
path: (.path // ""),
|
|
86
|
+
body: (.body // "")
|
|
87
|
+
})
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
{
|
|
93
|
+
excluded_resolved_threads: {
|
|
94
|
+
count: (resolved_comments | map(.thread_id // .id) | unique | length),
|
|
95
|
+
comment_count: (resolved_comments | length),
|
|
96
|
+
thread_ids: (resolved_comments | map(.thread_id // .id) | unique | sort)
|
|
97
|
+
},
|
|
98
|
+
active_review_comments: {
|
|
99
|
+
count: (active_comments | length),
|
|
100
|
+
state_counts: {
|
|
101
|
+
unresolved: (active_comments | map(select(comment_state(.) == "unresolved")) | length),
|
|
102
|
+
outdated: (active_comments | map(select(comment_state(.) == "outdated")) | length)
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
issue_clusters: {
|
|
106
|
+
count: (issue_clusters | length),
|
|
107
|
+
items: issue_clusters
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
'
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
script_dir() {
|
|
5
|
+
cd "$(dirname "${BASH_SOURCE[0]}")" && pwd
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
die() {
|
|
9
|
+
printf 'error: %s\n' "$*" >&2
|
|
10
|
+
exit 1
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
warn() {
|
|
14
|
+
printf 'warn: %s\n' "$*" >&2
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
require_cmd() {
|
|
18
|
+
command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
resolve_repo_root() {
|
|
22
|
+
local repo_arg="${1:-.}"
|
|
23
|
+
git -C "$repo_arg" rev-parse --show-toplevel 2>/dev/null || die "not a git repository: $repo_arg"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
origin_url() {
|
|
27
|
+
local repo_root=$1
|
|
28
|
+
git -C "$repo_root" remote get-url origin 2>/dev/null || die "repository has no origin remote: $repo_root"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
trim() {
|
|
32
|
+
local value=$1
|
|
33
|
+
value=${value#"${value%%[![:space:]]*}"}
|
|
34
|
+
value=${value%"${value##*[![:space:]]}"}
|
|
35
|
+
printf '%s' "$value"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
print_kv() {
|
|
39
|
+
local key=$1
|
|
40
|
+
local value=${2-}
|
|
41
|
+
printf '%s=%q\n' "$key" "$value"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
normalize_path() {
|
|
45
|
+
local raw=$1
|
|
46
|
+
python3 - "$raw" <<'PY'
|
|
47
|
+
import os
|
|
48
|
+
import sys
|
|
49
|
+
print(os.path.abspath(os.path.expanduser(sys.argv[1])))
|
|
50
|
+
PY
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
expand_path() {
|
|
54
|
+
local raw=$1
|
|
55
|
+
local repo_root=$2
|
|
56
|
+
|
|
57
|
+
if [[ -z "$raw" ]]; then
|
|
58
|
+
die "cannot expand empty path"
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
if [[ "$raw" == ~* ]]; then
|
|
62
|
+
raw="${raw/#\~/$HOME}"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
case "$raw" in
|
|
66
|
+
/*)
|
|
67
|
+
normalize_path "$raw"
|
|
68
|
+
;;
|
|
69
|
+
./*|../*)
|
|
70
|
+
normalize_path "$repo_root/$raw"
|
|
71
|
+
;;
|
|
72
|
+
*)
|
|
73
|
+
normalize_path "$raw"
|
|
74
|
+
;;
|
|
75
|
+
esac
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
sanitize_branch_name() {
|
|
79
|
+
local branch=$1
|
|
80
|
+
branch=${branch//\//-}
|
|
81
|
+
branch=${branch//:/-}
|
|
82
|
+
branch=${branch// /-}
|
|
83
|
+
printf '%s' "$branch"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
repository_slug_from_remote() {
|
|
87
|
+
local remote_url=$1
|
|
88
|
+
local slug
|
|
89
|
+
|
|
90
|
+
slug=$(printf '%s' "$remote_url" | sed -E 's#^[a-z]+://[^/]+/##; s#^git@[^:]+:##; s#\.git$##')
|
|
91
|
+
if [[ -z "$slug" || "$slug" == "$remote_url" ]]; then
|
|
92
|
+
die "unable to derive repository slug from remote: $remote_url"
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
printf '%s' "$slug"
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
load_kv_output() {
|
|
99
|
+
local output=$1
|
|
100
|
+
eval "$output"
|
|
101
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
|
5
|
+
# shellcheck source=./common.sh
|
|
6
|
+
source "$SCRIPT_DIR/common.sh"
|
|
7
|
+
|
|
8
|
+
repo=.
|
|
9
|
+
remote_url=
|
|
10
|
+
|
|
11
|
+
usage() {
|
|
12
|
+
cat <<'USAGE'
|
|
13
|
+
Usage: detect_platform.sh [--repo <path>] [--remote-url <url>]
|
|
14
|
+
|
|
15
|
+
Detect the remote VCS platform from a repository or explicit remote URL.
|
|
16
|
+
Prints one of: github, gitlab
|
|
17
|
+
USAGE
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
while [[ $# -gt 0 ]]; do
|
|
21
|
+
case "$1" in
|
|
22
|
+
--repo)
|
|
23
|
+
repo=$2
|
|
24
|
+
shift 2
|
|
25
|
+
;;
|
|
26
|
+
--remote-url)
|
|
27
|
+
remote_url=$2
|
|
28
|
+
shift 2
|
|
29
|
+
;;
|
|
30
|
+
--help|-h)
|
|
31
|
+
usage
|
|
32
|
+
exit 0
|
|
33
|
+
;;
|
|
34
|
+
*)
|
|
35
|
+
die "unknown argument: $1"
|
|
36
|
+
;;
|
|
37
|
+
esac
|
|
38
|
+
done
|
|
39
|
+
|
|
40
|
+
if [[ -z "$remote_url" ]]; then
|
|
41
|
+
repo=$(resolve_repo_root "$repo")
|
|
42
|
+
remote_url=$(origin_url "$repo")
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
case "$remote_url" in
|
|
46
|
+
*github.com* )
|
|
47
|
+
printf 'github\n'
|
|
48
|
+
;;
|
|
49
|
+
*gitlab* )
|
|
50
|
+
printf 'gitlab\n'
|
|
51
|
+
;;
|
|
52
|
+
* )
|
|
53
|
+
die "unsupported remote platform: $remote_url"
|
|
54
|
+
;;
|
|
55
|
+
esac
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
|
5
|
+
# shellcheck source=./common.sh
|
|
6
|
+
source "$SCRIPT_DIR/common.sh"
|
|
7
|
+
|
|
8
|
+
repo=.
|
|
9
|
+
platform=
|
|
10
|
+
number=
|
|
11
|
+
json_output=false
|
|
12
|
+
|
|
13
|
+
usage() {
|
|
14
|
+
cat <<'USAGE'
|
|
15
|
+
Usage: fetch_review_comments.sh --repo <path> --number <id> [--platform <github|gitlab>] [--json]
|
|
16
|
+
|
|
17
|
+
Fetch MR/PR comments and normalize them into code-review comments and discussion comments.
|
|
18
|
+
USAGE
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
while [[ $# -gt 0 ]]; do
|
|
22
|
+
case "$1" in
|
|
23
|
+
--repo)
|
|
24
|
+
repo=$2
|
|
25
|
+
shift 2
|
|
26
|
+
;;
|
|
27
|
+
--platform)
|
|
28
|
+
platform=$2
|
|
29
|
+
shift 2
|
|
30
|
+
;;
|
|
31
|
+
--number)
|
|
32
|
+
number=$2
|
|
33
|
+
shift 2
|
|
34
|
+
;;
|
|
35
|
+
--json)
|
|
36
|
+
json_output=true
|
|
37
|
+
shift
|
|
38
|
+
;;
|
|
39
|
+
--help|-h)
|
|
40
|
+
usage
|
|
41
|
+
exit 0
|
|
42
|
+
;;
|
|
43
|
+
*)
|
|
44
|
+
die "unknown argument: $1"
|
|
45
|
+
;;
|
|
46
|
+
esac
|
|
47
|
+
done
|
|
48
|
+
|
|
49
|
+
[[ -n "$number" ]] || die "--number is required"
|
|
50
|
+
|
|
51
|
+
repo=$(resolve_repo_root "$repo")
|
|
52
|
+
require_cmd jq
|
|
53
|
+
|
|
54
|
+
if [[ -z "$platform" ]]; then
|
|
55
|
+
platform=$("$SCRIPT_DIR/detect_platform.sh" --repo "$repo")
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
remote_url=$(origin_url "$repo")
|
|
59
|
+
repository=$(repository_slug_from_remote "$remote_url")
|
|
60
|
+
|
|
61
|
+
normalize_github_comments() {
|
|
62
|
+
local review_comments=$1
|
|
63
|
+
local discussion_comments=$2
|
|
64
|
+
local review_threads=$3
|
|
65
|
+
|
|
66
|
+
jq -cn \
|
|
67
|
+
--arg platform "$platform" \
|
|
68
|
+
--arg repository "$repository" \
|
|
69
|
+
--arg number "$number" \
|
|
70
|
+
--argjson review_comments "$review_comments" \
|
|
71
|
+
--argjson discussion_comments "$discussion_comments" \
|
|
72
|
+
--argjson review_threads "$review_threads" \
|
|
73
|
+
'
|
|
74
|
+
def github_thread_lookup:
|
|
75
|
+
(
|
|
76
|
+
$review_threads.data.repository.pullRequest.reviewThreads.nodes // []
|
|
77
|
+
)
|
|
78
|
+
| map(
|
|
79
|
+
. as $thread
|
|
80
|
+
| ($thread.comments.nodes // [])[]
|
|
81
|
+
| {
|
|
82
|
+
key: (.databaseId | tostring),
|
|
83
|
+
value: {
|
|
84
|
+
thread_id: ($thread.id // null),
|
|
85
|
+
thread_state: (
|
|
86
|
+
if ($thread.isResolved // false) then "resolved"
|
|
87
|
+
elif ($thread.isOutdated // false) then "outdated"
|
|
88
|
+
else "unresolved"
|
|
89
|
+
end
|
|
90
|
+
),
|
|
91
|
+
thread_resolved: ($thread.isResolved // false),
|
|
92
|
+
thread_outdated: ($thread.isOutdated // false)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
| from_entries;
|
|
97
|
+
|
|
98
|
+
{
|
|
99
|
+
platform: $platform,
|
|
100
|
+
repository: $repository,
|
|
101
|
+
number: $number,
|
|
102
|
+
code_review_comments: {
|
|
103
|
+
count: ($review_comments | length),
|
|
104
|
+
items: (
|
|
105
|
+
github_thread_lookup as $thread_lookup
|
|
106
|
+
| $review_comments
|
|
107
|
+
| map(
|
|
108
|
+
(.id | tostring) as $comment_id
|
|
109
|
+
| {
|
|
110
|
+
id: $comment_id,
|
|
111
|
+
author: (.user.login // .user.name // "unknown"),
|
|
112
|
+
body: (.body // ""),
|
|
113
|
+
path: (.path // ""),
|
|
114
|
+
start_line: (if .start_line == null then null else (.start_line | tostring) end),
|
|
115
|
+
line: (if .line == null then "" else (.line | tostring) end),
|
|
116
|
+
side: (.side // null),
|
|
117
|
+
start_side: (.start_side // null),
|
|
118
|
+
subject_type: (.subject_type // null),
|
|
119
|
+
original_line: (if .original_line == null then null else (.original_line | tostring) end),
|
|
120
|
+
original_start_line: (if .original_start_line == null then null else (.original_start_line | tostring) end),
|
|
121
|
+
original_position: (if .original_position == null then null else (.original_position | tostring) end),
|
|
122
|
+
commit_id: (.commit_id // null),
|
|
123
|
+
original_commit_id: (.original_commit_id // null),
|
|
124
|
+
diff_hunk: (.diff_hunk // null),
|
|
125
|
+
url: (.html_url // ""),
|
|
126
|
+
created_at: (.created_at // "")
|
|
127
|
+
}
|
|
128
|
+
+ ($thread_lookup[$comment_id] // {})
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
},
|
|
132
|
+
discussion_comments: {
|
|
133
|
+
count: ($discussion_comments | length),
|
|
134
|
+
items: ($discussion_comments | map({
|
|
135
|
+
id: (.id | tostring),
|
|
136
|
+
author: (.user.login // .user.name // "unknown"),
|
|
137
|
+
body: (.body // ""),
|
|
138
|
+
url: (.html_url // ""),
|
|
139
|
+
created_at: (.created_at // "")
|
|
140
|
+
}))
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
normalize_gitlab_comments() {
|
|
147
|
+
local discussions=$1
|
|
148
|
+
|
|
149
|
+
jq -cn \
|
|
150
|
+
--arg platform "$platform" \
|
|
151
|
+
--arg repository "$repository" \
|
|
152
|
+
--arg number "$number" \
|
|
153
|
+
--argjson discussions "$discussions" \
|
|
154
|
+
'
|
|
155
|
+
def active_notes:
|
|
156
|
+
[
|
|
157
|
+
$discussions[]
|
|
158
|
+
| .notes[]?
|
|
159
|
+
| select((.system // false) | not)
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
def review_notes:
|
|
163
|
+
[
|
|
164
|
+
active_notes[]
|
|
165
|
+
| . as $note
|
|
166
|
+
| $discussions[]
|
|
167
|
+
| select(any(.notes[]?; (.id // null) == ($note.id // null)))
|
|
168
|
+
| . as $discussion
|
|
169
|
+
| $note
|
|
170
|
+
| select(
|
|
171
|
+
(.position // null) != null
|
|
172
|
+
or (.line_code // null) != null
|
|
173
|
+
)
|
|
174
|
+
| . + {
|
|
175
|
+
thread_id: ($discussion.id // null),
|
|
176
|
+
thread_resolved: ($discussion.resolved // false),
|
|
177
|
+
thread_state: (
|
|
178
|
+
if ($discussion.resolved // false) then "resolved"
|
|
179
|
+
else "unresolved"
|
|
180
|
+
end
|
|
181
|
+
),
|
|
182
|
+
resolved_by: ($discussion.resolved_by // null),
|
|
183
|
+
resolved_at: ($discussion.resolved_at // null)
|
|
184
|
+
}
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
def discussion_notes:
|
|
188
|
+
[
|
|
189
|
+
active_notes[]
|
|
190
|
+
| select(
|
|
191
|
+
(.position // null) == null
|
|
192
|
+
and (.line_code // null) == null
|
|
193
|
+
)
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
{
|
|
197
|
+
platform: $platform,
|
|
198
|
+
repository: $repository,
|
|
199
|
+
number: $number,
|
|
200
|
+
code_review_comments: {
|
|
201
|
+
count: (review_notes | length),
|
|
202
|
+
items: (review_notes | map({
|
|
203
|
+
id: (.id | tostring),
|
|
204
|
+
author: (.author.username // .author.name // "unknown"),
|
|
205
|
+
body: (.body // ""),
|
|
206
|
+
path: (.position.new_path // .position.old_path // ""),
|
|
207
|
+
line: (
|
|
208
|
+
if (.position.new_line // null) != null then (.position.new_line | tostring)
|
|
209
|
+
elif (.position.old_line // null) != null then (.position.old_line | tostring)
|
|
210
|
+
else ""
|
|
211
|
+
end
|
|
212
|
+
),
|
|
213
|
+
new_path: (.position.new_path // null),
|
|
214
|
+
old_path: (.position.old_path // null),
|
|
215
|
+
new_line: (if (.position.new_line // null) != null then (.position.new_line | tostring) else null end),
|
|
216
|
+
old_line: (if (.position.old_line // null) != null then (.position.old_line | tostring) else null end),
|
|
217
|
+
base_sha: (.position.base_sha // null),
|
|
218
|
+
start_sha: (.position.start_sha // null),
|
|
219
|
+
head_sha: (.position.head_sha // null),
|
|
220
|
+
position_type: (.position.position_type // null),
|
|
221
|
+
line_range: (.position.line_range // null),
|
|
222
|
+
position: (.position // null),
|
|
223
|
+
thread_id: (.thread_id // null),
|
|
224
|
+
thread_state: (.thread_state // "unresolved"),
|
|
225
|
+
thread_resolved: (.thread_resolved // false),
|
|
226
|
+
resolved_by: (.resolved_by // null),
|
|
227
|
+
resolved_at: (.resolved_at // null),
|
|
228
|
+
url: (.url // .web_url // ""),
|
|
229
|
+
created_at: (.created_at // "")
|
|
230
|
+
}))
|
|
231
|
+
},
|
|
232
|
+
discussion_comments: {
|
|
233
|
+
count: (discussion_notes | length),
|
|
234
|
+
items: (discussion_notes | map({
|
|
235
|
+
id: (.id | tostring),
|
|
236
|
+
author: (.author.username // .author.name // "unknown"),
|
|
237
|
+
body: (.body // ""),
|
|
238
|
+
url: (.url // .web_url // ""),
|
|
239
|
+
created_at: (.created_at // "")
|
|
240
|
+
}))
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
'
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
emit_kv() {
|
|
247
|
+
local json=$1
|
|
248
|
+
print_kv platform "$(printf '%s' "$json" | jq -r '.platform')"
|
|
249
|
+
print_kv repository "$(printf '%s' "$json" | jq -r '.repository')"
|
|
250
|
+
print_kv number "$(printf '%s' "$json" | jq -r '.number')"
|
|
251
|
+
print_kv code_review_comment_count "$(printf '%s' "$json" | jq -r '.code_review_comments.count')"
|
|
252
|
+
print_kv discussion_comment_count "$(printf '%s' "$json" | jq -r '.discussion_comments.count')"
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
cd "$repo"
|
|
256
|
+
|
|
257
|
+
case "$platform" in
|
|
258
|
+
github)
|
|
259
|
+
require_cmd gh
|
|
260
|
+
review_comments=$(gh api "repos/$repository/pulls/$number/comments")
|
|
261
|
+
discussion_comments=$(gh api "repos/$repository/issues/$number/comments")
|
|
262
|
+
owner=${repository%%/*}
|
|
263
|
+
repo_name=${repository#*/}
|
|
264
|
+
review_threads=$(gh api graphql -f query='
|
|
265
|
+
query($owner: String!, $repo: String!, $number: Int!) {
|
|
266
|
+
repository(owner: $owner, name: $repo) {
|
|
267
|
+
pullRequest(number: $number) {
|
|
268
|
+
reviewThreads(first: 100) {
|
|
269
|
+
nodes {
|
|
270
|
+
id
|
|
271
|
+
isResolved
|
|
272
|
+
isOutdated
|
|
273
|
+
comments(first: 100) {
|
|
274
|
+
nodes {
|
|
275
|
+
databaseId
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
' -F owner="$owner" -F repo="$repo_name" -F number="$number")
|
|
284
|
+
normalized=$(normalize_github_comments "$review_comments" "$discussion_comments" "$review_threads")
|
|
285
|
+
;;
|
|
286
|
+
gitlab)
|
|
287
|
+
require_cmd glab
|
|
288
|
+
encoded_repository=$(jq -nr --arg value "$repository" '$value | @uri')
|
|
289
|
+
discussions=$(glab api "projects/$encoded_repository/merge_requests/$number/discussions" --paginate)
|
|
290
|
+
normalized=$(normalize_gitlab_comments "$discussions")
|
|
291
|
+
;;
|
|
292
|
+
*)
|
|
293
|
+
die "unsupported platform: $platform"
|
|
294
|
+
;;
|
|
295
|
+
esac
|
|
296
|
+
|
|
297
|
+
if [[ "$json_output" == true ]]; then
|
|
298
|
+
printf '%s\n' "$normalized"
|
|
299
|
+
else
|
|
300
|
+
emit_kv "$normalized"
|
|
301
|
+
fi
|