From cf5e6f9dc5196994aed64dfcba23f47419fc1888 Mon Sep 17 00:00:00 2001 From: typebrook Date: Sat, 1 Feb 2020 12:29:59 +0800 Subject: update --- gist | 511 ++++++++++++++++++++++++++++++++++++++----------------------------- 1 file changed, 293 insertions(+), 218 deletions(-) diff --git a/gist b/gist index 40d723a..ee400fb 100755 --- a/gist +++ b/gist @@ -15,13 +15,9 @@ # # * list your gists with format: [number] [url] [file_num] [comment_num] [short description] # gist [star | s] -# -# * clone gist repos which are not in local -# * pull master branch if a local repo is behind its remote -# gist (sync | S) # -# * Go to local gist repo -# . gist +# * show the path of local gist repo and files +# gist # # * create a new gist with files # gist (new | n) [-d | --desc ""] ... @@ -30,13 +26,13 @@ # gist (new | n) [-d | --desc ""] [-f | --file ] < # # * show the detail of a gist -# gist (detail | d) +# gist (detail | d) # # * edit a gist description -# gist (edit | e) +# gist (edit | e) # # * delete a gist -# gist (delete | D) ... +# gist (delete | D) ... # # * clean removed gists in local # gist (clean | C) @@ -46,109 +42,172 @@ # It is your business to do git commit and git push # # * configuration -# gist (config | c) [token ] [user ] [folder ] [auto-sync false] +# gist (config | c) [token|user|folder|auto-sync|EDITOR|action [value]] # # * show this help message # gist (help | h) -# TODO error handling, unit test # TODO parallel branch works with wget and other stuff -# TODO completion +# TODO new command "user" to fetch other user's gists # TODO grep mode for description, file content +# TODO push github.com (may need new token) +# TODO description for current directory +# TODO error handling, unit test +# TODO test on mac and remote machine +# TODO completion -# Validate settings. -config=~/.config/gistrc -set -eo pipefail +# Shell configuration [ "$TRACE" ] && set -x -## parse JSON from STDIN with string of commands -AccessJsonElement() { - PYTHONIOENCODING=utf-8 python -c "from __future__ import print_function; import sys, json; raw = json.load(sys.stdin); $1" - return "$?" +GITHUB_API=https://api.github.com +CONFIG=~/.config/gist.conf; mkdir -p ~/.config +configuredClient="" + +# handle configuration cases +_configure() { + local target="" + [[ -z "$@" ]] && (${EDITOR:-vi} $CONFIG) && return 0 + if [[ $1 == 'token' ]]; then + [[ ${#2} -eq 40 ]] && target=$1=$2 \ + || echo -e Invalid token format, it is not 40 chars '\n' > /dev/tty + elif [[ $1 == 'auto_sync' ]]; then + [[ $2 == 'false' ]] && target=$1=$2 \ + || target=$1=true + elif [[ $1 == 'folder' ]]; then + [[ -n "$2" ]] && target=$1=$2 \ + || target=$1=~/gist + elif [[ $1 == 'user' ]]; then + target=$1=$2 + fi + + umask 0077 && touch $CONFIG + [[ "$target" =~ [^=]$ ]] && sed -i "/^$1=/ d" $CONFIG && echo $target >> $CONFIG + cat $CONFIG } -_auth() { - echo 'Hi fellow! To access your gists, I need your Github username and a personal token with scope which allows "gist"!' +# prompt for username +_ask_username() { + while [[ ! $user =~ ^[[:alnum:]]+$ ]]; do + [[ -n $user ]] && echo "Not a valid username" read -p "Github username: " user < /dev/tty - if [[ $user =~ ^[[:alnum:]]+$ ]]; then - mkdir -p ~/.config - umask 0077 - echo user=$user > $config - else - echo "Not a valid username" - return 0 - fi - - echo -n "Create a new token from web browser? [Y/n] " - read answer < /dev/tty - if [[ ! $answer =~ ^(N|n|No|NO|no)$ ]]; then - python -mwebbrowser https://github.com/settings/tokens/new\?scopes\=gist; - fi + done + _configure user $user +} - read -p "Paste your token here: " new_token < /dev/tty - [[ $new_token =~ ^[[:alnum:]]{40}$ ]] \ - && echo token=$new_token >> $config \ - || echo "Not a valid token" +# prompt for toekn +# TODO token check, ref: https://developer.github.com/v3/apps/oauth_applications/#check-a-token +_ask_token() { + echo -n "Create a new token from web browser? [Y/n] " + read answer < /dev/tty + if [[ ! $answer =~ ^(N|n|No|NO|no)$ ]]; then + python -mwebbrowser https://github.com/settings/tokens/new\?scopes\=gist + fi + + while [[ ! $token =~ ^[[:alnum:]]{40}$ ]]; do + [[ -n $token ]] && echo "Not a valid token" + trap 'echo; return 1' INT + read -p "Paste your token here (Ctrl-C to skip): " token < /dev/tty + done + _configure token $token } -case "$1" in - config | c) ;; - *) - while ! source $config 2> /dev/null || [[ -z "$token" ]] || [[ -z "$user" ]]; do - _auth - done;; -esac +_validate_config(){ + source $CONFIG 2> /dev/null || true + if [[ ! -e $CONFIG || -z $user ]]; then + echo 'Hi fellow! To access your gists, I need your Github username' + echo "Also a personal token with scope which allows "gist"!'" + echo + _ask_username + _ask_token + elif [[ -z $token && $1 =~ ^(n|new|e|edit|D|delete)$ ]]; then + if ! (_ask_token); then + echo 'To create/edit/delete a gist, a token is needed' + return 1 + fi + elif [[ -z $token && $1 =~ ^(u|update)$ && $2 =~ ^(s|star) ]]; then + if ! (_ask_token); then + echo 'To get user starred gists, a token is needed' + return 1 + fi + fi +} + +# load configuration +_apply_config() { + source $CONFIG && _validate_config + + AUTH_HEADER="Authorization: token $token" + [[ -z "$action" ]] && action="${EDITOR:-vi} *" + [[ -z "$folder" ]] && folder=~/gist && mkdir -p $folder + INDEX=$folder/index +} -github_api=https://api.github.com -auth_header="Authorization: token $token" +_apply_config "$@" || exit 1 + +## This function determines which http get tool the system has installed and returns an error if there isnt one +getConfiguredClient() { + if command -v curl &>/dev/null; then + configuredClient="curl" + elif command -v wget &>/dev/null; then + configuredClient="wget" + elif command -v http &>/dev/null; then + configuredClient="httpie" + elif command -v fetch &>/dev/null; then + configuredClient="fetch" + else + echo "Error: This tool requires either curl, wget, httpie or fetch to be installed." >&2 + return 1 + fi +} -[[ -z "$folder" ]] && folder=~/gist -mkdir -p $folder -index=$folder/index -starred=$folder/starred +## Allows to call the users configured client without if statements everywhere +httpGet() { + local header="" + case "$configuredClient" in + curl) [[ -n $token ]] && header="--header Authorization: token $token"; curl -A curl -s $header "$@" ;; + wget) [[ -n $token ]] && header="--header Authorization: token $token"; wget -qO- $header "$@" ;; + httpie) [[ -n $token ]] && header="Authorization:token $token"; http -b GET "$@" "$header";; + fetch) fetch -q "$@" ;; + esac +} # Show the list of gist, but not updated time # TODO a way to show files # TODO show git status outdated _show_list() { - if [[ ! -e "$1" ]]; then - echo No local file found for last update - echo Please run command: - echo " gist update" - exit 0 - fi - cat $1 \ - | while read line_num link file_url_array file_num extra description; do - local repo=$folder/$(echo $link | sed 's#.*/##') - local occupy=0 - - # if repo is not yet cloned, show green message "Not cloned yet" - [[ ! -d $repo ]] && extra="\e[32m[Not cloned yet]\e[0m" && occupy=17 - - # if there are some changes in git index or working directory, show blue message "working" - [[ -n $(cd $repo && git status --short) ]] 2>/dev/null && extra="\e[36m[working]\e[0m" && occupy=10 - # if there is a commit not yet push, show red message "ahead" - [[ -n $(cd $repo && git cherry) ]] 2>/dev/null && extra="\e[31m[ahead]\e[0m" && occupy=8 - - echo -e $line_num $link $file_num $extra $(echo $description | cut -c -$(( 60 - $occupy )) ) - done + if [[ ! -e $INDEX ]]; then + echo 'No local file found for last update, please run command:' + echo ' gist update' + return 0 + fi + local filter="" + if [[ $1 == "s" ]]; then + filter='/^[^s]/ d' + else + filter='/^s/ d' + fi + cat $INDEX \ + | while read index link blob_code file_num extra description; do + local repo=$folder/$(echo $link | sed 's#.*/##') + local occupy=0 + + # if repo is not yet cloned, show green message "Not cloned yet" + [[ ! -d $repo ]] && extra="\e[32m[Not cloned yet]\e[0m" && occupy=16 + # if there are some changes in git index or working directory, show blue message "working" + [[ -n $(cd $repo && git status --short) ]] 2>/dev/null && extra="\e[36m[working]\e[0m" && occupy=9 + # if there is a commit not yet push, show red message "ahead" + [[ -n $(cd $repo && git cherry) ]] 2>/dev/null && extra="\e[31m[ahead]\e[0m" && occupy=7 + + echo -e $index $link $file_num $extra $(echo $description | cut -c -$(( 60 -$occupy -1 )) ) + done \ + | sed "$filter" } -# get the list of gists -_update() { - echo "fetching from api.github.com..." - echo - local list_file=$index - local route="users/$user/gists" - local mark="" - [[ "$1" =~ ^(star|s)$ ]] && list_file=$starred && route="gists/starred" && mark="s" - - curl -s -H "$auth_header" $github_api/$route \ - | _parse_response | nl -s' ' | sed -E "s/^ */$mark/" > $list_file \ - && _show_list $list_file \ - || echo Fail to update gists - - if [[ $auto_sync != "false" ]]; then (_sync_repos $1 > /dev/null 2>&1 &); fi +# parse JSON from STDIN with string of commands +AccessJsonElement() { + PYTHONIOENCODING=utf-8 \ + python -c "from __future__ import print_function; import sys, json; raw = json.load(sys.stdin); $1" 2> /dev/null + return "$?" } # equal to: jq '.[] | "\(.html_url) \([.files[] | .raw_url]) \(.files | keys | length) \(.comments) \(.description)"' @@ -157,6 +216,7 @@ _handle_gists() { for gist in raw: print(gist["html_url"], end=" ") print([file["raw_url"] for file in gist["files"].values()], end=" ") + print(gist["public"], end=" ") print(len(gist["files"]), end=" ") print(gist["comments"], end=" ") print(gist["description"]) @@ -164,83 +224,110 @@ for gist in raw: } # TODO check if a user create a very first gist +# parse response from gists require _parse_response() { - AccessJsonElement "$(_handle_gists)" \ - | tac | sed 's/, /,/g'\ - | while read link file_url_array file_num comment_num description; do - local blob_code=$(echo $file_url_array | tr ',' '\n' | sed -E 's#.*raw/(.*)/.*#\1#' | sort | cut -c -7 | paste -sd '-') - echo $link $blob_code $file_num $comment_num $description | tr -d '"' - done + AccessJsonElement "$(_handle_gists)" \ + | tac | sed 's/, /,/g' | nl -s' ' \ + | while read index link file_url_array public file_num comment_num description; do + local blob_code=$(echo $file_url_array | tr ',' '\n' | sed -E 's#.*raw/(.*)/.*#\1#' | sort | cut -c -7 | paste -sd '-') + [[ $public == 'False' ]] && local mark=p + [[ -n $1 ]] && local index=$1 + echo $mark$index $link $blob_code $file_num $comment_num $description | tr -d '"' + done +} + +# get latest list of gists from Github API +_update() { + echo "fetching $user's gists from $GITHUB_API..." + echo + local route="users/$user/gists" + local mark="" + local filter='/^[^s]/ d' + if [[ "$1" =~ ^(star|s)$ ]];then + route="gists/starred" + mark="s" + filter='/^[s]/ d' + fi + + local response=$(httpGet $GITHUB_API/$route) + false && echo Failed to update gists && return 1 + sed -i "$filter" $INDEX + echo $response | _parse_response >> $INDEX + _show_list $mark + + if [[ $auto_sync != "false" ]]; then (_sync_repos $1 > /dev/null 2>&1 &); fi } +# update local git repos _sync_repos() { - local list_file=$index - [[ "$1" =~ ^(star|s)$ ]] && list_file=$starred && route="gists/starred" - - # clone repos which are not in the local - comm -13 <(find $folder -maxdepth 1 -type d | sed '1d; s#.*/##' | sort) \ - <(cat $list_file | cut -d' ' -f2 | sed 's#.*/##' | sort) \ - | xargs -I{} --max-procs 8 git clone git@github.com:{}.git $folder/{} - - # pull if remote repo has different blob objects - cat $index | cut -d' ' -f2,3 \ - | while read url blob_code_remote; do - local repo=$folder/$(echo $url | sed 's#.*/##') - local blob_code_local=$(cd $repo && git ls-tree master | cut -d' ' -f3 | cut -c-7 | sort | paste -sd '-') - cd $repo \ - && [[ $blob_code_local != $blob_code_remote ]] \ - &&[[ $(git rev-parse origin/master) == $(git rev-parse master) ]] \ - && git pull - done - echo Everything is fine! + # clone repos which are not in the local + comm -13 <(find $folder -maxdepth 1 -type d | sed '1d; s#.*/##' | sort) \ + <(cat $INDEX | cut -d' ' -f2 | sed 's#.*/##' | sort) \ + | xargs -I{} --max-procs 8 git clone git@github.com:{}.git $folder/{} + + # pull if remote repo has different blob objects + cat $INDEX | cut -d' ' -f2,3 \ + | while read url blob_code_remote; do + local repo=$folder/$(echo $url | sed 's#.*/##') + local blob_code_local=$(cd $repo && git ls-tree master | cut -d' ' -f3 | cut -c-7 | sort | paste -sd '-') + cd $repo \ + && [[ $blob_code_local != $blob_code_remote ]] \ + && [[ $(git rev-parse origin/master) == $(git rev-parse master) ]] \ + && git pull + done + echo Everything is fine! } # get gist id from index files _gist_id() { - GIST_ID=$( (grep -hs '' $index $starred || true) | sed -n "/^$1 / p" | cut -d' ' -f2 | sed -E 's#.*/##') - if [[ -z "$GIST_ID" ]]; then - echo -e "Not a valid index: \e[31m$1\e[0m" - echo Use the index number in the first column instead: - echo - _show_list "$index" - exit 1 - fi + GIST_ID=$( (grep -hs '' $INDEX || true) | sed -n "/^$1 / p" | cut -d' ' -f2 | sed -E 's#.*/##') + if [[ -z "$GIST_ID" ]]; then + echo -e "Not a valid index: \e[31m$1\e[0m" + echo Use the index in the first column instead: + echo + _show_list + return 1 + fi } _goto_gist() { - _gist_id $1 + _gist_id $1 || return 1 - if [[ ! -d $folder/$GIST_ID ]]; then - echo 'Cloning gist as repo...' - git clone git@github.com:$GIST_ID.git $folder/$GIST_ID \ - && echo 'Repo is cloned' \ - || echo 'Failed to clone the gist' + if [[ ! -d $folder/$GIST_ID ]]; then + echo 'Cloning gist as repo...' + git clone git@github.com:$GIST_ID.git $folder/$GIST_ID + + if [[ $? -eq 0 ]]; then + echo 'Repo is cloned' > /dev/tty + else + echo 'Failed to clone the gist' > /dev/tty + return 1 fi + fi - echo This gist is at $folder/$GIST_ID - echo -e "You can run the following command to jump to this directory: \n" - echo -e " \e[32m. gist $1\e[0m\n" - cd $folder/$GIST_ID && ls && tig --all 2> /dev/null + (cd $folder/$GIST_ID && eval "$action") + echo $folder/$GIST_ID } _delete_gist() { - for i in "$@"; do - _gist_id "$i" - curl -X DELETE -s -H "$auth_header" $github_api/gists/$GIST_ID \ - && echo "$i" deleted \ - && sed -i -E "/^$i / d" $index - done + for i in "$@"; do + _gist_id "$i" + curl -X DELETE -s -H "$AUTH_HEADER" $GITHUB_API/gists/$GIST_ID \ + && echo "$i" deleted \ + && sed -i -E "/^$i / d" $INDEX + done } # remove repos which are not in user gists anymore _clean_repos() { - comm -23 <(find $folder -maxdepth 1 -type d | sed '1d; s#.*/##' | sort) \ - <(cat $index $starred 2> /dev/null | cut -d' ' -f2 | sed 's#.*/##' | sort) \ - | while read dir; do - mv $folder/$dir /tmp && echo move $folder/$dir to /tmp - done + comm -23 <(find $folder -maxdepth 1 -type d | sed '1d; s#.*/##' | sort) \ + <(cat $INDEX 2> /dev/null | cut -d' ' -f2 | sed 's#.*/##' | sort) \ + | while read dir; do + mv $folder/$dir /tmp && echo move $folder/$dir to /tmp + done } +# parse JSON from gist detail _handle_gist() { echo ' print("site:", raw["html_url"]) @@ -269,104 +356,92 @@ for comment in raw: # TODO format with simple text _show_detail() { - _gist_id $1 - curl -s $github_api/gists/$GIST_ID \ - | AccessJsonElement "$(_handle_gist)" + _gist_id $1 + httpGet $GITHUB_API/gists/$GIST_ID \ + | AccessJsonElement "$(_handle_gist)" - curl -s $github_api/gists/$GIST_ID/comments \ - | AccessJsonElement "$(_handle_comment)" + httpGet $GITHUB_API/gists/$GIST_ID/comments \ + | AccessJsonElement "$(_handle_comment)" } -# FIXME put file before parameters +# set filename/description/permission for a new gist _set_gist() { - while [[ "$1" =~ ^- && "$1" != "--" ]]; do case $1 in - -d | --desc) - description="$2" - shift; shift;; - -f | --file) - filename="$2" - shift; shift;; - esac - done - if [[ "$1" == '--' ]]; then shift; fi - files="$@" - ls $files > /dev/null || exit 1 + public=true + while [[ -n "$@" ]]; do case $1 in + -d | --desc) + description="$2" + shift; shift;; + -f | --file) + filename="$2" + shift; shift;; + -p) + public=false + shift;; + *) + files="$1 $files" + shift;; + esac + done + ls $files > /dev/null || return 1 } +# Let user type the content of gist before setting filename _new_file() { - [[ -t 0 ]] && echo "Type a gist. to cancel, when done" > /dev/tty - tmp_file=$(mktemp) - cat > $tmp_file - echo -e '\n' > /dev/tty - [[ -z "$1" ]] && read -p 'Type file name: ' filename < /dev/tty - mv $tmp_file /tmp/$filename - echo /tmp/$filename + [[ -t 0 ]] && echo "Type a gist. to cancel, when done" > /dev/tty + local tmp_file=$(mktemp) + cat > $tmp_file + echo -e '\n' > /dev/tty + [[ -z "$1" ]] && read -p 'Type file name: ' filename < /dev/tty + mv $tmp_file /tmp/$filename + echo /tmp/$filename } # create a new gist with files -# TODO support secret gist _create_gist() { - _set_gist "$@" - [[ -z "$files" ]] && files=$(_new_file $filename) - [[ -z "$description" ]] && read -p 'Type description: ' description < /dev/tty - - for file in $files; do - echo "\"$(basename $file)\": {\"content\": \"$(sed '$ !s/$/\\n/' $file)\"}," - done | tr -d '\n' | sed 's/^/{/; s/,$/}/' \ - | echo "{ \"public\": true, \"files\": $(cat -), \"description\": \"$description\"}" \ - | curl -s -H "$auth_header" --data @- $github_api/gists \ - | sed '1 s/^/[/; $ s/$/]/' \ - | _parse_response \ - | sed -E "s/^/$(( $(wc -l $index | cut -d' ' -f1) + 1 )) /" >> $index \ - && echo -e '\nGist created' \ - || echo 'Fail to create gist' - - _show_list $index | tail -1 + _set_gist "$@" || return 1 + [[ -z "$files" ]] && files=$(_new_file $filename) + [[ -z "$description" ]] && read -p 'Type description: ' description < /dev/tty + + local index=$(( $(sed '/^s/ d' $INDEX | wc -l) +1 )) + echo 'Creating a new gist...' + for file in $files; do + echo "\"$(basename $file)\": {\"content\": \"$(sed '$ !s/$/\\n/' $file)\"}," + done | tr -d '\n' | sed 's/^/{/; s/,$/}/' \ + | echo "{ \"public\": $public, \"files\": $(cat -), \"description\": \"$description\"}" \ + | curl -s -H "$AUTH_HEADER" --data @- $GITHUB_API/gists \ + | tee jojo \ + | sed '1 s/^/[/; $ s/$/]/' \ + | _parse_response $index >> $INDEX + + if [[ $? -eq 0 ]]; then + echo 'Gist is created' + _show_list | tail -1 + else + echo 'Failed to create gist' + fi } # update description of a gist _edit_gist() { - _gist_id $1 - - echo -n 'Type new description: ' - read DESC < /dev/tty - echo "{ \"description\": \"$DESC\" }" \ - | curl -X PATCH -H "$auth_header" --data @- $github_api/gists/$GIST_ID > /dev/null \ - && _update -} - -_help_message() { - sed -E -n ' /^$/ q; 8,$ s/^#//p' $0 -} + _gist_id $1 -_cases() { - if [[ $1 == 'token' ]]; then - [[ ${#2} -eq 40 ]] && echo $1=$2 \ - || echo -e Invalid token format, it is not 40 chars '\n' > /dev/tty - elif [[ $1 == 'auto_sync' ]]; then - [[ $2 == 'false' ]] && echo $1=$2 \ - || echo $1=true - elif [[ $1 == 'folder' ]]; then - [[ -n "$2" ]] && echo $1=$2 \ - || echo $1=~/gist - elif [[ $1 == 'user' ]]; then - echo $1=$2 - fi + echo -n 'Type new description: ' + read DESC < /dev/tty + echo "{ \"description\": \"$DESC\" }" \ + | curl -X PATCH -H "$AUTH_HEADER" --data @- $GITHUB_API/gists/$GIST_ID > /dev/null \ + && _update } -_configure() { - [[ -z "$@" ]] && (vim $config) && exit 0 - target=$(_cases "$@") - - [[ "$target" =~ [^=]$ ]] && sed -i "/^$1=/ d" $config && echo $target >> $config - cat $config +usage() { + sed -E -n ' /^$/ q; 8,$ s/^#//p' $0 } +getConfiguredClient case "$1" in "") - _show_list $index ;; + _show_list ;; star | s) - _show_list $starred ;; + _show_list s ;; update | u) _update "$2" ;; new | n) @@ -388,7 +463,7 @@ case "$1" in shift _configure "$@" ;; help | h) - _help_message ;; + usage ;; *) _goto_gist "$1" ;; esac -- cgit v1.2.3-70-g09d2