rev-parse: add option for absolute or relative path formatting

git rev-parse has several options which print various paths. Some of these paths are printed relative to the current working directory, and some are absolute. Normally, this is not a problem, but there are times when one wants paths entirely in one format or another. This can be done trivially if the paths are canonical, but canonicalizing paths is not possible on some shell scripting environments which lack realpath(1) and also in Go, which lacks functions that properly canonicalize paths on Windows. To help out the scripter, let's provide an option which turns most of the paths printed by git rev-parse to be either relative to the current working directory or absolute and canonical. Document which options are affected and which are not so that users are not confused. This approach is cleaner and tidier than providing duplicates of existing options which are either relative or absolute. Note that if the user needs both forms, it is possible to pass an additional option in the middle of the command line which changes the behavior of subsequent operations. Signed-off-by: brian m. carlson <sandals@crustytoothpaste.net> Signed-off-by: Junio C Hamano <gitster@pobox.com>

brian m. carlson committed Dec 13, 2020 at 00:25 UTC fac60b89252e5865da077bb6ccaa9bef43efbfeb
3 files changed +194 -43
Documentation/git-rev-parse.txt
+44 -30
@@ -212,6 +212,18 @@ Options for Files
212 Only the names of the variables are listed, not their value,
213 even if they are set.
214
215 +--path-format=(absolute|relative)::
216 + Controls the behavior of certain other options. If specified as absolute, the
217 + paths printed by those options will be absolute and canonical. If specified as
218 + relative, the paths will be relative to the current working directory if that
219 + is possible. The default is option specific.
220 ++
221 +This option may be specified multiple times and affects only the arguments that
222 +follow it on the command line, either to the end of the command line or the next
223 +instance of this option.
224 +
225 +The following options are modified by `--path-format`:
226 +
227 --git-dir::
228 Show `$GIT_DIR` if defined. Otherwise show the path to
229 the .git directory. The path shown, when relative, is
@@ -221,27 +233,9 @@ If `$GIT_DIR` is not defined and the current directory
233 is not detected to lie in a Git repository or work tree
234 print a message to stderr and exit with nonzero status.
235
224 ---absolute-git-dir::
225 - Like `--git-dir`, but its output is always the canonicalized
226 - absolute path.
227 -
236 --git-common-dir::
237 Show `$GIT_COMMON_DIR` if defined, else `$GIT_DIR`.
238
231 ---is-inside-git-dir::
232 - When the current working directory is below the repository
233 - directory print "true", otherwise "false".
234 -
235 ---is-inside-work-tree::
236 - When the current working directory is inside the work tree of the
237 - repository print "true", otherwise "false".
238 -
239 ---is-bare-repository::
240 - When the repository is bare print "true", otherwise "false".
241 -
242 ---is-shallow-repository::
243 - When the repository is shallow print "true", otherwise "false".
244 -
239 --resolve-git-dir <path>::
240 Check if <path> is a valid repository or a gitfile that
241 points at a valid repository, and print the location of the
@@ -255,19 +249,9 @@ print a message to stderr and exit with nonzero status.
249 $GIT_OBJECT_DIRECTORY is set to /foo/bar then "git rev-parse
250 --git-path objects/abc" returns /foo/bar/abc.
251
258 ---show-cdup::
259 - When the command is invoked from a subdirectory, show the
260 - path of the top-level directory relative to the current
261 - directory (typically a sequence of "../", or an empty string).
262 -
263 ---show-prefix::
264 - When the command is invoked from a subdirectory, show the
265 - path of the current directory relative to the top-level
266 - directory.
267 -
252 --show-toplevel::
269 - Show the absolute path of the top-level directory of the working
270 - tree. If there is no working tree, report an error.
253 + Show the (by default, absolute) path of the top-level directory
254 + of the working tree. If there is no working tree, report an error.
255
256 --show-superproject-working-tree::
257 Show the absolute path of the root of the superproject's
@@ -279,6 +263,36 @@ print a message to stderr and exit with nonzero status.
263 Show the path to the shared index file in split index mode, or
264 empty if not in split-index mode.
265
266 +The following options are unaffected by `--path-format`:
267 +
268 +--absolute-git-dir::
269 + Like `--git-dir`, but its output is always the canonicalized
270 + absolute path.
271 +
272 +--is-inside-git-dir::
273 + When the current working directory is below the repository
274 + directory print "true", otherwise "false".
275 +
276 +--is-inside-work-tree::
277 + When the current working directory is inside the work tree of the
278 + repository print "true", otherwise "false".
279 +
280 +--is-bare-repository::
281 + When the repository is bare print "true", otherwise "false".
282 +
283 +--is-shallow-repository::
284 + When the repository is shallow print "true", otherwise "false".
285 +
286 +--show-cdup::
287 + When the command is invoked from a subdirectory, show the
288 + path of the top-level directory relative to the current
289 + directory (typically a sequence of "../", or an empty string).
290 +
291 +--show-prefix::
292 + When the command is invoked from a subdirectory, show the
293 + path of the current directory relative to the top-level
294 + directory.
295 +
296 --show-object-format[=(storage|input|output)]::
297 Show the object format (hash algorithm) used for the repository
298 for storage inside the `.git` directory, input, or output. For
builtin/rev-parse.c
+94 -12
@@ -583,6 +583,75 @@ static void handle_ref_opt(const char *pattern, const char *prefix)
583 clear_ref_exclusion(&ref_excludes);
584 }
585
586 +enum format_type {
587 + /* We would like a relative path. */
588 + FORMAT_RELATIVE,
589 + /* We would like a canonical absolute path. */
590 + FORMAT_CANONICAL,
591 + /* We would like the default behavior. */
592 + FORMAT_DEFAULT,
593 +};
594 +
595 +enum default_type {
596 + /* Our default is a relative path. */
597 + DEFAULT_RELATIVE,
598 + /* Our default is a relative path if there's a shared root. */
599 + DEFAULT_RELATIVE_IF_SHARED,
600 + /* Our default is a canonical absolute path. */
601 + DEFAULT_CANONICAL,
602 + /* Our default is not to modify the item. */
603 + DEFAULT_UNMODIFIED,
604 +};
605 +
606 +static void print_path(const char *path, const char *prefix, enum format_type format, enum default_type def)
607 +{
608 + char *cwd = NULL;
609 + /*
610 + * We don't ever produce a relative path if prefix is NULL, so set the
611 + * prefix to the current directory so that we can produce a relative
612 + * path whenever possible. If we're using RELATIVE_IF_SHARED mode, then
613 + * we want an absolute path unless the two share a common prefix, so don't
614 + * set it in that case, since doing so causes a relative path to always
615 + * be produced if possible.
616 + */
617 + if (!prefix && (format != FORMAT_DEFAULT || def != DEFAULT_RELATIVE_IF_SHARED))
618 + prefix = cwd = xgetcwd();
619 + if (format == FORMAT_DEFAULT && def == DEFAULT_UNMODIFIED) {
620 + puts(path);
621 + } else if (format == FORMAT_RELATIVE ||
622 + (format == FORMAT_DEFAULT && def == DEFAULT_RELATIVE)) {
623 + /*
624 + * In order for relative_path to work as expected, we need to
625 + * make sure that both paths are absolute paths. If we don't,
626 + * we can end up with an unexpected absolute path that the user
627 + * didn't want.
628 + */
629 + struct strbuf buf = STRBUF_INIT, realbuf = STRBUF_INIT, prefixbuf = STRBUF_INIT;
630 + if (!is_absolute_path(path)) {
631 + strbuf_realpath_forgiving(&realbuf, path, 1);
632 + path = realbuf.buf;
633 + }
634 + if (!is_absolute_path(prefix)) {
635 + strbuf_realpath_forgiving(&prefixbuf, prefix, 1);
636 + prefix = prefixbuf.buf;
637 + }
638 + puts(relative_path(path, prefix, &buf));
639 + strbuf_release(&buf);
640 + strbuf_release(&realbuf);
641 + strbuf_release(&prefixbuf);
642 + } else if (format == FORMAT_DEFAULT && def == DEFAULT_RELATIVE_IF_SHARED) {
643 + struct strbuf buf = STRBUF_INIT;
644 + puts(relative_path(path, prefix, &buf));
645 + strbuf_release(&buf);
646 + } else {
647 + struct strbuf buf = STRBUF_INIT;
648 + strbuf_realpath_forgiving(&buf, path, 1);
649 + puts(buf.buf);
650 + strbuf_release(&buf);
651 + }
652 + free(cwd);
653 +}
654 +
655 int cmd_rev_parse(int argc, const char **argv, const char *prefix)
656 {
657 int i, as_is = 0, verify = 0, quiet = 0, revs_count = 0, type = 0;
@@ -596,6 +665,7 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
665 struct strbuf buf = STRBUF_INIT;
666 const int hexsz = the_hash_algo->hexsz;
667 int seen_end_of_options = 0;
668 + enum format_type format = FORMAT_DEFAULT;
669
670 if (argc > 1 && !strcmp("--parseopt", argv[1]))
671 return cmd_parseopt(argc - 1, argv + 1, prefix);
@@ -668,8 +738,9 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
738 if (!argv[i + 1])
739 die("--git-path requires an argument");
740 strbuf_reset(&buf);
671 - puts(relative_path(git_path("%s", argv[i + 1]),
672 - prefix, &buf));
741 + print_path(git_path("%s", argv[i + 1]), prefix,
742 + format,
743 + DEFAULT_RELATIVE_IF_SHARED);
744 i++;
745 continue;
746 }
@@ -687,6 +758,16 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
758 show(arg);
759 continue;
760 }
761 + if (opt_with_value(arg, "--path-format", &arg)) {
762 + if (!strcmp(arg, "absolute")) {
763 + format = FORMAT_CANONICAL;
764 + } else if (!strcmp(arg, "relative")) {
765 + format = FORMAT_RELATIVE;
766 + } else {
767 + die("unknown argument to --path-format: %s", arg);
768 + }
769 + continue;
770 + }
771 if (!strcmp(arg, "--default")) {
772 def = argv[++i];
773 if (!def)
@@ -807,7 +888,7 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
888 if (!strcmp(arg, "--show-toplevel")) {
889 const char *work_tree = get_git_work_tree();
890 if (work_tree)
810 - puts(work_tree);
891 + print_path(work_tree, prefix, format, DEFAULT_UNMODIFIED);
892 else
893 die("this operation must be run in a work tree");
894 continue;
@@ -815,7 +896,7 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
896 if (!strcmp(arg, "--show-superproject-working-tree")) {
897 struct strbuf superproject = STRBUF_INIT;
898 if (get_superproject_working_tree(&superproject))
818 - puts(superproject.buf);
899 + print_path(superproject.buf, prefix, format, DEFAULT_UNMODIFIED);
900 strbuf_release(&superproject);
901 continue;
902 }
@@ -850,16 +931,18 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
931 const char *gitdir = getenv(GIT_DIR_ENVIRONMENT);
932 char *cwd;
933 int len;
934 + enum format_type wanted = format;
935 if (arg[2] == 'g') { /* --git-dir */
936 if (gitdir) {
855 - puts(gitdir);
937 + print_path(gitdir, prefix, format, DEFAULT_UNMODIFIED);
938 continue;
939 }
940 if (!prefix) {
859 - puts(".git");
941 + print_path(".git", prefix, format, DEFAULT_UNMODIFIED);
942 continue;
943 }
944 } else { /* --absolute-git-dir */
945 + wanted = FORMAT_CANONICAL;
946 if (!gitdir && !prefix)
947 gitdir = ".git";
948 if (gitdir) {
@@ -872,14 +955,14 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
955 }
956 cwd = xgetcwd();
957 len = strlen(cwd);
875 - printf("%s%s.git\n", cwd, len && cwd[len-1] != '/' ? "/" : "");
958 + strbuf_reset(&buf);
959 + strbuf_addf(&buf, "%s%s.git", cwd, len && cwd[len-1] != '/' ? "/" : "");
960 free(cwd);
961 + print_path(buf.buf, prefix, wanted, DEFAULT_CANONICAL);
962 continue;
963 }
964 if (!strcmp(arg, "--git-common-dir")) {
880 - strbuf_reset(&buf);
881 - puts(relative_path(get_git_common_dir(),
882 - prefix, &buf));
965 + print_path(get_git_common_dir(), prefix, format, DEFAULT_RELATIVE_IF_SHARED);
966 continue;
967 }
968 if (!strcmp(arg, "--is-inside-git-dir")) {
@@ -909,8 +992,7 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)
992 if (the_index.split_index) {
993 const struct object_id *oid = &the_index.split_index->base_oid;
994 const char *path = git_path("sharedindex.%s", oid_to_hex(oid));
912 - strbuf_reset(&buf);
913 - puts(relative_path(path, prefix, &buf));
995 + print_path(path, prefix, format, DEFAULT_RELATIVE);
996 }
997 continue;
998 }
t/t1500-rev-parse.sh
+56 -1
@@ -3,6 +3,16 @@
3 test_description='test git rev-parse'
4 . ./test-lib.sh
5
6 +test_one () {
7 + dir="$1" &&
8 + expect="$2" &&
9 + shift &&
10 + shift &&
11 + echo "$expect" >expect &&
12 + git -C "$dir" rev-parse "$@" >actual &&
13 + test_cmp expect actual
14 +}
15 +
16 # usage: [options] label is-bare is-inside-git is-inside-work prefix git-dir absolute-git-dir
17 test_rev_parse () {
18 d=
@@ -60,7 +70,13 @@ ROOT=$(pwd)
70
71 test_expect_success 'setup' '
72 mkdir -p sub/dir work &&
63 - cp -R .git repo.git
73 + cp -R .git repo.git &&
74 + git checkout -B main &&
75 + test_commit abc &&
76 + git checkout -b side &&
77 + test_commit def &&
78 + git checkout main &&
79 + git worktree add worktree side
80 '
81
82 test_rev_parse toplevel false false true '' .git "$ROOT/.git"
@@ -88,6 +104,45 @@ test_rev_parse -C work -g ../repo.git -b t 'GIT_DIR=../repo.git, core.bare = tru
104
105 test_rev_parse -C work -g ../repo.git -b u 'GIT_DIR=../repo.git, core.bare undefined' false false true ''
106
107 +test_expect_success 'rev-parse --path-format=absolute' '
108 + test_one "." "$ROOT/.git" --path-format=absolute --git-dir &&
109 + test_one "." "$ROOT/.git" --path-format=absolute --git-common-dir &&
110 + test_one "sub/dir" "$ROOT/.git" --path-format=absolute --git-dir &&
111 + test_one "sub/dir" "$ROOT/.git" --path-format=absolute --git-common-dir &&
112 + test_one "worktree" "$ROOT/.git/worktrees/worktree" --path-format=absolute --git-dir &&
113 + test_one "worktree" "$ROOT/.git" --path-format=absolute --git-common-dir &&
114 + test_one "." "$ROOT" --path-format=absolute --show-toplevel &&
115 + test_one "." "$ROOT/.git/objects" --path-format=absolute --git-path objects &&
116 + test_one "." "$ROOT/.git/objects/foo/bar/baz" --path-format=absolute --git-path objects/foo/bar/baz
117 +'
118 +
119 +test_expect_success 'rev-parse --path-format=relative' '
120 + test_one "." ".git" --path-format=relative --git-dir &&
121 + test_one "." ".git" --path-format=relative --git-common-dir &&
122 + test_one "sub/dir" "../../.git" --path-format=relative --git-dir &&
123 + test_one "sub/dir" "../../.git" --path-format=relative --git-common-dir &&
124 + test_one "worktree" "../.git/worktrees/worktree" --path-format=relative --git-dir &&
125 + test_one "worktree" "../.git" --path-format=relative --git-common-dir &&
126 + test_one "." "./" --path-format=relative --show-toplevel &&
127 + test_one "." ".git/objects" --path-format=relative --git-path objects &&
128 + test_one "." ".git/objects/foo/bar/baz" --path-format=relative --git-path objects/foo/bar/baz
129 +'
130 +
131 +test_expect_success '--path-format=relative does not affect --absolute-git-dir' '
132 + git rev-parse --path-format=relative --absolute-git-dir >actual &&
133 + echo "$ROOT/.git" >expect &&
134 + test_cmp expect actual
135 +'
136 +
137 +test_expect_success '--path-format can change in the middle of the command line' '
138 + git rev-parse --path-format=absolute --git-dir --path-format=relative --git-path objects/foo/bar >actual &&
139 + cat >expect <<-EOF &&
140 + $ROOT/.git
141 + .git/objects/foo/bar
142 + EOF
143 + test_cmp expect actual
144 +'
145 +
146 test_expect_success 'git-common-dir from worktree root' '
147 echo .git >expect &&
148 git rev-parse --git-common-dir >actual &&