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