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 || [catch {set s $file_states($p)}]
29 || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} {
30 clear_diff
31 } else {
32 set save_pos [lindex [$ui_diff yview] 0]
33 show_diff $p $current_diff_side {} $save_pos
34 }
35}
36
37proc handle_empty_diff {} {
38 global current_diff_path file_states file_lists
39
40 set path $current_diff_path
41 set s $file_states($path)
42 if {[lindex $s 0] ne {_M}} return
43
44 info_popup [mc "No differences detected.
45
46%s has no changes.
47
48The modification date of this file was updated by another application, but the content within the file was not changed.
49
50A rescan will be automatically started to find other files which may have the same state." [short_path $path]]
51
52 clear_diff
53 display_file $path __
54 rescan ui_ready 0
55}
56
57proc show_diff {path w {lno {}} {scroll_pos {}}} {
58 global file_states file_lists
59 global is_3way_diff diff_active repo_config
60 global ui_diff ui_index ui_workdir
61 global current_diff_path current_diff_side current_diff_header
62
63 if {$diff_active || ![lock_index read]} return
64
65 clear_diff
66 if {$lno == {}} {
67 set lno [lsearch -sorted -exact $file_lists($w) $path]
68 if {$lno >= 0} {
69 incr lno
70 }
71 }
72 if {$lno >= 1} {
73 $w tag add in_diff $lno.0 [expr {$lno + 1}].0
74 }
75
76 set s $file_states($path)
77 set m [lindex $s 0]
78 set is_3way_diff 0
79 set diff_active 1
80 set current_diff_path $path
81 set current_diff_side $w
82 set current_diff_header {}
83 ui_status [mc "Loading diff of %s..." [escape_path $path]]
84
85 # - Git won't give us the diff, there's nothing to compare to!
86 #
87 if {$m eq {_O}} {
88 set max_sz [expr {128 * 1024}]
89 set type unknown
90 if {[catch {
91 set type [file type $path]
92 switch -- $type {
93 directory {
94 set type submodule
95 set content {}
96 set sz 0
97 }
98 link {
99 set content [file readlink $path]
100 set sz [string length $content]
101 }
102 file {
103 set fd [open $path r]
104 fconfigure $fd -eofchar {}
105 set content [read $fd $max_sz]
106 close $fd
107 set sz [file size $path]
108 }
109 default {
110 error "'$type' not supported"
111 }
112 }
113 } err ]} {
114 set diff_active 0
115 unlock_index
116 ui_status [mc "Unable to display %s" [escape_path $path]]
117 error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
118 return
119 }
120 $ui_diff conf -state normal
121 if {$type eq {submodule}} {
122 $ui_diff insert end [append \
123 "* " \
124 [mc "Git Repository (subproject)"] \
125 "\n"] d_@
126 } elseif {![catch {set type [exec file $path]}]} {
127 set n [string length $path]
128 if {[string equal -length $n $path $type]} {
129 set type [string range $type $n end]
130 regsub {^:?\s*} $type {} type
131 }
132 $ui_diff insert end "* $type\n" d_@
133 }
134 if {[string first "\0" $content] != -1} {
135 $ui_diff insert end \
136 [mc "* Binary file (not showing content)."] \
137 d_@
138 } else {
139 if {$sz > $max_sz} {
140 $ui_diff insert end \
141"* Untracked file is $sz bytes.
142* Showing only first $max_sz bytes.
143" d_@
144 }
145 $ui_diff insert end $content
146 if {$sz > $max_sz} {
147 $ui_diff insert end "
148* Untracked file clipped here by [appname].
149* To see the entire file, use an external editor.
150" d_@
151 }
152 }
153 $ui_diff conf -state disabled
154 set diff_active 0
155 unlock_index
156 if {$scroll_pos ne {}} {
157 update
158 $ui_diff yview moveto $scroll_pos
159 }
160 ui_ready
161 return
162 }
163
164 set cmd [list]
165 if {$w eq $ui_index} {
166 lappend cmd diff-index
167 lappend cmd --cached
168 } elseif {$w eq $ui_workdir} {
169 if {[string index $m 0] eq {U}} {
170 lappend cmd diff
171 } else {
172 lappend cmd diff-files
173 }
174 }
175
176 lappend cmd -p
177 lappend cmd --no-color
178 if {$repo_config(gui.diffcontext) >= 0} {
179 lappend cmd "-U$repo_config(gui.diffcontext)"
180 }
181 if {$w eq $ui_index} {
182 lappend cmd [PARENT]
183 }
184 lappend cmd --
185 lappend cmd $path
186
187 if {[catch {set fd [eval git_read --nice $cmd]} err]} {
188 set diff_active 0
189 unlock_index
190 ui_status [mc "Unable to display %s" [escape_path $path]]
191 error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
192 return
193 }
194
195 fconfigure $fd \
196 -blocking 0 \
197 -encoding binary \
198 -translation binary
199 fileevent $fd readable [list read_diff $fd $scroll_pos]
200}
201
202proc read_diff {fd scroll_pos} {
203 global ui_diff diff_active
204 global is_3way_diff current_diff_header
205
206 $ui_diff conf -state normal
207 while {[gets $fd line] >= 0} {
208 # -- Cleanup uninteresting diff header lines.
209 #
210 if { [string match {diff --git *} $line]
211 || [string match {diff --cc *} $line]
212 || [string match {diff --combined *} $line]
213 || [string match {--- *} $line]
214 || [string match {+++ *} $line]} {
215 append current_diff_header $line "\n"
216 continue
217 }
218 if {[string match {index *} $line]} continue
219 if {$line eq {deleted file mode 120000}} {
220 set line "deleted symlink"
221 }
222
223 # -- Automatically detect if this is a 3 way diff.
224 #
225 if {[string match {@@@ *} $line]} {set is_3way_diff 1}
226
227 if {[string match {mode *} $line]
228 || [string match {new file *} $line]
229 || [regexp {^(old|new) mode *} $line]
230 || [string match {deleted file *} $line]
231 || [string match {deleted symlink} $line]
232 || [string match {Binary files * and * differ} $line]
233 || $line eq {\ No newline at end of file}
234 || [regexp {^\* Unmerged path } $line]} {
235 set tags {}
236 } elseif {$is_3way_diff} {
237 set op [string range $line 0 1]
238 switch -- $op {
239 { } {set tags {}}
240 {@@} {set tags d_@}
241 { +} {set tags d_s+}
242 { -} {set tags d_s-}
243 {+ } {set tags d_+s}
244 {- } {set tags d_-s}
245 {--} {set tags d_--}
246 {++} {
247 if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
248 set line [string replace $line 0 1 { }]
249 set tags d$op
250 } else {
251 set tags d_++
252 }
253 }
254 default {
255 puts "error: Unhandled 3 way diff marker: {$op}"
256 set tags {}
257 }
258 }
259 } else {
260 set op [string index $line 0]
261 switch -- $op {
262 { } {set tags {}}
263 {@} {set tags d_@}
264 {-} {set tags d_-}
265 {+} {
266 if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
267 set line [string replace $line 0 0 { }]
268 set tags d$op
269 } else {
270 set tags d_+
271 }
272 }
273 default {
274 puts "error: Unhandled 2 way diff marker: {$op}"
275 set tags {}
276 }
277 }
278 }
279 $ui_diff insert end $line $tags
280 if {[string index $line end] eq "\r"} {
281 $ui_diff tag add d_cr {end - 2c}
282 }
283 $ui_diff insert end "\n" $tags
284 }
285 $ui_diff conf -state disabled
286
287 if {[eof $fd]} {
288 close $fd
289 set diff_active 0
290 unlock_index
291 if {$scroll_pos ne {}} {
292 update
293 $ui_diff yview moveto $scroll_pos
294 }
295 ui_ready
296
297 if {[$ui_diff index end] eq {2.0}} {
298 handle_empty_diff
299 }
300 }
301}
302
303proc apply_hunk {x y} {
304 global current_diff_path current_diff_header current_diff_side
305 global ui_diff ui_index file_states
306
307 if {$current_diff_path eq {} || $current_diff_header eq {}} return
308 if {![lock_index apply_hunk]} return
309
310 set apply_cmd {apply --cached --whitespace=nowarn}
311 set mi [lindex $file_states($current_diff_path) 0]
312 if {$current_diff_side eq $ui_index} {
313 set failed_msg [mc "Failed to unstage selected hunk."]
314 lappend apply_cmd --reverse
315 if {[string index $mi 0] ne {M}} {
316 unlock_index
317 return
318 }
319 } else {
320 set failed_msg [mc "Failed to stage selected hunk."]
321 if {[string index $mi 1] ne {M}} {
322 unlock_index
323 return
324 }
325 }
326
327 set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
328 set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
329 if {$s_lno eq {}} {
330 unlock_index
331 return
332 }
333
334 set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
335 if {$e_lno eq {}} {
336 set e_lno end
337 }
338
339 if {[catch {
340 set p [eval git_write $apply_cmd]
341 fconfigure $p -translation binary -encoding binary
342 puts -nonewline $p $current_diff_header
343 puts -nonewline $p [$ui_diff get $s_lno $e_lno]
344 close $p} err]} {
345 error_popup [append $failed_msg "\n\n$err"]
346 unlock_index
347 return
348 }
349
350 $ui_diff conf -state normal
351 $ui_diff delete $s_lno $e_lno
352 $ui_diff conf -state disabled
353
354 if {[$ui_diff get 1.0 end] eq "\n"} {
355 set o _
356 } else {
357 set o ?
358 }
359
360 if {$current_diff_side eq $ui_index} {
361 set mi ${o}M
362 } elseif {[string index $mi 0] eq {_}} {
363 set mi M$o
364 } else {
365 set mi ?$o
366 }
367 unlock_index
368 display_file $current_diff_path $mi
369 if {$o eq {_}} {
370 clear_diff
371 } else {
372 set current_diff_path $current_diff_path
373 }
374}
375
376proc apply_line {x y} {
377 global current_diff_path current_diff_header current_diff_side
378 global ui_diff ui_index file_states
379
380 if {$current_diff_path eq {} || $current_diff_header eq {}} return
381 if {![lock_index apply_hunk]} return
382
383 set apply_cmd {apply --cached --whitespace=nowarn}
384 set mi [lindex $file_states($current_diff_path) 0]
385 if {$current_diff_side eq $ui_index} {
386 set failed_msg [mc "Failed to unstage selected line."]
387 set to_context {+}
388 lappend apply_cmd --reverse
389 if {[string index $mi 0] ne {M}} {
390 unlock_index
391 return
392 }
393 } else {
394 set failed_msg [mc "Failed to stage selected line."]
395 set to_context {-}
396 if {[string index $mi 1] ne {M}} {
397 unlock_index
398 return
399 }
400 }
401
402 set the_l [$ui_diff index @$x,$y]
403
404 # operate only on change lines
405 set c1 [$ui_diff get "$the_l linestart"]
406 if {$c1 ne {+} && $c1 ne {-}} {
407 unlock_index
408 return
409 }
410 set sign $c1
411
412 set i_l [$ui_diff search -backwards -regexp ^@@ $the_l 0.0]
413 if {$i_l eq {}} {
414 unlock_index
415 return
416 }
417 # $i_l is now at the beginning of a line
418
419 # pick start line number from hunk header
420 set hh [$ui_diff get $i_l "$i_l + 1 lines"]
421 set hh [lindex [split $hh ,] 0]
422 set hln [lindex [split $hh -] 1]
423
424 # There is a special situation to take care of. Consider this hunk:
425 #
426 # @@ -10,4 +10,4 @@
427 # context before
428 # -old 1
429 # -old 2
430 # +new 1
431 # +new 2
432 # context after
433 #
434 # We used to keep the context lines in the order they appear in the
435 # hunk. But then it is not possible to correctly stage only
436 # "-old 1" and "+new 1" - it would result in this staged text:
437 #
438 # context before
439 # old 2
440 # new 1
441 # context after
442 #
443 # (By symmetry it is not possible to *un*stage "old 2" and "new 2".)
444 #
445 # We resolve the problem by introducing an asymmetry, namely, when
446 # a "+" line is *staged*, it is moved in front of the context lines
447 # that are generated from the "-" lines that are immediately before
448 # the "+" block. That is, we construct this patch:
449 #
450 # @@ -10,4 +10,5 @@
451 # context before
452 # +new 1
453 # old 1
454 # old 2
455 # context after
456 #
457 # But we do *not* treat "-" lines that are *un*staged in a special
458 # way.
459 #
460 # With this asymmetry it is possible to stage the change
461 # "old 1" -> "new 1" directly, and to stage the change
462 # "old 2" -> "new 2" by first staging the entire hunk and
463 # then unstaging the change "old 1" -> "new 1".
464
465 # This is non-empty if and only if we are _staging_ changes;
466 # then it accumulates the consecutive "-" lines (after converting
467 # them to context lines) in order to be moved after the "+" change
468 # line.
469 set pre_context {}
470
471 set n 0
472 set i_l [$ui_diff index "$i_l + 1 lines"]
473 set patch {}
474 while {[$ui_diff compare $i_l < "end - 1 chars"] &&
475 [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
476 set next_l [$ui_diff index "$i_l + 1 lines"]
477 set c1 [$ui_diff get $i_l]
478 if {[$ui_diff compare $i_l <= $the_l] &&
479 [$ui_diff compare $the_l < $next_l]} {
480 # the line to stage/unstage
481 set ln [$ui_diff get $i_l $next_l]
482 if {$c1 eq {-}} {
483 set n [expr $n+1]
484 set patch "$patch$pre_context$ln"
485 } else {
486 set patch "$patch$ln$pre_context"
487 }
488 set pre_context {}
489 } elseif {$c1 ne {-} && $c1 ne {+}} {
490 # context line
491 set ln [$ui_diff get $i_l $next_l]
492 set patch "$patch$pre_context$ln"
493 set n [expr $n+1]
494 set pre_context {}
495 } elseif {$c1 eq $to_context} {
496 # turn change line into context line
497 set ln [$ui_diff get "$i_l + 1 chars" $next_l]
498 if {$c1 eq {-}} {
499 set pre_context "$pre_context $ln"
500 } else {
501 set patch "$patch $ln"
502 }
503 set n [expr $n+1]
504 }
505 set i_l $next_l
506 }
507 set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch"
508
509 if {[catch {
510 set p [eval git_write $apply_cmd]
511 fconfigure $p -translation binary -encoding binary
512 puts -nonewline $p $current_diff_header
513 puts -nonewline $p $patch
514 close $p} err]} {
515 error_popup [append $failed_msg "\n\n$err"]
516 }
517
518 unlock_index
519}