#!/usr/bin/env bash # # Common code shared by the Cogito toolkit. # Copyright (c) Petr Baudis, 2005 # # This file provides a library containing common code shared with all the # Cogito programs. _cg_cmd=${0##*/} _cleanup_code= warn() { local beep= if [ "$1" = "-b" ]; then beep=1 shift fi echo "Warning: $@" >&2 [ "$beep" ] && echo -e "\a" >&2 } die() { echo "$_cg_cmd: $@" >&2 eval "$_cleanup_code" exit 1 } usage() { die "usage: $USAGE" } # Do this in case we get interrupted or prematurely die cleanup_trap() { _cleanup_code="$*" # die will execute the $_cleanup_code trap "echo; die \"interrupted\"" SIGINT SIGTERM } pager() { local cgless # A little trick to tell the difference between unset and set-to-empty # variable: if [ "${CG_LESS+set}" = "set" ]; then cgless="$CG_LESS" else cgless="R$_local_CG_LESS$LESS" fi local line # Invoke pager only if there's any actual output if IFS=$'\n' read -r line; then ( echo "$line"; cat; ) | LESS="$cgless" ${PAGER:-less} $PAGER_FLAGS fi } mktemp() { if [ "$has_mktemp" ]; then "$has_mktemp" "$@" return fi dirarg= if [ x"$1" = x"-d" ]; then dirarg="-d" shift fi prefix= if [ x"$1" = x"-t" ]; then prefix="${TMPDIR:-/tmp}/" shift fi "$(which mktemp)" $dirarg "$prefix$1" } stat() { if [ "$1" != "-c" ] || [ "$2" != "%s" -a "$2" != "%i" ]; then echo "INTERNAL ERROR: Unsupported stat call $@" >&2 return 1 fi if [ "$has_stat" ]; then shift;shift; "$has_stat" -f '%s' "$@" return fi # It's always -c '%s' now. if [ "$2" = "%s" ]; then ls -l "$3" | awk '{ print $5 }' elif [ "$2" = "%i" ]; then ls -lid "$3" | awk '{ print $1 }' fi } readlink() { if [ "$has_readlink" ]; then "$has_readlink" "$@" return fi if [ "$1" = "-f" ]; then shift target="$(maynormpath "$1")" target="${target%/}" # -e will test the existence of the final target; therefore, # it will also protect against recursive symlinks and such [ -e "$target" ] || return 1 while true; do if ! [ -L "$target" ]; then echo "$target" return 0 fi target2="$(readlink "$target" 2>/dev/null)" || return 1 [ "$target2" ] || return 1 target="$(maynormpath "$target2" "$target"/..)" done return 42 fi line="$(ls -ld "$1" 2>/dev/null)" || return 1 case "$line" in *-\>*) echo "${line#* -> }";; *) return 1;; esac return 0 } # tac is not POSIX :-( # tac() { sed -n '1!G;$p;h'; } - too cryptic tac() { if [ "$has_tac" ]; then "$has_tac" "$@" return fi perl -e 'print reverse <>' } # echo PATH | normpath # Normalize the path, handling and removing any superfluous .. and . # elements. Typically # echo ABSPATH/RELPATH | normpath # to get new absolute path. normpath() { local inp while IFS= read -r inp; do local path path2 path=() path2=() while [[ "$inp" == */* ]]; do path[${#path[@]}]="${inp%%/*}" inp="${inp#*/}" done path[${#path[@]}]="$inp" for (( i=0; $i < ${#path[@]}; i++ )); do [ "${path[$i]}" = "." ] && continue if [ "${path[$i]}" = ".." ]; then [ "${#path2[@]}" -gt 0 ] && unset path2[$((${#path2[@]} - 1))] continue fi path2[${#path2[@]}]="${path[$i]}" done for (( i=0; $i < ${#path2[@]}; i++ )); do echo -n "${path2[$i]}" [ $i -lt $((${#path2[@]} - 1)) ] && echo -n / done echo done } # maynormpath PATH [BASE] # If $PATH is relative, make it absolute wrt. $(pwd) or $BASE if specified. # Basically, call this instead of normpath() if $PATH can ever be absolute. maynormpath() { case "$1" in /*) echo "$1";; *) base="$2"; [ "$base" ] || base="$(pwd)" echo "$base/$1" | normpath esac } # xargs with one path argument per line path_xargs() { normpath | tr '\n' '\0' | xargs -0 "$@" } # Setup COGITO_REAL_SHARE to COGITO_SHARE if make install'd, or to # the most probable location if not. find_cogito_share() { if [ -n "${COGITO_SHARE}" ]; then COGITO_REAL_SHARE="${COGITO_SHARE}" return fi if [ "${0%/*}" != "$0" ]; then COGITO_REAL_SHARE="${0%/*}/" return fi # I'm not sure if the following normally ever gets triggered. # I can only do it by `sh cg-status`. --pasky COGITO_REAL_SHARE="./$_git_relpath/" } # Equivalent to cg-status -w -n -s '?', but the filenames are delimited # by '\0' instead of '\n'. # Usage: list_untracked_files DO_EXCLUDE SQUASH_DIRS [EXTRAEXCLUDE]... # DO_EXCLUDE: "no", "noexclude" means not to exclude anything, # otherwise the exclude rules apply # SQUASH_DIRS: "squashdirs" means that if a whole directory is untracked, # only the dirname/ will be listed, not all its contents # EXTRAEXCLUDE: extra exclude pattern list_untracked_files() { excludeflag="$1"; shift squashflag="$1"; shift EXCLUDE=() if [ "$excludeflag" != "no" -a "$excludeflag" != "noexclude" ]; then for excl in "$@"; do EXCLUDE[${#EXCLUDE[@]}]="--exclude=$excl" done find_cogito_share EXCLUDEFILE="${COGITO_REAL_SHARE}default-exclude" if [ -f "$EXCLUDEFILE" ]; then EXCLUDE[${#EXCLUDE[@]}]="--exclude-from=$EXCLUDEFILE" fi EXCLUDEFILE="$_git/info/exclude" if [ -f "$EXCLUDEFILE" ]; then EXCLUDE[${#EXCLUDE[@]}]="--exclude-from=$EXCLUDEFILE" fi # This is just for compatibility (2005-09-16). # To be removed later. EXCLUDEFILE="$_git/exclude" if [ -f $EXCLUDEFILE ]; then warn ".git/exclude is obsolete, use .git/info/exclude instead." EXCLUDE[${#EXCLUDE[@]}]="--exclude-from=$EXCLUDEFILE" fi EXCLUDE[${#EXCLUDE[@]}]="--exclude-per-directory=.gitignore" # Workaround for git < 1.2.0 if [ -n "$_git_relpath" ]; then local dir="${_git_relpath%/}" local reldir=".." while [ "$dir" != "." ]; do if [ "${dir%/*}" = "$dir" ]; then dir="." else dir="${dir%/*}" fi if [ -f "$reldir/.gitignore" ]; then EXCLUDE[${#EXCLUDE[@]}]="--exclude-from=$dir/.gitignore" fi reldir="../$reldir" done fi fi local listdirs= [ "$squashflag" = "squashdirs" ] && listdirs=--directory git-ls-files -z --others $listdirs "${EXCLUDE[@]}" } pick_id() { local lid="$1" uid="$2" local pick_id_script=' /^'$lid' /{ s/'\''/'\''\\'\'\''/g h s/^'$lid' \([^<]*\) <[^>]*> .*$/\1/ s/'\''/'\''\'\'\''/g s/.*/export GIT_'$uid'_NAME='\''&'\''/p g s/^'$lid' [^<]* <\([^>]*\)> .*$/\1/ s/'\''/'\''\'\'\''/g s/.*/export GIT_'$uid'_EMAIL='\''&'\''/p g s/^'$lid' [^<]* <[^>]*> \(.*\)$/\1/ s/'\''/'\''\'\'\''/g s/.*/export GIT_'$uid'_DATE='\''&'\''/p q } ' LANG=C LC_ALL=C sed -ne "$pick_id_script" # Ensure non-empty id name. echo "[ -z \"\$GIT_${uid}_NAME\" ] && export GIT_${uid}_NAME=\"\${GIT_${uid}_EMAIL%%@*}\"" } pick_author() { pick_id author AUTHOR } # Usage: showdate SECONDS TIMEZONE [FORMAT] # Display date nicely based on how GIT stores it. # Save the date to $_showdate showdate() { local secs=$1 tzhours=${2:0:3} tzmins=${2:0:1}${2:3} format="$3" # bash doesn't like leading zeros [ "${tzhours:1:1}" = 0 ] && tzhours=${2:0:1}${2:2:1} secs=$(($secs + $tzhours * 3600 + $tzmins * 60)) [ "$format" ] || format="+%a, %d %b %Y %H:%M:%S $2" if [ "$has_gnudate" ]; then _showdate="$(LANG=C "$has_gnudate" -ud "1970-01-01 UTC + $secs sec" "$format")" else _showdate="$(LANG=C date -u -r $secs "$format")" fi } # Usage: tree_timewarp [--no-head-update] DIRECTION_STR ROLLBACK_BOOL BASE BRANCH # Reset the current tree from version BASE to version BRANCH, properly updating # the working copy (if ROLLBACK_BOOL) and trying to keep local changes. # Returns false in case of local modifications. tree_timewarp() { local no_head_update= if [ "$1" = "--no-head-update" ]; then no_head_update=1 shift fi local dirstr="$1"; shift local rollback="$1"; shift local base="$1"; shift local branch="$1"; shift [ -s "$_git/merging" ] && die "merge in progress - cancel it by cg-reset first" local patchfile="$(mktemp -t gituncommit.XXXXXX)" if [ -n "$rollback" ]; then cg-diff -r "$base" >"$patchfile" [ -s "$patchfile" ] && warn "uncommitted local changes, trying to bring them $dirstr" else # XXX: This may be suboptimal, but it is also non-trivial to keep # the adds/removes properly. So this is just a quick hack to get it # working without much fuss. cg-diff -r "$branch" >"$patchfile" fi git-read-tree -m "$branch" || die "$branch: bad commit" [ "$no_head_update" ] || git-update-ref HEAD "$branch" || : # Kill gone files git-diff-tree -r "$base" "$branch" | while IFS=$'\t' read header file; do # match ":100755 000000 14d43b1abf... 000000000... D" if echo "$header" | egrep "^:([^ ][^ ]* ){4}D" >/dev/null; then rm -- "$file" fi done git-checkout-index -u -f -a # FIXME: Can produce bogus "contains only garbage" messages. cat "$patchfile" | cg-patch localmods=0 [ -s "$patchfile" ] && localmods=1 rm "$patchfile" return $localmods } # Determine the most conservative merge base of two commits - keep # recursing until we get only a single candidate for a merge base. # The merge base is returned as $_cg_baselist. If we had to recurse, # a non-zero number is stored in $_cg_base_conservative (otherwise, # it's set empty). conservative_merge_base() { local baselist safecounter baselist=("$@") _cg_base_conservative= for (( safecounter=0; $safecounter < 1000; safecounter++ )) ; do baselist=($(git-merge-base --all "${baselist[@]}")) || return 1 [ "${#baselist[@]}" -le "1" ] && break done [ $safecounter -gt 0 ] && _cg_base_conservative=$safecounter _cg_baselist=("${baselist[@]}") } # update_index will refresh the index and list the local modifications # Note that this isn't usually safe, since some of the modifications may # be recorded in the index file - modulo adds and removes also cg-restore # to historical revisions. Besides, it gives confusing output for relpath. # Never use it. If you do, accompany it with a comment explaining why is # it safe to use it. update_index() { git-update-index --refresh | sed 's/needs update$/locally modified/' } # Takes two object directories and checks if they are the same (symlinked # or so). is_same_repo() { local dir1="$1" dir2="$2" diff=1 # Originally, I wanted to compare readlink output, but that fails # in binding setup; it isn't likely the object database directories # themselves would be binded, but some trunk directories might. # So we just create a file inside and see if it appears on the # second side... if [ ! -w "$dir1" -o ! -w "$dir2" ]; then # ...except in readonly setups. [ "$(readlink -f "$dir1")" = "$(readlink -f "$dir2")" ] && diff=0 else n=$$ while [ -e "$dir1/.,,lnstest-$n" -o -e "$dir2/.,,lnstest-$n" ]; do n=$((n+1)) done touch "$dir1/.,,lnstest-$n" [ -e "$dir2/.,,lnstest-$n" ] && diff=0 rm "$dir1/.,,lnstest-$n" fi return $diff } # Checks if we weren't called through a deprecated alias deprecated_alias() { cmd="${0##*/}" propername="$1"; shift for a in "$@"; do [ "$cmd" = "$a" ] && \ warn "'$a' is a deprecated alias, please use '$propername' instead" done } print_help() { _cg_cmd="$(type -P "cg-$2")" [ -n "$_cg_cmd" ] || exit 1 sed -n '/^USAGE=/,0s/.*"\(.*\)"/Usage: \1/p; /^deprecated_alias/,0s/^deprecated_alias \([^ ]*\)/\1 is the new name for/p' < "$_cg_cmd" if [ x"$1" = xlong ]; then echo # TODO: Reduce this to just one sed if possible. sed -n '3,/^$/s/^# *//p' < "$_cg_cmd" | sed 's/^\(-.*\)::.*/\1::/' exit fi sed -n '3s/^# *//p' < "$_cg_cmd" echo echo "Options:" _cg_fmt=" %-20s %s\n" sed -n 's/# \(-.*\)::[^A-Za-z0-9]\(.*\)/\1\n\2/p' < "$_cg_cmd" | while read line; do case "$line" in -*) _cg_option="$line" ;; *) printf "$_cg_fmt" "$_cg_option" "$line" ;; esac done printf "$_cg_fmt" "-h, --help" "Print usage summary" printf "$_cg_fmt" "--long-help" "Print user manual" exit } for option in "$@"; do [ x"$option" = x-- ] && break if [ x"$option" = x"-h" ] || [ x"$option" = x"--help" ]; then print_help short "${_cg_cmd##cg-}" elif [ x"$option" = x"--long-help" ]; then print_help long "${_cg_cmd##cg-}" fi done ARGS=("$@") ARGPOS=0 set '' # clear positional parameters - use $ARGS[] instead if [ -z "$CG_NORC" -a -t 1 -a -e "$HOME/.cgrc" ]; then _cg_name="${_cg_cmd#cg-}" # We hope that there are no weird (regex-sensitive) characters # in Cogito command names. _cg_defaults1="$(sed -n "/^$_cg_cmd/s/^$_cg_cmd //p" < "$HOME/.cgrc")" _cg_defaults2="$(sed -n "/^$_cg_name/s/^$_cg_name //p" < "$HOME/.cgrc")" # And here we explicitly do not quote, allowing multiple arguments # to be specified - default word splitting will do its work here. ARGS=($_cg_defaults1 $_cg_defaults2 "${ARGS[@]}") fi optshift() { unset ARGS[$ARGPOS] ARGS=("${ARGS[@]}") [ -z "$1" -o -n "${ARGS[$ARGPOS]}" ] || die "option \`$1' requires an argument" } optfail() { die "unrecognized option \`${ARGS[$ARGPOS]}'" } optconflict() { die "conflicting option \`$CUROPT'" } optparse() { unset OPTARG if [ -z "$1" ]; then case "${ARGS[$ARGPOS]}" in --) optshift; return 1 ;; -*) return 0 ;; *) while (( ++ARGPOS < ${#ARGS[@]} )); do [[ "${ARGS[$ARGPOS]}" == -- ]] && return 1 [[ "${ARGS[$ARGPOS]}" == -* ]] && return 0 done; return 1 ;; esac fi CUROPT="${ARGS[$ARGPOS]}" local match="${1%=}" minmatch="${2:-1}" opt="$CUROPT" o="$CUROPT" val [[ "$1" == *= ]] && val="$match" case "$match" in --*) [ "$val" ] && o="${o%%=*}" [ ${#o} -ge $((2 + $minmatch)) -a \ "${match:0:${#o}}" = "$o" ] || return 1 [[ -n "$val" && "$opt" == *=?* ]] \ && ARGS[$ARGPOS]="${opt#*=}" \ || optshift "$val" ;; -?) [[ "$o" == $match* ]] || return 1 [[ "$o" != -?-* || -n "$val" ]] || optfail ARGS[$ARGPOS]=${o#$match} [ -n "${ARGS[$ARGPOS]}" ] \ && { [ -n "$val" ] || ARGS[$ARGPOS]=-"${ARGS[$ARGPOS]}"; } \ || optshift "$val" ;; *) die "optparse cannot handle $1" ;; esac if [ "$val" ]; then OPTARG="${ARGS[$ARGPOS]}" optshift fi } # Optional tools detection/stubbing # check_tool_presence NAME COMMAND EXENAME... # (use $cmd in COMMAND) check_tool() { cmdname="$1"; shift cmdtest="$1"; shift hasname="has_$cmdname" export $hasname= for exename in "$@"; do # We do our own $PATH iteration as it's faster than the fork() # of $(which), and this happens many times every time we # execute some cg tool. # Cut'n'pasted to the 'cg' source. save_IFS="$IFS"; IFS=: for dir in $PATH; do IFS="$save_IFS" cmd="$dir/$exename" if [ -x "$cmd" ] && eval "$cmdtest"; then export $hasname="$cmd" break fi done IFS="$save_IFS" [ "$hasname" ] && break done 2>/dev/null } if ! [ "$__cogito_subsequent" ]; then export __cogito_subsequent=1 check_tool mktemp 'todel="$("$cmd" -t)" && rm "$todel"' mktemp check_tool stat '"$cmd" -c %s / >/dev/null' stat gnustat gstat check_tool readlink '"$cmd" -f / >/dev/null' readlink check_tool gnudate '"$cmd" -Rud "1970-01-01 UTC" >/dev/null' date gnudate gdate check_tool tac 'tac /dev/null' tac fi _git="${GIT_DIR:-.git}" if [ ! "$_git_repo_unneeded" ] && [ ! "$GIT_DIR" ] && [ ! -d "$_git" ]; then _git_abs_path="$(git-rev-parse --git-dir 2>/dev/null)" if [ -d "$_git_abs_path" ]; then export _git_relpath="$(git-rev-parse --show-prefix)" cd "$_git_abs_path/.." fi fi _git_objects="${GIT_OBJECT_DIRECTORY:-$_git/objects}" # Check if we have something to work on, unless the script can do w/o it. if [ ! "$_git_repo_unneeded" ]; then if [ ! -d "$_git" ]; then echo "There is no GIT repository here ($_git not found)" >&2 exit 1 elif [ ! -x "$_git" ]; then echo "You do not have permission to access this GIT repository" >&2 exit 1 fi _git_head=master [ -s "$_git/HEAD" ] && { _git_head="$(git-symbolic-ref HEAD)"; _git_head="${_git_head#refs/heads/}"; } [ -s "$_git/head-name" ] && _git_head="$(cat "$_git/head-name")" fi # Check if the script requires to be called from the workdir root. if [ "$_git_requires_root" ] && [ "$_git_relpath" ]; then echo "This command can be ran only from the project root" >&2 exit 1 fi # Backward compatibility hacks: # Fortunately none as of now.