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