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