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