git-gui / lib / diff.tclon commit Merge branch 'jc/better-conflict-resolution' (9ba929e)
   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 {}} {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 "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 "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 "LOCAL:\n" d======= \
 133                            [list ":1:$current_diff_path" ":2:$current_diff_path"]]
 134                lappend current_diff_queue \
 135                        [list "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 [expr {128 * 1024}]
 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 \
 222"* Untracked file is $sz bytes.
 223* Showing only first $max_sz bytes.
 224" d_@
 225                        }
 226                        $ui_diff insert end $content
 227                        if {$sz > $max_sz} {
 228                                $ui_diff insert end "
 229* Untracked file clipped here by [appname].
 230* To see the entire file, use an external editor.
 231" 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 line [string replace $line 0 0 { }]
 381                                        set tags d$op
 382                                } else {
 383                                        set tags d_+
 384                                }
 385                        }
 386                        default {
 387                                puts "error: Unhandled 2 way diff marker: {$op}"
 388                                set tags {}
 389                        }
 390                        }
 391                }
 392                $ui_diff insert end $line $tags
 393                if {[string index $line end] eq "\r"} {
 394                        $ui_diff tag add d_cr {end - 2c}
 395                }
 396                $ui_diff insert end "\n" $tags
 397        }
 398        $ui_diff conf -state disabled
 399
 400        if {[eof $fd]} {
 401                close $fd
 402
 403                if {$current_diff_queue ne {}} {
 404                        advance_diff_queue $cont_info
 405                        return
 406                }
 407
 408                set diff_active 0
 409                unlock_index
 410                set scroll_pos [lindex $cont_info 0]
 411                if {$scroll_pos ne {}} {
 412                        update
 413                        $ui_diff yview moveto $scroll_pos
 414                }
 415                ui_ready
 416
 417                if {[$ui_diff index end] eq {2.0}} {
 418                        handle_empty_diff
 419                }
 420                set callback [lindex $cont_info 1]
 421                if {$callback ne {}} {
 422                        eval $callback
 423                }
 424        }
 425}
 426
 427proc apply_hunk {x y} {
 428        global current_diff_path current_diff_header current_diff_side
 429        global ui_diff ui_index file_states
 430
 431        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 432        if {![lock_index apply_hunk]} return
 433
 434        set apply_cmd {apply --cached --whitespace=nowarn}
 435        set mi [lindex $file_states($current_diff_path) 0]
 436        if {$current_diff_side eq $ui_index} {
 437                set failed_msg [mc "Failed to unstage selected hunk."]
 438                lappend apply_cmd --reverse
 439                if {[string index $mi 0] ne {M}} {
 440                        unlock_index
 441                        return
 442                }
 443        } else {
 444                set failed_msg [mc "Failed to stage selected hunk."]
 445                if {[string index $mi 1] ne {M}} {
 446                        unlock_index
 447                        return
 448                }
 449        }
 450
 451        set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
 452        set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
 453        if {$s_lno eq {}} {
 454                unlock_index
 455                return
 456        }
 457
 458        set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
 459        if {$e_lno eq {}} {
 460                set e_lno end
 461        }
 462
 463        if {[catch {
 464                set enc [get_path_encoding $current_diff_path]
 465                set p [eval git_write $apply_cmd]
 466                fconfigure $p -translation binary -encoding $enc
 467                puts -nonewline $p $current_diff_header
 468                puts -nonewline $p [$ui_diff get $s_lno $e_lno]
 469                close $p} err]} {
 470                error_popup [append $failed_msg "\n\n$err"]
 471                unlock_index
 472                return
 473        }
 474
 475        $ui_diff conf -state normal
 476        $ui_diff delete $s_lno $e_lno
 477        $ui_diff conf -state disabled
 478
 479        if {[$ui_diff get 1.0 end] eq "\n"} {
 480                set o _
 481        } else {
 482                set o ?
 483        }
 484
 485        if {$current_diff_side eq $ui_index} {
 486                set mi ${o}M
 487        } elseif {[string index $mi 0] eq {_}} {
 488                set mi M$o
 489        } else {
 490                set mi ?$o
 491        }
 492        unlock_index
 493        display_file $current_diff_path $mi
 494        # This should trigger shift to the next changed file
 495        if {$o eq {_}} {
 496                reshow_diff
 497        }
 498}
 499
 500proc apply_line {x y} {
 501        global current_diff_path current_diff_header current_diff_side
 502        global ui_diff ui_index file_states
 503
 504        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 505        if {![lock_index apply_hunk]} return
 506
 507        set apply_cmd {apply --cached --whitespace=nowarn}
 508        set mi [lindex $file_states($current_diff_path) 0]
 509        if {$current_diff_side eq $ui_index} {
 510                set failed_msg [mc "Failed to unstage selected line."]
 511                set to_context {+}
 512                lappend apply_cmd --reverse
 513                if {[string index $mi 0] ne {M}} {
 514                        unlock_index
 515                        return
 516                }
 517        } else {
 518                set failed_msg [mc "Failed to stage selected line."]
 519                set to_context {-}
 520                if {[string index $mi 1] ne {M}} {
 521                        unlock_index
 522                        return
 523                }
 524        }
 525
 526        set the_l [$ui_diff index @$x,$y]
 527
 528        # operate only on change lines
 529        set c1 [$ui_diff get "$the_l linestart"]
 530        if {$c1 ne {+} && $c1 ne {-}} {
 531                unlock_index
 532                return
 533        }
 534        set sign $c1
 535
 536        set i_l [$ui_diff search -backwards -regexp ^@@ $the_l 0.0]
 537        if {$i_l eq {}} {
 538                unlock_index
 539                return
 540        }
 541        # $i_l is now at the beginning of a line
 542
 543        # pick start line number from hunk header
 544        set hh [$ui_diff get $i_l "$i_l + 1 lines"]
 545        set hh [lindex [split $hh ,] 0]
 546        set hln [lindex [split $hh -] 1]
 547
 548        # There is a special situation to take care of. Consider this hunk:
 549        #
 550        #    @@ -10,4 +10,4 @@
 551        #     context before
 552        #    -old 1
 553        #    -old 2
 554        #    +new 1
 555        #    +new 2
 556        #     context after
 557        #
 558        # We used to keep the context lines in the order they appear in the
 559        # hunk. But then it is not possible to correctly stage only
 560        # "-old 1" and "+new 1" - it would result in this staged text:
 561        #
 562        #    context before
 563        #    old 2
 564        #    new 1
 565        #    context after
 566        #
 567        # (By symmetry it is not possible to *un*stage "old 2" and "new 2".)
 568        #
 569        # We resolve the problem by introducing an asymmetry, namely, when
 570        # a "+" line is *staged*, it is moved in front of the context lines
 571        # that are generated from the "-" lines that are immediately before
 572        # the "+" block. That is, we construct this patch:
 573        #
 574        #    @@ -10,4 +10,5 @@
 575        #     context before
 576        #    +new 1
 577        #     old 1
 578        #     old 2
 579        #     context after
 580        #
 581        # But we do *not* treat "-" lines that are *un*staged in a special
 582        # way.
 583        #
 584        # With this asymmetry it is possible to stage the change
 585        # "old 1" -> "new 1" directly, and to stage the change
 586        # "old 2" -> "new 2" by first staging the entire hunk and
 587        # then unstaging the change "old 1" -> "new 1".
 588
 589        # This is non-empty if and only if we are _staging_ changes;
 590        # then it accumulates the consecutive "-" lines (after converting
 591        # them to context lines) in order to be moved after the "+" change
 592        # line.
 593        set pre_context {}
 594
 595        set n 0
 596        set i_l [$ui_diff index "$i_l + 1 lines"]
 597        set patch {}
 598        while {[$ui_diff compare $i_l < "end - 1 chars"] &&
 599               [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
 600                set next_l [$ui_diff index "$i_l + 1 lines"]
 601                set c1 [$ui_diff get $i_l]
 602                if {[$ui_diff compare $i_l <= $the_l] &&
 603                    [$ui_diff compare $the_l < $next_l]} {
 604                        # the line to stage/unstage
 605                        set ln [$ui_diff get $i_l $next_l]
 606                        if {$c1 eq {-}} {
 607                                set n [expr $n+1]
 608                                set patch "$patch$pre_context$ln"
 609                        } else {
 610                                set patch "$patch$ln$pre_context"
 611                        }
 612                        set pre_context {}
 613                } elseif {$c1 ne {-} && $c1 ne {+}} {
 614                        # context line
 615                        set ln [$ui_diff get $i_l $next_l]
 616                        set patch "$patch$pre_context$ln"
 617                        set n [expr $n+1]
 618                        set pre_context {}
 619                } elseif {$c1 eq $to_context} {
 620                        # turn change line into context line
 621                        set ln [$ui_diff get "$i_l + 1 chars" $next_l]
 622                        if {$c1 eq {-}} {
 623                                set pre_context "$pre_context $ln"
 624                        } else {
 625                                set patch "$patch $ln"
 626                        }
 627                        set n [expr $n+1]
 628                }
 629                set i_l $next_l
 630        }
 631        set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch"
 632
 633        if {[catch {
 634                set enc [get_path_encoding $current_diff_path]
 635                set p [eval git_write $apply_cmd]
 636                fconfigure $p -translation binary -encoding $enc
 637                puts -nonewline $p $current_diff_header
 638                puts -nonewline $p $patch
 639                close $p} err]} {
 640                error_popup [append $failed_msg "\n\n$err"]
 641        }
 642
 643        unlock_index
 644}