git-gui.shon commit git-gui: Honor system encoding for filenames. (51a989b)
   1#!/bin/sh
   2# Tcl ignores the next line -*- tcl -*- \
   3exec wish "$0" -- "$@"
   4
   5set appvers {@@GIT_VERSION@@}
   6set copyright {
   7Copyright © 2006, 2007 Shawn Pearce, Paul Mackerras.
   8
   9This program is free software; you can redistribute it and/or modify
  10it under the terms of the GNU General Public License as published by
  11the Free Software Foundation; either version 2 of the License, or
  12(at your option) any later version.
  13
  14This program is distributed in the hope that it will be useful,
  15but WITHOUT ANY WARRANTY; without even the implied warranty of
  16MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  17GNU General Public License for more details.
  18
  19You should have received a copy of the GNU General Public License
  20along with this program; if not, write to the Free Software
  21Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA}
  22
  23######################################################################
  24##
  25## read only globals
  26
  27set _appname [lindex [file split $argv0] end]
  28set _gitdir {}
  29set _reponame {}
  30
  31proc appname {} {
  32        global _appname
  33        return $_appname
  34}
  35
  36proc gitdir {args} {
  37        global _gitdir
  38        if {$args eq {}} {
  39                return $_gitdir
  40        }
  41        return [eval [concat [list file join $_gitdir] $args]]
  42}
  43
  44proc reponame {} {
  45        global _reponame
  46        return $_reponame
  47}
  48
  49######################################################################
  50##
  51## config
  52
  53proc is_many_config {name} {
  54        switch -glob -- $name {
  55        remote.*.fetch -
  56        remote.*.push
  57                {return 1}
  58        *
  59                {return 0}
  60        }
  61}
  62
  63proc load_config {include_global} {
  64        global repo_config global_config default_config
  65
  66        array unset global_config
  67        if {$include_global} {
  68                catch {
  69                        set fd_rc [open "| git repo-config --global --list" r]
  70                        while {[gets $fd_rc line] >= 0} {
  71                                if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
  72                                        if {[is_many_config $name]} {
  73                                                lappend global_config($name) $value
  74                                        } else {
  75                                                set global_config($name) $value
  76                                        }
  77                                }
  78                        }
  79                        close $fd_rc
  80                }
  81        }
  82
  83        array unset repo_config
  84        catch {
  85                set fd_rc [open "| git repo-config --list" r]
  86                while {[gets $fd_rc line] >= 0} {
  87                        if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
  88                                if {[is_many_config $name]} {
  89                                        lappend repo_config($name) $value
  90                                } else {
  91                                        set repo_config($name) $value
  92                                }
  93                        }
  94                }
  95                close $fd_rc
  96        }
  97
  98        foreach name [array names default_config] {
  99                if {[catch {set v $global_config($name)}]} {
 100                        set global_config($name) $default_config($name)
 101                }
 102                if {[catch {set v $repo_config($name)}]} {
 103                        set repo_config($name) $default_config($name)
 104                }
 105        }
 106}
 107
 108proc save_config {} {
 109        global default_config font_descs
 110        global repo_config global_config
 111        global repo_config_new global_config_new
 112
 113        foreach option $font_descs {
 114                set name [lindex $option 0]
 115                set font [lindex $option 1]
 116                font configure $font \
 117                        -family $global_config_new(gui.$font^^family) \
 118                        -size $global_config_new(gui.$font^^size)
 119                font configure ${font}bold \
 120                        -family $global_config_new(gui.$font^^family) \
 121                        -size $global_config_new(gui.$font^^size)
 122                set global_config_new(gui.$name) [font configure $font]
 123                unset global_config_new(gui.$font^^family)
 124                unset global_config_new(gui.$font^^size)
 125        }
 126
 127        foreach name [array names default_config] {
 128                set value $global_config_new($name)
 129                if {$value ne $global_config($name)} {
 130                        if {$value eq $default_config($name)} {
 131                                catch {exec git repo-config --global --unset $name}
 132                        } else {
 133                                regsub -all "\[{}\]" $value {"} value
 134                                exec git repo-config --global $name $value
 135                        }
 136                        set global_config($name) $value
 137                        if {$value eq $repo_config($name)} {
 138                                catch {exec git repo-config --unset $name}
 139                                set repo_config($name) $value
 140                        }
 141                }
 142        }
 143
 144        foreach name [array names default_config] {
 145                set value $repo_config_new($name)
 146                if {$value ne $repo_config($name)} {
 147                        if {$value eq $global_config($name)} {
 148                                catch {exec git repo-config --unset $name}
 149                        } else {
 150                                regsub -all "\[{}\]" $value {"} value
 151                                exec git repo-config $name $value
 152                        }
 153                        set repo_config($name) $value
 154                }
 155        }
 156}
 157
 158proc error_popup {msg} {
 159        set title [appname]
 160        if {[reponame] ne {}} {
 161                append title " ([reponame])"
 162        }
 163        set cmd [list tk_messageBox \
 164                -icon error \
 165                -type ok \
 166                -title "$title: error" \
 167                -message $msg]
 168        if {[winfo ismapped .]} {
 169                lappend cmd -parent .
 170        }
 171        eval $cmd
 172}
 173
 174proc warn_popup {msg} {
 175        set title [appname]
 176        if {[reponame] ne {}} {
 177                append title " ([reponame])"
 178        }
 179        set cmd [list tk_messageBox \
 180                -icon warning \
 181                -type ok \
 182                -title "$title: warning" \
 183                -message $msg]
 184        if {[winfo ismapped .]} {
 185                lappend cmd -parent .
 186        }
 187        eval $cmd
 188}
 189
 190proc info_popup {msg} {
 191        set title [appname]
 192        if {[reponame] ne {}} {
 193                append title " ([reponame])"
 194        }
 195        tk_messageBox \
 196                -parent . \
 197                -icon info \
 198                -type ok \
 199                -title $title \
 200                -message $msg
 201}
 202
 203proc ask_popup {msg} {
 204        set title [appname]
 205        if {[reponame] ne {}} {
 206                append title " ([reponame])"
 207        }
 208        return [tk_messageBox \
 209                -parent . \
 210                -icon question \
 211                -type yesno \
 212                -title $title \
 213                -message $msg]
 214}
 215
 216######################################################################
 217##
 218## repository setup
 219
 220if {   [catch {set _gitdir $env(GIT_DIR)}]
 221        && [catch {set _gitdir [exec git rev-parse --git-dir]} err]} {
 222        catch {wm withdraw .}
 223        error_popup "Cannot find the git directory:\n\n$err"
 224        exit 1
 225}
 226if {![file isdirectory $_gitdir]} {
 227        catch {wm withdraw .}
 228        error_popup "Git directory not found:\n\n$_gitdir"
 229        exit 1
 230}
 231if {[lindex [file split $_gitdir] end] ne {.git}} {
 232        catch {wm withdraw .}
 233        error_popup "Cannot use funny .git directory:\n\n$gitdir"
 234        exit 1
 235}
 236if {[catch {cd [file dirname $_gitdir]} err]} {
 237        catch {wm withdraw .}
 238        error_popup "No working directory [file dirname $_gitdir]:\n\n$err"
 239        exit 1
 240}
 241set _reponame [lindex [file split \
 242        [file normalize [file dirname $_gitdir]]] \
 243        end]
 244
 245set single_commit 0
 246if {[appname] eq {git-citool}} {
 247        set single_commit 1
 248}
 249
 250######################################################################
 251##
 252## task management
 253
 254set rescan_active 0
 255set diff_active 0
 256set last_clicked {}
 257
 258set disable_on_lock [list]
 259set index_lock_type none
 260
 261proc lock_index {type} {
 262        global index_lock_type disable_on_lock
 263
 264        if {$index_lock_type eq {none}} {
 265                set index_lock_type $type
 266                foreach w $disable_on_lock {
 267                        uplevel #0 $w disabled
 268                }
 269                return 1
 270        } elseif {$index_lock_type eq "begin-$type"} {
 271                set index_lock_type $type
 272                return 1
 273        }
 274        return 0
 275}
 276
 277proc unlock_index {} {
 278        global index_lock_type disable_on_lock
 279
 280        set index_lock_type none
 281        foreach w $disable_on_lock {
 282                uplevel #0 $w normal
 283        }
 284}
 285
 286######################################################################
 287##
 288## status
 289
 290proc repository_state {ctvar hdvar mhvar} {
 291        global current_branch
 292        upvar $ctvar ct $hdvar hd $mhvar mh
 293
 294        set mh [list]
 295
 296        if {[catch {set current_branch [exec git symbolic-ref HEAD]}]} {
 297                set current_branch {}
 298        } else {
 299                regsub ^refs/((heads|tags|remotes)/)? \
 300                        $current_branch \
 301                        {} \
 302                        current_branch
 303        }
 304
 305        if {[catch {set hd [exec git rev-parse --verify HEAD]}]} {
 306                set hd {}
 307                set ct initial
 308                return
 309        }
 310
 311        set merge_head [gitdir MERGE_HEAD]
 312        if {[file exists $merge_head]} {
 313                set ct merge
 314                set fd_mh [open $merge_head r]
 315                while {[gets $fd_mh line] >= 0} {
 316                        lappend mh $line
 317                }
 318                close $fd_mh
 319                return
 320        }
 321
 322        set ct normal
 323}
 324
 325proc PARENT {} {
 326        global PARENT empty_tree
 327
 328        set p [lindex $PARENT 0]
 329        if {$p ne {}} {
 330                return $p
 331        }
 332        if {$empty_tree eq {}} {
 333                set empty_tree [exec git mktree << {}]
 334        }
 335        return $empty_tree
 336}
 337
 338proc rescan {after {honor_trustmtime 1}} {
 339        global HEAD PARENT MERGE_HEAD commit_type
 340        global ui_index ui_workdir ui_status_value ui_comm
 341        global rescan_active file_states
 342        global repo_config
 343
 344        if {$rescan_active > 0 || ![lock_index read]} return
 345
 346        repository_state newType newHEAD newMERGE_HEAD
 347        if {[string match amend* $commit_type]
 348                && $newType eq {normal}
 349                && $newHEAD eq $HEAD} {
 350        } else {
 351                set HEAD $newHEAD
 352                set PARENT $newHEAD
 353                set MERGE_HEAD $newMERGE_HEAD
 354                set commit_type $newType
 355        }
 356
 357        array unset file_states
 358
 359        if {![$ui_comm edit modified]
 360                || [string trim [$ui_comm get 0.0 end]] eq {}} {
 361                if {[load_message GITGUI_MSG]} {
 362                } elseif {[load_message MERGE_MSG]} {
 363                } elseif {[load_message SQUASH_MSG]} {
 364                }
 365                $ui_comm edit reset
 366                $ui_comm edit modified false
 367        }
 368
 369        if {$honor_trustmtime && $repo_config(gui.trustmtime) eq {true}} {
 370                rescan_stage2 {} $after
 371        } else {
 372                set rescan_active 1
 373                set ui_status_value {Refreshing file status...}
 374                set cmd [list git update-index]
 375                lappend cmd -q
 376                lappend cmd --unmerged
 377                lappend cmd --ignore-missing
 378                lappend cmd --refresh
 379                set fd_rf [open "| $cmd" r]
 380                fconfigure $fd_rf -blocking 0 -translation binary
 381                fileevent $fd_rf readable \
 382                        [list rescan_stage2 $fd_rf $after]
 383        }
 384}
 385
 386proc rescan_stage2 {fd after} {
 387        global ui_status_value
 388        global rescan_active buf_rdi buf_rdf buf_rlo
 389
 390        if {$fd ne {}} {
 391                read $fd
 392                if {![eof $fd]} return
 393                close $fd
 394        }
 395
 396        set ls_others [list | git ls-files --others -z \
 397                --exclude-per-directory=.gitignore]
 398        set info_exclude [gitdir info exclude]
 399        if {[file readable $info_exclude]} {
 400                lappend ls_others "--exclude-from=$info_exclude"
 401        }
 402
 403        set buf_rdi {}
 404        set buf_rdf {}
 405        set buf_rlo {}
 406
 407        set rescan_active 3
 408        set ui_status_value {Scanning for modified files ...}
 409        set fd_di [open "| git diff-index --cached -z [PARENT]" r]
 410        set fd_df [open "| git diff-files -z" r]
 411        set fd_lo [open $ls_others r]
 412
 413        fconfigure $fd_di -blocking 0 -translation binary -encoding binary
 414        fconfigure $fd_df -blocking 0 -translation binary -encoding binary
 415        fconfigure $fd_lo -blocking 0 -translation binary -encoding binary
 416        fileevent $fd_di readable [list read_diff_index $fd_di $after]
 417        fileevent $fd_df readable [list read_diff_files $fd_df $after]
 418        fileevent $fd_lo readable [list read_ls_others $fd_lo $after]
 419}
 420
 421proc load_message {file} {
 422        global ui_comm
 423
 424        set f [gitdir $file]
 425        if {[file isfile $f]} {
 426                if {[catch {set fd [open $f r]}]} {
 427                        return 0
 428                }
 429                set content [string trim [read $fd]]
 430                close $fd
 431                $ui_comm delete 0.0 end
 432                $ui_comm insert end $content
 433                return 1
 434        }
 435        return 0
 436}
 437
 438proc read_diff_index {fd after} {
 439        global buf_rdi
 440
 441        append buf_rdi [read $fd]
 442        set c 0
 443        set n [string length $buf_rdi]
 444        while {$c < $n} {
 445                set z1 [string first "\0" $buf_rdi $c]
 446                if {$z1 == -1} break
 447                incr z1
 448                set z2 [string first "\0" $buf_rdi $z1]
 449                if {$z2 == -1} break
 450
 451                incr c
 452                set i [split [string range $buf_rdi $c [expr {$z1 - 2}]] { }]
 453                set p [string range $buf_rdi $z1 [expr {$z2 - 1}]]
 454                merge_state \
 455                        [encoding convertfrom $p] \
 456                        [lindex $i 4]? \
 457                        [list [lindex $i 0] [lindex $i 2]] \
 458                        [list]
 459                set c $z2
 460                incr c
 461        }
 462        if {$c < $n} {
 463                set buf_rdi [string range $buf_rdi $c end]
 464        } else {
 465                set buf_rdi {}
 466        }
 467
 468        rescan_done $fd buf_rdi $after
 469}
 470
 471proc read_diff_files {fd after} {
 472        global buf_rdf
 473
 474        append buf_rdf [read $fd]
 475        set c 0
 476        set n [string length $buf_rdf]
 477        while {$c < $n} {
 478                set z1 [string first "\0" $buf_rdf $c]
 479                if {$z1 == -1} break
 480                incr z1
 481                set z2 [string first "\0" $buf_rdf $z1]
 482                if {$z2 == -1} break
 483
 484                incr c
 485                set i [split [string range $buf_rdf $c [expr {$z1 - 2}]] { }]
 486                set p [string range $buf_rdf $z1 [expr {$z2 - 1}]]
 487                merge_state \
 488                        [encoding convertfrom $p] \
 489                        ?[lindex $i 4] \
 490                        [list] \
 491                        [list [lindex $i 0] [lindex $i 2]]
 492                set c $z2
 493                incr c
 494        }
 495        if {$c < $n} {
 496                set buf_rdf [string range $buf_rdf $c end]
 497        } else {
 498                set buf_rdf {}
 499        }
 500
 501        rescan_done $fd buf_rdf $after
 502}
 503
 504proc read_ls_others {fd after} {
 505        global buf_rlo
 506
 507        append buf_rlo [read $fd]
 508        set pck [split $buf_rlo "\0"]
 509        set buf_rlo [lindex $pck end]
 510        foreach p [lrange $pck 0 end-1] {
 511                merge_state [encoding convertfrom $p] ?O
 512        }
 513        rescan_done $fd buf_rlo $after
 514}
 515
 516proc rescan_done {fd buf after} {
 517        global rescan_active
 518        global file_states repo_config
 519        upvar $buf to_clear
 520
 521        if {![eof $fd]} return
 522        set to_clear {}
 523        close $fd
 524        if {[incr rescan_active -1] > 0} return
 525
 526        prune_selection
 527        unlock_index
 528        display_all_files
 529        reshow_diff
 530        uplevel #0 $after
 531}
 532
 533proc prune_selection {} {
 534        global file_states selected_paths
 535
 536        foreach path [array names selected_paths] {
 537                if {[catch {set still_here $file_states($path)}]} {
 538                        unset selected_paths($path)
 539                }
 540        }
 541}
 542
 543######################################################################
 544##
 545## diff
 546
 547proc clear_diff {} {
 548        global ui_diff current_diff_path ui_index ui_workdir
 549
 550        $ui_diff conf -state normal
 551        $ui_diff delete 0.0 end
 552        $ui_diff conf -state disabled
 553
 554        set current_diff_path {}
 555
 556        $ui_index tag remove in_diff 0.0 end
 557        $ui_workdir tag remove in_diff 0.0 end
 558}
 559
 560proc reshow_diff {} {
 561        global ui_status_value file_states file_lists
 562        global current_diff_path current_diff_side
 563
 564        set p $current_diff_path
 565        if {$p eq {}
 566                || $current_diff_side eq {}
 567                || [catch {set s $file_states($p)}]
 568                || [lsearch -sorted $file_lists($current_diff_side) $p] == -1} {
 569                clear_diff
 570        } else {
 571                show_diff $p $current_diff_side
 572        }
 573}
 574
 575proc handle_empty_diff {} {
 576        global current_diff_path file_states file_lists
 577
 578        set path $current_diff_path
 579        set s $file_states($path)
 580        if {[lindex $s 0] ne {_M}} return
 581
 582        info_popup "No differences detected.
 583
 584[short_path $path] has no changes.
 585
 586The modification date of this file was updated
 587by another application, but the content within
 588the file was not changed.
 589
 590A rescan will be automatically started to find
 591other files which may have the same state."
 592
 593        clear_diff
 594        display_file $path __
 595        rescan {set ui_status_value {Ready.}} 0
 596}
 597
 598proc show_diff {path w {lno {}}} {
 599        global file_states file_lists
 600        global is_3way_diff diff_active repo_config
 601        global ui_diff ui_status_value ui_index ui_workdir
 602        global current_diff_path current_diff_side
 603
 604        if {$diff_active || ![lock_index read]} return
 605
 606        clear_diff
 607        if {$w eq {} || $lno == {}} {
 608                foreach w [array names file_lists] {
 609                        set lno [lsearch -sorted $file_lists($w) $path]
 610                        if {$lno >= 0} {
 611                                incr lno
 612                                break
 613                        }
 614                }
 615        }
 616        if {$w ne {} && $lno >= 1} {
 617                $w tag add in_diff $lno.0 [expr {$lno + 1}].0
 618        }
 619
 620        set s $file_states($path)
 621        set m [lindex $s 0]
 622        set is_3way_diff 0
 623        set diff_active 1
 624        set current_diff_path $path
 625        set current_diff_side $w
 626        set ui_status_value "Loading diff of [escape_path $path]..."
 627
 628        # - Git won't give us the diff, there's nothing to compare to!
 629        #
 630        if {$m eq {_O}} {
 631                set max_sz [expr {128 * 1024}]
 632                if {[catch {
 633                                set fd [open $path r]
 634                                set content [read $fd $max_sz]
 635                                close $fd
 636                                set sz [file size $path]
 637                        } err ]} {
 638                        set diff_active 0
 639                        unlock_index
 640                        set ui_status_value "Unable to display [escape_path $path]"
 641                        error_popup "Error loading file:\n\n$err"
 642                        return
 643                }
 644                $ui_diff conf -state normal
 645                if {![catch {set type [exec file $path]}]} {
 646                        set n [string length $path]
 647                        if {[string equal -length $n $path $type]} {
 648                                set type [string range $type $n end]
 649                                regsub {^:?\s*} $type {} type
 650                        }
 651                        $ui_diff insert end "* $type\n" d_@
 652                }
 653                if {[string first "\0" $content] != -1} {
 654                        $ui_diff insert end \
 655                                "* Binary file (not showing content)." \
 656                                d_@
 657                } else {
 658                        if {$sz > $max_sz} {
 659                                $ui_diff insert end \
 660"* Untracked file is $sz bytes.
 661* Showing only first $max_sz bytes.
 662" d_@
 663                        }
 664                        $ui_diff insert end $content
 665                        if {$sz > $max_sz} {
 666                                $ui_diff insert end "
 667* Untracked file clipped here by [appname].
 668* To see the entire file, use an external editor.
 669" d_@
 670                        }
 671                }
 672                $ui_diff conf -state disabled
 673                set diff_active 0
 674                unlock_index
 675                set ui_status_value {Ready.}
 676                return
 677        }
 678
 679        set cmd [list | git]
 680        if {$w eq $ui_index} {
 681                lappend cmd diff-index
 682                lappend cmd --cached
 683        } elseif {$w eq $ui_workdir} {
 684                if {[string index $m 0] eq {U}} {
 685                        lappend cmd diff
 686                } else {
 687                        lappend cmd diff-files
 688                }
 689        }
 690
 691        lappend cmd -p
 692        lappend cmd --no-color
 693        if {$repo_config(gui.diffcontext) > 0} {
 694                lappend cmd "-U$repo_config(gui.diffcontext)"
 695        }
 696        if {$w eq $ui_index} {
 697                lappend cmd [PARENT]
 698        }
 699        lappend cmd --
 700        lappend cmd $path
 701
 702        if {[catch {set fd [open $cmd r]} err]} {
 703                set diff_active 0
 704                unlock_index
 705                set ui_status_value "Unable to display [escape_path $path]"
 706                error_popup "Error loading diff:\n\n$err"
 707                return
 708        }
 709
 710        fconfigure $fd -blocking 0 -translation auto
 711        fileevent $fd readable [list read_diff $fd]
 712}
 713
 714proc read_diff {fd} {
 715        global ui_diff ui_status_value is_3way_diff diff_active
 716
 717        $ui_diff conf -state normal
 718        while {[gets $fd line] >= 0} {
 719                # -- Cleanup uninteresting diff header lines.
 720                #
 721                if {[string match {diff --git *}      $line]} continue
 722                if {[string match {diff --cc *}       $line]} continue
 723                if {[string match {diff --combined *} $line]} continue
 724                if {[string match {--- *}             $line]} continue
 725                if {[string match {+++ *}             $line]} continue
 726                if {$line eq {deleted file mode 120000}} {
 727                        set line "deleted symlink"
 728                }
 729
 730                # -- Automatically detect if this is a 3 way diff.
 731                #
 732                if {[string match {@@@ *} $line]} {set is_3way_diff 1}
 733
 734                if {[string match {index *} $line]
 735                        || [string match {mode *} $line]
 736                        || [string match {new file *} $line]
 737                        || [string match {deleted file *} $line]
 738                        || [string match {Binary files * and * differ} $line]
 739                        || $line eq {\ No newline at end of file}
 740                        || [regexp {^\* Unmerged path } $line]} {
 741                        set tags {}
 742                } elseif {$is_3way_diff} {
 743                        set op [string range $line 0 1]
 744                        switch -- $op {
 745                        {  } {set tags {}}
 746                        {@@} {set tags d_@}
 747                        { +} {set tags d_s+}
 748                        { -} {set tags d_s-}
 749                        {+ } {set tags d_+s}
 750                        {- } {set tags d_-s}
 751                        {--} {set tags d_--}
 752                        {++} {
 753                                if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
 754                                        set line [string replace $line 0 1 {  }]
 755                                        set tags d$op
 756                                } else {
 757                                        set tags d_++
 758                                }
 759                        }
 760                        default {
 761                                puts "error: Unhandled 3 way diff marker: {$op}"
 762                                set tags {}
 763                        }
 764                        }
 765                } else {
 766                        set op [string index $line 0]
 767                        switch -- $op {
 768                        { } {set tags {}}
 769                        {@} {set tags d_@}
 770                        {-} {set tags d_-}
 771                        {+} {
 772                                if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
 773                                        set line [string replace $line 0 0 { }]
 774                                        set tags d$op
 775                                } else {
 776                                        set tags d_+
 777                                }
 778                        }
 779                        default {
 780                                puts "error: Unhandled 2 way diff marker: {$op}"
 781                                set tags {}
 782                        }
 783                        }
 784                }
 785                $ui_diff insert end $line $tags
 786                $ui_diff insert end "\n" $tags
 787        }
 788        $ui_diff conf -state disabled
 789
 790        if {[eof $fd]} {
 791                close $fd
 792                set diff_active 0
 793                unlock_index
 794                set ui_status_value {Ready.}
 795
 796                if {[$ui_diff index end] eq {2.0}} {
 797                        handle_empty_diff
 798                }
 799        }
 800}
 801
 802######################################################################
 803##
 804## commit
 805
 806proc load_last_commit {} {
 807        global HEAD PARENT MERGE_HEAD commit_type ui_comm
 808
 809        if {[llength $PARENT] == 0} {
 810                error_popup {There is nothing to amend.
 811
 812You are about to create the initial commit.
 813There is no commit before this to amend.
 814}
 815                return
 816        }
 817
 818        repository_state curType curHEAD curMERGE_HEAD
 819        if {$curType eq {merge}} {
 820                error_popup {Cannot amend while merging.
 821
 822You are currently in the middle of a merge that
 823has not been fully completed.  You cannot amend
 824the prior commit unless you first abort the
 825current merge activity.
 826}
 827                return
 828        }
 829
 830        set msg {}
 831        set parents [list]
 832        if {[catch {
 833                        set fd [open "| git cat-file commit $curHEAD" r]
 834                        while {[gets $fd line] > 0} {
 835                                if {[string match {parent *} $line]} {
 836                                        lappend parents [string range $line 7 end]
 837                                }
 838                        }
 839                        set msg [string trim [read $fd]]
 840                        close $fd
 841                } err]} {
 842                error_popup "Error loading commit data for amend:\n\n$err"
 843                return
 844        }
 845
 846        set HEAD $curHEAD
 847        set PARENT $parents
 848        set MERGE_HEAD [list]
 849        switch -- [llength $parents] {
 850        0       {set commit_type amend-initial}
 851        1       {set commit_type amend}
 852        default {set commit_type amend-merge}
 853        }
 854
 855        $ui_comm delete 0.0 end
 856        $ui_comm insert end $msg
 857        $ui_comm edit reset
 858        $ui_comm edit modified false
 859        rescan {set ui_status_value {Ready.}}
 860}
 861
 862proc create_new_commit {} {
 863        global commit_type ui_comm
 864
 865        set commit_type normal
 866        $ui_comm delete 0.0 end
 867        $ui_comm edit reset
 868        $ui_comm edit modified false
 869        rescan {set ui_status_value {Ready.}}
 870}
 871
 872set GIT_COMMITTER_IDENT {}
 873
 874proc committer_ident {} {
 875        global GIT_COMMITTER_IDENT
 876
 877        if {$GIT_COMMITTER_IDENT eq {}} {
 878                if {[catch {set me [exec git var GIT_COMMITTER_IDENT]} err]} {
 879                        error_popup "Unable to obtain your identity:\n\n$err"
 880                        return {}
 881                }
 882                if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \
 883                        $me me GIT_COMMITTER_IDENT]} {
 884                        error_popup "Invalid GIT_COMMITTER_IDENT:\n\n$me"
 885                        return {}
 886                }
 887        }
 888
 889        return $GIT_COMMITTER_IDENT
 890}
 891
 892proc commit_tree {} {
 893        global HEAD commit_type file_states ui_comm repo_config
 894        global ui_status_value pch_error
 895
 896        if {![lock_index update]} return
 897        if {[committer_ident] eq {}} return
 898
 899        # -- Our in memory state should match the repository.
 900        #
 901        repository_state curType curHEAD curMERGE_HEAD
 902        if {[string match amend* $commit_type]
 903                && $curType eq {normal}
 904                && $curHEAD eq $HEAD} {
 905        } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
 906                info_popup {Last scanned state does not match repository state.
 907
 908Another Git program has modified this repository
 909since the last scan.  A rescan must be performed
 910before another commit can be created.
 911
 912The rescan will be automatically started now.
 913}
 914                unlock_index
 915                rescan {set ui_status_value {Ready.}}
 916                return
 917        }
 918
 919        # -- At least one file should differ in the index.
 920        #
 921        set files_ready 0
 922        foreach path [array names file_states] {
 923                switch -glob -- [lindex $file_states($path) 0] {
 924                _? {continue}
 925                A? -
 926                D? -
 927                M? {set files_ready 1}
 928                U? {
 929                        error_popup "Unmerged files cannot be committed.
 930
 931File [short_path $path] has merge conflicts.
 932You must resolve them and add the file before committing.
 933"
 934                        unlock_index
 935                        return
 936                }
 937                default {
 938                        error_popup "Unknown file state [lindex $s 0] detected.
 939
 940File [short_path $path] cannot be committed by this program.
 941"
 942                }
 943                }
 944        }
 945        if {!$files_ready} {
 946                info_popup {No changes to commit.
 947
 948You must add at least 1 file before you can commit.
 949}
 950                unlock_index
 951                return
 952        }
 953
 954        # -- A message is required.
 955        #
 956        set msg [string trim [$ui_comm get 1.0 end]]
 957        if {$msg eq {}} {
 958                error_popup {Please supply a commit message.
 959
 960A good commit message has the following format:
 961
 962- First line: Describe in one sentance what you did.
 963- Second line: Blank
 964- Remaining lines: Describe why this change is good.
 965}
 966                unlock_index
 967                return
 968        }
 969
 970        # -- Run the pre-commit hook.
 971        #
 972        set pchook [gitdir hooks pre-commit]
 973
 974        # On Cygwin [file executable] might lie so we need to ask
 975        # the shell if the hook is executable.  Yes that's annoying.
 976        #
 977        if {[is_Windows] && [file isfile $pchook]} {
 978                set pchook [list sh -c [concat \
 979                        "if test -x \"$pchook\";" \
 980                        "then exec \"$pchook\" 2>&1;" \
 981                        "fi"]]
 982        } elseif {[file executable $pchook]} {
 983                set pchook [list $pchook |& cat]
 984        } else {
 985                commit_writetree $curHEAD $msg
 986                return
 987        }
 988
 989        set ui_status_value {Calling pre-commit hook...}
 990        set pch_error {}
 991        set fd_ph [open "| $pchook" r]
 992        fconfigure $fd_ph -blocking 0 -translation binary
 993        fileevent $fd_ph readable \
 994                [list commit_prehook_wait $fd_ph $curHEAD $msg]
 995}
 996
 997proc commit_prehook_wait {fd_ph curHEAD msg} {
 998        global pch_error ui_status_value
 999
1000        append pch_error [read $fd_ph]
1001        fconfigure $fd_ph -blocking 1
1002        if {[eof $fd_ph]} {
1003                if {[catch {close $fd_ph}]} {
1004                        set ui_status_value {Commit declined by pre-commit hook.}
1005                        hook_failed_popup pre-commit $pch_error
1006                        unlock_index
1007                } else {
1008                        commit_writetree $curHEAD $msg
1009                }
1010                set pch_error {}
1011                return
1012        }
1013        fconfigure $fd_ph -blocking 0
1014}
1015
1016proc commit_writetree {curHEAD msg} {
1017        global ui_status_value
1018
1019        set ui_status_value {Committing changes...}
1020        set fd_wt [open "| git write-tree" r]
1021        fileevent $fd_wt readable \
1022                [list commit_committree $fd_wt $curHEAD $msg]
1023}
1024
1025proc commit_committree {fd_wt curHEAD msg} {
1026        global HEAD PARENT MERGE_HEAD commit_type
1027        global single_commit all_heads current_branch
1028        global ui_status_value ui_comm selected_commit_type
1029        global file_states selected_paths rescan_active
1030
1031        gets $fd_wt tree_id
1032        if {$tree_id eq {} || [catch {close $fd_wt} err]} {
1033                error_popup "write-tree failed:\n\n$err"
1034                set ui_status_value {Commit failed.}
1035                unlock_index
1036                return
1037        }
1038
1039        # -- Create the commit.
1040        #
1041        set cmd [list git commit-tree $tree_id]
1042        set parents [concat $PARENT $MERGE_HEAD]
1043        if {[llength $parents] > 0} {
1044                foreach p $parents {
1045                        lappend cmd -p $p
1046                }
1047        } else {
1048                # git commit-tree writes to stderr during initial commit.
1049                lappend cmd 2>/dev/null
1050        }
1051        lappend cmd << $msg
1052        if {[catch {set cmt_id [eval exec $cmd]} err]} {
1053                error_popup "commit-tree failed:\n\n$err"
1054                set ui_status_value {Commit failed.}
1055                unlock_index
1056                return
1057        }
1058
1059        # -- Update the HEAD ref.
1060        #
1061        set reflogm commit
1062        if {$commit_type ne {normal}} {
1063                append reflogm " ($commit_type)"
1064        }
1065        set i [string first "\n" $msg]
1066        if {$i >= 0} {
1067                append reflogm {: } [string range $msg 0 [expr {$i - 1}]]
1068        } else {
1069                append reflogm {: } $msg
1070        }
1071        set cmd [list git update-ref -m $reflogm HEAD $cmt_id $curHEAD]
1072        if {[catch {eval exec $cmd} err]} {
1073                error_popup "update-ref failed:\n\n$err"
1074                set ui_status_value {Commit failed.}
1075                unlock_index
1076                return
1077        }
1078
1079        # -- Make sure our current branch exists.
1080        #
1081        if {$commit_type eq {initial}} {
1082                lappend all_heads $current_branch
1083                set all_heads [lsort -unique $all_heads]
1084                populate_branch_menu
1085        }
1086
1087        # -- Cleanup after ourselves.
1088        #
1089        catch {file delete [gitdir MERGE_HEAD]}
1090        catch {file delete [gitdir MERGE_MSG]}
1091        catch {file delete [gitdir SQUASH_MSG]}
1092        catch {file delete [gitdir GITGUI_MSG]}
1093
1094        # -- Let rerere do its thing.
1095        #
1096        if {[file isdirectory [gitdir rr-cache]]} {
1097                catch {exec git rerere}
1098        }
1099
1100        # -- Run the post-commit hook.
1101        #
1102        set pchook [gitdir hooks post-commit]
1103        if {[is_Windows] && [file isfile $pchook]} {
1104                set pchook [list sh -c [concat \
1105                        "if test -x \"$pchook\";" \
1106                        "then exec \"$pchook\";" \
1107                        "fi"]]
1108        } elseif {![file executable $pchook]} {
1109                set pchook {}
1110        }
1111        if {$pchook ne {}} {
1112                catch {exec $pchook &}
1113        }
1114
1115        $ui_comm delete 0.0 end
1116        $ui_comm edit reset
1117        $ui_comm edit modified false
1118
1119        if {$single_commit} do_quit
1120
1121        # -- Update in memory status
1122        #
1123        set selected_commit_type new
1124        set commit_type normal
1125        set HEAD $cmt_id
1126        set PARENT $cmt_id
1127        set MERGE_HEAD [list]
1128
1129        foreach path [array names file_states] {
1130                set s $file_states($path)
1131                set m [lindex $s 0]
1132                switch -glob -- $m {
1133                _O -
1134                _M -
1135                _D {continue}
1136                __ -
1137                A_ -
1138                M_ -
1139                D_ {
1140                        unset file_states($path)
1141                        catch {unset selected_paths($path)}
1142                }
1143                DO {
1144                        set file_states($path) [list _O [lindex $s 1] {} {}]
1145                }
1146                AM -
1147                AD -
1148                MM -
1149                MD {
1150                        set file_states($path) [list \
1151                                _[string index $m 1] \
1152                                [lindex $s 1] \
1153                                [lindex $s 3] \
1154                                {}]
1155                }
1156                }
1157        }
1158
1159        display_all_files
1160        unlock_index
1161        reshow_diff
1162        set ui_status_value \
1163                "Changes committed as [string range $cmt_id 0 7]."
1164}
1165
1166######################################################################
1167##
1168## fetch pull push
1169
1170proc fetch_from {remote} {
1171        set w [new_console "fetch $remote" \
1172                "Fetching new changes from $remote"]
1173        set cmd [list git fetch]
1174        lappend cmd $remote
1175        console_exec $w $cmd
1176}
1177
1178proc pull_remote {remote branch} {
1179        global HEAD commit_type file_states repo_config
1180
1181        if {![lock_index update]} return
1182
1183        # -- Our in memory state should match the repository.
1184        #
1185        repository_state curType curHEAD curMERGE_HEAD
1186        if {$commit_type ne $curType || $HEAD ne $curHEAD} {
1187                info_popup {Last scanned state does not match repository state.
1188
1189Another Git program has modified this repository
1190since the last scan.  A rescan must be performed
1191before a pull operation can be started.
1192
1193The rescan will be automatically started now.
1194}
1195                unlock_index
1196                rescan {set ui_status_value {Ready.}}
1197                return
1198        }
1199
1200        # -- No differences should exist before a pull.
1201        #
1202        if {[array size file_states] != 0} {
1203                error_popup {Uncommitted but modified files are present.
1204
1205You should not perform a pull with unmodified
1206files in your working directory as Git will be
1207unable to recover from an incorrect merge.
1208
1209You should commit or revert all changes before
1210starting a pull operation.
1211}
1212                unlock_index
1213                return
1214        }
1215
1216        set w [new_console "pull $remote $branch" \
1217                "Pulling new changes from branch $branch in $remote"]
1218        set cmd [list git pull]
1219        if {$repo_config(gui.pullsummary) eq {false}} {
1220                lappend cmd --no-summary
1221        }
1222        lappend cmd $remote
1223        lappend cmd $branch
1224        console_exec $w $cmd [list post_pull_remote $remote $branch]
1225}
1226
1227proc post_pull_remote {remote branch success} {
1228        global HEAD PARENT MERGE_HEAD commit_type selected_commit_type
1229        global ui_status_value
1230
1231        unlock_index
1232        if {$success} {
1233                repository_state commit_type HEAD MERGE_HEAD
1234                set PARENT $HEAD
1235                set selected_commit_type new
1236                set ui_status_value "Pulling $branch from $remote complete."
1237        } else {
1238                rescan [list set ui_status_value \
1239                        "Conflicts detected while pulling $branch from $remote."]
1240        }
1241}
1242
1243proc push_to {remote} {
1244        set w [new_console "push $remote" \
1245                "Pushing changes to $remote"]
1246        set cmd [list git push]
1247        lappend cmd $remote
1248        console_exec $w $cmd
1249}
1250
1251######################################################################
1252##
1253## ui helpers
1254
1255proc mapicon {w state path} {
1256        global all_icons
1257
1258        if {[catch {set r $all_icons($state$w)}]} {
1259                puts "error: no icon for $w state={$state} $path"
1260                return file_plain
1261        }
1262        return $r
1263}
1264
1265proc mapdesc {state path} {
1266        global all_descs
1267
1268        if {[catch {set r $all_descs($state)}]} {
1269                puts "error: no desc for state={$state} $path"
1270                return $state
1271        }
1272        return $r
1273}
1274
1275proc escape_path {path} {
1276        regsub -all "\n" $path "\\n" path
1277        return $path
1278}
1279
1280proc short_path {path} {
1281        return [escape_path [lindex [file split $path] end]]
1282}
1283
1284set next_icon_id 0
1285set null_sha1 [string repeat 0 40]
1286
1287proc merge_state {path new_state {head_info {}} {index_info {}}} {
1288        global file_states next_icon_id null_sha1
1289
1290        set s0 [string index $new_state 0]
1291        set s1 [string index $new_state 1]
1292
1293        if {[catch {set info $file_states($path)}]} {
1294                set state __
1295                set icon n[incr next_icon_id]
1296        } else {
1297                set state [lindex $info 0]
1298                set icon [lindex $info 1]
1299                if {$head_info eq {}}  {set head_info  [lindex $info 2]}
1300                if {$index_info eq {}} {set index_info [lindex $info 3]}
1301        }
1302
1303        if     {$s0 eq {?}} {set s0 [string index $state 0]} \
1304        elseif {$s0 eq {_}} {set s0 _}
1305
1306        if     {$s1 eq {?}} {set s1 [string index $state 1]} \
1307        elseif {$s1 eq {_}} {set s1 _}
1308
1309        if {$s0 eq {A} && $s1 eq {_} && $head_info eq {}} {
1310                set head_info [list 0 $null_sha1]
1311        } elseif {$s0 ne {_} && [string index $state 0] eq {_}
1312                && $head_info eq {}} {
1313                set head_info $index_info
1314        }
1315
1316        set file_states($path) [list $s0$s1 $icon \
1317                $head_info $index_info \
1318                ]
1319        return $state
1320}
1321
1322proc display_file_helper {w path icon_name old_m new_m} {
1323        global file_lists
1324
1325        if {$new_m eq {_}} {
1326                set lno [lsearch -sorted $file_lists($w) $path]
1327                if {$lno >= 0} {
1328                        set file_lists($w) [lreplace $file_lists($w) $lno $lno]
1329                        incr lno
1330                        $w conf -state normal
1331                        $w delete $lno.0 [expr {$lno + 1}].0
1332                        $w conf -state disabled
1333                }
1334        } elseif {$old_m eq {_} && $new_m ne {_}} {
1335                lappend file_lists($w) $path
1336                set file_lists($w) [lsort -unique $file_lists($w)]
1337                set lno [lsearch -sorted $file_lists($w) $path]
1338                incr lno
1339                $w conf -state normal
1340                $w image create $lno.0 \
1341                        -align center -padx 5 -pady 1 \
1342                        -name $icon_name \
1343                        -image [mapicon $w $new_m $path]
1344                $w insert $lno.1 "[escape_path $path]\n"
1345                $w conf -state disabled
1346        } elseif {$old_m ne $new_m} {
1347                $w conf -state normal
1348                $w image conf $icon_name -image [mapicon $w $new_m $path]
1349                $w conf -state disabled
1350        }
1351}
1352
1353proc display_file {path state} {
1354        global file_states selected_paths
1355        global ui_index ui_workdir
1356
1357        set old_m [merge_state $path $state]
1358        set s $file_states($path)
1359        set new_m [lindex $s 0]
1360        set icon_name [lindex $s 1]
1361
1362        set o [string index $old_m 0]
1363        set n [string index $new_m 0]
1364        if {$o eq {U}} {
1365                set o _
1366        }
1367        if {$n eq {U}} {
1368                set n _
1369        }
1370        display_file_helper     $ui_index $path $icon_name $o $n
1371
1372        if {[string index $old_m 0] eq {U}} {
1373                set o U
1374        } else {
1375                set o [string index $old_m 1]
1376        }
1377        if {[string index $new_m 0] eq {U}} {
1378                set n U
1379        } else {
1380                set n [string index $new_m 1]
1381        }
1382        display_file_helper     $ui_workdir $path $icon_name $o $n
1383
1384        if {$new_m eq {__}} {
1385                unset file_states($path)
1386                catch {unset selected_paths($path)}
1387        }
1388}
1389
1390proc display_all_files_helper {w path icon_name m} {
1391        global file_lists
1392
1393        lappend file_lists($w) $path
1394        set lno [expr {[lindex [split [$w index end] .] 0] - 1}]
1395        $w image create end \
1396                -align center -padx 5 -pady 1 \
1397                -name $icon_name \
1398                -image [mapicon $w $m $path]
1399        $w insert end "[escape_path $path]\n"
1400}
1401
1402proc display_all_files {} {
1403        global ui_index ui_workdir
1404        global file_states file_lists
1405        global last_clicked
1406
1407        $ui_index conf -state normal
1408        $ui_workdir conf -state normal
1409
1410        $ui_index delete 0.0 end
1411        $ui_workdir delete 0.0 end
1412        set last_clicked {}
1413
1414        set file_lists($ui_index) [list]
1415        set file_lists($ui_workdir) [list]
1416
1417        foreach path [lsort [array names file_states]] {
1418                set s $file_states($path)
1419                set m [lindex $s 0]
1420                set icon_name [lindex $s 1]
1421
1422                set s [string index $m 0]
1423                if {$s ne {U} && $s ne {_}} {
1424                        display_all_files_helper $ui_index $path \
1425                                $icon_name $s
1426                }
1427
1428                if {[string index $m 0] eq {U}} {
1429                        set s U
1430                } else {
1431                        set s [string index $m 1]
1432                }
1433                if {$s ne {_}} {
1434                        display_all_files_helper $ui_workdir $path \
1435                                $icon_name $s
1436                }
1437        }
1438
1439        $ui_index conf -state disabled
1440        $ui_workdir conf -state disabled
1441}
1442
1443proc update_indexinfo {msg pathList after} {
1444        global update_index_cp ui_status_value
1445
1446        if {![lock_index update]} return
1447
1448        set update_index_cp 0
1449        set pathList [lsort $pathList]
1450        set totalCnt [llength $pathList]
1451        set batch [expr {int($totalCnt * .01) + 1}]
1452        if {$batch > 25} {set batch 25}
1453
1454        set ui_status_value [format \
1455                "$msg... %i/%i files (%.2f%%)" \
1456                $update_index_cp \
1457                $totalCnt \
1458                0.0]
1459        set fd [open "| git update-index -z --index-info" w]
1460        fconfigure $fd \
1461                -blocking 0 \
1462                -buffering full \
1463                -buffersize 512 \
1464                -encoding binary \
1465                -translation binary
1466        fileevent $fd writable [list \
1467                write_update_indexinfo \
1468                $fd \
1469                $pathList \
1470                $totalCnt \
1471                $batch \
1472                $msg \
1473                $after \
1474                ]
1475}
1476
1477proc write_update_indexinfo {fd pathList totalCnt batch msg after} {
1478        global update_index_cp ui_status_value
1479        global file_states current_diff_path
1480
1481        if {$update_index_cp >= $totalCnt} {
1482                close $fd
1483                unlock_index
1484                uplevel #0 $after
1485                return
1486        }
1487
1488        for {set i $batch} \
1489                {$update_index_cp < $totalCnt && $i > 0} \
1490                {incr i -1} {
1491                set path [lindex $pathList $update_index_cp]
1492                incr update_index_cp
1493
1494                set s $file_states($path)
1495                switch -glob -- [lindex $s 0] {
1496                A? {set new _O}
1497                M? {set new _M}
1498                D_ {set new _D}
1499                D? {set new _?}
1500                ?? {continue}
1501                }
1502                set info [lindex $s 2]
1503                if {$info eq {}} continue
1504
1505                puts -nonewline $fd "$info\t[encoding convertto $path]\0"
1506                display_file $path $new
1507        }
1508
1509        set ui_status_value [format \
1510                "$msg... %i/%i files (%.2f%%)" \
1511                $update_index_cp \
1512                $totalCnt \
1513                [expr {100.0 * $update_index_cp / $totalCnt}]]
1514}
1515
1516proc update_index {msg pathList after} {
1517        global update_index_cp ui_status_value
1518
1519        if {![lock_index update]} return
1520
1521        set update_index_cp 0
1522        set pathList [lsort $pathList]
1523        set totalCnt [llength $pathList]
1524        set batch [expr {int($totalCnt * .01) + 1}]
1525        if {$batch > 25} {set batch 25}
1526
1527        set ui_status_value [format \
1528                "$msg... %i/%i files (%.2f%%)" \
1529                $update_index_cp \
1530                $totalCnt \
1531                0.0]
1532        set fd [open "| git update-index --add --remove -z --stdin" w]
1533        fconfigure $fd \
1534                -blocking 0 \
1535                -buffering full \
1536                -buffersize 512 \
1537                -encoding binary \
1538                -translation binary
1539        fileevent $fd writable [list \
1540                write_update_index \
1541                $fd \
1542                $pathList \
1543                $totalCnt \
1544                $batch \
1545                $msg \
1546                $after \
1547                ]
1548}
1549
1550proc write_update_index {fd pathList totalCnt batch msg after} {
1551        global update_index_cp ui_status_value
1552        global file_states current_diff_path
1553
1554        if {$update_index_cp >= $totalCnt} {
1555                close $fd
1556                unlock_index
1557                uplevel #0 $after
1558                return
1559        }
1560
1561        for {set i $batch} \
1562                {$update_index_cp < $totalCnt && $i > 0} \
1563                {incr i -1} {
1564                set path [lindex $pathList $update_index_cp]
1565                incr update_index_cp
1566
1567                switch -glob -- [lindex $file_states($path) 0] {
1568                AD {set new __}
1569                ?D {set new D_}
1570                _O -
1571                AM {set new A_}
1572                U? {
1573                        if {[file exists $path]} {
1574                                set new M_
1575                        } else {
1576                                set new D_
1577                        }
1578                }
1579                ?M {set new M_}
1580                ?? {continue}
1581                }
1582                puts -nonewline $fd "[encoding convertto $path]\0"
1583                display_file $path $new
1584        }
1585
1586        set ui_status_value [format \
1587                "$msg... %i/%i files (%.2f%%)" \
1588                $update_index_cp \
1589                $totalCnt \
1590                [expr {100.0 * $update_index_cp / $totalCnt}]]
1591}
1592
1593proc checkout_index {msg pathList after} {
1594        global update_index_cp ui_status_value
1595
1596        if {![lock_index update]} return
1597
1598        set update_index_cp 0
1599        set pathList [lsort $pathList]
1600        set totalCnt [llength $pathList]
1601        set batch [expr {int($totalCnt * .01) + 1}]
1602        if {$batch > 25} {set batch 25}
1603
1604        set ui_status_value [format \
1605                "$msg... %i/%i files (%.2f%%)" \
1606                $update_index_cp \
1607                $totalCnt \
1608                0.0]
1609        set cmd [list git checkout-index]
1610        lappend cmd --index
1611        lappend cmd --quiet
1612        lappend cmd --force
1613        lappend cmd -z
1614        lappend cmd --stdin
1615        set fd [open "| $cmd " w]
1616        fconfigure $fd \
1617                -blocking 0 \
1618                -buffering full \
1619                -buffersize 512 \
1620                -encoding binary \
1621                -translation binary
1622        fileevent $fd writable [list \
1623                write_checkout_index \
1624                $fd \
1625                $pathList \
1626                $totalCnt \
1627                $batch \
1628                $msg \
1629                $after \
1630                ]
1631}
1632
1633proc write_checkout_index {fd pathList totalCnt batch msg after} {
1634        global update_index_cp ui_status_value
1635        global file_states current_diff_path
1636
1637        if {$update_index_cp >= $totalCnt} {
1638                close $fd
1639                unlock_index
1640                uplevel #0 $after
1641                return
1642        }
1643
1644        for {set i $batch} \
1645                {$update_index_cp < $totalCnt && $i > 0} \
1646                {incr i -1} {
1647                set path [lindex $pathList $update_index_cp]
1648                incr update_index_cp
1649                switch -glob -- [lindex $file_states($path) 0] {
1650                U? {continue}
1651                ?M -
1652                ?D {
1653                        puts -nonewline $fd "[encoding convertto $path]\0"
1654                        display_file $path ?_
1655                }
1656                }
1657        }
1658
1659        set ui_status_value [format \
1660                "$msg... %i/%i files (%.2f%%)" \
1661                $update_index_cp \
1662                $totalCnt \
1663                [expr {100.0 * $update_index_cp / $totalCnt}]]
1664}
1665
1666######################################################################
1667##
1668## branch management
1669
1670proc is_tracking_branch {name} {
1671        global tracking_branches
1672
1673        if {![catch {set info $tracking_branches($name)}]} {
1674                return 1
1675        }
1676        foreach t [array names tracking_branches] {
1677                if {[string match {*/\*} $t] && [string match $t $name]} {
1678                        return 1
1679                }
1680        }
1681        return 0
1682}
1683
1684proc load_all_heads {} {
1685        global all_heads
1686
1687        set all_heads [list]
1688        set fd [open "| git for-each-ref --format=%(refname) refs/heads" r]
1689        while {[gets $fd line] > 0} {
1690                if {[is_tracking_branch $line]} continue
1691                if {![regsub ^refs/heads/ $line {} name]} continue
1692                lappend all_heads $name
1693        }
1694        close $fd
1695
1696        set all_heads [lsort $all_heads]
1697}
1698
1699proc populate_branch_menu {} {
1700        global all_heads disable_on_lock
1701
1702        set m .mbar.branch
1703        set last [$m index last]
1704        for {set i 0} {$i <= $last} {incr i} {
1705                if {[$m type $i] eq {separator}} {
1706                        $m delete $i last
1707                        set new_dol [list]
1708                        foreach a $disable_on_lock {
1709                                if {[lindex $a 0] ne $m || [lindex $a 2] < $i} {
1710                                        lappend new_dol $a
1711                                }
1712                        }
1713                        set disable_on_lock $new_dol
1714                        break
1715                }
1716        }
1717
1718        $m add separator
1719        foreach b $all_heads {
1720                $m add radiobutton \
1721                        -label $b \
1722                        -command [list switch_branch $b] \
1723                        -variable current_branch \
1724                        -value $b \
1725                        -font font_ui
1726                lappend disable_on_lock \
1727                        [list $m entryconf [$m index last] -state]
1728        }
1729}
1730
1731proc all_tracking_branches {} {
1732        global tracking_branches
1733
1734        set all_trackings {}
1735        set cmd {}
1736        foreach name [array names tracking_branches] {
1737                if {[regsub {/\*$} $name {} name]} {
1738                        lappend cmd $name
1739                } else {
1740                        regsub ^refs/(heads|remotes)/ $name {} name
1741                        lappend all_trackings $name
1742                }
1743        }
1744
1745        if {$cmd ne {}} {
1746                set fd [open "| git for-each-ref --format=%(refname) $cmd" r]
1747                while {[gets $fd name] > 0} {
1748                        regsub ^refs/(heads|remotes)/ $name {} name
1749                        lappend all_trackings $name
1750                }
1751                close $fd
1752        }
1753
1754        return [lsort -unique $all_trackings]
1755}
1756
1757proc do_create_branch_action {w} {
1758        global all_heads null_sha1 repo_config
1759        global create_branch_checkout create_branch_revtype
1760        global create_branch_head create_branch_trackinghead
1761
1762        set newbranch [string trim [$w.desc.name_t get 0.0 end]]
1763        if {$newbranch eq {}
1764                || $newbranch eq $repo_config(gui.newbranchtemplate)} {
1765                tk_messageBox \
1766                        -icon error \
1767                        -type ok \
1768                        -title [wm title $w] \
1769                        -parent $w \
1770                        -message "Please supply a branch name."
1771                focus $w.desc.name_t
1772                return
1773        }
1774        if {![catch {exec git show-ref --verify -- "refs/heads/$newbranch"}]} {
1775                tk_messageBox \
1776                        -icon error \
1777                        -type ok \
1778                        -title [wm title $w] \
1779                        -parent $w \
1780                        -message "Branch '$newbranch' already exists."
1781                focus $w.desc.name_t
1782                return
1783        }
1784        if {[catch {exec git check-ref-format "heads/$newbranch"}]} {
1785                tk_messageBox \
1786                        -icon error \
1787                        -type ok \
1788                        -title [wm title $w] \
1789                        -parent $w \
1790                        -message "We do not like '$newbranch' as a branch name."
1791                focus $w.desc.name_t
1792                return
1793        }
1794
1795        set rev {}
1796        switch -- $create_branch_revtype {
1797        head {set rev $create_branch_head}
1798        tracking {set rev $create_branch_trackinghead}
1799        expression {set rev [string trim [$w.from.exp_t get 0.0 end]]}
1800        }
1801        if {[catch {set cmt [exec git rev-parse --verify "${rev}^0"]}]} {
1802                tk_messageBox \
1803                        -icon error \
1804                        -type ok \
1805                        -title [wm title $w] \
1806                        -parent $w \
1807                        -message "Invalid starting revision: $rev"
1808                return
1809        }
1810        set cmd [list git update-ref]
1811        lappend cmd -m
1812        lappend cmd "branch: Created from $rev"
1813        lappend cmd "refs/heads/$newbranch"
1814        lappend cmd $cmt
1815        lappend cmd $null_sha1
1816        if {[catch {eval exec $cmd} err]} {
1817                tk_messageBox \
1818                        -icon error \
1819                        -type ok \
1820                        -title [wm title $w] \
1821                        -parent $w \
1822                        -message "Failed to create '$newbranch'.\n\n$err"
1823                return
1824        }
1825
1826        lappend all_heads $newbranch
1827        set all_heads [lsort $all_heads]
1828        populate_branch_menu
1829        destroy $w
1830        if {$create_branch_checkout} {
1831                switch_branch $newbranch
1832        }
1833}
1834
1835proc radio_selector {varname value args} {
1836        upvar #0 $varname var
1837        set var $value
1838}
1839
1840trace add variable create_branch_head write \
1841        [list radio_selector create_branch_revtype head]
1842trace add variable create_branch_trackinghead write \
1843        [list radio_selector create_branch_revtype tracking]
1844
1845trace add variable delete_branch_head write \
1846        [list radio_selector delete_branch_checktype head]
1847trace add variable delete_branch_trackinghead write \
1848        [list radio_selector delete_branch_checktype tracking]
1849
1850proc do_create_branch {} {
1851        global all_heads current_branch repo_config
1852        global create_branch_checkout create_branch_revtype
1853        global create_branch_head create_branch_trackinghead
1854
1855        set w .branch_editor
1856        toplevel $w
1857        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
1858
1859        label $w.header -text {Create New Branch} \
1860                -font font_uibold
1861        pack $w.header -side top -fill x
1862
1863        frame $w.buttons
1864        button $w.buttons.create -text Create \
1865                -font font_ui \
1866                -default active \
1867                -command [list do_create_branch_action $w]
1868        pack $w.buttons.create -side right
1869        button $w.buttons.cancel -text {Cancel} \
1870                -font font_ui \
1871                -command [list destroy $w]
1872        pack $w.buttons.cancel -side right -padx 5
1873        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
1874
1875        labelframe $w.desc \
1876                -text {Branch Description} \
1877                -font font_ui
1878        label $w.desc.name_l -text {Name:} -font font_ui
1879        text $w.desc.name_t \
1880                -borderwidth 1 \
1881                -relief sunken \
1882                -height 1 \
1883                -width 40 \
1884                -font font_ui
1885        $w.desc.name_t insert 0.0 $repo_config(gui.newbranchtemplate)
1886        grid $w.desc.name_l $w.desc.name_t -stick we -padx {0 5}
1887        bind $w.desc.name_t <Shift-Key-Tab> {focus [tk_focusPrev %W];break}
1888        bind $w.desc.name_t <Key-Tab> {focus [tk_focusNext %W];break}
1889        bind $w.desc.name_t <Key-Return> "do_create_branch_action $w;break"
1890        bind $w.desc.name_t <Key> {
1891                if {{%K} ne {BackSpace}
1892                        && {%K} ne {Tab}
1893                        && {%K} ne {Escape}
1894                        && {%K} ne {Return}} {
1895                        if {%k <= 32} break
1896                        if {[string first %A {~^:?*[}] >= 0} break
1897                }
1898        }
1899        grid columnconfigure $w.desc 1 -weight 1
1900        pack $w.desc -anchor nw -fill x -pady 5 -padx 5
1901
1902        labelframe $w.from \
1903                -text {Starting Revision} \
1904                -font font_ui
1905        radiobutton $w.from.head_r \
1906                -text {Local Branch:} \
1907                -value head \
1908                -variable create_branch_revtype \
1909                -font font_ui
1910        eval tk_optionMenu $w.from.head_m create_branch_head $all_heads
1911        grid $w.from.head_r $w.from.head_m -sticky w
1912        set all_trackings [all_tracking_branches]
1913        if {$all_trackings ne {}} {
1914                set create_branch_trackinghead [lindex $all_trackings 0]
1915                radiobutton $w.from.tracking_r \
1916                        -text {Tracking Branch:} \
1917                        -value tracking \
1918                        -variable create_branch_revtype \
1919                        -font font_ui
1920                eval tk_optionMenu $w.from.tracking_m \
1921                        create_branch_trackinghead \
1922                        $all_trackings
1923                grid $w.from.tracking_r $w.from.tracking_m -sticky w
1924        }
1925        radiobutton $w.from.exp_r \
1926                -text {Revision Expression:} \
1927                -value expression \
1928                -variable create_branch_revtype \
1929                -font font_ui
1930        text $w.from.exp_t \
1931                -borderwidth 1 \
1932                -relief sunken \
1933                -height 1 \
1934                -width 50 \
1935                -font font_ui
1936        grid $w.from.exp_r $w.from.exp_t -stick we -padx {0 5}
1937        bind $w.from.exp_t <Shift-Key-Tab> {focus [tk_focusPrev %W];break}
1938        bind $w.from.exp_t <Key-Tab> {focus [tk_focusNext %W];break}
1939        bind $w.from.exp_t <Key-Return> "do_create_branch_action $w;break"
1940        bind $w.from.exp_t <Key-space> break
1941        bind $w.from.exp_t <Key> {set create_branch_revtype expression}
1942        grid columnconfigure $w.from 1 -weight 1
1943        pack $w.from -anchor nw -fill x -pady 5 -padx 5
1944
1945        labelframe $w.postActions \
1946                -text {Post Creation Actions} \
1947                -font font_ui
1948        checkbutton $w.postActions.checkout \
1949                -text {Checkout after creation} \
1950                -variable create_branch_checkout \
1951                -font font_ui
1952        pack $w.postActions.checkout -anchor nw
1953        pack $w.postActions -anchor nw -fill x -pady 5 -padx 5
1954
1955        set create_branch_checkout 1
1956        set create_branch_head $current_branch
1957        set create_branch_revtype head
1958
1959        bind $w <Visibility> "grab $w; focus $w.desc.name_t"
1960        bind $w <Key-Escape> "destroy $w"
1961        bind $w <Key-Return> "do_create_branch_action $w;break"
1962        wm title $w "[appname] ([reponame]): Create Branch"
1963        tkwait window $w
1964}
1965
1966proc do_delete_branch_action {w} {
1967        global all_heads
1968        global delete_branch_checktype delete_branch_head delete_branch_trackinghead
1969
1970        set check_rev {}
1971        switch -- $delete_branch_checktype {
1972        head {set check_rev $delete_branch_head}
1973        tracking {set check_rev $delete_branch_trackinghead}
1974        always {set check_rev {:none}}
1975        }
1976        if {$check_rev eq {:none}} {
1977                set check_cmt {}
1978        } elseif {[catch {set check_cmt [exec git rev-parse --verify "${check_rev}^0"]}]} {
1979                tk_messageBox \
1980                        -icon error \
1981                        -type ok \
1982                        -title [wm title $w] \
1983                        -parent $w \
1984                        -message "Invalid check revision: $check_rev"
1985                return
1986        }
1987
1988        set to_delete [list]
1989        set not_merged [list]
1990        foreach i [$w.list.l curselection] {
1991                set b [$w.list.l get $i]
1992                if {[catch {set o [exec git rev-parse --verify $b]}]} continue
1993                if {$check_cmt ne {}} {
1994                        if {$b eq $check_rev} continue
1995                        if {[catch {set m [exec git merge-base $o $check_cmt]}]} continue
1996                        if {$o ne $m} {
1997                                lappend not_merged $b
1998                                continue
1999                        }
2000                }
2001                lappend to_delete [list $b $o]
2002        }
2003        if {$not_merged ne {}} {
2004                set msg "The following branches are not completely merged into $check_rev:
2005
2006 - [join $not_merged "\n - "]"
2007                tk_messageBox \
2008                        -icon info \
2009                        -type ok \
2010                        -title [wm title $w] \
2011                        -parent $w \
2012                        -message $msg
2013        }
2014        if {$to_delete eq {}} return
2015        if {$delete_branch_checktype eq {always}} {
2016                set msg {Recovering deleted branches is difficult.
2017
2018Delete the selected branches?}
2019                if {[tk_messageBox \
2020                        -icon warning \
2021                        -type yesno \
2022                        -title [wm title $w] \
2023                        -parent $w \
2024                        -message $msg] ne yes} {
2025                        return
2026                }
2027        }
2028
2029        set failed {}
2030        foreach i $to_delete {
2031                set b [lindex $i 0]
2032                set o [lindex $i 1]
2033                if {[catch {exec git update-ref -d "refs/heads/$b" $o} err]} {
2034                        append failed " - $b: $err\n"
2035                } else {
2036                        set x [lsearch -sorted $all_heads $b]
2037                        if {$x >= 0} {
2038                                set all_heads [lreplace $all_heads $x $x]
2039                        }
2040                }
2041        }
2042
2043        if {$failed ne {}} {
2044                tk_messageBox \
2045                        -icon error \
2046                        -type ok \
2047                        -title [wm title $w] \
2048                        -parent $w \
2049                        -message "Failed to delete branches:\n$failed"
2050        }
2051
2052        set all_heads [lsort $all_heads]
2053        populate_branch_menu
2054        destroy $w
2055}
2056
2057proc do_delete_branch {} {
2058        global all_heads tracking_branches current_branch
2059        global delete_branch_checktype delete_branch_head delete_branch_trackinghead
2060
2061        set w .branch_editor
2062        toplevel $w
2063        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
2064
2065        label $w.header -text {Delete Local Branch} \
2066                -font font_uibold
2067        pack $w.header -side top -fill x
2068
2069        frame $w.buttons
2070        button $w.buttons.create -text Delete \
2071                -font font_ui \
2072                -command [list do_delete_branch_action $w]
2073        pack $w.buttons.create -side right
2074        button $w.buttons.cancel -text {Cancel} \
2075                -font font_ui \
2076                -command [list destroy $w]
2077        pack $w.buttons.cancel -side right -padx 5
2078        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
2079
2080        labelframe $w.list \
2081                -text {Local Branches} \
2082                -font font_ui
2083        listbox $w.list.l \
2084                -height 10 \
2085                -width 50 \
2086                -selectmode extended \
2087                -font font_ui
2088        foreach h $all_heads {
2089                if {$h ne $current_branch} {
2090                        $w.list.l insert end $h
2091                }
2092        }
2093        pack $w.list.l -fill both -pady 5 -padx 5
2094        pack $w.list -fill both -pady 5 -padx 5
2095
2096        labelframe $w.validate \
2097                -text {Delete Only If} \
2098                -font font_ui
2099        radiobutton $w.validate.head_r \
2100                -text {Merged Into Local Branch:} \
2101                -value head \
2102                -variable delete_branch_checktype \
2103                -font font_ui
2104        eval tk_optionMenu $w.validate.head_m delete_branch_head $all_heads
2105        grid $w.validate.head_r $w.validate.head_m -sticky w
2106        set all_trackings [all_tracking_branches]
2107        if {$all_trackings ne {}} {
2108                set delete_branch_trackinghead [lindex $all_trackings 0]
2109                radiobutton $w.validate.tracking_r \
2110                        -text {Merged Into Tracking Branch:} \
2111                        -value tracking \
2112                        -variable delete_branch_checktype \
2113                        -font font_ui
2114                eval tk_optionMenu $w.validate.tracking_m \
2115                        delete_branch_trackinghead \
2116                        $all_trackings
2117                grid $w.validate.tracking_r $w.validate.tracking_m -sticky w
2118        }
2119        radiobutton $w.validate.always_r \
2120                -text {Always (Do not perform merge checks)} \
2121                -value always \
2122                -variable delete_branch_checktype \
2123                -font font_ui
2124        grid $w.validate.always_r -columnspan 2 -sticky w
2125        grid columnconfigure $w.validate 1 -weight 1
2126        pack $w.validate -anchor nw -fill x -pady 5 -padx 5
2127
2128        set delete_branch_head $current_branch
2129        set delete_branch_checktype head
2130
2131        bind $w <Visibility> "grab $w; focus $w"
2132        bind $w <Key-Escape> "destroy $w"
2133        wm title $w "[appname] ([reponame]): Delete Branch"
2134        tkwait window $w
2135}
2136
2137proc switch_branch {b} {
2138        global HEAD commit_type file_states current_branch
2139        global selected_commit_type ui_comm
2140
2141        if {![lock_index switch]} return
2142
2143        # -- Backup the selected branch (repository_state resets it)
2144        #
2145        set new_branch $current_branch
2146
2147        # -- Our in memory state should match the repository.
2148        #
2149        repository_state curType curHEAD curMERGE_HEAD
2150        if {[string match amend* $commit_type]
2151                && $curType eq {normal}
2152                && $curHEAD eq $HEAD} {
2153        } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} {
2154                info_popup {Last scanned state does not match repository state.
2155
2156Another Git program has modified this repository
2157since the last scan.  A rescan must be performed
2158before the current branch can be changed.
2159
2160The rescan will be automatically started now.
2161}
2162                unlock_index
2163                rescan {set ui_status_value {Ready.}}
2164                return
2165        }
2166
2167        # -- Toss the message buffer if we are in amend mode.
2168        #
2169        if {[string match amend* $curType]} {
2170                $ui_comm delete 0.0 end
2171                $ui_comm edit reset
2172                $ui_comm edit modified false
2173        }
2174
2175        set selected_commit_type new
2176        set current_branch $new_branch
2177
2178        unlock_index
2179        error "NOT FINISHED"
2180}
2181
2182######################################################################
2183##
2184## remote management
2185
2186proc load_all_remotes {} {
2187        global repo_config
2188        global all_remotes tracking_branches
2189
2190        set all_remotes [list]
2191        array unset tracking_branches
2192
2193        set rm_dir [gitdir remotes]
2194        if {[file isdirectory $rm_dir]} {
2195                set all_remotes [glob \
2196                        -types f \
2197                        -tails \
2198                        -nocomplain \
2199                        -directory $rm_dir *]
2200
2201                foreach name $all_remotes {
2202                        catch {
2203                                set fd [open [file join $rm_dir $name] r]
2204                                while {[gets $fd line] >= 0} {
2205                                        if {![regexp {^Pull:[   ]*([^:]+):(.+)$} \
2206                                                $line line src dst]} continue
2207                                        if {![regexp ^refs/ $dst]} {
2208                                                set dst "refs/heads/$dst"
2209                                        }
2210                                        set tracking_branches($dst) [list $name $src]
2211                                }
2212                                close $fd
2213                        }
2214                }
2215        }
2216
2217        foreach line [array names repo_config remote.*.url] {
2218                if {![regexp ^remote\.(.*)\.url\$ $line line name]} continue
2219                lappend all_remotes $name
2220
2221                if {[catch {set fl $repo_config(remote.$name.fetch)}]} {
2222                        set fl {}
2223                }
2224                foreach line $fl {
2225                        if {![regexp {^([^:]+):(.+)$} $line line src dst]} continue
2226                        if {![regexp ^refs/ $dst]} {
2227                                set dst "refs/heads/$dst"
2228                        }
2229                        set tracking_branches($dst) [list $name $src]
2230                }
2231        }
2232
2233        set all_remotes [lsort -unique $all_remotes]
2234}
2235
2236proc populate_fetch_menu {m} {
2237        global all_remotes repo_config
2238
2239        foreach r $all_remotes {
2240                set enable 0
2241                if {![catch {set a $repo_config(remote.$r.url)}]} {
2242                        if {![catch {set a $repo_config(remote.$r.fetch)}]} {
2243                                set enable 1
2244                        }
2245                } else {
2246                        catch {
2247                                set fd [open [gitdir remotes $r] r]
2248                                while {[gets $fd n] >= 0} {
2249                                        if {[regexp {^Pull:[ \t]*([^:]+):} $n]} {
2250                                                set enable 1
2251                                                break
2252                                        }
2253                                }
2254                                close $fd
2255                        }
2256                }
2257
2258                if {$enable} {
2259                        $m add command \
2260                                -label "Fetch from $r..." \
2261                                -command [list fetch_from $r] \
2262                                -font font_ui
2263                }
2264        }
2265}
2266
2267proc populate_push_menu {m} {
2268        global all_remotes repo_config
2269
2270        foreach r $all_remotes {
2271                set enable 0
2272                if {![catch {set a $repo_config(remote.$r.url)}]} {
2273                        if {![catch {set a $repo_config(remote.$r.push)}]} {
2274                                set enable 1
2275                        }
2276                } else {
2277                        catch {
2278                                set fd [open [gitdir remotes $r] r]
2279                                while {[gets $fd n] >= 0} {
2280                                        if {[regexp {^Push:[ \t]*([^:]+):} $n]} {
2281                                                set enable 1
2282                                                break
2283                                        }
2284                                }
2285                                close $fd
2286                        }
2287                }
2288
2289                if {$enable} {
2290                        $m add command \
2291                                -label "Push to $r..." \
2292                                -command [list push_to $r] \
2293                                -font font_ui
2294                }
2295        }
2296}
2297
2298proc populate_pull_menu {m} {
2299        global repo_config all_remotes disable_on_lock
2300
2301        foreach remote $all_remotes {
2302                set rb_list [list]
2303                if {[array get repo_config remote.$remote.url] ne {}} {
2304                        if {[array get repo_config remote.$remote.fetch] ne {}} {
2305                                foreach line $repo_config(remote.$remote.fetch) {
2306                                        if {[regexp {^([^:]+):} $line line rb]} {
2307                                                lappend rb_list $rb
2308                                        }
2309                                }
2310                        }
2311                } else {
2312                        catch {
2313                                set fd [open [gitdir remotes $remote] r]
2314                                while {[gets $fd line] >= 0} {
2315                                        if {[regexp {^Pull:[ \t]*([^:]+):} $line line rb]} {
2316                                                lappend rb_list $rb
2317                                        }
2318                                }
2319                                close $fd
2320                        }
2321                }
2322
2323                foreach rb $rb_list {
2324                        regsub ^refs/heads/ $rb {} rb_short
2325                        $m add command \
2326                                -label "Branch $rb_short from $remote..." \
2327                                -command [list pull_remote $remote $rb] \
2328                                -font font_ui
2329                        lappend disable_on_lock \
2330                                [list $m entryconf [$m index last] -state]
2331                }
2332        }
2333}
2334
2335######################################################################
2336##
2337## icons
2338
2339set filemask {
2340#define mask_width 14
2341#define mask_height 15
2342static unsigned char mask_bits[] = {
2343   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
2344   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
2345   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f};
2346}
2347
2348image create bitmap file_plain -background white -foreground black -data {
2349#define plain_width 14
2350#define plain_height 15
2351static unsigned char plain_bits[] = {
2352   0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
2353   0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10,
2354   0x02, 0x10, 0x02, 0x10, 0xfe, 0x1f};
2355} -maskdata $filemask
2356
2357image create bitmap file_mod -background white -foreground blue -data {
2358#define mod_width 14
2359#define mod_height 15
2360static unsigned char mod_bits[] = {
2361   0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
2362   0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
2363   0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
2364} -maskdata $filemask
2365
2366image create bitmap file_fulltick -background white -foreground "#007000" -data {
2367#define file_fulltick_width 14
2368#define file_fulltick_height 15
2369static unsigned char file_fulltick_bits[] = {
2370   0xfe, 0x01, 0x02, 0x1a, 0x02, 0x0c, 0x02, 0x0c, 0x02, 0x16, 0x02, 0x16,
2371   0x02, 0x13, 0x00, 0x13, 0x86, 0x11, 0x8c, 0x11, 0xd8, 0x10, 0xf2, 0x10,
2372   0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
2373} -maskdata $filemask
2374
2375image create bitmap file_parttick -background white -foreground "#005050" -data {
2376#define parttick_width 14
2377#define parttick_height 15
2378static unsigned char parttick_bits[] = {
2379   0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
2380   0x7a, 0x14, 0x02, 0x16, 0x02, 0x13, 0x8a, 0x11, 0xda, 0x10, 0x72, 0x10,
2381   0x22, 0x10, 0x02, 0x10, 0xfe, 0x1f};
2382} -maskdata $filemask
2383
2384image create bitmap file_question -background white -foreground black -data {
2385#define file_question_width 14
2386#define file_question_height 15
2387static unsigned char file_question_bits[] = {
2388   0xfe, 0x01, 0x02, 0x02, 0xe2, 0x04, 0xf2, 0x09, 0x1a, 0x1b, 0x0a, 0x13,
2389   0x82, 0x11, 0xc2, 0x10, 0x62, 0x10, 0x62, 0x10, 0x02, 0x10, 0x62, 0x10,
2390   0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
2391} -maskdata $filemask
2392
2393image create bitmap file_removed -background white -foreground red -data {
2394#define file_removed_width 14
2395#define file_removed_height 15
2396static unsigned char file_removed_bits[] = {
2397   0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
2398   0x1a, 0x16, 0x32, 0x13, 0xe2, 0x11, 0xc2, 0x10, 0xe2, 0x11, 0x32, 0x13,
2399   0x1a, 0x16, 0x02, 0x10, 0xfe, 0x1f};
2400} -maskdata $filemask
2401
2402image create bitmap file_merge -background white -foreground blue -data {
2403#define file_merge_width 14
2404#define file_merge_height 15
2405static unsigned char file_merge_bits[] = {
2406   0xfe, 0x01, 0x02, 0x03, 0x62, 0x05, 0x62, 0x09, 0x62, 0x1f, 0x62, 0x10,
2407   0xfa, 0x11, 0xf2, 0x10, 0x62, 0x10, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
2408   0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
2409} -maskdata $filemask
2410
2411set ui_index .vpane.files.index.list
2412set ui_workdir .vpane.files.workdir.list
2413
2414set all_icons(_$ui_index)   file_plain
2415set all_icons(A$ui_index)   file_fulltick
2416set all_icons(M$ui_index)   file_fulltick
2417set all_icons(D$ui_index)   file_removed
2418set all_icons(U$ui_index)   file_merge
2419
2420set all_icons(_$ui_workdir) file_plain
2421set all_icons(M$ui_workdir) file_mod
2422set all_icons(D$ui_workdir) file_question
2423set all_icons(U$ui_workdir) file_merge
2424set all_icons(O$ui_workdir) file_plain
2425
2426set max_status_desc 0
2427foreach i {
2428                {__ "Unmodified"}
2429
2430                {_M "Modified, not staged"}
2431                {M_ "Staged for commit"}
2432                {MM "Portions staged for commit"}
2433                {MD "Staged for commit, missing"}
2434
2435                {_O "Untracked, not staged"}
2436                {A_ "Staged for commit"}
2437                {AM "Portions staged for commit"}
2438                {AD "Staged for commit, missing"}
2439
2440                {_D "Missing"}
2441                {D_ "Staged for removal"}
2442                {DO "Staged for removal, still present"}
2443
2444                {U_ "Requires merge resolution"}
2445                {UU "Requires merge resolution"}
2446                {UM "Requires merge resolution"}
2447                {UD "Requires merge resolution"}
2448        } {
2449        if {$max_status_desc < [string length [lindex $i 1]]} {
2450                set max_status_desc [string length [lindex $i 1]]
2451        }
2452        set all_descs([lindex $i 0]) [lindex $i 1]
2453}
2454unset i
2455
2456######################################################################
2457##
2458## util
2459
2460proc is_MacOSX {} {
2461        global tcl_platform tk_library
2462        if {[tk windowingsystem] eq {aqua}} {
2463                return 1
2464        }
2465        return 0
2466}
2467
2468proc is_Windows {} {
2469        global tcl_platform
2470        if {$tcl_platform(platform) eq {windows}} {
2471                return 1
2472        }
2473        return 0
2474}
2475
2476proc bind_button3 {w cmd} {
2477        bind $w <Any-Button-3> $cmd
2478        if {[is_MacOSX]} {
2479                bind $w <Control-Button-1> $cmd
2480        }
2481}
2482
2483proc incr_font_size {font {amt 1}} {
2484        set sz [font configure $font -size]
2485        incr sz $amt
2486        font configure $font -size $sz
2487        font configure ${font}bold -size $sz
2488}
2489
2490proc hook_failed_popup {hook msg} {
2491        set w .hookfail
2492        toplevel $w
2493
2494        frame $w.m
2495        label $w.m.l1 -text "$hook hook failed:" \
2496                -anchor w \
2497                -justify left \
2498                -font font_uibold
2499        text $w.m.t \
2500                -background white -borderwidth 1 \
2501                -relief sunken \
2502                -width 80 -height 10 \
2503                -font font_diff \
2504                -yscrollcommand [list $w.m.sby set]
2505        label $w.m.l2 \
2506                -text {You must correct the above errors before committing.} \
2507                -anchor w \
2508                -justify left \
2509                -font font_uibold
2510        scrollbar $w.m.sby -command [list $w.m.t yview]
2511        pack $w.m.l1 -side top -fill x
2512        pack $w.m.l2 -side bottom -fill x
2513        pack $w.m.sby -side right -fill y
2514        pack $w.m.t -side left -fill both -expand 1
2515        pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
2516
2517        $w.m.t insert 1.0 $msg
2518        $w.m.t conf -state disabled
2519
2520        button $w.ok -text OK \
2521                -width 15 \
2522                -font font_ui \
2523                -command "destroy $w"
2524        pack $w.ok -side bottom -anchor e -pady 10 -padx 10
2525
2526        bind $w <Visibility> "grab $w; focus $w"
2527        bind $w <Key-Return> "destroy $w"
2528        wm title $w "[appname] ([reponame]): error"
2529        tkwait window $w
2530}
2531
2532set next_console_id 0
2533
2534proc new_console {short_title long_title} {
2535        global next_console_id console_data
2536        set w .console[incr next_console_id]
2537        set console_data($w) [list $short_title $long_title]
2538        return [console_init $w]
2539}
2540
2541proc console_init {w} {
2542        global console_cr console_data M1B
2543
2544        set console_cr($w) 1.0
2545        toplevel $w
2546        frame $w.m
2547        label $w.m.l1 -text "[lindex $console_data($w) 1]:" \
2548                -anchor w \
2549                -justify left \
2550                -font font_uibold
2551        text $w.m.t \
2552                -background white -borderwidth 1 \
2553                -relief sunken \
2554                -width 80 -height 10 \
2555                -font font_diff \
2556                -state disabled \
2557                -yscrollcommand [list $w.m.sby set]
2558        label $w.m.s -text {Working... please wait...} \
2559                -anchor w \
2560                -justify left \
2561                -font font_uibold
2562        scrollbar $w.m.sby -command [list $w.m.t yview]
2563        pack $w.m.l1 -side top -fill x
2564        pack $w.m.s -side bottom -fill x
2565        pack $w.m.sby -side right -fill y
2566        pack $w.m.t -side left -fill both -expand 1
2567        pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
2568
2569        menu $w.ctxm -tearoff 0
2570        $w.ctxm add command -label "Copy" \
2571                -font font_ui \
2572                -command "tk_textCopy $w.m.t"
2573        $w.ctxm add command -label "Select All" \
2574                -font font_ui \
2575                -command "focus $w.m.t;$w.m.t tag add sel 0.0 end"
2576        $w.ctxm add command -label "Copy All" \
2577                -font font_ui \
2578                -command "
2579                        $w.m.t tag add sel 0.0 end
2580                        tk_textCopy $w.m.t
2581                        $w.m.t tag remove sel 0.0 end
2582                "
2583
2584        button $w.ok -text {Close} \
2585                -font font_ui \
2586                -state disabled \
2587                -command "destroy $w"
2588        pack $w.ok -side bottom -anchor e -pady 10 -padx 10
2589
2590        bind_button3 $w.m.t "tk_popup $w.ctxm %X %Y"
2591        bind $w.m.t <$M1B-Key-a> "$w.m.t tag add sel 0.0 end;break"
2592        bind $w.m.t <$M1B-Key-A> "$w.m.t tag add sel 0.0 end;break"
2593        bind $w <Visibility> "focus $w"
2594        wm title $w "[appname] ([reponame]): [lindex $console_data($w) 0]"
2595        return $w
2596}
2597
2598proc console_exec {w cmd {after {}}} {
2599        # -- Windows tosses the enviroment when we exec our child.
2600        #    But most users need that so we have to relogin. :-(
2601        #
2602        if {[is_Windows]} {
2603                set cmd [list sh --login -c "cd \"[pwd]\" && [join $cmd { }]"]
2604        }
2605
2606        # -- Tcl won't let us redirect both stdout and stderr to
2607        #    the same pipe.  So pass it through cat...
2608        #
2609        set cmd [concat | $cmd |& cat]
2610
2611        set fd_f [open $cmd r]
2612        fconfigure $fd_f -blocking 0 -translation binary
2613        fileevent $fd_f readable [list console_read $w $fd_f $after]
2614}
2615
2616proc console_read {w fd after} {
2617        global console_cr console_data
2618
2619        set buf [read $fd]
2620        if {$buf ne {}} {
2621                if {![winfo exists $w]} {console_init $w}
2622                $w.m.t conf -state normal
2623                set c 0
2624                set n [string length $buf]
2625                while {$c < $n} {
2626                        set cr [string first "\r" $buf $c]
2627                        set lf [string first "\n" $buf $c]
2628                        if {$cr < 0} {set cr [expr {$n + 1}]}
2629                        if {$lf < 0} {set lf [expr {$n + 1}]}
2630
2631                        if {$lf < $cr} {
2632                                $w.m.t insert end [string range $buf $c $lf]
2633                                set console_cr($w) [$w.m.t index {end -1c}]
2634                                set c $lf
2635                                incr c
2636                        } else {
2637                                $w.m.t delete $console_cr($w) end
2638                                $w.m.t insert end "\n"
2639                                $w.m.t insert end [string range $buf $c $cr]
2640                                set c $cr
2641                                incr c
2642                        }
2643                }
2644                $w.m.t conf -state disabled
2645                $w.m.t see end
2646        }
2647
2648        fconfigure $fd -blocking 1
2649        if {[eof $fd]} {
2650                if {[catch {close $fd}]} {
2651                        if {![winfo exists $w]} {console_init $w}
2652                        $w.m.s conf -background red -text {Error: Command Failed}
2653                        $w.ok conf -state normal
2654                        set ok 0
2655                } elseif {[winfo exists $w]} {
2656                        $w.m.s conf -background green -text {Success}
2657                        $w.ok conf -state normal
2658                        set ok 1
2659                }
2660                array unset console_cr $w
2661                array unset console_data $w
2662                if {$after ne {}} {
2663                        uplevel #0 $after $ok
2664                }
2665                return
2666        }
2667        fconfigure $fd -blocking 0
2668}
2669
2670######################################################################
2671##
2672## ui commands
2673
2674set starting_gitk_msg {Starting gitk... please wait...}
2675
2676proc do_gitk {revs} {
2677        global ui_status_value starting_gitk_msg
2678
2679        set cmd gitk
2680        if {$revs ne {}} {
2681                append cmd { }
2682                append cmd $revs
2683        }
2684        if {[is_Windows]} {
2685                set cmd "sh -c \"exec $cmd\""
2686        }
2687        append cmd { &}
2688
2689        if {[catch {eval exec $cmd} err]} {
2690                error_popup "Failed to start gitk:\n\n$err"
2691        } else {
2692                set ui_status_value $starting_gitk_msg
2693                after 10000 {
2694                        if {$ui_status_value eq $starting_gitk_msg} {
2695                                set ui_status_value {Ready.}
2696                        }
2697                }
2698        }
2699}
2700
2701proc do_gc {} {
2702        set w [new_console {gc} {Compressing the object database}]
2703        console_exec $w {git gc}
2704}
2705
2706proc do_fsck_objects {} {
2707        set w [new_console {fsck-objects} \
2708                {Verifying the object database with fsck-objects}]
2709        set cmd [list git fsck-objects]
2710        lappend cmd --full
2711        lappend cmd --cache
2712        lappend cmd --strict
2713        console_exec $w $cmd
2714}
2715
2716set is_quitting 0
2717
2718proc do_quit {} {
2719        global ui_comm is_quitting repo_config commit_type
2720
2721        if {$is_quitting} return
2722        set is_quitting 1
2723
2724        # -- Stash our current commit buffer.
2725        #
2726        set save [gitdir GITGUI_MSG]
2727        set msg [string trim [$ui_comm get 0.0 end]]
2728        if {![string match amend* $commit_type]
2729                && [$ui_comm edit modified]
2730                && $msg ne {}} {
2731                catch {
2732                        set fd [open $save w]
2733                        puts $fd [string trim [$ui_comm get 0.0 end]]
2734                        close $fd
2735                }
2736        } else {
2737                catch {file delete $save}
2738        }
2739
2740        # -- Stash our current window geometry into this repository.
2741        #
2742        set cfg_geometry [list]
2743        lappend cfg_geometry [wm geometry .]
2744        lappend cfg_geometry [lindex [.vpane sash coord 0] 1]
2745        lappend cfg_geometry [lindex [.vpane.files sash coord 0] 0]
2746        if {[catch {set rc_geometry $repo_config(gui.geometry)}]} {
2747                set rc_geometry {}
2748        }
2749        if {$cfg_geometry ne $rc_geometry} {
2750                catch {exec git repo-config gui.geometry $cfg_geometry}
2751        }
2752
2753        destroy .
2754}
2755
2756proc do_rescan {} {
2757        rescan {set ui_status_value {Ready.}}
2758}
2759
2760proc unstage_helper {txt paths} {
2761        global file_states current_diff_path
2762
2763        if {![lock_index begin-update]} return
2764
2765        set pathList [list]
2766        set after {}
2767        foreach path $paths {
2768                switch -glob -- [lindex $file_states($path) 0] {
2769                A? -
2770                M? -
2771                D? {
2772                        lappend pathList $path
2773                        if {$path eq $current_diff_path} {
2774                                set after {reshow_diff;}
2775                        }
2776                }
2777                }
2778        }
2779        if {$pathList eq {}} {
2780                unlock_index
2781        } else {
2782                update_indexinfo \
2783                        $txt \
2784                        $pathList \
2785                        [concat $after {set ui_status_value {Ready.}}]
2786        }
2787}
2788
2789proc do_unstage_selection {} {
2790        global current_diff_path selected_paths
2791
2792        if {[array size selected_paths] > 0} {
2793                unstage_helper \
2794                        {Unstaging selected files from commit} \
2795                        [array names selected_paths]
2796        } elseif {$current_diff_path ne {}} {
2797                unstage_helper \
2798                        "Unstaging [short_path $current_diff_path] from commit" \
2799                        [list $current_diff_path]
2800        }
2801}
2802
2803proc add_helper {txt paths} {
2804        global file_states current_diff_path
2805
2806        if {![lock_index begin-update]} return
2807
2808        set pathList [list]
2809        set after {}
2810        foreach path $paths {
2811                switch -glob -- [lindex $file_states($path) 0] {
2812                _O -
2813                ?M -
2814                ?D -
2815                U? {
2816                        lappend pathList $path
2817                        if {$path eq $current_diff_path} {
2818                                set after {reshow_diff;}
2819                        }
2820                }
2821                }
2822        }
2823        if {$pathList eq {}} {
2824                unlock_index
2825        } else {
2826                update_index \
2827                        $txt \
2828                        $pathList \
2829                        [concat $after {set ui_status_value {Ready to commit.}}]
2830        }
2831}
2832
2833proc do_add_selection {} {
2834        global current_diff_path selected_paths
2835
2836        if {[array size selected_paths] > 0} {
2837                add_helper \
2838                        {Adding selected files} \
2839                        [array names selected_paths]
2840        } elseif {$current_diff_path ne {}} {
2841                add_helper \
2842                        "Adding [short_path $current_diff_path]" \
2843                        [list $current_diff_path]
2844        }
2845}
2846
2847proc do_add_all {} {
2848        global file_states
2849
2850        set paths [list]
2851        foreach path [array names file_states] {
2852                switch -glob -- [lindex $file_states($path) 0] {
2853                U? {continue}
2854                ?M -
2855                ?D {lappend paths $path}
2856                }
2857        }
2858        add_helper {Adding all changed files} $paths
2859}
2860
2861proc revert_helper {txt paths} {
2862        global file_states current_diff_path
2863
2864        if {![lock_index begin-update]} return
2865
2866        set pathList [list]
2867        set after {}
2868        foreach path $paths {
2869                switch -glob -- [lindex $file_states($path) 0] {
2870                U? {continue}
2871                ?M -
2872                ?D {
2873                        lappend pathList $path
2874                        if {$path eq $current_diff_path} {
2875                                set after {reshow_diff;}
2876                        }
2877                }
2878                }
2879        }
2880
2881        set n [llength $pathList]
2882        if {$n == 0} {
2883                unlock_index
2884                return
2885        } elseif {$n == 1} {
2886                set s "[short_path [lindex $pathList]]"
2887        } else {
2888                set s "these $n files"
2889        }
2890
2891        set reply [tk_dialog \
2892                .confirm_revert \
2893                "[appname] ([reponame])" \
2894                "Revert changes in $s?
2895
2896Any unadded changes will be permanently lost by the revert." \
2897                question \
2898                1 \
2899                {Do Nothing} \
2900                {Revert Changes} \
2901                ]
2902        if {$reply == 1} {
2903                checkout_index \
2904                        $txt \
2905                        $pathList \
2906                        [concat $after {set ui_status_value {Ready.}}]
2907        } else {
2908                unlock_index
2909        }
2910}
2911
2912proc do_revert_selection {} {
2913        global current_diff_path selected_paths
2914
2915        if {[array size selected_paths] > 0} {
2916                revert_helper \
2917                        {Reverting selected files} \
2918                        [array names selected_paths]
2919        } elseif {$current_diff_path ne {}} {
2920                revert_helper \
2921                        "Reverting [short_path $current_diff_path]" \
2922                        [list $current_diff_path]
2923        }
2924}
2925
2926proc do_signoff {} {
2927        global ui_comm
2928
2929        set me [committer_ident]
2930        if {$me eq {}} return
2931
2932        set sob "Signed-off-by: $me"
2933        set last [$ui_comm get {end -1c linestart} {end -1c}]
2934        if {$last ne $sob} {
2935                $ui_comm edit separator
2936                if {$last ne {}
2937                        && ![regexp {^[A-Z][A-Za-z]*-[A-Za-z-]+: *} $last]} {
2938                        $ui_comm insert end "\n"
2939                }
2940                $ui_comm insert end "\n$sob"
2941                $ui_comm edit separator
2942                $ui_comm see end
2943        }
2944}
2945
2946proc do_select_commit_type {} {
2947        global commit_type selected_commit_type
2948
2949        if {$selected_commit_type eq {new}
2950                && [string match amend* $commit_type]} {
2951                create_new_commit
2952        } elseif {$selected_commit_type eq {amend}
2953                && ![string match amend* $commit_type]} {
2954                load_last_commit
2955
2956                # The amend request was rejected...
2957                #
2958                if {![string match amend* $commit_type]} {
2959                        set selected_commit_type new
2960                }
2961        }
2962}
2963
2964proc do_commit {} {
2965        commit_tree
2966}
2967
2968proc do_about {} {
2969        global appvers copyright
2970        global tcl_patchLevel tk_patchLevel
2971
2972        set w .about_dialog
2973        toplevel $w
2974        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
2975
2976        label $w.header -text "About [appname]" \
2977                -font font_uibold
2978        pack $w.header -side top -fill x
2979
2980        frame $w.buttons
2981        button $w.buttons.close -text {Close} \
2982                -font font_ui \
2983                -command [list destroy $w]
2984        pack $w.buttons.close -side right
2985        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
2986
2987        label $w.desc \
2988                -text "[appname] - a commit creation tool for Git.
2989$copyright" \
2990                -padx 5 -pady 5 \
2991                -justify left \
2992                -anchor w \
2993                -borderwidth 1 \
2994                -relief solid \
2995                -font font_ui
2996        pack $w.desc -side top -fill x -padx 5 -pady 5
2997
2998        set v {}
2999        append v "[appname] version $appvers\n"
3000        append v "[exec git version]\n"
3001        append v "\n"
3002        if {$tcl_patchLevel eq $tk_patchLevel} {
3003                append v "Tcl/Tk version $tcl_patchLevel"
3004        } else {
3005                append v "Tcl version $tcl_patchLevel"
3006                append v ", Tk version $tk_patchLevel"
3007        }
3008
3009        label $w.vers \
3010                -text $v \
3011                -padx 5 -pady 5 \
3012                -justify left \
3013                -anchor w \
3014                -borderwidth 1 \
3015                -relief solid \
3016                -font font_ui
3017        pack $w.vers -side top -fill x -padx 5 -pady 5
3018
3019        menu $w.ctxm -tearoff 0
3020        $w.ctxm add command \
3021                -label {Copy} \
3022                -font font_ui \
3023                -command "
3024                clipboard clear
3025                clipboard append -format STRING -type STRING -- \[$w.vers cget -text\]
3026        "
3027
3028        bind $w <Visibility> "grab $w; focus $w"
3029        bind $w <Key-Escape> "destroy $w"
3030        bind_button3 $w.vers "tk_popup $w.ctxm %X %Y; grab $w; focus $w"
3031        wm title $w "About [appname]"
3032        tkwait window $w
3033}
3034
3035proc do_options {} {
3036        global repo_config global_config font_descs
3037        global repo_config_new global_config_new
3038
3039        array unset repo_config_new
3040        array unset global_config_new
3041        foreach name [array names repo_config] {
3042                set repo_config_new($name) $repo_config($name)
3043        }
3044        load_config 1
3045        foreach name [array names repo_config] {
3046                switch -- $name {
3047                gui.diffcontext {continue}
3048                }
3049                set repo_config_new($name) $repo_config($name)
3050        }
3051        foreach name [array names global_config] {
3052                set global_config_new($name) $global_config($name)
3053        }
3054
3055        set w .options_editor
3056        toplevel $w
3057        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
3058
3059        label $w.header -text "[appname] Options" \
3060                -font font_uibold
3061        pack $w.header -side top -fill x
3062
3063        frame $w.buttons
3064        button $w.buttons.restore -text {Restore Defaults} \
3065                -font font_ui \
3066                -command do_restore_defaults
3067        pack $w.buttons.restore -side left
3068        button $w.buttons.save -text Save \
3069                -font font_ui \
3070                -command "
3071                        catch {eval \[bind \[focus -displayof $w\] <FocusOut>\]}
3072                        do_save_config $w
3073                "
3074        pack $w.buttons.save -side right
3075        button $w.buttons.cancel -text {Cancel} \
3076                -font font_ui \
3077                -command [list destroy $w]
3078        pack $w.buttons.cancel -side right -padx 5
3079        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
3080
3081        labelframe $w.repo -text "[reponame] Repository" \
3082                -font font_ui \
3083                -relief raised -borderwidth 2
3084        labelframe $w.global -text {Global (All Repositories)} \
3085                -font font_ui \
3086                -relief raised -borderwidth 2
3087        pack $w.repo -side left -fill both -expand 1 -pady 5 -padx 5
3088        pack $w.global -side right -fill both -expand 1 -pady 5 -padx 5
3089
3090        foreach option {
3091                {b pullsummary {Show Pull Summary}}
3092                {b trustmtime  {Trust File Modification Timestamps}}
3093                {i diffcontext {Number of Diff Context Lines}}
3094                {t newbranchtemplate {New Branch Name Template}}
3095                } {
3096                set type [lindex $option 0]
3097                set name [lindex $option 1]
3098                set text [lindex $option 2]
3099                foreach f {repo global} {
3100                        switch $type {
3101                        b {
3102                                checkbutton $w.$f.$name -text $text \
3103                                        -variable ${f}_config_new(gui.$name) \
3104                                        -onvalue true \
3105                                        -offvalue false \
3106                                        -font font_ui
3107                                pack $w.$f.$name -side top -anchor w
3108                        }
3109                        i {
3110                                frame $w.$f.$name
3111                                label $w.$f.$name.l -text "$text:" -font font_ui
3112                                pack $w.$f.$name.l -side left -anchor w -fill x
3113                                spinbox $w.$f.$name.v \
3114                                        -textvariable ${f}_config_new(gui.$name) \
3115                                        -from 1 -to 99 -increment 1 \
3116                                        -width 3 \
3117                                        -font font_ui
3118                                bind $w.$f.$name.v <FocusIn> {%W selection range 0 end}
3119                                pack $w.$f.$name.v -side right -anchor e -padx 5
3120                                pack $w.$f.$name -side top -anchor w -fill x
3121                        }
3122                        t {
3123                                frame $w.$f.$name
3124                                label $w.$f.$name.l -text "$text:" -font font_ui
3125                                text $w.$f.$name.v \
3126                                        -borderwidth 1 \
3127                                        -relief sunken \
3128                                        -height 1 \
3129                                        -width 20 \
3130                                        -font font_ui
3131                                $w.$f.$name.v insert 0.0 [set ${f}_config_new(gui.$name)]
3132                                bind $w.$f.$name.v <Shift-Key-Tab> {focus [tk_focusPrev %W];break}
3133                                bind $w.$f.$name.v <Key-Tab> {focus [tk_focusNext %W];break}
3134                                bind $w.$f.$name.v <Key-Return> break
3135                                bind $w.$f.$name.v <FocusIn> "$w.$f.$name.v tag add sel 0.0 end"
3136                                bind $w.$f.$name.v <FocusOut> "
3137                                        set ${f}_config_new(gui.$name) \
3138                                        \[string trim \[$w.$f.$name.v get 0.0 end\]\]
3139                                "
3140                                pack $w.$f.$name.l -side left -anchor w
3141                                pack $w.$f.$name.v -side left -anchor w \
3142                                        -fill x -expand 1 \
3143                                        -padx 5
3144                                pack $w.$f.$name -side top -anchor w -fill x
3145                        }
3146                        }
3147                }
3148        }
3149
3150        set all_fonts [lsort [font families]]
3151        foreach option $font_descs {
3152                set name [lindex $option 0]
3153                set font [lindex $option 1]
3154                set text [lindex $option 2]
3155
3156                set global_config_new(gui.$font^^family) \
3157                        [font configure $font -family]
3158                set global_config_new(gui.$font^^size) \
3159                        [font configure $font -size]
3160
3161                frame $w.global.$name
3162                label $w.global.$name.l -text "$text:" -font font_ui
3163                pack $w.global.$name.l -side left -anchor w -fill x
3164                eval tk_optionMenu $w.global.$name.family \
3165                        global_config_new(gui.$font^^family) \
3166                        $all_fonts
3167                spinbox $w.global.$name.size \
3168                        -textvariable global_config_new(gui.$font^^size) \
3169                        -from 2 -to 80 -increment 1 \
3170                        -width 3 \
3171                        -font font_ui
3172                bind $w.global.$name.size <FocusIn> {%W selection range 0 end}
3173                pack $w.global.$name.size -side right -anchor e
3174                pack $w.global.$name.family -side right -anchor e
3175                pack $w.global.$name -side top -anchor w -fill x
3176        }
3177
3178        bind $w <Visibility> "grab $w; focus $w"
3179        bind $w <Key-Escape> "destroy $w"
3180        wm title $w "[appname] ([reponame]): Options"
3181        tkwait window $w
3182}
3183
3184proc do_restore_defaults {} {
3185        global font_descs default_config repo_config
3186        global repo_config_new global_config_new
3187
3188        foreach name [array names default_config] {
3189                set repo_config_new($name) $default_config($name)
3190                set global_config_new($name) $default_config($name)
3191        }
3192
3193        foreach option $font_descs {
3194                set name [lindex $option 0]
3195                set repo_config(gui.$name) $default_config(gui.$name)
3196        }
3197        apply_config
3198
3199        foreach option $font_descs {
3200                set name [lindex $option 0]
3201                set font [lindex $option 1]
3202                set global_config_new(gui.$font^^family) \
3203                        [font configure $font -family]
3204                set global_config_new(gui.$font^^size) \
3205                        [font configure $font -size]
3206        }
3207}
3208
3209proc do_save_config {w} {
3210        if {[catch {save_config} err]} {
3211                error_popup "Failed to completely save options:\n\n$err"
3212        }
3213        reshow_diff
3214        destroy $w
3215}
3216
3217proc do_windows_shortcut {} {
3218        global argv0
3219
3220        if {[catch {
3221                set desktop [exec cygpath \
3222                        --windows \
3223                        --absolute \
3224                        --long-name \
3225                        --desktop]
3226                }]} {
3227                        set desktop .
3228        }
3229        set fn [tk_getSaveFile \
3230                -parent . \
3231                -title "[appname] ([reponame]): Create Desktop Icon" \
3232                -initialdir $desktop \
3233                -initialfile "Git [reponame].bat"]
3234        if {$fn != {}} {
3235                if {[catch {
3236                                set fd [open $fn w]
3237                                set sh [exec cygpath \
3238                                        --windows \
3239                                        --absolute \
3240                                        /bin/sh]
3241                                set me [exec cygpath \
3242                                        --unix \
3243                                        --absolute \
3244                                        $argv0]
3245                                set gd [exec cygpath \
3246                                        --unix \
3247                                        --absolute \
3248                                        [gitdir]]
3249                                set gw [exec cygpath \
3250                                        --windows \
3251                                        --absolute \
3252                                        [file dirname [gitdir]]]
3253                                regsub -all ' $me "'\\''" me
3254                                regsub -all ' $gd "'\\''" gd
3255                                puts $fd "@ECHO Entering $gw"
3256                                puts $fd "@ECHO Starting git-gui... please wait..."
3257                                puts -nonewline $fd "@\"$sh\" --login -c \""
3258                                puts -nonewline $fd "GIT_DIR='$gd'"
3259                                puts -nonewline $fd " '$me'"
3260                                puts $fd "&\""
3261                                close $fd
3262                        } err]} {
3263                        error_popup "Cannot write script:\n\n$err"
3264                }
3265        }
3266}
3267
3268proc do_macosx_app {} {
3269        global argv0 env
3270
3271        set fn [tk_getSaveFile \
3272                -parent . \
3273                -title "[appname] ([reponame]): Create Desktop Icon" \
3274                -initialdir [file join $env(HOME) Desktop] \
3275                -initialfile "Git [reponame].app"]
3276        if {$fn != {}} {
3277                if {[catch {
3278                                set Contents [file join $fn Contents]
3279                                set MacOS [file join $Contents MacOS]
3280                                set exe [file join $MacOS git-gui]
3281
3282                                file mkdir $MacOS
3283
3284                                set fd [open [file join $Contents Info.plist] w]
3285                                puts $fd {<?xml version="1.0" encoding="UTF-8"?>
3286<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3287<plist version="1.0">
3288<dict>
3289        <key>CFBundleDevelopmentRegion</key>
3290        <string>English</string>
3291        <key>CFBundleExecutable</key>
3292        <string>git-gui</string>
3293        <key>CFBundleIdentifier</key>
3294        <string>org.spearce.git-gui</string>
3295        <key>CFBundleInfoDictionaryVersion</key>
3296        <string>6.0</string>
3297        <key>CFBundlePackageType</key>
3298        <string>APPL</string>
3299        <key>CFBundleSignature</key>
3300        <string>????</string>
3301        <key>CFBundleVersion</key>
3302        <string>1.0</string>
3303        <key>NSPrincipalClass</key>
3304        <string>NSApplication</string>
3305</dict>
3306</plist>}
3307                                close $fd
3308
3309                                set fd [open $exe w]
3310                                set gd [file normalize [gitdir]]
3311                                set ep [file normalize [exec git --exec-path]]
3312                                regsub -all ' $gd "'\\''" gd
3313                                regsub -all ' $ep "'\\''" ep
3314                                puts $fd "#!/bin/sh"
3315                                foreach name [array names env] {
3316                                        if {[string match GIT_* $name]} {
3317                                                regsub -all ' $env($name) "'\\''" v
3318                                                puts $fd "export $name='$v'"
3319                                        }
3320                                }
3321                                puts $fd "export PATH='$ep':\$PATH"
3322                                puts $fd "export GIT_DIR='$gd'"
3323                                puts $fd "exec [file normalize $argv0]"
3324                                close $fd
3325
3326                                file attributes $exe -permissions u+x,g+x,o+x
3327                        } err]} {
3328                        error_popup "Cannot write icon:\n\n$err"
3329                }
3330        }
3331}
3332
3333proc toggle_or_diff {w x y} {
3334        global file_states file_lists current_diff_path ui_index ui_workdir
3335        global last_clicked selected_paths
3336
3337        set pos [split [$w index @$x,$y] .]
3338        set lno [lindex $pos 0]
3339        set col [lindex $pos 1]
3340        set path [lindex $file_lists($w) [expr {$lno - 1}]]
3341        if {$path eq {}} {
3342                set last_clicked {}
3343                return
3344        }
3345
3346        set last_clicked [list $w $lno]
3347        array unset selected_paths
3348        $ui_index tag remove in_sel 0.0 end
3349        $ui_workdir tag remove in_sel 0.0 end
3350
3351        if {$col == 0} {
3352                if {$current_diff_path eq $path} {
3353                        set after {reshow_diff;}
3354                } else {
3355                        set after {}
3356                }
3357                if {$w eq $ui_index} {
3358                        update_indexinfo \
3359                                "Unstaging [short_path $path] from commit" \
3360                                [list $path] \
3361                                [concat $after {set ui_status_value {Ready.}}]
3362                } elseif {$w eq $ui_workdir} {
3363                        update_index \
3364                                "Adding [short_path $path]" \
3365                                [list $path] \
3366                                [concat $after {set ui_status_value {Ready.}}]
3367                }
3368        } else {
3369                show_diff $path $w $lno
3370        }
3371}
3372
3373proc add_one_to_selection {w x y} {
3374        global file_lists last_clicked selected_paths
3375
3376        set lno [lindex [split [$w index @$x,$y] .] 0]
3377        set path [lindex $file_lists($w) [expr {$lno - 1}]]
3378        if {$path eq {}} {
3379                set last_clicked {}
3380                return
3381        }
3382
3383        if {$last_clicked ne {}
3384                && [lindex $last_clicked 0] ne $w} {
3385                array unset selected_paths
3386                [lindex $last_clicked 0] tag remove in_sel 0.0 end
3387        }
3388
3389        set last_clicked [list $w $lno]
3390        if {[catch {set in_sel $selected_paths($path)}]} {
3391                set in_sel 0
3392        }
3393        if {$in_sel} {
3394                unset selected_paths($path)
3395                $w tag remove in_sel $lno.0 [expr {$lno + 1}].0
3396        } else {
3397                set selected_paths($path) 1
3398                $w tag add in_sel $lno.0 [expr {$lno + 1}].0
3399        }
3400}
3401
3402proc add_range_to_selection {w x y} {
3403        global file_lists last_clicked selected_paths
3404
3405        if {[lindex $last_clicked 0] ne $w} {
3406                toggle_or_diff $w $x $y
3407                return
3408        }
3409
3410        set lno [lindex [split [$w index @$x,$y] .] 0]
3411        set lc [lindex $last_clicked 1]
3412        if {$lc < $lno} {
3413                set begin $lc
3414                set end $lno
3415        } else {
3416                set begin $lno
3417                set end $lc
3418        }
3419
3420        foreach path [lrange $file_lists($w) \
3421                [expr {$begin - 1}] \
3422                [expr {$end - 1}]] {
3423                set selected_paths($path) 1
3424        }
3425        $w tag add in_sel $begin.0 [expr {$end + 1}].0
3426}
3427
3428######################################################################
3429##
3430## config defaults
3431
3432set cursor_ptr arrow
3433font create font_diff -family Courier -size 10
3434font create font_ui
3435catch {
3436        label .dummy
3437        eval font configure font_ui [font actual [.dummy cget -font]]
3438        destroy .dummy
3439}
3440
3441font create font_uibold
3442font create font_diffbold
3443
3444if {[is_Windows]} {
3445        set M1B Control
3446        set M1T Ctrl
3447} elseif {[is_MacOSX]} {
3448        set M1B M1
3449        set M1T Cmd
3450} else {
3451        set M1B M1
3452        set M1T M1
3453}
3454
3455proc apply_config {} {
3456        global repo_config font_descs
3457
3458        foreach option $font_descs {
3459                set name [lindex $option 0]
3460                set font [lindex $option 1]
3461                if {[catch {
3462                        foreach {cn cv} $repo_config(gui.$name) {
3463                                font configure $font $cn $cv
3464                        }
3465                        } err]} {
3466                        error_popup "Invalid font specified in gui.$name:\n\n$err"
3467                }
3468                foreach {cn cv} [font configure $font] {
3469                        font configure ${font}bold $cn $cv
3470                }
3471                font configure ${font}bold -weight bold
3472        }
3473}
3474
3475set default_config(gui.trustmtime) false
3476set default_config(gui.pullsummary) true
3477set default_config(gui.diffcontext) 5
3478set default_config(gui.newbranchtemplate) {}
3479set default_config(gui.fontui) [font configure font_ui]
3480set default_config(gui.fontdiff) [font configure font_diff]
3481set font_descs {
3482        {fontui   font_ui   {Main Font}}
3483        {fontdiff font_diff {Diff/Console Font}}
3484}
3485load_config 0
3486apply_config
3487
3488######################################################################
3489##
3490## ui construction
3491
3492# -- Menu Bar
3493#
3494menu .mbar -tearoff 0
3495.mbar add cascade -label Repository -menu .mbar.repository
3496.mbar add cascade -label Edit -menu .mbar.edit
3497if {!$single_commit} {
3498        .mbar add cascade -label Branch -menu .mbar.branch
3499}
3500.mbar add cascade -label Commit -menu .mbar.commit
3501if {!$single_commit} {
3502        .mbar add cascade -label Fetch -menu .mbar.fetch
3503        .mbar add cascade -label Pull -menu .mbar.pull
3504        .mbar add cascade -label Push -menu .mbar.push
3505}
3506. configure -menu .mbar
3507
3508# -- Repository Menu
3509#
3510menu .mbar.repository
3511.mbar.repository add command \
3512        -label {Visualize Current Branch} \
3513        -command {do_gitk {}} \
3514        -font font_ui
3515if {![is_MacOSX]} {
3516        .mbar.repository add command \
3517                -label {Visualize All Branches} \
3518                -command {do_gitk {--all}} \
3519                -font font_ui
3520}
3521.mbar.repository add separator
3522
3523if {!$single_commit} {
3524        .mbar.repository add command -label {Compress Database} \
3525                -command do_gc \
3526                -font font_ui
3527
3528        .mbar.repository add command -label {Verify Database} \
3529                -command do_fsck_objects \
3530                -font font_ui
3531
3532        .mbar.repository add separator
3533
3534        if {[is_Windows]} {
3535                .mbar.repository add command \
3536                        -label {Create Desktop Icon} \
3537                        -command do_windows_shortcut \
3538                        -font font_ui
3539        } elseif {[is_MacOSX]} {
3540                .mbar.repository add command \
3541                        -label {Create Desktop Icon} \
3542                        -command do_macosx_app \
3543                        -font font_ui
3544        }
3545}
3546
3547.mbar.repository add command -label Quit \
3548        -command do_quit \
3549        -accelerator $M1T-Q \
3550        -font font_ui
3551
3552# -- Edit Menu
3553#
3554menu .mbar.edit
3555.mbar.edit add command -label Undo \
3556        -command {catch {[focus] edit undo}} \
3557        -accelerator $M1T-Z \
3558        -font font_ui
3559.mbar.edit add command -label Redo \
3560        -command {catch {[focus] edit redo}} \
3561        -accelerator $M1T-Y \
3562        -font font_ui
3563.mbar.edit add separator
3564.mbar.edit add command -label Cut \
3565        -command {catch {tk_textCut [focus]}} \
3566        -accelerator $M1T-X \
3567        -font font_ui
3568.mbar.edit add command -label Copy \
3569        -command {catch {tk_textCopy [focus]}} \
3570        -accelerator $M1T-C \
3571        -font font_ui
3572.mbar.edit add command -label Paste \
3573        -command {catch {tk_textPaste [focus]; [focus] see insert}} \
3574        -accelerator $M1T-V \
3575        -font font_ui
3576.mbar.edit add command -label Delete \
3577        -command {catch {[focus] delete sel.first sel.last}} \
3578        -accelerator Del \
3579        -font font_ui
3580.mbar.edit add separator
3581.mbar.edit add command -label {Select All} \
3582        -command {catch {[focus] tag add sel 0.0 end}} \
3583        -accelerator $M1T-A \
3584        -font font_ui
3585
3586# -- Branch Menu
3587#
3588if {!$single_commit} {
3589        menu .mbar.branch
3590
3591        .mbar.branch add command -label {Create...} \
3592                -command do_create_branch \
3593                -accelerator $M1T-N \
3594                -font font_ui
3595        lappend disable_on_lock [list .mbar.branch entryconf \
3596                [.mbar.branch index last] -state]
3597
3598        .mbar.branch add command -label {Delete...} \
3599                -command do_delete_branch \
3600                -font font_ui
3601        lappend disable_on_lock [list .mbar.branch entryconf \
3602                [.mbar.branch index last] -state]
3603}
3604
3605# -- Commit Menu
3606#
3607menu .mbar.commit
3608
3609.mbar.commit add radiobutton \
3610        -label {New Commit} \
3611        -command do_select_commit_type \
3612        -variable selected_commit_type \
3613        -value new \
3614        -font font_ui
3615lappend disable_on_lock \
3616        [list .mbar.commit entryconf [.mbar.commit index last] -state]
3617
3618.mbar.commit add radiobutton \
3619        -label {Amend Last Commit} \
3620        -command do_select_commit_type \
3621        -variable selected_commit_type \
3622        -value amend \
3623        -font font_ui
3624lappend disable_on_lock \
3625        [list .mbar.commit entryconf [.mbar.commit index last] -state]
3626
3627.mbar.commit add separator
3628
3629.mbar.commit add command -label Rescan \
3630        -command do_rescan \
3631        -accelerator F5 \
3632        -font font_ui
3633lappend disable_on_lock \
3634        [list .mbar.commit entryconf [.mbar.commit index last] -state]
3635
3636.mbar.commit add command -label {Add To Commit} \
3637        -command do_add_selection \
3638        -font font_ui
3639lappend disable_on_lock \
3640        [list .mbar.commit entryconf [.mbar.commit index last] -state]
3641
3642.mbar.commit add command -label {Add All To Commit} \
3643        -command do_add_all \
3644        -accelerator $M1T-I \
3645        -font font_ui
3646lappend disable_on_lock \
3647        [list .mbar.commit entryconf [.mbar.commit index last] -state]
3648
3649.mbar.commit add command -label {Unstage From Commit} \
3650        -command do_unstage_selection \
3651        -font font_ui
3652lappend disable_on_lock \
3653        [list .mbar.commit entryconf [.mbar.commit index last] -state]
3654
3655.mbar.commit add command -label {Revert Changes} \
3656        -command do_revert_selection \
3657        -font font_ui
3658lappend disable_on_lock \
3659        [list .mbar.commit entryconf [.mbar.commit index last] -state]
3660
3661.mbar.commit add separator
3662
3663.mbar.commit add command -label {Sign Off} \
3664        -command do_signoff \
3665        -accelerator $M1T-S \
3666        -font font_ui
3667
3668.mbar.commit add command -label Commit \
3669        -command do_commit \
3670        -accelerator $M1T-Return \
3671        -font font_ui
3672lappend disable_on_lock \
3673        [list .mbar.commit entryconf [.mbar.commit index last] -state]
3674
3675# -- Transport menus
3676#
3677if {!$single_commit} {
3678        menu .mbar.fetch
3679        menu .mbar.pull
3680        menu .mbar.push
3681}
3682
3683if {[is_MacOSX]} {
3684        # -- Apple Menu (Mac OS X only)
3685        #
3686        .mbar add cascade -label Apple -menu .mbar.apple
3687        menu .mbar.apple
3688
3689        .mbar.apple add command -label "About [appname]" \
3690                -command do_about \
3691                -font font_ui
3692        .mbar.apple add command -label "[appname] Options..." \
3693                -command do_options \
3694                -font font_ui
3695} else {
3696        # -- Edit Menu
3697        #
3698        .mbar.edit add separator
3699        .mbar.edit add command -label {Options...} \
3700                -command do_options \
3701                -font font_ui
3702
3703        # -- Tools Menu
3704        #
3705        if {[file exists /usr/local/miga/lib/gui-miga]
3706                && [file exists .pvcsrc]} {
3707        proc do_miga {} {
3708                global ui_status_value
3709                if {![lock_index update]} return
3710                set cmd [list sh --login -c "/usr/local/miga/lib/gui-miga \"[pwd]\""]
3711                set miga_fd [open "|$cmd" r]
3712                fconfigure $miga_fd -blocking 0
3713                fileevent $miga_fd readable [list miga_done $miga_fd]
3714                set ui_status_value {Running miga...}
3715        }
3716        proc miga_done {fd} {
3717                read $fd 512
3718                if {[eof $fd]} {
3719                        close $fd
3720                        unlock_index
3721                        rescan [list set ui_status_value {Ready.}]
3722                }
3723        }
3724        .mbar add cascade -label Tools -menu .mbar.tools
3725        menu .mbar.tools
3726        .mbar.tools add command -label "Migrate" \
3727                -command do_miga \
3728                -font font_ui
3729        lappend disable_on_lock \
3730                [list .mbar.tools entryconf [.mbar.tools index last] -state]
3731        }
3732
3733        # -- Help Menu
3734        #
3735        .mbar add cascade -label Help -menu .mbar.help
3736        menu .mbar.help
3737
3738        .mbar.help add command -label "About [appname]" \
3739                -command do_about \
3740                -font font_ui
3741}
3742
3743
3744# -- Branch Control
3745#
3746frame .branch \
3747        -borderwidth 1 \
3748        -relief sunken
3749label .branch.l1 \
3750        -text {Current Branch:} \
3751        -anchor w \
3752        -justify left \
3753        -font font_ui
3754label .branch.cb \
3755        -textvariable current_branch \
3756        -anchor w \
3757        -justify left \
3758        -font font_ui
3759pack .branch.l1 -side left
3760pack .branch.cb -side left -fill x
3761pack .branch -side top -fill x
3762
3763# -- Main Window Layout
3764#
3765panedwindow .vpane -orient vertical
3766panedwindow .vpane.files -orient horizontal
3767.vpane add .vpane.files -sticky nsew -height 100 -width 200
3768pack .vpane -anchor n -side top -fill both -expand 1
3769
3770# -- Index File List
3771#
3772frame .vpane.files.index -height 100 -width 200
3773label .vpane.files.index.title -text {Changes To Be Committed} \
3774        -background green \
3775        -font font_ui
3776text $ui_index -background white -borderwidth 0 \
3777        -width 20 -height 10 \
3778        -wrap none \
3779        -font font_ui \
3780        -cursor $cursor_ptr \
3781        -xscrollcommand {.vpane.files.index.sx set} \
3782        -yscrollcommand {.vpane.files.index.sy set} \
3783        -state disabled
3784scrollbar .vpane.files.index.sx -orient h -command [list $ui_index xview]
3785scrollbar .vpane.files.index.sy -orient v -command [list $ui_index yview]
3786pack .vpane.files.index.title -side top -fill x
3787pack .vpane.files.index.sx -side bottom -fill x
3788pack .vpane.files.index.sy -side right -fill y
3789pack $ui_index -side left -fill both -expand 1
3790.vpane.files add .vpane.files.index -sticky nsew
3791
3792# -- Working Directory File List
3793#
3794frame .vpane.files.workdir -height 100 -width 200
3795label .vpane.files.workdir.title -text {Changed But Not Updated} \
3796        -background red \
3797        -font font_ui
3798text $ui_workdir -background white -borderwidth 0 \
3799        -width 20 -height 10 \
3800        -wrap none \
3801        -font font_ui \
3802        -cursor $cursor_ptr \
3803        -xscrollcommand {.vpane.files.workdir.sx set} \
3804        -yscrollcommand {.vpane.files.workdir.sy set} \
3805        -state disabled
3806scrollbar .vpane.files.workdir.sx -orient h -command [list $ui_workdir xview]
3807scrollbar .vpane.files.workdir.sy -orient v -command [list $ui_workdir yview]
3808pack .vpane.files.workdir.title -side top -fill x
3809pack .vpane.files.workdir.sx -side bottom -fill x
3810pack .vpane.files.workdir.sy -side right -fill y
3811pack $ui_workdir -side left -fill both -expand 1
3812.vpane.files add .vpane.files.workdir -sticky nsew
3813
3814foreach i [list $ui_index $ui_workdir] {
3815        $i tag conf in_diff -font font_uibold
3816        $i tag conf in_sel \
3817                -background [$i cget -foreground] \
3818                -foreground [$i cget -background]
3819}
3820unset i
3821
3822# -- Diff and Commit Area
3823#
3824frame .vpane.lower -height 300 -width 400
3825frame .vpane.lower.commarea
3826frame .vpane.lower.diff -relief sunken -borderwidth 1
3827pack .vpane.lower.commarea -side top -fill x
3828pack .vpane.lower.diff -side bottom -fill both -expand 1
3829.vpane add .vpane.lower -stick nsew
3830
3831# -- Commit Area Buttons
3832#
3833frame .vpane.lower.commarea.buttons
3834label .vpane.lower.commarea.buttons.l -text {} \
3835        -anchor w \
3836        -justify left \
3837        -font font_ui
3838pack .vpane.lower.commarea.buttons.l -side top -fill x
3839pack .vpane.lower.commarea.buttons -side left -fill y
3840
3841button .vpane.lower.commarea.buttons.rescan -text {Rescan} \
3842        -command do_rescan \
3843        -font font_ui
3844pack .vpane.lower.commarea.buttons.rescan -side top -fill x
3845lappend disable_on_lock \
3846        {.vpane.lower.commarea.buttons.rescan conf -state}
3847
3848button .vpane.lower.commarea.buttons.incall -text {Add All} \
3849        -command do_add_all \
3850        -font font_ui
3851pack .vpane.lower.commarea.buttons.incall -side top -fill x
3852lappend disable_on_lock \
3853        {.vpane.lower.commarea.buttons.incall conf -state}
3854
3855button .vpane.lower.commarea.buttons.signoff -text {Sign Off} \
3856        -command do_signoff \
3857        -font font_ui
3858pack .vpane.lower.commarea.buttons.signoff -side top -fill x
3859
3860button .vpane.lower.commarea.buttons.commit -text {Commit} \
3861        -command do_commit \
3862        -font font_ui
3863pack .vpane.lower.commarea.buttons.commit -side top -fill x
3864lappend disable_on_lock \
3865        {.vpane.lower.commarea.buttons.commit conf -state}
3866
3867# -- Commit Message Buffer
3868#
3869frame .vpane.lower.commarea.buffer
3870frame .vpane.lower.commarea.buffer.header
3871set ui_comm .vpane.lower.commarea.buffer.t
3872set ui_coml .vpane.lower.commarea.buffer.header.l
3873radiobutton .vpane.lower.commarea.buffer.header.new \
3874        -text {New Commit} \
3875        -command do_select_commit_type \
3876        -variable selected_commit_type \
3877        -value new \
3878        -font font_ui
3879lappend disable_on_lock \
3880        [list .vpane.lower.commarea.buffer.header.new conf -state]
3881radiobutton .vpane.lower.commarea.buffer.header.amend \
3882        -text {Amend Last Commit} \
3883        -command do_select_commit_type \
3884        -variable selected_commit_type \
3885        -value amend \
3886        -font font_ui
3887lappend disable_on_lock \
3888        [list .vpane.lower.commarea.buffer.header.amend conf -state]
3889label $ui_coml \
3890        -anchor w \
3891        -justify left \
3892        -font font_ui
3893proc trace_commit_type {varname args} {
3894        global ui_coml commit_type
3895        switch -glob -- $commit_type {
3896        initial       {set txt {Initial Commit Message:}}
3897        amend         {set txt {Amended Commit Message:}}
3898        amend-initial {set txt {Amended Initial Commit Message:}}
3899        amend-merge   {set txt {Amended Merge Commit Message:}}
3900        merge         {set txt {Merge Commit Message:}}
3901        *             {set txt {Commit Message:}}
3902        }
3903        $ui_coml conf -text $txt
3904}
3905trace add variable commit_type write trace_commit_type
3906pack $ui_coml -side left -fill x
3907pack .vpane.lower.commarea.buffer.header.amend -side right
3908pack .vpane.lower.commarea.buffer.header.new -side right
3909
3910text $ui_comm -background white -borderwidth 1 \
3911        -undo true \
3912        -maxundo 20 \
3913        -autoseparators true \
3914        -relief sunken \
3915        -width 75 -height 9 -wrap none \
3916        -font font_diff \
3917        -yscrollcommand {.vpane.lower.commarea.buffer.sby set}
3918scrollbar .vpane.lower.commarea.buffer.sby \
3919        -command [list $ui_comm yview]
3920pack .vpane.lower.commarea.buffer.header -side top -fill x
3921pack .vpane.lower.commarea.buffer.sby -side right -fill y
3922pack $ui_comm -side left -fill y
3923pack .vpane.lower.commarea.buffer -side left -fill y
3924
3925# -- Commit Message Buffer Context Menu
3926#
3927set ctxm .vpane.lower.commarea.buffer.ctxm
3928menu $ctxm -tearoff 0
3929$ctxm add command \
3930        -label {Cut} \
3931        -font font_ui \
3932        -command {tk_textCut $ui_comm}
3933$ctxm add command \
3934        -label {Copy} \
3935        -font font_ui \
3936        -command {tk_textCopy $ui_comm}
3937$ctxm add command \
3938        -label {Paste} \
3939        -font font_ui \
3940        -command {tk_textPaste $ui_comm}
3941$ctxm add command \
3942        -label {Delete} \
3943        -font font_ui \
3944        -command {$ui_comm delete sel.first sel.last}
3945$ctxm add separator
3946$ctxm add command \
3947        -label {Select All} \
3948        -font font_ui \
3949        -command {focus $ui_comm;$ui_comm tag add sel 0.0 end}
3950$ctxm add command \
3951        -label {Copy All} \
3952        -font font_ui \
3953        -command {
3954                $ui_comm tag add sel 0.0 end
3955                tk_textCopy $ui_comm
3956                $ui_comm tag remove sel 0.0 end
3957        }
3958$ctxm add separator
3959$ctxm add command \
3960        -label {Sign Off} \
3961        -font font_ui \
3962        -command do_signoff
3963bind_button3 $ui_comm "tk_popup $ctxm %X %Y"
3964
3965# -- Diff Header
3966#
3967set current_diff_path {}
3968set diff_actions [list]
3969proc trace_current_diff_path {varname args} {
3970        global current_diff_path diff_actions file_states
3971        if {$current_diff_path eq {}} {
3972                set s {}
3973                set f {}
3974                set p {}
3975                set o disabled
3976        } else {
3977                set p $current_diff_path
3978                set s [mapdesc [lindex $file_states($p) 0] $p]
3979                set f {File:}
3980                set p [escape_path $p]
3981                set o normal
3982        }
3983
3984        .vpane.lower.diff.header.status configure -text $s
3985        .vpane.lower.diff.header.file configure -text $f
3986        .vpane.lower.diff.header.path configure -text $p
3987        foreach w $diff_actions {
3988                uplevel #0 $w $o
3989        }
3990}
3991trace add variable current_diff_path write trace_current_diff_path
3992
3993frame .vpane.lower.diff.header -background orange
3994label .vpane.lower.diff.header.status \
3995        -background orange \
3996        -width $max_status_desc \
3997        -anchor w \
3998        -justify left \
3999        -font font_ui
4000label .vpane.lower.diff.header.file \
4001        -background orange \
4002        -anchor w \
4003        -justify left \
4004        -font font_ui
4005label .vpane.lower.diff.header.path \
4006        -background orange \
4007        -anchor w \
4008        -justify left \
4009        -font font_ui
4010pack .vpane.lower.diff.header.status -side left
4011pack .vpane.lower.diff.header.file -side left
4012pack .vpane.lower.diff.header.path -fill x
4013set ctxm .vpane.lower.diff.header.ctxm
4014menu $ctxm -tearoff 0
4015$ctxm add command \
4016        -label {Copy} \
4017        -font font_ui \
4018        -command {
4019                clipboard clear
4020                clipboard append \
4021                        -format STRING \
4022                        -type STRING \
4023                        -- $current_diff_path
4024        }
4025lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
4026bind_button3 .vpane.lower.diff.header.path "tk_popup $ctxm %X %Y"
4027
4028# -- Diff Body
4029#
4030frame .vpane.lower.diff.body
4031set ui_diff .vpane.lower.diff.body.t
4032text $ui_diff -background white -borderwidth 0 \
4033        -width 80 -height 15 -wrap none \
4034        -font font_diff \
4035        -xscrollcommand {.vpane.lower.diff.body.sbx set} \
4036        -yscrollcommand {.vpane.lower.diff.body.sby set} \
4037        -state disabled
4038scrollbar .vpane.lower.diff.body.sbx -orient horizontal \
4039        -command [list $ui_diff xview]
4040scrollbar .vpane.lower.diff.body.sby -orient vertical \
4041        -command [list $ui_diff yview]
4042pack .vpane.lower.diff.body.sbx -side bottom -fill x
4043pack .vpane.lower.diff.body.sby -side right -fill y
4044pack $ui_diff -side left -fill both -expand 1
4045pack .vpane.lower.diff.header -side top -fill x
4046pack .vpane.lower.diff.body -side bottom -fill both -expand 1
4047
4048$ui_diff tag conf d_@ -foreground blue -font font_diffbold
4049$ui_diff tag conf d_+ -foreground {#00a000}
4050$ui_diff tag conf d_- -foreground red
4051
4052$ui_diff tag conf d_++ -foreground {#00a000}
4053$ui_diff tag conf d_-- -foreground red
4054$ui_diff tag conf d_+s \
4055        -foreground {#00a000} \
4056        -background {#e2effa}
4057$ui_diff tag conf d_-s \
4058        -foreground red \
4059        -background {#e2effa}
4060$ui_diff tag conf d_s+ \
4061        -foreground {#00a000} \
4062        -background ivory1
4063$ui_diff tag conf d_s- \
4064        -foreground red \
4065        -background ivory1
4066
4067$ui_diff tag conf d<<<<<<< \
4068        -foreground orange \
4069        -font font_diffbold
4070$ui_diff tag conf d======= \
4071        -foreground orange \
4072        -font font_diffbold
4073$ui_diff tag conf d>>>>>>> \
4074        -foreground orange \
4075        -font font_diffbold
4076
4077$ui_diff tag raise sel
4078
4079# -- Diff Body Context Menu
4080#
4081set ctxm .vpane.lower.diff.body.ctxm
4082menu $ctxm -tearoff 0
4083$ctxm add command \
4084        -label {Refresh} \
4085        -font font_ui \
4086        -command reshow_diff
4087$ctxm add command \
4088        -label {Copy} \
4089        -font font_ui \
4090        -command {tk_textCopy $ui_diff}
4091lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
4092$ctxm add command \
4093        -label {Select All} \
4094        -font font_ui \
4095        -command {focus $ui_diff;$ui_diff tag add sel 0.0 end}
4096lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
4097$ctxm add command \
4098        -label {Copy All} \
4099        -font font_ui \
4100        -command {
4101                $ui_diff tag add sel 0.0 end
4102                tk_textCopy $ui_diff
4103                $ui_diff tag remove sel 0.0 end
4104        }
4105lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
4106$ctxm add separator
4107$ctxm add command \
4108        -label {Decrease Font Size} \
4109        -font font_ui \
4110        -command {incr_font_size font_diff -1}
4111lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
4112$ctxm add command \
4113        -label {Increase Font Size} \
4114        -font font_ui \
4115        -command {incr_font_size font_diff 1}
4116lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
4117$ctxm add separator
4118$ctxm add command \
4119        -label {Show Less Context} \
4120        -font font_ui \
4121        -command {if {$repo_config(gui.diffcontext) >= 2} {
4122                incr repo_config(gui.diffcontext) -1
4123                reshow_diff
4124        }}
4125lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
4126$ctxm add command \
4127        -label {Show More Context} \
4128        -font font_ui \
4129        -command {
4130                incr repo_config(gui.diffcontext)
4131                reshow_diff
4132        }
4133lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
4134$ctxm add separator
4135$ctxm add command -label {Options...} \
4136        -font font_ui \
4137        -command do_options
4138bind_button3 $ui_diff "tk_popup $ctxm %X %Y"
4139
4140# -- Status Bar
4141#
4142set ui_status_value {Initializing...}
4143label .status -textvariable ui_status_value \
4144        -anchor w \
4145        -justify left \
4146        -borderwidth 1 \
4147        -relief sunken \
4148        -font font_ui
4149pack .status -anchor w -side bottom -fill x
4150
4151# -- Load geometry
4152#
4153catch {
4154set gm $repo_config(gui.geometry)
4155wm geometry . [lindex $gm 0]
4156.vpane sash place 0 \
4157        [lindex [.vpane sash coord 0] 0] \
4158        [lindex $gm 1]
4159.vpane.files sash place 0 \
4160        [lindex $gm 2] \
4161        [lindex [.vpane.files sash coord 0] 1]
4162unset gm
4163}
4164
4165# -- Key Bindings
4166#
4167bind $ui_comm <$M1B-Key-Return> {do_commit;break}
4168bind $ui_comm <$M1B-Key-i> {do_add_all;break}
4169bind $ui_comm <$M1B-Key-I> {do_add_all;break}
4170bind $ui_comm <$M1B-Key-x> {tk_textCut %W;break}
4171bind $ui_comm <$M1B-Key-X> {tk_textCut %W;break}
4172bind $ui_comm <$M1B-Key-c> {tk_textCopy %W;break}
4173bind $ui_comm <$M1B-Key-C> {tk_textCopy %W;break}
4174bind $ui_comm <$M1B-Key-v> {tk_textPaste %W; %W see insert; break}
4175bind $ui_comm <$M1B-Key-V> {tk_textPaste %W; %W see insert; break}
4176bind $ui_comm <$M1B-Key-a> {%W tag add sel 0.0 end;break}
4177bind $ui_comm <$M1B-Key-A> {%W tag add sel 0.0 end;break}
4178
4179bind $ui_diff <$M1B-Key-x> {tk_textCopy %W;break}
4180bind $ui_diff <$M1B-Key-X> {tk_textCopy %W;break}
4181bind $ui_diff <$M1B-Key-c> {tk_textCopy %W;break}
4182bind $ui_diff <$M1B-Key-C> {tk_textCopy %W;break}
4183bind $ui_diff <$M1B-Key-v> {break}
4184bind $ui_diff <$M1B-Key-V> {break}
4185bind $ui_diff <$M1B-Key-a> {%W tag add sel 0.0 end;break}
4186bind $ui_diff <$M1B-Key-A> {%W tag add sel 0.0 end;break}
4187bind $ui_diff <Key-Up>     {catch {%W yview scroll -1 units};break}
4188bind $ui_diff <Key-Down>   {catch {%W yview scroll  1 units};break}
4189bind $ui_diff <Key-Left>   {catch {%W xview scroll -1 units};break}
4190bind $ui_diff <Key-Right>  {catch {%W xview scroll  1 units};break}
4191
4192if {!$single_commit} {
4193        bind . <$M1B-Key-n> do_create_branch
4194        bind . <$M1B-Key-N> do_create_branch
4195}
4196
4197bind .   <Destroy> do_quit
4198bind all <Key-F5> do_rescan
4199bind all <$M1B-Key-r> do_rescan
4200bind all <$M1B-Key-R> do_rescan
4201bind .   <$M1B-Key-s> do_signoff
4202bind .   <$M1B-Key-S> do_signoff
4203bind .   <$M1B-Key-i> do_add_all
4204bind .   <$M1B-Key-I> do_add_all
4205bind .   <$M1B-Key-Return> do_commit
4206bind all <$M1B-Key-q> do_quit
4207bind all <$M1B-Key-Q> do_quit
4208bind all <$M1B-Key-w> {destroy [winfo toplevel %W]}
4209bind all <$M1B-Key-W> {destroy [winfo toplevel %W]}
4210foreach i [list $ui_index $ui_workdir] {
4211        bind $i <Button-1>       "toggle_or_diff         $i %x %y; break"
4212        bind $i <$M1B-Button-1>  "add_one_to_selection   $i %x %y; break"
4213        bind $i <Shift-Button-1> "add_range_to_selection $i %x %y; break"
4214}
4215unset i
4216
4217set file_lists($ui_index) [list]
4218set file_lists($ui_workdir) [list]
4219
4220set HEAD {}
4221set PARENT {}
4222set MERGE_HEAD [list]
4223set commit_type {}
4224set empty_tree {}
4225set current_branch {}
4226set current_diff_path {}
4227set selected_commit_type new
4228
4229wm title . "[appname] ([file normalize [file dirname [gitdir]]])"
4230focus -force $ui_comm
4231
4232# -- Warn the user about environmental problems.  Cygwin's Tcl
4233#    does *not* pass its env array onto any processes it spawns.
4234#    This means that git processes get none of our environment.
4235#
4236if {[is_Windows]} {
4237        set ignored_env 0
4238        set suggest_user {}
4239        set msg "Possible environment issues exist.
4240
4241The following environment variables are probably
4242going to be ignored by any Git subprocess run
4243by [appname]:
4244
4245"
4246        foreach name [array names env] {
4247                switch -regexp -- $name {
4248                {^GIT_INDEX_FILE$} -
4249                {^GIT_OBJECT_DIRECTORY$} -
4250                {^GIT_ALTERNATE_OBJECT_DIRECTORIES$} -
4251                {^GIT_DIFF_OPTS$} -
4252                {^GIT_EXTERNAL_DIFF$} -
4253                {^GIT_PAGER$} -
4254                {^GIT_TRACE$} -
4255                {^GIT_CONFIG$} -
4256                {^GIT_CONFIG_LOCAL$} -
4257                {^GIT_(AUTHOR|COMMITTER)_DATE$} {
4258                        append msg " - $name\n"
4259                        incr ignored_env
4260                }
4261                {^GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL)$} {
4262                        append msg " - $name\n"
4263                        incr ignored_env
4264                        set suggest_user $name
4265                }
4266                }
4267        }
4268        if {$ignored_env > 0} {
4269                append msg "
4270This is due to a known issue with the
4271Tcl binary distributed by Cygwin."
4272
4273                if {$suggest_user ne {}} {
4274                        append msg "
4275
4276A good replacement for $suggest_user
4277is placing values for the user.name and
4278user.email settings into your personal
4279~/.gitconfig file.
4280"
4281                }
4282                warn_popup $msg
4283        }
4284        unset ignored_env msg suggest_user name
4285}
4286
4287# -- Only initialize complex UI if we are going to stay running.
4288#
4289if {!$single_commit} {
4290        load_all_remotes
4291        load_all_heads
4292
4293        populate_branch_menu
4294        populate_fetch_menu .mbar.fetch
4295        populate_pull_menu .mbar.pull
4296        populate_push_menu .mbar.push
4297}
4298
4299# -- Only suggest a gc run if we are going to stay running.
4300#
4301if {!$single_commit} {
4302        set object_limit 2000
4303        if {[is_Windows]} {set object_limit 200}
4304        regexp {^([0-9]+) objects,} [exec git count-objects] _junk objects_current
4305        if {$objects_current >= $object_limit} {
4306                if {[ask_popup \
4307                        "This repository currently has $objects_current loose objects.
4308
4309To maintain optimal performance it is strongly
4310recommended that you compress the database
4311when more than $object_limit loose objects exist.
4312
4313Compress the database now?"] eq yes} {
4314                        do_gc
4315                }
4316        }
4317        unset object_limit _junk objects_current
4318}
4319
4320lock_index begin-read
4321after 1 do_rescan