git-gui / lib / diff.tclon commit Merge branch 'nd/maint-verify-objects' (0f559f5)
   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        } elseif {$w eq $ui_workdir} {
 291                if {[string first {U} $m] >= 0} {
 292                        lappend cmd diff
 293                } else {
 294                        lappend cmd diff-files
 295                }
 296        }
 297        if {![is_config_false gui.textconv] && [git-version >= 1.6.1]} {
 298                lappend cmd --textconv
 299        }
 300
 301        if {[string match {160000 *} [lindex $s 2]]
 302         || [string match {160000 *} [lindex $s 3]]} {
 303                set is_submodule_diff 1
 304
 305                if {[git-version >= "1.6.6"]} {
 306                        lappend cmd --submodule
 307                }
 308        }
 309
 310        lappend cmd -p
 311        lappend cmd --color
 312        set cmd [concat $cmd $repo_config(gui.diffopts)]
 313        if {$repo_config(gui.diffcontext) >= 1} {
 314                lappend cmd "-U$repo_config(gui.diffcontext)"
 315        }
 316        if {$w eq $ui_index} {
 317                lappend cmd [PARENT]
 318        }
 319        if {$add_opts ne {}} {
 320                eval lappend cmd $add_opts
 321        } else {
 322                lappend cmd --
 323                lappend cmd $path
 324        }
 325
 326        if {$is_submodule_diff && [git-version < "1.6.6"]} {
 327                if {$w eq $ui_index} {
 328                        set cmd [list submodule summary --cached -- $path]
 329                } else {
 330                        set cmd [list submodule summary --files -- $path]
 331                }
 332        }
 333
 334        if {[catch {set fd [eval git_read --nice $cmd]} err]} {
 335                set diff_active 0
 336                unlock_index
 337                ui_status [mc "Unable to display %s" [escape_path $path]]
 338                error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
 339                return
 340        }
 341
 342        set ::current_diff_inheader 1
 343        fconfigure $fd \
 344                -blocking 0 \
 345                -encoding [get_path_encoding $path] \
 346                -translation lf
 347        fileevent $fd readable [list read_diff $fd $conflict_size $cont_info]
 348}
 349
 350proc parse_color_line {line} {
 351        set start 0
 352        set result ""
 353        set markup [list]
 354        set regexp {\033\[((?:\d+;)*\d+)?m}
 355        set need_reset 0
 356        while {[regexp -indices -start $start $regexp $line match code]} {
 357                foreach {begin end} $match break
 358                append result [string range $line $start [expr {$begin - 1}]]
 359                set pos [string length $result]
 360                set col [eval [linsert $code 0 string range $line]]
 361                set start [incr end]
 362                if {$col eq "0" || $col eq ""} {
 363                        if {!$need_reset} continue
 364                        set need_reset 0
 365                } else {
 366                        set need_reset 1
 367                }
 368                lappend markup $pos $col
 369        }
 370        append result [string range $line $start end]
 371        if {[llength $markup] < 4} {set markup {}}
 372        return [list $result $markup]
 373}
 374
 375proc read_diff {fd conflict_size cont_info} {
 376        global ui_diff diff_active is_submodule_diff
 377        global is_3way_diff is_conflict_diff current_diff_header
 378        global current_diff_queue
 379        global diff_empty_count
 380
 381        $ui_diff conf -state normal
 382        while {[gets $fd line] >= 0} {
 383                foreach {line markup} [parse_color_line $line] break
 384                set line [string map {\033 ^} $line]
 385
 386                set tags {}
 387
 388                # -- Check for start of diff header.
 389                if {   [string match {diff --git *}      $line]
 390                    || [string match {diff --cc *}       $line]
 391                    || [string match {diff --combined *} $line]} {
 392                        set ::current_diff_inheader 1
 393                }
 394
 395                # -- Check for end of diff header (any hunk line will do this).
 396                #
 397                if {[regexp {^@@+ } $line]} {set ::current_diff_inheader 0}
 398
 399                # -- Automatically detect if this is a 3 way diff.
 400                #
 401                if {[string match {@@@ *} $line]} {set is_3way_diff 1}
 402
 403                if {$::current_diff_inheader} {
 404
 405                        # -- These two lines stop a diff header and shouldn't be in there
 406                        if {   [string match {Binary files * and * differ} $line]
 407                            || [regexp {^\* Unmerged path }                $line]} {
 408                                set ::current_diff_inheader 0
 409                        } else {
 410                                append current_diff_header $line "\n"
 411                        }
 412
 413                        # -- Cleanup uninteresting diff header lines.
 414                        #
 415                        if {   [string match {diff --git *}      $line]
 416                            || [string match {diff --cc *}       $line]
 417                            || [string match {diff --combined *} $line]
 418                            || [string match {--- *}             $line]
 419                            || [string match {+++ *}             $line]
 420                            || [string match {index *}           $line]} {
 421                                continue
 422                        }
 423
 424                        # -- Name it symlink, not 120000
 425                        #    Note, that the original line is in $current_diff_header
 426                        regsub {^(deleted|new) file mode 120000} $line {\1 symlink} line
 427
 428                } elseif {   $line eq {\ No newline at end of file}} {
 429                        # -- Handle some special lines
 430                } elseif {$is_3way_diff} {
 431                        set op [string range $line 0 1]
 432                        switch -- $op {
 433                        {  } {set tags {}}
 434                        {@@} {set tags d_@}
 435                        { +} {set tags d_s+}
 436                        { -} {set tags d_s-}
 437                        {+ } {set tags d_+s}
 438                        {- } {set tags d_-s}
 439                        {--} {set tags d_--}
 440                        {++} {
 441                                set regexp [string map [list %conflict_size $conflict_size]\
 442                                                                {^\+\+([<>=]){%conflict_size}(?: |$)}]
 443                                if {[regexp $regexp $line _g op]} {
 444                                        set is_conflict_diff 1
 445                                        set line [string replace $line 0 1 {  }]
 446                                        set tags d$op
 447                                } else {
 448                                        set tags d_++
 449                                }
 450                        }
 451                        default {
 452                                puts "error: Unhandled 3 way diff marker: {$op}"
 453                                set tags {}
 454                        }
 455                        }
 456                } elseif {$is_submodule_diff} {
 457                        if {$line == ""} continue
 458                        if {[regexp {^Submodule } $line]} {
 459                                set tags d_info
 460                        } elseif {[regexp {^\* } $line]} {
 461                                set line [string replace $line 0 1 {Submodule }]
 462                                set tags d_info
 463                        } else {
 464                                set op [string range $line 0 2]
 465                                switch -- $op {
 466                                {  <} {set tags d_-}
 467                                {  >} {set tags d_+}
 468                                {  W} {set tags {}}
 469                                default {
 470                                        puts "error: Unhandled submodule diff marker: {$op}"
 471                                        set tags {}
 472                                }
 473                                }
 474                        }
 475                } else {
 476                        set op [string index $line 0]
 477                        switch -- $op {
 478                        { } {set tags {}}
 479                        {@} {set tags d_@}
 480                        {-} {set tags d_-}
 481                        {+} {
 482                                set regexp [string map [list %conflict_size $conflict_size]\
 483                                                                {^\+([<>=]){%conflict_size}(?: |$)}]
 484                                if {[regexp $regexp $line _g op]} {
 485                                        set is_conflict_diff 1
 486                                        set tags d$op
 487                                } else {
 488                                        set tags d_+
 489                                }
 490                        }
 491                        default {
 492                                puts "error: Unhandled 2 way diff marker: {$op}"
 493                                set tags {}
 494                        }
 495                        }
 496                }
 497                set mark [$ui_diff index "end - 1 line linestart"]
 498                $ui_diff insert end $line $tags
 499                if {[string index $line end] eq "\r"} {
 500                        $ui_diff tag add d_cr {end - 2c}
 501                }
 502                $ui_diff insert end "\n" $tags
 503
 504                foreach {posbegin colbegin posend colend} $markup {
 505                        set prefix clr
 506                        foreach style [lsort -integer [split $colbegin ";"]] {
 507                                if {$style eq "7"} {append prefix i; continue}
 508                                if {$style != 4 && ($style < 30 || $style > 47)} {continue}
 509                                set a "$mark linestart + $posbegin chars"
 510                                set b "$mark linestart + $posend chars"
 511                                catch {$ui_diff tag add $prefix$style $a $b}
 512                        }
 513                }
 514        }
 515        $ui_diff conf -state disabled
 516
 517        if {[eof $fd]} {
 518                close $fd
 519
 520                if {$current_diff_queue ne {}} {
 521                        advance_diff_queue $cont_info
 522                        return
 523                }
 524
 525                set diff_active 0
 526                unlock_index
 527                set scroll_pos [lindex $cont_info 0]
 528                if {$scroll_pos ne {}} {
 529                        update
 530                        $ui_diff yview moveto $scroll_pos
 531                }
 532                ui_ready
 533
 534                if {[$ui_diff index end] eq {2.0}} {
 535                        handle_empty_diff
 536                } else {
 537                        set diff_empty_count 0
 538                }
 539
 540                set callback [lindex $cont_info 1]
 541                if {$callback ne {}} {
 542                        eval $callback
 543                }
 544        }
 545}
 546
 547proc apply_hunk {x y} {
 548        global current_diff_path current_diff_header current_diff_side
 549        global ui_diff ui_index file_states
 550
 551        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 552        if {![lock_index apply_hunk]} return
 553
 554        set apply_cmd {apply --cached --whitespace=nowarn}
 555        set mi [lindex $file_states($current_diff_path) 0]
 556        if {$current_diff_side eq $ui_index} {
 557                set failed_msg [mc "Failed to unstage selected hunk."]
 558                lappend apply_cmd --reverse
 559                if {[string index $mi 0] ne {M}} {
 560                        unlock_index
 561                        return
 562                }
 563        } else {
 564                set failed_msg [mc "Failed to stage selected hunk."]
 565                if {[string index $mi 1] ne {M}} {
 566                        unlock_index
 567                        return
 568                }
 569        }
 570
 571        set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
 572        set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
 573        if {$s_lno eq {}} {
 574                unlock_index
 575                return
 576        }
 577
 578        set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
 579        if {$e_lno eq {}} {
 580                set e_lno end
 581        }
 582
 583        if {[catch {
 584                set enc [get_path_encoding $current_diff_path]
 585                set p [eval git_write $apply_cmd]
 586                fconfigure $p -translation binary -encoding $enc
 587                puts -nonewline $p $current_diff_header
 588                puts -nonewline $p [$ui_diff get $s_lno $e_lno]
 589                close $p} err]} {
 590                error_popup [append $failed_msg "\n\n$err"]
 591                unlock_index
 592                return
 593        }
 594
 595        $ui_diff conf -state normal
 596        $ui_diff delete $s_lno $e_lno
 597        $ui_diff conf -state disabled
 598
 599        if {[$ui_diff get 1.0 end] eq "\n"} {
 600                set o _
 601        } else {
 602                set o ?
 603        }
 604
 605        if {$current_diff_side eq $ui_index} {
 606                set mi ${o}M
 607        } elseif {[string index $mi 0] eq {_}} {
 608                set mi M$o
 609        } else {
 610                set mi ?$o
 611        }
 612        unlock_index
 613        display_file $current_diff_path $mi
 614        # This should trigger shift to the next changed file
 615        if {$o eq {_}} {
 616                reshow_diff
 617        }
 618}
 619
 620proc apply_range_or_line {x y} {
 621        global current_diff_path current_diff_header current_diff_side
 622        global ui_diff ui_index file_states
 623
 624        set selected [$ui_diff tag nextrange sel 0.0]
 625
 626        if {$selected == {}} {
 627                set first [$ui_diff index "@$x,$y"]
 628                set last $first
 629        } else {
 630                set first [lindex $selected 0]
 631                set last [lindex $selected 1]
 632        }
 633
 634        set first_l [$ui_diff index "$first linestart"]
 635        set last_l [$ui_diff index "$last lineend"]
 636
 637        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 638        if {![lock_index apply_hunk]} return
 639
 640        set apply_cmd {apply --cached --whitespace=nowarn}
 641        set mi [lindex $file_states($current_diff_path) 0]
 642        if {$current_diff_side eq $ui_index} {
 643                set failed_msg [mc "Failed to unstage selected line."]
 644                set to_context {+}
 645                lappend apply_cmd --reverse
 646                if {[string index $mi 0] ne {M}} {
 647                        unlock_index
 648                        return
 649                }
 650        } else {
 651                set failed_msg [mc "Failed to stage selected line."]
 652                set to_context {-}
 653                if {[string index $mi 1] ne {M}} {
 654                        unlock_index
 655                        return
 656                }
 657        }
 658
 659        set wholepatch {}
 660
 661        while {$first_l < $last_l} {
 662                set i_l [$ui_diff search -backwards -regexp ^@@ $first_l 0.0]
 663                if {$i_l eq {}} {
 664                        # If there's not a @@ above, then the selected range
 665                        # must have come before the first_l @@
 666                        set i_l [$ui_diff search -regexp ^@@ $first_l $last_l]
 667                }
 668                if {$i_l eq {}} {
 669                        unlock_index
 670                        return
 671                }
 672                # $i_l is now at the beginning of a line
 673
 674                # pick start line number from hunk header
 675                set hh [$ui_diff get $i_l "$i_l + 1 lines"]
 676                set hh [lindex [split $hh ,] 0]
 677                set hln [lindex [split $hh -] 1]
 678
 679                # There is a special situation to take care of. Consider this
 680                # hunk:
 681                #
 682                #    @@ -10,4 +10,4 @@
 683                #     context before
 684                #    -old 1
 685                #    -old 2
 686                #    +new 1
 687                #    +new 2
 688                #     context after
 689                #
 690                # We used to keep the context lines in the order they appear in
 691                # the hunk. But then it is not possible to correctly stage only
 692                # "-old 1" and "+new 1" - it would result in this staged text:
 693                #
 694                #    context before
 695                #    old 2
 696                #    new 1
 697                #    context after
 698                #
 699                # (By symmetry it is not possible to *un*stage "old 2" and "new
 700                # 2".)
 701                #
 702                # We resolve the problem by introducing an asymmetry, namely,
 703                # when a "+" line is *staged*, it is moved in front of the
 704                # context lines that are generated from the "-" lines that are
 705                # immediately before the "+" block. That is, we construct this
 706                # patch:
 707                #
 708                #    @@ -10,4 +10,5 @@
 709                #     context before
 710                #    +new 1
 711                #     old 1
 712                #     old 2
 713                #     context after
 714                #
 715                # But we do *not* treat "-" lines that are *un*staged in a
 716                # special way.
 717                #
 718                # With this asymmetry it is possible to stage the change "old
 719                # 1" -> "new 1" directly, and to stage the change "old 2" ->
 720                # "new 2" by first staging the entire hunk and then unstaging
 721                # the change "old 1" -> "new 1".
 722                #
 723                # Applying multiple lines adds complexity to the special
 724                # situation.  The pre_context must be moved after the entire
 725                # first block of consecutive staged "+" lines, so that
 726                # staging both additions gives the following patch:
 727                #
 728                #    @@ -10,4 +10,6 @@
 729                #     context before
 730                #    +new 1
 731                #    +new 2
 732                #     old 1
 733                #     old 2
 734                #     context after
 735
 736                # This is non-empty if and only if we are _staging_ changes;
 737                # then it accumulates the consecutive "-" lines (after
 738                # converting them to context lines) in order to be moved after
 739                # "+" change lines.
 740                set pre_context {}
 741
 742                set n 0
 743                set m 0
 744                set i_l [$ui_diff index "$i_l + 1 lines"]
 745                set patch {}
 746                while {[$ui_diff compare $i_l < "end - 1 chars"] &&
 747                       [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
 748                        set next_l [$ui_diff index "$i_l + 1 lines"]
 749                        set c1 [$ui_diff get $i_l]
 750                        if {[$ui_diff compare $first_l <= $i_l] &&
 751                            [$ui_diff compare $i_l < $last_l] &&
 752                            ($c1 eq {-} || $c1 eq {+})} {
 753                                # a line to stage/unstage
 754                                set ln [$ui_diff get $i_l $next_l]
 755                                if {$c1 eq {-}} {
 756                                        set n [expr $n+1]
 757                                        set patch "$patch$pre_context$ln"
 758                                        set pre_context {}
 759                                } else {
 760                                        set m [expr $m+1]
 761                                        set patch "$patch$ln"
 762                                }
 763                        } elseif {$c1 ne {-} && $c1 ne {+}} {
 764                                # context line
 765                                set ln [$ui_diff get $i_l $next_l]
 766                                set patch "$patch$pre_context$ln"
 767                                set n [expr $n+1]
 768                                set m [expr $m+1]
 769                                set pre_context {}
 770                        } elseif {$c1 eq $to_context} {
 771                                # turn change line into context line
 772                                set ln [$ui_diff get "$i_l + 1 chars" $next_l]
 773                                if {$c1 eq {-}} {
 774                                        set pre_context "$pre_context $ln"
 775                                } else {
 776                                        set patch "$patch $ln"
 777                                }
 778                                set n [expr $n+1]
 779                                set m [expr $m+1]
 780                        } else {
 781                                # a change in the opposite direction of
 782                                # to_context which is outside the range of
 783                                # lines to apply.
 784                                set patch "$patch$pre_context"
 785                                set pre_context {}
 786                        }
 787                        set i_l $next_l
 788                }
 789                set patch "$patch$pre_context"
 790                set wholepatch "$wholepatch@@ -$hln,$n +$hln,$m @@\n$patch"
 791                set first_l [$ui_diff index "$next_l + 1 lines"]
 792        }
 793
 794        if {[catch {
 795                set enc [get_path_encoding $current_diff_path]
 796                set p [eval git_write $apply_cmd]
 797                fconfigure $p -translation binary -encoding $enc
 798                puts -nonewline $p $current_diff_header
 799                puts -nonewline $p $wholepatch
 800                close $p} err]} {
 801                error_popup [append $failed_msg "\n\n$err"]
 802        }
 803
 804        unlock_index
 805}