lib / diff.tclon commit git-gui: allow reverting selected hunk (62bd999)
   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_or_revert_hunk {x y revert} {
 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 --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 --cached
 582                if {[string index $mi 0] ne {M}} {
 583                        unlock_index
 584                        return
 585                }
 586        } else {
 587                if {$revert} {
 588                        set failed_msg [mc "Failed to revert selected hunk."]
 589                        lappend apply_cmd --reverse
 590                } else {
 591                        set failed_msg [mc "Failed to stage selected hunk."]
 592                        lappend apply_cmd --cached
 593                }
 594
 595                if {[string index $mi 1] ne {M}} {
 596                        unlock_index
 597                        return
 598                }
 599        }
 600
 601        set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
 602        set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
 603        if {$s_lno eq {}} {
 604                unlock_index
 605                return
 606        }
 607
 608        set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
 609        if {$e_lno eq {}} {
 610                set e_lno end
 611        }
 612
 613        if {[catch {
 614                set enc [get_path_encoding $current_diff_path]
 615                set p [eval git_write $apply_cmd]
 616                fconfigure $p -translation binary -encoding $enc
 617                puts -nonewline $p $current_diff_header
 618                puts -nonewline $p [$ui_diff get $s_lno $e_lno]
 619                close $p} err]} {
 620                error_popup "$failed_msg\n\n$err"
 621                unlock_index
 622                return
 623        }
 624
 625        $ui_diff conf -state normal
 626        $ui_diff delete $s_lno $e_lno
 627        $ui_diff conf -state disabled
 628
 629        # Check if the hunk was the last one in the file.
 630        if {[$ui_diff get 1.0 end] eq "\n"} {
 631                set o _
 632        } else {
 633                set o ?
 634        }
 635
 636        # Update the status flags.
 637        if {$revert} {
 638                set mi [string index $mi 0]$o
 639        } elseif {$current_diff_side eq $ui_index} {
 640                set mi ${o}M
 641        } elseif {[string index $mi 0] eq {_}} {
 642                set mi M$o
 643        } else {
 644                set mi ?$o
 645        }
 646        unlock_index
 647        display_file $current_diff_path $mi
 648        # This should trigger shift to the next changed file
 649        if {$o eq {_}} {
 650                reshow_diff
 651        }
 652}
 653
 654proc apply_or_revert_range_or_line {x y revert} {
 655        global current_diff_path current_diff_header current_diff_side
 656        global ui_diff ui_index file_states
 657
 658        set selected [$ui_diff tag nextrange sel 0.0]
 659
 660        if {$selected == {}} {
 661                set first [$ui_diff index "@$x,$y"]
 662                set last $first
 663        } else {
 664                set first [lindex $selected 0]
 665                set last [lindex $selected 1]
 666        }
 667
 668        set first_l [$ui_diff index "$first linestart"]
 669        set last_l [$ui_diff index "$last lineend"]
 670
 671        if {$current_diff_path eq {} || $current_diff_header eq {}} return
 672        if {![lock_index apply_hunk]} return
 673
 674        set apply_cmd {apply --whitespace=nowarn}
 675        set mi [lindex $file_states($current_diff_path) 0]
 676        if {$current_diff_side eq $ui_index} {
 677                set failed_msg [mc "Failed to unstage selected line."]
 678                set to_context {+}
 679                lappend apply_cmd --reverse --cached
 680                if {[string index $mi 0] ne {M}} {
 681                        unlock_index
 682                        return
 683                }
 684        } else {
 685                if {$revert} {
 686                        set failed_msg [mc "Failed to revert selected line."]
 687                        set to_context {+}
 688                        lappend apply_cmd --reverse
 689                } else {
 690                        set failed_msg [mc "Failed to stage selected line."]
 691                        set to_context {-}
 692                        lappend apply_cmd --cached
 693                }
 694
 695                if {[string index $mi 1] ne {M}} {
 696                        unlock_index
 697                        return
 698                }
 699        }
 700
 701        set wholepatch {}
 702
 703        while {$first_l < $last_l} {
 704                set i_l [$ui_diff search -backwards -regexp ^@@ $first_l 0.0]
 705                if {$i_l eq {}} {
 706                        # If there's not a @@ above, then the selected range
 707                        # must have come before the first_l @@
 708                        set i_l [$ui_diff search -regexp ^@@ $first_l $last_l]
 709                }
 710                if {$i_l eq {}} {
 711                        unlock_index
 712                        return
 713                }
 714                # $i_l is now at the beginning of a line
 715
 716                # pick start line number from hunk header
 717                set hh [$ui_diff get $i_l "$i_l + 1 lines"]
 718                set hh [lindex [split $hh ,] 0]
 719                set hln [lindex [split $hh -] 1]
 720
 721                # There is a special situation to take care of. Consider this
 722                # hunk:
 723                #
 724                #    @@ -10,4 +10,4 @@
 725                #     context before
 726                #    -old 1
 727                #    -old 2
 728                #    +new 1
 729                #    +new 2
 730                #     context after
 731                #
 732                # We used to keep the context lines in the order they appear in
 733                # the hunk. But then it is not possible to correctly stage only
 734                # "-old 1" and "+new 1" - it would result in this staged text:
 735                #
 736                #    context before
 737                #    old 2
 738                #    new 1
 739                #    context after
 740                #
 741                # (By symmetry it is not possible to *un*stage "old 2" and "new
 742                # 2".)
 743                #
 744                # We resolve the problem by introducing an asymmetry, namely,
 745                # when a "+" line is *staged*, it is moved in front of the
 746                # context lines that are generated from the "-" lines that are
 747                # immediately before the "+" block. That is, we construct this
 748                # patch:
 749                #
 750                #    @@ -10,4 +10,5 @@
 751                #     context before
 752                #    +new 1
 753                #     old 1
 754                #     old 2
 755                #     context after
 756                #
 757                # But we do *not* treat "-" lines that are *un*staged in a
 758                # special way.
 759                #
 760                # With this asymmetry it is possible to stage the change "old
 761                # 1" -> "new 1" directly, and to stage the change "old 2" ->
 762                # "new 2" by first staging the entire hunk and then unstaging
 763                # the change "old 1" -> "new 1".
 764                #
 765                # Applying multiple lines adds complexity to the special
 766                # situation.  The pre_context must be moved after the entire
 767                # first block of consecutive staged "+" lines, so that
 768                # staging both additions gives the following patch:
 769                #
 770                #    @@ -10,4 +10,6 @@
 771                #     context before
 772                #    +new 1
 773                #    +new 2
 774                #     old 1
 775                #     old 2
 776                #     context after
 777
 778                # This is non-empty if and only if we are _staging_ changes;
 779                # then it accumulates the consecutive "-" lines (after
 780                # converting them to context lines) in order to be moved after
 781                # "+" change lines.
 782                set pre_context {}
 783
 784                set n 0
 785                set m 0
 786                set i_l [$ui_diff index "$i_l + 1 lines"]
 787                set patch {}
 788                while {[$ui_diff compare $i_l < "end - 1 chars"] &&
 789                       [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
 790                        set next_l [$ui_diff index "$i_l + 1 lines"]
 791                        set c1 [$ui_diff get $i_l]
 792                        if {[$ui_diff compare $first_l <= $i_l] &&
 793                            [$ui_diff compare $i_l < $last_l] &&
 794                            ($c1 eq {-} || $c1 eq {+})} {
 795                                # a line to stage/unstage
 796                                set ln [$ui_diff get $i_l $next_l]
 797                                if {$c1 eq {-}} {
 798                                        set n [expr $n+1]
 799                                        set patch "$patch$pre_context$ln"
 800                                        set pre_context {}
 801                                } else {
 802                                        set m [expr $m+1]
 803                                        set patch "$patch$ln"
 804                                }
 805                        } elseif {$c1 ne {-} && $c1 ne {+}} {
 806                                # context line
 807                                set ln [$ui_diff get $i_l $next_l]
 808                                set patch "$patch$pre_context$ln"
 809                                # Skip the "\ No newline at end of
 810                                # file". Depending on the locale setting
 811                                # we don't know what this line looks
 812                                # like exactly. The only thing we do
 813                                # know is that it starts with "\ "
 814                                if {![string match {\\ *} $ln]} {
 815                                        set n [expr $n+1]
 816                                        set m [expr $m+1]
 817                                }
 818                                set pre_context {}
 819                        } elseif {$c1 eq $to_context} {
 820                                # turn change line into context line
 821                                set ln [$ui_diff get "$i_l + 1 chars" $next_l]
 822                                if {$c1 eq {-}} {
 823                                        set pre_context "$pre_context $ln"
 824                                } else {
 825                                        set patch "$patch $ln"
 826                                }
 827                                set n [expr $n+1]
 828                                set m [expr $m+1]
 829                        } else {
 830                                # a change in the opposite direction of
 831                                # to_context which is outside the range of
 832                                # lines to apply.
 833                                set patch "$patch$pre_context"
 834                                set pre_context {}
 835                        }
 836                        set i_l $next_l
 837                }
 838                set patch "$patch$pre_context"
 839                set wholepatch "$wholepatch@@ -$hln,$n +$hln,$m @@\n$patch"
 840                set first_l [$ui_diff index "$next_l + 1 lines"]
 841        }
 842
 843        if {[catch {
 844                set enc [get_path_encoding $current_diff_path]
 845                set p [eval git_write $apply_cmd]
 846                fconfigure $p -translation binary -encoding $enc
 847                puts -nonewline $p $current_diff_header
 848                puts -nonewline $p $wholepatch
 849                close $p} err]} {
 850                error_popup "$failed_msg\n\n$err"
 851        }
 852
 853        unlock_index
 854}