8fefc5d9ae9cad3be93946b67f20bf82f09d0c89
   1# git-gui diff viewer
   2# Copyright (C) 2006, 2007 Shawn Pearce
   3
   4proc clear_diff {} {
   5        global ui_diff current_diff_path current_diff_header
   6        global ui_index ui_workdir
   7
   8        $ui_diff conf -state normal
   9        $ui_diff delete 0.0 end
  10        $ui_diff conf -state disabled
  11
  12        set current_diff_path {}
  13        set current_diff_header {}
  14
  15        $ui_index tag remove in_diff 0.0 end
  16        $ui_workdir tag remove in_diff 0.0 end
  17}
  18
  19proc reshow_diff {} {
  20        global file_states file_lists
  21        global current_diff_path current_diff_side
  22        global ui_diff
  23
  24        set p $current_diff_path
  25        if {$p eq {}} {
  26                # No diff is being shown.
  27        } elseif {$current_diff_side eq {}} {
  28                clear_diff
  29        } elseif {[catch {set s $file_states($p)}]
  30                || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} {
  31
  32                if {[find_next_diff $current_diff_side $p {} {[^O]}]} {
  33                        next_diff
  34                } else {
  35                        clear_diff
  36                }
  37        } else {
  38                set save_pos [lindex [$ui_diff yview] 0]
  39                show_diff $p $current_diff_side {} $save_pos
  40        }
  41}
  42
  43proc handle_empty_diff {} {
  44        global current_diff_path file_states file_lists
  45
  46        set path $current_diff_path
  47        set s $file_states($path)
  48        if {[lindex $s 0] ne {_M}} return
  49
  50        info_popup [mc "No differences detected.
  51
  52%s has no changes.
  53
  54The modification date of this file was updated by another application, but the content within the file was not changed.
  55
  56A rescan will be automatically started to find other files which may have the same state." [short_path $path]]
  57
  58        clear_diff
  59        display_file $path __
  60        rescan ui_ready 0
  61}
  62
  63proc show_diff {path w {lno {}} {scroll_pos {}}} {
  64        global file_states file_lists
  65        global is_3way_diff diff_active repo_config
  66        global ui_diff ui_index ui_workdir
  67        global current_diff_path current_diff_side current_diff_header
  68        global current_diff_queue
  69
  70        if {$diff_active || ![lock_index read]} return
  71
  72        clear_diff
  73        if {$lno == {}} {
  74                set lno [lsearch -sorted -exact $file_lists($w) $path]
  75                if {$lno >= 0} {
  76                        incr lno
  77                }
  78        }
  79        if {$lno >= 1} {
  80                $w tag add in_diff $lno.0 [expr {$lno + 1}].0
  81                $w see $lno.0
  82        }
  83
  84        set s $file_states($path)
  85        set m [lindex $s 0]
  86        set current_diff_path $path
  87        set current_diff_side $w
  88        set current_diff_queue {}
  89        ui_status [mc "Loading diff of %s..." [escape_path $path]]
  90
  91        if {[string first {U} $m] >= 0} {
  92                merge_load_stages $path [list show_unmerged_diff $scroll_pos]
  93        } elseif {$m eq {_O}} {
  94                show_other_diff $path $w $m $scroll_pos
  95        } else {
  96                start_show_diff $scroll_pos
  97        }
  98}
  99
 100proc show_unmerged_diff {scroll_pos} {
 101        global current_diff_path current_diff_side
 102        global merge_stages ui_diff
 103        global current_diff_queue
 104
 105        if {$merge_stages(2) eq {}} {
 106                lappend current_diff_queue \
 107                        [list "LOCAL: deleted\nREMOTE:\n" d======= \
 108                            [list ":1:$current_diff_path" ":3:$current_diff_path"]]
 109        } elseif {$merge_stages(3) eq {}} {
 110                lappend current_diff_queue \
 111                        [list "REMOTE: deleted\nLOCAL:\n" d======= \
 112                            [list ":1:$current_diff_path" ":2:$current_diff_path"]]
 113        } elseif {[lindex $merge_stages(1) 0] eq {120000}
 114                || [lindex $merge_stages(2) 0] eq {120000}
 115                || [lindex $merge_stages(3) 0] eq {120000}} {
 116                lappend current_diff_queue \
 117                        [list "LOCAL:\n" d======= \
 118                            [list ":1:$current_diff_path" ":2:$current_diff_path"]]
 119                lappend current_diff_queue \
 120                        [list "REMOTE:\n" d======= \
 121                            [list ":1:$current_diff_path" ":3:$current_diff_path"]]
 122        } else {
 123                start_show_diff $scroll_pos
 124                return
 125        }
 126
 127        advance_diff_queue $scroll_pos
 128}
 129
 130proc advance_diff_queue {scroll_pos} {
 131        global current_diff_queue ui_diff
 132
 133        set item [lindex $current_diff_queue 0]
 134        set current_diff_queue [lrange $current_diff_queue 1 end]
 135
 136        $ui_diff conf -state normal
 137        $ui_diff insert end [lindex $item 0] [lindex $item 1]
 138        $ui_diff conf -state disabled
 139
 140        start_show_diff $scroll_pos [lindex $item 2]
 141}
 142
 143proc show_other_diff {path w m scroll_pos} {
 144        global file_states file_lists
 145        global is_3way_diff diff_active repo_config
 146        global ui_diff ui_index ui_workdir
 147        global current_diff_path current_diff_side current_diff_header
 148
 149        # - Git won't give us the diff, there's nothing to compare to!
 150        #
 151        if {$m eq {_O}} {
 152                set max_sz [expr {128 * 1024}]
 153                set type unknown
 154                if {[catch {
 155                                set type [file type $path]
 156                                switch -- $type {
 157                                directory {
 158                                        set type submodule
 159                                        set content {}
 160                                        set sz 0
 161                                }
 162                                link {
 163                                        set content [file readlink $path]
 164                                        set sz [string length $content]
 165                                }
 166                                file {
 167                                        set fd [open $path r]
 168                                        fconfigure $fd \
 169                                                -eofchar {} \
 170                                                -encoding [get_path_encoding $path]
 171                                        set content [read $fd $max_sz]
 172                                        close $fd
 173                                        set sz [file size $path]
 174                                }
 175                                default {
 176                                        error "'$type' not supported"
 177                                }
 178                                }
 179                        } err ]} {
 180                        set diff_active 0
 181                        unlock_index
 182                        ui_status [mc "Unable to display %s" [escape_path $path]]
 183                        error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
 184                        return
 185                }
 186                $ui_diff conf -state normal
 187                if {$type eq {submodule}} {
 188                        $ui_diff insert end [append \
 189                                "* " \
 190                                [mc "Git Repository (subproject)"] \
 191                                "\n"] d_@
 192                } elseif {![catch {set type [exec file $path]}]} {
 193                        set n [string length $path]
 194                        if {[string equal -length $n $path $type]} {
 195                                set type [string range $type $n end]
 196                                regsub {^:?\s*} $type {} type
 197                        }
 198                        $ui_diff insert end "* $type\n" d_@
 199                }
 200                if {[string first "\0" $content] != -1} {
 201                        $ui_diff insert end \
 202                                [mc "* Binary file (not showing content)."] \
 203                                d_@
 204                } else {
 205                        if {$sz > $max_sz} {
 206                                $ui_diff insert end \
 207"* Untracked file is $sz bytes.
 208* Showing only first $max_sz bytes.
 209" d_@
 210                        }
 211                        $ui_diff insert end $content
 212                        if {$sz > $max_sz} {
 213                                $ui_diff insert end "
 214* Untracked file clipped here by [appname].
 215* To see the entire file, use an external editor.
 216" d_@
 217                        }
 218                }
 219                $ui_diff conf -state disabled
 220                set diff_active 0
 221                unlock_index
 222                if {$scroll_pos ne {}} {
 223                        update
 224                        $ui_diff yview moveto $scroll_pos
 225                }
 226                ui_ready
 227                return
 228        }
 229}
 230
 231proc start_show_diff {scroll_pos {add_opts {}}} {
 232        global file_states file_lists
 233        global is_3way_diff diff_active repo_config
 234        global ui_diff ui_index ui_workdir
 235        global current_diff_path current_diff_side current_diff_header
 236
 237        set path $current_diff_path
 238        set w $current_diff_side
 239
 240        set s $file_states($path)
 241        set m [lindex $s 0]
 242        set is_3way_diff 0
 243        set diff_active 1
 244        set current_diff_header {}
 245
 246        set cmd [list]
 247        if {$w eq $ui_index} {
 248                lappend cmd diff-index
 249                lappend cmd --cached
 250        } elseif {$w eq $ui_workdir} {
 251                if {[string first {U} $m] >= 0} {
 252                        lappend cmd diff
 253                } else {
 254                        lappend cmd diff-files
 255                }
 256        }
 257
 258        lappend cmd -p
 259        lappend cmd --no-color
 260        if {$repo_config(gui.diffcontext) >= 1} {
 261                lappend cmd "-U$repo_config(gui.diffcontext)"
 262        }
 263        if {$w eq $ui_index} {
 264                lappend cmd [PARENT]
 265        }
 266        if {$add_opts ne {}} {
 267                eval lappend cmd $add_opts
 268        } else {
 269                lappend cmd --
 270                lappend cmd $path
 271        }
 272
 273        if {[catch {set fd [eval git_read --nice $cmd]} err]} {
 274                set diff_active 0
 275                unlock_index
 276                ui_status [mc "Unable to display %s" [escape_path $path]]
 277                error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
 278                return
 279        }
 280
 281        set ::current_diff_inheader 1
 282        fconfigure $fd \
 283                -blocking 0 \
 284                -encoding [get_path_encoding $path] \
 285                -translation lf
 286        fileevent $fd readable [list read_diff $fd $scroll_pos]
 287}
 288
 289proc read_diff {fd scroll_pos} {
 290        global ui_diff diff_active
 291        global is_3way_diff current_diff_header
 292        global current_diff_queue
 293
 294        $ui_diff conf -state normal
 295        while {[gets $fd line] >= 0} {
 296                # -- Cleanup uninteresting diff header lines.
 297                #
 298                if {$::current_diff_inheader} {
 299                        if {   [string match {diff --git *}      $line]
 300                            || [string match {diff --cc *}       $line]
 301                            || [string match {diff --combined *} $line]
 302                            || [string match {--- *}             $line]
 303                            || [string match {+++ *}             $line]} {
 304                                append current_diff_header $line "\n"
 305                                continue
 306                        }
 307                }
 308                if {[string match {index *} $line]} continue
 309                if {$line eq {deleted file mode 120000}} {
 310                        set line "deleted symlink"
 311                }
 312                set ::current_diff_inheader 0
 313
 314                # -- Automatically detect if this is a 3 way diff.
 315                #
 316                if {[string match {@@@ *} $line]} {set is_3way_diff 1}
 317
 318                if {[string match {mode *} $line]
 319                        || [string match {new file *} $line]
 320                        || [regexp {^(old|new) mode *} $line]
 321                        || [string match {deleted file *} $line]
 322                        || [string match {deleted symlink} $line]
 323                        || [string match {Binary files * and * differ} $line]
 324                        || $line eq {\ No newline at end of file}
 325                        || [regexp {^\* Unmerged path } $line]} {
 326                        set tags {}
 327                } elseif {$is_3way_diff} {
 328                        set op [string range $line 0 1]
 329                        switch -- $op {
 330                        {  } {set tags {}}
 331                        {@@} {set tags d_@}
 332                        { +} {set tags d_s+}
 333                        { -} {set tags d_s-}
 334                        {+ } {set tags d_+s}
 335                        {- } {set tags d_-s}
 336                        {--} {set tags d_--}
 337                        {++} {
 338                                if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
 339                                        set line [string replace $line 0 1 {  }]
 340                                        set tags d$op
 341                                } else {
 342                                        set tags d_++
 343                                }
 344                        }
 345                        default {
 346                                puts "error: Unhandled 3 way diff marker: {$op}"
 347                                set tags {}
 348                        }
 349                        }
 350                } else {
 351                        set op [string index $line 0]
 352                        switch -- $op {
 353                        { } {set tags {}}
 354                        {@} {set tags d_@}
 355                        {-} {set tags d_-}
 356                        {+} {
 357                                if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
 358                                        set line [string replace $line 0 0 { }]
 359                                        set tags d$op
 360                                } else {
 361                                        set tags d_+
 362                                }
 363                        }
 364                        default {
 365                                puts "error: Unhandled 2 way diff marker: {$op}"
 366                                set tags {}
 367                        }
 368                        }
 369                }
 370                $ui_diff insert end $line $tags
 371                if {[string index $line end] eq "\r"} {
 372                        $ui_diff tag add d_cr {end - 2c}
 373                }
 374                $ui_diff insert end "\n" $tags
 375        }
 376        $ui_diff conf -state disabled
 377
 378        if {[eof $fd]} {
 379                close $fd
 380
 381                if {$current_diff_queue ne {}} {
 382                        advance_diff_queue $scroll_pos
 383                        return
 384                }
 385
 386                set diff_active 0
 387                unlock_index
 388                if {$scroll_pos ne {}} {
 389                        update
 390                        $ui_diff yview moveto $scroll_pos
 391                }
 392                ui_ready
 393
 394                if {[$ui_diff index end] eq {2.0}} {
 395                        handle_empty_diff
 396                }
 397        }
 398}
 399
 400proc apply_hunk {x y} {
 401        global current_diff_path current_diff_header current_diff_side
 402        global ui_diff ui_index file_states
 403
 404        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 405        if {![lock_index apply_hunk]} return
 406
 407        set apply_cmd {apply --cached --whitespace=nowarn}
 408        set mi [lindex $file_states($current_diff_path) 0]
 409        if {$current_diff_side eq $ui_index} {
 410                set failed_msg [mc "Failed to unstage selected hunk."]
 411                lappend apply_cmd --reverse
 412                if {[string index $mi 0] ne {M}} {
 413                        unlock_index
 414                        return
 415                }
 416        } else {
 417                set failed_msg [mc "Failed to stage selected hunk."]
 418                if {[string index $mi 1] ne {M}} {
 419                        unlock_index
 420                        return
 421                }
 422        }
 423
 424        set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
 425        set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
 426        if {$s_lno eq {}} {
 427                unlock_index
 428                return
 429        }
 430
 431        set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
 432        if {$e_lno eq {}} {
 433                set e_lno end
 434        }
 435
 436        if {[catch {
 437                set enc [get_path_encoding $current_diff_path]
 438                set p [eval git_write $apply_cmd]
 439                fconfigure $p -translation binary -encoding $enc
 440                puts -nonewline $p $current_diff_header
 441                puts -nonewline $p [$ui_diff get $s_lno $e_lno]
 442                close $p} err]} {
 443                error_popup [append $failed_msg "\n\n$err"]
 444                unlock_index
 445                return
 446        }
 447
 448        $ui_diff conf -state normal
 449        $ui_diff delete $s_lno $e_lno
 450        $ui_diff conf -state disabled
 451
 452        if {[$ui_diff get 1.0 end] eq "\n"} {
 453                set o _
 454        } else {
 455                set o ?
 456        }
 457
 458        if {$current_diff_side eq $ui_index} {
 459                set mi ${o}M
 460        } elseif {[string index $mi 0] eq {_}} {
 461                set mi M$o
 462        } else {
 463                set mi ?$o
 464        }
 465        unlock_index
 466        display_file $current_diff_path $mi
 467        # This should trigger shift to the next changed file
 468        if {$o eq {_}} {
 469                reshow_diff
 470        }
 471}
 472
 473proc apply_line {x y} {
 474        global current_diff_path current_diff_header current_diff_side
 475        global ui_diff ui_index file_states
 476
 477        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 478        if {![lock_index apply_hunk]} return
 479
 480        set apply_cmd {apply --cached --whitespace=nowarn}
 481        set mi [lindex $file_states($current_diff_path) 0]
 482        if {$current_diff_side eq $ui_index} {
 483                set failed_msg [mc "Failed to unstage selected line."]
 484                set to_context {+}
 485                lappend apply_cmd --reverse
 486                if {[string index $mi 0] ne {M}} {
 487                        unlock_index
 488                        return
 489                }
 490        } else {
 491                set failed_msg [mc "Failed to stage selected line."]
 492                set to_context {-}
 493                if {[string index $mi 1] ne {M}} {
 494                        unlock_index
 495                        return
 496                }
 497        }
 498
 499        set the_l [$ui_diff index @$x,$y]
 500
 501        # operate only on change lines
 502        set c1 [$ui_diff get "$the_l linestart"]
 503        if {$c1 ne {+} && $c1 ne {-}} {
 504                unlock_index
 505                return
 506        }
 507        set sign $c1
 508
 509        set i_l [$ui_diff search -backwards -regexp ^@@ $the_l 0.0]
 510        if {$i_l eq {}} {
 511                unlock_index
 512                return
 513        }
 514        # $i_l is now at the beginning of a line
 515
 516        # pick start line number from hunk header
 517        set hh [$ui_diff get $i_l "$i_l + 1 lines"]
 518        set hh [lindex [split $hh ,] 0]
 519        set hln [lindex [split $hh -] 1]
 520
 521        # There is a special situation to take care of. Consider this hunk:
 522        #
 523        #    @@ -10,4 +10,4 @@
 524        #     context before
 525        #    -old 1
 526        #    -old 2
 527        #    +new 1
 528        #    +new 2
 529        #     context after
 530        #
 531        # We used to keep the context lines in the order they appear in the
 532        # hunk. But then it is not possible to correctly stage only
 533        # "-old 1" and "+new 1" - it would result in this staged text:
 534        #
 535        #    context before
 536        #    old 2
 537        #    new 1
 538        #    context after
 539        #
 540        # (By symmetry it is not possible to *un*stage "old 2" and "new 2".)
 541        #
 542        # We resolve the problem by introducing an asymmetry, namely, when
 543        # a "+" line is *staged*, it is moved in front of the context lines
 544        # that are generated from the "-" lines that are immediately before
 545        # the "+" block. That is, we construct this patch:
 546        #
 547        #    @@ -10,4 +10,5 @@
 548        #     context before
 549        #    +new 1
 550        #     old 1
 551        #     old 2
 552        #     context after
 553        #
 554        # But we do *not* treat "-" lines that are *un*staged in a special
 555        # way.
 556        #
 557        # With this asymmetry it is possible to stage the change
 558        # "old 1" -> "new 1" directly, and to stage the change
 559        # "old 2" -> "new 2" by first staging the entire hunk and
 560        # then unstaging the change "old 1" -> "new 1".
 561
 562        # This is non-empty if and only if we are _staging_ changes;
 563        # then it accumulates the consecutive "-" lines (after converting
 564        # them to context lines) in order to be moved after the "+" change
 565        # line.
 566        set pre_context {}
 567
 568        set n 0
 569        set i_l [$ui_diff index "$i_l + 1 lines"]
 570        set patch {}
 571        while {[$ui_diff compare $i_l < "end - 1 chars"] &&
 572               [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
 573                set next_l [$ui_diff index "$i_l + 1 lines"]
 574                set c1 [$ui_diff get $i_l]
 575                if {[$ui_diff compare $i_l <= $the_l] &&
 576                    [$ui_diff compare $the_l < $next_l]} {
 577                        # the line to stage/unstage
 578                        set ln [$ui_diff get $i_l $next_l]
 579                        if {$c1 eq {-}} {
 580                                set n [expr $n+1]
 581                                set patch "$patch$pre_context$ln"
 582                        } else {
 583                                set patch "$patch$ln$pre_context"
 584                        }
 585                        set pre_context {}
 586                } elseif {$c1 ne {-} && $c1 ne {+}} {
 587                        # context line
 588                        set ln [$ui_diff get $i_l $next_l]
 589                        set patch "$patch$pre_context$ln"
 590                        set n [expr $n+1]
 591                        set pre_context {}
 592                } elseif {$c1 eq $to_context} {
 593                        # turn change line into context line
 594                        set ln [$ui_diff get "$i_l + 1 chars" $next_l]
 595                        if {$c1 eq {-}} {
 596                                set pre_context "$pre_context $ln"
 597                        } else {
 598                                set patch "$patch $ln"
 599                        }
 600                        set n [expr $n+1]
 601                }
 602                set i_l $next_l
 603        }
 604        set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch"
 605
 606        if {[catch {
 607                set enc [get_path_encoding $current_diff_path]
 608                set p [eval git_write $apply_cmd]
 609                fconfigure $p -translation binary -encoding $enc
 610                puts -nonewline $p $current_diff_header
 611                puts -nonewline $p $patch
 612                close $p} err]} {
 613                error_popup [append $failed_msg "\n\n$err"]
 614        }
 615
 616        unlock_index
 617}