git-gui: Finished commit implementation.
[gitweb.git] / git-gui
diff --git a/git-gui b/git-gui
index d74509a20b6ccfad182f4efb10a656c81d901cef..0e9e519636530e4ae522333274e2ca5219880ef0 100755 (executable)
--- a/git-gui
+++ b/git-gui
@@ -7,35 +7,70 @@ exec wish "$0" -- "$@"
 # and distributed under the terms of the GNU General Public Licence,
 # either version 2, or (at your option) any later version.
 
-
 ######################################################################
 ##
-## status
+## task management
 
+set single_commit 0
 set status_active 0
 set diff_active 0
 set checkin_active 0
+set commit_active 0
 set update_index_fd {}
 
-proc is_busy {} {
-       global status_active diff_active checkin_active update_index_fd
+set disable_on_lock [list]
+set index_lock_type none
 
-       if {$status_active > 0
-               || $diff_active
-               || $checkin_active
-               || $update_index_fd != {}} {
+proc lock_index {type} {
+       global index_lock_type disable_on_lock
+
+       if {$index_lock_type == {none}} {
+               set index_lock_type $type
+               foreach w $disable_on_lock {
+                       uplevel #0 $w disabled
+               }
+               return 1
+       } elseif {$index_lock_type == {begin-update} && $type == {update}} {
+               set index_lock_type $type
                return 1
        }
        return 0
 }
 
+proc unlock_index {} {
+       global index_lock_type disable_on_lock
+
+       set index_lock_type none
+       foreach w $disable_on_lock {
+               uplevel #0 $w normal
+       }
+}
+
+######################################################################
+##
+## status
+
+proc repository_state {hdvar ctvar} {
+       global gitdir
+       upvar $hdvar hd $ctvar ct
+
+       if {[catch {set hd [exec git rev-parse --verify HEAD]}]} {
+               set ct initial
+       } elseif {[file exists [file join $gitdir MERGE_HEAD]]} {
+               set ct merge
+       } else {
+               set ct normal
+       }
+}
+
 proc update_status {} {
-       global gitdir HEAD commit_type
+       global HEAD commit_type
        global ui_index ui_other ui_status_value ui_comm
        global status_active file_states
 
-       if {[is_busy]} return
+       if {$status_active || ![lock_index read]} return
 
+       repository_state HEAD commit_type
        array unset file_states
        foreach w [list $ui_index $ui_other] {
                $w conf -state normal
@@ -43,12 +78,6 @@ proc update_status {} {
                $w conf -state disabled
        }
 
-       if {[catch {set HEAD [exec git rev-parse --verify HEAD]}]} {
-               set commit_type initial
-       } else {
-               set commit_type normal
-       }
-
        if {![$ui_comm edit modified]
            || [string trim [$ui_comm get 0.0 end]] == {}} {
                if {[load_message GITGUI_MSG]} {
@@ -160,6 +189,7 @@ proc status_eof {fd buf} {
                set $buf {}
                close $fd
                if {[incr status_active -1] == 0} {
+                       unlock_index
                        set ui_status_value {Ready.}
                        if {$ui_fname_value != {}} {
                                show_diff $ui_fname_value
@@ -186,7 +216,7 @@ proc show_diff {path} {
        global file_states HEAD diff_3way diff_active
        global ui_diff ui_fname_value ui_fstatus_value ui_status_value
 
-       if {[is_busy]} return
+       if {$diff_active || ![lock_index read]} return
 
        clear_diff
        set s $file_states($path)
@@ -211,6 +241,7 @@ proc show_diff {path} {
                                close $fd
                        } err ]} {
                        set diff_active 0
+                       unlock_index
                        set ui_status_value "Unable to display $path"
                        error_popup "Error loading file:\n$err"
                        return
@@ -224,12 +255,13 @@ proc show_diff {path} {
 
        if {[catch {set fd [open $cmd r]} err]} {
                set diff_active 0
+               unlock_index
                set ui_status_value "Unable to display $path"
                error_popup "Error loading diff:\n$err"
                return
        }
 
-       fconfigure $fd -blocking 0 -translation binary
+       fconfigure $fd -blocking 0 -translation auto
        fileevent $fd readable [list read_diff $fd]
 }
 
@@ -237,6 +269,10 @@ proc read_diff {fd} {
        global ui_diff ui_status_value diff_3way diff_active
 
        while {[gets $fd line] >= 0} {
+               if {[string match {diff --git *} $line]} continue
+               if {[string match {diff --combined *} $line]} continue
+               if {[string match {--- *} $line]} continue
+               if {[string match {+++ *} $line]} continue
                if {[string match index* $line]} {
                        if {[string first , $line] >= 0} {
                                set diff_3way 1
@@ -275,10 +311,212 @@ proc read_diff {fd} {
        if {[eof $fd]} {
                close $fd
                set diff_active 0
+               unlock_index
                set ui_status_value {Ready.}
        }
 }
 
+######################################################################
+##
+## commit
+
+proc commit_tree {} {
+       global tcl_platform HEAD gitdir commit_type file_states
+       global commit_active ui_status_value
+       global ui_comm
+
+       if {$commit_active || ![lock_index update]} return
+
+       # -- Our in memory state should match the repository.
+       #
+       repository_state curHEAD cur_type
+       if {$commit_type != $cur_type || $HEAD != $curHEAD} {
+               error_popup {Last scanned state does not match repository state.
+
+Its highly likely that another Git program modified the
+repository since our last scan.  A rescan is required
+before committing.
+}
+               unlock_index
+               update_status
+               return
+       }
+
+       # -- At least one file should differ in the index.
+       #
+       set files_ready 0
+       foreach path [array names file_states] {
+               set s $file_states($path)
+               switch -glob -- [lindex $s 0] {
+               _* {continue}
+               A* -
+               D* -
+               M* {set files_ready 1; break}
+               U* {
+                       error_popup "Unmerged files cannot be committed.
+
+File $path has merge conflicts.
+You must resolve them and check the file in before committing.
+"
+                       unlock_index
+                       return
+               }
+               default {
+                       error_popup "Unknown file state [lindex $s 0] detected.
+
+File $path cannot be committed by this program.
+"
+               }
+               }
+       }
+       if {!$files_ready} {
+               error_popup {No checked-in files to commit.
+
+You must check-in at least 1 file before you can commit.
+}
+               unlock_index
+               return
+       }
+
+       # -- A message is required.
+       #
+       set msg [string trim [$ui_comm get 1.0 end]]
+       if {$msg == {}} {
+               error_popup {Please supply a commit message.
+
+A good commit message has the following format:
+
+- First line: Describe in one sentance what you did.
+- Second line: Blank
+- Remaining lines: Describe why this change is good.
+}
+               unlock_index
+               return
+       }
+
+       # -- Ask the pre-commit hook for the go-ahead.
+       #
+       set pchook [file join $gitdir hooks pre-commit]
+       if {$tcl_platform(platform) == {windows} && [file exists $pchook]} {
+               set pchook [list sh -c \
+                       "if test -x \"$pchook\"; then exec \"$pchook\"; fi"]
+       } elseif {[file executable $pchook]} {
+               set pchook [list $pchook]
+       } else {
+               set pchook {}
+       }
+       if {$pchook != {} && [catch {eval exec $pchook} err]} {
+               hook_failed_popup pre-commit $err
+               unlock_index
+               return
+       }
+
+       # -- Write the tree in the background.
+       #
+       set commit_active 1
+       set ui_status_value {Committing changes...}
+
+       set fd_wt [open "| git write-tree" r]
+       fileevent $fd_wt readable \
+               [list commit_stage2 $fd_wt $curHEAD $msg]
+}
+
+proc commit_stage2 {fd_wt curHEAD msg} {
+       global single_commit gitdir HEAD commit_type
+       global commit_active ui_status_value comm_ui
+
+       gets $fd_wt tree_id
+       close $fd_wt
+
+       if {$tree_id == {}} {
+               error_popup "write-tree failed"
+               set commit_active 0
+               set ui_status_value {Commit failed.}
+               unlock_index
+               return
+       }
+
+       # -- Create the commit.
+       #
+       set cmd [list git commit-tree $tree_id]
+       if {$commit_type != {initial}} {
+               lappend cmd -p $HEAD
+       }
+       if {$commit_type == {merge}} {
+               if {[catch {
+                               set fd_mh [open [file join $gitdir MERGE_HEAD] r]
+                               while {[gets $fd_mh merge_head] > 0} {
+                                       lappend -p $merge_head
+                               }
+                               close $fd_mh
+                       } err]} {
+                       error_popup "Loading MERGE_HEADs failed:\n$err"
+                       set commit_active 0
+                       set ui_status_value {Commit failed.}
+                       unlock_index
+                       return
+               }
+       }
+       if {$commit_type == {initial}} {
+               # git commit-tree writes to stderr during initial commit.
+               lappend cmd 2>/dev/null
+       }
+       lappend cmd << $msg
+       if {[catch {set cmt_id [eval exec $cmd]} err]} {
+               error_popup "commit-tree failed:\n$err"
+               set commit_active 0
+               set ui_status_value {Commit failed.}
+               unlock_index
+               return
+       }
+
+       # -- Update the HEAD ref.
+       #
+       set reflogm commit
+       if {$commit_type != {normal}} {
+               append reflogm " ($commit_type)"
+       }
+       set i [string first "\n" $msg]
+       if {$i >= 0} {
+               append reflogm {: } [string range $msg 0 [expr $i - 1]]
+       } else {
+               append reflogm {: } $msg
+       }
+       set cmd [list git update-ref \
+               -m $reflogm \
+               HEAD $cmt_id $curHEAD]
+       if {[catch {eval exec $cmd} err]} {
+               error_popup "update-ref failed:\n$err"
+               set commit_active 0
+               set ui_status_value {Commit failed.}
+               unlock_index
+               return
+       }
+
+       # -- Cleanup after ourselves.
+       #
+       catch {file delete [file join $gitdir MERGE_HEAD]}
+       catch {file delete [file join $gitdir MERGE_MSG]}
+       catch {file delete [file join $gitdir SQUASH_MSG]}
+       catch {file delete [file join $gitdir GITGUI_MSG]}
+
+       # -- Let rerere do its thing.
+       #
+       if {[file isdirectory [file join $gitdir rr-cache]]} {
+               catch {exec git rerere}
+       }
+
+       $comm_ui delete 0.0 end
+       $comm_ui edit modified false
+
+       if {$single_commit} do_quit
+
+       set commit_active 0
+       set ui_status_value "Changes committed as $cmt_id."
+       unlock_index
+       update_status
+}
+
 ######################################################################
 ##
 ## ui helpers
@@ -413,6 +651,7 @@ proc with_update_index {body} {
        global update_index_fd
 
        if {$update_index_fd == {}} {
+               if {![lock_index update]} return
                set update_index_fd [open \
                        "| git update-index --add --remove -z --stdin" \
                        w]
@@ -420,6 +659,7 @@ proc with_update_index {body} {
                uplevel 1 $body
                close $update_index_fd
                set update_index_fd {}
+               unlock_index
        } else {
                uplevel 1 $body
        }
@@ -570,21 +810,78 @@ proc error_popup {msg} {
 }
 
 proc show_msg {w top msg} {
-       message $w.m -text $msg -justify center -aspect 400
-       pack $w.m -side top -fill x -padx 20 -pady 20
-       button $w.ok -text OK -command "destroy $top"
-       pack $w.ok -side bottom -fill x
+       global gitdir appname
+
+       message $w.m -text $msg -justify left -aspect 400
+       pack $w.m -side top -fill x -padx 5 -pady 10
+       button $w.ok -text OK \
+               -width 15 \
+               -command "destroy $top"
+       pack $w.ok -side bottom
        bind $top <Visibility> "grab $top; focus $top"
        bind $top <Key-Return> "destroy $top"
+       wm title $top "error: $appname ([file normalize [file dirname $gitdir]])"
        tkwait window $top
 }
 
+proc hook_failed_popup {hook msg} {
+       global gitdir mainfont difffont appname
+
+       set w .hookfail
+       toplevel $w
+       wm transient $w .
+
+       frame $w.m
+       label $w.m.l1 -text "$hook hook failed:" \
+               -anchor w \
+               -justify left \
+               -font [concat $mainfont bold]
+       text $w.m.t \
+               -background white -borderwidth 1 \
+               -relief sunken \
+               -width 80 -height 10 \
+               -font $difffont \
+               -yscrollcommand [list $w.m.sby set]
+       label $w.m.l2 \
+               -text {You must correct the above errors before committing.} \
+               -anchor w \
+               -justify left \
+               -font [concat $mainfont bold]
+       scrollbar $w.m.sby -command [list $w.m.t yview]
+       pack $w.m.l1 -side top -fill x
+       pack $w.m.l2 -side bottom -fill x
+       pack $w.m.sby -side right -fill y
+       pack $w.m.t -side left -fill both -expand 1
+       pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
+
+       $w.m.t insert 1.0 $msg
+       $w.m.t conf -state disabled
+
+       button $w.ok -text OK \
+               -width 15 \
+               -command "destroy $w"
+       pack $w.ok -side bottom
+
+       bind $w <Visibility> "grab $w; focus $w"
+       bind $w <Key-Return> "destroy $w"
+       wm title $w "error: $appname ([file normalize [file dirname $gitdir]])"
+       tkwait window $w
+}
+
 ######################################################################
 ##
 ## ui commands
 
+set starting_gitk_msg {Please wait... Starting gitk...}
 proc do_gitk {} {
-       global tcl_platform
+       global tcl_platform ui_status_value starting_gitk_msg
+
+       set ui_status_value $starting_gitk_msg
+       after 5000 {
+               if {$ui_status_value == $starting_gitk_msg} {
+                       set ui_status_value {Ready.}
+               }
+       }
 
     if {$tcl_platform(platform) == "windows"} {
                exec sh -c gitk &
@@ -597,14 +894,14 @@ proc do_quit {} {
        global gitdir ui_comm
 
        set save [file join $gitdir GITGUI_MSG]
-       if {[$ui_comm edit modified]
-           && [string trim [$ui_comm get 0.0 end]] != {}} {
+       set msg [string trim [$ui_comm get 0.0 end]]
+       if {[$ui_comm edit modified] && $msg != {}} {
                catch {
                        set fd [open $save w]
                        puts $fd [string trim [$ui_comm get 0.0 end]]
                        close $fd
                }
-       } elseif {[file exists $save]} {
+       } elseif {$msg == {} && [file exists $save]} {
                file delete $save
        }
 
@@ -618,7 +915,7 @@ proc do_rescan {} {
 proc do_checkin_all {} {
        global checkin_active ui_status_value
 
-       if {[is_busy]} return
+       if {$checkin_active || ![lock_index begin-update]} return
 
        set checkin_active 1
        set ui_status_value {Checking in all files...}
@@ -656,6 +953,10 @@ proc do_signoff {} {
        }
 }
 
+proc do_commit {} {
+       commit_tree
+}
+
 # shift == 1: left click
 #          3: right click  
 proc click {w x y shift wx wy} {
@@ -682,7 +983,7 @@ proc unclick {w x y} {
        set path [$w get $lno.1 $lno.end]
        if {$path == {}} return
 
-       if {$col == 0 && ![is_busy]} {
+       if {$col == 0} {
                toggle_mode $path
        }
 }
@@ -695,6 +996,11 @@ set mainfont {Helvetica 10}
 set difffont {Courier 10}
 set maincursor [. cget -cursor]
 
+switch -- $tcl_platform(platform) {
+windows {set M1B Control; set M1T Ctrl}
+default {set M1B M1; set M1T M1}
+}
+
 # -- Menu Bar
 menu .mbar -tearoff 0
 .mbar add cascade -label Project -menu .mbar.project
@@ -705,27 +1011,38 @@ menu .mbar -tearoff 0
 
 # -- Project Menu
 menu .mbar.project
-.mbar.project add command -label Visulize \
+.mbar.project add command -label Visualize \
        -command do_gitk \
        -font $mainfont
 .mbar.project add command -label Quit \
        -command do_quit \
+       -accelerator $M1T-Q \
        -font $mainfont
 
 # -- Commit Menu
 menu .mbar.commit
 .mbar.commit add command -label Rescan \
        -command do_rescan \
+       -accelerator F5 \
        -font $mainfont
+lappend disable_on_lock \
+       [list .mbar.commit entryconf [.mbar.commit index last] -state]
 .mbar.commit add command -label {Check-in All Files} \
        -command do_checkin_all \
+       -accelerator $M1T-U \
        -font $mainfont
+lappend disable_on_lock \
+       [list .mbar.commit entryconf [.mbar.commit index last] -state]
 .mbar.commit add command -label {Sign Off} \
        -command do_signoff \
+       -accelerator $M1T-S \
        -font $mainfont
 .mbar.commit add command -label Commit \
        -command do_commit \
+       -accelerator $M1T-Return \
        -font $mainfont
+lappend disable_on_lock \
+       [list .mbar.commit entryconf [.mbar.commit index last] -state]
 
 # -- Fetch Menu
 menu .mbar.fetch
@@ -736,7 +1053,7 @@ menu .mbar.pull
 # -- Main Window Layout
 panedwindow .vpane -orient vertical
 panedwindow .vpane.files -orient horizontal
-.vpane add .vpane.files -sticky nsew
+.vpane add .vpane.files -sticky nsew -height 100 -width 400
 pack .vpane -anchor n -side top -fill both -expand 1
 
 # -- Index File List
@@ -781,7 +1098,7 @@ $ui_other tag conf in_diff -font [concat $mainfont bold]
 # -- Diff Header
 set ui_fname_value {}
 set ui_fstatus_value {}
-frame .vpane.diff -height 50 -width 400
+frame .vpane.diff -height 200 -width 400
 frame .vpane.diff.header
 label .vpane.diff.header.l1 -text {File:} -font $mainfont
 label .vpane.diff.header.l2 -textvariable ui_fname_value \
@@ -803,7 +1120,7 @@ pack .vpane.diff.header.l3 -side right
 frame .vpane.diff.body
 set ui_diff .vpane.diff.body.t
 text $ui_diff -background white -borderwidth 0 \
-       -width 80 -height 15 \
+       -width 80 -height 15 -wrap none \
        -font $difffont \
        -xscrollcommand {.vpane.diff.body.sbx set} \
        -yscrollcommand {.vpane.diff.body.sby set} \
@@ -828,7 +1145,7 @@ $ui_diff tag conf dni -foreground "#a000a0"
 $ui_diff tag conf bold -font [concat $difffont bold]
 
 # -- Commit Area
-frame .vpane.commarea -height 50
+frame .vpane.commarea -height 150
 .vpane add .vpane.commarea -stick nsew
 
 # -- Commit Area Buttons
@@ -844,11 +1161,13 @@ button .vpane.commarea.buttons.rescan -text {Rescan} \
        -command do_rescan \
        -font $mainfont
 pack .vpane.commarea.buttons.rescan -side top -fill x
+lappend disable_on_lock {.vpane.commarea.buttons.rescan conf -state}
 
 button .vpane.commarea.buttons.ciall -text {Check-in All} \
        -command do_checkin_all \
        -font $mainfont
 pack .vpane.commarea.buttons.ciall -side top -fill x
+lappend disable_on_lock {.vpane.commarea.buttons.ciall conf -state}
 
 button .vpane.commarea.buttons.signoff -text {Sign Off} \
        -command do_signoff \
@@ -859,6 +1178,7 @@ button .vpane.commarea.buttons.commit -text {Commit} \
        -command do_commit \
        -font $mainfont
 pack .vpane.commarea.buttons.commit -side top -fill x
+lappend disable_on_lock {.vpane.commarea.buttons.commit conf -state}
 
 # -- Commit Message Buffer
 frame .vpane.commarea.buffer
@@ -890,23 +1210,24 @@ label .status -textvariable ui_status_value \
 pack .status -anchor w -side bottom -fill x
 
 # -- Key Bindings
+bind $ui_comm <$M1B-Key-Return> {do_commit;break}
 bind . <Destroy> do_quit
 bind . <Key-F5> do_rescan
-bind . <M1-Key-r> do_rescan
-bind . <M1-Key-R> do_rescan
-bind . <M1-Key-s> do_signoff
-bind . <M1-Key-S> do_signoff
-bind . <M1-Key-u> do_checkin_all
-bind . <M1-Key-U> do_checkin_all
-bind . <M1-Key-Return> do_commit
-bind . <M1-Key-q> do_quit
-bind . <M1-Key-Q> do_quit
+bind . <$M1B-Key-r> do_rescan
+bind . <$M1B-Key-R> do_rescan
+bind . <$M1B-Key-s> do_signoff
+bind . <$M1B-Key-S> do_signoff
+bind . <$M1B-Key-u> do_checkin_all
+bind . <$M1B-Key-U> do_checkin_all
+bind . <$M1B-Key-Return> do_commit
+bind . <$M1B-Key-q> do_quit
+bind . <$M1B-Key-Q> do_quit
 foreach i [list $ui_index $ui_other] {
        bind $i <Button-1> {click %W %x %y 1 %X %Y; break}
        bind $i <Button-3> {click %W %x %y 3 %X %Y; break}
        bind $i <ButtonRelease-1> {unclick %W %x %y; break}
 }
-unset i
+unset i M1B M1T
 
 ######################################################################
 ##
@@ -917,6 +1238,11 @@ if {[catch {set gitdir [exec git rev-parse --git-dir]} err]} {
        exit 1
 }
 
-wm title . "git-ui ([file normalize [file dirname $gitdir]])"
+set appname [lindex [file split $argv0] end]
+if {$appname == {git-citool}} {
+       set single_commit 1
+}
+
+wm title . "$appname ([file normalize [file dirname $gitdir]])"
 focus -force $ui_comm
 update_status