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