153437b18e15e31d310994df8e9f26cb563bfe2e
1# git-gui diff viewer
2# Copyright (C) 2006, 2007 Shawn Pearce
3
4proc clear_diff {} {
5 global ui_diff current_diff_path current_diff_header
6 global ui_index ui_workdir
7
8 $ui_diff conf -state normal
9 $ui_diff delete 0.0 end
10 $ui_diff conf -state disabled
11
12 set current_diff_path {}
13 set current_diff_header {}
14
15 $ui_index tag remove in_diff 0.0 end
16 $ui_workdir tag remove in_diff 0.0 end
17}
18
19proc reshow_diff {} {
20 global file_states file_lists
21 global current_diff_path current_diff_side
22 global ui_diff
23
24 set p $current_diff_path
25 if {$p eq {}} {
26 # No diff is being shown.
27 } elseif {$current_diff_side eq {}} {
28 clear_diff
29 } elseif {[catch {set s $file_states($p)}]
30 || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} {
31
32 if {[find_next_diff $current_diff_side $p {} {[^O]}]} {
33 next_diff
34 } else {
35 clear_diff
36 }
37 } else {
38 set save_pos [lindex [$ui_diff yview] 0]
39 show_diff $p $current_diff_side {} $save_pos
40 }
41}
42
43proc handle_empty_diff {} {
44 global current_diff_path file_states file_lists
45
46 set path $current_diff_path
47 set s $file_states($path)
48 if {[lindex $s 0] ne {_M}} return
49
50 info_popup [mc "No differences detected.
51
52%s has no changes.
53
54The modification date of this file was updated by another application, but the content within the file was not changed.
55
56A rescan will be automatically started to find other files which may have the same state." [short_path $path]]
57
58 clear_diff
59 display_file $path __
60 rescan ui_ready 0
61}
62
63proc 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
68
69 if {$diff_active || ![lock_index read]} return
70
71 clear_diff
72 if {$lno == {}} {
73 set lno [lsearch -sorted -exact $file_lists($w) $path]
74 if {$lno >= 0} {
75 incr lno
76 }
77 }
78 if {$lno >= 1} {
79 $w tag add in_diff $lno.0 [expr {$lno + 1}].0
80 $w see $lno.0
81 }
82
83 set s $file_states($path)
84 set m [lindex $s 0]
85 set is_3way_diff 0
86 set diff_active 1
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]]
91
92 # - Git won't give us the diff, there's nothing to compare to!
93 #
94 if {$m eq {_O}} {
95 set max_sz [expr {128 * 1024}]
96 set type unknown
97 if {[catch {
98 set type [file type $path]
99 switch -- $type {
100 directory {
101 set type submodule
102 set content {}
103 set sz 0
104 }
105 link {
106 set content [file readlink $path]
107 set sz [string length $content]
108 }
109 file {
110 set fd [open $path r]
111 fconfigure $fd -eofchar {}
112 set content [read $fd $max_sz]
113 close $fd
114 set sz [file size $path]
115 }
116 default {
117 error "'$type' not supported"
118 }
119 }
120 } err ]} {
121 set diff_active 0
122 unlock_index
123 ui_status [mc "Unable to display %s" [escape_path $path]]
124 error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
125 return
126 }
127 $ui_diff conf -state normal
128 if {$type eq {submodule}} {
129 $ui_diff insert end [append \
130 "* " \
131 [mc "Git Repository (subproject)"] \
132 "\n"] d_@
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
138 }
139 $ui_diff insert end "* $type\n" d_@
140 }
141 if {[string first "\0" $content] != -1} {
142 $ui_diff insert end \
143 [mc "* Binary file (not showing content)."] \
144 d_@
145 } else {
146 if {$sz > $max_sz} {
147 $ui_diff insert end \
148"* Untracked file is $sz bytes.
149* Showing only first $max_sz bytes.
150" d_@
151 }
152 $ui_diff insert end $content
153 if {$sz > $max_sz} {
154 $ui_diff insert end "
155* Untracked file clipped here by [appname].
156* To see the entire file, use an external editor.
157" d_@
158 }
159 }
160 $ui_diff conf -state disabled
161 set diff_active 0
162 unlock_index
163 if {$scroll_pos ne {}} {
164 update
165 $ui_diff yview moveto $scroll_pos
166 }
167 ui_ready
168 return
169 }
170
171 set cmd [list]
172 if {$w eq $ui_index} {
173 lappend cmd diff-index
174 lappend cmd --cached
175 } elseif {$w eq $ui_workdir} {
176 if {[string first {U} $m] >= 0} {
177 lappend cmd diff
178 } else {
179 lappend cmd diff-files
180 }
181 }
182
183 lappend cmd -p
184 lappend cmd --no-color
185 if {$repo_config(gui.diffcontext) >= 1} {
186 lappend cmd "-U$repo_config(gui.diffcontext)"
187 }
188 if {$w eq $ui_index} {
189 lappend cmd [PARENT]
190 }
191 lappend cmd --
192 lappend cmd $path
193
194 if {[catch {set fd [eval git_read --nice $cmd]} err]} {
195 set diff_active 0
196 unlock_index
197 ui_status [mc "Unable to display %s" [escape_path $path]]
198 error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
199 return
200 }
201
202 fconfigure $fd \
203 -blocking 0 \
204 -encoding binary \
205 -translation binary
206 fileevent $fd readable [list read_diff $fd $scroll_pos]
207}
208
209proc read_diff {fd scroll_pos} {
210 global ui_diff diff_active
211 global is_3way_diff current_diff_header
212
213 $ui_diff conf -state normal
214 while {[gets $fd line] >= 0} {
215 # -- Cleanup uninteresting diff header lines.
216 #
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"
223 continue
224 }
225 if {[string match {index *} $line]} continue
226 if {$line eq {deleted file mode 120000}} {
227 set line "deleted symlink"
228 }
229
230 # -- Automatically detect if this is a 3 way diff.
231 #
232 if {[string match {@@@ *} $line]} {set is_3way_diff 1}
233
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]} {
242 set tags {}
243 } elseif {$is_3way_diff} {
244 set op [string range $line 0 1]
245 switch -- $op {
246 { } {set tags {}}
247 {@@} {set tags d_@}
248 { +} {set tags d_s+}
249 { -} {set tags d_s-}
250 {+ } {set tags d_+s}
251 {- } {set tags d_-s}
252 {--} {set tags d_--}
253 {++} {
254 if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
255 set line [string replace $line 0 1 { }]
256 set tags d$op
257 } else {
258 set tags d_++
259 }
260 }
261 default {
262 puts "error: Unhandled 3 way diff marker: {$op}"
263 set tags {}
264 }
265 }
266 } else {
267 set op [string index $line 0]
268 switch -- $op {
269 { } {set tags {}}
270 {@} {set tags d_@}
271 {-} {set tags d_-}
272 {+} {
273 if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
274 set line [string replace $line 0 0 { }]
275 set tags d$op
276 } else {
277 set tags d_+
278 }
279 }
280 default {
281 puts "error: Unhandled 2 way diff marker: {$op}"
282 set tags {}
283 }
284 }
285 }
286 $ui_diff insert end $line $tags
287 if {[string index $line end] eq "\r"} {
288 $ui_diff tag add d_cr {end - 2c}
289 }
290 $ui_diff insert end "\n" $tags
291 }
292 $ui_diff conf -state disabled
293
294 if {[eof $fd]} {
295 close $fd
296 set diff_active 0
297 unlock_index
298 if {$scroll_pos ne {}} {
299 update
300 $ui_diff yview moveto $scroll_pos
301 }
302 ui_ready
303
304 if {[$ui_diff index end] eq {2.0}} {
305 handle_empty_diff
306 }
307 }
308}
309
310proc apply_hunk {x y} {
311 global current_diff_path current_diff_header current_diff_side
312 global ui_diff ui_index file_states
313
314 if {$current_diff_path eq {} || $current_diff_header eq {}} return
315 if {![lock_index apply_hunk]} return
316
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}} {
323 unlock_index
324 return
325 }
326 } else {
327 set failed_msg [mc "Failed to stage selected hunk."]
328 if {[string index $mi 1] ne {M}} {
329 unlock_index
330 return
331 }
332 }
333
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]
336 if {$s_lno eq {}} {
337 unlock_index
338 return
339 }
340
341 set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
342 if {$e_lno eq {}} {
343 set e_lno end
344 }
345
346 if {[catch {
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]
351 close $p} err]} {
352 error_popup [append $failed_msg "\n\n$err"]
353 unlock_index
354 return
355 }
356
357 $ui_diff conf -state normal
358 $ui_diff delete $s_lno $e_lno
359 $ui_diff conf -state disabled
360
361 if {[$ui_diff get 1.0 end] eq "\n"} {
362 set o _
363 } else {
364 set o ?
365 }
366
367 if {$current_diff_side eq $ui_index} {
368 set mi ${o}M
369 } elseif {[string index $mi 0] eq {_}} {
370 set mi M$o
371 } else {
372 set mi ?$o
373 }
374 unlock_index
375 display_file $current_diff_path $mi
376 # This should trigger shift to the next changed file
377 if {$o eq {_}} {
378 reshow_diff
379 }
380}
381
382proc apply_line {x y} {
383 global current_diff_path current_diff_header current_diff_side
384 global ui_diff ui_index file_states
385
386 if {$current_diff_path eq {} || $current_diff_header eq {}} return
387 if {![lock_index apply_hunk]} return
388
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."]
393 set to_context {+}
394 lappend apply_cmd --reverse
395 if {[string index $mi 0] ne {M}} {
396 unlock_index
397 return
398 }
399 } else {
400 set failed_msg [mc "Failed to stage selected line."]
401 set to_context {-}
402 if {[string index $mi 1] ne {M}} {
403 unlock_index
404 return
405 }
406 }
407
408 set the_l [$ui_diff index @$x,$y]
409
410 # operate only on change lines
411 set c1 [$ui_diff get "$the_l linestart"]
412 if {$c1 ne {+} && $c1 ne {-}} {
413 unlock_index
414 return
415 }
416 set sign $c1
417
418 set i_l [$ui_diff search -backwards -regexp ^@@ $the_l 0.0]
419 if {$i_l eq {}} {
420 unlock_index
421 return
422 }
423 # $i_l is now at the beginning of a line
424
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]
429
430 # There is a special situation to take care of. Consider this hunk:
431 #
432 # @@ -10,4 +10,4 @@
433 # context before
434 # -old 1
435 # -old 2
436 # +new 1
437 # +new 2
438 # context after
439 #
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:
443 #
444 # context before
445 # old 2
446 # new 1
447 # context after
448 #
449 # (By symmetry it is not possible to *un*stage "old 2" and "new 2".)
450 #
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:
455 #
456 # @@ -10,4 +10,5 @@
457 # context before
458 # +new 1
459 # old 1
460 # old 2
461 # context after
462 #
463 # But we do *not* treat "-" lines that are *un*staged in a special
464 # way.
465 #
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".
470
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
474 # line.
475 set pre_context {}
476
477 set n 0
478 set i_l [$ui_diff index "$i_l + 1 lines"]
479 set patch {}
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]
488 if {$c1 eq {-}} {
489 set n [expr $n+1]
490 set patch "$patch$pre_context$ln"
491 } else {
492 set patch "$patch$ln$pre_context"
493 }
494 set pre_context {}
495 } elseif {$c1 ne {-} && $c1 ne {+}} {
496 # context line
497 set ln [$ui_diff get $i_l $next_l]
498 set patch "$patch$pre_context$ln"
499 set n [expr $n+1]
500 set pre_context {}
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]
504 if {$c1 eq {-}} {
505 set pre_context "$pre_context $ln"
506 } else {
507 set patch "$patch $ln"
508 }
509 set n [expr $n+1]
510 }
511 set i_l $next_l
512 }
513 set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch"
514
515 if {[catch {
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
520 close $p} err]} {
521 error_popup [append $failed_msg "\n\n$err"]
522 }
523
524 unlock_index
525}