diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Makefile | 1 | ||||
-rw-r--r-- | builtin-fetch--tool.c | 505 | ||||
-rw-r--r-- | builtin.h | 1 | ||||
-rwxr-xr-x | git-fetch.sh | 251 | ||||
-rwxr-xr-x | git-parse-remote.sh | 47 | ||||
-rw-r--r-- | git.c | 1 |
7 files changed, 558 insertions, 249 deletions
diff --git a/.gitignore b/.gitignore index 27797d1491..e8d2731ee5 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ git-diff-tree git-describe git-fast-import git-fetch +git-fetch--tool git-fetch-pack git-findtags git-fmt-merge-msg @@ -293,6 +293,7 @@ BUILTIN_OBJS = \ builtin-diff-files.o \ builtin-diff-index.o \ builtin-diff-tree.o \ + builtin-fetch--tool.o \ builtin-fmt-merge-msg.o \ builtin-for-each-ref.o \ builtin-fsck.o \ diff --git a/builtin-fetch--tool.c b/builtin-fetch--tool.c new file mode 100644 index 0000000000..e9d6764550 --- /dev/null +++ b/builtin-fetch--tool.c @@ -0,0 +1,505 @@ +#include "cache.h" +#include "refs.h" +#include "commit.h" + +#define CHUNK_SIZE 1024 + +static char *get_stdin(void) +{ + int offset = 0; + char *data = xmalloc(CHUNK_SIZE); + + while (1) { + int cnt = xread(0, data + offset, CHUNK_SIZE); + if (cnt < 0) + die("error reading standard input: %s", + strerror(errno)); + if (cnt == 0) { + data[offset] = 0; + break; + } + offset += cnt; + data = xrealloc(data, offset + CHUNK_SIZE); + } + return data; +} + +static void show_new(enum object_type type, unsigned char *sha1_new) +{ + fprintf(stderr, " %s: %s\n", typename(type), + find_unique_abbrev(sha1_new, DEFAULT_ABBREV)); +} + +static int update_ref(const char *action, + const char *refname, + unsigned char *sha1, + unsigned char *oldval) +{ + int len; + char msg[1024]; + char *rla = getenv("GIT_REFLOG_ACTION"); + static struct ref_lock *lock; + + if (!rla) + rla = "(reflog update)"; + len = snprintf(msg, sizeof(msg), "%s: %s", rla, action); + if (sizeof(msg) <= len) + die("insanely long action"); + lock = lock_any_ref_for_update(refname, oldval); + if (!lock) + return 1; + if (write_ref_sha1(lock, sha1, msg) < 0) + return 1; + return 0; +} + +static int update_local_ref(const char *name, + const char *new_head, + const char *note, + int verbose, int force) +{ + unsigned char sha1_old[20], sha1_new[20]; + char oldh[41], newh[41]; + struct commit *current, *updated; + enum object_type type; + + if (get_sha1_hex(new_head, sha1_new)) + die("malformed object name %s", new_head); + + type = sha1_object_info(sha1_new, NULL); + if (type < 0) + die("object %s not found", new_head); + + if (!*name) { + /* Not storing */ + if (verbose) { + fprintf(stderr, "* fetched %s\n", note); + show_new(type, sha1_new); + } + return 0; + } + + if (get_sha1(name, sha1_old)) { + char *msg; + just_store: + /* new ref */ + if (!strncmp(name, "refs/tags/", 10)) + msg = "storing tag"; + else + msg = "storing head"; + fprintf(stderr, "* %s: storing %s\n", + name, note); + show_new(type, sha1_new); + return update_ref(msg, name, sha1_new, NULL); + } + + if (!hashcmp(sha1_old, sha1_new)) { + if (verbose) { + fprintf(stderr, "* %s: same as %s\n", name, note); + show_new(type, sha1_new); + } + return 0; + } + + if (!strncmp(name, "refs/tags/", 10)) { + fprintf(stderr, "* %s: updating with %s\n", name, note); + show_new(type, sha1_new); + return update_ref("updating tag", name, sha1_new, NULL); + } + + current = lookup_commit_reference(sha1_old); + updated = lookup_commit_reference(sha1_new); + if (!current || !updated) + goto just_store; + + strcpy(oldh, find_unique_abbrev(current->object.sha1, DEFAULT_ABBREV)); + strcpy(newh, find_unique_abbrev(sha1_new, DEFAULT_ABBREV)); + + if (in_merge_bases(current, &updated, 1)) { + fprintf(stderr, "* %s: fast forward to %s\n", + name, note); + fprintf(stderr, " old..new: %s..%s\n", oldh, newh); + return update_ref("fast forward", name, sha1_new, sha1_old); + } + if (!force) { + fprintf(stderr, + "* %s: not updating to non-fast forward %s\n", + name, note); + fprintf(stderr, + " old...new: %s...%s\n", oldh, newh); + return 1; + } + fprintf(stderr, + "* %s: forcing update to non-fast forward %s\n", + name, note); + fprintf(stderr, " old...new: %s...%s\n", oldh, newh); + return update_ref("forced-update", name, sha1_new, sha1_old); +} + +static int append_fetch_head(FILE *fp, + const char *head, const char *remote, + const char *remote_name, const char *remote_nick, + const char *local_name, int not_for_merge, + int verbose, int force) +{ + struct commit *commit; + int remote_len, i, note_len; + unsigned char sha1[20]; + char note[1024]; + const char *what, *kind; + + if (get_sha1(head, sha1)) + return error("Not a valid object name: %s", head); + commit = lookup_commit_reference(sha1); + if (!commit) + not_for_merge = 1; + + if (!strcmp(remote_name, "HEAD")) { + kind = ""; + what = ""; + } + else if (!strncmp(remote_name, "refs/heads/", 11)) { + kind = "branch"; + what = remote_name + 11; + } + else if (!strncmp(remote_name, "refs/tags/", 10)) { + kind = "tag"; + what = remote_name + 10; + } + else if (!strncmp(remote_name, "refs/remotes/", 13)) { + kind = "remote branch"; + what = remote_name + 13; + } + else { + kind = ""; + what = remote_name; + } + + remote_len = strlen(remote); + for (i = remote_len - 1; remote[i] == '/' && 0 <= i; i--) + ; + remote_len = i + 1; + if (4 < i && !strncmp(".git", remote + i - 3, 4)) + remote_len = i - 3; + + note_len = 0; + if (*what) { + if (*kind) + note_len += sprintf(note + note_len, "%s ", kind); + note_len += sprintf(note + note_len, "'%s' of ", what); + } + note_len += sprintf(note + note_len, "%.*s", remote_len, remote); + fprintf(fp, "%s\t%s\t%s\n", + sha1_to_hex(commit ? commit->object.sha1 : sha1), + not_for_merge ? "not-for-merge" : "", + note); + return update_local_ref(local_name, head, note, verbose, force); +} + +static char *keep; +static void remove_keep(void) +{ + if (keep && *keep) + unlink(keep); +} + +static void remove_keep_on_signal(int signo) +{ + remove_keep(); + signal(SIGINT, SIG_DFL); + raise(signo); +} + +static char *find_local_name(const char *remote_name, const char *refs, + int *force_p, int *not_for_merge_p) +{ + const char *ref = refs; + int len = strlen(remote_name); + + while (ref) { + const char *next; + int single_force, not_for_merge; + + while (*ref == '\n') + ref++; + if (!*ref) + break; + next = strchr(ref, '\n'); + + single_force = not_for_merge = 0; + if (*ref == '+') { + single_force = 1; + ref++; + } + if (*ref == '.') { + not_for_merge = 1; + ref++; + if (*ref == '+') { + single_force = 1; + ref++; + } + } + if (!strncmp(remote_name, ref, len) && ref[len] == ':') { + const char *local_part = ref + len + 1; + char *ret; + int retlen; + + if (!next) + retlen = strlen(local_part); + else + retlen = next - local_part; + ret = xmalloc(retlen + 1); + memcpy(ret, local_part, retlen); + ret[retlen] = 0; + *force_p = single_force; + *not_for_merge_p = not_for_merge; + return ret; + } + ref = next; + } + return NULL; +} + +static int fetch_native_store(FILE *fp, + const char *remote, + const char *remote_nick, + const char *refs, + int verbose, int force) +{ + char buffer[1024]; + int err = 0; + + signal(SIGINT, remove_keep_on_signal); + atexit(remove_keep); + + while (fgets(buffer, sizeof(buffer), stdin)) { + int len; + char *cp; + char *local_name; + int single_force, not_for_merge; + + for (cp = buffer; *cp && !isspace(*cp); cp++) + ; + if (*cp) + *cp++ = 0; + len = strlen(cp); + if (len && cp[len-1] == '\n') + cp[--len] = 0; + if (!strcmp(buffer, "failed")) + die("Fetch failure: %s", remote); + if (!strcmp(buffer, "pack")) + continue; + if (!strcmp(buffer, "keep")) { + char *od = get_object_directory(); + int len = strlen(od) + strlen(cp) + 50; + keep = xmalloc(len); + sprintf(keep, "%s/pack/pack-%s.keep", od, cp); + continue; + } + + local_name = find_local_name(cp, refs, + &single_force, ¬_for_merge); + if (!local_name) + continue; + err |= append_fetch_head(fp, + buffer, remote, cp, remote_nick, + local_name, not_for_merge, + verbose, force || single_force); + } + return err; +} + +static int parse_reflist(const char *reflist) +{ + const char *ref; + + printf("refs='"); + for (ref = reflist; ref; ) { + const char *next; + while (*ref && isspace(*ref)) + ref++; + if (!*ref) + break; + for (next = ref; *next && !isspace(*next); next++) + ; + printf("\n%.*s", (int)(next - ref), ref); + ref = next; + } + printf("'\n"); + + printf("rref='"); + for (ref = reflist; ref; ) { + const char *next, *colon; + while (*ref && isspace(*ref)) + ref++; + if (!*ref) + break; + for (next = ref; *next && !isspace(*next); next++) + ; + if (*ref == '.') + ref++; + if (*ref == '+') + ref++; + colon = strchr(ref, ':'); + putchar('\n'); + printf("%.*s", (int)((colon ? colon : next) - ref), ref); + ref = next; + } + printf("'\n"); + return 0; +} + +static int expand_refs_wildcard(const char *ls_remote_result, int numrefs, + const char **refs) +{ + int i, matchlen, replacelen; + int found_one = 0; + const char *remote = *refs++; + numrefs--; + + if (numrefs == 0) { + fprintf(stderr, "Nothing specified for fetching with remote.%s.fetch\n", + remote); + printf("empty\n"); + } + + for (i = 0; i < numrefs; i++) { + const char *ref = refs[i]; + const char *lref = ref; + const char *colon; + const char *tail; + const char *ls; + const char *next; + + if (*lref == '+') + lref++; + colon = strchr(lref, ':'); + tail = lref + strlen(lref); + if (!(colon && + 2 < colon - lref && + colon[-1] == '*' && + colon[-2] == '/' && + 2 < tail - (colon + 1) && + tail[-1] == '*' && + tail[-2] == '/')) { + /* not a glob */ + if (!found_one++) + printf("explicit\n"); + printf("%s\n", ref); + continue; + } + + /* glob */ + if (!found_one++) + printf("glob\n"); + + /* lref to colon-2 is remote hierarchy name; + * colon+1 to tail-2 is local. + */ + matchlen = (colon-1) - lref; + replacelen = (tail-1) - (colon+1); + for (ls = ls_remote_result; ls; ls = next) { + const char *eol; + unsigned char sha1[20]; + int namelen; + + while (*ls && isspace(*ls)) + ls++; + next = strchr(ls, '\n'); + eol = !next ? (ls + strlen(ls)) : next; + if (!memcmp("^{}", eol-3, 3)) + continue; + if (eol - ls < 40) + continue; + if (get_sha1_hex(ls, sha1)) + continue; + ls += 40; + while (ls < eol && isspace(*ls)) + ls++; + /* ls to next (or eol) is the name. + * is it identical to lref to colon-2? + */ + if ((eol - ls) <= matchlen || + strncmp(ls, lref, matchlen)) + continue; + + /* Yes, it is a match */ + namelen = eol - ls; + if (lref != ref) + putchar('+'); + printf("%.*s:%.*s%.*s\n", + namelen, ls, + replacelen, colon + 1, + namelen - matchlen, ls + matchlen); + } + } + return 0; +} + +int cmd_fetch__tool(int argc, const char **argv, const char *prefix) +{ + int verbose = 0; + int force = 0; + + while (1 < argc) { + const char *arg = argv[1]; + if (!strcmp("-v", arg)) + verbose = 1; + else if (!strcmp("-f", arg)) + force = 1; + else + break; + argc--; + argv++; + } + + if (argc <= 1) + return error("Missing subcommand"); + + if (!strcmp("append-fetch-head", argv[1])) { + int result; + FILE *fp; + + if (argc != 8) + return error("append-fetch-head takes 6 args"); + fp = fopen(git_path("FETCH_HEAD"), "a"); + result = append_fetch_head(fp, argv[2], argv[3], + argv[4], argv[5], + argv[6], !!argv[7][0], + verbose, force); + fclose(fp); + return result; + } + if (!strcmp("native-store", argv[1])) { + int result; + FILE *fp; + + if (argc != 5) + return error("fetch-native-store takes 3 args"); + fp = fopen(git_path("FETCH_HEAD"), "a"); + result = fetch_native_store(fp, argv[2], argv[3], argv[4], + verbose, force); + fclose(fp); + return result; + } + if (!strcmp("parse-reflist", argv[1])) { + const char *reflist; + if (argc != 3) + return error("parse-reflist takes 1 arg"); + reflist = argv[2]; + if (!strcmp(reflist, "-")) + reflist = get_stdin(); + return parse_reflist(reflist); + } + if (!strcmp("expand-refs-wildcard", argv[1])) { + const char *reflist; + if (argc < 4) + return error("expand-refs-wildcard takes at least 2 args"); + reflist = argv[2]; + if (!strcmp(reflist, "-")) + reflist = get_stdin(); + return expand_refs_wildcard(reflist, argc - 3, argv + 3); + } + + return error("Unknown subcommand: %s", argv[1]); +} @@ -32,6 +32,7 @@ extern int cmd_diff_files(int argc, const char **argv, const char *prefix); extern int cmd_diff_index(int argc, const char **argv, const char *prefix); extern int cmd_diff(int argc, const char **argv, const char *prefix); extern int cmd_diff_tree(int argc, const char **argv, const char *prefix); +extern int cmd_fetch__tool(int argc, const char **argv, const char *prefix); extern int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix); extern int cmd_for_each_ref(int argc, const char **argv, const char *prefix); extern int cmd_format_patch(int argc, const char **argv, const char *prefix); diff --git a/git-fetch.sh b/git-fetch.sh index 5ae0d28cc0..9d45dd266a 100755 --- a/git-fetch.sh +++ b/git-fetch.sh @@ -109,133 +109,11 @@ ls_remote_result=$(git ls-remote $exec "$remote") || die "Cannot get the repository state from $remote" append_fetch_head () { - head_="$1" - remote_="$2" - remote_name_="$3" - remote_nick_="$4" - local_name_="$5" - case "$6" in - t) not_for_merge_='not-for-merge' ;; - '') not_for_merge_= ;; - esac - - # remote-nick is the URL given on the command line (or a shorthand) - # remote-name is the $GIT_DIR relative refs/ path we computed - # for this refspec. - - # the $note_ variable will be fed to git-fmt-merge-msg for further - # processing. - case "$remote_name_" in - HEAD) - note_= ;; - refs/heads/*) - note_="$(expr "$remote_name_" : 'refs/heads/\(.*\)')" - note_="branch '$note_' of " ;; - refs/tags/*) - note_="$(expr "$remote_name_" : 'refs/tags/\(.*\)')" - note_="tag '$note_' of " ;; - refs/remotes/*) - note_="$(expr "$remote_name_" : 'refs/remotes/\(.*\)')" - note_="remote branch '$note_' of " ;; - *) - note_="$remote_name of " ;; - esac - remote_1_=$(expr "z$remote_" : 'z\(.*\)\.git/*$') && - remote_="$remote_1_" - note_="$note_$remote_" - - # 2.6.11-tree tag would not be happy to be fed to resolve. - if git-cat-file commit "$head_" >/dev/null 2>&1 - then - headc_=$(git-rev-parse --verify "$head_^0") || exit - echo "$headc_ $not_for_merge_ $note_" >>"$GIT_DIR/FETCH_HEAD" - else - echo "$head_ not-for-merge $note_" >>"$GIT_DIR/FETCH_HEAD" - fi - - update_local_ref "$local_name_" "$head_" "$note_" -} - -update_local_ref () { - # If we are storing the head locally make sure that it is - # a fast forward (aka "reverse push"). - - label_=$(git-cat-file -t $2) - newshort_=$(git-rev-parse --short $2) - if test -z "$1" ; then - [ "$verbose" ] && echo >&2 "* fetched $3" - [ "$verbose" ] && echo >&2 " $label_: $newshort_" - return 0 - fi - oldshort_=$(git show-ref --hash --abbrev "$1" 2>/dev/null) - - case "$1" in - refs/tags/*) - # Tags need not be pointing at commits so there - # is no way to guarantee "fast-forward" anyway. - if test -n "$oldshort_" - then - if now_=$(git show-ref --hash "$1") && test "$now_" = "$2" - then - [ "$verbose" ] && echo >&2 "* $1: same as $3" - [ "$verbose" ] && echo >&2 " $label_: $newshort_" ||: - else - echo >&2 "* $1: updating with $3" - echo >&2 " $label_: $newshort_" - git-update-ref -m "$GIT_REFLOG_ACTION: updating tag" "$1" "$2" - fi - else - echo >&2 "* $1: storing $3" - echo >&2 " $label_: $newshort_" - git-update-ref -m "$GIT_REFLOG_ACTION: storing tag" "$1" "$2" - fi - ;; - - refs/heads/* | refs/remotes/*) - # $1 is the ref being updated. - # $2 is the new value for the ref. - local=$(git-rev-parse --verify "$1^0" 2>/dev/null) - if test "$local" - then - # Require fast-forward. - mb=$(git-merge-base "$local" "$2") && - case "$2,$mb" in - $local,*) - if test -n "$verbose" - then - echo >&2 "* $1: same as $3" - echo >&2 " $label_: $newshort_" - fi - ;; - *,$local) - echo >&2 "* $1: fast forward to $3" - echo >&2 " old..new: $oldshort_..$newshort_" - git-update-ref -m "$GIT_REFLOG_ACTION: fast-forward" "$1" "$2" "$local" - ;; - *) - false - ;; - esac || { - case ",$force,$single_force," in - *,t,*) - echo >&2 "* $1: forcing update to non-fast forward $3" - echo >&2 " old...new: $oldshort_...$newshort_" - git-update-ref -m "$GIT_REFLOG_ACTION: forced-update" "$1" "$2" "$local" - ;; - *) - echo >&2 "* $1: not updating to non-fast forward $3" - echo >&2 " old...new: $oldshort_...$newshort_" - exit 1 - ;; - esac - } - else - echo >&2 "* $1: storing $3" - echo >&2 " $label_: $newshort_" - git-update-ref -m "$GIT_REFLOG_ACTION: storing head" "$1" "$2" - fi - ;; - esac + flags= + test -n "$verbose" && flags="$flags -v" + test -n "$force" && flags="$flags -f" + GIT_REFLOG_ACTION="$GIT_REFLOG_ACTION" \ + git-fetch--tool $flags append-fetch-head "$@" } # updating the current HEAD with git-fetch in a bare @@ -279,7 +157,38 @@ then fi fi -fetch_main () { +fetch_native () { + + eval=$(echo "$1" | git-fetch--tool parse-reflist "-") + eval "$eval" + + ( : subshell because we muck with IFS + IFS=" $LF" + ( + if test -f "$remote" ; then + test -n "$shallow_depth" && + die "shallow clone with bundle is not supported" + git-bundle unbundle "$remote" $rref || + echo failed "$remote" + else + git-fetch-pack --thin $exec $keep $shallow_depth $no_progress \ + "$remote" $rref || + echo failed "$remote" + fi + ) | + ( + flags= + test -n "$verbose" && flags="$flags -v" + test -n "$force" && flags="$flags -f" + GIT_REFLOG_ACTION="$GIT_REFLOG_ACTION" \ + git-fetch--tool $flags native-store \ + "$remote" "$remote_nick" "$refs" + ) + ) || exit + +} + +fetch_dumb () { reflist="$1" refs= rref= @@ -371,9 +280,6 @@ fetch_main () { rsync_slurped_objects=t } ;; - *) - # We will do git native transport with just one call later. - continue ;; esac append_fetch_head "$head" "$remote" \ @@ -381,80 +287,17 @@ fetch_main () { done - case "$remote" in - http://* | https://* | ftp://* | rsync://* ) - ;; # we are already done. - *) - ( : subshell because we muck with IFS - IFS=" $LF" - ( - if test -f "$remote" ; then - test -n "$shallow_depth" && - die "shallow clone with bundle is not supported" - git-bundle unbundle "$remote" $rref || - echo failed "$remote" - else - git-fetch-pack --thin $exec $keep $shallow_depth $no_progress \ - "$remote" $rref || - echo failed "$remote" - fi - ) | - ( - trap ' - if test -n "$keepfile" && test -f "$keepfile" - then - rm -f "$keepfile" - fi - ' 0 - - keepfile= - while read sha1 remote_name - do - case "$sha1" in - failed) - echo >&2 "Fetch failure: $remote" - exit 1 ;; - # special line coming from index-pack with the pack name - pack) - continue ;; - keep) - keepfile="$GIT_OBJECT_DIRECTORY/pack/pack-$remote_name.keep" - continue ;; - esac - found= - single_force= - for ref in $refs - do - case "$ref" in - +$remote_name:*) - single_force=t - not_for_merge= - found="$ref" - break ;; - .+$remote_name:*) - single_force=t - not_for_merge=t - found="$ref" - break ;; - .$remote_name:*) - not_for_merge=t - found="$ref" - break ;; - $remote_name:*) - not_for_merge= - found="$ref" - break ;; - esac - done - local_name=$(expr "z$found" : 'z[^:]*:\(.*\)') - append_fetch_head "$sha1" "$remote" \ - "$remote_name" "$remote_nick" "$local_name" \ - "$not_for_merge" || exit - done - ) - ) || exit ;; - esac +} +fetch_main () { + case "$remote" in + http://* | https://* | ftp://* | rsync://* ) + fetch_dumb "$@" + ;; + *) + fetch_native "$@" + ;; + esac } fetch_main "$reflist" || exit diff --git a/git-parse-remote.sh b/git-parse-remote.sh index 5208ee6ce0..c46131f6d6 100755 --- a/git-parse-remote.sh +++ b/git-parse-remote.sh @@ -81,51 +81,8 @@ get_remote_default_refs_for_push () { # is to help prevent randomly "globbed" ref from being chosen as # a merge candidate expand_refs_wildcard () { - remote="$1" - shift - first_one=yes - if test "$#" = 0 - then - echo empty - echo >&2 "Nothing specified for fetching with remote.$remote.fetch" - fi - for ref - do - lref=${ref#'+'} - # a non glob pattern is given back as-is. - expr "z$lref" : 'zrefs/.*/\*:refs/.*/\*$' >/dev/null || { - if test -n "$first_one" - then - echo "explicit" - first_one= - fi - echo "$ref" - continue - } - - # glob - if test -n "$first_one" - then - echo "glob" - first_one= - fi - from=`expr "z$lref" : 'z\(refs/.*/\)\*:refs/.*/\*$'` - to=`expr "z$lref" : 'zrefs/.*/\*:\(refs/.*/\)\*$'` - local_force= - test "z$lref" = "z$ref" || local_force='+' - echo "$ls_remote_result" | - sed -e '/\^{}$/d' | - ( - IFS=' ' - while read sha1 name - do - # ignore the ones that do not start with $from - mapped=${name#"$from"} - test "z$name" = "z$mapped" && continue - echo "${local_force}${name}:${to}${mapped}" - done - ) - done + echo "$ls_remote_result" | + git fetch--tool expand-refs-wildcard "-" "$@" } # Subroutine to canonicalize remote:local notation. @@ -243,6 +243,7 @@ static void handle_internal_command(int argc, const char **argv, char **envp) { "diff-files", cmd_diff_files }, { "diff-index", cmd_diff_index, RUN_SETUP }, { "diff-tree", cmd_diff_tree, RUN_SETUP }, + { "fetch--tool", cmd_fetch__tool, RUN_SETUP }, { "fmt-merge-msg", cmd_fmt_merge_msg, RUN_SETUP }, { "for-each-ref", cmd_for_each_ref, RUN_SETUP }, { "format-patch", cmd_format_patch, RUN_SETUP }, |