diff options
author | Florian Zschocke <florian.zschocke@devolo.de> | 2020-01-30 22:13:10 +0100 |
---|---|---|
committer | Florian Zschocke <florian.zschocke@devolo.de> | 2020-01-30 22:13:10 +0100 |
commit | 77f5b7acbb259268e0e02e37a0bc2db99e9fb41f (patch) | |
tree | 986e07a0cc11f15e7cb94d76b5939a5216cc23f7 | |
parent | bb5d1ef31e65a6160e4fa1302d0ee293d5c344ef (diff) | |
parent | 187f8e486ff4b71871bb682807f8068ef6c01001 (diff) | |
download | gitblit-77f5b7acbb259268e0e02e37a0bc2db99e9fb41f.tar.gz gitblit-77f5b7acbb259268e0e02e37a0bc2db99e9fb41f.zip |
Merge branch 'release-github' into master
-rwxr-xr-x | .github/ok.sh | 2560 | ||||
-rw-r--r-- | .github/workflows/ci-build.yml | 2 | ||||
-rw-r--r-- | README.markdown | 3 | ||||
-rw-r--r-- | build.moxie | 2 | ||||
-rw-r--r-- | build.xml | 244 | ||||
-rw-r--r-- | release.template | 71 | ||||
-rw-r--r-- | releases.moxie | 4 | ||||
-rw-r--r-- | src/site/siteindex.mkd | 4 | ||||
-rwxr-xr-x | src/site/templates/ghreleasenotes.awk | 66 |
9 files changed, 2903 insertions, 53 deletions
diff --git a/.github/ok.sh b/.github/ok.sh new file mode 100755 index 00000000..6ac429d5 --- /dev/null +++ b/.github/ok.sh @@ -0,0 +1,2560 @@ +#!/usr/bin/env sh +# # A GitHub API client library written in POSIX sh +# +# https://github.com/whiteinge/ok.sh +# BSD licensed. +# +# ## Requirements +# +# * A POSIX environment (tested against Busybox v1.19.4) +# * curl (tested against 7.32.0) +# +# ## Optional requirements +# +# * jq <http://stedolan.github.io/jq/> (tested against 1.3) +# If jq is not installed commands will output raw JSON; if jq is installed +# the output will be formatted and filtered for use with other shell tools. +# +# ## Setup +# +# Authentication credentials are read from a `$HOME/.netrc` file on UNIX +# machines or a `_netrc` file in `%HOME%` for UNIX environments under Windows. +# [Generate the token on GitHub](https://github.com/settings/tokens) under +# "Account Settings -> Applications". +# Restrict permissions on that file with `chmod 600 ~/.netrc`! +# +# machine api.github.com +# login <username> +# password <token> +# +# machine uploads.github.com +# login <username> +# password <token> +# +# Or set an environment `GITHUB_TOKEN=token` +# +# ## Configuration +# +# The following environment variables may be set to customize ${NAME}. +# +# * OK_SH_URL=${OK_SH_URL} +# Base URL for GitHub or GitHub Enterprise. +# * OK_SH_ACCEPT=${OK_SH_ACCEPT} +# The 'Accept' header to send with each request. +# * OK_SH_JQ_BIN=${OK_SH_JQ_BIN} +# The name of the jq binary, if installed. +# * OK_SH_VERBOSE=${OK_SH_VERBOSE} +# The debug logging verbosity level. Same as the verbose flag. +# * OK_SH_RATE_LIMIT=${OK_SH_RATE_LIMIT} +# Output current GitHub rate limit information to stderr. +# * OK_SH_DESTRUCTIVE=${OK_SH_DESTRUCTIVE} +# Allow destructive operations without prompting for confirmation. +# * OK_SH_MARKDOWN=${OK_SH_MARKDOWN} +# Output some text in Markdown format. + +export NAME=$(basename "$0") +export VERSION='0.5.1' + +export OK_SH_URL=${OK_SH_URL:-'https://api.github.com'} +export OK_SH_ACCEPT=${OK_SH_ACCEPT:-'application/vnd.github.v3+json'} +export OK_SH_JQ_BIN="${OK_SH_JQ_BIN:-jq}" +export OK_SH_VERBOSE="${OK_SH_VERBOSE:-0}" +export OK_SH_RATE_LIMIT="${OK_SH_RATE_LIMIT:-0}" +export OK_SH_DESTRUCTIVE="${OK_SH_DESTRUCTIVE:-0}" +export OK_SH_MARKDOWN="${OK_SH_MARKDOWN:-0}" + +# Detect if jq is installed. +command -v "$OK_SH_JQ_BIN" 1>/dev/null 2>/dev/null +NO_JQ=$? + +# Customizable logging output. +exec 4>/dev/null +exec 5>/dev/null +exec 6>/dev/null +export LINFO=4 # Info-level log messages. +export LDEBUG=5 # Debug-level log messages. +export LSUMMARY=6 # Summary output. + +# Generate a carriage return so we can match on it. +# Using a variable because these are tough to specify in a portable way. +crlf=$(printf '\r\n') + +# ## Main +# Generic functions not necessarily specific to working with GitHub. + +# ### Help +# Functions for fetching and formatting help text. + + _cols() { + sort | awk ' + { w[NR] = $0 } + END { + cols = 3 + per_col = sprintf("%.f", NR / cols + 0.5) # Round up if decimal. + + for (i = 1; i < per_col + 1; i += 1) { + for (j = 0; j < cols; j += 1) { + printf("%-24s", w[i + per_col * j]) + } + printf("\n") + } + } + ' + } + _links() { awk '{ print "* [" $0 "](#" $0 ")" }'; } + _funcsfmt() { if [ "$OK_SH_MARKDOWN" -eq 0 ]; then _cols; else _links; fi; } + +help() { + # Output the help text for a command + # + # Usage: + # + # help commandname + # + # Positional arguments + # + local fname="$1" + # Function name to search for; if omitted searches whole file. + + # Short-circuit if only producing help for a single function. + if [ $# -gt 0 ]; then + awk -v fname="^$fname\\\(\\\) \\\{$" '$0 ~ fname, /^}/ { print }' "$0" \ + | _helptext + return + fi + + _helptext < "$0" + printf '\n' + help __main + printf '\n' + + printf '## Table of Contents\n' + printf '\n### Utility and request/response commands\n\n' + _all_funcs public=0 | _funcsfmt + printf '\n### GitHub commands\n\n' + _all_funcs private=0 | _funcsfmt + printf '\n## Commands\n\n' + + for cmd in $(_all_funcs public=0); do + printf '### %s\n\n' "$cmd" + help "$cmd" + printf '\n' + done + + for cmd in $(_all_funcs private=0); do + printf '### %s\n\n' "$cmd" + help "$cmd" + printf '\n' + done +} + +_all_funcs() { + # List all functions found in the current file in the order they appear + # + # Keyword arguments + # + local public=1 + # `0` do not output public functions. + local private=1 + # `0` do not output private functions. + + for arg in "$@"; do + case $arg in + (public=*) public="${arg#*=}";; + (private=*) private="${arg#*=}";; + esac + done + + awk -v public="$public" -v private="$private" ' + $1 !~ /^__/ && /^[a-zA-Z0-9_]+\s*\(\)/ { + sub(/\(\)$/, "", $1) + if (!public && substr($1, 1, 1) != "_") next + if (!private && substr($1, 1, 1) == "_") next + print $1 + } + ' "$0" +} + +__main() { + # ## Usage + # + # `${NAME} [<flags>] (command [<arg>, <name=value>...])` + # + # ${NAME} -h # Short, usage help text. + # ${NAME} help # All help text. Warning: long! + # ${NAME} help command # Command-specific help text. + # ${NAME} command # Run a command with and without args. + # ${NAME} command foo bar baz=Baz qux='Qux arg here' + # + # Flag | Description + # ---- | ----------- + # -V | Show version. + # -h | Show this screen. + # -j | Output raw JSON; don't process with jq. + # -q | Quiet; don't print to stdout. + # -r | Print current GitHub API rate limit to stderr. + # -v | Logging output; specify multiple times: info, debug, trace. + # -x | Enable xtrace debug logging. + # -y | Answer 'yes' to any prompts. + # + # Flags _must_ be the first argument to `${NAME}`, before `command`. + + local cmd + local ret + local opt + local OPTARG + local OPTIND + local quiet=0 + local temp_dir="${TMPDIR-/tmp}/${NAME}.${$}.$(awk \ + 'BEGIN {srand(); printf "%d\n", rand() * 10^10}')" + local summary_fifo="${temp_dir}/oksh_summary.fifo" + + # shellcheck disable=SC2154 + trap ' + excode=$?; trap - EXIT; + exec 4>&- + exec 5>&- + exec 6>&- + rm -rf '"$temp_dir"' + exit $excode + ' INT TERM EXIT + + while getopts Vhjqrvxy opt; do + case $opt in + V) printf 'Version: %s\n' $VERSION + exit;; + h) help __main + printf '\nAvailable commands:\n\n' + _all_funcs private=0 | _cols + printf '\n' + exit;; + j) NO_JQ=1;; + q) quiet=1;; + r) OK_SH_RATE_LIMIT=1;; + v) OK_SH_VERBOSE=$(( OK_SH_VERBOSE + 1 ));; + x) set -x;; + y) OK_SH_DESTRUCTIVE=1;; + esac + done + shift $(( OPTIND - 1 )) + + if [ -z "$1" ] ; then + printf 'No command given. Available commands:\n\n%s\n' \ + "$(_all_funcs private=0 | _cols)" 1>&2 + exit 1 + fi + + [ $OK_SH_VERBOSE -gt 0 ] && exec 4>&2 + [ $OK_SH_VERBOSE -gt 1 ] && exec 5>&2 + if [ $quiet -eq 1 ]; then + exec 1>/dev/null 2>/dev/null + fi + + if [ "$OK_SH_RATE_LIMIT" -eq 1 ] ; then + mkdir -m 700 "$temp_dir" || { + printf 'failed to create temp_dir\n' >&2; exit 1; + } + mkfifo "$summary_fifo" + # Hold the fifo open so it will buffer input until emptied. + exec 6<>"$summary_fifo" + fi + + # Run the command. + cmd="$1" && shift + _log debug "Running command ${cmd}." + "$cmd" "$@" + ret=$? + _log debug "Command ${cmd} exited with ${?}." + + # Output any summary messages. + if [ "$OK_SH_RATE_LIMIT" -eq 1 ] ; then + cat "$summary_fifo" 1>&2 & + exec 6>&- + fi + + exit $ret +} + +_log() { + # A lightweight logging system based on file descriptors + # + # Usage: + # + # _log debug 'Starting the combobulator!' + # + # Positional arguments + # + local level="${1:?Level is required.}" + # The level for a given log message. (info or debug) + local message="${2:?Message is required.}" + # The log message. + + shift 2 + + local lname + + case "$level" in + info) lname='INFO'; level=$LINFO ;; + debug) lname='DEBUG'; level=$LDEBUG ;; + *) printf 'Invalid logging level: %s\n' "$level" ;; + esac + + printf '%s %s: %s\n' "$NAME" "$lname" "$message" 1>&$level +} + +_helptext() { + # Extract contiguous lines of comments and function params as help text + # + # Indentation will be ignored. She-bangs will be ignored. Local variable + # declarations and their default values can also be pulled in as + # documentation. Exits upon encountering the first blank line. + # + # Exported environment variables can be used for string interpolation in + # the extracted commented text. + # + # Input + # + # * (stdin) + # The text of a function body to parse. + + awk ' + NR != 1 && /^\s*#/ { + line=$0 + while(match(line, "[$]{[^}]*}")) { + var=substr(line, RSTART+2, RLENGTH -3) + gsub("[$]{"var"}", ENVIRON[var], line) + } + gsub(/^\s*#\s?/, "", line) + print line + } + /^\s*local/ { + sub(/^\s*local /, "") + sub(/\$\{/, "$", $0) + sub(/:.*}/, "", $0) + print "* `" $0 "`\n" + } + !NF { exit }' +} + +# ### Request-response +# Functions for making HTTP requests and processing HTTP responses. + +_format_json() { + # Create formatted JSON from name=value pairs + # + # Usage: + # ``` + # ok.sh _format_json foo=Foo bar=123 baz=true qux=Qux=Qux quux='Multi-line + # string' quuz=\'5.20170918\' \ + # corge="$(ok.sh _format_json grault=Grault)" \ + # garply="$(ok.sh _format_json -a waldo true 3)" + # ``` + # + # Return: + # ``` + # { + # "garply": [ + # "waldo", + # true, + # 3 + # ], + # "foo": "Foo", + # "corge": { + # "grault": "Grault" + # }, + # "baz": true, + # "qux": "Qux=Qux", + # "quux": "Multi-line\nstring", + # "quuz": "5.20170918", + # "bar": 123 + # } + # ``` + # + # Tries not to quote numbers, booleans, nulls, or nested structures. + # Note, nested structures must be quoted since the output contains spaces. + # + # The `-a` option will create an array instead of an object. This option + # must come directly after the _format_json command and before any + # operands. E.g., `_format_json -a foo bar baz`. + # + # If jq is installed it will also validate the output. + # + # Positional arguments + # + # * $1 - $9 + # + # Each positional arg must be in the format of `name=value` which will be + # added to a single, flat JSON object. + + local opt + local OPTIND + local is_array=0 + local use_env=1 + while getopts a opt; do + case $opt in + a) is_array=1; unset use_env;; + esac + done + shift $(( OPTIND - 1 )) + + _log debug "Formatting ${#} parameters as JSON." + + env -i -- ${use_env+"$@"} awk -v is_array="$is_array" ' + function isnum(x){ return (x == x + 0) } + function isnull(x){ return (x == "null" ) } + function isbool(x){ if (x == "true" || x == "false") return 1 } + function isnested(x) { if (substr(x, 0, 1) == "[" \ + || substr(x, 0, 1) == "{") return 1 } + function castOrQuote(val) { + if (!isbool(val) && !isnum(val) && !isnull(val) && !isnested(val)) { + sub(/^('\''|")/, "", val) # Remove surrounding quotes + sub(/('\''|")$/, "", val) + + gsub(/"/, "\\\"", val) # Escape double-quotes. + gsub(/\n/, "\\n", val) # Replace newlines with \n text. + val = "\"" val "\"" + return val + } else { + return val + } + } + + BEGIN { + printf("%s", is_array ? "[" : "{") + + for (i = 1; i < length(ARGV); i += 1) { + arg = ARGV[i] + + if (is_array == 1) { + val = castOrQuote(arg) + printf("%s%s", sep, val) + } else { + name = substr(arg, 0, index(arg, "=") - 1) + val = castOrQuote(ENVIRON[name]) + printf("%s\"%s\": %s", sep, name, val) + } + + sep = ", " + ARGV[i] = "" + } + printf("%s", is_array ? "]" : "}") + }' "$@" +} + +_format_urlencode() { + # URL encode and join name=value pairs + # + # Usage: + # ``` + # _format_urlencode foo='Foo Foo' bar='<Bar>&/Bar/' + # ``` + # + # Return: + # ``` + # foo=Foo%20Foo&bar=%3CBar%3E%26%2FBar%2F + # ``` + # + # Ignores pairs if the value begins with an underscore. + + _log debug "Formatting ${#} parameters as urlencoded" + + env -i -- "$@" awk ' + function escape(str, c, i, len, res) { + len = length(str) + res = "" + for (i = 1; i <= len; i += 1) { + c = substr(str, i, 1); + if (c ~ /[0-9A-Za-z]/) + res = res c + else + res = res "%" sprintf("%02X", ord[c]) + } + return res + } + + BEGIN { + for (i = 0; i <= 255; i += 1) ord[sprintf("%c", i)] = i; + + for (j = 1; j < length(ARGV); j += 1) { + arg = ARGV[j] + name = substr(arg, 0, index(arg, "=") - 1) + if (substr(name, 1, 1) == "_") continue + val = ENVIRON[name] + + printf("%s%s=%s", sep, name, escape(val)) + sep = "&" + ARGV[j] = "" + } + }' "$@" +} + +_filter_json() { + # Filter JSON input using jq; outputs raw JSON if jq is not installed + # + # Usage: + # + # printf '[{"foo": "One"}, {"foo": "Two"}]' | \ + # ok.sh _filter_json '.[] | "\(.foo)"' + # + # * (stdin) + # JSON input. + local _filter="$1" + # A string of jq filters to apply to the input stream. + + _log debug 'Filtering JSON.' + + if [ $NO_JQ -ne 0 ] ; then + _log debug 'Bypassing jq processing.' + cat + return + fi + + "${OK_SH_JQ_BIN}" -c -r "${_filter}" + [ $? -eq 0 ] || printf 'jq parse error; invalid JSON.\n' 1>&2 +} + +_get_mime_type() { + # Guess the mime type for a file based on the file extension + # + # Usage: + # + # local mime_type + # _get_mime_type "foo.tar"; printf 'mime is: %s' "$mime_type" + # + # Sets the global variable `mime_type` with the result. (If this function + # is called from within a function that has declared a local variable of + # that name it will update the local copy and not set a global.) + # + # Positional arguments + # + local filename="${1:?Filename is required.}" + # The full name of the file, with extension. + + # Taken from Apache's mime.types file (public domain). + case "$filename" in + *.bz2) mime_type=application/x-bzip2 ;; + *.exe) mime_type=application/x-msdownload ;; + *.tar.gz | *.gz | *.tgz) mime_type=application/x-gzip ;; + *.jpg | *.jpeg | *.jpe | *.jfif) mime_type=image/jpeg ;; + *.json) mime_type=application/json ;; + *.pdf) mime_type=application/pdf ;; + *.png) mime_type=image/png ;; + *.rpm) mime_type=application/x-rpm ;; + *.svg | *.svgz) mime_type=image/svg+xml ;; + *.tar) mime_type=application/x-tar ;; + *.txt) mime_type=text/plain ;; + *.yaml) mime_type=application/x-yaml ;; + *.apk) mime_type=application/vnd.android.package-archive ;; + *.zip) mime_type=application/zip ;; + *.jar) mime_type=application/java-archive ;; + *.war) mime_type=application/zip ;; + esac + + _log debug "Guessed mime type of '${mime_type}' for '${filename}'." +} + +_get_confirm() { + # Prompt the user for confirmation + # + # Usage: + # + # local confirm; _get_confirm + # [ "$confirm" -eq 1 ] && printf 'Good to go!\n' + # + # If global confirmation is set via `$OK_SH_DESTRUCTIVE` then the user + # is not prompted. Assigns the user's confirmation to the `confirm` global + # variable. (If this function is called within a function that has a local + # variable of that name, the local variable will be updated instead.) + # + # Positional arguments + # + local message="${1:-Are you sure?}" + # The message to prompt the user with. + + local answer + + if [ "$OK_SH_DESTRUCTIVE" -eq 1 ] ; then + confirm=$OK_SH_DESTRUCTIVE + return + fi + + printf '%s ' "$message" + read -r answer + + ! printf '%s\n' "$answer" | grep -Eq "$(locale yesexpr)" + confirm=$? +} + +_opts_filter() { + # Extract common jq filter keyword options and assign to vars + # + # Usage: + # + # local filter + # _opts_filter "$@" + + for arg in "$@"; do + case $arg in + (_filter=*) _filter="${arg#*=}";; + esac + done +} + +_opts_pagination() { + # Extract common pagination keyword options and assign to vars + # + # Usage: + # + # local _follow_next + # _opts_pagination "$@" + + for arg in "$@"; do + case $arg in + (_follow_next=*) _follow_next="${arg#*=}";; + (_follow_next_limit=*) _follow_next_limit="${arg#*=}";; + esac + done +} + +_opts_qs() { + # Extract common query string keyword options and assign to vars + # + # Usage: + # + # local qs + # _opts_qs "$@" + # _get "/some/path${qs}" + + local querystring=$(_format_urlencode "$@") + qs="${querystring:+?$querystring}" +} + +_request() { + # A wrapper around making HTTP requests with curl + # + # Usage: + # ``` + # # Get JSON for all issues: + # _request /repos/saltstack/salt/issues + # + # # Send a POST request; parse response using jq: + # printf '{"title": "%s", "body": "%s"}\n' "Stuff" "Things" \ + # | _request /some/path | jq -r '.[url]' + # + # # Send a PUT request; parse response using jq: + # printf '{"title": "%s", "body": "%s"}\n' "Stuff" "Things" \ + # | _request /repos/:owner/:repo/issues method=PUT | jq -r '.[url]' + # + # # Send a conditional-GET request: + # _request /users etag=edd3a0d38d8c329d3ccc6575f17a76bb + # ``` + # + # Input + # + # * (stdin) + # Data that will be used as the request body. + # + # Positional arguments + # + local path="${1:?Path is required.}" + # The URL path for the HTTP request. + # Must be an absolute path that starts with a `/` or a full URL that + # starts with http(s). Absolute paths will be append to the value in + # `$OK_SH_URL`. + # + # Keyword arguments + # + local method='GET' + # The method to use for the HTTP request. + local content_type='application/json' + # The value of the Content-Type header to use for the request. + local etag + # An optional Etag to send as the If-None-Match header. + + shift 1 + + local cmd + local arg + local has_stdin + local trace_curl + + case $path in + (http*) : ;; + *) path="${OK_SH_URL}${path}" ;; + esac + + for arg in "$@"; do + case $arg in + (method=*) method="${arg#*=}";; + (content_type=*) content_type="${arg#*=}";; + (etag=*) etag="${arg#*=}";; + esac + done + + case "$method" in + POST | PUT | PATCH) has_stdin=1;; + esac + + [ $OK_SH_VERBOSE -eq 3 ] && trace_curl=1 + + [ "$OK_SH_VERBOSE" -eq 1 ] && set -x + # shellcheck disable=SC2086 + curl -nsSig \ + -H "Accept: ${OK_SH_ACCEPT}" \ + -H "Content-Type: ${content_type}" \ + ${GITHUB_TOKEN:+-H "Authorization: token ${GITHUB_TOKEN}"} \ + ${etag:+-H "If-None-Match: \"${etag}\""} \ + ${has_stdin:+--data-binary @-} \ + ${trace_curl:+--trace-ascii /dev/stderr} \ + -X "${method}" \ + "${path}" + set +x +} + +_response() { + # Process an HTTP response from curl + # + # Output only headers of interest followed by the response body. Additional + # processing is performed on select headers to make them easier to parse + # using shell tools. + # + # Usage: + # ``` + # # Send a request; output the response and only select response headers: + # _request /some/path | _response status_code ETag Link_next + # + # # Make request using curl; output response with select response headers; + # # assign response headers to local variables: + # curl -isS example.com/some/path | _response status_code status_text | { + # local status_code status_text + # read -r status_code + # read -r status_text + # } + # ``` + # + # Header reformatting + # + # * HTTP Status + # + # The HTTP line is split into separate `http_version`, `status_code`, and + # `status_text` variables. + # + # * ETag + # + # The surrounding quotes are removed. + # + # * Link + # + # Each URL in the Link header is expanded with the URL type appended to + # the name. E.g., `Link_first`, `Link_last`, `Link_next`. + # + # Positional arguments + # + # * $1 - $9 + # + # Each positional arg is the name of an HTTP header. Each header value is + # output in the same order as each argument; each on a single line. A + # blank line is output for headers that cannot be found. + + local hdr + local val + local http_version + local status_code=100 + local status_text + local headers output + + _log debug 'Processing response.' + + while [ "${status_code}" = "100" ]; do + read -r http_version status_code status_text + status_text="${status_text%${crlf}}" + http_version="${http_version#HTTP/}" + + _log debug "Response status is: ${status_code} ${status_text}" + + if [ "${status_code}" = "100" ]; then + _log debug "Ignoring response '${status_code} ${status_text}', skipping to real response." + while IFS=": " read -r hdr val; do + # Headers stop at the first blank line. + [ "$hdr" = "$crlf" ] && break + val="${val%${crlf}}" + _log debug "Unexpected additional header: ${hdr}: ${val}" + done + + fi + done + + headers="http_version: ${http_version} +status_code: ${status_code} +status_text: ${status_text} +" + while IFS=": " read -r hdr val; do + # Headers stop at the first blank line. + [ "$hdr" = "$crlf" ] && break + val="${val%${crlf}}" + + # Process each header; reformat some to work better with sh tools. + case "$hdr" in + # Update the GitHub rate limit trackers. + X-RateLimit-Remaining) + printf 'GitHub remaining requests: %s\n' "$val" 1>&$LSUMMARY ;; + X-RateLimit-Reset) + awk -v gh_reset="$val" 'BEGIN { + srand(); curtime = srand() + print "GitHub seconds to reset: " gh_reset - curtime + }' 1>&$LSUMMARY ;; + + # Remove quotes from the etag header. + ETag) val="${val#\"}"; val="${val%\"}" ;; + + # Split the URLs in the Link header into separate pseudo-headers. + Link) headers="${headers}$(printf '%s' "$val" | awk ' + BEGIN { RS=", "; FS="; "; OFS=": " } + { + sub(/^rel="/, "", $2); sub(/"$/, "", $2) + sub(/^ *</, "", $1); sub(/>$/, "", $1) + print "Link_" $2, $1 + }') +" # need trailing newline + ;; + esac + + headers="${headers}${hdr}: ${val} +" # need trailing newline + + done + + # Output requested headers in deterministic order. + for arg in "$@"; do + _log debug "Outputting requested header '${arg}'." + output=$(printf '%s' "$headers" | while IFS=": " read -r hdr val; do + [ "$hdr" = "$arg" ] && printf '%s' "$val" + done) + printf '%s\n' "$output" + done + + # Output the response body. + cat +} + +_get() { + # A wrapper around _request() for common GET patterns + # + # Will automatically follow 'next' pagination URLs in the Link header. + # + # Usage: + # + # _get /some/path + # _get /some/path _follow_next=0 + # _get /some/path _follow_next_limit=200 | jq -c . + # + # Positional arguments + # + local path="${1:?Path is required.}" + # The HTTP path or URL to pass to _request(). + # + # Keyword arguments + # + # * _follow_next=1 + # + # Automatically look for a 'Links' header and follow any 'next' URLs. + # + # * _follow_next_limit=50 + # + # Maximum number of 'next' URLs to follow before stopping. + + shift 1 + local status_code + local status_text + local next_url + + # If the variable is unset or empty set it to a default value. Functions + # that call this function can pass these parameters in one of two ways: + # explicitly as a keyword arg or implicitly by setting variables of the same + # names within the local scope. + # shellcheck disable=SC2086 + if [ -z ${_follow_next+x} ] || [ -z "${_follow_next}" ]; then + local _follow_next=1 + fi + # shellcheck disable=SC2086 + if [ -z ${_follow_next_limit+x} ] || [ -z "${_follow_next_limit}" ]; then + local _follow_next_limit=50 + fi + + _opts_pagination "$@" + + _request "$path" | _response status_code status_text Link_next | { + read -r status_code + read -r status_text + read -r next_url + + case "$status_code" in + 20*) : ;; + 4*) printf 'Client Error: %s %s\n' \ + "$status_code" "$status_text" 1>&2; exit 1 ;; + 5*) printf 'Server Error: %s %s\n' \ + "$status_code" "$status_text" 1>&2; exit 1 ;; + esac + + # Output response body. + cat + + [ "$_follow_next" -eq 1 ] || return + + _log info "Remaining next link follows: ${_follow_next_limit}" + if [ -n "$next_url" ] && [ $_follow_next_limit -gt 0 ] ; then + _follow_next_limit=$(( _follow_next_limit - 1 )) + + _get "$next_url" "_follow_next_limit=${_follow_next_limit}" + fi + } +} + +_post() { + # A wrapper around _request() for common POST / PUT patterns + # + # Usage: + # + # _format_json foo=Foo bar=Bar | _post /some/path + # _format_json foo=Foo bar=Bar | _post /some/path method='PUT' + # _post /some/path filename=somearchive.tar + # _post /some/path filename=somearchive.tar mime_type=application/x-tar + # _post /some/path filename=somearchive.tar \ + # mime_type=$(file -b --mime-type somearchive.tar) + # + # Input + # + # * (stdin) + # Optional. See the `filename` argument also. + # Data that will be used as the request body. + # + # Positional arguments + # + local path="${1:?Path is required.}" + # The HTTP path or URL to pass to _request(). + # + # Keyword arguments + # + local method='POST' + # The method to use for the HTTP request. + local filename + # Optional. See the `stdin` option above also. + # Takes precedence over any data passed as stdin and loads a file off the + # file system to serve as the request body. + local mime_type + # The value of the Content-Type header to use for the request. + # If the `filename` argument is given this value will be guessed from the + # file extension. If the `filename` argument is not given (i.e., using + # stdin) this value defaults to `application/json`. Specifying this + # argument overrides all other defaults or guesses. + + shift 1 + + for arg in "$@"; do + case $arg in + (method=*) method="${arg#*=}";; + (filename=*) filename="${arg#*=}";; + (mime_type=*) mime_type="${arg#*=}";; + esac + done + + # Make either the file or stdin available as fd7. + if [ -n "$filename" ] ; then + if [ -r "$filename" ] ; then + _log debug "Using '${filename}' as POST data." + [ -n "$mime_type" ] || _get_mime_type "$filename" + : ${mime_type:?The MIME type could not be guessed.} + exec 7<"$filename" + else + printf 'File could not be found or read.\n' 1>&2 + exit 1 + fi + else + _log debug "Using stdin as POST data." + mime_type='application/json' + exec 7<&0 + fi + + _request "$path" method="$method" content_type="$mime_type" 0<&7 \ + | _response status_code status_text \ + | { + read -r status_code + read -r status_text + + case "$status_code" in + 20*) : ;; + 4*) printf 'Client Error: %s %s\n' \ + "$status_code" "$status_text" 1>&2; exit 1 ;; + 5*) printf 'Server Error: %s %s\n' \ + "$status_code" "$status_text" 1>&2; exit 1 ;; + esac + + # Output response body. + cat + } +} + +_delete() { + # A wrapper around _request() for common DELETE patterns + # + # Usage: + # + # _delete '/some/url' + # + # Return: 0 for success; 1 for failure. + # + # Positional arguments + # + local url="${1:?URL is required.}" + # The URL to send the DELETE request to. + + local status_code + + _request "${url}" method='DELETE' | _response status_code | { + read -r status_code + [ "$status_code" = "204" ] + exit $? + } +} + +# ## GitHub +# Friendly functions for common GitHub tasks. + +# ### Authorization +# Perform authentication and authorization. + +show_scopes() { + # Show the permission scopes for the currently authenticated user + # + # Usage: + # + # show_scopes + + local oauth_scopes + + _request '/' | _response X-OAuth-Scopes | { + read -r oauth_scopes + + printf '%s\n' "$oauth_scopes" + + # Dump any remaining response body. + cat >/dev/null + } +} + +# ### Repository +# Create, update, delete, list repositories. + +org_repos() { + # List organization repositories + # + # Usage: + # + # org_repos myorg + # org_repos myorg type=private per_page=10 + # org_repos myorg _filter='.[] | "\(.name)\t\(.owner.login)"' + # + # Positional arguments + # + local org="${1:?Org name required.}" + # Organization GitHub login or id for which to list repos. + # + # Keyword arguments + # + local _follow_next + # Automatically look for a 'Links' header and follow any 'next' URLs. + local _follow_next_limit + # Maximum number of 'next' URLs to follow before stopping. + local _filter='.[] | "\(.name)\t\(.ssh_url)"' + # A jq filter to apply to the return data. + # + # Querystring arguments may also be passed as keyword arguments: + # + # * `per_page` + # * `type` + + shift 1 + local qs + + _opts_pagination "$@" + _opts_filter "$@" + _opts_qs "$@" + + _get "/orgs/${org}/repos${qs}" | _filter_json "${_filter}" +} + +org_teams() { + # List teams + # + # Usage: + # + # org_teams org + # + # Positional arguments + # + local org="${1:?Org name required.}" + # Organization GitHub login or id. + # + # Keyword arguments + # + local _filter='.[] | "\(.name)\t\(.id)\t\(.permission)"' + # A jq filter to apply to the return data. + + shift 1 + + _opts_filter "$@" + + _get "/orgs/${org}/teams" \ + | _filter_json "${_filter}" +} + +org_members() { + # List organization members + # + # Usage: + # + # org_members org + # + # Positional arguments + # + local org="${1:?Org name required.}" + # Organization GitHub login or id. + # + # Keyword arguments + # + local _filter='.[] | "\(.login)\t\(.id)"' + # A jq filter to apply to the return data. + + shift 1 + + _opts_filter "$@" + + _get "/orgs/${org}/members" \ + | _filter_json "${_filter}" +} + +team_members() { + # List team members + # + # Usage: + # + # team_members team_id + # + # Positional arguments + # + local team_id="${1:?Team id required.}" + # Team id. + # + # Keyword arguments + # + local _filter='.[] | "\(.login)\t\(.id)"' + # A jq filter to apply to the return data. + + shift 1 + + _opts_filter "$@" + + _get "/teams/${team_id}/members" \ + | _filter_json "${_filter}" + +} + +list_repos() { + # List user repositories + # + # Usage: + # + # list_repos + # list_repos user + # + # Positional arguments + # + local user="$1" + # Optional GitHub user login or id for which to list repos. + # + # Keyword arguments + # + local _filter='.[] | "\(.name)\t\(.html_url)"' + # A jq filter to apply to the return data. + # + # Querystring arguments may also be passed as keyword arguments: + # + # * `direction` + # * `per_page` + # * `sort` + # * `type` + + + shift 1 + local qs + + _opts_filter "$@" + _opts_qs "$@" + + if [ -n "$user" ] ; then + url="/users/${user}/repos" + else + url='/user/repos' + fi + + _get "${url}${qs}" | _filter_json "${_filter}" +} + +list_branches() { + # List branches of a specified repository. + # ( https://developer.github.com/v3/repos/#list_branches ) + # + # Usage: + # + # list_branches user repo + # + # Positional arguments + # + # GitHub user login or id for which to list branches + # Name of the repo for which to list branches + # + local user="${1:?User name required.}" + local repo="${2:?Repo name required.}" + shift 2 + # + # Keyword arguments + # + local _filter='.[] | "\(.name)"' + # A jq filter to apply to the return data. + # + # Querystring arguments may also be passed as keyword arguments: + # + # * `direction` + # * `per_page` + # * `sort` + # * `type` + + local qs + + _opts_filter "$@" + _opts_qs "$@" + + url="/repos/${user}/${repo}/branches" + + _get "${url}${qs}" | _filter_json "${_filter}" +} + +list_contributors() { + # List contributors to the specified repository, sorted by the number of commits per contributor in descending order. + # ( https://developer.github.com/v3/repos/#list-contributors ) + # + # Usage: + # + # list_contributors user repo + # + # Positional arguments + # + local user="${1:?User name required.}" + # GitHub user login or id for which to list contributors + local repo="${2:?Repo name required.}" + # Name of the repo for which to list contributors + # + # Keyword arguments + # + local _filter='.[] | "\(.login)\t\(.type)\tType:\(.type)\tContributions:\(.contributions)"' + # A jq filter to apply to the return data. + # + # Querystring arguments may also be passed as keyword arguments: + # + # * `direction` + # * `per_page` + # * `sort` + # * `type` + + shift 2 + + local qs + + _opts_filter "$@" + _opts_qs "$@" + + url="/repos/${user}/${repo}/contributors" + + _get "${url}${qs}" | _filter_json "${_filter}" +} + +list_collaborators() { + # List collaborators to the specified repository, sorted by the number of commits per collaborator in descending order. + # ( https://developer.github.com/v3/repos/#list-collaborators ) + # + # Usage: + # + # list_collaborators someuser/somerepo + # + # Positional arguments + # GitHub user login or id for which to list collaborators + # Name of the repo for which to list collaborators + # + local repo="${1:?Repo name required.}" + # + # Keyword arguments + # + local _filter='.[] | "\(.login)\t\(.type)\tType:\(.type)\tPermissions:\(.permissions)"' + # A jq filter to apply to the return data. + # + # Querystring arguments may also be passed as keyword arguments: + # + # * `direction` + # * `per_page` + # * `sort` + # * `type` + + shift 1 + + local qs + + _opts_filter "$@" + _opts_qs "$@" + + url="/repos/${repo}/collaborators" + + _get "${url}${qs}" | _filter_json "${_filter}" +} + +list_hooks() { + # List webhooks from the specified repository. + # ( https://developer.github.com/v3/repos/hooks/#list-hooks ) + # + # Usage: + # + # list_hooks owner/repo + # + # Positional arguments + # + local repo="${1:?Repo name required.}" + # Name of the repo for which to list contributors + # Owner is mandatory, like 'owner/repo' + # + local _filter='.[] | "\(.name)\t\(.config.url)"' + # A jq filter to apply to the return data. + # + + shift 1 + + _opts_filter "$@" + + url="/repos/${repo}/hooks" + + _get "${url}" | _filter_json "${_filter}" +} + +list_gists() { + # List gists for the current authenticated user or a specific user + # + # https://developer.github.com/v3/gists/#list-a-users-gists + # + # Usage: + # + # list_gists + # list_gists <username> + # + # Positional arguments + # + local username="$1" + # An optional user to filter listing + # + # Keyword arguments + # + local _follow_next + # Automatically look for a 'Links' header and follow any 'next' URLs. + local _follow_next_limit + # Maximum number of 'next' URLs to follow before stopping. + local _filter='.[] | "\(.id)\t\(.description)"' + # A jq filter to apply to the return data. + + local url + case "$username" in + ('') url='/gists';; + (*=*) url='/gists';; + (*) url="/users/${username}/gists"; shift 1;; + esac + + _opts_pagination "$@" + _opts_filter "$@" + + _get "${url}" | _filter_json "${_filter}" +} + +public_gists() { + # List public gists + # + # https://developer.github.com/v3/gists/#list-all-public-gists + # + # Usage: + # + # public_gists + # + # Keyword arguments + # + local _follow_next + # Automatically look for a 'Links' header and follow any 'next' URLs. + local _follow_next_limit + # Maximum number of 'next' URLs to follow before stopping. + local _filter='.[] | "\(.id)\t\(.description)"' + # A jq filter to apply to the return data. + + _opts_pagination "$@" + _opts_filter "$@" + + _get '/gists/public' | _filter_json "${_filter}" +} + +gist() { + # Get a single gist + # + # https://developer.github.com/v3/gists/#get-a-single-gist + # + # Usage: + # + # get_gist + # + # Positional arguments + # + local gist_id="${1:?Gist ID required.}" + # ID of gist to fetch. + # + # Keyword arguments + # + local _filter='.files | keys | join(", ")' + # A jq filter to apply to the return data. + + shift 1 + + _opts_filter "$@" + + _get "/gists/${gist_id}" | _filter_json "${_filter}" +} + +add_collaborator() { + # Add a collaborator to a repository + # + # Usage: + # + # add_collaborator someuser/somerepo collaboratoruser permission + # + # Positional arguments + # + local repo="${1:?Repo name required.}" + # A GitHub repository. + local collaborator="${2:?Collaborator name required.}" + # A new collaborator. + local permission="${3:?Permission required. One of: push pull admin}" + # The permission level for this collaborator. One of `push`, `pull`, + # `admin`. The `pull` and `admin` permissions are valid for organization + # repos only. + case $permission in + push|pull|admin) :;; + *) printf 'Permission invalid: %s\nMust be one of: push pull admin\n' \ + "$permission" 1>&2; exit 1 ;; + esac + # + # Keyword arguments + # + local _filter='"\(.name)\t\(.color)"' + # A jq filter to apply to the return data. + + _opts_filter "$@" + + _format_json permission="$permission" \ + | _post "/repos/${repo}/collaborators/${collaborator}" method='PUT' \ + | _filter_json "$_filter" +} + +delete_collaborator() { + # Delete a collaborator to a repository + # + # Usage: + # + # delete_collaborator someuser/somerepo collaboratoruser permission + # + # Positional arguments + # + local repo="${1:?Repo name required.}" + # A GitHub repository. + local collaborator="${2:?Collaborator name required.}" + # A new collaborator. + + shift 2 + + local confirm + + _get_confirm 'This will permanently delete the collaborator from this repo. Continue?' + [ "$confirm" -eq 1 ] || exit 0 + + _delete "/repos/${repo}/collaborators/${collaborator}" + exit $? +} + +create_repo() { + # Create a repository for a user or organization + # + # Usage: + # + # create_repo foo + # create_repo bar description='Stuff and things' homepage='example.com' + # create_repo baz organization=myorg + # + # Positional arguments + # + local name="${1:?Repo name required.}" + # Name of the new repo + # + # Keyword arguments + # + local _filter='"\(.name)\t\(.html_url)"' + # A jq filter to apply to the return data. + # + # POST data may also be passed as keyword arguments: + # + # * `auto_init`, + # * `description` + # * `gitignore_template` + # * `has_downloads` + # * `has_issues` + # * `has_wiki`, + # * `homepage` + # * `organization` + # * `private` + # * `team_id` + + shift 1 + + _opts_filter "$@" + + local url + local organization + + for arg in "$@"; do + case $arg in + (organization=*) organization="${arg#*=}";; + esac + done + + if [ -n "$organization" ] ; then + url="/orgs/${organization}/repos" + else + url='/user/repos' + fi + + _format_json "name=${name}" "$@" | _post "$url" | _filter_json "${_filter}" +} + +delete_repo() { + # Delete a repository for a user or organization + # + # Usage: + # + # delete_repo owner repo + # + # The currently authenticated user must have the `delete_repo` scope. View + # current scopes with the `show_scopes()` function. + # + # Positional arguments + # + local owner="${1:?Owner name required.}" + # Name of the new repo + local repo="${2:?Repo name required.}" + # Name of the new repo + + shift 2 + + local confirm + + _get_confirm 'This will permanently delete a repository! Continue?' + [ "$confirm" -eq 1 ] || exit 0 + + _delete "/repos/${owner}/${repo}" + exit $? +} + +fork_repo() { + # Fork a repository from a user or organization to own account or organization + # + # Usage: + # + # fork_repo owner repo + # + # Positional arguments + # + local owner="${1:?Owner name required.}" + # Name of existing user or organization + local repo="${2:?Repo name required.}" + # Name of the existing repo + # + # + # Keyword arguments + # + local _filter='"\(.clone_url)\t\(.ssh_url)"' + # A jq filter to apply to the return data. + # + # POST data may also be passed as keyword arguments: + # + # * `organization` (The organization to clone into; default: your personal account) + + shift 2 + + _opts_filter "$@" + + _format_json "$@" | _post "/repos/${owner}/${repo}/forks" \ + | _filter_json "${_filter}" + exit $? # might take a bit time... +} + +# ### Releases +# Create, update, delete, list releases. + +list_releases() { + # List releases for a repository + # + # https://developer.github.com/v3/repos/releases/#list-releases-for-a-repository + # + # Usage: + # + # list_releases org repo '\(.assets[0].name)\t\(.name.id)' + # + # Positional arguments + # + local owner="${1:?Owner name required.}" + # A GitHub user or organization. + local repo="${2:?Repo name required.}" + # A GitHub repository. + # + # Keyword arguments + # + local _filter='.[] | "\(.name)\t\(.tag_name)\t\(.id)\t\(.html_url)"' + # A jq filter to apply to the return data. + + shift 2 + + _opts_filter "$@" + + _get "/repos/${owner}/${repo}/releases" \ + | _filter_json "${_filter}" +} + +release() { + # Get a release + # + # https://developer.github.com/v3/repos/releases/#get-a-single-release + # + # Usage: + # + # release user repo 1087855 + # + # Positional arguments + # + local owner="${1:?Owner name required.}" + # A GitHub user or organization. + local repo="${2:?Repo name required.}" + # A GitHub repository. + local release_id="${3:?Release ID required.}" + # The unique ID of the release; see list_releases. + # + # Keyword arguments + # + local _filter='"\(.author.login)\t\(.published_at)"' + # A jq filter to apply to the return data. + + shift 3 + + _opts_filter "$@" + + _get "/repos/${owner}/${repo}/releases/${release_id}" \ + | _filter_json "${_filter}" +} + +create_release() { + # Create a release + # + # https://developer.github.com/v3/repos/releases/#create-a-release + # + # Usage: + # + # create_release org repo v1.2.3 + # create_release user repo v3.2.1 draft=true + # + # Positional arguments + # + local owner="${1:?Owner name required.}" + # A GitHub user or organization. + local repo="${2:?Repo name required.}" + # A GitHub repository. + local tag_name="${3:?Tag name required.}" + # Git tag from which to create release. + # + # Keyword arguments + # + local _filter='"\(.name)\t\(.id)\t\(.html_url)"' + # A jq filter to apply to the return data. + # + # POST data may also be passed as keyword arguments: + # + # * `body` + # * `draft` + # * `name` + # * `prerelease` + # * `target_commitish` + + shift 3 + + _opts_filter "$@" + + _format_json "tag_name=${tag_name}" "$@" \ + | _post "/repos/${owner}/${repo}/releases" \ + | _filter_json "${_filter}" +} + +edit_release() { + # Edit a release + # + # https://developer.github.com/v3/repos/releases/#edit-a-release + # + # Usage: + # + # edit_release org repo 1087855 name='Foo Bar 1.4.6' + # edit_release user repo 1087855 draft=false + # + # Positional arguments + # + local owner="${1:?Owner name required.}" + # A GitHub user or organization. + local repo="${2:?Repo name required.}" + # A GitHub repository. + local release_id="${3:?Release ID required.}" + # The unique ID of the release; see list_releases. + # + # Keyword arguments + # + local _filter='"\(.tag_name)\t\(.name)\t\(.html_url)"' + # A jq filter to apply to the return data. + # + # POST data may also be passed as keyword arguments: + # + # * `tag_name` + # * `body` + # * `draft` + # * `name` + # * `prerelease` + # * `target_commitish` + + shift 3 + + _opts_filter "$@" + + _format_json "$@" \ + | _post "/repos/${owner}/${repo}/releases/${release_id}" method="PATCH" \ + | _filter_json "${_filter}" +} + +delete_release() { + # Delete a release + # + # https://developer.github.com/v3/repos/releases/#delete-a-release + # + # Usage: + # + # delete_release org repo 1087855 + # + # Return: 0 for success; 1 for failure. + # + # Positional arguments + # + local owner="${1:?Owner name required.}" + # A GitHub user or organization. + local repo="${2:?Repo name required.}" + # A GitHub repository. + local release_id="${3:?Release ID required.}" + # The unique ID of the release; see list_releases. + + shift 3 + + local confirm + + _get_confirm 'This will permanently delete a release. Continue?' + [ "$confirm" -eq 1 ] || exit 0 + + _delete "/repos/${owner}/${repo}/releases/${release_id}" + exit $? +} + +release_assets() { + # List release assets + # + # https://developer.github.com/v3/repos/releases/#list-assets-for-a-release + # + # Usage: + # + # release_assets user repo 1087855 + # + # Example of downloading release assets: + # + # ok.sh release_assets <user> <repo> <release_id> \ + # _filter='.[] | .browser_download_url' \ + # | xargs -L1 curl -L -O + # + # Example of the multi-step process for grabbing the release ID for + # a specific version, then grabbing the release asset IDs, and then + # downloading all the release assets (whew!): + # + # username='myuser' + # repo='myrepo' + # release_tag='v1.2.3' + # ok.sh list_releases "$myuser" "$myrepo" \ + # | awk -F'\t' -v tag="$release_tag" '$2 == tag { print $3 }' \ + # | xargs -I{} ./ok.sh release_assets "$myuser" "$myrepo" {} \ + # _filter='.[] | .browser_download_url' \ + # | xargs -L1 curl -n -L -O + # + # Positional arguments + # + local owner="${1:?Owner name required.}" + # A GitHub user or organization. + local repo="${2:?Repo name required.}" + # A GitHub repository. + local release_id="${3:?Release ID required.}" + # The unique ID of the release; see list_releases. + # + # Keyword arguments + # + local _filter='.[] | "\(.id)\t\(.name)\t\(.updated_at)"' + # A jq filter to apply to the return data. + + shift 3 + + _opts_filter "$@" + + _get "/repos/${owner}/${repo}/releases/${release_id}/assets" \ + | _filter_json "$_filter" +} + +upload_asset() { + # Upload a release asset + # + # https://developer.github.com/v3/repos/releases/#upload-a-release-asset + # + # Usage: + # + # upload_asset https://<upload-url> /path/to/file.zip + # + # The upload URL can be gotten from `release()`. There are multiple steps + # required to upload a file: get the release ID, get the upload URL, parse + # the upload URL, then finally upload the file. For example: + # + # ```sh + # USER="someuser" + # REPO="somerepo" + # TAG="1.2.3" + # FILE_NAME="foo.zip" + # FILE_PATH="/path/to/foo.zip" + # + # # Create a release then upload a file: + # ok.sh create_release "$USER" "$REPO" "$TAG" _filter='.upload_url' \ + # | sed 's/{.*$/?name='"$FILE_NAME"'/' \ + # | xargs -I@ ok.sh upload_asset @ "$FILE_PATH" + # + # # Find a release by tag then upload a file: + # ok.sh list_releases "$USER" "$REPO" \ + # | awk -v "tag=$TAG" -F'\t' '$2 == tag { print $3 }' \ + # | xargs -I@ ok.sh release "$USER" "$REPO" @ _filter='.upload_url' \ + # | sed 's/{.*$/?name='"$FILE_NAME"'/' \ + # | xargs -I@ ok.sh upload_asset @ "$FILE_PATH" + # ``` + # + # Positional arguments + # + local upload_url="${1:?upload_url is required.}" + # The _parsed_ upload_url returned from GitHub. + # + local file_path="${2:?file_path is required.}" + # A path to the file that should be uploaded. + # + # Keyword arguments + # + local _filter='"\(.state)\t\(.browser_download_url)"' + # A jq filter to apply to the return data. + # + # Also any other keyword arguments accepted by `_post()`. + + shift 2 + + _opts_filter "$@" + + _post "$upload_url" filename="$file_path" "$@" \ + | _filter_json "$_filter" +} + +# ### Issues +# Create, update, edit, delete, list issues and milestones. + +list_milestones() { + # List milestones for a repository + # + # Usage: + # + # list_milestones someuser/somerepo + # list_milestones someuser/somerepo state=closed + # + # Positional arguments + # + local repository="${1:?Repo name required.}" + # A GitHub repository. + # + # Keyword arguments + # + local _follow_next + # Automatically look for a 'Links' header and follow any 'next' URLs. + local _follow_next_limit + # Maximum number of 'next' URLs to follow before stopping. + local _filter='.[] | "\(.number)\t\(.open_issues)/\(.closed_issues)\t\(.title)"' + # A jq filter to apply to the return data. + # + # GitHub querystring arguments may also be passed as keyword arguments: + # + # * `direction` + # * `per_page` + # * `sort` + # * `state` + + shift 1 + local qs + + _opts_pagination "$@" + _opts_filter "$@" + _opts_qs "$@" + + _get "/repos/${repository}/milestones${qs}" | _filter_json "$_filter" +} + +create_milestone() { + # Create a milestone for a repository + # + # Usage: + # + # create_milestone someuser/somerepo MyMilestone + # + # create_milestone someuser/somerepo MyMilestone \ + # due_on=2015-06-16T16:54:00Z \ + # description='Long description here + # that spans multiple lines.' + # + # Positional arguments + # + local repo="${1:?Repo name required.}" + # A GitHub repository. + local title="${2:?Milestone name required.}" + # A unique title. + # + # Keyword arguments + # + local _filter='"\(.number)\t\(.html_url)"' + # A jq filter to apply to the return data. + # + # Milestone options may also be passed as keyword arguments: + # + # * `description` + # * `due_on` + # * `state` + + shift 2 + + _opts_filter "$@" + + _format_json title="$title" "$@" \ + | _post "/repos/${repo}/milestones" \ + | _filter_json "$_filter" +} + +add_comment() { + # Add a comment to an issue + # + # Usage: + # + # add_comment someuser/somerepo 123 'This is a comment' + # + # Positional arguments + # + local repository="${1:?Repo name required}" + # A GitHub repository + local number="${2:?Issue number required}" + # Issue Number + local comment="${3:?Comment required}" + # Comment to be added + # + # Keyword arguments + # + local _filter='"\(.id)\t\(.html_url)"' + # A jq filter to apply to the return data. + + shift 3 + _opts_filter "$@" + + _format_json body="$comment" \ + | _post "/repos/${repository}/issues/${number}/comments" \ + | _filter_json "${_filter}" +} + +add_commit_comment() { + # Add a comment to a commit + # + # Usage: + # + # add_commit_comment someuser/somerepo 123 'This is a comment' + # + # Positional arguments + # + local repository="${1:?Repo name required}" + # A GitHub repository + local hash="${2:?Commit hash required}" + # Commit hash + local comment="${3:?Comment required}" + # Comment to be added + # + # Keyword arguments + # + local _filter='"\(.id)\t\(.html_url)"' + # A jq filter to apply to the return data. + + shift 3 + _opts_filter "$@" + + _format_json body="$comment" \ + | _post "/repos/${repository}/commits/${hash}/comments" \ + | _filter_json "${_filter}" +} + +close_issue() { + # Close an issue + # + # Usage: + # + # close_issue someuser/somerepo 123 + # + # Positional arguments + # + local repository="${1:?Repo name required}" + # A GitHub repository + local number="${2:?Issue number required}" + # Issue Number + # + # Keyword arguments + # + local _filter='"\(.id)\t\(.state)\t\(.html_url)"' + # A jq filter to apply to the return data. + # + # POST data may also be passed as keyword arguments: + # + # * `assignee` + # * `labels` + # * `milestone` + + shift 2 + _opts_filter "$@" + + _format_json state="closed" "$@" \ + | _post "/repos/${repository}/issues/${number}" method='PATCH' \ + | _filter_json "${_filter}" +} + +list_issues() { + # List issues for the authenticated user or repository + # + # Usage: + # + # list_issues + # list_issues someuser/somerepo + # list_issues <any of the above> state=closed labels=foo,bar + # + # Positional arguments + # + # user or user/repository + # + # Keyword arguments + # + local _follow_next + # Automatically look for a 'Links' header and follow any 'next' URLs. + local _follow_next_limit + # Maximum number of 'next' URLs to follow before stopping. + local _filter='.[] | "\(.number)\t\(.title)"' + # A jq filter to apply to the return data. + # + # GitHub querystring arguments may also be passed as keyword arguments: + # + # * `assignee` + # * `creator` + # * `direction` + # * `labels` + # * `mentioned` + # * `milestone` + # * `per_page` + # * `since` + # * `sort` + # * `state` + + local url + local qs + + case $1 in + ('') url='/user/issues' ;; + (*=*) url='/user/issues' ;; + (*/*) url="/repos/${1}/issues"; shift 1 ;; + esac + + _opts_pagination "$@" + _opts_filter "$@" + _opts_qs "$@" + + _get "${url}${qs}" | _filter_json "$_filter" +} + +user_issues() { + # List all issues across owned and member repositories for the authenticated user + # + # Usage: + # + # user_issues + # user_issues since=2015-60-11T00:09:00Z + # + # Keyword arguments + # + local _follow_next + # Automatically look for a 'Links' header and follow any 'next' URLs. + local _follow_next_limit + # Maximum number of 'next' URLs to follow before stopping. + local _filter='.[] | "\(.repository.full_name)\t\(.number)\t\(.title)"' + # A jq filter to apply to the return data. + # + # GitHub querystring arguments may also be passed as keyword arguments: + # + # * `direction` + # * `filter` + # * `labels` + # * `per_page` + # * `since` + # * `sort` + # * `state` + + local qs + + _opts_pagination "$@" + _opts_filter "$@" + _opts_qs "$@" + + _get "/issues${qs}" | _filter_json "$_filter" +} + +create_issue() { + # Create an issue + # + # Usage: + # + # create_issue owner repo 'Issue title' body='Add multiline body + # content here' labels="$(./ok.sh _format_json -a foo bar)" + # + # Positional arguments + # + local owner="${1:?Owner name required.}" + # A GitHub repository. + local repo="${2:?Repo name required.}" + # A GitHub repository. + local title="${3:?Issue title required.}" + # A GitHub repository. + # + # Keyword arguments + # + local _filter='"\(.id)\t\(.number)\t\(.html_url)"' + # A jq filter to apply to the return data. + # + # Additional issue fields may be passed as keyword arguments: + # + # * `body` (string) + # * `assignee` (string) + # * `milestone` (integer) + # * `labels` (array of strings) + # * `assignees` (array of strings) + + shift 3 + + _opts_filter "$@" + + _format_json title="$title" "$@" \ + | _post "/repos/${owner}/${repo}/issues" \ + | _filter_json "$_filter" +} + +org_issues() { + # List all issues for a given organization for the authenticated user + # + # Usage: + # + # org_issues someorg + # + # Positional arguments + # + local org="${1:?Organization name required.}" + # Organization GitHub login or id. + # + # Keyword arguments + # + local _follow_next + # Automatically look for a 'Links' header and follow any 'next' URLs. + local _follow_next_limit + # Maximum number of 'next' URLs to follow before stopping. + local _filter='.[] | "\(.number)\t\(.title)"' + # A jq filter to apply to the return data. + # + # GitHub querystring arguments may also be passed as keyword arguments: + # + # * `direction` + # * `filter` + # * `labels` + # * `per_page` + # * `since` + # * `sort` + # * `state` + + shift 1 + local qs + + _opts_pagination "$@" + _opts_filter "$@" + _opts_qs "$@" + + _get "/orgs/${org}/issues${qs}" | _filter_json "$_filter" +} + +list_my_orgs() { + # List your organizations + # + # Usage: + # + # list_my_orgs + # + # Keyword arguments + # + local _follow_next + # Automatically look for a 'Links' header and follow any 'next' URLs. + local _follow_next_limit + # Maximum number of 'next' URLs to follow before stopping. + local _filter='.[] | "\(.login)\t\(.id)"' + # A jq filter to apply to the return data. + + local qs + + _opts_pagination "$@" + _opts_filter "$@" + _opts_qs "$@" + + _get "/user/orgs" | _filter_json "$_filter" +} + +list_orgs() { + # List all organizations + # + # Usage: + # + # list_orgs + # + # Keyword arguments + # + local _follow_next + # Automatically look for a 'Links' header and follow any 'next' URLs. + local _follow_next_limit + # Maximum number of 'next' URLs to follow before stopping. + local _filter='.[] | "\(.login)\t\(.id)"' + # A jq filter to apply to the return data. + + local qs + + _opts_pagination "$@" + _opts_filter "$@" + _opts_qs "$@" + + _get "/organizations" | _filter_json "$_filter" +} + +labels() { + # List available labels for a repository + # + # Usage: + # + # labels someuser/somerepo + # + # Positional arguments + # + local repo="$1" + # A GitHub repository. + # + # Keyword arguments + # + local _follow_next + # Automatically look for a 'Links' header and follow any 'next' URLs. + local _follow_next_limit + # Maximum number of 'next' URLs to follow before stopping. + local _filter='.[] | "\(.name)\t\(.color)"' + # A jq filter to apply to the return data. + + _opts_pagination "$@" + _opts_filter "$@" + + _get "/repos/${repo}/labels" | _filter_json "$_filter" +} + +add_label() { + # Add a label to a repository + # + # Usage: + # + # add_label someuser/somerepo LabelName color + # + # Positional arguments + # + local repo="${1:?Repo name required.}" + # A GitHub repository. + local label="${2:?Label name required.}" + # A new label. + local color="${3:?Hex color required.}" + # A color, in hex, without the leading `#`. + # + # Keyword arguments + # + local _filter='"\(.name)\t\(.color)"' + # A jq filter to apply to the return data. + + _opts_filter "$@" + + _format_json name="$label" color="$color" \ + | _post "/repos/${repo}/labels" \ + | _filter_json "$_filter" +} + +update_label() { + # Update a label + # + # Usage: + # + # update_label someuser/somerepo OldLabelName \ + # label=NewLabel color=newcolor + # + # Positional arguments + # + local repo="${1:?Repo name required.}" + # A GitHub repository. + local label="${2:?Label name required.}" + # The name of the label which will be updated + # + # Keyword arguments + # + local _filter='"\(.name)\t\(.color)"' + # A jq filter to apply to the return data. + # + # Label options may also be passed as keyword arguments, these will update + # the existing values: + # + # * `color` + # * `name` + + shift 2 + + _opts_filter "$@" + + _format_json "$@" \ + | _post "/repos/${repo}/labels/${label}" method='PATCH' \ + | _filter_json "$_filter" +} + +add_team_repo() { + # Add a team repository + # + # Usage: + # + # add_team_repo team_id organization repository_name permission + # + # Positional arguments + # + local team_id="${1:?Team id required.}" + # Team id to add repository to + local organization="${2:?Organization required.}" + # Organization to add repository to + local repository_name="${3:?Repository name required.}" + # Repository name to add + local permission="${4:?Permission required.}" + # Permission to grant: pull, push, admin + # + local url="/teams/${team_id}/repos/${organization}/${repository_name}" + + export OK_SH_ACCEPT="application/vnd.github.ironman-preview+json" + + _format_json "name=${name}" "permission=${permission}" | _post "$url" method='PUT' | _filter_json "${_filter}" +} + +list_pulls() { + # Lists the pull requests for a repository + # + # Usage: + # + # list_pulls user repo + # + # Positional arguments + # + local owner="${1:?Owner required.}" + # A GitHub owner. + local repo="${2:?Repo name required.}" + # A GitHub repository. + # + # Keyword arguments + # + local _follow_next + # Automatically look for a 'Links' header and follow any 'next' URLs. + local _follow_next_limit + # Maximum number of 'next' URLs to follow before stopping. + local _filter='.[] | "\(.number)\t\(.user.login)\t\(.head.repo.clone_url)\t\(.head.ref)"' + # A jq filter to apply to the return data. + + _opts_pagination "$@" + _opts_filter "$@" + + _get "/repos/${owner}/${repo}/pulls" | _filter_json "$_filter" +} + +create_pull_request() { + # Create a pull request for a repository + # + # Usage: + # + # create_pull_request someuser/somerepo title head base + # + # create_pull_request someuser/somerepo title head base body='Description here.' + # + # Positional arguments + # + local repo="${1:?Repo name required.}" + # A GitHub repository. + local title="${2:?Pull request title required.}" + # A title. + local head="${3:?Pull request head required.}" + # A head. + local base="${4:?Pull request base required.}" + # A base. + # + # Keyword arguments + # + local _filter='"\(.number)\t\(.html_url)"' + # A jq filter to apply to the return data. + # + # Pull request options may also be passed as keyword arguments: + # + # * `body` + # * `maintainer_can_modify` + + shift 4 + + _opts_filter "$@" + + _format_json title="$title" head="$head" base="$base" "$@" \ + | _post "/repos/${repo}/pulls" \ + | _filter_json "$_filter" +} + +update_pull_request() { + # Update a pull request for a repository + # + # Usage: + # + # update_pull_request someuser/somerepo number title='New title' body='New body' + # + # Positional arguments + # + local repo="${1:?Repo name required.}" + # A GitHub repository. + local number="${2:?Pull request number required.}" + # A pull request number. + # + # Keyword arguments + # + local _filter='"\(.number)\t\(.html_url)"' + # A jq filter to apply to the return data. + # + # Pull request options may also be passed as keyword arguments: + # + # * `base` + # * `body` + # * `maintainer_can_modify` + # * `state` (either open or closed) + # * `title` + + shift 2 + + _opts_filter "$@" + + _format_json "$@" \ + | _post "/repos/${repo}/pulls/${number}" method='PATCH' \ + | _filter_json "$_filter" +} + +transfer_repo() { + # Transfer a repository to a user or organization + # + # Usage: + # + # transfer_repo owner repo new_owner + # transfer_repo owner repo new_owner team_ids='[ 12, 345 ]' + # + # Positional arguments + # + local owner="${1:?Owner name required.}" + # Name of the current owner + # + local repo="${2:?Repo name required.}" + # Name of the current repo + # + local new_owner="${3:?New owner name required.}" + # Name of the new owner + # + # Keyword arguments + # + local _filter='"\(.name)"' + # A jq filter to apply to the return data. + # + # POST data may also be passed as keyword arguments: + # + # * `team_ids` + + shift 3 + + _opts_filter "$@" + + export OK_SH_ACCEPT='application/vnd.github.nightshade-preview+json' + _format_json "new_owner=${new_owner}" "$@" | _post "/repos/${owner}/${repo}/transfer" | _filter_json "${_filter}" +} + +archive_repo() { + # Archive a repo + # + # Usage: + # + # archive_repo owner/repo + # + # Positional arguments + # + local repo="${1:?Repo name required.}" + # A GitHub repository. + # + local _filter='"\(.name)\t\(.html_url)"' + # A jq filter to apply to the return data. + # + + shift 1 + + _opts_filter "$@" + + _format_json "archived=true" \ + | _post "/repos/${repo}" method='PATCH' \ + | _filter_json "$_filter" +} + +__main "$@" diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index e9961f2f..04a25ca1 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -62,4 +62,4 @@ jobs: moxie-0.9.4/bin/moxie -version - name: Build with Moxie - run: moxie-0.9.4/bin/moxie + run: moxie-0.9.4/bin/moxie test diff --git a/README.markdown b/README.markdown index 53776f8a..a40ccdf6 100644 --- a/README.markdown +++ b/README.markdown @@ -5,7 +5,7 @@ Gitblit is an open source, pure Java Git solution for managing, viewing, and ser More information about Gitblit can be found [here](http://gitblit.com).
-<a href='https://bintray.com/gitblit/releases/gitblit/_latestVersion'><img src='https://api.bintray.com/packages/gitblit/releases/gitblit/images/download.png'></a>
+<a href='https://github.com/fzs/gitblit/releases/latest'><img src='https://img.shields.io/badge/dynamic/json?color=9cf&label=Download&query=%24.name&url=https%3A%2F%2Fapi.github.com%2Frepos%2Ffzs%2Fgitblit%2Freleases%2Flatest'></a>
License
-------
@@ -26,7 +26,6 @@ Getting help | Documentation | [Gitblit website](http://gitblit.com) |
| Forums | [Google Groups](https://groups.google.com/forum/#!forum/gitblit) |
| Twitter | @gitblit or @jamesmoger |
-| Google+ | +gitblit or +jamesmoger |
Contributing
------------
diff --git a/build.moxie b/build.moxie index f4297923..d95db89a 100644 --- a/build.moxie +++ b/build.moxie @@ -94,6 +94,8 @@ dependencyDirectory: ext # Register the Eclipse JGit Maven repositories registeredRepositories: +- { id: central, url: 'https://repo1.maven.org/maven2' } +- { id: mavencentral, url: 'https://repo1.maven.org/maven2' } - { id: eclipse, url: 'http://repo.eclipse.org/content/groups/releases' } - { id: eclipse-snapshots, url: 'http://repo.eclipse.org/content/groups/snapshots' } - { id: gitblit, url: 'http://gitblit.github.io/gitblit-maven' } @@ -27,7 +27,12 @@ <property name="project.src.dir" value="${basedir}/src/main/java" />
<property name="project.resources.dir" value="${basedir}/src/main/resources" />
<property name="project.distrib.dir" value="${basedir}/src/main/distrib" />
-
+
+ <!-- Tools -->
+ <property name="octokit" location="${basedir}/.github/ok.sh" />
+ <property name="relnoawk" location="${basedir}/src/site/templates/ghreleasenotes.awk" />
+
+
<!--
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Initialize Moxie and setup build properties
@@ -41,6 +46,8 @@ <mx:init verbose="no" mxroot="${moxie.dir}" />
<!-- Set Ant project properties -->
+ <property name="release.tag" value="v${project.version}" />
+ <property name="currentRelease.tag" value="v${project.releaseVersion}" />
<property name="release.name" value="gitblit-${project.version}"/>
<property name="distribution.zipfile" value="${release.name}.zip" />
<property name="distribution.tgzfile" value="${release.name}.tar.gz" />
@@ -50,9 +57,13 @@ <property name="authority.zipfile" value="authority-${project.version}.zip" />
<property name="gbapi.zipfile" value="gbapi-${project.version}.zip" />
<property name="maven.directory" value="${basedir}/../gitblit-maven" />
+ <property name="releaselog" value="${basedir}/releases.moxie" />
+
+ <!-- GitHub user/organization name -->
+ <property name="gh.org" value="fzs" />
<!-- Download links -->
- <property name="gc.url" value="http://dl.bintray.com/gitblit/releases/" />
+ <property name="gc.url" value="https://github.com/${gh.org}/gitblit/releases/download/" />
<!-- Report Java version -->
<echo>JDK version: ${ant.java.version}</echo>
@@ -480,9 +491,9 @@ </fileset>
</delete>
</target>
-
-
- <!--
+
+
+ <!--
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Build the Gitblit Website
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -491,11 +502,9 @@ <echo>Building Gitblit Website ${project.version}</echo>
- <property name="releaselog" value="${basedir}/releases.moxie" />
-
<!-- Build Site -->
- <mx:doc googleplusid="114464678392593421684" googleanalyticsid="UA-24377072-1"
- googlePlusOne="true" minify="true" customless="custom.less">
+ <mx:doc googleanalyticsid="UA-24377072-1"
+ minify="true" customless="custom.less">
<structure>
<menu name="about">
<page name="overview" src="siteindex.mkd" out="index.html" headerLinks="false" />
@@ -567,20 +576,21 @@ </menu>
<menu name="downloads">
- <link name="Gitblit GO (Windows)" src="${gc.url}gitblit-${project.releaseVersion}.zip" />
- <link name="Gitblit GO (Linux/OSX)" src="${gc.url}gitblit-${project.releaseVersion}.tar.gz" />
- <link name="Gitblit WAR" src="${gc.url}gitblit-${project.releaseVersion}.war" />
+ <link name="Gitblit GO (Windows)" src="${gc.url}${currentRelease.tag}/gitblit-${project.releaseVersion}.zip" />
+ <link name="Gitblit GO (Linux/OSX)" src="${gc.url}${currentRelease.tag}/gitblit-${project.releaseVersion}.tar.gz" />
+ <link name="Gitblit WAR" src="${gc.url}${currentRelease.tag}/gitblit-${project.releaseVersion}.war" />
<divider />
<link name="Gitblit GO (Docker)" src="https://registry.hub.docker.com/u/jmoger/gitblit/" />
<divider />
<link name="Plugins Registry" src="http://plugins.gitblit.com" />
<divider />
- <link name="Gitblit Manager" src="${gc.url}manager-${project.releaseVersion}.zip" />
- <link name="Federation Client" src="${gc.url}fedclient-${project.releaseVersion}.zip" />
+ <link name="Gitblit Manager" src="${gc.url}${currentRelease.tag}/manager-${project.releaseVersion}.zip" />
+ <link name="Federation Client" src="${gc.url}${currentRelease.tag}/fedclient-${project.releaseVersion}.zip" />
<divider />
- <link name="API Library" src="${gc.url}gbapi-${project.releaseVersion}.zip" />
+ <link name="API Library" src="${gc.url}${currentRelease.tag}/gbapi-${project.releaseVersion}.zip" />
<divider />
- <link name="Bintray (1.4.0+)" src="https://bintray.com/gitblit/releases/gitblit" />
+ <link name="GitHub (1.9.0+)" src="https://github.com/${gh.org}/gitblit/releases" />
+ <link name="Bintray (1.4.0-1.8.0)" src="https://bintray.com/gitblit/releases/gitblit" />
<link name="GoogleCode (pre-1.4.0)" src="https://code.google.com/p/gitblit/downloads/list?can=1" />
<divider />
<link name="Maven Repository" src="${project.mavenUrl}" />
@@ -606,7 +616,7 @@ <divider />
</structure>
- <replace token="%GCURL%" value="${gc.url}" />
+ <replace token="%GCURL%" value="${gc.url}${currentRelease.tag}/" />
<properties token="%PROPERTIES%" file="${project.distrib.dir}/data/defaults.properties" />
@@ -688,7 +698,7 @@ <!-- Build gh-pages branch -->
<mx:ghpages repositorydir="${basedir}" obliterate="true" />
</target>
-
+
<!--
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -731,7 +741,84 @@ </target>
-
+
+ <!--
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ Publish binaries to GitHub release
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ -->
+ <target name="releaseBinaries" depends="prepare" description="Publish the Gitblit binaries to a GitHub release">
+
+ <ghReleaseDraft
+ releaselog="${releaselog}"
+ releasetag="${release.tag}"/>
+
+ <echo>Uploading Gitblit ${project.version} binaries</echo>
+
+ <!-- Upload Gitblit GO Windows ZIP file -->
+ <githubUpload
+ source="${project.targetDirectory}/${distribution.zipfile}"
+ target="gitblit-${project.version}.zip" />
+
+ <!-- Upload Gitblit GO Linux/Unix tar.gz file -->
+ <githubUpload
+ source="${project.targetDirectory}/${distribution.tgzfile}"
+ target="gitblit-${project.version}.tar.gz" />
+
+ <!-- Upload Gitblit WAR file -->
+ <githubUpload
+ source="${project.targetDirectory}/${distribution.warfile}"
+ target="gitblit-${project.version}.war" />
+
+ <!-- Upload Gitblit FedClient -->
+ <githubUpload
+ source="${project.targetDirectory}/${fedclient.zipfile}"
+ target="fedclient-${project.version}.zip" />
+
+ <!-- Upload Gitblit Manager -->
+ <githubUpload
+ source="${project.targetDirectory}/${manager.zipfile}"
+ target="manager-${project.version}.zip" />
+
+ <!-- Upload Gitblit API Library -->
+ <githubUpload
+ source="${project.targetDirectory}/${gbapi.zipfile}"
+ target="gbapi-${project.version}.zip" />
+
+
+ </target>
+
+
+ <!--
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ Publish GH release draft
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ -->
+ <target name="publishRelease" depends="prepare" description="Publish the GitHub release draft" >
+
+ <echo>Publishing Gitblit ${project.version} release draft on GitHub for tag ${release.tag}</echo>
+
+ <ghGetReleaseId
+ releaseVersion="${project.version}"/>
+ <exec executable="bash" logError="true" >
+ <arg value="-c" />
+ <arg value="${octokit} -q edit_release ${gh.org} gitblit ${ghrelease.id} tag_name='${release.tag}'"></arg>
+ </exec>
+ <ghPublishReleaseDraft
+ releaseid="${ghrelease.id}"/>
+
+ </target>
+
+
+ <!--
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ Build site and update GH pages for publishing
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ -->
+ <target name="updateSite" depends="buildSite,updateGhPages" description="Update the Gitblit pages site" >
+ </target>
+
+
<!--
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Publish site to site hosting service
@@ -760,16 +847,30 @@ <target name="tagRelease" depends="prepare" description="tag a new version and prepare for the next development cycle">
<!-- release -->
<property name="dryrun" value="false" />
- <mx:version stage="release" dryrun="${dryrun}" />
+ <mx:version stage="release" dryrun="${dryrun}" />
<property name="project.tag" value="v${project.version}" />
<!-- commit build.moxie & releases.moxie (automatic) -->
<mx:commit showtitle="no">
- <message>Prepare ${project.version} release</message>
+ <message>Prepare ${project.version} release</message>
<tag name="${project.tag}">
<message>${project.name} ${project.version} release</message>
</tag>
</mx:commit>
+ <!-- output version information for other scripts/programs to pick up -->
+ <mx:if>
+ <and>
+ <isset property="versionInfo" />
+ <not><equals arg1="${versionInfo}" arg2="" trim="true"/></not>
+ </and>
+ <then>
+ <echo file="${basedir}/${versionInfo}">
+GB_RELEASE_VERSION=${project.version}
+GB_RELEASE_TAG=${project.tag}
+</echo>
+ </then>
+ </mx:if>
+
<!-- create the release process script -->
<mx:if>
<os family="windows" />
@@ -862,6 +963,8 @@ <page name="bugtraq" src="setup_bugtraq.mkd" />
<page name="mirrors" src="setup_mirrors.mkd" />
<page name="scaling" src="setup_scaling.mkd" />
+ <page name="fail2ban" src="setup_fail2ban.mkd" />
+ <page name="filestore (Git LFS)" src="setup_filestore.mkd" />
<divider />
<page name="Gitblit as a viewer" src="setup_viewer.mkd" />
</menu>
@@ -893,10 +996,16 @@ <page name="design" src="design.mkd" />
<page name="rpc" src="rpc.mkd" />
</menu>
+
<menu name="changelog">
- <page name="current release" src="releasecurrent.mkd" />
- <page name="older releases" src="releasehistory.mkd" />
+ <page name="current release" out="releasenotes.html">
+ <template src="releasecurrent.ftl" data="${releaselog}" />
+ </page>
+ <page name="all releases" out="releases.html">
+ <template src="releasehistory.ftl" data="${releaselog}" />
+ </page>
</menu>
+
<menu name="links">
<link name="dev.gitblit.com (self-hosted)" src="https://dev.gitblit.com" />
<divider />
@@ -905,13 +1014,17 @@ <link name="Github" src="${project.scmUrl}" />
<link name="Issues" src="${project.issuesUrl}" />
<link name="Discussion" src="${project.forumUrl}" />
+ <link name="Twitter" src="https://twitter.com/gitblit" />
<link name="Ohloh" src="http://www.ohloh.net/p/gitblit" />
</menu>
</structure>
-
+
+ <replace token="%GCURL%" value="${gc.url}${currentRelease.tag}/" />
+
<properties token="%PROPERTIES%" file="${project.distrib.dir}/data/defaults.properties" />
-
- <regex searchPattern="\b(issue)(\s*[#]?|-){0,1}(\d+)\b" replacePattern="<a href='http://code.google.com/p/gitblit/issues/detail?id=$3'>issue $3</a>" />
+
+ <regex searchPattern="\b(commit)(\s*[#]?|-){0,1}([0-9a-fA-F]{5,})\b" replacePattern="<a href='https://github.com/gitblit/gitblit/commit/$3'>commit $3</a>" />
+ <regex searchPattern="\b(issue)(\s*[#]?|-){0,1}(\d+)\b" replacePattern="<a href='https://github.com/gitblit/gitblit/issues/$3'>issue $3</a>" />
<regex searchPattern="\b(pr|pull request)(\s*[#]?|-){0,1}(\d+)\b" replacePattern="<a href='https://github.com/gitblit/gitblit/pull/$3'>pull request #$3</a>" />
<regex searchPattern="\b(ticket)(\s*[#]?|-){0,1}(\d+)\b" replacePattern="<a href='https://dev.gitblit.com/tickets/gitblit.git/$3'>ticket $3</a>" />
@@ -983,7 +1096,7 @@ </copy>
</sequential>
</macrodef>
-
+
<!--
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Macro to upload binaries to Bintray
@@ -1002,6 +1115,83 @@ <!--
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ Macro to create release draft on GitHub
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ -->
+ <macrodef name="ghReleaseDraft">
+ <attribute name="releaselog" />
+ <attribute name="releasetag" />
+ <sequential>
+ <echo>creating release ${release.tag} draft on GitHub</echo>
+ <exec executable="bash" logError="true" failonerror="true" outputproperty="ghrelease.id">
+ <arg value="-c" />
+ <arg value="${octokit} create_release ${gh.org} gitblit @{releasetag} name=${project.version} draft=true | cut -f2"></arg>
+ </exec>
+ <exec executable="bash" logError="true" failonerror="true" outputproperty="ghrelease.upldUrl">
+ <arg value="-c" />
+ <arg value="${octokit} release ${gh.org} gitblit ${ghrelease.id} _filter=.upload_url | sed 's/{.*$/?name=/'"></arg>
+ </exec>
+ <exec executable="bash" logError="true" failonerror="true" outputproperty="ghrelease.notes">
+ <arg value="-c" />
+ <arg value="cat @{releaselog} | awk -f ${relnoawk} protect=true"></arg>
+ </exec>
+ <exec executable="bash" logError="true" >
+ <arg value="-c" />
+ <arg value="${octokit} -q edit_release ${gh.org} gitblit ${ghrelease.id} body='${ghrelease.notes}'"></arg>
+ </exec>
+ </sequential>
+ </macrodef>
+
+ <!--
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ Macro to upload binaries to GitHub
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ -->
+ <macrodef name="githubUpload">
+ <attribute name="source"/>
+ <attribute name="target"/>
+ <sequential>
+ <echo>uploading @{source} to GitHub release ${ghrelease.id}</echo>
+ <exec executable="bash" logError="true" failonerror="true" >
+ <arg value="-c" />
+ <arg value="${octokit} upload_asset ${ghrelease.upldUrl}@{target} @{source}"></arg>
+ </exec>
+ </sequential>
+ </macrodef>
+
+ <!--
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ Macro to publish release draft on GitHub
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ -->
+ <macrodef name="ghPublishReleaseDraft">
+ <attribute name="releaseid"/>
+ <sequential>
+ <echo>publishing GitHub release draft @{releaseid}</echo>
+ <exec executable="bash" logError="true" >
+ <arg value="-c" />
+ <arg value="${octokit} -q edit_release ${gh.org} gitblit @{releaseid} draft=false"></arg>
+ </exec>
+ </sequential>
+ </macrodef>
+
+ <!--
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ Macro to publish release draft on GitHub
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ -->
+ <macrodef name="ghGetReleaseId">
+ <attribute name="releaseVersion"/>
+ <sequential>
+ <exec executable="bash" logError="true" failonerror="true" outputproperty="ghrelease.id">
+ <arg value="-c" />
+ <arg value="${octokit} list_releases ${gh.org} gitblit _filter='.[] | "\(.name)\t\(.tag_name)\t\(.id)"' | grep @{releaseVersion} | cut -f3"></arg>
+ </exec>
+ </sequential>
+ </macrodef>
+
+ <!--
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Install Gitblit JAR for usage as Maven module
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-->
diff --git a/release.template b/release.template index 13225190..86a0b3f4 100644 --- a/release.template +++ b/release.template @@ -3,6 +3,28 @@ # ${project.version} release script # +# In order to build on CI systems, we need to make sure we have a Ant +# that we can use. Ant 1.10 requires Java 8, so we cannot use it for +# the current version. Instead, Moxie can be used if present. +if command -v ant 2> /dev/null 1>&2 ; then + if ant -version 2> /dev/null ; then + antCmd=ant + fi +fi +if [ -z "$antCmd" ] ; then + if command -v moxie 2> /dev/null 1>&2 ; then + antCmd=moxie + fi +fi +if [ -z "$antCmd" ] ; then + echo "Cannot find suitable ant or moxie. No build is possible." + exit 1 +fi + +# Check which branch we are on, so we can run this script not only on master +branch=$(git symbolic-ref -q --short HEAD) + + # ensure Maven repository is up-to-date echo "" echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" @@ -14,13 +36,13 @@ git checkout gh-pages git pull cd ${project.directory} -# go back one commit to RELEASE commit +# go back one commit to RELEASE commit (fzs: what, why? Let's go to the tag) echo "" echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" -echo "Checking out ${project.version} RELEASE commit ${project.commitId}" +echo "Checking out ${project.version} RELEASE commit ${project.tag}" echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" echo "" -git checkout ${project.commitId} +git checkout ${project.tag} # build RELEASE artifacts echo "" @@ -28,7 +50,7 @@ echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" echo "Building ${project.version} RELEASE artifacts" echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" echo "" -ant clean buildAll buildMavenArtifacts +$antCmd clean buildAll buildMavenArtifacts # commit all generated artifacts and metadata echo "" @@ -41,30 +63,22 @@ git add . git commit -m "${project.version} artifacts" cd ${project.directory} -# upload artifacts -echo "" -echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" -echo "Uploading ${project.version} artifacts" -echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" -echo "" -ant publishBinaries -# build site, update gh-pages, and ftp upload site to hosting provider +# build RELEASE site echo "" echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" -echo "Building ${project.version} website" +echo "Updating ${project.version} website" echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" echo "" -ant publishSite +$antCmd updateSite -# merge to master +# upload artifacts echo "" echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" -echo "Updating build identifier for next release cycle" +echo "Uploading ${project.version} artifacts" echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" echo "" -git checkout master -ant nextPointReleaseCycle +$antCmd releaseBinaries # push Maven repository to origin echo "" @@ -82,4 +96,23 @@ echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" echo "Pushing master, gh-pages, and tag ${project.tag}" echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" echo "" -git push origin master gh-pages ${project.tag} +git push origin gh-pages ${project.tag} + +# publish release draft +echo "" +echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" +echo "Publishing release ${project.version}" +echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" +echo "" +$antCmd publishRelease + +# merge to master (fzs: what? why merging?) +echo "" +echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" +echo "Updating build identifier for next release cycle" +echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" +echo "" +git checkout ${branch} +$antCmd nextPointReleaseCycle +git push origin + diff --git a/releases.moxie b/releases.moxie index db6c2dea..9776f0a9 100644 --- a/releases.moxie +++ b/releases.moxie @@ -63,8 +63,8 @@ r30: { - Encode email sender's name with UTF-8 (pr-1206) - Made Gitblit run on Java 9+ (issue-1262, issue-1294, pr-1266) - The JRE version is reported upon starting - - Add the 'ext' directory to the classpath on the command-line to start Gitblit and related programs. - - Report back that git command 'clone.bundle' is unsupported instead of simply failing + - Add the `ext` directory to the classpath on the command-line to start Gitblit and related programs. + - Report back that git command `clone.bundle` is unsupported instead of simply failing additions: - Added option to merge a ticket branch to the integration branch fast-forward or with a merge commit (pr-1142) - Added SSH key manager that retrieves keys from LDAP directory (pr-1160) diff --git a/src/site/siteindex.mkd b/src/site/siteindex.mkd index aec5c42a..40460e60 100644 --- a/src/site/siteindex.mkd +++ b/src/site/siteindex.mkd @@ -15,8 +15,7 @@ <div style="padding:5px;"><a style="width:175px;text-decoration:none;" class="btn btn-success" href="%GCURL%gitblit-${project.releaseVersion}.tar.gz">Download Gitblit GO (Linux/OSX)</a></div>
<div style="padding:5px;"><a style="width:175px;text-decoration:none;" class="btn btn-danger" href="%GCURL%gitblit-${project.releaseVersion}.war">Download Gitblit WAR</a></div>
<div style="padding:5px;"><a style="width:175px;text-decoration:none;" class="btn btn-primary" href="%GCURL%manager-${project.releaseVersion}.zip">Download Gitblit Manager</a></div>
- <a href='https://bintray.com/gitblit/releases/gitblit/view?source=watch' alt='Get automatic notifications about new "stable" versions'><img src='https://www.bintray.com/docs/images/bintray_badge_color.png'></a>
- </div>
+</div>
<div data-manifest="http://1c57d83a4c5f3a21ec25c050d4c5e37b.app.jelastic.com/xssu/cross/download/RDYYHABkAFJbUVlMMVU7RUtDARgATExFCEBuGS4jdQJKRUsEDwIBQmNTTEBI" data-width="280" data-theme="flat-blue" data-text="Get it hosted now!" data-tx-empty="Type your email and click the button" data-tx-invalid-email="Invalid email, please check the spelling" data-tx-error="An error has occurred, please try again later" data-tx-success="Check your email" class="je-app" ></div>
<div style="padding-top:5px;">
<table class="table condensed-table">
@@ -25,6 +24,7 @@ <tr><th>Sources</th><td><a href="${project.scmUrl}">GitHub</a></td></tr>
<tr><th>Issues</th><td><a href="${project.issuesUrl}">GitHub</a></td></tr>
<tr><th>Discussion</th><td><a href="${project.forumUrl}">Gitblit Group</a></td></tr>
+ <tr><th>Twitter</th><td><a href="https://twitter.com/gitblit">@gitblit</a></td></tr>
<tr><th>Ohloh</th><td><a target="_top" href="http://www.ohloh.net/p/gitblit"><img border="0" width="100" height="16" src="http://www.ohloh.net/p/gitblit/widgets/project_thin_badge.gif" alt="Ohloh project report for Gitblit" /></a></td></tr>
</tbody>
</table>
diff --git a/src/site/templates/ghreleasenotes.awk b/src/site/templates/ghreleasenotes.awk new file mode 100755 index 00000000..f4479552 --- /dev/null +++ b/src/site/templates/ghreleasenotes.awk @@ -0,0 +1,66 @@ +#! /usr/bin/env awk -f + +BEGIN { on=0 ; skip=1 ; block=0 ; section=""} + +/^[[:blank:]]*id:/ { relId = $NF } + +/r[0-9]+: *{/ { on=1 ; next } +/^[[:blank:]]*}[[:blank:]]*$/ { if (on) { + print "[Full release notes on gitblit.com](http://gitblit.com/releases.html#" relId ")" + exit 0 + } + } + + +on==1 && /^[[:blank:]]*[[:alnum:]]+:[[:blank:]]*(''|~)?$/ { + if (!block) { + skip=1 + if (section == "fixes:" || section == "changes:" || section == "additions:") { printf "\n</details>\n"} + if (section != "") print "" + if (section == "note:") print "----------" + section = "" + if ($NF == "~") next + } + else { + printSection() + next + } + if ($NF == "''") { + block = !block + } + } +on==1 && /^[[:blank:]]*note:/ { skip=0 ; section=$1; print "### Update Note" ; printSingleLineSection() ; next } +on==1 && /^[[:blank:]]*text:/ { skip=0 ; section=$1; printf "\n\n"; printSingleLineSection() ; next } +on==1 && /^[[:blank:]]*security:/ { skip=0 ; section=$1; print "### *Security*" ; next } +on==1 && /^[[:blank:]]*fixes:/ { skip=0 ; section=$1; printf "<details><summary>Fixes</summary>\n\n### Fixes\n" ; next} +on==1 && /^[[:blank:]]*changes:/ { skip=0 ; section=$1; printf "<details><summary>Changes</summary>\n\n### Changes\n" ; next} +on==1 && /^[[:blank:]]*additions:/ { skip=0 ; section=$1; printf "<details><summary>Additions</summary>\n\n### Additions\n" ; next} + +on==1 { + if ($1 == "''") { + block = !block + next + } + if ((block || !skip)) { + printSection() + } + } + +function printSingleLineSection() +{ + if (NF>1 && $2 != "''" && $2 != "~") { + if (protect) gsub(/'/, "'\\''") + for (i=2; i<= NF; i++) printf "%s ", $i + print "" + } +} + +function printSection() +{ + if (section != "text:") sub(/[[:blank:]]+/, "") + gsub(/pr-/, "PR #") + gsub(/issue-/, "issue #") + gsub(/commit-/, "commit ") + if (protect) gsub(/'/, "'\\''") + print $0 +}
\ No newline at end of file |