{"thread":{"id":"54815","subject":"[PATCH v5 2/2] rev-parse: add option for absolute or relative path formatting","startedAt":"2020-12-13T00:26:57Z","lastAt":"2020-12-13T02:36:30Z","messageCount":4,"participants":["brian m. carlson","Eric Sunshine"],"isPatch":true,"patchVersion":5,"patchTotal":2},"messages":[{"id":"412124","messageId":"20201213002529.542928-3-sandals@crustytoothpaste.net","threadId":"54815","inReplyTo":"20201213002529.542928-1-sandals@crustytoothpaste.net","subject":"[PATCH v5 2/2] rev-parse: add option for absolute or relative path formatting","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2020-12-13T00:25:29Z","receivedAt":"2020-12-13T00:26:57Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"git rev-parse has several options which print various paths.  Some of\nthese paths are printed relative to the current working directory, and\nsome are absolute.\n\nNormally, this is not a problem, but there are times when one wants\npaths entirely in one format or another.  This can be done trivially if\nthe paths are canonical, but canonicalizing paths is not possible on\nsome shell scripting environments which lack realpath(1) and also in Go,\nwhich lacks functions that properly canonicalize paths on Windows.\n\nTo help out the scripter, let's provide an option which turns most of\nthe paths printed by git rev-parse to be either relative to the current\nworking directory or absolute and canonical.  Document which options are\naffected and which are not so that users are not confused.\n\nThis approach is cleaner and tidier than providing duplicates of\nexisting options which are either relative or absolute.\n\nNote that if the user needs both forms, it is possible to pass an\nadditional option in the middle of the command line which changes the\nbehavior of subsequent operations.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n Documentation/git-rev-parse.txt |  74 +++++++++++++---------\n builtin/rev-parse.c             | 106 ++++++++++++++++++++++++++++----\n t/t1500-rev-parse.sh            |  57 ++++++++++++++++-\n 3 files changed, 194 insertions(+), 43 deletions(-)\n\ndiff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt\nindex 5013daa6ef..6b8ca085aa 100644\n--- a/Documentation/git-rev-parse.txt\n+++ b/Documentation/git-rev-parse.txt\n@@ -212,6 +212,18 @@ Options for Files\n \tOnly the names of the variables are listed, not their value,\n \teven if they are set.\n \n+--path-format=(absolute|relative)::\n+\tControls the behavior of certain other options. If specified as absolute, the\n+\tpaths printed by those options will be absolute and canonical. If specified as\n+\trelative, the paths will be relative to the current working directory if that\n+\tis possible.  The default is option specific.\n++\n+This option may be specified multiple times and affects only the arguments that\n+follow it on the command line, either to the end of the command line or the next\n+instance of this option.\n+\n+The following options are modified by `--path-format`:\n+\n --git-dir::\n \tShow `$GIT_DIR` if defined. Otherwise show the path to\n \tthe .git directory. The path shown, when relative, is\n@@ -221,13 +233,42 @@ If `$GIT_DIR` is not defined and the current directory\n is not detected to lie in a Git repository or work tree\n print a message to stderr and exit with nonzero status.\n \n+--git-common-dir::\n+\tShow `$GIT_COMMON_DIR` if defined, else `$GIT_DIR`.\n+\n+--resolve-git-dir <path>::\n+\tCheck if <path> is a valid repository or a gitfile that\n+\tpoints at a valid repository, and print the location of the\n+\trepository.  If <path> is a gitfile then the resolved path\n+\tto the real repository is printed.\n+\n+--git-path <path>::\n+\tResolve \"$GIT_DIR/<path>\" and takes other path relocation\n+\tvariables such as $GIT_OBJECT_DIRECTORY,\n+\t$GIT_INDEX_FILE... into account. For example, if\n+\t$GIT_OBJECT_DIRECTORY is set to /foo/bar then \"git rev-parse\n+\t--git-path objects/abc\" returns /foo/bar/abc.\n+\n+--show-toplevel::\n+\tShow the (by default, absolute) path of the top-level directory\n+\tof the working tree. If there is no working tree, report an error.\n+\n+--show-superproject-working-tree::\n+\tShow the absolute path of the root of the superproject's\n+\tworking tree (if exists) that uses the current repository as\n+\tits submodule.  Outputs nothing if the current repository is\n+\tnot used as a submodule by any project.\n+\n+--shared-index-path::\n+\tShow the path to the shared index file in split index mode, or\n+\tempty if not in split-index mode.\n+\n+The following options are unaffected by `--path-format`:\n+\n --absolute-git-dir::\n \tLike `--git-dir`, but its output is always the canonicalized\n \tabsolute path.\n \n---git-common-dir::\n-\tShow `$GIT_COMMON_DIR` if defined, else `$GIT_DIR`.\n-\n --is-inside-git-dir::\n \tWhen the current working directory is below the repository\n \tdirectory print \"true\", otherwise \"false\".\n@@ -242,19 +283,6 @@ print a message to stderr and exit with nonzero status.\n --is-shallow-repository::\n \tWhen the repository is shallow print \"true\", otherwise \"false\".\n \n---resolve-git-dir <path>::\n-\tCheck if <path> is a valid repository or a gitfile that\n-\tpoints at a valid repository, and print the location of the\n-\trepository.  If <path> is a gitfile then the resolved path\n-\tto the real repository is printed.\n-\n---git-path <path>::\n-\tResolve \"$GIT_DIR/<path>\" and takes other path relocation\n-\tvariables such as $GIT_OBJECT_DIRECTORY,\n-\t$GIT_INDEX_FILE... into account. For example, if\n-\t$GIT_OBJECT_DIRECTORY is set to /foo/bar then \"git rev-parse\n-\t--git-path objects/abc\" returns /foo/bar/abc.\n-\n --show-cdup::\n \tWhen the command is invoked from a subdirectory, show the\n \tpath of the top-level directory relative to the current\n@@ -265,20 +293,6 @@ print a message to stderr and exit with nonzero status.\n \tpath of the current directory relative to the top-level\n \tdirectory.\n \n---show-toplevel::\n-\tShow the absolute path of the top-level directory of the working\n-\ttree. If there is no working tree, report an error.\n-\n---show-superproject-working-tree::\n-\tShow the absolute path of the root of the superproject's\n-\tworking tree (if exists) that uses the current repository as\n-\tits submodule.  Outputs nothing if the current repository is\n-\tnot used as a submodule by any project.\n-\n---shared-index-path::\n-\tShow the path to the shared index file in split index mode, or\n-\tempty if not in split-index mode.\n-\n --show-object-format[=(storage|input|output)]::\n \tShow the object format (hash algorithm) used for the repository\n \tfor storage inside the `.git` directory, input, or output. For\ndiff --git a/builtin/rev-parse.c b/builtin/rev-parse.c\nindex 69ba7326cf..85bad9052e 100644\n--- a/builtin/rev-parse.c\n+++ b/builtin/rev-parse.c\n@@ -583,6 +583,75 @@ static void handle_ref_opt(const char *pattern, const char *prefix)\n \tclear_ref_exclusion(&ref_excludes);\n }\n \n+enum format_type {\n+\t/* We would like a relative path. */\n+\tFORMAT_RELATIVE,\n+\t/* We would like a canonical absolute path. */\n+\tFORMAT_CANONICAL,\n+\t/* We would like the default behavior. */\n+\tFORMAT_DEFAULT,\n+};\n+\n+enum default_type {\n+\t/* Our default is a relative path. */\n+\tDEFAULT_RELATIVE,\n+\t/* Our default is a relative path if there's a shared root. */\n+\tDEFAULT_RELATIVE_IF_SHARED,\n+\t/* Our default is a canonical absolute path. */\n+\tDEFAULT_CANONICAL,\n+\t/* Our default is not to modify the item. */\n+\tDEFAULT_UNMODIFIED,\n+};\n+\n+static void print_path(const char *path, const char *prefix, enum format_type format, enum default_type def)\n+{\n+\tchar *cwd = NULL;\n+\t/*\n+\t * We don't ever produce a relative path if prefix is NULL, so set the\n+\t * prefix to the current directory so that we can produce a relative\n+\t * path whenever possible.  If we're using RELATIVE_IF_SHARED mode, then\n+\t * we want an absolute path unless the two share a common prefix, so don't\n+\t * set it in that case, since doing so causes a relative path to always\n+\t * be produced if possible.\n+\t */\n+\tif (!prefix && (format != FORMAT_DEFAULT || def != DEFAULT_RELATIVE_IF_SHARED))\n+\t\tprefix = cwd = xgetcwd();\n+\tif (format == FORMAT_DEFAULT && def == DEFAULT_UNMODIFIED) {\n+\t\tputs(path);\n+\t} else if (format == FORMAT_RELATIVE ||\n+\t\t  (format == FORMAT_DEFAULT && def == DEFAULT_RELATIVE)) {\n+\t\t/*\n+\t\t * In order for relative_path to work as expected, we need to\n+\t\t * make sure that both paths are absolute paths.  If we don't,\n+\t\t * we can end up with an unexpected absolute path that the user\n+\t\t * didn't want.\n+\t\t */\n+\t\tstruct strbuf buf = STRBUF_INIT, realbuf = STRBUF_INIT, prefixbuf = STRBUF_INIT;\n+\t\tif (!is_absolute_path(path)) {\n+\t\t\tstrbuf_realpath_forgiving(&realbuf, path,  1);\n+\t\t\tpath = realbuf.buf;\n+\t\t}\n+\t\tif (!is_absolute_path(prefix)) {\n+\t\t\tstrbuf_realpath_forgiving(&prefixbuf, prefix, 1);\n+\t\t\tprefix = prefixbuf.buf;\n+\t\t}\n+\t\tputs(relative_path(path, prefix, &buf));\n+\t\tstrbuf_release(&buf);\n+\t\tstrbuf_release(&realbuf);\n+\t\tstrbuf_release(&prefixbuf);\n+\t} else if (format == FORMAT_DEFAULT && def == DEFAULT_RELATIVE_IF_SHARED) {\n+\t\tstruct strbuf buf = STRBUF_INIT;\n+\t\tputs(relative_path(path, prefix, &buf));\n+\t\tstrbuf_release(&buf);\n+\t} else {\n+\t\tstruct strbuf buf = STRBUF_INIT;\n+\t\tstrbuf_realpath_forgiving(&buf, path, 1);\n+\t\tputs(buf.buf);\n+\t\tstrbuf_release(&buf);\n+\t}\n+\tfree(cwd);\n+}\n+\n int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n {\n \tint i, as_is = 0, verify = 0, quiet = 0, revs_count = 0, type = 0;\n@@ -596,6 +665,7 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n \tstruct strbuf buf = STRBUF_INIT;\n \tconst int hexsz = the_hash_algo->hexsz;\n \tint seen_end_of_options = 0;\n+\tenum format_type format = FORMAT_DEFAULT;\n \n \tif (argc > 1 && !strcmp(\"--parseopt\", argv[1]))\n \t\treturn cmd_parseopt(argc - 1, argv + 1, prefix);\n@@ -668,8 +738,9 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n \t\t\t\tif (!argv[i + 1])\n \t\t\t\t\tdie(\"--git-path requires an argument\");\n \t\t\t\tstrbuf_reset(&buf);\n-\t\t\t\tputs(relative_path(git_path(\"%s\", argv[i + 1]),\n-\t\t\t\t\t\t   prefix, &buf));\n+\t\t\t\tprint_path(git_path(\"%s\", argv[i + 1]), prefix,\n+\t\t\t\t\t\tformat,\n+\t\t\t\t\t\tDEFAULT_RELATIVE_IF_SHARED);\n \t\t\t\ti++;\n \t\t\t\tcontinue;\n \t\t\t}\n@@ -687,6 +758,16 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n \t\t\t\t\tshow(arg);\n \t\t\t\tcontinue;\n \t\t\t}\n+\t\t\tif (opt_with_value(arg, \"--path-format\", &arg)) {\n+\t\t\t\tif (!strcmp(arg, \"absolute\")) {\n+\t\t\t\t\tformat = FORMAT_CANONICAL;\n+\t\t\t\t} else if (!strcmp(arg, \"relative\")) {\n+\t\t\t\t\tformat = FORMAT_RELATIVE;\n+\t\t\t\t} else {\n+\t\t\t\t\tdie(\"unknown argument to --path-format: %s\", arg);\n+\t\t\t\t}\n+\t\t\t\tcontinue;\n+\t\t\t}\n \t\t\tif (!strcmp(arg, \"--default\")) {\n \t\t\t\tdef = argv[++i];\n \t\t\t\tif (!def)\n@@ -807,7 +888,7 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n \t\t\tif (!strcmp(arg, \"--show-toplevel\")) {\n \t\t\t\tconst char *work_tree = get_git_work_tree();\n \t\t\t\tif (work_tree)\n-\t\t\t\t\tputs(work_tree);\n+\t\t\t\t\tprint_path(work_tree, prefix, format, DEFAULT_UNMODIFIED);\n \t\t\t\telse\n \t\t\t\t\tdie(\"this operation must be run in a work tree\");\n \t\t\t\tcontinue;\n@@ -815,7 +896,7 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n \t\t\tif (!strcmp(arg, \"--show-superproject-working-tree\")) {\n \t\t\t\tstruct strbuf superproject = STRBUF_INIT;\n \t\t\t\tif (get_superproject_working_tree(&superproject))\n-\t\t\t\t\tputs(superproject.buf);\n+\t\t\t\t\tprint_path(superproject.buf, prefix, format, DEFAULT_UNMODIFIED);\n \t\t\t\tstrbuf_release(&superproject);\n \t\t\t\tcontinue;\n \t\t\t}\n@@ -850,16 +931,18 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n \t\t\t\tconst char *gitdir = getenv(GIT_DIR_ENVIRONMENT);\n \t\t\t\tchar *cwd;\n \t\t\t\tint len;\n+\t\t\t\tenum format_type wanted = format;\n \t\t\t\tif (arg[2] == 'g') {\t/* --git-dir */\n \t\t\t\t\tif (gitdir) {\n-\t\t\t\t\t\tputs(gitdir);\n+\t\t\t\t\t\tprint_path(gitdir, prefix, format, DEFAULT_UNMODIFIED);\n \t\t\t\t\t\tcontinue;\n \t\t\t\t\t}\n \t\t\t\t\tif (!prefix) {\n-\t\t\t\t\t\tputs(\".git\");\n+\t\t\t\t\t\tprint_path(\".git\", prefix, format, DEFAULT_UNMODIFIED);\n \t\t\t\t\t\tcontinue;\n \t\t\t\t\t}\n \t\t\t\t} else {\t\t/* --absolute-git-dir */\n+\t\t\t\t\twanted = FORMAT_CANONICAL;\n \t\t\t\t\tif (!gitdir && !prefix)\n \t\t\t\t\t\tgitdir = \".git\";\n \t\t\t\t\tif (gitdir) {\n@@ -872,14 +955,14 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n \t\t\t\t}\n \t\t\t\tcwd = xgetcwd();\n \t\t\t\tlen = strlen(cwd);\n-\t\t\t\tprintf(\"%s%s.git\\n\", cwd, len && cwd[len-1] != '/' ? \"/\" : \"\");\n+\t\t\t\tstrbuf_reset(&buf);\n+\t\t\t\tstrbuf_addf(&buf, \"%s%s.git\", cwd, len && cwd[len-1] != '/' ? \"/\" : \"\");\n \t\t\t\tfree(cwd);\n+\t\t\t\tprint_path(buf.buf, prefix, wanted, DEFAULT_CANONICAL);\n \t\t\t\tcontinue;\n \t\t\t}\n \t\t\tif (!strcmp(arg, \"--git-common-dir\")) {\n-\t\t\t\tstrbuf_reset(&buf);\n-\t\t\t\tputs(relative_path(get_git_common_dir(),\n-\t\t\t\t\t\t   prefix, &buf));\n+\t\t\t\tprint_path(get_git_common_dir(), prefix, format, DEFAULT_RELATIVE_IF_SHARED);\n \t\t\t\tcontinue;\n \t\t\t}\n \t\t\tif (!strcmp(arg, \"--is-inside-git-dir\")) {\n@@ -909,8 +992,7 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n \t\t\t\tif (the_index.split_index) {\n \t\t\t\t\tconst struct object_id *oid = &the_index.split_index->base_oid;\n \t\t\t\t\tconst char *path = git_path(\"sharedindex.%s\", oid_to_hex(oid));\n-\t\t\t\t\tstrbuf_reset(&buf);\n-\t\t\t\t\tputs(relative_path(path, prefix, &buf));\n+\t\t\t\t\tprint_path(path, prefix, format, DEFAULT_RELATIVE);\n \t\t\t\t}\n \t\t\t\tcontinue;\n \t\t\t}\ndiff --git a/t/t1500-rev-parse.sh b/t/t1500-rev-parse.sh\nindex 408b97d5af..51d7d40ec1 100755\n--- a/t/t1500-rev-parse.sh\n+++ b/t/t1500-rev-parse.sh\n@@ -3,6 +3,16 @@\n test_description='test git rev-parse'\n . ./test-lib.sh\n \n+test_one () {\n+\tdir=\"$1\" &&\n+\texpect=\"$2\" &&\n+\tshift &&\n+\tshift &&\n+\techo \"$expect\" >expect &&\n+\tgit -C \"$dir\" rev-parse \"$@\" >actual &&\n+\ttest_cmp expect actual\n+}\n+\n # usage: [options] label is-bare is-inside-git is-inside-work prefix git-dir absolute-git-dir\n test_rev_parse () {\n \td=\n@@ -60,7 +70,13 @@ ROOT=$(pwd)\n \n test_expect_success 'setup' '\n \tmkdir -p sub/dir work &&\n-\tcp -R .git repo.git\n+\tcp -R .git repo.git &&\n+\tgit checkout -B main &&\n+\ttest_commit abc &&\n+\tgit checkout -b side &&\n+\ttest_commit def &&\n+\tgit checkout main &&\n+\tgit worktree add worktree side\n '\n \n test_rev_parse toplevel false false true '' .git \"$ROOT/.git\"\n@@ -88,6 +104,45 @@ test_rev_parse -C work -g ../repo.git -b t 'GIT_DIR=../repo.git, core.bare = tru\n \n test_rev_parse -C work -g ../repo.git -b u 'GIT_DIR=../repo.git, core.bare undefined' false false true ''\n \n+test_expect_success 'rev-parse --path-format=absolute' '\n+\ttest_one \".\" \"$ROOT/.git\" --path-format=absolute --git-dir &&\n+\ttest_one \".\" \"$ROOT/.git\" --path-format=absolute --git-common-dir &&\n+\ttest_one \"sub/dir\" \"$ROOT/.git\" --path-format=absolute --git-dir &&\n+\ttest_one \"sub/dir\" \"$ROOT/.git\" --path-format=absolute --git-common-dir &&\n+\ttest_one \"worktree\" \"$ROOT/.git/worktrees/worktree\" --path-format=absolute --git-dir &&\n+\ttest_one \"worktree\" \"$ROOT/.git\" --path-format=absolute --git-common-dir &&\n+\ttest_one \".\" \"$ROOT\" --path-format=absolute --show-toplevel &&\n+\ttest_one \".\" \"$ROOT/.git/objects\" --path-format=absolute --git-path objects &&\n+\ttest_one \".\" \"$ROOT/.git/objects/foo/bar/baz\" --path-format=absolute --git-path objects/foo/bar/baz\n+'\n+\n+test_expect_success 'rev-parse --path-format=relative' '\n+\ttest_one \".\" \".git\" --path-format=relative --git-dir &&\n+\ttest_one \".\" \".git\" --path-format=relative --git-common-dir &&\n+\ttest_one \"sub/dir\" \"../../.git\" --path-format=relative --git-dir &&\n+\ttest_one \"sub/dir\" \"../../.git\" --path-format=relative --git-common-dir &&\n+\ttest_one \"worktree\" \"../.git/worktrees/worktree\" --path-format=relative --git-dir &&\n+\ttest_one \"worktree\" \"../.git\" --path-format=relative --git-common-dir &&\n+\ttest_one \".\" \"./\" --path-format=relative --show-toplevel &&\n+\ttest_one \".\" \".git/objects\" --path-format=relative --git-path objects &&\n+\ttest_one \".\" \".git/objects/foo/bar/baz\" --path-format=relative --git-path objects/foo/bar/baz\n+'\n+\n+test_expect_success '--path-format=relative does not affect --absolute-git-dir' '\n+\tgit rev-parse --path-format=relative --absolute-git-dir >actual &&\n+\techo \"$ROOT/.git\" >expect &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success '--path-format can change in the middle of the command line' '\n+\tgit rev-parse --path-format=absolute --git-dir --path-format=relative --git-path objects/foo/bar >actual &&\n+\tcat >expect <<-EOF &&\n+\t$ROOT/.git\n+\t.git/objects/foo/bar\n+\tEOF\n+\ttest_cmp expect actual\n+'\n+\n test_expect_success 'git-common-dir from worktree root' '\n \techo .git >expect &&\n \tgit rev-parse --git-common-dir >actual &&\n"},{"id":"412125","messageId":"20201213002529.542928-1-sandals@crustytoothpaste.net","threadId":"54815","inReplyTo":null,"subject":"[PATCH v5 0/2] rev-parse options for absolute or relative paths","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2020-12-13T00:25:27Z","receivedAt":"2020-12-13T00:27:49Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"There are a bunch of different situations in which one would like to\nhave an absolute and canonical or a relative path from Git.  In many of\nthese cases, these values are already available from git rev-parse, but\nsome values only come in one form or another.\n\nChanges from v4:\n\n* Wrap an internal function.\n* Use bitflags.\n* Fix memory leaks.\n* Explain that --path-format can be specified multiple times and its\n  effects if that is done.\n* Expand commit message.\n\nChanges from v3:\n\n* Switch to a binary option instead of a number of missing path\n  components to skip.\n\nChanges from v2:\n\n* Incorporate multiple missing segment support into the strbuf_realpath\n  code.\n* Switch some invocations to use DEFAULT_UNMODIFIED, which should not\n  result in a change in behavior.\n* Rebase, resolving some conflicts.\n\nChanges from v1:\n\n* Add a function to handle missing trailing components when\n  canonicalizing paths and use it.\n* Improve commit messages.\n* Fix broken && chain.\n* Fix situation where relative paths are not relative.\n\nbrian m. carlson (2):\n  abspath: add a function to resolve paths with missing components\n  rev-parse: add option for absolute or relative path formatting\n\n Documentation/git-rev-parse.txt |  74 +++++++++++++---------\n abspath.c                       |  64 +++++++++++++------\n builtin/rev-parse.c             | 106 ++++++++++++++++++++++++++++----\n cache.h                         |   2 +\n t/t1500-rev-parse.sh            |  57 ++++++++++++++++-\n 5 files changed, 242 insertions(+), 61 deletions(-)\n\n"},{"id":"412126","messageId":"20201213002529.542928-2-sandals@crustytoothpaste.net","threadId":"54815","inReplyTo":"20201213002529.542928-1-sandals@crustytoothpaste.net","subject":"[PATCH v5 1/2] abspath: add a function to resolve paths with missing components","fromName":"brian m. carlson","fromEmail":"sandals@crustytoothpaste.net","sentAt":"2020-12-13T00:25:28Z","receivedAt":"2020-12-13T00:27:49Z","isPatch":true,"sender":{"key":"sandals@crustytoothpaste.net","avatar":"https://avatars.githubusercontent.com/u/497054?v=4"},"body":"Currently, we have a function to resolve paths, strbuf_realpath.  This\nfunction canonicalizes paths like realpath(3), but permits a trailing\ncomponent to be absent from the file system.  In other words, this is\nthe behavior of the GNU realpath(1) without any arguments.\n\nIn the future, we'll need this same behavior, except that we want to\nallow for any number of missing trailing components, which is the\nbehavior of GNU realpath(1) with the -m option.  This is useful because\nwe'll want to canonicalize a path that may point to a not yet present\npath under the .git directory.  For example, a user may want to know\nwhere an arbitrary ref would be stored if it existed in the file system.\n\nLet's refactor strbuf_realpath to move most of the code to an internal\nfunction and then pass it two flags to control its behavior.  We'll add\na strbuf_realpath_forgiving function that has our new behavior, and\nleave strbuf_realpath with the older, stricter behavior.\n\nSigned-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n---\n abspath.c | 64 +++++++++++++++++++++++++++++++++++++++----------------\n cache.h   |  2 ++\n 2 files changed, 48 insertions(+), 18 deletions(-)\n\ndiff --git a/abspath.c b/abspath.c\nindex 6f15a418bb..39e06b5848 100644\n--- a/abspath.c\n+++ b/abspath.c\n@@ -67,19 +67,15 @@ static void get_root_part(struct strbuf *resolved, struct strbuf *remaining)\n #endif\n \n /*\n- * Return the real path (i.e., absolute path, with symlinks resolved\n- * and extra slashes removed) equivalent to the specified path.  (If\n- * you want an absolute path but don't mind links, use\n- * absolute_path().)  Places the resolved realpath in the provided strbuf.\n- *\n- * The directory part of path (i.e., everything up to the last\n- * dir_sep) must denote a valid, existing directory, but the last\n- * component need not exist.  If die_on_error is set, then die with an\n- * informative error message if there is a problem.  Otherwise, return\n- * NULL on errors (without generating any output).\n+ * If set, any number of trailing components may be missing; otherwise, only one\n+ * may be.\n  */\n-char *strbuf_realpath(struct strbuf *resolved, const char *path,\n-\t\t      int die_on_error)\n+#define REALPATH_MANY_MISSING (1 << 0)\n+/* Should we die if there's an error? */\n+#define REALPATH_DIE_ON_ERROR (1 << 1)\n+\n+static char *strbuf_realpath_1(struct strbuf *resolved, const char *path,\n+\t\t\t       int flags)\n {\n \tstruct strbuf remaining = STRBUF_INIT;\n \tstruct strbuf next = STRBUF_INIT;\n@@ -89,7 +85,7 @@ char *strbuf_realpath(struct strbuf *resolved, const char *path,\n \tstruct stat st;\n \n \tif (!*path) {\n-\t\tif (die_on_error)\n+\t\tif (flags & REALPATH_DIE_ON_ERROR)\n \t\t\tdie(\"The empty string is not a valid path\");\n \t\telse\n \t\t\tgoto error_out;\n@@ -101,7 +97,7 @@ char *strbuf_realpath(struct strbuf *resolved, const char *path,\n \tif (!resolved->len) {\n \t\t/* relative path; can use CWD as the initial resolved path */\n \t\tif (strbuf_getcwd(resolved)) {\n-\t\t\tif (die_on_error)\n+\t\t\tif (flags & REALPATH_DIE_ON_ERROR)\n \t\t\t\tdie_errno(\"unable to get current working directory\");\n \t\t\telse\n \t\t\t\tgoto error_out;\n@@ -129,8 +125,9 @@ char *strbuf_realpath(struct strbuf *resolved, const char *path,\n \n \t\tif (lstat(resolved->buf, &st)) {\n \t\t\t/* error out unless this was the last component */\n-\t\t\tif (errno != ENOENT || remaining.len) {\n-\t\t\t\tif (die_on_error)\n+\t\t\tif (errno != ENOENT ||\n+\t\t\t   (!(flags & REALPATH_MANY_MISSING) && remaining.len)) {\n+\t\t\t\tif (flags & REALPATH_DIE_ON_ERROR)\n \t\t\t\t\tdie_errno(\"Invalid path '%s'\",\n \t\t\t\t\t\t  resolved->buf);\n \t\t\t\telse\n@@ -143,7 +140,7 @@ char *strbuf_realpath(struct strbuf *resolved, const char *path,\n \t\t\tif (num_symlinks++ > MAXSYMLINKS) {\n \t\t\t\terrno = ELOOP;\n \n-\t\t\t\tif (die_on_error)\n+\t\t\t\tif (flags & REALPATH_DIE_ON_ERROR)\n \t\t\t\t\tdie(\"More than %d nested symlinks \"\n \t\t\t\t\t    \"on path '%s'\", MAXSYMLINKS, path);\n \t\t\t\telse\n@@ -153,7 +150,7 @@ char *strbuf_realpath(struct strbuf *resolved, const char *path,\n \t\t\tlen = strbuf_readlink(&symlink, resolved->buf,\n \t\t\t\t\t      st.st_size);\n \t\t\tif (len < 0) {\n-\t\t\t\tif (die_on_error)\n+\t\t\t\tif (flags & REALPATH_DIE_ON_ERROR)\n \t\t\t\t\tdie_errno(\"Invalid symlink '%s'\",\n \t\t\t\t\t\t  resolved->buf);\n \t\t\t\telse\n@@ -202,6 +199,37 @@ char *strbuf_realpath(struct strbuf *resolved, const char *path,\n \treturn retval;\n }\n \n+/*\n+ * Return the real path (i.e., absolute path, with symlinks resolved\n+ * and extra slashes removed) equivalent to the specified path.  (If\n+ * you want an absolute path but don't mind links, use\n+ * absolute_path().)  Places the resolved realpath in the provided strbuf.\n+ *\n+ * The directory part of path (i.e., everything up to the last\n+ * dir_sep) must denote a valid, existing directory, but the last\n+ * component need not exist.  If die_on_error is set, then die with an\n+ * informative error message if there is a problem.  Otherwise, return\n+ * NULL on errors (without generating any output).\n+ */\n+char *strbuf_realpath(struct strbuf *resolved, const char *path,\n+\t\t      int die_on_error)\n+{\n+\treturn strbuf_realpath_1(resolved, path,\n+\t\t\t\t die_on_error ? REALPATH_DIE_ON_ERROR : 0);\n+}\n+\n+/*\n+ * Just like strbuf_realpath, but allows an arbitrary number of path\n+ * components to be missing.\n+ */\n+char *strbuf_realpath_forgiving(struct strbuf *resolved, const char *path,\n+\t\t\t\tint die_on_error)\n+{\n+\treturn strbuf_realpath_1(resolved, path,\n+\t\t\t\t ((die_on_error ? REALPATH_DIE_ON_ERROR : 0) |\n+\t\t\t\t  REALPATH_MANY_MISSING));\n+}\n+\n char *real_pathdup(const char *path, int die_on_error)\n {\n \tstruct strbuf realpath = STRBUF_INIT;\ndiff --git a/cache.h b/cache.h\nindex 8d279bc110..487ebab8b0 100644\n--- a/cache.h\n+++ b/cache.h\n@@ -1325,6 +1325,8 @@ static inline int is_absolute_path(const char *path)\n int is_directory(const char *);\n char *strbuf_realpath(struct strbuf *resolved, const char *path,\n \t\t      int die_on_error);\n+char *strbuf_realpath_forgiving(struct strbuf *resolved, const char *path,\n+\t\t\t\tint die_on_error);\n char *real_pathdup(const char *path, int die_on_error);\n const char *absolute_path(const char *path);\n char *absolute_pathdup(const char *path);\n"},{"id":"412129","messageId":"CAPig+cTFJv8LZWp6Mvi1OXabZ+5NmEw8fu8NeQuwhZDAbnA59Q@mail.gmail.com","threadId":"54815","inReplyTo":"20201213002529.542928-2-sandals@crustytoothpaste.net","subject":"Re: [PATCH v5 1/2] abspath: add a function to resolve paths with missing components","fromName":"Eric Sunshine","fromEmail":"sunshine@sunshineco.com","sentAt":"2020-12-13T02:35:28Z","receivedAt":"2020-12-13T02:36:30Z","isPatch":true,"sender":{"key":"sunshine@sunshineco.com","avatar":"https://avatars.githubusercontent.com/u/163641?v=4"},"body":"On Sat, Dec 12, 2020 at 7:26 PM brian m. carlson\n<sandals@crustytoothpaste.net> wrote:\n> Currently, we have a function to resolve paths, strbuf_realpath.  This\n> function canonicalizes paths like realpath(3), but permits a trailing\n> component to be absent from the file system.  In other words, this is\n> the behavior of the GNU realpath(1) without any arguments.\n>\n> In the future, we'll need this same behavior, except that we want to\n> allow for any number of missing trailing components, which is the\n> behavior of GNU realpath(1) with the -m option.  This is useful because\n> we'll want to canonicalize a path that may point to a not yet present\n> path under the .git directory.  For example, a user may want to know\n> where an arbitrary ref would be stored if it existed in the file system.\n\nThis improved explanation helps the reader better understand why such\nfunctionality is wanted. Thanks.\n\n> Let's refactor strbuf_realpath to move most of the code to an internal\n> function and then pass it two flags to control its behavior.  We'll add\n> a strbuf_realpath_forgiving function that has our new behavior, and\n> leave strbuf_realpath with the older, stricter behavior.\n>\n> Signed-off-by: brian m. carlson <sandals@crustytoothpaste.net>\n> ---\n> diff --git a/abspath.c b/abspath.c\n> +#define REALPATH_MANY_MISSING (1 << 0)\n> +#define REALPATH_DIE_ON_ERROR (1 << 1)\n> +\n> +static char *strbuf_realpath_1(struct strbuf *resolved, const char *path,\n> +                              int flags)\n\nWe normally declare these composed-of-bits \"flags\" arguments as\n`unsigned` rather than `int`.\n\n> @@ -89,7 +85,7 @@ char *strbuf_realpath(struct strbuf *resolved, const char *path,\n>         if (!*path) {\n> -               if (die_on_error)\n> +               if (flags & REALPATH_DIE_ON_ERROR)\n\nNit: If you declare a local variable named `die_on_error`:\n\n    int die_on_error = flags & REALPATH_DIE_ON_ERROR;\n\nthen you won't have to touch the existing five places where this\ncondition is checked, thus making the diff less noisy and the code\n(subjectively) a bit easier to read at a glance. Not at all worth a\nre-roll, though.\n\nAside from these minor comments, this version is a nice improvement\nover the last.\n"}]}