#! /usr/local/bin/bash # # neartool - a Clearcase emulator for working off-line. # # Usage: # # neartool mkelem [-c "comment"] [-nc] [-eltype type] filename # Marks a new file as a versioned element and checks it out. # # neartool mkdir [-c "comment"] [-nc] new_dir # Creates a new Clearcase directory. # # neartool mv old-file new-file # Renames or moves a versioned element. # # neartool mv file1 file2 file3 ... to-dir # Moves a number of versioned elements into a new directory. # # neartool (co|checkout) [-c "comment"] [-nc] filename # "checks out" a file by saving its current version and making a new # version writable. The file is also marked as "checked-out". # # neartool (unco|uncheckout) filename # Reverses the effect of the previous checkout. # # neartool (ci|checkin) [-c "comment"] [-nc] filename # Marks the file as "checked in" and finalizes its most recent # version, making it read-only. # # neartool revert filename # Backs up a version on a checked-in filename. # # neartool revertall filename # Reverts a filename all the way to its original state as extracted # from the Clearcase vobs. # # neartool collapse filename # Collapse all versioning information and mark the file as # "checked-in" and merged as is. Presumably this should only be # done after merging the changes back into the actual clearcase # vobs. # # neartool rmname filename # Removes a filename from a Clearcase directory. # # neartool find root [opts] # Finds all elements with versions at root or below. The options # are ignored. This is just a hack to support ctihave. # # neartool (lsco|lscheckout) [-l] file1 file2 file3... # Lists which of the files are currently checked out, if any. If no # filenames are given, lists all of the checked out files in the # current directory. The -l option is ignored. # # neartool diff [-pre] filename # Performs a diff between the indicated filename and its previous # version. The option -pre is ignored. # # neartool xdiff [-pre] filename # Performs an xdiff between the indicated filename and its previous # version. The option -pre is ignored. # #ENDCOMMENT # # get_nth_param ( param-no param1 param2 param3 ... ) # # Sets the variable nth_param to the value of the parameter # corresponding to param-no. # function get_nth_param { local param_no=$1 shift $param_no nth_param=$1 } # # get_last_param ( param1 param2 param3 ... ) # # Sets the variable last_param to the value of the last non-blank # parameter. # function get_last_param { last_param="$1" shift while [ ! -z "$*" ]; do last_param="$1" shift done } # # format_comment ( comment_str ) # # Writes the parameter -c '$comment_str', or -nc if the comment is # empty, to the standard output. Handles nested quotes appropriately. # function format_comment { local comment="$*" if [ -z "$comment" ]; then echo "-nc" else # Escaping single quotes inside a single-quoted string doesn't # seem to work too reliably. Instead, we'll just remove single # quotes. So there. # This absurd number of backslashes is needed to output a single # backslash through all this nesting. # echo "-c '"`echo $comment | sed "s/'/\\\\\\\\'/g"`"'" echo "-c '"`echo $comment | sed "s/'//g"`"'" fi } # # list_comments ( dirname basename ) # # Writes to stdout any comments associated with checked-out versions of # the indicated file, in order. # function list_comments { local dirname=$1 local basename=$2 local filename=$dirname/$basename local file comment version if [ -f $dirname/.ct0.$basename ]; then # Now look for comments, in version-number order. # We use a series of ls commands so we don't try to sort the # filenames between the one-, two-, and three-digit version # numbers. for file in `(cd $dirname; ls .ct[0-9].$basename; ls .ct[0-9][0-9].$basename; ls .ct[0-9][0-9][0-9].$basename) 2>/dev/null`; do version=`echo $file | sed "s/^\.ct\([0-9]*\).*$/\1/"` comment=$dirname/.ct${version}comment.$basename if [ -f $comment ]; then sed s'/^/ /' <$comment fi done fi } # # get_highest_version ( dirname basename ) # # Sets the variable $version to the numeric value that is the highest # existing version number defined for the given filename. If the # filename has no previous version numbers, sets $version to -1. # function get_highest_version { local dirname=$1 local basename=$2 local filename=$dirname/$basename local last if [ -f $dirname/.ct0.$basename ]; then # If there are any versions at all, get the highest-numbered one # and return it. # We use a series of ls commands so we don't try to sort the # filenames between the one-, two-, and three-digit version # numbers. last=`(cd $dirname; ls .ct[0-9].$basename; ls .ct[0-9][0-9].$basename; ls .ct[0-9][0-9][0-9].$basename) 2>/dev/null | tail -1` version=`echo $last | sed "s/^\.ct\([0-9]*\).*$/\1/"` else # If there aren't any versions yet, the highest-numbered existing # version number is -1. version=-1 fi } # # is_ctinternal ( basename ) # # Returns success if the filename matches one of the internal filenames # generated by this script, failure if it is a perfectly ordinary # non-ct file. # function is_ctinternal { local basename=$1 case $basename in .ct[0-9].*|ct[0-9][0-9].*|ct[0-9][0-9][0-9].*|\ .ct[0-9]comment.*|.ct[0-9][0-9]comment.*|.ct[0-9][0-9][0-9]comment*|\ .ctco.*|.ctnew.*) return 0;; esac return 1 } # # is_install_dir ( dirname ) # # Returns success if the directory is any of the directories known to # be install directories, such as inc, lib, lib/ss, etc. # function is_install_dir { local dirname=$1 get_rel_dir $projroot $dirname case $rel_dir in inc) return 0;; inc/*) return 0;; include/*) return 0;; lib) return 0;; lib/*) return 0;; esac return 1 } # # rm_all_ctinternals ( dirname basename ) # # Removes all the ctinternal files associated with the indicated file. # function rm_all_ctinternals { local dirname=$1 local basename=$2 rm -f $dirname/.ct[0-9].$basename rm -f $dirname/.ct[0-9][0-9].$basename rm -f $dirname/.ct[0-9][0-9][0-9].$basename rm -f $dirname/.ct[0-9]comment.$basename rm -f $dirname/.ct[0-9][0-9]comment.$basename rm -f $dirname/.ct[0-9][0-9][0-9]comment.$basename rm -f $dirname/.ctnew.$basename rm -f $dirname/.ctco.$basename } # # sane_filename ( dirname basename ) # # Performs some sanity checks on the filename. If the file is # unusable, exits the script with a status of 1. # function sane_filename { local dirname=$1 local basename=$2 local filename=$dirname/$basename if [ -z "$basename" ]; then echo "Filename unspecified." 1>&2 exit 1 fi if is_ctinternal $basename; then echo "$filename is an internal filename and should not be checked out." 1>&2 exit 1 fi # It's easy to accidentally attempt to check something out from the # inc directory. Let's detect this. if is_install_dir $dirname; then echo "$basename is in an install directory." 1>&2 echo "It should probably not be checked out." 1>&2 exit 1 fi if [ -d $filename ]; then # Let's just quietly ignore directories. If we issue the warning # statement, it makes ctaddtgt and ctaddpkg seem like they're # failing. # echo "Cannot operate on directories." 1>&2 exit 0 fi } # # get_fullpath ( local_dir ) # # Sets $fullpath to the fully-qualified pathname associated with $local_dir. # function get_fullpath { local local_dir=$1 if [ -z "$local_dir" ]; then fullpath=`pwd` else if [ ! -d "$local_dir" ]; then echo "Invalid directory: $local_dir" 1>&2 exit 1 fi # If we use pwd instead of /bin/pwd, $PWD will be used, which will give # the wrong answer fullpath=`(cd $local_dir; /bin/pwd)` fi } # # get_rel_dir ( root_dir local_dir ) # # Sets $rel_dir to the string which represents $local_dir relative to # $root_dir. This is a simple string-prefix operation, and could fail # in some obscure cases. # function get_rel_dir { get_fullpath $1 local root_dir=$fullpath get_fullpath $2 local local_dir=$fullpath # Now remove the initial prefix. if [ "$root_dir" = "$local_dir" ]; then rel_dir="." else rel_dir=`echo $local_dir | sed 's:^'$root_dir/'::'` if [ "$rel_dir" = "$local_dir" ]; then echo "$local_dir is not a directory within $root_dir." 1>&2 exit 1 fi fi } function ctmkelem { local filename dirname basename local comment eltype eltype=text_file while getopts c:n:e: flag; do case $flag in c) comment="$OPTARG";; n) comment="";; e) case $OPTARG in ltype) get_nth_param $OPTIND "$@" eltype=$nth_param OPTIND=`expr $OPTIND + 1`;; *) echo Invalid switch -e$OPTARG exit 1; esac;; \?) exit 1; esac done shift `expr $OPTIND - 1` if [ -z "$1" ]; then echo "No filenames given." exit 1 fi for filename in "$@"; do dirname=`dirname $filename` basename=`basename $filename` sane_filename $dirname $basename ctco=$dirname/.ctco.$basename if [ -f $ctco ]; then echo "$filename is already checked out." 1>&2 exit 1 fi if [ ! -f $filename ]; then # If the file doesn't exist, create it. echo -n "" >$filename fi get_highest_version $dirname $basename if [ $version -ne -1 ]; then echo "$filename already has versions." 1>&2 exit 1 fi if [ ! -w $filename ]; then echo "$filename has no write permission, it's probably already a versioned element." 1>&2 exit 1 fi # Create a ctnew file to indicate the file is newly created. ctnew=$dirname/.ctnew.$basename echo $eltype >$ctnew chmod uga-w $ctnew # Now "check out" the new filename by creating an empty first version. next=$dirname/.ct0.$basename nextcomment=$dirname/.ct0comment.$basename if [ ! -z "$comment" ]; then echo $comment >$nextcomment chmod uga-w $nextcomment fi echo -n "" > $next date >$ctco done } function ctmkdir { local filename dirname basename local comment while getopts c:n: flag; do case $flag in c) comment="$OPTARG";; n) comment="";; \?) exit 1; esac done shift `expr $OPTIND - 1` if [ -z "$1" ]; then echo "No filenames given." exit 1 fi for filename in "$@"; do dirname=`dirname $filename` basename=`basename $filename` if [ -z "$basename" ]; then echo "Filename unspecified." 1>&2 exit 1 fi if is_ctinternal $basename; then echo "$filename is an internal filename and should not be checked out." 1>&2 exit 1 fi if mkdir $filename; then # The directory is successfully made; record this in the # Clearcase instructions. get_rel_dir $projroot $dirname echo "ctco -nc $rel_dir" >>$projroot/.ctcmds echo "ctmkdir `format_comment $comment` $rel_dir/$basename" >>$projroot/.ctcmds else echo "Unable to create directory $filename." 1>&2 fi done } # # do_ctmv ( oldname newname ) # # The implementation of ctmv. This renames a single file, possibly # placing it in a new directory. # function do_ctmv { local oldfilename olddirname oldbasename local newfilename newdirname newbasename oldfilename=$1 olddirname=`dirname $oldfilename` oldbasename=`basename $oldfilename` newfilename=$2 newdirname=`dirname $newfilename` newbasename=`basename $newfilename` sane_filename $olddirname $oldbasename sane_filename $newdirname $newbasename if [ ! -f $oldfilename ]; then echo "$oldfilename does not exist." 1>&2 exit 1 fi if [ -f $newfilename ]; then echo "$newfilename already exists--remove it first." 1>&2 exit 1 fi oldctnew=$olddirname/.ctnew.$oldfilename if [ ! -f $oldctnew ]; then # The filename exists on the actual Clearcase vobs. We need to # record the renaming. get_rel_dir $projroot $olddirname local oldroot=$rel_dir get_rel_dir $projroot $newdirname local newroot=$rel_dir echo "ctco -nc $oldroot" >> $projroot/.ctcmds if [ "$oldroot" != "$newroot" ]; then echo "ctco -nc $newroot" >> $projroot/.ctcmds fi echo "ctmv $oldroot/$oldbasename $newroot/$newbasename" >> $projroot/.ctcmds fi # Now rename our local copy, and all of its version tracking stuff. get_fullpath $newdirname local newroot=$fullpath (cd $olddirname; for file in \ `ls .ct[0-9].$oldbasename \ .ct[0-9][0-9].$oldbasename \ .ct[0-9][0-9][0-9].$oldbasename \ .ct[0-9]comment.$oldbasename \ .ct[0-9][0-9]comment.$oldbasename \ .ct[0-9][0-9][0-9]comment.$oldbasename \ .ctnew.$oldbasename \ .ctco.$oldbasename \ $oldbasename 2>/dev/null`; do ctprefix=`echo $file | sed 's:'$oldbasename'$::'` mv -i $file $newroot/$ctprefix$newbasename done) } function ctmv { local source destination while getopts "" flag; do case $flag in \?) exit 1; esac done shift `expr $OPTIND - 1` get_last_param "$@" destination=$last_param; if [ ! -d "$destination" ]; then # If we're not moving to a directory, we are renaming a file--and # thus we can only allow two parameters. source=$1 shift 2 if [ ! -z "$*" ]; then echo Last filename must be a directory. exit 1 fi do_ctmv $source $destination else # Otherwise, we're moving a slew of files to a directory. Get # each filename and move it. for source in "$@"; do if [ "$source" != "$destination" ]; then do_ctmv $source $destination/$source fi done fi } function ctco { local filename dirname basename while getopts c:n: flag; do case $flag in c) comment="$OPTARG";; n) comment="";; \?) exit 1; esac done shift `expr $OPTIND - 1` if [ -z "$1" ]; then echo "No filenames given." exit 1 fi for filename in "$@"; do dirname=`dirname $filename` basename=`basename $filename` sane_filename $dirname $basename okflag=y if [ ! -f $filename ]; then echo "$filename does not exist." 1>&2 okflag= else ctco=$dirname/.ctco.$basename if [ -f $ctco ]; then echo "$filename is already checked out." 1>&2 okflag= else if [ -w $filename ]; then echo "$filename has write permission, it's probably not a versioned element." 1>&2 echo "Try neartool mkelem $filename." 1>&2 okflag= fi fi fi if [ ! -z "$okflag" ]; then get_highest_version $dirname $basename next=$dirname/.ct`expr $version + 1`.$basename nextcomment=$dirname/.ct`expr $version + 1`comment.$basename if [ ! -z "$comment" ]; then echo $comment >$nextcomment chmod uga-w $nextcomment fi mv $filename $next cp $next $filename chmod ug+w $filename date >$ctco fi done } function ctunco { local ignore filename dirname basename while getopts "r:" flag; do case $flag in r) ignore=;; \?) exit 1; esac done shift `expr $OPTIND - 1` if [ -z "$1" ]; then echo "No filenames given." exit 1 fi for filename in "$@"; do dirname=`dirname $filename` basename=`basename $filename` sane_filename $dirname $basename if [ ! -f $filename ]; then echo "$filename does not exist." 1>&2 exit 1 fi ctco=$dirname/.ctco.$basename if [ ! -f $ctco ]; then echo "$filename is not checked out." 1>&2 exit 1 fi get_highest_version $dirname $basename last=$dirname/.ct$version.$basename lastcomment=$dirname/.ct${version}comment.$basename mv -f $last $filename rm -f $lastcomment rm $ctco touch $filename # For emacs, and correct make behavior. done } function ctci { local filename dirname basename local comment while getopts c:n: flag; do case $flag in c) comment="$OPTARG";; n) comment="";; \?) exit 1; esac done shift `expr $OPTIND - 1` if [ -z "$1" ]; then echo "No filenames given." exit 1 fi for filename in "$@"; do dirname=`dirname $filename` basename=`basename $filename` sane_filename $dirname $basename if [ ! -f $filename ]; then echo "$filename does not exist." 1>&2 exit 1 fi ctco=$dirname/.ctco.$basename if [ ! -f $ctco ]; then echo "$filename is not checked out." 1>&2 exit 1 fi # We touch the file, so emacs will note that it has changed. touch $filename chmod uga-w $filename rm $ctco done } function ctrevert { local filename dirname basename while getopts "" flag; do case $flag in \?) exit 1; esac done shift `expr $OPTIND - 1` if [ -z "$1" ]; then echo "No filenames given." exit 1 fi for filename in "$@"; do dirname=`dirname $filename` basename=`basename $filename` sane_filename $dirname $basename if [ ! -f $filename ]; then echo "$filename does not exist." 1>&2 exit 1 fi ctco=$dirname/.ctco.$basename if [ -f $ctco ]; then echo "$filename is currently checked out. Use neartool unco $filename." 1>&2 exit 1 fi get_highest_version $dirname $basename if [ $version -eq -1 ]; then echo "$filename has no versions." 1>&2 exit 1 fi last=$dirname/.ct$version.$basename lastcomment=$dirname/.ct${version}comment.$basename mv -f $last $filename rm -f $lastcomment if [ $version -eq 0 ]; then ctnew=$dirname/.ctnew.$basename if [ -f $ctnew ]; then echo "Removing newly created element $filename." 1>&2 rm -f $ctnew $filename fi fi done } function ctrevertall { local filename dirname basename while getopts "" flag; do case $flag in \?) exit 1; esac done shift `expr $OPTIND - 1` if [ -z "$1" ]; then echo "No filenames given." exit 1 fi for filename in "$@"; do dirname=`dirname $filename` basename=`basename $filename` sane_filename $dirname $basename if [ ! -f $filename ]; then echo "$filename does not exist." 1>&2 exit 1 fi ct0=$dirname/.ct0.$basename if [ ! -f $ct0 ]; then echo "$filename has no versions." 1>&2 else ctnew=$dirname/.ctnew.$basename if [ ! -f $ctnew ]; then # If we didn't newly create this file, preserve its original version. mv -f $ct0 $filename fi rm_all_ctinternals $dirname $basename fi done } function ctcollapse { local filename dirname basename while getopts "" flag; do case $flag in \?) exit 1; esac done shift `expr $OPTIND - 1` if [ -z "$1" ]; then echo "No filenames given." exit 1 fi for filename in "$@"; do dirname=`dirname $filename` basename=`basename $filename` sane_filename $dirname $basename if [ ! -f $filename ]; then echo "$filename does not exist." 1>&2 exit 1 fi rm_all_ctinternals $dirname $basename chmod uga-w $filename touch $filename # For Emacs' benefit. done } function ctrmname { local filename dirname basename while getopts "" flag; do case $flag in \?) exit 1; esac done shift `expr $OPTIND - 1` if [ -z "$1" ]; then echo "No filenames given." exit 1 fi for filename in "$@"; do dirname=`dirname $filename` basename=`basename $filename` sane_filename $dirname $basename if [ ! -f $filename ]; then echo "$filename does not exist." 1>&2 exit 1 fi ctnew=$dirname/.ctnew.$basename if [ ! -f $ctnew ]; then # The filename exists on the actual Clearcase vobs. We need to # record the removing. get_rel_dir $projroot $dirname local root=$rel_dir echo "ctco -nc $root" >> $projroot/.ctcmds echo "ctrm $root/$basename" >> $projroot/.ctcmds fi # Now remove all of the versioned stuff. ctcollapse $filename # And remove the file itself. rm -f $filename done } function ctfind { local root=$1 if [ -z "$root" ]; then echo "Specify a starting point of the find." 1>&2 exit 1 fi find $root -name .ct0.\* -print | sed 's/\.ct0\.//' } function ctdescribe { local filename dirname basename while getopts "" flag; do case $flag in \?) exit 1; esac done shift `expr $OPTIND - 1` if [ -z "$1" ]; then echo "No filenames given." exit 1 fi for filename in `ls "$@"`; do dirname=`dirname $filename` basename=`basename $filename` ctco=$dirname/.ctco.$basename get_highest_version $dirname $basename version=`expr $version + 1` if [ -f $ctco ]; then if [ $version -eq 0 ]; then echo "$filename is checked out with no versions." else echo "$filename is checked out as version $version." list_comments $dirname $basename fi else if [ $version -eq 0 ]; then echo "$filename has not been checked out and has no versions." else echo "$filename is checked in as version $version." list_comments $dirname $basename fi fi done } function ctlsco { local filename dirname basename local ignore while getopts "l" flag; do case $flag in l) ignore=;; \?) exit 1; esac done shift `expr $OPTIND - 1` for filename in `ls "$@"`; do dirname=`dirname $filename` basename=`basename $filename` ctco=$dirname/.ctco.$basename if [ -f $ctco ]; then ctdescribe $filename fi done } function ctdiff { local filename dirname basename local graphical local ignore local diff while getopts "gp:" flag; do case $flag in g) graphical=y;; p) ignore=;; \?) exit 1; esac done shift `expr $OPTIND - 1` diff=diff if [ $graphical ]; then # The user requested a graphical difference; use gdiff if we can # find it. Otherwise, fall back to diff. if which gdiff 2>/dev/null >/dev/null; then diff=gdiff fi fi if [ -z "$1" ]; then echo "No filenames given." exit 1 fi for filename in "$@"; do dirname=`dirname $filename` basename=`basename $filename` sane_filename $dirname $basename if [ ! -f $filename ]; then echo "$filename does not exist." 1>&2 exit 1 fi get_highest_version $dirname $basename if [ $version -eq -1 ]; then echo "$filename has no versions." 1>&2 exit 1 fi last=$dirname/.ct$version.$basename # First, call diff (the real diff) just to see if the files are # different at all. if diff $last $filename >/dev/null; then echo Files are identical. else # If the files ARE different, then invoke diff again (which # might be gdiff, this time) to actually report the differences. # We must do it twice like this because gdiff might not return # non-zero if the files are different. $diff $last $filename fi done } function usage { sed '/#ENDCOMMENT/,$d' <$0 >&2 exit 1 } # # Main entry point # command=$1 if [ -z "$command" ]; then usage fi shift projroot=`ctproj -r` if [ -z "$projroot" ]; then echo "Not currently in a project tree." 1>&2 exit 1 fi case $command in mkelem) ctmkelem "$@";; mkdir) ctmkdir "$@";; mv) ctmv "$@";; co|checkout) ctco "$@";; unco|uncheckout) ctunco "$@";; ci|checkin) ctci "$@";; revert) ctrevert "$@";; revertall) ctrevertall "$@";; collapse) ctcollapse "$@";; rmname) ctrmname "$@";; find) ctfind "$@";; lsco|lscheckout) ctlsco "$@";; describe) ctdescribe "$@";; diff) ctdiff "$@";; xdiff) ctdiff -g "$@";; h|help|-h) usage;; *) echo "Invalid option: $command" 1>&2 exit 1; esac