From 4774c704d20e50ad710f65756099c3eedbfbe789 Mon Sep 17 00:00:00 2001
From: Mark Levedahl <mlevedahl@gmail.com>
Date: Wed, 20 Sep 2023 17:56:14 -0400
Subject: [PATCH 01/15] git-gui: remove Tcl 8.4 workaround on 2>@1 redirection

Since b792230 ("git-gui: Show a progress meter for checking out files",
2007-07-08), git-gui includes a workaround for Tcl that does not support
using 2>@1 to redirect stderr to stdout. Tcl added such support in
8.4.7, released in 2004, and this is fully supported in all 8.5
releases.

As git-gui has a hard-coded requirement for Tcl >= 8.5, the workaround
is no longer needed. Delete it.

Signed-off-by: Mark Levedahl <mlevedahl@gmail.com>
Signed-off-by: Johannes Sixt <j6t@kdbg.org>

Signed-off-by: Taylor Blau <me@ttaylorr.com>
---
 git-gui.sh | 21 +++------------------
 1 file changed, 3 insertions(+), 18 deletions(-)

Index: b/git-gui/git-gui.sh
===================================================================
--- a/git-gui/git-gui.sh
+++ b/git-gui/git-gui.sh
@@ -44,6 +44,56 @@ if {[catch {package require Tcl 8.5} err
 
 catch {rename send {}} ; # What an evil concept...
 
+# 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 in the background
+# 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_bg {cmd} {
+	eval exec [make_arglist_safe $cmd] &
+}
+
+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
+}
+
+# End exec/open wrappers
+
 ######################################################################
 ##
 ## locate our library
@@ -144,11 +194,11 @@ unset oguimsg
 
 if {[tk windowingsystem] eq "aqua"} {
 	catch {
-		exec osascript -e [format {
+		safe_exec [list osascript -e [format {
 			tell application "System Events"
 				set frontmost of processes whose unix id is %d to true
 			end tell
-		} [pid]]
+		} [pid]]]
 	}
 }
 
@@ -418,7 +468,7 @@ proc _git_cmd {name} {
 			# Tcl on Windows doesn't know it.
 			#
 			set p [gitexec git-$name]
-			set f [open $p r]
+			set f [safe_open_file $p r]
 			set s [gets $f]
 			close $f
 
@@ -489,7 +539,7 @@ proc _which {what args} {
 # Test a file for a hashbang to identify executable scripts on Windows.
 proc is_shellscript {filename} {
 	if {![file exists $filename]} {return 0}
-	set f [open $filename r]
+	set f [safe_open_file $filename r]
 	fconfigure $f -encoding binary
 	set magic [read $f 2]
 	close $f
@@ -512,6 +562,7 @@ proc open_cmd_pipe {cmd path} {
 	} else {
 		set run [list [shellpath] -c "$cmd \"\$0\"" $path]
 	}
+	set run [make_arglist_safe $run]
 	return [open |$run r]
 }
 
@@ -521,7 +572,7 @@ proc _lappend_nice {cmd_var} {
 
 	if {![info exists _nice]} {
 		set _nice [_which nice]
-		if {[catch {exec $_nice git version}]} {
+		if {[catch {safe_exec [list $_nice git version]}]} {
 			set _nice {}
 		} elseif {[is_Windows] && [file dirname $_nice] ne [file dirname $::_git]} {
 			set _nice {}
@@ -533,7 +584,11 @@ proc _lappend_nice {cmd_var} {
 }
 
 proc git {args} {
-	set fd [eval [list git_read] $args]
+	git_redir $args {}
+}
+
+proc git_redir {cmd redir} {
+	set fd [git_read $cmd $redir]
 	fconfigure $fd -translation binary -encoding utf-8
 	set result [string trimright [read $fd] "\n"]
 	close $fd
@@ -543,88 +598,47 @@ proc git {args} {
 	return $result
 }
 
-proc _open_stdout_stderr {cmd} {
-	_trace_exec $cmd
+proc safe_open_command {cmd {redir {}}} {
+	set cmd [make_arglist_safe $cmd]
+	_trace_exec [concat $cmd $redir]
 	if {[catch {
-			set fd [open [concat [list | ] $cmd] r]
-		} err]} {
-		if {   [lindex $cmd end] eq {2>@1}
-		    && $err eq {can not find channel named "1"}
-			} {
-			# Older versions of Tcl 8.4 don't have this 2>@1 IO
-			# redirect operator.  Fallback to |& cat for those.
-			# The command was not actually started, so its safe
-			# to try to start it a second time.
-			#
-			set fd [open [concat \
-				[list | ] \
-				[lrange $cmd 0 end-1] \
-				[list |& cat] \
-				] r]
-		} else {
-			error $err
-		}
+		set fd [open [concat [list | ] $cmd $redir] r]
+	} err]} {
+		error $err
 	}
 	fconfigure $fd -eofchar {}
 	return $fd
 }
 
-proc git_read {args} {
-	set opt [list]
-
-	while {1} {
-		switch -- [lindex $args 0] {
-		--nice {
-			_lappend_nice opt
-		}
-
-		--stderr {
-			lappend args 2>@1
-		}
+proc git_read {cmd {redir {}}} {
+	set cmdp [_git_cmd [lindex $cmd 0]]
+	set cmd [lrange $cmd 1 end]
 
-		default {
-			break
-		}
-
-		}
-
-		set args [lrange $args 1 end]
-	}
-
-	set cmdp [_git_cmd [lindex $args 0]]
-	set args [lrange $args 1 end]
-
-	return [_open_stdout_stderr [concat $opt $cmdp $args]]
+	return [safe_open_command [concat $cmdp $cmd] $redir]
 }
 
-proc git_write {args} {
+proc git_read_nice {cmd} {
 	set opt [list]
 
-	while {1} {
-		switch -- [lindex $args 0] {
-		--nice {
-			_lappend_nice opt
-		}
+	_lappend_nice opt
 
-		default {
-			break
-		}
+	set cmdp [_git_cmd [lindex $cmd 0]]
+	set cmd [lrange $cmd 1 end]
 
-		}
-
-		set args [lrange $args 1 end]
-	}
+	return [safe_open_command [concat $opt $cmdp $cmd]]
+}
 
-	set cmdp [_git_cmd [lindex $args 0]]
-	set args [lrange $args 1 end]
+proc git_write {cmd} {
+	set cmd [make_arglist_safe $cmd]
+	set cmdp [_git_cmd [lindex $cmd 0]]
+	set cmd [lrange $cmd 1 end]
 
-	_trace_exec [concat $opt $cmdp $args]
-	return [open [concat [list | ] $opt $cmdp $args] w]
+	_trace_exec [concat $cmdp $cmd]
+	return [open [concat [list | ] $cmdp $cmd] w]
 }
 
 proc githook_read {hook_name args} {
 	set pchook [gitdir hooks $hook_name]
-	lappend args 2>@1
 
 	# On Windows [file executable] might lie so we need to ask
 	# the shell if the hook is executable.  Yes that's annoying.
@@ -640,11 +654,11 @@ proc githook_read {hook_name args} {
 
 		set scr {if test -x "$1";then exec "$@";fi}
 		set sh_c [list $interp -c $scr $interp $pchook]
-		return [_open_stdout_stderr [concat $sh_c $args]]
+		return [safe_open_command [concat $sh_c $args] [list 2>@1]]
 	}
 
 	if {[file executable $pchook]} {
-		return [_open_stdout_stderr [concat [list $pchook] $args]]
+		return [safe_open_command [concat [list $pchook] $args] [list 2>@1]]
 	}
 
 	return {}
@@ -655,9 +669,9 @@ proc kill_file_process {fd} {
 
 	catch {
 		if {[is_Windows]} {
-			exec taskkill /pid $process
+			safe_exec [list taskkill /pid $process]
 		} else {
-			exec kill $process
+			safe_exec [list kill $process]
 		}
 	}
 }
@@ -683,7 +697,7 @@ proc sq {value} {
 proc load_current_branch {} {
 	global current_branch is_detached
 
-	set fd [open [gitdir HEAD] r]
+	set fd [safe_open_file [gitdir HEAD] r]
 	fconfigure $fd -translation binary -encoding utf-8
 	if {[gets $fd ref] < 1} {
 		set ref {}
@@ -1045,7 +1059,7 @@ You are using [git-version]:
 ## configure our library
 
 set idx [file join $oguilib tclIndex]
-if {[catch {set fd [open $idx r]} err]} {
+if {[catch {set fd [safe_open_file $idx r]} err]} {
 	catch {wm withdraw .}
 	tk_messageBox \
 		-icon error \
@@ -1083,53 +1097,30 @@ unset -nocomplain idx fd
 ##
 ## config file parsing
 
-git-version proc _parse_config {arr_name args} {
-	>= 1.5.3 {
-		upvar $arr_name arr
-		array unset arr
-		set buf {}
-		catch {
-			set fd_rc [eval \
-				[list git_read config] \
-				$args \
-				[list --null --list]]
-			fconfigure $fd_rc -translation binary -encoding utf-8
-			set buf [read $fd_rc]
-			close $fd_rc
-		}
-		foreach line [split $buf "\0"] {
-			if {[regexp {^([^\n]+)\n(.*)$} $line line name value]} {
-				if {[is_many_config $name]} {
-					lappend arr($name) $value
-				} else {
-					set arr($name) $value
-				}
-			} elseif {[regexp {^([^\n]+)$} $line line name]} {
-				# no value given, but interpreting them as
-				# boolean will be handled as true
-				set arr($name) {}
-			}
-		}
-	}
-	default {
-		upvar $arr_name arr
-		array unset arr
-		catch {
-			set fd_rc [eval [list git_read config --list] $args]
-			while {[gets $fd_rc line] >= 0} {
-				if {[regexp {^([^=]+)=(.*)$} $line line name value]} {
-					if {[is_many_config $name]} {
-						lappend arr($name) $value
-					} else {
-						set arr($name) $value
-					}
-				} elseif {[regexp {^([^=]+)$} $line line name]} {
-					# no value given, but interpreting them as
-					# boolean will be handled as true
-					set arr($name) {}
-				}
+proc _parse_config {arr_name args} {
+	upvar $arr_name arr
+	array unset arr
+	set buf {}
+	catch {
+		set fd_rc [eval \
+			[list git_read config] \
+			$args \
+			[list --null --list]]
+		fconfigure $fd_rc -translation binary -encoding utf-8
+		set buf [read $fd_rc]
+		close $fd_rc
+	}
+	foreach line [split $buf "\0"] {
+		if {[regexp {^([^\n]+)\n(.*)$} $line line name value]} {
+			if {[is_many_config $name]} {
+				lappend arr($name) $value
+			} else {
+				set arr($name) $value
 			}
-			close $fd_rc
+		} elseif {[regexp {^([^\n]+)$} $line line name]} {
+			# no value given, but interpreting them as
+			# boolean will be handled as true
+			set arr($name) {}
 		}
 	}
 }
@@ -1412,7 +1403,7 @@ proc repository_state {ctvar hdvar mhvar
 	set merge_head [gitdir MERGE_HEAD]
 	if {[file exists $merge_head]} {
 		set ct merge
-		set fd_mh [open $merge_head r]
+		set fd_mh [safe_open_file $merge_head r]
 		while {[gets $fd_mh line] >= 0} {
 			lappend mh $line
 		}
@@ -1431,7 +1422,7 @@ proc PARENT {} {
 		return $p
 	}
 	if {$empty_tree eq {}} {
-		set empty_tree [git mktree << {}]
+		set empty_tree [git_redir [list mktree] [list << {}]]
 	}
 	return $empty_tree
 }
@@ -1575,7 +1566,7 @@ proc load_message {file {encoding {}}} {
 
 	set f [gitdir $file]
 	if {[file isfile $f]} {
-		if {[catch {set fd [open $f r]}]} {
+		if {[catch {set fd [safe_open_file $f r]}]} {
 			return 0
 		}
 		fconfigure $fd -eofchar {}
@@ -1599,17 +1590,17 @@ proc run_prepare_commit_msg_hook {} {
 	# it will be .git/MERGE_MSG (merge), .git/SQUASH_MSG (squash), or an
 	# empty file but existent file.
 
-	set fd_pcm [open [gitdir PREPARE_COMMIT_MSG] a]
+	set fd_pcm [safe_open_file [gitdir PREPARE_COMMIT_MSG] a]
 
 	if {[file isfile [gitdir MERGE_MSG]]} {
 		set pcm_source "merge"
-		set fd_mm [open [gitdir MERGE_MSG] r]
+		set fd_mm [safe_open_file [gitdir MERGE_MSG] r]
 		fconfigure $fd_mm -encoding utf-8
 		puts -nonewline $fd_pcm [read $fd_mm]
 		close $fd_mm
 	} elseif {[file isfile [gitdir SQUASH_MSG]]} {
 		set pcm_source "squash"
-		set fd_sm [open [gitdir SQUASH_MSG] r]
+		set fd_sm [safe_open_file [gitdir SQUASH_MSG] r]
 		fconfigure $fd_sm -encoding utf-8
 		puts -nonewline $fd_pcm [read $fd_sm]
 		close $fd_sm
@@ -2199,7 +2190,7 @@ proc do_gitk {revs {is_submodule false}}
 			unset env(GIT_DIR)
 			unset env(GIT_WORK_TREE)
 		}
-		eval exec $cmd $revs "--" "--" &
+		safe_exec_bg [concat $cmd $revs "--" "--"]
 
 		set env(GIT_DIR) $_gitdir
 		set env(GIT_WORK_TREE) $_gitworktree
@@ -2236,7 +2227,7 @@ proc do_git_gui {} {
 		set pwd [pwd]
 		cd $current_diff_path
 
-		eval exec $exe gui &
+		safe_exec_bg [concat $exe gui]
 
 		set env(GIT_DIR) $_gitdir
 		set env(GIT_WORK_TREE) $_gitworktree
@@ -2265,16 +2256,18 @@ proc get_explorer {} {
 
 proc do_explore {} {
 	global _gitworktree
-	set explorer [get_explorer]
-	eval exec $explorer [list [file nativename $_gitworktree]] &
+	set cmd [get_explorer]
+	lappend cmd [file nativename $_gitworktree]
+	safe_exec_bg $cmd
 }
 
 # Open file relative to the working tree by the default associated app.
 proc do_file_open {file} {
 	global _gitworktree
-	set explorer [get_explorer]
+	set cmd [get_explorer]
 	set full_file_path [file join $_gitworktree $file]
-	exec $explorer [file nativename $full_file_path] &
+	lappend cmd [file nativename $full_file_path]
+	safe_exec_bg $cmd
 }
 
 set is_quitting 0
@@ -2757,13 +2750,13 @@ if {[is_Windows]} {
 	regsub "/mingw../libexec/git-core/git-gui$" \
 		$normalized "/git-bash.exe" cmdLine
 	if {$cmdLine != $normalized && [file exists $cmdLine]} {
-		set cmdLine [list "Git Bash" $cmdLine &]
+		set cmdLine [list "Git Bash" $cmdLine]
 	} else {
-		set cmdLine [list "Git Bash" bash --login -l &]
+		set cmdLine [list "Git Bash" bash --login -l]
 	}
 	.mbar.repository add command \
 		-label [mc "Git Bash"] \
-		-command {eval exec [auto_execok start] $cmdLine}
+		-command {safe_exec_bg [concat [list [auto_execok start]] $cmdLine]}
 }
 
 if {[is_Windows] || ![is_bare]} {
Index: b/git-gui/lib/checkout_op.tcl
===================================================================
--- a/git-gui/lib/checkout_op.tcl
+++ b/git-gui/lib/checkout_op.tcl
@@ -304,12 +304,12 @@ The rescan will be automatically started
 		_readtree $this
 	} else {
 		ui_status [mc "Refreshing file status..."]
-		set fd [git_read update-index \
+		set fd [git_read [list update-index \
 			-q \
 			--unmerged \
 			--ignore-missing \
 			--refresh \
-			]
+			]]
 		fconfigure $fd -blocking 0 -translation binary
 		fileevent $fd readable [cb _refresh_wait $fd]
 	}
@@ -345,14 +345,15 @@ method _readtree {} {
 		[mc "Updating working directory to '%s'..." [_name $this]] \
 		[mc "files checked out"]]
 
-	set fd [git_read --stderr read-tree \
+	set fd [git_read [list read-tree \
 		-m \
 		-u \
 		-v \
 		--exclude-per-directory=.gitignore \
 		$HEAD \
 		$new_hash \
-		]
+		] \
+		[list 2>@1]]
 	fconfigure $fd -blocking 0 -translation binary
 	fileevent $fd readable [cb _readtree_wait $fd $status_bar_operation]
 }
@@ -510,18 +511,8 @@ method _update_repo_state {} {
 	delete_this
 }
 
-git-version proc _detach_HEAD {log new} {
-	>= 1.5.3 {
-		git update-ref --no-deref -m $log HEAD $new
-	}
-	default {
-		set p [gitdir HEAD]
-		file delete $p
-		set fd [open $p w]
-		fconfigure $fd -translation lf -encoding utf-8
-		puts $fd $new
-		close $fd
-	}
+proc _detach_HEAD {log new} {
+	git update-ref --no-deref -m $log HEAD $new
 }
 
 method _confirm_reset {cur} {
@@ -582,7 +573,7 @@ method _confirm_reset {cur} {
 	pack $w.buttons.cancel -side right -padx 5
 	pack $w.buttons -side bottom -fill x -pady 10 -padx 10
 
-	set fd [git_read rev-list --pretty=oneline $cur ^$new_hash]
+	set fd [git_read [list rev-list --pretty=oneline $cur ^$new_hash]]
 	while {[gets $fd line] > 0} {
 		set abbr [string range $line 0 7]
 		set subj [string range $line 41 end]
Index: b/git-gui/lib/blame.tcl
===================================================================
--- a/git-gui/lib/blame.tcl
+++ b/git-gui/lib/blame.tcl
@@ -480,14 +480,14 @@ method _load {jump} {
 		if {$do_textconv ne 0} {
 			set fd [open_cmd_pipe $textconv $path]
 		} else {
-			set fd [open $path r]
+			set fd [safe_open_file $path r]
 		}
 		fconfigure $fd -eofchar {}
 	} else {
 		if {$do_textconv ne 0} {
-			set fd [git_read cat-file --textconv "$commit:$path"]
+			set fd [git_read [list cat-file --textconv "$commit:$path"]]
 		} else {
-			set fd [git_read cat-file blob "$commit:$path"]
+			set fd [git_read [list cat-file blob "$commit:$path"]]
 		}
 	}
 	fconfigure $fd \
@@ -616,7 +616,7 @@ method _exec_blame {cur_w cur_d options
 	}
 
 	lappend options -- $path
-	set fd [eval git_read --nice blame $options]
+	set fd [git_read_nice [concat blame $options]]
 	fconfigure $fd -blocking 0 -translation lf -encoding utf-8
 	fileevent $fd readable [cb _read_blame $fd $cur_w $cur_d]
 	set current_fd $fd
@@ -985,7 +985,7 @@ method _showcommit {cur_w lno} {
 		if {[catch {set msg $header($cmit,message)}]} {
 			set msg {}
 			catch {
-				set fd [git_read cat-file commit $cmit]
+				set fd [git_read [list cat-file commit $cmit]]
 				fconfigure $fd -encoding binary -translation lf
 				# By default commits are assumed to be in utf-8
 				set enc utf-8
@@ -1133,7 +1133,7 @@ method _blameparent {} {
 		} else {
 			set diffcmd [list diff-tree --unified=0 $cparent $cmit -- $new_path]
 		}
-		if {[catch {set fd [eval git_read $diffcmd]} err]} {
+		if {[catch {set fd [git_read $diffcmd]} err]} {
 			$status_operation stop [mc "Unable to display parent"]
 			error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
 			return
Index: b/git-gui/lib/choose_repository.tcl
===================================================================
--- a/git-gui/lib/choose_repository.tcl
+++ b/git-gui/lib/choose_repository.tcl
@@ -683,8 +683,8 @@ method _do_clone2 {} {
 			set pwd [pwd]
 			if {[catch {
 				file mkdir [gitdir objects info]
-				set f_in [open [file join $objdir info alternates] r]
-				set f_cp [open [gitdir objects info alternates] w]
+				set f_in [safe_open_file [file join $objdir info alternates] r]
+				set f_cp [safe_open_file [gitdir objects info alternates] w]
 				fconfigure $f_in -translation binary -encoding binary
 				fconfigure $f_cp -translation binary -encoding binary
 				cd $objdir
@@ -773,7 +773,7 @@ method _do_clone2 {} {
 			[cb _do_clone_tags]
 	}
 	shared {
-		set fd [open [gitdir objects info alternates] w]
+		set fd [safe_open_file [gitdir objects info alternates] w]
 		fconfigure $fd -translation binary
 		puts $fd $objdir
 		close $fd
@@ -806,8 +806,8 @@ method _copy_files {objdir tocopy} {
 	}
 	foreach p $tocopy {
 		if {[catch {
-				set f_in [open [file join $objdir $p] r]
-				set f_cp [open [file join .git objects $p] w]
+				set f_in [safe_open_file [file join $objdir $p] r]
+				set f_cp [safe_open_file [file join .git objects $p] w]
 				fconfigure $f_in -translation binary -encoding binary
 				fconfigure $f_cp -translation binary -encoding binary
 
@@ -864,12 +864,12 @@ method _clone_refs {} {
 		error_popup [mc "Not a Git repository: %s" [file tail $origin_url]]
 		return 0
 	}
-	set fd_in [git_read for-each-ref \
+	set fd_in [git_read [list for-each-ref \
 		--tcl \
-		{--format=list %(refname) %(objectname) %(*objectname)}]
+		{--format=list %(refname) %(objectname) %(*objectname)}]]
 	cd $pwd
 
-	set fd [open [gitdir packed-refs] w]
+	set fd [safe_open_file [gitdir packed-refs] w]
 	fconfigure $fd -translation binary
 	puts $fd "# pack-refs with: peeled"
 	while {[gets $fd_in line] >= 0} {
@@ -923,7 +923,7 @@ method _do_clone_full_end {ok} {
 
 		set HEAD {}
 		if {[file exists [gitdir FETCH_HEAD]]} {
-			set fd [open [gitdir FETCH_HEAD] r]
+			set fd [safe_open_file [gitdir FETCH_HEAD] r]
 			while {[gets $fd line] >= 0} {
 				if {[regexp "^(.{40})\t\t" $line line HEAD]} {
 					break
@@ -999,13 +999,14 @@ method _do_clone_checkout {HEAD} {
 		[mc "files"]]
 
 	set readtree_err {}
-	set fd [git_read --stderr read-tree \
+	set fd [git_read [list read-tree \
 		-m \
 		-u \
 		-v \
 		HEAD \
 		HEAD \
-		]
+		] \
+		[list 2>@1]]
 	fconfigure $fd -blocking 0 -translation binary
 	fileevent $fd readable [cb _readtree_wait $fd]
 }
Index: b/git-gui/lib/choose_rev.tcl
===================================================================
--- a/git-gui/lib/choose_rev.tcl
+++ b/git-gui/lib/choose_rev.tcl
@@ -146,14 +146,14 @@ constructor _new {path unmerged_only tit
 	append fmt { %(*subject)}
 	append fmt {]}
 	set all_refn [list]
-	set fr_fd [git_read for-each-ref \
+	set fr_fd [git_read [list for-each-ref \
 		--tcl \
 		--sort=-taggerdate \
 		--format=$fmt \
 		refs/heads \
 		refs/remotes \
 		refs/tags \
-		]
+		]]
 	fconfigure $fr_fd -translation lf -encoding utf-8
 	while {[gets $fr_fd line] > 0} {
 		set line [eval $line]
@@ -176,7 +176,7 @@ constructor _new {path unmerged_only tit
 	close $fr_fd
 
 	if {$unmerged_only} {
-		set fr_fd [git_read rev-list --all ^$::HEAD]
+		set fr_fd [git_read [list rev-list --all ^$::HEAD]]
 		while {[gets $fr_fd sha1] > 0} {
 			if {[catch {set rlst $cmt_refn($sha1)}]} continue
 			foreach refn $rlst {
@@ -579,7 +579,7 @@ method _reflog_last {name} {
 
 	set last {}
 	if {[catch {set last [file mtime [gitdir $name]]}]
-	&& ![catch {set g [open [gitdir logs $name] r]}]} {
+	&& ![catch {set g [safe_open_file [gitdir logs $name] r]}]} {
 		fconfigure $g -translation binary
 		while {[gets $g line] >= 0} {
 			if {[regexp {> ([1-9][0-9]*) } $line line when]} {
Index: b/git-gui/lib/commit.tcl
===================================================================
--- a/git-gui/lib/commit.tcl
+++ b/git-gui/lib/commit.tcl
@@ -27,7 +27,7 @@ You are currently in the middle of a mer
 	if {[catch {
 			set name ""
 			set email ""
-			set fd [git_read cat-file commit $curHEAD]
+			set fd [git_read [list cat-file commit $curHEAD]]
 			fconfigure $fd -encoding binary -translation lf
 			# By default commits are assumed to be in utf-8
 			set enc utf-8
@@ -225,7 +225,7 @@ A good commit message has the following
 	# -- Build the message file.
 	#
 	set msg_p [gitdir GITGUI_EDITMSG]
-	set msg_wt [open $msg_p w]
+	set msg_wt [safe_open_file $msg_p w]
 	fconfigure $msg_wt -translation lf
 	setup_commit_encoding $msg_wt
 	puts $msg_wt $msg
@@ -325,7 +325,7 @@ proc commit_commitmsg_wait {fd_ph curHEA
 
 proc commit_writetree {curHEAD msg_p} {
 	ui_status [mc "Committing changes..."]
-	set fd_wt [git_read write-tree]
+	set fd_wt [git_read [list write-tree]]
 	fileevent $fd_wt readable \
 		[list commit_committree $fd_wt $curHEAD $msg_p]
 }
@@ -350,7 +350,7 @@ proc commit_committree {fd_wt curHEAD ms
 	# -- Verify this wasn't an empty change.
 	#
 	if {$commit_type eq {normal}} {
-		set fd_ot [git_read cat-file commit $PARENT]
+		set fd_ot [git_read [list cat-file commit $PARENT]]
 		fconfigure $fd_ot -encoding binary -translation lf
 		set old_tree [gets $fd_ot]
 		close $fd_ot
@@ -388,8 +388,8 @@ A rescan will be automatically started n
 	foreach p [concat $PARENT $MERGE_HEAD] {
 		lappend cmd -p $p
 	}
-	lappend cmd <$msg_p
-	if {[catch {set cmt_id [eval git $cmd]} err]} {
+	set msgtxt [list <$msg_p]
+	if {[catch {set cmt_id [git_redir $cmd $msgtxt]} err]} {
 		catch {file delete $msg_p}
 		error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"]
 		ui_status [mc "Commit failed."]
@@ -409,7 +409,7 @@ A rescan will be automatically started n
 	if {$commit_type ne {normal}} {
 		append reflogm " ($commit_type)"
 	}
-	set msg_fd [open $msg_p r]
+	set msg_fd [safe_open_file $msg_p r]
 	setup_commit_encoding $msg_fd 1
 	gets $msg_fd subject
 	close $msg_fd
Index: b/git-gui/lib/diff.tcl
===================================================================
--- a/git-gui/lib/diff.tcl
+++ b/git-gui/lib/diff.tcl
@@ -202,7 +202,7 @@ proc show_other_diff {path w m cont_info
 					set sz [string length $content]
 				}
 				file {
-					set fd [open $path r]
+					set fd [safe_open_file $path r]
 					fconfigure $fd \
 						-eofchar {} \
 						-encoding [get_path_encoding $path]
@@ -226,7 +226,7 @@ proc show_other_diff {path w m cont_info
 			$ui_diff insert end \
 				"* [mc "Git Repository (subproject)"]\n" \
 				d_info
-		} elseif {![catch {set type [exec file $path]}]} {
+		} elseif {![catch {set type [safe_exec [list file $path]]}]} {
 			set n [string length $path]
 			if {[string equal -length $n $path $type]} {
 				set type [string range $type $n end]
@@ -338,7 +338,7 @@ proc start_show_diff {cont_info {add_opt
 		}
 	}
 
-	if {[catch {set fd [eval git_read --nice $cmd]} err]} {
+	if {[catch {set fd [git_read_nice $cmd]} err]} {
 		set diff_active 0
 		unlock_index
 		ui_status [mc "Unable to display %s" [escape_path $path]]
@@ -617,7 +617,7 @@ proc apply_or_revert_hunk {x y revert} {
 
 	if {[catch {
 		set enc [get_path_encoding $current_diff_path]
-		set p [eval git_write $apply_cmd]
+		set p [git_write $apply_cmd]
 		fconfigure $p -translation binary -encoding $enc
 		puts -nonewline $p $wholepatch
 		close $p} err]} {
@@ -853,7 +853,7 @@ proc apply_or_revert_range_or_line {x y
 
 	if {[catch {
 		set enc [get_path_encoding $current_diff_path]
-		set p [eval git_write $apply_cmd]
+		set p [git_write $apply_cmd]
 		fconfigure $p -translation binary -encoding $enc
 		puts -nonewline $p $current_diff_header
 		puts -nonewline $p $wholepatch
@@ -890,7 +890,7 @@ proc undo_last_revert {} {
 
 	if {[catch {
 		set enc $last_revert_enc
-		set p [eval git_write $apply_cmd]
+		set p [git_write $apply_cmd]
 		fconfigure $p -translation binary -encoding $enc
 		puts -nonewline $p $last_revert
 		close $p} err]} {
Index: b/git-gui/lib/merge.tcl
===================================================================
--- a/git-gui/lib/merge.tcl
+++ b/git-gui/lib/merge.tcl
@@ -93,7 +93,7 @@ method _start {} {
 	set spec [$w_rev get_tracking_branch]
 	set cmit [$w_rev get_commit]
 
-	set fh [open [gitdir FETCH_HEAD] w]
+	set fh [safe_open_file [gitdir FETCH_HEAD] w]
 	fconfigure $fh -translation lf
 	if {$spec eq {}} {
 		set remote .
@@ -118,7 +118,7 @@ method _start {} {
 		set cmd [list git]
 		lappend cmd merge
 		lappend cmd --strategy=recursive
-		lappend cmd [git fmt-merge-msg <[gitdir FETCH_HEAD]]
+		lappend cmd [git_redir [list fmt-merge-msg] [list <[gitdir FETCH_HEAD]]]
 		lappend cmd HEAD
 		lappend cmd $name
 	}
@@ -239,7 +239,7 @@ Continue with resetting the current chan
 	}
 
 	if {[ask_popup $op_question] eq {yes}} {
-		set fd [git_read --stderr read-tree --reset -u -v HEAD]
+		set fd [git_read [list read-tree --reset -u -v HEAD] [list 2>@1]]
 		fconfigure $fd -blocking 0 -translation binary
 		set status_bar_operation [$::main_status \
 			start \
Index: b/git-gui/lib/mergetool.tcl
===================================================================
--- a/git-gui/lib/mergetool.tcl
+++ b/git-gui/lib/mergetool.tcl
@@ -88,7 +88,7 @@ proc merge_load_stages {path cont} {
 	set merge_stages(3) {}
 	set merge_stages_buf {}
 
-	set merge_stages_fd [eval git_read ls-files -u -z -- {$path}]
+	set merge_stages_fd [git_read [list ls-files -u -z -- $path]]
 
 	fconfigure $merge_stages_fd -blocking 0 -translation binary -encoding binary
 	fileevent $merge_stages_fd readable [list read_merge_stages $merge_stages_fd $cont]
@@ -293,7 +293,7 @@ proc merge_tool_get_stages {target stage
 	foreach fname $stages {
 		if {$merge_stages($i) eq {}} {
 			file delete $fname
-			catch { close [open $fname w] }
+			catch { close [safe_open_file $fname w] }
 		} else {
 			# A hack to support autocrlf properly
 			git checkout-index -f --stage=$i -- $target
@@ -343,9 +343,9 @@ proc merge_tool_start {cmdline target ba
 
 	# Force redirection to avoid interpreting output on stderr
 	# as an error, and launch the tool
-	lappend cmdline {2>@1}
+	set redir [list {2>@1}]
 
-	if {[catch { set mtool_fd [_open_stdout_stderr $cmdline] } err]} {
+	if {[catch { set mtool_fd [safe_open_command $cmdline $redir] } err]} {
 		delete_temp_files $mtool_tmpfiles
 		error_popup [mc "Could not start the merge tool:\n\n%s" $err]
 		return
Index: b/git-gui/lib/remote.tcl
===================================================================
--- a/git-gui/lib/remote.tcl
+++ b/git-gui/lib/remote.tcl
@@ -32,7 +32,7 @@ proc all_tracking_branches {} {
 	}
 
 	if {$pat ne {}} {
-		set fd [eval git_read for-each-ref --format=%(refname) $cmd]
+		set fd [git_read [concat for-each-ref --format=%(refname) $cmd]]
 		while {[gets $fd n] > 0} {
 			foreach spec $pat {
 				set dst [string range [lindex $spec 0] 0 end-2]
@@ -75,7 +75,7 @@ proc load_all_remotes {} {
 
 		foreach name $all_remotes {
 			catch {
-				set fd [open [file join $rm_dir $name] r]
+				set fd [safe_open_file [file join $rm_dir $name] r]
 				while {[gets $fd line] >= 0} {
 					if {[regexp {^URL:[ 	]*(.+)$} $line line url]} {
 						set remote_url($name) $url
@@ -145,7 +145,7 @@ proc add_fetch_entry {r} {
 		}
 	} else {
 		catch {
-			set fd [open [gitdir remotes $r] r]
+			set fd [safe_open_file [gitdir remotes $r] r]
 			while {[gets $fd n] >= 0} {
 				if {[regexp {^Pull:[ \t]*([^:]+):} $n]} {
 					set enable 1
@@ -182,7 +182,7 @@ proc add_push_entry {r} {
 		}
 	} else {
 		catch {
-			set fd [open [gitdir remotes $r] r]
+			set fd [safe_open_file [gitdir remotes $r] r]
 			while {[gets $fd n] >= 0} {
 				if {[regexp {^Push:[ \t]*([^:]+):} $n]} {
 					set enable 1
Index: b/git-gui/lib/shortcut.tcl
===================================================================
--- a/git-gui/lib/shortcut.tcl
+++ b/git-gui/lib/shortcut.tcl
@@ -30,11 +30,11 @@ proc do_cygwin_shortcut {} {
 	global argv0 _gitworktree
 
 	if {[catch {
-		set desktop [exec cygpath \
+		set desktop [safe_exec [list cygpath \
 			--windows \
 			--absolute \
 			--long-name \
-			--desktop]
+			--desktop]]
 		}]} {
 			set desktop .
 	}
@@ -48,14 +48,14 @@ proc do_cygwin_shortcut {} {
 			set fn ${fn}.lnk
 		}
 		if {[catch {
-				set sh [exec cygpath \
+				set sh [safe_exec [list cygpath \
 					--windows \
 					--absolute \
-					/bin/sh.exe]
-				set me [exec cygpath \
+					/bin/sh.exe]]
+				set me [safe_exec [list cygpath \
 					--unix \
 					--absolute \
-					$argv0]
+					$argv0]]
 				win32_create_lnk $fn [list \
 					$sh -c \
 					"CHERE_INVOKING=1 source /etc/profile;[sq $me] &" \
@@ -86,7 +86,7 @@ proc do_macosx_app {} {
 
 				file mkdir $MacOS
 
-				set fd [open [file join $Contents Info.plist] w]
+				set fd [safe_open_file [file join $Contents Info.plist] w]
 				puts $fd {<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
@@ -111,7 +111,7 @@ proc do_macosx_app {} {
 </plist>}
 				close $fd
 
-				set fd [open $exe w]
+				set fd [safe_open_file $exe w]
 				puts $fd "#!/bin/sh"
 				foreach name [lsort [array names env]] {
 					set value $env($name)
Index: b/git-gui/lib/sshkey.tcl
===================================================================
--- a/git-gui/lib/sshkey.tcl
+++ b/git-gui/lib/sshkey.tcl
@@ -7,7 +7,7 @@ proc find_ssh_key {} {
 		~/.ssh/id_rsa.pub ~/.ssh/identity.pub
 	} {
 		if {[file exists $name]} {
-			set fh    [open $name r]
+			set fh    [safe_open_file $name r]
 			set cont  [read $fh]
 			close $fh
 			return [list $name $cont]
@@ -85,7 +85,7 @@ proc make_ssh_key {w} {
 
 	set cmdline [list sh -c {echo | ssh-keygen -q -t rsa -f ~/.ssh/id_rsa 2>&1}]
 
-	if {[catch { set sshkey_fd [_open_stdout_stderr $cmdline] } err]} {
+	if {[catch { set sshkey_fd [safe_open_command $cmdline] } err]} {
 		error_popup [mc "Could not start ssh-keygen:\n\n%s" $err]
 		return
 	}
Index: b/git-gui/lib/win32.tcl
===================================================================
--- a/git-gui/lib/win32.tcl
+++ b/git-gui/lib/win32.tcl
@@ -2,11 +2,11 @@
 # Copyright (C) 2007 Shawn Pearce
 
 proc win32_read_lnk {lnk_path} {
-	return [exec cscript.exe \
+	return [safe_exec [list cscript.exe \
 		/E:jscript \
 		/nologo \
 		[file join $::oguilib win32_shortcut.js] \
-		$lnk_path]
+		$lnk_path]]
 }
 
 proc win32_create_lnk {lnk_path lnk_exec lnk_dir} {
@@ -15,12 +15,13 @@ proc win32_create_lnk {lnk_path lnk_exec
 	set lnk_args [lrange $lnk_exec 1 end]
 	set lnk_exec [lindex $lnk_exec 0]
 
-	eval [list exec wscript.exe \
+	set cmd [list wscript.exe \
 		/E:jscript \
 		/nologo \
 		[file nativename [file join $oguilib win32_shortcut.js]] \
 		$lnk_path \
 		[file nativename [file join $oguilib git-gui.ico]] \
 		$lnk_dir \
-		$lnk_exec] $lnk_args
+		$lnk_exec]
+	safe_exec [concat $cmd $lnk_args]
 }
Index: b/git-gui/lib/console.tcl
===================================================================
--- a/git-gui/lib/console.tcl
+++ b/git-gui/lib/console.tcl
@@ -92,10 +92,9 @@ method _init {} {
 
 method exec {cmd {after {}}} {
 	if {[lindex $cmd 0] eq {git}} {
-		set fd_f [eval git_read --stderr [lrange $cmd 1 end]]
+		set fd_f [git_read [lrange $cmd 1 end] [list 2>@1]]
 	} else {
-		lappend cmd 2>@1
-		set fd_f [_open_stdout_stderr $cmd]
+		set fd_f [safe_open_command $cmd [list 2>@1]]
 	}
 	fconfigure $fd_f -blocking 0 -translation binary
 	fileevent $fd_f readable [cb _read $fd_f $after]
Index: b/git-gui/lib/branch.tcl
===================================================================
--- a/git-gui/lib/branch.tcl
+++ b/git-gui/lib/branch.tcl
@@ -7,7 +7,7 @@ proc load_all_heads {} {
 	set rh refs/heads
 	set rh_len [expr {[string length $rh] + 1}]
 	set all_heads [list]
-	set fd [git_read for-each-ref --format=%(refname) $rh]
+	set fd [git_read [list for-each-ref --format=%(refname) $rh]]
 	fconfigure $fd -translation binary -encoding utf-8
 	while {[gets $fd line] > 0} {
 		if {!$some_heads_tracking || ![is_tracking_branch $line]} {
@@ -21,10 +21,10 @@ proc load_all_heads {} {
 
 proc load_all_tags {} {
 	set all_tags [list]
-	set fd [git_read for-each-ref \
+	set fd [git_read [list for-each-ref \
 		--sort=-taggerdate \
 		--format=%(refname) \
-		refs/tags]
+		refs/tags]]
 	fconfigure $fd -translation binary -encoding utf-8
 	while {[gets $fd line] > 0} {
 		if {![regsub ^refs/tags/ $line {} name]} continue
Index: b/git-gui/lib/browser.tcl
===================================================================
--- a/git-gui/lib/browser.tcl
+++ b/git-gui/lib/browser.tcl
@@ -196,7 +196,7 @@ method _ls {tree_id {name {}}} {
 	lappend browser_stack [list $tree_id $name]
 	$w conf -state disabled
 
-	set fd [git_read ls-tree -z $tree_id]
+	set fd [git_read [list ls-tree -z $tree_id]]
 	fconfigure $fd -blocking 0 -translation binary -encoding utf-8
 	fileevent $fd readable [cb _read $fd]
 }
Index: b/git-gui/lib/database.tcl
===================================================================
--- a/git-gui/lib/database.tcl
+++ b/git-gui/lib/database.tcl
@@ -3,7 +3,7 @@
 
 proc do_stats {} {
 	global use_ttk NS
-	set fd [git_read count-objects -v]
+	set fd [git_read [list count-objects -v]]
 	while {[gets $fd line] > 0} {
 		if {[regexp {^([^:]+): (\d+)$} $line _ name value]} {
 			set stats($name) $value
Index: b/git-gui/lib/index.tcl
===================================================================
--- a/git-gui/lib/index.tcl
+++ b/git-gui/lib/index.tcl
@@ -75,7 +75,7 @@ proc update_indexinfo {msg path_list aft
 	if {$batch > 25} {set batch 25}
 
 	set status_bar_operation [$::main_status start $msg [mc "files"]]
-	set fd [git_write update-index -z --index-info]
+	set fd [git_write [list update-index -z --index-info]]
 	fconfigure $fd \
 		-blocking 0 \
 		-buffering full \
@@ -144,7 +144,7 @@ proc update_index {msg path_list after}
 	if {$batch > 25} {set batch 25}
 
 	set status_bar_operation [$::main_status start $msg [mc "files"]]
-	set fd [git_write update-index --add --remove -z --stdin]
+	set fd [git_write [list update-index --add --remove -z --stdin]]
 	fconfigure $fd \
 		-blocking 0 \
 		-buffering full \
@@ -218,13 +218,13 @@ proc checkout_index {msg path_list after
 	if {$batch > 25} {set batch 25}
 
 	set status_bar_operation [$::main_status start $msg [mc "files"]]
-	set fd [git_write checkout-index \
+	set fd [git_write [list checkout-index \
 		--index \
 		--quiet \
 		--force \
 		-z \
 		--stdin \
-		]
+		]]
 	fconfigure $fd \
 		-blocking 0 \
 		-buffering full \
Index: b/git-gui/lib/remote_branch_delete.tcl
===================================================================
--- a/git-gui/lib/remote_branch_delete.tcl
+++ b/git-gui/lib/remote_branch_delete.tcl
@@ -308,7 +308,7 @@ method _load {cache uri} {
 		set full_list [list]
 		set head_cache($cache) [list]
 		set full_cache($cache) [list]
-		set active_ls [git_read ls-remote $uri]
+		set active_ls [git_read [list ls-remote $uri]]
 		fconfigure $active_ls \
 			-blocking 0 \
 			-translation lf \
Index: b/git-gui/lib/tools.tcl
===================================================================
--- a/git-gui/lib/tools.tcl
+++ b/git-gui/lib/tools.tcl
@@ -130,8 +130,7 @@ proc tools_exec {fullname} {
 }
 
 proc tools_run_silent {cmd after} {
-	lappend cmd 2>@1
-	set fd [_open_stdout_stderr $cmd]
+	set fd [safe_open_command $cmd [list 2>@1]]
 
 	fconfigure $fd -blocking 0 -translation binary
 	fileevent $fd readable [list tools_consume_input $fd $after]
