git-gui: Allow as few as 0 lines of diff context
[git] / lib / diff.tcl
1 # git-gui diff viewer
2 # Copyright (C) 2006, 2007 Shawn Pearce
3
4 proc 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
19 proc 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
35 proc 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
46 The modification date of this file was updated by another application, but the content within the file was not changed.
47
48 A 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
55 proc 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
172 proc 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
267 proc 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 }