{"thread":{"id":"42528","subject":"[RFC/PATCH] push: deny policy to prevent pushes to unwanted remotes.","startedAt":"2016-06-04T14:51:01Z","lastAt":"2016-06-21T07:46:09Z","messageCount":10,"participants":["Antoine Queru","Remi Galan Alfonso","Matthieu Moy","Junio C Hamano","Lars Schneider"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"288366","messageId":"20160604145101.21928-1-Antoine.Queru@grenoble-inp.org","threadId":"42528","inReplyTo":null,"subject":"[RFC/PATCH] push: deny policy to prevent pushes to unwanted remotes.","fromName":"Antoine Queru","fromEmail":"antoine.queru@grenoble-inp.org","sentAt":"2016-06-04T14:51:01Z","receivedAt":"2016-06-04T14:51:01Z","isPatch":true,"sender":{"key":"antoine.queru@grenoble-inp.org","avatar":null},"body":"Currently, a user wanting to prevent accidental pushes to the wrong\nremote has to create a pre-push hook.  The feature offers a\nconfiguration to allow users to prevent accidental pushes to the wrong\nremote. The user may define a list of whitelisted remotes, a list of\nblacklisted remotes and a default policy (\"allow\" or \"deny\"). A push\nis denied if the remote is explicitely blacklisted or if it isn't\nwhitelisted and the default policy is \"deny\".\n\nThis feature is intended as a safety net more than a real security,\nthe user will always be able to modify the config if he wants to. It\nis here for him to consciously restrict his push possibilities. For\nexample, it may be useful for an unexperimented user fearing to push\nto the wrong remote, or for companies wanting to avoid unintentionnal\nleaking of private code on public repositories.\n\nBy default the whitelist/blacklist feature is disabled since the\ndefault policy is \"allow\".\n\nSigned-off-by: Antoine Queru <antoine.queru@grenoble-inp.org>\nSigned-off-by: Francois Beutin <francois.beutin@grenoble-inp.org>\nSigned-off-by: Matthieu Moy <matthieu.moy@grenoble-inp.fr>\n---\n\nChanges since v1 :\nScheme handled.\nDocumentation fixed and moved.\nOption \"-no-verify\" added to dismiss this process.\n\nRight now, one function \"longest_prefix_size\" is pretty awful because\nurl_normalize treat local path with \"file://...\" as an url but not\nlocal path without this prefix. So a local path and a repo without\nscheme are seen likewise. Forbid local path without \"file://..\" could\nmake the code easier and faster. What do you think ?\n\nUser, password and port are not treated. Should it be ?\n\n Documentation/config.txt            |  17 ++++++\n Documentation/urls-remotes.txt      |  55 ++++++++++++++++++\n builtin/push.c                      | 110 ++++++++++++++++++++++++++++++++++--\n t/t5544-push-whitelist-blacklist.sh |  51 +++++++++++++++++\n 4 files changed, 228 insertions(+), 5 deletions(-)\n create mode 100755 t/t5544-push-whitelist-blacklist.sh\n\ndiff --git a/Documentation/config.txt b/Documentation/config.txt\nindex 53f00db..7fe88f5 100644\n--- a/Documentation/config.txt\n+++ b/Documentation/config.txt\n@@ -2517,6 +2517,23 @@ remote.pushDefault::\n \t`branch.<name>.remote` for all branches, and is overridden by\n \t`branch.<name>.pushRemote` for specific branches.\n \n+remote.pushBlacklist::\n+\tThe list of remotes the user is forbidden to push to.\n+\tSee linkgit:git-push[1]\n+\n+remote.pushWhitelist::\n+\tThe list of remotes the user is allowed to push to.\n+\tSee linkgit:git-push[1]\n+\n+remote.pushDefaultPolicy::\n+\tCan be set to either 'allow' or 'deny', defines the policy to\n+\tadopt when the user tries to push to a remote not in the\n+\twhitelist or the blacklist. Defaults to 'allow'.\n+\n+remote.pushDenyMessage::\n+\tThe message printed when pushing to a forbidden remote.\n+\tDefaults to \"Pushing to this remote has been forbidden\".\n+\n remote.<name>.url::\n \tThe URL of a remote repository.  See linkgit:git-fetch[1] or\n \tlinkgit:git-push[1].\ndiff --git a/Documentation/urls-remotes.txt b/Documentation/urls-remotes.txt\nindex bd184cd..705914d 100644\n--- a/Documentation/urls-remotes.txt\n+++ b/Documentation/urls-remotes.txt\n@@ -89,6 +89,61 @@ git push uses:\n \tHEAD:refs/heads/<head>\n ------------\n \n+Denying pushes to the wrong remotes\n+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n+\n+If you fear accidental pushes to the wrong remotes, you can use the\n+blacklist/whitelist feature. The goal is to catch pushes to unwanted\n+remotes before they happen.\n+\n+The simplest way to forbid pushes to a remote is to add its url in the\n+config file under the 'remote.pushBlacklist' variable.  A more\n+restrictive way is to change 'remote.pushDefaultPolicy' to 'deny',\n+thus denying pushes to every remote not whitelisted. You can then add\n+the right remotes under 'remote.pushWhitelist'.\n+\n+You can also configure more advanced acceptation/denial behavior\n+following this rule: the more the url in the config prefixes the asked\n+url the more priority it has.\n+\n+For example, if we set up the configuration variables like this:\n+\n+-------------------------------\n+git config --add remote.pushBlacklist repository.com\n+git config --add remote.pushWhitelist repository.com/Special_Folder\n+-------------------------------\n+\n+Pushes like this  will be accepted:\n+-------------------------------\n+git push repository.com/Special_Path/*\n+-------------------------------\n+\n+While this one for example will be denied:\n+-------------------------------\n+git push repository.com/Other_Path/\n+-------------------------------\n+\n+Remember to use '--add' with 'git config' to add more than one\n+'remote.pushBlacklist'.\n+\n+Specific schemes can also be denied. For example :\n+\n+-------------------------------\n+git config --add remote.pushBlacklist http://repo.com\n+-------------------------------\n+\n+By doing so, only pushes to repo.com using 'http' will be denied,\n+whereas other will be allowed, like 'https'. If you want to deny\n+all pushes to a certain repo, don't put scheme at the beginning.\n+\n+Additionally, you can configure the message printed when a push is\n+denied with the 'remote.pushDenyMessage' configuration variable.\n+\n+You can dismiss this process by using '--no-verify'.\n+\n+An error will be raised if the url is blacklisted and whitelisted at\n+the same time.\n+\n \n \n \ndiff --git a/builtin/push.c b/builtin/push.c\nindex 4e9e4db..bf20257 100644\n--- a/builtin/push.c\n+++ b/builtin/push.c\n@@ -11,6 +11,8 @@\n #include \"submodule.h\"\n #include \"submodule-config.h\"\n #include \"send-pack.h\"\n+#include \"urlmatch.h\"\n+#include \"url.h\"\n \n static const char * const push_usage[] = {\n \tN_(\"git push [<options>] [<repository> [<refspec>...]]\"),\n@@ -353,6 +355,101 @@ static int push_with_options(struct transport *transport, int flags)\n \treturn 1;\n }\n \n+static void check_length_prefix(const int whitelist,\n+\t\t\t\tconst int blacklist,\n+\t\t\t\tconst char *repo,\n+\t\t\t\tconst char *deny_message,\n+\t\t\t\tconst char *default_policy)\n+{\n+\tif (whitelist < blacklist)\n+\t\tdie(_(\"%s\"), deny_message);\n+\tif (whitelist == blacklist) {\n+\t\tif (whitelist)\n+\t\t\tdie(_(\"%s cannot be whitelisted and blacklisted\"\n+\t\t\t      \"at the same time\"), repo);\n+\t\tif (!strcmp(default_policy, \"deny\"))\n+\t\t\tdie(_(\"%s\"), deny_message);\n+\t\tif (strcmp(default_policy, \"allow\"))\n+\t\t\tdie(_(\"Unrecognized value for remote.pushDefaultPolicy,\"\n+\t\t\t      \"should be 'allow' or 'deny'\"));\n+\t}\n+\n+}\n+\n+static void find_longest_prefix(const char *str, const char *pre, int *max)\n+{\n+\tif (starts_with(str, pre)) {\n+\t\tint tmp = strlen(pre);\n+\t\tif (*max < tmp)\n+\t\t\t*max = tmp;\n+\t}\n+}\n+\n+/*\n+ *NEEDSWORK: Ugly because file://... is recognized as an url, and we\n+ *may want to compare it to local path without this scheme. Forcing\n+ *the user to put file:// before every local path would make the code\n+ *easier and avoid confusion with a distant repo like 'github.com'\n+ *which is not an url.\n+ */\n+static int longest_prefix_size(const char* target_str,\n+\t\t\t\tconst struct string_list *list)\n+{\n+\tstruct string_list_item *curr_item;\n+\tstruct url_info target_url;\n+\tint max_size = 0;\n+\tif (!list)\n+\t\treturn 0;\n+\n+\tskip_prefix(target_str, \"file://\", &target_str);\n+\turl_normalize(target_str, &target_url);\n+\n+\tfor_each_string_list_item(curr_item, list) {\n+\t\tstruct url_info curr_url;\n+\t\tconst char *curr_str = curr_item->string;\n+\t\tskip_prefix(curr_str, \"file://\", &curr_str);\n+\t\turl_normalize(curr_str, &curr_url);\n+\t\tif (target_url.url &&\n+\t\t    curr_url.url &&\n+\t\t    target_url.scheme_len == curr_url.scheme_len &&\n+\t\t    !strncmp(target_url.url,curr_url.url,curr_url.scheme_len))\n+\t\t\tfind_longest_prefix(target_url.url + target_url.host_off,\n+\t\t\t\t\t    curr_url.url + curr_url.host_off,\n+\t\t\t\t\t    &max_size);\n+\t\telse if (target_url.url && !curr_url.url)\n+\t\t\tfind_longest_prefix(target_url.url + target_url.host_off,\n+\t\t\t\t\t    curr_str,\n+\t\t\t\t\t    &max_size);\n+\n+\t\telse if(!target_url.url && !curr_url.url)\n+\t\t\tfind_longest_prefix(target_str, curr_str, &max_size);\n+\t}\n+\treturn max_size;\n+}\n+\n+static void block_unauthorized_url(const char *repo)\n+{\n+\tstruct url_info target_url;\n+\tconst char *default_policy, *deny_message;\n+\tconst struct string_list *whitelist, *blacklist;\n+\tint whitelist_size, blacklist_size;\n+\n+\turl_normalize(repo, &target_url);\n+\n+\tif (git_config_get_value(\"remote.pushDefaultPolicy\", &default_policy))\n+\t\tdefault_policy = \"allow\";\n+\tif (git_config_get_value(\"remote.pushDenyMessage\", &deny_message))\n+\t\tdeny_message = \"Pushing to this remote using this protocol is forbidden\";\n+\n+\twhitelist = git_config_get_value_multi(\"remote.pushWhitelist\");\n+\tblacklist = git_config_get_value_multi(\"remote.pushBlacklist\");\n+\n+\twhitelist_size = longest_prefix_size(repo, whitelist);\n+\tblacklist_size = longest_prefix_size(repo, blacklist);\n+\n+\tcheck_length_prefix(whitelist_size, blacklist_size, repo, deny_message, default_policy);\n+}\n+\n static int do_push(const char *repo, int flags)\n {\n \tint i, errs;\n@@ -404,15 +501,18 @@ static int do_push(const char *repo, int flags)\n \turl_nr = push_url_of_remote(remote, &url);\n \tif (url_nr) {\n \t\tfor (i = 0; i < url_nr; i++) {\n-\t\t\tstruct transport *transport =\n-\t\t\t\ttransport_get(remote, url[i]);\n+\t\t\tstruct transport *transport;\n+\t\t\tif (!(flags & TRANSPORT_PUSH_NO_HOOK))\n+\t\t\t\tblock_unauthorized_url(url[i]);\n+\t\t\ttransport = transport_get(remote, url[i]);\n \t\t\tif (push_with_options(transport, flags))\n \t\t\t\terrs++;\n \t\t}\n \t} else {\n-\t\tstruct transport *transport =\n-\t\t\ttransport_get(remote, NULL);\n-\n+\t\tstruct transport *transport;\n+\t\tif (!(flags & TRANSPORT_PUSH_NO_HOOK))\n+\t\t\tblock_unauthorized_url(remote->url[0]);\n+\t\ttransport = transport_get(remote, NULL);\n \t\tif (push_with_options(transport, flags))\n \t\t\terrs++;\n \t}\ndiff --git a/t/t5544-push-whitelist-blacklist.sh b/t/t5544-push-whitelist-blacklist.sh\nnew file mode 100755\nindex 0000000..8f9f056\n--- /dev/null\n+++ b/t/t5544-push-whitelist-blacklist.sh\n@@ -0,0 +1,51 @@\n+#!/bin/sh\n+\n+test_description='blacklist for push'\n+\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' '\n+\tgit init --bare blacklist/ &&\n+\tgit init --bare whitelist/ &&\n+\tgit init --bare blacklist/allow &&\n+\ttest_commit commit &&\n+\techo \"fatal: Pushing to this remote using this protocol is forbidden\" > forbidden\n+'\n+\n+test_expect_success 'basic case' '\n+\tgit config --add remote.pushBlacklist http://blacklist.com &&\n+\ttest_must_fail git push http://blacklist.com HEAD 2> result &&\n+\ttest_cmp result forbidden\n+'\n+\n+test_expect_success 'no scheme and no path' '\n+\tgit config remote.pushBlacklist blacklist.com &&\n+\ttest_must_fail git push http://blacklist.com/foo HEAD 2> result &&\n+\ttest_cmp result forbidden\n+'\n+\n+test_expect_success 'local path' '\n+\tgit config remote.pushBlacklist blacklist &&\n+\ttest_must_fail git push blacklist HEAD 2> result &&\n+\ttest_cmp result forbidden\n+'\n+\n+test_expect_success 'local path with file://' '\n+\tgit config remote.pushBlacklist file://blacklist &&\n+\ttest_must_fail git push blacklist HEAD 2> result &&\n+\ttest_cmp result forbidden\n+'\n+test_expect_success 'only one scheme allowed' '\n+\tgit config remote.pushDefaultPolicy deny &&\n+\tgit config remote.pushWhitelist http://blacklist.com &&\n+\ttest_must_fail git push https://blacklist.com HEAD 2> result &&\n+\ttest_cmp result forbidden\n+'\n+\n+test_expect_success 'denied repo in allowed repo' '\n+\tgit config remote.pushBlacklist blacklist &&\n+\tgit config --add remote.pushWhitelist blacklist/allow &&\n+\tgit push blacklist/allow HEAD\n+'\n+\n+test_done\n-- \n2.8.2.625.g2f87638.dirty\n"},{"id":"288423","messageId":"998280064.353519.1465213163063.JavaMail.zimbra@ensimag.grenoble-inp.fr","threadId":"42528","inReplyTo":"20160604145101.21928-1-Antoine.Queru@grenoble-inp.org","subject":"Re: [RFC/PATCH] push: deny policy to prevent pushes to unwanted remotes.","fromName":"Remi Galan Alfonso","fromEmail":"remi.galan-alfonso@ensimag.grenoble-inp.fr","sentAt":"2016-06-06T11:39:23Z","receivedAt":"2016-06-06T11:39:23Z","isPatch":true,"sender":{"key":"remi.galan-alfonso@ensimag.grenoble-inp.fr","avatar":"https://avatars.githubusercontent.com/u/12509162?v=4"},"body":"Hi Antoine,\n\nAntoine Queru <Antoine.Queru@grenoble-inp.org> writes:\n> [...]\n> +For example, if we set up the configuration variables like this:\n> +\n> +-------------------------------\n> +git config --add remote.pushBlacklist repository.com\n> +git config --add remote.pushWhitelist repository.com/Special_Folder\n> +-------------------------------\n> +\n> +Pushes like this  will be accepted:\n> +-------------------------------\n> +git push repository.com/Special_Path/*\n> +-------------------------------\n\nAccording to your previous `git config`, it should be:\n\tgit push repository.com/Special_Folder/*\n\n> +\n> +While this one for example will be denied:\n> +-------------------------------\n> +git push repository.com/Other_Path/\n> +-------------------------------\n> +\n> [...]\n> +/*\n> + *NEEDSWORK: Ugly because file://... is recognized as an url, and we\n> + *may want to compare it to local path without this scheme. Forcing\n> + *the user to put file:// before every local path would make the code\n> + *easier and avoid confusion with a distant repo like 'github.com'\n> + *which is not an url.\n> + */\n\nStyle: space after '*' (when there is text after), meaning:\n\t/*\n\t * NEEDSWORK: Ugly because file://... is recognized [...]\n\t * [...]\n\t */\n\n> +static int longest_prefix_size(const char* target_str,\n> +                                const struct string_list *list)\n\nThat might just be my mailer but this line is not properly lined up\nwith the previous one (one space too much).\nIt should be:\nstatic int longest_prefix_size(const char* target_str,\n\t\t\t       const struct string_list *list)\n\n> [...]\n> +        for_each_string_list_item(curr_item, list) {\n> +                struct url_info curr_url;\n> +                const char *curr_str = curr_item->string;\n> +                skip_prefix(curr_str, \"file://\", &curr_str);\n> +                url_normalize(curr_str, &curr_url);\n> +                if (target_url.url &&\n> +                    curr_url.url &&\n\nYou can put target_url.url and curr_url.url on the same line.\n\n> +                    target_url.scheme_len == curr_url.scheme_len &&\n> +                    !strncmp(target_url.url,curr_url.url,curr_url.scheme_len))\n\nStyle: space after ','.\n\nWith those two things, the condition would look like this:\n\n\t\tif (target_url.url && curr_url.url &&\n\t\t    target_url.scheme_len == curr_url.scheme_len &&\n\t\t    !strncmp(target_url.url, curr_url.url, curr_url.scheme_len))\n\n> [...]\n> +        whitelist_size = longest_prefix_size(repo, whitelist);\n> +        blacklist_size = longest_prefix_size(repo, blacklist);\n> +\n> +        check_length_prefix(whitelist_size, blacklist_size, repo, deny_message, default_policy);\n\nThis line is above 80 characters, so:\n\n\tcheck_length_prefix(whitelist_size, blacklist_size, repo, deny_message,\n\t\t\t    default_policy);\n\n> [...]\n> +test_expect_success 'setup' '\n> +        git init --bare blacklist/ &&\n> +        git init --bare whitelist/ &&\n> +        git init --bare blacklist/allow &&\n> +        test_commit commit &&\n> +        echo \"fatal: Pushing to this remote using this protocol is forbidden\" > forbidden\n> +'\n> +\n> +test_expect_success 'basic case' '\n> +        git config --add remote.pushBlacklist http://blacklist.com &&\n\nYou use `git config` instead of `test_config`, meaning that the\nconfiguration you introduce will persist after the test.\n\nIt is not really a problem here since for the other tests you don't\nuse `git config --add` so the configuration will be\noverwritten. However I still think you should use `test_config` to\navoid causing trouble to potential future tests that would use\n`--add` and expect a clean state.\n\n> [...]\n> +test_expect_success 'local path with file://' '\n> +        git config remote.pushBlacklist file://blacklist &&\n> +        test_must_fail git push blacklist HEAD 2> result &&\n> +        test_cmp result forbidden\n> +'\n\n(you forgot a new line here)\n\n> +test_expect_success 'only one scheme allowed' '\n> +        git config remote.pushDefaultPolicy deny &&\n> +        git config remote.pushWhitelist http://blacklist.com &&\n> +        test_must_fail git push https://blacklist.com HEAD 2> result &&\n> +        test_cmp result forbidden\n> +'\n> +\n> +test_expect_success 'denied repo in allowed repo' '\n\n'allowed repo in denied remote'? In any case the current title is\nmisleading for me.\n\n> +        git config remote.pushBlacklist blacklist &&\n> +        git config --add remote.pushWhitelist blacklist/allow &&\n> +        git push blacklist/allow HEAD\n> +'\n\nThanks,\nRémi\n"},{"id":"288428","messageId":"2104102670.380496.1465220639026.JavaMail.zimbra@ensimag.grenoble-inp.fr","threadId":"42528","inReplyTo":"998280064.353519.1465213163063.JavaMail.zimbra@ensimag.grenoble-inp.fr","subject":"Re: [RFC/PATCH] push: deny policy to prevent pushes to unwanted remotes.","fromName":"Antoine Queru","fromEmail":"antoine.queru@ensimag.grenoble-inp.fr","sentAt":"2016-06-06T13:43:59Z","receivedAt":"2016-06-06T13:43:59Z","isPatch":true,"sender":{"key":"antoine.queru@ensimag.grenoble-inp.fr","avatar":null},"body":"\n\n----- Mail original -----\n> Hi Antoine,\n> \n> Antoine Queru <Antoine.Queru@grenoble-inp.org> writes:\n> > [...]\n> > +For example, if we set up the configuration variables like this:\n> > +\n> > +-------------------------------\n> > +git config --add remote.pushBlacklist repository.com\n> > +git config --add remote.pushWhitelist repository.com/Special_Folder\n> > +-------------------------------\n> > +\n> > +Pushes like this  will be accepted:\n> > +-------------------------------\n> > +git push repository.com/Special_Path/*\n> > +-------------------------------\n> \n> According to your previous `git config`, it should be:\n> \tgit push repository.com/Special_Folder/*\n> \n> > +\n> > +While this one for example will be denied:\n> > +-------------------------------\n> > +git push repository.com/Other_Path/\n> > +-------------------------------\n> > +\n> > [...]\n> > +/*\n> > + *NEEDSWORK: Ugly because file://... is recognized as an url, and we\n> > + *may want to compare it to local path without this scheme. Forcing\n> > + *the user to put file:// before every local path would make the code\n> > + *easier and avoid confusion with a distant repo like 'github.com'\n> > + *which is not an url.\n> > + */\n> \n> Style: space after '*' (when there is text after), meaning:\n> \t/*\n> \t * NEEDSWORK: Ugly because file://... is recognized [...]\n> \t * [...]\n> \t */\n> \n> > +static int longest_prefix_size(const char* target_str,\n> > +                                const struct string_list *list)\n> \n> That might just be my mailer but this line is not properly lined up\n> with the previous one (one space too much).\n> It should be:\n> static int longest_prefix_size(const char* target_str,\n> \t\t\t       const struct string_list *list)\n> \n> > [...]\n> > +        for_each_string_list_item(curr_item, list) {\n> > +                struct url_info curr_url;\n> > +                const char *curr_str = curr_item->string;\n> > +                skip_prefix(curr_str, \"file://\", &curr_str);\n> > +                url_normalize(curr_str, &curr_url);\n> > +                if (target_url.url &&\n> > +                    curr_url.url &&\n> \n> You can put target_url.url and curr_url.url on the same line.\n> \n> > +                    target_url.scheme_len == curr_url.scheme_len &&\n> > +\n> > !strncmp(target_url.url,curr_url.url,curr_url.scheme_len))\n> \n> Style: space after ','.\n> \n> With those two things, the condition would look like this:\n> \n> \t\tif (target_url.url && curr_url.url &&\n> \t\t    target_url.scheme_len == curr_url.scheme_len &&\n> \t\t    !strncmp(target_url.url, curr_url.url, curr_url.scheme_len))\n> \n> > [...]\n> > +        whitelist_size = longest_prefix_size(repo, whitelist);\n> > +        blacklist_size = longest_prefix_size(repo, blacklist);\n> > +\n> > +        check_length_prefix(whitelist_size, blacklist_size, repo,\n> > deny_message, default_policy);\n> \n> This line is above 80 characters, so:\n> \n> \tcheck_length_prefix(whitelist_size, blacklist_size, repo, deny_message,\n> \t\t\t    default_policy);\n> \n> > [...]\n> > +test_expect_success 'setup' '\n> > +        git init --bare blacklist/ &&\n> > +        git init --bare whitelist/ &&\n> > +        git init --bare blacklist/allow &&\n> > +        test_commit commit &&\n> > +        echo \"fatal: Pushing to this remote using this protocol is\n> > forbidden\" > forbidden\n> > +'\n> > +\n> > +test_expect_success 'basic case' '\n> > +        git config --add remote.pushBlacklist http://blacklist.com &&\n> \n> You use `git config` instead of `test_config`, meaning that the\n> configuration you introduce will persist after the test.\n> \n> It is not really a problem here since for the other tests you don't\n> use `git config --add` so the configuration will be\n> overwritten. However I still think you should use `test_config` to\n> avoid causing trouble to potential future tests that would use\n> `--add` and expect a clean state.\n> \n> > [...]\n> > +test_expect_success 'local path with file://' '\n> > +        git config remote.pushBlacklist file://blacklist &&\n> > +        test_must_fail git push blacklist HEAD 2> result &&\n> > +        test_cmp result forbidden\n> > +'\n> \n> (you forgot a new line here)\n> \n> > +test_expect_success 'only one scheme allowed' '\n> > +        git config remote.pushDefaultPolicy deny &&\n> > +        git config remote.pushWhitelist http://blacklist.com &&\n> > +        test_must_fail git push https://blacklist.com HEAD 2> result &&\n> > +        test_cmp result forbidden\n> > +'\n> > +\n> > +test_expect_success 'denied repo in allowed repo' '\n> \n> 'allowed repo in denied remote'? In any case the current title is\n> misleading for me.\n> \n> > +        git config remote.pushBlacklist blacklist &&\n> > +        git config --add remote.pushWhitelist blacklist/allow &&\n> > +        git push blacklist/allow HEAD\n> > +'\n> \n> Thanks,\n> Rémi\n> \n\nHello Rémi, thanks you for your input ! I'll make the appropriate changes \nand send a new version as soon as i can !\n"},{"id":"288441","messageId":"vpq37oqjqgn.fsf@anie.imag.fr","threadId":"42528","inReplyTo":"20160604145101.21928-1-Antoine.Queru@grenoble-inp.org","subject":"Re: [RFC/PATCH] push: deny policy to prevent pushes to unwanted remotes.","fromName":"Matthieu Moy","fromEmail":"matthieu.moy@grenoble-inp.fr","sentAt":"2016-06-06T15:18:32Z","receivedAt":"2016-06-06T15:18:32Z","isPatch":true,"sender":{"key":"matthieu.moy@grenoble-inp.fr","avatar":"https://gravatar.com/avatar/72c8a2705971a25dfaff23cece15130d405685845d911aedd5667ace277f3fc5?d=mp&s=160"},"body":"Antoine Queru <Antoine.Queru@grenoble-inp.org> writes:\n\n> Currently, a user wanting to prevent accidental pushes to the wrong\n> remote has to create a pre-push hook.  The feature\n\nIt's not clear what \"The feature\" refers to. Given the context, I read\nit as \"pre-push hook\", but I think this is not what you meant.\n\n> User, password and port are not treated. Should it be ?\n\nPlease elaborate on \"not treated\". What happens if github.com is\nblacklisted by the user pushes to http://me@github.com ?\n\n>  Documentation/config.txt            |  17 ++++++\n>  Documentation/urls-remotes.txt      |  55 ++++++++++++++++++\n>  builtin/push.c                      | 110 ++++++++++++++++++++++++++++++++++--\n>  t/t5544-push-whitelist-blacklist.sh |  51 +++++++++++++++++\n>  4 files changed, 228 insertions(+), 5 deletions(-)\n>  create mode 100755 t/t5544-push-whitelist-blacklist.sh\n>\n> diff --git a/Documentation/config.txt b/Documentation/config.txt\n> index 53f00db..7fe88f5 100644\n> --- a/Documentation/config.txt\n> +++ b/Documentation/config.txt\n> @@ -2517,6 +2517,23 @@ remote.pushDefault::\n>  \t`branch.<name>.remote` for all branches, and is overridden by\n>  \t`branch.<name>.pushRemote` for specific branches.\n>  \n> +remote.pushBlacklist::\n> +\tThe list of remotes the user is forbidden to push to.\n> +\tSee linkgit:git-push[1]\n\nThis is only true when remote.pushDefaultPolicy is \"allow\". It makes\nsense to say it explicitly here, if only to have a pointer to\npushDefaultPolicy.\n\n\tThe list of remotes the user is forbidden to push to when\n\t`remote.pushDefaultPolicy` is 'allow'\n\n?\n\nThis doesn't document the \"longest prefix wins\", so the complete\nsentence could be\n\n\tThe list of remotes the user is forbidden to push to when\n\t`remote.pushDefaultPolicy` is 'allow', or the list of exceptions\n\tto `remote.pushWhitelist` when it is 'deny'.\n\nOr perhaps it's just overkill.\n\n> +remote.pushWhitelist::\n> +\tThe list of remotes the user is allowed to push to.\n> +\tSee linkgit:git-push[1]\n\nLikewise.\n\n> +remote.pushDenyMessage::\n> +\tThe message printed when pushing to a forbidden remote.\n> +\tDefaults to \"Pushing to this remote has been forbidden\".\n\nPerhaps add something like \"Please check your Git configuration files\nfor details\" to give a hint to the user about what's going on? (and give\na chance to read the comment next to the configuration hopefully)\n\n>  remote.<name>.url::\n>  \tThe URL of a remote repository.  See linkgit:git-fetch[1] or\n>  \tlinkgit:git-push[1].\n> diff --git a/Documentation/urls-remotes.txt b/Documentation/urls-remotes.txt\n> index bd184cd..705914d 100644\n> --- a/Documentation/urls-remotes.txt\n> +++ b/Documentation/urls-remotes.txt\n> @@ -89,6 +89,61 @@ git push uses:\n>  \tHEAD:refs/heads/<head>\n>  ------------\n>  \n> +Denying pushes to the wrong remotes\n> +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n2 '~' missing above.\n\n> +The simplest way to forbid pushes to a remote is to add its url in the\n> +config file under the 'remote.pushBlacklist' variable.  A more\n> +restrictive way is to change 'remote.pushDefaultPolicy' to 'deny',\n> +thus denying pushes to every remote not whitelisted. You can then add\n> +the right remotes under 'remote.pushWhitelist'.\n\nA few meters from you are people working on guidelines for formatting of\nconfiguration variables. I guess they read this patch already but\nthey're too angry to tell you ;-).\n\n> +You can also configure more advanced acceptation/denial\n\nA native may confirm/infirm, but it seems \"acceptation\" is essentially\nthe word french people use to mean \"acceptance\".\n\n> +Pushes like this  will be accepted:\n> +-------------------------------\n> +git push repository.com/Special_Path/*\n> +-------------------------------\n\nThe '*' is misleading to me. As-is, it's a shell metacaracter that would\nbe expanded locally by my shell. I guess you don't mean it like this,\nbut rather \"type anything here\". Since you're already giving a concrete\nexample, I'd make it completely concrete like\n\ngit push repository.com/Special_Path/subdirectory\n\n> +While this one for example will be denied:\n> +-------------------------------\n> +git push repository.com/Other_Path/\n> +-------------------------------\n> +\n> +Remember to use '--add' with 'git config' to add more than one\n> +'remote.pushBlacklist'.\n\nI'd write:\n\n`remote.pushBlacklist` and `remote.pushWhitelist` are multi-valued\nvariables, hence you can more than one element using `git config --add`.\n\n\nNow, a real question about the behavior: what happens when multiple\npatterns match on the same side (white or black), like\n\npushBlacklist = example.com\npushBlacklist = example.com/a/b\npushWhitelist = example.com/a\n\n?\n\nI think this deserves a test.\n\n> +Specific schemes can also be denied. For example :\n\nNo space before :.\n\n> +-------------------------------\n> +git config --add remote.pushBlacklist http://repo.com\n> +-------------------------------\n\nAvoid using real domain names (or domain names that could become real)\nin examples. example.com is made precisely for this (rfc2606).\n\n> +By doing so, only pushes to repo.com using 'http' will be denied,\n\n`...` around `repo.com`.\n\n> +static void check_length_prefix(const int whitelist,\n> +\t\t\t\tconst int blacklist,\n\nOn the caller side, these are called *_size, and you dropped the _size\npart here. I find it much less readable.\n\n> +static void find_longest_prefix(const char *str, const char *pre, int *max)\n> +{\n> +\tif (starts_with(str, pre)) {\n> +\t\tint tmp = strlen(pre);\n> +\t\tif (*max < tmp)\n> +\t\t\t*max = tmp;\n> +\t}\n> +}\n\n>From the name of the function, I'd have expected it to find the longest\ncommon prefix of two strings or so, but it doesn't do that at all.\nPlease find a better name.\n\n> +/*\n> + *NEEDSWORK: Ugly because file://... is recognized as an url, and we\n> + *may want to compare it to local path without this scheme. Forcing\n> + *the user to put file:// before every local path would make the code\n> + *easier and avoid confusion with a distant repo like 'github.com'\n> + *which is not an url.\n> + */\n\nNot sure what you mean. In the text above, which instance refers to \"the\npath the user is pushing to\" and which one is \"the path that appears in\nthe whitelist/blacklist\"?\n\nI think you have to forbid relative local path here if you want to allow\nstrings like \"github.com\", which can both be a relative path and a\nhostname.\n\nIf you do, then you can apply the rule\n\nint is_local = 0;\nif (is_absolute_path(curr_url))\n\tis_local = 1;\nelse if (skip_prefix(curr_url, \"file://\", &curr_url))\n\tis_local = 1\n\nto know if the current item is local or not. OTOH, for target_str, the\nURL scheme has to be there for remote accesses so there's no ambiguity.\n\n> +static int longest_prefix_size(const char* target_str,\n> +\t\t\t\tconst struct string_list *list)\n> +{\n> +\tstruct string_list_item *curr_item;\n> +\tstruct url_info target_url;\n> +\tint max_size = 0;\n> +\tif (!list)\n> +\t\treturn 0;\n> +\n> +\tskip_prefix(target_str, \"file://\", &target_str);\n> +\turl_normalize(target_str, &target_url);\n> +\n> +\tfor_each_string_list_item(curr_item, list) {\n\ncurr_item just says it's the current item, but not of what. Perhaps\ncurr_prefix or curr_pattern (not 100% convinced myself actually).\n\n> +\t\tstruct url_info curr_url;\n> +\t\tconst char *curr_str = curr_item->string;\n> +\t\tskip_prefix(curr_str, \"file://\", &curr_str);\n> +\t\turl_normalize(curr_str, &curr_url);\n> +\t\tif (target_url.url &&\n> +\t\t    curr_url.url &&\n> +\t\t    target_url.scheme_len == curr_url.scheme_len &&\n> +\t\t    !strncmp(target_url.url,curr_url.url,curr_url.scheme_len))\n\nThis comparison is quite obscure if you don't have the whole picture in\nmind. It really deserves a comment (explanation and/or example).\n\n> +\t\t\tfind_longest_prefix(target_url.url + target_url.host_off,\n> +\t\t\t\t\t    curr_url.url + curr_url.host_off,\n> +\t\t\t\t\t    &max_size);\n> +\t\telse if (target_url.url && !curr_url.url)\n> +\t\t\tfind_longest_prefix(target_url.url + target_url.host_off,\n> +\t\t\t\t\t    curr_str,\n> +\t\t\t\t\t    &max_size);\n> +\n> +\t\telse if(!target_url.url && !curr_url.url)\n\nSpace after if.\n\n> +\t\t\tfind_longest_prefix(target_str, curr_str, &max_size);\n\nWhen no condition match, there's no \"else\" so you're doing nothing. This\nalso deserves a comment like\n\n/* else, either:\n * - !target_url.url && curr_url.url: ...\n * - target_url.url && curr_url.url but both use different schemes: ...\n */\n\nActually, I'd even write actual if/else branches for these cases, with\nan empty statement like\n\nif (target_url.url && curr_url.url)\n\tif (target_url.scheme_len == curr_url.scheme_len &&\n            !strncmp(target_url.url,curr_url.url,curr_url.scheme_len))\n        \tfind_longest_prefix(...)\n        else\n        \t; /* explanation of why you do nothing here */\n...\nelse\n\t; /* likewise */\n\n\n>  static int do_push(const char *repo, int flags)\n>  {\n>  \tint i, errs;\n> @@ -404,15 +501,18 @@ static int do_push(const char *repo, int flags)\n>  \turl_nr = push_url_of_remote(remote, &url);\n>  \tif (url_nr) {\n>  \t\tfor (i = 0; i < url_nr; i++) {\n> -\t\t\tstruct transport *transport =\n> -\t\t\t\ttransport_get(remote, url[i]);\n> +\t\t\tstruct transport *transport;\n> +\t\t\tif (!(flags & TRANSPORT_PUSH_NO_HOOK))\n> +\t\t\t\tblock_unauthorized_url(url[i]);\n> +\t\t\ttransport = transport_get(remote, url[i]);\n>  \t\t\tif (push_with_options(transport, flags))\n>  \t\t\t\terrs++;\n>  \t\t}\n>  \t} else {\n> -\t\tstruct transport *transport =\n> -\t\t\ttransport_get(remote, NULL);\n> -\n> +\t\tstruct transport *transport;\n> +\t\tif (!(flags & TRANSPORT_PUSH_NO_HOOK))\n> +\t\t\tblock_unauthorized_url(remote->url[0]);\n> +\t\ttransport = transport_get(remote, NULL);\n>  \t\tif (push_with_options(transport, flags))\n>  \t\t\terrs++;\n>  \t}\n\nI think it's time to introduce a helper function doing either the body\nof the \"for\" loop or the \"else\" branch: you're starting to get too much\ncode duplication IMHO.\n\n> --- /dev/null\n> +++ b/t/t5544-push-whitelist-blacklist.sh\n> @@ -0,0 +1,51 @@\n> +#!/bin/sh\n> +\n> +test_description='blacklist for push'\n> +\n> +. ./test-lib.sh\n> +\n> +test_expect_success 'setup' '\n> +\tgit init --bare blacklist/ &&\n> +\tgit init --bare whitelist/ &&\n> +\tgit init --bare blacklist/allow &&\n> +\ttest_commit commit &&\n> +\techo \"fatal: Pushing to this remote using this protocol is forbidden\" > forbidden\n> +'\n> +\n> +test_expect_success 'basic case' '\n> +\tgit config --add remote.pushBlacklist http://blacklist.com &&\n> +\ttest_must_fail git push http://blacklist.com HEAD 2> result &&\n> +\ttest_cmp result forbidden\n> +'\n> +\n> +test_expect_success 'no scheme and no path' '\n> +\tgit config remote.pushBlacklist blacklist.com &&\n> +\ttest_must_fail git push http://blacklist.com/foo HEAD 2> result &&\n> +\ttest_cmp result forbidden\n> +'\n> +\n> +test_expect_success 'local path' '\n> +\tgit config remote.pushBlacklist blacklist &&\n> +\ttest_must_fail git push blacklist HEAD 2> result &&\n> +\ttest_cmp result forbidden\n> +'\n> +\n> +test_expect_success 'local path with file://' '\n> +\tgit config remote.pushBlacklist file://blacklist &&\n> +\ttest_must_fail git push blacklist HEAD 2> result &&\n> +\ttest_cmp result forbidden\n> +'\n> +test_expect_success 'only one scheme allowed' '\n> +\tgit config remote.pushDefaultPolicy deny &&\n> +\tgit config remote.pushWhitelist http://blacklist.com &&\n> +\ttest_must_fail git push https://blacklist.com HEAD 2> result &&\n> +\ttest_cmp result forbidden\n> +'\n> +\n> +test_expect_success 'denied repo in allowed repo' '\n> +\tgit config remote.pushBlacklist blacklist &&\n> +\tgit config --add remote.pushWhitelist blacklist/allow &&\n\nThis is not self-contained as it relies on the configuration set by the\nprevious test. Actually, I think you don't want the --add here.\n\nI think\n\ngit push -c remote.pushBlacklist=blacklist \\\n         -c remote.pushWhitelist=http://blacklist.com \\\n         -c remote.pushWhitelist=blacklist/allow \\\n\tblacklist/allow HEAD\n\nwould be even clearer: there's a bit more syntactical disturbance (-c,\n\\), but the whole relevant config is under the reviwer's eyes at the\nright time.\n\n> +\tgit push blacklist/allow HEAD\n\nThis is the only positive case, and many others are missing. For example\n\"push to https://example.com\" is allowed even when \"http://example.com\"\nis blacklisted.\n\nIt's harder to test without doing an actual remote push, but I think you\ncan still manage using fake URL scheme like foo://example.com and check\nthat git-remote-foo is actually called.\n\n-- \nMatthieu Moy\nhttp://www-verimag.imag.fr/~moy/\n"},{"id":"288446","messageId":"xmqq7fe2z45p.fsf@gitster.mtv.corp.google.com","threadId":"42528","inReplyTo":"vpq37oqjqgn.fsf@anie.imag.fr","subject":"Re: [RFC/PATCH] push: deny policy to prevent pushes to unwanted remotes.","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2016-06-06T16:13:38Z","receivedAt":"2016-06-06T16:13:38Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Matthieu Moy <Matthieu.Moy@grenoble-inp.fr> writes:\n\n> Antoine Queru <Antoine.Queru@grenoble-inp.org> writes:\n>\n>> Currently, a user wanting to prevent accidental pushes to the wrong\n>> remote has to create a pre-push hook.  The feature\n>\n> It's not clear what \"The feature\" refers to. Given the context, I read\n> it as \"pre-push hook\", but I think this is not what you meant.\n>\n>> User, password and port are not treated. Should it be ?\n>\n> Please elaborate on \"not treated\". What happens if github.com is\n> blacklisted by the user pushes to http://me@github.com ?\n> ...\n>> +remote.pushBlacklist::\n>> +\tThe list of remotes the user is forbidden to push to.\n>> +\tSee linkgit:git-push[1]\n>\n> This is only true when remote.pushDefaultPolicy is \"allow\". It makes\n> sense to say it explicitly here, if only to have a pointer to\n> pushDefaultPolicy.\n> ...\n\nAll good comments and suggestions, but I am bothered a lot more by\nthis being called \"Policy\" and uses words like \"Prevent\" and\n\"Forbidden\" as if there is a real enforcement mechanism, when in\nreality what is proposed is only advisory.\n\nIt is a good design balance to leave such a client-side mechanism\nadvisory and go no further than that; a truly draconian client side\nmechanism has no value, as the Git protocol is open, well documented\nand you can write and use a more liberal client to work around such\na mechanism.  I do not think selling such an advisory mechanism as\nif it is a policy enforcement feature.\n\n\"user wanting to avoid accidental pushes\" the log message mentions\nsets the expectation at the right level, I would think.  I do not\nthink this is about a \"Policy enforcement\"; where its value lies in\nis accident prevention (and possibly self discipline enforcement).\n"},{"id":"288455","messageId":"vpqd1nue0jx.fsf@anie.imag.fr","threadId":"42528","inReplyTo":"vpq37oqjqgn.fsf@anie.imag.fr","subject":"Re: [RFC/PATCH] push: deny policy to prevent pushes to unwanted remotes.","fromName":"Matthieu Moy","fromEmail":"matthieu.moy@grenoble-inp.fr","sentAt":"2016-06-06T16:37:06Z","receivedAt":"2016-06-06T16:37:06Z","isPatch":true,"sender":{"key":"matthieu.moy@grenoble-inp.fr","avatar":"https://gravatar.com/avatar/72c8a2705971a25dfaff23cece15130d405685845d911aedd5667ace277f3fc5?d=mp&s=160"},"body":"Matthieu Moy <Matthieu.Moy@grenoble-inp.fr> writes:\n\n>> + *easier and avoid confusion with a distant repo like 'github.com'\n\nForgotten nit in previous message: s/distant/remote/.\n\n-- \nMatthieu Moy\nhttp://www-verimag.imag.fr/~moy/\n"},{"id":"289316","messageId":"3899461F-44B4-407F-ACCE-793E65486554@gmail.com","threadId":"42528","inReplyTo":"2104102670.380496.1465220639026.JavaMail.zimbra@ensimag.grenoble-inp.fr","subject":"Re: [RFC/PATCH] push: deny policy to prevent pushes to unwanted remotes.","fromName":"Lars Schneider","fromEmail":"larsxschneider@gmail.com","sentAt":"2016-06-15T19:37:51Z","receivedAt":"2016-06-16T06:55:53Z","isPatch":true,"sender":{"key":"larsxschneider@gmail.com","avatar":"https://avatars.githubusercontent.com/u/477434?v=4"},"body":"\n>> ...\n>> \n> \n> Hello Rémi, thanks you for your input ! I'll make the appropriate changes \n> and send a new version as soon as i can !\n\nHi Antoine,\n\ndo you have an updated version already or is this the one I should look at?\nhttp://article.gmane.org/gmane.comp.version-control.git/296445\n\nThanks,\nLars"},{"id":"289713","messageId":"1599012395.707458.1466461517473.JavaMail.zimbra@ensimag.grenoble-inp.fr","threadId":"42528","inReplyTo":"3899461F-44B4-407F-ACCE-793E65486554@gmail.com","subject":"Re: [RFC/PATCH] push: deny policy to prevent pushes to unwanted remotes.","fromName":"Antoine Queru","fromEmail":"antoine.queru@ensimag.grenoble-inp.fr","sentAt":"2016-06-20T22:25:17Z","receivedAt":"2016-06-20T22:16:31Z","isPatch":true,"sender":{"key":"antoine.queru@ensimag.grenoble-inp.fr","avatar":null},"body":"\n> \n> >> ...\n> >> \n> > \n> > Hello Rémi, thanks you for your input ! I'll make the appropriate changes\n> > and send a new version as soon as i can !\n> \n> Hi Antoine,\n> \n> do you have an updated version already or is this the one I should look at?\n> http://article.gmane.org/gmane.comp.version-control.git/296445\n> \n> Thanks,\n> Lars\n\nHello Lars ! I'm actually in holidays, but I will try to send a new\nversion by the end of the week, it's nearly done. The code has been\nrefactored with what Matthieu and Remi told, so don't bother to read\nthe last version.\n\nHowever, in the last version, if we want to deny an website,\nincluding all schemes, we can blacklist the url without the\nscheme. For example, \"pushBlacklist = github.com\". By doing so, this\nremote is not an url anymore, and it can't be differenced with a local\nrelative path. It's a problem because these two have a different\ntreatement. The choice we made to solve this is to force the user to\nput the scheme \"file://\" before any local relative path. What do you\nthink ?\n\nAntoine\n"},{"id":"289725","messageId":"vpqfus7jc59.fsf@anie.imag.fr","threadId":"42528","inReplyTo":"1599012395.707458.1466461517473.JavaMail.zimbra@ensimag.grenoble-inp.fr","subject":"Re: [RFC/PATCH] push: deny policy to prevent pushes to unwanted remotes.","fromName":"Matthieu Moy","fromEmail":"matthieu.moy@grenoble-inp.fr","sentAt":"2016-06-21T06:20:50Z","receivedAt":"2016-06-21T06:22:24Z","isPatch":true,"sender":{"key":"matthieu.moy@grenoble-inp.fr","avatar":"https://gravatar.com/avatar/72c8a2705971a25dfaff23cece15130d405685845d911aedd5667ace277f3fc5?d=mp&s=160"},"body":"Antoine Queru <antoine.queru@ensimag.grenoble-inp.fr> writes:\n\n> However, in the last version, if we want to deny an website,\n> including all schemes, we can blacklist the url without the\n> scheme. For example, \"pushBlacklist = github.com\". By doing so, this\n> remote is not an url anymore, and it can't be differenced with a local\n> relative path. It's a problem because these two have a different\n> treatement. The choice we made to solve this is to force the user to\n> put the scheme \"file://\" before any local relative path. What do you\n> think ?\n\nfile:// URL can not be relative (well, you can invent a syntax where\nthey are, but that would be weird).\n\nI think you can just forbid relative path in whitelist/blacklist, hence\nconsider that anything that is neither a full URL nor an absolute path\nis a protocol-less URL:\n\n* http://github.com = github.com with HTTP protocol\n\n* github.com = github.com with any protocol\n\n* /path/to/file or file:///path/to/file = local path\n\n-- \nMatthieu Moy\nhttp://www-verimag.imag.fr/~moy/\n"},{"id":"289730","messageId":"827DA549-F046-4B52-90C2-ADB5BC72C16F@gmail.com","threadId":"42528","inReplyTo":"vpqfus7jc59.fsf@anie.imag.fr","subject":"Re: [RFC/PATCH] push: deny policy to prevent pushes to unwanted remotes.","fromName":"Lars Schneider","fromEmail":"larsxschneider@gmail.com","sentAt":"2016-06-21T07:45:45Z","receivedAt":"2016-06-21T07:46:09Z","isPatch":true,"sender":{"key":"larsxschneider@gmail.com","avatar":"https://avatars.githubusercontent.com/u/477434?v=4"},"body":"\n> On 21 Jun 2016, at 08:20, Matthieu Moy <Matthieu.Moy@grenoble-inp.fr> wrote:\n> \n> Antoine Queru <antoine.queru@ensimag.grenoble-inp.fr> writes:\n> \n>> However, in the last version, if we want to deny an website,\n>> including all schemes, we can blacklist the url without the\n>> scheme. For example, \"pushBlacklist = github.com\". By doing so, this\n>> remote is not an url anymore, and it can't be differenced with a local\n>> relative path. It's a problem because these two have a different\n>> treatement. The choice we made to solve this is to force the user to\n>> put the scheme \"file://\" before any local relative path. What do you\n>> think ?\n> \n> file:// URL can not be relative (well, you can invent a syntax where\n> they are, but that would be weird).\n> \n> I think you can just forbid relative path in whitelist/blacklist, hence\n> consider that anything that is neither a full URL nor an absolute path\n> is a protocol-less URL:\n> \n> * http://github.com = github.com with HTTP protocol\n> \n> * github.com = github.com with any protocol\n> \n> * /path/to/file or file:///path/to/file = local path\n\nI agree. Ignoring relative paths (and mentioning that in the docs)\nsounds reasonable to me. I don't think that would be a limitation as\nI can't think of a white/blacklist use case for relative URLs.\n\nThanks,\nLars"}]}