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