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