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) >= 1} {
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 set ::current_diff_inheader 1
196 fconfigure $fd \
197 -blocking 0 \
198 -encoding binary \
199 -translation binary
200 fileevent $fd readable [list read_diff $fd $scroll_pos]
201}
202
203proc read_diff {fd scroll_pos} {
204 global ui_diff diff_active
205 global is_3way_diff current_diff_header
206
207 $ui_diff conf -state normal
208 while {[gets $fd line] >= 0} {
209 # -- Cleanup uninteresting diff header lines.
210 #
211 if {$::current_diff_inheader} {
212 if { [string match {diff --git *} $line]
213 || [string match {diff --cc *} $line]
214 || [string match {diff --combined *} $line]
215 || [string match {--- *} $line]
216 || [string match {+++ *} $line]} {
217 append current_diff_header $line "\n"
218 continue
219 }
220 }
221 if {[string match {index *} $line]} continue
222 if {$line eq {deleted file mode 120000}} {
223 set line "deleted symlink"
224 }
225 set ::current_diff_inheader 0
226
227 # -- Automatically detect if this is a 3 way diff.
228 #
229 if {[string match {@@@ *} $line]} {set is_3way_diff 1}
230
231 if {[string match {mode *} $line]
232 || [string match {new file *} $line]
233 || [regexp {^(old|new) mode *} $line]
234 || [string match {deleted file *} $line]
235 || [string match {deleted symlink} $line]
236 || [string match {Binary files * and * differ} $line]
237 || $line eq {\ No newline at end of file}
238 || [regexp {^\* Unmerged path } $line]} {
239 set tags {}
240 } elseif {$is_3way_diff} {
241 set op [string range $line 0 1]
242 switch -- $op {
243 { } {set tags {}}
244 {@@} {set tags d_@}
245 { +} {set tags d_s+}
246 { -} {set tags d_s-}
247 {+ } {set tags d_+s}
248 {- } {set tags d_-s}
249 {--} {set tags d_--}
250 {++} {
251 if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
252 set line [string replace $line 0 1 { }]
253 set tags d$op
254 } else {
255 set tags d_++
256 }
257 }
258 default {
259 puts "error: Unhandled 3 way diff marker: {$op}"
260 set tags {}
261 }
262 }
263 } else {
264 set op [string index $line 0]
265 switch -- $op {
266 { } {set tags {}}
267 {@} {set tags d_@}
268 {-} {set tags d_-}
269 {+} {
270 if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
271 set line [string replace $line 0 0 { }]
272 set tags d$op
273 } else {
274 set tags d_+
275 }
276 }
277 default {
278 puts "error: Unhandled 2 way diff marker: {$op}"
279 set tags {}
280 }
281 }
282 }
283 $ui_diff insert end $line $tags
284 if {[string index $line end] eq "\r"} {
285 $ui_diff tag add d_cr {end - 2c}
286 }
287 $ui_diff insert end "\n" $tags
288 }
289 $ui_diff conf -state disabled
290
291 if {[eof $fd]} {
292 close $fd
293 set diff_active 0
294 unlock_index
295 if {$scroll_pos ne {}} {
296 update
297 $ui_diff yview moveto $scroll_pos
298 }
299 ui_ready
300
301 if {[$ui_diff index end] eq {2.0}} {
302 handle_empty_diff
303 }
304 }
305}
306
307proc apply_hunk {x y} {
308 global current_diff_path current_diff_header current_diff_side
309 global ui_diff ui_index file_states
310
311 if {$current_diff_path eq {} || $current_diff_header eq {}} return
312 if {![lock_index apply_hunk]} return
313
314 set apply_cmd {apply --cached --whitespace=nowarn}
315 set mi [lindex $file_states($current_diff_path) 0]
316 if {$current_diff_side eq $ui_index} {
317 set failed_msg [mc "Failed to unstage selected hunk."]
318 lappend apply_cmd --reverse
319 if {[string index $mi 0] ne {M}} {
320 unlock_index
321 return
322 }
323 } else {
324 set failed_msg [mc "Failed to stage selected hunk."]
325 if {[string index $mi 1] ne {M}} {
326 unlock_index
327 return
328 }
329 }
330
331 set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
332 set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
333 if {$s_lno eq {}} {
334 unlock_index
335 return
336 }
337
338 set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
339 if {$e_lno eq {}} {
340 set e_lno end
341 }
342
343 if {[catch {
344 set p [eval git_write $apply_cmd]
345 fconfigure $p -translation binary -encoding binary
346 puts -nonewline $p $current_diff_header
347 puts -nonewline $p [$ui_diff get $s_lno $e_lno]
348 close $p} err]} {
349 error_popup [append $failed_msg "\n\n$err"]
350 unlock_index
351 return
352 }
353
354 $ui_diff conf -state normal
355 $ui_diff delete $s_lno $e_lno
356 $ui_diff conf -state disabled
357
358 if {[$ui_diff get 1.0 end] eq "\n"} {
359 set o _
360 } else {
361 set o ?
362 }
363
364 if {$current_diff_side eq $ui_index} {
365 set mi ${o}M
366 } elseif {[string index $mi 0] eq {_}} {
367 set mi M$o
368 } else {
369 set mi ?$o
370 }
371 unlock_index
372 display_file $current_diff_path $mi
373 if {$o eq {_}} {
374 clear_diff
375 } else {
376 set current_diff_path $current_diff_path
377 }
378}
379
380proc apply_line {x y} {
381 global current_diff_path current_diff_header current_diff_side
382 global ui_diff ui_index file_states
383
384 if {$current_diff_path eq {} || $current_diff_header eq {}} return
385 if {![lock_index apply_hunk]} return
386
387 set apply_cmd {apply --cached --whitespace=nowarn}
388 set mi [lindex $file_states($current_diff_path) 0]
389 if {$current_diff_side eq $ui_index} {
390 set failed_msg [mc "Failed to unstage selected line."]
391 set to_context {+}
392 lappend apply_cmd --reverse
393 if {[string index $mi 0] ne {M}} {
394 unlock_index
395 return
396 }
397 } else {
398 set failed_msg [mc "Failed to stage selected line."]
399 set to_context {-}
400 if {[string index $mi 1] ne {M}} {
401 unlock_index
402 return
403 }
404 }
405
406 set the_l [$ui_diff index @$x,$y]
407
408 # operate only on change lines
409 set c1 [$ui_diff get "$the_l linestart"]
410 if {$c1 ne {+} && $c1 ne {-}} {
411 unlock_index
412 return
413 }
414 set sign $c1
415
416 set i_l [$ui_diff search -backwards -regexp ^@@ $the_l 0.0]
417 if {$i_l eq {}} {
418 unlock_index
419 return
420 }
421 # $i_l is now at the beginning of a line
422
423 # pick start line number from hunk header
424 set hh [$ui_diff get $i_l "$i_l + 1 lines"]
425 set hh [lindex [split $hh ,] 0]
426 set hln [lindex [split $hh -] 1]
427
428 # There is a special situation to take care of. Consider this hunk:
429 #
430 # @@ -10,4 +10,4 @@
431 # context before
432 # -old 1
433 # -old 2
434 # +new 1
435 # +new 2
436 # context after
437 #
438 # We used to keep the context lines in the order they appear in the
439 # hunk. But then it is not possible to correctly stage only
440 # "-old 1" and "+new 1" - it would result in this staged text:
441 #
442 # context before
443 # old 2
444 # new 1
445 # context after
446 #
447 # (By symmetry it is not possible to *un*stage "old 2" and "new 2".)
448 #
449 # We resolve the problem by introducing an asymmetry, namely, when
450 # a "+" line is *staged*, it is moved in front of the context lines
451 # that are generated from the "-" lines that are immediately before
452 # the "+" block. That is, we construct this patch:
453 #
454 # @@ -10,4 +10,5 @@
455 # context before
456 # +new 1
457 # old 1
458 # old 2
459 # context after
460 #
461 # But we do *not* treat "-" lines that are *un*staged in a special
462 # way.
463 #
464 # With this asymmetry it is possible to stage the change
465 # "old 1" -> "new 1" directly, and to stage the change
466 # "old 2" -> "new 2" by first staging the entire hunk and
467 # then unstaging the change "old 1" -> "new 1".
468
469 # This is non-empty if and only if we are _staging_ changes;
470 # then it accumulates the consecutive "-" lines (after converting
471 # them to context lines) in order to be moved after the "+" change
472 # line.
473 set pre_context {}
474
475 set n 0
476 set i_l [$ui_diff index "$i_l + 1 lines"]
477 set patch {}
478 while {[$ui_diff compare $i_l < "end - 1 chars"] &&
479 [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
480 set next_l [$ui_diff index "$i_l + 1 lines"]
481 set c1 [$ui_diff get $i_l]
482 if {[$ui_diff compare $i_l <= $the_l] &&
483 [$ui_diff compare $the_l < $next_l]} {
484 # the line to stage/unstage
485 set ln [$ui_diff get $i_l $next_l]
486 if {$c1 eq {-}} {
487 set n [expr $n+1]
488 set patch "$patch$pre_context$ln"
489 } else {
490 set patch "$patch$ln$pre_context"
491 }
492 set pre_context {}
493 } elseif {$c1 ne {-} && $c1 ne {+}} {
494 # context line
495 set ln [$ui_diff get $i_l $next_l]
496 set patch "$patch$pre_context$ln"
497 set n [expr $n+1]
498 set pre_context {}
499 } elseif {$c1 eq $to_context} {
500 # turn change line into context line
501 set ln [$ui_diff get "$i_l + 1 chars" $next_l]
502 if {$c1 eq {-}} {
503 set pre_context "$pre_context $ln"
504 } else {
505 set patch "$patch $ln"
506 }
507 set n [expr $n+1]
508 }
509 set i_l $next_l
510 }
511 set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch"
512
513 if {[catch {
514 set p [eval git_write $apply_cmd]
515 fconfigure $p -translation binary -encoding binary
516 puts -nonewline $p $current_diff_header
517 puts -nonewline $p $patch
518 close $p} err]} {
519 error_popup [append $failed_msg "\n\n$err"]
520 }
521
522 unlock_index
523}