Skip to content

Commit 136cbab

Browse files
ralyodioclaude
andauthored
Give gh-issues a --csv that archives the whole result set (#9)
The table truncates titles at 70 columns and renders timestamps as "3 days ago", which is right for reading and wrong for keeping. --csv writes the same query to ~/gh-issues-YYYY-MM-DD.csv instead: whole titles, ISO 8601 CREATED and UPDATED, and a bare issue number so a spreadsheet reads it as a number. --csv=FILE picks the path. Bare --csv takes no argument, so `--csv --limit 10` cannot swallow the next flag as a filename. The destination directory is checked before any search runs, so a bad path costs no API calls. The two output shapes share their jq helpers rather than duplicating timeago and friends, and both now draw from one search_all_scopes function, so a scope can never be gathered for one form and missed by the other. Claude-Session: https://claude.ai/code/session_01XGe9mWC6FvUjT6p1HCVjba Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 859f898 commit 136cbab

2 files changed

Lines changed: 125 additions & 16 deletions

File tree

‎README.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,20 @@ gh-issues --users ralyodio
114114
gh-issues --orgs profullstack --users ralyodio --limit 50
115115
```
116116

117+
`--csv` writes the result set to `~/gh-issues-YYYY-MM-DD.csv` instead of
118+
printing the table, and `--csv=FILE` picks the path. The CSV is the archival
119+
form of the same query: whole titles rather than 70 columns of one, ISO 8601
120+
`CREATED` and `UPDATED` timestamps rather than "3 days ago", and a bare issue
121+
number so a spreadsheet reads it as one.
122+
123+
```sh
124+
gh-issues --orgs profullstack,moshcoder,h4kr,infernetprotocol --csv
125+
gh-issues --orgs profullstack --csv=/tmp/issues.csv
126+
```
127+
128+
Bare `--csv` takes no argument on purpose, so `--csv --limit 10` cannot swallow
129+
the next flag as a filename.
130+
117131
### `gh-prs-merge`
118132

119133
Walks the same scopes and squash-merges every PR that qualifies, oldest first.

‎bin/gh-issues‎

Lines changed: 111 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,30 @@ set -euo pipefail
44
orgs=''
55
users=''
66
limit=1000
7+
csv=0
8+
csv_path=''
79

810
usage() {
911
cat <<'EOF'
1012
Usage:
11-
gh-issues --orgs ORG1,ORG2 [--users USER1,USER2] [--limit NUMBER]
12-
gh-issues --users USER1,USER2 [--limit NUMBER]
13+
gh-issues --orgs ORG1,ORG2 [--users USER1,USER2] [--limit NUMBER] [--csv[=FILE]]
14+
gh-issues --users USER1,USER2 [--limit NUMBER] [--csv[=FILE]]
15+
16+
Options:
17+
--orgs ORGS comma-separated organizations to search
18+
--users USERS comma-separated personal repository owners to search
19+
--limit NUMBER maximum results per scope (default 1000)
20+
--csv write a CSV to ~/gh-issues-YYYY-MM-DD.csv instead of printing
21+
the table; titles are not truncated and the timestamps are
22+
ISO 8601 rather than "3 days ago"
23+
--csv=FILE write that CSV to FILE instead
1324
1425
Examples:
1526
gh-issues --orgs profullstack,moshcoder,h4kr,infernetprotocol
1627
gh-issues --users ralyodio,devpreshy
1728
gh-issues --orgs profullstack,moshcoder --users ralyodio,devpreshy
29+
gh-issues --orgs profullstack --csv
30+
gh-issues --orgs profullstack --csv=/tmp/issues.csv
1831
EOF
1932
}
2033

@@ -58,6 +71,19 @@ while (($#)); do
5871
shift
5972
;;
6073

74+
# Bare --csv deliberately takes no argument, so that `--csv --limit 10`
75+
# cannot swallow the next flag as a filename. Use --csv=FILE for a path.
76+
--csv)
77+
csv=1
78+
shift
79+
;;
80+
81+
--csv=*)
82+
csv=1
83+
csv_path=${1#*=}
84+
shift
85+
;;
86+
6187
-h|--help)
6288
usage
6389
exit 0
@@ -80,6 +106,20 @@ done
80106
((limit <= 1000)) ||
81107
die '--limit cannot exceed 1000'
82108

109+
if ((csv)); then
110+
if [[ -z $csv_path ]]; then
111+
csv_path="${HOME}/gh-issues-$(date +%Y-%m-%d).csv"
112+
fi
113+
114+
csv_directory=$(dirname -- "$csv_path")
115+
116+
[[ -d $csv_directory ]] ||
117+
die "no such directory: $csv_directory"
118+
119+
[[ -w $csv_directory ]] ||
120+
die "directory is not writable: $csv_directory"
121+
fi
122+
83123
org_list=()
84124
user_list=()
85125

@@ -125,14 +165,10 @@ search_scope() {
125165
--json repository,number,title,url,author,createdAt,updatedAt
126166
}
127167

128-
# Enable real terminal hyperlinks when stdout is a terminal.
129-
hyperlinks=0
130-
131-
if [[ -t 1 && ${TERM:-dumb} != dumb ]]; then
132-
hyperlinks=1
133-
fi
168+
search_all_scopes() {
169+
local org
170+
local user
134171

135-
{
136172
# Organizations use org:
137173
for org in "${org_list[@]}"; do
138174
search_scope org "$org"
@@ -142,8 +178,18 @@ fi
142178
for user in "${user_list[@]}"; do
143179
search_scope user "$user"
144180
done
145-
} |
146-
jq -rs '
181+
}
182+
183+
# Enable real terminal hyperlinks when stdout is a terminal.
184+
hyperlinks=0
185+
186+
if [[ -t 1 && ${TERM:-dumb} != dumb ]]; then
187+
hyperlinks=1
188+
fi
189+
190+
# Shared jq helpers. Single-quoted so jq's own $variables survive: the shell
191+
# must not expand them.
192+
jq_common='
147193
def clean:
148194
tostring | gsub("[\t\r\n]+"; " ");
149195
@@ -174,10 +220,15 @@ jq -rs '
174220
plural((($seconds / 31557600) | floor); "year")
175221
end;
176222
177-
(add // [])
178-
| unique_by(.url)
179-
| sort_by(.createdAt)
180-
| reverse
223+
def issues:
224+
(add // [])
225+
| unique_by(.url)
226+
| sort_by(.createdAt)
227+
| reverse;
228+
'
229+
230+
jq_table="$jq_common"'
231+
issues
181232
| (
182233
[
183234
"ORG/USER",
@@ -200,7 +251,51 @@ jq -rs '
200251
])
201252
)
202253
| @tsv
203-
' |
254+
'
255+
256+
# The CSV is the archival form: whole titles, real timestamps, and a bare issue
257+
# number so a spreadsheet reads it as one.
258+
jq_csv="$jq_common"'
259+
issues
260+
| (
261+
[
262+
"ORG/USER",
263+
"REPOSITORY",
264+
"ISSUE",
265+
"AUTHOR",
266+
"TITLE",
267+
"CREATED",
268+
"UPDATED",
269+
"LINK"
270+
],
271+
272+
(.[] | [
273+
(.repository.nameWithOwner | split("/")[0]),
274+
.repository.nameWithOwner,
275+
.number,
276+
(.author.login // "-"),
277+
(.title | clean),
278+
.createdAt,
279+
.updatedAt,
280+
.url
281+
])
282+
)
283+
| @csv
284+
'
285+
286+
if ((csv)); then
287+
search_all_scopes | jq -rs "$jq_csv" > "$csv_path"
288+
289+
# Every field is newline-free by construction, so one line is one row.
290+
written=$(wc -l < "$csv_path")
291+
292+
printf 'gh-issues: wrote %s issue(s) to %s\n' "$((written - 1))" "$csv_path"
293+
294+
exit 0
295+
fi
296+
297+
search_all_scopes |
298+
jq -rs "$jq_table" |
204299
awk -F '\t' -v hyperlinks="$hyperlinks" '
205300
{
206301
rows = NR

0 commit comments

Comments
 (0)