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