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