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