1# git-gui diff viewer
   2# Copyright (C) 2006, 2007 Shawn Pearce
   3proc clear_diff {} {
   5        global ui_diff current_diff_path current_diff_header
   6        global ui_index ui_workdir
   7        $ui_diff conf -state normal
   9        $ui_diff delete 0.0 end
  10        $ui_diff conf -state disabled
  11        set current_diff_path {}
  13        set current_diff_header {}
  14        $ui_index tag remove in_diff 0.0 end
  16        $ui_workdir tag remove in_diff 0.0 end
  17}
  18proc reshow_diff {{after {}}} {
  20        global file_states file_lists
  21        global current_diff_path current_diff_side
  22        global ui_diff
  23        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                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}
  42proc 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}
  51proc handle_empty_diff {} {
  53        global current_diff_path file_states file_lists
  54        global diff_empty_count
  55        set path $current_diff_path
  57        set s $file_states($path)
  58        if {[lindex $s 0] ne {_M}} return
  59        # Prevent infinite rescan loops
  61        incr diff_empty_count
  62        if {$diff_empty_count > 1} return
  63        info_popup [mc "No differences detected.
  65%s has no changes.
  67The modification date of this file was updated by another application, but the content within the file was not changed.
  69A rescan will be automatically started to find other files which may have the same state." [short_path $path]]
  71        clear_diff
  73        display_file $path __
  74        rescan ui_ready 0
  75}
  76proc 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        if {$diff_active || ![lock_index read]} return
  85        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        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        set cont_info [list $scroll_pos $callback]
 107        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}
 116proc 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        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        advance_diff_queue $cont_info
 148}
 149proc advance_diff_queue {cont_info} {
 151        global current_diff_queue ui_diff
 152        set item [lindex $current_diff_queue 0]
 154        set current_diff_queue [lrange $current_diff_queue 1 end]
 155        $ui_diff conf -state normal
 157        $ui_diff insert end [lindex $item 0] [lindex $item 1]
 158        $ui_diff conf -state disabled
 159        start_show_diff $cont_info [lindex $item 2]
 161}
 162proc 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        # - 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}
 255proc start_show_diff {cont_info {add_opts {}}} {
 257        global file_states file_lists
 258        global is_3way_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        set path $current_diff_path
 263        set w $current_diff_side
 264        set s $file_states($path)
 266        set m [lindex $s 0]
 267        set is_3way_diff 0
 268        set diff_active 1
 269        set current_diff_header {}
 270        set cmd [list]
 272        if {$w eq $ui_index} {
 273                lappend cmd diff-index
 274                lappend cmd --cached
 275        } elseif {$w eq $ui_workdir} {
 276                if {[string first {U} $m] >= 0} {
 277                        lappend cmd diff
 278                } else {
 279                        lappend cmd diff-files
 280                }
 281        }
 282        lappend cmd -p
 284        lappend cmd --no-color
 285        if {$repo_config(gui.diffcontext) >= 1} {
 286                lappend cmd "-U$repo_config(gui.diffcontext)"
 287        }
 288        if {$w eq $ui_index} {
 289                lappend cmd [PARENT]
 290        }
 291        if {$add_opts ne {}} {
 292                eval lappend cmd $add_opts
 293        } else {
 294                lappend cmd --
 295                lappend cmd $path
 296        }
 297        if {[catch {set fd [eval git_read --nice $cmd]} err]} {
 299                set diff_active 0
 300                unlock_index
 301                ui_status [mc "Unable to display %s" [escape_path $path]]
 302                error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
 303                return
 304        }
 305        set ::current_diff_inheader 1
 307        fconfigure $fd \
 308                -blocking 0 \
 309                -encoding [get_path_encoding $path] \
 310                -translation lf
 311        fileevent $fd readable [list read_diff $fd $cont_info]
 312}
 313proc read_diff {fd cont_info} {
 315        global ui_diff diff_active
 316        global is_3way_diff is_conflict_diff current_diff_header
 317        global current_diff_queue
 318        global diff_empty_count
 319        $ui_diff conf -state normal
 321        while {[gets $fd line] >= 0} {
 322                # -- Cleanup uninteresting diff header lines.
 323                #
 324                if {$::current_diff_inheader} {
 325                        if {   [string match {diff --git *}      $line]
 326                            || [string match {diff --cc *}       $line]
 327                            || [string match {diff --combined *} $line]
 328                            || [string match {--- *}             $line]
 329                            || [string match {+++ *}             $line]} {
 330                                append current_diff_header $line "\n"
 331                                continue
 332                        }
 333                }
 334                if {[string match {index *} $line]} continue
 335                if {$line eq {deleted file mode 120000}} {
 336                        set line "deleted symlink"
 337                }
 338                set ::current_diff_inheader 0
 339                # -- Automatically detect if this is a 3 way diff.
 341                #
 342                if {[string match {@@@ *} $line]} {set is_3way_diff 1}
 343                if {[string match {mode *} $line]
 345                        || [string match {new file *} $line]
 346                        || [regexp {^(old|new) mode *} $line]
 347                        || [string match {deleted file *} $line]
 348                        || [string match {deleted symlink} $line]
 349                        || [string match {Binary files * and * differ} $line]
 350                        || $line eq {\ No newline at end of file}
 351                        || [regexp {^\* Unmerged path } $line]} {
 352                        set tags {}
 353                } elseif {$is_3way_diff} {
 354                        set op [string range $line 0 1]
 355                        switch -- $op {
 356                        {  } {set tags {}}
 357                        {@@} {set tags d_@}
 358                        { +} {set tags d_s+}
 359                        { -} {set tags d_s-}
 360                        {+ } {set tags d_+s}
 361                        {- } {set tags d_-s}
 362                        {--} {set tags d_--}
 363                        {++} {
 364                                if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
 365                                        set is_conflict_diff 1
 366                                        set line [string replace $line 0 1 {  }]
 367                                        set tags d$op
 368                                } else {
 369                                        set tags d_++
 370                                }
 371                        }
 372                        default {
 373                                puts "error: Unhandled 3 way diff marker: {$op}"
 374                                set tags {}
 375                        }
 376                        }
 377                } else {
 378                        set op [string index $line 0]
 379                        switch -- $op {
 380                        { } {set tags {}}
 381                        {@} {set tags d_@}
 382                        {-} {set tags d_-}
 383                        {+} {
 384                                if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
 385                                        set is_conflict_diff 1
 386                                        set tags d$op
 387                                } else {
 388                                        set tags d_+
 389                                }
 390                        }
 391                        default {
 392                                puts "error: Unhandled 2 way diff marker: {$op}"
 393                                set tags {}
 394                        }
 395                        }
 396                }
 397                $ui_diff insert end $line $tags
 398                if {[string index $line end] eq "\r"} {
 399                        $ui_diff tag add d_cr {end - 2c}
 400                }
 401                $ui_diff insert end "\n" $tags
 402        }
 403        $ui_diff conf -state disabled
 404        if {[eof $fd]} {
 406                close $fd
 407                if {$current_diff_queue ne {}} {
 409                        advance_diff_queue $cont_info
 410                        return
 411                }
 412                set diff_active 0
 414                unlock_index
 415                set scroll_pos [lindex $cont_info 0]
 416                if {$scroll_pos ne {}} {
 417                        update
 418                        $ui_diff yview moveto $scroll_pos
 419                }
 420                ui_ready
 421                if {[$ui_diff index end] eq {2.0}} {
 423                        handle_empty_diff
 424                } else {
 425                        set diff_empty_count 0
 426                }
 427                set callback [lindex $cont_info 1]
 429                if {$callback ne {}} {
 430                        eval $callback
 431                }
 432        }
 433}
 434proc apply_hunk {x y} {
 436        global current_diff_path current_diff_header current_diff_side
 437        global ui_diff ui_index file_states
 438        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 440        if {![lock_index apply_hunk]} return
 441        set apply_cmd {apply --cached --whitespace=nowarn}
 443        set mi [lindex $file_states($current_diff_path) 0]
 444        if {$current_diff_side eq $ui_index} {
 445                set failed_msg [mc "Failed to unstage selected hunk."]
 446                lappend apply_cmd --reverse
 447                if {[string index $mi 0] ne {M}} {
 448                        unlock_index
 449                        return
 450                }
 451        } else {
 452                set failed_msg [mc "Failed to stage selected hunk."]
 453                if {[string index $mi 1] ne {M}} {
 454                        unlock_index
 455                        return
 456                }
 457        }
 458        set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
 460        set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
 461        if {$s_lno eq {}} {
 462                unlock_index
 463                return
 464        }
 465        set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
 467        if {$e_lno eq {}} {
 468                set e_lno end
 469        }
 470        if {[catch {
 472                set enc [get_path_encoding $current_diff_path]
 473                set p [eval git_write $apply_cmd]
 474                fconfigure $p -translation binary -encoding $enc
 475                puts -nonewline $p $current_diff_header
 476                puts -nonewline $p [$ui_diff get $s_lno $e_lno]
 477                close $p} err]} {
 478                error_popup [append $failed_msg "\n\n$err"]
 479                unlock_index
 480                return
 481        }
 482        $ui_diff conf -state normal
 484        $ui_diff delete $s_lno $e_lno
 485        $ui_diff conf -state disabled
 486        if {[$ui_diff get 1.0 end] eq "\n"} {
 488                set o _
 489        } else {
 490                set o ?
 491        }
 492        if {$current_diff_side eq $ui_index} {
 494                set mi ${o}M
 495        } elseif {[string index $mi 0] eq {_}} {
 496                set mi M$o
 497        } else {
 498                set mi ?$o
 499        }
 500        unlock_index
 501        display_file $current_diff_path $mi
 502        # This should trigger shift to the next changed file
 503        if {$o eq {_}} {
 504                reshow_diff
 505        }
 506}
 507proc apply_line {x y} {
 509        global current_diff_path current_diff_header current_diff_side
 510        global ui_diff ui_index file_states
 511        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 513        if {![lock_index apply_hunk]} return
 514        set apply_cmd {apply --cached --whitespace=nowarn}
 516        set mi [lindex $file_states($current_diff_path) 0]
 517        if {$current_diff_side eq $ui_index} {
 518                set failed_msg [mc "Failed to unstage selected line."]
 519                set to_context {+}
 520                lappend apply_cmd --reverse
 521                if {[string index $mi 0] ne {M}} {
 522                        unlock_index
 523                        return
 524                }
 525        } else {
 526                set failed_msg [mc "Failed to stage selected line."]
 527                set to_context {-}
 528                if {[string index $mi 1] ne {M}} {
 529                        unlock_index
 530                        return
 531                }
 532        }
 533        set the_l [$ui_diff index @$x,$y]
 535        # operate only on change lines
 537        set c1 [$ui_diff get "$the_l linestart"]
 538        if {$c1 ne {+} && $c1 ne {-}} {
 539                unlock_index
 540                return
 541        }
 542        set sign $c1
 543        set i_l [$ui_diff search -backwards -regexp ^@@ $the_l 0.0]
 545        if {$i_l eq {}} {
 546                unlock_index
 547                return
 548        }
 549        # $i_l is now at the beginning of a line
 550        # pick start line number from hunk header
 552        set hh [$ui_diff get $i_l "$i_l + 1 lines"]
 553        set hh [lindex [split $hh ,] 0]
 554        set hln [lindex [split $hh -] 1]
 555        # There is a special situation to take care of. Consider this hunk:
 557        #
 558        #    @@ -10,4 +10,4 @@
 559        #     context before
 560        #    -old 1
 561        #    -old 2
 562        #    +new 1
 563        #    +new 2
 564        #     context after
 565        #
 566        # We used to keep the context lines in the order they appear in the
 567        # hunk. But then it is not possible to correctly stage only
 568        # "-old 1" and "+new 1" - it would result in this staged text:
 569        #
 570        #    context before
 571        #    old 2
 572        #    new 1
 573        #    context after
 574        #
 575        # (By symmetry it is not possible to *un*stage "old 2" and "new 2".)
 576        #
 577        # We resolve the problem by introducing an asymmetry, namely, when
 578        # a "+" line is *staged*, it is moved in front of the context lines
 579        # that are generated from the "-" lines that are immediately before
 580        # the "+" block. That is, we construct this patch:
 581        #
 582        #    @@ -10,4 +10,5 @@
 583        #     context before
 584        #    +new 1
 585        #     old 1
 586        #     old 2
 587        #     context after
 588        #
 589        # But we do *not* treat "-" lines that are *un*staged in a special
 590        # way.
 591        #
 592        # With this asymmetry it is possible to stage the change
 593        # "old 1" -> "new 1" directly, and to stage the change
 594        # "old 2" -> "new 2" by first staging the entire hunk and
 595        # then unstaging the change "old 1" -> "new 1".
 596        # This is non-empty if and only if we are _staging_ changes;
 598        # then it accumulates the consecutive "-" lines (after converting
 599        # them to context lines) in order to be moved after the "+" change
 600        # line.
 601        set pre_context {}
 602        set n 0
 604        set i_l [$ui_diff index "$i_l + 1 lines"]
 605        set patch {}
 606        while {[$ui_diff compare $i_l < "end - 1 chars"] &&
 607               [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
 608                set next_l [$ui_diff index "$i_l + 1 lines"]
 609                set c1 [$ui_diff get $i_l]
 610                if {[$ui_diff compare $i_l <= $the_l] &&
 611                    [$ui_diff compare $the_l < $next_l]} {
 612                        # the line to stage/unstage
 613                        set ln [$ui_diff get $i_l $next_l]
 614                        if {$c1 eq {-}} {
 615                                set n [expr $n+1]
 616                                set patch "$patch$pre_context$ln"
 617                        } else {
 618                                set patch "$patch$ln$pre_context"
 619                        }
 620                        set pre_context {}
 621                } elseif {$c1 ne {-} && $c1 ne {+}} {
 622                        # context line
 623                        set ln [$ui_diff get $i_l $next_l]
 624                        set patch "$patch$pre_context$ln"
 625                        set n [expr $n+1]
 626                        set pre_context {}
 627                } elseif {$c1 eq $to_context} {
 628                        # turn change line into context line
 629                        set ln [$ui_diff get "$i_l + 1 chars" $next_l]
 630                        if {$c1 eq {-}} {
 631                                set pre_context "$pre_context $ln"
 632                        } else {
 633                                set patch "$patch $ln"
 634                        }
 635                        set n [expr $n+1]
 636                }
 637                set i_l $next_l
 638        }
 639        set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch"
 640        if {[catch {
 642                set enc [get_path_encoding $current_diff_path]
 643                set p [eval git_write $apply_cmd]
 644                fconfigure $p -translation binary -encoding $enc
 645                puts -nonewline $p $current_diff_header
 646                puts -nonewline $p $patch
 647                close $p} err]} {
 648                error_popup [append $failed_msg "\n\n$err"]
 649        }
 650        unlock_index
 652}