git-gui / lib / diff.tclon commit Merge branch 'maint' (c9c6cc8)
   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        set ::current_diff_inheader 1
 280        fconfigure $fd \
 281                -blocking 0 \
 282                -encoding binary \
 283                -translation binary
 284        fileevent $fd readable [list read_diff $fd $scroll_pos]
 285}
 286
 287proc read_diff {fd scroll_pos} {
 288        global ui_diff diff_active
 289        global is_3way_diff current_diff_header
 290        global current_diff_queue
 291
 292        $ui_diff conf -state normal
 293        while {[gets $fd line] >= 0} {
 294                # -- Cleanup uninteresting diff header lines.
 295                #
 296                if {$::current_diff_inheader} {
 297                        if {   [string match {diff --git *}      $line]
 298                            || [string match {diff --cc *}       $line]
 299                            || [string match {diff --combined *} $line]
 300                            || [string match {--- *}             $line]
 301                            || [string match {+++ *}             $line]} {
 302                                append current_diff_header $line "\n"
 303                                continue
 304                        }
 305                }
 306                if {[string match {index *} $line]} continue
 307                if {$line eq {deleted file mode 120000}} {
 308                        set line "deleted symlink"
 309                }
 310                set ::current_diff_inheader 0
 311
 312                # -- Automatically detect if this is a 3 way diff.
 313                #
 314                if {[string match {@@@ *} $line]} {set is_3way_diff 1}
 315
 316                if {[string match {mode *} $line]
 317                        || [string match {new file *} $line]
 318                        || [regexp {^(old|new) mode *} $line]
 319                        || [string match {deleted file *} $line]
 320                        || [string match {deleted symlink} $line]
 321                        || [string match {Binary files * and * differ} $line]
 322                        || $line eq {\ No newline at end of file}
 323                        || [regexp {^\* Unmerged path } $line]} {
 324                        set tags {}
 325                } elseif {$is_3way_diff} {
 326                        set op [string range $line 0 1]
 327                        switch -- $op {
 328                        {  } {set tags {}}
 329                        {@@} {set tags d_@}
 330                        { +} {set tags d_s+}
 331                        { -} {set tags d_s-}
 332                        {+ } {set tags d_+s}
 333                        {- } {set tags d_-s}
 334                        {--} {set tags d_--}
 335                        {++} {
 336                                if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
 337                                        set line [string replace $line 0 1 {  }]
 338                                        set tags d$op
 339                                } else {
 340                                        set tags d_++
 341                                }
 342                        }
 343                        default {
 344                                puts "error: Unhandled 3 way diff marker: {$op}"
 345                                set tags {}
 346                        }
 347                        }
 348                } else {
 349                        set op [string index $line 0]
 350                        switch -- $op {
 351                        { } {set tags {}}
 352                        {@} {set tags d_@}
 353                        {-} {set tags d_-}
 354                        {+} {
 355                                if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
 356                                        set line [string replace $line 0 0 { }]
 357                                        set tags d$op
 358                                } else {
 359                                        set tags d_+
 360                                }
 361                        }
 362                        default {
 363                                puts "error: Unhandled 2 way diff marker: {$op}"
 364                                set tags {}
 365                        }
 366                        }
 367                }
 368                $ui_diff insert end $line $tags
 369                if {[string index $line end] eq "\r"} {
 370                        $ui_diff tag add d_cr {end - 2c}
 371                }
 372                $ui_diff insert end "\n" $tags
 373        }
 374        $ui_diff conf -state disabled
 375
 376        if {[eof $fd]} {
 377                close $fd
 378
 379                if {$current_diff_queue ne {}} {
 380                        advance_diff_queue $scroll_pos
 381                        return
 382                }
 383
 384                set diff_active 0
 385                unlock_index
 386                if {$scroll_pos ne {}} {
 387                        update
 388                        $ui_diff yview moveto $scroll_pos
 389                }
 390                ui_ready
 391
 392                if {[$ui_diff index end] eq {2.0}} {
 393                        handle_empty_diff
 394                }
 395        }
 396}
 397
 398proc apply_hunk {x y} {
 399        global current_diff_path current_diff_header current_diff_side
 400        global ui_diff ui_index file_states
 401
 402        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 403        if {![lock_index apply_hunk]} return
 404
 405        set apply_cmd {apply --cached --whitespace=nowarn}
 406        set mi [lindex $file_states($current_diff_path) 0]
 407        if {$current_diff_side eq $ui_index} {
 408                set failed_msg [mc "Failed to unstage selected hunk."]
 409                lappend apply_cmd --reverse
 410                if {[string index $mi 0] ne {M}} {
 411                        unlock_index
 412                        return
 413                }
 414        } else {
 415                set failed_msg [mc "Failed to stage selected hunk."]
 416                if {[string index $mi 1] ne {M}} {
 417                        unlock_index
 418                        return
 419                }
 420        }
 421
 422        set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
 423        set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
 424        if {$s_lno eq {}} {
 425                unlock_index
 426                return
 427        }
 428
 429        set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
 430        if {$e_lno eq {}} {
 431                set e_lno end
 432        }
 433
 434        if {[catch {
 435                set p [eval git_write $apply_cmd]
 436                fconfigure $p -translation binary -encoding binary
 437                puts -nonewline $p $current_diff_header
 438                puts -nonewline $p [$ui_diff get $s_lno $e_lno]
 439                close $p} err]} {
 440                error_popup [append $failed_msg "\n\n$err"]
 441                unlock_index
 442                return
 443        }
 444
 445        $ui_diff conf -state normal
 446        $ui_diff delete $s_lno $e_lno
 447        $ui_diff conf -state disabled
 448
 449        if {[$ui_diff get 1.0 end] eq "\n"} {
 450                set o _
 451        } else {
 452                set o ?
 453        }
 454
 455        if {$current_diff_side eq $ui_index} {
 456                set mi ${o}M
 457        } elseif {[string index $mi 0] eq {_}} {
 458                set mi M$o
 459        } else {
 460                set mi ?$o
 461        }
 462        unlock_index
 463        display_file $current_diff_path $mi
 464        # This should trigger shift to the next changed file
 465        if {$o eq {_}} {
 466                reshow_diff
 467        }
 468}
 469
 470proc apply_line {x y} {
 471        global current_diff_path current_diff_header current_diff_side
 472        global ui_diff ui_index file_states
 473
 474        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 475        if {![lock_index apply_hunk]} return
 476
 477        set apply_cmd {apply --cached --whitespace=nowarn}
 478        set mi [lindex $file_states($current_diff_path) 0]
 479        if {$current_diff_side eq $ui_index} {
 480                set failed_msg [mc "Failed to unstage selected line."]
 481                set to_context {+}
 482                lappend apply_cmd --reverse
 483                if {[string index $mi 0] ne {M}} {
 484                        unlock_index
 485                        return
 486                }
 487        } else {
 488                set failed_msg [mc "Failed to stage selected line."]
 489                set to_context {-}
 490                if {[string index $mi 1] ne {M}} {
 491                        unlock_index
 492                        return
 493                }
 494        }
 495
 496        set the_l [$ui_diff index @$x,$y]
 497
 498        # operate only on change lines
 499        set c1 [$ui_diff get "$the_l linestart"]
 500        if {$c1 ne {+} && $c1 ne {-}} {
 501                unlock_index
 502                return
 503        }
 504        set sign $c1
 505
 506        set i_l [$ui_diff search -backwards -regexp ^@@ $the_l 0.0]
 507        if {$i_l eq {}} {
 508                unlock_index
 509                return
 510        }
 511        # $i_l is now at the beginning of a line
 512
 513        # pick start line number from hunk header
 514        set hh [$ui_diff get $i_l "$i_l + 1 lines"]
 515        set hh [lindex [split $hh ,] 0]
 516        set hln [lindex [split $hh -] 1]
 517
 518        # There is a special situation to take care of. Consider this hunk:
 519        #
 520        #    @@ -10,4 +10,4 @@
 521        #     context before
 522        #    -old 1
 523        #    -old 2
 524        #    +new 1
 525        #    +new 2
 526        #     context after
 527        #
 528        # We used to keep the context lines in the order they appear in the
 529        # hunk. But then it is not possible to correctly stage only
 530        # "-old 1" and "+new 1" - it would result in this staged text:
 531        #
 532        #    context before
 533        #    old 2
 534        #    new 1
 535        #    context after
 536        #
 537        # (By symmetry it is not possible to *un*stage "old 2" and "new 2".)
 538        #
 539        # We resolve the problem by introducing an asymmetry, namely, when
 540        # a "+" line is *staged*, it is moved in front of the context lines
 541        # that are generated from the "-" lines that are immediately before
 542        # the "+" block. That is, we construct this patch:
 543        #
 544        #    @@ -10,4 +10,5 @@
 545        #     context before
 546        #    +new 1
 547        #     old 1
 548        #     old 2
 549        #     context after
 550        #
 551        # But we do *not* treat "-" lines that are *un*staged in a special
 552        # way.
 553        #
 554        # With this asymmetry it is possible to stage the change
 555        # "old 1" -> "new 1" directly, and to stage the change
 556        # "old 2" -> "new 2" by first staging the entire hunk and
 557        # then unstaging the change "old 1" -> "new 1".
 558
 559        # This is non-empty if and only if we are _staging_ changes;
 560        # then it accumulates the consecutive "-" lines (after converting
 561        # them to context lines) in order to be moved after the "+" change
 562        # line.
 563        set pre_context {}
 564
 565        set n 0
 566        set i_l [$ui_diff index "$i_l + 1 lines"]
 567        set patch {}
 568        while {[$ui_diff compare $i_l < "end - 1 chars"] &&
 569               [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
 570                set next_l [$ui_diff index "$i_l + 1 lines"]
 571                set c1 [$ui_diff get $i_l]
 572                if {[$ui_diff compare $i_l <= $the_l] &&
 573                    [$ui_diff compare $the_l < $next_l]} {
 574                        # the line to stage/unstage
 575                        set ln [$ui_diff get $i_l $next_l]
 576                        if {$c1 eq {-}} {
 577                                set n [expr $n+1]
 578                                set patch "$patch$pre_context$ln"
 579                        } else {
 580                                set patch "$patch$ln$pre_context"
 581                        }
 582                        set pre_context {}
 583                } elseif {$c1 ne {-} && $c1 ne {+}} {
 584                        # context line
 585                        set ln [$ui_diff get $i_l $next_l]
 586                        set patch "$patch$pre_context$ln"
 587                        set n [expr $n+1]
 588                        set pre_context {}
 589                } elseif {$c1 eq $to_context} {
 590                        # turn change line into context line
 591                        set ln [$ui_diff get "$i_l + 1 chars" $next_l]
 592                        if {$c1 eq {-}} {
 593                                set pre_context "$pre_context $ln"
 594                        } else {
 595                                set patch "$patch $ln"
 596                        }
 597                        set n [expr $n+1]
 598                }
 599                set i_l $next_l
 600        }
 601        set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch"
 602
 603        if {[catch {
 604                set p [eval git_write $apply_cmd]
 605                fconfigure $p -translation binary -encoding binary
 606                puts -nonewline $p $current_diff_header
 607                puts -nonewline $p $patch
 608                close $p} err]} {
 609                error_popup [append $failed_msg "\n\n$err"]
 610        }
 611
 612        unlock_index
 613}