@plot-pm/board 0.2.0-rc.0 → 0.2.1-rc.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/package.json +6 -6
- package/plot-config.sh +75 -0
- package/plot-plan-meta.sh +250 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plot-pm/board",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1-rc.0",
|
|
4
4
|
"description": "Local Kanban board for Plot — a glanceable view of plan phases from docs/plans, with sprint and story filters",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
|
-
"dist/board-server.mjs"
|
|
24
|
+
"dist/board-server.mjs",
|
|
25
|
+
"plot-config.sh",
|
|
26
|
+
"plot-plan-meta.sh"
|
|
25
27
|
],
|
|
26
28
|
"scripts": {
|
|
27
29
|
"dev": "vite",
|
|
@@ -33,9 +35,6 @@
|
|
|
33
35
|
"test": "node --test test/*.test.mjs",
|
|
34
36
|
"test:integration": "pnpm build && vitest run"
|
|
35
37
|
},
|
|
36
|
-
"dependencies": {
|
|
37
|
-
"zod": "^4.4.0"
|
|
38
|
-
},
|
|
39
38
|
"devDependencies": {
|
|
40
39
|
"@radix-ui/react-checkbox": "^1.3.0",
|
|
41
40
|
"@radix-ui/react-popover": "^1.1.0",
|
|
@@ -56,6 +55,7 @@
|
|
|
56
55
|
"typescript": "^5.9.0",
|
|
57
56
|
"vite": "^8.1.0",
|
|
58
57
|
"vite-plugin-singlefile": "^2.3.0",
|
|
59
|
-
"vitest": "^4.1.10"
|
|
58
|
+
"vitest": "^4.1.10",
|
|
59
|
+
"zod": "^4.4.0"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/plot-config.sh
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Plot helper: read a key from the adopting project's `## Plot Config`.
|
|
3
|
+
# Usage: plot-config.sh get <key> [default]
|
|
4
|
+
# Output: the configured value, or the default (possibly empty). Exit 0 always
|
|
5
|
+
# for `get` — missing file, missing section, and missing key all fall
|
|
6
|
+
# back to the default so callers can rely on the output unconditionally.
|
|
7
|
+
# Designed for small-model consumption: one value on stdout, no interpretation.
|
|
8
|
+
#
|
|
9
|
+
# This is the ONE place that knows where plot configuration lives (currently a
|
|
10
|
+
# `## Plot Config` section in the repo-root CLAUDE.md). Helpers must call this
|
|
11
|
+
# instead of grepping CLAUDE.md themselves, so the storage location/format can
|
|
12
|
+
# evolve without touching every consumer.
|
|
13
|
+
#
|
|
14
|
+
# Grammar accepted inside the section (case-insensitive key, bold optional):
|
|
15
|
+
# - **Plan directory:** docs/plans/
|
|
16
|
+
# - Plan directory: docs/plans/
|
|
17
|
+
# - **Plan directory:** `docs/plans/` (with a backticked value + prose note)
|
|
18
|
+
# - **Branch prefixes:** `idea/` (plans), `feature/`, `bug/` (list + prose)
|
|
19
|
+
# Backticks (markdown decoration) and `(...)` (human prose) are stripped from
|
|
20
|
+
# the value; no documented key's value legitimately contains either. Lines
|
|
21
|
+
# outside the `## Plot Config` section never match (no prose false positives),
|
|
22
|
+
# and neither do HTML-commented example lines.
|
|
23
|
+
#
|
|
24
|
+
# Known keys (see the plot skill's Setup section):
|
|
25
|
+
# Project board | Branch prefixes | Plan directory | Active index |
|
|
26
|
+
# Delivered index | Sprint directory | Plan template | Main branch
|
|
27
|
+
#
|
|
28
|
+
# `Plan template` is a repo-root-relative path to the plan template /plot-idea
|
|
29
|
+
# instantiates; when absent, /plot-idea falls back to the shipped template.
|
|
30
|
+
|
|
31
|
+
set -uo pipefail
|
|
32
|
+
|
|
33
|
+
cmd="${1:?Usage: plot-config.sh get <key> [default]}"
|
|
34
|
+
key="${2:?Usage: plot-config.sh get <key> [default]}"
|
|
35
|
+
default="${3:-}"
|
|
36
|
+
|
|
37
|
+
if [ "$cmd" != "get" ]; then
|
|
38
|
+
echo "plot-config: unknown subcommand '$cmd' (only 'get' is supported)" >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
root=$(git rev-parse --show-toplevel 2>/dev/null) || root="."
|
|
43
|
+
config_file="$root/CLAUDE.md"
|
|
44
|
+
|
|
45
|
+
value=""
|
|
46
|
+
if [ -f "$config_file" ]; then
|
|
47
|
+
# Extract the `## Plot Config` section (case-insensitive, portable awk).
|
|
48
|
+
section=$(awk '
|
|
49
|
+
/^##[[:space:]]/ { in_section = (tolower($0) ~ /^##[[:space:]]+plot config[[:space:]]*$/) ; next }
|
|
50
|
+
in_section { print }
|
|
51
|
+
' "$config_file")
|
|
52
|
+
# Value extraction. A documented key's value is a path, a prefix list, or an
|
|
53
|
+
# owner/number — none of which legitimately contain backticks or parentheses.
|
|
54
|
+
# So we can uniformly treat backticks as markdown decoration and `(...)` as
|
|
55
|
+
# human prose, stripping both. This tolerates real-world config written like
|
|
56
|
+
# - **Plan directory:** `docs/plans/` (date-prefixed, never moved)
|
|
57
|
+
# - **Branch prefixes:** `idea/` (plans), `feature/`, `bug/`, `docs/`
|
|
58
|
+
# without truncating multi-value lists to their first backtick span.
|
|
59
|
+
value=$(printf '%s\n' "$section" \
|
|
60
|
+
| grep -m1 -iE "^[[:space:]]*[-*]?[[:space:]]*\**${key}[:*]" \
|
|
61
|
+
| sed -E '
|
|
62
|
+
s/^[^:]*:[[:space:]]*//; # drop list marker, bold, "key:"
|
|
63
|
+
s/^\**[[:space:]]*//; # drop leading bold before value
|
|
64
|
+
s/\([^)]*\)//g; # drop parenthetical prose
|
|
65
|
+
s/`//g; # drop markdown backticks
|
|
66
|
+
s/[[:space:]]*,[[:space:]]*/, /g; # normalize list separators
|
|
67
|
+
s/[[:space:]]+/ /g; # collapse internal whitespace
|
|
68
|
+
s/^[[:space:]]+//; s/[[:space:]]+$//') # trim ends
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
if [ -n "$value" ]; then
|
|
72
|
+
printf '%s\n' "$value"
|
|
73
|
+
else
|
|
74
|
+
printf '%s\n' "$default"
|
|
75
|
+
fi
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Plot helper: parse plan files into structured JSON.
|
|
3
|
+
# Usage: plot-plan-meta.sh <plan-file>... [--prefixes 'idea|feature|bug|docs|infra']
|
|
4
|
+
# Output: one compact JSON object per input file, one per line (JSON lines).
|
|
5
|
+
# Exit 0 always; parse problems are reported in the JSON, never as a
|
|
6
|
+
# crash. Missing files yield an error object (emitted first).
|
|
7
|
+
# Designed for small-model consumption: structured output, no interpretation.
|
|
8
|
+
#
|
|
9
|
+
# This is the ONE place that knows what a plan file looks like. Anything that
|
|
10
|
+
# needs plan metadata (phase, type, branches, PR numbers) must call this
|
|
11
|
+
# script instead of grepping plan files itself — the parser is the format
|
|
12
|
+
# contract. The contract is specified by example in test/reconcile/fixtures/
|
|
13
|
+
# (one fixture per supported shape) and enforced by test/reconcile/.
|
|
14
|
+
#
|
|
15
|
+
# Accepts many files in one invocation and parses them in a single awk pass —
|
|
16
|
+
# cheap enough to run over a 100-plan repo on every /plot. (The first version
|
|
17
|
+
# spawned a subprocess chain per file; at ~80ms/file that priced the parser
|
|
18
|
+
# out of ambient use.)
|
|
19
|
+
#
|
|
20
|
+
# Two plan formats are recognized:
|
|
21
|
+
#
|
|
22
|
+
# canonical the plan template's `## Status` body section:
|
|
23
|
+
# - **Phase:** Approved
|
|
24
|
+
# - **Type:** feature
|
|
25
|
+
# (bullet, bold, and plain `Phase: ...` variants all accepted)
|
|
26
|
+
#
|
|
27
|
+
# frontmatter YAML front matter at the top of the file:
|
|
28
|
+
# ---
|
|
29
|
+
# status: Approved
|
|
30
|
+
# phase: Approved
|
|
31
|
+
# type: feature
|
|
32
|
+
# ---
|
|
33
|
+
# `status:` is the primary field; `phase:` is reported as the
|
|
34
|
+
# alternate so callers can flag disagreement between the two.
|
|
35
|
+
#
|
|
36
|
+
# Front matter wins when both are present (it is the machine-facing surface).
|
|
37
|
+
# A file with neither is reported as format "none" (pre-plot / legacy plan).
|
|
38
|
+
#
|
|
39
|
+
# Phase values are normalized by scanning whitespace-separated tokens for the
|
|
40
|
+
# first known phase word — so decorated real-world values like
|
|
41
|
+
# "Delivered (2026-06-29) — split done" normalize to "delivered". A non-empty
|
|
42
|
+
# value with no known token normalizes to "UNKNOWN"; an absent field to "NONE".
|
|
43
|
+
#
|
|
44
|
+
# JSON fields:
|
|
45
|
+
# file the path given
|
|
46
|
+
# format canonical | frontmatter | none
|
|
47
|
+
# phase_raw primary phase value as written ("" if absent)
|
|
48
|
+
# phase normalized: draft|approved|delivered|released|rejected|
|
|
49
|
+
# superseded|UNKNOWN|NONE
|
|
50
|
+
# phase_alt_raw secondary value when the file carries two (front matter
|
|
51
|
+
# status: AND phase:), else ""
|
|
52
|
+
# phase_alt normalized phase_alt_raw (NONE when absent)
|
|
53
|
+
# type normalized plan type (feature|bug|docs|infra or "")
|
|
54
|
+
# title plan title: front matter `title:` wins, else the first H1
|
|
55
|
+
# (`# ...`) line, else "" (board-facing display field)
|
|
56
|
+
# sprint sprint slug the plan belongs to (`## Status` `Sprint:` or
|
|
57
|
+
# front matter `sprint:`); "" if absent or an HTML-comment
|
|
58
|
+
# placeholder
|
|
59
|
+
# story story slug the plan belongs to (`## Status` `Story:` or
|
|
60
|
+
# front matter `story:`); "" if absent or a placeholder
|
|
61
|
+
# assignee github handle from the `## Approval` `Assignee:` line or
|
|
62
|
+
# front matter `assignee:`; "" if absent
|
|
63
|
+
# branches branch names from the `## Branches` section (backtick-
|
|
64
|
+
# quoted, matching the known prefixes; sorted, unique)
|
|
65
|
+
# prs PR numbers from `→ #NNN` links in the `## Branches`
|
|
66
|
+
# section (sorted, unique)
|
|
67
|
+
#
|
|
68
|
+
# title/sprint/story/assignee are the board-facing surface (`@plot-pm/board`
|
|
69
|
+
# consumes this script instead of parsing plans itself). Front matter wins over
|
|
70
|
+
# the canonical body for every field, matching the `status:`/`phase:` rule.
|
|
71
|
+
|
|
72
|
+
set -uo pipefail
|
|
73
|
+
|
|
74
|
+
prefixes='idea|feature|bug|docs|infra'
|
|
75
|
+
files=()
|
|
76
|
+
missing=()
|
|
77
|
+
while [ $# -gt 0 ]; do
|
|
78
|
+
case "$1" in
|
|
79
|
+
--prefixes) prefixes="${2:?--prefixes needs a value}"; shift 2 ;;
|
|
80
|
+
-*) echo "plot-plan-meta: unknown flag: $1" >&2; shift ;;
|
|
81
|
+
*)
|
|
82
|
+
if [ -f "$1" ]; then files+=("$1"); else missing+=("$1"); fi
|
|
83
|
+
shift ;;
|
|
84
|
+
esac
|
|
85
|
+
done
|
|
86
|
+
|
|
87
|
+
if [ ${#files[@]} -eq 0 ] && [ ${#missing[@]} -eq 0 ]; then
|
|
88
|
+
echo "Usage: plot-plan-meta.sh <plan-file>... [--prefixes '<alternation>']" >&2
|
|
89
|
+
exit 1
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
for f in ${missing[@]+"${missing[@]}"}; do
|
|
93
|
+
printf '{"file":"%s","format":"none","error":"file not found","phase_raw":"","phase":"NONE","phase_alt_raw":"","phase_alt":"NONE","type":"","title":"","sprint":"","story":"","assignee":"","branches":[],"prs":[]}\n' \
|
|
94
|
+
"$(printf '%s' "$f" | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
|
95
|
+
done
|
|
96
|
+
|
|
97
|
+
[ ${#files[@]} -gt 0 ] || exit 0
|
|
98
|
+
|
|
99
|
+
awk -v PREFIXES="$prefixes" '
|
|
100
|
+
function jesc(s) {
|
|
101
|
+
gsub(/\\/, "\\\\", s); gsub(/"/, "\\\"", s); gsub(/\t/, "\\t", s)
|
|
102
|
+
return s
|
|
103
|
+
}
|
|
104
|
+
function trim(s) { sub(/^[ \t]+/, "", s); sub(/[ \t]+$/, "", s); return s }
|
|
105
|
+
# Value after the first colon, stripped of bold markers / quotes / space.
|
|
106
|
+
function val_after_colon(s) {
|
|
107
|
+
sub(/^[^:]*:/, "", s); sub(/^\**[ \t]*/, "", s)
|
|
108
|
+
sub(/^"/, "", s); sub(/"$/, "", s)
|
|
109
|
+
return trim(s)
|
|
110
|
+
}
|
|
111
|
+
# Template placeholders like "<!-- optional -->" mean "field absent".
|
|
112
|
+
function strip_placeholder(s) { return (s ~ /^<!--/) ? "" : s }
|
|
113
|
+
# First known phase token wins; NONE if empty; UNKNOWN otherwise.
|
|
114
|
+
function norm_phase(raw, lower, toks, n, i, t) {
|
|
115
|
+
if (raw == "") return "NONE"
|
|
116
|
+
lower = tolower(raw)
|
|
117
|
+
n = split(lower, toks, /[ \t]+/)
|
|
118
|
+
for (i = 1; i <= n; i++) {
|
|
119
|
+
t = toks[i]
|
|
120
|
+
gsub(/^[^a-z]+/, "", t); gsub(/[^a-z-]+$/, "", t)
|
|
121
|
+
if (t ~ /^(draft|approved|delivered|released|rejected|superseded)$/) return t
|
|
122
|
+
if (t == "ready-for-review" || t == "in-review") return "approved"
|
|
123
|
+
}
|
|
124
|
+
return "UNKNOWN"
|
|
125
|
+
}
|
|
126
|
+
function norm_type(raw, lower, toks, n, i, t) {
|
|
127
|
+
if (raw == "") return ""
|
|
128
|
+
lower = tolower(raw)
|
|
129
|
+
n = split(lower, toks, /[ \t]+/)
|
|
130
|
+
for (i = 1; i <= n; i++) {
|
|
131
|
+
t = toks[i]
|
|
132
|
+
gsub(/^[^a-z]+/, "", t); gsub(/[^a-z]+$/, "", t)
|
|
133
|
+
if (t ~ /^(feature|bug|docs|infra)$/) return t
|
|
134
|
+
}
|
|
135
|
+
return ""
|
|
136
|
+
}
|
|
137
|
+
function reset_state() {
|
|
138
|
+
fm_status = ""; fm_phase = ""; fm_type = ""
|
|
139
|
+
fm_title = ""; fm_sprint = ""; fm_story = ""; fm_assignee = ""
|
|
140
|
+
canon_phase = ""; canon_type = ""
|
|
141
|
+
canon_sprint = ""; canon_story = ""; canon_assignee = ""
|
|
142
|
+
h1_title = ""
|
|
143
|
+
in_fm = 0; section = ""
|
|
144
|
+
delete branches; n_branches = 0
|
|
145
|
+
delete prs; n_prs = 0
|
|
146
|
+
}
|
|
147
|
+
function emit_record( fmt, praw, palt_raw, traw, title, sprint, story, assignee, i, j, out, sorted_b, sorted_p, nb, np) {
|
|
148
|
+
if (fm_status != "" || fm_phase != "") {
|
|
149
|
+
fmt = "frontmatter"
|
|
150
|
+
praw = (fm_status != "") ? fm_status : fm_phase
|
|
151
|
+
palt_raw = (fm_status != "" && fm_phase != "") ? fm_phase : ""
|
|
152
|
+
traw = fm_type
|
|
153
|
+
} else if (canon_phase != "") {
|
|
154
|
+
fmt = "canonical"; praw = canon_phase; palt_raw = ""; traw = canon_type
|
|
155
|
+
} else {
|
|
156
|
+
fmt = "none"; praw = ""; palt_raw = ""; traw = ""
|
|
157
|
+
}
|
|
158
|
+
# Board-facing fields: front matter wins over the canonical body; H1 is the
|
|
159
|
+
# title fallback. Placeholders ("<!-- ... -->") count as absent.
|
|
160
|
+
title = strip_placeholder((fm_title != "") ? fm_title : h1_title)
|
|
161
|
+
sprint = strip_placeholder((fm_sprint != "") ? fm_sprint : canon_sprint)
|
|
162
|
+
story = strip_placeholder((fm_story != "") ? fm_story : canon_story)
|
|
163
|
+
assignee = strip_placeholder((fm_assignee != "") ? fm_assignee : canon_assignee)
|
|
164
|
+
# Insertion sort + dedupe (portable: no gawk asort).
|
|
165
|
+
nb = 0
|
|
166
|
+
for (i = 1; i <= n_branches; i++) {
|
|
167
|
+
for (j = 1; j <= nb && sorted_b[j] != branches[i]; j++) ;
|
|
168
|
+
if (j <= nb) continue
|
|
169
|
+
for (j = nb; j >= 1 && sorted_b[j] > branches[i]; j--) sorted_b[j+1] = sorted_b[j]
|
|
170
|
+
sorted_b[j+1] = branches[i]; nb++
|
|
171
|
+
}
|
|
172
|
+
np = 0
|
|
173
|
+
for (i = 1; i <= n_prs; i++) {
|
|
174
|
+
for (j = 1; j <= np && sorted_p[j] != prs[i]+0; j++) ;
|
|
175
|
+
if (j <= np) continue
|
|
176
|
+
for (j = np; j >= 1 && sorted_p[j] > prs[i]+0; j--) sorted_p[j+1] = sorted_p[j]
|
|
177
|
+
sorted_p[j+1] = prs[i]+0; np++
|
|
178
|
+
}
|
|
179
|
+
out = "{\"file\":\"" jesc(cur_file) "\",\"format\":\"" fmt "\""
|
|
180
|
+
out = out ",\"phase_raw\":\"" jesc(praw) "\",\"phase\":\"" norm_phase(praw) "\""
|
|
181
|
+
out = out ",\"phase_alt_raw\":\"" jesc(palt_raw) "\",\"phase_alt\":\"" norm_phase(palt_raw) "\""
|
|
182
|
+
out = out ",\"type\":\"" norm_type(traw) "\""
|
|
183
|
+
out = out ",\"title\":\"" jesc(title) "\",\"sprint\":\"" jesc(sprint) "\""
|
|
184
|
+
out = out ",\"story\":\"" jesc(story) "\",\"assignee\":\"" jesc(assignee) "\""
|
|
185
|
+
out = out ",\"branches\":["
|
|
186
|
+
for (i = 1; i <= nb; i++) out = out (i > 1 ? "," : "") "\"" jesc(sorted_b[i]) "\""
|
|
187
|
+
out = out "],\"prs\":["
|
|
188
|
+
for (i = 1; i <= np; i++) out = out (i > 1 ? "," : "") sorted_p[i]
|
|
189
|
+
out = out "]}"
|
|
190
|
+
print out
|
|
191
|
+
}
|
|
192
|
+
BEGIN { branch_re = "`(" PREFIXES ")/[^`]+`" }
|
|
193
|
+
FNR == 1 {
|
|
194
|
+
if (NR > 1) emit_record()
|
|
195
|
+
reset_state()
|
|
196
|
+
cur_file = FILENAME
|
|
197
|
+
if ($0 ~ /^---[ \t]*$/) { in_fm = 1; next }
|
|
198
|
+
}
|
|
199
|
+
in_fm {
|
|
200
|
+
if ($0 ~ /^---[ \t]*$/) { in_fm = 0; next }
|
|
201
|
+
lower = tolower($0)
|
|
202
|
+
if (lower ~ /^status:/ && fm_status == "") fm_status = val_after_colon($0)
|
|
203
|
+
else if (lower ~ /^phase:/ && fm_phase == "") fm_phase = val_after_colon($0)
|
|
204
|
+
else if (lower ~ /^type:/ && fm_type == "") fm_type = val_after_colon($0)
|
|
205
|
+
else if (lower ~ /^title:/ && fm_title == "") fm_title = val_after_colon($0)
|
|
206
|
+
else if (lower ~ /^sprint:/ && fm_sprint == "") fm_sprint = val_after_colon($0)
|
|
207
|
+
else if (lower ~ /^story:/ && fm_story == "") fm_story = val_after_colon($0)
|
|
208
|
+
else if (lower ~ /^assignee:/ && fm_assignee == "") fm_assignee = val_after_colon($0)
|
|
209
|
+
next
|
|
210
|
+
}
|
|
211
|
+
# First H1 is the title fallback (front matter title: still wins in emit).
|
|
212
|
+
/^#[ \t]/ && h1_title == "" { h1_title = trim(substr($0, 2)) }
|
|
213
|
+
/^## / {
|
|
214
|
+
if ($0 ~ /^## Status/) section = "status"
|
|
215
|
+
else if ($0 ~ /^## Branches/) section = "branches"
|
|
216
|
+
else if ($0 ~ /^## Approval/) section = "approval"
|
|
217
|
+
else section = ""
|
|
218
|
+
next
|
|
219
|
+
}
|
|
220
|
+
section == "status" {
|
|
221
|
+
lower = tolower($0)
|
|
222
|
+
if (lower ~ /^[ \t]*[-*]?[ \t]*\**phase[:*]/ && canon_phase == "") canon_phase = val_after_colon($0)
|
|
223
|
+
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**type[:*]/ && canon_type == "") canon_type = val_after_colon($0)
|
|
224
|
+
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**sprint[:*]/ && canon_sprint == "") canon_sprint = val_after_colon($0)
|
|
225
|
+
else if (lower ~ /^[ \t]*[-*]?[ \t]*\**story[:*]/ && canon_story == "") canon_story = val_after_colon($0)
|
|
226
|
+
next
|
|
227
|
+
}
|
|
228
|
+
section == "approval" {
|
|
229
|
+
lower = tolower($0)
|
|
230
|
+
if (lower ~ /^[ \t]*[-*]?[ \t]*\**assignee[:*]/ && canon_assignee == "") canon_assignee = val_after_colon($0)
|
|
231
|
+
next
|
|
232
|
+
}
|
|
233
|
+
section == "branches" {
|
|
234
|
+
line = $0
|
|
235
|
+
while (match(line, branch_re)) {
|
|
236
|
+
b = substr(line, RSTART + 1, RLENGTH - 2)
|
|
237
|
+
branches[++n_branches] = b
|
|
238
|
+
line = substr(line, RSTART + RLENGTH)
|
|
239
|
+
}
|
|
240
|
+
line = $0
|
|
241
|
+
while (match(line, /→ #[0-9]+/)) {
|
|
242
|
+
p = substr(line, RSTART, RLENGTH)
|
|
243
|
+
gsub(/[^0-9]/, "", p)
|
|
244
|
+
prs[++n_prs] = p
|
|
245
|
+
line = substr(line, RSTART + RLENGTH)
|
|
246
|
+
}
|
|
247
|
+
next
|
|
248
|
+
}
|
|
249
|
+
END { if (NR > 0) emit_record() }
|
|
250
|
+
' "${files[@]}"
|