#!/usr/bin/env bash # # Author: Hsieh Chin Fan (typebrook) # License: MIT # https://gist.github.com/typebrook/b0d2e7e67aa50298fdf8111ae7466b56 # # # This script host your gists as local cloned git repo # It works under GNU with jq and curl, both are easy to get in most cases # # Use the following commands to manage your gists: # # * update the local list of your gists, star for your starred gists # gist (update | u) [star | s] # # * 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 # # * create a new gist with files # gist (new | n) [-d | --desc ""] ... # # * create a new gist with STDIN # gist (new | n) [-d | --desc ""] [-f | --file ] < # # * show the detail of a gist # gist (detail | d) # # * edit a gist description # gist (edit | e) # # * delete a gist # gist (delete | D) ... # # * clean removed gists in local # gist (clean | C) # # * update a gist # Since now a gist is a local cloned repo # It is your business to do git commit and git push # # * show this help message # gist (help | h) # TODO add config to help message # TODO error handling, unit test # TODO parallel branch works with json parsing on python # TODO parallel branch works with wget and other stuff # TODO completion # Validate settings. config=~/.config/gistrc [ "$TRACE" ] && set -x # TODO error handling while password is not true # TODO support access token from input or web _auth() { local data="{\"scopes\":[\"gist\"], \"note\": \"gist-$(date -u +'%Y-%m-%dT%H:%M:%SZ')\"}" read -p "Github username: " user read -sp "Github password: " password mkdir -p ~/.config && umask 0077 && echo user=$user > $config curl https://api.github.com/authorizations \ --user "$user:$password" \ --data "$data" > /dev/null read -p "2-factor code: " OTP curl https://api.github.com/authorizations \ --user "$user:$password" -H "X-GitHub-OTP: $OTP" \ --data "$data" |\ sed '1 s/[^{]//g' | jq -r .token \ | sed 's/^/token=/' >> $config } case "$1" in config | c) ;; *) while ! source $config 2> /dev/null || [[ -z "$token" ]] || [[ -z "$user" ]]; do _auth done;; esac github_api=https://api.github.com auth_header="Authorization: token $token" [[ -z "$folder" ]] && folder=~/gist mkdir -p $folder index=$folder/index starred=$folder/starred # 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#.*/##') # if repo is not yet cloned, show green message "Sync Now" # FIXME [[ ! -d $repo ]] && extra="\e[32m[Sync Now]\e[0m" # if there are some changes in git index or working directory, show blue message "working" [[ -n $(git status --short) ]] 2>/dev/null && extra="\e[36m[working]\e[0m" # if there is a commit not yet push, show red message "ahead" [[ -n $(git cherry) ]] 2>/dev/null && extra="\e[31m[ahead]\e[0m" echo -e $line_num $link $file_num $extra $(echo $description | cut -c -60) done } # get the list of gists # TODO support secret gist _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 if [[ $auto_sync != "false" ]]; then (_sync_repos $1 > /dev/null 2>&1 &); fi } # TODO check if a user create a very first gist _parse_response() { jq '.[] | "\(.html_url) \([.files[] | .raw_url]) \(.files | keys | length) \(.comments) \(.description)"' \ | tac \ | while read link file_url_array file_num comment_num description; do local blob_code=$(echo $file_url_array | jq -r '.[]' | sed -E 's#.*raw/(.*)/.*#\1#' | sort | cut -c -7 | paste -sd '-') echo $link $blob_code $file_num $comment_num $description | tr -d '"' done } _sync_repos() { local list_file=$index [[ "$1" == "--star" ]] && 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{} 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! } _gist_id() { GIST_ID=$(cat $index $starred 2> /dev/null | 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 } # FIXME error handling, if repo not cloned yet _goto_gist() { _gist_id $1 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" echo cd $folder/$GIST_ID && ls && tig --all 2> /dev/null } _delete_gist() { for i in "$@"; do _gist_id "$i" curl -X DELETE -s -H "$auth_header" $github_api/gists/$GIST_ID && \ echo "$i" deleted done _update } # 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 | cut -d' ' -f2 | sed 's#.*/##' | sort) \ | while read dir; do mv $folder/$dir /tmp && echo move $folder/$dir to /tmp done } # TODO format with simple text _show_detail() { _gist_id $1 curl -s $github_api/gists/$GIST_ID \ | jq '{site: .html_url, description: .description, public: .public, API: .url, created_at: .created_at, updated_at: .updated_at, files: (.files | keys)}' curl -s $github_api/gists/$GIST_ID/comments \ | jq '.[] | {user: .user.login, created_at: .created_at, updated_at: .updated_at, body: .body}' } _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 # TODO could be simplified? files="$@" && echo $files | xargs ls > /dev/null || exit 1 } _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 # FIXME when file content is from STDIN, read fails [[ -z "$1" ]] && read -p 'Type file name: ' filename mv $tmp_file /tmp/$filename echo /tmp/$filename } # create a new gist with files # FIXME error handling if gist is not created, file doesn't exist _create_gist() { _set_gist "$@" [[ -z "$files" ]] && files=$(_new_file $filename) # FIXME when file content is from STDIN, read fails [[ -z "$description" ]] && read -p 'Type description: ' description for file in $files; do FILE=$(basename $file) jq --arg FILE "$FILE" '. as $content | { ($FILE): {content: $content} }' -Rs $file done \ | jq --slurp --arg DESC "$description" '{ public: true, files: add, description: ($DESC) }' \ | curl -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' _show_list $index | tail -1 } # update description of a gist _edit_gist() { _gist_id $1 echo -n 'Type new description: ' read DESC jq -n --arg DESC "$DESC" '{ 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 } _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 } _configure() { [[ -z "$@" ]] && (vim $config) && exit 0 target=$(_cases "$@") [[ "$target" =~ [^=]$ ]] && sed -i "/^$1=/ d" $config && echo $target >> $config cat $config } case "$1" in "") _show_list $index ;; star | s) _show_list $starred ;; update | u) _update "$2" ;; new | n) shift _create_gist "$@" ;; edit | e) _edit_gist "$2" ;; sync | S) _sync_repos ;; detail | d) shift _show_detail "$@" ;; delete | D) shift _delete_gist "$@" ;; clean | C) _clean_repos ;; config | c) shift _configure "$@" ;; help | h) _help_message ;; *) _goto_gist "$1" ;; esac