Merge branch 'jk/submodule-fix-loose' into maint-2.13

* jk/submodule-fix-loose:
  verify_path: disallow symlinks in .gitmodules
  update-index: stat updated files earlier
  verify_dotfile: mention case-insensitivity in comment
  verify_path: drop clever fallthrough
  skip_prefix: add case-insensitive variant
  is_{hfs,ntfs}_dotgitmodules: add tests
  is_ntfs_dotgit: match other .git files
  is_hfs_dotgit: match other .git files
  is_ntfs_dotgit: use a size_t for traversing string
  submodule-config: verify submodule names as paths
diff --git a/apply.c b/apply.c
index f8bf0bd..b96d375 100644
--- a/apply.c
+++ b/apply.c
@@ -3867,9 +3867,9 @@
 	if (!patch->is_delete)
 		new_name = patch->new_name;
 
-	if (old_name && !verify_path(old_name))
+	if (old_name && !verify_path(old_name, patch->old_mode))
 		return error(_("invalid path '%s'"), old_name);
-	if (new_name && !verify_path(new_name))
+	if (new_name && !verify_path(new_name, patch->new_mode))
 		return error(_("invalid path '%s'"), new_name);
 	return 0;
 }
diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c
index cbb17a9..b4b4d29 100644
--- a/builtin/submodule--helper.c
+++ b/builtin/submodule--helper.c
@@ -1195,6 +1195,29 @@
 	return !is_submodule_initialized(argv[1]);
 }
 
+/*
+ * Exit non-zero if any of the submodule names given on the command line is
+ * invalid. If no names are given, filter stdin to print only valid names
+ * (which is primarily intended for testing).
+ */
+static int check_name(int argc, const char **argv, const char *prefix)
+{
+	if (argc > 1) {
+		while (*++argv) {
+			if (check_submodule_name(*argv) < 0)
+				return 1;
+		}
+	} else {
+		struct strbuf buf = STRBUF_INIT;
+		while (strbuf_getline(&buf, stdin) != EOF) {
+			if (!check_submodule_name(buf.buf))
+				printf("%s\n", buf.buf);
+		}
+		strbuf_release(&buf);
+	}
+	return 0;
+}
+
 #define SUPPORT_SUPER_PREFIX (1<<0)
 
 struct cmd_struct {
@@ -1216,6 +1239,7 @@
 	{"push-check", push_check, 0},
 	{"absorb-git-dirs", absorb_git_dirs, SUPPORT_SUPER_PREFIX},
 	{"is-active", is_active, 0},
+	{"check-name", check_name, 0},
 };
 
 int cmd_submodule__helper(int argc, const char **argv, const char *prefix)
diff --git a/builtin/update-index.c b/builtin/update-index.c
index ebfc09f..1921659 100644
--- a/builtin/update-index.c
+++ b/builtin/update-index.c
@@ -358,10 +358,9 @@
 	return error("%s: is a directory - add files inside instead", path);
 }
 
-static int process_path(const char *path)
+static int process_path(const char *path, struct stat *st, int stat_errno)
 {
 	int pos, len;
-	struct stat st;
 	const struct cache_entry *ce;
 
 	len = strlen(path);
@@ -385,13 +384,13 @@
 	 * First things first: get the stat information, to decide
 	 * what to do about the pathname!
 	 */
-	if (lstat(path, &st) < 0)
-		return process_lstat_error(path, errno);
+	if (stat_errno)
+		return process_lstat_error(path, stat_errno);
 
-	if (S_ISDIR(st.st_mode))
-		return process_directory(path, len, &st);
+	if (S_ISDIR(st->st_mode))
+		return process_directory(path, len, st);
 
-	return add_one_path(ce, path, len, &st);
+	return add_one_path(ce, path, len, st);
 }
 
 static int add_cacheinfo(unsigned int mode, const struct object_id *oid,
@@ -400,7 +399,7 @@
 	int size, len, option;
 	struct cache_entry *ce;
 
-	if (!verify_path(path))
+	if (!verify_path(path, mode))
 		return error("Invalid path '%s'", path);
 
 	len = strlen(path);
@@ -443,7 +442,17 @@
 
 static void update_one(const char *path)
 {
-	if (!verify_path(path)) {
+	int stat_errno = 0;
+	struct stat st;
+
+	if (mark_valid_only || mark_skip_worktree_only || force_remove)
+		st.st_mode = 0;
+	else if (lstat(path, &st) < 0) {
+		st.st_mode = 0;
+		stat_errno = errno;
+	} /* else stat is valid */
+
+	if (!verify_path(path, st.st_mode)) {
 		fprintf(stderr, "Ignoring path %s\n", path);
 		return;
 	}
@@ -464,7 +473,7 @@
 		report("remove '%s'", path);
 		return;
 	}
-	if (process_path(path))
+	if (process_path(path, &st, stat_errno))
 		die("Unable to process path %s", path);
 	report("add '%s'", path);
 }
@@ -534,7 +543,7 @@
 			path_name = uq.buf;
 		}
 
-		if (!verify_path(path_name)) {
+		if (!verify_path(path_name, mode)) {
 			fprintf(stderr, "Ignoring path %s\n", path_name);
 			continue;
 		}
diff --git a/cache.h b/cache.h
index c1041cc..5a44f79 100644
--- a/cache.h
+++ b/cache.h
@@ -598,7 +598,7 @@
 extern int write_locked_index(struct index_state *, struct lock_file *lock, unsigned flags);
 extern int discard_index(struct index_state *);
 extern int unmerged_index(const struct index_state *);
-extern int verify_path(const char *path);
+extern int verify_path(const char *path, unsigned mode);
 extern int strcmp_offset(const char *s1, const char *s2, size_t *first_change);
 extern int index_dir_exists(struct index_state *istate, const char *name, int namelen);
 extern void adjust_dirname_case(struct index_state *istate, char *name);
@@ -1188,7 +1188,15 @@
 int longest_ancestor_length(const char *path, struct string_list *prefixes);
 char *strip_path_suffix(const char *path, const char *suffix);
 int daemon_avoid_alias(const char *path);
-extern int is_ntfs_dotgit(const char *name);
+
+/*
+ * These functions match their is_hfs_dotgit() counterparts; see utf8.h for
+ * details.
+ */
+int is_ntfs_dotgit(const char *name);
+int is_ntfs_dotgitmodules(const char *name);
+int is_ntfs_dotgitignore(const char *name);
+int is_ntfs_dotgitattributes(const char *name);
 
 /*
  * Returns true iff "str" could be confused as a command-line option when
diff --git a/git-compat-util.h b/git-compat-util.h
index 59866d7..4be15e5 100644
--- a/git-compat-util.h
+++ b/git-compat-util.h
@@ -956,6 +956,23 @@
 		return (x & 0x20) == 0;
 }
 
+/*
+ * Like skip_prefix, but compare case-insensitively. Note that the comparison
+ * is done via tolower(), so it is strictly ASCII (no multi-byte characters or
+ * locale-specific conversions).
+ */
+static inline int skip_iprefix(const char *str, const char *prefix,
+			       const char **out)
+{
+	do {
+		if (!*prefix) {
+			*out = str;
+			return 1;
+		}
+	} while (tolower(*str++) == tolower(*prefix++));
+	return 0;
+}
+
 static inline int strtoul_ui(char const *s, int base, unsigned int *result)
 {
 	unsigned long ul;
diff --git a/git-submodule.sh b/git-submodule.sh
index c0d0e9a..92750b9 100755
--- a/git-submodule.sh
+++ b/git-submodule.sh
@@ -228,6 +228,11 @@
 		sm_name="$sm_path"
 	fi
 
+	if ! git submodule--helper check-name "$sm_name"
+	then
+		die "$(eval_gettext "'$sm_name' is not a valid submodule name")"
+	fi
+
 	# perhaps the path exists and is already a git repo, else clone it
 	if test -e "$sm_path"
 	then
diff --git a/path.c b/path.c
index 0349a0a..c720625 100644
--- a/path.c
+++ b/path.c
@@ -1224,7 +1224,7 @@
 
 int is_ntfs_dotgit(const char *name)
 {
-	int len;
+	size_t len;
 
 	for (len = 0; ; len++)
 		if (!name[len] || name[len] == '\\' || is_dir_sep(name[len])) {
@@ -1241,6 +1241,90 @@
 		}
 }
 
+static int is_ntfs_dot_generic(const char *name,
+			       const char *dotgit_name,
+			       size_t len,
+			       const char *dotgit_ntfs_shortname_prefix)
+{
+	int saw_tilde;
+	size_t i;
+
+	if ((name[0] == '.' && !strncasecmp(name + 1, dotgit_name, len))) {
+		i = len + 1;
+only_spaces_and_periods:
+		for (;;) {
+			char c = name[i++];
+			if (!c)
+				return 1;
+			if (c != ' ' && c != '.')
+				return 0;
+		}
+	}
+
+	/*
+	 * Is it a regular NTFS short name, i.e. shortened to 6 characters,
+	 * followed by ~1, ... ~4?
+	 */
+	if (!strncasecmp(name, dotgit_name, 6) && name[6] == '~' &&
+	    name[7] >= '1' && name[7] <= '4') {
+		i = 8;
+		goto only_spaces_and_periods;
+	}
+
+	/*
+	 * Is it a fall-back NTFS short name (for details, see
+	 * https://en.wikipedia.org/wiki/8.3_filename?
+	 */
+	for (i = 0, saw_tilde = 0; i < 8; i++)
+		if (name[i] == '\0')
+			return 0;
+		else if (saw_tilde) {
+			if (name[i] < '0' || name[i] > '9')
+				return 0;
+		} else if (name[i] == '~') {
+			if (name[++i] < '1' || name[i] > '9')
+				return 0;
+			saw_tilde = 1;
+		} else if (i >= 6)
+			return 0;
+		else if (name[i] < 0) {
+			/*
+			 * We know our needles contain only ASCII, so we clamp
+			 * here to make the results of tolower() sane.
+			 */
+			return 0;
+		} else if (tolower(name[i]) != dotgit_ntfs_shortname_prefix[i])
+			return 0;
+
+	goto only_spaces_and_periods;
+}
+
+/*
+ * Inline helper to make sure compiler resolves strlen() on literals at
+ * compile time.
+ */
+static inline int is_ntfs_dot_str(const char *name, const char *dotgit_name,
+				  const char *dotgit_ntfs_shortname_prefix)
+{
+	return is_ntfs_dot_generic(name, dotgit_name, strlen(dotgit_name),
+				   dotgit_ntfs_shortname_prefix);
+}
+
+int is_ntfs_dotgitmodules(const char *name)
+{
+	return is_ntfs_dot_str(name, "gitmodules", "gi7eba");
+}
+
+int is_ntfs_dotgitignore(const char *name)
+{
+	return is_ntfs_dot_str(name, "gitignore", "gi250a");
+}
+
+int is_ntfs_dotgitattributes(const char *name)
+{
+	return is_ntfs_dot_str(name, "gitattributes", "gi7d29");
+}
+
 int looks_like_command_line_option(const char *str)
 {
 	return str && str[0] == '-';
diff --git a/read-cache.c b/read-cache.c
index 6238df4..aa99f1f 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -732,7 +732,7 @@
 	int size, len;
 	struct cache_entry *ce, *ret;
 
-	if (!verify_path(path)) {
+	if (!verify_path(path, mode)) {
 		error("Invalid path '%s'", path);
 		return NULL;
 	}
@@ -796,7 +796,7 @@
  * Also, we don't want double slashes or slashes at the
  * end that can make pathnames ambiguous.
  */
-static int verify_dotfile(const char *rest)
+static int verify_dotfile(const char *rest, unsigned mode)
 {
 	/*
 	 * The first character was '.', but that
@@ -810,8 +810,13 @@
 
 	switch (*rest) {
 	/*
-	 * ".git" followed by  NUL or slash is bad. This
-	 * shares the path end test with the ".." case.
+	 * ".git" followed by NUL or slash is bad. Note that we match
+	 * case-insensitively here, even if ignore_case is not set.
+	 * This outlaws ".GIT" everywhere out of an abundance of caution,
+	 * since there's really no good reason to allow it.
+	 *
+	 * Once we've seen ".git", we can also find ".gitmodules", etc (also
+	 * case-insensitively).
 	 */
 	case 'g':
 	case 'G':
@@ -819,8 +824,15 @@
 			break;
 		if (rest[2] != 't' && rest[2] != 'T')
 			break;
-		rest += 2;
-	/* fallthrough */
+		if (rest[3] == '\0' || is_dir_sep(rest[3]))
+			return 0;
+		if (S_ISLNK(mode)) {
+			rest += 3;
+			if (skip_iprefix(rest, "modules", &rest) &&
+			    (*rest == '\0' || is_dir_sep(*rest)))
+				return 0;
+		}
+		break;
 	case '.':
 		if (rest[1] == '\0' || is_dir_sep(rest[1]))
 			return 0;
@@ -828,7 +840,7 @@
 	return 1;
 }
 
-int verify_path(const char *path)
+int verify_path(const char *path, unsigned mode)
 {
 	char c;
 
@@ -841,12 +853,25 @@
 			return 1;
 		if (is_dir_sep(c)) {
 inside:
-			if (protect_hfs && is_hfs_dotgit(path))
-				return 0;
-			if (protect_ntfs && is_ntfs_dotgit(path))
-				return 0;
+			if (protect_hfs) {
+				if (is_hfs_dotgit(path))
+					return 0;
+				if (S_ISLNK(mode)) {
+					if (is_hfs_dotgitmodules(path))
+						return 0;
+				}
+			}
+			if (protect_ntfs) {
+				if (is_ntfs_dotgit(path))
+					return 0;
+				if (S_ISLNK(mode)) {
+					if (is_ntfs_dotgitmodules(path))
+						return 0;
+				}
+			}
+
 			c = *path++;
-			if ((c == '.' && !verify_dotfile(path)) ||
+			if ((c == '.' && !verify_dotfile(path, mode)) ||
 			    is_dir_sep(c) || c == '\0')
 				return 0;
 		}
@@ -1163,7 +1188,7 @@
 
 	if (!ok_to_add)
 		return -1;
-	if (!verify_path(ce->name))
+	if (!verify_path(ce->name, ce->ce_mode))
 		return error("Invalid path '%s'", ce->name);
 
 	if (!skip_df_check &&
diff --git a/submodule-config.c b/submodule-config.c
index 4f58491..de54351 100644
--- a/submodule-config.c
+++ b/submodule-config.c
@@ -163,6 +163,31 @@
 	return NULL;
 }
 
+int check_submodule_name(const char *name)
+{
+	/* Disallow empty names */
+	if (!*name)
+		return -1;
+
+	/*
+	 * Look for '..' as a path component. Check both '/' and '\\' as
+	 * separators rather than is_dir_sep(), because we want the name rules
+	 * to be consistent across platforms.
+	 */
+	goto in_component; /* always start inside component */
+	while (*name) {
+		char c = *name++;
+		if (c == '/' || c == '\\') {
+in_component:
+			if (name[0] == '.' && name[1] == '.' &&
+			    (!name[2] || name[2] == '/' || name[2] == '\\'))
+				return -1;
+		}
+	}
+
+	return 0;
+}
+
 static int name_and_item_from_var(const char *var, struct strbuf *name,
 				  struct strbuf *item)
 {
@@ -174,6 +199,12 @@
 		return 0;
 
 	strbuf_add(name, subsection, subsection_len);
+	if (check_submodule_name(name->buf) < 0) {
+		warning(_("ignoring suspicious submodule name: %s"), name->buf);
+		strbuf_release(name);
+		return 0;
+	}
+
 	strbuf_addstr(item, key);
 
 	return 1;
diff --git a/submodule-config.h b/submodule-config.h
index d434ecd..103cc79 100644
--- a/submodule-config.h
+++ b/submodule-config.h
@@ -35,4 +35,11 @@
 				      struct strbuf *rev);
 extern void submodule_free(void);
 
+/*
+ * Returns 0 if the name is syntactically acceptable as a submodule "name"
+ * (e.g., that may be found in the subsection of a .gitmodules file) and -1
+ * otherwise.
+ */
+int check_submodule_name(const char *name);
+
 #endif /* SUBMODULE_CONFIG_H */
diff --git a/t/helper/test-path-utils.c b/t/helper/test-path-utils.c
index 1ebe0f7..77517a4 100644
--- a/t/helper/test-path-utils.c
+++ b/t/helper/test-path-utils.c
@@ -1,5 +1,6 @@
 #include "cache.h"
 #include "string-list.h"
+#include "utf8.h"
 
 /*
  * A "string_list_each_func_t" function that normalizes an entry from
@@ -156,6 +157,11 @@
 	{ NULL,              NULL     }
 };
 
+static int is_dotgitmodules(const char *path)
+{
+	return is_hfs_dotgitmodules(path) || is_ntfs_dotgitmodules(path);
+}
+
 int cmd_main(int argc, const char **argv)
 {
 	if (argc == 3 && !strcmp(argv[1], "normalize_path_copy")) {
@@ -256,6 +262,20 @@
 	if (argc == 2 && !strcmp(argv[1], "dirname"))
 		return test_function(dirname_data, dirname, argv[1]);
 
+	if (argc > 2 && !strcmp(argv[1], "is_dotgitmodules")) {
+		int res = 0, expect = 1, i;
+		for (i = 2; i < argc; i++)
+			if (!strcmp("--not", argv[i]))
+				expect = !expect;
+			else if (expect != is_dotgitmodules(argv[i]))
+				res = error("'%s' is %s.gitmodules", argv[i],
+					    expect ? "not " : "");
+			else
+				fprintf(stderr, "ok: '%s' is %s.gitmodules\n",
+					argv[i], expect ? "" : "not ");
+		return !!res;
+	}
+
 	fprintf(stderr, "%s: unknown function name: %s\n", argv[0],
 		argv[1] ? argv[1] : "(there was none)");
 	return 1;
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index 7ea2bb5..3f3357e 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh
@@ -349,4 +349,90 @@
 test_submodule_relative_url "(null)" "user@host:path/to/repo" "../subrepo" "user@host:path/to/subrepo"
 test_submodule_relative_url "(null)" "user@host:repo" "../subrepo" "user@host:subrepo"
 
+test_expect_success 'match .gitmodules' '
+	test-path-utils is_dotgitmodules \
+		.gitmodules \
+		\
+		.git${u200c}modules \
+		\
+		.Gitmodules \
+		.gitmoduleS \
+		\
+		".gitmodules " \
+		".gitmodules." \
+		".gitmodules  " \
+		".gitmodules. " \
+		".gitmodules ." \
+		".gitmodules.." \
+		".gitmodules   " \
+		".gitmodules.  " \
+		".gitmodules . " \
+		".gitmodules  ." \
+		\
+		".Gitmodules " \
+		".Gitmodules." \
+		".Gitmodules  " \
+		".Gitmodules. " \
+		".Gitmodules ." \
+		".Gitmodules.." \
+		".Gitmodules   " \
+		".Gitmodules.  " \
+		".Gitmodules . " \
+		".Gitmodules  ." \
+		\
+		GITMOD~1 \
+		gitmod~1 \
+		GITMOD~2 \
+		gitmod~3 \
+		GITMOD~4 \
+		\
+		"GITMOD~1 " \
+		"gitmod~2." \
+		"GITMOD~3  " \
+		"gitmod~4. " \
+		"GITMOD~1 ." \
+		"gitmod~2   " \
+		"GITMOD~3.  " \
+		"gitmod~4 . " \
+		\
+		GI7EBA~1 \
+		gi7eba~9 \
+		\
+		GI7EB~10 \
+		GI7EB~11 \
+		GI7EB~99 \
+		GI7EB~10 \
+		GI7E~100 \
+		GI7E~101 \
+		GI7E~999 \
+		~1000000 \
+		~9999999 \
+		\
+		--not \
+		".gitmodules x"  \
+		".gitmodules .x" \
+		\
+		" .gitmodules" \
+		\
+		..gitmodules \
+		\
+		gitmodules \
+		\
+		.gitmodule \
+		\
+		".gitmodules x " \
+		".gitmodules .x" \
+		\
+		GI7EBA~ \
+		GI7EBA~0 \
+		GI7EBA~~1 \
+		GI7EBA~X \
+		Gx7EBA~1 \
+		GI7EBX~1 \
+		\
+		GI7EB~1 \
+		GI7EB~01 \
+		GI7EB~1X
+'
+
 test_done
diff --git a/t/t7415-submodule-names.sh b/t/t7415-submodule-names.sh
new file mode 100755
index 0000000..75fa071
--- /dev/null
+++ b/t/t7415-submodule-names.sh
@@ -0,0 +1,76 @@
+#!/bin/sh
+
+test_description='check handling of .. in submodule names
+
+Exercise the name-checking function on a variety of names, and then give a
+real-world setup that confirms we catch this in practice.
+'
+. ./test-lib.sh
+
+test_expect_success 'check names' '
+	cat >expect <<-\EOF &&
+	valid
+	valid/with/paths
+	EOF
+
+	git submodule--helper check-name >actual <<-\EOF &&
+	valid
+	valid/with/paths
+
+	../foo
+	/../foo
+	..\foo
+	\..\foo
+	foo/..
+	foo/../
+	foo\..
+	foo\..\
+	foo/../bar
+	EOF
+
+	test_cmp expect actual
+'
+
+test_expect_success 'create innocent subrepo' '
+	git init innocent &&
+	git -C innocent commit --allow-empty -m foo
+'
+
+test_expect_success 'submodule add refuses invalid names' '
+	test_must_fail \
+		git submodule add --name ../../modules/evil "$PWD/innocent" evil
+'
+
+test_expect_success 'add evil submodule' '
+	git submodule add "$PWD/innocent" evil &&
+
+	mkdir modules &&
+	cp -r .git/modules/evil modules &&
+	write_script modules/evil/hooks/post-checkout <<-\EOF &&
+	echo >&2 "RUNNING POST CHECKOUT"
+	EOF
+
+	git config -f .gitmodules submodule.evil.update checkout &&
+	git config -f .gitmodules --rename-section \
+		submodule.evil submodule.../../modules/evil &&
+	git add modules &&
+	git commit -am evil
+'
+
+# This step seems like it shouldn't be necessary, since the payload is
+# contained entirely in the evil submodule. But due to the vagaries of the
+# submodule code, checking out the evil module will fail unless ".git/modules"
+# exists. Adding another submodule (with a name that sorts before "evil") is an
+# easy way to make sure this is the case in the victim clone.
+test_expect_success 'add other submodule' '
+	git submodule add "$PWD/innocent" another-module &&
+	git add another-module &&
+	git commit -am another
+'
+
+test_expect_success 'clone evil superproject' '
+	git clone --recurse-submodules . victim >output 2>&1 &&
+	! grep "RUNNING POST CHECKOUT" output
+'
+
+test_done
diff --git a/utf8.c b/utf8.c
index 0c8e011..cbf22a7 100644
--- a/utf8.c
+++ b/utf8.c
@@ -619,28 +619,33 @@
 	}
 }
 
-int is_hfs_dotgit(const char *path)
+static int is_hfs_dot_generic(const char *path,
+			      const char *needle, size_t needle_len)
 {
 	ucs_char_t c;
 
 	c = next_hfs_char(&path);
 	if (c != '.')
 		return 0;
-	c = next_hfs_char(&path);
 
 	/*
 	 * there's a great deal of other case-folding that occurs
-	 * in HFS+, but this is enough to catch anything that will
-	 * convert to ".git"
+	 * in HFS+, but this is enough to catch our fairly vanilla
+	 * hard-coded needles.
 	 */
-	if (c != 'g' && c != 'G')
-		return 0;
-	c = next_hfs_char(&path);
-	if (c != 'i' && c != 'I')
-		return 0;
-	c = next_hfs_char(&path);
-	if (c != 't' && c != 'T')
-		return 0;
+	for (; needle_len > 0; needle++, needle_len--) {
+		c = next_hfs_char(&path);
+
+		/*
+		 * We know our needles contain only ASCII, so we clamp here to
+		 * make the results of tolower() sane.
+		 */
+		if (c > 127)
+			return 0;
+		if (tolower(c) != *needle)
+			return 0;
+	}
+
 	c = next_hfs_char(&path);
 	if (c && !is_dir_sep(c))
 		return 0;
@@ -648,6 +653,35 @@
 	return 1;
 }
 
+/*
+ * Inline wrapper to make sure the compiler resolves strlen() on literals at
+ * compile time.
+ */
+static inline int is_hfs_dot_str(const char *path, const char *needle)
+{
+	return is_hfs_dot_generic(path, needle, strlen(needle));
+}
+
+int is_hfs_dotgit(const char *path)
+{
+	return is_hfs_dot_str(path, "git");
+}
+
+int is_hfs_dotgitmodules(const char *path)
+{
+	return is_hfs_dot_str(path, "gitmodules");
+}
+
+int is_hfs_dotgitignore(const char *path)
+{
+	return is_hfs_dot_str(path, "gitignore");
+}
+
+int is_hfs_dotgitattributes(const char *path)
+{
+	return is_hfs_dot_str(path, "gitattributes");
+}
+
 const char utf8_bom[] = "\357\273\277";
 
 int skip_utf8_bom(char **text, size_t len)
diff --git a/utf8.h b/utf8.h
index 6bbcf31..da19b43 100644
--- a/utf8.h
+++ b/utf8.h
@@ -52,8 +52,13 @@
  * The path should be NUL-terminated, but we will match variants of both ".git\0"
  * and ".git/..." (but _not_ ".../.git"). This makes it suitable for both fsck
  * and verify_path().
+ *
+ * Likewise, the is_hfs_dotgitfoo() variants look for ".gitfoo".
  */
 int is_hfs_dotgit(const char *path);
+int is_hfs_dotgitmodules(const char *path);
+int is_hfs_dotgitignore(const char *path);
+int is_hfs_dotgitattributes(const char *path);
 
 typedef enum {
 	ALIGN_LEFT,