#!/bin/sh #-*-tcl-*- # aegis - project change supervisor # # tkaer - changeset review manager # Copyright (C) 2000, 2001 Scott Finneran # Copyright (C) 2007, 2008 Peter Miller # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see # . # # Makes use of tkdiff by by John M. Klassa. # TkDiff Home Page: http://www.accurev.com/free/tkdiff # # @configure_input@ # # comments wrap in Tcl, but not in sh \ exec wish $0 -- ${1+"$@"} # TODO: # - Is it worth extracting the common code between edit_review_comments etc. # - split out some of the more general stuff like change details viewer into # seperate file. # - a serious tidy up. (clean up some of the binding spaghetti) # - add more comments. set bindir @bindir@ set libdir @libdir@ set datadir @datadir@ set datarootdir @datarootdir@ wm title . tkaer wm iconname . tkaer wm iconbitmap . @$datadir/aegis.icon wm iconmask . @$datadir/aegis.mask proc read_pipe { command errok } { set data "" set fd [open $command r] if { $fd != "" } { set data [read $fd] set codevar "" catch { close $fd } codevar if { $codevar != "" && !$errok } { puts [format "Command \"%s\"\nreturned \"%s\"" \ $command $codevar] exit 1 } } return [string trim $data] } proc handle_finished_review { widgetname } { global change_details_is_displayed global review_comments_is_displayed if { $widgetname == "." } { # first tidy up what we were doing. if { $change_details_is_displayed } { handle_details_button .toplevel } if { $review_comments_is_displayed } { handle_comments_button .toplevel } # Prompt before quitting. set result [tk_dialog .dlg "Finished" \ "Select action" \ question \ 0 "Pass" "Fail" "Quit" "Resume"] } else { destroy $widgetname } switch $result { 0 { if {[aerpass]} { destroy $widgetname } } 1 { if {[aerfail]} { destroy $widgetname } } 2 { destroy $widgetname } } } proc list_selected { widgetname } { # default return value. set item "" set i [$widgetname curselection] if { $i != "" } { set item [$widgetname get $i] } return "$item" } proc display_list {group_name file_list label_text} { # List of files newly created by this change: frame $group_name label $group_name.label -text $label_text pack $group_name.label -side top -fill x listbox $group_name.listbox -relief raised -borderwidth 2 \ -yscrollcommand "$group_name.scroll set" scrollbar $group_name.scroll \ -command "$group_name.listbox yview" # Implement a complex search algorithm for finding # the longest filename :-) set longest_filename_length 0; # just to get the ball rolling foreach i $file_list { $group_name.listbox insert end $i if {[string length $i] > $longest_filename_length} { set longest_filename_length [string length $i] } } # and update the width of the listboxes to suit. $group_name.listbox configure -width $longest_filename_length pack $group_name.scroll -side right -fill y pack $group_name.listbox -side left -fill both -expand 1 pack $group_name -side left -fill both -expand 1 } proc aerpass { } { global project_name global change_number global comments_edited if { $comments_edited } { # Prompt before quitting. set result [tk_dialog .dlg "aerpass" \ "Review comments were edited. Are you sure you want to pass the change?" \ question \ 0 "Continue" "Cancel"] # Review changed their mind. if {$result == 1} { return 0 } } set result [read_pipe \ [format "|aegis -rpass -v -project=%s -change=%s 2>@stdout" \ $project_name $change_number] 0] # Display the output from aerpass for all to see puts $result # pass complete. return 1 } proc aerfail { } { global project_name global change_number global comment_text global comments_edited # no comments were entered, so this may be a mistake. if { !$comments_edited } { # Prompt before quitting. set result [tk_dialog .dlg "aerpass" \ "No Review comments were entered. Are you sure you want to fail the change?" \ question \ 0 "Continue" "Cancel"] # Review changed their mind. if {$result == 1} { return 0 } } # If the comment window is still open then kill it (and make sure that the # destroy is caught and that it extracts the text), then store the text # in the global comment_text variable into the "unique" file # and pass the filename into aerfail. if { $comments_edited } { set unique_filename "/tmp/tkaer_comments_[pid]" set comment_source "-file $unique_filename" # just in case the filename is there... delete it. file delete $unique_filename set fid [open $unique_filename w] puts -nonewline $fid $comment_text close $fid } else { set comment_source "-reason \"\"" } set result [read_pipe \ [format "|aegis -rfail -v -project=%s -change=%s %s 2>@stdout" \ $project_name $change_number $comment_source] 0] # And clean up. if { $comments_edited } { file delete $unique_filename } # Display the output from aerfail for all to see puts $result # fail complete return 1 } proc get_baseline_search_path {} { global baseline_search_path global project_name global change_number set baseline_search_path \ [read_pipe [format "|aesub %s -project=%s -change=%s" \ "\$search_path" $project_name $change_number] 0] # the first entry will be the development directory itself, # so prune it from the list. set first_colon_pos [string first ":" $baseline_search_path] set first_colon_pos [incr first_colon_pos] set baseline_search_path \ [string range $baseline_search_path $first_colon_pos end] } proc find_previous_version {filename} { global baseline_search_path set search_list [split $baseline_search_path :] foreach i $search_list { set temp_filename "$i/$filename" if { [file isfile $temp_filename] == 1 } { return $temp_filename } } } # In the place of pretty code, I will provide comments :-) # These 2 functions expect filenames to contain a string of the form: # new_file_name<-old_file_name. # I believe that they fit the Macquarie Dictionary definition of cute: # "Ugly but interesting". proc get_new_moved_filename { filenames } { set delimiter_pos [string first <- $filenames] set delimiter_pos [expr $delimiter_pos - 1] return [string range $filenames 0 $delimiter_pos] } proc get_old_moved_filename { filenames } { set delimiter_pos [string first <- $filenames] set delimiter_pos [expr $delimiter_pos + 2] return [string range $filenames $delimiter_pos end] } proc review_moved_file_against_ancestor { w X Y filenames } { global baseline_search_path set search_list [split $baseline_search_path :] set orig_filename [get_old_moved_filename $filenames] set ancestor_directory "" destroy $w.ancestor menu $w.ancestor foreach a $search_list { set command_string [list review_moved_file $filenames $a] if {[file exist "$a/$orig_filename"]} { $w.ancestor add command -label $a -command $command_string } } tk_popup $w.ancestor $X $Y } proc review_moved_file {filenames prev_dir} { global development_directory set new_filename [get_new_moved_filename $filenames] set old_filename [get_old_moved_filename $filenames] if {$prev_dir == ""} { set old_file [find_previous_version $old_filename] } else { set old_file "$prev_dir/$old_filename" } set new_file $development_directory/$new_filename diff_review_file $old_file $new_file } proc review_modified_file_against_ancestor { w X Y filename } { global baseline_search_path set search_list [split $baseline_search_path :] set ancestor_directory "" destroy $w.ancestor menu $w.ancestor foreach a $search_list { set command_string [list review_modified_file $filename $a] if {[file exist "$a/$filename"]} { $w.ancestor add command -label $a -command $command_string } } tk_popup $w.ancestor $X $Y } proc review_modified_file {file_to_review prev_dir} { global development_directory if {$prev_dir == ""} { set old_file [find_previous_version $file_to_review] } else { set old_file "$prev_dir/$file_to_review" } set new_file $development_directory/$file_to_review diff_review_file $old_file $new_file } proc diff_review_file {old_file new_file} { global development_directory global pref cd $development_directory eval "exec \"$pref(diff_command)\" $old_file $new_file &" } proc review_new_file {file_to_review} { global development_directory global pref cd $development_directory set file_to_review $development_directory/$file_to_review eval "exec $pref(view_command) $file_to_review &" } proc review_removed_file {file_to_review} { # Not much that can be done for removed files, but the review # may want to know what was previously in there. global pref set file_to_review [find_previous_version $file_to_review] eval "exec $pref(view_command) $file_to_review &" } proc process_config_file {} { global bindir global pref set config_filename "~/.tkaer" # if the config file already exists then just source (i.e. execute) it. # otherwise create one for them. if {![file exists $config_filename]} { if [catch {open $config_filename w 0600} fd] { puts stderr "Cannot create config file $config_filename" exit 1 } puts "default $config_filename created." # defaults: puts $fd "# TKAER CONFIGURATION FILE." puts $fd "# -------------------------\n" puts $fd "# Visual difference tool. This is used for comparing" puts $fd "# modified or moved files with older versions." if {[file exists $bindir/tkdiff]} { set cmd $bindir/tkdiff } else { set cmd tkdiff } puts $fd [list set pref(diff_command) $cmd] puts $fd "" puts $fd "# File Viewer." puts $fd "# This is used to view new or removed files." if {[file exists /usr/X11R6/bin/xterm]} { set cmd /usr/X11R6/bin/xterm } else { set cmd xterm } puts $fd [list set pref(view_command) "$cmd -e @VI_PROG@ -R"] puts $fd \ "#set pref(view_command) \"/usr/bin/gnome-terminal -x @VI_PROG@\"" puts $fd "#set pref(view_command) \"/usr/bin/emacs\"" puts $fd "" puts $fd "# Text Viewer/Editor Font." puts $fd \ "# Font used by the change details viewer and review comments editor." puts $fd "set pref(view_edit_font) \"*-fixed-medium-r-normal-*-12-*\"" puts $fd "#set pref(view_edit_font) \"fixed\"" puts $fd \ "#set pref(view_edit_font) \"*-lucidatypewriter-medium-r-normal-*-12-*\"" puts $fd \ "#set pref(view_edit_font) \"*-lucidatypewriter-medium-r-normal-*-14-*\"" puts $fd \ "#set pref(view_edit_font) \"*-courier-medium-r-normal-*-12-*\"" puts $fd \ "#set pref(view_edit_font) \"*-courier-medium-r-normal-*-14-*\"" close $fd } source $config_filename } proc process_command_line {} { global argc argv argv0 global project_name global change_number set argindex 0 set project_name "" set change_number "" # Loop through argv seeing and try to guess at what the user wants to do. while {$argindex < $argc} { set arg [lindex $argv $argindex] switch -regexp -- $arg { "^-p$" { incr argindex set project_name [lindex $argv $argindex] } "^-p.*" { set project_name [string range $arg 2 end] } "^-c$" { incr argindex set change_number [lindex $argv $argindex] } "^-c.*" { set change_number [string range $arg 2 end] } default { set change_number [lindex $argv $argindex] } } incr argindex } # ok now we have the info provided by the user, so let's try # to fill in the gaps. if { $project_name == "" } { # Set the project_name variable. We need to ask Aegis for this, # so that we get what *aegis* thinks is the default project name. set project_name [read_pipe "|aegis -list default_project" 1] if { $project_name == "" } { puts [format "\n%s couldn't determine the project name\n" $argv0] exit 1 } } if { $change_number == "" } { if { $argc > 1 } { puts "\nUsage tkaer \[Change Number\]\n" # can't do much more.... exit 1 } # well the user didn't specify the change number so ask aegis # for help. set change_number [read_pipe "|aegis -list default_change" 1] if { $change_number == "" } { puts [format "%s couldn't determine the change number\n" $argv0] exit 1 } } } proc view_change_details {parent} { global project_name global change_number global datadir global pref # catch the output from 'ael cd' for all to see set ael_cf_output [read_pipe \ [format "|aegis -list cd -v -project=%s -change=%s -no_pager \ -page_length=9999 2>@stdout" \ $project_name $change_number] 0] set title [format "tkaer - \"%s\" - Change \"%d\" Details" \ $project_name $change_number] set w "$parent.change_details" toplevel $w wm title $w "$title" wm iconname $w "$title" wm geometry $w "80x30" wm iconbitmap $w @$datadir/aegis.icon text $w.text -relief sunken -bd 2 -yscrollcommand "$w.fr.scroll set" \ -setgrid 1 -height 30 -font $pref(view_edit_font) -highlightthickness 0 frame $w.fr -borderwidth 0 scrollbar $w.fr.scroll -command "$w.text yview" pack $w.fr.scroll -side bottom -fill y -expand 1 pack $w.fr -side right -fill y pack $w.text -expand 1 -fill both $w.text insert 0.0 $ael_cf_output $w.text mark set insert 0.0 menu $w.text.p $w.text.p add command -label Quit -command "destroy $w" bind $w.text <3> "tk_popup $w.text.p %X %Y" bind $w "destroy $w" bind $w "$w.text yview scroll 1 pages" bind $w "$w.text yview scroll -1 pages" bind $w <1> "bind $w \ \"\" ; bind $w \ \"\"" bind $w "$w.text see 0.0" bind $w "$w.text see end" bind $w.text <4> "$w.text yview scroll -6 units" bind $w.text <5> "$w.text yview scroll 6 units" bind $w "destroy $w" bind $w "destroy $w" bind $w "set change_details_is_displayed 0; \ $parent.buttons.details configure -relief raised" } proc handle_details_button {parent} { global change_details_is_displayed if { $change_details_is_displayed } { $parent.buttons.details configure -relief raised destroy $parent.change_details set change_details_is_displayed 0 } else { $parent.buttons.details configure -relief sunken view_change_details $parent set change_details_is_displayed 1 } } proc edit_review_comments {parent} { global project_name global change_number global datadir global comment_text global pref set title [format "tkaer - \"%s\" - Change \"%d\" Details" \ $project_name $change_number] set w "$parent.review_comments" toplevel $w wm title $w "$title" wm iconname $w "$title" wm geometry $w "80x30" wm iconbitmap $w @$datadir/aegis.icon frame $w.menubar -relief raised -bd 2 text $w.text -relief sunken -bd 2 -yscrollcommand "$w.fr.scroll set" \ -setgrid 1 -height 30 -font $pref(view_edit_font) -highlightthickness 0 frame $w.fr -borderwidth 0 scrollbar $w.fr.scroll -command "$w.text yview" pack $w.fr.scroll -side bottom -fill y -expand 1 pack $w.fr -side right -fill y pack $w.menubar -side top -fill x menubutton $w.menubar.file -text File -menu $w.menubar.file.menu menu $w.menubar.file.menu -tearoff 0 $w.menubar.file.menu add command -label "Quit & Save" -command \ "set comments_edited 1;set comment_text \[$w.text get 0.0 end\];destroy $w" $w.menubar.file.menu add command -label Quit -command "destroy $w" pack $w.menubar.file -side left pack $w.text -expand 1 -fill both $w.text insert 0.0 $comment_text $w.text mark set insert 0.0 bind $w "$w.text yview scroll 1 pages" bind $w "$w.text yview scroll -1 pages" bind $w <1> "bind $w \ \"\" ; bind $w \ \"\"" bind $w "$w.text see 0.0" bind $w "$w.text see end" bind $w.text <4> "$w.text yview scroll -6 units" bind $w.text <5> "$w.text yview scroll 6 units" bind $w "set review_comments_is_displayed 0; \ $parent.buttons.comments configure -relief raised" } proc handle_comments_button {parent} { global review_comments_is_displayed global comment_text global comments_edited if { $review_comments_is_displayed } { set comment_text [$parent.review_comments.text get 0.0 end] set comments_edited 1 $parent.buttons.comments configure -relief raised destroy $parent.review_comments set review_comments_is_displayed 0 } else { $parent.buttons.comments configure -relief sunken edit_review_comments $parent set review_comments_is_displayed 1 } } # AND BEGIN.... # Firstly, lets figure out what to do. process_command_line # And read in this users config. process_config_file # Get the *_change_list etc. variables from the report. eval [read_pipe [format "|aereport -f %s/wish/tkaer_info.rpt -unf \ -pw=1000 -project=%s -change=%s" \ $datadir $project_name $change_number] 0] if { $development_directory == "" } { puts [format \ "%s couldn't find the development directory for change %s\n" \ $argv0 $change_number] exit 1 } get_baseline_search_path wm title . [format "tkaer - \"%s\" - Change \"%d\"" $project_name \ $change_number] wm iconname . tkaer # # Now create the widget heirarchy # set w .toplevel frame $w frame $w.files set c "" # List of files newly created by this change: if { [llength $new_files_list] != 0 } { if {$num_new_files == 1} { set list_title [concat $num_new_files " Created File:"] } else { set list_title [concat $num_new_files " Created Files:"] } set c "$c$list_title\n" foreach i $new_files_list { set c "$c\Comments for $i (create):\n" } display_list $w.files.created $new_files_list $list_title bind $w.files.created.listbox { review_new_file [list_selected $w.files.created.listbox] } bind $w.files.created.listbox { review_new_file [list_selected $w.files.created.listbox] } set c "$c\n" } # List of files modified by this change: if { [llength $modified_files_list] != 0 } { if {$num_modified_files == 1} { set list_title [concat $num_modified_files " Modified File:"] } else { set list_title [concat $num_modified_files " Modified Files:"] } set c "$c$list_title\n" foreach i $modified_files_list { set c "$c\Comments for $i (modify):\n" } display_list $w.files.modified $modified_files_list $list_title bind $w.files.modified.listbox { review_modified_file [list_selected $w.files.modified.listbox] "" } bind $w.files.modified.listbox { review_modified_file [list_selected $w.files.modified.listbox] "" } bind $w.files.modified.listbox { $w.files.modified.listbox selection clear 0 end; $w.files.modified.listbox selection set \ [$w.files.modified.listbox nearest %y]; review_modified_file_against_ancestor $w %X %Y \ [list_selected $w.files.modified.listbox] } bind $w.files.modified.listbox { $w.files.modified.listbox selection clear 0 end; $w.files.modified.listbox selection set \ [$w.files.modified.listbox nearest %y]; review_modified_file_against_ancestor $w %X %Y \ [list_selected $w.files.modified.listbox] } set c "$c\n" } # List of files moved by this change: if { [llength $moved_files_list] != 0 } { if {$num_moved_files == 1} { set list_title [concat $num_moved_files " Moved File:"] } else { set list_title [concat $num_moved_files " Moved Files:"] } set c "$c$list_title\n" foreach i $moved_files_list { set c "$c\Comments for $i (move):\n" } display_list $w.files.moved $moved_files_list $list_title bind $w.files.moved.listbox { review_moved_file [list_selected $w.files.moved.listbox] "" } bind $w.files.moved.listbox { review_moved_file [list_selected $w.files.moved.listbox] "" } bind $w.files.moved.listbox { $w.files.moved.listbox selection clear 0 end; $w.files.moved.listbox selection set \ [$w.files.moved.listbox nearest %y]; review_moved_file_against_ancestor $w %X %Y \ [list_selected $w.files.moved.listbox] } bind $w.files.moved.listbox { $w.files.moved.listbox selection clear 0 end; $w.files.moved.listbox selection set \ [$w.files.moved.listbox nearest %y]; review_moved_file_against_ancestor $w %X %Y \ [list_selected $w.files.moved.listbox] } set c "$c\n" } # List of files removed by this change: if { [llength $removed_files_list] != 0 } { if {$num_removed_files == 1} { set list_title [concat $num_removed_files " Removed File:"] } else { set list_title [concat $num_removed_files " Removed Files:"] } set c "$c$list_title\n" foreach i removed_files_list { set c "$c\Comments for $i (remove):\n" } display_list $w.files.removed $removed_files_list $list_title bind $w.files.removed.listbox { review_removed_file [list_selected $w.files.removed.listbox] } bind $w.files.removed.listbox { review_removed_file [list_selected $w.files.removed.listbox] } set c "$c\n" } # if this change is in being_reviewed state then the finished button # calls up a "whatdyawannado" dialog box. Otherwise, we just exit. if { $change_state == "being_reviewed" } { wm protocol . WM_DELETE_WINDOW { handle_finished_review . } set finished_command { handle_finished_review . } } else { set finished_command { exit 0 } } set comment_text $c set comments_edited 0 set change_details_is_displayed 0 set review_comments_is_displayed 0 # And get finished & change_details buttons. frame $w.buttons button $w.buttons.finished -text "Finished" -command $finished_command button $w.buttons.comments -text "Comments" \ -command { handle_comments_button $w} button $w.buttons.details -text "Details" -command {handle_details_button $w} pack $w.buttons.details -side left -expand 1 -padx 2m -pady 1m # To hide the change comments button, just don't pack it. if { $change_state == "being_reviewed" } { pack $w.buttons.comments -side left -expand 1 -padx 2m -pady 1m } pack $w.buttons.finished -side left -expand 1 -padx 2m -pady 1m # Pack buttons first so that they still appear if someone gets # a little too enthusiastic about shrinking the window size. pack $w.buttons -side bottom pack $w.files -side top -expand 1 -fill both pack $w -expand 1 -fill both set errorCode 0