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