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