Merge branch 'js/fix-open-exec'

This addresses CVE-2025-27613, Gitk can create and truncate a user's
files:

When a user clones an untrusted repository and runs gitk without
additional command arguments, files for which the user has write
permission can be created and truncated. The option "Support per-file
encoding" must have been enabled before in Gitk's Preferences.  This
option is disabled by default.

The same happens when "Show origin of this line" is used in the main
window (regardless of whether "Support per-file encoding" is enabled or
not).

Signed-off-by: Johannes Sixt <j6t@kdbg.org>
maint
Johannes Sixt 2025-05-14 19:56:27 +02:00 committed by Taylor Blau
commit 27fbab4898
1 changed files with 172 additions and 93 deletions

View File

@ -9,6 +9,92 @@ exec wish "$0" -- "$@"


package require Tk package require Tk



# Wrap exec/open to sanitize arguments

# unsafe arguments begin with redirections or the pipe or background operators
proc is_arg_unsafe {arg} {
regexp {^([<|>&]|2>)} $arg
}

proc make_arg_safe {arg} {
if {[is_arg_unsafe $arg]} {
set arg [file join . $arg]
}
return $arg
}

proc make_arglist_safe {arglist} {
set res {}
foreach arg $arglist {
lappend res [make_arg_safe $arg]
}
return $res
}

# executes one command
# no redirections or pipelines are possible
# cmd is a list that specifies the command and its arguments
# calls `exec` and returns its value
proc safe_exec {cmd} {
eval exec [make_arglist_safe $cmd]
}

# executes one command with redirections
# no pipelines are possible
# cmd is a list that specifies the command and its arguments
# redir is a list that specifies redirections (output, background, constant(!) commands)
# calls `exec` and returns its value
proc safe_exec_redirect {cmd redir} {
eval exec [make_arglist_safe $cmd] $redir
}

proc safe_open_file {filename flags} {
# a file name starting with "|" would attempt to run a process
# but such a file name must be treated as a relative path
# hide the "|" behind "./"
if {[string index $filename 0] eq "|"} {
set filename [file join . $filename]
}
open $filename $flags
}

# opens a command pipeline for reading
# cmd is a list that specifies the command and its arguments
# calls `open` and returns the file id
proc safe_open_command {cmd} {
open |[make_arglist_safe $cmd] r
}

# opens a command pipeline for reading and writing
# cmd is a list that specifies the command and its arguments
# calls `open` and returns the file id
proc safe_open_command_rw {cmd} {
open |[make_arglist_safe $cmd] r+
}

# opens a command pipeline for reading with redirections
# cmd is a list that specifies the command and its arguments
# redir is a list that specifies redirections
# calls `open` and returns the file id
proc safe_open_command_redirect {cmd redir} {
set cmd [make_arglist_safe $cmd]
open |[concat $cmd $redir] r
}

# opens a pipeline with several commands for reading
# cmds is a list of lists, each of which specifies a command and its arguments
# calls `open` and returns the file id
proc safe_open_pipeline {cmds} {
set cmd {}
foreach subcmd $cmds {
set cmd [concat $cmd | [make_arglist_safe $subcmd]]
}
open $cmd r
}

# End exec/open wrappers

proc hasworktree {} { proc hasworktree {} {
return [expr {[exec git rev-parse --is-bare-repository] == "false" && return [expr {[exec git rev-parse --is-bare-repository] == "false" &&
[exec git rev-parse --is-inside-git-dir] == "false"}] [exec git rev-parse --is-inside-git-dir] == "false"}]
@ -134,7 +220,7 @@ proc unmerged_files {files} {
set mlist {} set mlist {}
set nr_unmerged 0 set nr_unmerged 0
if {[catch { if {[catch {
set fd [open "| git ls-files -u" r] set fd [safe_open_command {git ls-files -u}]
} err]} { } err]} {
show_error {} . "[mc "Couldn't get list of unmerged files:"] $err" show_error {} . "[mc "Couldn't get list of unmerged files:"] $err"
exit 1 exit 1
@ -296,7 +382,7 @@ proc parseviewrevs {view revs} {
} elseif {[lsearch -exact $revs --all] >= 0} { } elseif {[lsearch -exact $revs --all] >= 0} {
lappend revs HEAD lappend revs HEAD
} }
if {[catch {set ids [eval exec git rev-parse $revs]} err]} { if {[catch {set ids [safe_exec [concat git rev-parse $revs]]} err]} {
# we get stdout followed by stderr in $err # we get stdout followed by stderr in $err
# for an unknown rev, git rev-parse echoes it and then errors out # for an unknown rev, git rev-parse echoes it and then errors out
set errlines [split $err "\n"] set errlines [split $err "\n"]
@ -374,7 +460,7 @@ proc start_rev_list {view} {
set args $viewargs($view) set args $viewargs($view)
if {$viewargscmd($view) ne {}} { if {$viewargscmd($view) ne {}} {
if {[catch { if {[catch {
set str [exec sh -c $viewargscmd($view)] set str [safe_exec [list sh -c $viewargscmd($view)]]
} err]} { } err]} {
error_popup "[mc "Error executing --argscmd command:"] $err" error_popup "[mc "Error executing --argscmd command:"] $err"
return 0 return 0
@ -412,9 +498,9 @@ proc start_rev_list {view} {
} }


if {[catch { if {[catch {
set fd [open [concat | git log --no-color -z --pretty=raw $show_notes \ set fd [safe_open_command_redirect [concat git log --no-color -z --pretty=raw $show_notes \
--parents --boundary $args --stdin \ --parents --boundary $args --stdin] \
[list "<<[join [concat $revs "--" $files] "\n"]"]] r] [list "<<[join [concat $revs "--" $files] "\n"]"]]
} err]} { } err]} {
error_popup "[mc "Error executing git log:"] $err" error_popup "[mc "Error executing git log:"] $err"
return 0 return 0
@ -448,9 +534,9 @@ proc stop_instance {inst} {
set pid [pid $fd] set pid [pid $fd]


if {$::tcl_platform(platform) eq {windows}} { if {$::tcl_platform(platform) eq {windows}} {
exec taskkill /pid $pid safe_exec [list taskkill /pid $pid]
} else { } else {
exec kill $pid safe_exec [list kill $pid]
} }
} }
catch {close $fd} catch {close $fd}
@ -565,9 +651,9 @@ proc updatecommits {} {
set args $vorigargs($view) set args $vorigargs($view)
} }
if {[catch { if {[catch {
set fd [open [concat | git log --no-color -z --pretty=raw $show_notes \ set fd [safe_open_command_redirect [concat git log --no-color -z --pretty=raw $show_notes \
--parents --boundary $args --stdin \ --parents --boundary $args --stdin] \
[list "<<[join [concat $revs "--" $vfilelimit($view)] "\n"]"]] r] [list "<<[join [concat $revs "--" $vfilelimit($view)] "\n"]"]]
} err]} { } err]} {
error_popup "[mc "Error executing git log:"] $err" error_popup "[mc "Error executing git log:"] $err"
return return
@ -1534,8 +1620,8 @@ proc getcommitlines {fd inst view updating} {
# and if we already know about it, using the rewritten # and if we already know about it, using the rewritten
# parent as a substitute parent for $id's children. # parent as a substitute parent for $id's children.
if {![catch { if {![catch {
set rwid [exec git rev-list --first-parent --max-count=1 \ set rwid [safe_exec [list git rev-list --first-parent --max-count=1 \
$id -- $vfilelimit($view)] $id -- $vfilelimit($view)]]
}]} { }]} {
if {$rwid ne {} && [info exists varcid($view,$rwid)]} { if {$rwid ne {} && [info exists varcid($view,$rwid)]} {
# use $rwid in place of $id # use $rwid in place of $id
@ -1655,7 +1741,7 @@ proc do_readcommit {id} {
global tclencoding global tclencoding


# Invoke git-log to handle automatic encoding conversion # Invoke git-log to handle automatic encoding conversion
set fd [open [concat | git log --no-color --pretty=raw -1 $id] r] set fd [safe_open_command [concat git log --no-color --pretty=raw -1 $id]]
# Read the results using i18n.logoutputencoding # Read the results using i18n.logoutputencoding
fconfigure $fd -translation lf -eofchar {} fconfigure $fd -translation lf -eofchar {}
if {$tclencoding != {}} { if {$tclencoding != {}} {
@ -1791,7 +1877,7 @@ proc readrefs {} {
foreach v {tagids idtags headids idheads otherrefids idotherrefs} { foreach v {tagids idtags headids idheads otherrefids idotherrefs} {
unset -nocomplain $v unset -nocomplain $v
} }
set refd [open [list | git show-ref -d] r] set refd [safe_open_command [list git show-ref -d]]
if {$tclencoding != {}} { if {$tclencoding != {}} {
fconfigure $refd -encoding $tclencoding fconfigure $refd -encoding $tclencoding
} }
@ -1839,7 +1925,7 @@ proc readrefs {} {
set selectheadid {} set selectheadid {}
if {$selecthead ne {}} { if {$selecthead ne {}} {
catch { catch {
set selectheadid [exec git rev-parse --verify $selecthead] set selectheadid [safe_exec [list git rev-parse --verify $selecthead]]
} }
} }
} }
@ -2099,7 +2185,7 @@ proc makewindow {} {
{mc "Reread re&ferences" command rereadrefs} {mc "Reread re&ferences" command rereadrefs}
{mc "&List references" command showrefs -accelerator F2} {mc "&List references" command showrefs -accelerator F2}
{xx "" separator} {xx "" separator}
{mc "Start git &gui" command {exec git gui &}} {mc "Start git &gui" command {safe_exec_redirect [list git gui] [list &]}}
{xx "" separator} {xx "" separator}
{mc "&Quit" command doquit -accelerator Meta1-Q} {mc "&Quit" command doquit -accelerator Meta1-Q}
}} }}
@ -2881,7 +2967,7 @@ proc savestuff {w} {
set remove_tmp 0 set remove_tmp 0
if {[catch { if {[catch {
set try_count 0 set try_count 0
while {[catch {set f [open $config_file_tmp {WRONLY CREAT EXCL}]}]} { while {[catch {set f [safe_open_file $config_file_tmp {WRONLY CREAT EXCL}]}]} {
if {[incr try_count] > 50} { if {[incr try_count] > 50} {
error "Unable to write config file: $config_file_tmp exists" error "Unable to write config file: $config_file_tmp exists"
} }
@ -3597,7 +3683,7 @@ proc gitknewtmpdir {} {
set tmpdir $gitdir set tmpdir $gitdir
} }
set gitktmpformat [file join $tmpdir ".gitk-tmp.XXXXXX"] set gitktmpformat [file join $tmpdir ".gitk-tmp.XXXXXX"]
if {[catch {set gitktmpdir [exec mktemp -d $gitktmpformat]}]} { if {[catch {set gitktmpdir [safe_exec [list mktemp -d $gitktmpformat]]}]} {
set gitktmpdir [file join $gitdir [format ".gitk-tmp.%s" [pid]]] set gitktmpdir [file join $gitdir [format ".gitk-tmp.%s" [pid]]]
} }
if {[catch {file mkdir $gitktmpdir} err]} { if {[catch {file mkdir $gitktmpdir} err]} {
@ -3619,7 +3705,7 @@ proc gitknewtmpdir {} {
proc save_file_from_commit {filename output what} { proc save_file_from_commit {filename output what} {
global nullfile global nullfile


if {[catch {exec git show $filename -- > $output} err]} { if {[catch {safe_exec_redirect [list git show $filename --] [list > $output]} err]} {
if {[string match "fatal: bad revision *" $err]} { if {[string match "fatal: bad revision *" $err]} {
return $nullfile return $nullfile
} }
@ -3684,7 +3770,7 @@ proc external_diff {} {


if {$difffromfile ne {} && $difftofile ne {}} { if {$difffromfile ne {} && $difftofile ne {}} {
set cmd [list [shellsplit $extdifftool] $difffromfile $difftofile] set cmd [list [shellsplit $extdifftool] $difffromfile $difftofile]
if {[catch {set fl [open |$cmd r]} err]} { if {[catch {set fl [safe_open_command $cmd]} err]} {
file delete -force $diffdir file delete -force $diffdir
error_popup "$extdifftool: [mc "command failed:"] $err" error_popup "$extdifftool: [mc "command failed:"] $err"
} else { } else {
@ -3788,7 +3874,7 @@ proc external_blame_diff {} {
# Find the SHA1 ID of the blob for file $fname in the index # Find the SHA1 ID of the blob for file $fname in the index
# at stage 0 or 2 # at stage 0 or 2
proc index_sha1 {fname} { proc index_sha1 {fname} {
set f [open [list | git ls-files -s $fname] r] set f [safe_open_command [list git ls-files -s $fname]]
while {[gets $f line] >= 0} { while {[gets $f line] >= 0} {
set info [lindex [split $line "\t"] 0] set info [lindex [split $line "\t"] 0]
set stage [lindex $info 2] set stage [lindex $info 2]
@ -3848,7 +3934,7 @@ proc external_blame {parent_idx {line {}}} {
# being given an absolute path... # being given an absolute path...
set f [make_relative $f] set f [make_relative $f]
lappend cmdline $base_commit $f lappend cmdline $base_commit $f
if {[catch {eval exec $cmdline &} err]} { if {[catch {safe_exec_redirect $cmdline [list &]} err]} {
error_popup "[mc "git gui blame: command failed:"] $err" error_popup "[mc "git gui blame: command failed:"] $err"
} }
} }
@ -3876,7 +3962,7 @@ proc show_line_source {} {
# must be a merge in progress... # must be a merge in progress...
if {[catch { if {[catch {
# get the last line from .git/MERGE_HEAD # get the last line from .git/MERGE_HEAD
set f [open [file join $gitdir MERGE_HEAD] r] set f [safe_open_file [file join $gitdir MERGE_HEAD] r]
set id [lindex [split [read $f] "\n"] end-1] set id [lindex [split [read $f] "\n"] end-1]
close $f close $f
} err]} { } err]} {
@ -3899,19 +3985,17 @@ proc show_line_source {} {
} }
set line [lindex $h 1] set line [lindex $h 1]
} }
set blameargs {} set blamefile [file join $cdup $flist_menu_file]
if {$from_index ne {}} { if {$from_index ne {}} {
lappend blameargs | git cat-file blob $from_index set blameargs [list \
} [list git cat-file blob $from_index] \
lappend blameargs | git blame -p -L$line,+1 [list git blame -p -L$line,+1 --contents - -- $blamefile]]
if {$from_index ne {}} {
lappend blameargs --contents -
} else { } else {
lappend blameargs $id set blameargs [list \
[list git blame -p -L$line,+1 $id -- $blamefile]]
} }
lappend blameargs -- [file join $cdup $flist_menu_file]
if {[catch { if {[catch {
set f [open $blameargs r] set f [safe_open_pipeline $blameargs]
} err]} { } err]} {
error_popup [mc "Couldn't start git blame: %s" $err] error_popup [mc "Couldn't start git blame: %s" $err]
return return
@ -4836,8 +4920,8 @@ proc do_file_hl {serial} {
# must be "containing:", i.e. we're searching commit info # must be "containing:", i.e. we're searching commit info
return return
} }
set cmd [concat | git diff-tree -r -s --stdin $gdtargs] set cmd [concat git diff-tree -r -s --stdin $gdtargs]
set filehighlight [open $cmd r+] set filehighlight [safe_open_command_rw $cmd]
fconfigure $filehighlight -blocking 0 fconfigure $filehighlight -blocking 0
filerun $filehighlight readfhighlight filerun $filehighlight readfhighlight
set fhl_list {} set fhl_list {}
@ -5266,8 +5350,8 @@ proc get_viewmainhead {view} {
global viewmainheadid vfilelimit viewinstances mainheadid global viewmainheadid vfilelimit viewinstances mainheadid


catch { catch {
set rfd [open [concat | git rev-list -1 $mainheadid \ set rfd [safe_open_command [concat git rev-list -1 $mainheadid \
-- $vfilelimit($view)] r] -- $vfilelimit($view)]]
set j [reg_instance $rfd] set j [reg_instance $rfd]
lappend viewinstances($view) $j lappend viewinstances($view) $j
fconfigure $rfd -blocking 0 fconfigure $rfd -blocking 0
@ -5332,14 +5416,14 @@ proc dodiffindex {} {
if {!$showlocalchanges || !$hasworktree} return if {!$showlocalchanges || !$hasworktree} return
incr lserial incr lserial
if {[package vcompare $git_version "1.7.2"] >= 0} { if {[package vcompare $git_version "1.7.2"] >= 0} {
set cmd "|git diff-index --cached --ignore-submodules=dirty HEAD" set cmd "git diff-index --cached --ignore-submodules=dirty HEAD"
} else { } else {
set cmd "|git diff-index --cached HEAD" set cmd "git diff-index --cached HEAD"
} }
if {$vfilelimit($curview) ne {}} { if {$vfilelimit($curview) ne {}} {
set cmd [concat $cmd -- $vfilelimit($curview)] set cmd [concat $cmd -- $vfilelimit($curview)]
} }
set fd [open $cmd r] set fd [safe_open_command $cmd]
fconfigure $fd -blocking 0 fconfigure $fd -blocking 0
set i [reg_instance $fd] set i [reg_instance $fd]
filerun $fd [list readdiffindex $fd $lserial $i] filerun $fd [list readdiffindex $fd $lserial $i]
@ -5364,11 +5448,11 @@ proc readdiffindex {fd serial inst} {
} }


# now see if there are any local changes not checked in to the index # now see if there are any local changes not checked in to the index
set cmd "|git diff-files" set cmd "git diff-files"
if {$vfilelimit($curview) ne {}} { if {$vfilelimit($curview) ne {}} {
set cmd [concat $cmd -- $vfilelimit($curview)] set cmd [concat $cmd -- $vfilelimit($curview)]
} }
set fd [open $cmd r] set fd [safe_open_command $cmd]
fconfigure $fd -blocking 0 fconfigure $fd -blocking 0
set i [reg_instance $fd] set i [reg_instance $fd]
filerun $fd [list readdifffiles $fd $serial $i] filerun $fd [list readdifffiles $fd $serial $i]
@ -7157,8 +7241,8 @@ proc browseweb {url} {
global web_browser global web_browser


if {$web_browser eq {}} return if {$web_browser eq {}} return
# Use eval here in case $web_browser is a command plus some arguments # Use concat here in case $web_browser is a command plus some arguments
if {[catch {eval exec $web_browser [list $url] &} err]} { if {[catch {safe_exec_redirect [concat $web_browser [list $url]] [list &]} err]} {
error_popup "[mc "Error starting web browser:"] $err" error_popup "[mc "Error starting web browser:"] $err"
} }
} }
@ -7660,13 +7744,13 @@ proc gettree {id} {
if {![info exists treefilelist($id)]} { if {![info exists treefilelist($id)]} {
if {![info exists treepending]} { if {![info exists treepending]} {
if {$id eq $nullid} { if {$id eq $nullid} {
set cmd [list | git ls-files] set cmd [list git ls-files]
} elseif {$id eq $nullid2} { } elseif {$id eq $nullid2} {
set cmd [list | git ls-files --stage -t] set cmd [list git ls-files --stage -t]
} else { } else {
set cmd [list | git ls-tree -r $id] set cmd [list git ls-tree -r $id]
} }
if {[catch {set gtf [open $cmd r]}]} { if {[catch {set gtf [safe_open_command $cmd]}]} {
return return
} }
set treepending $id set treepending $id
@ -7730,13 +7814,13 @@ proc showfile {f} {
return return
} }
if {$diffids eq $nullid} { if {$diffids eq $nullid} {
if {[catch {set bf [open $f r]} err]} { if {[catch {set bf [safe_open_file $f r]} err]} {
puts "oops, can't read $f: $err" puts "oops, can't read $f: $err"
return return
} }
} else { } else {
set blob [lindex $treeidlist($diffids) $i] set blob [lindex $treeidlist($diffids) $i]
if {[catch {set bf [open [concat | git cat-file blob $blob] r]} err]} { if {[catch {set bf [safe_open_command [concat git cat-file blob $blob]]} err]} {
puts "oops, error reading blob $blob: $err" puts "oops, error reading blob $blob: $err"
return return
} }
@ -7886,7 +7970,7 @@ proc diffcmd {ids flags} {
if {$i >= 0} { if {$i >= 0} {
if {[llength $ids] > 1 && $j < 0} { if {[llength $ids] > 1 && $j < 0} {
# comparing working directory with some specific revision # comparing working directory with some specific revision
set cmd [concat | git diff-index $flags] set cmd [concat git diff-index $flags]
if {$i == 0} { if {$i == 0} {
lappend cmd -R [lindex $ids 1] lappend cmd -R [lindex $ids 1]
} else { } else {
@ -7894,7 +7978,7 @@ proc diffcmd {ids flags} {
} }
} else { } else {
# comparing working directory with index # comparing working directory with index
set cmd [concat | git diff-files $flags] set cmd [concat git diff-files $flags]
if {$j == 1} { if {$j == 1} {
lappend cmd -R lappend cmd -R
} }
@ -7903,7 +7987,7 @@ proc diffcmd {ids flags} {
if {[package vcompare $git_version "1.7.2"] >= 0} { if {[package vcompare $git_version "1.7.2"] >= 0} {
set flags "$flags --ignore-submodules=dirty" set flags "$flags --ignore-submodules=dirty"
} }
set cmd [concat | git diff-index --cached $flags] set cmd [concat git diff-index --cached $flags]
if {[llength $ids] > 1} { if {[llength $ids] > 1} {
# comparing index with specific revision # comparing index with specific revision
if {$j == 0} { if {$j == 0} {
@ -7919,7 +8003,7 @@ proc diffcmd {ids flags} {
if {$log_showroot} { if {$log_showroot} {
lappend flags --root lappend flags --root
} }
set cmd [concat | git diff-tree -r $flags $ids] set cmd [concat git diff-tree -r $flags $ids]
} }
return $cmd return $cmd
} }
@ -7931,7 +8015,7 @@ proc gettreediffs {ids} {
if {$limitdiffs && $vfilelimit($curview) ne {}} { if {$limitdiffs && $vfilelimit($curview) ne {}} {
set cmd [concat $cmd -- $vfilelimit($curview)] set cmd [concat $cmd -- $vfilelimit($curview)]
} }
if {[catch {set gdtf [open $cmd r]}]} return if {[catch {set gdtf [safe_open_command $cmd]}]} return


set treepending $ids set treepending $ids
set treediff {} set treediff {}
@ -8051,7 +8135,7 @@ proc getblobdiffs {ids} {
if {$limitdiffs && $vfilelimit($curview) ne {}} { if {$limitdiffs && $vfilelimit($curview) ne {}} {
set cmd [concat $cmd -- $vfilelimit($curview)] set cmd [concat $cmd -- $vfilelimit($curview)]
} }
if {[catch {set bdf [open $cmd r]} err]} { if {[catch {set bdf [safe_open_command $cmd]} err]} {
error_popup [mc "Error getting diffs: %s" $err] error_popup [mc "Error getting diffs: %s" $err]
return return
} }
@ -8768,7 +8852,7 @@ proc gotocommit {} {
set id [lindex $matches 0] set id [lindex $matches 0]
} }
} else { } else {
if {[catch {set id [exec git rev-parse --verify $sha1string]}]} { if {[catch {set id [safe_exec [list git rev-parse --verify $sha1string]]}]} {
error_popup [mc "Revision %s is not known" $sha1string] error_popup [mc "Revision %s is not known" $sha1string]
return return
} }
@ -9074,10 +9158,8 @@ proc getpatchid {id} {


if {![info exists patchids($id)]} { if {![info exists patchids($id)]} {
set cmd [diffcmd [list $id] {-p --root}] set cmd [diffcmd [list $id] {-p --root}]
# trim off the initial "|"
set cmd [lrange $cmd 1 end]
if {[catch { if {[catch {
set x [eval exec $cmd | git patch-id] set x [safe_exec_redirect $cmd [list | git patch-id]]
set patchids($id) [lindex $x 0] set patchids($id) [lindex $x 0]
}]} { }]} {
set patchids($id) "error" set patchids($id) "error"
@ -9173,14 +9255,14 @@ proc diffcommits {a b} {
set fna [file join $tmpdir "commit-[string range $a 0 7]"] set fna [file join $tmpdir "commit-[string range $a 0 7]"]
set fnb [file join $tmpdir "commit-[string range $b 0 7]"] set fnb [file join $tmpdir "commit-[string range $b 0 7]"]
if {[catch { if {[catch {
exec git diff-tree -p --pretty $a >$fna safe_exec_redirect [list git diff-tree -p --pretty $a] [list >$fna]
exec git diff-tree -p --pretty $b >$fnb safe_exec_redirect [list git diff-tree -p --pretty $b] [list >$fnb]
} err]} { } err]} {
error_popup [mc "Error writing commit to file: %s" $err] error_popup [mc "Error writing commit to file: %s" $err]
return return
} }
if {[catch { if {[catch {
set fd [open "| diff -U$diffcontext $fna $fnb" r] set fd [safe_open_command "diff -U$diffcontext $fna $fnb"]
} err]} { } err]} {
error_popup [mc "Error diffing commits: %s" $err] error_popup [mc "Error diffing commits: %s" $err]
return return
@ -9320,10 +9402,7 @@ proc mkpatchgo {} {
set newid [$patchtop.tosha1 get] set newid [$patchtop.tosha1 get]
set fname [$patchtop.fname get] set fname [$patchtop.fname get]
set cmd [diffcmd [list $oldid $newid] -p] set cmd [diffcmd [list $oldid $newid] -p]
# trim off the initial "|" if {[catch {safe_exec_redirect $cmd [list >$fname &]} err]} {
set cmd [lrange $cmd 1 end]
lappend cmd >$fname &
if {[catch {eval exec $cmd} err]} {
error_popup "[mc "Error creating patch:"] $err" $patchtop error_popup "[mc "Error creating patch:"] $err" $patchtop
} }
catch {destroy $patchtop} catch {destroy $patchtop}
@ -9392,9 +9471,9 @@ proc domktag {} {
} }
if {[catch { if {[catch {
if {$msg != {}} { if {$msg != {}} {
exec git tag -a -m $msg $tag $id safe_exec [list git tag -a -m $msg $tag $id]
} else { } else {
exec git tag $tag $id safe_exec [list git tag $tag $id]
} }
} err]} { } err]} {
error_popup "[mc "Error creating tag:"] $err" $mktagtop error_popup "[mc "Error creating tag:"] $err" $mktagtop
@ -9462,7 +9541,7 @@ proc copyreference {} {
if {$autosellen < 40} { if {$autosellen < 40} {
lappend cmd --abbrev=$autosellen lappend cmd --abbrev=$autosellen
} }
set reference [eval exec $cmd $rowmenuid] set reference [safe_exec [concat $cmd $rowmenuid]]


clipboard clear clipboard clear
clipboard append $reference clipboard append $reference
@ -9512,7 +9591,7 @@ proc wrcomgo {} {
set id [$wrcomtop.sha1 get] set id [$wrcomtop.sha1 get]
set cmd "echo $id | [$wrcomtop.cmd get]" set cmd "echo $id | [$wrcomtop.cmd get]"
set fname [$wrcomtop.fname get] set fname [$wrcomtop.fname get]
if {[catch {exec sh -c $cmd >$fname &} err]} { if {[catch {safe_exec_redirect [list sh -c $cmd] [list >$fname &]} err]} {
error_popup "[mc "Error writing commit:"] $err" $wrcomtop error_popup "[mc "Error writing commit:"] $err" $wrcomtop
} }
catch {destroy $wrcomtop} catch {destroy $wrcomtop}
@ -9616,7 +9695,7 @@ proc mkbrgo {top} {
nowbusy newbranch nowbusy newbranch
update update
if {[catch { if {[catch {
eval exec git branch $cmdargs safe_exec [concat git branch $cmdargs]
} err]} { } err]} {
notbusy newbranch notbusy newbranch
error_popup $err error_popup $err
@ -9657,7 +9736,7 @@ proc mvbrgo {top prevname} {
nowbusy renamebranch nowbusy renamebranch
update update
if {[catch { if {[catch {
eval exec git branch $cmdargs safe_exec [concat git branch $cmdargs]
} err]} { } err]} {
notbusy renamebranch notbusy renamebranch
error_popup $err error_popup $err
@ -9698,7 +9777,7 @@ proc exec_citool {tool_args {baseid {}}} {
} }
} }


eval exec git citool $tool_args & safe_exec_redirect [concat git citool $tool_args] [list &]


array unset env GIT_AUTHOR_* array unset env GIT_AUTHOR_*
array set env $save_env array set env $save_env
@ -9721,7 +9800,7 @@ proc cherrypick {} {
update update
# Unfortunately git-cherry-pick writes stuff to stderr even when # Unfortunately git-cherry-pick writes stuff to stderr even when
# no error occurs, and exec takes that as an indication of error... # no error occurs, and exec takes that as an indication of error...
if {[catch {exec sh -c "git cherry-pick -r $rowmenuid 2>&1"} err]} { if {[catch {safe_exec [list sh -c "git cherry-pick -r $rowmenuid 2>&1"]} err]} {
notbusy cherrypick notbusy cherrypick
if {[regexp -line \ if {[regexp -line \
{Entry '(.*)' (would be overwritten by merge|not uptodate)} \ {Entry '(.*)' (would be overwritten by merge|not uptodate)} \
@ -9783,7 +9862,7 @@ proc revert {} {
nowbusy revert [mc "Reverting"] nowbusy revert [mc "Reverting"]
update update


if [catch {exec git revert --no-edit $rowmenuid} err] { if [catch {safe_exec [list git revert --no-edit $rowmenuid]} err] {
notbusy revert notbusy revert
if [regexp {files would be overwritten by merge:(\n(( |\t)+[^\n]+\n)+)}\ if [regexp {files would be overwritten by merge:(\n(( |\t)+[^\n]+\n)+)}\
$err match files] { $err match files] {
@ -9859,8 +9938,8 @@ proc resethead {} {
bind $w <Visibility> "grab $w; focus $w" bind $w <Visibility> "grab $w; focus $w"
tkwait window $w tkwait window $w
if {!$confirm_ok} return if {!$confirm_ok} return
if {[catch {set fd [open \ if {[catch {set fd [safe_open_command_redirect \
[list | git reset --$resettype $rowmenuid 2>@1] r]} err]} { [list git reset --$resettype $rowmenuid] [list 2>@1]]} err]} {
error_popup $err error_popup $err
} else { } else {
dohidelocalchanges dohidelocalchanges
@ -9931,7 +10010,7 @@ proc cobranch {} {


# check the tree is clean first?? # check the tree is clean first??
set newhead $headmenuhead set newhead $headmenuhead
set command [list | git checkout] set command [list git checkout]
if {[string match "remotes/*" $newhead]} { if {[string match "remotes/*" $newhead]} {
set remote $newhead set remote $newhead
set newhead [string range $newhead [expr [string last / $newhead] + 1] end] set newhead [string range $newhead [expr [string last / $newhead] + 1] end]
@ -9945,12 +10024,11 @@ proc cobranch {} {
} else { } else {
lappend command $newhead lappend command $newhead
} }
lappend command 2>@1
nowbusy checkout [mc "Checking out"] nowbusy checkout [mc "Checking out"]
update update
dohidelocalchanges dohidelocalchanges
if {[catch { if {[catch {
set fd [open $command r] set fd [safe_open_command_redirect $command [list 2>@1]]
} err]} { } err]} {
notbusy checkout notbusy checkout
error_popup $err error_popup $err
@ -10016,7 +10094,7 @@ proc rmbranch {} {
} }
nowbusy rmbranch nowbusy rmbranch
update update
if {[catch {exec git branch -D $head} err]} { if {[catch {safe_exec [list git branch -D $head]} err]} {
notbusy rmbranch notbusy rmbranch
error_popup $err error_popup $err
return return
@ -10207,7 +10285,7 @@ proc getallcommits {} {
set cachedarcs 0 set cachedarcs 0
set allccache [file join $gitdir "gitk.cache"] set allccache [file join $gitdir "gitk.cache"]
if {![catch { if {![catch {
set f [open $allccache r] set f [safe_open_file $allccache r]
set allcwait 1 set allcwait 1
getcache $f getcache $f
}]} return }]} return
@ -10216,7 +10294,7 @@ proc getallcommits {} {
if {$allcwait} { if {$allcwait} {
return return
} }
set cmd [list | git rev-list --parents] set cmd [list git rev-list --parents]
set allcupdate [expr {$seeds ne {}}] set allcupdate [expr {$seeds ne {}}]
if {!$allcupdate} { if {!$allcupdate} {
set ids "--all" set ids "--all"
@ -10244,10 +10322,11 @@ proc getallcommits {} {
if {$ids ne {}} { if {$ids ne {}} {
if {$ids eq "--all"} { if {$ids eq "--all"} {
set cmd [concat $cmd "--all"] set cmd [concat $cmd "--all"]
set fd [safe_open_command $cmd]
} else { } else {
set cmd [concat $cmd --stdin [list "<<[join $ids "\n"]"]] set cmd [concat $cmd --stdin]
set fd [safe_open_command_redirect $cmd [list "<<[join $ids "\n"]"]]
} }
set fd [open $cmd r]
fconfigure $fd -blocking 0 fconfigure $fd -blocking 0
incr allcommits incr allcommits
nowbusy allcommits nowbusy allcommits
@ -10637,7 +10716,7 @@ proc savecache {} {
set cachearc 0 set cachearc 0
set cachedarcs $nextarc set cachedarcs $nextarc
catch { catch {
set f [open $allccache w] set f [safe_open_file $allccache w]
puts $f [list 1 $cachedarcs] puts $f [list 1 $cachedarcs]
run writecache $f run writecache $f
} }
@ -11340,7 +11419,7 @@ proc add_tag_ctext {tag} {


if {![info exists cached_tagcontent($tag)]} { if {![info exists cached_tagcontent($tag)]} {
catch { catch {
set cached_tagcontent($tag) [exec git cat-file -p $tag] set cached_tagcontent($tag) [safe_exec [list git cat-file -p $tag]]
} }
} }
$ctext insert end "[mc "Tag"]: $tag\n" bold $ctext insert end "[mc "Tag"]: $tag\n" bold
@ -12226,7 +12305,7 @@ proc gitattr {path attr default} {
set r $path_attr_cache($attr,$path) set r $path_attr_cache($attr,$path)
} else { } else {
set r "unspecified" set r "unspecified"
if {![catch {set line [exec git check-attr $attr -- $path]}]} { if {![catch {set line [safe_exec [list git check-attr $attr -- $path]]}]} {
regexp "(.*): $attr: (.*)" $line m f r regexp "(.*): $attr: (.*)" $line m f r
} }
set path_attr_cache($attr,$path) $r set path_attr_cache($attr,$path) $r
@ -12253,7 +12332,7 @@ proc cache_gitattr {attr pathlist} {
while {$newlist ne {}} { while {$newlist ne {}} {
set head [lrange $newlist 0 [expr {$lim - 1}]] set head [lrange $newlist 0 [expr {$lim - 1}]]
set newlist [lrange $newlist $lim end] set newlist [lrange $newlist $lim end]
if {![catch {set rlist [eval exec git check-attr $attr -- $head]}]} { if {![catch {set rlist [safe_exec [concat git check-attr $attr -- $head]]}]} {
foreach row [split $rlist "\n"] { foreach row [split $rlist "\n"] {
if {[regexp "(.*): $attr: (.*)" $row m path value]} { if {[regexp "(.*): $attr: (.*)" $row m path value]} {
if {[string index $path 0] eq "\""} { if {[string index $path 0] eq "\""} {
@ -12306,11 +12385,11 @@ if {[catch {package require Tk 8.4} err]} {


# on OSX bring the current Wish process window to front # on OSX bring the current Wish process window to front
if {[tk windowingsystem] eq "aqua"} { if {[tk windowingsystem] eq "aqua"} {
exec osascript -e [format { safe_exec [list osascript -e [format {
tell application "System Events" tell application "System Events"
set frontmost of processes whose unix id is %d to true set frontmost of processes whose unix id is %d to true
end tell end tell
} [pid] ] } [pid] ]]
} }


# Unset GIT_TRACE var if set # Unset GIT_TRACE var if set
@ -12555,7 +12634,7 @@ if {$selecthead eq "HEAD"} {
if {$i >= [llength $argv] && $revtreeargs ne {}} { if {$i >= [llength $argv] && $revtreeargs ne {}} {
# no -- on command line, but some arguments (other than --argscmd) # no -- on command line, but some arguments (other than --argscmd)
if {[catch { if {[catch {
set f [eval exec git rev-parse --no-revs --no-flags $revtreeargs] set f [safe_exec [concat git rev-parse --no-revs --no-flags $revtreeargs]]
set cmdline_files [split $f "\n"] set cmdline_files [split $f "\n"]
set n [llength $cmdline_files] set n [llength $cmdline_files]
set revtreeargs [lrange $revtreeargs 0 end-$n] set revtreeargs [lrange $revtreeargs 0 end-$n]