git-guion commit git-gui: Implemented amended commits. (e57ca85)
   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
  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        array unset file_states
  88        foreach w [list $ui_index $ui_other] {
  89                $w conf -state normal
  90                $w delete 0.0 end
  91                $w conf -state disabled
  92        }
  93
  94        if {![$ui_comm edit modified]
  95            || [string trim [$ui_comm get 0.0 end]] == {}} {
  96                if {[load_message GITGUI_MSG]} {
  97                } elseif {[load_message MERGE_MSG]} {
  98                } elseif {[load_message SQUASH_MSG]} {
  99                }
 100                $ui_comm edit modified false
 101        }
 102
 103        set status_active 1
 104        set ui_status_value {Refreshing file status...}
 105        set fd_rf [open "| git update-index -q --unmerged --refresh" r]
 106        fconfigure $fd_rf -blocking 0 -translation binary
 107        fileevent $fd_rf readable [list read_refresh $fd_rf $final]
 108}
 109
 110proc read_refresh {fd final} {
 111        global gitdir PARENT commit_type
 112        global ui_index ui_other ui_status_value ui_comm
 113        global status_active file_states
 114
 115        read $fd
 116        if {![eof $fd]} return
 117        close $fd
 118
 119        set ls_others [list | git ls-files --others -z \
 120                --exclude-per-directory=.gitignore]
 121        set info_exclude [file join $gitdir info exclude]
 122        if {[file readable $info_exclude]} {
 123                lappend ls_others "--exclude-from=$info_exclude"
 124        }
 125
 126        set status_active 3
 127        set ui_status_value {Scanning for modified files ...}
 128        set fd_di [open "| git diff-index --cached -z $PARENT" r]
 129        set fd_df [open "| git diff-files -z" r]
 130        set fd_lo [open $ls_others r]
 131
 132        fconfigure $fd_di -blocking 0 -translation binary
 133        fconfigure $fd_df -blocking 0 -translation binary
 134        fconfigure $fd_lo -blocking 0 -translation binary
 135        fileevent $fd_di readable [list read_diff_index $fd_di $final]
 136        fileevent $fd_df readable [list read_diff_files $fd_df $final]
 137        fileevent $fd_lo readable [list read_ls_others $fd_lo $final]
 138}
 139
 140proc load_message {file} {
 141        global gitdir ui_comm
 142
 143        set f [file join $gitdir $file]
 144        if {[file isfile $f]} {
 145                if {[catch {set fd [open $f r]}]} {
 146                        return 0
 147                }
 148                set content [string trim [read $fd]]
 149                close $fd
 150                $ui_comm delete 0.0 end
 151                $ui_comm insert end $content
 152                return 1
 153        }
 154        return 0
 155}
 156
 157proc read_diff_index {fd final} {
 158        global buf_rdi
 159
 160        append buf_rdi [read $fd]
 161        set pck [split $buf_rdi "\0"]
 162        set buf_rdi [lindex $pck end]
 163        foreach {m p} [lrange $pck 0 end-1] {
 164                if {$m != {} && $p != {}} {
 165                        display_file $p [string index $m end]_
 166                }
 167        }
 168        status_eof $fd buf_rdi $final
 169}
 170
 171proc read_diff_files {fd final} {
 172        global buf_rdf
 173
 174        append buf_rdf [read $fd]
 175        set pck [split $buf_rdf "\0"]
 176        set buf_rdf [lindex $pck end]
 177        foreach {m p} [lrange $pck 0 end-1] {
 178                if {$m != {} && $p != {}} {
 179                        display_file $p _[string index $m end]
 180                }
 181        }
 182        status_eof $fd buf_rdf $final
 183}
 184
 185proc read_ls_others {fd final} {
 186        global buf_rlo
 187
 188        append buf_rlo [read $fd]
 189        set pck [split $buf_rlo "\0"]
 190        set buf_rlo [lindex $pck end]
 191        foreach p [lrange $pck 0 end-1] {
 192                display_file $p _O
 193        }
 194        status_eof $fd buf_rlo $final
 195}
 196
 197proc status_eof {fd buf final} {
 198        global status_active $buf
 199        global ui_fname_value ui_status_value file_states
 200
 201        if {[eof $fd]} {
 202                set $buf {}
 203                close $fd
 204                if {[incr status_active -1] == 0} {
 205                        unlock_index
 206
 207                        set ui_status_value $final
 208                        if {$ui_fname_value != {} && [array names file_states \
 209                                -exact $ui_fname_value] != {}}  {
 210                                show_diff $ui_fname_value
 211                        } else {
 212                                clear_diff
 213                        }
 214                }
 215        }
 216}
 217
 218######################################################################
 219##
 220## diff
 221
 222proc clear_diff {} {
 223        global ui_diff ui_fname_value ui_fstatus_value
 224
 225        $ui_diff conf -state normal
 226        $ui_diff delete 0.0 end
 227        $ui_diff conf -state disabled
 228        set ui_fname_value {}
 229        set ui_fstatus_value {}
 230}
 231
 232proc show_diff {path} {
 233        global file_states PARENT diff_3way diff_active
 234        global ui_diff ui_fname_value ui_fstatus_value ui_status_value
 235
 236        if {$diff_active || ![lock_index read]} return
 237
 238        clear_diff
 239        set s $file_states($path)
 240        set m [lindex $s 0]
 241        set diff_3way 0
 242        set diff_active 1
 243        set ui_fname_value $path
 244        set ui_fstatus_value [mapdesc $m $path]
 245        set ui_status_value "Loading diff of $path..."
 246
 247        set cmd [list | git diff-index -p $PARENT -- $path]
 248        switch $m {
 249        AM {
 250        }
 251        MM {
 252                set cmd [list | git diff-index -p -c $PARENT $path]
 253        }
 254        _O {
 255                if {[catch {
 256                                set fd [open $path r]
 257                                set content [read $fd]
 258                                close $fd
 259                        } err ]} {
 260                        set diff_active 0
 261                        unlock_index
 262                        set ui_status_value "Unable to display $path"
 263                        error_popup "Error loading file:\n$err"
 264                        return
 265                }
 266                $ui_diff conf -state normal
 267                $ui_diff insert end $content
 268                $ui_diff conf -state disabled
 269                return
 270        }
 271        }
 272
 273        if {[catch {set fd [open $cmd r]} err]} {
 274                set diff_active 0
 275                unlock_index
 276                set ui_status_value "Unable to display $path"
 277                error_popup "Error loading diff:\n$err"
 278                return
 279        }
 280
 281        fconfigure $fd -blocking 0 -translation auto
 282        fileevent $fd readable [list read_diff $fd]
 283}
 284
 285proc read_diff {fd} {
 286        global ui_diff ui_status_value diff_3way diff_active
 287
 288        while {[gets $fd line] >= 0} {
 289                if {[string match {diff --git *} $line]} continue
 290                if {[string match {diff --combined *} $line]} continue
 291                if {[string match {--- *} $line]} continue
 292                if {[string match {+++ *} $line]} continue
 293                if {[string match index* $line]} {
 294                        if {[string first , $line] >= 0} {
 295                                set diff_3way 1
 296                        }
 297                }
 298
 299                $ui_diff conf -state normal
 300                if {!$diff_3way} {
 301                        set x [string index $line 0]
 302                        switch -- $x {
 303                        "@" {set tags da}
 304                        "+" {set tags dp}
 305                        "-" {set tags dm}
 306                        default {set tags {}}
 307                        }
 308                } else {
 309                        set x [string range $line 0 1]
 310                        switch -- $x {
 311                        default {set tags {}}
 312                        "@@" {set tags da}
 313                        "++" {set tags dp; set x " +"}
 314                        " +" {set tags {di bold}; set x "++"}
 315                        "+ " {set tags dni; set x "-+"}
 316                        "--" {set tags dm; set x " -"}
 317                        " -" {set tags {dm bold}; set x "--"}
 318                        "- " {set tags di; set x "+-"}
 319                        default {set tags {}}
 320                        }
 321                        set line [string replace $line 0 1 $x]
 322                }
 323                $ui_diff insert end $line $tags
 324                $ui_diff insert end "\n"
 325                $ui_diff conf -state disabled
 326        }
 327
 328        if {[eof $fd]} {
 329                close $fd
 330                set diff_active 0
 331                unlock_index
 332                set ui_status_value {Ready.}
 333        }
 334}
 335
 336######################################################################
 337##
 338## commit
 339
 340proc load_last_commit {} {
 341        global HEAD PARENT commit_type ui_comm
 342
 343        if {$commit_type == {amend}} return
 344        if {$commit_type != {normal}} {
 345                error_popup "Can't amend a $commit_type commit."
 346                return
 347        }
 348
 349        set msg {}
 350        set parent {}
 351        set parent_count 0
 352        if {[catch {
 353                        set fd [open "| git cat-file commit $HEAD" r]
 354                        while {[gets $fd line] > 0} {
 355                                if {[string match {parent *} $line]} {
 356                                        set parent [string range $line 7 end]
 357                                        incr parent_count
 358                                }
 359                        }
 360                        set msg [string trim [read $fd]]
 361                        close $fd
 362                } err]} {
 363                error_popup "Error loading commit data for amend:\n$err"
 364                return
 365        }
 366
 367        if {$parent_count == 0} {
 368                set commit_type amend
 369                set HEAD {}
 370                set PARENT {}
 371                update_status
 372        } elseif {$parent_count == 1} {
 373                set commit_type amend
 374                set PARENT $parent
 375                $ui_comm delete 0.0 end
 376                $ui_comm insert end $msg
 377                $ui_comm edit modified false
 378                update_status
 379        } else {
 380                error_popup {You can't amend a merge commit.}
 381                return
 382        }
 383}
 384
 385proc commit_tree {} {
 386        global tcl_platform HEAD gitdir commit_type file_states
 387        global commit_active ui_status_value
 388        global ui_comm
 389
 390        if {$commit_active || ![lock_index update]} return
 391
 392        # -- Our in memory state should match the repository.
 393        #
 394        repository_state curHEAD cur_type
 395        if {$commit_type == {amend} 
 396                && $cur_type == {normal}
 397                && $curHEAD == $HEAD} {
 398        } elseif {$commit_type != $cur_type || $HEAD != $curHEAD} {
 399                error_popup {Last scanned state does not match repository state.
 400
 401Its highly likely that another Git program modified the
 402repository since our last scan.  A rescan is required
 403before committing.
 404}
 405                unlock_index
 406                update_status
 407                return
 408        }
 409
 410        # -- At least one file should differ in the index.
 411        #
 412        set files_ready 0
 413        foreach path [array names file_states] {
 414                set s $file_states($path)
 415                switch -glob -- [lindex $s 0] {
 416                _* {continue}
 417                A* -
 418                D* -
 419                M* {set files_ready 1; break}
 420                U* {
 421                        error_popup "Unmerged files cannot be committed.
 422
 423File $path has merge conflicts.
 424You must resolve them and check the file in before committing.
 425"
 426                        unlock_index
 427                        return
 428                }
 429                default {
 430                        error_popup "Unknown file state [lindex $s 0] detected.
 431
 432File $path cannot be committed by this program.
 433"
 434                }
 435                }
 436        }
 437        if {!$files_ready} {
 438                error_popup {No checked-in files to commit.
 439
 440You must check-in at least 1 file before you can commit.
 441}
 442                unlock_index
 443                return
 444        }
 445
 446        # -- A message is required.
 447        #
 448        set msg [string trim [$ui_comm get 1.0 end]]
 449        if {$msg == {}} {
 450                error_popup {Please supply a commit message.
 451
 452A good commit message has the following format:
 453
 454- First line: Describe in one sentance what you did.
 455- Second line: Blank
 456- Remaining lines: Describe why this change is good.
 457}
 458                unlock_index
 459                return
 460        }
 461
 462        # -- Ask the pre-commit hook for the go-ahead.
 463        #
 464        set pchook [file join $gitdir hooks pre-commit]
 465        if {$tcl_platform(platform) == {windows} && [file isfile $pchook]} {
 466                set pchook [list sh -c \
 467                        "if test -x \"$pchook\"; then exec \"$pchook\"; fi"]
 468        } elseif {[file executable $pchook]} {
 469                set pchook [list $pchook]
 470        } else {
 471                set pchook {}
 472        }
 473        if {$pchook != {} && [catch {eval exec $pchook} err]} {
 474                hook_failed_popup pre-commit $err
 475                unlock_index
 476                return
 477        }
 478
 479        # -- Write the tree in the background.
 480        #
 481        set commit_active 1
 482        set ui_status_value {Committing changes...}
 483
 484        set fd_wt [open "| git write-tree" r]
 485        fileevent $fd_wt readable \
 486                [list commit_stage2 $fd_wt $curHEAD $msg]
 487}
 488
 489proc commit_stage2 {fd_wt curHEAD msg} {
 490        global single_commit gitdir PARENT commit_type
 491        global commit_active ui_status_value ui_comm
 492
 493        gets $fd_wt tree_id
 494        close $fd_wt
 495
 496        if {$tree_id == {}} {
 497                error_popup "write-tree failed"
 498                set commit_active 0
 499                set ui_status_value {Commit failed.}
 500                unlock_index
 501                return
 502        }
 503
 504        # -- Create the commit.
 505        #
 506        set cmd [list git commit-tree $tree_id]
 507        if {$PARENT != {}} {
 508                lappend cmd -p $PARENT
 509        }
 510        if {$commit_type == {merge}} {
 511                if {[catch {
 512                                set fd_mh [open [file join $gitdir MERGE_HEAD] r]
 513                                while {[gets $fd_mh merge_head] > 0} {
 514                                        lappend -p $merge_head
 515                                }
 516                                close $fd_mh
 517                        } err]} {
 518                        error_popup "Loading MERGE_HEADs failed:\n$err"
 519                        set commit_active 0
 520                        set ui_status_value {Commit failed.}
 521                        unlock_index
 522                        return
 523                }
 524        }
 525        if {$PARENT == {}} {
 526                # git commit-tree writes to stderr during initial commit.
 527                lappend cmd 2>/dev/null
 528        }
 529        lappend cmd << $msg
 530        if {[catch {set cmt_id [eval exec $cmd]} err]} {
 531                error_popup "commit-tree failed:\n$err"
 532                set commit_active 0
 533                set ui_status_value {Commit failed.}
 534                unlock_index
 535                return
 536        }
 537
 538        # -- Update the HEAD ref.
 539        #
 540        set reflogm commit
 541        if {$commit_type != {normal}} {
 542                append reflogm " ($commit_type)"
 543        }
 544        set i [string first "\n" $msg]
 545        if {$i >= 0} {
 546                append reflogm {: } [string range $msg 0 [expr $i - 1]]
 547        } else {
 548                append reflogm {: } $msg
 549        }
 550        set cmd [list git update-ref -m $reflogm HEAD $cmt_id $curHEAD]
 551        if {[catch {eval exec $cmd} err]} {
 552                error_popup "update-ref failed:\n$err"
 553                set commit_active 0
 554                set ui_status_value {Commit failed.}
 555                unlock_index
 556                return
 557        }
 558
 559        # -- Cleanup after ourselves.
 560        #
 561        catch {file delete [file join $gitdir MERGE_HEAD]}
 562        catch {file delete [file join $gitdir MERGE_MSG]}
 563        catch {file delete [file join $gitdir SQUASH_MSG]}
 564        catch {file delete [file join $gitdir GITGUI_MSG]}
 565
 566        # -- Let rerere do its thing.
 567        #
 568        if {[file isdirectory [file join $gitdir rr-cache]]} {
 569                catch {exec git rerere}
 570        }
 571
 572        $ui_comm delete 0.0 end
 573        $ui_comm edit modified false
 574
 575        if {$single_commit} do_quit
 576
 577        set commit_type {}
 578        set commit_active 0
 579        unlock_index
 580        update_status "Changes committed as $cmt_id."
 581}
 582
 583######################################################################
 584##
 585## ui helpers
 586
 587proc mapcol {state path} {
 588        global all_cols
 589
 590        if {[catch {set r $all_cols($state)}]} {
 591                puts "error: no column for state={$state} $path"
 592                return o
 593        }
 594        return $r
 595}
 596
 597proc mapicon {state path} {
 598        global all_icons
 599
 600        if {[catch {set r $all_icons($state)}]} {
 601                puts "error: no icon for state={$state} $path"
 602                return file_plain
 603        }
 604        return $r
 605}
 606
 607proc mapdesc {state path} {
 608        global all_descs
 609
 610        if {[catch {set r $all_descs($state)}]} {
 611                puts "error: no desc for state={$state} $path"
 612                return $state
 613        }
 614        return $r
 615}
 616
 617proc bsearch {w path} {
 618        set hi [expr [lindex [split [$w index end] .] 0] - 2]
 619        if {$hi == 0} {
 620                return -1
 621        }
 622        set lo 0
 623        while {$lo < $hi} {
 624                set mi [expr [expr $lo + $hi] / 2]
 625                set ti [expr $mi + 1]
 626                set cmp [string compare [$w get $ti.1 $ti.end] $path]
 627                if {$cmp < 0} {
 628                        set lo $ti
 629                } elseif {$cmp == 0} {
 630                        return $mi
 631                } else {
 632                        set hi $mi
 633                }
 634        }
 635        return -[expr $lo + 1]
 636}
 637
 638proc merge_state {path state} {
 639        global file_states
 640
 641        if {[array names file_states -exact $path] == {}}  {
 642                set o __
 643                set s [list $o none none]
 644        } else {
 645                set s $file_states($path)
 646                set o [lindex $s 0]
 647        }
 648
 649        set m [lindex $s 0]
 650        if {[string index $state 0] == "_"} {
 651                set state [string index $m 0][string index $state 1]
 652        } elseif {[string index $state 0] == "*"} {
 653                set state _[string index $state 1]
 654        }
 655
 656        if {[string index $state 1] == "_"} {
 657                set state [string index $state 0][string index $m 1]
 658        } elseif {[string index $state 1] == "*"} {
 659                set state [string index $state 0]_
 660        }
 661
 662        set file_states($path) [lreplace $s 0 0 $state]
 663        return $o
 664}
 665
 666proc display_file {path state} {
 667        global ui_index ui_other file_states
 668
 669        set old_m [merge_state $path $state]
 670        set s $file_states($path)
 671        set m [lindex $s 0]
 672
 673        if {[mapcol $m $path] == "o"} {
 674                set ii 1
 675                set ai 2
 676                set iw $ui_index
 677                set aw $ui_other
 678        } else {
 679                set ii 2
 680                set ai 1
 681                set iw $ui_other
 682                set aw $ui_index
 683        }
 684
 685        set d [lindex $s $ii]
 686        if {$d != "none"} {
 687                set lno [bsearch $iw $path]
 688                if {$lno >= 0} {
 689                        incr lno
 690                        $iw conf -state normal
 691                        $iw delete $lno.0 [expr $lno + 1].0
 692                        $iw conf -state disabled
 693                        set s [lreplace $s $ii $ii none]
 694                }
 695        }
 696
 697        set d [lindex $s $ai]
 698        if {$d == "none"} {
 699                set lno [expr abs([bsearch $aw $path] + 1) + 1]
 700                $aw conf -state normal
 701                set ico [$aw image create $lno.0 \
 702                        -align center -padx 5 -pady 1 \
 703                        -image [mapicon $m $path]]
 704                $aw insert $lno.1 "$path\n"
 705                $aw conf -state disabled
 706                set file_states($path) [lreplace $s $ai $ai [list $ico]]
 707        } elseif {[mapicon $m $path] != [mapicon $old_m $path]} {
 708                set ico [lindex $d 0]
 709                $aw image conf $ico -image [mapicon $m $path]
 710        }
 711}
 712
 713proc with_update_index {body} {
 714        global update_index_fd
 715
 716        if {$update_index_fd == {}} {
 717                if {![lock_index update]} return
 718                set update_index_fd [open \
 719                        "| git update-index --add --remove -z --stdin" \
 720                        w]
 721                fconfigure $update_index_fd -translation binary
 722                uplevel 1 $body
 723                close $update_index_fd
 724                set update_index_fd {}
 725                unlock_index
 726        } else {
 727                uplevel 1 $body
 728        }
 729}
 730
 731proc update_index {path} {
 732        global update_index_fd
 733
 734        if {$update_index_fd == {}} {
 735                error {not in with_update_index}
 736        } else {
 737                puts -nonewline $update_index_fd "$path\0"
 738        }
 739}
 740
 741proc toggle_mode {path} {
 742        global file_states
 743
 744        set s $file_states($path)
 745        set m [lindex $s 0]
 746
 747        switch -- $m {
 748        AM -
 749        _O {set new A*}
 750        _M -
 751        MM {set new M*}
 752        _D {set new D*}
 753        default {return}
 754        }
 755
 756        with_update_index {update_index $path}
 757        display_file $path $new
 758}
 759
 760######################################################################
 761##
 762## icons
 763
 764set filemask {
 765#define mask_width 14
 766#define mask_height 15
 767static unsigned char mask_bits[] = {
 768   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
 769   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f,
 770   0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f};
 771}
 772
 773image create bitmap file_plain -background white -foreground black -data {
 774#define plain_width 14
 775#define plain_height 15
 776static unsigned char plain_bits[] = {
 777   0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
 778   0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10,
 779   0x02, 0x10, 0x02, 0x10, 0xfe, 0x1f};
 780} -maskdata $filemask
 781
 782image create bitmap file_mod -background white -foreground blue -data {
 783#define mod_width 14
 784#define mod_height 15
 785static unsigned char mod_bits[] = {
 786   0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
 787   0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
 788   0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
 789} -maskdata $filemask
 790
 791image create bitmap file_fulltick -background white -foreground "#007000" -data {
 792#define file_fulltick_width 14
 793#define file_fulltick_height 15
 794static unsigned char file_fulltick_bits[] = {
 795   0xfe, 0x01, 0x02, 0x1a, 0x02, 0x0c, 0x02, 0x0c, 0x02, 0x16, 0x02, 0x16,
 796   0x02, 0x13, 0x00, 0x13, 0x86, 0x11, 0x8c, 0x11, 0xd8, 0x10, 0xf2, 0x10,
 797   0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
 798} -maskdata $filemask
 799
 800image create bitmap file_parttick -background white -foreground "#005050" -data {
 801#define parttick_width 14
 802#define parttick_height 15
 803static unsigned char parttick_bits[] = {
 804   0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10,
 805   0x7a, 0x14, 0x02, 0x16, 0x02, 0x13, 0x8a, 0x11, 0xda, 0x10, 0x72, 0x10,
 806   0x22, 0x10, 0x02, 0x10, 0xfe, 0x1f};
 807} -maskdata $filemask
 808
 809image create bitmap file_question -background white -foreground black -data {
 810#define file_question_width 14
 811#define file_question_height 15
 812static unsigned char file_question_bits[] = {
 813   0xfe, 0x01, 0x02, 0x02, 0xe2, 0x04, 0xf2, 0x09, 0x1a, 0x1b, 0x0a, 0x13,
 814   0x82, 0x11, 0xc2, 0x10, 0x62, 0x10, 0x62, 0x10, 0x02, 0x10, 0x62, 0x10,
 815   0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f};
 816} -maskdata $filemask
 817
 818image create bitmap file_removed -background white -foreground red -data {
 819#define file_removed_width 14
 820#define file_removed_height 15
 821static unsigned char file_removed_bits[] = {
 822   0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10,
 823   0x1a, 0x16, 0x32, 0x13, 0xe2, 0x11, 0xc2, 0x10, 0xe2, 0x11, 0x32, 0x13,
 824   0x1a, 0x16, 0x02, 0x10, 0xfe, 0x1f};
 825} -maskdata $filemask
 826
 827image create bitmap file_merge -background white -foreground blue -data {
 828#define file_merge_width 14
 829#define file_merge_height 15
 830static unsigned char file_merge_bits[] = {
 831   0xfe, 0x01, 0x02, 0x03, 0x62, 0x05, 0x62, 0x09, 0x62, 0x1f, 0x62, 0x10,
 832   0xfa, 0x11, 0xf2, 0x10, 0x62, 0x10, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10,
 833   0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f};
 834} -maskdata $filemask
 835
 836set max_status_desc 0
 837foreach i {
 838                {__ i plain    "Unmodified"}
 839                {_M i mod      "Modified"}
 840                {M_ i fulltick "Checked in"}
 841                {MM i parttick "Partially checked in"}
 842
 843                {_O o plain    "Untracked"}
 844                {A_ o fulltick "Added"}
 845                {AM o parttick "Partially added"}
 846
 847                {_D i question "Missing"}
 848                {D_ i removed  "Removed"}
 849                {DD i removed  "Removed"}
 850                {DO i removed  "Removed (still exists)"}
 851
 852                {UM i merge    "Merge conflicts"}
 853                {U_ i merge    "Merge conflicts"}
 854        } {
 855        if {$max_status_desc < [string length [lindex $i 3]]} {
 856                set max_status_desc [string length [lindex $i 3]]
 857        }
 858        set all_cols([lindex $i 0]) [lindex $i 1]
 859        set all_icons([lindex $i 0]) file_[lindex $i 2]
 860        set all_descs([lindex $i 0]) [lindex $i 3]
 861}
 862unset filemask i
 863
 864######################################################################
 865##
 866## util
 867
 868proc error_popup {msg} {
 869        set w .error
 870        toplevel $w
 871        wm transient $w .
 872        show_msg $w $w $msg
 873}
 874
 875proc show_msg {w top msg} {
 876        global gitdir appname
 877
 878        message $w.m -text $msg -justify left -aspect 400
 879        pack $w.m -side top -fill x -padx 5 -pady 10
 880        button $w.ok -text OK \
 881                -width 15 \
 882                -command "destroy $top"
 883        pack $w.ok -side bottom
 884        bind $top <Visibility> "grab $top; focus $top"
 885        bind $top <Key-Return> "destroy $top"
 886        wm title $top "error: $appname ([file normalize [file dirname $gitdir]])"
 887        tkwait window $top
 888}
 889
 890proc hook_failed_popup {hook msg} {
 891        global gitdir mainfont difffont appname
 892
 893        set w .hookfail
 894        toplevel $w
 895        wm transient $w .
 896
 897        frame $w.m
 898        label $w.m.l1 -text "$hook hook failed:" \
 899                -anchor w \
 900                -justify left \
 901                -font [concat $mainfont bold]
 902        text $w.m.t \
 903                -background white -borderwidth 1 \
 904                -relief sunken \
 905                -width 80 -height 10 \
 906                -font $difffont \
 907                -yscrollcommand [list $w.m.sby set]
 908        label $w.m.l2 \
 909                -text {You must correct the above errors before committing.} \
 910                -anchor w \
 911                -justify left \
 912                -font [concat $mainfont bold]
 913        scrollbar $w.m.sby -command [list $w.m.t yview]
 914        pack $w.m.l1 -side top -fill x
 915        pack $w.m.l2 -side bottom -fill x
 916        pack $w.m.sby -side right -fill y
 917        pack $w.m.t -side left -fill both -expand 1
 918        pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10
 919
 920        $w.m.t insert 1.0 $msg
 921        $w.m.t conf -state disabled
 922
 923        button $w.ok -text OK \
 924                -width 15 \
 925                -command "destroy $w"
 926        pack $w.ok -side bottom
 927
 928        bind $w <Visibility> "grab $w; focus $w"
 929        bind $w <Key-Return> "destroy $w"
 930        wm title $w "error: $appname ([file normalize [file dirname $gitdir]])"
 931        tkwait window $w
 932}
 933
 934######################################################################
 935##
 936## ui commands
 937
 938set starting_gitk_msg {Please wait... Starting gitk...}
 939proc do_gitk {} {
 940        global tcl_platform ui_status_value starting_gitk_msg
 941
 942        set ui_status_value $starting_gitk_msg
 943        after 10000 {
 944                if {$ui_status_value == $starting_gitk_msg} {
 945                        set ui_status_value {Ready.}
 946                }
 947        }
 948
 949    if {$tcl_platform(platform) == "windows"} {
 950                exec sh -c gitk &
 951        } else {
 952                exec gitk &
 953        }
 954}
 955
 956proc do_quit {} {
 957        global gitdir ui_comm
 958
 959        set save [file join $gitdir GITGUI_MSG]
 960        set msg [string trim [$ui_comm get 0.0 end]]
 961        if {[$ui_comm edit modified] && $msg != {}} {
 962                catch {
 963                        set fd [open $save w]
 964                        puts $fd [string trim [$ui_comm get 0.0 end]]
 965                        close $fd
 966                }
 967        } elseif {$msg == {} && [file exists $save]} {
 968                file delete $save
 969        }
 970
 971        destroy .
 972}
 973
 974proc do_rescan {} {
 975        update_status
 976}
 977
 978proc do_checkin_all {} {
 979        global checkin_active ui_status_value
 980
 981        if {$checkin_active || ![lock_index begin-update]} return
 982
 983        set checkin_active 1
 984        set ui_status_value {Checking in all files...}
 985        after 1 {
 986                with_update_index {
 987                        foreach path [array names file_states] {
 988                                set s $file_states($path)
 989                                set m [lindex $s 0]
 990                                switch -- $m {
 991                                AM -
 992                                MM -
 993                                _M -
 994                                _D {toggle_mode $path}
 995                                }
 996                        }
 997                }
 998                set checkin_active 0
 999                set ui_status_value {Ready.}
1000        }
1001}
1002
1003proc do_signoff {} {
1004        global ui_comm
1005
1006        catch {
1007                set me [exec git var GIT_COMMITTER_IDENT]
1008                if {[regexp {(.*) [0-9]+ [-+0-9]+$} $me me name]} {
1009                        set str "Signed-off-by: $name"
1010                        if {[$ui_comm get {end -1c linestart} {end -1c}] != $str} {
1011                                $ui_comm insert end "\n"
1012                                $ui_comm insert end $str
1013                                $ui_comm see end
1014                        }
1015                }
1016        }
1017}
1018
1019proc do_amend_last {} {
1020        load_last_commit
1021}
1022
1023proc do_commit {} {
1024        commit_tree
1025}
1026
1027# shift == 1: left click
1028#          3: right click  
1029proc click {w x y shift wx wy} {
1030        global ui_index ui_other
1031
1032        set pos [split [$w index @$x,$y] .]
1033        set lno [lindex $pos 0]
1034        set col [lindex $pos 1]
1035        set path [$w get $lno.1 $lno.end]
1036        if {$path == {}} return
1037
1038        if {$col > 0 && $shift == 1} {
1039                $ui_index tag remove in_diff 0.0 end
1040                $ui_other tag remove in_diff 0.0 end
1041                $w tag add in_diff $lno.0 [expr $lno + 1].0
1042                show_diff $path
1043        }
1044}
1045
1046proc unclick {w x y} {
1047        set pos [split [$w index @$x,$y] .]
1048        set lno [lindex $pos 0]
1049        set col [lindex $pos 1]
1050        set path [$w get $lno.1 $lno.end]
1051        if {$path == {}} return
1052
1053        if {$col == 0} {
1054                toggle_mode $path
1055        }
1056}
1057
1058######################################################################
1059##
1060## ui init
1061
1062set mainfont {Helvetica 10}
1063set difffont {Courier 10}
1064set maincursor [. cget -cursor]
1065
1066switch -- $tcl_platform(platform) {
1067windows {set M1B Control; set M1T Ctrl}
1068default {set M1B M1; set M1T M1}
1069}
1070
1071# -- Menu Bar
1072menu .mbar -tearoff 0
1073.mbar add cascade -label Project -menu .mbar.project
1074.mbar add cascade -label Commit -menu .mbar.commit
1075.mbar add cascade -label Fetch -menu .mbar.fetch
1076.mbar add cascade -label Pull -menu .mbar.pull
1077. configure -menu .mbar
1078
1079# -- Project Menu
1080menu .mbar.project
1081.mbar.project add command -label Visualize \
1082        -command do_gitk \
1083        -font $mainfont
1084.mbar.project add command -label Quit \
1085        -command do_quit \
1086        -accelerator $M1T-Q \
1087        -font $mainfont
1088
1089# -- Commit Menu
1090menu .mbar.commit
1091.mbar.commit add command -label Rescan \
1092        -command do_rescan \
1093        -accelerator F5 \
1094        -font $mainfont
1095lappend disable_on_lock \
1096        [list .mbar.commit entryconf [.mbar.commit index last] -state]
1097.mbar.commit add command -label {Amend Last Commit} \
1098        -command do_amend_last \
1099        -font $mainfont
1100lappend disable_on_lock \
1101        [list .mbar.commit entryconf [.mbar.commit index last] -state]
1102.mbar.commit add command -label {Check-in All Files} \
1103        -command do_checkin_all \
1104        -accelerator $M1T-U \
1105        -font $mainfont
1106lappend disable_on_lock \
1107        [list .mbar.commit entryconf [.mbar.commit index last] -state]
1108.mbar.commit add command -label {Sign Off} \
1109        -command do_signoff \
1110        -accelerator $M1T-S \
1111        -font $mainfont
1112.mbar.commit add command -label Commit \
1113        -command do_commit \
1114        -accelerator $M1T-Return \
1115        -font $mainfont
1116lappend disable_on_lock \
1117        [list .mbar.commit entryconf [.mbar.commit index last] -state]
1118
1119# -- Fetch Menu
1120menu .mbar.fetch
1121
1122# -- Pull Menu
1123menu .mbar.pull
1124
1125# -- Main Window Layout
1126panedwindow .vpane -orient vertical
1127panedwindow .vpane.files -orient horizontal
1128.vpane add .vpane.files -sticky nsew -height 100 -width 400
1129pack .vpane -anchor n -side top -fill both -expand 1
1130
1131# -- Index File List
1132set ui_index .vpane.files.index.list
1133frame .vpane.files.index -height 100 -width 400
1134label .vpane.files.index.title -text {Modified Files} \
1135        -background green \
1136        -font $mainfont
1137text $ui_index -background white -borderwidth 0 \
1138        -width 40 -height 10 \
1139        -font $mainfont \
1140        -yscrollcommand {.vpane.files.index.sb set} \
1141        -cursor $maincursor \
1142        -state disabled
1143scrollbar .vpane.files.index.sb -command [list $ui_index yview]
1144pack .vpane.files.index.title -side top -fill x
1145pack .vpane.files.index.sb -side right -fill y
1146pack $ui_index -side left -fill both -expand 1
1147.vpane.files add .vpane.files.index -sticky nsew
1148
1149# -- Other (Add) File List
1150set ui_other .vpane.files.other.list
1151frame .vpane.files.other -height 100 -width 100
1152label .vpane.files.other.title -text {Untracked Files} \
1153        -background red \
1154        -font $mainfont
1155text $ui_other -background white -borderwidth 0 \
1156        -width 40 -height 10 \
1157        -font $mainfont \
1158        -yscrollcommand {.vpane.files.other.sb set} \
1159        -cursor $maincursor \
1160        -state disabled
1161scrollbar .vpane.files.other.sb -command [list $ui_other yview]
1162pack .vpane.files.other.title -side top -fill x
1163pack .vpane.files.other.sb -side right -fill y
1164pack $ui_other -side left -fill both -expand 1
1165.vpane.files add .vpane.files.other -sticky nsew
1166
1167$ui_index tag conf in_diff -font [concat $mainfont bold]
1168$ui_other tag conf in_diff -font [concat $mainfont bold]
1169
1170# -- Diff Header
1171set ui_fname_value {}
1172set ui_fstatus_value {}
1173frame .vpane.diff -height 200 -width 400
1174frame .vpane.diff.header
1175label .vpane.diff.header.l1 -text {File:} -font $mainfont
1176label .vpane.diff.header.l2 -textvariable ui_fname_value \
1177        -anchor w \
1178        -justify left \
1179        -font $mainfont
1180label .vpane.diff.header.l3 -text {Status:} -font $mainfont
1181label .vpane.diff.header.l4 -textvariable ui_fstatus_value \
1182        -width $max_status_desc \
1183        -anchor w \
1184        -justify left \
1185        -font $mainfont
1186pack .vpane.diff.header.l1 -side left
1187pack .vpane.diff.header.l2 -side left -fill x
1188pack .vpane.diff.header.l4 -side right
1189pack .vpane.diff.header.l3 -side right
1190
1191# -- Diff Body
1192frame .vpane.diff.body
1193set ui_diff .vpane.diff.body.t
1194text $ui_diff -background white -borderwidth 0 \
1195        -width 80 -height 15 -wrap none \
1196        -font $difffont \
1197        -xscrollcommand {.vpane.diff.body.sbx set} \
1198        -yscrollcommand {.vpane.diff.body.sby set} \
1199        -cursor $maincursor \
1200        -state disabled
1201scrollbar .vpane.diff.body.sbx -orient horizontal \
1202        -command [list $ui_diff xview]
1203scrollbar .vpane.diff.body.sby -orient vertical \
1204        -command [list $ui_diff yview]
1205pack .vpane.diff.body.sbx -side bottom -fill x
1206pack .vpane.diff.body.sby -side right -fill y
1207pack $ui_diff -side left -fill both -expand 1
1208pack .vpane.diff.header -side top -fill x
1209pack .vpane.diff.body -side bottom -fill both -expand 1
1210.vpane add .vpane.diff -stick nsew
1211
1212$ui_diff tag conf dm -foreground red
1213$ui_diff tag conf dp -foreground blue
1214$ui_diff tag conf da -font [concat $difffont bold]
1215$ui_diff tag conf di -foreground "#00a000"
1216$ui_diff tag conf dni -foreground "#a000a0"
1217$ui_diff tag conf bold -font [concat $difffont bold]
1218
1219# -- Commit Area
1220frame .vpane.commarea -height 170
1221.vpane add .vpane.commarea -stick nsew
1222
1223# -- Commit Area Buttons
1224frame .vpane.commarea.buttons
1225label .vpane.commarea.buttons.l -text {} \
1226        -anchor w \
1227        -justify left \
1228        -font $mainfont
1229pack .vpane.commarea.buttons.l -side top -fill x
1230pack .vpane.commarea.buttons -side left -fill y
1231
1232button .vpane.commarea.buttons.rescan -text {Rescan} \
1233        -command do_rescan \
1234        -font $mainfont
1235pack .vpane.commarea.buttons.rescan -side top -fill x
1236lappend disable_on_lock {.vpane.commarea.buttons.rescan conf -state}
1237
1238button .vpane.commarea.buttons.amend -text {Amend Last} \
1239        -command do_amend_last \
1240        -font $mainfont
1241pack .vpane.commarea.buttons.amend -side top -fill x
1242lappend disable_on_lock {.vpane.commarea.buttons.amend conf -state}
1243
1244button .vpane.commarea.buttons.ciall -text {Check-in All} \
1245        -command do_checkin_all \
1246        -font $mainfont
1247pack .vpane.commarea.buttons.ciall -side top -fill x
1248lappend disable_on_lock {.vpane.commarea.buttons.ciall conf -state}
1249
1250button .vpane.commarea.buttons.signoff -text {Sign Off} \
1251        -command do_signoff \
1252        -font $mainfont
1253pack .vpane.commarea.buttons.signoff -side top -fill x
1254
1255button .vpane.commarea.buttons.commit -text {Commit} \
1256        -command do_commit \
1257        -font $mainfont
1258pack .vpane.commarea.buttons.commit -side top -fill x
1259lappend disable_on_lock {.vpane.commarea.buttons.commit conf -state}
1260
1261# -- Commit Message Buffer
1262frame .vpane.commarea.buffer
1263set ui_comm .vpane.commarea.buffer.t
1264label .vpane.commarea.buffer.l -text {Commit Message:} \
1265        -anchor w \
1266        -justify left \
1267        -font $mainfont
1268text $ui_comm -background white -borderwidth 1 \
1269        -relief sunken \
1270        -width 75 -height 10 -wrap none \
1271        -font $difffont \
1272        -yscrollcommand {.vpane.commarea.buffer.sby set} \
1273        -cursor $maincursor
1274scrollbar .vpane.commarea.buffer.sby -command [list $ui_comm yview]
1275pack .vpane.commarea.buffer.l -side top -fill x
1276pack .vpane.commarea.buffer.sby -side right -fill y
1277pack $ui_comm -side left -fill y
1278pack .vpane.commarea.buffer -side left -fill y
1279
1280# -- Status Bar
1281set ui_status_value {Initializing...}
1282label .status -textvariable ui_status_value \
1283        -anchor w \
1284        -justify left \
1285        -borderwidth 1 \
1286        -relief sunken \
1287        -font $mainfont
1288pack .status -anchor w -side bottom -fill x
1289
1290# -- Key Bindings
1291bind $ui_comm <$M1B-Key-Return> {do_commit;break}
1292bind . <Destroy> do_quit
1293bind . <Key-F5> do_rescan
1294bind . <$M1B-Key-r> do_rescan
1295bind . <$M1B-Key-R> do_rescan
1296bind . <$M1B-Key-s> do_signoff
1297bind . <$M1B-Key-S> do_signoff
1298bind . <$M1B-Key-u> do_checkin_all
1299bind . <$M1B-Key-U> do_checkin_all
1300bind . <$M1B-Key-Return> do_commit
1301bind . <$M1B-Key-q> do_quit
1302bind . <$M1B-Key-Q> do_quit
1303foreach i [list $ui_index $ui_other] {
1304        bind $i <Button-1> {click %W %x %y 1 %X %Y; break}
1305        bind $i <Button-3> {click %W %x %y 3 %X %Y; break}
1306        bind $i <ButtonRelease-1> {unclick %W %x %y; break}
1307}
1308unset i M1B M1T
1309
1310######################################################################
1311##
1312## main
1313
1314if {[catch {set gitdir [exec git rev-parse --git-dir]} err]} {
1315        show_msg {} . "Cannot find the git directory: $err"
1316        exit 1
1317}
1318
1319set appname [lindex [file split $argv0] end]
1320if {$appname == {git-citool}} {
1321        set single_commit 1
1322}
1323
1324wm title . "$appname ([file normalize [file dirname $gitdir]])"
1325focus -force $ui_comm
1326update_status