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