2 # Simple wrapper around svn featuring auto-ChangeLog entries and emailing.
4 # Copyright (C) 2006 Benoit Sigoure.
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
21 # Quick install: alias svn=path/to/svn-wrapper.sh -- that's all.
27 # This script is a wrapper around the svn command-line client for UNIX. It has
28 # been designed mainly to automagically generate GNU-style ChangeLog entries
29 # when committing and mail them along with a diff and an optional comment from
30 # the author to a list of persons or a mailing list. It has been made so that
31 # it's as much portable as possible and covers as many use-case as possible.
33 # HOWEVER, there will be bugs, there will be cases in which the script doesn't
34 # wrap properly the svn-cli, etc. In this case, you can try to mail me at
35 # <tsuna at lrde dot epita dot fr>. Include the revision of the svn-wrapper
36 # you're using, and full description of what's wrong etc. so I can reproduce
39 # If you feel like, you can try to fix/enhance the script yourself. It only
40 # requires some basic Shell-scripting skills. Knowing sed might help :)
46 # If you're simply looking for the usage, run svn-wrapper.sh help (or
47 # svn help if you aliased `svn' on svn-wrapper.sh) as usual.
49 # This script is (hopefully) portable, widely commented and self-contained. Do
50 # not hesitate to hack it. It might look rather long (because it does a lot of
51 # things :P) but you should be able to easily locate which part of the code
54 # The script begins by defining several functions. Then it really starts where
55 # the comment "# `main' starts here. #" is placed.
56 # Some svn commands are hooked (eg, svn st displays colors). Hooks and
57 # extra commands are defined in functions named svn_<command-name>.
63 # * Customizable behavior/colors via some ~/.<config>rc file (?)
64 # * Improve Auto-Emailing (if possible in sh :|)
68 # Default values (the user can export them to override them).
78 # No args, invoke svn...
79 test $# -lt 1 && exec $SVN
83 # The `main' really starts after the functions definitions.
91 red
='\e[0;31m'; lred
='\e[1;31m'
92 green
='\e[0;32m'; lgreen
='\e[1;32m'
93 yellow
='\e[0;33m'; lyellow
='\e[1;33m'
94 blue
='\e[0;34m'; lblue
='\e[1;34m'
95 purple
='\e[0;35m'; lpurple
='\e[1;35m'
96 cyan
='\e[0;36m'; lcyan
='\e[1;36m'
97 grey
='\e[0;37m'; lgrey
='\e[1;37m'
98 white
='\e[0;38m'; lwhite
='\e[1;38m'
106 yellow
=''; lyellow
=''
108 purple
=''; lpurple
=''
118 echo "svn-wrapper: abort: $@" |
sed '1!s/^/ /' >&2
125 echo "svn-wrapper: warning: $@" |
sed '1!s/^/ /' >&2
131 echo "svn-wrapper: notice: $@" |
sed '1!s/^/ /' >&2
138 read answer ||
return 1
143 return 42 # should never happen...
149 echo "svn-wrapper: warning: cannot find the environment variable $1
150 You might consider using \`export $1 proper-value\`" >&2
153 # get_unique_file_name file-name
154 get_unique_file_name
()
157 echo "$1" && return 0
160 while test -f "$gufn.$i"; do
166 # ensure_not_empty description value
169 ene_val
=`echo "$2" | tr -d ' \t\n'`
170 test x
"$ene_val" = x
&& abort
"$1: empty value"
176 my_url
='http://www.lrde.epita.fr/~sigoure/svn-wrapper/'
177 # You can use https if you feel paranoiac.
179 echo ">>> Fetching svn-wrapper.sh from $my_url/svn-wrapper.sh"
180 tmp_me
=`get_unique_file_name "$TMPDIR/svn-wrapper.sh"`
181 if wget
--help >/dev
/null
2>/dev
/null
; then
182 wget
--no-check-certificate "$my_url/svn-wrapper.sh" -O "$tmp_me"
185 curl
--help >/dev
/null
2>/dev
/null
187 if [ $?
-gt 42 ]; then
188 abort
'Cannot find wget or curl.
189 How can I download any update without them?'
191 curl
--insecure "$my_url/svn-wrapper.sh" >"$tmp_me"
194 || abort
"Can't find the copy of myself I downloaded in $tmp_me"
196 tmp_ver
=`sed '/^# $Id$/!d;
198 s/ \([0-9][0-9]*\)/\1/' "$tmp_me"`
199 my_ver
=`sed '/^# $Id$/!d;
201 s/ \([0-9][0-9]*\)/\1/' "$0"`
202 if [ $my_ver -lt $tmp_ver ]; then
203 yesno
"An update is available, r$tmp_ver (your version is r$my_ver)
204 Do you want to install it?" ||
return 1
205 if yesno
"Do you want to see the ChangeLog between r$my_ver and r$tmp_ver?"
207 my_chlog
=`get_unique_file_name "$TMPDIR/ChangeLog"`
209 wget
) wget
--no-check-certificate "$my_url/ChangeLog" -o "$my_chlog"
211 curl
) curl
--insecure "$my_url/ChangeLog" >"$my_chlog"
213 *) abort
'Should never be here.'
216 sed "/^r$my_ver/q" "$my_chlog" |
$PAGER
218 if yesno
"Do you want to see the diff between r$my_ver and r$tmp_ver?"
220 (diff -u "$0" "$tmp_me" | diffstat
;
222 diff -u "$0" "$tmp_me") |
$PAGER
224 if yesno
"Overwrite $0 (r$my_ver) with $tmp_me (r$tmp_ver)?"; then
225 mv "$tmp_me" "$0" && exit 0
229 elif [ $my_ver -gt $tmp_ver ]; then
230 echo "Wow, you're more up to date than the master copy :)"
231 echo "Your version is r$my_ver and the master copy is r$tmp_ver."
233 echo "You're already up to date :)"
238 # ------------------------------- #
239 # Hooks for standard SVN commands #
240 # ------------------------------- #
242 # svn_commit [args...]
249 here
=`pwd`; found
=0; change_log_files
=''
250 while [ $found -eq 0 ]; do
251 if [ -f .
/ChangeLog
]; then
256 change_log_dir
=`pwd -P`
257 # Stop searching when in / ... hmz :P
258 test x
"$change_log_dir" = x
/ && {
259 if yesno
'svn-wrapper: Error: Cannot find a ChangeLog file!
260 You might want to create an empty one (eg: `touch ChangeLog` where appropriate)
261 Do you want to proceed without using a ChangeLog?'; then
263 $SVN commit
"$@" && $SVN update
270 echo "svn-wrapper: using $change_log_dir/ChangeLog"
272 $SVN update || abort
'svn update failed.'
274 # Warn for files that are not added in the repos.
275 conflicts
=`$SVN status | sed '/^\?/!d'`
276 test x
"$conflicts" != x
&& warn
"make sure you don't want to \`svn add'
277 any of the following files before committing:" \
278 && echo "$conflicts" |
sed "$sed_svn_st_color"
280 # Detect unresolved conflicts / missing files.
281 conflicts
=`$SVN status | sed '/^[C!]/!d'`
282 test x
"$conflicts" != x
&& abort
"there are unresolved conflicts (\`C')
283 and/or missing files (\`!'):
289 REV
=`$SVN info | sed '/^Revision: /!d;s///'`
290 test x
"$REV" = x
&& REV
=`$SVN info | sed '/^Last Changed Rev: /!d;s///'`
291 test x
"$REV" = x
&& warn
'Cannot detect the current revision.'
294 tmp_log
="$change_log_dir/svn-log"
295 if [ -f "$tmp_log" ] && yesno
"It looks like the last commit did not\
296 terminate successfully.
297 Would you like to resume it?"; then
299 internal_tags
=`sed '/^--- Internal stuff, DO NOT change please ---$/,$!d' \
301 saved_args
=`echo "$internal_tags" | sed '/^args: */!d;s///'`
302 if [ x
"$saved_args" != x
]; then
303 if [ x
"$@" != x
] && [ x
"$saved_args" != x
"$@" ]; then
304 warn
"overriding arguments:
305 you invoked $0 with the following arguments: $@
306 they have been replaced by these: $saved_args"
307 set dummy
"$saved_args"
310 notice
"setting the following arguments: $saved_args"
311 set dummy
"$saved_args"
315 svn_diff
=`$SVN diff "$@"`
316 svn_diff_stat
=`echo "$svn_diff" | diffstat`
318 else # Build the template message.
320 # ------------------------------------ #
321 # Gather info for the template message #
322 # ------------------------------------ #
324 repos_root
=`$SVN info | sed '/^Repository Root/!d;s/^Repository Root: //'`
325 # It looks like svn <1.3 didn't display a "Repository Root" entry.
326 test x
"$repos_root" = x
&& \
327 repos_root
=`$SVN info | sed '/^URL/!d;s/^URL: //'`
330 projname
=`$SVN propget project "$change_log_dir"`
331 # Try to be VCS-compatible and find a project name in a *.rb.
332 if [ x
"$projname" = x
] && [ -d "$change_log_dir/vcs" ]; then
333 projname
=`sed '/common_commit/!d;s/.*"\(.*\)<%= rev.*/\1/' \
334 "$change_log_dir"/vcs/*.rb`
335 test x
"$projname" != x \
336 && notice
"VCS-compat: found project name: $projname
337 in " "$change_log_dir"/vcs
/*.rb
339 test x
"$projname" != x
&& projname
=`echo "$projname" | sed '/[^ ]$/s/$/ /'`
341 mailto
=`$SVN propget mailto "$change_log_dir"`
342 if [ x
"$mailto" = x
]; then
343 warn
"no svn property mailto found in $change_log_dir
344 You might want to set default email adresses using:
345 svn propset mailto 'somebody@mail.com, foobar@example.com'\
347 # Try to be VCS-compatible and find a list of mails in a *.rb.
348 if [ -d "$change_log_dir/vcs" ]; then
349 mailto
=`grep '.@.*\..*' "$change_log_dir"/vcs/*.rb \
351 | sed 's/^.*[["]\([^["]*@[^]"]*\)[]"].*$/\1/' | xargs`
352 test x
"$mailto" != x
&& notice
"VCS-compat: found mailto: $mailto
353 in " "$change_log_dir"/vcs
/*.rb
355 fi # end guess mailto
357 # Ensure that emails are comma-separated.
358 mailto
=`echo "$mailto" | sed 's/[ ;]/,/g' | tr -s ',' | sed 's/,/, /g'`
359 test x
"$FULLNAME" = x
&& FULLNAME
='Type Your Name Here' \
361 test x
"$EMAIL" = x
&& EMAIL
='your.mail.here@FIXME.com' && warn_env EMAIL
363 # --ignore-externals appeared after svn 1.1.1
364 my_svn_st
=`$SVN status --ignore-externals "$@" \
365 || $SVN status "$@" | sed '/^Performing status on external/ {
370 # Files to put in the ChangeLog entry.
371 change_log_files
=`echo "$my_svn_st" | sed '
372 s/^M......\(.*\)$/ * \1: ./; t
373 s/^A......\(.*\)$/ * \1: New./; t
374 s/^D......\(.*\)$/ * \1: Remove./; t
377 if [ x
"$change_log_files" = x
]; then
378 yesno
'Nothing to commit, continue anyway?' ||
return 1
381 svn_diff
=`$SVN diff "$@"`
382 svn_diff_stat
=`echo "$svn_diff" | diffstat`
384 # Get any older svn-log out of the way.
385 test -f "$tmp_log" && mv "$tmp_log" `get_unique_file_name "$tmp_log"`
386 # If we can't get an older svn-log out of the way, find a new name...
387 test -f "$tmp_log" && tmp_log
=`get_unique_file_name "$tmp_log"`
389 --You must fill this file correctly to continue-- -*- vcs -*-
391 Subject: ${projname}r<REV>: <TITLE>
392 From: $FULLNAME <$EMAIL>
396 Repository: $repos_root
400 <YYYY>-<MM>-<DD> $FULLNAME <$EMAIL>
406 --This line, and those below, will be ignored--
409 - Fill the ChangeLog entry.
410 - If you feel like, write a comment in the 'Comment:' section.
411 This comment will only appear in the email, not in the ChangeLog.
412 By default only the location of the repository is in the comment.
413 - Some tags will be replaced. Tags are of the form: <TAG>. Unknown
414 tags will be left unchanged.
415 - Your ChangeLog entry will be used as commit message for svn.
417 --Preview of the message that will be sent--
419 Repository: $repos_root
420 Your comments (if any) will appear here.
423 $YYYY-$MM-$DD $FULLNAME <$EMAIL>
425 Your ChangeLog entry will appear here.
429 $svn_diff" >"$tmp_log"
432 --- Internal stuff, DO NOT change please ---
433 args: $@" >>"$tmp_log"
435 fi # end: if svn-log; then resume? else create template
438 # ------------------ #
439 # Re-"parse" the log #
440 # ------------------ #
442 # hmz this section is a bit messy...
443 sed_escape
='s/@/\\@/' # helper string... !@#$%* escaping \\\\\\...
444 sed_eval_tags
="s/<MM>/$MM/g; s/<DD>/$DD/g; s/<YYYY>/$YYYY/g; s/<REV>/$REV/g"
445 full_log
=`sed '/^--This line, and those below, will be ignored--$/,$d;
446 /^--You must fill this/d' "$tmp_log"`
447 chlog_entry
=`echo "$full_log" | sed '/^ChangeLog:$/,$!d; /^ChangeLog:$/d'`
448 ensure_not_empty
'ChangeLog entry' "$chlog_entry"
449 full_log
=`echo "$full_log" | sed '/^ChangeLog:$/,$d'`
450 mail_comment
=`echo "$full_log" | sed '/^Comment:$/,$!d; /^Comment:$/d'`
451 full_log
=`echo "$full_log" | sed '/^Comment:$/,$d'`
452 mail_title
=`echo "$full_log" | sed '/^Title:/!d;s/^Title: *//'`
453 ensure_not_empty
'commit title' "$mail_title"
454 mail_title
=`echo "$mail_title" | sed "$sed_eval_tags; $sed_escape"`
455 sed_eval_tags
="$sed_eval_tags; s@<TITLE>@$mail_title@g"
456 mail_comment
=`echo "$mail_comment" | sed "$sed_eval_tags"`
457 chlog_entry
=`echo "$chlog_entry" | sed "$sed_eval_tags; 1{
460 mail_subject
=`echo "$full_log" | sed '/^Subject:/!d;s/^Subject: *//'`
461 ensure_not_empty
'mail subject' "$mail_subject"
462 mail_subject
=`echo "$mail_subject" | sed "$sed_eval_tags"`
463 mail_to
=`echo "$full_log" | sed '/^To:/!d;s/^To: *//'`
464 ensure_not_empty
'"To:" field of the mail' "$mail_to"
465 mail_from
=`echo "$full_log" | sed '/^From:/!d;s/^From: *//'`
466 ensure_not_empty
'"From:" field of the mail' "$mail_from"
468 # Check whether the user passed -m | --message
470 while [ $i -lt $# ]; do
472 # This is not really a reliable way of knowing whether -m | --message was
473 # passed but hum... Let's assume it'll do :s
474 test x
"$arg" = 'x-m' && has_message
=1
475 test x
"$arg" = 'x--message' && has_message
=1
477 set dummy
"$@" "$arg"
481 if [ $has_message -eq 0 ]; then
482 my_message
=`echo "$chlog_entry" | grep -v "^$YYYY-$MM-$DD" | sed '1,2 {
485 set dummy
--message "$my_message" "$@"
488 notice
'you are overriding the commit message.'
492 yesno
'Are you sure you want to commit?' ||
return 1
494 # Add the ChangeLog entry
495 old_chlog
=`get_unique_file_name "$change_log_dir/ChangeLog.old"`
496 mv "$change_log_dir/ChangeLog" "$old_chlog" || \
497 abort
'Could not backup ChangeLog'
498 trap "echo SIGINT; mv \"$old_chlog\" \"$change_log_dir/ChangeLog\"" SIGINT
499 echo "$chlog_entry" >"$change_log_dir/ChangeLog"
500 echo >>"$change_log_dir/ChangeLog"
501 cat "$old_chlog" >>"$change_log_dir/ChangeLog"
503 # --Commit-- finally! :D
504 $SVN commit
"$@" ||
{
505 mv "$old_chlog" "$change_log_dir/ChangeLog"
506 abort
"Commit failed, $SVN returned $?"
509 # In the end, perform an svn up to update externals
510 $SVN update
"$change_log_dir"
514 mail_file
=`get_unique_file_name "$change_log_dir/mail"`
518 Subject: $mail_subject
527 $svn_diff" |
sed 's/^\.$/ ./' >"$mail_file"
528 # We change lines with only a `.' because they could mean "end-of-mail"
530 test -f ~
/.signature
&& echo '-- ' >>"$mail_file" && \
531 cat ~
/.signature
>>"$mail_file"
532 yesno
"Sign the mail using $GPG ?" && \
533 $GPG --clearsign "$mail_file"
534 test -f "$mail_file.asc" && \
535 mv "$mail_file.asc" "$mail_file"
536 # FIXME: This looks a bit fragile. What if $mail_to = -s foo ? Will it
537 # override the previous -s option?
538 cat "$mail_file" |
mail -s "$mail_subject" "$mail_to"
541 return $svn_commit_rv
547 echo "Using svn-wrapper v$version (C) SIGOURE Benoit [GPL]"
548 sed '/^# $Id[:].*$/!d;s/.*$Id[:] *//;s/ *$ *//;s/ \([0-9][0-9]*\)/ (r\1)/' "$0"
551 # has_prop prop-name [path]
552 # return value: 0 -> path has the property prop-name set.
553 # 1 -> path has no property prop-name.
557 hp_plist
=`$SVN proplist "$2"`
558 test $?
-ne 0 && return 2
559 hp_res
=`echo "$hp_plist" | sed "/^ *$1\$/!d"`
560 test x
"$hp_res" = x
&& return 1
564 # svn_propadd prop-name prop-val [path]
568 && abort
'Not enough arguments provided; try `svn help propadd` for more info'
570 && abort
'Too many arguments provided; try `svn help propadd` for more info'
573 test x
"$path" = x
&& path
='.' && set dummy
"$@" '.' && shift
574 has_prop
"$1" "$3" ||
{
575 test $?
-eq 2 && return 1 # svn error
577 yesno
"'$path' has no property named '$1', do you want to add it?" \
582 current_prop_val
=`$SVN propget "$1" "$3"`
583 test $?
-ne 0 && abort
"Failed to get the current value of property '$1'."
585 $SVN propset
"$1" "$current_prop_val
586 $2" "$3" >/dev
/null || abort
"Failed to add '$3' in the property '$1'."
588 current_prop_val
=`$SVN propget "$1" "$3" || echo "$current_prop_val
590 echo "property '$1' updated on '$path', new value:
594 # svn_propsed prop-name sed-script [path]
598 && abort
'Not enough arguments provided; try `svn help propsed` for more info'
600 && abort
'Too many arguments provided; try `svn help propsed` for more info'
603 test x
"$path" = x
&& path
='.'
604 has_prop
"$1" "$3" ||
{
605 test $?
-eq 2 && return 1 # svn error
607 abort
"'$path' has no property named '$1'."
610 prop_val
=`$SVN propget "$1" "$3"`
611 test $?
-ne 0 && abort
"Failed to get the current value of property '$1'."
613 prop_val
=`echo "$prop_val" | sed "$2"`
614 test $?
-ne 0 && abort
"Failed to run the sed script '$2'."
616 $SVN propset
"$1" "$prop_val" "$3" >/dev
/null \
617 || abort
"Failed to update the property '$1' with value '$prop_val'."
619 new_prop_val
=`$SVN propget "$1" "$3" || echo "$prop_val"`
620 echo "property '$1' updated on '$path', new value:
627 if [ $# -eq 0 ]; then # Simply display ignore-list.
628 $SVN propget
'svn:ignore'
629 elif [ $# -eq 1 ]; then
630 if [ -d "$1" ]; then # Display ignore-list for $1
631 $SVN propget
'svn:ignore' "$1"
632 else # Add $1 in ignore-list of `.'.
633 svn_propadd
'svn:ignore' "$1"
635 else # Add arguments in svn:ignore.
637 while [ $i -lt $# ]; do
640 if [ $i -eq $# ]; then
643 set dummy
"$@" "$arg"
648 i_files
=`echo "$*" | tr -s ' ' '\n'`
649 if [ -d "$last_arg" ]; then # Add in ignore-list of $last_arg
650 svn_propadd
'svn:ignore' "$i_files" "$last_arg"
651 else # Add in ignore-list of `.'
652 svn_propadd
'svn:ignore' "$i_files
661 if [ $# -eq 0 ]; then
666 Additionnal commands provided by svn-wrapper:
679 echo 'diffstat (ds): Display the histogram from svn diff-output.'
680 $SVN help diff |
sed '1d;
681 s/differences*/histogram/;
682 2,35 s/diff/diffstat/g'
685 echo 'ignore: Add some files in the svn:ignore property.
686 usage: 1. ignore [PATH]
687 2. ignore FILE [FILES...] [PATH]
689 1. Display the value of svn:ignore property on [PATH].
690 2. Add some files in the svn:ignore property of [PATH].
692 If you want to add directories in the ignore-list, be careful:
694 will add "foo/" in the property svn:ignore within the directory bar!
696 svn ignore foo/ bar/ .
697 (It'\''s somewhat like with mv)
703 echo 'propadd (padd, pa): Add something in the value of a property.
704 usage: propadd PROPNAME PROPVAL PATH
706 PROPVAL will be appended at the end of the property PROPNAME.
712 echo 'propsed (psed): Edit a property with sed.
713 usage: propsed PROPNAME SED-ARGS PATH
715 eg: svn propsed svn:externals "s/http/https/" .
721 echo 'revision (rev): Display the revision number of a local or remote item.'
722 $SVN help info |
sed '1d;
723 s/information/revision/g;
724 s/revision about/the revision of/g;
725 2,35 s/info/revision/g;
729 echo 'touch: Touch a file and svn add it.
730 usage: touch FILE [FILES]...
735 selfupdate | self-update
)
736 echo 'selfupdate: Attempt to update svn-wrapper.sh
743 echo 'version: Display the version info of svn and svn-wrapper.
754 # svn_status [args...]
757 $SVN status
"$@" |
sed "$sed_svn_st_color"
761 # ------------------- #
762 # `main' starts here. #
763 # ------------------- #
766 if echo | diffstat
>/dev
/null
2>/dev
/null
; then :; else
767 warn
'diffstat is not installed on your system or not in your PATH.'
768 test -f /etc
/debian_version \
769 && notice
'you might want to `apt-get install diffstat`.'
771 if echo |
mail >/dev
/null
2>/dev
/null
; then :; else
772 if [ $?
-gt 100 ]; then
773 warn
'mail is not installed on your system or not in your PATH.'
774 test -f /etc
/debian_version \
775 && notice
'you might want to:
776 # apt-get install mailx
777 # dpkg-reconfigure exim'
781 # Define colors if stdout is a tty.
784 else # stdout isn't a tty => don't print colors.
788 # Considere this as a sed function :P.
790 s/^\(.\)C/\1${lred}C${std}/
791 s/^A/${lgreen}A${std}/; t
792 s/^C/${lred}C${std}/; t
793 s/^D/${lyellow}D${std}/; t
794 s/^I/${purple}I${std}/; t
795 s/^M/${lgreen}M${std}/; t
796 s/^R/${lblue}R${std}/; t
797 s/^X/${lblue}X${std}/; t
798 s/^?/${lred}?${std}/; t
799 s/^!/${lred}!${std}/; t
800 s/^~/${lwhite}~${std}/; t"
803 test "x$1" = x--debug
&& shift && set -x
806 # ------------------------------- #
807 # Hooks for standard SVN commands #
808 # ------------------------------- #
821 # -------------------- #
822 # Custom SVN commands #
823 # -------------------- #
826 $SVN diff "$@" | diffstat
842 $SVN info
"$@" |
sed '/^Revision/!d;s/^Revision: //'
846 touch "$@" && svn add
"$@"
848 selfupdate | self-update
)
852 version |
-version |
--version)
854 set dummy
'--version' "$@"