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