git-gui.shon commit git-gui: Replace \ with \\ when showing paths. (42b922f)
   1#!/bin/sh
   2# Tcl ignores the next line -*- tcl -*- \
   3exec wish "$0" -- "$@"
   4
   5set appvers {@@GIT_VERSION@@}
   6set copyright {
   7Copyright © 2006, 2007 Shawn Pearce, Paul Mackerras.
   8
   9This program is free software; you can redistribute it and/or modify
  10it under the terms of the GNU General Public License as published by
  11the Free Software Foundation; either version 2 of the License, or
  12(at your option) any later version.
  13
  14This program is distributed in the hope that it will be useful,
  15but WITHOUT ANY WARRANTY; without even the implied warranty of
  16MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  17GNU General Public License for more details.
  18
  19You should have received a copy of the GNU General Public License
  20along with this program; if not, write to the Free Software
  21Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA}
  22
  23######################################################################
  24##
  25## read only globals
  26
  27set _appname [lindex [file split $argv0] end]
  28set _gitdir {}
  29set _gitexec {}
  30set _reponame {}
  31set _iscygwin {}
  32
  33proc appname {} {
  34        global _appname
  35        return $_appname
  36}
  37
  38proc gitdir {args} {
  39        global _gitdir
  40        if {$args eq {}} {
  41                return $_gitdir
  42        }
  43        return [eval [concat [list file join $_gitdir] $args]]
  44}
  45
  46proc gitexec {args} {
  47        global _gitexec
  48        if {$_gitexec eq {}} {
  49                if {[catch {set _gitexec [exec git --exec-path]} err]} {
  50                        error "Git not installed?\n\n$err"
  51                }
  52        }
  53        if {$args eq {}} {
  54                return $_gitexec
  55        }
  56        return [eval [concat [list file join $_gitexec] $args]]
  57}
  58
  59proc reponame {} {
  60        global _reponame
  61        return $_reponame
  62}
  63
  64proc is_MacOSX {} {
  65        global tcl_platform tk_library
  66        if {[tk windowingsystem] eq {aqua}} {
  67                return 1
  68        }
  69        return 0
  70}
  71
  72proc is_Windows {} {
  73        global tcl_platform
  74        if {$tcl_platform(platform) eq {windows}} {
  75                return 1
  76        }
  77        return 0
  78}
  79
  80proc is_Cygwin {} {
  81        global tcl_platform _iscygwin
  82        if {$_iscygwin eq {}} {
  83                if {$tcl_platform(platform) eq {windows}} {
  84                        if {[catch {set p [exec cygpath --windir]} err]} {
  85                                set _iscygwin 0
  86                        } else {
  87                                set _iscygwin 1
  88                        }
  89                } else {
  90                        set _iscygwin 0
  91                }
  92        }
  93        return $_iscygwin
  94}
  95
  96######################################################################
  97##
  98## config
  99
 100proc is_many_config {name} {
 101        switch -glob -- $name {
 102        remote.*.fetch -
 103        remote.*.push
 104                {return 1}
 105        *
 106                {return 0}
 107        }
 108}
 109
 110proc is_config_true {name} {
 111        global repo_config
 112        if {[catch {set v $repo_config($name)}]} {
 113                return 0
 114        } elseif {$v eq {true} || $v eq {1} || $v eq {yes}} {
 115                return 1
 116        } else {
 117                return 0
 118        }
 119}
 120
 121proc load_config {include_global} {
 122        global repo_config global_config default_config
 123
 124        array unset global_config
 125        if {$include_global} {
 126                catch {
 127                        set fd_rc [open "| git repo-config --global --list" r]
 128                        while {[gets $fd_rc line] >= 0} {
 129                                if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
 130                                        if {[is_many_config $name]} {
 131                                                lappend global_config($name) $value
 132                                        } else {
 133                                                set global_config($name) $value
 134                                        }
 135                                }
 136                        }
 137                        close $fd_rc
 138                }
 139        }
 140
 141        array unset repo_config
 142        catch {
 143                set fd_rc [open "| git repo-config --list" r]
 144                while {[gets $fd_rc line] >= 0} {
 145                        if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
 146                                if {[is_many_config $name]} {
 147                                        lappend repo_config($name) $value
 148                                } else {
 149                                        set repo_config($name) $value
 150                                }
 151                        }
 152                }
 153                close $fd_rc
 154        }
 155
 156        foreach name [array names default_config] {
 157                if {[catch {set v $global_config($name)}]} {
 158                        set global_config($name) $default_config($name)
 159                }
 160                if {[catch {set v $repo_config($name)}]} {
 161                        set repo_config($name) $default_config($name)
 162                }
 163        }
 164}
 165
 166proc save_config {} {
 167        global default_config font_descs
 168        global repo_config global_config
 169        global repo_config_new global_config_new
 170
 171        foreach option $font_descs {
 172                set name [lindex $option 0]
 173                set font [lindex $option 1]
 174                font configure $font \
 175                        -family $global_config_new(gui.$font^^family) \
 176                        -size $global_config_new(gui.$font^^size)
 177                font configure ${font}bold \
 178                        -family $global_config_new(gui.$font^^family) \
 179                        -size $global_config_new(gui.$font^^size)
 180                set global_config_new(gui.$name) [font configure $font]
 181                unset global_config_new(gui.$font^^family)
 182                unset global_config_new(gui.$font^^size)
 183        }
 184
 185        foreach name [array names default_config] {
 186                set value $global_config_new($name)
 187                if {$value ne $global_config($name)} {
 188                        if {$value eq $default_config($name)} {
 189                                catch {exec git repo-config --global --unset $name}
 190                        } else {
 191                                regsub -all "\[{}\]" $value {"} value
 192                                exec git repo-config --global $name $value
 193                        }
 194                        set global_config($name) $value
 195                        if {$value eq $repo_config($name)} {
 196                                catch {exec git repo-config --unset $name}
 197                                set repo_config($name) $value
 198                        }
 199                }
 200        }
 201
 202        foreach name [array names default_config] {
 203                set value $repo_config_new($name)
 204                if {$value ne $repo_config($name)} {
 205                        if {$value eq $global_config($name)} {
 206                                catch {exec git repo-config --unset $name}
 207                        } else {
 208                                regsub -all "\[{}\]" $value {"} value
 209                                exec git repo-config $name $value
 210                        }
 211                        set repo_config($name) $value
 212                }
 213        }
 214}
 215
 216proc error_popup {msg} {
 217        set title [appname]
 218        if {[reponame] ne {}} {
 219                append title " ([reponame])"
 220        }
 221        set cmd [list tk_messageBox \
 222                -icon error \
 223                -type ok \
 224                -title "$title: error" \
 225                -message $msg]
 226        if {[winfo ismapped .]} {
 227                lappend cmd -parent .
 228        }
 229        eval $cmd
 230}
 231
 232proc warn_popup {msg} {
 233        set title [appname]
 234        if {[reponame] ne {}} {
 235                append title " ([reponame])"
 236        }
 237        set cmd [list tk_messageBox \
 238                -icon warning \
 239                -type ok \
 240                -title "$title: warning" \
 241                -message $msg]
 242        if {[winfo ismapped .]} {
 243                lappend cmd -parent .
 244        }
 245        eval $cmd
 246}
 247
 248proc info_popup {msg {parent .}} {
 249        set title [appname]
 250        if {[reponame] ne {}} {
 251                append title " ([reponame])"
 252        }
 253        tk_messageBox \
 254                -parent $parent \
 255                -icon info \
 256                -type ok \
 257                -title $title \
 258                -message $msg
 259}
 260
 261proc ask_popup {msg} {
 262        set title [appname]
 263        if {[reponame] ne {}} {
 264                append title " ([reponame])"
 265        }
 266        return [tk_messageBox \
 267                -parent . \
 268                -icon question \
 269                -type yesno \
 270                -title $title \
 271                -message $msg]
 272}
 273
 274######################################################################
 275##
 276## repository setup
 277
 278if {   [catch {set _gitdir $env(GIT_DIR)}]
 279        && [catch {set _gitdir [exec git rev-parse --git-dir]} err]} {
 280        catch {wm withdraw .}
 281        error_popup "Cannot find the git directory:\n\n$err"
 282        exit 1
 283}
 284if {![file isdirectory $_gitdir] && [is_Cygwin]} {
 285        catch {set _gitdir [exec cygpath --unix $_gitdir]}
 286}
 287if {![file isdirectory $_gitdir]} {
 288        catch {wm withdraw .}
 289        error_popup "Git directory not found:\n\n$_gitdir"
 290        exit 1
 291}
 292if {[lindex [file split $_gitdir] end] ne {.git}} {
 293        catch {wm withdraw .}
 294        error_popup "Cannot use funny .git directory:\n\n$_gitdir"
 295        exit 1
 296}
 297if {[catch {cd [file dirname $_gitdir]} err]} {
 298        catch {wm withdraw .}
 299        error_popup "No working directory [file dirname $_gitdir]:\n\n$err"
 300        exit 1
 301}
 302set _reponame [lindex [file split \
 303        [file normalize [file dirname $_gitdir]]] \
 304        end]
 305
 306set single_commit 0
 307if {[appname] eq {git-citool}} {
 308        set single_commit 1
 309}
 310
 311######################################################################
 312##
 313## task management
 314
 315set rescan_active 0
 316set diff_active 0
 317set last_clicked {}
 318
 319set disable_on_lock [list]
 320set index_lock_type none
 321
 322proc lock_index {type} {
 323        global index_lock_type disable_on_lock
 324
 325        if {$index_lock_type eq {none}} {
 326                set index_lock_type $type
 327                foreach w $disable_on_lock {
 328                        uplevel #0 $w disabled
 329                }
 330                return 1
 331        } elseif {$index_lock_type eq "begin-$type"} {
 332                set index_lock_type $type
 333                return 1
 334        }
 335        return 0
 336}
 337
 338proc unlock_index {} {
 339        global index_lock_type disable_on_lock
 340
 341        set index_lock_type none
 342        foreach w $disable_on_lock {
 343                uplevel #0 $w normal
 344        }
 345}
 346
 347######################################################################
 348##
 349## status
 350
 351proc repository_state {ctvar hdvar mhvar} {
 352        global current_branch
 353        upvar $ctvar ct $hdvar hd $mhvar mh
 354
 355        set mh [list]
 356
 357        if {[catch {set current_branch [exec git symbolic-ref HEAD]}]} {
 358                set current_branch {}
 359        } else {
 360                regsub ^refs/((heads|tags|remotes)/)? \
 361                        $current_branch \
 362                        {} \
 363                        current_branch
 364        }
 365
 366        if {[catch {set hd [exec git rev-parse --verify HEAD]}]} {
 367                set hd {}
 368                set ct initial
 369                return
 370        }
 371
 372        set merge_head [gitdir MERGE_HEAD]
 373        if {[file exists $merge_head]} {
 374                set ct merge
 375                set fd_mh [open $merge_head r]
 376                while {[gets $fd_mh line] >= 0} {
 377                        lappend mh $line
 378                }
 379                close $fd_mh
 380                return
 381        }
 382
 383        set ct normal
 384}
 385
 386proc PARENT {} {
 387        global PARENT empty_tree
 388
 389        set p [lindex $PARENT 0]
 390        if {$p ne {}} {
 391                return $p
 392        }
 393        if {$empty_tree eq {}} {
 394                set empty_tree [exec git mktree << {}]
 395        }
 396        return $empty_tree
 397}
 398
 399proc rescan {after {honor_trustmtime 1}} {
 400        global HEAD PARENT MERGE_HEAD commit_type
 401        global ui_index ui_workdir ui_status_value ui_comm
 402        global rescan_active file_states
 403        global repo_config single_commit
 404
 405        if {$rescan_active > 0 || ![lock_index read]} return
 406
 407        repository_state newType newHEAD newMERGE_HEAD
 408        if {[string match amend* $commit_type]
 409                && $newType eq {normal}
 410                && $newHEAD eq $HEAD} {
 411        } else {
 412                set HEAD $newHEAD
 413                set PARENT $newHEAD
 414                set MERGE_HEAD $newMERGE_HEAD
 415                set commit_type $newType
 416        }
 417
 418        array unset file_states
 419
 420        if {![$ui_comm edit modified]
 421                || [string trim [$ui_comm get 0.0 end]] eq {}} {
 422                if {[load_message GITGUI_MSG]} {
 423                } elseif {[load_message MERGE_MSG]} {
 424                } elseif {[load_message SQUASH_MSG]} {
 425                }
 426                $ui_comm edit reset
 427                $ui_comm edit modified false
 428        }
 429
 430        if {!$single_commit} {
 431                load_all_heads
 432                populate_branch_menu
 433        }
 434
 435        if {$honor_trustmtime && $repo_config(gui.trustmtime) eq {true}} {
 436                rescan_stage2 {} $after
 437        } else {
 438                set rescan_active 1
 439                set ui_status_value {Refreshing file status...}
 440                set cmd [list git update-index]
 441                lappend cmd -q
 442                lappend cmd --unmerged
 443                lappend cmd --ignore-missing
 444                lappend cmd --refresh
 445                set fd_rf [open "| $cmd" r]
 446                fconfigure $fd_rf -blocking 0 -translation binary
 447                fileevent $fd_rf readable \
 448                        [list rescan_stage2 $fd_rf $after]
 449        }
 450}
 451
 452proc rescan_stage2 {fd after} {
 453        global ui_status_value
 454        global rescan_active buf_rdi buf_rdf buf_rlo
 455
 456        if {$fd ne {}} {
 457                read $fd
 458                if {![eof $fd]} return
 459                close $fd
 460        }
 461
 462        set ls_others [list | git ls-files --others -z \
 463                --exclude-per-directory=.gitignore]
 464        set info_exclude [gitdir info exclude]
 465        if {[file readable $info_exclude]} {
 466                lappend ls_others "--exclude-from=$info_exclude"
 467        }
 468
 469        set buf_rdi {}
 470        set buf_rdf {}
 471        set buf_rlo {}
 472
 473        set rescan_active 3
 474        set ui_status_value {Scanning for modified files ...}
 475        set fd_di [open "| git diff-index --cached -z [PARENT]" r]
 476        set fd_df [open "| git diff-files -z" r]
 477        set fd_lo [open $ls_others r]
 478
 479        fconfigure $fd_di -blocking 0 -translation binary -encoding binary
 480        fconfigure $fd_df -blocking 0 -translation binary -encoding binary
 481        fconfigure $fd_lo -blocking 0 -translation binary -encoding binary
 482        fileevent $fd_di readable [list read_diff_index $fd_di $after]
 483        fileevent $fd_df readable [list read_diff_files $fd_df $after]
 484        fileevent $fd_lo readable [list read_ls_others $fd_lo $after]
 485}
 486
 487proc load_message {file} {
 488        global ui_comm
 489
 490        set f [gitdir $file]
 491        if {[file isfile $f]} {
 492                if {[catch {set fd [open $f r]}]} {
 493                        return 0
 494                }
 495                set content [string trim [read $fd]]
 496                close $fd
 497                regsub -all -line {[ \r\t]+$} $content {} content
 498                $ui_comm delete 0.0 end
 499                $ui_comm insert end $content
 500                return 1
 501        }
 502        return 0
 503}
 504
 505proc read_diff_index {fd after} {
 506        global buf_rdi
 507
 508        append buf_rdi [read $fd]
 509        set c 0
 510        set n [string length $buf_rdi]
 511        while {$c < $n} {
 512                set z1 [string first "\0" $buf_rdi $c]
 513                if {$z1 == -1} break
 514                incr z1
 515                set z2 [string first "\0" $buf_rdi $z1]
 516                if {$z2 == -1} break
 517
 518                incr c
 519                set i [split [string range $buf_rdi $c [expr {$z1 - 2}]] { }]
 520                set p [string range $buf_rdi $z1 [expr {$z2 - 1}]]
 521                merge_state \
 522                        [encoding convertfrom $p] \
 523                        [lindex $i 4]? \
 524                        [list [lindex $i 0] [lindex $i 2]] \
 525                        [list]
 526                set c $z2
 527                incr c
 528        }
 529        if {$c < $n} {
 530                set buf_rdi [string range $buf_rdi $c end]
 531        } else {
 532                set buf_rdi {}
 533        }
 534
 535        rescan_done $fd buf_rdi $after
 536}
 537
 538proc read_diff_files {fd after} {
 539        global buf_rdf
 540
 541        append buf_rdf [read $fd]
 542        set c 0
 543        set n [string length $buf_rdf]
 544        while {$c < $n} {
 545                set z1 [string first "\0" $buf_rdf $c]
 546                if {$z1 == -1} break
 547                incr z1
 548                set z2 [string first "\0" $buf_rdf $z1]
 549                if {$z2 == -1} break
 550
 551                incr c
 552                set i [split [string range $buf_rdf $c [expr {$z1 - 2}]] { }]
 553                set p [string range $buf_rdf $z1 [expr {$z2 - 1}]]
 554                merge_state \
 555                        [encoding convertfrom $p] \
 556                        ?[lindex $i 4] \
 557                        [list] \
 558                        [list [lindex $i 0] [lindex $i 2]]
 559                set c $z2
 560                incr c
 561        }
 562        if {$c < $n} {
 563                set buf_rdf [string range $buf_rdf $c end]
 564        } else {
 565                set buf_rdf {}
 566        }
 567
 568        rescan_done $fd buf_rdf $after
 569}
 570
 571proc read_ls_others {fd after} {
 572        global buf_rlo
 573
 574        append buf_rlo [read $fd]
 575        set pck [split $buf_rlo "\0"]
 576        set buf_rlo [lindex $pck end]
 577        foreach p [lrange $pck 0 end-1] {
 578                merge_state [encoding convertfrom $p] ?O
 579        }
 580        rescan_done $fd buf_rlo $after
 581}
 582
 583proc rescan_done {fd buf after} {
 584        global rescan_active
 585        global file_states repo_config
 586        upvar $buf to_clear
 587
 588        if {![eof $fd]} return
 589        set to_clear {}
 590        close $fd
 591        if {[incr rescan_active -1] > 0} return
 592
 593        prune_selection
 594        unlock_index
 595        display_all_files
 596        reshow_diff
 597        uplevel #0 $after
 598}
 599
 600proc prune_selection {} {
 601        global file_states selected_paths
 602
 603        foreach path [array names selected_paths] {
 604                if {[catch {set still_here $file_states($path)}]} {
 605                        unset selected_paths($path)
 606                }
 607        }
 608}
 609
 610######################################################################
 611##
 612## diff
 613
 614proc clear_diff {} {
 615        global ui_diff current_diff_path current_diff_header
 616        global ui_index ui_workdir
 617
 618        $ui_diff conf -state normal
 619        $ui_diff delete 0.0 end
 620        $ui_diff conf -state disabled
 621
 622        set current_diff_path {}
 623        set current_diff_header {}
 624
 625        $ui_index tag remove in_diff 0.0 end
 626        $ui_workdir tag remove in_diff 0.0 end
 627}
 628
 629proc reshow_diff {} {
 630        global ui_status_value file_states file_lists
 631        global current_diff_path current_diff_side
 632
 633        set p $current_diff_path
 634        if {$p eq {}
 635                || $current_diff_side eq {}
 636                || [catch {set s $file_states($p)}]
 637                || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} {
 638                clear_diff
 639        } else {
 640                show_diff $p $current_diff_side
 641        }
 642}
 643
 644proc handle_empty_diff {} {
 645        global current_diff_path file_states file_lists
 646
 647        set path $current_diff_path
 648        set s $file_states($path)
 649        if {[lindex $s 0] ne {_M}} return
 650
 651        info_popup "No differences detected.
 652
 653[short_path $path] has no changes.
 654
 655The modification date of this file was updated
 656by another application, but the content within
 657the file was not changed.
 658
 659A rescan will be automatically started to find
 660other files which may have the same state."
 661
 662        clear_diff
 663        display_file $path __
 664        rescan {set ui_status_value {Ready.}} 0
 665}
 666
 667proc show_diff {path w {lno {}}} {
 668        global file_states file_lists
 669        global is_3way_diff diff_active repo_config
 670        global ui_diff ui_status_value ui_index ui_workdir
 671        global current_diff_path current_diff_side current_diff_header
 672
 673        if {$diff_active || ![lock_index read]} return
 674
 675        clear_diff
 676        if {$lno == {}} {
 677                set lno [lsearch -sorted -exact $file_lists($w) $path]
 678                if {$lno >= 0} {
 679                        incr lno
 680                }
 681        }
 682        if {$lno >= 1} {
 683                $w tag add in_diff $lno.0 [expr {$lno + 1}].0
 684        }
 685
 686        set s $file_states($path)
 687        set m [lindex $s 0]
 688        set is_3way_diff 0
 689        set diff_active 1
 690        set current_diff_path $path
 691        set current_diff_side $w
 692        set current_diff_header {}
 693        set ui_status_value "Loading diff of [escape_path $path]..."
 694
 695        # - Git won't give us the diff, there's nothing to compare to!
 696        #
 697        if {$m eq {_O}} {
 698                set max_sz [expr {128 * 1024}]
 699                if {[catch {
 700                                set fd [open $path r]
 701                                set content [read $fd $max_sz]
 702                                close $fd
 703                                set sz [file size $path]
 704                        } err ]} {
 705                        set diff_active 0
 706                        unlock_index
 707                        set ui_status_value "Unable to display [escape_path $path]"
 708                        error_popup "Error loading file:\n\n$err"
 709                        return
 710                }
 711                $ui_diff conf -state normal
 712                if {![catch {set type [exec file $path]}]} {
 713                        set n [string length $path]
 714                        if {[string equal -length $n $path $type]} {
 715                                set type [string range $type $n end]
 716                                regsub {^:?\s*} $type {} type
 717                        }
 718                        $ui_diff insert end "* $type\n" d_@
 719                }
 720                if {[string first "\0" $content] != -1} {
 721                        $ui_diff insert end \
 722                                "* Binary file (not showing content)." \
 723                                d_@
 724                } else {
 725                        if {$sz > $max_sz} {
 726                                $ui_diff insert end \
 727"* Untracked file is $sz bytes.
 728* Showing only first $max_sz bytes.
 729" d_@
 730                        }
 731                        $ui_diff insert end $content
 732                        if {$sz > $max_sz} {
 733                                $ui_diff insert end "
 734* Untracked file clipped here by [appname].
 735* To see the entire file, use an external editor.
 736" d_@
 737                        }
 738                }
 739                $ui_diff conf -state disabled
 740                set diff_active 0
 741                unlock_index
 742                set ui_status_value {Ready.}
 743                return
 744        }
 745
 746        set cmd [list | git]
 747        if {$w eq $ui_index} {
 748                lappend cmd diff-index
 749                lappend cmd --cached
 750        } elseif {$w eq $ui_workdir} {
 751                if {[string index $m 0] eq {U}} {
 752                        lappend cmd diff
 753                } else {
 754                        lappend cmd diff-files
 755                }
 756        }
 757
 758        lappend cmd -p
 759        lappend cmd --no-color
 760        if {$repo_config(gui.diffcontext) > 0} {
 761                lappend cmd "-U$repo_config(gui.diffcontext)"
 762        }
 763        if {$w eq $ui_index} {
 764                lappend cmd [PARENT]
 765        }
 766        lappend cmd --
 767        lappend cmd $path
 768
 769        if {[catch {set fd [open $cmd r]} err]} {
 770                set diff_active 0
 771                unlock_index
 772                set ui_status_value "Unable to display [escape_path $path]"
 773                error_popup "Error loading diff:\n\n$err"
 774                return
 775        }
 776
 777        fconfigure $fd \
 778                -blocking 0 \
 779                -encoding binary \
 780                -translation binary
 781        fileevent $fd readable [list read_diff $fd]
 782}
 783
 784proc read_diff {fd} {
 785        global ui_diff ui_status_value diff_active
 786        global is_3way_diff current_diff_header
 787
 788        $ui_diff conf -state normal
 789        while {[gets $fd line] >= 0} {
 790                # -- Cleanup uninteresting diff header lines.
 791                #
 792                if {   [string match {diff --git *}      $line]
 793                        || [string match {diff --cc *}       $line]
 794                        || [string match {diff --combined *} $line]
 795                        || [string match {--- *}             $line]
 796                        || [string match {+++ *}             $line]} {
 797                        append current_diff_header $line "\n"
 798                        continue
 799                }
 800                if {[string match {index *} $line]} continue
 801                if {$line eq {deleted file mode 120000}} {
 802                        set line "deleted symlink"
 803                }
 804
 805                # -- Automatically detect if this is a 3 way diff.
 806                #
 807                if {[string match {@@@ *} $line]} {set is_3way_diff 1}
 808
 809                if {[string match {mode *} $line]
 810                        || [string match {new file *} $line]
 811                        || [string match {deleted file *} $line]
 812                        || [string match {Binary files * and * differ} $line]
 813                        || $line eq {\ No newline at end of file}
 814                        || [regexp {^\* Unmerged path } $line]} {
 815                        set tags {}
 816                } elseif {$is_3way_diff} {
 817                        set op [string range $line 0 1]
 818                        switch -- $op {
 819                        {  } {set tags {}}
 820                        {@@} {set tags d_@}
 821                        { +} {set tags d_s+}
 822                        { -} {set tags d_s-}
 823                        {+ } {set tags d_+s}
 824                        {- } {set tags d_-s}
 825                        {--} {set tags d_--}
 826                        {++} {
 827                                if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
 828                                        set line [string replace $line 0 1 {  }]
 829                                        set tags d$op
 830                                } else {
 831                                        set tags d_++
 832                                }
 833                        }
 834                        default {
 835                                puts "error: Unhandled 3 way diff marker: {$op}"
 836                                set tags {}
 837                        }
 838                        }
 839                } else {
 840                        set op [string index $line 0]
 841                        switch -- $op {
 842                        { } {set tags {}}
 843                        {@} {set tags d_@}
 844                        {-} {set tags d_-}
 845                        {+} {
 846                                if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
 847                                        set line [string replace $line 0 0 { }]
 848                                        set tags d$op
 849                                } else {
 850                                        set tags d_+
 851                                }
 852                        }
 853                        default {
 854                                puts "error: Unhandled 2 way diff marker: {$op}"
 855                                set tags {}
 856                        }
 857                        }
 858                }
 859                $ui_diff insert end $line $tags
 860                if {[string index $line end] eq "\r"} {
 861                        $ui_diff tag add d_cr {end - 2c}
 862                }
 863                $ui_diff insert end "\n" $tags
 864        }
 865        $ui_diff conf -state disabled
 866
 867        if {[eof $fd]} {
 868                close $fd
 869                set diff_active 0
 870                unlock_index
 871                set ui_status_value {Ready.}
 872
 873                if {[$ui_diff index end] eq {2.0}} {
 874                        handle_empty_diff
 875                }
 876        }
 877}
 878
 879proc apply_hunk {x y} {
 880        global current_diff_path current_diff_header current_diff_side
 881        global ui_diff ui_index file_states
 882
 883        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 884        if {![lock_index apply_hunk]} return
 885
 886        set apply_cmd {git apply --cached --whitespace=nowarn}
 887        set mi [lindex $file_states($current_diff_path) 0]
 888        if {$current_diff_side eq $ui_index} {
 889                set mode unstage
 890                lappend apply_cmd --reverse
 891                if {[string index $mi 0] ne {M}} {
 892                        unlock_index
 893                        return
 894                }
 895        } else {
 896                set mode stage
 897                if {[string index $mi 1] ne {M}} {
 898                        unlock_index
 899                        return
 900                }
 901        }
 902
 903        set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
 904        set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
 905        if {$s_lno eq {}} {
 906                unlock_index
 907                return
 908        }
 909
 910        set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
 911        if {$e_lno eq {}} {
 912                set e_lno end
 913        }
 914
 915        if {[catch {
 916                set p [open "| $apply_cmd" w]
 917                fconfigure $p -translation binary -encoding binary
 918                puts -nonewline $p $current_diff_header
 919                puts -nonewline $p [$ui_diff get $s_lno $e_lno]
 920                close $p} err]} {
 921                error_popup "Failed to $mode selected hunk.\n\n$err"
 922                unlock_index
 923                return
 924        }
 925
 926        $ui_diff conf -state normal
 927        $ui_diff delete $s_lno $e_lno
 928        $ui_diff conf -state disabled
 929
 930        if {[$ui_diff get 1.0 end] eq "\n"} {
 931                set o _
 932        } else {
 933                set o ?
 934        }
 935
 936        if {$current_diff_side eq $ui_index} {
 937                set mi ${o}M
 938        } elseif {[string index $mi 0] eq {_}} {
 939                set mi M$o
 940        } else {
 941                set mi ?$o
 942        }
 943        unlock_index
 944        display_file $current_diff_path $mi
 945        if {$o eq {_}} {
 946                clear_diff
 947        }
 948}
 949
 950######################################################################
 951##
 952## commit
 953
 954proc load_last_commit {} {
 955        global HEAD PARENT MERGE_HEAD commit_type ui_comm
 956        global repo_config
 957
 958        if {[llength $PARENT] == 0} {
 959                error_popup {There is nothing to amend.
 960
 961You are about to create the initial commit.
 962There is no commit before this to amend.
 963}
 964                return
 965        }
 966
 967        repository_state curType curHEAD curMERGE_HEAD
 968        if {$curType eq {merge}} {
 969                error_popup {Cannot amend while merging.
 970
 971You are currently in the middle of a merge that
 972has not been fully completed.  You cannot amend
 973the prior commit unless you first abort the
 974current merge activity.
 975}
 976                return
 977        }
 978
 979        set msg {}
 980        set parents [list]
 981        if {[catch {
 982                        set fd [open "| git cat-file commit $curHEAD" r]
 983                        fconfigure $fd -encoding binary -translation lf
 984                        if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
 985                                set enc utf-8
 986                        }
 987                        while {[gets $fd line] > 0} {
 988                                if {[string match {parent *} $line]} {
 989                                        lappend parents [string range $line 7 end]
 990                                } elseif {[string match {encoding *} $line]} {
 991                                        set enc [string tolower [string range $line 9 end]]
 992                                }
 993                        }
 994                        fconfigure $fd -encoding $enc
 995                        set msg [string trim [read $fd]]
 996                        close $fd
 997                } err]} {
 998                error_popup "Error loading commit data for amend:\n\n$err"
 999                return
1000        }
1001
1002        set HEAD $curHEAD
1003        set PARENT $parents
1004        set MERGE_HEAD [list]
1005        switch -- [llength $parents] {
1006        0       {set commit_type amend-initial}
1007        1       {set commit_type amend}
1008        default {set commit_type amend-merge}
1009        }
1010
1011        $ui_comm delete 0.0 end
1012        $ui_comm insert end $msg
1013        $ui_comm edit reset
1014        $ui_comm edit modified false
1015        rescan {set ui_status_value {Ready.}}
1016}
1017
1018proc create_new_commit {} {
1019        global commit_type ui_comm
1020
1021        set commit_type normal
1022        $ui_comm delete 0.0 end
1023        $ui_comm edit reset
1024        $ui_comm edit modified false
1025        rescan {set ui_status_value {Ready.}}
1026}
1027
1028set GIT_COMMITTER_IDENT {}
1029
1030proc committer_ident {} {
1031        global GIT_COMMITTER_IDENT
1032
1033        if {$GIT_COMMITTER_IDENT eq {}} {
1034                if {[catch {set me [exec git var GIT_COMMITTER_IDENT]} err]} {
1035                        error_popup "Unable to obtain your identity:\n\n$err"
1036                        return {}
1037                }
1038                if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \
1039                        $me me GIT_COMMITTER_IDENT]} {
1040                        error_popup "Invalid GIT_COMMITTER_IDENT:\n\n$me"
1041                        return {}
1042                }
1043        }
1044
1045        return $GIT_COMMITTER_IDENT
1046}
1047
1048proc commit_tree {} {
1049        global HEAD commit_type file_states ui_comm repo_config
1050        global ui_status_value pch_error
1051
1052        if {[committer_ident] eq {}} return
1053        if {![lock_index update]} return
1054
1055        # -- Our in memory state should match the repository.
1056        #
1057        repository_state curType curHEAD curMERGE_HEAD
1058        if {[string match amend* $commit_type]
1059                && $curType eq {normal}
1060                && $curHEAD eq $HEAD} {
1061        } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
1062                info_popup {Last scanned state does not match repository state.
1063
1064Another Git program has modified this repository
1065since the last scan.  A rescan must be performed
1066before another commit can be created.
1067
1068The rescan will be automatically started now.
1069}
1070                unlock_index
1071                rescan {set ui_status_value {Ready.}}
1072                return
1073        }
1074
1075        # -- At least one file should differ in the index.
1076        #
1077        set files_ready 0
1078        foreach path [array names file_states] {
1079                switch -glob -- [lindex $file_states($path) 0] {
1080                _? {continue}
1081                A? -
1082                D? -
1083                M? {set files_ready 1}
1084                U? {
1085                        error_popup "Unmerged files cannot be committed.
1086
1087File [short_path $path] has merge conflicts.
1088You must resolve them and add the file before committing.
1089"
1090                        unlock_index
1091                        return
1092                }
1093                default {
1094                        error_popup "Unknown file state [lindex $s 0] detected.
1095
1096File [short_path $path] cannot be committed by this program.
1097"
1098                }
1099                }
1100        }
1101        if {!$files_ready} {
1102                info_popup {No changes to commit.
1103
1104You must add at least 1 file before you can commit.
1105}
1106                unlock_index
1107                return
1108        }
1109
1110        # -- A message is required.
1111        #
1112        set msg [string trim [$ui_comm get 1.0 end]]
1113        regsub -all -line {[ \t\r]+$} $msg {} msg
1114        if {$msg eq {}} {
1115                error_popup {Please supply a commit message.
1116
1117A good commit message has the following format:
1118
1119- First line: Describe in one sentance what you did.
1120- Second line: Blank
1121- Remaining lines: Describe why this change is good.
1122}
1123                unlock_index
1124                return
1125        }
1126
1127        # -- Run the pre-commit hook.
1128        #
1129        set pchook [gitdir hooks pre-commit]
1130
1131        # On Cygwin [file executable] might lie so we need to ask
1132        # the shell if the hook is executable.  Yes that's annoying.
1133        #
1134        if {[is_Cygwin] && [file isfile $pchook]} {
1135                set pchook [list sh -c [concat \
1136                        "if test -x \"$pchook\";" \
1137                        "then exec \"$pchook\" 2>&1;" \
1138                        "fi"]]
1139        } elseif {[file executable $pchook]} {
1140                set pchook [list $pchook |& cat]
1141        } else {
1142                commit_writetree $curHEAD $msg
1143                return
1144        }
1145
1146        set ui_status_value {Calling pre-commit hook...}
1147        set pch_error {}
1148        set fd_ph [open "| $pchook" r]
1149        fconfigure $fd_ph -blocking 0 -translation binary
1150        fileevent $fd_ph readable \
1151                [list commit_prehook_wait $fd_ph $curHEAD $msg]
1152}
1153
1154proc commit_prehook_wait {fd_ph curHEAD msg} {
1155        global pch_error ui_status_value
1156
1157        append pch_error [read $fd_ph]
1158        fconfigure $fd_ph -blocking 1
1159        if {[eof $fd_ph]} {
1160                if {[catch {close $fd_ph}]} {
1161                        set ui_status_value {Commit declined by pre-commit hook.}
1162                        hook_failed_popup pre-commit $pch_error
1163                        unlock_index
1164                } else {
1165                        commit_writetree $curHEAD $msg
1166                }
1167                set pch_error {}
1168                return
1169        }
1170        fconfigure $fd_ph -blocking 0
1171}
1172
1173proc commit_writetree {curHEAD msg} {
1174        global ui_status_value
1175
1176        set ui_status_value {Committing changes...}
1177        set fd_wt [open "| git write-tree" r]
1178        fileevent $fd_wt readable \
1179                [list commit_committree $fd_wt $curHEAD $msg]
1180}
1181
1182proc commit_committree {fd_wt curHEAD msg} {
1183        global HEAD PARENT MERGE_HEAD commit_type
1184        global single_commit all_heads current_branch
1185        global ui_status_value ui_comm selected_commit_type
1186        global file_states selected_paths rescan_active
1187        global repo_config
1188
1189        gets $fd_wt tree_id
1190        if {$tree_id eq {} || [catch {close $fd_wt} err]} {
1191                error_popup "write-tree failed:\n\n$err"
1192                set ui_status_value {Commit failed.}
1193                unlock_index
1194                return
1195        }
1196
1197        # -- Build the message.
1198        #
1199        set msg_p [gitdir COMMIT_EDITMSG]
1200        set msg_wt [open $msg_p w]
1201        if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
1202                set enc utf-8
1203        }
1204        fconfigure $msg_wt -encoding $enc -translation binary
1205        puts -nonewline $msg_wt $msg
1206        close $msg_wt
1207
1208        # -- Create the commit.
1209        #
1210        set cmd [list git commit-tree $tree_id]
1211        set parents [concat $PARENT $MERGE_HEAD]
1212        if {[llength $parents] > 0} {
1213                foreach p $parents {
1214                        lappend cmd -p $p
1215                }
1216        } else {
1217                # git commit-tree writes to stderr during initial commit.
1218                lappend cmd 2>/dev/null
1219        }
1220        lappend cmd <$msg_p
1221        if {[catch {set cmt_id [eval exec $cmd]} err]} {
1222                error_popup "commit-tree failed:\n\n$err"
1223                set ui_status_value {Commit failed.}
1224                unlock_index
1225                return
1226        }
1227
1228        # -- Update the HEAD ref.
1229        #
1230        set reflogm commit
1231        if {$commit_type ne {normal}} {
1232                append reflogm " ($commit_type)"
1233        }
1234        set i [string first "\n" $msg]
1235        if {$i >= 0} {
1236                append reflogm {: } [string range $msg 0 [expr {$i - 1}]]
1237        } else {
1238                append reflogm {: } $msg
1239        }
1240        set cmd [list git update-ref -m $reflogm HEAD $cmt_id $curHEAD]
1241        if {[catch {eval exec $cmd} err]} {
1242                error_popup "update-ref failed:\n\n$err"
1243                set ui_status_value {Commit failed.}
1244                unlock_index
1245                return
1246        }
1247
1248        # -- Make sure our current branch exists.
1249        #
1250        if {$commit_type eq {initial}} {
1251                lappend all_heads $current_branch
1252                set all_heads [lsort -unique $all_heads]
1253                populate_branch_menu
1254        }
1255
1256        # -- Cleanup after ourselves.
1257        #
1258        catch {file delete $msg_p}
1259        catch {file delete [gitdir MERGE_HEAD]}
1260        catch {file delete [gitdir MERGE_MSG]}
1261        catch {file delete [gitdir SQUASH_MSG]}
1262        catch {file delete [gitdir GITGUI_MSG]}
1263
1264        # -- Let rerere do its thing.
1265        #
1266        if {[file isdirectory [gitdir rr-cache]]} {
1267                catch {exec git rerere}
1268        }
1269
1270        # -- Run the post-commit hook.
1271        #
1272        set pchook [gitdir hooks post-commit]
1273        if {[is_Cygwin] && [file isfile $pchook]} {
1274                set pchook [list sh -c [concat \
1275                        "if test -x \"$pchook\";" \
1276                        "then exec \"$pchook\";" \
1277                        "fi"]]
1278        } elseif {![file executable $pchook]} {
1279                set pchook {}
1280        }
1281        if {$pchook ne {}} {
1282                catch {exec $pchook &}
1283        }
1284
1285        $ui_comm delete 0.0 end
1286        $ui_comm edit reset
1287        $ui_comm edit modified false
1288
1289        if {$single_commit} do_quit
1290
1291        # -- Update in memory status
1292        #
1293        set selected_commit_type new
1294        set commit_type normal
1295        set HEAD $cmt_id
1296        set PARENT $cmt_id
1297        set MERGE_HEAD [list]
1298
1299        foreach path [array names file_states] {
1300                set s $file_states($path)
1301                set m [lindex $s 0]
1302                switch -glob -- $m {
1303                _O -
1304                _M -
1305                _D {continue}
1306                __ -
1307                A_ -
1308                M_ -
1309                D_ {
1310                        unset file_states($path)
1311                        catch {unset selected_paths($path)}
1312                }
1313                DO {
1314                        set file_states($path) [list _O [lindex $s 1] {} {}]
1315                }
1316                AM -
1317                AD -
1318                MM -
1319                MD {
1320                        set file_states($path) [list \
1321                                _[string index $m 1] \
1322                                [lindex $s 1] \
1323                                [lindex $s 3] \
1324                                {}]
1325                }
1326                }
1327        }
1328
1329        display_all_files
1330        unlock_index
1331        reshow_diff
1332        set ui_status_value \
1333                "Changes committed as [string range $cmt_id 0 7]."
1334}
1335
1336######################################################################
1337##
1338## fetch push
1339
1340proc fetch_from {remote} {
1341        set w [new_console \
1342                "fetch $remote" \
1343                "Fetching new changes from $remote"]
1344        set cmd [list git fetch]
1345        lappend cmd $remote
1346        console_exec $w $cmd console_done
1347}
1348
1349proc push_to {remote} {
1350        set w [new_console \
1351                "push $remote" \
1352                "Pushing changes to $remote"]
1353        set cmd [list git push]
1354        lappend cmd -v
1355        lappend cmd $remote
1356        console_exec $w $cmd console_done
1357}
1358
1359######################################################################
1360##
1361## ui helpers
1362
1363proc mapicon {w state path} {
1364        global all_icons
1365
1366        if {[catch {set r $all_icons($state$w)}]} {
1367                puts "error: no icon for $w state={$state} $path"
1368                return file_plain
1369        }
1370        return $r
1371}
1372
1373proc mapdesc {state path} {
1374        global all_descs
1375
1376        if {[catch {set r $all_descs($state)}]} {
1377                puts "error: no desc for state={$state} $path"
1378                return $state
1379        }
1380        return $r
1381}
1382
1383proc escape_path {path} {
1384        regsub -all {\\} $path "\\\\" path
1385        regsub -all "\n" $path "\\n" path
1386        return $path
1387}
1388
1389proc short_path {path} {
1390        return [escape_path [lindex [file split $path] end]]
1391}
1392
1393set next_icon_id 0
1394set null_sha1 [string repeat 0 40]
1395
1396proc merge_state {path new_state {head_info {}} {index_info {}}} {
1397        global file_states next_icon_id null_sha1
1398
1399        set s0 [string index $new_state 0]
1400        set s1 [string index $new_state 1]
1401
1402        if {[catch {set info $file_states($path)}]} {
1403                set state __
1404                set icon n[incr next_icon_id]
1405        } else {
1406                set state [lindex $info 0]
1407                set icon [lindex $info 1]
1408                if {$head_info eq {}}  {set head_info  [lindex $info 2]}
1409                if {$index_info eq {}} {set index_info [lindex $info 3]}
1410        }
1411
1412        if     {$s0 eq {?}} {set s0 [string index $state 0]} \
1413        elseif {$s0 eq {_}} {set s0 _}
1414
1415        if     {$s1 eq {?}} {set s1 [string index $state 1]} \
1416        elseif {$s1 eq {_}} {set s1 _}
1417
1418        if {$s0 eq {A} && $s1 eq {_} && $head_info eq {}} {
1419                set head_info [list 0 $null_sha1]
1420        } elseif {$s0 ne {_} && [string index $state 0] eq {_}
1421                && $head_info eq {}} {
1422                set head_info $index_info
1423        }
1424
1425        set file_states($path) [list $s0$s1 $icon \
1426                $head_info $index_info \
1427                ]
1428        return $state
1429}
1430
1431proc display_file_helper {w path icon_name old_m new_m} {
1432        global file_lists
1433
1434        if {$new_m eq {_}} {
1435                set lno [lsearch -sorted -exact $file_lists($w) $path]
1436                if {$lno >= 0} {
1437                        set file_lists($w) [lreplace $file_lists($w) $lno $lno]
1438                        incr lno
1439                        $w conf -state normal
1440                        $w delete $lno.0 [expr {$lno + 1}].0
1441                        $w conf -state disabled
1442                }
1443        } elseif {$old_m eq {_} && $new_m ne {_}} {
1444                lappend file_lists($w) $path
1445                set file_lists($w) [lsort -unique $file_lists($w)]
1446                set lno [lsearch -sorted -exact $file_lists($w) $path]
1447                incr lno
1448                $w conf -state normal
1449                $w image create $lno.0 \
1450                        -align center -padx 5 -pady 1 \
1451                        -name $icon_name \
1452                        -image [mapicon $w $new_m $path]
1453                $w insert $lno.1 "[escape_path $path]\n"
1454                $w conf -state disabled
1455        } elseif {$old_m ne $new_m} {
1456                $w conf -state normal
1457                $w image conf $icon_name -image [mapicon $w $new_m $path]
1458                $w conf -state disabled
1459        }
1460}
1461
1462proc display_file {path state} {
1463        global file_states selected_paths
1464        global ui_index ui_workdir
1465
1466        set old_m [merge_state $path $state]
1467        set s $file_states($path)
1468        set new_m [lindex $s 0]
1469        set icon_name [lindex $s 1]
1470
1471        set o [string index $old_m 0]
1472        set n [string index $new_m 0]
1473        if {$o eq {U}} {
1474                set o _
1475        }
1476        if {$n eq {U}} {
1477                set n _
1478        }
1479        display_file_helper     $ui_index $path $icon_name $o $n
1480
1481        if {[string index $old_m 0] eq {U}} {
1482                set o U
1483        } else {
1484                set o [string index $old_m 1]
1485        }
1486        if {[string index $new_m 0] eq {U}} {
1487                set n U
1488        } else {
1489                set n [string index $new_m 1]
1490        }
1491        display_file_helper     $ui_workdir $path $icon_name $o $n
1492
1493        if {$new_m eq {__}} {
1494                unset file_states($path)
1495                catch {unset selected_paths($path)}
1496        }
1497}
1498
1499proc display_all_files_helper {w path icon_name m} {
1500        global file_lists
1501
1502        lappend file_lists($w) $path
1503        set lno [expr {[lindex [split [$w index end] .] 0] - 1}]
1504        $w image create end \
1505                -align center -padx 5 -pady 1 \
1506                -name $icon_name \
1507                -image [mapicon $w $m $path]
1508        $w insert end "[escape_path $path]\n"
1509}
1510
1511proc display_all_files {} {
1512        global ui_index ui_workdir
1513        global file_states file_lists
1514        global last_clicked
1515
1516        $ui_index conf -state normal
1517        $ui_workdir conf -state normal
1518
1519        $ui_index delete 0.0 end
1520        $ui_workdir delete 0.0 end
1521        set last_clicked {}
1522
1523        set file_lists($ui_index) [list]
1524        set file_lists($ui_workdir) [list]
1525
1526        foreach path [lsort [array names file_states]] {
1527                set s $file_states($path)
1528                set m [lindex $s 0]
1529                set icon_name [lindex $s 1]
1530
1531                set s [string index $m 0]
1532                if {$s ne {U} && $s ne {_}} {
1533                        display_all_files_helper $ui_index $path \
1534                                $icon_name $s
1535                }
1536
1537                if {[string index $m 0] eq {U}} {
1538                        set s U
1539                } else {
1540                        set s [string index $m 1]
1541                }
1542                if {$s ne {_}} {
1543                        display_all_files_helper $ui_workdir $path \
1544                                $icon_name $s
1545                }
1546        }
1547
1548        $ui_index conf -state disabled
1549        $ui_workdir conf -state disabled
1550}
1551
1552proc update_indexinfo {msg pathList after} {
1553        global update_index_cp ui_status_value
1554
1555        if {![lock_index update]} return
1556
1557        set update_index_cp 0
1558        set pathList [lsort $pathList]
1559        set totalCnt [llength $pathList]
1560        set batch [expr {int($totalCnt * .01) + 1}]
1561        if {$batch > 25} {set batch 25}
1562
1563        set ui_status_value [format \
1564                "$msg... %i/%i files (%.2f%%)" \
1565                $update_index_cp \
1566                $totalCnt \
1567                0.0]
1568        set fd [open "| git update-index -z --index-info" w]
1569        fconfigure $fd \
1570                -blocking 0 \
1571                -buffering full \
1572                -buffersize 512 \
1573                -encoding binary \
1574                -translation binary
1575        fileevent $fd writable [list \
1576                write_update_indexinfo \
1577                $fd \
1578                $pathList \
1579                $totalCnt \
1580                $batch \
1581                $msg \
1582                $after \
1583                ]
1584}
1585
1586proc write_update_indexinfo {fd pathList totalCnt batch msg after} {
1587        global update_index_cp ui_status_value
1588        global file_states current_diff_path
1589
1590        if {$update_index_cp >= $totalCnt} {
1591                close $fd
1592                unlock_index
1593                uplevel #0 $after
1594                return
1595        }
1596
1597        for {set i $batch} \
1598                {$update_index_cp < $totalCnt && $i > 0} \
1599                {incr i -1} {
1600                set path [lindex $pathList $update_index_cp]
1601                incr update_index_cp
1602
1603                set s $file_states($path)
1604                switch -glob -- [lindex $s 0] {
1605                A? {set new _O}
1606                M? {set new _M}
1607                D_ {set new _D}
1608                D? {set new _?}
1609                ?? {continue}
1610                }
1611                set info [lindex $s 2]
1612                if {$info eq {}} continue
1613
1614                puts -nonewline $fd "$info\t[encoding convertto $path]\0"
1615                display_file $path $new
1616        }
1617
1618        set ui_status_value [format \
1619                "$msg... %i/%i files (%.2f%%)" \
1620                $update_index_cp \
1621                $totalCnt \
1622                [expr {100.0 * $update_index_cp / $totalCnt}]]
1623}
1624
1625proc update_index {msg pathList after} {
1626        global update_index_cp ui_status_value
1627
1628        if {![lock_index update]} return
1629
1630        set update_index_cp 0
1631        set pathList [lsort $pathList]
1632        set totalCnt [llength $pathList]
1633        set batch [expr {int($totalCnt * .01) + 1}]
1634        if {$batch > 25} {set batch 25}
1635
1636        set ui_status_value [format \
1637                "$msg... %i/%i files (%.2f%%)" \
1638                $update_index_cp \
1639                $totalCnt \
1640                0.0]
1641        set fd [open "| git update-index --add --remove -z --stdin" w]
1642        fconfigure $fd \
1643                -blocking 0 \
1644                -buffering full \
1645                -buffersize 512 \
1646                -encoding binary \
1647                -translation binary
1648        fileevent $fd writable [list \
1649                write_update_index \
1650                $fd \
1651                $pathList \
1652                $totalCnt \
1653                $batch \
1654                $msg \
1655                $after \
1656                ]
1657}
1658
1659proc write_update_index {fd pathList totalCnt batch msg after} {
1660        global update_index_cp ui_status_value
1661        global file_states current_diff_path
1662
1663        if {$update_index_cp >= $totalCnt} {
1664                close $fd
1665                unlock_index
1666                uplevel #0 $after
1667                return
1668        }
1669
1670        for {set i $batch} \
1671                {$update_index_cp < $totalCnt && $i > 0} \
1672                {incr i -1} {
1673                set path [lindex $pathList $update_index_cp]
1674                incr update_index_cp
1675
1676                switch -glob -- [lindex $file_states($path) 0] {
1677                AD {set new __}
1678                ?D {set new D_}
1679                _O -
1680                AM {set new A_}
1681                U? {
1682                        if {[file exists $path]} {
1683                                set new M_
1684                        } else {
1685                                set new D_
1686                        }
1687                }
1688                ?M {set new M_}
1689                ?? {continue}
1690                }
1691                puts -nonewline $fd "[encoding convertto $path]\0"
1692                display_file $path $new
1693        }
1694
1695        set ui_status_value [format \
1696                "$msg... %i/%i files (%.2f%%)" \
1697                $update_index_cp \
1698                $totalCnt \
1699                [expr {100.0 * $update_index_cp / $totalCnt}]]
1700}
1701
1702proc checkout_index {msg pathList after} {
1703        global update_index_cp ui_status_value
1704
1705        if {![lock_index update]} return
1706
1707        set update_index_cp 0
1708        set pathList [lsort $pathList]
1709        set totalCnt [llength $pathList]
1710        set batch [expr {int($totalCnt * .01) + 1}]
1711        if {$batch > 25} {set batch 25}
1712
1713        set ui_status_value [format \
1714                "$msg... %i/%i files (%.2f%%)" \
1715                $update_index_cp \
1716                $totalCnt \
1717                0.0]
1718        set cmd [list git checkout-index]
1719        lappend cmd --index
1720        lappend cmd --quiet
1721        lappend cmd --force
1722        lappend cmd -z
1723        lappend cmd --stdin
1724        set fd [open "| $cmd " w]
1725        fconfigure $fd \
1726                -blocking 0 \
1727                -buffering full \
1728                -buffersize 512 \
1729                -encoding binary \
1730                -translation binary
1731        fileevent $fd writable [list \
1732                write_checkout_index \
1733                $fd \
1734                $pathList \
1735                $totalCnt \
1736                $batch \
1737                $msg \
1738                $after \
1739                ]
1740}
1741
1742proc write_checkout_index {fd pathList totalCnt batch msg after} {
1743        global update_index_cp ui_status_value
1744        global file_states current_diff_path
1745
1746        if {$update_index_cp >= $totalCnt} {
1747                close $fd
1748                unlock_index
1749                uplevel #0 $after
1750                return
1751        }
1752
1753        for {set i $batch} \
1754                {$update_index_cp < $totalCnt && $i > 0} \
1755                {incr i -1} {
1756                set path [lindex $pathList $update_index_cp]
1757                incr update_index_cp
1758                switch -glob -- [lindex $file_states($path) 0] {
1759                U? {continue}
1760                ?M -
1761                ?D {
1762                        puts -nonewline $fd "[encoding convertto $path]\0"
1763                        display_file $path ?_
1764                }
1765                }
1766        }
1767
1768        set ui_status_value [format \
1769                "$msg... %i/%i files (%.2f%%)" \
1770                $update_index_cp \
1771                $totalCnt \
1772                [expr {100.0 * $update_index_cp / $totalCnt}]]
1773}
1774
1775######################################################################
1776##
1777## branch management
1778
1779proc is_tracking_branch {name} {
1780        global tracking_branches
1781
1782        if {![catch {set info $tracking_branches($name)}]} {
1783                return 1
1784        }
1785        foreach t [array names tracking_branches] {
1786                if {[string match {*/\*} $t] && [string match $t $name]} {
1787                        return 1
1788                }
1789        }
1790        return 0
1791}
1792
1793proc load_all_heads {} {
1794        global all_heads
1795
1796        set all_heads [list]
1797        set fd [open "| git for-each-ref --format=%(refname) refs/heads" r]
1798        while {[gets $fd line] > 0} {
1799                if {[is_tracking_branch $line]} continue
1800                if {![regsub ^refs/heads/ $line {} name]} continue
1801                lappend all_heads $name
1802        }
1803        close $fd
1804
1805        set all_heads [lsort $all_heads]
1806}
1807
1808proc populate_branch_menu {} {
1809        global all_heads disable_on_lock
1810
1811        set m .mbar.branch
1812        set last [$m index last]
1813        for {set i 0} {$i <= $last} {incr i} {
1814                if {[$m type $i] eq {separator}} {
1815                        $m delete $i last
1816                        set new_dol [list]
1817                        foreach a $disable_on_lock {
1818                                if {[lindex $a 0] ne $m || [lindex $a 2] < $i} {
1819                                        lappend new_dol $a
1820                                }
1821                        }
1822                        set disable_on_lock $new_dol
1823                        break
1824                }
1825        }
1826
1827        if {$all_heads ne {}} {
1828                $m add separator
1829        }
1830        foreach b $all_heads {
1831                $m add radiobutton \
1832                        -label $b \
1833                        -command [list switch_branch $b] \
1834                        -variable current_branch \
1835                        -value $b \
1836                        -font font_ui
1837                lappend disable_on_lock \
1838                        [list $m entryconf [$m index last] -state]
1839        }
1840}
1841
1842proc all_tracking_branches {} {
1843        global tracking_branches
1844
1845        set all_trackings {}
1846        set cmd {}
1847        foreach name [array names tracking_branches] {
1848                if {[regsub {/\*$} $name {} name]} {
1849                        lappend cmd $name
1850                } else {
1851                        regsub ^refs/(heads|remotes)/ $name {} name
1852                        lappend all_trackings $name
1853                }
1854        }
1855
1856        if {$cmd ne {}} {
1857                set fd [open "| git for-each-ref --format=%(refname) $cmd" r]
1858                while {[gets $fd name] > 0} {
1859                        regsub ^refs/(heads|remotes)/ $name {} name
1860                        lappend all_trackings $name
1861                }
1862                close $fd
1863        }
1864
1865        return [lsort -unique $all_trackings]
1866}
1867
1868proc do_create_branch_action {w} {
1869        global all_heads null_sha1 repo_config
1870        global create_branch_checkout create_branch_revtype
1871        global create_branch_head create_branch_trackinghead
1872        global create_branch_name create_branch_revexp
1873
1874        set newbranch $create_branch_name
1875        if {$newbranch eq {}
1876                || $newbranch eq $repo_config(gui.newbranchtemplate)} {
1877                tk_messageBox \
1878                        -icon error \
1879                        -type ok \
1880                        -title [wm title $w] \
1881                        -parent $w \
1882                        -message "Please supply a branch name."
1883                focus $w.desc.name_t
1884                return
1885        }
1886        if {![catch {exec git show-ref --verify -- "refs/heads/$newbranch"}]} {
1887                tk_messageBox \
1888                        -icon error \
1889                        -type ok \
1890                        -title [wm title $w] \
1891                        -parent $w \
1892                        -message "Branch '$newbranch' already exists."
1893                focus $w.desc.name_t
1894                return
1895        }
1896        if {[catch {exec git check-ref-format "heads/$newbranch"}]} {
1897                tk_messageBox \
1898                        -icon error \
1899                        -type ok \
1900                        -title [wm title $w] \
1901                        -parent $w \
1902                        -message "We do not like '$newbranch' as a branch name."
1903                focus $w.desc.name_t
1904                return
1905        }
1906
1907        set rev {}
1908        switch -- $create_branch_revtype {
1909        head {set rev $create_branch_head}
1910        tracking {set rev $create_branch_trackinghead}
1911        expression {set rev $create_branch_revexp}
1912        }
1913        if {[catch {set cmt [exec git rev-parse --verify "${rev}^0"]}]} {
1914                tk_messageBox \
1915                        -icon error \
1916                        -type ok \
1917                        -title [wm title $w] \
1918                        -parent $w \
1919                        -message "Invalid starting revision: $rev"
1920                return
1921        }
1922        set cmd [list git update-ref]
1923        lappend cmd -m
1924        lappend cmd "branch: Created from $rev"
1925        lappend cmd "refs/heads/$newbranch"
1926        lappend cmd $cmt
1927        lappend cmd $null_sha1
1928        if {[catch {eval exec $cmd} err]} {
1929                tk_messageBox \
1930                        -icon error \
1931                        -type ok \
1932                        -title [wm title $w] \
1933                        -parent $w \
1934                        -message "Failed to create '$newbranch'.\n\n$err"
1935                return
1936        }
1937
1938        lappend all_heads $newbranch
1939        set all_heads [lsort $all_heads]
1940        populate_branch_menu
1941        destroy $w
1942        if {$create_branch_checkout} {
1943                switch_branch $newbranch
1944        }
1945}
1946
1947proc radio_selector {varname value args} {
1948        upvar #0 $varname var
1949        set var $value
1950}
1951
1952trace add variable create_branch_head write \
1953        [list radio_selector create_branch_revtype head]
1954trace add variable create_branch_trackinghead write \
1955        [list radio_selector create_branch_revtype tracking]
1956
1957trace add variable delete_branch_head write \
1958        [list radio_selector delete_branch_checktype head]
1959trace add variable delete_branch_trackinghead write \
1960        [list radio_selector delete_branch_checktype tracking]
1961
1962proc do_create_branch {} {
1963        global all_heads current_branch repo_config
1964        global create_branch_checkout create_branch_revtype
1965        global create_branch_head create_branch_trackinghead
1966        global create_branch_name create_branch_revexp
1967
1968        set w .branch_editor
1969        toplevel $w
1970        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
1971
1972        label $w.header -text {Create New Branch} \
1973                -font font_uibold
1974        pack $w.header -side top -fill x
1975
1976        frame $w.buttons
1977        button $w.buttons.create -text Create \
1978                -font font_ui \
1979                -default active \
1980                -command [list do_create_branch_action $w]
1981        pack $w.buttons.create -side right
1982        button $w.buttons.cancel -text {Cancel} \
1983                -font font_ui \
1984                -command [list destroy $w]
1985        pack $w.buttons.cancel -side right -padx 5
1986        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
1987
1988        labelframe $w.desc \
1989                -text {Branch Description} \
1990                -font font_ui
1991        label $w.desc.name_l -text {Name:} -font font_ui
1992        entry $w.desc.name_t \
1993                -borderwidth 1 \
1994                -relief sunken \
1995                -width 40 \
1996                -textvariable create_branch_name \
1997                -font font_ui \
1998                -validate key \
1999                -validatecommand {
2000                        if {%d == 1 && [regexp {[~^:?*\[\0- ]} %S]} {return 0}
2001                        return 1
2002                }
2003        grid $w.desc.name_l $w.desc.name_t -sticky we -padx {0 5}
2004        grid columnconfigure $w.desc 1 -weight 1
2005        pack $w.desc -anchor nw -fill x -pady 5 -padx 5
2006
2007        labelframe $w.from \
2008                -text {Starting Revision} \
2009                -font font_ui
2010        radiobutton $w.from.head_r \
2011                -text {Local Branch:} \
2012                -value head \
2013                -variable create_branch_revtype \
2014                -font font_ui
2015        eval tk_optionMenu $w.from.head_m create_branch_head $all_heads
2016        grid $w.from.head_r $w.from.head_m -sticky w
2017        set all_trackings [all_tracking_branches]
2018        if {$all_trackings ne {}} {
2019                set create_branch_trackinghead [lindex $all_trackings 0]
2020                radiobutton $w.from.tracking_r \
2021                        -text {Tracking Branch:} \
2022                        -value tracking \
2023                        -variable create_branch_revtype \
2024                        -font font_ui
2025                eval tk_optionMenu $w.from.tracking_m \
2026                        create_branch_trackinghead \
2027                        $all_trackings
2028                grid $w.from.tracking_r $w.from.tracking_m -sticky w
2029        }
2030        radiobutton $w.from.exp_r \
2031                -text {Revision Expression:} \
2032                -value expression \
2033                -variable create_branch_revtype \
2034                -font font_ui
2035        entry $w.from.exp_t \
2036                -borderwidth 1 \
2037                -relief sunken \
2038                -width 50 \
2039                -textvariable create_branch_revexp \
2040                -font font_ui \
2041                -validate key \
2042                -validatecommand {
2043                        if {%d == 1 && [regexp {\s} %S]} {return 0}
2044                        if {%d == 1 && [string length %S] > 0} {
2045                                set create_branch_revtype expression
2046                        }
2047                        return 1
2048                }
2049        grid $w.from.exp_r $w.from.exp_t -sticky we -padx {0 5}
2050        grid columnconfigure $w.from 1 -weight 1
2051        pack $w.from -anchor nw -fill x -pady 5 -padx 5
2052
2053        labelframe $w.postActions \
2054                -text {Post Creation Actions} \
2055                -font font_ui
2056        checkbutton $w.postActions.checkout \
2057                -text {Checkout after creation} \
2058                -variable create_branch_checkout \
2059                -font font_ui
2060        pack $w.postActions.checkout -anchor nw
2061        pack $w.postActions -anchor nw -fill x -pady 5 -padx 5
2062
2063        set create_branch_checkout 1
2064        set create_branch_head $current_branch
2065        set create_branch_revtype head
2066        set create_branch_name $repo_config(gui.newbranchtemplate)
2067        set create_branch_revexp {}
2068
2069        bind $w <Visibility> "
2070                grab $w
2071                $w.desc.name_t icursor end
2072                focus $w.desc.name_t
2073        "
2074        bind $w <Key-Escape> "destroy $w"
2075        bind $w <Key-Return> "do_create_branch_action $w;break"
2076        wm title $w "[appname] ([reponame]): Create Branch"
2077        tkwait window $w
2078}
2079
2080proc do_delete_branch_action {w} {
2081        global all_heads
2082        global delete_branch_checktype delete_branch_head delete_branch_trackinghead
2083
2084        set check_rev {}
2085        switch -- $delete_branch_checktype {
2086        head {set check_rev $delete_branch_head}
2087        tracking {set check_rev $delete_branch_trackinghead}
2088        always {set check_rev {:none}}
2089        }
2090        if {$check_rev eq {:none}} {
2091                set check_cmt {}
2092        } elseif {[catch {set check_cmt [exec git rev-parse --verify "${check_rev}^0"]}]} {
2093                tk_messageBox \
2094                        -icon error \
2095                        -type ok \
2096                        -title [wm title $w] \
2097                        -parent $w \
2098                        -message "Invalid check revision: $check_rev"
2099                return
2100        }
2101
2102        set to_delete [list]
2103        set not_merged [list]
2104        foreach i [$w.list.l curselection] {
2105                set b [$w.list.l get $i]
2106                if {[catch {set o [exec git rev-parse --verify $b]}]} continue
2107                if {$check_cmt ne {}} {
2108                        if {$b eq $check_rev} continue
2109                        if {[catch {set m [exec git merge-base $o $check_cmt]}]} continue
2110                        if {$o ne $m} {
2111                                lappend not_merged $b
2112                                continue
2113                        }
2114                }
2115                lappend to_delete [list $b $o]
2116        }
2117        if {$not_merged ne {}} {
2118                set msg "The following branches are not completely merged into $check_rev:
2119
2120 - [join $not_merged "\n - "]"
2121                tk_messageBox \
2122                        -icon info \
2123                        -type ok \
2124                        -title [wm title $w] \
2125                        -parent $w \
2126                        -message $msg
2127        }
2128        if {$to_delete eq {}} return
2129        if {$delete_branch_checktype eq {always}} {
2130                set msg {Recovering deleted branches is difficult.
2131
2132Delete the selected branches?}
2133                if {[tk_messageBox \
2134                        -icon warning \
2135                        -type yesno \
2136                        -title [wm title $w] \
2137                        -parent $w \
2138                        -message $msg] ne yes} {
2139                        return
2140                }
2141        }
2142
2143        set failed {}
2144        foreach i $to_delete {
2145                set b [lindex $i 0]
2146                set o [lindex $i 1]
2147                if {[catch {exec git update-ref -d "refs/heads/$b" $o} err]} {
2148                        append failed " - $b: $err\n"
2149                } else {
2150                        set x [lsearch -sorted -exact $all_heads $b]
2151                        if {$x >= 0} {
2152                                set all_heads [lreplace $all_heads $x $x]
2153                        }
2154                }
2155        }
2156
2157        if {$failed ne {}} {
2158                tk_messageBox \
2159                        -icon error \
2160                        -type ok \
2161                        -title [wm title $w] \
2162                        -parent $w \
2163                        -message "Failed to delete branches:\n$failed"
2164        }
2165
2166        set all_heads [lsort $all_heads]
2167        populate_branch_menu
2168        destroy $w
2169}
2170
2171proc do_delete_branch {} {
2172        global all_heads tracking_branches current_branch
2173        global delete_branch_checktype delete_branch_head delete_branch_trackinghead
2174
2175        set w .branch_editor
2176        toplevel $w
2177        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
2178
2179        label $w.header -text {Delete Local Branch} \
2180                -font font_uibold
2181        pack $w.header -side top -fill x
2182
2183        frame $w.buttons
2184        button $w.buttons.create -text Delete \
2185                -font font_ui \
2186                -command [list do_delete_branch_action $w]
2187        pack $w.buttons.create -side right
2188        button $w.buttons.cancel -text {Cancel} \
2189                -font font_ui \
2190                -command [list destroy $w]
2191        pack $w.buttons.cancel -side right -padx 5
2192        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
2193
2194        labelframe $w.list \
2195                -text {Local Branches} \
2196                -font font_ui
2197        listbox $w.list.l \
2198                -height 10 \
2199                -width 70 \
2200                -selectmode extended \
2201                -yscrollcommand [list $w.list.sby set] \
2202                -font font_ui
2203        foreach h $all_heads {
2204                if {$h ne $current_branch} {
2205                        $w.list.l insert end $h
2206                }
2207        }
2208        scrollbar $w.list.sby -command [list $w.list.l yview]
2209        pack $w.list.sby -side right -fill y
2210        pack $w.list.l -side left -fill both -expand 1
2211        pack $w.list -fill both -expand 1 -pady 5 -padx 5
2212
2213        labelframe $w.validate \
2214                -text {Delete Only If} \
2215                -font font_ui
2216        radiobutton $w.validate.head_r \
2217                -text {Merged Into Local Branch:} \
2218                -value head \
2219                -variable delete_branch_checktype \
2220                -font font_ui
2221        eval tk_optionMenu $w.validate.head_m delete_branch_head $all_heads
2222        grid $w.validate.head_r $w.validate.head_m -sticky w
2223        set all_trackings [all_tracking_branches]
2224        if {$all_trackings ne {}} {
2225                set delete_branch_trackinghead [lindex $all_trackings 0]
2226                radiobutton $w.validate.tracking_r \
2227                        -text {Merged Into Tracking Branch:} \
2228                        -value tracking \
2229                        -variable delete_branch_checktype \
2230                        -font font_ui
2231                eval tk_optionMenu $w.validate.tracking_m \
2232                        delete_branch_trackinghead \
2233                        $all_trackings
2234                grid $w.validate.tracking_r $w.validate.tracking_m -sticky w
2235        }
2236        radiobutton $w.validate.always_r \
2237                -text {Always (Do not perform merge checks)} \
2238                -value always \
2239                -variable delete_branch_checktype \
2240                -font font_ui
2241        grid $w.validate.always_r -columnspan 2 -sticky w
2242        grid columnconfigure $w.validate 1 -weight 1
2243        pack $w.validate -anchor nw -fill x -pady 5 -padx 5
2244
2245        set delete_branch_head $current_branch
2246        set delete_branch_checktype head
2247
2248        bind $w <Visibility> "grab $w; focus $w"
2249        bind $w <Key-Escape> "destroy $w"
2250        wm title $w "[appname] ([reponame]): Delete Branch"
2251        tkwait window $w
2252}
2253
2254proc switch_branch {new_branch} {
2255        global HEAD commit_type current_branch repo_config
2256
2257        if {![lock_index switch]} return
2258
2259        # -- Our in memory state should match the repository.
2260        #
2261        repository_state curType curHEAD curMERGE_HEAD
2262        if {[string match amend* $commit_type]
2263                && $curType eq {normal}
2264                && $curHEAD eq $HEAD} {
2265        } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
2266                info_popup {Last scanned state does not match repository state.
2267
2268Another Git program has modified this repository
2269since the last scan.  A rescan must be performed
2270before the current branch can be changed.
2271
2272The rescan will be automatically started now.
2273}
2274                unlock_index
2275                rescan {set ui_status_value {Ready.}}
2276                return
2277        }
2278
2279        # -- Don't do a pointless switch.
2280        #
2281        if {$current_branch eq $new_branch} {
2282                unlock_index
2283                return
2284        }
2285
2286        if {$repo_config(gui.trustmtime) eq {true}} {
2287                switch_branch_stage2 {} $new_branch
2288        } else {
2289                set ui_status_value {Refreshing file status...}
2290                set cmd [list git update-index]
2291                lappend cmd -q
2292                lappend cmd --unmerged
2293                lappend cmd --ignore-missing
2294                lappend cmd --refresh
2295                set fd_rf [open "| $cmd" r]
2296                fconfigure $fd_rf -blocking 0 -translation binary
2297                fileevent $fd_rf readable \
2298                        [list switch_branch_stage2 $fd_rf $new_branch]
2299        }
2300}
2301
2302proc switch_branch_stage2 {fd_rf new_branch} {
2303        global ui_status_value HEAD
2304
2305        if {$fd_rf ne {}} {
2306                read $fd_rf
2307                if {![eof $fd_rf]} return
2308                close $fd_rf
2309        }
2310
2311        set ui_status_value "Updating working directory to '$new_branch'..."
2312        set cmd [list git read-tree]
2313        lappend cmd -m
2314        lappend cmd -u
2315        lappend cmd --exclude-per-directory=.gitignore
2316        lappend cmd $HEAD
2317        lappend cmd $new_branch
2318        set fd_rt [open "| $cmd" r]
2319        fconfigure $fd_rt -blocking 0 -translation binary
2320        fileevent $fd_rt readable \
2321                [list switch_branch_readtree_wait $fd_rt $new_branch]
2322}
2323
2324proc switch_branch_readtree_wait {fd_rt new_branch} {
2325        global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
2326        global current_branch
2327        global ui_comm ui_status_value
2328
2329        # -- We never get interesting output on stdout; only stderr.
2330        #
2331        read $fd_rt
2332        fconfigure $fd_rt -blocking 1
2333        if {![eof $fd_rt]} {
2334                fconfigure $fd_rt -blocking 0
2335                return
2336        }
2337
2338        # -- The working directory wasn't in sync with the index and
2339        #    we'd have to overwrite something to make the switch. A
2340        #    merge is required.
2341        #
2342        if {[catch {close $fd_rt} err]} {
2343                regsub {^fatal: } $err {} err
2344                warn_popup "File level merge required.
2345
2346$err
2347
2348Staying on branch '$current_branch'."
2349                set ui_status_value "Aborted checkout of '$new_branch' (file level merging is required)."
2350                unlock_index
2351                return
2352        }
2353
2354        # -- Update the symbolic ref.  Core git doesn't even check for failure
2355        #    here, it Just Works(tm).  If it doesn't we are in some really ugly
2356        #    state that is difficult to recover from within git-gui.
2357        #
2358        if {[catch {exec git symbolic-ref HEAD "refs/heads/$new_branch"} err]} {
2359                error_popup "Failed to set current branch.
2360
2361This working directory is only partially switched.
2362We successfully updated your files, but failed to
2363update an internal Git file.
2364
2365This should not have occurred.  [appname] will now
2366close and give up.
2367
2368$err"
2369                do_quit
2370                return
2371        }
2372
2373        # -- Update our repository state.  If we were previously in amend mode
2374        #    we need to toss the current buffer and do a full rescan to update
2375        #    our file lists.  If we weren't in amend mode our file lists are
2376        #    accurate and we can avoid the rescan.
2377        #
2378        unlock_index
2379        set selected_commit_type new
2380        if {[string match amend* $commit_type]} {
2381                $ui_comm delete 0.0 end
2382                $ui_comm edit reset
2383                $ui_comm edit modified false
2384                rescan {set ui_status_value "Checked out branch '$current_branch'."}
2385        } else {
2386                repository_state commit_type HEAD MERGE_HEAD
2387                set PARENT $HEAD
2388                set ui_status_value "Checked out branch '$current_branch'."
2389        }
2390}
2391
2392######################################################################
2393##
2394## remote management
2395
2396proc load_all_remotes {} {
2397        global repo_config
2398        global all_remotes tracking_branches
2399
2400        set all_remotes [list]
2401        array unset tracking_branches
2402
2403        set rm_dir [gitdir remotes]
2404        if {[file isdirectory $rm_dir]} {
2405                set all_remotes [glob \
2406                        -types f \
2407                        -tails \
2408                        -nocomplain \
2409                        -directory $rm_dir *]
2410
2411                foreach name $all_remotes {
2412                        catch {
2413                                set fd [open [file join $rm_dir $name] r]
2414                                while {[gets $fd line] >= 0} {
2415                                        if {![regexp {^Pull:[   ]*([^:]+):(.+)$} \
2416                                                $line line src dst]} continue
2417                                        if {![regexp ^refs/ $dst]} {
2418                                                set dst "refs/heads/$dst"
2419                                        }
2420                                        set tracking_branches($dst) [list $name $src]
2421                                }
2422                                close $fd
2423                        }
2424                }
2425        }
2426
2427        foreach line [array names repo_config remote.*.url] {
2428                if {![regexp ^remote\.(.*)\.url\$ $line line name]} continue
2429                lappend all_remotes $name
2430
2431                if {[catch {set fl $repo_config(remote.$name.fetch)}]} {
2432                        set fl {}
2433                }
2434                foreach line $fl {
2435                        if {![regexp {^([^:]+):(.+)$} $line line src dst]} continue
2436                        if {![regexp ^refs/ $dst]} {
2437                                set dst "refs/heads/$dst"
2438                        }
2439                        set tracking_branches($dst) [list $name $src]
2440                }
2441        }
2442
2443        set all_remotes [lsort -unique $all_remotes]
2444}
2445
2446proc populate_fetch_menu {} {
2447        global all_remotes repo_config
2448
2449        set m .mbar.fetch
2450        foreach r $all_remotes {
2451                set enable 0
2452                if {![catch {set a $repo_config(remote.$r.url)}]} {
2453                        if {![catch {set a $repo_config(remote.$r.fetch)}]} {
2454                                set enable 1
2455                        }
2456                } else {
2457                        catch {
2458                                set fd [open [gitdir remotes $r] r]
2459                                while {[gets $fd n] >= 0} {
2460                                        if {[regexp {^Pull:[ \t]*([^:]+):} $n]} {
2461                                                set enable 1
2462                                                break
2463                                        }
2464                                }
2465                                close $fd
2466                        }
2467                }
2468
2469                if {$enable} {
2470                        $m add command \
2471                                -label "Fetch from $r..." \
2472                                -command [list fetch_from $r] \
2473                                -font font_ui
2474                }
2475        }
2476}
2477
2478proc populate_push_menu {} {
2479        global all_remotes repo_config
2480
2481        set m .mbar.push
2482        set fast_count 0
2483        foreach r $all_remotes {
2484                set enable 0
2485                if {![catch {set a $repo_config(remote.$r.url)}]} {
2486                        if {![catch {set a $repo_config(remote.$r.push)}]} {
2487                                set enable 1
2488                        }
2489                } else {
2490                        catch {
2491                                set fd [open [gitdir remotes $r] r]
2492                                while {[gets $fd n] >= 0} {
2493                                        if {[regexp {^Push:[ \t]*([^:]+):} $n]} {
2494                                                set enable 1
2495                                                break
2496                                        }
2497                                }
2498                                close $fd
2499                        }
2500                }
2501
2502                if {$enable} {
2503                        if {!$fast_count} {
2504                                $m add separator
2505                        }
2506                        $m add command \
2507                                -label "Push to $r..." \
2508                                -command [list push_to $r] \
2509                                -font font_ui
2510                        incr fast_count
2511                }
2512        }
2513}
2514
2515proc start_push_anywhere_action {w} {
2516        global push_urltype push_remote push_url push_thin push_tags
2517
2518        set r_url {}
2519        switch -- $push_urltype {
2520        remote {set r_url $push_remote}
2521        url {set r_url $push_url}
2522        }
2523        if {$r_url eq {}} return
2524
2525        set cmd [list git push]
2526        lappend cmd -v
2527        if {$push_thin} {
2528                lappend cmd --thin
2529        }
2530        if {$push_tags} {
2531                lappend cmd --tags
2532        }
2533        lappend cmd $r_url
2534        set cnt 0
2535        foreach i [$w.source.l curselection] {
2536                set b [$w.source.l get $i]
2537                lappend cmd "refs/heads/$b:refs/heads/$b"
2538                incr cnt
2539        }
2540        if {$cnt == 0} {
2541                return
2542        } elseif {$cnt == 1} {
2543                set unit branch
2544        } else {
2545                set unit branches
2546        }
2547
2548        set cons [new_console "push $r_url" "Pushing $cnt $unit to $r_url"]
2549        console_exec $cons $cmd console_done
2550        destroy $w
2551}
2552
2553trace add variable push_remote write \
2554        [list radio_selector push_urltype remote]
2555
2556proc do_push_anywhere {} {
2557        global all_heads all_remotes current_branch
2558        global push_urltype push_remote push_url push_thin push_tags
2559
2560        set w .push_setup
2561        toplevel $w
2562        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
2563
2564        label $w.header -text {Push Branches} -font font_uibold
2565        pack $w.header -side top -fill x
2566
2567        frame $w.buttons
2568        button $w.buttons.create -text Push \
2569                -font font_ui \
2570                -command [list start_push_anywhere_action $w]
2571        pack $w.buttons.create -side right
2572        button $w.buttons.cancel -text {Cancel} \
2573                -font font_ui \
2574                -command [list destroy $w]
2575        pack $w.buttons.cancel -side right -padx 5
2576        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
2577
2578        labelframe $w.source \
2579                -text {Source Branches} \
2580                -font font_ui
2581        listbox $w.source.l \
2582                -height 10 \
2583                -width 70 \
2584                -selectmode extended \
2585                -yscrollcommand [list $w.source.sby set] \
2586                -font font_ui
2587        foreach h $all_heads {
2588                $w.source.l insert end $h
2589                if {$h eq $current_branch} {
2590                        $w.source.l select set end
2591                }
2592        }
2593        scrollbar $w.source.sby -command [list $w.source.l yview]
2594        pack $w.source.sby -side right -fill y
2595        pack $w.source.l -side left -fill both -expand 1
2596        pack $w.source -fill both -expand 1 -pady 5 -padx 5
2597
2598        labelframe $w.dest \
2599                -text {Destination Repository} \
2600                -font font_ui
2601        if {$all_remotes ne {}} {
2602                radiobutton $w.dest.remote_r \
2603                        -text {Remote:} \
2604                        -value remote \
2605                        -variable push_urltype \
2606                        -font font_ui
2607                eval tk_optionMenu $w.dest.remote_m push_remote $all_remotes
2608                grid $w.dest.remote_r $w.dest.remote_m -sticky w
2609                if {[lsearch -sorted -exact $all_remotes origin] != -1} {
2610                        set push_remote origin
2611                } else {
2612                        set push_remote [lindex $all_remotes 0]
2613                }
2614                set push_urltype remote
2615        } else {
2616                set push_urltype url
2617        }
2618        radiobutton $w.dest.url_r \
2619                -text {Arbitrary URL:} \
2620                -value url \
2621                -variable push_urltype \
2622                -font font_ui
2623        entry $w.dest.url_t \
2624                -borderwidth 1 \
2625                -relief sunken \
2626                -width 50 \
2627                -textvariable push_url \
2628                -font font_ui \
2629                -validate key \
2630                -validatecommand {
2631                        if {%d == 1 && [regexp {\s} %S]} {return 0}
2632                        if {%d == 1 && [string length %S] > 0} {
2633                                set push_urltype url
2634                        }
2635                        return 1
2636                }
2637        grid $w.dest.url_r $w.dest.url_t -sticky we -padx {0 5}
2638        grid columnconfigure $w.dest 1 -weight 1
2639        pack $w.dest -anchor nw -fill x -pady 5 -padx 5
2640
2641        labelframe $w.options \
2642                -text {Transfer Options} \
2643                -font font_ui
2644        checkbutton $w.options.thin \
2645                -text {Use thin pack (for slow network connections)} \
2646                -variable push_thin \
2647                -font font_ui
2648        grid $w.options.thin -columnspan 2 -sticky w
2649        checkbutton $w.options.tags \
2650                -text {Include tags} \
2651                -variable push_tags \
2652                -font font_ui
2653        grid $w.options.tags -columnspan 2 -sticky w
2654        grid columnconfigure $w.options 1 -weight 1
2655        pack $w.options -anchor nw -fill x -pady 5 -padx 5
2656
2657        set push_url {}
2658        set push_thin 0
2659        set push_tags 0
2660
2661        bind $w <Visibility> "grab $w"
2662        bind $w <Key-Escape> "destroy $w"
2663        wm title $w "[appname] ([reponame]): Push"
2664        tkwait window $w
2665}
2666
2667######################################################################
2668##
2669## merge
2670
2671proc can_merge {} {
2672        global HEAD commit_type file_states
2673
2674        if {[string match amend* $commit_type]} {
2675                info_popup {Cannot merge while amending.
2676
2677You must finish amending this commit before
2678starting any type of merge.
2679}
2680                return 0
2681        }
2682
2683        if {[committer_ident] eq {}} {return 0}
2684        if {![lock_index merge]} {return 0}
2685
2686        # -- Our in memory state should match the repository.
2687        #
2688        repository_state curType curHEAD curMERGE_HEAD
2689        if {$commit_type ne $curType || $HEAD ne $curHEAD} {
2690                info_popup {Last scanned state does not match repository state.
2691
2692Another Git program has modified this repository
2693since the last scan.  A rescan must be performed
2694before a merge can be performed.
2695
2696The rescan will be automatically started now.
2697}
2698                unlock_index
2699                rescan {set ui_status_value {Ready.}}
2700                return 0
2701        }
2702
2703        foreach path [array names file_states] {
2704                switch -glob -- [lindex $file_states($path) 0] {
2705                _O {
2706                        continue; # and pray it works!
2707                }
2708                U? {
2709                        error_popup "You are in the middle of a conflicted merge.
2710
2711File [short_path $path] has merge conflicts.
2712
2713You must resolve them, add the file, and commit to
2714complete the current merge.  Only then can you
2715begin another merge.
2716"
2717                        unlock_index
2718                        return 0
2719                }
2720                ?? {
2721                        error_popup "You are in the middle of a change.
2722
2723File [short_path $path] is modified.
2724
2725You should complete the current commit before
2726starting a merge.  Doing so will help you abort
2727a failed merge, should the need arise.
2728"
2729                        unlock_index
2730                        return 0
2731                }
2732                }
2733        }
2734
2735        return 1
2736}
2737
2738proc visualize_local_merge {w} {
2739        set revs {}
2740        foreach i [$w.source.l curselection] {
2741                lappend revs [$w.source.l get $i]
2742        }
2743        if {$revs eq {}} return
2744        lappend revs --not HEAD
2745        do_gitk $revs
2746}
2747
2748proc start_local_merge_action {w} {
2749        global HEAD ui_status_value current_branch
2750
2751        set cmd [list git merge]
2752        set names {}
2753        set revcnt 0
2754        foreach i [$w.source.l curselection] {
2755                set b [$w.source.l get $i]
2756                lappend cmd $b
2757                lappend names $b
2758                incr revcnt
2759        }
2760
2761        if {$revcnt == 0} {
2762                return
2763        } elseif {$revcnt == 1} {
2764                set unit branch
2765        } elseif {$revcnt <= 15} {
2766                set unit branches
2767        } else {
2768                tk_messageBox \
2769                        -icon error \
2770                        -type ok \
2771                        -title [wm title $w] \
2772                        -parent $w \
2773                        -message "Too many branches selected.
2774
2775You have requested to merge $revcnt branches
2776in an octopus merge.  This exceeds Git's
2777internal limit of 15 branches per merge.
2778
2779Please select fewer branches.  To merge more
2780than 15 branches, merge the branches in batches.
2781"
2782                return
2783        }
2784
2785        set msg "Merging $current_branch, [join $names {, }]"
2786        set ui_status_value "$msg..."
2787        set cons [new_console "Merge" $msg]
2788        console_exec $cons $cmd [list finish_merge $revcnt]
2789        bind $w <Destroy> {}
2790        destroy $w
2791}
2792
2793proc finish_merge {revcnt w ok} {
2794        console_done $w $ok
2795        if {$ok} {
2796                set msg {Merge completed successfully.}
2797        } else {
2798                if {$revcnt != 1} {
2799                        info_popup "Octopus merge failed.
2800
2801Your merge of $revcnt branches has failed.
2802
2803There are file-level conflicts between the
2804branches which must be resolved manually.
2805
2806The working directory will now be reset.
2807
2808You can attempt this merge again
2809by merging only one branch at a time." $w
2810
2811                        set fd [open "| git read-tree --reset -u HEAD" r]
2812                        fconfigure $fd -blocking 0 -translation binary
2813                        fileevent $fd readable [list reset_hard_wait $fd]
2814                        set ui_status_value {Aborting... please wait...}
2815                        return
2816                }
2817
2818                set msg {Merge failed.  Conflict resolution is required.}
2819        }
2820        unlock_index
2821        rescan [list set ui_status_value $msg]
2822}
2823
2824proc do_local_merge {} {
2825        global current_branch
2826
2827        if {![can_merge]} return
2828
2829        set w .merge_setup
2830        toplevel $w
2831        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
2832
2833        label $w.header \
2834                -text "Merge Into $current_branch" \
2835                -font font_uibold
2836        pack $w.header -side top -fill x
2837
2838        frame $w.buttons
2839        button $w.buttons.visualize -text Visualize \
2840                -font font_ui \
2841                -command [list visualize_local_merge $w]
2842        pack $w.buttons.visualize -side left
2843        button $w.buttons.create -text Merge \
2844                -font font_ui \
2845                -command [list start_local_merge_action $w]
2846        pack $w.buttons.create -side right
2847        button $w.buttons.cancel -text {Cancel} \
2848                -font font_ui \
2849                -command [list destroy $w]
2850        pack $w.buttons.cancel -side right -padx 5
2851        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
2852
2853        labelframe $w.source \
2854                -text {Source Branches} \
2855                -font font_ui
2856        listbox $w.source.l \
2857                -height 10 \
2858                -width 70 \
2859                -selectmode extended \
2860                -yscrollcommand [list $w.source.sby set] \
2861                -font font_ui
2862        scrollbar $w.source.sby -command [list $w.source.l yview]
2863        pack $w.source.sby -side right -fill y
2864        pack $w.source.l -side left -fill both -expand 1
2865        pack $w.source -fill both -expand 1 -pady 5 -padx 5
2866
2867        set cmd [list git for-each-ref]
2868        lappend cmd {--format=%(objectname) %(refname)}
2869        lappend cmd refs/heads
2870        lappend cmd refs/remotes
2871        set fr_fd [open "| $cmd" r]
2872        fconfigure $fr_fd -translation binary
2873        while {[gets $fr_fd line] > 0} {
2874                set line [split $line { }]
2875                set sha1([lindex $line 0]) [lindex $line 1]
2876        }
2877        close $fr_fd
2878
2879        set to_show {}
2880        set fr_fd [open "| git rev-list --all --not HEAD"]
2881        while {[gets $fr_fd line] > 0} {
2882                if {[catch {set ref $sha1($line)}]} continue
2883                regsub ^refs/(heads|remotes)/ $ref {} ref
2884                lappend to_show $ref
2885        }
2886        close $fr_fd
2887
2888        foreach ref [lsort -unique $to_show] {
2889                $w.source.l insert end $ref
2890        }
2891
2892        bind $w <Visibility> "grab $w"
2893        bind $w <Key-Escape> "unlock_index;destroy $w"
2894        bind $w <Destroy> unlock_index
2895        wm title $w "[appname] ([reponame]): Merge"
2896        tkwait window $w
2897}
2898
2899proc do_reset_hard {} {
2900        global HEAD commit_type file_states
2901
2902        if {[string match amend* $commit_type]} {
2903                info_popup {Cannot abort while amending.
2904
2905You must finish amending this commit.
2906}
2907                return
2908        }
2909
2910        if {![lock_index abort]} return
2911
2912        if {[string match *merge* $commit_type]} {
2913                set op merge
2914        } else {
2915                set op commit
2916        }
2917
2918        if {[ask_popup "Abort $op?
2919
2920Aborting the current $op will cause
2921*ALL* uncommitted changes to be lost.
2922
2923Continue with aborting the current $op?"] eq {yes}} {
2924                set fd [open "| git read-tree --reset -u HEAD" r]
2925                fconfigure $fd -blocking 0 -translation binary
2926                fileevent $fd readable [list reset_hard_wait $fd]
2927                set ui_status_value {Aborting... please wait...}
2928        } else {
2929                unlock_index
2930        }
2931}
2932
2933proc reset_hard_wait {fd} {
2934        global ui_comm
2935
2936        read $fd
2937        if {[eof $fd]} {
2938                close $fd
2939                unlock_index
2940
2941                $ui_comm delete 0.0 end
2942                $ui_comm edit modified false
2943
2944                catch {file delete [gitdir MERGE_HEAD]}
2945                catch {file delete [gitdir rr-cache MERGE_RR]}
2946                catch {file delete [gitdir SQUASH_MSG]}
2947                catch {file delete [gitdir MERGE_MSG]}
2948                catch {file delete [gitdir GITGUI_MSG]}
2949
2950                rescan {set ui_status_value {Abort completed.  Ready.}}
2951        }
2952}
2953
2954######################################################################
2955##
2956## browser
2957
2958set next_browser_id 0
2959
2960proc new_browser {commit} {
2961        global next_browser_id cursor_ptr M1B
2962        global browser_commit browser_status browser_stack browser_path browser_busy
2963
2964        set w .browser[incr next_browser_id]
2965        set w_list $w.list.l
2966        set browser_commit($w_list) $commit
2967        set browser_status($w_list) {Starting...}
2968        set browser_stack($w_list) {}
2969        set browser_path($w_list) $browser_commit($w_list):
2970        set browser_busy($w_list) 1
2971
2972        toplevel $w
2973        label $w.path -textvariable browser_path($w_list) \
2974                -anchor w \
2975                -justify left \
2976                -borderwidth 1 \
2977                -relief sunken \
2978                -font font_uibold
2979        pack $w.path -anchor w -side top -fill x
2980
2981        frame $w.list
2982        text $w_list -background white -borderwidth 0 \
2983                -cursor $cursor_ptr \
2984                -state disabled \
2985                -wrap none \
2986                -height 20 \
2987                -width 70 \
2988                -xscrollcommand [list $w.list.sbx set] \
2989                -yscrollcommand [list $w.list.sby set] \
2990                -font font_ui
2991        $w_list tag conf in_sel \
2992                -background [$w_list cget -foreground] \
2993                -foreground [$w_list cget -background]
2994        scrollbar $w.list.sbx -orient h -command [list $w_list xview]
2995        scrollbar $w.list.sby -orient v -command [list $w_list yview]
2996        pack $w.list.sbx -side bottom -fill x
2997        pack $w.list.sby -side right -fill y
2998        pack $w_list -side left -fill both -expand 1
2999        pack $w.list -side top -fill both -expand 1
3000
3001        label $w.status -textvariable browser_status($w_list) \
3002                -anchor w \
3003                -justify left \
3004                -borderwidth 1 \
3005                -relief sunken \
3006                -font font_ui
3007        pack $w.status -anchor w -side bottom -fill x
3008
3009        bind $w_list <Button-1>        "browser_click 0 $w_list @%x,%y;break"
3010        bind $w_list <Double-Button-1> "browser_click 1 $w_list @%x,%y;break"
3011        bind $w_list <$M1B-Up>         "browser_parent $w_list;break"
3012        bind $w_list <$M1B-Left>       "browser_parent $w_list;break"
3013        bind $w_list <Up>              "browser_move -1 $w_list;break"
3014        bind $w_list <Down>            "browser_move 1 $w_list;break"
3015        bind $w_list <$M1B-Right>      "browser_enter $w_list;break"
3016        bind $w_list <Return>          "browser_enter $w_list;break"
3017        bind $w_list <Prior>           "browser_page -1 $w_list;break"
3018        bind $w_list <Next>            "browser_page 1 $w_list;break"
3019        bind $w_list <Left>            break
3020        bind $w_list <Right>           break
3021
3022        bind $w <Visibility> "focus $w"
3023        bind $w <Destroy> "
3024                array unset browser_buffer $w_list
3025                array unset browser_files $w_list
3026                array unset browser_status $w_list
3027                array unset browser_stack $w_list
3028                array unset browser_path $w_list
3029                array unset browser_commit $w_list
3030                array unset browser_busy $w_list
3031        "
3032        wm title $w "[appname] ([reponame]): File Browser"
3033        ls_tree $w_list $browser_commit($w_list) {}
3034}
3035
3036proc browser_move {dir w} {
3037        global browser_files browser_busy
3038
3039        if {$browser_busy($w)} return
3040        set lno [lindex [split [$w index in_sel.first] .] 0]
3041        incr lno $dir
3042        if {[lindex $browser_files($w) [expr {$lno - 1}]] ne {}} {
3043                $w tag remove in_sel 0.0 end
3044                $w tag add in_sel $lno.0 [expr {$lno + 1}].0
3045                $w see $lno.0
3046        }
3047}
3048
3049proc browser_page {dir w} {
3050        global browser_files browser_busy
3051
3052        if {$browser_busy($w)} return
3053        $w yview scroll $dir pages
3054        set lno [expr {int(
3055                  [lindex [$w yview] 0]
3056                * [llength $browser_files($w)]
3057                + 1)}]
3058        if {[lindex $browser_files($w) [expr {$lno - 1}]] ne {}} {
3059                $w tag remove in_sel 0.0 end
3060                $w tag add in_sel $lno.0 [expr {$lno + 1}].0
3061                $w see $lno.0
3062        }
3063}
3064
3065proc browser_parent {w} {
3066        global browser_files browser_status browser_path
3067        global browser_stack browser_busy
3068
3069        if {$browser_busy($w)} return
3070        set info [lindex $browser_files($w) 0]
3071        if {[lindex $info 0] eq {parent}} {
3072                set parent [lindex $browser_stack($w) end-1]
3073                set browser_stack($w) [lrange $browser_stack($w) 0 end-2]
3074                if {$browser_stack($w) eq {}} {
3075                        regsub {:.*$} $browser_path($w) {:} browser_path($w)
3076                } else {
3077                        regsub {/[^/]+$} $browser_path($w) {} browser_path($w)
3078                }
3079                set browser_status($w) "Loading $browser_path($w)..."
3080                ls_tree $w [lindex $parent 0] [lindex $parent 1]
3081        }
3082}
3083
3084proc browser_enter {w} {
3085        global browser_files browser_status browser_path
3086        global browser_commit browser_stack browser_busy
3087
3088        if {$browser_busy($w)} return
3089        set lno [lindex [split [$w index in_sel.first] .] 0]
3090        set info [lindex $browser_files($w) [expr {$lno - 1}]]
3091        if {$info ne {}} {
3092                switch -- [lindex $info 0] {
3093                parent {
3094                        browser_parent $w
3095                }
3096                tree {
3097                        set name [lindex $info 2]
3098                        set escn [escape_path $name]
3099                        set browser_status($w) "Loading $escn..."
3100                        append browser_path($w) $escn
3101                        ls_tree $w [lindex $info 1] $name
3102                }
3103                blob {
3104                        set name [lindex $info 2]
3105                        set p {}
3106                        foreach n $browser_stack($w) {
3107                                append p [lindex $n 1]
3108                        }
3109                        append p $name
3110                        show_blame $browser_commit($w) $p
3111                }
3112                }
3113        }
3114}
3115
3116proc browser_click {was_double_click w pos} {
3117        global browser_files browser_busy
3118
3119        if {$browser_busy($w)} return
3120        set lno [lindex [split [$w index $pos] .] 0]
3121        focus $w
3122
3123        if {[lindex $browser_files($w) [expr {$lno - 1}]] ne {}} {
3124                $w tag remove in_sel 0.0 end
3125                $w tag add in_sel $lno.0 [expr {$lno + 1}].0
3126                if {$was_double_click} {
3127                        browser_enter $w
3128                }
3129        }
3130}
3131
3132proc ls_tree {w tree_id name} {
3133        global browser_buffer browser_files browser_stack browser_busy
3134
3135        set browser_buffer($w) {}
3136        set browser_files($w) {}
3137        set browser_busy($w) 1
3138
3139        $w conf -state normal
3140        $w tag remove in_sel 0.0 end
3141        $w delete 0.0 end
3142        if {$browser_stack($w) ne {}} {
3143                $w image create end \
3144                        -align center -padx 5 -pady 1 \
3145                        -name icon0 \
3146                        -image file_uplevel
3147                $w insert end {[Up To Parent]}
3148                lappend browser_files($w) parent
3149        }
3150        lappend browser_stack($w) [list $tree_id $name]
3151        $w conf -state disabled
3152
3153        set cmd [list git ls-tree -z $tree_id]
3154        set fd [open "| $cmd" r]
3155        fconfigure $fd -blocking 0 -translation binary -encoding binary
3156        fileevent $fd readable [list read_ls_tree $fd $w]
3157}
3158
3159proc read_ls_tree {fd w} {
3160        global browser_buffer browser_files browser_status browser_busy
3161
3162        if {![winfo exists $w]} {
3163                catch {close $fd}
3164                return
3165        }
3166
3167        append browser_buffer($w) [read $fd]
3168        set pck [split $browser_buffer($w) "\0"]
3169        set browser_buffer($w) [lindex $pck end]
3170
3171        set n [llength $browser_files($w)]
3172        $w conf -state normal
3173        foreach p [lrange $pck 0 end-1] {
3174                set info [split $p "\t"]
3175                set path [lindex $info 1]
3176                set info [split [lindex $info 0] { }]
3177                set type [lindex $info 1]
3178                set object [lindex $info 2]
3179
3180                switch -- $type {
3181                blob {
3182                        set image file_mod
3183                }
3184                tree {
3185                        set image file_dir
3186                        append path /
3187                }
3188                default {
3189                        set image file_question
3190                }
3191                }
3192
3193                if {$n > 0} {$w insert end "\n"}
3194                $w image create end \
3195                        -align center -padx 5 -pady 1 \
3196                        -name icon[incr n] \
3197                        -image $image
3198                $w insert end [escape_path $path]
3199                lappend browser_files($w) [list $type $object $path]
3200        }
3201        $w conf -state disabled
3202
3203        if {[eof $fd]} {
3204                close $fd
3205                set browser_status($w) Ready.
3206                set browser_busy($w) 0
3207                array unset browser_buffer $w
3208                if {$n > 0} {
3209                        $w tag add in_sel 1.0 2.0
3210                        focus -force $w
3211                }
3212        }
3213}
3214
3215proc show_blame {commit path} {
3216        global next_browser_id blame_status blame_data
3217
3218        set w .browser[incr next_browser_id]
3219        set blame_status($w) {Loading current file content...}
3220        set texts [list]
3221
3222        toplevel $w
3223
3224        label $w.path -text "$commit:$path" \
3225                -anchor w \
3226                -justify left \
3227                -borderwidth 1 \
3228                -relief sunken \
3229                -font font_uibold
3230        pack $w.path -side top -fill x
3231
3232        set hbg #e2effa
3233        frame $w.out
3234        label $w.out.commit_l -text Commit \
3235                -relief solid \
3236                -borderwidth 1 \
3237                -background $hbg \
3238                -font font_uibold
3239        text $w.out.commit_t \
3240                -background white -borderwidth 0 \
3241                -state disabled \
3242                -wrap none \
3243                -height 40 \
3244                -width 9 \
3245                -font font_diff
3246        lappend texts $w.out.commit_t
3247
3248        label $w.out.author_l -text Author \
3249                -relief solid \
3250                -borderwidth 1 \
3251                -background $hbg \
3252                -font font_uibold
3253        text $w.out.author_t \
3254                -background white -borderwidth 0 \
3255                -state disabled \
3256                -wrap none \
3257                -height 40 \
3258                -width 20 \
3259                -font font_diff
3260        lappend texts $w.out.author_t
3261
3262        label $w.out.date_l -text Date \
3263                -relief solid \
3264                -borderwidth 1 \
3265                -background $hbg \
3266                -font font_uibold
3267        text $w.out.date_t \
3268                -background white -borderwidth 0 \
3269                -state disabled \
3270                -wrap none \
3271                -height 40 \
3272                -width [string length "yyyy-mm-dd hh:mm:ss"] \
3273                -font font_diff
3274        lappend texts $w.out.date_t
3275
3276        label $w.out.filename_l -text Filename \
3277                -relief solid \
3278                -borderwidth 1 \
3279                -background $hbg \
3280                -font font_uibold
3281        text $w.out.filename_t \
3282                -background white -borderwidth 0 \
3283                -state disabled \
3284                -wrap none \
3285                -height 40 \
3286                -width 20 \
3287                -font font_diff
3288        lappend texts $w.out.filename_t
3289
3290        label $w.out.origlinenumber_l -text {Orig Line} \
3291                -relief solid \
3292                -borderwidth 1 \
3293                -background $hbg \
3294                -font font_uibold
3295        text $w.out.origlinenumber_t \
3296                -background white -borderwidth 0 \
3297                -state disabled \
3298                -wrap none \
3299                -height 40 \
3300                -width 5 \
3301                -font font_diff
3302        $w.out.origlinenumber_t tag conf linenumber -justify right
3303        lappend texts $w.out.origlinenumber_t
3304
3305        label $w.out.linenumber_l -text {Curr Line} \
3306                -relief solid \
3307                -borderwidth 1 \
3308                -background $hbg \
3309                -font font_uibold
3310        text $w.out.linenumber_t \
3311                -background white -borderwidth 0 \
3312                -state disabled \
3313                -wrap none \
3314                -height 40 \
3315                -width 5 \
3316                -font font_diff
3317        $w.out.linenumber_t tag conf linenumber -justify right
3318        lappend texts $w.out.linenumber_t
3319
3320        label $w.out.file_l -text {File Content} \
3321                -relief solid \
3322                -borderwidth 1 \
3323                -background $hbg \
3324                -font font_uibold
3325        text $w.out.file_t \
3326                -background white -borderwidth 0 \
3327                -state disabled \
3328                -wrap none \
3329                -height 40 \
3330                -width 80 \
3331                -xscrollcommand [list $w.out.sbx set] \
3332                -font font_diff
3333        lappend texts $w.out.file_t
3334
3335        scrollbar $w.out.sbx -orient h -command [list $w.out.file_t xview]
3336        scrollbar $w.out.sby -orient v \
3337                -command [list scrollbar2many $texts yview]
3338        set labels [list]
3339        foreach i $texts {
3340                regsub {_t$} $i _l l
3341                lappend labels $l
3342        }
3343        set file_col [expr {[llength $texts] - 1}]
3344        eval grid $labels -sticky we
3345        eval grid $texts $w.out.sby -sticky nsew
3346        grid conf $w.out.sbx -column $file_col -sticky we
3347        grid columnconfigure $w.out $file_col -weight 1
3348        grid rowconfigure $w.out 1 -weight 1
3349        pack $w.out -fill both -expand 1
3350
3351        label $w.status -textvariable blame_status($w) \
3352                -anchor w \
3353                -justify left \
3354                -borderwidth 1 \
3355                -relief sunken \
3356                -font font_ui
3357        pack $w.status -side bottom -fill x
3358
3359        menu $w.ctxm -tearoff 0
3360        $w.ctxm add command -label "Copy Commit" \
3361                -font font_ui \
3362                -command "blame_copycommit $w \$cursorW @\$cursorX,\$cursorY"
3363
3364        foreach i $texts {
3365                $i tag conf in_sel \
3366                        -background [$i cget -foreground] \
3367                        -foreground [$i cget -background]
3368                $i conf -yscrollcommand \
3369                        [list many2scrollbar $texts yview $w.out.sby]
3370                bind $i <Button-1> "blame_highlight $i @%x,%y $texts;break"
3371                bind_button3 $i "
3372                        set cursorX %x
3373                        set cursorY %y
3374                        set cursorW %W
3375                        tk_popup $w.ctxm %X %Y
3376                "
3377        }
3378
3379        set blame_data($w,colors) {}
3380
3381        bind $w <Visibility> "focus $w"
3382        bind $w <Destroy> "
3383                array unset blame_status $w
3384                array unset blame_data $w,*
3385        "
3386        wm title $w "[appname] ([reponame]): File Viewer"
3387
3388        set blame_data($w,total_lines) 0
3389        set cmd [list git cat-file blob "$commit:$path"]
3390        set fd [open "| $cmd" r]
3391        fconfigure $fd -blocking 0 -translation lf -encoding binary
3392        fileevent $fd readable [list read_blame_catfile \
3393                $fd $w $commit $path \
3394                $texts $w.out.linenumber_t $w.out.file_t]
3395}
3396
3397proc read_blame_catfile {fd w commit path texts w_lno w_file} {
3398        global blame_status blame_data
3399
3400        if {![winfo exists $w_file]} {
3401                catch {close $fd}
3402                return
3403        }
3404
3405        set n $blame_data($w,total_lines)
3406        foreach i $texts {$i conf -state normal}
3407        while {[gets $fd line] >= 0} {
3408                regsub "\r\$" $line {} line
3409                incr n
3410                $w_lno insert end $n linenumber
3411                $w_file insert end $line
3412                foreach i $texts {$i insert end "\n"}
3413        }
3414        foreach i $texts {$i conf -state disabled}
3415        set blame_data($w,total_lines) $n
3416
3417        if {[eof $fd]} {
3418                close $fd
3419                set blame_status($w) {Loading annotations...}
3420                set cmd [list git blame -M -C --incremental]
3421                lappend cmd $commit -- $path
3422                set fd [open "| $cmd" r]
3423                fconfigure $fd -blocking 0 -translation lf -encoding binary
3424                fileevent $fd readable "read_blame_incremental $fd $w $texts"
3425        }
3426}
3427
3428proc read_blame_incremental {fd w
3429        w_commit w_author w_date w_filename w_olno
3430        w_lno w_file} {
3431        global blame_status blame_data
3432
3433        if {![winfo exists $w_commit]} {
3434                catch {close $fd}
3435                return
3436        }
3437
3438        set all [list \
3439                $w_commit \
3440                $w_author \
3441                $w_date \
3442                $w_filename \
3443                $w_olno \
3444                $w_lno \
3445                $w_file]
3446
3447        $w_commit conf -state normal
3448        $w_author conf -state normal
3449        $w_date conf -state normal
3450        $w_filename conf -state normal
3451        $w_olno conf -state normal
3452
3453        while {[gets $fd line] >= 0} {
3454                if {[regexp {^([a-z0-9]{40}) (\d+) (\d+) (\d+)$} $line line \
3455                        cmit original_line final_line line_count]} {
3456                        set blame_data($w,commit) $cmit
3457                        set blame_data($w,original_line) $original_line
3458                        set blame_data($w,final_line) $final_line
3459                        set blame_data($w,line_count) $line_count
3460
3461                        if {[catch {set g $blame_data($w,$cmit,seen)}]} {
3462                                if {$blame_data($w,colors) eq {}} {
3463                                        set blame_data($w,colors) {
3464                                                yellow
3465                                                red
3466                                                pink
3467                                                orange
3468                                                green
3469                                                grey
3470                                        }
3471                                }
3472                                set c [lindex $blame_data($w,colors) 0]
3473                                set blame_data($w,colors) \
3474                                        [lrange $blame_data($w,colors) 1 end]
3475                                foreach t $all {
3476                                        $t tag conf g$cmit -background $c
3477                                }
3478                        } else {
3479                                set blame_data($w,$cmit,seen) 1
3480                        }
3481                } elseif {[string match {filename *} $line]} {
3482                        set n $blame_data($w,line_count)
3483                        set lno $blame_data($w,final_line)
3484                        set ol $blame_data($w,original_line)
3485                        set file [string range $line 9 end]
3486                        set cmit $blame_data($w,commit)
3487                        set abbrev [string range $cmit 0 8]
3488
3489                        if {[catch {set author $blame_data($w,$cmit,author)} err]} {
3490                                set author {}
3491                        }
3492
3493                        if {[catch {set atime $blame_data($w,$cmit,author-time)}]} {
3494                                set atime {}
3495                        } else {
3496                                set atime [clock format $atime -format {%Y-%m-%d %T}]
3497                        }
3498
3499                        while {$n > 0} {
3500                                if {![catch {set g g$blame_data($w,line$lno,commit)}]} {
3501                                        foreach t $all {
3502                                                $t tag remove $g $lno.0 "$lno.0 lineend + 1c"
3503                                        }
3504                                }
3505
3506                                foreach t [list \
3507                                        $w_commit \
3508                                        $w_author \
3509                                        $w_date \
3510                                        $w_filename \
3511                                        $w_olno] {
3512                                        $t delete $lno.0 "$lno.0 lineend"
3513                                }
3514
3515                                $w_commit insert $lno.0 $abbrev
3516                                $w_author insert $lno.0 $author
3517                                $w_date insert $lno.0 $atime
3518                                $w_filename insert $lno.0 $file
3519                                $w_olno insert $lno.0 $ol linenumber
3520
3521                                set g g$cmit
3522                                foreach t $all {
3523                                        $t tag add $g $lno.0 "$lno.0 lineend + 1c"
3524                                }
3525
3526                                set blame_data($w,line$lno,commit) $cmit
3527
3528                                incr n -1
3529                                incr lno
3530                                incr ol
3531                        }
3532                } elseif {[regexp {^([a-z-]+) (.*)$} $line line header data]} {
3533                        set blame_data($w,$blame_data($w,commit),$header) $data
3534                }
3535        }
3536
3537        $w_commit conf -state disabled
3538        $w_author conf -state disabled
3539        $w_date conf -state disabled
3540        $w_filename conf -state disabled
3541        $w_olno conf -state disabled
3542
3543        if {[eof $fd]} {
3544                close $fd
3545                set blame_status($w) {Annotation complete.}
3546        }
3547}
3548
3549proc blame_highlight {w pos args} {
3550        set lno [lindex [split [$w index $pos] .] 0]
3551        foreach i $args {
3552                $i tag remove in_sel 0.0 end
3553        }
3554        if {$lno eq {}} return
3555        foreach i $args {
3556                $i tag add in_sel $lno.0 "$lno.0 + 1 line"
3557        }
3558}
3559
3560proc blame_copycommit {w i pos} {
3561        global blame_data
3562        set lno [lindex [split [$i index $pos] .] 0]
3563        if {![catch {set commit $blame_data($w,line$lno,commit)}]} {
3564                clipboard clear
3565                clipboard append \
3566                        -format STRING \
3567                        -type STRING \
3568                        -- $commit
3569        }
3570}
3571
3572######################################################################
3573##
3574## icons
3575
3576set filemask {
3577#define mask_width 14
3578#define mask_height 15
3579static unsigned char mask_bits[] = {
3580   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
3581   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
3582   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f};
3583}
3584
3585image create bitmap file_plain -background white -foreground black -data {
3586#define plain_width 14
3587#define plain_height 15
3588static unsigned char plain_bits[] = {
3589   0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
3590   0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10,
3591   0x02, 0x10, 0x02, 0x10, 0xfe, 0x1f};
3592} -maskdata $filemask
3593
3594image create bitmap file_mod -background white -foreground blue -data {
3595#define mod_width 14
3596#define mod_height 15
3597static unsigned char mod_bits[] = {
3598   0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
3599   0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
3600   0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
3601} -maskdata $filemask
3602
3603image create bitmap file_fulltick -background white -foreground "#007000" -data {
3604#define file_fulltick_width 14
3605#define file_fulltick_height 15
3606static unsigned char file_fulltick_bits[] = {
3607   0xfe, 0x01, 0x02, 0x1a, 0x02, 0x0c, 0x02, 0x0c, 0x02, 0x16, 0x02, 0x16,
3608   0x02, 0x13, 0x00, 0x13, 0x86, 0x11, 0x8c, 0x11, 0xd8, 0x10, 0xf2, 0x10,
3609   0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
3610} -maskdata $filemask
3611
3612image create bitmap file_parttick -background white -foreground "#005050" -data {
3613#define parttick_width 14
3614#define parttick_height 15
3615static unsigned char parttick_bits[] = {
3616   0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
3617   0x7a, 0x14, 0x02, 0x16, 0x02, 0x13, 0x8a, 0x11, 0xda, 0x10, 0x72, 0x10,
3618   0x22, 0x10, 0x02, 0x10, 0xfe, 0x1f};
3619} -maskdata $filemask
3620
3621image create bitmap file_question -background white -foreground black -data {
3622#define file_question_width 14
3623#define file_question_height 15
3624static unsigned char file_question_bits[] = {
3625   0xfe, 0x01, 0x02, 0x02, 0xe2, 0x04, 0xf2, 0x09, 0x1a, 0x1b, 0x0a, 0x13,
3626   0x82, 0x11, 0xc2, 0x10, 0x62, 0x10, 0x62, 0x10, 0x02, 0x10, 0x62, 0x10,
3627   0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
3628} -maskdata $filemask
3629
3630image create bitmap file_removed -background white -foreground red -data {
3631#define file_removed_width 14
3632#define file_removed_height 15
3633static unsigned char file_removed_bits[] = {
3634   0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
3635   0x1a, 0x16, 0x32, 0x13, 0xe2, 0x11, 0xc2, 0x10, 0xe2, 0x11, 0x32, 0x13,
3636   0x1a, 0x16, 0x02, 0x10, 0xfe, 0x1f};
3637} -maskdata $filemask
3638
3639image create bitmap file_merge -background white -foreground blue -data {
3640#define file_merge_width 14
3641#define file_merge_height 15
3642static unsigned char file_merge_bits[] = {
3643   0xfe, 0x01, 0x02, 0x03, 0x62, 0x05, 0x62, 0x09, 0x62, 0x1f, 0x62, 0x10,
3644   0xfa, 0x11, 0xf2, 0x10, 0x62, 0x10, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
3645   0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
3646} -maskdata $filemask
3647
3648set file_dir_data {
3649#define file_width 18
3650#define file_height 18
3651static unsigned char file_bits[] = {
3652  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x03, 0x00,
3653  0x0c, 0x03, 0x00, 0x04, 0xfe, 0x00, 0x06, 0x80, 0x00, 0xff, 0x9f, 0x00,
3654  0x03, 0x98, 0x00, 0x02, 0x90, 0x00, 0x06, 0xb0, 0x00, 0x04, 0xa0, 0x00,
3655  0x0c, 0xe0, 0x00, 0x08, 0xc0, 0x00, 0xf8, 0xff, 0x00, 0x00, 0x00, 0x00,
3656  0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
3657}
3658image create bitmap file_dir -background white -foreground blue \
3659        -data $file_dir_data -maskdata $file_dir_data
3660unset file_dir_data
3661
3662set file_uplevel_data {
3663#define up_width 15
3664#define up_height 15
3665static unsigned char up_bits[] = {
3666  0x80, 0x00, 0xc0, 0x01, 0xe0, 0x03, 0xf0, 0x07, 0xf8, 0x0f, 0xfc, 0x1f,
3667  0xfe, 0x3f, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01,
3668  0xc0, 0x01, 0xc0, 0x01, 0x00, 0x00};
3669}
3670image create bitmap file_uplevel -background white -foreground red \
3671        -data $file_uplevel_data -maskdata $file_uplevel_data
3672unset file_uplevel_data
3673
3674set ui_index .vpane.files.index.list
3675set ui_workdir .vpane.files.workdir.list
3676
3677set all_icons(_$ui_index)   file_plain
3678set all_icons(A$ui_index)   file_fulltick
3679set all_icons(M$ui_index)   file_fulltick
3680set all_icons(D$ui_index)   file_removed
3681set all_icons(U$ui_index)   file_merge
3682
3683set all_icons(_$ui_workdir) file_plain
3684set all_icons(M$ui_workdir) file_mod
3685set all_icons(D$ui_workdir) file_question
3686set all_icons(U$ui_workdir) file_merge
3687set all_icons(O$ui_workdir) file_plain
3688
3689set max_status_desc 0
3690foreach i {
3691                {__ "Unmodified"}
3692
3693                {_M "Modified, not staged"}
3694                {M_ "Staged for commit"}
3695                {MM "Portions staged for commit"}
3696                {MD "Staged for commit, missing"}
3697
3698                {_O "Untracked, not staged"}
3699                {A_ "Staged for commit"}
3700                {AM "Portions staged for commit"}
3701                {AD "Staged for commit, missing"}
3702
3703                {_D "Missing"}
3704                {D_ "Staged for removal"}
3705                {DO "Staged for removal, still present"}
3706
3707                {U_ "Requires merge resolution"}
3708                {UU "Requires merge resolution"}
3709                {UM "Requires merge resolution"}
3710                {UD "Requires merge resolution"}
3711        } {
3712        if {$max_status_desc < [string length [lindex $i 1]]} {
3713                set max_status_desc [string length [lindex $i 1]]
3714        }
3715        set all_descs([lindex $i 0]) [lindex $i 1]
3716}
3717unset i
3718
3719######################################################################
3720##
3721## util
3722
3723proc bind_button3 {w cmd} {
3724        bind $w <Any-Button-3> $cmd
3725        if {[is_MacOSX]} {
3726                bind $w <Control-Button-1> $cmd
3727        }
3728}
3729
3730proc scrollbar2many {list mode args} {
3731        foreach w $list {eval $w $mode $args}
3732}
3733
3734proc many2scrollbar {list mode sb top bottom} {
3735        $sb set $top $bottom
3736        foreach w $list {$w $mode moveto $top}
3737}
3738
3739proc incr_font_size {font {amt 1}} {
3740        set sz [font configure $font -size]
3741        incr sz $amt
3742        font configure $font -size $sz
3743        font configure ${font}bold -size $sz
3744}
3745
3746proc hook_failed_popup {hook msg} {
3747        set w .hookfail
3748        toplevel $w
3749
3750        frame $w.m
3751        label $w.m.l1 -text "$hook hook failed:" \
3752                -anchor w \
3753                -justify left \
3754                -font font_uibold
3755        text $w.m.t \
3756                -background white -borderwidth 1 \
3757                -relief sunken \
3758                -width 80 -height 10 \
3759                -font font_diff \
3760                -yscrollcommand [list $w.m.sby set]
3761        label $w.m.l2 \
3762                -text {You must correct the above errors before committing.} \
3763                -anchor w \
3764                -justify left \
3765                -font font_uibold
3766        scrollbar $w.m.sby -command [list $w.m.t yview]
3767        pack $w.m.l1 -side top -fill x
3768        pack $w.m.l2 -side bottom -fill x
3769        pack $w.m.sby -side right -fill y
3770        pack $w.m.t -side left -fill both -expand 1
3771        pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
3772
3773        $w.m.t insert 1.0 $msg
3774        $w.m.t conf -state disabled
3775
3776        button $w.ok -text OK \
3777                -width 15 \
3778                -font font_ui \
3779                -command "destroy $w"
3780        pack $w.ok -side bottom -anchor e -pady 10 -padx 10
3781
3782        bind $w <Visibility> "grab $w; focus $w"
3783        bind $w <Key-Return> "destroy $w"
3784        wm title $w "[appname] ([reponame]): error"
3785        tkwait window $w
3786}
3787
3788set next_console_id 0
3789
3790proc new_console {short_title long_title} {
3791        global next_console_id console_data
3792        set w .console[incr next_console_id]
3793        set console_data($w) [list $short_title $long_title]
3794        return [console_init $w]
3795}
3796
3797proc console_init {w} {
3798        global console_cr console_data M1B
3799
3800        set console_cr($w) 1.0
3801        toplevel $w
3802        frame $w.m
3803        label $w.m.l1 -text "[lindex $console_data($w) 1]:" \
3804                -anchor w \
3805                -justify left \
3806                -font font_uibold
3807        text $w.m.t \
3808                -background white -borderwidth 1 \
3809                -relief sunken \
3810                -width 80 -height 10 \
3811                -font font_diff \
3812                -state disabled \
3813                -yscrollcommand [list $w.m.sby set]
3814        label $w.m.s -text {Working... please wait...} \
3815                -anchor w \
3816                -justify left \
3817                -font font_uibold
3818        scrollbar $w.m.sby -command [list $w.m.t yview]
3819        pack $w.m.l1 -side top -fill x
3820        pack $w.m.s -side bottom -fill x
3821        pack $w.m.sby -side right -fill y
3822        pack $w.m.t -side left -fill both -expand 1
3823        pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
3824
3825        menu $w.ctxm -tearoff 0
3826        $w.ctxm add command -label "Copy" \
3827                -font font_ui \
3828                -command "tk_textCopy $w.m.t"
3829        $w.ctxm add command -label "Select All" \
3830                -font font_ui \
3831                -command "focus $w.m.t;$w.m.t tag add sel 0.0 end"
3832        $w.ctxm add command -label "Copy All" \
3833                -font font_ui \
3834                -command "
3835                        $w.m.t tag add sel 0.0 end
3836                        tk_textCopy $w.m.t
3837                        $w.m.t tag remove sel 0.0 end
3838                "
3839
3840        button $w.ok -text {Close} \
3841                -font font_ui \
3842                -state disabled \
3843                -command "destroy $w"
3844        pack $w.ok -side bottom -anchor e -pady 10 -padx 10
3845
3846        bind_button3 $w.m.t "tk_popup $w.ctxm %X %Y"
3847        bind $w.m.t <$M1B-Key-a> "$w.m.t tag add sel 0.0 end;break"
3848        bind $w.m.t <$M1B-Key-A> "$w.m.t tag add sel 0.0 end;break"
3849        bind $w <Visibility> "focus $w"
3850        wm title $w "[appname] ([reponame]): [lindex $console_data($w) 0]"
3851        return $w
3852}
3853
3854proc console_exec {w cmd after} {
3855        # -- Cygwin's Tcl tosses the enviroment when we exec our child.
3856        #    But most users need that so we have to relogin. :-(
3857        #
3858        if {[is_Cygwin]} {
3859                set cmd [list sh --login -c "cd \"[pwd]\" && [join $cmd { }]"]
3860        }
3861
3862        # -- Tcl won't let us redirect both stdout and stderr to
3863        #    the same pipe.  So pass it through cat...
3864        #
3865        set cmd [concat | $cmd |& cat]
3866
3867        set fd_f [open $cmd r]
3868        fconfigure $fd_f -blocking 0 -translation binary
3869        fileevent $fd_f readable [list console_read $w $fd_f $after]
3870}
3871
3872proc console_read {w fd after} {
3873        global console_cr
3874
3875        set buf [read $fd]
3876        if {$buf ne {}} {
3877                if {![winfo exists $w]} {console_init $w}
3878                $w.m.t conf -state normal
3879                set c 0
3880                set n [string length $buf]
3881                while {$c < $n} {
3882                        set cr [string first "\r" $buf $c]
3883                        set lf [string first "\n" $buf $c]
3884                        if {$cr < 0} {set cr [expr {$n + 1}]}
3885                        if {$lf < 0} {set lf [expr {$n + 1}]}
3886
3887                        if {$lf < $cr} {
3888                                $w.m.t insert end [string range $buf $c $lf]
3889                                set console_cr($w) [$w.m.t index {end -1c}]
3890                                set c $lf
3891                                incr c
3892                        } else {
3893                                $w.m.t delete $console_cr($w) end
3894                                $w.m.t insert end "\n"
3895                                $w.m.t insert end [string range $buf $c $cr]
3896                                set c $cr
3897                                incr c
3898                        }
3899                }
3900                $w.m.t conf -state disabled
3901                $w.m.t see end
3902        }
3903
3904        fconfigure $fd -blocking 1
3905        if {[eof $fd]} {
3906                if {[catch {close $fd}]} {
3907                        set ok 0
3908                } else {
3909                        set ok 1
3910                }
3911                uplevel #0 $after $w $ok
3912                return
3913        }
3914        fconfigure $fd -blocking 0
3915}
3916
3917proc console_chain {cmdlist w {ok 1}} {
3918        if {$ok} {
3919                if {[llength $cmdlist] == 0} {
3920                        console_done $w $ok
3921                        return
3922                }
3923
3924                set cmd [lindex $cmdlist 0]
3925                set cmdlist [lrange $cmdlist 1 end]
3926
3927                if {[lindex $cmd 0] eq {console_exec}} {
3928                        console_exec $w \
3929                                [lindex $cmd 1] \
3930                                [list console_chain $cmdlist]
3931                } else {
3932                        uplevel #0 $cmd $cmdlist $w $ok
3933                }
3934        } else {
3935                console_done $w $ok
3936        }
3937}
3938
3939proc console_done {args} {
3940        global console_cr console_data
3941
3942        switch -- [llength $args] {
3943        2 {
3944                set w [lindex $args 0]
3945                set ok [lindex $args 1]
3946        }
3947        3 {
3948                set w [lindex $args 1]
3949                set ok [lindex $args 2]
3950        }
3951        default {
3952                error "wrong number of args: console_done ?ignored? w ok"
3953        }
3954        }
3955
3956        if {$ok} {
3957                if {[winfo exists $w]} {
3958                        $w.m.s conf -background green -text {Success}
3959                        $w.ok conf -state normal
3960                }
3961        } else {
3962                if {![winfo exists $w]} {
3963                        console_init $w
3964                }
3965                $w.m.s conf -background red -text {Error: Command Failed}
3966                $w.ok conf -state normal
3967        }
3968
3969        array unset console_cr $w
3970        array unset console_data $w
3971}
3972
3973######################################################################
3974##
3975## ui commands
3976
3977set starting_gitk_msg {Starting gitk... please wait...}
3978
3979proc do_gitk {revs} {
3980        global env ui_status_value starting_gitk_msg
3981
3982        # -- On Windows gitk is severly broken, and right now it seems like
3983        #    nobody cares about fixing it.  The only known workaround is to
3984        #    always delete ~/.gitk before starting the program.
3985        #
3986        if {[is_Windows]} {
3987                catch {file delete [file join $env(HOME) .gitk]}
3988        }
3989
3990        # -- Always start gitk through whatever we were loaded with.  This
3991        #    lets us bypass using shell process on Windows systems.
3992        #
3993        set cmd [info nameofexecutable]
3994        lappend cmd [gitexec gitk]
3995        if {$revs ne {}} {
3996                append cmd { }
3997                append cmd $revs
3998        }
3999
4000        if {[catch {eval exec $cmd &} err]} {
4001                error_popup "Failed to start gitk:\n\n$err"
4002        } else {
4003                set ui_status_value $starting_gitk_msg
4004                after 10000 {
4005                        if {$ui_status_value eq $starting_gitk_msg} {
4006                                set ui_status_value {Ready.}
4007                        }
4008                }
4009        }
4010}
4011
4012proc do_stats {} {
4013        set fd [open "| git count-objects -v" r]
4014        while {[gets $fd line] > 0} {
4015                if {[regexp {^([^:]+): (\d+)$} $line _ name value]} {
4016                        set stats($name) $value
4017                }
4018        }
4019        close $fd
4020
4021        set packed_sz 0
4022        foreach p [glob -directory [gitdir objects pack] \
4023                -type f \
4024                -nocomplain -- *] {
4025                incr packed_sz [file size $p]
4026        }
4027        if {$packed_sz > 0} {
4028                set stats(size-pack) [expr {$packed_sz / 1024}]
4029        }
4030
4031        set w .stats_view
4032        toplevel $w
4033        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
4034
4035        label $w.header -text {Database Statistics} \
4036                -font font_uibold
4037        pack $w.header -side top -fill x
4038
4039        frame $w.buttons -border 1
4040        button $w.buttons.close -text Close \
4041                -font font_ui \
4042                -command [list destroy $w]
4043        button $w.buttons.gc -text {Compress Database} \
4044                -font font_ui \
4045                -command "destroy $w;do_gc"
4046        pack $w.buttons.close -side right
4047        pack $w.buttons.gc -side left
4048        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
4049
4050        frame $w.stat -borderwidth 1 -relief solid
4051        foreach s {
4052                {count           {Number of loose objects}}
4053                {size            {Disk space used by loose objects} { KiB}}
4054                {in-pack         {Number of packed objects}}
4055                {packs           {Number of packs}}
4056                {size-pack       {Disk space used by packed objects} { KiB}}
4057                {prune-packable  {Packed objects waiting for pruning}}
4058                {garbage         {Garbage files}}
4059                } {
4060                set name [lindex $s 0]
4061                set label [lindex $s 1]
4062                if {[catch {set value $stats($name)}]} continue
4063                if {[llength $s] > 2} {
4064                        set value "$value[lindex $s 2]"
4065                }
4066
4067                label $w.stat.l_$name -text "$label:" -anchor w -font font_ui
4068                label $w.stat.v_$name -text $value -anchor w -font font_ui
4069                grid $w.stat.l_$name $w.stat.v_$name -sticky we -padx {0 5}
4070        }
4071        pack $w.stat -pady 10 -padx 10
4072
4073        bind $w <Visibility> "grab $w; focus $w"
4074        bind $w <Key-Escape> [list destroy $w]
4075        bind $w <Key-Return> [list destroy $w]
4076        wm title $w "[appname] ([reponame]): Database Statistics"
4077        tkwait window $w
4078}
4079
4080proc do_gc {} {
4081        set w [new_console {gc} {Compressing the object database}]
4082        console_chain {
4083                {console_exec {git pack-refs --prune}}
4084                {console_exec {git reflog expire --all}}
4085                {console_exec {git repack -a -d -l}}
4086                {console_exec {git rerere gc}}
4087        } $w
4088}
4089
4090proc do_fsck_objects {} {
4091        set w [new_console {fsck-objects} \
4092                {Verifying the object database with fsck-objects}]
4093        set cmd [list git fsck-objects]
4094        lappend cmd --full
4095        lappend cmd --cache
4096        lappend cmd --strict
4097        console_exec $w $cmd console_done
4098}
4099
4100set is_quitting 0
4101
4102proc do_quit {} {
4103        global ui_comm is_quitting repo_config commit_type
4104
4105        if {$is_quitting} return
4106        set is_quitting 1
4107
4108        # -- Stash our current commit buffer.
4109        #
4110        set save [gitdir GITGUI_MSG]
4111        set msg [string trim [$ui_comm get 0.0 end]]
4112        regsub -all -line {[ \r\t]+$} $msg {} msg
4113        if {(![string match amend* $commit_type]
4114                || [$ui_comm edit modified])
4115                && $msg ne {}} {
4116                catch {
4117                        set fd [open $save w]
4118                        puts -nonewline $fd $msg
4119                        close $fd
4120                }
4121        } else {
4122                catch {file delete $save}
4123        }
4124
4125        # -- Stash our current window geometry into this repository.
4126        #
4127        set cfg_geometry [list]
4128        lappend cfg_geometry [wm geometry .]
4129        lappend cfg_geometry [lindex [.vpane sash coord 0] 1]
4130        lappend cfg_geometry [lindex [.vpane.files sash coord 0] 0]
4131        if {[catch {set rc_geometry $repo_config(gui.geometry)}]} {
4132                set rc_geometry {}
4133        }
4134        if {$cfg_geometry ne $rc_geometry} {
4135                catch {exec git repo-config gui.geometry $cfg_geometry}
4136        }
4137
4138        destroy .
4139}
4140
4141proc do_rescan {} {
4142        rescan {set ui_status_value {Ready.}}
4143}
4144
4145proc unstage_helper {txt paths} {
4146        global file_states current_diff_path
4147
4148        if {![lock_index begin-update]} return
4149
4150        set pathList [list]
4151        set after {}
4152        foreach path $paths {
4153                switch -glob -- [lindex $file_states($path) 0] {
4154                A? -
4155                M? -
4156                D? {
4157                        lappend pathList $path
4158                        if {$path eq $current_diff_path} {
4159                                set after {reshow_diff;}
4160                        }
4161                }
4162                }
4163        }
4164        if {$pathList eq {}} {
4165                unlock_index
4166        } else {
4167                update_indexinfo \
4168                        $txt \
4169                        $pathList \
4170                        [concat $after {set ui_status_value {Ready.}}]
4171        }
4172}
4173
4174proc do_unstage_selection {} {
4175        global current_diff_path selected_paths
4176
4177        if {[array size selected_paths] > 0} {
4178                unstage_helper \
4179                        {Unstaging selected files from commit} \
4180                        [array names selected_paths]
4181        } elseif {$current_diff_path ne {}} {
4182                unstage_helper \
4183                        "Unstaging [short_path $current_diff_path] from commit" \
4184                        [list $current_diff_path]
4185        }
4186}
4187
4188proc add_helper {txt paths} {
4189        global file_states current_diff_path
4190
4191        if {![lock_index begin-update]} return
4192
4193        set pathList [list]
4194        set after {}
4195        foreach path $paths {
4196                switch -glob -- [lindex $file_states($path) 0] {
4197                _O -
4198                ?M -
4199                ?D -
4200                U? {
4201                        lappend pathList $path
4202                        if {$path eq $current_diff_path} {
4203                                set after {reshow_diff;}
4204                        }
4205                }
4206                }
4207        }
4208        if {$pathList eq {}} {
4209                unlock_index
4210        } else {
4211                update_index \
4212                        $txt \
4213                        $pathList \
4214                        [concat $after {set ui_status_value {Ready to commit.}}]
4215        }
4216}
4217
4218proc do_add_selection {} {
4219        global current_diff_path selected_paths
4220
4221        if {[array size selected_paths] > 0} {
4222                add_helper \
4223                        {Adding selected files} \
4224                        [array names selected_paths]
4225        } elseif {$current_diff_path ne {}} {
4226                add_helper \
4227                        "Adding [short_path $current_diff_path]" \
4228                        [list $current_diff_path]
4229        }
4230}
4231
4232proc do_add_all {} {
4233        global file_states
4234
4235        set paths [list]
4236        foreach path [array names file_states] {
4237                switch -glob -- [lindex $file_states($path) 0] {
4238                U? {continue}
4239                ?M -
4240                ?D {lappend paths $path}
4241                }
4242        }
4243        add_helper {Adding all changed files} $paths
4244}
4245
4246proc revert_helper {txt paths} {
4247        global file_states current_diff_path
4248
4249        if {![lock_index begin-update]} return
4250
4251        set pathList [list]
4252        set after {}
4253        foreach path $paths {
4254                switch -glob -- [lindex $file_states($path) 0] {
4255                U? {continue}
4256                ?M -
4257                ?D {
4258                        lappend pathList $path
4259                        if {$path eq $current_diff_path} {
4260                                set after {reshow_diff;}
4261                        }
4262                }
4263                }
4264        }
4265
4266        set n [llength $pathList]
4267        if {$n == 0} {
4268                unlock_index
4269                return
4270        } elseif {$n == 1} {
4271                set s "[short_path [lindex $pathList]]"
4272        } else {
4273                set s "these $n files"
4274        }
4275
4276        set reply [tk_dialog \
4277                .confirm_revert \
4278                "[appname] ([reponame])" \
4279                "Revert changes in $s?
4280
4281Any unadded changes will be permanently lost by the revert." \
4282                question \
4283                1 \
4284                {Do Nothing} \
4285                {Revert Changes} \
4286                ]
4287        if {$reply == 1} {
4288                checkout_index \
4289                        $txt \
4290                        $pathList \
4291                        [concat $after {set ui_status_value {Ready.}}]
4292        } else {
4293                unlock_index
4294        }
4295}
4296
4297proc do_revert_selection {} {
4298        global current_diff_path selected_paths
4299
4300        if {[array size selected_paths] > 0} {
4301                revert_helper \
4302                        {Reverting selected files} \
4303                        [array names selected_paths]
4304        } elseif {$current_diff_path ne {}} {
4305                revert_helper \
4306                        "Reverting [short_path $current_diff_path]" \
4307                        [list $current_diff_path]
4308        }
4309}
4310
4311proc do_signoff {} {
4312        global ui_comm
4313
4314        set me [committer_ident]
4315        if {$me eq {}} return
4316
4317        set sob "Signed-off-by: $me"
4318        set last [$ui_comm get {end -1c linestart} {end -1c}]
4319        if {$last ne $sob} {
4320                $ui_comm edit separator
4321                if {$last ne {}
4322                        && ![regexp {^[A-Z][A-Za-z]*-[A-Za-z-]+: *} $last]} {
4323                        $ui_comm insert end "\n"
4324                }
4325                $ui_comm insert end "\n$sob"
4326                $ui_comm edit separator
4327                $ui_comm see end
4328        }
4329}
4330
4331proc do_select_commit_type {} {
4332        global commit_type selected_commit_type
4333
4334        if {$selected_commit_type eq {new}
4335                && [string match amend* $commit_type]} {
4336                create_new_commit
4337        } elseif {$selected_commit_type eq {amend}
4338                && ![string match amend* $commit_type]} {
4339                load_last_commit
4340
4341                # The amend request was rejected...
4342                #
4343                if {![string match amend* $commit_type]} {
4344                        set selected_commit_type new
4345                }
4346        }
4347}
4348
4349proc do_commit {} {
4350        commit_tree
4351}
4352
4353proc do_about {} {
4354        global appvers copyright
4355        global tcl_patchLevel tk_patchLevel
4356
4357        set w .about_dialog
4358        toplevel $w
4359        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
4360
4361        label $w.header -text "About [appname]" \
4362                -font font_uibold
4363        pack $w.header -side top -fill x
4364
4365        frame $w.buttons
4366        button $w.buttons.close -text {Close} \
4367                -font font_ui \
4368                -command [list destroy $w]
4369        pack $w.buttons.close -side right
4370        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
4371
4372        label $w.desc \
4373                -text "[appname] - a commit creation tool for Git.
4374$copyright" \
4375                -padx 5 -pady 5 \
4376                -justify left \
4377                -anchor w \
4378                -borderwidth 1 \
4379                -relief solid \
4380                -font font_ui
4381        pack $w.desc -side top -fill x -padx 5 -pady 5
4382
4383        set v {}
4384        append v "[appname] version $appvers\n"
4385        append v "[exec git version]\n"
4386        append v "\n"
4387        if {$tcl_patchLevel eq $tk_patchLevel} {
4388                append v "Tcl/Tk version $tcl_patchLevel"
4389        } else {
4390                append v "Tcl version $tcl_patchLevel"
4391                append v ", Tk version $tk_patchLevel"
4392        }
4393
4394        label $w.vers \
4395                -text $v \
4396                -padx 5 -pady 5 \
4397                -justify left \
4398                -anchor w \
4399                -borderwidth 1 \
4400                -relief solid \
4401                -font font_ui
4402        pack $w.vers -side top -fill x -padx 5 -pady 5
4403
4404        menu $w.ctxm -tearoff 0
4405        $w.ctxm add command \
4406                -label {Copy} \
4407                -font font_ui \
4408                -command "
4409                clipboard clear
4410                clipboard append -format STRING -type STRING -- \[$w.vers cget -text\]
4411        "
4412
4413        bind $w <Visibility> "grab $w; focus $w"
4414        bind $w <Key-Escape> "destroy $w"
4415        bind_button3 $w.vers "tk_popup $w.ctxm %X %Y; grab $w; focus $w"
4416        wm title $w "About [appname]"
4417        tkwait window $w
4418}
4419
4420proc do_options {} {
4421        global repo_config global_config font_descs
4422        global repo_config_new global_config_new
4423
4424        array unset repo_config_new
4425        array unset global_config_new
4426        foreach name [array names repo_config] {
4427                set repo_config_new($name) $repo_config($name)
4428        }
4429        load_config 1
4430        foreach name [array names repo_config] {
4431                switch -- $name {
4432                gui.diffcontext {continue}
4433                }
4434                set repo_config_new($name) $repo_config($name)
4435        }
4436        foreach name [array names global_config] {
4437                set global_config_new($name) $global_config($name)
4438        }
4439
4440        set w .options_editor
4441        toplevel $w
4442        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
4443
4444        label $w.header -text "[appname] Options" \
4445                -font font_uibold
4446        pack $w.header -side top -fill x
4447
4448        frame $w.buttons
4449        button $w.buttons.restore -text {Restore Defaults} \
4450                -font font_ui \
4451                -command do_restore_defaults
4452        pack $w.buttons.restore -side left
4453        button $w.buttons.save -text Save \
4454                -font font_ui \
4455                -command [list do_save_config $w]
4456        pack $w.buttons.save -side right
4457        button $w.buttons.cancel -text {Cancel} \
4458                -font font_ui \
4459                -command [list destroy $w]
4460        pack $w.buttons.cancel -side right -padx 5
4461        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
4462
4463        labelframe $w.repo -text "[reponame] Repository" \
4464                -font font_ui
4465        labelframe $w.global -text {Global (All Repositories)} \
4466                -font font_ui
4467        pack $w.repo -side left -fill both -expand 1 -pady 5 -padx 5
4468        pack $w.global -side right -fill both -expand 1 -pady 5 -padx 5
4469
4470        set optid 0
4471        foreach option {
4472                {t user.name {User Name}}
4473                {t user.email {Email Address}}
4474
4475                {b merge.summary {Summarize Merge Commits}}
4476                {i-1..5 merge.verbosity {Merge Verbosity}}
4477
4478                {b gui.trustmtime  {Trust File Modification Timestamps}}
4479                {i-1..99 gui.diffcontext {Number of Diff Context Lines}}
4480                {t gui.newbranchtemplate {New Branch Name Template}}
4481                } {
4482                set type [lindex $option 0]
4483                set name [lindex $option 1]
4484                set text [lindex $option 2]
4485                incr optid
4486                foreach f {repo global} {
4487                        switch -glob -- $type {
4488                        b {
4489                                checkbutton $w.$f.$optid -text $text \
4490                                        -variable ${f}_config_new($name) \
4491                                        -onvalue true \
4492                                        -offvalue false \
4493                                        -font font_ui
4494                                pack $w.$f.$optid -side top -anchor w
4495                        }
4496                        i-* {
4497                                regexp -- {-(\d+)\.\.(\d+)$} $type _junk min max
4498                                frame $w.$f.$optid
4499                                label $w.$f.$optid.l -text "$text:" -font font_ui
4500                                pack $w.$f.$optid.l -side left -anchor w -fill x
4501                                spinbox $w.$f.$optid.v \
4502                                        -textvariable ${f}_config_new($name) \
4503                                        -from $min \
4504                                        -to $max \
4505                                        -increment 1 \
4506                                        -width [expr {1 + [string length $max]}] \
4507                                        -font font_ui
4508                                bind $w.$f.$optid.v <FocusIn> {%W selection range 0 end}
4509                                pack $w.$f.$optid.v -side right -anchor e -padx 5
4510                                pack $w.$f.$optid -side top -anchor w -fill x
4511                        }
4512                        t {
4513                                frame $w.$f.$optid
4514                                label $w.$f.$optid.l -text "$text:" -font font_ui
4515                                entry $w.$f.$optid.v \
4516                                        -borderwidth 1 \
4517                                        -relief sunken \
4518                                        -width 20 \
4519                                        -textvariable ${f}_config_new($name) \
4520                                        -font font_ui
4521                                pack $w.$f.$optid.l -side left -anchor w
4522                                pack $w.$f.$optid.v -side left -anchor w \
4523                                        -fill x -expand 1 \
4524                                        -padx 5
4525                                pack $w.$f.$optid -side top -anchor w -fill x
4526                        }
4527                        }
4528                }
4529        }
4530
4531        set all_fonts [lsort [font families]]
4532        foreach option $font_descs {
4533                set name [lindex $option 0]
4534                set font [lindex $option 1]
4535                set text [lindex $option 2]
4536
4537                set global_config_new(gui.$font^^family) \
4538                        [font configure $font -family]
4539                set global_config_new(gui.$font^^size) \
4540                        [font configure $font -size]
4541
4542                frame $w.global.$name
4543                label $w.global.$name.l -text "$text:" -font font_ui
4544                pack $w.global.$name.l -side left -anchor w -fill x
4545                eval tk_optionMenu $w.global.$name.family \
4546                        global_config_new(gui.$font^^family) \
4547                        $all_fonts
4548                spinbox $w.global.$name.size \
4549                        -textvariable global_config_new(gui.$font^^size) \
4550                        -from 2 -to 80 -increment 1 \
4551                        -width 3 \
4552                        -font font_ui
4553                bind $w.global.$name.size <FocusIn> {%W selection range 0 end}
4554                pack $w.global.$name.size -side right -anchor e
4555                pack $w.global.$name.family -side right -anchor e
4556                pack $w.global.$name -side top -anchor w -fill x
4557        }
4558
4559        bind $w <Visibility> "grab $w; focus $w"
4560        bind $w <Key-Escape> "destroy $w"
4561        wm title $w "[appname] ([reponame]): Options"
4562        tkwait window $w
4563}
4564
4565proc do_restore_defaults {} {
4566        global font_descs default_config repo_config
4567        global repo_config_new global_config_new
4568
4569        foreach name [array names default_config] {
4570                set repo_config_new($name) $default_config($name)
4571                set global_config_new($name) $default_config($name)
4572        }
4573
4574        foreach option $font_descs {
4575                set name [lindex $option 0]
4576                set repo_config(gui.$name) $default_config(gui.$name)
4577        }
4578        apply_config
4579
4580        foreach option $font_descs {
4581                set name [lindex $option 0]
4582                set font [lindex $option 1]
4583                set global_config_new(gui.$font^^family) \
4584                        [font configure $font -family]
4585                set global_config_new(gui.$font^^size) \
4586                        [font configure $font -size]
4587        }
4588}
4589
4590proc do_save_config {w} {
4591        if {[catch {save_config} err]} {
4592                error_popup "Failed to completely save options:\n\n$err"
4593        }
4594        reshow_diff
4595        destroy $w
4596}
4597
4598proc do_windows_shortcut {} {
4599        global argv0
4600
4601        set fn [tk_getSaveFile \
4602                -parent . \
4603                -title "[appname] ([reponame]): Create Desktop Icon" \
4604                -initialfile "Git [reponame].bat"]
4605        if {$fn != {}} {
4606                if {[catch {
4607                                set fd [open $fn w]
4608                                puts $fd "@ECHO Entering [reponame]"
4609                                puts $fd "@ECHO Starting git-gui... please wait..."
4610                                puts $fd "@SET PATH=[file normalize [gitexec]];%PATH%"
4611                                puts $fd "@SET GIT_DIR=[file normalize [gitdir]]"
4612                                puts -nonewline $fd "@\"[info nameofexecutable]\""
4613                                puts $fd " \"[file normalize $argv0]\""
4614                                close $fd
4615                        } err]} {
4616                        error_popup "Cannot write script:\n\n$err"
4617                }
4618        }
4619}
4620
4621proc do_cygwin_shortcut {} {
4622        global argv0
4623
4624        if {[catch {
4625                set desktop [exec cygpath \
4626                        --windows \
4627                        --absolute \
4628                        --long-name \
4629                        --desktop]
4630                }]} {
4631                        set desktop .
4632        }
4633        set fn [tk_getSaveFile \
4634                -parent . \
4635                -title "[appname] ([reponame]): Create Desktop Icon" \
4636                -initialdir $desktop \
4637                -initialfile "Git [reponame].bat"]
4638        if {$fn != {}} {
4639                if {[catch {
4640                                set fd [open $fn w]
4641                                set sh [exec cygpath \
4642                                        --windows \
4643                                        --absolute \
4644                                        /bin/sh]
4645                                set me [exec cygpath \
4646                                        --unix \
4647                                        --absolute \
4648                                        $argv0]
4649                                set gd [exec cygpath \
4650                                        --unix \
4651                                        --absolute \
4652                                        [gitdir]]
4653                                set gw [exec cygpath \
4654                                        --windows \
4655                                        --absolute \
4656                                        [file dirname [gitdir]]]
4657                                regsub -all ' $me "'\\''" me
4658                                regsub -all ' $gd "'\\''" gd
4659                                puts $fd "@ECHO Entering $gw"
4660                                puts $fd "@ECHO Starting git-gui... please wait..."
4661                                puts -nonewline $fd "@\"$sh\" --login -c \""
4662                                puts -nonewline $fd "GIT_DIR='$gd'"
4663                                puts -nonewline $fd " '$me'"
4664                                puts $fd "&\""
4665                                close $fd
4666                        } err]} {
4667                        error_popup "Cannot write script:\n\n$err"
4668                }
4669        }
4670}
4671
4672proc do_macosx_app {} {
4673        global argv0 env
4674
4675        set fn [tk_getSaveFile \
4676                -parent . \
4677                -title "[appname] ([reponame]): Create Desktop Icon" \
4678                -initialdir [file join $env(HOME) Desktop] \
4679                -initialfile "Git [reponame].app"]
4680        if {$fn != {}} {
4681                if {[catch {
4682                                set Contents [file join $fn Contents]
4683                                set MacOS [file join $Contents MacOS]
4684                                set exe [file join $MacOS git-gui]
4685
4686                                file mkdir $MacOS
4687
4688                                set fd [open [file join $Contents Info.plist] w]
4689                                puts $fd {<?xml version="1.0" encoding="UTF-8"?>
4690<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4691<plist version="1.0">
4692<dict>
4693        <key>CFBundleDevelopmentRegion</key>
4694        <string>English</string>
4695        <key>CFBundleExecutable</key>
4696        <string>git-gui</string>
4697        <key>CFBundleIdentifier</key>
4698        <string>org.spearce.git-gui</string>
4699        <key>CFBundleInfoDictionaryVersion</key>
4700        <string>6.0</string>
4701        <key>CFBundlePackageType</key>
4702        <string>APPL</string>
4703        <key>CFBundleSignature</key>
4704        <string>????</string>
4705        <key>CFBundleVersion</key>
4706        <string>1.0</string>
4707        <key>NSPrincipalClass</key>
4708        <string>NSApplication</string>
4709</dict>
4710</plist>}
4711                                close $fd
4712
4713                                set fd [open $exe w]
4714                                set gd [file normalize [gitdir]]
4715                                set ep [file normalize [gitexec]]
4716                                regsub -all ' $gd "'\\''" gd
4717                                regsub -all ' $ep "'\\''" ep
4718                                puts $fd "#!/bin/sh"
4719                                foreach name [array names env] {
4720                                        if {[string match GIT_* $name]} {
4721                                                regsub -all ' $env($name) "'\\''" v
4722                                                puts $fd "export $name='$v'"
4723                                        }
4724                                }
4725                                puts $fd "export PATH='$ep':\$PATH"
4726                                puts $fd "export GIT_DIR='$gd'"
4727                                puts $fd "exec [file normalize $argv0]"
4728                                close $fd
4729
4730                                file attributes $exe -permissions u+x,g+x,o+x
4731                        } err]} {
4732                        error_popup "Cannot write icon:\n\n$err"
4733                }
4734        }
4735}
4736
4737proc toggle_or_diff {w x y} {
4738        global file_states file_lists current_diff_path ui_index ui_workdir
4739        global last_clicked selected_paths
4740
4741        set pos [split [$w index @$x,$y] .]
4742        set lno [lindex $pos 0]
4743        set col [lindex $pos 1]
4744        set path [lindex $file_lists($w) [expr {$lno - 1}]]
4745        if {$path eq {}} {
4746                set last_clicked {}
4747                return
4748        }
4749
4750        set last_clicked [list $w $lno]
4751        array unset selected_paths
4752        $ui_index tag remove in_sel 0.0 end
4753        $ui_workdir tag remove in_sel 0.0 end
4754
4755        if {$col == 0} {
4756                if {$current_diff_path eq $path} {
4757                        set after {reshow_diff;}
4758                } else {
4759                        set after {}
4760                }
4761                if {$w eq $ui_index} {
4762                        update_indexinfo \
4763                                "Unstaging [short_path $path] from commit" \
4764                                [list $path] \
4765                                [concat $after {set ui_status_value {Ready.}}]
4766                } elseif {$w eq $ui_workdir} {
4767                        update_index \
4768                                "Adding [short_path $path]" \
4769                                [list $path] \
4770                                [concat $after {set ui_status_value {Ready.}}]
4771                }
4772        } else {
4773                show_diff $path $w $lno
4774        }
4775}
4776
4777proc add_one_to_selection {w x y} {
4778        global file_lists last_clicked selected_paths
4779
4780        set lno [lindex [split [$w index @$x,$y] .] 0]
4781        set path [lindex $file_lists($w) [expr {$lno - 1}]]
4782        if {$path eq {}} {
4783                set last_clicked {}
4784                return
4785        }
4786
4787        if {$last_clicked ne {}
4788                && [lindex $last_clicked 0] ne $w} {
4789                array unset selected_paths
4790                [lindex $last_clicked 0] tag remove in_sel 0.0 end
4791        }
4792
4793        set last_clicked [list $w $lno]
4794        if {[catch {set in_sel $selected_paths($path)}]} {
4795                set in_sel 0
4796        }
4797        if {$in_sel} {
4798                unset selected_paths($path)
4799                $w tag remove in_sel $lno.0 [expr {$lno + 1}].0
4800        } else {
4801                set selected_paths($path) 1
4802                $w tag add in_sel $lno.0 [expr {$lno + 1}].0
4803        }
4804}
4805
4806proc add_range_to_selection {w x y} {
4807        global file_lists last_clicked selected_paths
4808
4809        if {[lindex $last_clicked 0] ne $w} {
4810                toggle_or_diff $w $x $y
4811                return
4812        }
4813
4814        set lno [lindex [split [$w index @$x,$y] .] 0]
4815        set lc [lindex $last_clicked 1]
4816        if {$lc < $lno} {
4817                set begin $lc
4818                set end $lno
4819        } else {
4820                set begin $lno
4821                set end $lc
4822        }
4823
4824        foreach path [lrange $file_lists($w) \
4825                [expr {$begin - 1}] \
4826                [expr {$end - 1}]] {
4827                set selected_paths($path) 1
4828        }
4829        $w tag add in_sel $begin.0 [expr {$end + 1}].0
4830}
4831
4832######################################################################
4833##
4834## config defaults
4835
4836set cursor_ptr arrow
4837font create font_diff -family Courier -size 10
4838font create font_ui
4839catch {
4840        label .dummy
4841        eval font configure font_ui [font actual [.dummy cget -font]]
4842        destroy .dummy
4843}
4844
4845font create font_uibold
4846font create font_diffbold
4847
4848if {[is_Windows]} {
4849        set M1B Control
4850        set M1T Ctrl
4851} elseif {[is_MacOSX]} {
4852        set M1B M1
4853        set M1T Cmd
4854} else {
4855        set M1B M1
4856        set M1T M1
4857}
4858
4859proc apply_config {} {
4860        global repo_config font_descs
4861
4862        foreach option $font_descs {
4863                set name [lindex $option 0]
4864                set font [lindex $option 1]
4865                if {[catch {
4866                        foreach {cn cv} $repo_config(gui.$name) {
4867                                font configure $font $cn $cv
4868                        }
4869                        } err]} {
4870                        error_popup "Invalid font specified in gui.$name:\n\n$err"
4871                }
4872                foreach {cn cv} [font configure $font] {
4873                        font configure ${font}bold $cn $cv
4874                }
4875                font configure ${font}bold -weight bold
4876        }
4877}
4878
4879set default_config(merge.summary) false
4880set default_config(merge.verbosity) 2
4881set default_config(user.name) {}
4882set default_config(user.email) {}
4883
4884set default_config(gui.trustmtime) false
4885set default_config(gui.diffcontext) 5
4886set default_config(gui.newbranchtemplate) {}
4887set default_config(gui.fontui) [font configure font_ui]
4888set default_config(gui.fontdiff) [font configure font_diff]
4889set font_descs {
4890        {fontui   font_ui   {Main Font}}
4891        {fontdiff font_diff {Diff/Console Font}}
4892}
4893load_config 0
4894apply_config
4895
4896######################################################################
4897##
4898## ui construction
4899
4900# -- Menu Bar
4901#
4902menu .mbar -tearoff 0
4903.mbar add cascade -label Repository -menu .mbar.repository
4904.mbar add cascade -label Edit -menu .mbar.edit
4905if {!$single_commit} {
4906        .mbar add cascade -label Branch -menu .mbar.branch
4907}
4908.mbar add cascade -label Commit -menu .mbar.commit
4909if {!$single_commit} {
4910        .mbar add cascade -label Merge -menu .mbar.merge
4911        .mbar add cascade -label Fetch -menu .mbar.fetch
4912        .mbar add cascade -label Push -menu .mbar.push
4913}
4914. configure -menu .mbar
4915
4916# -- Repository Menu
4917#
4918menu .mbar.repository
4919
4920.mbar.repository add command \
4921        -label {Browse Current Branch} \
4922        -command {new_browser $current_branch} \
4923        -font font_ui
4924.mbar.repository add separator
4925
4926.mbar.repository add command \
4927        -label {Visualize Current Branch} \
4928        -command {do_gitk {}} \
4929        -font font_ui
4930.mbar.repository add command \
4931        -label {Visualize All Branches} \
4932        -command {do_gitk {--all}} \
4933        -font font_ui
4934.mbar.repository add separator
4935
4936if {!$single_commit} {
4937        .mbar.repository add command -label {Database Statistics} \
4938                -command do_stats \
4939                -font font_ui
4940
4941        .mbar.repository add command -label {Compress Database} \
4942                -command do_gc \
4943                -font font_ui
4944
4945        .mbar.repository add command -label {Verify Database} \
4946                -command do_fsck_objects \
4947                -font font_ui
4948
4949        .mbar.repository add separator
4950
4951        if {[is_Cygwin]} {
4952                .mbar.repository add command \
4953                        -label {Create Desktop Icon} \
4954                        -command do_cygwin_shortcut \
4955                        -font font_ui
4956        } elseif {[is_Windows]} {
4957                .mbar.repository add command \
4958                        -label {Create Desktop Icon} \
4959                        -command do_windows_shortcut \
4960                        -font font_ui
4961        } elseif {[is_MacOSX]} {
4962                .mbar.repository add command \
4963                        -label {Create Desktop Icon} \
4964                        -command do_macosx_app \
4965                        -font font_ui
4966        }
4967}
4968
4969.mbar.repository add command -label Quit \
4970        -command do_quit \
4971        -accelerator $M1T-Q \
4972        -font font_ui
4973
4974# -- Edit Menu
4975#
4976menu .mbar.edit
4977.mbar.edit add command -label Undo \
4978        -command {catch {[focus] edit undo}} \
4979        -accelerator $M1T-Z \
4980        -font font_ui
4981.mbar.edit add command -label Redo \
4982        -command {catch {[focus] edit redo}} \
4983        -accelerator $M1T-Y \
4984        -font font_ui
4985.mbar.edit add separator
4986.mbar.edit add command -label Cut \
4987        -command {catch {tk_textCut [focus]}} \
4988        -accelerator $M1T-X \
4989        -font font_ui
4990.mbar.edit add command -label Copy \
4991        -command {catch {tk_textCopy [focus]}} \
4992        -accelerator $M1T-C \
4993        -font font_ui
4994.mbar.edit add command -label Paste \
4995        -command {catch {tk_textPaste [focus]; [focus] see insert}} \
4996        -accelerator $M1T-V \
4997        -font font_ui
4998.mbar.edit add command -label Delete \
4999        -command {catch {[focus] delete sel.first sel.last}} \
5000        -accelerator Del \
5001        -font font_ui
5002.mbar.edit add separator
5003.mbar.edit add command -label {Select All} \
5004        -command {catch {[focus] tag add sel 0.0 end}} \
5005        -accelerator $M1T-A \
5006        -font font_ui
5007
5008# -- Branch Menu
5009#
5010if {!$single_commit} {
5011        menu .mbar.branch
5012
5013        .mbar.branch add command -label {Create...} \
5014                -command do_create_branch \
5015                -accelerator $M1T-N \
5016                -font font_ui
5017        lappend disable_on_lock [list .mbar.branch entryconf \
5018                [.mbar.branch index last] -state]
5019
5020        .mbar.branch add command -label {Delete...} \
5021                -command do_delete_branch \
5022                -font font_ui
5023        lappend disable_on_lock [list .mbar.branch entryconf \
5024                [.mbar.branch index last] -state]
5025}
5026
5027# -- Commit Menu
5028#
5029menu .mbar.commit
5030
5031.mbar.commit add radiobutton \
5032        -label {New Commit} \
5033        -command do_select_commit_type \
5034        -variable selected_commit_type \
5035        -value new \
5036        -font font_ui
5037lappend disable_on_lock \
5038        [list .mbar.commit entryconf [.mbar.commit index last] -state]
5039
5040.mbar.commit add radiobutton \
5041        -label {Amend Last Commit} \
5042        -command do_select_commit_type \
5043        -variable selected_commit_type \
5044        -value amend \
5045        -font font_ui
5046lappend disable_on_lock \
5047        [list .mbar.commit entryconf [.mbar.commit index last] -state]
5048
5049.mbar.commit add separator
5050
5051.mbar.commit add command -label Rescan \
5052        -command do_rescan \
5053        -accelerator F5 \
5054        -font font_ui
5055lappend disable_on_lock \
5056        [list .mbar.commit entryconf [.mbar.commit index last] -state]
5057
5058.mbar.commit add command -label {Add To Commit} \
5059        -command do_add_selection \
5060        -font font_ui
5061lappend disable_on_lock \
5062        [list .mbar.commit entryconf [.mbar.commit index last] -state]
5063
5064.mbar.commit add command -label {Add All To Commit} \
5065        -command do_add_all \
5066        -accelerator $M1T-I \
5067        -font font_ui
5068lappend disable_on_lock \
5069        [list .mbar.commit entryconf [.mbar.commit index last] -state]
5070
5071.mbar.commit add command -label {Unstage From Commit} \
5072        -command do_unstage_selection \
5073        -font font_ui
5074lappend disable_on_lock \
5075        [list .mbar.commit entryconf [.mbar.commit index last] -state]
5076
5077.mbar.commit add command -label {Revert Changes} \
5078        -command do_revert_selection \
5079        -font font_ui
5080lappend disable_on_lock \
5081        [list .mbar.commit entryconf [.mbar.commit index last] -state]
5082
5083.mbar.commit add separator
5084
5085.mbar.commit add command -label {Sign Off} \
5086        -command do_signoff \
5087        -accelerator $M1T-S \
5088        -font font_ui
5089
5090.mbar.commit add command -label Commit \
5091        -command do_commit \
5092        -accelerator $M1T-Return \
5093        -font font_ui
5094lappend disable_on_lock \
5095        [list .mbar.commit entryconf [.mbar.commit index last] -state]
5096
5097if {[is_MacOSX]} {
5098        # -- Apple Menu (Mac OS X only)
5099        #
5100        .mbar add cascade -label Apple -menu .mbar.apple
5101        menu .mbar.apple
5102
5103        .mbar.apple add command -label "About [appname]" \
5104                -command do_about \
5105                -font font_ui
5106        .mbar.apple add command -label "[appname] Options..." \
5107                -command do_options \
5108                -font font_ui
5109} else {
5110        # -- Edit Menu
5111        #
5112        .mbar.edit add separator
5113        .mbar.edit add command -label {Options...} \
5114                -command do_options \
5115                -font font_ui
5116
5117        # -- Tools Menu
5118        #
5119        if {[file exists /usr/local/miga/lib/gui-miga]
5120                && [file exists .pvcsrc]} {
5121        proc do_miga {} {
5122                global ui_status_value
5123                if {![lock_index update]} return
5124                set cmd [list sh --login -c "/usr/local/miga/lib/gui-miga \"[pwd]\""]
5125                set miga_fd [open "|$cmd" r]
5126                fconfigure $miga_fd -blocking 0
5127                fileevent $miga_fd readable [list miga_done $miga_fd]
5128                set ui_status_value {Running miga...}
5129        }
5130        proc miga_done {fd} {
5131                read $fd 512
5132                if {[eof $fd]} {
5133                        close $fd
5134                        unlock_index
5135                        rescan [list set ui_status_value {Ready.}]
5136                }
5137        }
5138        .mbar add cascade -label Tools -menu .mbar.tools
5139        menu .mbar.tools
5140        .mbar.tools add command -label "Migrate" \
5141                -command do_miga \
5142                -font font_ui
5143        lappend disable_on_lock \
5144                [list .mbar.tools entryconf [.mbar.tools index last] -state]
5145        }
5146}
5147
5148# -- Help Menu
5149#
5150.mbar add cascade -label Help -menu .mbar.help
5151menu .mbar.help
5152
5153if {![is_MacOSX]} {
5154        .mbar.help add command -label "About [appname]" \
5155                -command do_about \
5156                -font font_ui
5157}
5158
5159set browser {}
5160catch {set browser $repo_config(instaweb.browser)}
5161set doc_path [file dirname [gitexec]]
5162set doc_path [file join $doc_path Documentation index.html]
5163
5164if {[is_Cygwin]} {
5165        set doc_path [exec cygpath --windows $doc_path]
5166}
5167
5168if {$browser eq {}} {
5169        if {[is_MacOSX]} {
5170                set browser open
5171        } elseif {[is_Cygwin]} {
5172                set program_files [file dirname [exec cygpath --windir]]
5173                set program_files [file join $program_files {Program Files}]
5174                set firefox [file join $program_files {Mozilla Firefox} firefox.exe]
5175                set ie [file join $program_files {Internet Explorer} IEXPLORE.EXE]
5176                if {[file exists $firefox]} {
5177                        set browser $firefox
5178                } elseif {[file exists $ie]} {
5179                        set browser $ie
5180                }
5181                unset program_files firefox ie
5182        }
5183}
5184
5185if {[file isfile $doc_path]} {
5186        set doc_url "file:$doc_path"
5187} else {
5188        set doc_url {http://www.kernel.org/pub/software/scm/git/docs/}
5189}
5190
5191if {$browser ne {}} {
5192        .mbar.help add command -label {Online Documentation} \
5193                -command [list exec $browser $doc_url &] \
5194                -font font_ui
5195}
5196unset browser doc_path doc_url
5197
5198# -- Branch Control
5199#
5200frame .branch \
5201        -borderwidth 1 \
5202        -relief sunken
5203label .branch.l1 \
5204        -text {Current Branch:} \
5205        -anchor w \
5206        -justify left \
5207        -font font_ui
5208label .branch.cb \
5209        -textvariable current_branch \
5210        -anchor w \
5211        -justify left \
5212        -font font_ui
5213pack .branch.l1 -side left
5214pack .branch.cb -side left -fill x
5215pack .branch -side top -fill x
5216
5217if {!$single_commit} {
5218        menu .mbar.merge
5219        .mbar.merge add command -label {Local Merge...} \
5220                -command do_local_merge \
5221                -font font_ui
5222        lappend disable_on_lock \
5223                [list .mbar.merge entryconf [.mbar.merge index last] -state]
5224        .mbar.merge add command -label {Abort Merge...} \
5225                -command do_reset_hard \
5226                -font font_ui
5227        lappend disable_on_lock \
5228                [list .mbar.merge entryconf [.mbar.merge index last] -state]
5229
5230
5231        menu .mbar.fetch
5232
5233        menu .mbar.push
5234        .mbar.push add command -label {Push...} \
5235                -command do_push_anywhere \
5236                -font font_ui
5237}
5238
5239# -- Main Window Layout
5240#
5241panedwindow .vpane -orient vertical
5242panedwindow .vpane.files -orient horizontal
5243.vpane add .vpane.files -sticky nsew -height 100 -width 200
5244pack .vpane -anchor n -side top -fill both -expand 1
5245
5246# -- Index File List
5247#
5248frame .vpane.files.index -height 100 -width 200
5249label .vpane.files.index.title -text {Changes To Be Committed} \
5250        -background green \
5251        -font font_ui
5252text $ui_index -background white -borderwidth 0 \
5253        -width 20 -height 10 \
5254        -wrap none \
5255        -font font_ui \
5256        -cursor $cursor_ptr \
5257        -xscrollcommand {.vpane.files.index.sx set} \
5258        -yscrollcommand {.vpane.files.index.sy set} \
5259        -state disabled
5260scrollbar .vpane.files.index.sx -orient h -command [list $ui_index xview]
5261scrollbar .vpane.files.index.sy -orient v -command [list $ui_index yview]
5262pack .vpane.files.index.title -side top -fill x
5263pack .vpane.files.index.sx -side bottom -fill x
5264pack .vpane.files.index.sy -side right -fill y
5265pack $ui_index -side left -fill both -expand 1
5266.vpane.files add .vpane.files.index -sticky nsew
5267
5268# -- Working Directory File List
5269#
5270frame .vpane.files.workdir -height 100 -width 200
5271label .vpane.files.workdir.title -text {Changed But Not Updated} \
5272        -background red \
5273        -font font_ui
5274text $ui_workdir -background white -borderwidth 0 \
5275        -width 20 -height 10 \
5276        -wrap none \
5277        -font font_ui \
5278        -cursor $cursor_ptr \
5279        -xscrollcommand {.vpane.files.workdir.sx set} \
5280        -yscrollcommand {.vpane.files.workdir.sy set} \
5281        -state disabled
5282scrollbar .vpane.files.workdir.sx -orient h -command [list $ui_workdir xview]
5283scrollbar .vpane.files.workdir.sy -orient v -command [list $ui_workdir yview]
5284pack .vpane.files.workdir.title -side top -fill x
5285pack .vpane.files.workdir.sx -side bottom -fill x
5286pack .vpane.files.workdir.sy -side right -fill y
5287pack $ui_workdir -side left -fill both -expand 1
5288.vpane.files add .vpane.files.workdir -sticky nsew
5289
5290foreach i [list $ui_index $ui_workdir] {
5291        $i tag conf in_diff -font font_uibold
5292        $i tag conf in_sel \
5293                -background [$i cget -foreground] \
5294                -foreground [$i cget -background]
5295}
5296unset i
5297
5298# -- Diff and Commit Area
5299#
5300frame .vpane.lower -height 300 -width 400
5301frame .vpane.lower.commarea
5302frame .vpane.lower.diff -relief sunken -borderwidth 1
5303pack .vpane.lower.commarea -side top -fill x
5304pack .vpane.lower.diff -side bottom -fill both -expand 1
5305.vpane add .vpane.lower -sticky nsew
5306
5307# -- Commit Area Buttons
5308#
5309frame .vpane.lower.commarea.buttons
5310label .vpane.lower.commarea.buttons.l -text {} \
5311        -anchor w \
5312        -justify left \
5313        -font font_ui
5314pack .vpane.lower.commarea.buttons.l -side top -fill x
5315pack .vpane.lower.commarea.buttons -side left -fill y
5316
5317button .vpane.lower.commarea.buttons.rescan -text {Rescan} \
5318        -command do_rescan \
5319        -font font_ui
5320pack .vpane.lower.commarea.buttons.rescan -side top -fill x
5321lappend disable_on_lock \
5322        {.vpane.lower.commarea.buttons.rescan conf -state}
5323
5324button .vpane.lower.commarea.buttons.incall -text {Add All} \
5325        -command do_add_all \
5326        -font font_ui
5327pack .vpane.lower.commarea.buttons.incall -side top -fill x
5328lappend disable_on_lock \
5329        {.vpane.lower.commarea.buttons.incall conf -state}
5330
5331button .vpane.lower.commarea.buttons.signoff -text {Sign Off} \
5332        -command do_signoff \
5333        -font font_ui
5334pack .vpane.lower.commarea.buttons.signoff -side top -fill x
5335
5336button .vpane.lower.commarea.buttons.commit -text {Commit} \
5337        -command do_commit \
5338        -font font_ui
5339pack .vpane.lower.commarea.buttons.commit -side top -fill x
5340lappend disable_on_lock \
5341        {.vpane.lower.commarea.buttons.commit conf -state}
5342
5343# -- Commit Message Buffer
5344#
5345frame .vpane.lower.commarea.buffer
5346frame .vpane.lower.commarea.buffer.header
5347set ui_comm .vpane.lower.commarea.buffer.t
5348set ui_coml .vpane.lower.commarea.buffer.header.l
5349radiobutton .vpane.lower.commarea.buffer.header.new \
5350        -text {New Commit} \
5351        -command do_select_commit_type \
5352        -variable selected_commit_type \
5353        -value new \
5354        -font font_ui
5355lappend disable_on_lock \
5356        [list .vpane.lower.commarea.buffer.header.new conf -state]
5357radiobutton .vpane.lower.commarea.buffer.header.amend \
5358        -text {Amend Last Commit} \
5359        -command do_select_commit_type \
5360        -variable selected_commit_type \
5361        -value amend \
5362        -font font_ui
5363lappend disable_on_lock \
5364        [list .vpane.lower.commarea.buffer.header.amend conf -state]
5365label $ui_coml \
5366        -anchor w \
5367        -justify left \
5368        -font font_ui
5369proc trace_commit_type {varname args} {
5370        global ui_coml commit_type
5371        switch -glob -- $commit_type {
5372        initial       {set txt {Initial Commit Message:}}
5373        amend         {set txt {Amended Commit Message:}}
5374        amend-initial {set txt {Amended Initial Commit Message:}}
5375        amend-merge   {set txt {Amended Merge Commit Message:}}
5376        merge         {set txt {Merge Commit Message:}}
5377        *             {set txt {Commit Message:}}
5378        }
5379        $ui_coml conf -text $txt
5380}
5381trace add variable commit_type write trace_commit_type
5382pack $ui_coml -side left -fill x
5383pack .vpane.lower.commarea.buffer.header.amend -side right
5384pack .vpane.lower.commarea.buffer.header.new -side right
5385
5386text $ui_comm -background white -borderwidth 1 \
5387        -undo true \
5388        -maxundo 20 \
5389        -autoseparators true \
5390        -relief sunken \
5391        -width 75 -height 9 -wrap none \
5392        -font font_diff \
5393        -yscrollcommand {.vpane.lower.commarea.buffer.sby set}
5394scrollbar .vpane.lower.commarea.buffer.sby \
5395        -command [list $ui_comm yview]
5396pack .vpane.lower.commarea.buffer.header -side top -fill x
5397pack .vpane.lower.commarea.buffer.sby -side right -fill y
5398pack $ui_comm -side left -fill y
5399pack .vpane.lower.commarea.buffer -side left -fill y
5400
5401# -- Commit Message Buffer Context Menu
5402#
5403set ctxm .vpane.lower.commarea.buffer.ctxm
5404menu $ctxm -tearoff 0
5405$ctxm add command \
5406        -label {Cut} \
5407        -font font_ui \
5408        -command {tk_textCut $ui_comm}
5409$ctxm add command \
5410        -label {Copy} \
5411        -font font_ui \
5412        -command {tk_textCopy $ui_comm}
5413$ctxm add command \
5414        -label {Paste} \
5415        -font font_ui \
5416        -command {tk_textPaste $ui_comm}
5417$ctxm add command \
5418        -label {Delete} \
5419        -font font_ui \
5420        -command {$ui_comm delete sel.first sel.last}
5421$ctxm add separator
5422$ctxm add command \
5423        -label {Select All} \
5424        -font font_ui \
5425        -command {focus $ui_comm;$ui_comm tag add sel 0.0 end}
5426$ctxm add command \
5427        -label {Copy All} \
5428        -font font_ui \
5429        -command {
5430                $ui_comm tag add sel 0.0 end
5431                tk_textCopy $ui_comm
5432                $ui_comm tag remove sel 0.0 end
5433        }
5434$ctxm add separator
5435$ctxm add command \
5436        -label {Sign Off} \
5437        -font font_ui \
5438        -command do_signoff
5439bind_button3 $ui_comm "tk_popup $ctxm %X %Y"
5440
5441# -- Diff Header
5442#
5443set current_diff_path {}
5444set current_diff_side {}
5445set diff_actions [list]
5446proc trace_current_diff_path {varname args} {
5447        global current_diff_path diff_actions file_states
5448        if {$current_diff_path eq {}} {
5449                set s {}
5450                set f {}
5451                set p {}
5452                set o disabled
5453        } else {
5454                set p $current_diff_path
5455                set s [mapdesc [lindex $file_states($p) 0] $p]
5456                set f {File:}
5457                set p [escape_path $p]
5458                set o normal
5459        }
5460
5461        .vpane.lower.diff.header.status configure -text $s
5462        .vpane.lower.diff.header.file configure -text $f
5463        .vpane.lower.diff.header.path configure -text $p
5464        foreach w $diff_actions {
5465                uplevel #0 $w $o
5466        }
5467}
5468trace add variable current_diff_path write trace_current_diff_path
5469
5470frame .vpane.lower.diff.header -background orange
5471label .vpane.lower.diff.header.status \
5472        -background orange \
5473        -width $max_status_desc \
5474        -anchor w \
5475        -justify left \
5476        -font font_ui
5477label .vpane.lower.diff.header.file \
5478        -background orange \
5479        -anchor w \
5480        -justify left \
5481        -font font_ui
5482label .vpane.lower.diff.header.path \
5483        -background orange \
5484        -anchor w \
5485        -justify left \
5486        -font font_ui
5487pack .vpane.lower.diff.header.status -side left
5488pack .vpane.lower.diff.header.file -side left
5489pack .vpane.lower.diff.header.path -fill x
5490set ctxm .vpane.lower.diff.header.ctxm
5491menu $ctxm -tearoff 0
5492$ctxm add command \
5493        -label {Copy} \
5494        -font font_ui \
5495        -command {
5496                clipboard clear
5497                clipboard append \
5498                        -format STRING \
5499                        -type STRING \
5500                        -- $current_diff_path
5501        }
5502lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5503bind_button3 .vpane.lower.diff.header.path "tk_popup $ctxm %X %Y"
5504
5505# -- Diff Body
5506#
5507frame .vpane.lower.diff.body
5508set ui_diff .vpane.lower.diff.body.t
5509text $ui_diff -background white -borderwidth 0 \
5510        -width 80 -height 15 -wrap none \
5511        -font font_diff \
5512        -xscrollcommand {.vpane.lower.diff.body.sbx set} \
5513        -yscrollcommand {.vpane.lower.diff.body.sby set} \
5514        -state disabled
5515scrollbar .vpane.lower.diff.body.sbx -orient horizontal \
5516        -command [list $ui_diff xview]
5517scrollbar .vpane.lower.diff.body.sby -orient vertical \
5518        -command [list $ui_diff yview]
5519pack .vpane.lower.diff.body.sbx -side bottom -fill x
5520pack .vpane.lower.diff.body.sby -side right -fill y
5521pack $ui_diff -side left -fill both -expand 1
5522pack .vpane.lower.diff.header -side top -fill x
5523pack .vpane.lower.diff.body -side bottom -fill both -expand 1
5524
5525$ui_diff tag conf d_cr -elide true
5526$ui_diff tag conf d_@ -foreground blue -font font_diffbold
5527$ui_diff tag conf d_+ -foreground {#00a000}
5528$ui_diff tag conf d_- -foreground red
5529
5530$ui_diff tag conf d_++ -foreground {#00a000}
5531$ui_diff tag conf d_-- -foreground red
5532$ui_diff tag conf d_+s \
5533        -foreground {#00a000} \
5534        -background {#e2effa}
5535$ui_diff tag conf d_-s \
5536        -foreground red \
5537        -background {#e2effa}
5538$ui_diff tag conf d_s+ \
5539        -foreground {#00a000} \
5540        -background ivory1
5541$ui_diff tag conf d_s- \
5542        -foreground red \
5543        -background ivory1
5544
5545$ui_diff tag conf d<<<<<<< \
5546        -foreground orange \
5547        -font font_diffbold
5548$ui_diff tag conf d======= \
5549        -foreground orange \
5550        -font font_diffbold
5551$ui_diff tag conf d>>>>>>> \
5552        -foreground orange \
5553        -font font_diffbold
5554
5555$ui_diff tag raise sel
5556
5557# -- Diff Body Context Menu
5558#
5559set ctxm .vpane.lower.diff.body.ctxm
5560menu $ctxm -tearoff 0
5561$ctxm add command \
5562        -label {Refresh} \
5563        -font font_ui \
5564        -command reshow_diff
5565lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5566$ctxm add command \
5567        -label {Copy} \
5568        -font font_ui \
5569        -command {tk_textCopy $ui_diff}
5570lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5571$ctxm add command \
5572        -label {Select All} \
5573        -font font_ui \
5574        -command {focus $ui_diff;$ui_diff tag add sel 0.0 end}
5575lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5576$ctxm add command \
5577        -label {Copy All} \
5578        -font font_ui \
5579        -command {
5580                $ui_diff tag add sel 0.0 end
5581                tk_textCopy $ui_diff
5582                $ui_diff tag remove sel 0.0 end
5583        }
5584lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5585$ctxm add separator
5586$ctxm add command \
5587        -label {Apply/Reverse Hunk} \
5588        -font font_ui \
5589        -command {apply_hunk $cursorX $cursorY}
5590set ui_diff_applyhunk [$ctxm index last]
5591lappend diff_actions [list $ctxm entryconf $ui_diff_applyhunk -state]
5592$ctxm add separator
5593$ctxm add command \
5594        -label {Decrease Font Size} \
5595        -font font_ui \
5596        -command {incr_font_size font_diff -1}
5597lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5598$ctxm add command \
5599        -label {Increase Font Size} \
5600        -font font_ui \
5601        -command {incr_font_size font_diff 1}
5602lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5603$ctxm add separator
5604$ctxm add command \
5605        -label {Show Less Context} \
5606        -font font_ui \
5607        -command {if {$repo_config(gui.diffcontext) >= 2} {
5608                incr repo_config(gui.diffcontext) -1
5609                reshow_diff
5610        }}
5611lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5612$ctxm add command \
5613        -label {Show More Context} \
5614        -font font_ui \
5615        -command {
5616                incr repo_config(gui.diffcontext)
5617                reshow_diff
5618        }
5619lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5620$ctxm add separator
5621$ctxm add command -label {Options...} \
5622        -font font_ui \
5623        -command do_options
5624bind_button3 $ui_diff "
5625        set cursorX %x
5626        set cursorY %y
5627        if {\$ui_index eq \$current_diff_side} {
5628                $ctxm entryconf $ui_diff_applyhunk -label {Unstage Hunk From Commit}
5629        } else {
5630                $ctxm entryconf $ui_diff_applyhunk -label {Stage Hunk For Commit}
5631        }
5632        tk_popup $ctxm %X %Y
5633"
5634unset ui_diff_applyhunk
5635
5636# -- Status Bar
5637#
5638set ui_status_value {Initializing...}
5639label .status -textvariable ui_status_value \
5640        -anchor w \
5641        -justify left \
5642        -borderwidth 1 \
5643        -relief sunken \
5644        -font font_ui
5645pack .status -anchor w -side bottom -fill x
5646
5647# -- Load geometry
5648#
5649catch {
5650set gm $repo_config(gui.geometry)
5651wm geometry . [lindex $gm 0]
5652.vpane sash place 0 \
5653        [lindex [.vpane sash coord 0] 0] \
5654        [lindex $gm 1]
5655.vpane.files sash place 0 \
5656        [lindex $gm 2] \
5657        [lindex [.vpane.files sash coord 0] 1]
5658unset gm
5659}
5660
5661# -- Key Bindings
5662#
5663bind $ui_comm <$M1B-Key-Return> {do_commit;break}
5664bind $ui_comm <$M1B-Key-i> {do_add_all;break}
5665bind $ui_comm <$M1B-Key-I> {do_add_all;break}
5666bind $ui_comm <$M1B-Key-x> {tk_textCut %W;break}
5667bind $ui_comm <$M1B-Key-X> {tk_textCut %W;break}
5668bind $ui_comm <$M1B-Key-c> {tk_textCopy %W;break}
5669bind $ui_comm <$M1B-Key-C> {tk_textCopy %W;break}
5670bind $ui_comm <$M1B-Key-v> {tk_textPaste %W; %W see insert; break}
5671bind $ui_comm <$M1B-Key-V> {tk_textPaste %W; %W see insert; break}
5672bind $ui_comm <$M1B-Key-a> {%W tag add sel 0.0 end;break}
5673bind $ui_comm <$M1B-Key-A> {%W tag add sel 0.0 end;break}
5674
5675bind $ui_diff <$M1B-Key-x> {tk_textCopy %W;break}
5676bind $ui_diff <$M1B-Key-X> {tk_textCopy %W;break}
5677bind $ui_diff <$M1B-Key-c> {tk_textCopy %W;break}
5678bind $ui_diff <$M1B-Key-C> {tk_textCopy %W;break}
5679bind $ui_diff <$M1B-Key-v> {break}
5680bind $ui_diff <$M1B-Key-V> {break}
5681bind $ui_diff <$M1B-Key-a> {%W tag add sel 0.0 end;break}
5682bind $ui_diff <$M1B-Key-A> {%W tag add sel 0.0 end;break}
5683bind $ui_diff <Key-Up>     {catch {%W yview scroll -1 units};break}
5684bind $ui_diff <Key-Down>   {catch {%W yview scroll  1 units};break}
5685bind $ui_diff <Key-Left>   {catch {%W xview scroll -1 units};break}
5686bind $ui_diff <Key-Right>  {catch {%W xview scroll  1 units};break}
5687bind $ui_diff <Button-1>   {focus %W}
5688
5689if {!$single_commit} {
5690        bind . <$M1B-Key-n> do_create_branch
5691        bind . <$M1B-Key-N> do_create_branch
5692}
5693
5694bind .   <Destroy> do_quit
5695bind all <Key-F5> do_rescan
5696bind all <$M1B-Key-r> do_rescan
5697bind all <$M1B-Key-R> do_rescan
5698bind .   <$M1B-Key-s> do_signoff
5699bind .   <$M1B-Key-S> do_signoff
5700bind .   <$M1B-Key-i> do_add_all
5701bind .   <$M1B-Key-I> do_add_all
5702bind .   <$M1B-Key-Return> do_commit
5703bind all <$M1B-Key-q> do_quit
5704bind all <$M1B-Key-Q> do_quit
5705bind all <$M1B-Key-w> {destroy [winfo toplevel %W]}
5706bind all <$M1B-Key-W> {destroy [winfo toplevel %W]}
5707foreach i [list $ui_index $ui_workdir] {
5708        bind $i <Button-1>       "toggle_or_diff         $i %x %y; break"
5709        bind $i <$M1B-Button-1>  "add_one_to_selection   $i %x %y; break"
5710        bind $i <Shift-Button-1> "add_range_to_selection $i %x %y; break"
5711}
5712unset i
5713
5714set file_lists($ui_index) [list]
5715set file_lists($ui_workdir) [list]
5716
5717set HEAD {}
5718set PARENT {}
5719set MERGE_HEAD [list]
5720set commit_type {}
5721set empty_tree {}
5722set current_branch {}
5723set current_diff_path {}
5724set selected_commit_type new
5725
5726wm title . "[appname] ([file normalize [file dirname [gitdir]]])"
5727focus -force $ui_comm
5728
5729# -- Warn the user about environmental problems.  Cygwin's Tcl
5730#    does *not* pass its env array onto any processes it spawns.
5731#    This means that git processes get none of our environment.
5732#
5733if {[is_Cygwin]} {
5734        set ignored_env 0
5735        set suggest_user {}
5736        set msg "Possible environment issues exist.
5737
5738The following environment variables are probably
5739going to be ignored by any Git subprocess run
5740by [appname]:
5741
5742"
5743        foreach name [array names env] {
5744                switch -regexp -- $name {
5745                {^GIT_INDEX_FILE$} -
5746                {^GIT_OBJECT_DIRECTORY$} -
5747                {^GIT_ALTERNATE_OBJECT_DIRECTORIES$} -
5748                {^GIT_DIFF_OPTS$} -
5749                {^GIT_EXTERNAL_DIFF$} -
5750                {^GIT_PAGER$} -
5751                {^GIT_TRACE$} -
5752                {^GIT_CONFIG$} -
5753                {^GIT_CONFIG_LOCAL$} -
5754                {^GIT_(AUTHOR|COMMITTER)_DATE$} {
5755                        append msg " - $name\n"
5756                        incr ignored_env
5757                }
5758                {^GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL)$} {
5759                        append msg " - $name\n"
5760                        incr ignored_env
5761                        set suggest_user $name
5762                }
5763                }
5764        }
5765        if {$ignored_env > 0} {
5766                append msg "
5767This is due to a known issue with the
5768Tcl binary distributed by Cygwin."
5769
5770                if {$suggest_user ne {}} {
5771                        append msg "
5772
5773A good replacement for $suggest_user
5774is placing values for the user.name and
5775user.email settings into your personal
5776~/.gitconfig file.
5777"
5778                }
5779                warn_popup $msg
5780        }
5781        unset ignored_env msg suggest_user name
5782}
5783
5784# -- Only initialize complex UI if we are going to stay running.
5785#
5786if {!$single_commit} {
5787        load_all_remotes
5788        load_all_heads
5789
5790        populate_branch_menu
5791        populate_fetch_menu
5792        populate_push_menu
5793}
5794
5795# -- Only suggest a gc run if we are going to stay running.
5796#
5797if {!$single_commit} {
5798        set object_limit 2000
5799        if {[is_Windows]} {set object_limit 200}
5800        regexp {^([0-9]+) objects,} [exec git count-objects] _junk objects_current
5801        if {$objects_current >= $object_limit} {
5802                if {[ask_popup \
5803                        "This repository currently has $objects_current loose objects.
5804
5805To maintain optimal performance it is strongly
5806recommended that you compress the database
5807when more than $object_limit loose objects exist.
5808
5809Compress the database now?"] eq yes} {
5810                        do_gc
5811                }
5812        }
5813        unset object_limit _junk objects_current
5814}
5815
5816lock_index begin-read
5817after 1 do_rescan