git-gui: Keep repo_config(gui.recentrepos) and .gitconfig in sync
[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 {{after {}}} {
20         global file_states file_lists
21         global current_diff_path current_diff_side
22         global ui_diff
23
24         set p $current_diff_path
25         if {$p eq {}} {
26                 # No diff is being shown.
27         } elseif {$current_diff_side eq {}} {
28                 clear_diff
29         } elseif {[catch {set s $file_states($p)}]
30                 || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} {
31
32                 if {[find_next_diff $current_diff_side $p {} {[^O]}]} {
33                         next_diff $after
34                 } else {
35                         clear_diff
36                 }
37         } else {
38                 set save_pos [lindex [$ui_diff yview] 0]
39                 show_diff $p $current_diff_side {} $save_pos $after
40         }
41 }
42
43 proc force_diff_encoding {enc} {
44         global current_diff_path
45         
46         if {$current_diff_path ne {}} {
47                 force_path_encoding $current_diff_path $enc
48                 reshow_diff
49         }
50 }
51
52 proc handle_empty_diff {} {
53         global current_diff_path file_states file_lists
54         global diff_empty_count
55
56         set path $current_diff_path
57         set s $file_states($path)
58         if {[lindex $s 0] ne {_M}} return
59
60         # Prevent infinite rescan loops
61         incr diff_empty_count
62         if {$diff_empty_count > 1} return
63
64         info_popup [mc "No differences detected.
65
66 %s has no changes.
67
68 The modification date of this file was updated by another application, but the content within the file was not changed.
69
70 A rescan will be automatically started to find other files which may have the same state." [short_path $path]]
71
72         clear_diff
73         display_file $path __
74         rescan ui_ready 0
75 }
76
77 proc show_diff {path w {lno {}} {scroll_pos {}} {callback {}}} {
78         global file_states file_lists
79         global is_3way_diff is_conflict_diff diff_active repo_config
80         global ui_diff ui_index ui_workdir
81         global current_diff_path current_diff_side current_diff_header
82         global current_diff_queue
83
84         if {$diff_active || ![lock_index read]} return
85
86         clear_diff
87         if {$lno == {}} {
88                 set lno [lsearch -sorted -exact $file_lists($w) $path]
89                 if {$lno >= 0} {
90                         incr lno
91                 }
92         }
93         if {$lno >= 1} {
94                 $w tag add in_diff $lno.0 [expr {$lno + 1}].0
95                 $w see $lno.0
96         }
97
98         set s $file_states($path)
99         set m [lindex $s 0]
100         set is_conflict_diff 0
101         set current_diff_path $path
102         set current_diff_side $w
103         set current_diff_queue {}
104         ui_status [mc "Loading diff of %s..." [escape_path $path]]
105
106         set cont_info [list $scroll_pos $callback]
107
108         if {[string first {U} $m] >= 0} {
109                 merge_load_stages $path [list show_unmerged_diff $cont_info]
110         } elseif {$m eq {_O}} {
111                 show_other_diff $path $w $m $cont_info
112         } else {
113                 start_show_diff $cont_info
114         }
115 }
116
117 proc show_unmerged_diff {cont_info} {
118         global current_diff_path current_diff_side
119         global merge_stages ui_diff is_conflict_diff
120         global current_diff_queue
121
122         if {$merge_stages(2) eq {}} {
123                 set is_conflict_diff 1
124                 lappend current_diff_queue \
125                         [list [mc "LOCAL: deleted\nREMOTE:\n"] d======= \
126                             [list ":1:$current_diff_path" ":3:$current_diff_path"]]
127         } elseif {$merge_stages(3) eq {}} {
128                 set is_conflict_diff 1
129                 lappend current_diff_queue \
130                         [list [mc "REMOTE: deleted\nLOCAL:\n"] d======= \
131                             [list ":1:$current_diff_path" ":2:$current_diff_path"]]
132         } elseif {[lindex $merge_stages(1) 0] eq {120000}
133                 || [lindex $merge_stages(2) 0] eq {120000}
134                 || [lindex $merge_stages(3) 0] eq {120000}} {
135                 set is_conflict_diff 1
136                 lappend current_diff_queue \
137                         [list [mc "LOCAL:\n"] d======= \
138                             [list ":1:$current_diff_path" ":2:$current_diff_path"]]
139                 lappend current_diff_queue \
140                         [list [mc "REMOTE:\n"] d======= \
141                             [list ":1:$current_diff_path" ":3:$current_diff_path"]]
142         } else {
143                 start_show_diff $cont_info
144                 return
145         }
146
147         advance_diff_queue $cont_info
148 }
149
150 proc advance_diff_queue {cont_info} {
151         global current_diff_queue ui_diff
152
153         set item [lindex $current_diff_queue 0]
154         set current_diff_queue [lrange $current_diff_queue 1 end]
155
156         $ui_diff conf -state normal
157         $ui_diff insert end [lindex $item 0] [lindex $item 1]
158         $ui_diff conf -state disabled
159
160         start_show_diff $cont_info [lindex $item 2]
161 }
162
163 proc show_other_diff {path w m cont_info} {
164         global file_states file_lists
165         global is_3way_diff diff_active repo_config
166         global ui_diff ui_index ui_workdir
167         global current_diff_path current_diff_side current_diff_header
168
169         # - Git won't give us the diff, there's nothing to compare to!
170         #
171         if {$m eq {_O}} {
172                 set max_sz 100000
173                 set type unknown
174                 if {[catch {
175                                 set type [file type $path]
176                                 switch -- $type {
177                                 directory {
178                                         set type submodule
179                                         set content {}
180                                         set sz 0
181                                 }
182                                 link {
183                                         set content [file readlink $path]
184                                         set sz [string length $content]
185                                 }
186                                 file {
187                                         set fd [open $path r]
188                                         fconfigure $fd \
189                                                 -eofchar {} \
190                                                 -encoding [get_path_encoding $path]
191                                         set content [read $fd $max_sz]
192                                         close $fd
193                                         set sz [file size $path]
194                                 }
195                                 default {
196                                         error "'$type' not supported"
197                                 }
198                                 }
199                         } err ]} {
200                         set diff_active 0
201                         unlock_index
202                         ui_status [mc "Unable to display %s" [escape_path $path]]
203                         error_popup [strcat [mc "Error loading file:"] "\n\n$err"]
204                         return
205                 }
206                 $ui_diff conf -state normal
207                 if {$type eq {submodule}} {
208                         $ui_diff insert end [append \
209                                 "* " \
210                                 [mc "Git Repository (subproject)"] \
211                                 "\n"] d_@
212                 } elseif {![catch {set type [exec file $path]}]} {
213                         set n [string length $path]
214                         if {[string equal -length $n $path $type]} {
215                                 set type [string range $type $n end]
216                                 regsub {^:?\s*} $type {} type
217                         }
218                         $ui_diff insert end "* $type\n" d_@
219                 }
220                 if {[string first "\0" $content] != -1} {
221                         $ui_diff insert end \
222                                 [mc "* Binary file (not showing content)."] \
223                                 d_@
224                 } else {
225                         if {$sz > $max_sz} {
226                                 $ui_diff insert end [mc \
227 "* Untracked file is %d bytes.
228 * Showing only first %d bytes.
229 " $sz $max_sz] d_@
230                         }
231                         $ui_diff insert end $content
232                         if {$sz > $max_sz} {
233                                 $ui_diff insert end [mc "
234 * Untracked file clipped here by %s.
235 * To see the entire file, use an external editor.
236 " [appname]] d_@
237                         }
238                 }
239                 $ui_diff conf -state disabled
240                 set diff_active 0
241                 unlock_index
242                 set scroll_pos [lindex $cont_info 0]
243                 if {$scroll_pos ne {}} {
244                         update
245                         $ui_diff yview moveto $scroll_pos
246                 }
247                 ui_ready
248                 set callback [lindex $cont_info 1]
249                 if {$callback ne {}} {
250                         eval $callback
251                 }
252                 return
253         }
254 }
255
256 proc start_show_diff {cont_info {add_opts {}}} {
257         global file_states file_lists
258         global is_3way_diff is_submodule_diff diff_active repo_config
259         global ui_diff ui_index ui_workdir
260         global current_diff_path current_diff_side current_diff_header
261
262         set path $current_diff_path
263         set w $current_diff_side
264
265         set s $file_states($path)
266         set m [lindex $s 0]
267         set is_3way_diff 0
268         set is_submodule_diff 0
269         set diff_active 1
270         set current_diff_header {}
271
272         set cmd [list]
273         if {$w eq $ui_index} {
274                 lappend cmd diff-index
275                 lappend cmd --cached
276         } elseif {$w eq $ui_workdir} {
277                 if {[string first {U} $m] >= 0} {
278                         lappend cmd diff
279                 } else {
280                         lappend cmd diff-files
281                 }
282         }
283
284         lappend cmd -p
285         lappend cmd --no-color
286         if {$repo_config(gui.diffcontext) >= 1} {
287                 lappend cmd "-U$repo_config(gui.diffcontext)"
288         }
289         if {$w eq $ui_index} {
290                 lappend cmd [PARENT]
291         }
292         if {$add_opts ne {}} {
293                 eval lappend cmd $add_opts
294         } else {
295                 lappend cmd --
296                 lappend cmd $path
297         }
298
299         if {[string match {160000 *} [lindex $s 2]]
300         || [string match {160000 *} [lindex $s 3]]} {
301                 set is_submodule_diff 1
302                 if {$w eq $ui_index} {
303                         set cmd [list submodule summary --cached -- $path]
304                 } else {
305                         set cmd [list submodule summary --files -- $path]
306                 }
307         }
308
309         if {[catch {set fd [eval git_read --nice $cmd]} err]} {
310                 set diff_active 0
311                 unlock_index
312                 ui_status [mc "Unable to display %s" [escape_path $path]]
313                 error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
314                 return
315         }
316
317         set ::current_diff_inheader 1
318         fconfigure $fd \
319                 -blocking 0 \
320                 -encoding [get_path_encoding $path] \
321                 -translation lf
322         fileevent $fd readable [list read_diff $fd $cont_info]
323 }
324
325 proc read_diff {fd cont_info} {
326         global ui_diff diff_active is_submodule_diff
327         global is_3way_diff is_conflict_diff current_diff_header
328         global current_diff_queue
329         global diff_empty_count
330
331         $ui_diff conf -state normal
332         while {[gets $fd line] >= 0} {
333                 # -- Cleanup uninteresting diff header lines.
334                 #
335                 if {$::current_diff_inheader} {
336                         if {   [string match {diff --git *}      $line]
337                             || [string match {diff --cc *}       $line]
338                             || [string match {diff --combined *} $line]
339                             || [string match {--- *}             $line]
340                             || [string match {+++ *}             $line]} {
341                                 append current_diff_header $line "\n"
342                                 continue
343                         }
344                 }
345                 if {[string match {index *} $line]} continue
346                 if {$line eq {deleted file mode 120000}} {
347                         set line "deleted symlink"
348                 }
349                 set ::current_diff_inheader 0
350
351                 # -- Automatically detect if this is a 3 way diff.
352                 #
353                 if {[string match {@@@ *} $line]} {set is_3way_diff 1}
354
355                 if {[string match {mode *} $line]
356                         || [string match {new file *} $line]
357                         || [regexp {^(old|new) mode *} $line]
358                         || [string match {deleted file *} $line]
359                         || [string match {deleted symlink} $line]
360                         || [string match {Binary files * and * differ} $line]
361                         || $line eq {\ No newline at end of file}
362                         || [regexp {^\* Unmerged path } $line]} {
363                         set tags {}
364                 } elseif {$is_3way_diff} {
365                         set op [string range $line 0 1]
366                         switch -- $op {
367                         {  } {set tags {}}
368                         {@@} {set tags d_@}
369                         { +} {set tags d_s+}
370                         { -} {set tags d_s-}
371                         {+ } {set tags d_+s}
372                         {- } {set tags d_-s}
373                         {--} {set tags d_--}
374                         {++} {
375                                 if {[regexp {^\+\+([<>]{7} |={7})} $line _g op]} {
376                                         set is_conflict_diff 1
377                                         set line [string replace $line 0 1 {  }]
378                                         set tags d$op
379                                 } else {
380                                         set tags d_++
381                                 }
382                         }
383                         default {
384                                 puts "error: Unhandled 3 way diff marker: {$op}"
385                                 set tags {}
386                         }
387                         }
388                 } elseif {$is_submodule_diff} {
389                         if {$line == ""} continue
390                         if {[regexp {^\* } $line]} {
391                                 set line [string replace $line 0 1 {Submodule }]
392                                 set tags d_@
393                         } else {
394                                 set op [string range $line 0 2]
395                                 switch -- $op {
396                                 {  <} {set tags d_-}
397                                 {  >} {set tags d_+}
398                                 {  W} {set tags {}}
399                                 default {
400                                         puts "error: Unhandled submodule diff marker: {$op}"
401                                         set tags {}
402                                 }
403                                 }
404                         }
405                 } else {
406                         set op [string index $line 0]
407                         switch -- $op {
408                         { } {set tags {}}
409                         {@} {set tags d_@}
410                         {-} {set tags d_-}
411                         {+} {
412                                 if {[regexp {^\+([<>]{7} |={7})} $line _g op]} {
413                                         set is_conflict_diff 1
414                                         set tags d$op
415                                 } else {
416                                         set tags d_+
417                                 }
418                         }
419                         default {
420                                 puts "error: Unhandled 2 way diff marker: {$op}"
421                                 set tags {}
422                         }
423                         }
424                 }
425                 $ui_diff insert end $line $tags
426                 if {[string index $line end] eq "\r"} {
427                         $ui_diff tag add d_cr {end - 2c}
428                 }
429                 $ui_diff insert end "\n" $tags
430         }
431         $ui_diff conf -state disabled
432
433         if {[eof $fd]} {
434                 close $fd
435
436                 if {$current_diff_queue ne {}} {
437                         advance_diff_queue $cont_info
438                         return
439                 }
440
441                 set diff_active 0
442                 unlock_index
443                 set scroll_pos [lindex $cont_info 0]
444                 if {$scroll_pos ne {}} {
445                         update
446                         $ui_diff yview moveto $scroll_pos
447                 }
448                 ui_ready
449
450                 if {[$ui_diff index end] eq {2.0}} {
451                         handle_empty_diff
452                 } else {
453                         set diff_empty_count 0
454                 }
455
456                 set callback [lindex $cont_info 1]
457                 if {$callback ne {}} {
458                         eval $callback
459                 }
460         }
461 }
462
463 proc apply_hunk {x y} {
464         global current_diff_path current_diff_header current_diff_side
465         global ui_diff ui_index file_states
466
467         if {$current_diff_path eq {} || $current_diff_header eq {}} return
468         if {![lock_index apply_hunk]} return
469
470         set apply_cmd {apply --cached --whitespace=nowarn}
471         set mi [lindex $file_states($current_diff_path) 0]
472         if {$current_diff_side eq $ui_index} {
473                 set failed_msg [mc "Failed to unstage selected hunk."]
474                 lappend apply_cmd --reverse
475                 if {[string index $mi 0] ne {M}} {
476                         unlock_index
477                         return
478                 }
479         } else {
480                 set failed_msg [mc "Failed to stage selected hunk."]
481                 if {[string index $mi 1] ne {M}} {
482                         unlock_index
483                         return
484                 }
485         }
486
487         set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0]
488         set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0]
489         if {$s_lno eq {}} {
490                 unlock_index
491                 return
492         }
493
494         set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end]
495         if {$e_lno eq {}} {
496                 set e_lno end
497         }
498
499         if {[catch {
500                 set enc [get_path_encoding $current_diff_path]
501                 set p [eval git_write $apply_cmd]
502                 fconfigure $p -translation binary -encoding $enc
503                 puts -nonewline $p $current_diff_header
504                 puts -nonewline $p [$ui_diff get $s_lno $e_lno]
505                 close $p} err]} {
506                 error_popup [append $failed_msg "\n\n$err"]
507                 unlock_index
508                 return
509         }
510
511         $ui_diff conf -state normal
512         $ui_diff delete $s_lno $e_lno
513         $ui_diff conf -state disabled
514
515         if {[$ui_diff get 1.0 end] eq "\n"} {
516                 set o _
517         } else {
518                 set o ?
519         }
520
521         if {$current_diff_side eq $ui_index} {
522                 set mi ${o}M
523         } elseif {[string index $mi 0] eq {_}} {
524                 set mi M$o
525         } else {
526                 set mi ?$o
527         }
528         unlock_index
529         display_file $current_diff_path $mi
530         # This should trigger shift to the next changed file
531         if {$o eq {_}} {
532                 reshow_diff
533         }
534 }
535
536 proc apply_line {x y} {
537         global current_diff_path current_diff_header current_diff_side
538         global ui_diff ui_index file_states
539
540         if {$current_diff_path eq {} || $current_diff_header eq {}} return
541         if {![lock_index apply_hunk]} return
542
543         set apply_cmd {apply --cached --whitespace=nowarn}
544         set mi [lindex $file_states($current_diff_path) 0]
545         if {$current_diff_side eq $ui_index} {
546                 set failed_msg [mc "Failed to unstage selected line."]
547                 set to_context {+}
548                 lappend apply_cmd --reverse
549                 if {[string index $mi 0] ne {M}} {
550                         unlock_index
551                         return
552                 }
553         } else {
554                 set failed_msg [mc "Failed to stage selected line."]
555                 set to_context {-}
556                 if {[string index $mi 1] ne {M}} {
557                         unlock_index
558                         return
559                 }
560         }
561
562         set the_l [$ui_diff index @$x,$y]
563
564         # operate only on change lines
565         set c1 [$ui_diff get "$the_l linestart"]
566         if {$c1 ne {+} && $c1 ne {-}} {
567                 unlock_index
568                 return
569         }
570         set sign $c1
571
572         set i_l [$ui_diff search -backwards -regexp ^@@ $the_l 0.0]
573         if {$i_l eq {}} {
574                 unlock_index
575                 return
576         }
577         # $i_l is now at the beginning of a line
578
579         # pick start line number from hunk header
580         set hh [$ui_diff get $i_l "$i_l + 1 lines"]
581         set hh [lindex [split $hh ,] 0]
582         set hln [lindex [split $hh -] 1]
583
584         # There is a special situation to take care of. Consider this hunk:
585         #
586         #    @@ -10,4 +10,4 @@
587         #     context before
588         #    -old 1
589         #    -old 2
590         #    +new 1
591         #    +new 2
592         #     context after
593         #
594         # We used to keep the context lines in the order they appear in the
595         # hunk. But then it is not possible to correctly stage only
596         # "-old 1" and "+new 1" - it would result in this staged text:
597         #
598         #    context before
599         #    old 2
600         #    new 1
601         #    context after
602         #
603         # (By symmetry it is not possible to *un*stage "old 2" and "new 2".)
604         #
605         # We resolve the problem by introducing an asymmetry, namely, when
606         # a "+" line is *staged*, it is moved in front of the context lines
607         # that are generated from the "-" lines that are immediately before
608         # the "+" block. That is, we construct this patch:
609         #
610         #    @@ -10,4 +10,5 @@
611         #     context before
612         #    +new 1
613         #     old 1
614         #     old 2
615         #     context after
616         #
617         # But we do *not* treat "-" lines that are *un*staged in a special
618         # way.
619         #
620         # With this asymmetry it is possible to stage the change
621         # "old 1" -> "new 1" directly, and to stage the change
622         # "old 2" -> "new 2" by first staging the entire hunk and
623         # then unstaging the change "old 1" -> "new 1".
624
625         # This is non-empty if and only if we are _staging_ changes;
626         # then it accumulates the consecutive "-" lines (after converting
627         # them to context lines) in order to be moved after the "+" change
628         # line.
629         set pre_context {}
630
631         set n 0
632         set i_l [$ui_diff index "$i_l + 1 lines"]
633         set patch {}
634         while {[$ui_diff compare $i_l < "end - 1 chars"] &&
635                [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} {
636                 set next_l [$ui_diff index "$i_l + 1 lines"]
637                 set c1 [$ui_diff get $i_l]
638                 if {[$ui_diff compare $i_l <= $the_l] &&
639                     [$ui_diff compare $the_l < $next_l]} {
640                         # the line to stage/unstage
641                         set ln [$ui_diff get $i_l $next_l]
642                         if {$c1 eq {-}} {
643                                 set n [expr $n+1]
644                                 set patch "$patch$pre_context$ln"
645                         } else {
646                                 set patch "$patch$ln$pre_context"
647                         }
648                         set pre_context {}
649                 } elseif {$c1 ne {-} && $c1 ne {+}} {
650                         # context line
651                         set ln [$ui_diff get $i_l $next_l]
652                         set patch "$patch$pre_context$ln"
653                         set n [expr $n+1]
654                         set pre_context {}
655                 } elseif {$c1 eq $to_context} {
656                         # turn change line into context line
657                         set ln [$ui_diff get "$i_l + 1 chars" $next_l]
658                         if {$c1 eq {-}} {
659                                 set pre_context "$pre_context $ln"
660                         } else {
661                                 set patch "$patch $ln"
662                         }
663                         set n [expr $n+1]
664                 }
665                 set i_l $next_l
666         }
667         set patch "@@ -$hln,$n +$hln,[eval expr $n $sign 1] @@\n$patch"
668
669         if {[catch {
670                 set enc [get_path_encoding $current_diff_path]
671                 set p [eval git_write $apply_cmd]
672                 fconfigure $p -translation binary -encoding $enc
673                 puts -nonewline $p $current_diff_header
674                 puts -nonewline $p $patch
675                 close $p} err]} {
676                 error_popup [append $failed_msg "\n\n$err"]
677         }
678
679         unlock_index
680 }