#!/usr/bin/ksh93 -p # # CDDL HEADER START # # The contents of this file are subject to the terms of the # Common Development and Distribution License (the "License"). # You may not use this file except in compliance with the License. # # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE # or http://www.opensolaris.org/os/licensing. # See the License for the specific language governing permissions # and limitations under the License. # # When distributing Covered Code, include this CDDL HEADER in each # file and include the License file at usr/src/OPENSOLARIS.LICENSE. # If applicable, add the following below this CDDL HEADER, with the # fields enclosed by brackets "[]" replaced with your own identifying # information: Portions Copyright [yyyy] [name of copyright owner] # # CDDL HEADER END # # # Copyright (c) 2002, 2010, Oracle and/or its affiliates. All rights reserved. # # Copyright 2008, 2010, Richard Lowe # # This script takes a file list and a workspace and builds a set of html files # suitable for doing a code review of source changes via a web page. # Documentation is available via the manual page, webrev.1, or just # type 'webrev -h'. # # Acknowledgements to contributors to webrev are listed in the webrev(1) # man page. # REMOVED_COLOR=brown CHANGED_COLOR=blue NEW_COLOR=blue HTML=' \n' FRAMEHTML=' \n' STDHEAD=' ' # # UDiffs need a slightly different CSS rule for 'new' items (we don't # want them to be bolded as we do in cdiffs or sdiffs). # UDIFFCSS=' ' # # Display remote target with prefix and trailing slash. # function print_upload_header { typeset -r prefix=$1 typeset display_target if [[ -z $tflag ]]; then display_target=${prefix}${remote_target} else display_target=${remote_target} fi if [[ ${display_target} != */ ]]; then display_target=${display_target}/ fi print " Upload to: ${display_target}\n" \ " Uploading: \c" } # # Upload the webrev via rsync. Return 0 on success, 1 on error. # function rsync_upload { if (( $# != 2 )); then print "\nERROR: rsync_upload: wrong usage ($#)" exit 1 fi typeset -r dst=$1 integer -r print_err_msg=$2 print_upload_header ${rsync_prefix} print "rsync ... \c" typeset -r err_msg=$( $MKTEMP /tmp/rsync_err.XXXXXX ) if [[ -z $err_msg ]]; then print "\nERROR: rsync_upload: cannot create temporary file" return 1 fi # # The source directory must end with a slash in order to copy just # directory contents, not the whole directory. # typeset src_dir=$WDIR if [[ ${src_dir} != */ ]]; then src_dir=${src_dir}/ fi $RSYNC -r -q ${src_dir} $dst 2>$err_msg if (( $? != 0 )); then if (( ${print_err_msg} > 0 )); then print "Failed.\nERROR: rsync failed" print "src dir: '${src_dir}'\ndst dir: '$dst'" print "error messages:" $SED 's/^/> /' $err_msg rm -f $err_msg fi return 1 fi rm -f $err_msg print "Done." return 0 } # # Create directories on remote host using SFTP. Return 0 on success, # 1 on failure. # function remote_mkdirs { typeset -r dir_spec=$1 typeset -r host_spec=$2 # # If the supplied path is absolute we assume all directories are # created, otherwise try to create all directories in the path # except the last one which will be created by scp. # if [[ "${dir_spec}" == */* && "${dir_spec}" != /* ]]; then print "mkdirs \c" # # Remove the last directory from directory specification. # typeset -r dirs_mk=${dir_spec%/*} typeset -r batch_file_mkdir=$( $MKTEMP \ /tmp/webrev_mkdir.XXXXXX ) if [[ -z $batch_file_mkdir ]]; then print "\nERROR: remote_mkdirs:" \ "cannot create temporary file for batch file" return 1 fi OLDIFS=$IFS IFS=/ typeset dir for dir in ${dirs_mk}; do # # Use the '-' prefix to ignore mkdir errors in order # to avoid an error in case the directory already # exists. We check the directory with chdir to be sure # there is one. # print -- "-mkdir ${dir}" >> ${batch_file_mkdir} print "chdir ${dir}" >> ${batch_file_mkdir} done IFS=$OLDIFS typeset -r sftp_err_msg=$( $MKTEMP /tmp/webrev_scp_err.XXXXXX ) if [[ -z ${sftp_err_msg} ]]; then print "\nERROR: remote_mkdirs:" \ "cannot create temporary file for error messages" return 1 fi $SFTP -b ${batch_file_mkdir} ${host_spec} 2>${sftp_err_msg} 1>&2 if (( $? != 0 )); then print "\nERROR: failed to create remote directories" print "error messages:" $SED 's/^/> /' ${sftp_err_msg} rm -f ${sftp_err_msg} ${batch_file_mkdir} return 1 fi rm -f ${sftp_err_msg} ${batch_file_mkdir} fi return 0 } # # Upload the webrev via SSH. Return 0 on success, 1 on error. # function ssh_upload { if (( $# != 1 )); then print "\nERROR: ssh_upload: wrong number of arguments" exit 1 fi typeset dst=$1 typeset -r host_spec=${dst%%:*} typeset -r dir_spec=${dst#*:} # # Display the upload information before calling delete_webrev # because it will also print its progress. # print_upload_header ${ssh_prefix} # # If the deletion was explicitly requested there is no need # to perform it again. # if [[ -z $Dflag ]]; then # # We do not care about return value because this might be # the first time this directory is uploaded. # delete_webrev 0 fi # # Create remote directories. Any error reporting will be done # in remote_mkdirs function. # remote_mkdirs ${dir_spec} ${host_spec} if (( $? != 0 )); then return 1 fi print "upload ... \c" typeset -r scp_err_msg=$( $MKTEMP /tmp/scp_err.XXXXXX ) if [[ -z ${scp_err_msg} ]]; then print "\nERROR: ssh_upload:" \ "cannot create temporary file for error messages" return 1 fi $SCP -q -C -B -o PreferredAuthentications=publickey -r \ $WDIR $dst 2>${scp_err_msg} if (( $? != 0 )); then print "Failed.\nERROR: scp failed" print "src dir: '$WDIR'\ndst dir: '$dst'" print "error messages:" $SED 's/^/> /' ${scp_err_msg} rm -f ${scp_err_msg} return 1 fi rm -f ${scp_err_msg} print "Done." return 0 } # # Delete webrev at remote site. Return 0 on success, 1 or exit code from sftp # on failure. If first argument is 1 then perform the check of sftp return # value otherwise ignore it. If second argument is present it means this run # only performs deletion. # function delete_webrev { if (( $# < 1 )); then print "delete_webrev: wrong number of arguments" exit 1 fi integer -r check=$1 integer delete_only=0 if (( $# == 2 )); then delete_only=1 fi # # Strip the transport specification part of remote target first. # typeset -r stripped_target=${remote_target##*://} typeset -r host_spec=${stripped_target%%:*} typeset -r dir_spec=${stripped_target#*:} typeset dir_rm # # Do not accept an absolute path. # if [[ ${dir_spec} == /* ]]; then return 1 fi # # Strip the ending slash. # if [[ ${dir_spec} == */ ]]; then dir_rm=${dir_spec%%/} else dir_rm=${dir_spec} fi if (( ${delete_only} > 0 )); then print " Removing: \c" else print "rmdir \c" fi if [[ -z "$dir_rm" ]]; then print "\nERROR: empty directory for removal" return 1 fi # # Prepare batch file. # typeset -r batch_file_rm=$( $MKTEMP /tmp/webrev_remove.XXXXXX ) if [[ -z $batch_file_rm ]]; then print "\nERROR: delete_webrev: cannot create temporary file" return 1 fi print "rename $dir_rm $TRASH_DIR/removed.$$" > $batch_file_rm # # Perform remote deletion and remove the batch file. # typeset -r sftp_err_msg=$( $MKTEMP /tmp/webrev_scp_err.XXXXXX ) if [[ -z ${sftp_err_msg} ]]; then print "\nERROR: delete_webrev:" \ "cannot create temporary file for error messages" return 1 fi $SFTP -b $batch_file_rm $host_spec 2>${sftp_err_msg} 1>&2 integer -r ret=$? rm -f $batch_file_rm if (( $ret != 0 && $check > 0 )); then print "Failed.\nERROR: failed to remove remote directories" print "error messages:" $SED 's/^/> /' ${sftp_err_msg} rm -f ${sftp_err_msg} return $ret fi rm -f ${sftp_err_msg} if (( ${delete_only} > 0 )); then print "Done." fi return 0 } # # Upload webrev to remote site # function upload_webrev { integer ret if [[ ! -d "$WDIR" ]]; then print "\nERROR: webrev directory '$WDIR' does not exist" return 1 fi # # Perform a late check to make sure we do not upload closed source # to remote target when -n is used. If the user used custom remote # target he probably knows what he is doing. # if [[ -n $nflag && -z $tflag ]]; then $FIND $WDIR -type d -name closed \ | $GREP closed >/dev/null if (( $? == 0 )); then print "\nERROR: directory '$WDIR' contains" \ "\"closed\" directory" return 1 fi fi # # We have the URI for remote destination now so let's start the upload. # if [[ -n $tflag ]]; then if [[ "${remote_target}" == ${rsync_prefix}?* ]]; then rsync_upload ${remote_target##$rsync_prefix} 1 ret=$? return $ret elif [[ "${remote_target}" == ${ssh_prefix}?* ]]; then ssh_upload ${remote_target##$ssh_prefix} ret=$? return $ret fi else # # Try rsync first and fallback to SSH in case it fails. # rsync_upload ${remote_target} 0 ret=$? if (( $ret != 0 )); then print "Failed. (falling back to SSH)" ssh_upload ${remote_target} ret=$? fi return $ret fi } # # input_cmd | url_encode | output_cmd # # URL-encode (percent-encode) reserved characters as defined in RFC 3986. # # Reserved characters are: :/?#[]@!$&'()*+,;= # # While not a reserved character itself, percent '%' is reserved by definition # so encode it first to avoid recursive transformation, and skip '/' which is # a path delimiter. # # The quotation character is deliberately not escaped in order to make # the substitution work with GNU sed. # function url_encode { $SED -e "s|%|%25|g" -e "s|:|%3A|g" -e "s|\&|%26|g" \ -e "s|?|%3F|g" -e "s|#|%23|g" -e "s|\[|%5B|g" \ -e "s|*|%2A|g" -e "s|@|%40|g" -e "s|\!|%21|g" \ -e "s|=|%3D|g" -e "s|;|%3B|g" -e "s|\]|%5D|g" \ -e "s|(|%28|g" -e "s|)|%29|g" -e "s|'|%27|g" \ -e "s|+|%2B|g" -e "s|\,|%2C|g" -e "s|\\\$|%24|g" } # # input_cmd | html_quote | output_cmd # or # html_quote filename | output_cmd # # Make a piece of source code safe for display in an HTML
block.
#
html_quote()
{
$SED -e "s/&/\&/g" -e "s/\</g" -e "s/>/\>/g" "$@" | expand
}
#
# Trim a digest-style revision to a conventionally readable yet useful length
#
trim_digest()
{
typeset digest=$1
echo $digest | $SED -e 's/\([0-9a-f]\{12\}\).*/\1/'
}
#
# input_cmd | its2url | output_cmd
#
# Scan for information tracking system references and insert links to the
# relevant databases.
#
its2url()
{
$SED -f ${its_sed_script}
}
#
# strip_unchanged | output_cmd
#
# Removes chunks of sdiff documents that have not changed. This makes it
# easier for a code reviewer to find the bits that have changed.
#
# Deleted lines of text are replaced by a horizontal rule. Some
# identical lines are retained before and after the changed lines to
# provide some context. The number of these lines is controlled by the
# variable C in the $AWK script below.
#
# The script detects changed lines as any line that has a " "
inx = c % C
c = C
}
for (i = 0; i < c; i++)
print ln[(inx + i) % C]
}
c = 0;
print
next
}
{ if (c >= C) {
ln[c % C] = $0
c++;
next;
}
c++;
print
}
END { if (c > (C * 2)) print "\n\n" }
# function sp(n) {for (i=0;i%4d %s \n", n, NR, $0}
# NR==8 {wl("#7A7ADD");next}
# NR==54 {wl("#7A7ADD");sp(3);next}
# NR==56 {wl("#7A7ADD");next}
# NR==57 {wl("black");printf "\n"; next}
# : :
#
# This script is then run on the original source file to generate the
# HTML that corresponds to the source file.
#
# The two HTML files are then combined into a single piece of HTML that
# uses an HTML table construct to present the files side by side. You'll
# notice that the changes are color-coded:
#
# black - unchanged lines
# blue - changed lines
# bold blue - new lines
# brown - deleted lines
#
# Blank lines are inserted in each file to keep unchanged lines in sync
# (side-by-side). This format is familiar to users of sdiff(1) or
# Teamware's filemerge tool.
#
sdiff_to_html()
{
diff -b $1 $2 > /tmp/$$.diffs
TNAME=$3
TPATH=$4
COMMENT=$5
#
# Now we have the diffs, generate the HTML for the old file.
#
$AWK '
BEGIN {
printf "function sp(n) {for (i=0;i%%4d %%s\\n\", NR, $0}\n"
printf "function changed() "
printf "{printf \"%%4d %%s\\n\", NR, $0}\n"
printf "function bl() {printf \"%%4d %%s\\n\", NR, $0}\n"
}
/^ {next}
/^>/ {next}
/^---/ {next}
{
split($1, a, /[cad]/) ;
if (index($1, "a")) {
if (a[1] == 0) {
n = split(a[2], r, /,/);
if (n == 1)
printf "BEGIN\t\t{sp(1)}\n"
else
printf "BEGIN\t\t{sp(%d)}\n",\
(r[2] - r[1]) + 1
next
}
printf "NR==%s\t\t{", a[1]
n = split(a[2], r, /,/);
s = r[1];
if (n == 1)
printf "bl();printf \"\\n\"; next}\n"
else {
n = r[2] - r[1]
printf "bl();sp(%d);next}\n",\
(r[2] - r[1]) + 1
}
next
}
if (index($1, "d")) {
n = split(a[1], r, /,/);
n1 = r[1]
n2 = r[2]
if (n == 1)
printf "NR==%s\t\t{removed(); next}\n" , n1
else
printf "NR==%s,NR==%s\t{removed(); next}\n" , n1, n2
next
}
if (index($1, "c")) {
n = split(a[1], r, /,/);
n1 = r[1]
n2 = r[2]
final = n2
d1 = 0
if (n == 1)
printf "NR==%s\t\t{changed();" , n1
else {
d1 = n2 - n1
printf "NR==%s,NR==%s\t{changed();" , n1, n2
}
m = split(a[2], r, /,/);
n1 = r[1]
n2 = r[2]
if (m > 1) {
d2 = n2 - n1
if (d2 > d1) {
if (n > 1) printf "if (NR==%d)", final
printf "sp(%d);", d2 - d1
}
}
printf "next}\n" ;
next
}
}
END { printf "{printf \"%%4d %%s\\n\", NR, $0 }\n" }
' /tmp/$$.diffs > /tmp/$$.file1
#
# Now generate the HTML for the new file
#
$AWK '
BEGIN {
printf "function sp(n) {for (i=0;i%%4d %%s\\n\", NR, $0}\n"
printf "function changed() "
printf "{printf \"%%4d %%s\\n\", NR, $0}\n"
printf "function bl() {printf \"%%4d %%s\\n\", NR, $0}\n"
}
/^ {next}
/^>/ {next}
/^---/ {next}
{
split($1, a, /[cad]/) ;
if (index($1, "d")) {
if (a[2] == 0) {
n = split(a[1], r, /,/);
if (n == 1)
printf "BEGIN\t\t{sp(1)}\n"
else
printf "BEGIN\t\t{sp(%d)}\n",\
(r[2] - r[1]) + 1
next
}
printf "NR==%s\t\t{", a[2]
n = split(a[1], r, /,/);
s = r[1];
if (n == 1)
printf "bl();printf \"\\n\"; next}\n"
else {
n = r[2] - r[1]
printf "bl();sp(%d);next}\n",\
(r[2] - r[1]) + 1
}
next
}
if (index($1, "a")) {
n = split(a[2], r, /,/);
n1 = r[1]
n2 = r[2]
if (n == 1)
printf "NR==%s\t\t{new() ; next}\n" , n1
else
printf "NR==%s,NR==%s\t{new() ; next}\n" , n1, n2
next
}
if (index($1, "c")) {
n = split(a[2], r, /,/);
n1 = r[1]
n2 = r[2]
final = n2
d2 = 0;
if (n == 1) {
final = n1
printf "NR==%s\t\t{changed();" , n1
} else {
d2 = n2 - n1
printf "NR==%s,NR==%s\t{changed();" , n1, n2
}
m = split(a[1], r, /,/);
n1 = r[1]
n2 = r[2]
if (m > 1) {
d1 = n2 - n1
if (d1 > d2) {
if (n > 1) printf "if (NR==%d)", final
printf "sp(%d);", d1 - d2
}
}
printf "next}\n" ;
next
}
}
END { printf "{printf \"%%4d %%s\\n\", NR, $0 }\n" }
' /tmp/$$.diffs > /tmp/$$.file2
#
# Post-process the HTML files by running them back through $AWK
#
html_quote < $1 | $AWK -f /tmp/$$.file1 > /tmp/$$.file1.html
html_quote < $2 | $AWK -f /tmp/$$.file2 > /tmp/$$.file2.html
#
# Now combine into a valid HTML file and side-by-side into a table
#
print "$HTML$STDHEAD"
print "$WNAME Sdiff $TPATH/$TNAME "
print ""
print "Print this page"
print "$COMMENT
\n"
print ""
print ""
strip_unchanged /tmp/$$.file1.html
print "
"
strip_unchanged /tmp/$$.file2.html
print "
"
print "
"
print ""
framed_sdiff $TNAME $TPATH /tmp/$$.file1.html /tmp/$$.file2.html \
"$COMMENT"
}
#
# framed_sdiff
#
# Expects lefthand and righthand side html files created by sdiff_to_html.
# We use insert_anchors() to augment those with HTML navigation anchors,
# and then emit the main frame. Content is placed into:
#
# $WDIR/DIR/$TNAME.lhs.html
# $WDIR/DIR/$TNAME.rhs.html
# $WDIR/DIR/$TNAME.frames.html
#
# NOTE: We rely on standard usage of $WDIR and $DIR.
#
function framed_sdiff
{
typeset TNAME=$1
typeset TPATH=$2
typeset lhsfile=$3
typeset rhsfile=$4
typeset comments=$5
typeset RTOP
# Enable html files to access WDIR via a relative path.
RTOP=$(relative_dir $TPATH $WDIR)
# Make the rhs/lhs files and output the frameset file.
print "$HTML$STDHEAD" > $WDIR/$DIR/$TNAME.lhs.html
cat >> $WDIR/$DIR/$TNAME.lhs.html <<-EOF
$comments
EOF
cp $WDIR/$DIR/$TNAME.lhs.html $WDIR/$DIR/$TNAME.rhs.html
insert_anchors $lhsfile >> $WDIR/$DIR/$TNAME.lhs.html
insert_anchors $rhsfile >> $WDIR/$DIR/$TNAME.rhs.html
close=''
print $close >> $WDIR/$DIR/$TNAME.lhs.html
print $close >> $WDIR/$DIR/$TNAME.rhs.html
print "$FRAMEHTML$STDHEAD" > $WDIR/$DIR/$TNAME.frames.html
print "$WNAME Framed-Sdiff " \
"$TPATH/$TNAME " >> $WDIR/$DIR/$TNAME.frames.html
cat >> $WDIR/$DIR/$TNAME.frames.html <<-EOF