Handle the <MYSELF> and <MAILTO> special tags.
[svn-wrapper.git] / svn-wrapper.sh
blob09b46f578d17791aafa237bafde5893ca9038bb6
1 #! /bin/sh
2 # Simple wrapper around SVN and Git featuring auto-ChangeLog entries and
3 # emailing.
4 # $Id$
5 # Copyright (C) 2006, 2007 Benoit Sigoure.
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 # Quick install: alias svn=path/to/svn-wrapper.sh -- that's all.
22 # ------ #
23 # README #
24 # ------ #
26 # This script is a wrapper around the SVN and Git command-line clients for
27 # UNIX. It has been designed mainly to automagically generate GNU-style
28 # ChangeLog entries when committing and mail them along with a diff and an
29 # optional comment from the author to a list of persons or a mailing list.
30 # Although this script was originally written for SVN (hence the name), it
31 # also works with Git and tries to detect whether the current repository is a
32 # Git or SVN repository.
33 # This script has been written to be as much portable as possible and cover as
34 # many use-case as reasonably possible.
36 # HOWEVER, there will be bugs, there will be cases in which the script doesn't
37 # wrap properly the svn-cli, etc. In this case, you can try to mail me at
38 # <tsuna at lrde dot epita dot fr>. Include the revision of the svn-wrapper
39 # you're using, and full description of what's wrong etc. so I can reproduce
40 # your problem.
42 # If you feel like, you can try to fix/enhance the script yourself. It only
43 # requires some basic Shell-scripting skills. Knowing sed will prove useful :)
45 # ------------- #
46 # DOCUMENTATION #
47 # ------------- #
49 # If you're simply looking for the usage, run `svn-wrapper.sh help' (or
50 # `svn help' if you aliased `svn' on svn-wrapper.sh) as usual.
52 # This script is (hopefully) portable, widely commented and self-contained. Do
53 # not hesitate to hack it. It might look rather long (because it does a lot of
54 # things :P) but you should be able to easily locate which part of the code
55 # you're looking for.
57 # The script begins by defining several functions. Then it really starts where
58 # the comment "# `main' starts here. #" is placed.
59 # Some svn commands are hooked (eg, `svn st' displays colors). Hooks and
60 # extra commands are defined in functions named `svn_<command-name>'.
62 # ---- #
63 # TODO #
64 # ---- #
66 # * Write a real testsuite. :/
67 # * Automatic proxy configuration depending on the IP in ifconfig?
68 # * Customizable behavior/colors via some ~/.<config>rc file (?)
69 # * Handle things such as svn ci --force foobar.
71 # * svn automerge + automatic fill in branches/README.branches.
72 # => won't do (SVN is too borken WRT merges, I use Git now so I don't feel
73 # like dealing with this sort of SVN issue)
74 # * Automatically recognize svn cp and svn mv instead of writing "New" and
75 # "Remove" in the template ChangeLog entry (hard). Pair the and new/remove
76 # to prepare a good ChangeLog entry (move to X, copy from X) [even harder].
77 # => won't do (Git does this fine, can't be bothered to deal with this for
78 # SVN)
82 # Default values (the user can export them to override them).
83 use_changelog=:
84 git_mode=false
85 case $SVN in
86 git*) git_mode=:;;
87 '')
88 if [ -d .git ] || [ -d ../.git ]; then
89 SVN=git
90 git_mode=:
91 else
92 SVN=svn
95 esac
97 : ${EDITOR=missing}
98 export EDITOR
99 : ${GPG=gpg}
100 : ${TMPDIR=/tmp}
101 export TMPDIR
102 : ${PAGER=missing}
103 export PAGER
104 # Override the locale.
105 LC_ALL='C'
106 export LC_ALL
107 : ${AWK=missing}
108 : ${DIFF=missing}
109 : ${COLORDIFF=missing}
111 # Signal number for traps (using plain signal names is not portable and breaks
112 # on recent Debians that use ash as their default shell).
113 SIGINT=2
115 me=$0
116 bme=`basename "$0"`
118 # Pitfall: some users might be tempted to export SVN=svn-wrapper.sh for some
119 # reason. This is just *wrong*. The following is an attempt to save them from
120 # some troubles.
121 if [ x`basename "$SVN"` = x"$bme" ]; then
122 echo "warning: setting SVN to $bme is wrong"
123 SVN='svn'
126 # This code comes (mostly) from Autoconf.
127 # The user is always right.
128 if test "${PATH_SEPARATOR+set}" != set; then
129 PATH_SEPARATOR=:
130 (PATH='/bin;/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 && {
131 (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 ||
132 PATH_SEPARATOR=';'
136 version='0.4'
137 full_rev='$Id$'
138 # Get the first 6 digits (without forking)
139 revision=${full_rev#'$'Id': '}
140 revision=${revision%??????????????????????????????????' $'}
142 # The `main' really starts after the functions definitions.
144 # ---------------- #
145 # Helper functions #
146 # ---------------- #
148 set_colors()
150 red='\e[0;31m'; lred='\e[1;31m'
151 green='\e[0;32m'; lgreen='\e[1;32m'
152 yellow='\e[0;33m'; lyellow='\e[1;33m'
153 blue='\e[0;34m'; lblue='\e[1;34m'
154 purple='\e[0;35m'; lpurple='\e[1;35m'
155 cyan='\e[0;36m'; lcyan='\e[1;36m'
156 grey='\e[0;37m'; lgrey='\e[1;37m'
157 white='\e[0;38m'; lwhite='\e[1;38m'
158 std='\e[m'
161 set_nocolors()
163 red=; lred=
164 green=; lgreen=
165 yellow=; lyellow=
166 blue=; lblue=
167 purple=; lpurple=
168 cyan=; lcyan=
169 grey=; lgrey=
170 white=; lwhite=
171 std=
174 # abort err-msg
175 abort()
177 echo "svn-wrapper: ${lred}abort${std}: $@" \
178 | sed '1!s/^[ ]*/ /' >&2
179 exit 1
182 # warn msg
183 warn()
185 echo "svn-wrapper: ${lred}warning${std}: $@" \
186 | sed '1!s/^[ ]*/ /' >&2
189 # notice msg
190 notice()
192 echo "svn-wrapper: ${lyellow}notice${std}: $@" \
193 | sed '1!s/^[ ]*/ /' >&2
196 # yesno question
197 yesno()
199 echo -n "$@ [y/N] "
200 read answer || return 1
201 case $answer in
202 y* | Y*) return 0;;
203 *) return 1;;
204 esac
205 return 42 # should never happen...
208 # yesnewproceed what
209 # returns true if `yes' or `proceed', false if `new'.
210 # the answer is stored in $yesnoproceed_res which is /yes|new|proceed/
211 yesnewproceed()
213 echo -n "$@ [(y)es/(p)roceed/(u)p+proceed/(N)ew] "
214 read answer || return 1
215 case $answer in
216 y* | Y*) yesnoproceed_res=yes; return 0;;
217 p* | P*) yesnoproceed_res=proceed; return 0;;
218 u* | U*) yesnoproceed_res=upproceed; return 0;;
219 *) yesnoproceed_res=new; return 1;;
220 esac
221 return 42 # should never happen...
224 # warn_env env-var
225 warn_env()
227 warn "cannot find the environment variable $1
228 You might consider using \`export $1='<FIXME>'\`"
231 # get_unique_file_name file-name
232 get_unique_file_name()
234 test -e "$1" || {
235 echo "$1" && return 0
237 gufn=$1; i=1
238 while test -e "$gufn.$i"; do
239 i=$((i + 1))
240 done
241 echo "$gufn.$i"
244 # ensure_not_empty description value
245 ensure_not_empty()
247 ene_val=`echo "$2" | tr -d ' \t\n'`
248 test x"$ene_val" = x && abort "$1: empty value"
251 # find_prog prog-name
252 # return true if prog-name is in the PATH
253 # echo the full path to prog-name on stdout.
254 # Based on a code from texi2dvi
255 find_prog()
257 save_IFS=$IFS
258 IFS=$PATH_SEPARATOR
259 for dir in $PATH; do
260 IFS=$save_IFS
261 test x"$dir" = x && continue
262 # The basic test for an executable is `test -f $f && test -x $f'.
263 # (`test -x' is not enough, because it can also be true for directories.)
264 # We have to try this both for $1 and $1.exe.
266 # Note: On Cygwin and DJGPP, `test -x' also looks for .exe. On Cygwin,
267 # also `test -f' has this enhancement, bot not on DJGPP. (Both are
268 # design decisions, so there is little chance to make them consistent.)
269 # Thusly, it seems to be difficult to make use of these enhancements.
271 if test -f "$dir/$1" && test -x "$dir/$1"; then
272 echo "$dir/$1"
273 return 0
274 elif test -f "$dir/$1.exe" && test -x "$dir/$1.exe"; then
275 echo "$dir/$1.exe"
276 return 0
278 done
279 return 1
282 # find_progs prog [progs...]
283 # Look in PATH for one of the programs given in argument.
284 # If none of the progs can be found, the string "exit 2" is "returned".
285 find_progs()
287 # This code comes mostly from Autoconf.
288 for fp_prog in "$@"; do
289 fp_res=`find_prog $fp_prog`
290 if [ $? -eq 0 ]; then
291 echo "$fp_res"
292 return 0
294 done
295 echo "exit 2"
296 return 1
299 test x"$EDITOR" = xmissing && EDITOR=`find_progs vim vi emacs nano`
300 test x"$PAGER" = xmissing && PAGER=`find_progs less more`
301 test x"$AWK" = xmissing && AWK=`find_progs gawk mawk nawk awk`
302 test x"$DIFF" = xmissing && DIFF=`find_progs diff`
303 test x"$COLORDIFF" = xmissing && COLORDIFF=`find_progs colordiff diff`
305 # -R will tell less to interpret some terminal codes, which will turn on
306 # colors.
307 case $PAGER in #(
308 less) PAGER='less -R';;
309 esac
311 # require_diffstat
312 # return true if diffstat is in the PATH
313 require_diffstat()
315 if [ x"$require_diffstat_cache" != x ]; then
316 return $require_diffstat_cache
318 if (echo | diffstat) >/dev/null 2>/dev/null; then :; else
319 warn 'diffstat is not installed on your system or not in your PATH.'
320 test -f /etc/debian_version \
321 && notice 'you might want to `apt-get install diffstat`.'
322 require_diffstat_cache=1
323 return 1
325 require_diffstat_cache=0
326 return 0
329 # require_mail
330 # return 0 -> found a mailer
331 # return !0 -> no mailer found
332 # The full path to the program found is echo'ed on stdout.
333 require_mail()
335 save_PATH=$PATH
336 PATH="${PATH}${PATH_SEPARATOR}/sbin${PATH_SEPARATOR}/usr/sbin${PATH_SEPARATOR}/usr/libexec"
337 export PATH
338 find_progs sendEmail sendmail mail
339 rv=$?
340 PATH=$save_PATH
341 export PATH
342 return $rv
345 # my_sendmail mail-file mail-subject mail-to [extra-headers]
346 # mail-to is a comma-separated list of email addresses.
347 # extra-headers is an optionnal argument and will be prepended at the
348 # beginning of the mail headers if the tool used to send mails supports it.
349 # The mail-file may also contain headers. They must be separated from the body
350 # of the mail by a blank line.
351 my_sendmail()
353 test -f "$1" || abort "my_sendmail: Cannot find the mail file: $1"
354 test x"$2" = x && warn 'my_sendmail: Empty subject.'
355 test x"$3" = x && abort 'my_sendmail: No recipient specified.'
357 content_type='Content-type: text/plain'
358 extra_headers="X-Mailer: svn-wrapper v$version (g$revision)
359 Mime-Version: 1.0
360 Content-Transfer-Encoding: 7bit"
361 if test x"$4" != x; then
362 extra_headers="$4
363 $extra_headers"
364 # Remove empty lines.
365 extra_headers=`echo "$extra_headers" | sed '/^[ ]*$/d;s/^[ ]*//'`
368 # If we have a signature, add it.
369 if test -f ~/.signature; then
370 # But don't add it if it is already in the mail.
371 if grep -Fe "`cat ~/.signature`" "$1" >/dev/null; then :; else
372 echo '-- ' >>"$1" && cat ~/.signature >>"$1"
375 # VCS-compat: handle user option 'sign'.
376 if (grep '^sign: false' ~/.vcs) >/dev/null 2>/dev/null; then :; else
377 ($GPG -h) >/dev/null 2>/dev/null
378 gpg_rv=$?
379 if [ -e ~/.gnupg ] || [ -e ~/.gpg ] || [ -e ~/.pgp ] && [ $gpg_rv -lt 42 ]
380 then
381 if grep 'BEGIN PGP SIGNATURE' "$1" >/dev/null; then
382 notice 'message is already GPG-signed'
383 elif yesno "Sign the mail using $GPG ?"; then
384 # Strip the headers
385 sed '1,/^$/d' "$1" >"$1.msg"
386 sed '1,/^$/!d' "$1" >"$1.hdr"
387 # Sign the message, keep only the PGP signature.
388 $GPG --clearsign <"$1.msg" >"$1.tmp" || {
389 rm -f "$1.msg" "$1.hdr" "$1.tmp"
390 abort "\`$GPG' failed (r=$?)"
392 sed '/^--*BEGIN PGP SIGNATURE--*$/,/^--*END PGP SIGNATURE--*$/!d' \
393 "$1.tmp" >"$1.sig"
395 boundary="svn-wrapper-2-$RANDOM"
396 boundary="$boundary$RANDOM"
397 # Prepend some stuff before the PGP signature.
398 echo "
399 --$boundary
400 content-type: application/pgp-signature; x-mac-type=70674453;
401 name=PGP.sig
402 content-description: This is a digitally signed message part
403 content-disposition: inline; filename=PGP.sig
404 content-transfer-encoding: 7bit" | cat - "$1.sig" >"$1.tmp"
405 mv -f "$1.tmp" "$1.sig"
407 # Append some stuff after the PGP signature.
408 echo "
409 --$boundary--" >>"$1.sig"
410 # Re-paste the headers before the signed body and prepend some stuff.
411 echo "This is an OpenPGP/MIME signed message (RFC 2440 and 3156)
412 --$boundary
413 Content-Transfer-Encoding: 8bit
414 Content-Type: text/plain; charset=iso-8859-1; format=flowed
416 | cat "$1.hdr" - "$1.msg" "$1.sig" >"$1.tmp" \
417 && mv -f "$1.tmp" "$1"
418 content_type="Content-Type: multipart/signed;\
419 protocol=\"application/pgp-signature\"; micalg=pgp-sha1;\
420 boundary=\"$boundary\""
421 # Cleanup.
422 rm -f "$1.tmp" "$1.sig" "$1.msg" "$1.hdr"
423 extra_headers="$extra_headers
424 X-Pgp-Agent: `$GPG --version | sed q`"
428 extra_headers="$extra_headers
429 $content_type"
431 mailer=`require_mail`
432 if [ $? -ne 0 ]; then
433 warn 'my_sendmail: No suitable mailer found.'
434 return 1
436 case $mailer in
437 */sendmail)
438 to=`echo "$3" | sed 's/,//g'`
439 echo "$extra_headers" | cat - "$1" | $mailer $to;;
440 */mail)
441 cat "$1" | $mailer -s "$2" "$3";;
442 */sendEmail)
443 if [ x"$SMTP" = x ]; then
444 warn 'my_sendmail: (sendEmail) please tell me the SMTP server to use'
445 echo -n 'STMP server: '
446 read SMTP || abort 'could not read the SMTP server'
447 notice "hint: you can export SMTP=$SMTP if you don't want to be asked"
449 sendEmail -f "$FULLNAME <$EMAIL>" -t "$3" -u "$2" \
450 -s "$SMTP" -o message-file="$1";;
451 *) # wtf
452 abort 'my_sendmail: Internal error.';;
453 esac
456 # selfupdate
457 selfupdate()
459 my_url='http://www.lrde.epita.fr/~sigoure/svn-wrapper'
460 # You can use https if you feel paranoiac.
462 echo ">>> Fetching svn-wrapper.sh from $my_url/svn-wrapper.sh"
464 # --------------------- #
465 # Fetch the new version #
466 # --------------------- #
468 tmp_me=`get_unique_file_name "$TMPDIR/svn-wrapper.sh"`
469 if (wget --help) >/dev/null 2>/dev/null; then
470 wget --no-check-certificate "$my_url/svn-wrapper.sh" -O "$tmp_me"
471 my_wget='wget'
472 else
473 curl --help >/dev/null 2>/dev/null
474 if [ $? -gt 42 ]; then
475 abort 'Cannot find wget or curl.
476 How can I download any update without them?'
478 my_wget='curl'
479 curl --insecure "$my_url/svn-wrapper.sh" >"$tmp_me"
482 test -r $tmp_me \
483 || abort "Cannot find the copy of myself I downloaded in $tmp_me"
485 # ---------------------------------------- #
486 # Compare versions and update if necessary #
487 # ---------------------------------------- #
489 my_ver=$revision
490 tmp_ver=`sed '/^# $Id[:].*$/!d;
491 s/.*$Id[:] *\([a-f0-9]\{6\}\).*/\1/' "$tmp_me"`
492 test x"$tmp_ver" = x && abort "Cannot find the revision of $tmp_me"
493 if [ x"$my_ver" != x"$tmp_ver" ]; then # There IS an update...
494 echo "An update is available, r$tmp_ver (your version is r$my_ver)"
496 # Wanna see the diff?
497 if yesno 'Do you want to see the diff?'
498 then
499 (require_diffstat && diff -uw "$me" "$tmp_me" | diffstat;
500 echo
501 $COLORDIFF -uw "$me" "$tmp_me") | $PAGER
504 # Let's go :)
505 if yesno "Overwrite $me (r$my_ver) with $tmp_me (r$tmp_ver)?"; then
506 chmod a+x "$tmp_me"
507 cp -p "$me" "$me.r$my_ver"
508 mv "$tmp_me" "$me" && exit 0
510 rm -f "$tmp_me"
511 return 1
512 else
513 echo "You're already up to date [r$my_ver] :)"
515 rm -f "$tmp_me"
518 # get_svn_diff_and_diffstat [files to diff]
519 # Helper for svn_commit
520 get_svn_diff_and_diffstat()
522 if $git_mode; then
523 svn_diff=`git diff --ignore-all-space --no-color -B -C --cached`
524 svn_diff_stat=`git diff --stat --ignore-all-space --no-color -B -C --cached`
525 else
526 # Ignore white spaces. Can't svn diff -x -w: svn 1.4 only.
527 svn_diff=`svn_diffw "$@"`
528 test x"$svn_diff" = x && svn_diff=`$SVN diff "$@"`
529 if require_diffstat; then
530 svn_diff_stat=`echo "$svn_diff" | diffstat`
531 else
532 svn_diff_stat='diffstat not available'
537 # Helper. Sets the variables repos_url, git_branch, git_head, repos_root and
538 # extra_repos_info properly.
539 git_get_repos_info_()
541 # FIXME: 1st commit: "fatal: bad default revision 'HEAD'" on stderr
542 repos_url=`git config --get svn-remote.svn.url`
543 test x"$repos_url" = x && repos_url='(git:unknown)'
544 git_branch=`git branch | awk '/^\*/ { print substr($0, 3) }'`
545 if [ x"$git_branch" = x'(no branch)' ]; then
546 yesno 'You are on a detached HEAD, do you really want to continue?' \
547 || return 1
549 git_head=`git rev-list --pretty=format:%h HEAD --max-count=1 | sed '1d;q'`
550 extra_repos_info="Git branch: $git_branch (HEAD: $git_head)"
551 repos_root=$repos_url
554 # Helper. Find the `mailto' property, be it an SVN property or a git-config
555 # option. Relies on the value of $change_log_dir and sets the values of
556 # $mailto (the value of the `mailto' property) and $to (a string to append in
557 # templates that contains the `To:' line, or an empty string if no mail must
558 # be sent).
559 get_mailto_property()
561 test -d "$change_log_dir" || abort 'Internal error in get_mailto_property:
562 $change_log_dir not pointing to a directory'
563 if $git_mode; then
564 mailto=`git config svnw.mailto`
565 if [ x"$mailto" = x ] \
566 && [ -f "$change_log_dir/.git/svn/git-svn/unhandled.log" ]
567 then
568 mailto=`grep mailto "$change_log_dir/.git/svn/git-svn/unhandled.log"`
569 sed_tmp='$!d;s/^.*+dir_prop: . mailto //;s/%40/@/g;s/%2C/,/g;s/%20/ /g;'
570 mailto=`echo "$mailto" | sed "$sed_tmp"`
572 if [ x"$mailto" = x ]; then
573 warn 'No mailto property set for this repository.
574 If this is a git-svn repository, do this in a SVN working copy:
575 svn propset mailto maintainer1@foo.com,maint2@bar.com .
576 If this is a real git repository, do this:
577 git config svnw.mailto maintainer1@foo.com,maint2@bar.com'
579 else
580 mailto=`$SVN propget mailto "$change_log_dir"`
583 if [ x"$mailto" = x ]; then
584 test x$new_user = xyes \
585 && warn "no svn property mailto found in $change_log_dir
586 You might want to set default email adresses using:
587 svn propset mailto 'somebody@mail.com, foobar@example.com'\
588 $change_log_dir" >&2
589 # Try to be VCS-compatible and find a list of mails in a *.rb.
590 if [ -d "$change_log_dir/vcs" ]; then
591 mailto=`grep '.@.*\..*' "$change_log_dir"/vcs/*.rb \
592 | tr '\n' ' ' \
593 | sed 's/^.*[["]\([^["]*@[^]"]*\)[]"].*$/\1/' | xargs`
594 test x"$mailto" != x && test x$new_user = xyes \
595 && notice "VCS-compat: found mailto: $mailto
596 in " "$change_log_dir"/vcs/*.rb
597 fi # end VCS compat
598 fi # end guess mailto
600 # Ensure that emails are comma-separated.
601 mailto=`echo "$mailto" | sed 's/[ ;]/,/g' | tr -s ',' | sed 's/,/, /g'`
604 if [ x"$mailto" != xdont_send_mails ] && [ x"$mailto" != xnothx ]; then
605 to="
606 To: $mailto"
612 # ------------------------------- #
613 # Hooks for standard SVN commands #
614 # ------------------------------- #
616 # svn_commit [args...]
617 # Here is how the commit process goes:
619 # First we look in the arguments passed to commit:
620 # If there are some files or paths, the user wants to commit these only. In
621 # this case, we must search for ChangeLogs from these paths. We might find
622 # more than one ChangeLog, in this case the user will be prompted to pick up
623 # one.
624 # Otherwise (no path passed in the command line) the user just wants to
625 # commit the current working directory.
626 # In any case, we schedule "ChangeLog" for commit.
628 # Alright now that we know which ChangeLog to use, we look in the ChangeLog's
629 # directory if there is a ",svn-log" file which would mean that a previous
630 # commit didn't finish successfully. If there is such a file, the user is
631 # prompted to know whether they want to resume that commit or simply start a
632 # new one.
633 # When the user wants to resume a commit, the ",svn-log" file is loaded and we
634 # retrieve the value of "$@" that was saved in the file.
635 # Otherwise we build a template ChangeLog entry.
636 # Then we open the template ChangeLog entry with $EDITOR so that the user
637 # fills it properly.
638 # Finally, we commit.
639 # Once the commit is sent, we ask the server to know which revision was
640 # commited and we also retrieve the diff. We then send a mail with these.
641 svn_commit()
643 here=`pwd -P`
644 dry_run=false
645 git_commit_all=false
646 use_log_message_from_file=false
647 log_message_to_use=
649 # Check if the user passed some paths to commit explicitly
650 # because in this case we must add the ChangeLog to the commit and search
651 # the ChangeLog from the dirname of that file.
652 i=0; search_from=; add_changelog=false; extra_files=
653 while [ $i -lt $# ]; do
654 arg=$1
655 case $arg in
656 --dry-run)
657 dry_run=:
658 shift
659 i=$((i + 1))
660 continue
662 -a|--all)
663 git_commit_all=:
665 --use-log-file)
666 shift
667 test -z "$1" && abort "$arg needs an argument"
668 test -r "$1" || abort "'$1' does not seem to be readable"
669 test -w "$1" || abort "'$1' does not seem to be writable"
670 test -d "$1" && abort "'$1' seems to be a directory"
671 use_log_message_from_file=:
672 log_message_to_use=$1
673 shift
674 continue
676 esac
677 # If the argument is a valid path: add the ChangeLog in the list of
678 # files to commit
679 if test -e "$arg"; then
680 add_changelog=:
681 if test -d "$arg"; then
682 search_from_add=$arg
683 else
684 search_from_add=`dirname "$arg"`
686 search_from="$search_from:$search_from_add"
688 shift
689 set dummy "$@" "$arg"
690 shift
691 i=$((i + 1))
692 done
693 if $add_changelog; then :; else
694 # There is no path/file in the command line: the user wants to commit the
695 # current directory. Make it explicit now:
696 extra_files=$here
698 search_from=`echo "$search_from" | sed 's/^://; s/^$/./'`
700 # ----------------- #
701 # Find ChangeLog(s) #
702 # ----------------- #
704 nb_chlogs=0; change_log_dirs=
705 save_IFS=$IFS; IFS=':'
706 for dir in $search_from; do
707 IFS=$save_IFS
708 $use_changelog || break
709 test -z "$dir" && dir='.'
710 # First: come back to the original place
711 cd "$here" || abort "Cannot cd to $here"
712 cd "$dir" || continue # Then: Enter $dir (which can be a relative path)
713 found=0
714 while [ $found -eq 0 ]; do
715 this_chlog_dir=`pwd -P`
716 if [ -f ./ChangeLog ]; then
717 found=1
718 nb_chlogs=$((nb_chlogs + 1))
719 change_log_dirs="$change_log_dirs:$this_chlog_dir"
720 else
721 cd ..
723 # Stop searching when in / ... hmz :P
724 test x"$this_chlog_dir" = x/ && break
725 done # end while: did we find a ChangeLog
726 done # end for: find ChangeLogs in $search_from
727 if [ $nb_chlogs -gt 0 ]; then
728 change_log_dirs=`echo "$change_log_dirs" | sed 's/^://' | tr ':' '\n' \
729 | sort -u`
730 nb_chlogs=`echo "$change_log_dirs" | wc -l`
733 # Did we find a ChangeLog? More than one?
734 if [ $nb_chlogs -eq 0 ] && $use_changelog; then
735 if yesno 'svn-wrapper: Error: Cannot find a ChangeLog file!
736 You might want to create an empty one (eg: `touch ChangeLog` where appropriate)
737 Do you want to proceed without using a ChangeLog?'; then
738 cd "$here"
739 $SVN commit "$@"
740 return $?
741 else
742 return 1
744 elif [ $nb_chlogs -gt 1 ]; then
745 notice "$nb_chlogs ChangeLogs were found, pick up one:"
747 IFS=':'; i=0
748 for a_chlog_dir in $change_log_dirs; do
749 i=$((i + 1))
750 echo "$i. $a_chlog_dir/ChangeLog"
751 done
752 echo -n "Which ChangeLog do you want to use? [1-$i] "
753 read chlog_no || abort 'Cannot read answer on stdin.'
755 case $chlog_no in
756 *[^0-9]*) abort "Invalid ChangeLog number: $chlog_no"
757 esac
758 test "$chlog_no" -le $i || abort "Invalid ChangeLog number: $chlog_no
759 max value was: $i"
760 test "$chlog_no" -ge 1 || abort "Invalid ChangeLog number: $chlog_no
761 min value was: 1"
762 change_log_dir=`echo "$change_log_dirs" | tr ':' '\n' | sed "${chlog_no}!d"`
763 else # Only one ChangeLog found
764 if $use_changelog; then
765 change_log_dir=$change_log_dirs
766 notice "using $change_log_dir/ChangeLog"
770 if $use_changelog; then
771 test -f "$change_log_dir/ChangeLog" \
772 || abort "No such file or directory: $change_log_dir/ChangeLog"
773 # Now we can safely schedule the ChangeLog for the commit.
774 extra_files="$extra_files:$change_log_dir/ChangeLog"
775 else
776 change_log_dir='.' # Hack. FIXME: Does this work in all cases?
779 if [ -d "$change_log_dir/.git" ] || $git_mode; then
780 SVN=git
781 git_mode=:
782 git_get_repos_info_
783 else
784 svn_st_tmp=`$SVN status "$change_log_dir"`
786 # Warn for files that are not added in the repos.
787 conflicts=`echo "$svn_st_tmp" | sed '/^ *$/d;
788 /^[^?]/d;
789 /^?.......*\/[,+]/d;
790 /^?......[,+]/d'`
791 if test x"$conflicts" != x; then
792 warn "make sure you don't want to \`svn add'
793 any of the following files before committing:"
794 echo "$conflicts" | sed "$sed_svn_st_color"
795 echo -n 'Type [ENTER] to continue :)' && read chiche_is_gay
798 # If there are changes in an svn:externals, advise the user to commit that
799 # first.
800 changed_externals=`echo "$svn_st_tmp" | $AWK \
801 'function printext()
803 if (ext && !printed)
805 print this_ext "\n";
806 printed = 1;
809 BEGIN { this_ext = ""; ext = 0; ext_modified = 0; }
810 /^Performing status on external/ {
811 ext = 1;
812 sub(/.* at ./, ""); sub(/.$/, ""); this_ext = $0;
813 printed = 0;
815 /^[ADMR]/ { ext_modified = ext; printext(); }
816 /^.[M]/ { ext_modified = ext; printext(); }
817 END { exit ext_modified; }'`
818 if [ $? -ne 0 ]; then
819 warn "the following external items have local modifications:
820 $changed_externals"
821 yesno "You are advised to commit them separately first. Continue anyway?" \
822 || return 1
825 # Detect unresolved conflicts / missing files.
826 conflicts=`echo "$svn_st_tmp" | sed '/^[C!]/!d'`
827 test x"$conflicts" != x && abort "there are unresolved conflicts (\`C')
828 and/or missing files (\`!'):
829 $conflicts"
831 svn_info_tmp=`$SVN info "$change_log_dir"`
832 test $? -ne 0 && abort "Failed to get svn info on $change_log_dir"
833 repos_root=`echo "$svn_info_tmp" | sed '/^Repository Root: /!d;s///'`
834 repos_url=`echo "$svn_info_tmp" | sed '/^URL: /!d;s///'`
835 # It looks like svn <1.3 didn't display a "Repository Root" entry.
836 test x"$repos_root" = x && repos_root=$repos_url
839 cd "$here"
841 YYYY=`date '+%Y'`
842 MM=`date '+%m'`
843 DD=`date '+%d'`
845 # VCS-compat: handle user option 'new_user'
846 new_user='yes'
847 grep '^new_user: false' ~/.vcs >/dev/null 2>/dev/null && new_user='no'
849 edit_changelog=:
850 tmp_log="$change_log_dir/,svn-log"
851 $use_log_message_from_file \
852 && tmp_log=$log_message_to_use \
853 && edit_changelog=false
855 if [ -f "$tmp_log" ] \
856 && { $use_log_message_from_file \
857 || yesnewproceed "It looks like the last commit did not\
858 terminate successfully.
859 Would you like to resume it or proceed immediately?"; }; then
860 case $yesnoproceed_res in
861 *proceed) edit_changelog=false;;
862 esac
863 if test x"$yesnoproceed_res" = xupproceed; then
864 svn_update "$@" || abort 'update failed'
866 echo 'Resuming ...'
867 internal_tags=`sed '/^--- Internal stuff, DO NOT change please ---$/,$!d' \
868 "$tmp_log"`
869 saved_args=`echo "$internal_tags" | sed '/^args: */!d;s///'`
870 extra_files=`echo "$internal_tags" | sed '/^extra_files: */!d;s///'`
871 if [ x"$saved_args" != x ]; then
872 if [ x"$*" != x ] && [ x"$saved_args" != x"$*" ]; then
873 warn "overriding arguments:
874 you invoked $me with the following arguments: $@
875 they have been replaced by these: $saved_args"
876 set dummy $saved_args
877 shift
878 else
879 notice "setting the following arguments: $saved_args"
880 set dummy $saved_args
881 shift
883 elif [ x"$*" != x ]; then
884 warn "overriding arguments:
885 you invoked $me with the following arguments: $@
886 they have been dropped"
889 for i; do
890 case $i in
891 -a|--all)
892 git_commit_all=:
893 if [ $git_mode ]; then
894 (cd $change_log_dir && git add -v -u) || abort '`git add -v -u` failed'
897 esac
898 done
900 get_svn_diff_and_diffstat "$@"
902 # Update the file with the new diff/diffstat in case it changed.
903 $AWK 'BEGIN {
904 tlatbwbi_seen = 0;
905 ycewah_seen = 0;
907 /^--This line, and those below, will be ignored--$/ {
908 tlatbwbi_seen = 1;
910 /^ Your ChangeLog entry will appear here\.$/ {
911 if (tlatbwbi_seen) ycewah_seen = 1;
914 if (ycewah_seen != 2) print;
915 if (ycewah_seen == 1) ycewah_seen = 2;
917 END {
918 if (tlatbwbi_seen == 0)
920 print "--This line, and those below, will be ignored--\n\n" \
921 " Your ChangeLog entry will appear here.";
923 }' "$tmp_log" >"$tmp_log.tmp"
924 echo "
926 $svn_diff_stat
928 $svn_diff
930 $internal_tags" >>"$tmp_log.tmp"
931 mv -f "$tmp_log.tmp" "$tmp_log" || abort "failed to write '$tmp_log'"
933 else # Build the template message.
935 # ------------------------------------ #
936 # Gather info for the template message #
937 # ------------------------------------ #
939 if $git_mode; then
940 projname=`git config svnw.project`
941 if [ x"$projname" = x ] \
942 && [ -f "$change_log_dir/.git/svn/git-svn/unhandled.log" ]
943 then
944 projname=`grep project "$change_log_dir/.git/svn/git-svn/unhandled.log"`
945 sed_tmp='$!d;s/^.*+dir_prop: . project //'
946 projname=`echo "$projname" | sed "$sed_tmp"`
948 if [ x"$projname" = x ]; then
949 warn 'No project name set for this repository.
950 If this is a git-svn repository, do this in a SVN working copy:
951 svn propset project myproj .
952 If this is a real git repository, do this:
953 git config svnw.project myproj'
955 else
956 projname=`$SVN propget project "$change_log_dir"`
958 # Try to be VCS-compatible and find a project name in a *.rb.
959 if [ x"$projname" = x ] && [ -d "$change_log_dir/vcs" ]; then
960 projname=`sed '/common_commit/!d;s/.*"\(.*\)<%= rev.*/\1/' \
961 "$change_log_dir"/vcs/*.rb`
962 test x"$projname" != x && test x$new_user = xyes \
963 && notice "VCS-compat: found project name: $projname
964 in " "$change_log_dir"/vcs/*.rb
966 test x"$projname" != x && projname=`echo "$projname" | sed '/[^ ]$/s/$/ /'`
968 get_mailto_property
970 test x"$FULLNAME" = x && FULLNAME='Type Your Name Here' \
971 && warn_env FULLNAME
972 test x"$EMAIL" = x && EMAIL='your.mail.here@FIXME.com' && warn_env EMAIL
974 if $git_mode; then
975 if $git_commit_all; then
976 (cd $change_log_dir && git add -v -u) || abort '`git add -v -u` failed'
978 my_git_st=`git diff -C --raw --cached`
979 test $? -eq 0 || abort 'git diff failed'
980 # Format: ":<old_mode> <new_mode> <old_sha1> <new_sha1> <status>[
981 # <similarity score>]\t<file-name>"
982 change_log_files=`echo "$my_git_st" | sed '
983 t dummy_sed_1
984 : dummy_sed_1
985 s/^:[0-7 ]* [0-9a-f. ]* M[^ ]* \(.*\)$/ * \1: ./; t
986 s/^:[0-7 ]* [0-9a-f. ]* A[^ ]* \(.*\)$/ * \1: New./; t
987 s/^:[0-7 ]* [0-9a-f. ]* D[^ ]* \(.*\)$/ * \1: Remove./; t
988 s/^:[0-7 ]* [0-9a-f. ]* R[^ ]* \([^ ]*\) \(.*\)$/ * \2: Rename from \1./;t
989 s/^:[0-7 ]* [0-9a-f. ]* C[^ ]* \([^ ]*\) \(.*\)$/ * \2: Copy from \1./;t
990 s/^:[0-7 ]* [0-9a-f. ]* T[^ ]* \(.*\)$/ * \1: ./; t
991 s/^:[0-7 ]* [0-9a-f. ]* X[^ ]* \(.*\)$/ * \1: ???./; t
992 s/^:[0-7 ]* [0-9a-f. ]* U[^ ]* \(.*\)$/ * \1: UNMERGED./; t
994 else
995 # --ignore-externals appeared after svn 1.1.1
996 my_svn_st=`$SVN status --ignore-externals "$@" \
997 || $SVN status "$@" | sed '/^Performing status on external/ {
1001 # Files to put in the ChangeLog entry.
1002 change_log_files=`echo "$my_svn_st" | sed '
1003 t dummy_sed_1
1004 : dummy_sed_1
1005 s/^M......\(.*\)$/ * \1: ./; t
1006 s/^A......\(.*\)$/ * \1: New./; t
1007 s/^D......\(.*\)$/ * \1: Remove./; t
1011 if [ x"$change_log_files" = x ]; then
1012 yesno 'Nothing to commit, continue anyway?' || return 1
1015 change_log_files=`echo "$change_log_files" | sort -u`
1017 get_svn_diff_and_diffstat "$@"
1019 # Get any older svn-log out of the way.
1020 test -f "$tmp_log" && mv "$tmp_log" `get_unique_file_name "$tmp_log"`
1021 # If we can't get an older svn-log out of the way, find a new name...
1022 test -f "$tmp_log" && tmp_log=`get_unique_file_name "$tmp_log"`
1023 if [ x$new_user = no ]; then
1024 commit_instructions='
1025 Instructions:
1026 - Fill the ChangeLog entry.
1027 - If you feel like, write a comment in the "Comment:" section.
1028 This comment will only appear in the email, not in the ChangeLog.
1029 By default only the location of the repository is in the comment.
1030 - Some tags will be replaced. Tags are of the form: <TAG>. Unknown
1031 tags will be left unchanged.
1032 - The tag <REV> may only be used in the Subject.
1033 - Your ChangeLog entry will be used as commit message for svn.'
1034 else
1035 commit_instructions=
1037 r_before_rev=r
1038 $git_mode && r_before_rev=
1039 test x"$extra_repos_info" = x || extra_repos_info="
1040 $extra_repos_info"
1041 echo "\
1042 --You must fill this file correctly to continue-- -*- vcs -*-
1043 Title:
1044 Subject: ${projname}$r_before_rev<REV>: <TITLE>
1045 From: $FULLNAME <$EMAIL>$to
1047 Comment:
1048 URL: $repos_url$extra_repos_info
1050 ChangeLog:
1052 <YYYY>-<MM>-<DD> $FULLNAME <$EMAIL>
1054 <TITLE>
1055 $change_log_files
1057 --This line, and those below, will be ignored--
1058 $commit_instructions
1059 --Preview of the message that will be sent--
1061 URL: $repos_url$extra_repos_info
1062 Your comments (if any) will appear here.
1064 ChangeLog:
1065 $YYYY-$MM-$DD $FULLNAME <$EMAIL>
1067 Your ChangeLog entry will appear here.
1070 $svn_diff_stat
1072 $svn_diff" >"$tmp_log"
1074 echo "
1075 --- Internal stuff, DO NOT change please ---
1076 args: $@" >>"$tmp_log"
1077 echo "extra_files: $extra_files
1078 vi: ft=diff:noet:tw=76:" >>"$tmp_log"
1080 fi # end: if svn-log; then resume? else create template
1081 $edit_changelog && $EDITOR "$tmp_log"
1083 # ------------------ #
1084 # Re-"parse" the log #
1085 # ------------------ #
1087 # hmz this section is a bit messy...
1088 # helper string... !@#$%* escaping \\\\\\...
1089 sed_escape='s/\\/\\\\/g;s/@/\\@/g;s/&/\\\&/g'
1090 sed_eval_tags="s/<MM>/$MM/g; s/<DD>/$DD/g; s/<YYYY>/$YYYY/g"
1091 full_log=`sed '/^--*This line, and those below, will be ignored--*$/,$d;
1092 /^--You must fill this/d' "$tmp_log"`
1093 chlog_entry=`echo "$full_log" | sed '/^ChangeLog:$/,$!d; //d'`
1094 ensure_not_empty 'ChangeLog entry' "$chlog_entry"
1095 full_log=`echo "$full_log" | sed '/^ChangeLog:$/,$d'`
1096 mail_comment=`echo "$full_log" | sed '/^Comment:$/,$!d; //d'`
1097 full_log=`echo "$full_log" | sed '/^Comment:$/,$d'`
1098 # Add a period at the end of the title.
1099 sed_tmp='/^Title: */!d;s///;/ *\.$/!s/ *$/./'
1100 mail_title=`echo "$full_log" | sed "$sed_tmp"`
1101 ensure_not_empty 'commit title' "$mail_title"
1102 mail_title=`echo "$mail_title" | sed "$sed_eval_tags; $sed_escape"`
1103 sed_eval_tags="$sed_eval_tags; s@<TITLE>\\.*@$mail_title@g"
1104 mail_comment=`echo "$mail_comment" | sed "$sed_eval_tags"`
1105 raw_chlog_entry=$chlog_entry # ChangeLog entry without tags expanded
1106 chlog_entry=`echo "$chlog_entry" | sed "$sed_eval_tags; 1{
1107 /^ *$/d
1109 mail_subject=`echo "$full_log" | sed '/^Subject: */!d;s///'`
1110 ensure_not_empty 'mail subject' "$mail_subject"
1111 mail_to=`echo "$full_log" | sed '/^To:/!d'`
1112 send_a_mail=:
1113 if test x"$mail_to" = x; then
1114 send_a_mail=false
1115 else
1116 mail_to=`echo "$mail_to" | sed 's/^To: *//;s///'`
1117 # If there is a <MAILTO> in the 'To:' line, we must expand it.
1118 case $mail_to in #(
1119 *'<MAILTO>'*)
1120 get_mailto_property
1121 # Are we meant to send a mail?
1122 case $to in #(
1123 '') # No, don't send a mail.
1124 mail_to=
1125 send_a_mail=false
1126 ;; #(
1127 *) # Yes, send a mail.
1128 mail_to=`echo "$mail_to" | sed "s#<MAILTO>#$mailto#g"`
1130 esac
1131 esac
1132 $send_a_mail && ensure_not_empty '"To:" field of the mail' "$mail_to"
1135 test x"$FULLNAME" = x && warn_env FULLNAME && FULLNAME=$USER
1136 test x"$EMAIL" = x && warn_env EMAIL && EMAIL=$USER
1137 myself=`echo "$FULLNAME <$EMAIL>" | sed "$sed_escape"`
1138 mail_from=`echo "$full_log" | sed "/^From: */!d;s///;s@<MYSELF>@$myself@g"`
1139 ensure_not_empty '"From:" field of the mail' "$mail_from"
1141 # ------------------------------------ #
1142 # Sanity checks on the ChangeLog entry #
1143 # ------------------------------------ #
1145 if echo "$chlog_entry" | grep '<REV>' >/dev/null; then
1146 warn 'Using the tag <REV> anywhere else than in the Subject is deprecated.'
1147 yesno 'Continue anyway?' || return 1
1150 if echo "$chlog_entry" | grep ': \.$' >/dev/null; then
1151 warn 'It looks like you did not fill all entries in the ChangeLog:'
1152 echo "$chlog_entry" | grep ': \.$'
1153 yesno 'Continue anyway?' || return 1
1156 if echo "$chlog_entry" | grep '^--* Internal stuff' >/dev/null; then
1157 warn "It looks like you messed up the delimiters and I did not properly
1158 find your ChangeLog entry. Here it is, make sure it is correct:"
1159 echo "$chlog_entry"
1160 yesno 'Continue anyway?' || return 1
1163 if echo "$chlog_entry" | grep -i 'dont[^a-z0-9]' >/dev/null; then
1164 warn "Please avoid typos such as ${lred}dont$std instead of\
1165 ${lgreen}don't$std:"
1166 echo "$chlog_entry" | grep -n -i 'dont[^a-z0-9]' \
1167 | sed "s/[dD][oO][nN][tT]/$lred&$std/g"
1168 yesno 'Continue anyway?' || return 1
1171 if echo "$chlog_entry" | grep -i 'cant[^a-z0-9]' >/dev/null; then
1172 warn "Please avoid typos such as ${lred}cant$std instead of\
1173 ${lgreen}can't$std:"
1174 echo "$chlog_entry" | grep -n -i 'cant[^a-z0-9]' \
1175 | sed "s/[cC][aA][nN][tT]/$lred&$std/g"
1176 yesno 'Continue anyway?' || return 1
1179 if echo "$chlog_entry" | grep '^.\{80,\}' >/dev/null; then
1180 warn 'Please avoid long lines in your ChangeLog entry (80 columns max):'
1181 echo "$chlog_entry" | grep '^.\{80,\}'
1182 yesno 'Continue anyway?' || return 1
1185 if echo "$mail_title" | grep -i '^[a-z][a-z]*ed[^a-z]' >/dev/null; then
1186 warn 'ChangeLog entries should be written in imperative form:'
1187 echo "$mail_title" | grep -i '^[a-z][a-z]*ed[^a-z]' \
1188 | sed "s/^\\([a-zA-Z][a-zA-Z]*ed\\)\\([^a-zA-Z]\\)/$lred\\1$std\\2/"
1189 yesno 'Continue anyway?' || return 1
1192 # Check whether the user passed -m | --message
1194 while [ $i -lt $# ]; do
1195 arg=$1
1196 # This is not really a reliable way of knowing whether -m | --message was
1197 # passed but hum... Let's assume it'll do :s
1198 if [ x"$arg" = 'x-m' ] || [ x"$arg" = 'x--message' ]; then
1199 my_message=$2
1201 shift
1202 set dummy "$@" "$arg"
1203 shift
1204 i=$((i + 1))
1205 done
1206 if [ x"$my_message" = x ]; then
1207 # The title must not be indented in the commit message and must be
1208 # followed by a blank line. This yields much better results with most
1209 # VC-viewer (especially for Git but including for SVN, such as Trac for
1210 # instance). We assume that the title will always be on the 1st line.
1211 sed_git_title="1s@^[ ]*<TITLE>\\.*@$mail_title\\
1212 @g; $sed_eval_tags"
1213 # First, remove empty lines at the beginning, if any.
1214 # Remove also the date information (useless in commit messages)
1215 my_message=`echo "$raw_chlog_entry" \
1216 | sed -e '1,4 {
1217 /^<YYYY>-<[MD][MD]>-<[DM][DM]>/d
1218 /^[1-9][0-9][0-9][0-9]-[0-9][0-9]*-[0-9][0-9]*/d
1219 /^ and .*<.*@.*>$/d
1220 /^ *$/d
1221 }' \
1222 | sed -e "$sed_git_title" \
1223 -e "$sed_eval_tags; 1{
1224 /^ *$/d
1226 else
1227 notice 'you are overriding the commit message.'
1230 # Show suspicious whitespace additions with Git.
1231 $git_mode && git diff --cached --check
1233 if $dry_run; then
1234 proposal_file=',proposal'
1235 test -f "$proposal_file" \
1236 && proposal_file=`get_unique_file_name "$proposal_file"`
1237 sed_tmp='s/<REV>/???/g;s/\([^.]\) *\.$/\1/'
1238 mail_subject=`echo "$mail_subject" | sed "$sed_eval_tags; $sed_tmp"`
1240 if [ x"$mailto" != xdont_send_mails ] && [ x"$mailto" != xnothx ]; then
1241 to="
1242 To: $mailto"
1244 echo "\
1245 From: $mail_from$to
1246 Subject: $mail_subject
1248 $mail_comment
1250 ChangeLog:
1251 $chlog_entry
1254 $svn_diff_stat
1256 $svn_diff" >"$proposal_file"
1257 notice "A proposal of your commit was left in '$proposal_file'"
1258 return 0
1261 # Are you sure?
1262 if $git_mode; then
1263 $git_commit_all \
1264 || notice 'You are using git, unlike SVN, do not forget to git add your
1265 changes'
1268 # Change edit_changelog so that we're asking the confirmation below.
1269 $use_log_message_from_file && edit_changelog=: \
1270 && notice "You are about to commit the following change:
1271 $mail_title"
1273 $edit_changelog \
1274 && {
1275 yesno 'Are you sure you want to commit?' \
1276 || return 1
1279 # Add the ChangeLog entry
1280 old_chlog=`get_unique_file_name "$change_log_dir/ChangeLog.old"`
1281 mv "$change_log_dir/ChangeLog" "$old_chlog" || \
1282 abort 'Could not backup ChangeLog'
1283 trap "echo SIGINT; mv \"$old_chlog\" \"$change_log_dir/ChangeLog\";
1284 exit 130" $SIGINT
1285 echo "$chlog_entry" >"$change_log_dir/ChangeLog"
1286 echo >>"$change_log_dir/ChangeLog"
1287 cat "$old_chlog" >>"$change_log_dir/ChangeLog"
1289 # Add extra files such as cwd or ChangeLog to the commit.
1290 tmp_sed='s/ /\\ /g' # Escape spaces for the shell.
1291 if $git_mode && $use_changelog; then
1292 # Schedule the ChangeLog for the next commit
1293 (cd "$change_log_dir" && git add ChangeLog) \
1294 || abort 'failed to git add the ChangeLog'
1295 extra_files=
1296 else
1297 extra_files=`echo "$extra_files" | sed "$tmp_sed" | tr ':' '\n'`
1300 # Always sign the commits with Git.
1301 $git_mode && set dummy -s "$@" && shift
1303 # Update the Git index if necessary (just in case the user changed his
1304 # working copy in the mean time)
1305 if $git_mode && $git_commit_all; then
1306 (cd $change_log_dir && git add -v -u) || abort '`git add -v -u` failed'
1309 # --Commit-- finally! :D
1310 $SVN commit -m "$my_message" "$@" $extra_files || {
1311 svn_commit_rv=$?
1312 mv "$old_chlog" "$change_log_dir/ChangeLog"
1313 abort "Commit failed, $SVN returned $svn_commit_rv"
1316 echo -n 'Getting the revision number... '
1317 if $git_mode; then
1318 REV=`git rev-list --pretty=format:%h HEAD --max-count=1 | sed '1d;q'`
1319 else
1320 svn_info_tmp=`$SVN info "$change_log_dir/ChangeLog"`
1321 REV=`echo "$svn_info_tmp" | sed '/^Revision: /!d;s///'`
1322 test x"$REV" = x && REV=`echo "$svn_info_tmp" \
1323 | sed '/^Last Changed Rev: /!d;s///'`
1325 test x"$REV" = x && abort 'Cannot detect the current revision.'
1326 echo "$REV"
1328 # Let's make sure we have the real diff by asking the ChangeSet we've just
1329 # committed to the server.
1331 # Backup the old stuff in case we fail to get the real diff from the server
1332 # for some reason...
1333 save_svn_diff=$svn_diff
1334 save_svn_diff_stat=$svn_diff_stat
1336 if $git_mode; then
1337 svn_diff=`git diff --ignore-all-space --no-color -C 'HEAD^' HEAD`
1338 svn_diff_stat=`git diff --stat --ignore-all-space --no-color -C 'HEAD^' HEAD`
1339 grep svn-remote "$change_log_dir/.git/config" >/dev/null 2>&1 && \
1340 notice 'Do not forget to use: git-svn dcommit to push your commits in SVN'
1341 else
1342 # Fetch the ChangeSet and filter out the ChangeLog entry. We don't use
1343 # svn diff -c because this option is not portable to older svn versions.
1344 REV_MINUS_ONE=$((REV - 1))
1345 svn_diff=`svn_diffw -r"$REV_MINUS_ONE:$REV" "$repos_root" \
1346 | $AWK '/^Index: / { if (in_chlog) in_chlog = 0; }
1347 /^Index: .*ChangeLog$/ { in_chlog = 1 }
1348 { if (!in_chlog) print }'`
1349 if [ x"$svn_diff" = x ]; then
1350 svn_diff=$save_svn_diff
1351 svn_diff_stat=$save_svn_diff_stat
1352 else
1353 if require_diffstat; then
1354 svn_diff_stat=`echo "$svn_diff" | diffstat`
1355 else
1356 svn_diff_stat='diffstat not available'
1361 # Expand <REV> and remove the final period from the mail subject if there is
1362 # only one period.
1363 sed_tmp="s/<REV>/$REV/g;"'s/\([^.]\) *\.$/\1/'
1364 mail_subject=`echo "$mail_subject" | sed "$sed_eval_tags; $sed_tmp"`
1366 mail_file=`get_unique_file_name "$change_log_dir/+mail"`
1367 echo "\
1368 From: $mail_from
1369 To: $mail_to
1370 Subject: $mail_subject
1372 $mail_comment
1374 ChangeLog:
1375 $chlog_entry
1378 $svn_diff_stat
1380 $svn_diff" | sed 's/^\.$/ ./' >"$mail_file"
1381 # We change lines with only a `.' because they could mean "end-of-mail"
1383 # Send the mail
1384 if $send_a_mail; then
1385 trap 'echo SIGINT; exec < /dev/null' $SIGINT
1386 # FIXME: Move the mail to the +committed right now, in case the user
1387 # CTLR+C the mail-sending-thing, so that the mail will be properly saved
1388 # their.
1389 my_sendmail "$mail_file" "$mail_subject" "$mail_to" \
1390 "X-svn-url: $repos_root
1391 X-svn-revision: $REV"
1392 fi # end do we have to send a mail?
1393 rm -f "$tmp_log"
1394 rm -f "$old_chlog"
1395 save_mail_file=`echo "$mail_file" | sed 's/+//'`
1396 mkdir -p "$change_log_dir/+committed" \
1397 || warn "Couldn't mkdir -p $change_log_dir/+committed"
1398 if [ -d "$change_log_dir/vcs" ] \
1399 || [ -d "$change_log_dir/+committed" ]
1400 then
1401 mkdir -p "$change_log_dir/+committed/$REV" \
1402 && mv "$mail_file" "$change_log_dir/+committed/$REV/mail"
1404 return $svn_commit_rv
1407 # svn_diffw [args...]
1408 svn_diff()
1410 # Ignore white spaces.
1411 if $git_mode; then
1412 git diff -C "$@"
1413 else
1414 diffarg='-u'
1415 # Can't svn diff -x -w: svn 1.4 only.
1416 # Moreover we *MUST* use -x -uw, not -x -u -w or -x -u -x -w ...
1417 # Hence the hack to stick both diff arguments together...
1418 # No comment :)
1419 test x"$1" = x'--SVNW-HACK-w' && shift && diffarg='-uw'
1420 $SVN diff --no-diff-deleted --diff-cmd $DIFF -x $diffarg "$@"
1424 # svn_diffw [args...]
1425 svn_diffw()
1427 # Ignore white spaces.
1428 if $git_mode; then
1429 svn_diff --ignore-all-space "$@"
1430 else
1431 svn_diff --SVNW-HACK-w "$@"
1435 # svn_mail REV [mails...]
1436 svn_mail()
1438 test $# -lt 1 && abort "Not enough arguments provided;
1439 Try 'svn help mail' for more info."
1440 case $1 in
1441 PREV)
1442 if $git_mode; then
1443 REV=`git rev-list --pretty=format:%h 'HEAD^' --max-count=1 | sed '1d;q'`
1444 else
1445 REV=`svn_revision || abort 'Cannot get current revision number'`
1446 test x"$REV" = x && abort 'Cannot get current revision number'
1447 if [ "$REV" -lt 1 ]; then
1448 abort 'No previous revision.'
1450 REV=$((REV - 1))
1453 HEAD)
1454 REV=`svn_revision || abort 'Cannot get current revision number'`
1455 test x"$REV" = x && abort 'Cannot get current revision number'
1457 *) REV=$1;;
1458 esac
1459 shift
1461 found_committed=0; found=0
1462 while [ $found -eq 0 ]; do
1463 this_chlog_dir=`pwd -P`
1464 if [ -d ./+committed ]; then
1465 found_committed=1
1466 if [ -d ./+committed/$REV ]; then
1467 found=1
1468 else
1469 cd ..
1471 else
1472 cd ..
1474 # Stop searching when in / ... hmz :P
1475 test x`pwd` = x/ && break
1476 done
1477 if [ $found -eq 0 ]; then
1478 if [ $found_committed -eq 0 ]; then
1479 abort 'Could not find the +committed directory.'
1480 else
1481 abort "Could not find the revision $REV in +committed."
1483 abort 'Internal error (should never be here).'
1486 mail_file=; subject=; to=
1487 if [ -f ./+committed/$REV/mail ]; then
1488 # svn-wrapper generated file
1489 mail_file="./+committed/$REV/mail"
1490 subject=`sed '/^Subject: /!d;s///' $mail_file | sed '1q'`
1491 to=`sed '/^To: /!d;s///' $mail_file | sed '1q'`
1492 elif [ -f ./+committed/$REV/,iform ] && [ -f ./+committed/$REV/,message ]
1493 then
1494 # VCS-generated file
1495 subject=`sed '/^Subject: /!d;s///;s/^"//;s/"$//' ./+committed/$REV/,iform \
1496 | sed "s/<%= *rev *%>/$REV/g"`
1497 to=`sed '/^To:/,/^[^-]/!d' ./+committed/$REV/,iform | sed '1d;s/^- //;$d' \
1498 | xargs | sed 's/ */, /g'`
1499 mail_file=`get_unique_file_name "$TMPDIR/mail.r$REV"`
1500 echo "From: $FULLNAME <$EMAIL>
1501 To: $to
1502 Subject: $subject
1503 " >"$mail_file" || abort "Cannot create $mail_file"
1504 cat ./+committed/$REV/,message >>"$mail_file" \
1505 || abort "Cannot copy ./+committed/$REV/,message in $mail_file"
1506 else
1507 abort "Couldn't find the mail to re-send in `pwd`/+committed/$REV"
1509 if [ $# -gt 0 ]; then
1510 to=`echo "$*" | sed 's/ */, /g'`
1513 test x"$to" = x && abort 'Cannot find the list of recipients.
1514 Please report this bug.'
1515 test x"$subject" = x && abort 'Cannot find the subject of the mail.
1516 Please report this bug.'
1518 if yesno "Re-sending the mail of r$REV
1519 Subject: $subject
1520 To: $to
1521 Are you sure?"; then :; else
1522 return 1
1525 if $git_mode; then
1526 git_get_repos_info_
1527 else
1528 svn_info_tmp=`$SVN info`
1529 test $? -ne 0 && abort "Failed to get svn info on `pwd`"
1530 repos_root=`echo "$svn_info_tmp" | sed '/^Repository Root: /!d;s///'`
1531 repos_url=`echo "$svn_info_tmp" | sed '/^URL: /!d;s///'`
1532 # It looks like svn <1.3 didn't display a "Repository Root" entry.
1533 test x"$repos_root" = x && repos_root=$repos_url
1536 my_sendmail "$mail_file" "$subject" "$to" \
1537 "X-svn-url: $repos_url
1538 X-svn-revision: $REV"
1541 # svn_version
1542 svn_version()
1544 echo "Using svn-wrapper v$version-g$revision (C) SIGOURE Benoit [GPL]"
1547 # has_prop prop-name [path]
1548 # return value: 0 -> path has the property prop-name set.
1549 # 1 -> path has no property prop-name.
1550 # 2 -> svn error.
1551 has_prop()
1553 hp_plist=`$SVN proplist "$2"`
1554 test $? -ne 0 && return 2
1555 hp_res=`echo "$hp_plist" | sed "/^ *$1\$/!d"`
1556 test x"$hp_res" = x && return 1
1557 return 0
1560 # svn_propadd prop-name prop-val [path]
1561 svn_propadd()
1563 if $git_mode; then
1564 abort 'propadd is only for SVN, not for Git.'
1566 test $# -lt 2 \
1567 && abort 'Not enough arguments provided;
1568 try `svn help propadd` for more info'
1569 test $# -gt 3 \
1570 && abort 'Too many arguments provided;
1571 try `svn help propadd` for more info'
1573 path=$3
1574 test x"$path" = x && path='.' && set dummy "$@" '.' && shift
1575 has_prop "$1" "$3" || {
1576 test $? -eq 2 && return 1 # svn error
1577 # no property found:
1578 yesno "'$path' has no property named '$1', do you want to add it?" \
1579 && $SVN propset "$@"
1580 return $?
1583 current_prop_val=`$SVN propget "$1" "$3"`
1584 test $? -ne 0 && abort "Failed to get the current value of property '$1'."
1586 $SVN propset "$1" "$current_prop_val
1587 $2" "$3" >/dev/null || abort "Failed to add '$3' in the property '$1'."
1589 current_prop_val=`$SVN propget "$1" "$3" || echo "$current_prop_val
1590 $2"`
1591 echo "property '$1' updated on '$path', new value:
1592 $current_prop_val"
1595 # svn_propsed prop-name sed-script [path]
1596 svn_propsed()
1598 if $git_mode; then
1599 abort 'propsed is only for SVN, not for Git.'
1601 test $# -lt 2 \
1602 && abort 'Not enough arguments provided;
1603 try `svn help propsed` for more info'
1604 test $# -gt 3 \
1605 && abort 'Too many arguments provided;
1606 try `svn help propsed` for more info'
1608 path=$3
1609 test x"$path" = x && path='.'
1610 has_prop "$1" "$3" || {
1611 test $? -eq 2 && return 1 # svn error
1612 # no property found:
1613 abort "'$path' has no property named '$1'."
1616 prop_val=`$SVN propget "$1" "$3"`
1617 test $? -ne 0 && abort "Failed to get the current value of property '$1'."
1619 prop_val=`echo "$prop_val" | sed "$2"`
1620 test $? -ne 0 && abort "Failed to run the sed script '$2'."
1622 $SVN propset "$1" "$prop_val" "$3" >/dev/null \
1623 || abort "Failed to update the property '$1' with value '$prop_val'."
1625 new_prop_val=`$SVN propget "$1" "$3" || echo "$prop_val"`
1626 echo "property '$1' updated on '$path', new value:
1627 $new_prop_val"
1630 # svn_revision [args...]
1631 svn_revision()
1633 if $git_mode; then
1634 short=`git rev-list --pretty=format:%h HEAD --max-count=1 | sed '1d;q'`
1635 long=`git rev-list --pretty=format:%H HEAD --max-count=1 | sed '1d;q'`
1636 echo "$short ($long)"
1637 else
1638 svn_revision_info_out=`$SVN info "$@"`
1639 svn_revision_rv=$?
1640 echo "$svn_revision_info_out" | sed '/^Revision: /!d;s///'
1641 return $svn_revision_rv
1645 # svn_ignore [paths]
1646 svn_ignore()
1648 if [ $# -eq 0 ]; then # Simply display ignore-list.
1649 if $git_mode; then
1650 test -f .gitignore && cat .gitignore
1651 else
1652 $SVN propget 'svn:ignore'
1654 elif [ $# -eq 1 ]; then
1655 b=`basename "$1"`
1656 d=`dirname "$1"`
1657 if $git_mode; then
1658 echo "$b" >>"$d/.gitignore"
1659 git add "$d/.gitignore"
1660 notice 'files ignored in this directory:'
1661 cat "$d/.gitignore"
1662 else
1663 svn_propadd 'svn:ignore' "$b" "$d"
1665 else # Add arguments in svn:ignore.
1666 # This part is a bit tricky:
1667 # For each argument, we find all the other arguments with the same dirname
1668 # $dname and we svn:ignore them all in $dname.
1669 while [ $# -ne 0 ]; do
1670 arg=$1
1671 dname=`dirname "$1"`
1672 files=`basename "$1"`
1673 shift
1674 j=0; argc=$#
1675 while [ $j -lt $argc ] && [ $# -ne 0 ]; do
1676 this_arg=$1
1677 shift
1678 this_dname=`dirname "$this_arg"`
1679 this_file=`basename "$this_arg"`
1680 if [ x"$dname" = x"$this_dname" ]; then
1681 files="$files
1682 $this_file"
1683 else
1684 set dummy "$@" "$this_arg"
1685 shift
1687 j=$((j + 1))
1688 done
1689 if $git_mode; then
1690 echo "$files" >>"$dname"/.gitignore
1691 git add "$dname"/.gitignore
1692 notice "files ignored in $dname:"
1693 cat "$dname"/.gitignore
1694 else
1695 svn_propadd 'svn:ignore' "$files" "$dname"
1697 done
1701 # svn_help
1702 # FIXME: Use here-docs.
1703 svn_help()
1705 if [ $# -eq 0 ]; then
1706 svn_version
1707 $SVN help
1708 rv=$?
1709 echo '
1710 Additionnal commands provided by svn-wrapper:
1711 diffstat (ds)
1712 diffw (dw)
1713 ignore
1714 mail
1715 propadd (padd, pa) -- SVN only
1716 proposal
1717 propsed (psed) -- SVN only
1718 revision (rev)
1719 touch
1720 selfupdate (selfup)
1721 version'
1722 return $rv
1723 else
1724 case $1 in
1725 commit | ci)
1726 $SVN help commit | sed '/^Valid options:/i\
1727 Extra options provided by svn-wrapper:\
1728 \ --dry-run : do not commit, simply generate a patch with what\
1729 \ would have been comitted.\
1730 \ --use-log-file FILE : extract the ChangeLog entry from FILE. This\
1731 \ entry must be formated in a similar fashion to\
1732 \ what svn-wrapper usually asks you to fill in.\
1733 \ The FILE needs to be writable and will be\
1734 \ removed by svn-wrapper upon success.\
1738 diffstat | ds)
1739 require_diffstat
1740 echo 'diffstat (ds): Display the histogram from svn diff-output.'
1741 $SVN help diff | sed '1d;
1742 s/differences*/histogram/;
1743 2,35 s/diff/diffstat/g'
1745 diffw | dw)
1746 echo "diffw (dw): Display the differences without taking whitespaces\
1747 into account."
1748 $SVN help diff | sed '1d;
1749 2,35 s/diff\([^a-z]\)/diffw\1/g;
1750 /--diff-cmd/,/--no-diff-deleted/d'
1752 ignore)
1753 what='svn:ignore property'
1754 $git_mode && what='.gitignore file'
1755 echo 'ignore: Add some files in the '"$what"'.
1756 usage: 1. ignore [PATH]
1757 2. ignore FILE [FILES...]
1759 1. Display the value of '"$what"' on [PATH].
1760 2. Add some files in the '"$what"' of the directory containing them.
1762 When adding ignores, each pattern is ignored in its own directory, e.g.:
1763 '"$bme"' ignore dir/file "d2/*.o"
1764 Will put `file'\'' in the '"$what"' of `dir'\'' and `*.o'\'' in the
1765 '"$what"' of `d2'\''
1767 Valid options:
1768 None.'
1770 mail)
1771 echo 'mail: Resend the mail of a given commit.
1772 usage: mail REV [emails]
1774 REV must have an email file associated in +committed/REV.
1775 REV can also be PREV or HEAD.
1777 By default the mail is sent to same email addresses as during the original
1778 commit unless more arguments are given.'
1780 propadd | padd | pa)
1781 echo 'propadd (padd, pa): Add something in the value of a property.
1782 usage: propadd PROPNAME PROPVAL PATH
1783 This command only works in SVN mode.
1785 PROPVAL will be appended at the end of the property PROPNAME.
1787 Valid options:
1788 None.'
1790 proposal)
1791 echo 'proposal: Alias for: commit --dry-run.
1792 See: svn help commit.'
1794 propsed | psed)
1795 echo 'propsed (psed): Edit a property with sed.
1796 usage: propsed PROPNAME SED-ARGS PATH
1797 This command only works in SVN mode.
1799 eg: svn propsed svn:externals "s/http/https/" .
1801 Valid options:
1802 None.'
1804 revision | rev)
1805 echo 'revision (rev): Display the revision number of a local or remote item.'
1806 $SVN help info | sed '1d;
1807 s/information/revision/g;
1808 s/revision about/the revision of/g;
1809 2,35 s/info/revision/g;
1810 /-xml/d'
1812 touch)
1813 echo 'touch: Touch a file and svn add it.
1814 usage: touch FILE [FILES]...
1816 Valid options:
1817 None.'
1819 selfupdate | selfup | self-update | self-up)
1820 echo 'selfupdate (selfup): Attempt to update svn-wrapper.sh
1821 usage: selfupdate
1823 Valid options:
1824 None.'
1826 version)
1827 echo 'version: Display the version info of svn and svn-wrapper.
1828 usage: version
1830 Valid options:
1831 None.'
1833 *) $SVN help "$@";;
1834 esac
1838 # svn_status [args...]
1839 svn_status()
1841 if $git_mode; then
1842 git status "$@"
1843 return $?
1845 svn_status_out=`$SVN status "$@"`
1846 svn_status_rv=$?
1847 test x"$svn_status_out" = x && return $svn_status_rv
1848 echo "$svn_status_out" | sed "$sed_svn_st_color"
1849 return $svn_status_rv
1852 # svn_update [args...]
1853 svn_update()
1855 svn_update_out=`$SVN update "$@"`
1856 svn_update_rv=$?
1857 echo "$svn_update_out" | sed "$sed_svn_up_colors"
1858 return $svn_update_rv
1861 # ------------------- #
1862 # `main' starts here. #
1863 # ------------------- #
1865 # Define colors if stdout is a tty.
1866 if test -t 1; then
1867 set_colors
1868 else # stdout isn't a tty => don't print colors.
1869 set_nocolors
1872 # Consider this as a sed function :P.
1873 sed_svn_st_color="
1874 t dummy_sed_1
1875 : dummy_sed_1
1876 s@^?\\(......\\)+@+\\1+@
1877 s@^?\\(......\\)\\(.*/\\)+@+\\1\\2+@
1878 s@^?\\(......\\),@,\\1,@
1879 s@^?\\(......\\)\\(.*/\\),@,\\1\\2,@
1880 s/^\\(.\\)C/\\1${lred}C${std}/
1881 t dummy_sed_2
1882 : dummy_sed_2
1883 s/^?/${lred}?${std}/; t
1884 s/^M/${lgreen}M${std}/; t
1885 s/^A/${lgreen}A${std}/; t
1886 s/^X/${lblue}X${std}/; t
1887 s/^+/${lyellow}+${std}/; t
1888 s/^D/${lyellow}D${std}/; t
1889 s/^,/${lred},${std}/; t
1890 s/^C/${lred}C${std}/; t
1891 s/^I/${purple}I${std}/; t
1892 s/^R/${lblue}R${std}/; t
1893 s/^!/${lred}!${std}/; t
1894 s/^~/${lwhite}~${std}/; t"
1896 sed_svn_up_colors="
1897 t dummy_sed_1
1898 : dummy_sed_1
1900 /^Updated/ t
1901 /^Fetching/ t
1902 /^External/ t
1903 s/^\\(.\\)C/\\1${lred}C${std}/
1904 s/^\\(.\\)U/\\1${lgreen}U${std}/
1905 s/^\\(.\\)D/\\1${lred}D${std}/
1906 t dummy_sed_2
1907 : dummy_sed_2
1908 s/^A/${lgreen}A${std}/; t
1909 s/^U/${lgreen}U${std}/; t
1910 s/^D/${lyellow}D${std}/; t
1911 s/^G/${purple}G${std}/; t
1912 s/^C/${lred}C${std}/; t"
1914 # For dev's:
1915 test "x$1" = x--debug && shift && set -x
1917 test "x$1" = x--git && shift && git_mode=: && SVN=git
1919 test "x$1" = x--no-changelog && shift && use_changelog=false
1921 case $1 in
1922 # ------------------------------- #
1923 # Hooks for standard SVN commands #
1924 # ------------------------------- #
1925 commit | ci)
1926 shift
1927 svn_commit "$@"
1929 help | \? | h)
1930 shift
1931 svn_help "$@"
1933 status | stat | st)
1934 shift
1935 svn_status "$@"
1937 update | up)
1938 shift
1939 svn_update "$@"
1941 # -------------------- #
1942 # Custom SVN commands #
1943 # -------------------- #
1944 diffstat | ds)
1945 shift
1946 if [ -d .git ]; then
1947 git diff --stat -C
1948 else
1949 require_diffstat && $SVN diff --no-diff-deleted "$@" | diffstat
1952 diff | di)
1953 shift
1954 DIFF=$COLORDIFF
1955 svn_diff "$@"
1957 diffw | dw)
1958 shift
1959 DIFF=$COLORDIFF
1960 svn_diffw "$@"
1962 ignore)
1963 shift
1964 svn_ignore "$@"
1966 mail)
1967 shift
1968 svn_mail "$@"
1970 propadd | padd | pa)
1971 shift
1972 svn_propadd "$@"
1974 proposal)
1975 shift
1976 svn_commit --dry-run "$@"
1978 propsed | psed)
1979 shift
1980 svn_propsed "$@"
1982 revision | rev)
1983 shift
1984 svn_revision "$@"
1986 touch)
1987 shift
1988 touch "$@" && $SVN add "$@"
1990 selfupdate | selfup | self-update | self-up)
1991 shift
1992 selfupdate "$@"
1994 version | -version | --version)
1995 shift
1996 set dummy '--version' "$@"
1997 shift
1998 svn_version
1999 exec $SVN "$@"
2001 *) exec $SVN "$@"
2003 esac