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