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