You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560
  1. #!/usr/bin/env sh
  2. # # A GitHub API client library written in POSIX sh
  3. #
  4. # https://github.com/whiteinge/ok.sh
  5. # BSD licensed.
  6. #
  7. # ## Requirements
  8. #
  9. # * A POSIX environment (tested against Busybox v1.19.4)
  10. # * curl (tested against 7.32.0)
  11. #
  12. # ## Optional requirements
  13. #
  14. # * jq <http://stedolan.github.io/jq/> (tested against 1.3)
  15. # If jq is not installed commands will output raw JSON; if jq is installed
  16. # the output will be formatted and filtered for use with other shell tools.
  17. #
  18. # ## Setup
  19. #
  20. # Authentication credentials are read from a `$HOME/.netrc` file on UNIX
  21. # machines or a `_netrc` file in `%HOME%` for UNIX environments under Windows.
  22. # [Generate the token on GitHub](https://github.com/settings/tokens) under
  23. # "Account Settings -> Applications".
  24. # Restrict permissions on that file with `chmod 600 ~/.netrc`!
  25. #
  26. # machine api.github.com
  27. # login <username>
  28. # password <token>
  29. #
  30. # machine uploads.github.com
  31. # login <username>
  32. # password <token>
  33. #
  34. # Or set an environment `GITHUB_TOKEN=token`
  35. #
  36. # ## Configuration
  37. #
  38. # The following environment variables may be set to customize ${NAME}.
  39. #
  40. # * OK_SH_URL=${OK_SH_URL}
  41. # Base URL for GitHub or GitHub Enterprise.
  42. # * OK_SH_ACCEPT=${OK_SH_ACCEPT}
  43. # The 'Accept' header to send with each request.
  44. # * OK_SH_JQ_BIN=${OK_SH_JQ_BIN}
  45. # The name of the jq binary, if installed.
  46. # * OK_SH_VERBOSE=${OK_SH_VERBOSE}
  47. # The debug logging verbosity level. Same as the verbose flag.
  48. # * OK_SH_RATE_LIMIT=${OK_SH_RATE_LIMIT}
  49. # Output current GitHub rate limit information to stderr.
  50. # * OK_SH_DESTRUCTIVE=${OK_SH_DESTRUCTIVE}
  51. # Allow destructive operations without prompting for confirmation.
  52. # * OK_SH_MARKDOWN=${OK_SH_MARKDOWN}
  53. # Output some text in Markdown format.
  54. export NAME=$(basename "$0")
  55. export VERSION='0.5.1'
  56. export OK_SH_URL=${OK_SH_URL:-'https://api.github.com'}
  57. export OK_SH_ACCEPT=${OK_SH_ACCEPT:-'application/vnd.github.v3+json'}
  58. export OK_SH_JQ_BIN="${OK_SH_JQ_BIN:-jq}"
  59. export OK_SH_VERBOSE="${OK_SH_VERBOSE:-0}"
  60. export OK_SH_RATE_LIMIT="${OK_SH_RATE_LIMIT:-0}"
  61. export OK_SH_DESTRUCTIVE="${OK_SH_DESTRUCTIVE:-0}"
  62. export OK_SH_MARKDOWN="${OK_SH_MARKDOWN:-0}"
  63. # Detect if jq is installed.
  64. command -v "$OK_SH_JQ_BIN" 1>/dev/null 2>/dev/null
  65. NO_JQ=$?
  66. # Customizable logging output.
  67. exec 4>/dev/null
  68. exec 5>/dev/null
  69. exec 6>/dev/null
  70. export LINFO=4 # Info-level log messages.
  71. export LDEBUG=5 # Debug-level log messages.
  72. export LSUMMARY=6 # Summary output.
  73. # Generate a carriage return so we can match on it.
  74. # Using a variable because these are tough to specify in a portable way.
  75. crlf=$(printf '\r\n')
  76. # ## Main
  77. # Generic functions not necessarily specific to working with GitHub.
  78. # ### Help
  79. # Functions for fetching and formatting help text.
  80. _cols() {
  81. sort | awk '
  82. { w[NR] = $0 }
  83. END {
  84. cols = 3
  85. per_col = sprintf("%.f", NR / cols + 0.5) # Round up if decimal.
  86. for (i = 1; i < per_col + 1; i += 1) {
  87. for (j = 0; j < cols; j += 1) {
  88. printf("%-24s", w[i + per_col * j])
  89. }
  90. printf("\n")
  91. }
  92. }
  93. '
  94. }
  95. _links() { awk '{ print "* [" $0 "](#" $0 ")" }'; }
  96. _funcsfmt() { if [ "$OK_SH_MARKDOWN" -eq 0 ]; then _cols; else _links; fi; }
  97. help() {
  98. # Output the help text for a command
  99. #
  100. # Usage:
  101. #
  102. # help commandname
  103. #
  104. # Positional arguments
  105. #
  106. local fname="$1"
  107. # Function name to search for; if omitted searches whole file.
  108. # Short-circuit if only producing help for a single function.
  109. if [ $# -gt 0 ]; then
  110. awk -v fname="^$fname\\\(\\\) \\\{$" '$0 ~ fname, /^}/ { print }' "$0" \
  111. | _helptext
  112. return
  113. fi
  114. _helptext < "$0"
  115. printf '\n'
  116. help __main
  117. printf '\n'
  118. printf '## Table of Contents\n'
  119. printf '\n### Utility and request/response commands\n\n'
  120. _all_funcs public=0 | _funcsfmt
  121. printf '\n### GitHub commands\n\n'
  122. _all_funcs private=0 | _funcsfmt
  123. printf '\n## Commands\n\n'
  124. for cmd in $(_all_funcs public=0); do
  125. printf '### %s\n\n' "$cmd"
  126. help "$cmd"
  127. printf '\n'
  128. done
  129. for cmd in $(_all_funcs private=0); do
  130. printf '### %s\n\n' "$cmd"
  131. help "$cmd"
  132. printf '\n'
  133. done
  134. }
  135. _all_funcs() {
  136. # List all functions found in the current file in the order they appear
  137. #
  138. # Keyword arguments
  139. #
  140. local public=1
  141. # `0` do not output public functions.
  142. local private=1
  143. # `0` do not output private functions.
  144. for arg in "$@"; do
  145. case $arg in
  146. (public=*) public="${arg#*=}";;
  147. (private=*) private="${arg#*=}";;
  148. esac
  149. done
  150. awk -v public="$public" -v private="$private" '
  151. $1 !~ /^__/ && /^[a-zA-Z0-9_]+\s*\(\)/ {
  152. sub(/\(\)$/, "", $1)
  153. if (!public && substr($1, 1, 1) != "_") next
  154. if (!private && substr($1, 1, 1) == "_") next
  155. print $1
  156. }
  157. ' "$0"
  158. }
  159. __main() {
  160. # ## Usage
  161. #
  162. # `${NAME} [<flags>] (command [<arg>, <name=value>...])`
  163. #
  164. # ${NAME} -h # Short, usage help text.
  165. # ${NAME} help # All help text. Warning: long!
  166. # ${NAME} help command # Command-specific help text.
  167. # ${NAME} command # Run a command with and without args.
  168. # ${NAME} command foo bar baz=Baz qux='Qux arg here'
  169. #
  170. # Flag | Description
  171. # ---- | -----------
  172. # -V | Show version.
  173. # -h | Show this screen.
  174. # -j | Output raw JSON; don't process with jq.
  175. # -q | Quiet; don't print to stdout.
  176. # -r | Print current GitHub API rate limit to stderr.
  177. # -v | Logging output; specify multiple times: info, debug, trace.
  178. # -x | Enable xtrace debug logging.
  179. # -y | Answer 'yes' to any prompts.
  180. #
  181. # Flags _must_ be the first argument to `${NAME}`, before `command`.
  182. local cmd
  183. local ret
  184. local opt
  185. local OPTARG
  186. local OPTIND
  187. local quiet=0
  188. local temp_dir="${TMPDIR-/tmp}/${NAME}.${$}.$(awk \
  189. 'BEGIN {srand(); printf "%d\n", rand() * 10^10}')"
  190. local summary_fifo="${temp_dir}/oksh_summary.fifo"
  191. # shellcheck disable=SC2154
  192. trap '
  193. excode=$?; trap - EXIT;
  194. exec 4>&-
  195. exec 5>&-
  196. exec 6>&-
  197. rm -rf '"$temp_dir"'
  198. exit $excode
  199. ' INT TERM EXIT
  200. while getopts Vhjqrvxy opt; do
  201. case $opt in
  202. V) printf 'Version: %s\n' $VERSION
  203. exit;;
  204. h) help __main
  205. printf '\nAvailable commands:\n\n'
  206. _all_funcs private=0 | _cols
  207. printf '\n'
  208. exit;;
  209. j) NO_JQ=1;;
  210. q) quiet=1;;
  211. r) OK_SH_RATE_LIMIT=1;;
  212. v) OK_SH_VERBOSE=$(( OK_SH_VERBOSE + 1 ));;
  213. x) set -x;;
  214. y) OK_SH_DESTRUCTIVE=1;;
  215. esac
  216. done
  217. shift $(( OPTIND - 1 ))
  218. if [ -z "$1" ] ; then
  219. printf 'No command given. Available commands:\n\n%s\n' \
  220. "$(_all_funcs private=0 | _cols)" 1>&2
  221. exit 1
  222. fi
  223. [ $OK_SH_VERBOSE -gt 0 ] && exec 4>&2
  224. [ $OK_SH_VERBOSE -gt 1 ] && exec 5>&2
  225. if [ $quiet -eq 1 ]; then
  226. exec 1>/dev/null 2>/dev/null
  227. fi
  228. if [ "$OK_SH_RATE_LIMIT" -eq 1 ] ; then
  229. mkdir -m 700 "$temp_dir" || {
  230. printf 'failed to create temp_dir\n' >&2; exit 1;
  231. }
  232. mkfifo "$summary_fifo"
  233. # Hold the fifo open so it will buffer input until emptied.
  234. exec 6<>"$summary_fifo"
  235. fi
  236. # Run the command.
  237. cmd="$1" && shift
  238. _log debug "Running command ${cmd}."
  239. "$cmd" "$@"
  240. ret=$?
  241. _log debug "Command ${cmd} exited with ${?}."
  242. # Output any summary messages.
  243. if [ "$OK_SH_RATE_LIMIT" -eq 1 ] ; then
  244. cat "$summary_fifo" 1>&2 &
  245. exec 6>&-
  246. fi
  247. exit $ret
  248. }
  249. _log() {
  250. # A lightweight logging system based on file descriptors
  251. #
  252. # Usage:
  253. #
  254. # _log debug 'Starting the combobulator!'
  255. #
  256. # Positional arguments
  257. #
  258. local level="${1:?Level is required.}"
  259. # The level for a given log message. (info or debug)
  260. local message="${2:?Message is required.}"
  261. # The log message.
  262. shift 2
  263. local lname
  264. case "$level" in
  265. info) lname='INFO'; level=$LINFO ;;
  266. debug) lname='DEBUG'; level=$LDEBUG ;;
  267. *) printf 'Invalid logging level: %s\n' "$level" ;;
  268. esac
  269. printf '%s %s: %s\n' "$NAME" "$lname" "$message" 1>&$level
  270. }
  271. _helptext() {
  272. # Extract contiguous lines of comments and function params as help text
  273. #
  274. # Indentation will be ignored. She-bangs will be ignored. Local variable
  275. # declarations and their default values can also be pulled in as
  276. # documentation. Exits upon encountering the first blank line.
  277. #
  278. # Exported environment variables can be used for string interpolation in
  279. # the extracted commented text.
  280. #
  281. # Input
  282. #
  283. # * (stdin)
  284. # The text of a function body to parse.
  285. awk '
  286. NR != 1 && /^\s*#/ {
  287. line=$0
  288. while(match(line, "[$]{[^}]*}")) {
  289. var=substr(line, RSTART+2, RLENGTH -3)
  290. gsub("[$]{"var"}", ENVIRON[var], line)
  291. }
  292. gsub(/^\s*#\s?/, "", line)
  293. print line
  294. }
  295. /^\s*local/ {
  296. sub(/^\s*local /, "")
  297. sub(/\$\{/, "$", $0)
  298. sub(/:.*}/, "", $0)
  299. print "* `" $0 "`\n"
  300. }
  301. !NF { exit }'
  302. }
  303. # ### Request-response
  304. # Functions for making HTTP requests and processing HTTP responses.
  305. _format_json() {
  306. # Create formatted JSON from name=value pairs
  307. #
  308. # Usage:
  309. # ```
  310. # ok.sh _format_json foo=Foo bar=123 baz=true qux=Qux=Qux quux='Multi-line
  311. # string' quuz=\'5.20170918\' \
  312. # corge="$(ok.sh _format_json grault=Grault)" \
  313. # garply="$(ok.sh _format_json -a waldo true 3)"
  314. # ```
  315. #
  316. # Return:
  317. # ```
  318. # {
  319. # "garply": [
  320. # "waldo",
  321. # true,
  322. # 3
  323. # ],
  324. # "foo": "Foo",
  325. # "corge": {
  326. # "grault": "Grault"
  327. # },
  328. # "baz": true,
  329. # "qux": "Qux=Qux",
  330. # "quux": "Multi-line\nstring",
  331. # "quuz": "5.20170918",
  332. # "bar": 123
  333. # }
  334. # ```
  335. #
  336. # Tries not to quote numbers, booleans, nulls, or nested structures.
  337. # Note, nested structures must be quoted since the output contains spaces.
  338. #
  339. # The `-a` option will create an array instead of an object. This option
  340. # must come directly after the _format_json command and before any
  341. # operands. E.g., `_format_json -a foo bar baz`.
  342. #
  343. # If jq is installed it will also validate the output.
  344. #
  345. # Positional arguments
  346. #
  347. # * $1 - $9
  348. #
  349. # Each positional arg must be in the format of `name=value` which will be
  350. # added to a single, flat JSON object.
  351. local opt
  352. local OPTIND
  353. local is_array=0
  354. local use_env=1
  355. while getopts a opt; do
  356. case $opt in
  357. a) is_array=1; unset use_env;;
  358. esac
  359. done
  360. shift $(( OPTIND - 1 ))
  361. _log debug "Formatting ${#} parameters as JSON."
  362. env -i -- ${use_env+"$@"} awk -v is_array="$is_array" '
  363. function isnum(x){ return (x == x + 0) }
  364. function isnull(x){ return (x == "null" ) }
  365. function isbool(x){ if (x == "true" || x == "false") return 1 }
  366. function isnested(x) { if (substr(x, 0, 1) == "[" \
  367. || substr(x, 0, 1) == "{") return 1 }
  368. function castOrQuote(val) {
  369. if (!isbool(val) && !isnum(val) && !isnull(val) && !isnested(val)) {
  370. sub(/^('\''|")/, "", val) # Remove surrounding quotes
  371. sub(/('\''|")$/, "", val)
  372. gsub(/"/, "\\\"", val) # Escape double-quotes.
  373. gsub(/\n/, "\\n", val) # Replace newlines with \n text.
  374. val = "\"" val "\""
  375. return val
  376. } else {
  377. return val
  378. }
  379. }
  380. BEGIN {
  381. printf("%s", is_array ? "[" : "{")
  382. for (i = 1; i < length(ARGV); i += 1) {
  383. arg = ARGV[i]
  384. if (is_array == 1) {
  385. val = castOrQuote(arg)
  386. printf("%s%s", sep, val)
  387. } else {
  388. name = substr(arg, 0, index(arg, "=") - 1)
  389. val = castOrQuote(ENVIRON[name])
  390. printf("%s\"%s\": %s", sep, name, val)
  391. }
  392. sep = ", "
  393. ARGV[i] = ""
  394. }
  395. printf("%s", is_array ? "]" : "}")
  396. }' "$@"
  397. }
  398. _format_urlencode() {
  399. # URL encode and join name=value pairs
  400. #
  401. # Usage:
  402. # ```
  403. # _format_urlencode foo='Foo Foo' bar='<Bar>&/Bar/'
  404. # ```
  405. #
  406. # Return:
  407. # ```
  408. # foo=Foo%20Foo&bar=%3CBar%3E%26%2FBar%2F
  409. # ```
  410. #
  411. # Ignores pairs if the value begins with an underscore.
  412. _log debug "Formatting ${#} parameters as urlencoded"
  413. env -i -- "$@" awk '
  414. function escape(str, c, i, len, res) {
  415. len = length(str)
  416. res = ""
  417. for (i = 1; i <= len; i += 1) {
  418. c = substr(str, i, 1);
  419. if (c ~ /[0-9A-Za-z]/)
  420. res = res c
  421. else
  422. res = res "%" sprintf("%02X", ord[c])
  423. }
  424. return res
  425. }
  426. BEGIN {
  427. for (i = 0; i <= 255; i += 1) ord[sprintf("%c", i)] = i;
  428. for (j = 1; j < length(ARGV); j += 1) {
  429. arg = ARGV[j]
  430. name = substr(arg, 0, index(arg, "=") - 1)
  431. if (substr(name, 1, 1) == "_") continue
  432. val = ENVIRON[name]
  433. printf("%s%s=%s", sep, name, escape(val))
  434. sep = "&"
  435. ARGV[j] = ""
  436. }
  437. }' "$@"
  438. }
  439. _filter_json() {
  440. # Filter JSON input using jq; outputs raw JSON if jq is not installed
  441. #
  442. # Usage:
  443. #
  444. # printf '[{"foo": "One"}, {"foo": "Two"}]' | \
  445. # ok.sh _filter_json '.[] | "\(.foo)"'
  446. #
  447. # * (stdin)
  448. # JSON input.
  449. local _filter="$1"
  450. # A string of jq filters to apply to the input stream.
  451. _log debug 'Filtering JSON.'
  452. if [ $NO_JQ -ne 0 ] ; then
  453. _log debug 'Bypassing jq processing.'
  454. cat
  455. return
  456. fi
  457. "${OK_SH_JQ_BIN}" -c -r "${_filter}"
  458. [ $? -eq 0 ] || printf 'jq parse error; invalid JSON.\n' 1>&2
  459. }
  460. _get_mime_type() {
  461. # Guess the mime type for a file based on the file extension
  462. #
  463. # Usage:
  464. #
  465. # local mime_type
  466. # _get_mime_type "foo.tar"; printf 'mime is: %s' "$mime_type"
  467. #
  468. # Sets the global variable `mime_type` with the result. (If this function
  469. # is called from within a function that has declared a local variable of
  470. # that name it will update the local copy and not set a global.)
  471. #
  472. # Positional arguments
  473. #
  474. local filename="${1:?Filename is required.}"
  475. # The full name of the file, with extension.
  476. # Taken from Apache's mime.types file (public domain).
  477. case "$filename" in
  478. *.bz2) mime_type=application/x-bzip2 ;;
  479. *.exe) mime_type=application/x-msdownload ;;
  480. *.tar.gz | *.gz | *.tgz) mime_type=application/x-gzip ;;
  481. *.jpg | *.jpeg | *.jpe | *.jfif) mime_type=image/jpeg ;;
  482. *.json) mime_type=application/json ;;
  483. *.pdf) mime_type=application/pdf ;;
  484. *.png) mime_type=image/png ;;
  485. *.rpm) mime_type=application/x-rpm ;;
  486. *.svg | *.svgz) mime_type=image/svg+xml ;;
  487. *.tar) mime_type=application/x-tar ;;
  488. *.txt) mime_type=text/plain ;;
  489. *.yaml) mime_type=application/x-yaml ;;
  490. *.apk) mime_type=application/vnd.android.package-archive ;;
  491. *.zip) mime_type=application/zip ;;
  492. *.jar) mime_type=application/java-archive ;;
  493. *.war) mime_type=application/zip ;;
  494. esac
  495. _log debug "Guessed mime type of '${mime_type}' for '${filename}'."
  496. }
  497. _get_confirm() {
  498. # Prompt the user for confirmation
  499. #
  500. # Usage:
  501. #
  502. # local confirm; _get_confirm
  503. # [ "$confirm" -eq 1 ] && printf 'Good to go!\n'
  504. #
  505. # If global confirmation is set via `$OK_SH_DESTRUCTIVE` then the user
  506. # is not prompted. Assigns the user's confirmation to the `confirm` global
  507. # variable. (If this function is called within a function that has a local
  508. # variable of that name, the local variable will be updated instead.)
  509. #
  510. # Positional arguments
  511. #
  512. local message="${1:-Are you sure?}"
  513. # The message to prompt the user with.
  514. local answer
  515. if [ "$OK_SH_DESTRUCTIVE" -eq 1 ] ; then
  516. confirm=$OK_SH_DESTRUCTIVE
  517. return
  518. fi
  519. printf '%s ' "$message"
  520. read -r answer
  521. ! printf '%s\n' "$answer" | grep -Eq "$(locale yesexpr)"
  522. confirm=$?
  523. }
  524. _opts_filter() {
  525. # Extract common jq filter keyword options and assign to vars
  526. #
  527. # Usage:
  528. #
  529. # local filter
  530. # _opts_filter "$@"
  531. for arg in "$@"; do
  532. case $arg in
  533. (_filter=*) _filter="${arg#*=}";;
  534. esac
  535. done
  536. }
  537. _opts_pagination() {
  538. # Extract common pagination keyword options and assign to vars
  539. #
  540. # Usage:
  541. #
  542. # local _follow_next
  543. # _opts_pagination "$@"
  544. for arg in "$@"; do
  545. case $arg in
  546. (_follow_next=*) _follow_next="${arg#*=}";;
  547. (_follow_next_limit=*) _follow_next_limit="${arg#*=}";;
  548. esac
  549. done
  550. }
  551. _opts_qs() {
  552. # Extract common query string keyword options and assign to vars
  553. #
  554. # Usage:
  555. #
  556. # local qs
  557. # _opts_qs "$@"
  558. # _get "/some/path${qs}"
  559. local querystring=$(_format_urlencode "$@")
  560. qs="${querystring:+?$querystring}"
  561. }
  562. _request() {
  563. # A wrapper around making HTTP requests with curl
  564. #
  565. # Usage:
  566. # ```
  567. # # Get JSON for all issues:
  568. # _request /repos/saltstack/salt/issues
  569. #
  570. # # Send a POST request; parse response using jq:
  571. # printf '{"title": "%s", "body": "%s"}\n' "Stuff" "Things" \
  572. # | _request /some/path | jq -r '.[url]'
  573. #
  574. # # Send a PUT request; parse response using jq:
  575. # printf '{"title": "%s", "body": "%s"}\n' "Stuff" "Things" \
  576. # | _request /repos/:owner/:repo/issues method=PUT | jq -r '.[url]'
  577. #
  578. # # Send a conditional-GET request:
  579. # _request /users etag=edd3a0d38d8c329d3ccc6575f17a76bb
  580. # ```
  581. #
  582. # Input
  583. #
  584. # * (stdin)
  585. # Data that will be used as the request body.
  586. #
  587. # Positional arguments
  588. #
  589. local path="${1:?Path is required.}"
  590. # The URL path for the HTTP request.
  591. # Must be an absolute path that starts with a `/` or a full URL that
  592. # starts with http(s). Absolute paths will be append to the value in
  593. # `$OK_SH_URL`.
  594. #
  595. # Keyword arguments
  596. #
  597. local method='GET'
  598. # The method to use for the HTTP request.
  599. local content_type='application/json'
  600. # The value of the Content-Type header to use for the request.
  601. local etag
  602. # An optional Etag to send as the If-None-Match header.
  603. shift 1
  604. local cmd
  605. local arg
  606. local has_stdin
  607. local trace_curl
  608. case $path in
  609. (http*) : ;;
  610. *) path="${OK_SH_URL}${path}" ;;
  611. esac
  612. for arg in "$@"; do
  613. case $arg in
  614. (method=*) method="${arg#*=}";;
  615. (content_type=*) content_type="${arg#*=}";;
  616. (etag=*) etag="${arg#*=}";;
  617. esac
  618. done
  619. case "$method" in
  620. POST | PUT | PATCH) has_stdin=1;;
  621. esac
  622. [ $OK_SH_VERBOSE -eq 3 ] && trace_curl=1
  623. [ "$OK_SH_VERBOSE" -eq 1 ] && set -x
  624. # shellcheck disable=SC2086
  625. curl -nsSig \
  626. -H "Accept: ${OK_SH_ACCEPT}" \
  627. -H "Content-Type: ${content_type}" \
  628. ${GITHUB_TOKEN:+-H "Authorization: token ${GITHUB_TOKEN}"} \
  629. ${etag:+-H "If-None-Match: \"${etag}\""} \
  630. ${has_stdin:+--data-binary @-} \
  631. ${trace_curl:+--trace-ascii /dev/stderr} \
  632. -X "${method}" \
  633. "${path}"
  634. set +x
  635. }
  636. _response() {
  637. # Process an HTTP response from curl
  638. #
  639. # Output only headers of interest followed by the response body. Additional
  640. # processing is performed on select headers to make them easier to parse
  641. # using shell tools.
  642. #
  643. # Usage:
  644. # ```
  645. # # Send a request; output the response and only select response headers:
  646. # _request /some/path | _response status_code ETag Link_next
  647. #
  648. # # Make request using curl; output response with select response headers;
  649. # # assign response headers to local variables:
  650. # curl -isS example.com/some/path | _response status_code status_text | {
  651. # local status_code status_text
  652. # read -r status_code
  653. # read -r status_text
  654. # }
  655. # ```
  656. #
  657. # Header reformatting
  658. #
  659. # * HTTP Status
  660. #
  661. # The HTTP line is split into separate `http_version`, `status_code`, and
  662. # `status_text` variables.
  663. #
  664. # * ETag
  665. #
  666. # The surrounding quotes are removed.
  667. #
  668. # * Link
  669. #
  670. # Each URL in the Link header is expanded with the URL type appended to
  671. # the name. E.g., `Link_first`, `Link_last`, `Link_next`.
  672. #
  673. # Positional arguments
  674. #
  675. # * $1 - $9
  676. #
  677. # Each positional arg is the name of an HTTP header. Each header value is
  678. # output in the same order as each argument; each on a single line. A
  679. # blank line is output for headers that cannot be found.
  680. local hdr
  681. local val
  682. local http_version
  683. local status_code=100
  684. local status_text
  685. local headers output
  686. _log debug 'Processing response.'
  687. while [ "${status_code}" = "100" ]; do
  688. read -r http_version status_code status_text
  689. status_text="${status_text%${crlf}}"
  690. http_version="${http_version#HTTP/}"
  691. _log debug "Response status is: ${status_code} ${status_text}"
  692. if [ "${status_code}" = "100" ]; then
  693. _log debug "Ignoring response '${status_code} ${status_text}', skipping to real response."
  694. while IFS=": " read -r hdr val; do
  695. # Headers stop at the first blank line.
  696. [ "$hdr" = "$crlf" ] && break
  697. val="${val%${crlf}}"
  698. _log debug "Unexpected additional header: ${hdr}: ${val}"
  699. done
  700. fi
  701. done
  702. headers="http_version: ${http_version}
  703. status_code: ${status_code}
  704. status_text: ${status_text}
  705. "
  706. while IFS=": " read -r hdr val; do
  707. # Headers stop at the first blank line.
  708. [ "$hdr" = "$crlf" ] && break
  709. val="${val%${crlf}}"
  710. # Process each header; reformat some to work better with sh tools.
  711. case "$hdr" in
  712. # Update the GitHub rate limit trackers.
  713. X-RateLimit-Remaining)
  714. printf 'GitHub remaining requests: %s\n' "$val" 1>&$LSUMMARY ;;
  715. X-RateLimit-Reset)
  716. awk -v gh_reset="$val" 'BEGIN {
  717. srand(); curtime = srand()
  718. print "GitHub seconds to reset: " gh_reset - curtime
  719. }' 1>&$LSUMMARY ;;
  720. # Remove quotes from the etag header.
  721. ETag) val="${val#\"}"; val="${val%\"}" ;;
  722. # Split the URLs in the Link header into separate pseudo-headers.
  723. Link) headers="${headers}$(printf '%s' "$val" | awk '
  724. BEGIN { RS=", "; FS="; "; OFS=": " }
  725. {
  726. sub(/^rel="/, "", $2); sub(/"$/, "", $2)
  727. sub(/^ *</, "", $1); sub(/>$/, "", $1)
  728. print "Link_" $2, $1
  729. }')
  730. " # need trailing newline
  731. ;;
  732. esac
  733. headers="${headers}${hdr}: ${val}
  734. " # need trailing newline
  735. done
  736. # Output requested headers in deterministic order.
  737. for arg in "$@"; do
  738. _log debug "Outputting requested header '${arg}'."
  739. output=$(printf '%s' "$headers" | while IFS=": " read -r hdr val; do
  740. [ "$hdr" = "$arg" ] && printf '%s' "$val"
  741. done)
  742. printf '%s\n' "$output"
  743. done
  744. # Output the response body.
  745. cat
  746. }
  747. _get() {
  748. # A wrapper around _request() for common GET patterns
  749. #
  750. # Will automatically follow 'next' pagination URLs in the Link header.
  751. #
  752. # Usage:
  753. #
  754. # _get /some/path
  755. # _get /some/path _follow_next=0
  756. # _get /some/path _follow_next_limit=200 | jq -c .
  757. #
  758. # Positional arguments
  759. #
  760. local path="${1:?Path is required.}"
  761. # The HTTP path or URL to pass to _request().
  762. #
  763. # Keyword arguments
  764. #
  765. # * _follow_next=1
  766. #
  767. # Automatically look for a 'Links' header and follow any 'next' URLs.
  768. #
  769. # * _follow_next_limit=50
  770. #
  771. # Maximum number of 'next' URLs to follow before stopping.
  772. shift 1
  773. local status_code
  774. local status_text
  775. local next_url
  776. # If the variable is unset or empty set it to a default value. Functions
  777. # that call this function can pass these parameters in one of two ways:
  778. # explicitly as a keyword arg or implicitly by setting variables of the same
  779. # names within the local scope.
  780. # shellcheck disable=SC2086
  781. if [ -z ${_follow_next+x} ] || [ -z "${_follow_next}" ]; then
  782. local _follow_next=1
  783. fi
  784. # shellcheck disable=SC2086
  785. if [ -z ${_follow_next_limit+x} ] || [ -z "${_follow_next_limit}" ]; then
  786. local _follow_next_limit=50
  787. fi
  788. _opts_pagination "$@"
  789. _request "$path" | _response status_code status_text Link_next | {
  790. read -r status_code
  791. read -r status_text
  792. read -r next_url
  793. case "$status_code" in
  794. 20*) : ;;
  795. 4*) printf 'Client Error: %s %s\n' \
  796. "$status_code" "$status_text" 1>&2; exit 1 ;;
  797. 5*) printf 'Server Error: %s %s\n' \
  798. "$status_code" "$status_text" 1>&2; exit 1 ;;
  799. esac
  800. # Output response body.
  801. cat
  802. [ "$_follow_next" -eq 1 ] || return
  803. _log info "Remaining next link follows: ${_follow_next_limit}"
  804. if [ -n "$next_url" ] && [ $_follow_next_limit -gt 0 ] ; then
  805. _follow_next_limit=$(( _follow_next_limit - 1 ))
  806. _get "$next_url" "_follow_next_limit=${_follow_next_limit}"
  807. fi
  808. }
  809. }
  810. _post() {
  811. # A wrapper around _request() for common POST / PUT patterns
  812. #
  813. # Usage:
  814. #
  815. # _format_json foo=Foo bar=Bar | _post /some/path
  816. # _format_json foo=Foo bar=Bar | _post /some/path method='PUT'
  817. # _post /some/path filename=somearchive.tar
  818. # _post /some/path filename=somearchive.tar mime_type=application/x-tar
  819. # _post /some/path filename=somearchive.tar \
  820. # mime_type=$(file -b --mime-type somearchive.tar)
  821. #
  822. # Input
  823. #
  824. # * (stdin)
  825. # Optional. See the `filename` argument also.
  826. # Data that will be used as the request body.
  827. #
  828. # Positional arguments
  829. #
  830. local path="${1:?Path is required.}"
  831. # The HTTP path or URL to pass to _request().
  832. #
  833. # Keyword arguments
  834. #
  835. local method='POST'
  836. # The method to use for the HTTP request.
  837. local filename
  838. # Optional. See the `stdin` option above also.
  839. # Takes precedence over any data passed as stdin and loads a file off the
  840. # file system to serve as the request body.
  841. local mime_type
  842. # The value of the Content-Type header to use for the request.
  843. # If the `filename` argument is given this value will be guessed from the
  844. # file extension. If the `filename` argument is not given (i.e., using
  845. # stdin) this value defaults to `application/json`. Specifying this
  846. # argument overrides all other defaults or guesses.
  847. shift 1
  848. for arg in "$@"; do
  849. case $arg in
  850. (method=*) method="${arg#*=}";;
  851. (filename=*) filename="${arg#*=}";;
  852. (mime_type=*) mime_type="${arg#*=}";;
  853. esac
  854. done
  855. # Make either the file or stdin available as fd7.
  856. if [ -n "$filename" ] ; then
  857. if [ -r "$filename" ] ; then
  858. _log debug "Using '${filename}' as POST data."
  859. [ -n "$mime_type" ] || _get_mime_type "$filename"
  860. : ${mime_type:?The MIME type could not be guessed.}
  861. exec 7<"$filename"
  862. else
  863. printf 'File could not be found or read.\n' 1>&2
  864. exit 1
  865. fi
  866. else
  867. _log debug "Using stdin as POST data."
  868. mime_type='application/json'
  869. exec 7<&0
  870. fi
  871. _request "$path" method="$method" content_type="$mime_type" 0<&7 \
  872. | _response status_code status_text \
  873. | {
  874. read -r status_code
  875. read -r status_text
  876. case "$status_code" in
  877. 20*) : ;;
  878. 4*) printf 'Client Error: %s %s\n' \
  879. "$status_code" "$status_text" 1>&2; exit 1 ;;
  880. 5*) printf 'Server Error: %s %s\n' \
  881. "$status_code" "$status_text" 1>&2; exit 1 ;;
  882. esac
  883. # Output response body.
  884. cat
  885. }
  886. }
  887. _delete() {
  888. # A wrapper around _request() for common DELETE patterns
  889. #
  890. # Usage:
  891. #
  892. # _delete '/some/url'
  893. #
  894. # Return: 0 for success; 1 for failure.
  895. #
  896. # Positional arguments
  897. #
  898. local url="${1:?URL is required.}"
  899. # The URL to send the DELETE request to.
  900. local status_code
  901. _request "${url}" method='DELETE' | _response status_code | {
  902. read -r status_code
  903. [ "$status_code" = "204" ]
  904. exit $?
  905. }
  906. }
  907. # ## GitHub
  908. # Friendly functions for common GitHub tasks.
  909. # ### Authorization
  910. # Perform authentication and authorization.
  911. show_scopes() {
  912. # Show the permission scopes for the currently authenticated user
  913. #
  914. # Usage:
  915. #
  916. # show_scopes
  917. local oauth_scopes
  918. _request '/' | _response X-OAuth-Scopes | {
  919. read -r oauth_scopes
  920. printf '%s\n' "$oauth_scopes"
  921. # Dump any remaining response body.
  922. cat >/dev/null
  923. }
  924. }
  925. # ### Repository
  926. # Create, update, delete, list repositories.
  927. org_repos() {
  928. # List organization repositories
  929. #
  930. # Usage:
  931. #
  932. # org_repos myorg
  933. # org_repos myorg type=private per_page=10
  934. # org_repos myorg _filter='.[] | "\(.name)\t\(.owner.login)"'
  935. #
  936. # Positional arguments
  937. #
  938. local org="${1:?Org name required.}"
  939. # Organization GitHub login or id for which to list repos.
  940. #
  941. # Keyword arguments
  942. #
  943. local _follow_next
  944. # Automatically look for a 'Links' header and follow any 'next' URLs.
  945. local _follow_next_limit
  946. # Maximum number of 'next' URLs to follow before stopping.
  947. local _filter='.[] | "\(.name)\t\(.ssh_url)"'
  948. # A jq filter to apply to the return data.
  949. #
  950. # Querystring arguments may also be passed as keyword arguments:
  951. #
  952. # * `per_page`
  953. # * `type`
  954. shift 1
  955. local qs
  956. _opts_pagination "$@"
  957. _opts_filter "$@"
  958. _opts_qs "$@"
  959. _get "/orgs/${org}/repos${qs}" | _filter_json "${_filter}"
  960. }
  961. org_teams() {
  962. # List teams
  963. #
  964. # Usage:
  965. #
  966. # org_teams org
  967. #
  968. # Positional arguments
  969. #
  970. local org="${1:?Org name required.}"
  971. # Organization GitHub login or id.
  972. #
  973. # Keyword arguments
  974. #
  975. local _filter='.[] | "\(.name)\t\(.id)\t\(.permission)"'
  976. # A jq filter to apply to the return data.
  977. shift 1
  978. _opts_filter "$@"
  979. _get "/orgs/${org}/teams" \
  980. | _filter_json "${_filter}"
  981. }
  982. org_members() {
  983. # List organization members
  984. #
  985. # Usage:
  986. #
  987. # org_members org
  988. #
  989. # Positional arguments
  990. #
  991. local org="${1:?Org name required.}"
  992. # Organization GitHub login or id.
  993. #
  994. # Keyword arguments
  995. #
  996. local _filter='.[] | "\(.login)\t\(.id)"'
  997. # A jq filter to apply to the return data.
  998. shift 1
  999. _opts_filter "$@"
  1000. _get "/orgs/${org}/members" \
  1001. | _filter_json "${_filter}"
  1002. }
  1003. team_members() {
  1004. # List team members
  1005. #
  1006. # Usage:
  1007. #
  1008. # team_members team_id
  1009. #
  1010. # Positional arguments
  1011. #
  1012. local team_id="${1:?Team id required.}"
  1013. # Team id.
  1014. #
  1015. # Keyword arguments
  1016. #
  1017. local _filter='.[] | "\(.login)\t\(.id)"'
  1018. # A jq filter to apply to the return data.
  1019. shift 1
  1020. _opts_filter "$@"
  1021. _get "/teams/${team_id}/members" \
  1022. | _filter_json "${_filter}"
  1023. }
  1024. list_repos() {
  1025. # List user repositories
  1026. #
  1027. # Usage:
  1028. #
  1029. # list_repos
  1030. # list_repos user
  1031. #
  1032. # Positional arguments
  1033. #
  1034. local user="$1"
  1035. # Optional GitHub user login or id for which to list repos.
  1036. #
  1037. # Keyword arguments
  1038. #
  1039. local _filter='.[] | "\(.name)\t\(.html_url)"'
  1040. # A jq filter to apply to the return data.
  1041. #
  1042. # Querystring arguments may also be passed as keyword arguments:
  1043. #
  1044. # * `direction`
  1045. # * `per_page`
  1046. # * `sort`
  1047. # * `type`
  1048. shift 1
  1049. local qs
  1050. _opts_filter "$@"
  1051. _opts_qs "$@"
  1052. if [ -n "$user" ] ; then
  1053. url="/users/${user}/repos"
  1054. else
  1055. url='/user/repos'
  1056. fi
  1057. _get "${url}${qs}" | _filter_json "${_filter}"
  1058. }
  1059. list_branches() {
  1060. # List branches of a specified repository.
  1061. # ( https://developer.github.com/v3/repos/#list_branches )
  1062. #
  1063. # Usage:
  1064. #
  1065. # list_branches user repo
  1066. #
  1067. # Positional arguments
  1068. #
  1069. # GitHub user login or id for which to list branches
  1070. # Name of the repo for which to list branches
  1071. #
  1072. local user="${1:?User name required.}"
  1073. local repo="${2:?Repo name required.}"
  1074. shift 2
  1075. #
  1076. # Keyword arguments
  1077. #
  1078. local _filter='.[] | "\(.name)"'
  1079. # A jq filter to apply to the return data.
  1080. #
  1081. # Querystring arguments may also be passed as keyword arguments:
  1082. #
  1083. # * `direction`
  1084. # * `per_page`
  1085. # * `sort`
  1086. # * `type`
  1087. local qs
  1088. _opts_filter "$@"
  1089. _opts_qs "$@"
  1090. url="/repos/${user}/${repo}/branches"
  1091. _get "${url}${qs}" | _filter_json "${_filter}"
  1092. }
  1093. list_contributors() {
  1094. # List contributors to the specified repository, sorted by the number of commits per contributor in descending order.
  1095. # ( https://developer.github.com/v3/repos/#list-contributors )
  1096. #
  1097. # Usage:
  1098. #
  1099. # list_contributors user repo
  1100. #
  1101. # Positional arguments
  1102. #
  1103. local user="${1:?User name required.}"
  1104. # GitHub user login or id for which to list contributors
  1105. local repo="${2:?Repo name required.}"
  1106. # Name of the repo for which to list contributors
  1107. #
  1108. # Keyword arguments
  1109. #
  1110. local _filter='.[] | "\(.login)\t\(.type)\tType:\(.type)\tContributions:\(.contributions)"'
  1111. # A jq filter to apply to the return data.
  1112. #
  1113. # Querystring arguments may also be passed as keyword arguments:
  1114. #
  1115. # * `direction`
  1116. # * `per_page`
  1117. # * `sort`
  1118. # * `type`
  1119. shift 2
  1120. local qs
  1121. _opts_filter "$@"
  1122. _opts_qs "$@"
  1123. url="/repos/${user}/${repo}/contributors"
  1124. _get "${url}${qs}" | _filter_json "${_filter}"
  1125. }
  1126. list_collaborators() {
  1127. # List collaborators to the specified repository, sorted by the number of commits per collaborator in descending order.
  1128. # ( https://developer.github.com/v3/repos/#list-collaborators )
  1129. #
  1130. # Usage:
  1131. #
  1132. # list_collaborators someuser/somerepo
  1133. #
  1134. # Positional arguments
  1135. # GitHub user login or id for which to list collaborators
  1136. # Name of the repo for which to list collaborators
  1137. #
  1138. local repo="${1:?Repo name required.}"
  1139. #
  1140. # Keyword arguments
  1141. #
  1142. local _filter='.[] | "\(.login)\t\(.type)\tType:\(.type)\tPermissions:\(.permissions)"'
  1143. # A jq filter to apply to the return data.
  1144. #
  1145. # Querystring arguments may also be passed as keyword arguments:
  1146. #
  1147. # * `direction`
  1148. # * `per_page`
  1149. # * `sort`
  1150. # * `type`
  1151. shift 1
  1152. local qs
  1153. _opts_filter "$@"
  1154. _opts_qs "$@"
  1155. url="/repos/${repo}/collaborators"
  1156. _get "${url}${qs}" | _filter_json "${_filter}"
  1157. }
  1158. list_hooks() {
  1159. # List webhooks from the specified repository.
  1160. # ( https://developer.github.com/v3/repos/hooks/#list-hooks )
  1161. #
  1162. # Usage:
  1163. #
  1164. # list_hooks owner/repo
  1165. #
  1166. # Positional arguments
  1167. #
  1168. local repo="${1:?Repo name required.}"
  1169. # Name of the repo for which to list contributors
  1170. # Owner is mandatory, like 'owner/repo'
  1171. #
  1172. local _filter='.[] | "\(.name)\t\(.config.url)"'
  1173. # A jq filter to apply to the return data.
  1174. #
  1175. shift 1
  1176. _opts_filter "$@"
  1177. url="/repos/${repo}/hooks"
  1178. _get "${url}" | _filter_json "${_filter}"
  1179. }
  1180. list_gists() {
  1181. # List gists for the current authenticated user or a specific user
  1182. #
  1183. # https://developer.github.com/v3/gists/#list-a-users-gists
  1184. #
  1185. # Usage:
  1186. #
  1187. # list_gists
  1188. # list_gists <username>
  1189. #
  1190. # Positional arguments
  1191. #
  1192. local username="$1"
  1193. # An optional user to filter listing
  1194. #
  1195. # Keyword arguments
  1196. #
  1197. local _follow_next
  1198. # Automatically look for a 'Links' header and follow any 'next' URLs.
  1199. local _follow_next_limit
  1200. # Maximum number of 'next' URLs to follow before stopping.
  1201. local _filter='.[] | "\(.id)\t\(.description)"'
  1202. # A jq filter to apply to the return data.
  1203. local url
  1204. case "$username" in
  1205. ('') url='/gists';;
  1206. (*=*) url='/gists';;
  1207. (*) url="/users/${username}/gists"; shift 1;;
  1208. esac
  1209. _opts_pagination "$@"
  1210. _opts_filter "$@"
  1211. _get "${url}" | _filter_json "${_filter}"
  1212. }
  1213. public_gists() {
  1214. # List public gists
  1215. #
  1216. # https://developer.github.com/v3/gists/#list-all-public-gists
  1217. #
  1218. # Usage:
  1219. #
  1220. # public_gists
  1221. #
  1222. # Keyword arguments
  1223. #
  1224. local _follow_next
  1225. # Automatically look for a 'Links' header and follow any 'next' URLs.
  1226. local _follow_next_limit
  1227. # Maximum number of 'next' URLs to follow before stopping.
  1228. local _filter='.[] | "\(.id)\t\(.description)"'
  1229. # A jq filter to apply to the return data.
  1230. _opts_pagination "$@"
  1231. _opts_filter "$@"
  1232. _get '/gists/public' | _filter_json "${_filter}"
  1233. }
  1234. gist() {
  1235. # Get a single gist
  1236. #
  1237. # https://developer.github.com/v3/gists/#get-a-single-gist
  1238. #
  1239. # Usage:
  1240. #
  1241. # get_gist
  1242. #
  1243. # Positional arguments
  1244. #
  1245. local gist_id="${1:?Gist ID required.}"
  1246. # ID of gist to fetch.
  1247. #
  1248. # Keyword arguments
  1249. #
  1250. local _filter='.files | keys | join(", ")'
  1251. # A jq filter to apply to the return data.
  1252. shift 1
  1253. _opts_filter "$@"
  1254. _get "/gists/${gist_id}" | _filter_json "${_filter}"
  1255. }
  1256. add_collaborator() {
  1257. # Add a collaborator to a repository
  1258. #
  1259. # Usage:
  1260. #
  1261. # add_collaborator someuser/somerepo collaboratoruser permission
  1262. #
  1263. # Positional arguments
  1264. #
  1265. local repo="${1:?Repo name required.}"
  1266. # A GitHub repository.
  1267. local collaborator="${2:?Collaborator name required.}"
  1268. # A new collaborator.
  1269. local permission="${3:?Permission required. One of: push pull admin}"
  1270. # The permission level for this collaborator. One of `push`, `pull`,
  1271. # `admin`. The `pull` and `admin` permissions are valid for organization
  1272. # repos only.
  1273. case $permission in
  1274. push|pull|admin) :;;
  1275. *) printf 'Permission invalid: %s\nMust be one of: push pull admin\n' \
  1276. "$permission" 1>&2; exit 1 ;;
  1277. esac
  1278. #
  1279. # Keyword arguments
  1280. #
  1281. local _filter='"\(.name)\t\(.color)"'
  1282. # A jq filter to apply to the return data.
  1283. _opts_filter "$@"
  1284. _format_json permission="$permission" \
  1285. | _post "/repos/${repo}/collaborators/${collaborator}" method='PUT' \
  1286. | _filter_json "$_filter"
  1287. }
  1288. delete_collaborator() {
  1289. # Delete a collaborator to a repository
  1290. #
  1291. # Usage:
  1292. #
  1293. # delete_collaborator someuser/somerepo collaboratoruser permission
  1294. #
  1295. # Positional arguments
  1296. #
  1297. local repo="${1:?Repo name required.}"
  1298. # A GitHub repository.
  1299. local collaborator="${2:?Collaborator name required.}"
  1300. # A new collaborator.
  1301. shift 2
  1302. local confirm
  1303. _get_confirm 'This will permanently delete the collaborator from this repo. Continue?'
  1304. [ "$confirm" -eq 1 ] || exit 0
  1305. _delete "/repos/${repo}/collaborators/${collaborator}"
  1306. exit $?
  1307. }
  1308. create_repo() {
  1309. # Create a repository for a user or organization
  1310. #
  1311. # Usage:
  1312. #
  1313. # create_repo foo
  1314. # create_repo bar description='Stuff and things' homepage='example.com'
  1315. # create_repo baz organization=myorg
  1316. #
  1317. # Positional arguments
  1318. #
  1319. local name="${1:?Repo name required.}"
  1320. # Name of the new repo
  1321. #
  1322. # Keyword arguments
  1323. #
  1324. local _filter='"\(.name)\t\(.html_url)"'
  1325. # A jq filter to apply to the return data.
  1326. #
  1327. # POST data may also be passed as keyword arguments:
  1328. #
  1329. # * `auto_init`,
  1330. # * `description`
  1331. # * `gitignore_template`
  1332. # * `has_downloads`
  1333. # * `has_issues`
  1334. # * `has_wiki`,
  1335. # * `homepage`
  1336. # * `organization`
  1337. # * `private`
  1338. # * `team_id`
  1339. shift 1
  1340. _opts_filter "$@"
  1341. local url
  1342. local organization
  1343. for arg in "$@"; do
  1344. case $arg in
  1345. (organization=*) organization="${arg#*=}";;
  1346. esac
  1347. done
  1348. if [ -n "$organization" ] ; then
  1349. url="/orgs/${organization}/repos"
  1350. else
  1351. url='/user/repos'
  1352. fi
  1353. _format_json "name=${name}" "$@" | _post "$url" | _filter_json "${_filter}"
  1354. }
  1355. delete_repo() {
  1356. # Delete a repository for a user or organization
  1357. #
  1358. # Usage:
  1359. #
  1360. # delete_repo owner repo
  1361. #
  1362. # The currently authenticated user must have the `delete_repo` scope. View
  1363. # current scopes with the `show_scopes()` function.
  1364. #
  1365. # Positional arguments
  1366. #
  1367. local owner="${1:?Owner name required.}"
  1368. # Name of the new repo
  1369. local repo="${2:?Repo name required.}"
  1370. # Name of the new repo
  1371. shift 2
  1372. local confirm
  1373. _get_confirm 'This will permanently delete a repository! Continue?'
  1374. [ "$confirm" -eq 1 ] || exit 0
  1375. _delete "/repos/${owner}/${repo}"
  1376. exit $?
  1377. }
  1378. fork_repo() {
  1379. # Fork a repository from a user or organization to own account or organization
  1380. #
  1381. # Usage:
  1382. #
  1383. # fork_repo owner repo
  1384. #
  1385. # Positional arguments
  1386. #
  1387. local owner="${1:?Owner name required.}"
  1388. # Name of existing user or organization
  1389. local repo="${2:?Repo name required.}"
  1390. # Name of the existing repo
  1391. #
  1392. #
  1393. # Keyword arguments
  1394. #
  1395. local _filter='"\(.clone_url)\t\(.ssh_url)"'
  1396. # A jq filter to apply to the return data.
  1397. #
  1398. # POST data may also be passed as keyword arguments:
  1399. #
  1400. # * `organization` (The organization to clone into; default: your personal account)
  1401. shift 2
  1402. _opts_filter "$@"
  1403. _format_json "$@" | _post "/repos/${owner}/${repo}/forks" \
  1404. | _filter_json "${_filter}"
  1405. exit $? # might take a bit time...
  1406. }
  1407. # ### Releases
  1408. # Create, update, delete, list releases.
  1409. list_releases() {
  1410. # List releases for a repository
  1411. #
  1412. # https://developer.github.com/v3/repos/releases/#list-releases-for-a-repository
  1413. #
  1414. # Usage:
  1415. #
  1416. # list_releases org repo '\(.assets[0].name)\t\(.name.id)'
  1417. #
  1418. # Positional arguments
  1419. #
  1420. local owner="${1:?Owner name required.}"
  1421. # A GitHub user or organization.
  1422. local repo="${2:?Repo name required.}"
  1423. # A GitHub repository.
  1424. #
  1425. # Keyword arguments
  1426. #
  1427. local _filter='.[] | "\(.name)\t\(.tag_name)\t\(.id)\t\(.html_url)"'
  1428. # A jq filter to apply to the return data.
  1429. shift 2
  1430. _opts_filter "$@"
  1431. _get "/repos/${owner}/${repo}/releases" \
  1432. | _filter_json "${_filter}"
  1433. }
  1434. release() {
  1435. # Get a release
  1436. #
  1437. # https://developer.github.com/v3/repos/releases/#get-a-single-release
  1438. #
  1439. # Usage:
  1440. #
  1441. # release user repo 1087855
  1442. #
  1443. # Positional arguments
  1444. #
  1445. local owner="${1:?Owner name required.}"
  1446. # A GitHub user or organization.
  1447. local repo="${2:?Repo name required.}"
  1448. # A GitHub repository.
  1449. local release_id="${3:?Release ID required.}"
  1450. # The unique ID of the release; see list_releases.
  1451. #
  1452. # Keyword arguments
  1453. #
  1454. local _filter='"\(.author.login)\t\(.published_at)"'
  1455. # A jq filter to apply to the return data.
  1456. shift 3
  1457. _opts_filter "$@"
  1458. _get "/repos/${owner}/${repo}/releases/${release_id}" \
  1459. | _filter_json "${_filter}"
  1460. }
  1461. create_release() {
  1462. # Create a release
  1463. #
  1464. # https://developer.github.com/v3/repos/releases/#create-a-release
  1465. #
  1466. # Usage:
  1467. #
  1468. # create_release org repo v1.2.3
  1469. # create_release user repo v3.2.1 draft=true
  1470. #
  1471. # Positional arguments
  1472. #
  1473. local owner="${1:?Owner name required.}"
  1474. # A GitHub user or organization.
  1475. local repo="${2:?Repo name required.}"
  1476. # A GitHub repository.
  1477. local tag_name="${3:?Tag name required.}"
  1478. # Git tag from which to create release.
  1479. #
  1480. # Keyword arguments
  1481. #
  1482. local _filter='"\(.name)\t\(.id)\t\(.html_url)"'
  1483. # A jq filter to apply to the return data.
  1484. #
  1485. # POST data may also be passed as keyword arguments:
  1486. #
  1487. # * `body`
  1488. # * `draft`
  1489. # * `name`
  1490. # * `prerelease`
  1491. # * `target_commitish`
  1492. shift 3
  1493. _opts_filter "$@"
  1494. _format_json "tag_name=${tag_name}" "$@" \
  1495. | _post "/repos/${owner}/${repo}/releases" \
  1496. | _filter_json "${_filter}"
  1497. }
  1498. edit_release() {
  1499. # Edit a release
  1500. #
  1501. # https://developer.github.com/v3/repos/releases/#edit-a-release
  1502. #
  1503. # Usage:
  1504. #
  1505. # edit_release org repo 1087855 name='Foo Bar 1.4.6'
  1506. # edit_release user repo 1087855 draft=false
  1507. #
  1508. # Positional arguments
  1509. #
  1510. local owner="${1:?Owner name required.}"
  1511. # A GitHub user or organization.
  1512. local repo="${2:?Repo name required.}"
  1513. # A GitHub repository.
  1514. local release_id="${3:?Release ID required.}"
  1515. # The unique ID of the release; see list_releases.
  1516. #
  1517. # Keyword arguments
  1518. #
  1519. local _filter='"\(.tag_name)\t\(.name)\t\(.html_url)"'
  1520. # A jq filter to apply to the return data.
  1521. #
  1522. # POST data may also be passed as keyword arguments:
  1523. #
  1524. # * `tag_name`
  1525. # * `body`
  1526. # * `draft`
  1527. # * `name`
  1528. # * `prerelease`
  1529. # * `target_commitish`
  1530. shift 3
  1531. _opts_filter "$@"
  1532. _format_json "$@" \
  1533. | _post "/repos/${owner}/${repo}/releases/${release_id}" method="PATCH" \
  1534. | _filter_json "${_filter}"
  1535. }
  1536. delete_release() {
  1537. # Delete a release
  1538. #
  1539. # https://developer.github.com/v3/repos/releases/#delete-a-release
  1540. #
  1541. # Usage:
  1542. #
  1543. # delete_release org repo 1087855
  1544. #
  1545. # Return: 0 for success; 1 for failure.
  1546. #
  1547. # Positional arguments
  1548. #
  1549. local owner="${1:?Owner name required.}"
  1550. # A GitHub user or organization.
  1551. local repo="${2:?Repo name required.}"
  1552. # A GitHub repository.
  1553. local release_id="${3:?Release ID required.}"
  1554. # The unique ID of the release; see list_releases.
  1555. shift 3
  1556. local confirm
  1557. _get_confirm 'This will permanently delete a release. Continue?'
  1558. [ "$confirm" -eq 1 ] || exit 0
  1559. _delete "/repos/${owner}/${repo}/releases/${release_id}"
  1560. exit $?
  1561. }
  1562. release_assets() {
  1563. # List release assets
  1564. #
  1565. # https://developer.github.com/v3/repos/releases/#list-assets-for-a-release
  1566. #
  1567. # Usage:
  1568. #
  1569. # release_assets user repo 1087855
  1570. #
  1571. # Example of downloading release assets:
  1572. #
  1573. # ok.sh release_assets <user> <repo> <release_id> \
  1574. # _filter='.[] | .browser_download_url' \
  1575. # | xargs -L1 curl -L -O
  1576. #
  1577. # Example of the multi-step process for grabbing the release ID for
  1578. # a specific version, then grabbing the release asset IDs, and then
  1579. # downloading all the release assets (whew!):
  1580. #
  1581. # username='myuser'
  1582. # repo='myrepo'
  1583. # release_tag='v1.2.3'
  1584. # ok.sh list_releases "$myuser" "$myrepo" \
  1585. # | awk -F'\t' -v tag="$release_tag" '$2 == tag { print $3 }' \
  1586. # | xargs -I{} ./ok.sh release_assets "$myuser" "$myrepo" {} \
  1587. # _filter='.[] | .browser_download_url' \
  1588. # | xargs -L1 curl -n -L -O
  1589. #
  1590. # Positional arguments
  1591. #
  1592. local owner="${1:?Owner name required.}"
  1593. # A GitHub user or organization.
  1594. local repo="${2:?Repo name required.}"
  1595. # A GitHub repository.
  1596. local release_id="${3:?Release ID required.}"
  1597. # The unique ID of the release; see list_releases.
  1598. #
  1599. # Keyword arguments
  1600. #
  1601. local _filter='.[] | "\(.id)\t\(.name)\t\(.updated_at)"'
  1602. # A jq filter to apply to the return data.
  1603. shift 3
  1604. _opts_filter "$@"
  1605. _get "/repos/${owner}/${repo}/releases/${release_id}/assets" \
  1606. | _filter_json "$_filter"
  1607. }
  1608. upload_asset() {
  1609. # Upload a release asset
  1610. #
  1611. # https://developer.github.com/v3/repos/releases/#upload-a-release-asset
  1612. #
  1613. # Usage:
  1614. #
  1615. # upload_asset https://<upload-url> /path/to/file.zip
  1616. #
  1617. # The upload URL can be gotten from `release()`. There are multiple steps
  1618. # required to upload a file: get the release ID, get the upload URL, parse
  1619. # the upload URL, then finally upload the file. For example:
  1620. #
  1621. # ```sh
  1622. # USER="someuser"
  1623. # REPO="somerepo"
  1624. # TAG="1.2.3"
  1625. # FILE_NAME="foo.zip"
  1626. # FILE_PATH="/path/to/foo.zip"
  1627. #
  1628. # # Create a release then upload a file:
  1629. # ok.sh create_release "$USER" "$REPO" "$TAG" _filter='.upload_url' \
  1630. # | sed 's/{.*$/?name='"$FILE_NAME"'/' \
  1631. # | xargs -I@ ok.sh upload_asset @ "$FILE_PATH"
  1632. #
  1633. # # Find a release by tag then upload a file:
  1634. # ok.sh list_releases "$USER" "$REPO" \
  1635. # | awk -v "tag=$TAG" -F'\t' '$2 == tag { print $3 }' \
  1636. # | xargs -I@ ok.sh release "$USER" "$REPO" @ _filter='.upload_url' \
  1637. # | sed 's/{.*$/?name='"$FILE_NAME"'/' \
  1638. # | xargs -I@ ok.sh upload_asset @ "$FILE_PATH"
  1639. # ```
  1640. #
  1641. # Positional arguments
  1642. #
  1643. local upload_url="${1:?upload_url is required.}"
  1644. # The _parsed_ upload_url returned from GitHub.
  1645. #
  1646. local file_path="${2:?file_path is required.}"
  1647. # A path to the file that should be uploaded.
  1648. #
  1649. # Keyword arguments
  1650. #
  1651. local _filter='"\(.state)\t\(.browser_download_url)"'
  1652. # A jq filter to apply to the return data.
  1653. #
  1654. # Also any other keyword arguments accepted by `_post()`.
  1655. shift 2
  1656. _opts_filter "$@"
  1657. _post "$upload_url" filename="$file_path" "$@" \
  1658. | _filter_json "$_filter"
  1659. }
  1660. # ### Issues
  1661. # Create, update, edit, delete, list issues and milestones.
  1662. list_milestones() {
  1663. # List milestones for a repository
  1664. #
  1665. # Usage:
  1666. #
  1667. # list_milestones someuser/somerepo
  1668. # list_milestones someuser/somerepo state=closed
  1669. #
  1670. # Positional arguments
  1671. #
  1672. local repository="${1:?Repo name required.}"
  1673. # A GitHub repository.
  1674. #
  1675. # Keyword arguments
  1676. #
  1677. local _follow_next
  1678. # Automatically look for a 'Links' header and follow any 'next' URLs.
  1679. local _follow_next_limit
  1680. # Maximum number of 'next' URLs to follow before stopping.
  1681. local _filter='.[] | "\(.number)\t\(.open_issues)/\(.closed_issues)\t\(.title)"'
  1682. # A jq filter to apply to the return data.
  1683. #
  1684. # GitHub querystring arguments may also be passed as keyword arguments:
  1685. #
  1686. # * `direction`
  1687. # * `per_page`
  1688. # * `sort`
  1689. # * `state`
  1690. shift 1
  1691. local qs
  1692. _opts_pagination "$@"
  1693. _opts_filter "$@"
  1694. _opts_qs "$@"
  1695. _get "/repos/${repository}/milestones${qs}" | _filter_json "$_filter"
  1696. }
  1697. create_milestone() {
  1698. # Create a milestone for a repository
  1699. #
  1700. # Usage:
  1701. #
  1702. # create_milestone someuser/somerepo MyMilestone
  1703. #
  1704. # create_milestone someuser/somerepo MyMilestone \
  1705. # due_on=2015-06-16T16:54:00Z \
  1706. # description='Long description here
  1707. # that spans multiple lines.'
  1708. #
  1709. # Positional arguments
  1710. #
  1711. local repo="${1:?Repo name required.}"
  1712. # A GitHub repository.
  1713. local title="${2:?Milestone name required.}"
  1714. # A unique title.
  1715. #
  1716. # Keyword arguments
  1717. #
  1718. local _filter='"\(.number)\t\(.html_url)"'
  1719. # A jq filter to apply to the return data.
  1720. #
  1721. # Milestone options may also be passed as keyword arguments:
  1722. #
  1723. # * `description`
  1724. # * `due_on`
  1725. # * `state`
  1726. shift 2
  1727. _opts_filter "$@"
  1728. _format_json title="$title" "$@" \
  1729. | _post "/repos/${repo}/milestones" \
  1730. | _filter_json "$_filter"
  1731. }
  1732. add_comment() {
  1733. # Add a comment to an issue
  1734. #
  1735. # Usage:
  1736. #
  1737. # add_comment someuser/somerepo 123 'This is a comment'
  1738. #
  1739. # Positional arguments
  1740. #
  1741. local repository="${1:?Repo name required}"
  1742. # A GitHub repository
  1743. local number="${2:?Issue number required}"
  1744. # Issue Number
  1745. local comment="${3:?Comment required}"
  1746. # Comment to be added
  1747. #
  1748. # Keyword arguments
  1749. #
  1750. local _filter='"\(.id)\t\(.html_url)"'
  1751. # A jq filter to apply to the return data.
  1752. shift 3
  1753. _opts_filter "$@"
  1754. _format_json body="$comment" \
  1755. | _post "/repos/${repository}/issues/${number}/comments" \
  1756. | _filter_json "${_filter}"
  1757. }
  1758. add_commit_comment() {
  1759. # Add a comment to a commit
  1760. #
  1761. # Usage:
  1762. #
  1763. # add_commit_comment someuser/somerepo 123 'This is a comment'
  1764. #
  1765. # Positional arguments
  1766. #
  1767. local repository="${1:?Repo name required}"
  1768. # A GitHub repository
  1769. local hash="${2:?Commit hash required}"
  1770. # Commit hash
  1771. local comment="${3:?Comment required}"
  1772. # Comment to be added
  1773. #
  1774. # Keyword arguments
  1775. #
  1776. local _filter='"\(.id)\t\(.html_url)"'
  1777. # A jq filter to apply to the return data.
  1778. shift 3
  1779. _opts_filter "$@"
  1780. _format_json body="$comment" \
  1781. | _post "/repos/${repository}/commits/${hash}/comments" \
  1782. | _filter_json "${_filter}"
  1783. }
  1784. close_issue() {
  1785. # Close an issue
  1786. #
  1787. # Usage:
  1788. #
  1789. # close_issue someuser/somerepo 123
  1790. #
  1791. # Positional arguments
  1792. #
  1793. local repository="${1:?Repo name required}"
  1794. # A GitHub repository
  1795. local number="${2:?Issue number required}"
  1796. # Issue Number
  1797. #
  1798. # Keyword arguments
  1799. #
  1800. local _filter='"\(.id)\t\(.state)\t\(.html_url)"'
  1801. # A jq filter to apply to the return data.
  1802. #
  1803. # POST data may also be passed as keyword arguments:
  1804. #
  1805. # * `assignee`
  1806. # * `labels`
  1807. # * `milestone`
  1808. shift 2
  1809. _opts_filter "$@"
  1810. _format_json state="closed" "$@" \
  1811. | _post "/repos/${repository}/issues/${number}" method='PATCH' \
  1812. | _filter_json "${_filter}"
  1813. }
  1814. list_issues() {
  1815. # List issues for the authenticated user or repository
  1816. #
  1817. # Usage:
  1818. #
  1819. # list_issues
  1820. # list_issues someuser/somerepo
  1821. # list_issues <any of the above> state=closed labels=foo,bar
  1822. #
  1823. # Positional arguments
  1824. #
  1825. # user or user/repository
  1826. #
  1827. # Keyword arguments
  1828. #
  1829. local _follow_next
  1830. # Automatically look for a 'Links' header and follow any 'next' URLs.
  1831. local _follow_next_limit
  1832. # Maximum number of 'next' URLs to follow before stopping.
  1833. local _filter='.[] | "\(.number)\t\(.title)"'
  1834. # A jq filter to apply to the return data.
  1835. #
  1836. # GitHub querystring arguments may also be passed as keyword arguments:
  1837. #
  1838. # * `assignee`
  1839. # * `creator`
  1840. # * `direction`
  1841. # * `labels`
  1842. # * `mentioned`
  1843. # * `milestone`
  1844. # * `per_page`
  1845. # * `since`
  1846. # * `sort`
  1847. # * `state`
  1848. local url
  1849. local qs
  1850. case $1 in
  1851. ('') url='/user/issues' ;;
  1852. (*=*) url='/user/issues' ;;
  1853. (*/*) url="/repos/${1}/issues"; shift 1 ;;
  1854. esac
  1855. _opts_pagination "$@"
  1856. _opts_filter "$@"
  1857. _opts_qs "$@"
  1858. _get "${url}${qs}" | _filter_json "$_filter"
  1859. }
  1860. user_issues() {
  1861. # List all issues across owned and member repositories for the authenticated user
  1862. #
  1863. # Usage:
  1864. #
  1865. # user_issues
  1866. # user_issues since=2015-60-11T00:09:00Z
  1867. #
  1868. # Keyword arguments
  1869. #
  1870. local _follow_next
  1871. # Automatically look for a 'Links' header and follow any 'next' URLs.
  1872. local _follow_next_limit
  1873. # Maximum number of 'next' URLs to follow before stopping.
  1874. local _filter='.[] | "\(.repository.full_name)\t\(.number)\t\(.title)"'
  1875. # A jq filter to apply to the return data.
  1876. #
  1877. # GitHub querystring arguments may also be passed as keyword arguments:
  1878. #
  1879. # * `direction`
  1880. # * `filter`
  1881. # * `labels`
  1882. # * `per_page`
  1883. # * `since`
  1884. # * `sort`
  1885. # * `state`
  1886. local qs
  1887. _opts_pagination "$@"
  1888. _opts_filter "$@"
  1889. _opts_qs "$@"
  1890. _get "/issues${qs}" | _filter_json "$_filter"
  1891. }
  1892. create_issue() {
  1893. # Create an issue
  1894. #
  1895. # Usage:
  1896. #
  1897. # create_issue owner repo 'Issue title' body='Add multiline body
  1898. # content here' labels="$(./ok.sh _format_json -a foo bar)"
  1899. #
  1900. # Positional arguments
  1901. #
  1902. local owner="${1:?Owner name required.}"
  1903. # A GitHub repository.
  1904. local repo="${2:?Repo name required.}"
  1905. # A GitHub repository.
  1906. local title="${3:?Issue title required.}"
  1907. # A GitHub repository.
  1908. #
  1909. # Keyword arguments
  1910. #
  1911. local _filter='"\(.id)\t\(.number)\t\(.html_url)"'
  1912. # A jq filter to apply to the return data.
  1913. #
  1914. # Additional issue fields may be passed as keyword arguments:
  1915. #
  1916. # * `body` (string)
  1917. # * `assignee` (string)
  1918. # * `milestone` (integer)
  1919. # * `labels` (array of strings)
  1920. # * `assignees` (array of strings)
  1921. shift 3
  1922. _opts_filter "$@"
  1923. _format_json title="$title" "$@" \
  1924. | _post "/repos/${owner}/${repo}/issues" \
  1925. | _filter_json "$_filter"
  1926. }
  1927. org_issues() {
  1928. # List all issues for a given organization for the authenticated user
  1929. #
  1930. # Usage:
  1931. #
  1932. # org_issues someorg
  1933. #
  1934. # Positional arguments
  1935. #
  1936. local org="${1:?Organization name required.}"
  1937. # Organization GitHub login or id.
  1938. #
  1939. # Keyword arguments
  1940. #
  1941. local _follow_next
  1942. # Automatically look for a 'Links' header and follow any 'next' URLs.
  1943. local _follow_next_limit
  1944. # Maximum number of 'next' URLs to follow before stopping.
  1945. local _filter='.[] | "\(.number)\t\(.title)"'
  1946. # A jq filter to apply to the return data.
  1947. #
  1948. # GitHub querystring arguments may also be passed as keyword arguments:
  1949. #
  1950. # * `direction`
  1951. # * `filter`
  1952. # * `labels`
  1953. # * `per_page`
  1954. # * `since`
  1955. # * `sort`
  1956. # * `state`
  1957. shift 1
  1958. local qs
  1959. _opts_pagination "$@"
  1960. _opts_filter "$@"
  1961. _opts_qs "$@"
  1962. _get "/orgs/${org}/issues${qs}" | _filter_json "$_filter"
  1963. }
  1964. list_my_orgs() {
  1965. # List your organizations
  1966. #
  1967. # Usage:
  1968. #
  1969. # list_my_orgs
  1970. #
  1971. # Keyword arguments
  1972. #
  1973. local _follow_next
  1974. # Automatically look for a 'Links' header and follow any 'next' URLs.
  1975. local _follow_next_limit
  1976. # Maximum number of 'next' URLs to follow before stopping.
  1977. local _filter='.[] | "\(.login)\t\(.id)"'
  1978. # A jq filter to apply to the return data.
  1979. local qs
  1980. _opts_pagination "$@"
  1981. _opts_filter "$@"
  1982. _opts_qs "$@"
  1983. _get "/user/orgs" | _filter_json "$_filter"
  1984. }
  1985. list_orgs() {
  1986. # List all organizations
  1987. #
  1988. # Usage:
  1989. #
  1990. # list_orgs
  1991. #
  1992. # Keyword arguments
  1993. #
  1994. local _follow_next
  1995. # Automatically look for a 'Links' header and follow any 'next' URLs.
  1996. local _follow_next_limit
  1997. # Maximum number of 'next' URLs to follow before stopping.
  1998. local _filter='.[] | "\(.login)\t\(.id)"'
  1999. # A jq filter to apply to the return data.
  2000. local qs
  2001. _opts_pagination "$@"
  2002. _opts_filter "$@"
  2003. _opts_qs "$@"
  2004. _get "/organizations" | _filter_json "$_filter"
  2005. }
  2006. labels() {
  2007. # List available labels for a repository
  2008. #
  2009. # Usage:
  2010. #
  2011. # labels someuser/somerepo
  2012. #
  2013. # Positional arguments
  2014. #
  2015. local repo="$1"
  2016. # A GitHub repository.
  2017. #
  2018. # Keyword arguments
  2019. #
  2020. local _follow_next
  2021. # Automatically look for a 'Links' header and follow any 'next' URLs.
  2022. local _follow_next_limit
  2023. # Maximum number of 'next' URLs to follow before stopping.
  2024. local _filter='.[] | "\(.name)\t\(.color)"'
  2025. # A jq filter to apply to the return data.
  2026. _opts_pagination "$@"
  2027. _opts_filter "$@"
  2028. _get "/repos/${repo}/labels" | _filter_json "$_filter"
  2029. }
  2030. add_label() {
  2031. # Add a label to a repository
  2032. #
  2033. # Usage:
  2034. #
  2035. # add_label someuser/somerepo LabelName color
  2036. #
  2037. # Positional arguments
  2038. #
  2039. local repo="${1:?Repo name required.}"
  2040. # A GitHub repository.
  2041. local label="${2:?Label name required.}"
  2042. # A new label.
  2043. local color="${3:?Hex color required.}"
  2044. # A color, in hex, without the leading `#`.
  2045. #
  2046. # Keyword arguments
  2047. #
  2048. local _filter='"\(.name)\t\(.color)"'
  2049. # A jq filter to apply to the return data.
  2050. _opts_filter "$@"
  2051. _format_json name="$label" color="$color" \
  2052. | _post "/repos/${repo}/labels" \
  2053. | _filter_json "$_filter"
  2054. }
  2055. update_label() {
  2056. # Update a label
  2057. #
  2058. # Usage:
  2059. #
  2060. # update_label someuser/somerepo OldLabelName \
  2061. # label=NewLabel color=newcolor
  2062. #
  2063. # Positional arguments
  2064. #
  2065. local repo="${1:?Repo name required.}"
  2066. # A GitHub repository.
  2067. local label="${2:?Label name required.}"
  2068. # The name of the label which will be updated
  2069. #
  2070. # Keyword arguments
  2071. #
  2072. local _filter='"\(.name)\t\(.color)"'
  2073. # A jq filter to apply to the return data.
  2074. #
  2075. # Label options may also be passed as keyword arguments, these will update
  2076. # the existing values:
  2077. #
  2078. # * `color`
  2079. # * `name`
  2080. shift 2
  2081. _opts_filter "$@"
  2082. _format_json "$@" \
  2083. | _post "/repos/${repo}/labels/${label}" method='PATCH' \
  2084. | _filter_json "$_filter"
  2085. }
  2086. add_team_repo() {
  2087. # Add a team repository
  2088. #
  2089. # Usage:
  2090. #
  2091. # add_team_repo team_id organization repository_name permission
  2092. #
  2093. # Positional arguments
  2094. #
  2095. local team_id="${1:?Team id required.}"
  2096. # Team id to add repository to
  2097. local organization="${2:?Organization required.}"
  2098. # Organization to add repository to
  2099. local repository_name="${3:?Repository name required.}"
  2100. # Repository name to add
  2101. local permission="${4:?Permission required.}"
  2102. # Permission to grant: pull, push, admin
  2103. #
  2104. local url="/teams/${team_id}/repos/${organization}/${repository_name}"
  2105. export OK_SH_ACCEPT="application/vnd.github.ironman-preview+json"
  2106. _format_json "name=${name}" "permission=${permission}" | _post "$url" method='PUT' | _filter_json "${_filter}"
  2107. }
  2108. list_pulls() {
  2109. # Lists the pull requests for a repository
  2110. #
  2111. # Usage:
  2112. #
  2113. # list_pulls user repo
  2114. #
  2115. # Positional arguments
  2116. #
  2117. local owner="${1:?Owner required.}"
  2118. # A GitHub owner.
  2119. local repo="${2:?Repo name required.}"
  2120. # A GitHub repository.
  2121. #
  2122. # Keyword arguments
  2123. #
  2124. local _follow_next
  2125. # Automatically look for a 'Links' header and follow any 'next' URLs.
  2126. local _follow_next_limit
  2127. # Maximum number of 'next' URLs to follow before stopping.
  2128. local _filter='.[] | "\(.number)\t\(.user.login)\t\(.head.repo.clone_url)\t\(.head.ref)"'
  2129. # A jq filter to apply to the return data.
  2130. _opts_pagination "$@"
  2131. _opts_filter "$@"
  2132. _get "/repos/${owner}/${repo}/pulls" | _filter_json "$_filter"
  2133. }
  2134. create_pull_request() {
  2135. # Create a pull request for a repository
  2136. #
  2137. # Usage:
  2138. #
  2139. # create_pull_request someuser/somerepo title head base
  2140. #
  2141. # create_pull_request someuser/somerepo title head base body='Description here.'
  2142. #
  2143. # Positional arguments
  2144. #
  2145. local repo="${1:?Repo name required.}"
  2146. # A GitHub repository.
  2147. local title="${2:?Pull request title required.}"
  2148. # A title.
  2149. local head="${3:?Pull request head required.}"
  2150. # A head.
  2151. local base="${4:?Pull request base required.}"
  2152. # A base.
  2153. #
  2154. # Keyword arguments
  2155. #
  2156. local _filter='"\(.number)\t\(.html_url)"'
  2157. # A jq filter to apply to the return data.
  2158. #
  2159. # Pull request options may also be passed as keyword arguments:
  2160. #
  2161. # * `body`
  2162. # * `maintainer_can_modify`
  2163. shift 4
  2164. _opts_filter "$@"
  2165. _format_json title="$title" head="$head" base="$base" "$@" \
  2166. | _post "/repos/${repo}/pulls" \
  2167. | _filter_json "$_filter"
  2168. }
  2169. update_pull_request() {
  2170. # Update a pull request for a repository
  2171. #
  2172. # Usage:
  2173. #
  2174. # update_pull_request someuser/somerepo number title='New title' body='New body'
  2175. #
  2176. # Positional arguments
  2177. #
  2178. local repo="${1:?Repo name required.}"
  2179. # A GitHub repository.
  2180. local number="${2:?Pull request number required.}"
  2181. # A pull request number.
  2182. #
  2183. # Keyword arguments
  2184. #
  2185. local _filter='"\(.number)\t\(.html_url)"'
  2186. # A jq filter to apply to the return data.
  2187. #
  2188. # Pull request options may also be passed as keyword arguments:
  2189. #
  2190. # * `base`
  2191. # * `body`
  2192. # * `maintainer_can_modify`
  2193. # * `state` (either open or closed)
  2194. # * `title`
  2195. shift 2
  2196. _opts_filter "$@"
  2197. _format_json "$@" \
  2198. | _post "/repos/${repo}/pulls/${number}" method='PATCH' \
  2199. | _filter_json "$_filter"
  2200. }
  2201. transfer_repo() {
  2202. # Transfer a repository to a user or organization
  2203. #
  2204. # Usage:
  2205. #
  2206. # transfer_repo owner repo new_owner
  2207. # transfer_repo owner repo new_owner team_ids='[ 12, 345 ]'
  2208. #
  2209. # Positional arguments
  2210. #
  2211. local owner="${1:?Owner name required.}"
  2212. # Name of the current owner
  2213. #
  2214. local repo="${2:?Repo name required.}"
  2215. # Name of the current repo
  2216. #
  2217. local new_owner="${3:?New owner name required.}"
  2218. # Name of the new owner
  2219. #
  2220. # Keyword arguments
  2221. #
  2222. local _filter='"\(.name)"'
  2223. # A jq filter to apply to the return data.
  2224. #
  2225. # POST data may also be passed as keyword arguments:
  2226. #
  2227. # * `team_ids`
  2228. shift 3
  2229. _opts_filter "$@"
  2230. export OK_SH_ACCEPT='application/vnd.github.nightshade-preview+json'
  2231. _format_json "new_owner=${new_owner}" "$@" | _post "/repos/${owner}/${repo}/transfer" | _filter_json "${_filter}"
  2232. }
  2233. archive_repo() {
  2234. # Archive a repo
  2235. #
  2236. # Usage:
  2237. #
  2238. # archive_repo owner/repo
  2239. #
  2240. # Positional arguments
  2241. #
  2242. local repo="${1:?Repo name required.}"
  2243. # A GitHub repository.
  2244. #
  2245. local _filter='"\(.name)\t\(.html_url)"'
  2246. # A jq filter to apply to the return data.
  2247. #
  2248. shift 1
  2249. _opts_filter "$@"
  2250. _format_json "archived=true" \
  2251. | _post "/repos/${repo}" method='PATCH' \
  2252. | _filter_json "$_filter"
  2253. }
  2254. __main "$@"