alternates: accept double-quoted paths

We read lists of alternates from objects/info/alternates files (delimited by newline), as well as from the GIT_ALTERNATE_OBJECT_DIRECTORIES environment variable (delimited by colon or semi-colon, depending on the platform). There's no mechanism for quoting the delimiters, so it's impossible to specify an alternate path that contains a colon in the environment, or one that contains a newline in a file. We've lived with that restriction for ages because both alternates and filenames with colons are relatively rare, and it's only a problem when the two meet. But since 722ff7f87 (receive-pack: quarantine objects until pre-receive accepts, 2016-10-03), which builds on the alternates system, every push causes the receiver to set GIT_ALTERNATE_OBJECT_DIRECTORIES internally. It would be convenient to have some way to quote the delimiter so that we can represent arbitrary paths. The simplest thing would be an escape character before a quoted delimiter (e.g., "\:" as a literal colon). But that creates a backwards compatibility problem: any path which uses that escape character is now broken, and we've just shifted the problem. We could choose an unlikely escape character (e.g., something from the non-printable ASCII range), but that's awkward to use. Instead, let's treat names as unquoted unless they begin with a double-quote, in which case they are interpreted via our usual C-stylke quoting rules. This also breaks backwards-compatibility, but in a smaller way: it only matters if your file has a double-quote as the very _first_ character in the path (whereas an escape character is a problem anywhere in the path). It's also consistent with many other parts of git, which accept either a bare pathname or a double-quoted one, and the sender can choose to quote or not as required. Signed-off-by: Jeff King <peff@peff.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jeff King committed Dec 12, 2016 at 14:52 UTC cf3c6352100a0d302276e46e3f9a7f0804e224d8
3 files changed +60 -11
Documentation/git.txt
+6
@@ -859,6 +859,12 @@ Git so take care if using a foreign front-end.
859 specifies a ":" separated (on Windows ";" separated) list
860 of Git object directories which can be used to search for Git
861 objects. New objects will not be written to these directories.
862 ++
863 + Entries that begin with `"` (double-quote) will be interpreted
864 + as C-style quoted paths, removing leading and trailing
865 + double-quotes and respecting backslash escapes. E.g., the value
866 + `"path-with-\"-and-:-in-it":vanilla-path` has two paths:
867 + `path-with-"-and-:-in-it` and `vanilla-path`.
868
869 `GIT_DIR`::
870 If the `GIT_DIR` environment variable is set then it
sha1_file.c
+36 -11
@@ -26,6 +26,7 @@
26 #include "mru.h"
27 #include "list.h"
28 #include "mergesort.h"
29 +#include "quote.h"
30
31 #ifndef O_NOATIME
32 #if defined(__linux__) && (defined(__i386__) || defined(__PPC__))
@@ -329,13 +330,40 @@ static int link_alt_odb_entry(const char *entry, const char *relative_base,
330 return 0;
331 }
332
333 +static const char *parse_alt_odb_entry(const char *string,
334 + int sep,
335 + struct strbuf *out)
336 +{
337 + const char *end;
338 +
339 + strbuf_reset(out);
340 +
341 + if (*string == '#') {
342 + /* comment; consume up to next separator */
343 + end = strchrnul(string, sep);
344 + } else if (*string == '"' && !unquote_c_style(out, string, &end)) {
345 + /*
346 + * quoted path; unquote_c_style has copied the
347 + * data for us and set "end". Broken quoting (e.g.,
348 + * an entry that doesn't end with a quote) falls
349 + * back to the unquoted case below.
350 + */
351 + } else {
352 + /* normal, unquoted path */
353 + end = strchrnul(string, sep);
354 + strbuf_add(out, string, end - string);
355 + }
356 +
357 + if (*end)
358 + end++;
359 + return end;
360 +}
361 +
362 static void link_alt_odb_entries(const char *alt, int len, int sep,
363 const char *relative_base, int depth)
364 {
335 - struct string_list entries = STRING_LIST_INIT_NODUP;
336 - char *alt_copy;
337 - int i;
365 struct strbuf objdirbuf = STRBUF_INIT;
366 + struct strbuf entry = STRBUF_INIT;
367
368 if (depth > 5) {
369 error("%s: ignoring alternate object stores, nesting too deep.",
@@ -348,16 +376,13 @@ static void link_alt_odb_entries(const char *alt, int len, int sep,
376 die("unable to normalize object directory: %s",
377 objdirbuf.buf);
378
351 - alt_copy = xmemdupz(alt, len);
352 - string_list_split_in_place(&entries, alt_copy, sep, -1);
353 - for (i = 0; i < entries.nr; i++) {
354 - const char *entry = entries.items[i].string;
355 - if (entry[0] == '\0' || entry[0] == '#')
379 + while (*alt) {
380 + alt = parse_alt_odb_entry(alt, sep, &entry);
381 + if (!entry.len)
382 continue;
357 - link_alt_odb_entry(entry, relative_base, depth, objdirbuf.buf);
383 + link_alt_odb_entry(entry.buf, relative_base, depth, objdirbuf.buf);
384 }
359 - string_list_clear(&entries, 0);
360 - free(alt_copy);
385 + strbuf_release(&entry);
386 strbuf_release(&objdirbuf);
387 }
388
t/t5615-alternate-env.sh
+18
@@ -68,4 +68,22 @@ test_expect_success 'access alternate via relative path (subdir)' '
68 EOF
69 '
70
71 +# set variables outside test to avoid quote insanity; the \057 is '/',
72 +# which doesn't need quoting, but just confirms that de-quoting
73 +# is working.
74 +quoted='"one.git\057objects"'
75 +unquoted='two.git/objects'
76 +test_expect_success 'mix of quoted and unquoted alternates' '
77 + check_obj "$quoted:$unquoted" <<-EOF
78 + $one blob
79 + $two blob
80 +'
81 +
82 +test_expect_success 'broken quoting falls back to interpreting raw' '
83 + mv one.git \"one.git &&
84 + check_obj \"one.git/objects <<-EOF
85 + $one blob
86 + EOF
87 +'
88 +
89 test_done