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