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