git-gui / lib / diff.tclon commit Merge branch 'maint' (7e4ad90)
   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) >= 0} {
 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        fconfigure $fd \
 196                -blocking 0 \
 197                -encoding binary \
 198                -translation binary
 199        fileevent $fd readable [list read_diff $fd $scroll_pos]
 200}
 201
 202proc read_diff {fd scroll_pos} {
 203        global ui_diff diff_active
 204        global is_3way_diff current_diff_header
 205
 206        $ui_diff conf -state normal
 207        while {[gets $fd line] >= 0} {
 208                # -- Cleanup uninteresting diff header lines.
 209                #
 210                if {   [string match {diff --git *}      $line]
 211                        || [string match {diff --cc *}       $line]
 212                        || [string match {diff --combined *} $line]
 213                        || [string match {--- *}             $line]
 214                        || [string match {+++ *}             $line]} {
 215                        append current_diff_header $line "\n"
 216                        continue
 217                }
 218                if {[string match {index *} $line]} continue
 219                if {$line eq {deleted file mode 120000}} {
 220                        set line "deleted symlink"
 221                }
 222
 223                # -- Automatically detect if this is a 3 way diff.
 224                #
 225                if {[string match {@@@ *} $line]} {set is_3way_diff 1}
 226
 227                if {[string match {mode *} $line]
 228                        || [string match {new file *} $line]
 229                        || [regexp {^(old|new) mode *} $line]
 230                        || [string match {deleted file *} $line]
 231                        || [string match {deleted symlink} $line]
 232                        || [string match {Binary files * and * differ} $line]
 233                        || $line eq {\ No newline at end of file}
 234                        || [regexp {^\* Unmerged path } $line]} {
 235                        set tags {}
 236                } elseif {$is_3way_diff} {
 237                        set op [string range $line 0 1]
 238                        switch -- $op {
 239                        {  } {set tags {}}
 240                        {@@} {set tags d_@}
 241                        { +} {set tags d_s+}
 242                        { -} {set tags d_s-}
 243                        {+ } {set tags d_+s}
 244                        {- } {set tags d_-s}
 245                        {--} {set tags d_--}
 246                        {++} {
 247                                if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
 248                                        set line [string replace $line 0 1 {  }]
 249                                        set tags d$op
 250                                } else {
 251                                        set tags d_++
 252                                }
 253                        }
 254                        default {
 255                                puts "error: Unhandled 3 way diff marker: {$op}"
 256                                set tags {}
 257                        }
 258                        }
 259                } else {
 260                        set op [string index $line 0]
 261                        switch -- $op {
 262                        { } {set tags {}}
 263                        {@} {set tags d_@}
 264                        {-} {set tags d_-}
 265                        {+} {
 266                                if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
 267                                        set line [string replace $line 0 0 { }]
 268                                        set tags d$op
 269                                } else {
 270                                        set tags d_+
 271                                }
 272                        }
 273                        default {
 274                                puts "error: Unhandled 2 way diff marker: {$op}"
 275                                set tags {}
 276                        }
 277                        }
 278                }
 279                $ui_diff insert end $line $tags
 280                if {[string index $line end] eq "\r"} {
 281                        $ui_diff tag add d_cr {end - 2c}
 282                }
 283                $ui_diff insert end "\n" $tags
 284        }
 285        $ui_diff conf -state disabled
 286
 287        if {[eof $fd]} {
 288                close $fd
 289                set diff_active 0
 290                unlock_index
 291                if {$scroll_pos ne {}} {
 292                        update
 293                        $ui_diff yview moveto $scroll_pos
 294                }
 295                ui_ready
 296
 297                if {[$ui_diff index end] eq {2.0}} {
 298                        handle_empty_diff
 299                }
 300        }
 301}
 302
 303proc apply_hunk {x y} {
 304        global current_diff_path current_diff_header current_diff_side
 305        global ui_diff ui_index file_states
 306
 307        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 308        if {![lock_index apply_hunk]} return
 309
 310        set apply_cmd {apply --cached --whitespace=nowarn}
 311        set mi [lindex $file_states($current_diff_path) 0]
 312        if {$current_diff_side eq $ui_index} {
 313                set failed_msg [mc "Failed to unstage selected hunk."]
 314                lappend apply_cmd --reverse
 315                if {[string index $mi 0] ne {M}} {
 316                        unlock_index
 317                        return
 318                }
 319        } else {
 320                set failed_msg [mc "Failed to stage selected hunk."]
 321                if {[string index $mi 1] ne {M}} {
 322                        unlock_index
 323                        return
 324                }
 325        }
 326
 327        set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
 328        set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
 329        if {$s_lno eq {}} {
 330                unlock_index
 331                return
 332        }
 333
 334        set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
 335        if {$e_lno eq {}} {
 336                set e_lno end
 337        }
 338
 339        if {[catch {
 340                set p [eval git_write $apply_cmd]
 341                fconfigure $p -translation binary -encoding binary
 342                puts -nonewline $p $current_diff_header
 343                puts -nonewline $p [$ui_diff get $s_lno $e_lno]
 344                close $p} err]} {
 345                error_popup [append $failed_msg "\n\n$err"]
 346                unlock_index
 347                return
 348        }
 349
 350        $ui_diff conf -state normal
 351        $ui_diff delete $s_lno $e_lno
 352        $ui_diff conf -state disabled
 353
 354        if {[$ui_diff get 1.0 end] eq "\n"} {
 355                set o _
 356        } else {
 357                set o ?
 358        }
 359
 360        if {$current_diff_side eq $ui_index} {
 361                set mi ${o}M
 362        } elseif {[string index $mi 0] eq {_}} {
 363                set mi M$o
 364        } else {
 365                set mi ?$o
 366        }
 367        unlock_index
 368        display_file $current_diff_path $mi
 369        if {$o eq {_}} {
 370                clear_diff
 371        } else {
 372                set current_diff_path $current_diff_path
 373        }
 374}
 375
 376proc apply_line {x y} {
 377        global current_diff_path current_diff_header current_diff_side
 378        global ui_diff ui_index file_states
 379
 380        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 381        if {![lock_index apply_hunk]} return
 382
 383        set apply_cmd {apply --cached --whitespace=nowarn}
 384        set mi [lindex $file_states($current_diff_path) 0]
 385        if {$current_diff_side eq $ui_index} {
 386                set failed_msg [mc "Failed to unstage selected line."]
 387                set to_context {+}
 388                lappend apply_cmd --reverse
 389                if {[string index $mi 0] ne {M}} {
 390                        unlock_index
 391                        return
 392                }
 393        } else {
 394                set failed_msg [mc "Failed to stage selected line."]
 395                set to_context {-}
 396                if {[string index $mi 1] ne {M}} {
 397                        unlock_index
 398                        return
 399                }
 400        }
 401
 402        set the_l [$ui_diff index @$x,$y]
 403
 404        # operate only on change lines
 405        set c1 [$ui_diff get "$the_l linestart"]
 406        if {$c1 ne {+} && $c1 ne {-}} {
 407                unlock_index
 408                return
 409        }
 410        set sign $c1
 411
 412        set i_l [$ui_diff search -backwards -regexp ^@@ $the_l 0.0]
 413        if {$i_l eq {}} {
 414                unlock_index
 415                return
 416        }
 417        # $i_l is now at the beginning of a line
 418
 419        # pick start line number from hunk header
 420        set hh [$ui_diff get $i_l "$i_l + 1 lines"]
 421        set hh [lindex [split $hh ,] 0]
 422        set hln [lindex [split $hh -] 1]
 423
 424        # There is a special situation to take care of. Consider this hunk:
 425        #
 426        #    @@ -10,4 +10,4 @@
 427        #     context before
 428        #    -old 1
 429        #    -old 2
 430        #    +new 1
 431        #    +new 2
 432        #     context after
 433        #
 434        # We used to keep the context lines in the order they appear in the
 435        # hunk. But then it is not possible to correctly stage only
 436        # "-old 1" and "+new 1" - it would result in this staged text:
 437        #
 438        #    context before
 439        #    old 2
 440        #    new 1
 441        #    context after
 442        #
 443        # (By symmetry it is not possible to *un*stage "old 2" and "new 2".)
 444        #
 445        # We resolve the problem by introducing an asymmetry, namely, when
 446        # a "+" line is *staged*, it is moved in front of the context lines
 447        # that are generated from the "-" lines that are immediately before
 448        # the "+" block. That is, we construct this patch:
 449        #
 450        #    @@ -10,4 +10,5 @@
 451        #     context before
 452        #    +new 1
 453        #     old 1
 454        #     old 2
 455        #     context after
 456        #
 457        # But we do *not* treat "-" lines that are *un*staged in a special
 458        # way.
 459        #
 460        # With this asymmetry it is possible to stage the change
 461        # "old 1" -> "new 1" directly, and to stage the change
 462        # "old 2" -> "new 2" by first staging the entire hunk and
 463        # then unstaging the change "old 1" -> "new 1".
 464
 465        # This is non-empty if and only if we are _staging_ changes;
 466        # then it accumulates the consecutive "-" lines (after converting
 467        # them to context lines) in order to be moved after the "+" change
 468        # line.
 469        set pre_context {}
 470
 471        set n 0
 472        set i_l [$ui_diff index "$i_l + 1 lines"]
 473        set patch {}
 474        while {[$ui_diff compare $i_l < "end - 1 chars"] &&
 475               [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
 476                set next_l [$ui_diff index "$i_l + 1 lines"]
 477                set c1 [$ui_diff get $i_l]
 478                if {[$ui_diff compare $i_l <= $the_l] &&
 479                    [$ui_diff compare $the_l < $next_l]} {
 480                        # the line to stage/unstage
 481                        set ln [$ui_diff get $i_l $next_l]
 482                        if {$c1 eq {-}} {
 483                                set n [expr $n+1]
 484                                set patch "$patch$pre_context$ln"
 485                        } else {
 486                                set patch "$patch$ln$pre_context"
 487                        }
 488                        set pre_context {}
 489                } elseif {$c1 ne {-} && $c1 ne {+}} {
 490                        # context line
 491                        set ln [$ui_diff get $i_l $next_l]
 492                        set patch "$patch$pre_context$ln"
 493                        set n [expr $n+1]
 494                        set pre_context {}
 495                } elseif {$c1 eq $to_context} {
 496                        # turn change line into context line
 497                        set ln [$ui_diff get "$i_l + 1 chars" $next_l]
 498                        if {$c1 eq {-}} {
 499                                set pre_context "$pre_context $ln"
 500                        } else {
 501                                set patch "$patch $ln"
 502                        }
 503                        set n [expr $n+1]
 504                }
 505                set i_l $next_l
 506        }
 507        set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch"
 508
 509        if {[catch {
 510                set p [eval git_write $apply_cmd]
 511                fconfigure $p -translation binary -encoding binary
 512                puts -nonewline $p $current_diff_header
 513                puts -nonewline $p $patch
 514                close $p} err]} {
 515                error_popup [append $failed_msg "\n\n$err"]
 516        }
 517
 518        unlock_index
 519}