From 755225de6ce4c7b39137afefb35208d0dbc14d9d Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Sat, 29 Apr 2006 21:22:49 -0700 Subject: git builtin "push" This adds a builtin "push" command, which is largely just a C'ification of the "git-push.sh" script. Now, the reason I did it as a built-in is partly because it's yet another step on relying less on shell, but it's actually mostly because I've wanted to be able to push to _multiple_ repositories, and the most obvious and simplest interface for that would seem be to just have a "remotes" file that has multiple URL entries. (For "pull", having multiple entries should either just select the first one, or you could fall back on the others on failure - your choice). And quite frankly, it just became too damn messy to do that in shell. Besides, we actually have a fair amount of infrastructure in C, so it just wasn't that hard to do. Of course, this is almost totally untested. It probably doesn't work for anything but the one trial I threw at it. "Simple" doesn't necessarily mean "obviously correct". Signed-off-by: Linus Torvalds Signed-off-by: Junio C Hamano diff --git a/Makefile b/Makefile index 8ce27a6..9919992 100644 --- a/Makefile +++ b/Makefile @@ -214,7 +214,7 @@ LIB_OBJS = \ $(DIFF_OBJS) BUILTIN_OBJS = \ - builtin-log.o builtin-help.o + builtin-log.o builtin-help.o builtin-push.o GITLIBS = $(LIB_FILE) $(XDIFF_LIB) LIBS = $(GITLIBS) -lz diff --git a/builtin-push.c b/builtin-push.c new file mode 100644 index 0000000..a0c1caa --- /dev/null +++ b/builtin-push.c @@ -0,0 +1,255 @@ +/* + * "git push" + */ +#include "cache.h" +#include "refs.h" +#include "run-command.h" +#include "builtin.h" + +#define MAX_URI (16) + +static const char push_usage[] = "git push [--all] [--tags] [--force] [...]"; + +static int all = 0, tags = 0, force = 0, thin = 1; +static const char *execute = NULL; + +#define BUF_SIZE (2084) +static char buffer[BUF_SIZE]; + +static const char **refspec = NULL; +static int refspec_nr = 0; + +static void add_refspec(const char *ref) +{ + int nr = refspec_nr + 1; + refspec = xrealloc(refspec, nr * sizeof(char *)); + refspec[nr-1] = ref; + refspec_nr = nr; +} + +static int expand_one_ref(const char *ref, const unsigned char *sha1) +{ + /* Ignore the "refs/" at the beginning of the refname */ + ref += 5; + + if (strncmp(ref, "tags/", 5)) + return 0; + + add_refspec(strdup(ref)); + return 0; +} + +static void expand_refspecs(void) +{ + if (all) { + if (refspec_nr) + die("cannot mix '--all' and a refspec"); + + /* + * No need to expand "--all" - we'll just use + * the "--all" flag to send-pack + */ + return; + } + if (!tags) + return; + for_each_ref(expand_one_ref); +} + +static void set_refspecs(const char **refs, int nr) +{ + if (nr) { + size_t bytes = nr * sizeof(char *); + + refspec = xrealloc(refspec, bytes); + memcpy(refspec, refs, bytes); + refspec_nr = nr; + } + expand_refspecs(); +} + +static int get_remotes_uri(const char *repo, const char *uri[MAX_URI]) +{ + int n = 0; + FILE *f = fopen(git_path("remotes/%s", repo), "r"); + + if (!f) + return -1; + while (fgets(buffer, BUF_SIZE, f)) { + char *s, *p; + + if (strncmp("URL: ", buffer, 5)) + continue; + s = buffer + 5; + + /* Remove whitespace at the head.. */ + while (isspace(*s)) + s++; + if (!*s) + continue; + + /* ..and at the end */ + p = s + strlen(s); + while (isspace(p[-1])) + *--p = 0; + + uri[n++] = strdup(s); + if (n == MAX_URI) + break; + } + fclose(f); + if (!n) + die("remote '%s' has no URL", repo); + return n; +} + +static int get_branches_uri(const char *repo, const char *uri[MAX_URI]) +{ + const char *slash = strchr(repo, '/'); + int n = slash ? slash - repo : 1000; + FILE *f = fopen(git_path("branches/%.*s", n, repo), "r"); + char *s, *p; + int len; + + if (!f) + return 0; + s = fgets(buffer, BUF_SIZE, f); + fclose(f); + if (!s) + return 0; + while (isspace(*s)) + s++; + if (!*s) + return 0; + p = s + strlen(s); + while (isspace(p[-1])) + *--p = 0; + len = p - s; + if (slash) + len += strlen(slash); + p = xmalloc(len + 1); + strcpy(p, s); + if (slash) + strcat(p, slash); + uri[0] = p; + return 1; +} + +static int get_uri(const char *repo, const char *uri[MAX_URI]) +{ + int n; + + if (*repo != '/') { + n = get_remotes_uri(repo, uri); + if (n > 0) + return n; + + n = get_branches_uri(repo, uri); + if (n > 0) + return n; + } + + uri[0] = repo; + return 1; +} + +static int do_push(const char *repo) +{ + const char *uri[MAX_URI]; + int i, n = get_uri(repo, uri); + int remote; + const char **argv; + int argc; + + n = get_uri(repo, uri); + if (n <= 0) + die("bad repository '%s'", repo); + + argv = xmalloc((refspec_nr + 10) * sizeof(char *)); + argv[0] = "dummy-send-pack"; + argc = 1; + if (all) + argv[argc++] = "--all"; + if (force) + argv[argc++] = "--force"; + if (execute) + argv[argc++] = execute; + if (thin) + argv[argc++] = "--thin"; + remote = argc; + argv[argc++] = "dummy-remote"; + while (refspec_nr--) + argv[argc++] = *refspec++; + argv[argc] = NULL; + + for (i = 0; i < n; i++) { + int error; + const char *dest = uri[i]; + const char *sender = "git-send-pack"; + if (!strncmp(dest, "http://", 7) || + !strncmp(dest, "https://", 8)) + sender = "git-http-push"; + argv[0] = sender; + argv[remote] = dest; + error = run_command_v(argc, argv); + if (!error) + continue; + switch (error) { + case -ERR_RUN_COMMAND_FORK: + die("unable to fork for %s", sender); + case -ERR_RUN_COMMAND_EXEC: + die("unable to exec %s", sender); + case -ERR_RUN_COMMAND_WAITPID: + case -ERR_RUN_COMMAND_WAITPID_WRONG_PID: + case -ERR_RUN_COMMAND_WAITPID_SIGNAL: + case -ERR_RUN_COMMAND_WAITPID_NOEXIT: + die("%s died with strange error", sender); + default: + return -error; + } + } + return 0; +} + +int cmd_push(int argc, const char **argv, char **envp) +{ + int i; + const char *repo = "origin"; // default repository + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (arg[0] != '-') { + repo = arg; + i++; + break; + } + if (!strcmp(arg, "--all")) { + all = 1; + continue; + } + if (!strcmp(arg, "--tags")) { + tags = 1; + continue; + } + if (!strcmp(arg, "--force")) { + force = 1; + continue; + } + if (!strcmp(arg, "--thin")) { + thin = 1; + continue; + } + if (!strcmp(arg, "--no-thin")) { + thin = 0; + continue; + } + if (!strncmp(arg, "--exec=", 7)) { + execute = arg; + continue; + } + usage(push_usage); + } + set_refspecs(argv + i, argc - i); + return do_push(repo); +} diff --git a/builtin.h b/builtin.h index 47408a0..94fa9b5 100644 --- a/builtin.h +++ b/builtin.h @@ -20,4 +20,6 @@ extern int cmd_whatchanged(int argc, const char **argv, char **envp); extern int cmd_show(int argc, const char **argv, char **envp); extern int cmd_log(int argc, const char **argv, char **envp); +extern int cmd_push(int argc, const char **argv, char **envp); + #endif diff --git a/git.c b/git.c index 01b7e28..fd479e9 100644 --- a/git.c +++ b/git.c @@ -46,6 +46,7 @@ static void handle_internal_command(int argc, const char **argv, char **envp) { "log", cmd_log }, { "whatchanged", cmd_whatchanged }, { "show", cmd_show }, + { "push", cmd_push }, }; int i; -- cgit v0.10.2-6-g49f6 From 5c477b972583e96b65ef1613c5d0fb1b90af3d0e Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sun, 30 Apr 2006 14:05:55 +0200 Subject: builtin-push: resurrect parsing of Push: lines The C'ification of push left these behind. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano diff --git a/builtin-push.c b/builtin-push.c index a0c1caa..4e659f0 100644 --- a/builtin-push.c +++ b/builtin-push.c @@ -68,6 +68,10 @@ static void set_refspecs(const char **refs, int nr) expand_refspecs(); } +#define MAX_REFSPECS 10 +static int current_refspec = 0; +static char *refspecs_[MAX_REFSPECS]; + static int get_remotes_uri(const char *repo, const char *uri[MAX_URI]) { int n = 0; @@ -76,11 +80,17 @@ static int get_remotes_uri(const char *repo, const char *uri[MAX_URI]) if (!f) return -1; while (fgets(buffer, BUF_SIZE, f)) { + int is_refspec; char *s, *p; - if (strncmp("URL: ", buffer, 5)) + if (!strncmp("URL: ", buffer, 5)) { + is_refspec = 0; + s = buffer + 5; + } else if (!strncmp("Push: ", buffer, 6)) { + is_refspec = 1; + s = buffer + 6; + } else continue; - s = buffer + 5; /* Remove whitespace at the head.. */ while (isspace(*s)) @@ -93,9 +103,10 @@ static int get_remotes_uri(const char *repo, const char *uri[MAX_URI]) while (isspace(p[-1])) *--p = 0; - uri[n++] = strdup(s); - if (n == MAX_URI) - break; + if (!is_refspec && n < MAX_URI) + uri[n++] = strdup(s); + else if (is_refspec && current_refspec < MAX_REFSPECS) + refspecs_[current_refspec++] = strdup(s); } fclose(f); if (!n) @@ -140,6 +151,8 @@ static int get_uri(const char *repo, const char *uri[MAX_URI]) int n; if (*repo != '/') { + current_refspec = 0; + n = get_remotes_uri(repo, uri); if (n > 0) return n; @@ -165,6 +178,9 @@ static int do_push(const char *repo) if (n <= 0) die("bad repository '%s'", repo); + if (refspec_nr == 0) + set_refspecs((const char**)refspecs_, current_refspec); + argv = xmalloc((refspec_nr + 10) * sizeof(char *)); argv[0] = "dummy-send-pack"; argc = 1; -- cgit v0.10.2-6-g49f6 From 7aaf83dafbde7aaa60a1e220e967faaeb28a46f6 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Sun, 30 Apr 2006 16:03:27 -0700 Subject: Fix builtin-push to honor Push: lines in remotes file. [jc: originally from Johannes Schindelin, but reworked to lift a hard limit of Push: lines] Signed-off-by: Junio C Hamano diff --git a/builtin-push.c b/builtin-push.c index 4e659f0..9a861b5 100644 --- a/builtin-push.c +++ b/builtin-push.c @@ -68,14 +68,11 @@ static void set_refspecs(const char **refs, int nr) expand_refspecs(); } -#define MAX_REFSPECS 10 -static int current_refspec = 0; -static char *refspecs_[MAX_REFSPECS]; - static int get_remotes_uri(const char *repo, const char *uri[MAX_URI]) { int n = 0; FILE *f = fopen(git_path("remotes/%s", repo), "r"); + int has_explicit_refspec = refspec_nr; if (!f) return -1; @@ -103,10 +100,14 @@ static int get_remotes_uri(const char *repo, const char *uri[MAX_URI]) while (isspace(p[-1])) *--p = 0; - if (!is_refspec && n < MAX_URI) - uri[n++] = strdup(s); - else if (is_refspec && current_refspec < MAX_REFSPECS) - refspecs_[current_refspec++] = strdup(s); + if (!is_refspec) { + if (n < MAX_URI) + uri[n++] = strdup(s); + else + error("more than %d URL's specified, ignoreing the rest", MAX_URI); + } + else if (is_refspec && !has_explicit_refspec) + add_refspec(strdup(s)); } fclose(f); if (!n) @@ -146,13 +147,17 @@ static int get_branches_uri(const char *repo, const char *uri[MAX_URI]) return 1; } -static int get_uri(const char *repo, const char *uri[MAX_URI]) +/* + * Read remotes and branches file, fill the push target URI + * list. If there is no command line refspecs, read Push: lines + * to set up the *refspec list as well. + * return the number of push target URIs + */ +static int read_config(const char *repo, const char *uri[MAX_URI]) { int n; if (*repo != '/') { - current_refspec = 0; - n = get_remotes_uri(repo, uri); if (n > 0) return n; @@ -169,18 +174,15 @@ static int get_uri(const char *repo, const char *uri[MAX_URI]) static int do_push(const char *repo) { const char *uri[MAX_URI]; - int i, n = get_uri(repo, uri); + int i, n; int remote; const char **argv; int argc; - n = get_uri(repo, uri); + n = read_config(repo, uri); if (n <= 0) die("bad repository '%s'", repo); - if (refspec_nr == 0) - set_refspecs((const char**)refspecs_, current_refspec); - argv = xmalloc((refspec_nr + 10) * sizeof(char *)); argv[0] = "dummy-send-pack"; argc = 1; -- cgit v0.10.2-6-g49f6 From 54eb2d3ff91bf7a914b6e1de9cf8f748d6a9ecb4 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Mon, 1 May 2006 23:40:37 -0700 Subject: builtin-push: make it official. Remove the shell script version, and hardlink the git binary to it. Signed-off-by: Junio C Hamano diff --git a/Makefile b/Makefile index 9919992..9cb40a8 100644 --- a/Makefile +++ b/Makefile @@ -118,7 +118,7 @@ SCRIPT_SH = \ git-count-objects.sh git-diff.sh git-fetch.sh \ git-format-patch.sh git-ls-remote.sh \ git-merge-one-file.sh git-parse-remote.sh \ - git-prune.sh git-pull.sh git-push.sh git-rebase.sh \ + git-prune.sh git-pull.sh git-rebase.sh \ git-repack.sh git-request-pull.sh git-reset.sh \ git-resolve.sh git-revert.sh git-rm.sh git-sh-setup.sh \ git-tag.sh git-verify-tag.sh git-whatchanged.sh \ @@ -167,7 +167,8 @@ PROGRAMS = \ git-name-rev$X git-pack-redundant$X git-repo-config$X git-var$X \ git-describe$X git-merge-tree$X git-blame$X git-imap-send$X -BUILT_INS = git-log$X +BUILT_INS = git-log$X \ + git-push$X # what 'all' will build and 'install' will install, in gitexecdir ALL_PROGRAMS = $(PROGRAMS) $(SIMPLE_PROGRAMS) $(SCRIPTS) -- cgit v0.10.2-6-g49f6 From 97d4df0b29d262df291985aed486e226b43132d0 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sun, 30 Apr 2006 15:23:48 +0200 Subject: builtin-push: also ask config for remote information Now you can store your remote information in the config file like this: [remote.upstream] url = me@company.com:the-project push = master:iceballs [jc: fixed up to adjust a different fix for Push: lines earlier.] Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano diff --git a/builtin-push.c b/builtin-push.c index 9a861b5..06d06ff 100644 --- a/builtin-push.c +++ b/builtin-push.c @@ -115,6 +115,41 @@ static int get_remotes_uri(const char *repo, const char *uri[MAX_URI]) return n; } +static const char **config_uri; +static const char *config_repo; +static int config_repo_len; +static int config_current_uri; +static int config_get_refspecs; + +static int get_remote_config(const char* key, const char* value) +{ + if (!strncmp(key, "remote.", 7) && + !strncmp(key + 7, config_repo, config_repo_len)) { + if (!strcmp(key + 7 + config_repo_len, ".url")) { + if (config_current_uri < MAX_URI) + config_uri[config_current_uri++] = strdup(value); + else + error("more than %d URL's specified, ignoring the rest", MAX_URI); + } + else if (config_get_refspecs && + !strcmp(key + 7 + config_repo_len, ".push")) + add_refspec(strdup(value)); + } + return 0; +} + +static int get_config_remotes_uri(const char *repo, const char *uri[MAX_URI]) +{ + config_repo_len = strlen(repo); + config_repo = repo; + config_current_uri = 0; + config_uri = uri; + config_get_refspecs = !refspec_nr; + + git_config(get_remote_config); + return config_current_uri; +} + static int get_branches_uri(const char *repo, const char *uri[MAX_URI]) { const char *slash = strchr(repo, '/'); @@ -162,6 +197,10 @@ static int read_config(const char *repo, const char *uri[MAX_URI]) if (n > 0) return n; + n = get_config_remotes_uri(repo, uri); + if (n > 0) + return n; + n = get_branches_uri(repo, uri); if (n > 0) return n; -- cgit v0.10.2-6-g49f6 From 5a223a0d434c874984a0251eca4520ef95718a6d Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Sun, 30 Apr 2006 15:24:03 +0200 Subject: fetch, pull: ask config for remote information Now you can say [remote.junio] url = git://git.kernel.org/pub/scm/git/git.git pull = next:next in your .git/config. Signed-off-by: Johannes Schindelin Signed-off-by: Junio C Hamano diff --git a/git-parse-remote.sh b/git-parse-remote.sh index c9b899e..8ce57c8 100755 --- a/git-parse-remote.sh +++ b/git-parse-remote.sh @@ -4,13 +4,43 @@ # this would fail in that case and would issue an error message. GIT_DIR=$(git-rev-parse --git-dir 2>/dev/null) || :; +if [ -d "$GIT_DIR"/remotes -a "$GIT_REWRITE_REMOTES" = true ]; then + echo "Rewriting $GIT_DIR/remotes" >&2 + error=0 + # rewrite into config + { + cd "$GIT_DIR"/remotes + ls | while read f; do + name=$(echo -n "$f" | tr -c "A-Za-z0-9" ".") + sed -n \ + -e "s/^URL: /remote.$name.url . /p" \ + -e "s/^Pull: /remote.$name.pull ^$ /p" \ + -e "s/^Push: /remote.$name.push ^$ /p" \ + < "$f" + done + echo done + } | while read key regex value; do + case $key in + done) + if [ $error = 0 ]; then + mv "$GIT_DIR"/remotes "$GIT_DIR"/remotes.old + fi ;; + *) + git-repo-config $key "$value" $regex || error=1 ;; + esac + done +fi + get_data_source () { case "$1" in */*) # Not so fast. This could be the partial URL shorthand... token=$(expr "z$1" : 'z\([^/]*\)/') remainder=$(expr "z$1" : 'z[^/]*/\(.*\)') - if test -f "$GIT_DIR/branches/$token" + if test "$(git-repo-config --get "remote.$token.url")" + then + echo config-partial + elif test -f "$GIT_DIR/branches/$token" then echo branches-partial else @@ -18,7 +48,10 @@ get_data_source () { fi ;; *) - if test -f "$GIT_DIR/remotes/$1" + if test "$(git-repo-config --get "remote.$1.url")" + then + echo config + elif test -f "$GIT_DIR/remotes/$1" then echo remotes elif test -f "$GIT_DIR/branches/$1" @@ -35,6 +68,15 @@ get_remote_url () { case "$data_source" in '') echo "$1" ;; + config-partial) + token=$(expr "z$1" : '\([^/]*\)/') + remainder=$(expr "z$1" : '[^/]*/\(.*\)') + url=$(git-repo-config --get "remote.$token.url") + echo "$url/$remainder" + ;; + config) + git-repo-config --get "remote.$1.url" + ;; remotes) sed -ne '/^URL: */{ s///p @@ -56,8 +98,10 @@ get_remote_url () { get_remote_default_refs_for_push () { data_source=$(get_data_source "$1") case "$data_source" in - '' | branches | branches-partial) + '' | config-partial | branches | branches-partial) ;; # no default push mapping, just send matching refs. + config) + git-repo-config --get-all "remote.$1.push" ;; remotes) sed -ne '/^Push: */{ s///p @@ -111,8 +155,11 @@ canon_refs_list_for_fetch () { get_remote_default_refs_for_fetch () { data_source=$(get_data_source "$1") case "$data_source" in - '' | branches-partial) + '' | config-partial | branches-partial) echo "HEAD:" ;; + config) + canon_refs_list_for_fetch \ + $(git-repo-config --get-all "remote.$1.pull") ;; branches) remote_branch=$(sed -ne '/#/s/.*#//p' "$GIT_DIR/branches/$1") case "$remote_branch" in '') remote_branch=master ;; esac -- cgit v0.10.2-6-g49f6 From c9889e40858fa8b9265b58519e7281c07f3632c6 Mon Sep 17 00:00:00 2001 From: Junio C Hamano Date: Tue, 2 May 2006 14:16:46 -0700 Subject: Revert "fetch, pull: ask config for remote information" This reverts 5a223a0d434c874984a0251eca4520ef95718a6d commit. I asked Johannes to roll an updated version, so let's wait for it. diff --git a/git-parse-remote.sh b/git-parse-remote.sh index 8ce57c8..c9b899e 100755 --- a/git-parse-remote.sh +++ b/git-parse-remote.sh @@ -4,43 +4,13 @@ # this would fail in that case and would issue an error message. GIT_DIR=$(git-rev-parse --git-dir 2>/dev/null) || :; -if [ -d "$GIT_DIR"/remotes -a "$GIT_REWRITE_REMOTES" = true ]; then - echo "Rewriting $GIT_DIR/remotes" >&2 - error=0 - # rewrite into config - { - cd "$GIT_DIR"/remotes - ls | while read f; do - name=$(echo -n "$f" | tr -c "A-Za-z0-9" ".") - sed -n \ - -e "s/^URL: /remote.$name.url . /p" \ - -e "s/^Pull: /remote.$name.pull ^$ /p" \ - -e "s/^Push: /remote.$name.push ^$ /p" \ - < "$f" - done - echo done - } | while read key regex value; do - case $key in - done) - if [ $error = 0 ]; then - mv "$GIT_DIR"/remotes "$GIT_DIR"/remotes.old - fi ;; - *) - git-repo-config $key "$value" $regex || error=1 ;; - esac - done -fi - get_data_source () { case "$1" in */*) # Not so fast. This could be the partial URL shorthand... token=$(expr "z$1" : 'z\([^/]*\)/') remainder=$(expr "z$1" : 'z[^/]*/\(.*\)') - if test "$(git-repo-config --get "remote.$token.url")" - then - echo config-partial - elif test -f "$GIT_DIR/branches/$token" + if test -f "$GIT_DIR/branches/$token" then echo branches-partial else @@ -48,10 +18,7 @@ get_data_source () { fi ;; *) - if test "$(git-repo-config --get "remote.$1.url")" - then - echo config - elif test -f "$GIT_DIR/remotes/$1" + if test -f "$GIT_DIR/remotes/$1" then echo remotes elif test -f "$GIT_DIR/branches/$1" @@ -68,15 +35,6 @@ get_remote_url () { case "$data_source" in '') echo "$1" ;; - config-partial) - token=$(expr "z$1" : '\([^/]*\)/') - remainder=$(expr "z$1" : '[^/]*/\(.*\)') - url=$(git-repo-config --get "remote.$token.url") - echo "$url/$remainder" - ;; - config) - git-repo-config --get "remote.$1.url" - ;; remotes) sed -ne '/^URL: */{ s///p @@ -98,10 +56,8 @@ get_remote_url () { get_remote_default_refs_for_push () { data_source=$(get_data_source "$1") case "$data_source" in - '' | config-partial | branches | branches-partial) + '' | branches | branches-partial) ;; # no default push mapping, just send matching refs. - config) - git-repo-config --get-all "remote.$1.push" ;; remotes) sed -ne '/^Push: */{ s///p @@ -155,11 +111,8 @@ canon_refs_list_for_fetch () { get_remote_default_refs_for_fetch () { data_source=$(get_data_source "$1") case "$data_source" in - '' | config-partial | branches-partial) + '' | branches-partial) echo "HEAD:" ;; - config) - canon_refs_list_for_fetch \ - $(git-repo-config --get-all "remote.$1.pull") ;; branches) remote_branch=$(sed -ne '/#/s/.*#//p' "$GIT_DIR/branches/$1") case "$remote_branch" in '') remote_branch=master ;; esac -- cgit v0.10.2-6-g49f6