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