Merge branch 'ho/dashless'
[git] / git-gui / lib / mergetool.tcl
1 # git-gui merge conflict resolution
2 # parts based on git-mergetool (c) 2006 Theodore Y. Ts'o
3
4 proc merge_resolve_one {stage} {
5         global current_diff_path
6
7         switch -- $stage {
8                 1 { set target [mc "the base version"] }
9                 2 { set target [mc "this branch"] }
10                 3 { set target [mc "the other branch"] }
11         }
12
13         set op_question [mc "Force resolution to %s?
14 Note that the diff shows only conflicting changes.
15
16 %s will be overwritten.
17
18 This operation can be undone only by restarting the merge." \
19                 $target [short_path $current_diff_path]]
20
21         if {[ask_popup $op_question] eq {yes}} {
22                 merge_load_stages $current_diff_path [list merge_force_stage $stage]
23         }
24 }
25
26 proc merge_add_resolution {path} {
27         global current_diff_path ui_workdir
28
29         set after [next_diff_after_action $ui_workdir $path {} {^_?U}]
30
31         update_index \
32                 [mc "Adding resolution for %s" [short_path $path]] \
33                 [list $path] \
34                 [concat $after [list ui_ready]]
35 }
36
37 proc merge_force_stage {stage} {
38         global current_diff_path merge_stages
39
40         if {$merge_stages($stage) ne {}} {
41                 git checkout-index -f --stage=$stage -- $current_diff_path
42         } else {
43                 file delete -- $current_diff_path
44         }
45
46         merge_add_resolution $current_diff_path
47 }
48
49 proc merge_load_stages {path cont} {
50         global merge_stages_fd merge_stages merge_stages_buf
51
52         if {[info exists merge_stages_fd]} {
53                 catch { kill_file_process $merge_stages_fd }
54                 catch { close $merge_stages_fd }
55         }
56
57         set merge_stages(0) {}
58         set merge_stages(1) {}
59         set merge_stages(2) {}
60         set merge_stages(3) {}
61         set merge_stages_buf {}
62
63         set merge_stages_fd [eval git_read ls-files -u -z -- $path]
64
65         fconfigure $merge_stages_fd -blocking 0 -translation binary -encoding binary
66         fileevent $merge_stages_fd readable [list read_merge_stages $merge_stages_fd $cont]
67 }
68
69 proc read_merge_stages {fd cont} {
70         global merge_stages_buf merge_stages_fd merge_stages
71
72         append merge_stages_buf [read $fd]
73         set pck [split $merge_stages_buf "\0"]
74         set merge_stages_buf [lindex $pck end]
75
76         if {[eof $fd] && $merge_stages_buf ne {}} {
77                 lappend pck {}
78                 set merge_stages_buf {}
79         }
80
81         foreach p [lrange $pck 0 end-1] {
82                 set fcols [split $p "\t"]
83                 set cols  [split [lindex $fcols 0] " "]
84                 set stage [lindex $cols 2]
85                 
86                 set merge_stages($stage) [lrange $cols 0 1]
87         }
88
89         if {[eof $fd]} {
90                 close $fd
91                 unset merge_stages_fd
92                 eval $cont
93         }
94 }
95
96 proc merge_resolve_tool {} {
97         global current_diff_path
98
99         merge_load_stages $current_diff_path [list merge_resolve_tool2]
100 }
101
102 proc merge_resolve_tool2 {} {
103         global current_diff_path merge_stages
104
105         # Validate the stages
106         if {$merge_stages(2) eq {} ||
107             [lindex $merge_stages(2) 0] eq {120000} ||
108             [lindex $merge_stages(2) 0] eq {160000} ||
109             $merge_stages(3) eq {} ||
110             [lindex $merge_stages(3) 0] eq {120000} ||
111             [lindex $merge_stages(3) 0] eq {160000}
112         } {
113                 error_popup [mc "Cannot resolve deletion or link conflicts using a tool"]
114                 return
115         }
116
117         if {![file exists $current_diff_path]} {
118                 error_popup [mc "Conflict file does not exist"]
119                 return
120         }
121
122         # Determine the tool to use
123         set tool [get_config merge.tool]
124         if {$tool eq {}} { set tool meld }
125
126         set merge_tool_path [get_config "mergetool.$tool.path"]
127         if {$merge_tool_path eq {}} {
128                 switch -- $tool {
129                 emerge { set merge_tool_path "emacs" }
130                 araxis { set merge_tool_path "compare" }
131                 default { set merge_tool_path $tool }
132                 }
133         }
134
135         # Make file names
136         set filebase [file rootname $current_diff_path]
137         set fileext  [file extension $current_diff_path]
138         set basename [lindex [file split $current_diff_path] end]
139
140         set MERGED   $current_diff_path
141         set BASE     "./$MERGED.BASE$fileext"
142         set LOCAL    "./$MERGED.LOCAL$fileext"
143         set REMOTE   "./$MERGED.REMOTE$fileext"
144         set BACKUP   "./$MERGED.BACKUP$fileext"
145
146         set base_stage $merge_stages(1)
147
148         # Build the command line
149         switch -- $tool {
150         kdiff3 {
151                 if {$base_stage ne {}} {
152                         set cmdline [list "$merge_tool_path" --auto --L1 "$MERGED (Base)" \
153                                 --L2 "$MERGED (Local)" --L3 "$MERGED (Remote)" -o "$MERGED" "$BASE" "$LOCAL" "$REMOTE"]
154                 } else {
155                         set cmdline [list "$merge_tool_path" --auto --L1 "$MERGED (Local)" \
156                                 --L2 "$MERGED (Remote)" -o "$MERGED" "$LOCAL" "$REMOTE"]
157                 }
158         }
159         tkdiff {
160                 if {$base_stage ne {}} {
161                         set cmdline [list "$merge_tool_path" -a "$BASE" -o "$MERGED" "$LOCAL" "$REMOTE"]
162                 } else {
163                         set cmdline [list "$merge_tool_path" -o "$MERGED" "$LOCAL" "$REMOTE"]
164                 }
165         }
166         meld {
167                 set cmdline [list "$merge_tool_path" "$LOCAL" "$MERGED" "$REMOTE"]
168         }
169         gvimdiff {
170                 set cmdline [list "$merge_tool_path" -f "$LOCAL" "$MERGED" "$REMOTE"]
171         }
172         xxdiff {
173                 if {$base_stage ne {}} {
174                         set cmdline [list "$merge_tool_path" -X --show-merged-pane \
175                                             -R {Accel.SaveAsMerged: "Ctrl-S"} \
176                                             -R {Accel.Search: "Ctrl+F"} \
177                                             -R {Accel.SearchForward: "Ctrl-G"} \
178                                             --merged-file "$MERGED" "$LOCAL" "$BASE" "$REMOTE"]
179                 } else {
180                         set cmdline [list "$merge_tool_path" -X --show-merged-pane \
181                                             -R {Accel.SaveAsMerged: "Ctrl-S"} \
182                                             -R {Accel.Search: "Ctrl+F"} \
183                                             -R {Accel.SearchForward: "Ctrl-G"} \
184                                             --merged-file "$MERGED" "$LOCAL" "$REMOTE"]
185                 }
186         }
187         opendiff {
188                 if {$base_stage ne {}} {
189                         set cmdline [list "$merge_tool_path" "$LOCAL" "$REMOTE" -ancestor "$BASE" -merge "$MERGED"]
190                 } else {
191                         set cmdline [list "$merge_tool_path" "$LOCAL" "$REMOTE" -merge "$MERGED"]
192                 }
193         }
194         ecmerge {
195                 if {$base_stage ne {}} {
196                         set cmdline [list "$merge_tool_path" "$BASE" "$LOCAL" "$REMOTE" --default --mode=merge3 --to="$MERGED"]
197                 } else {
198                         set cmdline [list "$merge_tool_path" "$LOCAL" "$REMOTE" --default --mode=merge2 --to="$MERGED"]
199                 }
200         }
201         emerge {
202                 if {$base_stage ne {}} {
203                         set cmdline [list "$merge_tool_path" -f emerge-files-with-ancestor-command \
204                                         "$LOCAL" "$REMOTE" "$BASE" "$basename"]
205                 } else {
206                         set cmdline [list "$merge_tool_path" -f emerge-files-command \
207                                         "$LOCAL" "$REMOTE" "$basename"]
208                 }
209         }
210         winmerge {
211                 if {$base_stage ne {}} {
212                         # This tool does not support 3-way merges.
213                         # Use the 'conflict file' resolution feature instead.
214                         set cmdline [list "$merge_tool_path" -e -ub "$MERGED"]
215                 } else {
216                         set cmdline [list "$merge_tool_path" -e -ub -wl \
217                                 -dl "Theirs File" -dr "Mine File" "$REMOTE" "$LOCAL" "$MERGED"]
218                 }
219         }
220         araxis {
221                 if {$base_stage ne {}} {
222                         set cmdline [list "$merge_tool_path" -wait -merge -3 -a1 \
223                                 -title1:"'$MERGED (Base)'" -title2:"'$MERGED (Local)'" \
224                                 -title3:"'$MERGED (Remote)'" \
225                                 "$BASE" "$LOCAL" "$REMOTE" "$MERGED"]
226                 } else {
227                         set cmdline [list "$merge_tool_path" -wait -2 \
228                                  -title1:"'$MERGED (Local)'" -title2:"'$MERGED (Remote)'" \
229                                  "$LOCAL" "$REMOTE" "$MERGED"]
230                 }
231         }
232         p4merge {
233                 set cmdline [list "$merge_tool_path" "$BASE" "$REMOTE" "$LOCAL" "$MERGED"]
234         }
235         vimdiff {
236                 error_popup [mc "Not a GUI merge tool: '%s'" $tool]
237                 return
238         }
239         default {
240                 error_popup [mc "Unsupported merge tool '%s'" $tool]
241                 return
242         }
243         }
244
245         merge_tool_start $cmdline $MERGED $BACKUP [list $BASE $LOCAL $REMOTE]
246 }
247
248 proc delete_temp_files {files} {
249         foreach fname $files {
250                 file delete $fname
251         }
252 }
253
254 proc merge_tool_get_stages {target stages} {
255         global merge_stages
256
257         set i 1
258         foreach fname $stages {
259                 if {$merge_stages($i) eq {}} {
260                         file delete $fname
261                         catch { close [open $fname w] }
262                 } else {
263                         # A hack to support autocrlf properly
264                         git checkout-index -f --stage=$i -- $target
265                         file rename -force -- $target $fname
266                 }
267                 incr i
268         }
269 }
270
271 proc merge_tool_start {cmdline target backup stages} {
272         global merge_stages mtool_target mtool_tmpfiles mtool_fd mtool_mtime
273
274         if {[info exists mtool_fd]} {
275                 if {[ask_popup [mc "Merge tool is already running, terminate it?"]] eq {yes}} {
276                         catch { kill_file_process $mtool_fd }
277                         catch { close $mtool_fd }
278                         unset mtool_fd
279
280                         set old_backup [lindex $mtool_tmpfiles end]
281                         file rename -force -- $old_backup $mtool_target
282                         delete_temp_files $mtool_tmpfiles
283                 } else {
284                         return
285                 }
286         }
287
288         # Save the original file
289         file rename -force -- $target $backup
290
291         # Get the blobs; it destroys $target
292         if {[catch {merge_tool_get_stages $target $stages} err]} {
293                 file rename -force -- $backup $target
294                 delete_temp_files $stages
295                 error_popup [mc "Error retrieving versions:\n%s" $err]
296                 return
297         }
298
299         # Restore the conflict file
300         file copy -force -- $backup $target
301
302         # Initialize global state
303         set mtool_target $target
304         set mtool_mtime [file mtime $target]
305         set mtool_tmpfiles $stages
306
307         lappend mtool_tmpfiles $backup
308
309         # Force redirection to avoid interpreting output on stderr
310         # as an error, and launch the tool
311         lappend cmdline {2>@1}
312
313         if {[catch { set mtool_fd [_open_stdout_stderr $cmdline] } err]} {
314                 delete_temp_files $mtool_tmpfiles
315                 error_popup [mc "Could not start the merge tool:\n\n%s" $err]
316                 return
317         }
318
319         ui_status [mc "Running merge tool..."]
320
321         fconfigure $mtool_fd -blocking 0 -translation binary -encoding binary
322         fileevent $mtool_fd readable [list read_mtool_output $mtool_fd]
323 }
324
325 proc read_mtool_output {fd} {
326         global mtool_fd mtool_tmpfiles
327
328         read $fd
329         if {[eof $fd]} {
330                 unset mtool_fd
331
332                 fconfigure $fd -blocking 1
333                 merge_tool_finish $fd
334         }
335 }
336
337 proc merge_tool_finish {fd} {
338         global mtool_tmpfiles mtool_target mtool_mtime
339
340         set backup [lindex $mtool_tmpfiles end]
341         set failed 0
342
343         # Check the return code
344         if {[catch {close $fd} err]} {
345                 set failed 1
346                 if {$err ne {child process exited abnormally}} {
347                         error_popup [strcat [mc "Merge tool failed."] "\n\n$err"]
348                 }
349         }
350
351         # Check the modification time of the target file
352         if {!$failed && [file mtime $mtool_target] eq $mtool_mtime} {
353                 if {[ask_popup [mc "File %s unchanged, still accept as resolved?" \
354                                 [short_path $mtool_target]]] ne {yes}} {
355                         set failed 1
356                 }
357         }
358
359         # Finish
360         if {$failed} {
361                 file rename -force -- $backup $mtool_target
362                 delete_temp_files $mtool_tmpfiles
363                 ui_status [mc "Merge tool failed."]
364         } else {
365                 if {[is_config_true merge.keepbackup]} {
366                         file rename -force -- $backup "$mtool_target.orig"
367                 }
368
369                 delete_temp_files $mtool_tmpfiles
370
371                 merge_add_resolution $mtool_target
372         }
373 }