git-gui / lib / diff.tclon commit Merge branch 'jc/doc-gc-prune-now' into maint (ce555f3)
   1# git-gui diff viewer
   2# Copyright (C) 2006, 2007 Shawn Pearce
   3
   4proc apply_tab_size {{firsttab {}}} {
   5        global have_tk85 repo_config ui_diff
   6
   7        set w [font measure font_diff "0"]
   8        if {$have_tk85 && $firsttab != 0} {
   9                $ui_diff configure -tabs [list [expr {$firsttab * $w}] [expr {($firsttab + $repo_config(gui.tabsize)) * $w}]]
  10        } elseif {$have_tk85 || $repo_config(gui.tabsize) != 8} {
  11                $ui_diff configure -tabs [expr {$repo_config(gui.tabsize) * $w}]
  12        } else {
  13                $ui_diff configure -tabs {}
  14        }
  15}
  16
  17proc clear_diff {} {
  18        global ui_diff current_diff_path current_diff_header
  19        global ui_index ui_workdir
  20
  21        $ui_diff conf -state normal
  22        $ui_diff delete 0.0 end
  23        $ui_diff conf -state disabled
  24
  25        set current_diff_path {}
  26        set current_diff_header {}
  27
  28        $ui_index tag remove in_diff 0.0 end
  29        $ui_workdir tag remove in_diff 0.0 end
  30}
  31
  32proc reshow_diff {{after {}}} {
  33        global file_states file_lists
  34        global current_diff_path current_diff_side
  35        global ui_diff
  36
  37        set p $current_diff_path
  38        if {$p eq {}} {
  39                # No diff is being shown.
  40        } elseif {$current_diff_side eq {}} {
  41                clear_diff
  42        } elseif {[catch {set s $file_states($p)}]
  43                || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} {
  44
  45                if {[find_next_diff $current_diff_side $p {} {[^O]}]} {
  46                        next_diff $after
  47                } else {
  48                        clear_diff
  49                }
  50        } else {
  51                set save_pos [lindex [$ui_diff yview] 0]
  52                show_diff $p $current_diff_side {} $save_pos $after
  53        }
  54}
  55
  56proc force_diff_encoding {enc} {
  57        global current_diff_path
  58        
  59        if {$current_diff_path ne {}} {
  60                force_path_encoding $current_diff_path $enc
  61                reshow_diff
  62        }
  63}
  64
  65proc handle_empty_diff {} {
  66        global current_diff_path file_states file_lists
  67        global diff_empty_count
  68
  69        set path $current_diff_path
  70        set s $file_states($path)
  71        if {[lindex $s 0] ne {_M} || [has_textconv $path]} return
  72
  73        # Prevent infinite rescan loops
  74        incr diff_empty_count
  75        if {$diff_empty_count > 1} return
  76
  77        info_popup [mc "No differences detected.
  78
  79%s has no changes.
  80
  81The modification date of this file was updated by another application, but the content within the file was not changed.
  82
  83A rescan will be automatically started to find other files which may have the same state." [short_path $path]]
  84
  85        clear_diff
  86        display_file $path __
  87        rescan ui_ready 0
  88}
  89
  90proc show_diff {path w {lno {}} {scroll_pos {}} {callback {}}} {
  91        global file_states file_lists
  92        global is_3way_diff is_conflict_diff diff_active repo_config
  93        global ui_diff ui_index ui_workdir
  94        global current_diff_path current_diff_side current_diff_header
  95        global current_diff_queue
  96
  97        if {$diff_active || ![lock_index read]} return
  98
  99        clear_diff
 100        if {$lno == {}} {
 101                set lno [lsearch -sorted -exact $file_lists($w) $path]
 102                if {$lno >= 0} {
 103                        incr lno
 104                }
 105        }
 106        if {$lno >= 1} {
 107                $w tag add in_diff $lno.0 [expr {$lno + 1}].0
 108                $w see $lno.0
 109        }
 110
 111        set s $file_states($path)
 112        set m [lindex $s 0]
 113        set is_conflict_diff 0
 114        set current_diff_path $path
 115        set current_diff_side $w
 116        set current_diff_queue {}
 117        ui_status [mc "Loading diff of %s..." [escape_path $path]]
 118
 119        set cont_info [list $scroll_pos $callback]
 120
 121        apply_tab_size 0
 122
 123        if {[string first {U} $m] >= 0} {
 124                merge_load_stages $path [list show_unmerged_diff $cont_info]
 125        } elseif {$m eq {_O}} {
 126                show_other_diff $path $w $m $cont_info
 127        } else {
 128                start_show_diff $cont_info
 129        }
 130}
 131
 132proc show_unmerged_diff {cont_info} {
 133        global current_diff_path current_diff_side
 134        global merge_stages ui_diff is_conflict_diff
 135        global current_diff_queue
 136
 137        if {$merge_stages(2) eq {}} {
 138                set is_conflict_diff 1
 139                lappend current_diff_queue \
 140                        [list [mc "LOCAL: deleted\nREMOTE:\n"] d= \
 141                            [list ":1:$current_diff_path" ":3:$current_diff_path"]]
 142        } elseif {$merge_stages(3) eq {}} {
 143                set is_conflict_diff 1
 144                lappend current_diff_queue \
 145                        [list [mc "REMOTE: deleted\nLOCAL:\n"] d= \
 146                            [list ":1:$current_diff_path" ":2:$current_diff_path"]]
 147        } elseif {[lindex $merge_stages(1) 0] eq {120000}
 148                || [lindex $merge_stages(2) 0] eq {120000}
 149                || [lindex $merge_stages(3) 0] eq {120000}} {
 150                set is_conflict_diff 1
 151                lappend current_diff_queue \
 152                        [list [mc "LOCAL:\n"] d= \
 153                            [list ":1:$current_diff_path" ":2:$current_diff_path"]]
 154                lappend current_diff_queue \
 155                        [list [mc "REMOTE:\n"] d= \
 156                            [list ":1:$current_diff_path" ":3:$current_diff_path"]]
 157        } else {
 158                start_show_diff $cont_info
 159                return
 160        }
 161
 162        advance_diff_queue $cont_info
 163}
 164
 165proc advance_diff_queue {cont_info} {
 166        global current_diff_queue ui_diff
 167
 168        set item [lindex $current_diff_queue 0]
 169        set current_diff_queue [lrange $current_diff_queue 1 end]
 170
 171        $ui_diff conf -state normal
 172        $ui_diff insert end [lindex $item 0] [lindex $item 1]
 173        $ui_diff conf -state disabled
 174
 175        start_show_diff $cont_info [lindex $item 2]
 176}
 177
 178proc show_other_diff {path w m cont_info} {
 179        global file_states file_lists
 180        global is_3way_diff diff_active repo_config
 181        global ui_diff ui_index ui_workdir
 182        global current_diff_path current_diff_side current_diff_header
 183
 184        # - Git won't give us the diff, there's nothing to compare to!
 185        #
 186        if {$m eq {_O}} {
 187                set max_sz 100000
 188                set type unknown
 189                if {[catch {
 190                                set type [file type $path]
 191                                switch -- $type {
 192                                directory {
 193                                        set type submodule
 194                                        set content {}
 195                                        set sz 0
 196                                }
 197                                link {
 198                                        set content [file readlink $path]
 199                                        set sz [string length $content]
 200                                }
 201                                file {
 202                                        set fd [open $path r]
 203                                        fconfigure $fd \
 204                                                -eofchar {} \
 205                                                -encoding [get_path_encoding $path]
 206                                        set content [read $fd $max_sz]
 207                                        close $fd
 208                                        set sz [file size $path]
 209                                }
 210                                default {
 211                                        error "'$type' not supported"
 212                                }
 213                                }
 214                        } err ]} {
 215                        set diff_active 0
 216                        unlock_index
 217                        ui_status [mc "Unable to display %s" [escape_path $path]]
 218                        error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
 219                        return
 220                }
 221                $ui_diff conf -state normal
 222                if {$type eq {submodule}} {
 223                        $ui_diff insert end [append \
 224                                "* " \
 225                                [mc "Git Repository (subproject)"] \
 226                                "\n"] d_info
 227                } elseif {![catch {set type [exec file $path]}]} {
 228                        set n [string length $path]
 229                        if {[string equal -length $n $path $type]} {
 230                                set type [string range $type $n end]
 231                                regsub {^:?\s*} $type {} type
 232                        }
 233                        $ui_diff insert end "* $type\n" d_info
 234                }
 235                if {[string first "\0" $content] != -1} {
 236                        $ui_diff insert end \
 237                                [mc "* Binary file (not showing content)."] \
 238                                d_info
 239                } else {
 240                        if {$sz > $max_sz} {
 241                                $ui_diff insert end [mc \
 242"* Untracked file is %d bytes.
 243* Showing only first %d bytes.
 244" $sz $max_sz] d_info
 245                        }
 246                        $ui_diff insert end $content
 247                        if {$sz > $max_sz} {
 248                                $ui_diff insert end [mc "
 249* Untracked file clipped here by %s.
 250* To see the entire file, use an external editor.
 251" [appname]] d_info
 252                        }
 253                }
 254                $ui_diff conf -state disabled
 255                set diff_active 0
 256                unlock_index
 257                set scroll_pos [lindex $cont_info 0]
 258                if {$scroll_pos ne {}} {
 259                        update
 260                        $ui_diff yview moveto $scroll_pos
 261                }
 262                ui_ready
 263                set callback [lindex $cont_info 1]
 264                if {$callback ne {}} {
 265                        eval $callback
 266                }
 267                return
 268        }
 269}
 270
 271proc get_conflict_marker_size {path} {
 272        set size 7
 273        catch {
 274                set fd_rc [eval [list git_read check-attr "conflict-marker-size" -- $path]]
 275                set ret [gets $fd_rc line]
 276                close $fd_rc
 277                if {$ret > 0} {
 278                        regexp {.*: conflict-marker-size: (\d+)$} $line line size
 279                }
 280        }
 281        return $size
 282}
 283
 284proc start_show_diff {cont_info {add_opts {}}} {
 285        global file_states file_lists
 286        global is_3way_diff is_submodule_diff diff_active repo_config
 287        global ui_diff ui_index ui_workdir
 288        global current_diff_path current_diff_side current_diff_header
 289
 290        set path $current_diff_path
 291        set w $current_diff_side
 292
 293        set s $file_states($path)
 294        set m [lindex $s 0]
 295        set is_3way_diff 0
 296        set is_submodule_diff 0
 297        set diff_active 1
 298        set current_diff_header {}
 299        set conflict_size [get_conflict_marker_size $path]
 300
 301        set cmd [list]
 302        if {$w eq $ui_index} {
 303                lappend cmd diff-index
 304                lappend cmd --cached
 305                if {[git-version >= "1.7.2"]} {
 306                        lappend cmd --ignore-submodules=dirty
 307                }
 308        } elseif {$w eq $ui_workdir} {
 309                if {[string first {U} $m] >= 0} {
 310                        lappend cmd diff
 311                } else {
 312                        lappend cmd diff-files
 313                }
 314        }
 315        if {![is_config_false gui.textconv] && [git-version >= 1.6.1]} {
 316                lappend cmd --textconv
 317        }
 318
 319        if {[string match {160000 *} [lindex $s 2]]
 320         || [string match {160000 *} [lindex $s 3]]} {
 321                set is_submodule_diff 1
 322
 323                if {[git-version >= "1.6.6"]} {
 324                        lappend cmd --submodule
 325                }
 326        }
 327
 328        lappend cmd -p
 329        lappend cmd --color
 330        set cmd [concat $cmd $repo_config(gui.diffopts)]
 331        if {$repo_config(gui.diffcontext) >= 1} {
 332                lappend cmd "-U$repo_config(gui.diffcontext)"
 333        }
 334        if {$w eq $ui_index} {
 335                lappend cmd [PARENT]
 336        }
 337        if {$add_opts ne {}} {
 338                eval lappend cmd $add_opts
 339        } else {
 340                lappend cmd --
 341                lappend cmd $path
 342        }
 343
 344        if {$is_submodule_diff && [git-version < "1.6.6"]} {
 345                if {$w eq $ui_index} {
 346                        set cmd [list submodule summary --cached -- $path]
 347                } else {
 348                        set cmd [list submodule summary --files -- $path]
 349                }
 350        }
 351
 352        if {[catch {set fd [eval git_read --nice $cmd]} err]} {
 353                set diff_active 0
 354                unlock_index
 355                ui_status [mc "Unable to display %s" [escape_path $path]]
 356                error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
 357                return
 358        }
 359
 360        set ::current_diff_inheader 1
 361        fconfigure $fd \
 362                -blocking 0 \
 363                -encoding [get_path_encoding $path] \
 364                -translation lf
 365        fileevent $fd readable [list read_diff $fd $conflict_size $cont_info]
 366}
 367
 368proc parse_color_line {line} {
 369        set start 0
 370        set result ""
 371        set markup [list]
 372        set regexp {\033\[((?:\d+;)*\d+)?m}
 373        set need_reset 0
 374        while {[regexp -indices -start $start $regexp $line match code]} {
 375                foreach {begin end} $match break
 376                append result [string range $line $start [expr {$begin - 1}]]
 377                set pos [string length $result]
 378                set col [eval [linsert $code 0 string range $line]]
 379                set start [incr end]
 380                if {$col eq "0" || $col eq ""} {
 381                        if {!$need_reset} continue
 382                        set need_reset 0
 383                } else {
 384                        set need_reset 1
 385                }
 386                lappend markup $pos $col
 387        }
 388        append result [string range $line $start end]
 389        if {[llength $markup] < 4} {set markup {}}
 390        return [list $result $markup]
 391}
 392
 393proc read_diff {fd conflict_size cont_info} {
 394        global ui_diff diff_active is_submodule_diff
 395        global is_3way_diff is_conflict_diff current_diff_header
 396        global current_diff_queue
 397        global diff_empty_count
 398
 399        $ui_diff conf -state normal
 400        while {[gets $fd line] >= 0} {
 401                foreach {line markup} [parse_color_line $line] break
 402                set line [string map {\033 ^} $line]
 403
 404                set tags {}
 405
 406                # -- Check for start of diff header.
 407                if {   [string match {diff --git *}      $line]
 408                    || [string match {diff --cc *}       $line]
 409                    || [string match {diff --combined *} $line]} {
 410                        set ::current_diff_inheader 1
 411                }
 412
 413                # -- Check for end of diff header (any hunk line will do this).
 414                #
 415                if {[regexp {^@@+ } $line]} {set ::current_diff_inheader 0}
 416
 417                # -- Automatically detect if this is a 3 way diff.
 418                #
 419                if {[string match {@@@ *} $line]} {
 420                        set is_3way_diff 1
 421                        apply_tab_size 1
 422                }
 423
 424                if {$::current_diff_inheader} {
 425
 426                        # -- These two lines stop a diff header and shouldn't be in there
 427                        if {   [string match {Binary files * and * differ} $line]
 428                            || [regexp {^\* Unmerged path }                $line]} {
 429                                set ::current_diff_inheader 0
 430                        } else {
 431                                append current_diff_header $line "\n"
 432                        }
 433
 434                        # -- Cleanup uninteresting diff header lines.
 435                        #
 436                        if {   [string match {diff --git *}      $line]
 437                            || [string match {diff --cc *}       $line]
 438                            || [string match {diff --combined *} $line]
 439                            || [string match {--- *}             $line]
 440                            || [string match {+++ *}             $line]
 441                            || [string match {index *}           $line]} {
 442                                continue
 443                        }
 444
 445                        # -- Name it symlink, not 120000
 446                        #    Note, that the original line is in $current_diff_header
 447                        regsub {^(deleted|new) file mode 120000} $line {\1 symlink} line
 448
 449                } elseif {   $line eq {\ No newline at end of file}} {
 450                        # -- Handle some special lines
 451                } elseif {$is_3way_diff} {
 452                        set op [string range $line 0 1]
 453                        switch -- $op {
 454                        {  } {set tags {}}
 455                        {@@} {set tags d_@}
 456                        { +} {set tags d_s+}
 457                        { -} {set tags d_s-}
 458                        {+ } {set tags d_+s}
 459                        {- } {set tags d_-s}
 460                        {--} {set tags d_--}
 461                        {++} {
 462                                set regexp [string map [list %conflict_size $conflict_size]\
 463                                                                {^\+\+([<>=]){%conflict_size}(?: |$)}]
 464                                if {[regexp $regexp $line _g op]} {
 465                                        set is_conflict_diff 1
 466                                        set line [string replace $line 0 1 {  }]
 467                                        set tags d$op
 468                                } else {
 469                                        set tags d_++
 470                                }
 471                        }
 472                        default {
 473                                puts "error: Unhandled 3 way diff marker: {$op}"
 474                                set tags {}
 475                        }
 476                        }
 477                } elseif {$is_submodule_diff} {
 478                        if {$line == ""} continue
 479                        if {[regexp {^Submodule } $line]} {
 480                                set tags d_info
 481                        } elseif {[regexp {^\* } $line]} {
 482                                set line [string replace $line 0 1 {Submodule }]
 483                                set tags d_info
 484                        } else {
 485                                set op [string range $line 0 2]
 486                                switch -- $op {
 487                                {  <} {set tags d_-}
 488                                {  >} {set tags d_+}
 489                                {  W} {set tags {}}
 490                                default {
 491                                        puts "error: Unhandled submodule diff marker: {$op}"
 492                                        set tags {}
 493                                }
 494                                }
 495                        }
 496                } else {
 497                        set op [string index $line 0]
 498                        switch -- $op {
 499                        { } {set tags {}}
 500                        {@} {set tags d_@}
 501                        {-} {set tags d_-}
 502                        {+} {
 503                                set regexp [string map [list %conflict_size $conflict_size]\
 504                                                                {^\+([<>=]){%conflict_size}(?: |$)}]
 505                                if {[regexp $regexp $line _g op]} {
 506                                        set is_conflict_diff 1
 507                                        set tags d$op
 508                                } else {
 509                                        set tags d_+
 510                                }
 511                        }
 512                        default {
 513                                puts "error: Unhandled 2 way diff marker: {$op}"
 514                                set tags {}
 515                        }
 516                        }
 517                }
 518                set mark [$ui_diff index "end - 1 line linestart"]
 519                $ui_diff insert end $line $tags
 520                if {[string index $line end] eq "\r"} {
 521                        $ui_diff tag add d_cr {end - 2c}
 522                }
 523                $ui_diff insert end "\n" $tags
 524
 525                foreach {posbegin colbegin posend colend} $markup {
 526                        set prefix clr
 527                        foreach style [lsort -integer [split $colbegin ";"]] {
 528                                if {$style eq "7"} {append prefix i; continue}
 529                                if {$style != 4 && ($style < 30 || $style > 47)} {continue}
 530                                set a "$mark linestart + $posbegin chars"
 531                                set b "$mark linestart + $posend chars"
 532                                catch {$ui_diff tag add $prefix$style $a $b}
 533                        }
 534                }
 535        }
 536        $ui_diff conf -state disabled
 537
 538        if {[eof $fd]} {
 539                close $fd
 540
 541                if {$current_diff_queue ne {}} {
 542                        advance_diff_queue $cont_info
 543                        return
 544                }
 545
 546                set diff_active 0
 547                unlock_index
 548                set scroll_pos [lindex $cont_info 0]
 549                if {$scroll_pos ne {}} {
 550                        update
 551                        $ui_diff yview moveto $scroll_pos
 552                }
 553                ui_ready
 554
 555                if {[$ui_diff index end] eq {2.0}} {
 556                        handle_empty_diff
 557                } else {
 558                        set diff_empty_count 0
 559                }
 560
 561                set callback [lindex $cont_info 1]
 562                if {$callback ne {}} {
 563                        eval $callback
 564                }
 565        }
 566}
 567
 568proc apply_hunk {x y} {
 569        global current_diff_path current_diff_header current_diff_side
 570        global ui_diff ui_index file_states
 571
 572        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 573        if {![lock_index apply_hunk]} return
 574
 575        set apply_cmd {apply --cached --whitespace=nowarn}
 576        set mi [lindex $file_states($current_diff_path) 0]
 577        if {$current_diff_side eq $ui_index} {
 578                set failed_msg [mc "Failed to unstage selected hunk."]
 579                lappend apply_cmd --reverse
 580                if {[string index $mi 0] ne {M}} {
 581                        unlock_index
 582                        return
 583                }
 584        } else {
 585                set failed_msg [mc "Failed to stage selected hunk."]
 586                if {[string index $mi 1] ne {M}} {
 587                        unlock_index
 588                        return
 589                }
 590        }
 591
 592        set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
 593        set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
 594        if {$s_lno eq {}} {
 595                unlock_index
 596                return
 597        }
 598
 599        set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
 600        if {$e_lno eq {}} {
 601                set e_lno end
 602        }
 603
 604        if {[catch {
 605                set enc [get_path_encoding $current_diff_path]
 606                set p [eval git_write $apply_cmd]
 607                fconfigure $p -translation binary -encoding $enc
 608                puts -nonewline $p $current_diff_header
 609                puts -nonewline $p [$ui_diff get $s_lno $e_lno]
 610                close $p} err]} {
 611                error_popup [append $failed_msg "\n\n$err"]
 612                unlock_index
 613                return
 614        }
 615
 616        $ui_diff conf -state normal
 617        $ui_diff delete $s_lno $e_lno
 618        $ui_diff conf -state disabled
 619
 620        if {[$ui_diff get 1.0 end] eq "\n"} {
 621                set o _
 622        } else {
 623                set o ?
 624        }
 625
 626        if {$current_diff_side eq $ui_index} {
 627                set mi ${o}M
 628        } elseif {[string index $mi 0] eq {_}} {
 629                set mi M$o
 630        } else {
 631                set mi ?$o
 632        }
 633        unlock_index
 634        display_file $current_diff_path $mi
 635        # This should trigger shift to the next changed file
 636        if {$o eq {_}} {
 637                reshow_diff
 638        }
 639}
 640
 641proc apply_range_or_line {x y} {
 642        global current_diff_path current_diff_header current_diff_side
 643        global ui_diff ui_index file_states
 644
 645        set selected [$ui_diff tag nextrange sel 0.0]
 646
 647        if {$selected == {}} {
 648                set first [$ui_diff index "@$x,$y"]
 649                set last $first
 650        } else {
 651                set first [lindex $selected 0]
 652                set last [lindex $selected 1]
 653        }
 654
 655        set first_l [$ui_diff index "$first linestart"]
 656        set last_l [$ui_diff index "$last lineend"]
 657
 658        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 659        if {![lock_index apply_hunk]} return
 660
 661        set apply_cmd {apply --cached --whitespace=nowarn}
 662        set mi [lindex $file_states($current_diff_path) 0]
 663        if {$current_diff_side eq $ui_index} {
 664                set failed_msg [mc "Failed to unstage selected line."]
 665                set to_context {+}
 666                lappend apply_cmd --reverse
 667                if {[string index $mi 0] ne {M}} {
 668                        unlock_index
 669                        return
 670                }
 671        } else {
 672                set failed_msg [mc "Failed to stage selected line."]
 673                set to_context {-}
 674                if {[string index $mi 1] ne {M}} {
 675                        unlock_index
 676                        return
 677                }
 678        }
 679
 680        set wholepatch {}
 681
 682        while {$first_l < $last_l} {
 683                set i_l [$ui_diff search -backwards -regexp ^@@ $first_l 0.0]
 684                if {$i_l eq {}} {
 685                        # If there's not a @@ above, then the selected range
 686                        # must have come before the first_l @@
 687                        set i_l [$ui_diff search -regexp ^@@ $first_l $last_l]
 688                }
 689                if {$i_l eq {}} {
 690                        unlock_index
 691                        return
 692                }
 693                # $i_l is now at the beginning of a line
 694
 695                # pick start line number from hunk header
 696                set hh [$ui_diff get $i_l "$i_l + 1 lines"]
 697                set hh [lindex [split $hh ,] 0]
 698                set hln [lindex [split $hh -] 1]
 699
 700                # There is a special situation to take care of. Consider this
 701                # hunk:
 702                #
 703                #    @@ -10,4 +10,4 @@
 704                #     context before
 705                #    -old 1
 706                #    -old 2
 707                #    +new 1
 708                #    +new 2
 709                #     context after
 710                #
 711                # We used to keep the context lines in the order they appear in
 712                # the hunk. But then it is not possible to correctly stage only
 713                # "-old 1" and "+new 1" - it would result in this staged text:
 714                #
 715                #    context before
 716                #    old 2
 717                #    new 1
 718                #    context after
 719                #
 720                # (By symmetry it is not possible to *un*stage "old 2" and "new
 721                # 2".)
 722                #
 723                # We resolve the problem by introducing an asymmetry, namely,
 724                # when a "+" line is *staged*, it is moved in front of the
 725                # context lines that are generated from the "-" lines that are
 726                # immediately before the "+" block. That is, we construct this
 727                # patch:
 728                #
 729                #    @@ -10,4 +10,5 @@
 730                #     context before
 731                #    +new 1
 732                #     old 1
 733                #     old 2
 734                #     context after
 735                #
 736                # But we do *not* treat "-" lines that are *un*staged in a
 737                # special way.
 738                #
 739                # With this asymmetry it is possible to stage the change "old
 740                # 1" -> "new 1" directly, and to stage the change "old 2" ->
 741                # "new 2" by first staging the entire hunk and then unstaging
 742                # the change "old 1" -> "new 1".
 743                #
 744                # Applying multiple lines adds complexity to the special
 745                # situation.  The pre_context must be moved after the entire
 746                # first block of consecutive staged "+" lines, so that
 747                # staging both additions gives the following patch:
 748                #
 749                #    @@ -10,4 +10,6 @@
 750                #     context before
 751                #    +new 1
 752                #    +new 2
 753                #     old 1
 754                #     old 2
 755                #     context after
 756
 757                # This is non-empty if and only if we are _staging_ changes;
 758                # then it accumulates the consecutive "-" lines (after
 759                # converting them to context lines) in order to be moved after
 760                # "+" change lines.
 761                set pre_context {}
 762
 763                set n 0
 764                set m 0
 765                set i_l [$ui_diff index "$i_l + 1 lines"]
 766                set patch {}
 767                while {[$ui_diff compare $i_l < "end - 1 chars"] &&
 768                       [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
 769                        set next_l [$ui_diff index "$i_l + 1 lines"]
 770                        set c1 [$ui_diff get $i_l]
 771                        if {[$ui_diff compare $first_l <= $i_l] &&
 772                            [$ui_diff compare $i_l < $last_l] &&
 773                            ($c1 eq {-} || $c1 eq {+})} {
 774                                # a line to stage/unstage
 775                                set ln [$ui_diff get $i_l $next_l]
 776                                if {$c1 eq {-}} {
 777                                        set n [expr $n+1]
 778                                        set patch "$patch$pre_context$ln"
 779                                        set pre_context {}
 780                                } else {
 781                                        set m [expr $m+1]
 782                                        set patch "$patch$ln"
 783                                }
 784                        } elseif {$c1 ne {-} && $c1 ne {+}} {
 785                                # context line
 786                                set ln [$ui_diff get $i_l $next_l]
 787                                set patch "$patch$pre_context$ln"
 788                                # Skip the "\ No newline at end of
 789                                # file". Depending on the locale setting
 790                                # we don't know what this line looks
 791                                # like exactly. The only thing we do
 792                                # know is that it starts with "\ "
 793                                if {![string match {\\ *} $ln]} {
 794                                        set n [expr $n+1]
 795                                        set m [expr $m+1]
 796                                }
 797                                set pre_context {}
 798                        } elseif {$c1 eq $to_context} {
 799                                # turn change line into context line
 800                                set ln [$ui_diff get "$i_l + 1 chars" $next_l]
 801                                if {$c1 eq {-}} {
 802                                        set pre_context "$pre_context $ln"
 803                                } else {
 804                                        set patch "$patch $ln"
 805                                }
 806                                set n [expr $n+1]
 807                                set m [expr $m+1]
 808                        } else {
 809                                # a change in the opposite direction of
 810                                # to_context which is outside the range of
 811                                # lines to apply.
 812                                set patch "$patch$pre_context"
 813                                set pre_context {}
 814                        }
 815                        set i_l $next_l
 816                }
 817                set patch "$patch$pre_context"
 818                set wholepatch "$wholepatch@@ -$hln,$n +$hln,$m @@\n$patch"
 819                set first_l [$ui_diff index "$next_l + 1 lines"]
 820        }
 821
 822        if {[catch {
 823                set enc [get_path_encoding $current_diff_path]
 824                set p [eval git_write $apply_cmd]
 825                fconfigure $p -translation binary -encoding $enc
 826                puts -nonewline $p $current_diff_header
 827                puts -nonewline $p $wholepatch
 828                close $p} err]} {
 829                error_popup [append $failed_msg "\n\n$err"]
 830        }
 831
 832        unlock_index
 833}