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