git-gui / lib / diff.tclon commit Merge branch 'ab/bundle' (ef567fe)
   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 [mc "Loading diff of %s..." [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 [mc "Unable to display %s" [escape_path $path]]
 115                        error_popup [strcat [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                                [mc "* 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 [mc "Unable to display %s" [escape_path $path]]
 185                error_popup [strcat [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                        || [regexp {^(old|new) mode *} $line]
 224                        || [string match {deleted file *} $line]
 225                        || [string match {deleted symlink} $line]
 226                        || [string match {Binary files * and * differ} $line]
 227                        || $line eq {\ No newline at end of file}
 228                        || [regexp {^\* Unmerged path } $line]} {
 229                        set tags {}
 230                } elseif {$is_3way_diff} {
 231                        set op [string range $line 0 1]
 232                        switch -- $op {
 233                        {  } {set tags {}}
 234                        {@@} {set tags d_@}
 235                        { +} {set tags d_s+}
 236                        { -} {set tags d_s-}
 237                        {+ } {set tags d_+s}
 238                        {- } {set tags d_-s}
 239                        {--} {set tags d_--}
 240                        {++} {
 241                                if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
 242                                        set line [string replace $line 0 1 {  }]
 243                                        set tags d$op
 244                                } else {
 245                                        set tags d_++
 246                                }
 247                        }
 248                        default {
 249                                puts "error: Unhandled 3 way diff marker: {$op}"
 250                                set tags {}
 251                        }
 252                        }
 253                } else {
 254                        set op [string index $line 0]
 255                        switch -- $op {
 256                        { } {set tags {}}
 257                        {@} {set tags d_@}
 258                        {-} {set tags d_-}
 259                        {+} {
 260                                if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
 261                                        set line [string replace $line 0 0 { }]
 262                                        set tags d$op
 263                                } else {
 264                                        set tags d_+
 265                                }
 266                        }
 267                        default {
 268                                puts "error: Unhandled 2 way diff marker: {$op}"
 269                                set tags {}
 270                        }
 271                        }
 272                }
 273                $ui_diff insert end $line $tags
 274                if {[string index $line end] eq "\r"} {
 275                        $ui_diff tag add d_cr {end - 2c}
 276                }
 277                $ui_diff insert end "\n" $tags
 278        }
 279        $ui_diff conf -state disabled
 280
 281        if {[eof $fd]} {
 282                close $fd
 283                set diff_active 0
 284                unlock_index
 285                ui_ready
 286
 287                if {[$ui_diff index end] eq {2.0}} {
 288                        handle_empty_diff
 289                }
 290        }
 291}
 292
 293proc apply_hunk {x y} {
 294        global current_diff_path current_diff_header current_diff_side
 295        global ui_diff ui_index file_states
 296
 297        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 298        if {![lock_index apply_hunk]} return
 299
 300        set apply_cmd {apply --cached --whitespace=nowarn}
 301        set mi [lindex $file_states($current_diff_path) 0]
 302        if {$current_diff_side eq $ui_index} {
 303                set failed_msg [mc "Failed to unstage selected hunk."]
 304                lappend apply_cmd --reverse
 305                if {[string index $mi 0] ne {M}} {
 306                        unlock_index
 307                        return
 308                }
 309        } else {
 310                set failed_msg [mc "Failed to stage selected hunk."]
 311                if {[string index $mi 1] ne {M}} {
 312                        unlock_index
 313                        return
 314                }
 315        }
 316
 317        set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
 318        set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
 319        if {$s_lno eq {}} {
 320                unlock_index
 321                return
 322        }
 323
 324        set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
 325        if {$e_lno eq {}} {
 326                set e_lno end
 327        }
 328
 329        if {[catch {
 330                set p [eval git_write $apply_cmd]
 331                fconfigure $p -translation binary -encoding binary
 332                puts -nonewline $p $current_diff_header
 333                puts -nonewline $p [$ui_diff get $s_lno $e_lno]
 334                close $p} err]} {
 335                error_popup [append $failed_msg "\n\n$err"]
 336                unlock_index
 337                return
 338        }
 339
 340        $ui_diff conf -state normal
 341        $ui_diff delete $s_lno $e_lno
 342        $ui_diff conf -state disabled
 343
 344        if {[$ui_diff get 1.0 end] eq "\n"} {
 345                set o _
 346        } else {
 347                set o ?
 348        }
 349
 350        if {$current_diff_side eq $ui_index} {
 351                set mi ${o}M
 352        } elseif {[string index $mi 0] eq {_}} {
 353                set mi M$o
 354        } else {
 355                set mi ?$o
 356        }
 357        unlock_index
 358        display_file $current_diff_path $mi
 359        if {$o eq {_}} {
 360                clear_diff
 361        } else {
 362                set current_diff_path $current_diff_path
 363        }
 364}
 365
 366proc apply_line {x y} {
 367        global current_diff_path current_diff_header current_diff_side
 368        global ui_diff ui_index file_states
 369
 370        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 371        if {![lock_index apply_hunk]} return
 372
 373        set apply_cmd {apply --cached --whitespace=nowarn}
 374        set mi [lindex $file_states($current_diff_path) 0]
 375        if {$current_diff_side eq $ui_index} {
 376                set failed_msg [mc "Failed to unstage selected line."]
 377                set to_context {+}
 378                lappend apply_cmd --reverse
 379                if {[string index $mi 0] ne {M}} {
 380                        unlock_index
 381                        return
 382                }
 383        } else {
 384                set failed_msg [mc "Failed to stage selected line."]
 385                set to_context {-}
 386                if {[string index $mi 1] ne {M}} {
 387                        unlock_index
 388                        return
 389                }
 390        }
 391
 392        set the_l [$ui_diff index @$x,$y]
 393
 394        # operate only on change lines
 395        set c1 [$ui_diff get "$the_l linestart"]
 396        if {$c1 ne {+} && $c1 ne {-}} {
 397                unlock_index
 398                return
 399        }
 400        set sign $c1
 401
 402        set i_l [$ui_diff search -backwards -regexp ^@@ $the_l 0.0]
 403        if {$i_l eq {}} {
 404                unlock_index
 405                return
 406        }
 407        # $i_l is now at the beginning of a line
 408
 409        # pick start line number from hunk header
 410        set hh [$ui_diff get $i_l "$i_l + 1 lines"]
 411        set hh [lindex [split $hh ,] 0]
 412        set hln [lindex [split $hh -] 1]
 413
 414        set n 0
 415        set i_l [$ui_diff index "$i_l + 1 lines"]
 416        set patch {}
 417        while {[$ui_diff compare $i_l < "end - 1 chars"] &&
 418               [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
 419                set next_l [$ui_diff index "$i_l + 1 lines"]
 420                set c1 [$ui_diff get $i_l]
 421                if {[$ui_diff compare $i_l <= $the_l] &&
 422                    [$ui_diff compare $the_l < $next_l]} {
 423                        # the line to stage/unstage
 424                        set ln [$ui_diff get $i_l $next_l]
 425                        set patch "$patch$ln"
 426                } elseif {$c1 ne {-} && $c1 ne {+}} {
 427                        # context line
 428                        set ln [$ui_diff get $i_l $next_l]
 429                        set patch "$patch$ln"
 430                        set n [expr $n+1]
 431                } elseif {$c1 eq $to_context} {
 432                        # turn change line into context line
 433                        set ln [$ui_diff get "$i_l + 1 chars" $next_l]
 434                        set patch "$patch $ln"
 435                        set n [expr $n+1]
 436                }
 437                set i_l $next_l
 438        }
 439        set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch"
 440
 441        if {[catch {
 442                set p [eval git_write $apply_cmd]
 443                fconfigure $p -translation binary -encoding binary
 444                puts -nonewline $p $current_diff_header
 445                puts -nonewline $p $patch
 446                close $p} err]} {
 447                error_popup [append $failed_msg "\n\n$err"]
 448        }
 449
 450        unlock_index
 451}