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