2 # Copyright (C) 2006, 2007 Shawn Pearce
5 global ui_diff current_diff_path current_diff_header
6 global ui_index ui_workdir
8 $ui_diff conf -state normal
9 $ui_diff delete 0.0 end
10 $ui_diff conf -state disabled
12 set current_diff_path {}
13 set current_diff_header {}
15 $ui_index tag remove in_diff 0.0 end
16 $ui_workdir tag remove in_diff 0.0 end
20 global file_states file_lists
21 global current_diff_path current_diff_side
24 set p $current_diff_path
26 # No diff is being shown.
27 } elseif {$current_diff_side eq {}} {
29 } elseif {[catch {set s $file_states($p)}]
30 || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} {
32 if {[find_next_diff $current_diff_side $p {} {[^O]}]} {
38 set save_pos [lindex [$ui_diff yview] 0]
39 show_diff $p $current_diff_side {} $save_pos
43 proc handle_empty_diff {} {
44 global current_diff_path file_states file_lists
46 set path $current_diff_path
47 set s $file_states($path)
48 if {[lindex $s 0] ne {_M}} return
50 info_popup [mc "No differences detected.
54 The modification date of this file was updated by another application, but the content within the file was not changed.
56 A rescan will be automatically started to find other files which may have the same state." [short_path $path]]
63 proc show_diff {path w {lno {}} {scroll_pos {}}} {
64 global file_states file_lists
65 global is_3way_diff diff_active repo_config
66 global ui_diff ui_index ui_workdir
67 global current_diff_path current_diff_side current_diff_header
69 if {$diff_active || ![lock_index read]} return
73 set lno [lsearch -sorted -exact $file_lists($w) $path]
79 $w tag add in_diff $lno.0 [expr {$lno + 1}].0
83 set s $file_states($path)
87 set current_diff_path $path
88 set current_diff_side $w
89 set current_diff_header {}
90 ui_status [mc "Loading diff of %s..." [escape_path $path]]
92 # - Git won't give us the diff, there's nothing to compare to!
95 set max_sz [expr {128 * 1024}]
98 set type [file type $path]
106 set content [file readlink $path]
107 set sz [string length $content]
110 set fd [open $path r]
111 fconfigure $fd -eofchar {}
112 set content [read $fd $max_sz]
114 set sz [file size $path]
117 error "'$type' not supported"
123 ui_status [mc "Unable to display %s" [escape_path $path]]
124 error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
127 $ui_diff conf -state normal
128 if {$type eq {submodule}} {
129 $ui_diff insert end [append \
131 [mc "Git Repository (subproject)"] \
133 } elseif {![catch {set type [exec file $path]}]} {
134 set n [string length $path]
135 if {[string equal -length $n $path $type]} {
136 set type [string range $type $n end]
137 regsub {^:?\s*} $type {} type
139 $ui_diff insert end "* $type\n" d_@
141 if {[string first "\0" $content] != -1} {
142 $ui_diff insert end \
143 [mc "* Binary file (not showing content)."] \
147 $ui_diff insert end \
148 "* Untracked file is $sz bytes.
149 * Showing only first $max_sz bytes.
152 $ui_diff insert end $content
154 $ui_diff insert end "
155 * Untracked file clipped here by [appname].
156 * To see the entire file, use an external editor.
160 $ui_diff conf -state disabled
163 if {$scroll_pos ne {}} {
165 $ui_diff yview moveto $scroll_pos
172 if {$w eq $ui_index} {
173 lappend cmd diff-index
175 } elseif {$w eq $ui_workdir} {
176 if {[string first {U} $m] >= 0} {
179 lappend cmd diff-files
184 lappend cmd --no-color
185 if {$repo_config(gui.diffcontext) >= 1} {
186 lappend cmd "-U$repo_config(gui.diffcontext)"
188 if {$w eq $ui_index} {
194 if {[catch {set fd [eval git_read --nice $cmd]} err]} {
197 ui_status [mc "Unable to display %s" [escape_path $path]]
198 error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
206 fileevent $fd readable [list read_diff $fd $scroll_pos]
209 proc read_diff {fd scroll_pos} {
210 global ui_diff diff_active
211 global is_3way_diff current_diff_header
213 $ui_diff conf -state normal
214 while {[gets $fd line] >= 0} {
215 # -- Cleanup uninteresting diff header lines.
217 if { [string match {diff --git *} $line]
218 || [string match {diff --cc *} $line]
219 || [string match {diff --combined *} $line]
220 || [string match {--- *} $line]
221 || [string match {+++ *} $line]} {
222 append current_diff_header $line "\n"
225 if {[string match {index *} $line]} continue
226 if {$line eq {deleted file mode 120000}} {
227 set line "deleted symlink"
230 # -- Automatically detect if this is a 3 way diff.
232 if {[string match {@@@ *} $line]} {set is_3way_diff 1}
234 if {[string match {mode *} $line]
235 || [string match {new file *} $line]
236 || [regexp {^(old|new) mode *} $line]
237 || [string match {deleted file *} $line]
238 || [string match {deleted symlink} $line]
239 || [string match {Binary files * and * differ} $line]
240 || $line eq {\ No newline at end of file}
241 || [regexp {^\* Unmerged path } $line]} {
243 } elseif {$is_3way_diff} {
244 set op [string range $line 0 1]
254 if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
255 set line [string replace $line 0 1 { }]
262 puts "error: Unhandled 3 way diff marker: {$op}"
267 set op [string index $line 0]
273 if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
274 set line [string replace $line 0 0 { }]
281 puts "error: Unhandled 2 way diff marker: {$op}"
286 $ui_diff insert end $line $tags
287 if {[string index $line end] eq "\r"} {
288 $ui_diff tag add d_cr {end - 2c}
290 $ui_diff insert end "\n" $tags
292 $ui_diff conf -state disabled
298 if {$scroll_pos ne {}} {
300 $ui_diff yview moveto $scroll_pos
304 if {[$ui_diff index end] eq {2.0}} {
310 proc apply_hunk {x y} {
311 global current_diff_path current_diff_header current_diff_side
312 global ui_diff ui_index file_states
314 if {$current_diff_path eq {} || $current_diff_header eq {}} return
315 if {![lock_index apply_hunk]} return
317 set apply_cmd {apply --cached --whitespace=nowarn}
318 set mi [lindex $file_states($current_diff_path) 0]
319 if {$current_diff_side eq $ui_index} {
320 set failed_msg [mc "Failed to unstage selected hunk."]
321 lappend apply_cmd --reverse
322 if {[string index $mi 0] ne {M}} {
327 set failed_msg [mc "Failed to stage selected hunk."]
328 if {[string index $mi 1] ne {M}} {
334 set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
335 set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
341 set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
347 set p [eval git_write $apply_cmd]
348 fconfigure $p -translation binary -encoding binary
349 puts -nonewline $p $current_diff_header
350 puts -nonewline $p [$ui_diff get $s_lno $e_lno]
352 error_popup [append $failed_msg "\n\n$err"]
357 $ui_diff conf -state normal
358 $ui_diff delete $s_lno $e_lno
359 $ui_diff conf -state disabled
361 if {[$ui_diff get 1.0 end] eq "\n"} {
367 if {$current_diff_side eq $ui_index} {
369 } elseif {[string index $mi 0] eq {_}} {
375 display_file $current_diff_path $mi
376 # This should trigger shift to the next changed file
382 proc apply_line {x y} {
383 global current_diff_path current_diff_header current_diff_side
384 global ui_diff ui_index file_states
386 if {$current_diff_path eq {} || $current_diff_header eq {}} return
387 if {![lock_index apply_hunk]} return
389 set apply_cmd {apply --cached --whitespace=nowarn}
390 set mi [lindex $file_states($current_diff_path) 0]
391 if {$current_diff_side eq $ui_index} {
392 set failed_msg [mc "Failed to unstage selected line."]
394 lappend apply_cmd --reverse
395 if {[string index $mi 0] ne {M}} {
400 set failed_msg [mc "Failed to stage selected line."]
402 if {[string index $mi 1] ne {M}} {
408 set the_l [$ui_diff index @$x,$y]
410 # operate only on change lines
411 set c1 [$ui_diff get "$the_l linestart"]
412 if {$c1 ne {+} && $c1 ne {-}} {
418 set i_l [$ui_diff search -backwards -regexp ^@@ $the_l 0.0]
423 # $i_l is now at the beginning of a line
425 # pick start line number from hunk header
426 set hh [$ui_diff get $i_l "$i_l + 1 lines"]
427 set hh [lindex [split $hh ,] 0]
428 set hln [lindex [split $hh -] 1]
430 # There is a special situation to take care of. Consider this hunk:
440 # We used to keep the context lines in the order they appear in the
441 # hunk. But then it is not possible to correctly stage only
442 # "-old 1" and "+new 1" - it would result in this staged text:
449 # (By symmetry it is not possible to *un*stage "old 2" and "new 2".)
451 # We resolve the problem by introducing an asymmetry, namely, when
452 # a "+" line is *staged*, it is moved in front of the context lines
453 # that are generated from the "-" lines that are immediately before
454 # the "+" block. That is, we construct this patch:
463 # But we do *not* treat "-" lines that are *un*staged in a special
466 # With this asymmetry it is possible to stage the change
467 # "old 1" -> "new 1" directly, and to stage the change
468 # "old 2" -> "new 2" by first staging the entire hunk and
469 # then unstaging the change "old 1" -> "new 1".
471 # This is non-empty if and only if we are _staging_ changes;
472 # then it accumulates the consecutive "-" lines (after converting
473 # them to context lines) in order to be moved after the "+" change
478 set i_l [$ui_diff index "$i_l + 1 lines"]
480 while {[$ui_diff compare $i_l < "end - 1 chars"] &&
481 [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
482 set next_l [$ui_diff index "$i_l + 1 lines"]
483 set c1 [$ui_diff get $i_l]
484 if {[$ui_diff compare $i_l <= $the_l] &&
485 [$ui_diff compare $the_l < $next_l]} {
486 # the line to stage/unstage
487 set ln [$ui_diff get $i_l $next_l]
490 set patch "$patch$pre_context$ln"
492 set patch "$patch$ln$pre_context"
495 } elseif {$c1 ne {-} && $c1 ne {+}} {
497 set ln [$ui_diff get $i_l $next_l]
498 set patch "$patch$pre_context$ln"
501 } elseif {$c1 eq $to_context} {
502 # turn change line into context line
503 set ln [$ui_diff get "$i_l + 1 chars" $next_l]
505 set pre_context "$pre_context $ln"
507 set patch "$patch $ln"
513 set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch"
516 set p [eval git_write $apply_cmd]
517 fconfigure $p -translation binary -encoding binary
518 puts -nonewline $p $current_diff_header
519 puts -nonewline $p $patch
521 error_popup [append $failed_msg "\n\n$err"]