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