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