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 || [string match {deleted file *} $line]
224 || [string match {deleted symlink} $line]
225 || [string match {Binary files * and * differ} $line]
226 || $line eq {\ No newline at end of file}
227 || [regexp {^\* Unmerged path } $line]} {
228 set tags {}
229 } elseif {$is_3way_diff} {
230 set op [string range $line 0 1]
231 switch -- $op {
232 { } {set tags {}}
233 {@@} {set tags d_@}
234 { +} {set tags d_s+}
235 { -} {set tags d_s-}
236 {+ } {set tags d_+s}
237 {- } {set tags d_-s}
238 {--} {set tags d_--}
239 {++} {
240 if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
241 set line [string replace $line 0 1 { }]
242 set tags d$op
243 } else {
244 set tags d_++
245 }
246 }
247 default {
248 puts "error: Unhandled 3 way diff marker: {$op}"
249 set tags {}
250 }
251 }
252 } else {
253 set op [string index $line 0]
254 switch -- $op {
255 { } {set tags {}}
256 {@} {set tags d_@}
257 {-} {set tags d_-}
258 {+} {
259 if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
260 set line [string replace $line 0 0 { }]
261 set tags d$op
262 } else {
263 set tags d_+
264 }
265 }
266 default {
267 puts "error: Unhandled 2 way diff marker: {$op}"
268 set tags {}
269 }
270 }
271 }
272 $ui_diff insert end $line $tags
273 if {[string index $line end] eq "\r"} {
274 $ui_diff tag add d_cr {end - 2c}
275 }
276 $ui_diff insert end "\n" $tags
277 }
278 $ui_diff conf -state disabled
279
280 if {[eof $fd]} {
281 close $fd
282 set diff_active 0
283 unlock_index
284 ui_ready
285
286 if {[$ui_diff index end] eq {2.0}} {
287 handle_empty_diff
288 }
289 }
290}
291
292proc apply_hunk {x y} {
293 global current_diff_path current_diff_header current_diff_side
294 global ui_diff ui_index file_states
295
296 if {$current_diff_path eq {} || $current_diff_header eq {}} return
297 if {![lock_index apply_hunk]} return
298
299 set apply_cmd {apply --cached --whitespace=nowarn}
300 set mi [lindex $file_states($current_diff_path) 0]
301 if {$current_diff_side eq $ui_index} {
302 set failed_msg [mc "Failed to unstage selected hunk."]
303 lappend apply_cmd --reverse
304 if {[string index $mi 0] ne {M}} {
305 unlock_index
306 return
307 }
308 } else {
309 set failed_msg [mc "Failed to stage selected hunk."]
310 if {[string index $mi 1] ne {M}} {
311 unlock_index
312 return
313 }
314 }
315
316 set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
317 set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
318 if {$s_lno eq {}} {
319 unlock_index
320 return
321 }
322
323 set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
324 if {$e_lno eq {}} {
325 set e_lno end
326 }
327
328 if {[catch {
329 set p [eval git_write $apply_cmd]
330 fconfigure $p -translation binary -encoding binary
331 puts -nonewline $p $current_diff_header
332 puts -nonewline $p [$ui_diff get $s_lno $e_lno]
333 close $p} err]} {
334 error_popup [append $failed_msg "\n\n$err"]
335 unlock_index
336 return
337 }
338
339 $ui_diff conf -state normal
340 $ui_diff delete $s_lno $e_lno
341 $ui_diff conf -state disabled
342
343 if {[$ui_diff get 1.0 end] eq "\n"} {
344 set o _
345 } else {
346 set o ?
347 }
348
349 if {$current_diff_side eq $ui_index} {
350 set mi ${o}M
351 } elseif {[string index $mi 0] eq {_}} {
352 set mi M$o
353 } else {
354 set mi ?$o
355 }
356 unlock_index
357 display_file $current_diff_path $mi
358 if {$o eq {_}} {
359 clear_diff
360 }
361}