Skip to content
Merged
Changes from 3 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
176 changes: 158 additions & 18 deletions bin/git-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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


show_contents() {
local file="${2/#~/$HOME}"
if [ -f "$file" ]; then
Expand Down Expand Up @@ -29,6 +30,17 @@ cd_to_git_root() {
fi
}

get_tmp_file() {
local tmpfile=
if ! git rev-parse --git-dir &>/dev/null || [ -z "$GIT_DIR" ]; then
tmpfile="$(mktemp -q "${TMPDIR:-/tmp}"/git-extras-ignore.XXXXXX 2>/dev/null)"
else
cd_to_git_root --warn &>/dev/null
tmpfile="$(mktemp -q "$GIT_DIR"/git-extras-ignore.XXXXXX 2>/dev/null)"

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.

Why don't we always put the temp file in $TMPDIR?

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.

Wanted it to work the same way git does and it writes its tmp files in .git. Changed to always use global tmp directory.

fi
echo "${tmpfile:-$(mktemp -q)}"
}

global_ignore() {
if ! git config --global core.excludesFile 2>/dev/null; then
if [ -f "$HOME/.gitignore" ]; then
Expand Down Expand Up @@ -94,29 +106,157 @@ 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)..." && $(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.

$(git var GIT_EDITOR) "$file" loses shell quoting for editor values like vim -c 'set ft=gitignore', and echo "Done." still runs even if the editor exits non-zero.

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.

Changed the line to use eval - it seems to fix this issue.

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="$(get_tmp_file)"
local file="${2/#~/$HOME}"
if [ -f "$file" ]; then
echo "Removing patterns from $1 gitignore ($2)..."
while read -r line; do

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.

It can corrupt preserved lines during remove. read without IFS= trims leading/trailing whitespace, echo "$line" mishandles values like -n, and the loop skips a final line without a trailing newline. Use while IFS= read -r line || [ -n "$line" ]; do and printf '%s\n' "$line".

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.

While thinking about these issues found that perl has \E \Q escapes. These could work too (something like perl -pe "s/\Q${pattern1}\E\n?//; s/\Q${pattern2}\E\n?//; ..."). Which of the approaches do you think is better?

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.

I wonder if we have to add perl in https://github.com/tj/git-extras/blob/main/Installation.md#dependencies. Could it be solved with awk or sed?

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.

I think I found a way to do that in awk and also keep echos saying what was dropped. Added it in 3e5076b.

local drop=
for pattern in "${args[@]}"; do
test "$pattern" = "$line" && drop="yes"
done
if [ -z "$drop" ]; then
echo "$line" >> "$tmpfile"
else
echo "Removed $line pattern."
fi
done < "$file"
mv "$tmpfile" "$file" # tmp file created in get_tmp_file 'deleted'

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.

If .gitignore or the global ignore file is a symlink, mv "$tmpfile" "$file" replaces the symlink instead of updating its target; it can also lose file metadata. Can we update it in place?

echo "Done."
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 [ "$1" != "" ]; do

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.

It breaks documented empty-string patterns. while [ "$1" != "" ] stops parsing as soon as a pattern is "", so existing examples like git ignore --local "" "# Temporary files" no longer add anything after the empty argument. Use while [ $# -gt 0 ] instead.

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
;;
global)
global_gitignore="$(global_ignore)"
edit_file global "$global_gitignore"
;;
private)
cd_to_git_root --error
test -d "${GIT_DIR}/info" || mkdir -p "${GIT_DIR}/info"
edit_file private "${GIT_DIR}/info/exclude"
;;
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[@]}"
;;
global)
global_gitignore="$(global_ignore)"
remove_patterns global "$global_gitignore" "${args[@]}"
;;
private)
cd_to_git_root --error
remove_patterns private "${GIT_DIR}/info/exclude" "${args[@]}"
;;
esac
exit 0

fi
fi
Loading