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