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 "No differences detected.
43
44[short_path $path] 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."
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 "Loading diff of [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 "Unable to display [escape_path $path]"
115 error_popup "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 "* Git Repository (subproject)\n" d_@
121 } elseif {![catch {set type [exec file $path]}]} {
122 set n [string length $path]
123 if {[string equal -length $n $path $type]} {
124 set type [string range $type $n end]
125 regsub {^:?\s*} $type {} type
126 }
127 $ui_diff insert end "* $type\n" d_@
128 }
129 if {[string first "\0" $content] != -1} {
130 $ui_diff insert end \
131 "* Binary file (not showing content)." \
132 d_@
133 } else {
134 if {$sz > $max_sz} {
135 $ui_diff insert end \
136"* Untracked file is $sz bytes.
137* Showing only first $max_sz bytes.
138" d_@
139 }
140 $ui_diff insert end $content
141 if {$sz > $max_sz} {
142 $ui_diff insert end "
143* Untracked file clipped here by [appname].
144* To see the entire file, use an external editor.
145" d_@
146 }
147 }
148 $ui_diff conf -state disabled
149 set diff_active 0
150 unlock_index
151 ui_ready
152 return
153 }
154
155 set cmd [list]
156 if {$w eq $ui_index} {
157 lappend cmd diff-index
158 lappend cmd --cached
159 } elseif {$w eq $ui_workdir} {
160 if {[string index $m 0] eq {U}} {
161 lappend cmd diff
162 } else {
163 lappend cmd diff-files
164 }
165 }
166
167 lappend cmd -p
168 lappend cmd --no-color
169 if {$repo_config(gui.diffcontext) >= 0} {
170 lappend cmd "-U$repo_config(gui.diffcontext)"
171 }
172 if {$w eq $ui_index} {
173 lappend cmd [PARENT]
174 }
175 lappend cmd --
176 lappend cmd $path
177
178 if {[catch {set fd [eval git_read --nice $cmd]} err]} {
179 set diff_active 0
180 unlock_index
181 ui_status "Unable to display [escape_path $path]"
182 error_popup "Error loading diff:\n\n$err"
183 return
184 }
185
186 fconfigure $fd \
187 -blocking 0 \
188 -encoding binary \
189 -translation binary
190 fileevent $fd readable [list read_diff $fd]
191}
192
193proc read_diff {fd} {
194 global ui_diff diff_active
195 global is_3way_diff current_diff_header
196
197 $ui_diff conf -state normal
198 while {[gets $fd line] >= 0} {
199 # -- Cleanup uninteresting diff header lines.
200 #
201 if { [string match {diff --git *} $line]
202 || [string match {diff --cc *} $line]
203 || [string match {diff --combined *} $line]
204 || [string match {--- *} $line]
205 || [string match {+++ *} $line]} {
206 append current_diff_header $line "\n"
207 continue
208 }
209 if {[string match {index *} $line]} continue
210 if {$line eq {deleted file mode 120000}} {
211 set line "deleted symlink"
212 }
213
214 # -- Automatically detect if this is a 3 way diff.
215 #
216 if {[string match {@@@ *} $line]} {set is_3way_diff 1}
217
218 if {[string match {mode *} $line]
219 || [string match {new file *} $line]
220 || [string match {deleted file *} $line]
221 || [string match {deleted symlink} $line]
222 || [string match {Binary files * and * differ} $line]
223 || $line eq {\ No newline at end of file}
224 || [regexp {^\* Unmerged path } $line]} {
225 set tags {}
226 } elseif {$is_3way_diff} {
227 set op [string range $line 0 1]
228 switch -- $op {
229 { } {set tags {}}
230 {@@} {set tags d_@}
231 { +} {set tags d_s+}
232 { -} {set tags d_s-}
233 {+ } {set tags d_+s}
234 {- } {set tags d_-s}
235 {--} {set tags d_--}
236 {++} {
237 if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
238 set line [string replace $line 0 1 { }]
239 set tags d$op
240 } else {
241 set tags d_++
242 }
243 }
244 default {
245 puts "error: Unhandled 3 way diff marker: {$op}"
246 set tags {}
247 }
248 }
249 } else {
250 set op [string index $line 0]
251 switch -- $op {
252 { } {set tags {}}
253 {@} {set tags d_@}
254 {-} {set tags d_-}
255 {+} {
256 if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
257 set line [string replace $line 0 0 { }]
258 set tags d$op
259 } else {
260 set tags d_+
261 }
262 }
263 default {
264 puts "error: Unhandled 2 way diff marker: {$op}"
265 set tags {}
266 }
267 }
268 }
269 $ui_diff insert end $line $tags
270 if {[string index $line end] eq "\r"} {
271 $ui_diff tag add d_cr {end - 2c}
272 }
273 $ui_diff insert end "\n" $tags
274 }
275 $ui_diff conf -state disabled
276
277 if {[eof $fd]} {
278 close $fd
279 set diff_active 0
280 unlock_index
281 ui_ready
282
283 if {[$ui_diff index end] eq {2.0}} {
284 handle_empty_diff
285 }
286 }
287}
288
289proc apply_hunk {x y} {
290 global current_diff_path current_diff_header current_diff_side
291 global ui_diff ui_index file_states
292
293 if {$current_diff_path eq {} || $current_diff_header eq {}} return
294 if {![lock_index apply_hunk]} return
295
296 set apply_cmd {apply --cached --whitespace=nowarn}
297 set mi [lindex $file_states($current_diff_path) 0]
298 if {$current_diff_side eq $ui_index} {
299 set mode unstage
300 lappend apply_cmd --reverse
301 if {[string index $mi 0] ne {M}} {
302 unlock_index
303 return
304 }
305 } else {
306 set mode stage
307 if {[string index $mi 1] ne {M}} {
308 unlock_index
309 return
310 }
311 }
312
313 set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
314 set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
315 if {$s_lno eq {}} {
316 unlock_index
317 return
318 }
319
320 set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
321 if {$e_lno eq {}} {
322 set e_lno end
323 }
324
325 if {[catch {
326 set p [eval git_write $apply_cmd]
327 fconfigure $p -translation binary -encoding binary
328 puts -nonewline $p $current_diff_header
329 puts -nonewline $p [$ui_diff get $s_lno $e_lno]
330 close $p} err]} {
331 error_popup "Failed to $mode selected hunk.\n\n$err"
332 unlock_index
333 return
334 }
335
336 $ui_diff conf -state normal
337 $ui_diff delete $s_lno $e_lno
338 $ui_diff conf -state disabled
339
340 if {[$ui_diff get 1.0 end] eq "\n"} {
341 set o _
342 } else {
343 set o ?
344 }
345
346 if {$current_diff_side eq $ui_index} {
347 set mi ${o}M
348 } elseif {[string index $mi 0] eq {_}} {
349 set mi M$o
350 } else {
351 set mi ?$o
352 }
353 unlock_index
354 display_file $current_diff_path $mi
355 if {$o eq {_}} {
356 clear_diff
357 }
358}