git-gui / lib / diff.tclon commit diffcore-rename: cache file deltas (eede7b7)
   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 "No differences detected.
  43
  44[short_path $path] 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."
  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 "Loading diff of [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 "Unable to display [escape_path $path]"
 115                        error_popup "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 "* Git Repository (subproject)\n" d_@
 121                } elseif {![catch {set type [exec file $path]}]} {
 122                        set n [string length $path]
 123                        if {[string equal -length $n $path $type]} {
 124                                set type [string range $type $n end]
 125                                regsub {^:?\s*} $type {} type
 126                        }
 127                        $ui_diff insert end "* $type\n" d_@
 128                }
 129                if {[string first "\0" $content] != -1} {
 130                        $ui_diff insert end \
 131                                "* Binary file (not showing content)." \
 132                                d_@
 133                } else {
 134                        if {$sz > $max_sz} {
 135                                $ui_diff insert end \
 136"* Untracked file is $sz bytes.
 137* Showing only first $max_sz bytes.
 138" d_@
 139                        }
 140                        $ui_diff insert end $content
 141                        if {$sz > $max_sz} {
 142                                $ui_diff insert end "
 143* Untracked file clipped here by [appname].
 144* To see the entire file, use an external editor.
 145" d_@
 146                        }
 147                }
 148                $ui_diff conf -state disabled
 149                set diff_active 0
 150                unlock_index
 151                ui_ready
 152                return
 153        }
 154
 155        set cmd [list]
 156        if {$w eq $ui_index} {
 157                lappend cmd diff-index
 158                lappend cmd --cached
 159        } elseif {$w eq $ui_workdir} {
 160                if {[string index $m 0] eq {U}} {
 161                        lappend cmd diff
 162                } else {
 163                        lappend cmd diff-files
 164                }
 165        }
 166
 167        lappend cmd -p
 168        lappend cmd --no-color
 169        if {$repo_config(gui.diffcontext) >= 0} {
 170                lappend cmd "-U$repo_config(gui.diffcontext)"
 171        }
 172        if {$w eq $ui_index} {
 173                lappend cmd [PARENT]
 174        }
 175        lappend cmd --
 176        lappend cmd $path
 177
 178        if {[catch {set fd [eval git_read --nice $cmd]} err]} {
 179                set diff_active 0
 180                unlock_index
 181                ui_status "Unable to display [escape_path $path]"
 182                error_popup "Error loading diff:\n\n$err"
 183                return
 184        }
 185
 186        fconfigure $fd \
 187                -blocking 0 \
 188                -encoding binary \
 189                -translation binary
 190        fileevent $fd readable [list read_diff $fd]
 191}
 192
 193proc read_diff {fd} {
 194        global ui_diff diff_active
 195        global is_3way_diff current_diff_header
 196
 197        $ui_diff conf -state normal
 198        while {[gets $fd line] >= 0} {
 199                # -- Cleanup uninteresting diff header lines.
 200                #
 201                if {   [string match {diff --git *}      $line]
 202                        || [string match {diff --cc *}       $line]
 203                        || [string match {diff --combined *} $line]
 204                        || [string match {--- *}             $line]
 205                        || [string match {+++ *}             $line]} {
 206                        append current_diff_header $line "\n"
 207                        continue
 208                }
 209                if {[string match {index *} $line]} continue
 210                if {$line eq {deleted file mode 120000}} {
 211                        set line "deleted symlink"
 212                }
 213
 214                # -- Automatically detect if this is a 3 way diff.
 215                #
 216                if {[string match {@@@ *} $line]} {set is_3way_diff 1}
 217
 218                if {[string match {mode *} $line]
 219                        || [string match {new file *} $line]
 220                        || [string match {deleted file *} $line]
 221                        || [string match {deleted symlink} $line]
 222                        || [string match {Binary files * and * differ} $line]
 223                        || $line eq {\ No newline at end of file}
 224                        || [regexp {^\* Unmerged path } $line]} {
 225                        set tags {}
 226                } elseif {$is_3way_diff} {
 227                        set op [string range $line 0 1]
 228                        switch -- $op {
 229                        {  } {set tags {}}
 230                        {@@} {set tags d_@}
 231                        { +} {set tags d_s+}
 232                        { -} {set tags d_s-}
 233                        {+ } {set tags d_+s}
 234                        {- } {set tags d_-s}
 235                        {--} {set tags d_--}
 236                        {++} {
 237                                if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
 238                                        set line [string replace $line 0 1 {  }]
 239                                        set tags d$op
 240                                } else {
 241                                        set tags d_++
 242                                }
 243                        }
 244                        default {
 245                                puts "error: Unhandled 3 way diff marker: {$op}"
 246                                set tags {}
 247                        }
 248                        }
 249                } else {
 250                        set op [string index $line 0]
 251                        switch -- $op {
 252                        { } {set tags {}}
 253                        {@} {set tags d_@}
 254                        {-} {set tags d_-}
 255                        {+} {
 256                                if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
 257                                        set line [string replace $line 0 0 { }]
 258                                        set tags d$op
 259                                } else {
 260                                        set tags d_+
 261                                }
 262                        }
 263                        default {
 264                                puts "error: Unhandled 2 way diff marker: {$op}"
 265                                set tags {}
 266                        }
 267                        }
 268                }
 269                $ui_diff insert end $line $tags
 270                if {[string index $line end] eq "\r"} {
 271                        $ui_diff tag add d_cr {end - 2c}
 272                }
 273                $ui_diff insert end "\n" $tags
 274        }
 275        $ui_diff conf -state disabled
 276
 277        if {[eof $fd]} {
 278                close $fd
 279                set diff_active 0
 280                unlock_index
 281                ui_ready
 282
 283                if {[$ui_diff index end] eq {2.0}} {
 284                        handle_empty_diff
 285                }
 286        }
 287}
 288
 289proc apply_hunk {x y} {
 290        global current_diff_path current_diff_header current_diff_side
 291        global ui_diff ui_index file_states
 292
 293        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 294        if {![lock_index apply_hunk]} return
 295
 296        set apply_cmd {apply --cached --whitespace=nowarn}
 297        set mi [lindex $file_states($current_diff_path) 0]
 298        if {$current_diff_side eq $ui_index} {
 299                set mode unstage
 300                lappend apply_cmd --reverse
 301                if {[string index $mi 0] ne {M}} {
 302                        unlock_index
 303                        return
 304                }
 305        } else {
 306                set mode stage
 307                if {[string index $mi 1] ne {M}} {
 308                        unlock_index
 309                        return
 310                }
 311        }
 312
 313        set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
 314        set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
 315        if {$s_lno eq {}} {
 316                unlock_index
 317                return
 318        }
 319
 320        set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
 321        if {$e_lno eq {}} {
 322                set e_lno end
 323        }
 324
 325        if {[catch {
 326                set p [eval git_write $apply_cmd]
 327                fconfigure $p -translation binary -encoding binary
 328                puts -nonewline $p $current_diff_header
 329                puts -nonewline $p [$ui_diff get $s_lno $e_lno]
 330                close $p} err]} {
 331                error_popup "Failed to $mode selected hunk.\n\n$err"
 332                unlock_index
 333                return
 334        }
 335
 336        $ui_diff conf -state normal
 337        $ui_diff delete $s_lno $e_lno
 338        $ui_diff conf -state disabled
 339
 340        if {[$ui_diff get 1.0 end] eq "\n"} {
 341                set o _
 342        } else {
 343                set o ?
 344        }
 345
 346        if {$current_diff_side eq $ui_index} {
 347                set mi ${o}M
 348        } elseif {[string index $mi 0] eq {_}} {
 349                set mi M$o
 350        } else {
 351                set mi ?$o
 352        }
 353        unlock_index
 354        display_file $current_diff_path $mi
 355        if {$o eq {_}} {
 356                clear_diff
 357        }
 358}