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