tj.git-extras/bin/git-ignore
2026-06-18 22:53:33 +03:00

286 lines
6.8 KiB
Bash
Executable file

#!/usr/bin/env bash
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
echo "$1 gitignore: $2" && cat "$file"
else
echo "There is no $1 .gitignore yet"
fi
}
cd_to_git_root() {
local error_level="$1"
if ! git rev-parse --git-dir &>/dev/null; then
if [ "$error_level" = '--warn' ]; then
echo "Warning: Not currently in a Git repository" >&2
elif [ "$error_level" = '--error' ]; then
echo "Error: Not currently in a Git repository" >&2
exit 1
fi
fi
local result=
if result=$(git rev-parse --show-toplevel 2>/dev/null); then
cd "$result" || exit
fi
}
global_ignore() {
if ! git config --global core.excludesFile 2>/dev/null; then
if [ -f "$HOME/.gitignore" ]; then
echo "$HOME/.gitignore"
else
echo "${XDG_CONFIG_HOME:-$HOME/.config}/git/ignore"
fi
fi
}
show_global() {
show_contents Global "$(global_ignore)"
}
add_global() {
local global_gitignore
global_gitignore="$(global_ignore)"
if [ -z "$global_gitignore" ]; then
echo "Can't find global .gitignore."
echo ""
echo "Use 'git config --global --add core.excludesfile ~/.gitignore-global' to set the path to your global gitignore file to '~/.gitignore-global'."
echo ""
else
add_patterns "$global_gitignore" "$@"
fi
}
show_local() {
cd_to_git_root --warn
show_contents Local .gitignore
}
add_local() {
cd_to_git_root --warn
add_patterns .gitignore "$@"
}
show_private() {
cd_to_git_root --error
show_contents Private "${GIT_DIR}/info/exclude"
}
add_private() {
cd_to_git_root --error
test -d "${GIT_DIR}/info" || mkdir -p "${GIT_DIR}/info"
add_patterns "${GIT_DIR}/info/exclude" "$@"
}
add_patterns() {
echo "Adding pattern(s) to: $1"
local file="${1/#~/$HOME}"
dir_name=$(dirname "$file")
if [ ! -d "$dir_name" ]; then
mkdir -p "$dir_name"
fi
if [ -s "$file" ]; then
# If the content of $file doesn't end with a newline, add one
test "$(tail -c 1 "$file")" != "" && echo "" >> "$file"
fi
for pattern in "${@:2}"; do
echo "... adding '$pattern'"
(test -f "$file" && test "$pattern" && grep -q -F -x -- "$pattern" "$file") || echo "$pattern" >> "$file"
done
}
edit_file() {
local file="${2/#~/$HOME}"
if [ -f "$file" ]; then
echo "Editing $1 gitignore: ($2)" && eval "$(git var GIT_EDITOR) $file"
echo "Done."
else
echo "There is no $1 .gitignore yet." >&2
exit 1
fi
}
_usage() {
cat << EOF
Usage: git ignore [OPTON]... [PATTERN]...
Modify or display local global or private gitignore files.
OPTIONs set context (local, global, private) and action (display, add,
remove, edit). In case of collision, last flag overwrites preceding ones.
Defaults are: local context, action is display (see man git ignore for
more details).
Context examples:
local: .../repo/.gitignore
private: .../repo/.git/info/exclude
global: \$HOME/.gitignore
PATTERNs are the gitignore patterns as specified in git documentation.
Options:
-l, --local Set context to local gitignore
-g, --global Set context to global gitignore
-p, --private Set context to private gitignore
-e, --edit Set action to edit
-r, --remove Set action to remove
-h, --help Print this message
-- End of options delimeter
EOF
}
remove_patterns() {
declare -a args
local tmpfile
args=( "${@:3}" )
local file="${2/#~/$HOME}"
if [ -f "$file" ]; then
tmpfile="$(mktemp -q "${TMPDIR:-/tmp}"/git-extras-ignore.XXXXXX 2>/dev/null)"
echo "Removing patterns from $1 gitignore: $2"
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
# 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
show_local
;;
global)
global_gitignore="$(global_ignore)"
remove_patterns global "$global_gitignore" "${args[@]}" && echo
show_global
;;
private)
cd_to_git_root --error
remove_patterns private "${GIT_DIR}/info/exclude" "${args[@]}" && echo
show_private
;;
esac
exit 0
fi
fi