{"thread":{"id":"24546","subject":"[PATCHv4 1/3] Allow creation of arbitrary git-shell commands","startedAt":"2010-07-28T07:43:01Z","lastAt":"2010-07-29T00:37:29Z","messageCount":7,"participants":["Greg Brockman","Junio C Hamano"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"146586","messageId":"1280302984-1752-1-git-send-email-gdb@mit.edu","threadId":"24546","inReplyTo":null,"subject":"[PATCHv4 0/3] Updated patch series for providing mechanism to list available repositories","fromName":"Greg Brockman","fromEmail":"gdb@mit.edu","sentAt":"2010-07-28T07:43:01Z","receivedAt":"2010-07-28T07:43:01Z","isPatch":false,"sender":{"key":"gdb@mit.edu","avatar":"https://gravatar.com/avatar/3b907cf60d0cadf6d0659d5429d971b16965fb6b1e3f5acd26a6ba2974b45da4?d=mp&s=160"},"body":"This revised patch series should address all comments given thus far.\nThe specific changes made between v3 and v4 are documented in the\npatch emails.\n\nThanks,\n\nGreg\n"},{"id":"146585","messageId":"1280302984-1752-2-git-send-email-gdb@mit.edu","threadId":"24546","inReplyTo":"1280302984-1752-1-git-send-email-gdb@mit.edu","subject":"[PATCHv4 1/3] Allow creation of arbitrary git-shell commands","fromName":"Greg Brockman","fromEmail":"gdb@mit.edu","sentAt":"2010-07-28T07:43:02Z","receivedAt":"2010-07-28T07:43:02Z","isPatch":false,"sender":{"key":"gdb@mit.edu","avatar":"https://gravatar.com/avatar/3b907cf60d0cadf6d0659d5429d971b16965fb6b1e3f5acd26a6ba2974b45da4?d=mp&s=160"},"body":"This provides a mechanism for the server to expose custom\nfunctionality to clients.  My particular use case is that I would like\na way of discovering all repositories available for cloning.  A\nclient that clones via\n\n  git clone user@example.com\n\ncan invoke a command by\n\n  ssh user@example.com $command\n\nSigned-off-by: Greg Brockman <gdb@mit.edu>\n---\n shell.c |   43 +++++++++++++++++++++++++++++++++++++++++--\n 1 files changed, 41 insertions(+), 2 deletions(-)\n\nThis patch differs from its v3 version by the addition of a 'cd_to_homedir' method\nand a call to it prior to running any commands in git-shell-commands.  This ensures\nthat all commands are run out of $HOME/git-shell-commands.\n\ndiff --git a/shell.c b/shell.c\nindex e4864e0..88b29aa 100644\n--- a/shell.c\n+++ b/shell.c\n@@ -3,6 +3,8 @@\n #include \"exec_cmd.h\"\n #include \"strbuf.h\"\n \n+#define COMMAND_DIR \"git-shell-commands\"\n+\n static int do_generic_cmd(const char *me, char *arg)\n {\n \tconst char *my_argv[4];\n@@ -33,6 +35,28 @@ static int do_cvs_cmd(const char *me, char *arg)\n \treturn execv_git_cmd(cvsserver_argv);\n }\n \n+static int is_valid_cmd_name(const char *cmd)\n+{\n+\t/* Test command contains no . or / characters */\n+\treturn cmd[strcspn(cmd, \"./\")] == '\\0';\n+}\n+\n+static char *make_cmd(const char *prog)\n+{\n+\tchar *prefix = xmalloc((strlen(prog) + strlen(COMMAND_DIR) + 2));\n+\tstrcpy(prefix, COMMAND_DIR);\n+\tstrcat(prefix, \"/\");\n+\tstrcat(prefix, prog);\n+\treturn prefix;\n+}\n+\n+static void cd_to_homedir(void) {\n+\tstruct passwd *pw = getpwuid(geteuid());\n+\tif (pw == NULL)\n+\t\tdie(\"could not retrieve user's passwd entry\");\n+\tif (chdir(pw->pw_dir) == -1)\n+\t\tdie(\"could not chdir to user's home directory\");\n+}\n \n static struct commands {\n \tconst char *name;\n@@ -48,6 +72,7 @@ static struct commands {\n int main(int argc, char **argv)\n {\n \tchar *prog;\n+\tconst char **user_argv;\n \tstruct commands *cmd;\n \tint devnull_fd;\n \n@@ -76,7 +101,7 @@ int main(int argc, char **argv)\n \telse if (argc != 3 || strcmp(argv[1], \"-c\"))\n \t\tdie(\"What do you think I am? A shell?\");\n \n-\tprog = argv[2];\n+\tprog = xstrdup(argv[2]);\n \tif (!strncmp(prog, \"git\", 3) && isspace(prog[3]))\n \t\t/* Accept \"git foo\" as if the caller said \"git-foo\". */\n \t\tprog[3] = '-';\n@@ -99,5 +124,19 @@ int main(int argc, char **argv)\n \t\t}\n \t\texit(cmd->exec(cmd->name, arg));\n \t}\n-\tdie(\"unrecognized command '%s'\", prog);\n+\n+\tcd_to_homedir();\n+\tif (split_cmdline(prog, &user_argv) != -1) {\n+\t\tif (is_valid_cmd_name(user_argv[0])) {\n+\t\t\tprog = make_cmd(user_argv[0]);\n+\t\t\tuser_argv[0] = prog;\n+\t\t\texecv(user_argv[0], (char *const *) user_argv);\n+\t\t}\n+\t\tfree(prog);\n+\t\tfree(user_argv);\n+\t\tdie(\"unrecognized command '%s'\", argv[2]);\n+\t} else {\n+\t\tfree(prog);\n+\t\tdie(\"invalid command format '%s'\", argv[2]);\n+\t}\n }\n-- \n1.7.0.4\n"},{"id":"146588","messageId":"1280302984-1752-3-git-send-email-gdb@mit.edu","threadId":"24546","inReplyTo":"1280302984-1752-1-git-send-email-gdb@mit.edu","subject":"[PATCHv4 2/3] Add interactive mode to git-shell for user-friendliness","fromName":"Greg Brockman","fromEmail":"gdb@mit.edu","sentAt":"2010-07-28T07:43:03Z","receivedAt":"2010-07-28T07:43:03Z","isPatch":false,"sender":{"key":"gdb@mit.edu","avatar":"https://gravatar.com/avatar/3b907cf60d0cadf6d0659d5429d971b16965fb6b1e3f5acd26a6ba2974b45da4?d=mp&s=160"},"body":"Signed-off-by: Greg Brockman <gdb@mit.edu>\n---\n shell.c |   76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----\n 1 files changed, 70 insertions(+), 6 deletions(-)\n\nThis patch differs from v3 by the addition of a 'cd_to_homedir' call prior to\nspawning the shell.\n\ndiff --git a/shell.c b/shell.c\nindex 88b29aa..a40b839 100644\n--- a/shell.c\n+++ b/shell.c\n@@ -2,8 +2,10 @@\n #include \"quote.h\"\n #include \"exec_cmd.h\"\n #include \"strbuf.h\"\n+#include \"run-command.h\"\n \n #define COMMAND_DIR \"git-shell-commands\"\n+#define HELP_COMMAND COMMAND_DIR \"/help\"\n \n static int do_generic_cmd(const char *me, char *arg)\n {\n@@ -58,6 +60,56 @@ static void cd_to_homedir(void) {\n \t\tdie(\"could not chdir to user's home directory\");\n }\n \n+static void run_shell(void)\n+{\n+\tint done = 0;\n+\tstatic const char *help_argv[] = { HELP_COMMAND, NULL };\n+\t/* Print help if enabled */\n+\trun_command_v_opt(help_argv, RUN_SILENT_EXEC_FAILURE);\n+\n+\tdo {\n+\t\tstruct strbuf line = STRBUF_INIT;\n+\t\tconst char *prog;\n+\t\tchar *full_cmd;\n+\t\tchar *rawargs;\n+\t\tconst char **argv;\n+\t\tint code;\n+\n+\t\tfprintf(stderr, \"git> \");\n+\t\tif (strbuf_getline(&line, stdin, '\\n') == EOF) {\n+\t\t\tfprintf(stderr, \"\\n\");\n+\t\t\tstrbuf_release(&line);\n+\t\t\tbreak;\n+\t\t}\n+\t\tstrbuf_trim(&line);\n+\t\trawargs = strbuf_detach(&line, NULL);\n+\t\tif (split_cmdline(rawargs, &argv) == -1) {\n+\t\t\tfree(rawargs);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tprog = argv[0];\n+\t\tif (!strcmp(prog, \"\")) {\n+\t\t} else if (!strcmp(prog, \"quit\") || !strcmp(prog, \"logout\") ||\n+\t\t\t   !strcmp(prog, \"exit\") || !strcmp(prog, \"bye\")) {\n+\t\t\tdone = 1;\n+\t\t} else if (is_valid_cmd_name(prog)) {\n+\t\t\tfull_cmd = make_cmd(prog);\n+\t\t\targv[0] = full_cmd;\n+\t\t\tcode = run_command_v_opt(argv, RUN_SILENT_EXEC_FAILURE);\n+\t\t\tif (code == -1 && errno == ENOENT) {\n+\t\t\t\tfprintf(stderr, \"unrecognized command '%s'\\n\", prog);\n+\t\t\t}\n+\t\t\tfree(full_cmd);\n+\t\t} else {\n+\t\t\tfprintf(stderr, \"invalid command format '%s'\\n\", prog);\n+\t\t}\n+\n+\t\tfree(argv);\n+\t\tfree(rawargs);\n+\t} while (!done);\n+}\n+\n static struct commands {\n \tconst char *name;\n \tint (*exec)(const char *me, char *arg);\n@@ -91,15 +143,27 @@ int main(int argc, char **argv)\n \t/*\n \t * Special hack to pretend to be a CVS server\n \t */\n-\tif (argc == 2 && !strcmp(argv[1], \"cvs server\"))\n+\tif (argc == 2 && !strcmp(argv[1], \"cvs server\")) {\n \t\targv--;\n-\n+\t}\n \t/*\n-\t * We do not accept anything but \"-c\" followed by \"cmd arg\",\n-\t * where \"cmd\" is a very limited subset of git commands.\n+\t * Allow the user to run an interactive shell\n \t */\n-\telse if (argc != 3 || strcmp(argv[1], \"-c\"))\n-\t\tdie(\"What do you think I am? A shell?\");\n+\telse if (argc == 1) {\n+\t\tcd_to_homedir();\n+\t\tif (access(COMMAND_DIR, R_OK | X_OK) == -1)\n+\t\t\tdie(\"Sorry, the interactive git-shell is not enabled\");\n+\t\trun_shell();\n+\t\texit(0);\n+\t}\n+\t/*\n+\t * We do not accept any other modes except \"-c\" followed by\n+\t * \"cmd arg\", where \"cmd\" is a very limited subset of git\n+\t * commands or a command in the COMMAND_DIR\n+\t */\n+\telse if (argc != 3 || strcmp(argv[1], \"-c\")) {\n+\t\tdie(\"Run with no arguments or with -c cmd\");\n+\t}\n \n \tprog = xstrdup(argv[2]);\n \tif (!strncmp(prog, \"git\", 3) && isspace(prog[3]))\n-- \n1.7.0.4\n"},{"id":"146589","messageId":"1280302984-1752-4-git-send-email-gdb@mit.edu","threadId":"24546","inReplyTo":"1280302984-1752-1-git-send-email-gdb@mit.edu","subject":"[PATCHv4 3/3] Add sample commands for git-shell","fromName":"Greg Brockman","fromEmail":"gdb@mit.edu","sentAt":"2010-07-28T07:43:04Z","receivedAt":"2010-07-28T07:43:04Z","isPatch":false,"sender":{"key":"gdb@mit.edu","avatar":"https://gravatar.com/avatar/3b907cf60d0cadf6d0659d5429d971b16965fb6b1e3f5acd26a6ba2974b45da4?d=mp&s=160"},"body":"Provide a 'list' command to view available bare repositories ending in\n.git and a 'help command to display usage.  Also add documentation in\na README\n\nSigned-off-by: Greg Brockman <gdb@mit.edu>\n---\n contrib/git-shell-commands/README |   18 ++++++++++++++++++\n contrib/git-shell-commands/help   |   16 ++++++++++++++++\n contrib/git-shell-commands/list   |   10 ++++++++++\n 3 files changed, 44 insertions(+), 0 deletions(-)\n create mode 100644 contrib/git-shell-commands/README\n create mode 100755 contrib/git-shell-commands/help\n create mode 100755 contrib/git-shell-commands/list\n\nThis differs from v3 in that:\n\n* the 'find' command in 'list' discards its stderr.  This is useful for\n  avoiding noise from inaccessible directories or non-git repositories with\n  name matching '*.git'.\n* the README does not mention that commands will be run from $(pwd)/git-shell-commands,\n  because they will now be run from $HOME/git-shell-commands.\n\ndiff --git a/contrib/git-shell-commands/README b/contrib/git-shell-commands/README\nnew file mode 100644\nindex 0000000..438463b\n--- /dev/null\n+++ b/contrib/git-shell-commands/README\n@@ -0,0 +1,18 @@\n+Sample programs callable through git-shell.  Place a directory named\n+'git-shell-commands' in the home directory of a user whose shell is\n+git-shell.  Then anyone logging in as that user will be able to run\n+executables in the 'git-shell-commands' directory.\n+\n+Provided commands:\n+\n+help: Prints out the names of available commands.  When run\n+interactively, git-shell will automatically run 'help' on startup,\n+provided it exists.\n+\n+list: Displays any bare repository whose name ends with \".git\" under\n+user's home directory.  No other git repositories are visible,\n+although they might be clonable through git-shell.  'list' is designed\n+to minimize the number of calls to git that must be made in finding\n+available repositories; if your setup has additional repositories that\n+should be user-discoverable, you may wish to modify 'list'\n+accordingly.\ndiff --git a/contrib/git-shell-commands/help b/contrib/git-shell-commands/help\nnew file mode 100755\nindex 0000000..a43fcd6\n--- /dev/null\n+++ b/contrib/git-shell-commands/help\n@@ -0,0 +1,16 @@\n+#!/bin/sh\n+\n+if tty -s; then\n+    echo \"Run 'help' for help, or 'exit' to leave.  Available commands:\"\n+else\n+    echo \"Run 'help' for help.  Available commands:\"\n+fi\n+\n+cd \"$(dirname \"$0\")\"\n+\n+for cmd in *; do\n+    case \"$cmd\" in\n+\thelp) ;;\n+\t*) [ -f \"$cmd\" ] && [ -x \"$cmd\" ] && echo \"$cmd\" ;;\n+    esac\n+done\ndiff --git a/contrib/git-shell-commands/list b/contrib/git-shell-commands/list\nnew file mode 100755\nindex 0000000..ffc5ac4\n--- /dev/null\n+++ b/contrib/git-shell-commands/list\n@@ -0,0 +1,10 @@\n+#!/bin/sh\n+\n+print_if_bare_repo='\n+       if \"$(git --git-dir=\"$1\" rev-parse --is-bare-repository)\" = true\n+       then\n+               printf \"%s\\n\" \"${1#./}\"\n+       fi\n+'\n+\n+find -type d -name \"*.git\" -exec sh -c \"$print_if_bare_repo\" -- \\{} \\; -prune 2>/dev/null\n-- \n1.7.0.4\n"},{"id":"146646","messageId":"7viq3z4j1e.fsf@alter.siamese.dyndns.org","threadId":"24546","inReplyTo":"1280302984-1752-1-git-send-email-gdb@mit.edu","subject":"Re: [PATCHv4 0/3] Updated patch series for providing mechanism to list available repositories","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2010-07-28T17:23:41Z","receivedAt":"2010-07-28T17:23:41Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Greg Brockman <gdb@MIT.EDU> writes:\n\n> This revised patch series should address all comments given thus far.\n> The specific changes made between v3 and v4 are documented in the\n> patch emails.\n\nThanks, all; will queue.\n"},{"id":"146669","messageId":"AANLkTim=iJUCZW+9Ttssnp=NpOpeQEhXsumCPw=HykX=@mail.gmail.com","threadId":"24546","inReplyTo":"1280302984-1752-2-git-send-email-gdb@mit.edu","subject":"Re: [PATCHv4 1/3] Allow creation of arbitrary git-shell commands","fromName":"Greg Brockman","fromEmail":"gdb@mit.edu","sentAt":"2010-07-29T00:31:01Z","receivedAt":"2010-07-29T00:31:01Z","isPatch":false,"sender":{"key":"gdb@mit.edu","avatar":"https://gravatar.com/avatar/3b907cf60d0cadf6d0659d5429d971b16965fb6b1e3f5acd26a6ba2974b45da4?d=mp&s=160"},"body":"This provides a mechanism for the server to expose custom\nfunctionality to clients.  My particular use case is that I would like\na way of discovering all repositories available for cloning.  A\nclient that clones via\n\n  git clone user@example.com\n\ncan invoke a command by\n\n  ssh user@example.com $command\n\nSigned-off-by: Greg Brockman <gdb@mit.edu>\n---\n shell.c |   44 ++++++++++++++++++++++++++++++++++++++++++--\n 1 files changed, 42 insertions(+), 2 deletions(-)\n\nAs discussed on-list, it should be fine to trust the value of HOME.\nThis patch differs from the previous only in the implementation of\ncd_to_homedir.\n\ndiff --git a/shell.c b/shell.c\nindex e4864e0..1e6077d 100644\n--- a/shell.c\n+++ b/shell.c\n@@ -3,6 +3,8 @@\n #include \"exec_cmd.h\"\n #include \"strbuf.h\"\n\n+#define COMMAND_DIR \"git-shell-commands\"\n+\n static int do_generic_cmd(const char *me, char *arg)\n {\n \tconst char *my_argv[4];\n@@ -33,6 +35,29 @@ static int do_cvs_cmd(const char *me, char *arg)\n \treturn execv_git_cmd(cvsserver_argv);\n }\n\n+static int is_valid_cmd_name(const char *cmd)\n+{\n+\t/* Test command contains no . or / characters */\n+\treturn cmd[strcspn(cmd, \"./\")] == '\\0';\n+}\n+\n+static char *make_cmd(const char *prog)\n+{\n+\tchar *prefix = xmalloc((strlen(prog) + strlen(COMMAND_DIR) + 2));\n+\tstrcpy(prefix, COMMAND_DIR);\n+\tstrcat(prefix, \"/\");\n+\tstrcat(prefix, prog);\n+\treturn prefix;\n+}\n+\n+static void cd_to_homedir(void)\n+{\n+\tchar *home = getenv(\"HOME\");\n+\tif (!home)\n+\t\tdie(\"could not determine user's home directory; HOME is unset\");\n+\tif (chdir(home) == -1)\n+\t\tdie(\"could not chdir to user's home directory\");\n+}\n\n static struct commands {\n \tconst char *name;\n@@ -48,6 +73,7 @@ static struct commands {\n int main(int argc, char **argv)\n {\n \tchar *prog;\n+\tconst char **user_argv;\n \tstruct commands *cmd;\n \tint devnull_fd;\n\n@@ -76,7 +102,7 @@ int main(int argc, char **argv)\n \telse if (argc != 3 || strcmp(argv[1], \"-c\"))\n \t\tdie(\"What do you think I am? A shell?\");\n\n-\tprog = argv[2];\n+\tprog = xstrdup(argv[2]);\n \tif (!strncmp(prog, \"git\", 3) && isspace(prog[3]))\n \t\t/* Accept \"git foo\" as if the caller said \"git-foo\". */\n \t\tprog[3] = '-';\n@@ -99,5 +125,19 @@ int main(int argc, char **argv)\n \t\t}\n \t\texit(cmd->exec(cmd->name, arg));\n \t}\n-\tdie(\"unrecognized command '%s'\", prog);\n+\n+\tcd_to_homedir();\n+\tif (split_cmdline(prog, &user_argv) != -1) {\n+\t\tif (is_valid_cmd_name(user_argv[0])) {\n+\t\t\tprog = make_cmd(user_argv[0]);\n+\t\t\tuser_argv[0] = prog;\n+\t\t\texecv(user_argv[0], (char *const *) user_argv);\n+\t\t}\n+\t\tfree(prog);\n+\t\tfree(user_argv);\n+\t\tdie(\"unrecognized command '%s'\", argv[2]);\n+\t} else {\n+\t\tfree(prog);\n+\t\tdie(\"invalid command format '%s'\", argv[2]);\n+\t}\n }\n-- \n1.7.0.4\n"},{"id":"146671","messageId":"AANLkTikKQb=WZQ=179+qNxn_1=5mJtuOnqb=Cdw78W4e@mail.gmail.com","threadId":"24546","inReplyTo":"AANLkTim=iJUCZW+9Ttssnp=NpOpeQEhXsumCPw=HykX=@mail.gmail.com","subject":"Re: [PATCHv4 1/3] Allow creation of arbitrary git-shell commands","fromName":"Greg Brockman","fromEmail":"gdb@mit.edu","sentAt":"2010-07-29T00:37:29Z","receivedAt":"2010-07-29T00:37:29Z","isPatch":false,"sender":{"key":"gdb@mit.edu","avatar":"https://gravatar.com/avatar/3b907cf60d0cadf6d0659d5429d971b16965fb6b1e3f5acd26a6ba2974b45da4?d=mp&s=160"},"body":"Err, actually it would probably be a good idea to\ns/char *home = getenv(\"HOME\");/const char *home = getenv(\"HOME\");/\n\nGreg\n\n\n\nOn Wed, Jul 28, 2010 at 5:31 PM, Greg Brockman <gdb@mit.edu> wrote:\n> This provides a mechanism for the server to expose custom\n> functionality to clients.  My particular use case is that I would like\n> a way of discovering all repositories available for cloning.  A\n> client that clones via\n>\n>  git clone user@example.com\n>\n> can invoke a command by\n>\n>  ssh user@example.com $command\n>\n> Signed-off-by: Greg Brockman <gdb@mit.edu>\n> ---\n>  shell.c |   44 ++++++++++++++++++++++++++++++++++++++++++--\n>  1 files changed, 42 insertions(+), 2 deletions(-)\n>\n> As discussed on-list, it should be fine to trust the value of HOME.\n> This patch differs from the previous only in the implementation of\n> cd_to_homedir.\n>\n> diff --git a/shell.c b/shell.c\n> index e4864e0..1e6077d 100644\n> --- a/shell.c\n> +++ b/shell.c\n> @@ -3,6 +3,8 @@\n>  #include \"exec_cmd.h\"\n>  #include \"strbuf.h\"\n>\n> +#define COMMAND_DIR \"git-shell-commands\"\n> +\n>  static int do_generic_cmd(const char *me, char *arg)\n>  {\n>        const char *my_argv[4];\n> @@ -33,6 +35,29 @@ static int do_cvs_cmd(const char *me, char *arg)\n>        return execv_git_cmd(cvsserver_argv);\n>  }\n>\n> +static int is_valid_cmd_name(const char *cmd)\n> +{\n> +       /* Test command contains no . or / characters */\n> +       return cmd[strcspn(cmd, \"./\")] == '\\0';\n> +}\n> +\n> +static char *make_cmd(const char *prog)\n> +{\n> +       char *prefix = xmalloc((strlen(prog) + strlen(COMMAND_DIR) + 2));\n> +       strcpy(prefix, COMMAND_DIR);\n> +       strcat(prefix, \"/\");\n> +       strcat(prefix, prog);\n> +       return prefix;\n> +}\n> +\n> +static void cd_to_homedir(void)\n> +{\n> +       char *home = getenv(\"HOME\");\n> +       if (!home)\n> +               die(\"could not determine user's home directory; HOME is unset\");\n> +       if (chdir(home) == -1)\n> +               die(\"could not chdir to user's home directory\");\n> +}\n>\n>  static struct commands {\n>        const char *name;\n> @@ -48,6 +73,7 @@ static struct commands {\n>  int main(int argc, char **argv)\n>  {\n>        char *prog;\n> +       const char **user_argv;\n>        struct commands *cmd;\n>        int devnull_fd;\n>\n> @@ -76,7 +102,7 @@ int main(int argc, char **argv)\n>        else if (argc != 3 || strcmp(argv[1], \"-c\"))\n>                die(\"What do you think I am? A shell?\");\n>\n> -       prog = argv[2];\n> +       prog = xstrdup(argv[2]);\n>        if (!strncmp(prog, \"git\", 3) && isspace(prog[3]))\n>                /* Accept \"git foo\" as if the caller said \"git-foo\". */\n>                prog[3] = '-';\n> @@ -99,5 +125,19 @@ int main(int argc, char **argv)\n>                }\n>                exit(cmd->exec(cmd->name, arg));\n>        }\n> -       die(\"unrecognized command '%s'\", prog);\n> +\n> +       cd_to_homedir();\n> +       if (split_cmdline(prog, &user_argv) != -1) {\n> +               if (is_valid_cmd_name(user_argv[0])) {\n> +                       prog = make_cmd(user_argv[0]);\n> +                       user_argv[0] = prog;\n> +                       execv(user_argv[0], (char *const *) user_argv);\n> +               }\n> +               free(prog);\n> +               free(user_argv);\n> +               die(\"unrecognized command '%s'\", argv[2]);\n> +       } else {\n> +               free(prog);\n> +               die(\"invalid command format '%s'\", argv[2]);\n> +       }\n>  }\n> --\n> 1.7.0.4\n>\n"}]}