git-gui / lib / diff.tclon commit Merge branch 'maint' of git://repo.or.cz/git-gui into maint (faf1dc7)
   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                || [catch {set s $file_states($p)}]
  29                || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} {
  30                clear_diff
  31        } else {
  32                set save_pos [lindex [$ui_diff yview] 0]
  33                show_diff $p $current_diff_side {} $save_pos
  34        }
  35}
  36
  37proc handle_empty_diff {} {
  38        global current_diff_path file_states file_lists
  39
  40        set path $current_diff_path
  41        set s $file_states($path)
  42        if {[lindex $s 0] ne {_M}} return
  43
  44        info_popup [mc "No differences detected.
  45
  46%s has no changes.
  47
  48The modification date of this file was updated by another application, but the content within the file was not changed.
  49
  50A rescan will be automatically started to find other files which may have the same state." [short_path $path]]
  51
  52        clear_diff
  53        display_file $path __
  54        rescan ui_ready 0
  55}
  56
  57proc show_diff {path w {lno {}} {scroll_pos {}}} {
  58        global file_states file_lists
  59        global is_3way_diff diff_active repo_config
  60        global ui_diff ui_index ui_workdir
  61        global current_diff_path current_diff_side current_diff_header
  62
  63        if {$diff_active || ![lock_index read]} return
  64
  65        clear_diff
  66        if {$lno == {}} {
  67                set lno [lsearch -sorted -exact $file_lists($w) $path]
  68                if {$lno >= 0} {
  69                        incr lno
  70                }
  71        }
  72        if {$lno >= 1} {
  73                $w tag add in_diff $lno.0 [expr {$lno + 1}].0
  74        }
  75
  76        set s $file_states($path)
  77        set m [lindex $s 0]
  78        set is_3way_diff 0
  79        set diff_active 1
  80        set current_diff_path $path
  81        set current_diff_side $w
  82        set current_diff_header {}
  83        ui_status [mc "Loading diff of %s..." [escape_path $path]]
  84
  85        # - Git won't give us the diff, there's nothing to compare to!
  86        #
  87        if {$m eq {_O}} {
  88                set max_sz [expr {128 * 1024}]
  89                set type unknown
  90                if {[catch {
  91                                set type [file type $path]
  92                                switch -- $type {
  93                                directory {
  94                                        set type submodule
  95                                        set content {}
  96                                        set sz 0
  97                                }
  98                                link {
  99                                        set content [file readlink $path]
 100                                        set sz [string length $content]
 101                                }
 102                                file {
 103                                        set fd [open $path r]
 104                                        fconfigure $fd -eofchar {}
 105                                        set content [read $fd $max_sz]
 106                                        close $fd
 107                                        set sz [file size $path]
 108                                }
 109                                default {
 110                                        error "'$type' not supported"
 111                                }
 112                                }
 113                        } err ]} {
 114                        set diff_active 0
 115                        unlock_index
 116                        ui_status [mc "Unable to display %s" [escape_path $path]]
 117                        error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
 118                        return
 119                }
 120                $ui_diff conf -state normal
 121                if {$type eq {submodule}} {
 122                        $ui_diff insert end [append \
 123                                "* " \
 124                                [mc "Git Repository (subproject)"] \
 125                                "\n"] d_@
 126                } elseif {![catch {set type [exec file $path]}]} {
 127                        set n [string length $path]
 128                        if {[string equal -length $n $path $type]} {
 129                                set type [string range $type $n end]
 130                                regsub {^:?\s*} $type {} type
 131                        }
 132                        $ui_diff insert end "* $type\n" d_@
 133                }
 134                if {[string first "\0" $content] != -1} {
 135                        $ui_diff insert end \
 136                                [mc "* Binary file (not showing content)."] \
 137                                d_@
 138                } else {
 139                        if {$sz > $max_sz} {
 140                                $ui_diff insert end \
 141"* Untracked file is $sz bytes.
 142* Showing only first $max_sz bytes.
 143" d_@
 144                        }
 145                        $ui_diff insert end $content
 146                        if {$sz > $max_sz} {
 147                                $ui_diff insert end "
 148* Untracked file clipped here by [appname].
 149* To see the entire file, use an external editor.
 150" d_@
 151                        }
 152                }
 153                $ui_diff conf -state disabled
 154                set diff_active 0
 155                unlock_index
 156                if {$scroll_pos ne {}} {
 157                        update
 158                        $ui_diff yview moveto $scroll_pos
 159                }
 160                ui_ready
 161                return
 162        }
 163
 164        set cmd [list]
 165        if {$w eq $ui_index} {
 166                lappend cmd diff-index
 167                lappend cmd --cached
 168        } elseif {$w eq $ui_workdir} {
 169                if {[string index $m 0] eq {U}} {
 170                        lappend cmd diff
 171                } else {
 172                        lappend cmd diff-files
 173                }
 174        }
 175
 176        lappend cmd -p
 177        lappend cmd --no-color
 178        if {$repo_config(gui.diffcontext) >= 1} {
 179                lappend cmd "-U$repo_config(gui.diffcontext)"
 180        }
 181        if {$w eq $ui_index} {
 182                lappend cmd [PARENT]
 183        }
 184        lappend cmd --
 185        lappend cmd $path
 186
 187        if {[catch {set fd [eval git_read --nice $cmd]} err]} {
 188                set diff_active 0
 189                unlock_index
 190                ui_status [mc "Unable to display %s" [escape_path $path]]
 191                error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
 192                return
 193        }
 194
 195        set ::current_diff_inheader 1
 196        fconfigure $fd \
 197                -blocking 0 \
 198                -encoding binary \
 199                -translation binary
 200        fileevent $fd readable [list read_diff $fd $scroll_pos]
 201}
 202
 203proc read_diff {fd scroll_pos} {
 204        global ui_diff diff_active
 205        global is_3way_diff current_diff_header
 206
 207        $ui_diff conf -state normal
 208        while {[gets $fd line] >= 0} {
 209                # -- Cleanup uninteresting diff header lines.
 210                #
 211                if {$::current_diff_inheader} {
 212                        if {   [string match {diff --git *}      $line]
 213                            || [string match {diff --cc *}       $line]
 214                            || [string match {diff --combined *} $line]
 215                            || [string match {--- *}             $line]
 216                            || [string match {+++ *}             $line]} {
 217                                append current_diff_header $line "\n"
 218                                continue
 219                        }
 220                }
 221                if {[string match {index *} $line]} continue
 222                if {$line eq {deleted file mode 120000}} {
 223                        set line "deleted symlink"
 224                }
 225                set ::current_diff_inheader 0
 226
 227                # -- Automatically detect if this is a 3 way diff.
 228                #
 229                if {[string match {@@@ *} $line]} {set is_3way_diff 1}
 230
 231                if {[string match {mode *} $line]
 232                        || [string match {new file *} $line]
 233                        || [regexp {^(old|new) mode *} $line]
 234                        || [string match {deleted file *} $line]
 235                        || [string match {deleted symlink} $line]
 236                        || [string match {Binary files * and * differ} $line]
 237                        || $line eq {\ No newline at end of file}
 238                        || [regexp {^\* Unmerged path } $line]} {
 239                        set tags {}
 240                } elseif {$is_3way_diff} {
 241                        set op [string range $line 0 1]
 242                        switch -- $op {
 243                        {  } {set tags {}}
 244                        {@@} {set tags d_@}
 245                        { +} {set tags d_s+}
 246                        { -} {set tags d_s-}
 247                        {+ } {set tags d_+s}
 248                        {- } {set tags d_-s}
 249                        {--} {set tags d_--}
 250                        {++} {
 251                                if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
 252                                        set line [string replace $line 0 1 {  }]
 253                                        set tags d$op
 254                                } else {
 255                                        set tags d_++
 256                                }
 257                        }
 258                        default {
 259                                puts "error: Unhandled 3 way diff marker: {$op}"
 260                                set tags {}
 261                        }
 262                        }
 263                } else {
 264                        set op [string index $line 0]
 265                        switch -- $op {
 266                        { } {set tags {}}
 267                        {@} {set tags d_@}
 268                        {-} {set tags d_-}
 269                        {+} {
 270                                if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
 271                                        set line [string replace $line 0 0 { }]
 272                                        set tags d$op
 273                                } else {
 274                                        set tags d_+
 275                                }
 276                        }
 277                        default {
 278                                puts "error: Unhandled 2 way diff marker: {$op}"
 279                                set tags {}
 280                        }
 281                        }
 282                }
 283                $ui_diff insert end $line $tags
 284                if {[string index $line end] eq "\r"} {
 285                        $ui_diff tag add d_cr {end - 2c}
 286                }
 287                $ui_diff insert end "\n" $tags
 288        }
 289        $ui_diff conf -state disabled
 290
 291        if {[eof $fd]} {
 292                close $fd
 293                set diff_active 0
 294                unlock_index
 295                if {$scroll_pos ne {}} {
 296                        update
 297                        $ui_diff yview moveto $scroll_pos
 298                }
 299                ui_ready
 300
 301                if {[$ui_diff index end] eq {2.0}} {
 302                        handle_empty_diff
 303                }
 304        }
 305}
 306
 307proc apply_hunk {x y} {
 308        global current_diff_path current_diff_header current_diff_side
 309        global ui_diff ui_index file_states
 310
 311        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 312        if {![lock_index apply_hunk]} return
 313
 314        set apply_cmd {apply --cached --whitespace=nowarn}
 315        set mi [lindex $file_states($current_diff_path) 0]
 316        if {$current_diff_side eq $ui_index} {
 317                set failed_msg [mc "Failed to unstage selected hunk."]
 318                lappend apply_cmd --reverse
 319                if {[string index $mi 0] ne {M}} {
 320                        unlock_index
 321                        return
 322                }
 323        } else {
 324                set failed_msg [mc "Failed to stage selected hunk."]
 325                if {[string index $mi 1] ne {M}} {
 326                        unlock_index
 327                        return
 328                }
 329        }
 330
 331        set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
 332        set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
 333        if {$s_lno eq {}} {
 334                unlock_index
 335                return
 336        }
 337
 338        set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
 339        if {$e_lno eq {}} {
 340                set e_lno end
 341        }
 342
 343        if {[catch {
 344                set p [eval git_write $apply_cmd]
 345                fconfigure $p -translation binary -encoding binary
 346                puts -nonewline $p $current_diff_header
 347                puts -nonewline $p [$ui_diff get $s_lno $e_lno]
 348                close $p} err]} {
 349                error_popup [append $failed_msg "\n\n$err"]
 350                unlock_index
 351                return
 352        }
 353
 354        $ui_diff conf -state normal
 355        $ui_diff delete $s_lno $e_lno
 356        $ui_diff conf -state disabled
 357
 358        if {[$ui_diff get 1.0 end] eq "\n"} {
 359                set o _
 360        } else {
 361                set o ?
 362        }
 363
 364        if {$current_diff_side eq $ui_index} {
 365                set mi ${o}M
 366        } elseif {[string index $mi 0] eq {_}} {
 367                set mi M$o
 368        } else {
 369                set mi ?$o
 370        }
 371        unlock_index
 372        display_file $current_diff_path $mi
 373        if {$o eq {_}} {
 374                clear_diff
 375        } else {
 376                set current_diff_path $current_diff_path
 377        }
 378}
 379
 380proc apply_line {x y} {
 381        global current_diff_path current_diff_header current_diff_side
 382        global ui_diff ui_index file_states
 383
 384        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 385        if {![lock_index apply_hunk]} return
 386
 387        set apply_cmd {apply --cached --whitespace=nowarn}
 388        set mi [lindex $file_states($current_diff_path) 0]
 389        if {$current_diff_side eq $ui_index} {
 390                set failed_msg [mc "Failed to unstage selected line."]
 391                set to_context {+}
 392                lappend apply_cmd --reverse
 393                if {[string index $mi 0] ne {M}} {
 394                        unlock_index
 395                        return
 396                }
 397        } else {
 398                set failed_msg [mc "Failed to stage selected line."]
 399                set to_context {-}
 400                if {[string index $mi 1] ne {M}} {
 401                        unlock_index
 402                        return
 403                }
 404        }
 405
 406        set the_l [$ui_diff index @$x,$y]
 407
 408        # operate only on change lines
 409        set c1 [$ui_diff get "$the_l linestart"]
 410        if {$c1 ne {+} && $c1 ne {-}} {
 411                unlock_index
 412                return
 413        }
 414        set sign $c1
 415
 416        set i_l [$ui_diff search -backwards -regexp ^@@ $the_l 0.0]
 417        if {$i_l eq {}} {
 418                unlock_index
 419                return
 420        }
 421        # $i_l is now at the beginning of a line
 422
 423        # pick start line number from hunk header
 424        set hh [$ui_diff get $i_l "$i_l + 1 lines"]
 425        set hh [lindex [split $hh ,] 0]
 426        set hln [lindex [split $hh -] 1]
 427
 428        # There is a special situation to take care of. Consider this hunk:
 429        #
 430        #    @@ -10,4 +10,4 @@
 431        #     context before
 432        #    -old 1
 433        #    -old 2
 434        #    +new 1
 435        #    +new 2
 436        #     context after
 437        #
 438        # We used to keep the context lines in the order they appear in the
 439        # hunk. But then it is not possible to correctly stage only
 440        # "-old 1" and "+new 1" - it would result in this staged text:
 441        #
 442        #    context before
 443        #    old 2
 444        #    new 1
 445        #    context after
 446        #
 447        # (By symmetry it is not possible to *un*stage "old 2" and "new 2".)
 448        #
 449        # We resolve the problem by introducing an asymmetry, namely, when
 450        # a "+" line is *staged*, it is moved in front of the context lines
 451        # that are generated from the "-" lines that are immediately before
 452        # the "+" block. That is, we construct this patch:
 453        #
 454        #    @@ -10,4 +10,5 @@
 455        #     context before
 456        #    +new 1
 457        #     old 1
 458        #     old 2
 459        #     context after
 460        #
 461        # But we do *not* treat "-" lines that are *un*staged in a special
 462        # way.
 463        #
 464        # With this asymmetry it is possible to stage the change
 465        # "old 1" -> "new 1" directly, and to stage the change
 466        # "old 2" -> "new 2" by first staging the entire hunk and
 467        # then unstaging the change "old 1" -> "new 1".
 468
 469        # This is non-empty if and only if we are _staging_ changes;
 470        # then it accumulates the consecutive "-" lines (after converting
 471        # them to context lines) in order to be moved after the "+" change
 472        # line.
 473        set pre_context {}
 474
 475        set n 0
 476        set i_l [$ui_diff index "$i_l + 1 lines"]
 477        set patch {}
 478        while {[$ui_diff compare $i_l < "end - 1 chars"] &&
 479               [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
 480                set next_l [$ui_diff index "$i_l + 1 lines"]
 481                set c1 [$ui_diff get $i_l]
 482                if {[$ui_diff compare $i_l <= $the_l] &&
 483                    [$ui_diff compare $the_l < $next_l]} {
 484                        # the line to stage/unstage
 485                        set ln [$ui_diff get $i_l $next_l]
 486                        if {$c1 eq {-}} {
 487                                set n [expr $n+1]
 488                                set patch "$patch$pre_context$ln"
 489                        } else {
 490                                set patch "$patch$ln$pre_context"
 491                        }
 492                        set pre_context {}
 493                } elseif {$c1 ne {-} && $c1 ne {+}} {
 494                        # context line
 495                        set ln [$ui_diff get $i_l $next_l]
 496                        set patch "$patch$pre_context$ln"
 497                        set n [expr $n+1]
 498                        set pre_context {}
 499                } elseif {$c1 eq $to_context} {
 500                        # turn change line into context line
 501                        set ln [$ui_diff get "$i_l + 1 chars" $next_l]
 502                        if {$c1 eq {-}} {
 503                                set pre_context "$pre_context $ln"
 504                        } else {
 505                                set patch "$patch $ln"
 506                        }
 507                        set n [expr $n+1]
 508                }
 509                set i_l $next_l
 510        }
 511        set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch"
 512
 513        if {[catch {
 514                set p [eval git_write $apply_cmd]
 515                fconfigure $p -translation binary -encoding binary
 516                puts -nonewline $p $current_diff_header
 517                puts -nonewline $p $patch
 518                close $p} err]} {
 519                error_popup [append $failed_msg "\n\n$err"]
 520        }
 521
 522        unlock_index
 523}