From 4dbdc42d9e7c3968ff7f690d00680419c9b8cb0f Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Tue, 9 Apr 2024 15:34:27 +0200 Subject: Adding upstream version 1:2.43.0. Signed-off-by: Daniel Baumann --- builtin/add.c | 576 +++++ builtin/am.c | 2521 ++++++++++++++++++++ builtin/annotate.c | 22 + builtin/apply.c | 34 + builtin/archive.c | 112 + builtin/bisect.c | 1449 ++++++++++++ builtin/blame.c | 1232 ++++++++++ builtin/branch.c | 986 ++++++++ builtin/bugreport.c | 200 ++ builtin/bundle.c | 240 ++ builtin/cat-file.c | 1075 +++++++++ builtin/check-attr.c | 205 ++ builtin/check-ignore.c | 199 ++ builtin/check-mailmap.c | 71 + builtin/check-ref-format.c | 98 + builtin/checkout--worker.c | 147 ++ builtin/checkout-index.c | 346 +++ builtin/checkout.c | 2007 ++++++++++++++++ builtin/clean.c | 1105 +++++++++ builtin/clone.c | 1448 ++++++++++++ builtin/column.c | 60 + builtin/commit-graph.c | 355 +++ builtin/commit-tree.c | 145 ++ builtin/commit.c | 1895 +++++++++++++++ builtin/config.c | 1002 ++++++++ builtin/count-objects.c | 176 ++ builtin/credential-cache--daemon.c | 331 +++ builtin/credential-cache.c | 187 ++ builtin/credential-store.c | 222 ++ builtin/credential.c | 34 + builtin/describe.c | 708 ++++++ builtin/diagnose.c | 65 + builtin/diff-files.c | 89 + builtin/diff-index.c | 79 + builtin/diff-tree.c | 236 ++ builtin/diff.c | 617 +++++ builtin/difftool.c | 794 +++++++ builtin/fast-export.c | 1301 +++++++++++ builtin/fast-import.c | 3659 +++++++++++++++++++++++++++++ builtin/fetch-pack.c | 280 +++ builtin/fetch.c | 2499 ++++++++++++++++++++ builtin/fmt-merge-msg.c | 70 + builtin/for-each-ref.c | 128 + builtin/for-each-repo.c | 62 + builtin/fsck.c | 1105 +++++++++ builtin/fsmonitor--daemon.c | 1597 +++++++++++++ builtin/gc.c | 2793 ++++++++++++++++++++++ builtin/get-tar-commit-id.c | 53 + builtin/grep.c | 1274 ++++++++++ builtin/hash-object.c | 174 ++ builtin/help.c | 726 ++++++ builtin/hook.c | 82 + builtin/index-pack.c | 1977 ++++++++++++++++ builtin/init-db.c | 240 ++ builtin/interpret-trailers.c | 145 ++ builtin/log.c | 2544 ++++++++++++++++++++ builtin/ls-files.c | 774 ++++++ builtin/ls-remote.c | 165 ++ builtin/ls-tree.c | 452 ++++ builtin/mailinfo.c | 116 + builtin/mailsplit.c | 372 +++ builtin/merge-base.c | 196 ++ builtin/merge-file.c | 156 ++ builtin/merge-index.c | 122 + builtin/merge-ours.c | 33 + builtin/merge-recursive.c | 96 + builtin/merge-tree.c | 655 ++++++ builtin/merge.c | 1786 ++++++++++++++ builtin/mktag.c | 113 + builtin/mktree.c | 203 ++ builtin/multi-pack-index.c | 291 +++ builtin/mv.c | 576 +++++ builtin/name-rev.c | 681 ++++++ builtin/notes.c | 1138 +++++++++ builtin/pack-objects.c | 4529 ++++++++++++++++++++++++++++++++++++ builtin/pack-redundant.c | 675 ++++++ builtin/pack-refs.c | 48 + builtin/patch-id.c | 243 ++ builtin/prune-packed.c | 32 + builtin/prune.c | 212 ++ builtin/pull.c | 1160 +++++++++ builtin/push.c | 708 ++++++ builtin/range-diff.c | 159 ++ builtin/read-tree.c | 289 +++ builtin/rebase.c | 1860 +++++++++++++++ builtin/receive-pack.c | 2628 +++++++++++++++++++++ builtin/reflog.c | 435 ++++ builtin/remote-ext.c | 206 ++ builtin/remote-fd.c | 84 + builtin/remote.c | 1788 ++++++++++++++ builtin/repack.c | 1538 ++++++++++++ builtin/replace.c | 634 +++++ builtin/rerere.c | 119 + builtin/reset.c | 522 +++++ builtin/rev-list.c | 801 +++++++ builtin/rev-parse.c | 1112 +++++++++ builtin/revert.c | 256 ++ builtin/rm.c | 444 ++++ builtin/send-pack.c | 347 +++ builtin/shortlog.c | 521 +++++ builtin/show-branch.c | 972 ++++++++ builtin/show-index.c | 111 + builtin/show-ref.c | 330 +++ builtin/sparse-checkout.c | 1048 +++++++++ builtin/stash.c | 1887 +++++++++++++++ builtin/stripspace.c | 69 + builtin/submodule--helper.c | 3429 +++++++++++++++++++++++++++ builtin/symbolic-ref.c | 88 + builtin/tag.c | 670 ++++++ builtin/unpack-file.c | 41 + builtin/unpack-objects.c | 696 ++++++ builtin/update-index.c | 1250 ++++++++++ builtin/update-ref.c | 582 +++++ builtin/update-server-info.c | 27 + builtin/upload-archive.c | 137 ++ builtin/upload-pack.c | 77 + builtin/var.c | 239 ++ builtin/verify-commit.c | 83 + builtin/verify-pack.c | 91 + builtin/verify-tag.c | 70 + builtin/worktree.c | 1416 +++++++++++ builtin/write-tree.c | 64 + 122 files changed, 84429 insertions(+) create mode 100644 builtin/add.c create mode 100644 builtin/am.c create mode 100644 builtin/annotate.c create mode 100644 builtin/apply.c create mode 100644 builtin/archive.c create mode 100644 builtin/bisect.c create mode 100644 builtin/blame.c create mode 100644 builtin/branch.c create mode 100644 builtin/bugreport.c create mode 100644 builtin/bundle.c create mode 100644 builtin/cat-file.c create mode 100644 builtin/check-attr.c create mode 100644 builtin/check-ignore.c create mode 100644 builtin/check-mailmap.c create mode 100644 builtin/check-ref-format.c create mode 100644 builtin/checkout--worker.c create mode 100644 builtin/checkout-index.c create mode 100644 builtin/checkout.c create mode 100644 builtin/clean.c create mode 100644 builtin/clone.c create mode 100644 builtin/column.c create mode 100644 builtin/commit-graph.c create mode 100644 builtin/commit-tree.c create mode 100644 builtin/commit.c create mode 100644 builtin/config.c create mode 100644 builtin/count-objects.c create mode 100644 builtin/credential-cache--daemon.c create mode 100644 builtin/credential-cache.c create mode 100644 builtin/credential-store.c create mode 100644 builtin/credential.c create mode 100644 builtin/describe.c create mode 100644 builtin/diagnose.c create mode 100644 builtin/diff-files.c create mode 100644 builtin/diff-index.c create mode 100644 builtin/diff-tree.c create mode 100644 builtin/diff.c create mode 100644 builtin/difftool.c create mode 100644 builtin/fast-export.c create mode 100644 builtin/fast-import.c create mode 100644 builtin/fetch-pack.c create mode 100644 builtin/fetch.c create mode 100644 builtin/fmt-merge-msg.c create mode 100644 builtin/for-each-ref.c create mode 100644 builtin/for-each-repo.c create mode 100644 builtin/fsck.c create mode 100644 builtin/fsmonitor--daemon.c create mode 100644 builtin/gc.c create mode 100644 builtin/get-tar-commit-id.c create mode 100644 builtin/grep.c create mode 100644 builtin/hash-object.c create mode 100644 builtin/help.c create mode 100644 builtin/hook.c create mode 100644 builtin/index-pack.c create mode 100644 builtin/init-db.c create mode 100644 builtin/interpret-trailers.c create mode 100644 builtin/log.c create mode 100644 builtin/ls-files.c create mode 100644 builtin/ls-remote.c create mode 100644 builtin/ls-tree.c create mode 100644 builtin/mailinfo.c create mode 100644 builtin/mailsplit.c create mode 100644 builtin/merge-base.c create mode 100644 builtin/merge-file.c create mode 100644 builtin/merge-index.c create mode 100644 builtin/merge-ours.c create mode 100644 builtin/merge-recursive.c create mode 100644 builtin/merge-tree.c create mode 100644 builtin/merge.c create mode 100644 builtin/mktag.c create mode 100644 builtin/mktree.c create mode 100644 builtin/multi-pack-index.c create mode 100644 builtin/mv.c create mode 100644 builtin/name-rev.c create mode 100644 builtin/notes.c create mode 100644 builtin/pack-objects.c create mode 100644 builtin/pack-redundant.c create mode 100644 builtin/pack-refs.c create mode 100644 builtin/patch-id.c create mode 100644 builtin/prune-packed.c create mode 100644 builtin/prune.c create mode 100644 builtin/pull.c create mode 100644 builtin/push.c create mode 100644 builtin/range-diff.c create mode 100644 builtin/read-tree.c create mode 100644 builtin/rebase.c create mode 100644 builtin/receive-pack.c create mode 100644 builtin/reflog.c create mode 100644 builtin/remote-ext.c create mode 100644 builtin/remote-fd.c create mode 100644 builtin/remote.c create mode 100644 builtin/repack.c create mode 100644 builtin/replace.c create mode 100644 builtin/rerere.c create mode 100644 builtin/reset.c create mode 100644 builtin/rev-list.c create mode 100644 builtin/rev-parse.c create mode 100644 builtin/revert.c create mode 100644 builtin/rm.c create mode 100644 builtin/send-pack.c create mode 100644 builtin/shortlog.c create mode 100644 builtin/show-branch.c create mode 100644 builtin/show-index.c create mode 100644 builtin/show-ref.c create mode 100644 builtin/sparse-checkout.c create mode 100644 builtin/stash.c create mode 100644 builtin/stripspace.c create mode 100644 builtin/submodule--helper.c create mode 100644 builtin/symbolic-ref.c create mode 100644 builtin/tag.c create mode 100644 builtin/unpack-file.c create mode 100644 builtin/unpack-objects.c create mode 100644 builtin/update-index.c create mode 100644 builtin/update-ref.c create mode 100644 builtin/update-server-info.c create mode 100644 builtin/upload-archive.c create mode 100644 builtin/upload-pack.c create mode 100644 builtin/var.c create mode 100644 builtin/verify-commit.c create mode 100644 builtin/verify-pack.c create mode 100644 builtin/verify-tag.c create mode 100644 builtin/worktree.c create mode 100644 builtin/write-tree.c (limited to 'builtin') diff --git a/builtin/add.c b/builtin/add.c new file mode 100644 index 0000000..5126d2e --- /dev/null +++ b/builtin/add.c @@ -0,0 +1,576 @@ +/* + * "git add" builtin command + * + * Copyright (C) 2006 Linus Torvalds + */ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "advice.h" +#include "config.h" +#include "lockfile.h" +#include "editor.h" +#include "dir.h" +#include "gettext.h" +#include "pathspec.h" +#include "exec-cmd.h" +#include "cache-tree.h" +#include "run-command.h" +#include "parse-options.h" +#include "path.h" +#include "preload-index.h" +#include "diff.h" +#include "diffcore.h" +#include "read-cache.h" +#include "repository.h" +#include "revision.h" +#include "bulk-checkin.h" +#include "strvec.h" +#include "submodule.h" +#include "add-interactive.h" + +static const char * const builtin_add_usage[] = { + N_("git add [] [--] ..."), + NULL +}; +static int patch_interactive, add_interactive, edit_interactive; +static int take_worktree_changes; +static int add_renormalize; +static int pathspec_file_nul; +static int include_sparse; +static const char *pathspec_from_file; + +static int chmod_pathspec(struct pathspec *pathspec, char flip, int show_only) +{ + int i, ret = 0; + + for (i = 0; i < the_index.cache_nr; i++) { + struct cache_entry *ce = the_index.cache[i]; + int err; + + if (!include_sparse && + (ce_skip_worktree(ce) || + !path_in_sparse_checkout(ce->name, &the_index))) + continue; + + if (pathspec && !ce_path_match(&the_index, ce, pathspec, NULL)) + continue; + + if (!show_only) + err = chmod_index_entry(&the_index, ce, flip); + else + err = S_ISREG(ce->ce_mode) ? 0 : -1; + + if (err < 0) + ret = error(_("cannot chmod %cx '%s'"), flip, ce->name); + } + + return ret; +} + +static int renormalize_tracked_files(const struct pathspec *pathspec, int flags) +{ + int i, retval = 0; + + for (i = 0; i < the_index.cache_nr; i++) { + struct cache_entry *ce = the_index.cache[i]; + + if (!include_sparse && + (ce_skip_worktree(ce) || + !path_in_sparse_checkout(ce->name, &the_index))) + continue; + if (ce_stage(ce)) + continue; /* do not touch unmerged paths */ + if (!S_ISREG(ce->ce_mode) && !S_ISLNK(ce->ce_mode)) + continue; /* do not touch non blobs */ + if (pathspec && !ce_path_match(&the_index, ce, pathspec, NULL)) + continue; + retval |= add_file_to_index(&the_index, ce->name, + flags | ADD_CACHE_RENORMALIZE); + } + + return retval; +} + +static char *prune_directory(struct dir_struct *dir, struct pathspec *pathspec, int prefix) +{ + char *seen; + int i; + struct dir_entry **src, **dst; + + seen = xcalloc(pathspec->nr, 1); + + src = dst = dir->entries; + i = dir->nr; + while (--i >= 0) { + struct dir_entry *entry = *src++; + if (dir_path_match(&the_index, entry, pathspec, prefix, seen)) + *dst++ = entry; + } + dir->nr = dst - dir->entries; + add_pathspec_matches_against_index(pathspec, &the_index, seen, + PS_IGNORE_SKIP_WORKTREE); + return seen; +} + +static int refresh(int verbose, const struct pathspec *pathspec) +{ + char *seen; + int i, ret = 0; + char *skip_worktree_seen = NULL; + struct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP; + int flags = REFRESH_IGNORE_SKIP_WORKTREE | + (verbose ? REFRESH_IN_PORCELAIN : REFRESH_QUIET); + + seen = xcalloc(pathspec->nr, 1); + refresh_index(&the_index, flags, pathspec, seen, + _("Unstaged changes after refreshing the index:")); + for (i = 0; i < pathspec->nr; i++) { + if (!seen[i]) { + const char *path = pathspec->items[i].original; + + if (matches_skip_worktree(pathspec, i, &skip_worktree_seen) || + !path_in_sparse_checkout(path, &the_index)) { + string_list_append(&only_match_skip_worktree, + pathspec->items[i].original); + } else { + die(_("pathspec '%s' did not match any files"), + pathspec->items[i].original); + } + } + } + + if (only_match_skip_worktree.nr) { + advise_on_updating_sparse_paths(&only_match_skip_worktree); + ret = 1; + } + + free(seen); + free(skip_worktree_seen); + string_list_clear(&only_match_skip_worktree, 0); + return ret; +} + +int interactive_add(const char **argv, const char *prefix, int patch) +{ + struct pathspec pathspec; + int unused; + + if (!git_config_get_bool("add.interactive.usebuiltin", &unused)) + warning(_("the add.interactive.useBuiltin setting has been removed!\n" + "See its entry in 'git help config' for details.")); + + parse_pathspec(&pathspec, 0, + PATHSPEC_PREFER_FULL | + PATHSPEC_SYMLINK_LEADING_PATH | + PATHSPEC_PREFIX_ORIGIN, + prefix, argv); + + if (patch) + return !!run_add_p(the_repository, ADD_P_ADD, NULL, &pathspec); + else + return !!run_add_i(the_repository, &pathspec); +} + +static int edit_patch(int argc, const char **argv, const char *prefix) +{ + char *file = git_pathdup("ADD_EDIT.patch"); + struct child_process child = CHILD_PROCESS_INIT; + struct rev_info rev; + int out; + struct stat st; + + git_config(git_diff_basic_config, NULL); /* no "diff" UI options */ + + if (repo_read_index(the_repository) < 0) + die(_("could not read the index")); + + repo_init_revisions(the_repository, &rev, prefix); + rev.diffopt.context = 7; + + argc = setup_revisions(argc, argv, &rev, NULL); + rev.diffopt.output_format = DIFF_FORMAT_PATCH; + rev.diffopt.use_color = 0; + rev.diffopt.flags.ignore_dirty_submodules = 1; + out = xopen(file, O_CREAT | O_WRONLY | O_TRUNC, 0666); + rev.diffopt.file = xfdopen(out, "w"); + rev.diffopt.close_file = 1; + run_diff_files(&rev, 0); + + if (launch_editor(file, NULL, NULL)) + die(_("editing patch failed")); + + if (stat(file, &st)) + die_errno(_("could not stat '%s'"), file); + if (!st.st_size) + die(_("empty patch. aborted")); + + child.git_cmd = 1; + strvec_pushl(&child.args, "apply", "--recount", "--cached", file, + NULL); + if (run_command(&child)) + die(_("could not apply '%s'"), file); + + unlink(file); + free(file); + release_revisions(&rev); + return 0; +} + +static const char ignore_error[] = +N_("The following paths are ignored by one of your .gitignore files:\n"); + +static int verbose, show_only, ignored_too, refresh_only; +static int ignore_add_errors, intent_to_add, ignore_missing; +static int warn_on_embedded_repo = 1; + +#define ADDREMOVE_DEFAULT 1 +static int addremove = ADDREMOVE_DEFAULT; +static int addremove_explicit = -1; /* unspecified */ + +static char *chmod_arg; + +static int ignore_removal_cb(const struct option *opt, const char *arg, int unset) +{ + BUG_ON_OPT_ARG(arg); + + /* if we are told to ignore, we are not adding removals */ + *(int *)opt->value = !unset ? 0 : 1; + return 0; +} + +static struct option builtin_add_options[] = { + OPT__DRY_RUN(&show_only, N_("dry run")), + OPT__VERBOSE(&verbose, N_("be verbose")), + OPT_GROUP(""), + OPT_BOOL('i', "interactive", &add_interactive, N_("interactive picking")), + OPT_BOOL('p', "patch", &patch_interactive, N_("select hunks interactively")), + OPT_BOOL('e', "edit", &edit_interactive, N_("edit current diff and apply")), + OPT__FORCE(&ignored_too, N_("allow adding otherwise ignored files"), 0), + OPT_BOOL('u', "update", &take_worktree_changes, N_("update tracked files")), + OPT_BOOL(0, "renormalize", &add_renormalize, N_("renormalize EOL of tracked files (implies -u)")), + OPT_BOOL('N', "intent-to-add", &intent_to_add, N_("record only the fact that the path will be added later")), + OPT_BOOL('A', "all", &addremove_explicit, N_("add changes from all tracked and untracked files")), + OPT_CALLBACK_F(0, "ignore-removal", &addremove_explicit, + NULL /* takes no arguments */, + N_("ignore paths removed in the working tree (same as --no-all)"), + PARSE_OPT_NOARG, ignore_removal_cb), + OPT_BOOL( 0 , "refresh", &refresh_only, N_("don't add, only refresh the index")), + OPT_BOOL( 0 , "ignore-errors", &ignore_add_errors, N_("just skip files which cannot be added because of errors")), + OPT_BOOL( 0 , "ignore-missing", &ignore_missing, N_("check if - even missing - files are ignored in dry run")), + OPT_BOOL(0, "sparse", &include_sparse, N_("allow updating entries outside of the sparse-checkout cone")), + OPT_STRING(0, "chmod", &chmod_arg, "(+|-)x", + N_("override the executable bit of the listed files")), + OPT_HIDDEN_BOOL(0, "warn-embedded-repo", &warn_on_embedded_repo, + N_("warn when adding an embedded repository")), + OPT_PATHSPEC_FROM_FILE(&pathspec_from_file), + OPT_PATHSPEC_FILE_NUL(&pathspec_file_nul), + OPT_END(), +}; + +static int add_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + if (!strcmp(var, "add.ignoreerrors") || + !strcmp(var, "add.ignore-errors")) { + ignore_add_errors = git_config_bool(var, value); + return 0; + } + + if (git_color_config(var, value, cb) < 0) + return -1; + + return git_default_config(var, value, ctx, cb); +} + +static const char embedded_advice[] = N_( +"You've added another git repository inside your current repository.\n" +"Clones of the outer repository will not contain the contents of\n" +"the embedded repository and will not know how to obtain it.\n" +"If you meant to add a submodule, use:\n" +"\n" +" git submodule add %s\n" +"\n" +"If you added this path by mistake, you can remove it from the\n" +"index with:\n" +"\n" +" git rm --cached %s\n" +"\n" +"See \"git help submodule\" for more information." +); + +static void check_embedded_repo(const char *path) +{ + struct strbuf name = STRBUF_INIT; + static int adviced_on_embedded_repo = 0; + + if (!warn_on_embedded_repo) + return; + if (!ends_with(path, "/")) + return; + + /* Drop trailing slash for aesthetics */ + strbuf_addstr(&name, path); + strbuf_strip_suffix(&name, "/"); + + warning(_("adding embedded git repository: %s"), name.buf); + if (!adviced_on_embedded_repo && + advice_enabled(ADVICE_ADD_EMBEDDED_REPO)) { + advise(embedded_advice, name.buf, name.buf); + adviced_on_embedded_repo = 1; + } + + strbuf_release(&name); +} + +static int add_files(struct dir_struct *dir, int flags) +{ + int i, exit_status = 0; + struct string_list matched_sparse_paths = STRING_LIST_INIT_NODUP; + + if (dir->ignored_nr) { + fprintf(stderr, _(ignore_error)); + for (i = 0; i < dir->ignored_nr; i++) + fprintf(stderr, "%s\n", dir->ignored[i]->name); + if (advice_enabled(ADVICE_ADD_IGNORED_FILE)) + advise(_("Use -f if you really want to add them.\n" + "Turn this message off by running\n" + "\"git config advice.addIgnoredFile false\"")); + exit_status = 1; + } + + for (i = 0; i < dir->nr; i++) { + if (!include_sparse && + !path_in_sparse_checkout(dir->entries[i]->name, &the_index)) { + string_list_append(&matched_sparse_paths, + dir->entries[i]->name); + continue; + } + if (add_file_to_index(&the_index, dir->entries[i]->name, flags)) { + if (!ignore_add_errors) + die(_("adding files failed")); + exit_status = 1; + } else { + check_embedded_repo(dir->entries[i]->name); + } + } + + if (matched_sparse_paths.nr) { + advise_on_updating_sparse_paths(&matched_sparse_paths); + exit_status = 1; + } + + string_list_clear(&matched_sparse_paths, 0); + + return exit_status; +} + +int cmd_add(int argc, const char **argv, const char *prefix) +{ + int exit_status = 0; + struct pathspec pathspec; + struct dir_struct dir = DIR_INIT; + int flags; + int add_new_files; + int require_pathspec; + char *seen = NULL; + struct lock_file lock_file = LOCK_INIT; + + git_config(add_config, NULL); + + argc = parse_options(argc, argv, prefix, builtin_add_options, + builtin_add_usage, PARSE_OPT_KEEP_ARGV0); + if (patch_interactive) + add_interactive = 1; + if (add_interactive) { + if (show_only) + die(_("options '%s' and '%s' cannot be used together"), "--dry-run", "--interactive/--patch"); + if (pathspec_from_file) + die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--interactive/--patch"); + exit(interactive_add(argv + 1, prefix, patch_interactive)); + } + + if (edit_interactive) { + if (pathspec_from_file) + die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--edit"); + return(edit_patch(argc, argv, prefix)); + } + argc--; + argv++; + + if (0 <= addremove_explicit) + addremove = addremove_explicit; + else if (take_worktree_changes && ADDREMOVE_DEFAULT) + addremove = 0; /* "-u" was given but not "-A" */ + + if (addremove && take_worktree_changes) + die(_("options '%s' and '%s' cannot be used together"), "-A", "-u"); + + if (!show_only && ignore_missing) + die(_("the option '%s' requires '%s'"), "--ignore-missing", "--dry-run"); + + if (chmod_arg && ((chmod_arg[0] != '-' && chmod_arg[0] != '+') || + chmod_arg[1] != 'x' || chmod_arg[2])) + die(_("--chmod param '%s' must be either -x or +x"), chmod_arg); + + add_new_files = !take_worktree_changes && !refresh_only && !add_renormalize; + require_pathspec = !(take_worktree_changes || (0 < addremove_explicit)); + + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR); + + /* + * Check the "pathspec '%s' did not match any files" block + * below before enabling new magic. + */ + parse_pathspec(&pathspec, PATHSPEC_ATTR, + PATHSPEC_PREFER_FULL | + PATHSPEC_SYMLINK_LEADING_PATH, + prefix, argv); + + if (pathspec_from_file) { + if (pathspec.nr) + die(_("'%s' and pathspec arguments cannot be used together"), "--pathspec-from-file"); + + parse_pathspec_file(&pathspec, PATHSPEC_ATTR, + PATHSPEC_PREFER_FULL | + PATHSPEC_SYMLINK_LEADING_PATH, + prefix, pathspec_from_file, pathspec_file_nul); + } else if (pathspec_file_nul) { + die(_("the option '%s' requires '%s'"), "--pathspec-file-nul", "--pathspec-from-file"); + } + + if (require_pathspec && pathspec.nr == 0) { + fprintf(stderr, _("Nothing specified, nothing added.\n")); + if (advice_enabled(ADVICE_ADD_EMPTY_PATHSPEC)) + advise( _("Maybe you wanted to say 'git add .'?\n" + "Turn this message off by running\n" + "\"git config advice.addEmptyPathspec false\"")); + return 0; + } + + if (!take_worktree_changes && addremove_explicit < 0 && pathspec.nr) + /* Turn "git add pathspec..." to "git add -A pathspec..." */ + addremove = 1; + + flags = ((verbose ? ADD_CACHE_VERBOSE : 0) | + (show_only ? ADD_CACHE_PRETEND : 0) | + (intent_to_add ? ADD_CACHE_INTENT : 0) | + (ignore_add_errors ? ADD_CACHE_IGNORE_ERRORS : 0) | + (!(addremove || take_worktree_changes) + ? ADD_CACHE_IGNORE_REMOVAL : 0)); + + if (repo_read_index_preload(the_repository, &pathspec, 0) < 0) + die(_("index file corrupt")); + + die_in_unpopulated_submodule(&the_index, prefix); + die_path_inside_submodule(&the_index, &pathspec); + + if (add_new_files) { + int baselen; + + /* Set up the default git porcelain excludes */ + if (!ignored_too) { + dir.flags |= DIR_COLLECT_IGNORED; + setup_standard_excludes(&dir); + } + + /* This picks up the paths that are not tracked */ + baselen = fill_directory(&dir, &the_index, &pathspec); + if (pathspec.nr) + seen = prune_directory(&dir, &pathspec, baselen); + } + + if (refresh_only) { + exit_status |= refresh(verbose, &pathspec); + goto finish; + } + + if (pathspec.nr) { + int i; + char *skip_worktree_seen = NULL; + struct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP; + + if (!seen) + seen = find_pathspecs_matching_against_index(&pathspec, + &the_index, PS_IGNORE_SKIP_WORKTREE); + + /* + * file_exists() assumes exact match + */ + GUARD_PATHSPEC(&pathspec, + PATHSPEC_FROMTOP | + PATHSPEC_LITERAL | + PATHSPEC_GLOB | + PATHSPEC_ICASE | + PATHSPEC_EXCLUDE); + + for (i = 0; i < pathspec.nr; i++) { + const char *path = pathspec.items[i].match; + + if (pathspec.items[i].magic & PATHSPEC_EXCLUDE) + continue; + if (seen[i]) + continue; + + if (!include_sparse && + matches_skip_worktree(&pathspec, i, &skip_worktree_seen)) { + string_list_append(&only_match_skip_worktree, + pathspec.items[i].original); + continue; + } + + /* Don't complain at 'git add .' on empty repo */ + if (!path[0]) + continue; + + if ((pathspec.items[i].magic & (PATHSPEC_GLOB | PATHSPEC_ICASE)) || + !file_exists(path)) { + if (ignore_missing) { + int dtype = DT_UNKNOWN; + if (is_excluded(&dir, &the_index, path, &dtype)) + dir_add_ignored(&dir, &the_index, + path, pathspec.items[i].len); + } else + die(_("pathspec '%s' did not match any files"), + pathspec.items[i].original); + } + } + + + if (only_match_skip_worktree.nr) { + advise_on_updating_sparse_paths(&only_match_skip_worktree); + exit_status = 1; + } + + free(seen); + free(skip_worktree_seen); + string_list_clear(&only_match_skip_worktree, 0); + } + + begin_odb_transaction(); + + if (add_renormalize) + exit_status |= renormalize_tracked_files(&pathspec, flags); + else + exit_status |= add_files_to_cache(the_repository, prefix, + &pathspec, include_sparse, + flags); + + if (add_new_files) + exit_status |= add_files(&dir, flags); + + if (chmod_arg && pathspec.nr) + exit_status |= chmod_pathspec(&pathspec, chmod_arg[0], show_only); + end_odb_transaction(); + +finish: + if (write_locked_index(&the_index, &lock_file, + COMMIT_LOCK | SKIP_IF_UNCHANGED)) + die(_("unable to write new index file")); + + dir_clear(&dir); + clear_pathspec(&pathspec); + return exit_status; +} diff --git a/builtin/am.c b/builtin/am.c new file mode 100644 index 0000000..9f084d5 --- /dev/null +++ b/builtin/am.c @@ -0,0 +1,2521 @@ +/* + * Builtin "git am" + * + * Based on git-am.sh by Junio C Hamano. + */ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "abspath.h" +#include "advice.h" +#include "config.h" +#include "editor.h" +#include "environment.h" +#include "exec-cmd.h" +#include "gettext.h" +#include "hex.h" +#include "parse-options.h" +#include "dir.h" +#include "run-command.h" +#include "hook.h" +#include "quote.h" +#include "tempfile.h" +#include "lockfile.h" +#include "cache-tree.h" +#include "refs.h" +#include "commit.h" +#include "diff.h" +#include "diffcore.h" +#include "unpack-trees.h" +#include "branch.h" +#include "object-name.h" +#include "preload-index.h" +#include "sequencer.h" +#include "revision.h" +#include "merge-recursive.h" +#include "log-tree.h" +#include "notes-utils.h" +#include "rerere.h" +#include "prompt.h" +#include "mailinfo.h" +#include "apply.h" +#include "string-list.h" +#include "packfile.h" +#include "pager.h" +#include "path.h" +#include "repository.h" +#include "pretty.h" + +/** + * Returns the length of the first line of msg. + */ +static int linelen(const char *msg) +{ + return strchrnul(msg, '\n') - msg; +} + +/** + * Returns true if `str` consists of only whitespace, false otherwise. + */ +static int str_isspace(const char *str) +{ + for (; *str; str++) + if (!isspace(*str)) + return 0; + + return 1; +} + +enum patch_format { + PATCH_FORMAT_UNKNOWN = 0, + PATCH_FORMAT_MBOX, + PATCH_FORMAT_STGIT, + PATCH_FORMAT_STGIT_SERIES, + PATCH_FORMAT_HG, + PATCH_FORMAT_MBOXRD +}; + +enum keep_type { + KEEP_FALSE = 0, + KEEP_TRUE, /* pass -k flag to git-mailinfo */ + KEEP_NON_PATCH /* pass -b flag to git-mailinfo */ +}; + +enum scissors_type { + SCISSORS_UNSET = -1, + SCISSORS_FALSE = 0, /* pass --no-scissors to git-mailinfo */ + SCISSORS_TRUE /* pass --scissors to git-mailinfo */ +}; + +enum signoff_type { + SIGNOFF_FALSE = 0, + SIGNOFF_TRUE = 1, + SIGNOFF_EXPLICIT /* --signoff was set on the command-line */ +}; + +enum resume_type { + RESUME_FALSE = 0, + RESUME_APPLY, + RESUME_RESOLVED, + RESUME_SKIP, + RESUME_ABORT, + RESUME_QUIT, + RESUME_SHOW_PATCH_RAW, + RESUME_SHOW_PATCH_DIFF, + RESUME_ALLOW_EMPTY, +}; + +enum empty_action { + STOP_ON_EMPTY_COMMIT = 0, /* output errors and stop in the middle of an am session */ + DROP_EMPTY_COMMIT, /* skip with a notice message, unless "--quiet" has been passed */ + KEEP_EMPTY_COMMIT, /* keep recording as empty commits */ +}; + +struct am_state { + /* state directory path */ + char *dir; + + /* current and last patch numbers, 1-indexed */ + int cur; + int last; + + /* commit metadata and message */ + char *author_name; + char *author_email; + char *author_date; + char *msg; + size_t msg_len; + + /* when --rebasing, records the original commit the patch came from */ + struct object_id orig_commit; + + /* number of digits in patch filename */ + int prec; + + /* various operating modes and command line options */ + int interactive; + int no_verify; + int threeway; + int quiet; + int signoff; /* enum signoff_type */ + int utf8; + int keep; /* enum keep_type */ + int message_id; + int scissors; /* enum scissors_type */ + int quoted_cr; /* enum quoted_cr_action */ + int empty_type; /* enum empty_action */ + struct strvec git_apply_opts; + const char *resolvemsg; + int committer_date_is_author_date; + int ignore_date; + int allow_rerere_autoupdate; + const char *sign_commit; + int rebasing; +}; + +/** + * Initializes am_state with the default values. + */ +static void am_state_init(struct am_state *state) +{ + int gpgsign; + + memset(state, 0, sizeof(*state)); + + state->dir = git_pathdup("rebase-apply"); + + state->prec = 4; + + git_config_get_bool("am.threeway", &state->threeway); + + state->utf8 = 1; + + git_config_get_bool("am.messageid", &state->message_id); + + state->scissors = SCISSORS_UNSET; + state->quoted_cr = quoted_cr_unset; + + strvec_init(&state->git_apply_opts); + + if (!git_config_get_bool("commit.gpgsign", &gpgsign)) + state->sign_commit = gpgsign ? "" : NULL; +} + +/** + * Releases memory allocated by an am_state. + */ +static void am_state_release(struct am_state *state) +{ + free(state->dir); + free(state->author_name); + free(state->author_email); + free(state->author_date); + free(state->msg); + strvec_clear(&state->git_apply_opts); +} + +static int am_option_parse_quoted_cr(const struct option *opt, + const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + + if (mailinfo_parse_quoted_cr_action(arg, opt->value) != 0) + return error(_("bad action '%s' for '%s'"), arg, "--quoted-cr"); + return 0; +} + +static int am_option_parse_empty(const struct option *opt, + const char *arg, int unset) +{ + int *opt_value = opt->value; + + BUG_ON_OPT_NEG(unset); + + if (!strcmp(arg, "stop")) + *opt_value = STOP_ON_EMPTY_COMMIT; + else if (!strcmp(arg, "drop")) + *opt_value = DROP_EMPTY_COMMIT; + else if (!strcmp(arg, "keep")) + *opt_value = KEEP_EMPTY_COMMIT; + else + return error(_("invalid value for '%s': '%s'"), "--empty", arg); + + return 0; +} + +/** + * Returns path relative to the am_state directory. + */ +static inline const char *am_path(const struct am_state *state, const char *path) +{ + return mkpath("%s/%s", state->dir, path); +} + +/** + * For convenience to call write_file() + */ +static void write_state_text(const struct am_state *state, + const char *name, const char *string) +{ + write_file(am_path(state, name), "%s", string); +} + +static void write_state_count(const struct am_state *state, + const char *name, int value) +{ + write_file(am_path(state, name), "%d", value); +} + +static void write_state_bool(const struct am_state *state, + const char *name, int value) +{ + write_state_text(state, name, value ? "t" : "f"); +} + +/** + * If state->quiet is false, calls fprintf(fp, fmt, ...), and appends a newline + * at the end. + */ +__attribute__((format (printf, 3, 4))) +static void say(const struct am_state *state, FILE *fp, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + if (!state->quiet) { + vfprintf(fp, fmt, ap); + putc('\n', fp); + } + va_end(ap); +} + +/** + * Returns 1 if there is an am session in progress, 0 otherwise. + */ +static int am_in_progress(const struct am_state *state) +{ + struct stat st; + + if (lstat(state->dir, &st) < 0 || !S_ISDIR(st.st_mode)) + return 0; + if (lstat(am_path(state, "last"), &st) || !S_ISREG(st.st_mode)) + return 0; + if (lstat(am_path(state, "next"), &st) || !S_ISREG(st.st_mode)) + return 0; + return 1; +} + +/** + * Reads the contents of `file` in the `state` directory into `sb`. Returns the + * number of bytes read on success, -1 if the file does not exist. If `trim` is + * set, trailing whitespace will be removed. + */ +static int read_state_file(struct strbuf *sb, const struct am_state *state, + const char *file, int trim) +{ + strbuf_reset(sb); + + if (strbuf_read_file(sb, am_path(state, file), 0) >= 0) { + if (trim) + strbuf_trim(sb); + + return sb->len; + } + + if (errno == ENOENT) + return -1; + + die_errno(_("could not read '%s'"), am_path(state, file)); +} + +/** + * Reads and parses the state directory's "author-script" file, and sets + * state->author_name, state->author_email and state->author_date accordingly. + * Returns 0 on success, -1 if the file could not be parsed. + * + * The author script is of the format: + * + * GIT_AUTHOR_NAME='$author_name' + * GIT_AUTHOR_EMAIL='$author_email' + * GIT_AUTHOR_DATE='$author_date' + * + * where $author_name, $author_email and $author_date are quoted. We are strict + * with our parsing, as the file was meant to be eval'd in the old git-am.sh + * script, and thus if the file differs from what this function expects, it is + * better to bail out than to do something that the user does not expect. + */ +static int read_am_author_script(struct am_state *state) +{ + const char *filename = am_path(state, "author-script"); + + assert(!state->author_name); + assert(!state->author_email); + assert(!state->author_date); + + return read_author_script(filename, &state->author_name, + &state->author_email, &state->author_date, 1); +} + +/** + * Saves state->author_name, state->author_email and state->author_date in the + * state directory's "author-script" file. + */ +static void write_author_script(const struct am_state *state) +{ + struct strbuf sb = STRBUF_INIT; + + strbuf_addstr(&sb, "GIT_AUTHOR_NAME="); + sq_quote_buf(&sb, state->author_name); + strbuf_addch(&sb, '\n'); + + strbuf_addstr(&sb, "GIT_AUTHOR_EMAIL="); + sq_quote_buf(&sb, state->author_email); + strbuf_addch(&sb, '\n'); + + strbuf_addstr(&sb, "GIT_AUTHOR_DATE="); + sq_quote_buf(&sb, state->author_date); + strbuf_addch(&sb, '\n'); + + write_state_text(state, "author-script", sb.buf); + + strbuf_release(&sb); +} + +/** + * Reads the commit message from the state directory's "final-commit" file, + * setting state->msg to its contents and state->msg_len to the length of its + * contents in bytes. + * + * Returns 0 on success, -1 if the file does not exist. + */ +static int read_commit_msg(struct am_state *state) +{ + struct strbuf sb = STRBUF_INIT; + + assert(!state->msg); + + if (read_state_file(&sb, state, "final-commit", 0) < 0) { + strbuf_release(&sb); + return -1; + } + + state->msg = strbuf_detach(&sb, &state->msg_len); + return 0; +} + +/** + * Saves state->msg in the state directory's "final-commit" file. + */ +static void write_commit_msg(const struct am_state *state) +{ + const char *filename = am_path(state, "final-commit"); + write_file_buf(filename, state->msg, state->msg_len); +} + +/** + * Loads state from disk. + */ +static void am_load(struct am_state *state) +{ + struct strbuf sb = STRBUF_INIT; + + if (read_state_file(&sb, state, "next", 1) < 0) + BUG("state file 'next' does not exist"); + state->cur = strtol(sb.buf, NULL, 10); + + if (read_state_file(&sb, state, "last", 1) < 0) + BUG("state file 'last' does not exist"); + state->last = strtol(sb.buf, NULL, 10); + + if (read_am_author_script(state) < 0) + die(_("could not parse author script")); + + read_commit_msg(state); + + if (read_state_file(&sb, state, "original-commit", 1) < 0) + oidclr(&state->orig_commit); + else if (get_oid_hex(sb.buf, &state->orig_commit) < 0) + die(_("could not parse %s"), am_path(state, "original-commit")); + + read_state_file(&sb, state, "threeway", 1); + state->threeway = !strcmp(sb.buf, "t"); + + read_state_file(&sb, state, "quiet", 1); + state->quiet = !strcmp(sb.buf, "t"); + + read_state_file(&sb, state, "sign", 1); + state->signoff = !strcmp(sb.buf, "t"); + + read_state_file(&sb, state, "utf8", 1); + state->utf8 = !strcmp(sb.buf, "t"); + + if (file_exists(am_path(state, "rerere-autoupdate"))) { + read_state_file(&sb, state, "rerere-autoupdate", 1); + state->allow_rerere_autoupdate = strcmp(sb.buf, "t") ? + RERERE_NOAUTOUPDATE : RERERE_AUTOUPDATE; + } else { + state->allow_rerere_autoupdate = 0; + } + + read_state_file(&sb, state, "keep", 1); + if (!strcmp(sb.buf, "t")) + state->keep = KEEP_TRUE; + else if (!strcmp(sb.buf, "b")) + state->keep = KEEP_NON_PATCH; + else + state->keep = KEEP_FALSE; + + read_state_file(&sb, state, "messageid", 1); + state->message_id = !strcmp(sb.buf, "t"); + + read_state_file(&sb, state, "scissors", 1); + if (!strcmp(sb.buf, "t")) + state->scissors = SCISSORS_TRUE; + else if (!strcmp(sb.buf, "f")) + state->scissors = SCISSORS_FALSE; + else + state->scissors = SCISSORS_UNSET; + + read_state_file(&sb, state, "quoted-cr", 1); + if (!*sb.buf) + state->quoted_cr = quoted_cr_unset; + else if (mailinfo_parse_quoted_cr_action(sb.buf, &state->quoted_cr) != 0) + die(_("could not parse %s"), am_path(state, "quoted-cr")); + + read_state_file(&sb, state, "apply-opt", 1); + strvec_clear(&state->git_apply_opts); + if (sq_dequote_to_strvec(sb.buf, &state->git_apply_opts) < 0) + die(_("could not parse %s"), am_path(state, "apply-opt")); + + state->rebasing = !!file_exists(am_path(state, "rebasing")); + + strbuf_release(&sb); +} + +/** + * Removes the am_state directory, forcefully terminating the current am + * session. + */ +static void am_destroy(const struct am_state *state) +{ + struct strbuf sb = STRBUF_INIT; + + strbuf_addstr(&sb, state->dir); + remove_dir_recursively(&sb, 0); + strbuf_release(&sb); +} + +/** + * Runs applypatch-msg hook. Returns its exit code. + */ +static int run_applypatch_msg_hook(struct am_state *state) +{ + int ret = 0; + + assert(state->msg); + + if (!state->no_verify) + ret = run_hooks_l("applypatch-msg", am_path(state, "final-commit"), NULL); + + if (!ret) { + FREE_AND_NULL(state->msg); + if (read_commit_msg(state) < 0) + die(_("'%s' was deleted by the applypatch-msg hook"), + am_path(state, "final-commit")); + } + + return ret; +} + +/** + * Runs post-rewrite hook. Returns it exit code. + */ +static int run_post_rewrite_hook(const struct am_state *state) +{ + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + + strvec_push(&opt.args, "rebase"); + opt.path_to_stdin = am_path(state, "rewritten"); + + return run_hooks_opt("post-rewrite", &opt); +} + +/** + * Reads the state directory's "rewritten" file, and copies notes from the old + * commits listed in the file to their rewritten commits. + * + * Returns 0 on success, -1 on failure. + */ +static int copy_notes_for_rebase(const struct am_state *state) +{ + struct notes_rewrite_cfg *c; + struct strbuf sb = STRBUF_INIT; + const char *invalid_line = _("Malformed input line: '%s'."); + const char *msg = "Notes added by 'git rebase'"; + FILE *fp; + int ret = 0; + + assert(state->rebasing); + + c = init_copy_notes_for_rewrite("rebase"); + if (!c) + return 0; + + fp = xfopen(am_path(state, "rewritten"), "r"); + + while (!strbuf_getline_lf(&sb, fp)) { + struct object_id from_obj, to_obj; + const char *p; + + if (sb.len != the_hash_algo->hexsz * 2 + 1) { + ret = error(invalid_line, sb.buf); + goto finish; + } + + if (parse_oid_hex(sb.buf, &from_obj, &p)) { + ret = error(invalid_line, sb.buf); + goto finish; + } + + if (*p != ' ') { + ret = error(invalid_line, sb.buf); + goto finish; + } + + if (get_oid_hex(p + 1, &to_obj)) { + ret = error(invalid_line, sb.buf); + goto finish; + } + + if (copy_note_for_rewrite(c, &from_obj, &to_obj)) + ret = error(_("Failed to copy notes from '%s' to '%s'"), + oid_to_hex(&from_obj), oid_to_hex(&to_obj)); + } + +finish: + finish_copy_notes_for_rewrite(the_repository, c, msg); + fclose(fp); + strbuf_release(&sb); + return ret; +} + +/** + * Determines if the file looks like a piece of RFC2822 mail by grabbing all + * non-indented lines and checking if they look like they begin with valid + * header field names. + * + * Returns 1 if the file looks like a piece of mail, 0 otherwise. + */ +static int is_mail(FILE *fp) +{ + const char *header_regex = "^[!-9;-~]+:"; + struct strbuf sb = STRBUF_INIT; + regex_t regex; + int ret = 1; + + if (fseek(fp, 0L, SEEK_SET)) + die_errno(_("fseek failed")); + + if (regcomp(®ex, header_regex, REG_NOSUB | REG_EXTENDED)) + die("invalid pattern: %s", header_regex); + + while (!strbuf_getline(&sb, fp)) { + if (!sb.len) + break; /* End of header */ + + /* Ignore indented folded lines */ + if (*sb.buf == '\t' || *sb.buf == ' ') + continue; + + /* It's a header if it matches header_regex */ + if (regexec(®ex, sb.buf, 0, NULL, 0)) { + ret = 0; + goto done; + } + } + +done: + regfree(®ex); + strbuf_release(&sb); + return ret; +} + +/** + * Attempts to detect the patch_format of the patches contained in `paths`, + * returning the PATCH_FORMAT_* enum value. Returns PATCH_FORMAT_UNKNOWN if + * detection fails. + */ +static int detect_patch_format(const char **paths) +{ + enum patch_format ret = PATCH_FORMAT_UNKNOWN; + struct strbuf l1 = STRBUF_INIT; + struct strbuf l2 = STRBUF_INIT; + struct strbuf l3 = STRBUF_INIT; + FILE *fp; + + /* + * We default to mbox format if input is from stdin and for directories + */ + if (!*paths || !strcmp(*paths, "-") || is_directory(*paths)) + return PATCH_FORMAT_MBOX; + + /* + * Otherwise, check the first few lines of the first patch, starting + * from the first non-blank line, to try to detect its format. + */ + + fp = xfopen(*paths, "r"); + + while (!strbuf_getline(&l1, fp)) { + if (l1.len) + break; + } + + if (starts_with(l1.buf, "From ") || starts_with(l1.buf, "From: ")) { + ret = PATCH_FORMAT_MBOX; + goto done; + } + + if (starts_with(l1.buf, "# This series applies on GIT commit")) { + ret = PATCH_FORMAT_STGIT_SERIES; + goto done; + } + + if (!strcmp(l1.buf, "# HG changeset patch")) { + ret = PATCH_FORMAT_HG; + goto done; + } + + strbuf_getline(&l2, fp); + strbuf_getline(&l3, fp); + + /* + * If the second line is empty and the third is a From, Author or Date + * entry, this is likely an StGit patch. + */ + if (l1.len && !l2.len && + (starts_with(l3.buf, "From:") || + starts_with(l3.buf, "Author:") || + starts_with(l3.buf, "Date:"))) { + ret = PATCH_FORMAT_STGIT; + goto done; + } + + if (l1.len && is_mail(fp)) { + ret = PATCH_FORMAT_MBOX; + goto done; + } + +done: + fclose(fp); + strbuf_release(&l1); + strbuf_release(&l2); + strbuf_release(&l3); + return ret; +} + +/** + * Splits out individual email patches from `paths`, where each path is either + * a mbox file or a Maildir. Returns 0 on success, -1 on failure. + */ +static int split_mail_mbox(struct am_state *state, const char **paths, + int keep_cr, int mboxrd) +{ + struct child_process cp = CHILD_PROCESS_INIT; + struct strbuf last = STRBUF_INIT; + int ret; + + cp.git_cmd = 1; + strvec_push(&cp.args, "mailsplit"); + strvec_pushf(&cp.args, "-d%d", state->prec); + strvec_pushf(&cp.args, "-o%s", state->dir); + strvec_push(&cp.args, "-b"); + if (keep_cr) + strvec_push(&cp.args, "--keep-cr"); + if (mboxrd) + strvec_push(&cp.args, "--mboxrd"); + strvec_push(&cp.args, "--"); + strvec_pushv(&cp.args, paths); + + ret = capture_command(&cp, &last, 8); + if (ret) + goto exit; + + state->cur = 1; + state->last = strtol(last.buf, NULL, 10); + +exit: + strbuf_release(&last); + return ret ? -1 : 0; +} + +/** + * Callback signature for split_mail_conv(). The foreign patch should be + * read from `in`, and the converted patch (in RFC2822 mail format) should be + * written to `out`. Return 0 on success, or -1 on failure. + */ +typedef int (*mail_conv_fn)(FILE *out, FILE *in, int keep_cr); + +/** + * Calls `fn` for each file in `paths` to convert the foreign patch to the + * RFC2822 mail format suitable for parsing with git-mailinfo. + * + * Returns 0 on success, -1 on failure. + */ +static int split_mail_conv(mail_conv_fn fn, struct am_state *state, + const char **paths, int keep_cr) +{ + static const char *stdin_only[] = {"-", NULL}; + int i; + + if (!*paths) + paths = stdin_only; + + for (i = 0; *paths; paths++, i++) { + FILE *in, *out; + const char *mail; + int ret; + + if (!strcmp(*paths, "-")) + in = stdin; + else + in = fopen(*paths, "r"); + + if (!in) + return error_errno(_("could not open '%s' for reading"), + *paths); + + mail = mkpath("%s/%0*d", state->dir, state->prec, i + 1); + + out = fopen(mail, "w"); + if (!out) { + if (in != stdin) + fclose(in); + return error_errno(_("could not open '%s' for writing"), + mail); + } + + ret = fn(out, in, keep_cr); + + fclose(out); + if (in != stdin) + fclose(in); + + if (ret) + return error(_("could not parse patch '%s'"), *paths); + } + + state->cur = 1; + state->last = i; + return 0; +} + +/** + * A split_mail_conv() callback that converts an StGit patch to an RFC2822 + * message suitable for parsing with git-mailinfo. + */ +static int stgit_patch_to_mail(FILE *out, FILE *in, int keep_cr UNUSED) +{ + struct strbuf sb = STRBUF_INIT; + int subject_printed = 0; + + while (!strbuf_getline_lf(&sb, in)) { + const char *str; + + if (str_isspace(sb.buf)) + continue; + else if (skip_prefix(sb.buf, "Author:", &str)) + fprintf(out, "From:%s\n", str); + else if (starts_with(sb.buf, "From") || starts_with(sb.buf, "Date")) + fprintf(out, "%s\n", sb.buf); + else if (!subject_printed) { + fprintf(out, "Subject: %s\n", sb.buf); + subject_printed = 1; + } else { + fprintf(out, "\n%s\n", sb.buf); + break; + } + } + + strbuf_reset(&sb); + while (strbuf_fread(&sb, 8192, in) > 0) { + fwrite(sb.buf, 1, sb.len, out); + strbuf_reset(&sb); + } + + strbuf_release(&sb); + return 0; +} + +/** + * This function only supports a single StGit series file in `paths`. + * + * Given an StGit series file, converts the StGit patches in the series into + * RFC2822 messages suitable for parsing with git-mailinfo, and queues them in + * the state directory. + * + * Returns 0 on success, -1 on failure. + */ +static int split_mail_stgit_series(struct am_state *state, const char **paths, + int keep_cr) +{ + const char *series_dir; + char *series_dir_buf; + FILE *fp; + struct strvec patches = STRVEC_INIT; + struct strbuf sb = STRBUF_INIT; + int ret; + + if (!paths[0] || paths[1]) + return error(_("Only one StGIT patch series can be applied at once")); + + series_dir_buf = xstrdup(*paths); + series_dir = dirname(series_dir_buf); + + fp = fopen(*paths, "r"); + if (!fp) + return error_errno(_("could not open '%s' for reading"), *paths); + + while (!strbuf_getline_lf(&sb, fp)) { + if (*sb.buf == '#') + continue; /* skip comment lines */ + + strvec_push(&patches, mkpath("%s/%s", series_dir, sb.buf)); + } + + fclose(fp); + strbuf_release(&sb); + free(series_dir_buf); + + ret = split_mail_conv(stgit_patch_to_mail, state, patches.v, keep_cr); + + strvec_clear(&patches); + return ret; +} + +/** + * A split_patches_conv() callback that converts a mercurial patch to a RFC2822 + * message suitable for parsing with git-mailinfo. + */ +static int hg_patch_to_mail(FILE *out, FILE *in, int keep_cr UNUSED) +{ + struct strbuf sb = STRBUF_INIT; + int rc = 0; + + while (!strbuf_getline_lf(&sb, in)) { + const char *str; + + if (skip_prefix(sb.buf, "# User ", &str)) + fprintf(out, "From: %s\n", str); + else if (skip_prefix(sb.buf, "# Date ", &str)) { + timestamp_t timestamp; + long tz, tz2; + char *end; + + errno = 0; + timestamp = parse_timestamp(str, &end, 10); + if (errno) { + rc = error(_("invalid timestamp")); + goto exit; + } + + if (!skip_prefix(end, " ", &str)) { + rc = error(_("invalid Date line")); + goto exit; + } + + errno = 0; + tz = strtol(str, &end, 10); + if (errno) { + rc = error(_("invalid timezone offset")); + goto exit; + } + + if (*end) { + rc = error(_("invalid Date line")); + goto exit; + } + + /* + * mercurial's timezone is in seconds west of UTC, + * however git's timezone is in hours + minutes east of + * UTC. Convert it. + */ + tz2 = labs(tz) / 3600 * 100 + labs(tz) % 3600 / 60; + if (tz > 0) + tz2 = -tz2; + + fprintf(out, "Date: %s\n", show_date(timestamp, tz2, DATE_MODE(RFC2822))); + } else if (starts_with(sb.buf, "# ")) { + continue; + } else { + fprintf(out, "\n%s\n", sb.buf); + break; + } + } + + strbuf_reset(&sb); + while (strbuf_fread(&sb, 8192, in) > 0) { + fwrite(sb.buf, 1, sb.len, out); + strbuf_reset(&sb); + } +exit: + strbuf_release(&sb); + return rc; +} + +/** + * Splits a list of files/directories into individual email patches. Each path + * in `paths` must be a file/directory that is formatted according to + * `patch_format`. + * + * Once split out, the individual email patches will be stored in the state + * directory, with each patch's filename being its index, padded to state->prec + * digits. + * + * state->cur will be set to the index of the first mail, and state->last will + * be set to the index of the last mail. + * + * Set keep_cr to 0 to convert all lines ending with \r\n to end with \n, 1 + * to disable this behavior, -1 to use the default configured setting. + * + * Returns 0 on success, -1 on failure. + */ +static int split_mail(struct am_state *state, enum patch_format patch_format, + const char **paths, int keep_cr) +{ + if (keep_cr < 0) { + keep_cr = 0; + git_config_get_bool("am.keepcr", &keep_cr); + } + + switch (patch_format) { + case PATCH_FORMAT_MBOX: + return split_mail_mbox(state, paths, keep_cr, 0); + case PATCH_FORMAT_STGIT: + return split_mail_conv(stgit_patch_to_mail, state, paths, keep_cr); + case PATCH_FORMAT_STGIT_SERIES: + return split_mail_stgit_series(state, paths, keep_cr); + case PATCH_FORMAT_HG: + return split_mail_conv(hg_patch_to_mail, state, paths, keep_cr); + case PATCH_FORMAT_MBOXRD: + return split_mail_mbox(state, paths, keep_cr, 1); + default: + BUG("invalid patch_format"); + } + return -1; +} + +/** + * Setup a new am session for applying patches + */ +static void am_setup(struct am_state *state, enum patch_format patch_format, + const char **paths, int keep_cr) +{ + struct object_id curr_head; + const char *str; + struct strbuf sb = STRBUF_INIT; + + if (!patch_format) + patch_format = detect_patch_format(paths); + + if (!patch_format) { + fprintf_ln(stderr, _("Patch format detection failed.")); + exit(128); + } + + if (mkdir(state->dir, 0777) < 0 && errno != EEXIST) + die_errno(_("failed to create directory '%s'"), state->dir); + delete_ref(NULL, "REBASE_HEAD", NULL, REF_NO_DEREF); + + if (split_mail(state, patch_format, paths, keep_cr) < 0) { + am_destroy(state); + die(_("Failed to split patches.")); + } + + if (state->rebasing) + state->threeway = 1; + + write_state_bool(state, "threeway", state->threeway); + write_state_bool(state, "quiet", state->quiet); + write_state_bool(state, "sign", state->signoff); + write_state_bool(state, "utf8", state->utf8); + + if (state->allow_rerere_autoupdate) + write_state_bool(state, "rerere-autoupdate", + state->allow_rerere_autoupdate == RERERE_AUTOUPDATE); + + switch (state->keep) { + case KEEP_FALSE: + str = "f"; + break; + case KEEP_TRUE: + str = "t"; + break; + case KEEP_NON_PATCH: + str = "b"; + break; + default: + BUG("invalid value for state->keep"); + } + + write_state_text(state, "keep", str); + write_state_bool(state, "messageid", state->message_id); + + switch (state->scissors) { + case SCISSORS_UNSET: + str = ""; + break; + case SCISSORS_FALSE: + str = "f"; + break; + case SCISSORS_TRUE: + str = "t"; + break; + default: + BUG("invalid value for state->scissors"); + } + write_state_text(state, "scissors", str); + + switch (state->quoted_cr) { + case quoted_cr_unset: + str = ""; + break; + case quoted_cr_nowarn: + str = "nowarn"; + break; + case quoted_cr_warn: + str = "warn"; + break; + case quoted_cr_strip: + str = "strip"; + break; + default: + BUG("invalid value for state->quoted_cr"); + } + write_state_text(state, "quoted-cr", str); + + sq_quote_argv(&sb, state->git_apply_opts.v); + write_state_text(state, "apply-opt", sb.buf); + + if (state->rebasing) + write_state_text(state, "rebasing", ""); + else + write_state_text(state, "applying", ""); + + if (!repo_get_oid(the_repository, "HEAD", &curr_head)) { + write_state_text(state, "abort-safety", oid_to_hex(&curr_head)); + if (!state->rebasing) + update_ref("am", "ORIG_HEAD", &curr_head, NULL, 0, + UPDATE_REFS_DIE_ON_ERR); + } else { + write_state_text(state, "abort-safety", ""); + if (!state->rebasing) + delete_ref(NULL, "ORIG_HEAD", NULL, 0); + } + + /* + * NOTE: Since the "next" and "last" files determine if an am_state + * session is in progress, they should be written last. + */ + + write_state_count(state, "next", state->cur); + write_state_count(state, "last", state->last); + + strbuf_release(&sb); +} + +/** + * Increments the patch pointer, and cleans am_state for the application of the + * next patch. + */ +static void am_next(struct am_state *state) +{ + struct object_id head; + + FREE_AND_NULL(state->author_name); + FREE_AND_NULL(state->author_email); + FREE_AND_NULL(state->author_date); + FREE_AND_NULL(state->msg); + state->msg_len = 0; + + unlink(am_path(state, "author-script")); + unlink(am_path(state, "final-commit")); + + oidclr(&state->orig_commit); + unlink(am_path(state, "original-commit")); + delete_ref(NULL, "REBASE_HEAD", NULL, REF_NO_DEREF); + + if (!repo_get_oid(the_repository, "HEAD", &head)) + write_state_text(state, "abort-safety", oid_to_hex(&head)); + else + write_state_text(state, "abort-safety", ""); + + state->cur++; + write_state_count(state, "next", state->cur); +} + +/** + * Returns the filename of the current patch email. + */ +static const char *msgnum(const struct am_state *state) +{ + static struct strbuf sb = STRBUF_INIT; + + strbuf_reset(&sb); + strbuf_addf(&sb, "%0*d", state->prec, state->cur); + + return sb.buf; +} + +/** + * Dies with a user-friendly message on how to proceed after resolving the + * problem. This message can be overridden with state->resolvemsg. + */ +static void NORETURN die_user_resolve(const struct am_state *state) +{ + if (state->resolvemsg) { + printf_ln("%s", state->resolvemsg); + } else { + const char *cmdline = state->interactive ? "git am -i" : "git am"; + + printf_ln(_("When you have resolved this problem, run \"%s --continue\"."), cmdline); + printf_ln(_("If you prefer to skip this patch, run \"%s --skip\" instead."), cmdline); + + if (advice_enabled(ADVICE_AM_WORK_DIR) && + is_empty_or_missing_file(am_path(state, "patch")) && + !repo_index_has_changes(the_repository, NULL, NULL)) + printf_ln(_("To record the empty patch as an empty commit, run \"%s --allow-empty\"."), cmdline); + + printf_ln(_("To restore the original branch and stop patching, run \"%s --abort\"."), cmdline); + } + + exit(128); +} + +/** + * Appends signoff to the "msg" field of the am_state. + */ +static void am_append_signoff(struct am_state *state) +{ + struct strbuf sb = STRBUF_INIT; + + strbuf_attach(&sb, state->msg, state->msg_len, state->msg_len); + append_signoff(&sb, 0, 0); + state->msg = strbuf_detach(&sb, &state->msg_len); +} + +/** + * Parses `mail` using git-mailinfo, extracting its patch and authorship info. + * state->msg will be set to the patch message. state->author_name, + * state->author_email and state->author_date will be set to the patch author's + * name, email and date respectively. The patch body will be written to the + * state directory's "patch" file. + * + * Returns 1 if the patch should be skipped, 0 otherwise. + */ +static int parse_mail(struct am_state *state, const char *mail) +{ + FILE *fp; + struct strbuf sb = STRBUF_INIT; + struct strbuf msg = STRBUF_INIT; + struct strbuf author_name = STRBUF_INIT; + struct strbuf author_date = STRBUF_INIT; + struct strbuf author_email = STRBUF_INIT; + int ret = 0; + struct mailinfo mi; + + setup_mailinfo(&mi); + + if (state->utf8) + mi.metainfo_charset = get_commit_output_encoding(); + else + mi.metainfo_charset = NULL; + + switch (state->keep) { + case KEEP_FALSE: + break; + case KEEP_TRUE: + mi.keep_subject = 1; + break; + case KEEP_NON_PATCH: + mi.keep_non_patch_brackets_in_subject = 1; + break; + default: + BUG("invalid value for state->keep"); + } + + if (state->message_id) + mi.add_message_id = 1; + + switch (state->scissors) { + case SCISSORS_UNSET: + break; + case SCISSORS_FALSE: + mi.use_scissors = 0; + break; + case SCISSORS_TRUE: + mi.use_scissors = 1; + break; + default: + BUG("invalid value for state->scissors"); + } + + switch (state->quoted_cr) { + case quoted_cr_unset: + break; + case quoted_cr_nowarn: + case quoted_cr_warn: + case quoted_cr_strip: + mi.quoted_cr = state->quoted_cr; + break; + default: + BUG("invalid value for state->quoted_cr"); + } + + mi.input = xfopen(mail, "r"); + mi.output = xfopen(am_path(state, "info"), "w"); + if (mailinfo(&mi, am_path(state, "msg"), am_path(state, "patch"))) + die("could not parse patch"); + + fclose(mi.input); + fclose(mi.output); + + if (mi.format_flowed) + warning(_("Patch sent with format=flowed; " + "space at the end of lines might be lost.")); + + /* Extract message and author information */ + fp = xfopen(am_path(state, "info"), "r"); + while (!strbuf_getline_lf(&sb, fp)) { + const char *x; + + if (skip_prefix(sb.buf, "Subject: ", &x)) { + if (msg.len) + strbuf_addch(&msg, '\n'); + strbuf_addstr(&msg, x); + } else if (skip_prefix(sb.buf, "Author: ", &x)) + strbuf_addstr(&author_name, x); + else if (skip_prefix(sb.buf, "Email: ", &x)) + strbuf_addstr(&author_email, x); + else if (skip_prefix(sb.buf, "Date: ", &x)) + strbuf_addstr(&author_date, x); + } + fclose(fp); + + /* Skip pine's internal folder data */ + if (!strcmp(author_name.buf, "Mail System Internal Data")) { + ret = 1; + goto finish; + } + + strbuf_addstr(&msg, "\n\n"); + strbuf_addbuf(&msg, &mi.log_message); + strbuf_stripspace(&msg, '\0'); + + assert(!state->author_name); + state->author_name = strbuf_detach(&author_name, NULL); + + assert(!state->author_email); + state->author_email = strbuf_detach(&author_email, NULL); + + assert(!state->author_date); + state->author_date = strbuf_detach(&author_date, NULL); + + assert(!state->msg); + state->msg = strbuf_detach(&msg, &state->msg_len); + +finish: + strbuf_release(&msg); + strbuf_release(&author_date); + strbuf_release(&author_email); + strbuf_release(&author_name); + strbuf_release(&sb); + clear_mailinfo(&mi); + return ret; +} + +/** + * Sets commit_id to the commit hash where the mail was generated from. + * Returns 0 on success, -1 on failure. + */ +static int get_mail_commit_oid(struct object_id *commit_id, const char *mail) +{ + struct strbuf sb = STRBUF_INIT; + FILE *fp = xfopen(mail, "r"); + const char *x; + int ret = 0; + + if (strbuf_getline_lf(&sb, fp) || + !skip_prefix(sb.buf, "From ", &x) || + get_oid_hex(x, commit_id) < 0) + ret = -1; + + strbuf_release(&sb); + fclose(fp); + return ret; +} + +/** + * Sets state->msg, state->author_name, state->author_email, state->author_date + * to the commit's respective info. + */ +static void get_commit_info(struct am_state *state, struct commit *commit) +{ + const char *buffer, *ident_line, *msg; + size_t ident_len; + struct ident_split id; + + buffer = repo_logmsg_reencode(the_repository, commit, NULL, + get_commit_output_encoding()); + + ident_line = find_commit_header(buffer, "author", &ident_len); + if (!ident_line) + die(_("missing author line in commit %s"), + oid_to_hex(&commit->object.oid)); + if (split_ident_line(&id, ident_line, ident_len) < 0) + die(_("invalid ident line: %.*s"), (int)ident_len, ident_line); + + assert(!state->author_name); + if (id.name_begin) + state->author_name = + xmemdupz(id.name_begin, id.name_end - id.name_begin); + else + state->author_name = xstrdup(""); + + assert(!state->author_email); + if (id.mail_begin) + state->author_email = + xmemdupz(id.mail_begin, id.mail_end - id.mail_begin); + else + state->author_email = xstrdup(""); + + assert(!state->author_date); + state->author_date = xstrdup(show_ident_date(&id, DATE_MODE(NORMAL))); + + assert(!state->msg); + msg = strstr(buffer, "\n\n"); + if (!msg) + die(_("unable to parse commit %s"), oid_to_hex(&commit->object.oid)); + state->msg = xstrdup(msg + 2); + state->msg_len = strlen(state->msg); + repo_unuse_commit_buffer(the_repository, commit, buffer); +} + +/** + * Writes `commit` as a patch to the state directory's "patch" file. + */ +static void write_commit_patch(const struct am_state *state, struct commit *commit) +{ + struct rev_info rev_info; + FILE *fp; + + fp = xfopen(am_path(state, "patch"), "w"); + repo_init_revisions(the_repository, &rev_info, NULL); + rev_info.diff = 1; + rev_info.abbrev = 0; + rev_info.disable_stdin = 1; + rev_info.show_root_diff = 1; + rev_info.diffopt.output_format = DIFF_FORMAT_PATCH; + rev_info.no_commit_id = 1; + rev_info.diffopt.flags.binary = 1; + rev_info.diffopt.flags.full_index = 1; + rev_info.diffopt.use_color = 0; + rev_info.diffopt.file = fp; + rev_info.diffopt.close_file = 1; + add_pending_object(&rev_info, &commit->object, ""); + diff_setup_done(&rev_info.diffopt); + log_tree_commit(&rev_info, commit); + release_revisions(&rev_info); +} + +/** + * Writes the diff of the index against HEAD as a patch to the state + * directory's "patch" file. + */ +static void write_index_patch(const struct am_state *state) +{ + struct tree *tree; + struct object_id head; + struct rev_info rev_info; + FILE *fp; + + if (!repo_get_oid(the_repository, "HEAD", &head)) { + struct commit *commit = lookup_commit_or_die(&head, "HEAD"); + tree = repo_get_commit_tree(the_repository, commit); + } else + tree = lookup_tree(the_repository, + the_repository->hash_algo->empty_tree); + + fp = xfopen(am_path(state, "patch"), "w"); + repo_init_revisions(the_repository, &rev_info, NULL); + rev_info.diff = 1; + rev_info.disable_stdin = 1; + rev_info.no_commit_id = 1; + rev_info.diffopt.output_format = DIFF_FORMAT_PATCH; + rev_info.diffopt.use_color = 0; + rev_info.diffopt.file = fp; + rev_info.diffopt.close_file = 1; + add_pending_object(&rev_info, &tree->object, ""); + diff_setup_done(&rev_info.diffopt); + run_diff_index(&rev_info, DIFF_INDEX_CACHED); + release_revisions(&rev_info); +} + +/** + * Like parse_mail(), but parses the mail by looking up its commit ID + * directly. This is used in --rebasing mode to bypass git-mailinfo's munging + * of patches. + * + * state->orig_commit will be set to the original commit ID. + * + * Will always return 0 as the patch should never be skipped. + */ +static int parse_mail_rebase(struct am_state *state, const char *mail) +{ + struct commit *commit; + struct object_id commit_oid; + + if (get_mail_commit_oid(&commit_oid, mail) < 0) + die(_("could not parse %s"), mail); + + commit = lookup_commit_or_die(&commit_oid, mail); + + get_commit_info(state, commit); + + write_commit_patch(state, commit); + + oidcpy(&state->orig_commit, &commit_oid); + write_state_text(state, "original-commit", oid_to_hex(&commit_oid)); + update_ref("am", "REBASE_HEAD", &commit_oid, + NULL, REF_NO_DEREF, UPDATE_REFS_DIE_ON_ERR); + + return 0; +} + +/** + * Applies current patch with git-apply. Returns 0 on success, -1 otherwise. If + * `index_file` is not NULL, the patch will be applied to that index. + */ +static int run_apply(const struct am_state *state, const char *index_file) +{ + struct strvec apply_paths = STRVEC_INIT; + struct strvec apply_opts = STRVEC_INIT; + struct apply_state apply_state; + int res, opts_left; + int force_apply = 0; + int options = 0; + const char **apply_argv; + + if (init_apply_state(&apply_state, the_repository, NULL)) + BUG("init_apply_state() failed"); + + strvec_push(&apply_opts, "apply"); + strvec_pushv(&apply_opts, state->git_apply_opts.v); + + /* + * Build a copy that apply_parse_options() can rearrange. + * apply_opts.v keeps referencing the allocated strings for + * strvec_clear() to release. + */ + DUP_ARRAY(apply_argv, apply_opts.v, apply_opts.nr); + + opts_left = apply_parse_options(apply_opts.nr, apply_argv, + &apply_state, &force_apply, &options, + NULL); + + if (opts_left != 0) + die("unknown option passed through to git apply"); + + if (index_file) { + apply_state.index_file = index_file; + apply_state.cached = 1; + } else + apply_state.check_index = 1; + + /* + * If we are allowed to fall back on 3-way merge, don't give false + * errors during the initial attempt. + */ + if (state->threeway && !index_file) + apply_state.apply_verbosity = verbosity_silent; + + if (check_apply_state(&apply_state, force_apply)) + BUG("check_apply_state() failed"); + + strvec_push(&apply_paths, am_path(state, "patch")); + + res = apply_all_patches(&apply_state, apply_paths.nr, apply_paths.v, options); + + strvec_clear(&apply_paths); + strvec_clear(&apply_opts); + clear_apply_state(&apply_state); + free(apply_argv); + + if (res) + return res; + + if (index_file) { + /* Reload index as apply_all_patches() will have modified it. */ + discard_index(&the_index); + read_index_from(&the_index, index_file, get_git_dir()); + } + + return 0; +} + +/** + * Builds an index that contains just the blobs needed for a 3way merge. + */ +static int build_fake_ancestor(const struct am_state *state, const char *index_file) +{ + struct child_process cp = CHILD_PROCESS_INIT; + + cp.git_cmd = 1; + strvec_push(&cp.args, "apply"); + strvec_pushv(&cp.args, state->git_apply_opts.v); + strvec_pushf(&cp.args, "--build-fake-ancestor=%s", index_file); + strvec_push(&cp.args, am_path(state, "patch")); + + if (run_command(&cp)) + return -1; + + return 0; +} + +/** + * Attempt a threeway merge, using index_path as the temporary index. + */ +static int fall_back_threeway(const struct am_state *state, const char *index_path) +{ + struct object_id orig_tree, their_tree, our_tree; + const struct object_id *bases[1] = { &orig_tree }; + struct merge_options o; + struct commit *result; + char *their_tree_name; + + if (repo_get_oid(the_repository, "HEAD", &our_tree) < 0) + oidcpy(&our_tree, the_hash_algo->empty_tree); + + if (build_fake_ancestor(state, index_path)) + return error("could not build fake ancestor"); + + discard_index(&the_index); + read_index_from(&the_index, index_path, get_git_dir()); + + if (write_index_as_tree(&orig_tree, &the_index, index_path, 0, NULL)) + return error(_("Repository lacks necessary blobs to fall back on 3-way merge.")); + + say(state, stdout, _("Using index info to reconstruct a base tree...")); + + if (!state->quiet) { + /* + * List paths that needed 3-way fallback, so that the user can + * review them with extra care to spot mismerges. + */ + struct rev_info rev_info; + + repo_init_revisions(the_repository, &rev_info, NULL); + rev_info.diffopt.output_format = DIFF_FORMAT_NAME_STATUS; + rev_info.diffopt.filter |= diff_filter_bit('A'); + rev_info.diffopt.filter |= diff_filter_bit('M'); + add_pending_oid(&rev_info, "HEAD", &our_tree, 0); + diff_setup_done(&rev_info.diffopt); + run_diff_index(&rev_info, DIFF_INDEX_CACHED); + release_revisions(&rev_info); + } + + if (run_apply(state, index_path)) + return error(_("Did you hand edit your patch?\n" + "It does not apply to blobs recorded in its index.")); + + if (write_index_as_tree(&their_tree, &the_index, index_path, 0, NULL)) + return error("could not write tree"); + + say(state, stdout, _("Falling back to patching base and 3-way merge...")); + + discard_index(&the_index); + repo_read_index(the_repository); + + /* + * This is not so wrong. Depending on which base we picked, orig_tree + * may be wildly different from ours, but their_tree has the same set of + * wildly different changes in parts the patch did not touch, so + * recursive ends up canceling them, saying that we reverted all those + * changes. + */ + + init_merge_options(&o, the_repository); + + o.branch1 = "HEAD"; + their_tree_name = xstrfmt("%.*s", linelen(state->msg), state->msg); + o.branch2 = their_tree_name; + o.detect_directory_renames = MERGE_DIRECTORY_RENAMES_NONE; + + if (state->quiet) + o.verbosity = 0; + + if (merge_recursive_generic(&o, &our_tree, &their_tree, 1, bases, &result)) { + repo_rerere(the_repository, state->allow_rerere_autoupdate); + free(their_tree_name); + return error(_("Failed to merge in the changes.")); + } + + free(their_tree_name); + return 0; +} + +/** + * Commits the current index with state->msg as the commit message and + * state->author_name, state->author_email and state->author_date as the author + * information. + */ +static void do_commit(const struct am_state *state) +{ + struct object_id tree, parent, commit; + const struct object_id *old_oid; + struct commit_list *parents = NULL; + const char *reflog_msg, *author, *committer = NULL; + struct strbuf sb = STRBUF_INIT; + + if (!state->no_verify && run_hooks("pre-applypatch")) + exit(1); + + if (write_index_as_tree(&tree, &the_index, get_index_file(), 0, NULL)) + die(_("git write-tree failed to write a tree")); + + if (!repo_get_oid_commit(the_repository, "HEAD", &parent)) { + old_oid = &parent; + commit_list_insert(lookup_commit(the_repository, &parent), + &parents); + } else { + old_oid = NULL; + say(state, stderr, _("applying to an empty history")); + } + + author = fmt_ident(state->author_name, state->author_email, + WANT_AUTHOR_IDENT, + state->ignore_date ? NULL : state->author_date, + IDENT_STRICT); + + if (state->committer_date_is_author_date) + committer = fmt_ident(getenv("GIT_COMMITTER_NAME"), + getenv("GIT_COMMITTER_EMAIL"), + WANT_COMMITTER_IDENT, + state->ignore_date ? NULL + : state->author_date, + IDENT_STRICT); + + if (commit_tree_extended(state->msg, state->msg_len, &tree, parents, + &commit, author, committer, state->sign_commit, + NULL)) + die(_("failed to write commit object")); + + reflog_msg = getenv("GIT_REFLOG_ACTION"); + if (!reflog_msg) + reflog_msg = "am"; + + strbuf_addf(&sb, "%s: %.*s", reflog_msg, linelen(state->msg), + state->msg); + + update_ref(sb.buf, "HEAD", &commit, old_oid, 0, + UPDATE_REFS_DIE_ON_ERR); + + if (state->rebasing) { + FILE *fp = xfopen(am_path(state, "rewritten"), "a"); + + assert(!is_null_oid(&state->orig_commit)); + fprintf(fp, "%s ", oid_to_hex(&state->orig_commit)); + fprintf(fp, "%s\n", oid_to_hex(&commit)); + fclose(fp); + } + + run_hooks("post-applypatch"); + + strbuf_release(&sb); +} + +/** + * Validates the am_state for resuming -- the "msg" and authorship fields must + * be filled up. + */ +static void validate_resume_state(const struct am_state *state) +{ + if (!state->msg) + die(_("cannot resume: %s does not exist."), + am_path(state, "final-commit")); + + if (!state->author_name || !state->author_email || !state->author_date) + die(_("cannot resume: %s does not exist."), + am_path(state, "author-script")); +} + +/** + * Interactively prompt the user on whether the current patch should be + * applied. + * + * Returns 0 if the user chooses to apply the patch, 1 if the user chooses to + * skip it. + */ +static int do_interactive(struct am_state *state) +{ + assert(state->msg); + + for (;;) { + char reply[64]; + + puts(_("Commit Body is:")); + puts("--------------------------"); + printf("%s", state->msg); + puts("--------------------------"); + + /* + * TRANSLATORS: Make sure to include [y], [n], [e], [v] and [a] + * in your translation. The program will only accept English + * input at this point. + */ + printf(_("Apply? [y]es/[n]o/[e]dit/[v]iew patch/[a]ccept all: ")); + if (!fgets(reply, sizeof(reply), stdin)) + die("unable to read from stdin; aborting"); + + if (*reply == 'y' || *reply == 'Y') { + return 0; + } else if (*reply == 'a' || *reply == 'A') { + state->interactive = 0; + return 0; + } else if (*reply == 'n' || *reply == 'N') { + return 1; + } else if (*reply == 'e' || *reply == 'E') { + struct strbuf msg = STRBUF_INIT; + + if (!launch_editor(am_path(state, "final-commit"), &msg, NULL)) { + free(state->msg); + state->msg = strbuf_detach(&msg, &state->msg_len); + } + strbuf_release(&msg); + } else if (*reply == 'v' || *reply == 'V') { + const char *pager = git_pager(1); + struct child_process cp = CHILD_PROCESS_INIT; + + if (!pager) + pager = "cat"; + prepare_pager_args(&cp, pager); + strvec_push(&cp.args, am_path(state, "patch")); + run_command(&cp); + } + } +} + +/** + * Applies all queued mail. + * + * If `resume` is true, we are "resuming". The "msg" and authorship fields, as + * well as the state directory's "patch" file is used as-is for applying the + * patch and committing it. + */ +static void am_run(struct am_state *state, int resume) +{ + struct strbuf sb = STRBUF_INIT; + + unlink(am_path(state, "dirtyindex")); + + if (repo_refresh_and_write_index(the_repository, REFRESH_QUIET, 0, 0, + NULL, NULL, NULL) < 0) + die(_("unable to write index file")); + + if (repo_index_has_changes(the_repository, NULL, &sb)) { + write_state_bool(state, "dirtyindex", 1); + die(_("Dirty index: cannot apply patches (dirty: %s)"), sb.buf); + } + + strbuf_release(&sb); + + while (state->cur <= state->last) { + const char *mail = am_path(state, msgnum(state)); + int apply_status; + int to_keep; + + reset_ident_date(); + + if (!file_exists(mail)) + goto next; + + if (resume) { + validate_resume_state(state); + } else { + int skip; + + if (state->rebasing) + skip = parse_mail_rebase(state, mail); + else + skip = parse_mail(state, mail); + + if (skip) + goto next; /* mail should be skipped */ + + if (state->signoff) + am_append_signoff(state); + + write_author_script(state); + write_commit_msg(state); + } + + if (state->interactive && do_interactive(state)) + goto next; + + to_keep = 0; + if (is_empty_or_missing_file(am_path(state, "patch"))) { + switch (state->empty_type) { + case DROP_EMPTY_COMMIT: + say(state, stdout, _("Skipping: %.*s"), linelen(state->msg), state->msg); + goto next; + break; + case KEEP_EMPTY_COMMIT: + to_keep = 1; + say(state, stdout, _("Creating an empty commit: %.*s"), + linelen(state->msg), state->msg); + break; + case STOP_ON_EMPTY_COMMIT: + printf_ln(_("Patch is empty.")); + die_user_resolve(state); + break; + } + } + + if (run_applypatch_msg_hook(state)) + exit(1); + if (to_keep) + goto commit; + + say(state, stdout, _("Applying: %.*s"), linelen(state->msg), state->msg); + + apply_status = run_apply(state, NULL); + + if (apply_status && state->threeway) { + struct strbuf sb = STRBUF_INIT; + + strbuf_addstr(&sb, am_path(state, "patch-merge-index")); + apply_status = fall_back_threeway(state, sb.buf); + strbuf_release(&sb); + + /* + * Applying the patch to an earlier tree and merging + * the result may have produced the same tree as ours. + */ + if (!apply_status && + !repo_index_has_changes(the_repository, NULL, NULL)) { + say(state, stdout, _("No changes -- Patch already applied.")); + goto next; + } + } + + if (apply_status) { + printf_ln(_("Patch failed at %s %.*s"), msgnum(state), + linelen(state->msg), state->msg); + + if (advice_enabled(ADVICE_AM_WORK_DIR)) + advise(_("Use 'git am --show-current-patch=diff' to see the failed patch")); + + die_user_resolve(state); + } + +commit: + do_commit(state); + +next: + am_next(state); + + if (resume) + am_load(state); + resume = 0; + } + + if (!is_empty_or_missing_file(am_path(state, "rewritten"))) { + assert(state->rebasing); + copy_notes_for_rebase(state); + run_post_rewrite_hook(state); + } + + /* + * In rebasing mode, it's up to the caller to take care of + * housekeeping. + */ + if (!state->rebasing) { + am_destroy(state); + run_auto_maintenance(state->quiet); + } +} + +/** + * Resume the current am session after patch application failure. The user did + * all the hard work, and we do not have to do any patch application. Just + * trust and commit what the user has in the index and working tree. If `allow_empty` + * is true, commit as an empty commit when index has not changed and lacking a patch. + */ +static void am_resolve(struct am_state *state, int allow_empty) +{ + validate_resume_state(state); + + say(state, stdout, _("Applying: %.*s"), linelen(state->msg), state->msg); + + if (!repo_index_has_changes(the_repository, NULL, NULL)) { + if (allow_empty && is_empty_or_missing_file(am_path(state, "patch"))) { + printf_ln(_("No changes - recorded it as an empty commit.")); + } else { + printf_ln(_("No changes - did you forget to use 'git add'?\n" + "If there is nothing left to stage, chances are that something else\n" + "already introduced the same changes; you might want to skip this patch.")); + die_user_resolve(state); + } + } + + if (unmerged_index(&the_index)) { + printf_ln(_("You still have unmerged paths in your index.\n" + "You should 'git add' each file with resolved conflicts to mark them as such.\n" + "You might run `git rm` on a file to accept \"deleted by them\" for it.")); + die_user_resolve(state); + } + + if (state->interactive) { + write_index_patch(state); + if (do_interactive(state)) + goto next; + } + + repo_rerere(the_repository, 0); + + do_commit(state); + +next: + am_next(state); + am_load(state); + am_run(state, 0); +} + +/** + * Performs a checkout fast-forward from `head` to `remote`. If `reset` is + * true, any unmerged entries will be discarded. Returns 0 on success, -1 on + * failure. + */ +static int fast_forward_to(struct tree *head, struct tree *remote, int reset) +{ + struct lock_file lock_file = LOCK_INIT; + struct unpack_trees_options opts; + struct tree_desc t[2]; + + if (parse_tree(head) || parse_tree(remote)) + return -1; + + repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR); + + refresh_index(&the_index, REFRESH_QUIET, NULL, NULL, NULL); + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = 1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.merge = 1; + opts.reset = reset ? UNPACK_RESET_PROTECT_UNTRACKED : 0; + opts.preserve_ignored = 0; /* FIXME: !overwrite_ignore */ + opts.fn = twoway_merge; + init_tree_desc(&t[0], head->buffer, head->size); + init_tree_desc(&t[1], remote->buffer, remote->size); + + if (unpack_trees(2, t, &opts)) { + rollback_lock_file(&lock_file); + return -1; + } + + if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK)) + die(_("unable to write new index file")); + + return 0; +} + +/** + * Merges a tree into the index. The index's stat info will take precedence + * over the merged tree's. Returns 0 on success, -1 on failure. + */ +static int merge_tree(struct tree *tree) +{ + struct lock_file lock_file = LOCK_INIT; + struct unpack_trees_options opts; + struct tree_desc t[1]; + + if (parse_tree(tree)) + return -1; + + repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR); + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = 1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.merge = 1; + opts.fn = oneway_merge; + init_tree_desc(&t[0], tree->buffer, tree->size); + + if (unpack_trees(1, t, &opts)) { + rollback_lock_file(&lock_file); + return -1; + } + + if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK)) + die(_("unable to write new index file")); + + return 0; +} + +/** + * Clean the index without touching entries that are not modified between + * `head` and `remote`. + */ +static int clean_index(const struct object_id *head, const struct object_id *remote) +{ + struct tree *head_tree, *remote_tree, *index_tree; + struct object_id index; + + head_tree = parse_tree_indirect(head); + if (!head_tree) + return error(_("Could not parse object '%s'."), oid_to_hex(head)); + + remote_tree = parse_tree_indirect(remote); + if (!remote_tree) + return error(_("Could not parse object '%s'."), oid_to_hex(remote)); + + repo_read_index_unmerged(the_repository); + + if (fast_forward_to(head_tree, head_tree, 1)) + return -1; + + if (write_index_as_tree(&index, &the_index, get_index_file(), 0, NULL)) + return -1; + + index_tree = parse_tree_indirect(&index); + if (!index_tree) + return error(_("Could not parse object '%s'."), oid_to_hex(&index)); + + if (fast_forward_to(index_tree, remote_tree, 0)) + return -1; + + if (merge_tree(remote_tree)) + return -1; + + remove_branch_state(the_repository, 0); + + return 0; +} + +/** + * Resets rerere's merge resolution metadata. + */ +static void am_rerere_clear(void) +{ + struct string_list merge_rr = STRING_LIST_INIT_DUP; + rerere_clear(the_repository, &merge_rr); + string_list_clear(&merge_rr, 1); +} + +/** + * Resume the current am session by skipping the current patch. + */ +static void am_skip(struct am_state *state) +{ + struct object_id head; + + am_rerere_clear(); + + if (repo_get_oid(the_repository, "HEAD", &head)) + oidcpy(&head, the_hash_algo->empty_tree); + + if (clean_index(&head, &head)) + die(_("failed to clean index")); + + if (state->rebasing) { + FILE *fp = xfopen(am_path(state, "rewritten"), "a"); + + assert(!is_null_oid(&state->orig_commit)); + fprintf(fp, "%s ", oid_to_hex(&state->orig_commit)); + fprintf(fp, "%s\n", oid_to_hex(&head)); + fclose(fp); + } + + am_next(state); + am_load(state); + am_run(state, 0); +} + +/** + * Returns true if it is safe to reset HEAD to the ORIG_HEAD, false otherwise. + * + * It is not safe to reset HEAD when: + * 1. git-am previously failed because the index was dirty. + * 2. HEAD has moved since git-am previously failed. + */ +static int safe_to_abort(const struct am_state *state) +{ + struct strbuf sb = STRBUF_INIT; + struct object_id abort_safety, head; + + if (file_exists(am_path(state, "dirtyindex"))) + return 0; + + if (read_state_file(&sb, state, "abort-safety", 1) > 0) { + if (get_oid_hex(sb.buf, &abort_safety)) + die(_("could not parse %s"), am_path(state, "abort-safety")); + } else + oidclr(&abort_safety); + strbuf_release(&sb); + + if (repo_get_oid(the_repository, "HEAD", &head)) + oidclr(&head); + + if (oideq(&head, &abort_safety)) + return 1; + + warning(_("You seem to have moved HEAD since the last 'am' failure.\n" + "Not rewinding to ORIG_HEAD")); + + return 0; +} + +/** + * Aborts the current am session if it is safe to do so. + */ +static void am_abort(struct am_state *state) +{ + struct object_id curr_head, orig_head; + int has_curr_head, has_orig_head; + char *curr_branch; + + if (!safe_to_abort(state)) { + am_destroy(state); + return; + } + + am_rerere_clear(); + + curr_branch = resolve_refdup("HEAD", 0, &curr_head, NULL); + has_curr_head = curr_branch && !is_null_oid(&curr_head); + if (!has_curr_head) + oidcpy(&curr_head, the_hash_algo->empty_tree); + + has_orig_head = !repo_get_oid(the_repository, "ORIG_HEAD", &orig_head); + if (!has_orig_head) + oidcpy(&orig_head, the_hash_algo->empty_tree); + + if (clean_index(&curr_head, &orig_head)) + die(_("failed to clean index")); + + if (has_orig_head) + update_ref("am --abort", "HEAD", &orig_head, + has_curr_head ? &curr_head : NULL, 0, + UPDATE_REFS_DIE_ON_ERR); + else if (curr_branch) + delete_ref(NULL, curr_branch, NULL, REF_NO_DEREF); + + free(curr_branch); + am_destroy(state); +} + +static int show_patch(struct am_state *state, enum resume_type resume_mode) +{ + struct strbuf sb = STRBUF_INIT; + const char *patch_path; + int len; + + if (!is_null_oid(&state->orig_commit)) { + struct child_process cmd = CHILD_PROCESS_INIT; + + strvec_pushl(&cmd.args, "show", oid_to_hex(&state->orig_commit), + "--", NULL); + cmd.git_cmd = 1; + return run_command(&cmd); + } + + switch (resume_mode) { + case RESUME_SHOW_PATCH_RAW: + patch_path = am_path(state, msgnum(state)); + break; + case RESUME_SHOW_PATCH_DIFF: + patch_path = am_path(state, "patch"); + break; + default: + BUG("invalid mode for --show-current-patch"); + } + + len = strbuf_read_file(&sb, patch_path, 0); + if (len < 0) + die_errno(_("failed to read '%s'"), patch_path); + + setup_pager(); + write_in_full(1, sb.buf, sb.len); + strbuf_release(&sb); + return 0; +} + +/** + * parse_options() callback that validates and sets opt->value to the + * PATCH_FORMAT_* enum value corresponding to `arg`. + */ +static int parse_opt_patchformat(const struct option *opt, const char *arg, int unset) +{ + int *opt_value = opt->value; + + if (unset) + *opt_value = PATCH_FORMAT_UNKNOWN; + else if (!strcmp(arg, "mbox")) + *opt_value = PATCH_FORMAT_MBOX; + else if (!strcmp(arg, "stgit")) + *opt_value = PATCH_FORMAT_STGIT; + else if (!strcmp(arg, "stgit-series")) + *opt_value = PATCH_FORMAT_STGIT_SERIES; + else if (!strcmp(arg, "hg")) + *opt_value = PATCH_FORMAT_HG; + else if (!strcmp(arg, "mboxrd")) + *opt_value = PATCH_FORMAT_MBOXRD; + /* + * Please update $__git_patchformat in git-completion.bash + * when you add new options + */ + else + return error(_("invalid value for '%s': '%s'"), + "--patch-format", arg); + return 0; +} + +static int parse_opt_show_current_patch(const struct option *opt, const char *arg, int unset) +{ + int *opt_value = opt->value; + + BUG_ON_OPT_NEG(unset); + + if (!arg) + *opt_value = opt->defval; + else if (!strcmp(arg, "raw")) + *opt_value = RESUME_SHOW_PATCH_RAW; + else if (!strcmp(arg, "diff")) + *opt_value = RESUME_SHOW_PATCH_DIFF; + /* + * Please update $__git_showcurrentpatch in git-completion.bash + * when you add new options + */ + else + return error(_("invalid value for '%s': '%s'"), + "--show-current-patch", arg); + return 0; +} + +int cmd_am(int argc, const char **argv, const char *prefix) +{ + struct am_state state; + int binary = -1; + int keep_cr = -1; + int patch_format = PATCH_FORMAT_UNKNOWN; + enum resume_type resume_mode = RESUME_FALSE; + int in_progress; + int ret = 0; + + const char * const usage[] = { + N_("git am [] [( | )...]"), + N_("git am [] (--continue | --skip | --abort)"), + NULL + }; + + struct option options[] = { + OPT_BOOL('i', "interactive", &state.interactive, + N_("run interactively")), + OPT_BOOL('n', "no-verify", &state.no_verify, + N_("bypass pre-applypatch and applypatch-msg hooks")), + OPT_HIDDEN_BOOL('b', "binary", &binary, + N_("historical option -- no-op")), + OPT_BOOL('3', "3way", &state.threeway, + N_("allow fall back on 3way merging if needed")), + OPT__QUIET(&state.quiet, N_("be quiet")), + OPT_SET_INT('s', "signoff", &state.signoff, + N_("add a Signed-off-by trailer to the commit message"), + SIGNOFF_EXPLICIT), + OPT_BOOL('u', "utf8", &state.utf8, + N_("recode into utf8 (default)")), + OPT_SET_INT('k', "keep", &state.keep, + N_("pass -k flag to git-mailinfo"), KEEP_TRUE), + OPT_SET_INT(0, "keep-non-patch", &state.keep, + N_("pass -b flag to git-mailinfo"), KEEP_NON_PATCH), + OPT_BOOL('m', "message-id", &state.message_id, + N_("pass -m flag to git-mailinfo")), + OPT_SET_INT(0, "keep-cr", &keep_cr, + N_("pass --keep-cr flag to git-mailsplit for mbox format"), + 1), + OPT_BOOL('c', "scissors", &state.scissors, + N_("strip everything before a scissors line")), + OPT_CALLBACK_F(0, "quoted-cr", &state.quoted_cr, N_("action"), + N_("pass it through git-mailinfo"), + PARSE_OPT_NONEG, am_option_parse_quoted_cr), + OPT_PASSTHRU_ARGV(0, "whitespace", &state.git_apply_opts, N_("action"), + N_("pass it through git-apply"), + 0), + OPT_PASSTHRU_ARGV(0, "ignore-space-change", &state.git_apply_opts, NULL, + N_("pass it through git-apply"), + PARSE_OPT_NOARG), + OPT_PASSTHRU_ARGV(0, "ignore-whitespace", &state.git_apply_opts, NULL, + N_("pass it through git-apply"), + PARSE_OPT_NOARG), + OPT_PASSTHRU_ARGV(0, "directory", &state.git_apply_opts, N_("root"), + N_("pass it through git-apply"), + 0), + OPT_PASSTHRU_ARGV(0, "exclude", &state.git_apply_opts, N_("path"), + N_("pass it through git-apply"), + 0), + OPT_PASSTHRU_ARGV(0, "include", &state.git_apply_opts, N_("path"), + N_("pass it through git-apply"), + 0), + OPT_PASSTHRU_ARGV('C', NULL, &state.git_apply_opts, N_("n"), + N_("pass it through git-apply"), + 0), + OPT_PASSTHRU_ARGV('p', NULL, &state.git_apply_opts, N_("num"), + N_("pass it through git-apply"), + 0), + OPT_CALLBACK(0, "patch-format", &patch_format, N_("format"), + N_("format the patch(es) are in"), + parse_opt_patchformat), + OPT_PASSTHRU_ARGV(0, "reject", &state.git_apply_opts, NULL, + N_("pass it through git-apply"), + PARSE_OPT_NOARG), + OPT_STRING(0, "resolvemsg", &state.resolvemsg, NULL, + N_("override error message when patch failure occurs")), + OPT_CMDMODE(0, "continue", &resume_mode, + N_("continue applying patches after resolving a conflict"), + RESUME_RESOLVED), + OPT_CMDMODE('r', "resolved", &resume_mode, + N_("synonyms for --continue"), + RESUME_RESOLVED), + OPT_CMDMODE(0, "skip", &resume_mode, + N_("skip the current patch"), + RESUME_SKIP), + OPT_CMDMODE(0, "abort", &resume_mode, + N_("restore the original branch and abort the patching operation"), + RESUME_ABORT), + OPT_CMDMODE(0, "quit", &resume_mode, + N_("abort the patching operation but keep HEAD where it is"), + RESUME_QUIT), + { OPTION_CALLBACK, 0, "show-current-patch", &resume_mode, + "(diff|raw)", + N_("show the patch being applied"), + PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, + parse_opt_show_current_patch, RESUME_SHOW_PATCH_RAW }, + OPT_CMDMODE(0, "allow-empty", &resume_mode, + N_("record the empty patch as an empty commit"), + RESUME_ALLOW_EMPTY), + OPT_BOOL(0, "committer-date-is-author-date", + &state.committer_date_is_author_date, + N_("lie about committer date")), + OPT_BOOL(0, "ignore-date", &state.ignore_date, + N_("use current timestamp for author date")), + OPT_RERERE_AUTOUPDATE(&state.allow_rerere_autoupdate), + { OPTION_STRING, 'S', "gpg-sign", &state.sign_commit, N_("key-id"), + N_("GPG-sign commits"), + PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + OPT_CALLBACK_F(0, "empty", &state.empty_type, "(stop|drop|keep)", + N_("how to handle empty patches"), + PARSE_OPT_NONEG, am_option_parse_empty), + OPT_HIDDEN_BOOL(0, "rebasing", &state.rebasing, + N_("(internal use for git-rebase)")), + OPT_END() + }; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(usage, options); + + git_config(git_default_config, NULL); + + am_state_init(&state); + + in_progress = am_in_progress(&state); + if (in_progress) + am_load(&state); + + argc = parse_options(argc, argv, prefix, options, usage, 0); + + if (binary >= 0) + fprintf_ln(stderr, _("The -b/--binary option has been a no-op for long time, and\n" + "it will be removed. Please do not use it anymore.")); + + /* Ensure a valid committer ident can be constructed */ + git_committer_info(IDENT_STRICT); + + if (repo_read_index_preload(the_repository, NULL, 0) < 0) + die(_("failed to read the index")); + + if (in_progress) { + /* + * Catch user error to feed us patches when there is a session + * in progress: + * + * 1. mbox path(s) are provided on the command-line. + * 2. stdin is not a tty: the user is trying to feed us a patch + * from standard input. This is somewhat unreliable -- stdin + * could be /dev/null for example and the caller did not + * intend to feed us a patch but wanted to continue + * unattended. + */ + if (argc || (resume_mode == RESUME_FALSE && !isatty(0))) + die(_("previous rebase directory %s still exists but mbox given."), + state.dir); + + if (resume_mode == RESUME_FALSE) + resume_mode = RESUME_APPLY; + + if (state.signoff == SIGNOFF_EXPLICIT) + am_append_signoff(&state); + } else { + struct strvec paths = STRVEC_INIT; + int i; + + /* + * Handle stray state directory in the independent-run case. In + * the --rebasing case, it is up to the caller to take care of + * stray directories. + */ + if (file_exists(state.dir) && !state.rebasing) { + if (resume_mode == RESUME_ABORT || resume_mode == RESUME_QUIT) { + am_destroy(&state); + am_state_release(&state); + return 0; + } + + die(_("Stray %s directory found.\n" + "Use \"git am --abort\" to remove it."), + state.dir); + } + + if (resume_mode) + die(_("Resolve operation not in progress, we are not resuming.")); + + for (i = 0; i < argc; i++) { + if (is_absolute_path(argv[i]) || !prefix) + strvec_push(&paths, argv[i]); + else + strvec_push(&paths, mkpath("%s/%s", prefix, argv[i])); + } + + if (state.interactive && !paths.nr) + die(_("interactive mode requires patches on the command line")); + + am_setup(&state, patch_format, paths.v, keep_cr); + + strvec_clear(&paths); + } + + switch (resume_mode) { + case RESUME_FALSE: + am_run(&state, 0); + break; + case RESUME_APPLY: + am_run(&state, 1); + break; + case RESUME_RESOLVED: + case RESUME_ALLOW_EMPTY: + am_resolve(&state, resume_mode == RESUME_ALLOW_EMPTY ? 1 : 0); + break; + case RESUME_SKIP: + am_skip(&state); + break; + case RESUME_ABORT: + am_abort(&state); + break; + case RESUME_QUIT: + am_rerere_clear(); + am_destroy(&state); + break; + case RESUME_SHOW_PATCH_RAW: + case RESUME_SHOW_PATCH_DIFF: + ret = show_patch(&state, resume_mode); + break; + default: + BUG("invalid resume value"); + } + + am_state_release(&state); + + return ret; +} diff --git a/builtin/annotate.c b/builtin/annotate.c new file mode 100644 index 0000000..58ff977 --- /dev/null +++ b/builtin/annotate.c @@ -0,0 +1,22 @@ +/* + * "git annotate" builtin alias + * + * Copyright (C) 2006 Ryan Anderson + */ +#include "git-compat-util.h" +#include "builtin.h" +#include "strvec.h" + +int cmd_annotate(int argc, const char **argv, const char *prefix) +{ + struct strvec args = STRVEC_INIT; + int i; + + strvec_pushl(&args, "annotate", "-c", NULL); + + for (i = 1; i < argc; i++) { + strvec_push(&args, argv[i]); + } + + return cmd_blame(args.nr, args.v, prefix); +} diff --git a/builtin/apply.c b/builtin/apply.c new file mode 100644 index 0000000..c18b7ea --- /dev/null +++ b/builtin/apply.c @@ -0,0 +1,34 @@ +#include "builtin.h" +#include "gettext.h" +#include "parse-options.h" +#include "repository.h" +#include "apply.h" + +static const char * const apply_usage[] = { + N_("git apply [] [...]"), + NULL +}; + +int cmd_apply(int argc, const char **argv, const char *prefix) +{ + int force_apply = 0; + int options = 0; + int ret; + struct apply_state state; + + if (init_apply_state(&state, the_repository, prefix)) + exit(128); + + argc = apply_parse_options(argc, argv, + &state, &force_apply, &options, + apply_usage); + + if (check_apply_state(&state, force_apply)) + exit(128); + + ret = apply_all_patches(&state, argc, argv, options); + + clear_apply_state(&state); + + return ret; +} diff --git a/builtin/archive.c b/builtin/archive.c new file mode 100644 index 0000000..90761fd --- /dev/null +++ b/builtin/archive.c @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2006 Franck Bui-Huu + * Copyright (c) 2006 Rene Scharfe + */ +#include "builtin.h" +#include "archive.h" +#include "gettext.h" +#include "transport.h" +#include "parse-options.h" +#include "pkt-line.h" +#include "repository.h" +#include "sideband.h" + +static void create_output_file(const char *output_file) +{ + int output_fd = xopen(output_file, O_CREAT | O_WRONLY | O_TRUNC, 0666); + if (output_fd != 1) { + if (dup2(output_fd, 1) < 0) + die_errno(_("could not redirect output")); + else + close(output_fd); + } +} + +static int run_remote_archiver(int argc, const char **argv, + const char *remote, const char *exec, + const char *name_hint) +{ + int fd[2], i, rv; + struct transport *transport; + struct remote *_remote; + struct packet_reader reader; + + _remote = remote_get(remote); + if (!_remote->url[0]) + die(_("git archive: Remote with no URL")); + transport = transport_get(_remote, _remote->url[0]); + transport_connect(transport, "git-upload-archive", exec, fd); + + /* + * Inject a fake --format field at the beginning of the + * arguments, with the format inferred from our output + * filename. This way explicit --format options can override + * it. + */ + if (name_hint) { + const char *format = archive_format_from_filename(name_hint); + if (format) + packet_write_fmt(fd[1], "argument --format=%s\n", format); + } + for (i = 1; i < argc; i++) + packet_write_fmt(fd[1], "argument %s\n", argv[i]); + packet_flush(fd[1]); + + packet_reader_init(&reader, fd[0], NULL, 0, + PACKET_READ_CHOMP_NEWLINE | + PACKET_READ_DIE_ON_ERR_PACKET); + + if (packet_reader_read(&reader) != PACKET_READ_NORMAL) + die(_("git archive: expected ACK/NAK, got a flush packet")); + if (strcmp(reader.line, "ACK")) { + if (starts_with(reader.line, "NACK ")) + die(_("git archive: NACK %s"), reader.line + 5); + die(_("git archive: protocol error")); + } + + if (packet_reader_read(&reader) != PACKET_READ_FLUSH) + die(_("git archive: expected a flush")); + + /* Now, start reading from fd[0] and spit it out to stdout */ + rv = recv_sideband("archive", fd[0], 1); + rv |= transport_disconnect(transport); + + return !!rv; +} + +#define PARSE_OPT_KEEP_ALL ( PARSE_OPT_KEEP_DASHDASH | \ + PARSE_OPT_KEEP_ARGV0 | \ + PARSE_OPT_KEEP_UNKNOWN_OPT | \ + PARSE_OPT_NO_INTERNAL_HELP ) + +int cmd_archive(int argc, const char **argv, const char *prefix) +{ + const char *exec = "git-upload-archive"; + char *output = NULL; + const char *remote = NULL; + struct option local_opts[] = { + OPT_FILENAME('o', "output", &output, + N_("write the archive to this file")), + OPT_STRING(0, "remote", &remote, N_("repo"), + N_("retrieve the archive from remote repository ")), + OPT_STRING(0, "exec", &exec, N_("command"), + N_("path to the remote git-upload-archive command")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, local_opts, NULL, + PARSE_OPT_KEEP_ALL); + + init_archivers(); + + if (output) + create_output_file(output); + + if (remote) + return run_remote_archiver(argc, argv, remote, exec, output); + + setvbuf(stderr, NULL, _IOLBF, BUFSIZ); + + UNLEAK(output); + return write_archive(argc, argv, prefix, the_repository, output, 0); +} diff --git a/builtin/bisect.c b/builtin/bisect.c new file mode 100644 index 0000000..35938b0 --- /dev/null +++ b/builtin/bisect.c @@ -0,0 +1,1449 @@ +#include "builtin.h" +#include "copy.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "object-name.h" +#include "parse-options.h" +#include "bisect.h" +#include "refs.h" +#include "dir.h" +#include "strvec.h" +#include "run-command.h" +#include "oid-array.h" +#include "path.h" +#include "prompt.h" +#include "quote.h" +#include "revision.h" + +static GIT_PATH_FUNC(git_path_bisect_terms, "BISECT_TERMS") +static GIT_PATH_FUNC(git_path_bisect_expected_rev, "BISECT_EXPECTED_REV") +static GIT_PATH_FUNC(git_path_bisect_ancestors_ok, "BISECT_ANCESTORS_OK") +static GIT_PATH_FUNC(git_path_bisect_start, "BISECT_START") +static GIT_PATH_FUNC(git_path_bisect_log, "BISECT_LOG") +static GIT_PATH_FUNC(git_path_bisect_names, "BISECT_NAMES") +static GIT_PATH_FUNC(git_path_bisect_first_parent, "BISECT_FIRST_PARENT") +static GIT_PATH_FUNC(git_path_bisect_run, "BISECT_RUN") + +#define BUILTIN_GIT_BISECT_START_USAGE \ + N_("git bisect start [--term-(new|bad)= --term-(old|good)=]" \ + " [--no-checkout] [--first-parent] [ [...]] [--]" \ + " [...]") +#define BUILTIN_GIT_BISECT_STATE_USAGE \ + N_("git bisect (good|bad) [...]") +#define BUILTIN_GIT_BISECT_TERMS_USAGE \ + "git bisect terms [--term-good | --term-bad]" +#define BUILTIN_GIT_BISECT_SKIP_USAGE \ + N_("git bisect skip [(|)...]") +#define BUILTIN_GIT_BISECT_NEXT_USAGE \ + "git bisect next" +#define BUILTIN_GIT_BISECT_RESET_USAGE \ + N_("git bisect reset []") +#define BUILTIN_GIT_BISECT_VISUALIZE_USAGE \ + "git bisect visualize" +#define BUILTIN_GIT_BISECT_REPLAY_USAGE \ + N_("git bisect replay ") +#define BUILTIN_GIT_BISECT_LOG_USAGE \ + "git bisect log" +#define BUILTIN_GIT_BISECT_RUN_USAGE \ + N_("git bisect run [...]") + +static const char * const git_bisect_usage[] = { + BUILTIN_GIT_BISECT_START_USAGE, + BUILTIN_GIT_BISECT_STATE_USAGE, + BUILTIN_GIT_BISECT_TERMS_USAGE, + BUILTIN_GIT_BISECT_SKIP_USAGE, + BUILTIN_GIT_BISECT_NEXT_USAGE, + BUILTIN_GIT_BISECT_RESET_USAGE, + BUILTIN_GIT_BISECT_VISUALIZE_USAGE, + BUILTIN_GIT_BISECT_REPLAY_USAGE, + BUILTIN_GIT_BISECT_LOG_USAGE, + BUILTIN_GIT_BISECT_RUN_USAGE, + NULL +}; + +struct add_bisect_ref_data { + struct rev_info *revs; + unsigned int object_flags; +}; + +struct bisect_terms { + char *term_good; + char *term_bad; +}; + +static void free_terms(struct bisect_terms *terms) +{ + FREE_AND_NULL(terms->term_good); + FREE_AND_NULL(terms->term_bad); +} + +static void set_terms(struct bisect_terms *terms, const char *bad, + const char *good) +{ + free((void *)terms->term_good); + terms->term_good = xstrdup(good); + free((void *)terms->term_bad); + terms->term_bad = xstrdup(bad); +} + +static const char vocab_bad[] = "bad|new"; +static const char vocab_good[] = "good|old"; + +static int bisect_autostart(struct bisect_terms *terms); + +/* + * Check whether the string `term` belongs to the set of strings + * included in the variable arguments. + */ +LAST_ARG_MUST_BE_NULL +static int one_of(const char *term, ...) +{ + int res = 0; + va_list matches; + const char *match; + + va_start(matches, term); + while (!res && (match = va_arg(matches, const char *))) + res = !strcmp(term, match); + va_end(matches); + + return res; +} + +/* + * return code BISECT_INTERNAL_SUCCESS_MERGE_BASE + * and BISECT_INTERNAL_SUCCESS_1ST_BAD_FOUND are codes + * that indicate special success. + */ + +static int is_bisect_success(enum bisect_error res) +{ + return !res || + res == BISECT_INTERNAL_SUCCESS_1ST_BAD_FOUND || + res == BISECT_INTERNAL_SUCCESS_MERGE_BASE; +} + +static int write_in_file(const char *path, const char *mode, const char *format, va_list args) +{ + FILE *fp = NULL; + int res = 0; + + if (strcmp(mode, "w") && strcmp(mode, "a")) + BUG("write-in-file does not support '%s' mode", mode); + fp = fopen(path, mode); + if (!fp) + return error_errno(_("cannot open file '%s' in mode '%s'"), path, mode); + res = vfprintf(fp, format, args); + + if (res < 0) { + int saved_errno = errno; + fclose(fp); + errno = saved_errno; + return error_errno(_("could not write to file '%s'"), path); + } + + return fclose(fp); +} + +__attribute__((format (printf, 2, 3))) +static int write_to_file(const char *path, const char *format, ...) +{ + int res; + va_list args; + + va_start(args, format); + res = write_in_file(path, "w", format, args); + va_end(args); + + return res; +} + +__attribute__((format (printf, 2, 3))) +static int append_to_file(const char *path, const char *format, ...) +{ + int res; + va_list args; + + va_start(args, format); + res = write_in_file(path, "a", format, args); + va_end(args); + + return res; +} + +static int print_file_to_stdout(const char *path) +{ + int fd = open(path, O_RDONLY); + int ret = 0; + + if (fd < 0) + return error_errno(_("cannot open file '%s' for reading"), path); + if (copy_fd(fd, 1) < 0) + ret = error_errno(_("failed to read '%s'"), path); + close(fd); + return ret; +} + +static int check_term_format(const char *term, const char *orig_term) +{ + int res; + char *new_term = xstrfmt("refs/bisect/%s", term); + + res = check_refname_format(new_term, 0); + free(new_term); + + if (res) + return error(_("'%s' is not a valid term"), term); + + if (one_of(term, "help", "start", "skip", "next", "reset", + "visualize", "view", "replay", "log", "run", "terms", NULL)) + return error(_("can't use the builtin command '%s' as a term"), term); + + /* + * In theory, nothing prevents swapping completely good and bad, + * but this situation could be confusing and hasn't been tested + * enough. Forbid it for now. + */ + + if ((strcmp(orig_term, "bad") && one_of(term, "bad", "new", NULL)) || + (strcmp(orig_term, "good") && one_of(term, "good", "old", NULL))) + return error(_("can't change the meaning of the term '%s'"), term); + + return 0; +} + +static int write_terms(const char *bad, const char *good) +{ + int res; + + if (!strcmp(bad, good)) + return error(_("please use two different terms")); + + if (check_term_format(bad, "bad") || check_term_format(good, "good")) + return -1; + + res = write_to_file(git_path_bisect_terms(), "%s\n%s\n", bad, good); + + return res; +} + +static int bisect_reset(const char *commit) +{ + struct strbuf branch = STRBUF_INIT; + + if (!commit) { + if (strbuf_read_file(&branch, git_path_bisect_start(), 0) < 1) { + printf(_("We are not bisecting.\n")); + return 0; + } + strbuf_rtrim(&branch); + } else { + struct object_id oid; + + if (repo_get_oid_commit(the_repository, commit, &oid)) + return error(_("'%s' is not a valid commit"), commit); + strbuf_addstr(&branch, commit); + } + + if (!ref_exists("BISECT_HEAD")) { + struct child_process cmd = CHILD_PROCESS_INIT; + + cmd.git_cmd = 1; + strvec_pushl(&cmd.args, "checkout", "--ignore-other-worktrees", + branch.buf, "--", NULL); + if (run_command(&cmd)) { + error(_("could not check out original" + " HEAD '%s'. Try 'git bisect" + " reset '."), branch.buf); + strbuf_release(&branch); + return -1; + } + } + + strbuf_release(&branch); + return bisect_clean_state(); +} + +static void log_commit(FILE *fp, char *fmt, const char *state, + struct commit *commit) +{ + struct pretty_print_context pp = {0}; + struct strbuf commit_msg = STRBUF_INIT; + char *label = xstrfmt(fmt, state); + + repo_format_commit_message(the_repository, commit, "%s", &commit_msg, + &pp); + + fprintf(fp, "# %s: [%s] %s\n", label, oid_to_hex(&commit->object.oid), + commit_msg.buf); + + strbuf_release(&commit_msg); + free(label); +} + +static int bisect_write(const char *state, const char *rev, + const struct bisect_terms *terms, int nolog) +{ + struct strbuf tag = STRBUF_INIT; + struct object_id oid; + struct commit *commit; + FILE *fp = NULL; + int res = 0; + + if (!strcmp(state, terms->term_bad)) { + strbuf_addf(&tag, "refs/bisect/%s", state); + } else if (one_of(state, terms->term_good, "skip", NULL)) { + strbuf_addf(&tag, "refs/bisect/%s-%s", state, rev); + } else { + res = error(_("Bad bisect_write argument: %s"), state); + goto finish; + } + + if (repo_get_oid(the_repository, rev, &oid)) { + res = error(_("couldn't get the oid of the rev '%s'"), rev); + goto finish; + } + + if (update_ref(NULL, tag.buf, &oid, NULL, 0, + UPDATE_REFS_MSG_ON_ERR)) { + res = -1; + goto finish; + } + + fp = fopen(git_path_bisect_log(), "a"); + if (!fp) { + res = error_errno(_("couldn't open the file '%s'"), git_path_bisect_log()); + goto finish; + } + + commit = lookup_commit_reference(the_repository, &oid); + log_commit(fp, "%s", state, commit); + + if (!nolog) + fprintf(fp, "git bisect %s %s\n", state, rev); + +finish: + if (fp) + fclose(fp); + strbuf_release(&tag); + return res; +} + +static int check_and_set_terms(struct bisect_terms *terms, const char *cmd) +{ + int has_term_file = !is_empty_or_missing_file(git_path_bisect_terms()); + + if (one_of(cmd, "skip", "start", "terms", NULL)) + return 0; + + if (has_term_file && strcmp(cmd, terms->term_bad) && + strcmp(cmd, terms->term_good)) + return error(_("Invalid command: you're currently in a " + "%s/%s bisect"), terms->term_bad, + terms->term_good); + + if (!has_term_file) { + if (one_of(cmd, "bad", "good", NULL)) { + set_terms(terms, "bad", "good"); + return write_terms(terms->term_bad, terms->term_good); + } + if (one_of(cmd, "new", "old", NULL)) { + set_terms(terms, "new", "old"); + return write_terms(terms->term_bad, terms->term_good); + } + } + + return 0; +} + +static int inc_nr(const char *refname UNUSED, + const struct object_id *oid UNUSED, + int flag UNUSED, void *cb_data) +{ + unsigned int *nr = (unsigned int *)cb_data; + (*nr)++; + return 0; +} + +static const char need_bad_and_good_revision_warning[] = + N_("You need to give me at least one %s and %s revision.\n" + "You can use \"git bisect %s\" and \"git bisect %s\" for that."); + +static const char need_bisect_start_warning[] = + N_("You need to start by \"git bisect start\".\n" + "You then need to give me at least one %s and %s revision.\n" + "You can use \"git bisect %s\" and \"git bisect %s\" for that."); + +static int decide_next(const struct bisect_terms *terms, + const char *current_term, int missing_good, + int missing_bad) +{ + if (!missing_good && !missing_bad) + return 0; + if (!current_term) + return -1; + + if (missing_good && !missing_bad && + !strcmp(current_term, terms->term_good)) { + char *yesno; + /* + * have bad (or new) but not good (or old). We could bisect + * although this is less optimum. + */ + warning(_("bisecting only with a %s commit"), terms->term_bad); + if (!isatty(0)) + return 0; + /* + * TRANSLATORS: Make sure to include [Y] and [n] in your + * translation. The program will only accept English input + * at this point. + */ + yesno = git_prompt(_("Are you sure [Y/n]? "), PROMPT_ECHO); + if (starts_with(yesno, "N") || starts_with(yesno, "n")) + return -1; + return 0; + } + + if (!is_empty_or_missing_file(git_path_bisect_start())) + return error(_(need_bad_and_good_revision_warning), + vocab_bad, vocab_good, vocab_bad, vocab_good); + else + return error(_(need_bisect_start_warning), + vocab_good, vocab_bad, vocab_good, vocab_bad); +} + +static void bisect_status(struct bisect_state *state, + const struct bisect_terms *terms) +{ + char *bad_ref = xstrfmt("refs/bisect/%s", terms->term_bad); + char *good_glob = xstrfmt("%s-*", terms->term_good); + + if (ref_exists(bad_ref)) + state->nr_bad = 1; + + for_each_glob_ref_in(inc_nr, good_glob, "refs/bisect/", + (void *) &state->nr_good); + + free(good_glob); + free(bad_ref); +} + +__attribute__((format (printf, 1, 2))) +static void bisect_log_printf(const char *fmt, ...) +{ + struct strbuf buf = STRBUF_INIT; + va_list ap; + + va_start(ap, fmt); + strbuf_vaddf(&buf, fmt, ap); + va_end(ap); + + printf("%s", buf.buf); + append_to_file(git_path_bisect_log(), "# %s", buf.buf); + + strbuf_release(&buf); +} + +static void bisect_print_status(const struct bisect_terms *terms) +{ + struct bisect_state state = { 0 }; + + bisect_status(&state, terms); + + /* If we had both, we'd already be started, and shouldn't get here. */ + if (state.nr_good && state.nr_bad) + return; + + if (!state.nr_good && !state.nr_bad) + bisect_log_printf(_("status: waiting for both good and bad commits\n")); + else if (state.nr_good) + bisect_log_printf(Q_("status: waiting for bad commit, %d good commit known\n", + "status: waiting for bad commit, %d good commits known\n", + state.nr_good), state.nr_good); + else + bisect_log_printf(_("status: waiting for good commit(s), bad commit known\n")); +} + +static int bisect_next_check(const struct bisect_terms *terms, + const char *current_term) +{ + struct bisect_state state = { 0 }; + bisect_status(&state, terms); + return decide_next(terms, current_term, !state.nr_good, !state.nr_bad); +} + +static int get_terms(struct bisect_terms *terms) +{ + struct strbuf str = STRBUF_INIT; + FILE *fp = NULL; + int res = 0; + + fp = fopen(git_path_bisect_terms(), "r"); + if (!fp) { + res = -1; + goto finish; + } + + free_terms(terms); + strbuf_getline_lf(&str, fp); + terms->term_bad = strbuf_detach(&str, NULL); + strbuf_getline_lf(&str, fp); + terms->term_good = strbuf_detach(&str, NULL); + +finish: + if (fp) + fclose(fp); + strbuf_release(&str); + return res; +} + +static int bisect_terms(struct bisect_terms *terms, const char *option) +{ + if (get_terms(terms)) + return error(_("no terms defined")); + + if (!option) { + printf(_("Your current terms are %s for the old state\n" + "and %s for the new state.\n"), + terms->term_good, terms->term_bad); + return 0; + } + if (one_of(option, "--term-good", "--term-old", NULL)) + printf("%s\n", terms->term_good); + else if (one_of(option, "--term-bad", "--term-new", NULL)) + printf("%s\n", terms->term_bad); + else + return error(_("invalid argument %s for 'git bisect terms'.\n" + "Supported options are: " + "--term-good|--term-old and " + "--term-bad|--term-new."), option); + + return 0; +} + +static int bisect_append_log_quoted(const char **argv) +{ + int res = 0; + FILE *fp = fopen(git_path_bisect_log(), "a"); + struct strbuf orig_args = STRBUF_INIT; + + if (!fp) + return -1; + + if (fprintf(fp, "git bisect start") < 1) { + res = -1; + goto finish; + } + + sq_quote_argv(&orig_args, argv); + if (fprintf(fp, "%s\n", orig_args.buf) < 1) + res = -1; + +finish: + fclose(fp); + strbuf_release(&orig_args); + return res; +} + +static int add_bisect_ref(const char *refname, const struct object_id *oid, + int flags UNUSED, void *cb) +{ + struct add_bisect_ref_data *data = cb; + + add_pending_oid(data->revs, refname, oid, data->object_flags); + + return 0; +} + +static int prepare_revs(struct bisect_terms *terms, struct rev_info *revs) +{ + int res = 0; + struct add_bisect_ref_data cb = { revs }; + char *good = xstrfmt("%s-*", terms->term_good); + + /* + * We cannot use terms->term_bad directly in + * for_each_glob_ref_in() and we have to append a '*' to it, + * otherwise for_each_glob_ref_in() will append '/' and '*'. + */ + char *bad = xstrfmt("%s*", terms->term_bad); + + /* + * It is important to reset the flags used by revision walks + * as the previous call to bisect_next_all() in turn + * sets up a revision walk. + */ + reset_revision_walk(); + repo_init_revisions(the_repository, revs, NULL); + setup_revisions(0, NULL, revs, NULL); + for_each_glob_ref_in(add_bisect_ref, bad, "refs/bisect/", &cb); + cb.object_flags = UNINTERESTING; + for_each_glob_ref_in(add_bisect_ref, good, "refs/bisect/", &cb); + if (prepare_revision_walk(revs)) + res = error(_("revision walk setup failed\n")); + + free(good); + free(bad); + return res; +} + +static int bisect_skipped_commits(struct bisect_terms *terms) +{ + int res; + FILE *fp = NULL; + struct rev_info revs; + struct commit *commit; + struct pretty_print_context pp = {0}; + struct strbuf commit_name = STRBUF_INIT; + + res = prepare_revs(terms, &revs); + if (res) + return res; + + fp = fopen(git_path_bisect_log(), "a"); + if (!fp) + return error_errno(_("could not open '%s' for appending"), + git_path_bisect_log()); + + if (fprintf(fp, "# only skipped commits left to test\n") < 0) + return error_errno(_("failed to write to '%s'"), git_path_bisect_log()); + + while ((commit = get_revision(&revs)) != NULL) { + strbuf_reset(&commit_name); + repo_format_commit_message(the_repository, commit, "%s", + &commit_name, &pp); + fprintf(fp, "# possible first %s commit: [%s] %s\n", + terms->term_bad, oid_to_hex(&commit->object.oid), + commit_name.buf); + } + + /* + * Reset the flags used by revision walks in case + * there is another revision walk after this one. + */ + reset_revision_walk(); + + strbuf_release(&commit_name); + release_revisions(&revs); + fclose(fp); + return 0; +} + +static int bisect_successful(struct bisect_terms *terms) +{ + struct object_id oid; + struct commit *commit; + struct pretty_print_context pp = {0}; + struct strbuf commit_name = STRBUF_INIT; + char *bad_ref = xstrfmt("refs/bisect/%s",terms->term_bad); + int res; + + read_ref(bad_ref, &oid); + commit = lookup_commit_reference_by_name(bad_ref); + repo_format_commit_message(the_repository, commit, "%s", &commit_name, + &pp); + + res = append_to_file(git_path_bisect_log(), "# first %s commit: [%s] %s\n", + terms->term_bad, oid_to_hex(&commit->object.oid), + commit_name.buf); + + strbuf_release(&commit_name); + free(bad_ref); + return res; +} + +static enum bisect_error bisect_next(struct bisect_terms *terms, const char *prefix) +{ + enum bisect_error res; + + if (bisect_autostart(terms)) + return BISECT_FAILED; + + if (bisect_next_check(terms, terms->term_good)) + return BISECT_FAILED; + + /* Perform all bisection computation */ + res = bisect_next_all(the_repository, prefix); + + if (res == BISECT_INTERNAL_SUCCESS_1ST_BAD_FOUND) { + res = bisect_successful(terms); + return res ? res : BISECT_INTERNAL_SUCCESS_1ST_BAD_FOUND; + } else if (res == BISECT_ONLY_SKIPPED_LEFT) { + res = bisect_skipped_commits(terms); + return res ? res : BISECT_ONLY_SKIPPED_LEFT; + } + return res; +} + +static enum bisect_error bisect_auto_next(struct bisect_terms *terms, const char *prefix) +{ + if (bisect_next_check(terms, NULL)) { + bisect_print_status(terms); + return BISECT_OK; + } + + return bisect_next(terms, prefix); +} + +static enum bisect_error bisect_start(struct bisect_terms *terms, int argc, + const char **argv) +{ + int no_checkout = 0; + int first_parent_only = 0; + int i, has_double_dash = 0, must_write_terms = 0, bad_seen = 0; + int flags, pathspec_pos; + enum bisect_error res = BISECT_OK; + struct string_list revs = STRING_LIST_INIT_DUP; + struct string_list states = STRING_LIST_INIT_DUP; + struct strbuf start_head = STRBUF_INIT; + struct strbuf bisect_names = STRBUF_INIT; + struct object_id head_oid; + struct object_id oid; + const char *head; + + if (is_bare_repository()) + no_checkout = 1; + + /* + * Check for one bad and then some good revisions + */ + for (i = 0; i < argc; i++) { + if (!strcmp(argv[i], "--")) { + has_double_dash = 1; + break; + } + } + + for (i = 0; i < argc; i++) { + const char *arg = argv[i]; + if (!strcmp(argv[i], "--")) { + break; + } else if (!strcmp(arg, "--no-checkout")) { + no_checkout = 1; + } else if (!strcmp(arg, "--first-parent")) { + first_parent_only = 1; + } else if (!strcmp(arg, "--term-good") || + !strcmp(arg, "--term-old")) { + i++; + if (argc <= i) + return error(_("'' is not a valid term")); + must_write_terms = 1; + free((void *) terms->term_good); + terms->term_good = xstrdup(argv[i]); + } else if (skip_prefix(arg, "--term-good=", &arg) || + skip_prefix(arg, "--term-old=", &arg)) { + must_write_terms = 1; + free((void *) terms->term_good); + terms->term_good = xstrdup(arg); + } else if (!strcmp(arg, "--term-bad") || + !strcmp(arg, "--term-new")) { + i++; + if (argc <= i) + return error(_("'' is not a valid term")); + must_write_terms = 1; + free((void *) terms->term_bad); + terms->term_bad = xstrdup(argv[i]); + } else if (skip_prefix(arg, "--term-bad=", &arg) || + skip_prefix(arg, "--term-new=", &arg)) { + must_write_terms = 1; + free((void *) terms->term_bad); + terms->term_bad = xstrdup(arg); + } else if (starts_with(arg, "--")) { + return error(_("unrecognized option: '%s'"), arg); + } else if (!get_oidf(&oid, "%s^{commit}", arg)) { + string_list_append(&revs, oid_to_hex(&oid)); + } else if (has_double_dash) { + die(_("'%s' does not appear to be a valid " + "revision"), arg); + } else { + break; + } + } + pathspec_pos = i; + + /* + * The user ran "git bisect start ", hence did not + * explicitly specify the terms, but we are already starting to + * set references named with the default terms, and won't be able + * to change afterwards. + */ + if (revs.nr) + must_write_terms = 1; + for (i = 0; i < revs.nr; i++) { + if (bad_seen) { + string_list_append(&states, terms->term_good); + } else { + bad_seen = 1; + string_list_append(&states, terms->term_bad); + } + } + + /* + * Verify HEAD + */ + head = resolve_ref_unsafe("HEAD", 0, &head_oid, &flags); + if (!head) + if (repo_get_oid(the_repository, "HEAD", &head_oid)) + return error(_("bad HEAD - I need a HEAD")); + + /* + * Check if we are bisecting + */ + if (!is_empty_or_missing_file(git_path_bisect_start())) { + /* Reset to the rev from where we started */ + strbuf_read_file(&start_head, git_path_bisect_start(), 0); + strbuf_trim(&start_head); + if (!no_checkout) { + struct child_process cmd = CHILD_PROCESS_INIT; + + cmd.git_cmd = 1; + strvec_pushl(&cmd.args, "checkout", start_head.buf, + "--", NULL); + if (run_command(&cmd)) { + res = error(_("checking out '%s' failed." + " Try 'git bisect start " + "'."), + start_head.buf); + goto finish; + } + } + } else { + /* Get the rev from where we start. */ + if (!repo_get_oid(the_repository, head, &head_oid) && + !starts_with(head, "refs/heads/")) { + strbuf_reset(&start_head); + strbuf_addstr(&start_head, oid_to_hex(&head_oid)); + } else if (!repo_get_oid(the_repository, head, &head_oid) && + skip_prefix(head, "refs/heads/", &head)) { + strbuf_addstr(&start_head, head); + } else { + return error(_("bad HEAD - strange symbolic ref")); + } + } + + /* + * Get rid of any old bisect state. + */ + if (bisect_clean_state()) + return BISECT_FAILED; + + /* + * Write new start state + */ + write_file(git_path_bisect_start(), "%s\n", start_head.buf); + + if (first_parent_only) + write_file(git_path_bisect_first_parent(), "\n"); + + if (no_checkout) { + if (repo_get_oid(the_repository, start_head.buf, &oid) < 0) { + res = error(_("invalid ref: '%s'"), start_head.buf); + goto finish; + } + if (update_ref(NULL, "BISECT_HEAD", &oid, NULL, 0, + UPDATE_REFS_MSG_ON_ERR)) { + res = BISECT_FAILED; + goto finish; + } + } + + if (pathspec_pos < argc - 1) + sq_quote_argv(&bisect_names, argv + pathspec_pos); + write_file(git_path_bisect_names(), "%s\n", bisect_names.buf); + + for (i = 0; i < states.nr; i++) + if (bisect_write(states.items[i].string, + revs.items[i].string, terms, 1)) { + res = BISECT_FAILED; + goto finish; + } + + if (must_write_terms && write_terms(terms->term_bad, + terms->term_good)) { + res = BISECT_FAILED; + goto finish; + } + + res = bisect_append_log_quoted(argv); + if (res) + res = BISECT_FAILED; + +finish: + string_list_clear(&revs, 0); + string_list_clear(&states, 0); + strbuf_release(&start_head); + strbuf_release(&bisect_names); + if (res) + return res; + + res = bisect_auto_next(terms, NULL); + if (!is_bisect_success(res)) + bisect_clean_state(); + return res; +} + +static inline int file_is_not_empty(const char *path) +{ + return !is_empty_or_missing_file(path); +} + +static int bisect_autostart(struct bisect_terms *terms) +{ + int res; + const char *yesno; + + if (file_is_not_empty(git_path_bisect_start())) + return 0; + + fprintf_ln(stderr, _("You need to start by \"git bisect " + "start\"\n")); + + if (!isatty(STDIN_FILENO)) + return -1; + + /* + * TRANSLATORS: Make sure to include [Y] and [n] in your + * translation. The program will only accept English input + * at this point. + */ + yesno = git_prompt(_("Do you want me to do it for you " + "[Y/n]? "), PROMPT_ECHO); + res = tolower(*yesno) == 'n' ? + -1 : bisect_start(terms, 0, empty_strvec); + + return res; +} + +static enum bisect_error bisect_state(struct bisect_terms *terms, int argc, + const char **argv) +{ + const char *state; + int i, verify_expected = 1; + struct object_id oid, expected; + struct strbuf buf = STRBUF_INIT; + struct oid_array revs = OID_ARRAY_INIT; + + if (!argc) + return error(_("Please call `--bisect-state` with at least one argument")); + + if (bisect_autostart(terms)) + return BISECT_FAILED; + + state = argv[0]; + if (check_and_set_terms(terms, state) || + !one_of(state, terms->term_good, terms->term_bad, "skip", NULL)) + return BISECT_FAILED; + + argv++; + argc--; + if (argc > 1 && !strcmp(state, terms->term_bad)) + return error(_("'git bisect %s' can take only one argument."), terms->term_bad); + + if (argc == 0) { + const char *head = "BISECT_HEAD"; + enum get_oid_result res_head = repo_get_oid(the_repository, + head, &oid); + + if (res_head == MISSING_OBJECT) { + head = "HEAD"; + res_head = repo_get_oid(the_repository, head, &oid); + } + + if (res_head) + error(_("Bad rev input: %s"), head); + oid_array_append(&revs, &oid); + } + + /* + * All input revs must be checked before executing bisect_write() + * to discard junk revs. + */ + + for (; argc; argc--, argv++) { + struct commit *commit; + + if (repo_get_oid(the_repository, *argv, &oid)){ + error(_("Bad rev input: %s"), *argv); + oid_array_clear(&revs); + return BISECT_FAILED; + } + + commit = lookup_commit_reference(the_repository, &oid); + if (!commit) + die(_("Bad rev input (not a commit): %s"), *argv); + + oid_array_append(&revs, &commit->object.oid); + } + + if (strbuf_read_file(&buf, git_path_bisect_expected_rev(), 0) < the_hash_algo->hexsz || + get_oid_hex(buf.buf, &expected) < 0) + verify_expected = 0; /* Ignore invalid file contents */ + strbuf_release(&buf); + + for (i = 0; i < revs.nr; i++) { + if (bisect_write(state, oid_to_hex(&revs.oid[i]), terms, 0)) { + oid_array_clear(&revs); + return BISECT_FAILED; + } + if (verify_expected && !oideq(&revs.oid[i], &expected)) { + unlink_or_warn(git_path_bisect_ancestors_ok()); + unlink_or_warn(git_path_bisect_expected_rev()); + verify_expected = 0; + } + } + + oid_array_clear(&revs); + return bisect_auto_next(terms, NULL); +} + +static enum bisect_error bisect_log(void) +{ + int fd, status; + const char* filename = git_path_bisect_log(); + + if (is_empty_or_missing_file(filename)) + return error(_("We are not bisecting.")); + + fd = open(filename, O_RDONLY); + if (fd < 0) + return BISECT_FAILED; + + status = copy_fd(fd, STDOUT_FILENO); + close(fd); + return status ? BISECT_FAILED : BISECT_OK; +} + +static int process_replay_line(struct bisect_terms *terms, struct strbuf *line) +{ + const char *p = line->buf + strspn(line->buf, " \t"); + char *word_end, *rev; + + if ((!skip_prefix(p, "git bisect", &p) && + !skip_prefix(p, "git-bisect", &p)) || !isspace(*p)) + return 0; + p += strspn(p, " \t"); + + word_end = (char *)p + strcspn(p, " \t"); + rev = word_end + strspn(word_end, " \t"); + *word_end = '\0'; /* NUL-terminate the word */ + + get_terms(terms); + if (check_and_set_terms(terms, p)) + return -1; + + if (!strcmp(p, "start")) { + struct strvec argv = STRVEC_INIT; + int res; + sq_dequote_to_strvec(rev, &argv); + res = bisect_start(terms, argv.nr, argv.v); + strvec_clear(&argv); + return res; + } + + if (one_of(p, terms->term_good, + terms->term_bad, "skip", NULL)) + return bisect_write(p, rev, terms, 0); + + if (!strcmp(p, "terms")) { + struct strvec argv = STRVEC_INIT; + int res; + sq_dequote_to_strvec(rev, &argv); + res = bisect_terms(terms, argv.nr == 1 ? argv.v[0] : NULL); + strvec_clear(&argv); + return res; + } + error(_("'%s'?? what are you talking about?"), p); + + return -1; +} + +static enum bisect_error bisect_replay(struct bisect_terms *terms, const char *filename) +{ + FILE *fp = NULL; + enum bisect_error res = BISECT_OK; + struct strbuf line = STRBUF_INIT; + + if (is_empty_or_missing_file(filename)) + return error(_("cannot read file '%s' for replaying"), filename); + + if (bisect_reset(NULL)) + return BISECT_FAILED; + + fp = fopen(filename, "r"); + if (!fp) + return BISECT_FAILED; + + while ((strbuf_getline(&line, fp) != EOF) && !res) + res = process_replay_line(terms, &line); + + strbuf_release(&line); + fclose(fp); + + if (res) + return BISECT_FAILED; + + return bisect_auto_next(terms, NULL); +} + +static enum bisect_error bisect_skip(struct bisect_terms *terms, int argc, + const char **argv) +{ + int i; + enum bisect_error res; + struct strvec argv_state = STRVEC_INIT; + + strvec_push(&argv_state, "skip"); + + for (i = 0; i < argc; i++) { + const char *dotdot = strstr(argv[i], ".."); + + if (dotdot) { + struct rev_info revs; + struct commit *commit; + + repo_init_revisions(the_repository, &revs, NULL); + setup_revisions(2, argv + i - 1, &revs, NULL); + + if (prepare_revision_walk(&revs)) + die(_("revision walk setup failed\n")); + while ((commit = get_revision(&revs)) != NULL) + strvec_push(&argv_state, + oid_to_hex(&commit->object.oid)); + + reset_revision_walk(); + release_revisions(&revs); + } else { + strvec_push(&argv_state, argv[i]); + } + } + res = bisect_state(terms, argv_state.nr, argv_state.v); + + strvec_clear(&argv_state); + return res; +} + +static int bisect_visualize(struct bisect_terms *terms, int argc, + const char **argv) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + struct strbuf sb = STRBUF_INIT; + + if (bisect_next_check(terms, NULL) != 0) + return BISECT_FAILED; + + cmd.no_stdin = 1; + if (!argc) { + if ((getenv("DISPLAY") || getenv("SESSIONNAME") || getenv("MSYSTEM") || + getenv("SECURITYSESSIONID")) && exists_in_PATH("gitk")) { + strvec_push(&cmd.args, "gitk"); + } else { + strvec_push(&cmd.args, "log"); + cmd.git_cmd = 1; + } + } else { + if (argv[0][0] == '-') { + strvec_push(&cmd.args, "log"); + cmd.git_cmd = 1; + } else if (strcmp(argv[0], "tig") && !starts_with(argv[0], "git")) + cmd.git_cmd = 1; + + strvec_pushv(&cmd.args, argv); + } + + strvec_pushl(&cmd.args, "--bisect", "--", NULL); + + strbuf_read_file(&sb, git_path_bisect_names(), 0); + sq_dequote_to_strvec(sb.buf, &cmd.args); + strbuf_release(&sb); + + return run_command(&cmd); +} + +static int get_first_good(const char *refname UNUSED, + const struct object_id *oid, + int flag UNUSED, void *cb_data) +{ + oidcpy(cb_data, oid); + return 1; +} + +static int do_bisect_run(const char *command) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + + printf(_("running %s\n"), command); + cmd.use_shell = 1; + strvec_push(&cmd.args, command); + return run_command(&cmd); +} + +static int verify_good(const struct bisect_terms *terms, const char *command) +{ + int rc; + enum bisect_error res; + struct object_id good_rev; + struct object_id current_rev; + char *good_glob = xstrfmt("%s-*", terms->term_good); + int no_checkout = ref_exists("BISECT_HEAD"); + + for_each_glob_ref_in(get_first_good, good_glob, "refs/bisect/", + &good_rev); + free(good_glob); + + if (read_ref(no_checkout ? "BISECT_HEAD" : "HEAD", ¤t_rev)) + return -1; + + res = bisect_checkout(&good_rev, no_checkout); + if (res != BISECT_OK) + return -1; + + rc = do_bisect_run(command); + + res = bisect_checkout(¤t_rev, no_checkout); + if (res != BISECT_OK) + return -1; + + return rc; +} + +static int bisect_run(struct bisect_terms *terms, int argc, const char **argv) +{ + int res = BISECT_OK; + struct strbuf command = STRBUF_INIT; + const char *new_state; + int temporary_stdout_fd, saved_stdout; + int is_first_run = 1; + + if (bisect_next_check(terms, NULL)) + return BISECT_FAILED; + + if (!argc) { + error(_("bisect run failed: no command provided.")); + return BISECT_FAILED; + } + + sq_quote_argv(&command, argv); + strbuf_ltrim(&command); + while (1) { + res = do_bisect_run(command.buf); + + /* + * Exit code 126 and 127 can either come from the shell + * if it was unable to execute or even find the script, + * or from the script itself. Check with a known-good + * revision to avoid trashing the bisect run due to a + * missing or non-executable script. + */ + if (is_first_run && (res == 126 || res == 127)) { + int rc = verify_good(terms, command.buf); + is_first_run = 0; + if (rc < 0 || 128 <= rc) { + error(_("unable to verify %s on good" + " revision"), command.buf); + res = BISECT_FAILED; + break; + } + if (rc == res) { + error(_("bogus exit code %d for good revision"), + rc); + res = BISECT_FAILED; + break; + } + } + + if (res < 0 || 128 <= res) { + error(_("bisect run failed: exit code %d from" + " %s is < 0 or >= 128"), res, command.buf); + break; + } + + if (res == 125) + new_state = "skip"; + else if (!res) + new_state = terms->term_good; + else + new_state = terms->term_bad; + + temporary_stdout_fd = open(git_path_bisect_run(), O_CREAT | O_WRONLY | O_TRUNC, 0666); + + if (temporary_stdout_fd < 0) { + res = error_errno(_("cannot open file '%s' for writing"), git_path_bisect_run()); + break; + } + + fflush(stdout); + saved_stdout = dup(1); + dup2(temporary_stdout_fd, 1); + + res = bisect_state(terms, 1, &new_state); + + fflush(stdout); + dup2(saved_stdout, 1); + close(saved_stdout); + close(temporary_stdout_fd); + + print_file_to_stdout(git_path_bisect_run()); + + if (res == BISECT_ONLY_SKIPPED_LEFT) + error(_("bisect run cannot continue any more")); + else if (res == BISECT_INTERNAL_SUCCESS_MERGE_BASE) { + puts(_("bisect run success")); + res = BISECT_OK; + } else if (res == BISECT_INTERNAL_SUCCESS_1ST_BAD_FOUND) { + puts(_("bisect found first bad commit")); + res = BISECT_OK; + } else if (res) { + error(_("bisect run failed: 'git bisect %s'" + " exited with error code %d"), new_state, res); + } else { + continue; + } + break; + } + + strbuf_release(&command); + return res; +} + +static int cmd_bisect__reset(int argc, const char **argv, const char *prefix UNUSED) +{ + if (argc > 1) + return error(_("'%s' requires either no argument or a commit"), + "git bisect reset"); + return bisect_reset(argc ? argv[0] : NULL); +} + +static int cmd_bisect__terms(int argc, const char **argv, const char *prefix UNUSED) +{ + int res; + struct bisect_terms terms = { 0 }; + + if (argc > 1) + return error(_("'%s' requires 0 or 1 argument"), + "git bisect terms"); + res = bisect_terms(&terms, argc == 1 ? argv[0] : NULL); + free_terms(&terms); + return res; +} + +static int cmd_bisect__start(int argc, const char **argv, const char *prefix UNUSED) +{ + int res; + struct bisect_terms terms = { 0 }; + + set_terms(&terms, "bad", "good"); + res = bisect_start(&terms, argc, argv); + free_terms(&terms); + return res; +} + +static int cmd_bisect__next(int argc, const char **argv UNUSED, const char *prefix) +{ + int res; + struct bisect_terms terms = { 0 }; + + if (argc) + return error(_("'%s' requires 0 arguments"), + "git bisect next"); + get_terms(&terms); + res = bisect_next(&terms, prefix); + free_terms(&terms); + return res; +} + +static int cmd_bisect__log(int argc UNUSED, const char **argv UNUSED, const char *prefix UNUSED) +{ + return bisect_log(); +} + +static int cmd_bisect__replay(int argc, const char **argv, const char *prefix UNUSED) +{ + int res; + struct bisect_terms terms = { 0 }; + + if (argc != 1) + return error(_("no logfile given")); + set_terms(&terms, "bad", "good"); + res = bisect_replay(&terms, argv[0]); + free_terms(&terms); + return res; +} + +static int cmd_bisect__skip(int argc, const char **argv, const char *prefix UNUSED) +{ + int res; + struct bisect_terms terms = { 0 }; + + set_terms(&terms, "bad", "good"); + get_terms(&terms); + res = bisect_skip(&terms, argc, argv); + free_terms(&terms); + return res; +} + +static int cmd_bisect__visualize(int argc, const char **argv, const char *prefix UNUSED) +{ + int res; + struct bisect_terms terms = { 0 }; + + get_terms(&terms); + res = bisect_visualize(&terms, argc, argv); + free_terms(&terms); + return res; +} + +static int cmd_bisect__run(int argc, const char **argv, const char *prefix UNUSED) +{ + int res; + struct bisect_terms terms = { 0 }; + + if (!argc) + return error(_("'%s' failed: no command provided."), "git bisect run"); + get_terms(&terms); + res = bisect_run(&terms, argc, argv); + free_terms(&terms); + return res; +} + +int cmd_bisect(int argc, const char **argv, const char *prefix) +{ + int res = 0; + parse_opt_subcommand_fn *fn = NULL; + struct option options[] = { + OPT_SUBCOMMAND("reset", &fn, cmd_bisect__reset), + OPT_SUBCOMMAND("terms", &fn, cmd_bisect__terms), + OPT_SUBCOMMAND("start", &fn, cmd_bisect__start), + OPT_SUBCOMMAND("next", &fn, cmd_bisect__next), + OPT_SUBCOMMAND("log", &fn, cmd_bisect__log), + OPT_SUBCOMMAND("replay", &fn, cmd_bisect__replay), + OPT_SUBCOMMAND("skip", &fn, cmd_bisect__skip), + OPT_SUBCOMMAND("visualize", &fn, cmd_bisect__visualize), + OPT_SUBCOMMAND("view", &fn, cmd_bisect__visualize), + OPT_SUBCOMMAND("run", &fn, cmd_bisect__run), + OPT_END() + }; + argc = parse_options(argc, argv, prefix, options, git_bisect_usage, + PARSE_OPT_SUBCOMMAND_OPTIONAL); + + if (!fn) { + struct bisect_terms terms = { 0 }; + + if (!argc) + usage_msg_opt(_("need a command"), git_bisect_usage, options); + + set_terms(&terms, "bad", "good"); + get_terms(&terms); + if (check_and_set_terms(&terms, argv[0])) + usage_msg_optf(_("unknown command: '%s'"), git_bisect_usage, + options, argv[0]); + res = bisect_state(&terms, argc, argv); + free_terms(&terms); + } else { + argc--; + argv++; + res = fn(argc, argv, prefix); + } + + return is_bisect_success(res) ? 0 : -res; +} diff --git a/builtin/blame.c b/builtin/blame.c new file mode 100644 index 0000000..9c987d6 --- /dev/null +++ b/builtin/blame.c @@ -0,0 +1,1232 @@ +/* + * Blame + * + * Copyright (c) 2006, 2014 by its authors + * See COPYING for licensing conditions + */ + +#include "git-compat-util.h" +#include "config.h" +#include "color.h" +#include "builtin.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "repository.h" +#include "commit.h" +#include "diff.h" +#include "revision.h" +#include "quote.h" +#include "string-list.h" +#include "mailmap.h" +#include "parse-options.h" +#include "prio-queue.h" +#include "utf8.h" +#include "userdiff.h" +#include "line-range.h" +#include "line-log.h" +#include "dir.h" +#include "progress.h" +#include "object-name.h" +#include "object-store-ll.h" +#include "pager.h" +#include "blame.h" +#include "refs.h" +#include "setup.h" +#include "tag.h" +#include "write-or-die.h" + +static char blame_usage[] = N_("git blame [] [] [] [--] "); +static char annotate_usage[] = N_("git annotate [] [] [] [--] "); + +static const char *blame_opt_usage[] = { + blame_usage, + "", + N_(" are documented in git-rev-list(1)"), + NULL +}; + +static const char *annotate_opt_usage[] = { + annotate_usage, + "", + N_(" are documented in git-rev-list(1)"), + NULL +}; + +static int longest_file; +static int longest_author; +static int max_orig_digits; +static int max_digits; +static int max_score_digits; +static int show_root; +static int reverse; +static int blank_boundary; +static int incremental; +static int xdl_opts; +static int abbrev = -1; +static int no_whole_file_rename; +static int show_progress; +static char repeated_meta_color[COLOR_MAXLEN]; +static int coloring_mode; +static struct string_list ignore_revs_file_list = STRING_LIST_INIT_NODUP; +static int mark_unblamable_lines; +static int mark_ignored_lines; + +static struct date_mode blame_date_mode = { DATE_ISO8601 }; +static size_t blame_date_width; + +static struct string_list mailmap = STRING_LIST_INIT_NODUP; + +#ifndef DEBUG_BLAME +#define DEBUG_BLAME 0 +#endif + +static unsigned blame_move_score; +static unsigned blame_copy_score; + +/* Remember to update object flag allocation in object.h */ +#define METAINFO_SHOWN (1u<<12) +#define MORE_THAN_ONE_PATH (1u<<13) + +struct progress_info { + struct progress *progress; + int blamed_lines; +}; + +static const char *nth_line_cb(void *data, long lno) +{ + return blame_nth_line((struct blame_scoreboard *)data, lno); +} + +/* + * Information on commits, used for output. + */ +struct commit_info { + struct strbuf author; + struct strbuf author_mail; + timestamp_t author_time; + struct strbuf author_tz; + + /* filled only when asked for details */ + struct strbuf committer; + struct strbuf committer_mail; + timestamp_t committer_time; + struct strbuf committer_tz; + + struct strbuf summary; +}; + +#define COMMIT_INFO_INIT { \ + .author = STRBUF_INIT, \ + .author_mail = STRBUF_INIT, \ + .author_tz = STRBUF_INIT, \ + .committer = STRBUF_INIT, \ + .committer_mail = STRBUF_INIT, \ + .committer_tz = STRBUF_INIT, \ + .summary = STRBUF_INIT, \ +} + +/* + * Parse author/committer line in the commit object buffer + */ +static void get_ac_line(const char *inbuf, const char *what, + struct strbuf *name, struct strbuf *mail, + timestamp_t *time, struct strbuf *tz) +{ + struct ident_split ident; + size_t len, maillen, namelen; + char *tmp, *endp; + const char *namebuf, *mailbuf; + + tmp = strstr(inbuf, what); + if (!tmp) + goto error_out; + tmp += strlen(what); + endp = strchr(tmp, '\n'); + if (!endp) + len = strlen(tmp); + else + len = endp - tmp; + + if (split_ident_line(&ident, tmp, len)) { + error_out: + /* Ugh */ + tmp = "(unknown)"; + strbuf_addstr(name, tmp); + strbuf_addstr(mail, tmp); + strbuf_addstr(tz, tmp); + *time = 0; + return; + } + + namelen = ident.name_end - ident.name_begin; + namebuf = ident.name_begin; + + maillen = ident.mail_end - ident.mail_begin; + mailbuf = ident.mail_begin; + + if (ident.date_begin && ident.date_end) + *time = strtoul(ident.date_begin, NULL, 10); + else + *time = 0; + + if (ident.tz_begin && ident.tz_end) + strbuf_add(tz, ident.tz_begin, ident.tz_end - ident.tz_begin); + else + strbuf_addstr(tz, "(unknown)"); + + /* + * Now, convert both name and e-mail using mailmap + */ + map_user(&mailmap, &mailbuf, &maillen, + &namebuf, &namelen); + + strbuf_addf(mail, "<%.*s>", (int)maillen, mailbuf); + strbuf_add(name, namebuf, namelen); +} + +static void commit_info_destroy(struct commit_info *ci) +{ + + strbuf_release(&ci->author); + strbuf_release(&ci->author_mail); + strbuf_release(&ci->author_tz); + strbuf_release(&ci->committer); + strbuf_release(&ci->committer_mail); + strbuf_release(&ci->committer_tz); + strbuf_release(&ci->summary); +} + +static void get_commit_info(struct commit *commit, + struct commit_info *ret, + int detailed) +{ + int len; + const char *subject, *encoding; + const char *message; + + encoding = get_log_output_encoding(); + message = repo_logmsg_reencode(the_repository, commit, NULL, encoding); + get_ac_line(message, "\nauthor ", + &ret->author, &ret->author_mail, + &ret->author_time, &ret->author_tz); + + if (!detailed) { + repo_unuse_commit_buffer(the_repository, commit, message); + return; + } + + get_ac_line(message, "\ncommitter ", + &ret->committer, &ret->committer_mail, + &ret->committer_time, &ret->committer_tz); + + len = find_commit_subject(message, &subject); + if (len) + strbuf_add(&ret->summary, subject, len); + else + strbuf_addf(&ret->summary, "(%s)", oid_to_hex(&commit->object.oid)); + + repo_unuse_commit_buffer(the_repository, commit, message); +} + +/* + * Write out any suspect information which depends on the path. This must be + * handled separately from emit_one_suspect_detail(), because a given commit + * may have changes in multiple paths. So this needs to appear each time + * we mention a new group. + * + * To allow LF and other nonportable characters in pathnames, + * they are c-style quoted as needed. + */ +static void write_filename_info(struct blame_origin *suspect) +{ + if (suspect->previous) { + struct blame_origin *prev = suspect->previous; + printf("previous %s ", oid_to_hex(&prev->commit->object.oid)); + write_name_quoted(prev->path, stdout, '\n'); + } + printf("filename "); + write_name_quoted(suspect->path, stdout, '\n'); +} + +/* + * Porcelain/Incremental format wants to show a lot of details per + * commit. Instead of repeating this every line, emit it only once, + * the first time each commit appears in the output (unless the + * user has specifically asked for us to repeat). + */ +static int emit_one_suspect_detail(struct blame_origin *suspect, int repeat) +{ + struct commit_info ci = COMMIT_INFO_INIT; + + if (!repeat && (suspect->commit->object.flags & METAINFO_SHOWN)) + return 0; + + suspect->commit->object.flags |= METAINFO_SHOWN; + get_commit_info(suspect->commit, &ci, 1); + printf("author %s\n", ci.author.buf); + printf("author-mail %s\n", ci.author_mail.buf); + printf("author-time %"PRItime"\n", ci.author_time); + printf("author-tz %s\n", ci.author_tz.buf); + printf("committer %s\n", ci.committer.buf); + printf("committer-mail %s\n", ci.committer_mail.buf); + printf("committer-time %"PRItime"\n", ci.committer_time); + printf("committer-tz %s\n", ci.committer_tz.buf); + printf("summary %s\n", ci.summary.buf); + if (suspect->commit->object.flags & UNINTERESTING) + printf("boundary\n"); + + commit_info_destroy(&ci); + + return 1; +} + +/* + * The blame_entry is found to be guilty for the range. + * Show it in incremental output. + */ +static void found_guilty_entry(struct blame_entry *ent, void *data) +{ + struct progress_info *pi = (struct progress_info *)data; + + if (incremental) { + struct blame_origin *suspect = ent->suspect; + + printf("%s %d %d %d\n", + oid_to_hex(&suspect->commit->object.oid), + ent->s_lno + 1, ent->lno + 1, ent->num_lines); + emit_one_suspect_detail(suspect, 0); + write_filename_info(suspect); + maybe_flush_or_die(stdout, "stdout"); + } + pi->blamed_lines += ent->num_lines; + display_progress(pi->progress, pi->blamed_lines); +} + +static const char *format_time(timestamp_t time, const char *tz_str, + int show_raw_time) +{ + static struct strbuf time_buf = STRBUF_INIT; + + strbuf_reset(&time_buf); + if (show_raw_time) { + strbuf_addf(&time_buf, "%"PRItime" %s", time, tz_str); + } + else { + const char *time_str; + size_t time_width; + int tz; + tz = atoi(tz_str); + time_str = show_date(time, tz, &blame_date_mode); + strbuf_addstr(&time_buf, time_str); + /* + * Add space paddings to time_buf to display a fixed width + * string, and use time_width for display width calibration. + */ + for (time_width = utf8_strwidth(time_str); + time_width < blame_date_width; + time_width++) + strbuf_addch(&time_buf, ' '); + } + return time_buf.buf; +} + +#define OUTPUT_ANNOTATE_COMPAT (1U<<0) +#define OUTPUT_LONG_OBJECT_NAME (1U<<1) +#define OUTPUT_RAW_TIMESTAMP (1U<<2) +#define OUTPUT_PORCELAIN (1U<<3) +#define OUTPUT_SHOW_NAME (1U<<4) +#define OUTPUT_SHOW_NUMBER (1U<<5) +#define OUTPUT_SHOW_SCORE (1U<<6) +#define OUTPUT_NO_AUTHOR (1U<<7) +#define OUTPUT_SHOW_EMAIL (1U<<8) +#define OUTPUT_LINE_PORCELAIN (1U<<9) +#define OUTPUT_COLOR_LINE (1U<<10) +#define OUTPUT_SHOW_AGE_WITH_COLOR (1U<<11) + +static void emit_porcelain_details(struct blame_origin *suspect, int repeat) +{ + if (emit_one_suspect_detail(suspect, repeat) || + (suspect->commit->object.flags & MORE_THAN_ONE_PATH)) + write_filename_info(suspect); +} + +static void emit_porcelain(struct blame_scoreboard *sb, struct blame_entry *ent, + int opt) +{ + int repeat = opt & OUTPUT_LINE_PORCELAIN; + int cnt; + const char *cp; + struct blame_origin *suspect = ent->suspect; + char hex[GIT_MAX_HEXSZ + 1]; + + oid_to_hex_r(hex, &suspect->commit->object.oid); + printf("%s %d %d %d\n", + hex, + ent->s_lno + 1, + ent->lno + 1, + ent->num_lines); + emit_porcelain_details(suspect, repeat); + + cp = blame_nth_line(sb, ent->lno); + for (cnt = 0; cnt < ent->num_lines; cnt++) { + char ch; + if (cnt) { + printf("%s %d %d\n", hex, + ent->s_lno + 1 + cnt, + ent->lno + 1 + cnt); + if (repeat) + emit_porcelain_details(suspect, 1); + } + putchar('\t'); + do { + ch = *cp++; + putchar(ch); + } while (ch != '\n' && + cp < sb->final_buf + sb->final_buf_size); + } + + if (sb->final_buf_size && cp[-1] != '\n') + putchar('\n'); +} + +static struct color_field { + timestamp_t hop; + char col[COLOR_MAXLEN]; +} *colorfield; +static int colorfield_nr, colorfield_alloc; + +static void parse_color_fields(const char *s) +{ + struct string_list l = STRING_LIST_INIT_DUP; + struct string_list_item *item; + enum { EXPECT_DATE, EXPECT_COLOR } next = EXPECT_COLOR; + + colorfield_nr = 0; + + /* Ideally this would be stripped and split at the same time? */ + string_list_split(&l, s, ',', -1); + ALLOC_GROW(colorfield, colorfield_nr + 1, colorfield_alloc); + + for_each_string_list_item(item, &l) { + switch (next) { + case EXPECT_DATE: + colorfield[colorfield_nr].hop = approxidate(item->string); + next = EXPECT_COLOR; + colorfield_nr++; + ALLOC_GROW(colorfield, colorfield_nr + 1, colorfield_alloc); + break; + case EXPECT_COLOR: + if (color_parse(item->string, colorfield[colorfield_nr].col)) + die(_("expecting a color: %s"), item->string); + next = EXPECT_DATE; + break; + } + } + + if (next == EXPECT_COLOR) + die(_("must end with a color")); + + colorfield[colorfield_nr].hop = TIME_MAX; + string_list_clear(&l, 0); +} + +static void setup_default_color_by_age(void) +{ + parse_color_fields("blue,12 month ago,white,1 month ago,red"); +} + +static void determine_line_heat(struct commit_info *ci, const char **dest_color) +{ + int i = 0; + + while (i < colorfield_nr && ci->author_time > colorfield[i].hop) + i++; + + *dest_color = colorfield[i].col; +} + +static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent, int opt) +{ + int cnt; + const char *cp; + struct blame_origin *suspect = ent->suspect; + struct commit_info ci = COMMIT_INFO_INIT; + char hex[GIT_MAX_HEXSZ + 1]; + int show_raw_time = !!(opt & OUTPUT_RAW_TIMESTAMP); + const char *default_color = NULL, *color = NULL, *reset = NULL; + + get_commit_info(suspect->commit, &ci, 1); + oid_to_hex_r(hex, &suspect->commit->object.oid); + + cp = blame_nth_line(sb, ent->lno); + + if (opt & OUTPUT_SHOW_AGE_WITH_COLOR) { + determine_line_heat(&ci, &default_color); + color = default_color; + reset = GIT_COLOR_RESET; + } + + for (cnt = 0; cnt < ent->num_lines; cnt++) { + char ch; + int length = (opt & OUTPUT_LONG_OBJECT_NAME) ? the_hash_algo->hexsz : abbrev; + + if (opt & OUTPUT_COLOR_LINE) { + if (cnt > 0) { + color = repeated_meta_color; + reset = GIT_COLOR_RESET; + } else { + color = default_color ? default_color : NULL; + reset = default_color ? GIT_COLOR_RESET : NULL; + } + } + if (color) + fputs(color, stdout); + + if (suspect->commit->object.flags & UNINTERESTING) { + if (blank_boundary) + memset(hex, ' ', length); + else if (!(opt & OUTPUT_ANNOTATE_COMPAT)) { + length--; + putchar('^'); + } + } + + if (mark_unblamable_lines && ent->unblamable) { + length--; + putchar('*'); + } + if (mark_ignored_lines && ent->ignored) { + length--; + putchar('?'); + } + printf("%.*s", length, hex); + if (opt & OUTPUT_ANNOTATE_COMPAT) { + const char *name; + if (opt & OUTPUT_SHOW_EMAIL) + name = ci.author_mail.buf; + else + name = ci.author.buf; + printf("\t(%10s\t%10s\t%d)", name, + format_time(ci.author_time, ci.author_tz.buf, + show_raw_time), + ent->lno + 1 + cnt); + } else { + if (opt & OUTPUT_SHOW_SCORE) + printf(" %*d %02d", + max_score_digits, ent->score, + ent->suspect->refcnt); + if (opt & OUTPUT_SHOW_NAME) + printf(" %-*.*s", longest_file, longest_file, + suspect->path); + if (opt & OUTPUT_SHOW_NUMBER) + printf(" %*d", max_orig_digits, + ent->s_lno + 1 + cnt); + + if (!(opt & OUTPUT_NO_AUTHOR)) { + const char *name; + int pad; + if (opt & OUTPUT_SHOW_EMAIL) + name = ci.author_mail.buf; + else + name = ci.author.buf; + pad = longest_author - utf8_strwidth(name); + printf(" (%s%*s %10s", + name, pad, "", + format_time(ci.author_time, + ci.author_tz.buf, + show_raw_time)); + } + printf(" %*d) ", + max_digits, ent->lno + 1 + cnt); + } + if (reset) + fputs(reset, stdout); + do { + ch = *cp++; + putchar(ch); + } while (ch != '\n' && + cp < sb->final_buf + sb->final_buf_size); + } + + if (sb->final_buf_size && cp[-1] != '\n') + putchar('\n'); + + commit_info_destroy(&ci); +} + +static void output(struct blame_scoreboard *sb, int option) +{ + struct blame_entry *ent; + + if (option & OUTPUT_PORCELAIN) { + for (ent = sb->ent; ent; ent = ent->next) { + int count = 0; + struct blame_origin *suspect; + struct commit *commit = ent->suspect->commit; + if (commit->object.flags & MORE_THAN_ONE_PATH) + continue; + for (suspect = get_blame_suspects(commit); suspect; suspect = suspect->next) { + if (suspect->guilty && count++) { + commit->object.flags |= MORE_THAN_ONE_PATH; + break; + } + } + } + } + + for (ent = sb->ent; ent; ent = ent->next) { + if (option & OUTPUT_PORCELAIN) + emit_porcelain(sb, ent, option); + else { + emit_other(sb, ent, option); + } + } +} + +/* + * Add phony grafts for use with -S; this is primarily to + * support git's cvsserver that wants to give a linear history + * to its clients. + */ +static int read_ancestry(const char *graft_file) +{ + FILE *fp = fopen_or_warn(graft_file, "r"); + struct strbuf buf = STRBUF_INIT; + if (!fp) + return -1; + while (!strbuf_getwholeline(&buf, fp, '\n')) { + /* The format is just "Commit Parent1 Parent2 ...\n" */ + struct commit_graft *graft = read_graft_line(&buf); + if (graft) + register_commit_graft(the_repository, graft, 0); + } + fclose(fp); + strbuf_release(&buf); + return 0; +} + +static int update_auto_abbrev(int auto_abbrev, struct blame_origin *suspect) +{ + const char *uniq = repo_find_unique_abbrev(the_repository, + &suspect->commit->object.oid, + auto_abbrev); + int len = strlen(uniq); + if (auto_abbrev < len) + return len; + return auto_abbrev; +} + +/* + * How many columns do we need to show line numbers, authors, + * and filenames? + */ +static void find_alignment(struct blame_scoreboard *sb, int *option) +{ + int longest_src_lines = 0; + int longest_dst_lines = 0; + unsigned largest_score = 0; + struct blame_entry *e; + int compute_auto_abbrev = (abbrev < 0); + int auto_abbrev = DEFAULT_ABBREV; + + for (e = sb->ent; e; e = e->next) { + struct blame_origin *suspect = e->suspect; + int num; + + if (compute_auto_abbrev) + auto_abbrev = update_auto_abbrev(auto_abbrev, suspect); + if (strcmp(suspect->path, sb->path)) + *option |= OUTPUT_SHOW_NAME; + num = strlen(suspect->path); + if (longest_file < num) + longest_file = num; + if (!(suspect->commit->object.flags & METAINFO_SHOWN)) { + struct commit_info ci = COMMIT_INFO_INIT; + suspect->commit->object.flags |= METAINFO_SHOWN; + get_commit_info(suspect->commit, &ci, 1); + if (*option & OUTPUT_SHOW_EMAIL) + num = utf8_strwidth(ci.author_mail.buf); + else + num = utf8_strwidth(ci.author.buf); + if (longest_author < num) + longest_author = num; + commit_info_destroy(&ci); + } + num = e->s_lno + e->num_lines; + if (longest_src_lines < num) + longest_src_lines = num; + num = e->lno + e->num_lines; + if (longest_dst_lines < num) + longest_dst_lines = num; + if (largest_score < blame_entry_score(sb, e)) + largest_score = blame_entry_score(sb, e); + } + max_orig_digits = decimal_width(longest_src_lines); + max_digits = decimal_width(longest_dst_lines); + max_score_digits = decimal_width(largest_score); + + if (compute_auto_abbrev) + /* one more abbrev length is needed for the boundary commit */ + abbrev = auto_abbrev + 1; +} + +static void sanity_check_on_fail(struct blame_scoreboard *sb, int baa) +{ + int opt = OUTPUT_SHOW_SCORE | OUTPUT_SHOW_NUMBER | OUTPUT_SHOW_NAME; + find_alignment(sb, &opt); + output(sb, opt); + die("Baa %d!", baa); +} + +static unsigned parse_score(const char *arg) +{ + char *end; + unsigned long score = strtoul(arg, &end, 10); + if (*end) + return 0; + return score; +} + +static const char *add_prefix(const char *prefix, const char *path) +{ + return prefix_path(prefix, prefix ? strlen(prefix) : 0, path); +} + +static int git_blame_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + if (!strcmp(var, "blame.showroot")) { + show_root = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "blame.blankboundary")) { + blank_boundary = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "blame.showemail")) { + int *output_option = cb; + if (git_config_bool(var, value)) + *output_option |= OUTPUT_SHOW_EMAIL; + else + *output_option &= ~OUTPUT_SHOW_EMAIL; + return 0; + } + if (!strcmp(var, "blame.date")) { + if (!value) + return config_error_nonbool(var); + parse_date_format(value, &blame_date_mode); + return 0; + } + if (!strcmp(var, "blame.ignorerevsfile")) { + const char *str; + int ret; + + ret = git_config_pathname(&str, var, value); + if (ret) + return ret; + string_list_insert(&ignore_revs_file_list, str); + return 0; + } + if (!strcmp(var, "blame.markunblamablelines")) { + mark_unblamable_lines = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "blame.markignoredlines")) { + mark_ignored_lines = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "color.blame.repeatedlines")) { + if (color_parse_mem(value, strlen(value), repeated_meta_color)) + warning(_("invalid value for '%s': '%s'"), + "color.blame.repeatedLines", value); + return 0; + } + if (!strcmp(var, "color.blame.highlightrecent")) { + parse_color_fields(value); + return 0; + } + + if (!strcmp(var, "blame.coloring")) { + if (!strcmp(value, "repeatedLines")) { + coloring_mode |= OUTPUT_COLOR_LINE; + } else if (!strcmp(value, "highlightRecent")) { + coloring_mode |= OUTPUT_SHOW_AGE_WITH_COLOR; + } else if (!strcmp(value, "none")) { + coloring_mode &= ~(OUTPUT_COLOR_LINE | + OUTPUT_SHOW_AGE_WITH_COLOR); + } else { + warning(_("invalid value for '%s': '%s'"), + "blame.coloring", value); + return 0; + } + } + + if (git_diff_heuristic_config(var, value, cb) < 0) + return -1; + if (userdiff_config(var, value) < 0) + return -1; + + return git_default_config(var, value, ctx, cb); +} + +static int blame_copy_callback(const struct option *option, const char *arg, int unset) +{ + int *opt = option->value; + + BUG_ON_OPT_NEG(unset); + + /* + * -C enables copy from removed files; + * -C -C enables copy from existing files, but only + * when blaming a new file; + * -C -C -C enables copy from existing files for + * everybody + */ + if (*opt & PICKAXE_BLAME_COPY_HARDER) + *opt |= PICKAXE_BLAME_COPY_HARDEST; + if (*opt & PICKAXE_BLAME_COPY) + *opt |= PICKAXE_BLAME_COPY_HARDER; + *opt |= PICKAXE_BLAME_COPY | PICKAXE_BLAME_MOVE; + + if (arg) + blame_copy_score = parse_score(arg); + return 0; +} + +static int blame_move_callback(const struct option *option, const char *arg, int unset) +{ + int *opt = option->value; + + BUG_ON_OPT_NEG(unset); + + *opt |= PICKAXE_BLAME_MOVE; + + if (arg) + blame_move_score = parse_score(arg); + return 0; +} + +static int is_a_rev(const char *name) +{ + struct object_id oid; + + if (repo_get_oid(the_repository, name, &oid)) + return 0; + return OBJ_NONE < oid_object_info(the_repository, &oid, NULL); +} + +static int peel_to_commit_oid(struct object_id *oid_ret, void *cbdata) +{ + struct repository *r = ((struct blame_scoreboard *)cbdata)->repo; + struct object_id oid; + + oidcpy(&oid, oid_ret); + while (1) { + struct object *obj; + int kind = oid_object_info(r, &oid, NULL); + if (kind == OBJ_COMMIT) { + oidcpy(oid_ret, &oid); + return 0; + } + if (kind != OBJ_TAG) + return -1; + obj = deref_tag(r, parse_object(r, &oid), NULL, 0); + if (!obj) + return -1; + oidcpy(&oid, &obj->oid); + } +} + +static void build_ignorelist(struct blame_scoreboard *sb, + struct string_list *ignore_revs_file_list, + struct string_list *ignore_rev_list) +{ + struct string_list_item *i; + struct object_id oid; + + oidset_init(&sb->ignore_list, 0); + for_each_string_list_item(i, ignore_revs_file_list) { + if (!strcmp(i->string, "")) + oidset_clear(&sb->ignore_list); + else + oidset_parse_file_carefully(&sb->ignore_list, i->string, + peel_to_commit_oid, sb); + } + for_each_string_list_item(i, ignore_rev_list) { + if (repo_get_oid_committish(the_repository, i->string, &oid) || + peel_to_commit_oid(&oid, sb)) + die(_("cannot find revision %s to ignore"), i->string); + oidset_insert(&sb->ignore_list, &oid); + } +} + +int cmd_blame(int argc, const char **argv, const char *prefix) +{ + struct rev_info revs; + const char *path; + struct blame_scoreboard sb; + struct blame_origin *o; + struct blame_entry *ent = NULL; + long dashdash_pos, lno; + struct progress_info pi = { NULL, 0 }; + + struct string_list range_list = STRING_LIST_INIT_NODUP; + struct string_list ignore_rev_list = STRING_LIST_INIT_NODUP; + int output_option = 0, opt = 0; + int show_stats = 0; + const char *revs_file = NULL; + const char *contents_from = NULL; + const struct option options[] = { + OPT_BOOL(0, "incremental", &incremental, N_("show blame entries as we find them, incrementally")), + OPT_BOOL('b', NULL, &blank_boundary, N_("do not show object names of boundary commits (Default: off)")), + OPT_BOOL(0, "root", &show_root, N_("do not treat root commits as boundaries (Default: off)")), + OPT_BOOL(0, "show-stats", &show_stats, N_("show work cost statistics")), + OPT_BOOL(0, "progress", &show_progress, N_("force progress reporting")), + OPT_BIT(0, "score-debug", &output_option, N_("show output score for blame entries"), OUTPUT_SHOW_SCORE), + OPT_BIT('f', "show-name", &output_option, N_("show original filename (Default: auto)"), OUTPUT_SHOW_NAME), + OPT_BIT('n', "show-number", &output_option, N_("show original linenumber (Default: off)"), OUTPUT_SHOW_NUMBER), + OPT_BIT('p', "porcelain", &output_option, N_("show in a format designed for machine consumption"), OUTPUT_PORCELAIN), + OPT_BIT(0, "line-porcelain", &output_option, N_("show porcelain format with per-line commit information"), OUTPUT_PORCELAIN|OUTPUT_LINE_PORCELAIN), + OPT_BIT('c', NULL, &output_option, N_("use the same output mode as git-annotate (Default: off)"), OUTPUT_ANNOTATE_COMPAT), + OPT_BIT('t', NULL, &output_option, N_("show raw timestamp (Default: off)"), OUTPUT_RAW_TIMESTAMP), + OPT_BIT('l', NULL, &output_option, N_("show long commit SHA1 (Default: off)"), OUTPUT_LONG_OBJECT_NAME), + OPT_BIT('s', NULL, &output_option, N_("suppress author name and timestamp (Default: off)"), OUTPUT_NO_AUTHOR), + OPT_BIT('e', "show-email", &output_option, N_("show author email instead of name (Default: off)"), OUTPUT_SHOW_EMAIL), + OPT_BIT('w', NULL, &xdl_opts, N_("ignore whitespace differences"), XDF_IGNORE_WHITESPACE), + OPT_STRING_LIST(0, "ignore-rev", &ignore_rev_list, N_("rev"), N_("ignore when blaming")), + OPT_STRING_LIST(0, "ignore-revs-file", &ignore_revs_file_list, N_("file"), N_("ignore revisions from ")), + OPT_BIT(0, "color-lines", &output_option, N_("color redundant metadata from previous line differently"), OUTPUT_COLOR_LINE), + OPT_BIT(0, "color-by-age", &output_option, N_("color lines by age"), OUTPUT_SHOW_AGE_WITH_COLOR), + OPT_BIT(0, "minimal", &xdl_opts, N_("spend extra cycles to find better match"), XDF_NEED_MINIMAL), + OPT_STRING('S', NULL, &revs_file, N_("file"), N_("use revisions from instead of calling git-rev-list")), + OPT_STRING(0, "contents", &contents_from, N_("file"), N_("use 's contents as the final image")), + OPT_CALLBACK_F('C', NULL, &opt, N_("score"), N_("find line copies within and across files"), PARSE_OPT_OPTARG, blame_copy_callback), + OPT_CALLBACK_F('M', NULL, &opt, N_("score"), N_("find line movements within and across files"), PARSE_OPT_OPTARG, blame_move_callback), + OPT_STRING_LIST('L', NULL, &range_list, N_("range"), + N_("process only line range , or function :")), + OPT__ABBREV(&abbrev), + OPT_END() + }; + + struct parse_opt_ctx_t ctx; + int cmd_is_annotate = !strcmp(argv[0], "annotate"); + struct range_set ranges; + unsigned int range_i; + long anchor; + const int hexsz = the_hash_algo->hexsz; + long num_lines = 0; + const char *str_usage = cmd_is_annotate ? annotate_usage : blame_usage; + const char **opt_usage = cmd_is_annotate ? annotate_opt_usage : blame_opt_usage; + + setup_default_color_by_age(); + git_config(git_blame_config, &output_option); + repo_init_revisions(the_repository, &revs, NULL); + revs.date_mode = blame_date_mode; + revs.diffopt.flags.allow_textconv = 1; + revs.diffopt.flags.follow_renames = 1; + + save_commit_buffer = 0; + dashdash_pos = 0; + show_progress = -1; + + parse_options_start(&ctx, argc, argv, prefix, options, + PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_ARGV0); + for (;;) { + switch (parse_options_step(&ctx, options, opt_usage)) { + case PARSE_OPT_NON_OPTION: + case PARSE_OPT_UNKNOWN: + break; + case PARSE_OPT_HELP: + case PARSE_OPT_ERROR: + case PARSE_OPT_SUBCOMMAND: + exit(129); + case PARSE_OPT_COMPLETE: + exit(0); + case PARSE_OPT_DONE: + if (ctx.argv[0]) + dashdash_pos = ctx.cpidx; + goto parse_done; + } + + if (!strcmp(ctx.argv[0], "--reverse")) { + ctx.argv[0] = "--children"; + reverse = 1; + } + parse_revision_opt(&revs, &ctx, options, opt_usage); + } +parse_done: + revision_opts_finish(&revs); + no_whole_file_rename = !revs.diffopt.flags.follow_renames; + xdl_opts |= revs.diffopt.xdl_opts & XDF_INDENT_HEURISTIC; + revs.diffopt.flags.follow_renames = 0; + argc = parse_options_end(&ctx); + + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + if (incremental || (output_option & OUTPUT_PORCELAIN)) { + if (show_progress > 0) + die(_("--progress can't be used with --incremental or porcelain formats")); + show_progress = 0; + } else if (show_progress < 0) + show_progress = isatty(2); + + if (0 < abbrev && abbrev < hexsz) + /* one more abbrev length is needed for the boundary commit */ + abbrev++; + else if (!abbrev) + abbrev = hexsz; + + if (revs_file && read_ancestry(revs_file)) + die_errno("reading graft file '%s' failed", revs_file); + + if (cmd_is_annotate) { + output_option |= OUTPUT_ANNOTATE_COMPAT; + blame_date_mode.type = DATE_ISO8601; + } else { + blame_date_mode = revs.date_mode; + } + + /* The maximum width used to show the dates */ + switch (blame_date_mode.type) { + case DATE_RFC2822: + blame_date_width = sizeof("Thu, 19 Oct 2006 16:00:04 -0700"); + break; + case DATE_ISO8601_STRICT: + blame_date_width = sizeof("2006-10-19T16:00:04-07:00"); + break; + case DATE_ISO8601: + blame_date_width = sizeof("2006-10-19 16:00:04 -0700"); + break; + case DATE_RAW: + blame_date_width = sizeof("1161298804 -0700"); + break; + case DATE_UNIX: + blame_date_width = sizeof("1161298804"); + break; + case DATE_SHORT: + blame_date_width = sizeof("2006-10-19"); + break; + case DATE_RELATIVE: + /* + * TRANSLATORS: This string is used to tell us the + * maximum display width for a relative timestamp in + * "git blame" output. For C locale, "4 years, 11 + * months ago", which takes 22 places, is the longest + * among various forms of relative timestamps, but + * your language may need more or fewer display + * columns. + */ + blame_date_width = utf8_strwidth(_("4 years, 11 months ago")) + 1; /* add the null */ + break; + case DATE_HUMAN: + /* If the year is shown, no time is shown */ + blame_date_width = sizeof("Thu Oct 19 16:00"); + break; + case DATE_NORMAL: + blame_date_width = sizeof("Thu Oct 19 16:00:04 2006 -0700"); + break; + case DATE_STRFTIME: + blame_date_width = strlen(show_date(0, 0, &blame_date_mode)) + 1; /* add the null */ + break; + } + blame_date_width -= 1; /* strip the null */ + + if (revs.diffopt.flags.find_copies_harder) + opt |= (PICKAXE_BLAME_COPY | PICKAXE_BLAME_MOVE | + PICKAXE_BLAME_COPY_HARDER); + + /* + * We have collected options unknown to us in argv[1..unk] + * which are to be passed to revision machinery if we are + * going to do the "bottom" processing. + * + * The remaining are: + * + * (1) if dashdash_pos != 0, it is either + * "blame [revisions] -- " or + * "blame -- " + * + * (2) otherwise, it is one of the two: + * "blame [revisions] " + * "blame " + * + * Note that we must strip out from the arguments: we do not + * want the path pruning but we may want "bottom" processing. + */ + if (dashdash_pos) { + switch (argc - dashdash_pos - 1) { + case 2: /* (1b) */ + if (argc != 4) + usage_with_options(opt_usage, options); + /* reorder for the new way: -- */ + argv[1] = argv[3]; + argv[3] = argv[2]; + argv[2] = "--"; + /* FALLTHROUGH */ + case 1: /* (1a) */ + path = add_prefix(prefix, argv[--argc]); + argv[argc] = NULL; + break; + default: + usage_with_options(opt_usage, options); + } + } else { + if (argc < 2) + usage_with_options(opt_usage, options); + if (argc == 3 && is_a_rev(argv[argc - 1])) { /* (2b) */ + path = add_prefix(prefix, argv[1]); + argv[1] = argv[2]; + } else { /* (2a) */ + if (argc == 2 && is_a_rev(argv[1]) && !get_git_work_tree()) + die("missing to blame"); + path = add_prefix(prefix, argv[argc - 1]); + } + argv[argc - 1] = "--"; + } + + revs.disable_stdin = 1; + setup_revisions(argc, argv, &revs, NULL); + if (!revs.pending.nr && is_bare_repository()) { + struct commit *head_commit; + struct object_id head_oid; + + if (!resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, + &head_oid, NULL) || + !(head_commit = lookup_commit_reference_gently(revs.repo, + &head_oid, 1))) + die("no such ref: HEAD"); + + add_pending_object(&revs, &head_commit->object, "HEAD"); + } + + init_scoreboard(&sb); + sb.revs = &revs; + sb.contents_from = contents_from; + sb.reverse = reverse; + sb.repo = the_repository; + sb.path = path; + build_ignorelist(&sb, &ignore_revs_file_list, &ignore_rev_list); + string_list_clear(&ignore_revs_file_list, 0); + string_list_clear(&ignore_rev_list, 0); + setup_scoreboard(&sb, &o); + + /* + * Changed-path Bloom filters are disabled when looking + * for copies. + */ + if (!(opt & PICKAXE_BLAME_COPY)) + setup_blame_bloom_data(&sb); + + lno = sb.num_lines; + + if (lno && !range_list.nr) + string_list_append(&range_list, "1"); + + anchor = 1; + range_set_init(&ranges, range_list.nr); + for (range_i = 0; range_i < range_list.nr; ++range_i) { + long bottom, top; + if (parse_range_arg(range_list.items[range_i].string, + nth_line_cb, &sb, lno, anchor, + &bottom, &top, sb.path, + the_repository->index)) + usage(str_usage); + if ((!lno && (top || bottom)) || lno < bottom) + die(Q_("file %s has only %lu line", + "file %s has only %lu lines", + lno), sb.path, lno); + if (bottom < 1) + bottom = 1; + if (top < 1 || lno < top) + top = lno; + bottom--; + range_set_append_unsafe(&ranges, bottom, top); + anchor = top + 1; + } + sort_and_merge_range_set(&ranges); + + for (range_i = ranges.nr; range_i > 0; --range_i) { + const struct range *r = &ranges.ranges[range_i - 1]; + ent = blame_entry_prepend(ent, r->start, r->end, o); + num_lines += (r->end - r->start); + } + if (!num_lines) + num_lines = sb.num_lines; + + o->suspects = ent; + prio_queue_put(&sb.commits, o->commit); + + blame_origin_decref(o); + + range_set_release(&ranges); + string_list_clear(&range_list, 0); + + sb.ent = NULL; + + if (blame_move_score) + sb.move_score = blame_move_score; + if (blame_copy_score) + sb.copy_score = blame_copy_score; + + sb.debug = DEBUG_BLAME; + sb.on_sanity_fail = &sanity_check_on_fail; + + sb.show_root = show_root; + sb.xdl_opts = xdl_opts; + sb.no_whole_file_rename = no_whole_file_rename; + + read_mailmap(&mailmap); + + sb.found_guilty_entry = &found_guilty_entry; + sb.found_guilty_entry_data = π + if (show_progress) + pi.progress = start_delayed_progress(_("Blaming lines"), num_lines); + + assign_blame(&sb, opt); + + stop_progress(&pi.progress); + + if (!incremental) + setup_pager(); + else + goto cleanup; + + blame_sort_final(&sb); + + blame_coalesce(&sb); + + if (!(output_option & (OUTPUT_COLOR_LINE | OUTPUT_SHOW_AGE_WITH_COLOR))) + output_option |= coloring_mode; + + if (!(output_option & OUTPUT_PORCELAIN)) { + find_alignment(&sb, &output_option); + if (!*repeated_meta_color && + (output_option & OUTPUT_COLOR_LINE)) + xsnprintf(repeated_meta_color, + sizeof(repeated_meta_color), + "%s", GIT_COLOR_CYAN); + } + if (output_option & OUTPUT_ANNOTATE_COMPAT) + output_option &= ~(OUTPUT_COLOR_LINE | OUTPUT_SHOW_AGE_WITH_COLOR); + + output(&sb, output_option); + free((void *)sb.final_buf); + for (ent = sb.ent; ent; ) { + struct blame_entry *e = ent->next; + free(ent); + ent = e; + } + + if (show_stats) { + printf("num read blob: %d\n", sb.num_read_blob); + printf("num get patch: %d\n", sb.num_get_patch); + printf("num commits: %d\n", sb.num_commits); + } + +cleanup: + cleanup_scoreboard(&sb); + release_revisions(&revs); + return 0; +} diff --git a/builtin/branch.c b/builtin/branch.c new file mode 100644 index 0000000..e7ee9bd --- /dev/null +++ b/builtin/branch.c @@ -0,0 +1,986 @@ +/* + * Builtin "git branch" + * + * Copyright (c) 2006 Kristian Høgsberg + * Based on git-branch.sh by Junio C Hamano. + */ + +#include "builtin.h" +#include "config.h" +#include "color.h" +#include "editor.h" +#include "environment.h" +#include "refs.h" +#include "commit.h" +#include "gettext.h" +#include "object-name.h" +#include "remote.h" +#include "parse-options.h" +#include "branch.h" +#include "diff.h" +#include "path.h" +#include "revision.h" +#include "string-list.h" +#include "column.h" +#include "utf8.h" +#include "wt-status.h" +#include "ref-filter.h" +#include "worktree.h" +#include "help.h" +#include "commit-reach.h" + +static const char * const builtin_branch_usage[] = { + N_("git branch [] [-r | -a] [--merged] [--no-merged]"), + N_("git branch [] [-f] [--recurse-submodules] []"), + N_("git branch [] [-l] [...]"), + N_("git branch [] [-r] (-d | -D) ..."), + N_("git branch [] (-m | -M) [] "), + N_("git branch [] (-c | -C) [] "), + N_("git branch [] [-r | -a] [--points-at]"), + N_("git branch [] [-r | -a] [--format]"), + NULL +}; + +static const char *head; +static struct object_id head_oid; +static int recurse_submodules = 0; +static int submodule_propagate_branches = 0; +static int omit_empty = 0; + +static int branch_use_color = -1; +static char branch_colors[][COLOR_MAXLEN] = { + GIT_COLOR_RESET, + GIT_COLOR_NORMAL, /* PLAIN */ + GIT_COLOR_RED, /* REMOTE */ + GIT_COLOR_NORMAL, /* LOCAL */ + GIT_COLOR_GREEN, /* CURRENT */ + GIT_COLOR_BLUE, /* UPSTREAM */ + GIT_COLOR_CYAN, /* WORKTREE */ +}; +enum color_branch { + BRANCH_COLOR_RESET = 0, + BRANCH_COLOR_PLAIN = 1, + BRANCH_COLOR_REMOTE = 2, + BRANCH_COLOR_LOCAL = 3, + BRANCH_COLOR_CURRENT = 4, + BRANCH_COLOR_UPSTREAM = 5, + BRANCH_COLOR_WORKTREE = 6 +}; + +static const char *color_branch_slots[] = { + [BRANCH_COLOR_RESET] = "reset", + [BRANCH_COLOR_PLAIN] = "plain", + [BRANCH_COLOR_REMOTE] = "remote", + [BRANCH_COLOR_LOCAL] = "local", + [BRANCH_COLOR_CURRENT] = "current", + [BRANCH_COLOR_UPSTREAM] = "upstream", + [BRANCH_COLOR_WORKTREE] = "worktree", +}; + +static struct string_list output = STRING_LIST_INIT_DUP; +static unsigned int colopts; + +define_list_config_array(color_branch_slots); + +static int git_branch_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + const char *slot_name; + + if (!strcmp(var, "branch.sort")) { + if (!value) + return config_error_nonbool(var); + string_list_append(cb, value); + return 0; + } + + if (starts_with(var, "column.")) + return git_column_config(var, value, "branch", &colopts); + if (!strcmp(var, "color.branch")) { + branch_use_color = git_config_colorbool(var, value); + return 0; + } + if (skip_prefix(var, "color.branch.", &slot_name)) { + int slot = LOOKUP_CONFIG(color_branch_slots, slot_name); + if (slot < 0) + return 0; + if (!value) + return config_error_nonbool(var); + return color_parse(value, branch_colors[slot]); + } + if (!strcmp(var, "submodule.recurse")) { + recurse_submodules = git_config_bool(var, value); + return 0; + } + if (!strcasecmp(var, "submodule.propagateBranches")) { + submodule_propagate_branches = git_config_bool(var, value); + return 0; + } + + if (git_color_config(var, value, cb) < 0) + return -1; + + return git_default_config(var, value, ctx, cb); +} + +static const char *branch_get_color(enum color_branch ix) +{ + if (want_color(branch_use_color)) + return branch_colors[ix]; + return ""; +} + +static int branch_merged(int kind, const char *name, + struct commit *rev, struct commit *head_rev) +{ + /* + * This checks whether the merge bases of branch and HEAD (or + * the other branch this branch builds upon) contains the + * branch, which means that the branch has already been merged + * safely to HEAD (or the other branch). + */ + struct commit *reference_rev = NULL; + const char *reference_name = NULL; + void *reference_name_to_free = NULL; + int merged; + + if (kind == FILTER_REFS_BRANCHES) { + struct branch *branch = branch_get(name); + const char *upstream = branch_get_upstream(branch, NULL); + struct object_id oid; + + if (upstream && + (reference_name = reference_name_to_free = + resolve_refdup(upstream, RESOLVE_REF_READING, + &oid, NULL)) != NULL) + reference_rev = lookup_commit_reference(the_repository, + &oid); + } + if (!reference_rev) + reference_rev = head_rev; + + merged = reference_rev ? repo_in_merge_bases(the_repository, rev, + reference_rev) : 0; + + /* + * After the safety valve is fully redefined to "check with + * upstream, if any, otherwise with HEAD", we should just + * return the result of the repo_in_merge_bases() above without + * any of the following code, but during the transition period, + * a gentle reminder is in order. + */ + if ((head_rev != reference_rev) && + (head_rev ? repo_in_merge_bases(the_repository, rev, head_rev) : 0) != merged) { + if (merged) + warning(_("deleting branch '%s' that has been merged to\n" + " '%s', but not yet merged to HEAD"), + name, reference_name); + else + warning(_("not deleting branch '%s' that is not yet merged to\n" + " '%s', even though it is merged to HEAD"), + name, reference_name); + } + free(reference_name_to_free); + return merged; +} + +static int check_branch_commit(const char *branchname, const char *refname, + const struct object_id *oid, struct commit *head_rev, + int kinds, int force) +{ + struct commit *rev = lookup_commit_reference(the_repository, oid); + if (!force && !rev) { + error(_("couldn't look up commit object for '%s'"), refname); + return -1; + } + if (!force && !branch_merged(kinds, branchname, rev, head_rev)) { + error(_("the branch '%s' is not fully merged.\n" + "If you are sure you want to delete it, " + "run 'git branch -D %s'"), branchname, branchname); + return -1; + } + return 0; +} + +static void delete_branch_config(const char *branchname) +{ + struct strbuf buf = STRBUF_INIT; + strbuf_addf(&buf, "branch.%s", branchname); + if (git_config_rename_section(buf.buf, NULL) < 0) + warning(_("update of config-file failed")); + strbuf_release(&buf); +} + +static int delete_branches(int argc, const char **argv, int force, int kinds, + int quiet) +{ + struct commit *head_rev = NULL; + struct object_id oid; + char *name = NULL; + const char *fmt; + int i; + int ret = 0; + int remote_branch = 0; + struct strbuf bname = STRBUF_INIT; + unsigned allowed_interpret; + struct string_list refs_to_delete = STRING_LIST_INIT_DUP; + struct string_list_item *item; + int branch_name_pos; + const char *fmt_remotes = "refs/remotes/%s"; + + switch (kinds) { + case FILTER_REFS_REMOTES: + fmt = fmt_remotes; + /* For subsequent UI messages */ + remote_branch = 1; + allowed_interpret = INTERPRET_BRANCH_REMOTE; + + force = 1; + break; + case FILTER_REFS_BRANCHES: + fmt = "refs/heads/%s"; + allowed_interpret = INTERPRET_BRANCH_LOCAL; + break; + default: + die(_("cannot use -a with -d")); + } + branch_name_pos = strcspn(fmt, "%"); + + if (!force) + head_rev = lookup_commit_reference(the_repository, &head_oid); + + for (i = 0; i < argc; i++, strbuf_reset(&bname)) { + char *target = NULL; + int flags = 0; + + strbuf_branchname(&bname, argv[i], allowed_interpret); + free(name); + name = mkpathdup(fmt, bname.buf); + + if (kinds == FILTER_REFS_BRANCHES) { + const char *path; + if ((path = branch_checked_out(name))) { + error(_("cannot delete branch '%s' " + "used by worktree at '%s'"), + bname.buf, path); + ret = 1; + continue; + } + } + + target = resolve_refdup(name, + RESOLVE_REF_READING + | RESOLVE_REF_NO_RECURSE + | RESOLVE_REF_ALLOW_BAD_NAME, + &oid, &flags); + if (!target) { + if (remote_branch) { + error(_("remote-tracking branch '%s' not found"), bname.buf); + } else { + char *virtual_name = mkpathdup(fmt_remotes, bname.buf); + char *virtual_target = resolve_refdup(virtual_name, + RESOLVE_REF_READING + | RESOLVE_REF_NO_RECURSE + | RESOLVE_REF_ALLOW_BAD_NAME, + &oid, &flags); + FREE_AND_NULL(virtual_name); + + if (virtual_target) + error(_("branch '%s' not found.\n" + "Did you forget --remote?"), + bname.buf); + else + error(_("branch '%s' not found"), bname.buf); + FREE_AND_NULL(virtual_target); + } + ret = 1; + continue; + } + + if (!(flags & (REF_ISSYMREF|REF_ISBROKEN)) && + check_branch_commit(bname.buf, name, &oid, head_rev, kinds, + force)) { + ret = 1; + goto next; + } + + item = string_list_append(&refs_to_delete, name); + item->util = xstrdup((flags & REF_ISBROKEN) ? "broken" + : (flags & REF_ISSYMREF) ? target + : repo_find_unique_abbrev(the_repository, &oid, DEFAULT_ABBREV)); + + next: + free(target); + } + + if (delete_refs(NULL, &refs_to_delete, REF_NO_DEREF)) + ret = 1; + + for_each_string_list_item(item, &refs_to_delete) { + char *describe_ref = item->util; + char *name = item->string; + if (!ref_exists(name)) { + char *refname = name + branch_name_pos; + if (!quiet) + printf(remote_branch + ? _("Deleted remote-tracking branch %s (was %s).\n") + : _("Deleted branch %s (was %s).\n"), + name + branch_name_pos, describe_ref); + + delete_branch_config(refname); + } + free(describe_ref); + } + string_list_clear(&refs_to_delete, 0); + + free(name); + strbuf_release(&bname); + + return ret; +} + +static int calc_maxwidth(struct ref_array *refs, int remote_bonus) +{ + int i, max = 0; + for (i = 0; i < refs->nr; i++) { + struct ref_array_item *it = refs->items[i]; + const char *desc = it->refname; + int w; + + skip_prefix(it->refname, "refs/heads/", &desc); + skip_prefix(it->refname, "refs/remotes/", &desc); + if (it->kind == FILTER_REFS_DETACHED_HEAD) { + char *head_desc = get_head_description(); + w = utf8_strwidth(head_desc); + free(head_desc); + } else + w = utf8_strwidth(desc); + + if (it->kind == FILTER_REFS_REMOTES) + w += remote_bonus; + if (w > max) + max = w; + } + return max; +} + +static const char *quote_literal_for_format(const char *s) +{ + static struct strbuf buf = STRBUF_INIT; + + strbuf_reset(&buf); + while (strbuf_expand_step(&buf, &s)) + strbuf_addstr(&buf, "%%"); + return buf.buf; +} + +static char *build_format(struct ref_filter *filter, int maxwidth, const char *remote_prefix) +{ + struct strbuf fmt = STRBUF_INIT; + struct strbuf local = STRBUF_INIT; + struct strbuf remote = STRBUF_INIT; + + strbuf_addf(&local, "%%(if)%%(HEAD)%%(then)* %s%%(else)%%(if)%%(worktreepath)%%(then)+ %s%%(else) %s%%(end)%%(end)", + branch_get_color(BRANCH_COLOR_CURRENT), + branch_get_color(BRANCH_COLOR_WORKTREE), + branch_get_color(BRANCH_COLOR_LOCAL)); + strbuf_addf(&remote, " %s", + branch_get_color(BRANCH_COLOR_REMOTE)); + + if (filter->verbose) { + struct strbuf obname = STRBUF_INIT; + + if (filter->abbrev < 0) + strbuf_addf(&obname, "%%(objectname:short)"); + else if (!filter->abbrev) + strbuf_addf(&obname, "%%(objectname)"); + else + strbuf_addf(&obname, "%%(objectname:short=%d)", filter->abbrev); + + strbuf_addf(&local, "%%(align:%d,left)%%(refname:lstrip=2)%%(end)", maxwidth); + strbuf_addstr(&local, branch_get_color(BRANCH_COLOR_RESET)); + strbuf_addf(&local, " %s ", obname.buf); + + if (filter->verbose > 1) + { + strbuf_addf(&local, "%%(if:notequals=*)%%(HEAD)%%(then)%%(if)%%(worktreepath)%%(then)(%s%%(worktreepath)%s) %%(end)%%(end)", + branch_get_color(BRANCH_COLOR_WORKTREE), branch_get_color(BRANCH_COLOR_RESET)); + strbuf_addf(&local, "%%(if)%%(upstream)%%(then)[%s%%(upstream:short)%s%%(if)%%(upstream:track)" + "%%(then): %%(upstream:track,nobracket)%%(end)] %%(end)%%(contents:subject)", + branch_get_color(BRANCH_COLOR_UPSTREAM), branch_get_color(BRANCH_COLOR_RESET)); + } + else + strbuf_addf(&local, "%%(if)%%(upstream:track)%%(then)%%(upstream:track) %%(end)%%(contents:subject)"); + + strbuf_addf(&remote, "%%(align:%d,left)%s%%(refname:lstrip=2)%%(end)%s" + "%%(if)%%(symref)%%(then) -> %%(symref:short)" + "%%(else) %s %%(contents:subject)%%(end)", + maxwidth, quote_literal_for_format(remote_prefix), + branch_get_color(BRANCH_COLOR_RESET), obname.buf); + strbuf_release(&obname); + } else { + strbuf_addf(&local, "%%(refname:lstrip=2)%s%%(if)%%(symref)%%(then) -> %%(symref:short)%%(end)", + branch_get_color(BRANCH_COLOR_RESET)); + strbuf_addf(&remote, "%s%%(refname:lstrip=2)%s%%(if)%%(symref)%%(then) -> %%(symref:short)%%(end)", + quote_literal_for_format(remote_prefix), + branch_get_color(BRANCH_COLOR_RESET)); + } + + strbuf_addf(&fmt, "%%(if:notequals=refs/remotes)%%(refname:rstrip=-2)%%(then)%s%%(else)%s%%(end)", local.buf, remote.buf); + + strbuf_release(&local); + strbuf_release(&remote); + return strbuf_detach(&fmt, NULL); +} + +static void print_ref_list(struct ref_filter *filter, struct ref_sorting *sorting, + struct ref_format *format, struct string_list *output) +{ + int i; + struct ref_array array; + struct strbuf out = STRBUF_INIT; + struct strbuf err = STRBUF_INIT; + int maxwidth = 0; + const char *remote_prefix = ""; + char *to_free = NULL; + + /* + * If we are listing more than just remote branches, + * then remote branches will have a "remotes/" prefix. + * We need to account for this in the width. + */ + if (filter->kind != FILTER_REFS_REMOTES) + remote_prefix = "remotes/"; + + memset(&array, 0, sizeof(array)); + + filter_refs(&array, filter, filter->kind); + + if (filter->verbose) + maxwidth = calc_maxwidth(&array, strlen(remote_prefix)); + + if (!format->format) + format->format = to_free = build_format(filter, maxwidth, remote_prefix); + format->use_color = branch_use_color; + + if (verify_ref_format(format)) + die(_("unable to parse format string")); + + filter_ahead_behind(the_repository, format, &array); + ref_array_sort(sorting, &array); + + for (i = 0; i < array.nr; i++) { + strbuf_reset(&err); + strbuf_reset(&out); + if (format_ref_array_item(array.items[i], format, &out, &err)) + die("%s", err.buf); + if (column_active(colopts)) { + assert(!filter->verbose && "--column and --verbose are incompatible"); + /* format to a string_list to let print_columns() do its job */ + string_list_append(output, out.buf); + } else { + fwrite(out.buf, 1, out.len, stdout); + if (out.len || !omit_empty) + putchar('\n'); + } + } + + strbuf_release(&err); + strbuf_release(&out); + ref_array_clear(&array); + free(to_free); +} + +static void print_current_branch_name(void) +{ + int flags; + const char *refname = resolve_ref_unsafe("HEAD", 0, NULL, &flags); + const char *shortname; + if (!refname) + die(_("could not resolve HEAD")); + else if (!(flags & REF_ISSYMREF)) + return; + else if (skip_prefix(refname, "refs/heads/", &shortname)) + puts(shortname); + else + die(_("HEAD (%s) points outside of refs/heads/"), refname); +} + +static void reject_rebase_or_bisect_branch(struct worktree **worktrees, + const char *target) +{ + int i; + + for (i = 0; worktrees[i]; i++) { + struct worktree *wt = worktrees[i]; + + if (!wt->is_detached) + continue; + + if (is_worktree_being_rebased(wt, target)) + die(_("branch %s is being rebased at %s"), + target, wt->path); + + if (is_worktree_being_bisected(wt, target)) + die(_("branch %s is being bisected at %s"), + target, wt->path); + } +} + +/* + * Update all per-worktree HEADs pointing at the old ref to point the new ref. + * This will be used when renaming a branch. Returns 0 if successful, non-zero + * otherwise. + */ +static int replace_each_worktree_head_symref(struct worktree **worktrees, + const char *oldref, const char *newref, + const char *logmsg) +{ + int ret = 0; + int i; + + for (i = 0; worktrees[i]; i++) { + struct ref_store *refs; + + if (worktrees[i]->is_detached) + continue; + if (!worktrees[i]->head_ref) + continue; + if (strcmp(oldref, worktrees[i]->head_ref)) + continue; + + refs = get_worktree_ref_store(worktrees[i]); + if (refs_create_symref(refs, "HEAD", newref, logmsg)) + ret = error(_("HEAD of working tree %s is not updated"), + worktrees[i]->path); + } + + return ret; +} + +#define IS_HEAD 1 +#define IS_ORPHAN 2 + +static void copy_or_rename_branch(const char *oldname, const char *newname, int copy, int force) +{ + struct strbuf oldref = STRBUF_INIT, newref = STRBUF_INIT, logmsg = STRBUF_INIT; + struct strbuf oldsection = STRBUF_INIT, newsection = STRBUF_INIT; + const char *interpreted_oldname = NULL; + const char *interpreted_newname = NULL; + int recovery = 0, oldref_usage = 0; + struct worktree **worktrees = get_worktrees(); + + if (strbuf_check_branch_ref(&oldref, oldname)) { + /* + * Bad name --- this could be an attempt to rename a + * ref that we used to allow to be created by accident. + */ + if (ref_exists(oldref.buf)) + recovery = 1; + else + die(_("invalid branch name: '%s'"), oldname); + } + + for (int i = 0; worktrees[i]; i++) { + struct worktree *wt = worktrees[i]; + + if (wt->head_ref && !strcmp(oldref.buf, wt->head_ref)) { + oldref_usage |= IS_HEAD; + if (is_null_oid(&wt->head_oid)) + oldref_usage |= IS_ORPHAN; + break; + } + } + + if ((copy || !(oldref_usage & IS_HEAD)) && !ref_exists(oldref.buf)) { + if (oldref_usage & IS_HEAD) + die(_("no commit on branch '%s' yet"), oldname); + else + die(_("no branch named '%s'"), oldname); + } + + /* + * A command like "git branch -M currentbranch currentbranch" cannot + * cause the worktree to become inconsistent with HEAD, so allow it. + */ + if (!strcmp(oldname, newname)) + validate_branchname(newname, &newref); + else + validate_new_branchname(newname, &newref, force); + + reject_rebase_or_bisect_branch(worktrees, oldref.buf); + + if (!skip_prefix(oldref.buf, "refs/heads/", &interpreted_oldname) || + !skip_prefix(newref.buf, "refs/heads/", &interpreted_newname)) { + BUG("expected prefix missing for refs"); + } + + if (copy) + strbuf_addf(&logmsg, "Branch: copied %s to %s", + oldref.buf, newref.buf); + else + strbuf_addf(&logmsg, "Branch: renamed %s to %s", + oldref.buf, newref.buf); + + if (!copy && !(oldref_usage & IS_ORPHAN) && + rename_ref(oldref.buf, newref.buf, logmsg.buf)) + die(_("branch rename failed")); + if (copy && copy_existing_ref(oldref.buf, newref.buf, logmsg.buf)) + die(_("branch copy failed")); + + if (recovery) { + if (copy) + warning(_("created a copy of a misnamed branch '%s'"), + interpreted_oldname); + else + warning(_("renamed a misnamed branch '%s' away"), + interpreted_oldname); + } + + if (!copy && (oldref_usage & IS_HEAD) && + replace_each_worktree_head_symref(worktrees, oldref.buf, newref.buf, + logmsg.buf)) + die(_("branch renamed to %s, but HEAD is not updated"), newname); + + strbuf_release(&logmsg); + + strbuf_addf(&oldsection, "branch.%s", interpreted_oldname); + strbuf_addf(&newsection, "branch.%s", interpreted_newname); + if (!copy && git_config_rename_section(oldsection.buf, newsection.buf) < 0) + die(_("branch is renamed, but update of config-file failed")); + if (copy && strcmp(interpreted_oldname, interpreted_newname) && git_config_copy_section(oldsection.buf, newsection.buf) < 0) + die(_("branch is copied, but update of config-file failed")); + strbuf_release(&oldref); + strbuf_release(&newref); + strbuf_release(&oldsection); + strbuf_release(&newsection); + free_worktrees(worktrees); +} + +static GIT_PATH_FUNC(edit_description, "EDIT_DESCRIPTION") + +static int edit_branch_description(const char *branch_name) +{ + int exists; + struct strbuf buf = STRBUF_INIT; + struct strbuf name = STRBUF_INIT; + + exists = !read_branch_desc(&buf, branch_name); + if (!buf.len || buf.buf[buf.len-1] != '\n') + strbuf_addch(&buf, '\n'); + strbuf_commented_addf(&buf, comment_line_char, + _("Please edit the description for the branch\n" + " %s\n" + "Lines starting with '%c' will be stripped.\n"), + branch_name, comment_line_char); + write_file_buf(edit_description(), buf.buf, buf.len); + strbuf_reset(&buf); + if (launch_editor(edit_description(), &buf, NULL)) { + strbuf_release(&buf); + return -1; + } + strbuf_stripspace(&buf, comment_line_char); + + strbuf_addf(&name, "branch.%s.description", branch_name); + if (buf.len || exists) + git_config_set(name.buf, buf.len ? buf.buf : NULL); + strbuf_release(&name); + strbuf_release(&buf); + + return 0; +} + +int cmd_branch(int argc, const char **argv, const char *prefix) +{ + /* possible actions */ + int delete = 0, rename = 0, copy = 0, list = 0, + unset_upstream = 0, show_current = 0, edit_description = 0; + const char *new_upstream = NULL; + int noncreate_actions = 0; + /* possible options */ + int reflog = 0, quiet = 0, icase = 0, force = 0, + recurse_submodules_explicit = 0; + enum branch_track track; + struct ref_filter filter = REF_FILTER_INIT; + static struct ref_sorting *sorting; + struct string_list sorting_options = STRING_LIST_INIT_DUP; + struct ref_format format = REF_FORMAT_INIT; + + struct option options[] = { + OPT_GROUP(N_("Generic options")), + OPT__VERBOSE(&filter.verbose, + N_("show hash and subject, give twice for upstream branch")), + OPT__QUIET(&quiet, N_("suppress informational messages")), + OPT_CALLBACK_F('t', "track", &track, "(direct|inherit)", + N_("set branch tracking configuration"), + PARSE_OPT_OPTARG, + parse_opt_tracking_mode), + OPT_SET_INT_F(0, "set-upstream", &track, N_("do not use"), + BRANCH_TRACK_OVERRIDE, PARSE_OPT_HIDDEN), + OPT_STRING('u', "set-upstream-to", &new_upstream, N_("upstream"), N_("change the upstream info")), + OPT_BOOL(0, "unset-upstream", &unset_upstream, N_("unset the upstream info")), + OPT__COLOR(&branch_use_color, N_("use colored output")), + OPT_SET_INT_F('r', "remotes", &filter.kind, N_("act on remote-tracking branches"), + FILTER_REFS_REMOTES, + PARSE_OPT_NONEG), + OPT_CONTAINS(&filter.with_commit, N_("print only branches that contain the commit")), + OPT_NO_CONTAINS(&filter.no_commit, N_("print only branches that don't contain the commit")), + OPT_WITH(&filter.with_commit, N_("print only branches that contain the commit")), + OPT_WITHOUT(&filter.no_commit, N_("print only branches that don't contain the commit")), + OPT__ABBREV(&filter.abbrev), + + OPT_GROUP(N_("Specific git-branch actions:")), + OPT_SET_INT_F('a', "all", &filter.kind, N_("list both remote-tracking and local branches"), + FILTER_REFS_REMOTES | FILTER_REFS_BRANCHES, + PARSE_OPT_NONEG), + OPT_BIT('d', "delete", &delete, N_("delete fully merged branch"), 1), + OPT_BIT('D', NULL, &delete, N_("delete branch (even if not merged)"), 2), + OPT_BIT('m', "move", &rename, N_("move/rename a branch and its reflog"), 1), + OPT_BIT('M', NULL, &rename, N_("move/rename a branch, even if target exists"), 2), + OPT_BOOL(0, "omit-empty", &omit_empty, + N_("do not output a newline after empty formatted refs")), + OPT_BIT('c', "copy", ©, N_("copy a branch and its reflog"), 1), + OPT_BIT('C', NULL, ©, N_("copy a branch, even if target exists"), 2), + OPT_BOOL('l', "list", &list, N_("list branch names")), + OPT_BOOL(0, "show-current", &show_current, N_("show current branch name")), + OPT_BOOL(0, "create-reflog", &reflog, N_("create the branch's reflog")), + OPT_BOOL(0, "edit-description", &edit_description, + N_("edit the description for the branch")), + OPT__FORCE(&force, N_("force creation, move/rename, deletion"), PARSE_OPT_NOCOMPLETE), + OPT_MERGED(&filter, N_("print only branches that are merged")), + OPT_NO_MERGED(&filter, N_("print only branches that are not merged")), + OPT_COLUMN(0, "column", &colopts, N_("list branches in columns")), + OPT_REF_SORT(&sorting_options), + OPT_CALLBACK(0, "points-at", &filter.points_at, N_("object"), + N_("print only branches of the object"), parse_opt_object_name), + OPT_BOOL('i', "ignore-case", &icase, N_("sorting and filtering are case insensitive")), + OPT_BOOL(0, "recurse-submodules", &recurse_submodules_explicit, N_("recurse through submodules")), + OPT_STRING( 0 , "format", &format.format, N_("format"), N_("format to use for the output")), + OPT_END(), + }; + + setup_ref_filter_porcelain_msg(); + + filter.kind = FILTER_REFS_BRANCHES; + filter.abbrev = -1; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(builtin_branch_usage, options); + + git_config(git_branch_config, &sorting_options); + + track = git_branch_track; + + head = resolve_refdup("HEAD", 0, &head_oid, NULL); + if (!head) + die(_("failed to resolve HEAD as a valid ref")); + if (!strcmp(head, "HEAD")) + filter.detached = 1; + else if (!skip_prefix(head, "refs/heads/", &head)) + die(_("HEAD not found below refs/heads!")); + + argc = parse_options(argc, argv, prefix, options, builtin_branch_usage, + 0); + + if (!delete && !rename && !copy && !edit_description && !new_upstream && + !show_current && !unset_upstream && argc == 0) + list = 1; + + if (filter.with_commit || filter.no_commit || + filter.reachable_from || filter.unreachable_from || filter.points_at.nr) + list = 1; + + noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream + + !!show_current + !!list + !!edit_description + + !!unset_upstream; + if (noncreate_actions > 1) + usage_with_options(builtin_branch_usage, options); + + if (recurse_submodules_explicit) { + if (!submodule_propagate_branches) + die(_("branch with --recurse-submodules can only be used if submodule.propagateBranches is enabled")); + if (noncreate_actions) + die(_("--recurse-submodules can only be used to create branches")); + } + + recurse_submodules = + (recurse_submodules || recurse_submodules_explicit) && + submodule_propagate_branches; + + if (filter.abbrev == -1) + filter.abbrev = DEFAULT_ABBREV; + filter.ignore_case = icase; + + finalize_colopts(&colopts, -1); + if (filter.verbose) { + if (explicitly_enable_column(colopts)) + die(_("options '%s' and '%s' cannot be used together"), "--column", "--verbose"); + colopts = 0; + } + + if (force) { + delete *= 2; + rename *= 2; + copy *= 2; + } + + if (list) + setup_auto_pager("branch", 1); + + UNLEAK(sorting_options); + + if (delete) { + if (!argc) + die(_("branch name required")); + return delete_branches(argc, argv, delete > 1, filter.kind, quiet); + } else if (show_current) { + print_current_branch_name(); + return 0; + } else if (list) { + /* git branch --list also shows HEAD when it is detached */ + if ((filter.kind & FILTER_REFS_BRANCHES) && filter.detached) + filter.kind |= FILTER_REFS_DETACHED_HEAD; + filter.name_patterns = argv; + /* + * If no sorting parameter is given then we default to sorting + * by 'refname'. This would give us an alphabetically sorted + * array with the 'HEAD' ref at the beginning followed by + * local branches 'refs/heads/...' and finally remote-tracking + * branches 'refs/remotes/...'. + */ + sorting = ref_sorting_options(&sorting_options); + ref_sorting_set_sort_flags_all(sorting, REF_SORTING_ICASE, icase); + ref_sorting_set_sort_flags_all( + sorting, REF_SORTING_DETACHED_HEAD_FIRST, 1); + print_ref_list(&filter, sorting, &format, &output); + print_columns(&output, colopts, NULL); + string_list_clear(&output, 0); + ref_sorting_release(sorting); + ref_filter_clear(&filter); + return 0; + } else if (edit_description) { + const char *branch_name; + struct strbuf branch_ref = STRBUF_INIT; + struct strbuf buf = STRBUF_INIT; + int ret = 1; /* assume failure */ + + if (!argc) { + if (filter.detached) + die(_("cannot give description to detached HEAD")); + branch_name = head; + } else if (argc == 1) { + strbuf_branchname(&buf, argv[0], INTERPRET_BRANCH_LOCAL); + branch_name = buf.buf; + } else { + die(_("cannot edit description of more than one branch")); + } + + strbuf_addf(&branch_ref, "refs/heads/%s", branch_name); + if (!ref_exists(branch_ref.buf)) + error((!argc || branch_checked_out(branch_ref.buf)) + ? _("no commit on branch '%s' yet") + : _("no branch named '%s'"), + branch_name); + else if (!edit_branch_description(branch_name)) + ret = 0; /* happy */ + + strbuf_release(&branch_ref); + strbuf_release(&buf); + + return ret; + } else if (copy || rename) { + if (!argc) + die(_("branch name required")); + else if ((argc == 1) && filter.detached) + die(copy? _("cannot copy the current branch while not on any") + : _("cannot rename the current branch while not on any")); + else if (argc == 1) + copy_or_rename_branch(head, argv[0], copy, copy + rename > 1); + else if (argc == 2) + copy_or_rename_branch(argv[0], argv[1], copy, copy + rename > 1); + else + die(copy? _("too many branches for a copy operation") + : _("too many arguments for a rename operation")); + } else if (new_upstream) { + struct branch *branch; + struct strbuf buf = STRBUF_INIT; + + if (!argc) + branch = branch_get(NULL); + else if (argc == 1) { + strbuf_branchname(&buf, argv[0], INTERPRET_BRANCH_LOCAL); + branch = branch_get(buf.buf); + } else + die(_("too many arguments to set new upstream")); + + if (!branch) { + if (!argc || !strcmp(argv[0], "HEAD")) + die(_("could not set upstream of HEAD to %s when " + "it does not point to any branch"), + new_upstream); + die(_("no such branch '%s'"), argv[0]); + } + + if (!ref_exists(branch->refname)) { + if (!argc || branch_checked_out(branch->refname)) + die(_("no commit on branch '%s' yet"), branch->name); + die(_("branch '%s' does not exist"), branch->name); + } + + dwim_and_setup_tracking(the_repository, branch->name, + new_upstream, BRANCH_TRACK_OVERRIDE, + quiet); + strbuf_release(&buf); + } else if (unset_upstream) { + struct branch *branch; + struct strbuf buf = STRBUF_INIT; + + if (!argc) + branch = branch_get(NULL); + else if (argc == 1) { + strbuf_branchname(&buf, argv[0], INTERPRET_BRANCH_LOCAL); + branch = branch_get(buf.buf); + } else + die(_("too many arguments to unset upstream")); + + if (!branch) { + if (!argc || !strcmp(argv[0], "HEAD")) + die(_("could not unset upstream of HEAD when " + "it does not point to any branch")); + die(_("no such branch '%s'"), argv[0]); + } + + if (!branch_has_merge_config(branch)) + die(_("branch '%s' has no upstream information"), branch->name); + + strbuf_reset(&buf); + strbuf_addf(&buf, "branch.%s.remote", branch->name); + git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE); + strbuf_reset(&buf); + strbuf_addf(&buf, "branch.%s.merge", branch->name); + git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE); + strbuf_release(&buf); + } else if (!noncreate_actions && argc > 0 && argc <= 2) { + const char *branch_name = argv[0]; + const char *start_name = argc == 2 ? argv[1] : head; + + if (filter.kind != FILTER_REFS_BRANCHES) + die(_("the -a, and -r, options to 'git branch' do not take a branch name.\n" + "Did you mean to use: -a|-r --list ?")); + + if (track == BRANCH_TRACK_OVERRIDE) + die(_("the '--set-upstream' option is no longer supported. Please use '--track' or '--set-upstream-to' instead")); + + if (recurse_submodules) { + create_branches_recursively(the_repository, branch_name, + start_name, NULL, force, + reflog, quiet, track, 0); + return 0; + } + create_branch(the_repository, branch_name, start_name, force, 0, + reflog, quiet, track, 0); + } else + usage_with_options(builtin_branch_usage, options); + + return 0; +} diff --git a/builtin/bugreport.c b/builtin/bugreport.c new file mode 100644 index 0000000..3106e56 --- /dev/null +++ b/builtin/bugreport.c @@ -0,0 +1,200 @@ +#include "builtin.h" +#include "abspath.h" +#include "editor.h" +#include "gettext.h" +#include "parse-options.h" +#include "strbuf.h" +#include "help.h" +#include "compat/compiler.h" +#include "hook.h" +#include "hook-list.h" +#include "diagnose.h" +#include "object-file.h" +#include "setup.h" + +static void get_system_info(struct strbuf *sys_info) +{ + struct utsname uname_info; + char *shell = NULL; + + /* get git version from native cmd */ + strbuf_addstr(sys_info, _("git version:\n")); + get_version_info(sys_info, 1); + + /* system call for other version info */ + strbuf_addstr(sys_info, "uname: "); + if (uname(&uname_info)) + strbuf_addf(sys_info, _("uname() failed with error '%s' (%d)\n"), + strerror(errno), + errno); + else + strbuf_addf(sys_info, "%s %s %s %s\n", + uname_info.sysname, + uname_info.release, + uname_info.version, + uname_info.machine); + + strbuf_addstr(sys_info, _("compiler info: ")); + get_compiler_info(sys_info); + + strbuf_addstr(sys_info, _("libc info: ")); + get_libc_info(sys_info); + + shell = getenv("SHELL"); + strbuf_addf(sys_info, "$SHELL (typically, interactive shell): %s\n", + shell ? shell : ""); +} + +static void get_populated_hooks(struct strbuf *hook_info, int nongit) +{ + const char **p; + + if (nongit) { + strbuf_addstr(hook_info, + _("not run from a git repository - no hooks to show\n")); + return; + } + + for (p = hook_name_list; *p; p++) { + const char *hook = *p; + + if (hook_exists(hook)) + strbuf_addf(hook_info, "%s\n", hook); + } +} + +static const char * const bugreport_usage[] = { + N_("git bugreport [(-o | --output-directory) ] [(-s | --suffix) ]\n" + " [--diagnose[=]]"), + NULL +}; + +static int get_bug_template(struct strbuf *template) +{ + const char template_text[] = N_( +"Thank you for filling out a Git bug report!\n" +"Please answer the following questions to help us understand your issue.\n" +"\n" +"What did you do before the bug happened? (Steps to reproduce your issue)\n" +"\n" +"What did you expect to happen? (Expected behavior)\n" +"\n" +"What happened instead? (Actual behavior)\n" +"\n" +"What's different between what you expected and what actually happened?\n" +"\n" +"Anything else you want to add:\n" +"\n" +"Please review the rest of the bug report below.\n" +"You can delete any lines you don't wish to share.\n"); + + strbuf_addstr(template, _(template_text)); + return 0; +} + +static void get_header(struct strbuf *buf, const char *title) +{ + strbuf_addf(buf, "\n\n[%s]\n", title); +} + +int cmd_bugreport(int argc, const char **argv, const char *prefix) +{ + struct strbuf buffer = STRBUF_INIT; + struct strbuf report_path = STRBUF_INIT; + int report = -1; + time_t now = time(NULL); + struct tm tm; + enum diagnose_mode diagnose = DIAGNOSE_NONE; + char *option_output = NULL; + char *option_suffix = "%Y-%m-%d-%H%M"; + const char *user_relative_path = NULL; + char *prefixed_filename; + size_t output_path_len; + int ret; + + const struct option bugreport_options[] = { + OPT_CALLBACK_F(0, "diagnose", &diagnose, N_("mode"), + N_("create an additional zip archive of detailed diagnostics (default 'stats')"), + PARSE_OPT_OPTARG, option_parse_diagnose), + OPT_STRING('o', "output-directory", &option_output, N_("path"), + N_("specify a destination for the bugreport file(s)")), + OPT_STRING('s', "suffix", &option_suffix, N_("format"), + N_("specify a strftime format suffix for the filename(s)")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, bugreport_options, + bugreport_usage, 0); + + if (argc) { + error(_("unknown argument `%s'"), argv[0]); + usage(bugreport_usage[0]); + } + + /* Prepare the path to put the result */ + prefixed_filename = prefix_filename(prefix, + option_output ? option_output : ""); + strbuf_addstr(&report_path, prefixed_filename); + strbuf_complete(&report_path, '/'); + output_path_len = report_path.len; + + strbuf_addstr(&report_path, "git-bugreport-"); + strbuf_addftime(&report_path, option_suffix, localtime_r(&now, &tm), 0, 0); + strbuf_addstr(&report_path, ".txt"); + + switch (safe_create_leading_directories(report_path.buf)) { + case SCLD_OK: + case SCLD_EXISTS: + break; + default: + die(_("could not create leading directories for '%s'"), + report_path.buf); + } + + /* Prepare diagnostics, if requested */ + if (diagnose != DIAGNOSE_NONE) { + struct strbuf zip_path = STRBUF_INIT; + strbuf_add(&zip_path, report_path.buf, output_path_len); + strbuf_addstr(&zip_path, "git-diagnostics-"); + strbuf_addftime(&zip_path, option_suffix, localtime_r(&now, &tm), 0, 0); + strbuf_addstr(&zip_path, ".zip"); + + if (create_diagnostics_archive(&zip_path, diagnose)) + die_errno(_("unable to create diagnostics archive %s"), zip_path.buf); + + strbuf_release(&zip_path); + } + + /* Prepare the report contents */ + get_bug_template(&buffer); + + get_header(&buffer, _("System Info")); + get_system_info(&buffer); + + get_header(&buffer, _("Enabled Hooks")); + get_populated_hooks(&buffer, !startup_info->have_repository); + + /* fopen doesn't offer us an O_EXCL alternative, except with glibc. */ + report = xopen(report_path.buf, O_CREAT | O_EXCL | O_WRONLY, 0666); + + if (write_in_full(report, buffer.buf, buffer.len) < 0) + die_errno(_("unable to write to %s"), report_path.buf); + + close(report); + + /* + * We want to print the path relative to the user, but we still need the + * path relative to us to give to the editor. + */ + if (!(prefix && skip_prefix(report_path.buf, prefix, &user_relative_path))) + user_relative_path = report_path.buf; + fprintf(stderr, _("Created new report at '%s'.\n"), + user_relative_path); + + free(prefixed_filename); + strbuf_release(&buffer); + + ret = !!launch_editor(report_path.buf, NULL, NULL); + strbuf_release(&report_path); + return ret; +} diff --git a/builtin/bundle.c b/builtin/bundle.c new file mode 100644 index 0000000..3ad11dc --- /dev/null +++ b/builtin/bundle.c @@ -0,0 +1,240 @@ +#include "builtin.h" +#include "abspath.h" +#include "gettext.h" +#include "setup.h" +#include "strvec.h" +#include "parse-options.h" +#include "pkt-line.h" +#include "repository.h" +#include "bundle.h" + +/* + * Basic handler for bundle files to connect repositories via sneakernet. + * Invocation must include action. + * This function can create a bundle or provide information on an existing + * bundle supporting "fetch", "pull", and "ls-remote". + */ + +#define BUILTIN_BUNDLE_CREATE_USAGE \ + N_("git bundle create [-q | --quiet | --progress]\n" \ + " [--version=] ") +#define BUILTIN_BUNDLE_VERIFY_USAGE \ + N_("git bundle verify [-q | --quiet] ") +#define BUILTIN_BUNDLE_LIST_HEADS_USAGE \ + N_("git bundle list-heads [...]") +#define BUILTIN_BUNDLE_UNBUNDLE_USAGE \ + N_("git bundle unbundle [--progress] [...]") + +static char const * const builtin_bundle_usage[] = { + BUILTIN_BUNDLE_CREATE_USAGE, + BUILTIN_BUNDLE_VERIFY_USAGE, + BUILTIN_BUNDLE_LIST_HEADS_USAGE, + BUILTIN_BUNDLE_UNBUNDLE_USAGE, + NULL, +}; + +static const char * const builtin_bundle_create_usage[] = { + BUILTIN_BUNDLE_CREATE_USAGE, + NULL +}; + +static const char * const builtin_bundle_verify_usage[] = { + BUILTIN_BUNDLE_VERIFY_USAGE, + NULL +}; + +static const char * const builtin_bundle_list_heads_usage[] = { + BUILTIN_BUNDLE_LIST_HEADS_USAGE, + NULL +}; + +static const char * const builtin_bundle_unbundle_usage[] = { + BUILTIN_BUNDLE_UNBUNDLE_USAGE, + NULL +}; + +static int parse_options_cmd_bundle(int argc, + const char **argv, + const char* prefix, + const char * const usagestr[], + const struct option options[], + char **bundle_file) { + argc = parse_options(argc, argv, NULL, options, usagestr, + PARSE_OPT_STOP_AT_NON_OPTION); + if (!argc) + usage_msg_opt(_("need a argument"), usagestr, options); + *bundle_file = prefix_filename_except_for_dash(prefix, argv[0]); + return argc; +} + +static int cmd_bundle_create(int argc, const char **argv, const char *prefix) { + struct strvec pack_opts = STRVEC_INIT; + int version = -1; + int ret; + struct option options[] = { + OPT_PASSTHRU_ARGV('q', "quiet", &pack_opts, NULL, + N_("do not show progress meter"), + PARSE_OPT_NOARG), + OPT_PASSTHRU_ARGV(0, "progress", &pack_opts, NULL, + N_("show progress meter"), + PARSE_OPT_NOARG), + OPT_PASSTHRU_ARGV(0, "all-progress", &pack_opts, NULL, + N_("historical; same as --progress"), + PARSE_OPT_NOARG | PARSE_OPT_HIDDEN), + OPT_PASSTHRU_ARGV(0, "all-progress-implied", &pack_opts, NULL, + N_("historical; does nothing"), + PARSE_OPT_NOARG | PARSE_OPT_HIDDEN), + OPT_INTEGER(0, "version", &version, + N_("specify bundle format version")), + OPT_END() + }; + char *bundle_file; + + if (isatty(STDERR_FILENO)) + strvec_push(&pack_opts, "--progress"); + strvec_push(&pack_opts, "--all-progress-implied"); + + argc = parse_options_cmd_bundle(argc, argv, prefix, + builtin_bundle_create_usage, options, &bundle_file); + /* bundle internals use argv[1] as further parameters */ + + if (!startup_info->have_repository) + die(_("Need a repository to create a bundle.")); + ret = !!create_bundle(the_repository, bundle_file, argc, argv, &pack_opts, version); + strvec_clear(&pack_opts); + free(bundle_file); + return ret; +} + +/* + * Similar to read_bundle_header(), but handle "-" as stdin. + */ +static int open_bundle(const char *path, struct bundle_header *header, + const char **name) +{ + if (!strcmp(path, "-")) { + if (name) + *name = ""; + return read_bundle_header_fd(0, header, ""); + } + + if (name) + *name = path; + return read_bundle_header(path, header); +} + +static int cmd_bundle_verify(int argc, const char **argv, const char *prefix) { + struct bundle_header header = BUNDLE_HEADER_INIT; + int bundle_fd = -1; + int quiet = 0; + int ret; + struct option options[] = { + OPT_BOOL('q', "quiet", &quiet, + N_("do not show bundle details")), + OPT_END() + }; + char *bundle_file; + const char *name; + + argc = parse_options_cmd_bundle(argc, argv, prefix, + builtin_bundle_verify_usage, options, &bundle_file); + /* bundle internals use argv[1] as further parameters */ + + if ((bundle_fd = open_bundle(bundle_file, &header, &name)) < 0) { + ret = 1; + goto cleanup; + } + close(bundle_fd); + if (verify_bundle(the_repository, &header, + quiet ? VERIFY_BUNDLE_QUIET : VERIFY_BUNDLE_VERBOSE)) { + ret = 1; + goto cleanup; + } + + fprintf(stderr, _("%s is okay\n"), name); + ret = 0; +cleanup: + free(bundle_file); + bundle_header_release(&header); + return ret; +} + +static int cmd_bundle_list_heads(int argc, const char **argv, const char *prefix) { + struct bundle_header header = BUNDLE_HEADER_INIT; + int bundle_fd = -1; + int ret; + struct option options[] = { + OPT_END() + }; + char *bundle_file; + + argc = parse_options_cmd_bundle(argc, argv, prefix, + builtin_bundle_list_heads_usage, options, &bundle_file); + /* bundle internals use argv[1] as further parameters */ + + if ((bundle_fd = open_bundle(bundle_file, &header, NULL)) < 0) { + ret = 1; + goto cleanup; + } + close(bundle_fd); + ret = !!list_bundle_refs(&header, argc, argv); +cleanup: + free(bundle_file); + bundle_header_release(&header); + return ret; +} + +static int cmd_bundle_unbundle(int argc, const char **argv, const char *prefix) { + struct bundle_header header = BUNDLE_HEADER_INIT; + int bundle_fd = -1; + int ret; + int progress = isatty(2); + + struct option options[] = { + OPT_BOOL(0, "progress", &progress, + N_("show progress meter")), + OPT_END() + }; + char *bundle_file; + struct strvec extra_index_pack_args = STRVEC_INIT; + + argc = parse_options_cmd_bundle(argc, argv, prefix, + builtin_bundle_unbundle_usage, options, &bundle_file); + /* bundle internals use argv[1] as further parameters */ + + if ((bundle_fd = open_bundle(bundle_file, &header, NULL)) < 0) { + ret = 1; + goto cleanup; + } + if (!startup_info->have_repository) + die(_("Need a repository to unbundle.")); + if (progress) + strvec_pushl(&extra_index_pack_args, "-v", "--progress-title", + _("Unbundling objects"), NULL); + ret = !!unbundle(the_repository, &header, bundle_fd, + &extra_index_pack_args, 0) || + list_bundle_refs(&header, argc, argv); + bundle_header_release(&header); +cleanup: + free(bundle_file); + return ret; +} + +int cmd_bundle(int argc, const char **argv, const char *prefix) +{ + parse_opt_subcommand_fn *fn = NULL; + struct option options[] = { + OPT_SUBCOMMAND("create", &fn, cmd_bundle_create), + OPT_SUBCOMMAND("verify", &fn, cmd_bundle_verify), + OPT_SUBCOMMAND("list-heads", &fn, cmd_bundle_list_heads), + OPT_SUBCOMMAND("unbundle", &fn, cmd_bundle_unbundle), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, builtin_bundle_usage, + 0); + + packet_trace_identity("bundle"); + + return !!fn(argc, argv, prefix); +} diff --git a/builtin/cat-file.c b/builtin/cat-file.c new file mode 100644 index 0000000..ea8ad60 --- /dev/null +++ b/builtin/cat-file.c @@ -0,0 +1,1075 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "config.h" +#include "convert.h" +#include "diff.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "ident.h" +#include "parse-options.h" +#include "userdiff.h" +#include "streaming.h" +#include "tree-walk.h" +#include "oid-array.h" +#include "packfile.h" +#include "object-file.h" +#include "object-name.h" +#include "object-store-ll.h" +#include "replace-object.h" +#include "promisor-remote.h" +#include "mailmap.h" +#include "write-or-die.h" + +enum batch_mode { + BATCH_MODE_CONTENTS, + BATCH_MODE_INFO, + BATCH_MODE_QUEUE_AND_DISPATCH, +}; + +struct batch_options { + int enabled; + int follow_symlinks; + enum batch_mode batch_mode; + int buffer_output; + int all_objects; + int unordered; + int transform_mode; /* may be 'w' or 'c' for --filters or --textconv */ + char input_delim; + char output_delim; + const char *format; +}; + +static const char *force_path; + +static struct string_list mailmap = STRING_LIST_INIT_NODUP; +static int use_mailmap; + +static char *replace_idents_using_mailmap(char *, size_t *); + +static char *replace_idents_using_mailmap(char *object_buf, size_t *size) +{ + struct strbuf sb = STRBUF_INIT; + const char *headers[] = { "author ", "committer ", "tagger ", NULL }; + + strbuf_attach(&sb, object_buf, *size, *size + 1); + apply_mailmap_to_header(&sb, headers, &mailmap); + *size = sb.len; + return strbuf_detach(&sb, NULL); +} + +static int filter_object(const char *path, unsigned mode, + const struct object_id *oid, + char **buf, unsigned long *size) +{ + enum object_type type; + + *buf = repo_read_object_file(the_repository, oid, &type, size); + if (!*buf) + return error(_("cannot read object %s '%s'"), + oid_to_hex(oid), path); + if ((type == OBJ_BLOB) && S_ISREG(mode)) { + struct strbuf strbuf = STRBUF_INIT; + struct checkout_metadata meta; + + init_checkout_metadata(&meta, NULL, NULL, oid); + if (convert_to_working_tree(&the_index, path, *buf, *size, &strbuf, &meta)) { + free(*buf); + *size = strbuf.len; + *buf = strbuf_detach(&strbuf, NULL); + } + } + + return 0; +} + +static int stream_blob(const struct object_id *oid) +{ + if (stream_blob_to_fd(1, oid, NULL, 0)) + die("unable to stream %s to stdout", oid_to_hex(oid)); + return 0; +} + +static int cat_one_file(int opt, const char *exp_type, const char *obj_name, + int unknown_type) +{ + int ret; + struct object_id oid; + enum object_type type; + char *buf; + unsigned long size; + struct object_context obj_context; + struct object_info oi = OBJECT_INFO_INIT; + struct strbuf sb = STRBUF_INIT; + unsigned flags = OBJECT_INFO_LOOKUP_REPLACE; + unsigned get_oid_flags = GET_OID_RECORD_PATH | GET_OID_ONLY_TO_DIE; + const char *path = force_path; + const int opt_cw = (opt == 'c' || opt == 'w'); + if (!path && opt_cw) + get_oid_flags |= GET_OID_REQUIRE_PATH; + + if (unknown_type) + flags |= OBJECT_INFO_ALLOW_UNKNOWN_TYPE; + + if (get_oid_with_context(the_repository, obj_name, get_oid_flags, &oid, + &obj_context)) + die("Not a valid object name %s", obj_name); + + if (!path) + path = obj_context.path; + if (obj_context.mode == S_IFINVALID) + obj_context.mode = 0100644; + + buf = NULL; + switch (opt) { + case 't': + oi.type_name = &sb; + if (oid_object_info_extended(the_repository, &oid, &oi, flags) < 0) + die("git cat-file: could not get object info"); + if (sb.len) { + printf("%s\n", sb.buf); + strbuf_release(&sb); + ret = 0; + goto cleanup; + } + break; + + case 's': + oi.sizep = &size; + + if (use_mailmap) { + oi.typep = &type; + oi.contentp = (void**)&buf; + } + + if (oid_object_info_extended(the_repository, &oid, &oi, flags) < 0) + die("git cat-file: could not get object info"); + + if (use_mailmap && (type == OBJ_COMMIT || type == OBJ_TAG)) { + size_t s = size; + buf = replace_idents_using_mailmap(buf, &s); + size = cast_size_t_to_ulong(s); + } + + printf("%"PRIuMAX"\n", (uintmax_t)size); + ret = 0; + goto cleanup; + + case 'e': + return !repo_has_object_file(the_repository, &oid); + + case 'w': + + if (filter_object(path, obj_context.mode, + &oid, &buf, &size)) { + ret = -1; + goto cleanup; + } + break; + + case 'c': + if (textconv_object(the_repository, path, obj_context.mode, + &oid, 1, &buf, &size)) + break; + /* else fallthrough */ + + case 'p': + type = oid_object_info(the_repository, &oid, NULL); + if (type < 0) + die("Not a valid object name %s", obj_name); + + /* custom pretty-print here */ + if (type == OBJ_TREE) { + const char *ls_args[3] = { NULL }; + ls_args[0] = "ls-tree"; + ls_args[1] = obj_name; + ret = cmd_ls_tree(2, ls_args, NULL); + goto cleanup; + } + + if (type == OBJ_BLOB) { + ret = stream_blob(&oid); + goto cleanup; + } + buf = repo_read_object_file(the_repository, &oid, &type, + &size); + if (!buf) + die("Cannot read object %s", obj_name); + + if (use_mailmap) { + size_t s = size; + buf = replace_idents_using_mailmap(buf, &s); + size = cast_size_t_to_ulong(s); + } + + /* otherwise just spit out the data */ + break; + + case 0: + { + enum object_type exp_type_id = type_from_string(exp_type); + + if (exp_type_id == OBJ_BLOB) { + struct object_id blob_oid; + if (oid_object_info(the_repository, &oid, NULL) == OBJ_TAG) { + char *buffer = repo_read_object_file(the_repository, + &oid, + &type, + &size); + const char *target; + if (!skip_prefix(buffer, "object ", &target) || + get_oid_hex(target, &blob_oid)) + die("%s not a valid tag", oid_to_hex(&oid)); + free(buffer); + } else + oidcpy(&blob_oid, &oid); + + if (oid_object_info(the_repository, &blob_oid, NULL) == OBJ_BLOB) { + ret = stream_blob(&blob_oid); + goto cleanup; + } + /* + * we attempted to dereference a tag to a blob + * and failed; there may be new dereference + * mechanisms this code is not aware of. + * fall-back to the usual case. + */ + } + buf = read_object_with_reference(the_repository, &oid, + exp_type_id, &size, NULL); + + if (use_mailmap) { + size_t s = size; + buf = replace_idents_using_mailmap(buf, &s); + size = cast_size_t_to_ulong(s); + } + break; + } + default: + die("git cat-file: unknown option: %s", exp_type); + } + + if (!buf) + die("git cat-file %s: bad file", obj_name); + + write_or_die(1, buf, size); + ret = 0; +cleanup: + free(buf); + free(obj_context.path); + return ret; +} + +struct expand_data { + struct object_id oid; + enum object_type type; + unsigned long size; + off_t disk_size; + const char *rest; + struct object_id delta_base_oid; + + /* + * If mark_query is true, we do not expand anything, but rather + * just mark the object_info with items we wish to query. + */ + int mark_query; + + /* + * Whether to split the input on whitespace before feeding it to + * get_sha1; this is decided during the mark_query phase based on + * whether we have a %(rest) token in our format. + */ + int split_on_whitespace; + + /* + * After a mark_query run, this object_info is set up to be + * passed to oid_object_info_extended. It will point to the data + * elements above, so you can retrieve the response from there. + */ + struct object_info info; + + /* + * This flag will be true if the requested batch format and options + * don't require us to call oid_object_info, which can then be + * optimized out. + */ + unsigned skip_object_info : 1; +}; + +static int is_atom(const char *atom, const char *s, int slen) +{ + int alen = strlen(atom); + return alen == slen && !memcmp(atom, s, alen); +} + +static void expand_atom(struct strbuf *sb, const char *atom, int len, + struct expand_data *data) +{ + if (is_atom("objectname", atom, len)) { + if (!data->mark_query) + strbuf_addstr(sb, oid_to_hex(&data->oid)); + } else if (is_atom("objecttype", atom, len)) { + if (data->mark_query) + data->info.typep = &data->type; + else + strbuf_addstr(sb, type_name(data->type)); + } else if (is_atom("objectsize", atom, len)) { + if (data->mark_query) + data->info.sizep = &data->size; + else + strbuf_addf(sb, "%"PRIuMAX , (uintmax_t)data->size); + } else if (is_atom("objectsize:disk", atom, len)) { + if (data->mark_query) + data->info.disk_sizep = &data->disk_size; + else + strbuf_addf(sb, "%"PRIuMAX, (uintmax_t)data->disk_size); + } else if (is_atom("rest", atom, len)) { + if (data->mark_query) + data->split_on_whitespace = 1; + else if (data->rest) + strbuf_addstr(sb, data->rest); + } else if (is_atom("deltabase", atom, len)) { + if (data->mark_query) + data->info.delta_base_oid = &data->delta_base_oid; + else + strbuf_addstr(sb, + oid_to_hex(&data->delta_base_oid)); + } else + die("unknown format element: %.*s", len, atom); +} + +static void expand_format(struct strbuf *sb, const char *start, + struct expand_data *data) +{ + while (strbuf_expand_step(sb, &start)) { + const char *end; + + if (skip_prefix(start, "%", &start) || *start != '(') + strbuf_addch(sb, '%'); + else if (!(end = strchr(start + 1, ')'))) + die("format element '%s' does not end in ')'", start); + else { + expand_atom(sb, start + 1, end - start - 1, data); + start = end + 1; + } + } +} + +static void batch_write(struct batch_options *opt, const void *data, int len) +{ + if (opt->buffer_output) { + if (fwrite(data, 1, len, stdout) != len) + die_errno("unable to write to stdout"); + } else + write_or_die(1, data, len); +} + +static void print_object_or_die(struct batch_options *opt, struct expand_data *data) +{ + const struct object_id *oid = &data->oid; + + assert(data->info.typep); + + if (data->type == OBJ_BLOB) { + if (opt->buffer_output) + fflush(stdout); + if (opt->transform_mode) { + char *contents; + unsigned long size; + + if (!data->rest) + die("missing path for '%s'", oid_to_hex(oid)); + + if (opt->transform_mode == 'w') { + if (filter_object(data->rest, 0100644, oid, + &contents, &size)) + die("could not convert '%s' %s", + oid_to_hex(oid), data->rest); + } else if (opt->transform_mode == 'c') { + enum object_type type; + if (!textconv_object(the_repository, + data->rest, 0100644, oid, + 1, &contents, &size)) + contents = repo_read_object_file(the_repository, + oid, + &type, + &size); + if (!contents) + die("could not convert '%s' %s", + oid_to_hex(oid), data->rest); + } else + BUG("invalid transform_mode: %c", opt->transform_mode); + batch_write(opt, contents, size); + free(contents); + } else { + stream_blob(oid); + } + } + else { + enum object_type type; + unsigned long size; + void *contents; + + contents = repo_read_object_file(the_repository, oid, &type, + &size); + + if (use_mailmap) { + size_t s = size; + contents = replace_idents_using_mailmap(contents, &s); + size = cast_size_t_to_ulong(s); + } + + if (!contents) + die("object %s disappeared", oid_to_hex(oid)); + if (type != data->type) + die("object %s changed type!?", oid_to_hex(oid)); + if (data->info.sizep && size != data->size && !use_mailmap) + die("object %s changed size!?", oid_to_hex(oid)); + + batch_write(opt, contents, size); + free(contents); + } +} + +static void print_default_format(struct strbuf *scratch, struct expand_data *data, + struct batch_options *opt) +{ + strbuf_addf(scratch, "%s %s %"PRIuMAX"%c", oid_to_hex(&data->oid), + type_name(data->type), + (uintmax_t)data->size, opt->output_delim); +} + +/* + * If "pack" is non-NULL, then "offset" is the byte offset within the pack from + * which the object may be accessed (though note that we may also rely on + * data->oid, too). If "pack" is NULL, then offset is ignored. + */ +static void batch_object_write(const char *obj_name, + struct strbuf *scratch, + struct batch_options *opt, + struct expand_data *data, + struct packed_git *pack, + off_t offset) +{ + if (!data->skip_object_info) { + int ret; + + if (use_mailmap) + data->info.typep = &data->type; + + if (pack) + ret = packed_object_info(the_repository, pack, offset, + &data->info); + else + ret = oid_object_info_extended(the_repository, + &data->oid, &data->info, + OBJECT_INFO_LOOKUP_REPLACE); + if (ret < 0) { + printf("%s missing%c", + obj_name ? obj_name : oid_to_hex(&data->oid), opt->output_delim); + fflush(stdout); + return; + } + + if (use_mailmap && (data->type == OBJ_COMMIT || data->type == OBJ_TAG)) { + size_t s = data->size; + char *buf = NULL; + + buf = repo_read_object_file(the_repository, &data->oid, &data->type, + &data->size); + buf = replace_idents_using_mailmap(buf, &s); + data->size = cast_size_t_to_ulong(s); + + free(buf); + } + } + + strbuf_reset(scratch); + + if (!opt->format) { + print_default_format(scratch, data, opt); + } else { + expand_format(scratch, opt->format, data); + strbuf_addch(scratch, opt->output_delim); + } + + batch_write(opt, scratch->buf, scratch->len); + + if (opt->batch_mode == BATCH_MODE_CONTENTS) { + print_object_or_die(opt, data); + batch_write(opt, &opt->output_delim, 1); + } +} + +static void batch_one_object(const char *obj_name, + struct strbuf *scratch, + struct batch_options *opt, + struct expand_data *data) +{ + struct object_context ctx; + int flags = opt->follow_symlinks ? GET_OID_FOLLOW_SYMLINKS : 0; + enum get_oid_result result; + + result = get_oid_with_context(the_repository, obj_name, + flags, &data->oid, &ctx); + if (result != FOUND) { + switch (result) { + case MISSING_OBJECT: + printf("%s missing%c", obj_name, opt->output_delim); + break; + case SHORT_NAME_AMBIGUOUS: + printf("%s ambiguous%c", obj_name, opt->output_delim); + break; + case DANGLING_SYMLINK: + printf("dangling %"PRIuMAX"%c%s%c", + (uintmax_t)strlen(obj_name), + opt->output_delim, obj_name, opt->output_delim); + break; + case SYMLINK_LOOP: + printf("loop %"PRIuMAX"%c%s%c", + (uintmax_t)strlen(obj_name), + opt->output_delim, obj_name, opt->output_delim); + break; + case NOT_DIR: + printf("notdir %"PRIuMAX"%c%s%c", + (uintmax_t)strlen(obj_name), + opt->output_delim, obj_name, opt->output_delim); + break; + default: + BUG("unknown get_sha1_with_context result %d\n", + result); + break; + } + fflush(stdout); + return; + } + + if (ctx.mode == 0) { + printf("symlink %"PRIuMAX"%c%s%c", + (uintmax_t)ctx.symlink_path.len, + opt->output_delim, ctx.symlink_path.buf, opt->output_delim); + fflush(stdout); + return; + } + + batch_object_write(obj_name, scratch, opt, data, NULL, 0); +} + +struct object_cb_data { + struct batch_options *opt; + struct expand_data *expand; + struct oidset *seen; + struct strbuf *scratch; +}; + +static int batch_object_cb(const struct object_id *oid, void *vdata) +{ + struct object_cb_data *data = vdata; + oidcpy(&data->expand->oid, oid); + batch_object_write(NULL, data->scratch, data->opt, data->expand, + NULL, 0); + return 0; +} + +static int collect_loose_object(const struct object_id *oid, + const char *path UNUSED, + void *data) +{ + oid_array_append(data, oid); + return 0; +} + +static int collect_packed_object(const struct object_id *oid, + struct packed_git *pack UNUSED, + uint32_t pos UNUSED, + void *data) +{ + oid_array_append(data, oid); + return 0; +} + +static int batch_unordered_object(const struct object_id *oid, + struct packed_git *pack, off_t offset, + void *vdata) +{ + struct object_cb_data *data = vdata; + + if (oidset_insert(data->seen, oid)) + return 0; + + oidcpy(&data->expand->oid, oid); + batch_object_write(NULL, data->scratch, data->opt, data->expand, + pack, offset); + return 0; +} + +static int batch_unordered_loose(const struct object_id *oid, + const char *path UNUSED, + void *data) +{ + return batch_unordered_object(oid, NULL, 0, data); +} + +static int batch_unordered_packed(const struct object_id *oid, + struct packed_git *pack, + uint32_t pos, + void *data) +{ + return batch_unordered_object(oid, pack, + nth_packed_object_offset(pack, pos), + data); +} + +typedef void (*parse_cmd_fn_t)(struct batch_options *, const char *, + struct strbuf *, struct expand_data *); + +struct queued_cmd { + parse_cmd_fn_t fn; + char *line; +}; + +static void parse_cmd_contents(struct batch_options *opt, + const char *line, + struct strbuf *output, + struct expand_data *data) +{ + opt->batch_mode = BATCH_MODE_CONTENTS; + batch_one_object(line, output, opt, data); +} + +static void parse_cmd_info(struct batch_options *opt, + const char *line, + struct strbuf *output, + struct expand_data *data) +{ + opt->batch_mode = BATCH_MODE_INFO; + batch_one_object(line, output, opt, data); +} + +static void dispatch_calls(struct batch_options *opt, + struct strbuf *output, + struct expand_data *data, + struct queued_cmd *cmd, + int nr) +{ + int i; + + if (!opt->buffer_output) + die(_("flush is only for --buffer mode")); + + for (i = 0; i < nr; i++) + cmd[i].fn(opt, cmd[i].line, output, data); + + fflush(stdout); +} + +static void free_cmds(struct queued_cmd *cmd, size_t *nr) +{ + size_t i; + + for (i = 0; i < *nr; i++) + FREE_AND_NULL(cmd[i].line); + + *nr = 0; +} + + +static const struct parse_cmd { + const char *name; + parse_cmd_fn_t fn; + unsigned takes_args; +} commands[] = { + { "contents", parse_cmd_contents, 1}, + { "info", parse_cmd_info, 1}, + { "flush", NULL, 0}, +}; + +static void batch_objects_command(struct batch_options *opt, + struct strbuf *output, + struct expand_data *data) +{ + struct strbuf input = STRBUF_INIT; + struct queued_cmd *queued_cmd = NULL; + size_t alloc = 0, nr = 0; + + while (strbuf_getdelim_strip_crlf(&input, stdin, opt->input_delim) != EOF) { + int i; + const struct parse_cmd *cmd = NULL; + const char *p = NULL, *cmd_end; + struct queued_cmd call = {0}; + + if (!input.len) + die(_("empty command in input")); + if (isspace(*input.buf)) + die(_("whitespace before command: '%s'"), input.buf); + + for (i = 0; i < ARRAY_SIZE(commands); i++) { + if (!skip_prefix(input.buf, commands[i].name, &cmd_end)) + continue; + + cmd = &commands[i]; + if (cmd->takes_args) { + if (*cmd_end != ' ') + die(_("%s requires arguments"), + commands[i].name); + + p = cmd_end + 1; + } else if (*cmd_end) { + die(_("%s takes no arguments"), + commands[i].name); + } + + break; + } + + if (!cmd) + die(_("unknown command: '%s'"), input.buf); + + if (!strcmp(cmd->name, "flush")) { + dispatch_calls(opt, output, data, queued_cmd, nr); + free_cmds(queued_cmd, &nr); + } else if (!opt->buffer_output) { + cmd->fn(opt, p, output, data); + } else { + ALLOC_GROW(queued_cmd, nr + 1, alloc); + call.fn = cmd->fn; + call.line = xstrdup_or_null(p); + queued_cmd[nr++] = call; + } + } + + if (opt->buffer_output && + nr && + !git_env_bool("GIT_TEST_CAT_FILE_NO_FLUSH_ON_EXIT", 0)) { + dispatch_calls(opt, output, data, queued_cmd, nr); + free_cmds(queued_cmd, &nr); + } + + free_cmds(queued_cmd, &nr); + free(queued_cmd); + strbuf_release(&input); +} + +#define DEFAULT_FORMAT "%(objectname) %(objecttype) %(objectsize)" + +static int batch_objects(struct batch_options *opt) +{ + struct strbuf input = STRBUF_INIT; + struct strbuf output = STRBUF_INIT; + struct expand_data data; + int save_warning; + int retval = 0; + + /* + * Expand once with our special mark_query flag, which will prime the + * object_info to be handed to oid_object_info_extended for each + * object. + */ + memset(&data, 0, sizeof(data)); + data.mark_query = 1; + expand_format(&output, + opt->format ? opt->format : DEFAULT_FORMAT, + &data); + data.mark_query = 0; + strbuf_release(&output); + if (opt->transform_mode) + data.split_on_whitespace = 1; + + if (opt->format && !strcmp(opt->format, DEFAULT_FORMAT)) + opt->format = NULL; + /* + * If we are printing out the object, then always fill in the type, + * since we will want to decide whether or not to stream. + */ + if (opt->batch_mode == BATCH_MODE_CONTENTS) + data.info.typep = &data.type; + + if (opt->all_objects) { + struct object_cb_data cb; + struct object_info empty = OBJECT_INFO_INIT; + + if (!memcmp(&data.info, &empty, sizeof(empty))) + data.skip_object_info = 1; + + if (repo_has_promisor_remote(the_repository)) + warning("This repository uses promisor remotes. Some objects may not be loaded."); + + disable_replace_refs(); + + cb.opt = opt; + cb.expand = &data; + cb.scratch = &output; + + if (opt->unordered) { + struct oidset seen = OIDSET_INIT; + + cb.seen = &seen; + + for_each_loose_object(batch_unordered_loose, &cb, 0); + for_each_packed_object(batch_unordered_packed, &cb, + FOR_EACH_OBJECT_PACK_ORDER); + + oidset_clear(&seen); + } else { + struct oid_array sa = OID_ARRAY_INIT; + + for_each_loose_object(collect_loose_object, &sa, 0); + for_each_packed_object(collect_packed_object, &sa, 0); + + oid_array_for_each_unique(&sa, batch_object_cb, &cb); + + oid_array_clear(&sa); + } + + strbuf_release(&output); + return 0; + } + + /* + * We are going to call get_sha1 on a potentially very large number of + * objects. In most large cases, these will be actual object sha1s. The + * cost to double-check that each one is not also a ref (just so we can + * warn) ends up dwarfing the actual cost of the object lookups + * themselves. We can work around it by just turning off the warning. + */ + save_warning = warn_on_object_refname_ambiguity; + warn_on_object_refname_ambiguity = 0; + + if (opt->batch_mode == BATCH_MODE_QUEUE_AND_DISPATCH) { + batch_objects_command(opt, &output, &data); + goto cleanup; + } + + while (strbuf_getdelim_strip_crlf(&input, stdin, opt->input_delim) != EOF) { + if (data.split_on_whitespace) { + /* + * Split at first whitespace, tying off the beginning + * of the string and saving the remainder (or NULL) in + * data.rest. + */ + char *p = strpbrk(input.buf, " \t"); + if (p) { + while (*p && strchr(" \t", *p)) + *p++ = '\0'; + } + data.rest = p; + } + + batch_one_object(input.buf, &output, opt, &data); + } + + cleanup: + strbuf_release(&input); + strbuf_release(&output); + warn_on_object_refname_ambiguity = save_warning; + return retval; +} + +static int git_cat_file_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + if (userdiff_config(var, value) < 0) + return -1; + + return git_default_config(var, value, ctx, cb); +} + +static int batch_option_callback(const struct option *opt, + const char *arg, + int unset) +{ + struct batch_options *bo = opt->value; + + BUG_ON_OPT_NEG(unset); + + if (bo->enabled) { + return error(_("only one batch option may be specified")); + } + + bo->enabled = 1; + + if (!strcmp(opt->long_name, "batch")) + bo->batch_mode = BATCH_MODE_CONTENTS; + else if (!strcmp(opt->long_name, "batch-check")) + bo->batch_mode = BATCH_MODE_INFO; + else if (!strcmp(opt->long_name, "batch-command")) + bo->batch_mode = BATCH_MODE_QUEUE_AND_DISPATCH; + else + BUG("%s given to batch-option-callback", opt->long_name); + + bo->format = arg; + + return 0; +} + +int cmd_cat_file(int argc, const char **argv, const char *prefix) +{ + int opt = 0; + int opt_cw = 0; + int opt_epts = 0; + const char *exp_type = NULL, *obj_name = NULL; + struct batch_options batch = {0}; + int unknown_type = 0; + int input_nul_terminated = 0; + int nul_terminated = 0; + + const char * const usage[] = { + N_("git cat-file "), + N_("git cat-file (-e | -p) "), + N_("git cat-file (-t | -s) [--allow-unknown-type] "), + N_("git cat-file (--textconv | --filters)\n" + " [: | --path= ]"), + N_("git cat-file (--batch | --batch-check | --batch-command) [--batch-all-objects]\n" + " [--buffer] [--follow-symlinks] [--unordered]\n" + " [--textconv | --filters] [-Z]"), + NULL + }; + const struct option options[] = { + /* Simple queries */ + OPT_GROUP(N_("Check object existence or emit object contents")), + OPT_CMDMODE('e', NULL, &opt, + N_("check if exists"), 'e'), + OPT_CMDMODE('p', NULL, &opt, N_("pretty-print content"), 'p'), + + OPT_GROUP(N_("Emit [broken] object attributes")), + OPT_CMDMODE('t', NULL, &opt, N_("show object type (one of 'blob', 'tree', 'commit', 'tag', ...)"), 't'), + OPT_CMDMODE('s', NULL, &opt, N_("show object size"), 's'), + OPT_BOOL(0, "allow-unknown-type", &unknown_type, + N_("allow -s and -t to work with broken/corrupt objects")), + OPT_BOOL(0, "use-mailmap", &use_mailmap, N_("use mail map file")), + OPT_ALIAS(0, "mailmap", "use-mailmap"), + /* Batch mode */ + OPT_GROUP(N_("Batch objects requested on stdin (or --batch-all-objects)")), + OPT_CALLBACK_F(0, "batch", &batch, N_("format"), + N_("show full or contents"), + PARSE_OPT_OPTARG | PARSE_OPT_NONEG, + batch_option_callback), + OPT_CALLBACK_F(0, "batch-check", &batch, N_("format"), + N_("like --batch, but don't emit "), + PARSE_OPT_OPTARG | PARSE_OPT_NONEG, + batch_option_callback), + OPT_BOOL_F('z', NULL, &input_nul_terminated, N_("stdin is NUL-terminated"), + PARSE_OPT_HIDDEN), + OPT_BOOL('Z', NULL, &nul_terminated, N_("stdin and stdout is NUL-terminated")), + OPT_CALLBACK_F(0, "batch-command", &batch, N_("format"), + N_("read commands from stdin"), + PARSE_OPT_OPTARG | PARSE_OPT_NONEG, + batch_option_callback), + OPT_CMDMODE(0, "batch-all-objects", &opt, + N_("with --batch[-check]: ignores stdin, batches all known objects"), 'b'), + /* Batch-specific options */ + OPT_GROUP(N_("Change or optimize batch output")), + OPT_BOOL(0, "buffer", &batch.buffer_output, N_("buffer --batch output")), + OPT_BOOL(0, "follow-symlinks", &batch.follow_symlinks, + N_("follow in-tree symlinks")), + OPT_BOOL(0, "unordered", &batch.unordered, + N_("do not order objects before emitting them")), + /* Textconv options, stand-ole*/ + OPT_GROUP(N_("Emit object (blob or tree) with conversion or filter (stand-alone, or with batch)")), + OPT_CMDMODE(0, "textconv", &opt, + N_("run textconv on object's content"), 'c'), + OPT_CMDMODE(0, "filters", &opt, + N_("run filters on object's content"), 'w'), + OPT_STRING(0, "path", &force_path, N_("blob|tree"), + N_("use a for (--textconv | --filters); Not with 'batch'")), + OPT_END() + }; + + git_config(git_cat_file_config, NULL); + + batch.buffer_output = -1; + + argc = parse_options(argc, argv, prefix, options, usage, 0); + opt_cw = (opt == 'c' || opt == 'w'); + opt_epts = (opt == 'e' || opt == 'p' || opt == 't' || opt == 's'); + + if (use_mailmap) + read_mailmap(&mailmap); + + /* --batch-all-objects? */ + if (opt == 'b') + batch.all_objects = 1; + + /* Option compatibility */ + if (force_path && !opt_cw) + usage_msg_optf(_("'%s=<%s>' needs '%s' or '%s'"), + usage, options, + "--path", _("path|tree-ish"), "--filters", + "--textconv"); + + /* Option compatibility with batch mode */ + if (batch.enabled) + ; + else if (batch.follow_symlinks) + usage_msg_optf(_("'%s' requires a batch mode"), usage, options, + "--follow-symlinks"); + else if (batch.buffer_output >= 0) + usage_msg_optf(_("'%s' requires a batch mode"), usage, options, + "--buffer"); + else if (batch.all_objects) + usage_msg_optf(_("'%s' requires a batch mode"), usage, options, + "--batch-all-objects"); + else if (input_nul_terminated) + usage_msg_optf(_("'%s' requires a batch mode"), usage, options, + "-z"); + else if (nul_terminated) + usage_msg_optf(_("'%s' requires a batch mode"), usage, options, + "-Z"); + + batch.input_delim = batch.output_delim = '\n'; + if (input_nul_terminated) + batch.input_delim = '\0'; + if (nul_terminated) + batch.input_delim = batch.output_delim = '\0'; + + /* Batch defaults */ + if (batch.buffer_output < 0) + batch.buffer_output = batch.all_objects; + + /* Return early if we're in batch mode? */ + if (batch.enabled) { + if (opt_cw) + batch.transform_mode = opt; + else if (opt && opt != 'b') + usage_msg_optf(_("'-%c' is incompatible with batch mode"), + usage, options, opt); + else if (argc) + usage_msg_opt(_("batch modes take no arguments"), usage, + options); + + return batch_objects(&batch); + } + + if (opt) { + if (!argc && opt == 'c') + usage_msg_optf(_(" required with '%s'"), + usage, options, "--textconv"); + else if (!argc && opt == 'w') + usage_msg_optf(_(" required with '%s'"), + usage, options, "--filters"); + else if (!argc && opt_epts) + usage_msg_optf(_(" required with '-%c'"), + usage, options, opt); + else if (argc == 1) + obj_name = argv[0]; + else + usage_msg_opt(_("too many arguments"), usage, options); + } else if (!argc) { + usage_with_options(usage, options); + } else if (argc != 2) { + usage_msg_optf(_("only two arguments allowed in mode, not %d"), + usage, options, argc); + } else if (argc) { + exp_type = argv[0]; + obj_name = argv[1]; + } + + if (unknown_type && opt != 't' && opt != 's') + die("git cat-file --allow-unknown-type: use with -s or -t"); + return cat_one_file(opt, exp_type, obj_name, unknown_type); +} diff --git a/builtin/check-attr.c b/builtin/check-attr.c new file mode 100644 index 0000000..c1da1d1 --- /dev/null +++ b/builtin/check-attr.c @@ -0,0 +1,205 @@ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "config.h" +#include "attr.h" +#include "environment.h" +#include "gettext.h" +#include "object-name.h" +#include "quote.h" +#include "repository.h" +#include "setup.h" +#include "parse-options.h" +#include "write-or-die.h" + +static int all_attrs; +static int cached_attrs; +static int stdin_paths; +static char *source; +static const char * const check_attr_usage[] = { +N_("git check-attr [--source ] [-a | --all | ...] [--] ..."), +N_("git check-attr --stdin [-z] [--source ] [-a | --all | ...]"), +NULL +}; + +static int nul_term_line; + +static const struct option check_attr_options[] = { + OPT_BOOL('a', "all", &all_attrs, N_("report all attributes set on file")), + OPT_BOOL(0, "cached", &cached_attrs, N_("use .gitattributes only from the index")), + OPT_BOOL(0 , "stdin", &stdin_paths, N_("read file names from stdin")), + OPT_BOOL('z', NULL, &nul_term_line, + N_("terminate input and output records by a NUL character")), + OPT_STRING(0, "source", &source, N_(""), N_("which tree-ish to check attributes at")), + OPT_END() +}; + +static void output_attr(struct attr_check *check, const char *file) +{ + int j; + int cnt = check->nr; + + for (j = 0; j < cnt; j++) { + const char *value = check->items[j].value; + + if (ATTR_TRUE(value)) + value = "set"; + else if (ATTR_FALSE(value)) + value = "unset"; + else if (ATTR_UNSET(value)) + value = "unspecified"; + + if (nul_term_line) { + printf("%s%c" /* path */ + "%s%c" /* attrname */ + "%s%c" /* attrvalue */, + file, 0, + git_attr_name(check->items[j].attr), 0, value, 0); + } else { + quote_c_style(file, NULL, stdout, 0); + printf(": %s: %s\n", + git_attr_name(check->items[j].attr), value); + } + } +} + +static void check_attr(const char *prefix, struct attr_check *check, + int collect_all, + const char *file) + +{ + char *full_path = + prefix_path(prefix, prefix ? strlen(prefix) : 0, file); + + if (collect_all) { + git_all_attrs(&the_index, full_path, check); + } else { + git_check_attr(&the_index, full_path, check); + } + output_attr(check, file); + + free(full_path); +} + +static void check_attr_stdin_paths(const char *prefix, struct attr_check *check, + int collect_all) +{ + struct strbuf buf = STRBUF_INIT; + struct strbuf unquoted = STRBUF_INIT; + strbuf_getline_fn getline_fn; + + getline_fn = nul_term_line ? strbuf_getline_nul : strbuf_getline_lf; + while (getline_fn(&buf, stdin) != EOF) { + if (!nul_term_line && buf.buf[0] == '"') { + strbuf_reset(&unquoted); + if (unquote_c_style(&unquoted, buf.buf, NULL)) + die("line is badly quoted"); + strbuf_swap(&buf, &unquoted); + } + check_attr(prefix, check, collect_all, buf.buf); + maybe_flush_or_die(stdout, "attribute to stdout"); + } + strbuf_release(&buf); + strbuf_release(&unquoted); +} + +static NORETURN void error_with_usage(const char *msg) +{ + error("%s", msg); + usage_with_options(check_attr_usage, check_attr_options); +} + +int cmd_check_attr(int argc, const char **argv, const char *prefix) +{ + struct attr_check *check; + struct object_id initialized_oid; + int cnt, i, doubledash, filei; + + if (!is_bare_repository()) + setup_work_tree(); + + git_config(git_default_config, NULL); + + argc = parse_options(argc, argv, prefix, check_attr_options, + check_attr_usage, PARSE_OPT_KEEP_DASHDASH); + + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + if (repo_read_index(the_repository) < 0) { + die("invalid cache"); + } + + if (cached_attrs) + git_attr_set_direction(GIT_ATTR_INDEX); + + doubledash = -1; + for (i = 0; doubledash < 0 && i < argc; i++) { + if (!strcmp(argv[i], "--")) + doubledash = i; + } + + /* Process --all and/or attribute arguments: */ + if (all_attrs) { + if (doubledash >= 1) + error_with_usage("Attributes and --all both specified"); + + cnt = 0; + filei = doubledash + 1; + } else if (doubledash == 0) { + error_with_usage("No attribute specified"); + } else if (doubledash < 0) { + if (!argc) + error_with_usage("No attribute specified"); + + if (stdin_paths) { + /* Treat all arguments as attribute names. */ + cnt = argc; + filei = argc; + } else { + /* Treat exactly one argument as an attribute name. */ + cnt = 1; + filei = 1; + } + } else { + cnt = doubledash; + filei = doubledash + 1; + } + + /* Check file argument(s): */ + if (stdin_paths) { + if (filei < argc) + error_with_usage("Can't specify files with --stdin"); + } else { + if (filei >= argc) + error_with_usage("No file specified"); + } + + check = attr_check_alloc(); + if (!all_attrs) { + for (i = 0; i < cnt; i++) { + const struct git_attr *a = git_attr(argv[i]); + + if (!a) + return error("%s: not a valid attribute name", + argv[i]); + attr_check_append(check, a); + } + } + + if (source) { + if (repo_get_oid_tree(the_repository, source, &initialized_oid)) + die("%s: not a valid tree-ish source", source); + set_git_attr_source(source); + } + + if (stdin_paths) + check_attr_stdin_paths(prefix, check, all_attrs); + else { + for (i = filei; i < argc; i++) + check_attr(prefix, check, all_attrs, argv[i]); + maybe_flush_or_die(stdout, "attribute to stdout"); + } + + attr_check_free(check); + return 0; +} diff --git a/builtin/check-ignore.c b/builtin/check-ignore.c new file mode 100644 index 0000000..906cd96 --- /dev/null +++ b/builtin/check-ignore.c @@ -0,0 +1,199 @@ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "config.h" +#include "dir.h" +#include "gettext.h" +#include "quote.h" +#include "pathspec.h" +#include "parse-options.h" +#include "repository.h" +#include "submodule.h" +#include "write-or-die.h" + +static int quiet, verbose, stdin_paths, show_non_matching, no_index; +static const char * const check_ignore_usage[] = { +"git check-ignore [] ...", +"git check-ignore [] --stdin", +NULL +}; + +static int nul_term_line; + +static const struct option check_ignore_options[] = { + OPT__QUIET(&quiet, N_("suppress progress reporting")), + OPT__VERBOSE(&verbose, N_("be verbose")), + OPT_GROUP(""), + OPT_BOOL(0, "stdin", &stdin_paths, + N_("read file names from stdin")), + OPT_BOOL('z', NULL, &nul_term_line, + N_("terminate input and output records by a NUL character")), + OPT_BOOL('n', "non-matching", &show_non_matching, + N_("show non-matching input paths")), + OPT_BOOL(0, "no-index", &no_index, + N_("ignore index when checking")), + OPT_END() +}; + +static void output_pattern(const char *path, struct path_pattern *pattern) +{ + char *bang = (pattern && pattern->flags & PATTERN_FLAG_NEGATIVE) ? "!" : ""; + char *slash = (pattern && pattern->flags & PATTERN_FLAG_MUSTBEDIR) ? "/" : ""; + if (!nul_term_line) { + if (!verbose) { + write_name_quoted(path, stdout, '\n'); + } else { + if (pattern) { + quote_c_style(pattern->pl->src, NULL, stdout, 0); + printf(":%d:%s%s%s\t", + pattern->srcpos, + bang, pattern->pattern, slash); + } + else { + printf("::\t"); + } + quote_c_style(path, NULL, stdout, 0); + fputc('\n', stdout); + } + } else { + if (!verbose) { + printf("%s%c", path, '\0'); + } else { + if (pattern) + printf("%s%c%d%c%s%s%s%c%s%c", + pattern->pl->src, '\0', + pattern->srcpos, '\0', + bang, pattern->pattern, slash, '\0', + path, '\0'); + else + printf("%c%c%c%s%c", '\0', '\0', '\0', path, '\0'); + } + } +} + +static int check_ignore(struct dir_struct *dir, + const char *prefix, int argc, const char **argv) +{ + const char *full_path; + char *seen; + int num_ignored = 0, i; + struct path_pattern *pattern; + struct pathspec pathspec; + + if (!argc) { + if (!quiet) + fprintf(stderr, "no pathspec given.\n"); + return 0; + } + + /* + * check-ignore just needs paths. Magic beyond :/ is really + * irrelevant. + */ + parse_pathspec(&pathspec, + PATHSPEC_ALL_MAGIC & ~PATHSPEC_FROMTOP, + PATHSPEC_SYMLINK_LEADING_PATH | + PATHSPEC_KEEP_ORDER, + prefix, argv); + + die_path_inside_submodule(&the_index, &pathspec); + + /* + * look for pathspecs matching entries in the index, since these + * should not be ignored, in order to be consistent with + * 'git status', 'git add' etc. + */ + seen = find_pathspecs_matching_against_index(&pathspec, &the_index, + PS_HEED_SKIP_WORKTREE); + for (i = 0; i < pathspec.nr; i++) { + full_path = pathspec.items[i].match; + pattern = NULL; + if (!seen[i]) { + int dtype = DT_UNKNOWN; + pattern = last_matching_pattern(dir, &the_index, + full_path, &dtype); + if (!verbose && pattern && + pattern->flags & PATTERN_FLAG_NEGATIVE) + pattern = NULL; + } + if (!quiet && (pattern || show_non_matching)) + output_pattern(pathspec.items[i].original, pattern); + if (pattern) + num_ignored++; + } + free(seen); + clear_pathspec(&pathspec); + + return num_ignored; +} + +static int check_ignore_stdin_paths(struct dir_struct *dir, const char *prefix) +{ + struct strbuf buf = STRBUF_INIT; + struct strbuf unquoted = STRBUF_INIT; + char *pathspec[2] = { NULL, NULL }; + strbuf_getline_fn getline_fn; + int num_ignored = 0; + + getline_fn = nul_term_line ? strbuf_getline_nul : strbuf_getline_lf; + while (getline_fn(&buf, stdin) != EOF) { + if (!nul_term_line && buf.buf[0] == '"') { + strbuf_reset(&unquoted); + if (unquote_c_style(&unquoted, buf.buf, NULL)) + die("line is badly quoted"); + strbuf_swap(&buf, &unquoted); + } + pathspec[0] = buf.buf; + num_ignored += check_ignore(dir, prefix, + 1, (const char **)pathspec); + maybe_flush_or_die(stdout, "check-ignore to stdout"); + } + strbuf_release(&buf); + strbuf_release(&unquoted); + return num_ignored; +} + +int cmd_check_ignore(int argc, const char **argv, const char *prefix) +{ + int num_ignored; + struct dir_struct dir = DIR_INIT; + + git_config(git_default_config, NULL); + + argc = parse_options(argc, argv, prefix, check_ignore_options, + check_ignore_usage, 0); + + if (stdin_paths) { + if (argc > 0) + die(_("cannot specify pathnames with --stdin")); + } else { + if (nul_term_line) + die(_("-z only makes sense with --stdin")); + if (argc == 0) + die(_("no path specified")); + } + if (quiet) { + if (argc > 1) + die(_("--quiet is only valid with a single pathname")); + if (verbose) + die(_("cannot have both --quiet and --verbose")); + } + if (show_non_matching && !verbose) + die(_("--non-matching is only valid with --verbose")); + + /* read_cache() is only necessary so we can watch out for submodules. */ + if (!no_index && repo_read_index(the_repository) < 0) + die(_("index file corrupt")); + + setup_standard_excludes(&dir); + + if (stdin_paths) { + num_ignored = check_ignore_stdin_paths(&dir, prefix); + } else { + num_ignored = check_ignore(&dir, prefix, argc, argv); + maybe_flush_or_die(stdout, "ignore to stdout"); + } + + dir_clear(&dir); + + return !num_ignored; +} diff --git a/builtin/check-mailmap.c b/builtin/check-mailmap.c new file mode 100644 index 0000000..b8a05b8 --- /dev/null +++ b/builtin/check-mailmap.c @@ -0,0 +1,71 @@ +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "ident.h" +#include "mailmap.h" +#include "parse-options.h" +#include "strbuf.h" +#include "string-list.h" +#include "write-or-die.h" + +static int use_stdin; +static const char * const check_mailmap_usage[] = { +N_("git check-mailmap [] ..."), +NULL +}; + +static const struct option check_mailmap_options[] = { + OPT_BOOL(0, "stdin", &use_stdin, N_("also read contacts from stdin")), + OPT_END() +}; + +static void check_mailmap(struct string_list *mailmap, const char *contact) +{ + const char *name, *mail; + size_t namelen, maillen; + struct ident_split ident; + + if (split_ident_line(&ident, contact, strlen(contact))) + die(_("unable to parse contact: %s"), contact); + + name = ident.name_begin; + namelen = ident.name_end - ident.name_begin; + mail = ident.mail_begin; + maillen = ident.mail_end - ident.mail_begin; + + map_user(mailmap, &mail, &maillen, &name, &namelen); + + if (namelen) + printf("%.*s ", (int)namelen, name); + printf("<%.*s>\n", (int)maillen, mail); +} + +int cmd_check_mailmap(int argc, const char **argv, const char *prefix) +{ + int i; + struct string_list mailmap = STRING_LIST_INIT_NODUP; + + git_config(git_default_config, NULL); + argc = parse_options(argc, argv, prefix, check_mailmap_options, + check_mailmap_usage, 0); + if (argc == 0 && !use_stdin) + die(_("no contacts specified")); + + read_mailmap(&mailmap); + + for (i = 0; i < argc; ++i) + check_mailmap(&mailmap, argv[i]); + maybe_flush_or_die(stdout, "stdout"); + + if (use_stdin) { + struct strbuf buf = STRBUF_INIT; + while (strbuf_getline_lf(&buf, stdin) != EOF) { + check_mailmap(&mailmap, buf.buf); + maybe_flush_or_die(stdout, "stdout"); + } + strbuf_release(&buf); + } + + clear_mailmap(&mailmap); + return 0; +} diff --git a/builtin/check-ref-format.c b/builtin/check-ref-format.c new file mode 100644 index 0000000..5eb6bdc --- /dev/null +++ b/builtin/check-ref-format.c @@ -0,0 +1,98 @@ +/* + * GIT - The information manager from hell + */ + +#include "builtin.h" +#include "refs.h" +#include "setup.h" +#include "strbuf.h" + +static const char builtin_check_ref_format_usage[] = +"git check-ref-format [--normalize] [] \n" +" or: git check-ref-format --branch "; + +/* + * Return a copy of refname but with leading slashes removed and runs + * of adjacent slashes replaced with single slashes. + * + * This function is similar to normalize_path_copy(), but stripped down + * to meet check_ref_format's simpler needs. + */ +static char *collapse_slashes(const char *refname) +{ + char *ret = xmallocz(strlen(refname)); + char ch; + char prev = '/'; + char *cp = ret; + + while ((ch = *refname++) != '\0') { + if (prev == '/' && ch == prev) + continue; + + *cp++ = ch; + prev = ch; + } + *cp = '\0'; + return ret; +} + +static int check_ref_format_branch(const char *arg) +{ + struct strbuf sb = STRBUF_INIT; + const char *name; + int nongit; + + setup_git_directory_gently(&nongit); + if (strbuf_check_branch_ref(&sb, arg) || + !skip_prefix(sb.buf, "refs/heads/", &name)) + die("'%s' is not a valid branch name", arg); + printf("%s\n", name); + strbuf_release(&sb); + return 0; +} + +int cmd_check_ref_format(int argc, const char **argv, const char *prefix) +{ + int i; + int normalize = 0; + int flags = 0; + const char *refname; + char *to_free = NULL; + int ret = 1; + + BUG_ON_NON_EMPTY_PREFIX(prefix); + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage(builtin_check_ref_format_usage); + + if (argc == 3 && !strcmp(argv[1], "--branch")) + return check_ref_format_branch(argv[2]); + + for (i = 1; i < argc && argv[i][0] == '-'; i++) { + if (!strcmp(argv[i], "--normalize") || !strcmp(argv[i], "--print")) + normalize = 1; + else if (!strcmp(argv[i], "--allow-onelevel")) + flags |= REFNAME_ALLOW_ONELEVEL; + else if (!strcmp(argv[i], "--no-allow-onelevel")) + flags &= ~REFNAME_ALLOW_ONELEVEL; + else if (!strcmp(argv[i], "--refspec-pattern")) + flags |= REFNAME_REFSPEC_PATTERN; + else + usage(builtin_check_ref_format_usage); + } + if (! (i == argc - 1)) + usage(builtin_check_ref_format_usage); + + refname = argv[i]; + if (normalize) + refname = to_free = collapse_slashes(refname); + if (check_refname_format(refname, flags)) + goto cleanup; + if (normalize) + printf("%s\n", refname); + + ret = 0; +cleanup: + free(to_free); + return ret; +} diff --git a/builtin/checkout--worker.c b/builtin/checkout--worker.c new file mode 100644 index 0000000..6b62b53 --- /dev/null +++ b/builtin/checkout--worker.c @@ -0,0 +1,147 @@ +#include "builtin.h" +#include "config.h" +#include "entry.h" +#include "gettext.h" +#include "parallel-checkout.h" +#include "parse-options.h" +#include "pkt-line.h" +#include "read-cache-ll.h" + +static void packet_to_pc_item(const char *buffer, int len, + struct parallel_checkout_item *pc_item) +{ + const struct pc_item_fixed_portion *fixed_portion; + const char *variant; + char *encoding; + + if (len < sizeof(struct pc_item_fixed_portion)) + BUG("checkout worker received too short item (got %dB, exp %dB)", + len, (int)sizeof(struct pc_item_fixed_portion)); + + fixed_portion = (struct pc_item_fixed_portion *)buffer; + + if (len - sizeof(struct pc_item_fixed_portion) != + fixed_portion->name_len + fixed_portion->working_tree_encoding_len) + BUG("checkout worker received corrupted item"); + + variant = buffer + sizeof(struct pc_item_fixed_portion); + + /* + * Note: the main process uses zero length to communicate that the + * encoding is NULL. There is no use case that requires sending an + * actual empty string, since convert_attrs() never sets + * ca.working_tree_enconding to "". + */ + if (fixed_portion->working_tree_encoding_len) { + encoding = xmemdupz(variant, + fixed_portion->working_tree_encoding_len); + variant += fixed_portion->working_tree_encoding_len; + } else { + encoding = NULL; + } + + memset(pc_item, 0, sizeof(*pc_item)); + pc_item->ce = make_empty_transient_cache_entry(fixed_portion->name_len, NULL); + pc_item->ce->ce_namelen = fixed_portion->name_len; + pc_item->ce->ce_mode = fixed_portion->ce_mode; + memcpy(pc_item->ce->name, variant, pc_item->ce->ce_namelen); + oidcpy(&pc_item->ce->oid, &fixed_portion->oid); + + pc_item->id = fixed_portion->id; + pc_item->ca.crlf_action = fixed_portion->crlf_action; + pc_item->ca.ident = fixed_portion->ident; + pc_item->ca.working_tree_encoding = encoding; +} + +static void report_result(struct parallel_checkout_item *pc_item) +{ + struct pc_item_result res = { 0 }; + size_t size; + + res.id = pc_item->id; + res.status = pc_item->status; + + if (pc_item->status == PC_ITEM_WRITTEN) { + res.st = pc_item->st; + size = sizeof(res); + } else { + size = PC_ITEM_RESULT_BASE_SIZE; + } + + packet_write(1, (const char *)&res, size); +} + +/* Free the worker-side malloced data, but not pc_item itself. */ +static void release_pc_item_data(struct parallel_checkout_item *pc_item) +{ + free((char *)pc_item->ca.working_tree_encoding); + discard_cache_entry(pc_item->ce); +} + +static void worker_loop(struct checkout *state) +{ + struct parallel_checkout_item *items = NULL; + size_t i, nr = 0, alloc = 0; + + while (1) { + int len = packet_read(0, packet_buffer, sizeof(packet_buffer), + 0); + + if (len < 0) + BUG("packet_read() returned negative value"); + else if (!len) + break; + + ALLOC_GROW(items, nr + 1, alloc); + packet_to_pc_item(packet_buffer, len, &items[nr++]); + } + + for (i = 0; i < nr; i++) { + struct parallel_checkout_item *pc_item = &items[i]; + write_pc_item(pc_item, state); + report_result(pc_item); + release_pc_item_data(pc_item); + } + + packet_flush(1); + + free(items); +} + +static const char * const checkout_worker_usage[] = { + N_("git checkout--worker []"), + NULL +}; + +int cmd_checkout__worker(int argc, const char **argv, const char *prefix) +{ + struct checkout state = CHECKOUT_INIT; + struct option checkout_worker_options[] = { + OPT_STRING(0, "prefix", &state.base_dir, N_("string"), + N_("when creating files, prepend ")), + OPT_END() + }; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(checkout_worker_usage, + checkout_worker_options); + + git_config(git_default_config, NULL); + argc = parse_options(argc, argv, prefix, checkout_worker_options, + checkout_worker_usage, 0); + if (argc > 0) + usage_with_options(checkout_worker_usage, checkout_worker_options); + + if (state.base_dir) + state.base_dir_len = strlen(state.base_dir); + + /* + * Setting this on a worker won't actually update the index. We just + * need to tell the checkout machinery to lstat() the written entries, + * so that we can send this data back to the main process. + */ + state.refresh_cache = 1; + + worker_loop(&state); + return 0; +} diff --git a/builtin/checkout-index.c b/builtin/checkout-index.c new file mode 100644 index 0000000..3b68b47 --- /dev/null +++ b/builtin/checkout-index.c @@ -0,0 +1,346 @@ +/* + * Check-out files from the "current cache directory" + * + * Copyright (C) 2005 Linus Torvalds + * + */ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "config.h" +#include "dir.h" +#include "gettext.h" +#include "lockfile.h" +#include "quote.h" +#include "repository.h" +#include "cache-tree.h" +#include "parse-options.h" +#include "entry.h" +#include "parallel-checkout.h" +#include "read-cache-ll.h" +#include "setup.h" +#include "sparse-index.h" + +#define CHECKOUT_ALL 4 +static int nul_term_line; +static int checkout_stage; /* default to checkout stage0 */ +static int ignore_skip_worktree; /* default to 0 */ +static int to_tempfile = -1; +static char topath[4][TEMPORARY_FILENAME_LENGTH + 1]; + +static struct checkout state = CHECKOUT_INIT; + +static void write_tempfile_record(const char *name, const char *prefix) +{ + int i; + int have_tempname = 0; + + if (CHECKOUT_ALL == checkout_stage) { + for (i = 1; i < 4; i++) + if (topath[i][0]) { + have_tempname = 1; + break; + } + + if (have_tempname) { + for (i = 1; i < 4; i++) { + if (i > 1) + putchar(' '); + if (topath[i][0]) + fputs(topath[i], stdout); + else + putchar('.'); + } + } + } else if (topath[checkout_stage][0]) { + have_tempname = 1; + fputs(topath[checkout_stage], stdout); + } + + if (have_tempname) { + putchar('\t'); + write_name_quoted_relative(name, prefix, stdout, + nul_term_line ? '\0' : '\n'); + } + + for (i = 0; i < 4; i++) { + topath[i][0] = 0; + } +} + +static int checkout_file(const char *name, const char *prefix) +{ + int namelen = strlen(name); + int pos = index_name_pos(&the_index, name, namelen); + int has_same_name = 0; + int is_file = 0; + int is_skipped = 1; + int did_checkout = 0; + int errs = 0; + + if (pos < 0) + pos = -pos - 1; + + while (pos < the_index.cache_nr) { + struct cache_entry *ce = the_index.cache[pos]; + if (ce_namelen(ce) != namelen || + memcmp(ce->name, name, namelen)) + break; + has_same_name = 1; + pos++; + if (S_ISSPARSEDIR(ce->ce_mode)) + break; + is_file = 1; + if (!ignore_skip_worktree && ce_skip_worktree(ce)) + break; + is_skipped = 0; + if (ce_stage(ce) != checkout_stage + && (CHECKOUT_ALL != checkout_stage || !ce_stage(ce))) + continue; + did_checkout = 1; + if (checkout_entry(ce, &state, + to_tempfile ? topath[ce_stage(ce)] : NULL, + NULL) < 0) + errs++; + } + + if (did_checkout) { + if (to_tempfile) + write_tempfile_record(name, prefix); + return errs > 0 ? -1 : 0; + } + + /* + * At this point we know we didn't try to check anything out. If it was + * because we did find an entry but it was stage 0, that's not an + * error. + */ + if (has_same_name && checkout_stage == CHECKOUT_ALL) + return 0; + + if (!state.quiet) { + fprintf(stderr, "git checkout-index: %s ", name); + if (!has_same_name) + fprintf(stderr, "is not in the cache"); + else if (!is_file) + fprintf(stderr, "is a sparse directory"); + else if (is_skipped) + fprintf(stderr, "has skip-worktree enabled; " + "use '--ignore-skip-worktree-bits' to checkout"); + else if (checkout_stage) + fprintf(stderr, "does not exist at stage %d", + checkout_stage); + else + fprintf(stderr, "is unmerged"); + fputc('\n', stderr); + } + return -1; +} + +static int checkout_all(const char *prefix, int prefix_length) +{ + int i, errs = 0; + struct cache_entry *last_ce = NULL; + + for (i = 0; i < the_index.cache_nr ; i++) { + struct cache_entry *ce = the_index.cache[i]; + + if (S_ISSPARSEDIR(ce->ce_mode)) { + if (!ce_skip_worktree(ce)) + BUG("sparse directory '%s' does not have skip-worktree set", ce->name); + + /* + * If the current entry is a sparse directory and skip-worktree + * entries are being checked out, expand the index and continue + * the loop on the current index position (now pointing to the + * first entry inside the expanded sparse directory). + */ + if (ignore_skip_worktree) { + ensure_full_index(&the_index); + ce = the_index.cache[i]; + } + } + + if (!ignore_skip_worktree && ce_skip_worktree(ce)) + continue; + if (ce_stage(ce) != checkout_stage + && (CHECKOUT_ALL != checkout_stage || !ce_stage(ce))) + continue; + if (prefix && *prefix && + (ce_namelen(ce) <= prefix_length || + memcmp(prefix, ce->name, prefix_length))) + continue; + if (last_ce && to_tempfile) { + if (ce_namelen(last_ce) != ce_namelen(ce) + || memcmp(last_ce->name, ce->name, ce_namelen(ce))) + write_tempfile_record(last_ce->name, prefix); + } + if (checkout_entry(ce, &state, + to_tempfile ? topath[ce_stage(ce)] : NULL, + NULL) < 0) + errs++; + last_ce = ce; + } + if (last_ce && to_tempfile) + write_tempfile_record(last_ce->name, prefix); + return !!errs; +} + +static const char * const builtin_checkout_index_usage[] = { + N_("git checkout-index [] [--] [...]"), + NULL +}; + +static int option_parse_stage(const struct option *opt, + const char *arg, int unset) +{ + int *stage = opt->value; + + BUG_ON_OPT_NEG(unset); + + if (!strcmp(arg, "all")) { + *stage = CHECKOUT_ALL; + } else { + int ch = arg[0]; + if ('1' <= ch && ch <= '3') + *stage = arg[0] - '0'; + else + die(_("stage should be between 1 and 3 or all")); + } + return 0; +} + +int cmd_checkout_index(int argc, const char **argv, const char *prefix) +{ + int i; + struct lock_file lock_file = LOCK_INIT; + int all = 0; + int read_from_stdin = 0; + int prefix_length; + int force = 0, quiet = 0, not_new = 0; + int index_opt = 0; + int err = 0; + int pc_workers, pc_threshold; + struct option builtin_checkout_index_options[] = { + OPT_BOOL('a', "all", &all, + N_("check out all files in the index")), + OPT_BOOL(0, "ignore-skip-worktree-bits", &ignore_skip_worktree, + N_("do not skip files with skip-worktree set")), + OPT__FORCE(&force, N_("force overwrite of existing files"), 0), + OPT__QUIET(&quiet, + N_("no warning for existing files and files not in index")), + OPT_BOOL('n', "no-create", ¬_new, + N_("don't checkout new files")), + OPT_BOOL('u', "index", &index_opt, + N_("update stat information in the index file")), + OPT_BOOL('z', NULL, &nul_term_line, + N_("paths are separated with NUL character")), + OPT_BOOL(0, "stdin", &read_from_stdin, + N_("read list of paths from the standard input")), + OPT_BOOL(0, "temp", &to_tempfile, + N_("write the content to temporary files")), + OPT_STRING(0, "prefix", &state.base_dir, N_("string"), + N_("when creating files, prepend ")), + OPT_CALLBACK_F(0, "stage", &checkout_stage, "(1|2|3|all)", + N_("copy out the files from named stage"), + PARSE_OPT_NONEG, option_parse_stage), + OPT_END() + }; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(builtin_checkout_index_usage, + builtin_checkout_index_options); + git_config(git_default_config, NULL); + prefix_length = prefix ? strlen(prefix) : 0; + + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + if (repo_read_index(the_repository) < 0) { + die("invalid cache"); + } + + argc = parse_options(argc, argv, prefix, builtin_checkout_index_options, + builtin_checkout_index_usage, 0); + state.istate = &the_index; + state.force = force; + state.quiet = quiet; + state.not_new = not_new; + + if (!state.base_dir) + state.base_dir = ""; + state.base_dir_len = strlen(state.base_dir); + + if (to_tempfile < 0) + to_tempfile = (checkout_stage == CHECKOUT_ALL); + if (!to_tempfile && checkout_stage == CHECKOUT_ALL) + die(_("options '%s' and '%s' cannot be used together"), + "--stage=all", "--no-temp"); + + /* + * when --prefix is specified we do not want to update cache. + */ + if (index_opt && !state.base_dir_len && !to_tempfile) { + state.refresh_cache = 1; + state.istate = &the_index; + repo_hold_locked_index(the_repository, &lock_file, + LOCK_DIE_ON_ERROR); + } + + get_parallel_checkout_configs(&pc_workers, &pc_threshold); + if (pc_workers > 1) + init_parallel_checkout(); + + /* Check out named files first */ + for (i = 0; i < argc; i++) { + const char *arg = argv[i]; + char *p; + + if (all) + die("git checkout-index: don't mix '--all' and explicit filenames"); + if (read_from_stdin) + die("git checkout-index: don't mix '--stdin' and explicit filenames"); + p = prefix_path(prefix, prefix_length, arg); + err |= checkout_file(p, prefix); + free(p); + } + + if (read_from_stdin) { + struct strbuf buf = STRBUF_INIT; + struct strbuf unquoted = STRBUF_INIT; + strbuf_getline_fn getline_fn; + + if (all) + die("git checkout-index: don't mix '--all' and '--stdin'"); + + getline_fn = nul_term_line ? strbuf_getline_nul : strbuf_getline_lf; + while (getline_fn(&buf, stdin) != EOF) { + char *p; + if (!nul_term_line && buf.buf[0] == '"') { + strbuf_reset(&unquoted); + if (unquote_c_style(&unquoted, buf.buf, NULL)) + die("line is badly quoted"); + strbuf_swap(&buf, &unquoted); + } + p = prefix_path(prefix, prefix_length, buf.buf); + err |= checkout_file(p, prefix); + free(p); + } + strbuf_release(&unquoted); + strbuf_release(&buf); + } + + if (all) + err |= checkout_all(prefix, prefix_length); + + if (pc_workers > 1) + err |= run_parallel_checkout(&state, pc_workers, pc_threshold, + NULL, NULL); + + if (err) + return 1; + + if (is_lock_file_locked(&lock_file) && + write_locked_index(&the_index, &lock_file, COMMIT_LOCK)) + die("Unable to write new index file"); + return 0; +} diff --git a/builtin/checkout.c b/builtin/checkout.c new file mode 100644 index 0000000..f02434b --- /dev/null +++ b/builtin/checkout.c @@ -0,0 +1,2007 @@ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "advice.h" +#include "blob.h" +#include "branch.h" +#include "cache-tree.h" +#include "checkout.h" +#include "commit.h" +#include "config.h" +#include "diff.h" +#include "dir.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "hook.h" +#include "merge-ll.h" +#include "lockfile.h" +#include "mem-pool.h" +#include "merge-recursive.h" +#include "object-name.h" +#include "object-store-ll.h" +#include "parse-options.h" +#include "path.h" +#include "preload-index.h" +#include "read-cache.h" +#include "refs.h" +#include "remote.h" +#include "resolve-undo.h" +#include "revision.h" +#include "run-command.h" +#include "setup.h" +#include "submodule.h" +#include "submodule-config.h" +#include "symlinks.h" +#include "trace2.h" +#include "tree.h" +#include "tree-walk.h" +#include "unpack-trees.h" +#include "wt-status.h" +#include "xdiff-interface.h" +#include "entry.h" +#include "parallel-checkout.h" +#include "add-interactive.h" + +static const char * const checkout_usage[] = { + N_("git checkout [] "), + N_("git checkout [] [] -- ..."), + NULL, +}; + +static const char * const switch_branch_usage[] = { + N_("git switch [] []"), + NULL, +}; + +static const char * const restore_usage[] = { + N_("git restore [] [--source=] ..."), + NULL, +}; + +struct checkout_opts { + int patch_mode; + int quiet; + int merge; + int force; + int force_detach; + int implicit_detach; + int writeout_stage; + int overwrite_ignore; + int ignore_skipworktree; + int ignore_other_worktrees; + int show_progress; + int count_checkout_paths; + int overlay_mode; + int dwim_new_local_branch; + int discard_changes; + int accept_ref; + int accept_pathspec; + int switch_branch_doing_nothing_is_ok; + int only_merge_on_switching_branches; + int can_switch_when_in_progress; + int orphan_from_empty_tree; + int empty_pathspec_ok; + int checkout_index; + int checkout_worktree; + const char *ignore_unmerged_opt; + int ignore_unmerged; + int pathspec_file_nul; + char *pathspec_from_file; + + const char *new_branch; + const char *new_branch_force; + const char *new_orphan_branch; + int new_branch_log; + enum branch_track track; + struct diff_options diff_options; + char *conflict_style; + + int branch_exists; + const char *prefix; + struct pathspec pathspec; + const char *from_treeish; + struct tree *source_tree; +}; + +struct branch_info { + char *name; /* The short name used */ + char *path; /* The full name of a real branch */ + struct commit *commit; /* The named commit */ + char *refname; /* The full name of the ref being checked out. */ + struct object_id oid; /* The object ID of the commit being checked out. */ + /* + * if not null the branch is detached because it's already + * checked out in this checkout + */ + char *checkout; +}; + +static void branch_info_release(struct branch_info *info) +{ + free(info->name); + free(info->path); + free(info->refname); + free(info->checkout); +} + +static int post_checkout_hook(struct commit *old_commit, struct commit *new_commit, + int changed) +{ + return run_hooks_l("post-checkout", + oid_to_hex(old_commit ? &old_commit->object.oid : null_oid()), + oid_to_hex(new_commit ? &new_commit->object.oid : null_oid()), + changed ? "1" : "0", NULL); + /* "new_commit" can be NULL when checking out from the index before + a commit exists. */ + +} + +static int update_some(const struct object_id *oid, struct strbuf *base, + const char *pathname, unsigned mode, void *context UNUSED) +{ + int len; + struct cache_entry *ce; + int pos; + + if (S_ISDIR(mode)) + return READ_TREE_RECURSIVE; + + len = base->len + strlen(pathname); + ce = make_empty_cache_entry(&the_index, len); + oidcpy(&ce->oid, oid); + memcpy(ce->name, base->buf, base->len); + memcpy(ce->name + base->len, pathname, len - base->len); + ce->ce_flags = create_ce_flags(0) | CE_UPDATE; + ce->ce_namelen = len; + ce->ce_mode = create_ce_mode(mode); + + /* + * If the entry is the same as the current index, we can leave the old + * entry in place. Whether it is UPTODATE or not, checkout_entry will + * do the right thing. + */ + pos = index_name_pos(&the_index, ce->name, ce->ce_namelen); + if (pos >= 0) { + struct cache_entry *old = the_index.cache[pos]; + if (ce->ce_mode == old->ce_mode && + !ce_intent_to_add(old) && + oideq(&ce->oid, &old->oid)) { + old->ce_flags |= CE_UPDATE; + discard_cache_entry(ce); + return 0; + } + } + + add_index_entry(&the_index, ce, + ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE); + return 0; +} + +static int read_tree_some(struct tree *tree, const struct pathspec *pathspec) +{ + read_tree(the_repository, tree, + pathspec, update_some, NULL); + + /* update the index with the given tree's info + * for all args, expanding wildcards, and exit + * with any non-zero return code. + */ + return 0; +} + +static int skip_same_name(const struct cache_entry *ce, int pos) +{ + while (++pos < the_index.cache_nr && + !strcmp(the_index.cache[pos]->name, ce->name)) + ; /* skip */ + return pos; +} + +static int check_stage(int stage, const struct cache_entry *ce, int pos, + int overlay_mode) +{ + while (pos < the_index.cache_nr && + !strcmp(the_index.cache[pos]->name, ce->name)) { + if (ce_stage(the_index.cache[pos]) == stage) + return 0; + pos++; + } + if (!overlay_mode) + return 0; + if (stage == 2) + return error(_("path '%s' does not have our version"), ce->name); + else + return error(_("path '%s' does not have their version"), ce->name); +} + +static int check_stages(unsigned stages, const struct cache_entry *ce, int pos) +{ + unsigned seen = 0; + const char *name = ce->name; + + while (pos < the_index.cache_nr) { + ce = the_index.cache[pos]; + if (strcmp(name, ce->name)) + break; + seen |= (1 << ce_stage(ce)); + pos++; + } + if ((stages & seen) != stages) + return error(_("path '%s' does not have all necessary versions"), + name); + return 0; +} + +static int checkout_stage(int stage, const struct cache_entry *ce, int pos, + const struct checkout *state, int *nr_checkouts, + int overlay_mode) +{ + while (pos < the_index.cache_nr && + !strcmp(the_index.cache[pos]->name, ce->name)) { + if (ce_stage(the_index.cache[pos]) == stage) + return checkout_entry(the_index.cache[pos], state, + NULL, nr_checkouts); + pos++; + } + if (!overlay_mode) { + unlink_entry(ce, NULL); + return 0; + } + if (stage == 2) + return error(_("path '%s' does not have our version"), ce->name); + else + return error(_("path '%s' does not have their version"), ce->name); +} + +static int checkout_merged(int pos, const struct checkout *state, + int *nr_checkouts, struct mem_pool *ce_mem_pool) +{ + struct cache_entry *ce = the_index.cache[pos]; + const char *path = ce->name; + mmfile_t ancestor, ours, theirs; + enum ll_merge_result merge_status; + int status; + struct object_id oid; + mmbuffer_t result_buf; + struct object_id threeway[3]; + unsigned mode = 0; + struct ll_merge_options ll_opts; + int renormalize = 0; + + memset(threeway, 0, sizeof(threeway)); + while (pos < the_index.cache_nr) { + int stage; + stage = ce_stage(ce); + if (!stage || strcmp(path, ce->name)) + break; + oidcpy(&threeway[stage - 1], &ce->oid); + if (stage == 2) + mode = create_ce_mode(ce->ce_mode); + pos++; + ce = the_index.cache[pos]; + } + if (is_null_oid(&threeway[1]) || is_null_oid(&threeway[2])) + return error(_("path '%s' does not have necessary versions"), path); + + read_mmblob(&ancestor, &threeway[0]); + read_mmblob(&ours, &threeway[1]); + read_mmblob(&theirs, &threeway[2]); + + memset(&ll_opts, 0, sizeof(ll_opts)); + git_config_get_bool("merge.renormalize", &renormalize); + ll_opts.renormalize = renormalize; + merge_status = ll_merge(&result_buf, path, &ancestor, "base", + &ours, "ours", &theirs, "theirs", + state->istate, &ll_opts); + free(ancestor.ptr); + free(ours.ptr); + free(theirs.ptr); + if (merge_status == LL_MERGE_BINARY_CONFLICT) + warning("Cannot merge binary files: %s (%s vs. %s)", + path, "ours", "theirs"); + if (merge_status < 0 || !result_buf.ptr) { + free(result_buf.ptr); + return error(_("path '%s': cannot merge"), path); + } + + /* + * NEEDSWORK: + * There is absolutely no reason to write this as a blob object + * and create a phony cache entry. This hack is primarily to get + * to the write_entry() machinery that massages the contents to + * work-tree format and writes out which only allows it for a + * cache entry. The code in write_entry() needs to be refactored + * to allow us to feed a instead of a cache + * entry. Such a refactoring would help merge_recursive as well + * (it also writes the merge result to the object database even + * when it may contain conflicts). + */ + if (write_object_file(result_buf.ptr, result_buf.size, OBJ_BLOB, &oid)) + die(_("Unable to add merge result for '%s'"), path); + free(result_buf.ptr); + ce = make_transient_cache_entry(mode, &oid, path, 2, ce_mem_pool); + if (!ce) + die(_("make_cache_entry failed for path '%s'"), path); + status = checkout_entry(ce, state, NULL, nr_checkouts); + return status; +} + +static void mark_ce_for_checkout_overlay(struct cache_entry *ce, + char *ps_matched, + const struct checkout_opts *opts) +{ + ce->ce_flags &= ~CE_MATCHED; + if (!opts->ignore_skipworktree && ce_skip_worktree(ce)) + return; + if (opts->source_tree && !(ce->ce_flags & CE_UPDATE)) + /* + * "git checkout tree-ish -- path", but this entry + * is in the original index but is not in tree-ish + * or does not match the pathspec; it will not be + * checked out to the working tree. We will not do + * anything to this entry at all. + */ + return; + /* + * Either this entry came from the tree-ish we are + * checking the paths out of, or we are checking out + * of the index. + * + * If it comes from the tree-ish, we already know it + * matches the pathspec and could just stamp + * CE_MATCHED to it from update_some(). But we still + * need ps_matched and read_tree (and + * eventually tree_entry_interesting) cannot fill + * ps_matched yet. Once it can, we can avoid calling + * match_pathspec() for _all_ entries when + * opts->source_tree != NULL. + */ + if (ce_path_match(&the_index, ce, &opts->pathspec, ps_matched)) + ce->ce_flags |= CE_MATCHED; +} + +static void mark_ce_for_checkout_no_overlay(struct cache_entry *ce, + char *ps_matched, + const struct checkout_opts *opts) +{ + ce->ce_flags &= ~CE_MATCHED; + if (!opts->ignore_skipworktree && ce_skip_worktree(ce)) + return; + if (ce_path_match(&the_index, ce, &opts->pathspec, ps_matched)) { + ce->ce_flags |= CE_MATCHED; + if (opts->source_tree && !(ce->ce_flags & CE_UPDATE)) + /* + * In overlay mode, but the path is not in + * tree-ish, which means we should remove it + * from the index and the working tree. + */ + ce->ce_flags |= CE_REMOVE | CE_WT_REMOVE; + } +} + +static int checkout_worktree(const struct checkout_opts *opts, + const struct branch_info *info) +{ + struct checkout state = CHECKOUT_INIT; + int nr_checkouts = 0, nr_unmerged = 0; + int errs = 0; + int pos; + int pc_workers, pc_threshold; + struct mem_pool ce_mem_pool; + + state.force = 1; + state.refresh_cache = 1; + state.istate = &the_index; + + mem_pool_init(&ce_mem_pool, 0); + get_parallel_checkout_configs(&pc_workers, &pc_threshold); + init_checkout_metadata(&state.meta, info->refname, + info->commit ? &info->commit->object.oid : &info->oid, + NULL); + + enable_delayed_checkout(&state); + + if (pc_workers > 1) + init_parallel_checkout(); + + for (pos = 0; pos < the_index.cache_nr; pos++) { + struct cache_entry *ce = the_index.cache[pos]; + if (ce->ce_flags & CE_MATCHED) { + if (!ce_stage(ce)) { + errs |= checkout_entry(ce, &state, + NULL, &nr_checkouts); + continue; + } + if (opts->writeout_stage) + errs |= checkout_stage(opts->writeout_stage, + ce, pos, + &state, + &nr_checkouts, opts->overlay_mode); + else if (opts->merge) + errs |= checkout_merged(pos, &state, + &nr_unmerged, + &ce_mem_pool); + pos = skip_same_name(ce, pos) - 1; + } + } + if (pc_workers > 1) + errs |= run_parallel_checkout(&state, pc_workers, pc_threshold, + NULL, NULL); + mem_pool_discard(&ce_mem_pool, should_validate_cache_entries()); + remove_marked_cache_entries(&the_index, 1); + remove_scheduled_dirs(); + errs |= finish_delayed_checkout(&state, opts->show_progress); + + if (opts->count_checkout_paths) { + if (nr_unmerged) + fprintf_ln(stderr, Q_("Recreated %d merge conflict", + "Recreated %d merge conflicts", + nr_unmerged), + nr_unmerged); + if (opts->source_tree) + fprintf_ln(stderr, Q_("Updated %d path from %s", + "Updated %d paths from %s", + nr_checkouts), + nr_checkouts, + repo_find_unique_abbrev(the_repository, &opts->source_tree->object.oid, + DEFAULT_ABBREV)); + else if (!nr_unmerged || nr_checkouts) + fprintf_ln(stderr, Q_("Updated %d path from the index", + "Updated %d paths from the index", + nr_checkouts), + nr_checkouts); + } + + return errs; +} + +static int checkout_paths(const struct checkout_opts *opts, + const struct branch_info *new_branch_info) +{ + int pos; + static char *ps_matched; + struct object_id rev; + struct commit *head; + int errs = 0; + struct lock_file lock_file = LOCK_INIT; + int checkout_index; + + trace2_cmd_mode(opts->patch_mode ? "patch" : "path"); + + if (opts->track != BRANCH_TRACK_UNSPECIFIED) + die(_("'%s' cannot be used with updating paths"), "--track"); + + if (opts->new_branch_log) + die(_("'%s' cannot be used with updating paths"), "-l"); + + if (opts->ignore_unmerged && opts->patch_mode) + die(_("'%s' cannot be used with updating paths"), + opts->ignore_unmerged_opt); + + if (opts->force_detach) + die(_("'%s' cannot be used with updating paths"), "--detach"); + + if (opts->merge && opts->patch_mode) + die(_("options '%s' and '%s' cannot be used together"), "--merge", "--patch"); + + if (opts->ignore_unmerged && opts->merge) + die(_("options '%s' and '%s' cannot be used together"), + opts->ignore_unmerged_opt, "-m"); + + if (opts->new_branch) + die(_("Cannot update paths and switch to branch '%s' at the same time."), + opts->new_branch); + + if (!opts->checkout_worktree && !opts->checkout_index) + die(_("neither '%s' or '%s' is specified"), + "--staged", "--worktree"); + + if (!opts->checkout_worktree && !opts->from_treeish) + die(_("'%s' must be used when '%s' is not specified"), + "--worktree", "--source"); + + /* + * Reject --staged option to the restore command when combined with + * merge-related options. Use the accept_ref flag to distinguish it + * from the checkout command, which does not accept --staged anyway. + * + * `restore --ours|--theirs --worktree --staged` could mean resolving + * conflicted paths to one side in both the worktree and the index, + * but does not currently. + * + * `restore --merge|--conflict=