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.

ok.sh 73KB

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