diff options
-rwxr-xr-x | gist | 185 |
1 files changed, 101 insertions, 84 deletions
@@ -4,60 +4,42 @@ | |||
4 | # License: MIT | 4 | # License: MIT |
5 | # https://gist.github.com/typebrook/b0d2e7e67aa50298fdf8111ae7466b56 | 5 | # https://gist.github.com/typebrook/b0d2e7e67aa50298fdf8111ae7466b56 |
6 | # | 6 | # |
7 | # gist | ||
8 | # Description: Host your gists as local cloned git repo | ||
9 | # Usage: gist [command] [<args>] | ||
7 | # | 10 | # |
8 | # This script host your gists as local cloned git repo | 11 | # [star | s] list your gists with format below, star for your starred gists: |
9 | # It works under GNU curl, which is easy to get in most cases | 12 | # [index_of_gist] [url] [file_num] [comment_num] [short description] |
13 | # update, u [star | s] update the local list of your gists, star for your starred gists | ||
14 | # <index_of_gist> show the path of local gist repo and do custom actions | ||
15 | # new, n [-d | --desc <description>] <files>... create a new gist with files | ||
16 | # new, n [-d | --desc <description>] [-f | --file <file_name>] create a new gist from STDIN | ||
17 | # detail, d <index_of_gist> show the detail of a gist | ||
18 | # edit, e <index_of_gist> edit a gist description | ||
19 | # delete, D <index_of_gist>... delete a gist | ||
20 | # clean, C clean removed gists in local | ||
21 | # config, c [token | user | folder | auto-sync | EDITOR | action [value] ] do configuration | ||
22 | # user, U <user> get gists from a given Github user | ||
23 | # help, h show this help message | ||
10 | # | 24 | # |
11 | # Use the following commands to manage your gists: | 25 | # Example: |
12 | # | 26 | # gist (Show your gists) |
13 | # * update the local list of your gists, star for your starred gists | 27 | # gist 3 (show the repo path of your 3rd gist, and do custom actions) |
14 | # gist (update | u) [star | s] | ||
15 | # | ||
16 | # * list your gists with format: [number] [url] [file_num] [comment_num] [short description] | ||
17 | # gist [star | s] | ||
18 | # | 28 | # |
19 | # * show the path of local gist repo and files | ||
20 | # gist <index_of_gist> | ||
21 | # | ||
22 | # * create a new gist with files | ||
23 | # gist (new | n) [-d | --desc "<gist-description>"] <files>... | ||
24 | # | ||
25 | # * create a new gist with STDIN | ||
26 | # gist (new | n) [-d | --desc "<gist-description>"] [-f | --file <file>] < <file-with-content> | ||
27 | # | ||
28 | # * show the detail of a gist | ||
29 | # gist (detail | d) <index_of_gist> | ||
30 | # | ||
31 | # * edit a gist description | ||
32 | # gist (edit | e) <index_of_gist> | ||
33 | # | ||
34 | # * delete a gist | ||
35 | # gist (delete | D) <index_of_gist>... | ||
36 | # | ||
37 | # * clean removed gists in local | ||
38 | # gist (clean | C) | ||
39 | # | ||
40 | # * update a gist | ||
41 | # Since now a gist is a local cloned repo | 29 | # Since now a gist is a local cloned repo |
42 | # It is your business to do git commit and git push | 30 | # It is your business to do git commit and git push |
43 | # | ||
44 | # * configuration | ||
45 | # gist (config | c) [token|user|folder|auto-sync|EDITOR|action [value]] | ||
46 | # | ||
47 | # * show this help message | ||
48 | # gist (help | h) | ||
49 | 31 | ||
50 | # TODO parallel branch works with wget and other stuff | ||
51 | # TODO new command "user" to fetch other user's gists | ||
52 | # TODO grep mode for description, file content | 32 | # TODO grep mode for description, file content |
53 | # TODO push github.com (may need new token) | 33 | # TODO push github.com (may need new token) |
54 | # TODO description for current directory | 34 | # TODO description for current directory |
55 | # TODO error handling, unit test | 35 | # TODO unit test |
56 | # TODO test on mac and remote machine | 36 | # TODO test on mac and remote machine |
57 | # TODO completion | 37 | # TODO completion |
58 | 38 | ||
59 | # Shell configuration | 39 | # Shell configuration |
40 | set -o pipefail | ||
60 | [ "$TRACE" ] && set -x | 41 | [ "$TRACE" ] && set -x |
42 | trap 'rm -f "$http_data" "tmp_file"' EXIT | ||
61 | 43 | ||
62 | GITHUB_API=https://api.github.com | 44 | GITHUB_API=https://api.github.com |
63 | CONFIG=~/.config/gist.conf; mkdir -p ~/.config | 45 | CONFIG=~/.config/gist.conf; mkdir -p ~/.config |
@@ -134,17 +116,17 @@ _validate_config(){ | |||
134 | 116 | ||
135 | # load configuration | 117 | # load configuration |
136 | _apply_config() { | 118 | _apply_config() { |
137 | source $CONFIG && _validate_config | 119 | _validate_config "$@" || return 1 |
138 | 120 | ||
139 | AUTH_HEADER="Authorization: token $token" | 121 | AUTH_HEADER="Authorization: token $token" |
140 | [[ -z "$action" ]] && action="${EDITOR:-vi} *" | 122 | [[ -z "$action" ]] && action="${EDITOR:-vi} ." |
141 | [[ -z "$folder" ]] && folder=~/gist && mkdir -p $folder | 123 | [[ -z "$folder" ]] && folder=~/gist && mkdir -p $folder |
142 | INDEX=$folder/index | 124 | INDEX=$folder/index |
143 | } | 125 | } |
144 | 126 | ||
145 | _apply_config "$@" || exit 1 | 127 | _apply_config "$@" || exit 1 |
146 | 128 | ||
147 | ## This function determines which http get tool the system has installed and returns an error if there isnt one | 129 | # This function determines which http get tool the system has installed and returns an error if there isnt one |
148 | getConfiguredClient() { | 130 | getConfiguredClient() { |
149 | if command -v curl &>/dev/null; then | 131 | if command -v curl &>/dev/null; then |
150 | configuredClient="curl" | 132 | configuredClient="curl" |
@@ -160,13 +142,20 @@ getConfiguredClient() { | |||
160 | fi | 142 | fi |
161 | } | 143 | } |
162 | 144 | ||
163 | ## Allows to call the users configured client without if statements everywhere | 145 | # Allows to call the users configured client without if statements everywhere |
164 | httpGet() { | 146 | http_method() { |
165 | local header="" | 147 | local METHOD=$1; shift |
166 | case "$configuredClient" in | 148 | case "$configuredClient" in |
167 | curl) [[ -n $token ]] && header="--header Authorization: token $token"; curl -A curl -s $header "$@" ;; | 149 | curl) [[ -n $token ]] && local extra="--header" local header="Authorization: token $token" |
168 | wget) [[ -n $token ]] && header="--header Authorization: token $token"; wget -qO- $header "$@" ;; | 150 | [[ $METHOD =~ (POST|PATCH) ]] && extra2="--data" |
169 | httpie) [[ -n $token ]] && header="Authorization:token $token"; http -b GET "$@" "$header";; | 151 | curl -X $METHOD -A curl -s $extra "$header" $extra2 @$http_data "$@" ;; |
152 | wget) [[ -n $token ]] && local extra="--header" local header="Authorization: token $token" | ||
153 | [[ $METHOD =~ (POST|PATCH) ]] && extra2='--body-file' | ||
154 | wget --method=$METHOD -qO- $extra "$header" $extra2 $http_data "$@" ;; | ||
155 | httpie) [[ -n $token ]] && header="Authorization:token $token" | ||
156 | [[ $METHOD =~ (POST|PATCH) ]] && extra2="@$http_data" | ||
157 | http -b $METHOD "$@" "$header" $extra2 ;; | ||
158 | # TODO add other methods | ||
170 | fetch) fetch -q "$@" ;; | 159 | fetch) fetch -q "$@" ;; |
171 | esac | 160 | esac |
172 | } | 161 | } |
@@ -180,14 +169,11 @@ _show_list() { | |||
180 | echo ' gist update' | 169 | echo ' gist update' |
181 | return 0 | 170 | return 0 |
182 | fi | 171 | fi |
183 | local filter="" | 172 | local filter='/^s/ d; /^$/ d' |
184 | if [[ $1 == "s" ]]; then | 173 | [[ $1 == "s" ]] && filter='/^[^s]/ d; /^$/ d' |
185 | filter='/^[^s]/ d' | 174 | |
186 | else | 175 | while read index link blob_code file_num extra author description; do |
187 | filter='/^s/ d' | 176 | [[ $1 == "s" ]] && local author=$author |
188 | fi | ||
189 | cat $INDEX \ | ||
190 | | while read index link blob_code file_num extra description; do | ||
191 | local repo=$folder/$(echo $link | sed 's#.*/##') | 177 | local repo=$folder/$(echo $link | sed 's#.*/##') |
192 | local occupy=0 | 178 | local occupy=0 |
193 | 179 | ||
@@ -198,27 +184,30 @@ _show_list() { | |||
198 | # if there is a commit not yet push, show red message "ahead" | 184 | # if there is a commit not yet push, show red message "ahead" |
199 | [[ -n $(cd $repo && git cherry) ]] 2>/dev/null && extra="\e[31m[ahead]\e[0m" && occupy=7 | 185 | [[ -n $(cd $repo && git cherry) ]] 2>/dev/null && extra="\e[31m[ahead]\e[0m" && occupy=7 |
200 | 186 | ||
201 | echo -e $index $link $file_num $extra $(echo $description | cut -c -$(( 60 -$occupy -1 )) ) | 187 | echo -e $index $link $author $file_num $extra $(echo $description | cut -c -$(( 60 -$occupy -1 )) ) |
202 | done \ | 188 | done < $INDEX \ |
203 | | sed "$filter" | 189 | | sed "$filter" |
190 | echo -e '\nrun "gist help" for more details' | ||
204 | } | 191 | } |
205 | 192 | ||
206 | # parse JSON from STDIN with string of commands | 193 | # parse JSON from STDIN with string of commands |
207 | AccessJsonElement() { | 194 | AccessJsonElement() { |
208 | PYTHONIOENCODING=utf-8 \ | 195 | PYTHONIOENCODING=utf-8 \ |
209 | python -c "from __future__ import print_function; import sys, json; raw = json.load(sys.stdin); $1" 2> /dev/null | 196 | python -c "from __future__ import print_function; import sys, json; $1" |
210 | return "$?" | 197 | return "$?" |
211 | } | 198 | } |
212 | 199 | ||
213 | # equal to: jq '.[] | "\(.html_url) \([.files[] | .raw_url]) \(.files | keys | length) \(.comments) \(.description)"' | 200 | # equal to: jq '.[] | "\(.html_url) \([.files[] | .raw_url]) \(.files | keys | length) \(.comments) \(.description)"' |
214 | _handle_gists() { | 201 | _handle_gists() { |
215 | echo ' | 202 | echo ' |
203 | raw = json.load(sys.stdin) | ||
216 | for gist in raw: | 204 | for gist in raw: |
217 | print(gist["html_url"], end=" ") | 205 | print(gist["html_url"], end=" ") |
218 | print([file["raw_url"] for file in gist["files"].values()], end=" ") | 206 | print([file["raw_url"] for file in gist["files"].values()], end=" ") |
219 | print(gist["public"], end=" ") | 207 | print(gist["public"], end=" ") |
220 | print(len(gist["files"]), end=" ") | 208 | print(len(gist["files"]), end=" ") |
221 | print(gist["comments"], end=" ") | 209 | print(gist["comments"], end=" ") |
210 | print(gist["owner"]["login"], end=" ") | ||
222 | print(gist["description"]) | 211 | print(gist["description"]) |
223 | ' | 212 | ' |
224 | } | 213 | } |
@@ -228,36 +217,48 @@ for gist in raw: | |||
228 | _parse_response() { | 217 | _parse_response() { |
229 | AccessJsonElement "$(_handle_gists)" \ | 218 | AccessJsonElement "$(_handle_gists)" \ |
230 | | tac | sed 's/, /,/g' | nl -s' ' \ | 219 | | tac | sed 's/, /,/g' | nl -s' ' \ |
231 | | while read index link file_url_array public file_num comment_num description; do | 220 | | while read index link file_url_array public file_num comment_num author description; do |
232 | local blob_code=$(echo $file_url_array | tr ',' '\n' | sed -E 's#.*raw/(.*)/.*#\1#' | sort | cut -c -7 | paste -sd '-') | 221 | local blob_code=$(echo $file_url_array | tr ',' '\n' | sed -E 's#.*raw/(.*)/.*#\1#' | sort | cut -c -7 | paste -sd '-') |
233 | [[ $public == 'False' ]] && local mark=p | 222 | [[ $public == 'False' ]] && local mark=p |
234 | [[ -n $1 ]] && local index=$1 | 223 | [[ -n $1 ]] && local index=$1 |
235 | echo $mark$index $link $blob_code $file_num $comment_num $description | tr -d '"' | 224 | echo $mark$index $link $blob_code $file_num $comment_num $author $description | tr -d '"' |
236 | done | 225 | done |
237 | } | 226 | } |
238 | 227 | ||
228 | # TODO add author, files and date of a gist | ||
239 | # get latest list of gists from Github API | 229 | # get latest list of gists from Github API |
240 | _update() { | 230 | _update() { |
241 | echo "fetching $user's gists from $GITHUB_API..." | 231 | echo "fetching $user's gists from $GITHUB_API..." |
242 | echo | 232 | echo |
243 | local route="users/$user/gists" | 233 | local route="users/$user/gists" |
244 | local mark="" | 234 | local mark="" |
245 | local filter='/^[^s]/ d' | 235 | local filter='/^[^s]/ d; /^$/ d' |
246 | if [[ "$1" =~ ^(star|s)$ ]];then | 236 | if [[ "$1" =~ ^(star|s)$ ]];then |
247 | route="gists/starred" | 237 | route="gists/starred" |
248 | mark="s" | 238 | mark="s" |
249 | filter='/^[s]/ d' | 239 | filter='/^[s]/ d; /^$/ d' |
250 | fi | 240 | fi |
251 | 241 | ||
252 | local response=$(httpGet $GITHUB_API/$route) | 242 | result=$(http_method GET $GITHUB_API/$route | _parse_response) |
253 | false && echo Failed to update gists && return 1 | 243 | [[ -z $result ]] && echo Failed to update gists && return 1 |
254 | sed -i "$filter" $INDEX | 244 | |
255 | echo $response | _parse_response >> $INDEX | 245 | sed -i "$filter" $INDEX && echo "$result" >> $INDEX |
256 | _show_list $mark | 246 | _show_list $mark |
257 | 247 | ||
258 | if [[ $auto_sync != "false" ]]; then (_sync_repos $1 > /dev/null 2>&1 &); fi | 248 | if [[ $auto_sync != "false" ]]; then (_sync_repos $1 > /dev/null 2>&1 &); fi |
259 | } | 249 | } |
260 | 250 | ||
251 | _query_user() { | ||
252 | local route="users/$1/gists" | ||
253 | result=$(http_method GET $GITHUB_API/$route | _parse_response) | ||
254 | [[ -z $result ]] && echo Failed to update gists && return 1 | ||
255 | |||
256 | echo "$result" \ | ||
257 | | while read index link blob_code file_num extra description; do | ||
258 | echo $link $file_num $extra $(echo $description | cut -c -70 ) | ||
259 | done | ||
260 | } | ||
261 | |||
261 | # update local git repos | 262 | # update local git repos |
262 | _sync_repos() { | 263 | _sync_repos() { |
263 | # clone repos which are not in the local | 264 | # clone repos which are not in the local |
@@ -312,7 +313,7 @@ _goto_gist() { | |||
312 | _delete_gist() { | 313 | _delete_gist() { |
313 | for i in "$@"; do | 314 | for i in "$@"; do |
314 | _gist_id "$i" | 315 | _gist_id "$i" |
315 | curl -X DELETE -s -H "$AUTH_HEADER" $GITHUB_API/gists/$GIST_ID \ | 316 | http_method DELETE $GITHUB_API/gists/$GIST_ID \ |
316 | && echo "$i" deleted \ | 317 | && echo "$i" deleted \ |
317 | && sed -i -E "/^$i / d" $INDEX | 318 | && sed -i -E "/^$i / d" $INDEX |
318 | done | 319 | done |
@@ -330,6 +331,7 @@ _clean_repos() { | |||
330 | # parse JSON from gist detail | 331 | # parse JSON from gist detail |
331 | _handle_gist() { | 332 | _handle_gist() { |
332 | echo ' | 333 | echo ' |
334 | raw = json.load(sys.stdin) | ||
333 | print("site:", raw["html_url"]) | 335 | print("site:", raw["html_url"]) |
334 | print("description:", raw["description"]) | 336 | print("description:", raw["description"]) |
335 | print("public:", raw["public"]) | 337 | print("public:", raw["public"]) |
@@ -345,6 +347,7 @@ for file in raw["files"].keys(): | |||
345 | # equal to jq '.[] | {user: .user.login, created_at: .created_at, updated_at: .updated_at, body: .body}' | 347 | # equal to jq '.[] | {user: .user.login, created_at: .created_at, updated_at: .updated_at, body: .body}' |
346 | _handle_comment() { | 348 | _handle_comment() { |
347 | echo ' | 349 | echo ' |
350 | raw = json.load(sys.stdin); | ||
348 | for comment in raw: | 351 | for comment in raw: |
349 | print() | 352 | print() |
350 | print("|", "user:", comment["user"]["login"]) | 353 | print("|", "user:", comment["user"]["login"]) |
@@ -357,16 +360,16 @@ for comment in raw: | |||
357 | # TODO format with simple text | 360 | # TODO format with simple text |
358 | _show_detail() { | 361 | _show_detail() { |
359 | _gist_id $1 | 362 | _gist_id $1 |
360 | httpGet $GITHUB_API/gists/$GIST_ID \ | 363 | http_method GET $GITHUB_API/gists/$GIST_ID \ |
361 | | AccessJsonElement "$(_handle_gist)" | 364 | | AccessJsonElement "$(_handle_gist)" |
362 | 365 | ||
363 | httpGet $GITHUB_API/gists/$GIST_ID/comments \ | 366 | http_method GET $GITHUB_API/gists/$GIST_ID/comments \ |
364 | | AccessJsonElement "$(_handle_comment)" | 367 | | AccessJsonElement "$(_handle_comment)" |
365 | } | 368 | } |
366 | 369 | ||
367 | # set filename/description/permission for a new gist | 370 | # set filename/description/permission for a new gist |
368 | _set_gist() { | 371 | _set_gist() { |
369 | public=true | 372 | public=True |
370 | while [[ -n "$@" ]]; do case $1 in | 373 | while [[ -n "$@" ]]; do case $1 in |
371 | -d | --desc) | 374 | -d | --desc) |
372 | description="$2" | 375 | description="$2" |
@@ -375,7 +378,7 @@ _set_gist() { | |||
375 | filename="$2" | 378 | filename="$2" |
376 | shift; shift;; | 379 | shift; shift;; |
377 | -p) | 380 | -p) |
378 | public=false | 381 | public=False |
379 | shift;; | 382 | shift;; |
380 | *) | 383 | *) |
381 | files="$1 $files" | 384 | files="$1 $files" |
@@ -388,7 +391,7 @@ _set_gist() { | |||
388 | # Let user type the content of gist before setting filename | 391 | # Let user type the content of gist before setting filename |
389 | _new_file() { | 392 | _new_file() { |
390 | [[ -t 0 ]] && echo "Type a gist. <Ctrl-C> to cancel, <Ctrl-D> when done" > /dev/tty | 393 | [[ -t 0 ]] && echo "Type a gist. <Ctrl-C> to cancel, <Ctrl-D> when done" > /dev/tty |
391 | local tmp_file=$(mktemp) | 394 | tmp_file=$(mktemp) |
392 | cat > $tmp_file | 395 | cat > $tmp_file |
393 | echo -e '\n' > /dev/tty | 396 | echo -e '\n' > /dev/tty |
394 | [[ -z "$1" ]] && read -p 'Type file name: ' filename < /dev/tty | 397 | [[ -z "$1" ]] && read -p 'Type file name: ' filename < /dev/tty |
@@ -396,22 +399,33 @@ _new_file() { | |||
396 | echo /tmp/$filename | 399 | echo /tmp/$filename |
397 | } | 400 | } |
398 | 401 | ||
402 | _gist_body(){ | ||
403 | echo " | ||
404 | import os.path | ||
405 | files_json = {} | ||
406 | files = sys.stdin.readline().split() | ||
407 | description = sys.stdin.readline().replace('\n','') | ||
408 | for file in files: | ||
409 | with open(file, 'r') as f: | ||
410 | files_json[os.path.basename(file)] = {'content': f.read()} | ||
411 | print(json.dumps({'public': $public, 'files': files_json, 'description': description})) | ||
412 | " | ||
413 | } | ||
414 | |||
399 | # create a new gist with files | 415 | # create a new gist with files |
400 | _create_gist() { | 416 | _create_gist() { |
401 | _set_gist "$@" || return 1 | 417 | _set_gist "$@" || return 1 |
402 | [[ -z "$files" ]] && files=$(_new_file $filename) | 418 | [[ -z "$files" ]] && files=$(_new_file $filename) |
403 | [[ -z "$description" ]] && read -p 'Type description: ' description < /dev/tty | 419 | [[ -z "$description" ]] && read -p 'Type description: ' description < /dev/tty |
404 | 420 | ||
405 | local index=$(( $(sed '/^s/ d' $INDEX | wc -l) +1 )) | ||
406 | echo 'Creating a new gist...' | 421 | echo 'Creating a new gist...' |
407 | for file in $files; do | 422 | http_data=$(mktemp) |
408 | echo "\"$(basename $file)\": {\"content\": \"$(sed '$ !s/$/\\n/' $file)\"}," | 423 | |
409 | done | tr -d '\n' | sed 's/^/{/; s/,$/}/' \ | 424 | echo -e "$files\n$description" \ |
410 | | echo "{ \"public\": $public, \"files\": $(cat -), \"description\": \"$description\"}" \ | 425 | | AccessJsonElement "$(_gist_body)" > $http_data \ |
411 | | curl -s -H "$AUTH_HEADER" --data @- $GITHUB_API/gists \ | 426 | && http_method POST $GITHUB_API/gists \ |
412 | | tee jojo \ | ||
413 | | sed '1 s/^/[/; $ s/$/]/' \ | 427 | | sed '1 s/^/[/; $ s/$/]/' \ |
414 | | _parse_response $index >> $INDEX | 428 | | _parse_response $(( $(sed '/^s/ d' $INDEX | wc -l) +1 )) >> $INDEX |
415 | 429 | ||
416 | if [[ $? -eq 0 ]]; then | 430 | if [[ $? -eq 0 ]]; then |
417 | echo 'Gist is created' | 431 | echo 'Gist is created' |
@@ -428,12 +442,12 @@ _edit_gist() { | |||
428 | echo -n 'Type new description: ' | 442 | echo -n 'Type new description: ' |
429 | read DESC < /dev/tty | 443 | read DESC < /dev/tty |
430 | echo "{ \"description\": \"$DESC\" }" \ | 444 | echo "{ \"description\": \"$DESC\" }" \ |
431 | | curl -X PATCH -H "$AUTH_HEADER" --data @- $GITHUB_API/gists/$GIST_ID > /dev/null \ | 445 | | http_method PATCH $GITHUB_API/gists/$GIST_ID > /dev/null \ |
432 | && _update | 446 | && _update |
433 | } | 447 | } |
434 | 448 | ||
435 | usage() { | 449 | usage() { |
436 | sed -E -n ' /^$/ q; 8,$ s/^#//p' $0 | 450 | sed -E -n ' /^$/ q; 7,$ s/^#//p' $0 |
437 | } | 451 | } |
438 | 452 | ||
439 | getConfiguredClient | 453 | getConfiguredClient |
@@ -462,6 +476,9 @@ case "$1" in | |||
462 | config | c) | 476 | config | c) |
463 | shift | 477 | shift |
464 | _configure "$@" ;; | 478 | _configure "$@" ;; |
479 | user | U) | ||
480 | shift | ||
481 | _query_user "$@" ;; | ||
465 | help | h) | 482 | help | h) |
466 | usage ;; | 483 | usage ;; |
467 | *) | 484 | *) |