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