lib / diff.tclon commit Merge branch 'maint' (c8dd7f6)
   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 "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 [append [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                                "* 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 "Unable to display [escape_path $path]"
 185                error_popup [append [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                        || [string match {deleted file *} $line]
 224                        || [string match {deleted symlink} $line]
 225                        || [string match {Binary files * and * differ} $line]
 226                        || $line eq {\ No newline at end of file}
 227                        || [regexp {^\* Unmerged path } $line]} {
 228                        set tags {}
 229                } elseif {$is_3way_diff} {
 230                        set op [string range $line 0 1]
 231                        switch -- $op {
 232                        {  } {set tags {}}
 233                        {@@} {set tags d_@}
 234                        { +} {set tags d_s+}
 235                        { -} {set tags d_s-}
 236                        {+ } {set tags d_+s}
 237                        {- } {set tags d_-s}
 238                        {--} {set tags d_--}
 239                        {++} {
 240                                if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
 241                                        set line [string replace $line 0 1 {  }]
 242                                        set tags d$op
 243                                } else {
 244                                        set tags d_++
 245                                }
 246                        }
 247                        default {
 248                                puts "error: Unhandled 3 way diff marker: {$op}"
 249                                set tags {}
 250                        }
 251                        }
 252                } else {
 253                        set op [string index $line 0]
 254                        switch -- $op {
 255                        { } {set tags {}}
 256                        {@} {set tags d_@}
 257                        {-} {set tags d_-}
 258                        {+} {
 259                                if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
 260                                        set line [string replace $line 0 0 { }]
 261                                        set tags d$op
 262                                } else {
 263                                        set tags d_+
 264                                }
 265                        }
 266                        default {
 267                                puts "error: Unhandled 2 way diff marker: {$op}"
 268                                set tags {}
 269                        }
 270                        }
 271                }
 272                $ui_diff insert end $line $tags
 273                if {[string index $line end] eq "\r"} {
 274                        $ui_diff tag add d_cr {end - 2c}
 275                }
 276                $ui_diff insert end "\n" $tags
 277        }
 278        $ui_diff conf -state disabled
 279
 280        if {[eof $fd]} {
 281                close $fd
 282                set diff_active 0
 283                unlock_index
 284                ui_ready
 285
 286                if {[$ui_diff index end] eq {2.0}} {
 287                        handle_empty_diff
 288                }
 289        }
 290}
 291
 292proc apply_hunk {x y} {
 293        global current_diff_path current_diff_header current_diff_side
 294        global ui_diff ui_index file_states
 295
 296        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 297        if {![lock_index apply_hunk]} return
 298
 299        set apply_cmd {apply --cached --whitespace=nowarn}
 300        set mi [lindex $file_states($current_diff_path) 0]
 301        if {$current_diff_side eq $ui_index} {
 302                set failed_msg [mc "Failed to unstage selected hunk."]
 303                lappend apply_cmd --reverse
 304                if {[string index $mi 0] ne {M}} {
 305                        unlock_index
 306                        return
 307                }
 308        } else {
 309                set failed_msg [mc "Failed to stage selected hunk."]
 310                if {[string index $mi 1] ne {M}} {
 311                        unlock_index
 312                        return
 313                }
 314        }
 315
 316        set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
 317        set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
 318        if {$s_lno eq {}} {
 319                unlock_index
 320                return
 321        }
 322
 323        set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
 324        if {$e_lno eq {}} {
 325                set e_lno end
 326        }
 327
 328        if {[catch {
 329                set p [eval git_write $apply_cmd]
 330                fconfigure $p -translation binary -encoding binary
 331                puts -nonewline $p $current_diff_header
 332                puts -nonewline $p [$ui_diff get $s_lno $e_lno]
 333                close $p} err]} {
 334                error_popup [append $failed_msg "\n\n$err"]
 335                unlock_index
 336                return
 337        }
 338
 339        $ui_diff conf -state normal
 340        $ui_diff delete $s_lno $e_lno
 341        $ui_diff conf -state disabled
 342
 343        if {[$ui_diff get 1.0 end] eq "\n"} {
 344                set o _
 345        } else {
 346                set o ?
 347        }
 348
 349        if {$current_diff_side eq $ui_index} {
 350                set mi ${o}M
 351        } elseif {[string index $mi 0] eq {_}} {
 352                set mi M$o
 353        } else {
 354                set mi ?$o
 355        }
 356        unlock_index
 357        display_file $current_diff_path $mi
 358        if {$o eq {_}} {
 359                clear_diff
 360        }
 361}