git-gui: Force focus to the diff viewer on mouse click.
[gitweb.git] / git-gui.sh
index 6a4086d4755f3d92fdc21ea6a6fe76bc4c3f78fc..ea4136c98b0621264969f47a11f80d24834bf77f 100755 (executable)
@@ -428,6 +428,7 @@ proc load_message {file} {
                }
                set content [string trim [read $fd]]
                close $fd
+               regsub -all -line {[ \r\t]+$} $content {} content
                $ui_comm delete 0.0 end
                $ui_comm insert end $content
                return 1
@@ -545,13 +546,15 @@ proc prune_selection {} {
 ## diff
 
 proc clear_diff {} {
-       global ui_diff current_diff_path ui_index ui_workdir
+       global ui_diff current_diff_path current_diff_header
+       global ui_index ui_workdir
 
        $ui_diff conf -state normal
        $ui_diff delete 0.0 end
        $ui_diff conf -state disabled
 
        set current_diff_path {}
+       set current_diff_header {}
 
        $ui_index tag remove in_diff 0.0 end
        $ui_workdir tag remove in_diff 0.0 end
@@ -599,7 +602,7 @@ proc show_diff {path w {lno {}}} {
        global file_states file_lists
        global is_3way_diff diff_active repo_config
        global ui_diff ui_status_value ui_index ui_workdir
-       global current_diff_path current_diff_side
+       global current_diff_path current_diff_side current_diff_header
 
        if {$diff_active || ![lock_index read]} return
 
@@ -623,6 +626,7 @@ proc show_diff {path w {lno {}}} {
        set diff_active 1
        set current_diff_path $path
        set current_diff_side $w
+       set current_diff_header {}
        set ui_status_value "Loading diff of [escape_path $path]..."
 
        # - Git won't give us the diff, there's nothing to compare to!
@@ -707,22 +711,30 @@ proc show_diff {path w {lno {}}} {
                return
        }
 
-       fconfigure $fd -blocking 0 -translation auto
+       fconfigure $fd \
+               -blocking 0 \
+               -encoding binary \
+               -translation binary
        fileevent $fd readable [list read_diff $fd]
 }
 
 proc read_diff {fd} {
-       global ui_diff ui_status_value is_3way_diff diff_active
+       global ui_diff ui_status_value diff_active
+       global is_3way_diff current_diff_header
 
        $ui_diff conf -state normal
        while {[gets $fd line] >= 0} {
                # -- Cleanup uninteresting diff header lines.
                #
-               if {[string match {diff --git *}      $line]} continue
-               if {[string match {diff --cc *}       $line]} continue
-               if {[string match {diff --combined *} $line]} continue
-               if {[string match {--- *}             $line]} continue
-               if {[string match {+++ *}             $line]} continue
+               if {   [string match {diff --git *}      $line]
+                       || [string match {diff --cc *}       $line]
+                       || [string match {diff --combined *} $line]
+                       || [string match {--- *}             $line]
+                       || [string match {+++ *}             $line]} {
+                       append current_diff_header $line "\n"
+                       continue
+               }
+               if {[string match {index *} $line]} continue
                if {$line eq {deleted file mode 120000}} {
                        set line "deleted symlink"
                }
@@ -731,8 +743,7 @@ proc read_diff {fd} {
                #
                if {[string match {@@@ *} $line]} {set is_3way_diff 1}
 
-               if {[string match {index *} $line]
-                       || [string match {mode *} $line]
+               if {[string match {mode *} $line]
                        || [string match {new file *} $line]
                        || [string match {deleted file *} $line]
                        || [string match {Binary files * and * differ} $line]
@@ -783,6 +794,9 @@ proc read_diff {fd} {
                        }
                }
                $ui_diff insert end $line $tags
+               if {[string index $line end] eq "\r"} {
+                       $ui_diff tag add d_cr {end - 2c}
+               }
                $ui_diff insert end "\n" $tags
        }
        $ui_diff conf -state disabled
@@ -799,6 +813,77 @@ proc read_diff {fd} {
        }
 }
 
+proc apply_hunk {x y} {
+       global current_diff_path current_diff_header current_diff_side
+       global ui_diff ui_index file_states
+
+       if {$current_diff_path eq {} || $current_diff_header eq {}} return
+       if {![lock_index apply_hunk]} return
+
+       set apply_cmd {git apply --cached --whitespace=nowarn}
+       set mi [lindex $file_states($current_diff_path) 0]
+       if {$current_diff_side eq $ui_index} {
+               set mode unstage
+               lappend apply_cmd --reverse
+               if {[string index $mi 0] ne {M}} {
+                       unlock_index
+                       return
+               }
+       } else {
+               set mode stage
+               if {[string index $mi 1] ne {M}} {
+                       unlock_index
+                       return
+               }
+       }
+
+       set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
+       set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
+       if {$s_lno eq {}} {
+               unlock_index
+               return
+       }
+
+       set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
+       if {$e_lno eq {}} {
+               set e_lno end
+       }
+
+       if {[catch {
+               set p [open "| $apply_cmd" w]
+               fconfigure $p -translation binary -encoding binary
+               puts -nonewline $p $current_diff_header
+               puts -nonewline $p [$ui_diff get $s_lno $e_lno]
+               close $p} err]} {
+               error_popup "Failed to $mode selected hunk.\n\n$err"
+               unlock_index
+               return
+       }
+
+       $ui_diff conf -state normal
+       $ui_diff delete $s_lno $e_lno
+       $ui_diff conf -state disabled
+
+       if {[$ui_diff get 1.0 end] eq "\n"} {
+               set o _
+       } else {
+               set o ?
+       }
+
+       if {$current_diff_side eq $ui_index} {
+               set mi ${o}M
+       } elseif {[string index $mi 0] eq {_}} {
+               set mi M$o
+       } else {
+               set mi ?$o
+       }
+       unlock_index
+       display_file $current_diff_path $mi
+       if {$o eq {_}} {
+               clear_diff
+       }
+}
+
 ######################################################################
 ##
 ## commit
@@ -962,6 +1047,7 @@ You must add at least 1 file before you can commit.
        # -- A message is required.
        #
        set msg [string trim [$ui_comm get 1.0 end]]
+       regsub -all -line {[ \t\r]+$} $msg {} msg
        if {$msg eq {}} {
                error_popup {Please supply a commit message.
 
@@ -1904,7 +1990,7 @@ proc do_create_branch {} {
                -width 40 \
                -font font_ui
        $w.desc.name_t insert 0.0 $repo_config(gui.newbranchtemplate)
-       grid $w.desc.name_l $w.desc.name_t -stick we -padx {0 5}
+       grid $w.desc.name_l $w.desc.name_t -sticky we -padx {0 5}
        bind $w.desc.name_t <Shift-Key-Tab> {focus [tk_focusPrev %W];break}
        bind $w.desc.name_t <Key-Tab> {focus [tk_focusNext %W];break}
        bind $w.desc.name_t <Key-Return> "do_create_branch_action $w;break"
@@ -1954,7 +2040,7 @@ proc do_create_branch {} {
                -height 1 \
                -width 50 \
                -font font_ui
-       grid $w.from.exp_r $w.from.exp_t -stick we -padx {0 5}
+       grid $w.from.exp_r $w.from.exp_t -sticky we -padx {0 5}
        bind $w.from.exp_t <Shift-Key-Tab> {focus [tk_focusPrev %W];break}
        bind $w.from.exp_t <Key-Tab> {focus [tk_focusNext %W];break}
        bind $w.from.exp_t <Key-Return> "do_create_branch_action $w;break"
@@ -2155,16 +2241,11 @@ proc do_delete_branch {} {
        tkwait window $w
 }
 
-proc switch_branch {b} {
-       global HEAD commit_type file_states current_branch
-       global selected_commit_type ui_comm
+proc switch_branch {new_branch} {
+       global HEAD commit_type current_branch repo_config
 
        if {![lock_index switch]} return
 
-       # -- Backup the selected branch (repository_state resets it)
-       #
-       set new_branch $current_branch
-
        # -- Our in memory state should match the repository.
        #
        repository_state curType curHEAD curMERGE_HEAD
@@ -2185,19 +2266,110 @@ The rescan will be automatically started now.
                return
        }
 
-       # -- Toss the message buffer if we are in amend mode.
+       if {$repo_config(gui.trustmtime) eq {true}} {
+               switch_branch_stage2 {} $new_branch
+       } else {
+               set ui_status_value {Refreshing file status...}
+               set cmd [list git update-index]
+               lappend cmd -q
+               lappend cmd --unmerged
+               lappend cmd --ignore-missing
+               lappend cmd --refresh
+               set fd_rf [open "| $cmd" r]
+               fconfigure $fd_rf -blocking 0 -translation binary
+               fileevent $fd_rf readable \
+                       [list switch_branch_stage2 $fd_rf $new_branch]
+       }
+}
+
+proc switch_branch_stage2 {fd_rf new_branch} {
+       global ui_status_value HEAD
+
+       if {$fd_rf ne {}} {
+               read $fd_rf
+               if {![eof $fd_rf]} return
+               close $fd_rf
+       }
+
+       set ui_status_value "Updating working directory to '$new_branch'..."
+       set cmd [list git read-tree]
+       lappend cmd -m
+       lappend cmd -u
+       lappend cmd --exclude-per-directory=.gitignore
+       lappend cmd $HEAD
+       lappend cmd $new_branch
+       set fd_rt [open "| $cmd" r]
+       fconfigure $fd_rt -blocking 0 -translation binary
+       fileevent $fd_rt readable \
+               [list switch_branch_readtree_wait $fd_rt $new_branch]
+}
+
+proc switch_branch_readtree_wait {fd_rt new_branch} {
+       global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
+       global current_branch
+       global ui_comm ui_status_value
+
+       # -- We never get interesting output on stdout; only stderr.
        #
-       if {[string match amend* $curType]} {
-               $ui_comm delete 0.0 end
-               $ui_comm edit reset
-               $ui_comm edit modified false
+       read $fd_rt
+       fconfigure $fd_rt -blocking 1
+       if {![eof $fd_rt]} {
+               fconfigure $fd_rt -blocking 0
+               return
        }
 
-       set selected_commit_type new
-       set current_branch $new_branch
+       # -- The working directory wasn't in sync with the index and
+       #    we'd have to overwrite something to make the switch. A
+       #    merge is required.
+       #
+       if {[catch {close $fd_rt} err]} {
+               regsub {^fatal: } $err {} err
+               warn_popup "File level merge required.
+
+$err
+
+Staying on branch '$current_branch'."
+               set ui_status_value "Aborted checkout of '$new_branch' (file level merging is required)."
+               unlock_index
+               return
+       }
 
+       # -- Update the symbolic ref.  Core git doesn't even check for failure
+       #    here, it Just Works(tm).  If it doesn't we are in some really ugly
+       #    state that is difficult to recover from within git-gui.
+       #
+       if {[catch {exec git symbolic-ref HEAD "refs/heads/$new_branch"} err]} {
+               error_popup "Failed to set current branch.
+
+This working directory is only partially switched.
+We successfully updated your files, but failed to
+update an internal Git file.
+
+This should not have occurred.  [appname] will now
+close and give up.
+
+$err"
+               do_quit
+               return
+       }
+
+       # -- Update our repository state.  If we were previously in amend mode
+       #    we need to toss the current buffer and do a full rescan to update
+       #    our file lists.  If we weren't in amend mode our file lists are
+       #    accurate and we can avoid the rescan.
+       #
        unlock_index
-       error "NOT FINISHED"
+       set selected_commit_type new
+       if {[string match amend* $commit_type]} {
+               $ui_comm delete 0.0 end
+               $ui_comm edit reset
+               $ui_comm edit modified false
+               rescan {set ui_status_value "Checked out branch '$current_branch'."}
+       } else {
+               repository_state commit_type HEAD MERGE_HEAD
+               set PARENT $HEAD
+               set ui_status_value "Checked out branch '$current_branch'."
+       }
 }
 
 ######################################################################
@@ -2719,6 +2891,74 @@ proc do_gitk {revs} {
        }
 }
 
+proc do_stats {} {
+       set fd [open "| git count-objects -v" r]
+       while {[gets $fd line] > 0} {
+               if {[regexp {^([^:]+): (\d+)$} $line _ name value]} {
+                       set stats($name) $value
+               }
+       }
+       close $fd
+
+       set packed_sz 0
+       foreach p [glob -directory [gitdir objects pack] \
+               -type f \
+               -nocomplain -- *] {
+               incr packed_sz [file size $p]
+       }
+       if {$packed_sz > 0} {
+               set stats(size-pack) [expr {$packed_sz / 1024}]
+       }
+
+       set w .stats_view
+       toplevel $w
+       wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
+
+       label $w.header -text {Database Statistics} \
+               -font font_uibold
+       pack $w.header -side top -fill x
+
+       frame $w.buttons -border 1
+       button $w.buttons.close -text Close \
+               -font font_ui \
+               -command [list destroy $w]
+       button $w.buttons.gc -text {Compress Database} \
+               -font font_ui \
+               -command "destroy $w;do_gc"
+       pack $w.buttons.close -side right
+       pack $w.buttons.gc -side left
+       pack $w.buttons -side bottom -fill x -pady 10 -padx 10
+
+       frame $w.stat -borderwidth 1 -relief solid
+       foreach s {
+               {count           {Number of loose objects}}
+               {size            {Disk space used by loose objects} { KiB}}
+               {in-pack         {Number of packed objects}}
+               {packs           {Number of packs}}
+               {size-pack       {Disk space used by packed objects} { KiB}}
+               {prune-packable  {Packed objects waiting for pruning}}
+               {garbage         {Garbage files}}
+               } {
+               set name [lindex $s 0]
+               set label [lindex $s 1]
+               if {[catch {set value $stats($name)}]} continue
+               if {[llength $s] > 2} {
+                       set value "$value[lindex $s 2]"
+               }
+
+               label $w.stat.l_$name -text "$label:" -anchor w -font font_ui
+               label $w.stat.v_$name -text $value -anchor w -font font_ui
+               grid $w.stat.l_$name $w.stat.v_$name -sticky we -padx {0 5}
+       }
+       pack $w.stat
+
+       bind $w <Visibility> "grab $w; focus $w"
+       bind $w <Key-Escape> [list destroy $w]
+       bind $w <Key-Return> [list destroy $w]
+       wm title $w "[appname] ([reponame]): Database Statistics"
+       tkwait window $w
+}
+
 proc do_gc {} {
        set w [new_console {gc} {Compressing the object database}]
        console_exec $w {git gc}
@@ -2746,12 +2986,13 @@ proc do_quit {} {
        #
        set save [gitdir GITGUI_MSG]
        set msg [string trim [$ui_comm get 0.0 end]]
-       if {![string match amend* $commit_type]
-               && [$ui_comm edit modified]
+       regsub -all -line {[ \r\t]+$} $msg {} msg
+       if {(![string match amend* $commit_type]
+               || [$ui_comm edit modified])
                && $msg ne {}} {
                catch {
                        set fd [open $save w]
-                       puts $fd [string trim [$ui_comm get 0.0 end]]
+                       puts -nonewline $fd $msg
                        close $fd
                }
        } else {
@@ -3100,11 +3341,9 @@ proc do_options {} {
        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
 
        labelframe $w.repo -text "[reponame] Repository" \
-               -font font_ui \
-               -relief raised -borderwidth 2
+               -font font_ui
        labelframe $w.global -text {Global (All Repositories)} \
-               -font font_ui \
-               -relief raised -borderwidth 2
+               -font font_ui
        pack $w.repo -side left -fill both -expand 1 -pady 5 -padx 5
        pack $w.global -side right -fill both -expand 1 -pady 5 -padx 5
 
@@ -3542,6 +3781,10 @@ if {![is_MacOSX]} {
 .mbar.repository add separator
 
 if {!$single_commit} {
+       .mbar.repository add command -label {Database Statistics} \
+               -command do_stats \
+               -font font_ui
+
        .mbar.repository add command -label {Compress Database} \
                -command do_gc \
                -font font_ui
@@ -3847,7 +4090,7 @@ frame .vpane.lower.commarea
 frame .vpane.lower.diff -relief sunken -borderwidth 1
 pack .vpane.lower.commarea -side top -fill x
 pack .vpane.lower.diff -side bottom -fill both -expand 1
-.vpane add .vpane.lower -stick nsew
+.vpane add .vpane.lower -sticky nsew
 
 # -- Commit Area Buttons
 #
@@ -3986,6 +4229,7 @@ bind_button3 $ui_comm "tk_popup $ctxm %X %Y"
 # -- Diff Header
 #
 set current_diff_path {}
+set current_diff_side {}
 set diff_actions [list]
 proc trace_current_diff_path {varname args} {
        global current_diff_path diff_actions file_states
@@ -4066,6 +4310,7 @@ pack $ui_diff -side left -fill both -expand 1
 pack .vpane.lower.diff.header -side top -fill x
 pack .vpane.lower.diff.body -side bottom -fill both -expand 1
 
+$ui_diff tag conf d_cr -elide true
 $ui_diff tag conf d_@ -foreground blue -font font_diffbold
 $ui_diff tag conf d_+ -foreground {#00a000}
 $ui_diff tag conf d_- -foreground red
@@ -4105,6 +4350,7 @@ $ctxm add command \
        -label {Refresh} \
        -font font_ui \
        -command reshow_diff
+lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
 $ctxm add command \
        -label {Copy} \
        -font font_ui \
@@ -4125,6 +4371,13 @@ $ctxm add command \
        }
 lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
 $ctxm add separator
+$ctxm add command \
+       -label {Apply/Reverse Hunk} \
+       -font font_ui \
+       -command {apply_hunk $cursorX $cursorY}
+set ui_diff_applyhunk [$ctxm index last]
+lappend diff_actions [list $ctxm entryconf $ui_diff_applyhunk -state]
+$ctxm add separator
 $ctxm add command \
        -label {Decrease Font Size} \
        -font font_ui \
@@ -4156,7 +4409,17 @@ $ctxm add separator
 $ctxm add command -label {Options...} \
        -font font_ui \
        -command do_options
-bind_button3 $ui_diff "tk_popup $ctxm %X %Y"
+bind_button3 $ui_diff "
+       set cursorX %x
+       set cursorY %y
+       if {\$ui_index eq \$current_diff_side} {
+               $ctxm entryconf $ui_diff_applyhunk -label {Unstage Hunk From Commit}
+       } else {
+               $ctxm entryconf $ui_diff_applyhunk -label {Stage Hunk For Commit}
+       }
+       tk_popup $ctxm %X %Y
+"
+unset ui_diff_applyhunk
 
 # -- Status Bar
 #
@@ -4209,6 +4472,7 @@ bind $ui_diff <Key-Up>     {catch {%W yview scroll -1 units};break}
 bind $ui_diff <Key-Down>   {catch {%W yview scroll  1 units};break}
 bind $ui_diff <Key-Left>   {catch {%W xview scroll -1 units};break}
 bind $ui_diff <Key-Right>  {catch {%W xview scroll  1 units};break}
+bind $ui_diff <Button-1>   {focus %W}
 
 if {!$single_commit} {
        bind . <$M1B-Key-n> do_create_branch