git-gui.shon commit git-gui: Support keyboard traversal in browser. (9bccb78)
   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 "\n" $path "\\n" path
1385        return $path
1386}
1387
1388proc short_path {path} {
1389        return [escape_path [lindex [file split $path] end]]
1390}
1391
1392set next_icon_id 0
1393set null_sha1 [string repeat 0 40]
1394
1395proc merge_state {path new_state {head_info {}} {index_info {}}} {
1396        global file_states next_icon_id null_sha1
1397
1398        set s0 [string index $new_state 0]
1399        set s1 [string index $new_state 1]
1400
1401        if {[catch {set info $file_states($path)}]} {
1402                set state __
1403                set icon n[incr next_icon_id]
1404        } else {
1405                set state [lindex $info 0]
1406                set icon [lindex $info 1]
1407                if {$head_info eq {}}  {set head_info  [lindex $info 2]}
1408                if {$index_info eq {}} {set index_info [lindex $info 3]}
1409        }
1410
1411        if     {$s0 eq {?}} {set s0 [string index $state 0]} \
1412        elseif {$s0 eq {_}} {set s0 _}
1413
1414        if     {$s1 eq {?}} {set s1 [string index $state 1]} \
1415        elseif {$s1 eq {_}} {set s1 _}
1416
1417        if {$s0 eq {A} && $s1 eq {_} && $head_info eq {}} {
1418                set head_info [list 0 $null_sha1]
1419        } elseif {$s0 ne {_} && [string index $state 0] eq {_}
1420                && $head_info eq {}} {
1421                set head_info $index_info
1422        }
1423
1424        set file_states($path) [list $s0$s1 $icon \
1425                $head_info $index_info \
1426                ]
1427        return $state
1428}
1429
1430proc display_file_helper {w path icon_name old_m new_m} {
1431        global file_lists
1432
1433        if {$new_m eq {_}} {
1434                set lno [lsearch -sorted -exact $file_lists($w) $path]
1435                if {$lno >= 0} {
1436                        set file_lists($w) [lreplace $file_lists($w) $lno $lno]
1437                        incr lno
1438                        $w conf -state normal
1439                        $w delete $lno.0 [expr {$lno + 1}].0
1440                        $w conf -state disabled
1441                }
1442        } elseif {$old_m eq {_} && $new_m ne {_}} {
1443                lappend file_lists($w) $path
1444                set file_lists($w) [lsort -unique $file_lists($w)]
1445                set lno [lsearch -sorted -exact $file_lists($w) $path]
1446                incr lno
1447                $w conf -state normal
1448                $w image create $lno.0 \
1449                        -align center -padx 5 -pady 1 \
1450                        -name $icon_name \
1451                        -image [mapicon $w $new_m $path]
1452                $w insert $lno.1 "[escape_path $path]\n"
1453                $w conf -state disabled
1454        } elseif {$old_m ne $new_m} {
1455                $w conf -state normal
1456                $w image conf $icon_name -image [mapicon $w $new_m $path]
1457                $w conf -state disabled
1458        }
1459}
1460
1461proc display_file {path state} {
1462        global file_states selected_paths
1463        global ui_index ui_workdir
1464
1465        set old_m [merge_state $path $state]
1466        set s $file_states($path)
1467        set new_m [lindex $s 0]
1468        set icon_name [lindex $s 1]
1469
1470        set o [string index $old_m 0]
1471        set n [string index $new_m 0]
1472        if {$o eq {U}} {
1473                set o _
1474        }
1475        if {$n eq {U}} {
1476                set n _
1477        }
1478        display_file_helper     $ui_index $path $icon_name $o $n
1479
1480        if {[string index $old_m 0] eq {U}} {
1481                set o U
1482        } else {
1483                set o [string index $old_m 1]
1484        }
1485        if {[string index $new_m 0] eq {U}} {
1486                set n U
1487        } else {
1488                set n [string index $new_m 1]
1489        }
1490        display_file_helper     $ui_workdir $path $icon_name $o $n
1491
1492        if {$new_m eq {__}} {
1493                unset file_states($path)
1494                catch {unset selected_paths($path)}
1495        }
1496}
1497
1498proc display_all_files_helper {w path icon_name m} {
1499        global file_lists
1500
1501        lappend file_lists($w) $path
1502        set lno [expr {[lindex [split [$w index end] .] 0] - 1}]
1503        $w image create end \
1504                -align center -padx 5 -pady 1 \
1505                -name $icon_name \
1506                -image [mapicon $w $m $path]
1507        $w insert end "[escape_path $path]\n"
1508}
1509
1510proc display_all_files {} {
1511        global ui_index ui_workdir
1512        global file_states file_lists
1513        global last_clicked
1514
1515        $ui_index conf -state normal
1516        $ui_workdir conf -state normal
1517
1518        $ui_index delete 0.0 end
1519        $ui_workdir delete 0.0 end
1520        set last_clicked {}
1521
1522        set file_lists($ui_index) [list]
1523        set file_lists($ui_workdir) [list]
1524
1525        foreach path [lsort [array names file_states]] {
1526                set s $file_states($path)
1527                set m [lindex $s 0]
1528                set icon_name [lindex $s 1]
1529
1530                set s [string index $m 0]
1531                if {$s ne {U} && $s ne {_}} {
1532                        display_all_files_helper $ui_index $path \
1533                                $icon_name $s
1534                }
1535
1536                if {[string index $m 0] eq {U}} {
1537                        set s U
1538                } else {
1539                        set s [string index $m 1]
1540                }
1541                if {$s ne {_}} {
1542                        display_all_files_helper $ui_workdir $path \
1543                                $icon_name $s
1544                }
1545        }
1546
1547        $ui_index conf -state disabled
1548        $ui_workdir conf -state disabled
1549}
1550
1551proc update_indexinfo {msg pathList after} {
1552        global update_index_cp ui_status_value
1553
1554        if {![lock_index update]} return
1555
1556        set update_index_cp 0
1557        set pathList [lsort $pathList]
1558        set totalCnt [llength $pathList]
1559        set batch [expr {int($totalCnt * .01) + 1}]
1560        if {$batch > 25} {set batch 25}
1561
1562        set ui_status_value [format \
1563                "$msg... %i/%i files (%.2f%%)" \
1564                $update_index_cp \
1565                $totalCnt \
1566                0.0]
1567        set fd [open "| git update-index -z --index-info" w]
1568        fconfigure $fd \
1569                -blocking 0 \
1570                -buffering full \
1571                -buffersize 512 \
1572                -encoding binary \
1573                -translation binary
1574        fileevent $fd writable [list \
1575                write_update_indexinfo \
1576                $fd \
1577                $pathList \
1578                $totalCnt \
1579                $batch \
1580                $msg \
1581                $after \
1582                ]
1583}
1584
1585proc write_update_indexinfo {fd pathList totalCnt batch msg after} {
1586        global update_index_cp ui_status_value
1587        global file_states current_diff_path
1588
1589        if {$update_index_cp >= $totalCnt} {
1590                close $fd
1591                unlock_index
1592                uplevel #0 $after
1593                return
1594        }
1595
1596        for {set i $batch} \
1597                {$update_index_cp < $totalCnt && $i > 0} \
1598                {incr i -1} {
1599                set path [lindex $pathList $update_index_cp]
1600                incr update_index_cp
1601
1602                set s $file_states($path)
1603                switch -glob -- [lindex $s 0] {
1604                A? {set new _O}
1605                M? {set new _M}
1606                D_ {set new _D}
1607                D? {set new _?}
1608                ?? {continue}
1609                }
1610                set info [lindex $s 2]
1611                if {$info eq {}} continue
1612
1613                puts -nonewline $fd "$info\t[encoding convertto $path]\0"
1614                display_file $path $new
1615        }
1616
1617        set ui_status_value [format \
1618                "$msg... %i/%i files (%.2f%%)" \
1619                $update_index_cp \
1620                $totalCnt \
1621                [expr {100.0 * $update_index_cp / $totalCnt}]]
1622}
1623
1624proc update_index {msg pathList after} {
1625        global update_index_cp ui_status_value
1626
1627        if {![lock_index update]} return
1628
1629        set update_index_cp 0
1630        set pathList [lsort $pathList]
1631        set totalCnt [llength $pathList]
1632        set batch [expr {int($totalCnt * .01) + 1}]
1633        if {$batch > 25} {set batch 25}
1634
1635        set ui_status_value [format \
1636                "$msg... %i/%i files (%.2f%%)" \
1637                $update_index_cp \
1638                $totalCnt \
1639                0.0]
1640        set fd [open "| git update-index --add --remove -z --stdin" w]
1641        fconfigure $fd \
1642                -blocking 0 \
1643                -buffering full \
1644                -buffersize 512 \
1645                -encoding binary \
1646                -translation binary
1647        fileevent $fd writable [list \
1648                write_update_index \
1649                $fd \
1650                $pathList \
1651                $totalCnt \
1652                $batch \
1653                $msg \
1654                $after \
1655                ]
1656}
1657
1658proc write_update_index {fd pathList totalCnt batch msg after} {
1659        global update_index_cp ui_status_value
1660        global file_states current_diff_path
1661
1662        if {$update_index_cp >= $totalCnt} {
1663                close $fd
1664                unlock_index
1665                uplevel #0 $after
1666                return
1667        }
1668
1669        for {set i $batch} \
1670                {$update_index_cp < $totalCnt && $i > 0} \
1671                {incr i -1} {
1672                set path [lindex $pathList $update_index_cp]
1673                incr update_index_cp
1674
1675                switch -glob -- [lindex $file_states($path) 0] {
1676                AD {set new __}
1677                ?D {set new D_}
1678                _O -
1679                AM {set new A_}
1680                U? {
1681                        if {[file exists $path]} {
1682                                set new M_
1683                        } else {
1684                                set new D_
1685                        }
1686                }
1687                ?M {set new M_}
1688                ?? {continue}
1689                }
1690                puts -nonewline $fd "[encoding convertto $path]\0"
1691                display_file $path $new
1692        }
1693
1694        set ui_status_value [format \
1695                "$msg... %i/%i files (%.2f%%)" \
1696                $update_index_cp \
1697                $totalCnt \
1698                [expr {100.0 * $update_index_cp / $totalCnt}]]
1699}
1700
1701proc checkout_index {msg pathList after} {
1702        global update_index_cp ui_status_value
1703
1704        if {![lock_index update]} return
1705
1706        set update_index_cp 0
1707        set pathList [lsort $pathList]
1708        set totalCnt [llength $pathList]
1709        set batch [expr {int($totalCnt * .01) + 1}]
1710        if {$batch > 25} {set batch 25}
1711
1712        set ui_status_value [format \
1713                "$msg... %i/%i files (%.2f%%)" \
1714                $update_index_cp \
1715                $totalCnt \
1716                0.0]
1717        set cmd [list git checkout-index]
1718        lappend cmd --index
1719        lappend cmd --quiet
1720        lappend cmd --force
1721        lappend cmd -z
1722        lappend cmd --stdin
1723        set fd [open "| $cmd " w]
1724        fconfigure $fd \
1725                -blocking 0 \
1726                -buffering full \
1727                -buffersize 512 \
1728                -encoding binary \
1729                -translation binary
1730        fileevent $fd writable [list \
1731                write_checkout_index \
1732                $fd \
1733                $pathList \
1734                $totalCnt \
1735                $batch \
1736                $msg \
1737                $after \
1738                ]
1739}
1740
1741proc write_checkout_index {fd pathList totalCnt batch msg after} {
1742        global update_index_cp ui_status_value
1743        global file_states current_diff_path
1744
1745        if {$update_index_cp >= $totalCnt} {
1746                close $fd
1747                unlock_index
1748                uplevel #0 $after
1749                return
1750        }
1751
1752        for {set i $batch} \
1753                {$update_index_cp < $totalCnt && $i > 0} \
1754                {incr i -1} {
1755                set path [lindex $pathList $update_index_cp]
1756                incr update_index_cp
1757                switch -glob -- [lindex $file_states($path) 0] {
1758                U? {continue}
1759                ?M -
1760                ?D {
1761                        puts -nonewline $fd "[encoding convertto $path]\0"
1762                        display_file $path ?_
1763                }
1764                }
1765        }
1766
1767        set ui_status_value [format \
1768                "$msg... %i/%i files (%.2f%%)" \
1769                $update_index_cp \
1770                $totalCnt \
1771                [expr {100.0 * $update_index_cp / $totalCnt}]]
1772}
1773
1774######################################################################
1775##
1776## branch management
1777
1778proc is_tracking_branch {name} {
1779        global tracking_branches
1780
1781        if {![catch {set info $tracking_branches($name)}]} {
1782                return 1
1783        }
1784        foreach t [array names tracking_branches] {
1785                if {[string match {*/\*} $t] && [string match $t $name]} {
1786                        return 1
1787                }
1788        }
1789        return 0
1790}
1791
1792proc load_all_heads {} {
1793        global all_heads
1794
1795        set all_heads [list]
1796        set fd [open "| git for-each-ref --format=%(refname) refs/heads" r]
1797        while {[gets $fd line] > 0} {
1798                if {[is_tracking_branch $line]} continue
1799                if {![regsub ^refs/heads/ $line {} name]} continue
1800                lappend all_heads $name
1801        }
1802        close $fd
1803
1804        set all_heads [lsort $all_heads]
1805}
1806
1807proc populate_branch_menu {} {
1808        global all_heads disable_on_lock
1809
1810        set m .mbar.branch
1811        set last [$m index last]
1812        for {set i 0} {$i <= $last} {incr i} {
1813                if {[$m type $i] eq {separator}} {
1814                        $m delete $i last
1815                        set new_dol [list]
1816                        foreach a $disable_on_lock {
1817                                if {[lindex $a 0] ne $m || [lindex $a 2] < $i} {
1818                                        lappend new_dol $a
1819                                }
1820                        }
1821                        set disable_on_lock $new_dol
1822                        break
1823                }
1824        }
1825
1826        if {$all_heads ne {}} {
1827                $m add separator
1828        }
1829        foreach b $all_heads {
1830                $m add radiobutton \
1831                        -label $b \
1832                        -command [list switch_branch $b] \
1833                        -variable current_branch \
1834                        -value $b \
1835                        -font font_ui
1836                lappend disable_on_lock \
1837                        [list $m entryconf [$m index last] -state]
1838        }
1839}
1840
1841proc all_tracking_branches {} {
1842        global tracking_branches
1843
1844        set all_trackings {}
1845        set cmd {}
1846        foreach name [array names tracking_branches] {
1847                if {[regsub {/\*$} $name {} name]} {
1848                        lappend cmd $name
1849                } else {
1850                        regsub ^refs/(heads|remotes)/ $name {} name
1851                        lappend all_trackings $name
1852                }
1853        }
1854
1855        if {$cmd ne {}} {
1856                set fd [open "| git for-each-ref --format=%(refname) $cmd" r]
1857                while {[gets $fd name] > 0} {
1858                        regsub ^refs/(heads|remotes)/ $name {} name
1859                        lappend all_trackings $name
1860                }
1861                close $fd
1862        }
1863
1864        return [lsort -unique $all_trackings]
1865}
1866
1867proc do_create_branch_action {w} {
1868        global all_heads null_sha1 repo_config
1869        global create_branch_checkout create_branch_revtype
1870        global create_branch_head create_branch_trackinghead
1871        global create_branch_name create_branch_revexp
1872
1873        set newbranch $create_branch_name
1874        if {$newbranch eq {}
1875                || $newbranch eq $repo_config(gui.newbranchtemplate)} {
1876                tk_messageBox \
1877                        -icon error \
1878                        -type ok \
1879                        -title [wm title $w] \
1880                        -parent $w \
1881                        -message "Please supply a branch name."
1882                focus $w.desc.name_t
1883                return
1884        }
1885        if {![catch {exec git show-ref --verify -- "refs/heads/$newbranch"}]} {
1886                tk_messageBox \
1887                        -icon error \
1888                        -type ok \
1889                        -title [wm title $w] \
1890                        -parent $w \
1891                        -message "Branch '$newbranch' already exists."
1892                focus $w.desc.name_t
1893                return
1894        }
1895        if {[catch {exec git check-ref-format "heads/$newbranch"}]} {
1896                tk_messageBox \
1897                        -icon error \
1898                        -type ok \
1899                        -title [wm title $w] \
1900                        -parent $w \
1901                        -message "We do not like '$newbranch' as a branch name."
1902                focus $w.desc.name_t
1903                return
1904        }
1905
1906        set rev {}
1907        switch -- $create_branch_revtype {
1908        head {set rev $create_branch_head}
1909        tracking {set rev $create_branch_trackinghead}
1910        expression {set rev $create_branch_revexp}
1911        }
1912        if {[catch {set cmt [exec git rev-parse --verify "${rev}^0"]}]} {
1913                tk_messageBox \
1914                        -icon error \
1915                        -type ok \
1916                        -title [wm title $w] \
1917                        -parent $w \
1918                        -message "Invalid starting revision: $rev"
1919                return
1920        }
1921        set cmd [list git update-ref]
1922        lappend cmd -m
1923        lappend cmd "branch: Created from $rev"
1924        lappend cmd "refs/heads/$newbranch"
1925        lappend cmd $cmt
1926        lappend cmd $null_sha1
1927        if {[catch {eval exec $cmd} err]} {
1928                tk_messageBox \
1929                        -icon error \
1930                        -type ok \
1931                        -title [wm title $w] \
1932                        -parent $w \
1933                        -message "Failed to create '$newbranch'.\n\n$err"
1934                return
1935        }
1936
1937        lappend all_heads $newbranch
1938        set all_heads [lsort $all_heads]
1939        populate_branch_menu
1940        destroy $w
1941        if {$create_branch_checkout} {
1942                switch_branch $newbranch
1943        }
1944}
1945
1946proc radio_selector {varname value args} {
1947        upvar #0 $varname var
1948        set var $value
1949}
1950
1951trace add variable create_branch_head write \
1952        [list radio_selector create_branch_revtype head]
1953trace add variable create_branch_trackinghead write \
1954        [list radio_selector create_branch_revtype tracking]
1955
1956trace add variable delete_branch_head write \
1957        [list radio_selector delete_branch_checktype head]
1958trace add variable delete_branch_trackinghead write \
1959        [list radio_selector delete_branch_checktype tracking]
1960
1961proc do_create_branch {} {
1962        global all_heads current_branch repo_config
1963        global create_branch_checkout create_branch_revtype
1964        global create_branch_head create_branch_trackinghead
1965        global create_branch_name create_branch_revexp
1966
1967        set w .branch_editor
1968        toplevel $w
1969        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
1970
1971        label $w.header -text {Create New Branch} \
1972                -font font_uibold
1973        pack $w.header -side top -fill x
1974
1975        frame $w.buttons
1976        button $w.buttons.create -text Create \
1977                -font font_ui \
1978                -default active \
1979                -command [list do_create_branch_action $w]
1980        pack $w.buttons.create -side right
1981        button $w.buttons.cancel -text {Cancel} \
1982                -font font_ui \
1983                -command [list destroy $w]
1984        pack $w.buttons.cancel -side right -padx 5
1985        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
1986
1987        labelframe $w.desc \
1988                -text {Branch Description} \
1989                -font font_ui
1990        label $w.desc.name_l -text {Name:} -font font_ui
1991        entry $w.desc.name_t \
1992                -borderwidth 1 \
1993                -relief sunken \
1994                -width 40 \
1995                -textvariable create_branch_name \
1996                -font font_ui \
1997                -validate key \
1998                -validatecommand {
1999                        if {%d == 1 && [regexp {[~^:?*\[\0- ]} %S]} {return 0}
2000                        return 1
2001                }
2002        grid $w.desc.name_l $w.desc.name_t -sticky we -padx {0 5}
2003        grid columnconfigure $w.desc 1 -weight 1
2004        pack $w.desc -anchor nw -fill x -pady 5 -padx 5
2005
2006        labelframe $w.from \
2007                -text {Starting Revision} \
2008                -font font_ui
2009        radiobutton $w.from.head_r \
2010                -text {Local Branch:} \
2011                -value head \
2012                -variable create_branch_revtype \
2013                -font font_ui
2014        eval tk_optionMenu $w.from.head_m create_branch_head $all_heads
2015        grid $w.from.head_r $w.from.head_m -sticky w
2016        set all_trackings [all_tracking_branches]
2017        if {$all_trackings ne {}} {
2018                set create_branch_trackinghead [lindex $all_trackings 0]
2019                radiobutton $w.from.tracking_r \
2020                        -text {Tracking Branch:} \
2021                        -value tracking \
2022                        -variable create_branch_revtype \
2023                        -font font_ui
2024                eval tk_optionMenu $w.from.tracking_m \
2025                        create_branch_trackinghead \
2026                        $all_trackings
2027                grid $w.from.tracking_r $w.from.tracking_m -sticky w
2028        }
2029        radiobutton $w.from.exp_r \
2030                -text {Revision Expression:} \
2031                -value expression \
2032                -variable create_branch_revtype \
2033                -font font_ui
2034        entry $w.from.exp_t \
2035                -borderwidth 1 \
2036                -relief sunken \
2037                -width 50 \
2038                -textvariable create_branch_revexp \
2039                -font font_ui \
2040                -validate key \
2041                -validatecommand {
2042                        if {%d == 1 && [regexp {\s} %S]} {return 0}
2043                        if {%d == 1 && [string length %S] > 0} {
2044                                set create_branch_revtype expression
2045                        }
2046                        return 1
2047                }
2048        grid $w.from.exp_r $w.from.exp_t -sticky we -padx {0 5}
2049        grid columnconfigure $w.from 1 -weight 1
2050        pack $w.from -anchor nw -fill x -pady 5 -padx 5
2051
2052        labelframe $w.postActions \
2053                -text {Post Creation Actions} \
2054                -font font_ui
2055        checkbutton $w.postActions.checkout \
2056                -text {Checkout after creation} \
2057                -variable create_branch_checkout \
2058                -font font_ui
2059        pack $w.postActions.checkout -anchor nw
2060        pack $w.postActions -anchor nw -fill x -pady 5 -padx 5
2061
2062        set create_branch_checkout 1
2063        set create_branch_head $current_branch
2064        set create_branch_revtype head
2065        set create_branch_name $repo_config(gui.newbranchtemplate)
2066        set create_branch_revexp {}
2067
2068        bind $w <Visibility> "
2069                grab $w
2070                $w.desc.name_t icursor end
2071                focus $w.desc.name_t
2072        "
2073        bind $w <Key-Escape> "destroy $w"
2074        bind $w <Key-Return> "do_create_branch_action $w;break"
2075        wm title $w "[appname] ([reponame]): Create Branch"
2076        tkwait window $w
2077}
2078
2079proc do_delete_branch_action {w} {
2080        global all_heads
2081        global delete_branch_checktype delete_branch_head delete_branch_trackinghead
2082
2083        set check_rev {}
2084        switch -- $delete_branch_checktype {
2085        head {set check_rev $delete_branch_head}
2086        tracking {set check_rev $delete_branch_trackinghead}
2087        always {set check_rev {:none}}
2088        }
2089        if {$check_rev eq {:none}} {
2090                set check_cmt {}
2091        } elseif {[catch {set check_cmt [exec git rev-parse --verify "${check_rev}^0"]}]} {
2092                tk_messageBox \
2093                        -icon error \
2094                        -type ok \
2095                        -title [wm title $w] \
2096                        -parent $w \
2097                        -message "Invalid check revision: $check_rev"
2098                return
2099        }
2100
2101        set to_delete [list]
2102        set not_merged [list]
2103        foreach i [$w.list.l curselection] {
2104                set b [$w.list.l get $i]
2105                if {[catch {set o [exec git rev-parse --verify $b]}]} continue
2106                if {$check_cmt ne {}} {
2107                        if {$b eq $check_rev} continue
2108                        if {[catch {set m [exec git merge-base $o $check_cmt]}]} continue
2109                        if {$o ne $m} {
2110                                lappend not_merged $b
2111                                continue
2112                        }
2113                }
2114                lappend to_delete [list $b $o]
2115        }
2116        if {$not_merged ne {}} {
2117                set msg "The following branches are not completely merged into $check_rev:
2118
2119 - [join $not_merged "\n - "]"
2120                tk_messageBox \
2121                        -icon info \
2122                        -type ok \
2123                        -title [wm title $w] \
2124                        -parent $w \
2125                        -message $msg
2126        }
2127        if {$to_delete eq {}} return
2128        if {$delete_branch_checktype eq {always}} {
2129                set msg {Recovering deleted branches is difficult.
2130
2131Delete the selected branches?}
2132                if {[tk_messageBox \
2133                        -icon warning \
2134                        -type yesno \
2135                        -title [wm title $w] \
2136                        -parent $w \
2137                        -message $msg] ne yes} {
2138                        return
2139                }
2140        }
2141
2142        set failed {}
2143        foreach i $to_delete {
2144                set b [lindex $i 0]
2145                set o [lindex $i 1]
2146                if {[catch {exec git update-ref -d "refs/heads/$b" $o} err]} {
2147                        append failed " - $b: $err\n"
2148                } else {
2149                        set x [lsearch -sorted -exact $all_heads $b]
2150                        if {$x >= 0} {
2151                                set all_heads [lreplace $all_heads $x $x]
2152                        }
2153                }
2154        }
2155
2156        if {$failed ne {}} {
2157                tk_messageBox \
2158                        -icon error \
2159                        -type ok \
2160                        -title [wm title $w] \
2161                        -parent $w \
2162                        -message "Failed to delete branches:\n$failed"
2163        }
2164
2165        set all_heads [lsort $all_heads]
2166        populate_branch_menu
2167        destroy $w
2168}
2169
2170proc do_delete_branch {} {
2171        global all_heads tracking_branches current_branch
2172        global delete_branch_checktype delete_branch_head delete_branch_trackinghead
2173
2174        set w .branch_editor
2175        toplevel $w
2176        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
2177
2178        label $w.header -text {Delete Local Branch} \
2179                -font font_uibold
2180        pack $w.header -side top -fill x
2181
2182        frame $w.buttons
2183        button $w.buttons.create -text Delete \
2184                -font font_ui \
2185                -command [list do_delete_branch_action $w]
2186        pack $w.buttons.create -side right
2187        button $w.buttons.cancel -text {Cancel} \
2188                -font font_ui \
2189                -command [list destroy $w]
2190        pack $w.buttons.cancel -side right -padx 5
2191        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
2192
2193        labelframe $w.list \
2194                -text {Local Branches} \
2195                -font font_ui
2196        listbox $w.list.l \
2197                -height 10 \
2198                -width 70 \
2199                -selectmode extended \
2200                -yscrollcommand [list $w.list.sby set] \
2201                -font font_ui
2202        foreach h $all_heads {
2203                if {$h ne $current_branch} {
2204                        $w.list.l insert end $h
2205                }
2206        }
2207        scrollbar $w.list.sby -command [list $w.list.l yview]
2208        pack $w.list.sby -side right -fill y
2209        pack $w.list.l -side left -fill both -expand 1
2210        pack $w.list -fill both -expand 1 -pady 5 -padx 5
2211
2212        labelframe $w.validate \
2213                -text {Delete Only If} \
2214                -font font_ui
2215        radiobutton $w.validate.head_r \
2216                -text {Merged Into Local Branch:} \
2217                -value head \
2218                -variable delete_branch_checktype \
2219                -font font_ui
2220        eval tk_optionMenu $w.validate.head_m delete_branch_head $all_heads
2221        grid $w.validate.head_r $w.validate.head_m -sticky w
2222        set all_trackings [all_tracking_branches]
2223        if {$all_trackings ne {}} {
2224                set delete_branch_trackinghead [lindex $all_trackings 0]
2225                radiobutton $w.validate.tracking_r \
2226                        -text {Merged Into Tracking Branch:} \
2227                        -value tracking \
2228                        -variable delete_branch_checktype \
2229                        -font font_ui
2230                eval tk_optionMenu $w.validate.tracking_m \
2231                        delete_branch_trackinghead \
2232                        $all_trackings
2233                grid $w.validate.tracking_r $w.validate.tracking_m -sticky w
2234        }
2235        radiobutton $w.validate.always_r \
2236                -text {Always (Do not perform merge checks)} \
2237                -value always \
2238                -variable delete_branch_checktype \
2239                -font font_ui
2240        grid $w.validate.always_r -columnspan 2 -sticky w
2241        grid columnconfigure $w.validate 1 -weight 1
2242        pack $w.validate -anchor nw -fill x -pady 5 -padx 5
2243
2244        set delete_branch_head $current_branch
2245        set delete_branch_checktype head
2246
2247        bind $w <Visibility> "grab $w; focus $w"
2248        bind $w <Key-Escape> "destroy $w"
2249        wm title $w "[appname] ([reponame]): Delete Branch"
2250        tkwait window $w
2251}
2252
2253proc switch_branch {new_branch} {
2254        global HEAD commit_type current_branch repo_config
2255
2256        if {![lock_index switch]} return
2257
2258        # -- Our in memory state should match the repository.
2259        #
2260        repository_state curType curHEAD curMERGE_HEAD
2261        if {[string match amend* $commit_type]
2262                && $curType eq {normal}
2263                && $curHEAD eq $HEAD} {
2264        } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
2265                info_popup {Last scanned state does not match repository state.
2266
2267Another Git program has modified this repository
2268since the last scan.  A rescan must be performed
2269before the current branch can be changed.
2270
2271The rescan will be automatically started now.
2272}
2273                unlock_index
2274                rescan {set ui_status_value {Ready.}}
2275                return
2276        }
2277
2278        # -- Don't do a pointless switch.
2279        #
2280        if {$current_branch eq $new_branch} {
2281                unlock_index
2282                return
2283        }
2284
2285        if {$repo_config(gui.trustmtime) eq {true}} {
2286                switch_branch_stage2 {} $new_branch
2287        } else {
2288                set ui_status_value {Refreshing file status...}
2289                set cmd [list git update-index]
2290                lappend cmd -q
2291                lappend cmd --unmerged
2292                lappend cmd --ignore-missing
2293                lappend cmd --refresh
2294                set fd_rf [open "| $cmd" r]
2295                fconfigure $fd_rf -blocking 0 -translation binary
2296                fileevent $fd_rf readable \
2297                        [list switch_branch_stage2 $fd_rf $new_branch]
2298        }
2299}
2300
2301proc switch_branch_stage2 {fd_rf new_branch} {
2302        global ui_status_value HEAD
2303
2304        if {$fd_rf ne {}} {
2305                read $fd_rf
2306                if {![eof $fd_rf]} return
2307                close $fd_rf
2308        }
2309
2310        set ui_status_value "Updating working directory to '$new_branch'..."
2311        set cmd [list git read-tree]
2312        lappend cmd -m
2313        lappend cmd -u
2314        lappend cmd --exclude-per-directory=.gitignore
2315        lappend cmd $HEAD
2316        lappend cmd $new_branch
2317        set fd_rt [open "| $cmd" r]
2318        fconfigure $fd_rt -blocking 0 -translation binary
2319        fileevent $fd_rt readable \
2320                [list switch_branch_readtree_wait $fd_rt $new_branch]
2321}
2322
2323proc switch_branch_readtree_wait {fd_rt new_branch} {
2324        global selected_commit_type commit_type HEAD MERGE_HEAD PARENT
2325        global current_branch
2326        global ui_comm ui_status_value
2327
2328        # -- We never get interesting output on stdout; only stderr.
2329        #
2330        read $fd_rt
2331        fconfigure $fd_rt -blocking 1
2332        if {![eof $fd_rt]} {
2333                fconfigure $fd_rt -blocking 0
2334                return
2335        }
2336
2337        # -- The working directory wasn't in sync with the index and
2338        #    we'd have to overwrite something to make the switch. A
2339        #    merge is required.
2340        #
2341        if {[catch {close $fd_rt} err]} {
2342                regsub {^fatal: } $err {} err
2343                warn_popup "File level merge required.
2344
2345$err
2346
2347Staying on branch '$current_branch'."
2348                set ui_status_value "Aborted checkout of '$new_branch' (file level merging is required)."
2349                unlock_index
2350                return
2351        }
2352
2353        # -- Update the symbolic ref.  Core git doesn't even check for failure
2354        #    here, it Just Works(tm).  If it doesn't we are in some really ugly
2355        #    state that is difficult to recover from within git-gui.
2356        #
2357        if {[catch {exec git symbolic-ref HEAD "refs/heads/$new_branch"} err]} {
2358                error_popup "Failed to set current branch.
2359
2360This working directory is only partially switched.
2361We successfully updated your files, but failed to
2362update an internal Git file.
2363
2364This should not have occurred.  [appname] will now
2365close and give up.
2366
2367$err"
2368                do_quit
2369                return
2370        }
2371
2372        # -- Update our repository state.  If we were previously in amend mode
2373        #    we need to toss the current buffer and do a full rescan to update
2374        #    our file lists.  If we weren't in amend mode our file lists are
2375        #    accurate and we can avoid the rescan.
2376        #
2377        unlock_index
2378        set selected_commit_type new
2379        if {[string match amend* $commit_type]} {
2380                $ui_comm delete 0.0 end
2381                $ui_comm edit reset
2382                $ui_comm edit modified false
2383                rescan {set ui_status_value "Checked out branch '$current_branch'."}
2384        } else {
2385                repository_state commit_type HEAD MERGE_HEAD
2386                set PARENT $HEAD
2387                set ui_status_value "Checked out branch '$current_branch'."
2388        }
2389}
2390
2391######################################################################
2392##
2393## remote management
2394
2395proc load_all_remotes {} {
2396        global repo_config
2397        global all_remotes tracking_branches
2398
2399        set all_remotes [list]
2400        array unset tracking_branches
2401
2402        set rm_dir [gitdir remotes]
2403        if {[file isdirectory $rm_dir]} {
2404                set all_remotes [glob \
2405                        -types f \
2406                        -tails \
2407                        -nocomplain \
2408                        -directory $rm_dir *]
2409
2410                foreach name $all_remotes {
2411                        catch {
2412                                set fd [open [file join $rm_dir $name] r]
2413                                while {[gets $fd line] >= 0} {
2414                                        if {![regexp {^Pull:[   ]*([^:]+):(.+)$} \
2415                                                $line line src dst]} continue
2416                                        if {![regexp ^refs/ $dst]} {
2417                                                set dst "refs/heads/$dst"
2418                                        }
2419                                        set tracking_branches($dst) [list $name $src]
2420                                }
2421                                close $fd
2422                        }
2423                }
2424        }
2425
2426        foreach line [array names repo_config remote.*.url] {
2427                if {![regexp ^remote\.(.*)\.url\$ $line line name]} continue
2428                lappend all_remotes $name
2429
2430                if {[catch {set fl $repo_config(remote.$name.fetch)}]} {
2431                        set fl {}
2432                }
2433                foreach line $fl {
2434                        if {![regexp {^([^:]+):(.+)$} $line line src dst]} continue
2435                        if {![regexp ^refs/ $dst]} {
2436                                set dst "refs/heads/$dst"
2437                        }
2438                        set tracking_branches($dst) [list $name $src]
2439                }
2440        }
2441
2442        set all_remotes [lsort -unique $all_remotes]
2443}
2444
2445proc populate_fetch_menu {} {
2446        global all_remotes repo_config
2447
2448        set m .mbar.fetch
2449        foreach r $all_remotes {
2450                set enable 0
2451                if {![catch {set a $repo_config(remote.$r.url)}]} {
2452                        if {![catch {set a $repo_config(remote.$r.fetch)}]} {
2453                                set enable 1
2454                        }
2455                } else {
2456                        catch {
2457                                set fd [open [gitdir remotes $r] r]
2458                                while {[gets $fd n] >= 0} {
2459                                        if {[regexp {^Pull:[ \t]*([^:]+):} $n]} {
2460                                                set enable 1
2461                                                break
2462                                        }
2463                                }
2464                                close $fd
2465                        }
2466                }
2467
2468                if {$enable} {
2469                        $m add command \
2470                                -label "Fetch from $r..." \
2471                                -command [list fetch_from $r] \
2472                                -font font_ui
2473                }
2474        }
2475}
2476
2477proc populate_push_menu {} {
2478        global all_remotes repo_config
2479
2480        set m .mbar.push
2481        set fast_count 0
2482        foreach r $all_remotes {
2483                set enable 0
2484                if {![catch {set a $repo_config(remote.$r.url)}]} {
2485                        if {![catch {set a $repo_config(remote.$r.push)}]} {
2486                                set enable 1
2487                        }
2488                } else {
2489                        catch {
2490                                set fd [open [gitdir remotes $r] r]
2491                                while {[gets $fd n] >= 0} {
2492                                        if {[regexp {^Push:[ \t]*([^:]+):} $n]} {
2493                                                set enable 1
2494                                                break
2495                                        }
2496                                }
2497                                close $fd
2498                        }
2499                }
2500
2501                if {$enable} {
2502                        if {!$fast_count} {
2503                                $m add separator
2504                        }
2505                        $m add command \
2506                                -label "Push to $r..." \
2507                                -command [list push_to $r] \
2508                                -font font_ui
2509                        incr fast_count
2510                }
2511        }
2512}
2513
2514proc start_push_anywhere_action {w} {
2515        global push_urltype push_remote push_url push_thin push_tags
2516
2517        set r_url {}
2518        switch -- $push_urltype {
2519        remote {set r_url $push_remote}
2520        url {set r_url $push_url}
2521        }
2522        if {$r_url eq {}} return
2523
2524        set cmd [list git push]
2525        lappend cmd -v
2526        if {$push_thin} {
2527                lappend cmd --thin
2528        }
2529        if {$push_tags} {
2530                lappend cmd --tags
2531        }
2532        lappend cmd $r_url
2533        set cnt 0
2534        foreach i [$w.source.l curselection] {
2535                set b [$w.source.l get $i]
2536                lappend cmd "refs/heads/$b:refs/heads/$b"
2537                incr cnt
2538        }
2539        if {$cnt == 0} {
2540                return
2541        } elseif {$cnt == 1} {
2542                set unit branch
2543        } else {
2544                set unit branches
2545        }
2546
2547        set cons [new_console "push $r_url" "Pushing $cnt $unit to $r_url"]
2548        console_exec $cons $cmd console_done
2549        destroy $w
2550}
2551
2552trace add variable push_remote write \
2553        [list radio_selector push_urltype remote]
2554
2555proc do_push_anywhere {} {
2556        global all_heads all_remotes current_branch
2557        global push_urltype push_remote push_url push_thin push_tags
2558
2559        set w .push_setup
2560        toplevel $w
2561        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
2562
2563        label $w.header -text {Push Branches} -font font_uibold
2564        pack $w.header -side top -fill x
2565
2566        frame $w.buttons
2567        button $w.buttons.create -text Push \
2568                -font font_ui \
2569                -command [list start_push_anywhere_action $w]
2570        pack $w.buttons.create -side right
2571        button $w.buttons.cancel -text {Cancel} \
2572                -font font_ui \
2573                -command [list destroy $w]
2574        pack $w.buttons.cancel -side right -padx 5
2575        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
2576
2577        labelframe $w.source \
2578                -text {Source Branches} \
2579                -font font_ui
2580        listbox $w.source.l \
2581                -height 10 \
2582                -width 70 \
2583                -selectmode extended \
2584                -yscrollcommand [list $w.source.sby set] \
2585                -font font_ui
2586        foreach h $all_heads {
2587                $w.source.l insert end $h
2588                if {$h eq $current_branch} {
2589                        $w.source.l select set end
2590                }
2591        }
2592        scrollbar $w.source.sby -command [list $w.source.l yview]
2593        pack $w.source.sby -side right -fill y
2594        pack $w.source.l -side left -fill both -expand 1
2595        pack $w.source -fill both -expand 1 -pady 5 -padx 5
2596
2597        labelframe $w.dest \
2598                -text {Destination Repository} \
2599                -font font_ui
2600        if {$all_remotes ne {}} {
2601                radiobutton $w.dest.remote_r \
2602                        -text {Remote:} \
2603                        -value remote \
2604                        -variable push_urltype \
2605                        -font font_ui
2606                eval tk_optionMenu $w.dest.remote_m push_remote $all_remotes
2607                grid $w.dest.remote_r $w.dest.remote_m -sticky w
2608                if {[lsearch -sorted -exact $all_remotes origin] != -1} {
2609                        set push_remote origin
2610                } else {
2611                        set push_remote [lindex $all_remotes 0]
2612                }
2613                set push_urltype remote
2614        } else {
2615                set push_urltype url
2616        }
2617        radiobutton $w.dest.url_r \
2618                -text {Arbitrary URL:} \
2619                -value url \
2620                -variable push_urltype \
2621                -font font_ui
2622        entry $w.dest.url_t \
2623                -borderwidth 1 \
2624                -relief sunken \
2625                -width 50 \
2626                -textvariable push_url \
2627                -font font_ui \
2628                -validate key \
2629                -validatecommand {
2630                        if {%d == 1 && [regexp {\s} %S]} {return 0}
2631                        if {%d == 1 && [string length %S] > 0} {
2632                                set push_urltype url
2633                        }
2634                        return 1
2635                }
2636        grid $w.dest.url_r $w.dest.url_t -sticky we -padx {0 5}
2637        grid columnconfigure $w.dest 1 -weight 1
2638        pack $w.dest -anchor nw -fill x -pady 5 -padx 5
2639
2640        labelframe $w.options \
2641                -text {Transfer Options} \
2642                -font font_ui
2643        checkbutton $w.options.thin \
2644                -text {Use thin pack (for slow network connections)} \
2645                -variable push_thin \
2646                -font font_ui
2647        grid $w.options.thin -columnspan 2 -sticky w
2648        checkbutton $w.options.tags \
2649                -text {Include tags} \
2650                -variable push_tags \
2651                -font font_ui
2652        grid $w.options.tags -columnspan 2 -sticky w
2653        grid columnconfigure $w.options 1 -weight 1
2654        pack $w.options -anchor nw -fill x -pady 5 -padx 5
2655
2656        set push_url {}
2657        set push_thin 0
2658        set push_tags 0
2659
2660        bind $w <Visibility> "grab $w"
2661        bind $w <Key-Escape> "destroy $w"
2662        wm title $w "[appname] ([reponame]): Push"
2663        tkwait window $w
2664}
2665
2666######################################################################
2667##
2668## merge
2669
2670proc can_merge {} {
2671        global HEAD commit_type file_states
2672
2673        if {[string match amend* $commit_type]} {
2674                info_popup {Cannot merge while amending.
2675
2676You must finish amending this commit before
2677starting any type of merge.
2678}
2679                return 0
2680        }
2681
2682        if {[committer_ident] eq {}} {return 0}
2683        if {![lock_index merge]} {return 0}
2684
2685        # -- Our in memory state should match the repository.
2686        #
2687        repository_state curType curHEAD curMERGE_HEAD
2688        if {$commit_type ne $curType || $HEAD ne $curHEAD} {
2689                info_popup {Last scanned state does not match repository state.
2690
2691Another Git program has modified this repository
2692since the last scan.  A rescan must be performed
2693before a merge can be performed.
2694
2695The rescan will be automatically started now.
2696}
2697                unlock_index
2698                rescan {set ui_status_value {Ready.}}
2699                return 0
2700        }
2701
2702        foreach path [array names file_states] {
2703                switch -glob -- [lindex $file_states($path) 0] {
2704                _O {
2705                        continue; # and pray it works!
2706                }
2707                U? {
2708                        error_popup "You are in the middle of a conflicted merge.
2709
2710File [short_path $path] has merge conflicts.
2711
2712You must resolve them, add the file, and commit to
2713complete the current merge.  Only then can you
2714begin another merge.
2715"
2716                        unlock_index
2717                        return 0
2718                }
2719                ?? {
2720                        error_popup "You are in the middle of a change.
2721
2722File [short_path $path] is modified.
2723
2724You should complete the current commit before
2725starting a merge.  Doing so will help you abort
2726a failed merge, should the need arise.
2727"
2728                        unlock_index
2729                        return 0
2730                }
2731                }
2732        }
2733
2734        return 1
2735}
2736
2737proc visualize_local_merge {w} {
2738        set revs {}
2739        foreach i [$w.source.l curselection] {
2740                lappend revs [$w.source.l get $i]
2741        }
2742        if {$revs eq {}} return
2743        lappend revs --not HEAD
2744        do_gitk $revs
2745}
2746
2747proc start_local_merge_action {w} {
2748        global HEAD ui_status_value current_branch
2749
2750        set cmd [list git merge]
2751        set names {}
2752        set revcnt 0
2753        foreach i [$w.source.l curselection] {
2754                set b [$w.source.l get $i]
2755                lappend cmd $b
2756                lappend names $b
2757                incr revcnt
2758        }
2759
2760        if {$revcnt == 0} {
2761                return
2762        } elseif {$revcnt == 1} {
2763                set unit branch
2764        } elseif {$revcnt <= 15} {
2765                set unit branches
2766        } else {
2767                tk_messageBox \
2768                        -icon error \
2769                        -type ok \
2770                        -title [wm title $w] \
2771                        -parent $w \
2772                        -message "Too many branches selected.
2773
2774You have requested to merge $revcnt branches
2775in an octopus merge.  This exceeds Git's
2776internal limit of 15 branches per merge.
2777
2778Please select fewer branches.  To merge more
2779than 15 branches, merge the branches in batches.
2780"
2781                return
2782        }
2783
2784        set msg "Merging $current_branch, [join $names {, }]"
2785        set ui_status_value "$msg..."
2786        set cons [new_console "Merge" $msg]
2787        console_exec $cons $cmd [list finish_merge $revcnt]
2788        bind $w <Destroy> {}
2789        destroy $w
2790}
2791
2792proc finish_merge {revcnt w ok} {
2793        console_done $w $ok
2794        if {$ok} {
2795                set msg {Merge completed successfully.}
2796        } else {
2797                if {$revcnt != 1} {
2798                        info_popup "Octopus merge failed.
2799
2800Your merge of $revcnt branches has failed.
2801
2802There are file-level conflicts between the
2803branches which must be resolved manually.
2804
2805The working directory will now be reset.
2806
2807You can attempt this merge again
2808by merging only one branch at a time." $w
2809
2810                        set fd [open "| git read-tree --reset -u HEAD" r]
2811                        fconfigure $fd -blocking 0 -translation binary
2812                        fileevent $fd readable [list reset_hard_wait $fd]
2813                        set ui_status_value {Aborting... please wait...}
2814                        return
2815                }
2816
2817                set msg {Merge failed.  Conflict resolution is required.}
2818        }
2819        unlock_index
2820        rescan [list set ui_status_value $msg]
2821}
2822
2823proc do_local_merge {} {
2824        global current_branch
2825
2826        if {![can_merge]} return
2827
2828        set w .merge_setup
2829        toplevel $w
2830        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
2831
2832        label $w.header \
2833                -text "Merge Into $current_branch" \
2834                -font font_uibold
2835        pack $w.header -side top -fill x
2836
2837        frame $w.buttons
2838        button $w.buttons.visualize -text Visualize \
2839                -font font_ui \
2840                -command [list visualize_local_merge $w]
2841        pack $w.buttons.visualize -side left
2842        button $w.buttons.create -text Merge \
2843                -font font_ui \
2844                -command [list start_local_merge_action $w]
2845        pack $w.buttons.create -side right
2846        button $w.buttons.cancel -text {Cancel} \
2847                -font font_ui \
2848                -command [list destroy $w]
2849        pack $w.buttons.cancel -side right -padx 5
2850        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
2851
2852        labelframe $w.source \
2853                -text {Source Branches} \
2854                -font font_ui
2855        listbox $w.source.l \
2856                -height 10 \
2857                -width 70 \
2858                -selectmode extended \
2859                -yscrollcommand [list $w.source.sby set] \
2860                -font font_ui
2861        scrollbar $w.source.sby -command [list $w.source.l yview]
2862        pack $w.source.sby -side right -fill y
2863        pack $w.source.l -side left -fill both -expand 1
2864        pack $w.source -fill both -expand 1 -pady 5 -padx 5
2865
2866        set cmd [list git for-each-ref]
2867        lappend cmd {--format=%(objectname) %(refname)}
2868        lappend cmd refs/heads
2869        lappend cmd refs/remotes
2870        set fr_fd [open "| $cmd" r]
2871        fconfigure $fr_fd -translation binary
2872        while {[gets $fr_fd line] > 0} {
2873                set line [split $line { }]
2874                set sha1([lindex $line 0]) [lindex $line 1]
2875        }
2876        close $fr_fd
2877
2878        set to_show {}
2879        set fr_fd [open "| git rev-list --all --not HEAD"]
2880        while {[gets $fr_fd line] > 0} {
2881                if {[catch {set ref $sha1($line)}]} continue
2882                regsub ^refs/(heads|remotes)/ $ref {} ref
2883                lappend to_show $ref
2884        }
2885        close $fr_fd
2886
2887        foreach ref [lsort -unique $to_show] {
2888                $w.source.l insert end $ref
2889        }
2890
2891        bind $w <Visibility> "grab $w"
2892        bind $w <Key-Escape> "unlock_index;destroy $w"
2893        bind $w <Destroy> unlock_index
2894        wm title $w "[appname] ([reponame]): Merge"
2895        tkwait window $w
2896}
2897
2898proc do_reset_hard {} {
2899        global HEAD commit_type file_states
2900
2901        if {[string match amend* $commit_type]} {
2902                info_popup {Cannot abort while amending.
2903
2904You must finish amending this commit.
2905}
2906                return
2907        }
2908
2909        if {![lock_index abort]} return
2910
2911        if {[string match *merge* $commit_type]} {
2912                set op merge
2913        } else {
2914                set op commit
2915        }
2916
2917        if {[ask_popup "Abort $op?
2918
2919Aborting the current $op will cause
2920*ALL* uncommitted changes to be lost.
2921
2922Continue with aborting the current $op?"] eq {yes}} {
2923                set fd [open "| git read-tree --reset -u HEAD" r]
2924                fconfigure $fd -blocking 0 -translation binary
2925                fileevent $fd readable [list reset_hard_wait $fd]
2926                set ui_status_value {Aborting... please wait...}
2927        } else {
2928                unlock_index
2929        }
2930}
2931
2932proc reset_hard_wait {fd} {
2933        global ui_comm
2934
2935        read $fd
2936        if {[eof $fd]} {
2937                close $fd
2938                unlock_index
2939
2940                $ui_comm delete 0.0 end
2941                $ui_comm edit modified false
2942
2943                catch {file delete [gitdir MERGE_HEAD]}
2944                catch {file delete [gitdir rr-cache MERGE_RR]}
2945                catch {file delete [gitdir SQUASH_MSG]}
2946                catch {file delete [gitdir MERGE_MSG]}
2947                catch {file delete [gitdir GITGUI_MSG]}
2948
2949                rescan {set ui_status_value {Abort completed.  Ready.}}
2950        }
2951}
2952
2953######################################################################
2954##
2955## browser
2956
2957set next_browser_id 0
2958
2959proc new_browser {commit} {
2960        global next_browser_id cursor_ptr M1B
2961        global browser_commit browser_status browser_stack browser_path browser_busy
2962
2963        set w .browser[incr next_browser_id]
2964        set w_list $w.list.l
2965        set browser_commit($w_list) $commit
2966        set browser_status($w_list) {Starting...}
2967        set browser_stack($w_list) {}
2968        set browser_path($w_list) $browser_commit($w_list):
2969        set browser_busy($w_list) 1
2970
2971        toplevel $w
2972        label $w.path -textvariable browser_path($w_list) \
2973                -anchor w \
2974                -justify left \
2975                -borderwidth 1 \
2976                -relief sunken \
2977                -font font_uibold
2978        pack $w.path -anchor w -side top -fill x
2979
2980        frame $w.list
2981        text $w_list -background white -borderwidth 0 \
2982                -cursor $cursor_ptr \
2983                -state disabled \
2984                -wrap none \
2985                -height 20 \
2986                -width 70 \
2987                -xscrollcommand [list $w.list.sbx set] \
2988                -yscrollcommand [list $w.list.sby set] \
2989                -font font_ui
2990        $w_list tag conf in_sel \
2991                -background [$w_list cget -foreground] \
2992                -foreground [$w_list cget -background]
2993        scrollbar $w.list.sbx -orient h -command [list $w_list xview]
2994        scrollbar $w.list.sby -orient v -command [list $w_list yview]
2995        pack $w.list.sbx -side bottom -fill x
2996        pack $w.list.sby -side right -fill y
2997        pack $w_list -side left -fill both -expand 1
2998        pack $w.list -side top -fill both -expand 1
2999
3000        label $w.status -textvariable browser_status($w_list) \
3001                -anchor w \
3002                -justify left \
3003                -borderwidth 1 \
3004                -relief sunken \
3005                -font font_ui
3006        pack $w.status -anchor w -side bottom -fill x
3007
3008        bind $w_list <Button-1>        "browser_click 0 $w_list @%x,%y;break"
3009        bind $w_list <Double-Button-1> "browser_click 1 $w_list @%x,%y;break"
3010        bind $w_list <$M1B-Up>         "browser_parent $w_list;break"
3011        bind $w_list <$M1B-Left>       "browser_parent $w_list;break"
3012        bind $w_list <Up>              "browser_move -1 $w_list;break"
3013        bind $w_list <Down>            "browser_move 1 $w_list;break"
3014        bind $w_list <$M1B-Right>      "browser_enter $w_list;break"
3015        bind $w_list <Return>          "browser_enter $w_list;break"
3016        bind $w_list <Prior>           "browser_page -1 $w_list;break"
3017        bind $w_list <Next>            "browser_page 1 $w_list;break"
3018        bind $w_list <Left>            break
3019        bind $w_list <Right>           break
3020
3021        bind $w <Visibility> "focus $w"
3022        bind $w <Destroy> "
3023                array unset browser_buffer $w_list
3024                array unset browser_files $w_list
3025                array unset browser_status $w_list
3026                array unset browser_stack $w_list
3027                array unset browser_path $w_list
3028                array unset browser_commit $w_list
3029                array unset browser_busy $w_list
3030        "
3031        wm title $w "[appname] ([reponame]): File Browser"
3032        ls_tree $w_list $browser_commit($w_list) {}
3033}
3034
3035proc browser_move {dir w} {
3036        global browser_files browser_busy
3037
3038        if {$browser_busy($w)} return
3039        set lno [lindex [split [$w index in_sel.first] .] 0]
3040        incr lno $dir
3041        if {[lindex $browser_files($w) [expr {$lno - 1}]] ne {}} {
3042                $w tag remove in_sel 0.0 end
3043                $w tag add in_sel $lno.0 [expr {$lno + 1}].0
3044                $w see $lno.0
3045        }
3046}
3047
3048proc browser_page {dir w} {
3049        global browser_files browser_busy
3050
3051        if {$browser_busy($w)} return
3052        $w yview scroll $dir pages
3053        set lno [expr {int(
3054                  [lindex [$w yview] 0]
3055                * [llength $browser_files($w)]
3056                + 1)}]
3057        if {[lindex $browser_files($w) [expr {$lno - 1}]] ne {}} {
3058                $w tag remove in_sel 0.0 end
3059                $w tag add in_sel $lno.0 [expr {$lno + 1}].0
3060                $w see $lno.0
3061        }
3062}
3063
3064proc browser_parent {w} {
3065        global browser_files browser_status browser_path
3066        global browser_stack browser_busy
3067
3068        if {$browser_busy($w)} return
3069        set info [lindex $browser_files($w) 0]
3070        if {[lindex $info 0] eq {parent}} {
3071                set parent [lindex $browser_stack($w) end-1]
3072                set browser_stack($w) [lrange $browser_stack($w) 0 end-2]
3073                if {$browser_stack($w) eq {}} {
3074                        regsub {:.*$} $browser_path($w) {:} browser_path($w)
3075                } else {
3076                        regsub {/[^/]+$} $browser_path($w) {} browser_path($w)
3077                }
3078                set browser_status($w) "Loading $browser_path($w)..."
3079                ls_tree $w [lindex $parent 0] [lindex $parent 1]
3080        }
3081}
3082
3083proc browser_enter {w} {
3084        global browser_files browser_status browser_path
3085        global browser_commit browser_stack browser_busy
3086
3087        if {$browser_busy($w)} return
3088        set lno [lindex [split [$w index in_sel.first] .] 0]
3089        set info [lindex $browser_files($w) [expr {$lno - 1}]]
3090        if {$info ne {}} {
3091                switch -- [lindex $info 0] {
3092                parent {
3093                        browser_parent $w
3094                }
3095                tree {
3096                        set name [lindex $info 2]
3097                        set escn [escape_path $name]
3098                        set browser_status($w) "Loading $escn..."
3099                        append browser_path($w) $escn
3100                        ls_tree $w [lindex $info 1] $name
3101                }
3102                blob {
3103                        set name [lindex $info 2]
3104                        set p {}
3105                        foreach n $browser_stack($w) {
3106                                append p [lindex $n 1]
3107                        }
3108                        append p $name
3109                        show_blame $browser_commit($w) $p
3110                }
3111                }
3112        }
3113}
3114
3115proc browser_click {was_double_click w pos} {
3116        global browser_files browser_busy
3117
3118        if {$browser_busy($w)} return
3119        set lno [lindex [split [$w index $pos] .] 0]
3120        focus $w
3121
3122        if {[lindex $browser_files($w) [expr {$lno - 1}]] ne {}} {
3123                $w tag remove in_sel 0.0 end
3124                $w tag add in_sel $lno.0 [expr {$lno + 1}].0
3125                if {$was_double_click} {
3126                        browser_enter $w
3127                }
3128        }
3129}
3130
3131proc ls_tree {w tree_id name} {
3132        global browser_buffer browser_files browser_stack browser_busy
3133
3134        set browser_buffer($w) {}
3135        set browser_files($w) {}
3136        set browser_busy($w) 1
3137
3138        $w conf -state normal
3139        $w tag remove in_sel 0.0 end
3140        $w delete 0.0 end
3141        if {$browser_stack($w) ne {}} {
3142                $w image create end \
3143                        -align center -padx 5 -pady 1 \
3144                        -name icon0 \
3145                        -image file_uplevel
3146                $w insert end {[Up To Parent]}
3147                lappend browser_files($w) parent
3148        }
3149        lappend browser_stack($w) [list $tree_id $name]
3150        $w conf -state disabled
3151
3152        set cmd [list git ls-tree -z $tree_id]
3153        set fd [open "| $cmd" r]
3154        fconfigure $fd -blocking 0 -translation binary -encoding binary
3155        fileevent $fd readable [list read_ls_tree $fd $w]
3156}
3157
3158proc read_ls_tree {fd w} {
3159        global browser_buffer browser_files browser_status browser_busy
3160
3161        if {![winfo exists $w]} {
3162                catch {close $fd}
3163                return
3164        }
3165
3166        append browser_buffer($w) [read $fd]
3167        set pck [split $browser_buffer($w) "\0"]
3168        set browser_buffer($w) [lindex $pck end]
3169
3170        set n [llength $browser_files($w)]
3171        $w conf -state normal
3172        foreach p [lrange $pck 0 end-1] {
3173                set info [split $p "\t"]
3174                set path [lindex $info 1]
3175                set info [split [lindex $info 0] { }]
3176                set type [lindex $info 1]
3177                set object [lindex $info 2]
3178
3179                switch -- $type {
3180                blob {
3181                        set image file_mod
3182                }
3183                tree {
3184                        set image file_dir
3185                        append path /
3186                }
3187                default {
3188                        set image file_question
3189                }
3190                }
3191
3192                if {$n > 0} {$w insert end "\n"}
3193                $w image create end \
3194                        -align center -padx 5 -pady 1 \
3195                        -name icon[incr n] \
3196                        -image $image
3197                $w insert end [escape_path $path]
3198                lappend browser_files($w) [list $type $object $path]
3199        }
3200        $w conf -state disabled
3201
3202        if {[eof $fd]} {
3203                close $fd
3204                set browser_status($w) Ready.
3205                set browser_busy($w) 0
3206                array unset browser_buffer $w
3207                if {$n > 0} {
3208                        $w tag add in_sel 1.0 2.0
3209                        focus -force $w
3210                }
3211        }
3212}
3213
3214proc show_blame {commit path} {
3215        global next_browser_id blame_status blame_data
3216
3217        set w .browser[incr next_browser_id]
3218        set blame_status($w) {Loading current file content...}
3219        set texts [list]
3220
3221        toplevel $w
3222
3223        label $w.path -text "$commit:$path" \
3224                -anchor w \
3225                -justify left \
3226                -borderwidth 1 \
3227                -relief sunken \
3228                -font font_uibold
3229        pack $w.path -side top -fill x
3230
3231        set hbg #e2effa
3232        frame $w.out
3233        label $w.out.commit_l -text Commit \
3234                -relief solid \
3235                -borderwidth 1 \
3236                -background $hbg \
3237                -font font_uibold
3238        text $w.out.commit_t \
3239                -background white -borderwidth 0 \
3240                -state disabled \
3241                -wrap none \
3242                -height 40 \
3243                -width 9 \
3244                -font font_diff
3245        lappend texts $w.out.commit_t
3246
3247        label $w.out.author_l -text Author \
3248                -relief solid \
3249                -borderwidth 1 \
3250                -background $hbg \
3251                -font font_uibold
3252        text $w.out.author_t \
3253                -background white -borderwidth 0 \
3254                -state disabled \
3255                -wrap none \
3256                -height 40 \
3257                -width 20 \
3258                -font font_diff
3259        lappend texts $w.out.author_t
3260
3261        label $w.out.date_l -text Date \
3262                -relief solid \
3263                -borderwidth 1 \
3264                -background $hbg \
3265                -font font_uibold
3266        text $w.out.date_t \
3267                -background white -borderwidth 0 \
3268                -state disabled \
3269                -wrap none \
3270                -height 40 \
3271                -width [string length "yyyy-mm-dd hh:mm:ss"] \
3272                -font font_diff
3273        lappend texts $w.out.date_t
3274
3275        label $w.out.filename_l -text Filename \
3276                -relief solid \
3277                -borderwidth 1 \
3278                -background $hbg \
3279                -font font_uibold
3280        text $w.out.filename_t \
3281                -background white -borderwidth 0 \
3282                -state disabled \
3283                -wrap none \
3284                -height 40 \
3285                -width 20 \
3286                -font font_diff
3287        lappend texts $w.out.filename_t
3288
3289        label $w.out.origlinenumber_l -text {Orig Line} \
3290                -relief solid \
3291                -borderwidth 1 \
3292                -background $hbg \
3293                -font font_uibold
3294        text $w.out.origlinenumber_t \
3295                -background white -borderwidth 0 \
3296                -state disabled \
3297                -wrap none \
3298                -height 40 \
3299                -width 5 \
3300                -font font_diff
3301        $w.out.origlinenumber_t tag conf linenumber -justify right
3302        lappend texts $w.out.origlinenumber_t
3303
3304        label $w.out.linenumber_l -text {Curr Line} \
3305                -relief solid \
3306                -borderwidth 1 \
3307                -background $hbg \
3308                -font font_uibold
3309        text $w.out.linenumber_t \
3310                -background white -borderwidth 0 \
3311                -state disabled \
3312                -wrap none \
3313                -height 40 \
3314                -width 5 \
3315                -font font_diff
3316        $w.out.linenumber_t tag conf linenumber -justify right
3317        lappend texts $w.out.linenumber_t
3318
3319        label $w.out.file_l -text {File Content} \
3320                -relief solid \
3321                -borderwidth 1 \
3322                -background $hbg \
3323                -font font_uibold
3324        text $w.out.file_t \
3325                -background white -borderwidth 0 \
3326                -state disabled \
3327                -wrap none \
3328                -height 40 \
3329                -width 80 \
3330                -xscrollcommand [list $w.out.sbx set] \
3331                -font font_diff
3332        lappend texts $w.out.file_t
3333
3334        scrollbar $w.out.sbx -orient h -command [list $w.out.file_t xview]
3335        scrollbar $w.out.sby -orient v \
3336                -command [list scrollbar2many $texts yview]
3337        set labels [list]
3338        foreach i $texts {
3339                regsub {_t$} $i _l l
3340                lappend labels $l
3341        }
3342        set file_col [expr {[llength $texts] - 1}]
3343        eval grid $labels -sticky we
3344        eval grid $texts $w.out.sby -sticky nsew
3345        grid conf $w.out.sbx -column $file_col -sticky we
3346        grid columnconfigure $w.out $file_col -weight 1
3347        grid rowconfigure $w.out 1 -weight 1
3348        pack $w.out -fill both -expand 1
3349
3350        label $w.status -textvariable blame_status($w) \
3351                -anchor w \
3352                -justify left \
3353                -borderwidth 1 \
3354                -relief sunken \
3355                -font font_ui
3356        pack $w.status -side bottom -fill x
3357
3358        menu $w.ctxm -tearoff 0
3359        $w.ctxm add command -label "Copy Commit" \
3360                -font font_ui \
3361                -command "blame_copycommit $w \$cursorW @\$cursorX,\$cursorY"
3362
3363        foreach i $texts {
3364                $i tag conf in_sel \
3365                        -background [$i cget -foreground] \
3366                        -foreground [$i cget -background]
3367                $i conf -yscrollcommand \
3368                        [list many2scrollbar $texts yview $w.out.sby]
3369                bind $i <Button-1> "blame_highlight $i @%x,%y $texts;break"
3370                bind_button3 $i "
3371                        set cursorX %x
3372                        set cursorY %y
3373                        set cursorW %W
3374                        tk_popup $w.ctxm %X %Y
3375                "
3376        }
3377
3378        set blame_data($w,colors) {}
3379
3380        bind $w <Visibility> "focus $w"
3381        bind $w <Destroy> "
3382                array unset blame_status $w
3383                array unset blame_data $w,*
3384        "
3385        wm title $w "[appname] ([reponame]): File Viewer"
3386
3387        set blame_data($w,total_lines) 0
3388        set cmd [list git cat-file blob "$commit:$path"]
3389        set fd [open "| $cmd" r]
3390        fconfigure $fd -blocking 0 -translation lf -encoding binary
3391        fileevent $fd readable [list read_blame_catfile \
3392                $fd $w $commit $path \
3393                $texts $w.out.linenumber_t $w.out.file_t]
3394}
3395
3396proc read_blame_catfile {fd w commit path texts w_lno w_file} {
3397        global blame_status blame_data
3398
3399        if {![winfo exists $w_file]} {
3400                catch {close $fd}
3401                return
3402        }
3403
3404        set n $blame_data($w,total_lines)
3405        foreach i $texts {$i conf -state normal}
3406        while {[gets $fd line] >= 0} {
3407                regsub "\r\$" $line {} line
3408                incr n
3409                $w_lno insert end $n linenumber
3410                $w_file insert end $line
3411                foreach i $texts {$i insert end "\n"}
3412        }
3413        foreach i $texts {$i conf -state disabled}
3414        set blame_data($w,total_lines) $n
3415
3416        if {[eof $fd]} {
3417                close $fd
3418                set blame_status($w) {Loading annotations...}
3419                set cmd [list git blame -M -C --incremental]
3420                lappend cmd $commit -- $path
3421                set fd [open "| $cmd" r]
3422                fconfigure $fd -blocking 0 -translation lf -encoding binary
3423                fileevent $fd readable "read_blame_incremental $fd $w $texts"
3424        }
3425}
3426
3427proc read_blame_incremental {fd w
3428        w_commit w_author w_date w_filename w_olno
3429        w_lno w_file} {
3430        global blame_status blame_data
3431
3432        if {![winfo exists $w_commit]} {
3433                catch {close $fd}
3434                return
3435        }
3436
3437        set all [list \
3438                $w_commit \
3439                $w_author \
3440                $w_date \
3441                $w_filename \
3442                $w_olno \
3443                $w_lno \
3444                $w_file]
3445
3446        $w_commit conf -state normal
3447        $w_author conf -state normal
3448        $w_date conf -state normal
3449        $w_filename conf -state normal
3450        $w_olno conf -state normal
3451
3452        while {[gets $fd line] >= 0} {
3453                if {[regexp {^([a-z0-9]{40}) (\d+) (\d+) (\d+)$} $line line \
3454                        cmit original_line final_line line_count]} {
3455                        set blame_data($w,commit) $cmit
3456                        set blame_data($w,original_line) $original_line
3457                        set blame_data($w,final_line) $final_line
3458                        set blame_data($w,line_count) $line_count
3459
3460                        if {[catch {set g $blame_data($w,$cmit,seen)}]} {
3461                                if {$blame_data($w,colors) eq {}} {
3462                                        set blame_data($w,colors) {
3463                                                yellow
3464                                                red
3465                                                pink
3466                                                orange
3467                                                green
3468                                                grey
3469                                        }
3470                                }
3471                                set c [lindex $blame_data($w,colors) 0]
3472                                set blame_data($w,colors) \
3473                                        [lrange $blame_data($w,colors) 1 end]
3474                                foreach t $all {
3475                                        $t tag conf g$cmit -background $c
3476                                }
3477                        } else {
3478                                set blame_data($w,$cmit,seen) 1
3479                        }
3480                } elseif {[string match {filename *} $line]} {
3481                        set n $blame_data($w,line_count)
3482                        set lno $blame_data($w,final_line)
3483                        set ol $blame_data($w,original_line)
3484                        set file [string range $line 9 end]
3485                        set cmit $blame_data($w,commit)
3486                        set abbrev [string range $cmit 0 8]
3487
3488                        if {[catch {set author $blame_data($w,$cmit,author)} err]} {
3489                                set author {}
3490                        }
3491
3492                        if {[catch {set atime $blame_data($w,$cmit,author-time)}]} {
3493                                set atime {}
3494                        } else {
3495                                set atime [clock format $atime -format {%Y-%m-%d %T}]
3496                        }
3497
3498                        while {$n > 0} {
3499                                if {![catch {set g g$blame_data($w,line$lno,commit)}]} {
3500                                        foreach t $all {
3501                                                $t tag remove $g $lno.0 "$lno.0 lineend + 1c"
3502                                        }
3503                                }
3504
3505                                foreach t [list \
3506                                        $w_commit \
3507                                        $w_author \
3508                                        $w_date \
3509                                        $w_filename \
3510                                        $w_olno] {
3511                                        $t delete $lno.0 "$lno.0 lineend"
3512                                }
3513
3514                                $w_commit insert $lno.0 $abbrev
3515                                $w_author insert $lno.0 $author
3516                                $w_date insert $lno.0 $atime
3517                                $w_filename insert $lno.0 $file
3518                                $w_olno insert $lno.0 $ol linenumber
3519
3520                                set g g$cmit
3521                                foreach t $all {
3522                                        $t tag add $g $lno.0 "$lno.0 lineend + 1c"
3523                                }
3524
3525                                set blame_data($w,line$lno,commit) $cmit
3526
3527                                incr n -1
3528                                incr lno
3529                                incr ol
3530                        }
3531                } elseif {[regexp {^([a-z-]+) (.*)$} $line line header data]} {
3532                        set blame_data($w,$blame_data($w,commit),$header) $data
3533                }
3534        }
3535
3536        $w_commit conf -state disabled
3537        $w_author conf -state disabled
3538        $w_date conf -state disabled
3539        $w_filename conf -state disabled
3540        $w_olno conf -state disabled
3541
3542        if {[eof $fd]} {
3543                close $fd
3544                set blame_status($w) {Annotation complete.}
3545        }
3546}
3547
3548proc blame_highlight {w pos args} {
3549        set lno [lindex [split [$w index $pos] .] 0]
3550        foreach i $args {
3551                $i tag remove in_sel 0.0 end
3552        }
3553        if {$lno eq {}} return
3554        foreach i $args {
3555                $i tag add in_sel $lno.0 "$lno.0 + 1 line"
3556        }
3557}
3558
3559proc blame_copycommit {w i pos} {
3560        global blame_data
3561        set lno [lindex [split [$i index $pos] .] 0]
3562        if {![catch {set commit $blame_data($w,line$lno,commit)}]} {
3563                clipboard clear
3564                clipboard append \
3565                        -format STRING \
3566                        -type STRING \
3567                        -- $commit
3568        }
3569}
3570
3571######################################################################
3572##
3573## icons
3574
3575set filemask {
3576#define mask_width 14
3577#define mask_height 15
3578static unsigned char mask_bits[] = {
3579   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
3580   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
3581   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f};
3582}
3583
3584image create bitmap file_plain -background white -foreground black -data {
3585#define plain_width 14
3586#define plain_height 15
3587static unsigned char plain_bits[] = {
3588   0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
3589   0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10,
3590   0x02, 0x10, 0x02, 0x10, 0xfe, 0x1f};
3591} -maskdata $filemask
3592
3593image create bitmap file_mod -background white -foreground blue -data {
3594#define mod_width 14
3595#define mod_height 15
3596static unsigned char mod_bits[] = {
3597   0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
3598   0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
3599   0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
3600} -maskdata $filemask
3601
3602image create bitmap file_fulltick -background white -foreground "#007000" -data {
3603#define file_fulltick_width 14
3604#define file_fulltick_height 15
3605static unsigned char file_fulltick_bits[] = {
3606   0xfe, 0x01, 0x02, 0x1a, 0x02, 0x0c, 0x02, 0x0c, 0x02, 0x16, 0x02, 0x16,
3607   0x02, 0x13, 0x00, 0x13, 0x86, 0x11, 0x8c, 0x11, 0xd8, 0x10, 0xf2, 0x10,
3608   0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
3609} -maskdata $filemask
3610
3611image create bitmap file_parttick -background white -foreground "#005050" -data {
3612#define parttick_width 14
3613#define parttick_height 15
3614static unsigned char parttick_bits[] = {
3615   0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
3616   0x7a, 0x14, 0x02, 0x16, 0x02, 0x13, 0x8a, 0x11, 0xda, 0x10, 0x72, 0x10,
3617   0x22, 0x10, 0x02, 0x10, 0xfe, 0x1f};
3618} -maskdata $filemask
3619
3620image create bitmap file_question -background white -foreground black -data {
3621#define file_question_width 14
3622#define file_question_height 15
3623static unsigned char file_question_bits[] = {
3624   0xfe, 0x01, 0x02, 0x02, 0xe2, 0x04, 0xf2, 0x09, 0x1a, 0x1b, 0x0a, 0x13,
3625   0x82, 0x11, 0xc2, 0x10, 0x62, 0x10, 0x62, 0x10, 0x02, 0x10, 0x62, 0x10,
3626   0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
3627} -maskdata $filemask
3628
3629image create bitmap file_removed -background white -foreground red -data {
3630#define file_removed_width 14
3631#define file_removed_height 15
3632static unsigned char file_removed_bits[] = {
3633   0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
3634   0x1a, 0x16, 0x32, 0x13, 0xe2, 0x11, 0xc2, 0x10, 0xe2, 0x11, 0x32, 0x13,
3635   0x1a, 0x16, 0x02, 0x10, 0xfe, 0x1f};
3636} -maskdata $filemask
3637
3638image create bitmap file_merge -background white -foreground blue -data {
3639#define file_merge_width 14
3640#define file_merge_height 15
3641static unsigned char file_merge_bits[] = {
3642   0xfe, 0x01, 0x02, 0x03, 0x62, 0x05, 0x62, 0x09, 0x62, 0x1f, 0x62, 0x10,
3643   0xfa, 0x11, 0xf2, 0x10, 0x62, 0x10, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
3644   0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
3645} -maskdata $filemask
3646
3647set file_dir_data {
3648#define file_width 18
3649#define file_height 18
3650static unsigned char file_bits[] = {
3651  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x03, 0x00,
3652  0x0c, 0x03, 0x00, 0x04, 0xfe, 0x00, 0x06, 0x80, 0x00, 0xff, 0x9f, 0x00,
3653  0x03, 0x98, 0x00, 0x02, 0x90, 0x00, 0x06, 0xb0, 0x00, 0x04, 0xa0, 0x00,
3654  0x0c, 0xe0, 0x00, 0x08, 0xc0, 0x00, 0xf8, 0xff, 0x00, 0x00, 0x00, 0x00,
3655  0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
3656}
3657image create bitmap file_dir -background white -foreground blue \
3658        -data $file_dir_data -maskdata $file_dir_data
3659unset file_dir_data
3660
3661set file_uplevel_data {
3662#define up_width 15
3663#define up_height 15
3664static unsigned char up_bits[] = {
3665  0x80, 0x00, 0xc0, 0x01, 0xe0, 0x03, 0xf0, 0x07, 0xf8, 0x0f, 0xfc, 0x1f,
3666  0xfe, 0x3f, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01, 0xc0, 0x01,
3667  0xc0, 0x01, 0xc0, 0x01, 0x00, 0x00};
3668}
3669image create bitmap file_uplevel -background white -foreground red \
3670        -data $file_uplevel_data -maskdata $file_uplevel_data
3671unset file_uplevel_data
3672
3673set ui_index .vpane.files.index.list
3674set ui_workdir .vpane.files.workdir.list
3675
3676set all_icons(_$ui_index)   file_plain
3677set all_icons(A$ui_index)   file_fulltick
3678set all_icons(M$ui_index)   file_fulltick
3679set all_icons(D$ui_index)   file_removed
3680set all_icons(U$ui_index)   file_merge
3681
3682set all_icons(_$ui_workdir) file_plain
3683set all_icons(M$ui_workdir) file_mod
3684set all_icons(D$ui_workdir) file_question
3685set all_icons(U$ui_workdir) file_merge
3686set all_icons(O$ui_workdir) file_plain
3687
3688set max_status_desc 0
3689foreach i {
3690                {__ "Unmodified"}
3691
3692                {_M "Modified, not staged"}
3693                {M_ "Staged for commit"}
3694                {MM "Portions staged for commit"}
3695                {MD "Staged for commit, missing"}
3696
3697                {_O "Untracked, not staged"}
3698                {A_ "Staged for commit"}
3699                {AM "Portions staged for commit"}
3700                {AD "Staged for commit, missing"}
3701
3702                {_D "Missing"}
3703                {D_ "Staged for removal"}
3704                {DO "Staged for removal, still present"}
3705
3706                {U_ "Requires merge resolution"}
3707                {UU "Requires merge resolution"}
3708                {UM "Requires merge resolution"}
3709                {UD "Requires merge resolution"}
3710        } {
3711        if {$max_status_desc < [string length [lindex $i 1]]} {
3712                set max_status_desc [string length [lindex $i 1]]
3713        }
3714        set all_descs([lindex $i 0]) [lindex $i 1]
3715}
3716unset i
3717
3718######################################################################
3719##
3720## util
3721
3722proc bind_button3 {w cmd} {
3723        bind $w <Any-Button-3> $cmd
3724        if {[is_MacOSX]} {
3725                bind $w <Control-Button-1> $cmd
3726        }
3727}
3728
3729proc scrollbar2many {list mode args} {
3730        foreach w $list {eval $w $mode $args}
3731}
3732
3733proc many2scrollbar {list mode sb top bottom} {
3734        $sb set $top $bottom
3735        foreach w $list {$w $mode moveto $top}
3736}
3737
3738proc incr_font_size {font {amt 1}} {
3739        set sz [font configure $font -size]
3740        incr sz $amt
3741        font configure $font -size $sz
3742        font configure ${font}bold -size $sz
3743}
3744
3745proc hook_failed_popup {hook msg} {
3746        set w .hookfail
3747        toplevel $w
3748
3749        frame $w.m
3750        label $w.m.l1 -text "$hook hook failed:" \
3751                -anchor w \
3752                -justify left \
3753                -font font_uibold
3754        text $w.m.t \
3755                -background white -borderwidth 1 \
3756                -relief sunken \
3757                -width 80 -height 10 \
3758                -font font_diff \
3759                -yscrollcommand [list $w.m.sby set]
3760        label $w.m.l2 \
3761                -text {You must correct the above errors before committing.} \
3762                -anchor w \
3763                -justify left \
3764                -font font_uibold
3765        scrollbar $w.m.sby -command [list $w.m.t yview]
3766        pack $w.m.l1 -side top -fill x
3767        pack $w.m.l2 -side bottom -fill x
3768        pack $w.m.sby -side right -fill y
3769        pack $w.m.t -side left -fill both -expand 1
3770        pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
3771
3772        $w.m.t insert 1.0 $msg
3773        $w.m.t conf -state disabled
3774
3775        button $w.ok -text OK \
3776                -width 15 \
3777                -font font_ui \
3778                -command "destroy $w"
3779        pack $w.ok -side bottom -anchor e -pady 10 -padx 10
3780
3781        bind $w <Visibility> "grab $w; focus $w"
3782        bind $w <Key-Return> "destroy $w"
3783        wm title $w "[appname] ([reponame]): error"
3784        tkwait window $w
3785}
3786
3787set next_console_id 0
3788
3789proc new_console {short_title long_title} {
3790        global next_console_id console_data
3791        set w .console[incr next_console_id]
3792        set console_data($w) [list $short_title $long_title]
3793        return [console_init $w]
3794}
3795
3796proc console_init {w} {
3797        global console_cr console_data M1B
3798
3799        set console_cr($w) 1.0
3800        toplevel $w
3801        frame $w.m
3802        label $w.m.l1 -text "[lindex $console_data($w) 1]:" \
3803                -anchor w \
3804                -justify left \
3805                -font font_uibold
3806        text $w.m.t \
3807                -background white -borderwidth 1 \
3808                -relief sunken \
3809                -width 80 -height 10 \
3810                -font font_diff \
3811                -state disabled \
3812                -yscrollcommand [list $w.m.sby set]
3813        label $w.m.s -text {Working... please wait...} \
3814                -anchor w \
3815                -justify left \
3816                -font font_uibold
3817        scrollbar $w.m.sby -command [list $w.m.t yview]
3818        pack $w.m.l1 -side top -fill x
3819        pack $w.m.s -side bottom -fill x
3820        pack $w.m.sby -side right -fill y
3821        pack $w.m.t -side left -fill both -expand 1
3822        pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
3823
3824        menu $w.ctxm -tearoff 0
3825        $w.ctxm add command -label "Copy" \
3826                -font font_ui \
3827                -command "tk_textCopy $w.m.t"
3828        $w.ctxm add command -label "Select All" \
3829                -font font_ui \
3830                -command "focus $w.m.t;$w.m.t tag add sel 0.0 end"
3831        $w.ctxm add command -label "Copy All" \
3832                -font font_ui \
3833                -command "
3834                        $w.m.t tag add sel 0.0 end
3835                        tk_textCopy $w.m.t
3836                        $w.m.t tag remove sel 0.0 end
3837                "
3838
3839        button $w.ok -text {Close} \
3840                -font font_ui \
3841                -state disabled \
3842                -command "destroy $w"
3843        pack $w.ok -side bottom -anchor e -pady 10 -padx 10
3844
3845        bind_button3 $w.m.t "tk_popup $w.ctxm %X %Y"
3846        bind $w.m.t <$M1B-Key-a> "$w.m.t tag add sel 0.0 end;break"
3847        bind $w.m.t <$M1B-Key-A> "$w.m.t tag add sel 0.0 end;break"
3848        bind $w <Visibility> "focus $w"
3849        wm title $w "[appname] ([reponame]): [lindex $console_data($w) 0]"
3850        return $w
3851}
3852
3853proc console_exec {w cmd after} {
3854        # -- Cygwin's Tcl tosses the enviroment when we exec our child.
3855        #    But most users need that so we have to relogin. :-(
3856        #
3857        if {[is_Cygwin]} {
3858                set cmd [list sh --login -c "cd \"[pwd]\" && [join $cmd { }]"]
3859        }
3860
3861        # -- Tcl won't let us redirect both stdout and stderr to
3862        #    the same pipe.  So pass it through cat...
3863        #
3864        set cmd [concat | $cmd |& cat]
3865
3866        set fd_f [open $cmd r]
3867        fconfigure $fd_f -blocking 0 -translation binary
3868        fileevent $fd_f readable [list console_read $w $fd_f $after]
3869}
3870
3871proc console_read {w fd after} {
3872        global console_cr
3873
3874        set buf [read $fd]
3875        if {$buf ne {}} {
3876                if {![winfo exists $w]} {console_init $w}
3877                $w.m.t conf -state normal
3878                set c 0
3879                set n [string length $buf]
3880                while {$c < $n} {
3881                        set cr [string first "\r" $buf $c]
3882                        set lf [string first "\n" $buf $c]
3883                        if {$cr < 0} {set cr [expr {$n + 1}]}
3884                        if {$lf < 0} {set lf [expr {$n + 1}]}
3885
3886                        if {$lf < $cr} {
3887                                $w.m.t insert end [string range $buf $c $lf]
3888                                set console_cr($w) [$w.m.t index {end -1c}]
3889                                set c $lf
3890                                incr c
3891                        } else {
3892                                $w.m.t delete $console_cr($w) end
3893                                $w.m.t insert end "\n"
3894                                $w.m.t insert end [string range $buf $c $cr]
3895                                set c $cr
3896                                incr c
3897                        }
3898                }
3899                $w.m.t conf -state disabled
3900                $w.m.t see end
3901        }
3902
3903        fconfigure $fd -blocking 1
3904        if {[eof $fd]} {
3905                if {[catch {close $fd}]} {
3906                        set ok 0
3907                } else {
3908                        set ok 1
3909                }
3910                uplevel #0 $after $w $ok
3911                return
3912        }
3913        fconfigure $fd -blocking 0
3914}
3915
3916proc console_chain {cmdlist w {ok 1}} {
3917        if {$ok} {
3918                if {[llength $cmdlist] == 0} {
3919                        console_done $w $ok
3920                        return
3921                }
3922
3923                set cmd [lindex $cmdlist 0]
3924                set cmdlist [lrange $cmdlist 1 end]
3925
3926                if {[lindex $cmd 0] eq {console_exec}} {
3927                        console_exec $w \
3928                                [lindex $cmd 1] \
3929                                [list console_chain $cmdlist]
3930                } else {
3931                        uplevel #0 $cmd $cmdlist $w $ok
3932                }
3933        } else {
3934                console_done $w $ok
3935        }
3936}
3937
3938proc console_done {args} {
3939        global console_cr console_data
3940
3941        switch -- [llength $args] {
3942        2 {
3943                set w [lindex $args 0]
3944                set ok [lindex $args 1]
3945        }
3946        3 {
3947                set w [lindex $args 1]
3948                set ok [lindex $args 2]
3949        }
3950        default {
3951                error "wrong number of args: console_done ?ignored? w ok"
3952        }
3953        }
3954
3955        if {$ok} {
3956                if {[winfo exists $w]} {
3957                        $w.m.s conf -background green -text {Success}
3958                        $w.ok conf -state normal
3959                }
3960        } else {
3961                if {![winfo exists $w]} {
3962                        console_init $w
3963                }
3964                $w.m.s conf -background red -text {Error: Command Failed}
3965                $w.ok conf -state normal
3966        }
3967
3968        array unset console_cr $w
3969        array unset console_data $w
3970}
3971
3972######################################################################
3973##
3974## ui commands
3975
3976set starting_gitk_msg {Starting gitk... please wait...}
3977
3978proc do_gitk {revs} {
3979        global env ui_status_value starting_gitk_msg
3980
3981        # -- On Windows gitk is severly broken, and right now it seems like
3982        #    nobody cares about fixing it.  The only known workaround is to
3983        #    always delete ~/.gitk before starting the program.
3984        #
3985        if {[is_Windows]} {
3986                catch {file delete [file join $env(HOME) .gitk]}
3987        }
3988
3989        # -- Always start gitk through whatever we were loaded with.  This
3990        #    lets us bypass using shell process on Windows systems.
3991        #
3992        set cmd [info nameofexecutable]
3993        lappend cmd [gitexec gitk]
3994        if {$revs ne {}} {
3995                append cmd { }
3996                append cmd $revs
3997        }
3998
3999        if {[catch {eval exec $cmd &} err]} {
4000                error_popup "Failed to start gitk:\n\n$err"
4001        } else {
4002                set ui_status_value $starting_gitk_msg
4003                after 10000 {
4004                        if {$ui_status_value eq $starting_gitk_msg} {
4005                                set ui_status_value {Ready.}
4006                        }
4007                }
4008        }
4009}
4010
4011proc do_stats {} {
4012        set fd [open "| git count-objects -v" r]
4013        while {[gets $fd line] > 0} {
4014                if {[regexp {^([^:]+): (\d+)$} $line _ name value]} {
4015                        set stats($name) $value
4016                }
4017        }
4018        close $fd
4019
4020        set packed_sz 0
4021        foreach p [glob -directory [gitdir objects pack] \
4022                -type f \
4023                -nocomplain -- *] {
4024                incr packed_sz [file size $p]
4025        }
4026        if {$packed_sz > 0} {
4027                set stats(size-pack) [expr {$packed_sz / 1024}]
4028        }
4029
4030        set w .stats_view
4031        toplevel $w
4032        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
4033
4034        label $w.header -text {Database Statistics} \
4035                -font font_uibold
4036        pack $w.header -side top -fill x
4037
4038        frame $w.buttons -border 1
4039        button $w.buttons.close -text Close \
4040                -font font_ui \
4041                -command [list destroy $w]
4042        button $w.buttons.gc -text {Compress Database} \
4043                -font font_ui \
4044                -command "destroy $w;do_gc"
4045        pack $w.buttons.close -side right
4046        pack $w.buttons.gc -side left
4047        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
4048
4049        frame $w.stat -borderwidth 1 -relief solid
4050        foreach s {
4051                {count           {Number of loose objects}}
4052                {size            {Disk space used by loose objects} { KiB}}
4053                {in-pack         {Number of packed objects}}
4054                {packs           {Number of packs}}
4055                {size-pack       {Disk space used by packed objects} { KiB}}
4056                {prune-packable  {Packed objects waiting for pruning}}
4057                {garbage         {Garbage files}}
4058                } {
4059                set name [lindex $s 0]
4060                set label [lindex $s 1]
4061                if {[catch {set value $stats($name)}]} continue
4062                if {[llength $s] > 2} {
4063                        set value "$value[lindex $s 2]"
4064                }
4065
4066                label $w.stat.l_$name -text "$label:" -anchor w -font font_ui
4067                label $w.stat.v_$name -text $value -anchor w -font font_ui
4068                grid $w.stat.l_$name $w.stat.v_$name -sticky we -padx {0 5}
4069        }
4070        pack $w.stat -pady 10 -padx 10
4071
4072        bind $w <Visibility> "grab $w; focus $w"
4073        bind $w <Key-Escape> [list destroy $w]
4074        bind $w <Key-Return> [list destroy $w]
4075        wm title $w "[appname] ([reponame]): Database Statistics"
4076        tkwait window $w
4077}
4078
4079proc do_gc {} {
4080        set w [new_console {gc} {Compressing the object database}]
4081        console_chain {
4082                {console_exec {git pack-refs --prune}}
4083                {console_exec {git reflog expire --all}}
4084                {console_exec {git repack -a -d -l}}
4085                {console_exec {git rerere gc}}
4086        } $w
4087}
4088
4089proc do_fsck_objects {} {
4090        set w [new_console {fsck-objects} \
4091                {Verifying the object database with fsck-objects}]
4092        set cmd [list git fsck-objects]
4093        lappend cmd --full
4094        lappend cmd --cache
4095        lappend cmd --strict
4096        console_exec $w $cmd console_done
4097}
4098
4099set is_quitting 0
4100
4101proc do_quit {} {
4102        global ui_comm is_quitting repo_config commit_type
4103
4104        if {$is_quitting} return
4105        set is_quitting 1
4106
4107        # -- Stash our current commit buffer.
4108        #
4109        set save [gitdir GITGUI_MSG]
4110        set msg [string trim [$ui_comm get 0.0 end]]
4111        regsub -all -line {[ \r\t]+$} $msg {} msg
4112        if {(![string match amend* $commit_type]
4113                || [$ui_comm edit modified])
4114                && $msg ne {}} {
4115                catch {
4116                        set fd [open $save w]
4117                        puts -nonewline $fd $msg
4118                        close $fd
4119                }
4120        } else {
4121                catch {file delete $save}
4122        }
4123
4124        # -- Stash our current window geometry into this repository.
4125        #
4126        set cfg_geometry [list]
4127        lappend cfg_geometry [wm geometry .]
4128        lappend cfg_geometry [lindex [.vpane sash coord 0] 1]
4129        lappend cfg_geometry [lindex [.vpane.files sash coord 0] 0]
4130        if {[catch {set rc_geometry $repo_config(gui.geometry)}]} {
4131                set rc_geometry {}
4132        }
4133        if {$cfg_geometry ne $rc_geometry} {
4134                catch {exec git repo-config gui.geometry $cfg_geometry}
4135        }
4136
4137        destroy .
4138}
4139
4140proc do_rescan {} {
4141        rescan {set ui_status_value {Ready.}}
4142}
4143
4144proc unstage_helper {txt paths} {
4145        global file_states current_diff_path
4146
4147        if {![lock_index begin-update]} return
4148
4149        set pathList [list]
4150        set after {}
4151        foreach path $paths {
4152                switch -glob -- [lindex $file_states($path) 0] {
4153                A? -
4154                M? -
4155                D? {
4156                        lappend pathList $path
4157                        if {$path eq $current_diff_path} {
4158                                set after {reshow_diff;}
4159                        }
4160                }
4161                }
4162        }
4163        if {$pathList eq {}} {
4164                unlock_index
4165        } else {
4166                update_indexinfo \
4167                        $txt \
4168                        $pathList \
4169                        [concat $after {set ui_status_value {Ready.}}]
4170        }
4171}
4172
4173proc do_unstage_selection {} {
4174        global current_diff_path selected_paths
4175
4176        if {[array size selected_paths] > 0} {
4177                unstage_helper \
4178                        {Unstaging selected files from commit} \
4179                        [array names selected_paths]
4180        } elseif {$current_diff_path ne {}} {
4181                unstage_helper \
4182                        "Unstaging [short_path $current_diff_path] from commit" \
4183                        [list $current_diff_path]
4184        }
4185}
4186
4187proc add_helper {txt paths} {
4188        global file_states current_diff_path
4189
4190        if {![lock_index begin-update]} return
4191
4192        set pathList [list]
4193        set after {}
4194        foreach path $paths {
4195                switch -glob -- [lindex $file_states($path) 0] {
4196                _O -
4197                ?M -
4198                ?D -
4199                U? {
4200                        lappend pathList $path
4201                        if {$path eq $current_diff_path} {
4202                                set after {reshow_diff;}
4203                        }
4204                }
4205                }
4206        }
4207        if {$pathList eq {}} {
4208                unlock_index
4209        } else {
4210                update_index \
4211                        $txt \
4212                        $pathList \
4213                        [concat $after {set ui_status_value {Ready to commit.}}]
4214        }
4215}
4216
4217proc do_add_selection {} {
4218        global current_diff_path selected_paths
4219
4220        if {[array size selected_paths] > 0} {
4221                add_helper \
4222                        {Adding selected files} \
4223                        [array names selected_paths]
4224        } elseif {$current_diff_path ne {}} {
4225                add_helper \
4226                        "Adding [short_path $current_diff_path]" \
4227                        [list $current_diff_path]
4228        }
4229}
4230
4231proc do_add_all {} {
4232        global file_states
4233
4234        set paths [list]
4235        foreach path [array names file_states] {
4236                switch -glob -- [lindex $file_states($path) 0] {
4237                U? {continue}
4238                ?M -
4239                ?D {lappend paths $path}
4240                }
4241        }
4242        add_helper {Adding all changed files} $paths
4243}
4244
4245proc revert_helper {txt paths} {
4246        global file_states current_diff_path
4247
4248        if {![lock_index begin-update]} return
4249
4250        set pathList [list]
4251        set after {}
4252        foreach path $paths {
4253                switch -glob -- [lindex $file_states($path) 0] {
4254                U? {continue}
4255                ?M -
4256                ?D {
4257                        lappend pathList $path
4258                        if {$path eq $current_diff_path} {
4259                                set after {reshow_diff;}
4260                        }
4261                }
4262                }
4263        }
4264
4265        set n [llength $pathList]
4266        if {$n == 0} {
4267                unlock_index
4268                return
4269        } elseif {$n == 1} {
4270                set s "[short_path [lindex $pathList]]"
4271        } else {
4272                set s "these $n files"
4273        }
4274
4275        set reply [tk_dialog \
4276                .confirm_revert \
4277                "[appname] ([reponame])" \
4278                "Revert changes in $s?
4279
4280Any unadded changes will be permanently lost by the revert." \
4281                question \
4282                1 \
4283                {Do Nothing} \
4284                {Revert Changes} \
4285                ]
4286        if {$reply == 1} {
4287                checkout_index \
4288                        $txt \
4289                        $pathList \
4290                        [concat $after {set ui_status_value {Ready.}}]
4291        } else {
4292                unlock_index
4293        }
4294}
4295
4296proc do_revert_selection {} {
4297        global current_diff_path selected_paths
4298
4299        if {[array size selected_paths] > 0} {
4300                revert_helper \
4301                        {Reverting selected files} \
4302                        [array names selected_paths]
4303        } elseif {$current_diff_path ne {}} {
4304                revert_helper \
4305                        "Reverting [short_path $current_diff_path]" \
4306                        [list $current_diff_path]
4307        }
4308}
4309
4310proc do_signoff {} {
4311        global ui_comm
4312
4313        set me [committer_ident]
4314        if {$me eq {}} return
4315
4316        set sob "Signed-off-by: $me"
4317        set last [$ui_comm get {end -1c linestart} {end -1c}]
4318        if {$last ne $sob} {
4319                $ui_comm edit separator
4320                if {$last ne {}
4321                        && ![regexp {^[A-Z][A-Za-z]*-[A-Za-z-]+: *} $last]} {
4322                        $ui_comm insert end "\n"
4323                }
4324                $ui_comm insert end "\n$sob"
4325                $ui_comm edit separator
4326                $ui_comm see end
4327        }
4328}
4329
4330proc do_select_commit_type {} {
4331        global commit_type selected_commit_type
4332
4333        if {$selected_commit_type eq {new}
4334                && [string match amend* $commit_type]} {
4335                create_new_commit
4336        } elseif {$selected_commit_type eq {amend}
4337                && ![string match amend* $commit_type]} {
4338                load_last_commit
4339
4340                # The amend request was rejected...
4341                #
4342                if {![string match amend* $commit_type]} {
4343                        set selected_commit_type new
4344                }
4345        }
4346}
4347
4348proc do_commit {} {
4349        commit_tree
4350}
4351
4352proc do_about {} {
4353        global appvers copyright
4354        global tcl_patchLevel tk_patchLevel
4355
4356        set w .about_dialog
4357        toplevel $w
4358        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
4359
4360        label $w.header -text "About [appname]" \
4361                -font font_uibold
4362        pack $w.header -side top -fill x
4363
4364        frame $w.buttons
4365        button $w.buttons.close -text {Close} \
4366                -font font_ui \
4367                -command [list destroy $w]
4368        pack $w.buttons.close -side right
4369        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
4370
4371        label $w.desc \
4372                -text "[appname] - a commit creation tool for Git.
4373$copyright" \
4374                -padx 5 -pady 5 \
4375                -justify left \
4376                -anchor w \
4377                -borderwidth 1 \
4378                -relief solid \
4379                -font font_ui
4380        pack $w.desc -side top -fill x -padx 5 -pady 5
4381
4382        set v {}
4383        append v "[appname] version $appvers\n"
4384        append v "[exec git version]\n"
4385        append v "\n"
4386        if {$tcl_patchLevel eq $tk_patchLevel} {
4387                append v "Tcl/Tk version $tcl_patchLevel"
4388        } else {
4389                append v "Tcl version $tcl_patchLevel"
4390                append v ", Tk version $tk_patchLevel"
4391        }
4392
4393        label $w.vers \
4394                -text $v \
4395                -padx 5 -pady 5 \
4396                -justify left \
4397                -anchor w \
4398                -borderwidth 1 \
4399                -relief solid \
4400                -font font_ui
4401        pack $w.vers -side top -fill x -padx 5 -pady 5
4402
4403        menu $w.ctxm -tearoff 0
4404        $w.ctxm add command \
4405                -label {Copy} \
4406                -font font_ui \
4407                -command "
4408                clipboard clear
4409                clipboard append -format STRING -type STRING -- \[$w.vers cget -text\]
4410        "
4411
4412        bind $w <Visibility> "grab $w; focus $w"
4413        bind $w <Key-Escape> "destroy $w"
4414        bind_button3 $w.vers "tk_popup $w.ctxm %X %Y; grab $w; focus $w"
4415        wm title $w "About [appname]"
4416        tkwait window $w
4417}
4418
4419proc do_options {} {
4420        global repo_config global_config font_descs
4421        global repo_config_new global_config_new
4422
4423        array unset repo_config_new
4424        array unset global_config_new
4425        foreach name [array names repo_config] {
4426                set repo_config_new($name) $repo_config($name)
4427        }
4428        load_config 1
4429        foreach name [array names repo_config] {
4430                switch -- $name {
4431                gui.diffcontext {continue}
4432                }
4433                set repo_config_new($name) $repo_config($name)
4434        }
4435        foreach name [array names global_config] {
4436                set global_config_new($name) $global_config($name)
4437        }
4438
4439        set w .options_editor
4440        toplevel $w
4441        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
4442
4443        label $w.header -text "[appname] Options" \
4444                -font font_uibold
4445        pack $w.header -side top -fill x
4446
4447        frame $w.buttons
4448        button $w.buttons.restore -text {Restore Defaults} \
4449                -font font_ui \
4450                -command do_restore_defaults
4451        pack $w.buttons.restore -side left
4452        button $w.buttons.save -text Save \
4453                -font font_ui \
4454                -command [list do_save_config $w]
4455        pack $w.buttons.save -side right
4456        button $w.buttons.cancel -text {Cancel} \
4457                -font font_ui \
4458                -command [list destroy $w]
4459        pack $w.buttons.cancel -side right -padx 5
4460        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
4461
4462        labelframe $w.repo -text "[reponame] Repository" \
4463                -font font_ui
4464        labelframe $w.global -text {Global (All Repositories)} \
4465                -font font_ui
4466        pack $w.repo -side left -fill both -expand 1 -pady 5 -padx 5
4467        pack $w.global -side right -fill both -expand 1 -pady 5 -padx 5
4468
4469        set optid 0
4470        foreach option {
4471                {t user.name {User Name}}
4472                {t user.email {Email Address}}
4473
4474                {b merge.summary {Summarize Merge Commits}}
4475                {i-1..5 merge.verbosity {Merge Verbosity}}
4476
4477                {b gui.trustmtime  {Trust File Modification Timestamps}}
4478                {i-1..99 gui.diffcontext {Number of Diff Context Lines}}
4479                {t gui.newbranchtemplate {New Branch Name Template}}
4480                } {
4481                set type [lindex $option 0]
4482                set name [lindex $option 1]
4483                set text [lindex $option 2]
4484                incr optid
4485                foreach f {repo global} {
4486                        switch -glob -- $type {
4487                        b {
4488                                checkbutton $w.$f.$optid -text $text \
4489                                        -variable ${f}_config_new($name) \
4490                                        -onvalue true \
4491                                        -offvalue false \
4492                                        -font font_ui
4493                                pack $w.$f.$optid -side top -anchor w
4494                        }
4495                        i-* {
4496                                regexp -- {-(\d+)\.\.(\d+)$} $type _junk min max
4497                                frame $w.$f.$optid
4498                                label $w.$f.$optid.l -text "$text:" -font font_ui
4499                                pack $w.$f.$optid.l -side left -anchor w -fill x
4500                                spinbox $w.$f.$optid.v \
4501                                        -textvariable ${f}_config_new($name) \
4502                                        -from $min \
4503                                        -to $max \
4504                                        -increment 1 \
4505                                        -width [expr {1 + [string length $max]}] \
4506                                        -font font_ui
4507                                bind $w.$f.$optid.v <FocusIn> {%W selection range 0 end}
4508                                pack $w.$f.$optid.v -side right -anchor e -padx 5
4509                                pack $w.$f.$optid -side top -anchor w -fill x
4510                        }
4511                        t {
4512                                frame $w.$f.$optid
4513                                label $w.$f.$optid.l -text "$text:" -font font_ui
4514                                entry $w.$f.$optid.v \
4515                                        -borderwidth 1 \
4516                                        -relief sunken \
4517                                        -width 20 \
4518                                        -textvariable ${f}_config_new($name) \
4519                                        -font font_ui
4520                                pack $w.$f.$optid.l -side left -anchor w
4521                                pack $w.$f.$optid.v -side left -anchor w \
4522                                        -fill x -expand 1 \
4523                                        -padx 5
4524                                pack $w.$f.$optid -side top -anchor w -fill x
4525                        }
4526                        }
4527                }
4528        }
4529
4530        set all_fonts [lsort [font families]]
4531        foreach option $font_descs {
4532                set name [lindex $option 0]
4533                set font [lindex $option 1]
4534                set text [lindex $option 2]
4535
4536                set global_config_new(gui.$font^^family) \
4537                        [font configure $font -family]
4538                set global_config_new(gui.$font^^size) \
4539                        [font configure $font -size]
4540
4541                frame $w.global.$name
4542                label $w.global.$name.l -text "$text:" -font font_ui
4543                pack $w.global.$name.l -side left -anchor w -fill x
4544                eval tk_optionMenu $w.global.$name.family \
4545                        global_config_new(gui.$font^^family) \
4546                        $all_fonts
4547                spinbox $w.global.$name.size \
4548                        -textvariable global_config_new(gui.$font^^size) \
4549                        -from 2 -to 80 -increment 1 \
4550                        -width 3 \
4551                        -font font_ui
4552                bind $w.global.$name.size <FocusIn> {%W selection range 0 end}
4553                pack $w.global.$name.size -side right -anchor e
4554                pack $w.global.$name.family -side right -anchor e
4555                pack $w.global.$name -side top -anchor w -fill x
4556        }
4557
4558        bind $w <Visibility> "grab $w; focus $w"
4559        bind $w <Key-Escape> "destroy $w"
4560        wm title $w "[appname] ([reponame]): Options"
4561        tkwait window $w
4562}
4563
4564proc do_restore_defaults {} {
4565        global font_descs default_config repo_config
4566        global repo_config_new global_config_new
4567
4568        foreach name [array names default_config] {
4569                set repo_config_new($name) $default_config($name)
4570                set global_config_new($name) $default_config($name)
4571        }
4572
4573        foreach option $font_descs {
4574                set name [lindex $option 0]
4575                set repo_config(gui.$name) $default_config(gui.$name)
4576        }
4577        apply_config
4578
4579        foreach option $font_descs {
4580                set name [lindex $option 0]
4581                set font [lindex $option 1]
4582                set global_config_new(gui.$font^^family) \
4583                        [font configure $font -family]
4584                set global_config_new(gui.$font^^size) \
4585                        [font configure $font -size]
4586        }
4587}
4588
4589proc do_save_config {w} {
4590        if {[catch {save_config} err]} {
4591                error_popup "Failed to completely save options:\n\n$err"
4592        }
4593        reshow_diff
4594        destroy $w
4595}
4596
4597proc do_windows_shortcut {} {
4598        global argv0
4599
4600        set fn [tk_getSaveFile \
4601                -parent . \
4602                -title "[appname] ([reponame]): Create Desktop Icon" \
4603                -initialfile "Git [reponame].bat"]
4604        if {$fn != {}} {
4605                if {[catch {
4606                                set fd [open $fn w]
4607                                puts $fd "@ECHO Entering [reponame]"
4608                                puts $fd "@ECHO Starting git-gui... please wait..."
4609                                puts $fd "@SET PATH=[file normalize [gitexec]];%PATH%"
4610                                puts $fd "@SET GIT_DIR=[file normalize [gitdir]]"
4611                                puts -nonewline $fd "@\"[info nameofexecutable]\""
4612                                puts $fd " \"[file normalize $argv0]\""
4613                                close $fd
4614                        } err]} {
4615                        error_popup "Cannot write script:\n\n$err"
4616                }
4617        }
4618}
4619
4620proc do_cygwin_shortcut {} {
4621        global argv0
4622
4623        if {[catch {
4624                set desktop [exec cygpath \
4625                        --windows \
4626                        --absolute \
4627                        --long-name \
4628                        --desktop]
4629                }]} {
4630                        set desktop .
4631        }
4632        set fn [tk_getSaveFile \
4633                -parent . \
4634                -title "[appname] ([reponame]): Create Desktop Icon" \
4635                -initialdir $desktop \
4636                -initialfile "Git [reponame].bat"]
4637        if {$fn != {}} {
4638                if {[catch {
4639                                set fd [open $fn w]
4640                                set sh [exec cygpath \
4641                                        --windows \
4642                                        --absolute \
4643                                        /bin/sh]
4644                                set me [exec cygpath \
4645                                        --unix \
4646                                        --absolute \
4647                                        $argv0]
4648                                set gd [exec cygpath \
4649                                        --unix \
4650                                        --absolute \
4651                                        [gitdir]]
4652                                set gw [exec cygpath \
4653                                        --windows \
4654                                        --absolute \
4655                                        [file dirname [gitdir]]]
4656                                regsub -all ' $me "'\\''" me
4657                                regsub -all ' $gd "'\\''" gd
4658                                puts $fd "@ECHO Entering $gw"
4659                                puts $fd "@ECHO Starting git-gui... please wait..."
4660                                puts -nonewline $fd "@\"$sh\" --login -c \""
4661                                puts -nonewline $fd "GIT_DIR='$gd'"
4662                                puts -nonewline $fd " '$me'"
4663                                puts $fd "&\""
4664                                close $fd
4665                        } err]} {
4666                        error_popup "Cannot write script:\n\n$err"
4667                }
4668        }
4669}
4670
4671proc do_macosx_app {} {
4672        global argv0 env
4673
4674        set fn [tk_getSaveFile \
4675                -parent . \
4676                -title "[appname] ([reponame]): Create Desktop Icon" \
4677                -initialdir [file join $env(HOME) Desktop] \
4678                -initialfile "Git [reponame].app"]
4679        if {$fn != {}} {
4680                if {[catch {
4681                                set Contents [file join $fn Contents]
4682                                set MacOS [file join $Contents MacOS]
4683                                set exe [file join $MacOS git-gui]
4684
4685                                file mkdir $MacOS
4686
4687                                set fd [open [file join $Contents Info.plist] w]
4688                                puts $fd {<?xml version="1.0" encoding="UTF-8"?>
4689<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4690<plist version="1.0">
4691<dict>
4692        <key>CFBundleDevelopmentRegion</key>
4693        <string>English</string>
4694        <key>CFBundleExecutable</key>
4695        <string>git-gui</string>
4696        <key>CFBundleIdentifier</key>
4697        <string>org.spearce.git-gui</string>
4698        <key>CFBundleInfoDictionaryVersion</key>
4699        <string>6.0</string>
4700        <key>CFBundlePackageType</key>
4701        <string>APPL</string>
4702        <key>CFBundleSignature</key>
4703        <string>????</string>
4704        <key>CFBundleVersion</key>
4705        <string>1.0</string>
4706        <key>NSPrincipalClass</key>
4707        <string>NSApplication</string>
4708</dict>
4709</plist>}
4710                                close $fd
4711
4712                                set fd [open $exe w]
4713                                set gd [file normalize [gitdir]]
4714                                set ep [file normalize [gitexec]]
4715                                regsub -all ' $gd "'\\''" gd
4716                                regsub -all ' $ep "'\\''" ep
4717                                puts $fd "#!/bin/sh"
4718                                foreach name [array names env] {
4719                                        if {[string match GIT_* $name]} {
4720                                                regsub -all ' $env($name) "'\\''" v
4721                                                puts $fd "export $name='$v'"
4722                                        }
4723                                }
4724                                puts $fd "export PATH='$ep':\$PATH"
4725                                puts $fd "export GIT_DIR='$gd'"
4726                                puts $fd "exec [file normalize $argv0]"
4727                                close $fd
4728
4729                                file attributes $exe -permissions u+x,g+x,o+x
4730                        } err]} {
4731                        error_popup "Cannot write icon:\n\n$err"
4732                }
4733        }
4734}
4735
4736proc toggle_or_diff {w x y} {
4737        global file_states file_lists current_diff_path ui_index ui_workdir
4738        global last_clicked selected_paths
4739
4740        set pos [split [$w index @$x,$y] .]
4741        set lno [lindex $pos 0]
4742        set col [lindex $pos 1]
4743        set path [lindex $file_lists($w) [expr {$lno - 1}]]
4744        if {$path eq {}} {
4745                set last_clicked {}
4746                return
4747        }
4748
4749        set last_clicked [list $w $lno]
4750        array unset selected_paths
4751        $ui_index tag remove in_sel 0.0 end
4752        $ui_workdir tag remove in_sel 0.0 end
4753
4754        if {$col == 0} {
4755                if {$current_diff_path eq $path} {
4756                        set after {reshow_diff;}
4757                } else {
4758                        set after {}
4759                }
4760                if {$w eq $ui_index} {
4761                        update_indexinfo \
4762                                "Unstaging [short_path $path] from commit" \
4763                                [list $path] \
4764                                [concat $after {set ui_status_value {Ready.}}]
4765                } elseif {$w eq $ui_workdir} {
4766                        update_index \
4767                                "Adding [short_path $path]" \
4768                                [list $path] \
4769                                [concat $after {set ui_status_value {Ready.}}]
4770                }
4771        } else {
4772                show_diff $path $w $lno
4773        }
4774}
4775
4776proc add_one_to_selection {w x y} {
4777        global file_lists last_clicked selected_paths
4778
4779        set lno [lindex [split [$w index @$x,$y] .] 0]
4780        set path [lindex $file_lists($w) [expr {$lno - 1}]]
4781        if {$path eq {}} {
4782                set last_clicked {}
4783                return
4784        }
4785
4786        if {$last_clicked ne {}
4787                && [lindex $last_clicked 0] ne $w} {
4788                array unset selected_paths
4789                [lindex $last_clicked 0] tag remove in_sel 0.0 end
4790        }
4791
4792        set last_clicked [list $w $lno]
4793        if {[catch {set in_sel $selected_paths($path)}]} {
4794                set in_sel 0
4795        }
4796        if {$in_sel} {
4797                unset selected_paths($path)
4798                $w tag remove in_sel $lno.0 [expr {$lno + 1}].0
4799        } else {
4800                set selected_paths($path) 1
4801                $w tag add in_sel $lno.0 [expr {$lno + 1}].0
4802        }
4803}
4804
4805proc add_range_to_selection {w x y} {
4806        global file_lists last_clicked selected_paths
4807
4808        if {[lindex $last_clicked 0] ne $w} {
4809                toggle_or_diff $w $x $y
4810                return
4811        }
4812
4813        set lno [lindex [split [$w index @$x,$y] .] 0]
4814        set lc [lindex $last_clicked 1]
4815        if {$lc < $lno} {
4816                set begin $lc
4817                set end $lno
4818        } else {
4819                set begin $lno
4820                set end $lc
4821        }
4822
4823        foreach path [lrange $file_lists($w) \
4824                [expr {$begin - 1}] \
4825                [expr {$end - 1}]] {
4826                set selected_paths($path) 1
4827        }
4828        $w tag add in_sel $begin.0 [expr {$end + 1}].0
4829}
4830
4831######################################################################
4832##
4833## config defaults
4834
4835set cursor_ptr arrow
4836font create font_diff -family Courier -size 10
4837font create font_ui
4838catch {
4839        label .dummy
4840        eval font configure font_ui [font actual [.dummy cget -font]]
4841        destroy .dummy
4842}
4843
4844font create font_uibold
4845font create font_diffbold
4846
4847if {[is_Windows]} {
4848        set M1B Control
4849        set M1T Ctrl
4850} elseif {[is_MacOSX]} {
4851        set M1B M1
4852        set M1T Cmd
4853} else {
4854        set M1B M1
4855        set M1T M1
4856}
4857
4858proc apply_config {} {
4859        global repo_config font_descs
4860
4861        foreach option $font_descs {
4862                set name [lindex $option 0]
4863                set font [lindex $option 1]
4864                if {[catch {
4865                        foreach {cn cv} $repo_config(gui.$name) {
4866                                font configure $font $cn $cv
4867                        }
4868                        } err]} {
4869                        error_popup "Invalid font specified in gui.$name:\n\n$err"
4870                }
4871                foreach {cn cv} [font configure $font] {
4872                        font configure ${font}bold $cn $cv
4873                }
4874                font configure ${font}bold -weight bold
4875        }
4876}
4877
4878set default_config(merge.summary) false
4879set default_config(merge.verbosity) 2
4880set default_config(user.name) {}
4881set default_config(user.email) {}
4882
4883set default_config(gui.trustmtime) false
4884set default_config(gui.diffcontext) 5
4885set default_config(gui.newbranchtemplate) {}
4886set default_config(gui.fontui) [font configure font_ui]
4887set default_config(gui.fontdiff) [font configure font_diff]
4888set font_descs {
4889        {fontui   font_ui   {Main Font}}
4890        {fontdiff font_diff {Diff/Console Font}}
4891}
4892load_config 0
4893apply_config
4894
4895######################################################################
4896##
4897## ui construction
4898
4899# -- Menu Bar
4900#
4901menu .mbar -tearoff 0
4902.mbar add cascade -label Repository -menu .mbar.repository
4903.mbar add cascade -label Edit -menu .mbar.edit
4904if {!$single_commit} {
4905        .mbar add cascade -label Branch -menu .mbar.branch
4906}
4907.mbar add cascade -label Commit -menu .mbar.commit
4908if {!$single_commit} {
4909        .mbar add cascade -label Merge -menu .mbar.merge
4910        .mbar add cascade -label Fetch -menu .mbar.fetch
4911        .mbar add cascade -label Push -menu .mbar.push
4912}
4913. configure -menu .mbar
4914
4915# -- Repository Menu
4916#
4917menu .mbar.repository
4918
4919.mbar.repository add command \
4920        -label {Browse Current Branch} \
4921        -command {new_browser $current_branch} \
4922        -font font_ui
4923.mbar.repository add separator
4924
4925.mbar.repository add command \
4926        -label {Visualize Current Branch} \
4927        -command {do_gitk {}} \
4928        -font font_ui
4929.mbar.repository add command \
4930        -label {Visualize All Branches} \
4931        -command {do_gitk {--all}} \
4932        -font font_ui
4933.mbar.repository add separator
4934
4935if {!$single_commit} {
4936        .mbar.repository add command -label {Database Statistics} \
4937                -command do_stats \
4938                -font font_ui
4939
4940        .mbar.repository add command -label {Compress Database} \
4941                -command do_gc \
4942                -font font_ui
4943
4944        .mbar.repository add command -label {Verify Database} \
4945                -command do_fsck_objects \
4946                -font font_ui
4947
4948        .mbar.repository add separator
4949
4950        if {[is_Cygwin]} {
4951                .mbar.repository add command \
4952                        -label {Create Desktop Icon} \
4953                        -command do_cygwin_shortcut \
4954                        -font font_ui
4955        } elseif {[is_Windows]} {
4956                .mbar.repository add command \
4957                        -label {Create Desktop Icon} \
4958                        -command do_windows_shortcut \
4959                        -font font_ui
4960        } elseif {[is_MacOSX]} {
4961                .mbar.repository add command \
4962                        -label {Create Desktop Icon} \
4963                        -command do_macosx_app \
4964                        -font font_ui
4965        }
4966}
4967
4968.mbar.repository add command -label Quit \
4969        -command do_quit \
4970        -accelerator $M1T-Q \
4971        -font font_ui
4972
4973# -- Edit Menu
4974#
4975menu .mbar.edit
4976.mbar.edit add command -label Undo \
4977        -command {catch {[focus] edit undo}} \
4978        -accelerator $M1T-Z \
4979        -font font_ui
4980.mbar.edit add command -label Redo \
4981        -command {catch {[focus] edit redo}} \
4982        -accelerator $M1T-Y \
4983        -font font_ui
4984.mbar.edit add separator
4985.mbar.edit add command -label Cut \
4986        -command {catch {tk_textCut [focus]}} \
4987        -accelerator $M1T-X \
4988        -font font_ui
4989.mbar.edit add command -label Copy \
4990        -command {catch {tk_textCopy [focus]}} \
4991        -accelerator $M1T-C \
4992        -font font_ui
4993.mbar.edit add command -label Paste \
4994        -command {catch {tk_textPaste [focus]; [focus] see insert}} \
4995        -accelerator $M1T-V \
4996        -font font_ui
4997.mbar.edit add command -label Delete \
4998        -command {catch {[focus] delete sel.first sel.last}} \
4999        -accelerator Del \
5000        -font font_ui
5001.mbar.edit add separator
5002.mbar.edit add command -label {Select All} \
5003        -command {catch {[focus] tag add sel 0.0 end}} \
5004        -accelerator $M1T-A \
5005        -font font_ui
5006
5007# -- Branch Menu
5008#
5009if {!$single_commit} {
5010        menu .mbar.branch
5011
5012        .mbar.branch add command -label {Create...} \
5013                -command do_create_branch \
5014                -accelerator $M1T-N \
5015                -font font_ui
5016        lappend disable_on_lock [list .mbar.branch entryconf \
5017                [.mbar.branch index last] -state]
5018
5019        .mbar.branch add command -label {Delete...} \
5020                -command do_delete_branch \
5021                -font font_ui
5022        lappend disable_on_lock [list .mbar.branch entryconf \
5023                [.mbar.branch index last] -state]
5024}
5025
5026# -- Commit Menu
5027#
5028menu .mbar.commit
5029
5030.mbar.commit add radiobutton \
5031        -label {New Commit} \
5032        -command do_select_commit_type \
5033        -variable selected_commit_type \
5034        -value new \
5035        -font font_ui
5036lappend disable_on_lock \
5037        [list .mbar.commit entryconf [.mbar.commit index last] -state]
5038
5039.mbar.commit add radiobutton \
5040        -label {Amend Last Commit} \
5041        -command do_select_commit_type \
5042        -variable selected_commit_type \
5043        -value amend \
5044        -font font_ui
5045lappend disable_on_lock \
5046        [list .mbar.commit entryconf [.mbar.commit index last] -state]
5047
5048.mbar.commit add separator
5049
5050.mbar.commit add command -label Rescan \
5051        -command do_rescan \
5052        -accelerator F5 \
5053        -font font_ui
5054lappend disable_on_lock \
5055        [list .mbar.commit entryconf [.mbar.commit index last] -state]
5056
5057.mbar.commit add command -label {Add To Commit} \
5058        -command do_add_selection \
5059        -font font_ui
5060lappend disable_on_lock \
5061        [list .mbar.commit entryconf [.mbar.commit index last] -state]
5062
5063.mbar.commit add command -label {Add All To Commit} \
5064        -command do_add_all \
5065        -accelerator $M1T-I \
5066        -font font_ui
5067lappend disable_on_lock \
5068        [list .mbar.commit entryconf [.mbar.commit index last] -state]
5069
5070.mbar.commit add command -label {Unstage From Commit} \
5071        -command do_unstage_selection \
5072        -font font_ui
5073lappend disable_on_lock \
5074        [list .mbar.commit entryconf [.mbar.commit index last] -state]
5075
5076.mbar.commit add command -label {Revert Changes} \
5077        -command do_revert_selection \
5078        -font font_ui
5079lappend disable_on_lock \
5080        [list .mbar.commit entryconf [.mbar.commit index last] -state]
5081
5082.mbar.commit add separator
5083
5084.mbar.commit add command -label {Sign Off} \
5085        -command do_signoff \
5086        -accelerator $M1T-S \
5087        -font font_ui
5088
5089.mbar.commit add command -label Commit \
5090        -command do_commit \
5091        -accelerator $M1T-Return \
5092        -font font_ui
5093lappend disable_on_lock \
5094        [list .mbar.commit entryconf [.mbar.commit index last] -state]
5095
5096if {[is_MacOSX]} {
5097        # -- Apple Menu (Mac OS X only)
5098        #
5099        .mbar add cascade -label Apple -menu .mbar.apple
5100        menu .mbar.apple
5101
5102        .mbar.apple add command -label "About [appname]" \
5103                -command do_about \
5104                -font font_ui
5105        .mbar.apple add command -label "[appname] Options..." \
5106                -command do_options \
5107                -font font_ui
5108} else {
5109        # -- Edit Menu
5110        #
5111        .mbar.edit add separator
5112        .mbar.edit add command -label {Options...} \
5113                -command do_options \
5114                -font font_ui
5115
5116        # -- Tools Menu
5117        #
5118        if {[file exists /usr/local/miga/lib/gui-miga]
5119                && [file exists .pvcsrc]} {
5120        proc do_miga {} {
5121                global ui_status_value
5122                if {![lock_index update]} return
5123                set cmd [list sh --login -c "/usr/local/miga/lib/gui-miga \"[pwd]\""]
5124                set miga_fd [open "|$cmd" r]
5125                fconfigure $miga_fd -blocking 0
5126                fileevent $miga_fd readable [list miga_done $miga_fd]
5127                set ui_status_value {Running miga...}
5128        }
5129        proc miga_done {fd} {
5130                read $fd 512
5131                if {[eof $fd]} {
5132                        close $fd
5133                        unlock_index
5134                        rescan [list set ui_status_value {Ready.}]
5135                }
5136        }
5137        .mbar add cascade -label Tools -menu .mbar.tools
5138        menu .mbar.tools
5139        .mbar.tools add command -label "Migrate" \
5140                -command do_miga \
5141                -font font_ui
5142        lappend disable_on_lock \
5143                [list .mbar.tools entryconf [.mbar.tools index last] -state]
5144        }
5145}
5146
5147# -- Help Menu
5148#
5149.mbar add cascade -label Help -menu .mbar.help
5150menu .mbar.help
5151
5152if {![is_MacOSX]} {
5153        .mbar.help add command -label "About [appname]" \
5154                -command do_about \
5155                -font font_ui
5156}
5157
5158set browser {}
5159catch {set browser $repo_config(instaweb.browser)}
5160set doc_path [file dirname [gitexec]]
5161set doc_path [file join $doc_path Documentation index.html]
5162
5163if {[is_Cygwin]} {
5164        set doc_path [exec cygpath --windows $doc_path]
5165}
5166
5167if {$browser eq {}} {
5168        if {[is_MacOSX]} {
5169                set browser open
5170        } elseif {[is_Cygwin]} {
5171                set program_files [file dirname [exec cygpath --windir]]
5172                set program_files [file join $program_files {Program Files}]
5173                set firefox [file join $program_files {Mozilla Firefox} firefox.exe]
5174                set ie [file join $program_files {Internet Explorer} IEXPLORE.EXE]
5175                if {[file exists $firefox]} {
5176                        set browser $firefox
5177                } elseif {[file exists $ie]} {
5178                        set browser $ie
5179                }
5180                unset program_files firefox ie
5181        }
5182}
5183
5184if {[file isfile $doc_path]} {
5185        set doc_url "file:$doc_path"
5186} else {
5187        set doc_url {http://www.kernel.org/pub/software/scm/git/docs/}
5188}
5189
5190if {$browser ne {}} {
5191        .mbar.help add command -label {Online Documentation} \
5192                -command [list exec $browser $doc_url &] \
5193                -font font_ui
5194}
5195unset browser doc_path doc_url
5196
5197# -- Branch Control
5198#
5199frame .branch \
5200        -borderwidth 1 \
5201        -relief sunken
5202label .branch.l1 \
5203        -text {Current Branch:} \
5204        -anchor w \
5205        -justify left \
5206        -font font_ui
5207label .branch.cb \
5208        -textvariable current_branch \
5209        -anchor w \
5210        -justify left \
5211        -font font_ui
5212pack .branch.l1 -side left
5213pack .branch.cb -side left -fill x
5214pack .branch -side top -fill x
5215
5216if {!$single_commit} {
5217        menu .mbar.merge
5218        .mbar.merge add command -label {Local Merge...} \
5219                -command do_local_merge \
5220                -font font_ui
5221        lappend disable_on_lock \
5222                [list .mbar.merge entryconf [.mbar.merge index last] -state]
5223        .mbar.merge add command -label {Abort Merge...} \
5224                -command do_reset_hard \
5225                -font font_ui
5226        lappend disable_on_lock \
5227                [list .mbar.merge entryconf [.mbar.merge index last] -state]
5228
5229
5230        menu .mbar.fetch
5231
5232        menu .mbar.push
5233        .mbar.push add command -label {Push...} \
5234                -command do_push_anywhere \
5235                -font font_ui
5236}
5237
5238# -- Main Window Layout
5239#
5240panedwindow .vpane -orient vertical
5241panedwindow .vpane.files -orient horizontal
5242.vpane add .vpane.files -sticky nsew -height 100 -width 200
5243pack .vpane -anchor n -side top -fill both -expand 1
5244
5245# -- Index File List
5246#
5247frame .vpane.files.index -height 100 -width 200
5248label .vpane.files.index.title -text {Changes To Be Committed} \
5249        -background green \
5250        -font font_ui
5251text $ui_index -background white -borderwidth 0 \
5252        -width 20 -height 10 \
5253        -wrap none \
5254        -font font_ui \
5255        -cursor $cursor_ptr \
5256        -xscrollcommand {.vpane.files.index.sx set} \
5257        -yscrollcommand {.vpane.files.index.sy set} \
5258        -state disabled
5259scrollbar .vpane.files.index.sx -orient h -command [list $ui_index xview]
5260scrollbar .vpane.files.index.sy -orient v -command [list $ui_index yview]
5261pack .vpane.files.index.title -side top -fill x
5262pack .vpane.files.index.sx -side bottom -fill x
5263pack .vpane.files.index.sy -side right -fill y
5264pack $ui_index -side left -fill both -expand 1
5265.vpane.files add .vpane.files.index -sticky nsew
5266
5267# -- Working Directory File List
5268#
5269frame .vpane.files.workdir -height 100 -width 200
5270label .vpane.files.workdir.title -text {Changed But Not Updated} \
5271        -background red \
5272        -font font_ui
5273text $ui_workdir -background white -borderwidth 0 \
5274        -width 20 -height 10 \
5275        -wrap none \
5276        -font font_ui \
5277        -cursor $cursor_ptr \
5278        -xscrollcommand {.vpane.files.workdir.sx set} \
5279        -yscrollcommand {.vpane.files.workdir.sy set} \
5280        -state disabled
5281scrollbar .vpane.files.workdir.sx -orient h -command [list $ui_workdir xview]
5282scrollbar .vpane.files.workdir.sy -orient v -command [list $ui_workdir yview]
5283pack .vpane.files.workdir.title -side top -fill x
5284pack .vpane.files.workdir.sx -side bottom -fill x
5285pack .vpane.files.workdir.sy -side right -fill y
5286pack $ui_workdir -side left -fill both -expand 1
5287.vpane.files add .vpane.files.workdir -sticky nsew
5288
5289foreach i [list $ui_index $ui_workdir] {
5290        $i tag conf in_diff -font font_uibold
5291        $i tag conf in_sel \
5292                -background [$i cget -foreground] \
5293                -foreground [$i cget -background]
5294}
5295unset i
5296
5297# -- Diff and Commit Area
5298#
5299frame .vpane.lower -height 300 -width 400
5300frame .vpane.lower.commarea
5301frame .vpane.lower.diff -relief sunken -borderwidth 1
5302pack .vpane.lower.commarea -side top -fill x
5303pack .vpane.lower.diff -side bottom -fill both -expand 1
5304.vpane add .vpane.lower -sticky nsew
5305
5306# -- Commit Area Buttons
5307#
5308frame .vpane.lower.commarea.buttons
5309label .vpane.lower.commarea.buttons.l -text {} \
5310        -anchor w \
5311        -justify left \
5312        -font font_ui
5313pack .vpane.lower.commarea.buttons.l -side top -fill x
5314pack .vpane.lower.commarea.buttons -side left -fill y
5315
5316button .vpane.lower.commarea.buttons.rescan -text {Rescan} \
5317        -command do_rescan \
5318        -font font_ui
5319pack .vpane.lower.commarea.buttons.rescan -side top -fill x
5320lappend disable_on_lock \
5321        {.vpane.lower.commarea.buttons.rescan conf -state}
5322
5323button .vpane.lower.commarea.buttons.incall -text {Add All} \
5324        -command do_add_all \
5325        -font font_ui
5326pack .vpane.lower.commarea.buttons.incall -side top -fill x
5327lappend disable_on_lock \
5328        {.vpane.lower.commarea.buttons.incall conf -state}
5329
5330button .vpane.lower.commarea.buttons.signoff -text {Sign Off} \
5331        -command do_signoff \
5332        -font font_ui
5333pack .vpane.lower.commarea.buttons.signoff -side top -fill x
5334
5335button .vpane.lower.commarea.buttons.commit -text {Commit} \
5336        -command do_commit \
5337        -font font_ui
5338pack .vpane.lower.commarea.buttons.commit -side top -fill x
5339lappend disable_on_lock \
5340        {.vpane.lower.commarea.buttons.commit conf -state}
5341
5342# -- Commit Message Buffer
5343#
5344frame .vpane.lower.commarea.buffer
5345frame .vpane.lower.commarea.buffer.header
5346set ui_comm .vpane.lower.commarea.buffer.t
5347set ui_coml .vpane.lower.commarea.buffer.header.l
5348radiobutton .vpane.lower.commarea.buffer.header.new \
5349        -text {New Commit} \
5350        -command do_select_commit_type \
5351        -variable selected_commit_type \
5352        -value new \
5353        -font font_ui
5354lappend disable_on_lock \
5355        [list .vpane.lower.commarea.buffer.header.new conf -state]
5356radiobutton .vpane.lower.commarea.buffer.header.amend \
5357        -text {Amend Last Commit} \
5358        -command do_select_commit_type \
5359        -variable selected_commit_type \
5360        -value amend \
5361        -font font_ui
5362lappend disable_on_lock \
5363        [list .vpane.lower.commarea.buffer.header.amend conf -state]
5364label $ui_coml \
5365        -anchor w \
5366        -justify left \
5367        -font font_ui
5368proc trace_commit_type {varname args} {
5369        global ui_coml commit_type
5370        switch -glob -- $commit_type {
5371        initial       {set txt {Initial Commit Message:}}
5372        amend         {set txt {Amended Commit Message:}}
5373        amend-initial {set txt {Amended Initial Commit Message:}}
5374        amend-merge   {set txt {Amended Merge Commit Message:}}
5375        merge         {set txt {Merge Commit Message:}}
5376        *             {set txt {Commit Message:}}
5377        }
5378        $ui_coml conf -text $txt
5379}
5380trace add variable commit_type write trace_commit_type
5381pack $ui_coml -side left -fill x
5382pack .vpane.lower.commarea.buffer.header.amend -side right
5383pack .vpane.lower.commarea.buffer.header.new -side right
5384
5385text $ui_comm -background white -borderwidth 1 \
5386        -undo true \
5387        -maxundo 20 \
5388        -autoseparators true \
5389        -relief sunken \
5390        -width 75 -height 9 -wrap none \
5391        -font font_diff \
5392        -yscrollcommand {.vpane.lower.commarea.buffer.sby set}
5393scrollbar .vpane.lower.commarea.buffer.sby \
5394        -command [list $ui_comm yview]
5395pack .vpane.lower.commarea.buffer.header -side top -fill x
5396pack .vpane.lower.commarea.buffer.sby -side right -fill y
5397pack $ui_comm -side left -fill y
5398pack .vpane.lower.commarea.buffer -side left -fill y
5399
5400# -- Commit Message Buffer Context Menu
5401#
5402set ctxm .vpane.lower.commarea.buffer.ctxm
5403menu $ctxm -tearoff 0
5404$ctxm add command \
5405        -label {Cut} \
5406        -font font_ui \
5407        -command {tk_textCut $ui_comm}
5408$ctxm add command \
5409        -label {Copy} \
5410        -font font_ui \
5411        -command {tk_textCopy $ui_comm}
5412$ctxm add command \
5413        -label {Paste} \
5414        -font font_ui \
5415        -command {tk_textPaste $ui_comm}
5416$ctxm add command \
5417        -label {Delete} \
5418        -font font_ui \
5419        -command {$ui_comm delete sel.first sel.last}
5420$ctxm add separator
5421$ctxm add command \
5422        -label {Select All} \
5423        -font font_ui \
5424        -command {focus $ui_comm;$ui_comm tag add sel 0.0 end}
5425$ctxm add command \
5426        -label {Copy All} \
5427        -font font_ui \
5428        -command {
5429                $ui_comm tag add sel 0.0 end
5430                tk_textCopy $ui_comm
5431                $ui_comm tag remove sel 0.0 end
5432        }
5433$ctxm add separator
5434$ctxm add command \
5435        -label {Sign Off} \
5436        -font font_ui \
5437        -command do_signoff
5438bind_button3 $ui_comm "tk_popup $ctxm %X %Y"
5439
5440# -- Diff Header
5441#
5442set current_diff_path {}
5443set current_diff_side {}
5444set diff_actions [list]
5445proc trace_current_diff_path {varname args} {
5446        global current_diff_path diff_actions file_states
5447        if {$current_diff_path eq {}} {
5448                set s {}
5449                set f {}
5450                set p {}
5451                set o disabled
5452        } else {
5453                set p $current_diff_path
5454                set s [mapdesc [lindex $file_states($p) 0] $p]
5455                set f {File:}
5456                set p [escape_path $p]
5457                set o normal
5458        }
5459
5460        .vpane.lower.diff.header.status configure -text $s
5461        .vpane.lower.diff.header.file configure -text $f
5462        .vpane.lower.diff.header.path configure -text $p
5463        foreach w $diff_actions {
5464                uplevel #0 $w $o
5465        }
5466}
5467trace add variable current_diff_path write trace_current_diff_path
5468
5469frame .vpane.lower.diff.header -background orange
5470label .vpane.lower.diff.header.status \
5471        -background orange \
5472        -width $max_status_desc \
5473        -anchor w \
5474        -justify left \
5475        -font font_ui
5476label .vpane.lower.diff.header.file \
5477        -background orange \
5478        -anchor w \
5479        -justify left \
5480        -font font_ui
5481label .vpane.lower.diff.header.path \
5482        -background orange \
5483        -anchor w \
5484        -justify left \
5485        -font font_ui
5486pack .vpane.lower.diff.header.status -side left
5487pack .vpane.lower.diff.header.file -side left
5488pack .vpane.lower.diff.header.path -fill x
5489set ctxm .vpane.lower.diff.header.ctxm
5490menu $ctxm -tearoff 0
5491$ctxm add command \
5492        -label {Copy} \
5493        -font font_ui \
5494        -command {
5495                clipboard clear
5496                clipboard append \
5497                        -format STRING \
5498                        -type STRING \
5499                        -- $current_diff_path
5500        }
5501lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5502bind_button3 .vpane.lower.diff.header.path "tk_popup $ctxm %X %Y"
5503
5504# -- Diff Body
5505#
5506frame .vpane.lower.diff.body
5507set ui_diff .vpane.lower.diff.body.t
5508text $ui_diff -background white -borderwidth 0 \
5509        -width 80 -height 15 -wrap none \
5510        -font font_diff \
5511        -xscrollcommand {.vpane.lower.diff.body.sbx set} \
5512        -yscrollcommand {.vpane.lower.diff.body.sby set} \
5513        -state disabled
5514scrollbar .vpane.lower.diff.body.sbx -orient horizontal \
5515        -command [list $ui_diff xview]
5516scrollbar .vpane.lower.diff.body.sby -orient vertical \
5517        -command [list $ui_diff yview]
5518pack .vpane.lower.diff.body.sbx -side bottom -fill x
5519pack .vpane.lower.diff.body.sby -side right -fill y
5520pack $ui_diff -side left -fill both -expand 1
5521pack .vpane.lower.diff.header -side top -fill x
5522pack .vpane.lower.diff.body -side bottom -fill both -expand 1
5523
5524$ui_diff tag conf d_cr -elide true
5525$ui_diff tag conf d_@ -foreground blue -font font_diffbold
5526$ui_diff tag conf d_+ -foreground {#00a000}
5527$ui_diff tag conf d_- -foreground red
5528
5529$ui_diff tag conf d_++ -foreground {#00a000}
5530$ui_diff tag conf d_-- -foreground red
5531$ui_diff tag conf d_+s \
5532        -foreground {#00a000} \
5533        -background {#e2effa}
5534$ui_diff tag conf d_-s \
5535        -foreground red \
5536        -background {#e2effa}
5537$ui_diff tag conf d_s+ \
5538        -foreground {#00a000} \
5539        -background ivory1
5540$ui_diff tag conf d_s- \
5541        -foreground red \
5542        -background ivory1
5543
5544$ui_diff tag conf d<<<<<<< \
5545        -foreground orange \
5546        -font font_diffbold
5547$ui_diff tag conf d======= \
5548        -foreground orange \
5549        -font font_diffbold
5550$ui_diff tag conf d>>>>>>> \
5551        -foreground orange \
5552        -font font_diffbold
5553
5554$ui_diff tag raise sel
5555
5556# -- Diff Body Context Menu
5557#
5558set ctxm .vpane.lower.diff.body.ctxm
5559menu $ctxm -tearoff 0
5560$ctxm add command \
5561        -label {Refresh} \
5562        -font font_ui \
5563        -command reshow_diff
5564lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5565$ctxm add command \
5566        -label {Copy} \
5567        -font font_ui \
5568        -command {tk_textCopy $ui_diff}
5569lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5570$ctxm add command \
5571        -label {Select All} \
5572        -font font_ui \
5573        -command {focus $ui_diff;$ui_diff tag add sel 0.0 end}
5574lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5575$ctxm add command \
5576        -label {Copy All} \
5577        -font font_ui \
5578        -command {
5579                $ui_diff tag add sel 0.0 end
5580                tk_textCopy $ui_diff
5581                $ui_diff tag remove sel 0.0 end
5582        }
5583lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5584$ctxm add separator
5585$ctxm add command \
5586        -label {Apply/Reverse Hunk} \
5587        -font font_ui \
5588        -command {apply_hunk $cursorX $cursorY}
5589set ui_diff_applyhunk [$ctxm index last]
5590lappend diff_actions [list $ctxm entryconf $ui_diff_applyhunk -state]
5591$ctxm add separator
5592$ctxm add command \
5593        -label {Decrease Font Size} \
5594        -font font_ui \
5595        -command {incr_font_size font_diff -1}
5596lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5597$ctxm add command \
5598        -label {Increase Font Size} \
5599        -font font_ui \
5600        -command {incr_font_size font_diff 1}
5601lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5602$ctxm add separator
5603$ctxm add command \
5604        -label {Show Less Context} \
5605        -font font_ui \
5606        -command {if {$repo_config(gui.diffcontext) >= 2} {
5607                incr repo_config(gui.diffcontext) -1
5608                reshow_diff
5609        }}
5610lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5611$ctxm add command \
5612        -label {Show More Context} \
5613        -font font_ui \
5614        -command {
5615                incr repo_config(gui.diffcontext)
5616                reshow_diff
5617        }
5618lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
5619$ctxm add separator
5620$ctxm add command -label {Options...} \
5621        -font font_ui \
5622        -command do_options
5623bind_button3 $ui_diff "
5624        set cursorX %x
5625        set cursorY %y
5626        if {\$ui_index eq \$current_diff_side} {
5627                $ctxm entryconf $ui_diff_applyhunk -label {Unstage Hunk From Commit}
5628        } else {
5629                $ctxm entryconf $ui_diff_applyhunk -label {Stage Hunk For Commit}
5630        }
5631        tk_popup $ctxm %X %Y
5632"
5633unset ui_diff_applyhunk
5634
5635# -- Status Bar
5636#
5637set ui_status_value {Initializing...}
5638label .status -textvariable ui_status_value \
5639        -anchor w \
5640        -justify left \
5641        -borderwidth 1 \
5642        -relief sunken \
5643        -font font_ui
5644pack .status -anchor w -side bottom -fill x
5645
5646# -- Load geometry
5647#
5648catch {
5649set gm $repo_config(gui.geometry)
5650wm geometry . [lindex $gm 0]
5651.vpane sash place 0 \
5652        [lindex [.vpane sash coord 0] 0] \
5653        [lindex $gm 1]
5654.vpane.files sash place 0 \
5655        [lindex $gm 2] \
5656        [lindex [.vpane.files sash coord 0] 1]
5657unset gm
5658}
5659
5660# -- Key Bindings
5661#
5662bind $ui_comm <$M1B-Key-Return> {do_commit;break}
5663bind $ui_comm <$M1B-Key-i> {do_add_all;break}
5664bind $ui_comm <$M1B-Key-I> {do_add_all;break}
5665bind $ui_comm <$M1B-Key-x> {tk_textCut %W;break}
5666bind $ui_comm <$M1B-Key-X> {tk_textCut %W;break}
5667bind $ui_comm <$M1B-Key-c> {tk_textCopy %W;break}
5668bind $ui_comm <$M1B-Key-C> {tk_textCopy %W;break}
5669bind $ui_comm <$M1B-Key-v> {tk_textPaste %W; %W see insert; break}
5670bind $ui_comm <$M1B-Key-V> {tk_textPaste %W; %W see insert; break}
5671bind $ui_comm <$M1B-Key-a> {%W tag add sel 0.0 end;break}
5672bind $ui_comm <$M1B-Key-A> {%W tag add sel 0.0 end;break}
5673
5674bind $ui_diff <$M1B-Key-x> {tk_textCopy %W;break}
5675bind $ui_diff <$M1B-Key-X> {tk_textCopy %W;break}
5676bind $ui_diff <$M1B-Key-c> {tk_textCopy %W;break}
5677bind $ui_diff <$M1B-Key-C> {tk_textCopy %W;break}
5678bind $ui_diff <$M1B-Key-v> {break}
5679bind $ui_diff <$M1B-Key-V> {break}
5680bind $ui_diff <$M1B-Key-a> {%W tag add sel 0.0 end;break}
5681bind $ui_diff <$M1B-Key-A> {%W tag add sel 0.0 end;break}
5682bind $ui_diff <Key-Up>     {catch {%W yview scroll -1 units};break}
5683bind $ui_diff <Key-Down>   {catch {%W yview scroll  1 units};break}
5684bind $ui_diff <Key-Left>   {catch {%W xview scroll -1 units};break}
5685bind $ui_diff <Key-Right>  {catch {%W xview scroll  1 units};break}
5686bind $ui_diff <Button-1>   {focus %W}
5687
5688if {!$single_commit} {
5689        bind . <$M1B-Key-n> do_create_branch
5690        bind . <$M1B-Key-N> do_create_branch
5691}
5692
5693bind .   <Destroy> do_quit
5694bind all <Key-F5> do_rescan
5695bind all <$M1B-Key-r> do_rescan
5696bind all <$M1B-Key-R> do_rescan
5697bind .   <$M1B-Key-s> do_signoff
5698bind .   <$M1B-Key-S> do_signoff
5699bind .   <$M1B-Key-i> do_add_all
5700bind .   <$M1B-Key-I> do_add_all
5701bind .   <$M1B-Key-Return> do_commit
5702bind all <$M1B-Key-q> do_quit
5703bind all <$M1B-Key-Q> do_quit
5704bind all <$M1B-Key-w> {destroy [winfo toplevel %W]}
5705bind all <$M1B-Key-W> {destroy [winfo toplevel %W]}
5706foreach i [list $ui_index $ui_workdir] {
5707        bind $i <Button-1>       "toggle_or_diff         $i %x %y; break"
5708        bind $i <$M1B-Button-1>  "add_one_to_selection   $i %x %y; break"
5709        bind $i <Shift-Button-1> "add_range_to_selection $i %x %y; break"
5710}
5711unset i
5712
5713set file_lists($ui_index) [list]
5714set file_lists($ui_workdir) [list]
5715
5716set HEAD {}
5717set PARENT {}
5718set MERGE_HEAD [list]
5719set commit_type {}
5720set empty_tree {}
5721set current_branch {}
5722set current_diff_path {}
5723set selected_commit_type new
5724
5725wm title . "[appname] ([file normalize [file dirname [gitdir]]])"
5726focus -force $ui_comm
5727
5728# -- Warn the user about environmental problems.  Cygwin's Tcl
5729#    does *not* pass its env array onto any processes it spawns.
5730#    This means that git processes get none of our environment.
5731#
5732if {[is_Cygwin]} {
5733        set ignored_env 0
5734        set suggest_user {}
5735        set msg "Possible environment issues exist.
5736
5737The following environment variables are probably
5738going to be ignored by any Git subprocess run
5739by [appname]:
5740
5741"
5742        foreach name [array names env] {
5743                switch -regexp -- $name {
5744                {^GIT_INDEX_FILE$} -
5745                {^GIT_OBJECT_DIRECTORY$} -
5746                {^GIT_ALTERNATE_OBJECT_DIRECTORIES$} -
5747                {^GIT_DIFF_OPTS$} -
5748                {^GIT_EXTERNAL_DIFF$} -
5749                {^GIT_PAGER$} -
5750                {^GIT_TRACE$} -
5751                {^GIT_CONFIG$} -
5752                {^GIT_CONFIG_LOCAL$} -
5753                {^GIT_(AUTHOR|COMMITTER)_DATE$} {
5754                        append msg " - $name\n"
5755                        incr ignored_env
5756                }
5757                {^GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL)$} {
5758                        append msg " - $name\n"
5759                        incr ignored_env
5760                        set suggest_user $name
5761                }
5762                }
5763        }
5764        if {$ignored_env > 0} {
5765                append msg "
5766This is due to a known issue with the
5767Tcl binary distributed by Cygwin."
5768
5769                if {$suggest_user ne {}} {
5770                        append msg "
5771
5772A good replacement for $suggest_user
5773is placing values for the user.name and
5774user.email settings into your personal
5775~/.gitconfig file.
5776"
5777                }
5778                warn_popup $msg
5779        }
5780        unset ignored_env msg suggest_user name
5781}
5782
5783# -- Only initialize complex UI if we are going to stay running.
5784#
5785if {!$single_commit} {
5786        load_all_remotes
5787        load_all_heads
5788
5789        populate_branch_menu
5790        populate_fetch_menu
5791        populate_push_menu
5792}
5793
5794# -- Only suggest a gc run if we are going to stay running.
5795#
5796if {!$single_commit} {
5797        set object_limit 2000
5798        if {[is_Windows]} {set object_limit 200}
5799        regexp {^([0-9]+) objects,} [exec git count-objects] _junk objects_current
5800        if {$objects_current >= $object_limit} {
5801                if {[ask_popup \
5802                        "This repository currently has $objects_current loose objects.
5803
5804To maintain optimal performance it is strongly
5805recommended that you compress the database
5806when more than $object_limit loose objects exist.
5807
5808Compress the database now?"] eq yes} {
5809                        do_gc
5810                }
5811        }
5812        unset object_limit _junk objects_current
5813}
5814
5815lock_index begin-read
5816after 1 do_rescan