#!/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) $(printf '%q' "$file")" echo "Done." else echo "There is no $1 .gitignore yet." >&2 exit 1 fi } _usage() { cat << EOF Usage: git ignore [OPTION]... [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 delimiter 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