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