Skip to content
Merged
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 157 additions & 18 deletions bin/git-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)

# awk script that takes 2 input files. The first one is newline deilmeted
# string of patterns to look for, second one is the file to remove them from.
# outfile has to be passed with -v upon call and is the file result is
# written to.
read -r -d '' AWK_SCRIPT <<'EOF'
# process the first "file" (newline split argument list)
NR==FNR { a[b++]=$0; next; }
# process file
{
found=0;
for (i=0; i != b; i++) {
if (a[i] == $0) { found=1; }
}
if (found == 1) { print "... removing '"$0"'"; }
else { print $0 > outfile }
}
EOF



show_contents() {
local file="${2/#~/$HOME}"
if [ -f "$file" ]; then
Expand Down Expand Up @@ -94,29 +114,148 @@ add_patterns() {
done
}

# TODO: create a file if does not exist??
edit_file() {
local file="${2/#~/$HOME}"
if [ -f "$file" ]; then
echo "Editing $1 gitignore: ($2)" && eval "$(git var GIT_EDITOR) $file"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will spaces or shell metacharacters in the $file break the eval since we don't quote them?

@kdergachev kdergachev Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I guess so. The safest way to prevent that seems to be with $(printf '%q' "$file") (${file@Q} should work too, but for old versions of bash is unavailable). Should we do the same to the GIT_EDITOR part or is it assumed to be safe?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do it.

@kdergachev kdergachev Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed it. Though left GIT_EDITOR part the same since I realized that it will then treat the whole string in that variable as the command, but GIT_EDITOR (I suppose) can contain flags or be even more complex than "<editor> <flags>..." (for example try eval " 'vim -e' ").

echo "Done."
else
echo "There is no $1 .gitignore yet." >&2
exit 1
fi
}

_usage() {
# TODO

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's either fulfill it or drop it

echo "Under construction."
}

remove_patterns() {
declare -a args
local tmpfile
args=( "${@:3}" )
tmpfile="$(mktemp -q "${TMPDIR:-/tmp}"/git-extras-ignore.XXXXXX 2>/dev/null)"
local file="${2/#~/$HOME}"
if [ -f "$file" ]; then
echo "Removing patterns from $1 gitignore: $2"
Comment thread
spacewander marked this conversation as resolved.
awk -v outfile="$tmpfile" "${AWK_SCRIPT}"\
<(printf '%s\n' "${args[@]}") "$file"
cat "$tmpfile" > "$file"
rm "$tmpfile"
else
echo "There is no $1 .gitignore yet"
exit 1
fi
}


# can have only 1 action and 1 level, if conflicting flags are passed
# latter ones overwrite former
# sanity of args not tested (e.g. there should be no args if -e is supplied)
if test $# -eq 0; then
show_global
echo "---------------------------------"
show_local
echo "---------------------------------"
show_private
else
case "$1" in
-l|--local)
test $# -gt 1 && add_local "${@:2}" && echo
show_local
;;
-g|--global)
test $# -gt 1 && add_global "${@:2}" && echo
show_global
;;
-p|--private)
test $# -gt 1 && add_private "${@:2}" && echo
show_private
;;
*)
add_local "$@"
;;
esac
fi
# parse flags and args
level="none"
action="none"
declare -a args
while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
_usage
exit 0
;;
-l|--local)
level="local"
;;
-g|--global)
level="global"
;;
-p|--private)
level="private"
;;
-e|--edit)
action="edit"
;;
-r|--remove)
action="remove"
;;
--)
shift
args+=("$@")
break
;;
*)
args+=("$1")
;;
esac
shift
done
# compatible with previous version
if [ "$action" = "none" ]; then
case "$level" in
local)
test ${#args[@]} -ne 0 && add_local "${args[@]}" && echo
show_local
;;
global)
test ${#args[@]} -ne 0 && add_global "${args[@]}" && echo
show_global
;;
private)
test ${#args[@]} -ne 0 && add_private "${args[@]}" && echo
show_private
;;
none)
add_local "${args[@]}"
# maybe show_local here too?
;;
esac
exit 0
# open file in editor
elif [ "$action" = "edit" ]; then
case "$level" in
local|none)
cd_to_git_root --warn
edit_file 'local' .gitignore && echo
;;
global)
global_gitignore="$(global_ignore)"
edit_file global "$global_gitignore" && echo
;;
private)
cd_to_git_root --error
test -d "${GIT_DIR}/info" || mkdir -p "${GIT_DIR}/info"
edit_file private "${GIT_DIR}/info/exclude" && echo
;;
esac
exit 0
# remove entries
else
if [ ${#args[@]} -eq 0 ]; then
echo "Nothing to remove."
exit 0
fi
case "$level" in
local|none)
cd_to_git_root --warn
remove_patterns 'local' .gitignore "${args[@]}" && echo
;;
global)
global_gitignore="$(global_ignore)"
remove_patterns global "$global_gitignore" "${args[@]}" && echo
;;
private)
cd_to_git_root --error
remove_patterns private "${GIT_DIR}/info/exclude" "${args[@]}" && echo
;;
esac
exit 0

fi
fi
Loading