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