Skip to content

Commit 7e30682

Browse files
angavrilovspearce
authored andcommitted
git-gui: Support calling merge tools.
Adds an item to the diff context menu in conflict mode, which invokes a merge tool for the selected file. Tool command-line handling code was ported from git-mergetool. Automatic default tool selection and custom merge tools are not supported. If merge.tool is not set, git-gui defaults to meld. This implementation uses a checkout-index hack in order to retrieve all stages with autocrlf and filters properly applied. It requires temporarily moving the original conflict file out of the way. Signed-off-by: Alexander Gavrilov <angavrilov@gmail.com> Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
1 parent 042c232 commit 7e30682

File tree

3 files changed

+260
-0
lines changed

3 files changed

+260
-0
lines changed

git-gui.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,8 @@ proc apply_config {} {
657657
}
658658
659659
set default_config(branch.autosetupmerge) true
660+
set default_config(merge.tool) {}
661+
set default_config(merge.keepbackup) true
660662
set default_config(merge.diffstat) true
661663
set default_config(merge.summary) false
662664
set default_config(merge.verbosity) 2
@@ -2775,6 +2777,11 @@ create_common_diff_popup $ctxm
27752777
27762778
set ctxmmg .vpane.lower.diff.body.ctxmmg
27772779
menu $ctxmmg -tearoff 0
2780+
$ctxmmg add command \
2781+
-label [mc "Run Merge Tool"] \
2782+
-command {merge_resolve_tool}
2783+
lappend diff_actions [list $ctxmmg entryconf [$ctxmmg index last] -state]
2784+
$ctxmmg add separator
27782785
$ctxmmg add command \
27792786
-label [mc "Use Remote Version"] \
27802787
-command {merge_resolve_one 3}

lib/mergetool.tcl

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,255 @@ proc read_merge_stages {fd cont} {
9696
eval $cont
9797
}
9898
}
99+
100+
proc merge_resolve_tool {} {
101+
global current_diff_path
102+
103+
merge_load_stages $current_diff_path [list merge_resolve_tool2]
104+
}
105+
106+
proc merge_resolve_tool2 {} {
107+
global current_diff_path merge_stages
108+
109+
# Validate the stages
110+
if {$merge_stages(2) eq {} ||
111+
[lindex $merge_stages(2) 0] eq {120000} ||
112+
[lindex $merge_stages(2) 0] eq {160000} ||
113+
$merge_stages(3) eq {} ||
114+
[lindex $merge_stages(3) 0] eq {120000} ||
115+
[lindex $merge_stages(3) 0] eq {160000}
116+
} {
117+
error_popup [mc "Cannot resolve deletion or link conflicts using a tool"]
118+
return
119+
}
120+
121+
if {![file exists $current_diff_path]} {
122+
error_popup [mc "Conflict file does not exist"]
123+
return
124+
}
125+
126+
# Determine the tool to use
127+
set tool [get_config merge.tool]
128+
if {$tool eq {}} { set tool meld }
129+
130+
set merge_tool_path [get_config "mergetool.$tool.path"]
131+
if {$merge_tool_path eq {}} {
132+
switch -- $tool {
133+
emerge { set merge_tool_path "emacs" }
134+
default { set merge_tool_path $tool }
135+
}
136+
}
137+
138+
# Make file names
139+
set filebase [file rootname $current_diff_path]
140+
set fileext [file extension $current_diff_path]
141+
set basename [lindex [file split $current_diff_path] end]
142+
143+
set MERGED $current_diff_path
144+
set BASE "./$MERGED.BASE$fileext"
145+
set LOCAL "./$MERGED.LOCAL$fileext"
146+
set REMOTE "./$MERGED.REMOTE$fileext"
147+
set BACKUP "./$MERGED.BACKUP$fileext"
148+
149+
set base_stage $merge_stages(1)
150+
151+
# Build the command line
152+
switch -- $tool {
153+
kdiff3 {
154+
if {$base_stage ne {}} {
155+
set cmdline [list "$merge_tool_path" --auto --L1 "$MERGED (Base)" \
156+
--L2 "$MERGED (Local)" --L3 "$MERGED (Remote)" -o "$MERGED" "$BASE" "$LOCAL" "$REMOTE"]
157+
} else {
158+
set cmdline [list "$merge_tool_path" --auto --L1 "$MERGED (Local)" \
159+
--L2 "$MERGED (Remote)" -o "$MERGED" "$LOCAL" "$REMOTE"]
160+
}
161+
}
162+
tkdiff {
163+
if {$base_stage ne {}} {
164+
set cmdline [list "$merge_tool_path" -a "$BASE" -o "$MERGED" "$LOCAL" "$REMOTE"]
165+
} else {
166+
set cmdline [list "$merge_tool_path" -o "$MERGED" "$LOCAL" "$REMOTE"]
167+
}
168+
}
169+
meld {
170+
set cmdline [list "$merge_tool_path" "$LOCAL" "$MERGED" "$REMOTE"]
171+
}
172+
gvimdiff {
173+
set cmdline [list "$merge_tool_path" -f "$LOCAL" "$MERGED" "$REMOTE"]
174+
}
175+
xxdiff {
176+
if {$base_stage ne {}} {
177+
set cmdline [list "$merge_tool_path" -X --show-merged-pane \
178+
-R {Accel.SaveAsMerged: "Ctrl-S"} \
179+
-R {Accel.Search: "Ctrl+F"} \
180+
-R {Accel.SearchForward: "Ctrl-G"} \
181+
--merged-file "$MERGED" "$LOCAL" "$BASE" "$REMOTE"]
182+
} else {
183+
set cmdline [list "$merge_tool_path" -X --show-merged-pane \
184+
-R {Accel.SaveAsMerged: "Ctrl-S"} \
185+
-R {Accel.Search: "Ctrl+F"} \
186+
-R {Accel.SearchForward: "Ctrl-G"} \
187+
--merged-file "$MERGED" "$LOCAL" "$REMOTE"]
188+
}
189+
}
190+
opendiff {
191+
if {$base_stage ne {}} {
192+
set cmdline [list "$merge_tool_path" "$LOCAL" "$REMOTE" -ancestor "$BASE" -merge "$MERGED"]
193+
} else {
194+
set cmdline [list "$merge_tool_path" "$LOCAL" "$REMOTE" -merge "$MERGED"]
195+
}
196+
}
197+
ecmerge {
198+
if {$base_stage ne {}} {
199+
set cmdline [list "$merge_tool_path" "$BASE" "$LOCAL" "$REMOTE" --default --mode=merge3 --to="$MERGED"]
200+
} else {
201+
set cmdline [list "$merge_tool_path" "$LOCAL" "$REMOTE" --default --mode=merge2 --to="$MERGED"]
202+
}
203+
}
204+
emerge {
205+
if {$base_stage ne {}} {
206+
set cmdline [list "$merge_tool_path" -f emerge-files-with-ancestor-command \
207+
"$LOCAL" "$REMOTE" "$BASE" "$basename"]
208+
} else {
209+
set cmdline [list "$merge_tool_path" -f emerge-files-command \
210+
"$LOCAL" "$REMOTE" "$basename"]
211+
}
212+
}
213+
vimdiff {
214+
error_popup [mc "Not a GUI merge tool: '%s'" $tool]
215+
return
216+
}
217+
default {
218+
error_popup [mc "Unsupported merge tool '%s'" $tool]
219+
return
220+
}
221+
}
222+
223+
merge_tool_start $cmdline $MERGED $BACKUP [list $BASE $LOCAL $REMOTE]
224+
}
225+
226+
proc delete_temp_files {files} {
227+
foreach fname $files {
228+
file delete $fname
229+
}
230+
}
231+
232+
proc merge_tool_get_stages {target stages} {
233+
global merge_stages
234+
235+
set i 1
236+
foreach fname $stages {
237+
if {$merge_stages($i) eq {}} {
238+
file delete $fname
239+
} else {
240+
# A hack to support autocrlf properly
241+
git checkout-index -f --stage=$i -- $target
242+
file rename -force -- $target $fname
243+
}
244+
incr i
245+
}
246+
}
247+
248+
proc merge_tool_start {cmdline target backup stages} {
249+
global merge_stages mtool_target mtool_tmpfiles mtool_fd mtool_mtime
250+
251+
if {[info exists mtool_fd]} {
252+
if {[ask_popup [mc "Merge tool is already running, terminate it?"]] eq {yes}} {
253+
catch { kill_file_process $mtool_fd }
254+
catch { close $mtool_fd }
255+
unset mtool_fd
256+
257+
set old_backup [lindex $mtool_tmpfiles end]
258+
file rename -force -- $old_backup $mtool_target
259+
delete_temp_files $mtool_tmpfiles
260+
} else {
261+
return
262+
}
263+
}
264+
265+
# Save the original file
266+
file rename -force -- $target $backup
267+
268+
# Get the blobs; it destroys $target
269+
if {[catch {merge_tool_get_stages $target $stages} err]} {
270+
file rename -force -- $backup $target
271+
delete_temp_files $stages
272+
error_popup [mc "Error retrieving versions:\n%s" $err]
273+
return
274+
}
275+
276+
# Restore the conflict file
277+
file copy -force -- $backup $target
278+
279+
# Initialize global state
280+
set mtool_target $target
281+
set mtool_mtime [file mtime $target]
282+
set mtool_tmpfiles $stages
283+
284+
lappend mtool_tmpfiles $backup
285+
286+
# Force redirection to avoid interpreting output on stderr
287+
# as an error, and launch the tool
288+
lappend cmdline {2>@1}
289+
290+
if {[catch { set mtool_fd [_open_stdout_stderr $cmdline] } err]} {
291+
delete_temp_files $mtool_tmpfiles
292+
error_popup [mc "Could not start the merge tool:\n\n%s" $err]
293+
return
294+
}
295+
296+
ui_status [mc "Running merge tool..."]
297+
298+
fconfigure $mtool_fd -blocking 0 -translation binary -encoding binary
299+
fileevent $mtool_fd readable [list read_mtool_output $mtool_fd]
300+
}
301+
302+
proc read_mtool_output {fd} {
303+
global mtool_fd mtool_tmpfiles
304+
305+
read $fd
306+
if {[eof $fd]} {
307+
unset mtool_fd
308+
309+
fconfigure $fd -blocking 1
310+
merge_tool_finish $fd
311+
}
312+
}
313+
314+
proc merge_tool_finish {fd} {
315+
global mtool_tmpfiles mtool_target mtool_mtime
316+
317+
set backup [lindex $mtool_tmpfiles end]
318+
set failed 0
319+
320+
# Check the return code
321+
if {[catch {close $fd} err]} {
322+
set failed 1
323+
if {$err ne {child process exited abnormally}} {
324+
error_popup [strcat [mc "Merge tool failed."] "\n\n$err"]
325+
}
326+
}
327+
328+
# Check the modification time of the target file
329+
if {!$failed && [file mtime $mtool_target] eq $mtool_mtime} {
330+
if {[ask_popup [mc "File %s unchanged, still accept as resolved?" \
331+
[short_path $mtool_target]]] ne {yes}} {
332+
set failed 1
333+
}
334+
}
335+
336+
# Finish
337+
if {$failed} {
338+
file rename -force -- $backup $mtool_target
339+
delete_temp_files $mtool_tmpfiles
340+
ui_status [mc "Merge tool failed."]
341+
} else {
342+
if {[is_config_true merge.keepbackup]} {
343+
file rename -force -- $backup "$mtool_target.orig"
344+
}
345+
346+
delete_temp_files $mtool_tmpfiles
347+
348+
merge_add_resolution $mtool_target
349+
}
350+
}

lib/option.tcl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ proc do_options {} {
119119
{b merge.summary {mc "Summarize Merge Commits"}}
120120
{i-1..5 merge.verbosity {mc "Merge Verbosity"}}
121121
{b merge.diffstat {mc "Show Diffstat After Merge"}}
122+
{t merge.tool {mc "Use Merge Tool"}}
122123

123124
{b gui.trustmtime {mc "Trust File Modification Timestamps"}}
124125
{b gui.pruneduringfetch {mc "Prune Tracking Branches During Fetch"}}

0 commit comments

Comments
 (0)