Housekeeping on Tuesday, 9th of November, Anno Domini MMX, at the hour of the Dog
[git/dscho.git] / upload.sh
blob04ace8e9bde67f5723eb0d2f57c254470cde1f14
1 #!/bin/sh
3 # This is a simple script that will produce my blog on repo.or.cz
5 # The idea is to have source-<timestamp>.txt files as input, having the
6 # stories, and this script turning them into nice HTML, committing
7 # everything, and then pushing it to my repository.
9 # The blog will then be served using gitweb.
11 # To make it easier on me, if a file "new" exists, it is automatically
12 # renamed using the current timestamp.
14 # How to use:
16 # $ mkdir my-blog
17 # $ cd my-blog
18 # $ git init
20 # Then symlink or copy this file (upload.sh); you can track it or add it
21 # to .gitignore, does not matter.
23 # Add a remote "origin" (you might want to track only the appropriate branch
24 # if the repository contains other branches, too), add a background image,
25 # and then set the config variables gitweb.url, blog.title, blog.background
26 # and blog.branch appropriately.
28 # Example:
30 # $ git remote add -t blog repo.or.cz:/srv/git/git/dscho.git/
31 # $ git symbolic-ref HEAD refs/heads/blog
32 # $ cp ~/images/background.jpg ./
33 # $ git config gitweb.url http://repo.or.cz/w/git/dscho.git
34 # $ git config blog.title "Dscho's blog"
35 # $ git config blog.description "A few stories told by Dscho"
36 # $ git config blog.background background.jpg
37 # $ git config blog.branch blog
39 # Now you can start writing posts, by creating a file called "new", and
40 # calling ./upload.sh to commit the post together with the images and
41 # push all.
43 # Note that no file names may contain spaces.
45 # TODO: document the "syntax" of the source-*.txt files
48 # make sure we're in the correct working directory
49 cd "$(dirname "$0")"
51 GITWEBURL="$(git config gitweb.url)"
52 test -z "$GITWEBURL" && {
53 if test -f .gitconfig
54 then
55 git config -l -f .gitconfig |
56 while read line
58 key=${line%%=*}
59 value=${line#*=}
60 test -z "$(git config "$key")" &&
61 git config "$key" "$value"
62 done
63 else
64 echo "Please set gitweb.url in the Git config first!" >&2
65 exit 1
69 get_config () {
70 value=$(git config blog.$1)
71 test -z "$value" && value=$2
72 echo $value
75 BACKGROUNDIMG=$(get_config background paper.jpg)
76 TITLE=$(get_config title "Dscho's Git log")
77 MAXENTRIES=$(get_config maxPostsPerPage 10)
78 BRANCH=$(get_config branch blog)
79 DESC=$(get_config description "A few stories told by Dscho")
81 URLPREFIX="$(dirname "$GITWEBURL")"/
82 REMOTEREPOSITORY="$(basename "$GITWEBURL")"
83 case "$GITWEBURL" in
84 *'?'*) BLOBPLAIN="$REMOTEREPOSITORY;a=blob_plain";;
85 */) URLPREFIX=$GITWEBURL; BLOBPLAIN="a=blob_plain";;
86 *) BLOBPLAIN="$REMOTEREPOSITORY?a=blob_plain";;
87 esac
88 URL="$BLOBPLAIN;hb=$BRANCH;f="
89 ORIGURL=$URL
90 NEW=new
91 OUTPUT=index.html
92 RSS=blog.rss
93 TEST=test.html
94 THIS=$0
96 LC_ALL=C
97 export LC_ALL
99 move_new_entry_back () {
100 test -f source-$now.txt &&
101 mv source-$now.txt $NEW &&
102 git rm --cached -f source-$now.txt
105 die () {
106 move_new_entry_back
107 echo "$*" >&2
108 exit 1
111 strip_prefix () {
112 echo "${1#$2}"
115 chomp () {
116 strip_prefix "${1%$3}" "$2"
119 nth () {
120 # add illogical suffix
121 case "$1" in
122 *1?|*[04-9]) echo "$1th";;
123 *1) echo "$1st";;
124 *2) echo "$1nd";;
125 *3) echo "$1rd";;
126 esac
129 make_chinese_hour () {
130 case $1 in
131 23|00) echo Rat;;
132 01|02) echo Buffalo;;
133 03|04) echo Tiger;;
134 05|06) echo Rabbit;;
135 07|08) echo Dragon;;
136 09|10) echo Snake;;
137 11|12) echo Horse;;
138 13|14) echo Goat;;
139 15|16) echo Monkey;;
140 17|18) echo Rooster;;
141 19|20) echo Dog;;
142 21|22) echo Pig;;
143 esac
146 digit_to_roman () {
147 case $1 in
148 1) echo $2;;
149 2) echo $2$2;;
150 3) echo $2$2$2;;
151 4) echo $2$3;;
152 5) echo $3;;
153 6) echo $3$2;;
154 7) echo $3$2$2;;
155 8) echo $3$2$2$2;;
156 9) echo $2$4;;
157 esac
160 make_roman_number () {
161 case $1 in
162 '') ;;
163 ?) digit_to_roman $1 I V X;;
164 ??) echo $(digit_to_roman ${1%?} X L C)$(make_roman_number ${1#?});;
165 ???) echo $(digit_to_roman ${1%??} C D M)$(make_roman_number ${1#?});;
166 ????) echo $(digit_to_roman ${1%???} M)$(make_roman_number ${1#?});;
167 esac
170 make_date () {
171 printf "%s, %s of %s, Anno Domini %s, at the hour of the %s\n" \
172 $(date +%A -d @$1) \
173 $(nth $(date +%e -d @$1)) \
174 $(date +%B -d @$1) \
175 $(make_roman_number $(date +%Y -d @$1)) \
176 $(make_chinese_hour $(date +%H -d @$1))
179 # make an argument for sed, to replace $1..$1 by <$2>..</$2>
180 markup_substitution () {
181 # sed does not know minimal match with .*, only greedy one
182 middle=
183 middleend=
184 delim=$1
185 right_no=3
186 while test ! -z "$delim"
188 right_no=$(($right_no+1))
189 test $right_no -gt 9 &&
190 die "Invalid markup pattern: $1"
191 first=$(expr "$delim" : '\(.\)')
192 delim=${delim#$first}
193 test -z "$delim" && {
194 middle="$middle\\([^$first]\\|$first[A-Za-z0-9]\\)"
195 break
197 middle="$middle\\([^$first]\\|$first"
198 middleend="$middleend\\)"
199 done
201 left="\\(^\\|[^A-Za-z0-9]\\)$1\\($middle$middleend*\\)"
202 right="$1\\($\\|[^A-Za-z0-9]\\)"
203 # work around stupid dash interpreting backslashes when expanding vars
204 bs="\\\\"
205 test "\\" = "$(echo "$bs")" || bs="\\"
206 echo "s/$left$right/${bs}1<$2>${bs}2<\/$2>$bs$right_no/g"
209 space80=' '
210 space80="$space80$space80 "
211 # transform markup in stdin to HTML
212 markup () {
213 case "$*" in
214 *invert-bash*) bash_bg=white; bash_fg=black;;
215 *) bash_bg=black; bash_fg=white;;
216 esac
217 sed -e 's!^$!</p><p>!' \
218 -e "$(markup_substitution "''" i)" \
219 -e "$(markup_substitution "_" u)" \
220 -e 's!IMHO!in my humble opinion!g' \
221 -e 's!BTW!By the way,!g' \
222 -e 's!\([^/]\)repo.or.cz!\1<a href=http://&>&</a>!g' \
223 -e 's!:-)!\&#x263a;!g' \
224 -e "s!\\[\\[\(Image\|SVG\):.*!$THIS handle &!e" \
225 -e 's!<bash>!<table\
226 border=1 bgcolor='$bash_bg'>\
227 <tr><td bgcolor=lightblue colspan=3>\
228 <pre>'"$space80"'</pre>\
229 </td></tr>\
230 <tr><td>\
231 <table cellspacing=5 border=0\
232 style="color:'$bash_fg';">\
233 <tr><td>\
234 <pre>!' \
235 -e 's!</bash>!</pre>\
236 </td></tr>\
237 </table>\
238 </td></tr>\
239 </table>!' \
243 # output lines containing <timestamp> <filename> <title>
244 get_blog_entries () {
245 for file in $(ls -r source-*.txt)
247 timestamp=$(chomp $file source- .txt)
248 title="$(sed 1q < $file | markup)"
249 echo "$timestamp $file $title"
250 done
253 get_last_removed_entry () {
254 git log --pretty=format: --name-only --diff-filter=D HEAD |
255 while read line
257 case "$line" in
258 source-*.txt) file=$line;;
259 '') test -z "$file" || {
260 echo "$file"
261 break
263 esac
264 done
267 box_count=0
268 begin_box () {
269 test $box_count = 0 || echo "<br>"
270 echo "<table width=$toc_width bgcolor=#e0e0e0 border=0>"
271 echo "<tr><th>$1</th></tr>"
272 echo "<tr><td>"
275 end_box () {
276 echo "</td></tr></table>"
277 box_count=$(($box_count+1))
280 # make HTML page
281 make_html () {
282 body_style="width:800px"
283 body_style="$body_style;background-image:url($URL$BACKGROUNDIMG)"
284 body_style="$body_style;background-repeat:repeat-y"
285 body_style="$body_style;background-attachment:scroll"
286 body_style="$body_style;padding:0px;"
287 text_style="width:610px"
288 text_style="$text_style;margin-left:120px"
289 text_style="$text_style;margin-top:50px"
290 text_style="$text_style;align:left"
291 text_style="$text_style;vertical-align:top;"
292 text_style="$text_style;font-family: Arial, sans-serif;"
293 cat << EOF
294 <html>
295 <head>
296 <title>$TITLE</title>
297 <meta http-equiv="Content-Type"
298 content="text/html; charset=UTF-8"/>
299 </head>
300 <body style="$body_style">
301 <div style="$text_style">
302 <h1>$TITLE</h1>
304 indent='\t\t\t'
306 # make toc
307 toc_width=400px
308 toc_style="position:absolute;top:50px;left:810px;width=$toc_width"
310 echo "<div style=\"$toc_style\">"
311 begin_box "Table of contents:"
312 echo '<p><ul>'
313 get_blog_entries |
314 while read timestamp filename title
316 date="$(date +"%d %b %Y" -d @$timestamp)"
317 echo "<li><a href=#$timestamp>$date $title</a>"
318 done
319 echo '</ul></p>'
320 file=
321 last_removed_entry=$(get_last_removed_entry)
322 test -z "$last_removed_entry" || {
323 commit=$(git log --pretty=format:%H --diff-filter=AM \
324 -- $last_removed_entry |
325 head -n 1)
326 previous="$BLOBPLAIN;hb=$commit"
327 echo "<a href=$previous;f=index.html>Older posts</a>"
329 end_box
331 # RSS feed
332 rss_style="background-color:orange;text-decoration:none"
333 rss_style="$rss_style;color:white;font-family:sans-serif;"
334 echo '<br>'
335 echo '<div style="text-align:right;">'
336 echo "<a href=\"$ORIGURL$RSS\""
337 echo ' title="Subscribe to my RSS feed"'
338 echo ' class="rss" rel="nofollow"'
339 echo " style=\"$rss_style\">RSS</a>"
340 echo '</div>'
342 # About
343 test -f about.html && {
344 begin_box "About this blog:"
345 cat about.html
346 end_box
349 # Links
350 test -f links.html && {
351 begin_box "Links:"
352 cat links.html
353 end_box
356 # Google AdSense
357 test -z "$DRYRUN" && test -f google.adsense && {
358 begin_box "Google Ads:"
359 cat google.adsense
360 end_box
363 echo '</div>'
364 } | sed -s "s/^/$indent/"
367 # timestamps will not need padding to sort correctly, for some time...
368 get_blog_entries |
369 while read timestamp filename title
371 echo "<h6>$(make_date $timestamp)</h6>"
372 echo "<a name=$timestamp>"
373 echo "<h2>$title</h2>"
374 echo ""
375 echo "<p>"
376 sed 1d < $filename | markup
377 echo "</p>"
378 done |
379 sed -e "s/^./$indent&/" \
380 -e "/<pre>/,/<\/pre>/s/^$indent//"
382 cat << EOF
383 </div>
384 </body>
385 </html>
389 generate_rss () {
390 echo '<?xml version="1.0" encoding="utf-8"?>'
391 echo '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">'
392 echo '<channel>'
393 echo "<title>$TITLE</title>"
394 echo "<link>$URLPREFIX${URL}index.html</link>"
395 self="$URLPREFIX$ORIGURL$RSS"
396 selfattribs='rel="self" type="application/rss+xml"'
397 echo "<atom:link href=\"$self\" $selfattribs/>"
398 echo "<description>$DESC</description>"
399 echo "<lastBuildDate>$(date --rfc-2822)</lastBuildDate>"
400 echo '<language>en-us</language>'
402 get_blog_entries |
403 while read timestamp filename title
405 # remove all tags
406 title=$(echo "$title" | sed 's/<[^>]*>//g')
407 echo '<item>'
408 echo "<title>$title</title>"
409 echo "<link>$URLPREFIX${URL}index.html#$timestamp</link>"
410 echo "<guid>$URLPREFIX${URL}index.html#$timestamp</guid>"
411 echo "<pubDate>$(date --rfc-2822 -d @$timestamp)</pubDate>"
412 description="$(cat < $filename | markup invert-bash)"
413 echo "<description><![CDATA[$description]]></description>"
414 echo "</item>"
415 done
417 echo '</channel>'
418 echo '</rss>'
421 get_image_files () {
422 git ls-files |
423 grep -v '\.\(rss\|html\|gitignore\|in\|sh\|txt\|adsense\)$'
426 remove_old_entries () {
427 count=$(ls source-*.txt | wc -l)
428 test $MAXENTRIES -ge $count && return
430 for file in source-*.txt
432 test $MAXENTRIES -lt $count || break
433 git rm $file > /dev/null || return 1
434 count=$(($count-1))
435 echo $file
436 done
438 # remove no-longer referenced images
439 image_files=$(get_image_files)
440 referenced_files="$(cat source-*.txt |
441 tr ']|' '\n' |
442 sed -ne 's/\[\[\(Image\|SVG\)://p') $BACKGROUNDIMG"
443 for file in $(echo $image_files $referenced_files $referenced_files |
444 tr ' ' '\n' | sort | uniq -u)
446 git rm $file > /dev/null || return 1
447 echo $file
448 done
451 # never, ever have spaces in the file names
452 commit_new_images () {
453 files="$(remove_old_entries) $RSS $BACKGROUNDIMG" ||
454 die "Could not remove old entries"
457 generate_rss > $RSS &&
458 git add $RSS ||
459 die "Could not generate $RSS"
461 for image in $(cat source-* |
462 tr ' ]|' '\n' |
463 sed -n 's/.*\[\[\(Image\|SVG\)://p' |
464 sort |
465 uniq)
467 git add $image || die "Could not git add image $image"
468 files="$files $image"
469 done
471 git update-index --refresh &&
472 git diff-files --quiet -- $files &&
473 git diff --cached --quiet HEAD -- $files ||
474 git commit -s -m "Housekeeping on $(make_date $now)" $files
477 get_image_url () {
478 test ! -z "$DRYRUN" && echo "$1" && return
479 rev=$(git rev-list -1 HEAD -- $1)
480 test -z "$rev" && die "No revision found for $1"
481 echo "$BLOBPLAIN;hb=$rev;f=$1"
484 handle_svg_file () {
485 # for some reason, Firefox adds scrollbars, so nudge the width a bit
486 width=$(sed -ne 's/.* width="\([^"]*\).*/\1/p' -e '/<metadata/q' < "$1")
487 test -z "$width" || width=" width=$(($width+5))"
488 url=$(get_image_url "$1")
489 cat << EOF
490 <center>
491 <table border=0>
492 <tr>
493 <td align=center>
494 <embed type="image/svg+xml"
495 src="$url"$width />
496 </td>
497 </tr>
498 <tr>
499 <td align=center>
500 <a href=$url>$1</a>
501 </td>
502 </tr>
503 </table>
504 </center>
508 handle_image_file () {
509 echo "<center><img src=$(get_image_url "${1%% *}") ${1#* }></center>"
514 # parse command line option
515 case "$1" in
516 *dry*|*preview*) DRYRUN=1; export DRYRUN; shift;;
517 *show*) xdg-open "$(pwd)"/$TEST; exit;;
518 *remote*) xdg-open $URLPREFIX$URL$OUTPUT; exit;;
519 handle)
520 shift
521 case "$1" in
522 "[[SVG:"*) handle_svg_file "$(chomp "$*" '\[\[SVG:' '\]\]')";;
523 "[[Image:"*) handle_image_file "$(chomp "$*" '\[\[Image:' '\]\]')";;
524 esac
525 exit
527 ''|*upload*) ;;
528 *help*)
529 cat << EOF
530 Usage: $0 [command]
532 Commands:
533 dry, preview compile
534 show show locally
535 remote show on the remote
536 upload (default) upload
539 *) die "Unknown command: $1";;
540 esac
542 test "$#" = 0 ||
543 die "Usage: $0 [--dry-run]"
545 # make sure we're on the correct branch
546 test refs/heads/$BRANCH = $(git symbolic-ref HEAD) ||
547 die "Not on branch $BRANCH"
549 # make sure there are no uncommitted changes
550 git update-index --refresh &&
551 git diff-files --quiet ||
552 die "Have unstaged changes!"
554 # rename the new blog entry if it exists
555 now=$(date +%s)
556 test ! -f $NEW || {
557 mv -i $NEW source-$now.txt &&
558 git add source-$now.txt
559 } ||
560 die "Could not rename source.txt"
562 # commit the images that are referenced and not yet committed
563 test ! -z "$DRYRUN" ||
564 commit_new_images ||
565 die "Could not commit new images"
567 # to find the images reliably, we have to use the commit name, not the branch
568 # we use the latest commit touching an image file.
569 IMAGEFILES="$(get_image_files)"
570 REV=$(git rev-list -1 HEAD -- $IMAGEFILES)
571 test -z "$REV" && REV=$BRANCH
572 URL="$BLOBPLAIN;hb=$REV;f="
574 if test ! -z "$DRYRUN"
575 then
576 # Output to test.html and have local links into the current directory
577 OUTPUT=$TEST
578 URL=
581 make_html > $OUTPUT || die "Could not write $OUTPUT"
583 test ! -z "$DRYRUN" && {
584 move_new_entry_back
585 exit
588 git add $OUTPUT &&
589 git commit -s -m "Update $(make_date $now)" &&
590 git push origin +$BRANCH