153437b18e15e31d310994df8e9f26cb563bfe2e
   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        global ui_diff
  23
  24        set p $current_diff_path
  25        if {$p eq {}} {
  26                # No diff is being shown.
  27        } elseif {$current_diff_side eq {}} {
  28                clear_diff
  29        } elseif {[catch {set s $file_states($p)}]
  30                || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} {
  31
  32                if {[find_next_diff $current_diff_side $p {} {[^O]}]} {
  33                        next_diff
  34                } else {
  35                        clear_diff
  36                }
  37        } else {
  38                set save_pos [lindex [$ui_diff yview] 0]
  39                show_diff $p $current_diff_side {} $save_pos
  40        }
  41}
  42
  43proc handle_empty_diff {} {
  44        global current_diff_path file_states file_lists
  45
  46        set path $current_diff_path
  47        set s $file_states($path)
  48        if {[lindex $s 0] ne {_M}} return
  49
  50        info_popup [mc "No differences detected.
  51
  52%s has no changes.
  53
  54The modification date of this file was updated by another application, but the content within the file was not changed.
  55
  56A rescan will be automatically started to find other files which may have the same state." [short_path $path]]
  57
  58        clear_diff
  59        display_file $path __
  60        rescan ui_ready 0
  61}
  62
  63proc show_diff {path w {lno {}} {scroll_pos {}}} {
  64        global file_states file_lists
  65        global is_3way_diff diff_active repo_config
  66        global ui_diff ui_index ui_workdir
  67        global current_diff_path current_diff_side current_diff_header
  68
  69        if {$diff_active || ![lock_index read]} return
  70
  71        clear_diff
  72        if {$lno == {}} {
  73                set lno [lsearch -sorted -exact $file_lists($w) $path]
  74                if {$lno >= 0} {
  75                        incr lno
  76                }
  77        }
  78        if {$lno >= 1} {
  79                $w tag add in_diff $lno.0 [expr {$lno + 1}].0
  80                $w see $lno.0
  81        }
  82
  83        set s $file_states($path)
  84        set m [lindex $s 0]
  85        set is_3way_diff 0
  86        set diff_active 1
  87        set current_diff_path $path
  88        set current_diff_side $w
  89        set current_diff_header {}
  90        ui_status [mc "Loading diff of %s..." [escape_path $path]]
  91
  92        # - Git won't give us the diff, there's nothing to compare to!
  93        #
  94        if {$m eq {_O}} {
  95                set max_sz [expr {128 * 1024}]
  96                set type unknown
  97                if {[catch {
  98                                set type [file type $path]
  99                                switch -- $type {
 100                                directory {
 101                                        set type submodule
 102                                        set content {}
 103                                        set sz 0
 104                                }
 105                                link {
 106                                        set content [file readlink $path]
 107                                        set sz [string length $content]
 108                                }
 109                                file {
 110                                        set fd [open $path r]
 111                                        fconfigure $fd -eofchar {}
 112                                        set content [read $fd $max_sz]
 113                                        close $fd
 114                                        set sz [file size $path]
 115                                }
 116                                default {
 117                                        error "'$type' not supported"
 118                                }
 119                                }
 120                        } err ]} {
 121                        set diff_active 0
 122                        unlock_index
 123                        ui_status [mc "Unable to display %s" [escape_path $path]]
 124                        error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
 125                        return
 126                }
 127                $ui_diff conf -state normal
 128                if {$type eq {submodule}} {
 129                        $ui_diff insert end [append \
 130                                "* " \
 131                                [mc "Git Repository (subproject)"] \
 132                                "\n"] d_@
 133                } elseif {![catch {set type [exec file $path]}]} {
 134                        set n [string length $path]
 135                        if {[string equal -length $n $path $type]} {
 136                                set type [string range $type $n end]
 137                                regsub {^:?\s*} $type {} type
 138                        }
 139                        $ui_diff insert end "* $type\n" d_@
 140                }
 141                if {[string first "\0" $content] != -1} {
 142                        $ui_diff insert end \
 143                                [mc "* Binary file (not showing content)."] \
 144                                d_@
 145                } else {
 146                        if {$sz > $max_sz} {
 147                                $ui_diff insert end \
 148"* Untracked file is $sz bytes.
 149* Showing only first $max_sz bytes.
 150" d_@
 151                        }
 152                        $ui_diff insert end $content
 153                        if {$sz > $max_sz} {
 154                                $ui_diff insert end "
 155* Untracked file clipped here by [appname].
 156* To see the entire file, use an external editor.
 157" d_@
 158                        }
 159                }
 160                $ui_diff conf -state disabled
 161                set diff_active 0
 162                unlock_index
 163                if {$scroll_pos ne {}} {
 164                        update
 165                        $ui_diff yview moveto $scroll_pos
 166                }
 167                ui_ready
 168                return
 169        }
 170
 171        set cmd [list]
 172        if {$w eq $ui_index} {
 173                lappend cmd diff-index
 174                lappend cmd --cached
 175        } elseif {$w eq $ui_workdir} {
 176                if {[string first {U} $m] >= 0} {
 177                        lappend cmd diff
 178                } else {
 179                        lappend cmd diff-files
 180                }
 181        }
 182
 183        lappend cmd -p
 184        lappend cmd --no-color
 185        if {$repo_config(gui.diffcontext) >= 1} {
 186                lappend cmd "-U$repo_config(gui.diffcontext)"
 187        }
 188        if {$w eq $ui_index} {
 189                lappend cmd [PARENT]
 190        }
 191        lappend cmd --
 192        lappend cmd $path
 193
 194        if {[catch {set fd [eval git_read --nice $cmd]} err]} {
 195                set diff_active 0
 196                unlock_index
 197                ui_status [mc "Unable to display %s" [escape_path $path]]
 198                error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
 199                return
 200        }
 201
 202        fconfigure $fd \
 203                -blocking 0 \
 204                -encoding binary \
 205                -translation binary
 206        fileevent $fd readable [list read_diff $fd $scroll_pos]
 207}
 208
 209proc read_diff {fd scroll_pos} {
 210        global ui_diff diff_active
 211        global is_3way_diff current_diff_header
 212
 213        $ui_diff conf -state normal
 214        while {[gets $fd line] >= 0} {
 215                # -- Cleanup uninteresting diff header lines.
 216                #
 217                if {   [string match {diff --git *}      $line]
 218                        || [string match {diff --cc *}       $line]
 219                        || [string match {diff --combined *} $line]
 220                        || [string match {--- *}             $line]
 221                        || [string match {+++ *}             $line]} {
 222                        append current_diff_header $line "\n"
 223                        continue
 224                }
 225                if {[string match {index *} $line]} continue
 226                if {$line eq {deleted file mode 120000}} {
 227                        set line "deleted symlink"
 228                }
 229
 230                # -- Automatically detect if this is a 3 way diff.
 231                #
 232                if {[string match {@@@ *} $line]} {set is_3way_diff 1}
 233
 234                if {[string match {mode *} $line]
 235                        || [string match {new file *} $line]
 236                        || [regexp {^(old|new) mode *} $line]
 237                        || [string match {deleted file *} $line]
 238                        || [string match {deleted symlink} $line]
 239                        || [string match {Binary files * and * differ} $line]
 240                        || $line eq {\ No newline at end of file}
 241                        || [regexp {^\* Unmerged path } $line]} {
 242                        set tags {}
 243                } elseif {$is_3way_diff} {
 244                        set op [string range $line 0 1]
 245                        switch -- $op {
 246                        {  } {set tags {}}
 247                        {@@} {set tags d_@}
 248                        { +} {set tags d_s+}
 249                        { -} {set tags d_s-}
 250                        {+ } {set tags d_+s}
 251                        {- } {set tags d_-s}
 252                        {--} {set tags d_--}
 253                        {++} {
 254                                if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
 255                                        set line [string replace $line 0 1 {  }]
 256                                        set tags d$op
 257                                } else {
 258                                        set tags d_++
 259                                }
 260                        }
 261                        default {
 262                                puts "error: Unhandled 3 way diff marker: {$op}"
 263                                set tags {}
 264                        }
 265                        }
 266                } else {
 267                        set op [string index $line 0]
 268                        switch -- $op {
 269                        { } {set tags {}}
 270                        {@} {set tags d_@}
 271                        {-} {set tags d_-}
 272                        {+} {
 273                                if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
 274                                        set line [string replace $line 0 0 { }]
 275                                        set tags d$op
 276                                } else {
 277                                        set tags d_+
 278                                }
 279                        }
 280                        default {
 281                                puts "error: Unhandled 2 way diff marker: {$op}"
 282                                set tags {}
 283                        }
 284                        }
 285                }
 286                $ui_diff insert end $line $tags
 287                if {[string index $line end] eq "\r"} {
 288                        $ui_diff tag add d_cr {end - 2c}
 289                }
 290                $ui_diff insert end "\n" $tags
 291        }
 292        $ui_diff conf -state disabled
 293
 294        if {[eof $fd]} {
 295                close $fd
 296                set diff_active 0
 297                unlock_index
 298                if {$scroll_pos ne {}} {
 299                        update
 300                        $ui_diff yview moveto $scroll_pos
 301                }
 302                ui_ready
 303
 304                if {[$ui_diff index end] eq {2.0}} {
 305                        handle_empty_diff
 306                }
 307        }
 308}
 309
 310proc apply_hunk {x y} {
 311        global current_diff_path current_diff_header current_diff_side
 312        global ui_diff ui_index file_states
 313
 314        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 315        if {![lock_index apply_hunk]} return
 316
 317        set apply_cmd {apply --cached --whitespace=nowarn}
 318        set mi [lindex $file_states($current_diff_path) 0]
 319        if {$current_diff_side eq $ui_index} {
 320                set failed_msg [mc "Failed to unstage selected hunk."]
 321                lappend apply_cmd --reverse
 322                if {[string index $mi 0] ne {M}} {
 323                        unlock_index
 324                        return
 325                }
 326        } else {
 327                set failed_msg [mc "Failed to stage selected hunk."]
 328                if {[string index $mi 1] ne {M}} {
 329                        unlock_index
 330                        return
 331                }
 332        }
 333
 334        set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
 335        set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
 336        if {$s_lno eq {}} {
 337                unlock_index
 338                return
 339        }
 340
 341        set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
 342        if {$e_lno eq {}} {
 343                set e_lno end
 344        }
 345
 346        if {[catch {
 347                set p [eval git_write $apply_cmd]
 348                fconfigure $p -translation binary -encoding binary
 349                puts -nonewline $p $current_diff_header
 350                puts -nonewline $p [$ui_diff get $s_lno $e_lno]
 351                close $p} err]} {
 352                error_popup [append $failed_msg "\n\n$err"]
 353                unlock_index
 354                return
 355        }
 356
 357        $ui_diff conf -state normal
 358        $ui_diff delete $s_lno $e_lno
 359        $ui_diff conf -state disabled
 360
 361        if {[$ui_diff get 1.0 end] eq "\n"} {
 362                set o _
 363        } else {
 364                set o ?
 365        }
 366
 367        if {$current_diff_side eq $ui_index} {
 368                set mi ${o}M
 369        } elseif {[string index $mi 0] eq {_}} {
 370                set mi M$o
 371        } else {
 372                set mi ?$o
 373        }
 374        unlock_index
 375        display_file $current_diff_path $mi
 376        # This should trigger shift to the next changed file
 377        if {$o eq {_}} {
 378                reshow_diff
 379        }
 380}
 381
 382proc apply_line {x y} {
 383        global current_diff_path current_diff_header current_diff_side
 384        global ui_diff ui_index file_states
 385
 386        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 387        if {![lock_index apply_hunk]} return
 388
 389        set apply_cmd {apply --cached --whitespace=nowarn}
 390        set mi [lindex $file_states($current_diff_path) 0]
 391        if {$current_diff_side eq $ui_index} {
 392                set failed_msg [mc "Failed to unstage selected line."]
 393                set to_context {+}
 394                lappend apply_cmd --reverse
 395                if {[string index $mi 0] ne {M}} {
 396                        unlock_index
 397                        return
 398                }
 399        } else {
 400                set failed_msg [mc "Failed to stage selected line."]
 401                set to_context {-}
 402                if {[string index $mi 1] ne {M}} {
 403                        unlock_index
 404                        return
 405                }
 406        }
 407
 408        set the_l [$ui_diff index @$x,$y]
 409
 410        # operate only on change lines
 411        set c1 [$ui_diff get "$the_l linestart"]
 412        if {$c1 ne {+} && $c1 ne {-}} {
 413                unlock_index
 414                return
 415        }
 416        set sign $c1
 417
 418        set i_l [$ui_diff search -backwards -regexp ^@@ $the_l 0.0]
 419        if {$i_l eq {}} {
 420                unlock_index
 421                return
 422        }
 423        # $i_l is now at the beginning of a line
 424
 425        # pick start line number from hunk header
 426        set hh [$ui_diff get $i_l "$i_l + 1 lines"]
 427        set hh [lindex [split $hh ,] 0]
 428        set hln [lindex [split $hh -] 1]
 429
 430        # There is a special situation to take care of. Consider this hunk:
 431        #
 432        #    @@ -10,4 +10,4 @@
 433        #     context before
 434        #    -old 1
 435        #    -old 2
 436        #    +new 1
 437        #    +new 2
 438        #     context after
 439        #
 440        # We used to keep the context lines in the order they appear in the
 441        # hunk. But then it is not possible to correctly stage only
 442        # "-old 1" and "+new 1" - it would result in this staged text:
 443        #
 444        #    context before
 445        #    old 2
 446        #    new 1
 447        #    context after
 448        #
 449        # (By symmetry it is not possible to *un*stage "old 2" and "new 2".)
 450        #
 451        # We resolve the problem by introducing an asymmetry, namely, when
 452        # a "+" line is *staged*, it is moved in front of the context lines
 453        # that are generated from the "-" lines that are immediately before
 454        # the "+" block. That is, we construct this patch:
 455        #
 456        #    @@ -10,4 +10,5 @@
 457        #     context before
 458        #    +new 1
 459        #     old 1
 460        #     old 2
 461        #     context after
 462        #
 463        # But we do *not* treat "-" lines that are *un*staged in a special
 464        # way.
 465        #
 466        # With this asymmetry it is possible to stage the change
 467        # "old 1" -> "new 1" directly, and to stage the change
 468        # "old 2" -> "new 2" by first staging the entire hunk and
 469        # then unstaging the change "old 1" -> "new 1".
 470
 471        # This is non-empty if and only if we are _staging_ changes;
 472        # then it accumulates the consecutive "-" lines (after converting
 473        # them to context lines) in order to be moved after the "+" change
 474        # line.
 475        set pre_context {}
 476
 477        set n 0
 478        set i_l [$ui_diff index "$i_l + 1 lines"]
 479        set patch {}
 480        while {[$ui_diff compare $i_l < "end - 1 chars"] &&
 481               [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
 482                set next_l [$ui_diff index "$i_l + 1 lines"]
 483                set c1 [$ui_diff get $i_l]
 484                if {[$ui_diff compare $i_l <= $the_l] &&
 485                    [$ui_diff compare $the_l < $next_l]} {
 486                        # the line to stage/unstage
 487                        set ln [$ui_diff get $i_l $next_l]
 488                        if {$c1 eq {-}} {
 489                                set n [expr $n+1]
 490                                set patch "$patch$pre_context$ln"
 491                        } else {
 492                                set patch "$patch$ln$pre_context"
 493                        }
 494                        set pre_context {}
 495                } elseif {$c1 ne {-} && $c1 ne {+}} {
 496                        # context line
 497                        set ln [$ui_diff get $i_l $next_l]
 498                        set patch "$patch$pre_context$ln"
 499                        set n [expr $n+1]
 500                        set pre_context {}
 501                } elseif {$c1 eq $to_context} {
 502                        # turn change line into context line
 503                        set ln [$ui_diff get "$i_l + 1 chars" $next_l]
 504                        if {$c1 eq {-}} {
 505                                set pre_context "$pre_context $ln"
 506                        } else {
 507                                set patch "$patch $ln"
 508                        }
 509                        set n [expr $n+1]
 510                }
 511                set i_l $next_l
 512        }
 513        set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch"
 514
 515        if {[catch {
 516                set p [eval git_write $apply_cmd]
 517                fconfigure $p -translation binary -encoding binary
 518                puts -nonewline $p $current_diff_header
 519                puts -nonewline $p $patch
 520                close $p} err]} {
 521                error_popup [append $failed_msg "\n\n$err"]
 522        }
 523
 524        unlock_index
 525}