diff --git a/.gitignore b/.gitignore index 4470d7cfc0ae72..fd0707fadfee0b 100644 --- a/.gitignore +++ b/.gitignore @@ -76,6 +76,7 @@ /git-http-backend /git-http-fetch /git-http-push +/git-ignore /git-imap-send /git-index-pack /git-init diff --git a/Documentation/git-ignore.txt b/Documentation/git-ignore.txt new file mode 100644 index 00000000000000..df1dfd7a8896c5 --- /dev/null +++ b/Documentation/git-ignore.txt @@ -0,0 +1,116 @@ +git-ignore(1) +============= + +NAME +---- +git-ignore - Easily add entries to your .gitignore file + + +SYNOPSIS +-------- +[verse] +'git ignore' [--dry-run | -n] [--ext | -e] [--all-ext | -E] [--dir | -d] + [--all-file | -a] [--parent-level | -p ] pathspec [...] +'git ignore' --edit [--parent-level | -p ] + + +DESCRIPTION +----------- +The command modifies a .gitignore file in your path easily. By default, +it adds lines to the .gitignore found in the root of your repository. It +can, however, add lines to a gitignore anywhere inbetween the file(s) +passed in and the root directory. The lines added can be based on +filename, extension, directory, or recursive glob. + +Also, you can easily open the gitignore file using the $EDITOR environment +variable. + + +OPTIONS +------- +...:: + Files to add to a gitignore. Fileglobs (e.g. `*.c`) can + be given to add all matching files. Also a + directory name can be given to add it to the gitignore + as well. + +--edit:: + Open the appropriate gitignore file in your default editor (using the + $EDITOR variable). This option can be combined with `--parent-level` based + on your current working directory. + +-n:: +--dry-run:: + Don't actually edit the gitignore(s), just show what changes + would have taken place. + +-e:: +--ext:: + Add the relative filepath based on extension. If pathspec + references path/to/file.log, the added gitignore line would + be path/to/*.log. + +-E:: +--all-ext:: + Add a global exclusion of the given extension. If pathspec + references path/to/file.log, the added gitignore line would + be **/*.log. + +-d:: +--dir:: + Add the contents of the parent directory. If pathspec references + path/to/file.log, the added gitignore line would be path/to/*. + +-a:: +--all-file:: + Add a global exclusion of the given filename. If pathspec references + path/to/file.log, the added gitignore line would be **/file.log. + +-p:: +--parent-level :: + Modifications will go to a gitignore located + directories above each of the files passed in. If the number + of parent levels causes the directory to fall outside of the + root of the git repository, a warning is printed and the root + of the repository is used instead. Using a parent-level of 0 + will use the gitignore in the directory of each file passed in. + ++ +Note that the parent level is calculated for each file passed in. If multiple +files are passed in that have different parents at a given parent level, then +they will cause separate gitignore files to be written. + + +EXAMPLES +-------- + +* Adds all `*.log` files under `tmp` directory +and its subdirectories to the gitignore file found at the +root of the repository: ++ +------------ +$ pwd +/user/test/git_repo/src +$ git ignore -e tmp/file.log +------------ ++ +Results in `tmp/*.log` added to /user/test/git_repo/.gitignore + + +* Add the files under the `var/uploaded` directory to the gitignore above + ++ +------------ +$ pwd +/user/test/git_repo/ +$ git ignore -d --parent-level=1 www/var/uploaded/\* +------------ +Results in `var/uploaded/*` added to /user/test/git_repo/www/.gitignore + +SEE ALSO +-------- +linkgit:gitignore[5] + +GIT +--- +Part of the linkgit:git[1] suite diff --git a/Makefile b/Makefile index f58bf14c7bf3d9..77ad5bd8e249f3 100644 --- a/Makefile +++ b/Makefile @@ -606,6 +606,7 @@ TEST_PROGRAMS_NEED_X = unexport CDPATH SCRIPT_SH += git-bisect.sh +SCRIPT_SH += git-ignore.sh SCRIPT_SH += git-difftool--helper.sh SCRIPT_SH += git-filter-branch.sh SCRIPT_SH += git-merge-octopus.sh diff --git a/git-ignore.sh b/git-ignore.sh new file mode 100755 index 00000000000000..7cdc76caaf4013 --- /dev/null +++ b/git-ignore.sh @@ -0,0 +1,222 @@ +#!/bin/sh +# +# Copyright (c) 2016, Thurston Stone +# +# unit test: t7900 + +test -z "$DDDEBUG" || set -x + +_verbose=0 + +SUBDIRECTORY_OK=Yes +OPTIONS_KEEPDASHDASH= +OPTIONS_STUCKLONG=t +# Would be nice to have examples, but rev-parse sees '*' as a symbol to hide everything afterwards +#e,ext add relative path for any file of that type (ex. path/to/*.ext) +#E,all-ext all files of that extention anywhere (ex. **/*.ext) +#d,dir all files under the parent directory (ex. directory/*) +#a,all-file all files of that file name (ex. **/filename.ext) +OPTIONS_SPEC="git ignore [options] [file|glob ...] +-- + Miscelleneous +edit open the pertinent gitignore with your default text editor (Requires \$EDITOR to be set) +v,verbose show verbose output +n,dry-run do not actually edit any .gitignore files + Determine what files to add to the gitignore(s): +e,ext add relative path for any file of that type +E,all-ext all files of that extention anywhere +d,dir all files under the parent directory +a,all-file all files of that file name + Determine what gitignore(s) to use: +p,parent-level= number of parent directories containing the gitignore to edit. Set to 0 to put it in the local directory" + +. git-sh-setup +. git-sh-i18n + +write_output () { + if test $_verbose -eq 1 + then + say $1 + fi +} + +get_git_ignore () { + directory=$1 + + # if we don't yet have the repo root directory, get it + if test -z "$repo_root" + then + #First, determine the root of the repository + repo_root="$(git rev-parse --show-toplevel)/" + write_output "repo_root=$repo_root" + fi + + # get the path relative to the repo root + rel_directory="${directory#$repo_root}" + # if the relative path is the same as it was, try converting it to aa *nix + # style path + if test "$rel_directory" = "$directory" + then + # repo root 2 (cygwin-ified path) didn't work + # try the other one + write_output "changing repo_root from $repo_root" + #On windows, this turns to C:\... instead of /c/... from some other commands + repo_root=$(printf "$repo_root" | awk -F":" '{ if ($2) print "/" tolower($1) $2; else print $1 }') + write_output " to $repo_root" + rel_directory="${directory#$repo_root}" + fi + # default gitignore + gitignore="${repo_root}.gitignore" + + # ------------------------------------------------ + # Determine the correct git ignore and the path of + # the file relative to it + # ------------------------------------------------ + if test $_parent_level -ge 0 + then + parent=${directory} + write_output "parent=${parent}" + + if test $_parent_level -ne 0 + then + i=1 + while test "$i" -le $_parent_level + do + parent="$(dirname "$parent")/" + write_output "parent=${parent}" + i=$(($i + 1)) + done + fi + root_len=$(printf "${repo_root}" | wc -m) + parent_len=$(printf "${parent}" | wc -m) + if test $root_len -ge $parent_len + then + write_output "root_len(${root_len}) >= parent_len(${parent_len})... + uh-oh" + gettextln "WARNING: Parent directory is outside of the repository" + parent="${repo_root}" + else + write_output "root_len(${root_len}) < parent_len(${parent_len})... + good" + fi + rel_directory="${directory#$parent}" + gitignore="${parent}.gitignore" + fi + + write_output "rel_directory=${rel_directory}" + write_output "gitignore=${gitignore}" + +} + +add_ignore () { + # get the absolute path of the file + file="$(cd "$(dirname "$1")"; pwd)/$(basename "$1")" + write_output "file=$file" + + directory="$(dirname "$file")/" + write_output "directory=$directory" + get_git_ignore "$directory" + + filename=$(basename "$file") + write_output "filename=$filename" + extension="${filename##*.}" + write_output "extension=$extension" + # defaault line + line="${rel_directory}${filename}" + + # ------------------------------------------------ + # Determine the correct line to add to the gitignore + # based on user inputs + # ------------------------------------------------ + if test $_ext -eq 1 + then + line="${rel_directory}*.$extension" + fi + if test $_directory -eq 1 + then + line="${rel_directory}*" + fi + if test $_file_anywhere -eq 1 + then + line="**/$filename" + fi + if test $_ext_anywhere -eq 1 + then + line="**/*.$extension" + fi + write_output "line=${line}" + dryrun="" + if test $_dry_run -eq 1 + then + dryrun="$(gettext "DRY-RUN!")" + fi + say "$dryrun $(eval_gettext "Adding \$line to \$gitignore")" + if test $_dry_run -eq 0 + then + echo "$line" >>"$gitignore" + fi +} + +_ext=0 +_directory=0 +_file_anywhere=0 +_ext_anywhere=0 +_parent_level=-1 +_edit=0 +_dry_run=0 + +while test $# != 0 +do + case "$1" in + --ext) + _ext=1 + ;; + --all-ext) + _ext_anywhere=1 + ;; + --dir) + _directory=1 + ;; + --all-file) + _file_anywhere=1 + ;; + --parent-level=*) + _parent_level="${1#--parent-level=}" + if ! echo $_parent_level | grep -q '^[0-9]\+$' + then + gettextln "ILLEGAL PARAMETER: -p|--parent-level requires a numerical argument" + usage + fi + ;; + --dry-run) + _dry_run=1 + ;; + --edit) + if test -z $EDITOR + then + gettextln "ERROR: Shell variable \$EDITOR must be set" + usage + fi + _edit=1 + ;; + --verbose) + _verbose=1 + ;; + --) + only_files_left=1 + ;; + *) + if test $only_files_left -eq 1 + then + add_ignore "$1" + fi + ;; + esac + shift +done +if test $_edit -eq 1 +then + get_git_ignore "$(pwd)/" + git_editor "$gitignore" +fi +exit 0 diff --git a/t/t7070-ignore.sh b/t/t7070-ignore.sh new file mode 100755 index 00000000000000..4966f62ac226b3 --- /dev/null +++ b/t/t7070-ignore.sh @@ -0,0 +1,180 @@ +#!/bin/sh +# +# Copyright (c) 2016 Thurston Stone +# + + +test_description='check-git-ignore-cmd' + +. ./test-lib.sh + +test_expect_success 'setup' ' + echo a >a && + git add a && + git commit -m"adding initial files" +' + + +test_expect_success 'ignore at root' ' + echo a >ignoreme.txt && + DDDEBUG=1 git ignore ignoreme.txt && + echo "ignoreme.txt" >expect && + cat .gitignore >actual && + test_cmp expect actual +' + +test_expect_success 'ignore in subdir' ' + rm .gitignore && + mkdir -p "sub/dir with space" && + echo a >"sub/dir with space/ignoreme.txt" && + ( + cd "sub/dir with space" && + DDDEBUG=1 git ignore -v ignoreme.txt + ) && + echo "sub/dir with space/ignoreme.txt" >expect && + cat .gitignore >actual && + test_cmp expect actual +' + +test_expect_success 'ignore extentions at root' ' + rm .gitignore && + echo a >ignoreme.txt && + DDDEBUG=1 git ignore -v -e ignoreme.txt && + echo "*.txt" >expect && + cat .gitignore >actual && + test_cmp expect actual +' + +test_expect_success 'ignore extentions in subdir' ' + rm .gitignore && + mkdir -p "sub/dir with space" && + echo a >"sub/dir with space/ignoreme.txt" && + ( + cd "sub/dir with space" && + DDDEBUG=1 git ignore -v -e ignoreme.txt + ) && + echo "sub/dir with space/*.txt" >expect && + cat .gitignore >actual && + test_cmp expect actual +' + +test_expect_success 'ignore extentions anywhere' ' + rm .gitignore && + mkdir -p "sub/dir with space" && + echo a >"sub/dir with space/ignoreme.txt" && + ( + cd "sub/dir with space" && + DDDEBUG=1 git ignore -v -E ignoreme.txt + ) && + echo "**/*.txt" >expect && + cat .gitignore >actual && + test_cmp expect actual +' + +test_expect_success 'ignore directory' ' + rm .gitignore && + mkdir -p "sub/dir with space" && + echo a >"sub/dir with space/ignoreme.txt" && + ( + cd "sub/dir with space" && + DDDEBUG=1 git ignore -v -d ignoreme.txt + ) && + echo "sub/dir with space/*" >expect && + cat .gitignore >actual && + test_cmp expect actual +' + +test_expect_success 'ignore filename anywhere' ' + rm .gitignore && + mkdir -p "sub/dir with space" && + echo a >"sub/dir with space/ignoreme.txt" && + ( + cd "sub/dir with space" && + DDDEBUG=1 git ignore -v -a ignoreme.txt + ) && + echo "**/ignoreme.txt" >expect && + cat .gitignore >actual && + test_cmp expect actual +' + +test_expect_success 'dry run does not write anything' ' + rm .gitignore && + echo a >ignoreme.txt && + DDDEBUG=1 git ignore -v -n ignoreme.txt >output && + grep "^DRY-RUN!" "sub/dir with space/ignoreme.txt" && + ( + cd "sub/dir with space" && + DDDEBUG=1 git ignore -v -p 0 ignoreme.txt + ) && + echo "ignoreme.txt" >expect && + cat "sub/dir with space/.gitignore" >actual && + test_cmp expect actual +' + +test_expect_success 'parent-level set to dir outside of repo top-level' ' + mkdir -p "sub/dir with space" && + echo a >"sub/dir with space/ignoreme.txt" && + ( + cd "sub/dir with space" && + DDDEBUG=1 git ignore -v -p 2 ignoreme.txt >output + ) && + grep "^WARNING" <"sub/dir with space/output" && + echo "sub/dir with space/ignoreme.txt" >expect && + cat .gitignore >actual && + test_cmp expect actual +' + +test_expect_success 'parent-level set to mutliple gitignores' ' + mkdir -p "sub/dir1 with space/test" && + echo a >"sub/dir1 with space/test/ignoreme.txt" && + mkdir -p "sub/dir2 with space/test" && + echo a >"sub/dir2 with space/test/ignoreme.txt" && + DDDEBUG=1 git ignore -v -p 1 "sub/dir1 with space/test/ignoreme.txt" "sub/dir2 with space/test/ignoreme.txt" && + echo "test/ignoreme.txt" >expect && + cat "sub/dir1 with space/.gitignore" >actual && + test_cmp expect actual && + cat "sub/dir2 with space/.gitignore" >actual && + test_cmp expect actual +' + +setup_fake_editor () { + write_script fake-editor <<-\EOF +set -x +file=$1 +printf "edited the file like a boss">"$1" +EOF +} + +test_set_editor "$(pwd)/fake-editor" + +test_expect_success 'edit root gitignore' ' + setup_fake_editor && + mkdir -p "sub/dir with space" && + ( + cd "sub/dir with space" && + DDDEBUG=1 git ignore -v --edit + ) && + printf "edited the file like a boss" >expect && + cat .gitignore >actual && + test_cmp expect actual +' + +test_expect_success 'edit root gitignore using --parent-level' ' + setup_fake_editor && + mkdir -p "sub/dir with space/test" && + ( + cd "sub/dir with space/test" && + DDDEBUG=1 git ignore -p 2 --edit + ) && + printf "edited the file like a boss" >expect && + cat sub/.gitignore >actual && + test_cmp expect actual +' + +test_done