git-guion commit git-gui: Add menu option to include only selected files. (c4ed879)
   1#!/bin/sh
   2# Tcl ignores the next line -*- tcl -*- \
   3exec wish "$0" -- "$@"
   4
   5# Copyright (C) 2006 Shawn Pearce, Paul Mackerras.  All rights reserved.
   6# This program is free software; it may be used, copied, modified
   7# and distributed under the terms of the GNU General Public Licence,
   8# either version 2, or (at your option) any later version.
   9
  10set appname [lindex [file split $argv0] end]
  11set gitdir {}
  12
  13######################################################################
  14##
  15## config
  16
  17proc is_many_config {name} {
  18        switch -glob -- $name {
  19        remote.*.fetch -
  20        remote.*.push
  21                {return 1}
  22        *
  23                {return 0}
  24        }
  25}
  26
  27proc load_config {include_global} {
  28        global repo_config global_config default_config
  29
  30        array unset global_config
  31        if {$include_global} {
  32                catch {
  33                        set fd_rc [open "| git repo-config --global --list" r]
  34                        while {[gets $fd_rc line] >= 0} {
  35                                if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
  36                                        if {[is_many_config $name]} {
  37                                                lappend global_config($name) $value
  38                                        } else {
  39                                                set global_config($name) $value
  40                                        }
  41                                }
  42                        }
  43                        close $fd_rc
  44                }
  45        }
  46
  47        array unset repo_config
  48        catch {
  49                set fd_rc [open "| git repo-config --list" r]
  50                while {[gets $fd_rc line] >= 0} {
  51                        if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
  52                                if {[is_many_config $name]} {
  53                                        lappend repo_config($name) $value
  54                                } else {
  55                                        set repo_config($name) $value
  56                                }
  57                        }
  58                }
  59                close $fd_rc
  60        }
  61
  62        foreach name [array names default_config] {
  63                if {[catch {set v $global_config($name)}]} {
  64                        set global_config($name) $default_config($name)
  65                }
  66                if {[catch {set v $repo_config($name)}]} {
  67                        set repo_config($name) $default_config($name)
  68                }
  69        }
  70}
  71
  72proc save_config {} {
  73        global default_config font_descs
  74        global repo_config global_config
  75        global repo_config_new global_config_new
  76
  77        foreach option $font_descs {
  78                set name [lindex $option 0]
  79                set font [lindex $option 1]
  80                font configure $font \
  81                        -family $global_config_new(gui.$font^^family) \
  82                        -size $global_config_new(gui.$font^^size)
  83                font configure ${font}bold \
  84                        -family $global_config_new(gui.$font^^family) \
  85                        -size $global_config_new(gui.$font^^size)
  86                set global_config_new(gui.$name) [font configure $font]
  87                unset global_config_new(gui.$font^^family)
  88                unset global_config_new(gui.$font^^size)
  89        }
  90
  91        foreach name [array names default_config] {
  92                set value $global_config_new($name)
  93                if {$value ne $global_config($name)} {
  94                        if {$value eq $default_config($name)} {
  95                                catch {exec git repo-config --global --unset $name}
  96                        } else {
  97                                regsub -all "\[{}\]" $value {"} value
  98                                exec git repo-config --global $name $value
  99                        }
 100                        set global_config($name) $value
 101                        if {$value eq $repo_config($name)} {
 102                                catch {exec git repo-config --unset $name}
 103                                set repo_config($name) $value
 104                        }
 105                }
 106        }
 107
 108        foreach name [array names default_config] {
 109                set value $repo_config_new($name)
 110                if {$value ne $repo_config($name)} {
 111                        if {$value eq $global_config($name)} {
 112                                catch {exec git repo-config --unset $name}
 113                        } else {
 114                                regsub -all "\[{}\]" $value {"} value
 115                                exec git repo-config $name $value
 116                        }
 117                        set repo_config($name) $value
 118                }
 119        }
 120}
 121
 122proc error_popup {msg} {
 123        global gitdir appname
 124
 125        set title $appname
 126        if {$gitdir ne {}} {
 127                append title { (}
 128                append title [lindex \
 129                        [file split [file normalize [file dirname $gitdir]]] \
 130                        end]
 131                append title {)}
 132        }
 133        set cmd [list tk_messageBox \
 134                -icon error \
 135                -type ok \
 136                -title "$title: error" \
 137                -message $msg]
 138        if {[winfo ismapped .]} {
 139                lappend cmd -parent .
 140        }
 141        eval $cmd
 142}
 143
 144proc info_popup {msg} {
 145        global gitdir appname
 146
 147        set title $appname
 148        if {$gitdir ne {}} {
 149                append title { (}
 150                append title [lindex \
 151                        [file split [file normalize [file dirname $gitdir]]] \
 152                        end]
 153                append title {)}
 154        }
 155        tk_messageBox \
 156                -parent . \
 157                -icon error \
 158                -type ok \
 159                -title $title \
 160                -message $msg
 161}
 162
 163######################################################################
 164##
 165## repository setup
 166
 167if {   [catch {set gitdir $env(GIT_DIR)}]
 168        && [catch {set gitdir [exec git rev-parse --git-dir]} err]} {
 169        catch {wm withdraw .}
 170        error_popup "Cannot find the git directory:\n\n$err"
 171        exit 1
 172}
 173if {![file isdirectory $gitdir]} {
 174        catch {wm withdraw .}
 175        error_popup "Git directory not found:\n\n$gitdir"
 176        exit 1
 177}
 178if {[lindex [file split $gitdir] end] ne {.git}} {
 179        catch {wm withdraw .}
 180        error_popup "Cannot use funny .git directory:\n\n$gitdir"
 181        exit 1
 182}
 183if {[catch {cd [file dirname $gitdir]} err]} {
 184        catch {wm withdraw .}
 185        error_popup "No working directory [file dirname $gitdir]:\n\n$err"
 186        exit 1
 187}
 188
 189set single_commit 0
 190if {$appname eq {git-citool}} {
 191        set single_commit 1
 192}
 193
 194######################################################################
 195##
 196## task management
 197
 198set rescan_active 0
 199set diff_active 0
 200set last_clicked {}
 201
 202set disable_on_lock [list]
 203set index_lock_type none
 204
 205set HEAD {}
 206set PARENT {}
 207set commit_type {}
 208set empty_tree {}
 209
 210proc lock_index {type} {
 211        global index_lock_type disable_on_lock
 212
 213        if {$index_lock_type eq {none}} {
 214                set index_lock_type $type
 215                foreach w $disable_on_lock {
 216                        uplevel #0 $w disabled
 217                }
 218                return 1
 219        } elseif {$index_lock_type eq {begin-update} && $type eq {update}} {
 220                set index_lock_type $type
 221                return 1
 222        }
 223        return 0
 224}
 225
 226proc unlock_index {} {
 227        global index_lock_type disable_on_lock
 228
 229        set index_lock_type none
 230        foreach w $disable_on_lock {
 231                uplevel #0 $w normal
 232        }
 233}
 234
 235######################################################################
 236##
 237## status
 238
 239proc repository_state {hdvar ctvar} {
 240        global gitdir
 241        upvar $hdvar hd $ctvar ct
 242
 243        if {[catch {set hd [exec git rev-parse --verify HEAD]}]} {
 244                set hd {}
 245                set ct initial
 246        } elseif {[file exists [file join $gitdir MERGE_HEAD]]} {
 247                set ct merge
 248        } else {
 249                set ct normal
 250        }
 251}
 252
 253proc PARENT {} {
 254        global PARENT empty_tree
 255
 256        if {$PARENT ne {}} {
 257                return $PARENT
 258        }
 259        if {$empty_tree eq {}} {
 260                set empty_tree [exec git mktree << {}]
 261        }
 262        return $empty_tree
 263}
 264
 265proc rescan {after} {
 266        global HEAD PARENT commit_type
 267        global ui_index ui_other ui_status_value ui_comm
 268        global rescan_active file_states
 269        global repo_config
 270
 271        if {$rescan_active > 0 || ![lock_index read]} return
 272
 273        repository_state new_HEAD new_type
 274        if {[string match amend* $commit_type]
 275                && $new_type eq {normal}
 276                && $new_HEAD eq $HEAD} {
 277        } else {
 278                set HEAD $new_HEAD
 279                set PARENT $new_HEAD
 280                set commit_type $new_type
 281        }
 282
 283        array unset file_states
 284
 285        if {![$ui_comm edit modified]
 286                || [string trim [$ui_comm get 0.0 end]] eq {}} {
 287                if {[load_message GITGUI_MSG]} {
 288                } elseif {[load_message MERGE_MSG]} {
 289                } elseif {[load_message SQUASH_MSG]} {
 290                }
 291                $ui_comm edit modified false
 292                $ui_comm edit reset
 293        }
 294
 295        if {$repo_config(gui.trustmtime) eq {true}} {
 296                rescan_stage2 {} $after
 297        } else {
 298                set rescan_active 1
 299                set ui_status_value {Refreshing file status...}
 300                set cmd [list git update-index]
 301                lappend cmd -q
 302                lappend cmd --unmerged
 303                lappend cmd --ignore-missing
 304                lappend cmd --refresh
 305                set fd_rf [open "| $cmd" r]
 306                fconfigure $fd_rf -blocking 0 -translation binary
 307                fileevent $fd_rf readable \
 308                        [list rescan_stage2 $fd_rf $after]
 309        }
 310}
 311
 312proc rescan_stage2 {fd after} {
 313        global gitdir ui_status_value
 314        global rescan_active buf_rdi buf_rdf buf_rlo
 315
 316        if {$fd ne {}} {
 317                read $fd
 318                if {![eof $fd]} return
 319                close $fd
 320        }
 321
 322        set ls_others [list | git ls-files --others -z \
 323                --exclude-per-directory=.gitignore]
 324        set info_exclude [file join $gitdir info exclude]
 325        if {[file readable $info_exclude]} {
 326                lappend ls_others "--exclude-from=$info_exclude"
 327        }
 328
 329        set buf_rdi {}
 330        set buf_rdf {}
 331        set buf_rlo {}
 332
 333        set rescan_active 3
 334        set ui_status_value {Scanning for modified files ...}
 335        set fd_di [open "| git diff-index --cached -z [PARENT]" r]
 336        set fd_df [open "| git diff-files -z" r]
 337        set fd_lo [open $ls_others r]
 338
 339        fconfigure $fd_di -blocking 0 -translation binary
 340        fconfigure $fd_df -blocking 0 -translation binary
 341        fconfigure $fd_lo -blocking 0 -translation binary
 342        fileevent $fd_di readable [list read_diff_index $fd_di $after]
 343        fileevent $fd_df readable [list read_diff_files $fd_df $after]
 344        fileevent $fd_lo readable [list read_ls_others $fd_lo $after]
 345}
 346
 347proc load_message {file} {
 348        global gitdir ui_comm
 349
 350        set f [file join $gitdir $file]
 351        if {[file isfile $f]} {
 352                if {[catch {set fd [open $f r]}]} {
 353                        return 0
 354                }
 355                set content [string trim [read $fd]]
 356                close $fd
 357                $ui_comm delete 0.0 end
 358                $ui_comm insert end $content
 359                return 1
 360        }
 361        return 0
 362}
 363
 364proc read_diff_index {fd after} {
 365        global buf_rdi
 366
 367        append buf_rdi [read $fd]
 368        set c 0
 369        set n [string length $buf_rdi]
 370        while {$c < $n} {
 371                set z1 [string first "\0" $buf_rdi $c]
 372                if {$z1 == -1} break
 373                incr z1
 374                set z2 [string first "\0" $buf_rdi $z1]
 375                if {$z2 == -1} break
 376
 377                set c $z2
 378                incr z2 -1
 379                display_file \
 380                        [string range $buf_rdi $z1 $z2] \
 381                        [string index $buf_rdi [expr {$z1 - 2}]]?
 382                incr c
 383        }
 384        if {$c < $n} {
 385                set buf_rdi [string range $buf_rdi $c end]
 386        } else {
 387                set buf_rdi {}
 388        }
 389
 390        rescan_done $fd buf_rdi $after
 391}
 392
 393proc read_diff_files {fd after} {
 394        global buf_rdf
 395
 396        append buf_rdf [read $fd]
 397        set c 0
 398        set n [string length $buf_rdf]
 399        while {$c < $n} {
 400                set z1 [string first "\0" $buf_rdf $c]
 401                if {$z1 == -1} break
 402                incr z1
 403                set z2 [string first "\0" $buf_rdf $z1]
 404                if {$z2 == -1} break
 405
 406                set c $z2
 407                incr z2 -1
 408                display_file \
 409                        [string range $buf_rdf $z1 $z2] \
 410                        ?[string index $buf_rdf [expr {$z1 - 2}]]
 411                incr c
 412        }
 413        if {$c < $n} {
 414                set buf_rdf [string range $buf_rdf $c end]
 415        } else {
 416                set buf_rdf {}
 417        }
 418
 419        rescan_done $fd buf_rdf $after
 420}
 421
 422proc read_ls_others {fd after} {
 423        global buf_rlo
 424
 425        append buf_rlo [read $fd]
 426        set pck [split $buf_rlo "\0"]
 427        set buf_rlo [lindex $pck end]
 428        foreach p [lrange $pck 0 end-1] {
 429                display_file $p ?O
 430        }
 431        rescan_done $fd buf_rlo $after
 432}
 433
 434proc rescan_done {fd buf after} {
 435        global rescan_active
 436        global file_states repo_config
 437        upvar $buf to_clear
 438
 439        if {![eof $fd]} return
 440        set to_clear {}
 441        close $fd
 442        if {[incr rescan_active -1] > 0} return
 443
 444        prune_selection
 445        unlock_index
 446        display_all_files
 447
 448        if {$repo_config(gui.partialinclude) ne {true}} {
 449                set pathList [list]
 450                foreach path [array names file_states] {
 451                        switch -- [lindex $file_states($path) 0] {
 452                        AM -
 453                        MM {lappend pathList $path}
 454                        }
 455                }
 456                if {$pathList ne {}} {
 457                        update_index \
 458                                "Updating included files" \
 459                                $pathList \
 460                                [concat {reshow_diff;} $after]
 461                        return
 462                }
 463        }
 464
 465        reshow_diff
 466        uplevel #0 $after
 467}
 468
 469proc prune_selection {} {
 470        global file_states selected_paths
 471
 472        foreach path [array names selected_paths] {
 473                if {[catch {set still_here $file_states($path)}]} {
 474                        unset selected_paths($path)
 475                }
 476        }
 477}
 478
 479######################################################################
 480##
 481## diff
 482
 483proc clear_diff {} {
 484        global ui_diff current_diff ui_index ui_other
 485
 486        $ui_diff conf -state normal
 487        $ui_diff delete 0.0 end
 488        $ui_diff conf -state disabled
 489
 490        set current_diff {}
 491
 492        $ui_index tag remove in_diff 0.0 end
 493        $ui_other tag remove in_diff 0.0 end
 494}
 495
 496proc reshow_diff {} {
 497        global current_diff ui_status_value file_states
 498
 499        if {$current_diff eq {}
 500                || [catch {set s $file_states($current_diff)}]} {
 501                clear_diff
 502        } else {
 503                show_diff $current_diff
 504        }
 505}
 506
 507proc handle_empty_diff {} {
 508        global current_diff file_states file_lists
 509
 510        set path $current_diff
 511        set s $file_states($path)
 512        if {[lindex $s 0] ne {_M}} return
 513
 514        info_popup "No differences detected.
 515
 516[short_path $path] has no changes.
 517
 518The modification date of this file was updated
 519by another application and you currently have
 520the Trust File Modification Timestamps option
 521enabled, so Git did not automatically detect
 522that there are no content differences in this
 523file.
 524
 525This file will now be removed from the modified
 526files list, to prevent possible confusion.
 527"
 528        if {[catch {exec git update-index -- $path} err]} {
 529                error_popup "Failed to refresh index:\n\n$err"
 530        }
 531
 532        clear_diff
 533        set old_w [mapcol [lindex $file_states($path) 0] $path]
 534        set lno [lsearch -sorted $file_lists($old_w) $path]
 535        if {$lno >= 0} {
 536                set file_lists($old_w) \
 537                        [lreplace $file_lists($old_w) $lno $lno]
 538                incr lno
 539                $old_w conf -state normal
 540                $old_w delete $lno.0 [expr {$lno + 1}].0
 541                $old_w conf -state disabled
 542        }
 543}
 544
 545proc show_diff {path {w {}} {lno {}}} {
 546        global file_states file_lists
 547        global diff_3way diff_active repo_config
 548        global ui_diff current_diff ui_status_value
 549
 550        if {$diff_active || ![lock_index read]} return
 551
 552        clear_diff
 553        if {$w eq {} || $lno == {}} {
 554                foreach w [array names file_lists] {
 555                        set lno [lsearch -sorted $file_lists($w) $path]
 556                        if {$lno >= 0} {
 557                                incr lno
 558                                break
 559                        }
 560                }
 561        }
 562        if {$w ne {} && $lno >= 1} {
 563                $w tag add in_diff $lno.0 [expr {$lno + 1}].0
 564        }
 565
 566        set s $file_states($path)
 567        set m [lindex $s 0]
 568        set diff_3way 0
 569        set diff_active 1
 570        set current_diff $path
 571        set ui_status_value "Loading diff of [escape_path $path]..."
 572
 573        set cmd [list | git diff-index]
 574        lappend cmd --no-color
 575        if {$repo_config(gui.diffcontext) > 0} {
 576                lappend cmd "-U$repo_config(gui.diffcontext)"
 577        }
 578        lappend cmd -p
 579
 580        switch $m {
 581        MM {
 582                lappend cmd -c
 583        }
 584        _O {
 585                if {[catch {
 586                                set fd [open $path r]
 587                                set content [read $fd]
 588                                close $fd
 589                        } err ]} {
 590                        set diff_active 0
 591                        unlock_index
 592                        set ui_status_value "Unable to display [escape_path $path]"
 593                        error_popup "Error loading file:\n\n$err"
 594                        return
 595                }
 596                $ui_diff conf -state normal
 597                $ui_diff insert end $content
 598                $ui_diff conf -state disabled
 599                set diff_active 0
 600                unlock_index
 601                set ui_status_value {Ready.}
 602                return
 603        }
 604        }
 605
 606        lappend cmd [PARENT]
 607        lappend cmd --
 608        lappend cmd $path
 609
 610        if {[catch {set fd [open $cmd r]} err]} {
 611                set diff_active 0
 612                unlock_index
 613                set ui_status_value "Unable to display [escape_path $path]"
 614                error_popup "Error loading diff:\n\n$err"
 615                return
 616        }
 617
 618        fconfigure $fd -blocking 0 -translation auto
 619        fileevent $fd readable [list read_diff $fd]
 620}
 621
 622proc read_diff {fd} {
 623        global ui_diff ui_status_value diff_3way diff_active
 624        global repo_config
 625
 626        while {[gets $fd line] >= 0} {
 627                if {[string match {diff --git *} $line]} continue
 628                if {[string match {diff --combined *} $line]} continue
 629                if {[string match {--- *} $line]} continue
 630                if {[string match {+++ *} $line]} continue
 631                if {[string match index* $line]} {
 632                        if {[string first , $line] >= 0} {
 633                                set diff_3way 1
 634                        }
 635                }
 636
 637                $ui_diff conf -state normal
 638                if {!$diff_3way} {
 639                        set x [string index $line 0]
 640                        switch -- $x {
 641                        "@" {set tags da}
 642                        "+" {set tags dp}
 643                        "-" {set tags dm}
 644                        default {set tags {}}
 645                        }
 646                } else {
 647                        set x [string range $line 0 1]
 648                        switch -- $x {
 649                        default {set tags {}}
 650                        "@@" {set tags da}
 651                        "++" {set tags dp; set x " +"}
 652                        " +" {set tags {di bold}; set x "++"}
 653                        "+ " {set tags dni; set x "-+"}
 654                        "--" {set tags dm; set x " -"}
 655                        " -" {set tags {dm bold}; set x "--"}
 656                        "- " {set tags di; set x "+-"}
 657                        default {set tags {}}
 658                        }
 659                        set line [string replace $line 0 1 $x]
 660                }
 661                $ui_diff insert end $line $tags
 662                $ui_diff insert end "\n"
 663                $ui_diff conf -state disabled
 664        }
 665
 666        if {[eof $fd]} {
 667                close $fd
 668                set diff_active 0
 669                unlock_index
 670                set ui_status_value {Ready.}
 671
 672                if {$repo_config(gui.trustmtime) eq {true}
 673                        && [$ui_diff index end] eq {2.0}} {
 674                        handle_empty_diff
 675                }
 676        }
 677}
 678
 679######################################################################
 680##
 681## commit
 682
 683proc load_last_commit {} {
 684        global HEAD PARENT commit_type ui_comm
 685
 686        if {[string match amend* $commit_type]} return
 687        if {$commit_type ne {normal}} {
 688                error_popup "Can't amend a $commit_type commit."
 689                return
 690        }
 691
 692        set msg {}
 693        set parent {}
 694        set parent_count 0
 695        if {[catch {
 696                        set fd [open "| git cat-file commit $HEAD" r]
 697                        while {[gets $fd line] > 0} {
 698                                if {[string match {parent *} $line]} {
 699                                        set parent [string range $line 7 end]
 700                                        incr parent_count
 701                                }
 702                        }
 703                        set msg [string trim [read $fd]]
 704                        close $fd
 705                } err]} {
 706                error_popup "Error loading commit data for amend:\n\n$err"
 707                return
 708        }
 709
 710        if {$parent_count > 1} {
 711                error_popup {Can't amend a merge commit.}
 712                return
 713        }
 714
 715        if {$parent_count == 0} {
 716                set commit_type amend-initial
 717                set PARENT {}
 718        } elseif {$parent_count == 1} {
 719                set commit_type amend
 720                set PARENT $parent
 721        }
 722
 723        $ui_comm delete 0.0 end
 724        $ui_comm insert end $msg
 725        $ui_comm edit modified false
 726        $ui_comm edit reset
 727        rescan {set ui_status_value {Ready.}}
 728}
 729
 730proc commit_tree {} {
 731        global HEAD commit_type file_states ui_comm repo_config
 732
 733        if {![lock_index update]} return
 734
 735        # -- Our in memory state should match the repository.
 736        #
 737        repository_state curHEAD cur_type
 738        if {[string match amend* $commit_type]
 739                && $cur_type eq {normal}
 740                && $curHEAD eq $HEAD} {
 741        } elseif {$commit_type ne $cur_type || $HEAD ne $curHEAD} {
 742                error_popup {Last scanned state does not match repository state.
 743
 744Its highly likely that another Git program modified the
 745repository since the last scan.  A rescan is required
 746before committing.
 747
 748A rescan will be automatically started now.
 749}
 750                unlock_index
 751                rescan {set ui_status_value {Ready.}}
 752                return
 753        }
 754
 755        # -- At least one file should differ in the index.
 756        #
 757        set files_ready 0
 758        foreach path [array names file_states] {
 759                switch -glob -- [lindex $file_states($path) 0] {
 760                _? {continue}
 761                A? -
 762                D? -
 763                M? {set files_ready 1; break}
 764                U? {
 765                        error_popup "Unmerged files cannot be committed.
 766
 767File [short_path $path] has merge conflicts.
 768You must resolve them and include the file before committing.
 769"
 770                        unlock_index
 771                        return
 772                }
 773                default {
 774                        error_popup "Unknown file state [lindex $s 0] detected.
 775
 776File [short_path $path] cannot be committed by this program.
 777"
 778                }
 779                }
 780        }
 781        if {!$files_ready} {
 782                error_popup {No included files to commit.
 783
 784You must include at least 1 file before you can commit.
 785}
 786                unlock_index
 787                return
 788        }
 789
 790        # -- A message is required.
 791        #
 792        set msg [string trim [$ui_comm get 1.0 end]]
 793        if {$msg eq {}} {
 794                error_popup {Please supply a commit message.
 795
 796A good commit message has the following format:
 797
 798- First line: Describe in one sentance what you did.
 799- Second line: Blank
 800- Remaining lines: Describe why this change is good.
 801}
 802                unlock_index
 803                return
 804        }
 805
 806        # -- Update included files if partialincludes are off.
 807        #
 808        if {$repo_config(gui.partialinclude) ne {true}} {
 809                set pathList [list]
 810                foreach path [array names file_states] {
 811                        switch -glob -- [lindex $file_states($path) 0] {
 812                        A? -
 813                        M? {lappend pathList $path}
 814                        }
 815                }
 816                if {$pathList ne {}} {
 817                        unlock_index
 818                        update_index \
 819                                "Updating included files" \
 820                                $pathList \
 821                                [concat {lock_index update;} \
 822                                        [list commit_prehook $curHEAD $msg]]
 823                        return
 824                }
 825        }
 826
 827        commit_prehook $curHEAD $msg
 828}
 829
 830proc commit_prehook {curHEAD msg} {
 831        global tcl_platform gitdir ui_status_value pch_error
 832
 833        # On Cygwin [file executable] might lie so we need to ask
 834        # the shell if the hook is executable.  Yes that's annoying.
 835
 836        set pchook [file join $gitdir hooks pre-commit]
 837        if {$tcl_platform(platform) eq {windows}
 838                && [file isfile $pchook]} {
 839                set pchook [list sh -c [concat \
 840                        "if test -x \"$pchook\";" \
 841                        "then exec \"$pchook\" 2>&1;" \
 842                        "fi"]]
 843        } elseif {[file executable $pchook]} {
 844                set pchook [list $pchook |& cat]
 845        } else {
 846                commit_writetree $curHEAD $msg
 847                return
 848        }
 849
 850        set ui_status_value {Calling pre-commit hook...}
 851        set pch_error {}
 852        set fd_ph [open "| $pchook" r]
 853        fconfigure $fd_ph -blocking 0 -translation binary
 854        fileevent $fd_ph readable \
 855                [list commit_prehook_wait $fd_ph $curHEAD $msg]
 856}
 857
 858proc commit_prehook_wait {fd_ph curHEAD msg} {
 859        global pch_error ui_status_value
 860
 861        append pch_error [read $fd_ph]
 862        fconfigure $fd_ph -blocking 1
 863        if {[eof $fd_ph]} {
 864                if {[catch {close $fd_ph}]} {
 865                        set ui_status_value {Commit declined by pre-commit hook.}
 866                        hook_failed_popup pre-commit $pch_error
 867                        unlock_index
 868                } else {
 869                        commit_writetree $curHEAD $msg
 870                }
 871                set pch_error {}
 872                return
 873        }
 874        fconfigure $fd_ph -blocking 0
 875}
 876
 877proc commit_writetree {curHEAD msg} {
 878        global ui_status_value
 879
 880        set ui_status_value {Committing changes...}
 881        set fd_wt [open "| git write-tree" r]
 882        fileevent $fd_wt readable \
 883                [list commit_committree $fd_wt $curHEAD $msg]
 884}
 885
 886proc commit_committree {fd_wt curHEAD msg} {
 887        global single_commit gitdir HEAD PARENT commit_type tcl_platform
 888        global ui_status_value ui_comm
 889        global file_states selected_paths
 890
 891        gets $fd_wt tree_id
 892        if {$tree_id eq {} || [catch {close $fd_wt} err]} {
 893                error_popup "write-tree failed:\n\n$err"
 894                set ui_status_value {Commit failed.}
 895                unlock_index
 896                return
 897        }
 898
 899        # -- Create the commit.
 900        #
 901        set cmd [list git commit-tree $tree_id]
 902        if {$PARENT ne {}} {
 903                lappend cmd -p $PARENT
 904        }
 905        if {$commit_type eq {merge}} {
 906                if {[catch {
 907                                set fd_mh [open [file join $gitdir MERGE_HEAD] r]
 908                                while {[gets $fd_mh merge_head] >= 0} {
 909                                        lappend cmd -p $merge_head
 910                                }
 911                                close $fd_mh
 912                        } err]} {
 913                        error_popup "Loading MERGE_HEAD failed:\n\n$err"
 914                        set ui_status_value {Commit failed.}
 915                        unlock_index
 916                        return
 917                }
 918        }
 919        if {$PARENT eq {}} {
 920                # git commit-tree writes to stderr during initial commit.
 921                lappend cmd 2>/dev/null
 922        }
 923        lappend cmd << $msg
 924        if {[catch {set cmt_id [eval exec $cmd]} err]} {
 925                error_popup "commit-tree failed:\n\n$err"
 926                set ui_status_value {Commit failed.}
 927                unlock_index
 928                return
 929        }
 930
 931        # -- Update the HEAD ref.
 932        #
 933        set reflogm commit
 934        if {$commit_type ne {normal}} {
 935                append reflogm " ($commit_type)"
 936        }
 937        set i [string first "\n" $msg]
 938        if {$i >= 0} {
 939                append reflogm {: } [string range $msg 0 [expr {$i - 1}]]
 940        } else {
 941                append reflogm {: } $msg
 942        }
 943        set cmd [list git update-ref -m $reflogm HEAD $cmt_id $curHEAD]
 944        if {[catch {eval exec $cmd} err]} {
 945                error_popup "update-ref failed:\n\n$err"
 946                set ui_status_value {Commit failed.}
 947                unlock_index
 948                return
 949        }
 950
 951        # -- Cleanup after ourselves.
 952        #
 953        catch {file delete [file join $gitdir MERGE_HEAD]}
 954        catch {file delete [file join $gitdir MERGE_MSG]}
 955        catch {file delete [file join $gitdir SQUASH_MSG]}
 956        catch {file delete [file join $gitdir GITGUI_MSG]}
 957
 958        # -- Let rerere do its thing.
 959        #
 960        if {[file isdirectory [file join $gitdir rr-cache]]} {
 961                catch {exec git rerere}
 962        }
 963
 964        # -- Run the post-commit hook.
 965        #
 966        set pchook [file join $gitdir hooks post-commit]
 967        if {$tcl_platform(platform) eq {windows} && [file isfile $pchook]} {
 968                set pchook [list sh -c [concat \
 969                        "if test -x \"$pchook\";" \
 970                        "then exec \"$pchook\";" \
 971                        "fi"]]
 972        } elseif {![file executable $pchook]} {
 973                set pchook {}
 974        }
 975        if {$pchook ne {}} {
 976                catch {exec $pchook &}
 977        }
 978
 979        $ui_comm delete 0.0 end
 980        $ui_comm edit modified false
 981        $ui_comm edit reset
 982
 983        if {$single_commit} do_quit
 984
 985        # -- Update status without invoking any git commands.
 986        #
 987        set commit_type normal
 988        set HEAD $cmt_id
 989        set PARENT $cmt_id
 990
 991        foreach path [array names file_states] {
 992                set s $file_states($path)
 993                set m [lindex $s 0]
 994                switch -glob -- $m {
 995                A? -
 996                M? -
 997                D? {set m _[string index $m 1]}
 998                }
 999
1000                if {$m eq {__}} {
1001                        unset file_states($path)
1002                        catch {unset selected_paths($path)}
1003                } else {
1004                        lset file_states($path) 0 $m
1005                }
1006        }
1007
1008        display_all_files
1009        unlock_index
1010        reshow_diff
1011        set ui_status_value \
1012                "Changes committed as [string range $cmt_id 0 7]."
1013}
1014
1015######################################################################
1016##
1017## fetch pull push
1018
1019proc fetch_from {remote} {
1020        set w [new_console "fetch $remote" \
1021                "Fetching new changes from $remote"]
1022        set cmd [list git fetch]
1023        lappend cmd $remote
1024        console_exec $w $cmd
1025}
1026
1027proc pull_remote {remote branch} {
1028        global HEAD commit_type file_states repo_config
1029
1030        if {![lock_index update]} return
1031
1032        # -- Our in memory state should match the repository.
1033        #
1034        repository_state curHEAD cur_type
1035        if {$commit_type ne $cur_type || $HEAD ne $curHEAD} {
1036                error_popup {Last scanned state does not match repository state.
1037
1038Its highly likely that another Git program modified the
1039repository since our last scan.  A rescan is required
1040before a pull can be started.
1041}
1042                unlock_index
1043                rescan {set ui_status_value {Ready.}}
1044                return
1045        }
1046
1047        # -- No differences should exist before a pull.
1048        #
1049        if {[array size file_states] != 0} {
1050                error_popup {Uncommitted but modified files are present.
1051
1052You should not perform a pull with unmodified files in your working
1053directory as Git would be unable to recover from an incorrect merge.
1054
1055Commit or throw away all changes before starting a pull operation.
1056}
1057                unlock_index
1058                return
1059        }
1060
1061        set w [new_console "pull $remote $branch" \
1062                "Pulling new changes from branch $branch in $remote"]
1063        set cmd [list git pull]
1064        if {$repo_config(gui.pullsummary) eq {false}} {
1065                lappend cmd --no-summary
1066        }
1067        lappend cmd $remote
1068        lappend cmd $branch
1069        console_exec $w $cmd [list post_pull_remote $remote $branch]
1070}
1071
1072proc post_pull_remote {remote branch success} {
1073        global HEAD PARENT commit_type
1074        global ui_status_value
1075
1076        unlock_index
1077        if {$success} {
1078                repository_state HEAD commit_type
1079                set PARENT $HEAD
1080                set $ui_status_value "Pulling $branch from $remote complete."
1081        } else {
1082                set m "Conflicts detected while pulling $branch from $remote."
1083                rescan "set ui_status_value {$m}"
1084        }
1085}
1086
1087proc push_to {remote} {
1088        set w [new_console "push $remote" \
1089                "Pushing changes to $remote"]
1090        set cmd [list git push]
1091        lappend cmd $remote
1092        console_exec $w $cmd
1093}
1094
1095######################################################################
1096##
1097## ui helpers
1098
1099proc mapcol {state path} {
1100        global all_cols ui_other
1101
1102        if {[catch {set r $all_cols($state)}]} {
1103                puts "error: no column for state={$state} $path"
1104                return $ui_other
1105        }
1106        return $r
1107}
1108
1109proc mapicon {state path} {
1110        global all_icons
1111
1112        if {[catch {set r $all_icons($state)}]} {
1113                puts "error: no icon for state={$state} $path"
1114                return file_plain
1115        }
1116        return $r
1117}
1118
1119proc mapdesc {state path} {
1120        global all_descs
1121
1122        if {[catch {set r $all_descs($state)}]} {
1123                puts "error: no desc for state={$state} $path"
1124                return $state
1125        }
1126        return $r
1127}
1128
1129proc escape_path {path} {
1130        regsub -all "\n" $path "\\n" path
1131        return $path
1132}
1133
1134proc short_path {path} {
1135        return [escape_path [lindex [file split $path] end]]
1136}
1137
1138set next_icon_id 0
1139
1140proc merge_state {path new_state} {
1141        global file_states next_icon_id
1142
1143        set s0 [string index $new_state 0]
1144        set s1 [string index $new_state 1]
1145
1146        if {[catch {set info $file_states($path)}]} {
1147                set state __
1148                set icon n[incr next_icon_id]
1149        } else {
1150                set state [lindex $info 0]
1151                set icon [lindex $info 1]
1152        }
1153
1154        if {$s0 eq {?}} {
1155                set s0 [string index $state 0]
1156        } elseif {$s0 eq {_}} {
1157                set s0 _
1158        }
1159
1160        if {$s1 eq {?}} {
1161                set s1 [string index $state 1]
1162        } elseif {$s1 eq {_}} {
1163                set s1 _
1164        }
1165
1166        set file_states($path) [list $s0$s1 $icon]
1167        return $state
1168}
1169
1170proc display_file {path state} {
1171        global file_states file_lists selected_paths rescan_active
1172
1173        set old_m [merge_state $path $state]
1174        if {$rescan_active > 0} return
1175
1176        set s $file_states($path)
1177        set new_m [lindex $s 0]
1178        set new_w [mapcol $new_m $path] 
1179        set old_w [mapcol $old_m $path]
1180        set new_icon [mapicon $new_m $path]
1181
1182        if {$new_w ne $old_w} {
1183                set lno [lsearch -sorted $file_lists($old_w) $path]
1184                if {$lno >= 0} {
1185                        incr lno
1186                        $old_w conf -state normal
1187                        $old_w delete $lno.0 [expr {$lno + 1}].0
1188                        $old_w conf -state disabled
1189                }
1190
1191                lappend file_lists($new_w) $path
1192                set file_lists($new_w) [lsort $file_lists($new_w)]
1193                set lno [lsearch -sorted $file_lists($new_w) $path]
1194                incr lno
1195                $new_w conf -state normal
1196                $new_w image create $lno.0 \
1197                        -align center -padx 5 -pady 1 \
1198                        -name [lindex $s 1] \
1199                        -image $new_icon
1200                $new_w insert $lno.1 "[escape_path $path]\n"
1201                if {[catch {set in_sel $selected_paths($path)}]} {
1202                        set in_sel 0
1203                }
1204                if {$in_sel} {
1205                        $new_w tag add in_sel $lno.0 [expr {$lno + 1}].0
1206                }
1207                $new_w conf -state disabled
1208        } elseif {$new_icon ne [mapicon $old_m $path]} {
1209                $new_w conf -state normal
1210                $new_w image conf [lindex $s 1] -image $new_icon
1211                $new_w conf -state disabled
1212        }
1213}
1214
1215proc display_all_files {} {
1216        global ui_index ui_other
1217        global file_states file_lists
1218        global last_clicked selected_paths
1219
1220        $ui_index conf -state normal
1221        $ui_other conf -state normal
1222
1223        $ui_index delete 0.0 end
1224        $ui_other delete 0.0 end
1225        set last_clicked {}
1226
1227        set file_lists($ui_index) [list]
1228        set file_lists($ui_other) [list]
1229
1230        foreach path [lsort [array names file_states]] {
1231                set s $file_states($path)
1232                set m [lindex $s 0]
1233                set w [mapcol $m $path]
1234                lappend file_lists($w) $path
1235                set lno [expr {[lindex [split [$w index end] .] 0] - 1}]
1236                $w image create end \
1237                        -align center -padx 5 -pady 1 \
1238                        -name [lindex $s 1] \
1239                        -image [mapicon $m $path]
1240                $w insert end "[escape_path $path]\n"
1241                if {[catch {set in_sel $selected_paths($path)}]} {
1242                        set in_sel 0
1243                }
1244                if {$in_sel} {
1245                        $w tag add in_sel $lno.0 [expr {$lno + 1}].0
1246                }
1247        }
1248
1249        $ui_index conf -state disabled
1250        $ui_other conf -state disabled
1251}
1252
1253proc update_index {msg pathList after} {
1254        global update_index_cp ui_status_value
1255
1256        if {![lock_index update]} return
1257
1258        set update_index_cp 0
1259        set pathList [lsort $pathList]
1260        set totalCnt [llength $pathList]
1261        set batch [expr {int($totalCnt * .01) + 1}]
1262        if {$batch > 25} {set batch 25}
1263
1264        set ui_status_value [format \
1265                "$msg... %i/%i files (%.2f%%)" \
1266                $update_index_cp \
1267                $totalCnt \
1268                0.0]
1269        set fd [open "| git update-index --add --remove -z --stdin" w]
1270        fconfigure $fd \
1271                -blocking 0 \
1272                -buffering full \
1273                -buffersize 512 \
1274                -translation binary
1275        fileevent $fd writable [list \
1276                write_update_index \
1277                $fd \
1278                $pathList \
1279                $totalCnt \
1280                $batch \
1281                $msg \
1282                $after \
1283                ]
1284}
1285
1286proc write_update_index {fd pathList totalCnt batch msg after} {
1287        global update_index_cp ui_status_value
1288        global file_states current_diff
1289
1290        if {$update_index_cp >= $totalCnt} {
1291                close $fd
1292                unlock_index
1293                uplevel #0 $after
1294                return
1295        }
1296
1297        for {set i $batch} \
1298                {$update_index_cp < $totalCnt && $i > 0} \
1299                {incr i -1} {
1300                set path [lindex $pathList $update_index_cp]
1301                incr update_index_cp
1302
1303                switch -glob -- [lindex $file_states($path) 0] {
1304                AD -
1305                MD -
1306                _D {set new D_}
1307
1308                _M -
1309                MM -
1310                M_ {set new M_}
1311
1312                _O -
1313                AM -
1314                A_ {set new A_}
1315
1316                ?? {continue}
1317                }
1318
1319                puts -nonewline $fd $path
1320                puts -nonewline $fd "\0"
1321                display_file $path $new
1322        }
1323
1324        set ui_status_value [format \
1325                "$msg... %i/%i files (%.2f%%)" \
1326                $update_index_cp \
1327                $totalCnt \
1328                [expr {100.0 * $update_index_cp / $totalCnt}]]
1329}
1330
1331######################################################################
1332##
1333## remote management
1334
1335proc load_all_remotes {} {
1336        global gitdir all_remotes repo_config
1337
1338        set all_remotes [list]
1339        set rm_dir [file join $gitdir remotes]
1340        if {[file isdirectory $rm_dir]} {
1341                set all_remotes [concat $all_remotes [glob \
1342                        -types f \
1343                        -tails \
1344                        -nocomplain \
1345                        -directory $rm_dir *]]
1346        }
1347
1348        foreach line [array names repo_config remote.*.url] {
1349                if {[regexp ^remote\.(.*)\.url\$ $line line name]} {
1350                        lappend all_remotes $name
1351                }
1352        }
1353
1354        set all_remotes [lsort -unique $all_remotes]
1355}
1356
1357proc populate_fetch_menu {m} {
1358        global gitdir all_remotes repo_config
1359
1360        foreach r $all_remotes {
1361                set enable 0
1362                if {![catch {set a $repo_config(remote.$r.url)}]} {
1363                        if {![catch {set a $repo_config(remote.$r.fetch)}]} {
1364                                set enable 1
1365                        }
1366                } else {
1367                        catch {
1368                                set fd [open [file join $gitdir remotes $r] r]
1369                                while {[gets $fd n] >= 0} {
1370                                        if {[regexp {^Pull:[ \t]*([^:]+):} $n]} {
1371                                                set enable 1
1372                                                break
1373                                        }
1374                                }
1375                                close $fd
1376                        }
1377                }
1378
1379                if {$enable} {
1380                        $m add command \
1381                                -label "Fetch from $r..." \
1382                                -command [list fetch_from $r] \
1383                                -font font_ui
1384                }
1385        }
1386}
1387
1388proc populate_push_menu {m} {
1389        global gitdir all_remotes repo_config
1390
1391        foreach r $all_remotes {
1392                set enable 0
1393                if {![catch {set a $repo_config(remote.$r.url)}]} {
1394                        if {![catch {set a $repo_config(remote.$r.push)}]} {
1395                                set enable 1
1396                        }
1397                } else {
1398                        catch {
1399                                set fd [open [file join $gitdir remotes $r] r]
1400                                while {[gets $fd n] >= 0} {
1401                                        if {[regexp {^Push:[ \t]*([^:]+):} $n]} {
1402                                                set enable 1
1403                                                break
1404                                        }
1405                                }
1406                                close $fd
1407                        }
1408                }
1409
1410                if {$enable} {
1411                        $m add command \
1412                                -label "Push to $r..." \
1413                                -command [list push_to $r] \
1414                                -font font_ui
1415                }
1416        }
1417}
1418
1419proc populate_pull_menu {m} {
1420        global gitdir repo_config all_remotes disable_on_lock
1421
1422        foreach remote $all_remotes {
1423                set rb {}
1424                if {[array get repo_config remote.$remote.url] ne {}} {
1425                        if {[array get repo_config remote.$remote.fetch] ne {}} {
1426                                regexp {^([^:]+):} \
1427                                        [lindex $repo_config(remote.$remote.fetch) 0] \
1428                                        line rb
1429                        }
1430                } else {
1431                        catch {
1432                                set fd [open [file join $gitdir remotes $remote] r]
1433                                while {[gets $fd line] >= 0} {
1434                                        if {[regexp {^Pull:[ \t]*([^:]+):} $line line rb]} {
1435                                                break
1436                                        }
1437                                }
1438                                close $fd
1439                        }
1440                }
1441
1442                set rb_short $rb
1443                regsub ^refs/heads/ $rb {} rb_short
1444                if {$rb_short ne {}} {
1445                        $m add command \
1446                                -label "Branch $rb_short from $remote..." \
1447                                -command [list pull_remote $remote $rb] \
1448                                -font font_ui
1449                        lappend disable_on_lock \
1450                                [list $m entryconf [$m index last] -state]
1451                }
1452        }
1453}
1454
1455######################################################################
1456##
1457## icons
1458
1459set filemask {
1460#define mask_width 14
1461#define mask_height 15
1462static unsigned char mask_bits[] = {
1463   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
1464   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
1465   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f};
1466}
1467
1468image create bitmap file_plain -background white -foreground black -data {
1469#define plain_width 14
1470#define plain_height 15
1471static unsigned char plain_bits[] = {
1472   0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
1473   0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10,
1474   0x02, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1475} -maskdata $filemask
1476
1477image create bitmap file_mod -background white -foreground blue -data {
1478#define mod_width 14
1479#define mod_height 15
1480static unsigned char mod_bits[] = {
1481   0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
1482   0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
1483   0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
1484} -maskdata $filemask
1485
1486image create bitmap file_fulltick -background white -foreground "#007000" -data {
1487#define file_fulltick_width 14
1488#define file_fulltick_height 15
1489static unsigned char file_fulltick_bits[] = {
1490   0xfe, 0x01, 0x02, 0x1a, 0x02, 0x0c, 0x02, 0x0c, 0x02, 0x16, 0x02, 0x16,
1491   0x02, 0x13, 0x00, 0x13, 0x86, 0x11, 0x8c, 0x11, 0xd8, 0x10, 0xf2, 0x10,
1492   0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1493} -maskdata $filemask
1494
1495image create bitmap file_parttick -background white -foreground "#005050" -data {
1496#define parttick_width 14
1497#define parttick_height 15
1498static unsigned char parttick_bits[] = {
1499   0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
1500   0x7a, 0x14, 0x02, 0x16, 0x02, 0x13, 0x8a, 0x11, 0xda, 0x10, 0x72, 0x10,
1501   0x22, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1502} -maskdata $filemask
1503
1504image create bitmap file_question -background white -foreground black -data {
1505#define file_question_width 14
1506#define file_question_height 15
1507static unsigned char file_question_bits[] = {
1508   0xfe, 0x01, 0x02, 0x02, 0xe2, 0x04, 0xf2, 0x09, 0x1a, 0x1b, 0x0a, 0x13,
1509   0x82, 0x11, 0xc2, 0x10, 0x62, 0x10, 0x62, 0x10, 0x02, 0x10, 0x62, 0x10,
1510   0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1511} -maskdata $filemask
1512
1513image create bitmap file_removed -background white -foreground red -data {
1514#define file_removed_width 14
1515#define file_removed_height 15
1516static unsigned char file_removed_bits[] = {
1517   0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
1518   0x1a, 0x16, 0x32, 0x13, 0xe2, 0x11, 0xc2, 0x10, 0xe2, 0x11, 0x32, 0x13,
1519   0x1a, 0x16, 0x02, 0x10, 0xfe, 0x1f};
1520} -maskdata $filemask
1521
1522image create bitmap file_merge -background white -foreground blue -data {
1523#define file_merge_width 14
1524#define file_merge_height 15
1525static unsigned char file_merge_bits[] = {
1526   0xfe, 0x01, 0x02, 0x03, 0x62, 0x05, 0x62, 0x09, 0x62, 0x1f, 0x62, 0x10,
1527   0xfa, 0x11, 0xf2, 0x10, 0x62, 0x10, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
1528   0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
1529} -maskdata $filemask
1530
1531set ui_index .vpane.files.index.list
1532set ui_other .vpane.files.other.list
1533set max_status_desc 0
1534foreach i {
1535                {__ i plain    "Unmodified"}
1536                {_M i mod      "Modified"}
1537                {M_ i fulltick "Included in commit"}
1538                {MM i parttick "Partially included"}
1539
1540                {_O o plain    "Untracked"}
1541                {A_ o fulltick "Added by commit"}
1542                {AM o parttick "Partially added"}
1543                {AD o question "Added (but now gone)"}
1544
1545                {_D i question "Missing"}
1546                {D_ i removed  "Removed by commit"}
1547                {DD i removed  "Removed by commit"}
1548                {DO i removed  "Removed (still exists)"}
1549
1550                {UM i merge    "Merge conflicts"}
1551                {U_ i merge    "Merge conflicts"}
1552        } {
1553        if {$max_status_desc < [string length [lindex $i 3]]} {
1554                set max_status_desc [string length [lindex $i 3]]
1555        }
1556        if {[lindex $i 1] eq {i}} {
1557                set all_cols([lindex $i 0]) $ui_index
1558        } else {
1559                set all_cols([lindex $i 0]) $ui_other
1560        }
1561        set all_icons([lindex $i 0]) file_[lindex $i 2]
1562        set all_descs([lindex $i 0]) [lindex $i 3]
1563}
1564unset filemask i
1565
1566######################################################################
1567##
1568## util
1569
1570proc is_MacOSX {} {
1571        global tcl_platform tk_library
1572        if {$tcl_platform(platform) eq {unix}
1573                && $tcl_platform(os) eq {Darwin}
1574                && [string match /Library/Frameworks/* $tk_library]} {
1575                return 1
1576        }
1577        return 0
1578}
1579
1580proc bind_button3 {w cmd} {
1581        bind $w <Any-Button-3> $cmd
1582        if {[is_MacOSX]} {
1583                bind $w <Control-Button-1> $cmd
1584        }
1585}
1586
1587proc incr_font_size {font {amt 1}} {
1588        set sz [font configure $font -size]
1589        incr sz $amt
1590        font configure $font -size $sz
1591        font configure ${font}bold -size $sz
1592}
1593
1594proc hook_failed_popup {hook msg} {
1595        global gitdir appname
1596
1597        set w .hookfail
1598        toplevel $w
1599
1600        frame $w.m
1601        label $w.m.l1 -text "$hook hook failed:" \
1602                -anchor w \
1603                -justify left \
1604                -font font_uibold
1605        text $w.m.t \
1606                -background white -borderwidth 1 \
1607                -relief sunken \
1608                -width 80 -height 10 \
1609                -font font_diff \
1610                -yscrollcommand [list $w.m.sby set]
1611        label $w.m.l2 \
1612                -text {You must correct the above errors before committing.} \
1613                -anchor w \
1614                -justify left \
1615                -font font_uibold
1616        scrollbar $w.m.sby -command [list $w.m.t yview]
1617        pack $w.m.l1 -side top -fill x
1618        pack $w.m.l2 -side bottom -fill x
1619        pack $w.m.sby -side right -fill y
1620        pack $w.m.t -side left -fill both -expand 1
1621        pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
1622
1623        $w.m.t insert 1.0 $msg
1624        $w.m.t conf -state disabled
1625
1626        button $w.ok -text OK \
1627                -width 15 \
1628                -font font_ui \
1629                -command "destroy $w"
1630        pack $w.ok -side bottom -anchor e -pady 10 -padx 10
1631
1632        bind $w <Visibility> "grab $w; focus $w"
1633        bind $w <Key-Return> "destroy $w"
1634        wm title $w "$appname ([lindex [file split \
1635                [file normalize [file dirname $gitdir]]] \
1636                end]): error"
1637        tkwait window $w
1638}
1639
1640set next_console_id 0
1641
1642proc new_console {short_title long_title} {
1643        global next_console_id console_data
1644        set w .console[incr next_console_id]
1645        set console_data($w) [list $short_title $long_title]
1646        return [console_init $w]
1647}
1648
1649proc console_init {w} {
1650        global console_cr console_data
1651        global gitdir appname M1B
1652
1653        set console_cr($w) 1.0
1654        toplevel $w
1655        frame $w.m
1656        label $w.m.l1 -text "[lindex $console_data($w) 1]:" \
1657                -anchor w \
1658                -justify left \
1659                -font font_uibold
1660        text $w.m.t \
1661                -background white -borderwidth 1 \
1662                -relief sunken \
1663                -width 80 -height 10 \
1664                -font font_diff \
1665                -state disabled \
1666                -yscrollcommand [list $w.m.sby set]
1667        label $w.m.s -text {Working... please wait...} \
1668                -anchor w \
1669                -justify left \
1670                -font font_uibold
1671        scrollbar $w.m.sby -command [list $w.m.t yview]
1672        pack $w.m.l1 -side top -fill x
1673        pack $w.m.s -side bottom -fill x
1674        pack $w.m.sby -side right -fill y
1675        pack $w.m.t -side left -fill both -expand 1
1676        pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
1677
1678        menu $w.ctxm -tearoff 0
1679        $w.ctxm add command -label "Copy" \
1680                -font font_ui \
1681                -command "tk_textCopy $w.m.t"
1682        $w.ctxm add command -label "Select All" \
1683                -font font_ui \
1684                -command "$w.m.t tag add sel 0.0 end"
1685        $w.ctxm add command -label "Copy All" \
1686                -font font_ui \
1687                -command "
1688                        $w.m.t tag add sel 0.0 end
1689                        tk_textCopy $w.m.t
1690                        $w.m.t tag remove sel 0.0 end
1691                "
1692
1693        button $w.ok -text {Close} \
1694                -font font_ui \
1695                -state disabled \
1696                -command "destroy $w"
1697        pack $w.ok -side bottom -anchor e -pady 10 -padx 10
1698
1699        bind_button3 $w.m.t "tk_popup $w.ctxm %X %Y"
1700        bind $w.m.t <$M1B-Key-a> "$w.m.t tag add sel 0.0 end;break"
1701        bind $w.m.t <$M1B-Key-A> "$w.m.t tag add sel 0.0 end;break"
1702        bind $w <Visibility> "focus $w"
1703        wm title $w "$appname ([lindex [file split \
1704                [file normalize [file dirname $gitdir]]] \
1705                end]): [lindex $console_data($w) 0]"
1706        return $w
1707}
1708
1709proc console_exec {w cmd {after {}}} {
1710        global tcl_platform
1711
1712        # -- Windows tosses the enviroment when we exec our child.
1713        #    But most users need that so we have to relogin. :-(
1714        #
1715        if {$tcl_platform(platform) eq {windows}} {
1716                set cmd [list sh --login -c "cd \"[pwd]\" && [join $cmd { }]"]
1717        }
1718
1719        # -- Tcl won't let us redirect both stdout and stderr to
1720        #    the same pipe.  So pass it through cat...
1721        #
1722        set cmd [concat | $cmd |& cat]
1723
1724        set fd_f [open $cmd r]
1725        fconfigure $fd_f -blocking 0 -translation binary
1726        fileevent $fd_f readable [list console_read $w $fd_f $after]
1727}
1728
1729proc console_read {w fd after} {
1730        global console_cr console_data
1731
1732        set buf [read $fd]
1733        if {$buf ne {}} {
1734                if {![winfo exists $w]} {console_init $w}
1735                $w.m.t conf -state normal
1736                set c 0
1737                set n [string length $buf]
1738                while {$c < $n} {
1739                        set cr [string first "\r" $buf $c]
1740                        set lf [string first "\n" $buf $c]
1741                        if {$cr < 0} {set cr [expr {$n + 1}]}
1742                        if {$lf < 0} {set lf [expr {$n + 1}]}
1743
1744                        if {$lf < $cr} {
1745                                $w.m.t insert end [string range $buf $c $lf]
1746                                set console_cr($w) [$w.m.t index {end -1c}]
1747                                set c $lf
1748                                incr c
1749                        } else {
1750                                $w.m.t delete $console_cr($w) end
1751                                $w.m.t insert end "\n"
1752                                $w.m.t insert end [string range $buf $c $cr]
1753                                set c $cr
1754                                incr c
1755                        }
1756                }
1757                $w.m.t conf -state disabled
1758                $w.m.t see end
1759        }
1760
1761        fconfigure $fd -blocking 1
1762        if {[eof $fd]} {
1763                if {[catch {close $fd}]} {
1764                        if {![winfo exists $w]} {console_init $w}
1765                        $w.m.s conf -background red -text {Error: Command Failed}
1766                        $w.ok conf -state normal
1767                        set ok 0
1768                } elseif {[winfo exists $w]} {
1769                        $w.m.s conf -background green -text {Success}
1770                        $w.ok conf -state normal
1771                        set ok 1
1772                }
1773                array unset console_cr $w
1774                array unset console_data $w
1775                if {$after ne {}} {
1776                        uplevel #0 $after $ok
1777                }
1778                return
1779        }
1780        fconfigure $fd -blocking 0
1781}
1782
1783######################################################################
1784##
1785## ui commands
1786
1787set starting_gitk_msg {Please wait... Starting gitk...}
1788
1789proc do_gitk {} {
1790        global tcl_platform ui_status_value starting_gitk_msg
1791
1792        set ui_status_value $starting_gitk_msg
1793        after 10000 {
1794                if {$ui_status_value eq $starting_gitk_msg} {
1795                        set ui_status_value {Ready.}
1796                }
1797        }
1798
1799        if {$tcl_platform(platform) eq {windows}} {
1800                exec sh -c gitk &
1801        } else {
1802                exec gitk &
1803        }
1804}
1805
1806proc do_repack {} {
1807        set w [new_console "repack" "Repacking the object database"]
1808        set cmd [list git repack]
1809        lappend cmd -a
1810        lappend cmd -d
1811        console_exec $w $cmd
1812}
1813
1814set is_quitting 0
1815
1816proc do_quit {} {
1817        global gitdir ui_comm is_quitting repo_config
1818
1819        if {$is_quitting} return
1820        set is_quitting 1
1821
1822        # -- Stash our current commit buffer.
1823        #
1824        set save [file join $gitdir GITGUI_MSG]
1825        set msg [string trim [$ui_comm get 0.0 end]]
1826        if {[$ui_comm edit modified] && $msg ne {}} {
1827                catch {
1828                        set fd [open $save w]
1829                        puts $fd [string trim [$ui_comm get 0.0 end]]
1830                        close $fd
1831                }
1832        } elseif {$msg eq {} && [file exists $save]} {
1833                file delete $save
1834        }
1835
1836        # -- Stash our current window geometry into this repository.
1837        #
1838        set cfg_geometry [list]
1839        lappend cfg_geometry [wm geometry .]
1840        lappend cfg_geometry [lindex [.vpane sash coord 0] 1]
1841        lappend cfg_geometry [lindex [.vpane.files sash coord 0] 0]
1842        if {[catch {set rc_geometry $repo_config(gui.geometry)}]} {
1843                set rc_geometry {}
1844        }
1845        if {$cfg_geometry ne $rc_geometry} {
1846                catch {exec git repo-config gui.geometry $cfg_geometry}
1847        }
1848
1849        destroy .
1850}
1851
1852proc do_rescan {} {
1853        rescan {set ui_status_value {Ready.}}
1854}
1855
1856proc include_helper {txt paths} {
1857        global file_states current_diff
1858
1859        if {![lock_index begin-update]} return
1860
1861        set pathList [list]
1862        set after {}
1863        foreach path $paths {
1864                switch -- [lindex $file_states($path) 0] {
1865                AM -
1866                MM -
1867                _M -
1868                _D {
1869                        lappend pathList $path
1870                        if {$path eq $current_diff} {
1871                                set after {reshow_diff;}
1872                        }
1873                }
1874                }
1875        }
1876        if {$pathList eq {}} {
1877                unlock_index
1878        } else {
1879                update_index \
1880                        $txt \
1881                        $pathList \
1882                        [concat $after {set ui_status_value {Ready to commit.}}]
1883        }
1884}
1885
1886proc do_include_selection {} {
1887        global current_diff selected_paths
1888
1889        if {[array size selected_paths] > 0} {
1890                include_helper \
1891                        {Including selected files} \
1892                        [array names selected_paths]
1893        } elseif {$current_diff ne {}} {
1894                include_helper \
1895                        "Including [short_path $current_diff]" \
1896                        [list $current_diff]
1897        }
1898}
1899
1900proc do_include_all {} {
1901        global file_states
1902        include_helper \
1903                {Including all modified files} \
1904                [array names file_states]
1905}
1906
1907set GIT_COMMITTER_IDENT {}
1908
1909proc do_signoff {} {
1910        global ui_comm GIT_COMMITTER_IDENT
1911
1912        if {$GIT_COMMITTER_IDENT eq {}} {
1913                if {[catch {set me [exec git var GIT_COMMITTER_IDENT]} err]} {
1914                        error_popup "Unable to obtain your identity:\n\n$err"
1915                        return
1916                }
1917                if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \
1918                        $me me GIT_COMMITTER_IDENT]} {
1919                        error_popup "Invalid GIT_COMMITTER_IDENT:\n\n$me"
1920                        return
1921                }
1922        }
1923
1924        set sob "Signed-off-by: $GIT_COMMITTER_IDENT"
1925        set last [$ui_comm get {end -1c linestart} {end -1c}]
1926        if {$last ne $sob} {
1927                $ui_comm edit separator
1928                if {$last ne {}
1929                        && ![regexp {^[A-Z][A-Za-z]*-[A-Za-z-]+: *} $last]} {
1930                        $ui_comm insert end "\n"
1931                }
1932                $ui_comm insert end "\n$sob"
1933                $ui_comm edit separator
1934                $ui_comm see end
1935        }
1936}
1937
1938proc do_amend_last {} {
1939        load_last_commit
1940}
1941
1942proc do_commit {} {
1943        commit_tree
1944}
1945
1946proc do_options {} {
1947        global appname gitdir font_descs
1948        global repo_config global_config
1949        global repo_config_new global_config_new
1950
1951        array unset repo_config_new
1952        array unset global_config_new
1953        foreach name [array names repo_config] {
1954                set repo_config_new($name) $repo_config($name)
1955        }
1956        load_config 1
1957        foreach name [array names repo_config] {
1958                switch -- $name {
1959                gui.diffcontext {continue}
1960                }
1961                set repo_config_new($name) $repo_config($name)
1962        }
1963        foreach name [array names global_config] {
1964                set global_config_new($name) $global_config($name)
1965        }
1966        set reponame [lindex [file split \
1967                [file normalize [file dirname $gitdir]]] \
1968                end]
1969
1970        set w .options_editor
1971        toplevel $w
1972        wm geometry $w "+[winfo rootx .]+[winfo rooty .]"
1973
1974        label $w.header -text "$appname Options" \
1975                -font font_uibold
1976        pack $w.header -side top -fill x
1977
1978        frame $w.buttons
1979        button $w.buttons.restore -text {Restore Defaults} \
1980                -font font_ui \
1981                -command do_restore_defaults
1982        pack $w.buttons.restore -side left
1983        button $w.buttons.save -text Save \
1984                -font font_ui \
1985                -command [list do_save_config $w]
1986        pack $w.buttons.save -side right
1987        button $w.buttons.cancel -text {Cancel} \
1988                -font font_ui \
1989                -command [list destroy $w]
1990        pack $w.buttons.cancel -side right
1991        pack $w.buttons -side bottom -fill x -pady 10 -padx 10
1992
1993        labelframe $w.repo -text "$reponame Repository" \
1994                -font font_ui \
1995                -relief raised -borderwidth 2
1996        labelframe $w.global -text {Global (All Repositories)} \
1997                -font font_ui \
1998                -relief raised -borderwidth 2
1999        pack $w.repo -side left -fill both -expand 1 -pady 5 -padx 5
2000        pack $w.global -side right -fill both -expand 1 -pady 5 -padx 5
2001
2002        foreach option {
2003                {b partialinclude {Allow Partially Included Files}}
2004                {b pullsummary {Show Pull Summary}}
2005                {b trustmtime  {Trust File Modification Timestamps}}
2006                {i diffcontext {Number of Diff Context Lines}}
2007                } {
2008                set type [lindex $option 0]
2009                set name [lindex $option 1]
2010                set text [lindex $option 2]
2011                foreach f {repo global} {
2012                        switch $type {
2013                        b {
2014                                checkbutton $w.$f.$name -text $text \
2015                                        -variable ${f}_config_new(gui.$name) \
2016                                        -onvalue true \
2017                                        -offvalue false \
2018                                        -font font_ui
2019                                pack $w.$f.$name -side top -anchor w
2020                        }
2021                        i {
2022                                frame $w.$f.$name
2023                                label $w.$f.$name.l -text "$text:" -font font_ui
2024                                pack $w.$f.$name.l -side left -anchor w -fill x
2025                                spinbox $w.$f.$name.v \
2026                                        -textvariable ${f}_config_new(gui.$name) \
2027                                        -from 1 -to 99 -increment 1 \
2028                                        -width 3 \
2029                                        -font font_ui
2030                                pack $w.$f.$name.v -side right -anchor e
2031                                pack $w.$f.$name -side top -anchor w -fill x
2032                        }
2033                        }
2034                }
2035        }
2036
2037        set all_fonts [lsort [font families]]
2038        foreach option $font_descs {
2039                set name [lindex $option 0]
2040                set font [lindex $option 1]
2041                set text [lindex $option 2]
2042
2043                set global_config_new(gui.$font^^family) \
2044                        [font configure $font -family]
2045                set global_config_new(gui.$font^^size) \
2046                        [font configure $font -size]
2047
2048                frame $w.global.$name
2049                label $w.global.$name.l -text "$text:" -font font_ui
2050                pack $w.global.$name.l -side left -anchor w -fill x
2051                eval tk_optionMenu $w.global.$name.family \
2052                        global_config_new(gui.$font^^family) \
2053                        $all_fonts
2054                spinbox $w.global.$name.size \
2055                        -textvariable global_config_new(gui.$font^^size) \
2056                        -from 2 -to 80 -increment 1 \
2057                        -width 3 \
2058                        -font font_ui
2059                pack $w.global.$name.size -side right -anchor e
2060                pack $w.global.$name.family -side right -anchor e
2061                pack $w.global.$name -side top -anchor w -fill x
2062        }
2063
2064        bind $w <Visibility> "grab $w; focus $w"
2065        bind $w <Key-Escape> "destroy $w"
2066        wm title $w "$appname ($reponame): Options"
2067        tkwait window $w
2068}
2069
2070proc do_restore_defaults {} {
2071        global font_descs default_config repo_config
2072        global repo_config_new global_config_new
2073
2074        foreach name [array names default_config] {
2075                set repo_config_new($name) $default_config($name)
2076                set global_config_new($name) $default_config($name)
2077        }
2078
2079        foreach option $font_descs {
2080                set name [lindex $option 0]
2081                set repo_config(gui.$name) $default_config(gui.$name)
2082        }
2083        apply_config
2084
2085        foreach option $font_descs {
2086                set name [lindex $option 0]
2087                set font [lindex $option 1]
2088                set global_config_new(gui.$font^^family) \
2089                        [font configure $font -family]
2090                set global_config_new(gui.$font^^size) \
2091                        [font configure $font -size]
2092        }
2093}
2094
2095proc do_save_config {w} {
2096        if {[catch {save_config} err]} {
2097                error_popup "Failed to completely save options:\n\n$err"
2098        }
2099        reshow_diff
2100        destroy $w
2101}
2102
2103proc do_windows_shortcut {} {
2104        global gitdir appname argv0
2105
2106        set reponame [lindex [file split \
2107                [file normalize [file dirname $gitdir]]] \
2108                end]
2109
2110        if {[catch {
2111                set desktop [exec cygpath \
2112                        --windows \
2113                        --absolute \
2114                        --long-name \
2115                        --desktop]
2116                }]} {
2117                        set desktop .
2118        }
2119        set fn [tk_getSaveFile \
2120                -parent . \
2121                -title "$appname ($reponame): Create Desktop Icon" \
2122                -initialdir $desktop \
2123                -initialfile "Git $reponame.bat"]
2124        if {$fn != {}} {
2125                if {[catch {
2126                                set fd [open $fn w]
2127                                set sh [exec cygpath \
2128                                        --windows \
2129                                        --absolute \
2130                                        --long-name \
2131                                        /bin/sh]
2132                                set me [exec cygpath \
2133                                        --unix \
2134                                        --absolute \
2135                                        $argv0]
2136                                set gd [exec cygpath \
2137                                        --unix \
2138                                        --absolute \
2139                                        $gitdir]
2140                                regsub -all ' $me "'\\''" me
2141                                regsub -all ' $gd "'\\''" gd
2142                                puts -nonewline $fd "\"$sh\" --login -c \""
2143                                puts -nonewline $fd "GIT_DIR='$gd'"
2144                                puts -nonewline $fd " '$me'"
2145                                puts $fd "&\""
2146                                close $fd
2147                        } err]} {
2148                        error_popup "Cannot write script:\n\n$err"
2149                }
2150        }
2151}
2152
2153proc do_macosx_app {} {
2154        global gitdir appname argv0 env
2155
2156        set reponame [lindex [file split \
2157                [file normalize [file dirname $gitdir]]] \
2158                end]
2159
2160        set fn [tk_getSaveFile \
2161                -parent . \
2162                -title "$appname ($reponame): Create Desktop Icon" \
2163                -initialdir [file join $env(HOME) Desktop] \
2164                -initialfile "Git $reponame.app"]
2165        if {$fn != {}} {
2166                if {[catch {
2167                                set Contents [file join $fn Contents]
2168                                set MacOS [file join $Contents MacOS]
2169                                set exe [file join $MacOS git-gui]
2170
2171                                file mkdir $MacOS
2172
2173                                set fd [open [file join $Contents PkgInfo] w]
2174                                puts -nonewline $fd {APPL????}
2175                                close $fd
2176
2177                                set fd [open [file join $Contents Info.plist] w]
2178                                puts $fd {<?xml version="1.0" encoding="UTF-8"?>
2179<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2180<plist version="1.0">
2181<dict>
2182        <key>CFBundleDevelopmentRegion</key>
2183        <string>English</string>
2184        <key>CFBundleExecutable</key>
2185        <string>git-gui</string>
2186        <key>CFBundleIdentifier</key>
2187        <string>org.spearce.git-gui</string>
2188        <key>CFBundleInfoDictionaryVersion</key>
2189        <string>6.0</string>
2190        <key>CFBundlePackageType</key>
2191        <string>APPL</string>
2192        <key>CFBundleSignature</key>
2193        <string>????</string>
2194        <key>CFBundleVersion</key>
2195        <string>1.0</string>
2196        <key>NSPrincipalClass</key>
2197        <string>NSApplication</string>
2198</dict>
2199</plist>}
2200                                close $fd
2201
2202                                set fd [open $exe w]
2203                                set gd [file normalize $gitdir]
2204                                set ep [file normalize [exec git --exec-path]]
2205                                regsub -all ' $gd "'\\''" gd
2206                                regsub -all ' $ep "'\\''" ep
2207                                puts $fd "#!/bin/sh"
2208                                foreach name [array names env] {
2209                                        if {[string match GIT_* $name]} {
2210                                                regsub -all ' $env($name) "'\\''" v
2211                                                puts $fd "export $name='$v'"
2212                                        }
2213                                }
2214                                puts $fd "export PATH='$ep':\$PATH"
2215                                puts $fd "export GIT_DIR='$gd'"
2216                                puts $fd "exec [file normalize $argv0]"
2217                                close $fd
2218
2219                                file attributes $exe -permissions u+x,g+x,o+x
2220                        } err]} {
2221                        error_popup "Cannot write icon:\n\n$err"
2222                }
2223        }
2224}
2225
2226proc toggle_or_diff {w x y} {
2227        global file_lists current_diff ui_index ui_other
2228        global last_clicked selected_paths
2229
2230        set pos [split [$w index @$x,$y] .]
2231        set lno [lindex $pos 0]
2232        set col [lindex $pos 1]
2233        set path [lindex $file_lists($w) [expr {$lno - 1}]]
2234        if {$path eq {}} {
2235                set last_clicked {}
2236                return
2237        }
2238
2239        set last_clicked [list $w $lno]
2240        array unset selected_paths
2241        $ui_index tag remove in_sel 0.0 end
2242        $ui_other tag remove in_sel 0.0 end
2243
2244        if {$col == 0} {
2245                if {$current_diff eq $path} {
2246                        set after {reshow_diff;}
2247                } else {
2248                        set after {}
2249                }
2250                update_index \
2251                        "Including [short_path $path]" \
2252                        [list $path] \
2253                        [concat $after {set ui_status_value {Ready.}}]
2254        } else {
2255                show_diff $path $w $lno
2256        }
2257}
2258
2259proc add_one_to_selection {w x y} {
2260        global file_lists
2261        global last_clicked selected_paths
2262
2263        set pos [split [$w index @$x,$y] .]
2264        set lno [lindex $pos 0]
2265        set col [lindex $pos 1]
2266        set path [lindex $file_lists($w) [expr {$lno - 1}]]
2267        if {$path eq {}} {
2268                set last_clicked {}
2269                return
2270        }
2271
2272        set last_clicked [list $w $lno]
2273        if {[catch {set in_sel $selected_paths($path)}]} {
2274                set in_sel 0
2275        }
2276        if {$in_sel} {
2277                unset selected_paths($path)
2278                $w tag remove in_sel $lno.0 [expr {$lno + 1}].0
2279        } else {
2280                set selected_paths($path) 1
2281                $w tag add in_sel $lno.0 [expr {$lno + 1}].0
2282        }
2283}
2284
2285proc add_range_to_selection {w x y} {
2286        global file_lists
2287        global last_clicked selected_paths
2288
2289        if {[lindex $last_clicked 0] ne $w} {
2290                toggle_or_diff $w $x $y
2291                return
2292        }
2293
2294        set pos [split [$w index @$x,$y] .]
2295        set lno [lindex $pos 0]
2296        set lc [lindex $last_clicked 1]
2297        if {$lc < $lno} {
2298                set begin $lc
2299                set end $lno
2300        } else {
2301                set begin $lno
2302                set end $lc
2303        }
2304
2305        foreach path [lrange $file_lists($w) \
2306                [expr {$begin - 1}] \
2307                [expr {$end - 1}]] {
2308                set selected_paths($path) 1
2309        }
2310        $w tag add in_sel $begin.0 [expr {$end + 1}].0
2311}
2312
2313######################################################################
2314##
2315## config defaults
2316
2317set cursor_ptr arrow
2318font create font_diff -family Courier -size 10
2319font create font_ui
2320catch {
2321        label .dummy
2322        eval font configure font_ui [font actual [.dummy cget -font]]
2323        destroy .dummy
2324}
2325
2326font create font_uibold
2327font create font_diffbold
2328
2329set M1B M1
2330set M1T M1
2331if {$tcl_platform(platform) eq {windows}} {
2332        set M1B Control
2333        set M1T Ctrl
2334} elseif {[is_MacOSX]} {
2335        set M1B M1
2336        set M1T Cmd
2337}
2338
2339proc apply_config {} {
2340        global repo_config font_descs
2341
2342        foreach option $font_descs {
2343                set name [lindex $option 0]
2344                set font [lindex $option 1]
2345                if {[catch {
2346                        foreach {cn cv} $repo_config(gui.$name) {
2347                                font configure $font $cn $cv
2348                        }
2349                        } err]} {
2350                        error_popup "Invalid font specified in gui.$name:\n\n$err"
2351                }
2352                foreach {cn cv} [font configure $font] {
2353                        font configure ${font}bold $cn $cv
2354                }
2355                font configure ${font}bold -weight bold
2356        }
2357}
2358
2359set default_config(gui.trustmtime) false
2360set default_config(gui.pullsummary) true
2361set default_config(gui.partialinclude) false
2362set default_config(gui.diffcontext) 5
2363set default_config(gui.fontui) [font configure font_ui]
2364set default_config(gui.fontdiff) [font configure font_diff]
2365set font_descs {
2366        {fontui   font_ui   {Main Font}}
2367        {fontdiff font_diff {Diff/Console Font}}
2368}
2369load_config 0
2370apply_config
2371
2372######################################################################
2373##
2374## ui construction
2375
2376# -- Menu Bar
2377menu .mbar -tearoff 0
2378.mbar add cascade -label Project -menu .mbar.project
2379.mbar add cascade -label Edit -menu .mbar.edit
2380.mbar add cascade -label Commit -menu .mbar.commit
2381if {!$single_commit} {
2382        .mbar add cascade -label Fetch -menu .mbar.fetch
2383        .mbar add cascade -label Pull -menu .mbar.pull
2384        .mbar add cascade -label Push -menu .mbar.push
2385}
2386. configure -menu .mbar
2387
2388# -- Project Menu
2389menu .mbar.project
2390.mbar.project add command -label Visualize \
2391        -command do_gitk \
2392        -font font_ui
2393if {!$single_commit} {
2394        .mbar.project add command -label {Repack Database} \
2395                -command do_repack \
2396                -font font_ui
2397
2398        if {$tcl_platform(platform) eq {windows}} {
2399                .mbar.project add command \
2400                        -label {Create Desktop Icon} \
2401                        -command do_windows_shortcut \
2402                        -font font_ui
2403        } elseif {[is_MacOSX]} {
2404                .mbar.project add command \
2405                        -label {Create Desktop Icon} \
2406                        -command do_macosx_app \
2407                        -font font_ui
2408        }
2409}
2410.mbar.project add command -label Quit \
2411        -command do_quit \
2412        -accelerator $M1T-Q \
2413        -font font_ui
2414
2415# -- Edit Menu
2416#
2417menu .mbar.edit
2418.mbar.edit add command -label Undo \
2419        -command {catch {[focus] edit undo}} \
2420        -accelerator $M1T-Z \
2421        -font font_ui
2422.mbar.edit add command -label Redo \
2423        -command {catch {[focus] edit redo}} \
2424        -accelerator $M1T-Y \
2425        -font font_ui
2426.mbar.edit add separator
2427.mbar.edit add command -label Cut \
2428        -command {catch {tk_textCut [focus]}} \
2429        -accelerator $M1T-X \
2430        -font font_ui
2431.mbar.edit add command -label Copy \
2432        -command {catch {tk_textCopy [focus]}} \
2433        -accelerator $M1T-C \
2434        -font font_ui
2435.mbar.edit add command -label Paste \
2436        -command {catch {tk_textPaste [focus]; [focus] see insert}} \
2437        -accelerator $M1T-V \
2438        -font font_ui
2439.mbar.edit add command -label Delete \
2440        -command {catch {[focus] delete sel.first sel.last}} \
2441        -accelerator Del \
2442        -font font_ui
2443.mbar.edit add separator
2444.mbar.edit add command -label {Select All} \
2445        -command {catch {[focus] tag add sel 0.0 end}} \
2446        -accelerator $M1T-A \
2447        -font font_ui
2448.mbar.edit add separator
2449.mbar.edit add command -label {Options...} \
2450        -command do_options \
2451        -font font_ui
2452
2453# -- Commit Menu
2454menu .mbar.commit
2455.mbar.commit add command -label Rescan \
2456        -command do_rescan \
2457        -accelerator F5 \
2458        -font font_ui
2459lappend disable_on_lock \
2460        [list .mbar.commit entryconf [.mbar.commit index last] -state]
2461.mbar.commit add command -label {Amend Last Commit} \
2462        -command do_amend_last \
2463        -font font_ui
2464lappend disable_on_lock \
2465        [list .mbar.commit entryconf [.mbar.commit index last] -state]
2466.mbar.commit add command -label {Include Selected Files} \
2467        -command do_include_selection \
2468        -font font_ui
2469lappend disable_on_lock \
2470        [list .mbar.commit entryconf [.mbar.commit index last] -state]
2471.mbar.commit add command -label {Include All Files} \
2472        -command do_include_all \
2473        -accelerator $M1T-I \
2474        -font font_ui
2475lappend disable_on_lock \
2476        [list .mbar.commit entryconf [.mbar.commit index last] -state]
2477.mbar.commit add command -label {Sign Off} \
2478        -command do_signoff \
2479        -accelerator $M1T-S \
2480        -font font_ui
2481.mbar.commit add command -label Commit \
2482        -command do_commit \
2483        -accelerator $M1T-Return \
2484        -font font_ui
2485lappend disable_on_lock \
2486        [list .mbar.commit entryconf [.mbar.commit index last] -state]
2487
2488if {!$single_commit} {
2489        # -- Fetch Menu
2490        menu .mbar.fetch
2491
2492        # -- Pull Menu
2493        menu .mbar.pull
2494
2495        # -- Push Menu
2496        menu .mbar.push
2497}
2498
2499# -- Main Window Layout
2500panedwindow .vpane -orient vertical
2501panedwindow .vpane.files -orient horizontal
2502.vpane add .vpane.files -sticky nsew -height 100 -width 400
2503pack .vpane -anchor n -side top -fill both -expand 1
2504
2505# -- Index File List
2506frame .vpane.files.index -height 100 -width 400
2507label .vpane.files.index.title -text {Modified Files} \
2508        -background green \
2509        -font font_ui
2510text $ui_index -background white -borderwidth 0 \
2511        -width 40 -height 10 \
2512        -font font_ui \
2513        -cursor $cursor_ptr \
2514        -yscrollcommand {.vpane.files.index.sb set} \
2515        -state disabled
2516scrollbar .vpane.files.index.sb -command [list $ui_index yview]
2517pack .vpane.files.index.title -side top -fill x
2518pack .vpane.files.index.sb -side right -fill y
2519pack $ui_index -side left -fill both -expand 1
2520.vpane.files add .vpane.files.index -sticky nsew
2521
2522# -- Other (Add) File List
2523frame .vpane.files.other -height 100 -width 100
2524label .vpane.files.other.title -text {Untracked Files} \
2525        -background red \
2526        -font font_ui
2527text $ui_other -background white -borderwidth 0 \
2528        -width 40 -height 10 \
2529        -font font_ui \
2530        -cursor $cursor_ptr \
2531        -yscrollcommand {.vpane.files.other.sb set} \
2532        -state disabled
2533scrollbar .vpane.files.other.sb -command [list $ui_other yview]
2534pack .vpane.files.other.title -side top -fill x
2535pack .vpane.files.other.sb -side right -fill y
2536pack $ui_other -side left -fill both -expand 1
2537.vpane.files add .vpane.files.other -sticky nsew
2538
2539foreach i [list $ui_index $ui_other] {
2540        $i tag conf in_diff -font font_uibold
2541        $i tag conf in_sel \
2542                -background [$i cget -foreground] \
2543                -foreground [$i cget -background]
2544}
2545unset i
2546
2547# -- Diff and Commit Area
2548frame .vpane.lower -height 300 -width 400
2549frame .vpane.lower.commarea
2550frame .vpane.lower.diff -relief sunken -borderwidth 1
2551pack .vpane.lower.commarea -side top -fill x
2552pack .vpane.lower.diff -side bottom -fill both -expand 1
2553.vpane add .vpane.lower -stick nsew
2554
2555# -- Commit Area Buttons
2556frame .vpane.lower.commarea.buttons
2557label .vpane.lower.commarea.buttons.l -text {} \
2558        -anchor w \
2559        -justify left \
2560        -font font_ui
2561pack .vpane.lower.commarea.buttons.l -side top -fill x
2562pack .vpane.lower.commarea.buttons -side left -fill y
2563
2564button .vpane.lower.commarea.buttons.rescan -text {Rescan} \
2565        -command do_rescan \
2566        -font font_ui
2567pack .vpane.lower.commarea.buttons.rescan -side top -fill x
2568lappend disable_on_lock \
2569        {.vpane.lower.commarea.buttons.rescan conf -state}
2570
2571button .vpane.lower.commarea.buttons.amend -text {Amend Last} \
2572        -command do_amend_last \
2573        -font font_ui
2574pack .vpane.lower.commarea.buttons.amend -side top -fill x
2575lappend disable_on_lock \
2576        {.vpane.lower.commarea.buttons.amend conf -state}
2577
2578button .vpane.lower.commarea.buttons.incall -text {Include All} \
2579        -command do_include_all \
2580        -font font_ui
2581pack .vpane.lower.commarea.buttons.incall -side top -fill x
2582lappend disable_on_lock \
2583        {.vpane.lower.commarea.buttons.incall conf -state}
2584
2585button .vpane.lower.commarea.buttons.signoff -text {Sign Off} \
2586        -command do_signoff \
2587        -font font_ui
2588pack .vpane.lower.commarea.buttons.signoff -side top -fill x
2589
2590button .vpane.lower.commarea.buttons.commit -text {Commit} \
2591        -command do_commit \
2592        -font font_ui
2593pack .vpane.lower.commarea.buttons.commit -side top -fill x
2594lappend disable_on_lock \
2595        {.vpane.lower.commarea.buttons.commit conf -state}
2596
2597# -- Commit Message Buffer
2598frame .vpane.lower.commarea.buffer
2599set ui_comm .vpane.lower.commarea.buffer.t
2600set ui_coml .vpane.lower.commarea.buffer.l
2601label $ui_coml -text {Commit Message:} \
2602        -anchor w \
2603        -justify left \
2604        -font font_ui
2605proc trace_commit_type {varname args} {
2606        global ui_coml commit_type
2607        switch -glob -- $commit_type {
2608        initial       {set txt {Initial Commit Message:}}
2609        amend         {set txt {Amended Commit Message:}}
2610        amend-initial {set txt {Amended Initial Commit Message:}}
2611        merge         {set txt {Merge Commit Message:}}
2612        *             {set txt {Commit Message:}}
2613        }
2614        $ui_coml conf -text $txt
2615}
2616trace add variable commit_type write trace_commit_type
2617text $ui_comm -background white -borderwidth 1 \
2618        -undo true \
2619        -maxundo 20 \
2620        -autoseparators true \
2621        -relief sunken \
2622        -width 75 -height 9 -wrap none \
2623        -font font_diff \
2624        -yscrollcommand {.vpane.lower.commarea.buffer.sby set}
2625scrollbar .vpane.lower.commarea.buffer.sby \
2626        -command [list $ui_comm yview]
2627pack $ui_coml -side top -fill x
2628pack .vpane.lower.commarea.buffer.sby -side right -fill y
2629pack $ui_comm -side left -fill y
2630pack .vpane.lower.commarea.buffer -side left -fill y
2631
2632# -- Commit Message Buffer Context Menu
2633#
2634set ctxm .vpane.lower.commarea.buffer.ctxm
2635menu $ctxm -tearoff 0
2636$ctxm add command \
2637        -label {Cut} \
2638        -font font_ui \
2639        -command {tk_textCut $ui_comm}
2640$ctxm add command \
2641        -label {Copy} \
2642        -font font_ui \
2643        -command {tk_textCopy $ui_comm}
2644$ctxm add command \
2645        -label {Paste} \
2646        -font font_ui \
2647        -command {tk_textPaste $ui_comm}
2648$ctxm add command \
2649        -label {Delete} \
2650        -font font_ui \
2651        -command {$ui_comm delete sel.first sel.last}
2652$ctxm add separator
2653$ctxm add command \
2654        -label {Select All} \
2655        -font font_ui \
2656        -command {$ui_comm tag add sel 0.0 end}
2657$ctxm add command \
2658        -label {Copy All} \
2659        -font font_ui \
2660        -command {
2661                $ui_comm tag add sel 0.0 end
2662                tk_textCopy $ui_comm
2663                $ui_comm tag remove sel 0.0 end
2664        }
2665$ctxm add separator
2666$ctxm add command \
2667        -label {Sign Off} \
2668        -font font_ui \
2669        -command do_signoff
2670bind_button3 $ui_comm "tk_popup $ctxm %X %Y"
2671
2672# -- Diff Header
2673set current_diff {}
2674set diff_actions [list]
2675proc trace_current_diff {varname args} {
2676        global current_diff diff_actions file_states
2677        if {$current_diff eq {}} {
2678                set s {}
2679                set f {}
2680                set p {}
2681                set o disabled
2682        } else {
2683                set p $current_diff
2684                set s [mapdesc [lindex $file_states($p) 0] $p]
2685                set f {File:}
2686                set p [escape_path $p]
2687                set o normal
2688        }
2689
2690        .vpane.lower.diff.header.status configure -text $s
2691        .vpane.lower.diff.header.file configure -text $f
2692        .vpane.lower.diff.header.path configure -text $p
2693        foreach w $diff_actions {
2694                uplevel #0 $w $o
2695        }
2696}
2697trace add variable current_diff write trace_current_diff
2698
2699frame .vpane.lower.diff.header -background orange
2700label .vpane.lower.diff.header.status \
2701        -background orange \
2702        -width $max_status_desc \
2703        -anchor w \
2704        -justify left \
2705        -font font_ui
2706label .vpane.lower.diff.header.file \
2707        -background orange \
2708        -anchor w \
2709        -justify left \
2710        -font font_ui
2711label .vpane.lower.diff.header.path \
2712        -background orange \
2713        -anchor w \
2714        -justify left \
2715        -font font_ui
2716pack .vpane.lower.diff.header.status -side left
2717pack .vpane.lower.diff.header.file -side left
2718pack .vpane.lower.diff.header.path -fill x
2719set ctxm .vpane.lower.diff.header.ctxm
2720menu $ctxm -tearoff 0
2721$ctxm add command \
2722        -label {Copy} \
2723        -font font_ui \
2724        -command {
2725                clipboard clear
2726                clipboard append \
2727                        -format STRING \
2728                        -type STRING \
2729                        -- $current_diff
2730        }
2731lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2732bind_button3 .vpane.lower.diff.header.path "tk_popup $ctxm %X %Y"
2733
2734# -- Diff Body
2735frame .vpane.lower.diff.body
2736set ui_diff .vpane.lower.diff.body.t
2737text $ui_diff -background white -borderwidth 0 \
2738        -width 80 -height 15 -wrap none \
2739        -font font_diff \
2740        -xscrollcommand {.vpane.lower.diff.body.sbx set} \
2741        -yscrollcommand {.vpane.lower.diff.body.sby set} \
2742        -state disabled
2743scrollbar .vpane.lower.diff.body.sbx -orient horizontal \
2744        -command [list $ui_diff xview]
2745scrollbar .vpane.lower.diff.body.sby -orient vertical \
2746        -command [list $ui_diff yview]
2747pack .vpane.lower.diff.body.sbx -side bottom -fill x
2748pack .vpane.lower.diff.body.sby -side right -fill y
2749pack $ui_diff -side left -fill both -expand 1
2750pack .vpane.lower.diff.header -side top -fill x
2751pack .vpane.lower.diff.body -side bottom -fill both -expand 1
2752
2753$ui_diff tag conf dm -foreground red
2754$ui_diff tag conf dp -foreground blue
2755$ui_diff tag conf di -foreground {#00a000}
2756$ui_diff tag conf dni -foreground {#a000a0}
2757$ui_diff tag conf da -font font_diffbold
2758$ui_diff tag conf bold -font font_diffbold
2759
2760# -- Diff Body Context Menu
2761#
2762set ctxm .vpane.lower.diff.body.ctxm
2763menu $ctxm -tearoff 0
2764$ctxm add command \
2765        -label {Copy} \
2766        -font font_ui \
2767        -command {tk_textCopy $ui_diff}
2768lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2769$ctxm add command \
2770        -label {Select All} \
2771        -font font_ui \
2772        -command {$ui_diff tag add sel 0.0 end}
2773lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2774$ctxm add command \
2775        -label {Copy All} \
2776        -font font_ui \
2777        -command {
2778                $ui_diff tag add sel 0.0 end
2779                tk_textCopy $ui_diff
2780                $ui_diff tag remove sel 0.0 end
2781        }
2782lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2783$ctxm add separator
2784$ctxm add command \
2785        -label {Decrease Font Size} \
2786        -font font_ui \
2787        -command {incr_font_size font_diff -1}
2788lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2789$ctxm add command \
2790        -label {Increase Font Size} \
2791        -font font_ui \
2792        -command {incr_font_size font_diff 1}
2793lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2794$ctxm add separator
2795$ctxm add command \
2796        -label {Show Less Context} \
2797        -font font_ui \
2798        -command {if {$repo_config(gui.diffcontext) >= 2} {
2799                incr repo_config(gui.diffcontext) -1
2800                reshow_diff
2801        }}
2802lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2803$ctxm add command \
2804        -label {Show More Context} \
2805        -font font_ui \
2806        -command {
2807                incr repo_config(gui.diffcontext)
2808                reshow_diff
2809        }
2810lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state]
2811$ctxm add separator
2812$ctxm add command -label {Options...} \
2813        -font font_ui \
2814        -command do_options
2815bind_button3 $ui_diff "tk_popup $ctxm %X %Y"
2816
2817# -- Status Bar
2818#
2819set ui_status_value {Initializing...}
2820label .status -textvariable ui_status_value \
2821        -anchor w \
2822        -justify left \
2823        -borderwidth 1 \
2824        -relief sunken \
2825        -font font_ui
2826pack .status -anchor w -side bottom -fill x
2827
2828# -- Load geometry
2829#
2830catch {
2831set gm $repo_config(gui.geometry)
2832wm geometry . [lindex $gm 0]
2833.vpane sash place 0 \
2834        [lindex [.vpane sash coord 0] 0] \
2835        [lindex $gm 1]
2836.vpane.files sash place 0 \
2837        [lindex $gm 2] \
2838        [lindex [.vpane.files sash coord 0] 1]
2839unset gm
2840}
2841
2842# -- Key Bindings
2843#
2844bind $ui_comm <$M1B-Key-Return> {do_commit;break}
2845bind $ui_comm <$M1B-Key-i> {do_include_all;break}
2846bind $ui_comm <$M1B-Key-I> {do_include_all;break}
2847bind $ui_comm <$M1B-Key-x> {tk_textCut %W;break}
2848bind $ui_comm <$M1B-Key-X> {tk_textCut %W;break}
2849bind $ui_comm <$M1B-Key-c> {tk_textCopy %W;break}
2850bind $ui_comm <$M1B-Key-C> {tk_textCopy %W;break}
2851bind $ui_comm <$M1B-Key-v> {tk_textPaste %W; %W see insert; break}
2852bind $ui_comm <$M1B-Key-V> {tk_textPaste %W; %W see insert; break}
2853bind $ui_comm <$M1B-Key-a> {%W tag add sel 0.0 end;break}
2854bind $ui_comm <$M1B-Key-A> {%W tag add sel 0.0 end;break}
2855
2856bind $ui_diff <$M1B-Key-x> {tk_textCopy %W;break}
2857bind $ui_diff <$M1B-Key-X> {tk_textCopy %W;break}
2858bind $ui_diff <$M1B-Key-c> {tk_textCopy %W;break}
2859bind $ui_diff <$M1B-Key-C> {tk_textCopy %W;break}
2860bind $ui_diff <$M1B-Key-v> {break}
2861bind $ui_diff <$M1B-Key-V> {break}
2862bind $ui_diff <$M1B-Key-a> {%W tag add sel 0.0 end;break}
2863bind $ui_diff <$M1B-Key-A> {%W tag add sel 0.0 end;break}
2864bind $ui_diff <Key-Up>     {catch {%W yview scroll -1 units};break}
2865bind $ui_diff <Key-Down>   {catch {%W yview scroll  1 units};break}
2866bind $ui_diff <Key-Left>   {catch {%W xview scroll -1 units};break}
2867bind $ui_diff <Key-Right>  {catch {%W xview scroll  1 units};break}
2868
2869bind .   <Destroy> do_quit
2870bind all <Key-F5> do_rescan
2871bind all <$M1B-Key-r> do_rescan
2872bind all <$M1B-Key-R> do_rescan
2873bind .   <$M1B-Key-s> do_signoff
2874bind .   <$M1B-Key-S> do_signoff
2875bind .   <$M1B-Key-i> do_include_all
2876bind .   <$M1B-Key-I> do_include_all
2877bind .   <$M1B-Key-Return> do_commit
2878bind all <$M1B-Key-q> do_quit
2879bind all <$M1B-Key-Q> do_quit
2880bind all <$M1B-Key-w> {destroy [winfo toplevel %W]}
2881bind all <$M1B-Key-W> {destroy [winfo toplevel %W]}
2882foreach i [list $ui_index $ui_other] {
2883        bind $i <Button-1>       "toggle_or_diff         $i %x %y; break"
2884        bind $i <$M1B-Button-1>  "add_one_to_selection   $i %x %y; break"
2885        bind $i <Shift-Button-1> "add_range_to_selection $i %x %y; break"
2886}
2887unset i
2888
2889set file_lists($ui_index) [list]
2890set file_lists($ui_other) [list]
2891set current_diff {}
2892
2893wm title . "$appname ([file normalize [file dirname $gitdir]])"
2894focus -force $ui_comm
2895if {!$single_commit} {
2896        load_all_remotes
2897        populate_fetch_menu .mbar.fetch
2898        populate_pull_menu .mbar.pull
2899        populate_push_menu .mbar.push
2900}
2901after 1 do_rescan