git-guion commit git-gui: Simplified format of geometry configuration. (c4fe772)
   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 load_repo_config {} {
  18        global repo_config
  19        global cfg_trust_mtime
  20
  21        array unset repo_config
  22        catch {
  23                set fd_rc [open "| git repo-config --list" r]
  24                while {[gets $fd_rc line] >= 0} {
  25                        if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
  26                                lappend repo_config($name) $value
  27                        }
  28                }
  29                close $fd_rc
  30        }
  31
  32        if {[catch {set cfg_trust_mtime \
  33                        [lindex $repo_config(gui.trustmtime) 0]
  34                }]} {
  35                set cfg_trust_mtime false
  36        }
  37}
  38
  39proc save_my_config {} {
  40        global repo_config
  41        global cfg_trust_mtime
  42
  43        if {[catch {set rc_trustMTime $repo_config(gui.trustmtime)}]} {
  44                set rc_trustMTime [list false]
  45        }
  46        if {$cfg_trust_mtime != [lindex $rc_trustMTime 0]} {
  47                exec git repo-config gui.trustMTime $cfg_trust_mtime
  48                set repo_config(gui.trustmtime) [list $cfg_trust_mtime]
  49        }
  50
  51        set cfg_geometry [wm geometry .]
  52        append cfg_geometry " [lindex [.vpane sash coord 0] 1]"
  53        append cfg_geometry " [lindex [.vpane.files sash coord 0] 0]"
  54        if {[catch {set rc_geometry $repo_config(gui.geometry)}]} {
  55                set rc_geometry [list [list]]
  56        }
  57        if {$cfg_geometry != [lindex $rc_geometry 0]} {
  58                exec git repo-config gui.geometry $cfg_geometry
  59                set repo_config(gui.geometry) [list $cfg_geometry]
  60        }
  61}
  62
  63proc error_popup {msg} {
  64        global gitdir appname
  65
  66        set title $appname
  67        if {$gitdir != {}} {
  68                append title { (}
  69                append title [lindex \
  70                        [file split [file normalize [file dirname $gitdir]]] \
  71                        end]
  72                append title {)}
  73        }
  74        tk_messageBox \
  75                -parent . \
  76                -icon error \
  77                -type ok \
  78                -title "$title: error" \
  79                -message $msg
  80}
  81
  82######################################################################
  83##
  84## repository setup
  85
  86if {   [catch {set cdup [exec git rev-parse --show-cdup]} err]
  87        || [catch {set gitdir [exec git rev-parse --git-dir]} err]} {
  88        catch {wm withdraw .}
  89        error_popup "Cannot find the git directory:\n\n$err"
  90        exit 1
  91}
  92if {$cdup != ""} {
  93        cd $cdup
  94}
  95unset cdup
  96
  97if {$appname == {git-citool}} {
  98        set single_commit 1
  99}
 100
 101load_repo_config
 102
 103######################################################################
 104##
 105## task management
 106
 107set single_commit 0
 108set status_active 0
 109set diff_active 0
 110set update_active 0
 111set commit_active 0
 112set update_index_fd {}
 113
 114set disable_on_lock [list]
 115set index_lock_type none
 116
 117set HEAD {}
 118set PARENT {}
 119set commit_type {}
 120
 121proc lock_index {type} {
 122        global index_lock_type disable_on_lock
 123
 124        if {$index_lock_type == {none}} {
 125                set index_lock_type $type
 126                foreach w $disable_on_lock {
 127                        uplevel #0 $w disabled
 128                }
 129                return 1
 130        } elseif {$index_lock_type == {begin-update} && $type == {update}} {
 131                set index_lock_type $type
 132                return 1
 133        }
 134        return 0
 135}
 136
 137proc unlock_index {} {
 138        global index_lock_type disable_on_lock
 139
 140        set index_lock_type none
 141        foreach w $disable_on_lock {
 142                uplevel #0 $w normal
 143        }
 144}
 145
 146######################################################################
 147##
 148## status
 149
 150proc repository_state {hdvar ctvar} {
 151        global gitdir
 152        upvar $hdvar hd $ctvar ct
 153
 154        if {[catch {set hd [exec git rev-parse --verify HEAD]}]} {
 155                set ct initial
 156        } elseif {[file exists [file join $gitdir MERGE_HEAD]]} {
 157                set ct merge
 158        } else {
 159                set ct normal
 160        }
 161}
 162
 163proc update_status {{final Ready.}} {
 164        global HEAD PARENT commit_type
 165        global ui_index ui_other ui_status_value ui_comm
 166        global status_active file_states
 167        global cfg_trust_mtime
 168
 169        if {$status_active || ![lock_index read]} return
 170
 171        repository_state new_HEAD new_type
 172        if {$commit_type == {amend} 
 173                && $new_type == {normal}
 174                && $new_HEAD == $HEAD} {
 175        } else {
 176                set HEAD $new_HEAD
 177                set PARENT $new_HEAD
 178                set commit_type $new_type
 179        }
 180
 181        array unset file_states
 182
 183        if {![$ui_comm edit modified]
 184                || [string trim [$ui_comm get 0.0 end]] == {}} {
 185                if {[load_message GITGUI_MSG]} {
 186                } elseif {[load_message MERGE_MSG]} {
 187                } elseif {[load_message SQUASH_MSG]} {
 188                }
 189                $ui_comm edit modified false
 190                $ui_comm edit reset
 191        }
 192
 193        if {$cfg_trust_mtime == {true}} {
 194                update_status_stage2 {} $final
 195        } else {
 196                set status_active 1
 197                set ui_status_value {Refreshing file status...}
 198                set fd_rf [open "| git update-index -q --unmerged --refresh" r]
 199                fconfigure $fd_rf -blocking 0 -translation binary
 200                fileevent $fd_rf readable [list update_status_stage2 $fd_rf $final]
 201        }
 202}
 203
 204proc update_status_stage2 {fd final} {
 205        global gitdir PARENT commit_type
 206        global ui_index ui_other ui_status_value ui_comm
 207        global status_active
 208        global buf_rdi buf_rdf buf_rlo
 209
 210        if {$fd != {}} {
 211                read $fd
 212                if {![eof $fd]} return
 213                close $fd
 214        }
 215
 216        set ls_others [list | git ls-files --others -z \
 217                --exclude-per-directory=.gitignore]
 218        set info_exclude [file join $gitdir info exclude]
 219        if {[file readable $info_exclude]} {
 220                lappend ls_others "--exclude-from=$info_exclude"
 221        }
 222
 223        set buf_rdi {}
 224        set buf_rdf {}
 225        set buf_rlo {}
 226
 227        set status_active 3
 228        set ui_status_value {Scanning for modified files ...}
 229        set fd_di [open "| git diff-index --cached -z $PARENT" r]
 230        set fd_df [open "| git diff-files -z" r]
 231        set fd_lo [open $ls_others r]
 232
 233        fconfigure $fd_di -blocking 0 -translation binary
 234        fconfigure $fd_df -blocking 0 -translation binary
 235        fconfigure $fd_lo -blocking 0 -translation binary
 236        fileevent $fd_di readable [list read_diff_index $fd_di $final]
 237        fileevent $fd_df readable [list read_diff_files $fd_df $final]
 238        fileevent $fd_lo readable [list read_ls_others $fd_lo $final]
 239}
 240
 241proc load_message {file} {
 242        global gitdir ui_comm
 243
 244        set f [file join $gitdir $file]
 245        if {[file isfile $f]} {
 246                if {[catch {set fd [open $f r]}]} {
 247                        return 0
 248                }
 249                set content [string trim [read $fd]]
 250                close $fd
 251                $ui_comm delete 0.0 end
 252                $ui_comm insert end $content
 253                return 1
 254        }
 255        return 0
 256}
 257
 258proc read_diff_index {fd final} {
 259        global buf_rdi
 260
 261        append buf_rdi [read $fd]
 262        set c 0
 263        set n [string length $buf_rdi]
 264        while {$c < $n} {
 265                set z1 [string first "\0" $buf_rdi $c]
 266                if {$z1 == -1} break
 267                incr z1
 268                set z2 [string first "\0" $buf_rdi $z1]
 269                if {$z2 == -1} break
 270
 271                set c $z2
 272                incr z2 -1
 273                display_file \
 274                        [string range $buf_rdi $z1 $z2] \
 275                        [string index $buf_rdi [expr $z1 - 2]]_
 276                incr c
 277        }
 278        if {$c < $n} {
 279                set buf_rdi [string range $buf_rdi $c end]
 280        } else {
 281                set buf_rdi {}
 282        }
 283
 284        status_eof $fd buf_rdi $final
 285}
 286
 287proc read_diff_files {fd final} {
 288        global buf_rdf
 289
 290        append buf_rdf [read $fd]
 291        set c 0
 292        set n [string length $buf_rdf]
 293        while {$c < $n} {
 294                set z1 [string first "\0" $buf_rdf $c]
 295                if {$z1 == -1} break
 296                incr z1
 297                set z2 [string first "\0" $buf_rdf $z1]
 298                if {$z2 == -1} break
 299
 300                set c $z2
 301                incr z2 -1
 302                display_file \
 303                        [string range $buf_rdf $z1 $z2] \
 304                        _[string index $buf_rdf [expr $z1 - 2]]
 305                incr c
 306        }
 307        if {$c < $n} {
 308                set buf_rdf [string range $buf_rdf $c end]
 309        } else {
 310                set buf_rdf {}
 311        }
 312
 313        status_eof $fd buf_rdf $final
 314}
 315
 316proc read_ls_others {fd final} {
 317        global buf_rlo
 318
 319        append buf_rlo [read $fd]
 320        set pck [split $buf_rlo "\0"]
 321        set buf_rlo [lindex $pck end]
 322        foreach p [lrange $pck 0 end-1] {
 323                display_file $p _O
 324        }
 325        status_eof $fd buf_rlo $final
 326}
 327
 328proc status_eof {fd buf final} {
 329        global status_active ui_status_value
 330        upvar $buf to_clear
 331
 332        if {[eof $fd]} {
 333                set to_clear {}
 334                close $fd
 335
 336                if {[incr status_active -1] == 0} {
 337                        display_all_files
 338                        unlock_index
 339                        reshow_diff
 340                        set ui_status_value $final
 341                }
 342        }
 343}
 344
 345######################################################################
 346##
 347## diff
 348
 349proc clear_diff {} {
 350        global ui_diff ui_fname_value ui_fstatus_value ui_index ui_other
 351
 352        $ui_diff conf -state normal
 353        $ui_diff delete 0.0 end
 354        $ui_diff conf -state disabled
 355
 356        set ui_fname_value {}
 357        set ui_fstatus_value {}
 358
 359        $ui_index tag remove in_diff 0.0 end
 360        $ui_other tag remove in_diff 0.0 end
 361}
 362
 363proc reshow_diff {} {
 364        global ui_fname_value ui_status_value file_states
 365
 366        if {$ui_fname_value == {}
 367                || [catch {set s $file_states($ui_fname_value)}]} {
 368                clear_diff
 369        } else {
 370                show_diff $ui_fname_value
 371        }
 372}
 373
 374proc show_diff {path {w {}} {lno {}}} {
 375        global file_states file_lists
 376        global PARENT diff_3way diff_active
 377        global ui_diff ui_fname_value ui_fstatus_value ui_status_value
 378
 379        if {$diff_active || ![lock_index read]} return
 380
 381        clear_diff
 382        if {$w == {} || $lno == {}} {
 383                foreach w [array names file_lists] {
 384                        set lno [lsearch -sorted $file_lists($w) $path]
 385                        if {$lno >= 0} {
 386                                incr lno
 387                                break
 388                        }
 389                }
 390        }
 391        if {$w != {} && $lno >= 1} {
 392                $w tag add in_diff $lno.0 [expr $lno + 1].0
 393        }
 394
 395        set s $file_states($path)
 396        set m [lindex $s 0]
 397        set diff_3way 0
 398        set diff_active 1
 399        set ui_fname_value [escape_path $path]
 400        set ui_fstatus_value [mapdesc $m $path]
 401        set ui_status_value "Loading diff of [escape_path $path]..."
 402
 403        set cmd [list | git diff-index -p $PARENT -- $path]
 404        switch $m {
 405        AM {
 406        }
 407        MM {
 408                set cmd [list | git diff-index -p -c $PARENT $path]
 409        }
 410        _O {
 411                if {[catch {
 412                                set fd [open $path r]
 413                                set content [read $fd]
 414                                close $fd
 415                        } err ]} {
 416                        set diff_active 0
 417                        unlock_index
 418                        set ui_status_value "Unable to display [escape_path $path]"
 419                        error_popup "Error loading file:\n\n$err"
 420                        return
 421                }
 422                $ui_diff conf -state normal
 423                $ui_diff insert end $content
 424                $ui_diff conf -state disabled
 425                set diff_active 0
 426                unlock_index
 427                set ui_status_value {Ready.}
 428                return
 429        }
 430        }
 431
 432        if {[catch {set fd [open $cmd r]} err]} {
 433                set diff_active 0
 434                unlock_index
 435                set ui_status_value "Unable to display [escape_path $path]"
 436                error_popup "Error loading diff:\n\n$err"
 437                return
 438        }
 439
 440        fconfigure $fd -blocking 0 -translation auto
 441        fileevent $fd readable [list read_diff $fd]
 442}
 443
 444proc read_diff {fd} {
 445        global ui_diff ui_status_value diff_3way diff_active
 446
 447        while {[gets $fd line] >= 0} {
 448                if {[string match {diff --git *} $line]} continue
 449                if {[string match {diff --combined *} $line]} continue
 450                if {[string match {--- *} $line]} continue
 451                if {[string match {+++ *} $line]} continue
 452                if {[string match index* $line]} {
 453                        if {[string first , $line] >= 0} {
 454                                set diff_3way 1
 455                        }
 456                }
 457
 458                $ui_diff conf -state normal
 459                if {!$diff_3way} {
 460                        set x [string index $line 0]
 461                        switch -- $x {
 462                        "@" {set tags da}
 463                        "+" {set tags dp}
 464                        "-" {set tags dm}
 465                        default {set tags {}}
 466                        }
 467                } else {
 468                        set x [string range $line 0 1]
 469                        switch -- $x {
 470                        default {set tags {}}
 471                        "@@" {set tags da}
 472                        "++" {set tags dp; set x " +"}
 473                        " +" {set tags {di bold}; set x "++"}
 474                        "+ " {set tags dni; set x "-+"}
 475                        "--" {set tags dm; set x " -"}
 476                        " -" {set tags {dm bold}; set x "--"}
 477                        "- " {set tags di; set x "+-"}
 478                        default {set tags {}}
 479                        }
 480                        set line [string replace $line 0 1 $x]
 481                }
 482                $ui_diff insert end $line $tags
 483                $ui_diff insert end "\n"
 484                $ui_diff conf -state disabled
 485        }
 486
 487        if {[eof $fd]} {
 488                close $fd
 489                set diff_active 0
 490                unlock_index
 491                set ui_status_value {Ready.}
 492        }
 493}
 494
 495######################################################################
 496##
 497## commit
 498
 499proc load_last_commit {} {
 500        global HEAD PARENT commit_type ui_comm
 501
 502        if {$commit_type == {amend}} return
 503        if {$commit_type != {normal}} {
 504                error_popup "Can't amend a $commit_type commit."
 505                return
 506        }
 507
 508        set msg {}
 509        set parent {}
 510        set parent_count 0
 511        if {[catch {
 512                        set fd [open "| git cat-file commit $HEAD" r]
 513                        while {[gets $fd line] > 0} {
 514                                if {[string match {parent *} $line]} {
 515                                        set parent [string range $line 7 end]
 516                                        incr parent_count
 517                                }
 518                        }
 519                        set msg [string trim [read $fd]]
 520                        close $fd
 521                } err]} {
 522                error_popup "Error loading commit data for amend:\n\n$err"
 523                return
 524        }
 525
 526        if {$parent_count == 0} {
 527                set commit_type amend
 528                set HEAD {}
 529                set PARENT {}
 530                update_status
 531        } elseif {$parent_count == 1} {
 532                set commit_type amend
 533                set PARENT $parent
 534                $ui_comm delete 0.0 end
 535                $ui_comm insert end $msg
 536                $ui_comm edit modified false
 537                $ui_comm edit reset
 538                update_status
 539        } else {
 540                error_popup {You can't amend a merge commit.}
 541                return
 542        }
 543}
 544
 545proc commit_tree {} {
 546        global tcl_platform HEAD gitdir commit_type file_states
 547        global commit_active ui_status_value
 548        global ui_comm
 549
 550        if {$commit_active || ![lock_index update]} return
 551
 552        # -- Our in memory state should match the repository.
 553        #
 554        repository_state curHEAD cur_type
 555        if {$commit_type == {amend} 
 556                && $cur_type == {normal}
 557                && $curHEAD == $HEAD} {
 558        } elseif {$commit_type != $cur_type || $HEAD != $curHEAD} {
 559                error_popup {Last scanned state does not match repository state.
 560
 561Its highly likely that another Git program modified the
 562repository since our last scan.  A rescan is required
 563before committing.
 564}
 565                unlock_index
 566                update_status
 567                return
 568        }
 569
 570        # -- At least one file should differ in the index.
 571        #
 572        set files_ready 0
 573        foreach path [array names file_states] {
 574                set s $file_states($path)
 575                switch -glob -- [lindex $s 0] {
 576                _? {continue}
 577                A? -
 578                D? -
 579                M? {set files_ready 1; break}
 580                U? {
 581                        error_popup "Unmerged files cannot be committed.
 582
 583File [escape_path $path] has merge conflicts.
 584You must resolve them and include the file before committing.
 585"
 586                        unlock_index
 587                        return
 588                }
 589                default {
 590                        error_popup "Unknown file state [lindex $s 0] detected.
 591
 592File [escape_path $path] cannot be committed by this program.
 593"
 594                }
 595                }
 596        }
 597        if {!$files_ready} {
 598                error_popup {No included files to commit.
 599
 600You must include at least 1 file before you can commit.
 601}
 602                unlock_index
 603                return
 604        }
 605
 606        # -- A message is required.
 607        #
 608        set msg [string trim [$ui_comm get 1.0 end]]
 609        if {$msg == {}} {
 610                error_popup {Please supply a commit message.
 611
 612A good commit message has the following format:
 613
 614- First line: Describe in one sentance what you did.
 615- Second line: Blank
 616- Remaining lines: Describe why this change is good.
 617}
 618                unlock_index
 619                return
 620        }
 621
 622        # -- Ask the pre-commit hook for the go-ahead.
 623        #
 624        set pchook [file join $gitdir hooks pre-commit]
 625        if {$tcl_platform(platform) == {windows} && [file isfile $pchook]} {
 626                set pchook [list sh -c \
 627                        "if test -x \"$pchook\"; then exec \"$pchook\"; fi"]
 628        } elseif {[file executable $pchook]} {
 629                set pchook [list $pchook]
 630        } else {
 631                set pchook {}
 632        }
 633        if {$pchook != {} && [catch {eval exec $pchook} err]} {
 634                hook_failed_popup pre-commit $err
 635                unlock_index
 636                return
 637        }
 638
 639        # -- Write the tree in the background.
 640        #
 641        set commit_active 1
 642        set ui_status_value {Committing changes...}
 643
 644        set fd_wt [open "| git write-tree" r]
 645        fileevent $fd_wt readable [list commit_stage2 $fd_wt $curHEAD $msg]
 646}
 647
 648proc commit_stage2 {fd_wt curHEAD msg} {
 649        global single_commit gitdir HEAD PARENT commit_type
 650        global commit_active ui_status_value ui_comm
 651        global file_states
 652
 653        gets $fd_wt tree_id
 654        if {$tree_id == {} || [catch {close $fd_wt} err]} {
 655                error_popup "write-tree failed:\n\n$err"
 656                set commit_active 0
 657                set ui_status_value {Commit failed.}
 658                unlock_index
 659                return
 660        }
 661
 662        # -- Create the commit.
 663        #
 664        set cmd [list git commit-tree $tree_id]
 665        if {$PARENT != {}} {
 666                lappend cmd -p $PARENT
 667        }
 668        if {$commit_type == {merge}} {
 669                if {[catch {
 670                                set fd_mh [open [file join $gitdir MERGE_HEAD] r]
 671                                while {[gets $fd_mh merge_head] >= 0} {
 672                                        lappend cmd -p $merge_head
 673                                }
 674                                close $fd_mh
 675                        } err]} {
 676                        error_popup "Loading MERGE_HEAD failed:\n\n$err"
 677                        set commit_active 0
 678                        set ui_status_value {Commit failed.}
 679                        unlock_index
 680                        return
 681                }
 682        }
 683        if {$PARENT == {}} {
 684                # git commit-tree writes to stderr during initial commit.
 685                lappend cmd 2>/dev/null
 686        }
 687        lappend cmd << $msg
 688        if {[catch {set cmt_id [eval exec $cmd]} err]} {
 689                error_popup "commit-tree failed:\n\n$err"
 690                set commit_active 0
 691                set ui_status_value {Commit failed.}
 692                unlock_index
 693                return
 694        }
 695
 696        # -- Update the HEAD ref.
 697        #
 698        set reflogm commit
 699        if {$commit_type != {normal}} {
 700                append reflogm " ($commit_type)"
 701        }
 702        set i [string first "\n" $msg]
 703        if {$i >= 0} {
 704                append reflogm {: } [string range $msg 0 [expr $i - 1]]
 705        } else {
 706                append reflogm {: } $msg
 707        }
 708        set cmd [list git update-ref -m $reflogm HEAD $cmt_id $curHEAD]
 709        if {[catch {eval exec $cmd} err]} {
 710                error_popup "update-ref failed:\n\n$err"
 711                set commit_active 0
 712                set ui_status_value {Commit failed.}
 713                unlock_index
 714                return
 715        }
 716
 717        # -- Cleanup after ourselves.
 718        #
 719        catch {file delete [file join $gitdir MERGE_HEAD]}
 720        catch {file delete [file join $gitdir MERGE_MSG]}
 721        catch {file delete [file join $gitdir SQUASH_MSG]}
 722        catch {file delete [file join $gitdir GITGUI_MSG]}
 723
 724        # -- Let rerere do its thing.
 725        #
 726        if {[file isdirectory [file join $gitdir rr-cache]]} {
 727                catch {exec git rerere}
 728        }
 729
 730        $ui_comm delete 0.0 end
 731        $ui_comm edit modified false
 732        $ui_comm edit reset
 733
 734        if {$single_commit} do_quit
 735
 736        # -- Update status without invoking any git commands.
 737        #
 738        set commit_active 0
 739        set commit_type normal
 740        set HEAD $cmt_id
 741        set PARENT $cmt_id
 742
 743        foreach path [array names file_states] {
 744                set s $file_states($path)
 745                set m [lindex $s 0]
 746                switch -glob -- $m {
 747                A? -
 748                M? -
 749                D? {set m _[string index $m 1]}
 750                }
 751
 752                if {$m == {__}} {
 753                        unset file_states($path)
 754                } else {
 755                        lset file_states($path) 0 $m
 756                }
 757        }
 758
 759        display_all_files
 760        unlock_index
 761        reshow_diff
 762        set ui_status_value \
 763                "Changes committed as [string range $cmt_id 0 7]."
 764}
 765
 766######################################################################
 767##
 768## fetch pull push
 769
 770proc fetch_from {remote} {
 771        set w [new_console "fetch $remote" \
 772                "Fetching new changes from $remote"]
 773        set cmd [list git fetch]
 774        lappend cmd $remote
 775        console_exec $w $cmd
 776}
 777
 778proc pull_remote {remote branch} {
 779        global HEAD commit_type
 780        global file_states
 781
 782        if {![lock_index update]} return
 783
 784        # -- Our in memory state should match the repository.
 785        #
 786        repository_state curHEAD cur_type
 787        if {$commit_type != $cur_type || $HEAD != $curHEAD} {
 788                error_popup {Last scanned state does not match repository state.
 789
 790Its highly likely that another Git program modified the
 791repository since our last scan.  A rescan is required
 792before a pull can be started.
 793}
 794                unlock_index
 795                update_status
 796                return
 797        }
 798
 799        # -- No differences should exist before a pull.
 800        #
 801        if {[array size file_states] != 0} {
 802                error_popup {Uncommitted but modified files are present.
 803
 804You should not perform a pull with unmodified files in your working
 805directory as Git would be unable to recover from an incorrect merge.
 806
 807Commit or throw away all changes before starting a pull operation.
 808}
 809                unlock_index
 810                return
 811        }
 812
 813        set w [new_console "pull $remote $branch" \
 814                "Pulling new changes from branch $branch in $remote"]
 815        set cmd [list git pull]
 816        lappend cmd $remote
 817        lappend cmd $branch
 818        console_exec $w $cmd [list post_pull_remote $remote $branch]
 819}
 820
 821proc post_pull_remote {remote branch success} {
 822        global HEAD PARENT commit_type
 823        global ui_status_value
 824
 825        unlock_index
 826        if {$success} {
 827                repository_state HEAD commit_type
 828                set PARENT $HEAD
 829                set $ui_status_value {Ready.}
 830        } else {
 831                update_status "Conflicts detected while pulling $branch from $remote."
 832        }
 833}
 834
 835proc push_to {remote} {
 836        set w [new_console "push $remote" \
 837                "Pushing changes to $remote"]
 838        set cmd [list git push]
 839        lappend cmd $remote
 840        console_exec $w $cmd
 841}
 842
 843######################################################################
 844##
 845## ui helpers
 846
 847proc mapcol {state path} {
 848        global all_cols ui_other
 849
 850        if {[catch {set r $all_cols($state)}]} {
 851                puts "error: no column for state={$state} $path"
 852                return $ui_other
 853        }
 854        return $r
 855}
 856
 857proc mapicon {state path} {
 858        global all_icons
 859
 860        if {[catch {set r $all_icons($state)}]} {
 861                puts "error: no icon for state={$state} $path"
 862                return file_plain
 863        }
 864        return $r
 865}
 866
 867proc mapdesc {state path} {
 868        global all_descs
 869
 870        if {[catch {set r $all_descs($state)}]} {
 871                puts "error: no desc for state={$state} $path"
 872                return $state
 873        }
 874        return $r
 875}
 876
 877proc escape_path {path} {
 878        regsub -all "\n" $path "\\n" path
 879        return $path
 880}
 881
 882set next_icon_id 0
 883
 884proc merge_state {path new_state} {
 885        global file_states next_icon_id
 886
 887        set s0 [string index $new_state 0]
 888        set s1 [string index $new_state 1]
 889
 890        if {[catch {set info $file_states($path)}]} {
 891                set state __
 892                set icon n[incr next_icon_id]
 893        } else {
 894                set state [lindex $info 0]
 895                set icon [lindex $info 1]
 896        }
 897
 898        if {$s0 == {_}} {
 899                set s0 [string index $state 0]
 900        } elseif {$s0 == {*}} {
 901                set s0 _
 902        }
 903
 904        if {$s1 == {_}} {
 905                set s1 [string index $state 1]
 906        } elseif {$s1 == {*}} {
 907                set s1 _
 908        }
 909
 910        set file_states($path) [list $s0$s1 $icon]
 911        return $state
 912}
 913
 914proc display_file {path state} {
 915        global ui_index ui_other
 916        global file_states file_lists status_active
 917
 918        set old_m [merge_state $path $state]
 919        if {$status_active} return
 920
 921        set s $file_states($path)
 922        set new_m [lindex $s 0]
 923        set new_w [mapcol $new_m $path] 
 924        set old_w [mapcol $old_m $path]
 925        set new_icon [mapicon $new_m $path]
 926
 927        if {$new_w != $old_w} {
 928                set lno [lsearch -sorted $file_lists($old_w) $path]
 929                if {$lno >= 0} {
 930                        incr lno
 931                        $old_w conf -state normal
 932                        $old_w delete $lno.0 [expr $lno + 1].0
 933                        $old_w conf -state disabled
 934                }
 935
 936                lappend file_lists($new_w) $path
 937                set file_lists($new_w) [lsort $file_lists($new_w)]
 938                set lno [lsearch -sorted $file_lists($new_w) $path]
 939                incr lno
 940                $new_w conf -state normal
 941                $new_w image create $lno.0 \
 942                        -align center -padx 5 -pady 1 \
 943                        -name [lindex $s 1] \
 944                        -image $new_icon
 945                $new_w insert $lno.1 "[escape_path $path]\n"
 946                $new_w conf -state disabled
 947        } elseif {$new_icon != [mapicon $old_m $path]} {
 948                $new_w conf -state normal
 949                $new_w image conf [lindex $s 1] -image $new_icon
 950                $new_w conf -state disabled
 951        }
 952}
 953
 954proc display_all_files {} {
 955        global ui_index ui_other file_states file_lists
 956
 957        $ui_index conf -state normal
 958        $ui_other conf -state normal
 959
 960        $ui_index delete 0.0 end
 961        $ui_other delete 0.0 end
 962
 963        array unset file_lists
 964        foreach path [lsort [array names file_states]] {
 965                set s $file_states($path)
 966                set m [lindex $s 0]
 967                set w [mapcol $m $path]
 968                lappend file_lists($w) $path
 969                $w image create end \
 970                        -align center -padx 5 -pady 1 \
 971                        -name [lindex $s 1] \
 972                        -image [mapicon $m $path]
 973                $w insert end "[escape_path $path]\n"
 974        }
 975
 976        $ui_index conf -state disabled
 977        $ui_other conf -state disabled
 978}
 979
 980proc with_update_index {body} {
 981        global update_index_fd
 982
 983        if {$update_index_fd == {}} {
 984                if {![lock_index update]} return
 985                set update_index_fd [open \
 986                        "| git update-index --add --remove -z --stdin" \
 987                        w]
 988                fconfigure $update_index_fd -translation binary
 989                uplevel 1 $body
 990                close $update_index_fd
 991                set update_index_fd {}
 992                unlock_index
 993        } else {
 994                uplevel 1 $body
 995        }
 996}
 997
 998proc update_index {path} {
 999        global update_index_fd
1000
1001        if {$update_index_fd == {}} {
1002                error {not in with_update_index}
1003        } else {
1004                puts -nonewline $update_index_fd "$path\0"
1005        }
1006}
1007
1008proc toggle_mode {path} {
1009        global file_states ui_fname_value
1010
1011        set s $file_states($path)
1012        set m [lindex $s 0]
1013
1014        switch -- $m {
1015        AM -
1016        _O {set new A*}
1017        _M -
1018        MM {set new M*}
1019        AD -
1020        _D {set new D*}
1021        default {return}
1022        }
1023
1024        with_update_index {update_index $path}
1025        display_file $path $new
1026        if {$ui_fname_value == $path} {
1027                show_diff $path
1028        }
1029}
1030
1031######################################################################
1032##
1033## remote management
1034
1035proc load_all_remotes {} {
1036        global gitdir all_remotes repo_config
1037
1038        set all_remotes [list]
1039        set rm_dir [file join $gitdir remotes]
1040        if {[file isdirectory $rm_dir]} {
1041                set all_remotes [concat $all_remotes [glob \
1042                        -types f \
1043                        -tails \
1044                        -nocomplain \
1045                        -directory $rm_dir *]]
1046        }
1047
1048        foreach line [array names repo_config remote.*.url] {
1049                if {[regexp ^remote\.(.*)\.url\$ $line line name]} {
1050                        lappend all_remotes $name
1051                }
1052        }
1053
1054        set all_remotes [lsort -unique $all_remotes]
1055}
1056
1057proc populate_remote_menu {m pfx op} {
1058        global all_remotes font_ui
1059
1060        foreach remote $all_remotes {
1061                $m add command -label "$pfx $remote..." \
1062                        -command [list $op $remote] \
1063                        -font $font_ui
1064        }
1065}
1066
1067proc populate_pull_menu {m} {
1068        global gitdir repo_config all_remotes font_ui disable_on_lock
1069
1070        foreach remote $all_remotes {
1071                set rb {}
1072                if {[array get repo_config remote.$remote.url] != {}} {
1073                        if {[array get repo_config remote.$remote.fetch] != {}} {
1074                                regexp {^([^:]+):} \
1075                                        [lindex $repo_config(remote.$remote.fetch) 0] \
1076                                        line rb
1077                        }
1078                } else {
1079                        catch {
1080                                set fd [open [file join $gitdir remotes $remote] r]
1081                                while {[gets $fd line] >= 0} {
1082                                        if {[regexp {^Pull:[ \t]*([^:]+):} $line line rb]} {
1083                                                break
1084                                        }
1085                                }
1086                                close $fd
1087                        }
1088                }
1089
1090                set rb_short $rb
1091                regsub ^refs/heads/ $rb {} rb_short
1092                if {$rb_short != {}} {
1093                        $m add command \
1094                                -label "Branch $rb_short from $remote..." \
1095                                -command [list pull_remote $remote $rb] \
1096                                -font $font_ui
1097                        lappend disable_on_lock \
1098                                [list $m entryconf [$m index last] -state]
1099                }
1100        }
1101}
1102
1103######################################################################
1104##
1105## icons
1106
1107set filemask {
1108#define mask_width 14
1109#define mask_height 15
1110static unsigned char mask_bits[] = {
1111   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
1112   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
1113   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f};
1114}
1115
1116image create bitmap file_plain -background white -foreground black -data {
1117#define plain_width 14
1118#define plain_height 15
1119static unsigned char plain_bits[] = {
1120   0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
1121   0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10,
1122   0x02, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1123} -maskdata $filemask
1124
1125image create bitmap file_mod -background white -foreground blue -data {
1126#define mod_width 14
1127#define mod_height 15
1128static unsigned char mod_bits[] = {
1129   0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
1130   0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
1131   0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
1132} -maskdata $filemask
1133
1134image create bitmap file_fulltick -background white -foreground "#007000" -data {
1135#define file_fulltick_width 14
1136#define file_fulltick_height 15
1137static unsigned char file_fulltick_bits[] = {
1138   0xfe, 0x01, 0x02, 0x1a, 0x02, 0x0c, 0x02, 0x0c, 0x02, 0x16, 0x02, 0x16,
1139   0x02, 0x13, 0x00, 0x13, 0x86, 0x11, 0x8c, 0x11, 0xd8, 0x10, 0xf2, 0x10,
1140   0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1141} -maskdata $filemask
1142
1143image create bitmap file_parttick -background white -foreground "#005050" -data {
1144#define parttick_width 14
1145#define parttick_height 15
1146static unsigned char parttick_bits[] = {
1147   0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
1148   0x7a, 0x14, 0x02, 0x16, 0x02, 0x13, 0x8a, 0x11, 0xda, 0x10, 0x72, 0x10,
1149   0x22, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1150} -maskdata $filemask
1151
1152image create bitmap file_question -background white -foreground black -data {
1153#define file_question_width 14
1154#define file_question_height 15
1155static unsigned char file_question_bits[] = {
1156   0xfe, 0x01, 0x02, 0x02, 0xe2, 0x04, 0xf2, 0x09, 0x1a, 0x1b, 0x0a, 0x13,
1157   0x82, 0x11, 0xc2, 0x10, 0x62, 0x10, 0x62, 0x10, 0x02, 0x10, 0x62, 0x10,
1158   0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
1159} -maskdata $filemask
1160
1161image create bitmap file_removed -background white -foreground red -data {
1162#define file_removed_width 14
1163#define file_removed_height 15
1164static unsigned char file_removed_bits[] = {
1165   0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
1166   0x1a, 0x16, 0x32, 0x13, 0xe2, 0x11, 0xc2, 0x10, 0xe2, 0x11, 0x32, 0x13,
1167   0x1a, 0x16, 0x02, 0x10, 0xfe, 0x1f};
1168} -maskdata $filemask
1169
1170image create bitmap file_merge -background white -foreground blue -data {
1171#define file_merge_width 14
1172#define file_merge_height 15
1173static unsigned char file_merge_bits[] = {
1174   0xfe, 0x01, 0x02, 0x03, 0x62, 0x05, 0x62, 0x09, 0x62, 0x1f, 0x62, 0x10,
1175   0xfa, 0x11, 0xf2, 0x10, 0x62, 0x10, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
1176   0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
1177} -maskdata $filemask
1178
1179set ui_index .vpane.files.index.list
1180set ui_other .vpane.files.other.list
1181set max_status_desc 0
1182foreach i {
1183                {__ i plain    "Unmodified"}
1184                {_M i mod      "Modified"}
1185                {M_ i fulltick "Checked in"}
1186                {MM i parttick "Partially included"}
1187
1188                {_O o plain    "Untracked"}
1189                {A_ o fulltick "Added"}
1190                {AM o parttick "Partially added"}
1191                {AD o question "Added (but now gone)"}
1192
1193                {_D i question "Missing"}
1194                {D_ i removed  "Removed"}
1195                {DD i removed  "Removed"}
1196                {DO i removed  "Removed (still exists)"}
1197
1198                {UM i merge    "Merge conflicts"}
1199                {U_ i merge    "Merge conflicts"}
1200        } {
1201        if {$max_status_desc < [string length [lindex $i 3]]} {
1202                set max_status_desc [string length [lindex $i 3]]
1203        }
1204        if {[lindex $i 1] == {i}} {
1205                set all_cols([lindex $i 0]) $ui_index
1206        } else {
1207                set all_cols([lindex $i 0]) $ui_other
1208        }
1209        set all_icons([lindex $i 0]) file_[lindex $i 2]
1210        set all_descs([lindex $i 0]) [lindex $i 3]
1211}
1212unset filemask i
1213
1214######################################################################
1215##
1216## util
1217
1218proc hook_failed_popup {hook msg} {
1219        global gitdir font_ui font_diff appname
1220
1221        set w .hookfail
1222        toplevel $w
1223        wm transient $w .
1224
1225        frame $w.m
1226        label $w.m.l1 -text "$hook hook failed:" \
1227                -anchor w \
1228                -justify left \
1229                -font [concat $font_ui bold]
1230        text $w.m.t \
1231                -background white -borderwidth 1 \
1232                -relief sunken \
1233                -width 80 -height 10 \
1234                -font $font_diff \
1235                -yscrollcommand [list $w.m.sby set]
1236        label $w.m.l2 \
1237                -text {You must correct the above errors before committing.} \
1238                -anchor w \
1239                -justify left \
1240                -font [concat $font_ui bold]
1241        scrollbar $w.m.sby -command [list $w.m.t yview]
1242        pack $w.m.l1 -side top -fill x
1243        pack $w.m.l2 -side bottom -fill x
1244        pack $w.m.sby -side right -fill y
1245        pack $w.m.t -side left -fill both -expand 1
1246        pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
1247
1248        $w.m.t insert 1.0 $msg
1249        $w.m.t conf -state disabled
1250
1251        button $w.ok -text OK \
1252                -width 15 \
1253                -font $font_ui \
1254                -command "destroy $w"
1255        pack $w.ok -side bottom
1256
1257        bind $w <Visibility> "grab $w; focus $w"
1258        bind $w <Key-Return> "destroy $w"
1259        wm title $w "$appname ([lindex [file split \
1260                [file normalize [file dirname $gitdir]]] \
1261                end]): error"
1262        tkwait window $w
1263}
1264
1265set next_console_id 0
1266
1267proc new_console {short_title long_title} {
1268        global next_console_id console_data
1269        set w .console[incr next_console_id]
1270        set console_data($w) [list $short_title $long_title]
1271        return [console_init $w]
1272}
1273
1274proc console_init {w} {
1275        global console_cr console_data
1276        global gitdir appname font_ui font_diff
1277
1278        set console_cr($w) 1.0
1279        toplevel $w
1280        frame $w.m
1281        label $w.m.l1 -text "[lindex $console_data($w) 1]:" \
1282                -anchor w \
1283                -justify left \
1284                -font [concat $font_ui bold]
1285        text $w.m.t \
1286                -background white -borderwidth 1 \
1287                -relief sunken \
1288                -width 80 -height 10 \
1289                -font $font_diff \
1290                -state disabled \
1291                -yscrollcommand [list $w.m.sby set]
1292        label $w.m.s -anchor w \
1293                -justify left \
1294                -font [concat $font_ui bold]
1295        scrollbar $w.m.sby -command [list $w.m.t yview]
1296        pack $w.m.l1 -side top -fill x
1297        pack $w.m.s -side bottom -fill x
1298        pack $w.m.sby -side right -fill y
1299        pack $w.m.t -side left -fill both -expand 1
1300        pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
1301
1302        button $w.ok -text {Running...} \
1303                -width 15 \
1304                -font $font_ui \
1305                -state disabled \
1306                -command "destroy $w"
1307        pack $w.ok -side bottom
1308
1309        bind $w <Visibility> "focus $w"
1310        wm title $w "$appname ([lindex [file split \
1311                [file normalize [file dirname $gitdir]]] \
1312                end]): [lindex $console_data($w) 0]"
1313        return $w
1314}
1315
1316proc console_exec {w cmd {after {}}} {
1317        global tcl_platform
1318
1319        # -- Windows tosses the enviroment when we exec our child.
1320        #    But most users need that so we have to relogin. :-(
1321        #
1322        if {$tcl_platform(platform) == {windows}} {
1323                set cmd [list sh --login -c "cd \"[pwd]\" && [join $cmd { }]"]
1324        }
1325
1326        # -- Tcl won't let us redirect both stdout and stderr to
1327        #    the same pipe.  So pass it through cat...
1328        #
1329        set cmd [concat | $cmd |& cat]
1330
1331        set fd_f [open $cmd r]
1332        fconfigure $fd_f -blocking 0 -translation binary
1333        fileevent $fd_f readable [list console_read $w $fd_f $after]
1334}
1335
1336proc console_read {w fd after} {
1337        global console_cr console_data
1338
1339        set buf [read $fd]
1340        if {$buf != {}} {
1341                if {![winfo exists $w]} {console_init $w}
1342                $w.m.t conf -state normal
1343                set c 0
1344                set n [string length $buf]
1345                while {$c < $n} {
1346                        set cr [string first "\r" $buf $c]
1347                        set lf [string first "\n" $buf $c]
1348                        if {$cr < 0} {set cr [expr $n + 1]}
1349                        if {$lf < 0} {set lf [expr $n + 1]}
1350
1351                        if {$lf < $cr} {
1352                                $w.m.t insert end [string range $buf $c $lf]
1353                                set console_cr($w) [$w.m.t index {end -1c}]
1354                                set c $lf
1355                                incr c
1356                        } else {
1357                                $w.m.t delete $console_cr($w) end
1358                                $w.m.t insert end "\n"
1359                                $w.m.t insert end [string range $buf $c $cr]
1360                                set c $cr
1361                                incr c
1362                        }
1363                }
1364                $w.m.t conf -state disabled
1365                $w.m.t see end
1366        }
1367
1368        fconfigure $fd -blocking 1
1369        if {[eof $fd]} {
1370                if {[catch {close $fd}]} {
1371                        if {![winfo exists $w]} {console_init $w}
1372                        $w.m.s conf -background red -text {Error: Command Failed}
1373                        $w.ok conf -text Close
1374                        $w.ok conf -state normal
1375                        set ok 0
1376                } elseif {[winfo exists $w]} {
1377                        $w.m.s conf -background green -text {Success}
1378                        $w.ok conf -text Close
1379                        $w.ok conf -state normal
1380                        set ok 1
1381                }
1382                array unset console_cr $w
1383                array unset console_data $w
1384                if {$after != {}} {
1385                        uplevel #0 $after $ok
1386                }
1387                return
1388        }
1389        fconfigure $fd -blocking 0
1390}
1391
1392######################################################################
1393##
1394## ui commands
1395
1396set starting_gitk_msg {Please wait... Starting gitk...}
1397
1398proc do_gitk {} {
1399        global tcl_platform ui_status_value starting_gitk_msg
1400
1401        set ui_status_value $starting_gitk_msg
1402        after 10000 {
1403                if {$ui_status_value == $starting_gitk_msg} {
1404                        set ui_status_value {Ready.}
1405                }
1406        }
1407
1408        if {$tcl_platform(platform) == {windows}} {
1409                exec sh -c gitk &
1410        } else {
1411                exec gitk &
1412        }
1413}
1414
1415proc do_repack {} {
1416        set w [new_console "repack" "Repacking the object database"]
1417        set cmd [list git repack]
1418        lappend cmd -a
1419        lappend cmd -d
1420        console_exec $w $cmd
1421}
1422
1423set quitting 0
1424
1425proc do_quit {} {
1426        global gitdir ui_comm quitting
1427
1428        if {$quitting} return
1429        set quitting 1
1430
1431        set save [file join $gitdir GITGUI_MSG]
1432        set msg [string trim [$ui_comm get 0.0 end]]
1433        if {[$ui_comm edit modified] && $msg != {}} {
1434                catch {
1435                        set fd [open $save w]
1436                        puts $fd [string trim [$ui_comm get 0.0 end]]
1437                        close $fd
1438                }
1439        } elseif {$msg == {} && [file exists $save]} {
1440                file delete $save
1441        }
1442
1443        save_my_config
1444        destroy .
1445}
1446
1447proc do_rescan {} {
1448        update_status
1449}
1450
1451proc do_include_all {} {
1452        global update_active ui_status_value
1453
1454        if {$update_active || ![lock_index begin-update]} return
1455
1456        set update_active 1
1457        set ui_status_value {Including all modified files...}
1458        after 1 {
1459                with_update_index {
1460                        foreach path [array names file_states] {
1461                                set s $file_states($path)
1462                                set m [lindex $s 0]
1463                                switch -- $m {
1464                                AM -
1465                                MM -
1466                                _M -
1467                                _D {toggle_mode $path}
1468                                }
1469                        }
1470                }
1471                set update_active 0
1472                set ui_status_value {Ready.}
1473        }
1474}
1475
1476set GIT_COMMITTER_IDENT {}
1477
1478proc do_signoff {} {
1479        global ui_comm GIT_COMMITTER_IDENT
1480
1481        if {$GIT_COMMITTER_IDENT == {}} {
1482                if {[catch {set me [exec git var GIT_COMMITTER_IDENT]} err]} {
1483                        error_popup "Unable to obtain your identity:\n\n$err"
1484                        return
1485                }
1486                if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \
1487                        $me me GIT_COMMITTER_IDENT]} {
1488                        error_popup "Invalid GIT_COMMITTER_IDENT:\n\n$me"
1489                        return
1490                }
1491        }
1492
1493        set str "Signed-off-by: $GIT_COMMITTER_IDENT"
1494        if {[$ui_comm get {end -1c linestart} {end -1c}] != $str} {
1495                $ui_comm edit separator
1496                $ui_comm insert end "\n$str"
1497                $ui_comm edit separator
1498                $ui_comm see end
1499        }
1500}
1501
1502proc do_amend_last {} {
1503        load_last_commit
1504}
1505
1506proc do_commit {} {
1507        commit_tree
1508}
1509
1510# shift == 1: left click
1511#          3: right click  
1512proc click {w x y shift wx wy} {
1513        global ui_index ui_other file_lists
1514
1515        set pos [split [$w index @$x,$y] .]
1516        set lno [lindex $pos 0]
1517        set col [lindex $pos 1]
1518        set path [lindex $file_lists($w) [expr $lno - 1]]
1519        if {$path == {}} return
1520
1521        if {$col > 0 && $shift == 1} {
1522                show_diff $path $w $lno
1523        }
1524}
1525
1526proc unclick {w x y} {
1527        global file_lists
1528
1529        set pos [split [$w index @$x,$y] .]
1530        set lno [lindex $pos 0]
1531        set col [lindex $pos 1]
1532        set path [lindex $file_lists($w) [expr $lno - 1]]
1533        if {$path == {}} return
1534
1535        if {$col == 0} {
1536                toggle_mode $path
1537        }
1538}
1539
1540######################################################################
1541##
1542## ui init
1543
1544set font_ui {Helvetica 10}
1545set font_diff {Courier 10}
1546set maincursor [. cget -cursor]
1547
1548switch -glob -- "$tcl_platform(platform),$tcl_platform(os)" {
1549windows,*   {set M1B Control; set M1T Ctrl}
1550unix,Darwin {set M1B M1; set M1T Cmd}
1551default     {set M1B M1; set M1T M1}
1552}
1553
1554# -- Menu Bar
1555menu .mbar -tearoff 0
1556.mbar add cascade -label Project -menu .mbar.project
1557.mbar add cascade -label Edit -menu .mbar.edit
1558.mbar add cascade -label Commit -menu .mbar.commit
1559.mbar add cascade -label Fetch -menu .mbar.fetch
1560.mbar add cascade -label Pull -menu .mbar.pull
1561.mbar add cascade -label Push -menu .mbar.push
1562.mbar add cascade -label Options -menu .mbar.options
1563. configure -menu .mbar
1564
1565# -- Project Menu
1566menu .mbar.project
1567.mbar.project add command -label Visualize \
1568        -command do_gitk \
1569        -font $font_ui
1570.mbar.project add command -label {Repack Database} \
1571        -command do_repack \
1572        -font $font_ui
1573.mbar.project add command -label Quit \
1574        -command do_quit \
1575        -accelerator $M1T-Q \
1576        -font $font_ui
1577
1578# -- Edit Menu
1579#
1580menu .mbar.edit
1581.mbar.edit add command -label Undo \
1582        -command {catch {[focus] edit undo}} \
1583        -accelerator $M1T-Z \
1584        -font $font_ui
1585.mbar.edit add command -label Redo \
1586        -command {catch {[focus] edit redo}} \
1587        -accelerator $M1T-Y \
1588        -font $font_ui
1589.mbar.edit add separator
1590.mbar.edit add command -label Cut \
1591        -command {catch {tk_textCut [focus]}} \
1592        -accelerator $M1T-X \
1593        -font $font_ui
1594.mbar.edit add command -label Copy \
1595        -command {catch {tk_textCopy [focus]}} \
1596        -accelerator $M1T-C \
1597        -font $font_ui
1598.mbar.edit add command -label Paste \
1599        -command {catch {tk_textPaste [focus]; [focus] see insert}} \
1600        -accelerator $M1T-V \
1601        -font $font_ui
1602.mbar.edit add command -label Delete \
1603        -command {catch {[focus] delete sel.first sel.last}} \
1604        -accelerator Del \
1605        -font $font_ui
1606.mbar.edit add separator
1607.mbar.edit add command -label {Select All} \
1608        -command {catch {[focus] tag add sel 0.0 end}} \
1609        -accelerator $M1T-A \
1610        -font $font_ui
1611
1612# -- Commit Menu
1613menu .mbar.commit
1614.mbar.commit add command -label Rescan \
1615        -command do_rescan \
1616        -accelerator F5 \
1617        -font $font_ui
1618lappend disable_on_lock \
1619        [list .mbar.commit entryconf [.mbar.commit index last] -state]
1620.mbar.commit add command -label {Amend Last Commit} \
1621        -command do_amend_last \
1622        -font $font_ui
1623lappend disable_on_lock \
1624        [list .mbar.commit entryconf [.mbar.commit index last] -state]
1625.mbar.commit add command -label {Include All Files} \
1626        -command do_include_all \
1627        -accelerator $M1T-I \
1628        -font $font_ui
1629lappend disable_on_lock \
1630        [list .mbar.commit entryconf [.mbar.commit index last] -state]
1631.mbar.commit add command -label {Sign Off} \
1632        -command do_signoff \
1633        -accelerator $M1T-S \
1634        -font $font_ui
1635.mbar.commit add command -label Commit \
1636        -command do_commit \
1637        -accelerator $M1T-Return \
1638        -font $font_ui
1639lappend disable_on_lock \
1640        [list .mbar.commit entryconf [.mbar.commit index last] -state]
1641
1642# -- Fetch Menu
1643menu .mbar.fetch
1644
1645# -- Pull Menu
1646menu .mbar.pull
1647
1648# -- Push Menu
1649menu .mbar.push
1650
1651# -- Options Menu
1652menu .mbar.options
1653.mbar.options add checkbutton \
1654        -label {Trust File Modification Timestamps} \
1655        -offvalue false \
1656        -onvalue true \
1657        -variable cfg_trust_mtime
1658
1659# -- Main Window Layout
1660panedwindow .vpane -orient vertical
1661panedwindow .vpane.files -orient horizontal
1662.vpane add .vpane.files -sticky nsew -height 100 -width 400
1663pack .vpane -anchor n -side top -fill both -expand 1
1664
1665# -- Index File List
1666frame .vpane.files.index -height 100 -width 400
1667label .vpane.files.index.title -text {Modified Files} \
1668        -background green \
1669        -font $font_ui
1670text $ui_index -background white -borderwidth 0 \
1671        -width 40 -height 10 \
1672        -font $font_ui \
1673        -yscrollcommand {.vpane.files.index.sb set} \
1674        -cursor $maincursor \
1675        -state disabled
1676scrollbar .vpane.files.index.sb -command [list $ui_index yview]
1677pack .vpane.files.index.title -side top -fill x
1678pack .vpane.files.index.sb -side right -fill y
1679pack $ui_index -side left -fill both -expand 1
1680.vpane.files add .vpane.files.index -sticky nsew
1681
1682# -- Other (Add) File List
1683frame .vpane.files.other -height 100 -width 100
1684label .vpane.files.other.title -text {Untracked Files} \
1685        -background red \
1686        -font $font_ui
1687text $ui_other -background white -borderwidth 0 \
1688        -width 40 -height 10 \
1689        -font $font_ui \
1690        -yscrollcommand {.vpane.files.other.sb set} \
1691        -cursor $maincursor \
1692        -state disabled
1693scrollbar .vpane.files.other.sb -command [list $ui_other yview]
1694pack .vpane.files.other.title -side top -fill x
1695pack .vpane.files.other.sb -side right -fill y
1696pack $ui_other -side left -fill both -expand 1
1697.vpane.files add .vpane.files.other -sticky nsew
1698
1699$ui_index tag conf in_diff -font [concat $font_ui bold]
1700$ui_other tag conf in_diff -font [concat $font_ui bold]
1701
1702# -- Diff and Commit Area
1703frame .vpane.lower -height 400 -width 400
1704frame .vpane.lower.commarea
1705frame .vpane.lower.diff -relief sunken -borderwidth 1
1706pack .vpane.lower.commarea -side top -fill x
1707pack .vpane.lower.diff -side bottom -fill both -expand 1
1708.vpane add .vpane.lower -stick nsew
1709
1710# -- Commit Area Buttons
1711frame .vpane.lower.commarea.buttons
1712label .vpane.lower.commarea.buttons.l -text {} \
1713        -anchor w \
1714        -justify left \
1715        -font $font_ui
1716pack .vpane.lower.commarea.buttons.l -side top -fill x
1717pack .vpane.lower.commarea.buttons -side left -fill y
1718
1719button .vpane.lower.commarea.buttons.rescan -text {Rescan} \
1720        -command do_rescan \
1721        -font $font_ui
1722pack .vpane.lower.commarea.buttons.rescan -side top -fill x
1723lappend disable_on_lock {.vpane.lower.commarea.buttons.rescan conf -state}
1724
1725button .vpane.lower.commarea.buttons.amend -text {Amend Last} \
1726        -command do_amend_last \
1727        -font $font_ui
1728pack .vpane.lower.commarea.buttons.amend -side top -fill x
1729lappend disable_on_lock {.vpane.lower.commarea.buttons.amend conf -state}
1730
1731button .vpane.lower.commarea.buttons.incall -text {Include All} \
1732        -command do_include_all \
1733        -font $font_ui
1734pack .vpane.lower.commarea.buttons.incall -side top -fill x
1735lappend disable_on_lock {.vpane.lower.commarea.buttons.incall conf -state}
1736
1737button .vpane.lower.commarea.buttons.signoff -text {Sign Off} \
1738        -command do_signoff \
1739        -font $font_ui
1740pack .vpane.lower.commarea.buttons.signoff -side top -fill x
1741
1742button .vpane.lower.commarea.buttons.commit -text {Commit} \
1743        -command do_commit \
1744        -font $font_ui
1745pack .vpane.lower.commarea.buttons.commit -side top -fill x
1746lappend disable_on_lock {.vpane.lower.commarea.buttons.commit conf -state}
1747
1748# -- Commit Message Buffer
1749frame .vpane.lower.commarea.buffer
1750set ui_comm .vpane.lower.commarea.buffer.t
1751set ui_coml .vpane.lower.commarea.buffer.l
1752label $ui_coml -text {Commit Message:} \
1753        -anchor w \
1754        -justify left \
1755        -font $font_ui
1756trace add variable commit_type write {uplevel #0 {
1757        switch -glob $commit_type \
1758        initial {$ui_coml conf -text {Initial Commit Message:}} \
1759        amend   {$ui_coml conf -text {Amended Commit Message:}} \
1760        merge   {$ui_coml conf -text {Merge Commit Message:}} \
1761        *       {$ui_coml conf -text {Commit Message:}}
1762}}
1763text $ui_comm -background white -borderwidth 1 \
1764        -undo true \
1765        -maxundo 20 \
1766        -autoseparators true \
1767        -relief sunken \
1768        -width 75 -height 9 -wrap none \
1769        -font $font_diff \
1770        -yscrollcommand {.vpane.lower.commarea.buffer.sby set} \
1771        -cursor $maincursor
1772scrollbar .vpane.lower.commarea.buffer.sby -command [list $ui_comm yview]
1773pack $ui_coml -side top -fill x
1774pack .vpane.lower.commarea.buffer.sby -side right -fill y
1775pack $ui_comm -side left -fill y
1776pack .vpane.lower.commarea.buffer -side left -fill y
1777
1778# -- Diff Header
1779set ui_fname_value {}
1780set ui_fstatus_value {}
1781frame .vpane.lower.diff.header -background orange
1782label .vpane.lower.diff.header.l1 -text {File:} \
1783        -background orange \
1784        -font $font_ui
1785label .vpane.lower.diff.header.l2 -textvariable ui_fname_value \
1786        -background orange \
1787        -anchor w \
1788        -justify left \
1789        -font $font_ui
1790label .vpane.lower.diff.header.l3 -text {Status:} \
1791        -background orange \
1792        -font $font_ui
1793label .vpane.lower.diff.header.l4 -textvariable ui_fstatus_value \
1794        -background orange \
1795        -width $max_status_desc \
1796        -anchor w \
1797        -justify left \
1798        -font $font_ui
1799pack .vpane.lower.diff.header.l1 -side left
1800pack .vpane.lower.diff.header.l2 -side left -fill x
1801pack .vpane.lower.diff.header.l4 -side right
1802pack .vpane.lower.diff.header.l3 -side right
1803
1804# -- Diff Body
1805frame .vpane.lower.diff.body
1806set ui_diff .vpane.lower.diff.body.t
1807text $ui_diff -background white -borderwidth 0 \
1808        -width 80 -height 15 -wrap none \
1809        -font $font_diff \
1810        -xscrollcommand {.vpane.lower.diff.body.sbx set} \
1811        -yscrollcommand {.vpane.lower.diff.body.sby set} \
1812        -cursor $maincursor \
1813        -state disabled
1814scrollbar .vpane.lower.diff.body.sbx -orient horizontal \
1815        -command [list $ui_diff xview]
1816scrollbar .vpane.lower.diff.body.sby -orient vertical \
1817        -command [list $ui_diff yview]
1818pack .vpane.lower.diff.body.sbx -side bottom -fill x
1819pack .vpane.lower.diff.body.sby -side right -fill y
1820pack $ui_diff -side left -fill both -expand 1
1821pack .vpane.lower.diff.header -side top -fill x
1822pack .vpane.lower.diff.body -side bottom -fill both -expand 1
1823
1824$ui_diff tag conf dm -foreground red
1825$ui_diff tag conf dp -foreground blue
1826$ui_diff tag conf da -font [concat $font_diff bold]
1827$ui_diff tag conf di -foreground "#00a000"
1828$ui_diff tag conf dni -foreground "#a000a0"
1829$ui_diff tag conf bold -font [concat $font_diff bold]
1830
1831# -- Status Bar
1832set ui_status_value {Initializing...}
1833label .status -textvariable ui_status_value \
1834        -anchor w \
1835        -justify left \
1836        -borderwidth 1 \
1837        -relief sunken \
1838        -font $font_ui
1839pack .status -anchor w -side bottom -fill x
1840
1841# -- Load geometry
1842catch {
1843set gm [lindex $repo_config(gui.geometry) 0]
1844wm geometry . [lindex $gm 0]
1845.vpane sash place 0 \
1846        [lindex [.vpane sash coord 0] 0] \
1847        [lindex $gm 1]
1848.vpane.files sash place 0 \
1849        [lindex $gm 2] \
1850        [lindex [.vpane.files sash coord 0] 1]
1851}
1852unset gm
1853
1854# -- Key Bindings
1855bind $ui_comm <$M1B-Key-Return> {do_commit;break}
1856bind $ui_comm <$M1B-Key-i> {do_include_all;break}
1857bind $ui_comm <$M1B-Key-I> {do_include_all;break}
1858bind $ui_comm <$M1B-Key-x> {tk_textCut %W;break}
1859bind $ui_comm <$M1B-Key-X> {tk_textCut %W;break}
1860bind $ui_comm <$M1B-Key-c> {tk_textCopy %W;break}
1861bind $ui_comm <$M1B-Key-C> {tk_textCopy %W;break}
1862bind $ui_comm <$M1B-Key-v> {tk_textPaste %W; %W see insert; break}
1863bind $ui_comm <$M1B-Key-V> {tk_textPaste %W; %W see insert; break}
1864bind $ui_comm <$M1B-Key-a> {%W tag add sel 0.0 end;break}
1865bind $ui_comm <$M1B-Key-A> {%W tag add sel 0.0 end;break}
1866
1867bind $ui_diff <$M1B-Key-x> {tk_textCopy %W;break}
1868bind $ui_diff <$M1B-Key-X> {tk_textCopy %W;break}
1869bind $ui_diff <$M1B-Key-c> {tk_textCopy %W;break}
1870bind $ui_diff <$M1B-Key-C> {tk_textCopy %W;break}
1871bind $ui_diff <$M1B-Key-v> {break}
1872bind $ui_diff <$M1B-Key-V> {break}
1873bind $ui_diff <$M1B-Key-a> {%W tag add sel 0.0 end;break}
1874bind $ui_diff <$M1B-Key-A> {%W tag add sel 0.0 end;break}
1875bind $ui_diff <Key-Up>     {catch {%W yview scroll -1 units};break}
1876bind $ui_diff <Key-Down>   {catch {%W yview scroll  1 units};break}
1877bind $ui_diff <Key-Left>   {catch {%W xview scroll -1 units};break}
1878bind $ui_diff <Key-Right>  {catch {%W xview scroll  1 units};break}
1879
1880bind .   <Destroy> do_quit
1881bind all <Key-F5> do_rescan
1882bind all <$M1B-Key-r> do_rescan
1883bind all <$M1B-Key-R> do_rescan
1884bind .   <$M1B-Key-s> do_signoff
1885bind .   <$M1B-Key-S> do_signoff
1886bind .   <$M1B-Key-i> do_include_all
1887bind .   <$M1B-Key-I> do_include_all
1888bind .   <$M1B-Key-Return> do_commit
1889bind all <$M1B-Key-q> do_quit
1890bind all <$M1B-Key-Q> do_quit
1891bind all <$M1B-Key-w> {destroy [winfo toplevel %W]}
1892bind all <$M1B-Key-W> {destroy [winfo toplevel %W]}
1893foreach i [list $ui_index $ui_other] {
1894        bind $i <Button-1> {click %W %x %y 1 %X %Y; break}
1895        bind $i <Button-3> {click %W %x %y 3 %X %Y; break}
1896        bind $i <ButtonRelease-1> {unclick %W %x %y; break}
1897}
1898unset i M1B M1T
1899
1900wm title . "$appname ([file normalize [file dirname $gitdir]])"
1901focus -force $ui_comm
1902load_all_remotes
1903populate_remote_menu .mbar.fetch From fetch_from
1904populate_remote_menu .mbar.push To push_to
1905populate_pull_menu .mbar.pull
1906update_status