Update Friday, 17th of April, Anno Domini MMIX, at the hour of the Monkey
[git/dscho.git] / upload.sh
blob86ee6f2fb1ac93682d723babe3c42ff9eae5557d
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 echo "Please set gitweb.url in the Git config first!" >&2
54 exit 1
57 get_config () {
58 value=$(git config blog.$1)
59 test -z "$value" && value=$2
60 echo $value
63 BACKGROUNDIMG=$(get_config background paper.jpg)
64 TITLE=$(get_config title "Dscho's Git log")
65 MAXENTRIES=$(get_config maxPostsPerPage 10)
66 BRANCH=$(get_config branch blog)
67 DESC=$(get_config description "A few stories told by Dscho")
69 URLPREFIX="$(dirname "$GITWEBURL")"/
70 REMOTEREPOSITORY="$(basename "$GITWEBURL")"
71 case "$GITWEBURL" in
72 *'?'*) BLOBPLAIN="$REMOTEREPOSITORY;a=blob_plain";;
73 */) URLPREFIX=$GITWEBURL; BLOBPLAIN="a=blob_plain";;
74 *) BLOBPLAIN="$REMOTEREPOSITORY?a=blob_plain";;
75 esac
76 URL="$BLOBPLAIN;hb=$BRANCH;f="
77 ORIGURL=$URL
78 NEW=new
79 OUTPUT=index.html
80 RSS=blog.rss
81 TEST=test.html
82 THIS=$0
84 LC_ALL=C
85 export LC_ALL
87 move_new_entry_back () {
88 test -f source-$now.txt &&
89 mv source-$now.txt $NEW &&
90 git rm --cached -f source-$now.txt
93 die () {
94 move_new_entry_back
95 echo "$*" >&2
96 exit 1
99 strip_prefix () {
100 echo "${1#$2}"
103 chomp () {
104 strip_prefix "${1%$3}" "$2"
107 nth () {
108 # add illogical suffix
109 case "$1" in
110 *1?|*[04-9]) echo "$1th";;
111 *1) echo "$1st";;
112 *2) echo "$1nd";;
113 *3) echo "$1rd";;
114 esac
117 make_chinese_hour () {
118 case $1 in
119 23|00) echo Rat;;
120 01|02) echo Buffalo;;
121 03|04) echo Tiger;;
122 05|06) echo Rabbit;;
123 07|08) echo Dragon;;
124 09|10) echo Snake;;
125 11|12) echo Horse;;
126 13|14) echo Goat;;
127 15|16) echo Monkey;;
128 17|18) echo Rooster;;
129 19|20) echo Dog;;
130 21|22) echo Pig;;
131 esac
134 digit_to_roman () {
135 case $1 in
136 1) echo $2;;
137 2) echo $2$2;;
138 3) echo $2$2$2;;
139 4) echo $2$3;;
140 5) echo $3;;
141 6) echo $3$2;;
142 7) echo $3$2$2;;
143 8) echo $3$2$2$2;;
144 9) echo $2$4;;
145 esac
148 make_roman_number () {
149 case $1 in
150 '') ;;
151 ?) digit_to_roman $1 I V X;;
152 ??) echo $(digit_to_roman ${1%?} X L C)$(make_roman_number ${1#?});;
153 ???) echo $(digit_to_roman ${1%??} C D M)$(make_roman_number ${1#?});;
154 ????) echo $(digit_to_roman ${1%???} M)$(make_roman_number ${1#?});;
155 esac
158 make_date () {
159 printf "%s, %s of %s, Anno Domini %s, at the hour of the %s\n" \
160 $(date +%A -d @$1) \
161 $(nth $(date +%e -d @$1)) \
162 $(date +%B -d @$1) \
163 $(make_roman_number $(date +%Y -d @$1)) \
164 $(make_chinese_hour $(date +%H -d @$1))
167 # make an argument for sed, to replace $1..$1 by <$2>..</$2>
168 markup_substitution () {
169 # sed does not know minimal match with .*, only greedy one
170 middle=
171 middleend=
172 delim=$1
173 right_no=3
174 while test ! -z "$delim"
176 right_no=$(($right_no+1))
177 test $right_no -gt 9 &&
178 die "Invalid markup pattern: $1"
179 first=$(expr "$delim" : '\(.\)')
180 delim=${delim#$first}
181 test -z "$delim" && {
182 middle="$middle\\([^$first]\\|$first[A-Za-z0-9]\\)"
183 break
185 middle="$middle\\([^$first]\\|$first"
186 middleend="$middleend\\)"
187 done
189 left="\\(^\\|[^A-Za-z0-9]\\)$1\\($middle$middleend*\\)"
190 right="$1\\($\\|[^A-Za-z0-9]\\)"
191 # work around stupid dash interpreting backslashes when expanding vars
192 bs="\\\\"
193 test "\\" = "$(echo "$bs")" || bs="\\"
194 echo "s/$left$right/${bs}1<$2>${bs}2<\/$2>$bs$right_no/g"
197 space80=' '
198 space80="$space80$space80$space80 "
199 # transform markup in stdin to HTML
200 markup () {
201 case "$*" in
202 *invert-bash*) bash_bg=white; bash_fg=black;;
203 *) bash_bg=black; bash_fg=white;;
204 esac
205 sed -e 's!^$!</p><p>!' \
206 -e "$(markup_substitution "''" i)" \
207 -e "$(markup_substitution "_" u)" \
208 -e 's!IMHO!in my humble opinion!g' \
209 -e 's!BTW!By the way,!g' \
210 -e 's!repo.or.cz!<a href=http://&>&</a>!g' \
211 -e 's!:-)!\&#x263a;!g' \
212 -e "s!\\[\\[\(Image\|SVG\):.*!$THIS handle &!e" \
213 -e 's!<bash>!<table\
214 border=1 bgcolor='$bash_bg'>\
215 <tr><td bgcolor=lightblue colspan=3>\
216 <pre>'"$space80"'</pre>\
217 </td></tr>\
218 <tr><td>\
219 <table cellspacing=5 border=0\
220 style="color:'$bash_fg';">\
221 <tr><td>\
222 <pre>!' \
223 -e 's!</bash>!</pre>\
224 </td></tr>\
225 </table>\
226 </td></tr>\
227 </table>!' \
231 # output lines containing <timestamp> <filename> <title>
232 get_blog_entries () {
233 for file in $(ls -r source-*.txt)
235 timestamp=$(chomp $file source- .txt)
236 title="$(sed 1q < $file | markup)"
237 echo "$timestamp $file $title"
238 done
241 get_last_removed_entry () {
242 git log --pretty=format: --name-only --diff-filter=D HEAD |
243 while read line
245 case "$line" in
246 source-*.txt) file=$line;;
247 '') test -z "$file" || {
248 echo "$file"
249 break
251 esac
252 done
255 box_count=0
256 begin_box () {
257 test $box_count = 0 || echo "<br>"
258 echo "<table width=$toc_width bgcolor=#e0e0e0 border=1>"
259 echo "<tr><th>$1</th></tr>"
260 echo "<tr><td>"
263 end_box () {
264 echo "</td></tr></table>"
265 box_count=$(($box_count+1))
268 # make HTML page
269 make_html () {
270 body_style="width:800px"
271 body_style="$body_style;background-image:url($URL$BACKGROUNDIMG)"
272 body_style="$body_style;background-repeat:repeat-y"
273 body_style="$body_style;background-attachment:scroll"
274 body_style="$body_style;padding:0px;"
275 text_style="width:610px"
276 text_style="$text_style;margin-left:120px"
277 text_style="$text_style;margin-top:50px"
278 text_style="$text_style;align:left"
279 text_style="$text_style;vertical-align:top;"
280 cat << EOF
281 <html>
282 <head>
283 <title>$TITLE</title>
284 <meta http-equiv="Content-Type"
285 content="text/html; charset=UTF-8"/>
286 </head>
287 <body style="$body_style">
288 <div style="$text_style">
289 <h1>$TITLE</h1>
291 indent='\t\t\t'
293 # make toc
294 toc_width=400px
295 toc_style="position:absolute;top:50px;left:810px;width=$toc_width"
297 echo "<div style=\"$toc_style\">"
298 begin_box "Table of contents:"
299 echo '<p><ul>'
300 get_blog_entries |
301 while read timestamp filename title
303 date="$(date +"%d %b %Y" -d @$timestamp)"
304 echo "<li><a href=#$timestamp>$date $title</a>"
305 done
306 echo '</ul></p>'
307 file=
308 last_removed_entry=$(get_last_removed_entry)
309 test -z "$last_removed_entry" || {
310 commit=$(git log --pretty=format:%H --diff-filter=AM \
311 -- $last_removed_entry |
312 head -n 1)
313 previous="$BLOBPLAIN;hb=$commit"
314 echo "<a href=$previous;f=index.html>Older posts</a>"
316 end_box
318 # RSS feed
319 rss_style="background-color:orange;text-decoration:none"
320 rss_style="$rss_style;color:white;font-family:sans-serif;"
321 echo '<br>'
322 echo '<div style="text-align:right;">'
323 echo "<a href=\"$ORIGURL$RSS\""
324 echo ' title="Subscribe to my RSS feed"'
325 echo ' class="rss" rel="nofollow"'
326 echo " style=\"$rss_style\">RSS</a>"
327 echo '</div>'
329 # About
330 test -f about.html && {
331 begin_box "About this blog:"
332 cat about.html
333 end_box
336 # Links
337 test -f links.html && {
338 begin_box "Links:"
339 cat links.html
340 end_box
343 # Google AdSense
344 test -z "$DRYRUN" && test -f google.adsense && {
345 begin_box "Google Ads:"
346 cat google.adsense
347 end_box
350 echo '</div>'
351 } | sed -s "s/^/$indent/"
354 # timestamps will not need padding to sort correctly, for some time...
355 get_blog_entries |
356 while read timestamp filename title
358 echo "<h6>$(make_date $timestamp)</h6>"
359 echo "<a name=$timestamp>"
360 echo "<h2>$title</h2>"
361 echo ""
362 echo "<p>"
363 sed 1d < $filename | markup
364 echo "</p>"
365 done |
366 sed -e "s/^./$indent&/" \
367 -e "/<pre>/,/<\/pre>/s/^$indent//"
369 cat << EOF
370 </div>
371 </body>
372 </html>
376 generate_rss () {
377 echo '<?xml version="1.0" encoding="utf-8"?>'
378 echo '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">'
379 echo '<channel>'
380 echo "<title>$TITLE</title>"
381 echo "<link>$URLPREFIX${URL}index.html</link>"
382 self="$URLPREFIX$ORIGURL$RSS"
383 selfattribs='rel="self" type="application/rss+xml"'
384 echo "<atom:link href=\"$self\" $selfattribs/>"
385 echo "<description>$DESC</description>"
386 echo "<lastBuildDate>$(date --rfc-2822)</lastBuildDate>"
387 echo '<language>en-us</language>'
389 get_blog_entries |
390 while read timestamp filename title
392 # remove all tags
393 title=$(echo "$title" | sed 's/<[^>]*>//g')
394 echo '<item>'
395 echo "<title>$title</title>"
396 echo "<link>$URLPREFIX${URL}index.html#$timestamp</link>"
397 echo "<guid>$URLPREFIX${URL}index.html#$timestamp</guid>"
398 echo "<pubDate>$(date --rfc-2822 -d @$timestamp)</pubDate>"
399 description="$(cat < $filename | markup invert-bash)"
400 echo "<description><![CDATA[$description]]></description>"
401 echo "</item>"
402 done
404 echo '</channel>'
405 echo '</rss>'
408 get_image_files () {
409 git ls-files |
410 grep -v '\.\(rss\|html\|gitignore\|in\|sh\|txt\|adsense\)$'
413 remove_old_entries () {
414 count=$(ls source-*.txt | wc -l)
415 test $MAXENTRIES -ge $count && return
417 for file in source-*.txt
419 test $MAXENTRIES -lt $count || break
420 git rm $file > /dev/null || return 1
421 count=$(($count-1))
422 echo $file
423 done
425 # remove no-longer referenced images
426 image_files=$(get_image_files)
427 referenced_files="$(cat source-*.txt |
428 tr ']|' '\n' |
429 sed -ne 's/\[\[\(Image\|SVG\)://p') $BACKGROUNDIMG"
430 for file in $(echo $image_files $referenced_files $referenced_files |
431 tr ' ' '\n' | sort | uniq -u)
433 git rm $file > /dev/null || return 1
434 echo $file
435 done
438 # never, ever have spaces in the file names
439 commit_new_images () {
440 files="$(remove_old_entries) $RSS $BACKGROUNDIMG" ||
441 die "Could not remove old entries"
444 generate_rss > $RSS &&
445 git add $RSS ||
446 die "Could not generate $RSS"
448 for image in $(cat source-* |
449 tr ' ]|' '\n' |
450 sed -n 's/.*\[\[\(Image\|SVG\)://p' |
451 sort |
452 uniq)
454 git add $image || die "Could not git add image $image"
455 files="$files $image"
456 done
458 git update-index --refresh &&
459 git diff-files --quiet -- $files &&
460 git diff --cached --quiet HEAD -- $files ||
461 git commit -s -m "Housekeeping on $(make_date $now)" $files
464 get_image_url () {
465 test ! -z "$DRYRUN" && echo "$1" && return
466 rev=$(git rev-list -1 HEAD -- $1)
467 test -z "$rev" && die "No revision found for $1"
468 echo "$BLOBPLAIN;hb=$rev;f=$1"
471 handle_svg_file () {
472 # for some reason, Firefox adds scrollbars, so nudge the width a bit
473 width=$(sed -ne 's/.* width="\([^"]*\).*/\1/p' -e '/<metadata/q' < "$1")
474 test -z "$width" || width=" width=$(($width+5))"
475 url=$(get_image_url "$1")
476 cat << EOF
477 <center>
478 <table border=0>
479 <tr>
480 <td align=center>
481 <embed type="image/svg+xml"
482 src="$url"$width />
483 </td>
484 </tr>
485 <tr>
486 <td align=center>
487 <a href=$url>$1</a>
488 </td>
489 </tr>
490 </table>
491 </center>
495 handle_image_file () {
496 echo "<center><img src=$(get_image_url "${1%% *}") ${1#* }></center>"
501 # parse command line option
502 case "$1" in
503 *dry*) DRYRUN=1; export DRYRUN; shift;;
504 *show*) firefox "$(pwd)"/$TEST; exit;;
505 *remote*) firefox $URLPREFIX$URL$OUTPUT; exit;;
506 handle)
507 shift
508 case "$1" in
509 "[[SVG:"*) handle_svg_file "$(chomp "$*" '\[\[SVG:' '\]\]')";;
510 "[[Image:"*) handle_image_file "$(chomp "$*" '\[\[Image:' '\]\]')";;
511 esac
512 exit
514 '') ;;
515 *) die "Unknown command: $1";;
516 esac
518 test "$#" = 0 ||
519 die "Usage: $0 [--dry-run]"
521 # make sure we're on the correct branch
522 test refs/heads/$BRANCH = $(git symbolic-ref HEAD) ||
523 die "Not on branch $BRANCH"
525 # make sure there are no uncommitted changes
526 git update-index --refresh &&
527 git diff-files --quiet ||
528 die "Have unstaged changes!"
530 # rename the new blog entry if it exists
531 now=$(date +%s)
532 test ! -f $NEW || {
533 mv -i $NEW source-$now.txt &&
534 git add source-$now.txt
535 } ||
536 die "Could not rename source.txt"
538 # commit the images that are referenced and not yet committed
539 test ! -z "$DRYRUN" ||
540 commit_new_images ||
541 die "Could not commit new images"
543 # to find the images reliably, we have to use the commit name, not the branch
544 # we use the latest commit touching an image file.
545 IMAGEFILES="$(get_image_files)"
546 REV=$(git rev-list -1 HEAD -- $IMAGEFILES)
547 test -z "$REV" && REV=$BRANCH
548 URL="$BLOBPLAIN;hb=$REV;f="
550 if test ! -z "$DRYRUN"
551 then
552 # Output to test.html and have local links into the current directory
553 OUTPUT=$TEST
554 URL=
557 make_html > $OUTPUT || die "Could not write $OUTPUT"
559 test ! -z "$DRYRUN" && {
560 move_new_entry_back
561 exit
564 git add $OUTPUT &&
565 git commit -s -m "Update $(make_date $now)" &&
566 git push origin +$BRANCH