diff options
Diffstat (limited to 'builtin')
122 files changed, 84429 insertions, 0 deletions
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 [<options>] [--] <pathspec>..."), + 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 <url> %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 [<options>] [(<mbox> | <Maildir>)...]"), + N_("git am [<options>] (--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 [<options>] [<patch>...]"), + 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 <repo>")), + 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> --term-(old|good)=<term>]" \ + " [--no-checkout] [--first-parent] [<bad> [<good>...]] [--]" \ + " [<pathspec>...]") +#define BUILTIN_GIT_BISECT_STATE_USAGE \ + N_("git bisect (good|bad) [<rev>...]") +#define BUILTIN_GIT_BISECT_TERMS_USAGE \ + "git bisect terms [--term-good | --term-bad]" +#define BUILTIN_GIT_BISECT_SKIP_USAGE \ + N_("git bisect skip [(<rev>|<range>)...]") +#define BUILTIN_GIT_BISECT_NEXT_USAGE \ + "git bisect next" +#define BUILTIN_GIT_BISECT_RESET_USAGE \ + N_("git bisect reset [<commit>]") +#define BUILTIN_GIT_BISECT_VISUALIZE_USAGE \ + "git bisect visualize" +#define BUILTIN_GIT_BISECT_REPLAY_USAGE \ + N_("git bisect replay <logfile>") +#define BUILTIN_GIT_BISECT_LOG_USAGE \ + "git bisect log" +#define BUILTIN_GIT_BISECT_RUN_USAGE \ + N_("git bisect run <cmd> [<arg>...]") + +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 <commit>'."), 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 <sha1> <sha1>", 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 " + "<valid-branch>'."), + 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 [<options>] [<rev-opts>] [<rev>] [--] <file>"); +static char annotate_usage[] = N_("git annotate [<options>] [<rev-opts>] [<rev>] [--] <file>"); + +static const char *blame_opt_usage[] = { + blame_usage, + "", + N_("<rev-opts> are documented in git-rev-list(1)"), + NULL +}; + +static const char *annotate_opt_usage[] = { + annotate_usage, + "", + N_("<rev-opts> 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 <rev> when blaming")), + OPT_STRING_LIST(0, "ignore-revs-file", &ignore_revs_file_list, N_("file"), N_("ignore revisions from <file>")), + 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 <file> instead of calling git-rev-list")), + OPT_STRING(0, "contents", &contents_from, N_("file"), N_("use <file>'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 <start>,<end> or function :<funcname>")), + 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] -- <path>" or + * "blame -- <path> <rev>" + * + * (2) otherwise, it is one of the two: + * "blame [revisions] <path>" + * "blame <path> <rev>" + * + * Note that we must strip out <path> 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: <rev> -- <path> */ + 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 <path> 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 <krh@redhat.com> + * 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 [<options>] [-r | -a] [--merged] [--no-merged]"), + N_("git branch [<options>] [-f] [--recurse-submodules] <branch-name> [<start-point>]"), + N_("git branch [<options>] [-l] [<pattern>...]"), + N_("git branch [<options>] [-r] (-d | -D) <branch-name>..."), + N_("git branch [<options>] (-m | -M) [<old-branch>] <new-branch>"), + N_("git branch [<options>] (-c | -C) [<old-branch>] <new-branch>"), + N_("git branch [<options>] [-r | -a] [--points-at]"), + N_("git branch [<options>] [-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 <pattern>?")); + + 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 : "<unset>"); +} + +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) <path>] [(-s | --suffix) <format>]\n" + " [--diagnose[=<mode>]]"), + 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=<version>] <file> <git-rev-list-args>") +#define BUILTIN_BUNDLE_VERIFY_USAGE \ + N_("git bundle verify [-q | --quiet] <file>") +#define BUILTIN_BUNDLE_LIST_HEADS_USAGE \ + N_("git bundle list-heads <file> [<refname>...]") +#define BUILTIN_BUNDLE_UNBUNDLE_USAGE \ + N_("git bundle unbundle [--progress] <file> [<refname>...]") + +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 <file> 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 = "<stdin>"; + return read_bundle_header_fd(0, header, "<stdin>"); + } + + 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 <type> <object>"), + N_("git cat-file (-e | -p) <object>"), + N_("git cat-file (-t | -s) [--allow-unknown-type] <object>"), + N_("git cat-file (--textconv | --filters)\n" + " [<rev>:<path|tree-ish> | --path=<path|tree-ish> <rev>]"), + 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 <object> exists"), 'e'), + OPT_CMDMODE('p', NULL, &opt, N_("pretty-print <object> 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 <object> or <rev> 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 <contents>"), + 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 <path> 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(_("<rev> required with '%s'"), + usage, options, "--textconv"); + else if (!argc && opt == 'w') + usage_msg_optf(_("<rev> required with '%s'"), + usage, options, "--filters"); + else if (!argc && opt_epts) + usage_msg_optf(_("<object> 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 <type> <object> 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 <tree-ish>] [-a | --all | <attr>...] [--] <pathname>..."), +N_("git check-attr --stdin [-z] [--source <tree-ish>] [-a | --all | <attr>...]"), +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_("<tree-ish>"), 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 [<options>] <pathname>...", +"git check-ignore [<options>] --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 [<options>] <contact>..."), +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] [<options>] <refname>\n" +" or: git check-ref-format --branch <branchname-shorthand>"; + +/* + * 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 [<options>]"), + 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 <string>")), + 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 [<options>] [--] [<file>...]"), + 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 <string>")), + 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 [<options>] <branch>"), + N_("git checkout [<options>] [<branch>] -- <file>..."), + NULL, +}; + +static const char * const switch_branch_usage[] = { + N_("git switch [<options>] [<branch>]"), + NULL, +}; + +static const char * const restore_usage[] = { + N_("git restore [<options>] [--source=<branch>] <file>..."), + 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 <buffer, size, mode> 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=<style>` already recreates conflicts + * in both the worktree and the index, so adding --staged would be + * meaningless. + */ + if (!opts->accept_ref && opts->checkout_index) { + if (opts->writeout_stage) + die(_("'%s' or '%s' cannot be used with %s"), + "--ours", "--theirs", "--staged"); + + if (opts->merge) + die(_("'%s' or '%s' cannot be used with %s"), + "--merge", "--conflict", "--staged"); + } + + /* + * recreating unmerged index entries and writing out data from + * unmerged index entries would make no sense when checking out + * of a tree-ish. + */ + if ((opts->merge || opts->writeout_stage) && opts->source_tree) + die(_("'%s', '%s', or '%s' cannot be used when checking out of a tree"), + "--merge", "--ours", "--theirs"); + + if (opts->patch_mode) { + enum add_p_mode patch_mode; + const char *rev = new_branch_info->name; + char rev_oid[GIT_MAX_HEXSZ + 1]; + + /* + * Since rev can be in the form of `<a>...<b>` (which is not + * recognized by diff-index), we will always replace the name + * with the hex of the commit (whether it's in `...` form or + * not) for the run_add_interactive() machinery to work + * properly. However, there is special logic for the HEAD case + * so we mustn't replace that. Also, when we were given a + * tree-object, new_branch_info->commit would be NULL, but we + * do not have to do any replacement, either. + */ + if (rev && new_branch_info->commit && strcmp(rev, "HEAD")) + rev = oid_to_hex_r(rev_oid, &new_branch_info->commit->object.oid); + + if (opts->checkout_index && opts->checkout_worktree) + patch_mode = ADD_P_CHECKOUT; + else if (opts->checkout_index && !opts->checkout_worktree) + patch_mode = ADD_P_RESET; + else if (!opts->checkout_index && opts->checkout_worktree) + patch_mode = ADD_P_WORKTREE; + else + BUG("either flag must have been set, worktree=%d, index=%d", + opts->checkout_worktree, opts->checkout_index); + return !!run_add_p(the_repository, patch_mode, rev, + &opts->pathspec); + } + + repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR); + if (repo_read_index_preload(the_repository, &opts->pathspec, 0) < 0) + return error(_("index file corrupt")); + + if (opts->source_tree) + read_tree_some(opts->source_tree, &opts->pathspec); + if (opts->merge) + unmerge_index(&the_index, &opts->pathspec, CE_MATCHED); + + ps_matched = xcalloc(opts->pathspec.nr, 1); + + /* + * Make sure all pathspecs participated in locating the paths + * to be checked out. + */ + for (pos = 0; pos < the_index.cache_nr; pos++) + if (opts->overlay_mode) + mark_ce_for_checkout_overlay(the_index.cache[pos], + ps_matched, + opts); + else + mark_ce_for_checkout_no_overlay(the_index.cache[pos], + ps_matched, + opts); + + if (report_path_error(ps_matched, &opts->pathspec)) { + free(ps_matched); + return 1; + } + free(ps_matched); + + /* Any unmerged paths? */ + for (pos = 0; pos < the_index.cache_nr; pos++) { + const struct cache_entry *ce = the_index.cache[pos]; + if (ce->ce_flags & CE_MATCHED) { + if (!ce_stage(ce)) + continue; + if (opts->ignore_unmerged) { + if (!opts->quiet) + warning(_("path '%s' is unmerged"), ce->name); + } else if (opts->writeout_stage) { + errs |= check_stage(opts->writeout_stage, ce, pos, opts->overlay_mode); + } else if (opts->merge) { + errs |= check_stages((1<<2) | (1<<3), ce, pos); + } else { + errs = 1; + error(_("path '%s' is unmerged"), ce->name); + } + pos = skip_same_name(ce, pos) - 1; + } + } + if (errs) + return 1; + + /* Now we are committed to check them out */ + if (opts->checkout_worktree) + errs |= checkout_worktree(opts, new_branch_info); + else + remove_marked_cache_entries(&the_index, 1); + + /* + * Allow updating the index when checking out from the index. + * This is to save new stat info. + */ + if (opts->checkout_worktree && !opts->checkout_index && !opts->source_tree) + checkout_index = 1; + else + checkout_index = opts->checkout_index; + + if (checkout_index) { + if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK)) + die(_("unable to write new index file")); + } else { + /* + * NEEDSWORK: if --worktree is not specified, we + * should save stat info of checked out files in the + * index to avoid the next (potentially costly) + * refresh. But it's a bit tricker to do... + */ + rollback_lock_file(&lock_file); + } + + read_ref_full("HEAD", 0, &rev, NULL); + head = lookup_commit_reference_gently(the_repository, &rev, 1); + + errs |= post_checkout_hook(head, head, 0); + return errs; +} + +static void show_local_changes(struct object *head, + const struct diff_options *opts) +{ + struct rev_info rev; + /* I think we want full paths, even if we're in a subdirectory. */ + repo_init_revisions(the_repository, &rev, NULL); + rev.diffopt.flags = opts->flags; + rev.diffopt.output_format |= DIFF_FORMAT_NAME_STATUS; + rev.diffopt.flags.recursive = 1; + diff_setup_done(&rev.diffopt); + add_pending_object(&rev, head, NULL); + run_diff_index(&rev, 0); + release_revisions(&rev); +} + +static void describe_detached_head(const char *msg, struct commit *commit) +{ + struct strbuf sb = STRBUF_INIT; + + if (!repo_parse_commit(the_repository, commit)) + pp_commit_easy(CMIT_FMT_ONELINE, commit, &sb); + if (print_sha1_ellipsis()) { + fprintf(stderr, "%s %s... %s\n", msg, + repo_find_unique_abbrev(the_repository, &commit->object.oid, DEFAULT_ABBREV), + sb.buf); + } else { + fprintf(stderr, "%s %s %s\n", msg, + repo_find_unique_abbrev(the_repository, &commit->object.oid, DEFAULT_ABBREV), + sb.buf); + } + strbuf_release(&sb); +} + +static int reset_tree(struct tree *tree, const struct checkout_opts *o, + int worktree, int *writeout_error, + struct branch_info *info) +{ + struct unpack_trees_options opts; + struct tree_desc tree_desc; + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = -1; + opts.update = worktree; + opts.skip_unmerged = !worktree; + opts.reset = o->force ? UNPACK_RESET_OVERWRITE_UNTRACKED : + UNPACK_RESET_PROTECT_UNTRACKED; + opts.preserve_ignored = (!o->force && !o->overwrite_ignore); + opts.merge = 1; + opts.fn = oneway_merge; + opts.verbose_update = o->show_progress; + opts.src_index = &the_index; + opts.dst_index = &the_index; + init_checkout_metadata(&opts.meta, info->refname, + info->commit ? &info->commit->object.oid : null_oid(), + NULL); + parse_tree(tree); + init_tree_desc(&tree_desc, tree->buffer, tree->size); + switch (unpack_trees(1, &tree_desc, &opts)) { + case -2: + *writeout_error = 1; + /* + * We return 0 nevertheless, as the index is all right + * and more importantly we have made best efforts to + * update paths in the work tree, and we cannot revert + * them. + */ + /* fallthrough */ + case 0: + return 0; + default: + return 128; + } +} + +static void setup_branch_path(struct branch_info *branch) +{ + struct strbuf buf = STRBUF_INIT; + + /* + * If this is a ref, resolve it; otherwise, look up the OID for our + * expression. Failure here is okay. + */ + if (!repo_dwim_ref(the_repository, branch->name, strlen(branch->name), + &branch->oid, &branch->refname, 0)) + repo_get_oid_committish(the_repository, branch->name, &branch->oid); + + strbuf_branchname(&buf, branch->name, INTERPRET_BRANCH_LOCAL); + if (strcmp(buf.buf, branch->name)) { + free(branch->name); + branch->name = xstrdup(buf.buf); + } + strbuf_splice(&buf, 0, 0, "refs/heads/", 11); + free(branch->path); + branch->path = strbuf_detach(&buf, NULL); +} + +static void init_topts(struct unpack_trees_options *topts, int merge, + int show_progress, int overwrite_ignore, + struct commit *old_commit) +{ + memset(topts, 0, sizeof(*topts)); + topts->head_idx = -1; + topts->src_index = &the_index; + topts->dst_index = &the_index; + + setup_unpack_trees_porcelain(topts, "checkout"); + + topts->initial_checkout = is_index_unborn(&the_index); + topts->update = 1; + topts->merge = 1; + topts->quiet = merge && old_commit; + topts->verbose_update = show_progress; + topts->fn = twoway_merge; + topts->preserve_ignored = !overwrite_ignore; +} + +static int merge_working_tree(const struct checkout_opts *opts, + struct branch_info *old_branch_info, + struct branch_info *new_branch_info, + int *writeout_error) +{ + int ret; + struct lock_file lock_file = LOCK_INIT; + struct tree *new_tree; + + repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR); + if (repo_read_index_preload(the_repository, NULL, 0) < 0) + return error(_("index file corrupt")); + + resolve_undo_clear_index(&the_index); + if (opts->new_orphan_branch && opts->orphan_from_empty_tree) { + if (new_branch_info->commit) + BUG("'switch --orphan' should never accept a commit as starting point"); + new_tree = parse_tree_indirect(the_hash_algo->empty_tree); + } else + new_tree = repo_get_commit_tree(the_repository, + new_branch_info->commit); + if (opts->discard_changes) { + ret = reset_tree(new_tree, opts, 1, writeout_error, new_branch_info); + if (ret) + return ret; + } else { + struct tree_desc trees[2]; + struct tree *tree; + struct unpack_trees_options topts; + const struct object_id *old_commit_oid; + + refresh_index(&the_index, REFRESH_QUIET, NULL, NULL, NULL); + + if (unmerged_index(&the_index)) { + error(_("you need to resolve your current index first")); + return 1; + } + + /* 2-way merge to the new branch */ + init_topts(&topts, opts->merge, opts->show_progress, + opts->overwrite_ignore, old_branch_info->commit); + init_checkout_metadata(&topts.meta, new_branch_info->refname, + new_branch_info->commit ? + &new_branch_info->commit->object.oid : + &new_branch_info->oid, NULL); + + old_commit_oid = old_branch_info->commit ? + &old_branch_info->commit->object.oid : + the_hash_algo->empty_tree; + tree = parse_tree_indirect(old_commit_oid); + if (!tree) + die(_("unable to parse commit %s"), + oid_to_hex(old_commit_oid)); + + init_tree_desc(&trees[0], tree->buffer, tree->size); + parse_tree(new_tree); + tree = new_tree; + init_tree_desc(&trees[1], tree->buffer, tree->size); + + ret = unpack_trees(2, trees, &topts); + clear_unpack_trees_porcelain(&topts); + if (ret == -1) { + /* + * Unpack couldn't do a trivial merge; either + * give up or do a real merge, depending on + * whether the merge flag was used. + */ + struct tree *work; + struct tree *old_tree; + struct merge_options o; + struct strbuf sb = STRBUF_INIT; + struct strbuf old_commit_shortname = STRBUF_INIT; + + if (!opts->merge) + return 1; + + /* + * Without old_branch_info->commit, the below is the same as + * the two-tree unpack we already tried and failed. + */ + if (!old_branch_info->commit) + return 1; + old_tree = repo_get_commit_tree(the_repository, + old_branch_info->commit); + + if (repo_index_has_changes(the_repository, old_tree, &sb)) + die(_("cannot continue with staged changes in " + "the following files:\n%s"), sb.buf); + strbuf_release(&sb); + + /* Do more real merge */ + + /* + * We update the index fully, then write the + * tree from the index, then merge the new + * branch with the current tree, with the old + * branch as the base. Then we reset the index + * (but not the working tree) to the new + * branch, leaving the working tree as the + * merged version, but skipping unmerged + * entries in the index. + */ + + add_files_to_cache(the_repository, NULL, NULL, 0, 0); + init_merge_options(&o, the_repository); + o.verbosity = 0; + work = write_in_core_index_as_tree(the_repository); + + ret = reset_tree(new_tree, + opts, 1, + writeout_error, new_branch_info); + if (ret) + return ret; + o.ancestor = old_branch_info->name; + if (!old_branch_info->name) { + strbuf_add_unique_abbrev(&old_commit_shortname, + &old_branch_info->commit->object.oid, + DEFAULT_ABBREV); + o.ancestor = old_commit_shortname.buf; + } + o.branch1 = new_branch_info->name; + o.branch2 = "local"; + ret = merge_trees(&o, + new_tree, + work, + old_tree); + if (ret < 0) + exit(128); + ret = reset_tree(new_tree, + opts, 0, + writeout_error, new_branch_info); + strbuf_release(&o.obuf); + strbuf_release(&old_commit_shortname); + if (ret) + return ret; + } + } + + if (!cache_tree_fully_valid(the_index.cache_tree)) + cache_tree_update(&the_index, WRITE_TREE_SILENT | WRITE_TREE_REPAIR); + + if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK)) + die(_("unable to write new index file")); + + if (!opts->discard_changes && !opts->quiet && new_branch_info->commit) + show_local_changes(&new_branch_info->commit->object, &opts->diff_options); + + return 0; +} + +static void report_tracking(struct branch_info *new_branch_info) +{ + struct strbuf sb = STRBUF_INIT; + struct branch *branch = branch_get(new_branch_info->name); + + if (!format_tracking_info(branch, &sb, AHEAD_BEHIND_FULL, 1)) + return; + fputs(sb.buf, stdout); + strbuf_release(&sb); +} + +static void update_refs_for_switch(const struct checkout_opts *opts, + struct branch_info *old_branch_info, + struct branch_info *new_branch_info) +{ + struct strbuf msg = STRBUF_INIT; + const char *old_desc, *reflog_msg; + if (opts->new_branch) { + if (opts->new_orphan_branch) { + char *refname; + + refname = mkpathdup("refs/heads/%s", opts->new_orphan_branch); + if (opts->new_branch_log && + !should_autocreate_reflog(refname)) { + int ret; + struct strbuf err = STRBUF_INIT; + + ret = safe_create_reflog(refname, &err); + if (ret) { + fprintf(stderr, _("Can not do reflog for '%s': %s\n"), + opts->new_orphan_branch, err.buf); + strbuf_release(&err); + free(refname); + return; + } + strbuf_release(&err); + } + free(refname); + } + else + create_branch(the_repository, + opts->new_branch, new_branch_info->name, + opts->new_branch_force ? 1 : 0, + opts->new_branch_force ? 1 : 0, + opts->new_branch_log, + opts->quiet, + opts->track, + 0); + free(new_branch_info->name); + free(new_branch_info->refname); + new_branch_info->name = xstrdup(opts->new_branch); + setup_branch_path(new_branch_info); + } + + old_desc = old_branch_info->name; + if (!old_desc && old_branch_info->commit) + old_desc = oid_to_hex(&old_branch_info->commit->object.oid); + + reflog_msg = getenv("GIT_REFLOG_ACTION"); + if (!reflog_msg) + strbuf_addf(&msg, "checkout: moving from %s to %s", + old_desc ? old_desc : "(invalid)", new_branch_info->name); + else + strbuf_insertstr(&msg, 0, reflog_msg); + + if (!strcmp(new_branch_info->name, "HEAD") && !new_branch_info->path && !opts->force_detach) { + /* Nothing to do. */ + } else if (opts->force_detach || !new_branch_info->path) { /* No longer on any branch. */ + update_ref(msg.buf, "HEAD", &new_branch_info->commit->object.oid, NULL, + REF_NO_DEREF, UPDATE_REFS_DIE_ON_ERR); + if (!opts->quiet) { + if (old_branch_info->path && + advice_enabled(ADVICE_DETACHED_HEAD) && !opts->force_detach) + detach_advice(new_branch_info->name); + describe_detached_head(_("HEAD is now at"), new_branch_info->commit); + } + } else if (new_branch_info->path) { /* Switch branches. */ + if (create_symref("HEAD", new_branch_info->path, msg.buf) < 0) + die(_("unable to update HEAD")); + if (!opts->quiet) { + if (old_branch_info->path && !strcmp(new_branch_info->path, old_branch_info->path)) { + if (opts->new_branch_force) + fprintf(stderr, _("Reset branch '%s'\n"), + new_branch_info->name); + else + fprintf(stderr, _("Already on '%s'\n"), + new_branch_info->name); + } else if (opts->new_branch) { + if (opts->branch_exists) + fprintf(stderr, _("Switched to and reset branch '%s'\n"), new_branch_info->name); + else + fprintf(stderr, _("Switched to a new branch '%s'\n"), new_branch_info->name); + } else { + fprintf(stderr, _("Switched to branch '%s'\n"), + new_branch_info->name); + } + } + if (old_branch_info->path && old_branch_info->name) { + if (!ref_exists(old_branch_info->path) && reflog_exists(old_branch_info->path)) + delete_reflog(old_branch_info->path); + } + } + remove_branch_state(the_repository, !opts->quiet); + strbuf_release(&msg); + if (!opts->quiet && + (new_branch_info->path || (!opts->force_detach && !strcmp(new_branch_info->name, "HEAD")))) + report_tracking(new_branch_info); +} + +static int add_pending_uninteresting_ref(const char *refname, + const struct object_id *oid, + int flags UNUSED, void *cb_data) +{ + add_pending_oid(cb_data, refname, oid, UNINTERESTING); + return 0; +} + +static void describe_one_orphan(struct strbuf *sb, struct commit *commit) +{ + strbuf_addstr(sb, " "); + strbuf_add_unique_abbrev(sb, &commit->object.oid, DEFAULT_ABBREV); + strbuf_addch(sb, ' '); + if (!repo_parse_commit(the_repository, commit)) + pp_commit_easy(CMIT_FMT_ONELINE, commit, sb); + strbuf_addch(sb, '\n'); +} + +#define ORPHAN_CUTOFF 4 +static void suggest_reattach(struct commit *commit, struct rev_info *revs) +{ + struct commit *c, *last = NULL; + struct strbuf sb = STRBUF_INIT; + int lost = 0; + while ((c = get_revision(revs)) != NULL) { + if (lost < ORPHAN_CUTOFF) + describe_one_orphan(&sb, c); + last = c; + lost++; + } + if (ORPHAN_CUTOFF < lost) { + int more = lost - ORPHAN_CUTOFF; + if (more == 1) + describe_one_orphan(&sb, last); + else + strbuf_addf(&sb, _(" ... and %d more.\n"), more); + } + + fprintf(stderr, + Q_( + /* The singular version */ + "Warning: you are leaving %d commit behind, " + "not connected to\n" + "any of your branches:\n\n" + "%s\n", + /* The plural version */ + "Warning: you are leaving %d commits behind, " + "not connected to\n" + "any of your branches:\n\n" + "%s\n", + /* Give ngettext() the count */ + lost), + lost, + sb.buf); + strbuf_release(&sb); + + if (advice_enabled(ADVICE_DETACHED_HEAD)) + fprintf(stderr, + Q_( + /* The singular version */ + "If you want to keep it by creating a new branch, " + "this may be a good time\nto do so with:\n\n" + " git branch <new-branch-name> %s\n\n", + /* The plural version */ + "If you want to keep them by creating a new branch, " + "this may be a good time\nto do so with:\n\n" + " git branch <new-branch-name> %s\n\n", + /* Give ngettext() the count */ + lost), + repo_find_unique_abbrev(the_repository, &commit->object.oid, DEFAULT_ABBREV)); +} + +/* + * We are about to leave commit that was at the tip of a detached + * HEAD. If it is not reachable from any ref, this is the last chance + * for the user to do so without resorting to reflog. + */ +static void orphaned_commit_warning(struct commit *old_commit, struct commit *new_commit) +{ + struct rev_info revs; + struct object *object = &old_commit->object; + + repo_init_revisions(the_repository, &revs, NULL); + setup_revisions(0, NULL, &revs, NULL); + + object->flags &= ~UNINTERESTING; + add_pending_object(&revs, object, oid_to_hex(&object->oid)); + + for_each_ref(add_pending_uninteresting_ref, &revs); + if (new_commit) + add_pending_oid(&revs, "HEAD", + &new_commit->object.oid, + UNINTERESTING); + + if (prepare_revision_walk(&revs)) + die(_("internal error in revision walk")); + if (!(old_commit->object.flags & UNINTERESTING)) + suggest_reattach(old_commit, &revs); + else + describe_detached_head(_("Previous HEAD position was"), old_commit); + + /* Clean up objects used, as they will be reused. */ + repo_clear_commit_marks(the_repository, ALL_REV_FLAGS); + release_revisions(&revs); +} + +static int switch_branches(const struct checkout_opts *opts, + struct branch_info *new_branch_info) +{ + int ret = 0; + struct branch_info old_branch_info = { 0 }; + struct object_id rev; + int flag, writeout_error = 0; + int do_merge = 1; + + trace2_cmd_mode("branch"); + + memset(&old_branch_info, 0, sizeof(old_branch_info)); + old_branch_info.path = resolve_refdup("HEAD", 0, &rev, &flag); + if (old_branch_info.path) + old_branch_info.commit = lookup_commit_reference_gently(the_repository, &rev, 1); + if (!(flag & REF_ISSYMREF)) + FREE_AND_NULL(old_branch_info.path); + + if (old_branch_info.path) { + const char *const prefix = "refs/heads/"; + const char *p; + if (skip_prefix(old_branch_info.path, prefix, &p)) + old_branch_info.name = xstrdup(p); + } + + if (opts->new_orphan_branch && opts->orphan_from_empty_tree) { + if (new_branch_info->name) + BUG("'switch --orphan' should never accept a commit as starting point"); + new_branch_info->commit = NULL; + new_branch_info->name = xstrdup("(empty)"); + do_merge = 1; + } + + if (!new_branch_info->name) { + new_branch_info->name = xstrdup("HEAD"); + new_branch_info->commit = old_branch_info.commit; + if (!new_branch_info->commit) + die(_("You are on a branch yet to be born")); + parse_commit_or_die(new_branch_info->commit); + + if (opts->only_merge_on_switching_branches) + do_merge = 0; + } + + if (do_merge) { + ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error); + if (ret) { + branch_info_release(&old_branch_info); + return ret; + } + } + + if (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit) + orphaned_commit_warning(old_branch_info.commit, new_branch_info->commit); + + update_refs_for_switch(opts, &old_branch_info, new_branch_info); + + ret = post_checkout_hook(old_branch_info.commit, new_branch_info->commit, 1); + branch_info_release(&old_branch_info); + + return ret || writeout_error; +} + +static int git_checkout_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + struct checkout_opts *opts = cb; + + if (!strcmp(var, "diff.ignoresubmodules")) { + handle_ignore_submodules_arg(&opts->diff_options, value); + return 0; + } + if (!strcmp(var, "checkout.guess")) { + opts->dwim_new_local_branch = git_config_bool(var, value); + return 0; + } + + if (starts_with(var, "submodule.")) + return git_default_submodule_config(var, value, NULL); + + return git_xmerge_config(var, value, ctx, NULL); +} + +static void setup_new_branch_info_and_source_tree( + struct branch_info *new_branch_info, + struct checkout_opts *opts, + struct object_id *rev, + const char *arg) +{ + struct tree **source_tree = &opts->source_tree; + struct object_id branch_rev; + + new_branch_info->name = xstrdup(arg); + setup_branch_path(new_branch_info); + + if (!check_refname_format(new_branch_info->path, 0) && + !read_ref(new_branch_info->path, &branch_rev)) + oidcpy(rev, &branch_rev); + else + /* not an existing branch */ + FREE_AND_NULL(new_branch_info->path); + + new_branch_info->commit = lookup_commit_reference_gently(the_repository, rev, 1); + if (!new_branch_info->commit) { + /* not a commit */ + *source_tree = parse_tree_indirect(rev); + } else { + parse_commit_or_die(new_branch_info->commit); + *source_tree = repo_get_commit_tree(the_repository, + new_branch_info->commit); + } +} + +static const char *parse_remote_branch(const char *arg, + struct object_id *rev, + int could_be_checkout_paths) +{ + int num_matches = 0; + const char *remote = unique_tracking_name(arg, rev, &num_matches); + + if (remote && could_be_checkout_paths) { + die(_("'%s' could be both a local file and a tracking branch.\n" + "Please use -- (and optionally --no-guess) to disambiguate"), + arg); + } + + if (!remote && num_matches > 1) { + if (advice_enabled(ADVICE_CHECKOUT_AMBIGUOUS_REMOTE_BRANCH_NAME)) { + advise(_("If you meant to check out a remote tracking branch on, e.g. 'origin',\n" + "you can do so by fully qualifying the name with the --track option:\n" + "\n" + " git checkout --track origin/<name>\n" + "\n" + "If you'd like to always have checkouts of an ambiguous <name> prefer\n" + "one remote, e.g. the 'origin' remote, consider setting\n" + "checkout.defaultRemote=origin in your config.")); + } + + die(_("'%s' matched multiple (%d) remote tracking branches"), + arg, num_matches); + } + + return remote; +} + +static int parse_branchname_arg(int argc, const char **argv, + int dwim_new_local_branch_ok, + struct branch_info *new_branch_info, + struct checkout_opts *opts, + struct object_id *rev) +{ + const char **new_branch = &opts->new_branch; + int argcount = 0; + const char *arg; + int dash_dash_pos; + int has_dash_dash = 0; + int i; + + /* + * case 1: git checkout <ref> -- [<paths>] + * + * <ref> must be a valid tree, everything after the '--' must be + * a path. + * + * case 2: git checkout -- [<paths>] + * + * everything after the '--' must be paths. + * + * case 3: git checkout <something> [--] + * + * (a) If <something> is a commit, that is to + * switch to the branch or detach HEAD at it. As a special case, + * if <something> is A...B (missing A or B means HEAD but you can + * omit at most one side), and if there is a unique merge base + * between A and B, A...B names that merge base. + * + * (b) If <something> is _not_ a commit, either "--" is present + * or <something> is not a path, no -t or -b was given, + * and there is a tracking branch whose name is <something> + * in one and only one remote (or if the branch exists on the + * remote named in checkout.defaultRemote), then this is a + * short-hand to fork local <something> from that + * remote-tracking branch. + * + * (c) Otherwise, if "--" is present, treat it like case (1). + * + * (d) Otherwise : + * - if it's a reference, treat it like case (1) + * - else if it's a path, treat it like case (2) + * - else: fail. + * + * case 4: git checkout <something> <paths> + * + * The first argument must not be ambiguous. + * - If it's *only* a reference, treat it like case (1). + * - If it's only a path, treat it like case (2). + * - else: fail. + * + */ + if (!argc) + return 0; + + if (!opts->accept_pathspec) { + if (argc > 1) + die(_("only one reference expected")); + has_dash_dash = 1; /* helps disambiguate */ + } + + arg = argv[0]; + dash_dash_pos = -1; + for (i = 0; i < argc; i++) { + if (opts->accept_pathspec && !strcmp(argv[i], "--")) { + dash_dash_pos = i; + break; + } + } + if (dash_dash_pos == 0) + return 1; /* case (2) */ + else if (dash_dash_pos == 1) + has_dash_dash = 1; /* case (3) or (1) */ + else if (dash_dash_pos >= 2) + die(_("only one reference expected, %d given."), dash_dash_pos); + opts->count_checkout_paths = !opts->quiet && !has_dash_dash; + + if (!strcmp(arg, "-")) + arg = "@{-1}"; + + if (repo_get_oid_mb(the_repository, arg, rev)) { + /* + * Either case (3) or (4), with <something> not being + * a commit, or an attempt to use case (1) with an + * invalid ref. + * + * It's likely an error, but we need to find out if + * we should auto-create the branch, case (3).(b). + */ + int recover_with_dwim = dwim_new_local_branch_ok; + + int could_be_checkout_paths = !has_dash_dash && + check_filename(opts->prefix, arg); + + if (!has_dash_dash && !no_wildcard(arg)) + recover_with_dwim = 0; + + /* + * Accept "git checkout foo", "git checkout foo --" + * and "git switch foo" as candidates for dwim. + */ + if (!(argc == 1 && !has_dash_dash) && + !(argc == 2 && has_dash_dash) && + opts->accept_pathspec) + recover_with_dwim = 0; + + if (recover_with_dwim) { + const char *remote = parse_remote_branch(arg, rev, + could_be_checkout_paths); + if (remote) { + *new_branch = arg; + arg = remote; + /* DWIMmed to create local branch, case (3).(b) */ + } else { + recover_with_dwim = 0; + } + } + + if (!recover_with_dwim) { + if (has_dash_dash) + die(_("invalid reference: %s"), arg); + return argcount; + } + } + + /* we can't end up being in (2) anymore, eat the argument */ + argcount++; + argv++; + argc--; + + setup_new_branch_info_and_source_tree(new_branch_info, opts, rev, arg); + + if (!opts->source_tree) /* case (1): want a tree */ + die(_("reference is not a tree: %s"), arg); + + if (!has_dash_dash) { /* case (3).(d) -> (1) */ + /* + * Do not complain the most common case + * git checkout branch + * even if there happen to be a file called 'branch'; + * it would be extremely annoying. + */ + if (argc) + verify_non_filename(opts->prefix, arg); + } else if (opts->accept_pathspec) { + argcount++; + argv++; + argc--; + } + + return argcount; +} + +static int switch_unborn_to_new_branch(const struct checkout_opts *opts) +{ + int status; + struct strbuf branch_ref = STRBUF_INIT; + + trace2_cmd_mode("unborn"); + + if (!opts->new_branch) + die(_("You are on a branch yet to be born")); + strbuf_addf(&branch_ref, "refs/heads/%s", opts->new_branch); + status = create_symref("HEAD", branch_ref.buf, "checkout -b"); + strbuf_release(&branch_ref); + if (!opts->quiet) + fprintf(stderr, _("Switched to a new branch '%s'\n"), + opts->new_branch); + return status; +} + +static void die_expecting_a_branch(const struct branch_info *branch_info) +{ + struct object_id oid; + char *to_free; + int code; + + if (repo_dwim_ref(the_repository, branch_info->name, + strlen(branch_info->name), &oid, &to_free, 0) == 1) { + const char *ref = to_free; + + if (skip_prefix(ref, "refs/tags/", &ref)) + code = die_message(_("a branch is expected, got tag '%s'"), ref); + else if (skip_prefix(ref, "refs/remotes/", &ref)) + code = die_message(_("a branch is expected, got remote branch '%s'"), ref); + else + code = die_message(_("a branch is expected, got '%s'"), ref); + } + else if (branch_info->commit) + code = die_message(_("a branch is expected, got commit '%s'"), branch_info->name); + else + /* + * This case should never happen because we already die() on + * non-commit, but just in case. + */ + code = die_message(_("a branch is expected, got '%s'"), branch_info->name); + + if (advice_enabled(ADVICE_SUGGEST_DETACHING_HEAD)) + advise(_("If you want to detach HEAD at the commit, try again with the --detach option.")); + + exit(code); +} + +static void die_if_some_operation_in_progress(void) +{ + struct wt_status_state state; + + memset(&state, 0, sizeof(state)); + wt_status_get_state(the_repository, &state, 0); + + if (state.merge_in_progress) + die(_("cannot switch branch while merging\n" + "Consider \"git merge --quit\" " + "or \"git worktree add\".")); + if (state.am_in_progress) + die(_("cannot switch branch in the middle of an am session\n" + "Consider \"git am --quit\" " + "or \"git worktree add\".")); + if (state.rebase_interactive_in_progress || state.rebase_in_progress) + die(_("cannot switch branch while rebasing\n" + "Consider \"git rebase --quit\" " + "or \"git worktree add\".")); + if (state.cherry_pick_in_progress) + die(_("cannot switch branch while cherry-picking\n" + "Consider \"git cherry-pick --quit\" " + "or \"git worktree add\".")); + if (state.revert_in_progress) + die(_("cannot switch branch while reverting\n" + "Consider \"git revert --quit\" " + "or \"git worktree add\".")); + if (state.bisect_in_progress) + warning(_("you are switching branch while bisecting")); + + wt_status_state_free_buffers(&state); +} + +static int checkout_branch(struct checkout_opts *opts, + struct branch_info *new_branch_info) +{ + if (opts->pathspec.nr) + die(_("paths cannot be used with switching branches")); + + if (opts->patch_mode) + die(_("'%s' cannot be used with switching branches"), + "--patch"); + + if (opts->overlay_mode != -1) + die(_("'%s' cannot be used with switching branches"), + "--[no]-overlay"); + + if (opts->writeout_stage) + die(_("'%s' cannot be used with switching branches"), + "--ours/--theirs"); + + if (opts->force && opts->merge) + die(_("'%s' cannot be used with '%s'"), "-f", "-m"); + + if (opts->discard_changes && opts->merge) + die(_("'%s' cannot be used with '%s'"), "--discard-changes", "--merge"); + + if (opts->force_detach && opts->new_branch) + die(_("'%s' cannot be used with '%s'"), + "--detach", "-b/-B/--orphan"); + + if (opts->new_orphan_branch) { + if (opts->track != BRANCH_TRACK_UNSPECIFIED) + die(_("'%s' cannot be used with '%s'"), "--orphan", "-t"); + if (opts->orphan_from_empty_tree && new_branch_info->name) + die(_("'%s' cannot take <start-point>"), "--orphan"); + } else if (opts->force_detach) { + if (opts->track != BRANCH_TRACK_UNSPECIFIED) + die(_("'%s' cannot be used with '%s'"), "--detach", "-t"); + } else if (opts->track == BRANCH_TRACK_UNSPECIFIED) + opts->track = git_branch_track; + + if (new_branch_info->name && !new_branch_info->commit) + die(_("Cannot switch branch to a non-commit '%s'"), + new_branch_info->name); + + if (!opts->switch_branch_doing_nothing_is_ok && + !new_branch_info->name && + !opts->new_branch && + !opts->force_detach) + die(_("missing branch or commit argument")); + + if (!opts->implicit_detach && + !opts->force_detach && + !opts->new_branch && + !opts->new_branch_force && + new_branch_info->name && + !new_branch_info->path) + die_expecting_a_branch(new_branch_info); + + if (!opts->can_switch_when_in_progress) + die_if_some_operation_in_progress(); + + if (new_branch_info->path && !opts->force_detach && !opts->new_branch && + !opts->ignore_other_worktrees) { + int flag; + char *head_ref = resolve_refdup("HEAD", 0, NULL, &flag); + if (head_ref && + (!(flag & REF_ISSYMREF) || strcmp(head_ref, new_branch_info->path))) + die_if_checked_out(new_branch_info->path, 1); + free(head_ref); + } + + if (!new_branch_info->commit && opts->new_branch) { + struct object_id rev; + int flag; + + if (!read_ref_full("HEAD", 0, &rev, &flag) && + (flag & REF_ISSYMREF) && is_null_oid(&rev)) + return switch_unborn_to_new_branch(opts); + } + return switch_branches(opts, new_branch_info); +} + +static struct option *add_common_options(struct checkout_opts *opts, + struct option *prevopts) +{ + struct option options[] = { + OPT__QUIET(&opts->quiet, N_("suppress progress reporting")), + OPT_CALLBACK_F(0, "recurse-submodules", NULL, + "checkout", "control recursive updating of submodules", + PARSE_OPT_OPTARG, option_parse_recurse_submodules_worktree_updater), + OPT_BOOL(0, "progress", &opts->show_progress, N_("force progress reporting")), + OPT_BOOL('m', "merge", &opts->merge, N_("perform a 3-way merge with the new branch")), + OPT_STRING(0, "conflict", &opts->conflict_style, N_("style"), + N_("conflict style (merge, diff3, or zdiff3)")), + OPT_END() + }; + struct option *newopts = parse_options_concat(prevopts, options); + free(prevopts); + return newopts; +} + +static struct option *add_common_switch_branch_options( + struct checkout_opts *opts, struct option *prevopts) +{ + struct option options[] = { + OPT_BOOL('d', "detach", &opts->force_detach, N_("detach HEAD at named commit")), + OPT_CALLBACK_F('t', "track", &opts->track, "(direct|inherit)", + N_("set branch tracking configuration"), + PARSE_OPT_OPTARG, + parse_opt_tracking_mode), + OPT__FORCE(&opts->force, N_("force checkout (throw away local modifications)"), + PARSE_OPT_NOCOMPLETE), + OPT_STRING(0, "orphan", &opts->new_orphan_branch, N_("new-branch"), N_("new unparented branch")), + OPT_BOOL_F(0, "overwrite-ignore", &opts->overwrite_ignore, + N_("update ignored files (default)"), + PARSE_OPT_NOCOMPLETE), + OPT_BOOL(0, "ignore-other-worktrees", &opts->ignore_other_worktrees, + N_("do not check if another worktree is holding the given ref")), + OPT_END() + }; + struct option *newopts = parse_options_concat(prevopts, options); + free(prevopts); + return newopts; +} + +static struct option *add_checkout_path_options(struct checkout_opts *opts, + struct option *prevopts) +{ + struct option options[] = { + OPT_SET_INT_F('2', "ours", &opts->writeout_stage, + N_("checkout our version for unmerged files"), + 2, PARSE_OPT_NONEG), + OPT_SET_INT_F('3', "theirs", &opts->writeout_stage, + N_("checkout their version for unmerged files"), + 3, PARSE_OPT_NONEG), + OPT_BOOL('p', "patch", &opts->patch_mode, N_("select hunks interactively")), + OPT_BOOL(0, "ignore-skip-worktree-bits", &opts->ignore_skipworktree, + N_("do not limit pathspecs to sparse entries only")), + OPT_PATHSPEC_FROM_FILE(&opts->pathspec_from_file), + OPT_PATHSPEC_FILE_NUL(&opts->pathspec_file_nul), + OPT_END() + }; + struct option *newopts = parse_options_concat(prevopts, options); + free(prevopts); + return newopts; +} + +/* create-branch option (either b or c) */ +static char cb_option = 'b'; + +static int checkout_main(int argc, const char **argv, const char *prefix, + struct checkout_opts *opts, struct option *options, + const char * const usagestr[], + struct branch_info *new_branch_info) +{ + int parseopt_flags = 0; + + opts->overwrite_ignore = 1; + opts->prefix = prefix; + opts->show_progress = -1; + + git_config(git_checkout_config, opts); + if (the_repository->gitdir) { + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + } + + opts->track = BRANCH_TRACK_UNSPECIFIED; + + if (!opts->accept_pathspec && !opts->accept_ref) + BUG("make up your mind, you need to take _something_"); + if (opts->accept_pathspec && opts->accept_ref) + parseopt_flags = PARSE_OPT_KEEP_DASHDASH; + + argc = parse_options(argc, argv, prefix, options, + usagestr, parseopt_flags); + + if (opts->show_progress < 0) { + if (opts->quiet) + opts->show_progress = 0; + else + opts->show_progress = isatty(2); + } + + if (opts->conflict_style) { + struct key_value_info kvi = KVI_INIT; + struct config_context ctx = { + .kvi = &kvi, + }; + opts->merge = 1; /* implied */ + git_xmerge_config("merge.conflictstyle", opts->conflict_style, + &ctx, NULL); + } + if (opts->force) { + opts->discard_changes = 1; + opts->ignore_unmerged_opt = "--force"; + opts->ignore_unmerged = 1; + } + + if ((!!opts->new_branch + !!opts->new_branch_force + !!opts->new_orphan_branch) > 1) + die(_("options '-%c', '-%c', and '%s' cannot be used together"), + cb_option, toupper(cb_option), "--orphan"); + + if (opts->overlay_mode == 1 && opts->patch_mode) + die(_("options '%s' and '%s' cannot be used together"), "-p", "--overlay"); + + if (opts->checkout_index >= 0 || opts->checkout_worktree >= 0) { + if (opts->checkout_index < 0) + opts->checkout_index = 0; + if (opts->checkout_worktree < 0) + opts->checkout_worktree = 0; + } else { + if (opts->checkout_index < 0) + opts->checkout_index = -opts->checkout_index - 1; + if (opts->checkout_worktree < 0) + opts->checkout_worktree = -opts->checkout_worktree - 1; + } + if (opts->checkout_index < 0 || opts->checkout_worktree < 0) + BUG("these flags should be non-negative by now"); + /* + * convenient shortcut: "git restore --staged [--worktree]" equals + * "git restore --staged [--worktree] --source HEAD" + */ + if (!opts->from_treeish && opts->checkout_index) + opts->from_treeish = "HEAD"; + + /* + * From here on, new_branch will contain the branch to be checked out, + * and new_branch_force and new_orphan_branch will tell us which one of + * -b/-B/-c/-C/--orphan is being used. + */ + if (opts->new_branch_force) + opts->new_branch = opts->new_branch_force; + + if (opts->new_orphan_branch) + opts->new_branch = opts->new_orphan_branch; + + /* --track without -c/-C/-b/-B/--orphan should DWIM */ + if (opts->track != BRANCH_TRACK_UNSPECIFIED && !opts->new_branch) { + const char *argv0 = argv[0]; + if (!argc || !strcmp(argv0, "--")) + die(_("--track needs a branch name")); + skip_prefix(argv0, "refs/", &argv0); + skip_prefix(argv0, "remotes/", &argv0); + argv0 = strchr(argv0, '/'); + if (!argv0 || !argv0[1]) + die(_("missing branch name; try -%c"), cb_option); + opts->new_branch = argv0 + 1; + } + + /* + * Extract branch name from command line arguments, so + * all that is left is pathspecs. + * + * Handle + * + * 1) git checkout <tree> -- [<paths>] + * 2) git checkout -- [<paths>] + * 3) git checkout <something> [<paths>] + * + * including "last branch" syntax and DWIM-ery for names of + * remote branches, erroring out for invalid or ambiguous cases. + */ + if (argc && opts->accept_ref) { + struct object_id rev; + int dwim_ok = + !opts->patch_mode && + opts->dwim_new_local_branch && + opts->track == BRANCH_TRACK_UNSPECIFIED && + !opts->new_branch; + int n = parse_branchname_arg(argc, argv, dwim_ok, + new_branch_info, opts, &rev); + argv += n; + argc -= n; + } else if (!opts->accept_ref && opts->from_treeish) { + struct object_id rev; + + if (repo_get_oid_mb(the_repository, opts->from_treeish, &rev)) + die(_("could not resolve %s"), opts->from_treeish); + + setup_new_branch_info_and_source_tree(new_branch_info, + opts, &rev, + opts->from_treeish); + + if (!opts->source_tree) + die(_("reference is not a tree: %s"), opts->from_treeish); + } + + if (argc) { + parse_pathspec(&opts->pathspec, 0, + opts->patch_mode ? PATHSPEC_PREFIX_ORIGIN : 0, + prefix, argv); + + if (!opts->pathspec.nr) + die(_("invalid path specification")); + + /* + * Try to give more helpful suggestion. + * new_branch && argc > 1 will be caught later. + */ + if (opts->new_branch && argc == 1 && !new_branch_info->commit) + die(_("'%s' is not a commit and a branch '%s' cannot be created from it"), + argv[0], opts->new_branch); + + if (opts->force_detach) + die(_("git checkout: --detach does not take a path argument '%s'"), + argv[0]); + } + + if (opts->pathspec_from_file) { + if (opts->pathspec.nr) + die(_("'%s' and pathspec arguments cannot be used together"), "--pathspec-from-file"); + + if (opts->force_detach) + die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--detach"); + + if (opts->patch_mode) + die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--patch"); + + parse_pathspec_file(&opts->pathspec, 0, + 0, + prefix, opts->pathspec_from_file, opts->pathspec_file_nul); + } else if (opts->pathspec_file_nul) { + die(_("the option '%s' requires '%s'"), "--pathspec-file-nul", "--pathspec-from-file"); + } + + opts->pathspec.recursive = 1; + + if (opts->pathspec.nr) { + if (1 < !!opts->writeout_stage + !!opts->force + !!opts->merge) + die(_("git checkout: --ours/--theirs, --force and --merge are incompatible when\n" + "checking out of the index.")); + } else { + if (opts->accept_pathspec && !opts->empty_pathspec_ok && + !opts->patch_mode) /* patch mode is special */ + die(_("you must specify path(s) to restore")); + } + + if (opts->new_branch) { + struct strbuf buf = STRBUF_INIT; + + if (opts->new_branch_force) + opts->branch_exists = validate_branchname(opts->new_branch, &buf); + else + opts->branch_exists = + validate_new_branchname(opts->new_branch, &buf, 0); + strbuf_release(&buf); + } + + if (opts->patch_mode || opts->pathspec.nr) + return checkout_paths(opts, new_branch_info); + else + return checkout_branch(opts, new_branch_info); +} + +int cmd_checkout(int argc, const char **argv, const char *prefix) +{ + struct checkout_opts opts; + struct option *options; + struct option checkout_options[] = { + OPT_STRING('b', NULL, &opts.new_branch, N_("branch"), + N_("create and checkout a new branch")), + OPT_STRING('B', NULL, &opts.new_branch_force, N_("branch"), + N_("create/reset and checkout a branch")), + OPT_BOOL('l', NULL, &opts.new_branch_log, N_("create reflog for new branch")), + OPT_BOOL(0, "guess", &opts.dwim_new_local_branch, + N_("second guess 'git checkout <no-such-branch>' (default)")), + OPT_BOOL(0, "overlay", &opts.overlay_mode, N_("use overlay mode (default)")), + OPT_END() + }; + int ret; + struct branch_info new_branch_info = { 0 }; + + memset(&opts, 0, sizeof(opts)); + opts.dwim_new_local_branch = 1; + opts.switch_branch_doing_nothing_is_ok = 1; + opts.only_merge_on_switching_branches = 0; + opts.accept_ref = 1; + opts.accept_pathspec = 1; + opts.implicit_detach = 1; + opts.can_switch_when_in_progress = 1; + opts.orphan_from_empty_tree = 0; + opts.empty_pathspec_ok = 1; + opts.overlay_mode = -1; + opts.checkout_index = -2; /* default on */ + opts.checkout_worktree = -2; /* default on */ + + if (argc == 3 && !strcmp(argv[1], "-b")) { + /* + * User ran 'git checkout -b <branch>' and expects + * the same behavior as 'git switch -c <branch>'. + */ + opts.switch_branch_doing_nothing_is_ok = 0; + opts.only_merge_on_switching_branches = 1; + } + + options = parse_options_dup(checkout_options); + options = add_common_options(&opts, options); + options = add_common_switch_branch_options(&opts, options); + options = add_checkout_path_options(&opts, options); + + ret = checkout_main(argc, argv, prefix, &opts, + options, checkout_usage, &new_branch_info); + branch_info_release(&new_branch_info); + clear_pathspec(&opts.pathspec); + free(opts.pathspec_from_file); + FREE_AND_NULL(options); + return ret; +} + +int cmd_switch(int argc, const char **argv, const char *prefix) +{ + struct checkout_opts opts; + struct option *options = NULL; + struct option switch_options[] = { + OPT_STRING('c', "create", &opts.new_branch, N_("branch"), + N_("create and switch to a new branch")), + OPT_STRING('C', "force-create", &opts.new_branch_force, N_("branch"), + N_("create/reset and switch to a branch")), + OPT_BOOL(0, "guess", &opts.dwim_new_local_branch, + N_("second guess 'git switch <no-such-branch>'")), + OPT_BOOL(0, "discard-changes", &opts.discard_changes, + N_("throw away local modifications")), + OPT_END() + }; + int ret; + struct branch_info new_branch_info = { 0 }; + + memset(&opts, 0, sizeof(opts)); + opts.dwim_new_local_branch = 1; + opts.accept_ref = 1; + opts.accept_pathspec = 0; + opts.switch_branch_doing_nothing_is_ok = 0; + opts.only_merge_on_switching_branches = 1; + opts.implicit_detach = 0; + opts.can_switch_when_in_progress = 0; + opts.orphan_from_empty_tree = 1; + opts.overlay_mode = -1; + + options = parse_options_dup(switch_options); + options = add_common_options(&opts, options); + options = add_common_switch_branch_options(&opts, options); + + cb_option = 'c'; + + ret = checkout_main(argc, argv, prefix, &opts, + options, switch_branch_usage, &new_branch_info); + branch_info_release(&new_branch_info); + FREE_AND_NULL(options); + return ret; +} + +int cmd_restore(int argc, const char **argv, const char *prefix) +{ + struct checkout_opts opts; + struct option *options; + struct option restore_options[] = { + OPT_STRING('s', "source", &opts.from_treeish, "<tree-ish>", + N_("which tree-ish to checkout from")), + OPT_BOOL('S', "staged", &opts.checkout_index, + N_("restore the index")), + OPT_BOOL('W', "worktree", &opts.checkout_worktree, + N_("restore the working tree (default)")), + OPT_BOOL(0, "ignore-unmerged", &opts.ignore_unmerged, + N_("ignore unmerged entries")), + OPT_BOOL(0, "overlay", &opts.overlay_mode, N_("use overlay mode")), + OPT_END() + }; + int ret; + struct branch_info new_branch_info = { 0 }; + + memset(&opts, 0, sizeof(opts)); + opts.accept_ref = 0; + opts.accept_pathspec = 1; + opts.empty_pathspec_ok = 0; + opts.overlay_mode = 0; + opts.checkout_index = -1; /* default off */ + opts.checkout_worktree = -2; /* default on */ + opts.ignore_unmerged_opt = "--ignore-unmerged"; + + options = parse_options_dup(restore_options); + options = add_common_options(&opts, options); + options = add_checkout_path_options(&opts, options); + + ret = checkout_main(argc, argv, prefix, &opts, + options, restore_usage, &new_branch_info); + branch_info_release(&new_branch_info); + FREE_AND_NULL(options); + return ret; +} diff --git a/builtin/clean.c b/builtin/clean.c new file mode 100644 index 0000000..49c224e --- /dev/null +++ b/builtin/clean.c @@ -0,0 +1,1105 @@ +/* + * "git clean" builtin command + * + * Copyright (C) 2007 Shawn Bohrer + * + * Based on git-clean.sh by Pavel Roskin + */ + +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "abspath.h" +#include "config.h" +#include "dir.h" +#include "gettext.h" +#include "parse-options.h" +#include "path.h" +#include "read-cache-ll.h" +#include "repository.h" +#include "setup.h" +#include "string-list.h" +#include "quote.h" +#include "column.h" +#include "color.h" +#include "pathspec.h" +#include "help.h" +#include "prompt.h" + +static int force = -1; /* unset */ +static int interactive; +static struct string_list del_list = STRING_LIST_INIT_DUP; +static unsigned int colopts; + +static const char *const builtin_clean_usage[] = { + N_("git clean [-d] [-f] [-i] [-n] [-q] [-e <pattern>] [-x | -X] [--] [<pathspec>...]"), + NULL +}; + +static const char *msg_remove = N_("Removing %s\n"); +static const char *msg_would_remove = N_("Would remove %s\n"); +static const char *msg_skip_git_dir = N_("Skipping repository %s\n"); +static const char *msg_would_skip_git_dir = N_("Would skip repository %s\n"); +static const char *msg_warn_remove_failed = N_("failed to remove %s"); +static const char *msg_warn_lstat_failed = N_("could not lstat %s\n"); +static const char *msg_skip_cwd = N_("Refusing to remove current working directory\n"); +static const char *msg_would_skip_cwd = N_("Would refuse to remove current working directory\n"); + +enum color_clean { + CLEAN_COLOR_RESET = 0, + CLEAN_COLOR_PLAIN = 1, + CLEAN_COLOR_PROMPT = 2, + CLEAN_COLOR_HEADER = 3, + CLEAN_COLOR_HELP = 4, + CLEAN_COLOR_ERROR = 5 +}; + +static const char *color_interactive_slots[] = { + [CLEAN_COLOR_ERROR] = "error", + [CLEAN_COLOR_HEADER] = "header", + [CLEAN_COLOR_HELP] = "help", + [CLEAN_COLOR_PLAIN] = "plain", + [CLEAN_COLOR_PROMPT] = "prompt", + [CLEAN_COLOR_RESET] = "reset", +}; + +static int clean_use_color = -1; +static char clean_colors[][COLOR_MAXLEN] = { + [CLEAN_COLOR_ERROR] = GIT_COLOR_BOLD_RED, + [CLEAN_COLOR_HEADER] = GIT_COLOR_BOLD, + [CLEAN_COLOR_HELP] = GIT_COLOR_BOLD_RED, + [CLEAN_COLOR_PLAIN] = GIT_COLOR_NORMAL, + [CLEAN_COLOR_PROMPT] = GIT_COLOR_BOLD_BLUE, + [CLEAN_COLOR_RESET] = GIT_COLOR_RESET, +}; + +#define MENU_OPTS_SINGLETON 01 +#define MENU_OPTS_IMMEDIATE 02 +#define MENU_OPTS_LIST_ONLY 04 + +struct menu_opts { + const char *header; + const char *prompt; + int flags; +}; + +#define MENU_RETURN_NO_LOOP 10 + +struct menu_item { + char hotkey; + const char *title; + int selected; + int (*fn)(void); +}; + +enum menu_stuff_type { + MENU_STUFF_TYPE_STRING_LIST = 1, + MENU_STUFF_TYPE_MENU_ITEM +}; + +struct menu_stuff { + enum menu_stuff_type type; + int nr; + void *stuff; +}; + +define_list_config_array(color_interactive_slots); + +static int git_clean_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + const char *slot_name; + + if (starts_with(var, "column.")) + return git_column_config(var, value, "clean", &colopts); + + /* honors the color.interactive* config variables which also + applied in git-add--interactive and git-stash */ + if (!strcmp(var, "color.interactive")) { + clean_use_color = git_config_colorbool(var, value); + return 0; + } + if (skip_prefix(var, "color.interactive.", &slot_name)) { + int slot = LOOKUP_CONFIG(color_interactive_slots, slot_name); + if (slot < 0) + return 0; + if (!value) + return config_error_nonbool(var); + return color_parse(value, clean_colors[slot]); + } + + if (!strcmp(var, "clean.requireforce")) { + force = !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 *clean_get_color(enum color_clean ix) +{ + if (want_color(clean_use_color)) + return clean_colors[ix]; + return ""; +} + +static void clean_print_color(enum color_clean ix) +{ + printf("%s", clean_get_color(ix)); +} + +static int exclude_cb(const struct option *opt, const char *arg, int unset) +{ + struct string_list *exclude_list = opt->value; + BUG_ON_OPT_NEG(unset); + string_list_append(exclude_list, arg); + return 0; +} + +static int remove_dirs(struct strbuf *path, const char *prefix, int force_flag, + int dry_run, int quiet, int *dir_gone) +{ + DIR *dir; + struct strbuf quoted = STRBUF_INIT; + struct strbuf realpath = STRBUF_INIT; + struct strbuf real_ocwd = STRBUF_INIT; + struct dirent *e; + int res = 0, ret = 0, gone = 1, original_len = path->len, len; + struct string_list dels = STRING_LIST_INIT_DUP; + + *dir_gone = 1; + + if ((force_flag & REMOVE_DIR_KEEP_NESTED_GIT) && + is_nonbare_repository_dir(path)) { + if (!quiet) { + quote_path(path->buf, prefix, "ed, 0); + printf(dry_run ? _(msg_would_skip_git_dir) : _(msg_skip_git_dir), + quoted.buf); + } + + *dir_gone = 0; + goto out; + } + + dir = opendir(path->buf); + if (!dir) { + /* an empty dir could be removed even if it is unreadble */ + res = dry_run ? 0 : rmdir(path->buf); + if (res) { + int saved_errno = errno; + quote_path(path->buf, prefix, "ed, 0); + errno = saved_errno; + warning_errno(_(msg_warn_remove_failed), quoted.buf); + *dir_gone = 0; + } + ret = res; + goto out; + } + + strbuf_complete(path, '/'); + + len = path->len; + while ((e = readdir_skip_dot_and_dotdot(dir)) != NULL) { + struct stat st; + + strbuf_setlen(path, len); + strbuf_addstr(path, e->d_name); + if (lstat(path->buf, &st)) + warning_errno(_(msg_warn_lstat_failed), path->buf); + else if (S_ISDIR(st.st_mode)) { + if (remove_dirs(path, prefix, force_flag, dry_run, quiet, &gone)) + ret = 1; + if (gone) { + quote_path(path->buf, prefix, "ed, 0); + string_list_append(&dels, quoted.buf); + } else + *dir_gone = 0; + continue; + } else { + res = dry_run ? 0 : unlink(path->buf); + if (!res) { + quote_path(path->buf, prefix, "ed, 0); + string_list_append(&dels, quoted.buf); + } else { + int saved_errno = errno; + quote_path(path->buf, prefix, "ed, 0); + errno = saved_errno; + warning_errno(_(msg_warn_remove_failed), quoted.buf); + *dir_gone = 0; + ret = 1; + } + continue; + } + + /* path too long, stat fails, or non-directory still exists */ + *dir_gone = 0; + ret = 1; + break; + } + closedir(dir); + + strbuf_setlen(path, original_len); + + if (*dir_gone) { + /* + * Normalize path components in path->buf, e.g. change '\' to + * '/' on Windows. + */ + strbuf_realpath(&realpath, path->buf, 1); + + /* + * path and realpath are absolute; for comparison, we would + * like to transform startup_info->original_cwd to an absolute + * path too. + */ + if (startup_info->original_cwd) + strbuf_realpath(&real_ocwd, + startup_info->original_cwd, 1); + + if (!strbuf_cmp(&realpath, &real_ocwd)) { + printf("%s", dry_run ? _(msg_would_skip_cwd) : _(msg_skip_cwd)); + *dir_gone = 0; + } else { + res = dry_run ? 0 : rmdir(path->buf); + if (!res) + *dir_gone = 1; + else { + int saved_errno = errno; + quote_path(path->buf, prefix, "ed, 0); + errno = saved_errno; + warning_errno(_(msg_warn_remove_failed), quoted.buf); + *dir_gone = 0; + ret = 1; + } + } + } + + if (!*dir_gone && !quiet) { + int i; + for (i = 0; i < dels.nr; i++) + printf(dry_run ? _(msg_would_remove) : _(msg_remove), dels.items[i].string); + } +out: + strbuf_release(&realpath); + strbuf_release(&real_ocwd); + strbuf_release("ed); + string_list_clear(&dels, 0); + return ret; +} + +static void pretty_print_dels(void) +{ + struct string_list list = STRING_LIST_INIT_DUP; + struct string_list_item *item; + struct strbuf buf = STRBUF_INIT; + const char *qname; + struct column_options copts; + + for_each_string_list_item(item, &del_list) { + qname = quote_path(item->string, NULL, &buf, 0); + string_list_append(&list, qname); + } + + /* + * always enable column display, we only consult column.* + * about layout strategy and stuff + */ + colopts = (colopts & ~COL_ENABLE_MASK) | COL_ENABLED; + memset(&copts, 0, sizeof(copts)); + copts.indent = " "; + copts.padding = 2; + print_columns(&list, colopts, &copts); + strbuf_release(&buf); + string_list_clear(&list, 0); +} + +static void pretty_print_menus(struct string_list *menu_list) +{ + unsigned int local_colopts = 0; + struct column_options copts; + + local_colopts = COL_ENABLED | COL_ROW; + memset(&copts, 0, sizeof(copts)); + copts.indent = " "; + copts.padding = 2; + print_columns(menu_list, local_colopts, &copts); +} + +static void prompt_help_cmd(int singleton) +{ + clean_print_color(CLEAN_COLOR_HELP); + printf(singleton ? + _("Prompt help:\n" + "1 - select a numbered item\n" + "foo - select item based on unique prefix\n" + " - (empty) select nothing\n") : + _("Prompt help:\n" + "1 - select a single item\n" + "3-5 - select a range of items\n" + "2-3,6-9 - select multiple ranges\n" + "foo - select item based on unique prefix\n" + "-... - unselect specified items\n" + "* - choose all items\n" + " - (empty) finish selecting\n")); + clean_print_color(CLEAN_COLOR_RESET); +} + +/* + * display menu stuff with number prefix and hotkey highlight + */ +static void print_highlight_menu_stuff(struct menu_stuff *stuff, int **chosen) +{ + struct string_list menu_list = STRING_LIST_INIT_DUP; + struct strbuf menu = STRBUF_INIT; + struct menu_item *menu_item; + struct string_list_item *string_list_item; + int i; + + switch (stuff->type) { + default: + die("Bad type of menu_stuff when print menu"); + case MENU_STUFF_TYPE_MENU_ITEM: + menu_item = (struct menu_item *)stuff->stuff; + for (i = 0; i < stuff->nr; i++, menu_item++) { + const char *p; + int highlighted = 0; + + p = menu_item->title; + if ((*chosen)[i] < 0) + (*chosen)[i] = menu_item->selected ? 1 : 0; + strbuf_addf(&menu, "%s%2d: ", (*chosen)[i] ? "*" : " ", i+1); + for (; *p; p++) { + if (!highlighted && *p == menu_item->hotkey) { + strbuf_addstr(&menu, clean_get_color(CLEAN_COLOR_PROMPT)); + strbuf_addch(&menu, *p); + strbuf_addstr(&menu, clean_get_color(CLEAN_COLOR_RESET)); + highlighted = 1; + } else { + strbuf_addch(&menu, *p); + } + } + string_list_append(&menu_list, menu.buf); + strbuf_reset(&menu); + } + break; + case MENU_STUFF_TYPE_STRING_LIST: + i = 0; + for_each_string_list_item(string_list_item, (struct string_list *)stuff->stuff) { + if ((*chosen)[i] < 0) + (*chosen)[i] = 0; + strbuf_addf(&menu, "%s%2d: %s", + (*chosen)[i] ? "*" : " ", i+1, string_list_item->string); + string_list_append(&menu_list, menu.buf); + strbuf_reset(&menu); + i++; + } + break; + } + + pretty_print_menus(&menu_list); + + strbuf_release(&menu); + string_list_clear(&menu_list, 0); +} + +static int find_unique(const char *choice, struct menu_stuff *menu_stuff) +{ + struct menu_item *menu_item; + struct string_list_item *string_list_item; + int i, len, found = 0; + + len = strlen(choice); + switch (menu_stuff->type) { + default: + die("Bad type of menu_stuff when parse choice"); + case MENU_STUFF_TYPE_MENU_ITEM: + + menu_item = (struct menu_item *)menu_stuff->stuff; + for (i = 0; i < menu_stuff->nr; i++, menu_item++) { + if (len == 1 && *choice == menu_item->hotkey) { + found = i + 1; + break; + } + if (!strncasecmp(choice, menu_item->title, len)) { + if (found) { + if (len == 1) { + /* continue for hotkey matching */ + found = -1; + } else { + found = 0; + break; + } + } else { + found = i + 1; + } + } + } + break; + case MENU_STUFF_TYPE_STRING_LIST: + string_list_item = ((struct string_list *)menu_stuff->stuff)->items; + for (i = 0; i < menu_stuff->nr; i++, string_list_item++) { + if (!strncasecmp(choice, string_list_item->string, len)) { + if (found) { + found = 0; + break; + } + found = i + 1; + } + } + break; + } + return found; +} + +/* + * Parse user input, and return choice(s) for menu (menu_stuff). + * + * Input + * (for single choice) + * 1 - select a numbered item + * foo - select item based on menu title + * - (empty) select nothing + * + * (for multiple choice) + * 1 - select a single item + * 3-5 - select a range of items + * 2-3,6-9 - select multiple ranges + * foo - select item based on menu title + * -... - unselect specified items + * * - choose all items + * - (empty) finish selecting + * + * The parse result will be saved in array **chosen, and + * return number of total selections. + */ +static int parse_choice(struct menu_stuff *menu_stuff, + int is_single, + struct strbuf input, + int **chosen) +{ + struct strbuf **choice_list, **ptr; + int nr = 0; + int i; + + if (is_single) { + choice_list = strbuf_split_max(&input, '\n', 0); + } else { + char *p = input.buf; + do { + if (*p == ',') + *p = ' '; + } while (*p++); + choice_list = strbuf_split_max(&input, ' ', 0); + } + + for (ptr = choice_list; *ptr; ptr++) { + char *p; + int choose = 1; + int bottom = 0, top = 0; + int is_range, is_number; + + strbuf_trim(*ptr); + if (!(*ptr)->len) + continue; + + /* Input that begins with '-'; unchoose */ + if (*(*ptr)->buf == '-') { + choose = 0; + strbuf_remove((*ptr), 0, 1); + } + + is_range = 0; + is_number = 1; + for (p = (*ptr)->buf; *p; p++) { + if ('-' == *p) { + if (!is_range) { + is_range = 1; + is_number = 0; + } else { + is_number = 0; + is_range = 0; + break; + } + } else if (!isdigit(*p)) { + is_number = 0; + is_range = 0; + break; + } + } + + if (is_number) { + bottom = atoi((*ptr)->buf); + top = bottom; + } else if (is_range) { + bottom = atoi((*ptr)->buf); + /* a range can be specified like 5-7 or 5- */ + if (!*(strchr((*ptr)->buf, '-') + 1)) + top = menu_stuff->nr; + else + top = atoi(strchr((*ptr)->buf, '-') + 1); + } else if (!strcmp((*ptr)->buf, "*")) { + bottom = 1; + top = menu_stuff->nr; + } else { + bottom = find_unique((*ptr)->buf, menu_stuff); + top = bottom; + } + + if (top <= 0 || bottom <= 0 || top > menu_stuff->nr || bottom > top || + (is_single && bottom != top)) { + clean_print_color(CLEAN_COLOR_ERROR); + printf(_("Huh (%s)?\n"), (*ptr)->buf); + clean_print_color(CLEAN_COLOR_RESET); + continue; + } + + for (i = bottom; i <= top; i++) + (*chosen)[i-1] = choose; + } + + strbuf_list_free(choice_list); + + for (i = 0; i < menu_stuff->nr; i++) + nr += (*chosen)[i]; + return nr; +} + +/* + * Implement a git-add-interactive compatible UI, which is borrowed + * from add-interactive.c. + * + * Return value: + * + * - Return an array of integers + * - , and it is up to you to free the allocated memory. + * - The array ends with EOF. + * - If user pressed CTRL-D (i.e. EOF), no selection returned. + */ +static int *list_and_choose(struct menu_opts *opts, struct menu_stuff *stuff) +{ + struct strbuf choice = STRBUF_INIT; + int *chosen, *result; + int nr = 0; + int eof = 0; + int i; + + ALLOC_ARRAY(chosen, stuff->nr); + /* set chosen as uninitialized */ + for (i = 0; i < stuff->nr; i++) + chosen[i] = -1; + + for (;;) { + if (opts->header) { + printf_ln("%s%s%s", + clean_get_color(CLEAN_COLOR_HEADER), + _(opts->header), + clean_get_color(CLEAN_COLOR_RESET)); + } + + /* chosen will be initialized by print_highlight_menu_stuff */ + print_highlight_menu_stuff(stuff, &chosen); + + if (opts->flags & MENU_OPTS_LIST_ONLY) + break; + + if (opts->prompt) { + printf("%s%s%s%s", + clean_get_color(CLEAN_COLOR_PROMPT), + _(opts->prompt), + opts->flags & MENU_OPTS_SINGLETON ? "> " : ">> ", + clean_get_color(CLEAN_COLOR_RESET)); + } + + if (git_read_line_interactively(&choice) == EOF) { + eof = 1; + break; + } + + /* help for prompt */ + if (!strcmp(choice.buf, "?")) { + prompt_help_cmd(opts->flags & MENU_OPTS_SINGLETON); + continue; + } + + /* for a multiple-choice menu, press ENTER (empty) will return back */ + if (!(opts->flags & MENU_OPTS_SINGLETON) && !choice.len) + break; + + nr = parse_choice(stuff, + opts->flags & MENU_OPTS_SINGLETON, + choice, + &chosen); + + if (opts->flags & MENU_OPTS_SINGLETON) { + if (nr) + break; + } else if (opts->flags & MENU_OPTS_IMMEDIATE) { + break; + } + } + + if (eof) { + result = xmalloc(sizeof(int)); + *result = EOF; + } else { + int j = 0; + + /* + * recalculate nr, if return back from menu directly with + * default selections. + */ + if (!nr) { + for (i = 0; i < stuff->nr; i++) + nr += chosen[i]; + } + + CALLOC_ARRAY(result, st_add(nr, 1)); + for (i = 0; i < stuff->nr && j < nr; i++) { + if (chosen[i]) + result[j++] = i; + } + result[j] = EOF; + } + + free(chosen); + strbuf_release(&choice); + return result; +} + +static int clean_cmd(void) +{ + return MENU_RETURN_NO_LOOP; +} + +static int filter_by_patterns_cmd(void) +{ + struct dir_struct dir = DIR_INIT; + struct strbuf confirm = STRBUF_INIT; + struct strbuf **ignore_list; + struct string_list_item *item; + struct pattern_list *pl; + int changed = -1, i; + + for (;;) { + if (!del_list.nr) + break; + + if (changed) + pretty_print_dels(); + + clean_print_color(CLEAN_COLOR_PROMPT); + printf(_("Input ignore patterns>> ")); + clean_print_color(CLEAN_COLOR_RESET); + if (git_read_line_interactively(&confirm) == EOF) + putchar('\n'); + + /* quit filter_by_pattern mode if press ENTER or Ctrl-D */ + if (!confirm.len) + break; + + pl = add_pattern_list(&dir, EXC_CMDL, "manual exclude"); + ignore_list = strbuf_split_max(&confirm, ' ', 0); + + for (i = 0; ignore_list[i]; i++) { + strbuf_trim(ignore_list[i]); + if (!ignore_list[i]->len) + continue; + + add_pattern(ignore_list[i]->buf, "", 0, pl, -(i+1)); + } + + changed = 0; + for_each_string_list_item(item, &del_list) { + int dtype = DT_UNKNOWN; + + if (is_excluded(&dir, &the_index, item->string, &dtype)) { + *item->string = '\0'; + changed++; + } + } + + if (changed) { + string_list_remove_empty_items(&del_list, 0); + } else { + clean_print_color(CLEAN_COLOR_ERROR); + printf_ln(_("WARNING: Cannot find items matched by: %s"), confirm.buf); + clean_print_color(CLEAN_COLOR_RESET); + } + + strbuf_list_free(ignore_list); + dir_clear(&dir); + } + + strbuf_release(&confirm); + return 0; +} + +static int select_by_numbers_cmd(void) +{ + struct menu_opts menu_opts; + struct menu_stuff menu_stuff; + struct string_list_item *items; + int *chosen; + int i, j; + + menu_opts.header = NULL; + menu_opts.prompt = N_("Select items to delete"); + menu_opts.flags = 0; + + menu_stuff.type = MENU_STUFF_TYPE_STRING_LIST; + menu_stuff.stuff = &del_list; + menu_stuff.nr = del_list.nr; + + chosen = list_and_choose(&menu_opts, &menu_stuff); + items = del_list.items; + for (i = 0, j = 0; i < del_list.nr; i++) { + if (i < chosen[j]) { + *(items[i].string) = '\0'; + } else if (i == chosen[j]) { + /* delete selected item */ + j++; + continue; + } else { + /* end of chosen (chosen[j] == EOF), won't delete */ + *(items[i].string) = '\0'; + } + } + + string_list_remove_empty_items(&del_list, 0); + + free(chosen); + return 0; +} + +static int ask_each_cmd(void) +{ + struct strbuf confirm = STRBUF_INIT; + struct strbuf buf = STRBUF_INIT; + struct string_list_item *item; + const char *qname; + int changed = 0, eof = 0; + + for_each_string_list_item(item, &del_list) { + /* Ctrl-D should stop removing files */ + if (!eof) { + qname = quote_path(item->string, NULL, &buf, 0); + /* TRANSLATORS: Make sure to keep [y/N] as is */ + printf(_("Remove %s [y/N]? "), qname); + if (git_read_line_interactively(&confirm) == EOF) { + putchar('\n'); + eof = 1; + } + } + if (!confirm.len || strncasecmp(confirm.buf, "yes", confirm.len)) { + *item->string = '\0'; + changed++; + } + } + + if (changed) + string_list_remove_empty_items(&del_list, 0); + + strbuf_release(&buf); + strbuf_release(&confirm); + return MENU_RETURN_NO_LOOP; +} + +static int quit_cmd(void) +{ + string_list_clear(&del_list, 0); + printf(_("Bye.\n")); + return MENU_RETURN_NO_LOOP; +} + +static int help_cmd(void) +{ + clean_print_color(CLEAN_COLOR_HELP); + printf_ln(_( + "clean - start cleaning\n" + "filter by pattern - exclude items from deletion\n" + "select by numbers - select items to be deleted by numbers\n" + "ask each - confirm each deletion (like \"rm -i\")\n" + "quit - stop cleaning\n" + "help - this screen\n" + "? - help for prompt selection" + )); + clean_print_color(CLEAN_COLOR_RESET); + return 0; +} + +static void interactive_main_loop(void) +{ + while (del_list.nr) { + struct menu_opts menu_opts; + struct menu_stuff menu_stuff; + struct menu_item menus[] = { + {'c', "clean", 0, clean_cmd}, + {'f', "filter by pattern", 0, filter_by_patterns_cmd}, + {'s', "select by numbers", 0, select_by_numbers_cmd}, + {'a', "ask each", 0, ask_each_cmd}, + {'q', "quit", 0, quit_cmd}, + {'h', "help", 0, help_cmd}, + }; + int *chosen; + + menu_opts.header = N_("*** Commands ***"); + menu_opts.prompt = N_("What now"); + menu_opts.flags = MENU_OPTS_SINGLETON; + + menu_stuff.type = MENU_STUFF_TYPE_MENU_ITEM; + menu_stuff.stuff = menus; + menu_stuff.nr = sizeof(menus) / sizeof(struct menu_item); + + clean_print_color(CLEAN_COLOR_HEADER); + printf_ln(Q_("Would remove the following item:", + "Would remove the following items:", + del_list.nr)); + clean_print_color(CLEAN_COLOR_RESET); + + pretty_print_dels(); + + chosen = list_and_choose(&menu_opts, &menu_stuff); + + if (*chosen != EOF) { + int ret; + ret = menus[*chosen].fn(); + if (ret != MENU_RETURN_NO_LOOP) { + FREE_AND_NULL(chosen); + if (!del_list.nr) { + clean_print_color(CLEAN_COLOR_ERROR); + printf_ln(_("No more files to clean, exiting.")); + clean_print_color(CLEAN_COLOR_RESET); + break; + } + continue; + } + } else { + quit_cmd(); + } + + FREE_AND_NULL(chosen); + break; + } +} + +static void correct_untracked_entries(struct dir_struct *dir) +{ + int src, dst, ign; + + for (src = dst = ign = 0; src < dir->nr; src++) { + /* skip paths in ignored[] that cannot be inside entries[src] */ + while (ign < dir->ignored_nr && + 0 <= cmp_dir_entry(&dir->entries[src], &dir->ignored[ign])) + ign++; + + if (ign < dir->ignored_nr && + check_dir_entry_contains(dir->entries[src], dir->ignored[ign])) { + /* entries[src] contains an ignored path, so we drop it */ + free(dir->entries[src]); + } else { + struct dir_entry *ent = dir->entries[src++]; + + /* entries[src] does not contain an ignored path, so we keep it */ + dir->entries[dst++] = ent; + + /* then discard paths in entries[] contained inside entries[src] */ + while (src < dir->nr && + check_dir_entry_contains(ent, dir->entries[src])) + free(dir->entries[src++]); + + /* compensate for the outer loop's loop control */ + src--; + } + } + dir->nr = dst; +} + +int cmd_clean(int argc, const char **argv, const char *prefix) +{ + int i, res; + int dry_run = 0, remove_directories = 0, quiet = 0, ignored = 0; + int ignored_only = 0, config_set = 0, errors = 0, gone = 1; + int rm_flags = REMOVE_DIR_KEEP_NESTED_GIT; + struct strbuf abs_path = STRBUF_INIT; + struct dir_struct dir = DIR_INIT; + struct pathspec pathspec; + struct strbuf buf = STRBUF_INIT; + struct string_list exclude_list = STRING_LIST_INIT_NODUP; + struct pattern_list *pl; + struct string_list_item *item; + const char *qname; + struct option options[] = { + OPT__QUIET(&quiet, N_("do not print names of files removed")), + OPT__DRY_RUN(&dry_run, N_("dry run")), + OPT__FORCE(&force, N_("force"), PARSE_OPT_NOCOMPLETE), + OPT_BOOL('i', "interactive", &interactive, N_("interactive cleaning")), + OPT_BOOL('d', NULL, &remove_directories, + N_("remove whole directories")), + OPT_CALLBACK_F('e', "exclude", &exclude_list, N_("pattern"), + N_("add <pattern> to ignore rules"), PARSE_OPT_NONEG, exclude_cb), + OPT_BOOL('x', NULL, &ignored, N_("remove ignored files, too")), + OPT_BOOL('X', NULL, &ignored_only, + N_("remove only ignored files")), + OPT_END() + }; + + git_config(git_clean_config, NULL); + if (force < 0) + force = 0; + else + config_set = 1; + + argc = parse_options(argc, argv, prefix, options, builtin_clean_usage, + 0); + + if (!interactive && !dry_run && !force) { + if (config_set) + die(_("clean.requireForce set to true and neither -i, -n, nor -f given; " + "refusing to clean")); + else + die(_("clean.requireForce defaults to true and neither -i, -n, nor -f given;" + " refusing to clean")); + } + + if (force > 1) + rm_flags = 0; + else + dir.flags |= DIR_SKIP_NESTED_GIT; + + dir.flags |= DIR_SHOW_OTHER_DIRECTORIES; + + if (ignored && ignored_only) + die(_("-x and -X cannot be used together")); + if (!ignored) + setup_standard_excludes(&dir); + if (ignored_only) + dir.flags |= DIR_SHOW_IGNORED; + + if (argc) { + /* + * Remaining args implies pathspecs specified, and we should + * recurse within those. + */ + remove_directories = 1; + } + + if (remove_directories && !ignored_only) { + /* + * We need to know about ignored files too: + * + * If (ignored), then we will delete ignored files as well. + * + * If (!ignored), then even though we not are doing + * anything with ignored files, we need to know about them + * so that we can avoid deleting a directory of untracked + * files that also contains an ignored file within it. + * + * For the (!ignored) case, since we only need to avoid + * deleting ignored files, we can set + * DIR_SHOW_IGNORED_TOO_MODE_MATCHING in order to avoid + * recursing into a directory which is itself ignored. + */ + dir.flags |= DIR_SHOW_IGNORED_TOO; + if (!ignored) + dir.flags |= DIR_SHOW_IGNORED_TOO_MODE_MATCHING; + + /* + * Let the fill_directory() machinery know that we aren't + * just recursing to collect the ignored files; we want all + * the untracked ones so that we can delete them. (Note: + * we could also set DIR_KEEP_UNTRACKED_CONTENTS when + * ignored_only is true, since DIR_KEEP_UNTRACKED_CONTENTS + * only has effect in combination with DIR_SHOW_IGNORED_TOO. It makes + * the code clearer to exclude it, though. + */ + dir.flags |= DIR_KEEP_UNTRACKED_CONTENTS; + } + + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + if (repo_read_index(the_repository) < 0) + die(_("index file corrupt")); + + pl = add_pattern_list(&dir, EXC_CMDL, "--exclude option"); + for (i = 0; i < exclude_list.nr; i++) + add_pattern(exclude_list.items[i].string, "", 0, pl, -(i+1)); + + parse_pathspec(&pathspec, 0, + PATHSPEC_PREFER_CWD, + prefix, argv); + + fill_directory(&dir, &the_index, &pathspec); + correct_untracked_entries(&dir); + + for (i = 0; i < dir.nr; i++) { + struct dir_entry *ent = dir.entries[i]; + struct stat st; + const char *rel; + + if (!index_name_is_other(&the_index, ent->name, ent->len)) + continue; + + if (lstat(ent->name, &st)) + die_errno("Cannot lstat '%s'", ent->name); + + if (S_ISDIR(st.st_mode) && !remove_directories) + continue; + + rel = relative_path(ent->name, prefix, &buf); + string_list_append(&del_list, rel); + } + + dir_clear(&dir); + + if (interactive && del_list.nr > 0) + interactive_main_loop(); + + for_each_string_list_item(item, &del_list) { + struct stat st; + + strbuf_reset(&abs_path); + if (prefix) + strbuf_addstr(&abs_path, prefix); + + strbuf_addstr(&abs_path, item->string); + + /* + * we might have removed this as part of earlier + * recursive directory removal, so lstat() here could + * fail with ENOENT. + */ + if (lstat(abs_path.buf, &st)) + continue; + + if (S_ISDIR(st.st_mode)) { + if (remove_dirs(&abs_path, prefix, rm_flags, dry_run, quiet, &gone)) + errors++; + if (gone && !quiet) { + qname = quote_path(item->string, NULL, &buf, 0); + printf(dry_run ? _(msg_would_remove) : _(msg_remove), qname); + } + } else { + res = dry_run ? 0 : unlink(abs_path.buf); + if (res) { + int saved_errno = errno; + qname = quote_path(item->string, NULL, &buf, 0); + errno = saved_errno; + warning_errno(_(msg_warn_remove_failed), qname); + errors++; + } else if (!quiet) { + qname = quote_path(item->string, NULL, &buf, 0); + printf(dry_run ? _(msg_would_remove) : _(msg_remove), qname); + } + } + } + + strbuf_release(&abs_path); + strbuf_release(&buf); + string_list_clear(&del_list, 0); + string_list_clear(&exclude_list, 0); + clear_pathspec(&pathspec); + return (errors != 0); +} diff --git a/builtin/clone.c b/builtin/clone.c new file mode 100644 index 0000000..c6357af --- /dev/null +++ b/builtin/clone.c @@ -0,0 +1,1448 @@ +/* + * Builtin "git clone" + * + * Copyright (c) 2007 Kristian Høgsberg <krh@redhat.com>, + * 2008 Daniel Barkalow <barkalow@iabervon.org> + * Based on git-commit.sh by Junio C Hamano and Linus Torvalds + * + * Clone a repository into a different directory that does not yet exist. + */ + +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "abspath.h" +#include "advice.h" +#include "config.h" +#include "copy.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "lockfile.h" +#include "parse-options.h" +#include "fetch-pack.h" +#include "refs.h" +#include "refspec.h" +#include "object-file.h" +#include "object-store-ll.h" +#include "tree.h" +#include "tree-walk.h" +#include "unpack-trees.h" +#include "transport.h" +#include "strbuf.h" +#include "dir.h" +#include "dir-iterator.h" +#include "iterator.h" +#include "sigchain.h" +#include "branch.h" +#include "remote.h" +#include "run-command.h" +#include "setup.h" +#include "connected.h" +#include "packfile.h" +#include "path.h" +#include "pkt-line.h" +#include "list-objects-filter-options.h" +#include "hook.h" +#include "bundle.h" +#include "bundle-uri.h" + +/* + * Overall FIXMEs: + * - respect DB_ENVIRONMENT for .git/objects. + * + * Implementation notes: + * - dropping use-separate-remote and no-separate-remote compatibility + * + */ +static const char * const builtin_clone_usage[] = { + N_("git clone [<options>] [--] <repo> [<dir>]"), + NULL +}; + +static int option_no_checkout, option_bare, option_mirror, option_single_branch = -1; +static int option_local = -1, option_no_hardlinks, option_shared; +static int option_no_tags; +static int option_shallow_submodules; +static int option_reject_shallow = -1; /* unspecified */ +static int config_reject_shallow = -1; /* unspecified */ +static int deepen; +static char *option_template, *option_depth, *option_since; +static char *option_origin = NULL; +static char *remote_name = NULL; +static char *option_branch = NULL; +static struct string_list option_not = STRING_LIST_INIT_NODUP; +static const char *real_git_dir; +static char *option_upload_pack = "git-upload-pack"; +static int option_verbosity; +static int option_progress = -1; +static int option_sparse_checkout; +static enum transport_family family; +static struct string_list option_config = STRING_LIST_INIT_NODUP; +static struct string_list option_required_reference = STRING_LIST_INIT_NODUP; +static struct string_list option_optional_reference = STRING_LIST_INIT_NODUP; +static int option_dissociate; +static int max_jobs = -1; +static struct string_list option_recurse_submodules = STRING_LIST_INIT_NODUP; +static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT; +static int option_filter_submodules = -1; /* unspecified */ +static int config_filter_submodules = -1; /* unspecified */ +static struct string_list server_options = STRING_LIST_INIT_NODUP; +static int option_remote_submodules; +static const char *bundle_uri; + +static int recurse_submodules_cb(const struct option *opt, + const char *arg, int unset) +{ + if (unset) + string_list_clear((struct string_list *)opt->value, 0); + else if (arg) + string_list_append((struct string_list *)opt->value, arg); + else + string_list_append((struct string_list *)opt->value, + (const char *)opt->defval); + + return 0; +} + +static struct option builtin_clone_options[] = { + OPT__VERBOSITY(&option_verbosity), + OPT_BOOL(0, "progress", &option_progress, + N_("force progress reporting")), + OPT_BOOL(0, "reject-shallow", &option_reject_shallow, + N_("don't clone shallow repository")), + OPT_BOOL('n', "no-checkout", &option_no_checkout, + N_("don't create a checkout")), + OPT_BOOL(0, "bare", &option_bare, N_("create a bare repository")), + OPT_HIDDEN_BOOL(0, "naked", &option_bare, + N_("create a bare repository")), + OPT_BOOL(0, "mirror", &option_mirror, + N_("create a mirror repository (implies bare)")), + OPT_BOOL('l', "local", &option_local, + N_("to clone from a local repository")), + OPT_BOOL(0, "no-hardlinks", &option_no_hardlinks, + N_("don't use local hardlinks, always copy")), + OPT_BOOL('s', "shared", &option_shared, + N_("setup as shared repository")), + { OPTION_CALLBACK, 0, "recurse-submodules", &option_recurse_submodules, + N_("pathspec"), N_("initialize submodules in the clone"), + PARSE_OPT_OPTARG, recurse_submodules_cb, (intptr_t)"." }, + OPT_ALIAS(0, "recursive", "recurse-submodules"), + OPT_INTEGER('j', "jobs", &max_jobs, + N_("number of submodules cloned in parallel")), + OPT_STRING(0, "template", &option_template, N_("template-directory"), + N_("directory from which templates will be used")), + OPT_STRING_LIST(0, "reference", &option_required_reference, N_("repo"), + N_("reference repository")), + OPT_STRING_LIST(0, "reference-if-able", &option_optional_reference, + N_("repo"), N_("reference repository")), + OPT_BOOL(0, "dissociate", &option_dissociate, + N_("use --reference only while cloning")), + OPT_STRING('o', "origin", &option_origin, N_("name"), + N_("use <name> instead of 'origin' to track upstream")), + OPT_STRING('b', "branch", &option_branch, N_("branch"), + N_("checkout <branch> instead of the remote's HEAD")), + OPT_STRING('u', "upload-pack", &option_upload_pack, N_("path"), + N_("path to git-upload-pack on the remote")), + OPT_STRING(0, "depth", &option_depth, N_("depth"), + N_("create a shallow clone of that depth")), + OPT_STRING(0, "shallow-since", &option_since, N_("time"), + N_("create a shallow clone since a specific time")), + OPT_STRING_LIST(0, "shallow-exclude", &option_not, N_("revision"), + N_("deepen history of shallow clone, excluding rev")), + OPT_BOOL(0, "single-branch", &option_single_branch, + N_("clone only one branch, HEAD or --branch")), + OPT_BOOL(0, "no-tags", &option_no_tags, + N_("don't clone any tags, and make later fetches not to follow them")), + OPT_BOOL(0, "shallow-submodules", &option_shallow_submodules, + N_("any cloned submodules will be shallow")), + OPT_STRING(0, "separate-git-dir", &real_git_dir, N_("gitdir"), + N_("separate git dir from working tree")), + OPT_STRING_LIST('c', "config", &option_config, N_("key=value"), + N_("set config inside the new repository")), + OPT_STRING_LIST(0, "server-option", &server_options, + N_("server-specific"), N_("option to transmit")), + OPT_IPVERSION(&family), + OPT_PARSE_LIST_OBJECTS_FILTER(&filter_options), + OPT_BOOL(0, "also-filter-submodules", &option_filter_submodules, + N_("apply partial clone filters to submodules")), + OPT_BOOL(0, "remote-submodules", &option_remote_submodules, + N_("any cloned submodules will use their remote-tracking branch")), + OPT_BOOL(0, "sparse", &option_sparse_checkout, + N_("initialize sparse-checkout file to include only files at root")), + OPT_STRING(0, "bundle-uri", &bundle_uri, + N_("uri"), N_("a URI for downloading bundles before fetching from origin remote")), + OPT_END() +}; + +static const char *get_repo_path_1(struct strbuf *path, int *is_bundle) +{ + static char *suffix[] = { "/.git", "", ".git/.git", ".git" }; + static char *bundle_suffix[] = { ".bundle", "" }; + size_t baselen = path->len; + struct stat st; + int i; + + for (i = 0; i < ARRAY_SIZE(suffix); i++) { + strbuf_setlen(path, baselen); + strbuf_addstr(path, suffix[i]); + if (stat(path->buf, &st)) + continue; + if (S_ISDIR(st.st_mode) && is_git_directory(path->buf)) { + *is_bundle = 0; + return path->buf; + } else if (S_ISREG(st.st_mode) && st.st_size > 8) { + /* Is it a "gitfile"? */ + char signature[8]; + const char *dst; + int len, fd = open(path->buf, O_RDONLY); + if (fd < 0) + continue; + len = read_in_full(fd, signature, 8); + close(fd); + if (len != 8 || strncmp(signature, "gitdir: ", 8)) + continue; + dst = read_gitfile(path->buf); + if (dst) { + *is_bundle = 0; + return dst; + } + } + } + + for (i = 0; i < ARRAY_SIZE(bundle_suffix); i++) { + strbuf_setlen(path, baselen); + strbuf_addstr(path, bundle_suffix[i]); + if (!stat(path->buf, &st) && S_ISREG(st.st_mode)) { + *is_bundle = 1; + return path->buf; + } + } + + return NULL; +} + +static char *get_repo_path(const char *repo, int *is_bundle) +{ + struct strbuf path = STRBUF_INIT; + const char *raw; + char *canon; + + strbuf_addstr(&path, repo); + raw = get_repo_path_1(&path, is_bundle); + canon = raw ? absolute_pathdup(raw) : NULL; + strbuf_release(&path); + return canon; +} + +static int add_one_reference(struct string_list_item *item, void *cb_data) +{ + struct strbuf err = STRBUF_INIT; + int *required = cb_data; + char *ref_git = compute_alternate_path(item->string, &err); + + if (!ref_git) { + if (*required) + die("%s", err.buf); + else + fprintf(stderr, + _("info: Could not add alternate for '%s': %s\n"), + item->string, err.buf); + } else { + struct strbuf sb = STRBUF_INIT; + strbuf_addf(&sb, "%s/objects", ref_git); + add_to_alternates_file(sb.buf); + strbuf_release(&sb); + } + + strbuf_release(&err); + free(ref_git); + return 0; +} + +static void setup_reference(void) +{ + int required = 1; + for_each_string_list(&option_required_reference, + add_one_reference, &required); + required = 0; + for_each_string_list(&option_optional_reference, + add_one_reference, &required); +} + +static void copy_alternates(struct strbuf *src, const char *src_repo) +{ + /* + * Read from the source objects/info/alternates file + * and copy the entries to corresponding file in the + * destination repository with add_to_alternates_file(). + * Both src and dst have "$path/objects/info/alternates". + * + * Instead of copying bit-for-bit from the original, + * we need to append to existing one so that the already + * created entry via "clone -s" is not lost, and also + * to turn entries with paths relative to the original + * absolute, so that they can be used in the new repository. + */ + FILE *in = xfopen(src->buf, "r"); + struct strbuf line = STRBUF_INIT; + + while (strbuf_getline(&line, in) != EOF) { + char *abs_path; + if (!line.len || line.buf[0] == '#') + continue; + if (is_absolute_path(line.buf)) { + add_to_alternates_file(line.buf); + continue; + } + abs_path = mkpathdup("%s/objects/%s", src_repo, line.buf); + if (!normalize_path_copy(abs_path, abs_path)) + add_to_alternates_file(abs_path); + else + warning("skipping invalid relative alternate: %s/%s", + src_repo, line.buf); + free(abs_path); + } + strbuf_release(&line); + fclose(in); +} + +static void mkdir_if_missing(const char *pathname, mode_t mode) +{ + struct stat st; + + if (!mkdir(pathname, mode)) + return; + + if (errno != EEXIST) + die_errno(_("failed to create directory '%s'"), pathname); + else if (stat(pathname, &st)) + die_errno(_("failed to stat '%s'"), pathname); + else if (!S_ISDIR(st.st_mode)) + die(_("%s exists and is not a directory"), pathname); +} + +static void copy_or_link_directory(struct strbuf *src, struct strbuf *dest, + const char *src_repo) +{ + int src_len, dest_len; + struct dir_iterator *iter; + int iter_status; + struct strbuf realpath = STRBUF_INIT; + + mkdir_if_missing(dest->buf, 0777); + + iter = dir_iterator_begin(src->buf, DIR_ITERATOR_PEDANTIC); + + if (!iter) { + if (errno == ENOTDIR) { + int saved_errno = errno; + struct stat st; + + if (!lstat(src->buf, &st) && S_ISLNK(st.st_mode)) + die(_("'%s' is a symlink, refusing to clone with --local"), + src->buf); + errno = saved_errno; + } + die_errno(_("failed to start iterator over '%s'"), src->buf); + } + + strbuf_addch(src, '/'); + src_len = src->len; + strbuf_addch(dest, '/'); + dest_len = dest->len; + + while ((iter_status = dir_iterator_advance(iter)) == ITER_OK) { + strbuf_setlen(src, src_len); + strbuf_addstr(src, iter->relative_path); + strbuf_setlen(dest, dest_len); + strbuf_addstr(dest, iter->relative_path); + + if (S_ISLNK(iter->st.st_mode)) + die(_("symlink '%s' exists, refusing to clone with --local"), + iter->relative_path); + + if (S_ISDIR(iter->st.st_mode)) { + mkdir_if_missing(dest->buf, 0777); + continue; + } + + /* Files that cannot be copied bit-for-bit... */ + if (!fspathcmp(iter->relative_path, "info/alternates")) { + copy_alternates(src, src_repo); + continue; + } + + if (unlink(dest->buf) && errno != ENOENT) + die_errno(_("failed to unlink '%s'"), dest->buf); + if (!option_no_hardlinks) { + strbuf_realpath(&realpath, src->buf, 1); + if (!link(realpath.buf, dest->buf)) + continue; + if (option_local > 0) + die_errno(_("failed to create link '%s'"), dest->buf); + option_no_hardlinks = 1; + } + if (copy_file_with_time(dest->buf, src->buf, 0666)) + die_errno(_("failed to copy file to '%s'"), dest->buf); + } + + if (iter_status != ITER_DONE) { + strbuf_setlen(src, src_len); + die(_("failed to iterate over '%s'"), src->buf); + } + + strbuf_release(&realpath); +} + +static void clone_local(const char *src_repo, const char *dest_repo) +{ + if (option_shared) { + struct strbuf alt = STRBUF_INIT; + get_common_dir(&alt, src_repo); + strbuf_addstr(&alt, "/objects"); + add_to_alternates_file(alt.buf); + strbuf_release(&alt); + } else { + struct strbuf src = STRBUF_INIT; + struct strbuf dest = STRBUF_INIT; + get_common_dir(&src, src_repo); + get_common_dir(&dest, dest_repo); + strbuf_addstr(&src, "/objects"); + strbuf_addstr(&dest, "/objects"); + copy_or_link_directory(&src, &dest, src_repo); + strbuf_release(&src); + strbuf_release(&dest); + } + + if (0 <= option_verbosity) + fprintf(stderr, _("done.\n")); +} + +static const char *junk_work_tree; +static int junk_work_tree_flags; +static const char *junk_git_dir; +static int junk_git_dir_flags; +static enum { + JUNK_LEAVE_NONE, + JUNK_LEAVE_REPO, + JUNK_LEAVE_ALL +} junk_mode = JUNK_LEAVE_NONE; + +static const char junk_leave_repo_msg[] = +N_("Clone succeeded, but checkout failed.\n" + "You can inspect what was checked out with 'git status'\n" + "and retry with 'git restore --source=HEAD :/'\n"); + +static void remove_junk(void) +{ + struct strbuf sb = STRBUF_INIT; + + switch (junk_mode) { + case JUNK_LEAVE_REPO: + warning("%s", _(junk_leave_repo_msg)); + /* fall-through */ + case JUNK_LEAVE_ALL: + return; + default: + /* proceed to removal */ + break; + } + + if (junk_git_dir) { + strbuf_addstr(&sb, junk_git_dir); + remove_dir_recursively(&sb, junk_git_dir_flags); + strbuf_reset(&sb); + } + if (junk_work_tree) { + strbuf_addstr(&sb, junk_work_tree); + remove_dir_recursively(&sb, junk_work_tree_flags); + } + strbuf_release(&sb); +} + +static void remove_junk_on_signal(int signo) +{ + remove_junk(); + sigchain_pop(signo); + raise(signo); +} + +static struct ref *find_remote_branch(const struct ref *refs, const char *branch) +{ + struct ref *ref; + struct strbuf head = STRBUF_INIT; + strbuf_addstr(&head, "refs/heads/"); + strbuf_addstr(&head, branch); + ref = find_ref_by_name(refs, head.buf); + strbuf_release(&head); + + if (ref) + return ref; + + strbuf_addstr(&head, "refs/tags/"); + strbuf_addstr(&head, branch); + ref = find_ref_by_name(refs, head.buf); + strbuf_release(&head); + + return ref; +} + +static struct ref *wanted_peer_refs(const struct ref *refs, + struct refspec *refspec) +{ + struct ref *head = copy_ref(find_ref_by_name(refs, "HEAD")); + struct ref *local_refs = head; + struct ref **tail = head ? &head->next : &local_refs; + + if (option_single_branch) { + struct ref *remote_head = NULL; + + if (!option_branch) + remote_head = guess_remote_head(head, refs, 0); + else { + local_refs = NULL; + tail = &local_refs; + remote_head = copy_ref(find_remote_branch(refs, option_branch)); + } + + if (!remote_head && option_branch) + warning(_("Could not find remote branch %s to clone."), + option_branch); + else { + int i; + for (i = 0; i < refspec->nr; i++) + get_fetch_map(remote_head, &refspec->items[i], + &tail, 0); + + /* if --branch=tag, pull the requested tag explicitly */ + get_fetch_map(remote_head, tag_refspec, &tail, 0); + } + free_refs(remote_head); + } else { + int i; + for (i = 0; i < refspec->nr; i++) + get_fetch_map(refs, &refspec->items[i], &tail, 0); + } + + if (!option_mirror && !option_single_branch && !option_no_tags) + get_fetch_map(refs, tag_refspec, &tail, 0); + + return local_refs; +} + +static void write_remote_refs(const struct ref *local_refs) +{ + const struct ref *r; + + struct ref_transaction *t; + struct strbuf err = STRBUF_INIT; + + t = ref_transaction_begin(&err); + if (!t) + die("%s", err.buf); + + for (r = local_refs; r; r = r->next) { + if (!r->peer_ref) + continue; + if (ref_transaction_create(t, r->peer_ref->name, &r->old_oid, + 0, NULL, &err)) + die("%s", err.buf); + } + + if (initial_ref_transaction_commit(t, &err)) + die("%s", err.buf); + + strbuf_release(&err); + ref_transaction_free(t); +} + +static void write_followtags(const struct ref *refs, const char *msg) +{ + const struct ref *ref; + for (ref = refs; ref; ref = ref->next) { + if (!starts_with(ref->name, "refs/tags/")) + continue; + if (ends_with(ref->name, "^{}")) + continue; + if (!repo_has_object_file_with_flags(the_repository, &ref->old_oid, + OBJECT_INFO_QUICK | + OBJECT_INFO_SKIP_FETCH_OBJECT)) + continue; + update_ref(msg, ref->name, &ref->old_oid, NULL, 0, + UPDATE_REFS_DIE_ON_ERR); + } +} + +static const struct object_id *iterate_ref_map(void *cb_data) +{ + struct ref **rm = cb_data; + struct ref *ref = *rm; + + /* + * Skip anything missing a peer_ref, which we are not + * actually going to write a ref for. + */ + while (ref && !ref->peer_ref) + ref = ref->next; + if (!ref) + return NULL; + + *rm = ref->next; + return &ref->old_oid; +} + +static void update_remote_refs(const struct ref *refs, + const struct ref *mapped_refs, + const struct ref *remote_head_points_at, + const char *branch_top, + const char *msg, + struct transport *transport, + int check_connectivity) +{ + const struct ref *rm = mapped_refs; + + if (check_connectivity) { + struct check_connected_options opt = CHECK_CONNECTED_INIT; + + opt.transport = transport; + opt.progress = transport->progress; + + if (check_connected(iterate_ref_map, &rm, &opt)) + die(_("remote did not send all necessary objects")); + } + + if (refs) { + write_remote_refs(mapped_refs); + if (option_single_branch && !option_no_tags) + write_followtags(refs, msg); + } + + if (remote_head_points_at && !option_bare) { + struct strbuf head_ref = STRBUF_INIT; + strbuf_addstr(&head_ref, branch_top); + strbuf_addstr(&head_ref, "HEAD"); + if (create_symref(head_ref.buf, + remote_head_points_at->peer_ref->name, + msg) < 0) + die(_("unable to update %s"), head_ref.buf); + strbuf_release(&head_ref); + } +} + +static void update_head(const struct ref *our, const struct ref *remote, + const char *unborn, const char *msg) +{ + const char *head; + if (our && skip_prefix(our->name, "refs/heads/", &head)) { + /* Local default branch link */ + if (create_symref("HEAD", our->name, NULL) < 0) + die(_("unable to update HEAD")); + if (!option_bare) { + update_ref(msg, "HEAD", &our->old_oid, NULL, 0, + UPDATE_REFS_DIE_ON_ERR); + install_branch_config(0, head, remote_name, our->name); + } + } else if (our) { + struct commit *c = lookup_commit_reference(the_repository, + &our->old_oid); + /* --branch specifies a non-branch (i.e. tags), detach HEAD */ + update_ref(msg, "HEAD", &c->object.oid, NULL, REF_NO_DEREF, + UPDATE_REFS_DIE_ON_ERR); + } else if (remote) { + /* + * We know remote HEAD points to a non-branch, or + * HEAD points to a branch but we don't know which one. + * Detach HEAD in all these cases. + */ + update_ref(msg, "HEAD", &remote->old_oid, NULL, REF_NO_DEREF, + UPDATE_REFS_DIE_ON_ERR); + } else if (unborn && skip_prefix(unborn, "refs/heads/", &head)) { + /* + * Unborn head from remote; same as "our" case above except + * that we have no ref to update. + */ + if (create_symref("HEAD", unborn, NULL) < 0) + die(_("unable to update HEAD")); + if (!option_bare) + install_branch_config(0, head, remote_name, unborn); + } +} + +static int git_sparse_checkout_init(const char *repo) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + int result = 0; + strvec_pushl(&cmd.args, "-C", repo, "sparse-checkout", "set", NULL); + + /* + * We must apply the setting in the current process + * for the later checkout to use the sparse-checkout file. + */ + core_apply_sparse_checkout = 1; + + cmd.git_cmd = 1; + if (run_command(&cmd)) { + error(_("failed to initialize sparse-checkout")); + result = 1; + } + + return result; +} + +static int checkout(int submodule_progress, int filter_submodules) +{ + struct object_id oid; + char *head; + struct lock_file lock_file = LOCK_INIT; + struct unpack_trees_options opts; + struct tree *tree; + struct tree_desc t; + int err = 0; + + if (option_no_checkout) + return 0; + + head = resolve_refdup("HEAD", RESOLVE_REF_READING, &oid, NULL); + if (!head) { + warning(_("remote HEAD refers to nonexistent ref, " + "unable to checkout")); + return 0; + } + if (!strcmp(head, "HEAD")) { + if (advice_enabled(ADVICE_DETACHED_HEAD)) + detach_advice(oid_to_hex(&oid)); + FREE_AND_NULL(head); + } else { + if (!starts_with(head, "refs/heads/")) + die(_("HEAD not found below refs/heads!")); + } + + /* We need to be in the new work tree for the checkout */ + setup_work_tree(); + + repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR); + + memset(&opts, 0, sizeof opts); + opts.update = 1; + opts.merge = 1; + opts.clone = 1; + opts.preserve_ignored = 0; + opts.fn = oneway_merge; + opts.verbose_update = (option_verbosity >= 0); + opts.src_index = &the_index; + opts.dst_index = &the_index; + init_checkout_metadata(&opts.meta, head, &oid, NULL); + + tree = parse_tree_indirect(&oid); + if (!tree) + die(_("unable to parse commit %s"), oid_to_hex(&oid)); + parse_tree(tree); + init_tree_desc(&t, tree->buffer, tree->size); + if (unpack_trees(1, &t, &opts) < 0) + die(_("unable to checkout working tree")); + + free(head); + + if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK)) + die(_("unable to write new index file")); + + err |= run_hooks_l("post-checkout", oid_to_hex(null_oid()), + oid_to_hex(&oid), "1", NULL); + + if (!err && (option_recurse_submodules.nr > 0)) { + struct child_process cmd = CHILD_PROCESS_INIT; + strvec_pushl(&cmd.args, "submodule", "update", "--require-init", + "--recursive", NULL); + + if (option_shallow_submodules == 1) + strvec_push(&cmd.args, "--depth=1"); + + if (max_jobs != -1) + strvec_pushf(&cmd.args, "--jobs=%d", max_jobs); + + if (submodule_progress) + strvec_push(&cmd.args, "--progress"); + + if (option_verbosity < 0) + strvec_push(&cmd.args, "--quiet"); + + if (option_remote_submodules) { + strvec_push(&cmd.args, "--remote"); + strvec_push(&cmd.args, "--no-fetch"); + } + + if (filter_submodules && filter_options.choice) + strvec_pushf(&cmd.args, "--filter=%s", + expand_list_objects_filter_spec(&filter_options)); + + if (option_single_branch >= 0) + strvec_push(&cmd.args, option_single_branch ? + "--single-branch" : + "--no-single-branch"); + + cmd.git_cmd = 1; + err = run_command(&cmd); + } + + return err; +} + +static int git_clone_config(const char *k, const char *v, + const struct config_context *ctx, void *cb) +{ + if (!strcmp(k, "clone.defaultremotename")) { + free(remote_name); + remote_name = xstrdup(v); + } + if (!strcmp(k, "clone.rejectshallow")) + config_reject_shallow = git_config_bool(k, v); + if (!strcmp(k, "clone.filtersubmodules")) + config_filter_submodules = git_config_bool(k, v); + + return git_default_config(k, v, ctx, cb); +} + +static int write_one_config(const char *key, const char *value, + const struct config_context *ctx, + void *data) +{ + /* + * give git_clone_config a chance to write config values back to the + * environment, since git_config_set_multivar_gently only deals with + * config-file writes + */ + int apply_failed = git_clone_config(key, value, ctx, data); + if (apply_failed) + return apply_failed; + + return git_config_set_multivar_gently(key, + value ? value : "true", + CONFIG_REGEX_NONE, 0); +} + +static void write_config(struct string_list *config) +{ + int i; + + for (i = 0; i < config->nr; i++) { + if (git_config_parse_parameter(config->items[i].string, + write_one_config, NULL) < 0) + die(_("unable to write parameters to config file")); + } +} + +static void write_refspec_config(const char *src_ref_prefix, + const struct ref *our_head_points_at, + const struct ref *remote_head_points_at, + struct strbuf *branch_top) +{ + struct strbuf key = STRBUF_INIT; + struct strbuf value = STRBUF_INIT; + + if (option_mirror || !option_bare) { + if (option_single_branch && !option_mirror) { + if (option_branch) { + if (starts_with(our_head_points_at->name, "refs/tags/")) + strbuf_addf(&value, "+%s:%s", our_head_points_at->name, + our_head_points_at->name); + else + strbuf_addf(&value, "+%s:%s%s", our_head_points_at->name, + branch_top->buf, option_branch); + } else if (remote_head_points_at) { + const char *head = remote_head_points_at->name; + if (!skip_prefix(head, "refs/heads/", &head)) + BUG("remote HEAD points at non-head?"); + + strbuf_addf(&value, "+%s:%s%s", remote_head_points_at->name, + branch_top->buf, head); + } + /* + * otherwise, the next "git fetch" will + * simply fetch from HEAD without updating + * any remote-tracking branch, which is what + * we want. + */ + } else { + strbuf_addf(&value, "+%s*:%s*", src_ref_prefix, branch_top->buf); + } + /* Configure the remote */ + if (value.len) { + strbuf_addf(&key, "remote.%s.fetch", remote_name); + git_config_set_multivar(key.buf, value.buf, "^$", 0); + strbuf_reset(&key); + + if (option_mirror) { + strbuf_addf(&key, "remote.%s.mirror", remote_name); + git_config_set(key.buf, "true"); + strbuf_reset(&key); + } + } + } + + strbuf_release(&key); + strbuf_release(&value); +} + +static void dissociate_from_references(void) +{ + char *alternates = git_pathdup("objects/info/alternates"); + + if (!access(alternates, F_OK)) { + struct child_process cmd = CHILD_PROCESS_INIT; + + cmd.git_cmd = 1; + cmd.no_stdin = 1; + strvec_pushl(&cmd.args, "repack", "-a", "-d", NULL); + if (run_command(&cmd)) + die(_("cannot repack to clean up")); + if (unlink(alternates) && errno != ENOENT) + die_errno(_("cannot unlink temporary alternates file")); + } + free(alternates); +} + +static int path_exists(const char *path) +{ + struct stat sb; + return !stat(path, &sb); +} + +int cmd_clone(int argc, const char **argv, const char *prefix) +{ + int is_bundle = 0, is_local; + int reject_shallow = 0; + const char *repo_name, *repo, *work_tree, *git_dir; + char *repo_to_free = NULL; + char *path = NULL, *dir, *display_repo = NULL; + int dest_exists, real_dest_exists = 0; + const struct ref *refs, *remote_head; + struct ref *remote_head_points_at = NULL; + const struct ref *our_head_points_at; + char *unborn_head = NULL; + struct ref *mapped_refs = NULL; + const struct ref *ref; + struct strbuf key = STRBUF_INIT; + struct strbuf branch_top = STRBUF_INIT, reflog_msg = STRBUF_INIT; + struct transport *transport = NULL; + const char *src_ref_prefix = "refs/heads/"; + struct remote *remote; + int err = 0, complete_refs_before_fetch = 1; + int submodule_progress; + int filter_submodules = 0; + int hash_algo; + const int do_not_override_repo_unix_permissions = -1; + + struct transport_ls_refs_options transport_ls_refs_options = + TRANSPORT_LS_REFS_OPTIONS_INIT; + + packet_trace_identity("clone"); + + git_config(git_clone_config, NULL); + + argc = parse_options(argc, argv, prefix, builtin_clone_options, + builtin_clone_usage, 0); + + if (argc > 2) + usage_msg_opt(_("Too many arguments."), + builtin_clone_usage, builtin_clone_options); + + if (argc == 0) + usage_msg_opt(_("You must specify a repository to clone."), + builtin_clone_usage, builtin_clone_options); + + if (option_depth || option_since || option_not.nr) + deepen = 1; + if (option_single_branch == -1) + option_single_branch = deepen ? 1 : 0; + + if (option_mirror) + option_bare = 1; + + if (option_bare) { + if (real_git_dir) + die(_("options '%s' and '%s' cannot be used together"), "--bare", "--separate-git-dir"); + option_no_checkout = 1; + } + + if (bundle_uri && deepen) + die(_("--bundle-uri is incompatible with --depth, --shallow-since, and --shallow-exclude")); + + repo_name = argv[0]; + + path = get_repo_path(repo_name, &is_bundle); + if (path) { + FREE_AND_NULL(path); + repo = repo_to_free = absolute_pathdup(repo_name); + } else if (strchr(repo_name, ':')) { + repo = repo_name; + display_repo = transport_anonymize_url(repo); + } else + die(_("repository '%s' does not exist"), repo_name); + + /* no need to be strict, transport_set_option() will validate it again */ + if (option_depth && atoi(option_depth) < 1) + die(_("depth %s is not a positive number"), option_depth); + + if (argc == 2) + dir = xstrdup(argv[1]); + else + dir = git_url_basename(repo_name, is_bundle, option_bare); + strip_dir_trailing_slashes(dir); + + dest_exists = path_exists(dir); + if (dest_exists && !is_empty_dir(dir)) + die(_("destination path '%s' already exists and is not " + "an empty directory."), dir); + + if (real_git_dir) { + real_dest_exists = path_exists(real_git_dir); + if (real_dest_exists && !is_empty_dir(real_git_dir)) + die(_("repository path '%s' already exists and is not " + "an empty directory."), real_git_dir); + } + + + strbuf_addf(&reflog_msg, "clone: from %s", + display_repo ? display_repo : repo); + free(display_repo); + + if (option_bare) + work_tree = NULL; + else { + work_tree = getenv("GIT_WORK_TREE"); + if (work_tree && path_exists(work_tree)) + die(_("working tree '%s' already exists."), work_tree); + } + + if (option_bare || work_tree) + git_dir = xstrdup(dir); + else { + work_tree = dir; + git_dir = mkpathdup("%s/.git", dir); + } + + atexit(remove_junk); + sigchain_push_common(remove_junk_on_signal); + + if (!option_bare) { + if (safe_create_leading_directories_const(work_tree) < 0) + die_errno(_("could not create leading directories of '%s'"), + work_tree); + if (dest_exists) + junk_work_tree_flags |= REMOVE_DIR_KEEP_TOPLEVEL; + else if (mkdir(work_tree, 0777)) + die_errno(_("could not create work tree dir '%s'"), + work_tree); + junk_work_tree = work_tree; + set_git_work_tree(work_tree); + } + + if (real_git_dir) { + if (real_dest_exists) + junk_git_dir_flags |= REMOVE_DIR_KEEP_TOPLEVEL; + junk_git_dir = real_git_dir; + } else { + if (dest_exists) + junk_git_dir_flags |= REMOVE_DIR_KEEP_TOPLEVEL; + junk_git_dir = git_dir; + } + if (safe_create_leading_directories_const(git_dir) < 0) + die(_("could not create leading directories of '%s'"), git_dir); + + if (0 <= option_verbosity) { + if (option_bare) + fprintf(stderr, _("Cloning into bare repository '%s'...\n"), dir); + else + fprintf(stderr, _("Cloning into '%s'...\n"), dir); + } + + if (option_recurse_submodules.nr > 0) { + struct string_list_item *item; + struct strbuf sb = STRBUF_INIT; + int val; + + /* remove duplicates */ + string_list_sort(&option_recurse_submodules); + string_list_remove_duplicates(&option_recurse_submodules, 0); + + /* + * NEEDSWORK: In a multi-working-tree world, this needs to be + * set in the per-worktree config. + */ + for_each_string_list_item(item, &option_recurse_submodules) { + strbuf_addf(&sb, "submodule.active=%s", + item->string); + string_list_append(&option_config, + strbuf_detach(&sb, NULL)); + } + + if (!git_config_get_bool("submodule.stickyRecursiveClone", &val) && + val) + string_list_append(&option_config, "submodule.recurse=true"); + + if (option_required_reference.nr && + option_optional_reference.nr) + die(_("clone --recursive is not compatible with " + "both --reference and --reference-if-able")); + else if (option_required_reference.nr) { + string_list_append(&option_config, + "submodule.alternateLocation=superproject"); + string_list_append(&option_config, + "submodule.alternateErrorStrategy=die"); + } else if (option_optional_reference.nr) { + string_list_append(&option_config, + "submodule.alternateLocation=superproject"); + string_list_append(&option_config, + "submodule.alternateErrorStrategy=info"); + } + } + + init_db(git_dir, real_git_dir, option_template, GIT_HASH_UNKNOWN, NULL, + do_not_override_repo_unix_permissions, INIT_DB_QUIET); + + if (real_git_dir) { + free((char *)git_dir); + git_dir = real_git_dir; + } + + /* + * additional config can be injected with -c, make sure it's included + * after init_db, which clears the entire config environment. + */ + write_config(&option_config); + + /* + * re-read config after init_db and write_config to pick up any config + * injected by --template and --config, respectively. + */ + git_config(git_clone_config, NULL); + + /* + * If option_reject_shallow is specified from CLI option, + * ignore config_reject_shallow from git_clone_config. + */ + if (config_reject_shallow != -1) + reject_shallow = config_reject_shallow; + if (option_reject_shallow != -1) + reject_shallow = option_reject_shallow; + + /* + * If option_filter_submodules is specified from CLI option, + * ignore config_filter_submodules from git_clone_config. + */ + if (config_filter_submodules != -1) + filter_submodules = config_filter_submodules; + if (option_filter_submodules != -1) + filter_submodules = option_filter_submodules; + + /* + * Exit if the user seems to be doing something silly with submodule + * filter flags (but not with filter configs, as those should be + * set-and-forget). + */ + if (option_filter_submodules > 0 && !filter_options.choice) + die(_("the option '%s' requires '%s'"), + "--also-filter-submodules", "--filter"); + if (option_filter_submodules > 0 && !option_recurse_submodules.nr) + die(_("the option '%s' requires '%s'"), + "--also-filter-submodules", "--recurse-submodules"); + + /* + * apply the remote name provided by --origin only after this second + * call to git_config, to ensure it overrides all config-based values. + */ + if (option_origin) { + free(remote_name); + remote_name = xstrdup(option_origin); + } + + if (!remote_name) + remote_name = xstrdup("origin"); + + if (!valid_remote_name(remote_name)) + die(_("'%s' is not a valid remote name"), remote_name); + + if (option_bare) { + if (option_mirror) + src_ref_prefix = "refs/"; + strbuf_addstr(&branch_top, src_ref_prefix); + + git_config_set("core.bare", "true"); + } else { + strbuf_addf(&branch_top, "refs/remotes/%s/", remote_name); + } + + strbuf_addf(&key, "remote.%s.url", remote_name); + git_config_set(key.buf, repo); + strbuf_reset(&key); + + if (option_no_tags) { + strbuf_addf(&key, "remote.%s.tagOpt", remote_name); + git_config_set(key.buf, "--no-tags"); + strbuf_reset(&key); + } + + if (option_required_reference.nr || option_optional_reference.nr) + setup_reference(); + + if (option_sparse_checkout && git_sparse_checkout_init(dir)) + return 1; + + remote = remote_get(remote_name); + + refspec_appendf(&remote->fetch, "+%s*:%s*", src_ref_prefix, + branch_top.buf); + + path = get_repo_path(remote->url[0], &is_bundle); + is_local = option_local != 0 && path && !is_bundle; + if (is_local) { + if (option_depth) + warning(_("--depth is ignored in local clones; use file:// instead.")); + if (option_since) + warning(_("--shallow-since is ignored in local clones; use file:// instead.")); + if (option_not.nr) + warning(_("--shallow-exclude is ignored in local clones; use file:// instead.")); + if (filter_options.choice) + warning(_("--filter is ignored in local clones; use file:// instead.")); + if (!access(mkpath("%s/shallow", path), F_OK)) { + if (reject_shallow) + die(_("source repository is shallow, reject to clone.")); + if (option_local > 0) + warning(_("source repository is shallow, ignoring --local")); + is_local = 0; + } + } + if (option_local > 0 && !is_local) + warning(_("--local is ignored")); + + transport = transport_get(remote, path ? path : remote->url[0]); + transport_set_verbosity(transport, option_verbosity, option_progress); + transport->family = family; + transport->cloning = 1; + + if (is_bundle) { + struct bundle_header header = BUNDLE_HEADER_INIT; + int fd = read_bundle_header(path, &header); + int has_filter = header.filter.choice != LOFC_DISABLED; + + if (fd > 0) + close(fd); + bundle_header_release(&header); + if (has_filter) + die(_("cannot clone from filtered bundle")); + } + + transport_set_option(transport, TRANS_OPT_KEEP, "yes"); + + if (reject_shallow) + transport_set_option(transport, TRANS_OPT_REJECT_SHALLOW, "1"); + if (option_depth) + transport_set_option(transport, TRANS_OPT_DEPTH, + option_depth); + if (option_since) + transport_set_option(transport, TRANS_OPT_DEEPEN_SINCE, + option_since); + if (option_not.nr) + transport_set_option(transport, TRANS_OPT_DEEPEN_NOT, + (const char *)&option_not); + if (option_single_branch) + transport_set_option(transport, TRANS_OPT_FOLLOWTAGS, "1"); + + if (option_upload_pack) + transport_set_option(transport, TRANS_OPT_UPLOADPACK, + option_upload_pack); + + if (server_options.nr) + transport->server_options = &server_options; + + if (filter_options.choice) { + const char *spec = + expand_list_objects_filter_spec(&filter_options); + transport_set_option(transport, TRANS_OPT_LIST_OBJECTS_FILTER, + spec); + transport_set_option(transport, TRANS_OPT_FROM_PROMISOR, "1"); + } + + if (transport->smart_options && !deepen && !filter_options.choice) + transport->smart_options->check_self_contained_and_connected = 1; + + /* + * Before fetching from the remote, download and install bundle + * data from the --bundle-uri option. + */ + if (bundle_uri) { + int has_heuristic = 0; + + /* At this point, we need the_repository to match the cloned repo. */ + if (repo_init(the_repository, git_dir, work_tree)) + warning(_("failed to initialize the repo, skipping bundle URI")); + else if (fetch_bundle_uri(the_repository, bundle_uri, &has_heuristic)) + warning(_("failed to fetch objects from bundle URI '%s'"), + bundle_uri); + else if (has_heuristic) + git_config_set_gently("fetch.bundleuri", bundle_uri); + } + + strvec_push(&transport_ls_refs_options.ref_prefixes, "HEAD"); + refspec_ref_prefixes(&remote->fetch, + &transport_ls_refs_options.ref_prefixes); + if (option_branch) + expand_ref_prefix(&transport_ls_refs_options.ref_prefixes, + option_branch); + if (!option_no_tags) + strvec_push(&transport_ls_refs_options.ref_prefixes, + "refs/tags/"); + + refs = transport_get_remote_refs(transport, &transport_ls_refs_options); + + if (refs) + mapped_refs = wanted_peer_refs(refs, &remote->fetch); + + if (!bundle_uri) { + /* + * Populate transport->got_remote_bundle_uri and + * transport->bundle_uri. We might get nothing. + */ + transport_get_remote_bundle_uri(transport); + + if (transport->bundles && + hashmap_get_size(&transport->bundles->bundles)) { + /* At this point, we need the_repository to match the cloned repo. */ + if (repo_init(the_repository, git_dir, work_tree)) + warning(_("failed to initialize the repo, skipping bundle URI")); + else if (fetch_bundle_list(the_repository, + transport->bundles)) + warning(_("failed to fetch advertised bundles")); + } else { + clear_bundle_list(transport->bundles); + FREE_AND_NULL(transport->bundles); + } + } + + /* + * Now that we know what algorithm the remote side is using, + * let's set ours to the same thing. + */ + hash_algo = hash_algo_by_ptr(transport_get_hash_algo(transport)); + initialize_repository_version(hash_algo, 1); + repo_set_hash_algo(the_repository, hash_algo); + + if (mapped_refs) { + /* + * transport_get_remote_refs() may return refs with null sha-1 + * in mapped_refs (see struct transport->get_refs_list + * comment). In that case we need fetch it early because + * remote_head code below relies on it. + * + * for normal clones, transport_get_remote_refs() should + * return reliable ref set, we can delay cloning until after + * remote HEAD check. + */ + for (ref = refs; ref; ref = ref->next) + if (is_null_oid(&ref->old_oid)) { + complete_refs_before_fetch = 0; + break; + } + + if (!is_local && !complete_refs_before_fetch) { + if (transport_fetch_refs(transport, mapped_refs)) + die(_("remote transport reported error")); + } + } + + remote_head = find_ref_by_name(refs, "HEAD"); + remote_head_points_at = guess_remote_head(remote_head, mapped_refs, 0); + + if (option_branch) { + our_head_points_at = find_remote_branch(mapped_refs, option_branch); + if (!our_head_points_at) + die(_("Remote branch %s not found in upstream %s"), + option_branch, remote_name); + } else if (remote_head_points_at) { + our_head_points_at = remote_head_points_at; + } else if (remote_head) { + our_head_points_at = NULL; + } else { + const char *branch; + + if (!mapped_refs) { + warning(_("You appear to have cloned an empty repository.")); + option_no_checkout = 1; + } + + if (transport_ls_refs_options.unborn_head_target && + skip_prefix(transport_ls_refs_options.unborn_head_target, + "refs/heads/", &branch)) { + unborn_head = xstrdup(transport_ls_refs_options.unborn_head_target); + } else { + branch = git_default_branch_name(0); + unborn_head = xstrfmt("refs/heads/%s", branch); + } + + /* + * We may have selected a local default branch name "foo", + * and even though the remote's HEAD does not point there, + * it may still have a "foo" branch. If so, set it up so + * that we can follow the usual checkout code later. + * + * Note that for an empty repo we'll already have set + * option_no_checkout above, which would work against us here. + * But for an empty repo, find_remote_branch() can never find + * a match. + */ + our_head_points_at = find_remote_branch(mapped_refs, branch); + } + + write_refspec_config(src_ref_prefix, our_head_points_at, + remote_head_points_at, &branch_top); + + if (filter_options.choice) + partial_clone_register(remote_name, &filter_options); + + if (is_local) + clone_local(path, git_dir); + else if (mapped_refs && complete_refs_before_fetch) { + if (transport_fetch_refs(transport, mapped_refs)) + die(_("remote transport reported error")); + } + + update_remote_refs(refs, mapped_refs, remote_head_points_at, + branch_top.buf, reflog_msg.buf, transport, + !is_local); + + update_head(our_head_points_at, remote_head, unborn_head, reflog_msg.buf); + + /* + * We want to show progress for recursive submodule clones iff + * we did so for the main clone. But only the transport knows + * the final decision for this flag, so we need to rescue the value + * before we free the transport. + */ + submodule_progress = transport->progress; + + transport_unlock_pack(transport, 0); + transport_disconnect(transport); + + if (option_dissociate) { + close_object_store(the_repository->objects); + dissociate_from_references(); + } + + junk_mode = JUNK_LEAVE_REPO; + err = checkout(submodule_progress, filter_submodules); + + free(remote_name); + strbuf_release(&reflog_msg); + strbuf_release(&branch_top); + strbuf_release(&key); + free_refs(mapped_refs); + free_refs(remote_head_points_at); + free(unborn_head); + free(dir); + free(path); + free(repo_to_free); + junk_mode = JUNK_LEAVE_ALL; + + transport_ls_refs_options_release(&transport_ls_refs_options); + return err; +} diff --git a/builtin/column.c b/builtin/column.c new file mode 100644 index 0000000..a83be8b --- /dev/null +++ b/builtin/column.c @@ -0,0 +1,60 @@ +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "strbuf.h" +#include "parse-options.h" +#include "string-list.h" +#include "column.h" + +static const char * const builtin_column_usage[] = { + N_("git column [<options>]"), + NULL +}; +static unsigned int colopts; + +static int column_config(const char *var, const char *value, + const struct config_context *ctx UNUSED, void *cb) +{ + return git_column_config(var, value, cb, &colopts); +} + +int cmd_column(int argc, const char **argv, const char *prefix) +{ + struct string_list list = STRING_LIST_INIT_DUP; + struct strbuf sb = STRBUF_INIT; + struct column_options copts; + const char *command = NULL, *real_command = NULL; + struct option options[] = { + OPT_STRING(0, "command", &real_command, N_("name"), N_("lookup config vars")), + OPT_COLUMN(0, "mode", &colopts, N_("layout to use")), + OPT_INTEGER(0, "raw-mode", &colopts, N_("layout to use")), + OPT_INTEGER(0, "width", &copts.width, N_("maximum width")), + OPT_STRING(0, "indent", &copts.indent, N_("string"), N_("padding space on left border")), + OPT_STRING(0, "nl", &copts.nl, N_("string"), N_("padding space on right border")), + OPT_INTEGER(0, "padding", &copts.padding, N_("padding space between columns")), + OPT_END() + }; + + /* This one is special and must be the first one */ + if (argc > 1 && starts_with(argv[1], "--command=")) { + command = argv[1] + 10; + git_config(column_config, (void *)command); + } else + git_config(column_config, NULL); + + memset(&copts, 0, sizeof(copts)); + copts.padding = 1; + argc = parse_options(argc, argv, prefix, options, builtin_column_usage, 0); + if (argc) + usage_with_options(builtin_column_usage, options); + if (real_command || command) { + if (!real_command || !command || strcmp(real_command, command)) + die(_("--command must be the first argument")); + } + finalize_colopts(&colopts, -1); + while (!strbuf_getline(&sb, stdin)) + string_list_append(&list, sb.buf); + + print_columns(&list, colopts, &copts); + return 0; +} diff --git a/builtin/commit-graph.c b/builtin/commit-graph.c new file mode 100644 index 0000000..45d035a --- /dev/null +++ b/builtin/commit-graph.c @@ -0,0 +1,355 @@ +#include "builtin.h" +#include "commit.h" +#include "config.h" +#include "dir.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "lockfile.h" +#include "parse-options.h" +#include "repository.h" +#include "commit-graph.h" +#include "object-store-ll.h" +#include "progress.h" +#include "replace-object.h" +#include "tag.h" +#include "trace2.h" + +#define BUILTIN_COMMIT_GRAPH_VERIFY_USAGE \ + N_("git commit-graph verify [--object-dir <dir>] [--shallow] [--[no-]progress]") + +#define BUILTIN_COMMIT_GRAPH_WRITE_USAGE \ + N_("git commit-graph write [--object-dir <dir>] [--append]\n" \ + " [--split[=<strategy>]] [--reachable | --stdin-packs | --stdin-commits]\n" \ + " [--changed-paths] [--[no-]max-new-filters <n>] [--[no-]progress]\n" \ + " <split options>") + +static const char * builtin_commit_graph_verify_usage[] = { + BUILTIN_COMMIT_GRAPH_VERIFY_USAGE, + NULL +}; + +static const char * builtin_commit_graph_write_usage[] = { + BUILTIN_COMMIT_GRAPH_WRITE_USAGE, + NULL +}; + +static char const * const builtin_commit_graph_usage[] = { + BUILTIN_COMMIT_GRAPH_VERIFY_USAGE, + BUILTIN_COMMIT_GRAPH_WRITE_USAGE, + NULL, +}; + +static struct opts_commit_graph { + const char *obj_dir; + int reachable; + int stdin_packs; + int stdin_commits; + int append; + int split; + int shallow; + int progress; + int enable_changed_paths; +} opts; + +static struct option common_opts[] = { + OPT_STRING(0, "object-dir", &opts.obj_dir, + N_("dir"), + N_("the object directory to store the graph")), + OPT_END() +}; + +static struct option *add_common_options(struct option *to) +{ + return parse_options_concat(common_opts, to); +} + +static int graph_verify(int argc, const char **argv, const char *prefix) +{ + struct commit_graph *graph = NULL; + struct object_directory *odb = NULL; + char *graph_name; + char *chain_name; + enum { OPENED_NONE, OPENED_GRAPH, OPENED_CHAIN } opened = OPENED_NONE; + int fd; + struct stat st; + int flags = 0; + int incomplete_chain = 0; + int ret; + + static struct option builtin_commit_graph_verify_options[] = { + OPT_BOOL(0, "shallow", &opts.shallow, + N_("if the commit-graph is split, only verify the tip file")), + OPT_BOOL(0, "progress", &opts.progress, + N_("force progress reporting")), + OPT_END(), + }; + struct option *options = add_common_options(builtin_commit_graph_verify_options); + + trace2_cmd_mode("verify"); + + opts.progress = isatty(2); + argc = parse_options(argc, argv, prefix, + options, + builtin_commit_graph_verify_usage, 0); + if (argc) + usage_with_options(builtin_commit_graph_verify_usage, options); + + if (!opts.obj_dir) + opts.obj_dir = get_object_directory(); + if (opts.shallow) + flags |= COMMIT_GRAPH_VERIFY_SHALLOW; + if (opts.progress) + flags |= COMMIT_GRAPH_WRITE_PROGRESS; + + odb = find_odb(the_repository, opts.obj_dir); + graph_name = get_commit_graph_filename(odb); + chain_name = get_commit_graph_chain_filename(odb); + if (open_commit_graph(graph_name, &fd, &st)) + opened = OPENED_GRAPH; + else if (errno != ENOENT) + die_errno(_("Could not open commit-graph '%s'"), graph_name); + else if (open_commit_graph_chain(chain_name, &fd, &st)) + opened = OPENED_CHAIN; + else if (errno != ENOENT) + die_errno(_("could not open commit-graph chain '%s'"), chain_name); + + FREE_AND_NULL(graph_name); + FREE_AND_NULL(chain_name); + FREE_AND_NULL(options); + + if (opened == OPENED_NONE) + return 0; + else if (opened == OPENED_GRAPH) + graph = load_commit_graph_one_fd_st(the_repository, fd, &st, odb); + else + graph = load_commit_graph_chain_fd_st(the_repository, fd, &st, + &incomplete_chain); + + if (!graph) + return 1; + + ret = verify_commit_graph(the_repository, graph, flags); + free_commit_graph(graph); + + if (incomplete_chain) { + error("one or more commit-graph chain files could not be loaded"); + ret |= 1; + } + + return ret; +} + +extern int read_replace_refs; +static struct commit_graph_opts write_opts; + +static int write_option_parse_split(const struct option *opt, const char *arg, + int unset) +{ + enum commit_graph_split_flags *flags = opt->value; + + BUG_ON_OPT_NEG(unset); + + opts.split = 1; + if (!arg) + return 0; + + if (!strcmp(arg, "no-merge")) + *flags = COMMIT_GRAPH_SPLIT_MERGE_PROHIBITED; + else if (!strcmp(arg, "replace")) + *flags = COMMIT_GRAPH_SPLIT_REPLACE; + else + die(_("unrecognized --split argument, %s"), arg); + + return 0; +} + +static int read_one_commit(struct oidset *commits, struct progress *progress, + const char *hash) +{ + struct object *result; + struct object_id oid; + const char *end; + + if (parse_oid_hex(hash, &oid, &end)) + return error(_("unexpected non-hex object ID: %s"), hash); + + result = deref_tag(the_repository, parse_object(the_repository, &oid), + NULL, 0); + if (!result) + return error(_("invalid object: %s"), hash); + else if (object_as_type(result, OBJ_COMMIT, 1)) + oidset_insert(commits, &result->oid); + + display_progress(progress, oidset_size(commits)); + + return 0; +} + +static int write_option_max_new_filters(const struct option *opt, + const char *arg, + int unset) +{ + int *to = opt->value; + if (unset) + *to = -1; + else { + const char *s; + *to = strtol(arg, (char **)&s, 10); + if (*s) + return error(_("option `%s' expects a numerical value"), + "max-new-filters"); + } + return 0; +} + +static int git_commit_graph_write_config(const char *var, const char *value, + const struct config_context *ctx, + void *cb UNUSED) +{ + if (!strcmp(var, "commitgraph.maxnewfilters")) + write_opts.max_new_filters = git_config_int(var, value, ctx->kvi); + /* + * No need to fall-back to 'git_default_config', since this was already + * called in 'cmd_commit_graph()'. + */ + return 0; +} + +static int graph_write(int argc, const char **argv, const char *prefix) +{ + struct string_list pack_indexes = STRING_LIST_INIT_DUP; + struct strbuf buf = STRBUF_INIT; + struct oidset commits = OIDSET_INIT; + struct object_directory *odb = NULL; + int result = 0; + enum commit_graph_write_flags flags = 0; + struct progress *progress = NULL; + + static struct option builtin_commit_graph_write_options[] = { + OPT_BOOL(0, "reachable", &opts.reachable, + N_("start walk at all refs")), + OPT_BOOL(0, "stdin-packs", &opts.stdin_packs, + N_("scan pack-indexes listed by stdin for commits")), + OPT_BOOL(0, "stdin-commits", &opts.stdin_commits, + N_("start walk at commits listed by stdin")), + OPT_BOOL(0, "append", &opts.append, + N_("include all commits already in the commit-graph file")), + OPT_BOOL(0, "changed-paths", &opts.enable_changed_paths, + N_("enable computation for changed paths")), + OPT_CALLBACK_F(0, "split", &write_opts.split_flags, NULL, + N_("allow writing an incremental commit-graph file"), + PARSE_OPT_OPTARG | PARSE_OPT_NONEG, + write_option_parse_split), + OPT_INTEGER(0, "max-commits", &write_opts.max_commits, + N_("maximum number of commits in a non-base split commit-graph")), + OPT_INTEGER(0, "size-multiple", &write_opts.size_multiple, + N_("maximum ratio between two levels of a split commit-graph")), + OPT_EXPIRY_DATE(0, "expire-time", &write_opts.expire_time, + N_("only expire files older than a given date-time")), + OPT_CALLBACK_F(0, "max-new-filters", &write_opts.max_new_filters, + NULL, N_("maximum number of changed-path Bloom filters to compute"), + 0, write_option_max_new_filters), + OPT_BOOL(0, "progress", &opts.progress, + N_("force progress reporting")), + OPT_END(), + }; + struct option *options = add_common_options(builtin_commit_graph_write_options); + + opts.progress = isatty(2); + opts.enable_changed_paths = -1; + write_opts.size_multiple = 2; + write_opts.max_commits = 0; + write_opts.expire_time = 0; + write_opts.max_new_filters = -1; + + trace2_cmd_mode("write"); + + git_config(git_commit_graph_write_config, &opts); + + argc = parse_options(argc, argv, prefix, + options, + builtin_commit_graph_write_usage, 0); + if (argc) + usage_with_options(builtin_commit_graph_write_usage, options); + + if (opts.reachable + opts.stdin_packs + opts.stdin_commits > 1) + die(_("use at most one of --reachable, --stdin-commits, or --stdin-packs")); + if (!opts.obj_dir) + opts.obj_dir = get_object_directory(); + if (opts.append) + flags |= COMMIT_GRAPH_WRITE_APPEND; + if (opts.split) + flags |= COMMIT_GRAPH_WRITE_SPLIT; + if (opts.progress) + flags |= COMMIT_GRAPH_WRITE_PROGRESS; + if (!opts.enable_changed_paths) + flags |= COMMIT_GRAPH_NO_WRITE_BLOOM_FILTERS; + if (opts.enable_changed_paths == 1 || + git_env_bool(GIT_TEST_COMMIT_GRAPH_CHANGED_PATHS, 0)) + flags |= COMMIT_GRAPH_WRITE_BLOOM_FILTERS; + + odb = find_odb(the_repository, opts.obj_dir); + + if (opts.reachable) { + if (write_commit_graph_reachable(odb, flags, &write_opts)) + result = 1; + goto cleanup; + } + + if (opts.stdin_packs) { + while (strbuf_getline(&buf, stdin) != EOF) + string_list_append_nodup(&pack_indexes, + strbuf_detach(&buf, NULL)); + } else if (opts.stdin_commits) { + oidset_init(&commits, 0); + if (opts.progress) + progress = start_delayed_progress( + _("Collecting commits from input"), 0); + + while (strbuf_getline(&buf, stdin) != EOF) { + if (read_one_commit(&commits, progress, buf.buf)) { + result = 1; + goto cleanup; + } + } + + stop_progress(&progress); + } + + if (write_commit_graph(odb, + opts.stdin_packs ? &pack_indexes : NULL, + opts.stdin_commits ? &commits : NULL, + flags, + &write_opts)) + result = 1; + +cleanup: + FREE_AND_NULL(options); + string_list_clear(&pack_indexes, 0); + strbuf_release(&buf); + oidset_clear(&commits); + return result; +} + +int cmd_commit_graph(int argc, const char **argv, const char *prefix) +{ + parse_opt_subcommand_fn *fn = NULL; + struct option builtin_commit_graph_options[] = { + OPT_SUBCOMMAND("verify", &fn, graph_verify), + OPT_SUBCOMMAND("write", &fn, graph_write), + OPT_END(), + }; + struct option *options = parse_options_concat(builtin_commit_graph_options, common_opts); + + git_config(git_default_config, NULL); + + disable_replace_refs(); + save_commit_buffer = 0; + + argc = parse_options(argc, argv, prefix, options, + builtin_commit_graph_usage, 0); + FREE_AND_NULL(options); + + return fn(argc, argv, prefix); +} diff --git a/builtin/commit-tree.c b/builtin/commit-tree.c new file mode 100644 index 0000000..02625e7 --- /dev/null +++ b/builtin/commit-tree.c @@ -0,0 +1,145 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "hex.h" +#include "object-name.h" +#include "object-store-ll.h" +#include "repository.h" +#include "commit.h" +#include "tree.h" +#include "utf8.h" +#include "gpg-interface.h" +#include "parse-options.h" + +static const char * const commit_tree_usage[] = { + N_("git commit-tree <tree> [(-p <parent>)...]"), + N_("git commit-tree [(-p <parent>)...] [-S[<keyid>]] [(-m <message>)...]\n" + " [(-F <file>)...] <tree>"), + NULL +}; + +static const char *sign_commit; + +static void new_parent(struct commit *parent, struct commit_list **parents_p) +{ + struct object_id *oid = &parent->object.oid; + struct commit_list *parents; + for (parents = *parents_p; parents; parents = parents->next) { + if (parents->item == parent) { + error(_("duplicate parent %s ignored"), oid_to_hex(oid)); + return; + } + parents_p = &parents->next; + } + commit_list_insert(parent, parents_p); +} + +static int parse_parent_arg_callback(const struct option *opt, + const char *arg, int unset) +{ + struct object_id oid; + struct commit_list **parents = opt->value; + + BUG_ON_OPT_NEG_NOARG(unset, arg); + + if (repo_get_oid_commit(the_repository, arg, &oid)) + die(_("not a valid object name %s"), arg); + + assert_oid_type(&oid, OBJ_COMMIT); + new_parent(lookup_commit(the_repository, &oid), parents); + return 0; +} + +static int parse_message_arg_callback(const struct option *opt, + const char *arg, int unset) +{ + struct strbuf *buf = opt->value; + + BUG_ON_OPT_NEG_NOARG(unset, arg); + + if (buf->len) + strbuf_addch(buf, '\n'); + strbuf_addstr(buf, arg); + strbuf_complete_line(buf); + + return 0; +} + +static int parse_file_arg_callback(const struct option *opt, + const char *arg, int unset) +{ + int fd; + struct strbuf *buf = opt->value; + + BUG_ON_OPT_NEG_NOARG(unset, arg); + + if (buf->len) + strbuf_addch(buf, '\n'); + if (!strcmp(arg, "-")) + fd = 0; + else { + fd = xopen(arg, O_RDONLY); + } + if (strbuf_read(buf, fd, 0) < 0) + die_errno(_("git commit-tree: failed to read '%s'"), arg); + if (fd && close(fd)) + die_errno(_("git commit-tree: failed to close '%s'"), arg); + + return 0; +} + +int cmd_commit_tree(int argc, const char **argv, const char *prefix) +{ + static struct strbuf buffer = STRBUF_INIT; + struct commit_list *parents = NULL; + struct object_id tree_oid; + struct object_id commit_oid; + + struct option options[] = { + OPT_CALLBACK_F('p', NULL, &parents, N_("parent"), + N_("id of a parent commit object"), PARSE_OPT_NONEG, + parse_parent_arg_callback), + OPT_CALLBACK_F('m', NULL, &buffer, N_("message"), + N_("commit message"), PARSE_OPT_NONEG, + parse_message_arg_callback), + OPT_CALLBACK_F('F', NULL, &buffer, N_("file"), + N_("read commit log message from file"), PARSE_OPT_NONEG, + parse_file_arg_callback), + { OPTION_STRING, 'S', "gpg-sign", &sign_commit, N_("key-id"), + N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + OPT_END() + }; + + git_config(git_default_config, NULL); + + if (argc < 2 || !strcmp(argv[1], "-h")) + usage_with_options(commit_tree_usage, options); + + argc = parse_options(argc, argv, prefix, options, commit_tree_usage, 0); + + if (argc != 1) + die(_("must give exactly one tree")); + + if (repo_get_oid_tree(the_repository, argv[0], &tree_oid)) + die(_("not a valid object name %s"), argv[0]); + + if (!buffer.len) { + if (strbuf_read(&buffer, 0, 0) < 0) + die_errno(_("git commit-tree: failed to read")); + } + + if (commit_tree(buffer.buf, buffer.len, &tree_oid, parents, &commit_oid, + NULL, sign_commit)) { + strbuf_release(&buffer); + return 1; + } + + printf("%s\n", oid_to_hex(&commit_oid)); + strbuf_release(&buffer); + return 0; +} diff --git a/builtin/commit.c b/builtin/commit.c new file mode 100644 index 0000000..781af2e --- /dev/null +++ b/builtin/commit.c @@ -0,0 +1,1895 @@ +/* + * Builtin "git commit" + * + * Copyright (c) 2007 Kristian Høgsberg <krh@redhat.com> + * Based on git-commit.sh by Junio C Hamano and Linus Torvalds + */ + +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "advice.h" +#include "config.h" +#include "lockfile.h" +#include "cache-tree.h" +#include "color.h" +#include "dir.h" +#include "editor.h" +#include "environment.h" +#include "diff.h" +#include "diffcore.h" +#include "commit.h" +#include "gettext.h" +#include "revision.h" +#include "wt-status.h" +#include "run-command.h" +#include "hook.h" +#include "refs.h" +#include "log-tree.h" +#include "strbuf.h" +#include "utf8.h" +#include "object-name.h" +#include "parse-options.h" +#include "path.h" +#include "preload-index.h" +#include "read-cache.h" +#include "string-list.h" +#include "rerere.h" +#include "unpack-trees.h" +#include "quote.h" +#include "submodule.h" +#include "gpg-interface.h" +#include "column.h" +#include "sequencer.h" +#include "sparse-index.h" +#include "mailmap.h" +#include "help.h" +#include "commit-reach.h" +#include "commit-graph.h" +#include "pretty.h" + +static const char * const builtin_commit_usage[] = { + N_("git commit [-a | --interactive | --patch] [-s] [-v] [-u<mode>] [--amend]\n" + " [--dry-run] [(-c | -C | --squash) <commit> | --fixup [(amend|reword):]<commit>)]\n" + " [-F <file> | -m <msg>] [--reset-author] [--allow-empty]\n" + " [--allow-empty-message] [--no-verify] [-e] [--author=<author>]\n" + " [--date=<date>] [--cleanup=<mode>] [--[no-]status]\n" + " [-i | -o] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n" + " [(--trailer <token>[(=|:)<value>])...] [-S[<keyid>]]\n" + " [--] [<pathspec>...]"), + NULL +}; + +static const char * const builtin_status_usage[] = { + N_("git status [<options>] [--] [<pathspec>...]"), + NULL +}; + +static const char empty_amend_advice[] = +N_("You asked to amend the most recent commit, but doing so would make\n" +"it empty. You can repeat your command with --allow-empty, or you can\n" +"remove the commit entirely with \"git reset HEAD^\".\n"); + +static const char empty_cherry_pick_advice[] = +N_("The previous cherry-pick is now empty, possibly due to conflict resolution.\n" +"If you wish to commit it anyway, use:\n" +"\n" +" git commit --allow-empty\n" +"\n"); + +static const char empty_rebase_pick_advice[] = +N_("Otherwise, please use 'git rebase --skip'\n"); + +static const char empty_cherry_pick_advice_single[] = +N_("Otherwise, please use 'git cherry-pick --skip'\n"); + +static const char empty_cherry_pick_advice_multi[] = +N_("and then use:\n" +"\n" +" git cherry-pick --continue\n" +"\n" +"to resume cherry-picking the remaining commits.\n" +"If you wish to skip this commit, use:\n" +"\n" +" git cherry-pick --skip\n" +"\n"); + +static const char *color_status_slots[] = { + [WT_STATUS_HEADER] = "header", + [WT_STATUS_UPDATED] = "updated", + [WT_STATUS_CHANGED] = "changed", + [WT_STATUS_UNTRACKED] = "untracked", + [WT_STATUS_NOBRANCH] = "noBranch", + [WT_STATUS_UNMERGED] = "unmerged", + [WT_STATUS_LOCAL_BRANCH] = "localBranch", + [WT_STATUS_REMOTE_BRANCH] = "remoteBranch", + [WT_STATUS_ONBRANCH] = "branch", +}; + +static const char *use_message_buffer; +static struct lock_file index_lock; /* real index */ +static struct lock_file false_lock; /* used only for partial commits */ +static enum { + COMMIT_AS_IS = 1, + COMMIT_NORMAL, + COMMIT_PARTIAL +} commit_style; + +static const char *logfile, *force_author; +static const char *template_file; +/* + * The _message variables are commit names from which to take + * the commit message and/or authorship. + */ +static const char *author_message, *author_message_buffer; +static char *edit_message, *use_message; +static char *fixup_message, *fixup_commit, *squash_message; +static const char *fixup_prefix; +static int all, also, interactive, patch_interactive, only, amend, signoff; +static int edit_flag = -1; /* unspecified */ +static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship; +static int config_commit_verbose = -1; /* unspecified */ +static int no_post_rewrite, allow_empty_message, pathspec_file_nul; +static char *untracked_files_arg, *force_date, *ignore_submodule_arg, *ignored_arg; +static char *sign_commit, *pathspec_from_file; +static struct strvec trailer_args = STRVEC_INIT; + +/* + * The default commit message cleanup mode will remove the lines + * beginning with # (shell comments) and leading and trailing + * whitespaces (empty lines or containing only whitespaces) + * if editor is used, and only the whitespaces if the message + * is specified explicitly. + */ +static enum commit_msg_cleanup_mode cleanup_mode; +static const char *cleanup_arg; + +static enum commit_whence whence; +static int use_editor = 1, include_status = 1; +static int have_option_m; +static struct strbuf message = STRBUF_INIT; + +static enum wt_status_format status_format = STATUS_FORMAT_UNSPECIFIED; + +static int opt_pass_trailer(const struct option *opt, const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + + strvec_pushl(opt->value, "--trailer", arg, NULL); + return 0; +} + +static int opt_parse_porcelain(const struct option *opt, const char *arg, int unset) +{ + enum wt_status_format *value = (enum wt_status_format *)opt->value; + if (unset) + *value = STATUS_FORMAT_NONE; + else if (!arg) + *value = STATUS_FORMAT_PORCELAIN; + else if (!strcmp(arg, "v1") || !strcmp(arg, "1")) + *value = STATUS_FORMAT_PORCELAIN; + else if (!strcmp(arg, "v2") || !strcmp(arg, "2")) + *value = STATUS_FORMAT_PORCELAIN_V2; + else + die("unsupported porcelain version '%s'", arg); + + return 0; +} + +static int opt_parse_m(const struct option *opt, const char *arg, int unset) +{ + struct strbuf *buf = opt->value; + if (unset) { + have_option_m = 0; + strbuf_setlen(buf, 0); + } else { + have_option_m = 1; + if (buf->len) + strbuf_addch(buf, '\n'); + strbuf_addstr(buf, arg); + strbuf_complete_line(buf); + } + return 0; +} + +static int opt_parse_rename_score(const struct option *opt, const char *arg, int unset) +{ + const char **value = opt->value; + + BUG_ON_OPT_NEG(unset); + + if (arg != NULL && *arg == '=') + arg = arg + 1; + + *value = arg; + return 0; +} + +static void determine_whence(struct wt_status *s) +{ + if (file_exists(git_path_merge_head(the_repository))) + whence = FROM_MERGE; + else if (!sequencer_determine_whence(the_repository, &whence)) + whence = FROM_COMMIT; + if (s) + s->whence = whence; +} + +static void status_init_config(struct wt_status *s, config_fn_t fn) +{ + wt_status_prepare(the_repository, s); + init_diff_ui_defaults(); + git_config(fn, s); + determine_whence(s); + s->hints = advice_enabled(ADVICE_STATUS_HINTS); /* must come after git_config() */ +} + +static void rollback_index_files(void) +{ + switch (commit_style) { + case COMMIT_AS_IS: + break; /* nothing to do */ + case COMMIT_NORMAL: + rollback_lock_file(&index_lock); + break; + case COMMIT_PARTIAL: + rollback_lock_file(&index_lock); + rollback_lock_file(&false_lock); + break; + } +} + +static int commit_index_files(void) +{ + int err = 0; + + switch (commit_style) { + case COMMIT_AS_IS: + break; /* nothing to do */ + case COMMIT_NORMAL: + err = commit_lock_file(&index_lock); + break; + case COMMIT_PARTIAL: + err = commit_lock_file(&index_lock); + rollback_lock_file(&false_lock); + break; + } + + return err; +} + +/* + * Take a union of paths in the index and the named tree (typically, "HEAD"), + * and return the paths that match the given pattern in list. + */ +static int list_paths(struct string_list *list, const char *with_tree, + const struct pathspec *pattern) +{ + int i, ret; + char *m; + + if (!pattern->nr) + return 0; + + m = xcalloc(1, pattern->nr); + + if (with_tree) { + char *max_prefix = common_prefix(pattern); + overlay_tree_on_index(&the_index, with_tree, max_prefix); + free(max_prefix); + } + + /* TODO: audit for interaction with sparse-index. */ + ensure_full_index(&the_index); + for (i = 0; i < the_index.cache_nr; i++) { + const struct cache_entry *ce = the_index.cache[i]; + struct string_list_item *item; + + if (ce->ce_flags & CE_UPDATE) + continue; + if (!ce_path_match(&the_index, ce, pattern, m)) + continue; + item = string_list_insert(list, ce->name); + if (ce_skip_worktree(ce)) + item->util = item; /* better a valid pointer than a fake one */ + } + + ret = report_path_error(m, pattern); + free(m); + return ret; +} + +static void add_remove_files(struct string_list *list) +{ + int i; + for (i = 0; i < list->nr; i++) { + struct stat st; + struct string_list_item *p = &(list->items[i]); + + /* p->util is skip-worktree */ + if (p->util) + continue; + + if (!lstat(p->string, &st)) { + if (add_to_index(&the_index, p->string, &st, 0)) + die(_("updating files failed")); + } else + remove_file_from_index(&the_index, p->string); + } +} + +static void create_base_index(const struct commit *current_head) +{ + struct tree *tree; + struct unpack_trees_options opts; + struct tree_desc t; + + if (!current_head) { + discard_index(&the_index); + return; + } + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = 1; + opts.index_only = 1; + opts.merge = 1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + + opts.fn = oneway_merge; + tree = parse_tree_indirect(¤t_head->object.oid); + if (!tree) + die(_("failed to unpack HEAD tree object")); + parse_tree(tree); + init_tree_desc(&t, tree->buffer, tree->size); + if (unpack_trees(1, &t, &opts)) + exit(128); /* We've already reported the error, finish dying */ +} + +static void refresh_cache_or_die(int refresh_flags) +{ + /* + * refresh_flags contains REFRESH_QUIET, so the only errors + * are for unmerged entries. + */ + if (refresh_index(&the_index, refresh_flags | REFRESH_IN_PORCELAIN, NULL, NULL, NULL)) + die_resolve_conflict("commit"); +} + +static const char *prepare_index(const char **argv, const char *prefix, + const struct commit *current_head, int is_status) +{ + struct string_list partial = STRING_LIST_INIT_DUP; + struct pathspec pathspec; + int refresh_flags = REFRESH_QUIET; + const char *ret; + + if (is_status) + refresh_flags |= REFRESH_UNMERGED; + parse_pathspec(&pathspec, 0, + PATHSPEC_PREFER_FULL, + prefix, argv); + + if (pathspec_from_file) { + if (interactive) + die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--interactive/--patch"); + + if (all) + die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "-a"); + + if (pathspec.nr) + die(_("'%s' and pathspec arguments cannot be used together"), "--pathspec-from-file"); + + parse_pathspec_file(&pathspec, 0, + PATHSPEC_PREFER_FULL, + 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 (!pathspec.nr && (also || (only && !allow_empty && + (!amend || (fixup_message && strcmp(fixup_prefix, "amend")))))) + die(_("No paths with --include/--only does not make sense.")); + + if (repo_read_index_preload(the_repository, &pathspec, 0) < 0) + die(_("index file corrupt")); + + if (interactive) { + char *old_index_env = NULL, *old_repo_index_file; + repo_hold_locked_index(the_repository, &index_lock, + LOCK_DIE_ON_ERROR); + + refresh_cache_or_die(refresh_flags); + + if (write_locked_index(&the_index, &index_lock, 0)) + die(_("unable to create temporary index")); + + old_repo_index_file = the_repository->index_file; + the_repository->index_file = + (char *)get_lock_file_path(&index_lock); + old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT)); + setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1); + + if (interactive_add(argv, prefix, patch_interactive) != 0) + die(_("interactive add failed")); + + the_repository->index_file = old_repo_index_file; + if (old_index_env && *old_index_env) + setenv(INDEX_ENVIRONMENT, old_index_env, 1); + else + unsetenv(INDEX_ENVIRONMENT); + FREE_AND_NULL(old_index_env); + + discard_index(&the_index); + read_index_from(&the_index, get_lock_file_path(&index_lock), + get_git_dir()); + if (cache_tree_update(&the_index, WRITE_TREE_SILENT) == 0) { + if (reopen_lock_file(&index_lock) < 0) + die(_("unable to write index file")); + if (write_locked_index(&the_index, &index_lock, 0)) + die(_("unable to update temporary index")); + } else + warning(_("Failed to update main cache tree")); + + commit_style = COMMIT_NORMAL; + ret = get_lock_file_path(&index_lock); + goto out; + } + + /* + * Non partial, non as-is commit. + * + * (1) get the real index; + * (2) update the_index as necessary; + * (3) write the_index out to the real index (still locked); + * (4) return the name of the locked index file. + * + * The caller should run hooks on the locked real index, and + * (A) if all goes well, commit the real index; + * (B) on failure, rollback the real index. + */ + if (all || (also && pathspec.nr)) { + repo_hold_locked_index(the_repository, &index_lock, + LOCK_DIE_ON_ERROR); + add_files_to_cache(the_repository, also ? prefix : NULL, + &pathspec, 0, 0); + refresh_cache_or_die(refresh_flags); + cache_tree_update(&the_index, WRITE_TREE_SILENT); + if (write_locked_index(&the_index, &index_lock, 0)) + die(_("unable to write new index file")); + commit_style = COMMIT_NORMAL; + ret = get_lock_file_path(&index_lock); + goto out; + } + + /* + * As-is commit. + * + * (1) return the name of the real index file. + * + * The caller should run hooks on the real index, + * and create commit from the_index. + * We still need to refresh the index here. + */ + if (!only && !pathspec.nr) { + repo_hold_locked_index(the_repository, &index_lock, + LOCK_DIE_ON_ERROR); + refresh_cache_or_die(refresh_flags); + if (the_index.cache_changed + || !cache_tree_fully_valid(the_index.cache_tree)) + cache_tree_update(&the_index, WRITE_TREE_SILENT); + if (write_locked_index(&the_index, &index_lock, + COMMIT_LOCK | SKIP_IF_UNCHANGED)) + die(_("unable to write new index file")); + commit_style = COMMIT_AS_IS; + ret = get_index_file(); + goto out; + } + + /* + * A partial commit. + * + * (0) find the set of affected paths; + * (1) get lock on the real index file; + * (2) update the_index with the given paths; + * (3) write the_index out to the real index (still locked); + * (4) get lock on the false index file; + * (5) reset the_index from HEAD; + * (6) update the_index the same way as (2); + * (7) write the_index out to the false index file; + * (8) return the name of the false index file (still locked); + * + * The caller should run hooks on the locked false index, and + * create commit from it. Then + * (A) if all goes well, commit the real index; + * (B) on failure, rollback the real index; + * In either case, rollback the false index. + */ + commit_style = COMMIT_PARTIAL; + + if (whence != FROM_COMMIT) { + if (whence == FROM_MERGE) + die(_("cannot do a partial commit during a merge.")); + else if (is_from_cherry_pick(whence)) + die(_("cannot do a partial commit during a cherry-pick.")); + else if (is_from_rebase(whence)) + die(_("cannot do a partial commit during a rebase.")); + } + + if (list_paths(&partial, !current_head ? NULL : "HEAD", &pathspec)) + exit(1); + + discard_index(&the_index); + if (repo_read_index(the_repository) < 0) + die(_("cannot read the index")); + + repo_hold_locked_index(the_repository, &index_lock, LOCK_DIE_ON_ERROR); + add_remove_files(&partial); + refresh_index(&the_index, REFRESH_QUIET, NULL, NULL, NULL); + cache_tree_update(&the_index, WRITE_TREE_SILENT); + if (write_locked_index(&the_index, &index_lock, 0)) + die(_("unable to write new index file")); + + hold_lock_file_for_update(&false_lock, + git_path("next-index-%"PRIuMAX, + (uintmax_t) getpid()), + LOCK_DIE_ON_ERROR); + + create_base_index(current_head); + add_remove_files(&partial); + refresh_index(&the_index, REFRESH_QUIET, NULL, NULL, NULL); + + if (write_locked_index(&the_index, &false_lock, 0)) + die(_("unable to write temporary index file")); + + discard_index(&the_index); + ret = get_lock_file_path(&false_lock); + read_index_from(&the_index, ret, get_git_dir()); +out: + string_list_clear(&partial, 0); + clear_pathspec(&pathspec); + return ret; +} + +static int run_status(FILE *fp, const char *index_file, const char *prefix, int nowarn, + struct wt_status *s) +{ + struct object_id oid; + + if (s->relative_paths) + s->prefix = prefix; + + if (amend) { + s->amend = 1; + s->reference = "HEAD^1"; + } + s->verbose = verbose; + s->index_file = index_file; + s->fp = fp; + s->nowarn = nowarn; + s->is_initial = repo_get_oid(the_repository, s->reference, &oid) ? 1 : 0; + if (!s->is_initial) + oidcpy(&s->oid_commit, &oid); + s->status_format = status_format; + s->ignore_submodule_arg = ignore_submodule_arg; + + wt_status_collect(s); + wt_status_print(s); + wt_status_collect_free_buffers(s); + + return s->committable; +} + +static int is_a_merge(const struct commit *current_head) +{ + return !!(current_head->parents && current_head->parents->next); +} + +static void assert_split_ident(struct ident_split *id, const struct strbuf *buf) +{ + if (split_ident_line(id, buf->buf, buf->len) || !id->date_begin) + BUG("unable to parse our own ident: %s", buf->buf); +} + +static void export_one(const char *var, const char *s, const char *e, int hack) +{ + struct strbuf buf = STRBUF_INIT; + if (hack) + strbuf_addch(&buf, hack); + strbuf_add(&buf, s, e - s); + setenv(var, buf.buf, 1); + strbuf_release(&buf); +} + +static int parse_force_date(const char *in, struct strbuf *out) +{ + strbuf_addch(out, '@'); + + if (parse_date(in, out) < 0) { + int errors = 0; + unsigned long t = approxidate_careful(in, &errors); + if (errors) + return -1; + strbuf_addf(out, "%lu", t); + } + + return 0; +} + +static void set_ident_var(char **buf, char *val) +{ + free(*buf); + *buf = val; +} + +static void determine_author_info(struct strbuf *author_ident) +{ + char *name, *email, *date; + struct ident_split author; + + name = xstrdup_or_null(getenv("GIT_AUTHOR_NAME")); + email = xstrdup_or_null(getenv("GIT_AUTHOR_EMAIL")); + date = xstrdup_or_null(getenv("GIT_AUTHOR_DATE")); + + if (author_message) { + struct ident_split ident; + size_t len; + const char *a; + + a = find_commit_header(author_message_buffer, "author", &len); + if (!a) + die(_("commit '%s' lacks author header"), author_message); + if (split_ident_line(&ident, a, len) < 0) + die(_("commit '%s' has malformed author line"), author_message); + + set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin)); + set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin)); + + if (ident.date_begin) { + struct strbuf date_buf = STRBUF_INIT; + strbuf_addch(&date_buf, '@'); + strbuf_add(&date_buf, ident.date_begin, ident.date_end - ident.date_begin); + strbuf_addch(&date_buf, ' '); + strbuf_add(&date_buf, ident.tz_begin, ident.tz_end - ident.tz_begin); + set_ident_var(&date, strbuf_detach(&date_buf, NULL)); + } + } + + if (force_author) { + struct ident_split ident; + + if (split_ident_line(&ident, force_author, strlen(force_author)) < 0) + die(_("malformed --author parameter")); + set_ident_var(&name, xmemdupz(ident.name_begin, ident.name_end - ident.name_begin)); + set_ident_var(&email, xmemdupz(ident.mail_begin, ident.mail_end - ident.mail_begin)); + } + + if (force_date) { + struct strbuf date_buf = STRBUF_INIT; + if (parse_force_date(force_date, &date_buf)) + die(_("invalid date format: %s"), force_date); + set_ident_var(&date, strbuf_detach(&date_buf, NULL)); + } + + strbuf_addstr(author_ident, fmt_ident(name, email, WANT_AUTHOR_IDENT, date, + IDENT_STRICT)); + assert_split_ident(&author, author_ident); + export_one("GIT_AUTHOR_NAME", author.name_begin, author.name_end, 0); + export_one("GIT_AUTHOR_EMAIL", author.mail_begin, author.mail_end, 0); + export_one("GIT_AUTHOR_DATE", author.date_begin, author.tz_end, '@'); + free(name); + free(email); + free(date); +} + +static int author_date_is_interesting(void) +{ + return author_message || force_date; +} + +static void adjust_comment_line_char(const struct strbuf *sb) +{ + char candidates[] = "#;@!$%^&|:"; + char *candidate; + const char *p; + + comment_line_char = candidates[0]; + if (!memchr(sb->buf, comment_line_char, sb->len)) + return; + + p = sb->buf; + candidate = strchr(candidates, *p); + if (candidate) + *candidate = ' '; + for (p = sb->buf; *p; p++) { + if ((p[0] == '\n' || p[0] == '\r') && p[1]) { + candidate = strchr(candidates, p[1]); + if (candidate) + *candidate = ' '; + } + } + + for (p = candidates; *p == ' '; p++) + ; + if (!*p) + die(_("unable to select a comment character that is not used\n" + "in the current commit message")); + comment_line_char = *p; +} + +static void prepare_amend_commit(struct commit *commit, struct strbuf *sb, + struct pretty_print_context *ctx) +{ + const char *buffer, *subject, *fmt; + + buffer = repo_get_commit_buffer(the_repository, commit, NULL); + find_commit_subject(buffer, &subject); + /* + * If we amend the 'amend!' commit then we don't want to + * duplicate the subject line. + */ + fmt = starts_with(subject, "amend!") ? "%b" : "%B"; + repo_format_commit_message(the_repository, commit, fmt, sb, ctx); + repo_unuse_commit_buffer(the_repository, commit, buffer); +} + +static int prepare_to_commit(const char *index_file, const char *prefix, + struct commit *current_head, + struct wt_status *s, + struct strbuf *author_ident) +{ + struct stat statbuf; + struct strbuf committer_ident = STRBUF_INIT; + int committable; + struct strbuf sb = STRBUF_INIT; + const char *hook_arg1 = NULL; + const char *hook_arg2 = NULL; + int clean_message_contents = (cleanup_mode != COMMIT_MSG_CLEANUP_NONE); + int old_display_comment_prefix; + int merge_contains_scissors = 0; + int invoked_hook; + + /* This checks and barfs if author is badly specified */ + determine_author_info(author_ident); + + if (!no_verify && run_commit_hook(use_editor, index_file, &invoked_hook, + "pre-commit", NULL)) + return 0; + + if (squash_message) { + /* + * Insert the proper subject line before other commit + * message options add their content. + */ + if (use_message && !strcmp(use_message, squash_message)) + strbuf_addstr(&sb, "squash! "); + else { + struct pretty_print_context ctx = {0}; + struct commit *c; + c = lookup_commit_reference_by_name(squash_message); + if (!c) + die(_("could not lookup commit '%s'"), squash_message); + ctx.output_encoding = get_commit_output_encoding(); + repo_format_commit_message(the_repository, c, + "squash! %s\n\n", &sb, + &ctx); + } + } + + if (have_option_m && !fixup_message) { + strbuf_addbuf(&sb, &message); + hook_arg1 = "message"; + } else if (logfile && !strcmp(logfile, "-")) { + if (isatty(0)) + fprintf(stderr, _("(reading log message from standard input)\n")); + if (strbuf_read(&sb, 0, 0) < 0) + die_errno(_("could not read log from standard input")); + hook_arg1 = "message"; + } else if (logfile) { + if (strbuf_read_file(&sb, logfile, 0) < 0) + die_errno(_("could not read log file '%s'"), + logfile); + hook_arg1 = "message"; + } else if (use_message) { + char *buffer; + buffer = strstr(use_message_buffer, "\n\n"); + if (buffer) + strbuf_addstr(&sb, skip_blank_lines(buffer + 2)); + hook_arg1 = "commit"; + hook_arg2 = use_message; + } else if (fixup_message) { + struct pretty_print_context ctx = {0}; + struct commit *commit; + char *fmt; + commit = lookup_commit_reference_by_name(fixup_commit); + if (!commit) + die(_("could not lookup commit '%s'"), fixup_commit); + ctx.output_encoding = get_commit_output_encoding(); + fmt = xstrfmt("%s! %%s\n\n", fixup_prefix); + repo_format_commit_message(the_repository, commit, fmt, &sb, + &ctx); + free(fmt); + hook_arg1 = "message"; + + /* + * Only `-m` commit message option is checked here, as + * it supports `--fixup` to append the commit message. + * + * The other commit message options `-c`/`-C`/`-F` are + * incompatible with all the forms of `--fixup` and + * have already errored out while parsing the `git commit` + * options. + */ + if (have_option_m && !strcmp(fixup_prefix, "fixup")) + strbuf_addbuf(&sb, &message); + + if (!strcmp(fixup_prefix, "amend")) { + if (have_option_m) + die(_("options '%s' and '%s:%s' cannot be used together"), "-m", "--fixup", fixup_message); + prepare_amend_commit(commit, &sb, &ctx); + } + } else if (!stat(git_path_merge_msg(the_repository), &statbuf)) { + size_t merge_msg_start; + + /* + * prepend SQUASH_MSG here if it exists and a + * "merge --squash" was originally performed + */ + if (!stat(git_path_squash_msg(the_repository), &statbuf)) { + if (strbuf_read_file(&sb, git_path_squash_msg(the_repository), 0) < 0) + die_errno(_("could not read SQUASH_MSG")); + hook_arg1 = "squash"; + } else + hook_arg1 = "merge"; + + merge_msg_start = sb.len; + if (strbuf_read_file(&sb, git_path_merge_msg(the_repository), 0) < 0) + die_errno(_("could not read MERGE_MSG")); + + if (cleanup_mode == COMMIT_MSG_CLEANUP_SCISSORS && + wt_status_locate_end(sb.buf + merge_msg_start, + sb.len - merge_msg_start) < + sb.len - merge_msg_start) + merge_contains_scissors = 1; + } else if (!stat(git_path_squash_msg(the_repository), &statbuf)) { + if (strbuf_read_file(&sb, git_path_squash_msg(the_repository), 0) < 0) + die_errno(_("could not read SQUASH_MSG")); + hook_arg1 = "squash"; + } else if (template_file) { + if (strbuf_read_file(&sb, template_file, 0) < 0) + die_errno(_("could not read '%s'"), template_file); + hook_arg1 = "template"; + clean_message_contents = 0; + } + + /* + * The remaining cases don't modify the template message, but + * just set the argument(s) to the prepare-commit-msg hook. + */ + else if (whence == FROM_MERGE) + hook_arg1 = "merge"; + else if (is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK) { + hook_arg1 = "commit"; + hook_arg2 = "CHERRY_PICK_HEAD"; + } + + if (squash_message) { + /* + * If squash_commit was used for the commit subject, + * then we're possibly hijacking other commit log options. + * Reset the hook args to tell the real story. + */ + hook_arg1 = "message"; + hook_arg2 = ""; + } + + s->fp = fopen_for_writing(git_path_commit_editmsg()); + if (!s->fp) + die_errno(_("could not open '%s'"), git_path_commit_editmsg()); + + /* Ignore status.displayCommentPrefix: we do need comments in COMMIT_EDITMSG. */ + old_display_comment_prefix = s->display_comment_prefix; + s->display_comment_prefix = 1; + + /* + * Most hints are counter-productive when the commit has + * already started. + */ + s->hints = 0; + + if (clean_message_contents) + strbuf_stripspace(&sb, '\0'); + + if (signoff) + append_signoff(&sb, ignore_non_trailer(sb.buf, sb.len), 0); + + if (fwrite(sb.buf, 1, sb.len, s->fp) < sb.len) + die_errno(_("could not write commit template")); + + if (auto_comment_line_char) + adjust_comment_line_char(&sb); + strbuf_release(&sb); + + /* This checks if committer ident is explicitly given */ + strbuf_addstr(&committer_ident, git_committer_info(IDENT_STRICT)); + if (use_editor && include_status) { + int ident_shown = 0; + int saved_color_setting; + struct ident_split ci, ai; + const char *hint_cleanup_all = allow_empty_message ? + _("Please enter the commit message for your changes." + " Lines starting\nwith '%c' will be ignored.\n") : + _("Please enter the commit message for your changes." + " Lines starting\nwith '%c' will be ignored, and an empty" + " message aborts the commit.\n"); + const char *hint_cleanup_space = allow_empty_message ? + _("Please enter the commit message for your changes." + " Lines starting\n" + "with '%c' will be kept; you may remove them" + " yourself if you want to.\n") : + _("Please enter the commit message for your changes." + " Lines starting\n" + "with '%c' will be kept; you may remove them" + " yourself if you want to.\n" + "An empty message aborts the commit.\n"); + if (whence != FROM_COMMIT) { + if (cleanup_mode == COMMIT_MSG_CLEANUP_SCISSORS && + !merge_contains_scissors) + wt_status_add_cut_line(s->fp); + status_printf_ln( + s, GIT_COLOR_NORMAL, + whence == FROM_MERGE ? + _("\n" + "It looks like you may be committing a merge.\n" + "If this is not correct, please run\n" + " git update-ref -d MERGE_HEAD\n" + "and try again.\n") : + _("\n" + "It looks like you may be committing a cherry-pick.\n" + "If this is not correct, please run\n" + " git update-ref -d CHERRY_PICK_HEAD\n" + "and try again.\n")); + } + + fprintf(s->fp, "\n"); + if (cleanup_mode == COMMIT_MSG_CLEANUP_ALL) + status_printf(s, GIT_COLOR_NORMAL, hint_cleanup_all, comment_line_char); + else if (cleanup_mode == COMMIT_MSG_CLEANUP_SCISSORS) { + if (whence == FROM_COMMIT && !merge_contains_scissors) + wt_status_add_cut_line(s->fp); + } else /* COMMIT_MSG_CLEANUP_SPACE, that is. */ + status_printf(s, GIT_COLOR_NORMAL, hint_cleanup_space, comment_line_char); + + /* + * These should never fail because they come from our own + * fmt_ident. They may fail the sane_ident test, but we know + * that the name and mail pointers will at least be valid, + * which is enough for our tests and printing here. + */ + assert_split_ident(&ai, author_ident); + assert_split_ident(&ci, &committer_ident); + + if (ident_cmp(&ai, &ci)) + status_printf_ln(s, GIT_COLOR_NORMAL, + _("%s" + "Author: %.*s <%.*s>"), + ident_shown++ ? "" : "\n", + (int)(ai.name_end - ai.name_begin), ai.name_begin, + (int)(ai.mail_end - ai.mail_begin), ai.mail_begin); + + if (author_date_is_interesting()) + status_printf_ln(s, GIT_COLOR_NORMAL, + _("%s" + "Date: %s"), + ident_shown++ ? "" : "\n", + show_ident_date(&ai, DATE_MODE(NORMAL))); + + if (!committer_ident_sufficiently_given()) + status_printf_ln(s, GIT_COLOR_NORMAL, + _("%s" + "Committer: %.*s <%.*s>"), + ident_shown++ ? "" : "\n", + (int)(ci.name_end - ci.name_begin), ci.name_begin, + (int)(ci.mail_end - ci.mail_begin), ci.mail_begin); + + status_printf_ln(s, GIT_COLOR_NORMAL, "%s", ""); /* Add new line for clarity */ + + saved_color_setting = s->use_color; + s->use_color = 0; + committable = run_status(s->fp, index_file, prefix, 1, s); + s->use_color = saved_color_setting; + string_list_clear(&s->change, 1); + } else { + struct object_id oid; + const char *parent = "HEAD"; + + if (!the_index.initialized && repo_read_index(the_repository) < 0) + die(_("Cannot read index")); + + if (amend) + parent = "HEAD^1"; + + if (repo_get_oid(the_repository, parent, &oid)) { + int i, ita_nr = 0; + + /* TODO: audit for interaction with sparse-index. */ + ensure_full_index(&the_index); + for (i = 0; i < the_index.cache_nr; i++) + if (ce_intent_to_add(the_index.cache[i])) + ita_nr++; + committable = the_index.cache_nr - ita_nr > 0; + } else { + /* + * Unless the user did explicitly request a submodule + * ignore mode by passing a command line option we do + * not ignore any changed submodule SHA-1s when + * comparing index and parent, no matter what is + * configured. Otherwise we won't commit any + * submodules which were manually staged, which would + * be really confusing. + */ + struct diff_flags flags = DIFF_FLAGS_INIT; + flags.override_submodule_config = 1; + if (ignore_submodule_arg && + !strcmp(ignore_submodule_arg, "all")) + flags.ignore_submodules = 1; + committable = index_differs_from(the_repository, + parent, &flags, 1); + } + } + strbuf_release(&committer_ident); + + fclose(s->fp); + + if (trailer_args.nr) { + struct child_process run_trailer = CHILD_PROCESS_INIT; + + strvec_pushl(&run_trailer.args, "interpret-trailers", + "--in-place", "--no-divider", + git_path_commit_editmsg(), NULL); + strvec_pushv(&run_trailer.args, trailer_args.v); + run_trailer.git_cmd = 1; + if (run_command(&run_trailer)) + die(_("unable to pass trailers to --trailers")); + strvec_clear(&trailer_args); + } + + /* + * Reject an attempt to record a non-merge empty commit without + * explicit --allow-empty. In the cherry-pick case, it may be + * empty due to conflict resolution, which the user should okay. + */ + if (!committable && whence != FROM_MERGE && !allow_empty && + !(amend && is_a_merge(current_head))) { + s->hints = advice_enabled(ADVICE_STATUS_HINTS); + s->display_comment_prefix = old_display_comment_prefix; + run_status(stdout, index_file, prefix, 0, s); + if (amend) + fputs(_(empty_amend_advice), stderr); + else if (is_from_cherry_pick(whence) || + whence == FROM_REBASE_PICK) { + fputs(_(empty_cherry_pick_advice), stderr); + if (whence == FROM_CHERRY_PICK_SINGLE) + fputs(_(empty_cherry_pick_advice_single), stderr); + else if (whence == FROM_CHERRY_PICK_MULTI) + fputs(_(empty_cherry_pick_advice_multi), stderr); + else + fputs(_(empty_rebase_pick_advice), stderr); + } + return 0; + } + + if (!no_verify && invoked_hook) { + /* + * Re-read the index as the pre-commit-commit hook was invoked + * and could have updated it. We must do this before we invoke + * the editor and after we invoke run_status above. + */ + discard_index(&the_index); + } + read_index_from(&the_index, index_file, get_git_dir()); + + if (cache_tree_update(&the_index, 0)) { + error(_("Error building trees")); + return 0; + } + + if (run_commit_hook(use_editor, index_file, NULL, "prepare-commit-msg", + git_path_commit_editmsg(), hook_arg1, hook_arg2, NULL)) + return 0; + + if (use_editor) { + struct strvec env = STRVEC_INIT; + + strvec_pushf(&env, "GIT_INDEX_FILE=%s", index_file); + if (launch_editor(git_path_commit_editmsg(), NULL, env.v)) { + fprintf(stderr, + _("Please supply the message using either -m or -F option.\n")); + exit(1); + } + strvec_clear(&env); + } + + if (!no_verify && + run_commit_hook(use_editor, index_file, NULL, "commit-msg", + git_path_commit_editmsg(), NULL)) { + return 0; + } + + return 1; +} + +static const char *find_author_by_nickname(const char *name) +{ + struct rev_info revs; + struct commit *commit; + struct strbuf buf = STRBUF_INIT; + const char *av[20]; + int ac = 0; + + repo_init_revisions(the_repository, &revs, NULL); + strbuf_addf(&buf, "--author=%s", name); + av[++ac] = "--all"; + av[++ac] = "-i"; + av[++ac] = buf.buf; + av[++ac] = NULL; + setup_revisions(ac, av, &revs, NULL); + revs.mailmap = xmalloc(sizeof(struct string_list)); + string_list_init_nodup(revs.mailmap); + read_mailmap(revs.mailmap); + + if (prepare_revision_walk(&revs)) + die(_("revision walk setup failed")); + commit = get_revision(&revs); + if (commit) { + struct pretty_print_context ctx = {0}; + ctx.date_mode.type = DATE_NORMAL; + strbuf_release(&buf); + repo_format_commit_message(the_repository, commit, + "%aN <%aE>", &buf, &ctx); + release_revisions(&revs); + return strbuf_detach(&buf, NULL); + } + die(_("--author '%s' is not 'Name <email>' and matches no existing author"), name); +} + +static void handle_ignored_arg(struct wt_status *s) +{ + if (!ignored_arg) + ; /* default already initialized */ + else if (!strcmp(ignored_arg, "traditional")) + s->show_ignored_mode = SHOW_TRADITIONAL_IGNORED; + else if (!strcmp(ignored_arg, "no")) + s->show_ignored_mode = SHOW_NO_IGNORED; + else if (!strcmp(ignored_arg, "matching")) + s->show_ignored_mode = SHOW_MATCHING_IGNORED; + else + die(_("Invalid ignored mode '%s'"), ignored_arg); +} + +static void handle_untracked_files_arg(struct wt_status *s) +{ + if (!untracked_files_arg) + ; /* default already initialized */ + else if (!strcmp(untracked_files_arg, "no")) + s->show_untracked_files = SHOW_NO_UNTRACKED_FILES; + else if (!strcmp(untracked_files_arg, "normal")) + s->show_untracked_files = SHOW_NORMAL_UNTRACKED_FILES; + else if (!strcmp(untracked_files_arg, "all")) + s->show_untracked_files = SHOW_ALL_UNTRACKED_FILES; + /* + * Please update $__git_untracked_file_modes in + * git-completion.bash when you add new options + */ + else + die(_("Invalid untracked files mode '%s'"), untracked_files_arg); +} + +static const char *read_commit_message(const char *name) +{ + const char *out_enc; + struct commit *commit; + + commit = lookup_commit_reference_by_name(name); + if (!commit) + die(_("could not lookup commit '%s'"), name); + out_enc = get_commit_output_encoding(); + return repo_logmsg_reencode(the_repository, commit, NULL, out_enc); +} + +/* + * Enumerate what needs to be propagated when --porcelain + * is not in effect here. + */ +static struct status_deferred_config { + enum wt_status_format status_format; + int show_branch; + enum ahead_behind_flags ahead_behind; +} status_deferred_config = { + STATUS_FORMAT_UNSPECIFIED, + -1, /* unspecified */ + AHEAD_BEHIND_UNSPECIFIED, +}; + +static void finalize_deferred_config(struct wt_status *s) +{ + int use_deferred_config = (status_format != STATUS_FORMAT_PORCELAIN && + status_format != STATUS_FORMAT_PORCELAIN_V2 && + !s->null_termination); + + if (s->null_termination) { + if (status_format == STATUS_FORMAT_NONE || + status_format == STATUS_FORMAT_UNSPECIFIED) + status_format = STATUS_FORMAT_PORCELAIN; + else if (status_format == STATUS_FORMAT_LONG) + die(_("options '%s' and '%s' cannot be used together"), "--long", "-z"); + } + + if (use_deferred_config && status_format == STATUS_FORMAT_UNSPECIFIED) + status_format = status_deferred_config.status_format; + if (status_format == STATUS_FORMAT_UNSPECIFIED) + status_format = STATUS_FORMAT_NONE; + + if (use_deferred_config && s->show_branch < 0) + s->show_branch = status_deferred_config.show_branch; + if (s->show_branch < 0) + s->show_branch = 0; + + /* + * If the user did not give a "--[no]-ahead-behind" command + * line argument *AND* we will print in a human-readable format + * (short, long etc.) then we inherit from the status.aheadbehind + * config setting. In all other cases (and porcelain V[12] formats + * in particular), we inherit _FULL for backwards compatibility. + */ + if (use_deferred_config && + s->ahead_behind_flags == AHEAD_BEHIND_UNSPECIFIED) + s->ahead_behind_flags = status_deferred_config.ahead_behind; + + if (s->ahead_behind_flags == AHEAD_BEHIND_UNSPECIFIED) + s->ahead_behind_flags = AHEAD_BEHIND_FULL; +} + +static void check_fixup_reword_options(int argc, const char *argv[]) { + if (whence != FROM_COMMIT) { + if (whence == FROM_MERGE) + die(_("You are in the middle of a merge -- cannot reword.")); + else if (is_from_cherry_pick(whence)) + die(_("You are in the middle of a cherry-pick -- cannot reword.")); + } + if (argc) + die(_("reword option of '%s' and path '%s' cannot be used together"), "--fixup", *argv); + if (patch_interactive || interactive || all || also || only) + die(_("reword option of '%s' and '%s' cannot be used together"), + "--fixup", "--patch/--interactive/--all/--include/--only"); +} + +static int parse_and_validate_options(int argc, const char *argv[], + const struct option *options, + const char * const usage[], + const char *prefix, + struct commit *current_head, + struct wt_status *s) +{ + argc = parse_options(argc, argv, prefix, options, usage, 0); + finalize_deferred_config(s); + + if (force_author && !strchr(force_author, '>')) + force_author = find_author_by_nickname(force_author); + + if (force_author && renew_authorship) + die(_("options '%s' and '%s' cannot be used together"), "--reset-author", "--author"); + + if (logfile || have_option_m || use_message) + use_editor = 0; + + /* Sanity check options */ + if (amend && !current_head) + die(_("You have nothing to amend.")); + if (amend && whence != FROM_COMMIT) { + if (whence == FROM_MERGE) + die(_("You are in the middle of a merge -- cannot amend.")); + else if (is_from_cherry_pick(whence)) + die(_("You are in the middle of a cherry-pick -- cannot amend.")); + else if (whence == FROM_REBASE_PICK) + die(_("You are in the middle of a rebase -- cannot amend.")); + } + if (fixup_message && squash_message) + die(_("options '%s' and '%s' cannot be used together"), "--squash", "--fixup"); + die_for_incompatible_opt4(!!use_message, "-C", + !!edit_message, "-c", + !!logfile, "-F", + !!fixup_message, "--fixup"); + die_for_incompatible_opt4(have_option_m, "-m", + !!edit_message, "-c", + !!use_message, "-C", + !!logfile, "-F"); + if (use_message || edit_message || logfile ||fixup_message || have_option_m) + template_file = NULL; + if (edit_message) + use_message = edit_message; + if (amend && !use_message && !fixup_message) + use_message = "HEAD"; + if (!use_message && !is_from_cherry_pick(whence) && + !is_from_rebase(whence) && renew_authorship) + die(_("--reset-author can be used only with -C, -c or --amend.")); + if (use_message) { + use_message_buffer = read_commit_message(use_message); + if (!renew_authorship) { + author_message = use_message; + author_message_buffer = use_message_buffer; + } + } + if ((is_from_cherry_pick(whence) || whence == FROM_REBASE_PICK) && + !renew_authorship) { + author_message = "CHERRY_PICK_HEAD"; + author_message_buffer = read_commit_message(author_message); + } + + if (patch_interactive) + interactive = 1; + + die_for_incompatible_opt4(also, "-i/--include", + only, "-o/--only", + all, "-a/--all", + interactive, "--interactive/-p/--patch"); + if (fixup_message) { + /* + * We limit --fixup's suboptions to only alpha characters. + * If the first character after a run of alpha is colon, + * then the part before the colon may be a known suboption + * name like `amend` or `reword`, or a misspelt suboption + * name. In either case, we treat it as + * --fixup=<suboption>:<arg>. + * + * Otherwise, we are dealing with --fixup=<commit>. + */ + char *p = fixup_message; + while (isalpha(*p)) + p++; + if (p > fixup_message && *p == ':') { + *p = '\0'; + fixup_commit = p + 1; + if (!strcmp("amend", fixup_message) || + !strcmp("reword", fixup_message)) { + fixup_prefix = "amend"; + allow_empty = 1; + if (*fixup_message == 'r') { + check_fixup_reword_options(argc, argv); + only = 1; + } + } else { + die(_("unknown option: --fixup=%s:%s"), fixup_message, fixup_commit); + } + } else { + fixup_commit = fixup_message; + fixup_prefix = "fixup"; + use_editor = 0; + } + } + + if (0 <= edit_flag) + use_editor = edit_flag; + + cleanup_mode = get_cleanup_mode(cleanup_arg, use_editor); + + handle_untracked_files_arg(s); + + if (all && argc > 0) + die(_("paths '%s ...' with -a does not make sense"), + argv[0]); + + if (status_format != STATUS_FORMAT_NONE) + dry_run = 1; + + return argc; +} + +static int dry_run_commit(const char **argv, const char *prefix, + const struct commit *current_head, struct wt_status *s) +{ + int committable; + const char *index_file; + + index_file = prepare_index(argv, prefix, current_head, 1); + committable = run_status(stdout, index_file, prefix, 0, s); + rollback_index_files(); + + return committable ? 0 : 1; +} + +define_list_config_array_extra(color_status_slots, {"added"}); + +static int parse_status_slot(const char *slot) +{ + if (!strcasecmp(slot, "added")) + return WT_STATUS_UPDATED; + + return LOOKUP_CONFIG(color_status_slots, slot); +} + +static int git_status_config(const char *k, const char *v, + const struct config_context *ctx, void *cb) +{ + struct wt_status *s = cb; + const char *slot_name; + + if (starts_with(k, "column.")) + return git_column_config(k, v, "status", &s->colopts); + if (!strcmp(k, "status.submodulesummary")) { + int is_bool; + s->submodule_summary = git_config_bool_or_int(k, v, ctx->kvi, + &is_bool); + if (is_bool && s->submodule_summary) + s->submodule_summary = -1; + return 0; + } + if (!strcmp(k, "status.short")) { + if (git_config_bool(k, v)) + status_deferred_config.status_format = STATUS_FORMAT_SHORT; + else + status_deferred_config.status_format = STATUS_FORMAT_NONE; + return 0; + } + if (!strcmp(k, "status.branch")) { + status_deferred_config.show_branch = git_config_bool(k, v); + return 0; + } + if (!strcmp(k, "status.aheadbehind")) { + status_deferred_config.ahead_behind = git_config_bool(k, v); + return 0; + } + if (!strcmp(k, "status.showstash")) { + s->show_stash = git_config_bool(k, v); + return 0; + } + if (!strcmp(k, "status.color") || !strcmp(k, "color.status")) { + s->use_color = git_config_colorbool(k, v); + return 0; + } + if (!strcmp(k, "status.displaycommentprefix")) { + s->display_comment_prefix = git_config_bool(k, v); + return 0; + } + if (skip_prefix(k, "status.color.", &slot_name) || + skip_prefix(k, "color.status.", &slot_name)) { + int slot = parse_status_slot(slot_name); + if (slot < 0) + return 0; + if (!v) + return config_error_nonbool(k); + return color_parse(v, s->color_palette[slot]); + } + if (!strcmp(k, "status.relativepaths")) { + s->relative_paths = git_config_bool(k, v); + return 0; + } + if (!strcmp(k, "status.showuntrackedfiles")) { + if (!v) + return config_error_nonbool(k); + else if (!strcmp(v, "no")) + s->show_untracked_files = SHOW_NO_UNTRACKED_FILES; + else if (!strcmp(v, "normal")) + s->show_untracked_files = SHOW_NORMAL_UNTRACKED_FILES; + else if (!strcmp(v, "all")) + s->show_untracked_files = SHOW_ALL_UNTRACKED_FILES; + else + return error(_("Invalid untracked files mode '%s'"), v); + return 0; + } + if (!strcmp(k, "diff.renamelimit")) { + if (s->rename_limit == -1) + s->rename_limit = git_config_int(k, v, ctx->kvi); + return 0; + } + if (!strcmp(k, "status.renamelimit")) { + s->rename_limit = git_config_int(k, v, ctx->kvi); + return 0; + } + if (!strcmp(k, "diff.renames")) { + if (s->detect_rename == -1) + s->detect_rename = git_config_rename(k, v); + return 0; + } + if (!strcmp(k, "status.renames")) { + s->detect_rename = git_config_rename(k, v); + return 0; + } + return git_diff_ui_config(k, v, ctx, NULL); +} + +int cmd_status(int argc, const char **argv, const char *prefix) +{ + static int no_renames = -1; + static const char *rename_score_arg = (const char *)-1; + static struct wt_status s; + unsigned int progress_flag = 0; + int fd; + struct object_id oid; + static struct option builtin_status_options[] = { + OPT__VERBOSE(&verbose, N_("be verbose")), + OPT_SET_INT('s', "short", &status_format, + N_("show status concisely"), STATUS_FORMAT_SHORT), + OPT_BOOL('b', "branch", &s.show_branch, + N_("show branch information")), + OPT_BOOL(0, "show-stash", &s.show_stash, + N_("show stash information")), + OPT_BOOL(0, "ahead-behind", &s.ahead_behind_flags, + N_("compute full ahead/behind values")), + OPT_CALLBACK_F(0, "porcelain", &status_format, + N_("version"), N_("machine-readable output"), + PARSE_OPT_OPTARG, opt_parse_porcelain), + OPT_SET_INT(0, "long", &status_format, + N_("show status in long format (default)"), + STATUS_FORMAT_LONG), + OPT_BOOL('z', "null", &s.null_termination, + N_("terminate entries with NUL")), + { OPTION_STRING, 'u', "untracked-files", &untracked_files_arg, + N_("mode"), + N_("show untracked files, optional modes: all, normal, no. (Default: all)"), + PARSE_OPT_OPTARG, NULL, (intptr_t)"all" }, + { OPTION_STRING, 0, "ignored", &ignored_arg, + N_("mode"), + N_("show ignored files, optional modes: traditional, matching, no. (Default: traditional)"), + PARSE_OPT_OPTARG, NULL, (intptr_t)"traditional" }, + { OPTION_STRING, 0, "ignore-submodules", &ignore_submodule_arg, N_("when"), + N_("ignore changes to submodules, optional when: all, dirty, untracked. (Default: all)"), + PARSE_OPT_OPTARG, NULL, (intptr_t)"all" }, + OPT_COLUMN(0, "column", &s.colopts, N_("list untracked files in columns")), + OPT_BOOL(0, "no-renames", &no_renames, N_("do not detect renames")), + OPT_CALLBACK_F('M', "find-renames", &rename_score_arg, + N_("n"), N_("detect renames, optionally set similarity index"), + PARSE_OPT_OPTARG | PARSE_OPT_NONEG, opt_parse_rename_score), + OPT_END(), + }; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(builtin_status_usage, builtin_status_options); + + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + status_init_config(&s, git_status_config); + argc = parse_options(argc, argv, prefix, + builtin_status_options, + builtin_status_usage, 0); + finalize_colopts(&s.colopts, -1); + finalize_deferred_config(&s); + + handle_untracked_files_arg(&s); + handle_ignored_arg(&s); + + if (s.show_ignored_mode == SHOW_MATCHING_IGNORED && + s.show_untracked_files == SHOW_NO_UNTRACKED_FILES) + die(_("Unsupported combination of ignored and untracked-files arguments")); + + parse_pathspec(&s.pathspec, 0, + PATHSPEC_PREFER_FULL, + prefix, argv); + + if (status_format != STATUS_FORMAT_PORCELAIN && + status_format != STATUS_FORMAT_PORCELAIN_V2) + progress_flag = REFRESH_PROGRESS; + repo_read_index(the_repository); + refresh_index(&the_index, + REFRESH_QUIET|REFRESH_UNMERGED|progress_flag, + &s.pathspec, NULL, NULL); + + if (use_optional_locks()) + fd = repo_hold_locked_index(the_repository, &index_lock, 0); + else + fd = -1; + + s.is_initial = repo_get_oid(the_repository, s.reference, &oid) ? 1 : 0; + if (!s.is_initial) + oidcpy(&s.oid_commit, &oid); + + s.ignore_submodule_arg = ignore_submodule_arg; + s.status_format = status_format; + s.verbose = verbose; + if (no_renames != -1) + s.detect_rename = !no_renames; + if ((intptr_t)rename_score_arg != -1) { + if (s.detect_rename < DIFF_DETECT_RENAME) + s.detect_rename = DIFF_DETECT_RENAME; + if (rename_score_arg) + s.rename_score = parse_rename_score(&rename_score_arg); + } + + wt_status_collect(&s); + + if (0 <= fd) + repo_update_index_if_able(the_repository, &index_lock); + + if (s.relative_paths) + s.prefix = prefix; + + wt_status_print(&s); + wt_status_collect_free_buffers(&s); + + return 0; +} + +static int git_commit_config(const char *k, const char *v, + const struct config_context *ctx, void *cb) +{ + struct wt_status *s = cb; + + if (!strcmp(k, "commit.template")) + return git_config_pathname(&template_file, k, v); + if (!strcmp(k, "commit.status")) { + include_status = git_config_bool(k, v); + return 0; + } + if (!strcmp(k, "commit.cleanup")) + return git_config_string(&cleanup_arg, k, v); + if (!strcmp(k, "commit.gpgsign")) { + sign_commit = git_config_bool(k, v) ? "" : NULL; + return 0; + } + if (!strcmp(k, "commit.verbose")) { + int is_bool; + config_commit_verbose = git_config_bool_or_int(k, v, ctx->kvi, + &is_bool); + return 0; + } + + return git_status_config(k, v, ctx, s); +} + +int cmd_commit(int argc, const char **argv, const char *prefix) +{ + static struct wt_status s; + static struct option builtin_commit_options[] = { + OPT__QUIET(&quiet, N_("suppress summary after successful commit")), + OPT__VERBOSE(&verbose, N_("show diff in commit message template")), + + OPT_GROUP(N_("Commit message options")), + OPT_FILENAME('F', "file", &logfile, N_("read message from file")), + OPT_STRING(0, "author", &force_author, N_("author"), N_("override author for commit")), + OPT_STRING(0, "date", &force_date, N_("date"), N_("override date for commit")), + OPT_CALLBACK('m', "message", &message, N_("message"), N_("commit message"), opt_parse_m), + OPT_STRING('c', "reedit-message", &edit_message, N_("commit"), N_("reuse and edit message from specified commit")), + OPT_STRING('C', "reuse-message", &use_message, N_("commit"), N_("reuse message from specified commit")), + /* + * TRANSLATORS: Leave "[(amend|reword):]" as-is, + * and only translate <commit>. + */ + OPT_STRING(0, "fixup", &fixup_message, N_("[(amend|reword):]commit"), N_("use autosquash formatted message to fixup or amend/reword specified commit")), + OPT_STRING(0, "squash", &squash_message, N_("commit"), N_("use autosquash formatted message to squash specified commit")), + OPT_BOOL(0, "reset-author", &renew_authorship, N_("the commit is authored by me now (used with -C/-c/--amend)")), + OPT_CALLBACK_F(0, "trailer", &trailer_args, N_("trailer"), N_("add custom trailer(s)"), PARSE_OPT_NONEG, opt_pass_trailer), + OPT_BOOL('s', "signoff", &signoff, N_("add a Signed-off-by trailer")), + OPT_FILENAME('t', "template", &template_file, N_("use specified template file")), + OPT_BOOL('e', "edit", &edit_flag, N_("force edit of commit")), + OPT_CLEANUP(&cleanup_arg), + OPT_BOOL(0, "status", &include_status, N_("include status in commit message template")), + { OPTION_STRING, 'S', "gpg-sign", &sign_commit, N_("key-id"), + N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + /* end commit message options */ + + OPT_GROUP(N_("Commit contents options")), + OPT_BOOL('a', "all", &all, N_("commit all changed files")), + OPT_BOOL('i', "include", &also, N_("add specified files to index for commit")), + OPT_BOOL(0, "interactive", &interactive, N_("interactively add files")), + OPT_BOOL('p', "patch", &patch_interactive, N_("interactively add changes")), + OPT_BOOL('o', "only", &only, N_("commit only specified files")), + OPT_BOOL('n', "no-verify", &no_verify, N_("bypass pre-commit and commit-msg hooks")), + OPT_BOOL(0, "dry-run", &dry_run, N_("show what would be committed")), + OPT_SET_INT(0, "short", &status_format, N_("show status concisely"), + STATUS_FORMAT_SHORT), + OPT_BOOL(0, "branch", &s.show_branch, N_("show branch information")), + OPT_BOOL(0, "ahead-behind", &s.ahead_behind_flags, + N_("compute full ahead/behind values")), + OPT_SET_INT(0, "porcelain", &status_format, + N_("machine-readable output"), STATUS_FORMAT_PORCELAIN), + OPT_SET_INT(0, "long", &status_format, + N_("show status in long format (default)"), + STATUS_FORMAT_LONG), + OPT_BOOL('z', "null", &s.null_termination, + N_("terminate entries with NUL")), + OPT_BOOL(0, "amend", &amend, N_("amend previous commit")), + OPT_BOOL(0, "no-post-rewrite", &no_post_rewrite, N_("bypass post-rewrite hook")), + { OPTION_STRING, 'u', "untracked-files", &untracked_files_arg, N_("mode"), N_("show untracked files, optional modes: all, normal, no. (Default: all)"), PARSE_OPT_OPTARG, NULL, (intptr_t)"all" }, + OPT_PATHSPEC_FROM_FILE(&pathspec_from_file), + OPT_PATHSPEC_FILE_NUL(&pathspec_file_nul), + /* end commit contents options */ + + OPT_HIDDEN_BOOL(0, "allow-empty", &allow_empty, + N_("ok to record an empty change")), + OPT_HIDDEN_BOOL(0, "allow-empty-message", &allow_empty_message, + N_("ok to record a change with an empty message")), + + OPT_END() + }; + + struct strbuf sb = STRBUF_INIT; + struct strbuf author_ident = STRBUF_INIT; + const char *index_file, *reflog_msg; + struct object_id oid; + struct commit_list *parents = NULL; + struct stat statbuf; + struct commit *current_head = NULL; + struct commit_extra_header *extra = NULL; + struct strbuf err = STRBUF_INIT; + int ret = 0; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(builtin_commit_usage, builtin_commit_options); + + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + status_init_config(&s, git_commit_config); + s.commit_template = 1; + status_format = STATUS_FORMAT_NONE; /* Ignore status.short */ + s.colopts = 0; + + if (repo_get_oid(the_repository, "HEAD", &oid)) + current_head = NULL; + else { + current_head = lookup_commit_or_die(&oid, "HEAD"); + if (repo_parse_commit(the_repository, current_head)) + die(_("could not parse HEAD commit")); + } + verbose = -1; /* unspecified */ + argc = parse_and_validate_options(argc, argv, builtin_commit_options, + builtin_commit_usage, + prefix, current_head, &s); + if (verbose == -1) + verbose = (config_commit_verbose < 0) ? 0 : config_commit_verbose; + + if (dry_run) + return dry_run_commit(argv, prefix, current_head, &s); + index_file = prepare_index(argv, prefix, current_head, 0); + + /* Set up everything for writing the commit object. This includes + running hooks, writing the trees, and interacting with the user. */ + if (!prepare_to_commit(index_file, prefix, + current_head, &s, &author_ident)) { + ret = 1; + rollback_index_files(); + goto cleanup; + } + + /* Determine parents */ + reflog_msg = getenv("GIT_REFLOG_ACTION"); + if (!current_head) { + if (!reflog_msg) + reflog_msg = "commit (initial)"; + } else if (amend) { + if (!reflog_msg) + reflog_msg = "commit (amend)"; + parents = copy_commit_list(current_head->parents); + } else if (whence == FROM_MERGE) { + struct strbuf m = STRBUF_INIT; + FILE *fp; + int allow_fast_forward = 1; + struct commit_list **pptr = &parents; + + if (!reflog_msg) + reflog_msg = "commit (merge)"; + pptr = commit_list_append(current_head, pptr); + fp = xfopen(git_path_merge_head(the_repository), "r"); + while (strbuf_getline_lf(&m, fp) != EOF) { + struct commit *parent; + + parent = get_merge_parent(m.buf); + if (!parent) + die(_("Corrupt MERGE_HEAD file (%s)"), m.buf); + pptr = commit_list_append(parent, pptr); + } + fclose(fp); + strbuf_release(&m); + if (!stat(git_path_merge_mode(the_repository), &statbuf)) { + if (strbuf_read_file(&sb, git_path_merge_mode(the_repository), 0) < 0) + die_errno(_("could not read MERGE_MODE")); + if (!strcmp(sb.buf, "no-ff")) + allow_fast_forward = 0; + } + if (allow_fast_forward) + reduce_heads_replace(&parents); + } else { + if (!reflog_msg) + reflog_msg = is_from_cherry_pick(whence) + ? "commit (cherry-pick)" + : is_from_rebase(whence) + ? "commit (rebase)" + : "commit"; + commit_list_insert(current_head, &parents); + } + + /* Finally, get the commit message */ + strbuf_reset(&sb); + if (strbuf_read_file(&sb, git_path_commit_editmsg(), 0) < 0) { + int saved_errno = errno; + rollback_index_files(); + die(_("could not read commit message: %s"), strerror(saved_errno)); + } + + cleanup_message(&sb, cleanup_mode, verbose); + + if (message_is_empty(&sb, cleanup_mode) && !allow_empty_message) { + rollback_index_files(); + fprintf(stderr, _("Aborting commit due to empty commit message.\n")); + exit(1); + } + if (template_untouched(&sb, template_file, cleanup_mode) && !allow_empty_message) { + rollback_index_files(); + fprintf(stderr, _("Aborting commit; you did not edit the message.\n")); + exit(1); + } + + if (fixup_message && starts_with(sb.buf, "amend! ") && + !allow_empty_message) { + struct strbuf body = STRBUF_INIT; + size_t len = commit_subject_length(sb.buf); + strbuf_addstr(&body, sb.buf + len); + if (message_is_empty(&body, cleanup_mode)) { + rollback_index_files(); + fprintf(stderr, _("Aborting commit due to empty commit message body.\n")); + exit(1); + } + strbuf_release(&body); + } + + if (amend) { + const char *exclude_gpgsig[3] = { "gpgsig", "gpgsig-sha256", NULL }; + extra = read_commit_extra_headers(current_head, exclude_gpgsig); + } else { + struct commit_extra_header **tail = &extra; + append_merge_tag_headers(parents, &tail); + } + + if (commit_tree_extended(sb.buf, sb.len, &the_index.cache_tree->oid, + parents, &oid, author_ident.buf, NULL, + sign_commit, extra)) { + rollback_index_files(); + die(_("failed to write commit object")); + } + free_commit_extra_headers(extra); + + if (update_head_with_reflog(current_head, &oid, reflog_msg, &sb, + &err)) { + rollback_index_files(); + die("%s", err.buf); + } + + sequencer_post_commit_cleanup(the_repository, 0); + unlink(git_path_merge_head(the_repository)); + unlink(git_path_merge_msg(the_repository)); + unlink(git_path_merge_mode(the_repository)); + unlink(git_path_squash_msg(the_repository)); + + if (commit_index_files()) + die(_("repository has been updated, but unable to write\n" + "new index file. Check that disk is not full and quota is\n" + "not exceeded, and then \"git restore --staged :/\" to recover.")); + + git_test_write_commit_graph_or_die(); + + repo_rerere(the_repository, 0); + run_auto_maintenance(quiet); + run_commit_hook(use_editor, get_index_file(), NULL, "post-commit", + NULL); + if (amend && !no_post_rewrite) { + commit_post_rewrite(the_repository, current_head, &oid); + } + if (!quiet) { + unsigned int flags = 0; + + if (!current_head) + flags |= SUMMARY_INITIAL_COMMIT; + if (author_date_is_interesting()) + flags |= SUMMARY_SHOW_AUTHOR_DATE; + print_commit_summary(the_repository, prefix, + &oid, flags); + } + + apply_autostash(git_path_merge_autostash(the_repository)); + +cleanup: + strbuf_release(&author_ident); + strbuf_release(&err); + strbuf_release(&sb); + return ret; +} diff --git a/builtin/config.c b/builtin/config.c new file mode 100644 index 0000000..11a4d4e --- /dev/null +++ b/builtin/config.c @@ -0,0 +1,1002 @@ +#include "builtin.h" +#include "abspath.h" +#include "config.h" +#include "color.h" +#include "editor.h" +#include "environment.h" +#include "repository.h" +#include "gettext.h" +#include "ident.h" +#include "parse-options.h" +#include "urlmatch.h" +#include "path.h" +#include "quote.h" +#include "setup.h" +#include "strbuf.h" +#include "worktree.h" + +static const char *const builtin_config_usage[] = { + N_("git config [<options>]"), + NULL +}; + +static char *key; +static regex_t *key_regexp; +static const char *value_pattern; +static regex_t *regexp; +static int show_keys; +static int omit_values; +static int use_key_regexp; +static int do_all; +static int do_not_match; +static char delim = '='; +static char key_delim = ' '; +static char term = '\n'; + +static int use_global_config, use_system_config, use_local_config; +static int use_worktree_config; +static struct git_config_source given_config_source; +static int actions, type; +static char *default_value; +static int end_nul; +static int respect_includes_opt = -1; +static struct config_options config_options; +static int show_origin; +static int show_scope; +static int fixed_value; + +#define ACTION_GET (1<<0) +#define ACTION_GET_ALL (1<<1) +#define ACTION_GET_REGEXP (1<<2) +#define ACTION_REPLACE_ALL (1<<3) +#define ACTION_ADD (1<<4) +#define ACTION_UNSET (1<<5) +#define ACTION_UNSET_ALL (1<<6) +#define ACTION_RENAME_SECTION (1<<7) +#define ACTION_REMOVE_SECTION (1<<8) +#define ACTION_LIST (1<<9) +#define ACTION_EDIT (1<<10) +#define ACTION_SET (1<<11) +#define ACTION_SET_ALL (1<<12) +#define ACTION_GET_COLOR (1<<13) +#define ACTION_GET_COLORBOOL (1<<14) +#define ACTION_GET_URLMATCH (1<<15) + +/* + * The actions "ACTION_LIST | ACTION_GET_*" which may produce more than + * one line of output and which should therefore be paged. + */ +#define PAGING_ACTIONS (ACTION_LIST | ACTION_GET_ALL | \ + ACTION_GET_REGEXP | ACTION_GET_URLMATCH) + +#define TYPE_BOOL 1 +#define TYPE_INT 2 +#define TYPE_BOOL_OR_INT 3 +#define TYPE_PATH 4 +#define TYPE_EXPIRY_DATE 5 +#define TYPE_COLOR 6 +#define TYPE_BOOL_OR_STR 7 + +#define OPT_CALLBACK_VALUE(s, l, v, h, i) \ + { OPTION_CALLBACK, (s), (l), (v), NULL, (h), PARSE_OPT_NOARG | \ + PARSE_OPT_NONEG, option_parse_type, (i) } + +static NORETURN void usage_builtin_config(void); + +static int option_parse_type(const struct option *opt, const char *arg, + int unset) +{ + int new_type, *to_type; + + if (unset) { + *((int *) opt->value) = 0; + return 0; + } + + /* + * To support '--<type>' style flags, begin with new_type equal to + * opt->defval. + */ + new_type = opt->defval; + if (!new_type) { + if (!strcmp(arg, "bool")) + new_type = TYPE_BOOL; + else if (!strcmp(arg, "int")) + new_type = TYPE_INT; + else if (!strcmp(arg, "bool-or-int")) + new_type = TYPE_BOOL_OR_INT; + else if (!strcmp(arg, "bool-or-str")) + new_type = TYPE_BOOL_OR_STR; + else if (!strcmp(arg, "path")) + new_type = TYPE_PATH; + else if (!strcmp(arg, "expiry-date")) + new_type = TYPE_EXPIRY_DATE; + else if (!strcmp(arg, "color")) + new_type = TYPE_COLOR; + else + die(_("unrecognized --type argument, %s"), arg); + } + + to_type = opt->value; + if (*to_type && *to_type != new_type) { + /* + * Complain when there is a new type not equal to the old type. + * This allows for combinations like '--int --type=int' and + * '--type=int --type=int', but disallows ones like '--type=bool + * --int' and '--type=bool + * --type=int'. + */ + error(_("only one type at a time")); + usage_builtin_config(); + } + *to_type = new_type; + + return 0; +} + +static struct option builtin_config_options[] = { + OPT_GROUP(N_("Config file location")), + OPT_BOOL(0, "global", &use_global_config, N_("use global config file")), + OPT_BOOL(0, "system", &use_system_config, N_("use system config file")), + OPT_BOOL(0, "local", &use_local_config, N_("use repository config file")), + OPT_BOOL(0, "worktree", &use_worktree_config, N_("use per-worktree config file")), + OPT_STRING('f', "file", &given_config_source.file, N_("file"), N_("use given config file")), + OPT_STRING(0, "blob", &given_config_source.blob, N_("blob-id"), N_("read config from given blob object")), + OPT_GROUP(N_("Action")), + OPT_BIT(0, "get", &actions, N_("get value: name [value-pattern]"), ACTION_GET), + OPT_BIT(0, "get-all", &actions, N_("get all values: key [value-pattern]"), ACTION_GET_ALL), + OPT_BIT(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-pattern]"), ACTION_GET_REGEXP), + OPT_BIT(0, "get-urlmatch", &actions, N_("get value specific for the URL: section[.var] URL"), ACTION_GET_URLMATCH), + OPT_BIT(0, "replace-all", &actions, N_("replace all matching variables: name value [value-pattern]"), ACTION_REPLACE_ALL), + OPT_BIT(0, "add", &actions, N_("add a new variable: name value"), ACTION_ADD), + OPT_BIT(0, "unset", &actions, N_("remove a variable: name [value-pattern]"), ACTION_UNSET), + OPT_BIT(0, "unset-all", &actions, N_("remove all matches: name [value-pattern]"), ACTION_UNSET_ALL), + OPT_BIT(0, "rename-section", &actions, N_("rename section: old-name new-name"), ACTION_RENAME_SECTION), + OPT_BIT(0, "remove-section", &actions, N_("remove a section: name"), ACTION_REMOVE_SECTION), + OPT_BIT('l', "list", &actions, N_("list all"), ACTION_LIST), + OPT_BOOL(0, "fixed-value", &fixed_value, N_("use string equality when comparing values to 'value-pattern'")), + OPT_BIT('e', "edit", &actions, N_("open an editor"), ACTION_EDIT), + OPT_BIT(0, "get-color", &actions, N_("find the color configured: slot [default]"), ACTION_GET_COLOR), + OPT_BIT(0, "get-colorbool", &actions, N_("find the color setting: slot [stdout-is-tty]"), ACTION_GET_COLORBOOL), + OPT_GROUP(N_("Type")), + OPT_CALLBACK('t', "type", &type, N_("type"), N_("value is given this type"), option_parse_type), + OPT_CALLBACK_VALUE(0, "bool", &type, N_("value is \"true\" or \"false\""), TYPE_BOOL), + OPT_CALLBACK_VALUE(0, "int", &type, N_("value is decimal number"), TYPE_INT), + OPT_CALLBACK_VALUE(0, "bool-or-int", &type, N_("value is --bool or --int"), TYPE_BOOL_OR_INT), + OPT_CALLBACK_VALUE(0, "bool-or-str", &type, N_("value is --bool or string"), TYPE_BOOL_OR_STR), + OPT_CALLBACK_VALUE(0, "path", &type, N_("value is a path (file or directory name)"), TYPE_PATH), + OPT_CALLBACK_VALUE(0, "expiry-date", &type, N_("value is an expiry date"), TYPE_EXPIRY_DATE), + OPT_GROUP(N_("Other")), + OPT_BOOL('z', "null", &end_nul, N_("terminate values with NUL byte")), + OPT_BOOL(0, "name-only", &omit_values, N_("show variable names only")), + OPT_BOOL(0, "includes", &respect_includes_opt, N_("respect include directives on lookup")), + OPT_BOOL(0, "show-origin", &show_origin, N_("show origin of config (file, standard input, blob, command line)")), + OPT_BOOL(0, "show-scope", &show_scope, N_("show scope of config (worktree, local, global, system, command)")), + OPT_STRING(0, "default", &default_value, N_("value"), N_("with --get, use default value when missing entry")), + OPT_END(), +}; + +static NORETURN void usage_builtin_config(void) +{ + usage_with_options(builtin_config_usage, builtin_config_options); +} + +static void check_argc(int argc, int min, int max) +{ + if (argc >= min && argc <= max) + return; + if (min == max) + error(_("wrong number of arguments, should be %d"), min); + else + error(_("wrong number of arguments, should be from %d to %d"), + min, max); + usage_builtin_config(); +} + +static void show_config_origin(const struct key_value_info *kvi, + struct strbuf *buf) +{ + const char term = end_nul ? '\0' : '\t'; + + strbuf_addstr(buf, config_origin_type_name(kvi->origin_type)); + strbuf_addch(buf, ':'); + if (end_nul) + strbuf_addstr(buf, kvi->filename ? kvi->filename : ""); + else + quote_c_style(kvi->filename ? kvi->filename : "", buf, NULL, 0); + strbuf_addch(buf, term); +} + +static void show_config_scope(const struct key_value_info *kvi, + struct strbuf *buf) +{ + const char term = end_nul ? '\0' : '\t'; + const char *scope = config_scope_name(kvi->scope); + + strbuf_addstr(buf, N_(scope)); + strbuf_addch(buf, term); +} + +static int show_all_config(const char *key_, const char *value_, + const struct config_context *ctx, + void *cb UNUSED) +{ + const struct key_value_info *kvi = ctx->kvi; + + if (show_origin || show_scope) { + struct strbuf buf = STRBUF_INIT; + if (show_scope) + show_config_scope(kvi, &buf); + if (show_origin) + show_config_origin(kvi, &buf); + /* Use fwrite as "buf" can contain \0's if "end_null" is set. */ + fwrite(buf.buf, 1, buf.len, stdout); + strbuf_release(&buf); + } + if (!omit_values && value_) + printf("%s%c%s%c", key_, delim, value_, term); + else + printf("%s%c", key_, term); + return 0; +} + +struct strbuf_list { + struct strbuf *items; + int nr; + int alloc; +}; + +static int format_config(struct strbuf *buf, const char *key_, + const char *value_, const struct key_value_info *kvi) +{ + if (show_scope) + show_config_scope(kvi, buf); + if (show_origin) + show_config_origin(kvi, buf); + if (show_keys) + strbuf_addstr(buf, key_); + if (!omit_values) { + if (show_keys) + strbuf_addch(buf, key_delim); + + if (type == TYPE_INT) + strbuf_addf(buf, "%"PRId64, + git_config_int64(key_, value_ ? value_ : "", kvi)); + else if (type == TYPE_BOOL) + strbuf_addstr(buf, git_config_bool(key_, value_) ? + "true" : "false"); + else if (type == TYPE_BOOL_OR_INT) { + int is_bool, v; + v = git_config_bool_or_int(key_, value_, kvi, + &is_bool); + if (is_bool) + strbuf_addstr(buf, v ? "true" : "false"); + else + strbuf_addf(buf, "%d", v); + } else if (type == TYPE_BOOL_OR_STR) { + int v = git_parse_maybe_bool(value_); + if (v < 0) + strbuf_addstr(buf, value_); + else + strbuf_addstr(buf, v ? "true" : "false"); + } else if (type == TYPE_PATH) { + const char *v; + if (git_config_pathname(&v, key_, value_) < 0) + return -1; + strbuf_addstr(buf, v); + free((char *)v); + } else if (type == TYPE_EXPIRY_DATE) { + timestamp_t t; + if (git_config_expiry_date(&t, key_, value_) < 0) + return -1; + strbuf_addf(buf, "%"PRItime, t); + } else if (type == TYPE_COLOR) { + char v[COLOR_MAXLEN]; + if (git_config_color(v, key_, value_) < 0) + return -1; + strbuf_addstr(buf, v); + } else if (value_) { + strbuf_addstr(buf, value_); + } else { + /* Just show the key name; back out delimiter */ + if (show_keys) + strbuf_setlen(buf, buf->len - 1); + } + } + strbuf_addch(buf, term); + return 0; +} + +static int collect_config(const char *key_, const char *value_, + const struct config_context *ctx, void *cb) +{ + struct strbuf_list *values = cb; + const struct key_value_info *kvi = ctx->kvi; + + if (!use_key_regexp && strcmp(key_, key)) + return 0; + if (use_key_regexp && regexec(key_regexp, key_, 0, NULL, 0)) + return 0; + if (fixed_value && strcmp(value_pattern, (value_?value_:""))) + return 0; + if (regexp != NULL && + (do_not_match ^ !!regexec(regexp, (value_?value_:""), 0, NULL, 0))) + return 0; + + ALLOC_GROW(values->items, values->nr + 1, values->alloc); + strbuf_init(&values->items[values->nr], 0); + + return format_config(&values->items[values->nr++], key_, value_, kvi); +} + +static int get_value(const char *key_, const char *regex_, unsigned flags) +{ + int ret = CONFIG_GENERIC_ERROR; + struct strbuf_list values = {NULL}; + int i; + + if (use_key_regexp) { + char *tl; + + /* + * NEEDSWORK: this naive pattern lowercasing obviously does not + * work for more complex patterns like "^[^.]*Foo.*bar". + * Perhaps we should deprecate this altogether someday. + */ + + key = xstrdup(key_); + for (tl = key + strlen(key) - 1; + tl >= key && *tl != '.'; + tl--) + *tl = tolower(*tl); + for (tl = key; *tl && *tl != '.'; tl++) + *tl = tolower(*tl); + + key_regexp = (regex_t*)xmalloc(sizeof(regex_t)); + if (regcomp(key_regexp, key, REG_EXTENDED)) { + error(_("invalid key pattern: %s"), key_); + FREE_AND_NULL(key_regexp); + ret = CONFIG_INVALID_PATTERN; + goto free_strings; + } + } else { + if (git_config_parse_key(key_, &key, NULL)) { + ret = CONFIG_INVALID_KEY; + goto free_strings; + } + } + + if (regex_ && (flags & CONFIG_FLAGS_FIXED_VALUE)) + value_pattern = regex_; + else if (regex_) { + if (regex_[0] == '!') { + do_not_match = 1; + regex_++; + } + + regexp = (regex_t*)xmalloc(sizeof(regex_t)); + if (regcomp(regexp, regex_, REG_EXTENDED)) { + error(_("invalid pattern: %s"), regex_); + FREE_AND_NULL(regexp); + ret = CONFIG_INVALID_PATTERN; + goto free_strings; + } + } + + config_with_options(collect_config, &values, + &given_config_source, the_repository, + &config_options); + + if (!values.nr && default_value) { + struct key_value_info kvi = KVI_INIT; + struct strbuf *item; + + kvi_from_param(&kvi); + ALLOC_GROW(values.items, values.nr + 1, values.alloc); + item = &values.items[values.nr++]; + strbuf_init(item, 0); + if (format_config(item, key_, default_value, &kvi) < 0) + die(_("failed to format default config value: %s"), + default_value); + } + + ret = !values.nr; + + for (i = 0; i < values.nr; i++) { + struct strbuf *buf = values.items + i; + if (do_all || i == values.nr - 1) + fwrite(buf->buf, 1, buf->len, stdout); + strbuf_release(buf); + } + free(values.items); + +free_strings: + free(key); + if (key_regexp) { + regfree(key_regexp); + free(key_regexp); + } + if (regexp) { + regfree(regexp); + free(regexp); + } + + return ret; +} + +static char *normalize_value(const char *key, const char *value, + struct key_value_info *kvi) +{ + if (!value) + return NULL; + + if (type == 0 || type == TYPE_PATH || type == TYPE_EXPIRY_DATE) + /* + * We don't do normalization for TYPE_PATH here: If + * the path is like ~/foobar/, we prefer to store + * "~/foobar/" in the config file, and to expand the ~ + * when retrieving the value. + * Also don't do normalization for expiry dates. + */ + return xstrdup(value); + if (type == TYPE_INT) + return xstrfmt("%"PRId64, git_config_int64(key, value, kvi)); + if (type == TYPE_BOOL) + return xstrdup(git_config_bool(key, value) ? "true" : "false"); + if (type == TYPE_BOOL_OR_INT) { + int is_bool, v; + v = git_config_bool_or_int(key, value, kvi, &is_bool); + if (!is_bool) + return xstrfmt("%d", v); + else + return xstrdup(v ? "true" : "false"); + } + if (type == TYPE_BOOL_OR_STR) { + int v = git_parse_maybe_bool(value); + if (v < 0) + return xstrdup(value); + else + return xstrdup(v ? "true" : "false"); + } + if (type == TYPE_COLOR) { + char v[COLOR_MAXLEN]; + if (git_config_color(v, key, value)) + die(_("cannot parse color '%s'"), value); + + /* + * The contents of `v` now contain an ANSI escape + * sequence, not suitable for including within a + * configuration file. Treat the above as a + * "sanity-check", and return the given value, which we + * know is representable as valid color code. + */ + return xstrdup(value); + } + + BUG("cannot normalize type %d", type); +} + +static int get_color_found; +static const char *get_color_slot; +static const char *get_colorbool_slot; +static char parsed_color[COLOR_MAXLEN]; + +static int git_get_color_config(const char *var, const char *value, + const struct config_context *ctx UNUSED, + void *cb UNUSED) +{ + if (!strcmp(var, get_color_slot)) { + if (!value) + config_error_nonbool(var); + if (color_parse(value, parsed_color) < 0) + return -1; + get_color_found = 1; + } + return 0; +} + +static void get_color(const char *var, const char *def_color) +{ + get_color_slot = var; + get_color_found = 0; + parsed_color[0] = '\0'; + config_with_options(git_get_color_config, NULL, + &given_config_source, the_repository, + &config_options); + + if (!get_color_found && def_color) { + if (color_parse(def_color, parsed_color) < 0) + die(_("unable to parse default color value")); + } + + fputs(parsed_color, stdout); +} + +static int get_colorbool_found; +static int get_diff_color_found; +static int get_color_ui_found; +static int git_get_colorbool_config(const char *var, const char *value, + const struct config_context *ctx UNUSED, + void *data UNUSED) +{ + if (!strcmp(var, get_colorbool_slot)) + get_colorbool_found = git_config_colorbool(var, value); + else if (!strcmp(var, "diff.color")) + get_diff_color_found = git_config_colorbool(var, value); + else if (!strcmp(var, "color.ui")) + get_color_ui_found = git_config_colorbool(var, value); + return 0; +} + +static int get_colorbool(const char *var, int print) +{ + get_colorbool_slot = var; + get_colorbool_found = -1; + get_diff_color_found = -1; + get_color_ui_found = -1; + config_with_options(git_get_colorbool_config, NULL, + &given_config_source, the_repository, + &config_options); + + if (get_colorbool_found < 0) { + if (!strcmp(get_colorbool_slot, "color.diff")) + get_colorbool_found = get_diff_color_found; + if (get_colorbool_found < 0) + get_colorbool_found = get_color_ui_found; + } + + if (get_colorbool_found < 0) + /* default value if none found in config */ + get_colorbool_found = GIT_COLOR_AUTO; + + get_colorbool_found = want_color(get_colorbool_found); + + if (print) { + printf("%s\n", get_colorbool_found ? "true" : "false"); + return 0; + } else + return get_colorbool_found ? 0 : 1; +} + +static void check_write(void) +{ + if (!given_config_source.file && !startup_info->have_repository) + die(_("not in a git directory")); + + if (given_config_source.use_stdin) + die(_("writing to stdin is not supported")); + + if (given_config_source.blob) + die(_("writing config blobs is not supported")); +} + +struct urlmatch_current_candidate_value { + char value_is_null; + struct strbuf value; + struct key_value_info kvi; +}; + +static int urlmatch_collect_fn(const char *var, const char *value, + const struct config_context *ctx, + void *cb) +{ + struct string_list *values = cb; + struct string_list_item *item = string_list_insert(values, var); + struct urlmatch_current_candidate_value *matched = item->util; + const struct key_value_info *kvi = ctx->kvi; + + if (!matched) { + matched = xmalloc(sizeof(*matched)); + strbuf_init(&matched->value, 0); + item->util = matched; + } else { + strbuf_reset(&matched->value); + } + matched->kvi = *kvi; + + if (value) { + strbuf_addstr(&matched->value, value); + matched->value_is_null = 0; + } else { + matched->value_is_null = 1; + } + return 0; +} + +static int get_urlmatch(const char *var, const char *url) +{ + int ret; + char *section_tail; + struct string_list_item *item; + struct urlmatch_config config = URLMATCH_CONFIG_INIT; + struct string_list values = STRING_LIST_INIT_DUP; + + config.collect_fn = urlmatch_collect_fn; + config.cascade_fn = NULL; + config.cb = &values; + + if (!url_normalize(url, &config.url)) + die("%s", config.url.err); + + config.section = xstrdup_tolower(var); + section_tail = strchr(config.section, '.'); + if (section_tail) { + *section_tail = '\0'; + config.key = section_tail + 1; + show_keys = 0; + } else { + config.key = NULL; + show_keys = 1; + } + + config_with_options(urlmatch_config_entry, &config, + &given_config_source, the_repository, + &config_options); + + ret = !values.nr; + + for_each_string_list_item(item, &values) { + struct urlmatch_current_candidate_value *matched = item->util; + struct strbuf buf = STRBUF_INIT; + + format_config(&buf, item->string, + matched->value_is_null ? NULL : matched->value.buf, + &matched->kvi); + fwrite(buf.buf, 1, buf.len, stdout); + strbuf_release(&buf); + + strbuf_release(&matched->value); + } + urlmatch_config_release(&config); + string_list_clear(&values, 1); + free(config.url.url); + + free((void *)config.section); + return ret; +} + +static char *default_user_config(void) +{ + struct strbuf buf = STRBUF_INIT; + strbuf_addf(&buf, + _("# This is Git's per-user configuration file.\n" + "[user]\n" + "# Please adapt and uncomment the following lines:\n" + "# name = %s\n" + "# email = %s\n"), + ident_default_name(), + ident_default_email()); + return strbuf_detach(&buf, NULL); +} + +int cmd_config(int argc, const char **argv, const char *prefix) +{ + int nongit = !startup_info->have_repository; + char *value = NULL; + int flags = 0; + int ret = 0; + struct key_value_info default_kvi = KVI_INIT; + + given_config_source.file = xstrdup_or_null(getenv(CONFIG_ENVIRONMENT)); + + argc = parse_options(argc, argv, prefix, builtin_config_options, + builtin_config_usage, + PARSE_OPT_STOP_AT_NON_OPTION); + + if (use_global_config + use_system_config + use_local_config + + use_worktree_config + + !!given_config_source.file + !!given_config_source.blob > 1) { + error(_("only one config file at a time")); + usage_builtin_config(); + } + + if (nongit) { + if (use_local_config) + die(_("--local can only be used inside a git repository")); + if (given_config_source.blob) + die(_("--blob can only be used inside a git repository")); + if (use_worktree_config) + die(_("--worktree can only be used inside a git repository")); + + } + + if (given_config_source.file && + !strcmp(given_config_source.file, "-")) { + given_config_source.file = NULL; + given_config_source.use_stdin = 1; + given_config_source.scope = CONFIG_SCOPE_COMMAND; + } + + if (use_global_config) { + char *user_config, *xdg_config; + + git_global_config(&user_config, &xdg_config); + if (!user_config) + /* + * It is unknown if HOME/.gitconfig exists, so + * we do not know if we should write to XDG + * location; error out even if XDG_CONFIG_HOME + * is set and points at a sane location. + */ + die(_("$HOME not set")); + + given_config_source.scope = CONFIG_SCOPE_GLOBAL; + + if (access_or_warn(user_config, R_OK, 0) && + xdg_config && !access_or_warn(xdg_config, R_OK, 0)) { + given_config_source.file = xdg_config; + free(user_config); + } else { + given_config_source.file = user_config; + free(xdg_config); + } + } + else if (use_system_config) { + given_config_source.file = git_system_config(); + given_config_source.scope = CONFIG_SCOPE_SYSTEM; + } else if (use_local_config) { + given_config_source.file = git_pathdup("config"); + given_config_source.scope = CONFIG_SCOPE_LOCAL; + } else if (use_worktree_config) { + struct worktree **worktrees = get_worktrees(); + if (the_repository->repository_format_worktree_config) + given_config_source.file = git_pathdup("config.worktree"); + else if (worktrees[0] && worktrees[1]) + die(_("--worktree cannot be used with multiple " + "working trees unless the config\n" + "extension worktreeConfig is enabled. " + "Please read \"CONFIGURATION FILE\"\n" + "section in \"git help worktree\" for details")); + else + given_config_source.file = git_pathdup("config"); + given_config_source.scope = CONFIG_SCOPE_LOCAL; + free_worktrees(worktrees); + } else if (given_config_source.file) { + if (!is_absolute_path(given_config_source.file) && prefix) + given_config_source.file = + prefix_filename(prefix, given_config_source.file); + given_config_source.scope = CONFIG_SCOPE_COMMAND; + } else if (given_config_source.blob) { + given_config_source.scope = CONFIG_SCOPE_COMMAND; + } + + + if (respect_includes_opt == -1) + config_options.respect_includes = !given_config_source.file; + else + config_options.respect_includes = respect_includes_opt; + if (!nongit) { + config_options.commondir = get_git_common_dir(); + config_options.git_dir = get_git_dir(); + } + + if (end_nul) { + term = '\0'; + delim = '\n'; + key_delim = '\n'; + } + + if ((actions & (ACTION_GET_COLOR|ACTION_GET_COLORBOOL)) && type) { + error(_("--get-color and variable type are incoherent")); + usage_builtin_config(); + } + + if (HAS_MULTI_BITS(actions)) { + error(_("only one action at a time")); + usage_builtin_config(); + } + if (actions == 0) + switch (argc) { + case 1: actions = ACTION_GET; break; + case 2: actions = ACTION_SET; break; + case 3: actions = ACTION_SET_ALL; break; + default: + usage_builtin_config(); + } + if (omit_values && + !(actions == ACTION_LIST || actions == ACTION_GET_REGEXP)) { + error(_("--name-only is only applicable to --list or --get-regexp")); + usage_builtin_config(); + } + + if (show_origin && !(actions & + (ACTION_GET|ACTION_GET_ALL|ACTION_GET_REGEXP|ACTION_LIST))) { + error(_("--show-origin is only applicable to --get, --get-all, " + "--get-regexp, and --list")); + usage_builtin_config(); + } + + if (default_value && !(actions & ACTION_GET)) { + error(_("--default is only applicable to --get")); + usage_builtin_config(); + } + + /* check usage of --fixed-value */ + if (fixed_value) { + int allowed_usage = 0; + + switch (actions) { + /* git config --get <name> <value-pattern> */ + case ACTION_GET: + /* git config --get-all <name> <value-pattern> */ + case ACTION_GET_ALL: + /* git config --get-regexp <name-pattern> <value-pattern> */ + case ACTION_GET_REGEXP: + /* git config --unset <name> <value-pattern> */ + case ACTION_UNSET: + /* git config --unset-all <name> <value-pattern> */ + case ACTION_UNSET_ALL: + allowed_usage = argc > 1 && !!argv[1]; + break; + + /* git config <name> <value> <value-pattern> */ + case ACTION_SET_ALL: + /* git config --replace-all <name> <value> <value-pattern> */ + case ACTION_REPLACE_ALL: + allowed_usage = argc > 2 && !!argv[2]; + break; + + /* other options don't allow --fixed-value */ + } + + if (!allowed_usage) { + error(_("--fixed-value only applies with 'value-pattern'")); + usage_builtin_config(); + } + + flags |= CONFIG_FLAGS_FIXED_VALUE; + } + + if (actions & PAGING_ACTIONS) + setup_auto_pager("config", 1); + + if (actions == ACTION_LIST) { + check_argc(argc, 0, 0); + if (config_with_options(show_all_config, NULL, + &given_config_source, the_repository, + &config_options) < 0) { + if (given_config_source.file) + die_errno(_("unable to read config file '%s'"), + given_config_source.file); + else + die(_("error processing config file(s)")); + } + } + else if (actions == ACTION_EDIT) { + char *config_file; + + check_argc(argc, 0, 0); + if (!given_config_source.file && nongit) + die(_("not in a git directory")); + if (given_config_source.use_stdin) + die(_("editing stdin is not supported")); + if (given_config_source.blob) + die(_("editing blobs is not supported")); + git_config(git_default_config, NULL); + config_file = given_config_source.file ? + xstrdup(given_config_source.file) : + git_pathdup("config"); + if (use_global_config) { + int fd = open(config_file, O_CREAT | O_EXCL | O_WRONLY, 0666); + if (fd >= 0) { + char *content = default_user_config(); + write_str_in_full(fd, content); + free(content); + close(fd); + } + else if (errno != EEXIST) + die_errno(_("cannot create configuration file %s"), config_file); + } + launch_editor(config_file, NULL, NULL); + free(config_file); + } + else if (actions == ACTION_SET) { + check_write(); + check_argc(argc, 2, 2); + value = normalize_value(argv[0], argv[1], &default_kvi); + ret = git_config_set_in_file_gently(given_config_source.file, argv[0], value); + if (ret == CONFIG_NOTHING_SET) + error(_("cannot overwrite multiple values with a single value\n" + " Use a regexp, --add or --replace-all to change %s."), argv[0]); + } + else if (actions == ACTION_SET_ALL) { + check_write(); + check_argc(argc, 2, 3); + value = normalize_value(argv[0], argv[1], &default_kvi); + ret = git_config_set_multivar_in_file_gently(given_config_source.file, + argv[0], value, argv[2], + flags); + } + else if (actions == ACTION_ADD) { + check_write(); + check_argc(argc, 2, 2); + value = normalize_value(argv[0], argv[1], &default_kvi); + ret = git_config_set_multivar_in_file_gently(given_config_source.file, + argv[0], value, + CONFIG_REGEX_NONE, + flags); + } + else if (actions == ACTION_REPLACE_ALL) { + check_write(); + check_argc(argc, 2, 3); + value = normalize_value(argv[0], argv[1], &default_kvi); + ret = git_config_set_multivar_in_file_gently(given_config_source.file, + argv[0], value, argv[2], + flags | CONFIG_FLAGS_MULTI_REPLACE); + } + else if (actions == ACTION_GET) { + check_argc(argc, 1, 2); + return get_value(argv[0], argv[1], flags); + } + else if (actions == ACTION_GET_ALL) { + do_all = 1; + check_argc(argc, 1, 2); + return get_value(argv[0], argv[1], flags); + } + else if (actions == ACTION_GET_REGEXP) { + show_keys = 1; + use_key_regexp = 1; + do_all = 1; + check_argc(argc, 1, 2); + return get_value(argv[0], argv[1], flags); + } + else if (actions == ACTION_GET_URLMATCH) { + check_argc(argc, 2, 2); + return get_urlmatch(argv[0], argv[1]); + } + else if (actions == ACTION_UNSET) { + check_write(); + check_argc(argc, 1, 2); + if (argc == 2) + return git_config_set_multivar_in_file_gently(given_config_source.file, + argv[0], NULL, argv[1], + flags); + else + return git_config_set_in_file_gently(given_config_source.file, + argv[0], NULL); + } + else if (actions == ACTION_UNSET_ALL) { + check_write(); + check_argc(argc, 1, 2); + return git_config_set_multivar_in_file_gently(given_config_source.file, + argv[0], NULL, argv[1], + flags | CONFIG_FLAGS_MULTI_REPLACE); + } + else if (actions == ACTION_RENAME_SECTION) { + check_write(); + check_argc(argc, 2, 2); + ret = git_config_rename_section_in_file(given_config_source.file, + argv[0], argv[1]); + if (ret < 0) + return ret; + else if (!ret) + die(_("no such section: %s"), argv[0]); + else + ret = 0; + } + else if (actions == ACTION_REMOVE_SECTION) { + check_write(); + check_argc(argc, 1, 1); + ret = git_config_rename_section_in_file(given_config_source.file, + argv[0], NULL); + if (ret < 0) + return ret; + else if (!ret) + die(_("no such section: %s"), argv[0]); + else + ret = 0; + } + else if (actions == ACTION_GET_COLOR) { + check_argc(argc, 1, 2); + get_color(argv[0], argv[1]); + } + else if (actions == ACTION_GET_COLORBOOL) { + check_argc(argc, 1, 2); + if (argc == 2) + color_stdout_is_tty = git_config_bool("command line", argv[1]); + return get_colorbool(argv[0], argc == 2); + } + + free(value); + return ret; +} diff --git a/builtin/count-objects.c b/builtin/count-objects.c new file mode 100644 index 0000000..2d4bb5e --- /dev/null +++ b/builtin/count-objects.c @@ -0,0 +1,176 @@ +/* + * Builtin "git count-objects". + * + * Copyright (c) 2006 Junio C Hamano + */ + +#include "builtin.h" +#include "config.h" +#include "dir.h" +#include "environment.h" +#include "gettext.h" +#include "path.h" +#include "repository.h" +#include "parse-options.h" +#include "quote.h" +#include "packfile.h" +#include "object-store-ll.h" + +static unsigned long garbage; +static off_t size_garbage; +static int verbose; +static unsigned long loose, packed, packed_loose; +static off_t loose_size; + +static const char *bits_to_msg(unsigned seen_bits) +{ + switch (seen_bits) { + case 0: + return "no corresponding .idx or .pack"; + case PACKDIR_FILE_GARBAGE: + return "garbage found"; + case PACKDIR_FILE_PACK: + return "no corresponding .idx"; + case PACKDIR_FILE_IDX: + return "no corresponding .pack"; + case PACKDIR_FILE_PACK|PACKDIR_FILE_IDX: + default: + return NULL; + } +} + +static void real_report_garbage(unsigned seen_bits, const char *path) +{ + struct stat st; + const char *desc = bits_to_msg(seen_bits); + + if (!desc) + return; + + if (!stat(path, &st)) + size_garbage += st.st_size; + warning("%s: %s", desc, path); + garbage++; +} + +static void loose_garbage(const char *path) +{ + if (verbose) + report_garbage(PACKDIR_FILE_GARBAGE, path); +} + +static int count_loose(const struct object_id *oid, const char *path, + void *data UNUSED) +{ + struct stat st; + + if (lstat(path, &st) || !S_ISREG(st.st_mode)) + loose_garbage(path); + else { + loose_size += on_disk_bytes(st); + loose++; + if (verbose && has_object_pack(oid)) + packed_loose++; + } + return 0; +} + +static int count_cruft(const char *basename UNUSED, const char *path, + void *data UNUSED) +{ + loose_garbage(path); + return 0; +} + +static int print_alternate(struct object_directory *odb, void *data UNUSED) +{ + printf("alternate: "); + quote_c_style(odb->path, NULL, stdout, 0); + putchar('\n'); + return 0; +} + +static char const * const count_objects_usage[] = { + "git count-objects [-v] [-H | --human-readable]", + NULL +}; + +int cmd_count_objects(int argc, const char **argv, const char *prefix) +{ + int human_readable = 0; + struct option opts[] = { + OPT__VERBOSE(&verbose, N_("be verbose")), + OPT_BOOL('H', "human-readable", &human_readable, + N_("print sizes in human readable format")), + OPT_END(), + }; + + git_config(git_default_config, NULL); + + argc = parse_options(argc, argv, prefix, opts, count_objects_usage, 0); + /* we do not take arguments other than flags for now */ + if (argc) + usage_with_options(count_objects_usage, opts); + if (verbose) { + report_garbage = real_report_garbage; + report_linked_checkout_garbage(); + } + + for_each_loose_file_in_objdir(get_object_directory(), + count_loose, count_cruft, NULL, NULL); + + if (verbose) { + struct packed_git *p; + unsigned long num_pack = 0; + off_t size_pack = 0; + struct strbuf loose_buf = STRBUF_INIT; + struct strbuf pack_buf = STRBUF_INIT; + struct strbuf garbage_buf = STRBUF_INIT; + + for (p = get_all_packs(the_repository); p; p = p->next) { + if (!p->pack_local) + continue; + if (open_pack_index(p)) + continue; + packed += p->num_objects; + size_pack += p->pack_size + p->index_size; + num_pack++; + } + + if (human_readable) { + strbuf_humanise_bytes(&loose_buf, loose_size); + strbuf_humanise_bytes(&pack_buf, size_pack); + strbuf_humanise_bytes(&garbage_buf, size_garbage); + } else { + strbuf_addf(&loose_buf, "%lu", + (unsigned long)(loose_size / 1024)); + strbuf_addf(&pack_buf, "%lu", + (unsigned long)(size_pack / 1024)); + strbuf_addf(&garbage_buf, "%lu", + (unsigned long)(size_garbage / 1024)); + } + + printf("count: %lu\n", loose); + printf("size: %s\n", loose_buf.buf); + printf("in-pack: %lu\n", packed); + printf("packs: %lu\n", num_pack); + printf("size-pack: %s\n", pack_buf.buf); + printf("prune-packable: %lu\n", packed_loose); + printf("garbage: %lu\n", garbage); + printf("size-garbage: %s\n", garbage_buf.buf); + foreach_alt_odb(print_alternate, NULL); + strbuf_release(&loose_buf); + strbuf_release(&pack_buf); + strbuf_release(&garbage_buf); + } else { + struct strbuf buf = STRBUF_INIT; + if (human_readable) + strbuf_humanise_bytes(&buf, loose_size); + else + strbuf_addf(&buf, "%lu kilobytes", + (unsigned long)(loose_size / 1024)); + printf("%lu objects, %s\n", loose, buf.buf); + strbuf_release(&buf); + } + return 0; +} diff --git a/builtin/credential-cache--daemon.c b/builtin/credential-cache--daemon.c new file mode 100644 index 0000000..3a6a750 --- /dev/null +++ b/builtin/credential-cache--daemon.c @@ -0,0 +1,331 @@ +#include "builtin.h" +#include "abspath.h" +#include "gettext.h" +#include "object-file.h" +#include "parse-options.h" + +#ifndef NO_UNIX_SOCKETS + +#include "config.h" +#include "tempfile.h" +#include "credential.h" +#include "unix-socket.h" + +struct credential_cache_entry { + struct credential item; + timestamp_t expiration; +}; +static struct credential_cache_entry *entries; +static int entries_nr; +static int entries_alloc; + +static void cache_credential(struct credential *c, int timeout) +{ + struct credential_cache_entry *e; + + ALLOC_GROW(entries, entries_nr + 1, entries_alloc); + e = &entries[entries_nr++]; + + /* take ownership of pointers */ + memcpy(&e->item, c, sizeof(*c)); + memset(c, 0, sizeof(*c)); + e->expiration = time(NULL) + timeout; +} + +static struct credential_cache_entry *lookup_credential(const struct credential *c) +{ + int i; + for (i = 0; i < entries_nr; i++) { + struct credential *e = &entries[i].item; + if (credential_match(c, e, 0)) + return &entries[i]; + } + return NULL; +} + +static void remove_credential(const struct credential *c, int match_password) +{ + struct credential_cache_entry *e; + + int i; + for (i = 0; i < entries_nr; i++) { + e = &entries[i]; + if (credential_match(c, &e->item, match_password)) + e->expiration = 0; + } +} + +static timestamp_t check_expirations(void) +{ + static timestamp_t wait_for_entry_until; + int i = 0; + timestamp_t now = time(NULL); + timestamp_t next = TIME_MAX; + + /* + * Initially give the client 30 seconds to actually contact us + * and store a credential before we decide there's no point in + * keeping the daemon around. + */ + if (!wait_for_entry_until) + wait_for_entry_until = now + 30; + + while (i < entries_nr) { + if (entries[i].expiration <= now) { + entries_nr--; + credential_clear(&entries[i].item); + if (i != entries_nr) + memcpy(&entries[i], &entries[entries_nr], sizeof(*entries)); + /* + * Stick around 30 seconds in case a new credential + * shows up (e.g., because we just removed a failed + * one, and we will soon get the correct one). + */ + wait_for_entry_until = now + 30; + } + else { + if (entries[i].expiration < next) + next = entries[i].expiration; + i++; + } + } + + if (!entries_nr) { + if (wait_for_entry_until <= now) + return 0; + next = wait_for_entry_until; + } + + return next - now; +} + +static int read_request(FILE *fh, struct credential *c, + struct strbuf *action, int *timeout) +{ + static struct strbuf item = STRBUF_INIT; + const char *p; + + strbuf_getline_lf(&item, fh); + if (!skip_prefix(item.buf, "action=", &p)) + return error("client sent bogus action line: %s", item.buf); + strbuf_addstr(action, p); + + strbuf_getline_lf(&item, fh); + if (!skip_prefix(item.buf, "timeout=", &p)) + return error("client sent bogus timeout line: %s", item.buf); + *timeout = atoi(p); + + if (credential_read(c, fh) < 0) + return -1; + return 0; +} + +static void serve_one_client(FILE *in, FILE *out) +{ + struct credential c = CREDENTIAL_INIT; + struct strbuf action = STRBUF_INIT; + int timeout = -1; + + if (read_request(in, &c, &action, &timeout) < 0) + /* ignore error */ ; + else if (!strcmp(action.buf, "get")) { + struct credential_cache_entry *e = lookup_credential(&c); + if (e) { + fprintf(out, "username=%s\n", e->item.username); + fprintf(out, "password=%s\n", e->item.password); + if (e->item.password_expiry_utc != TIME_MAX) + fprintf(out, "password_expiry_utc=%"PRItime"\n", + e->item.password_expiry_utc); + if (e->item.oauth_refresh_token) + fprintf(out, "oauth_refresh_token=%s\n", + e->item.oauth_refresh_token); + } + } + else if (!strcmp(action.buf, "exit")) { + /* + * It's important that we clean up our socket first, and then + * signal the client only once we have finished the cleanup. + * Calling exit() directly does this, because we clean up in + * our atexit() handler, and then signal the client when our + * process actually ends, which closes the socket and gives + * them EOF. + */ + exit(0); + } + else if (!strcmp(action.buf, "erase")) + remove_credential(&c, 1); + else if (!strcmp(action.buf, "store")) { + if (timeout < 0) + warning("cache client didn't specify a timeout"); + else if (!c.username || !c.password) + warning("cache client gave us a partial credential"); + else { + remove_credential(&c, 0); + cache_credential(&c, timeout); + } + } + else + warning("cache client sent unknown action: %s", action.buf); + + credential_clear(&c); + strbuf_release(&action); +} + +static int serve_cache_loop(int fd) +{ + struct pollfd pfd; + timestamp_t wakeup; + + wakeup = check_expirations(); + if (!wakeup) + return 0; + + pfd.fd = fd; + pfd.events = POLLIN; + if (poll(&pfd, 1, 1000 * wakeup) < 0) { + if (errno != EINTR) + die_errno("poll failed"); + return 1; + } + + if (pfd.revents & POLLIN) { + int client, client2; + FILE *in, *out; + + client = accept(fd, NULL, NULL); + if (client < 0) { + warning_errno("accept failed"); + return 1; + } + client2 = dup(client); + if (client2 < 0) { + warning_errno("dup failed"); + close(client); + return 1; + } + + in = xfdopen(client, "r"); + out = xfdopen(client2, "w"); + serve_one_client(in, out); + fclose(in); + fclose(out); + } + return 1; +} + +static void serve_cache(const char *socket_path, int debug) +{ + struct unix_stream_listen_opts opts = UNIX_STREAM_LISTEN_OPTS_INIT; + int fd; + + fd = unix_stream_listen(socket_path, &opts); + if (fd < 0) + die_errno("unable to bind to '%s'", socket_path); + + printf("ok\n"); + fclose(stdout); + if (!debug) { + if (!freopen("/dev/null", "w", stderr)) + die_errno("unable to point stderr to /dev/null"); + } + + while (serve_cache_loop(fd)) + ; /* nothing */ + + close(fd); +} + +static const char permissions_advice[] = N_( +"The permissions on your socket directory are too loose; other\n" +"users may be able to read your cached credentials. Consider running:\n" +"\n" +" chmod 0700 %s"); +static void init_socket_directory(const char *path) +{ + struct stat st; + char *path_copy = xstrdup(path); + char *dir = dirname(path_copy); + + if (!stat(dir, &st)) { + if (st.st_mode & 077) + die(_(permissions_advice), dir); + } else { + /* + * We must be sure to create the directory with the correct mode, + * not just chmod it after the fact; otherwise, there is a race + * condition in which somebody can chdir to it, sleep, then try to open + * our protected socket. + */ + if (safe_create_leading_directories_const(dir) < 0) + die_errno("unable to create directories for '%s'", dir); + if (mkdir(dir, 0700) < 0) + die_errno("unable to mkdir '%s'", dir); + } + + if (chdir(dir)) + /* + * We don't actually care what our cwd is; we chdir here just to + * be a friendly daemon and avoid tying up our original cwd. + * If this fails, it's OK to just continue without that benefit. + */ + ; + + free(path_copy); +} + +int cmd_credential_cache_daemon(int argc, const char **argv, const char *prefix) +{ + struct tempfile *socket_file; + const char *socket_path; + int ignore_sighup = 0; + static const char *usage[] = { + "git credential-cache--daemon [--debug] <socket-path>", + NULL + }; + int debug = 0; + const struct option options[] = { + OPT_BOOL(0, "debug", &debug, + N_("print debugging messages to stderr")), + OPT_END() + }; + + git_config_get_bool("credentialcache.ignoresighup", &ignore_sighup); + + argc = parse_options(argc, argv, prefix, options, usage, 0); + socket_path = argv[0]; + + if (!socket_path) + usage_with_options(usage, options); + + if (!is_absolute_path(socket_path)) + die("socket directory must be an absolute path"); + + init_socket_directory(socket_path); + socket_file = register_tempfile(socket_path); + + if (ignore_sighup) + signal(SIGHUP, SIG_IGN); + + serve_cache(socket_path, debug); + delete_tempfile(&socket_file); + + return 0; +} + +#else + +int cmd_credential_cache_daemon(int argc, const char **argv, const char *prefix) +{ + const char * const usage[] = { + "git credential-cache--daemon [--debug] <socket-path>", + "", + "credential-cache--daemon is disabled in this build of Git", + NULL + }; + struct option options[] = { OPT_END() }; + + argc = parse_options(argc, argv, prefix, options, usage, 0); + die(_("credential-cache--daemon unavailable; no unix socket support")); +} + +#endif /* NO_UNIX_SOCKET */ diff --git a/builtin/credential-cache.c b/builtin/credential-cache.c new file mode 100644 index 0000000..43b9d0e --- /dev/null +++ b/builtin/credential-cache.c @@ -0,0 +1,187 @@ +#include "builtin.h" +#include "gettext.h" +#include "parse-options.h" +#include "path.h" +#include "strbuf.h" +#include "write-or-die.h" + +#ifndef NO_UNIX_SOCKETS + +#include "credential.h" +#include "string-list.h" +#include "unix-socket.h" +#include "run-command.h" + +#define FLAG_SPAWN 0x1 +#define FLAG_RELAY 0x2 + +#ifdef GIT_WINDOWS_NATIVE + +static int connection_closed(int error) +{ + return (error == EINVAL); +} + +static int connection_fatally_broken(int error) +{ + return (error != ENOENT) && (error != ENETDOWN); +} + +#else + +static int connection_closed(int error) +{ + return (error == ECONNRESET); +} + +static int connection_fatally_broken(int error) +{ + return (error != ENOENT) && (error != ECONNREFUSED); +} + +#endif + +static int send_request(const char *socket, const struct strbuf *out) +{ + int got_data = 0; + int fd = unix_stream_connect(socket, 0); + + if (fd < 0) + return -1; + + if (write_in_full(fd, out->buf, out->len) < 0) + die_errno("unable to write to cache daemon"); + shutdown(fd, SHUT_WR); + + while (1) { + char in[1024]; + int r; + + r = read_in_full(fd, in, sizeof(in)); + if (r == 0 || (r < 0 && connection_closed(errno))) + break; + if (r < 0) + die_errno("read error from cache daemon"); + write_or_die(1, in, r); + got_data = 1; + } + close(fd); + return got_data; +} + +static void spawn_daemon(const char *socket) +{ + struct child_process daemon = CHILD_PROCESS_INIT; + char buf[128]; + int r; + + strvec_pushl(&daemon.args, + "credential-cache--daemon", socket, + NULL); + daemon.git_cmd = 1; + daemon.no_stdin = 1; + daemon.out = -1; + + if (start_command(&daemon)) + die_errno("unable to start cache daemon"); + r = read_in_full(daemon.out, buf, sizeof(buf)); + if (r < 0) + die_errno("unable to read result code from cache daemon"); + if (r != 3 || memcmp(buf, "ok\n", 3)) + die("cache daemon did not start: %.*s", r, buf); + close(daemon.out); +} + +static void do_cache(const char *socket, const char *action, int timeout, + int flags) +{ + struct strbuf buf = STRBUF_INIT; + + strbuf_addf(&buf, "action=%s\n", action); + strbuf_addf(&buf, "timeout=%d\n", timeout); + if (flags & FLAG_RELAY) { + if (strbuf_read(&buf, 0, 0) < 0) + die_errno("unable to relay credential"); + } + + if (send_request(socket, &buf) < 0) { + if (connection_fatally_broken(errno)) + die_errno("unable to connect to cache daemon"); + if (flags & FLAG_SPAWN) { + spawn_daemon(socket); + if (send_request(socket, &buf) < 0) + die_errno("unable to connect to cache daemon"); + } + } + strbuf_release(&buf); +} + +static char *get_socket_path(void) +{ + struct stat sb; + char *old_dir, *socket; + old_dir = interpolate_path("~/.git-credential-cache", 0); + if (old_dir && !stat(old_dir, &sb) && S_ISDIR(sb.st_mode)) + socket = xstrfmt("%s/socket", old_dir); + else + socket = xdg_cache_home("credential/socket"); + free(old_dir); + return socket; +} + +int cmd_credential_cache(int argc, const char **argv, const char *prefix) +{ + char *socket_path = NULL; + int timeout = 900; + const char *op; + const char * const usage[] = { + "git credential-cache [<options>] <action>", + NULL + }; + struct option options[] = { + OPT_INTEGER(0, "timeout", &timeout, + "number of seconds to cache credentials"), + OPT_STRING(0, "socket", &socket_path, "path", + "path of cache-daemon socket"), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, usage, 0); + if (!argc) + usage_with_options(usage, options); + op = argv[0]; + + if (!socket_path) + socket_path = get_socket_path(); + if (!socket_path) + die("unable to find a suitable socket path; use --socket"); + + if (!strcmp(op, "exit")) + do_cache(socket_path, op, timeout, 0); + else if (!strcmp(op, "get") || !strcmp(op, "erase")) + do_cache(socket_path, op, timeout, FLAG_RELAY); + else if (!strcmp(op, "store")) + do_cache(socket_path, op, timeout, FLAG_RELAY|FLAG_SPAWN); + else + ; /* ignore unknown operation */ + + return 0; +} + +#else + +int cmd_credential_cache(int argc, const char **argv, const char *prefix) +{ + const char * const usage[] = { + "git credential-cache [options] <action>", + "", + "credential-cache is disabled in this build of Git", + NULL + }; + struct option options[] = { OPT_END() }; + + argc = parse_options(argc, argv, prefix, options, usage, 0); + die(_("credential-cache unavailable; no unix socket support")); +} + +#endif /* NO_UNIX_SOCKETS */ diff --git a/builtin/credential-store.c b/builtin/credential-store.c new file mode 100644 index 0000000..4a49241 --- /dev/null +++ b/builtin/credential-store.c @@ -0,0 +1,222 @@ +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "lockfile.h" +#include "credential.h" +#include "path.h" +#include "string-list.h" +#include "parse-options.h" +#include "write-or-die.h" + +static struct lock_file credential_lock; + +static int parse_credential_file(const char *fn, + struct credential *c, + void (*match_cb)(struct credential *), + void (*other_cb)(struct strbuf *), + int match_password) +{ + FILE *fh; + struct strbuf line = STRBUF_INIT; + struct credential entry = CREDENTIAL_INIT; + int found_credential = 0; + + fh = fopen(fn, "r"); + if (!fh) { + if (errno != ENOENT && errno != EACCES) + die_errno("unable to open %s", fn); + return found_credential; + } + + while (strbuf_getline_lf(&line, fh) != EOF) { + if (!credential_from_url_gently(&entry, line.buf, 1) && + entry.username && entry.password && + credential_match(c, &entry, match_password)) { + found_credential = 1; + if (match_cb) { + match_cb(&entry); + break; + } + } + else if (other_cb) + other_cb(&line); + } + + credential_clear(&entry); + strbuf_release(&line); + fclose(fh); + return found_credential; +} + +static void print_entry(struct credential *c) +{ + printf("username=%s\n", c->username); + printf("password=%s\n", c->password); +} + +static void print_line(struct strbuf *buf) +{ + strbuf_addch(buf, '\n'); + write_or_die(get_lock_file_fd(&credential_lock), buf->buf, buf->len); +} + +static void rewrite_credential_file(const char *fn, struct credential *c, + struct strbuf *extra, int match_password) +{ + int timeout_ms = 1000; + + git_config_get_int("credentialstore.locktimeoutms", &timeout_ms); + if (hold_lock_file_for_update_timeout(&credential_lock, fn, 0, timeout_ms) < 0) + die_errno(_("unable to get credential storage lock in %d ms"), timeout_ms); + if (extra) + print_line(extra); + parse_credential_file(fn, c, NULL, print_line, match_password); + if (commit_lock_file(&credential_lock) < 0) + die_errno("unable to write credential store"); +} + +static int is_rfc3986_unreserved(char ch) +{ + return isalnum(ch) || + ch == '-' || ch == '_' || ch == '.' || ch == '~'; +} + +static int is_rfc3986_reserved_or_unreserved(char ch) +{ + if (is_rfc3986_unreserved(ch)) + return 1; + switch (ch) { + case '!': case '*': case '\'': case '(': case ')': case ';': + case ':': case '@': case '&': case '=': case '+': case '$': + case ',': case '/': case '?': case '#': case '[': case ']': + return 1; + } + return 0; +} + +static void store_credential_file(const char *fn, struct credential *c) +{ + struct strbuf buf = STRBUF_INIT; + + strbuf_addf(&buf, "%s://", c->protocol); + strbuf_addstr_urlencode(&buf, c->username, is_rfc3986_unreserved); + strbuf_addch(&buf, ':'); + strbuf_addstr_urlencode(&buf, c->password, is_rfc3986_unreserved); + strbuf_addch(&buf, '@'); + if (c->host) + strbuf_addstr_urlencode(&buf, c->host, is_rfc3986_unreserved); + if (c->path) { + strbuf_addch(&buf, '/'); + strbuf_addstr_urlencode(&buf, c->path, + is_rfc3986_reserved_or_unreserved); + } + + rewrite_credential_file(fn, c, &buf, 0); + strbuf_release(&buf); +} + +static void store_credential(const struct string_list *fns, struct credential *c) +{ + struct string_list_item *fn; + + /* + * Sanity check that what we are storing is actually sensible. + * In particular, we can't make a URL without a protocol field. + * Without either a host or pathname (depending on the scheme), + * we have no primary key. And without a username and password, + * we are not actually storing a credential. + */ + if (!c->protocol || !(c->host || c->path) || !c->username || !c->password) + return; + + for_each_string_list_item(fn, fns) + if (!access(fn->string, F_OK)) { + store_credential_file(fn->string, c); + return; + } + /* + * Write credential to the filename specified by fns->items[0], thus + * creating it + */ + if (fns->nr) + store_credential_file(fns->items[0].string, c); +} + +static void remove_credential(const struct string_list *fns, struct credential *c) +{ + struct string_list_item *fn; + + /* + * Sanity check that we actually have something to match + * against. The input we get is a restrictive pattern, + * so technically a blank credential means "erase everything". + * But it is too easy to accidentally send this, since it is equivalent + * to empty input. So explicitly disallow it, and require that the + * pattern have some actual content to match. + */ + if (!c->protocol && !c->host && !c->path && !c->username) + return; + for_each_string_list_item(fn, fns) + if (!access(fn->string, F_OK)) + rewrite_credential_file(fn->string, c, NULL, 1); +} + +static void lookup_credential(const struct string_list *fns, struct credential *c) +{ + struct string_list_item *fn; + + for_each_string_list_item(fn, fns) + if (parse_credential_file(fn->string, c, print_entry, NULL, 0)) + return; /* Found credential */ +} + +int cmd_credential_store(int argc, const char **argv, const char *prefix) +{ + const char * const usage[] = { + "git credential-store [<options>] <action>", + NULL + }; + const char *op; + struct credential c = CREDENTIAL_INIT; + struct string_list fns = STRING_LIST_INIT_DUP; + char *file = NULL; + struct option options[] = { + OPT_STRING(0, "file", &file, "path", + "fetch and store credentials in <path>"), + OPT_END() + }; + + umask(077); + + argc = parse_options(argc, (const char **)argv, prefix, options, usage, 0); + if (argc != 1) + usage_with_options(usage, options); + op = argv[0]; + + if (file) { + string_list_append(&fns, file); + } else { + if ((file = interpolate_path("~/.git-credentials", 0))) + string_list_append_nodup(&fns, file); + file = xdg_config_home("credentials"); + if (file) + string_list_append_nodup(&fns, file); + } + if (!fns.nr) + die("unable to set up default path; use --file"); + + if (credential_read(&c, stdin) < 0) + die("unable to read credential"); + + if (!strcmp(op, "get")) + lookup_credential(&fns, &c); + else if (!strcmp(op, "erase")) + remove_credential(&fns, &c); + else if (!strcmp(op, "store")) + store_credential(&fns, &c); + else + ; /* Ignore unknown operation. */ + + string_list_clear(&fns, 0); + return 0; +} diff --git a/builtin/credential.c b/builtin/credential.c new file mode 100644 index 0000000..7010752 --- /dev/null +++ b/builtin/credential.c @@ -0,0 +1,34 @@ +#include "git-compat-util.h" +#include "credential.h" +#include "builtin.h" +#include "config.h" + +static const char usage_msg[] = + "git credential (fill|approve|reject)"; + +int cmd_credential(int argc, const char **argv, const char *prefix UNUSED) +{ + const char *op; + struct credential c = CREDENTIAL_INIT; + + git_config(git_default_config, NULL); + + if (argc != 2 || !strcmp(argv[1], "-h")) + usage(usage_msg); + op = argv[1]; + + if (credential_read(&c, stdin) < 0) + die("unable to read credential from stdin"); + + if (!strcmp(op, "fill")) { + credential_fill(&c); + credential_write(&c, stdout); + } else if (!strcmp(op, "approve")) { + credential_approve(&c); + } else if (!strcmp(op, "reject")) { + credential_reject(&c); + } else { + usage(usage_msg); + } + return 0; +} diff --git a/builtin/describe.c b/builtin/describe.c new file mode 100644 index 0000000..fb6b050 --- /dev/null +++ b/builtin/describe.c @@ -0,0 +1,708 @@ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "config.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "lockfile.h" +#include "commit.h" +#include "tag.h" +#include "blob.h" +#include "refs.h" +#include "exec-cmd.h" +#include "object-name.h" +#include "parse-options.h" +#include "read-cache-ll.h" +#include "revision.h" +#include "diff.h" +#include "hashmap.h" +#include "setup.h" +#include "strvec.h" +#include "run-command.h" +#include "object-store-ll.h" +#include "list-objects.h" +#include "commit-slab.h" +#include "wildmatch.h" + +#define MAX_TAGS (FLAG_BITS - 1) +#define DEFAULT_CANDIDATES 10 + +define_commit_slab(commit_names, struct commit_name *); + +static const char * const describe_usage[] = { + N_("git describe [--all] [--tags] [--contains] [--abbrev=<n>] [<commit-ish>...]"), + N_("git describe [--all] [--tags] [--contains] [--abbrev=<n>] --dirty[=<mark>]"), + N_("git describe <blob>"), + NULL +}; + +static int debug; /* Display lots of verbose info */ +static int all; /* Any valid ref can be used */ +static int tags; /* Allow lightweight tags */ +static int longformat; +static int first_parent; +static int abbrev = -1; /* unspecified */ +static int max_candidates = DEFAULT_CANDIDATES; +static struct hashmap names; +static int have_util; +static struct string_list patterns = STRING_LIST_INIT_NODUP; +static struct string_list exclude_patterns = STRING_LIST_INIT_NODUP; +static int always; +static const char *suffix, *dirty, *broken; +static struct commit_names commit_names; + +/* diff-index command arguments to check if working tree is dirty. */ +static const char *diff_index_args[] = { + "diff-index", "--quiet", "HEAD", "--", NULL +}; + +struct commit_name { + struct hashmap_entry entry; + struct object_id peeled; + struct tag *tag; + unsigned prio:2; /* annotated tag = 2, tag = 1, head = 0 */ + unsigned name_checked:1; + unsigned misnamed:1; + struct object_id oid; + char *path; +}; + +static const char *prio_names[] = { + N_("head"), N_("lightweight"), N_("annotated"), +}; + +static int commit_name_neq(const void *cmp_data UNUSED, + const struct hashmap_entry *eptr, + const struct hashmap_entry *entry_or_key, + const void *peeled) +{ + const struct commit_name *cn1, *cn2; + + cn1 = container_of(eptr, const struct commit_name, entry); + cn2 = container_of(entry_or_key, const struct commit_name, entry); + + return !oideq(&cn1->peeled, peeled ? peeled : &cn2->peeled); +} + +static inline struct commit_name *find_commit_name(const struct object_id *peeled) +{ + return hashmap_get_entry_from_hash(&names, oidhash(peeled), peeled, + struct commit_name, entry); +} + +static int replace_name(struct commit_name *e, + int prio, + const struct object_id *oid, + struct tag **tag) +{ + if (!e || e->prio < prio) + return 1; + + if (e->prio == 2 && prio == 2) { + /* Multiple annotated tags point to the same commit. + * Select one to keep based upon their tagger date. + */ + struct tag *t; + + if (!e->tag) { + t = lookup_tag(the_repository, &e->oid); + if (!t || parse_tag(t)) + return 1; + e->tag = t; + } + + t = lookup_tag(the_repository, oid); + if (!t || parse_tag(t)) + return 0; + *tag = t; + + if (e->tag->date < t->date) + return 1; + } + + return 0; +} + +static void add_to_known_names(const char *path, + const struct object_id *peeled, + int prio, + const struct object_id *oid) +{ + struct commit_name *e = find_commit_name(peeled); + struct tag *tag = NULL; + if (replace_name(e, prio, oid, &tag)) { + if (!e) { + e = xmalloc(sizeof(struct commit_name)); + oidcpy(&e->peeled, peeled); + hashmap_entry_init(&e->entry, oidhash(peeled)); + hashmap_add(&names, &e->entry); + e->path = NULL; + } + e->tag = tag; + e->prio = prio; + e->name_checked = 0; + e->misnamed = 0; + oidcpy(&e->oid, oid); + free(e->path); + e->path = xstrdup(path); + } +} + +static int get_name(const char *path, const struct object_id *oid, + int flag UNUSED, void *cb_data UNUSED) +{ + int is_tag = 0; + struct object_id peeled; + int is_annotated, prio; + const char *path_to_match = NULL; + + if (skip_prefix(path, "refs/tags/", &path_to_match)) { + is_tag = 1; + } else if (all) { + if ((exclude_patterns.nr || patterns.nr) && + !skip_prefix(path, "refs/heads/", &path_to_match) && + !skip_prefix(path, "refs/remotes/", &path_to_match)) { + /* Only accept reference of known type if there are match/exclude patterns */ + return 0; + } + } else { + /* Reject anything outside refs/tags/ unless --all */ + return 0; + } + + /* + * If we're given exclude patterns, first exclude any tag which match + * any of the exclude pattern. + */ + if (exclude_patterns.nr) { + struct string_list_item *item; + + for_each_string_list_item(item, &exclude_patterns) { + if (!wildmatch(item->string, path_to_match, 0)) + return 0; + } + } + + /* + * If we're given patterns, accept only tags which match at least one + * pattern. + */ + if (patterns.nr) { + int found = 0; + struct string_list_item *item; + + for_each_string_list_item(item, &patterns) { + if (!wildmatch(item->string, path_to_match, 0)) { + found = 1; + break; + } + } + + if (!found) + return 0; + } + + /* Is it annotated? */ + if (!peel_iterated_oid(oid, &peeled)) { + is_annotated = !oideq(oid, &peeled); + } else { + oidcpy(&peeled, oid); + is_annotated = 0; + } + + /* + * By default, we only use annotated tags, but with --tags + * we fall back to lightweight ones (even without --tags, + * we still remember lightweight ones, only to give hints + * in an error message). --all allows any refs to be used. + */ + if (is_annotated) + prio = 2; + else if (is_tag) + prio = 1; + else + prio = 0; + + add_to_known_names(all ? path + 5 : path + 10, &peeled, prio, oid); + return 0; +} + +struct possible_tag { + struct commit_name *name; + int depth; + int found_order; + unsigned flag_within; +}; + +static int compare_pt(const void *a_, const void *b_) +{ + struct possible_tag *a = (struct possible_tag *)a_; + struct possible_tag *b = (struct possible_tag *)b_; + if (a->depth != b->depth) + return a->depth - b->depth; + if (a->found_order != b->found_order) + return a->found_order - b->found_order; + return 0; +} + +static unsigned long finish_depth_computation( + struct commit_list **list, + struct possible_tag *best) +{ + unsigned long seen_commits = 0; + while (*list) { + struct commit *c = pop_commit(list); + struct commit_list *parents = c->parents; + seen_commits++; + if (c->object.flags & best->flag_within) { + struct commit_list *a = *list; + while (a) { + struct commit *i = a->item; + if (!(i->object.flags & best->flag_within)) + break; + a = a->next; + } + if (!a) + break; + } else + best->depth++; + while (parents) { + struct commit *p = parents->item; + repo_parse_commit(the_repository, p); + if (!(p->object.flags & SEEN)) + commit_list_insert_by_date(p, list); + p->object.flags |= c->object.flags; + parents = parents->next; + } + } + return seen_commits; +} + +static void append_name(struct commit_name *n, struct strbuf *dst) +{ + if (n->prio == 2 && !n->tag) { + n->tag = lookup_tag(the_repository, &n->oid); + if (!n->tag || parse_tag(n->tag)) + die(_("annotated tag %s not available"), n->path); + } + if (n->tag && !n->name_checked) { + if (strcmp(n->tag->tag, all ? n->path + 5 : n->path)) { + warning(_("tag '%s' is externally known as '%s'"), + n->path, n->tag->tag); + n->misnamed = 1; + } + n->name_checked = 1; + } + + if (n->tag) { + if (all) + strbuf_addstr(dst, "tags/"); + strbuf_addstr(dst, n->tag->tag); + } else { + strbuf_addstr(dst, n->path); + } +} + +static void append_suffix(int depth, const struct object_id *oid, struct strbuf *dst) +{ + strbuf_addf(dst, "-%d-g%s", depth, + repo_find_unique_abbrev(the_repository, oid, abbrev)); +} + +static void describe_commit(struct object_id *oid, struct strbuf *dst) +{ + struct commit *cmit, *gave_up_on = NULL; + struct commit_list *list; + struct commit_name *n; + struct possible_tag all_matches[MAX_TAGS]; + unsigned int match_cnt = 0, annotated_cnt = 0, cur_match; + unsigned long seen_commits = 0; + unsigned int unannotated_cnt = 0; + + cmit = lookup_commit_reference(the_repository, oid); + + n = find_commit_name(&cmit->object.oid); + if (n && (tags || all || n->prio == 2)) { + /* + * Exact match to an existing ref. + */ + append_name(n, dst); + if (n->misnamed || longformat) + append_suffix(0, n->tag ? get_tagged_oid(n->tag) : oid, dst); + if (suffix) + strbuf_addstr(dst, suffix); + return; + } + + if (!max_candidates) + die(_("no tag exactly matches '%s'"), oid_to_hex(&cmit->object.oid)); + if (debug) + fprintf(stderr, _("No exact match on refs or tags, searching to describe\n")); + + if (!have_util) { + struct hashmap_iter iter; + struct commit *c; + struct commit_name *n; + + init_commit_names(&commit_names); + hashmap_for_each_entry(&names, &iter, n, + entry /* member name */) { + c = lookup_commit_reference_gently(the_repository, + &n->peeled, 1); + if (c) + *commit_names_at(&commit_names, c) = n; + } + have_util = 1; + } + + list = NULL; + cmit->object.flags = SEEN; + commit_list_insert(cmit, &list); + while (list) { + struct commit *c = pop_commit(&list); + struct commit_list *parents = c->parents; + struct commit_name **slot; + + seen_commits++; + slot = commit_names_peek(&commit_names, c); + n = slot ? *slot : NULL; + if (n) { + if (!tags && !all && n->prio < 2) { + unannotated_cnt++; + } else if (match_cnt < max_candidates) { + struct possible_tag *t = &all_matches[match_cnt++]; + t->name = n; + t->depth = seen_commits - 1; + t->flag_within = 1u << match_cnt; + t->found_order = match_cnt; + c->object.flags |= t->flag_within; + if (n->prio == 2) + annotated_cnt++; + } + else { + gave_up_on = c; + break; + } + } + for (cur_match = 0; cur_match < match_cnt; cur_match++) { + struct possible_tag *t = &all_matches[cur_match]; + if (!(c->object.flags & t->flag_within)) + t->depth++; + } + /* Stop if last remaining path already covered by best candidate(s) */ + if (annotated_cnt && !list) { + int best_depth = INT_MAX; + unsigned best_within = 0; + for (cur_match = 0; cur_match < match_cnt; cur_match++) { + struct possible_tag *t = &all_matches[cur_match]; + if (t->depth < best_depth) { + best_depth = t->depth; + best_within = t->flag_within; + } else if (t->depth == best_depth) { + best_within |= t->flag_within; + } + } + if ((c->object.flags & best_within) == best_within) { + if (debug) + fprintf(stderr, _("finished search at %s\n"), + oid_to_hex(&c->object.oid)); + break; + } + } + while (parents) { + struct commit *p = parents->item; + repo_parse_commit(the_repository, p); + if (!(p->object.flags & SEEN)) + commit_list_insert_by_date(p, &list); + p->object.flags |= c->object.flags; + parents = parents->next; + + if (first_parent) + break; + } + } + + if (!match_cnt) { + struct object_id *cmit_oid = &cmit->object.oid; + if (always) { + strbuf_add_unique_abbrev(dst, cmit_oid, abbrev); + if (suffix) + strbuf_addstr(dst, suffix); + return; + } + if (unannotated_cnt) + die(_("No annotated tags can describe '%s'.\n" + "However, there were unannotated tags: try --tags."), + oid_to_hex(cmit_oid)); + else + die(_("No tags can describe '%s'.\n" + "Try --always, or create some tags."), + oid_to_hex(cmit_oid)); + } + + QSORT(all_matches, match_cnt, compare_pt); + + if (gave_up_on) { + commit_list_insert_by_date(gave_up_on, &list); + seen_commits--; + } + seen_commits += finish_depth_computation(&list, &all_matches[0]); + free_commit_list(list); + + if (debug) { + static int label_width = -1; + if (label_width < 0) { + int i, w; + for (i = 0; i < ARRAY_SIZE(prio_names); i++) { + w = strlen(_(prio_names[i])); + if (label_width < w) + label_width = w; + } + } + for (cur_match = 0; cur_match < match_cnt; cur_match++) { + struct possible_tag *t = &all_matches[cur_match]; + fprintf(stderr, " %-*s %8d %s\n", + label_width, _(prio_names[t->name->prio]), + t->depth, t->name->path); + } + fprintf(stderr, _("traversed %lu commits\n"), seen_commits); + if (gave_up_on) { + fprintf(stderr, + _("more than %i tags found; listed %i most recent\n" + "gave up search at %s\n"), + max_candidates, max_candidates, + oid_to_hex(&gave_up_on->object.oid)); + } + } + + append_name(all_matches[0].name, dst); + if (all_matches[0].name->misnamed || abbrev) + append_suffix(all_matches[0].depth, &cmit->object.oid, dst); + if (suffix) + strbuf_addstr(dst, suffix); +} + +struct process_commit_data { + struct object_id current_commit; + struct object_id looking_for; + struct strbuf *dst; + struct rev_info *revs; +}; + +static void process_commit(struct commit *commit, void *data) +{ + struct process_commit_data *pcd = data; + pcd->current_commit = commit->object.oid; +} + +static void process_object(struct object *obj, const char *path, void *data) +{ + struct process_commit_data *pcd = data; + + if (oideq(&pcd->looking_for, &obj->oid) && !pcd->dst->len) { + reset_revision_walk(); + describe_commit(&pcd->current_commit, pcd->dst); + strbuf_addf(pcd->dst, ":%s", path); + free_commit_list(pcd->revs->commits); + pcd->revs->commits = NULL; + } +} + +static void describe_blob(struct object_id oid, struct strbuf *dst) +{ + struct rev_info revs; + struct strvec args = STRVEC_INIT; + struct process_commit_data pcd = { *null_oid(), oid, dst, &revs}; + + strvec_pushl(&args, "internal: The first arg is not parsed", + "--objects", "--in-commit-order", "--reverse", "HEAD", + NULL); + + repo_init_revisions(the_repository, &revs, NULL); + if (setup_revisions(args.nr, args.v, &revs, NULL) > 1) + BUG("setup_revisions could not handle all args?"); + + if (prepare_revision_walk(&revs)) + die("revision walk setup failed"); + + traverse_commit_list(&revs, process_commit, process_object, &pcd); + reset_revision_walk(); + release_revisions(&revs); +} + +static void describe(const char *arg, int last_one) +{ + struct object_id oid; + struct commit *cmit; + struct strbuf sb = STRBUF_INIT; + + if (debug) + fprintf(stderr, _("describe %s\n"), arg); + + if (repo_get_oid(the_repository, arg, &oid)) + die(_("Not a valid object name %s"), arg); + cmit = lookup_commit_reference_gently(the_repository, &oid, 1); + + if (cmit) + describe_commit(&oid, &sb); + else if (oid_object_info(the_repository, &oid, NULL) == OBJ_BLOB) + describe_blob(oid, &sb); + else + die(_("%s is neither a commit nor blob"), arg); + + puts(sb.buf); + + if (!last_one) + clear_commit_marks(cmit, -1); + + strbuf_release(&sb); +} + +static int option_parse_exact_match(const struct option *opt, const char *arg, + int unset) +{ + int *val = opt->value; + + BUG_ON_OPT_ARG(arg); + + *val = unset ? DEFAULT_CANDIDATES : 0; + return 0; +} + +int cmd_describe(int argc, const char **argv, const char *prefix) +{ + int contains = 0; + struct option options[] = { + OPT_BOOL(0, "contains", &contains, N_("find the tag that comes after the commit")), + OPT_BOOL(0, "debug", &debug, N_("debug search strategy on stderr")), + OPT_BOOL(0, "all", &all, N_("use any ref")), + OPT_BOOL(0, "tags", &tags, N_("use any tag, even unannotated")), + OPT_BOOL(0, "long", &longformat, N_("always use long format")), + OPT_BOOL(0, "first-parent", &first_parent, N_("only follow first parent")), + OPT__ABBREV(&abbrev), + OPT_CALLBACK_F(0, "exact-match", &max_candidates, NULL, + N_("only output exact matches"), + PARSE_OPT_NOARG, option_parse_exact_match), + OPT_INTEGER(0, "candidates", &max_candidates, + N_("consider <n> most recent tags (default: 10)")), + OPT_STRING_LIST(0, "match", &patterns, N_("pattern"), + N_("only consider tags matching <pattern>")), + OPT_STRING_LIST(0, "exclude", &exclude_patterns, N_("pattern"), + N_("do not consider tags matching <pattern>")), + OPT_BOOL(0, "always", &always, + N_("show abbreviated commit object as fallback")), + {OPTION_STRING, 0, "dirty", &dirty, N_("mark"), + N_("append <mark> on dirty working tree (default: \"-dirty\")"), + PARSE_OPT_OPTARG, NULL, (intptr_t) "-dirty"}, + {OPTION_STRING, 0, "broken", &broken, N_("mark"), + N_("append <mark> on broken working tree (default: \"-broken\")"), + PARSE_OPT_OPTARG, NULL, (intptr_t) "-broken"}, + OPT_END(), + }; + + git_config(git_default_config, NULL); + argc = parse_options(argc, argv, prefix, options, describe_usage, 0); + if (abbrev < 0) + abbrev = DEFAULT_ABBREV; + + if (max_candidates < 0) + max_candidates = 0; + else if (max_candidates > MAX_TAGS) + max_candidates = MAX_TAGS; + + save_commit_buffer = 0; + + if (longformat && abbrev == 0) + die(_("options '%s' and '%s' cannot be used together"), "--long", "--abbrev=0"); + + if (contains) { + struct string_list_item *item; + struct strvec args; + + strvec_init(&args); + strvec_pushl(&args, "name-rev", + "--peel-tag", "--name-only", "--no-undefined", + NULL); + if (always) + strvec_push(&args, "--always"); + if (!all) { + strvec_push(&args, "--tags"); + for_each_string_list_item(item, &patterns) + strvec_pushf(&args, "--refs=refs/tags/%s", item->string); + for_each_string_list_item(item, &exclude_patterns) + strvec_pushf(&args, "--exclude=refs/tags/%s", item->string); + } + if (argc) + strvec_pushv(&args, argv); + else + strvec_push(&args, "HEAD"); + return cmd_name_rev(args.nr, args.v, prefix); + } + + hashmap_init(&names, commit_name_neq, NULL, 0); + for_each_rawref(get_name, NULL); + if (!hashmap_get_size(&names) && !always) + die(_("No names found, cannot describe anything.")); + + if (argc == 0) { + if (broken) { + struct child_process cp = CHILD_PROCESS_INIT; + strvec_pushv(&cp.args, diff_index_args); + cp.git_cmd = 1; + cp.no_stdin = 1; + cp.no_stdout = 1; + + if (!dirty) + dirty = "-dirty"; + + switch (run_command(&cp)) { + case 0: + suffix = NULL; + break; + case 1: + suffix = dirty; + break; + default: + /* diff-index aborted abnormally */ + suffix = broken; + } + } else if (dirty) { + struct lock_file index_lock = LOCK_INIT; + struct rev_info revs; + struct strvec args = STRVEC_INIT; + int fd; + + setup_work_tree(); + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + repo_read_index(the_repository); + refresh_index(&the_index, REFRESH_QUIET|REFRESH_UNMERGED, + NULL, NULL, NULL); + fd = repo_hold_locked_index(the_repository, + &index_lock, 0); + if (0 <= fd) + repo_update_index_if_able(the_repository, &index_lock); + + repo_init_revisions(the_repository, &revs, prefix); + strvec_pushv(&args, diff_index_args); + if (setup_revisions(args.nr, args.v, &revs, NULL) != 1) + BUG("malformed internal diff-index command line"); + run_diff_index(&revs, 0); + + if (!diff_result_code(&revs.diffopt)) + suffix = NULL; + else + suffix = dirty; + release_revisions(&revs); + } + describe("HEAD", 1); + } else if (dirty) { + die(_("option '%s' and commit-ishes cannot be used together"), "--dirty"); + } else if (broken) { + die(_("option '%s' and commit-ishes cannot be used together"), "--broken"); + } else { + while (argc-- > 0) + describe(*argv++, argc == 0); + } + return 0; +} diff --git a/builtin/diagnose.c b/builtin/diagnose.c new file mode 100644 index 0000000..4f22eb2 --- /dev/null +++ b/builtin/diagnose.c @@ -0,0 +1,65 @@ +#include "builtin.h" +#include "abspath.h" +#include "gettext.h" +#include "object-file.h" +#include "parse-options.h" +#include "diagnose.h" + +static const char * const diagnose_usage[] = { + N_("git diagnose [(-o | --output-directory) <path>] [(-s | --suffix) <format>]\n" + " [--mode=<mode>]"), + NULL +}; + +int cmd_diagnose(int argc, const char **argv, const char *prefix) +{ + struct strbuf zip_path = STRBUF_INIT; + time_t now = time(NULL); + struct tm tm; + enum diagnose_mode mode = DIAGNOSE_STATS; + char *option_output = NULL; + char *option_suffix = "%Y-%m-%d-%H%M"; + char *prefixed_filename; + + const struct option diagnose_options[] = { + OPT_STRING('o', "output-directory", &option_output, N_("path"), + N_("specify a destination for the diagnostics archive")), + OPT_STRING('s', "suffix", &option_suffix, N_("format"), + N_("specify a strftime format suffix for the filename")), + OPT_CALLBACK_F(0, "mode", &mode, "(stats|all)", + N_("specify the content of the diagnostic archive"), + PARSE_OPT_NONEG, option_parse_diagnose), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, diagnose_options, + diagnose_usage, 0); + + /* Prepare the path to put the result */ + prefixed_filename = prefix_filename(prefix, + option_output ? option_output : ""); + strbuf_addstr(&zip_path, prefixed_filename); + strbuf_complete(&zip_path, '/'); + + strbuf_addstr(&zip_path, "git-diagnostics-"); + strbuf_addftime(&zip_path, option_suffix, localtime_r(&now, &tm), 0, 0); + strbuf_addstr(&zip_path, ".zip"); + + switch (safe_create_leading_directories(zip_path.buf)) { + case SCLD_OK: + case SCLD_EXISTS: + break; + default: + die_errno(_("could not create leading directories for '%s'"), + zip_path.buf); + } + + /* Prepare diagnostics */ + if (create_diagnostics_archive(&zip_path, mode)) + die_errno(_("unable to create diagnostics archive %s"), + zip_path.buf); + + free(prefixed_filename); + strbuf_release(&zip_path); + return 0; +} diff --git a/builtin/diff-files.c b/builtin/diff-files.c new file mode 100644 index 0000000..f38912c --- /dev/null +++ b/builtin/diff-files.c @@ -0,0 +1,89 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "builtin.h" +#include "config.h" +#include "diff.h" +#include "diff-merges.h" +#include "commit.h" +#include "preload-index.h" +#include "repository.h" +#include "revision.h" +#include "submodule.h" + +static const char diff_files_usage[] = +"git diff-files [-q] [-0 | -1 | -2 | -3 | -c | --cc] [<common-diff-options>] [<path>...]" +"\n" +COMMON_DIFF_OPTIONS_HELP; + +int cmd_diff_files(int argc, const char **argv, const char *prefix) +{ + struct rev_info rev; + int result; + unsigned options = 0; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage(diff_files_usage); + + git_config(git_diff_basic_config, NULL); /* no "diff" UI options */ + + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + repo_init_revisions(the_repository, &rev, prefix); + rev.abbrev = 0; + + /* + * Consider "intent-to-add" files as new by default, unless + * explicitly specified in the command line or anywhere else. + */ + rev.diffopt.ita_invisible_in_index = 1; + + prefix = precompose_argv_prefix(argc, argv, prefix); + + argc = setup_revisions(argc, argv, &rev, NULL); + while (1 < argc && argv[1][0] == '-') { + if (!strcmp(argv[1], "--base")) + rev.max_count = 1; + else if (!strcmp(argv[1], "--ours")) + rev.max_count = 2; + else if (!strcmp(argv[1], "--theirs")) + rev.max_count = 3; + else if (!strcmp(argv[1], "-q")) + options |= DIFF_SILENT_ON_REMOVED; + else + usage(diff_files_usage); + argv++; argc--; + } + if (!rev.diffopt.output_format) + rev.diffopt.output_format = DIFF_FORMAT_RAW; + rev.diffopt.rotate_to_strict = 1; + + /* + * Make sure there are NO revision (i.e. pending object) parameter, + * rev.max_count is reasonable (0 <= n <= 3), and + * there is no other revision filtering parameters. + */ + if (rev.pending.nr || + rev.min_age != -1 || rev.max_age != -1 || + 3 < rev.max_count) + usage(diff_files_usage); + + /* + * "diff-files --base -p" should not combine merges because it + * was not asked to. "diff-files -c -p" should not densify + * (the user should ask with "diff-files --cc" explicitly). + */ + if (rev.max_count == -1 && + (rev.diffopt.output_format & DIFF_FORMAT_PATCH)) + diff_merges_set_dense_combined_if_unset(&rev); + + if (repo_read_index_preload(the_repository, &rev.diffopt.pathspec, 0) < 0) + die_errno("repo_read_index_preload"); + run_diff_files(&rev, options); + result = diff_result_code(&rev.diffopt); + release_revisions(&rev); + return result; +} diff --git a/builtin/diff-index.c b/builtin/diff-index.c new file mode 100644 index 0000000..220f341 --- /dev/null +++ b/builtin/diff-index.c @@ -0,0 +1,79 @@ +#include "builtin.h" +#include "config.h" +#include "diff.h" +#include "diff-merges.h" +#include "commit.h" +#include "preload-index.h" +#include "repository.h" +#include "revision.h" +#include "setup.h" +#include "sparse-index.h" +#include "submodule.h" + +static const char diff_cache_usage[] = +"git diff-index [-m] [--cached] [--merge-base] " +"[<common-diff-options>] <tree-ish> [<path>...]" +"\n" +COMMON_DIFF_OPTIONS_HELP; + +int cmd_diff_index(int argc, const char **argv, const char *prefix) +{ + struct rev_info rev; + unsigned int option = 0; + int i; + int result; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage(diff_cache_usage); + + git_config(git_diff_basic_config, NULL); /* no "diff" UI options */ + repo_init_revisions(the_repository, &rev, prefix); + rev.abbrev = 0; + prefix = precompose_argv_prefix(argc, argv, prefix); + + /* + * We need (some of) diff for merges options (e.g., --cc), and we need + * to avoid conflict with our own meaning of "-m". + */ + diff_merges_suppress_m_parsing(); + + argc = setup_revisions(argc, argv, &rev, NULL); + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (!strcmp(arg, "--cached")) + option |= DIFF_INDEX_CACHED; + else if (!strcmp(arg, "--merge-base")) + option |= DIFF_INDEX_MERGE_BASE; + else if (!strcmp(arg, "-m")) + rev.match_missing = 1; + else + usage(diff_cache_usage); + } + if (!rev.diffopt.output_format) + rev.diffopt.output_format = DIFF_FORMAT_RAW; + + rev.diffopt.rotate_to_strict = 1; + + /* + * Make sure there is one revision (i.e. pending object), + * and there is no revision filtering parameters. + */ + if (rev.pending.nr != 1 || + rev.max_count != -1 || rev.min_age != -1 || rev.max_age != -1) + usage(diff_cache_usage); + if (!(option & DIFF_INDEX_CACHED)) { + setup_work_tree(); + if (repo_read_index_preload(the_repository, &rev.diffopt.pathspec, 0) < 0) { + perror("repo_read_index_preload"); + return -1; + } + } else if (repo_read_index(the_repository) < 0) { + perror("repo_read_index"); + return -1; + } + run_diff_index(&rev, option); + result = diff_result_code(&rev.diffopt); + release_revisions(&rev); + return result; +} diff --git a/builtin/diff-tree.c b/builtin/diff-tree.c new file mode 100644 index 0000000..86be634 --- /dev/null +++ b/builtin/diff-tree.c @@ -0,0 +1,236 @@ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "config.h" +#include "diff.h" +#include "commit.h" +#include "gettext.h" +#include "hex.h" +#include "log-tree.h" +#include "submodule.h" +#include "read-cache-ll.h" +#include "repository.h" +#include "revision.h" +#include "tree.h" + +static struct rev_info log_tree_opt; + +static int diff_tree_commit_oid(const struct object_id *oid) +{ + struct commit *commit = lookup_commit_reference(the_repository, oid); + if (!commit) + return -1; + return log_tree_commit(&log_tree_opt, commit); +} + +/* Diff one or more commits. */ +static int stdin_diff_commit(struct commit *commit, const char *p) +{ + struct object_id oid; + struct commit_list **pptr = NULL; + + /* Graft the fake parents locally to the commit */ + while (isspace(*p++) && !parse_oid_hex(p, &oid, &p)) { + struct commit *parent = lookup_commit(the_repository, &oid); + if (!pptr) { + /* Free the real parent list */ + free_commit_list(commit->parents); + commit->parents = NULL; + pptr = &(commit->parents); + } + if (parent) { + pptr = &commit_list_insert(parent, pptr)->next; + } + } + return log_tree_commit(&log_tree_opt, commit); +} + +/* Diff two trees. */ +static int stdin_diff_trees(struct tree *tree1, const char *p) +{ + struct object_id oid; + struct tree *tree2; + if (!isspace(*p++) || parse_oid_hex(p, &oid, &p) || *p) + return error("Need exactly two trees, separated by a space"); + tree2 = lookup_tree(the_repository, &oid); + if (!tree2 || parse_tree(tree2)) + return -1; + printf("%s %s\n", oid_to_hex(&tree1->object.oid), + oid_to_hex(&tree2->object.oid)); + diff_tree_oid(&tree1->object.oid, &tree2->object.oid, + "", &log_tree_opt.diffopt); + log_tree_diff_flush(&log_tree_opt); + return 0; +} + +static int diff_tree_stdin(char *line) +{ + int len = strlen(line); + struct object_id oid; + struct object *obj; + const char *p; + + if (!len || line[len-1] != '\n') + return -1; + line[len-1] = 0; + if (parse_oid_hex(line, &oid, &p)) + return -1; + obj = parse_object(the_repository, &oid); + if (!obj) + return -1; + if (obj->type == OBJ_COMMIT) + return stdin_diff_commit((struct commit *)obj, p); + if (obj->type == OBJ_TREE) + return stdin_diff_trees((struct tree *)obj, p); + error("Object %s is a %s, not a commit or tree", + oid_to_hex(&oid), type_name(obj->type)); + return -1; +} + +static const char diff_tree_usage[] = +"git diff-tree [--stdin] [-m] [-s] [-v] [--no-commit-id] [--pretty]\n" +" [-t] [-r] [-c | --cc] [--combined-all-paths] [--root] [--merge-base]\n" +" [<common-diff-options>] <tree-ish> [<tree-ish>] [<path>...]\n" +"\n" +" -r diff recursively\n" +" -c show combined diff for merge commits\n" +" --cc show combined diff for merge commits removing uninteresting hunks\n" +" --combined-all-paths\n" +" show name of file in all parents for combined diffs\n" +" --root include the initial commit as diff against /dev/null\n" +COMMON_DIFF_OPTIONS_HELP; + +static void diff_tree_tweak_rev(struct rev_info *rev) +{ + if (!rev->diffopt.output_format) { + if (rev->dense_combined_merges) + rev->diffopt.output_format = DIFF_FORMAT_PATCH; + else + rev->diffopt.output_format = DIFF_FORMAT_RAW; + } +} + +int cmd_diff_tree(int argc, const char **argv, const char *prefix) +{ + char line[1000]; + struct object *tree1, *tree2; + static struct rev_info *opt = &log_tree_opt; + struct setup_revision_opt s_r_opt; + struct userformat_want w; + int read_stdin = 0; + int merge_base = 0; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage(diff_tree_usage); + + git_config(git_diff_basic_config, NULL); /* no "diff" UI options */ + + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + repo_init_revisions(the_repository, opt, prefix); + if (repo_read_index(the_repository) < 0) + die(_("index file corrupt")); + opt->abbrev = 0; + opt->diff = 1; + opt->disable_stdin = 1; + memset(&s_r_opt, 0, sizeof(s_r_opt)); + s_r_opt.tweak = diff_tree_tweak_rev; + + prefix = precompose_argv_prefix(argc, argv, prefix); + argc = setup_revisions(argc, argv, opt, &s_r_opt); + + memset(&w, 0, sizeof(w)); + userformat_find_requirements(NULL, &w); + + if (!opt->show_notes_given && w.notes) + opt->show_notes = 1; + if (opt->show_notes) + load_display_notes(&opt->notes_opt); + + while (--argc > 0) { + const char *arg = *++argv; + + if (!strcmp(arg, "--stdin")) { + read_stdin = 1; + continue; + } + if (!strcmp(arg, "--merge-base")) { + merge_base = 1; + continue; + } + usage(diff_tree_usage); + } + + if (read_stdin && merge_base) + die(_("options '%s' and '%s' cannot be used together"), "--stdin", "--merge-base"); + if (merge_base && opt->pending.nr != 2) + die(_("--merge-base only works with two commits")); + + opt->diffopt.rotate_to_strict = 1; + + /* + * NOTE! We expect "a..b" to expand to "^a b" but it is + * perfectly valid for revision range parser to yield "b ^a", + * which means the same thing. If we get the latter, i.e. the + * second one is marked UNINTERESTING, we recover the original + * order the user gave, i.e. "a..b", by swapping the trees. + */ + switch (opt->pending.nr) { + case 0: + if (!read_stdin) + usage(diff_tree_usage); + break; + case 1: + tree1 = opt->pending.objects[0].item; + diff_tree_commit_oid(&tree1->oid); + break; + case 2: + tree1 = opt->pending.objects[0].item; + tree2 = opt->pending.objects[1].item; + if (merge_base) { + struct object_id oid; + + diff_get_merge_base(opt, &oid); + tree1 = lookup_object(the_repository, &oid); + } else if (tree2->flags & UNINTERESTING) { + SWAP(tree2, tree1); + } + diff_tree_oid(&tree1->oid, &tree2->oid, "", &opt->diffopt); + log_tree_diff_flush(opt); + break; + } + + if (read_stdin) { + int saved_nrl = 0; + int saved_dcctc = 0; + + opt->diffopt.rotate_to_strict = 0; + opt->diffopt.no_free = 1; + if (opt->diffopt.detect_rename) { + if (!the_index.cache) + repo_read_index(the_repository); + opt->diffopt.setup |= DIFF_SETUP_USE_SIZE_CACHE; + } + while (fgets(line, sizeof(line), stdin)) { + struct object_id oid; + + if (get_oid_hex(line, &oid)) { + fputs(line, stdout); + fflush(stdout); + } + else { + diff_tree_stdin(line); + if (saved_nrl < opt->diffopt.needed_rename_limit) + saved_nrl = opt->diffopt.needed_rename_limit; + if (opt->diffopt.degraded_cc_to_c) + saved_dcctc = 1; + } + } + opt->diffopt.degraded_cc_to_c = saved_dcctc; + opt->diffopt.needed_rename_limit = saved_nrl; + opt->diffopt.no_free = 0; + diff_free(&opt->diffopt); + } + + return diff_result_code(&opt->diffopt); +} diff --git a/builtin/diff.c b/builtin/diff.c new file mode 100644 index 0000000..55e7d21 --- /dev/null +++ b/builtin/diff.c @@ -0,0 +1,617 @@ +/* + * Builtin "git diff" + * + * Copyright (c) 2006 Junio C Hamano + */ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "config.h" +#include "ewah/ewok.h" +#include "lockfile.h" +#include "color.h" +#include "commit.h" +#include "blob.h" +#include "gettext.h" +#include "tag.h" +#include "diff.h" +#include "diff-merges.h" +#include "diffcore.h" +#include "preload-index.h" +#include "read-cache-ll.h" +#include "revision.h" +#include "log-tree.h" +#include "setup.h" +#include "submodule.h" +#include "oid-array.h" +#include "tree.h" + +#define DIFF_NO_INDEX_EXPLICIT 1 +#define DIFF_NO_INDEX_IMPLICIT 2 + +static const char builtin_diff_usage[] = +"git diff [<options>] [<commit>] [--] [<path>...]\n" +" or: git diff [<options>] --cached [--merge-base] [<commit>] [--] [<path>...]\n" +" or: git diff [<options>] [--merge-base] <commit> [<commit>...] <commit> [--] [<path>...]\n" +" or: git diff [<options>] <commit>...<commit> [--] [<path>...]\n" +" or: git diff [<options>] <blob> <blob>\n" +" or: git diff [<options>] --no-index [--] <path> <path>" +"\n" +COMMON_DIFF_OPTIONS_HELP; + +static const char *blob_path(struct object_array_entry *entry) +{ + return entry->path ? entry->path : entry->name; +} + +static void stuff_change(struct diff_options *opt, + unsigned old_mode, unsigned new_mode, + const struct object_id *old_oid, + const struct object_id *new_oid, + int old_oid_valid, + int new_oid_valid, + const char *old_path, + const char *new_path) +{ + struct diff_filespec *one, *two; + + if (!is_null_oid(old_oid) && !is_null_oid(new_oid) && + oideq(old_oid, new_oid) && (old_mode == new_mode)) + return; + + if (opt->flags.reverse_diff) { + SWAP(old_mode, new_mode); + SWAP(old_oid, new_oid); + SWAP(old_path, new_path); + } + + if (opt->prefix && + (strncmp(old_path, opt->prefix, opt->prefix_length) || + strncmp(new_path, opt->prefix, opt->prefix_length))) + return; + + one = alloc_filespec(old_path); + two = alloc_filespec(new_path); + fill_filespec(one, old_oid, old_oid_valid, old_mode); + fill_filespec(two, new_oid, new_oid_valid, new_mode); + + diff_queue(&diff_queued_diff, one, two); +} + +static void builtin_diff_b_f(struct rev_info *revs, + int argc, const char **argv UNUSED, + struct object_array_entry **blob) +{ + /* Blob vs file in the working tree*/ + struct stat st; + const char *path; + + if (argc > 1) + usage(builtin_diff_usage); + + GUARD_PATHSPEC(&revs->prune_data, PATHSPEC_FROMTOP | PATHSPEC_LITERAL); + path = revs->prune_data.items[0].match; + + if (lstat(path, &st)) + die_errno(_("failed to stat '%s'"), path); + if (!(S_ISREG(st.st_mode) || S_ISLNK(st.st_mode))) + die(_("'%s': not a regular file or symlink"), path); + + diff_set_mnemonic_prefix(&revs->diffopt, "o/", "w/"); + + if (blob[0]->mode == S_IFINVALID) + blob[0]->mode = canon_mode(st.st_mode); + + stuff_change(&revs->diffopt, + blob[0]->mode, canon_mode(st.st_mode), + &blob[0]->item->oid, null_oid(), + 1, 0, + blob[0]->path ? blob[0]->path : path, + path); + diffcore_std(&revs->diffopt); + diff_flush(&revs->diffopt); +} + +static void builtin_diff_blobs(struct rev_info *revs, + int argc, const char **argv UNUSED, + struct object_array_entry **blob) +{ + const unsigned mode = canon_mode(S_IFREG | 0644); + + if (argc > 1) + usage(builtin_diff_usage); + + if (blob[0]->mode == S_IFINVALID) + blob[0]->mode = mode; + + if (blob[1]->mode == S_IFINVALID) + blob[1]->mode = mode; + + stuff_change(&revs->diffopt, + blob[0]->mode, blob[1]->mode, + &blob[0]->item->oid, &blob[1]->item->oid, + 1, 1, + blob_path(blob[0]), blob_path(blob[1])); + diffcore_std(&revs->diffopt); + diff_flush(&revs->diffopt); +} + +static void builtin_diff_index(struct rev_info *revs, + int argc, const char **argv) +{ + unsigned int option = 0; + while (1 < argc) { + const char *arg = argv[1]; + if (!strcmp(arg, "--cached") || !strcmp(arg, "--staged")) + option |= DIFF_INDEX_CACHED; + else if (!strcmp(arg, "--merge-base")) + option |= DIFF_INDEX_MERGE_BASE; + else + usage(builtin_diff_usage); + argv++; argc--; + } + /* + * Make sure there is one revision (i.e. pending object), + * and there is no revision filtering parameters. + */ + if (revs->pending.nr != 1 || + revs->max_count != -1 || revs->min_age != -1 || + revs->max_age != -1) + usage(builtin_diff_usage); + if (!(option & DIFF_INDEX_CACHED)) { + setup_work_tree(); + if (repo_read_index_preload(the_repository, + &revs->diffopt.pathspec, 0) < 0) { + die_errno("repo_read_index_preload"); + } + } else if (repo_read_index(the_repository) < 0) { + die_errno("repo_read_cache"); + } + run_diff_index(revs, option); +} + +static void builtin_diff_tree(struct rev_info *revs, + int argc, const char **argv, + struct object_array_entry *ent0, + struct object_array_entry *ent1) +{ + const struct object_id *(oid[2]); + struct object_id mb_oid; + int merge_base = 0; + + while (1 < argc) { + const char *arg = argv[1]; + if (!strcmp(arg, "--merge-base")) + merge_base = 1; + else + usage(builtin_diff_usage); + argv++; argc--; + } + + if (merge_base) { + diff_get_merge_base(revs, &mb_oid); + oid[0] = &mb_oid; + oid[1] = &revs->pending.objects[1].item->oid; + } else { + int swap = 0; + + /* + * We saw two trees, ent0 and ent1. If ent1 is uninteresting, + * swap them. + */ + if (ent1->item->flags & UNINTERESTING) + swap = 1; + oid[swap] = &ent0->item->oid; + oid[1 - swap] = &ent1->item->oid; + } + diff_tree_oid(oid[0], oid[1], "", &revs->diffopt); + log_tree_diff_flush(revs); +} + +static void builtin_diff_combined(struct rev_info *revs, + int argc, const char **argv UNUSED, + struct object_array_entry *ent, + int ents, int first_non_parent) +{ + struct oid_array parents = OID_ARRAY_INIT; + int i; + + if (argc > 1) + usage(builtin_diff_usage); + + if (first_non_parent < 0) + die(_("no merge given, only parents.")); + if (first_non_parent >= ents) + BUG("first_non_parent out of range: %d", first_non_parent); + + diff_merges_set_dense_combined_if_unset(revs); + + for (i = 0; i < ents; i++) { + if (i != first_non_parent) + oid_array_append(&parents, &ent[i].item->oid); + } + diff_tree_combined(&ent[first_non_parent].item->oid, &parents, revs); + oid_array_clear(&parents); +} + +static void refresh_index_quietly(void) +{ + struct lock_file lock_file = LOCK_INIT; + int fd; + + fd = repo_hold_locked_index(the_repository, &lock_file, 0); + if (fd < 0) + return; + discard_index(&the_index); + repo_read_index(the_repository); + refresh_index(&the_index, REFRESH_QUIET|REFRESH_UNMERGED, NULL, NULL, + NULL); + repo_update_index_if_able(the_repository, &lock_file); +} + +static void builtin_diff_files(struct rev_info *revs, int argc, const char **argv) +{ + unsigned int options = 0; + + while (1 < argc && argv[1][0] == '-') { + if (!strcmp(argv[1], "--base")) + revs->max_count = 1; + else if (!strcmp(argv[1], "--ours")) + revs->max_count = 2; + else if (!strcmp(argv[1], "--theirs")) + revs->max_count = 3; + else if (!strcmp(argv[1], "-q")) + options |= DIFF_SILENT_ON_REMOVED; + else if (!strcmp(argv[1], "-h")) + usage(builtin_diff_usage); + else { + error(_("invalid option: %s"), argv[1]); + usage(builtin_diff_usage); + } + argv++; argc--; + } + + /* + * "diff --base" should not combine merges because it was not + * asked to. "diff -c" should not densify (if the user wants + * dense one, --cc can be explicitly asked for, or just rely + * on the default). + */ + if (revs->max_count == -1 && + (revs->diffopt.output_format & DIFF_FORMAT_PATCH)) + diff_merges_set_dense_combined_if_unset(revs); + + setup_work_tree(); + if (repo_read_index_preload(the_repository, &revs->diffopt.pathspec, + 0) < 0) { + die_errno("repo_read_index_preload"); + } + run_diff_files(revs, options); +} + +struct symdiff { + struct bitmap *skip; + int warn; + const char *base, *left, *right; +}; + +/* + * Check for symmetric-difference arguments, and if present, arrange + * everything we need to know to handle them correctly. As a bonus, + * weed out all bogus range-based revision specifications, e.g., + * "git diff A..B C..D" or "git diff A..B C" get rejected. + * + * For an actual symmetric diff, *symdiff is set this way: + * + * - its skip is non-NULL and marks *all* rev->pending.objects[i] + * indices that the caller should ignore (extra merge bases, of + * which there might be many, and A in A...B). Note that the + * chosen merge base and right side are NOT marked. + * - warn is set if there are multiple merge bases. + * - base, left, and right point to the names to use in a + * warning about multiple merge bases. + * + * If there is no symmetric diff argument, sym->skip is NULL and + * sym->warn is cleared. The remaining fields are not set. + */ +static void symdiff_prepare(struct rev_info *rev, struct symdiff *sym) +{ + int i, is_symdiff = 0, basecount = 0, othercount = 0; + int lpos = -1, rpos = -1, basepos = -1; + struct bitmap *map = NULL; + + /* + * Use the whence fields to find merge bases and left and + * right parts of symmetric difference, so that we do not + * depend on the order that revisions are parsed. If there + * are any revs that aren't from these sources, we have a + * "git diff C A...B" or "git diff A...B C" case. Or we + * could even get "git diff A...B C...E", for instance. + * + * If we don't have just one merge base, we pick one + * at random. + * + * NB: REV_CMD_LEFT, REV_CMD_RIGHT are also used for A..B, + * so we must check for SYMMETRIC_LEFT too. The two arrays + * rev->pending.objects and rev->cmdline.rev are parallel. + */ + for (i = 0; i < rev->cmdline.nr; i++) { + struct object *obj = rev->pending.objects[i].item; + switch (rev->cmdline.rev[i].whence) { + case REV_CMD_MERGE_BASE: + if (basepos < 0) + basepos = i; + basecount++; + break; /* do mark all bases */ + case REV_CMD_LEFT: + if (lpos >= 0) + usage(builtin_diff_usage); + lpos = i; + if (obj->flags & SYMMETRIC_LEFT) { + is_symdiff = 1; + break; /* do mark A */ + } + continue; + case REV_CMD_RIGHT: + if (rpos >= 0) + usage(builtin_diff_usage); + rpos = i; + continue; /* don't mark B */ + case REV_CMD_PARENTS_ONLY: + case REV_CMD_REF: + case REV_CMD_REV: + othercount++; + continue; + } + if (!map) + map = bitmap_new(); + bitmap_set(map, i); + } + + /* + * Forbid any additional revs for both A...B and A..B. + */ + if (lpos >= 0 && othercount > 0) + usage(builtin_diff_usage); + + if (!is_symdiff) { + bitmap_free(map); + sym->warn = 0; + sym->skip = NULL; + return; + } + + sym->left = rev->pending.objects[lpos].name; + sym->right = rev->pending.objects[rpos].name; + if (basecount == 0) + die(_("%s...%s: no merge base"), sym->left, sym->right); + sym->base = rev->pending.objects[basepos].name; + bitmap_unset(map, basepos); /* unmark the base we want */ + sym->warn = basecount > 1; + sym->skip = map; +} + +int cmd_diff(int argc, const char **argv, const char *prefix) +{ + int i; + struct rev_info rev; + struct object_array ent = OBJECT_ARRAY_INIT; + int first_non_parent = -1; + int blobs = 0, paths = 0; + struct object_array_entry *blob[2]; + int nongit = 0, no_index = 0; + int result; + struct symdiff sdiff; + + /* + * We could get N tree-ish in the rev.pending_objects list. + * Also there could be M blobs there, and P pathspecs. --cached may + * also be present. + * + * N=0, M=0: + * cache vs files (diff-files) + * + * N=0, M=0, --cached: + * HEAD vs cache (diff-index --cached) + * + * N=0, M=2: + * compare two random blobs. P must be zero. + * + * N=0, M=1, P=1: + * compare a blob with a working tree file. + * + * N=1, M=0: + * tree vs files (diff-index) + * + * N=1, M=0, --cached: + * tree vs cache (diff-index --cached) + * + * N=2, M=0: + * tree vs tree (diff-tree) + * + * N=0, M=0, P=2: + * compare two filesystem entities (aka --no-index). + * + * Other cases are errors. + */ + + /* Were we asked to do --no-index explicitly? */ + for (i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--")) { + i++; + break; + } + if (!strcmp(argv[i], "--no-index")) + no_index = DIFF_NO_INDEX_EXPLICIT; + if (argv[i][0] != '-') + break; + } + + prefix = setup_git_directory_gently(&nongit); + + if (!nongit) { + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + } + + if (!no_index) { + /* + * Treat git diff with at least one path outside of the + * repo the same as if the command would have been executed + * outside of a git repository. In this case it behaves + * the same way as "git diff --no-index <a> <b>", which acts + * as a colourful "diff" replacement. + */ + if (nongit || ((argc == i + 2) && + (!path_inside_repo(prefix, argv[i]) || + !path_inside_repo(prefix, argv[i + 1])))) + no_index = DIFF_NO_INDEX_IMPLICIT; + } + + init_diff_ui_defaults(); + git_config(git_diff_ui_config, NULL); + prefix = precompose_argv_prefix(argc, argv, prefix); + + repo_init_revisions(the_repository, &rev, prefix); + + /* Set up defaults that will apply to both no-index and regular diffs. */ + init_diffstat_widths(&rev.diffopt); + rev.diffopt.flags.allow_external = 1; + rev.diffopt.flags.allow_textconv = 1; + + /* If this is a no-index diff, just run it and exit there. */ + if (no_index) + exit(diff_no_index(&rev, no_index == DIFF_NO_INDEX_IMPLICIT, + argc, argv)); + + + /* + * Otherwise, we are doing the usual "git" diff; set up any + * further defaults that apply to regular diffs. + */ + rev.diffopt.skip_stat_unmatch = !!diff_auto_refresh_index; + + /* + * Default to intent-to-add entries invisible in the + * index. This makes them show up as new files in diff-files + * and not at all in diff-cached. + */ + rev.diffopt.ita_invisible_in_index = 1; + + if (nongit) + die(_("Not a git repository")); + argc = setup_revisions(argc, argv, &rev, NULL); + if (!rev.diffopt.output_format) { + rev.diffopt.output_format = DIFF_FORMAT_PATCH; + diff_setup_done(&rev.diffopt); + } + + rev.diffopt.flags.recursive = 1; + rev.diffopt.rotate_to_strict = 1; + + setup_diff_pager(&rev.diffopt); + + /* + * Do we have --cached and not have a pending object, then + * default to HEAD by hand. Eek. + */ + if (!rev.pending.nr) { + int i; + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (!strcmp(arg, "--")) + break; + else if (!strcmp(arg, "--cached") || + !strcmp(arg, "--staged")) { + add_head_to_pending(&rev); + if (!rev.pending.nr) { + struct tree *tree; + tree = lookup_tree(the_repository, + the_repository->hash_algo->empty_tree); + add_pending_object(&rev, &tree->object, "HEAD"); + } + break; + } + } + } + + symdiff_prepare(&rev, &sdiff); + for (i = 0; i < rev.pending.nr; i++) { + struct object_array_entry *entry = &rev.pending.objects[i]; + struct object *obj = entry->item; + const char *name = entry->name; + int flags = (obj->flags & UNINTERESTING); + if (!obj->parsed) + obj = parse_object(the_repository, &obj->oid); + obj = deref_tag(the_repository, obj, NULL, 0); + if (!obj) + die(_("invalid object '%s' given."), name); + if (obj->type == OBJ_COMMIT) + obj = &repo_get_commit_tree(the_repository, + ((struct commit *)obj))->object; + + if (obj->type == OBJ_TREE) { + if (sdiff.skip && bitmap_get(sdiff.skip, i)) + continue; + obj->flags |= flags; + add_object_array(obj, name, &ent); + if (first_non_parent < 0 && + (i >= rev.cmdline.nr || /* HEAD by hand. */ + rev.cmdline.rev[i].whence != REV_CMD_PARENTS_ONLY)) + first_non_parent = ent.nr - 1; + } else if (obj->type == OBJ_BLOB) { + if (2 <= blobs) + die(_("more than two blobs given: '%s'"), name); + blob[blobs] = entry; + blobs++; + + } else { + die(_("unhandled object '%s' given."), name); + } + } + if (rev.prune_data.nr) + paths += rev.prune_data.nr; + + /* + * Now, do the arguments look reasonable? + */ + if (!ent.nr) { + switch (blobs) { + case 0: + builtin_diff_files(&rev, argc, argv); + break; + case 1: + if (paths != 1) + usage(builtin_diff_usage); + builtin_diff_b_f(&rev, argc, argv, blob); + break; + case 2: + if (paths) + usage(builtin_diff_usage); + builtin_diff_blobs(&rev, argc, argv, blob); + break; + default: + usage(builtin_diff_usage); + } + } + else if (blobs) + usage(builtin_diff_usage); + else if (ent.nr == 1) + builtin_diff_index(&rev, argc, argv); + else if (ent.nr == 2) { + if (sdiff.warn) + warning(_("%s...%s: multiple merge bases, using %s"), + sdiff.left, sdiff.right, sdiff.base); + builtin_diff_tree(&rev, argc, argv, + &ent.objects[0], &ent.objects[1]); + } else + builtin_diff_combined(&rev, argc, argv, + ent.objects, ent.nr, + first_non_parent); + result = diff_result_code(&rev.diffopt); + if (1 < rev.diffopt.skip_stat_unmatch) + refresh_index_quietly(); + release_revisions(&rev); + object_array_clear(&ent); + UNLEAK(blob); + return result; +} diff --git a/builtin/difftool.c b/builtin/difftool.c new file mode 100644 index 0000000..0f5eae9 --- /dev/null +++ b/builtin/difftool.c @@ -0,0 +1,794 @@ +/* + * "git difftool" builtin command + * + * This is a wrapper around the GIT_EXTERNAL_DIFF-compatible + * git-difftool--helper script. + * + * This script exports GIT_EXTERNAL_DIFF and GIT_PAGER for use by git. + * The GIT_DIFF* variables are exported for use by git-difftool--helper. + * + * Any arguments that are unknown to this script are forwarded to 'git diff'. + * + * Copyright (C) 2016 Johannes Schindelin + */ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "abspath.h" +#include "config.h" +#include "copy.h" +#include "run-command.h" +#include "environment.h" +#include "exec-cmd.h" +#include "gettext.h" +#include "hex.h" +#include "parse-options.h" +#include "read-cache-ll.h" +#include "sparse-index.h" +#include "strvec.h" +#include "strbuf.h" +#include "lockfile.h" +#include "object-file.h" +#include "object-store-ll.h" +#include "dir.h" +#include "entry.h" +#include "setup.h" + +static int trust_exit_code; + +static const char *const builtin_difftool_usage[] = { + N_("git difftool [<options>] [<commit> [<commit>]] [--] [<path>...]"), + NULL +}; + +static int difftool_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + if (!strcmp(var, "difftool.trustexitcode")) { + trust_exit_code = git_config_bool(var, value); + return 0; + } + + return git_default_config(var, value, ctx, cb); +} + +static int print_tool_help(void) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + + cmd.git_cmd = 1; + strvec_pushl(&cmd.args, "mergetool", "--tool-help=diff", NULL); + return run_command(&cmd); +} + +static int parse_index_info(char *p, int *mode1, int *mode2, + struct object_id *oid1, struct object_id *oid2, + char *status) +{ + if (*p != ':') + return error("expected ':', got '%c'", *p); + *mode1 = (int)strtol(p + 1, &p, 8); + if (*p != ' ') + return error("expected ' ', got '%c'", *p); + *mode2 = (int)strtol(p + 1, &p, 8); + if (*p != ' ') + return error("expected ' ', got '%c'", *p); + if (parse_oid_hex(++p, oid1, (const char **)&p)) + return error("expected object ID, got '%s'", p); + if (*p != ' ') + return error("expected ' ', got '%c'", *p); + if (parse_oid_hex(++p, oid2, (const char **)&p)) + return error("expected object ID, got '%s'", p); + if (*p != ' ') + return error("expected ' ', got '%c'", *p); + *status = *++p; + if (!*status) + return error("missing status"); + if (p[1] && !isdigit(p[1])) + return error("unexpected trailer: '%s'", p + 1); + return 0; +} + +/* + * Remove any trailing slash from $workdir + * before starting to avoid double slashes in symlink targets. + */ +static void add_path(struct strbuf *buf, size_t base_len, const char *path) +{ + strbuf_setlen(buf, base_len); + if (buf->len && buf->buf[buf->len - 1] != '/') + strbuf_addch(buf, '/'); + strbuf_addstr(buf, path); +} + +/* + * Determine whether we can simply reuse the file in the worktree. + */ +static int use_wt_file(const char *workdir, const char *name, + struct object_id *oid) +{ + struct strbuf buf = STRBUF_INIT; + struct stat st; + int use = 0; + + strbuf_addstr(&buf, workdir); + add_path(&buf, buf.len, name); + + if (!lstat(buf.buf, &st) && !S_ISLNK(st.st_mode)) { + struct object_id wt_oid; + int fd = open(buf.buf, O_RDONLY); + + if (fd >= 0 && + !index_fd(&the_index, &wt_oid, fd, &st, OBJ_BLOB, name, 0)) { + if (is_null_oid(oid)) { + oidcpy(oid, &wt_oid); + use = 1; + } else if (oideq(oid, &wt_oid)) + use = 1; + } + } + + strbuf_release(&buf); + + return use; +} + +struct working_tree_entry { + struct hashmap_entry entry; + char path[FLEX_ARRAY]; +}; + +static int working_tree_entry_cmp(const void *cmp_data UNUSED, + const struct hashmap_entry *eptr, + const struct hashmap_entry *entry_or_key, + const void *keydata UNUSED) +{ + const struct working_tree_entry *a, *b; + + a = container_of(eptr, const struct working_tree_entry, entry); + b = container_of(entry_or_key, const struct working_tree_entry, entry); + + return strcmp(a->path, b->path); +} + +/* + * The `left` and `right` entries hold paths for the symlinks hashmap, + * and a SHA-1 surrounded by brief text for submodules. + */ +struct pair_entry { + struct hashmap_entry entry; + char left[PATH_MAX], right[PATH_MAX]; + const char path[FLEX_ARRAY]; +}; + +static int pair_cmp(const void *cmp_data UNUSED, + const struct hashmap_entry *eptr, + const struct hashmap_entry *entry_or_key, + const void *keydata UNUSED) +{ + const struct pair_entry *a, *b; + + a = container_of(eptr, const struct pair_entry, entry); + b = container_of(entry_or_key, const struct pair_entry, entry); + + return strcmp(a->path, b->path); +} + +static void add_left_or_right(struct hashmap *map, const char *path, + const char *content, int is_right) +{ + struct pair_entry *e, *existing; + + FLEX_ALLOC_STR(e, path, path); + hashmap_entry_init(&e->entry, strhash(path)); + existing = hashmap_get_entry(map, e, entry, NULL); + if (existing) { + free(e); + e = existing; + } else { + e->left[0] = e->right[0] = '\0'; + hashmap_add(map, &e->entry); + } + strlcpy(is_right ? e->right : e->left, content, PATH_MAX); +} + +struct path_entry { + struct hashmap_entry entry; + char path[FLEX_ARRAY]; +}; + +static int path_entry_cmp(const void *cmp_data UNUSED, + const struct hashmap_entry *eptr, + const struct hashmap_entry *entry_or_key, + const void *key) +{ + const struct path_entry *a, *b; + + a = container_of(eptr, const struct path_entry, entry); + b = container_of(entry_or_key, const struct path_entry, entry); + + return strcmp(a->path, key ? key : b->path); +} + +static void changed_files(struct hashmap *result, const char *index_path, + const char *workdir) +{ + struct child_process update_index = CHILD_PROCESS_INIT; + struct child_process diff_files = CHILD_PROCESS_INIT; + struct strbuf buf = STRBUF_INIT; + const char *git_dir = absolute_path(get_git_dir()); + FILE *fp; + + strvec_pushl(&update_index.args, + "--git-dir", git_dir, "--work-tree", workdir, + "update-index", "--really-refresh", "-q", + "--unmerged", NULL); + update_index.no_stdin = 1; + update_index.no_stdout = 1; + update_index.no_stderr = 1; + update_index.git_cmd = 1; + update_index.use_shell = 0; + update_index.clean_on_exit = 1; + update_index.dir = workdir; + strvec_pushf(&update_index.env, "GIT_INDEX_FILE=%s", index_path); + /* Ignore any errors of update-index */ + run_command(&update_index); + + strvec_pushl(&diff_files.args, + "--git-dir", git_dir, "--work-tree", workdir, + "diff-files", "--name-only", "-z", NULL); + diff_files.no_stdin = 1; + diff_files.git_cmd = 1; + diff_files.use_shell = 0; + diff_files.clean_on_exit = 1; + diff_files.out = -1; + diff_files.dir = workdir; + strvec_pushf(&diff_files.env, "GIT_INDEX_FILE=%s", index_path); + if (start_command(&diff_files)) + die("could not obtain raw diff"); + fp = xfdopen(diff_files.out, "r"); + while (!strbuf_getline_nul(&buf, fp)) { + struct path_entry *entry; + FLEX_ALLOC_STR(entry, path, buf.buf); + hashmap_entry_init(&entry->entry, strhash(buf.buf)); + hashmap_add(result, &entry->entry); + } + fclose(fp); + if (finish_command(&diff_files)) + die("diff-files did not exit properly"); + strbuf_release(&buf); +} + +static int ensure_leading_directories(char *path) +{ + switch (safe_create_leading_directories(path)) { + case SCLD_OK: + case SCLD_EXISTS: + return 0; + default: + return error(_("could not create leading directories " + "of '%s'"), path); + } +} + +/* + * Unconditional writing of a plain regular file is what + * "git difftool --dir-diff" wants to do for symlinks. We are preparing two + * temporary directories to be fed to a Git-unaware tool that knows how to + * show a diff of two directories (e.g. "diff -r A B"). + * + * Because the tool is Git-unaware, if a symbolic link appears in either of + * these temporary directories, it will try to dereference and show the + * difference of the target of the symbolic link, which is not what we want, + * as the goal of the dir-diff mode is to produce an output that is logically + * equivalent to what "git diff" produces. + * + * Most importantly, we want to get textual comparison of the result of the + * readlink(2). get_symlink() provides that---it returns the contents of + * the symlink that gets written to a regular file to force the external tool + * to compare the readlink(2) result as text, even on a filesystem that is + * capable of doing a symbolic link. + */ +static char *get_symlink(const struct object_id *oid, const char *path) +{ + char *data; + if (is_null_oid(oid)) { + /* The symlink is unknown to Git so read from the filesystem */ + struct strbuf link = STRBUF_INIT; + if (has_symlinks) { + if (strbuf_readlink(&link, path, strlen(path))) + die(_("could not read symlink %s"), path); + } else if (strbuf_read_file(&link, path, 128)) + die(_("could not read symlink file %s"), path); + + data = strbuf_detach(&link, NULL); + } else { + enum object_type type; + unsigned long size; + data = repo_read_object_file(the_repository, oid, &type, + &size); + if (!data) + die(_("could not read object %s for symlink %s"), + oid_to_hex(oid), path); + } + + return data; +} + +static int checkout_path(unsigned mode, struct object_id *oid, + const char *path, const struct checkout *state) +{ + struct cache_entry *ce; + int ret; + + ce = make_transient_cache_entry(mode, oid, path, 0, NULL); + ret = checkout_entry(ce, state, NULL, NULL); + + discard_cache_entry(ce); + return ret; +} + +static void write_file_in_directory(struct strbuf *dir, size_t dir_len, + const char *path, const char *content) +{ + add_path(dir, dir_len, path); + ensure_leading_directories(dir->buf); + unlink(dir->buf); + write_file(dir->buf, "%s", content); +} + +/* Write the file contents for the left and right sides of the difftool + * dir-diff representation for submodules and symlinks. Symlinks and submodules + * are written as regular text files so that external diff tools can diff them + * as text files, resulting in behavior that is analogous to to what "git diff" + * displays for symlink and submodule diffs. + */ +static void write_standin_files(struct pair_entry *entry, + struct strbuf *ldir, size_t ldir_len, + struct strbuf *rdir, size_t rdir_len) +{ + if (*entry->left) + write_file_in_directory(ldir, ldir_len, entry->path, entry->left); + if (*entry->right) + write_file_in_directory(rdir, rdir_len, entry->path, entry->right); +} + +static int run_dir_diff(const char *extcmd, int symlinks, const char *prefix, + struct child_process *child) +{ + struct strbuf info = STRBUF_INIT, lpath = STRBUF_INIT; + struct strbuf rpath = STRBUF_INIT, buf = STRBUF_INIT; + struct strbuf ldir = STRBUF_INIT, rdir = STRBUF_INIT; + struct strbuf wtdir = STRBUF_INIT; + struct strbuf tmpdir = STRBUF_INIT; + char *lbase_dir = NULL, *rbase_dir = NULL; + size_t ldir_len, rdir_len, wtdir_len; + const char *workdir, *tmp; + int ret = 0, i; + FILE *fp = NULL; + struct hashmap working_tree_dups = HASHMAP_INIT(working_tree_entry_cmp, + NULL); + struct hashmap submodules = HASHMAP_INIT(pair_cmp, NULL); + struct hashmap symlinks2 = HASHMAP_INIT(pair_cmp, NULL); + struct hashmap_iter iter; + struct pair_entry *entry; + struct index_state wtindex = INDEX_STATE_INIT(the_repository); + struct checkout lstate, rstate; + int err = 0; + struct child_process cmd = CHILD_PROCESS_INIT; + struct hashmap wt_modified, tmp_modified; + int indices_loaded = 0; + + workdir = get_git_work_tree(); + + /* Setup temp directories */ + tmp = getenv("TMPDIR"); + strbuf_add_absolute_path(&tmpdir, tmp ? tmp : "/tmp"); + strbuf_trim_trailing_dir_sep(&tmpdir); + strbuf_addstr(&tmpdir, "/git-difftool.XXXXXX"); + if (!mkdtemp(tmpdir.buf)) { + ret = error("could not create '%s'", tmpdir.buf); + goto finish; + } + strbuf_addf(&ldir, "%s/left/", tmpdir.buf); + strbuf_addf(&rdir, "%s/right/", tmpdir.buf); + strbuf_addstr(&wtdir, workdir); + if (!wtdir.len || !is_dir_sep(wtdir.buf[wtdir.len - 1])) + strbuf_addch(&wtdir, '/'); + mkdir(ldir.buf, 0700); + mkdir(rdir.buf, 0700); + + memset(&lstate, 0, sizeof(lstate)); + lstate.base_dir = lbase_dir = xstrdup(ldir.buf); + lstate.base_dir_len = ldir.len; + lstate.force = 1; + memset(&rstate, 0, sizeof(rstate)); + rstate.base_dir = rbase_dir = xstrdup(rdir.buf); + rstate.base_dir_len = rdir.len; + rstate.force = 1; + + ldir_len = ldir.len; + rdir_len = rdir.len; + wtdir_len = wtdir.len; + + child->no_stdin = 1; + child->git_cmd = 1; + child->use_shell = 0; + child->clean_on_exit = 1; + child->dir = prefix; + child->out = -1; + if (start_command(child)) + die("could not obtain raw diff"); + fp = xfdopen(child->out, "r"); + + /* Build index info for left and right sides of the diff */ + i = 0; + while (!strbuf_getline_nul(&info, fp)) { + int lmode, rmode; + struct object_id loid, roid; + char status; + const char *src_path, *dst_path; + + if (starts_with(info.buf, "::")) + die(N_("combined diff formats ('-c' and '--cc') are " + "not supported in\n" + "directory diff mode ('-d' and '--dir-diff').")); + + if (parse_index_info(info.buf, &lmode, &rmode, &loid, &roid, + &status)) + break; + if (strbuf_getline_nul(&lpath, fp)) + break; + src_path = lpath.buf; + + i++; + if (status != 'C' && status != 'R') { + dst_path = src_path; + } else { + if (strbuf_getline_nul(&rpath, fp)) + break; + dst_path = rpath.buf; + } + + if (S_ISGITLINK(lmode) || S_ISGITLINK(rmode)) { + strbuf_reset(&buf); + strbuf_addf(&buf, "Subproject commit %s", + oid_to_hex(&loid)); + add_left_or_right(&submodules, src_path, buf.buf, 0); + strbuf_reset(&buf); + strbuf_addf(&buf, "Subproject commit %s", + oid_to_hex(&roid)); + if (oideq(&loid, &roid)) + strbuf_addstr(&buf, "-dirty"); + add_left_or_right(&submodules, dst_path, buf.buf, 1); + continue; + } + + if (S_ISLNK(lmode)) { + char *content = get_symlink(&loid, src_path); + add_left_or_right(&symlinks2, src_path, content, 0); + free(content); + } + + if (S_ISLNK(rmode)) { + char *content = get_symlink(&roid, dst_path); + add_left_or_right(&symlinks2, dst_path, content, 1); + free(content); + } + + if (lmode && status != 'C') { + if (checkout_path(lmode, &loid, src_path, &lstate)) { + ret = error("could not write '%s'", src_path); + goto finish; + } + } + + if (rmode && !S_ISLNK(rmode)) { + struct working_tree_entry *entry; + + /* Avoid duplicate working_tree entries */ + FLEX_ALLOC_STR(entry, path, dst_path); + hashmap_entry_init(&entry->entry, strhash(dst_path)); + if (hashmap_get(&working_tree_dups, &entry->entry, + NULL)) { + free(entry); + continue; + } + hashmap_add(&working_tree_dups, &entry->entry); + + if (!use_wt_file(workdir, dst_path, &roid)) { + if (checkout_path(rmode, &roid, dst_path, + &rstate)) { + ret = error("could not write '%s'", + dst_path); + goto finish; + } + } else if (!is_null_oid(&roid)) { + /* + * Changes in the working tree need special + * treatment since they are not part of the + * index. + */ + struct cache_entry *ce2 = + make_cache_entry(&wtindex, rmode, &roid, + dst_path, 0, 0); + + add_index_entry(&wtindex, ce2, + ADD_CACHE_JUST_APPEND); + + add_path(&rdir, rdir_len, dst_path); + if (ensure_leading_directories(rdir.buf)) { + ret = error("could not create " + "directory for '%s'", + dst_path); + goto finish; + } + add_path(&wtdir, wtdir_len, dst_path); + if (symlinks) { + if (symlink(wtdir.buf, rdir.buf)) { + ret = error_errno("could not symlink '%s' to '%s'", wtdir.buf, rdir.buf); + goto finish; + } + } else { + struct stat st; + if (stat(wtdir.buf, &st)) + st.st_mode = 0644; + if (copy_file(rdir.buf, wtdir.buf, + st.st_mode)) { + ret = error("could not copy '%s' to '%s'", wtdir.buf, rdir.buf); + goto finish; + } + } + } + } + } + + fclose(fp); + fp = NULL; + if (finish_command(child)) { + ret = error("error occurred running diff --raw"); + goto finish; + } + + if (!i) + goto finish; + + /* + * Changes to submodules require special treatment.This loop writes a + * temporary file to both the left and right directories to show the + * change in the recorded SHA1 for the submodule. + */ + hashmap_for_each_entry(&submodules, &iter, entry, + entry /* member name */) { + write_standin_files(entry, &ldir, ldir_len, &rdir, rdir_len); + } + + /* + * Symbolic links require special treatment. The standard "git diff" + * shows only the link itself, not the contents of the link target. + * This loop replicates that behavior. + */ + hashmap_for_each_entry(&symlinks2, &iter, entry, + entry /* member name */) { + + write_standin_files(entry, &ldir, ldir_len, &rdir, rdir_len); + } + + strbuf_setlen(&ldir, ldir_len); + strbuf_setlen(&rdir, rdir_len); + + if (extcmd) { + strvec_push(&cmd.args, extcmd); + } else { + strvec_push(&cmd.args, "difftool--helper"); + cmd.git_cmd = 1; + setenv("GIT_DIFFTOOL_DIRDIFF", "true", 1); + } + strvec_pushl(&cmd.args, ldir.buf, rdir.buf, NULL); + ret = run_command(&cmd); + + /* TODO: audit for interaction with sparse-index. */ + ensure_full_index(&wtindex); + + /* + * If the diff includes working copy files and those + * files were modified during the diff, then the changes + * should be copied back to the working tree. + * Do not copy back files when symlinks are used and the + * external tool did not replace the original link with a file. + * + * These hashes are loaded lazily since they aren't needed + * in the common case of --symlinks and the difftool updating + * files through the symlink. + */ + hashmap_init(&wt_modified, path_entry_cmp, NULL, wtindex.cache_nr); + hashmap_init(&tmp_modified, path_entry_cmp, NULL, wtindex.cache_nr); + + for (i = 0; i < wtindex.cache_nr; i++) { + struct hashmap_entry dummy; + const char *name = wtindex.cache[i]->name; + struct stat st; + + add_path(&rdir, rdir_len, name); + if (lstat(rdir.buf, &st)) + continue; + + if ((symlinks && S_ISLNK(st.st_mode)) || !S_ISREG(st.st_mode)) + continue; + + if (!indices_loaded) { + struct lock_file lock = LOCK_INIT; + strbuf_reset(&buf); + strbuf_addf(&buf, "%s/wtindex", tmpdir.buf); + if (hold_lock_file_for_update(&lock, buf.buf, 0) < 0 || + write_locked_index(&wtindex, &lock, COMMIT_LOCK)) { + ret = error("could not write %s", buf.buf); + goto finish; + } + changed_files(&wt_modified, buf.buf, workdir); + strbuf_setlen(&rdir, rdir_len); + changed_files(&tmp_modified, buf.buf, rdir.buf); + add_path(&rdir, rdir_len, name); + indices_loaded = 1; + } + + hashmap_entry_init(&dummy, strhash(name)); + if (hashmap_get(&tmp_modified, &dummy, name)) { + add_path(&wtdir, wtdir_len, name); + if (hashmap_get(&wt_modified, &dummy, name)) { + warning(_("both files modified: '%s' and '%s'."), + wtdir.buf, rdir.buf); + warning(_("working tree file has been left.")); + warning("%s", ""); + err = 1; + } else if (unlink(wtdir.buf) || + copy_file(wtdir.buf, rdir.buf, st.st_mode)) + warning_errno(_("could not copy '%s' to '%s'"), + rdir.buf, wtdir.buf); + } + } + + if (err) { + warning(_("temporary files exist in '%s'."), tmpdir.buf); + warning(_("you may want to cleanup or recover these.")); + ret = 1; + } else { + remove_dir_recursively(&tmpdir, 0); + if (ret) + warning(_("failed: %d"), ret); + } + +finish: + if (fp) + fclose(fp); + + free(lbase_dir); + free(rbase_dir); + strbuf_release(&ldir); + strbuf_release(&rdir); + strbuf_release(&wtdir); + strbuf_release(&buf); + strbuf_release(&tmpdir); + + return (ret < 0) ? 1 : ret; +} + +static int run_file_diff(int prompt, const char *prefix, + struct child_process *child) +{ + const char *env[] = { + "GIT_PAGER=", "GIT_EXTERNAL_DIFF=git-difftool--helper", NULL, + NULL + }; + + if (prompt > 0) + env[2] = "GIT_DIFFTOOL_PROMPT=true"; + else if (!prompt) + env[2] = "GIT_DIFFTOOL_NO_PROMPT=true"; + + child->git_cmd = 1; + child->dir = prefix; + strvec_pushv(&child->env, env); + + return run_command(child); +} + +int cmd_difftool(int argc, const char **argv, const char *prefix) +{ + int use_gui_tool = -1, dir_diff = 0, prompt = -1, symlinks = 0, + tool_help = 0, no_index = 0; + static char *difftool_cmd = NULL, *extcmd = NULL; + struct option builtin_difftool_options[] = { + OPT_BOOL('g', "gui", &use_gui_tool, + N_("use `diff.guitool` instead of `diff.tool`")), + OPT_BOOL('d', "dir-diff", &dir_diff, + N_("perform a full-directory diff")), + OPT_SET_INT_F('y', "no-prompt", &prompt, + N_("do not prompt before launching a diff tool"), + 0, PARSE_OPT_NONEG), + OPT_SET_INT_F(0, "prompt", &prompt, NULL, + 1, PARSE_OPT_NONEG | PARSE_OPT_HIDDEN), + OPT_BOOL(0, "symlinks", &symlinks, + N_("use symlinks in dir-diff mode")), + OPT_STRING('t', "tool", &difftool_cmd, N_("tool"), + N_("use the specified diff tool")), + OPT_BOOL(0, "tool-help", &tool_help, + N_("print a list of diff tools that may be used with " + "`--tool`")), + OPT_BOOL(0, "trust-exit-code", &trust_exit_code, + N_("make 'git-difftool' exit when an invoked diff " + "tool returns a non-zero exit code")), + OPT_STRING('x', "extcmd", &extcmd, N_("command"), + N_("specify a custom command for viewing diffs")), + OPT_BOOL(0, "no-index", &no_index, N_("passed to `diff`")), + OPT_END() + }; + struct child_process child = CHILD_PROCESS_INIT; + + git_config(difftool_config, NULL); + symlinks = has_symlinks; + + argc = parse_options(argc, argv, prefix, builtin_difftool_options, + builtin_difftool_usage, PARSE_OPT_KEEP_UNKNOWN_OPT | + PARSE_OPT_KEEP_DASHDASH); + + if (tool_help) + return print_tool_help(); + + if (!no_index && !startup_info->have_repository) + die(_("difftool requires worktree or --no-index")); + + if (!no_index){ + setup_work_tree(); + setenv(GIT_DIR_ENVIRONMENT, absolute_path(get_git_dir()), 1); + setenv(GIT_WORK_TREE_ENVIRONMENT, absolute_path(get_git_work_tree()), 1); + } else if (dir_diff) + die(_("options '%s' and '%s' cannot be used together"), "--dir-diff", "--no-index"); + + die_for_incompatible_opt3(use_gui_tool == 1, "--gui", + !!difftool_cmd, "--tool", + !!extcmd, "--extcmd"); + + /* + * Explicitly specified GUI option is forwarded to git-mergetool--lib.sh; + * empty or unset means "use the difftool.guiDefault config or default to + * false". + */ + if (use_gui_tool == 1) + setenv("GIT_MERGETOOL_GUI", "true", 1); + else if (use_gui_tool == 0) + setenv("GIT_MERGETOOL_GUI", "false", 1); + + if (difftool_cmd) { + if (*difftool_cmd) + setenv("GIT_DIFF_TOOL", difftool_cmd, 1); + else + die(_("no <tool> given for --tool=<tool>")); + } + + if (extcmd) { + if (*extcmd) + setenv("GIT_DIFFTOOL_EXTCMD", extcmd, 1); + else + die(_("no <cmd> given for --extcmd=<cmd>")); + } + + setenv("GIT_DIFFTOOL_TRUST_EXIT_CODE", + trust_exit_code ? "true" : "false", 1); + + /* + * In directory diff mode, 'git-difftool--helper' is called once + * to compare the a / b directories. In file diff mode, 'git diff' + * will invoke a separate instance of 'git-difftool--helper' for + * each file that changed. + */ + strvec_push(&child.args, "diff"); + if (no_index) + strvec_push(&child.args, "--no-index"); + if (dir_diff) + strvec_pushl(&child.args, "--raw", "--no-abbrev", "-z", NULL); + strvec_pushv(&child.args, argv); + + if (dir_diff) + return run_dir_diff(extcmd, symlinks, prefix, &child); + return run_file_diff(prompt, prefix, &child); +} diff --git a/builtin/fast-export.c b/builtin/fast-export.c new file mode 100644 index 0000000..70aff51 --- /dev/null +++ b/builtin/fast-export.c @@ -0,0 +1,1301 @@ +/* + * "git fast-export" builtin command + * + * Copyright (C) 2007 Johannes E. Schindelin + */ +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "hex.h" +#include "refs.h" +#include "refspec.h" +#include "object-file.h" +#include "object-store-ll.h" +#include "commit.h" +#include "object.h" +#include "tag.h" +#include "diff.h" +#include "diffcore.h" +#include "log-tree.h" +#include "revision.h" +#include "decorate.h" +#include "string-list.h" +#include "utf8.h" +#include "parse-options.h" +#include "quote.h" +#include "remote.h" +#include "blob.h" +#include "commit-slab.h" + +static const char *fast_export_usage[] = { + N_("git fast-export [<rev-list-opts>]"), + NULL +}; + +static int progress; +static enum signed_tag_mode { SIGNED_TAG_ABORT, VERBATIM, WARN, WARN_STRIP, STRIP } signed_tag_mode = SIGNED_TAG_ABORT; +static enum tag_of_filtered_mode { TAG_FILTERING_ABORT, DROP, REWRITE } tag_of_filtered_mode = TAG_FILTERING_ABORT; +static enum reencode_mode { REENCODE_ABORT, REENCODE_YES, REENCODE_NO } reencode_mode = REENCODE_ABORT; +static int fake_missing_tagger; +static int use_done_feature; +static int no_data; +static int full_tree; +static int reference_excluded_commits; +static int show_original_ids; +static int mark_tags; +static struct string_list extra_refs = STRING_LIST_INIT_NODUP; +static struct string_list tag_refs = STRING_LIST_INIT_NODUP; +static struct refspec refspecs = REFSPEC_INIT_FETCH; +static int anonymize; +static struct hashmap anonymized_seeds; +static struct revision_sources revision_sources; + +static int parse_opt_signed_tag_mode(const struct option *opt, + const char *arg, int unset) +{ + enum signed_tag_mode *val = opt->value; + + if (unset || !strcmp(arg, "abort")) + *val = SIGNED_TAG_ABORT; + else if (!strcmp(arg, "verbatim") || !strcmp(arg, "ignore")) + *val = VERBATIM; + else if (!strcmp(arg, "warn")) + *val = WARN; + else if (!strcmp(arg, "warn-strip")) + *val = WARN_STRIP; + else if (!strcmp(arg, "strip")) + *val = STRIP; + else + return error("Unknown signed-tags mode: %s", arg); + return 0; +} + +static int parse_opt_tag_of_filtered_mode(const struct option *opt, + const char *arg, int unset) +{ + enum tag_of_filtered_mode *val = opt->value; + + if (unset || !strcmp(arg, "abort")) + *val = TAG_FILTERING_ABORT; + else if (!strcmp(arg, "drop")) + *val = DROP; + else if (!strcmp(arg, "rewrite")) + *val = REWRITE; + else + return error("Unknown tag-of-filtered mode: %s", arg); + return 0; +} + +static int parse_opt_reencode_mode(const struct option *opt, + const char *arg, int unset) +{ + enum reencode_mode *val = opt->value; + + if (unset) { + *val = REENCODE_ABORT; + return 0; + } + + switch (git_parse_maybe_bool(arg)) { + case 0: + *val = REENCODE_NO; + break; + case 1: + *val = REENCODE_YES; + break; + default: + if (!strcasecmp(arg, "abort")) + *val = REENCODE_ABORT; + else + return error("Unknown reencoding mode: %s", arg); + } + + return 0; +} + +static struct decoration idnums; +static uint32_t last_idnum; +struct anonymized_entry { + struct hashmap_entry hash; + char *anon; + const char orig[FLEX_ARRAY]; +}; + +struct anonymized_entry_key { + struct hashmap_entry hash; + const char *orig; + size_t orig_len; +}; + +static int anonymized_entry_cmp(const void *cmp_data UNUSED, + const struct hashmap_entry *eptr, + const struct hashmap_entry *entry_or_key, + const void *keydata) +{ + const struct anonymized_entry *a, *b; + + a = container_of(eptr, const struct anonymized_entry, hash); + if (keydata) { + const struct anonymized_entry_key *key = keydata; + int equal = !strncmp(a->orig, key->orig, key->orig_len) && + !a->orig[key->orig_len]; + return !equal; + } + + b = container_of(entry_or_key, const struct anonymized_entry, hash); + return strcmp(a->orig, b->orig); +} + +static struct anonymized_entry *add_anonymized_entry(struct hashmap *map, + unsigned hash, + const char *orig, size_t len, + char *anon) +{ + struct anonymized_entry *ret, *old; + + if (!map->cmpfn) + hashmap_init(map, anonymized_entry_cmp, NULL, 0); + + FLEX_ALLOC_MEM(ret, orig, orig, len); + hashmap_entry_init(&ret->hash, hash); + ret->anon = anon; + old = hashmap_put_entry(map, ret, hash); + + if (old) { + free(old->anon); + free(old); + } + + return ret; +} + +/* + * Basically keep a cache of X->Y so that we can repeatedly replace + * the same anonymized string with another. The actual generation + * is farmed out to the generate function. + */ +static const char *anonymize_str(struct hashmap *map, + char *(*generate)(void), + const char *orig, size_t len) +{ + struct anonymized_entry_key key; + struct anonymized_entry *ret; + + hashmap_entry_init(&key.hash, memhash(orig, len)); + key.orig = orig; + key.orig_len = len; + + /* First check if it's a token the user configured manually... */ + ret = hashmap_get_entry(&anonymized_seeds, &key, hash, &key); + + /* ...otherwise check if we've already seen it in this context... */ + if (!ret) + ret = hashmap_get_entry(map, &key, hash, &key); + + /* ...and finally generate a new mapping if necessary */ + if (!ret) + ret = add_anonymized_entry(map, key.hash.hash, + orig, len, generate()); + + return ret->anon; +} + +/* + * We anonymize each component of a path individually, + * so that paths a/b and a/c will share a common root. + * The paths are cached via anonymize_mem so that repeated + * lookups for "a" will yield the same value. + */ +static void anonymize_path(struct strbuf *out, const char *path, + struct hashmap *map, + char *(*generate)(void)) +{ + while (*path) { + const char *end_of_component = strchrnul(path, '/'); + size_t len = end_of_component - path; + const char *c = anonymize_str(map, generate, path, len); + strbuf_addstr(out, c); + path = end_of_component; + if (*path) + strbuf_addch(out, *path++); + } +} + +static inline void *mark_to_ptr(uint32_t mark) +{ + return (void *)(uintptr_t)mark; +} + +static inline uint32_t ptr_to_mark(void * mark) +{ + return (uint32_t)(uintptr_t)mark; +} + +static inline void mark_object(struct object *object, uint32_t mark) +{ + add_decoration(&idnums, object, mark_to_ptr(mark)); +} + +static inline void mark_next_object(struct object *object) +{ + mark_object(object, ++last_idnum); +} + +static int get_object_mark(struct object *object) +{ + void *decoration = lookup_decoration(&idnums, object); + if (!decoration) + return 0; + return ptr_to_mark(decoration); +} + +static struct commit *rewrite_commit(struct commit *p) +{ + for (;;) { + if (p->parents && p->parents->next) + break; + if (p->object.flags & UNINTERESTING) + break; + if (!(p->object.flags & TREESAME)) + break; + if (!p->parents) + return NULL; + p = p->parents->item; + } + return p; +} + +static void show_progress(void) +{ + static int counter = 0; + if (!progress) + return; + if ((++counter % progress) == 0) + printf("progress %d objects\n", counter); +} + +/* + * Ideally we would want some transformation of the blob data here + * that is unreversible, but would still be the same size and have + * the same data relationship to other blobs (so that we get the same + * delta and packing behavior as the original). But the first and last + * requirements there are probably mutually exclusive, so let's take + * the easy way out for now, and just generate arbitrary content. + * + * There's no need to cache this result with anonymize_mem, since + * we already handle blob content caching with marks. + */ +static char *anonymize_blob(unsigned long *size) +{ + static int counter; + struct strbuf out = STRBUF_INIT; + strbuf_addf(&out, "anonymous blob %d", counter++); + *size = out.len; + return strbuf_detach(&out, NULL); +} + +static void export_blob(const struct object_id *oid) +{ + unsigned long size; + enum object_type type; + char *buf; + struct object *object; + int eaten; + + if (no_data) + return; + + if (is_null_oid(oid)) + return; + + object = lookup_object(the_repository, oid); + if (object && object->flags & SHOWN) + return; + + if (anonymize) { + buf = anonymize_blob(&size); + object = (struct object *)lookup_blob(the_repository, oid); + eaten = 0; + } else { + buf = repo_read_object_file(the_repository, oid, &type, &size); + if (!buf) + die("could not read blob %s", oid_to_hex(oid)); + if (check_object_signature(the_repository, oid, buf, size, + type) < 0) + die("oid mismatch in blob %s", oid_to_hex(oid)); + object = parse_object_buffer(the_repository, oid, type, + size, buf, &eaten); + } + + if (!object) + die("Could not read blob %s", oid_to_hex(oid)); + + mark_next_object(object); + + printf("blob\nmark :%"PRIu32"\n", last_idnum); + if (show_original_ids) + printf("original-oid %s\n", oid_to_hex(oid)); + printf("data %"PRIuMAX"\n", (uintmax_t)size); + if (size && fwrite(buf, size, 1, stdout) != 1) + die_errno("could not write blob '%s'", oid_to_hex(oid)); + printf("\n"); + + show_progress(); + + object->flags |= SHOWN; + if (!eaten) + free(buf); +} + +static int depth_first(const void *a_, const void *b_) +{ + const struct diff_filepair *a = *((const struct diff_filepair **)a_); + const struct diff_filepair *b = *((const struct diff_filepair **)b_); + const char *name_a, *name_b; + int len_a, len_b, len; + int cmp; + + name_a = a->one ? a->one->path : a->two->path; + name_b = b->one ? b->one->path : b->two->path; + + len_a = strlen(name_a); + len_b = strlen(name_b); + len = (len_a < len_b) ? len_a : len_b; + + /* strcmp will sort 'd' before 'd/e', we want 'd/e' before 'd' */ + cmp = memcmp(name_a, name_b, len); + if (cmp) + return cmp; + cmp = len_b - len_a; + if (cmp) + return cmp; + /* + * Move 'R'ename entries last so that all references of the file + * appear in the output before it is renamed (e.g., when a file + * was copied and renamed in the same commit). + */ + return (a->status == 'R') - (b->status == 'R'); +} + +static void print_path_1(const char *path) +{ + int need_quote = quote_c_style(path, NULL, NULL, 0); + if (need_quote) + quote_c_style(path, NULL, stdout, 0); + else if (strchr(path, ' ')) + printf("\"%s\"", path); + else + printf("%s", path); +} + +static char *anonymize_path_component(void) +{ + static int counter; + struct strbuf out = STRBUF_INIT; + strbuf_addf(&out, "path%d", counter++); + return strbuf_detach(&out, NULL); +} + +static void print_path(const char *path) +{ + if (!anonymize) + print_path_1(path); + else { + static struct hashmap paths; + static struct strbuf anon = STRBUF_INIT; + + anonymize_path(&anon, path, &paths, anonymize_path_component); + print_path_1(anon.buf); + strbuf_reset(&anon); + } +} + +static char *generate_fake_oid(void) +{ + static uint32_t counter = 1; /* avoid null oid */ + const unsigned hashsz = the_hash_algo->rawsz; + struct object_id oid; + char *hex = xmallocz(GIT_MAX_HEXSZ); + + oidclr(&oid); + put_be32(oid.hash + hashsz - 4, counter++); + return oid_to_hex_r(hex, &oid); +} + +static const char *anonymize_oid(const char *oid_hex) +{ + static struct hashmap objs; + size_t len = strlen(oid_hex); + return anonymize_str(&objs, generate_fake_oid, oid_hex, len); +} + +static void show_filemodify(struct diff_queue_struct *q, + struct diff_options *options UNUSED, void *data) +{ + int i; + struct string_list *changed = data; + + /* + * Handle files below a directory first, in case they are all deleted + * and the directory changes to a file or symlink. + */ + QSORT(q->queue, q->nr, depth_first); + + for (i = 0; i < q->nr; i++) { + struct diff_filespec *ospec = q->queue[i]->one; + struct diff_filespec *spec = q->queue[i]->two; + + switch (q->queue[i]->status) { + case DIFF_STATUS_DELETED: + printf("D "); + print_path(spec->path); + string_list_insert(changed, spec->path); + putchar('\n'); + break; + + case DIFF_STATUS_COPIED: + case DIFF_STATUS_RENAMED: + /* + * If a change in the file corresponding to ospec->path + * has been observed, we cannot trust its contents + * because the diff is calculated based on the prior + * contents, not the current contents. So, declare a + * copy or rename only if there was no change observed. + */ + if (!string_list_has_string(changed, ospec->path)) { + printf("%c ", q->queue[i]->status); + print_path(ospec->path); + putchar(' '); + print_path(spec->path); + string_list_insert(changed, spec->path); + putchar('\n'); + + if (oideq(&ospec->oid, &spec->oid) && + ospec->mode == spec->mode) + break; + } + /* fallthrough */ + + case DIFF_STATUS_TYPE_CHANGED: + case DIFF_STATUS_MODIFIED: + case DIFF_STATUS_ADDED: + /* + * Links refer to objects in another repositories; + * output the SHA-1 verbatim. + */ + if (no_data || S_ISGITLINK(spec->mode)) + printf("M %06o %s ", spec->mode, + anonymize ? + anonymize_oid(oid_to_hex(&spec->oid)) : + oid_to_hex(&spec->oid)); + else { + struct object *object = lookup_object(the_repository, + &spec->oid); + printf("M %06o :%d ", spec->mode, + get_object_mark(object)); + } + print_path(spec->path); + string_list_insert(changed, spec->path); + putchar('\n'); + break; + + default: + die("Unexpected comparison status '%c' for %s, %s", + q->queue[i]->status, + ospec->path ? ospec->path : "none", + spec->path ? spec->path : "none"); + } + } +} + +static const char *find_encoding(const char *begin, const char *end) +{ + const char *needle = "\nencoding "; + char *bol, *eol; + + bol = memmem(begin, end ? end - begin : strlen(begin), + needle, strlen(needle)); + if (!bol) + return NULL; + bol += strlen(needle); + eol = strchrnul(bol, '\n'); + *eol = '\0'; + return bol; +} + +static char *anonymize_ref_component(void) +{ + static int counter; + struct strbuf out = STRBUF_INIT; + strbuf_addf(&out, "ref%d", counter++); + return strbuf_detach(&out, NULL); +} + +static const char *anonymize_refname(const char *refname) +{ + /* + * If any of these prefixes is found, we will leave it intact + * so that tags remain tags and so forth. + */ + static const char *prefixes[] = { + "refs/heads/", + "refs/tags/", + "refs/remotes/", + "refs/" + }; + static struct hashmap refs; + static struct strbuf anon = STRBUF_INIT; + int i; + + strbuf_reset(&anon); + for (i = 0; i < ARRAY_SIZE(prefixes); i++) { + if (skip_prefix(refname, prefixes[i], &refname)) { + strbuf_addstr(&anon, prefixes[i]); + break; + } + } + + anonymize_path(&anon, refname, &refs, anonymize_ref_component); + return anon.buf; +} + +/* + * We do not even bother to cache commit messages, as they are unlikely + * to be repeated verbatim, and it is not that interesting when they are. + */ +static char *anonymize_commit_message(void) +{ + static int counter; + return xstrfmt("subject %d\n\nbody\n", counter++); +} + +static char *anonymize_ident(void) +{ + static int counter; + struct strbuf out = STRBUF_INIT; + strbuf_addf(&out, "User %d <user%d@example.com>", counter, counter); + counter++; + return strbuf_detach(&out, NULL); +} + +/* + * Our strategy here is to anonymize the names and email addresses, + * but keep timestamps intact, as they influence things like traversal + * order (and by themselves should not be too revealing). + */ +static void anonymize_ident_line(const char **beg, const char **end) +{ + static struct hashmap idents; + static struct strbuf buffers[] = { STRBUF_INIT, STRBUF_INIT }; + static unsigned which_buffer; + + struct strbuf *out; + struct ident_split split; + const char *end_of_header; + + out = &buffers[which_buffer++]; + which_buffer %= ARRAY_SIZE(buffers); + strbuf_reset(out); + + /* skip "committer", "author", "tagger", etc */ + end_of_header = strchr(*beg, ' '); + if (!end_of_header) + BUG("malformed line fed to anonymize_ident_line: %.*s", + (int)(*end - *beg), *beg); + end_of_header++; + strbuf_add(out, *beg, end_of_header - *beg); + + if (!split_ident_line(&split, end_of_header, *end - end_of_header) && + split.date_begin) { + const char *ident; + size_t len; + + len = split.mail_end - split.name_begin; + ident = anonymize_str(&idents, anonymize_ident, + split.name_begin, len); + strbuf_addstr(out, ident); + strbuf_addch(out, ' '); + strbuf_add(out, split.date_begin, split.tz_end - split.date_begin); + } else { + strbuf_addstr(out, "Malformed Ident <malformed@example.com> 0 -0000"); + } + + *beg = out->buf; + *end = out->buf + out->len; +} + +static void handle_commit(struct commit *commit, struct rev_info *rev, + struct string_list *paths_of_changed_objects) +{ + int saved_output_format = rev->diffopt.output_format; + const char *commit_buffer; + const char *author, *author_end, *committer, *committer_end; + const char *encoding, *message; + char *reencoded = NULL; + struct commit_list *p; + const char *refname; + int i; + + rev->diffopt.output_format = DIFF_FORMAT_CALLBACK; + + parse_commit_or_die(commit); + commit_buffer = repo_get_commit_buffer(the_repository, commit, NULL); + author = strstr(commit_buffer, "\nauthor "); + if (!author) + die("could not find author in commit %s", + oid_to_hex(&commit->object.oid)); + author++; + author_end = strchrnul(author, '\n'); + committer = strstr(author_end, "\ncommitter "); + if (!committer) + die("could not find committer in commit %s", + oid_to_hex(&commit->object.oid)); + committer++; + committer_end = strchrnul(committer, '\n'); + message = strstr(committer_end, "\n\n"); + encoding = find_encoding(committer_end, message); + if (message) + message += 2; + + if (commit->parents && + (get_object_mark(&commit->parents->item->object) != 0 || + reference_excluded_commits) && + !full_tree) { + parse_commit_or_die(commit->parents->item); + diff_tree_oid(get_commit_tree_oid(commit->parents->item), + get_commit_tree_oid(commit), "", &rev->diffopt); + } + else + diff_root_tree_oid(get_commit_tree_oid(commit), + "", &rev->diffopt); + + /* Export the referenced blobs, and remember the marks. */ + for (i = 0; i < diff_queued_diff.nr; i++) + if (!S_ISGITLINK(diff_queued_diff.queue[i]->two->mode)) + export_blob(&diff_queued_diff.queue[i]->two->oid); + + refname = *revision_sources_at(&revision_sources, commit); + /* + * FIXME: string_list_remove() below for each ref is overall + * O(N^2). Compared to a history walk and diffing trees, this is + * just lost in the noise in practice. However, theoretically a + * repo may have enough refs for this to become slow. + */ + string_list_remove(&extra_refs, refname, 0); + if (anonymize) { + refname = anonymize_refname(refname); + anonymize_ident_line(&committer, &committer_end); + anonymize_ident_line(&author, &author_end); + } + + mark_next_object(&commit->object); + if (anonymize) { + reencoded = anonymize_commit_message(); + } else if (encoding) { + switch(reencode_mode) { + case REENCODE_YES: + reencoded = reencode_string(message, "UTF-8", encoding); + break; + case REENCODE_NO: + break; + case REENCODE_ABORT: + die("Encountered commit-specific encoding %s in commit " + "%s; use --reencode=[yes|no] to handle it", + encoding, oid_to_hex(&commit->object.oid)); + } + } + if (!commit->parents) + printf("reset %s\n", refname); + printf("commit %s\nmark :%"PRIu32"\n", refname, last_idnum); + if (show_original_ids) + printf("original-oid %s\n", oid_to_hex(&commit->object.oid)); + printf("%.*s\n%.*s\n", + (int)(author_end - author), author, + (int)(committer_end - committer), committer); + if (!reencoded && encoding) + printf("encoding %s\n", encoding); + printf("data %u\n%s", + (unsigned)(reencoded + ? strlen(reencoded) : message + ? strlen(message) : 0), + reencoded ? reencoded : message ? message : ""); + free(reencoded); + repo_unuse_commit_buffer(the_repository, commit, commit_buffer); + + for (i = 0, p = commit->parents; p; p = p->next) { + struct object *obj = &p->item->object; + int mark = get_object_mark(obj); + + if (!mark && !reference_excluded_commits) + continue; + if (i == 0) + printf("from "); + else + printf("merge "); + if (mark) + printf(":%d\n", mark); + else + printf("%s\n", + anonymize ? + anonymize_oid(oid_to_hex(&obj->oid)) : + oid_to_hex(&obj->oid)); + i++; + } + + if (full_tree) + printf("deleteall\n"); + log_tree_diff_flush(rev); + string_list_clear(paths_of_changed_objects, 0); + rev->diffopt.output_format = saved_output_format; + + printf("\n"); + + show_progress(); +} + +static char *anonymize_tag(void) +{ + static int counter; + struct strbuf out = STRBUF_INIT; + strbuf_addf(&out, "tag message %d", counter++); + return strbuf_detach(&out, NULL); +} + + +static void handle_tag(const char *name, struct tag *tag) +{ + unsigned long size; + enum object_type type; + char *buf; + const char *tagger, *tagger_end, *message; + size_t message_size = 0; + struct object *tagged; + int tagged_mark; + struct commit *p; + + /* Trees have no identifier in fast-export output, thus we have no way + * to output tags of trees, tags of tags of trees, etc. Simply omit + * such tags. + */ + tagged = tag->tagged; + while (tagged->type == OBJ_TAG) { + tagged = ((struct tag *)tagged)->tagged; + } + if (tagged->type == OBJ_TREE) { + warning("Omitting tag %s,\nsince tags of trees (or tags of tags of trees, etc.) are not supported.", + oid_to_hex(&tag->object.oid)); + return; + } + + buf = repo_read_object_file(the_repository, &tag->object.oid, &type, + &size); + if (!buf) + die("could not read tag %s", oid_to_hex(&tag->object.oid)); + message = memmem(buf, size, "\n\n", 2); + if (message) { + message += 2; + message_size = strlen(message); + } + tagger = memmem(buf, message ? message - buf : size, "\ntagger ", 8); + if (!tagger) { + if (fake_missing_tagger) + tagger = "tagger Unspecified Tagger " + "<unspecified-tagger> 0 +0000"; + else + tagger = ""; + tagger_end = tagger + strlen(tagger); + } else { + tagger++; + tagger_end = strchrnul(tagger, '\n'); + if (anonymize) + anonymize_ident_line(&tagger, &tagger_end); + } + + if (anonymize) { + name = anonymize_refname(name); + if (message) { + static struct hashmap tags; + message = anonymize_str(&tags, anonymize_tag, + message, message_size); + message_size = strlen(message); + } + } + + /* handle signed tags */ + if (message) { + const char *signature = strstr(message, + "\n-----BEGIN PGP SIGNATURE-----\n"); + if (signature) + switch(signed_tag_mode) { + case SIGNED_TAG_ABORT: + die("encountered signed tag %s; use " + "--signed-tags=<mode> to handle it", + oid_to_hex(&tag->object.oid)); + case WARN: + warning("exporting signed tag %s", + oid_to_hex(&tag->object.oid)); + /* fallthru */ + case VERBATIM: + break; + case WARN_STRIP: + warning("stripping signature from tag %s", + oid_to_hex(&tag->object.oid)); + /* fallthru */ + case STRIP: + message_size = signature + 1 - message; + break; + } + } + + /* handle tag->tagged having been filtered out due to paths specified */ + tagged = tag->tagged; + tagged_mark = get_object_mark(tagged); + if (!tagged_mark) { + switch(tag_of_filtered_mode) { + case TAG_FILTERING_ABORT: + die("tag %s tags unexported object; use " + "--tag-of-filtered-object=<mode> to handle it", + oid_to_hex(&tag->object.oid)); + case DROP: + /* Ignore this tag altogether */ + free(buf); + return; + case REWRITE: + if (tagged->type == OBJ_TAG && !mark_tags) { + die(_("Error: Cannot export nested tags unless --mark-tags is specified.")); + } else if (tagged->type == OBJ_COMMIT) { + p = rewrite_commit((struct commit *)tagged); + if (!p) { + printf("reset %s\nfrom %s\n\n", + name, oid_to_hex(null_oid())); + free(buf); + return; + } + tagged_mark = get_object_mark(&p->object); + } else { + /* tagged->type is either OBJ_BLOB or OBJ_TAG */ + tagged_mark = get_object_mark(tagged); + } + } + } + + if (tagged->type == OBJ_TAG) { + printf("reset %s\nfrom %s\n\n", + name, oid_to_hex(null_oid())); + } + skip_prefix(name, "refs/tags/", &name); + printf("tag %s\n", name); + if (mark_tags) { + mark_next_object(&tag->object); + printf("mark :%"PRIu32"\n", last_idnum); + } + if (tagged_mark) + printf("from :%d\n", tagged_mark); + else + printf("from %s\n", oid_to_hex(&tagged->oid)); + + if (show_original_ids) + printf("original-oid %s\n", oid_to_hex(&tag->object.oid)); + printf("%.*s%sdata %d\n%.*s\n", + (int)(tagger_end - tagger), tagger, + tagger == tagger_end ? "" : "\n", + (int)message_size, (int)message_size, message ? message : ""); + free(buf); +} + +static struct commit *get_commit(struct rev_cmdline_entry *e, char *full_name) +{ + switch (e->item->type) { + case OBJ_COMMIT: + return (struct commit *)e->item; + case OBJ_TAG: { + struct tag *tag = (struct tag *)e->item; + + /* handle nested tags */ + while (tag && tag->object.type == OBJ_TAG) { + parse_object(the_repository, &tag->object.oid); + string_list_append(&tag_refs, full_name)->util = tag; + tag = (struct tag *)tag->tagged; + } + if (!tag) + die("Tag %s points nowhere?", e->name); + return (struct commit *)tag; + } + default: + return NULL; + } +} + +static void get_tags_and_duplicates(struct rev_cmdline_info *info) +{ + int i; + + for (i = 0; i < info->nr; i++) { + struct rev_cmdline_entry *e = info->rev + i; + struct object_id oid; + struct commit *commit; + char *full_name; + + if (e->flags & UNINTERESTING) + continue; + + if (repo_dwim_ref(the_repository, e->name, strlen(e->name), + &oid, &full_name, 0) != 1) + continue; + + if (refspecs.nr) { + char *private; + private = apply_refspecs(&refspecs, full_name); + if (private) { + free(full_name); + full_name = private; + } + } + + commit = get_commit(e, full_name); + if (!commit) { + warning("%s: Unexpected object of type %s, skipping.", + e->name, + type_name(e->item->type)); + continue; + } + + switch(commit->object.type) { + case OBJ_COMMIT: + break; + case OBJ_BLOB: + export_blob(&commit->object.oid); + continue; + default: /* OBJ_TAG (nested tags) is already handled */ + warning("Tag points to object of unexpected type %s, skipping.", + type_name(commit->object.type)); + continue; + } + + /* + * Make sure this ref gets properly updated eventually, whether + * through a commit or manually at the end. + */ + if (e->item->type != OBJ_TAG) + string_list_append(&extra_refs, full_name)->util = commit; + + if (!*revision_sources_at(&revision_sources, commit)) + *revision_sources_at(&revision_sources, commit) = full_name; + } + + string_list_sort(&extra_refs); + string_list_remove_duplicates(&extra_refs, 0); +} + +static void handle_tags_and_duplicates(struct string_list *extras) +{ + struct commit *commit; + int i; + + for (i = extras->nr - 1; i >= 0; i--) { + const char *name = extras->items[i].string; + struct object *object = extras->items[i].util; + int mark; + + switch (object->type) { + case OBJ_TAG: + handle_tag(name, (struct tag *)object); + break; + case OBJ_COMMIT: + if (anonymize) + name = anonymize_refname(name); + /* create refs pointing to already seen commits */ + commit = rewrite_commit((struct commit *)object); + if (!commit) { + /* + * Neither this object nor any of its + * ancestors touch any relevant paths, so + * it has been filtered to nothing. Delete + * it. + */ + printf("reset %s\nfrom %s\n\n", + name, oid_to_hex(null_oid())); + continue; + } + + mark = get_object_mark(&commit->object); + if (!mark) { + /* + * Getting here means we have a commit which + * was excluded by a negative refspec (e.g. + * fast-export ^HEAD HEAD). If we are + * referencing excluded commits, set the ref + * to the exact commit. Otherwise, the user + * wants the branch exported but every commit + * in its history to be deleted, which basically + * just means deletion of the ref. + */ + if (!reference_excluded_commits) { + /* delete the ref */ + printf("reset %s\nfrom %s\n\n", + name, oid_to_hex(null_oid())); + continue; + } + /* set ref to commit using oid, not mark */ + printf("reset %s\nfrom %s\n\n", name, + oid_to_hex(&commit->object.oid)); + continue; + } + + printf("reset %s\nfrom :%d\n\n", name, mark + ); + show_progress(); + break; + } + } +} + +static void export_marks(char *file) +{ + unsigned int i; + uint32_t mark; + struct decoration_entry *deco = idnums.entries; + FILE *f; + int e = 0; + + f = fopen_for_writing(file); + if (!f) + die_errno("Unable to open marks file %s for writing.", file); + + for (i = 0; i < idnums.size; i++) { + if (deco->base && deco->base->type == 1) { + mark = ptr_to_mark(deco->decoration); + if (fprintf(f, ":%"PRIu32" %s\n", mark, + oid_to_hex(&deco->base->oid)) < 0) { + e = 1; + break; + } + } + deco++; + } + + e |= ferror(f); + e |= fclose(f); + if (e) + error("Unable to write marks file %s.", file); +} + +static void import_marks(char *input_file, int check_exists) +{ + char line[512]; + FILE *f; + struct stat sb; + + if (check_exists && stat(input_file, &sb)) + return; + + f = xfopen(input_file, "r"); + while (fgets(line, sizeof(line), f)) { + uint32_t mark; + char *line_end, *mark_end; + struct object_id oid; + struct object *object; + struct commit *commit; + enum object_type type; + + line_end = strchr(line, '\n'); + if (line[0] != ':' || !line_end) + die("corrupt mark line: %s", line); + *line_end = '\0'; + + mark = strtoumax(line + 1, &mark_end, 10); + if (!mark || mark_end == line + 1 + || *mark_end != ' ' || get_oid_hex(mark_end + 1, &oid)) + die("corrupt mark line: %s", line); + + if (last_idnum < mark) + last_idnum = mark; + + type = oid_object_info(the_repository, &oid, NULL); + if (type < 0) + die("object not found: %s", oid_to_hex(&oid)); + + if (type != OBJ_COMMIT) + /* only commits */ + continue; + + commit = lookup_commit(the_repository, &oid); + if (!commit) + die("not a commit? can't happen: %s", oid_to_hex(&oid)); + + object = &commit->object; + + if (object->flags & SHOWN) + error("Object %s already has a mark", oid_to_hex(&oid)); + + mark_object(object, mark); + + object->flags |= SHOWN; + } + fclose(f); +} + +static void handle_deletes(void) +{ + int i; + for (i = 0; i < refspecs.nr; i++) { + struct refspec_item *refspec = &refspecs.items[i]; + if (*refspec->src) + continue; + + printf("reset %s\nfrom %s\n\n", + refspec->dst, oid_to_hex(null_oid())); + } +} + +static int parse_opt_anonymize_map(const struct option *opt, + const char *arg, int unset) +{ + struct hashmap *map = opt->value; + const char *delim, *value; + size_t keylen; + + BUG_ON_OPT_NEG(unset); + + delim = strchr(arg, ':'); + if (delim) { + keylen = delim - arg; + value = delim + 1; + } else { + keylen = strlen(arg); + value = arg; + } + + if (!keylen || !*value) + return error(_("--anonymize-map token cannot be empty")); + + add_anonymized_entry(map, memhash(arg, keylen), arg, keylen, + xstrdup(value)); + + return 0; +} + +int cmd_fast_export(int argc, const char **argv, const char *prefix) +{ + struct rev_info revs; + struct commit *commit; + char *export_filename = NULL, + *import_filename = NULL, + *import_filename_if_exists = NULL; + uint32_t lastimportid; + struct string_list refspecs_list = STRING_LIST_INIT_NODUP; + struct string_list paths_of_changed_objects = STRING_LIST_INIT_DUP; + struct option options[] = { + OPT_INTEGER(0, "progress", &progress, + N_("show progress after <n> objects")), + OPT_CALLBACK(0, "signed-tags", &signed_tag_mode, N_("mode"), + N_("select handling of signed tags"), + parse_opt_signed_tag_mode), + OPT_CALLBACK(0, "tag-of-filtered-object", &tag_of_filtered_mode, N_("mode"), + N_("select handling of tags that tag filtered objects"), + parse_opt_tag_of_filtered_mode), + OPT_CALLBACK(0, "reencode", &reencode_mode, N_("mode"), + N_("select handling of commit messages in an alternate encoding"), + parse_opt_reencode_mode), + OPT_STRING(0, "export-marks", &export_filename, N_("file"), + N_("dump marks to this file")), + OPT_STRING(0, "import-marks", &import_filename, N_("file"), + N_("import marks from this file")), + OPT_STRING(0, "import-marks-if-exists", + &import_filename_if_exists, + N_("file"), + N_("import marks from this file if it exists")), + OPT_BOOL(0, "fake-missing-tagger", &fake_missing_tagger, + N_("fake a tagger when tags lack one")), + OPT_BOOL(0, "full-tree", &full_tree, + N_("output full tree for each commit")), + OPT_BOOL(0, "use-done-feature", &use_done_feature, + N_("use the done feature to terminate the stream")), + OPT_BOOL(0, "no-data", &no_data, N_("skip output of blob data")), + OPT_STRING_LIST(0, "refspec", &refspecs_list, N_("refspec"), + N_("apply refspec to exported refs")), + OPT_BOOL(0, "anonymize", &anonymize, N_("anonymize output")), + OPT_CALLBACK_F(0, "anonymize-map", &anonymized_seeds, N_("from:to"), + N_("convert <from> to <to> in anonymized output"), + PARSE_OPT_NONEG, parse_opt_anonymize_map), + OPT_BOOL(0, "reference-excluded-parents", + &reference_excluded_commits, N_("reference parents which are not in fast-export stream by object id")), + OPT_BOOL(0, "show-original-ids", &show_original_ids, + N_("show original object ids of blobs/commits")), + OPT_BOOL(0, "mark-tags", &mark_tags, + N_("label tags with mark ids")), + + OPT_END() + }; + + if (argc == 1) + usage_with_options (fast_export_usage, options); + + /* we handle encodings */ + git_config(git_default_config, NULL); + + repo_init_revisions(the_repository, &revs, prefix); + init_revision_sources(&revision_sources); + revs.topo_order = 1; + revs.sources = &revision_sources; + revs.rewrite_parents = 1; + argc = parse_options(argc, argv, prefix, options, fast_export_usage, + PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT); + argc = setup_revisions(argc, argv, &revs, NULL); + if (argc > 1) + usage_with_options (fast_export_usage, options); + + if (anonymized_seeds.cmpfn && !anonymize) + die(_("the option '%s' requires '%s'"), "--anonymize-map", "--anonymize"); + + if (refspecs_list.nr) { + int i; + + for (i = 0; i < refspecs_list.nr; i++) + refspec_append(&refspecs, refspecs_list.items[i].string); + + string_list_clear(&refspecs_list, 1); + } + + if (use_done_feature) + printf("feature done\n"); + + if (import_filename && import_filename_if_exists) + die(_("options '%s' and '%s' cannot be used together"), "--import-marks", "--import-marks-if-exists"); + if (import_filename) + import_marks(import_filename, 0); + else if (import_filename_if_exists) + import_marks(import_filename_if_exists, 1); + lastimportid = last_idnum; + + if (import_filename && revs.prune_data.nr) + full_tree = 1; + + get_tags_and_duplicates(&revs.cmdline); + + if (prepare_revision_walk(&revs)) + die("revision walk setup failed"); + + revs.reverse = 1; + revs.diffopt.format_callback = show_filemodify; + revs.diffopt.format_callback_data = &paths_of_changed_objects; + revs.diffopt.flags.recursive = 1; + revs.diffopt.no_free = 1; + while ((commit = get_revision(&revs))) + handle_commit(commit, &revs, &paths_of_changed_objects); + + handle_tags_and_duplicates(&extra_refs); + handle_tags_and_duplicates(&tag_refs); + handle_deletes(); + + if (export_filename && lastimportid != last_idnum) + export_marks(export_filename); + + if (use_done_feature) + printf("done\n"); + + refspec_clear(&refspecs); + release_revisions(&revs); + + return 0; +} diff --git a/builtin/fast-import.c b/builtin/fast-import.c new file mode 100644 index 0000000..444f41c --- /dev/null +++ b/builtin/fast-import.c @@ -0,0 +1,3659 @@ +#include "builtin.h" +#include "abspath.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "repository.h" +#include "config.h" +#include "lockfile.h" +#include "object.h" +#include "blob.h" +#include "tree.h" +#include "commit.h" +#include "delta.h" +#include "pack.h" +#include "path.h" +#include "refs.h" +#include "csum-file.h" +#include "quote.h" +#include "dir.h" +#include "run-command.h" +#include "packfile.h" +#include "object-file.h" +#include "object-name.h" +#include "object-store-ll.h" +#include "mem-pool.h" +#include "commit-reach.h" +#include "khash.h" +#include "date.h" + +#define PACK_ID_BITS 16 +#define MAX_PACK_ID ((1<<PACK_ID_BITS)-1) +#define DEPTH_BITS 13 +#define MAX_DEPTH ((1<<DEPTH_BITS)-1) + +/* + * We abuse the setuid bit on directories to mean "do not delta". + */ +#define NO_DELTA S_ISUID + +/* + * The amount of additional space required in order to write an object into the + * current pack. This is the hash lengths at the end of the pack, plus the + * length of one object ID. + */ +#define PACK_SIZE_THRESHOLD (the_hash_algo->rawsz * 3) + +struct object_entry { + struct pack_idx_entry idx; + struct hashmap_entry ent; + uint32_t type : TYPE_BITS, + pack_id : PACK_ID_BITS, + depth : DEPTH_BITS; +}; + +static int object_entry_hashcmp(const void *map_data UNUSED, + const struct hashmap_entry *eptr, + const struct hashmap_entry *entry_or_key, + const void *keydata) +{ + const struct object_id *oid = keydata; + const struct object_entry *e1, *e2; + + e1 = container_of(eptr, const struct object_entry, ent); + if (oid) + return oidcmp(&e1->idx.oid, oid); + + e2 = container_of(entry_or_key, const struct object_entry, ent); + return oidcmp(&e1->idx.oid, &e2->idx.oid); +} + +struct object_entry_pool { + struct object_entry_pool *next_pool; + struct object_entry *next_free; + struct object_entry *end; + struct object_entry entries[FLEX_ARRAY]; /* more */ +}; + +struct mark_set { + union { + struct object_id *oids[1024]; + struct object_entry *marked[1024]; + struct mark_set *sets[1024]; + } data; + unsigned int shift; +}; + +struct last_object { + struct strbuf data; + off_t offset; + unsigned int depth; + unsigned no_swap : 1; +}; + +struct atom_str { + struct atom_str *next_atom; + unsigned short str_len; + char str_dat[FLEX_ARRAY]; /* more */ +}; + +struct tree_content; +struct tree_entry { + struct tree_content *tree; + struct atom_str *name; + struct tree_entry_ms { + uint16_t mode; + struct object_id oid; + } versions[2]; +}; + +struct tree_content { + unsigned int entry_capacity; /* must match avail_tree_content */ + unsigned int entry_count; + unsigned int delta_depth; + struct tree_entry *entries[FLEX_ARRAY]; /* more */ +}; + +struct avail_tree_content { + unsigned int entry_capacity; /* must match tree_content */ + struct avail_tree_content *next_avail; +}; + +struct branch { + struct branch *table_next_branch; + struct branch *active_next_branch; + const char *name; + struct tree_entry branch_tree; + uintmax_t last_commit; + uintmax_t num_notes; + unsigned active : 1; + unsigned delete : 1; + unsigned pack_id : PACK_ID_BITS; + struct object_id oid; +}; + +struct tag { + struct tag *next_tag; + const char *name; + unsigned int pack_id; + struct object_id oid; +}; + +struct hash_list { + struct hash_list *next; + struct object_id oid; +}; + +typedef enum { + WHENSPEC_RAW = 1, + WHENSPEC_RAW_PERMISSIVE, + WHENSPEC_RFC2822, + WHENSPEC_NOW +} whenspec_type; + +struct recent_command { + struct recent_command *prev; + struct recent_command *next; + char *buf; +}; + +typedef void (*mark_set_inserter_t)(struct mark_set **s, struct object_id *oid, uintmax_t mark); +typedef void (*each_mark_fn_t)(uintmax_t mark, void *obj, void *cbp); + +/* Configured limits on output */ +static unsigned long max_depth = 50; +static off_t max_packsize; +static int unpack_limit = 100; +static int force_update; + +/* Stats and misc. counters */ +static uintmax_t alloc_count; +static uintmax_t marks_set_count; +static uintmax_t object_count_by_type[1 << TYPE_BITS]; +static uintmax_t duplicate_count_by_type[1 << TYPE_BITS]; +static uintmax_t delta_count_by_type[1 << TYPE_BITS]; +static uintmax_t delta_count_attempts_by_type[1 << TYPE_BITS]; +static unsigned long object_count; +static unsigned long branch_count; +static unsigned long branch_load_count; +static int failure; +static FILE *pack_edges; +static unsigned int show_stats = 1; +static int global_argc; +static const char **global_argv; +static const char *global_prefix; + +/* Memory pools */ +static struct mem_pool fi_mem_pool = { + .block_alloc = 2*1024*1024 - sizeof(struct mp_block), +}; + +/* Atom management */ +static unsigned int atom_table_sz = 4451; +static unsigned int atom_cnt; +static struct atom_str **atom_table; + +/* The .pack file being generated */ +static struct pack_idx_option pack_idx_opts; +static unsigned int pack_id; +static struct hashfile *pack_file; +static struct packed_git *pack_data; +static struct packed_git **all_packs; +static off_t pack_size; + +/* Table of objects we've written. */ +static unsigned int object_entry_alloc = 5000; +static struct object_entry_pool *blocks; +static struct hashmap object_table; +static struct mark_set *marks; +static const char *export_marks_file; +static const char *import_marks_file; +static int import_marks_file_from_stream; +static int import_marks_file_ignore_missing; +static int import_marks_file_done; +static int relative_marks_paths; + +/* Our last blob */ +static struct last_object last_blob = { + .data = STRBUF_INIT, + }; + +/* Tree management */ +static unsigned int tree_entry_alloc = 1000; +static void *avail_tree_entry; +static unsigned int avail_tree_table_sz = 100; +static struct avail_tree_content **avail_tree_table; +static size_t tree_entry_allocd; +static struct strbuf old_tree = STRBUF_INIT; +static struct strbuf new_tree = STRBUF_INIT; + +/* Branch data */ +static unsigned long max_active_branches = 5; +static unsigned long cur_active_branches; +static unsigned long branch_table_sz = 1039; +static struct branch **branch_table; +static struct branch *active_branches; + +/* Tag data */ +static struct tag *first_tag; +static struct tag *last_tag; + +/* Input stream parsing */ +static whenspec_type whenspec = WHENSPEC_RAW; +static struct strbuf command_buf = STRBUF_INIT; +static int unread_command_buf; +static struct recent_command cmd_hist = { + .prev = &cmd_hist, + .next = &cmd_hist, +}; +static struct recent_command *cmd_tail = &cmd_hist; +static struct recent_command *rc_free; +static unsigned int cmd_save = 100; +static uintmax_t next_mark; +static struct strbuf new_data = STRBUF_INIT; +static int seen_data_command; +static int require_explicit_termination; +static int allow_unsafe_features; + +/* Signal handling */ +static volatile sig_atomic_t checkpoint_requested; + +/* Submodule marks */ +static struct string_list sub_marks_from = STRING_LIST_INIT_DUP; +static struct string_list sub_marks_to = STRING_LIST_INIT_DUP; +static kh_oid_map_t *sub_oid_map; + +/* Where to write output of cat-blob commands */ +static int cat_blob_fd = STDOUT_FILENO; + +static void parse_argv(void); +static void parse_get_mark(const char *p); +static void parse_cat_blob(const char *p); +static void parse_ls(const char *p, struct branch *b); + +static void for_each_mark(struct mark_set *m, uintmax_t base, each_mark_fn_t callback, void *p) +{ + uintmax_t k; + if (m->shift) { + for (k = 0; k < 1024; k++) { + if (m->data.sets[k]) + for_each_mark(m->data.sets[k], base + (k << m->shift), callback, p); + } + } else { + for (k = 0; k < 1024; k++) { + if (m->data.marked[k]) + callback(base + k, m->data.marked[k], p); + } + } +} + +static void dump_marks_fn(uintmax_t mark, void *object, void *cbp) { + struct object_entry *e = object; + FILE *f = cbp; + + fprintf(f, ":%" PRIuMAX " %s\n", mark, oid_to_hex(&e->idx.oid)); +} + +static void write_branch_report(FILE *rpt, struct branch *b) +{ + fprintf(rpt, "%s:\n", b->name); + + fprintf(rpt, " status :"); + if (b->active) + fputs(" active", rpt); + if (b->branch_tree.tree) + fputs(" loaded", rpt); + if (is_null_oid(&b->branch_tree.versions[1].oid)) + fputs(" dirty", rpt); + fputc('\n', rpt); + + fprintf(rpt, " tip commit : %s\n", oid_to_hex(&b->oid)); + fprintf(rpt, " old tree : %s\n", + oid_to_hex(&b->branch_tree.versions[0].oid)); + fprintf(rpt, " cur tree : %s\n", + oid_to_hex(&b->branch_tree.versions[1].oid)); + fprintf(rpt, " commit clock: %" PRIuMAX "\n", b->last_commit); + + fputs(" last pack : ", rpt); + if (b->pack_id < MAX_PACK_ID) + fprintf(rpt, "%u", b->pack_id); + fputc('\n', rpt); + + fputc('\n', rpt); +} + +static void write_crash_report(const char *err) +{ + char *loc = git_pathdup("fast_import_crash_%"PRIuMAX, (uintmax_t) getpid()); + FILE *rpt = fopen(loc, "w"); + struct branch *b; + unsigned long lu; + struct recent_command *rc; + + if (!rpt) { + error_errno("can't write crash report %s", loc); + free(loc); + return; + } + + fprintf(stderr, "fast-import: dumping crash report to %s\n", loc); + + fprintf(rpt, "fast-import crash report:\n"); + fprintf(rpt, " fast-import process: %"PRIuMAX"\n", (uintmax_t) getpid()); + fprintf(rpt, " parent process : %"PRIuMAX"\n", (uintmax_t) getppid()); + fprintf(rpt, " at %s\n", show_date(time(NULL), 0, DATE_MODE(ISO8601))); + fputc('\n', rpt); + + fputs("fatal: ", rpt); + fputs(err, rpt); + fputc('\n', rpt); + + fputc('\n', rpt); + fputs("Most Recent Commands Before Crash\n", rpt); + fputs("---------------------------------\n", rpt); + for (rc = cmd_hist.next; rc != &cmd_hist; rc = rc->next) { + if (rc->next == &cmd_hist) + fputs("* ", rpt); + else + fputs(" ", rpt); + fputs(rc->buf, rpt); + fputc('\n', rpt); + } + + fputc('\n', rpt); + fputs("Active Branch LRU\n", rpt); + fputs("-----------------\n", rpt); + fprintf(rpt, " active_branches = %lu cur, %lu max\n", + cur_active_branches, + max_active_branches); + fputc('\n', rpt); + fputs(" pos clock name\n", rpt); + fputs(" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n", rpt); + for (b = active_branches, lu = 0; b; b = b->active_next_branch) + fprintf(rpt, " %2lu) %6" PRIuMAX" %s\n", + ++lu, b->last_commit, b->name); + + fputc('\n', rpt); + fputs("Inactive Branches\n", rpt); + fputs("-----------------\n", rpt); + for (lu = 0; lu < branch_table_sz; lu++) { + for (b = branch_table[lu]; b; b = b->table_next_branch) + write_branch_report(rpt, b); + } + + if (first_tag) { + struct tag *tg; + fputc('\n', rpt); + fputs("Annotated Tags\n", rpt); + fputs("--------------\n", rpt); + for (tg = first_tag; tg; tg = tg->next_tag) { + fputs(oid_to_hex(&tg->oid), rpt); + fputc(' ', rpt); + fputs(tg->name, rpt); + fputc('\n', rpt); + } + } + + fputc('\n', rpt); + fputs("Marks\n", rpt); + fputs("-----\n", rpt); + if (export_marks_file) + fprintf(rpt, " exported to %s\n", export_marks_file); + else + for_each_mark(marks, 0, dump_marks_fn, rpt); + + fputc('\n', rpt); + fputs("-------------------\n", rpt); + fputs("END OF CRASH REPORT\n", rpt); + fclose(rpt); + free(loc); +} + +static void end_packfile(void); +static void unkeep_all_packs(void); +static void dump_marks(void); + +static NORETURN void die_nicely(const char *err, va_list params) +{ + va_list cp; + static int zombie; + report_fn die_message_fn = get_die_message_routine(); + + va_copy(cp, params); + die_message_fn(err, params); + + if (!zombie) { + char message[2 * PATH_MAX]; + + zombie = 1; + vsnprintf(message, sizeof(message), err, cp); + write_crash_report(message); + end_packfile(); + unkeep_all_packs(); + dump_marks(); + } + exit(128); +} + +#ifndef SIGUSR1 /* Windows, for example */ + +static void set_checkpoint_signal(void) +{ +} + +#else + +static void checkpoint_signal(int signo UNUSED) +{ + checkpoint_requested = 1; +} + +static void set_checkpoint_signal(void) +{ + struct sigaction sa; + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = checkpoint_signal; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sigaction(SIGUSR1, &sa, NULL); +} + +#endif + +static void alloc_objects(unsigned int cnt) +{ + struct object_entry_pool *b; + + b = xmalloc(sizeof(struct object_entry_pool) + + cnt * sizeof(struct object_entry)); + b->next_pool = blocks; + b->next_free = b->entries; + b->end = b->entries + cnt; + blocks = b; + alloc_count += cnt; +} + +static struct object_entry *new_object(struct object_id *oid) +{ + struct object_entry *e; + + if (blocks->next_free == blocks->end) + alloc_objects(object_entry_alloc); + + e = blocks->next_free++; + oidcpy(&e->idx.oid, oid); + return e; +} + +static struct object_entry *find_object(struct object_id *oid) +{ + return hashmap_get_entry_from_hash(&object_table, oidhash(oid), oid, + struct object_entry, ent); +} + +static struct object_entry *insert_object(struct object_id *oid) +{ + struct object_entry *e; + unsigned int hash = oidhash(oid); + + e = hashmap_get_entry_from_hash(&object_table, hash, oid, + struct object_entry, ent); + if (!e) { + e = new_object(oid); + e->idx.offset = 0; + hashmap_entry_init(&e->ent, hash); + hashmap_add(&object_table, &e->ent); + } + + return e; +} + +static void invalidate_pack_id(unsigned int id) +{ + unsigned long lu; + struct tag *t; + struct hashmap_iter iter; + struct object_entry *e; + + hashmap_for_each_entry(&object_table, &iter, e, ent) { + if (e->pack_id == id) + e->pack_id = MAX_PACK_ID; + } + + for (lu = 0; lu < branch_table_sz; lu++) { + struct branch *b; + + for (b = branch_table[lu]; b; b = b->table_next_branch) + if (b->pack_id == id) + b->pack_id = MAX_PACK_ID; + } + + for (t = first_tag; t; t = t->next_tag) + if (t->pack_id == id) + t->pack_id = MAX_PACK_ID; +} + +static unsigned int hc_str(const char *s, size_t len) +{ + unsigned int r = 0; + while (len-- > 0) + r = r * 31 + *s++; + return r; +} + +static void insert_mark(struct mark_set **top, uintmax_t idnum, struct object_entry *oe) +{ + struct mark_set *s = *top; + + while ((idnum >> s->shift) >= 1024) { + s = mem_pool_calloc(&fi_mem_pool, 1, sizeof(struct mark_set)); + s->shift = (*top)->shift + 10; + s->data.sets[0] = *top; + *top = s; + } + while (s->shift) { + uintmax_t i = idnum >> s->shift; + idnum -= i << s->shift; + if (!s->data.sets[i]) { + s->data.sets[i] = mem_pool_calloc(&fi_mem_pool, 1, sizeof(struct mark_set)); + s->data.sets[i]->shift = s->shift - 10; + } + s = s->data.sets[i]; + } + if (!s->data.marked[idnum]) + marks_set_count++; + s->data.marked[idnum] = oe; +} + +static void *find_mark(struct mark_set *s, uintmax_t idnum) +{ + uintmax_t orig_idnum = idnum; + struct object_entry *oe = NULL; + if ((idnum >> s->shift) < 1024) { + while (s && s->shift) { + uintmax_t i = idnum >> s->shift; + idnum -= i << s->shift; + s = s->data.sets[i]; + } + if (s) + oe = s->data.marked[idnum]; + } + if (!oe) + die("mark :%" PRIuMAX " not declared", orig_idnum); + return oe; +} + +static struct atom_str *to_atom(const char *s, unsigned short len) +{ + unsigned int hc = hc_str(s, len) % atom_table_sz; + struct atom_str *c; + + for (c = atom_table[hc]; c; c = c->next_atom) + if (c->str_len == len && !strncmp(s, c->str_dat, len)) + return c; + + c = mem_pool_alloc(&fi_mem_pool, sizeof(struct atom_str) + len + 1); + c->str_len = len; + memcpy(c->str_dat, s, len); + c->str_dat[len] = 0; + c->next_atom = atom_table[hc]; + atom_table[hc] = c; + atom_cnt++; + return c; +} + +static struct branch *lookup_branch(const char *name) +{ + unsigned int hc = hc_str(name, strlen(name)) % branch_table_sz; + struct branch *b; + + for (b = branch_table[hc]; b; b = b->table_next_branch) + if (!strcmp(name, b->name)) + return b; + return NULL; +} + +static struct branch *new_branch(const char *name) +{ + unsigned int hc = hc_str(name, strlen(name)) % branch_table_sz; + struct branch *b = lookup_branch(name); + + if (b) + die("Invalid attempt to create duplicate branch: %s", name); + if (check_refname_format(name, REFNAME_ALLOW_ONELEVEL)) + die("Branch name doesn't conform to GIT standards: %s", name); + + b = mem_pool_calloc(&fi_mem_pool, 1, sizeof(struct branch)); + b->name = mem_pool_strdup(&fi_mem_pool, name); + b->table_next_branch = branch_table[hc]; + b->branch_tree.versions[0].mode = S_IFDIR; + b->branch_tree.versions[1].mode = S_IFDIR; + b->num_notes = 0; + b->active = 0; + b->pack_id = MAX_PACK_ID; + branch_table[hc] = b; + branch_count++; + return b; +} + +static unsigned int hc_entries(unsigned int cnt) +{ + cnt = cnt & 7 ? (cnt / 8) + 1 : cnt / 8; + return cnt < avail_tree_table_sz ? cnt : avail_tree_table_sz - 1; +} + +static struct tree_content *new_tree_content(unsigned int cnt) +{ + struct avail_tree_content *f, *l = NULL; + struct tree_content *t; + unsigned int hc = hc_entries(cnt); + + for (f = avail_tree_table[hc]; f; l = f, f = f->next_avail) + if (f->entry_capacity >= cnt) + break; + + if (f) { + if (l) + l->next_avail = f->next_avail; + else + avail_tree_table[hc] = f->next_avail; + } else { + cnt = cnt & 7 ? ((cnt / 8) + 1) * 8 : cnt; + f = mem_pool_alloc(&fi_mem_pool, sizeof(*t) + sizeof(t->entries[0]) * cnt); + f->entry_capacity = cnt; + } + + t = (struct tree_content*)f; + t->entry_count = 0; + t->delta_depth = 0; + return t; +} + +static void release_tree_entry(struct tree_entry *e); +static void release_tree_content(struct tree_content *t) +{ + struct avail_tree_content *f = (struct avail_tree_content*)t; + unsigned int hc = hc_entries(f->entry_capacity); + f->next_avail = avail_tree_table[hc]; + avail_tree_table[hc] = f; +} + +static void release_tree_content_recursive(struct tree_content *t) +{ + unsigned int i; + for (i = 0; i < t->entry_count; i++) + release_tree_entry(t->entries[i]); + release_tree_content(t); +} + +static struct tree_content *grow_tree_content( + struct tree_content *t, + int amt) +{ + struct tree_content *r = new_tree_content(t->entry_count + amt); + r->entry_count = t->entry_count; + r->delta_depth = t->delta_depth; + COPY_ARRAY(r->entries, t->entries, t->entry_count); + release_tree_content(t); + return r; +} + +static struct tree_entry *new_tree_entry(void) +{ + struct tree_entry *e; + + if (!avail_tree_entry) { + unsigned int n = tree_entry_alloc; + tree_entry_allocd += n * sizeof(struct tree_entry); + ALLOC_ARRAY(e, n); + avail_tree_entry = e; + while (n-- > 1) { + *((void**)e) = e + 1; + e++; + } + *((void**)e) = NULL; + } + + e = avail_tree_entry; + avail_tree_entry = *((void**)e); + return e; +} + +static void release_tree_entry(struct tree_entry *e) +{ + if (e->tree) + release_tree_content_recursive(e->tree); + *((void**)e) = avail_tree_entry; + avail_tree_entry = e; +} + +static struct tree_content *dup_tree_content(struct tree_content *s) +{ + struct tree_content *d; + struct tree_entry *a, *b; + unsigned int i; + + if (!s) + return NULL; + d = new_tree_content(s->entry_count); + for (i = 0; i < s->entry_count; i++) { + a = s->entries[i]; + b = new_tree_entry(); + memcpy(b, a, sizeof(*a)); + if (a->tree && is_null_oid(&b->versions[1].oid)) + b->tree = dup_tree_content(a->tree); + else + b->tree = NULL; + d->entries[i] = b; + } + d->entry_count = s->entry_count; + d->delta_depth = s->delta_depth; + + return d; +} + +static void start_packfile(void) +{ + struct strbuf tmp_file = STRBUF_INIT; + struct packed_git *p; + int pack_fd; + + pack_fd = odb_mkstemp(&tmp_file, "pack/tmp_pack_XXXXXX"); + FLEX_ALLOC_STR(p, pack_name, tmp_file.buf); + strbuf_release(&tmp_file); + + p->pack_fd = pack_fd; + p->do_not_close = 1; + pack_file = hashfd(pack_fd, p->pack_name); + + pack_data = p; + pack_size = write_pack_header(pack_file, 0); + object_count = 0; + + REALLOC_ARRAY(all_packs, pack_id + 1); + all_packs[pack_id] = p; +} + +static const char *create_index(void) +{ + const char *tmpfile; + struct pack_idx_entry **idx, **c, **last; + struct object_entry *e; + struct object_entry_pool *o; + + /* Build the table of object IDs. */ + ALLOC_ARRAY(idx, object_count); + c = idx; + for (o = blocks; o; o = o->next_pool) + for (e = o->next_free; e-- != o->entries;) + if (pack_id == e->pack_id) + *c++ = &e->idx; + last = idx + object_count; + if (c != last) + die("internal consistency error creating the index"); + + tmpfile = write_idx_file(NULL, idx, object_count, &pack_idx_opts, + pack_data->hash); + free(idx); + return tmpfile; +} + +static char *keep_pack(const char *curr_index_name) +{ + static const char *keep_msg = "fast-import"; + struct strbuf name = STRBUF_INIT; + int keep_fd; + + odb_pack_name(&name, pack_data->hash, "keep"); + keep_fd = odb_pack_keep(name.buf); + if (keep_fd < 0) + die_errno("cannot create keep file"); + write_or_die(keep_fd, keep_msg, strlen(keep_msg)); + if (close(keep_fd)) + die_errno("failed to write keep file"); + + odb_pack_name(&name, pack_data->hash, "pack"); + if (finalize_object_file(pack_data->pack_name, name.buf)) + die("cannot store pack file"); + + odb_pack_name(&name, pack_data->hash, "idx"); + if (finalize_object_file(curr_index_name, name.buf)) + die("cannot store index file"); + free((void *)curr_index_name); + return strbuf_detach(&name, NULL); +} + +static void unkeep_all_packs(void) +{ + struct strbuf name = STRBUF_INIT; + int k; + + for (k = 0; k < pack_id; k++) { + struct packed_git *p = all_packs[k]; + odb_pack_name(&name, p->hash, "keep"); + unlink_or_warn(name.buf); + } + strbuf_release(&name); +} + +static int loosen_small_pack(const struct packed_git *p) +{ + struct child_process unpack = CHILD_PROCESS_INIT; + + if (lseek(p->pack_fd, 0, SEEK_SET) < 0) + die_errno("Failed seeking to start of '%s'", p->pack_name); + + unpack.in = p->pack_fd; + unpack.git_cmd = 1; + unpack.stdout_to_stderr = 1; + strvec_push(&unpack.args, "unpack-objects"); + if (!show_stats) + strvec_push(&unpack.args, "-q"); + + return run_command(&unpack); +} + +static void end_packfile(void) +{ + static int running; + + if (running || !pack_data) + return; + + running = 1; + clear_delta_base_cache(); + if (object_count) { + struct packed_git *new_p; + struct object_id cur_pack_oid; + char *idx_name; + int i; + struct branch *b; + struct tag *t; + + close_pack_windows(pack_data); + finalize_hashfile(pack_file, cur_pack_oid.hash, FSYNC_COMPONENT_PACK, 0); + fixup_pack_header_footer(pack_data->pack_fd, pack_data->hash, + pack_data->pack_name, object_count, + cur_pack_oid.hash, pack_size); + + if (object_count <= unpack_limit) { + if (!loosen_small_pack(pack_data)) { + invalidate_pack_id(pack_id); + goto discard_pack; + } + } + + close(pack_data->pack_fd); + idx_name = keep_pack(create_index()); + + /* Register the packfile with core git's machinery. */ + new_p = add_packed_git(idx_name, strlen(idx_name), 1); + if (!new_p) + die("core git rejected index %s", idx_name); + all_packs[pack_id] = new_p; + install_packed_git(the_repository, new_p); + free(idx_name); + + /* Print the boundary */ + if (pack_edges) { + fprintf(pack_edges, "%s:", new_p->pack_name); + for (i = 0; i < branch_table_sz; i++) { + for (b = branch_table[i]; b; b = b->table_next_branch) { + if (b->pack_id == pack_id) + fprintf(pack_edges, " %s", + oid_to_hex(&b->oid)); + } + } + for (t = first_tag; t; t = t->next_tag) { + if (t->pack_id == pack_id) + fprintf(pack_edges, " %s", + oid_to_hex(&t->oid)); + } + fputc('\n', pack_edges); + fflush(pack_edges); + } + + pack_id++; + } + else { +discard_pack: + close(pack_data->pack_fd); + unlink_or_warn(pack_data->pack_name); + } + FREE_AND_NULL(pack_data); + running = 0; + + /* We can't carry a delta across packfiles. */ + strbuf_release(&last_blob.data); + last_blob.offset = 0; + last_blob.depth = 0; +} + +static void cycle_packfile(void) +{ + end_packfile(); + start_packfile(); +} + +static int store_object( + enum object_type type, + struct strbuf *dat, + struct last_object *last, + struct object_id *oidout, + uintmax_t mark) +{ + void *out, *delta; + struct object_entry *e; + unsigned char hdr[96]; + struct object_id oid; + unsigned long hdrlen, deltalen; + git_hash_ctx c; + git_zstream s; + + hdrlen = format_object_header((char *)hdr, sizeof(hdr), type, + dat->len); + the_hash_algo->init_fn(&c); + the_hash_algo->update_fn(&c, hdr, hdrlen); + the_hash_algo->update_fn(&c, dat->buf, dat->len); + the_hash_algo->final_oid_fn(&oid, &c); + if (oidout) + oidcpy(oidout, &oid); + + e = insert_object(&oid); + if (mark) + insert_mark(&marks, mark, e); + if (e->idx.offset) { + duplicate_count_by_type[type]++; + return 1; + } else if (find_sha1_pack(oid.hash, + get_all_packs(the_repository))) { + e->type = type; + e->pack_id = MAX_PACK_ID; + e->idx.offset = 1; /* just not zero! */ + duplicate_count_by_type[type]++; + return 1; + } + + if (last && last->data.len && last->data.buf && last->depth < max_depth + && dat->len > the_hash_algo->rawsz) { + + delta_count_attempts_by_type[type]++; + delta = diff_delta(last->data.buf, last->data.len, + dat->buf, dat->len, + &deltalen, dat->len - the_hash_algo->rawsz); + } else + delta = NULL; + + git_deflate_init(&s, pack_compression_level); + if (delta) { + s.next_in = delta; + s.avail_in = deltalen; + } else { + s.next_in = (void *)dat->buf; + s.avail_in = dat->len; + } + s.avail_out = git_deflate_bound(&s, s.avail_in); + s.next_out = out = xmalloc(s.avail_out); + while (git_deflate(&s, Z_FINISH) == Z_OK) + ; /* nothing */ + git_deflate_end(&s); + + /* Determine if we should auto-checkpoint. */ + if ((max_packsize + && (pack_size + PACK_SIZE_THRESHOLD + s.total_out) > max_packsize) + || (pack_size + PACK_SIZE_THRESHOLD + s.total_out) < pack_size) { + + /* This new object needs to *not* have the current pack_id. */ + e->pack_id = pack_id + 1; + cycle_packfile(); + + /* We cannot carry a delta into the new pack. */ + if (delta) { + FREE_AND_NULL(delta); + + git_deflate_init(&s, pack_compression_level); + s.next_in = (void *)dat->buf; + s.avail_in = dat->len; + s.avail_out = git_deflate_bound(&s, s.avail_in); + s.next_out = out = xrealloc(out, s.avail_out); + while (git_deflate(&s, Z_FINISH) == Z_OK) + ; /* nothing */ + git_deflate_end(&s); + } + } + + e->type = type; + e->pack_id = pack_id; + e->idx.offset = pack_size; + object_count++; + object_count_by_type[type]++; + + crc32_begin(pack_file); + + if (delta) { + off_t ofs = e->idx.offset - last->offset; + unsigned pos = sizeof(hdr) - 1; + + delta_count_by_type[type]++; + e->depth = last->depth + 1; + + hdrlen = encode_in_pack_object_header(hdr, sizeof(hdr), + OBJ_OFS_DELTA, deltalen); + hashwrite(pack_file, hdr, hdrlen); + pack_size += hdrlen; + + hdr[pos] = ofs & 127; + while (ofs >>= 7) + hdr[--pos] = 128 | (--ofs & 127); + hashwrite(pack_file, hdr + pos, sizeof(hdr) - pos); + pack_size += sizeof(hdr) - pos; + } else { + e->depth = 0; + hdrlen = encode_in_pack_object_header(hdr, sizeof(hdr), + type, dat->len); + hashwrite(pack_file, hdr, hdrlen); + pack_size += hdrlen; + } + + hashwrite(pack_file, out, s.total_out); + pack_size += s.total_out; + + e->idx.crc32 = crc32_end(pack_file); + + free(out); + free(delta); + if (last) { + if (last->no_swap) { + last->data = *dat; + } else { + strbuf_swap(&last->data, dat); + } + last->offset = e->idx.offset; + last->depth = e->depth; + } + return 0; +} + +static void truncate_pack(struct hashfile_checkpoint *checkpoint) +{ + if (hashfile_truncate(pack_file, checkpoint)) + die_errno("cannot truncate pack to skip duplicate"); + pack_size = checkpoint->offset; +} + +static void stream_blob(uintmax_t len, struct object_id *oidout, uintmax_t mark) +{ + size_t in_sz = 64 * 1024, out_sz = 64 * 1024; + unsigned char *in_buf = xmalloc(in_sz); + unsigned char *out_buf = xmalloc(out_sz); + struct object_entry *e; + struct object_id oid; + unsigned long hdrlen; + off_t offset; + git_hash_ctx c; + git_zstream s; + struct hashfile_checkpoint checkpoint; + int status = Z_OK; + + /* Determine if we should auto-checkpoint. */ + if ((max_packsize + && (pack_size + PACK_SIZE_THRESHOLD + len) > max_packsize) + || (pack_size + PACK_SIZE_THRESHOLD + len) < pack_size) + cycle_packfile(); + + the_hash_algo->init_fn(&checkpoint.ctx); + hashfile_checkpoint(pack_file, &checkpoint); + offset = checkpoint.offset; + + hdrlen = format_object_header((char *)out_buf, out_sz, OBJ_BLOB, len); + + the_hash_algo->init_fn(&c); + the_hash_algo->update_fn(&c, out_buf, hdrlen); + + crc32_begin(pack_file); + + git_deflate_init(&s, pack_compression_level); + + hdrlen = encode_in_pack_object_header(out_buf, out_sz, OBJ_BLOB, len); + + s.next_out = out_buf + hdrlen; + s.avail_out = out_sz - hdrlen; + + while (status != Z_STREAM_END) { + if (0 < len && !s.avail_in) { + size_t cnt = in_sz < len ? in_sz : (size_t)len; + size_t n = fread(in_buf, 1, cnt, stdin); + if (!n && feof(stdin)) + die("EOF in data (%" PRIuMAX " bytes remaining)", len); + + the_hash_algo->update_fn(&c, in_buf, n); + s.next_in = in_buf; + s.avail_in = n; + len -= n; + } + + status = git_deflate(&s, len ? 0 : Z_FINISH); + + if (!s.avail_out || status == Z_STREAM_END) { + size_t n = s.next_out - out_buf; + hashwrite(pack_file, out_buf, n); + pack_size += n; + s.next_out = out_buf; + s.avail_out = out_sz; + } + + switch (status) { + case Z_OK: + case Z_BUF_ERROR: + case Z_STREAM_END: + continue; + default: + die("unexpected deflate failure: %d", status); + } + } + git_deflate_end(&s); + the_hash_algo->final_oid_fn(&oid, &c); + + if (oidout) + oidcpy(oidout, &oid); + + e = insert_object(&oid); + + if (mark) + insert_mark(&marks, mark, e); + + if (e->idx.offset) { + duplicate_count_by_type[OBJ_BLOB]++; + truncate_pack(&checkpoint); + + } else if (find_sha1_pack(oid.hash, + get_all_packs(the_repository))) { + e->type = OBJ_BLOB; + e->pack_id = MAX_PACK_ID; + e->idx.offset = 1; /* just not zero! */ + duplicate_count_by_type[OBJ_BLOB]++; + truncate_pack(&checkpoint); + + } else { + e->depth = 0; + e->type = OBJ_BLOB; + e->pack_id = pack_id; + e->idx.offset = offset; + e->idx.crc32 = crc32_end(pack_file); + object_count++; + object_count_by_type[OBJ_BLOB]++; + } + + free(in_buf); + free(out_buf); +} + +/* All calls must be guarded by find_object() or find_mark() to + * ensure the 'struct object_entry' passed was written by this + * process instance. We unpack the entry by the offset, avoiding + * the need for the corresponding .idx file. This unpacking rule + * works because we only use OBJ_REF_DELTA within the packfiles + * created by fast-import. + * + * oe must not be NULL. Such an oe usually comes from giving + * an unknown SHA-1 to find_object() or an undefined mark to + * find_mark(). Callers must test for this condition and use + * the standard read_sha1_file() when it happens. + * + * oe->pack_id must not be MAX_PACK_ID. Such an oe is usually from + * find_mark(), where the mark was reloaded from an existing marks + * file and is referencing an object that this fast-import process + * instance did not write out to a packfile. Callers must test for + * this condition and use read_sha1_file() instead. + */ +static void *gfi_unpack_entry( + struct object_entry *oe, + unsigned long *sizep) +{ + enum object_type type; + struct packed_git *p = all_packs[oe->pack_id]; + if (p == pack_data && p->pack_size < (pack_size + the_hash_algo->rawsz)) { + /* The object is stored in the packfile we are writing to + * and we have modified it since the last time we scanned + * back to read a previously written object. If an old + * window covered [p->pack_size, p->pack_size + rawsz) its + * data is stale and is not valid. Closing all windows + * and updating the packfile length ensures we can read + * the newly written data. + */ + close_pack_windows(p); + hashflush(pack_file); + + /* We have to offer rawsz bytes additional on the end of + * the packfile as the core unpacker code assumes the + * footer is present at the file end and must promise + * at least rawsz bytes within any window it maps. But + * we don't actually create the footer here. + */ + p->pack_size = pack_size + the_hash_algo->rawsz; + } + return unpack_entry(the_repository, p, oe->idx.offset, &type, sizep); +} + +static const char *get_mode(const char *str, uint16_t *modep) +{ + unsigned char c; + uint16_t mode = 0; + + while ((c = *str++) != ' ') { + if (c < '0' || c > '7') + return NULL; + mode = (mode << 3) + (c - '0'); + } + *modep = mode; + return str; +} + +static void load_tree(struct tree_entry *root) +{ + struct object_id *oid = &root->versions[1].oid; + struct object_entry *myoe; + struct tree_content *t; + unsigned long size; + char *buf; + const char *c; + + root->tree = t = new_tree_content(8); + if (is_null_oid(oid)) + return; + + myoe = find_object(oid); + if (myoe && myoe->pack_id != MAX_PACK_ID) { + if (myoe->type != OBJ_TREE) + die("Not a tree: %s", oid_to_hex(oid)); + t->delta_depth = myoe->depth; + buf = gfi_unpack_entry(myoe, &size); + if (!buf) + die("Can't load tree %s", oid_to_hex(oid)); + } else { + enum object_type type; + buf = repo_read_object_file(the_repository, oid, &type, &size); + if (!buf || type != OBJ_TREE) + die("Can't load tree %s", oid_to_hex(oid)); + } + + c = buf; + while (c != (buf + size)) { + struct tree_entry *e = new_tree_entry(); + + if (t->entry_count == t->entry_capacity) + root->tree = t = grow_tree_content(t, t->entry_count); + t->entries[t->entry_count++] = e; + + e->tree = NULL; + c = get_mode(c, &e->versions[1].mode); + if (!c) + die("Corrupt mode in %s", oid_to_hex(oid)); + e->versions[0].mode = e->versions[1].mode; + e->name = to_atom(c, strlen(c)); + c += e->name->str_len + 1; + oidread(&e->versions[0].oid, (unsigned char *)c); + oidread(&e->versions[1].oid, (unsigned char *)c); + c += the_hash_algo->rawsz; + } + free(buf); +} + +static int tecmp0 (const void *_a, const void *_b) +{ + struct tree_entry *a = *((struct tree_entry**)_a); + struct tree_entry *b = *((struct tree_entry**)_b); + return base_name_compare( + a->name->str_dat, a->name->str_len, a->versions[0].mode, + b->name->str_dat, b->name->str_len, b->versions[0].mode); +} + +static int tecmp1 (const void *_a, const void *_b) +{ + struct tree_entry *a = *((struct tree_entry**)_a); + struct tree_entry *b = *((struct tree_entry**)_b); + return base_name_compare( + a->name->str_dat, a->name->str_len, a->versions[1].mode, + b->name->str_dat, b->name->str_len, b->versions[1].mode); +} + +static void mktree(struct tree_content *t, int v, struct strbuf *b) +{ + size_t maxlen = 0; + unsigned int i; + + if (!v) + QSORT(t->entries, t->entry_count, tecmp0); + else + QSORT(t->entries, t->entry_count, tecmp1); + + for (i = 0; i < t->entry_count; i++) { + if (t->entries[i]->versions[v].mode) + maxlen += t->entries[i]->name->str_len + 34; + } + + strbuf_reset(b); + strbuf_grow(b, maxlen); + for (i = 0; i < t->entry_count; i++) { + struct tree_entry *e = t->entries[i]; + if (!e->versions[v].mode) + continue; + strbuf_addf(b, "%o %s%c", + (unsigned int)(e->versions[v].mode & ~NO_DELTA), + e->name->str_dat, '\0'); + strbuf_add(b, e->versions[v].oid.hash, the_hash_algo->rawsz); + } +} + +static void store_tree(struct tree_entry *root) +{ + struct tree_content *t; + unsigned int i, j, del; + struct last_object lo = { STRBUF_INIT, 0, 0, /* no_swap */ 1 }; + struct object_entry *le = NULL; + + if (!is_null_oid(&root->versions[1].oid)) + return; + + if (!root->tree) + load_tree(root); + t = root->tree; + + for (i = 0; i < t->entry_count; i++) { + if (t->entries[i]->tree) + store_tree(t->entries[i]); + } + + if (!(root->versions[0].mode & NO_DELTA)) + le = find_object(&root->versions[0].oid); + if (S_ISDIR(root->versions[0].mode) && le && le->pack_id == pack_id) { + mktree(t, 0, &old_tree); + lo.data = old_tree; + lo.offset = le->idx.offset; + lo.depth = t->delta_depth; + } + + mktree(t, 1, &new_tree); + store_object(OBJ_TREE, &new_tree, &lo, &root->versions[1].oid, 0); + + t->delta_depth = lo.depth; + for (i = 0, j = 0, del = 0; i < t->entry_count; i++) { + struct tree_entry *e = t->entries[i]; + if (e->versions[1].mode) { + e->versions[0].mode = e->versions[1].mode; + oidcpy(&e->versions[0].oid, &e->versions[1].oid); + t->entries[j++] = e; + } else { + release_tree_entry(e); + del++; + } + } + t->entry_count -= del; +} + +static void tree_content_replace( + struct tree_entry *root, + const struct object_id *oid, + const uint16_t mode, + struct tree_content *newtree) +{ + if (!S_ISDIR(mode)) + die("Root cannot be a non-directory"); + oidclr(&root->versions[0].oid); + oidcpy(&root->versions[1].oid, oid); + if (root->tree) + release_tree_content_recursive(root->tree); + root->tree = newtree; +} + +static int tree_content_set( + struct tree_entry *root, + const char *p, + const struct object_id *oid, + const uint16_t mode, + struct tree_content *subtree) +{ + struct tree_content *t; + const char *slash1; + unsigned int i, n; + struct tree_entry *e; + + slash1 = strchrnul(p, '/'); + n = slash1 - p; + if (!n) + die("Empty path component found in input"); + if (!*slash1 && !S_ISDIR(mode) && subtree) + die("Non-directories cannot have subtrees"); + + if (!root->tree) + load_tree(root); + t = root->tree; + for (i = 0; i < t->entry_count; i++) { + e = t->entries[i]; + if (e->name->str_len == n && !fspathncmp(p, e->name->str_dat, n)) { + if (!*slash1) { + if (!S_ISDIR(mode) + && e->versions[1].mode == mode + && oideq(&e->versions[1].oid, oid)) + return 0; + e->versions[1].mode = mode; + oidcpy(&e->versions[1].oid, oid); + if (e->tree) + release_tree_content_recursive(e->tree); + e->tree = subtree; + + /* + * We need to leave e->versions[0].sha1 alone + * to avoid modifying the preimage tree used + * when writing out the parent directory. + * But after replacing the subdir with a + * completely different one, it's not a good + * delta base any more, and besides, we've + * thrown away the tree entries needed to + * make a delta against it. + * + * So let's just explicitly disable deltas + * for the subtree. + */ + if (S_ISDIR(e->versions[0].mode)) + e->versions[0].mode |= NO_DELTA; + + oidclr(&root->versions[1].oid); + return 1; + } + if (!S_ISDIR(e->versions[1].mode)) { + e->tree = new_tree_content(8); + e->versions[1].mode = S_IFDIR; + } + if (!e->tree) + load_tree(e); + if (tree_content_set(e, slash1 + 1, oid, mode, subtree)) { + oidclr(&root->versions[1].oid); + return 1; + } + return 0; + } + } + + if (t->entry_count == t->entry_capacity) + root->tree = t = grow_tree_content(t, t->entry_count); + e = new_tree_entry(); + e->name = to_atom(p, n); + e->versions[0].mode = 0; + oidclr(&e->versions[0].oid); + t->entries[t->entry_count++] = e; + if (*slash1) { + e->tree = new_tree_content(8); + e->versions[1].mode = S_IFDIR; + tree_content_set(e, slash1 + 1, oid, mode, subtree); + } else { + e->tree = subtree; + e->versions[1].mode = mode; + oidcpy(&e->versions[1].oid, oid); + } + oidclr(&root->versions[1].oid); + return 1; +} + +static int tree_content_remove( + struct tree_entry *root, + const char *p, + struct tree_entry *backup_leaf, + int allow_root) +{ + struct tree_content *t; + const char *slash1; + unsigned int i, n; + struct tree_entry *e; + + slash1 = strchrnul(p, '/'); + n = slash1 - p; + + if (!root->tree) + load_tree(root); + + if (!*p && allow_root) { + e = root; + goto del_entry; + } + + t = root->tree; + for (i = 0; i < t->entry_count; i++) { + e = t->entries[i]; + if (e->name->str_len == n && !fspathncmp(p, e->name->str_dat, n)) { + if (*slash1 && !S_ISDIR(e->versions[1].mode)) + /* + * If p names a file in some subdirectory, and a + * file or symlink matching the name of the + * parent directory of p exists, then p cannot + * exist and need not be deleted. + */ + return 1; + if (!*slash1 || !S_ISDIR(e->versions[1].mode)) + goto del_entry; + if (!e->tree) + load_tree(e); + if (tree_content_remove(e, slash1 + 1, backup_leaf, 0)) { + for (n = 0; n < e->tree->entry_count; n++) { + if (e->tree->entries[n]->versions[1].mode) { + oidclr(&root->versions[1].oid); + return 1; + } + } + backup_leaf = NULL; + goto del_entry; + } + return 0; + } + } + return 0; + +del_entry: + if (backup_leaf) + memcpy(backup_leaf, e, sizeof(*backup_leaf)); + else if (e->tree) + release_tree_content_recursive(e->tree); + e->tree = NULL; + e->versions[1].mode = 0; + oidclr(&e->versions[1].oid); + oidclr(&root->versions[1].oid); + return 1; +} + +static int tree_content_get( + struct tree_entry *root, + const char *p, + struct tree_entry *leaf, + int allow_root) +{ + struct tree_content *t; + const char *slash1; + unsigned int i, n; + struct tree_entry *e; + + slash1 = strchrnul(p, '/'); + n = slash1 - p; + if (!n && !allow_root) + die("Empty path component found in input"); + + if (!root->tree) + load_tree(root); + + if (!n) { + e = root; + goto found_entry; + } + + t = root->tree; + for (i = 0; i < t->entry_count; i++) { + e = t->entries[i]; + if (e->name->str_len == n && !fspathncmp(p, e->name->str_dat, n)) { + if (!*slash1) + goto found_entry; + if (!S_ISDIR(e->versions[1].mode)) + return 0; + if (!e->tree) + load_tree(e); + return tree_content_get(e, slash1 + 1, leaf, 0); + } + } + return 0; + +found_entry: + memcpy(leaf, e, sizeof(*leaf)); + if (e->tree && is_null_oid(&e->versions[1].oid)) + leaf->tree = dup_tree_content(e->tree); + else + leaf->tree = NULL; + return 1; +} + +static int update_branch(struct branch *b) +{ + static const char *msg = "fast-import"; + struct ref_transaction *transaction; + struct object_id old_oid; + struct strbuf err = STRBUF_INIT; + + if (is_null_oid(&b->oid)) { + if (b->delete) + delete_ref(NULL, b->name, NULL, 0); + return 0; + } + if (read_ref(b->name, &old_oid)) + oidclr(&old_oid); + if (!force_update && !is_null_oid(&old_oid)) { + struct commit *old_cmit, *new_cmit; + + old_cmit = lookup_commit_reference_gently(the_repository, + &old_oid, 0); + new_cmit = lookup_commit_reference_gently(the_repository, + &b->oid, 0); + if (!old_cmit || !new_cmit) + return error("Branch %s is missing commits.", b->name); + + if (!repo_in_merge_bases(the_repository, old_cmit, new_cmit)) { + warning("Not updating %s" + " (new tip %s does not contain %s)", + b->name, oid_to_hex(&b->oid), + oid_to_hex(&old_oid)); + return -1; + } + } + transaction = ref_transaction_begin(&err); + if (!transaction || + ref_transaction_update(transaction, b->name, &b->oid, &old_oid, + 0, msg, &err) || + ref_transaction_commit(transaction, &err)) { + ref_transaction_free(transaction); + error("%s", err.buf); + strbuf_release(&err); + return -1; + } + ref_transaction_free(transaction); + strbuf_release(&err); + return 0; +} + +static void dump_branches(void) +{ + unsigned int i; + struct branch *b; + + for (i = 0; i < branch_table_sz; i++) { + for (b = branch_table[i]; b; b = b->table_next_branch) + failure |= update_branch(b); + } +} + +static void dump_tags(void) +{ + static const char *msg = "fast-import"; + struct tag *t; + struct strbuf ref_name = STRBUF_INIT; + struct strbuf err = STRBUF_INIT; + struct ref_transaction *transaction; + + transaction = ref_transaction_begin(&err); + if (!transaction) { + failure |= error("%s", err.buf); + goto cleanup; + } + for (t = first_tag; t; t = t->next_tag) { + strbuf_reset(&ref_name); + strbuf_addf(&ref_name, "refs/tags/%s", t->name); + + if (ref_transaction_update(transaction, ref_name.buf, + &t->oid, NULL, 0, msg, &err)) { + failure |= error("%s", err.buf); + goto cleanup; + } + } + if (ref_transaction_commit(transaction, &err)) + failure |= error("%s", err.buf); + + cleanup: + ref_transaction_free(transaction); + strbuf_release(&ref_name); + strbuf_release(&err); +} + +static void dump_marks(void) +{ + struct lock_file mark_lock = LOCK_INIT; + FILE *f; + + if (!export_marks_file || (import_marks_file && !import_marks_file_done)) + return; + + if (safe_create_leading_directories_const(export_marks_file)) { + failure |= error_errno("unable to create leading directories of %s", + export_marks_file); + return; + } + + if (hold_lock_file_for_update(&mark_lock, export_marks_file, 0) < 0) { + failure |= error_errno("Unable to write marks file %s", + export_marks_file); + return; + } + + f = fdopen_lock_file(&mark_lock, "w"); + if (!f) { + int saved_errno = errno; + rollback_lock_file(&mark_lock); + failure |= error("Unable to write marks file %s: %s", + export_marks_file, strerror(saved_errno)); + return; + } + + for_each_mark(marks, 0, dump_marks_fn, f); + if (commit_lock_file(&mark_lock)) { + failure |= error_errno("Unable to write file %s", + export_marks_file); + return; + } +} + +static void insert_object_entry(struct mark_set **s, struct object_id *oid, uintmax_t mark) +{ + struct object_entry *e; + e = find_object(oid); + if (!e) { + enum object_type type = oid_object_info(the_repository, + oid, NULL); + if (type < 0) + die("object not found: %s", oid_to_hex(oid)); + e = insert_object(oid); + e->type = type; + e->pack_id = MAX_PACK_ID; + e->idx.offset = 1; /* just not zero! */ + } + insert_mark(s, mark, e); +} + +static void insert_oid_entry(struct mark_set **s, struct object_id *oid, uintmax_t mark) +{ + insert_mark(s, mark, xmemdupz(oid, sizeof(*oid))); +} + +static void read_mark_file(struct mark_set **s, FILE *f, mark_set_inserter_t inserter) +{ + char line[512]; + while (fgets(line, sizeof(line), f)) { + uintmax_t mark; + char *end; + struct object_id oid; + + /* Ensure SHA-1 objects are padded with zeros. */ + memset(oid.hash, 0, sizeof(oid.hash)); + + end = strchr(line, '\n'); + if (line[0] != ':' || !end) + die("corrupt mark line: %s", line); + *end = 0; + mark = strtoumax(line + 1, &end, 10); + if (!mark || end == line + 1 + || *end != ' ' + || get_oid_hex_any(end + 1, &oid) == GIT_HASH_UNKNOWN) + die("corrupt mark line: %s", line); + inserter(s, &oid, mark); + } +} + +static void read_marks(void) +{ + FILE *f = fopen(import_marks_file, "r"); + if (f) + ; + else if (import_marks_file_ignore_missing && errno == ENOENT) + goto done; /* Marks file does not exist */ + else + die_errno("cannot read '%s'", import_marks_file); + read_mark_file(&marks, f, insert_object_entry); + fclose(f); +done: + import_marks_file_done = 1; +} + + +static int read_next_command(void) +{ + static int stdin_eof = 0; + + if (stdin_eof) { + unread_command_buf = 0; + return EOF; + } + + for (;;) { + if (unread_command_buf) { + unread_command_buf = 0; + } else { + struct recent_command *rc; + + stdin_eof = strbuf_getline_lf(&command_buf, stdin); + if (stdin_eof) + return EOF; + + if (!seen_data_command + && !starts_with(command_buf.buf, "feature ") + && !starts_with(command_buf.buf, "option ")) { + parse_argv(); + } + + rc = rc_free; + if (rc) + rc_free = rc->next; + else { + rc = cmd_hist.next; + cmd_hist.next = rc->next; + cmd_hist.next->prev = &cmd_hist; + free(rc->buf); + } + + rc->buf = xstrdup(command_buf.buf); + rc->prev = cmd_tail; + rc->next = cmd_hist.prev; + rc->prev->next = rc; + cmd_tail = rc; + } + if (command_buf.buf[0] == '#') + continue; + return 0; + } +} + +static void skip_optional_lf(void) +{ + int term_char = fgetc(stdin); + if (term_char != '\n' && term_char != EOF) + ungetc(term_char, stdin); +} + +static void parse_mark(void) +{ + const char *v; + if (skip_prefix(command_buf.buf, "mark :", &v)) { + next_mark = strtoumax(v, NULL, 10); + read_next_command(); + } + else + next_mark = 0; +} + +static void parse_original_identifier(void) +{ + const char *v; + if (skip_prefix(command_buf.buf, "original-oid ", &v)) + read_next_command(); +} + +static int parse_data(struct strbuf *sb, uintmax_t limit, uintmax_t *len_res) +{ + const char *data; + strbuf_reset(sb); + + if (!skip_prefix(command_buf.buf, "data ", &data)) + die("Expected 'data n' command, found: %s", command_buf.buf); + + if (skip_prefix(data, "<<", &data)) { + char *term = xstrdup(data); + size_t term_len = command_buf.len - (data - command_buf.buf); + + for (;;) { + if (strbuf_getline_lf(&command_buf, stdin) == EOF) + die("EOF in data (terminator '%s' not found)", term); + if (term_len == command_buf.len + && !strcmp(term, command_buf.buf)) + break; + strbuf_addbuf(sb, &command_buf); + strbuf_addch(sb, '\n'); + } + free(term); + } + else { + uintmax_t len = strtoumax(data, NULL, 10); + size_t n = 0, length = (size_t)len; + + if (limit && limit < len) { + *len_res = len; + return 0; + } + if (length < len) + die("data is too large to use in this context"); + + while (n < length) { + size_t s = strbuf_fread(sb, length - n, stdin); + if (!s && feof(stdin)) + die("EOF in data (%lu bytes remaining)", + (unsigned long)(length - n)); + n += s; + } + } + + skip_optional_lf(); + return 1; +} + +static int validate_raw_date(const char *src, struct strbuf *result, int strict) +{ + const char *orig_src = src; + char *endp; + unsigned long num; + + errno = 0; + + num = strtoul(src, &endp, 10); + /* + * NEEDSWORK: perhaps check for reasonable values? For example, we + * could error on values representing times more than a + * day in the future. + */ + if (errno || endp == src || *endp != ' ') + return -1; + + src = endp + 1; + if (*src != '-' && *src != '+') + return -1; + + num = strtoul(src + 1, &endp, 10); + /* + * NEEDSWORK: check for brokenness other than num > 1400, such as + * (num % 100) >= 60, or ((num % 100) % 15) != 0 ? + */ + if (errno || endp == src + 1 || *endp || /* did not parse */ + (strict && (1400 < num)) /* parsed a broken timezone */ + ) + return -1; + + strbuf_addstr(result, orig_src); + return 0; +} + +static char *parse_ident(const char *buf) +{ + const char *ltgt; + size_t name_len; + struct strbuf ident = STRBUF_INIT; + + /* ensure there is a space delimiter even if there is no name */ + if (*buf == '<') + --buf; + + ltgt = buf + strcspn(buf, "<>"); + if (*ltgt != '<') + die("Missing < in ident string: %s", buf); + if (ltgt != buf && ltgt[-1] != ' ') + die("Missing space before < in ident string: %s", buf); + ltgt = ltgt + 1 + strcspn(ltgt + 1, "<>"); + if (*ltgt != '>') + die("Missing > in ident string: %s", buf); + ltgt++; + if (*ltgt != ' ') + die("Missing space after > in ident string: %s", buf); + ltgt++; + name_len = ltgt - buf; + strbuf_add(&ident, buf, name_len); + + switch (whenspec) { + case WHENSPEC_RAW: + if (validate_raw_date(ltgt, &ident, 1) < 0) + die("Invalid raw date \"%s\" in ident: %s", ltgt, buf); + break; + case WHENSPEC_RAW_PERMISSIVE: + if (validate_raw_date(ltgt, &ident, 0) < 0) + die("Invalid raw date \"%s\" in ident: %s", ltgt, buf); + break; + case WHENSPEC_RFC2822: + if (parse_date(ltgt, &ident) < 0) + die("Invalid rfc2822 date \"%s\" in ident: %s", ltgt, buf); + break; + case WHENSPEC_NOW: + if (strcmp("now", ltgt)) + die("Date in ident must be 'now': %s", buf); + datestamp(&ident); + break; + } + + return strbuf_detach(&ident, NULL); +} + +static void parse_and_store_blob( + struct last_object *last, + struct object_id *oidout, + uintmax_t mark) +{ + static struct strbuf buf = STRBUF_INIT; + uintmax_t len; + + if (parse_data(&buf, big_file_threshold, &len)) + store_object(OBJ_BLOB, &buf, last, oidout, mark); + else { + if (last) { + strbuf_release(&last->data); + last->offset = 0; + last->depth = 0; + } + stream_blob(len, oidout, mark); + skip_optional_lf(); + } +} + +static void parse_new_blob(void) +{ + read_next_command(); + parse_mark(); + parse_original_identifier(); + parse_and_store_blob(&last_blob, NULL, next_mark); +} + +static void unload_one_branch(void) +{ + while (cur_active_branches + && cur_active_branches >= max_active_branches) { + uintmax_t min_commit = ULONG_MAX; + struct branch *e, *l = NULL, *p = NULL; + + for (e = active_branches; e; e = e->active_next_branch) { + if (e->last_commit < min_commit) { + p = l; + min_commit = e->last_commit; + } + l = e; + } + + if (p) { + e = p->active_next_branch; + p->active_next_branch = e->active_next_branch; + } else { + e = active_branches; + active_branches = e->active_next_branch; + } + e->active = 0; + e->active_next_branch = NULL; + if (e->branch_tree.tree) { + release_tree_content_recursive(e->branch_tree.tree); + e->branch_tree.tree = NULL; + } + cur_active_branches--; + } +} + +static void load_branch(struct branch *b) +{ + load_tree(&b->branch_tree); + if (!b->active) { + b->active = 1; + b->active_next_branch = active_branches; + active_branches = b; + cur_active_branches++; + branch_load_count++; + } +} + +static unsigned char convert_num_notes_to_fanout(uintmax_t num_notes) +{ + unsigned char fanout = 0; + while ((num_notes >>= 8)) + fanout++; + return fanout; +} + +static void construct_path_with_fanout(const char *hex_sha1, + unsigned char fanout, char *path) +{ + unsigned int i = 0, j = 0; + if (fanout >= the_hash_algo->rawsz) + die("Too large fanout (%u)", fanout); + while (fanout) { + path[i++] = hex_sha1[j++]; + path[i++] = hex_sha1[j++]; + path[i++] = '/'; + fanout--; + } + memcpy(path + i, hex_sha1 + j, the_hash_algo->hexsz - j); + path[i + the_hash_algo->hexsz - j] = '\0'; +} + +static uintmax_t do_change_note_fanout( + struct tree_entry *orig_root, struct tree_entry *root, + char *hex_oid, unsigned int hex_oid_len, + char *fullpath, unsigned int fullpath_len, + unsigned char fanout) +{ + struct tree_content *t; + struct tree_entry *e, leaf; + unsigned int i, tmp_hex_oid_len, tmp_fullpath_len; + uintmax_t num_notes = 0; + struct object_id oid; + /* hex oid + '/' between each pair of hex digits + NUL */ + char realpath[GIT_MAX_HEXSZ + ((GIT_MAX_HEXSZ / 2) - 1) + 1]; + const unsigned hexsz = the_hash_algo->hexsz; + + if (!root->tree) + load_tree(root); + t = root->tree; + + for (i = 0; t && i < t->entry_count; i++) { + e = t->entries[i]; + tmp_hex_oid_len = hex_oid_len + e->name->str_len; + tmp_fullpath_len = fullpath_len; + + /* + * We're interested in EITHER existing note entries (entries + * with exactly 40 hex chars in path, not including directory + * separators), OR directory entries that may contain note + * entries (with < 40 hex chars in path). + * Also, each path component in a note entry must be a multiple + * of 2 chars. + */ + if (!e->versions[1].mode || + tmp_hex_oid_len > hexsz || + e->name->str_len % 2) + continue; + + /* This _may_ be a note entry, or a subdir containing notes */ + memcpy(hex_oid + hex_oid_len, e->name->str_dat, + e->name->str_len); + if (tmp_fullpath_len) + fullpath[tmp_fullpath_len++] = '/'; + memcpy(fullpath + tmp_fullpath_len, e->name->str_dat, + e->name->str_len); + tmp_fullpath_len += e->name->str_len; + fullpath[tmp_fullpath_len] = '\0'; + + if (tmp_hex_oid_len == hexsz && !get_oid_hex(hex_oid, &oid)) { + /* This is a note entry */ + if (fanout == 0xff) { + /* Counting mode, no rename */ + num_notes++; + continue; + } + construct_path_with_fanout(hex_oid, fanout, realpath); + if (!strcmp(fullpath, realpath)) { + /* Note entry is in correct location */ + num_notes++; + continue; + } + + /* Rename fullpath to realpath */ + if (!tree_content_remove(orig_root, fullpath, &leaf, 0)) + die("Failed to remove path %s", fullpath); + tree_content_set(orig_root, realpath, + &leaf.versions[1].oid, + leaf.versions[1].mode, + leaf.tree); + } else if (S_ISDIR(e->versions[1].mode)) { + /* This is a subdir that may contain note entries */ + num_notes += do_change_note_fanout(orig_root, e, + hex_oid, tmp_hex_oid_len, + fullpath, tmp_fullpath_len, fanout); + } + + /* The above may have reallocated the current tree_content */ + t = root->tree; + } + return num_notes; +} + +static uintmax_t change_note_fanout(struct tree_entry *root, + unsigned char fanout) +{ + /* + * The size of path is due to one slash between every two hex digits, + * plus the terminating NUL. Note that there is no slash at the end, so + * the number of slashes is one less than half the number of hex + * characters. + */ + char hex_oid[GIT_MAX_HEXSZ], path[GIT_MAX_HEXSZ + (GIT_MAX_HEXSZ / 2) - 1 + 1]; + return do_change_note_fanout(root, root, hex_oid, 0, path, 0, fanout); +} + +static int parse_mapped_oid_hex(const char *hex, struct object_id *oid, const char **end) +{ + int algo; + khiter_t it; + + /* Make SHA-1 object IDs have all-zero padding. */ + memset(oid->hash, 0, sizeof(oid->hash)); + + algo = parse_oid_hex_any(hex, oid, end); + if (algo == GIT_HASH_UNKNOWN) + return -1; + + it = kh_get_oid_map(sub_oid_map, *oid); + /* No such object? */ + if (it == kh_end(sub_oid_map)) { + /* If we're using the same algorithm, pass it through. */ + if (hash_algos[algo].format_id == the_hash_algo->format_id) + return 0; + return -1; + } + oidcpy(oid, kh_value(sub_oid_map, it)); + return 0; +} + +/* + * Given a pointer into a string, parse a mark reference: + * + * idnum ::= ':' bigint; + * + * Return the first character after the value in *endptr. + * + * Complain if the following character is not what is expected, + * either a space or end of the string. + */ +static uintmax_t parse_mark_ref(const char *p, char **endptr) +{ + uintmax_t mark; + + assert(*p == ':'); + p++; + mark = strtoumax(p, endptr, 10); + if (*endptr == p) + die("No value after ':' in mark: %s", command_buf.buf); + return mark; +} + +/* + * Parse the mark reference, and complain if this is not the end of + * the string. + */ +static uintmax_t parse_mark_ref_eol(const char *p) +{ + char *end; + uintmax_t mark; + + mark = parse_mark_ref(p, &end); + if (*end != '\0') + die("Garbage after mark: %s", command_buf.buf); + return mark; +} + +/* + * Parse the mark reference, demanding a trailing space. Return a + * pointer to the space. + */ +static uintmax_t parse_mark_ref_space(const char **p) +{ + uintmax_t mark; + char *end; + + mark = parse_mark_ref(*p, &end); + if (*end++ != ' ') + die("Missing space after mark: %s", command_buf.buf); + *p = end; + return mark; +} + +static void file_change_m(const char *p, struct branch *b) +{ + static struct strbuf uq = STRBUF_INIT; + const char *endp; + struct object_entry *oe; + struct object_id oid; + uint16_t mode, inline_data = 0; + + p = get_mode(p, &mode); + if (!p) + die("Corrupt mode: %s", command_buf.buf); + switch (mode) { + case 0644: + case 0755: + mode |= S_IFREG; + case S_IFREG | 0644: + case S_IFREG | 0755: + case S_IFLNK: + case S_IFDIR: + case S_IFGITLINK: + /* ok */ + break; + default: + die("Corrupt mode: %s", command_buf.buf); + } + + if (*p == ':') { + oe = find_mark(marks, parse_mark_ref_space(&p)); + oidcpy(&oid, &oe->idx.oid); + } else if (skip_prefix(p, "inline ", &p)) { + inline_data = 1; + oe = NULL; /* not used with inline_data, but makes gcc happy */ + } else { + if (parse_mapped_oid_hex(p, &oid, &p)) + die("Invalid dataref: %s", command_buf.buf); + oe = find_object(&oid); + if (*p++ != ' ') + die("Missing space after SHA1: %s", command_buf.buf); + } + + strbuf_reset(&uq); + if (!unquote_c_style(&uq, p, &endp)) { + if (*endp) + die("Garbage after path in: %s", command_buf.buf); + p = uq.buf; + } + + /* Git does not track empty, non-toplevel directories. */ + if (S_ISDIR(mode) && is_empty_tree_oid(&oid) && *p) { + tree_content_remove(&b->branch_tree, p, NULL, 0); + return; + } + + if (S_ISGITLINK(mode)) { + if (inline_data) + die("Git links cannot be specified 'inline': %s", + command_buf.buf); + else if (oe) { + if (oe->type != OBJ_COMMIT) + die("Not a commit (actually a %s): %s", + type_name(oe->type), command_buf.buf); + } + /* + * Accept the sha1 without checking; it expected to be in + * another repository. + */ + } else if (inline_data) { + if (S_ISDIR(mode)) + die("Directories cannot be specified 'inline': %s", + command_buf.buf); + if (p != uq.buf) { + strbuf_addstr(&uq, p); + p = uq.buf; + } + while (read_next_command() != EOF) { + const char *v; + if (skip_prefix(command_buf.buf, "cat-blob ", &v)) + parse_cat_blob(v); + else { + parse_and_store_blob(&last_blob, &oid, 0); + break; + } + } + } else { + enum object_type expected = S_ISDIR(mode) ? + OBJ_TREE: OBJ_BLOB; + enum object_type type = oe ? oe->type : + oid_object_info(the_repository, &oid, + NULL); + if (type < 0) + die("%s not found: %s", + S_ISDIR(mode) ? "Tree" : "Blob", + command_buf.buf); + if (type != expected) + die("Not a %s (actually a %s): %s", + type_name(expected), type_name(type), + command_buf.buf); + } + + if (!*p) { + tree_content_replace(&b->branch_tree, &oid, mode, NULL); + return; + } + tree_content_set(&b->branch_tree, p, &oid, mode, NULL); +} + +static void file_change_d(const char *p, struct branch *b) +{ + static struct strbuf uq = STRBUF_INIT; + const char *endp; + + strbuf_reset(&uq); + if (!unquote_c_style(&uq, p, &endp)) { + if (*endp) + die("Garbage after path in: %s", command_buf.buf); + p = uq.buf; + } + tree_content_remove(&b->branch_tree, p, NULL, 1); +} + +static void file_change_cr(const char *s, struct branch *b, int rename) +{ + const char *d; + static struct strbuf s_uq = STRBUF_INIT; + static struct strbuf d_uq = STRBUF_INIT; + const char *endp; + struct tree_entry leaf; + + strbuf_reset(&s_uq); + if (!unquote_c_style(&s_uq, s, &endp)) { + if (*endp != ' ') + die("Missing space after source: %s", command_buf.buf); + } else { + endp = strchr(s, ' '); + if (!endp) + die("Missing space after source: %s", command_buf.buf); + strbuf_add(&s_uq, s, endp - s); + } + s = s_uq.buf; + + endp++; + if (!*endp) + die("Missing dest: %s", command_buf.buf); + + d = endp; + strbuf_reset(&d_uq); + if (!unquote_c_style(&d_uq, d, &endp)) { + if (*endp) + die("Garbage after dest in: %s", command_buf.buf); + d = d_uq.buf; + } + + memset(&leaf, 0, sizeof(leaf)); + if (rename) + tree_content_remove(&b->branch_tree, s, &leaf, 1); + else + tree_content_get(&b->branch_tree, s, &leaf, 1); + if (!leaf.versions[1].mode) + die("Path %s not in branch", s); + if (!*d) { /* C "path/to/subdir" "" */ + tree_content_replace(&b->branch_tree, + &leaf.versions[1].oid, + leaf.versions[1].mode, + leaf.tree); + return; + } + tree_content_set(&b->branch_tree, d, + &leaf.versions[1].oid, + leaf.versions[1].mode, + leaf.tree); +} + +static void note_change_n(const char *p, struct branch *b, unsigned char *old_fanout) +{ + static struct strbuf uq = STRBUF_INIT; + struct object_entry *oe; + struct branch *s; + struct object_id oid, commit_oid; + char path[GIT_MAX_RAWSZ * 3]; + uint16_t inline_data = 0; + unsigned char new_fanout; + + /* + * When loading a branch, we don't traverse its tree to count the real + * number of notes (too expensive to do this for all non-note refs). + * This means that recently loaded notes refs might incorrectly have + * b->num_notes == 0, and consequently, old_fanout might be wrong. + * + * Fix this by traversing the tree and counting the number of notes + * when b->num_notes == 0. If the notes tree is truly empty, the + * calculation should not take long. + */ + if (b->num_notes == 0 && *old_fanout == 0) { + /* Invoke change_note_fanout() in "counting mode". */ + b->num_notes = change_note_fanout(&b->branch_tree, 0xff); + *old_fanout = convert_num_notes_to_fanout(b->num_notes); + } + + /* Now parse the notemodify command. */ + /* <dataref> or 'inline' */ + if (*p == ':') { + oe = find_mark(marks, parse_mark_ref_space(&p)); + oidcpy(&oid, &oe->idx.oid); + } else if (skip_prefix(p, "inline ", &p)) { + inline_data = 1; + oe = NULL; /* not used with inline_data, but makes gcc happy */ + } else { + if (parse_mapped_oid_hex(p, &oid, &p)) + die("Invalid dataref: %s", command_buf.buf); + oe = find_object(&oid); + if (*p++ != ' ') + die("Missing space after SHA1: %s", command_buf.buf); + } + + /* <commit-ish> */ + s = lookup_branch(p); + if (s) { + if (is_null_oid(&s->oid)) + die("Can't add a note on empty branch."); + oidcpy(&commit_oid, &s->oid); + } else if (*p == ':') { + uintmax_t commit_mark = parse_mark_ref_eol(p); + struct object_entry *commit_oe = find_mark(marks, commit_mark); + if (commit_oe->type != OBJ_COMMIT) + die("Mark :%" PRIuMAX " not a commit", commit_mark); + oidcpy(&commit_oid, &commit_oe->idx.oid); + } else if (!repo_get_oid(the_repository, p, &commit_oid)) { + unsigned long size; + char *buf = read_object_with_reference(the_repository, + &commit_oid, + OBJ_COMMIT, &size, + &commit_oid); + if (!buf || size < the_hash_algo->hexsz + 6) + die("Not a valid commit: %s", p); + free(buf); + } else + die("Invalid ref name or SHA1 expression: %s", p); + + if (inline_data) { + if (p != uq.buf) { + strbuf_addstr(&uq, p); + p = uq.buf; + } + read_next_command(); + parse_and_store_blob(&last_blob, &oid, 0); + } else if (oe) { + if (oe->type != OBJ_BLOB) + die("Not a blob (actually a %s): %s", + type_name(oe->type), command_buf.buf); + } else if (!is_null_oid(&oid)) { + enum object_type type = oid_object_info(the_repository, &oid, + NULL); + if (type < 0) + die("Blob not found: %s", command_buf.buf); + if (type != OBJ_BLOB) + die("Not a blob (actually a %s): %s", + type_name(type), command_buf.buf); + } + + construct_path_with_fanout(oid_to_hex(&commit_oid), *old_fanout, path); + if (tree_content_remove(&b->branch_tree, path, NULL, 0)) + b->num_notes--; + + if (is_null_oid(&oid)) + return; /* nothing to insert */ + + b->num_notes++; + new_fanout = convert_num_notes_to_fanout(b->num_notes); + construct_path_with_fanout(oid_to_hex(&commit_oid), new_fanout, path); + tree_content_set(&b->branch_tree, path, &oid, S_IFREG | 0644, NULL); +} + +static void file_change_deleteall(struct branch *b) +{ + release_tree_content_recursive(b->branch_tree.tree); + oidclr(&b->branch_tree.versions[0].oid); + oidclr(&b->branch_tree.versions[1].oid); + load_tree(&b->branch_tree); + b->num_notes = 0; +} + +static void parse_from_commit(struct branch *b, char *buf, unsigned long size) +{ + if (!buf || size < the_hash_algo->hexsz + 6) + die("Not a valid commit: %s", oid_to_hex(&b->oid)); + if (memcmp("tree ", buf, 5) + || get_oid_hex(buf + 5, &b->branch_tree.versions[1].oid)) + die("The commit %s is corrupt", oid_to_hex(&b->oid)); + oidcpy(&b->branch_tree.versions[0].oid, + &b->branch_tree.versions[1].oid); +} + +static void parse_from_existing(struct branch *b) +{ + if (is_null_oid(&b->oid)) { + oidclr(&b->branch_tree.versions[0].oid); + oidclr(&b->branch_tree.versions[1].oid); + } else { + unsigned long size; + char *buf; + + buf = read_object_with_reference(the_repository, + &b->oid, OBJ_COMMIT, &size, + &b->oid); + parse_from_commit(b, buf, size); + free(buf); + } +} + +static int parse_objectish(struct branch *b, const char *objectish) +{ + struct branch *s; + struct object_id oid; + + oidcpy(&oid, &b->branch_tree.versions[1].oid); + + s = lookup_branch(objectish); + if (b == s) + die("Can't create a branch from itself: %s", b->name); + else if (s) { + struct object_id *t = &s->branch_tree.versions[1].oid; + oidcpy(&b->oid, &s->oid); + oidcpy(&b->branch_tree.versions[0].oid, t); + oidcpy(&b->branch_tree.versions[1].oid, t); + } else if (*objectish == ':') { + uintmax_t idnum = parse_mark_ref_eol(objectish); + struct object_entry *oe = find_mark(marks, idnum); + if (oe->type != OBJ_COMMIT) + die("Mark :%" PRIuMAX " not a commit", idnum); + if (!oideq(&b->oid, &oe->idx.oid)) { + oidcpy(&b->oid, &oe->idx.oid); + if (oe->pack_id != MAX_PACK_ID) { + unsigned long size; + char *buf = gfi_unpack_entry(oe, &size); + parse_from_commit(b, buf, size); + free(buf); + } else + parse_from_existing(b); + } + } else if (!repo_get_oid(the_repository, objectish, &b->oid)) { + parse_from_existing(b); + if (is_null_oid(&b->oid)) + b->delete = 1; + } + else + die("Invalid ref name or SHA1 expression: %s", objectish); + + if (b->branch_tree.tree && !oideq(&oid, &b->branch_tree.versions[1].oid)) { + release_tree_content_recursive(b->branch_tree.tree); + b->branch_tree.tree = NULL; + } + + read_next_command(); + return 1; +} + +static int parse_from(struct branch *b) +{ + const char *from; + + if (!skip_prefix(command_buf.buf, "from ", &from)) + return 0; + + return parse_objectish(b, from); +} + +static int parse_objectish_with_prefix(struct branch *b, const char *prefix) +{ + const char *base; + + if (!skip_prefix(command_buf.buf, prefix, &base)) + return 0; + + return parse_objectish(b, base); +} + +static struct hash_list *parse_merge(unsigned int *count) +{ + struct hash_list *list = NULL, **tail = &list, *n; + const char *from; + struct branch *s; + + *count = 0; + while (skip_prefix(command_buf.buf, "merge ", &from)) { + n = xmalloc(sizeof(*n)); + s = lookup_branch(from); + if (s) + oidcpy(&n->oid, &s->oid); + else if (*from == ':') { + uintmax_t idnum = parse_mark_ref_eol(from); + struct object_entry *oe = find_mark(marks, idnum); + if (oe->type != OBJ_COMMIT) + die("Mark :%" PRIuMAX " not a commit", idnum); + oidcpy(&n->oid, &oe->idx.oid); + } else if (!repo_get_oid(the_repository, from, &n->oid)) { + unsigned long size; + char *buf = read_object_with_reference(the_repository, + &n->oid, + OBJ_COMMIT, + &size, &n->oid); + if (!buf || size < the_hash_algo->hexsz + 6) + die("Not a valid commit: %s", from); + free(buf); + } else + die("Invalid ref name or SHA1 expression: %s", from); + + n->next = NULL; + *tail = n; + tail = &n->next; + + (*count)++; + read_next_command(); + } + return list; +} + +static void parse_new_commit(const char *arg) +{ + static struct strbuf msg = STRBUF_INIT; + struct branch *b; + char *author = NULL; + char *committer = NULL; + char *encoding = NULL; + struct hash_list *merge_list = NULL; + unsigned int merge_count; + unsigned char prev_fanout, new_fanout; + const char *v; + + b = lookup_branch(arg); + if (!b) + b = new_branch(arg); + + read_next_command(); + parse_mark(); + parse_original_identifier(); + if (skip_prefix(command_buf.buf, "author ", &v)) { + author = parse_ident(v); + read_next_command(); + } + if (skip_prefix(command_buf.buf, "committer ", &v)) { + committer = parse_ident(v); + read_next_command(); + } + if (!committer) + die("Expected committer but didn't get one"); + if (skip_prefix(command_buf.buf, "encoding ", &v)) { + encoding = xstrdup(v); + read_next_command(); + } + parse_data(&msg, 0, NULL); + read_next_command(); + parse_from(b); + merge_list = parse_merge(&merge_count); + + /* ensure the branch is active/loaded */ + if (!b->branch_tree.tree || !max_active_branches) { + unload_one_branch(); + load_branch(b); + } + + prev_fanout = convert_num_notes_to_fanout(b->num_notes); + + /* file_change* */ + while (command_buf.len > 0) { + if (skip_prefix(command_buf.buf, "M ", &v)) + file_change_m(v, b); + else if (skip_prefix(command_buf.buf, "D ", &v)) + file_change_d(v, b); + else if (skip_prefix(command_buf.buf, "R ", &v)) + file_change_cr(v, b, 1); + else if (skip_prefix(command_buf.buf, "C ", &v)) + file_change_cr(v, b, 0); + else if (skip_prefix(command_buf.buf, "N ", &v)) + note_change_n(v, b, &prev_fanout); + else if (!strcmp("deleteall", command_buf.buf)) + file_change_deleteall(b); + else if (skip_prefix(command_buf.buf, "ls ", &v)) + parse_ls(v, b); + else if (skip_prefix(command_buf.buf, "cat-blob ", &v)) + parse_cat_blob(v); + else { + unread_command_buf = 1; + break; + } + if (read_next_command() == EOF) + break; + } + + new_fanout = convert_num_notes_to_fanout(b->num_notes); + if (new_fanout != prev_fanout) + b->num_notes = change_note_fanout(&b->branch_tree, new_fanout); + + /* build the tree and the commit */ + store_tree(&b->branch_tree); + oidcpy(&b->branch_tree.versions[0].oid, + &b->branch_tree.versions[1].oid); + + strbuf_reset(&new_data); + strbuf_addf(&new_data, "tree %s\n", + oid_to_hex(&b->branch_tree.versions[1].oid)); + if (!is_null_oid(&b->oid)) + strbuf_addf(&new_data, "parent %s\n", + oid_to_hex(&b->oid)); + while (merge_list) { + struct hash_list *next = merge_list->next; + strbuf_addf(&new_data, "parent %s\n", + oid_to_hex(&merge_list->oid)); + free(merge_list); + merge_list = next; + } + strbuf_addf(&new_data, + "author %s\n" + "committer %s\n", + author ? author : committer, committer); + if (encoding) + strbuf_addf(&new_data, + "encoding %s\n", + encoding); + strbuf_addch(&new_data, '\n'); + strbuf_addbuf(&new_data, &msg); + free(author); + free(committer); + free(encoding); + + if (!store_object(OBJ_COMMIT, &new_data, NULL, &b->oid, next_mark)) + b->pack_id = pack_id; + b->last_commit = object_count_by_type[OBJ_COMMIT]; +} + +static void parse_new_tag(const char *arg) +{ + static struct strbuf msg = STRBUF_INIT; + const char *from; + char *tagger; + struct branch *s; + struct tag *t; + uintmax_t from_mark = 0; + struct object_id oid; + enum object_type type; + const char *v; + + t = mem_pool_alloc(&fi_mem_pool, sizeof(struct tag)); + memset(t, 0, sizeof(struct tag)); + t->name = mem_pool_strdup(&fi_mem_pool, arg); + if (last_tag) + last_tag->next_tag = t; + else + first_tag = t; + last_tag = t; + read_next_command(); + parse_mark(); + + /* from ... */ + if (!skip_prefix(command_buf.buf, "from ", &from)) + die("Expected from command, got %s", command_buf.buf); + s = lookup_branch(from); + if (s) { + if (is_null_oid(&s->oid)) + die("Can't tag an empty branch."); + oidcpy(&oid, &s->oid); + type = OBJ_COMMIT; + } else if (*from == ':') { + struct object_entry *oe; + from_mark = parse_mark_ref_eol(from); + oe = find_mark(marks, from_mark); + type = oe->type; + oidcpy(&oid, &oe->idx.oid); + } else if (!repo_get_oid(the_repository, from, &oid)) { + struct object_entry *oe = find_object(&oid); + if (!oe) { + type = oid_object_info(the_repository, &oid, NULL); + if (type < 0) + die("Not a valid object: %s", from); + } else + type = oe->type; + } else + die("Invalid ref name or SHA1 expression: %s", from); + read_next_command(); + + /* original-oid ... */ + parse_original_identifier(); + + /* tagger ... */ + if (skip_prefix(command_buf.buf, "tagger ", &v)) { + tagger = parse_ident(v); + read_next_command(); + } else + tagger = NULL; + + /* tag payload/message */ + parse_data(&msg, 0, NULL); + + /* build the tag object */ + strbuf_reset(&new_data); + + strbuf_addf(&new_data, + "object %s\n" + "type %s\n" + "tag %s\n", + oid_to_hex(&oid), type_name(type), t->name); + if (tagger) + strbuf_addf(&new_data, + "tagger %s\n", tagger); + strbuf_addch(&new_data, '\n'); + strbuf_addbuf(&new_data, &msg); + free(tagger); + + if (store_object(OBJ_TAG, &new_data, NULL, &t->oid, next_mark)) + t->pack_id = MAX_PACK_ID; + else + t->pack_id = pack_id; +} + +static void parse_reset_branch(const char *arg) +{ + struct branch *b; + const char *tag_name; + + b = lookup_branch(arg); + if (b) { + oidclr(&b->oid); + oidclr(&b->branch_tree.versions[0].oid); + oidclr(&b->branch_tree.versions[1].oid); + if (b->branch_tree.tree) { + release_tree_content_recursive(b->branch_tree.tree); + b->branch_tree.tree = NULL; + } + } + else + b = new_branch(arg); + read_next_command(); + parse_from(b); + if (b->delete && skip_prefix(b->name, "refs/tags/", &tag_name)) { + /* + * Elsewhere, we call dump_branches() before dump_tags(), + * and dump_branches() will handle ref deletions first, so + * in order to make sure the deletion actually takes effect, + * we need to remove the tag from our list of tags to update. + * + * NEEDSWORK: replace list of tags with hashmap for faster + * deletion? + */ + struct tag *t, *prev = NULL; + for (t = first_tag; t; t = t->next_tag) { + if (!strcmp(t->name, tag_name)) + break; + prev = t; + } + if (t) { + if (prev) + prev->next_tag = t->next_tag; + else + first_tag = t->next_tag; + if (!t->next_tag) + last_tag = prev; + /* There is no mem_pool_free(t) function to call. */ + } + } + if (command_buf.len > 0) + unread_command_buf = 1; +} + +static void cat_blob_write(const char *buf, unsigned long size) +{ + if (write_in_full(cat_blob_fd, buf, size) < 0) + die_errno("Write to frontend failed"); +} + +static void cat_blob(struct object_entry *oe, struct object_id *oid) +{ + struct strbuf line = STRBUF_INIT; + unsigned long size; + enum object_type type = 0; + char *buf; + + if (!oe || oe->pack_id == MAX_PACK_ID) { + buf = repo_read_object_file(the_repository, oid, &type, &size); + } else { + type = oe->type; + buf = gfi_unpack_entry(oe, &size); + } + + /* + * Output based on batch_one_object() from cat-file.c. + */ + if (type <= 0) { + strbuf_reset(&line); + strbuf_addf(&line, "%s missing\n", oid_to_hex(oid)); + cat_blob_write(line.buf, line.len); + strbuf_release(&line); + free(buf); + return; + } + if (!buf) + die("Can't read object %s", oid_to_hex(oid)); + if (type != OBJ_BLOB) + die("Object %s is a %s but a blob was expected.", + oid_to_hex(oid), type_name(type)); + strbuf_reset(&line); + strbuf_addf(&line, "%s %s %"PRIuMAX"\n", oid_to_hex(oid), + type_name(type), (uintmax_t)size); + cat_blob_write(line.buf, line.len); + strbuf_release(&line); + cat_blob_write(buf, size); + cat_blob_write("\n", 1); + if (oe && oe->pack_id == pack_id) { + last_blob.offset = oe->idx.offset; + strbuf_attach(&last_blob.data, buf, size, size); + last_blob.depth = oe->depth; + } else + free(buf); +} + +static void parse_get_mark(const char *p) +{ + struct object_entry *oe; + char output[GIT_MAX_HEXSZ + 2]; + + /* get-mark SP <object> LF */ + if (*p != ':') + die("Not a mark: %s", p); + + oe = find_mark(marks, parse_mark_ref_eol(p)); + if (!oe) + die("Unknown mark: %s", command_buf.buf); + + xsnprintf(output, sizeof(output), "%s\n", oid_to_hex(&oe->idx.oid)); + cat_blob_write(output, the_hash_algo->hexsz + 1); +} + +static void parse_cat_blob(const char *p) +{ + struct object_entry *oe; + struct object_id oid; + + /* cat-blob SP <object> LF */ + if (*p == ':') { + oe = find_mark(marks, parse_mark_ref_eol(p)); + if (!oe) + die("Unknown mark: %s", command_buf.buf); + oidcpy(&oid, &oe->idx.oid); + } else { + if (parse_mapped_oid_hex(p, &oid, &p)) + die("Invalid dataref: %s", command_buf.buf); + if (*p) + die("Garbage after SHA1: %s", command_buf.buf); + oe = find_object(&oid); + } + + cat_blob(oe, &oid); +} + +static struct object_entry *dereference(struct object_entry *oe, + struct object_id *oid) +{ + unsigned long size; + char *buf = NULL; + const unsigned hexsz = the_hash_algo->hexsz; + + if (!oe) { + enum object_type type = oid_object_info(the_repository, oid, + NULL); + if (type < 0) + die("object not found: %s", oid_to_hex(oid)); + /* cache it! */ + oe = insert_object(oid); + oe->type = type; + oe->pack_id = MAX_PACK_ID; + oe->idx.offset = 1; + } + switch (oe->type) { + case OBJ_TREE: /* easy case. */ + return oe; + case OBJ_COMMIT: + case OBJ_TAG: + break; + default: + die("Not a tree-ish: %s", command_buf.buf); + } + + if (oe->pack_id != MAX_PACK_ID) { /* in a pack being written */ + buf = gfi_unpack_entry(oe, &size); + } else { + enum object_type unused; + buf = repo_read_object_file(the_repository, oid, &unused, + &size); + } + if (!buf) + die("Can't load object %s", oid_to_hex(oid)); + + /* Peel one layer. */ + switch (oe->type) { + case OBJ_TAG: + if (size < hexsz + strlen("object ") || + get_oid_hex(buf + strlen("object "), oid)) + die("Invalid SHA1 in tag: %s", command_buf.buf); + break; + case OBJ_COMMIT: + if (size < hexsz + strlen("tree ") || + get_oid_hex(buf + strlen("tree "), oid)) + die("Invalid SHA1 in commit: %s", command_buf.buf); + } + + free(buf); + return find_object(oid); +} + +static void insert_mapped_mark(uintmax_t mark, void *object, void *cbp) +{ + struct object_id *fromoid = object; + struct object_id *tooid = find_mark(cbp, mark); + int ret; + khiter_t it; + + it = kh_put_oid_map(sub_oid_map, *fromoid, &ret); + /* We've already seen this object. */ + if (ret == 0) + return; + kh_value(sub_oid_map, it) = tooid; +} + +static void build_mark_map_one(struct mark_set *from, struct mark_set *to) +{ + for_each_mark(from, 0, insert_mapped_mark, to); +} + +static void build_mark_map(struct string_list *from, struct string_list *to) +{ + struct string_list_item *fromp, *top; + + sub_oid_map = kh_init_oid_map(); + + for_each_string_list_item(fromp, from) { + top = string_list_lookup(to, fromp->string); + if (!fromp->util) { + die(_("Missing from marks for submodule '%s'"), fromp->string); + } else if (!top || !top->util) { + die(_("Missing to marks for submodule '%s'"), fromp->string); + } + build_mark_map_one(fromp->util, top->util); + } +} + +static struct object_entry *parse_treeish_dataref(const char **p) +{ + struct object_id oid; + struct object_entry *e; + + if (**p == ':') { /* <mark> */ + e = find_mark(marks, parse_mark_ref_space(p)); + if (!e) + die("Unknown mark: %s", command_buf.buf); + oidcpy(&oid, &e->idx.oid); + } else { /* <sha1> */ + if (parse_mapped_oid_hex(*p, &oid, p)) + die("Invalid dataref: %s", command_buf.buf); + e = find_object(&oid); + if (*(*p)++ != ' ') + die("Missing space after tree-ish: %s", command_buf.buf); + } + + while (!e || e->type != OBJ_TREE) + e = dereference(e, &oid); + return e; +} + +static void print_ls(int mode, const unsigned char *hash, const char *path) +{ + static struct strbuf line = STRBUF_INIT; + + /* See show_tree(). */ + const char *type = + S_ISGITLINK(mode) ? commit_type : + S_ISDIR(mode) ? tree_type : + blob_type; + + if (!mode) { + /* missing SP path LF */ + strbuf_reset(&line); + strbuf_addstr(&line, "missing "); + quote_c_style(path, &line, NULL, 0); + strbuf_addch(&line, '\n'); + } else { + /* mode SP type SP object_name TAB path LF */ + strbuf_reset(&line); + strbuf_addf(&line, "%06o %s %s\t", + mode & ~NO_DELTA, type, hash_to_hex(hash)); + quote_c_style(path, &line, NULL, 0); + strbuf_addch(&line, '\n'); + } + cat_blob_write(line.buf, line.len); +} + +static void parse_ls(const char *p, struct branch *b) +{ + struct tree_entry *root = NULL; + struct tree_entry leaf = {NULL}; + + /* ls SP (<tree-ish> SP)? <path> */ + if (*p == '"') { + if (!b) + die("Not in a commit: %s", command_buf.buf); + root = &b->branch_tree; + } else { + struct object_entry *e = parse_treeish_dataref(&p); + root = new_tree_entry(); + oidcpy(&root->versions[1].oid, &e->idx.oid); + if (!is_null_oid(&root->versions[1].oid)) + root->versions[1].mode = S_IFDIR; + load_tree(root); + } + if (*p == '"') { + static struct strbuf uq = STRBUF_INIT; + const char *endp; + strbuf_reset(&uq); + if (unquote_c_style(&uq, p, &endp)) + die("Invalid path: %s", command_buf.buf); + if (*endp) + die("Garbage after path in: %s", command_buf.buf); + p = uq.buf; + } + tree_content_get(root, p, &leaf, 1); + /* + * A directory in preparation would have a sha1 of zero + * until it is saved. Save, for simplicity. + */ + if (S_ISDIR(leaf.versions[1].mode)) + store_tree(&leaf); + + print_ls(leaf.versions[1].mode, leaf.versions[1].oid.hash, p); + if (leaf.tree) + release_tree_content_recursive(leaf.tree); + if (!b || root != &b->branch_tree) + release_tree_entry(root); +} + +static void checkpoint(void) +{ + checkpoint_requested = 0; + if (object_count) { + cycle_packfile(); + } + dump_branches(); + dump_tags(); + dump_marks(); +} + +static void parse_checkpoint(void) +{ + checkpoint_requested = 1; + skip_optional_lf(); +} + +static void parse_progress(void) +{ + fwrite(command_buf.buf, 1, command_buf.len, stdout); + fputc('\n', stdout); + fflush(stdout); + skip_optional_lf(); +} + +static void parse_alias(void) +{ + struct object_entry *e; + struct branch b; + + skip_optional_lf(); + read_next_command(); + + /* mark ... */ + parse_mark(); + if (!next_mark) + die(_("Expected 'mark' command, got %s"), command_buf.buf); + + /* to ... */ + memset(&b, 0, sizeof(b)); + if (!parse_objectish_with_prefix(&b, "to ")) + die(_("Expected 'to' command, got %s"), command_buf.buf); + e = find_object(&b.oid); + assert(e); + insert_mark(&marks, next_mark, e); +} + +static char* make_fast_import_path(const char *path) +{ + if (!relative_marks_paths || is_absolute_path(path)) + return prefix_filename(global_prefix, path); + return git_pathdup("info/fast-import/%s", path); +} + +static void option_import_marks(const char *marks, + int from_stream, int ignore_missing) +{ + if (import_marks_file) { + if (from_stream) + die("Only one import-marks command allowed per stream"); + + /* read previous mark file */ + if(!import_marks_file_from_stream) + read_marks(); + } + + import_marks_file = make_fast_import_path(marks); + import_marks_file_from_stream = from_stream; + import_marks_file_ignore_missing = ignore_missing; +} + +static void option_date_format(const char *fmt) +{ + if (!strcmp(fmt, "raw")) + whenspec = WHENSPEC_RAW; + else if (!strcmp(fmt, "raw-permissive")) + whenspec = WHENSPEC_RAW_PERMISSIVE; + else if (!strcmp(fmt, "rfc2822")) + whenspec = WHENSPEC_RFC2822; + else if (!strcmp(fmt, "now")) + whenspec = WHENSPEC_NOW; + else + die("unknown --date-format argument %s", fmt); +} + +static unsigned long ulong_arg(const char *option, const char *arg) +{ + char *endptr; + unsigned long rv = strtoul(arg, &endptr, 0); + if (strchr(arg, '-') || endptr == arg || *endptr) + die("%s: argument must be a non-negative integer", option); + return rv; +} + +static void option_depth(const char *depth) +{ + max_depth = ulong_arg("--depth", depth); + if (max_depth > MAX_DEPTH) + die("--depth cannot exceed %u", MAX_DEPTH); +} + +static void option_active_branches(const char *branches) +{ + max_active_branches = ulong_arg("--active-branches", branches); +} + +static void option_export_marks(const char *marks) +{ + export_marks_file = make_fast_import_path(marks); +} + +static void option_cat_blob_fd(const char *fd) +{ + unsigned long n = ulong_arg("--cat-blob-fd", fd); + if (n > (unsigned long) INT_MAX) + die("--cat-blob-fd cannot exceed %d", INT_MAX); + cat_blob_fd = (int) n; +} + +static void option_export_pack_edges(const char *edges) +{ + char *fn = prefix_filename(global_prefix, edges); + if (pack_edges) + fclose(pack_edges); + pack_edges = xfopen(fn, "a"); + free(fn); +} + +static void option_rewrite_submodules(const char *arg, struct string_list *list) +{ + struct mark_set *ms; + FILE *fp; + char *s = xstrdup(arg); + char *f = strchr(s, ':'); + if (!f) + die(_("Expected format name:filename for submodule rewrite option")); + *f = '\0'; + f++; + CALLOC_ARRAY(ms, 1); + + f = prefix_filename(global_prefix, f); + fp = fopen(f, "r"); + if (!fp) + die_errno("cannot read '%s'", f); + read_mark_file(&ms, fp, insert_oid_entry); + fclose(fp); + free(f); + + string_list_insert(list, s)->util = ms; +} + +static int parse_one_option(const char *option) +{ + if (skip_prefix(option, "max-pack-size=", &option)) { + unsigned long v; + if (!git_parse_ulong(option, &v)) + return 0; + if (v < 8192) { + warning("max-pack-size is now in bytes, assuming --max-pack-size=%lum", v); + v *= 1024 * 1024; + } else if (v < 1024 * 1024) { + warning("minimum max-pack-size is 1 MiB"); + v = 1024 * 1024; + } + max_packsize = v; + } else if (skip_prefix(option, "big-file-threshold=", &option)) { + unsigned long v; + if (!git_parse_ulong(option, &v)) + return 0; + big_file_threshold = v; + } else if (skip_prefix(option, "depth=", &option)) { + option_depth(option); + } else if (skip_prefix(option, "active-branches=", &option)) { + option_active_branches(option); + } else if (skip_prefix(option, "export-pack-edges=", &option)) { + option_export_pack_edges(option); + } else if (!strcmp(option, "quiet")) { + show_stats = 0; + } else if (!strcmp(option, "stats")) { + show_stats = 1; + } else if (!strcmp(option, "allow-unsafe-features")) { + ; /* already handled during early option parsing */ + } else { + return 0; + } + + return 1; +} + +static void check_unsafe_feature(const char *feature, int from_stream) +{ + if (from_stream && !allow_unsafe_features) + die(_("feature '%s' forbidden in input without --allow-unsafe-features"), + feature); +} + +static int parse_one_feature(const char *feature, int from_stream) +{ + const char *arg; + + if (skip_prefix(feature, "date-format=", &arg)) { + option_date_format(arg); + } else if (skip_prefix(feature, "import-marks=", &arg)) { + check_unsafe_feature("import-marks", from_stream); + option_import_marks(arg, from_stream, 0); + } else if (skip_prefix(feature, "import-marks-if-exists=", &arg)) { + check_unsafe_feature("import-marks-if-exists", from_stream); + option_import_marks(arg, from_stream, 1); + } else if (skip_prefix(feature, "export-marks=", &arg)) { + check_unsafe_feature(feature, from_stream); + option_export_marks(arg); + } else if (!strcmp(feature, "alias")) { + ; /* Don't die - this feature is supported */ + } else if (skip_prefix(feature, "rewrite-submodules-to=", &arg)) { + option_rewrite_submodules(arg, &sub_marks_to); + } else if (skip_prefix(feature, "rewrite-submodules-from=", &arg)) { + option_rewrite_submodules(arg, &sub_marks_from); + } else if (!strcmp(feature, "get-mark")) { + ; /* Don't die - this feature is supported */ + } else if (!strcmp(feature, "cat-blob")) { + ; /* Don't die - this feature is supported */ + } else if (!strcmp(feature, "relative-marks")) { + relative_marks_paths = 1; + } else if (!strcmp(feature, "no-relative-marks")) { + relative_marks_paths = 0; + } else if (!strcmp(feature, "done")) { + require_explicit_termination = 1; + } else if (!strcmp(feature, "force")) { + force_update = 1; + } else if (!strcmp(feature, "notes") || !strcmp(feature, "ls")) { + ; /* do nothing; we have the feature */ + } else { + return 0; + } + + return 1; +} + +static void parse_feature(const char *feature) +{ + if (seen_data_command) + die("Got feature command '%s' after data command", feature); + + if (parse_one_feature(feature, 1)) + return; + + die("This version of fast-import does not support feature %s.", feature); +} + +static void parse_option(const char *option) +{ + if (seen_data_command) + die("Got option command '%s' after data command", option); + + if (parse_one_option(option)) + return; + + die("This version of fast-import does not support option: %s", option); +} + +static void git_pack_config(void) +{ + int indexversion_value; + int limit; + unsigned long packsizelimit_value; + + if (!git_config_get_ulong("pack.depth", &max_depth)) { + if (max_depth > MAX_DEPTH) + max_depth = MAX_DEPTH; + } + if (!git_config_get_int("pack.indexversion", &indexversion_value)) { + pack_idx_opts.version = indexversion_value; + if (pack_idx_opts.version > 2) + git_die_config("pack.indexversion", + "bad pack.indexVersion=%"PRIu32, pack_idx_opts.version); + } + if (!git_config_get_ulong("pack.packsizelimit", &packsizelimit_value)) + max_packsize = packsizelimit_value; + + if (!git_config_get_int("fastimport.unpacklimit", &limit)) + unpack_limit = limit; + else if (!git_config_get_int("transfer.unpacklimit", &limit)) + unpack_limit = limit; + + git_config(git_default_config, NULL); +} + +static const char fast_import_usage[] = +"git fast-import [--date-format=<f>] [--max-pack-size=<n>] [--big-file-threshold=<n>] [--depth=<n>] [--active-branches=<n>] [--export-marks=<marks.file>]"; + +static void parse_argv(void) +{ + unsigned int i; + + for (i = 1; i < global_argc; i++) { + const char *a = global_argv[i]; + + if (*a != '-' || !strcmp(a, "--")) + break; + + if (!skip_prefix(a, "--", &a)) + die("unknown option %s", a); + + if (parse_one_option(a)) + continue; + + if (parse_one_feature(a, 0)) + continue; + + if (skip_prefix(a, "cat-blob-fd=", &a)) { + option_cat_blob_fd(a); + continue; + } + + die("unknown option --%s", a); + } + if (i != global_argc) + usage(fast_import_usage); + + seen_data_command = 1; + if (import_marks_file) + read_marks(); + build_mark_map(&sub_marks_from, &sub_marks_to); +} + +int cmd_fast_import(int argc, const char **argv, const char *prefix) +{ + unsigned int i; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage(fast_import_usage); + + reset_pack_idx_option(&pack_idx_opts); + git_pack_config(); + + alloc_objects(object_entry_alloc); + strbuf_init(&command_buf, 0); + CALLOC_ARRAY(atom_table, atom_table_sz); + CALLOC_ARRAY(branch_table, branch_table_sz); + CALLOC_ARRAY(avail_tree_table, avail_tree_table_sz); + marks = mem_pool_calloc(&fi_mem_pool, 1, sizeof(struct mark_set)); + + hashmap_init(&object_table, object_entry_hashcmp, NULL, 0); + + /* + * We don't parse most options until after we've seen the set of + * "feature" lines at the start of the stream (which allows the command + * line to override stream data). But we must do an early parse of any + * command-line options that impact how we interpret the feature lines. + */ + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (*arg != '-' || !strcmp(arg, "--")) + break; + if (!strcmp(arg, "--allow-unsafe-features")) + allow_unsafe_features = 1; + } + + global_argc = argc; + global_argv = argv; + global_prefix = prefix; + + rc_free = mem_pool_alloc(&fi_mem_pool, cmd_save * sizeof(*rc_free)); + for (i = 0; i < (cmd_save - 1); i++) + rc_free[i].next = &rc_free[i + 1]; + rc_free[cmd_save - 1].next = NULL; + + start_packfile(); + set_die_routine(die_nicely); + set_checkpoint_signal(); + while (read_next_command() != EOF) { + const char *v; + if (!strcmp("blob", command_buf.buf)) + parse_new_blob(); + else if (skip_prefix(command_buf.buf, "commit ", &v)) + parse_new_commit(v); + else if (skip_prefix(command_buf.buf, "tag ", &v)) + parse_new_tag(v); + else if (skip_prefix(command_buf.buf, "reset ", &v)) + parse_reset_branch(v); + else if (skip_prefix(command_buf.buf, "ls ", &v)) + parse_ls(v, NULL); + else if (skip_prefix(command_buf.buf, "cat-blob ", &v)) + parse_cat_blob(v); + else if (skip_prefix(command_buf.buf, "get-mark ", &v)) + parse_get_mark(v); + else if (!strcmp("checkpoint", command_buf.buf)) + parse_checkpoint(); + else if (!strcmp("done", command_buf.buf)) + break; + else if (!strcmp("alias", command_buf.buf)) + parse_alias(); + else if (starts_with(command_buf.buf, "progress ")) + parse_progress(); + else if (skip_prefix(command_buf.buf, "feature ", &v)) + parse_feature(v); + else if (skip_prefix(command_buf.buf, "option git ", &v)) + parse_option(v); + else if (starts_with(command_buf.buf, "option ")) + /* ignore non-git options*/; + else + die("Unsupported command: %s", command_buf.buf); + + if (checkpoint_requested) + checkpoint(); + } + + /* argv hasn't been parsed yet, do so */ + if (!seen_data_command) + parse_argv(); + + if (require_explicit_termination && feof(stdin)) + die("stream ends early"); + + end_packfile(); + + dump_branches(); + dump_tags(); + unkeep_all_packs(); + dump_marks(); + + if (pack_edges) + fclose(pack_edges); + + if (show_stats) { + uintmax_t total_count = 0, duplicate_count = 0; + for (i = 0; i < ARRAY_SIZE(object_count_by_type); i++) + total_count += object_count_by_type[i]; + for (i = 0; i < ARRAY_SIZE(duplicate_count_by_type); i++) + duplicate_count += duplicate_count_by_type[i]; + + fprintf(stderr, "%s statistics:\n", argv[0]); + fprintf(stderr, "---------------------------------------------------------------------\n"); + fprintf(stderr, "Alloc'd objects: %10" PRIuMAX "\n", alloc_count); + fprintf(stderr, "Total objects: %10" PRIuMAX " (%10" PRIuMAX " duplicates )\n", total_count, duplicate_count); + fprintf(stderr, " blobs : %10" PRIuMAX " (%10" PRIuMAX " duplicates %10" PRIuMAX " deltas of %10" PRIuMAX" attempts)\n", object_count_by_type[OBJ_BLOB], duplicate_count_by_type[OBJ_BLOB], delta_count_by_type[OBJ_BLOB], delta_count_attempts_by_type[OBJ_BLOB]); + fprintf(stderr, " trees : %10" PRIuMAX " (%10" PRIuMAX " duplicates %10" PRIuMAX " deltas of %10" PRIuMAX" attempts)\n", object_count_by_type[OBJ_TREE], duplicate_count_by_type[OBJ_TREE], delta_count_by_type[OBJ_TREE], delta_count_attempts_by_type[OBJ_TREE]); + fprintf(stderr, " commits: %10" PRIuMAX " (%10" PRIuMAX " duplicates %10" PRIuMAX " deltas of %10" PRIuMAX" attempts)\n", object_count_by_type[OBJ_COMMIT], duplicate_count_by_type[OBJ_COMMIT], delta_count_by_type[OBJ_COMMIT], delta_count_attempts_by_type[OBJ_COMMIT]); + fprintf(stderr, " tags : %10" PRIuMAX " (%10" PRIuMAX " duplicates %10" PRIuMAX " deltas of %10" PRIuMAX" attempts)\n", object_count_by_type[OBJ_TAG], duplicate_count_by_type[OBJ_TAG], delta_count_by_type[OBJ_TAG], delta_count_attempts_by_type[OBJ_TAG]); + fprintf(stderr, "Total branches: %10lu (%10lu loads )\n", branch_count, branch_load_count); + fprintf(stderr, " marks: %10" PRIuMAX " (%10" PRIuMAX " unique )\n", (((uintmax_t)1) << marks->shift) * 1024, marks_set_count); + fprintf(stderr, " atoms: %10u\n", atom_cnt); + fprintf(stderr, "Memory total: %10" PRIuMAX " KiB\n", (tree_entry_allocd + fi_mem_pool.pool_alloc + alloc_count*sizeof(struct object_entry))/1024); + fprintf(stderr, " pools: %10lu KiB\n", (unsigned long)((tree_entry_allocd + fi_mem_pool.pool_alloc) /1024)); + fprintf(stderr, " objects: %10" PRIuMAX " KiB\n", (alloc_count*sizeof(struct object_entry))/1024); + fprintf(stderr, "---------------------------------------------------------------------\n"); + pack_report(); + fprintf(stderr, "---------------------------------------------------------------------\n"); + fprintf(stderr, "\n"); + } + + return failure ? 1 : 0; +} diff --git a/builtin/fetch-pack.c b/builtin/fetch-pack.c new file mode 100644 index 0000000..44c05ee --- /dev/null +++ b/builtin/fetch-pack.c @@ -0,0 +1,280 @@ +#include "builtin.h" +#include "gettext.h" +#include "hex.h" +#include "object-file.h" +#include "pkt-line.h" +#include "fetch-pack.h" +#include "remote.h" +#include "connect.h" +#include "oid-array.h" +#include "protocol.h" + +static const char fetch_pack_usage[] = +"git fetch-pack [--all] [--stdin] [--quiet | -q] [--keep | -k] [--thin] " +"[--include-tag] [--upload-pack=<git-upload-pack>] [--depth=<n>] " +"[--no-progress] [--diag-url] [-v] [<host>:]<directory> [<refs>...]"; + +static void add_sought_entry(struct ref ***sought, int *nr, int *alloc, + const char *name) +{ + struct ref *ref; + struct object_id oid; + const char *p; + + if (!parse_oid_hex(name, &oid, &p)) { + if (*p == ' ') { + /* <oid> <ref>, find refname */ + name = p + 1; + } else if (*p == '\0') { + ; /* <oid>, leave oid as name */ + } else { + /* <ref>, clear cruft from oid */ + oidclr(&oid); + } + } else { + /* <ref>, clear cruft from get_oid_hex */ + oidclr(&oid); + } + + ref = alloc_ref(name); + oidcpy(&ref->old_oid, &oid); + (*nr)++; + ALLOC_GROW(*sought, *nr, *alloc); + (*sought)[*nr - 1] = ref; +} + +int cmd_fetch_pack(int argc, const char **argv, const char *prefix UNUSED) +{ + int i, ret; + struct ref *ref = NULL; + const char *dest = NULL; + struct ref **sought = NULL; + int nr_sought = 0, alloc_sought = 0; + int fd[2]; + struct string_list pack_lockfiles = STRING_LIST_INIT_DUP; + struct string_list *pack_lockfiles_ptr = NULL; + struct child_process *conn; + struct fetch_pack_args args; + struct oid_array shallow = OID_ARRAY_INIT; + struct string_list deepen_not = STRING_LIST_INIT_DUP; + struct packet_reader reader; + enum protocol_version version; + + fetch_if_missing = 0; + + packet_trace_identity("fetch-pack"); + + memset(&args, 0, sizeof(args)); + list_objects_filter_init(&args.filter_options); + args.uploadpack = "git-upload-pack"; + + for (i = 1; i < argc && *argv[i] == '-'; i++) { + const char *arg = argv[i]; + + if (skip_prefix(arg, "--upload-pack=", &arg)) { + args.uploadpack = arg; + continue; + } + if (skip_prefix(arg, "--exec=", &arg)) { + args.uploadpack = arg; + continue; + } + if (!strcmp("--quiet", arg) || !strcmp("-q", arg)) { + args.quiet = 1; + continue; + } + if (!strcmp("--keep", arg) || !strcmp("-k", arg)) { + args.lock_pack = args.keep_pack; + args.keep_pack = 1; + continue; + } + if (!strcmp("--thin", arg)) { + args.use_thin_pack = 1; + continue; + } + if (!strcmp("--include-tag", arg)) { + args.include_tag = 1; + continue; + } + if (!strcmp("--all", arg)) { + args.fetch_all = 1; + continue; + } + if (!strcmp("--stdin", arg)) { + args.stdin_refs = 1; + continue; + } + if (!strcmp("--diag-url", arg)) { + args.diag_url = 1; + continue; + } + if (!strcmp("-v", arg)) { + args.verbose = 1; + continue; + } + if (skip_prefix(arg, "--depth=", &arg)) { + args.depth = strtol(arg, NULL, 0); + continue; + } + if (skip_prefix(arg, "--shallow-since=", &arg)) { + args.deepen_since = xstrdup(arg); + continue; + } + if (skip_prefix(arg, "--shallow-exclude=", &arg)) { + string_list_append(&deepen_not, arg); + continue; + } + if (!strcmp(arg, "--deepen-relative")) { + args.deepen_relative = 1; + continue; + } + if (!strcmp("--no-progress", arg)) { + args.no_progress = 1; + continue; + } + if (!strcmp("--stateless-rpc", arg)) { + args.stateless_rpc = 1; + continue; + } + if (!strcmp("--lock-pack", arg)) { + args.lock_pack = 1; + pack_lockfiles_ptr = &pack_lockfiles; + continue; + } + if (!strcmp("--check-self-contained-and-connected", arg)) { + args.check_self_contained_and_connected = 1; + continue; + } + if (!strcmp("--cloning", arg)) { + args.cloning = 1; + continue; + } + if (!strcmp("--update-shallow", arg)) { + args.update_shallow = 1; + continue; + } + if (!strcmp("--from-promisor", arg)) { + args.from_promisor = 1; + continue; + } + if (!strcmp("--refetch", arg)) { + args.refetch = 1; + continue; + } + if (skip_prefix(arg, ("--filter="), &arg)) { + parse_list_objects_filter(&args.filter_options, arg); + continue; + } + if (!strcmp(arg, ("--no-filter"))) { + list_objects_filter_set_no_filter(&args.filter_options); + continue; + } + usage(fetch_pack_usage); + } + if (deepen_not.nr) + args.deepen_not = &deepen_not; + + if (i < argc) + dest = argv[i++]; + else + usage(fetch_pack_usage); + + /* + * Copy refs from cmdline to growable list, then append any + * refs from the standard input: + */ + for (; i < argc; i++) + add_sought_entry(&sought, &nr_sought, &alloc_sought, argv[i]); + if (args.stdin_refs) { + if (args.stateless_rpc) { + /* in stateless RPC mode we use pkt-line to read + * from stdin, until we get a flush packet + */ + for (;;) { + char *line = packet_read_line(0, NULL); + if (!line) + break; + add_sought_entry(&sought, &nr_sought, &alloc_sought, line); + } + } + else { + /* read from stdin one ref per line, until EOF */ + struct strbuf line = STRBUF_INIT; + while (strbuf_getline_lf(&line, stdin) != EOF) + add_sought_entry(&sought, &nr_sought, &alloc_sought, line.buf); + strbuf_release(&line); + } + } + + if (args.stateless_rpc) { + conn = NULL; + fd[0] = 0; + fd[1] = 1; + } else { + int flags = args.verbose ? CONNECT_VERBOSE : 0; + if (args.diag_url) + flags |= CONNECT_DIAG_URL; + conn = git_connect(fd, dest, "git-upload-pack", + args.uploadpack, flags); + if (!conn) + return args.diag_url ? 0 : 1; + } + + packet_reader_init(&reader, fd[0], NULL, 0, + PACKET_READ_CHOMP_NEWLINE | + PACKET_READ_GENTLE_ON_EOF | + PACKET_READ_DIE_ON_ERR_PACKET); + + version = discover_version(&reader); + switch (version) { + case protocol_v2: + get_remote_refs(fd[1], &reader, &ref, 0, NULL, NULL, + args.stateless_rpc); + break; + case protocol_v1: + case protocol_v0: + get_remote_heads(&reader, &ref, 0, NULL, &shallow); + break; + case protocol_unknown_version: + BUG("unknown protocol version"); + } + + ref = fetch_pack(&args, fd, ref, sought, nr_sought, + &shallow, pack_lockfiles_ptr, version); + if (pack_lockfiles.nr) { + int i; + + printf("lock %s\n", pack_lockfiles.items[0].string); + fflush(stdout); + for (i = 1; i < pack_lockfiles.nr; i++) + warning(_("Lockfile created but not reported: %s"), + pack_lockfiles.items[i].string); + } + if (args.check_self_contained_and_connected && + args.self_contained_and_connected) { + printf("connectivity-ok\n"); + fflush(stdout); + } + close(fd[0]); + close(fd[1]); + if (finish_connect(conn)) + return 1; + + ret = !ref; + + /* + * If the heads to pull were given, we should have consumed + * all of them by matching the remote. Otherwise, 'git fetch + * remote no-such-ref' would silently succeed without issuing + * an error. + */ + ret |= report_unmatched_refs(sought, nr_sought); + + while (ref) { + printf("%s %s\n", + oid_to_hex(&ref->old_oid), ref->name); + ref = ref->next; + } + + return ret; +} diff --git a/builtin/fetch.c b/builtin/fetch.c new file mode 100644 index 0000000..fd134ba --- /dev/null +++ b/builtin/fetch.c @@ -0,0 +1,2499 @@ +/* + * "git fetch" + */ +#include "builtin.h" +#include "advice.h" +#include "config.h" +#include "gettext.h" +#include "environment.h" +#include "hex.h" +#include "repository.h" +#include "refs.h" +#include "refspec.h" +#include "object-name.h" +#include "object-store-ll.h" +#include "oidset.h" +#include "oid-array.h" +#include "commit.h" +#include "string-list.h" +#include "remote.h" +#include "transport.h" +#include "run-command.h" +#include "parse-options.h" +#include "sigchain.h" +#include "submodule-config.h" +#include "submodule.h" +#include "connected.h" +#include "strvec.h" +#include "utf8.h" +#include "packfile.h" +#include "pager.h" +#include "path.h" +#include "pkt-line.h" +#include "list-objects-filter-options.h" +#include "commit-reach.h" +#include "branch.h" +#include "promisor-remote.h" +#include "commit-graph.h" +#include "shallow.h" +#include "trace.h" +#include "trace2.h" +#include "worktree.h" +#include "bundle-uri.h" + +#define FORCED_UPDATES_DELAY_WARNING_IN_MS (10 * 1000) + +static const char * const builtin_fetch_usage[] = { + N_("git fetch [<options>] [<repository> [<refspec>...]]"), + N_("git fetch [<options>] <group>"), + N_("git fetch --multiple [<options>] [(<repository> | <group>)...]"), + N_("git fetch --all [<options>]"), + NULL +}; + +enum { + TAGS_UNSET = 0, + TAGS_DEFAULT = 1, + TAGS_SET = 2 +}; + +enum display_format { + DISPLAY_FORMAT_FULL, + DISPLAY_FORMAT_COMPACT, + DISPLAY_FORMAT_PORCELAIN, +}; + +struct display_state { + struct strbuf buf; + + int refcol_width; + enum display_format format; + + char *url; + int url_len, shown_url; +}; + +static uint64_t forced_updates_ms = 0; +static int prefetch = 0; +static int prune = -1; /* unspecified */ +#define PRUNE_BY_DEFAULT 0 /* do we prune by default? */ + +static int prune_tags = -1; /* unspecified */ +#define PRUNE_TAGS_BY_DEFAULT 0 /* do we prune tags by default? */ + +static int append, dry_run, force, keep, update_head_ok; +static int write_fetch_head = 1; +static int verbosity, deepen_relative, set_upstream, refetch; +static int progress = -1; +static int tags = TAGS_DEFAULT, update_shallow, deepen; +static int atomic_fetch; +static enum transport_family family; +static const char *depth; +static const char *deepen_since; +static const char *upload_pack; +static struct string_list deepen_not = STRING_LIST_INIT_NODUP; +static struct strbuf default_rla = STRBUF_INIT; +static struct transport *gtransport; +static struct transport *gsecondary; +static struct refspec refmap = REFSPEC_INIT_FETCH; +static struct list_objects_filter_options filter_options = LIST_OBJECTS_FILTER_INIT; +static struct string_list server_options = STRING_LIST_INIT_DUP; +static struct string_list negotiation_tip = STRING_LIST_INIT_NODUP; + +struct fetch_config { + enum display_format display_format; + int prune; + int prune_tags; + int show_forced_updates; + int recurse_submodules; + int parallel; + int submodule_fetch_jobs; +}; + +static int git_fetch_config(const char *k, const char *v, + const struct config_context *ctx, void *cb) +{ + struct fetch_config *fetch_config = cb; + + if (!strcmp(k, "fetch.prune")) { + fetch_config->prune = git_config_bool(k, v); + return 0; + } + + if (!strcmp(k, "fetch.prunetags")) { + fetch_config->prune_tags = git_config_bool(k, v); + return 0; + } + + if (!strcmp(k, "fetch.showforcedupdates")) { + fetch_config->show_forced_updates = git_config_bool(k, v); + return 0; + } + + if (!strcmp(k, "submodule.recurse")) { + int r = git_config_bool(k, v) ? + RECURSE_SUBMODULES_ON : RECURSE_SUBMODULES_OFF; + fetch_config->recurse_submodules = r; + } + + if (!strcmp(k, "submodule.fetchjobs")) { + fetch_config->submodule_fetch_jobs = parse_submodule_fetchjobs(k, v, ctx->kvi); + return 0; + } else if (!strcmp(k, "fetch.recursesubmodules")) { + fetch_config->recurse_submodules = parse_fetch_recurse_submodules_arg(k, v); + return 0; + } + + if (!strcmp(k, "fetch.parallel")) { + fetch_config->parallel = git_config_int(k, v, ctx->kvi); + if (fetch_config->parallel < 0) + die(_("fetch.parallel cannot be negative")); + if (!fetch_config->parallel) + fetch_config->parallel = online_cpus(); + return 0; + } + + if (!strcmp(k, "fetch.output")) { + if (!v) + return config_error_nonbool(k); + else if (!strcasecmp(v, "full")) + fetch_config->display_format = DISPLAY_FORMAT_FULL; + else if (!strcasecmp(v, "compact")) + fetch_config->display_format = DISPLAY_FORMAT_COMPACT; + else + die(_("invalid value for '%s': '%s'"), + "fetch.output", v); + } + + return git_default_config(k, v, ctx, cb); +} + +static int parse_refmap_arg(const struct option *opt, const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + + /* + * "git fetch --refmap='' origin foo" + * can be used to tell the command not to store anywhere + */ + refspec_append(opt->value, arg); + + return 0; +} + +static void unlock_pack(unsigned int flags) +{ + if (gtransport) + transport_unlock_pack(gtransport, flags); + if (gsecondary) + transport_unlock_pack(gsecondary, flags); +} + +static void unlock_pack_atexit(void) +{ + unlock_pack(0); +} + +static void unlock_pack_on_signal(int signo) +{ + unlock_pack(TRANSPORT_UNLOCK_PACK_IN_SIGNAL_HANDLER); + sigchain_pop(signo); + raise(signo); +} + +static void add_merge_config(struct ref **head, + const struct ref *remote_refs, + struct branch *branch, + struct ref ***tail) +{ + int i; + + for (i = 0; i < branch->merge_nr; i++) { + struct ref *rm, **old_tail = *tail; + struct refspec_item refspec; + + for (rm = *head; rm; rm = rm->next) { + if (branch_merge_matches(branch, i, rm->name)) { + rm->fetch_head_status = FETCH_HEAD_MERGE; + break; + } + } + if (rm) + continue; + + /* + * Not fetched to a remote-tracking branch? We need to fetch + * it anyway to allow this branch's "branch.$name.merge" + * to be honored by 'git pull', but we do not have to + * fail if branch.$name.merge is misconfigured to point + * at a nonexisting branch. If we were indeed called by + * 'git pull', it will notice the misconfiguration because + * there is no entry in the resulting FETCH_HEAD marked + * for merging. + */ + memset(&refspec, 0, sizeof(refspec)); + refspec.src = branch->merge[i]->src; + get_fetch_map(remote_refs, &refspec, tail, 1); + for (rm = *old_tail; rm; rm = rm->next) + rm->fetch_head_status = FETCH_HEAD_MERGE; + } +} + +static void create_fetch_oidset(struct ref **head, struct oidset *out) +{ + struct ref *rm = *head; + while (rm) { + oidset_insert(out, &rm->old_oid); + rm = rm->next; + } +} + +struct refname_hash_entry { + struct hashmap_entry ent; + struct object_id oid; + int ignore; + char refname[FLEX_ARRAY]; +}; + +static int refname_hash_entry_cmp(const void *hashmap_cmp_fn_data UNUSED, + const struct hashmap_entry *eptr, + const struct hashmap_entry *entry_or_key, + const void *keydata) +{ + const struct refname_hash_entry *e1, *e2; + + e1 = container_of(eptr, const struct refname_hash_entry, ent); + e2 = container_of(entry_or_key, const struct refname_hash_entry, ent); + return strcmp(e1->refname, keydata ? keydata : e2->refname); +} + +static struct refname_hash_entry *refname_hash_add(struct hashmap *map, + const char *refname, + const struct object_id *oid) +{ + struct refname_hash_entry *ent; + size_t len = strlen(refname); + + FLEX_ALLOC_MEM(ent, refname, refname, len); + hashmap_entry_init(&ent->ent, strhash(refname)); + oidcpy(&ent->oid, oid); + hashmap_add(map, &ent->ent); + return ent; +} + +static int add_one_refname(const char *refname, + const struct object_id *oid, + int flag UNUSED, void *cbdata) +{ + struct hashmap *refname_map = cbdata; + + (void) refname_hash_add(refname_map, refname, oid); + return 0; +} + +static void refname_hash_init(struct hashmap *map) +{ + hashmap_init(map, refname_hash_entry_cmp, NULL, 0); +} + +static int refname_hash_exists(struct hashmap *map, const char *refname) +{ + return !!hashmap_get_from_hash(map, strhash(refname), refname); +} + +static void clear_item(struct refname_hash_entry *item) +{ + item->ignore = 1; +} + + +static void add_already_queued_tags(const char *refname, + const struct object_id *old_oid UNUSED, + const struct object_id *new_oid, + void *cb_data) +{ + struct hashmap *queued_tags = cb_data; + if (starts_with(refname, "refs/tags/") && new_oid) + (void) refname_hash_add(queued_tags, refname, new_oid); +} + +static void find_non_local_tags(const struct ref *refs, + struct ref_transaction *transaction, + struct ref **head, + struct ref ***tail) +{ + struct hashmap existing_refs; + struct hashmap remote_refs; + struct oidset fetch_oids = OIDSET_INIT; + struct string_list remote_refs_list = STRING_LIST_INIT_NODUP; + struct string_list_item *remote_ref_item; + const struct ref *ref; + struct refname_hash_entry *item = NULL; + const int quick_flags = OBJECT_INFO_QUICK | OBJECT_INFO_SKIP_FETCH_OBJECT; + + refname_hash_init(&existing_refs); + refname_hash_init(&remote_refs); + create_fetch_oidset(head, &fetch_oids); + + for_each_ref(add_one_refname, &existing_refs); + + /* + * If we already have a transaction, then we need to filter out all + * tags which have already been queued up. + */ + if (transaction) + ref_transaction_for_each_queued_update(transaction, + add_already_queued_tags, + &existing_refs); + + for (ref = refs; ref; ref = ref->next) { + if (!starts_with(ref->name, "refs/tags/")) + continue; + + /* + * The peeled ref always follows the matching base + * ref, so if we see a peeled ref that we don't want + * to fetch then we can mark the ref entry in the list + * as one to ignore by setting util to NULL. + */ + if (ends_with(ref->name, "^{}")) { + if (item && + !repo_has_object_file_with_flags(the_repository, &ref->old_oid, quick_flags) && + !oidset_contains(&fetch_oids, &ref->old_oid) && + !repo_has_object_file_with_flags(the_repository, &item->oid, quick_flags) && + !oidset_contains(&fetch_oids, &item->oid)) + clear_item(item); + item = NULL; + continue; + } + + /* + * If item is non-NULL here, then we previously saw a + * ref not followed by a peeled reference, so we need + * to check if it is a lightweight tag that we want to + * fetch. + */ + if (item && + !repo_has_object_file_with_flags(the_repository, &item->oid, quick_flags) && + !oidset_contains(&fetch_oids, &item->oid)) + clear_item(item); + + item = NULL; + + /* skip duplicates and refs that we already have */ + if (refname_hash_exists(&remote_refs, ref->name) || + refname_hash_exists(&existing_refs, ref->name)) + continue; + + item = refname_hash_add(&remote_refs, ref->name, &ref->old_oid); + string_list_insert(&remote_refs_list, ref->name); + } + hashmap_clear_and_free(&existing_refs, struct refname_hash_entry, ent); + + /* + * We may have a final lightweight tag that needs to be + * checked to see if it needs fetching. + */ + if (item && + !repo_has_object_file_with_flags(the_repository, &item->oid, quick_flags) && + !oidset_contains(&fetch_oids, &item->oid)) + clear_item(item); + + /* + * For all the tags in the remote_refs_list, + * add them to the list of refs to be fetched + */ + for_each_string_list_item(remote_ref_item, &remote_refs_list) { + const char *refname = remote_ref_item->string; + struct ref *rm; + unsigned int hash = strhash(refname); + + item = hashmap_get_entry_from_hash(&remote_refs, hash, refname, + struct refname_hash_entry, ent); + if (!item) + BUG("unseen remote ref?"); + + /* Unless we have already decided to ignore this item... */ + if (item->ignore) + continue; + + rm = alloc_ref(item->refname); + rm->peer_ref = alloc_ref(item->refname); + oidcpy(&rm->old_oid, &item->oid); + **tail = rm; + *tail = &rm->next; + } + hashmap_clear_and_free(&remote_refs, struct refname_hash_entry, ent); + string_list_clear(&remote_refs_list, 0); + oidset_clear(&fetch_oids); +} + +static void filter_prefetch_refspec(struct refspec *rs) +{ + int i; + + if (!prefetch) + return; + + for (i = 0; i < rs->nr; i++) { + struct strbuf new_dst = STRBUF_INIT; + char *old_dst; + const char *sub = NULL; + + if (rs->items[i].negative) + continue; + if (!rs->items[i].dst || + (rs->items[i].src && + !strncmp(rs->items[i].src, + ref_namespace[NAMESPACE_TAGS].ref, + strlen(ref_namespace[NAMESPACE_TAGS].ref)))) { + int j; + + free(rs->items[i].src); + free(rs->items[i].dst); + + for (j = i + 1; j < rs->nr; j++) { + rs->items[j - 1] = rs->items[j]; + rs->raw[j - 1] = rs->raw[j]; + } + rs->nr--; + i--; + continue; + } + + old_dst = rs->items[i].dst; + strbuf_addstr(&new_dst, ref_namespace[NAMESPACE_PREFETCH].ref); + + /* + * If old_dst starts with "refs/", then place + * sub after that prefix. Otherwise, start at + * the beginning of the string. + */ + if (!skip_prefix(old_dst, "refs/", &sub)) + sub = old_dst; + strbuf_addstr(&new_dst, sub); + + rs->items[i].dst = strbuf_detach(&new_dst, NULL); + rs->items[i].force = 1; + + free(old_dst); + } +} + +static struct ref *get_ref_map(struct remote *remote, + const struct ref *remote_refs, + struct refspec *rs, + int tags, int *autotags) +{ + int i; + struct ref *rm; + struct ref *ref_map = NULL; + struct ref **tail = &ref_map; + + /* opportunistically-updated references: */ + struct ref *orefs = NULL, **oref_tail = &orefs; + + struct hashmap existing_refs; + int existing_refs_populated = 0; + + filter_prefetch_refspec(rs); + if (remote) + filter_prefetch_refspec(&remote->fetch); + + if (rs->nr) { + struct refspec *fetch_refspec; + + for (i = 0; i < rs->nr; i++) { + get_fetch_map(remote_refs, &rs->items[i], &tail, 0); + if (rs->items[i].dst && rs->items[i].dst[0]) + *autotags = 1; + } + /* Merge everything on the command line (but not --tags) */ + for (rm = ref_map; rm; rm = rm->next) + rm->fetch_head_status = FETCH_HEAD_MERGE; + + /* + * For any refs that we happen to be fetching via + * command-line arguments, the destination ref might + * have been missing or have been different than the + * remote-tracking ref that would be derived from the + * configured refspec. In these cases, we want to + * take the opportunity to update their configured + * remote-tracking reference. However, we do not want + * to mention these entries in FETCH_HEAD at all, as + * they would simply be duplicates of existing + * entries, so we set them FETCH_HEAD_IGNORE below. + * + * We compute these entries now, based only on the + * refspecs specified on the command line. But we add + * them to the list following the refspecs resulting + * from the tags option so that one of the latter, + * which has FETCH_HEAD_NOT_FOR_MERGE, is not removed + * by ref_remove_duplicates() in favor of one of these + * opportunistic entries with FETCH_HEAD_IGNORE. + */ + if (refmap.nr) + fetch_refspec = &refmap; + else + fetch_refspec = &remote->fetch; + + for (i = 0; i < fetch_refspec->nr; i++) + get_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1); + } else if (refmap.nr) { + die("--refmap option is only meaningful with command-line refspec(s)"); + } else { + /* Use the defaults */ + struct branch *branch = branch_get(NULL); + int has_merge = branch_has_merge_config(branch); + if (remote && + (remote->fetch.nr || + /* Note: has_merge implies non-NULL branch->remote_name */ + (has_merge && !strcmp(branch->remote_name, remote->name)))) { + for (i = 0; i < remote->fetch.nr; i++) { + get_fetch_map(remote_refs, &remote->fetch.items[i], &tail, 0); + if (remote->fetch.items[i].dst && + remote->fetch.items[i].dst[0]) + *autotags = 1; + if (!i && !has_merge && ref_map && + !remote->fetch.items[0].pattern) + ref_map->fetch_head_status = FETCH_HEAD_MERGE; + } + /* + * if the remote we're fetching from is the same + * as given in branch.<name>.remote, we add the + * ref given in branch.<name>.merge, too. + * + * Note: has_merge implies non-NULL branch->remote_name + */ + if (has_merge && + !strcmp(branch->remote_name, remote->name)) + add_merge_config(&ref_map, remote_refs, branch, &tail); + } else if (!prefetch) { + ref_map = get_remote_ref(remote_refs, "HEAD"); + if (!ref_map) + die(_("couldn't find remote ref HEAD")); + ref_map->fetch_head_status = FETCH_HEAD_MERGE; + tail = &ref_map->next; + } + } + + if (tags == TAGS_SET) + /* also fetch all tags */ + get_fetch_map(remote_refs, tag_refspec, &tail, 0); + else if (tags == TAGS_DEFAULT && *autotags) + find_non_local_tags(remote_refs, NULL, &ref_map, &tail); + + /* Now append any refs to be updated opportunistically: */ + *tail = orefs; + for (rm = orefs; rm; rm = rm->next) { + rm->fetch_head_status = FETCH_HEAD_IGNORE; + tail = &rm->next; + } + + /* + * apply negative refspecs first, before we remove duplicates. This is + * necessary as negative refspecs might remove an otherwise conflicting + * duplicate. + */ + if (rs->nr) + ref_map = apply_negative_refspecs(ref_map, rs); + else + ref_map = apply_negative_refspecs(ref_map, &remote->fetch); + + ref_map = ref_remove_duplicates(ref_map); + + for (rm = ref_map; rm; rm = rm->next) { + if (rm->peer_ref) { + const char *refname = rm->peer_ref->name; + struct refname_hash_entry *peer_item; + unsigned int hash = strhash(refname); + + if (!existing_refs_populated) { + refname_hash_init(&existing_refs); + for_each_ref(add_one_refname, &existing_refs); + existing_refs_populated = 1; + } + + peer_item = hashmap_get_entry_from_hash(&existing_refs, + hash, refname, + struct refname_hash_entry, ent); + if (peer_item) { + struct object_id *old_oid = &peer_item->oid; + oidcpy(&rm->peer_ref->old_oid, old_oid); + } + } + } + if (existing_refs_populated) + hashmap_clear_and_free(&existing_refs, struct refname_hash_entry, ent); + + return ref_map; +} + +#define STORE_REF_ERROR_OTHER 1 +#define STORE_REF_ERROR_DF_CONFLICT 2 + +static int s_update_ref(const char *action, + struct ref *ref, + struct ref_transaction *transaction, + int check_old) +{ + char *msg; + char *rla = getenv("GIT_REFLOG_ACTION"); + struct ref_transaction *our_transaction = NULL; + struct strbuf err = STRBUF_INIT; + int ret; + + if (dry_run) + return 0; + if (!rla) + rla = default_rla.buf; + msg = xstrfmt("%s: %s", rla, action); + + /* + * If no transaction was passed to us, we manage the transaction + * ourselves. Otherwise, we trust the caller to handle the transaction + * lifecycle. + */ + if (!transaction) { + transaction = our_transaction = ref_transaction_begin(&err); + if (!transaction) { + ret = STORE_REF_ERROR_OTHER; + goto out; + } + } + + ret = ref_transaction_update(transaction, ref->name, &ref->new_oid, + check_old ? &ref->old_oid : NULL, + 0, msg, &err); + if (ret) { + ret = STORE_REF_ERROR_OTHER; + goto out; + } + + if (our_transaction) { + switch (ref_transaction_commit(our_transaction, &err)) { + case 0: + break; + case TRANSACTION_NAME_CONFLICT: + ret = STORE_REF_ERROR_DF_CONFLICT; + goto out; + default: + ret = STORE_REF_ERROR_OTHER; + goto out; + } + } + +out: + ref_transaction_free(our_transaction); + if (ret) + error("%s", err.buf); + strbuf_release(&err); + free(msg); + return ret; +} + +static int refcol_width(const struct ref *ref_map, int compact_format) +{ + const struct ref *ref; + int max, width = 10; + + max = term_columns(); + if (compact_format) + max = max * 2 / 3; + + for (ref = ref_map; ref; ref = ref->next) { + int rlen, llen = 0, len; + + if (ref->status == REF_STATUS_REJECT_SHALLOW || + !ref->peer_ref || + !strcmp(ref->name, "HEAD")) + continue; + + /* uptodate lines are only shown on high verbosity level */ + if (verbosity <= 0 && oideq(&ref->peer_ref->old_oid, &ref->old_oid)) + continue; + + rlen = utf8_strwidth(prettify_refname(ref->name)); + if (!compact_format) + llen = utf8_strwidth(prettify_refname(ref->peer_ref->name)); + + /* + * rough estimation to see if the output line is too long and + * should not be counted (we can't do precise calculation + * anyway because we don't know if the error explanation part + * will be printed in update_local_ref) + */ + len = 21 /* flag and summary */ + rlen + 4 /* -> */ + llen; + if (len >= max) + continue; + + if (width < rlen) + width = rlen; + } + + return width; +} + +static void display_state_init(struct display_state *display_state, struct ref *ref_map, + const char *raw_url, enum display_format format) +{ + int i; + + memset(display_state, 0, sizeof(*display_state)); + strbuf_init(&display_state->buf, 0); + display_state->format = format; + + if (raw_url) + display_state->url = transport_anonymize_url(raw_url); + else + display_state->url = xstrdup("foreign"); + + display_state->url_len = strlen(display_state->url); + for (i = display_state->url_len - 1; display_state->url[i] == '/' && 0 <= i; i--) + ; + display_state->url_len = i + 1; + if (4 < i && !strncmp(".git", display_state->url + i - 3, 4)) + display_state->url_len = i - 3; + + if (verbosity < 0) + return; + + switch (display_state->format) { + case DISPLAY_FORMAT_FULL: + case DISPLAY_FORMAT_COMPACT: + display_state->refcol_width = refcol_width(ref_map, + display_state->format == DISPLAY_FORMAT_COMPACT); + break; + case DISPLAY_FORMAT_PORCELAIN: + /* We don't need to precompute anything here. */ + break; + default: + BUG("unexpected display format %d", display_state->format); + } +} + +static void display_state_release(struct display_state *display_state) +{ + strbuf_release(&display_state->buf); + free(display_state->url); +} + +static void print_remote_to_local(struct display_state *display_state, + const char *remote, const char *local) +{ + strbuf_addf(&display_state->buf, "%-*s -> %s", + display_state->refcol_width, remote, local); +} + +static int find_and_replace(struct strbuf *haystack, + const char *needle, + const char *placeholder) +{ + const char *p = NULL; + int plen, nlen; + + nlen = strlen(needle); + if (ends_with(haystack->buf, needle)) + p = haystack->buf + haystack->len - nlen; + else + p = strstr(haystack->buf, needle); + if (!p) + return 0; + + if (p > haystack->buf && p[-1] != '/') + return 0; + + plen = strlen(p); + if (plen > nlen && p[nlen] != '/') + return 0; + + strbuf_splice(haystack, p - haystack->buf, nlen, + placeholder, strlen(placeholder)); + return 1; +} + +static void print_compact(struct display_state *display_state, + const char *remote, const char *local) +{ + struct strbuf r = STRBUF_INIT; + struct strbuf l = STRBUF_INIT; + + if (!strcmp(remote, local)) { + strbuf_addf(&display_state->buf, "%-*s -> *", display_state->refcol_width, remote); + return; + } + + strbuf_addstr(&r, remote); + strbuf_addstr(&l, local); + + if (!find_and_replace(&r, local, "*")) + find_and_replace(&l, remote, "*"); + print_remote_to_local(display_state, r.buf, l.buf); + + strbuf_release(&r); + strbuf_release(&l); +} + +static void display_ref_update(struct display_state *display_state, char code, + const char *summary, const char *error, + const char *remote, const char *local, + const struct object_id *old_oid, + const struct object_id *new_oid, + int summary_width) +{ + FILE *f = stderr; + + if (verbosity < 0) + return; + + strbuf_reset(&display_state->buf); + + switch (display_state->format) { + case DISPLAY_FORMAT_FULL: + case DISPLAY_FORMAT_COMPACT: { + int width; + + if (!display_state->shown_url) { + strbuf_addf(&display_state->buf, _("From %.*s\n"), + display_state->url_len, display_state->url); + display_state->shown_url = 1; + } + + width = (summary_width + strlen(summary) - gettext_width(summary)); + remote = prettify_refname(remote); + local = prettify_refname(local); + + strbuf_addf(&display_state->buf, " %c %-*s ", code, width, summary); + + if (display_state->format != DISPLAY_FORMAT_COMPACT) + print_remote_to_local(display_state, remote, local); + else + print_compact(display_state, remote, local); + + if (error) + strbuf_addf(&display_state->buf, " (%s)", error); + + break; + } + case DISPLAY_FORMAT_PORCELAIN: + strbuf_addf(&display_state->buf, "%c %s %s %s", code, + oid_to_hex(old_oid), oid_to_hex(new_oid), local); + f = stdout; + break; + default: + BUG("unexpected display format %d", display_state->format); + }; + strbuf_addch(&display_state->buf, '\n'); + + fputs(display_state->buf.buf, f); +} + +static int update_local_ref(struct ref *ref, + struct ref_transaction *transaction, + struct display_state *display_state, + const struct ref *remote_ref, + int summary_width, + const struct fetch_config *config) +{ + struct commit *current = NULL, *updated; + int fast_forward = 0; + + if (!repo_has_object_file(the_repository, &ref->new_oid)) + die(_("object %s not found"), oid_to_hex(&ref->new_oid)); + + if (oideq(&ref->old_oid, &ref->new_oid)) { + if (verbosity > 0) + display_ref_update(display_state, '=', _("[up to date]"), NULL, + remote_ref->name, ref->name, + &ref->old_oid, &ref->new_oid, summary_width); + return 0; + } + + if (!update_head_ok && + !is_null_oid(&ref->old_oid) && + branch_checked_out(ref->name)) { + /* + * If this is the head, and it's not okay to update + * the head, and the old value of the head isn't empty... + */ + display_ref_update(display_state, '!', _("[rejected]"), + _("can't fetch into checked-out branch"), + remote_ref->name, ref->name, + &ref->old_oid, &ref->new_oid, summary_width); + return 1; + } + + if (!is_null_oid(&ref->old_oid) && + starts_with(ref->name, "refs/tags/")) { + if (force || ref->force) { + int r; + r = s_update_ref("updating tag", ref, transaction, 0); + display_ref_update(display_state, r ? '!' : 't', _("[tag update]"), + r ? _("unable to update local ref") : NULL, + remote_ref->name, ref->name, + &ref->old_oid, &ref->new_oid, summary_width); + return r; + } else { + display_ref_update(display_state, '!', _("[rejected]"), + _("would clobber existing tag"), + remote_ref->name, ref->name, + &ref->old_oid, &ref->new_oid, summary_width); + return 1; + } + } + + current = lookup_commit_reference_gently(the_repository, + &ref->old_oid, 1); + updated = lookup_commit_reference_gently(the_repository, + &ref->new_oid, 1); + if (!current || !updated) { + const char *msg; + const char *what; + int r; + /* + * Nicely describe the new ref we're fetching. + * Base this on the remote's ref name, as it's + * more likely to follow a standard layout. + */ + if (starts_with(remote_ref->name, "refs/tags/")) { + msg = "storing tag"; + what = _("[new tag]"); + } else if (starts_with(remote_ref->name, "refs/heads/")) { + msg = "storing head"; + what = _("[new branch]"); + } else { + msg = "storing ref"; + what = _("[new ref]"); + } + + r = s_update_ref(msg, ref, transaction, 0); + display_ref_update(display_state, r ? '!' : '*', what, + r ? _("unable to update local ref") : NULL, + remote_ref->name, ref->name, + &ref->old_oid, &ref->new_oid, summary_width); + return r; + } + + if (config->show_forced_updates) { + uint64_t t_before = getnanotime(); + fast_forward = repo_in_merge_bases(the_repository, current, + updated); + forced_updates_ms += (getnanotime() - t_before) / 1000000; + } else { + fast_forward = 1; + } + + if (fast_forward) { + struct strbuf quickref = STRBUF_INIT; + int r; + + strbuf_add_unique_abbrev(&quickref, ¤t->object.oid, DEFAULT_ABBREV); + strbuf_addstr(&quickref, ".."); + strbuf_add_unique_abbrev(&quickref, &ref->new_oid, DEFAULT_ABBREV); + r = s_update_ref("fast-forward", ref, transaction, 1); + display_ref_update(display_state, r ? '!' : ' ', quickref.buf, + r ? _("unable to update local ref") : NULL, + remote_ref->name, ref->name, + &ref->old_oid, &ref->new_oid, summary_width); + strbuf_release(&quickref); + return r; + } else if (force || ref->force) { + struct strbuf quickref = STRBUF_INIT; + int r; + strbuf_add_unique_abbrev(&quickref, ¤t->object.oid, DEFAULT_ABBREV); + strbuf_addstr(&quickref, "..."); + strbuf_add_unique_abbrev(&quickref, &ref->new_oid, DEFAULT_ABBREV); + r = s_update_ref("forced-update", ref, transaction, 1); + display_ref_update(display_state, r ? '!' : '+', quickref.buf, + r ? _("unable to update local ref") : _("forced update"), + remote_ref->name, ref->name, + &ref->old_oid, &ref->new_oid, summary_width); + strbuf_release(&quickref); + return r; + } else { + display_ref_update(display_state, '!', _("[rejected]"), _("non-fast-forward"), + remote_ref->name, ref->name, + &ref->old_oid, &ref->new_oid, summary_width); + return 1; + } +} + +static const struct object_id *iterate_ref_map(void *cb_data) +{ + struct ref **rm = cb_data; + struct ref *ref = *rm; + + while (ref && ref->status == REF_STATUS_REJECT_SHALLOW) + ref = ref->next; + if (!ref) + return NULL; + *rm = ref->next; + return &ref->old_oid; +} + +struct fetch_head { + FILE *fp; + struct strbuf buf; +}; + +static int open_fetch_head(struct fetch_head *fetch_head) +{ + const char *filename = git_path_fetch_head(the_repository); + + if (write_fetch_head) { + fetch_head->fp = fopen(filename, "a"); + if (!fetch_head->fp) + return error_errno(_("cannot open '%s'"), filename); + strbuf_init(&fetch_head->buf, 0); + } else { + fetch_head->fp = NULL; + } + + return 0; +} + +static void append_fetch_head(struct fetch_head *fetch_head, + const struct object_id *old_oid, + enum fetch_head_status fetch_head_status, + const char *note, + const char *url, size_t url_len) +{ + char old_oid_hex[GIT_MAX_HEXSZ + 1]; + const char *merge_status_marker; + size_t i; + + if (!fetch_head->fp) + return; + + switch (fetch_head_status) { + case FETCH_HEAD_NOT_FOR_MERGE: + merge_status_marker = "not-for-merge"; + break; + case FETCH_HEAD_MERGE: + merge_status_marker = ""; + break; + default: + /* do not write anything to FETCH_HEAD */ + return; + } + + strbuf_addf(&fetch_head->buf, "%s\t%s\t%s", + oid_to_hex_r(old_oid_hex, old_oid), merge_status_marker, note); + for (i = 0; i < url_len; ++i) + if ('\n' == url[i]) + strbuf_addstr(&fetch_head->buf, "\\n"); + else + strbuf_addch(&fetch_head->buf, url[i]); + strbuf_addch(&fetch_head->buf, '\n'); + + /* + * When using an atomic fetch, we do not want to update FETCH_HEAD if + * any of the reference updates fails. We thus have to write all + * updates to a buffer first and only commit it as soon as all + * references have been successfully updated. + */ + if (!atomic_fetch) { + strbuf_write(&fetch_head->buf, fetch_head->fp); + strbuf_reset(&fetch_head->buf); + } +} + +static void commit_fetch_head(struct fetch_head *fetch_head) +{ + if (!fetch_head->fp || !atomic_fetch) + return; + strbuf_write(&fetch_head->buf, fetch_head->fp); +} + +static void close_fetch_head(struct fetch_head *fetch_head) +{ + if (!fetch_head->fp) + return; + + fclose(fetch_head->fp); + strbuf_release(&fetch_head->buf); +} + +static const char warn_show_forced_updates[] = +N_("fetch normally indicates which branches had a forced update,\n" + "but that check has been disabled; to re-enable, use '--show-forced-updates'\n" + "flag or run 'git config fetch.showForcedUpdates true'"); +static const char warn_time_show_forced_updates[] = +N_("it took %.2f seconds to check forced updates; you can use\n" + "'--no-show-forced-updates' or run 'git config fetch.showForcedUpdates false'\n" + "to avoid this check\n"); + +static int store_updated_refs(struct display_state *display_state, + const char *remote_name, + int connectivity_checked, + struct ref_transaction *transaction, struct ref *ref_map, + struct fetch_head *fetch_head, + const struct fetch_config *config) +{ + int rc = 0; + struct strbuf note = STRBUF_INIT; + const char *what, *kind; + struct ref *rm; + int want_status; + int summary_width = 0; + + if (verbosity >= 0) + summary_width = transport_summary_width(ref_map); + + if (!connectivity_checked) { + struct check_connected_options opt = CHECK_CONNECTED_INIT; + + opt.exclude_hidden_refs_section = "fetch"; + rm = ref_map; + if (check_connected(iterate_ref_map, &rm, &opt)) { + rc = error(_("%s did not send all necessary objects\n"), + display_state->url); + goto abort; + } + } + + /* + * We do a pass for each fetch_head_status type in their enum order, so + * merged entries are written before not-for-merge. That lets readers + * use FETCH_HEAD as a refname to refer to the ref to be merged. + */ + for (want_status = FETCH_HEAD_MERGE; + want_status <= FETCH_HEAD_IGNORE; + want_status++) { + for (rm = ref_map; rm; rm = rm->next) { + struct ref *ref = NULL; + + if (rm->status == REF_STATUS_REJECT_SHALLOW) { + if (want_status == FETCH_HEAD_MERGE) + warning(_("rejected %s because shallow roots are not allowed to be updated"), + rm->peer_ref ? rm->peer_ref->name : rm->name); + continue; + } + + /* + * When writing FETCH_HEAD we need to determine whether + * we already have the commit or not. If not, then the + * reference is not for merge and needs to be written + * to the reflog after other commits which we already + * have. We're not interested in this property though + * in case FETCH_HEAD is not to be updated, so we can + * skip the classification in that case. + */ + if (fetch_head->fp) { + struct commit *commit = NULL; + + /* + * References in "refs/tags/" are often going to point + * to annotated tags, which are not part of the + * commit-graph. We thus only try to look up refs in + * the graph which are not in that namespace to not + * regress performance in repositories with many + * annotated tags. + */ + if (!starts_with(rm->name, "refs/tags/")) + commit = lookup_commit_in_graph(the_repository, &rm->old_oid); + if (!commit) { + commit = lookup_commit_reference_gently(the_repository, + &rm->old_oid, + 1); + if (!commit) + rm->fetch_head_status = FETCH_HEAD_NOT_FOR_MERGE; + } + } + + if (rm->fetch_head_status != want_status) + continue; + + if (rm->peer_ref) { + ref = alloc_ref(rm->peer_ref->name); + oidcpy(&ref->old_oid, &rm->peer_ref->old_oid); + oidcpy(&ref->new_oid, &rm->old_oid); + ref->force = rm->peer_ref->force; + } + + if (config->recurse_submodules != RECURSE_SUBMODULES_OFF && + (!rm->peer_ref || !oideq(&ref->old_oid, &ref->new_oid))) { + check_for_new_submodule_commits(&rm->old_oid); + } + + if (!strcmp(rm->name, "HEAD")) { + kind = ""; + what = ""; + } else if (skip_prefix(rm->name, "refs/heads/", &what)) { + kind = "branch"; + } else if (skip_prefix(rm->name, "refs/tags/", &what)) { + kind = "tag"; + } else if (skip_prefix(rm->name, "refs/remotes/", &what)) { + kind = "remote-tracking branch"; + } else { + kind = ""; + what = rm->name; + } + + strbuf_reset(¬e); + if (*what) { + if (*kind) + strbuf_addf(¬e, "%s ", kind); + strbuf_addf(¬e, "'%s' of ", what); + } + + append_fetch_head(fetch_head, &rm->old_oid, + rm->fetch_head_status, + note.buf, display_state->url, + display_state->url_len); + + if (ref) { + rc |= update_local_ref(ref, transaction, display_state, + rm, summary_width, config); + free(ref); + } else if (write_fetch_head || dry_run) { + /* + * Display fetches written to FETCH_HEAD (or + * would be written to FETCH_HEAD, if --dry-run + * is set). + */ + display_ref_update(display_state, '*', + *kind ? kind : "branch", NULL, + rm->name, + "FETCH_HEAD", + &rm->new_oid, &rm->old_oid, + summary_width); + } + } + } + + if (rc & STORE_REF_ERROR_DF_CONFLICT) + error(_("some local refs could not be updated; try running\n" + " 'git remote prune %s' to remove any old, conflicting " + "branches"), remote_name); + + if (advice_enabled(ADVICE_FETCH_SHOW_FORCED_UPDATES)) { + if (!config->show_forced_updates) { + warning(_(warn_show_forced_updates)); + } else if (forced_updates_ms > FORCED_UPDATES_DELAY_WARNING_IN_MS) { + warning(_(warn_time_show_forced_updates), + forced_updates_ms / 1000.0); + } + } + + abort: + strbuf_release(¬e); + return rc; +} + +/* + * We would want to bypass the object transfer altogether if + * everything we are going to fetch already exists and is connected + * locally. + */ +static int check_exist_and_connected(struct ref *ref_map) +{ + struct ref *rm = ref_map; + struct check_connected_options opt = CHECK_CONNECTED_INIT; + struct ref *r; + + /* + * If we are deepening a shallow clone we already have these + * objects reachable. Running rev-list here will return with + * a good (0) exit status and we'll bypass the fetch that we + * really need to perform. Claiming failure now will ensure + * we perform the network exchange to deepen our history. + */ + if (deepen) + return -1; + + /* + * Similarly, if we need to refetch, we always want to perform a full + * fetch ignoring existing objects. + */ + if (refetch) + return -1; + + + /* + * check_connected() allows objects to merely be promised, but + * we need all direct targets to exist. + */ + for (r = rm; r; r = r->next) { + if (!repo_has_object_file_with_flags(the_repository, &r->old_oid, + OBJECT_INFO_SKIP_FETCH_OBJECT)) + return -1; + } + + opt.quiet = 1; + opt.exclude_hidden_refs_section = "fetch"; + return check_connected(iterate_ref_map, &rm, &opt); +} + +static int fetch_and_consume_refs(struct display_state *display_state, + struct transport *transport, + struct ref_transaction *transaction, + struct ref *ref_map, + struct fetch_head *fetch_head, + const struct fetch_config *config) +{ + int connectivity_checked = 1; + int ret; + + /* + * We don't need to perform a fetch in case we can already satisfy all + * refs. + */ + ret = check_exist_and_connected(ref_map); + if (ret) { + trace2_region_enter("fetch", "fetch_refs", the_repository); + ret = transport_fetch_refs(transport, ref_map); + trace2_region_leave("fetch", "fetch_refs", the_repository); + if (ret) + goto out; + connectivity_checked = transport->smart_options ? + transport->smart_options->connectivity_checked : 0; + } + + trace2_region_enter("fetch", "consume_refs", the_repository); + ret = store_updated_refs(display_state, transport->remote->name, + connectivity_checked, transaction, ref_map, + fetch_head, config); + trace2_region_leave("fetch", "consume_refs", the_repository); + +out: + transport_unlock_pack(transport, 0); + return ret; +} + +static int prune_refs(struct display_state *display_state, + struct refspec *rs, + struct ref_transaction *transaction, + struct ref *ref_map) +{ + int result = 0; + struct ref *ref, *stale_refs = get_stale_heads(rs, ref_map); + struct strbuf err = STRBUF_INIT; + const char *dangling_msg = dry_run + ? _(" (%s will become dangling)") + : _(" (%s has become dangling)"); + + if (!dry_run) { + if (transaction) { + for (ref = stale_refs; ref; ref = ref->next) { + result = ref_transaction_delete(transaction, ref->name, NULL, 0, + "fetch: prune", &err); + if (result) + goto cleanup; + } + } else { + struct string_list refnames = STRING_LIST_INIT_NODUP; + + for (ref = stale_refs; ref; ref = ref->next) + string_list_append(&refnames, ref->name); + + result = delete_refs("fetch: prune", &refnames, 0); + string_list_clear(&refnames, 0); + } + } + + if (verbosity >= 0) { + int summary_width = transport_summary_width(stale_refs); + + for (ref = stale_refs; ref; ref = ref->next) { + display_ref_update(display_state, '-', _("[deleted]"), NULL, + _("(none)"), ref->name, + &ref->new_oid, &ref->old_oid, + summary_width); + warn_dangling_symref(stderr, dangling_msg, ref->name); + } + } + +cleanup: + strbuf_release(&err); + free_refs(stale_refs); + return result; +} + +static void check_not_current_branch(struct ref *ref_map) +{ + const char *path; + for (; ref_map; ref_map = ref_map->next) + if (ref_map->peer_ref && + starts_with(ref_map->peer_ref->name, "refs/heads/") && + (path = branch_checked_out(ref_map->peer_ref->name))) + die(_("refusing to fetch into branch '%s' " + "checked out at '%s'"), + ref_map->peer_ref->name, path); +} + +static int truncate_fetch_head(void) +{ + const char *filename = git_path_fetch_head(the_repository); + FILE *fp = fopen_for_writing(filename); + + if (!fp) + return error_errno(_("cannot open '%s'"), filename); + fclose(fp); + return 0; +} + +static void set_option(struct transport *transport, const char *name, const char *value) +{ + int r = transport_set_option(transport, name, value); + if (r < 0) + die(_("option \"%s\" value \"%s\" is not valid for %s"), + name, value, transport->url); + if (r > 0) + warning(_("option \"%s\" is ignored for %s\n"), + name, transport->url); +} + + +static int add_oid(const char *refname UNUSED, + const struct object_id *oid, + int flags UNUSED, void *cb_data) +{ + struct oid_array *oids = cb_data; + + oid_array_append(oids, oid); + return 0; +} + +static void add_negotiation_tips(struct git_transport_options *smart_options) +{ + struct oid_array *oids = xcalloc(1, sizeof(*oids)); + int i; + + for (i = 0; i < negotiation_tip.nr; i++) { + const char *s = negotiation_tip.items[i].string; + int old_nr; + if (!has_glob_specials(s)) { + struct object_id oid; + if (repo_get_oid(the_repository, s, &oid)) + die(_("%s is not a valid object"), s); + if (!has_object(the_repository, &oid, 0)) + die(_("the object %s does not exist"), s); + oid_array_append(oids, &oid); + continue; + } + old_nr = oids->nr; + for_each_glob_ref(add_oid, s, oids); + if (old_nr == oids->nr) + warning("ignoring --negotiation-tip=%s because it does not match any refs", + s); + } + smart_options->negotiation_tips = oids; +} + +static struct transport *prepare_transport(struct remote *remote, int deepen) +{ + struct transport *transport; + + transport = transport_get(remote, NULL); + transport_set_verbosity(transport, verbosity, progress); + transport->family = family; + if (upload_pack) + set_option(transport, TRANS_OPT_UPLOADPACK, upload_pack); + if (keep) + set_option(transport, TRANS_OPT_KEEP, "yes"); + if (depth) + set_option(transport, TRANS_OPT_DEPTH, depth); + if (deepen && deepen_since) + set_option(transport, TRANS_OPT_DEEPEN_SINCE, deepen_since); + if (deepen && deepen_not.nr) + set_option(transport, TRANS_OPT_DEEPEN_NOT, + (const char *)&deepen_not); + if (deepen_relative) + set_option(transport, TRANS_OPT_DEEPEN_RELATIVE, "yes"); + if (update_shallow) + set_option(transport, TRANS_OPT_UPDATE_SHALLOW, "yes"); + if (refetch) + set_option(transport, TRANS_OPT_REFETCH, "yes"); + if (filter_options.choice) { + const char *spec = + expand_list_objects_filter_spec(&filter_options); + set_option(transport, TRANS_OPT_LIST_OBJECTS_FILTER, spec); + set_option(transport, TRANS_OPT_FROM_PROMISOR, "1"); + } + if (negotiation_tip.nr) { + if (transport->smart_options) + add_negotiation_tips(transport->smart_options); + else + warning("ignoring --negotiation-tip because the protocol does not support it"); + } + return transport; +} + +static int backfill_tags(struct display_state *display_state, + struct transport *transport, + struct ref_transaction *transaction, + struct ref *ref_map, + struct fetch_head *fetch_head, + const struct fetch_config *config) +{ + int retcode, cannot_reuse; + + /* + * Once we have set TRANS_OPT_DEEPEN_SINCE, we can't unset it + * when remote helper is used (setting it to an empty string + * is not unsetting). We could extend the remote helper + * protocol for that, but for now, just force a new connection + * without deepen-since. Similar story for deepen-not. + */ + cannot_reuse = transport->cannot_reuse || + deepen_since || deepen_not.nr; + if (cannot_reuse) { + gsecondary = prepare_transport(transport->remote, 0); + transport = gsecondary; + } + + transport_set_option(transport, TRANS_OPT_FOLLOWTAGS, NULL); + transport_set_option(transport, TRANS_OPT_DEPTH, "0"); + transport_set_option(transport, TRANS_OPT_DEEPEN_RELATIVE, NULL); + retcode = fetch_and_consume_refs(display_state, transport, transaction, ref_map, + fetch_head, config); + + if (gsecondary) { + transport_disconnect(gsecondary); + gsecondary = NULL; + } + + return retcode; +} + +static int do_fetch(struct transport *transport, + struct refspec *rs, + const struct fetch_config *config) +{ + struct ref_transaction *transaction = NULL; + struct ref *ref_map = NULL; + struct display_state display_state = { 0 }; + int autotags = (transport->remote->fetch_tags == 1); + int retcode = 0; + const struct ref *remote_refs; + struct transport_ls_refs_options transport_ls_refs_options = + TRANSPORT_LS_REFS_OPTIONS_INIT; + int must_list_refs = 1; + struct fetch_head fetch_head = { 0 }; + struct strbuf err = STRBUF_INIT; + + if (tags == TAGS_DEFAULT) { + if (transport->remote->fetch_tags == 2) + tags = TAGS_SET; + if (transport->remote->fetch_tags == -1) + tags = TAGS_UNSET; + } + + /* if not appending, truncate FETCH_HEAD */ + if (!append && write_fetch_head) { + retcode = truncate_fetch_head(); + if (retcode) + goto cleanup; + } + + if (rs->nr) { + int i; + + refspec_ref_prefixes(rs, &transport_ls_refs_options.ref_prefixes); + + /* + * We can avoid listing refs if all of them are exact + * OIDs + */ + must_list_refs = 0; + for (i = 0; i < rs->nr; i++) { + if (!rs->items[i].exact_sha1) { + must_list_refs = 1; + break; + } + } + } else { + struct branch *branch = branch_get(NULL); + + if (transport->remote->fetch.nr) + refspec_ref_prefixes(&transport->remote->fetch, + &transport_ls_refs_options.ref_prefixes); + if (branch_has_merge_config(branch) && + !strcmp(branch->remote_name, transport->remote->name)) { + int i; + for (i = 0; i < branch->merge_nr; i++) { + strvec_push(&transport_ls_refs_options.ref_prefixes, + branch->merge[i]->src); + } + } + } + + if (tags == TAGS_SET || tags == TAGS_DEFAULT) { + must_list_refs = 1; + if (transport_ls_refs_options.ref_prefixes.nr) + strvec_push(&transport_ls_refs_options.ref_prefixes, + "refs/tags/"); + } + + if (must_list_refs) { + trace2_region_enter("fetch", "remote_refs", the_repository); + remote_refs = transport_get_remote_refs(transport, + &transport_ls_refs_options); + trace2_region_leave("fetch", "remote_refs", the_repository); + } else + remote_refs = NULL; + + transport_ls_refs_options_release(&transport_ls_refs_options); + + ref_map = get_ref_map(transport->remote, remote_refs, rs, + tags, &autotags); + if (!update_head_ok) + check_not_current_branch(ref_map); + + retcode = open_fetch_head(&fetch_head); + if (retcode) + goto cleanup; + + display_state_init(&display_state, ref_map, transport->url, + config->display_format); + + if (atomic_fetch) { + transaction = ref_transaction_begin(&err); + if (!transaction) { + retcode = error("%s", err.buf); + goto cleanup; + } + } + + if (tags == TAGS_DEFAULT && autotags) + transport_set_option(transport, TRANS_OPT_FOLLOWTAGS, "1"); + if (prune) { + /* + * We only prune based on refspecs specified + * explicitly (via command line or configuration); we + * don't care whether --tags was specified. + */ + if (rs->nr) { + retcode = prune_refs(&display_state, rs, transaction, ref_map); + } else { + retcode = prune_refs(&display_state, &transport->remote->fetch, + transaction, ref_map); + } + if (retcode != 0) + retcode = 1; + } + + if (fetch_and_consume_refs(&display_state, transport, transaction, ref_map, + &fetch_head, config)) { + retcode = 1; + goto cleanup; + } + + /* + * If neither --no-tags nor --tags was specified, do automated tag + * following. + */ + if (tags == TAGS_DEFAULT && autotags) { + struct ref *tags_ref_map = NULL, **tail = &tags_ref_map; + + find_non_local_tags(remote_refs, transaction, &tags_ref_map, &tail); + if (tags_ref_map) { + /* + * If backfilling of tags fails then we want to tell + * the user so, but we have to continue regardless to + * populate upstream information of the references we + * have already fetched above. The exception though is + * when `--atomic` is passed: in that case we'll abort + * the transaction and don't commit anything. + */ + if (backfill_tags(&display_state, transport, transaction, tags_ref_map, + &fetch_head, config)) + retcode = 1; + } + + free_refs(tags_ref_map); + } + + if (transaction) { + if (retcode) + goto cleanup; + + retcode = ref_transaction_commit(transaction, &err); + if (retcode) { + error("%s", err.buf); + ref_transaction_free(transaction); + transaction = NULL; + goto cleanup; + } + } + + commit_fetch_head(&fetch_head); + + if (set_upstream) { + struct branch *branch = branch_get("HEAD"); + struct ref *rm; + struct ref *source_ref = NULL; + + /* + * We're setting the upstream configuration for the + * current branch. The relevant upstream is the + * fetched branch that is meant to be merged with the + * current one, i.e. the one fetched to FETCH_HEAD. + * + * When there are several such branches, consider the + * request ambiguous and err on the safe side by doing + * nothing and just emit a warning. + */ + for (rm = ref_map; rm; rm = rm->next) { + if (!rm->peer_ref) { + if (source_ref) { + warning(_("multiple branches detected, incompatible with --set-upstream")); + goto cleanup; + } else { + source_ref = rm; + } + } + } + if (source_ref) { + if (!branch) { + const char *shortname = source_ref->name; + skip_prefix(shortname, "refs/heads/", &shortname); + + warning(_("could not set upstream of HEAD to '%s' from '%s' when " + "it does not point to any branch."), + shortname, transport->remote->name); + goto cleanup; + } + + if (!strcmp(source_ref->name, "HEAD") || + starts_with(source_ref->name, "refs/heads/")) + install_branch_config(0, + branch->name, + transport->remote->name, + source_ref->name); + else if (starts_with(source_ref->name, "refs/remotes/")) + warning(_("not setting upstream for a remote remote-tracking branch")); + else if (starts_with(source_ref->name, "refs/tags/")) + warning(_("not setting upstream for a remote tag")); + else + warning(_("unknown branch type")); + } else { + warning(_("no source branch found;\n" + "you need to specify exactly one branch with the --set-upstream option")); + } + } + +cleanup: + if (retcode && transaction) { + ref_transaction_abort(transaction, &err); + error("%s", err.buf); + } + + display_state_release(&display_state); + close_fetch_head(&fetch_head); + strbuf_release(&err); + free_refs(ref_map); + return retcode; +} + +static int get_one_remote_for_fetch(struct remote *remote, void *priv) +{ + struct string_list *list = priv; + if (!remote->skip_default_update) + string_list_append(list, remote->name); + return 0; +} + +struct remote_group_data { + const char *name; + struct string_list *list; +}; + +static int get_remote_group(const char *key, const char *value, + const struct config_context *ctx UNUSED, + void *priv) +{ + struct remote_group_data *g = priv; + + if (skip_prefix(key, "remotes.", &key) && !strcmp(key, g->name)) { + /* split list by white space */ + while (*value) { + size_t wordlen = strcspn(value, " \t\n"); + + if (wordlen >= 1) + string_list_append_nodup(g->list, + xstrndup(value, wordlen)); + value += wordlen + (value[wordlen] != '\0'); + } + } + + return 0; +} + +static int add_remote_or_group(const char *name, struct string_list *list) +{ + int prev_nr = list->nr; + struct remote_group_data g; + g.name = name; g.list = list; + + git_config(get_remote_group, &g); + if (list->nr == prev_nr) { + struct remote *remote = remote_get(name); + if (!remote_is_configured(remote, 0)) + return 0; + string_list_append(list, remote->name); + } + return 1; +} + +static void add_options_to_argv(struct strvec *argv, + const struct fetch_config *config) +{ + if (dry_run) + strvec_push(argv, "--dry-run"); + if (prune != -1) + strvec_push(argv, prune ? "--prune" : "--no-prune"); + if (prune_tags != -1) + strvec_push(argv, prune_tags ? "--prune-tags" : "--no-prune-tags"); + if (update_head_ok) + strvec_push(argv, "--update-head-ok"); + if (force) + strvec_push(argv, "--force"); + if (keep) + strvec_push(argv, "--keep"); + if (config->recurse_submodules == RECURSE_SUBMODULES_ON) + strvec_push(argv, "--recurse-submodules"); + else if (config->recurse_submodules == RECURSE_SUBMODULES_OFF) + strvec_push(argv, "--no-recurse-submodules"); + else if (config->recurse_submodules == RECURSE_SUBMODULES_ON_DEMAND) + strvec_push(argv, "--recurse-submodules=on-demand"); + if (tags == TAGS_SET) + strvec_push(argv, "--tags"); + else if (tags == TAGS_UNSET) + strvec_push(argv, "--no-tags"); + if (verbosity >= 2) + strvec_push(argv, "-v"); + if (verbosity >= 1) + strvec_push(argv, "-v"); + else if (verbosity < 0) + strvec_push(argv, "-q"); + if (family == TRANSPORT_FAMILY_IPV4) + strvec_push(argv, "--ipv4"); + else if (family == TRANSPORT_FAMILY_IPV6) + strvec_push(argv, "--ipv6"); + if (!write_fetch_head) + strvec_push(argv, "--no-write-fetch-head"); + if (config->display_format == DISPLAY_FORMAT_PORCELAIN) + strvec_pushf(argv, "--porcelain"); +} + +/* Fetch multiple remotes in parallel */ + +struct parallel_fetch_state { + const char **argv; + struct string_list *remotes; + int next, result; + const struct fetch_config *config; +}; + +static int fetch_next_remote(struct child_process *cp, + struct strbuf *out UNUSED, + void *cb, void **task_cb) +{ + struct parallel_fetch_state *state = cb; + char *remote; + + if (state->next < 0 || state->next >= state->remotes->nr) + return 0; + + remote = state->remotes->items[state->next++].string; + *task_cb = remote; + + strvec_pushv(&cp->args, state->argv); + strvec_push(&cp->args, remote); + cp->git_cmd = 1; + + if (verbosity >= 0 && state->config->display_format != DISPLAY_FORMAT_PORCELAIN) + printf(_("Fetching %s\n"), remote); + + return 1; +} + +static int fetch_failed_to_start(struct strbuf *out UNUSED, + void *cb, void *task_cb) +{ + struct parallel_fetch_state *state = cb; + const char *remote = task_cb; + + state->result = error(_("could not fetch %s"), remote); + + return 0; +} + +static int fetch_finished(int result, struct strbuf *out, + void *cb, void *task_cb) +{ + struct parallel_fetch_state *state = cb; + const char *remote = task_cb; + + if (result) { + strbuf_addf(out, _("could not fetch '%s' (exit code: %d)\n"), + remote, result); + state->result = -1; + } + + return 0; +} + +static int fetch_multiple(struct string_list *list, int max_children, + const struct fetch_config *config) +{ + int i, result = 0; + struct strvec argv = STRVEC_INIT; + + if (!append && write_fetch_head) { + int errcode = truncate_fetch_head(); + if (errcode) + return errcode; + } + + /* + * Cancel out the fetch.bundleURI config when running subprocesses, + * to avoid fetching from the same bundle list multiple times. + */ + strvec_pushl(&argv, "-c", "fetch.bundleURI=", + "fetch", "--append", "--no-auto-gc", + "--no-write-commit-graph", NULL); + add_options_to_argv(&argv, config); + + if (max_children != 1 && list->nr != 1) { + struct parallel_fetch_state state = { argv.v, list, 0, 0, config }; + const struct run_process_parallel_opts opts = { + .tr2_category = "fetch", + .tr2_label = "parallel/fetch", + + .processes = max_children, + + .get_next_task = &fetch_next_remote, + .start_failure = &fetch_failed_to_start, + .task_finished = &fetch_finished, + .data = &state, + }; + + strvec_push(&argv, "--end-of-options"); + + run_processes_parallel(&opts); + result = state.result; + } else + for (i = 0; i < list->nr; i++) { + const char *name = list->items[i].string; + struct child_process cmd = CHILD_PROCESS_INIT; + + strvec_pushv(&cmd.args, argv.v); + strvec_push(&cmd.args, name); + if (verbosity >= 0 && config->display_format != DISPLAY_FORMAT_PORCELAIN) + printf(_("Fetching %s\n"), name); + cmd.git_cmd = 1; + if (run_command(&cmd)) { + error(_("could not fetch %s"), name); + result = 1; + } + } + + strvec_clear(&argv); + return !!result; +} + +/* + * Fetching from the promisor remote should use the given filter-spec + * or inherit the default filter-spec from the config. + */ +static inline void fetch_one_setup_partial(struct remote *remote) +{ + /* + * Explicit --no-filter argument overrides everything, regardless + * of any prior partial clones and fetches. + */ + if (filter_options.no_filter) + return; + + /* + * If no prior partial clone/fetch and the current fetch DID NOT + * request a partial-fetch, do a normal fetch. + */ + if (!repo_has_promisor_remote(the_repository) && !filter_options.choice) + return; + + /* + * If this is a partial-fetch request, we enable partial on + * this repo if not already enabled and remember the given + * filter-spec as the default for subsequent fetches to this + * remote if there is currently no default filter-spec. + */ + if (filter_options.choice) { + partial_clone_register(remote->name, &filter_options); + return; + } + + /* + * Do a partial-fetch from the promisor remote using either the + * explicitly given filter-spec or inherit the filter-spec from + * the config. + */ + if (!filter_options.choice) + partial_clone_get_default_filter_spec(&filter_options, remote->name); + return; +} + +static int fetch_one(struct remote *remote, int argc, const char **argv, + int prune_tags_ok, int use_stdin_refspecs, + const struct fetch_config *config) +{ + struct refspec rs = REFSPEC_INIT_FETCH; + int i; + int exit_code; + int maybe_prune_tags; + int remote_via_config = remote_is_configured(remote, 0); + + if (!remote) + die(_("no remote repository specified; please specify either a URL or a\n" + "remote name from which new revisions should be fetched")); + + gtransport = prepare_transport(remote, 1); + + if (prune < 0) { + /* no command line request */ + if (0 <= remote->prune) + prune = remote->prune; + else if (0 <= config->prune) + prune = config->prune; + else + prune = PRUNE_BY_DEFAULT; + } + + if (prune_tags < 0) { + /* no command line request */ + if (0 <= remote->prune_tags) + prune_tags = remote->prune_tags; + else if (0 <= config->prune_tags) + prune_tags = config->prune_tags; + else + prune_tags = PRUNE_TAGS_BY_DEFAULT; + } + + maybe_prune_tags = prune_tags_ok && prune_tags; + if (maybe_prune_tags && remote_via_config) + refspec_append(&remote->fetch, TAG_REFSPEC); + + if (maybe_prune_tags && (argc || !remote_via_config)) + refspec_append(&rs, TAG_REFSPEC); + + for (i = 0; i < argc; i++) { + if (!strcmp(argv[i], "tag")) { + i++; + if (i >= argc) + die(_("you need to specify a tag name")); + + refspec_appendf(&rs, "refs/tags/%s:refs/tags/%s", + argv[i], argv[i]); + } else { + refspec_append(&rs, argv[i]); + } + } + + if (use_stdin_refspecs) { + struct strbuf line = STRBUF_INIT; + while (strbuf_getline_lf(&line, stdin) != EOF) + refspec_append(&rs, line.buf); + strbuf_release(&line); + } + + if (server_options.nr) + gtransport->server_options = &server_options; + + sigchain_push_common(unlock_pack_on_signal); + atexit(unlock_pack_atexit); + sigchain_push(SIGPIPE, SIG_IGN); + exit_code = do_fetch(gtransport, &rs, config); + sigchain_pop(SIGPIPE); + refspec_clear(&rs); + transport_disconnect(gtransport); + gtransport = NULL; + return exit_code; +} + +int cmd_fetch(int argc, const char **argv, const char *prefix) +{ + struct fetch_config config = { + .display_format = DISPLAY_FORMAT_FULL, + .prune = -1, + .prune_tags = -1, + .show_forced_updates = 1, + .recurse_submodules = RECURSE_SUBMODULES_DEFAULT, + .parallel = 1, + .submodule_fetch_jobs = -1, + }; + const char *submodule_prefix = ""; + const char *bundle_uri; + struct string_list list = STRING_LIST_INIT_DUP; + struct remote *remote = NULL; + int all = 0, multiple = 0; + int result = 0; + int prune_tags_ok = 1; + int enable_auto_gc = 1; + int unshallow = 0; + int max_jobs = -1; + int recurse_submodules_cli = RECURSE_SUBMODULES_DEFAULT; + int recurse_submodules_default = RECURSE_SUBMODULES_ON_DEMAND; + int fetch_write_commit_graph = -1; + int stdin_refspecs = 0; + int negotiate_only = 0; + int porcelain = 0; + int i; + + struct option builtin_fetch_options[] = { + OPT__VERBOSITY(&verbosity), + OPT_BOOL(0, "all", &all, + N_("fetch from all remotes")), + OPT_BOOL(0, "set-upstream", &set_upstream, + N_("set upstream for git pull/fetch")), + OPT_BOOL('a', "append", &append, + N_("append to .git/FETCH_HEAD instead of overwriting")), + OPT_BOOL(0, "atomic", &atomic_fetch, + N_("use atomic transaction to update references")), + OPT_STRING(0, "upload-pack", &upload_pack, N_("path"), + N_("path to upload pack on remote end")), + OPT__FORCE(&force, N_("force overwrite of local reference"), 0), + OPT_BOOL('m', "multiple", &multiple, + N_("fetch from multiple remotes")), + OPT_SET_INT('t', "tags", &tags, + N_("fetch all tags and associated objects"), TAGS_SET), + OPT_SET_INT('n', NULL, &tags, + N_("do not fetch all tags (--no-tags)"), TAGS_UNSET), + OPT_INTEGER('j', "jobs", &max_jobs, + N_("number of submodules fetched in parallel")), + OPT_BOOL(0, "prefetch", &prefetch, + N_("modify the refspec to place all refs within refs/prefetch/")), + OPT_BOOL('p', "prune", &prune, + N_("prune remote-tracking branches no longer on remote")), + OPT_BOOL('P', "prune-tags", &prune_tags, + N_("prune local tags no longer on remote and clobber changed tags")), + OPT_CALLBACK_F(0, "recurse-submodules", &recurse_submodules_cli, N_("on-demand"), + N_("control recursive fetching of submodules"), + PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules), + OPT_BOOL(0, "dry-run", &dry_run, + N_("dry run")), + OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")), + OPT_BOOL(0, "write-fetch-head", &write_fetch_head, + N_("write fetched references to the FETCH_HEAD file")), + OPT_BOOL('k', "keep", &keep, N_("keep downloaded pack")), + OPT_BOOL('u', "update-head-ok", &update_head_ok, + N_("allow updating of HEAD ref")), + OPT_BOOL(0, "progress", &progress, N_("force progress reporting")), + OPT_STRING(0, "depth", &depth, N_("depth"), + N_("deepen history of shallow clone")), + OPT_STRING(0, "shallow-since", &deepen_since, N_("time"), + N_("deepen history of shallow repository based on time")), + OPT_STRING_LIST(0, "shallow-exclude", &deepen_not, N_("revision"), + N_("deepen history of shallow clone, excluding rev")), + OPT_INTEGER(0, "deepen", &deepen_relative, + N_("deepen history of shallow clone")), + OPT_SET_INT_F(0, "unshallow", &unshallow, + N_("convert to a complete repository"), + 1, PARSE_OPT_NONEG), + OPT_SET_INT_F(0, "refetch", &refetch, + N_("re-fetch without negotiating common commits"), + 1, PARSE_OPT_NONEG), + { OPTION_STRING, 0, "submodule-prefix", &submodule_prefix, N_("dir"), + N_("prepend this to submodule path output"), PARSE_OPT_HIDDEN }, + OPT_CALLBACK_F(0, "recurse-submodules-default", + &recurse_submodules_default, N_("on-demand"), + N_("default for recursive fetching of submodules " + "(lower priority than config files)"), + PARSE_OPT_HIDDEN, option_fetch_parse_recurse_submodules), + OPT_BOOL(0, "update-shallow", &update_shallow, + N_("accept refs that update .git/shallow")), + OPT_CALLBACK_F(0, "refmap", &refmap, N_("refmap"), + N_("specify fetch refmap"), PARSE_OPT_NONEG, parse_refmap_arg), + OPT_STRING_LIST('o', "server-option", &server_options, N_("server-specific"), N_("option to transmit")), + OPT_IPVERSION(&family), + OPT_STRING_LIST(0, "negotiation-tip", &negotiation_tip, N_("revision"), + N_("report that we have only objects reachable from this object")), + OPT_BOOL(0, "negotiate-only", &negotiate_only, + N_("do not fetch a packfile; instead, print ancestors of negotiation tips")), + OPT_PARSE_LIST_OBJECTS_FILTER(&filter_options), + OPT_BOOL(0, "auto-maintenance", &enable_auto_gc, + N_("run 'maintenance --auto' after fetching")), + OPT_BOOL(0, "auto-gc", &enable_auto_gc, + N_("run 'maintenance --auto' after fetching")), + OPT_BOOL(0, "show-forced-updates", &config.show_forced_updates, + N_("check for forced-updates on all updated branches")), + OPT_BOOL(0, "write-commit-graph", &fetch_write_commit_graph, + N_("write the commit-graph after fetching")), + OPT_BOOL(0, "stdin", &stdin_refspecs, + N_("accept refspecs from stdin")), + OPT_END() + }; + + packet_trace_identity("fetch"); + + /* Record the command line for the reflog */ + strbuf_addstr(&default_rla, "fetch"); + for (i = 1; i < argc; i++) { + /* This handles non-URLs gracefully */ + char *anon = transport_anonymize_url(argv[i]); + + strbuf_addf(&default_rla, " %s", anon); + free(anon); + } + + git_config(git_fetch_config, &config); + if (the_repository->gitdir) { + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + } + + argc = parse_options(argc, argv, prefix, + builtin_fetch_options, builtin_fetch_usage, 0); + + if (recurse_submodules_cli != RECURSE_SUBMODULES_DEFAULT) + config.recurse_submodules = recurse_submodules_cli; + + if (negotiate_only) { + switch (recurse_submodules_cli) { + case RECURSE_SUBMODULES_OFF: + case RECURSE_SUBMODULES_DEFAULT: + /* + * --negotiate-only should never recurse into + * submodules. Skip it by setting recurse_submodules to + * RECURSE_SUBMODULES_OFF. + */ + config.recurse_submodules = RECURSE_SUBMODULES_OFF; + break; + + default: + die(_("options '%s' and '%s' cannot be used together"), + "--negotiate-only", "--recurse-submodules"); + } + } + + if (config.recurse_submodules != RECURSE_SUBMODULES_OFF) { + int *sfjc = config.submodule_fetch_jobs == -1 + ? &config.submodule_fetch_jobs : NULL; + int *rs = config.recurse_submodules == RECURSE_SUBMODULES_DEFAULT + ? &config.recurse_submodules : NULL; + + fetch_config_from_gitmodules(sfjc, rs); + } + + + if (porcelain) { + switch (recurse_submodules_cli) { + case RECURSE_SUBMODULES_OFF: + case RECURSE_SUBMODULES_DEFAULT: + /* + * Reference updates in submodules would be ambiguous + * in porcelain mode, so we reject this combination. + */ + config.recurse_submodules = RECURSE_SUBMODULES_OFF; + break; + + default: + die(_("options '%s' and '%s' cannot be used together"), + "--porcelain", "--recurse-submodules"); + } + + config.display_format = DISPLAY_FORMAT_PORCELAIN; + } + + if (negotiate_only && !negotiation_tip.nr) + die(_("--negotiate-only needs one or more --negotiation-tip=*")); + + if (deepen_relative) { + if (deepen_relative < 0) + die(_("negative depth in --deepen is not supported")); + if (depth) + die(_("options '%s' and '%s' cannot be used together"), "--deepen", "--depth"); + depth = xstrfmt("%d", deepen_relative); + } + if (unshallow) { + if (depth) + die(_("options '%s' and '%s' cannot be used together"), "--depth", "--unshallow"); + else if (!is_repository_shallow(the_repository)) + die(_("--unshallow on a complete repository does not make sense")); + else + depth = xstrfmt("%d", INFINITE_DEPTH); + } + + /* no need to be strict, transport_set_option() will validate it again */ + if (depth && atoi(depth) < 1) + die(_("depth %s is not a positive number"), depth); + if (depth || deepen_since || deepen_not.nr) + deepen = 1; + + /* FETCH_HEAD never gets updated in --dry-run mode */ + if (dry_run) + write_fetch_head = 0; + + if (!max_jobs) + max_jobs = online_cpus(); + + if (!git_config_get_string_tmp("fetch.bundleuri", &bundle_uri) && + fetch_bundle_uri(the_repository, bundle_uri, NULL)) + warning(_("failed to fetch bundles from '%s'"), bundle_uri); + + if (all) { + if (argc == 1) + die(_("fetch --all does not take a repository argument")); + else if (argc > 1) + die(_("fetch --all does not make sense with refspecs")); + (void) for_each_remote(get_one_remote_for_fetch, &list); + + /* do not do fetch_multiple() of one */ + if (list.nr == 1) + remote = remote_get(list.items[0].string); + } else if (argc == 0) { + /* No arguments -- use default remote */ + remote = remote_get(NULL); + } else if (multiple) { + /* All arguments are assumed to be remotes or groups */ + for (i = 0; i < argc; i++) + if (!add_remote_or_group(argv[i], &list)) + die(_("no such remote or remote group: %s"), + argv[i]); + } else { + /* Single remote or group */ + (void) add_remote_or_group(argv[0], &list); + if (list.nr > 1) { + /* More than one remote */ + if (argc > 1) + die(_("fetching a group and specifying refspecs does not make sense")); + } else { + /* Zero or one remotes */ + remote = remote_get(argv[0]); + prune_tags_ok = (argc == 1); + argc--; + argv++; + } + } + string_list_remove_duplicates(&list, 0); + + if (negotiate_only) { + struct oidset acked_commits = OIDSET_INIT; + struct oidset_iter iter; + const struct object_id *oid; + + if (!remote) + die(_("must supply remote when using --negotiate-only")); + gtransport = prepare_transport(remote, 1); + if (gtransport->smart_options) { + gtransport->smart_options->acked_commits = &acked_commits; + } else { + warning(_("protocol does not support --negotiate-only, exiting")); + result = 1; + goto cleanup; + } + if (server_options.nr) + gtransport->server_options = &server_options; + result = transport_fetch_refs(gtransport, NULL); + + oidset_iter_init(&acked_commits, &iter); + while ((oid = oidset_iter_next(&iter))) + printf("%s\n", oid_to_hex(oid)); + oidset_clear(&acked_commits); + } else if (remote) { + if (filter_options.choice || repo_has_promisor_remote(the_repository)) + fetch_one_setup_partial(remote); + result = fetch_one(remote, argc, argv, prune_tags_ok, stdin_refspecs, + &config); + } else { + int max_children = max_jobs; + + if (filter_options.choice) + die(_("--filter can only be used with the remote " + "configured in extensions.partialclone")); + + if (atomic_fetch) + die(_("--atomic can only be used when fetching " + "from one remote")); + + if (stdin_refspecs) + die(_("--stdin can only be used when fetching " + "from one remote")); + + if (max_children < 0) + max_children = config.parallel; + + /* TODO should this also die if we have a previous partial-clone? */ + result = fetch_multiple(&list, max_children, &config); + } + + /* + * This is only needed after fetch_one(), which does not fetch + * submodules by itself. + * + * When we fetch from multiple remotes, fetch_multiple() has + * already updated submodules to grab commits necessary for + * the fetched history from each remote, so there is no need + * to fetch submodules from here. + */ + if (!result && remote && (config.recurse_submodules != RECURSE_SUBMODULES_OFF)) { + struct strvec options = STRVEC_INIT; + int max_children = max_jobs; + + if (max_children < 0) + max_children = config.submodule_fetch_jobs; + if (max_children < 0) + max_children = config.parallel; + + add_options_to_argv(&options, &config); + result = fetch_submodules(the_repository, + &options, + submodule_prefix, + config.recurse_submodules, + recurse_submodules_default, + verbosity < 0, + max_children); + strvec_clear(&options); + } + + /* + * Skip irrelevant tasks because we know objects were not + * fetched. + * + * NEEDSWORK: as a future optimization, we can return early + * whenever objects were not fetched e.g. if we already have all + * of them. + */ + if (negotiate_only) + goto cleanup; + + prepare_repo_settings(the_repository); + if (fetch_write_commit_graph > 0 || + (fetch_write_commit_graph < 0 && + the_repository->settings.fetch_write_commit_graph)) { + int commit_graph_flags = COMMIT_GRAPH_WRITE_SPLIT; + + if (progress) + commit_graph_flags |= COMMIT_GRAPH_WRITE_PROGRESS; + + write_commit_graph_reachable(the_repository->objects->odb, + commit_graph_flags, + NULL); + } + + if (enable_auto_gc) { + if (refetch) { + /* + * Hint auto-maintenance strongly to encourage repacking, + * but respect config settings disabling it. + */ + int opt_val; + if (git_config_get_int("gc.autopacklimit", &opt_val)) + opt_val = -1; + if (opt_val != 0) + git_config_push_parameter("gc.autoPackLimit=1"); + + if (git_config_get_int("maintenance.incremental-repack.auto", &opt_val)) + opt_val = -1; + if (opt_val != 0) + git_config_push_parameter("maintenance.incremental-repack.auto=-1"); + } + run_auto_maintenance(verbosity < 0); + } + + cleanup: + string_list_clear(&list, 0); + return result; +} diff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c new file mode 100644 index 0000000..0f9855b --- /dev/null +++ b/builtin/fmt-merge-msg.c @@ -0,0 +1,70 @@ +#include "builtin.h" +#include "config.h" +#include "fmt-merge-msg.h" +#include "gettext.h" +#include "parse-options.h" + +static const char * const fmt_merge_msg_usage[] = { + N_("git fmt-merge-msg [-m <message>] [--log[=<n>] | --no-log] [--file <file>]"), + NULL +}; + +int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix) +{ + const char *inpath = NULL; + const char *message = NULL; + char *into_name = NULL; + int shortlog_len = -1; + struct option options[] = { + { OPTION_INTEGER, 0, "log", &shortlog_len, N_("n"), + N_("populate log with at most <n> entries from shortlog"), + PARSE_OPT_OPTARG, NULL, DEFAULT_MERGE_LOG_LEN }, + { OPTION_INTEGER, 0, "summary", &shortlog_len, N_("n"), + N_("alias for --log (deprecated)"), + PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN, NULL, + DEFAULT_MERGE_LOG_LEN }, + OPT_STRING('m', "message", &message, N_("text"), + N_("use <text> as start of message")), + OPT_STRING(0, "into-name", &into_name, N_("name"), + N_("use <name> instead of the real target branch")), + OPT_FILENAME('F', "file", &inpath, N_("file to read from")), + OPT_END() + }; + + FILE *in = stdin; + struct strbuf input = STRBUF_INIT, output = STRBUF_INIT; + int ret; + struct fmt_merge_msg_opts opts; + + git_config(fmt_merge_msg_config, NULL); + argc = parse_options(argc, argv, prefix, options, fmt_merge_msg_usage, + 0); + if (argc > 0) + usage_with_options(fmt_merge_msg_usage, options); + if (shortlog_len < 0) + shortlog_len = (merge_log_config > 0) ? merge_log_config : 0; + + if (inpath && strcmp(inpath, "-")) { + in = fopen(inpath, "r"); + if (!in) + die_errno("cannot open '%s'", inpath); + } + + if (strbuf_read(&input, fileno(in), 0) < 0) + die_errno("could not read input file"); + + if (message) + strbuf_addstr(&output, message); + + memset(&opts, 0, sizeof(opts)); + opts.add_title = !message; + opts.credit_people = 1; + opts.shortlog_len = shortlog_len; + opts.into_name = into_name; + + ret = fmt_merge_msg(&input, &output, &opts); + if (ret) + return ret; + write_in_full(STDOUT_FILENO, output.buf, output.len); + return 0; +} diff --git a/builtin/for-each-ref.c b/builtin/for-each-ref.c new file mode 100644 index 0000000..350bfa6 --- /dev/null +++ b/builtin/for-each-ref.c @@ -0,0 +1,128 @@ +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "refs.h" +#include "object.h" +#include "parse-options.h" +#include "ref-filter.h" +#include "strbuf.h" +#include "strvec.h" +#include "commit-reach.h" + +static char const * const for_each_ref_usage[] = { + N_("git for-each-ref [<options>] [<pattern>]"), + N_("git for-each-ref [--points-at <object>]"), + N_("git for-each-ref [--merged [<commit>]] [--no-merged [<commit>]]"), + N_("git for-each-ref [--contains [<commit>]] [--no-contains [<commit>]]"), + NULL +}; + +int cmd_for_each_ref(int argc, const char **argv, const char *prefix) +{ + int i; + struct ref_sorting *sorting; + struct string_list sorting_options = STRING_LIST_INIT_DUP; + int maxcount = 0, icase = 0, omit_empty = 0; + struct ref_array array; + struct ref_filter filter = REF_FILTER_INIT; + struct ref_format format = REF_FORMAT_INIT; + struct strbuf output = STRBUF_INIT; + struct strbuf err = STRBUF_INIT; + int from_stdin = 0; + struct strvec vec = STRVEC_INIT; + + struct option opts[] = { + OPT_BIT('s', "shell", &format.quote_style, + N_("quote placeholders suitably for shells"), QUOTE_SHELL), + OPT_BIT('p', "perl", &format.quote_style, + N_("quote placeholders suitably for perl"), QUOTE_PERL), + OPT_BIT(0 , "python", &format.quote_style, + N_("quote placeholders suitably for python"), QUOTE_PYTHON), + OPT_BIT(0 , "tcl", &format.quote_style, + N_("quote placeholders suitably for Tcl"), QUOTE_TCL), + OPT_BOOL(0, "omit-empty", &omit_empty, + N_("do not output a newline after empty formatted refs")), + + OPT_GROUP(""), + OPT_INTEGER( 0 , "count", &maxcount, N_("show only <n> matched refs")), + OPT_STRING( 0 , "format", &format.format, N_("format"), N_("format to use for the output")), + OPT__COLOR(&format.use_color, N_("respect format colors")), + OPT_REF_FILTER_EXCLUDE(&filter), + OPT_REF_SORT(&sorting_options), + OPT_CALLBACK(0, "points-at", &filter.points_at, + N_("object"), N_("print only refs which points at the given object"), + parse_opt_object_name), + OPT_MERGED(&filter, N_("print only refs that are merged")), + OPT_NO_MERGED(&filter, N_("print only refs that are not merged")), + OPT_CONTAINS(&filter.with_commit, N_("print only refs which contain the commit")), + OPT_NO_CONTAINS(&filter.no_commit, N_("print only refs which don't contain the commit")), + OPT_BOOL(0, "ignore-case", &icase, N_("sorting and filtering are case insensitive")), + OPT_BOOL(0, "stdin", &from_stdin, N_("read reference patterns from stdin")), + OPT_END(), + }; + + memset(&array, 0, sizeof(array)); + + format.format = "%(objectname) %(objecttype)\t%(refname)"; + + git_config(git_default_config, NULL); + + parse_options(argc, argv, prefix, opts, for_each_ref_usage, 0); + if (maxcount < 0) { + error("invalid --count argument: `%d'", maxcount); + usage_with_options(for_each_ref_usage, opts); + } + if (HAS_MULTI_BITS(format.quote_style)) { + error("more than one quoting style?"); + usage_with_options(for_each_ref_usage, opts); + } + if (verify_ref_format(&format)) + usage_with_options(for_each_ref_usage, opts); + + sorting = ref_sorting_options(&sorting_options); + ref_sorting_set_sort_flags_all(sorting, REF_SORTING_ICASE, icase); + filter.ignore_case = icase; + + if (from_stdin) { + struct strbuf line = STRBUF_INIT; + + if (argv[0]) + die(_("unknown arguments supplied with --stdin")); + + while (strbuf_getline(&line, stdin) != EOF) + strvec_push(&vec, line.buf); + + strbuf_release(&line); + + /* vec.v is NULL-terminated, just like 'argv'. */ + filter.name_patterns = vec.v; + } else { + filter.name_patterns = argv; + } + + filter.match_as_path = 1; + filter_refs(&array, &filter, FILTER_REFS_ALL); + filter_ahead_behind(the_repository, &format, &array); + + ref_array_sort(sorting, &array); + + if (!maxcount || array.nr < maxcount) + maxcount = array.nr; + for (i = 0; i < maxcount; i++) { + strbuf_reset(&err); + strbuf_reset(&output); + if (format_ref_array_item(array.items[i], &format, &output, &err)) + die("%s", err.buf); + fwrite(output.buf, 1, output.len, stdout); + if (output.len || !omit_empty) + putchar('\n'); + } + + strbuf_release(&err); + strbuf_release(&output); + ref_array_clear(&array); + ref_filter_clear(&filter); + ref_sorting_release(sorting); + strvec_clear(&vec); + return 0; +} diff --git a/builtin/for-each-repo.c b/builtin/for-each-repo.c new file mode 100644 index 0000000..28186b3 --- /dev/null +++ b/builtin/for-each-repo.c @@ -0,0 +1,62 @@ +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "parse-options.h" +#include "path.h" +#include "repository.h" +#include "run-command.h" +#include "string-list.h" + +static const char * const for_each_repo_usage[] = { + N_("git for-each-repo --config=<config> [--] <arguments>"), + NULL +}; + +static int run_command_on_repo(const char *path, int argc, const char ** argv) +{ + int i; + struct child_process child = CHILD_PROCESS_INIT; + char *abspath = interpolate_path(path, 0); + + child.git_cmd = 1; + strvec_pushl(&child.args, "-C", abspath, NULL); + + for (i = 0; i < argc; i++) + strvec_push(&child.args, argv[i]); + + free(abspath); + + return run_command(&child); +} + +int cmd_for_each_repo(int argc, const char **argv, const char *prefix) +{ + static const char *config_key = NULL; + int i, result = 0; + const struct string_list *values; + int err; + + const struct option options[] = { + OPT_STRING(0, "config", &config_key, N_("config"), + N_("config key storing a list of repository paths")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, for_each_repo_usage, + PARSE_OPT_STOP_AT_NON_OPTION); + + if (!config_key) + die(_("missing --config=<config>")); + + err = repo_config_get_string_multi(the_repository, config_key, &values); + if (err < 0) + usage_msg_optf(_("got bad config --config=%s"), + for_each_repo_usage, options, config_key); + else if (err) + return 0; + + for (i = 0; !result && i < values->nr; i++) + result = run_command_on_repo(values->items[i].string, argc, argv); + + return result; +} diff --git a/builtin/fsck.c b/builtin/fsck.c new file mode 100644 index 0000000..6119259 --- /dev/null +++ b/builtin/fsck.c @@ -0,0 +1,1105 @@ +#include "builtin.h" +#include "gettext.h" +#include "hex.h" +#include "repository.h" +#include "config.h" +#include "commit.h" +#include "tree.h" +#include "blob.h" +#include "tag.h" +#include "refs.h" +#include "pack.h" +#include "cache-tree.h" +#include "tree-walk.h" +#include "fsck.h" +#include "parse-options.h" +#include "dir.h" +#include "progress.h" +#include "streaming.h" +#include "decorate.h" +#include "packfile.h" +#include "object-file.h" +#include "object-name.h" +#include "object-store-ll.h" +#include "path.h" +#include "read-cache-ll.h" +#include "replace-object.h" +#include "resolve-undo.h" +#include "run-command.h" +#include "sparse-index.h" +#include "worktree.h" +#include "pack-revindex.h" +#include "pack-bitmap.h" + +#define REACHABLE 0x0001 +#define SEEN 0x0002 +#define HAS_OBJ 0x0004 +/* This flag is set if something points to this object. */ +#define USED 0x0008 + +static int show_root; +static int show_tags; +static int show_unreachable; +static int include_reflogs = 1; +static int check_full = 1; +static int connectivity_only; +static int check_strict; +static int keep_cache_objects; +static struct fsck_options fsck_walk_options = FSCK_OPTIONS_DEFAULT; +static struct fsck_options fsck_obj_options = FSCK_OPTIONS_DEFAULT; +static int errors_found; +static int write_lost_and_found; +static int verbose; +static int show_progress = -1; +static int show_dangling = 1; +static int name_objects; +#define ERROR_OBJECT 01 +#define ERROR_REACHABLE 02 +#define ERROR_PACK 04 +#define ERROR_REFS 010 +#define ERROR_COMMIT_GRAPH 020 +#define ERROR_MULTI_PACK_INDEX 040 +#define ERROR_PACK_REV_INDEX 0100 +#define ERROR_BITMAP 0200 + +static const char *describe_object(const struct object_id *oid) +{ + return fsck_describe_object(&fsck_walk_options, oid); +} + +static const char *printable_type(const struct object_id *oid, + enum object_type type) +{ + const char *ret; + + if (type == OBJ_NONE) + type = oid_object_info(the_repository, oid, NULL); + + ret = type_name(type); + if (!ret) + ret = _("unknown"); + + return ret; +} + +static int objerror(struct object *obj, const char *err) +{ + errors_found |= ERROR_OBJECT; + /* TRANSLATORS: e.g. error in tree 01bfda: <more explanation> */ + fprintf_ln(stderr, _("error in %s %s: %s"), + printable_type(&obj->oid, obj->type), + describe_object(&obj->oid), err); + return -1; +} + +static int fsck_error_func(struct fsck_options *o UNUSED, + const struct object_id *oid, + enum object_type object_type, + enum fsck_msg_type msg_type, + enum fsck_msg_id msg_id UNUSED, + const char *message) +{ + switch (msg_type) { + case FSCK_WARN: + /* TRANSLATORS: e.g. warning in tree 01bfda: <more explanation> */ + fprintf_ln(stderr, _("warning in %s %s: %s"), + printable_type(oid, object_type), + describe_object(oid), message); + return 0; + case FSCK_ERROR: + /* TRANSLATORS: e.g. error in tree 01bfda: <more explanation> */ + fprintf_ln(stderr, _("error in %s %s: %s"), + printable_type(oid, object_type), + describe_object(oid), message); + return 1; + default: + BUG("%d (FSCK_IGNORE?) should never trigger this callback", + msg_type); + } +} + +static struct object_array pending; + +static int mark_object(struct object *obj, enum object_type type, + void *data, struct fsck_options *options UNUSED) +{ + struct object *parent = data; + + /* + * The only case data is NULL or type is OBJ_ANY is when + * mark_object_reachable() calls us. All the callers of + * that function has non-NULL obj hence ... + */ + if (!obj) { + /* ... these references to parent->fld are safe here */ + printf_ln(_("broken link from %7s %s"), + printable_type(&parent->oid, parent->type), + describe_object(&parent->oid)); + printf_ln(_("broken link from %7s %s"), + (type == OBJ_ANY ? _("unknown") : type_name(type)), + _("unknown")); + errors_found |= ERROR_REACHABLE; + return 1; + } + + if (type != OBJ_ANY && obj->type != type) + /* ... and the reference to parent is safe here */ + objerror(parent, _("wrong object type in link")); + + if (obj->flags & REACHABLE) + return 0; + obj->flags |= REACHABLE; + + if (is_promisor_object(&obj->oid)) + /* + * Further recursion does not need to be performed on this + * object since it is a promisor object (so it does not need to + * be added to "pending"). + */ + return 0; + + if (!(obj->flags & HAS_OBJ)) { + if (parent && !has_object(the_repository, &obj->oid, 1)) { + printf_ln(_("broken link from %7s %s\n" + " to %7s %s"), + printable_type(&parent->oid, parent->type), + describe_object(&parent->oid), + printable_type(&obj->oid, obj->type), + describe_object(&obj->oid)); + errors_found |= ERROR_REACHABLE; + } + return 1; + } + + add_object_array(obj, NULL, &pending); + return 0; +} + +static void mark_object_reachable(struct object *obj) +{ + mark_object(obj, OBJ_ANY, NULL, NULL); +} + +static int traverse_one_object(struct object *obj) +{ + int result = fsck_walk(obj, obj, &fsck_walk_options); + + if (obj->type == OBJ_TREE) { + struct tree *tree = (struct tree *)obj; + free_tree_buffer(tree); + } + return result; +} + +static int traverse_reachable(void) +{ + struct progress *progress = NULL; + unsigned int nr = 0; + int result = 0; + if (show_progress) + progress = start_delayed_progress(_("Checking connectivity"), 0); + while (pending.nr) { + result |= traverse_one_object(object_array_pop(&pending)); + display_progress(progress, ++nr); + } + stop_progress(&progress); + return !!result; +} + +static int mark_used(struct object *obj, enum object_type type UNUSED, + void *data UNUSED, struct fsck_options *options UNUSED) +{ + if (!obj) + return 1; + obj->flags |= USED; + return 0; +} + +static void mark_unreachable_referents(const struct object_id *oid) +{ + struct fsck_options options = FSCK_OPTIONS_DEFAULT; + struct object *obj = lookup_object(the_repository, oid); + + if (!obj || !(obj->flags & HAS_OBJ)) + return; /* not part of our original set */ + if (obj->flags & REACHABLE) + return; /* reachable objects already traversed */ + + /* + * Avoid passing OBJ_NONE to fsck_walk, which will parse the object + * (and we want to avoid parsing blobs). + */ + if (obj->type == OBJ_NONE) { + enum object_type type = oid_object_info(the_repository, + &obj->oid, NULL); + if (type > 0) + object_as_type(obj, type, 0); + } + + options.walk = mark_used; + fsck_walk(obj, NULL, &options); + if (obj->type == OBJ_TREE) + free_tree_buffer((struct tree *)obj); +} + +static int mark_loose_unreachable_referents(const struct object_id *oid, + const char *path UNUSED, + void *data UNUSED) +{ + mark_unreachable_referents(oid); + return 0; +} + +static int mark_packed_unreachable_referents(const struct object_id *oid, + struct packed_git *pack UNUSED, + uint32_t pos UNUSED, + void *data UNUSED) +{ + mark_unreachable_referents(oid); + return 0; +} + +/* + * Check a single reachable object + */ +static void check_reachable_object(struct object *obj) +{ + /* + * We obviously want the object to be parsed, + * except if it was in a pack-file and we didn't + * do a full fsck + */ + if (!(obj->flags & HAS_OBJ)) { + if (is_promisor_object(&obj->oid)) + return; + if (has_object_pack(&obj->oid)) + return; /* it is in pack - forget about it */ + printf_ln(_("missing %s %s"), + printable_type(&obj->oid, obj->type), + describe_object(&obj->oid)); + errors_found |= ERROR_REACHABLE; + return; + } +} + +/* + * Check a single unreachable object + */ +static void check_unreachable_object(struct object *obj) +{ + /* + * Missing unreachable object? Ignore it. It's not like + * we miss it (since it can't be reached), nor do we want + * to complain about it being unreachable (since it does + * not exist). + */ + if (!(obj->flags & HAS_OBJ)) + return; + + /* + * Unreachable object that exists? Show it if asked to, + * since this is something that is prunable. + */ + if (show_unreachable) { + printf_ln(_("unreachable %s %s"), + printable_type(&obj->oid, obj->type), + describe_object(&obj->oid)); + return; + } + + /* + * "!USED" means that nothing at all points to it, including + * other unreachable objects. In other words, it's the "tip" + * of some set of unreachable objects, usually a commit that + * got dropped. + * + * Such starting points are more interesting than some random + * set of unreachable objects, so we show them even if the user + * hasn't asked for _all_ unreachable objects. If you have + * deleted a branch by mistake, this is a prime candidate to + * start looking at, for example. + */ + if (!(obj->flags & USED)) { + if (show_dangling) + printf_ln(_("dangling %s %s"), + printable_type(&obj->oid, obj->type), + describe_object(&obj->oid)); + if (write_lost_and_found) { + char *filename = git_pathdup("lost-found/%s/%s", + obj->type == OBJ_COMMIT ? "commit" : "other", + describe_object(&obj->oid)); + FILE *f; + + if (safe_create_leading_directories_const(filename)) { + error(_("could not create lost-found")); + free(filename); + return; + } + f = xfopen(filename, "w"); + if (obj->type == OBJ_BLOB) { + if (stream_blob_to_fd(fileno(f), &obj->oid, NULL, 1)) + die_errno(_("could not write '%s'"), filename); + } else + fprintf(f, "%s\n", describe_object(&obj->oid)); + if (fclose(f)) + die_errno(_("could not finish '%s'"), + filename); + free(filename); + } + return; + } + + /* + * Otherwise? It's there, it's unreachable, and some other unreachable + * object points to it. Ignore it - it's not interesting, and we showed + * all the interesting cases above. + */ +} + +static void check_object(struct object *obj) +{ + if (verbose) + fprintf_ln(stderr, _("Checking %s"), describe_object(&obj->oid)); + + if (obj->flags & REACHABLE) + check_reachable_object(obj); + else + check_unreachable_object(obj); +} + +static void check_connectivity(void) +{ + int i, max; + + /* Traverse the pending reachable objects */ + traverse_reachable(); + + /* + * With --connectivity-only, we won't have actually opened and marked + * unreachable objects with USED. Do that now to make --dangling, etc + * accurate. + */ + if (connectivity_only && (show_dangling || write_lost_and_found)) { + /* + * Even though we already have a "struct object" for each of + * these in memory, we must not iterate over the internal + * object hash as we do below. Our loop would potentially + * resize the hash, making our iteration invalid. + * + * Instead, we'll just go back to the source list of objects, + * and ignore any that weren't present in our earlier + * traversal. + */ + for_each_loose_object(mark_loose_unreachable_referents, NULL, 0); + for_each_packed_object(mark_packed_unreachable_referents, NULL, 0); + } + + /* Look up all the requirements, warn about missing objects.. */ + max = get_max_object_index(); + if (verbose) + fprintf_ln(stderr, _("Checking connectivity (%d objects)"), max); + + for (i = 0; i < max; i++) { + struct object *obj = get_indexed_object(i); + + if (obj) + check_object(obj); + } +} + +static int fsck_obj(struct object *obj, void *buffer, unsigned long size) +{ + int err; + + if (obj->flags & SEEN) + return 0; + obj->flags |= SEEN; + + if (verbose) + fprintf_ln(stderr, _("Checking %s %s"), + printable_type(&obj->oid, obj->type), + describe_object(&obj->oid)); + + if (fsck_walk(obj, NULL, &fsck_obj_options)) + objerror(obj, _("broken links")); + err = fsck_object(obj, buffer, size, &fsck_obj_options); + if (err) + goto out; + + if (obj->type == OBJ_COMMIT) { + struct commit *commit = (struct commit *) obj; + + if (!commit->parents && show_root) + printf_ln(_("root %s"), + describe_object(&commit->object.oid)); + } + + if (obj->type == OBJ_TAG) { + struct tag *tag = (struct tag *) obj; + + if (show_tags && tag->tagged) { + printf_ln(_("tagged %s %s (%s) in %s"), + printable_type(&tag->tagged->oid, tag->tagged->type), + describe_object(&tag->tagged->oid), + tag->tag, + describe_object(&tag->object.oid)); + } + } + +out: + if (obj->type == OBJ_TREE) + free_tree_buffer((struct tree *)obj); + return err; +} + +static int fsck_obj_buffer(const struct object_id *oid, enum object_type type, + unsigned long size, void *buffer, int *eaten) +{ + /* + * Note, buffer may be NULL if type is OBJ_BLOB. See + * verify_packfile(), data_valid variable for details. + */ + struct object *obj; + obj = parse_object_buffer(the_repository, oid, type, size, buffer, + eaten); + if (!obj) { + errors_found |= ERROR_OBJECT; + return error(_("%s: object corrupt or missing"), + oid_to_hex(oid)); + } + obj->flags &= ~(REACHABLE | SEEN); + obj->flags |= HAS_OBJ; + return fsck_obj(obj, buffer, size); +} + +static int default_refs; + +static void fsck_handle_reflog_oid(const char *refname, struct object_id *oid, + timestamp_t timestamp) +{ + struct object *obj; + + if (!is_null_oid(oid)) { + obj = lookup_object(the_repository, oid); + if (obj && (obj->flags & HAS_OBJ)) { + if (timestamp) + fsck_put_object_name(&fsck_walk_options, oid, + "%s@{%"PRItime"}", + refname, timestamp); + obj->flags |= USED; + mark_object_reachable(obj); + } else if (!is_promisor_object(oid)) { + error(_("%s: invalid reflog entry %s"), + refname, oid_to_hex(oid)); + errors_found |= ERROR_REACHABLE; + } + } +} + +static int fsck_handle_reflog_ent(struct object_id *ooid, struct object_id *noid, + const char *email UNUSED, + timestamp_t timestamp, int tz UNUSED, + const char *message UNUSED, void *cb_data) +{ + const char *refname = cb_data; + + if (verbose) + fprintf_ln(stderr, _("Checking reflog %s->%s"), + oid_to_hex(ooid), oid_to_hex(noid)); + + fsck_handle_reflog_oid(refname, ooid, 0); + fsck_handle_reflog_oid(refname, noid, timestamp); + return 0; +} + +static int fsck_handle_reflog(const char *logname, + const struct object_id *oid UNUSED, + int flag UNUSED, void *cb_data) +{ + struct strbuf refname = STRBUF_INIT; + + strbuf_worktree_ref(cb_data, &refname, logname); + for_each_reflog_ent(refname.buf, fsck_handle_reflog_ent, refname.buf); + strbuf_release(&refname); + return 0; +} + +static int fsck_handle_ref(const char *refname, const struct object_id *oid, + int flag UNUSED, void *cb_data UNUSED) +{ + struct object *obj; + + obj = parse_object(the_repository, oid); + if (!obj) { + if (is_promisor_object(oid)) { + /* + * Increment default_refs anyway, because this is a + * valid ref. + */ + default_refs++; + return 0; + } + error(_("%s: invalid sha1 pointer %s"), + refname, oid_to_hex(oid)); + errors_found |= ERROR_REACHABLE; + /* We'll continue with the rest despite the error.. */ + return 0; + } + if (obj->type != OBJ_COMMIT && is_branch(refname)) { + error(_("%s: not a commit"), refname); + errors_found |= ERROR_REFS; + } + default_refs++; + obj->flags |= USED; + fsck_put_object_name(&fsck_walk_options, + oid, "%s", refname); + mark_object_reachable(obj); + + return 0; +} + +static int fsck_head_link(const char *head_ref_name, + const char **head_points_at, + struct object_id *head_oid); + +static void get_default_heads(void) +{ + struct worktree **worktrees, **p; + const char *head_points_at; + struct object_id head_oid; + + for_each_rawref(fsck_handle_ref, NULL); + + worktrees = get_worktrees(); + for (p = worktrees; *p; p++) { + struct worktree *wt = *p; + struct strbuf ref = STRBUF_INIT; + + strbuf_worktree_ref(wt, &ref, "HEAD"); + fsck_head_link(ref.buf, &head_points_at, &head_oid); + if (head_points_at && !is_null_oid(&head_oid)) + fsck_handle_ref(ref.buf, &head_oid, 0, NULL); + strbuf_release(&ref); + + if (include_reflogs) + refs_for_each_reflog(get_worktree_ref_store(wt), + fsck_handle_reflog, wt); + } + free_worktrees(worktrees); + + /* + * Not having any default heads isn't really fatal, but + * it does mean that "--unreachable" no longer makes any + * sense (since in this case everything will obviously + * be unreachable by definition. + * + * Showing dangling objects is valid, though (as those + * dangling objects are likely lost heads). + * + * So we just print a warning about it, and clear the + * "show_unreachable" flag. + */ + if (!default_refs) { + fprintf_ln(stderr, _("notice: No default references")); + show_unreachable = 0; + } +} + +struct for_each_loose_cb +{ + struct progress *progress; + struct strbuf obj_type; +}; + +static int fsck_loose(const struct object_id *oid, const char *path, void *data) +{ + struct for_each_loose_cb *cb_data = data; + struct object *obj; + enum object_type type = OBJ_NONE; + unsigned long size; + void *contents = NULL; + int eaten; + struct object_info oi = OBJECT_INFO_INIT; + struct object_id real_oid = *null_oid(); + int err = 0; + + strbuf_reset(&cb_data->obj_type); + oi.type_name = &cb_data->obj_type; + oi.sizep = &size; + oi.typep = &type; + + if (read_loose_object(path, oid, &real_oid, &contents, &oi) < 0) { + if (contents && !oideq(&real_oid, oid)) + err = error(_("%s: hash-path mismatch, found at: %s"), + oid_to_hex(&real_oid), path); + else + err = error(_("%s: object corrupt or missing: %s"), + oid_to_hex(oid), path); + } + if (type != OBJ_NONE && type < 0) + err = error(_("%s: object is of unknown type '%s': %s"), + oid_to_hex(&real_oid), cb_data->obj_type.buf, + path); + if (err < 0) { + errors_found |= ERROR_OBJECT; + free(contents); + return 0; /* keep checking other objects */ + } + + if (!contents && type != OBJ_BLOB) + BUG("read_loose_object streamed a non-blob"); + + obj = parse_object_buffer(the_repository, oid, type, size, + contents, &eaten); + + if (!obj) { + errors_found |= ERROR_OBJECT; + error(_("%s: object could not be parsed: %s"), + oid_to_hex(oid), path); + if (!eaten) + free(contents); + return 0; /* keep checking other objects */ + } + + obj->flags &= ~(REACHABLE | SEEN); + obj->flags |= HAS_OBJ; + if (fsck_obj(obj, contents, size)) + errors_found |= ERROR_OBJECT; + + if (!eaten) + free(contents); + return 0; /* keep checking other objects, even if we saw an error */ +} + +static int fsck_cruft(const char *basename, const char *path, + void *data UNUSED) +{ + if (!starts_with(basename, "tmp_obj_")) + fprintf_ln(stderr, _("bad sha1 file: %s"), path); + return 0; +} + +static int fsck_subdir(unsigned int nr, const char *path UNUSED, void *data) +{ + struct for_each_loose_cb *cb_data = data; + struct progress *progress = cb_data->progress; + display_progress(progress, nr + 1); + return 0; +} + +static void fsck_object_dir(const char *path) +{ + struct progress *progress = NULL; + struct for_each_loose_cb cb_data = { + .obj_type = STRBUF_INIT, + .progress = progress, + }; + + if (verbose) + fprintf_ln(stderr, _("Checking object directory")); + + if (show_progress) + progress = start_progress(_("Checking object directories"), 256); + + for_each_loose_file_in_objdir(path, fsck_loose, fsck_cruft, fsck_subdir, + &cb_data); + display_progress(progress, 256); + stop_progress(&progress); + strbuf_release(&cb_data.obj_type); +} + +static int fsck_head_link(const char *head_ref_name, + const char **head_points_at, + struct object_id *head_oid) +{ + int null_is_error = 0; + + if (verbose) + fprintf_ln(stderr, _("Checking %s link"), head_ref_name); + + *head_points_at = resolve_ref_unsafe(head_ref_name, 0, head_oid, NULL); + if (!*head_points_at) { + errors_found |= ERROR_REFS; + return error(_("invalid %s"), head_ref_name); + } + if (!strcmp(*head_points_at, head_ref_name)) + /* detached HEAD */ + null_is_error = 1; + else if (!starts_with(*head_points_at, "refs/heads/")) { + errors_found |= ERROR_REFS; + return error(_("%s points to something strange (%s)"), + head_ref_name, *head_points_at); + } + if (is_null_oid(head_oid)) { + if (null_is_error) { + errors_found |= ERROR_REFS; + return error(_("%s: detached HEAD points at nothing"), + head_ref_name); + } + fprintf_ln(stderr, + _("notice: %s points to an unborn branch (%s)"), + head_ref_name, *head_points_at + 11); + } + return 0; +} + +static int fsck_cache_tree(struct cache_tree *it, const char *index_path) +{ + int i; + int err = 0; + + if (verbose) + fprintf_ln(stderr, _("Checking cache tree of %s"), index_path); + + if (0 <= it->entry_count) { + struct object *obj = parse_object(the_repository, &it->oid); + if (!obj) { + error(_("%s: invalid sha1 pointer in cache-tree of %s"), + oid_to_hex(&it->oid), index_path); + errors_found |= ERROR_REFS; + return 1; + } + obj->flags |= USED; + fsck_put_object_name(&fsck_walk_options, &it->oid, ":"); + mark_object_reachable(obj); + if (obj->type != OBJ_TREE) + err |= objerror(obj, _("non-tree in cache-tree")); + } + for (i = 0; i < it->subtree_nr; i++) + err |= fsck_cache_tree(it->down[i]->cache_tree, index_path); + return err; +} + +static int fsck_resolve_undo(struct index_state *istate, + const char *index_path) +{ + struct string_list_item *item; + struct string_list *resolve_undo = istate->resolve_undo; + + if (!resolve_undo) + return 0; + + for_each_string_list_item(item, resolve_undo) { + const char *path = item->string; + struct resolve_undo_info *ru = item->util; + int i; + + if (!ru) + continue; + for (i = 0; i < 3; i++) { + struct object *obj; + + if (!ru->mode[i] || !S_ISREG(ru->mode[i])) + continue; + + obj = parse_object(the_repository, &ru->oid[i]); + if (!obj) { + error(_("%s: invalid sha1 pointer in resolve-undo of %s"), + oid_to_hex(&ru->oid[i]), + index_path); + errors_found |= ERROR_REFS; + continue; + } + obj->flags |= USED; + fsck_put_object_name(&fsck_walk_options, &ru->oid[i], + ":(%d):%s", i, path); + mark_object_reachable(obj); + } + } + return 0; +} + +static void fsck_index(struct index_state *istate, const char *index_path, + int is_current_worktree) +{ + unsigned int i; + + /* TODO: audit for interaction with sparse-index. */ + ensure_full_index(istate); + for (i = 0; i < istate->cache_nr; i++) { + unsigned int mode; + struct blob *blob; + struct object *obj; + + mode = istate->cache[i]->ce_mode; + if (S_ISGITLINK(mode)) + continue; + blob = lookup_blob(the_repository, + &istate->cache[i]->oid); + if (!blob) + continue; + obj = &blob->object; + obj->flags |= USED; + fsck_put_object_name(&fsck_walk_options, &obj->oid, + "%s:%s", + is_current_worktree ? "" : index_path, + istate->cache[i]->name); + mark_object_reachable(obj); + } + if (istate->cache_tree) + fsck_cache_tree(istate->cache_tree, index_path); + fsck_resolve_undo(istate, index_path); +} + +static void mark_object_for_connectivity(const struct object_id *oid) +{ + struct object *obj = lookup_unknown_object(the_repository, oid); + obj->flags |= HAS_OBJ; +} + +static int mark_loose_for_connectivity(const struct object_id *oid, + const char *path UNUSED, + void *data UNUSED) +{ + mark_object_for_connectivity(oid); + return 0; +} + +static int mark_packed_for_connectivity(const struct object_id *oid, + struct packed_git *pack UNUSED, + uint32_t pos UNUSED, + void *data UNUSED) +{ + mark_object_for_connectivity(oid); + return 0; +} + +static int check_pack_rev_indexes(struct repository *r, int show_progress) +{ + struct progress *progress = NULL; + uint32_t pack_count = 0; + int res = 0; + + if (show_progress) { + for (struct packed_git *p = get_all_packs(r); p; p = p->next) + pack_count++; + progress = start_delayed_progress("Verifying reverse pack-indexes", pack_count); + pack_count = 0; + } + + for (struct packed_git *p = get_all_packs(r); p; p = p->next) { + int load_error = load_pack_revindex_from_disk(p); + + if (load_error < 0) { + error(_("unable to load rev-index for pack '%s'"), p->pack_name); + res = ERROR_PACK_REV_INDEX; + } else if (!load_error && + !load_pack_revindex(r, p) && + verify_pack_revindex(p)) { + error(_("invalid rev-index for pack '%s'"), p->pack_name); + res = ERROR_PACK_REV_INDEX; + } + display_progress(progress, ++pack_count); + } + stop_progress(&progress); + + return res; +} + +static char const * const fsck_usage[] = { + N_("git fsck [--tags] [--root] [--unreachable] [--cache] [--no-reflogs]\n" + " [--[no-]full] [--strict] [--verbose] [--lost-found]\n" + " [--[no-]dangling] [--[no-]progress] [--connectivity-only]\n" + " [--[no-]name-objects] [<object>...]"), + NULL +}; + +static struct option fsck_opts[] = { + OPT__VERBOSE(&verbose, N_("be verbose")), + OPT_BOOL(0, "unreachable", &show_unreachable, N_("show unreachable objects")), + OPT_BOOL(0, "dangling", &show_dangling, N_("show dangling objects")), + OPT_BOOL(0, "tags", &show_tags, N_("report tags")), + OPT_BOOL(0, "root", &show_root, N_("report root nodes")), + OPT_BOOL(0, "cache", &keep_cache_objects, N_("make index objects head nodes")), + OPT_BOOL(0, "reflogs", &include_reflogs, N_("make reflogs head nodes (default)")), + OPT_BOOL(0, "full", &check_full, N_("also consider packs and alternate objects")), + OPT_BOOL(0, "connectivity-only", &connectivity_only, N_("check only connectivity")), + OPT_BOOL(0, "strict", &check_strict, N_("enable more strict checking")), + OPT_BOOL(0, "lost-found", &write_lost_and_found, + N_("write dangling objects in .git/lost-found")), + OPT_BOOL(0, "progress", &show_progress, N_("show progress")), + OPT_BOOL(0, "name-objects", &name_objects, N_("show verbose names for reachable objects")), + OPT_END(), +}; + +int cmd_fsck(int argc, const char **argv, const char *prefix) +{ + int i; + struct object_directory *odb; + + /* fsck knows how to handle missing promisor objects */ + fetch_if_missing = 0; + + errors_found = 0; + disable_replace_refs(); + save_commit_buffer = 0; + + argc = parse_options(argc, argv, prefix, fsck_opts, fsck_usage, 0); + + fsck_walk_options.walk = mark_object; + fsck_obj_options.walk = mark_used; + fsck_obj_options.error_func = fsck_error_func; + if (check_strict) + fsck_obj_options.strict = 1; + + if (show_progress == -1) + show_progress = isatty(2); + if (verbose) + show_progress = 0; + + if (write_lost_and_found) { + check_full = 1; + include_reflogs = 0; + } + + if (name_objects) + fsck_enable_object_names(&fsck_walk_options); + + git_config(git_fsck_config, &fsck_obj_options); + prepare_repo_settings(the_repository); + + if (connectivity_only) { + for_each_loose_object(mark_loose_for_connectivity, NULL, 0); + for_each_packed_object(mark_packed_for_connectivity, NULL, 0); + } else { + prepare_alt_odb(the_repository); + for (odb = the_repository->objects->odb; odb; odb = odb->next) + fsck_object_dir(odb->path); + + if (check_full) { + struct packed_git *p; + uint32_t total = 0, count = 0; + struct progress *progress = NULL; + + if (show_progress) { + for (p = get_all_packs(the_repository); p; + p = p->next) { + if (open_pack_index(p)) + continue; + total += p->num_objects; + } + + progress = start_progress(_("Checking objects"), total); + } + for (p = get_all_packs(the_repository); p; + p = p->next) { + /* verify gives error messages itself */ + if (verify_pack(the_repository, + p, fsck_obj_buffer, + progress, count)) + errors_found |= ERROR_PACK; + count += p->num_objects; + } + stop_progress(&progress); + } + + if (fsck_finish(&fsck_obj_options)) + errors_found |= ERROR_OBJECT; + } + + for (i = 0; i < argc; i++) { + const char *arg = argv[i]; + struct object_id oid; + if (!repo_get_oid(the_repository, arg, &oid)) { + struct object *obj = lookup_object(the_repository, + &oid); + + if (!obj || !(obj->flags & HAS_OBJ)) { + if (is_promisor_object(&oid)) + continue; + error(_("%s: object missing"), oid_to_hex(&oid)); + errors_found |= ERROR_OBJECT; + continue; + } + + obj->flags |= USED; + fsck_put_object_name(&fsck_walk_options, &oid, + "%s", arg); + mark_object_reachable(obj); + continue; + } + error(_("invalid parameter: expected sha1, got '%s'"), arg); + errors_found |= ERROR_OBJECT; + } + + /* + * If we've not been given any explicit head information, do the + * default ones from .git/refs. We also consider the index file + * in this case (ie this implies --cache). + */ + if (!argc) { + get_default_heads(); + keep_cache_objects = 1; + } + + if (keep_cache_objects) { + struct worktree **worktrees, **p; + + verify_index_checksum = 1; + verify_ce_order = 1; + + worktrees = get_worktrees(); + for (p = worktrees; *p; p++) { + struct worktree *wt = *p; + struct index_state istate = + INDEX_STATE_INIT(the_repository); + char *path; + + /* + * Make a copy since the buffer is reusable + * and may get overwritten by other calls + * while we're examining the index. + */ + path = xstrdup(worktree_git_path(wt, "index")); + read_index_from(&istate, path, get_worktree_git_dir(wt)); + fsck_index(&istate, path, wt->is_current); + discard_index(&istate); + free(path); + } + free_worktrees(worktrees); + } + + errors_found |= check_pack_rev_indexes(the_repository, show_progress); + if (verify_bitmap_files(the_repository)) + errors_found |= ERROR_BITMAP; + + check_connectivity(); + + if (the_repository->settings.core_commit_graph) { + struct child_process commit_graph_verify = CHILD_PROCESS_INIT; + + prepare_alt_odb(the_repository); + for (odb = the_repository->objects->odb; odb; odb = odb->next) { + child_process_init(&commit_graph_verify); + commit_graph_verify.git_cmd = 1; + strvec_pushl(&commit_graph_verify.args, "commit-graph", + "verify", "--object-dir", odb->path, NULL); + if (show_progress) + strvec_push(&commit_graph_verify.args, "--progress"); + else + strvec_push(&commit_graph_verify.args, "--no-progress"); + if (run_command(&commit_graph_verify)) + errors_found |= ERROR_COMMIT_GRAPH; + } + } + + if (the_repository->settings.core_multi_pack_index) { + struct child_process midx_verify = CHILD_PROCESS_INIT; + + prepare_alt_odb(the_repository); + for (odb = the_repository->objects->odb; odb; odb = odb->next) { + child_process_init(&midx_verify); + midx_verify.git_cmd = 1; + strvec_pushl(&midx_verify.args, "multi-pack-index", + "verify", "--object-dir", odb->path, NULL); + if (show_progress) + strvec_push(&midx_verify.args, "--progress"); + else + strvec_push(&midx_verify.args, "--no-progress"); + if (run_command(&midx_verify)) + errors_found |= ERROR_MULTI_PACK_INDEX; + } + } + + return errors_found; +} diff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c new file mode 100644 index 0000000..5d01db5 --- /dev/null +++ b/builtin/fsmonitor--daemon.c @@ -0,0 +1,1597 @@ +#include "builtin.h" +#include "abspath.h" +#include "config.h" +#include "environment.h" +#include "gettext.h" +#include "parse-options.h" +#include "fsmonitor-ll.h" +#include "fsmonitor-ipc.h" +#include "fsmonitor-path-utils.h" +#include "fsmonitor-settings.h" +#include "compat/fsmonitor/fsm-health.h" +#include "compat/fsmonitor/fsm-listen.h" +#include "fsmonitor--daemon.h" +#include "simple-ipc.h" +#include "khash.h" +#include "pkt-line.h" +#include "trace.h" +#include "trace2.h" + +static const char * const builtin_fsmonitor__daemon_usage[] = { + N_("git fsmonitor--daemon start [<options>]"), + N_("git fsmonitor--daemon run [<options>]"), + "git fsmonitor--daemon stop", + "git fsmonitor--daemon status", + NULL +}; + +#ifdef HAVE_FSMONITOR_DAEMON_BACKEND +/* + * Global state loaded from config. + */ +#define FSMONITOR__IPC_THREADS "fsmonitor.ipcthreads" +static int fsmonitor__ipc_threads = 8; + +#define FSMONITOR__START_TIMEOUT "fsmonitor.starttimeout" +static int fsmonitor__start_timeout_sec = 60; + +#define FSMONITOR__ANNOUNCE_STARTUP "fsmonitor.announcestartup" +static int fsmonitor__announce_startup = 0; + +static int fsmonitor_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + if (!strcmp(var, FSMONITOR__IPC_THREADS)) { + int i = git_config_int(var, value, ctx->kvi); + if (i < 1) + return error(_("value of '%s' out of range: %d"), + FSMONITOR__IPC_THREADS, i); + fsmonitor__ipc_threads = i; + return 0; + } + + if (!strcmp(var, FSMONITOR__START_TIMEOUT)) { + int i = git_config_int(var, value, ctx->kvi); + if (i < 0) + return error(_("value of '%s' out of range: %d"), + FSMONITOR__START_TIMEOUT, i); + fsmonitor__start_timeout_sec = i; + return 0; + } + + if (!strcmp(var, FSMONITOR__ANNOUNCE_STARTUP)) { + int is_bool; + int i = git_config_bool_or_int(var, value, ctx->kvi, &is_bool); + if (i < 0) + return error(_("value of '%s' not bool or int: %d"), + var, i); + fsmonitor__announce_startup = i; + return 0; + } + + return git_default_config(var, value, ctx, cb); +} + +/* + * Acting as a CLIENT. + * + * Send a "quit" command to the `git-fsmonitor--daemon` (if running) + * and wait for it to shutdown. + */ +static int do_as_client__send_stop(void) +{ + struct strbuf answer = STRBUF_INIT; + int ret; + + ret = fsmonitor_ipc__send_command("quit", &answer); + + /* The quit command does not return any response data. */ + strbuf_release(&answer); + + if (ret) + return ret; + + trace2_region_enter("fsm_client", "polling-for-daemon-exit", NULL); + while (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) + sleep_millisec(50); + trace2_region_leave("fsm_client", "polling-for-daemon-exit", NULL); + + return 0; +} + +static int do_as_client__status(void) +{ + enum ipc_active_state state = fsmonitor_ipc__get_state(); + + switch (state) { + case IPC_STATE__LISTENING: + printf(_("fsmonitor-daemon is watching '%s'\n"), + the_repository->worktree); + return 0; + + default: + printf(_("fsmonitor-daemon is not watching '%s'\n"), + the_repository->worktree); + return 1; + } +} + +enum fsmonitor_cookie_item_result { + FCIR_ERROR = -1, /* could not create cookie file ? */ + FCIR_INIT, + FCIR_SEEN, + FCIR_ABORT, +}; + +struct fsmonitor_cookie_item { + struct hashmap_entry entry; + char *name; + enum fsmonitor_cookie_item_result result; +}; + +static int cookies_cmp(const void *data UNUSED, + const struct hashmap_entry *he1, + const struct hashmap_entry *he2, const void *keydata) +{ + const struct fsmonitor_cookie_item *a = + container_of(he1, const struct fsmonitor_cookie_item, entry); + const struct fsmonitor_cookie_item *b = + container_of(he2, const struct fsmonitor_cookie_item, entry); + + return strcmp(a->name, keydata ? keydata : b->name); +} + +static enum fsmonitor_cookie_item_result with_lock__wait_for_cookie( + struct fsmonitor_daemon_state *state) +{ + /* assert current thread holding state->main_lock */ + + int fd; + struct fsmonitor_cookie_item *cookie; + struct strbuf cookie_pathname = STRBUF_INIT; + struct strbuf cookie_filename = STRBUF_INIT; + enum fsmonitor_cookie_item_result result; + int my_cookie_seq; + + CALLOC_ARRAY(cookie, 1); + + my_cookie_seq = state->cookie_seq++; + + strbuf_addf(&cookie_filename, "%i-%i", getpid(), my_cookie_seq); + + strbuf_addbuf(&cookie_pathname, &state->path_cookie_prefix); + strbuf_addbuf(&cookie_pathname, &cookie_filename); + + cookie->name = strbuf_detach(&cookie_filename, NULL); + cookie->result = FCIR_INIT; + hashmap_entry_init(&cookie->entry, strhash(cookie->name)); + + hashmap_add(&state->cookies, &cookie->entry); + + trace_printf_key(&trace_fsmonitor, "cookie-wait: '%s' '%s'", + cookie->name, cookie_pathname.buf); + + /* + * Create the cookie file on disk and then wait for a notification + * that the listener thread has seen it. + */ + fd = open(cookie_pathname.buf, O_WRONLY | O_CREAT | O_EXCL, 0600); + if (fd < 0) { + error_errno(_("could not create fsmonitor cookie '%s'"), + cookie->name); + + cookie->result = FCIR_ERROR; + goto done; + } + + /* + * Technically, close() and unlink() can fail, but we don't + * care here. We only created the file to trigger a watch + * event from the FS to know that when we're up to date. + */ + close(fd); + unlink(cookie_pathname.buf); + + /* + * Technically, this is an infinite wait (well, unless another + * thread sends us an abort). I'd like to change this to + * use `pthread_cond_timedwait()` and return an error/timeout + * and let the caller do the trivial response thing, but we + * don't have that routine in our thread-utils. + * + * After extensive beta testing I'm not really worried about + * this. Also note that the above open() and unlink() calls + * will cause at least two FS events on that path, so the odds + * of getting stuck are pretty slim. + */ + while (cookie->result == FCIR_INIT) + pthread_cond_wait(&state->cookies_cond, + &state->main_lock); + +done: + hashmap_remove(&state->cookies, &cookie->entry, NULL); + + result = cookie->result; + + free(cookie->name); + free(cookie); + strbuf_release(&cookie_pathname); + + return result; +} + +/* + * Mark these cookies as _SEEN and wake up the corresponding client threads. + */ +static void with_lock__mark_cookies_seen(struct fsmonitor_daemon_state *state, + const struct string_list *cookie_names) +{ + /* assert current thread holding state->main_lock */ + + int k; + int nr_seen = 0; + + for (k = 0; k < cookie_names->nr; k++) { + struct fsmonitor_cookie_item key; + struct fsmonitor_cookie_item *cookie; + + key.name = cookie_names->items[k].string; + hashmap_entry_init(&key.entry, strhash(key.name)); + + cookie = hashmap_get_entry(&state->cookies, &key, entry, NULL); + if (cookie) { + trace_printf_key(&trace_fsmonitor, "cookie-seen: '%s'", + cookie->name); + cookie->result = FCIR_SEEN; + nr_seen++; + } + } + + if (nr_seen) + pthread_cond_broadcast(&state->cookies_cond); +} + +/* + * Set _ABORT on all pending cookies and wake up all client threads. + */ +static void with_lock__abort_all_cookies(struct fsmonitor_daemon_state *state) +{ + /* assert current thread holding state->main_lock */ + + struct hashmap_iter iter; + struct fsmonitor_cookie_item *cookie; + int nr_aborted = 0; + + hashmap_for_each_entry(&state->cookies, &iter, cookie, entry) { + trace_printf_key(&trace_fsmonitor, "cookie-abort: '%s'", + cookie->name); + cookie->result = FCIR_ABORT; + nr_aborted++; + } + + if (nr_aborted) + pthread_cond_broadcast(&state->cookies_cond); +} + +/* + * Requests to and from a FSMonitor Protocol V2 provider use an opaque + * "token" as a virtual timestamp. Clients can request a summary of all + * created/deleted/modified files relative to a token. In the response, + * clients receive a new token for the next (relative) request. + * + * + * Token Format + * ============ + * + * The contents of the token are private and provider-specific. + * + * For the built-in fsmonitor--daemon, we define a token as follows: + * + * "builtin" ":" <token_id> ":" <sequence_nr> + * + * The "builtin" prefix is used as a namespace to avoid conflicts + * with other providers (such as Watchman). + * + * The <token_id> is an arbitrary OPAQUE string, such as a GUID, + * UUID, or {timestamp,pid}. It is used to group all filesystem + * events that happened while the daemon was monitoring (and in-sync + * with the filesystem). + * + * Unlike FSMonitor Protocol V1, it is not defined as a timestamp + * and does not define less-than/greater-than relationships. + * (There are too many race conditions to rely on file system + * event timestamps.) + * + * The <sequence_nr> is a simple integer incremented whenever the + * daemon needs to make its state public. For example, if 1000 file + * system events come in, but no clients have requested the data, + * the daemon can continue to accumulate file changes in the same + * bin and does not need to advance the sequence number. However, + * as soon as a client does arrive, the daemon needs to start a new + * bin and increment the sequence number. + * + * The sequence number serves as the boundary between 2 sets + * of bins -- the older ones that the client has already seen + * and the newer ones that it hasn't. + * + * When a new <token_id> is created, the <sequence_nr> is reset to + * zero. + * + * + * About Token Ids + * =============== + * + * A new token_id is created: + * + * [1] each time the daemon is started. + * + * [2] any time that the daemon must re-sync with the filesystem + * (such as when the kernel drops or we miss events on a very + * active volume). + * + * [3] in response to a client "flush" command (for dropped event + * testing). + * + * When a new token_id is created, the daemon is free to discard all + * cached filesystem events associated with any previous token_ids. + * Events associated with a non-current token_id will never be sent + * to a client. A token_id change implicitly means that the daemon + * has gap in its event history. + * + * Therefore, clients that present a token with a stale (non-current) + * token_id will always be given a trivial response. + */ +struct fsmonitor_token_data { + struct strbuf token_id; + struct fsmonitor_batch *batch_head; + struct fsmonitor_batch *batch_tail; + uint64_t client_ref_count; +}; + +struct fsmonitor_batch { + struct fsmonitor_batch *next; + uint64_t batch_seq_nr; + const char **interned_paths; + size_t nr, alloc; + time_t pinned_time; +}; + +static struct fsmonitor_token_data *fsmonitor_new_token_data(void) +{ + static int test_env_value = -1; + static uint64_t flush_count = 0; + struct fsmonitor_token_data *token; + struct fsmonitor_batch *batch; + + CALLOC_ARRAY(token, 1); + batch = fsmonitor_batch__new(); + + strbuf_init(&token->token_id, 0); + token->batch_head = batch; + token->batch_tail = batch; + token->client_ref_count = 0; + + if (test_env_value < 0) + test_env_value = git_env_bool("GIT_TEST_FSMONITOR_TOKEN", 0); + + if (!test_env_value) { + struct timeval tv; + struct tm tm; + time_t secs; + + gettimeofday(&tv, NULL); + secs = tv.tv_sec; + gmtime_r(&secs, &tm); + + strbuf_addf(&token->token_id, + "%"PRIu64".%d.%4d%02d%02dT%02d%02d%02d.%06ldZ", + flush_count++, + getpid(), + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec, + (long)tv.tv_usec); + } else { + strbuf_addf(&token->token_id, "test_%08x", test_env_value++); + } + + /* + * We created a new <token_id> and are starting a new series + * of tokens with a zero <seq_nr>. + * + * Since clients cannot guess our new (non test) <token_id> + * they will always receive a trivial response (because of the + * mismatch on the <token_id>). The trivial response will + * tell them our new <token_id> so that subsequent requests + * will be relative to our new series. (And when sending that + * response, we pin the current head of the batch list.) + * + * Even if the client correctly guesses the <token_id>, their + * request of "builtin:<token_id>:0" asks for all changes MORE + * RECENT than batch/bin 0. + * + * This implies that it is a waste to accumulate paths in the + * initial batch/bin (because they will never be transmitted). + * + * So the daemon could be running for days and watching the + * file system, but doesn't need to actually accumulate any + * paths UNTIL we need to set a reference point for a later + * relative request. + * + * However, it is very useful for testing to always have a + * reference point set. Pin batch 0 to force early file system + * events to accumulate. + */ + if (test_env_value) + batch->pinned_time = time(NULL); + + return token; +} + +struct fsmonitor_batch *fsmonitor_batch__new(void) +{ + struct fsmonitor_batch *batch; + + CALLOC_ARRAY(batch, 1); + + return batch; +} + +void fsmonitor_batch__free_list(struct fsmonitor_batch *batch) +{ + while (batch) { + struct fsmonitor_batch *next = batch->next; + + /* + * The actual strings within the array of this batch + * are interned, so we don't own them. We only own + * the array. + */ + free(batch->interned_paths); + free(batch); + + batch = next; + } +} + +void fsmonitor_batch__add_path(struct fsmonitor_batch *batch, + const char *path) +{ + const char *interned_path = strintern(path); + + trace_printf_key(&trace_fsmonitor, "event: %s", interned_path); + + ALLOC_GROW(batch->interned_paths, batch->nr + 1, batch->alloc); + batch->interned_paths[batch->nr++] = interned_path; +} + +static void fsmonitor_batch__combine(struct fsmonitor_batch *batch_dest, + const struct fsmonitor_batch *batch_src) +{ + size_t k; + + ALLOC_GROW(batch_dest->interned_paths, + batch_dest->nr + batch_src->nr + 1, + batch_dest->alloc); + + for (k = 0; k < batch_src->nr; k++) + batch_dest->interned_paths[batch_dest->nr++] = + batch_src->interned_paths[k]; +} + +/* + * To keep the batch list from growing unbounded in response to filesystem + * activity, we try to truncate old batches from the end of the list as + * they become irrelevant. + * + * We assume that the .git/index will be updated with the most recent token + * any time the index is updated. And future commands will only ask for + * recent changes *since* that new token. So as tokens advance into the + * future, older batch items will never be requested/needed. So we can + * truncate them without loss of functionality. + * + * However, multiple commands may be talking to the daemon concurrently + * or perform a slow command, so a little "token skew" is possible. + * Therefore, we want this to be a little bit lazy and have a generous + * delay. + * + * The current reader thread walked backwards in time from `token->batch_head` + * back to `batch_marker` somewhere in the middle of the batch list. + * + * Let's walk backwards in time from that marker an arbitrary delay + * and truncate the list there. Note that these timestamps are completely + * artificial (based on when we pinned the batch item) and not on any + * filesystem activity. + * + * Return the obsolete portion of the list after we have removed it from + * the official list so that the caller can free it after leaving the lock. + */ +#define MY_TIME_DELAY_SECONDS (5 * 60) /* seconds */ + +static struct fsmonitor_batch *with_lock__truncate_old_batches( + struct fsmonitor_daemon_state *state, + const struct fsmonitor_batch *batch_marker) +{ + /* assert current thread holding state->main_lock */ + + const struct fsmonitor_batch *batch; + struct fsmonitor_batch *remainder; + + if (!batch_marker) + return NULL; + + trace_printf_key(&trace_fsmonitor, "Truncate: mark (%"PRIu64",%"PRIu64")", + batch_marker->batch_seq_nr, + (uint64_t)batch_marker->pinned_time); + + for (batch = batch_marker; batch; batch = batch->next) { + time_t t; + + if (!batch->pinned_time) /* an overflow batch */ + continue; + + t = batch->pinned_time + MY_TIME_DELAY_SECONDS; + if (t > batch_marker->pinned_time) /* too close to marker */ + continue; + + goto truncate_past_here; + } + + return NULL; + +truncate_past_here: + state->current_token_data->batch_tail = (struct fsmonitor_batch *)batch; + + remainder = ((struct fsmonitor_batch *)batch)->next; + ((struct fsmonitor_batch *)batch)->next = NULL; + + return remainder; +} + +static void fsmonitor_free_token_data(struct fsmonitor_token_data *token) +{ + if (!token) + return; + + assert(token->client_ref_count == 0); + + strbuf_release(&token->token_id); + + fsmonitor_batch__free_list(token->batch_head); + + free(token); +} + +/* + * Flush all of our cached data about the filesystem. Call this if we + * lose sync with the filesystem and miss some notification events. + * + * [1] If we are missing events, then we no longer have a complete + * history of the directory (relative to our current start token). + * We should create a new token and start fresh (as if we just + * booted up). + * + * [2] Some of those lost events may have been for cookie files. We + * should assume the worst and abort them rather letting them starve. + * + * If there are no concurrent threads reading the current token data + * series, we can free it now. Otherwise, let the last reader free + * it. + * + * Either way, the old token data series is no longer associated with + * our state data. + */ +static void with_lock__do_force_resync(struct fsmonitor_daemon_state *state) +{ + /* assert current thread holding state->main_lock */ + + struct fsmonitor_token_data *free_me = NULL; + struct fsmonitor_token_data *new_one = NULL; + + new_one = fsmonitor_new_token_data(); + + if (state->current_token_data->client_ref_count == 0) + free_me = state->current_token_data; + state->current_token_data = new_one; + + fsmonitor_free_token_data(free_me); + + with_lock__abort_all_cookies(state); +} + +void fsmonitor_force_resync(struct fsmonitor_daemon_state *state) +{ + pthread_mutex_lock(&state->main_lock); + with_lock__do_force_resync(state); + pthread_mutex_unlock(&state->main_lock); +} + +/* + * Format an opaque token string to send to the client. + */ +static void with_lock__format_response_token( + struct strbuf *response_token, + const struct strbuf *response_token_id, + const struct fsmonitor_batch *batch) +{ + /* assert current thread holding state->main_lock */ + + strbuf_reset(response_token); + strbuf_addf(response_token, "builtin:%s:%"PRIu64, + response_token_id->buf, batch->batch_seq_nr); +} + +/* + * Parse an opaque token from the client. + * Returns -1 on error. + */ +static int fsmonitor_parse_client_token(const char *buf_token, + struct strbuf *requested_token_id, + uint64_t *seq_nr) +{ + const char *p; + char *p_end; + + strbuf_reset(requested_token_id); + *seq_nr = 0; + + if (!skip_prefix(buf_token, "builtin:", &p)) + return -1; + + while (*p && *p != ':') + strbuf_addch(requested_token_id, *p++); + if (!*p++) + return -1; + + *seq_nr = (uint64_t)strtoumax(p, &p_end, 10); + if (*p_end) + return -1; + + return 0; +} + +KHASH_INIT(str, const char *, int, 0, kh_str_hash_func, kh_str_hash_equal) + +static int do_handle_client(struct fsmonitor_daemon_state *state, + const char *command, + ipc_server_reply_cb *reply, + struct ipc_server_reply_data *reply_data) +{ + struct fsmonitor_token_data *token_data = NULL; + struct strbuf response_token = STRBUF_INIT; + struct strbuf requested_token_id = STRBUF_INIT; + struct strbuf payload = STRBUF_INIT; + uint64_t requested_oldest_seq_nr = 0; + uint64_t total_response_len = 0; + const char *p; + const struct fsmonitor_batch *batch_head; + const struct fsmonitor_batch *batch; + struct fsmonitor_batch *remainder = NULL; + intmax_t count = 0, duplicates = 0; + kh_str_t *shown; + int hash_ret; + int do_trivial = 0; + int do_flush = 0; + int do_cookie = 0; + enum fsmonitor_cookie_item_result cookie_result; + + /* + * We expect `command` to be of the form: + * + * <command> := quit NUL + * | flush NUL + * | <V1-time-since-epoch-ns> NUL + * | <V2-opaque-fsmonitor-token> NUL + */ + + if (!strcmp(command, "quit")) { + /* + * A client has requested over the socket/pipe that the + * daemon shutdown. + * + * Tell the IPC thread pool to shutdown (which completes + * the await in the main thread (which can stop the + * fsmonitor listener thread)). + * + * There is no reply to the client. + */ + return SIMPLE_IPC_QUIT; + + } else if (!strcmp(command, "flush")) { + /* + * Flush all of our cached data and generate a new token + * just like if we lost sync with the filesystem. + * + * Then send a trivial response using the new token. + */ + do_flush = 1; + do_trivial = 1; + + } else if (!skip_prefix(command, "builtin:", &p)) { + /* assume V1 timestamp or garbage */ + + char *p_end; + + strtoumax(command, &p_end, 10); + trace_printf_key(&trace_fsmonitor, + ((*p_end) ? + "fsmonitor: invalid command line '%s'" : + "fsmonitor: unsupported V1 protocol '%s'"), + command); + do_trivial = 1; + do_cookie = 1; + + } else { + /* We have "builtin:*" */ + if (fsmonitor_parse_client_token(command, &requested_token_id, + &requested_oldest_seq_nr)) { + trace_printf_key(&trace_fsmonitor, + "fsmonitor: invalid V2 protocol token '%s'", + command); + do_trivial = 1; + do_cookie = 1; + + } else { + /* + * We have a V2 valid token: + * "builtin:<token_id>:<seq_nr>" + */ + do_cookie = 1; + } + } + + pthread_mutex_lock(&state->main_lock); + + if (!state->current_token_data) + BUG("fsmonitor state does not have a current token"); + + /* + * Write a cookie file inside the directory being watched in + * an effort to flush out existing filesystem events that we + * actually care about. Suspend this client thread until we + * see the filesystem events for this cookie file. + * + * Creating the cookie lets us guarantee that our FS listener + * thread has drained the kernel queue and we are caught up + * with the kernel. + * + * If we cannot create the cookie (or otherwise guarantee that + * we are caught up), we send a trivial response. We have to + * assume that there might be some very, very recent activity + * on the FS still in flight. + */ + if (do_cookie) { + cookie_result = with_lock__wait_for_cookie(state); + if (cookie_result != FCIR_SEEN) { + error(_("fsmonitor: cookie_result '%d' != SEEN"), + cookie_result); + do_trivial = 1; + } + } + + if (do_flush) + with_lock__do_force_resync(state); + + /* + * We mark the current head of the batch list as "pinned" so + * that the listener thread will treat this item as read-only + * (and prevent any more paths from being added to it) from + * now on. + */ + token_data = state->current_token_data; + batch_head = token_data->batch_head; + ((struct fsmonitor_batch *)batch_head)->pinned_time = time(NULL); + + /* + * FSMonitor Protocol V2 requires that we send a response header + * with a "new current token" and then all of the paths that changed + * since the "requested token". We send the seq_nr of the just-pinned + * head batch so that future requests from a client will be relative + * to it. + */ + with_lock__format_response_token(&response_token, + &token_data->token_id, batch_head); + + reply(reply_data, response_token.buf, response_token.len + 1); + total_response_len += response_token.len + 1; + + trace2_data_string("fsmonitor", the_repository, "response/token", + response_token.buf); + trace_printf_key(&trace_fsmonitor, "response token: %s", + response_token.buf); + + if (!do_trivial) { + if (strcmp(requested_token_id.buf, token_data->token_id.buf)) { + /* + * The client last spoke to a different daemon + * instance -OR- the daemon had to resync with + * the filesystem (and lost events), so reject. + */ + trace2_data_string("fsmonitor", the_repository, + "response/token", "different"); + do_trivial = 1; + + } else if (requested_oldest_seq_nr < + token_data->batch_tail->batch_seq_nr) { + /* + * The client wants older events than we have for + * this token_id. This means that the end of our + * batch list was truncated and we cannot give the + * client a complete snapshot relative to their + * request. + */ + trace_printf_key(&trace_fsmonitor, + "client requested truncated data"); + do_trivial = 1; + } + } + + if (do_trivial) { + pthread_mutex_unlock(&state->main_lock); + + reply(reply_data, "/", 2); + + trace2_data_intmax("fsmonitor", the_repository, + "response/trivial", 1); + + goto cleanup; + } + + /* + * We're going to hold onto a pointer to the current + * token-data while we walk the list of batches of files. + * During this time, we will NOT be under the lock. + * So we ref-count it. + * + * This allows the listener thread to continue prepending + * new batches of items to the token-data (which we'll ignore). + * + * AND it allows the listener thread to do a token-reset + * (and install a new `current_token_data`). + */ + token_data->client_ref_count++; + + pthread_mutex_unlock(&state->main_lock); + + /* + * The client request is relative to the token that they sent, + * so walk the batch list backwards from the current head back + * to the batch (sequence number) they named. + * + * We use khash to de-dup the list of pathnames. + * + * NEEDSWORK: each batch contains a list of interned strings, + * so we only need to do pointer comparisons here to build the + * hash table. Currently, we're still comparing the string + * values. + */ + shown = kh_init_str(); + for (batch = batch_head; + batch && batch->batch_seq_nr > requested_oldest_seq_nr; + batch = batch->next) { + size_t k; + + for (k = 0; k < batch->nr; k++) { + const char *s = batch->interned_paths[k]; + size_t s_len; + + if (kh_get_str(shown, s) != kh_end(shown)) + duplicates++; + else { + kh_put_str(shown, s, &hash_ret); + + trace_printf_key(&trace_fsmonitor, + "send[%"PRIuMAX"]: %s", + count, s); + + /* Each path gets written with a trailing NUL */ + s_len = strlen(s) + 1; + + if (payload.len + s_len >= + LARGE_PACKET_DATA_MAX) { + reply(reply_data, payload.buf, + payload.len); + total_response_len += payload.len; + strbuf_reset(&payload); + } + + strbuf_add(&payload, s, s_len); + count++; + } + } + } + + if (payload.len) { + reply(reply_data, payload.buf, payload.len); + total_response_len += payload.len; + } + + kh_release_str(shown); + + pthread_mutex_lock(&state->main_lock); + + if (token_data->client_ref_count > 0) + token_data->client_ref_count--; + + if (token_data->client_ref_count == 0) { + if (token_data != state->current_token_data) { + /* + * The listener thread did a token-reset while we were + * walking the batch list. Therefore, this token is + * stale and can be discarded completely. If we are + * the last reader thread using this token, we own + * that work. + */ + fsmonitor_free_token_data(token_data); + } else if (batch) { + /* + * We are holding the lock and are the only + * reader of the ref-counted portion of the + * list, so we get the honor of seeing if the + * list can be truncated to save memory. + * + * The main loop did not walk to the end of the + * list, so this batch is the first item in the + * batch-list that is older than the requested + * end-point sequence number. See if the tail + * end of the list is obsolete. + */ + remainder = with_lock__truncate_old_batches(state, + batch); + } + } + + pthread_mutex_unlock(&state->main_lock); + + if (remainder) + fsmonitor_batch__free_list(remainder); + + trace2_data_intmax("fsmonitor", the_repository, "response/length", total_response_len); + trace2_data_intmax("fsmonitor", the_repository, "response/count/files", count); + trace2_data_intmax("fsmonitor", the_repository, "response/count/duplicates", duplicates); + +cleanup: + strbuf_release(&response_token); + strbuf_release(&requested_token_id); + strbuf_release(&payload); + + return 0; +} + +static ipc_server_application_cb handle_client; + +static int handle_client(void *data, + const char *command, size_t command_len, + ipc_server_reply_cb *reply, + struct ipc_server_reply_data *reply_data) +{ + struct fsmonitor_daemon_state *state = data; + int result; + + /* + * The Simple IPC API now supports {char*, len} arguments, but + * FSMonitor always uses proper null-terminated strings, so + * we can ignore the command_len argument. (Trust, but verify.) + */ + if (command_len != strlen(command)) + BUG("FSMonitor assumes text messages"); + + trace_printf_key(&trace_fsmonitor, "requested token: %s", command); + + trace2_region_enter("fsmonitor", "handle_client", the_repository); + trace2_data_string("fsmonitor", the_repository, "request", command); + + result = do_handle_client(state, command, reply, reply_data); + + trace2_region_leave("fsmonitor", "handle_client", the_repository); + + return result; +} + +#define FSMONITOR_DIR "fsmonitor--daemon" +#define FSMONITOR_COOKIE_DIR "cookies" +#define FSMONITOR_COOKIE_PREFIX (FSMONITOR_DIR "/" FSMONITOR_COOKIE_DIR "/") + +enum fsmonitor_path_type fsmonitor_classify_path_workdir_relative( + const char *rel) +{ + if (fspathncmp(rel, ".git", 4)) + return IS_WORKDIR_PATH; + rel += 4; + + if (!*rel) + return IS_DOT_GIT; + if (*rel != '/') + return IS_WORKDIR_PATH; /* e.g. .gitignore */ + rel++; + + if (!fspathncmp(rel, FSMONITOR_COOKIE_PREFIX, + strlen(FSMONITOR_COOKIE_PREFIX))) + return IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX; + + return IS_INSIDE_DOT_GIT; +} + +enum fsmonitor_path_type fsmonitor_classify_path_gitdir_relative( + const char *rel) +{ + if (!fspathncmp(rel, FSMONITOR_COOKIE_PREFIX, + strlen(FSMONITOR_COOKIE_PREFIX))) + return IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX; + + return IS_INSIDE_GITDIR; +} + +static enum fsmonitor_path_type try_classify_workdir_abs_path( + struct fsmonitor_daemon_state *state, + const char *path) +{ + const char *rel; + + if (fspathncmp(path, state->path_worktree_watch.buf, + state->path_worktree_watch.len)) + return IS_OUTSIDE_CONE; + + rel = path + state->path_worktree_watch.len; + + if (!*rel) + return IS_WORKDIR_PATH; /* it is the root dir exactly */ + if (*rel != '/') + return IS_OUTSIDE_CONE; + rel++; + + return fsmonitor_classify_path_workdir_relative(rel); +} + +enum fsmonitor_path_type fsmonitor_classify_path_absolute( + struct fsmonitor_daemon_state *state, + const char *path) +{ + const char *rel; + enum fsmonitor_path_type t; + + t = try_classify_workdir_abs_path(state, path); + if (state->nr_paths_watching == 1) + return t; + if (t != IS_OUTSIDE_CONE) + return t; + + if (fspathncmp(path, state->path_gitdir_watch.buf, + state->path_gitdir_watch.len)) + return IS_OUTSIDE_CONE; + + rel = path + state->path_gitdir_watch.len; + + if (!*rel) + return IS_GITDIR; /* it is the <gitdir> exactly */ + if (*rel != '/') + return IS_OUTSIDE_CONE; + rel++; + + return fsmonitor_classify_path_gitdir_relative(rel); +} + +/* + * We try to combine small batches at the front of the batch-list to avoid + * having a long list. This hopefully makes it a little easier when we want + * to truncate and maintain the list. However, we don't want the paths array + * to just keep growing and growing with realloc, so we insert an arbitrary + * limit. + */ +#define MY_COMBINE_LIMIT (1024) + +void fsmonitor_publish(struct fsmonitor_daemon_state *state, + struct fsmonitor_batch *batch, + const struct string_list *cookie_names) +{ + if (!batch && !cookie_names->nr) + return; + + pthread_mutex_lock(&state->main_lock); + + if (batch) { + struct fsmonitor_batch *head; + + head = state->current_token_data->batch_head; + if (!head) { + BUG("token does not have batch"); + } else if (head->pinned_time) { + /* + * We cannot alter the current batch list + * because: + * + * [a] it is being transmitted to at least one + * client and the handle_client() thread has a + * ref-count, but not a lock on the batch list + * starting with this item. + * + * [b] it has been transmitted in the past to + * at least one client such that future + * requests are relative to this head batch. + * + * So, we can only prepend a new batch onto + * the front of the list. + */ + batch->batch_seq_nr = head->batch_seq_nr + 1; + batch->next = head; + state->current_token_data->batch_head = batch; + } else if (!head->batch_seq_nr) { + /* + * Batch 0 is unpinned. See the note in + * `fsmonitor_new_token_data()` about why we + * don't need to accumulate these paths. + */ + fsmonitor_batch__free_list(batch); + } else if (head->nr + batch->nr > MY_COMBINE_LIMIT) { + /* + * The head batch in the list has never been + * transmitted to a client, but folding the + * contents of the new batch onto it would + * exceed our arbitrary limit, so just prepend + * the new batch onto the list. + */ + batch->batch_seq_nr = head->batch_seq_nr + 1; + batch->next = head; + state->current_token_data->batch_head = batch; + } else { + /* + * We are free to add the paths in the given + * batch onto the end of the current head batch. + */ + fsmonitor_batch__combine(head, batch); + fsmonitor_batch__free_list(batch); + } + } + + if (cookie_names->nr) + with_lock__mark_cookies_seen(state, cookie_names); + + pthread_mutex_unlock(&state->main_lock); +} + +static void *fsm_health__thread_proc(void *_state) +{ + struct fsmonitor_daemon_state *state = _state; + + trace2_thread_start("fsm-health"); + + fsm_health__loop(state); + + trace2_thread_exit(); + return NULL; +} + +static void *fsm_listen__thread_proc(void *_state) +{ + struct fsmonitor_daemon_state *state = _state; + + trace2_thread_start("fsm-listen"); + + trace_printf_key(&trace_fsmonitor, "Watching: worktree '%s'", + state->path_worktree_watch.buf); + if (state->nr_paths_watching > 1) + trace_printf_key(&trace_fsmonitor, "Watching: gitdir '%s'", + state->path_gitdir_watch.buf); + + fsm_listen__loop(state); + + pthread_mutex_lock(&state->main_lock); + if (state->current_token_data && + state->current_token_data->client_ref_count == 0) + fsmonitor_free_token_data(state->current_token_data); + state->current_token_data = NULL; + pthread_mutex_unlock(&state->main_lock); + + trace2_thread_exit(); + return NULL; +} + +static int fsmonitor_run_daemon_1(struct fsmonitor_daemon_state *state) +{ + struct ipc_server_opts ipc_opts = { + .nr_threads = fsmonitor__ipc_threads, + + /* + * We know that there are no other active threads yet, + * so we can let the IPC layer temporarily chdir() if + * it needs to when creating the server side of the + * Unix domain socket. + */ + .uds_disallow_chdir = 0 + }; + int health_started = 0; + int listener_started = 0; + int err = 0; + + /* + * Start the IPC thread pool before the we've started the file + * system event listener thread so that we have the IPC handle + * before we need it. + */ + if (ipc_server_run_async(&state->ipc_server_data, + state->path_ipc.buf, &ipc_opts, + handle_client, state)) + return error_errno( + _("could not start IPC thread pool on '%s'"), + state->path_ipc.buf); + + /* + * Start the fsmonitor listener thread to collect filesystem + * events. + */ + if (pthread_create(&state->listener_thread, NULL, + fsm_listen__thread_proc, state)) { + ipc_server_stop_async(state->ipc_server_data); + err = error(_("could not start fsmonitor listener thread")); + goto cleanup; + } + listener_started = 1; + + /* + * Start the health thread to watch over our process. + */ + if (pthread_create(&state->health_thread, NULL, + fsm_health__thread_proc, state)) { + ipc_server_stop_async(state->ipc_server_data); + err = error(_("could not start fsmonitor health thread")); + goto cleanup; + } + health_started = 1; + + /* + * The daemon is now fully functional in background threads. + * Our primary thread should now just wait while the threads + * do all the work. + */ +cleanup: + /* + * Wait for the IPC thread pool to shutdown (whether by client + * request, from filesystem activity, or an error). + */ + ipc_server_await(state->ipc_server_data); + + /* + * The fsmonitor listener thread may have received a shutdown + * event from the IPC thread pool, but it doesn't hurt to tell + * it again. And wait for it to shutdown. + */ + if (listener_started) { + fsm_listen__stop_async(state); + pthread_join(state->listener_thread, NULL); + } + + if (health_started) { + fsm_health__stop_async(state); + pthread_join(state->health_thread, NULL); + } + + if (err) + return err; + if (state->listen_error_code) + return state->listen_error_code; + if (state->health_error_code) + return state->health_error_code; + return 0; +} + +static int fsmonitor_run_daemon(void) +{ + struct fsmonitor_daemon_state state; + const char *home; + int err; + + memset(&state, 0, sizeof(state)); + + hashmap_init(&state.cookies, cookies_cmp, NULL, 0); + pthread_mutex_init(&state.main_lock, NULL); + pthread_cond_init(&state.cookies_cond, NULL); + state.listen_error_code = 0; + state.health_error_code = 0; + state.current_token_data = fsmonitor_new_token_data(); + + /* Prepare to (recursively) watch the <worktree-root> directory. */ + strbuf_init(&state.path_worktree_watch, 0); + strbuf_addstr(&state.path_worktree_watch, absolute_path(get_git_work_tree())); + state.nr_paths_watching = 1; + + strbuf_init(&state.alias.alias, 0); + strbuf_init(&state.alias.points_to, 0); + if ((err = fsmonitor__get_alias(state.path_worktree_watch.buf, &state.alias))) + goto done; + + /* + * We create and delete cookie files somewhere inside the .git + * directory to help us keep sync with the file system. If + * ".git" is not a directory, then <gitdir> is not inside the + * cone of <worktree-root>, so set up a second watch to watch + * the <gitdir> so that we get events for the cookie files. + */ + strbuf_init(&state.path_gitdir_watch, 0); + strbuf_addbuf(&state.path_gitdir_watch, &state.path_worktree_watch); + strbuf_addstr(&state.path_gitdir_watch, "/.git"); + if (!is_directory(state.path_gitdir_watch.buf)) { + strbuf_reset(&state.path_gitdir_watch); + strbuf_addstr(&state.path_gitdir_watch, absolute_path(get_git_dir())); + state.nr_paths_watching = 2; + } + + /* + * We will write filesystem syncing cookie files into + * <gitdir>/<fsmonitor-dir>/<cookie-dir>/<pid>-<seq>. + * + * The extra layers of subdirectories here keep us from + * changing the mtime on ".git/" or ".git/foo/" when we create + * or delete cookie files. + * + * There have been problems with some IDEs that do a + * non-recursive watch of the ".git/" directory and run a + * series of commands any time something happens. + * + * For example, if we place our cookie files directly in + * ".git/" or ".git/foo/" then a `git status` (or similar + * command) from the IDE will cause a cookie file to be + * created in one of those dirs. This causes the mtime of + * those dirs to change. This triggers the IDE's watch + * notification. This triggers the IDE to run those commands + * again. And the process repeats and the machine never goes + * idle. + * + * Adding the extra layers of subdirectories prevents the + * mtime of ".git/" and ".git/foo" from changing when a + * cookie file is created. + */ + strbuf_init(&state.path_cookie_prefix, 0); + strbuf_addbuf(&state.path_cookie_prefix, &state.path_gitdir_watch); + + strbuf_addch(&state.path_cookie_prefix, '/'); + strbuf_addstr(&state.path_cookie_prefix, FSMONITOR_DIR); + mkdir(state.path_cookie_prefix.buf, 0777); + + strbuf_addch(&state.path_cookie_prefix, '/'); + strbuf_addstr(&state.path_cookie_prefix, FSMONITOR_COOKIE_DIR); + mkdir(state.path_cookie_prefix.buf, 0777); + + strbuf_addch(&state.path_cookie_prefix, '/'); + + /* + * We create a named-pipe or unix domain socket inside of the + * ".git" directory. (Well, on Windows, we base our named + * pipe in the NPFS on the absolute path of the git + * directory.) + */ + strbuf_init(&state.path_ipc, 0); + strbuf_addstr(&state.path_ipc, + absolute_path(fsmonitor_ipc__get_path(the_repository))); + + /* + * Confirm that we can create platform-specific resources for the + * filesystem listener before we bother starting all the threads. + */ + if (fsm_listen__ctor(&state)) { + err = error(_("could not initialize listener thread")); + goto done; + } + + if (fsm_health__ctor(&state)) { + err = error(_("could not initialize health thread")); + goto done; + } + + /* + * CD out of the worktree root directory. + * + * The common Git startup mechanism causes our CWD to be the + * root of the worktree. On Windows, this causes our process + * to hold a locked handle on the CWD. This prevents the + * worktree from being moved or deleted while the daemon is + * running. + * + * We assume that our FS and IPC listener threads have either + * opened all of the handles that they need or will do + * everything using absolute paths. + */ + home = getenv("HOME"); + if (home && *home && chdir(home)) + die_errno(_("could not cd home '%s'"), home); + + err = fsmonitor_run_daemon_1(&state); + +done: + pthread_cond_destroy(&state.cookies_cond); + pthread_mutex_destroy(&state.main_lock); + fsm_listen__dtor(&state); + fsm_health__dtor(&state); + + ipc_server_free(state.ipc_server_data); + + strbuf_release(&state.path_worktree_watch); + strbuf_release(&state.path_gitdir_watch); + strbuf_release(&state.path_cookie_prefix); + strbuf_release(&state.path_ipc); + strbuf_release(&state.alias.alias); + strbuf_release(&state.alias.points_to); + + return err; +} + +static int try_to_run_foreground_daemon(int detach_console MAYBE_UNUSED) +{ + /* + * Technically, we don't need to probe for an existing daemon + * process, since we could just call `fsmonitor_run_daemon()` + * and let it fail if the pipe/socket is busy. + * + * However, this method gives us a nicer error message for a + * common error case. + */ + if (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) + die(_("fsmonitor--daemon is already running '%s'"), + the_repository->worktree); + + if (fsmonitor__announce_startup) { + fprintf(stderr, _("running fsmonitor-daemon in '%s'\n"), + the_repository->worktree); + fflush(stderr); + } + +#ifdef GIT_WINDOWS_NATIVE + if (detach_console) + FreeConsole(); +#endif + + return !!fsmonitor_run_daemon(); +} + +static start_bg_wait_cb bg_wait_cb; + +static int bg_wait_cb(const struct child_process *cp UNUSED, + void *cb_data UNUSED) +{ + enum ipc_active_state s = fsmonitor_ipc__get_state(); + + switch (s) { + case IPC_STATE__LISTENING: + /* child is "ready" */ + return 0; + + case IPC_STATE__NOT_LISTENING: + case IPC_STATE__PATH_NOT_FOUND: + /* give child more time */ + return 1; + + default: + case IPC_STATE__INVALID_PATH: + case IPC_STATE__OTHER_ERROR: + /* all the time in world won't help */ + return -1; + } +} + +static int try_to_start_background_daemon(void) +{ + struct child_process cp = CHILD_PROCESS_INIT; + enum start_bg_result sbgr; + + /* + * Before we try to create a background daemon process, see + * if a daemon process is already listening. This makes it + * easier for us to report an already-listening error to the + * console, since our spawn/daemon can only report the success + * of creating the background process (and not whether it + * immediately exited). + */ + if (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) + die(_("fsmonitor--daemon is already running '%s'"), + the_repository->worktree); + + if (fsmonitor__announce_startup) { + fprintf(stderr, _("starting fsmonitor-daemon in '%s'\n"), + the_repository->worktree); + fflush(stderr); + } + + cp.git_cmd = 1; + + strvec_push(&cp.args, "fsmonitor--daemon"); + strvec_push(&cp.args, "run"); + strvec_push(&cp.args, "--detach"); + strvec_pushf(&cp.args, "--ipc-threads=%d", fsmonitor__ipc_threads); + + cp.no_stdin = 1; + cp.no_stdout = 1; + cp.no_stderr = 1; + + sbgr = start_bg_command(&cp, bg_wait_cb, NULL, + fsmonitor__start_timeout_sec); + + switch (sbgr) { + case SBGR_READY: + return 0; + + default: + case SBGR_ERROR: + case SBGR_CB_ERROR: + return error(_("daemon failed to start")); + + case SBGR_TIMEOUT: + return error(_("daemon not online yet")); + + case SBGR_DIED: + return error(_("daemon terminated")); + } +} + +int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix) +{ + const char *subcmd; + enum fsmonitor_reason reason; + int detach_console = 0; + + struct option options[] = { + OPT_BOOL(0, "detach", &detach_console, N_("detach from console")), + OPT_INTEGER(0, "ipc-threads", + &fsmonitor__ipc_threads, + N_("use <n> ipc worker threads")), + OPT_INTEGER(0, "start-timeout", + &fsmonitor__start_timeout_sec, + N_("max seconds to wait for background daemon startup")), + + OPT_END() + }; + + git_config(fsmonitor_config, NULL); + + argc = parse_options(argc, argv, prefix, options, + builtin_fsmonitor__daemon_usage, 0); + if (argc != 1) + usage_with_options(builtin_fsmonitor__daemon_usage, options); + subcmd = argv[0]; + + if (fsmonitor__ipc_threads < 1) + die(_("invalid 'ipc-threads' value (%d)"), + fsmonitor__ipc_threads); + + prepare_repo_settings(the_repository); + /* + * If the repo is fsmonitor-compatible, explicitly set IPC-mode + * (without bothering to load the `core.fsmonitor` config settings). + * + * If the repo is not compatible, the repo-settings will be set to + * incompatible rather than IPC, so we can use one of the __get + * routines to detect the discrepancy. + */ + fsm_settings__set_ipc(the_repository); + + reason = fsm_settings__get_reason(the_repository); + if (reason > FSMONITOR_REASON_OK) + die("%s", + fsm_settings__get_incompatible_msg(the_repository, + reason)); + + if (!strcmp(subcmd, "start")) + return !!try_to_start_background_daemon(); + + if (!strcmp(subcmd, "run")) + return !!try_to_run_foreground_daemon(detach_console); + + if (!strcmp(subcmd, "stop")) + return !!do_as_client__send_stop(); + + if (!strcmp(subcmd, "status")) + return !!do_as_client__status(); + + die(_("Unhandled subcommand '%s'"), subcmd); +} + +#else +int cmd_fsmonitor__daemon(int argc, const char **argv, const char *prefix UNUSED) +{ + struct option options[] = { + OPT_END() + }; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(builtin_fsmonitor__daemon_usage, options); + + die(_("fsmonitor--daemon not supported on this platform")); +} +#endif diff --git a/builtin/gc.c b/builtin/gc.c new file mode 100644 index 0000000..7c11d5e --- /dev/null +++ b/builtin/gc.c @@ -0,0 +1,2793 @@ +/* + * git gc builtin command + * + * Cleanup unreachable files and optimize the repository. + * + * Copyright (c) 2007 James Bowes + * + * Based on git-gc.sh, which is + * + * Copyright (c) 2006 Shawn O. Pearce + */ + +#include "builtin.h" +#include "abspath.h" +#include "date.h" +#include "environment.h" +#include "hex.h" +#include "repository.h" +#include "config.h" +#include "tempfile.h" +#include "lockfile.h" +#include "parse-options.h" +#include "run-command.h" +#include "sigchain.h" +#include "strvec.h" +#include "commit.h" +#include "commit-graph.h" +#include "packfile.h" +#include "object-file.h" +#include "object-store-ll.h" +#include "pack.h" +#include "pack-objects.h" +#include "path.h" +#include "blob.h" +#include "tree.h" +#include "promisor-remote.h" +#include "refs.h" +#include "remote.h" +#include "exec-cmd.h" +#include "gettext.h" +#include "hook.h" +#include "setup.h" +#include "trace2.h" + +#define FAILED_RUN "failed to run %s" + +static const char * const builtin_gc_usage[] = { + N_("git gc [<options>]"), + NULL +}; + +static int pack_refs = 1; +static int prune_reflogs = 1; +static int cruft_packs = 1; +static unsigned long max_cruft_size; +static int aggressive_depth = 50; +static int aggressive_window = 250; +static int gc_auto_threshold = 6700; +static int gc_auto_pack_limit = 50; +static int detach_auto = 1; +static timestamp_t gc_log_expire_time; +static const char *gc_log_expire = "1.day.ago"; +static const char *prune_expire = "2.weeks.ago"; +static const char *prune_worktrees_expire = "3.months.ago"; +static char *repack_filter; +static char *repack_filter_to; +static unsigned long big_pack_threshold; +static unsigned long max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE; + +static struct strvec reflog = STRVEC_INIT; +static struct strvec repack = STRVEC_INIT; +static struct strvec prune = STRVEC_INIT; +static struct strvec prune_worktrees = STRVEC_INIT; +static struct strvec rerere = STRVEC_INIT; + +static struct tempfile *pidfile; +static struct lock_file log_lock; + +static struct string_list pack_garbage = STRING_LIST_INIT_DUP; + +static void clean_pack_garbage(void) +{ + int i; + for (i = 0; i < pack_garbage.nr; i++) + unlink_or_warn(pack_garbage.items[i].string); + string_list_clear(&pack_garbage, 0); +} + +static void report_pack_garbage(unsigned seen_bits, const char *path) +{ + if (seen_bits == PACKDIR_FILE_IDX) + string_list_append(&pack_garbage, path); +} + +static void process_log_file(void) +{ + struct stat st; + if (fstat(get_lock_file_fd(&log_lock), &st)) { + /* + * Perhaps there was an i/o error or another + * unlikely situation. Try to make a note of + * this in gc.log along with any existing + * messages. + */ + int saved_errno = errno; + fprintf(stderr, _("Failed to fstat %s: %s"), + get_lock_file_path(&log_lock), + strerror(saved_errno)); + fflush(stderr); + commit_lock_file(&log_lock); + errno = saved_errno; + } else if (st.st_size) { + /* There was some error recorded in the lock file */ + commit_lock_file(&log_lock); + } else { + /* No error, clean up any old gc.log */ + unlink(git_path("gc.log")); + rollback_lock_file(&log_lock); + } +} + +static void process_log_file_at_exit(void) +{ + fflush(stderr); + process_log_file(); +} + +static void process_log_file_on_signal(int signo) +{ + process_log_file(); + sigchain_pop(signo); + raise(signo); +} + +static int gc_config_is_timestamp_never(const char *var) +{ + const char *value; + timestamp_t expire; + + if (!git_config_get_value(var, &value) && value) { + if (parse_expiry_date(value, &expire)) + die(_("failed to parse '%s' value '%s'"), var, value); + return expire == 0; + } + return 0; +} + +static void gc_config(void) +{ + const char *value; + + if (!git_config_get_value("gc.packrefs", &value)) { + if (value && !strcmp(value, "notbare")) + pack_refs = -1; + else + pack_refs = git_config_bool("gc.packrefs", value); + } + + if (gc_config_is_timestamp_never("gc.reflogexpire") && + gc_config_is_timestamp_never("gc.reflogexpireunreachable")) + prune_reflogs = 0; + + git_config_get_int("gc.aggressivewindow", &aggressive_window); + git_config_get_int("gc.aggressivedepth", &aggressive_depth); + git_config_get_int("gc.auto", &gc_auto_threshold); + git_config_get_int("gc.autopacklimit", &gc_auto_pack_limit); + git_config_get_bool("gc.autodetach", &detach_auto); + git_config_get_bool("gc.cruftpacks", &cruft_packs); + git_config_get_ulong("gc.maxcruftsize", &max_cruft_size); + git_config_get_expiry("gc.pruneexpire", &prune_expire); + git_config_get_expiry("gc.worktreepruneexpire", &prune_worktrees_expire); + git_config_get_expiry("gc.logexpiry", &gc_log_expire); + + git_config_get_ulong("gc.bigpackthreshold", &big_pack_threshold); + git_config_get_ulong("pack.deltacachesize", &max_delta_cache_size); + + git_config_get_string("gc.repackfilter", &repack_filter); + git_config_get_string("gc.repackfilterto", &repack_filter_to); + + git_config(git_default_config, NULL); +} + +struct maintenance_run_opts; +static int maintenance_task_pack_refs(MAYBE_UNUSED struct maintenance_run_opts *opts) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + + cmd.git_cmd = 1; + strvec_pushl(&cmd.args, "pack-refs", "--all", "--prune", NULL); + return run_command(&cmd); +} + +static int too_many_loose_objects(void) +{ + /* + * Quickly check if a "gc" is needed, by estimating how + * many loose objects there are. Because SHA-1 is evenly + * distributed, we can check only one and get a reasonable + * estimate. + */ + DIR *dir; + struct dirent *ent; + int auto_threshold; + int num_loose = 0; + int needed = 0; + const unsigned hexsz_loose = the_hash_algo->hexsz - 2; + + dir = opendir(git_path("objects/17")); + if (!dir) + return 0; + + auto_threshold = DIV_ROUND_UP(gc_auto_threshold, 256); + while ((ent = readdir(dir)) != NULL) { + if (strspn(ent->d_name, "0123456789abcdef") != hexsz_loose || + ent->d_name[hexsz_loose] != '\0') + continue; + if (++num_loose > auto_threshold) { + needed = 1; + break; + } + } + closedir(dir); + return needed; +} + +static struct packed_git *find_base_packs(struct string_list *packs, + unsigned long limit) +{ + struct packed_git *p, *base = NULL; + + for (p = get_all_packs(the_repository); p; p = p->next) { + if (!p->pack_local || p->is_cruft) + continue; + if (limit) { + if (p->pack_size >= limit) + string_list_append(packs, p->pack_name); + } else if (!base || base->pack_size < p->pack_size) { + base = p; + } + } + + if (base) + string_list_append(packs, base->pack_name); + + return base; +} + +static int too_many_packs(void) +{ + struct packed_git *p; + int cnt; + + if (gc_auto_pack_limit <= 0) + return 0; + + for (cnt = 0, p = get_all_packs(the_repository); p; p = p->next) { + if (!p->pack_local) + continue; + if (p->pack_keep) + continue; + /* + * Perhaps check the size of the pack and count only + * very small ones here? + */ + cnt++; + } + return gc_auto_pack_limit < cnt; +} + +static uint64_t total_ram(void) +{ +#if defined(HAVE_SYSINFO) + struct sysinfo si; + + if (!sysinfo(&si)) + return si.totalram; +#elif defined(HAVE_BSD_SYSCTL) && (defined(HW_MEMSIZE) || defined(HW_PHYSMEM)) + int64_t physical_memory; + int mib[2]; + size_t length; + + mib[0] = CTL_HW; +# if defined(HW_MEMSIZE) + mib[1] = HW_MEMSIZE; +# else + mib[1] = HW_PHYSMEM; +# endif + length = sizeof(int64_t); + if (!sysctl(mib, 2, &physical_memory, &length, NULL, 0)) + return physical_memory; +#elif defined(GIT_WINDOWS_NATIVE) + MEMORYSTATUSEX memInfo; + + memInfo.dwLength = sizeof(MEMORYSTATUSEX); + if (GlobalMemoryStatusEx(&memInfo)) + return memInfo.ullTotalPhys; +#endif + return 0; +} + +static uint64_t estimate_repack_memory(struct packed_git *pack) +{ + unsigned long nr_objects = repo_approximate_object_count(the_repository); + size_t os_cache, heap; + + if (!pack || !nr_objects) + return 0; + + /* + * First we have to scan through at least one pack. + * Assume enough room in OS file cache to keep the entire pack + * or we may accidentally evict data of other processes from + * the cache. + */ + os_cache = pack->pack_size + pack->index_size; + /* then pack-objects needs lots more for book keeping */ + heap = sizeof(struct object_entry) * nr_objects; + /* + * internal rev-list --all --objects takes up some memory too, + * let's say half of it is for blobs + */ + heap += sizeof(struct blob) * nr_objects / 2; + /* + * and the other half is for trees (commits and tags are + * usually insignificant) + */ + heap += sizeof(struct tree) * nr_objects / 2; + /* and then obj_hash[], underestimated in fact */ + heap += sizeof(struct object *) * nr_objects; + /* revindex is used also */ + heap += (sizeof(off_t) + sizeof(uint32_t)) * nr_objects; + /* + * read_sha1_file() (either at delta calculation phase, or + * writing phase) also fills up the delta base cache + */ + heap += delta_base_cache_limit; + /* and of course pack-objects has its own delta cache */ + heap += max_delta_cache_size; + + return os_cache + heap; +} + +static int keep_one_pack(struct string_list_item *item, void *data UNUSED) +{ + strvec_pushf(&repack, "--keep-pack=%s", basename(item->string)); + return 0; +} + +static void add_repack_all_option(struct string_list *keep_pack) +{ + if (prune_expire && !strcmp(prune_expire, "now")) + strvec_push(&repack, "-a"); + else if (cruft_packs) { + strvec_push(&repack, "--cruft"); + if (prune_expire) + strvec_pushf(&repack, "--cruft-expiration=%s", prune_expire); + if (max_cruft_size) + strvec_pushf(&repack, "--max-cruft-size=%lu", + max_cruft_size); + } else { + strvec_push(&repack, "-A"); + if (prune_expire) + strvec_pushf(&repack, "--unpack-unreachable=%s", prune_expire); + } + + if (keep_pack) + for_each_string_list(keep_pack, keep_one_pack, NULL); + + if (repack_filter && *repack_filter) + strvec_pushf(&repack, "--filter=%s", repack_filter); + if (repack_filter_to && *repack_filter_to) + strvec_pushf(&repack, "--filter-to=%s", repack_filter_to); +} + +static void add_repack_incremental_option(void) +{ + strvec_push(&repack, "--no-write-bitmap-index"); +} + +static int need_to_gc(void) +{ + /* + * Setting gc.auto to 0 or negative can disable the + * automatic gc. + */ + if (gc_auto_threshold <= 0) + return 0; + + /* + * If there are too many loose objects, but not too many + * packs, we run "repack -d -l". If there are too many packs, + * we run "repack -A -d -l". Otherwise we tell the caller + * there is no need. + */ + if (too_many_packs()) { + struct string_list keep_pack = STRING_LIST_INIT_NODUP; + + if (big_pack_threshold) { + find_base_packs(&keep_pack, big_pack_threshold); + if (keep_pack.nr >= gc_auto_pack_limit) { + big_pack_threshold = 0; + string_list_clear(&keep_pack, 0); + find_base_packs(&keep_pack, 0); + } + } else { + struct packed_git *p = find_base_packs(&keep_pack, 0); + uint64_t mem_have, mem_want; + + mem_have = total_ram(); + mem_want = estimate_repack_memory(p); + + /* + * Only allow 1/2 of memory for pack-objects, leave + * the rest for the OS and other processes in the + * system. + */ + if (!mem_have || mem_want < mem_have / 2) + string_list_clear(&keep_pack, 0); + } + + add_repack_all_option(&keep_pack); + string_list_clear(&keep_pack, 0); + } else if (too_many_loose_objects()) + add_repack_incremental_option(); + else + return 0; + + if (run_hooks("pre-auto-gc")) + return 0; + return 1; +} + +/* return NULL on success, else hostname running the gc */ +static const char *lock_repo_for_gc(int force, pid_t* ret_pid) +{ + struct lock_file lock = LOCK_INIT; + char my_host[HOST_NAME_MAX + 1]; + struct strbuf sb = STRBUF_INIT; + struct stat st; + uintmax_t pid; + FILE *fp; + int fd; + char *pidfile_path; + + if (is_tempfile_active(pidfile)) + /* already locked */ + return NULL; + + if (xgethostname(my_host, sizeof(my_host))) + xsnprintf(my_host, sizeof(my_host), "unknown"); + + pidfile_path = git_pathdup("gc.pid"); + fd = hold_lock_file_for_update(&lock, pidfile_path, + LOCK_DIE_ON_ERROR); + if (!force) { + static char locking_host[HOST_NAME_MAX + 1]; + static char *scan_fmt; + int should_exit; + + if (!scan_fmt) + scan_fmt = xstrfmt("%s %%%ds", "%"SCNuMAX, HOST_NAME_MAX); + fp = fopen(pidfile_path, "r"); + memset(locking_host, 0, sizeof(locking_host)); + should_exit = + fp != NULL && + !fstat(fileno(fp), &st) && + /* + * 12 hour limit is very generous as gc should + * never take that long. On the other hand we + * don't really need a strict limit here, + * running gc --auto one day late is not a big + * problem. --force can be used in manual gc + * after the user verifies that no gc is + * running. + */ + time(NULL) - st.st_mtime <= 12 * 3600 && + fscanf(fp, scan_fmt, &pid, locking_host) == 2 && + /* be gentle to concurrent "gc" on remote hosts */ + (strcmp(locking_host, my_host) || !kill(pid, 0) || errno == EPERM); + if (fp) + fclose(fp); + if (should_exit) { + if (fd >= 0) + rollback_lock_file(&lock); + *ret_pid = pid; + free(pidfile_path); + return locking_host; + } + } + + strbuf_addf(&sb, "%"PRIuMAX" %s", + (uintmax_t) getpid(), my_host); + write_in_full(fd, sb.buf, sb.len); + strbuf_release(&sb); + commit_lock_file(&lock); + pidfile = register_tempfile(pidfile_path); + free(pidfile_path); + return NULL; +} + +/* + * Returns 0 if there was no previous error and gc can proceed, 1 if + * gc should not proceed due to an error in the last run. Prints a + * message and returns with a non-[01] status code if an error occurred + * while reading gc.log + */ +static int report_last_gc_error(void) +{ + struct strbuf sb = STRBUF_INIT; + int ret = 0; + ssize_t len; + struct stat st; + char *gc_log_path = git_pathdup("gc.log"); + + if (stat(gc_log_path, &st)) { + if (errno == ENOENT) + goto done; + + ret = die_message_errno(_("cannot stat '%s'"), gc_log_path); + goto done; + } + + if (st.st_mtime < gc_log_expire_time) + goto done; + + len = strbuf_read_file(&sb, gc_log_path, 0); + if (len < 0) + ret = die_message_errno(_("cannot read '%s'"), gc_log_path); + else if (len > 0) { + /* + * A previous gc failed. Report the error, and don't + * bother with an automatic gc run since it is likely + * to fail in the same way. + */ + warning(_("The last gc run reported the following. " + "Please correct the root cause\n" + "and remove %s\n" + "Automatic cleanup will not be performed " + "until the file is removed.\n\n" + "%s"), + gc_log_path, sb.buf); + ret = 1; + } + strbuf_release(&sb); +done: + free(gc_log_path); + return ret; +} + +static void gc_before_repack(void) +{ + /* + * We may be called twice, as both the pre- and + * post-daemonized phases will call us, but running these + * commands more than once is pointless and wasteful. + */ + static int done = 0; + if (done++) + return; + + if (pack_refs && maintenance_task_pack_refs(NULL)) + die(FAILED_RUN, "pack-refs"); + + if (prune_reflogs) { + struct child_process cmd = CHILD_PROCESS_INIT; + + cmd.git_cmd = 1; + strvec_pushv(&cmd.args, reflog.v); + if (run_command(&cmd)) + die(FAILED_RUN, reflog.v[0]); + } +} + +int cmd_gc(int argc, const char **argv, const char *prefix) +{ + int aggressive = 0; + int auto_gc = 0; + int quiet = 0; + int force = 0; + const char *name; + pid_t pid; + int daemonized = 0; + int keep_largest_pack = -1; + timestamp_t dummy; + struct child_process rerere_cmd = CHILD_PROCESS_INIT; + + struct option builtin_gc_options[] = { + OPT__QUIET(&quiet, N_("suppress progress reporting")), + { OPTION_STRING, 0, "prune", &prune_expire, N_("date"), + N_("prune unreferenced objects"), + PARSE_OPT_OPTARG, NULL, (intptr_t)prune_expire }, + OPT_BOOL(0, "cruft", &cruft_packs, N_("pack unreferenced objects separately")), + OPT_MAGNITUDE(0, "max-cruft-size", &max_cruft_size, + N_("with --cruft, limit the size of new cruft packs")), + OPT_BOOL(0, "aggressive", &aggressive, N_("be more thorough (increased runtime)")), + OPT_BOOL_F(0, "auto", &auto_gc, N_("enable auto-gc mode"), + PARSE_OPT_NOCOMPLETE), + OPT_BOOL_F(0, "force", &force, + N_("force running gc even if there may be another gc running"), + PARSE_OPT_NOCOMPLETE), + OPT_BOOL(0, "keep-largest-pack", &keep_largest_pack, + N_("repack all other packs except the largest pack")), + OPT_END() + }; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(builtin_gc_usage, builtin_gc_options); + + strvec_pushl(&reflog, "reflog", "expire", "--all", NULL); + strvec_pushl(&repack, "repack", "-d", "-l", NULL); + strvec_pushl(&prune, "prune", "--expire", NULL); + strvec_pushl(&prune_worktrees, "worktree", "prune", "--expire", NULL); + strvec_pushl(&rerere, "rerere", "gc", NULL); + + /* default expiry time, overwritten in gc_config */ + gc_config(); + if (parse_expiry_date(gc_log_expire, &gc_log_expire_time)) + die(_("failed to parse gc.logExpiry value %s"), gc_log_expire); + + if (pack_refs < 0) + pack_refs = !is_bare_repository(); + + argc = parse_options(argc, argv, prefix, builtin_gc_options, + builtin_gc_usage, 0); + if (argc > 0) + usage_with_options(builtin_gc_usage, builtin_gc_options); + + if (prune_expire && parse_expiry_date(prune_expire, &dummy)) + die(_("failed to parse prune expiry value %s"), prune_expire); + + if (aggressive) { + strvec_push(&repack, "-f"); + if (aggressive_depth > 0) + strvec_pushf(&repack, "--depth=%d", aggressive_depth); + if (aggressive_window > 0) + strvec_pushf(&repack, "--window=%d", aggressive_window); + } + if (quiet) + strvec_push(&repack, "-q"); + + if (auto_gc) { + /* + * Auto-gc should be least intrusive as possible. + */ + if (!need_to_gc()) + return 0; + if (!quiet) { + if (detach_auto) + fprintf(stderr, _("Auto packing the repository in background for optimum performance.\n")); + else + fprintf(stderr, _("Auto packing the repository for optimum performance.\n")); + fprintf(stderr, _("See \"git help gc\" for manual housekeeping.\n")); + } + if (detach_auto) { + int ret = report_last_gc_error(); + + if (ret == 1) + /* Last gc --auto failed. Skip this one. */ + return 0; + else if (ret) + /* an I/O error occurred, already reported */ + return ret; + + if (lock_repo_for_gc(force, &pid)) + return 0; + gc_before_repack(); /* dies on failure */ + delete_tempfile(&pidfile); + + /* + * failure to daemonize is ok, we'll continue + * in foreground + */ + daemonized = !daemonize(); + } + } else { + struct string_list keep_pack = STRING_LIST_INIT_NODUP; + + if (keep_largest_pack != -1) { + if (keep_largest_pack) + find_base_packs(&keep_pack, 0); + } else if (big_pack_threshold) { + find_base_packs(&keep_pack, big_pack_threshold); + } + + add_repack_all_option(&keep_pack); + string_list_clear(&keep_pack, 0); + } + + name = lock_repo_for_gc(force, &pid); + if (name) { + if (auto_gc) + return 0; /* be quiet on --auto */ + die(_("gc is already running on machine '%s' pid %"PRIuMAX" (use --force if not)"), + name, (uintmax_t)pid); + } + + if (daemonized) { + hold_lock_file_for_update(&log_lock, + git_path("gc.log"), + LOCK_DIE_ON_ERROR); + dup2(get_lock_file_fd(&log_lock), 2); + sigchain_push_common(process_log_file_on_signal); + atexit(process_log_file_at_exit); + } + + gc_before_repack(); + + if (!repository_format_precious_objects) { + struct child_process repack_cmd = CHILD_PROCESS_INIT; + + repack_cmd.git_cmd = 1; + repack_cmd.close_object_store = 1; + strvec_pushv(&repack_cmd.args, repack.v); + if (run_command(&repack_cmd)) + die(FAILED_RUN, repack.v[0]); + + if (prune_expire) { + struct child_process prune_cmd = CHILD_PROCESS_INIT; + + /* run `git prune` even if using cruft packs */ + strvec_push(&prune, prune_expire); + if (quiet) + strvec_push(&prune, "--no-progress"); + if (repo_has_promisor_remote(the_repository)) + strvec_push(&prune, + "--exclude-promisor-objects"); + prune_cmd.git_cmd = 1; + strvec_pushv(&prune_cmd.args, prune.v); + if (run_command(&prune_cmd)) + die(FAILED_RUN, prune.v[0]); + } + } + + if (prune_worktrees_expire) { + struct child_process prune_worktrees_cmd = CHILD_PROCESS_INIT; + + strvec_push(&prune_worktrees, prune_worktrees_expire); + prune_worktrees_cmd.git_cmd = 1; + strvec_pushv(&prune_worktrees_cmd.args, prune_worktrees.v); + if (run_command(&prune_worktrees_cmd)) + die(FAILED_RUN, prune_worktrees.v[0]); + } + + rerere_cmd.git_cmd = 1; + strvec_pushv(&rerere_cmd.args, rerere.v); + if (run_command(&rerere_cmd)) + die(FAILED_RUN, rerere.v[0]); + + report_garbage = report_pack_garbage; + reprepare_packed_git(the_repository); + if (pack_garbage.nr > 0) { + close_object_store(the_repository->objects); + clean_pack_garbage(); + } + + if (the_repository->settings.gc_write_commit_graph == 1) + write_commit_graph_reachable(the_repository->objects->odb, + !quiet && !daemonized ? COMMIT_GRAPH_WRITE_PROGRESS : 0, + NULL); + + if (auto_gc && too_many_loose_objects()) + warning(_("There are too many unreachable loose objects; " + "run 'git prune' to remove them.")); + + if (!daemonized) + unlink(git_path("gc.log")); + + return 0; +} + +static const char *const builtin_maintenance_run_usage[] = { + N_("git maintenance run [--auto] [--[no-]quiet] [--task=<task>] [--schedule]"), + NULL +}; + +enum schedule_priority { + SCHEDULE_NONE = 0, + SCHEDULE_WEEKLY = 1, + SCHEDULE_DAILY = 2, + SCHEDULE_HOURLY = 3, +}; + +static enum schedule_priority parse_schedule(const char *value) +{ + if (!value) + return SCHEDULE_NONE; + if (!strcasecmp(value, "hourly")) + return SCHEDULE_HOURLY; + if (!strcasecmp(value, "daily")) + return SCHEDULE_DAILY; + if (!strcasecmp(value, "weekly")) + return SCHEDULE_WEEKLY; + return SCHEDULE_NONE; +} + +static int maintenance_opt_schedule(const struct option *opt, const char *arg, + int unset) +{ + enum schedule_priority *priority = opt->value; + + if (unset) + die(_("--no-schedule is not allowed")); + + *priority = parse_schedule(arg); + + if (!*priority) + die(_("unrecognized --schedule argument '%s'"), arg); + + return 0; +} + +struct maintenance_run_opts { + int auto_flag; + int quiet; + enum schedule_priority schedule; +}; + +/* Remember to update object flag allocation in object.h */ +#define SEEN (1u<<0) + +struct cg_auto_data { + int num_not_in_graph; + int limit; +}; + +static int dfs_on_ref(const char *refname UNUSED, + const struct object_id *oid, + int flags UNUSED, + void *cb_data) +{ + struct cg_auto_data *data = (struct cg_auto_data *)cb_data; + int result = 0; + struct object_id peeled; + struct commit_list *stack = NULL; + struct commit *commit; + + if (!peel_iterated_oid(oid, &peeled)) + oid = &peeled; + if (oid_object_info(the_repository, oid, NULL) != OBJ_COMMIT) + return 0; + + commit = lookup_commit(the_repository, oid); + if (!commit) + return 0; + if (repo_parse_commit(the_repository, commit) || + commit_graph_position(commit) != COMMIT_NOT_FROM_GRAPH) + return 0; + + data->num_not_in_graph++; + + if (data->num_not_in_graph >= data->limit) + return 1; + + commit_list_append(commit, &stack); + + while (!result && stack) { + struct commit_list *parent; + + commit = pop_commit(&stack); + + for (parent = commit->parents; parent; parent = parent->next) { + if (repo_parse_commit(the_repository, parent->item) || + commit_graph_position(parent->item) != COMMIT_NOT_FROM_GRAPH || + parent->item->object.flags & SEEN) + continue; + + parent->item->object.flags |= SEEN; + data->num_not_in_graph++; + + if (data->num_not_in_graph >= data->limit) { + result = 1; + break; + } + + commit_list_append(parent->item, &stack); + } + } + + free_commit_list(stack); + return result; +} + +static int should_write_commit_graph(void) +{ + int result; + struct cg_auto_data data; + + data.num_not_in_graph = 0; + data.limit = 100; + git_config_get_int("maintenance.commit-graph.auto", + &data.limit); + + if (!data.limit) + return 0; + if (data.limit < 0) + return 1; + + result = for_each_ref(dfs_on_ref, &data); + + repo_clear_commit_marks(the_repository, SEEN); + + return result; +} + +static int run_write_commit_graph(struct maintenance_run_opts *opts) +{ + struct child_process child = CHILD_PROCESS_INIT; + + child.git_cmd = child.close_object_store = 1; + strvec_pushl(&child.args, "commit-graph", "write", + "--split", "--reachable", NULL); + + if (opts->quiet) + strvec_push(&child.args, "--no-progress"); + + return !!run_command(&child); +} + +static int maintenance_task_commit_graph(struct maintenance_run_opts *opts) +{ + prepare_repo_settings(the_repository); + if (!the_repository->settings.core_commit_graph) + return 0; + + if (run_write_commit_graph(opts)) { + error(_("failed to write commit-graph")); + return 1; + } + + return 0; +} + +static int fetch_remote(struct remote *remote, void *cbdata) +{ + struct maintenance_run_opts *opts = cbdata; + struct child_process child = CHILD_PROCESS_INIT; + + if (remote->skip_default_update) + return 0; + + child.git_cmd = 1; + strvec_pushl(&child.args, "fetch", remote->name, + "--prefetch", "--prune", "--no-tags", + "--no-write-fetch-head", "--recurse-submodules=no", + NULL); + + if (opts->quiet) + strvec_push(&child.args, "--quiet"); + + return !!run_command(&child); +} + +static int maintenance_task_prefetch(struct maintenance_run_opts *opts) +{ + if (for_each_remote(fetch_remote, opts)) { + error(_("failed to prefetch remotes")); + return 1; + } + + return 0; +} + +static int maintenance_task_gc(struct maintenance_run_opts *opts) +{ + struct child_process child = CHILD_PROCESS_INIT; + + child.git_cmd = child.close_object_store = 1; + strvec_push(&child.args, "gc"); + + if (opts->auto_flag) + strvec_push(&child.args, "--auto"); + if (opts->quiet) + strvec_push(&child.args, "--quiet"); + else + strvec_push(&child.args, "--no-quiet"); + + return run_command(&child); +} + +static int prune_packed(struct maintenance_run_opts *opts) +{ + struct child_process child = CHILD_PROCESS_INIT; + + child.git_cmd = 1; + strvec_push(&child.args, "prune-packed"); + + if (opts->quiet) + strvec_push(&child.args, "--quiet"); + + return !!run_command(&child); +} + +struct write_loose_object_data { + FILE *in; + int count; + int batch_size; +}; + +static int loose_object_auto_limit = 100; + +static int loose_object_count(const struct object_id *oid UNUSED, + const char *path UNUSED, + void *data) +{ + int *count = (int*)data; + if (++(*count) >= loose_object_auto_limit) + return 1; + return 0; +} + +static int loose_object_auto_condition(void) +{ + int count = 0; + + git_config_get_int("maintenance.loose-objects.auto", + &loose_object_auto_limit); + + if (!loose_object_auto_limit) + return 0; + if (loose_object_auto_limit < 0) + return 1; + + return for_each_loose_file_in_objdir(the_repository->objects->odb->path, + loose_object_count, + NULL, NULL, &count); +} + +static int bail_on_loose(const struct object_id *oid UNUSED, + const char *path UNUSED, + void *data UNUSED) +{ + return 1; +} + +static int write_loose_object_to_stdin(const struct object_id *oid, + const char *path UNUSED, + void *data) +{ + struct write_loose_object_data *d = (struct write_loose_object_data *)data; + + fprintf(d->in, "%s\n", oid_to_hex(oid)); + + return ++(d->count) > d->batch_size; +} + +static int pack_loose(struct maintenance_run_opts *opts) +{ + struct repository *r = the_repository; + int result = 0; + struct write_loose_object_data data; + struct child_process pack_proc = CHILD_PROCESS_INIT; + + /* + * Do not start pack-objects process + * if there are no loose objects. + */ + if (!for_each_loose_file_in_objdir(r->objects->odb->path, + bail_on_loose, + NULL, NULL, NULL)) + return 0; + + pack_proc.git_cmd = 1; + + strvec_push(&pack_proc.args, "pack-objects"); + if (opts->quiet) + strvec_push(&pack_proc.args, "--quiet"); + strvec_pushf(&pack_proc.args, "%s/pack/loose", r->objects->odb->path); + + pack_proc.in = -1; + + if (start_command(&pack_proc)) { + error(_("failed to start 'git pack-objects' process")); + return 1; + } + + data.in = xfdopen(pack_proc.in, "w"); + data.count = 0; + data.batch_size = 50000; + + for_each_loose_file_in_objdir(r->objects->odb->path, + write_loose_object_to_stdin, + NULL, + NULL, + &data); + + fclose(data.in); + + if (finish_command(&pack_proc)) { + error(_("failed to finish 'git pack-objects' process")); + result = 1; + } + + return result; +} + +static int maintenance_task_loose_objects(struct maintenance_run_opts *opts) +{ + return prune_packed(opts) || pack_loose(opts); +} + +static int incremental_repack_auto_condition(void) +{ + struct packed_git *p; + int incremental_repack_auto_limit = 10; + int count = 0; + + prepare_repo_settings(the_repository); + if (!the_repository->settings.core_multi_pack_index) + return 0; + + git_config_get_int("maintenance.incremental-repack.auto", + &incremental_repack_auto_limit); + + if (!incremental_repack_auto_limit) + return 0; + if (incremental_repack_auto_limit < 0) + return 1; + + for (p = get_packed_git(the_repository); + count < incremental_repack_auto_limit && p; + p = p->next) { + if (!p->multi_pack_index) + count++; + } + + return count >= incremental_repack_auto_limit; +} + +static int multi_pack_index_write(struct maintenance_run_opts *opts) +{ + struct child_process child = CHILD_PROCESS_INIT; + + child.git_cmd = 1; + strvec_pushl(&child.args, "multi-pack-index", "write", NULL); + + if (opts->quiet) + strvec_push(&child.args, "--no-progress"); + + if (run_command(&child)) + return error(_("failed to write multi-pack-index")); + + return 0; +} + +static int multi_pack_index_expire(struct maintenance_run_opts *opts) +{ + struct child_process child = CHILD_PROCESS_INIT; + + child.git_cmd = child.close_object_store = 1; + strvec_pushl(&child.args, "multi-pack-index", "expire", NULL); + + if (opts->quiet) + strvec_push(&child.args, "--no-progress"); + + if (run_command(&child)) + return error(_("'git multi-pack-index expire' failed")); + + return 0; +} + +#define TWO_GIGABYTES (INT32_MAX) + +static off_t get_auto_pack_size(void) +{ + /* + * The "auto" value is special: we optimize for + * one large pack-file (i.e. from a clone) and + * expect the rest to be small and they can be + * repacked quickly. + * + * The strategy we select here is to select a + * size that is one more than the second largest + * pack-file. This ensures that we will repack + * at least two packs if there are three or more + * packs. + */ + off_t max_size = 0; + off_t second_largest_size = 0; + off_t result_size; + struct packed_git *p; + struct repository *r = the_repository; + + reprepare_packed_git(r); + for (p = get_all_packs(r); p; p = p->next) { + if (p->pack_size > max_size) { + second_largest_size = max_size; + max_size = p->pack_size; + } else if (p->pack_size > second_largest_size) + second_largest_size = p->pack_size; + } + + result_size = second_largest_size + 1; + + /* But limit ourselves to a batch size of 2g */ + if (result_size > TWO_GIGABYTES) + result_size = TWO_GIGABYTES; + + return result_size; +} + +static int multi_pack_index_repack(struct maintenance_run_opts *opts) +{ + struct child_process child = CHILD_PROCESS_INIT; + + child.git_cmd = child.close_object_store = 1; + strvec_pushl(&child.args, "multi-pack-index", "repack", NULL); + + if (opts->quiet) + strvec_push(&child.args, "--no-progress"); + + strvec_pushf(&child.args, "--batch-size=%"PRIuMAX, + (uintmax_t)get_auto_pack_size()); + + if (run_command(&child)) + return error(_("'git multi-pack-index repack' failed")); + + return 0; +} + +static int maintenance_task_incremental_repack(struct maintenance_run_opts *opts) +{ + prepare_repo_settings(the_repository); + if (!the_repository->settings.core_multi_pack_index) { + warning(_("skipping incremental-repack task because core.multiPackIndex is disabled")); + return 0; + } + + if (multi_pack_index_write(opts)) + return 1; + if (multi_pack_index_expire(opts)) + return 1; + if (multi_pack_index_repack(opts)) + return 1; + return 0; +} + +typedef int maintenance_task_fn(struct maintenance_run_opts *opts); + +/* + * An auto condition function returns 1 if the task should run + * and 0 if the task should NOT run. See needs_to_gc() for an + * example. + */ +typedef int maintenance_auto_fn(void); + +struct maintenance_task { + const char *name; + maintenance_task_fn *fn; + maintenance_auto_fn *auto_condition; + unsigned enabled:1; + + enum schedule_priority schedule; + + /* -1 if not selected. */ + int selected_order; +}; + +enum maintenance_task_label { + TASK_PREFETCH, + TASK_LOOSE_OBJECTS, + TASK_INCREMENTAL_REPACK, + TASK_GC, + TASK_COMMIT_GRAPH, + TASK_PACK_REFS, + + /* Leave as final value */ + TASK__COUNT +}; + +static struct maintenance_task tasks[] = { + [TASK_PREFETCH] = { + "prefetch", + maintenance_task_prefetch, + }, + [TASK_LOOSE_OBJECTS] = { + "loose-objects", + maintenance_task_loose_objects, + loose_object_auto_condition, + }, + [TASK_INCREMENTAL_REPACK] = { + "incremental-repack", + maintenance_task_incremental_repack, + incremental_repack_auto_condition, + }, + [TASK_GC] = { + "gc", + maintenance_task_gc, + need_to_gc, + 1, + }, + [TASK_COMMIT_GRAPH] = { + "commit-graph", + maintenance_task_commit_graph, + should_write_commit_graph, + }, + [TASK_PACK_REFS] = { + "pack-refs", + maintenance_task_pack_refs, + NULL, + }, +}; + +static int compare_tasks_by_selection(const void *a_, const void *b_) +{ + const struct maintenance_task *a = a_; + const struct maintenance_task *b = b_; + + return b->selected_order - a->selected_order; +} + +static int maintenance_run_tasks(struct maintenance_run_opts *opts) +{ + int i, found_selected = 0; + int result = 0; + struct lock_file lk; + struct repository *r = the_repository; + char *lock_path = xstrfmt("%s/maintenance", r->objects->odb->path); + + if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) { + /* + * Another maintenance command is running. + * + * If --auto was provided, then it is likely due to a + * recursive process stack. Do not report an error in + * that case. + */ + if (!opts->auto_flag && !opts->quiet) + warning(_("lock file '%s' exists, skipping maintenance"), + lock_path); + free(lock_path); + return 0; + } + free(lock_path); + + for (i = 0; !found_selected && i < TASK__COUNT; i++) + found_selected = tasks[i].selected_order >= 0; + + if (found_selected) + QSORT(tasks, TASK__COUNT, compare_tasks_by_selection); + + for (i = 0; i < TASK__COUNT; i++) { + if (found_selected && tasks[i].selected_order < 0) + continue; + + if (!found_selected && !tasks[i].enabled) + continue; + + if (opts->auto_flag && + (!tasks[i].auto_condition || + !tasks[i].auto_condition())) + continue; + + if (opts->schedule && tasks[i].schedule < opts->schedule) + continue; + + trace2_region_enter("maintenance", tasks[i].name, r); + if (tasks[i].fn(opts)) { + error(_("task '%s' failed"), tasks[i].name); + result = 1; + } + trace2_region_leave("maintenance", tasks[i].name, r); + } + + rollback_lock_file(&lk); + return result; +} + +static void initialize_maintenance_strategy(void) +{ + char *config_str; + + if (git_config_get_string("maintenance.strategy", &config_str)) + return; + + if (!strcasecmp(config_str, "incremental")) { + tasks[TASK_GC].schedule = SCHEDULE_NONE; + tasks[TASK_COMMIT_GRAPH].enabled = 1; + tasks[TASK_COMMIT_GRAPH].schedule = SCHEDULE_HOURLY; + tasks[TASK_PREFETCH].enabled = 1; + tasks[TASK_PREFETCH].schedule = SCHEDULE_HOURLY; + tasks[TASK_INCREMENTAL_REPACK].enabled = 1; + tasks[TASK_INCREMENTAL_REPACK].schedule = SCHEDULE_DAILY; + tasks[TASK_LOOSE_OBJECTS].enabled = 1; + tasks[TASK_LOOSE_OBJECTS].schedule = SCHEDULE_DAILY; + tasks[TASK_PACK_REFS].enabled = 1; + tasks[TASK_PACK_REFS].schedule = SCHEDULE_WEEKLY; + } +} + +static void initialize_task_config(int schedule) +{ + int i; + struct strbuf config_name = STRBUF_INIT; + gc_config(); + + if (schedule) + initialize_maintenance_strategy(); + + for (i = 0; i < TASK__COUNT; i++) { + int config_value; + char *config_str; + + strbuf_reset(&config_name); + strbuf_addf(&config_name, "maintenance.%s.enabled", + tasks[i].name); + + if (!git_config_get_bool(config_name.buf, &config_value)) + tasks[i].enabled = config_value; + + strbuf_reset(&config_name); + strbuf_addf(&config_name, "maintenance.%s.schedule", + tasks[i].name); + + if (!git_config_get_string(config_name.buf, &config_str)) { + tasks[i].schedule = parse_schedule(config_str); + free(config_str); + } + } + + strbuf_release(&config_name); +} + +static int task_option_parse(const struct option *opt UNUSED, + const char *arg, int unset) +{ + int i, num_selected = 0; + struct maintenance_task *task = NULL; + + BUG_ON_OPT_NEG(unset); + + for (i = 0; i < TASK__COUNT; i++) { + if (tasks[i].selected_order >= 0) + num_selected++; + if (!strcasecmp(tasks[i].name, arg)) { + task = &tasks[i]; + } + } + + if (!task) { + error(_("'%s' is not a valid task"), arg); + return 1; + } + + if (task->selected_order >= 0) { + error(_("task '%s' cannot be selected multiple times"), arg); + return 1; + } + + task->selected_order = num_selected + 1; + + return 0; +} + +static int maintenance_run(int argc, const char **argv, const char *prefix) +{ + int i; + struct maintenance_run_opts opts; + struct option builtin_maintenance_run_options[] = { + OPT_BOOL(0, "auto", &opts.auto_flag, + N_("run tasks based on the state of the repository")), + OPT_CALLBACK(0, "schedule", &opts.schedule, N_("frequency"), + N_("run tasks based on frequency"), + maintenance_opt_schedule), + OPT_BOOL(0, "quiet", &opts.quiet, + N_("do not report progress or other information over stderr")), + OPT_CALLBACK_F(0, "task", NULL, N_("task"), + N_("run a specific task"), + PARSE_OPT_NONEG, task_option_parse), + OPT_END() + }; + memset(&opts, 0, sizeof(opts)); + + opts.quiet = !isatty(2); + + for (i = 0; i < TASK__COUNT; i++) + tasks[i].selected_order = -1; + + argc = parse_options(argc, argv, prefix, + builtin_maintenance_run_options, + builtin_maintenance_run_usage, + PARSE_OPT_STOP_AT_NON_OPTION); + + if (opts.auto_flag && opts.schedule) + die(_("use at most one of --auto and --schedule=<frequency>")); + + initialize_task_config(opts.schedule); + + if (argc != 0) + usage_with_options(builtin_maintenance_run_usage, + builtin_maintenance_run_options); + return maintenance_run_tasks(&opts); +} + +static char *get_maintpath(void) +{ + struct strbuf sb = STRBUF_INIT; + const char *p = the_repository->worktree ? + the_repository->worktree : the_repository->gitdir; + + strbuf_realpath(&sb, p, 1); + return strbuf_detach(&sb, NULL); +} + +static char const * const builtin_maintenance_register_usage[] = { + "git maintenance register [--config-file <path>]", + NULL +}; + +static int maintenance_register(int argc, const char **argv, const char *prefix) +{ + char *config_file = NULL; + struct option options[] = { + OPT_STRING(0, "config-file", &config_file, N_("file"), N_("use given config file")), + OPT_END(), + }; + int found = 0; + const char *key = "maintenance.repo"; + char *maintpath = get_maintpath(); + struct string_list_item *item; + const struct string_list *list; + + argc = parse_options(argc, argv, prefix, options, + builtin_maintenance_register_usage, 0); + if (argc) + usage_with_options(builtin_maintenance_register_usage, + options); + + /* Disable foreground maintenance */ + git_config_set("maintenance.auto", "false"); + + /* Set maintenance strategy, if unset */ + if (git_config_get("maintenance.strategy")) + git_config_set("maintenance.strategy", "incremental"); + + if (!git_config_get_string_multi(key, &list)) { + for_each_string_list_item(item, list) { + if (!strcmp(maintpath, item->string)) { + found = 1; + break; + } + } + } + + if (!found) { + int rc; + char *user_config = NULL, *xdg_config = NULL; + + if (!config_file) { + git_global_config(&user_config, &xdg_config); + config_file = user_config; + if (!user_config) + die(_("$HOME not set")); + } + rc = git_config_set_multivar_in_file_gently( + config_file, "maintenance.repo", maintpath, + CONFIG_REGEX_NONE, 0); + free(user_config); + free(xdg_config); + + if (rc) + die(_("unable to add '%s' value of '%s'"), + key, maintpath); + } + + free(maintpath); + return 0; +} + +static char const * const builtin_maintenance_unregister_usage[] = { + "git maintenance unregister [--config-file <path>] [--force]", + NULL +}; + +static int maintenance_unregister(int argc, const char **argv, const char *prefix) +{ + int force = 0; + char *config_file = NULL; + struct option options[] = { + OPT_STRING(0, "config-file", &config_file, N_("file"), N_("use given config file")), + OPT__FORCE(&force, + N_("return success even if repository was not registered"), + PARSE_OPT_NOCOMPLETE), + OPT_END(), + }; + const char *key = "maintenance.repo"; + char *maintpath = get_maintpath(); + int found = 0; + struct string_list_item *item; + const struct string_list *list; + struct config_set cs = { { 0 } }; + + argc = parse_options(argc, argv, prefix, options, + builtin_maintenance_unregister_usage, 0); + if (argc) + usage_with_options(builtin_maintenance_unregister_usage, + options); + + if (config_file) { + git_configset_init(&cs); + git_configset_add_file(&cs, config_file); + } + if (!(config_file + ? git_configset_get_string_multi(&cs, key, &list) + : git_config_get_string_multi(key, &list))) { + for_each_string_list_item(item, list) { + if (!strcmp(maintpath, item->string)) { + found = 1; + break; + } + } + } + + if (found) { + int rc; + char *user_config = NULL, *xdg_config = NULL; + if (!config_file) { + git_global_config(&user_config, &xdg_config); + config_file = user_config; + if (!user_config) + die(_("$HOME not set")); + } + rc = git_config_set_multivar_in_file_gently( + config_file, key, NULL, maintpath, + CONFIG_FLAGS_MULTI_REPLACE | CONFIG_FLAGS_FIXED_VALUE); + free(user_config); + free(xdg_config); + + if (rc && + (!force || rc == CONFIG_NOTHING_SET)) + die(_("unable to unset '%s' value of '%s'"), + key, maintpath); + } else if (!force) { + die(_("repository '%s' is not registered"), maintpath); + } + + git_configset_clear(&cs); + free(maintpath); + return 0; +} + +static const char *get_frequency(enum schedule_priority schedule) +{ + switch (schedule) { + case SCHEDULE_HOURLY: + return "hourly"; + case SCHEDULE_DAILY: + return "daily"; + case SCHEDULE_WEEKLY: + return "weekly"; + default: + BUG("invalid schedule %d", schedule); + } +} + +/* + * get_schedule_cmd` reads the GIT_TEST_MAINT_SCHEDULER environment variable + * to mock the schedulers that `git maintenance start` rely on. + * + * For test purpose, GIT_TEST_MAINT_SCHEDULER can be set to a comma-separated + * list of colon-separated key/value pairs where each pair contains a scheduler + * and its corresponding mock. + * + * * If $GIT_TEST_MAINT_SCHEDULER is not set, return false and leave the + * arguments unmodified. + * + * * If $GIT_TEST_MAINT_SCHEDULER is set, return true. + * In this case, the *cmd value is read as input. + * + * * if the input value *cmd is the key of one of the comma-separated list + * item, then *is_available is set to true and *cmd is modified and becomes + * the mock command. + * + * * if the input value *cmd isn’t the key of any of the comma-separated list + * item, then *is_available is set to false. + * + * Ex.: + * GIT_TEST_MAINT_SCHEDULER not set + * +-------+-------------------------------------------------+ + * | Input | Output | + * | *cmd | return code | *cmd | *is_available | + * +-------+-------------+-------------------+---------------+ + * | "foo" | false | "foo" (unchanged) | (unchanged) | + * +-------+-------------+-------------------+---------------+ + * + * GIT_TEST_MAINT_SCHEDULER set to “foo:./mock_foo.sh,bar:./mock_bar.sh” + * +-------+-------------------------------------------------+ + * | Input | Output | + * | *cmd | return code | *cmd | *is_available | + * +-------+-------------+-------------------+---------------+ + * | "foo" | true | "./mock.foo.sh" | true | + * | "qux" | true | "qux" (unchanged) | false | + * +-------+-------------+-------------------+---------------+ + */ +static int get_schedule_cmd(const char **cmd, int *is_available) +{ + char *testing = xstrdup_or_null(getenv("GIT_TEST_MAINT_SCHEDULER")); + struct string_list_item *item; + struct string_list list = STRING_LIST_INIT_NODUP; + + if (!testing) + return 0; + + if (is_available) + *is_available = 0; + + string_list_split_in_place(&list, testing, ",", -1); + for_each_string_list_item(item, &list) { + struct string_list pair = STRING_LIST_INIT_NODUP; + + if (string_list_split_in_place(&pair, item->string, ":", 2) != 2) + continue; + + if (!strcmp(*cmd, pair.items[0].string)) { + *cmd = pair.items[1].string; + if (is_available) + *is_available = 1; + string_list_clear(&list, 0); + UNLEAK(testing); + return 1; + } + } + + string_list_clear(&list, 0); + free(testing); + return 1; +} + +static int get_random_minute(void) +{ + /* Use a static value when under tests. */ + if (getenv("GIT_TEST_MAINT_SCHEDULER")) + return 13; + + return git_rand() % 60; +} + +static int is_launchctl_available(void) +{ + const char *cmd = "launchctl"; + int is_available; + if (get_schedule_cmd(&cmd, &is_available)) + return is_available; + +#ifdef __APPLE__ + return 1; +#else + return 0; +#endif +} + +static char *launchctl_service_name(const char *frequency) +{ + struct strbuf label = STRBUF_INIT; + strbuf_addf(&label, "org.git-scm.git.%s", frequency); + return strbuf_detach(&label, NULL); +} + +static char *launchctl_service_filename(const char *name) +{ + char *expanded; + struct strbuf filename = STRBUF_INIT; + strbuf_addf(&filename, "~/Library/LaunchAgents/%s.plist", name); + + expanded = interpolate_path(filename.buf, 1); + if (!expanded) + die(_("failed to expand path '%s'"), filename.buf); + + strbuf_release(&filename); + return expanded; +} + +static char *launchctl_get_uid(void) +{ + return xstrfmt("gui/%d", getuid()); +} + +static int launchctl_boot_plist(int enable, const char *filename) +{ + const char *cmd = "launchctl"; + int result; + struct child_process child = CHILD_PROCESS_INIT; + char *uid = launchctl_get_uid(); + + get_schedule_cmd(&cmd, NULL); + strvec_split(&child.args, cmd); + strvec_pushl(&child.args, enable ? "bootstrap" : "bootout", uid, + filename, NULL); + + child.no_stderr = 1; + child.no_stdout = 1; + + if (start_command(&child)) + die(_("failed to start launchctl")); + + result = finish_command(&child); + + free(uid); + return result; +} + +static int launchctl_remove_plist(enum schedule_priority schedule) +{ + const char *frequency = get_frequency(schedule); + char *name = launchctl_service_name(frequency); + char *filename = launchctl_service_filename(name); + int result = launchctl_boot_plist(0, filename); + unlink(filename); + free(filename); + free(name); + return result; +} + +static int launchctl_remove_plists(void) +{ + return launchctl_remove_plist(SCHEDULE_HOURLY) || + launchctl_remove_plist(SCHEDULE_DAILY) || + launchctl_remove_plist(SCHEDULE_WEEKLY); +} + +static int launchctl_list_contains_plist(const char *name, const char *cmd) +{ + struct child_process child = CHILD_PROCESS_INIT; + + strvec_split(&child.args, cmd); + strvec_pushl(&child.args, "list", name, NULL); + + child.no_stderr = 1; + child.no_stdout = 1; + + if (start_command(&child)) + die(_("failed to start launchctl")); + + /* Returns failure if 'name' doesn't exist. */ + return !finish_command(&child); +} + +static int launchctl_schedule_plist(const char *exec_path, enum schedule_priority schedule) +{ + int i, fd; + const char *preamble, *repeat; + const char *frequency = get_frequency(schedule); + char *name = launchctl_service_name(frequency); + char *filename = launchctl_service_filename(name); + struct lock_file lk = LOCK_INIT; + static unsigned long lock_file_timeout_ms = ULONG_MAX; + struct strbuf plist = STRBUF_INIT, plist2 = STRBUF_INIT; + struct stat st; + const char *cmd = "launchctl"; + int minute = get_random_minute(); + + get_schedule_cmd(&cmd, NULL); + preamble = "<?xml version=\"1.0\"?>\n" + "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n" + "<plist version=\"1.0\">" + "<dict>\n" + "<key>Label</key><string>%s</string>\n" + "<key>ProgramArguments</key>\n" + "<array>\n" + "<string>%s/git</string>\n" + "<string>--exec-path=%s</string>\n" + "<string>for-each-repo</string>\n" + "<string>--config=maintenance.repo</string>\n" + "<string>maintenance</string>\n" + "<string>run</string>\n" + "<string>--schedule=%s</string>\n" + "</array>\n" + "<key>StartCalendarInterval</key>\n" + "<array>\n"; + strbuf_addf(&plist, preamble, name, exec_path, exec_path, frequency); + + switch (schedule) { + case SCHEDULE_HOURLY: + repeat = "<dict>\n" + "<key>Hour</key><integer>%d</integer>\n" + "<key>Minute</key><integer>%d</integer>\n" + "</dict>\n"; + for (i = 1; i <= 23; i++) + strbuf_addf(&plist, repeat, i, minute); + break; + + case SCHEDULE_DAILY: + repeat = "<dict>\n" + "<key>Day</key><integer>%d</integer>\n" + "<key>Hour</key><integer>0</integer>\n" + "<key>Minute</key><integer>%d</integer>\n" + "</dict>\n"; + for (i = 1; i <= 6; i++) + strbuf_addf(&plist, repeat, i, minute); + break; + + case SCHEDULE_WEEKLY: + strbuf_addf(&plist, + "<dict>\n" + "<key>Day</key><integer>0</integer>\n" + "<key>Hour</key><integer>0</integer>\n" + "<key>Minute</key><integer>%d</integer>\n" + "</dict>\n", + minute); + break; + + default: + /* unreachable */ + break; + } + strbuf_addstr(&plist, "</array>\n</dict>\n</plist>\n"); + + if (safe_create_leading_directories(filename)) + die(_("failed to create directories for '%s'"), filename); + + if ((long)lock_file_timeout_ms < 0 && + git_config_get_ulong("gc.launchctlplistlocktimeoutms", + &lock_file_timeout_ms)) + lock_file_timeout_ms = 150; + + fd = hold_lock_file_for_update_timeout(&lk, filename, LOCK_DIE_ON_ERROR, + lock_file_timeout_ms); + + /* + * Does this file already exist? With the intended contents? Is it + * registered already? Then it does not need to be re-registered. + */ + if (!stat(filename, &st) && st.st_size == plist.len && + strbuf_read_file(&plist2, filename, plist.len) == plist.len && + !strbuf_cmp(&plist, &plist2) && + launchctl_list_contains_plist(name, cmd)) + rollback_lock_file(&lk); + else { + if (write_in_full(fd, plist.buf, plist.len) < 0 || + commit_lock_file(&lk)) + die_errno(_("could not write '%s'"), filename); + + /* bootout might fail if not already running, so ignore */ + launchctl_boot_plist(0, filename); + if (launchctl_boot_plist(1, filename)) + die(_("failed to bootstrap service %s"), filename); + } + + free(filename); + free(name); + strbuf_release(&plist); + strbuf_release(&plist2); + return 0; +} + +static int launchctl_add_plists(void) +{ + const char *exec_path = git_exec_path(); + + return launchctl_schedule_plist(exec_path, SCHEDULE_HOURLY) || + launchctl_schedule_plist(exec_path, SCHEDULE_DAILY) || + launchctl_schedule_plist(exec_path, SCHEDULE_WEEKLY); +} + +static int launchctl_update_schedule(int run_maintenance, int fd UNUSED) +{ + if (run_maintenance) + return launchctl_add_plists(); + else + return launchctl_remove_plists(); +} + +static int is_schtasks_available(void) +{ + const char *cmd = "schtasks"; + int is_available; + if (get_schedule_cmd(&cmd, &is_available)) + return is_available; + +#ifdef GIT_WINDOWS_NATIVE + return 1; +#else + return 0; +#endif +} + +static char *schtasks_task_name(const char *frequency) +{ + struct strbuf label = STRBUF_INIT; + strbuf_addf(&label, "Git Maintenance (%s)", frequency); + return strbuf_detach(&label, NULL); +} + +static int schtasks_remove_task(enum schedule_priority schedule) +{ + const char *cmd = "schtasks"; + struct child_process child = CHILD_PROCESS_INIT; + const char *frequency = get_frequency(schedule); + char *name = schtasks_task_name(frequency); + + get_schedule_cmd(&cmd, NULL); + strvec_split(&child.args, cmd); + strvec_pushl(&child.args, "/delete", "/tn", name, "/f", NULL); + free(name); + + return run_command(&child); +} + +static int schtasks_remove_tasks(void) +{ + return schtasks_remove_task(SCHEDULE_HOURLY) || + schtasks_remove_task(SCHEDULE_DAILY) || + schtasks_remove_task(SCHEDULE_WEEKLY); +} + +static int schtasks_schedule_task(const char *exec_path, enum schedule_priority schedule) +{ + const char *cmd = "schtasks"; + int result; + struct child_process child = CHILD_PROCESS_INIT; + const char *xml; + struct tempfile *tfile; + const char *frequency = get_frequency(schedule); + char *name = schtasks_task_name(frequency); + struct strbuf tfilename = STRBUF_INIT; + int minute = get_random_minute(); + + get_schedule_cmd(&cmd, NULL); + + strbuf_addf(&tfilename, "%s/schedule_%s_XXXXXX", + get_git_common_dir(), frequency); + tfile = xmks_tempfile(tfilename.buf); + strbuf_release(&tfilename); + + if (!fdopen_tempfile(tfile, "w")) + die(_("failed to create temp xml file")); + + xml = "<?xml version=\"1.0\" ?>\n" + "<Task version=\"1.4\" xmlns=\"http://schemas.microsoft.com/windows/2004/02/mit/task\">\n" + "<Triggers>\n" + "<CalendarTrigger>\n"; + fputs(xml, tfile->fp); + + switch (schedule) { + case SCHEDULE_HOURLY: + fprintf(tfile->fp, + "<StartBoundary>2020-01-01T01:%02d:00</StartBoundary>\n" + "<Enabled>true</Enabled>\n" + "<ScheduleByDay>\n" + "<DaysInterval>1</DaysInterval>\n" + "</ScheduleByDay>\n" + "<Repetition>\n" + "<Interval>PT1H</Interval>\n" + "<Duration>PT23H</Duration>\n" + "<StopAtDurationEnd>false</StopAtDurationEnd>\n" + "</Repetition>\n", + minute); + break; + + case SCHEDULE_DAILY: + fprintf(tfile->fp, + "<StartBoundary>2020-01-01T00:%02d:00</StartBoundary>\n" + "<Enabled>true</Enabled>\n" + "<ScheduleByWeek>\n" + "<DaysOfWeek>\n" + "<Monday />\n" + "<Tuesday />\n" + "<Wednesday />\n" + "<Thursday />\n" + "<Friday />\n" + "<Saturday />\n" + "</DaysOfWeek>\n" + "<WeeksInterval>1</WeeksInterval>\n" + "</ScheduleByWeek>\n", + minute); + break; + + case SCHEDULE_WEEKLY: + fprintf(tfile->fp, + "<StartBoundary>2020-01-01T00:%02d:00</StartBoundary>\n" + "<Enabled>true</Enabled>\n" + "<ScheduleByWeek>\n" + "<DaysOfWeek>\n" + "<Sunday />\n" + "</DaysOfWeek>\n" + "<WeeksInterval>1</WeeksInterval>\n" + "</ScheduleByWeek>\n", + minute); + break; + + default: + break; + } + + xml = "</CalendarTrigger>\n" + "</Triggers>\n" + "<Principals>\n" + "<Principal id=\"Author\">\n" + "<LogonType>InteractiveToken</LogonType>\n" + "<RunLevel>LeastPrivilege</RunLevel>\n" + "</Principal>\n" + "</Principals>\n" + "<Settings>\n" + "<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\n" + "<Enabled>true</Enabled>\n" + "<Hidden>true</Hidden>\n" + "<UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>\n" + "<WakeToRun>false</WakeToRun>\n" + "<ExecutionTimeLimit>PT72H</ExecutionTimeLimit>\n" + "<Priority>7</Priority>\n" + "</Settings>\n" + "<Actions Context=\"Author\">\n" + "<Exec>\n" + "<Command>\"%s\\headless-git.exe\"</Command>\n" + "<Arguments>--exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%s</Arguments>\n" + "</Exec>\n" + "</Actions>\n" + "</Task>\n"; + fprintf(tfile->fp, xml, exec_path, exec_path, frequency); + strvec_split(&child.args, cmd); + strvec_pushl(&child.args, "/create", "/tn", name, "/f", "/xml", + get_tempfile_path(tfile), NULL); + close_tempfile_gently(tfile); + + child.no_stdout = 1; + child.no_stderr = 1; + + if (start_command(&child)) + die(_("failed to start schtasks")); + result = finish_command(&child); + + delete_tempfile(&tfile); + free(name); + return result; +} + +static int schtasks_schedule_tasks(void) +{ + const char *exec_path = git_exec_path(); + + return schtasks_schedule_task(exec_path, SCHEDULE_HOURLY) || + schtasks_schedule_task(exec_path, SCHEDULE_DAILY) || + schtasks_schedule_task(exec_path, SCHEDULE_WEEKLY); +} + +static int schtasks_update_schedule(int run_maintenance, int fd UNUSED) +{ + if (run_maintenance) + return schtasks_schedule_tasks(); + else + return schtasks_remove_tasks(); +} + +MAYBE_UNUSED +static int check_crontab_process(const char *cmd) +{ + struct child_process child = CHILD_PROCESS_INIT; + + strvec_split(&child.args, cmd); + strvec_push(&child.args, "-l"); + child.no_stdin = 1; + child.no_stdout = 1; + child.no_stderr = 1; + child.silent_exec_failure = 1; + + if (start_command(&child)) + return 0; + /* Ignore exit code, as an empty crontab will return error. */ + finish_command(&child); + return 1; +} + +static int is_crontab_available(void) +{ + const char *cmd = "crontab"; + int is_available; + + if (get_schedule_cmd(&cmd, &is_available)) + return is_available; + +#ifdef __APPLE__ + /* + * macOS has cron, but it requires special permissions and will + * create a UI alert when attempting to run this command. + */ + return 0; +#else + return check_crontab_process(cmd); +#endif +} + +#define BEGIN_LINE "# BEGIN GIT MAINTENANCE SCHEDULE" +#define END_LINE "# END GIT MAINTENANCE SCHEDULE" + +static int crontab_update_schedule(int run_maintenance, int fd) +{ + const char *cmd = "crontab"; + int result = 0; + int in_old_region = 0; + struct child_process crontab_list = CHILD_PROCESS_INIT; + struct child_process crontab_edit = CHILD_PROCESS_INIT; + FILE *cron_list, *cron_in; + struct strbuf line = STRBUF_INIT; + struct tempfile *tmpedit = NULL; + int minute = get_random_minute(); + + get_schedule_cmd(&cmd, NULL); + strvec_split(&crontab_list.args, cmd); + strvec_push(&crontab_list.args, "-l"); + crontab_list.in = -1; + crontab_list.out = dup(fd); + crontab_list.git_cmd = 0; + + if (start_command(&crontab_list)) + return error(_("failed to run 'crontab -l'; your system might not support 'cron'")); + + /* Ignore exit code, as an empty crontab will return error. */ + finish_command(&crontab_list); + + tmpedit = mks_tempfile_t(".git_cron_edit_tmpXXXXXX"); + if (!tmpedit) { + result = error(_("failed to create crontab temporary file")); + goto out; + } + cron_in = fdopen_tempfile(tmpedit, "w"); + if (!cron_in) { + result = error(_("failed to open temporary file")); + goto out; + } + + /* + * Read from the .lock file, filtering out the old + * schedule while appending the new schedule. + */ + cron_list = fdopen(fd, "r"); + rewind(cron_list); + + while (!strbuf_getline_lf(&line, cron_list)) { + if (!in_old_region && !strcmp(line.buf, BEGIN_LINE)) + in_old_region = 1; + else if (in_old_region && !strcmp(line.buf, END_LINE)) + in_old_region = 0; + else if (!in_old_region) + fprintf(cron_in, "%s\n", line.buf); + } + strbuf_release(&line); + + if (run_maintenance) { + struct strbuf line_format = STRBUF_INIT; + const char *exec_path = git_exec_path(); + + fprintf(cron_in, "%s\n", BEGIN_LINE); + fprintf(cron_in, + "# The following schedule was created by Git\n"); + fprintf(cron_in, "# Any edits made in this region might be\n"); + fprintf(cron_in, + "# replaced in the future by a Git command.\n\n"); + + strbuf_addf(&line_format, + "%%d %%s * * %%s \"%s/git\" --exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%s\n", + exec_path, exec_path); + fprintf(cron_in, line_format.buf, minute, "1-23", "*", "hourly"); + fprintf(cron_in, line_format.buf, minute, "0", "1-6", "daily"); + fprintf(cron_in, line_format.buf, minute, "0", "0", "weekly"); + strbuf_release(&line_format); + + fprintf(cron_in, "\n%s\n", END_LINE); + } + + fflush(cron_in); + + strvec_split(&crontab_edit.args, cmd); + strvec_push(&crontab_edit.args, get_tempfile_path(tmpedit)); + crontab_edit.git_cmd = 0; + + if (start_command(&crontab_edit)) { + result = error(_("failed to run 'crontab'; your system might not support 'cron'")); + goto out; + } + + if (finish_command(&crontab_edit)) + result = error(_("'crontab' died")); + else + fclose(cron_list); +out: + delete_tempfile(&tmpedit); + return result; +} + +static int real_is_systemd_timer_available(void) +{ + struct child_process child = CHILD_PROCESS_INIT; + + strvec_pushl(&child.args, "systemctl", "--user", "list-timers", NULL); + child.no_stdin = 1; + child.no_stdout = 1; + child.no_stderr = 1; + child.silent_exec_failure = 1; + + if (start_command(&child)) + return 0; + if (finish_command(&child)) + return 0; + return 1; +} + +static int is_systemd_timer_available(void) +{ + const char *cmd = "systemctl"; + int is_available; + + if (get_schedule_cmd(&cmd, &is_available)) + return is_available; + + return real_is_systemd_timer_available(); +} + +static char *xdg_config_home_systemd(const char *filename) +{ + return xdg_config_home_for("systemd/user", filename); +} + +#define SYSTEMD_UNIT_FORMAT "git-maintenance@%s.%s" + +static int systemd_timer_delete_timer_file(enum schedule_priority priority) +{ + int ret = 0; + const char *frequency = get_frequency(priority); + char *local_timer_name = xstrfmt(SYSTEMD_UNIT_FORMAT, frequency, "timer"); + char *filename = xdg_config_home_systemd(local_timer_name); + + if (unlink(filename) && !is_missing_file_error(errno)) + ret = error_errno(_("failed to delete '%s'"), filename); + + free(filename); + free(local_timer_name); + return ret; +} + +static int systemd_timer_delete_service_template(void) +{ + int ret = 0; + char *local_service_name = xstrfmt(SYSTEMD_UNIT_FORMAT, "", "service"); + char *filename = xdg_config_home_systemd(local_service_name); + if (unlink(filename) && !is_missing_file_error(errno)) + ret = error_errno(_("failed to delete '%s'"), filename); + + free(filename); + free(local_service_name); + return ret; +} + +/* + * Write the schedule information into a git-maintenance@<schedule>.timer + * file using a custom minute. This timer file cannot use the templating + * system, so we generate a specific file for each. + */ +static int systemd_timer_write_timer_file(enum schedule_priority schedule, + int minute) +{ + int res = -1; + char *filename; + FILE *file; + const char *unit; + char *schedule_pattern = NULL; + const char *frequency = get_frequency(schedule); + char *local_timer_name = xstrfmt(SYSTEMD_UNIT_FORMAT, frequency, "timer"); + + filename = xdg_config_home_systemd(local_timer_name); + + if (safe_create_leading_directories(filename)) { + error(_("failed to create directories for '%s'"), filename); + goto error; + } + file = fopen_or_warn(filename, "w"); + if (!file) + goto error; + + switch (schedule) { + case SCHEDULE_HOURLY: + schedule_pattern = xstrfmt("*-*-* 1..23:%02d:00", minute); + break; + + case SCHEDULE_DAILY: + schedule_pattern = xstrfmt("Tue..Sun *-*-* 0:%02d:00", minute); + break; + + case SCHEDULE_WEEKLY: + schedule_pattern = xstrfmt("Mon 0:%02d:00", minute); + break; + + default: + BUG("Unhandled schedule_priority"); + } + + unit = "# This file was created and is maintained by Git.\n" + "# Any edits made in this file might be replaced in the future\n" + "# by a Git command.\n" + "\n" + "[Unit]\n" + "Description=Optimize Git repositories data\n" + "\n" + "[Timer]\n" + "OnCalendar=%s\n" + "Persistent=true\n" + "\n" + "[Install]\n" + "WantedBy=timers.target\n"; + if (fprintf(file, unit, schedule_pattern) < 0) { + error(_("failed to write to '%s'"), filename); + fclose(file); + goto error; + } + if (fclose(file) == EOF) { + error_errno(_("failed to flush '%s'"), filename); + goto error; + } + + res = 0; + +error: + free(schedule_pattern); + free(local_timer_name); + free(filename); + return res; +} + +/* + * No matter the schedule, we use the same service and can make use of the + * templating system. When installing git-maintenance@<schedule>.timer, + * systemd will notice that git-maintenance@.service exists as a template + * and will use this file and insert the <schedule> into the template at + * the position of "%i". + */ +static int systemd_timer_write_service_template(const char *exec_path) +{ + int res = -1; + char *filename; + FILE *file; + const char *unit; + char *local_service_name = xstrfmt(SYSTEMD_UNIT_FORMAT, "", "service"); + + filename = xdg_config_home_systemd(local_service_name); + if (safe_create_leading_directories(filename)) { + error(_("failed to create directories for '%s'"), filename); + goto error; + } + file = fopen_or_warn(filename, "w"); + if (!file) + goto error; + + unit = "# This file was created and is maintained by Git.\n" + "# Any edits made in this file might be replaced in the future\n" + "# by a Git command.\n" + "\n" + "[Unit]\n" + "Description=Optimize Git repositories data\n" + "\n" + "[Service]\n" + "Type=oneshot\n" + "ExecStart=\"%s/git\" --exec-path=\"%s\" for-each-repo --config=maintenance.repo maintenance run --schedule=%%i\n" + "LockPersonality=yes\n" + "MemoryDenyWriteExecute=yes\n" + "NoNewPrivileges=yes\n" + "RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_VSOCK\n" + "RestrictNamespaces=yes\n" + "RestrictRealtime=yes\n" + "RestrictSUIDSGID=yes\n" + "SystemCallArchitectures=native\n" + "SystemCallFilter=@system-service\n"; + if (fprintf(file, unit, exec_path, exec_path) < 0) { + error(_("failed to write to '%s'"), filename); + fclose(file); + goto error; + } + if (fclose(file) == EOF) { + error_errno(_("failed to flush '%s'"), filename); + goto error; + } + + res = 0; + +error: + free(local_service_name); + free(filename); + return res; +} + +static int systemd_timer_enable_unit(int enable, + enum schedule_priority schedule, + int minute) +{ + const char *cmd = "systemctl"; + struct child_process child = CHILD_PROCESS_INIT; + const char *frequency = get_frequency(schedule); + + /* + * Disabling the systemd unit while it is already disabled makes + * systemctl print an error. + * Let's ignore it since it means we already are in the expected state: + * the unit is disabled. + * + * On the other hand, enabling a systemd unit which is already enabled + * produces no error. + */ + if (!enable) + child.no_stderr = 1; + else if (systemd_timer_write_timer_file(schedule, minute)) + return -1; + + get_schedule_cmd(&cmd, NULL); + strvec_split(&child.args, cmd); + strvec_pushl(&child.args, "--user", enable ? "enable" : "disable", + "--now", NULL); + strvec_pushf(&child.args, SYSTEMD_UNIT_FORMAT, frequency, "timer"); + + if (start_command(&child)) + return error(_("failed to start systemctl")); + if (finish_command(&child)) + /* + * Disabling an already disabled systemd unit makes + * systemctl fail. + * Let's ignore this failure. + * + * Enabling an enabled systemd unit doesn't fail. + */ + if (enable) + return error(_("failed to run systemctl")); + return 0; +} + +/* + * A previous version of Git wrote the timer units as template files. + * Clean these up, if they exist. + */ +static void systemd_timer_delete_stale_timer_templates(void) +{ + char *timer_template_name = xstrfmt(SYSTEMD_UNIT_FORMAT, "", "timer"); + char *filename = xdg_config_home_systemd(timer_template_name); + + if (unlink(filename) && !is_missing_file_error(errno)) + warning(_("failed to delete '%s'"), filename); + + free(filename); + free(timer_template_name); +} + +static int systemd_timer_delete_unit_files(void) +{ + systemd_timer_delete_stale_timer_templates(); + + /* Purposefully not short-circuited to make sure all are called. */ + return systemd_timer_delete_timer_file(SCHEDULE_HOURLY) | + systemd_timer_delete_timer_file(SCHEDULE_DAILY) | + systemd_timer_delete_timer_file(SCHEDULE_WEEKLY) | + systemd_timer_delete_service_template(); +} + +static int systemd_timer_delete_units(void) +{ + int minute = get_random_minute(); + /* Purposefully not short-circuited to make sure all are called. */ + return systemd_timer_enable_unit(0, SCHEDULE_HOURLY, minute) | + systemd_timer_enable_unit(0, SCHEDULE_DAILY, minute) | + systemd_timer_enable_unit(0, SCHEDULE_WEEKLY, minute) | + systemd_timer_delete_unit_files(); +} + +static int systemd_timer_setup_units(void) +{ + int minute = get_random_minute(); + const char *exec_path = git_exec_path(); + + int ret = systemd_timer_write_service_template(exec_path) || + systemd_timer_enable_unit(1, SCHEDULE_HOURLY, minute) || + systemd_timer_enable_unit(1, SCHEDULE_DAILY, minute) || + systemd_timer_enable_unit(1, SCHEDULE_WEEKLY, minute); + + if (ret) + systemd_timer_delete_units(); + else + systemd_timer_delete_stale_timer_templates(); + + return ret; +} + +static int systemd_timer_update_schedule(int run_maintenance, int fd UNUSED) +{ + if (run_maintenance) + return systemd_timer_setup_units(); + else + return systemd_timer_delete_units(); +} + +enum scheduler { + SCHEDULER_INVALID = -1, + SCHEDULER_AUTO, + SCHEDULER_CRON, + SCHEDULER_SYSTEMD, + SCHEDULER_LAUNCHCTL, + SCHEDULER_SCHTASKS, +}; + +static const struct { + const char *name; + int (*is_available)(void); + int (*update_schedule)(int run_maintenance, int fd); +} scheduler_fn[] = { + [SCHEDULER_CRON] = { + .name = "crontab", + .is_available = is_crontab_available, + .update_schedule = crontab_update_schedule, + }, + [SCHEDULER_SYSTEMD] = { + .name = "systemctl", + .is_available = is_systemd_timer_available, + .update_schedule = systemd_timer_update_schedule, + }, + [SCHEDULER_LAUNCHCTL] = { + .name = "launchctl", + .is_available = is_launchctl_available, + .update_schedule = launchctl_update_schedule, + }, + [SCHEDULER_SCHTASKS] = { + .name = "schtasks", + .is_available = is_schtasks_available, + .update_schedule = schtasks_update_schedule, + }, +}; + +static enum scheduler parse_scheduler(const char *value) +{ + if (!value) + return SCHEDULER_INVALID; + else if (!strcasecmp(value, "auto")) + return SCHEDULER_AUTO; + else if (!strcasecmp(value, "cron") || !strcasecmp(value, "crontab")) + return SCHEDULER_CRON; + else if (!strcasecmp(value, "systemd") || + !strcasecmp(value, "systemd-timer")) + return SCHEDULER_SYSTEMD; + else if (!strcasecmp(value, "launchctl")) + return SCHEDULER_LAUNCHCTL; + else if (!strcasecmp(value, "schtasks")) + return SCHEDULER_SCHTASKS; + else + return SCHEDULER_INVALID; +} + +static int maintenance_opt_scheduler(const struct option *opt, const char *arg, + int unset) +{ + enum scheduler *scheduler = opt->value; + + BUG_ON_OPT_NEG(unset); + + *scheduler = parse_scheduler(arg); + if (*scheduler == SCHEDULER_INVALID) + return error(_("unrecognized --scheduler argument '%s'"), arg); + return 0; +} + +struct maintenance_start_opts { + enum scheduler scheduler; +}; + +static enum scheduler resolve_scheduler(enum scheduler scheduler) +{ + if (scheduler != SCHEDULER_AUTO) + return scheduler; + +#if defined(__APPLE__) + return SCHEDULER_LAUNCHCTL; + +#elif defined(GIT_WINDOWS_NATIVE) + return SCHEDULER_SCHTASKS; + +#elif defined(__linux__) + if (is_systemd_timer_available()) + return SCHEDULER_SYSTEMD; + else if (is_crontab_available()) + return SCHEDULER_CRON; + else + die(_("neither systemd timers nor crontab are available")); + +#else + return SCHEDULER_CRON; +#endif +} + +static void validate_scheduler(enum scheduler scheduler) +{ + if (scheduler == SCHEDULER_INVALID) + BUG("invalid scheduler"); + if (scheduler == SCHEDULER_AUTO) + BUG("resolve_scheduler should have been called before"); + + if (!scheduler_fn[scheduler].is_available()) + die(_("%s scheduler is not available"), + scheduler_fn[scheduler].name); +} + +static int update_background_schedule(const struct maintenance_start_opts *opts, + int enable) +{ + unsigned int i; + int result = 0; + struct lock_file lk; + char *lock_path = xstrfmt("%s/schedule", the_repository->objects->odb->path); + + if (hold_lock_file_for_update(&lk, lock_path, LOCK_NO_DEREF) < 0) { + free(lock_path); + return error(_("another process is scheduling background maintenance")); + } + + for (i = 1; i < ARRAY_SIZE(scheduler_fn); i++) { + if (enable && opts->scheduler == i) + continue; + if (!scheduler_fn[i].is_available()) + continue; + scheduler_fn[i].update_schedule(0, get_lock_file_fd(&lk)); + } + + if (enable) + result = scheduler_fn[opts->scheduler].update_schedule( + 1, get_lock_file_fd(&lk)); + + rollback_lock_file(&lk); + + free(lock_path); + return result; +} + +static const char *const builtin_maintenance_start_usage[] = { + N_("git maintenance start [--scheduler=<scheduler>]"), + NULL +}; + +static int maintenance_start(int argc, const char **argv, const char *prefix) +{ + struct maintenance_start_opts opts = { 0 }; + struct option options[] = { + OPT_CALLBACK_F( + 0, "scheduler", &opts.scheduler, N_("scheduler"), + N_("scheduler to trigger git maintenance run"), + PARSE_OPT_NONEG, maintenance_opt_scheduler), + OPT_END() + }; + const char *register_args[] = { "register", NULL }; + + argc = parse_options(argc, argv, prefix, options, + builtin_maintenance_start_usage, 0); + if (argc) + usage_with_options(builtin_maintenance_start_usage, options); + + opts.scheduler = resolve_scheduler(opts.scheduler); + validate_scheduler(opts.scheduler); + + if (update_background_schedule(&opts, 1)) + die(_("failed to set up maintenance schedule")); + + if (maintenance_register(ARRAY_SIZE(register_args)-1, register_args, NULL)) + warning(_("failed to add repo to global config")); + return 0; +} + +static const char *const builtin_maintenance_stop_usage[] = { + "git maintenance stop", + NULL +}; + +static int maintenance_stop(int argc, const char **argv, const char *prefix) +{ + struct option options[] = { + OPT_END() + }; + argc = parse_options(argc, argv, prefix, options, + builtin_maintenance_stop_usage, 0); + if (argc) + usage_with_options(builtin_maintenance_stop_usage, options); + return update_background_schedule(NULL, 0); +} + +static const char * const builtin_maintenance_usage[] = { + N_("git maintenance <subcommand> [<options>]"), + NULL, +}; + +int cmd_maintenance(int argc, const char **argv, const char *prefix) +{ + parse_opt_subcommand_fn *fn = NULL; + struct option builtin_maintenance_options[] = { + OPT_SUBCOMMAND("run", &fn, maintenance_run), + OPT_SUBCOMMAND("start", &fn, maintenance_start), + OPT_SUBCOMMAND("stop", &fn, maintenance_stop), + OPT_SUBCOMMAND("register", &fn, maintenance_register), + OPT_SUBCOMMAND("unregister", &fn, maintenance_unregister), + OPT_END(), + }; + + argc = parse_options(argc, argv, prefix, builtin_maintenance_options, + builtin_maintenance_usage, 0); + return fn(argc, argv, prefix); +} diff --git a/builtin/get-tar-commit-id.c b/builtin/get-tar-commit-id.c new file mode 100644 index 0000000..20d0dfe --- /dev/null +++ b/builtin/get-tar-commit-id.c @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2005, 2006 Rene Scharfe + */ +#include "builtin.h" +#include "commit.h" +#include "tar.h" +#include "quote.h" + +static const char builtin_get_tar_commit_id_usage[] = +"git get-tar-commit-id"; + +/* ustar header + extended global header content */ +#define RECORDSIZE (512) +#define HEADERSIZE (2 * RECORDSIZE) + +int cmd_get_tar_commit_id(int argc, const char **argv UNUSED, const char *prefix) +{ + char buffer[HEADERSIZE]; + struct ustar_header *header = (struct ustar_header *)buffer; + char *content = buffer + RECORDSIZE; + const char *comment; + ssize_t n; + long len; + char *end; + + BUG_ON_NON_EMPTY_PREFIX(prefix); + + if (argc != 1) + usage(builtin_get_tar_commit_id_usage); + + n = read_in_full(0, buffer, HEADERSIZE); + if (n < 0) + die_errno("git get-tar-commit-id: read error"); + if (n != HEADERSIZE) + die_errno("git get-tar-commit-id: EOF before reading tar header"); + if (header->typeflag[0] != TYPEFLAG_GLOBAL_HEADER) + return 1; + + len = strtol(content, &end, 10); + if (errno == ERANGE || end == content || len < 0) + return 1; + if (!skip_prefix(end, " comment=", &comment)) + return 1; + len -= comment - content; + if (len < 1 || !(len % 2) || + hash_algo_by_length((len - 1) / 2) == GIT_HASH_UNKNOWN) + return 1; + + if (write_in_full(1, comment, len) < 0) + die_errno("git get-tar-commit-id: write error"); + + return 0; +} diff --git a/builtin/grep.c b/builtin/grep.c new file mode 100644 index 0000000..fe78d4c --- /dev/null +++ b/builtin/grep.c @@ -0,0 +1,1274 @@ +/* + * Builtin "git grep" + * + * Copyright (c) 2006 Junio C Hamano + */ +#include "builtin.h" +#include "abspath.h" +#include "gettext.h" +#include "hex.h" +#include "repository.h" +#include "config.h" +#include "blob.h" +#include "tree.h" +#include "commit.h" +#include "tag.h" +#include "tree-walk.h" +#include "parse-options.h" +#include "string-list.h" +#include "run-command.h" +#include "userdiff.h" +#include "grep.h" +#include "quote.h" +#include "dir.h" +#include "pathspec.h" +#include "setup.h" +#include "submodule.h" +#include "submodule-config.h" +#include "object-file.h" +#include "object-name.h" +#include "object-store-ll.h" +#include "packfile.h" +#include "pager.h" +#include "path.h" +#include "read-cache-ll.h" +#include "write-or-die.h" + +static const char *grep_prefix; + +static char const * const grep_usage[] = { + N_("git grep [<options>] [-e] <pattern> [<rev>...] [[--] <path>...]"), + NULL +}; + +static int recurse_submodules; + +static int num_threads; + +static pthread_t *threads; + +/* We use one producer thread and THREADS consumer + * threads. The producer adds struct work_items to 'todo' and the + * consumers pick work items from the same array. + */ +struct work_item { + struct grep_source source; + char done; + struct strbuf out; +}; + +/* In the range [todo_done, todo_start) in 'todo' we have work_items + * that have been or are processed by a consumer thread. We haven't + * written the result for these to stdout yet. + * + * The work_items in [todo_start, todo_end) are waiting to be picked + * up by a consumer thread. + * + * The ranges are modulo TODO_SIZE. + */ +#define TODO_SIZE 128 +static struct work_item todo[TODO_SIZE]; +static int todo_start; +static int todo_end; +static int todo_done; + +/* Has all work items been added? */ +static int all_work_added; + +static struct repository **repos_to_free; +static size_t repos_to_free_nr, repos_to_free_alloc; + +/* This lock protects all the variables above. */ +static pthread_mutex_t grep_mutex; + +static inline void grep_lock(void) +{ + pthread_mutex_lock(&grep_mutex); +} + +static inline void grep_unlock(void) +{ + pthread_mutex_unlock(&grep_mutex); +} + +/* Signalled when a new work_item is added to todo. */ +static pthread_cond_t cond_add; + +/* Signalled when the result from one work_item is written to + * stdout. + */ +static pthread_cond_t cond_write; + +/* Signalled when we are finished with everything. */ +static pthread_cond_t cond_result; + +static int skip_first_line; + +static void add_work(struct grep_opt *opt, struct grep_source *gs) +{ + if (opt->binary != GREP_BINARY_TEXT) + grep_source_load_driver(gs, opt->repo->index); + + grep_lock(); + + while ((todo_end+1) % ARRAY_SIZE(todo) == todo_done) { + pthread_cond_wait(&cond_write, &grep_mutex); + } + + todo[todo_end].source = *gs; + todo[todo_end].done = 0; + strbuf_reset(&todo[todo_end].out); + todo_end = (todo_end + 1) % ARRAY_SIZE(todo); + + pthread_cond_signal(&cond_add); + grep_unlock(); +} + +static struct work_item *get_work(void) +{ + struct work_item *ret; + + grep_lock(); + while (todo_start == todo_end && !all_work_added) { + pthread_cond_wait(&cond_add, &grep_mutex); + } + + if (todo_start == todo_end && all_work_added) { + ret = NULL; + } else { + ret = &todo[todo_start]; + todo_start = (todo_start + 1) % ARRAY_SIZE(todo); + } + grep_unlock(); + return ret; +} + +static void work_done(struct work_item *w) +{ + int old_done; + + grep_lock(); + w->done = 1; + old_done = todo_done; + for(; todo[todo_done].done && todo_done != todo_start; + todo_done = (todo_done+1) % ARRAY_SIZE(todo)) { + w = &todo[todo_done]; + if (w->out.len) { + const char *p = w->out.buf; + size_t len = w->out.len; + + /* Skip the leading hunk mark of the first file. */ + if (skip_first_line) { + while (len) { + len--; + if (*p++ == '\n') + break; + } + skip_first_line = 0; + } + + write_or_die(1, p, len); + } + grep_source_clear(&w->source); + } + + if (old_done != todo_done) + pthread_cond_signal(&cond_write); + + if (all_work_added && todo_done == todo_end) + pthread_cond_signal(&cond_result); + + grep_unlock(); +} + +static void free_repos(void) +{ + int i; + + for (i = 0; i < repos_to_free_nr; i++) { + repo_clear(repos_to_free[i]); + free(repos_to_free[i]); + } + FREE_AND_NULL(repos_to_free); + repos_to_free_nr = 0; + repos_to_free_alloc = 0; +} + +static void *run(void *arg) +{ + int hit = 0; + struct grep_opt *opt = arg; + + while (1) { + struct work_item *w = get_work(); + if (!w) + break; + + opt->output_priv = w; + hit |= grep_source(opt, &w->source); + grep_source_clear_data(&w->source); + work_done(w); + } + free_grep_patterns(opt); + free(opt); + + return (void*) (intptr_t) hit; +} + +static void strbuf_out(struct grep_opt *opt, const void *buf, size_t size) +{ + struct work_item *w = opt->output_priv; + strbuf_add(&w->out, buf, size); +} + +static void start_threads(struct grep_opt *opt) +{ + int i; + + pthread_mutex_init(&grep_mutex, NULL); + pthread_mutex_init(&grep_attr_mutex, NULL); + pthread_cond_init(&cond_add, NULL); + pthread_cond_init(&cond_write, NULL); + pthread_cond_init(&cond_result, NULL); + grep_use_locks = 1; + enable_obj_read_lock(); + + for (i = 0; i < ARRAY_SIZE(todo); i++) { + strbuf_init(&todo[i].out, 0); + } + + CALLOC_ARRAY(threads, num_threads); + for (i = 0; i < num_threads; i++) { + int err; + struct grep_opt *o = grep_opt_dup(opt); + o->output = strbuf_out; + compile_grep_patterns(o); + err = pthread_create(&threads[i], NULL, run, o); + + if (err) + die(_("grep: failed to create thread: %s"), + strerror(err)); + } +} + +static int wait_all(void) +{ + int hit = 0; + int i; + + if (!HAVE_THREADS) + BUG("Never call this function unless you have started threads"); + + grep_lock(); + all_work_added = 1; + + /* Wait until all work is done. */ + while (todo_done != todo_end) + pthread_cond_wait(&cond_result, &grep_mutex); + + /* Wake up all the consumer threads so they can see that there + * is no more work to do. + */ + pthread_cond_broadcast(&cond_add); + grep_unlock(); + + for (i = 0; i < num_threads; i++) { + void *h; + pthread_join(threads[i], &h); + hit |= (int) (intptr_t) h; + } + + free(threads); + + pthread_mutex_destroy(&grep_mutex); + pthread_mutex_destroy(&grep_attr_mutex); + pthread_cond_destroy(&cond_add); + pthread_cond_destroy(&cond_write); + pthread_cond_destroy(&cond_result); + grep_use_locks = 0; + disable_obj_read_lock(); + + return hit; +} + +static int grep_cmd_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + int st = grep_config(var, value, ctx, cb); + + if (git_color_config(var, value, cb) < 0) + st = -1; + else if (git_default_config(var, value, ctx, cb) < 0) + st = -1; + + if (!strcmp(var, "grep.threads")) { + num_threads = git_config_int(var, value, ctx->kvi); + if (num_threads < 0) + die(_("invalid number of threads specified (%d) for %s"), + num_threads, var); + else if (!HAVE_THREADS && num_threads > 1) { + /* + * TRANSLATORS: %s is the configuration + * variable for tweaking threads, currently + * grep.threads + */ + warning(_("no threads support, ignoring %s"), var); + num_threads = 1; + } + } + + if (!strcmp(var, "submodule.recurse")) + recurse_submodules = git_config_bool(var, value); + + return st; +} + +static void grep_source_name(struct grep_opt *opt, const char *filename, + int tree_name_len, struct strbuf *out) +{ + strbuf_reset(out); + + if (opt->null_following_name) { + if (opt->relative && grep_prefix) { + struct strbuf rel_buf = STRBUF_INIT; + const char *rel_name = + relative_path(filename + tree_name_len, + grep_prefix, &rel_buf); + + if (tree_name_len) + strbuf_add(out, filename, tree_name_len); + + strbuf_addstr(out, rel_name); + strbuf_release(&rel_buf); + } else { + strbuf_addstr(out, filename); + } + return; + } + + if (opt->relative && grep_prefix) + quote_path(filename + tree_name_len, grep_prefix, out, 0); + else + quote_c_style(filename + tree_name_len, out, NULL, 0); + + if (tree_name_len) + strbuf_insert(out, 0, filename, tree_name_len); +} + +static int grep_oid(struct grep_opt *opt, const struct object_id *oid, + const char *filename, int tree_name_len, + const char *path) +{ + struct strbuf pathbuf = STRBUF_INIT; + struct grep_source gs; + + grep_source_name(opt, filename, tree_name_len, &pathbuf); + grep_source_init_oid(&gs, pathbuf.buf, path, oid, opt->repo); + strbuf_release(&pathbuf); + + if (num_threads > 1) { + /* + * add_work() copies gs and thus assumes ownership of + * its fields, so do not call grep_source_clear() + */ + add_work(opt, &gs); + return 0; + } else { + int hit; + + hit = grep_source(opt, &gs); + + grep_source_clear(&gs); + return hit; + } +} + +static int grep_file(struct grep_opt *opt, const char *filename) +{ + struct strbuf buf = STRBUF_INIT; + struct grep_source gs; + + grep_source_name(opt, filename, 0, &buf); + grep_source_init_file(&gs, buf.buf, filename); + strbuf_release(&buf); + + if (num_threads > 1) { + /* + * add_work() copies gs and thus assumes ownership of + * its fields, so do not call grep_source_clear() + */ + add_work(opt, &gs); + return 0; + } else { + int hit; + + hit = grep_source(opt, &gs); + + grep_source_clear(&gs); + return hit; + } +} + +static void append_path(struct grep_opt *opt, const void *data, size_t len) +{ + struct string_list *path_list = opt->output_priv; + + if (len == 1 && *(const char *)data == '\0') + return; + string_list_append_nodup(path_list, xstrndup(data, len)); +} + +static void run_pager(struct grep_opt *opt, const char *prefix) +{ + struct string_list *path_list = opt->output_priv; + struct child_process child = CHILD_PROCESS_INIT; + int i, status; + + for (i = 0; i < path_list->nr; i++) + strvec_push(&child.args, path_list->items[i].string); + child.dir = prefix; + child.use_shell = 1; + + status = run_command(&child); + if (status) + exit(status); +} + +static int grep_cache(struct grep_opt *opt, + const struct pathspec *pathspec, int cached); +static int grep_tree(struct grep_opt *opt, const struct pathspec *pathspec, + struct tree_desc *tree, struct strbuf *base, int tn_len, + int check_attr); + +static int grep_submodule(struct grep_opt *opt, + const struct pathspec *pathspec, + const struct object_id *oid, + const char *filename, const char *path, int cached) +{ + struct repository *subrepo; + struct repository *superproject = opt->repo; + struct grep_opt subopt; + int hit = 0; + + if (!is_submodule_active(superproject, path)) + return 0; + + subrepo = xmalloc(sizeof(*subrepo)); + if (repo_submodule_init(subrepo, superproject, path, null_oid())) { + free(subrepo); + return 0; + } + ALLOC_GROW(repos_to_free, repos_to_free_nr + 1, repos_to_free_alloc); + repos_to_free[repos_to_free_nr++] = subrepo; + + /* + * NEEDSWORK: repo_read_gitmodules() might call + * add_to_alternates_memory() via config_from_gitmodules(). This + * operation causes a race condition with concurrent object readings + * performed by the worker threads. That's why we need obj_read_lock() + * here. It should be removed once it's no longer necessary to add the + * subrepo's odbs to the in-memory alternates list. + */ + obj_read_lock(); + + /* + * NEEDSWORK: when reading a submodule, the sparsity settings in the + * superproject are incorrectly forgotten or misused. For example: + * + * 1. "command_requires_full_index" + * When this setting is turned on for `grep`, only the superproject + * knows it. All the submodules are read with their own configs + * and get prepare_repo_settings()'d. Therefore, these submodules + * "forget" the sparse-index feature switch. As a result, the index + * of these submodules are expanded unexpectedly. + * + * 2. "core_apply_sparse_checkout" + * When running `grep` in the superproject, this setting is + * populated using the superproject's configs. However, once + * initialized, this config is globally accessible and is read by + * prepare_repo_settings() for the submodules. For instance, if a + * submodule is using a sparse-checkout, however, the superproject + * is not, the result is that the config from the superproject will + * dictate the behavior for the submodule, making it "forget" its + * sparse-checkout state. + * + * 3. "core_sparse_checkout_cone" + * ditto. + * + * Note that this list is not exhaustive. + */ + repo_read_gitmodules(subrepo, 0); + + /* + * All code paths tested by test code no longer need submodule ODBs to + * be added as alternates, but add it to the list just in case. + * Submodule ODBs added through add_submodule_odb_by_path() will be + * lazily registered as alternates when needed (and except in an + * unexpected code interaction, it won't be needed). + */ + add_submodule_odb_by_path(subrepo->objects->odb->path); + obj_read_unlock(); + + memcpy(&subopt, opt, sizeof(subopt)); + subopt.repo = subrepo; + + if (oid) { + enum object_type object_type; + struct tree_desc tree; + void *data; + unsigned long size; + struct strbuf base = STRBUF_INIT; + + obj_read_lock(); + object_type = oid_object_info(subrepo, oid, NULL); + obj_read_unlock(); + data = read_object_with_reference(subrepo, + oid, OBJ_TREE, + &size, NULL); + if (!data) + die(_("unable to read tree (%s)"), oid_to_hex(oid)); + + strbuf_addstr(&base, filename); + strbuf_addch(&base, '/'); + + init_tree_desc(&tree, data, size); + hit = grep_tree(&subopt, pathspec, &tree, &base, base.len, + object_type == OBJ_COMMIT); + strbuf_release(&base); + free(data); + } else { + hit = grep_cache(&subopt, pathspec, cached); + } + + return hit; +} + +static int grep_cache(struct grep_opt *opt, + const struct pathspec *pathspec, int cached) +{ + struct repository *repo = opt->repo; + int hit = 0; + int nr; + struct strbuf name = STRBUF_INIT; + int name_base_len = 0; + if (repo->submodule_prefix) { + name_base_len = strlen(repo->submodule_prefix); + strbuf_addstr(&name, repo->submodule_prefix); + } + + if (repo_read_index(repo) < 0) + die(_("index file corrupt")); + + for (nr = 0; nr < repo->index->cache_nr; nr++) { + const struct cache_entry *ce = repo->index->cache[nr]; + + if (!cached && ce_skip_worktree(ce)) + continue; + + strbuf_setlen(&name, name_base_len); + strbuf_addstr(&name, ce->name); + if (S_ISSPARSEDIR(ce->ce_mode)) { + enum object_type type; + struct tree_desc tree; + void *data; + unsigned long size; + + data = repo_read_object_file(the_repository, &ce->oid, + &type, &size); + init_tree_desc(&tree, data, size); + + hit |= grep_tree(opt, pathspec, &tree, &name, 0, 0); + strbuf_setlen(&name, name_base_len); + strbuf_addstr(&name, ce->name); + free(data); + } else if (S_ISREG(ce->ce_mode) && + match_pathspec(repo->index, pathspec, name.buf, name.len, 0, NULL, + S_ISDIR(ce->ce_mode) || + S_ISGITLINK(ce->ce_mode))) { + /* + * If CE_VALID is on, we assume worktree file and its + * cache entry are identical, even if worktree file has + * been modified, so use cache version instead + */ + if (cached || (ce->ce_flags & CE_VALID)) { + if (ce_stage(ce) || ce_intent_to_add(ce)) + continue; + hit |= grep_oid(opt, &ce->oid, name.buf, + 0, name.buf); + } else { + hit |= grep_file(opt, name.buf); + } + } else if (recurse_submodules && S_ISGITLINK(ce->ce_mode) && + submodule_path_match(repo->index, pathspec, name.buf, NULL)) { + hit |= grep_submodule(opt, pathspec, NULL, ce->name, + ce->name, cached); + } else { + continue; + } + + if (ce_stage(ce)) { + do { + nr++; + } while (nr < repo->index->cache_nr && + !strcmp(ce->name, repo->index->cache[nr]->name)); + nr--; /* compensate for loop control */ + } + if (hit && opt->status_only) + break; + } + + strbuf_release(&name); + return hit; +} + +static int grep_tree(struct grep_opt *opt, const struct pathspec *pathspec, + struct tree_desc *tree, struct strbuf *base, int tn_len, + int check_attr) +{ + struct repository *repo = opt->repo; + int hit = 0; + enum interesting match = entry_not_interesting; + struct name_entry entry; + int old_baselen = base->len; + struct strbuf name = STRBUF_INIT; + int name_base_len = 0; + if (repo->submodule_prefix) { + strbuf_addstr(&name, repo->submodule_prefix); + name_base_len = name.len; + } + + while (tree_entry(tree, &entry)) { + int te_len = tree_entry_len(&entry); + + if (match != all_entries_interesting) { + strbuf_addstr(&name, base->buf + tn_len); + match = tree_entry_interesting(repo->index, + &entry, &name, + pathspec); + strbuf_setlen(&name, name_base_len); + + if (match == all_entries_not_interesting) + break; + if (match == entry_not_interesting) + continue; + } + + strbuf_add(base, entry.path, te_len); + + if (S_ISREG(entry.mode)) { + hit |= grep_oid(opt, &entry.oid, base->buf, tn_len, + check_attr ? base->buf + tn_len : NULL); + } else if (S_ISDIR(entry.mode)) { + enum object_type type; + struct tree_desc sub; + void *data; + unsigned long size; + + data = repo_read_object_file(the_repository, + &entry.oid, &type, &size); + if (!data) + die(_("unable to read tree (%s)"), + oid_to_hex(&entry.oid)); + + strbuf_addch(base, '/'); + init_tree_desc(&sub, data, size); + hit |= grep_tree(opt, pathspec, &sub, base, tn_len, + check_attr); + free(data); + } else if (recurse_submodules && S_ISGITLINK(entry.mode)) { + hit |= grep_submodule(opt, pathspec, &entry.oid, + base->buf, base->buf + tn_len, + 1); /* ignored */ + } + + strbuf_setlen(base, old_baselen); + + if (hit && opt->status_only) + break; + } + + strbuf_release(&name); + return hit; +} + +static int grep_object(struct grep_opt *opt, const struct pathspec *pathspec, + struct object *obj, const char *name, const char *path) +{ + if (obj->type == OBJ_BLOB) + return grep_oid(opt, &obj->oid, name, 0, path); + if (obj->type == OBJ_COMMIT || obj->type == OBJ_TREE) { + struct tree_desc tree; + void *data; + unsigned long size; + struct strbuf base; + int hit, len; + + data = read_object_with_reference(opt->repo, + &obj->oid, OBJ_TREE, + &size, NULL); + if (!data) + die(_("unable to read tree (%s)"), oid_to_hex(&obj->oid)); + + len = name ? strlen(name) : 0; + strbuf_init(&base, PATH_MAX + len + 1); + if (len) { + strbuf_add(&base, name, len); + strbuf_addch(&base, ':'); + } + init_tree_desc(&tree, data, size); + hit = grep_tree(opt, pathspec, &tree, &base, base.len, + obj->type == OBJ_COMMIT); + strbuf_release(&base); + free(data); + return hit; + } + die(_("unable to grep from object of type %s"), type_name(obj->type)); +} + +static int grep_objects(struct grep_opt *opt, const struct pathspec *pathspec, + const struct object_array *list) +{ + unsigned int i; + int hit = 0; + const unsigned int nr = list->nr; + + for (i = 0; i < nr; i++) { + struct object *real_obj; + + obj_read_lock(); + real_obj = deref_tag(opt->repo, list->objects[i].item, + NULL, 0); + obj_read_unlock(); + + if (!real_obj) { + char hex[GIT_MAX_HEXSZ + 1]; + const char *name = list->objects[i].name; + + if (!name) { + oid_to_hex_r(hex, &list->objects[i].item->oid); + name = hex; + } + die(_("invalid object '%s' given."), name); + } + + /* load the gitmodules file for this rev */ + if (recurse_submodules) { + submodule_free(opt->repo); + obj_read_lock(); + gitmodules_config_oid(&real_obj->oid); + obj_read_unlock(); + } + if (grep_object(opt, pathspec, real_obj, list->objects[i].name, + list->objects[i].path)) { + hit = 1; + if (opt->status_only) + break; + } + } + return hit; +} + +static int grep_directory(struct grep_opt *opt, const struct pathspec *pathspec, + int exc_std, int use_index) +{ + struct dir_struct dir = DIR_INIT; + int i, hit = 0; + + if (!use_index) + dir.flags |= DIR_NO_GITLINKS; + if (exc_std) + setup_standard_excludes(&dir); + + fill_directory(&dir, opt->repo->index, pathspec); + for (i = 0; i < dir.nr; i++) { + hit |= grep_file(opt, dir.entries[i]->name); + if (hit && opt->status_only) + break; + } + dir_clear(&dir); + return hit; +} + +static int context_callback(const struct option *opt, const char *arg, + int unset) +{ + struct grep_opt *grep_opt = opt->value; + int value; + const char *endp; + + if (unset) { + grep_opt->pre_context = grep_opt->post_context = 0; + return 0; + } + value = strtol(arg, (char **)&endp, 10); + if (*endp) { + return error(_("switch `%c' expects a numerical value"), + opt->short_name); + } + grep_opt->pre_context = grep_opt->post_context = value; + return 0; +} + +static int file_callback(const struct option *opt, const char *arg, int unset) +{ + struct grep_opt *grep_opt = opt->value; + int from_stdin; + const char *filename = arg; + FILE *patterns; + int lno = 0; + struct strbuf sb = STRBUF_INIT; + + BUG_ON_OPT_NEG(unset); + + if (!*filename) + ; /* leave it as-is */ + else + filename = prefix_filename_except_for_dash(grep_prefix, filename); + + from_stdin = !strcmp(filename, "-"); + patterns = from_stdin ? stdin : fopen(filename, "r"); + if (!patterns) + die_errno(_("cannot open '%s'"), arg); + while (strbuf_getline(&sb, patterns) == 0) { + /* ignore empty line like grep does */ + if (sb.len == 0) + continue; + + append_grep_pat(grep_opt, sb.buf, sb.len, arg, ++lno, + GREP_PATTERN); + } + if (!from_stdin) + fclose(patterns); + strbuf_release(&sb); + if (filename != arg) + free((void *)filename); + return 0; +} + +static int not_callback(const struct option *opt, const char *arg, int unset) +{ + struct grep_opt *grep_opt = opt->value; + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + append_grep_pattern(grep_opt, "--not", "command line", 0, GREP_NOT); + return 0; +} + +static int and_callback(const struct option *opt, const char *arg, int unset) +{ + struct grep_opt *grep_opt = opt->value; + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + append_grep_pattern(grep_opt, "--and", "command line", 0, GREP_AND); + return 0; +} + +static int open_callback(const struct option *opt, const char *arg, int unset) +{ + struct grep_opt *grep_opt = opt->value; + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + append_grep_pattern(grep_opt, "(", "command line", 0, GREP_OPEN_PAREN); + return 0; +} + +static int close_callback(const struct option *opt, const char *arg, int unset) +{ + struct grep_opt *grep_opt = opt->value; + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + append_grep_pattern(grep_opt, ")", "command line", 0, GREP_CLOSE_PAREN); + return 0; +} + +static int pattern_callback(const struct option *opt, const char *arg, + int unset) +{ + struct grep_opt *grep_opt = opt->value; + BUG_ON_OPT_NEG(unset); + append_grep_pattern(grep_opt, arg, "-e option", 0, GREP_PATTERN); + return 0; +} + +int cmd_grep(int argc, const char **argv, const char *prefix) +{ + int hit = 0; + int cached = 0, untracked = 0, opt_exclude = -1; + int seen_dashdash = 0; + int external_grep_allowed__ignored; + const char *show_in_pager = NULL, *default_pager = "dummy"; + struct grep_opt opt; + struct object_array list = OBJECT_ARRAY_INIT; + struct pathspec pathspec; + struct string_list path_list = STRING_LIST_INIT_DUP; + int i; + int dummy; + int use_index = 1; + int allow_revs; + + struct option options[] = { + OPT_BOOL(0, "cached", &cached, + N_("search in index instead of in the work tree")), + OPT_NEGBIT(0, "no-index", &use_index, + N_("find in contents not managed by git"), 1), + OPT_BOOL(0, "untracked", &untracked, + N_("search in both tracked and untracked files")), + OPT_SET_INT(0, "exclude-standard", &opt_exclude, + N_("ignore files specified via '.gitignore'"), 1), + OPT_BOOL(0, "recurse-submodules", &recurse_submodules, + N_("recursively search in each submodule")), + OPT_GROUP(""), + OPT_BOOL('v', "invert-match", &opt.invert, + N_("show non-matching lines")), + OPT_BOOL('i', "ignore-case", &opt.ignore_case, + N_("case insensitive matching")), + OPT_BOOL('w', "word-regexp", &opt.word_regexp, + N_("match patterns only at word boundaries")), + OPT_SET_INT('a', "text", &opt.binary, + N_("process binary files as text"), GREP_BINARY_TEXT), + OPT_SET_INT('I', NULL, &opt.binary, + N_("don't match patterns in binary files"), + GREP_BINARY_NOMATCH), + OPT_BOOL(0, "textconv", &opt.allow_textconv, + N_("process binary files with textconv filters")), + OPT_SET_INT('r', "recursive", &opt.max_depth, + N_("search in subdirectories (default)"), -1), + OPT_INTEGER_F(0, "max-depth", &opt.max_depth, + N_("descend at most <n> levels"), PARSE_OPT_NONEG), + OPT_GROUP(""), + OPT_SET_INT('E', "extended-regexp", &opt.pattern_type_option, + N_("use extended POSIX regular expressions"), + GREP_PATTERN_TYPE_ERE), + OPT_SET_INT('G', "basic-regexp", &opt.pattern_type_option, + N_("use basic POSIX regular expressions (default)"), + GREP_PATTERN_TYPE_BRE), + OPT_SET_INT('F', "fixed-strings", &opt.pattern_type_option, + N_("interpret patterns as fixed strings"), + GREP_PATTERN_TYPE_FIXED), + OPT_SET_INT('P', "perl-regexp", &opt.pattern_type_option, + N_("use Perl-compatible regular expressions"), + GREP_PATTERN_TYPE_PCRE), + OPT_GROUP(""), + OPT_BOOL('n', "line-number", &opt.linenum, N_("show line numbers")), + OPT_BOOL(0, "column", &opt.columnnum, N_("show column number of first match")), + OPT_NEGBIT('h', NULL, &opt.pathname, N_("don't show filenames"), 1), + OPT_BIT('H', NULL, &opt.pathname, N_("show filenames"), 1), + OPT_NEGBIT(0, "full-name", &opt.relative, + N_("show filenames relative to top directory"), 1), + OPT_BOOL('l', "files-with-matches", &opt.name_only, + N_("show only filenames instead of matching lines")), + OPT_BOOL(0, "name-only", &opt.name_only, + N_("synonym for --files-with-matches")), + OPT_BOOL('L', "files-without-match", + &opt.unmatch_name_only, + N_("show only the names of files without match")), + OPT_BOOL_F('z', "null", &opt.null_following_name, + N_("print NUL after filenames"), + PARSE_OPT_NOCOMPLETE), + OPT_BOOL('o', "only-matching", &opt.only_matching, + N_("show only matching parts of a line")), + OPT_BOOL('c', "count", &opt.count, + N_("show the number of matches instead of matching lines")), + OPT__COLOR(&opt.color, N_("highlight matches")), + OPT_BOOL(0, "break", &opt.file_break, + N_("print empty line between matches from different files")), + OPT_BOOL(0, "heading", &opt.heading, + N_("show filename only once above matches from same file")), + OPT_GROUP(""), + OPT_CALLBACK('C', "context", &opt, N_("n"), + N_("show <n> context lines before and after matches"), + context_callback), + OPT_INTEGER('B', "before-context", &opt.pre_context, + N_("show <n> context lines before matches")), + OPT_INTEGER('A', "after-context", &opt.post_context, + N_("show <n> context lines after matches")), + OPT_INTEGER(0, "threads", &num_threads, + N_("use <n> worker threads")), + OPT_NUMBER_CALLBACK(&opt, N_("shortcut for -C NUM"), + context_callback), + OPT_BOOL('p', "show-function", &opt.funcname, + N_("show a line with the function name before matches")), + OPT_BOOL('W', "function-context", &opt.funcbody, + N_("show the surrounding function")), + OPT_GROUP(""), + OPT_CALLBACK('f', NULL, &opt, N_("file"), + N_("read patterns from file"), file_callback), + OPT_CALLBACK_F('e', NULL, &opt, N_("pattern"), + N_("match <pattern>"), PARSE_OPT_NONEG, pattern_callback), + OPT_CALLBACK_F(0, "and", &opt, NULL, + N_("combine patterns specified with -e"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, and_callback), + OPT_BOOL_F(0, "or", &dummy, "", PARSE_OPT_NONEG), + OPT_CALLBACK_F(0, "not", &opt, NULL, "", + PARSE_OPT_NOARG | PARSE_OPT_NONEG, not_callback), + OPT_CALLBACK_F('(', NULL, &opt, NULL, "", + PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_NODASH, + open_callback), + OPT_CALLBACK_F(')', NULL, &opt, NULL, "", + PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_NODASH, + close_callback), + OPT__QUIET(&opt.status_only, + N_("indicate hit with exit status without output")), + OPT_BOOL(0, "all-match", &opt.all_match, + N_("show only matches from files that match all patterns")), + OPT_GROUP(""), + { OPTION_STRING, 'O', "open-files-in-pager", &show_in_pager, + N_("pager"), N_("show matching files in the pager"), + PARSE_OPT_OPTARG | PARSE_OPT_NOCOMPLETE, + NULL, (intptr_t)default_pager }, + OPT_BOOL_F(0, "ext-grep", &external_grep_allowed__ignored, + N_("allow calling of grep(1) (ignored by this build)"), + PARSE_OPT_NOCOMPLETE), + OPT_INTEGER('m', "max-count", &opt.max_count, + N_("maximum number of results per file")), + OPT_END() + }; + grep_prefix = prefix; + + grep_init(&opt, the_repository); + git_config(grep_cmd_config, &opt); + + /* + * If there is no -- then the paths must exist in the working + * tree. If there is no explicit pattern specified with -e or + * -f, we take the first unrecognized non option to be the + * pattern, but then what follows it must be zero or more + * valid refs up to the -- (if exists), and then existing + * paths. If there is an explicit pattern, then the first + * unrecognized non option is the beginning of the refs list + * that continues up to the -- (if exists), and then paths. + */ + argc = parse_options(argc, argv, prefix, options, grep_usage, + PARSE_OPT_KEEP_DASHDASH | + PARSE_OPT_STOP_AT_NON_OPTION); + + if (the_repository->gitdir) { + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + } + + if (use_index && !startup_info->have_repository) { + int fallback = 0; + git_config_get_bool("grep.fallbacktonoindex", &fallback); + if (fallback) + use_index = 0; + else + /* die the same way as if we did it at the beginning */ + setup_git_directory(); + } + /* Ignore --recurse-submodules if --no-index is given or implied */ + if (!use_index) + recurse_submodules = 0; + + /* + * skip a -- separator; we know it cannot be + * separating revisions from pathnames if + * we haven't even had any patterns yet + */ + if (argc > 0 && !opt.pattern_list && !strcmp(argv[0], "--")) { + argv++; + argc--; + } + + /* First unrecognized non-option token */ + if (argc > 0 && !opt.pattern_list) { + append_grep_pattern(&opt, argv[0], "command line", 0, + GREP_PATTERN); + argv++; + argc--; + } + + if (show_in_pager == default_pager) + show_in_pager = git_pager(1); + if (show_in_pager) { + opt.color = 0; + opt.name_only = 1; + opt.null_following_name = 1; + opt.output_priv = &path_list; + opt.output = append_path; + string_list_append(&path_list, show_in_pager); + } + + if (!opt.pattern_list) + die(_("no pattern given")); + + /* --only-matching has no effect with --invert. */ + if (opt.invert) + opt.only_matching = 0; + + /* + * We have to find "--" in a separate pass, because its presence + * influences how we will parse arguments that come before it. + */ + for (i = 0; i < argc; i++) { + if (!strcmp(argv[i], "--")) { + seen_dashdash = 1; + break; + } + } + + /* + * Resolve any rev arguments. If we have a dashdash, then everything up + * to it must resolve as a rev. If not, then we stop at the first + * non-rev and assume everything else is a path. + */ + allow_revs = use_index && !untracked; + for (i = 0; i < argc; i++) { + const char *arg = argv[i]; + struct object_id oid; + struct object_context oc; + struct object *object; + + if (!strcmp(arg, "--")) { + i++; + break; + } + + if (!allow_revs) { + if (seen_dashdash) + die(_("--no-index or --untracked cannot be used with revs")); + break; + } + + if (get_oid_with_context(the_repository, arg, + GET_OID_RECORD_PATH, + &oid, &oc)) { + if (seen_dashdash) + die(_("unable to resolve revision: %s"), arg); + break; + } + + object = parse_object_or_die(&oid, arg); + if (!seen_dashdash) + verify_non_filename(prefix, arg); + add_object_array_with_path(object, arg, &list, oc.mode, oc.path); + free(oc.path); + } + + /* + * Anything left over is presumed to be a path. But in the non-dashdash + * "do what I mean" case, we verify and complain when that isn't true. + */ + if (!seen_dashdash) { + int j; + for (j = i; j < argc; j++) + verify_filename(prefix, argv[j], j == i && allow_revs); + } + + parse_pathspec(&pathspec, 0, + PATHSPEC_PREFER_CWD | + (opt.max_depth != -1 ? PATHSPEC_MAXDEPTH_VALID : 0), + prefix, argv + i); + pathspec.max_depth = opt.max_depth; + pathspec.recursive = 1; + pathspec.recurse_submodules = !!recurse_submodules; + + if (recurse_submodules && untracked) + die(_("--untracked not supported with --recurse-submodules")); + + /* + * Optimize out the case where the amount of matches is limited to zero. + * We do this to keep results consistent with GNU grep(1). + */ + if (opt.max_count == 0) + return 1; + + if (show_in_pager) { + if (num_threads > 1) + warning(_("invalid option combination, ignoring --threads")); + num_threads = 1; + } else if (!HAVE_THREADS && num_threads > 1) { + warning(_("no threads support, ignoring --threads")); + num_threads = 1; + } else if (num_threads < 0) + die(_("invalid number of threads specified (%d)"), num_threads); + else if (num_threads == 0) + num_threads = HAVE_THREADS ? online_cpus() : 1; + + if (num_threads > 1) { + if (!HAVE_THREADS) + BUG("Somebody got num_threads calculation wrong!"); + if (!(opt.name_only || opt.unmatch_name_only || opt.count) + && (opt.pre_context || opt.post_context || + opt.file_break || opt.funcbody)) + skip_first_line = 1; + + /* + * Pre-read gitmodules (if not read already) and force eager + * initialization of packed_git to prevent racy lazy + * reading/initialization once worker threads are started. + */ + if (recurse_submodules) + repo_read_gitmodules(the_repository, 1); + if (startup_info->have_repository) + (void)get_packed_git(the_repository); + + start_threads(&opt); + } else { + /* + * The compiled patterns on the main path are only + * used when not using threading. Otherwise + * start_threads() above calls compile_grep_patterns() + * for each thread. + */ + compile_grep_patterns(&opt); + } + + if (show_in_pager && (cached || list.nr)) + die(_("--open-files-in-pager only works on the worktree")); + + if (show_in_pager && opt.pattern_list && !opt.pattern_list->next) { + const char *pager = path_list.items[0].string; + int len = strlen(pager); + + if (len > 4 && is_dir_sep(pager[len - 5])) + pager += len - 4; + + if (opt.ignore_case && !strcmp("less", pager)) + string_list_append(&path_list, "-I"); + + if (!strcmp("less", pager) || !strcmp("vi", pager)) { + struct strbuf buf = STRBUF_INIT; + strbuf_addf(&buf, "+/%s%s", + strcmp("less", pager) ? "" : "*", + opt.pattern_list->pattern); + string_list_append_nodup(&path_list, + strbuf_detach(&buf, NULL)); + } + } + + if (!show_in_pager && !opt.status_only) + setup_pager(); + + die_for_incompatible_opt3(!use_index, "--no-index", + untracked, "--untracked", + cached, "--cached"); + + if (!use_index || untracked) { + int use_exclude = (opt_exclude < 0) ? use_index : !!opt_exclude; + hit = grep_directory(&opt, &pathspec, use_exclude, use_index); + } else if (0 <= opt_exclude) { + die(_("--[no-]exclude-standard cannot be used for tracked contents")); + } else if (!list.nr) { + if (!cached) + setup_work_tree(); + + hit = grep_cache(&opt, &pathspec, cached); + } else { + if (cached) + die(_("both --cached and trees are given")); + + hit = grep_objects(&opt, &pathspec, &list); + } + + if (num_threads > 1) + hit |= wait_all(); + if (hit && show_in_pager) + run_pager(&opt, prefix); + clear_pathspec(&pathspec); + string_list_clear(&path_list, 0); + free_grep_patterns(&opt); + object_array_clear(&list); + free_repos(); + return !hit; +} diff --git a/builtin/hash-object.c b/builtin/hash-object.c new file mode 100644 index 0000000..5ffec99 --- /dev/null +++ b/builtin/hash-object.c @@ -0,0 +1,174 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + * Copyright (C) Junio C Hamano, 2005 + */ +#include "builtin.h" +#include "abspath.h" +#include "config.h" +#include "gettext.h" +#include "hex.h" +#include "object-file.h" +#include "object-store-ll.h" +#include "blob.h" +#include "quote.h" +#include "parse-options.h" +#include "exec-cmd.h" +#include "setup.h" +#include "strbuf.h" +#include "write-or-die.h" + +/* + * This is to create corrupt objects for debugging and as such it + * needs to bypass the data conversion performed by, and the type + * limitation imposed by, index_fd() and its callees. + */ +static int hash_literally(struct object_id *oid, int fd, const char *type, unsigned flags) +{ + struct strbuf buf = STRBUF_INIT; + int ret; + + if (strbuf_read(&buf, fd, 4096) < 0) + ret = -1; + else + ret = write_object_file_literally(buf.buf, buf.len, type, oid, + flags); + close(fd); + strbuf_release(&buf); + return ret; +} + +static void hash_fd(int fd, const char *type, const char *path, unsigned flags, + int literally) +{ + struct stat st; + struct object_id oid; + + if (fstat(fd, &st) < 0 || + (literally + ? hash_literally(&oid, fd, type, flags) + : index_fd(the_repository->index, &oid, fd, &st, + type_from_string(type), path, flags))) + die((flags & HASH_WRITE_OBJECT) + ? "Unable to add %s to database" + : "Unable to hash %s", path); + printf("%s\n", oid_to_hex(&oid)); + maybe_flush_or_die(stdout, "hash to stdout"); +} + +static void hash_object(const char *path, const char *type, const char *vpath, + unsigned flags, int literally) +{ + int fd; + fd = xopen(path, O_RDONLY); + hash_fd(fd, type, vpath, flags, literally); +} + +static void hash_stdin_paths(const char *type, int no_filters, unsigned flags, + int literally) +{ + struct strbuf buf = STRBUF_INIT; + struct strbuf unquoted = STRBUF_INIT; + + while (strbuf_getline(&buf, stdin) != EOF) { + if (buf.buf[0] == '"') { + strbuf_reset(&unquoted); + if (unquote_c_style(&unquoted, buf.buf, NULL)) + die("line is badly quoted"); + strbuf_swap(&buf, &unquoted); + } + hash_object(buf.buf, type, no_filters ? NULL : buf.buf, flags, + literally); + } + strbuf_release(&buf); + strbuf_release(&unquoted); +} + +int cmd_hash_object(int argc, const char **argv, const char *prefix) +{ + static const char * const hash_object_usage[] = { + N_("git hash-object [-t <type>] [-w] [--path=<file> | --no-filters]\n" + " [--stdin [--literally]] [--] <file>..."), + N_("git hash-object [-t <type>] [-w] --stdin-paths [--no-filters]"), + NULL + }; + const char *type = blob_type; + int hashstdin = 0; + int stdin_paths = 0; + int no_filters = 0; + int literally = 0; + int nongit = 0; + unsigned flags = HASH_FORMAT_CHECK; + const char *vpath = NULL; + char *vpath_free = NULL; + const struct option hash_object_options[] = { + OPT_STRING('t', NULL, &type, N_("type"), N_("object type")), + OPT_BIT('w', NULL, &flags, N_("write the object into the object database"), + HASH_WRITE_OBJECT), + OPT_COUNTUP( 0 , "stdin", &hashstdin, N_("read the object from stdin")), + OPT_BOOL( 0 , "stdin-paths", &stdin_paths, N_("read file names from stdin")), + OPT_BOOL( 0 , "no-filters", &no_filters, N_("store file as is without filters")), + OPT_BOOL( 0, "literally", &literally, N_("just hash any random garbage to create corrupt objects for debugging Git")), + OPT_STRING( 0 , "path", &vpath, N_("file"), N_("process file as it were from this path")), + OPT_END() + }; + int i; + const char *errstr = NULL; + + argc = parse_options(argc, argv, prefix, hash_object_options, + hash_object_usage, 0); + + if (flags & HASH_WRITE_OBJECT) + prefix = setup_git_directory(); + else + prefix = setup_git_directory_gently(&nongit); + + if (vpath && prefix) { + vpath_free = prefix_filename(prefix, vpath); + vpath = vpath_free; + } + + git_config(git_default_config, NULL); + + if (stdin_paths) { + if (hashstdin) + errstr = "Can't use --stdin-paths with --stdin"; + else if (argc) + errstr = "Can't specify files with --stdin-paths"; + else if (vpath) + errstr = "Can't use --stdin-paths with --path"; + } + else { + if (hashstdin > 1) + errstr = "Multiple --stdin arguments are not supported"; + if (vpath && no_filters) + errstr = "Can't use --path with --no-filters"; + } + + if (errstr) { + error("%s", errstr); + usage_with_options(hash_object_usage, hash_object_options); + } + + if (hashstdin) + hash_fd(0, type, vpath, flags, literally); + + for (i = 0 ; i < argc; i++) { + const char *arg = argv[i]; + char *to_free = NULL; + + if (prefix) + arg = to_free = prefix_filename(prefix, arg); + hash_object(arg, type, no_filters ? NULL : vpath ? vpath : arg, + flags, literally); + free(to_free); + } + + if (stdin_paths) + hash_stdin_paths(type, no_filters, flags, literally); + + free(vpath_free); + + return 0; +} diff --git a/builtin/help.c b/builtin/help.c new file mode 100644 index 0000000..dc1fbe2 --- /dev/null +++ b/builtin/help.c @@ -0,0 +1,726 @@ +/* + * Builtin help command + */ +#include "builtin.h" +#include "config.h" +#include "exec-cmd.h" +#include "gettext.h" +#include "pager.h" +#include "parse-options.h" +#include "path.h" +#include "run-command.h" +#include "config-list.h" +#include "help.h" +#include "alias.h" +#include "setup.h" + +#ifndef DEFAULT_HELP_FORMAT +#define DEFAULT_HELP_FORMAT "man" +#endif + +static struct man_viewer_list { + struct man_viewer_list *next; + char name[FLEX_ARRAY]; +} *man_viewer_list; + +static struct man_viewer_info_list { + struct man_viewer_info_list *next; + const char *info; + char name[FLEX_ARRAY]; +} *man_viewer_info_list; + +enum help_format { + HELP_FORMAT_NONE, + HELP_FORMAT_MAN, + HELP_FORMAT_INFO, + HELP_FORMAT_WEB +}; + +enum show_config_type { + SHOW_CONFIG_HUMAN, + SHOW_CONFIG_VARS, + SHOW_CONFIG_SECTIONS, +}; + +static enum help_action { + HELP_ACTION_ALL = 1, + HELP_ACTION_GUIDES, + HELP_ACTION_CONFIG, + HELP_ACTION_USER_INTERFACES, + HELP_ACTION_DEVELOPER_INTERFACES, + HELP_ACTION_CONFIG_FOR_COMPLETION, + HELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION, +} cmd_mode; + +static const char *html_path; +static int verbose = 1; +static enum help_format help_format = HELP_FORMAT_NONE; +static int exclude_guides; +static int show_external_commands = -1; +static int show_aliases = -1; +static struct option builtin_help_options[] = { + OPT_CMDMODE('a', "all", &cmd_mode, N_("print all available commands"), + HELP_ACTION_ALL), + OPT_BOOL(0, "external-commands", &show_external_commands, + N_("show external commands in --all")), + OPT_BOOL(0, "aliases", &show_aliases, N_("show aliases in --all")), + OPT_HIDDEN_BOOL(0, "exclude-guides", &exclude_guides, N_("exclude guides")), + OPT_SET_INT('m', "man", &help_format, N_("show man page"), HELP_FORMAT_MAN), + OPT_SET_INT('w', "web", &help_format, N_("show manual in web browser"), + HELP_FORMAT_WEB), + OPT_SET_INT('i', "info", &help_format, N_("show info page"), + HELP_FORMAT_INFO), + OPT__VERBOSE(&verbose, N_("print command description")), + + OPT_CMDMODE('g', "guides", &cmd_mode, N_("print list of useful guides"), + HELP_ACTION_GUIDES), + OPT_CMDMODE(0, "user-interfaces", &cmd_mode, + N_("print list of user-facing repository, command and file interfaces"), + HELP_ACTION_USER_INTERFACES), + OPT_CMDMODE(0, "developer-interfaces", &cmd_mode, + N_("print list of file formats, protocols and other developer interfaces"), + HELP_ACTION_DEVELOPER_INTERFACES), + OPT_CMDMODE('c', "config", &cmd_mode, N_("print all configuration variable names"), + HELP_ACTION_CONFIG), + OPT_CMDMODE_F(0, "config-for-completion", &cmd_mode, "", + HELP_ACTION_CONFIG_FOR_COMPLETION, PARSE_OPT_HIDDEN), + OPT_CMDMODE_F(0, "config-sections-for-completion", &cmd_mode, "", + HELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION, PARSE_OPT_HIDDEN), + + OPT_END(), +}; + +static const char * const builtin_help_usage[] = { + "git help [-a|--all] [--[no-]verbose] [--[no-]external-commands] [--[no-]aliases]", + N_("git help [[-i|--info] [-m|--man] [-w|--web]] [<command>|<doc>]"), + "git help [-g|--guides]", + "git help [-c|--config]", + "git help [--user-interfaces]", + "git help [--developer-interfaces]", + NULL +}; + +struct slot_expansion { + const char *prefix; + const char *placeholder; + void (*fn)(struct string_list *list, const char *prefix); + int found; +}; + +static void list_config_help(enum show_config_type type) +{ + struct slot_expansion slot_expansions[] = { + { "advice", "*", list_config_advices }, + { "color.branch", "<slot>", list_config_color_branch_slots }, + { "color.decorate", "<slot>", list_config_color_decorate_slots }, + { "color.diff", "<slot>", list_config_color_diff_slots }, + { "color.grep", "<slot>", list_config_color_grep_slots }, + { "color.interactive", "<slot>", list_config_color_interactive_slots }, + { "color.remote", "<slot>", list_config_color_sideband_slots }, + { "color.status", "<slot>", list_config_color_status_slots }, + { "fsck", "<msg-id>", list_config_fsck_msg_ids }, + { "receive.fsck", "<msg-id>", list_config_fsck_msg_ids }, + { NULL, NULL, NULL } + }; + const char **p; + struct slot_expansion *e; + struct string_list keys = STRING_LIST_INIT_DUP; + struct string_list keys_uniq = STRING_LIST_INIT_DUP; + struct string_list_item *item; + int i; + + for (p = config_name_list; *p; p++) { + const char *var = *p; + struct strbuf sb = STRBUF_INIT; + + for (e = slot_expansions; e->prefix; e++) { + + strbuf_reset(&sb); + strbuf_addf(&sb, "%s.%s", e->prefix, e->placeholder); + if (!strcasecmp(var, sb.buf)) { + e->fn(&keys, e->prefix); + e->found++; + break; + } + } + strbuf_release(&sb); + if (!e->prefix) + string_list_append(&keys, var); + } + + for (e = slot_expansions; e->prefix; e++) + if (!e->found) + BUG("slot_expansion %s.%s is not used", + e->prefix, e->placeholder); + + string_list_sort(&keys); + for (i = 0; i < keys.nr; i++) { + const char *var = keys.items[i].string; + const char *wildcard, *tag, *cut; + const char *dot = NULL; + struct strbuf sb = STRBUF_INIT; + + switch (type) { + case SHOW_CONFIG_HUMAN: + puts(var); + continue; + case SHOW_CONFIG_SECTIONS: + dot = strchr(var, '.'); + break; + case SHOW_CONFIG_VARS: + break; + } + wildcard = strchr(var, '*'); + tag = strchr(var, '<'); + + if (!dot && !wildcard && !tag) { + string_list_append(&keys_uniq, var); + continue; + } + + if (dot) + cut = dot; + else if (wildcard && !tag) + cut = wildcard; + else if (!wildcard && tag) + cut = tag; + else + cut = wildcard < tag ? wildcard : tag; + + strbuf_add(&sb, var, cut - var); + string_list_append(&keys_uniq, sb.buf); + strbuf_release(&sb); + + } + string_list_clear(&keys, 0); + string_list_remove_duplicates(&keys_uniq, 0); + for_each_string_list_item(item, &keys_uniq) + puts(item->string); + string_list_clear(&keys_uniq, 0); +} + +static enum help_format parse_help_format(const char *format) +{ + if (!strcmp(format, "man")) + return HELP_FORMAT_MAN; + if (!strcmp(format, "info")) + return HELP_FORMAT_INFO; + if (!strcmp(format, "web") || !strcmp(format, "html")) + return HELP_FORMAT_WEB; + /* + * Please update _git_config() in git-completion.bash when you + * add new help formats. + */ + die(_("unrecognized help format '%s'"), format); +} + +static const char *get_man_viewer_info(const char *name) +{ + struct man_viewer_info_list *viewer; + + for (viewer = man_viewer_info_list; viewer; viewer = viewer->next) + { + if (!strcasecmp(name, viewer->name)) + return viewer->info; + } + return NULL; +} + +static int check_emacsclient_version(void) +{ + struct strbuf buffer = STRBUF_INIT; + struct child_process ec_process = CHILD_PROCESS_INIT; + int version; + + /* emacsclient prints its version number on stderr */ + strvec_pushl(&ec_process.args, "emacsclient", "--version", NULL); + ec_process.err = -1; + ec_process.stdout_to_stderr = 1; + if (start_command(&ec_process)) + return error(_("Failed to start emacsclient.")); + + strbuf_read(&buffer, ec_process.err, 20); + close(ec_process.err); + + /* + * Don't bother checking return value, because "emacsclient --version" + * seems to always exits with code 1. + */ + finish_command(&ec_process); + + if (!starts_with(buffer.buf, "emacsclient")) { + strbuf_release(&buffer); + return error(_("Failed to parse emacsclient version.")); + } + + strbuf_remove(&buffer, 0, strlen("emacsclient")); + version = atoi(buffer.buf); + + if (version < 22) { + strbuf_release(&buffer); + return error(_("emacsclient version '%d' too old (< 22)."), + version); + } + + strbuf_release(&buffer); + return 0; +} + +static void exec_woman_emacs(const char *path, const char *page) +{ + if (!check_emacsclient_version()) { + /* This works only with emacsclient version >= 22. */ + struct strbuf man_page = STRBUF_INIT; + + if (!path) + path = "emacsclient"; + strbuf_addf(&man_page, "(woman \"%s\")", page); + execlp(path, "emacsclient", "-e", man_page.buf, (char *)NULL); + warning_errno(_("failed to exec '%s'"), path); + strbuf_release(&man_page); + } +} + +static void exec_man_konqueror(const char *path, const char *page) +{ + const char *display = getenv("DISPLAY"); + if (display && *display) { + struct strbuf man_page = STRBUF_INIT; + const char *filename = "kfmclient"; + + /* It's simpler to launch konqueror using kfmclient. */ + if (path) { + size_t len; + if (strip_suffix(path, "/konqueror", &len)) + path = xstrfmt("%.*s/kfmclient", (int)len, path); + filename = basename((char *)path); + } else + path = "kfmclient"; + strbuf_addf(&man_page, "man:%s(1)", page); + execlp(path, filename, "newTab", man_page.buf, (char *)NULL); + warning_errno(_("failed to exec '%s'"), path); + strbuf_release(&man_page); + } +} + +static void exec_man_man(const char *path, const char *page) +{ + if (!path) + path = "man"; + execlp(path, "man", page, (char *)NULL); + warning_errno(_("failed to exec '%s'"), path); +} + +static void exec_man_cmd(const char *cmd, const char *page) +{ + struct strbuf shell_cmd = STRBUF_INIT; + strbuf_addf(&shell_cmd, "%s %s", cmd, page); + execl(SHELL_PATH, SHELL_PATH, "-c", shell_cmd.buf, (char *)NULL); + warning(_("failed to exec '%s'"), cmd); + strbuf_release(&shell_cmd); +} + +static void add_man_viewer(const char *name) +{ + struct man_viewer_list **p = &man_viewer_list; + + while (*p) + p = &((*p)->next); + FLEX_ALLOC_STR(*p, name, name); +} + +static int supported_man_viewer(const char *name, size_t len) +{ + return (!strncasecmp("man", name, len) || + !strncasecmp("woman", name, len) || + !strncasecmp("konqueror", name, len)); +} + +static void do_add_man_viewer_info(const char *name, + size_t len, + const char *value) +{ + struct man_viewer_info_list *new_man_viewer; + FLEX_ALLOC_MEM(new_man_viewer, name, name, len); + new_man_viewer->info = xstrdup(value); + new_man_viewer->next = man_viewer_info_list; + man_viewer_info_list = new_man_viewer; +} + +static int add_man_viewer_path(const char *name, + size_t len, + const char *value) +{ + if (supported_man_viewer(name, len)) + do_add_man_viewer_info(name, len, value); + else + warning(_("'%s': path for unsupported man viewer.\n" + "Please consider using 'man.<tool>.cmd' instead."), + name); + + return 0; +} + +static int add_man_viewer_cmd(const char *name, + size_t len, + const char *value) +{ + if (supported_man_viewer(name, len)) + warning(_("'%s': cmd for supported man viewer.\n" + "Please consider using 'man.<tool>.path' instead."), + name); + else + do_add_man_viewer_info(name, len, value); + + return 0; +} + +static int add_man_viewer_info(const char *var, const char *value) +{ + const char *name, *subkey; + size_t namelen; + + if (parse_config_key(var, "man", &name, &namelen, &subkey) < 0 || !name) + return 0; + + if (!strcmp(subkey, "path")) { + if (!value) + return config_error_nonbool(var); + return add_man_viewer_path(name, namelen, value); + } + if (!strcmp(subkey, "cmd")) { + if (!value) + return config_error_nonbool(var); + return add_man_viewer_cmd(name, namelen, value); + } + + return 0; +} + +static int git_help_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + if (!strcmp(var, "help.format")) { + if (!value) + return config_error_nonbool(var); + help_format = parse_help_format(value); + return 0; + } + if (!strcmp(var, "help.htmlpath")) { + if (!value) + return config_error_nonbool(var); + html_path = xstrdup(value); + return 0; + } + if (!strcmp(var, "man.viewer")) { + if (!value) + return config_error_nonbool(var); + add_man_viewer(value); + return 0; + } + if (starts_with(var, "man.")) + return add_man_viewer_info(var, value); + + return git_default_config(var, value, ctx, cb); +} + +static struct cmdnames main_cmds, other_cmds; + +static int is_git_command(const char *s) +{ + if (is_builtin(s)) + return 1; + + load_command_list("git-", &main_cmds, &other_cmds); + return is_in_cmdlist(&main_cmds, s) || + is_in_cmdlist(&other_cmds, s); +} + +static const char *cmd_to_page(const char *git_cmd) +{ + if (!git_cmd) + return "git"; + else if (starts_with(git_cmd, "git")) + return git_cmd; + else if (is_git_command(git_cmd)) + return xstrfmt("git-%s", git_cmd); + else if (!strcmp("scalar", git_cmd)) + return xstrdup(git_cmd); + else + return xstrfmt("git%s", git_cmd); +} + +static void setup_man_path(void) +{ + struct strbuf new_path = STRBUF_INIT; + const char *old_path = getenv("MANPATH"); + char *git_man_path = system_path(GIT_MAN_PATH); + + /* We should always put ':' after our path. If there is no + * old_path, the ':' at the end will let 'man' to try + * system-wide paths after ours to find the manual page. If + * there is old_path, we need ':' as delimiter. */ + strbuf_addstr(&new_path, git_man_path); + strbuf_addch(&new_path, ':'); + if (old_path) + strbuf_addstr(&new_path, old_path); + + free(git_man_path); + setenv("MANPATH", new_path.buf, 1); + + strbuf_release(&new_path); +} + +static void exec_viewer(const char *name, const char *page) +{ + const char *info = get_man_viewer_info(name); + + if (!strcasecmp(name, "man")) + exec_man_man(info, page); + else if (!strcasecmp(name, "woman")) + exec_woman_emacs(info, page); + else if (!strcasecmp(name, "konqueror")) + exec_man_konqueror(info, page); + else if (info) + exec_man_cmd(info, page); + else + warning(_("'%s': unknown man viewer."), name); +} + +static void show_man_page(const char *page) +{ + struct man_viewer_list *viewer; + const char *fallback = getenv("GIT_MAN_VIEWER"); + + setup_man_path(); + for (viewer = man_viewer_list; viewer; viewer = viewer->next) + { + exec_viewer(viewer->name, page); /* will return when unable */ + } + if (fallback) + exec_viewer(fallback, page); + exec_viewer("man", page); + die(_("no man viewer handled the request")); +} + +static void show_info_page(const char *page) +{ + setenv("INFOPATH", system_path(GIT_INFO_PATH), 1); + execlp("info", "info", "gitman", page, (char *)NULL); + die(_("no info viewer handled the request")); +} + +static void get_html_page_path(struct strbuf *page_path, const char *page) +{ + struct stat st; + char *to_free = NULL; + + if (!html_path) + html_path = to_free = system_path(GIT_HTML_PATH); + + /* + * Check that the page we're looking for exists. + */ + if (!strstr(html_path, "://")) { + if (stat(mkpath("%s/%s.html", html_path, page), &st) + || !S_ISREG(st.st_mode)) + die("'%s/%s.html': documentation file not found.", + html_path, page); + } + + strbuf_init(page_path, 0); + strbuf_addf(page_path, "%s/%s.html", html_path, page); + free(to_free); +} + +static void open_html(const char *path) +{ + execl_git_cmd("web--browse", "-c", "help.browser", path, (char *)NULL); +} + +static void show_html_page(const char *page) +{ + struct strbuf page_path; /* it leaks but we exec bellow */ + + get_html_page_path(&page_path, page); + + open_html(page_path.buf); +} + +static const char *check_git_cmd(const char* cmd) +{ + char *alias; + + if (is_git_command(cmd)) + return cmd; + + alias = alias_lookup(cmd); + if (alias) { + const char **argv; + int count; + + /* + * handle_builtin() in git.c rewrites "git cmd --help" + * to "git help --exclude-guides cmd", so we can use + * exclude_guides to distinguish "git cmd --help" from + * "git help cmd". In the latter case, or if cmd is an + * alias for a shell command, just print the alias + * definition. + */ + if (!exclude_guides || alias[0] == '!') { + printf_ln(_("'%s' is aliased to '%s'"), cmd, alias); + free(alias); + exit(0); + } + /* + * Otherwise, we pretend that the command was "git + * word0 --help". We use split_cmdline() to get the + * first word of the alias, to ensure that we use the + * same rules as when the alias is actually + * used. split_cmdline() modifies alias in-place. + */ + fprintf_ln(stderr, _("'%s' is aliased to '%s'"), cmd, alias); + count = split_cmdline(alias, &argv); + if (count < 0) + die(_("bad alias.%s string: %s"), cmd, + split_cmdline_strerror(count)); + free(argv); + UNLEAK(alias); + return alias; + } + + if (exclude_guides) + return help_unknown_cmd(cmd); + + return cmd; +} + +static void no_help_format(const char *opt_mode, enum help_format fmt) +{ + const char *opt_fmt; + + switch (fmt) { + case HELP_FORMAT_NONE: + return; + case HELP_FORMAT_MAN: + opt_fmt = "--man"; + break; + case HELP_FORMAT_INFO: + opt_fmt = "--info"; + break; + case HELP_FORMAT_WEB: + opt_fmt = "--web"; + break; + default: + BUG("unreachable"); + } + + usage_msg_optf(_("options '%s' and '%s' cannot be used together"), + builtin_help_usage, builtin_help_options, opt_mode, + opt_fmt); +} + +static void opt_mode_usage(int argc, const char *opt_mode, + enum help_format fmt) +{ + if (argc) + usage_msg_optf(_("the '%s' option doesn't take any non-option arguments"), + builtin_help_usage, builtin_help_options, + opt_mode); + + no_help_format(opt_mode, fmt); +} + +int cmd_help(int argc, const char **argv, const char *prefix) +{ + int nongit; + enum help_format parsed_help_format; + const char *page; + + argc = parse_options(argc, argv, prefix, builtin_help_options, + builtin_help_usage, 0); + parsed_help_format = help_format; + + if (cmd_mode != HELP_ACTION_ALL && + (show_external_commands >= 0 || + show_aliases >= 0)) + usage_msg_opt(_("the '--no-[external-commands|aliases]' options can only be used with '--all'"), + builtin_help_usage, builtin_help_options); + + switch (cmd_mode) { + case HELP_ACTION_ALL: + opt_mode_usage(argc, "--all", help_format); + if (verbose) { + setup_pager(); + list_all_cmds_help(show_external_commands, + show_aliases); + return 0; + } + printf(_("usage: %s%s"), _(git_usage_string), "\n\n"); + load_command_list("git-", &main_cmds, &other_cmds); + list_commands(&main_cmds, &other_cmds); + printf("%s\n", _(git_more_info_string)); + break; + case HELP_ACTION_GUIDES: + opt_mode_usage(argc, "--guides", help_format); + list_guides_help(); + printf("%s\n", _(git_more_info_string)); + return 0; + case HELP_ACTION_CONFIG_FOR_COMPLETION: + opt_mode_usage(argc, "--config-for-completion", help_format); + list_config_help(SHOW_CONFIG_VARS); + return 0; + case HELP_ACTION_USER_INTERFACES: + opt_mode_usage(argc, "--user-interfaces", help_format); + list_user_interfaces_help(); + return 0; + case HELP_ACTION_DEVELOPER_INTERFACES: + opt_mode_usage(argc, "--developer-interfaces", help_format); + list_developer_interfaces_help(); + return 0; + case HELP_ACTION_CONFIG_SECTIONS_FOR_COMPLETION: + opt_mode_usage(argc, "--config-sections-for-completion", + help_format); + list_config_help(SHOW_CONFIG_SECTIONS); + return 0; + case HELP_ACTION_CONFIG: + opt_mode_usage(argc, "--config", help_format); + setup_pager(); + list_config_help(SHOW_CONFIG_HUMAN); + printf("\n%s\n", _("'git help config' for more information")); + return 0; + } + + if (!argv[0]) { + printf(_("usage: %s%s"), _(git_usage_string), "\n\n"); + list_common_cmds_help(); + printf("\n%s\n", _(git_more_info_string)); + return 0; + } + + setup_git_directory_gently(&nongit); + git_config(git_help_config, NULL); + + if (parsed_help_format != HELP_FORMAT_NONE) + help_format = parsed_help_format; + if (help_format == HELP_FORMAT_NONE) + help_format = parse_help_format(DEFAULT_HELP_FORMAT); + + argv[0] = check_git_cmd(argv[0]); + + page = cmd_to_page(argv[0]); + switch (help_format) { + case HELP_FORMAT_NONE: + case HELP_FORMAT_MAN: + show_man_page(page); + break; + case HELP_FORMAT_INFO: + show_info_page(page); + break; + case HELP_FORMAT_WEB: + show_html_page(page); + break; + } + + return 0; +} diff --git a/builtin/hook.c b/builtin/hook.c new file mode 100644 index 0000000..09b51a6 --- /dev/null +++ b/builtin/hook.c @@ -0,0 +1,82 @@ +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "hook.h" +#include "parse-options.h" +#include "strbuf.h" +#include "strvec.h" + +#define BUILTIN_HOOK_RUN_USAGE \ + N_("git hook run [--ignore-missing] [--to-stdin=<path>] <hook-name> [-- <hook-args>]") + +static const char * const builtin_hook_usage[] = { + BUILTIN_HOOK_RUN_USAGE, + NULL +}; + +static const char * const builtin_hook_run_usage[] = { + BUILTIN_HOOK_RUN_USAGE, + NULL +}; + +static int run(int argc, const char **argv, const char *prefix) +{ + int i; + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + int ignore_missing = 0; + const char *hook_name; + struct option run_options[] = { + OPT_BOOL(0, "ignore-missing", &ignore_missing, + N_("silently ignore missing requested <hook-name>")), + OPT_STRING(0, "to-stdin", &opt.path_to_stdin, N_("path"), + N_("file to read into hooks' stdin")), + OPT_END(), + }; + int ret; + + argc = parse_options(argc, argv, prefix, run_options, + builtin_hook_run_usage, + PARSE_OPT_KEEP_DASHDASH); + + if (!argc) + goto usage; + + /* + * Having a -- for "run" when providing <hook-args> is + * mandatory. + */ + if (argc > 1 && strcmp(argv[1], "--") && + strcmp(argv[1], "--end-of-options")) + goto usage; + + /* Add our arguments, start after -- */ + for (i = 2 ; i < argc; i++) + strvec_push(&opt.args, argv[i]); + + /* Need to take into account core.hooksPath */ + git_config(git_default_config, NULL); + + hook_name = argv[0]; + if (!ignore_missing) + opt.error_if_missing = 1; + ret = run_hooks_opt(hook_name, &opt); + if (ret < 0) /* error() return */ + ret = 1; + return ret; +usage: + usage_with_options(builtin_hook_run_usage, run_options); +} + +int cmd_hook(int argc, const char **argv, const char *prefix) +{ + parse_opt_subcommand_fn *fn = NULL; + struct option builtin_hook_options[] = { + OPT_SUBCOMMAND("run", &fn, run), + OPT_END(), + }; + + argc = parse_options(argc, argv, NULL, builtin_hook_options, + builtin_hook_usage, 0); + + return fn(argc, argv, prefix); +} diff --git a/builtin/index-pack.c b/builtin/index-pack.c new file mode 100644 index 0000000..dda94a9 --- /dev/null +++ b/builtin/index-pack.c @@ -0,0 +1,1977 @@ +#include "builtin.h" +#include "config.h" +#include "delta.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "pack.h" +#include "csum-file.h" +#include "blob.h" +#include "commit.h" +#include "tag.h" +#include "tree.h" +#include "progress.h" +#include "fsck.h" +#include "exec-cmd.h" +#include "strbuf.h" +#include "streaming.h" +#include "thread-utils.h" +#include "packfile.h" +#include "pack-revindex.h" +#include "object-file.h" +#include "object-store-ll.h" +#include "oid-array.h" +#include "replace-object.h" +#include "promisor-remote.h" +#include "setup.h" + +static const char index_pack_usage[] = +"git index-pack [-v] [-o <index-file>] [--keep | --keep=<msg>] [--[no-]rev-index] [--verify] [--strict] (<pack-file> | --stdin [--fix-thin] [<pack-file>])"; + +struct object_entry { + struct pack_idx_entry idx; + unsigned long size; + unsigned char hdr_size; + signed char type; + signed char real_type; +}; + +struct object_stat { + unsigned delta_depth; + int base_object_no; +}; + +struct base_data { + /* Initialized by make_base(). */ + struct base_data *base; + struct object_entry *obj; + int ref_first, ref_last; + int ofs_first, ofs_last; + /* + * Threads should increment retain_data if they are about to call + * patch_delta() using this struct's data as a base, and decrement this + * when they are done. While retain_data is nonzero, this struct's data + * will not be freed even if the delta base cache limit is exceeded. + */ + int retain_data; + /* + * The number of direct children that have not been fully processed + * (entered work_head, entered done_head, left done_head). When this + * number reaches zero, this struct base_data can be freed. + */ + int children_remaining; + + /* Not initialized by make_base(). */ + struct list_head list; + void *data; + unsigned long size; +}; + +/* + * Stack of struct base_data that have unprocessed children. + * threaded_second_pass() uses this as a source of work (the other being the + * objects array). + * + * Guarded by work_mutex. + */ +static LIST_HEAD(work_head); + +/* + * Stack of struct base_data that have children, all of whom have been + * processed or are being processed, and at least one child is being processed. + * These struct base_data must be kept around until the last child is + * processed. + * + * Guarded by work_mutex. + */ +static LIST_HEAD(done_head); + +/* + * All threads share one delta base cache. + * + * base_cache_used is guarded by work_mutex, and base_cache_limit is read-only + * in a thread. + */ +static size_t base_cache_used; +static size_t base_cache_limit; + +struct thread_local { + pthread_t thread; + int pack_fd; +}; + +/* Remember to update object flag allocation in object.h */ +#define FLAG_LINK (1u<<20) +#define FLAG_CHECKED (1u<<21) + +struct ofs_delta_entry { + off_t offset; + int obj_no; +}; + +struct ref_delta_entry { + struct object_id oid; + int obj_no; +}; + +static struct object_entry *objects; +static struct object_stat *obj_stat; +static struct ofs_delta_entry *ofs_deltas; +static struct ref_delta_entry *ref_deltas; +static struct thread_local nothread_data; +static int nr_objects; +static int nr_ofs_deltas; +static int nr_ref_deltas; +static int ref_deltas_alloc; +static int nr_resolved_deltas; +static int nr_threads; + +static int from_stdin; +static int strict; +static int do_fsck_object; +static struct fsck_options fsck_options = FSCK_OPTIONS_MISSING_GITMODULES; +static int verbose; +static const char *progress_title; +static int show_resolving_progress; +static int show_stat; +static int check_self_contained_and_connected; + +static struct progress *progress; + +/* We always read in 4kB chunks. */ +static unsigned char input_buffer[4096]; +static unsigned int input_offset, input_len; +static off_t consumed_bytes; +static off_t max_input_size; +static unsigned deepest_delta; +static git_hash_ctx input_ctx; +static uint32_t input_crc32; +static int input_fd, output_fd; +static const char *curr_pack; + +static struct thread_local *thread_data; +static int nr_dispatched; +static int threads_active; + +static pthread_mutex_t read_mutex; +#define read_lock() lock_mutex(&read_mutex) +#define read_unlock() unlock_mutex(&read_mutex) + +static pthread_mutex_t counter_mutex; +#define counter_lock() lock_mutex(&counter_mutex) +#define counter_unlock() unlock_mutex(&counter_mutex) + +static pthread_mutex_t work_mutex; +#define work_lock() lock_mutex(&work_mutex) +#define work_unlock() unlock_mutex(&work_mutex) + +static pthread_mutex_t deepest_delta_mutex; +#define deepest_delta_lock() lock_mutex(&deepest_delta_mutex) +#define deepest_delta_unlock() unlock_mutex(&deepest_delta_mutex) + +static pthread_key_t key; + +static inline void lock_mutex(pthread_mutex_t *mutex) +{ + if (threads_active) + pthread_mutex_lock(mutex); +} + +static inline void unlock_mutex(pthread_mutex_t *mutex) +{ + if (threads_active) + pthread_mutex_unlock(mutex); +} + +/* + * Mutex and conditional variable can't be statically-initialized on Windows. + */ +static void init_thread(void) +{ + int i; + init_recursive_mutex(&read_mutex); + pthread_mutex_init(&counter_mutex, NULL); + pthread_mutex_init(&work_mutex, NULL); + if (show_stat) + pthread_mutex_init(&deepest_delta_mutex, NULL); + pthread_key_create(&key, NULL); + CALLOC_ARRAY(thread_data, nr_threads); + for (i = 0; i < nr_threads; i++) { + thread_data[i].pack_fd = xopen(curr_pack, O_RDONLY); + } + + threads_active = 1; +} + +static void cleanup_thread(void) +{ + int i; + if (!threads_active) + return; + threads_active = 0; + pthread_mutex_destroy(&read_mutex); + pthread_mutex_destroy(&counter_mutex); + pthread_mutex_destroy(&work_mutex); + if (show_stat) + pthread_mutex_destroy(&deepest_delta_mutex); + for (i = 0; i < nr_threads; i++) + close(thread_data[i].pack_fd); + pthread_key_delete(key); + free(thread_data); +} + +static int mark_link(struct object *obj, enum object_type type, + void *data UNUSED, + struct fsck_options *options UNUSED) +{ + if (!obj) + return -1; + + if (type != OBJ_ANY && obj->type != type) + die(_("object type mismatch at %s"), oid_to_hex(&obj->oid)); + + obj->flags |= FLAG_LINK; + return 0; +} + +/* The content of each linked object must have been checked + or it must be already present in the object database */ +static unsigned check_object(struct object *obj) +{ + if (!obj) + return 0; + + if (!(obj->flags & FLAG_LINK)) + return 0; + + if (!(obj->flags & FLAG_CHECKED)) { + unsigned long size; + int type = oid_object_info(the_repository, &obj->oid, &size); + if (type <= 0) + die(_("did not receive expected object %s"), + oid_to_hex(&obj->oid)); + if (type != obj->type) + die(_("object %s: expected type %s, found %s"), + oid_to_hex(&obj->oid), + type_name(obj->type), type_name(type)); + obj->flags |= FLAG_CHECKED; + return 1; + } + + return 0; +} + +static unsigned check_objects(void) +{ + unsigned i, max, foreign_nr = 0; + + max = get_max_object_index(); + + if (verbose) + progress = start_delayed_progress(_("Checking objects"), max); + + for (i = 0; i < max; i++) { + foreign_nr += check_object(get_indexed_object(i)); + display_progress(progress, i + 1); + } + + stop_progress(&progress); + return foreign_nr; +} + + +/* Discard current buffer used content. */ +static void flush(void) +{ + if (input_offset) { + if (output_fd >= 0) + write_or_die(output_fd, input_buffer, input_offset); + the_hash_algo->update_fn(&input_ctx, input_buffer, input_offset); + memmove(input_buffer, input_buffer + input_offset, input_len); + input_offset = 0; + } +} + +/* + * Make sure at least "min" bytes are available in the buffer, and + * return the pointer to the buffer. + */ +static void *fill(int min) +{ + if (min <= input_len) + return input_buffer + input_offset; + if (min > sizeof(input_buffer)) + die(Q_("cannot fill %d byte", + "cannot fill %d bytes", + min), + min); + flush(); + do { + ssize_t ret = xread(input_fd, input_buffer + input_len, + sizeof(input_buffer) - input_len); + if (ret <= 0) { + if (!ret) + die(_("early EOF")); + die_errno(_("read error on input")); + } + input_len += ret; + if (from_stdin) + display_throughput(progress, consumed_bytes + input_len); + } while (input_len < min); + return input_buffer; +} + +static void use(int bytes) +{ + if (bytes > input_len) + die(_("used more bytes than were available")); + input_crc32 = crc32(input_crc32, input_buffer + input_offset, bytes); + input_len -= bytes; + input_offset += bytes; + + /* make sure off_t is sufficiently large not to wrap */ + if (signed_add_overflows(consumed_bytes, bytes)) + die(_("pack too large for current definition of off_t")); + consumed_bytes += bytes; + if (max_input_size && consumed_bytes > max_input_size) { + struct strbuf size_limit = STRBUF_INIT; + strbuf_humanise_bytes(&size_limit, max_input_size); + die(_("pack exceeds maximum allowed size (%s)"), + size_limit.buf); + } +} + +static const char *open_pack_file(const char *pack_name) +{ + if (from_stdin) { + input_fd = 0; + if (!pack_name) { + struct strbuf tmp_file = STRBUF_INIT; + output_fd = odb_mkstemp(&tmp_file, + "pack/tmp_pack_XXXXXX"); + pack_name = strbuf_detach(&tmp_file, NULL); + } else { + output_fd = xopen(pack_name, O_CREAT|O_EXCL|O_RDWR, 0600); + } + nothread_data.pack_fd = output_fd; + } else { + input_fd = xopen(pack_name, O_RDONLY); + output_fd = -1; + nothread_data.pack_fd = input_fd; + } + the_hash_algo->init_fn(&input_ctx); + return pack_name; +} + +static void parse_pack_header(void) +{ + struct pack_header *hdr = fill(sizeof(struct pack_header)); + + /* Header consistency check */ + if (hdr->hdr_signature != htonl(PACK_SIGNATURE)) + die(_("pack signature mismatch")); + if (!pack_version_ok(hdr->hdr_version)) + die(_("pack version %"PRIu32" unsupported"), + ntohl(hdr->hdr_version)); + + nr_objects = ntohl(hdr->hdr_entries); + use(sizeof(struct pack_header)); +} + +__attribute__((format (printf, 2, 3))) +static NORETURN void bad_object(off_t offset, const char *format, ...) +{ + va_list params; + char buf[1024]; + + va_start(params, format); + vsnprintf(buf, sizeof(buf), format, params); + va_end(params); + die(_("pack has bad object at offset %"PRIuMAX": %s"), + (uintmax_t)offset, buf); +} + +static inline struct thread_local *get_thread_data(void) +{ + if (HAVE_THREADS) { + if (threads_active) + return pthread_getspecific(key); + assert(!threads_active && + "This should only be reached when all threads are gone"); + } + return ¬hread_data; +} + +static void set_thread_data(struct thread_local *data) +{ + if (threads_active) + pthread_setspecific(key, data); +} + +static void free_base_data(struct base_data *c) +{ + if (c->data) { + FREE_AND_NULL(c->data); + base_cache_used -= c->size; + } +} + +static void prune_base_data(struct base_data *retain) +{ + struct list_head *pos; + + if (base_cache_used <= base_cache_limit) + return; + + list_for_each_prev(pos, &done_head) { + struct base_data *b = list_entry(pos, struct base_data, list); + if (b->retain_data || b == retain) + continue; + if (b->data) { + free_base_data(b); + if (base_cache_used <= base_cache_limit) + return; + } + } + + list_for_each_prev(pos, &work_head) { + struct base_data *b = list_entry(pos, struct base_data, list); + if (b->retain_data || b == retain) + continue; + if (b->data) { + free_base_data(b); + if (base_cache_used <= base_cache_limit) + return; + } + } +} + +static int is_delta_type(enum object_type type) +{ + return (type == OBJ_REF_DELTA || type == OBJ_OFS_DELTA); +} + +static void *unpack_entry_data(off_t offset, unsigned long size, + enum object_type type, struct object_id *oid) +{ + static char fixed_buf[8192]; + int status; + git_zstream stream; + void *buf; + git_hash_ctx c; + char hdr[32]; + int hdrlen; + + if (!is_delta_type(type)) { + hdrlen = format_object_header(hdr, sizeof(hdr), type, size); + the_hash_algo->init_fn(&c); + the_hash_algo->update_fn(&c, hdr, hdrlen); + } else + oid = NULL; + if (type == OBJ_BLOB && size > big_file_threshold) + buf = fixed_buf; + else + buf = xmallocz(size); + + memset(&stream, 0, sizeof(stream)); + git_inflate_init(&stream); + stream.next_out = buf; + stream.avail_out = buf == fixed_buf ? sizeof(fixed_buf) : size; + + do { + unsigned char *last_out = stream.next_out; + stream.next_in = fill(1); + stream.avail_in = input_len; + status = git_inflate(&stream, 0); + use(input_len - stream.avail_in); + if (oid) + the_hash_algo->update_fn(&c, last_out, stream.next_out - last_out); + if (buf == fixed_buf) { + stream.next_out = buf; + stream.avail_out = sizeof(fixed_buf); + } + } while (status == Z_OK); + if (stream.total_out != size || status != Z_STREAM_END) + bad_object(offset, _("inflate returned %d"), status); + git_inflate_end(&stream); + if (oid) + the_hash_algo->final_oid_fn(oid, &c); + return buf == fixed_buf ? NULL : buf; +} + +static void *unpack_raw_entry(struct object_entry *obj, + off_t *ofs_offset, + struct object_id *ref_oid, + struct object_id *oid) +{ + unsigned char *p; + unsigned long size, c; + off_t base_offset; + unsigned shift; + void *data; + + obj->idx.offset = consumed_bytes; + input_crc32 = crc32(0, NULL, 0); + + p = fill(1); + c = *p; + use(1); + obj->type = (c >> 4) & 7; + size = (c & 15); + shift = 4; + while (c & 0x80) { + p = fill(1); + c = *p; + use(1); + size += (c & 0x7f) << shift; + shift += 7; + } + obj->size = size; + + switch (obj->type) { + case OBJ_REF_DELTA: + oidread(ref_oid, fill(the_hash_algo->rawsz)); + use(the_hash_algo->rawsz); + break; + case OBJ_OFS_DELTA: + p = fill(1); + c = *p; + use(1); + base_offset = c & 127; + while (c & 128) { + base_offset += 1; + if (!base_offset || MSB(base_offset, 7)) + bad_object(obj->idx.offset, _("offset value overflow for delta base object")); + p = fill(1); + c = *p; + use(1); + base_offset = (base_offset << 7) + (c & 127); + } + *ofs_offset = obj->idx.offset - base_offset; + if (*ofs_offset <= 0 || *ofs_offset >= obj->idx.offset) + bad_object(obj->idx.offset, _("delta base offset is out of bound")); + break; + case OBJ_COMMIT: + case OBJ_TREE: + case OBJ_BLOB: + case OBJ_TAG: + break; + default: + bad_object(obj->idx.offset, _("unknown object type %d"), obj->type); + } + obj->hdr_size = consumed_bytes - obj->idx.offset; + + data = unpack_entry_data(obj->idx.offset, obj->size, obj->type, oid); + obj->idx.crc32 = input_crc32; + return data; +} + +static void *unpack_data(struct object_entry *obj, + int (*consume)(const unsigned char *, unsigned long, void *), + void *cb_data) +{ + off_t from = obj[0].idx.offset + obj[0].hdr_size; + off_t len = obj[1].idx.offset - from; + unsigned char *data, *inbuf; + git_zstream stream; + int status; + + data = xmallocz(consume ? 64*1024 : obj->size); + inbuf = xmalloc((len < 64*1024) ? (int)len : 64*1024); + + memset(&stream, 0, sizeof(stream)); + git_inflate_init(&stream); + stream.next_out = data; + stream.avail_out = consume ? 64*1024 : obj->size; + + do { + ssize_t n = (len < 64*1024) ? (ssize_t)len : 64*1024; + n = xpread(get_thread_data()->pack_fd, inbuf, n, from); + if (n < 0) + die_errno(_("cannot pread pack file")); + if (!n) + die(Q_("premature end of pack file, %"PRIuMAX" byte missing", + "premature end of pack file, %"PRIuMAX" bytes missing", + len), + (uintmax_t)len); + from += n; + len -= n; + stream.next_in = inbuf; + stream.avail_in = n; + if (!consume) + status = git_inflate(&stream, 0); + else { + do { + status = git_inflate(&stream, 0); + if (consume(data, stream.next_out - data, cb_data)) { + free(inbuf); + free(data); + return NULL; + } + stream.next_out = data; + stream.avail_out = 64*1024; + } while (status == Z_OK && stream.avail_in); + } + } while (len && status == Z_OK && !stream.avail_in); + + /* This has been inflated OK when first encountered, so... */ + if (status != Z_STREAM_END || stream.total_out != obj->size) + die(_("serious inflate inconsistency")); + + git_inflate_end(&stream); + free(inbuf); + if (consume) { + FREE_AND_NULL(data); + } + return data; +} + +static void *get_data_from_pack(struct object_entry *obj) +{ + return unpack_data(obj, NULL, NULL); +} + +static int compare_ofs_delta_bases(off_t offset1, off_t offset2, + enum object_type type1, + enum object_type type2) +{ + int cmp = type1 - type2; + if (cmp) + return cmp; + return offset1 < offset2 ? -1 : + offset1 > offset2 ? 1 : + 0; +} + +static int find_ofs_delta(const off_t offset) +{ + int first = 0, last = nr_ofs_deltas; + + while (first < last) { + int next = first + (last - first) / 2; + struct ofs_delta_entry *delta = &ofs_deltas[next]; + int cmp; + + cmp = compare_ofs_delta_bases(offset, delta->offset, + OBJ_OFS_DELTA, + objects[delta->obj_no].type); + if (!cmp) + return next; + if (cmp < 0) { + last = next; + continue; + } + first = next+1; + } + return -first-1; +} + +static void find_ofs_delta_children(off_t offset, + int *first_index, int *last_index) +{ + int first = find_ofs_delta(offset); + int last = first; + int end = nr_ofs_deltas - 1; + + if (first < 0) { + *first_index = 0; + *last_index = -1; + return; + } + while (first > 0 && ofs_deltas[first - 1].offset == offset) + --first; + while (last < end && ofs_deltas[last + 1].offset == offset) + ++last; + *first_index = first; + *last_index = last; +} + +static int compare_ref_delta_bases(const struct object_id *oid1, + const struct object_id *oid2, + enum object_type type1, + enum object_type type2) +{ + int cmp = type1 - type2; + if (cmp) + return cmp; + return oidcmp(oid1, oid2); +} + +static int find_ref_delta(const struct object_id *oid) +{ + int first = 0, last = nr_ref_deltas; + + while (first < last) { + int next = first + (last - first) / 2; + struct ref_delta_entry *delta = &ref_deltas[next]; + int cmp; + + cmp = compare_ref_delta_bases(oid, &delta->oid, + OBJ_REF_DELTA, + objects[delta->obj_no].type); + if (!cmp) + return next; + if (cmp < 0) { + last = next; + continue; + } + first = next+1; + } + return -first-1; +} + +static void find_ref_delta_children(const struct object_id *oid, + int *first_index, int *last_index) +{ + int first = find_ref_delta(oid); + int last = first; + int end = nr_ref_deltas - 1; + + if (first < 0) { + *first_index = 0; + *last_index = -1; + return; + } + while (first > 0 && oideq(&ref_deltas[first - 1].oid, oid)) + --first; + while (last < end && oideq(&ref_deltas[last + 1].oid, oid)) + ++last; + *first_index = first; + *last_index = last; +} + +struct compare_data { + struct object_entry *entry; + struct git_istream *st; + unsigned char *buf; + unsigned long buf_size; +}; + +static int compare_objects(const unsigned char *buf, unsigned long size, + void *cb_data) +{ + struct compare_data *data = cb_data; + + if (data->buf_size < size) { + free(data->buf); + data->buf = xmalloc(size); + data->buf_size = size; + } + + while (size) { + ssize_t len = read_istream(data->st, data->buf, size); + if (len == 0) + die(_("SHA1 COLLISION FOUND WITH %s !"), + oid_to_hex(&data->entry->idx.oid)); + if (len < 0) + die(_("unable to read %s"), + oid_to_hex(&data->entry->idx.oid)); + if (memcmp(buf, data->buf, len)) + die(_("SHA1 COLLISION FOUND WITH %s !"), + oid_to_hex(&data->entry->idx.oid)); + size -= len; + buf += len; + } + return 0; +} + +static int check_collison(struct object_entry *entry) +{ + struct compare_data data; + enum object_type type; + unsigned long size; + + if (entry->size <= big_file_threshold || entry->type != OBJ_BLOB) + return -1; + + memset(&data, 0, sizeof(data)); + data.entry = entry; + data.st = open_istream(the_repository, &entry->idx.oid, &type, &size, + NULL); + if (!data.st) + return -1; + if (size != entry->size || type != entry->type) + die(_("SHA1 COLLISION FOUND WITH %s !"), + oid_to_hex(&entry->idx.oid)); + unpack_data(entry, compare_objects, &data); + close_istream(data.st); + free(data.buf); + return 0; +} + +static void sha1_object(const void *data, struct object_entry *obj_entry, + unsigned long size, enum object_type type, + const struct object_id *oid) +{ + void *new_data = NULL; + int collision_test_needed = 0; + + assert(data || obj_entry); + + if (startup_info->have_repository) { + read_lock(); + collision_test_needed = + repo_has_object_file_with_flags(the_repository, oid, + OBJECT_INFO_QUICK); + read_unlock(); + } + + if (collision_test_needed && !data) { + read_lock(); + if (!check_collison(obj_entry)) + collision_test_needed = 0; + read_unlock(); + } + if (collision_test_needed) { + void *has_data; + enum object_type has_type; + unsigned long has_size; + read_lock(); + has_type = oid_object_info(the_repository, oid, &has_size); + if (has_type < 0) + die(_("cannot read existing object info %s"), oid_to_hex(oid)); + if (has_type != type || has_size != size) + die(_("SHA1 COLLISION FOUND WITH %s !"), oid_to_hex(oid)); + has_data = repo_read_object_file(the_repository, oid, + &has_type, &has_size); + read_unlock(); + if (!data) + data = new_data = get_data_from_pack(obj_entry); + if (!has_data) + die(_("cannot read existing object %s"), oid_to_hex(oid)); + if (size != has_size || type != has_type || + memcmp(data, has_data, size) != 0) + die(_("SHA1 COLLISION FOUND WITH %s !"), oid_to_hex(oid)); + free(has_data); + } + + if (strict || do_fsck_object) { + read_lock(); + if (type == OBJ_BLOB) { + struct blob *blob = lookup_blob(the_repository, oid); + if (blob) + blob->object.flags |= FLAG_CHECKED; + else + die(_("invalid blob object %s"), oid_to_hex(oid)); + if (do_fsck_object && + fsck_object(&blob->object, (void *)data, size, &fsck_options)) + die(_("fsck error in packed object")); + } else { + struct object *obj; + int eaten; + void *buf = (void *) data; + + assert(data && "data can only be NULL for large _blobs_"); + + /* + * we do not need to free the memory here, as the + * buf is deleted by the caller. + */ + obj = parse_object_buffer(the_repository, oid, type, + size, buf, + &eaten); + if (!obj) + die(_("invalid %s"), type_name(type)); + if (do_fsck_object && + fsck_object(obj, buf, size, &fsck_options)) + die(_("fsck error in packed object")); + if (strict && fsck_walk(obj, NULL, &fsck_options)) + die(_("Not all child objects of %s are reachable"), oid_to_hex(&obj->oid)); + + if (obj->type == OBJ_TREE) { + struct tree *item = (struct tree *) obj; + item->buffer = NULL; + obj->parsed = 0; + } + if (obj->type == OBJ_COMMIT) { + struct commit *commit = (struct commit *) obj; + if (detach_commit_buffer(commit, NULL) != data) + BUG("parse_object_buffer transmogrified our buffer"); + } + obj->flags |= FLAG_CHECKED; + } + read_unlock(); + } + + free(new_data); +} + +/* + * Ensure that this node has been reconstructed and return its contents. + * + * In the typical and best case, this node would already be reconstructed + * (through the invocation to resolve_delta() in threaded_second_pass()) and it + * would not be pruned. However, if pruning of this node was necessary due to + * reaching delta_base_cache_limit, this function will find the closest + * ancestor with reconstructed data that has not been pruned (or if there is + * none, the ultimate base object), and reconstruct each node in the delta + * chain in order to generate the reconstructed data for this node. + */ +static void *get_base_data(struct base_data *c) +{ + if (!c->data) { + struct object_entry *obj = c->obj; + struct base_data **delta = NULL; + int delta_nr = 0, delta_alloc = 0; + + while (is_delta_type(c->obj->type) && !c->data) { + ALLOC_GROW(delta, delta_nr + 1, delta_alloc); + delta[delta_nr++] = c; + c = c->base; + } + if (!delta_nr) { + c->data = get_data_from_pack(obj); + c->size = obj->size; + base_cache_used += c->size; + prune_base_data(c); + } + for (; delta_nr > 0; delta_nr--) { + void *base, *raw; + c = delta[delta_nr - 1]; + obj = c->obj; + base = get_base_data(c->base); + raw = get_data_from_pack(obj); + c->data = patch_delta( + base, c->base->size, + raw, obj->size, + &c->size); + free(raw); + if (!c->data) + bad_object(obj->idx.offset, _("failed to apply delta")); + base_cache_used += c->size; + prune_base_data(c); + } + free(delta); + } + return c->data; +} + +static struct base_data *make_base(struct object_entry *obj, + struct base_data *parent) +{ + struct base_data *base = xcalloc(1, sizeof(struct base_data)); + base->base = parent; + base->obj = obj; + find_ref_delta_children(&obj->idx.oid, + &base->ref_first, &base->ref_last); + find_ofs_delta_children(obj->idx.offset, + &base->ofs_first, &base->ofs_last); + base->children_remaining = base->ref_last - base->ref_first + + base->ofs_last - base->ofs_first + 2; + return base; +} + +static struct base_data *resolve_delta(struct object_entry *delta_obj, + struct base_data *base) +{ + void *delta_data, *result_data; + struct base_data *result; + unsigned long result_size; + + if (show_stat) { + int i = delta_obj - objects; + int j = base->obj - objects; + obj_stat[i].delta_depth = obj_stat[j].delta_depth + 1; + deepest_delta_lock(); + if (deepest_delta < obj_stat[i].delta_depth) + deepest_delta = obj_stat[i].delta_depth; + deepest_delta_unlock(); + obj_stat[i].base_object_no = j; + } + delta_data = get_data_from_pack(delta_obj); + assert(base->data); + result_data = patch_delta(base->data, base->size, + delta_data, delta_obj->size, &result_size); + free(delta_data); + if (!result_data) + bad_object(delta_obj->idx.offset, _("failed to apply delta")); + hash_object_file(the_hash_algo, result_data, result_size, + delta_obj->real_type, &delta_obj->idx.oid); + sha1_object(result_data, NULL, result_size, delta_obj->real_type, + &delta_obj->idx.oid); + + result = make_base(delta_obj, base); + result->data = result_data; + result->size = result_size; + + counter_lock(); + nr_resolved_deltas++; + counter_unlock(); + + return result; +} + +static int compare_ofs_delta_entry(const void *a, const void *b) +{ + const struct ofs_delta_entry *delta_a = a; + const struct ofs_delta_entry *delta_b = b; + + return delta_a->offset < delta_b->offset ? -1 : + delta_a->offset > delta_b->offset ? 1 : + 0; +} + +static int compare_ref_delta_entry(const void *a, const void *b) +{ + const struct ref_delta_entry *delta_a = a; + const struct ref_delta_entry *delta_b = b; + + return oidcmp(&delta_a->oid, &delta_b->oid); +} + +static void *threaded_second_pass(void *data) +{ + if (data) + set_thread_data(data); + for (;;) { + struct base_data *parent = NULL; + struct object_entry *child_obj; + struct base_data *child; + + counter_lock(); + display_progress(progress, nr_resolved_deltas); + counter_unlock(); + + work_lock(); + if (list_empty(&work_head)) { + /* + * Take an object from the object array. + */ + while (nr_dispatched < nr_objects && + is_delta_type(objects[nr_dispatched].type)) + nr_dispatched++; + if (nr_dispatched >= nr_objects) { + work_unlock(); + break; + } + child_obj = &objects[nr_dispatched++]; + } else { + /* + * Peek at the top of the stack, and take a child from + * it. + */ + parent = list_first_entry(&work_head, struct base_data, + list); + + if (parent->ref_first <= parent->ref_last) { + int offset = ref_deltas[parent->ref_first++].obj_no; + child_obj = objects + offset; + if (child_obj->real_type != OBJ_REF_DELTA) + die("REF_DELTA at offset %"PRIuMAX" already resolved (duplicate base %s?)", + (uintmax_t) child_obj->idx.offset, + oid_to_hex(&parent->obj->idx.oid)); + child_obj->real_type = parent->obj->real_type; + } else { + child_obj = objects + + ofs_deltas[parent->ofs_first++].obj_no; + assert(child_obj->real_type == OBJ_OFS_DELTA); + child_obj->real_type = parent->obj->real_type; + } + + if (parent->ref_first > parent->ref_last && + parent->ofs_first > parent->ofs_last) { + /* + * This parent has run out of children, so move + * it to done_head. + */ + list_del(&parent->list); + list_add(&parent->list, &done_head); + } + + /* + * Ensure that the parent has data, since we will need + * it later. + * + * NEEDSWORK: If parent data needs to be reloaded, this + * prolongs the time that the current thread spends in + * the mutex. A mitigating factor is that parent data + * needs to be reloaded only if the delta base cache + * limit is exceeded, so in the typical case, this does + * not happen. + */ + get_base_data(parent); + parent->retain_data++; + } + work_unlock(); + + if (parent) { + child = resolve_delta(child_obj, parent); + if (!child->children_remaining) + FREE_AND_NULL(child->data); + } else { + child = make_base(child_obj, NULL); + if (child->children_remaining) { + /* + * Since this child has its own delta children, + * we will need this data in the future. + * Inflate now so that future iterations will + * have access to this object's data while + * outside the work mutex. + */ + child->data = get_data_from_pack(child_obj); + child->size = child_obj->size; + } + } + + work_lock(); + if (parent) + parent->retain_data--; + if (child->data) { + /* + * This child has its own children, so add it to + * work_head. + */ + list_add(&child->list, &work_head); + base_cache_used += child->size; + prune_base_data(NULL); + free_base_data(child); + } else { + /* + * This child does not have its own children. It may be + * the last descendant of its ancestors; free those + * that we can. + */ + struct base_data *p = parent; + + while (p) { + struct base_data *next_p; + + p->children_remaining--; + if (p->children_remaining) + break; + + next_p = p->base; + free_base_data(p); + list_del(&p->list); + free(p); + + p = next_p; + } + FREE_AND_NULL(child); + } + work_unlock(); + } + return NULL; +} + +/* + * First pass: + * - find locations of all objects; + * - calculate SHA1 of all non-delta objects; + * - remember base (SHA1 or offset) for all deltas. + */ +static void parse_pack_objects(unsigned char *hash) +{ + int i, nr_delays = 0; + struct ofs_delta_entry *ofs_delta = ofs_deltas; + struct object_id ref_delta_oid; + struct stat st; + git_hash_ctx tmp_ctx; + + if (verbose) + progress = start_progress( + progress_title ? progress_title : + from_stdin ? _("Receiving objects") : _("Indexing objects"), + nr_objects); + for (i = 0; i < nr_objects; i++) { + struct object_entry *obj = &objects[i]; + void *data = unpack_raw_entry(obj, &ofs_delta->offset, + &ref_delta_oid, + &obj->idx.oid); + obj->real_type = obj->type; + if (obj->type == OBJ_OFS_DELTA) { + nr_ofs_deltas++; + ofs_delta->obj_no = i; + ofs_delta++; + } else if (obj->type == OBJ_REF_DELTA) { + ALLOC_GROW(ref_deltas, nr_ref_deltas + 1, ref_deltas_alloc); + oidcpy(&ref_deltas[nr_ref_deltas].oid, &ref_delta_oid); + ref_deltas[nr_ref_deltas].obj_no = i; + nr_ref_deltas++; + } else if (!data) { + /* large blobs, check later */ + obj->real_type = OBJ_BAD; + nr_delays++; + } else + sha1_object(data, NULL, obj->size, obj->type, + &obj->idx.oid); + free(data); + display_progress(progress, i+1); + } + objects[i].idx.offset = consumed_bytes; + stop_progress(&progress); + + /* Check pack integrity */ + flush(); + the_hash_algo->init_fn(&tmp_ctx); + the_hash_algo->clone_fn(&tmp_ctx, &input_ctx); + the_hash_algo->final_fn(hash, &tmp_ctx); + if (!hasheq(fill(the_hash_algo->rawsz), hash)) + die(_("pack is corrupted (SHA1 mismatch)")); + use(the_hash_algo->rawsz); + + /* If input_fd is a file, we should have reached its end now. */ + if (fstat(input_fd, &st)) + die_errno(_("cannot fstat packfile")); + if (S_ISREG(st.st_mode) && + lseek(input_fd, 0, SEEK_CUR) - input_len != st.st_size) + die(_("pack has junk at the end")); + + for (i = 0; i < nr_objects; i++) { + struct object_entry *obj = &objects[i]; + if (obj->real_type != OBJ_BAD) + continue; + obj->real_type = obj->type; + sha1_object(NULL, obj, obj->size, obj->type, + &obj->idx.oid); + nr_delays--; + } + if (nr_delays) + die(_("confusion beyond insanity in parse_pack_objects()")); +} + +/* + * Second pass: + * - for all non-delta objects, look if it is used as a base for + * deltas; + * - if used as a base, uncompress the object and apply all deltas, + * recursively checking if the resulting object is used as a base + * for some more deltas. + */ +static void resolve_deltas(void) +{ + int i; + + if (!nr_ofs_deltas && !nr_ref_deltas) + return; + + /* Sort deltas by base SHA1/offset for fast searching */ + QSORT(ofs_deltas, nr_ofs_deltas, compare_ofs_delta_entry); + QSORT(ref_deltas, nr_ref_deltas, compare_ref_delta_entry); + + if (verbose || show_resolving_progress) + progress = start_progress(_("Resolving deltas"), + nr_ref_deltas + nr_ofs_deltas); + + nr_dispatched = 0; + base_cache_limit = delta_base_cache_limit * nr_threads; + if (nr_threads > 1 || getenv("GIT_FORCE_THREADS")) { + init_thread(); + for (i = 0; i < nr_threads; i++) { + int ret = pthread_create(&thread_data[i].thread, NULL, + threaded_second_pass, thread_data + i); + if (ret) + die(_("unable to create thread: %s"), + strerror(ret)); + } + for (i = 0; i < nr_threads; i++) + pthread_join(thread_data[i].thread, NULL); + cleanup_thread(); + return; + } + threaded_second_pass(¬hread_data); +} + +/* + * Third pass: + * - append objects to convert thin pack to full pack if required + * - write the final pack hash + */ +static void fix_unresolved_deltas(struct hashfile *f); +static void conclude_pack(int fix_thin_pack, const char *curr_pack, unsigned char *pack_hash) +{ + if (nr_ref_deltas + nr_ofs_deltas == nr_resolved_deltas) { + stop_progress(&progress); + /* Flush remaining pack final hash. */ + flush(); + return; + } + + if (fix_thin_pack) { + struct hashfile *f; + unsigned char read_hash[GIT_MAX_RAWSZ], tail_hash[GIT_MAX_RAWSZ]; + struct strbuf msg = STRBUF_INIT; + int nr_unresolved = nr_ofs_deltas + nr_ref_deltas - nr_resolved_deltas; + int nr_objects_initial = nr_objects; + if (nr_unresolved <= 0) + die(_("confusion beyond insanity")); + REALLOC_ARRAY(objects, nr_objects + nr_unresolved + 1); + memset(objects + nr_objects + 1, 0, + nr_unresolved * sizeof(*objects)); + f = hashfd(output_fd, curr_pack); + fix_unresolved_deltas(f); + strbuf_addf(&msg, Q_("completed with %d local object", + "completed with %d local objects", + nr_objects - nr_objects_initial), + nr_objects - nr_objects_initial); + stop_progress_msg(&progress, msg.buf); + strbuf_release(&msg); + finalize_hashfile(f, tail_hash, FSYNC_COMPONENT_PACK, 0); + hashcpy(read_hash, pack_hash); + fixup_pack_header_footer(output_fd, pack_hash, + curr_pack, nr_objects, + read_hash, consumed_bytes-the_hash_algo->rawsz); + if (!hasheq(read_hash, tail_hash)) + die(_("Unexpected tail checksum for %s " + "(disk corruption?)"), curr_pack); + } + if (nr_ofs_deltas + nr_ref_deltas != nr_resolved_deltas) + die(Q_("pack has %d unresolved delta", + "pack has %d unresolved deltas", + nr_ofs_deltas + nr_ref_deltas - nr_resolved_deltas), + nr_ofs_deltas + nr_ref_deltas - nr_resolved_deltas); +} + +static int write_compressed(struct hashfile *f, void *in, unsigned int size) +{ + git_zstream stream; + int status; + unsigned char outbuf[4096]; + + git_deflate_init(&stream, zlib_compression_level); + stream.next_in = in; + stream.avail_in = size; + + do { + stream.next_out = outbuf; + stream.avail_out = sizeof(outbuf); + status = git_deflate(&stream, Z_FINISH); + hashwrite(f, outbuf, sizeof(outbuf) - stream.avail_out); + } while (status == Z_OK); + + if (status != Z_STREAM_END) + die(_("unable to deflate appended object (%d)"), status); + size = stream.total_out; + git_deflate_end(&stream); + return size; +} + +static struct object_entry *append_obj_to_pack(struct hashfile *f, + const unsigned char *sha1, void *buf, + unsigned long size, enum object_type type) +{ + struct object_entry *obj = &objects[nr_objects++]; + unsigned char header[10]; + unsigned long s = size; + int n = 0; + unsigned char c = (type << 4) | (s & 15); + s >>= 4; + while (s) { + header[n++] = c | 0x80; + c = s & 0x7f; + s >>= 7; + } + header[n++] = c; + crc32_begin(f); + hashwrite(f, header, n); + obj[0].size = size; + obj[0].hdr_size = n; + obj[0].type = type; + obj[0].real_type = type; + obj[1].idx.offset = obj[0].idx.offset + n; + obj[1].idx.offset += write_compressed(f, buf, size); + obj[0].idx.crc32 = crc32_end(f); + hashflush(f); + oidread(&obj->idx.oid, sha1); + return obj; +} + +static int delta_pos_compare(const void *_a, const void *_b) +{ + struct ref_delta_entry *a = *(struct ref_delta_entry **)_a; + struct ref_delta_entry *b = *(struct ref_delta_entry **)_b; + return a->obj_no - b->obj_no; +} + +static void fix_unresolved_deltas(struct hashfile *f) +{ + struct ref_delta_entry **sorted_by_pos; + int i; + + /* + * Since many unresolved deltas may well be themselves base objects + * for more unresolved deltas, we really want to include the + * smallest number of base objects that would cover as much delta + * as possible by picking the + * trunc deltas first, allowing for other deltas to resolve without + * additional base objects. Since most base objects are to be found + * before deltas depending on them, a good heuristic is to start + * resolving deltas in the same order as their position in the pack. + */ + ALLOC_ARRAY(sorted_by_pos, nr_ref_deltas); + for (i = 0; i < nr_ref_deltas; i++) + sorted_by_pos[i] = &ref_deltas[i]; + QSORT(sorted_by_pos, nr_ref_deltas, delta_pos_compare); + + if (repo_has_promisor_remote(the_repository)) { + /* + * Prefetch the delta bases. + */ + struct oid_array to_fetch = OID_ARRAY_INIT; + for (i = 0; i < nr_ref_deltas; i++) { + struct ref_delta_entry *d = sorted_by_pos[i]; + if (!oid_object_info_extended(the_repository, &d->oid, + NULL, + OBJECT_INFO_FOR_PREFETCH)) + continue; + oid_array_append(&to_fetch, &d->oid); + } + promisor_remote_get_direct(the_repository, + to_fetch.oid, to_fetch.nr); + oid_array_clear(&to_fetch); + } + + for (i = 0; i < nr_ref_deltas; i++) { + struct ref_delta_entry *d = sorted_by_pos[i]; + enum object_type type; + void *data; + unsigned long size; + + if (objects[d->obj_no].real_type != OBJ_REF_DELTA) + continue; + data = repo_read_object_file(the_repository, &d->oid, &type, + &size); + if (!data) + continue; + + if (check_object_signature(the_repository, &d->oid, data, size, + type) < 0) + die(_("local object %s is corrupt"), oid_to_hex(&d->oid)); + + /* + * Add this as an object to the objects array and call + * threaded_second_pass() (which will pick up the added + * object). + */ + append_obj_to_pack(f, d->oid.hash, data, size, type); + free(data); + threaded_second_pass(NULL); + + display_progress(progress, nr_resolved_deltas); + } + free(sorted_by_pos); +} + +static const char *derive_filename(const char *pack_name, const char *strip, + const char *suffix, struct strbuf *buf) +{ + size_t len; + if (!strip_suffix(pack_name, strip, &len) || !len || + pack_name[len - 1] != '.') + die(_("packfile name '%s' does not end with '.%s'"), + pack_name, strip); + strbuf_add(buf, pack_name, len); + strbuf_addstr(buf, suffix); + return buf->buf; +} + +static void write_special_file(const char *suffix, const char *msg, + const char *pack_name, const unsigned char *hash, + const char **report) +{ + struct strbuf name_buf = STRBUF_INIT; + const char *filename; + int fd; + int msg_len = strlen(msg); + + if (pack_name) + filename = derive_filename(pack_name, "pack", suffix, &name_buf); + else + filename = odb_pack_name(&name_buf, hash, suffix); + + fd = odb_pack_keep(filename); + if (fd < 0) { + if (errno != EEXIST) + die_errno(_("cannot write %s file '%s'"), + suffix, filename); + } else { + if (msg_len > 0) { + write_or_die(fd, msg, msg_len); + write_or_die(fd, "\n", 1); + } + if (close(fd) != 0) + die_errno(_("cannot close written %s file '%s'"), + suffix, filename); + if (report) + *report = suffix; + } + strbuf_release(&name_buf); +} + +static void rename_tmp_packfile(const char **final_name, + const char *curr_name, + struct strbuf *name, unsigned char *hash, + const char *ext, int make_read_only_if_same) +{ + if (*final_name != curr_name) { + if (!*final_name) + *final_name = odb_pack_name(name, hash, ext); + if (finalize_object_file(curr_name, *final_name)) + die(_("unable to rename temporary '*.%s' file to '%s'"), + ext, *final_name); + } else if (make_read_only_if_same) { + chmod(*final_name, 0444); + } +} + +static void final(const char *final_pack_name, const char *curr_pack_name, + const char *final_index_name, const char *curr_index_name, + const char *final_rev_index_name, const char *curr_rev_index_name, + const char *keep_msg, const char *promisor_msg, + unsigned char *hash) +{ + const char *report = "pack"; + struct strbuf pack_name = STRBUF_INIT; + struct strbuf index_name = STRBUF_INIT; + struct strbuf rev_index_name = STRBUF_INIT; + int err; + + if (!from_stdin) { + close(input_fd); + } else { + fsync_component_or_die(FSYNC_COMPONENT_PACK, output_fd, curr_pack_name); + err = close(output_fd); + if (err) + die_errno(_("error while closing pack file")); + } + + if (keep_msg) + write_special_file("keep", keep_msg, final_pack_name, hash, + &report); + if (promisor_msg) + write_special_file("promisor", promisor_msg, final_pack_name, + hash, NULL); + + rename_tmp_packfile(&final_pack_name, curr_pack_name, &pack_name, + hash, "pack", from_stdin); + if (curr_rev_index_name) + rename_tmp_packfile(&final_rev_index_name, curr_rev_index_name, + &rev_index_name, hash, "rev", 1); + rename_tmp_packfile(&final_index_name, curr_index_name, &index_name, + hash, "idx", 1); + + if (do_fsck_object) { + struct packed_git *p; + p = add_packed_git(final_index_name, strlen(final_index_name), 0); + if (p) + install_packed_git(the_repository, p); + } + + if (!from_stdin) { + printf("%s\n", hash_to_hex(hash)); + } else { + struct strbuf buf = STRBUF_INIT; + + strbuf_addf(&buf, "%s\t%s\n", report, hash_to_hex(hash)); + write_or_die(1, buf.buf, buf.len); + strbuf_release(&buf); + + /* + * Let's just mimic git-unpack-objects here and write + * the last part of the input buffer to stdout. + */ + while (input_len) { + err = xwrite(1, input_buffer + input_offset, input_len); + if (err <= 0) + break; + input_len -= err; + input_offset += err; + } + } + + strbuf_release(&rev_index_name); + strbuf_release(&index_name); + strbuf_release(&pack_name); +} + +static int git_index_pack_config(const char *k, const char *v, + const struct config_context *ctx, void *cb) +{ + struct pack_idx_option *opts = cb; + + if (!strcmp(k, "pack.indexversion")) { + opts->version = git_config_int(k, v, ctx->kvi); + if (opts->version > 2) + die(_("bad pack.indexVersion=%"PRIu32), opts->version); + return 0; + } + if (!strcmp(k, "pack.threads")) { + nr_threads = git_config_int(k, v, ctx->kvi); + if (nr_threads < 0) + die(_("invalid number of threads specified (%d)"), + nr_threads); + if (!HAVE_THREADS && nr_threads != 1) { + warning(_("no threads support, ignoring %s"), k); + nr_threads = 1; + } + return 0; + } + if (!strcmp(k, "pack.writereverseindex")) { + if (git_config_bool(k, v)) + opts->flags |= WRITE_REV; + else + opts->flags &= ~WRITE_REV; + } + return git_default_config(k, v, ctx, cb); +} + +static int cmp_uint32(const void *a_, const void *b_) +{ + uint32_t a = *((uint32_t *)a_); + uint32_t b = *((uint32_t *)b_); + + return (a < b) ? -1 : (a != b); +} + +static void read_v2_anomalous_offsets(struct packed_git *p, + struct pack_idx_option *opts) +{ + const uint32_t *idx1, *idx2; + uint32_t i; + + /* The address of the 4-byte offset table */ + idx1 = (((const uint32_t *)((const uint8_t *)p->index_data + p->crc_offset)) + + (size_t)p->num_objects /* CRC32 table */ + ); + + /* The address of the 8-byte offset table */ + idx2 = idx1 + p->num_objects; + + for (i = 0; i < p->num_objects; i++) { + uint32_t off = ntohl(idx1[i]); + if (!(off & 0x80000000)) + continue; + off = off & 0x7fffffff; + check_pack_index_ptr(p, &idx2[off * 2]); + if (idx2[off * 2]) + continue; + /* + * The real offset is ntohl(idx2[off * 2]) in high 4 + * octets, and ntohl(idx2[off * 2 + 1]) in low 4 + * octets. But idx2[off * 2] is Zero!!! + */ + ALLOC_GROW(opts->anomaly, opts->anomaly_nr + 1, opts->anomaly_alloc); + opts->anomaly[opts->anomaly_nr++] = ntohl(idx2[off * 2 + 1]); + } + + QSORT(opts->anomaly, opts->anomaly_nr, cmp_uint32); +} + +static void read_idx_option(struct pack_idx_option *opts, const char *pack_name) +{ + struct packed_git *p = add_packed_git(pack_name, strlen(pack_name), 1); + + if (!p) + die(_("Cannot open existing pack file '%s'"), pack_name); + if (open_pack_index(p)) + die(_("Cannot open existing pack idx file for '%s'"), pack_name); + + /* Read the attributes from the existing idx file */ + opts->version = p->index_version; + + if (opts->version == 2) + read_v2_anomalous_offsets(p, opts); + + /* + * Get rid of the idx file as we do not need it anymore. + * NEEDSWORK: extract this bit from free_pack_by_name() in + * object-file.c, perhaps? It shouldn't matter very much as we + * know we haven't installed this pack (hence we never have + * read anything from it). + */ + close_pack_index(p); + free(p); +} + +static void show_pack_info(int stat_only) +{ + int i, baseobjects = nr_objects - nr_ref_deltas - nr_ofs_deltas; + unsigned long *chain_histogram = NULL; + + if (deepest_delta) + CALLOC_ARRAY(chain_histogram, deepest_delta); + + for (i = 0; i < nr_objects; i++) { + struct object_entry *obj = &objects[i]; + + if (is_delta_type(obj->type)) + chain_histogram[obj_stat[i].delta_depth - 1]++; + if (stat_only) + continue; + printf("%s %-6s %"PRIuMAX" %"PRIuMAX" %"PRIuMAX, + oid_to_hex(&obj->idx.oid), + type_name(obj->real_type), (uintmax_t)obj->size, + (uintmax_t)(obj[1].idx.offset - obj->idx.offset), + (uintmax_t)obj->idx.offset); + if (is_delta_type(obj->type)) { + struct object_entry *bobj = &objects[obj_stat[i].base_object_no]; + printf(" %u %s", obj_stat[i].delta_depth, + oid_to_hex(&bobj->idx.oid)); + } + putchar('\n'); + } + + if (baseobjects) + printf_ln(Q_("non delta: %d object", + "non delta: %d objects", + baseobjects), + baseobjects); + for (i = 0; i < deepest_delta; i++) { + if (!chain_histogram[i]) + continue; + printf_ln(Q_("chain length = %d: %lu object", + "chain length = %d: %lu objects", + chain_histogram[i]), + i + 1, + chain_histogram[i]); + } + free(chain_histogram); +} + +int cmd_index_pack(int argc, const char **argv, const char *prefix) +{ + int i, fix_thin_pack = 0, verify = 0, stat_only = 0, rev_index; + const char *curr_index; + const char *curr_rev_index = NULL; + const char *index_name = NULL, *pack_name = NULL, *rev_index_name = NULL; + const char *keep_msg = NULL; + const char *promisor_msg = NULL; + struct strbuf index_name_buf = STRBUF_INIT; + struct strbuf rev_index_name_buf = STRBUF_INIT; + struct pack_idx_entry **idx_objects; + struct pack_idx_option opts; + unsigned char pack_hash[GIT_MAX_RAWSZ]; + unsigned foreign_nr = 1; /* zero is a "good" value, assume bad */ + int report_end_of_input = 0; + int hash_algo = 0; + + /* + * index-pack never needs to fetch missing objects except when + * REF_DELTA bases are missing (which are explicitly handled). It only + * accesses the repo to do hash collision checks and to check which + * REF_DELTA bases need to be fetched. + */ + fetch_if_missing = 0; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage(index_pack_usage); + + disable_replace_refs(); + fsck_options.walk = mark_link; + + reset_pack_idx_option(&opts); + opts.flags |= WRITE_REV; + git_config(git_index_pack_config, &opts); + if (prefix && chdir(prefix)) + die(_("Cannot come back to cwd")); + + if (git_env_bool(GIT_TEST_NO_WRITE_REV_INDEX, 0)) + rev_index = 0; + else + rev_index = !!(opts.flags & (WRITE_REV_VERIFY | WRITE_REV)); + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (*arg == '-') { + if (!strcmp(arg, "--stdin")) { + from_stdin = 1; + } else if (!strcmp(arg, "--fix-thin")) { + fix_thin_pack = 1; + } else if (skip_to_optional_arg(arg, "--strict", &arg)) { + strict = 1; + do_fsck_object = 1; + fsck_set_msg_types(&fsck_options, arg); + } else if (!strcmp(arg, "--check-self-contained-and-connected")) { + strict = 1; + check_self_contained_and_connected = 1; + } else if (!strcmp(arg, "--fsck-objects")) { + do_fsck_object = 1; + } else if (!strcmp(arg, "--verify")) { + verify = 1; + } else if (!strcmp(arg, "--verify-stat")) { + verify = 1; + show_stat = 1; + } else if (!strcmp(arg, "--verify-stat-only")) { + verify = 1; + show_stat = 1; + stat_only = 1; + } else if (skip_to_optional_arg(arg, "--keep", &keep_msg)) { + ; /* nothing to do */ + } else if (skip_to_optional_arg(arg, "--promisor", &promisor_msg)) { + ; /* already parsed */ + } else if (starts_with(arg, "--threads=")) { + char *end; + nr_threads = strtoul(arg+10, &end, 0); + if (!arg[10] || *end || nr_threads < 0) + usage(index_pack_usage); + if (!HAVE_THREADS && nr_threads != 1) { + warning(_("no threads support, ignoring %s"), arg); + nr_threads = 1; + } + } else if (starts_with(arg, "--pack_header=")) { + struct pack_header *hdr; + char *c; + + hdr = (struct pack_header *)input_buffer; + hdr->hdr_signature = htonl(PACK_SIGNATURE); + hdr->hdr_version = htonl(strtoul(arg + 14, &c, 10)); + if (*c != ',') + die(_("bad %s"), arg); + hdr->hdr_entries = htonl(strtoul(c + 1, &c, 10)); + if (*c) + die(_("bad %s"), arg); + input_len = sizeof(*hdr); + } else if (!strcmp(arg, "-v")) { + verbose = 1; + } else if (!strcmp(arg, "--progress-title")) { + if (progress_title || (i+1) >= argc) + usage(index_pack_usage); + progress_title = argv[++i]; + } else if (!strcmp(arg, "--show-resolving-progress")) { + show_resolving_progress = 1; + } else if (!strcmp(arg, "--report-end-of-input")) { + report_end_of_input = 1; + } else if (!strcmp(arg, "-o")) { + if (index_name || (i+1) >= argc) + usage(index_pack_usage); + index_name = argv[++i]; + } else if (starts_with(arg, "--index-version=")) { + char *c; + opts.version = strtoul(arg + 16, &c, 10); + if (opts.version > 2) + die(_("bad %s"), arg); + if (*c == ',') + opts.off32_limit = strtoul(c+1, &c, 0); + if (*c || opts.off32_limit & 0x80000000) + die(_("bad %s"), arg); + } else if (skip_prefix(arg, "--max-input-size=", &arg)) { + max_input_size = strtoumax(arg, NULL, 10); + } else if (skip_prefix(arg, "--object-format=", &arg)) { + hash_algo = hash_algo_by_name(arg); + if (hash_algo == GIT_HASH_UNKNOWN) + die(_("unknown hash algorithm '%s'"), arg); + repo_set_hash_algo(the_repository, hash_algo); + } else if (!strcmp(arg, "--rev-index")) { + rev_index = 1; + } else if (!strcmp(arg, "--no-rev-index")) { + rev_index = 0; + } else + usage(index_pack_usage); + continue; + } + + if (pack_name) + usage(index_pack_usage); + pack_name = arg; + } + + if (!pack_name && !from_stdin) + usage(index_pack_usage); + if (fix_thin_pack && !from_stdin) + die(_("the option '%s' requires '%s'"), "--fix-thin", "--stdin"); + if (from_stdin && !startup_info->have_repository) + die(_("--stdin requires a git repository")); + if (from_stdin && hash_algo) + die(_("options '%s' and '%s' cannot be used together"), "--object-format", "--stdin"); + if (!index_name && pack_name) + index_name = derive_filename(pack_name, "pack", "idx", &index_name_buf); + + opts.flags &= ~(WRITE_REV | WRITE_REV_VERIFY); + if (rev_index) { + opts.flags |= verify ? WRITE_REV_VERIFY : WRITE_REV; + if (index_name) + rev_index_name = derive_filename(index_name, + "idx", "rev", + &rev_index_name_buf); + } + + if (verify) { + if (!index_name) + die(_("--verify with no packfile name given")); + read_idx_option(&opts, index_name); + opts.flags |= WRITE_IDX_VERIFY | WRITE_IDX_STRICT; + } + if (strict) + opts.flags |= WRITE_IDX_STRICT; + + if (HAVE_THREADS && !nr_threads) { + nr_threads = online_cpus(); + /* + * Experiments show that going above 20 threads doesn't help, + * no matter how many cores you have. Below that, we tend to + * max at half the number of online_cpus(), presumably because + * half of those are hyperthreads rather than full cores. We'll + * never reduce the level below "3", though, to match a + * historical value that nobody complained about. + */ + if (nr_threads < 4) + ; /* too few cores to consider capping */ + else if (nr_threads < 6) + nr_threads = 3; /* historic cap */ + else if (nr_threads < 40) + nr_threads /= 2; + else + nr_threads = 20; /* hard cap */ + } + + curr_pack = open_pack_file(pack_name); + parse_pack_header(); + CALLOC_ARRAY(objects, st_add(nr_objects, 1)); + if (show_stat) + CALLOC_ARRAY(obj_stat, st_add(nr_objects, 1)); + CALLOC_ARRAY(ofs_deltas, nr_objects); + parse_pack_objects(pack_hash); + if (report_end_of_input) + write_in_full(2, "\0", 1); + resolve_deltas(); + conclude_pack(fix_thin_pack, curr_pack, pack_hash); + free(ofs_deltas); + free(ref_deltas); + if (strict) + foreign_nr = check_objects(); + + if (show_stat) + show_pack_info(stat_only); + + ALLOC_ARRAY(idx_objects, nr_objects); + for (i = 0; i < nr_objects; i++) + idx_objects[i] = &objects[i].idx; + curr_index = write_idx_file(index_name, idx_objects, nr_objects, &opts, pack_hash); + if (rev_index) + curr_rev_index = write_rev_file(rev_index_name, idx_objects, + nr_objects, pack_hash, + opts.flags); + free(idx_objects); + + if (!verify) + final(pack_name, curr_pack, + index_name, curr_index, + rev_index_name, curr_rev_index, + keep_msg, promisor_msg, + pack_hash); + else + close(input_fd); + + if (do_fsck_object && fsck_finish(&fsck_options)) + die(_("fsck error in pack objects")); + + free(opts.anomaly); + free(objects); + strbuf_release(&index_name_buf); + strbuf_release(&rev_index_name_buf); + if (!pack_name) + free((void *) curr_pack); + if (!index_name) + free((void *) curr_index); + if (!rev_index_name) + free((void *) curr_rev_index); + + /* + * Let the caller know this pack is not self contained + */ + if (check_self_contained_and_connected && foreign_nr) + return 1; + + return 0; +} diff --git a/builtin/init-db.c b/builtin/init-db.c new file mode 100644 index 0000000..cb727c8 --- /dev/null +++ b/builtin/init-db.c @@ -0,0 +1,240 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "builtin.h" +#include "abspath.h" +#include "config.h" +#include "environment.h" +#include "gettext.h" +#include "object-file.h" +#include "parse-options.h" +#include "path.h" +#include "setup.h" +#include "strbuf.h" + +static int guess_repository_type(const char *git_dir) +{ + const char *slash; + char *cwd; + int cwd_is_git_dir; + + /* + * "GIT_DIR=. git init" is always bare. + * "GIT_DIR=`pwd` git init" too. + */ + if (!strcmp(".", git_dir)) + return 1; + cwd = xgetcwd(); + cwd_is_git_dir = !strcmp(git_dir, cwd); + free(cwd); + if (cwd_is_git_dir) + return 1; + /* + * "GIT_DIR=.git or GIT_DIR=something/.git is usually not. + */ + if (!strcmp(git_dir, ".git")) + return 0; + slash = strrchr(git_dir, '/'); + if (slash && !strcmp(slash, "/.git")) + return 0; + + /* + * Otherwise it is often bare. At this point + * we are just guessing. + */ + return 1; +} + +static int shared_callback(const struct option *opt, const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + *((int *) opt->value) = (arg) ? git_config_perm("arg", arg) : PERM_GROUP; + return 0; +} + +static const char *const init_db_usage[] = { + N_("git init [-q | --quiet] [--bare] [--template=<template-directory>]\n" + " [--separate-git-dir <git-dir>] [--object-format=<format>]\n" + " [-b <branch-name> | --initial-branch=<branch-name>]\n" + " [--shared[=<permissions>]] [<directory>]"), + NULL +}; + +/* + * If you want to, you can share the DB area with any number of branches. + * That has advantages: you can save space by sharing all the SHA1 objects. + * On the other hand, it might just make lookup slower and messier. You + * be the judge. The default case is to have one DB per managed directory. + */ +int cmd_init_db(int argc, const char **argv, const char *prefix) +{ + const char *git_dir; + const char *real_git_dir = NULL; + const char *work_tree; + const char *template_dir = NULL; + unsigned int flags = 0; + const char *object_format = NULL; + const char *initial_branch = NULL; + int hash_algo = GIT_HASH_UNKNOWN; + int init_shared_repository = -1; + const struct option init_db_options[] = { + OPT_STRING(0, "template", &template_dir, N_("template-directory"), + N_("directory from which templates will be used")), + OPT_SET_INT(0, "bare", &is_bare_repository_cfg, + N_("create a bare repository"), 1), + { OPTION_CALLBACK, 0, "shared", &init_shared_repository, + N_("permissions"), + N_("specify that the git repository is to be shared amongst several users"), + PARSE_OPT_OPTARG | PARSE_OPT_NONEG, shared_callback, 0}, + OPT_BIT('q', "quiet", &flags, N_("be quiet"), INIT_DB_QUIET), + OPT_STRING(0, "separate-git-dir", &real_git_dir, N_("gitdir"), + N_("separate git dir from working tree")), + OPT_STRING('b', "initial-branch", &initial_branch, N_("name"), + N_("override the name of the initial branch")), + OPT_STRING(0, "object-format", &object_format, N_("hash"), + N_("specify the hash algorithm to use")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, init_db_options, init_db_usage, 0); + + if (real_git_dir && is_bare_repository_cfg == 1) + die(_("options '%s' and '%s' cannot be used together"), "--separate-git-dir", "--bare"); + + if (real_git_dir && !is_absolute_path(real_git_dir)) + real_git_dir = real_pathdup(real_git_dir, 1); + + if (template_dir && *template_dir && !is_absolute_path(template_dir)) { + template_dir = absolute_pathdup(template_dir); + UNLEAK(template_dir); + } + + if (argc == 1) { + int mkdir_tried = 0; + retry: + if (chdir(argv[0]) < 0) { + if (!mkdir_tried) { + int saved; + /* + * At this point we haven't read any configuration, + * and we know shared_repository should always be 0; + * but just in case we play safe. + */ + saved = get_shared_repository(); + set_shared_repository(0); + switch (safe_create_leading_directories_const(argv[0])) { + case SCLD_OK: + case SCLD_PERMS: + break; + case SCLD_EXISTS: + errno = EEXIST; + /* fallthru */ + default: + die_errno(_("cannot mkdir %s"), argv[0]); + break; + } + set_shared_repository(saved); + if (mkdir(argv[0], 0777) < 0) + die_errno(_("cannot mkdir %s"), argv[0]); + mkdir_tried = 1; + goto retry; + } + die_errno(_("cannot chdir to %s"), argv[0]); + } + } else if (0 < argc) { + usage(init_db_usage[0]); + } + if (is_bare_repository_cfg == 1) { + char *cwd = xgetcwd(); + setenv(GIT_DIR_ENVIRONMENT, cwd, argc > 0); + free(cwd); + } + + if (object_format) { + hash_algo = hash_algo_by_name(object_format); + if (hash_algo == GIT_HASH_UNKNOWN) + die(_("unknown hash algorithm '%s'"), object_format); + } + + if (init_shared_repository != -1) + set_shared_repository(init_shared_repository); + + /* + * GIT_WORK_TREE makes sense only in conjunction with GIT_DIR + * without --bare. Catch the error early. + */ + git_dir = xstrdup_or_null(getenv(GIT_DIR_ENVIRONMENT)); + work_tree = xstrdup_or_null(getenv(GIT_WORK_TREE_ENVIRONMENT)); + if ((!git_dir || is_bare_repository_cfg == 1) && work_tree) + die(_("%s (or --work-tree=<directory>) not allowed without " + "specifying %s (or --git-dir=<directory>)"), + GIT_WORK_TREE_ENVIRONMENT, + GIT_DIR_ENVIRONMENT); + + /* + * Set up the default .git directory contents + */ + if (!git_dir) + git_dir = DEFAULT_GIT_DIR_ENVIRONMENT; + + /* + * When --separate-git-dir is used inside a linked worktree, take + * care to ensure that the common .git/ directory is relocated, not + * the worktree-specific .git/worktrees/<id>/ directory. + */ + if (real_git_dir) { + int err; + const char *p; + struct strbuf sb = STRBUF_INIT; + + p = read_gitfile_gently(git_dir, &err); + if (p && get_common_dir(&sb, p)) { + struct strbuf mainwt = STRBUF_INIT; + + strbuf_addbuf(&mainwt, &sb); + strbuf_strip_suffix(&mainwt, "/.git"); + if (chdir(mainwt.buf) < 0) + die_errno(_("cannot chdir to %s"), mainwt.buf); + strbuf_release(&mainwt); + git_dir = strbuf_detach(&sb, NULL); + } + strbuf_release(&sb); + } + + if (is_bare_repository_cfg < 0) + is_bare_repository_cfg = guess_repository_type(git_dir); + + if (!is_bare_repository_cfg) { + const char *git_dir_parent = strrchr(git_dir, '/'); + if (git_dir_parent) { + char *rel = xstrndup(git_dir, git_dir_parent - git_dir); + git_work_tree_cfg = real_pathdup(rel, 1); + free(rel); + } + if (!git_work_tree_cfg) + git_work_tree_cfg = xgetcwd(); + if (work_tree) + set_git_work_tree(work_tree); + else + set_git_work_tree(git_work_tree_cfg); + if (access(get_git_work_tree(), X_OK)) + die_errno (_("Cannot access work tree '%s'"), + get_git_work_tree()); + } + else { + if (real_git_dir) + die(_("--separate-git-dir incompatible with bare repository")); + if (work_tree) + set_git_work_tree(work_tree); + } + + UNLEAK(real_git_dir); + UNLEAK(git_dir); + UNLEAK(work_tree); + + flags |= INIT_DB_EXIST_OK; + return init_db(git_dir, real_git_dir, template_dir, hash_algo, + initial_branch, init_shared_repository, flags); +} diff --git a/builtin/interpret-trailers.c b/builtin/interpret-trailers.c new file mode 100644 index 0000000..033bd15 --- /dev/null +++ b/builtin/interpret-trailers.c @@ -0,0 +1,145 @@ +/* + * Builtin "git interpret-trailers" + * + * Copyright (c) 2013, 2014 Christian Couder <chriscool@tuxfamily.org> + * + */ + +#include "builtin.h" +#include "gettext.h" +#include "parse-options.h" +#include "string-list.h" +#include "trailer.h" +#include "config.h" + +static const char * const git_interpret_trailers_usage[] = { + N_("git interpret-trailers [--in-place] [--trim-empty]\n" + " [(--trailer (<key>|<keyAlias>)[(=|:)<value>])...]\n" + " [--parse] [<file>...]"), + NULL +}; + +static enum trailer_where where; +static enum trailer_if_exists if_exists; +static enum trailer_if_missing if_missing; + +static int option_parse_where(const struct option *opt, + const char *arg, int unset UNUSED) +{ + /* unset implies NULL arg, which is handled in our helper */ + return trailer_set_where(opt->value, arg); +} + +static int option_parse_if_exists(const struct option *opt, + const char *arg, int unset UNUSED) +{ + /* unset implies NULL arg, which is handled in our helper */ + return trailer_set_if_exists(opt->value, arg); +} + +static int option_parse_if_missing(const struct option *opt, + const char *arg, int unset UNUSED) +{ + /* unset implies NULL arg, which is handled in our helper */ + return trailer_set_if_missing(opt->value, arg); +} + +static void new_trailers_clear(struct list_head *trailers) +{ + struct list_head *pos, *tmp; + struct new_trailer_item *item; + + list_for_each_safe(pos, tmp, trailers) { + item = list_entry(pos, struct new_trailer_item, list); + list_del(pos); + free(item); + } +} + +static int option_parse_trailer(const struct option *opt, + const char *arg, int unset) +{ + struct list_head *trailers = opt->value; + struct new_trailer_item *item; + + if (unset) { + new_trailers_clear(trailers); + return 0; + } + + if (!arg) + return -1; + + item = xmalloc(sizeof(*item)); + item->text = arg; + item->where = where; + item->if_exists = if_exists; + item->if_missing = if_missing; + list_add_tail(&item->list, trailers); + return 0; +} + +static int parse_opt_parse(const struct option *opt, const char *arg, + int unset) +{ + struct process_trailer_options *v = opt->value; + v->only_trailers = 1; + v->only_input = 1; + v->unfold = 1; + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + return 0; +} + +int cmd_interpret_trailers(int argc, const char **argv, const char *prefix) +{ + struct process_trailer_options opts = PROCESS_TRAILER_OPTIONS_INIT; + LIST_HEAD(trailers); + + struct option options[] = { + OPT_BOOL(0, "in-place", &opts.in_place, N_("edit files in place")), + OPT_BOOL(0, "trim-empty", &opts.trim_empty, N_("trim empty trailers")), + + OPT_CALLBACK(0, "where", &where, N_("placement"), + N_("where to place the new trailer"), option_parse_where), + OPT_CALLBACK(0, "if-exists", &if_exists, N_("action"), + N_("action if trailer already exists"), option_parse_if_exists), + OPT_CALLBACK(0, "if-missing", &if_missing, N_("action"), + N_("action if trailer is missing"), option_parse_if_missing), + + OPT_BOOL(0, "only-trailers", &opts.only_trailers, N_("output only the trailers")), + OPT_BOOL(0, "only-input", &opts.only_input, N_("do not apply trailer.* configuration variables")), + OPT_BOOL(0, "unfold", &opts.unfold, N_("reformat multiline trailer values as single-line values")), + OPT_CALLBACK_F(0, "parse", &opts, NULL, N_("alias for --only-trailers --only-input --unfold"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, parse_opt_parse), + OPT_BOOL(0, "no-divider", &opts.no_divider, N_("do not treat \"---\" as the end of input")), + OPT_CALLBACK(0, "trailer", &trailers, N_("trailer"), + N_("trailer(s) to add"), option_parse_trailer), + OPT_END() + }; + + git_config(git_default_config, NULL); + + argc = parse_options(argc, argv, prefix, options, + git_interpret_trailers_usage, 0); + + if (opts.only_input && !list_empty(&trailers)) + usage_msg_opt( + _("--trailer with --only-input does not make sense"), + git_interpret_trailers_usage, + options); + + if (argc) { + int i; + for (i = 0; i < argc; i++) + process_trailers(argv[i], &opts, &trailers); + } else { + if (opts.in_place) + die(_("no input file given for in-place editing")); + process_trailers(NULL, &opts, &trailers); + } + + new_trailers_clear(&trailers); + + return 0; +} diff --git a/builtin/log.c b/builtin/log.c new file mode 100644 index 0000000..ba775d7 --- /dev/null +++ b/builtin/log.c @@ -0,0 +1,2544 @@ +/* + * Builtin "git log" and related commands (show, whatchanged) + * + * (C) Copyright 2006 Linus Torvalds + * 2006 Junio Hamano + */ +#include "git-compat-util.h" +#include "abspath.h" +#include "config.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "refs.h" +#include "object-file.h" +#include "object-name.h" +#include "object-store-ll.h" +#include "pager.h" +#include "color.h" +#include "commit.h" +#include "diff.h" +#include "diff-merges.h" +#include "revision.h" +#include "log-tree.h" +#include "builtin.h" +#include "oid-array.h" +#include "tag.h" +#include "reflog-walk.h" +#include "patch-ids.h" +#include "run-command.h" +#include "shortlog.h" +#include "remote.h" +#include "string-list.h" +#include "parse-options.h" +#include "line-log.h" +#include "branch.h" +#include "streaming.h" +#include "version.h" +#include "mailmap.h" +#include "gpg-interface.h" +#include "progress.h" +#include "commit-slab.h" +#include "repository.h" +#include "commit-reach.h" +#include "range-diff.h" +#include "tmp-objdir.h" +#include "tree.h" +#include "write-or-die.h" + +#define MAIL_DEFAULT_WRAP 72 +#define COVER_FROM_AUTO_MAX_SUBJECT_LEN 100 +#define FORMAT_PATCH_NAME_MAX_DEFAULT 64 + +/* Set a default date-time format for git log ("log.date" config variable) */ +static const char *default_date_mode = NULL; + +static int default_abbrev_commit; +static int default_show_root = 1; +static int default_follow; +static int default_show_signature; +static int default_encode_email_headers = 1; +static int decoration_style; +static int decoration_given; +static int use_mailmap_config = 1; +static unsigned int force_in_body_from; +static int stdout_mboxrd; +static const char *fmt_patch_subject_prefix = "PATCH"; +static int fmt_patch_name_max = FORMAT_PATCH_NAME_MAX_DEFAULT; +static const char *fmt_pretty; +static int format_no_prefix; + +static const char * const builtin_log_usage[] = { + N_("git log [<options>] [<revision-range>] [[--] <path>...]"), + N_("git show [<options>] <object>..."), + NULL +}; + +struct line_opt_callback_data { + struct rev_info *rev; + const char *prefix; + struct string_list args; +}; + +static int session_is_interactive(void) +{ + return isatty(1) || pager_in_use(); +} + +static int auto_decoration_style(void) +{ + return session_is_interactive() ? DECORATE_SHORT_REFS : 0; +} + +static int parse_decoration_style(const char *value) +{ + switch (git_parse_maybe_bool(value)) { + case 1: + return DECORATE_SHORT_REFS; + case 0: + return 0; + default: + break; + } + if (!strcmp(value, "full")) + return DECORATE_FULL_REFS; + else if (!strcmp(value, "short")) + return DECORATE_SHORT_REFS; + else if (!strcmp(value, "auto")) + return auto_decoration_style(); + /* + * Please update _git_log() in git-completion.bash when you + * add new decoration styles. + */ + return -1; +} + +static int use_default_decoration_filter = 1; +static struct string_list decorate_refs_exclude = STRING_LIST_INIT_NODUP; +static struct string_list decorate_refs_exclude_config = STRING_LIST_INIT_NODUP; +static struct string_list decorate_refs_include = STRING_LIST_INIT_NODUP; + +static int clear_decorations_callback(const struct option *opt UNUSED, + const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + string_list_clear(&decorate_refs_include, 0); + string_list_clear(&decorate_refs_exclude, 0); + use_default_decoration_filter = 0; + return 0; +} + +static int decorate_callback(const struct option *opt UNUSED, const char *arg, + int unset) +{ + if (unset) + decoration_style = 0; + else if (arg) + decoration_style = parse_decoration_style(arg); + else + decoration_style = DECORATE_SHORT_REFS; + + if (decoration_style < 0) + die(_("invalid --decorate option: %s"), arg); + + decoration_given = 1; + + return 0; +} + +static int log_line_range_callback(const struct option *option, const char *arg, int unset) +{ + struct line_opt_callback_data *data = option->value; + + BUG_ON_OPT_NEG(unset); + + if (!arg) + return -1; + + data->rev->line_level_traverse = 1; + string_list_append(&data->args, arg); + + return 0; +} + +static void init_log_defaults(void) +{ + init_diff_ui_defaults(); + + decoration_style = auto_decoration_style(); +} + +static void cmd_log_init_defaults(struct rev_info *rev) +{ + if (fmt_pretty) + get_commit_format(fmt_pretty, rev); + if (default_follow) + rev->diffopt.flags.default_follow_renames = 1; + rev->verbose_header = 1; + init_diffstat_widths(&rev->diffopt); + rev->diffopt.flags.recursive = 1; + rev->diffopt.flags.allow_textconv = 1; + rev->abbrev_commit = default_abbrev_commit; + rev->show_root_diff = default_show_root; + rev->subject_prefix = fmt_patch_subject_prefix; + rev->patch_name_max = fmt_patch_name_max; + rev->show_signature = default_show_signature; + rev->encode_email_headers = default_encode_email_headers; + + if (default_date_mode) + parse_date_format(default_date_mode, &rev->date_mode); +} + +static void set_default_decoration_filter(struct decoration_filter *decoration_filter) +{ + int i; + char *value = NULL; + struct string_list *include = decoration_filter->include_ref_pattern; + const struct string_list *config_exclude; + + if (!git_config_get_string_multi("log.excludeDecoration", + &config_exclude)) { + struct string_list_item *item; + for_each_string_list_item(item, config_exclude) + string_list_append(decoration_filter->exclude_ref_config_pattern, + item->string); + } + + /* + * By default, decorate_all is disabled. Enable it if + * log.initialDecorationSet=all. Don't ever disable it by config, + * since the command-line takes precedent. + */ + if (use_default_decoration_filter && + !git_config_get_string("log.initialdecorationset", &value) && + !strcmp("all", value)) + use_default_decoration_filter = 0; + free(value); + + if (!use_default_decoration_filter || + decoration_filter->exclude_ref_pattern->nr || + decoration_filter->include_ref_pattern->nr || + decoration_filter->exclude_ref_config_pattern->nr) + return; + + /* + * No command-line or config options were given, so + * populate with sensible defaults. + */ + for (i = 0; i < ARRAY_SIZE(ref_namespace); i++) { + if (!ref_namespace[i].decoration) + continue; + + string_list_append(include, ref_namespace[i].ref); + } +} + +static void cmd_log_init_finish(int argc, const char **argv, const char *prefix, + struct rev_info *rev, struct setup_revision_opt *opt) +{ + struct userformat_want w; + int quiet = 0, source = 0, mailmap; + static struct line_opt_callback_data line_cb = {NULL, NULL, STRING_LIST_INIT_DUP}; + struct decoration_filter decoration_filter = { + .exclude_ref_pattern = &decorate_refs_exclude, + .include_ref_pattern = &decorate_refs_include, + .exclude_ref_config_pattern = &decorate_refs_exclude_config, + }; + static struct revision_sources revision_sources; + + const struct option builtin_log_options[] = { + OPT__QUIET(&quiet, N_("suppress diff output")), + OPT_BOOL(0, "source", &source, N_("show source")), + OPT_BOOL(0, "use-mailmap", &mailmap, N_("use mail map file")), + OPT_ALIAS(0, "mailmap", "use-mailmap"), + OPT_CALLBACK_F(0, "clear-decorations", NULL, NULL, + N_("clear all previously-defined decoration filters"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, + clear_decorations_callback), + OPT_STRING_LIST(0, "decorate-refs", &decorate_refs_include, + N_("pattern"), N_("only decorate refs that match <pattern>")), + OPT_STRING_LIST(0, "decorate-refs-exclude", &decorate_refs_exclude, + N_("pattern"), N_("do not decorate refs that match <pattern>")), + OPT_CALLBACK_F(0, "decorate", NULL, NULL, N_("decorate options"), + PARSE_OPT_OPTARG, decorate_callback), + OPT_CALLBACK('L', NULL, &line_cb, "range:file", + N_("trace the evolution of line range <start>,<end> or function :<funcname> in <file>"), + log_line_range_callback), + OPT_END() + }; + + line_cb.rev = rev; + line_cb.prefix = prefix; + + mailmap = use_mailmap_config; + argc = parse_options(argc, argv, prefix, + builtin_log_options, builtin_log_usage, + PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT | + PARSE_OPT_KEEP_DASHDASH); + + if (quiet) + rev->diffopt.output_format |= DIFF_FORMAT_NO_OUTPUT; + argc = setup_revisions(argc, argv, rev, opt); + + /* Any arguments at this point are not recognized */ + if (argc > 1) + die(_("unrecognized argument: %s"), argv[1]); + + if (rev->line_level_traverse && rev->prune_data.nr) + die(_("-L<range>:<file> cannot be used with pathspec")); + + memset(&w, 0, sizeof(w)); + userformat_find_requirements(NULL, &w); + + if (!rev->show_notes_given && (!rev->pretty_given || w.notes)) + rev->show_notes = 1; + if (rev->show_notes) + load_display_notes(&rev->notes_opt); + + if ((rev->diffopt.pickaxe_opts & DIFF_PICKAXE_KINDS_MASK) || + rev->diffopt.filter || rev->diffopt.flags.follow_renames) + rev->always_show_header = 0; + + if (source || w.source) { + init_revision_sources(&revision_sources); + rev->sources = &revision_sources; + } + + if (mailmap) { + rev->mailmap = xmalloc(sizeof(struct string_list)); + string_list_init_nodup(rev->mailmap); + read_mailmap(rev->mailmap); + } + + if (rev->pretty_given && rev->commit_format == CMIT_FMT_RAW) { + /* + * "log --pretty=raw" is special; ignore UI oriented + * configuration variables such as decoration. + */ + if (!decoration_given) + decoration_style = 0; + if (!rev->abbrev_commit_given) + rev->abbrev_commit = 0; + } + + if (rev->commit_format == CMIT_FMT_USERFORMAT) { + if (!w.decorate) { + /* + * Disable decoration loading if the format will not + * show them anyway. + */ + decoration_style = 0; + } else if (!decoration_style) { + /* + * If we are going to show them, make sure we do load + * them here, but taking care not to override a + * specific style set by config or --decorate. + */ + decoration_style = DECORATE_SHORT_REFS; + } + } + + if (decoration_style || rev->simplify_by_decoration) { + set_default_decoration_filter(&decoration_filter); + + if (decoration_style) + rev->show_decorations = 1; + + load_ref_decorations(&decoration_filter, decoration_style); + } + + if (rev->line_level_traverse) + line_log_init(rev, line_cb.prefix, &line_cb.args); + + setup_pager(); +} + +static void cmd_log_init(int argc, const char **argv, const char *prefix, + struct rev_info *rev, struct setup_revision_opt *opt) +{ + cmd_log_init_defaults(rev); + cmd_log_init_finish(argc, argv, prefix, rev, opt); +} + +static int cmd_log_deinit(int ret, struct rev_info *rev) +{ + release_revisions(rev); + return ret; +} + +/* + * This gives a rough estimate for how many commits we + * will print out in the list. + */ +static int estimate_commit_count(struct commit_list *list) +{ + int n = 0; + + while (list) { + struct commit *commit = list->item; + unsigned int flags = commit->object.flags; + list = list->next; + if (!(flags & (TREESAME | UNINTERESTING))) + n++; + } + return n; +} + +static void show_early_header(struct rev_info *rev, const char *stage, int nr) +{ + if (rev->shown_one) { + rev->shown_one = 0; + if (rev->commit_format != CMIT_FMT_ONELINE) + putchar(rev->diffopt.line_termination); + } + fprintf(rev->diffopt.file, _("Final output: %d %s\n"), nr, stage); +} + +static struct itimerval early_output_timer; + +static void log_show_early(struct rev_info *revs, struct commit_list *list) +{ + int i = revs->early_output; + int show_header = 1; + int no_free = revs->diffopt.no_free; + + revs->diffopt.no_free = 0; + sort_in_topological_order(&list, revs->sort_order); + while (list && i) { + struct commit *commit = list->item; + switch (simplify_commit(revs, commit)) { + case commit_show: + if (show_header) { + int n = estimate_commit_count(list); + show_early_header(revs, "incomplete", n); + show_header = 0; + } + log_tree_commit(revs, commit); + i--; + break; + case commit_ignore: + break; + case commit_error: + revs->diffopt.no_free = no_free; + diff_free(&revs->diffopt); + return; + } + list = list->next; + } + + /* Did we already get enough commits for the early output? */ + if (!i) { + revs->diffopt.no_free = 0; + diff_free(&revs->diffopt); + return; + } + + /* + * ..if no, then repeat it twice a second until we + * do. + * + * NOTE! We don't use "it_interval", because if the + * reader isn't listening, we want our output to be + * throttled by the writing, and not have the timer + * trigger every second even if we're blocked on a + * reader! + */ + early_output_timer.it_value.tv_sec = 0; + early_output_timer.it_value.tv_usec = 500000; + setitimer(ITIMER_REAL, &early_output_timer, NULL); +} + +static void early_output(int signal UNUSED) +{ + show_early_output = log_show_early; +} + +static void setup_early_output(void) +{ + struct sigaction sa; + + /* + * Set up the signal handler, minimally intrusively: + * we only set a single volatile integer word (not + * using sigatomic_t - trying to avoid unnecessary + * system dependencies and headers), and using + * SA_RESTART. + */ + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = early_output; + sigemptyset(&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sigaction(SIGALRM, &sa, NULL); + + /* + * If we can get the whole output in less than a + * tenth of a second, don't even bother doing the + * early-output thing.. + * + * This is a one-time-only trigger. + */ + early_output_timer.it_value.tv_sec = 0; + early_output_timer.it_value.tv_usec = 100000; + setitimer(ITIMER_REAL, &early_output_timer, NULL); +} + +static void finish_early_output(struct rev_info *rev) +{ + int n = estimate_commit_count(rev->commits); + signal(SIGALRM, SIG_IGN); + show_early_header(rev, "done", n); +} + +static int cmd_log_walk_no_free(struct rev_info *rev) +{ + struct commit *commit; + int saved_nrl = 0; + int saved_dcctc = 0; + + if (rev->remerge_diff) { + rev->remerge_objdir = tmp_objdir_create("remerge-diff"); + if (!rev->remerge_objdir) + die(_("unable to create temporary object directory")); + tmp_objdir_replace_primary_odb(rev->remerge_objdir, 1); + } + + if (rev->early_output) + setup_early_output(); + + if (prepare_revision_walk(rev)) + die(_("revision walk setup failed")); + + if (rev->early_output) + finish_early_output(rev); + + /* + * For --check and --exit-code, the exit code is based on CHECK_FAILED + * and HAS_CHANGES being accumulated in rev->diffopt, so be careful to + * retain that state information if replacing rev->diffopt in this loop + */ + while ((commit = get_revision(rev)) != NULL) { + if (!log_tree_commit(rev, commit) && rev->max_count >= 0) + /* + * We decremented max_count in get_revision, + * but we didn't actually show the commit. + */ + rev->max_count++; + if (!rev->reflog_info) { + /* + * We may show a given commit multiple times when + * walking the reflogs. + */ + free_commit_buffer(the_repository->parsed_objects, + commit); + free_commit_list(commit->parents); + commit->parents = NULL; + } + if (saved_nrl < rev->diffopt.needed_rename_limit) + saved_nrl = rev->diffopt.needed_rename_limit; + if (rev->diffopt.degraded_cc_to_c) + saved_dcctc = 1; + } + rev->diffopt.degraded_cc_to_c = saved_dcctc; + rev->diffopt.needed_rename_limit = saved_nrl; + + if (rev->remerge_diff) { + tmp_objdir_destroy(rev->remerge_objdir); + rev->remerge_objdir = NULL; + } + + if (rev->diffopt.output_format & DIFF_FORMAT_CHECKDIFF && + rev->diffopt.flags.check_failed) { + return 02; + } + return diff_result_code(&rev->diffopt); +} + +static int cmd_log_walk(struct rev_info *rev) +{ + int retval; + + rev->diffopt.no_free = 1; + retval = cmd_log_walk_no_free(rev); + rev->diffopt.no_free = 0; + diff_free(&rev->diffopt); + return retval; +} + +static int git_log_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + const char *slot_name; + + if (!strcmp(var, "format.pretty")) + return git_config_string(&fmt_pretty, var, value); + if (!strcmp(var, "format.subjectprefix")) + return git_config_string(&fmt_patch_subject_prefix, var, value); + if (!strcmp(var, "format.filenamemaxlength")) { + fmt_patch_name_max = git_config_int(var, value, ctx->kvi); + return 0; + } + if (!strcmp(var, "format.encodeemailheaders")) { + default_encode_email_headers = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "log.abbrevcommit")) { + default_abbrev_commit = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "log.date")) + return git_config_string(&default_date_mode, var, value); + if (!strcmp(var, "log.decorate")) { + decoration_style = parse_decoration_style(value); + if (decoration_style < 0) + decoration_style = 0; /* maybe warn? */ + return 0; + } + if (!strcmp(var, "log.diffmerges")) + return diff_merges_config(value); + if (!strcmp(var, "log.showroot")) { + default_show_root = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "log.follow")) { + default_follow = git_config_bool(var, value); + return 0; + } + if (skip_prefix(var, "color.decorate.", &slot_name)) + return parse_decorate_color_config(var, slot_name, value); + if (!strcmp(var, "log.mailmap")) { + use_mailmap_config = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "log.showsignature")) { + default_show_signature = git_config_bool(var, value); + return 0; + } + + return git_diff_ui_config(var, value, ctx, cb); +} + +int cmd_whatchanged(int argc, const char **argv, const char *prefix) +{ + struct rev_info rev; + struct setup_revision_opt opt; + + init_log_defaults(); + git_config(git_log_config, NULL); + + repo_init_revisions(the_repository, &rev, prefix); + git_config(grep_config, &rev.grep_filter); + + rev.diff = 1; + rev.simplify_history = 0; + memset(&opt, 0, sizeof(opt)); + opt.def = "HEAD"; + opt.revarg_opt = REVARG_COMMITTISH; + cmd_log_init(argc, argv, prefix, &rev, &opt); + if (!rev.diffopt.output_format) + rev.diffopt.output_format = DIFF_FORMAT_RAW; + return cmd_log_deinit(cmd_log_walk(&rev), &rev); +} + +static void show_tagger(const char *buf, struct rev_info *rev) +{ + struct strbuf out = STRBUF_INIT; + struct pretty_print_context pp = {0}; + + pp.fmt = rev->commit_format; + pp.date_mode = rev->date_mode; + pp_user_info(&pp, "Tagger", &out, buf, get_log_output_encoding()); + fprintf(rev->diffopt.file, "%s", out.buf); + strbuf_release(&out); +} + +static int show_blob_object(const struct object_id *oid, struct rev_info *rev, const char *obj_name) +{ + struct object_id oidc; + struct object_context obj_context; + char *buf; + unsigned long size; + + fflush(rev->diffopt.file); + if (!rev->diffopt.flags.textconv_set_via_cmdline || + !rev->diffopt.flags.allow_textconv) + return stream_blob_to_fd(1, oid, NULL, 0); + + if (get_oid_with_context(the_repository, obj_name, + GET_OID_RECORD_PATH, + &oidc, &obj_context)) + die(_("not a valid object name %s"), obj_name); + if (!obj_context.path || + !textconv_object(the_repository, obj_context.path, + obj_context.mode, &oidc, 1, &buf, &size)) { + free(obj_context.path); + return stream_blob_to_fd(1, oid, NULL, 0); + } + + if (!buf) + die(_("git show %s: bad file"), obj_name); + + write_or_die(1, buf, size); + free(obj_context.path); + return 0; +} + +static int show_tag_object(const struct object_id *oid, struct rev_info *rev) +{ + unsigned long size; + enum object_type type; + char *buf = repo_read_object_file(the_repository, oid, &type, &size); + int offset = 0; + + if (!buf) + return error(_("could not read object %s"), oid_to_hex(oid)); + + assert(type == OBJ_TAG); + while (offset < size && buf[offset] != '\n') { + int new_offset = offset + 1; + const char *ident; + while (new_offset < size && buf[new_offset++] != '\n') + ; /* do nothing */ + if (skip_prefix(buf + offset, "tagger ", &ident)) + show_tagger(ident, rev); + offset = new_offset; + } + + if (offset < size) + fwrite(buf + offset, size - offset, 1, rev->diffopt.file); + free(buf); + return 0; +} + +static int show_tree_object(const struct object_id *oid UNUSED, + struct strbuf *base UNUSED, + const char *pathname, unsigned mode, + void *context) +{ + FILE *file = context; + fprintf(file, "%s%s\n", pathname, S_ISDIR(mode) ? "/" : ""); + return 0; +} + +static void show_setup_revisions_tweak(struct rev_info *rev) +{ + if (rev->first_parent_only) + diff_merges_default_to_first_parent(rev); + else + diff_merges_default_to_dense_combined(rev); + if (!rev->diffopt.output_format) + rev->diffopt.output_format = DIFF_FORMAT_PATCH; +} + +int cmd_show(int argc, const char **argv, const char *prefix) +{ + struct rev_info rev; + unsigned int i; + struct setup_revision_opt opt; + struct pathspec match_all; + int ret = 0; + + init_log_defaults(); + git_config(git_log_config, NULL); + + if (the_repository->gitdir) { + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + } + + memset(&match_all, 0, sizeof(match_all)); + repo_init_revisions(the_repository, &rev, prefix); + git_config(grep_config, &rev.grep_filter); + + rev.diff = 1; + rev.always_show_header = 1; + rev.no_walk = 1; + rev.diffopt.stat_width = -1; /* Scale to real terminal size */ + + memset(&opt, 0, sizeof(opt)); + opt.def = "HEAD"; + opt.tweak = show_setup_revisions_tweak; + cmd_log_init(argc, argv, prefix, &rev, &opt); + + if (!rev.no_walk) + return cmd_log_deinit(cmd_log_walk(&rev), &rev); + + rev.diffopt.no_free = 1; + for (i = 0; i < rev.pending.nr && !ret; i++) { + struct object *o = rev.pending.objects[i].item; + const char *name = rev.pending.objects[i].name; + switch (o->type) { + case OBJ_BLOB: + ret = show_blob_object(&o->oid, &rev, name); + break; + case OBJ_TAG: { + struct tag *t = (struct tag *)o; + struct object_id *oid = get_tagged_oid(t); + + if (rev.shown_one) + putchar('\n'); + fprintf(rev.diffopt.file, "%stag %s%s\n", + diff_get_color_opt(&rev.diffopt, DIFF_COMMIT), + t->tag, + diff_get_color_opt(&rev.diffopt, DIFF_RESET)); + ret = show_tag_object(&o->oid, &rev); + rev.shown_one = 1; + if (ret) + break; + o = parse_object(the_repository, oid); + if (!o) + ret = error(_("could not read object %s"), + oid_to_hex(oid)); + rev.pending.objects[i].item = o; + i--; + break; + } + case OBJ_TREE: + if (rev.shown_one) + putchar('\n'); + fprintf(rev.diffopt.file, "%stree %s%s\n\n", + diff_get_color_opt(&rev.diffopt, DIFF_COMMIT), + name, + diff_get_color_opt(&rev.diffopt, DIFF_RESET)); + read_tree(the_repository, (struct tree *)o, + &match_all, show_tree_object, + rev.diffopt.file); + rev.shown_one = 1; + break; + case OBJ_COMMIT: + { + struct object_array old; + struct object_array blank = OBJECT_ARRAY_INIT; + + memcpy(&old, &rev.pending, sizeof(old)); + memcpy(&rev.pending, &blank, sizeof(rev.pending)); + + add_object_array(o, name, &rev.pending); + ret = cmd_log_walk_no_free(&rev); + + /* + * No need for + * object_array_clear(&pending). It was + * cleared already in prepare_revision_walk() + */ + memcpy(&rev.pending, &old, sizeof(rev.pending)); + break; + } + default: + ret = error(_("unknown type: %d"), o->type); + } + } + + rev.diffopt.no_free = 0; + diff_free(&rev.diffopt); + + return cmd_log_deinit(ret, &rev); +} + +/* + * This is equivalent to "git log -g --abbrev-commit --pretty=oneline" + */ +int cmd_log_reflog(int argc, const char **argv, const char *prefix) +{ + struct rev_info rev; + struct setup_revision_opt opt; + + init_log_defaults(); + git_config(git_log_config, NULL); + + repo_init_revisions(the_repository, &rev, prefix); + init_reflog_walk(&rev.reflog_info); + git_config(grep_config, &rev.grep_filter); + + rev.verbose_header = 1; + memset(&opt, 0, sizeof(opt)); + opt.def = "HEAD"; + cmd_log_init_defaults(&rev); + rev.abbrev_commit = 1; + rev.commit_format = CMIT_FMT_ONELINE; + rev.use_terminator = 1; + rev.always_show_header = 1; + cmd_log_init_finish(argc, argv, prefix, &rev, &opt); + + return cmd_log_deinit(cmd_log_walk(&rev), &rev); +} + +static void log_setup_revisions_tweak(struct rev_info *rev) +{ + if (rev->diffopt.flags.default_follow_renames && + diff_check_follow_pathspec(&rev->prune_data, 0)) + rev->diffopt.flags.follow_renames = 1; + + if (rev->first_parent_only) + diff_merges_default_to_first_parent(rev); +} + +int cmd_log(int argc, const char **argv, const char *prefix) +{ + struct rev_info rev; + struct setup_revision_opt opt; + + init_log_defaults(); + git_config(git_log_config, NULL); + + repo_init_revisions(the_repository, &rev, prefix); + git_config(grep_config, &rev.grep_filter); + + rev.always_show_header = 1; + memset(&opt, 0, sizeof(opt)); + opt.def = "HEAD"; + opt.revarg_opt = REVARG_COMMITTISH; + opt.tweak = log_setup_revisions_tweak; + cmd_log_init(argc, argv, prefix, &rev, &opt); + return cmd_log_deinit(cmd_log_walk(&rev), &rev); +} + +/* format-patch */ + +static const char *fmt_patch_suffix = ".patch"; +static int numbered = 0; +static int auto_number = 1; + +static char *default_attach = NULL; + +static struct string_list extra_hdr = STRING_LIST_INIT_NODUP; +static struct string_list extra_to = STRING_LIST_INIT_NODUP; +static struct string_list extra_cc = STRING_LIST_INIT_NODUP; + +static void add_header(const char *value) +{ + struct string_list_item *item; + int len = strlen(value); + while (len && value[len - 1] == '\n') + len--; + + if (!strncasecmp(value, "to: ", 4)) { + item = string_list_append(&extra_to, value + 4); + len -= 4; + } else if (!strncasecmp(value, "cc: ", 4)) { + item = string_list_append(&extra_cc, value + 4); + len -= 4; + } else { + item = string_list_append(&extra_hdr, value); + } + + item->string[len] = '\0'; +} + +enum cover_setting { + COVER_UNSET, + COVER_OFF, + COVER_ON, + COVER_AUTO +}; + +enum thread_level { + THREAD_UNSET, + THREAD_SHALLOW, + THREAD_DEEP +}; + +enum cover_from_description { + COVER_FROM_NONE, + COVER_FROM_MESSAGE, + COVER_FROM_SUBJECT, + COVER_FROM_AUTO +}; + +enum auto_base_setting { + AUTO_BASE_NEVER, + AUTO_BASE_ALWAYS, + AUTO_BASE_WHEN_ABLE +}; + +static enum thread_level thread; +static int do_signoff; +static enum auto_base_setting auto_base; +static char *from; +static const char *signature = git_version_string; +static const char *signature_file; +static enum cover_setting config_cover_letter; +static const char *config_output_directory; +static enum cover_from_description cover_from_description_mode = COVER_FROM_MESSAGE; +static int show_notes; +static struct display_notes_opt notes_opt; + +static enum cover_from_description parse_cover_from_description(const char *arg) +{ + if (!arg || !strcmp(arg, "default")) + return COVER_FROM_MESSAGE; + else if (!strcmp(arg, "none")) + return COVER_FROM_NONE; + else if (!strcmp(arg, "message")) + return COVER_FROM_MESSAGE; + else if (!strcmp(arg, "subject")) + return COVER_FROM_SUBJECT; + else if (!strcmp(arg, "auto")) + return COVER_FROM_AUTO; + else + die(_("%s: invalid cover from description mode"), arg); +} + +static int git_format_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + if (!strcmp(var, "format.headers")) { + if (!value) + die(_("format.headers without value")); + add_header(value); + return 0; + } + if (!strcmp(var, "format.suffix")) + return git_config_string(&fmt_patch_suffix, var, value); + if (!strcmp(var, "format.to")) { + if (!value) + return config_error_nonbool(var); + string_list_append(&extra_to, value); + return 0; + } + if (!strcmp(var, "format.cc")) { + if (!value) + return config_error_nonbool(var); + string_list_append(&extra_cc, value); + return 0; + } + if (!strcmp(var, "diff.color") || !strcmp(var, "color.diff") || + !strcmp(var, "color.ui") || !strcmp(var, "diff.submodule")) { + return 0; + } + if (!strcmp(var, "format.numbered")) { + if (value && !strcasecmp(value, "auto")) { + auto_number = 1; + return 0; + } + numbered = git_config_bool(var, value); + auto_number = auto_number && numbered; + return 0; + } + if (!strcmp(var, "format.attach")) { + if (value && *value) + default_attach = xstrdup(value); + else if (value && !*value) + FREE_AND_NULL(default_attach); + else + default_attach = xstrdup(git_version_string); + return 0; + } + if (!strcmp(var, "format.thread")) { + if (value && !strcasecmp(value, "deep")) { + thread = THREAD_DEEP; + return 0; + } + if (value && !strcasecmp(value, "shallow")) { + thread = THREAD_SHALLOW; + return 0; + } + thread = git_config_bool(var, value) ? THREAD_SHALLOW : THREAD_UNSET; + return 0; + } + if (!strcmp(var, "format.signoff")) { + do_signoff = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "format.signature")) + return git_config_string(&signature, var, value); + if (!strcmp(var, "format.signaturefile")) + return git_config_pathname(&signature_file, var, value); + if (!strcmp(var, "format.coverletter")) { + if (value && !strcasecmp(value, "auto")) { + config_cover_letter = COVER_AUTO; + return 0; + } + config_cover_letter = git_config_bool(var, value) ? COVER_ON : COVER_OFF; + return 0; + } + if (!strcmp(var, "format.outputdirectory")) + return git_config_string(&config_output_directory, var, value); + if (!strcmp(var, "format.useautobase")) { + if (value && !strcasecmp(value, "whenAble")) { + auto_base = AUTO_BASE_WHEN_ABLE; + return 0; + } + auto_base = git_config_bool(var, value) ? AUTO_BASE_ALWAYS : AUTO_BASE_NEVER; + return 0; + } + if (!strcmp(var, "format.from")) { + int b = git_parse_maybe_bool(value); + free(from); + if (b < 0) + from = xstrdup(value); + else if (b) + from = xstrdup(git_committer_info(IDENT_NO_DATE)); + else + from = NULL; + return 0; + } + if (!strcmp(var, "format.forceinbodyfrom")) { + force_in_body_from = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "format.notes")) { + int b = git_parse_maybe_bool(value); + if (b < 0) + enable_ref_display_notes(¬es_opt, &show_notes, value); + else if (b) + enable_default_display_notes(¬es_opt, &show_notes); + else + disable_display_notes(¬es_opt, &show_notes); + return 0; + } + if (!strcmp(var, "format.coverfromdescription")) { + cover_from_description_mode = parse_cover_from_description(value); + return 0; + } + if (!strcmp(var, "format.mboxrd")) { + stdout_mboxrd = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "format.noprefix")) { + format_no_prefix = 1; + return 0; + } + + /* + * ignore some porcelain config which would otherwise be parsed by + * git_diff_ui_config(), via git_log_config(); we can't just avoid + * diff_ui_config completely, because we do care about some ui options + * like color. + */ + if (!strcmp(var, "diff.noprefix")) + return 0; + + return git_log_config(var, value, ctx, cb); +} + +static const char *output_directory = NULL; +static int outdir_offset; + +static int open_next_file(struct commit *commit, const char *subject, + struct rev_info *rev, int quiet) +{ + struct strbuf filename = STRBUF_INIT; + + if (output_directory) { + strbuf_addstr(&filename, output_directory); + strbuf_complete(&filename, '/'); + } + + if (rev->numbered_files) + strbuf_addf(&filename, "%d", rev->nr); + else if (commit) + fmt_output_commit(&filename, commit, rev); + else + fmt_output_subject(&filename, subject, rev); + + if (!quiet) + printf("%s\n", filename.buf + outdir_offset); + + if (!(rev->diffopt.file = fopen(filename.buf, "w"))) { + error_errno(_("cannot open patch file %s"), filename.buf); + strbuf_release(&filename); + return -1; + } + + strbuf_release(&filename); + return 0; +} + +static void get_patch_ids(struct rev_info *rev, struct patch_ids *ids) +{ + struct rev_info check_rev; + struct commit *commit, *c1, *c2; + struct object *o1, *o2; + unsigned flags1, flags2; + + if (rev->pending.nr != 2) + die(_("need exactly one range")); + + o1 = rev->pending.objects[0].item; + o2 = rev->pending.objects[1].item; + flags1 = o1->flags; + flags2 = o2->flags; + c1 = lookup_commit_reference(the_repository, &o1->oid); + c2 = lookup_commit_reference(the_repository, &o2->oid); + + if ((flags1 & UNINTERESTING) == (flags2 & UNINTERESTING)) + die(_("not a range")); + + init_patch_ids(the_repository, ids); + + /* given a range a..b get all patch ids for b..a */ + repo_init_revisions(the_repository, &check_rev, rev->prefix); + check_rev.max_parents = 1; + o1->flags ^= UNINTERESTING; + o2->flags ^= UNINTERESTING; + add_pending_object(&check_rev, o1, "o1"); + add_pending_object(&check_rev, o2, "o2"); + if (prepare_revision_walk(&check_rev)) + die(_("revision walk setup failed")); + + while ((commit = get_revision(&check_rev)) != NULL) { + add_commit_patch_id(commit, ids); + } + + /* reset for next revision walk */ + clear_commit_marks(c1, SEEN | UNINTERESTING | SHOWN | ADDED); + clear_commit_marks(c2, SEEN | UNINTERESTING | SHOWN | ADDED); + o1->flags = flags1; + o2->flags = flags2; +} + +static void gen_message_id(struct rev_info *info, char *base) +{ + struct strbuf buf = STRBUF_INIT; + strbuf_addf(&buf, "%s.%"PRItime".git.%s", base, + (timestamp_t) time(NULL), + git_committer_info(IDENT_NO_NAME|IDENT_NO_DATE|IDENT_STRICT)); + info->message_id = strbuf_detach(&buf, NULL); +} + +static void print_signature(FILE *file) +{ + if (!signature || !*signature) + return; + + fprintf(file, "-- \n%s", signature); + if (signature[strlen(signature)-1] != '\n') + putc('\n', file); + putc('\n', file); +} + +static char *find_branch_name(struct rev_info *rev) +{ + int i, positive = -1; + struct object_id branch_oid; + const struct object_id *tip_oid; + const char *ref, *v; + char *full_ref, *branch = NULL; + + for (i = 0; i < rev->cmdline.nr; i++) { + if (rev->cmdline.rev[i].flags & UNINTERESTING) + continue; + if (positive < 0) + positive = i; + else + return NULL; + } + if (positive < 0) + return NULL; + ref = rev->cmdline.rev[positive].name; + tip_oid = &rev->cmdline.rev[positive].item->oid; + if (repo_dwim_ref(the_repository, ref, strlen(ref), &branch_oid, + &full_ref, 0) && + skip_prefix(full_ref, "refs/heads/", &v) && + oideq(tip_oid, &branch_oid)) + branch = xstrdup(v); + free(full_ref); + return branch; +} + +static void show_diffstat(struct rev_info *rev, + struct commit *origin, struct commit *head) +{ + struct diff_options opts; + + memcpy(&opts, &rev->diffopt, sizeof(opts)); + opts.output_format = DIFF_FORMAT_SUMMARY | DIFF_FORMAT_DIFFSTAT; + diff_setup_done(&opts); + + diff_tree_oid(get_commit_tree_oid(origin), + get_commit_tree_oid(head), + "", &opts); + diffcore_std(&opts); + diff_flush(&opts); + + fprintf(rev->diffopt.file, "\n"); +} + +static void read_desc_file(struct strbuf *buf, const char *desc_file) +{ + if (strbuf_read_file(buf, desc_file, 0) < 0) + die_errno(_("unable to read branch description file '%s'"), + desc_file); +} + +static void prepare_cover_text(struct pretty_print_context *pp, + const char *description_file, + const char *branch_name, + struct strbuf *sb, + const char *encoding, + int need_8bit_cte) +{ + const char *subject = "*** SUBJECT HERE ***"; + const char *body = "*** BLURB HERE ***"; + struct strbuf description_sb = STRBUF_INIT; + struct strbuf subject_sb = STRBUF_INIT; + + if (cover_from_description_mode == COVER_FROM_NONE) + goto do_pp; + + if (description_file && *description_file) + read_desc_file(&description_sb, description_file); + else if (branch_name && *branch_name) + read_branch_desc(&description_sb, branch_name); + if (!description_sb.len) + goto do_pp; + + if (cover_from_description_mode == COVER_FROM_SUBJECT || + cover_from_description_mode == COVER_FROM_AUTO) + body = format_subject(&subject_sb, description_sb.buf, " "); + + if (cover_from_description_mode == COVER_FROM_MESSAGE || + (cover_from_description_mode == COVER_FROM_AUTO && + subject_sb.len > COVER_FROM_AUTO_MAX_SUBJECT_LEN)) + body = description_sb.buf; + else + subject = subject_sb.buf; + +do_pp: + pp_title_line(pp, &subject, sb, encoding, need_8bit_cte); + pp_remainder(pp, &body, sb, 0); + + strbuf_release(&description_sb); + strbuf_release(&subject_sb); +} + +static int get_notes_refs(struct string_list_item *item, void *arg) +{ + strvec_pushf(arg, "--notes=%s", item->string); + return 0; +} + +static void get_notes_args(struct strvec *arg, struct rev_info *rev) +{ + if (!rev->show_notes) { + strvec_push(arg, "--no-notes"); + } else if (rev->notes_opt.use_default_notes > 0 || + (rev->notes_opt.use_default_notes == -1 && + !rev->notes_opt.extra_notes_refs.nr)) { + strvec_push(arg, "--notes"); + } else { + for_each_string_list(&rev->notes_opt.extra_notes_refs, get_notes_refs, arg); + } +} + +static void make_cover_letter(struct rev_info *rev, int use_separate_file, + struct commit *origin, + int nr, struct commit **list, + const char *description_file, + const char *branch_name, + int quiet) +{ + const char *committer; + struct shortlog log; + struct strbuf sb = STRBUF_INIT; + int i; + const char *encoding = "UTF-8"; + int need_8bit_cte = 0; + struct pretty_print_context pp = {0}; + struct commit *head = list[0]; + + if (!cmit_fmt_is_mail(rev->commit_format)) + die(_("cover letter needs email format")); + + committer = git_committer_info(0); + + if (use_separate_file && + open_next_file(NULL, rev->numbered_files ? NULL : "cover-letter", rev, quiet)) + die(_("failed to create cover-letter file")); + + log_write_email_headers(rev, head, &pp.after_subject, &need_8bit_cte, 0); + + for (i = 0; !need_8bit_cte && i < nr; i++) { + const char *buf = repo_get_commit_buffer(the_repository, + list[i], NULL); + if (has_non_ascii(buf)) + need_8bit_cte = 1; + repo_unuse_commit_buffer(the_repository, list[i], buf); + } + + if (!branch_name) + branch_name = find_branch_name(rev); + + pp.fmt = CMIT_FMT_EMAIL; + pp.date_mode.type = DATE_RFC2822; + pp.rev = rev; + pp.print_email_subject = 1; + pp_user_info(&pp, NULL, &sb, committer, encoding); + prepare_cover_text(&pp, description_file, branch_name, &sb, + encoding, need_8bit_cte); + fprintf(rev->diffopt.file, "%s\n", sb.buf); + + strbuf_release(&sb); + + shortlog_init(&log); + log.wrap_lines = 1; + log.wrap = MAIL_DEFAULT_WRAP; + log.in1 = 2; + log.in2 = 4; + log.file = rev->diffopt.file; + log.groups = SHORTLOG_GROUP_AUTHOR; + shortlog_finish_setup(&log); + for (i = 0; i < nr; i++) + shortlog_add_commit(&log, list[i]); + + shortlog_output(&log); + + /* We can only do diffstat with a unique reference point */ + if (origin) + show_diffstat(rev, origin, head); + + if (rev->idiff_oid1) { + fprintf_ln(rev->diffopt.file, "%s", rev->idiff_title); + show_interdiff(rev->idiff_oid1, rev->idiff_oid2, 0, + &rev->diffopt); + } + + if (rev->rdiff1) { + /* + * Pass minimum required diff-options to range-diff; others + * can be added later if deemed desirable. + */ + struct diff_options opts; + struct strvec other_arg = STRVEC_INIT; + struct range_diff_options range_diff_opts = { + .creation_factor = rev->creation_factor, + .dual_color = 1, + .diffopt = &opts, + .other_arg = &other_arg + }; + + repo_diff_setup(the_repository, &opts); + opts.file = rev->diffopt.file; + opts.use_color = rev->diffopt.use_color; + diff_setup_done(&opts); + fprintf_ln(rev->diffopt.file, "%s", rev->rdiff_title); + get_notes_args(&other_arg, rev); + show_range_diff(rev->rdiff1, rev->rdiff2, &range_diff_opts); + strvec_clear(&other_arg); + } +} + +static char *clean_message_id(const char *msg_id) +{ + char ch; + const char *a, *z, *m; + + m = msg_id; + while ((ch = *m) && (isspace(ch) || (ch == '<'))) + m++; + a = m; + z = NULL; + while ((ch = *m)) { + if (!isspace(ch) && (ch != '>')) + z = m; + m++; + } + if (!z) + die(_("insane in-reply-to: %s"), msg_id); + if (++z == m) + return xstrdup(a); + return xmemdupz(a, z - a); +} + +static const char *set_outdir(const char *prefix, const char *output_directory) +{ + if (output_directory && is_absolute_path(output_directory)) + return output_directory; + + if (!prefix || !*prefix) { + if (output_directory) + return output_directory; + /* The user did not explicitly ask for "./" */ + outdir_offset = 2; + return "./"; + } + + outdir_offset = strlen(prefix); + if (!output_directory) + return prefix; + + return prefix_filename(prefix, output_directory); +} + +static const char * const builtin_format_patch_usage[] = { + N_("git format-patch [<options>] [<since> | <revision-range>]"), + NULL +}; + +static int keep_subject = 0; + +static int keep_callback(const struct option *opt, const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + ((struct rev_info *)opt->value)->total = -1; + keep_subject = 1; + return 0; +} + +static int subject_prefix = 0; + +static int subject_prefix_callback(const struct option *opt, const char *arg, + int unset) +{ + struct strbuf *sprefix; + + BUG_ON_OPT_NEG(unset); + sprefix = opt->value; + subject_prefix = 1; + strbuf_reset(sprefix); + strbuf_addstr(sprefix, arg); + return 0; +} + +static int numbered_cmdline_opt = 0; + +static int numbered_callback(const struct option *opt, const char *arg, + int unset) +{ + BUG_ON_OPT_ARG(arg); + *(int *)opt->value = numbered_cmdline_opt = unset ? 0 : 1; + if (unset) + auto_number = 0; + return 0; +} + +static int no_numbered_callback(const struct option *opt, const char *arg, + int unset) +{ + BUG_ON_OPT_NEG(unset); + return numbered_callback(opt, arg, 1); +} + +static int output_directory_callback(const struct option *opt, const char *arg, + int unset) +{ + const char **dir = (const char **)opt->value; + BUG_ON_OPT_NEG(unset); + if (*dir) + die(_("two output directories?")); + *dir = arg; + return 0; +} + +static int thread_callback(const struct option *opt, const char *arg, int unset) +{ + enum thread_level *thread = (enum thread_level *)opt->value; + if (unset) + *thread = THREAD_UNSET; + else if (!arg || !strcmp(arg, "shallow")) + *thread = THREAD_SHALLOW; + else if (!strcmp(arg, "deep")) + *thread = THREAD_DEEP; + /* + * Please update _git_formatpatch() in git-completion.bash + * when you add new options. + */ + else + return 1; + return 0; +} + +static int attach_callback(const struct option *opt, const char *arg, int unset) +{ + struct rev_info *rev = (struct rev_info *)opt->value; + if (unset) + rev->mime_boundary = NULL; + else if (arg) + rev->mime_boundary = arg; + else + rev->mime_boundary = git_version_string; + rev->no_inline = unset ? 0 : 1; + return 0; +} + +static int inline_callback(const struct option *opt, const char *arg, int unset) +{ + struct rev_info *rev = (struct rev_info *)opt->value; + if (unset) + rev->mime_boundary = NULL; + else if (arg) + rev->mime_boundary = arg; + else + rev->mime_boundary = git_version_string; + rev->no_inline = 0; + return 0; +} + +static int header_callback(const struct option *opt UNUSED, const char *arg, + int unset) +{ + if (unset) { + string_list_clear(&extra_hdr, 0); + string_list_clear(&extra_to, 0); + string_list_clear(&extra_cc, 0); + } else { + add_header(arg); + } + return 0; +} + +static int from_callback(const struct option *opt, const char *arg, int unset) +{ + char **from = opt->value; + + free(*from); + + if (unset) + *from = NULL; + else if (arg) + *from = xstrdup(arg); + else + *from = xstrdup(git_committer_info(IDENT_NO_DATE)); + return 0; +} + +static int base_callback(const struct option *opt, const char *arg, int unset) +{ + const char **base_commit = opt->value; + + if (unset) { + auto_base = AUTO_BASE_NEVER; + *base_commit = NULL; + } else if (!strcmp(arg, "auto")) { + auto_base = AUTO_BASE_ALWAYS; + *base_commit = NULL; + } else { + auto_base = AUTO_BASE_NEVER; + *base_commit = arg; + } + return 0; +} + +struct base_tree_info { + struct object_id base_commit; + int nr_patch_id, alloc_patch_id; + struct object_id *patch_id; +}; + +static struct commit *get_base_commit(const char *base_commit, + struct commit **list, + int total) +{ + struct commit *base = NULL; + struct commit **rev; + int i = 0, rev_nr = 0, auto_select, die_on_failure; + + switch (auto_base) { + case AUTO_BASE_NEVER: + if (base_commit) { + auto_select = 0; + die_on_failure = 1; + } else { + /* no base information is requested */ + return NULL; + } + break; + case AUTO_BASE_ALWAYS: + case AUTO_BASE_WHEN_ABLE: + if (base_commit) { + BUG("requested automatic base selection but a commit was provided"); + } else { + auto_select = 1; + die_on_failure = auto_base == AUTO_BASE_ALWAYS; + } + break; + default: + BUG("unexpected automatic base selection method"); + } + + if (!auto_select) { + base = lookup_commit_reference_by_name(base_commit); + if (!base) + die(_("unknown commit %s"), base_commit); + } else { + struct branch *curr_branch = branch_get(NULL); + const char *upstream = branch_get_upstream(curr_branch, NULL); + if (upstream) { + struct commit_list *base_list; + struct commit *commit; + struct object_id oid; + + if (repo_get_oid(the_repository, upstream, &oid)) { + if (die_on_failure) + die(_("failed to resolve '%s' as a valid ref"), upstream); + else + return NULL; + } + commit = lookup_commit_or_die(&oid, "upstream base"); + base_list = repo_get_merge_bases_many(the_repository, + commit, total, + list); + /* There should be one and only one merge base. */ + if (!base_list || base_list->next) { + if (die_on_failure) { + die(_("could not find exact merge base")); + } else { + free_commit_list(base_list); + return NULL; + } + } + base = base_list->item; + free_commit_list(base_list); + } else { + if (die_on_failure) + die(_("failed to get upstream, if you want to record base commit automatically,\n" + "please use git branch --set-upstream-to to track a remote branch.\n" + "Or you could specify base commit by --base=<base-commit-id> manually")); + else + return NULL; + } + } + + ALLOC_ARRAY(rev, total); + for (i = 0; i < total; i++) + rev[i] = list[i]; + + rev_nr = total; + /* + * Get merge base through pair-wise computations + * and store it in rev[0]. + */ + while (rev_nr > 1) { + for (i = 0; i < rev_nr / 2; i++) { + struct commit_list *merge_base; + merge_base = repo_get_merge_bases(the_repository, + rev[2 * i], + rev[2 * i + 1]); + if (!merge_base || merge_base->next) { + if (die_on_failure) { + die(_("failed to find exact merge base")); + } else { + free(rev); + return NULL; + } + } + + rev[i] = merge_base->item; + } + + if (rev_nr % 2) + rev[i] = rev[2 * i]; + rev_nr = DIV_ROUND_UP(rev_nr, 2); + } + + if (!repo_in_merge_bases(the_repository, base, rev[0])) { + if (die_on_failure) { + die(_("base commit should be the ancestor of revision list")); + } else { + free(rev); + return NULL; + } + } + + for (i = 0; i < total; i++) { + if (base == list[i]) { + if (die_on_failure) { + die(_("base commit shouldn't be in revision list")); + } else { + free(rev); + return NULL; + } + } + } + + free(rev); + return base; +} + +define_commit_slab(commit_base, int); + +static void prepare_bases(struct base_tree_info *bases, + struct commit *base, + struct commit **list, + int total) +{ + struct commit *commit; + struct rev_info revs; + struct diff_options diffopt; + struct commit_base commit_base; + int i; + + if (!base) + return; + + init_commit_base(&commit_base); + repo_diff_setup(the_repository, &diffopt); + diffopt.flags.recursive = 1; + diff_setup_done(&diffopt); + + oidcpy(&bases->base_commit, &base->object.oid); + + repo_init_revisions(the_repository, &revs, NULL); + revs.max_parents = 1; + revs.topo_order = 1; + for (i = 0; i < total; i++) { + list[i]->object.flags &= ~UNINTERESTING; + add_pending_object(&revs, &list[i]->object, "rev_list"); + *commit_base_at(&commit_base, list[i]) = 1; + } + base->object.flags |= UNINTERESTING; + add_pending_object(&revs, &base->object, "base"); + + if (prepare_revision_walk(&revs)) + die(_("revision walk setup failed")); + /* + * Traverse the commits list, get prerequisite patch ids + * and stuff them in bases structure. + */ + while ((commit = get_revision(&revs)) != NULL) { + struct object_id oid; + struct object_id *patch_id; + if (*commit_base_at(&commit_base, commit)) + continue; + if (commit_patch_id(commit, &diffopt, &oid, 0)) + die(_("cannot get patch id")); + ALLOC_GROW(bases->patch_id, bases->nr_patch_id + 1, bases->alloc_patch_id); + patch_id = bases->patch_id + bases->nr_patch_id; + oidcpy(patch_id, &oid); + bases->nr_patch_id++; + } + clear_commit_base(&commit_base); +} + +static void print_bases(struct base_tree_info *bases, FILE *file) +{ + int i; + + /* Only do this once, either for the cover or for the first one */ + if (is_null_oid(&bases->base_commit)) + return; + + /* Show the base commit */ + fprintf(file, "\nbase-commit: %s\n", oid_to_hex(&bases->base_commit)); + + /* Show the prerequisite patches */ + for (i = bases->nr_patch_id - 1; i >= 0; i--) + fprintf(file, "prerequisite-patch-id: %s\n", oid_to_hex(&bases->patch_id[i])); + + free(bases->patch_id); + bases->nr_patch_id = 0; + bases->alloc_patch_id = 0; + oidclr(&bases->base_commit); +} + +static const char *diff_title(struct strbuf *sb, + const char *reroll_count, + const char *generic, + const char *rerolled) +{ + int v; + + /* RFC may be v0, so allow -v1 to diff against v0 */ + if (reroll_count && !strtol_i(reroll_count, 10, &v) && + v >= 1) + strbuf_addf(sb, rerolled, v - 1); + else + strbuf_addstr(sb, generic); + return sb->buf; +} + +static void infer_range_diff_ranges(struct strbuf *r1, + struct strbuf *r2, + const char *prev, + struct commit *origin, + struct commit *head) +{ + const char *head_oid = oid_to_hex(&head->object.oid); + int prev_is_range = is_range_diff_range(prev); + + if (prev_is_range) + strbuf_addstr(r1, prev); + else + strbuf_addf(r1, "%s..%s", head_oid, prev); + + if (origin) + strbuf_addf(r2, "%s..%s", oid_to_hex(&origin->object.oid), head_oid); + else if (prev_is_range) + die(_("failed to infer range-diff origin of current series")); + else { + warning(_("using '%s' as range-diff origin of current series"), prev); + strbuf_addf(r2, "%s..%s", prev, head_oid); + } +} + +int cmd_format_patch(int argc, const char **argv, const char *prefix) +{ + struct commit *commit; + struct commit **list = NULL; + struct rev_info rev; + char *to_free = NULL; + struct setup_revision_opt s_r_opt; + int nr = 0, total, i; + int use_stdout = 0; + int start_number = -1; + int just_numbers = 0; + int ignore_if_in_upstream = 0; + int cover_letter = -1; + int boundary_count = 0; + int no_binary_diff = 0; + int zero_commit = 0; + struct commit *origin = NULL; + const char *in_reply_to = NULL; + struct patch_ids ids; + struct strbuf buf = STRBUF_INIT; + int use_patch_format = 0; + int quiet = 0; + const char *reroll_count = NULL; + char *cover_from_description_arg = NULL; + char *description_file = NULL; + char *branch_name = NULL; + char *base_commit = NULL; + struct base_tree_info bases; + struct commit *base; + int show_progress = 0; + struct progress *progress = NULL; + struct oid_array idiff_prev = OID_ARRAY_INIT; + struct strbuf idiff_title = STRBUF_INIT; + const char *rdiff_prev = NULL; + struct strbuf rdiff1 = STRBUF_INIT; + struct strbuf rdiff2 = STRBUF_INIT; + struct strbuf rdiff_title = STRBUF_INIT; + struct strbuf sprefix = STRBUF_INIT; + int creation_factor = -1; + int rfc = 0; + + const struct option builtin_format_patch_options[] = { + OPT_CALLBACK_F('n', "numbered", &numbered, NULL, + N_("use [PATCH n/m] even with a single patch"), + PARSE_OPT_NOARG, numbered_callback), + OPT_CALLBACK_F('N', "no-numbered", &numbered, NULL, + N_("use [PATCH] even with multiple patches"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, no_numbered_callback), + OPT_BOOL('s', "signoff", &do_signoff, N_("add a Signed-off-by trailer")), + OPT_BOOL(0, "stdout", &use_stdout, + N_("print patches to standard out")), + OPT_BOOL(0, "cover-letter", &cover_letter, + N_("generate a cover letter")), + OPT_BOOL(0, "numbered-files", &just_numbers, + N_("use simple number sequence for output file names")), + OPT_STRING(0, "suffix", &fmt_patch_suffix, N_("sfx"), + N_("use <sfx> instead of '.patch'")), + OPT_INTEGER(0, "start-number", &start_number, + N_("start numbering patches at <n> instead of 1")), + OPT_STRING('v', "reroll-count", &reroll_count, N_("reroll-count"), + N_("mark the series as Nth re-roll")), + OPT_INTEGER(0, "filename-max-length", &fmt_patch_name_max, + N_("max length of output filename")), + OPT_BOOL(0, "rfc", &rfc, N_("use [RFC PATCH] instead of [PATCH]")), + OPT_STRING(0, "cover-from-description", &cover_from_description_arg, + N_("cover-from-description-mode"), + N_("generate parts of a cover letter based on a branch's description")), + OPT_FILENAME(0, "description-file", &description_file, + N_("use branch description from file")), + OPT_CALLBACK_F(0, "subject-prefix", &sprefix, N_("prefix"), + N_("use [<prefix>] instead of [PATCH]"), + PARSE_OPT_NONEG, subject_prefix_callback), + OPT_CALLBACK_F('o', "output-directory", &output_directory, + N_("dir"), N_("store resulting files in <dir>"), + PARSE_OPT_NONEG, output_directory_callback), + OPT_CALLBACK_F('k', "keep-subject", &rev, NULL, + N_("don't strip/add [PATCH]"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, keep_callback), + OPT_BOOL(0, "no-binary", &no_binary_diff, + N_("don't output binary diffs")), + OPT_BOOL(0, "zero-commit", &zero_commit, + N_("output all-zero hash in From header")), + OPT_BOOL(0, "ignore-if-in-upstream", &ignore_if_in_upstream, + N_("don't include a patch matching a commit upstream")), + OPT_SET_INT_F('p', "no-stat", &use_patch_format, + N_("show patch format instead of default (patch + stat)"), + 1, PARSE_OPT_NONEG), + OPT_GROUP(N_("Messaging")), + OPT_CALLBACK(0, "add-header", NULL, N_("header"), + N_("add email header"), header_callback), + OPT_STRING_LIST(0, "to", &extra_to, N_("email"), N_("add To: header")), + OPT_STRING_LIST(0, "cc", &extra_cc, N_("email"), N_("add Cc: header")), + OPT_CALLBACK_F(0, "from", &from, N_("ident"), + N_("set From address to <ident> (or committer ident if absent)"), + PARSE_OPT_OPTARG, from_callback), + OPT_STRING(0, "in-reply-to", &in_reply_to, N_("message-id"), + N_("make first mail a reply to <message-id>")), + OPT_CALLBACK_F(0, "attach", &rev, N_("boundary"), + N_("attach the patch"), PARSE_OPT_OPTARG, + attach_callback), + OPT_CALLBACK_F(0, "inline", &rev, N_("boundary"), + N_("inline the patch"), + PARSE_OPT_OPTARG | PARSE_OPT_NONEG, + inline_callback), + OPT_CALLBACK_F(0, "thread", &thread, N_("style"), + N_("enable message threading, styles: shallow, deep"), + PARSE_OPT_OPTARG, thread_callback), + OPT_STRING(0, "signature", &signature, N_("signature"), + N_("add a signature")), + OPT_CALLBACK_F(0, "base", &base_commit, N_("base-commit"), + N_("add prerequisite tree info to the patch series"), + 0, base_callback), + OPT_FILENAME(0, "signature-file", &signature_file, + N_("add a signature from a file")), + OPT__QUIET(&quiet, N_("don't print the patch filenames")), + OPT_BOOL(0, "progress", &show_progress, + N_("show progress while generating patches")), + OPT_CALLBACK(0, "interdiff", &idiff_prev, N_("rev"), + N_("show changes against <rev> in cover letter or single patch"), + parse_opt_object_name), + OPT_STRING(0, "range-diff", &rdiff_prev, N_("refspec"), + N_("show changes against <refspec> in cover letter or single patch")), + OPT_INTEGER(0, "creation-factor", &creation_factor, + N_("percentage by which creation is weighted")), + OPT_BOOL(0, "force-in-body-from", &force_in_body_from, + N_("show in-body From: even if identical to the e-mail header")), + OPT_END() + }; + + extra_hdr.strdup_strings = 1; + extra_to.strdup_strings = 1; + extra_cc.strdup_strings = 1; + + init_log_defaults(); + init_display_notes(¬es_opt); + git_config(git_format_config, NULL); + repo_init_revisions(the_repository, &rev, prefix); + git_config(grep_config, &rev.grep_filter); + + rev.show_notes = show_notes; + memcpy(&rev.notes_opt, ¬es_opt, sizeof(notes_opt)); + rev.commit_format = CMIT_FMT_EMAIL; + rev.encode_email_headers = default_encode_email_headers; + rev.expand_tabs_in_log_default = 0; + rev.verbose_header = 1; + rev.diff = 1; + rev.max_parents = 1; + rev.diffopt.flags.recursive = 1; + rev.diffopt.no_free = 1; + memset(&s_r_opt, 0, sizeof(s_r_opt)); + s_r_opt.def = "HEAD"; + s_r_opt.revarg_opt = REVARG_COMMITTISH; + + strbuf_addstr(&sprefix, fmt_patch_subject_prefix); + if (format_no_prefix) + diff_set_noprefix(&rev.diffopt); + + if (default_attach) { + rev.mime_boundary = default_attach; + rev.no_inline = 1; + } + + /* + * Parse the arguments before setup_revisions(), or something + * like "git format-patch -o a123 HEAD^.." may fail; a123 is + * possibly a valid SHA1. + */ + argc = parse_options(argc, argv, prefix, builtin_format_patch_options, + builtin_format_patch_usage, + PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT | + PARSE_OPT_KEEP_DASHDASH); + + rev.force_in_body_from = force_in_body_from; + + /* Make sure "0000-$sub.patch" gives non-negative length for $sub */ + if (fmt_patch_name_max <= strlen("0000-") + strlen(fmt_patch_suffix)) + fmt_patch_name_max = strlen("0000-") + strlen(fmt_patch_suffix); + + if (cover_from_description_arg) + cover_from_description_mode = parse_cover_from_description(cover_from_description_arg); + + if (rfc) + strbuf_insertstr(&sprefix, 0, "RFC "); + + if (reroll_count) { + strbuf_addf(&sprefix, " v%s", reroll_count); + rev.reroll_count = reroll_count; + } + + rev.subject_prefix = sprefix.buf; + + for (i = 0; i < extra_hdr.nr; i++) { + strbuf_addstr(&buf, extra_hdr.items[i].string); + strbuf_addch(&buf, '\n'); + } + + if (extra_to.nr) + strbuf_addstr(&buf, "To: "); + for (i = 0; i < extra_to.nr; i++) { + if (i) + strbuf_addstr(&buf, " "); + strbuf_addstr(&buf, extra_to.items[i].string); + if (i + 1 < extra_to.nr) + strbuf_addch(&buf, ','); + strbuf_addch(&buf, '\n'); + } + + if (extra_cc.nr) + strbuf_addstr(&buf, "Cc: "); + for (i = 0; i < extra_cc.nr; i++) { + if (i) + strbuf_addstr(&buf, " "); + strbuf_addstr(&buf, extra_cc.items[i].string); + if (i + 1 < extra_cc.nr) + strbuf_addch(&buf, ','); + strbuf_addch(&buf, '\n'); + } + + rev.extra_headers = to_free = strbuf_detach(&buf, NULL); + + if (from) { + if (split_ident_line(&rev.from_ident, from, strlen(from))) + die(_("invalid ident line: %s"), from); + } + + if (start_number < 0) + start_number = 1; + + /* + * If numbered is set solely due to format.numbered in config, + * and it would conflict with --keep-subject (-k) from the + * command line, reset "numbered". + */ + if (numbered && keep_subject && !numbered_cmdline_opt) + numbered = 0; + + if (numbered && keep_subject) + die(_("options '%s' and '%s' cannot be used together"), "-n", "-k"); + if (keep_subject && subject_prefix) + die(_("options '%s' and '%s' cannot be used together"), "--subject-prefix/--rfc", "-k"); + rev.preserve_subject = keep_subject; + + argc = setup_revisions(argc, argv, &rev, &s_r_opt); + if (argc > 1) + die(_("unrecognized argument: %s"), argv[1]); + + if (rev.diffopt.output_format & DIFF_FORMAT_NAME) + die(_("--name-only does not make sense")); + if (rev.diffopt.output_format & DIFF_FORMAT_NAME_STATUS) + die(_("--name-status does not make sense")); + if (rev.diffopt.output_format & DIFF_FORMAT_CHECKDIFF) + die(_("--check does not make sense")); + if (rev.remerge_diff) + die(_("--remerge-diff does not make sense")); + + if (!use_patch_format && + (!rev.diffopt.output_format || + rev.diffopt.output_format == DIFF_FORMAT_PATCH)) + rev.diffopt.output_format = DIFF_FORMAT_DIFFSTAT | DIFF_FORMAT_SUMMARY; + if (!rev.diffopt.stat_width) + rev.diffopt.stat_width = MAIL_DEFAULT_WRAP; + + /* Always generate a patch */ + rev.diffopt.output_format |= DIFF_FORMAT_PATCH; + rev.always_show_header = 1; + + rev.zero_commit = zero_commit; + rev.patch_name_max = fmt_patch_name_max; + + if (!rev.diffopt.flags.text && !no_binary_diff) + rev.diffopt.flags.binary = 1; + + if (rev.show_notes) + load_display_notes(&rev.notes_opt); + + die_for_incompatible_opt3(use_stdout, "--stdout", + rev.diffopt.close_file, "--output", + !!output_directory, "--output-directory"); + + if (use_stdout && stdout_mboxrd) + rev.commit_format = CMIT_FMT_MBOXRD; + + if (use_stdout) { + setup_pager(); + } else if (!rev.diffopt.close_file) { + int saved; + + if (!output_directory) + output_directory = config_output_directory; + output_directory = set_outdir(prefix, output_directory); + + if (rev.diffopt.use_color != GIT_COLOR_ALWAYS) + rev.diffopt.use_color = GIT_COLOR_NEVER; + /* + * We consider <outdir> as 'outside of gitdir', therefore avoid + * applying adjust_shared_perm in s-c-l-d. + */ + saved = get_shared_repository(); + set_shared_repository(0); + switch (safe_create_leading_directories_const(output_directory)) { + case SCLD_OK: + case SCLD_EXISTS: + break; + default: + die(_("could not create leading directories " + "of '%s'"), output_directory); + } + set_shared_repository(saved); + if (mkdir(output_directory, 0777) < 0 && errno != EEXIST) + die_errno(_("could not create directory '%s'"), + output_directory); + } + + if (rev.pending.nr == 1) { + int check_head = 0; + + if (rev.max_count < 0 && !rev.show_root_diff) { + /* + * This is traditional behaviour of "git format-patch + * origin" that prepares what the origin side still + * does not have. + */ + rev.pending.objects[0].item->flags |= UNINTERESTING; + add_head_to_pending(&rev); + check_head = 1; + } + /* + * Otherwise, it is "format-patch -22 HEAD", and/or + * "format-patch --root HEAD". The user wants + * get_revision() to do the usual traversal. + */ + + if (!strcmp(rev.pending.objects[0].name, "HEAD")) + check_head = 1; + + if (check_head) { + const char *ref, *v; + ref = resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, + NULL, NULL); + if (ref && skip_prefix(ref, "refs/heads/", &v)) + branch_name = xstrdup(v); + else + branch_name = xstrdup(""); /* no branch */ + } + } + + /* + * We cannot move this anywhere earlier because we do want to + * know if --root was given explicitly from the command line. + */ + rev.show_root_diff = 1; + + if (ignore_if_in_upstream) { + /* Don't say anything if head and upstream are the same. */ + if (rev.pending.nr == 2) { + struct object_array_entry *o = rev.pending.objects; + if (oideq(&o[0].item->oid, &o[1].item->oid)) + goto done; + } + get_patch_ids(&rev, &ids); + } + + if (prepare_revision_walk(&rev)) + die(_("revision walk setup failed")); + rev.boundary = 1; + while ((commit = get_revision(&rev)) != NULL) { + if (commit->object.flags & BOUNDARY) { + boundary_count++; + origin = (boundary_count == 1) ? commit : NULL; + continue; + } + + if (ignore_if_in_upstream && has_commit_patch_id(commit, &ids)) + continue; + + nr++; + REALLOC_ARRAY(list, nr); + list[nr - 1] = commit; + } + if (nr == 0) + /* nothing to do */ + goto done; + total = nr; + if (cover_letter == -1) { + if (config_cover_letter == COVER_AUTO) + cover_letter = (total > 1); + else + cover_letter = (config_cover_letter == COVER_ON); + } + if (!keep_subject && auto_number && (total > 1 || cover_letter)) + numbered = 1; + if (numbered) + rev.total = total + start_number - 1; + + if (idiff_prev.nr) { + if (!cover_letter && total != 1) + die(_("--interdiff requires --cover-letter or single patch")); + rev.idiff_oid1 = &idiff_prev.oid[idiff_prev.nr - 1]; + rev.idiff_oid2 = get_commit_tree_oid(list[0]); + rev.idiff_title = diff_title(&idiff_title, reroll_count, + _("Interdiff:"), + _("Interdiff against v%d:")); + } + + if (creation_factor < 0) + creation_factor = RANGE_DIFF_CREATION_FACTOR_DEFAULT; + else if (!rdiff_prev) + die(_("the option '%s' requires '%s'"), "--creation-factor", "--range-diff"); + + if (rdiff_prev) { + if (!cover_letter && total != 1) + die(_("--range-diff requires --cover-letter or single patch")); + + infer_range_diff_ranges(&rdiff1, &rdiff2, rdiff_prev, + origin, list[0]); + rev.rdiff1 = rdiff1.buf; + rev.rdiff2 = rdiff2.buf; + rev.creation_factor = creation_factor; + rev.rdiff_title = diff_title(&rdiff_title, reroll_count, + _("Range-diff:"), + _("Range-diff against v%d:")); + } + + if (!signature) { + ; /* --no-signature inhibits all signatures */ + } else if (signature && signature != git_version_string) { + ; /* non-default signature already set */ + } else if (signature_file) { + struct strbuf buf = STRBUF_INIT; + + if (strbuf_read_file(&buf, signature_file, 128) < 0) + die_errno(_("unable to read signature file '%s'"), signature_file); + signature = strbuf_detach(&buf, NULL); + } + + memset(&bases, 0, sizeof(bases)); + base = get_base_commit(base_commit, list, nr); + if (base) { + reset_revision_walk(); + clear_object_flags(UNINTERESTING); + prepare_bases(&bases, base, list, nr); + } + + if (in_reply_to || thread || cover_letter) { + rev.ref_message_ids = xmalloc(sizeof(*rev.ref_message_ids)); + string_list_init_dup(rev.ref_message_ids); + } + if (in_reply_to) { + char *msgid = clean_message_id(in_reply_to); + string_list_append_nodup(rev.ref_message_ids, msgid); + } + rev.numbered_files = just_numbers; + rev.patch_suffix = fmt_patch_suffix; + if (cover_letter) { + if (thread) + gen_message_id(&rev, "cover"); + make_cover_letter(&rev, !!output_directory, + origin, nr, list, description_file, branch_name, quiet); + print_bases(&bases, rev.diffopt.file); + print_signature(rev.diffopt.file); + total++; + start_number--; + /* interdiff/range-diff in cover-letter; omit from patches */ + rev.idiff_oid1 = NULL; + rev.rdiff1 = NULL; + } + rev.add_signoff = do_signoff; + + if (show_progress) + progress = start_delayed_progress(_("Generating patches"), total); + while (0 <= --nr) { + int shown; + display_progress(progress, total - nr); + commit = list[nr]; + rev.nr = total - nr + (start_number - 1); + /* Make the second and subsequent mails replies to the first */ + if (thread) { + /* Have we already had a message ID? */ + if (rev.message_id) { + /* + * For deep threading: make every mail + * a reply to the previous one, no + * matter what other options are set. + * + * For shallow threading: + * + * Without --cover-letter and + * --in-reply-to, make every mail a + * reply to the one before. + * + * With --in-reply-to but no + * --cover-letter, make every mail a + * reply to the <reply-to>. + * + * With --cover-letter, make every + * mail but the cover letter a reply + * to the cover letter. The cover + * letter is a reply to the + * --in-reply-to, if specified. + */ + if (thread == THREAD_SHALLOW + && rev.ref_message_ids->nr > 0 + && (!cover_letter || rev.nr > 1)) + free(rev.message_id); + else + string_list_append_nodup(rev.ref_message_ids, + rev.message_id); + } + gen_message_id(&rev, oid_to_hex(&commit->object.oid)); + } + + if (output_directory && + open_next_file(rev.numbered_files ? NULL : commit, NULL, &rev, quiet)) + die(_("failed to create output files")); + shown = log_tree_commit(&rev, commit); + free_commit_buffer(the_repository->parsed_objects, + commit); + + /* We put one extra blank line between formatted + * patches and this flag is used by log-tree code + * to see if it needs to emit a LF before showing + * the log; when using one file per patch, we do + * not want the extra blank line. + */ + if (output_directory) + rev.shown_one = 0; + if (shown) { + print_bases(&bases, rev.diffopt.file); + if (rev.mime_boundary) + fprintf(rev.diffopt.file, "\n--%s%s--\n\n\n", + mime_boundary_leader, + rev.mime_boundary); + else + print_signature(rev.diffopt.file); + } + if (output_directory) + fclose(rev.diffopt.file); + } + stop_progress(&progress); + free(list); + free(branch_name); + string_list_clear(&extra_to, 0); + string_list_clear(&extra_cc, 0); + string_list_clear(&extra_hdr, 0); + if (ignore_if_in_upstream) + free_patch_ids(&ids); + +done: + oid_array_clear(&idiff_prev); + strbuf_release(&idiff_title); + strbuf_release(&rdiff1); + strbuf_release(&rdiff2); + strbuf_release(&rdiff_title); + strbuf_release(&sprefix); + free(to_free); + free(rev.message_id); + if (rev.ref_message_ids) + string_list_clear(rev.ref_message_ids, 0); + free(rev.ref_message_ids); + return cmd_log_deinit(0, &rev); +} + +static int add_pending_commit(const char *arg, struct rev_info *revs, int flags) +{ + struct object_id oid; + if (repo_get_oid(the_repository, arg, &oid) == 0) { + struct commit *commit = lookup_commit_reference(the_repository, + &oid); + if (commit) { + commit->object.flags |= flags; + add_pending_object(revs, &commit->object, arg); + return 0; + } + } + return -1; +} + +static const char * const cherry_usage[] = { + N_("git cherry [-v] [<upstream> [<head> [<limit>]]]"), + NULL +}; + +static void print_commit(char sign, struct commit *commit, int verbose, + int abbrev, FILE *file) +{ + if (!verbose) { + fprintf(file, "%c %s\n", sign, + repo_find_unique_abbrev(the_repository, &commit->object.oid, abbrev)); + } else { + struct strbuf buf = STRBUF_INIT; + pp_commit_easy(CMIT_FMT_ONELINE, commit, &buf); + fprintf(file, "%c %s %s\n", sign, + repo_find_unique_abbrev(the_repository, &commit->object.oid, abbrev), + buf.buf); + strbuf_release(&buf); + } +} + +int cmd_cherry(int argc, const char **argv, const char *prefix) +{ + struct rev_info revs; + struct patch_ids ids; + struct commit *commit; + struct commit_list *list = NULL; + struct branch *current_branch; + const char *upstream; + const char *head = "HEAD"; + const char *limit = NULL; + int verbose = 0, abbrev = 0; + + struct option options[] = { + OPT__ABBREV(&abbrev), + OPT__VERBOSE(&verbose, N_("be verbose")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, cherry_usage, 0); + + switch (argc) { + case 3: + limit = argv[2]; + /* FALLTHROUGH */ + case 2: + head = argv[1]; + /* FALLTHROUGH */ + case 1: + upstream = argv[0]; + break; + default: + current_branch = branch_get(NULL); + upstream = branch_get_upstream(current_branch, NULL); + if (!upstream) { + fprintf(stderr, _("Could not find a tracked" + " remote branch, please" + " specify <upstream> manually.\n")); + usage_with_options(cherry_usage, options); + } + } + + repo_init_revisions(the_repository, &revs, prefix); + revs.max_parents = 1; + + if (add_pending_commit(head, &revs, 0)) + die(_("unknown commit %s"), head); + if (add_pending_commit(upstream, &revs, UNINTERESTING)) + die(_("unknown commit %s"), upstream); + + /* Don't say anything if head and upstream are the same. */ + if (revs.pending.nr == 2) { + struct object_array_entry *o = revs.pending.objects; + if (oideq(&o[0].item->oid, &o[1].item->oid)) + return 0; + } + + get_patch_ids(&revs, &ids); + + if (limit && add_pending_commit(limit, &revs, UNINTERESTING)) + die(_("unknown commit %s"), limit); + + /* reverse the list of commits */ + if (prepare_revision_walk(&revs)) + die(_("revision walk setup failed")); + while ((commit = get_revision(&revs)) != NULL) { + commit_list_insert(commit, &list); + } + + while (list) { + char sign = '+'; + + commit = list->item; + if (has_commit_patch_id(commit, &ids)) + sign = '-'; + print_commit(sign, commit, verbose, abbrev, revs.diffopt.file); + list = list->next; + } + + free_patch_ids(&ids); + return 0; +} diff --git a/builtin/ls-files.c b/builtin/ls-files.c new file mode 100644 index 0000000..a0229c3 --- /dev/null +++ b/builtin/ls-files.c @@ -0,0 +1,774 @@ +/* + * This merges the file listing in the directory cache index + * with the actual working directory list, and shows different + * combinations of the two. + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "builtin.h" +#include "repository.h" +#include "config.h" +#include "convert.h" +#include "quote.h" +#include "dir.h" +#include "gettext.h" +#include "object-name.h" +#include "strbuf.h" +#include "tree.h" +#include "cache-tree.h" +#include "parse-options.h" +#include "resolve-undo.h" +#include "string-list.h" +#include "path.h" +#include "pathspec.h" +#include "read-cache.h" +#include "run-command.h" +#include "setup.h" +#include "sparse-index.h" +#include "submodule.h" +#include "submodule-config.h" +#include "object-store.h" +#include "hex.h" + + +static int abbrev; +static int show_deleted; +static int show_cached; +static int show_others; +static int show_stage; +static int show_unmerged; +static int show_resolve_undo; +static int show_modified; +static int show_killed; +static int show_valid_bit; +static int show_fsmonitor_bit; +static int line_terminator = '\n'; +static int debug_mode; +static int show_eol; +static int recurse_submodules; +static int skipping_duplicates; +static int show_sparse_dirs; + +static const char *prefix; +static int max_prefix_len; +static int prefix_len; +static struct pathspec pathspec; +static int error_unmatch; +static char *ps_matched; +static const char *with_tree; +static int exc_given; +static int exclude_args; +static const char *format; + +static const char *tag_cached = ""; +static const char *tag_unmerged = ""; +static const char *tag_removed = ""; +static const char *tag_other = ""; +static const char *tag_killed = ""; +static const char *tag_modified = ""; +static const char *tag_skip_worktree = ""; +static const char *tag_resolve_undo = ""; + +static void write_eolinfo(struct index_state *istate, + const struct cache_entry *ce, const char *path) +{ + if (show_eol) { + struct stat st; + const char *i_txt = ""; + const char *w_txt = ""; + const char *a_txt = get_convert_attr_ascii(istate, path); + if (ce && S_ISREG(ce->ce_mode)) + i_txt = get_cached_convert_stats_ascii(istate, + ce->name); + if (!lstat(path, &st) && S_ISREG(st.st_mode)) + w_txt = get_wt_convert_stats_ascii(path); + printf("i/%-5s w/%-5s attr/%-17s\t", i_txt, w_txt, a_txt); + } +} + +static void write_name(const char *name) +{ + /* + * With "--full-name", prefix_len=0; this caller needs to pass + * an empty string in that case (a NULL is good for ""). + */ + write_name_quoted_relative(name, prefix_len ? prefix : NULL, + stdout, line_terminator); +} + +static void write_name_to_buf(struct strbuf *sb, const char *name) +{ + struct strbuf buf = STRBUF_INIT; + const char *rel = relative_path(name, prefix_len ? prefix : NULL, &buf); + + if (line_terminator) + quote_c_style(rel, sb, NULL, 0); + else + strbuf_addstr(sb, rel); + + strbuf_release(&buf); +} + +static const char *get_tag(const struct cache_entry *ce, const char *tag) +{ + static char alttag[4]; + + if (tag && *tag && ((show_valid_bit && (ce->ce_flags & CE_VALID)) || + (show_fsmonitor_bit && (ce->ce_flags & CE_FSMONITOR_VALID)))) { + memcpy(alttag, tag, 3); + + if (isalpha(tag[0])) { + alttag[0] = tolower(tag[0]); + } else if (tag[0] == '?') { + alttag[0] = '!'; + } else { + alttag[0] = 'v'; + alttag[1] = tag[0]; + alttag[2] = ' '; + alttag[3] = 0; + } + + tag = alttag; + } + + return tag; +} + +static void print_debug(const struct cache_entry *ce) +{ + if (debug_mode) { + const struct stat_data *sd = &ce->ce_stat_data; + + printf(" ctime: %u:%u\n", sd->sd_ctime.sec, sd->sd_ctime.nsec); + printf(" mtime: %u:%u\n", sd->sd_mtime.sec, sd->sd_mtime.nsec); + printf(" dev: %u\tino: %u\n", sd->sd_dev, sd->sd_ino); + printf(" uid: %u\tgid: %u\n", sd->sd_uid, sd->sd_gid); + printf(" size: %u\tflags: %x\n", sd->sd_size, ce->ce_flags); + } +} + +static void show_dir_entry(struct index_state *istate, + const char *tag, struct dir_entry *ent) +{ + int len = max_prefix_len; + + if (len > ent->len) + die("git ls-files: internal error - directory entry not superset of prefix"); + + /* If ps_matches is non-NULL, figure out which pathspec(s) match. */ + if (ps_matched) + dir_path_match(istate, ent, &pathspec, len, ps_matched); + + fputs(tag, stdout); + write_eolinfo(istate, NULL, ent->name); + write_name(ent->name); +} + +static void show_other_files(struct index_state *istate, + const struct dir_struct *dir) +{ + int i; + + for (i = 0; i < dir->nr; i++) { + struct dir_entry *ent = dir->entries[i]; + if (!index_name_is_other(istate, ent->name, ent->len)) + continue; + show_dir_entry(istate, tag_other, ent); + } +} + +static void show_killed_files(struct index_state *istate, + const struct dir_struct *dir) +{ + int i; + for (i = 0; i < dir->nr; i++) { + struct dir_entry *ent = dir->entries[i]; + char *cp, *sp; + int pos, len, killed = 0; + + for (cp = ent->name; cp - ent->name < ent->len; cp = sp + 1) { + sp = strchr(cp, '/'); + if (!sp) { + /* If ent->name is prefix of an entry in the + * cache, it will be killed. + */ + pos = index_name_pos(istate, ent->name, ent->len); + if (0 <= pos) + BUG("killed-file %.*s not found", + ent->len, ent->name); + pos = -pos - 1; + while (pos < istate->cache_nr && + ce_stage(istate->cache[pos])) + pos++; /* skip unmerged */ + if (istate->cache_nr <= pos) + break; + /* pos points at a name immediately after + * ent->name in the cache. Does it expect + * ent->name to be a directory? + */ + len = ce_namelen(istate->cache[pos]); + if ((ent->len < len) && + !strncmp(istate->cache[pos]->name, + ent->name, ent->len) && + istate->cache[pos]->name[ent->len] == '/') + killed = 1; + break; + } + if (0 <= index_name_pos(istate, ent->name, sp - ent->name)) { + /* If any of the leading directories in + * ent->name is registered in the cache, + * ent->name will be killed. + */ + killed = 1; + break; + } + } + if (killed) + show_dir_entry(istate, tag_killed, dir->entries[i]); + } +} + +static void show_files(struct repository *repo, struct dir_struct *dir); + +static void show_submodule(struct repository *superproject, + struct dir_struct *dir, const char *path) +{ + struct repository subrepo; + + if (repo_submodule_init(&subrepo, superproject, path, null_oid())) + return; + + if (repo_read_index(&subrepo) < 0) + die("index file corrupt"); + + show_files(&subrepo, dir); + + repo_clear(&subrepo); +} + +static void expand_objectsize(struct strbuf *line, const struct object_id *oid, + const enum object_type type, unsigned int padded) +{ + if (type == OBJ_BLOB) { + unsigned long size; + if (oid_object_info(the_repository, oid, &size) < 0) + die(_("could not get object info about '%s'"), + oid_to_hex(oid)); + if (padded) + strbuf_addf(line, "%7"PRIuMAX, (uintmax_t)size); + else + strbuf_addf(line, "%"PRIuMAX, (uintmax_t)size); + } else if (padded) { + strbuf_addf(line, "%7s", "-"); + } else { + strbuf_addstr(line, "-"); + } +} + +static void show_ce_fmt(struct repository *repo, const struct cache_entry *ce, + const char *format, const char *fullname) { + struct strbuf sb = STRBUF_INIT; + + while (strbuf_expand_step(&sb, &format)) { + const char *end; + size_t len; + struct stat st; + + if (skip_prefix(format, "%", &format)) + strbuf_addch(&sb, '%'); + else if ((len = strbuf_expand_literal(&sb, format))) + format += len; + else if (*format != '(') + die(_("bad ls-files format: element '%s' " + "does not start with '('"), format); + else if (!(end = strchr(format + 1, ')'))) + die(_("bad ls-files format: element '%s' " + "does not end in ')'"), format); + else if (skip_prefix(format, "(objectmode)", &format)) + strbuf_addf(&sb, "%06o", ce->ce_mode); + else if (skip_prefix(format, "(objectname)", &format)) + strbuf_add_unique_abbrev(&sb, &ce->oid, abbrev); + else if (skip_prefix(format, "(objecttype)", &format)) + strbuf_addstr(&sb, type_name(object_type(ce->ce_mode))); + else if (skip_prefix(format, "(objectsize:padded)", &format)) + expand_objectsize(&sb, &ce->oid, + object_type(ce->ce_mode), 1); + else if (skip_prefix(format, "(objectsize)", &format)) + expand_objectsize(&sb, &ce->oid, + object_type(ce->ce_mode), 0); + else if (skip_prefix(format, "(stage)", &format)) + strbuf_addf(&sb, "%d", ce_stage(ce)); + else if (skip_prefix(format, "(eolinfo:index)", &format)) + strbuf_addstr(&sb, S_ISREG(ce->ce_mode) ? + get_cached_convert_stats_ascii(repo->index, + ce->name) : ""); + else if (skip_prefix(format, "(eolinfo:worktree)", &format)) + strbuf_addstr(&sb, !lstat(fullname, &st) && + S_ISREG(st.st_mode) ? + get_wt_convert_stats_ascii(fullname) : ""); + else if (skip_prefix(format, "(eolattr)", &format)) + strbuf_addstr(&sb, get_convert_attr_ascii(repo->index, + fullname)); + else if (skip_prefix(format, "(path)", &format)) + write_name_to_buf(&sb, fullname); + else + die(_("bad ls-files format: %%%.*s"), + (int)(end - format + 1), format); + } + strbuf_addch(&sb, line_terminator); + fwrite(sb.buf, sb.len, 1, stdout); + strbuf_release(&sb); +} + +static void show_ce(struct repository *repo, struct dir_struct *dir, + const struct cache_entry *ce, const char *fullname, + const char *tag) +{ + if (max_prefix_len > strlen(fullname)) + die("git ls-files: internal error - cache entry not superset of prefix"); + + if (recurse_submodules && S_ISGITLINK(ce->ce_mode) && + is_submodule_active(repo, ce->name)) { + show_submodule(repo, dir, ce->name); + } else if (match_pathspec(repo->index, &pathspec, fullname, strlen(fullname), + max_prefix_len, ps_matched, + S_ISDIR(ce->ce_mode) || + S_ISGITLINK(ce->ce_mode))) { + if (format) { + show_ce_fmt(repo, ce, format, fullname); + print_debug(ce); + return; + } + + tag = get_tag(ce, tag); + + if (!show_stage) { + fputs(tag, stdout); + } else { + printf("%s%06o %s %d\t", + tag, + ce->ce_mode, + repo_find_unique_abbrev(repo, &ce->oid, abbrev), + ce_stage(ce)); + } + write_eolinfo(repo->index, ce, fullname); + write_name(fullname); + print_debug(ce); + } +} + +static void show_ru_info(struct index_state *istate) +{ + struct string_list_item *item; + + if (!istate->resolve_undo) + return; + + for_each_string_list_item(item, istate->resolve_undo) { + const char *path = item->string; + struct resolve_undo_info *ui = item->util; + int i, len; + + len = strlen(path); + if (len < max_prefix_len) + continue; /* outside of the prefix */ + if (!match_pathspec(istate, &pathspec, path, len, + max_prefix_len, ps_matched, 0)) + continue; /* uninterested */ + for (i = 0; i < 3; i++) { + if (!ui->mode[i]) + continue; + printf("%s%06o %s %d\t", tag_resolve_undo, ui->mode[i], + repo_find_unique_abbrev(the_repository, &ui->oid[i], abbrev), + i + 1); + write_name(path); + } + } +} + +static int ce_excluded(struct dir_struct *dir, struct index_state *istate, + const char *fullname, const struct cache_entry *ce) +{ + int dtype = ce_to_dtype(ce); + return is_excluded(dir, istate, fullname, &dtype); +} + +static void construct_fullname(struct strbuf *out, const struct repository *repo, + const struct cache_entry *ce) +{ + strbuf_reset(out); + if (repo->submodule_prefix) + strbuf_addstr(out, repo->submodule_prefix); + strbuf_addstr(out, ce->name); +} + +static void show_files(struct repository *repo, struct dir_struct *dir) +{ + int i; + struct strbuf fullname = STRBUF_INIT; + + /* For cached/deleted files we don't need to even do the readdir */ + if (show_others || show_killed) { + if (!show_others) + dir->flags |= DIR_COLLECT_KILLED_ONLY; + fill_directory(dir, repo->index, &pathspec); + if (show_others) + show_other_files(repo->index, dir); + if (show_killed) + show_killed_files(repo->index, dir); + } + + if (!(show_cached || show_stage || show_deleted || show_modified)) + return; + + if (!show_sparse_dirs) + ensure_full_index(repo->index); + + for (i = 0; i < repo->index->cache_nr; i++) { + const struct cache_entry *ce = repo->index->cache[i]; + struct stat st; + int stat_err; + + construct_fullname(&fullname, repo, ce); + + if ((dir->flags & DIR_SHOW_IGNORED) && + !ce_excluded(dir, repo->index, fullname.buf, ce)) + continue; + if (ce->ce_flags & CE_UPDATE) + continue; + if ((show_cached || show_stage) && + (!show_unmerged || ce_stage(ce))) { + show_ce(repo, dir, ce, fullname.buf, + ce_stage(ce) ? tag_unmerged : + (ce_skip_worktree(ce) ? tag_skip_worktree : + tag_cached)); + if (skipping_duplicates) + goto skip_to_next_name; + } + + if (!(show_deleted || show_modified)) + continue; + if (ce_skip_worktree(ce)) + continue; + stat_err = lstat(fullname.buf, &st); + if (stat_err && (errno != ENOENT && errno != ENOTDIR)) + error_errno("cannot lstat '%s'", fullname.buf); + if (stat_err && show_deleted) { + show_ce(repo, dir, ce, fullname.buf, tag_removed); + if (skipping_duplicates) + goto skip_to_next_name; + } + if (show_modified && + (stat_err || ie_modified(repo->index, ce, &st, 0))) { + show_ce(repo, dir, ce, fullname.buf, tag_modified); + if (skipping_duplicates) + goto skip_to_next_name; + } + continue; + +skip_to_next_name: + { + int j; + struct cache_entry **cache = repo->index->cache; + for (j = i + 1; j < repo->index->cache_nr; j++) + if (strcmp(ce->name, cache[j]->name)) + break; + i = j - 1; /* compensate for the for loop */ + } + } + + strbuf_release(&fullname); +} + +/* + * Prune the index to only contain stuff starting with "prefix" + */ +static void prune_index(struct index_state *istate, + const char *prefix, size_t prefixlen) +{ + int pos; + unsigned int first, last; + + if (!prefix || !istate->cache_nr) + return; + pos = index_name_pos(istate, prefix, prefixlen); + if (pos < 0) + pos = -pos-1; + first = pos; + last = istate->cache_nr; + while (last > first) { + int next = first + ((last - first) >> 1); + const struct cache_entry *ce = istate->cache[next]; + if (!strncmp(ce->name, prefix, prefixlen)) { + first = next+1; + continue; + } + last = next; + } + MOVE_ARRAY(istate->cache, istate->cache + pos, last - pos); + istate->cache_nr = last - pos; +} + +static int get_common_prefix_len(const char *common_prefix) +{ + int common_prefix_len; + + if (!common_prefix) + return 0; + + common_prefix_len = strlen(common_prefix); + + /* + * If the prefix has a trailing slash, strip it so that submodules wont + * be pruned from the index. + */ + if (common_prefix[common_prefix_len - 1] == '/') + common_prefix_len--; + + return common_prefix_len; +} + +static const char * const ls_files_usage[] = { + N_("git ls-files [<options>] [<file>...]"), + NULL +}; + +static int option_parse_exclude(const struct option *opt, + const char *arg, int unset) +{ + struct string_list *exclude_list = opt->value; + + BUG_ON_OPT_NEG(unset); + + exc_given = 1; + string_list_append(exclude_list, arg); + + return 0; +} + +static int option_parse_exclude_from(const struct option *opt, + const char *arg, int unset) +{ + struct dir_struct *dir = opt->value; + + BUG_ON_OPT_NEG(unset); + + exc_given = 1; + add_patterns_from_file(dir, arg); + + return 0; +} + +static int option_parse_exclude_standard(const struct option *opt, + const char *arg, int unset) +{ + struct dir_struct *dir = opt->value; + + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + + exc_given = 1; + setup_standard_excludes(dir); + + return 0; +} + +int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix) +{ + int require_work_tree = 0, show_tag = 0, i; + char *max_prefix; + struct dir_struct dir = DIR_INIT; + struct pattern_list *pl; + struct string_list exclude_list = STRING_LIST_INIT_NODUP; + struct option builtin_ls_files_options[] = { + /* Think twice before adding "--nul" synonym to this */ + OPT_SET_INT('z', NULL, &line_terminator, + N_("separate paths with the NUL character"), '\0'), + OPT_BOOL('t', NULL, &show_tag, + N_("identify the file status with tags")), + OPT_BOOL('v', NULL, &show_valid_bit, + N_("use lowercase letters for 'assume unchanged' files")), + OPT_BOOL('f', NULL, &show_fsmonitor_bit, + N_("use lowercase letters for 'fsmonitor clean' files")), + OPT_BOOL('c', "cached", &show_cached, + N_("show cached files in the output (default)")), + OPT_BOOL('d', "deleted", &show_deleted, + N_("show deleted files in the output")), + OPT_BOOL('m', "modified", &show_modified, + N_("show modified files in the output")), + OPT_BOOL('o', "others", &show_others, + N_("show other files in the output")), + OPT_BIT('i', "ignored", &dir.flags, + N_("show ignored files in the output"), + DIR_SHOW_IGNORED), + OPT_BOOL('s', "stage", &show_stage, + N_("show staged contents' object name in the output")), + OPT_BOOL('k', "killed", &show_killed, + N_("show files on the filesystem that need to be removed")), + OPT_BIT(0, "directory", &dir.flags, + N_("show 'other' directories' names only"), + DIR_SHOW_OTHER_DIRECTORIES), + OPT_BOOL(0, "eol", &show_eol, N_("show line endings of files")), + OPT_NEGBIT(0, "empty-directory", &dir.flags, + N_("don't show empty directories"), + DIR_HIDE_EMPTY_DIRECTORIES), + OPT_BOOL('u', "unmerged", &show_unmerged, + N_("show unmerged files in the output")), + OPT_BOOL(0, "resolve-undo", &show_resolve_undo, + N_("show resolve-undo information")), + OPT_CALLBACK_F('x', "exclude", &exclude_list, N_("pattern"), + N_("skip files matching pattern"), + PARSE_OPT_NONEG, option_parse_exclude), + OPT_CALLBACK_F('X', "exclude-from", &dir, N_("file"), + N_("read exclude patterns from <file>"), + PARSE_OPT_NONEG, option_parse_exclude_from), + OPT_STRING(0, "exclude-per-directory", &dir.exclude_per_dir, N_("file"), + N_("read additional per-directory exclude patterns in <file>")), + OPT_CALLBACK_F(0, "exclude-standard", &dir, NULL, + N_("add the standard git exclusions"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, + option_parse_exclude_standard), + OPT_SET_INT_F(0, "full-name", &prefix_len, + N_("make the output relative to the project top directory"), + 0, PARSE_OPT_NONEG), + OPT_BOOL(0, "recurse-submodules", &recurse_submodules, + N_("recurse through submodules")), + OPT_BOOL(0, "error-unmatch", &error_unmatch, + N_("if any <file> is not in the index, treat this as an error")), + OPT_STRING(0, "with-tree", &with_tree, N_("tree-ish"), + N_("pretend that paths removed since <tree-ish> are still present")), + OPT__ABBREV(&abbrev), + OPT_BOOL(0, "debug", &debug_mode, N_("show debugging data")), + OPT_BOOL(0, "deduplicate", &skipping_duplicates, + N_("suppress duplicate entries")), + OPT_BOOL(0, "sparse", &show_sparse_dirs, + N_("show sparse directories in the presence of a sparse index")), + OPT_STRING_F(0, "format", &format, N_("format"), + N_("format to use for the output"), + PARSE_OPT_NONEG), + OPT_END() + }; + int ret = 0; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(ls_files_usage, builtin_ls_files_options); + + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + prefix = cmd_prefix; + if (prefix) + prefix_len = strlen(prefix); + git_config(git_default_config, NULL); + + if (repo_read_index(the_repository) < 0) + die("index file corrupt"); + + argc = parse_options(argc, argv, prefix, builtin_ls_files_options, + ls_files_usage, 0); + pl = add_pattern_list(&dir, EXC_CMDL, "--exclude option"); + for (i = 0; i < exclude_list.nr; i++) { + add_pattern(exclude_list.items[i].string, "", 0, pl, --exclude_args); + } + + if (format && (show_stage || show_others || show_killed || + show_resolve_undo || skipping_duplicates || show_eol || show_tag)) + usage_msg_opt(_("--format cannot be used with -s, -o, -k, -t, " + "--resolve-undo, --deduplicate, --eol"), + ls_files_usage, builtin_ls_files_options); + + if (show_tag || show_valid_bit || show_fsmonitor_bit) { + tag_cached = "H "; + tag_unmerged = "M "; + tag_removed = "R "; + tag_modified = "C "; + tag_other = "? "; + tag_killed = "K "; + tag_skip_worktree = "S "; + tag_resolve_undo = "U "; + } + if (show_modified || show_others || show_deleted || (dir.flags & DIR_SHOW_IGNORED) || show_killed) + require_work_tree = 1; + if (show_unmerged) + /* + * There's no point in showing unmerged unless + * you also show the stage information. + */ + show_stage = 1; + if (show_tag || show_stage) + skipping_duplicates = 0; + if (dir.exclude_per_dir) + exc_given = 1; + + if (require_work_tree && !is_inside_work_tree()) + setup_work_tree(); + + if (recurse_submodules && + (show_deleted || show_others || show_unmerged || + show_killed || show_modified || show_resolve_undo || with_tree)) + die("ls-files --recurse-submodules unsupported mode"); + + if (recurse_submodules && error_unmatch) + die("ls-files --recurse-submodules does not support " + "--error-unmatch"); + + parse_pathspec(&pathspec, 0, + PATHSPEC_PREFER_CWD, + prefix, argv); + + /* + * Find common prefix for all pathspec's + * This is used as a performance optimization which unfortunately cannot + * be done when recursing into submodules because when a pathspec is + * given which spans repository boundaries you can't simply remove the + * submodule entry because the pathspec may match something inside the + * submodule. + */ + if (recurse_submodules) + max_prefix = NULL; + else + max_prefix = common_prefix(&pathspec); + max_prefix_len = get_common_prefix_len(max_prefix); + + prune_index(the_repository->index, max_prefix, max_prefix_len); + + /* Treat unmatching pathspec elements as errors */ + if (pathspec.nr && error_unmatch) + ps_matched = xcalloc(pathspec.nr, 1); + + if ((dir.flags & DIR_SHOW_IGNORED) && !show_others && !show_cached) + die("ls-files -i must be used with either -o or -c"); + + if ((dir.flags & DIR_SHOW_IGNORED) && !exc_given) + die("ls-files --ignored needs some exclude pattern"); + + /* With no flags, we default to showing the cached files */ + if (!(show_stage || show_deleted || show_others || show_unmerged || + show_killed || show_modified || show_resolve_undo)) + show_cached = 1; + + if (with_tree) { + /* + * Basic sanity check; show-stages and show-unmerged + * would not make any sense with this option. + */ + if (show_stage || show_unmerged) + die(_("options '%s' and '%s' cannot be used together"), "ls-files --with-tree", "-s/-u"); + overlay_tree_on_index(the_repository->index, with_tree, max_prefix); + } + + show_files(the_repository, &dir); + + if (show_resolve_undo) + show_ru_info(the_repository->index); + + if (ps_matched && report_path_error(ps_matched, &pathspec)) { + fprintf(stderr, "Did you forget to 'git add'?\n"); + ret = 1; + } + + string_list_clear(&exclude_list, 0); + dir_clear(&dir); + free(max_prefix); + return ret; +} diff --git a/builtin/ls-remote.c b/builtin/ls-remote.c new file mode 100644 index 0000000..fc76575 --- /dev/null +++ b/builtin/ls-remote.c @@ -0,0 +1,165 @@ +#include "builtin.h" +#include "gettext.h" +#include "hex.h" +#include "transport.h" +#include "pkt-line.h" +#include "ref-filter.h" +#include "remote.h" +#include "refs.h" +#include "parse-options.h" +#include "wildmatch.h" + +static const char * const ls_remote_usage[] = { + N_("git ls-remote [--heads] [--tags] [--refs] [--upload-pack=<exec>]\n" + " [-q | --quiet] [--exit-code] [--get-url] [--sort=<key>]\n" + " [--symref] [<repository> [<patterns>...]]"), + NULL +}; + +/* + * Is there one among the list of patterns that match the tail part + * of the path? + */ +static int tail_match(const char **pattern, const char *path) +{ + const char *p; + char *pathbuf; + + if (!pattern) + return 1; /* no restriction */ + + pathbuf = xstrfmt("/%s", path); + while ((p = *(pattern++)) != NULL) { + if (!wildmatch(p, pathbuf, 0)) { + free(pathbuf); + return 1; + } + } + free(pathbuf); + return 0; +} + +int cmd_ls_remote(int argc, const char **argv, const char *prefix) +{ + const char *dest = NULL; + unsigned flags = 0; + int get_url = 0; + int quiet = 0; + int status = 0; + int show_symref_target = 0; + const char *uploadpack = NULL; + const char **pattern = NULL; + struct transport_ls_refs_options transport_options = + TRANSPORT_LS_REFS_OPTIONS_INIT; + int i; + struct string_list server_options = STRING_LIST_INIT_DUP; + + struct remote *remote; + struct transport *transport; + const struct ref *ref; + struct ref_array ref_array; + struct string_list sorting_options = STRING_LIST_INIT_DUP; + + struct option options[] = { + OPT__QUIET(&quiet, N_("do not print remote URL")), + OPT_STRING(0, "upload-pack", &uploadpack, N_("exec"), + N_("path of git-upload-pack on the remote host")), + { OPTION_STRING, 0, "exec", &uploadpack, N_("exec"), + N_("path of git-upload-pack on the remote host"), + PARSE_OPT_HIDDEN }, + OPT_BIT('t', "tags", &flags, N_("limit to tags"), REF_TAGS), + OPT_BIT('h', "heads", &flags, N_("limit to heads"), REF_HEADS), + OPT_BIT(0, "refs", &flags, N_("do not show peeled tags"), REF_NORMAL), + OPT_BOOL(0, "get-url", &get_url, + N_("take url.<base>.insteadOf into account")), + OPT_REF_SORT(&sorting_options), + OPT_SET_INT_F(0, "exit-code", &status, + N_("exit with exit code 2 if no matching refs are found"), + 2, PARSE_OPT_NOCOMPLETE), + OPT_BOOL(0, "symref", &show_symref_target, + N_("show underlying ref in addition to the object pointed by it")), + OPT_STRING_LIST('o', "server-option", &server_options, N_("server-specific"), N_("option to transmit")), + OPT_END() + }; + + memset(&ref_array, 0, sizeof(ref_array)); + + argc = parse_options(argc, argv, prefix, options, ls_remote_usage, + PARSE_OPT_STOP_AT_NON_OPTION); + dest = argv[0]; + + packet_trace_identity("ls-remote"); + + if (argc > 1) { + int i; + CALLOC_ARRAY(pattern, argc); + for (i = 1; i < argc; i++) { + pattern[i - 1] = xstrfmt("*/%s", argv[i]); + } + } + + if (flags & REF_TAGS) + strvec_push(&transport_options.ref_prefixes, "refs/tags/"); + if (flags & REF_HEADS) + strvec_push(&transport_options.ref_prefixes, "refs/heads/"); + + remote = remote_get(dest); + if (!remote) { + if (dest) + die("bad repository '%s'", dest); + die("No remote configured to list refs from."); + } + if (!remote->url_nr) + die("remote %s has no configured URL", dest); + + if (get_url) { + printf("%s\n", *remote->url); + return 0; + } + + transport = transport_get(remote, NULL); + if (uploadpack) + transport_set_option(transport, TRANS_OPT_UPLOADPACK, uploadpack); + if (server_options.nr) + transport->server_options = &server_options; + + ref = transport_get_remote_refs(transport, &transport_options); + if (ref) { + int hash_algo = hash_algo_by_ptr(transport_get_hash_algo(transport)); + repo_set_hash_algo(the_repository, hash_algo); + } + + if (!dest && !quiet) + fprintf(stderr, "From %s\n", *remote->url); + for ( ; ref; ref = ref->next) { + struct ref_array_item *item; + if (!check_ref_type(ref, flags)) + continue; + if (!tail_match(pattern, ref->name)) + continue; + item = ref_array_push(&ref_array, ref->name, &ref->old_oid); + item->symref = xstrdup_or_null(ref->symref); + } + + if (sorting_options.nr) { + struct ref_sorting *sorting; + + sorting = ref_sorting_options(&sorting_options); + ref_array_sort(sorting, &ref_array); + ref_sorting_release(sorting); + } + + for (i = 0; i < ref_array.nr; i++) { + const struct ref_array_item *ref = ref_array.items[i]; + if (show_symref_target && ref->symref) + printf("ref: %s\t%s\n", ref->symref, ref->refname); + printf("%s\t%s\n", oid_to_hex(&ref->objectname), ref->refname); + status = 0; /* we found something */ + } + + ref_array_clear(&ref_array); + if (transport_disconnect(transport)) + status = 1; + transport_ls_refs_options_release(&transport_options); + return status; +} diff --git a/builtin/ls-tree.c b/builtin/ls-tree.c new file mode 100644 index 0000000..209d2dc --- /dev/null +++ b/builtin/ls-tree.c @@ -0,0 +1,452 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "hex.h" +#include "object-name.h" +#include "object-store-ll.h" +#include "blob.h" +#include "tree.h" +#include "commit.h" +#include "path.h" +#include "quote.h" +#include "parse-options.h" +#include "pathspec.h" + +static const char * const ls_tree_usage[] = { + N_("git ls-tree [<options>] <tree-ish> [<path>...]"), + NULL +}; + +static void expand_objectsize(struct strbuf *line, const struct object_id *oid, + const enum object_type type, unsigned int padded) +{ + if (type == OBJ_BLOB) { + unsigned long size; + if (oid_object_info(the_repository, oid, &size) < 0) + die(_("could not get object info about '%s'"), + oid_to_hex(oid)); + if (padded) + strbuf_addf(line, "%7"PRIuMAX, (uintmax_t)size); + else + strbuf_addf(line, "%"PRIuMAX, (uintmax_t)size); + } else if (padded) { + strbuf_addf(line, "%7s", "-"); + } else { + strbuf_addstr(line, "-"); + } +} + +struct ls_tree_options { + unsigned null_termination:1; + int abbrev; + enum ls_tree_path_options { + LS_RECURSIVE = 1 << 0, + LS_TREE_ONLY = 1 << 1, + LS_SHOW_TREES = 1 << 2, + } ls_options; + struct pathspec pathspec; + const char *prefix; + const char *format; +}; + +static int show_recursive(struct ls_tree_options *options, const char *base, + size_t baselen, const char *pathname) +{ + int i; + + if (options->ls_options & LS_RECURSIVE) + return 1; + + if (!options->pathspec.nr) + return 0; + + for (i = 0; i < options->pathspec.nr; i++) { + const char *spec = options->pathspec.items[i].match; + size_t len, speclen; + + if (strncmp(base, spec, baselen)) + continue; + len = strlen(pathname); + spec += baselen; + speclen = strlen(spec); + if (speclen <= len) + continue; + if (spec[len] && spec[len] != '/') + continue; + if (memcmp(pathname, spec, len)) + continue; + return 1; + } + return 0; +} + +static int show_tree_fmt(const struct object_id *oid, struct strbuf *base, + const char *pathname, unsigned mode, void *context) +{ + struct ls_tree_options *options = context; + int recurse = 0; + struct strbuf sb = STRBUF_INIT; + enum object_type type = object_type(mode); + const char *format = options->format; + + if (type == OBJ_TREE && show_recursive(options, base->buf, base->len, pathname)) + recurse = READ_TREE_RECURSIVE; + if (type == OBJ_TREE && recurse && !(options->ls_options & LS_SHOW_TREES)) + return recurse; + if (type == OBJ_BLOB && (options->ls_options & LS_TREE_ONLY)) + return 0; + + while (strbuf_expand_step(&sb, &format)) { + const char *end; + size_t len; + + if (skip_prefix(format, "%", &format)) + strbuf_addch(&sb, '%'); + else if ((len = strbuf_expand_literal(&sb, format))) + format += len; + else if (*format != '(') + die(_("bad ls-tree format: element '%s' " + "does not start with '('"), format); + else if (!(end = strchr(format + 1, ')'))) + die(_("bad ls-tree format: element '%s' " + "does not end in ')'"), format); + else if (skip_prefix(format, "(objectmode)", &format)) + strbuf_addf(&sb, "%06o", mode); + else if (skip_prefix(format, "(objecttype)", &format)) + strbuf_addstr(&sb, type_name(type)); + else if (skip_prefix(format, "(objectsize:padded)", &format)) + expand_objectsize(&sb, oid, type, 1); + else if (skip_prefix(format, "(objectsize)", &format)) + expand_objectsize(&sb, oid, type, 0); + else if (skip_prefix(format, "(objectname)", &format)) + strbuf_add_unique_abbrev(&sb, oid, options->abbrev); + else if (skip_prefix(format, "(path)", &format)) { + const char *name; + const char *prefix = options->prefix; + struct strbuf sbuf = STRBUF_INIT; + size_t baselen = base->len; + + strbuf_addstr(base, pathname); + name = relative_path(base->buf, prefix, &sbuf); + quote_c_style(name, &sb, NULL, 0); + strbuf_setlen(base, baselen); + strbuf_release(&sbuf); + } else + die(_("bad ls-tree format: %%%.*s"), + (int)(end - format + 1), format); + } + strbuf_addch(&sb, options->null_termination ? '\0' : '\n'); + fwrite(sb.buf, sb.len, 1, stdout); + strbuf_release(&sb); + return recurse; +} + +static int show_tree_common(struct ls_tree_options *options, int *recurse, + struct strbuf *base, const char *pathname, + enum object_type type) +{ + int ret = -1; + *recurse = 0; + + if (type == OBJ_BLOB) { + if (options->ls_options & LS_TREE_ONLY) + ret = 0; + } else if (type == OBJ_TREE && + show_recursive(options, base->buf, base->len, pathname)) { + *recurse = READ_TREE_RECURSIVE; + if (!(options->ls_options & LS_SHOW_TREES)) + ret = *recurse; + } + + return ret; +} + +static void show_tree_common_default_long(struct ls_tree_options *options, + struct strbuf *base, + const char *pathname, + const size_t baselen) +{ + const char *prefix = options->prefix; + + strbuf_addstr(base, pathname); + + if (options->null_termination) { + struct strbuf sb = STRBUF_INIT; + const char *name = relative_path(base->buf, prefix, &sb); + + fputs(name, stdout); + fputc('\0', stdout); + + strbuf_release(&sb); + } else { + write_name_quoted_relative(base->buf, prefix, stdout, '\n'); + } + + strbuf_setlen(base, baselen); +} + +static int show_tree_default(const struct object_id *oid, struct strbuf *base, + const char *pathname, unsigned mode, + void *context) +{ + struct ls_tree_options *options = context; + int early; + int recurse; + enum object_type type = object_type(mode); + + early = show_tree_common(options, &recurse, base, pathname, type); + if (early >= 0) + return early; + + printf("%06o %s %s\t", mode, type_name(object_type(mode)), + repo_find_unique_abbrev(the_repository, oid, options->abbrev)); + show_tree_common_default_long(options, base, pathname, base->len); + return recurse; +} + +static int show_tree_long(const struct object_id *oid, struct strbuf *base, + const char *pathname, unsigned mode, + void *context) +{ + struct ls_tree_options *options = context; + int early; + int recurse; + char size_text[24]; + enum object_type type = object_type(mode); + + early = show_tree_common(options, &recurse, base, pathname, type); + if (early >= 0) + return early; + + if (type == OBJ_BLOB) { + unsigned long size; + if (oid_object_info(the_repository, oid, &size) == OBJ_BAD) + xsnprintf(size_text, sizeof(size_text), "BAD"); + else + xsnprintf(size_text, sizeof(size_text), + "%" PRIuMAX, (uintmax_t)size); + } else { + xsnprintf(size_text, sizeof(size_text), "-"); + } + + printf("%06o %s %s %7s\t", mode, type_name(type), + repo_find_unique_abbrev(the_repository, oid, options->abbrev), + size_text); + show_tree_common_default_long(options, base, pathname, base->len); + return recurse; +} + +static int show_tree_name_only(const struct object_id *oid UNUSED, + struct strbuf *base, + const char *pathname, unsigned mode, + void *context) +{ + struct ls_tree_options *options = context; + int early; + int recurse; + const size_t baselen = base->len; + enum object_type type = object_type(mode); + const char *prefix; + + early = show_tree_common(options, &recurse, base, pathname, type); + if (early >= 0) + return early; + + prefix = options->prefix; + strbuf_addstr(base, pathname); + if (options->null_termination) { + struct strbuf sb = STRBUF_INIT; + const char *name = relative_path(base->buf, prefix, &sb); + + fputs(name, stdout); + fputc('\0', stdout); + + strbuf_release(&sb); + } else { + write_name_quoted_relative(base->buf, prefix, stdout, '\n'); + } + strbuf_setlen(base, baselen); + return recurse; +} + +static int show_tree_object(const struct object_id *oid, struct strbuf *base, + const char *pathname, unsigned mode, + void *context) +{ + struct ls_tree_options *options = context; + int early; + int recurse; + enum object_type type = object_type(mode); + const char *str; + + early = show_tree_common(options, &recurse, base, pathname, type); + if (early >= 0) + return early; + + str = repo_find_unique_abbrev(the_repository, oid, options->abbrev); + if (options->null_termination) { + fputs(str, stdout); + fputc('\0', stdout); + } else { + puts(str); + } + return recurse; +} + +enum ls_tree_cmdmode { + MODE_DEFAULT = 0, + MODE_LONG, + MODE_NAME_ONLY, + MODE_NAME_STATUS, + MODE_OBJECT_ONLY, +}; + +struct ls_tree_cmdmode_to_fmt { + enum ls_tree_cmdmode mode; + const char *const fmt; + read_tree_fn_t fn; +}; + +static struct ls_tree_cmdmode_to_fmt ls_tree_cmdmode_format[] = { + { + .mode = MODE_DEFAULT, + .fmt = "%(objectmode) %(objecttype) %(objectname)%x09%(path)", + .fn = show_tree_default, + }, + { + .mode = MODE_LONG, + .fmt = "%(objectmode) %(objecttype) %(objectname) %(objectsize:padded)%x09%(path)", + .fn = show_tree_long, + }, + { + .mode = MODE_NAME_ONLY, /* And MODE_NAME_STATUS */ + .fmt = "%(path)", + .fn = show_tree_name_only, + }, + { + .mode = MODE_OBJECT_ONLY, + .fmt = "%(objectname)", + .fn = show_tree_object + }, + { + /* fallback */ + .fn = show_tree_default, + }, +}; + +int cmd_ls_tree(int argc, const char **argv, const char *prefix) +{ + struct object_id oid; + struct tree *tree; + int i, full_tree = 0; + int full_name = !prefix || !*prefix; + read_tree_fn_t fn = NULL; + enum ls_tree_cmdmode cmdmode = MODE_DEFAULT; + int null_termination = 0; + struct ls_tree_options options = { 0 }; + const struct option ls_tree_options[] = { + OPT_BIT('d', NULL, &options.ls_options, N_("only show trees"), + LS_TREE_ONLY), + OPT_BIT('r', NULL, &options.ls_options, N_("recurse into subtrees"), + LS_RECURSIVE), + OPT_BIT('t', NULL, &options.ls_options, N_("show trees when recursing"), + LS_SHOW_TREES), + OPT_BOOL('z', NULL, &null_termination, + N_("terminate entries with NUL byte")), + OPT_CMDMODE('l', "long", &cmdmode, N_("include object size"), + MODE_LONG), + OPT_CMDMODE(0, "name-only", &cmdmode, N_("list only filenames"), + MODE_NAME_ONLY), + OPT_CMDMODE(0, "name-status", &cmdmode, N_("list only filenames"), + MODE_NAME_STATUS), + OPT_CMDMODE(0, "object-only", &cmdmode, N_("list only objects"), + MODE_OBJECT_ONLY), + OPT_BOOL(0, "full-name", &full_name, N_("use full path names")), + OPT_BOOL(0, "full-tree", &full_tree, + N_("list entire tree; not just current directory " + "(implies --full-name)")), + OPT_STRING_F(0, "format", &options.format, N_("format"), + N_("format to use for the output"), + PARSE_OPT_NONEG), + OPT__ABBREV(&options.abbrev), + OPT_END() + }; + struct ls_tree_cmdmode_to_fmt *m2f = ls_tree_cmdmode_format; + int ret; + + git_config(git_default_config, NULL); + + argc = parse_options(argc, argv, prefix, ls_tree_options, + ls_tree_usage, 0); + options.null_termination = null_termination; + + if (full_tree) + prefix = NULL; + options.prefix = full_name ? NULL : prefix; + + /* + * We wanted to detect conflicts between --name-only and + * --name-status, but once we're done with that subsequent + * code should only need to check the primary name. + */ + if (cmdmode == MODE_NAME_STATUS) + cmdmode = MODE_NAME_ONLY; + + /* -d -r should imply -t, but -d by itself should not have to. */ + if ( (LS_TREE_ONLY|LS_RECURSIVE) == + ((LS_TREE_ONLY|LS_RECURSIVE) & options.ls_options)) + options.ls_options |= LS_SHOW_TREES; + + if (options.format && cmdmode) + usage_msg_opt( + _("--format can't be combined with other format-altering options"), + ls_tree_usage, ls_tree_options); + if (argc < 1) + usage_with_options(ls_tree_usage, ls_tree_options); + if (repo_get_oid(the_repository, argv[0], &oid)) + die("Not a valid object name %s", argv[0]); + + /* + * show_recursive() rolls its own matching code and is + * generally ignorant of 'struct pathspec'. The magic mask + * cannot be lifted until it is converted to use + * match_pathspec() or tree_entry_interesting() + */ + parse_pathspec(&options.pathspec, PATHSPEC_ALL_MAGIC & + ~(PATHSPEC_FROMTOP | PATHSPEC_LITERAL), + PATHSPEC_PREFER_CWD, + prefix, argv + 1); + for (i = 0; i < options.pathspec.nr; i++) + options.pathspec.items[i].nowildcard_len = options.pathspec.items[i].len; + options.pathspec.has_wildcard = 0; + tree = parse_tree_indirect(&oid); + if (!tree) + die("not a tree object"); + /* + * The generic show_tree_fmt() is slower than show_tree(), so + * take the fast path if possible. + */ + while (m2f) { + if (!m2f->fmt) { + fn = options.format ? show_tree_fmt : show_tree_default; + } else if (options.format && !strcmp(options.format, m2f->fmt)) { + cmdmode = m2f->mode; + fn = m2f->fn; + } else if (!options.format && cmdmode == m2f->mode) { + fn = m2f->fn; + } else { + m2f++; + continue; + } + break; + } + + ret = !!read_tree(the_repository, tree, &options.pathspec, fn, &options); + clear_pathspec(&options.pathspec); + return ret; +} diff --git a/builtin/mailinfo.c b/builtin/mailinfo.c new file mode 100644 index 0000000..53b55dd --- /dev/null +++ b/builtin/mailinfo.c @@ -0,0 +1,116 @@ +/* + * Another stupid program, this one parsing the headers of an + * email to figure out authorship and subject + */ +#include "builtin.h" +#include "abspath.h" +#include "environment.h" +#include "gettext.h" +#include "utf8.h" +#include "strbuf.h" +#include "mailinfo.h" +#include "parse-options.h" + +static const char * const mailinfo_usage[] = { + /* TRANSLATORS: keep <> in "<" mail ">" info. */ + N_("git mailinfo [<options>] <msg> <patch> < mail >info"), + NULL, +}; + +struct metainfo_charset +{ + enum { + CHARSET_DEFAULT, + CHARSET_NO_REENCODE, + CHARSET_EXPLICIT, + } policy; + const char *charset; +}; + +static int parse_opt_explicit_encoding(const struct option *opt, + const char *arg, int unset) +{ + struct metainfo_charset *meta_charset = opt->value; + + BUG_ON_OPT_NEG(unset); + + meta_charset->policy = CHARSET_EXPLICIT; + meta_charset->charset = arg; + + return 0; +} + +static int parse_opt_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; +} + +int cmd_mailinfo(int argc, const char **argv, const char *prefix) +{ + struct metainfo_charset meta_charset; + struct mailinfo mi; + int status; + char *msgfile, *patchfile; + + struct option options[] = { + OPT_BOOL('k', NULL, &mi.keep_subject, N_("keep subject")), + OPT_BOOL('b', NULL, &mi.keep_non_patch_brackets_in_subject, + N_("keep non patch brackets in subject")), + OPT_BOOL('m', "message-id", &mi.add_message_id, + N_("copy Message-ID to the end of commit message")), + OPT_SET_INT_F('u', NULL, &meta_charset.policy, + N_("re-code metadata to i18n.commitEncoding"), + CHARSET_DEFAULT, PARSE_OPT_NONEG), + OPT_SET_INT_F('n', NULL, &meta_charset.policy, + N_("disable charset re-coding of metadata"), + CHARSET_NO_REENCODE, PARSE_OPT_NONEG), + OPT_CALLBACK_F(0, "encoding", &meta_charset, N_("encoding"), + N_("re-code metadata to this encoding"), + PARSE_OPT_NONEG, parse_opt_explicit_encoding), + OPT_BOOL(0, "scissors", &mi.use_scissors, N_("use scissors")), + OPT_CALLBACK_F(0, "quoted-cr", &mi.quoted_cr, N_("<action>"), + N_("action when quoted CR is found"), + PARSE_OPT_NONEG, parse_opt_quoted_cr), + OPT_HIDDEN_BOOL(0, "inbody-headers", &mi.use_inbody_headers, + N_("use headers in message's body")), + OPT_END() + }; + + setup_mailinfo(&mi); + meta_charset.policy = CHARSET_DEFAULT; + + argc = parse_options(argc, argv, prefix, options, mailinfo_usage, 0); + + if (argc != 2) + usage_with_options(mailinfo_usage, options); + + switch (meta_charset.policy) { + case CHARSET_DEFAULT: + mi.metainfo_charset = get_commit_output_encoding(); + break; + case CHARSET_NO_REENCODE: + mi.metainfo_charset = NULL; + break; + case CHARSET_EXPLICIT: + break; + default: + BUG("invalid meta_charset.policy"); + } + + mi.input = stdin; + mi.output = stdout; + + msgfile = prefix_filename(prefix, argv[0]); + patchfile = prefix_filename(prefix, argv[1]); + + status = !!mailinfo(&mi, msgfile, patchfile); + clear_mailinfo(&mi); + + free(msgfile); + free(patchfile); + return status; +} diff --git a/builtin/mailsplit.c b/builtin/mailsplit.c new file mode 100644 index 0000000..3af9ddb --- /dev/null +++ b/builtin/mailsplit.c @@ -0,0 +1,372 @@ +/* + * Totally braindamaged mbox splitter program. + * + * It just splits a mbox into a list of files: "0001" "0002" .. + * so you can process them further from there. + */ +#include "builtin.h" +#include "gettext.h" +#include "string-list.h" +#include "strbuf.h" + +static const char git_mailsplit_usage[] = +"git mailsplit [-d<prec>] [-f<n>] [-b] [--keep-cr] -o<directory> [(<mbox>|<Maildir>)...]"; + +static int is_from_line(const char *line, int len) +{ + const char *colon; + + if (len < 20 || memcmp("From ", line, 5)) + return 0; + + colon = line + len - 2; + line += 5; + for (;;) { + if (colon < line) + return 0; + if (*--colon == ':') + break; + } + + if (!isdigit(colon[-4]) || + !isdigit(colon[-2]) || + !isdigit(colon[-1]) || + !isdigit(colon[ 1]) || + !isdigit(colon[ 2])) + return 0; + + /* year */ + if (strtol(colon+3, NULL, 10) <= 90) + return 0; + + /* Ok, close enough */ + return 1; +} + +static struct strbuf buf = STRBUF_INIT; +static int keep_cr; +static int mboxrd; + +static int is_gtfrom(const struct strbuf *buf) +{ + size_t min = strlen(">From "); + size_t ngt; + + if (buf->len < min) + return 0; + + ngt = strspn(buf->buf, ">"); + return ngt && starts_with(buf->buf + ngt, "From "); +} + +/* Called with the first line (potentially partial) + * already in buf[] -- normally that should begin with + * the Unix "From " line. Write it into the specified + * file. + */ +static int split_one(FILE *mbox, const char *name, int allow_bare) +{ + FILE *output; + int fd; + int status = 0; + int is_bare = !is_from_line(buf.buf, buf.len); + + if (is_bare && !allow_bare) { + fprintf(stderr, "corrupt mailbox\n"); + exit(1); + } + fd = xopen(name, O_WRONLY | O_CREAT | O_EXCL, 0666); + output = xfdopen(fd, "w"); + + /* Copy it out, while searching for a line that begins with + * "From " and having something that looks like a date format. + */ + for (;;) { + if (!keep_cr && buf.len > 1 && buf.buf[buf.len-1] == '\n' && + buf.buf[buf.len-2] == '\r') { + strbuf_setlen(&buf, buf.len-2); + strbuf_addch(&buf, '\n'); + } + + if (mboxrd && is_gtfrom(&buf)) + strbuf_remove(&buf, 0, 1); + + if (fwrite(buf.buf, 1, buf.len, output) != buf.len) + die_errno("cannot write output"); + + if (strbuf_getwholeline(&buf, mbox, '\n')) { + if (feof(mbox)) { + status = 1; + break; + } + die_errno("cannot read mbox"); + } + if (!is_bare && is_from_line(buf.buf, buf.len)) + break; /* done with one message */ + } + fclose(output); + return status; +} + +static int populate_maildir_list(struct string_list *list, const char *path) +{ + DIR *dir; + struct dirent *dent; + char *name = NULL; + char *subs[] = { "cur", "new", NULL }; + char **sub; + int ret = -1; + + for (sub = subs; *sub; ++sub) { + free(name); + name = xstrfmt("%s/%s", path, *sub); + if (!(dir = opendir(name))) { + if (errno == ENOENT) + continue; + error_errno("cannot opendir %s", name); + goto out; + } + + while ((dent = readdir(dir)) != NULL) { + if (dent->d_name[0] == '.') + continue; + free(name); + name = xstrfmt("%s/%s", *sub, dent->d_name); + string_list_insert(list, name); + } + + closedir(dir); + } + + ret = 0; + +out: + free(name); + return ret; +} + +static int maildir_filename_cmp(const char *a, const char *b) +{ + while (*a && *b) { + if (isdigit(*a) && isdigit(*b)) { + long int na, nb; + na = strtol(a, (char **)&a, 10); + nb = strtol(b, (char **)&b, 10); + if (na != nb) + return na - nb; + /* strtol advanced our pointers */ + } + else { + if (*a != *b) + return (unsigned char)*a - (unsigned char)*b; + a++; + b++; + } + } + return (unsigned char)*a - (unsigned char)*b; +} + +static int split_maildir(const char *maildir, const char *dir, + int nr_prec, int skip) +{ + char *file = NULL; + FILE *f = NULL; + int ret = -1; + int i; + struct string_list list = STRING_LIST_INIT_DUP; + + list.cmp = maildir_filename_cmp; + + if (populate_maildir_list(&list, maildir) < 0) + goto out; + + for (i = 0; i < list.nr; i++) { + char *name; + + free(file); + file = xstrfmt("%s/%s", maildir, list.items[i].string); + + f = fopen(file, "r"); + if (!f) { + error_errno("cannot open mail %s", file); + goto out; + } + + if (strbuf_getwholeline(&buf, f, '\n')) { + error_errno("cannot read mail %s", file); + goto out; + } + + name = xstrfmt("%s/%0*d", dir, nr_prec, ++skip); + split_one(f, name, 1); + free(name); + + fclose(f); + f = NULL; + } + + ret = skip; +out: + if (f) + fclose(f); + free(file); + string_list_clear(&list, 1); + return ret; +} + +static int split_mbox(const char *file, const char *dir, int allow_bare, + int nr_prec, int skip) +{ + int ret = -1; + int peek; + + FILE *f = !strcmp(file, "-") ? stdin : fopen(file, "r"); + int file_done = 0; + + if (isatty(fileno(f))) + warning(_("reading patches from stdin/tty...")); + + if (!f) { + error_errno("cannot open mbox %s", file); + goto out; + } + + do { + peek = fgetc(f); + if (peek == EOF) { + if (f == stdin) + /* empty stdin is OK */ + ret = skip; + else { + fclose(f); + error(_("empty mbox: '%s'"), file); + } + goto out; + } + } while (isspace(peek)); + ungetc(peek, f); + + if (strbuf_getwholeline(&buf, f, '\n')) { + /* empty stdin is OK */ + if (f != stdin) { + error("cannot read mbox %s", file); + goto out; + } + file_done = 1; + } + + while (!file_done) { + char *name = xstrfmt("%s/%0*d", dir, nr_prec, ++skip); + file_done = split_one(f, name, allow_bare); + free(name); + } + + if (f != stdin) + fclose(f); + + ret = skip; +out: + return ret; +} + +int cmd_mailsplit(int argc, const char **argv, const char *prefix) +{ + int nr = 0, nr_prec = 4, num = 0; + int allow_bare = 0; + const char *dir = NULL; + const char **argp; + static const char *stdin_only[] = { "-", NULL }; + + BUG_ON_NON_EMPTY_PREFIX(prefix); + + for (argp = argv+1; *argp; argp++) { + const char *arg = *argp; + + if (arg[0] != '-') + break; + /* do flags here */ + if ( arg[1] == 'd' ) { + nr_prec = strtol(arg+2, NULL, 10); + if (nr_prec < 3 || 10 <= nr_prec) + usage(git_mailsplit_usage); + continue; + } else if ( arg[1] == 'f' ) { + nr = strtol(arg+2, NULL, 10); + } else if ( arg[1] == 'h' ) { + usage(git_mailsplit_usage); + } else if ( arg[1] == 'b' && !arg[2] ) { + allow_bare = 1; + } else if (!strcmp(arg, "--keep-cr")) { + keep_cr = 1; + } else if ( arg[1] == 'o' && arg[2] ) { + dir = arg+2; + } else if (!strcmp(arg, "--mboxrd")) { + mboxrd = 1; + } else if ( arg[1] == '-' && !arg[2] ) { + argp++; /* -- marks end of options */ + break; + } else { + die("unknown option: %s", arg); + } + } + + if ( !dir ) { + /* Backwards compatibility: if no -o specified, accept + <mbox> <dir> or just <dir> */ + switch (argc - (argp-argv)) { + case 1: + dir = argp[0]; + argp = stdin_only; + break; + case 2: + stdin_only[0] = argp[0]; + dir = argp[1]; + argp = stdin_only; + break; + default: + usage(git_mailsplit_usage); + } + } else { + /* New usage: if no more argument, parse stdin */ + if ( !*argp ) + argp = stdin_only; + } + + while (*argp) { + const char *arg = *argp++; + struct stat argstat; + int ret = 0; + + if (arg[0] == '-' && arg[1] == 0) { + ret = split_mbox(arg, dir, allow_bare, nr_prec, nr); + if (ret < 0) { + error("cannot split patches from stdin"); + return 1; + } + num += (ret - nr); + nr = ret; + continue; + } + + if (stat(arg, &argstat) == -1) { + error_errno("cannot stat %s", arg); + return 1; + } + + if (S_ISDIR(argstat.st_mode)) + ret = split_maildir(arg, dir, nr_prec, nr); + else + ret = split_mbox(arg, dir, allow_bare, nr_prec, nr); + + if (ret < 0) { + error("cannot split patches from %s", arg); + return 1; + } + num += (ret - nr); + nr = ret; + } + + printf("%d\n", num); + + return 0; +} diff --git a/builtin/merge-base.c b/builtin/merge-base.c new file mode 100644 index 0000000..e68b7fe --- /dev/null +++ b/builtin/merge-base.c @@ -0,0 +1,196 @@ +#include "builtin.h" +#include "config.h" +#include "commit.h" +#include "gettext.h" +#include "hex.h" +#include "refs.h" +#include "diff.h" +#include "revision.h" +#include "object-name.h" +#include "parse-options.h" +#include "repository.h" +#include "commit-reach.h" + +static int show_merge_base(struct commit **rev, int rev_nr, int show_all) +{ + struct commit_list *result, *r; + + result = repo_get_merge_bases_many_dirty(the_repository, rev[0], + rev_nr - 1, rev + 1); + + if (!result) + return 1; + + for (r = result; r; r = r->next) { + printf("%s\n", oid_to_hex(&r->item->object.oid)); + if (!show_all) + break; + } + + free_commit_list(result); + return 0; +} + +static const char * const merge_base_usage[] = { + N_("git merge-base [-a | --all] <commit> <commit>..."), + N_("git merge-base [-a | --all] --octopus <commit>..."), + N_("git merge-base --is-ancestor <commit> <commit>"), + N_("git merge-base --independent <commit>..."), + N_("git merge-base --fork-point <ref> [<commit>]"), + NULL +}; + +static struct commit *get_commit_reference(const char *arg) +{ + struct object_id revkey; + struct commit *r; + + if (repo_get_oid(the_repository, arg, &revkey)) + die("Not a valid object name %s", arg); + r = lookup_commit_reference(the_repository, &revkey); + if (!r) + die("Not a valid commit name %s", arg); + + return r; +} + +static int handle_independent(int count, const char **args) +{ + struct commit_list *revs = NULL, *rev; + int i; + + for (i = count - 1; i >= 0; i--) + commit_list_insert(get_commit_reference(args[i]), &revs); + + reduce_heads_replace(&revs); + + if (!revs) + return 1; + + for (rev = revs; rev; rev = rev->next) + printf("%s\n", oid_to_hex(&rev->item->object.oid)); + + free_commit_list(revs); + return 0; +} + +static int handle_octopus(int count, const char **args, int show_all) +{ + struct commit_list *revs = NULL; + struct commit_list *result, *rev; + int i; + + for (i = count - 1; i >= 0; i--) + commit_list_insert(get_commit_reference(args[i]), &revs); + + result = get_octopus_merge_bases(revs); + free_commit_list(revs); + reduce_heads_replace(&result); + + if (!result) + return 1; + + for (rev = result; rev; rev = rev->next) { + printf("%s\n", oid_to_hex(&rev->item->object.oid)); + if (!show_all) + break; + } + + free_commit_list(result); + return 0; +} + +static int handle_is_ancestor(int argc, const char **argv) +{ + struct commit *one, *two; + + if (argc != 2) + die("--is-ancestor takes exactly two commits"); + one = get_commit_reference(argv[0]); + two = get_commit_reference(argv[1]); + if (repo_in_merge_bases(the_repository, one, two)) + return 0; + else + return 1; +} + +static int handle_fork_point(int argc, const char **argv) +{ + struct object_id oid; + struct commit *derived, *fork_point; + const char *commitname; + + commitname = (argc == 2) ? argv[1] : "HEAD"; + if (repo_get_oid(the_repository, commitname, &oid)) + die("Not a valid object name: '%s'", commitname); + + derived = lookup_commit_reference(the_repository, &oid); + + fork_point = get_fork_point(argv[0], derived); + + if (!fork_point) + return 1; + + printf("%s\n", oid_to_hex(&fork_point->object.oid)); + return 0; +} + +int cmd_merge_base(int argc, const char **argv, const char *prefix) +{ + struct commit **rev; + int rev_nr = 0; + int show_all = 0; + int cmdmode = 0; + int ret; + + struct option options[] = { + OPT_BOOL('a', "all", &show_all, N_("output all common ancestors")), + OPT_CMDMODE(0, "octopus", &cmdmode, + N_("find ancestors for a single n-way merge"), 'o'), + OPT_CMDMODE(0, "independent", &cmdmode, + N_("list revs not reachable from others"), 'r'), + OPT_CMDMODE(0, "is-ancestor", &cmdmode, + N_("is the first one ancestor of the other?"), 'a'), + OPT_CMDMODE(0, "fork-point", &cmdmode, + N_("find where <commit> forked from reflog of <ref>"), 'f'), + OPT_END() + }; + + git_config(git_default_config, NULL); + argc = parse_options(argc, argv, prefix, options, merge_base_usage, 0); + + if (cmdmode == 'a') { + if (argc < 2) + usage_with_options(merge_base_usage, options); + if (show_all) + die(_("options '%s' and '%s' cannot be used together"), + "--is-ancestor", "--all"); + return handle_is_ancestor(argc, argv); + } + + if (cmdmode == 'r' && show_all) + die(_("options '%s' and '%s' cannot be used together"), + "--independent", "--all"); + + if (cmdmode == 'o') + return handle_octopus(argc, argv, show_all); + + if (cmdmode == 'r') + return handle_independent(argc, argv); + + if (cmdmode == 'f') { + if (argc < 1 || 2 < argc) + usage_with_options(merge_base_usage, options); + return handle_fork_point(argc, argv); + } + + if (argc < 2) + usage_with_options(merge_base_usage, options); + + ALLOC_ARRAY(rev, argc); + while (argc-- > 0) + rev[rev_nr++] = get_commit_reference(*argv++); + ret = show_merge_base(rev, rev_nr, show_all); + free(rev); + return ret; +} diff --git a/builtin/merge-file.c b/builtin/merge-file.c new file mode 100644 index 0000000..832c93d --- /dev/null +++ b/builtin/merge-file.c @@ -0,0 +1,156 @@ +#include "builtin.h" +#include "abspath.h" +#include "hex.h" +#include "object-name.h" +#include "object-store.h" +#include "config.h" +#include "gettext.h" +#include "setup.h" +#include "xdiff/xdiff.h" +#include "xdiff-interface.h" +#include "parse-options.h" + +static const char *const merge_file_usage[] = { + N_("git merge-file [<options>] [-L <name1> [-L <orig> [-L <name2>]]] <file1> <orig-file> <file2>"), + NULL +}; + +static int label_cb(const struct option *opt, const char *arg, int unset) +{ + static int label_count = 0; + const char **names = (const char **)opt->value; + + BUG_ON_OPT_NEG(unset); + + if (label_count >= 3) + return error("too many labels on the command line"); + names[label_count++] = arg; + return 0; +} + +int cmd_merge_file(int argc, const char **argv, const char *prefix) +{ + const char *names[3] = { 0 }; + mmfile_t mmfs[3] = { 0 }; + mmbuffer_t result = { 0 }; + xmparam_t xmp = { 0 }; + int ret = 0, i = 0, to_stdout = 0, object_id = 0; + int quiet = 0; + struct option options[] = { + OPT_BOOL('p', "stdout", &to_stdout, N_("send results to standard output")), + OPT_BOOL(0, "object-id", &object_id, N_("use object IDs instead of filenames")), + OPT_SET_INT(0, "diff3", &xmp.style, N_("use a diff3 based merge"), XDL_MERGE_DIFF3), + OPT_SET_INT(0, "zdiff3", &xmp.style, N_("use a zealous diff3 based merge"), + XDL_MERGE_ZEALOUS_DIFF3), + OPT_SET_INT(0, "ours", &xmp.favor, N_("for conflicts, use our version"), + XDL_MERGE_FAVOR_OURS), + OPT_SET_INT(0, "theirs", &xmp.favor, N_("for conflicts, use their version"), + XDL_MERGE_FAVOR_THEIRS), + OPT_SET_INT(0, "union", &xmp.favor, N_("for conflicts, use a union version"), + XDL_MERGE_FAVOR_UNION), + OPT_INTEGER(0, "marker-size", &xmp.marker_size, + N_("for conflicts, use this marker size")), + OPT__QUIET(&quiet, N_("do not warn about conflicts")), + OPT_CALLBACK('L', NULL, names, N_("name"), + N_("set labels for file1/orig-file/file2"), &label_cb), + OPT_END(), + }; + + xmp.level = XDL_MERGE_ZEALOUS_ALNUM; + xmp.style = 0; + xmp.favor = 0; + + if (startup_info->have_repository) { + /* Read the configuration file */ + git_config(git_xmerge_config, NULL); + if (0 <= git_xmerge_style) + xmp.style = git_xmerge_style; + } + + argc = parse_options(argc, argv, prefix, options, merge_file_usage, 0); + if (argc != 3) + usage_with_options(merge_file_usage, options); + if (quiet) { + if (!freopen("/dev/null", "w", stderr)) + return error_errno("failed to redirect stderr to /dev/null"); + } + + if (object_id) + setup_git_directory(); + + for (i = 0; i < 3; i++) { + char *fname; + struct object_id oid; + mmfile_t *mmf = mmfs + i; + + if (!names[i]) + names[i] = argv[i]; + + fname = prefix_filename(prefix, argv[i]); + + if (object_id) { + if (repo_get_oid(the_repository, argv[i], &oid)) + ret = error(_("object '%s' does not exist"), + argv[i]); + else if (!oideq(&oid, the_hash_algo->empty_blob)) + read_mmblob(mmf, &oid); + else + read_mmfile(mmf, "/dev/null"); + } else if (read_mmfile(mmf, fname)) { + ret = -1; + } + if (ret != -1 && (mmf->size > MAX_XDIFF_SIZE || + buffer_is_binary(mmf->ptr, mmf->size))) { + ret = error("Cannot merge binary files: %s", + argv[i]); + } + + free(fname); + if (ret) + goto cleanup; + + } + + xmp.ancestor = names[1]; + xmp.file1 = names[0]; + xmp.file2 = names[2]; + ret = xdl_merge(mmfs + 1, mmfs + 0, mmfs + 2, &xmp, &result); + + if (ret >= 0) { + if (object_id && !to_stdout) { + struct object_id oid; + if (result.size) { + if (write_object_file(result.ptr, result.size, OBJ_BLOB, &oid) < 0) + ret = error(_("Could not write object file")); + } else { + oidcpy(&oid, the_hash_algo->empty_blob); + } + if (ret >= 0) + printf("%s\n", oid_to_hex(&oid)); + } else { + const char *filename = argv[0]; + char *fpath = prefix_filename(prefix, argv[0]); + FILE *f = to_stdout ? stdout : fopen(fpath, "wb"); + + if (!f) + ret = error_errno("Could not open %s for writing", + filename); + else if (result.size && + fwrite(result.ptr, result.size, 1, f) != 1) + ret = error_errno("Could not write to %s", filename); + else if (fclose(f)) + ret = error_errno("Could not close %s", filename); + free(fpath); + } + free(result.ptr); + } + + if (ret > 127) + ret = 127; + +cleanup: + for (i = 0; i < 3; i++) + free(mmfs[i].ptr); + + return ret; +} diff --git a/builtin/merge-index.c b/builtin/merge-index.c new file mode 100644 index 0000000..270d5f6 --- /dev/null +++ b/builtin/merge-index.c @@ -0,0 +1,122 @@ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "hex.h" +#include "read-cache-ll.h" +#include "repository.h" +#include "run-command.h" +#include "sparse-index.h" + +static const char *pgm; +static int one_shot, quiet; +static int err; + +static int merge_entry(int pos, const char *path) +{ + int found; + const char *arguments[] = { pgm, "", "", "", path, "", "", "", NULL }; + char hexbuf[4][GIT_MAX_HEXSZ + 1]; + char ownbuf[4][60]; + struct child_process cmd = CHILD_PROCESS_INIT; + + if (pos >= the_index.cache_nr) + die("git merge-index: %s not in the cache", path); + found = 0; + do { + const struct cache_entry *ce = the_index.cache[pos]; + int stage = ce_stage(ce); + + if (strcmp(ce->name, path)) + break; + found++; + oid_to_hex_r(hexbuf[stage], &ce->oid); + xsnprintf(ownbuf[stage], sizeof(ownbuf[stage]), "%o", ce->ce_mode); + arguments[stage] = hexbuf[stage]; + arguments[stage + 4] = ownbuf[stage]; + } while (++pos < the_index.cache_nr); + if (!found) + die("git merge-index: %s not in the cache", path); + + strvec_pushv(&cmd.args, arguments); + if (run_command(&cmd)) { + if (one_shot) + err++; + else { + if (!quiet) + die("merge program failed"); + exit(1); + } + } + return found; +} + +static void merge_one_path(const char *path) +{ + int pos = index_name_pos(&the_index, path, strlen(path)); + + /* + * If it already exists in the cache as stage0, it's + * already merged and there is nothing to do. + */ + if (pos < 0) + merge_entry(-pos-1, path); +} + +static void merge_all(void) +{ + int i; + /* TODO: audit for interaction with sparse-index. */ + ensure_full_index(&the_index); + for (i = 0; i < the_index.cache_nr; i++) { + const struct cache_entry *ce = the_index.cache[i]; + if (!ce_stage(ce)) + continue; + i += merge_entry(i, ce->name)-1; + } +} + +int cmd_merge_index(int argc, const char **argv, const char *prefix UNUSED) +{ + int i, force_file = 0; + + /* Without this we cannot rely on waitpid() to tell + * what happened to our children. + */ + signal(SIGCHLD, SIG_DFL); + + if (argc < 3) + usage("git merge-index [-o] [-q] <merge-program> (-a | [--] [<filename>...])"); + + repo_read_index(the_repository); + + /* TODO: audit for interaction with sparse-index. */ + ensure_full_index(&the_index); + + i = 1; + if (!strcmp(argv[i], "-o")) { + one_shot = 1; + i++; + } + if (!strcmp(argv[i], "-q")) { + quiet = 1; + i++; + } + pgm = argv[i++]; + for (; i < argc; i++) { + const char *arg = argv[i]; + if (!force_file && *arg == '-') { + if (!strcmp(arg, "--")) { + force_file = 1; + continue; + } + if (!strcmp(arg, "-a")) { + merge_all(); + continue; + } + die("git merge-index: unknown option %s", arg); + } + merge_one_path(arg); + } + if (err && !quiet) + die("merge program failed"); + return err; +} diff --git a/builtin/merge-ours.c b/builtin/merge-ours.c new file mode 100644 index 0000000..932924e --- /dev/null +++ b/builtin/merge-ours.c @@ -0,0 +1,33 @@ +/* + * Implementation of git-merge-ours.sh as builtin + * + * Copyright (c) 2007 Thomas Harning Jr + * Original: + * Original Copyright (c) 2005 Junio C Hamano + * + * Pretend we resolved the heads, but declare our tree trumps everybody else. + */ +#include "git-compat-util.h" +#include "builtin.h" +#include "diff.h" +#include "repository.h" + +static const char builtin_merge_ours_usage[] = + "git merge-ours <base>... -- HEAD <remote>..."; + +int cmd_merge_ours(int argc, const char **argv, const char *prefix UNUSED) +{ + if (argc == 2 && !strcmp(argv[1], "-h")) + usage(builtin_merge_ours_usage); + + /* + * The contents of the current index becomes the tree we + * commit. The index must match HEAD, or this merge cannot go + * through. + */ + if (repo_read_index(the_repository) < 0) + die_errno("read_cache failed"); + if (index_differs_from(the_repository, "HEAD", NULL, 0)) + return 2; + return 0; +} diff --git a/builtin/merge-recursive.c b/builtin/merge-recursive.c new file mode 100644 index 0000000..3366699 --- /dev/null +++ b/builtin/merge-recursive.c @@ -0,0 +1,96 @@ +#include "builtin.h" +#include "advice.h" +#include "commit.h" +#include "gettext.h" +#include "hash.h" +#include "tag.h" +#include "merge-recursive.h" +#include "object-name.h" +#include "repository.h" +#include "xdiff-interface.h" + +static const char builtin_merge_recursive_usage[] = + "git %s <base>... -- <head> <remote> ..."; + +static char *better_branch_name(const char *branch) +{ + static char githead_env[8 + GIT_MAX_HEXSZ + 1]; + char *name; + + if (strlen(branch) != the_hash_algo->hexsz) + return xstrdup(branch); + xsnprintf(githead_env, sizeof(githead_env), "GITHEAD_%s", branch); + name = getenv(githead_env); + return xstrdup(name ? name : branch); +} + +int cmd_merge_recursive(int argc, const char **argv, const char *prefix UNUSED) +{ + const struct object_id *bases[21]; + unsigned bases_count = 0; + int i, failed; + struct object_id h1, h2; + struct merge_options o; + char *better1, *better2; + struct commit *result; + + init_merge_options(&o, the_repository); + if (argv[0] && ends_with(argv[0], "-subtree")) + o.subtree_shift = ""; + + if (argc < 4) + usagef(builtin_merge_recursive_usage, argv[0]); + + for (i = 1; i < argc; ++i) { + const char *arg = argv[i]; + + if (starts_with(arg, "--")) { + if (!arg[2]) + break; + if (parse_merge_opt(&o, arg + 2)) + die(_("unknown option %s"), arg); + continue; + } + if (bases_count < ARRAY_SIZE(bases)-1) { + struct object_id *oid = xmalloc(sizeof(struct object_id)); + if (repo_get_oid(the_repository, argv[i], oid)) + die(_("could not parse object '%s'"), argv[i]); + bases[bases_count++] = oid; + } + else + warning(Q_("cannot handle more than %d base. " + "Ignoring %s.", + "cannot handle more than %d bases. " + "Ignoring %s.", + ARRAY_SIZE(bases)-1), + (int)ARRAY_SIZE(bases)-1, argv[i]); + } + if (argc - i != 3) /* "--" "<head>" "<remote>" */ + die(_("not handling anything other than two heads merge.")); + + if (repo_read_index_unmerged(the_repository)) + die_resolve_conflict("merge"); + + o.branch1 = argv[++i]; + o.branch2 = argv[++i]; + + if (repo_get_oid(the_repository, o.branch1, &h1)) + die(_("could not resolve ref '%s'"), o.branch1); + if (repo_get_oid(the_repository, o.branch2, &h2)) + die(_("could not resolve ref '%s'"), o.branch2); + + o.branch1 = better1 = better_branch_name(o.branch1); + o.branch2 = better2 = better_branch_name(o.branch2); + + if (o.verbosity >= 3) + printf(_("Merging %s with %s\n"), o.branch1, o.branch2); + + failed = merge_recursive_generic(&o, &h1, &h2, bases_count, bases, &result); + + free(better1); + free(better2); + + if (failed < 0) + return 128; /* die() error code */ + return failed; +} diff --git a/builtin/merge-tree.c b/builtin/merge-tree.c new file mode 100644 index 0000000..a35e045 --- /dev/null +++ b/builtin/merge-tree.c @@ -0,0 +1,655 @@ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "tree-walk.h" +#include "xdiff-interface.h" +#include "help.h" +#include "gettext.h" +#include "hex.h" +#include "commit.h" +#include "commit-reach.h" +#include "merge-ort.h" +#include "object-name.h" +#include "object-store-ll.h" +#include "parse-options.h" +#include "repository.h" +#include "blob.h" +#include "exec-cmd.h" +#include "merge-blobs.h" +#include "quote.h" +#include "tree.h" +#include "config.h" +#include "strvec.h" + +static int line_termination = '\n'; + +struct merge_list { + struct merge_list *next; + struct merge_list *link; /* other stages for this object */ + + unsigned int stage : 2; + unsigned int mode; + const char *path; + struct blob *blob; +}; + +static struct merge_list *merge_result, **merge_result_end = &merge_result; + +static void add_merge_entry(struct merge_list *entry) +{ + *merge_result_end = entry; + merge_result_end = &entry->next; +} + +static void trivial_merge_trees(struct tree_desc t[3], const char *base); + +static const char *explanation(struct merge_list *entry) +{ + switch (entry->stage) { + case 0: + return "merged"; + case 3: + return "added in remote"; + case 2: + if (entry->link) + return "added in both"; + return "added in local"; + } + + /* Existed in base */ + entry = entry->link; + if (!entry) + return "removed in both"; + + if (entry->link) + return "changed in both"; + + if (entry->stage == 3) + return "removed in local"; + return "removed in remote"; +} + +static void *result(struct merge_list *entry, unsigned long *size) +{ + enum object_type type; + struct blob *base, *our, *their; + const char *path = entry->path; + + if (!entry->stage) + return repo_read_object_file(the_repository, + &entry->blob->object.oid, &type, + size); + base = NULL; + if (entry->stage == 1) { + base = entry->blob; + entry = entry->link; + } + our = NULL; + if (entry && entry->stage == 2) { + our = entry->blob; + entry = entry->link; + } + their = NULL; + if (entry) + their = entry->blob; + return merge_blobs(the_repository->index, path, + base, our, their, size); +} + +static void *origin(struct merge_list *entry, unsigned long *size) +{ + enum object_type type; + while (entry) { + if (entry->stage == 2) + return repo_read_object_file(the_repository, + &entry->blob->object.oid, + &type, size); + entry = entry->link; + } + return NULL; +} + +static int show_outf(void *priv UNUSED, mmbuffer_t *mb, int nbuf) +{ + int i; + for (i = 0; i < nbuf; i++) + printf("%.*s", (int) mb[i].size, mb[i].ptr); + return 0; +} + +static void show_diff(struct merge_list *entry) +{ + unsigned long size; + mmfile_t src, dst; + xpparam_t xpp; + xdemitconf_t xecfg; + xdemitcb_t ecb = { .out_line = show_outf }; + + memset(&xpp, 0, sizeof(xpp)); + xpp.flags = 0; + memset(&xecfg, 0, sizeof(xecfg)); + xecfg.ctxlen = 3; + + src.ptr = origin(entry, &size); + if (!src.ptr) + size = 0; + src.size = size; + dst.ptr = result(entry, &size); + if (!dst.ptr) + size = 0; + dst.size = size; + if (xdi_diff(&src, &dst, &xpp, &xecfg, &ecb)) + die("unable to generate diff"); + free(src.ptr); + free(dst.ptr); +} + +static void show_result_list(struct merge_list *entry) +{ + printf("%s\n", explanation(entry)); + do { + struct merge_list *link = entry->link; + static const char *desc[4] = { "result", "base", "our", "their" }; + printf(" %-6s %o %s %s\n", desc[entry->stage], entry->mode, oid_to_hex(&entry->blob->object.oid), entry->path); + entry = link; + } while (entry); +} + +static void show_result(void) +{ + struct merge_list *walk; + + walk = merge_result; + while (walk) { + show_result_list(walk); + show_diff(walk); + walk = walk->next; + } +} + +/* An empty entry never compares same, not even to another empty entry */ +static int same_entry(struct name_entry *a, struct name_entry *b) +{ + return !is_null_oid(&a->oid) && + !is_null_oid(&b->oid) && + oideq(&a->oid, &b->oid) && + a->mode == b->mode; +} + +static int both_empty(struct name_entry *a, struct name_entry *b) +{ + return is_null_oid(&a->oid) && is_null_oid(&b->oid); +} + +static struct merge_list *create_entry(unsigned stage, unsigned mode, const struct object_id *oid, const char *path) +{ + struct merge_list *res = xcalloc(1, sizeof(*res)); + + res->stage = stage; + res->path = path; + res->mode = mode; + res->blob = lookup_blob(the_repository, oid); + return res; +} + +static char *traverse_path(const struct traverse_info *info, const struct name_entry *n) +{ + struct strbuf buf = STRBUF_INIT; + strbuf_make_traverse_path(&buf, info, n->path, n->pathlen); + return strbuf_detach(&buf, NULL); +} + +static void resolve(const struct traverse_info *info, struct name_entry *ours, struct name_entry *result) +{ + struct merge_list *orig, *final; + const char *path; + + /* If it's already ours, don't bother showing it */ + if (!ours) + return; + + path = traverse_path(info, result); + orig = create_entry(2, ours->mode, &ours->oid, path); + final = create_entry(0, result->mode, &result->oid, path); + + final->link = orig; + + add_merge_entry(final); +} + +static void unresolved_directory(const struct traverse_info *info, + struct name_entry n[3]) +{ + struct repository *r = the_repository; + char *newbase; + struct name_entry *p; + struct tree_desc t[3]; + void *buf0, *buf1, *buf2; + + for (p = n; p < n + 3; p++) { + if (p->mode && S_ISDIR(p->mode)) + break; + } + if (n + 3 <= p) + return; /* there is no tree here */ + + newbase = traverse_path(info, p); + +#define ENTRY_OID(e) (((e)->mode && S_ISDIR((e)->mode)) ? &(e)->oid : NULL) + buf0 = fill_tree_descriptor(r, t + 0, ENTRY_OID(n + 0)); + buf1 = fill_tree_descriptor(r, t + 1, ENTRY_OID(n + 1)); + buf2 = fill_tree_descriptor(r, t + 2, ENTRY_OID(n + 2)); +#undef ENTRY_OID + + trivial_merge_trees(t, newbase); + + free(buf0); + free(buf1); + free(buf2); + free(newbase); +} + + +static struct merge_list *link_entry(unsigned stage, const struct traverse_info *info, struct name_entry *n, struct merge_list *entry) +{ + const char *path; + struct merge_list *link; + + if (!n->mode) + return entry; + if (entry) + path = entry->path; + else + path = traverse_path(info, n); + link = create_entry(stage, n->mode, &n->oid, path); + link->link = entry; + return link; +} + +static void unresolved(const struct traverse_info *info, struct name_entry n[3]) +{ + struct merge_list *entry = NULL; + int i; + unsigned dirmask = 0, mask = 0; + + for (i = 0; i < 3; i++) { + mask |= (1 << i); + /* + * Treat missing entries as directories so that we return + * after unresolved_directory has handled this. + */ + if (!n[i].mode || S_ISDIR(n[i].mode)) + dirmask |= (1 << i); + } + + unresolved_directory(info, n); + + if (dirmask == mask) + return; + + if (n[2].mode && !S_ISDIR(n[2].mode)) + entry = link_entry(3, info, n + 2, entry); + if (n[1].mode && !S_ISDIR(n[1].mode)) + entry = link_entry(2, info, n + 1, entry); + if (n[0].mode && !S_ISDIR(n[0].mode)) + entry = link_entry(1, info, n + 0, entry); + + add_merge_entry(entry); +} + +/* + * Merge two trees together (t[1] and t[2]), using a common base (t[0]) + * as the origin. + * + * This walks the (sorted) trees in lock-step, checking every possible + * name. Note that directories automatically sort differently from other + * files (see "base_name_compare"), so you'll never see file/directory + * conflicts, because they won't ever compare the same. + * + * IOW, if a directory changes to a filename, it will automatically be + * seen as the directory going away, and the filename being created. + * + * Think of this as a three-way diff. + * + * The output will be either: + * - successful merge + * "0 mode sha1 filename" + * NOTE NOTE NOTE! FIXME! We really really need to walk the index + * in parallel with this too! + * + * - conflict: + * "1 mode sha1 filename" + * "2 mode sha1 filename" + * "3 mode sha1 filename" + * where not all of the 1/2/3 lines may exist, of course. + * + * The successful merge rules are the same as for the three-way merge + * in git-read-tree. + */ +static int threeway_callback(int n UNUSED, unsigned long mask, + unsigned long dirmask UNUSED, + struct name_entry *entry, struct traverse_info *info) +{ + /* Same in both? */ + if (same_entry(entry+1, entry+2) || both_empty(entry+1, entry+2)) { + /* Modified, added or removed identically */ + resolve(info, NULL, entry+1); + return mask; + } + + if (same_entry(entry+0, entry+1)) { + if (!is_null_oid(&entry[2].oid) && !S_ISDIR(entry[2].mode)) { + /* We did not touch, they modified -- take theirs */ + resolve(info, entry+1, entry+2); + return mask; + } + /* + * If we did not touch a directory but they made it + * into a file, we fall through and unresolved() + * recurses down. Likewise for the opposite case. + */ + } + + if (same_entry(entry+0, entry+2) || both_empty(entry+0, entry+2)) { + /* We added, modified or removed, they did not touch -- take ours */ + resolve(info, NULL, entry+1); + return mask; + } + + unresolved(info, entry); + return mask; +} + +static void trivial_merge_trees(struct tree_desc t[3], const char *base) +{ + struct traverse_info info; + + setup_traverse_info(&info, base); + info.fn = threeway_callback; + traverse_trees(&the_index, 3, t, &info); +} + +static void *get_tree_descriptor(struct repository *r, + struct tree_desc *desc, + const char *rev) +{ + struct object_id oid; + void *buf; + + if (repo_get_oid(r, rev, &oid)) + die("unknown rev %s", rev); + buf = fill_tree_descriptor(r, desc, &oid); + if (!buf) + die("%s is not a tree", rev); + return buf; +} + +static int trivial_merge(const char *base, + const char *branch1, + const char *branch2) +{ + struct repository *r = the_repository; + struct tree_desc t[3]; + void *buf1, *buf2, *buf3; + + buf1 = get_tree_descriptor(r, t+0, base); + buf2 = get_tree_descriptor(r, t+1, branch1); + buf3 = get_tree_descriptor(r, t+2, branch2); + trivial_merge_trees(t, ""); + free(buf1); + free(buf2); + free(buf3); + + show_result(); + return 0; +} + +enum mode { + MODE_UNKNOWN, + MODE_TRIVIAL, + MODE_REAL, +}; + +struct merge_tree_options { + int mode; + int allow_unrelated_histories; + int show_messages; + int name_only; + int use_stdin; + struct merge_options merge_options; +}; + +static int real_merge(struct merge_tree_options *o, + const char *merge_base, + const char *branch1, const char *branch2, + const char *prefix) +{ + struct commit *parent1, *parent2; + struct commit_list *merge_bases = NULL; + struct merge_result result = { 0 }; + int show_messages = o->show_messages; + struct merge_options opt; + + copy_merge_options(&opt, &o->merge_options); + parent1 = get_merge_parent(branch1); + if (!parent1) + help_unknown_ref(branch1, "merge-tree", + _("not something we can merge")); + + parent2 = get_merge_parent(branch2); + if (!parent2) + help_unknown_ref(branch2, "merge-tree", + _("not something we can merge")); + + opt.show_rename_progress = 0; + + opt.branch1 = branch1; + opt.branch2 = branch2; + + if (merge_base) { + struct commit *base_commit; + struct tree *base_tree, *parent1_tree, *parent2_tree; + + base_commit = lookup_commit_reference_by_name(merge_base); + if (!base_commit) + die(_("could not lookup commit '%s'"), merge_base); + + opt.ancestor = merge_base; + base_tree = repo_get_commit_tree(the_repository, base_commit); + parent1_tree = repo_get_commit_tree(the_repository, parent1); + parent2_tree = repo_get_commit_tree(the_repository, parent2); + merge_incore_nonrecursive(&opt, base_tree, parent1_tree, parent2_tree, &result); + } else { + /* + * Get the merge bases, in reverse order; see comment above + * merge_incore_recursive in merge-ort.h + */ + merge_bases = repo_get_merge_bases(the_repository, parent1, + parent2); + if (!merge_bases && !o->allow_unrelated_histories) + die(_("refusing to merge unrelated histories")); + merge_bases = reverse_commit_list(merge_bases); + merge_incore_recursive(&opt, merge_bases, parent1, parent2, &result); + } + + if (result.clean < 0) + die(_("failure to merge")); + + if (show_messages == -1) + show_messages = !result.clean; + + if (o->use_stdin) + printf("%d%c", result.clean, line_termination); + printf("%s%c", oid_to_hex(&result.tree->object.oid), line_termination); + if (!result.clean) { + struct string_list conflicted_files = STRING_LIST_INIT_NODUP; + const char *last = NULL; + int i; + + merge_get_conflicted_files(&result, &conflicted_files); + for (i = 0; i < conflicted_files.nr; i++) { + const char *name = conflicted_files.items[i].string; + struct stage_info *c = conflicted_files.items[i].util; + if (!o->name_only) + printf("%06o %s %d\t", + c->mode, oid_to_hex(&c->oid), c->stage); + else if (last && !strcmp(last, name)) + continue; + write_name_quoted_relative( + name, prefix, stdout, line_termination); + last = name; + } + string_list_clear(&conflicted_files, 1); + } + if (show_messages) { + putchar(line_termination); + merge_display_update_messages(&opt, line_termination == '\0', + &result); + } + if (o->use_stdin) + putchar(line_termination); + merge_finalize(&opt, &result); + clear_merge_options(&opt); + return !result.clean; /* result.clean < 0 handled above */ +} + +int cmd_merge_tree(int argc, const char **argv, const char *prefix) +{ + struct merge_tree_options o = { .show_messages = -1 }; + struct strvec xopts = STRVEC_INIT; + int expected_remaining_argc; + int original_argc; + const char *merge_base = NULL; + + const char * const merge_tree_usage[] = { + N_("git merge-tree [--write-tree] [<options>] <branch1> <branch2>"), + N_("git merge-tree [--trivial-merge] <base-tree> <branch1> <branch2>"), + NULL + }; + struct option mt_options[] = { + OPT_CMDMODE(0, "write-tree", &o.mode, + N_("do a real merge instead of a trivial merge"), + MODE_REAL), + OPT_CMDMODE(0, "trivial-merge", &o.mode, + N_("do a trivial merge only"), MODE_TRIVIAL), + OPT_BOOL(0, "messages", &o.show_messages, + N_("also show informational/conflict messages")), + OPT_SET_INT('z', NULL, &line_termination, + N_("separate paths with the NUL character"), '\0'), + OPT_BOOL_F(0, "name-only", + &o.name_only, + N_("list filenames without modes/oids/stages"), + PARSE_OPT_NONEG), + OPT_BOOL_F(0, "allow-unrelated-histories", + &o.allow_unrelated_histories, + N_("allow merging unrelated histories"), + PARSE_OPT_NONEG), + OPT_BOOL_F(0, "stdin", + &o.use_stdin, + N_("perform multiple merges, one per line of input"), + PARSE_OPT_NONEG), + OPT_STRING(0, "merge-base", + &merge_base, + N_("commit"), + N_("specify a merge-base for the merge")), + OPT_STRVEC('X', "strategy-option", &xopts, N_("option=value"), + N_("option for selected merge strategy")), + OPT_END() + }; + + /* Init merge options */ + init_merge_options(&o.merge_options, the_repository); + + /* Parse arguments */ + original_argc = argc - 1; /* ignoring argv[0] */ + argc = parse_options(argc, argv, prefix, mt_options, + merge_tree_usage, PARSE_OPT_STOP_AT_NON_OPTION); + + if (xopts.nr && o.mode == MODE_TRIVIAL) + die(_("--trivial-merge is incompatible with all other options")); + for (int x = 0; x < xopts.nr; x++) + if (parse_merge_opt(&o.merge_options, xopts.v[x])) + die(_("unknown strategy option: -X%s"), xopts.v[x]); + + /* Handle --stdin */ + if (o.use_stdin) { + struct strbuf buf = STRBUF_INIT; + + if (o.mode == MODE_TRIVIAL) + die(_("--trivial-merge is incompatible with all other options")); + if (merge_base) + die(_("--merge-base is incompatible with --stdin")); + line_termination = '\0'; + while (strbuf_getline_lf(&buf, stdin) != EOF) { + struct strbuf **split; + int result; + const char *input_merge_base = NULL; + + split = strbuf_split(&buf, ' '); + if (!split[0] || !split[1]) + die(_("malformed input line: '%s'."), buf.buf); + strbuf_rtrim(split[0]); + strbuf_rtrim(split[1]); + + /* parse the merge-base */ + if (!strcmp(split[1]->buf, "--")) { + input_merge_base = split[0]->buf; + } + + if (input_merge_base && split[2] && split[3] && !split[4]) { + strbuf_rtrim(split[2]); + strbuf_rtrim(split[3]); + result = real_merge(&o, input_merge_base, split[2]->buf, split[3]->buf, prefix); + } else if (!input_merge_base && !split[2]) { + result = real_merge(&o, NULL, split[0]->buf, split[1]->buf, prefix); + } else { + die(_("malformed input line: '%s'."), buf.buf); + } + + if (result < 0) + die(_("merging cannot continue; got unclean result of %d"), result); + strbuf_list_free(split); + } + strbuf_release(&buf); + return 0; + } + + /* Figure out which mode to use */ + switch (o.mode) { + default: + BUG("unexpected command mode %d", o.mode); + case MODE_UNKNOWN: + switch (argc) { + default: + usage_with_options(merge_tree_usage, mt_options); + case 2: + o.mode = MODE_REAL; + break; + case 3: + o.mode = MODE_TRIVIAL; + break; + } + expected_remaining_argc = argc; + break; + case MODE_REAL: + expected_remaining_argc = 2; + break; + case MODE_TRIVIAL: + expected_remaining_argc = 3; + /* Removal of `--trivial-merge` is expected */ + original_argc--; + break; + } + if (o.mode == MODE_TRIVIAL && argc < original_argc) + die(_("--trivial-merge is incompatible with all other options")); + + if (argc != expected_remaining_argc) + usage_with_options(merge_tree_usage, mt_options); + + git_config(git_default_config, NULL); + + /* Do the relevant type of merge */ + if (o.mode == MODE_REAL) + return real_merge(&o, merge_base, argv[0], argv[1], prefix); + else + return trivial_merge(argv[0], argv[1], argv[2]); +} diff --git a/builtin/merge.c b/builtin/merge.c new file mode 100644 index 0000000..d748d46 --- /dev/null +++ b/builtin/merge.c @@ -0,0 +1,1786 @@ +/* + * Builtin "git merge" + * + * Copyright (c) 2008 Miklos Vajna <vmiklos@frugalware.org> + * + * Based on git-merge.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 "gettext.h" +#include "hex.h" +#include "object-name.h" +#include "parse-options.h" +#include "lockfile.h" +#include "run-command.h" +#include "hook.h" +#include "diff.h" +#include "diff-merges.h" +#include "refs.h" +#include "refspec.h" +#include "commit.h" +#include "diffcore.h" +#include "path.h" +#include "revision.h" +#include "unpack-trees.h" +#include "cache-tree.h" +#include "dir.h" +#include "utf8.h" +#include "log-tree.h" +#include "color.h" +#include "rerere.h" +#include "help.h" +#include "merge.h" +#include "merge-recursive.h" +#include "merge-ort-wrappers.h" +#include "resolve-undo.h" +#include "remote.h" +#include "fmt-merge-msg.h" +#include "gpg-interface.h" +#include "sequencer.h" +#include "string-list.h" +#include "packfile.h" +#include "tag.h" +#include "alias.h" +#include "branch.h" +#include "commit-reach.h" +#include "wt-status.h" +#include "commit-graph.h" + +#define DEFAULT_TWOHEAD (1<<0) +#define DEFAULT_OCTOPUS (1<<1) +#define NO_FAST_FORWARD (1<<2) +#define NO_TRIVIAL (1<<3) + +struct strategy { + const char *name; + unsigned attr; +}; + +static const char * const builtin_merge_usage[] = { + N_("git merge [<options>] [<commit>...]"), + "git merge --abort", + "git merge --continue", + NULL +}; + +static int show_diffstat = 1, shortlog_len = -1, squash; +static int option_commit = -1; +static int option_edit = -1; +static int allow_trivial = 1, have_message, verify_signatures; +static int check_trust_level = 1; +static int overwrite_ignore = 1; +static struct strbuf merge_msg = STRBUF_INIT; +static struct strategy **use_strategies; +static size_t use_strategies_nr, use_strategies_alloc; +static struct strvec xopts = STRVEC_INIT; +static const char *branch; +static char *branch_mergeoptions; +static int verbosity; +static int allow_rerere_auto; +static int abort_current_merge; +static int quit_current_merge; +static int continue_current_merge; +static int allow_unrelated_histories; +static int show_progress = -1; +static int default_to_upstream = 1; +static int signoff; +static const char *sign_commit; +static int autostash; +static int no_verify; +static char *into_name; + +static struct strategy all_strategy[] = { + { "recursive", NO_TRIVIAL }, + { "octopus", DEFAULT_OCTOPUS }, + { "ort", DEFAULT_TWOHEAD | NO_TRIVIAL }, + { "resolve", 0 }, + { "ours", NO_FAST_FORWARD | NO_TRIVIAL }, + { "subtree", NO_FAST_FORWARD | NO_TRIVIAL }, +}; + +static const char *pull_twohead, *pull_octopus; + +enum ff_type { + FF_NO, + FF_ALLOW, + FF_ONLY +}; + +static enum ff_type fast_forward = FF_ALLOW; + +static const char *cleanup_arg; +static enum commit_msg_cleanup_mode cleanup_mode; + +static int option_parse_message(const struct option *opt, + const char *arg, int unset) +{ + struct strbuf *buf = opt->value; + + if (unset) + strbuf_setlen(buf, 0); + else if (arg) { + strbuf_addf(buf, "%s%s", buf->len ? "\n\n" : "", arg); + have_message = 1; + } else + return error(_("switch `m' requires a value")); + return 0; +} + +static enum parse_opt_result option_read_message(struct parse_opt_ctx_t *ctx, + const struct option *opt, + const char *arg_not_used, + int unset) +{ + struct strbuf *buf = opt->value; + const char *arg; + + BUG_ON_OPT_ARG(arg_not_used); + if (unset) + BUG("-F cannot be negated"); + + if (ctx->opt) { + arg = ctx->opt; + ctx->opt = NULL; + } else if (ctx->argc > 1) { + ctx->argc--; + arg = *++ctx->argv; + } else + return error(_("option `%s' requires a value"), opt->long_name); + + if (buf->len) + strbuf_addch(buf, '\n'); + if (ctx->prefix && !is_absolute_path(arg)) + arg = prefix_filename(ctx->prefix, arg); + if (strbuf_read_file(buf, arg, 0) < 0) + return error(_("could not read file '%s'"), arg); + have_message = 1; + + return 0; +} + +static struct strategy *get_strategy(const char *name) +{ + int i; + struct strategy *ret; + static struct cmdnames main_cmds, other_cmds; + static int loaded; + char *default_strategy = getenv("GIT_TEST_MERGE_ALGORITHM"); + + if (!name) + return NULL; + + if (default_strategy && + !strcmp(default_strategy, "ort") && + !strcmp(name, "recursive")) { + name = "ort"; + } + + for (i = 0; i < ARRAY_SIZE(all_strategy); i++) + if (!strcmp(name, all_strategy[i].name)) + return &all_strategy[i]; + + if (!loaded) { + struct cmdnames not_strategies; + loaded = 1; + + memset(¬_strategies, 0, sizeof(struct cmdnames)); + load_command_list("git-merge-", &main_cmds, &other_cmds); + for (i = 0; i < main_cmds.cnt; i++) { + int j, found = 0; + struct cmdname *ent = main_cmds.names[i]; + for (j = 0; !found && j < ARRAY_SIZE(all_strategy); j++) + if (!strncmp(ent->name, all_strategy[j].name, ent->len) + && !all_strategy[j].name[ent->len]) + found = 1; + if (!found) + add_cmdname(¬_strategies, ent->name, ent->len); + } + exclude_cmds(&main_cmds, ¬_strategies); + } + if (!is_in_cmdlist(&main_cmds, name) && !is_in_cmdlist(&other_cmds, name)) { + fprintf(stderr, _("Could not find merge strategy '%s'.\n"), name); + fprintf(stderr, _("Available strategies are:")); + for (i = 0; i < main_cmds.cnt; i++) + fprintf(stderr, " %s", main_cmds.names[i]->name); + fprintf(stderr, ".\n"); + if (other_cmds.cnt) { + fprintf(stderr, _("Available custom strategies are:")); + for (i = 0; i < other_cmds.cnt; i++) + fprintf(stderr, " %s", other_cmds.names[i]->name); + fprintf(stderr, ".\n"); + } + exit(1); + } + + CALLOC_ARRAY(ret, 1); + ret->name = xstrdup(name); + ret->attr = NO_TRIVIAL; + return ret; +} + +static void append_strategy(struct strategy *s) +{ + ALLOC_GROW(use_strategies, use_strategies_nr + 1, use_strategies_alloc); + use_strategies[use_strategies_nr++] = s; +} + +static int option_parse_strategy(const struct option *opt UNUSED, + const char *name, int unset) +{ + if (unset) + return 0; + + append_strategy(get_strategy(name)); + return 0; +} + +static struct option builtin_merge_options[] = { + OPT_SET_INT('n', NULL, &show_diffstat, + N_("do not show a diffstat at the end of the merge"), 0), + OPT_BOOL(0, "stat", &show_diffstat, + N_("show a diffstat at the end of the merge")), + OPT_BOOL(0, "summary", &show_diffstat, N_("(synonym to --stat)")), + { OPTION_INTEGER, 0, "log", &shortlog_len, N_("n"), + N_("add (at most <n>) entries from shortlog to merge commit message"), + PARSE_OPT_OPTARG, NULL, DEFAULT_MERGE_LOG_LEN }, + OPT_BOOL(0, "squash", &squash, + N_("create a single commit instead of doing a merge")), + OPT_BOOL(0, "commit", &option_commit, + N_("perform a commit if the merge succeeds (default)")), + OPT_BOOL('e', "edit", &option_edit, + N_("edit message before committing")), + OPT_CLEANUP(&cleanup_arg), + OPT_SET_INT(0, "ff", &fast_forward, N_("allow fast-forward (default)"), FF_ALLOW), + OPT_SET_INT_F(0, "ff-only", &fast_forward, + N_("abort if fast-forward is not possible"), + FF_ONLY, PARSE_OPT_NONEG), + OPT_RERERE_AUTOUPDATE(&allow_rerere_auto), + OPT_BOOL(0, "verify-signatures", &verify_signatures, + N_("verify that the named commit has a valid GPG signature")), + OPT_CALLBACK('s', "strategy", NULL, N_("strategy"), + N_("merge strategy to use"), option_parse_strategy), + OPT_STRVEC('X', "strategy-option", &xopts, N_("option=value"), + N_("option for selected merge strategy")), + OPT_CALLBACK('m', "message", &merge_msg, N_("message"), + N_("merge commit message (for a non-fast-forward merge)"), + option_parse_message), + { OPTION_LOWLEVEL_CALLBACK, 'F', "file", &merge_msg, N_("path"), + N_("read message from file"), PARSE_OPT_NONEG, + NULL, 0, option_read_message }, + OPT_STRING(0, "into-name", &into_name, N_("name"), + N_("use <name> instead of the real target")), + OPT__VERBOSITY(&verbosity), + OPT_BOOL(0, "abort", &abort_current_merge, + N_("abort the current in-progress merge")), + OPT_BOOL(0, "quit", &quit_current_merge, + N_("--abort but leave index and working tree alone")), + OPT_BOOL(0, "continue", &continue_current_merge, + N_("continue the current in-progress merge")), + OPT_BOOL(0, "allow-unrelated-histories", &allow_unrelated_histories, + N_("allow merging unrelated histories")), + OPT_SET_INT(0, "progress", &show_progress, N_("force progress reporting"), 1), + { OPTION_STRING, 'S', "gpg-sign", &sign_commit, N_("key-id"), + N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + OPT_AUTOSTASH(&autostash), + OPT_BOOL(0, "overwrite-ignore", &overwrite_ignore, N_("update ignored files (default)")), + OPT_BOOL(0, "signoff", &signoff, N_("add a Signed-off-by trailer")), + OPT_BOOL(0, "no-verify", &no_verify, N_("bypass pre-merge-commit and commit-msg hooks")), + OPT_END() +}; + +static int save_state(struct object_id *stash) +{ + int len; + struct child_process cp = CHILD_PROCESS_INIT; + struct strbuf buffer = STRBUF_INIT; + struct lock_file lock_file = LOCK_INIT; + int fd; + int rc = -1; + + fd = repo_hold_locked_index(the_repository, &lock_file, 0); + refresh_index(&the_index, REFRESH_QUIET, NULL, NULL, NULL); + if (0 <= fd) + repo_update_index_if_able(the_repository, &lock_file); + rollback_lock_file(&lock_file); + + strvec_pushl(&cp.args, "stash", "create", NULL); + cp.out = -1; + cp.git_cmd = 1; + + if (start_command(&cp)) + die(_("could not run stash.")); + len = strbuf_read(&buffer, cp.out, 1024); + close(cp.out); + + if (finish_command(&cp) || len < 0) + die(_("stash failed")); + else if (!len) /* no changes */ + goto out; + strbuf_setlen(&buffer, buffer.len-1); + if (repo_get_oid(the_repository, buffer.buf, stash)) + die(_("not a valid object: %s"), buffer.buf); + rc = 0; +out: + strbuf_release(&buffer); + return rc; +} + +static void read_empty(const struct object_id *oid) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + + strvec_pushl(&cmd.args, "read-tree", "-m", "-u", empty_tree_oid_hex(), + oid_to_hex(oid), NULL); + cmd.git_cmd = 1; + + if (run_command(&cmd)) + die(_("read-tree failed")); +} + +static void reset_hard(const struct object_id *oid) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + + strvec_pushl(&cmd.args, "read-tree", "-v", "--reset", "-u", + oid_to_hex(oid), NULL); + cmd.git_cmd = 1; + + if (run_command(&cmd)) + die(_("read-tree failed")); +} + +static void restore_state(const struct object_id *head, + const struct object_id *stash) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + + reset_hard(head); + + if (is_null_oid(stash)) + goto refresh_cache; + + strvec_pushl(&cmd.args, "stash", "apply", "--index", "--quiet", NULL); + strvec_push(&cmd.args, oid_to_hex(stash)); + + /* + * It is OK to ignore error here, for example when there was + * nothing to restore. + */ + cmd.git_cmd = 1; + run_command(&cmd); + +refresh_cache: + discard_index(&the_index); + if (repo_read_index(the_repository) < 0) + die(_("could not read index")); +} + +/* This is called when no merge was necessary. */ +static void finish_up_to_date(void) +{ + if (verbosity >= 0) { + if (squash) + puts(_("Already up to date. (nothing to squash)")); + else + puts(_("Already up to date.")); + } + remove_merge_branch_state(the_repository); +} + +static void squash_message(struct commit *commit, struct commit_list *remoteheads) +{ + struct rev_info rev; + struct strbuf out = STRBUF_INIT; + struct commit_list *j; + struct pretty_print_context ctx = {0}; + + printf(_("Squash commit -- not updating HEAD\n")); + + repo_init_revisions(the_repository, &rev, NULL); + diff_merges_suppress(&rev); + rev.commit_format = CMIT_FMT_MEDIUM; + + commit->object.flags |= UNINTERESTING; + add_pending_object(&rev, &commit->object, NULL); + + for (j = remoteheads; j; j = j->next) + add_pending_object(&rev, &j->item->object, NULL); + + setup_revisions(0, NULL, &rev, NULL); + if (prepare_revision_walk(&rev)) + die(_("revision walk setup failed")); + + ctx.abbrev = rev.abbrev; + ctx.date_mode = rev.date_mode; + ctx.fmt = rev.commit_format; + + strbuf_addstr(&out, "Squashed commit of the following:\n"); + while ((commit = get_revision(&rev)) != NULL) { + strbuf_addch(&out, '\n'); + strbuf_addf(&out, "commit %s\n", + oid_to_hex(&commit->object.oid)); + pretty_print_commit(&ctx, commit, &out); + } + write_file_buf(git_path_squash_msg(the_repository), out.buf, out.len); + strbuf_release(&out); + release_revisions(&rev); +} + +static void finish(struct commit *head_commit, + struct commit_list *remoteheads, + const struct object_id *new_head, const char *msg) +{ + struct strbuf reflog_message = STRBUF_INIT; + const struct object_id *head = &head_commit->object.oid; + + if (!msg) + strbuf_addstr(&reflog_message, getenv("GIT_REFLOG_ACTION")); + else { + if (verbosity >= 0) + printf("%s\n", msg); + strbuf_addf(&reflog_message, "%s: %s", + getenv("GIT_REFLOG_ACTION"), msg); + } + if (squash) { + squash_message(head_commit, remoteheads); + } else { + if (verbosity >= 0 && !merge_msg.len) + printf(_("No merge message -- not updating HEAD\n")); + else { + update_ref(reflog_message.buf, "HEAD", new_head, head, + 0, UPDATE_REFS_DIE_ON_ERR); + /* + * We ignore errors in 'gc --auto', since the + * user should see them. + */ + run_auto_maintenance(verbosity < 0); + } + } + if (new_head && show_diffstat) { + struct diff_options opts; + repo_diff_setup(the_repository, &opts); + init_diffstat_widths(&opts); + opts.output_format |= + DIFF_FORMAT_SUMMARY | DIFF_FORMAT_DIFFSTAT; + opts.detect_rename = DIFF_DETECT_RENAME; + diff_setup_done(&opts); + diff_tree_oid(head, new_head, "", &opts); + diffcore_std(&opts); + diff_flush(&opts); + } + + /* Run a post-merge hook */ + run_hooks_l("post-merge", squash ? "1" : "0", NULL); + + if (new_head) + apply_autostash(git_path_merge_autostash(the_repository)); + strbuf_release(&reflog_message); +} + +/* Get the name for the merge commit's message. */ +static void merge_name(const char *remote, struct strbuf *msg) +{ + struct commit *remote_head; + struct object_id branch_head; + struct strbuf bname = STRBUF_INIT; + struct merge_remote_desc *desc; + const char *ptr; + char *found_ref = NULL; + int len, early; + + strbuf_branchname(&bname, remote, 0); + remote = bname.buf; + + oidclr(&branch_head); + remote_head = get_merge_parent(remote); + if (!remote_head) + die(_("'%s' does not point to a commit"), remote); + + if (repo_dwim_ref(the_repository, remote, strlen(remote), &branch_head, + &found_ref, 0) > 0) { + if (starts_with(found_ref, "refs/heads/")) { + strbuf_addf(msg, "%s\t\tbranch '%s' of .\n", + oid_to_hex(&branch_head), remote); + goto cleanup; + } + if (starts_with(found_ref, "refs/tags/")) { + strbuf_addf(msg, "%s\t\ttag '%s' of .\n", + oid_to_hex(&branch_head), remote); + goto cleanup; + } + if (starts_with(found_ref, "refs/remotes/")) { + strbuf_addf(msg, "%s\t\tremote-tracking branch '%s' of .\n", + oid_to_hex(&branch_head), remote); + goto cleanup; + } + } + + /* See if remote matches <name>^^^.. or <name>~<number> */ + for (len = 0, ptr = remote + strlen(remote); + remote < ptr && ptr[-1] == '^'; + ptr--) + len++; + if (len) + early = 1; + else { + early = 0; + ptr = strrchr(remote, '~'); + if (ptr) { + int seen_nonzero = 0; + + len++; /* count ~ */ + while (*++ptr && isdigit(*ptr)) { + seen_nonzero |= (*ptr != '0'); + len++; + } + if (*ptr) + len = 0; /* not ...~<number> */ + else if (seen_nonzero) + early = 1; + else if (len == 1) + early = 1; /* "name~" is "name~1"! */ + } + } + if (len) { + struct strbuf truname = STRBUF_INIT; + strbuf_addf(&truname, "refs/heads/%s", remote); + strbuf_setlen(&truname, truname.len - len); + if (ref_exists(truname.buf)) { + strbuf_addf(msg, + "%s\t\tbranch '%s'%s of .\n", + oid_to_hex(&remote_head->object.oid), + truname.buf + 11, + (early ? " (early part)" : "")); + strbuf_release(&truname); + goto cleanup; + } + strbuf_release(&truname); + } + + desc = merge_remote_util(remote_head); + if (desc && desc->obj && desc->obj->type == OBJ_TAG) { + strbuf_addf(msg, "%s\t\t%s '%s'\n", + oid_to_hex(&desc->obj->oid), + type_name(desc->obj->type), + remote); + goto cleanup; + } + + strbuf_addf(msg, "%s\t\tcommit '%s'\n", + oid_to_hex(&remote_head->object.oid), remote); +cleanup: + free(found_ref); + strbuf_release(&bname); +} + +static void parse_branch_merge_options(char *bmo) +{ + const char **argv; + int argc; + + if (!bmo) + return; + argc = split_cmdline(bmo, &argv); + if (argc < 0) + die(_("Bad branch.%s.mergeoptions string: %s"), branch, + _(split_cmdline_strerror(argc))); + REALLOC_ARRAY(argv, argc + 2); + MOVE_ARRAY(argv + 1, argv, argc + 1); + argc++; + argv[0] = "branch.*.mergeoptions"; + parse_options(argc, argv, NULL, builtin_merge_options, + builtin_merge_usage, 0); + free(argv); +} + +static int git_merge_config(const char *k, const char *v, + const struct config_context *ctx, void *cb) +{ + int status; + const char *str; + + if (branch && + skip_prefix(k, "branch.", &str) && + skip_prefix(str, branch, &str) && + !strcmp(str, ".mergeoptions")) { + free(branch_mergeoptions); + branch_mergeoptions = xstrdup(v); + return 0; + } + + if (!strcmp(k, "merge.diffstat") || !strcmp(k, "merge.stat")) + show_diffstat = git_config_bool(k, v); + else if (!strcmp(k, "merge.verifysignatures")) + verify_signatures = git_config_bool(k, v); + else if (!strcmp(k, "pull.twohead")) + return git_config_string(&pull_twohead, k, v); + else if (!strcmp(k, "pull.octopus")) + return git_config_string(&pull_octopus, k, v); + else if (!strcmp(k, "commit.cleanup")) + return git_config_string(&cleanup_arg, k, v); + else if (!strcmp(k, "merge.ff")) { + int boolval = git_parse_maybe_bool(v); + if (0 <= boolval) { + fast_forward = boolval ? FF_ALLOW : FF_NO; + } else if (v && !strcmp(v, "only")) { + fast_forward = FF_ONLY; + } /* do not barf on values from future versions of git */ + return 0; + } else if (!strcmp(k, "merge.defaulttoupstream")) { + default_to_upstream = git_config_bool(k, v); + return 0; + } else if (!strcmp(k, "commit.gpgsign")) { + sign_commit = git_config_bool(k, v) ? "" : NULL; + return 0; + } else if (!strcmp(k, "gpg.mintrustlevel")) { + check_trust_level = 0; + } else if (!strcmp(k, "merge.autostash")) { + autostash = git_config_bool(k, v); + return 0; + } + + status = fmt_merge_msg_config(k, v, ctx, cb); + if (status) + return status; + return git_diff_ui_config(k, v, ctx, cb); +} + +static int read_tree_trivial(struct object_id *common, struct object_id *head, + struct object_id *one) +{ + int i, nr_trees = 0; + struct tree *trees[MAX_UNPACK_TREES]; + struct tree_desc t[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = 2; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.update = 1; + opts.verbose_update = 1; + opts.trivial_merges_only = 1; + opts.merge = 1; + opts.preserve_ignored = 0; /* FIXME: !overwrite_ignore */ + trees[nr_trees] = parse_tree_indirect(common); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(head); + if (!trees[nr_trees++]) + return -1; + trees[nr_trees] = parse_tree_indirect(one); + if (!trees[nr_trees++]) + return -1; + opts.fn = threeway_merge; + cache_tree_free(&the_index.cache_tree); + for (i = 0; i < nr_trees; i++) { + parse_tree(trees[i]); + init_tree_desc(t+i, trees[i]->buffer, trees[i]->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return -1; + return 0; +} + +static void write_tree_trivial(struct object_id *oid) +{ + if (write_index_as_tree(oid, &the_index, get_index_file(), 0, NULL)) + die(_("git write-tree failed to write a tree")); +} + +static int try_merge_strategy(const char *strategy, struct commit_list *common, + struct commit_list *remoteheads, + struct commit *head) +{ + const char *head_arg = "HEAD"; + + if (repo_refresh_and_write_index(the_repository, REFRESH_QUIET, + SKIP_IF_UNCHANGED, 0, NULL, NULL, + NULL) < 0) + return error(_("Unable to write index.")); + + if (!strcmp(strategy, "recursive") || !strcmp(strategy, "subtree") || + !strcmp(strategy, "ort")) { + struct lock_file lock = LOCK_INIT; + int clean, x; + struct commit *result; + struct commit_list *reversed = NULL; + struct merge_options o; + struct commit_list *j; + + if (remoteheads->next) { + error(_("Not handling anything other than two heads merge.")); + return 2; + } + + init_merge_options(&o, the_repository); + if (!strcmp(strategy, "subtree")) + o.subtree_shift = ""; + + o.show_rename_progress = + show_progress == -1 ? isatty(2) : show_progress; + + for (x = 0; x < xopts.nr; x++) + if (parse_merge_opt(&o, xopts.v[x])) + die(_("unknown strategy option: -X%s"), xopts.v[x]); + + o.branch1 = head_arg; + o.branch2 = merge_remote_util(remoteheads->item)->name; + + for (j = common; j; j = j->next) + commit_list_insert(j->item, &reversed); + + repo_hold_locked_index(the_repository, &lock, + LOCK_DIE_ON_ERROR); + if (!strcmp(strategy, "ort")) + clean = merge_ort_recursive(&o, head, remoteheads->item, + reversed, &result); + else + clean = merge_recursive(&o, head, remoteheads->item, + reversed, &result); + if (clean < 0) { + rollback_lock_file(&lock); + return 2; + } + if (write_locked_index(&the_index, &lock, + COMMIT_LOCK | SKIP_IF_UNCHANGED)) + die(_("unable to write %s"), get_index_file()); + return clean ? 0 : 1; + } else { + return try_merge_command(the_repository, + strategy, xopts.nr, xopts.v, + common, head_arg, remoteheads); + } +} + +static void count_diff_files(struct diff_queue_struct *q, + struct diff_options *opt UNUSED, void *data) +{ + int *count = data; + + (*count) += q->nr; +} + +static int count_unmerged_entries(void) +{ + int i, ret = 0; + + for (i = 0; i < the_index.cache_nr; i++) + if (ce_stage(the_index.cache[i])) + ret++; + + return ret; +} + +static void add_strategies(const char *string, unsigned attr) +{ + int i; + + if (string) { + struct string_list list = STRING_LIST_INIT_DUP; + struct string_list_item *item; + string_list_split(&list, string, ' ', -1); + for_each_string_list_item(item, &list) + append_strategy(get_strategy(item->string)); + string_list_clear(&list, 0); + return; + } + for (i = 0; i < ARRAY_SIZE(all_strategy); i++) + if (all_strategy[i].attr & attr) + append_strategy(&all_strategy[i]); + +} + +static void read_merge_msg(struct strbuf *msg) +{ + const char *filename = git_path_merge_msg(the_repository); + strbuf_reset(msg); + if (strbuf_read_file(msg, filename, 0) < 0) + die_errno(_("Could not read from '%s'"), filename); +} + +static void write_merge_state(struct commit_list *); +static void abort_commit(struct commit_list *remoteheads, const char *err_msg) +{ + if (err_msg) + error("%s", err_msg); + fprintf(stderr, + _("Not committing merge; use 'git commit' to complete the merge.\n")); + write_merge_state(remoteheads); + exit(1); +} + +static const char merge_editor_comment[] = +N_("Please enter a commit message to explain why this merge is necessary,\n" + "especially if it merges an updated upstream into a topic branch.\n" + "\n"); + +static const char scissors_editor_comment[] = +N_("An empty message aborts the commit.\n"); + +static const char no_scissors_editor_comment[] = +N_("Lines starting with '%c' will be ignored, and an empty message aborts\n" + "the commit.\n"); + +static void write_merge_heads(struct commit_list *); +static void prepare_to_commit(struct commit_list *remoteheads) +{ + struct strbuf msg = STRBUF_INIT; + const char *index_file = get_index_file(); + + if (!no_verify) { + int invoked_hook; + + if (run_commit_hook(0 < option_edit, index_file, &invoked_hook, + "pre-merge-commit", NULL)) + abort_commit(remoteheads, NULL); + /* + * Re-read the index as pre-merge-commit hook could have updated it, + * and write it out as a tree. We must do this before we invoke + * the editor and after we invoke run_status above. + */ + if (invoked_hook) + discard_index(&the_index); + } + read_index_from(&the_index, index_file, get_git_dir()); + strbuf_addbuf(&msg, &merge_msg); + if (squash) + BUG("the control must not reach here under --squash"); + if (0 < option_edit) { + strbuf_addch(&msg, '\n'); + if (cleanup_mode == COMMIT_MSG_CLEANUP_SCISSORS) { + wt_status_append_cut_line(&msg); + strbuf_commented_addf(&msg, comment_line_char, "\n"); + } + strbuf_commented_addf(&msg, comment_line_char, + _(merge_editor_comment)); + if (cleanup_mode == COMMIT_MSG_CLEANUP_SCISSORS) + strbuf_commented_addf(&msg, comment_line_char, + _(scissors_editor_comment)); + else + strbuf_commented_addf(&msg, comment_line_char, + _(no_scissors_editor_comment), comment_line_char); + } + if (signoff) + append_signoff(&msg, ignore_non_trailer(msg.buf, msg.len), 0); + write_merge_heads(remoteheads); + write_file_buf(git_path_merge_msg(the_repository), msg.buf, msg.len); + if (run_commit_hook(0 < option_edit, get_index_file(), NULL, + "prepare-commit-msg", + git_path_merge_msg(the_repository), "merge", NULL)) + abort_commit(remoteheads, NULL); + if (0 < option_edit) { + if (launch_editor(git_path_merge_msg(the_repository), NULL, NULL)) + abort_commit(remoteheads, NULL); + } + + if (!no_verify && run_commit_hook(0 < option_edit, get_index_file(), + NULL, "commit-msg", + git_path_merge_msg(the_repository), NULL)) + abort_commit(remoteheads, NULL); + + read_merge_msg(&msg); + cleanup_message(&msg, cleanup_mode, 0); + if (!msg.len) + abort_commit(remoteheads, _("Empty commit message.")); + strbuf_release(&merge_msg); + strbuf_addbuf(&merge_msg, &msg); + strbuf_release(&msg); +} + +static int merge_trivial(struct commit *head, struct commit_list *remoteheads) +{ + struct object_id result_tree, result_commit; + struct commit_list *parents, **pptr = &parents; + + if (repo_refresh_and_write_index(the_repository, REFRESH_QUIET, + SKIP_IF_UNCHANGED, 0, NULL, NULL, + NULL) < 0) + return error(_("Unable to write index.")); + + write_tree_trivial(&result_tree); + printf(_("Wonderful.\n")); + pptr = commit_list_append(head, pptr); + pptr = commit_list_append(remoteheads->item, pptr); + prepare_to_commit(remoteheads); + if (commit_tree(merge_msg.buf, merge_msg.len, &result_tree, parents, + &result_commit, NULL, sign_commit)) + die(_("failed to write commit object")); + finish(head, remoteheads, &result_commit, "In-index merge"); + remove_merge_branch_state(the_repository); + return 0; +} + +static int finish_automerge(struct commit *head, + int head_subsumed, + struct commit_list *common, + struct commit_list *remoteheads, + struct object_id *result_tree, + const char *wt_strategy) +{ + struct commit_list *parents = NULL; + struct strbuf buf = STRBUF_INIT; + struct object_id result_commit; + + write_tree_trivial(result_tree); + free_commit_list(common); + parents = remoteheads; + if (!head_subsumed || fast_forward == FF_NO) + commit_list_insert(head, &parents); + prepare_to_commit(remoteheads); + if (commit_tree(merge_msg.buf, merge_msg.len, result_tree, parents, + &result_commit, NULL, sign_commit)) + die(_("failed to write commit object")); + strbuf_addf(&buf, "Merge made by the '%s' strategy.", wt_strategy); + finish(head, remoteheads, &result_commit, buf.buf); + strbuf_release(&buf); + remove_merge_branch_state(the_repository); + return 0; +} + +static int suggest_conflicts(void) +{ + const char *filename; + FILE *fp; + struct strbuf msgbuf = STRBUF_INIT; + + filename = git_path_merge_msg(the_repository); + fp = xfopen(filename, "a"); + + /* + * We can't use cleanup_mode because if we're not using the editor, + * get_cleanup_mode will return COMMIT_MSG_CLEANUP_SPACE instead, even + * though the message is meant to be processed later by git-commit. + * Thus, we will get the cleanup mode which is returned when we _are_ + * using an editor. + */ + append_conflicts_hint(&the_index, &msgbuf, + get_cleanup_mode(cleanup_arg, 1)); + fputs(msgbuf.buf, fp); + strbuf_release(&msgbuf); + fclose(fp); + repo_rerere(the_repository, allow_rerere_auto); + printf(_("Automatic merge failed; " + "fix conflicts and then commit the result.\n")); + return 1; +} + +static int evaluate_result(void) +{ + int cnt = 0; + struct rev_info rev; + + /* Check how many files differ. */ + repo_init_revisions(the_repository, &rev, ""); + setup_revisions(0, NULL, &rev, NULL); + rev.diffopt.output_format |= + DIFF_FORMAT_CALLBACK; + rev.diffopt.format_callback = count_diff_files; + rev.diffopt.format_callback_data = &cnt; + run_diff_files(&rev, 0); + + /* + * Check how many unmerged entries are + * there. + */ + cnt += count_unmerged_entries(); + + release_revisions(&rev); + return cnt; +} + +/* + * Pretend as if the user told us to merge with the remote-tracking + * branch we have for the upstream of the current branch + */ +static int setup_with_upstream(const char ***argv) +{ + struct branch *branch = branch_get(NULL); + int i; + const char **args; + + if (!branch) + die(_("No current branch.")); + if (!branch->remote_name) + die(_("No remote for the current branch.")); + if (!branch->merge_nr) + die(_("No default upstream defined for the current branch.")); + + args = xcalloc(st_add(branch->merge_nr, 1), sizeof(char *)); + for (i = 0; i < branch->merge_nr; i++) { + if (!branch->merge[i]->dst) + die(_("No remote-tracking branch for %s from %s"), + branch->merge[i]->src, branch->remote_name); + args[i] = branch->merge[i]->dst; + } + args[i] = NULL; + *argv = args; + return i; +} + +static void write_merge_heads(struct commit_list *remoteheads) +{ + struct commit_list *j; + struct strbuf buf = STRBUF_INIT; + + for (j = remoteheads; j; j = j->next) { + struct object_id *oid; + struct commit *c = j->item; + struct merge_remote_desc *desc; + + desc = merge_remote_util(c); + if (desc && desc->obj) { + oid = &desc->obj->oid; + } else { + oid = &c->object.oid; + } + strbuf_addf(&buf, "%s\n", oid_to_hex(oid)); + } + write_file_buf(git_path_merge_head(the_repository), buf.buf, buf.len); + + strbuf_reset(&buf); + if (fast_forward == FF_NO) + strbuf_addstr(&buf, "no-ff"); + write_file_buf(git_path_merge_mode(the_repository), buf.buf, buf.len); + strbuf_release(&buf); +} + +static void write_merge_state(struct commit_list *remoteheads) +{ + write_merge_heads(remoteheads); + strbuf_addch(&merge_msg, '\n'); + write_file_buf(git_path_merge_msg(the_repository), merge_msg.buf, + merge_msg.len); +} + +static int default_edit_option(void) +{ + static const char name[] = "GIT_MERGE_AUTOEDIT"; + const char *e = getenv(name); + struct stat st_stdin, st_stdout; + + if (have_message) + /* an explicit -m msg without --[no-]edit */ + return 0; + + if (e) { + int v = git_parse_maybe_bool(e); + if (v < 0) + die(_("Bad value '%s' in environment '%s'"), e, name); + return v; + } + + /* Use editor if stdin and stdout are the same and is a tty */ + return (!fstat(0, &st_stdin) && + !fstat(1, &st_stdout) && + isatty(0) && isatty(1) && + st_stdin.st_dev == st_stdout.st_dev && + st_stdin.st_ino == st_stdout.st_ino && + st_stdin.st_mode == st_stdout.st_mode); +} + +static struct commit_list *reduce_parents(struct commit *head_commit, + int *head_subsumed, + struct commit_list *remoteheads) +{ + struct commit_list *parents, **remotes; + + /* + * Is the current HEAD reachable from another commit being + * merged? If so we do not want to record it as a parent of + * the resulting merge, unless --no-ff is given. We will flip + * this variable to 0 when we find HEAD among the independent + * tips being merged. + */ + *head_subsumed = 1; + + /* Find what parents to record by checking independent ones. */ + parents = reduce_heads(remoteheads); + free_commit_list(remoteheads); + + remoteheads = NULL; + remotes = &remoteheads; + while (parents) { + struct commit *commit = pop_commit(&parents); + if (commit == head_commit) + *head_subsumed = 0; + else + remotes = &commit_list_insert(commit, remotes)->next; + } + return remoteheads; +} + +static void prepare_merge_message(struct strbuf *merge_names, struct strbuf *merge_msg) +{ + struct fmt_merge_msg_opts opts; + + memset(&opts, 0, sizeof(opts)); + opts.add_title = !have_message; + opts.shortlog_len = shortlog_len; + opts.credit_people = (0 < option_edit); + opts.into_name = into_name; + + fmt_merge_msg(merge_names, merge_msg, &opts); + if (merge_msg->len) + strbuf_setlen(merge_msg, merge_msg->len - 1); +} + +static void handle_fetch_head(struct commit_list **remotes, struct strbuf *merge_names) +{ + const char *filename; + int fd, pos, npos; + struct strbuf fetch_head_file = STRBUF_INIT; + const unsigned hexsz = the_hash_algo->hexsz; + + if (!merge_names) + merge_names = &fetch_head_file; + + filename = git_path_fetch_head(the_repository); + fd = xopen(filename, O_RDONLY); + + if (strbuf_read(merge_names, fd, 0) < 0) + die_errno(_("could not read '%s'"), filename); + if (close(fd) < 0) + die_errno(_("could not close '%s'"), filename); + + for (pos = 0; pos < merge_names->len; pos = npos) { + struct object_id oid; + char *ptr; + struct commit *commit; + + ptr = strchr(merge_names->buf + pos, '\n'); + if (ptr) + npos = ptr - merge_names->buf + 1; + else + npos = merge_names->len; + + if (npos - pos < hexsz + 2 || + get_oid_hex(merge_names->buf + pos, &oid)) + commit = NULL; /* bad */ + else if (memcmp(merge_names->buf + pos + hexsz, "\t\t", 2)) + continue; /* not-for-merge */ + else { + char saved = merge_names->buf[pos + hexsz]; + merge_names->buf[pos + hexsz] = '\0'; + commit = get_merge_parent(merge_names->buf + pos); + merge_names->buf[pos + hexsz] = saved; + } + if (!commit) { + if (ptr) + *ptr = '\0'; + die(_("not something we can merge in %s: %s"), + filename, merge_names->buf + pos); + } + remotes = &commit_list_insert(commit, remotes)->next; + } + + if (merge_names == &fetch_head_file) + strbuf_release(&fetch_head_file); +} + +static struct commit_list *collect_parents(struct commit *head_commit, + int *head_subsumed, + int argc, const char **argv, + struct strbuf *merge_msg) +{ + int i; + struct commit_list *remoteheads = NULL; + struct commit_list **remotes = &remoteheads; + struct strbuf merge_names = STRBUF_INIT, *autogen = NULL; + + if (merge_msg && (!have_message || shortlog_len)) + autogen = &merge_names; + + if (head_commit) + remotes = &commit_list_insert(head_commit, remotes)->next; + + if (argc == 1 && !strcmp(argv[0], "FETCH_HEAD")) { + handle_fetch_head(remotes, autogen); + remoteheads = reduce_parents(head_commit, head_subsumed, remoteheads); + } else { + for (i = 0; i < argc; i++) { + struct commit *commit = get_merge_parent(argv[i]); + if (!commit) + help_unknown_ref(argv[i], "merge", + _("not something we can merge")); + remotes = &commit_list_insert(commit, remotes)->next; + } + remoteheads = reduce_parents(head_commit, head_subsumed, remoteheads); + if (autogen) { + struct commit_list *p; + for (p = remoteheads; p; p = p->next) + merge_name(merge_remote_util(p->item)->name, autogen); + } + } + + if (autogen) { + prepare_merge_message(autogen, merge_msg); + strbuf_release(autogen); + } + + return remoteheads; +} + +static int merging_a_throwaway_tag(struct commit *commit) +{ + char *tag_ref; + struct object_id oid; + int is_throwaway_tag = 0; + + /* Are we merging a tag? */ + if (!merge_remote_util(commit) || + !merge_remote_util(commit)->obj || + merge_remote_util(commit)->obj->type != OBJ_TAG) + return is_throwaway_tag; + + /* + * Now we know we are merging a tag object. Are we downstream + * and following the tags from upstream? If so, we must have + * the tag object pointed at by "refs/tags/$T" where $T is the + * tagname recorded in the tag object. We want to allow such + * a "just to catch up" merge to fast-forward. + * + * Otherwise, we are playing an integrator's role, making a + * merge with a throw-away tag from a contributor with + * something like "git pull $contributor $signed_tag". + * We want to forbid such a merge from fast-forwarding + * by default; otherwise we would not keep the signature + * anywhere. + */ + tag_ref = xstrfmt("refs/tags/%s", + ((struct tag *)merge_remote_util(commit)->obj)->tag); + if (!read_ref(tag_ref, &oid) && + oideq(&oid, &merge_remote_util(commit)->obj->oid)) + is_throwaway_tag = 0; + else + is_throwaway_tag = 1; + free(tag_ref); + return is_throwaway_tag; +} + +int cmd_merge(int argc, const char **argv, const char *prefix) +{ + struct object_id result_tree, stash, head_oid; + struct commit *head_commit; + struct strbuf buf = STRBUF_INIT; + int i, ret = 0, head_subsumed; + int best_cnt = -1, merge_was_ok = 0, automerge_was_ok = 0; + struct commit_list *common = NULL; + const char *best_strategy = NULL, *wt_strategy = NULL; + struct commit_list *remoteheads = NULL, *p; + void *branch_to_free; + int orig_argc = argc; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(builtin_merge_usage, builtin_merge_options); + + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + /* + * Check if we are _not_ on a detached HEAD, i.e. if there is a + * current branch. + */ + branch = branch_to_free = resolve_refdup("HEAD", 0, &head_oid, NULL); + if (branch) + skip_prefix(branch, "refs/heads/", &branch); + + if (!pull_twohead) { + char *default_strategy = getenv("GIT_TEST_MERGE_ALGORITHM"); + if (default_strategy && !strcmp(default_strategy, "ort")) + pull_twohead = "ort"; + } + + init_diff_ui_defaults(); + git_config(git_merge_config, NULL); + + if (!branch || is_null_oid(&head_oid)) + head_commit = NULL; + else + head_commit = lookup_commit_or_die(&head_oid, "HEAD"); + + if (branch_mergeoptions) + parse_branch_merge_options(branch_mergeoptions); + argc = parse_options(argc, argv, prefix, builtin_merge_options, + builtin_merge_usage, 0); + if (shortlog_len < 0) + shortlog_len = (merge_log_config > 0) ? merge_log_config : 0; + + if (verbosity < 0 && show_progress == -1) + show_progress = 0; + + if (abort_current_merge) { + int nargc = 2; + const char *nargv[] = {"reset", "--merge", NULL}; + struct strbuf stash_oid = STRBUF_INIT; + + if (orig_argc != 2) + usage_msg_opt(_("--abort expects no arguments"), + builtin_merge_usage, builtin_merge_options); + + if (!file_exists(git_path_merge_head(the_repository))) + die(_("There is no merge to abort (MERGE_HEAD missing).")); + + if (read_oneliner(&stash_oid, git_path_merge_autostash(the_repository), + READ_ONELINER_SKIP_IF_EMPTY)) + unlink(git_path_merge_autostash(the_repository)); + + /* Invoke 'git reset --merge' */ + ret = cmd_reset(nargc, nargv, prefix); + + if (stash_oid.len) + apply_autostash_oid(stash_oid.buf); + + strbuf_release(&stash_oid); + goto done; + } + + if (quit_current_merge) { + if (orig_argc != 2) + usage_msg_opt(_("--quit expects no arguments"), + builtin_merge_usage, + builtin_merge_options); + + remove_merge_branch_state(the_repository); + goto done; + } + + if (continue_current_merge) { + int nargc = 1; + const char *nargv[] = {"commit", NULL}; + + if (orig_argc != 2) + usage_msg_opt(_("--continue expects no arguments"), + builtin_merge_usage, builtin_merge_options); + + if (!file_exists(git_path_merge_head(the_repository))) + die(_("There is no merge in progress (MERGE_HEAD missing).")); + + /* Invoke 'git commit' */ + ret = cmd_commit(nargc, nargv, prefix); + goto done; + } + + if (repo_read_index_unmerged(the_repository)) + die_resolve_conflict("merge"); + + if (file_exists(git_path_merge_head(the_repository))) { + /* + * There is no unmerged entry, don't advise 'git + * add/rm <file>', just 'git commit'. + */ + if (advice_enabled(ADVICE_RESOLVE_CONFLICT)) + die(_("You have not concluded your merge (MERGE_HEAD exists).\n" + "Please, commit your changes before you merge.")); + else + die(_("You have not concluded your merge (MERGE_HEAD exists).")); + } + if (ref_exists("CHERRY_PICK_HEAD")) { + if (advice_enabled(ADVICE_RESOLVE_CONFLICT)) + die(_("You have not concluded your cherry-pick (CHERRY_PICK_HEAD exists).\n" + "Please, commit your changes before you merge.")); + else + die(_("You have not concluded your cherry-pick (CHERRY_PICK_HEAD exists).")); + } + resolve_undo_clear_index(&the_index); + + if (option_edit < 0) + option_edit = default_edit_option(); + + cleanup_mode = get_cleanup_mode(cleanup_arg, 0 < option_edit); + + if (verbosity < 0) + show_diffstat = 0; + + if (squash) { + if (fast_forward == FF_NO) + die(_("options '%s' and '%s' cannot be used together"), "--squash", "--no-ff."); + if (option_commit > 0) + die(_("options '%s' and '%s' cannot be used together"), "--squash", "--commit."); + /* + * squash can now silently disable option_commit - this is not + * a problem as it is only overriding the default, not a user + * supplied option. + */ + option_commit = 0; + } + + if (option_commit < 0) + option_commit = 1; + + if (!argc) { + if (default_to_upstream) + argc = setup_with_upstream(&argv); + else + die(_("No commit specified and merge.defaultToUpstream not set.")); + } else if (argc == 1 && !strcmp(argv[0], "-")) { + argv[0] = "@{-1}"; + } + + if (!argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + if (!head_commit) { + /* + * If the merged head is a valid one there is no reason + * to forbid "git merge" into a branch yet to be born. + * We do the same for "git pull". + */ + struct object_id *remote_head_oid; + if (squash) + die(_("Squash commit into empty head not supported yet")); + if (fast_forward == FF_NO) + die(_("Non-fast-forward commit does not make sense into " + "an empty head")); + remoteheads = collect_parents(head_commit, &head_subsumed, + argc, argv, NULL); + if (!remoteheads) + die(_("%s - not something we can merge"), argv[0]); + if (remoteheads->next) + die(_("Can merge only exactly one commit into empty head")); + + if (verify_signatures) + verify_merge_signature(remoteheads->item, verbosity, + check_trust_level); + + remote_head_oid = &remoteheads->item->object.oid; + read_empty(remote_head_oid); + update_ref("initial pull", "HEAD", remote_head_oid, NULL, 0, + UPDATE_REFS_DIE_ON_ERR); + goto done; + } + + /* + * All the rest are the commits being merged; prepare + * the standard merge summary message to be appended + * to the given message. + */ + remoteheads = collect_parents(head_commit, &head_subsumed, + argc, argv, &merge_msg); + + if (!head_commit || !argc) + usage_with_options(builtin_merge_usage, + builtin_merge_options); + + if (verify_signatures) { + for (p = remoteheads; p; p = p->next) { + verify_merge_signature(p->item, verbosity, + check_trust_level); + } + } + + strbuf_addstr(&buf, "merge"); + for (p = remoteheads; p; p = p->next) + strbuf_addf(&buf, " %s", merge_remote_util(p->item)->name); + setenv("GIT_REFLOG_ACTION", buf.buf, 0); + strbuf_reset(&buf); + + for (p = remoteheads; p; p = p->next) { + struct commit *commit = p->item; + strbuf_addf(&buf, "GITHEAD_%s", + oid_to_hex(&commit->object.oid)); + setenv(buf.buf, merge_remote_util(commit)->name, 1); + strbuf_reset(&buf); + if (fast_forward != FF_ONLY && merging_a_throwaway_tag(commit)) + fast_forward = FF_NO; + } + + if (!use_strategies && !pull_twohead && + remoteheads && !remoteheads->next) { + char *default_strategy = getenv("GIT_TEST_MERGE_ALGORITHM"); + if (default_strategy) + append_strategy(get_strategy(default_strategy)); + } + if (!use_strategies) { + if (!remoteheads) + ; /* already up-to-date */ + else if (!remoteheads->next) + add_strategies(pull_twohead, DEFAULT_TWOHEAD); + else + add_strategies(pull_octopus, DEFAULT_OCTOPUS); + } + + for (i = 0; i < use_strategies_nr; i++) { + if (use_strategies[i]->attr & NO_FAST_FORWARD) + fast_forward = FF_NO; + if (use_strategies[i]->attr & NO_TRIVIAL) + allow_trivial = 0; + } + + if (!remoteheads) + ; /* already up-to-date */ + else if (!remoteheads->next) + common = repo_get_merge_bases(the_repository, head_commit, + remoteheads->item); + else { + struct commit_list *list = remoteheads; + commit_list_insert(head_commit, &list); + common = get_octopus_merge_bases(list); + free(list); + } + + update_ref("updating ORIG_HEAD", "ORIG_HEAD", + &head_commit->object.oid, NULL, 0, UPDATE_REFS_DIE_ON_ERR); + + if (remoteheads && !common) { + /* No common ancestors found. */ + if (!allow_unrelated_histories) + die(_("refusing to merge unrelated histories")); + /* otherwise, we need a real merge. */ + } else if (!remoteheads || + (!remoteheads->next && !common->next && + common->item == remoteheads->item)) { + /* + * If head can reach all the merge then we are up to date. + * but first the most common case of merging one remote. + */ + finish_up_to_date(); + goto done; + } else if (fast_forward != FF_NO && !remoteheads->next && + !common->next && + oideq(&common->item->object.oid, &head_commit->object.oid)) { + /* Again the most common case of merging one remote. */ + const char *msg = have_message ? + "Fast-forward (no commit created; -m option ignored)" : + "Fast-forward"; + struct commit *commit; + + if (verbosity >= 0) { + printf(_("Updating %s..%s\n"), + repo_find_unique_abbrev(the_repository, &head_commit->object.oid, + DEFAULT_ABBREV), + repo_find_unique_abbrev(the_repository, &remoteheads->item->object.oid, + DEFAULT_ABBREV)); + } + commit = remoteheads->item; + if (!commit) { + ret = 1; + goto done; + } + + if (autostash) + create_autostash(the_repository, + git_path_merge_autostash(the_repository)); + if (checkout_fast_forward(the_repository, + &head_commit->object.oid, + &commit->object.oid, + overwrite_ignore)) { + apply_autostash(git_path_merge_autostash(the_repository)); + ret = 1; + goto done; + } + + finish(head_commit, remoteheads, &commit->object.oid, msg); + remove_merge_branch_state(the_repository); + goto done; + } else if (!remoteheads->next && common->next) + ; + /* + * We are not doing octopus and not fast-forward. Need + * a real merge. + */ + else if (!remoteheads->next && !common->next && option_commit) { + /* + * We are not doing octopus, not fast-forward, and have + * only one common. + */ + refresh_index(&the_index, REFRESH_QUIET, NULL, NULL, NULL); + if (allow_trivial && fast_forward != FF_ONLY) { + /* + * Must first ensure that index matches HEAD before + * attempting a trivial merge. + */ + struct tree *head_tree = repo_get_commit_tree(the_repository, + head_commit); + struct strbuf sb = STRBUF_INIT; + + if (repo_index_has_changes(the_repository, head_tree, + &sb)) { + error(_("Your local changes to the following files would be overwritten by merge:\n %s"), + sb.buf); + strbuf_release(&sb); + ret = 2; + goto done; + } + + /* See if it is really trivial. */ + git_committer_info(IDENT_STRICT); + printf(_("Trying really trivial in-index merge...\n")); + if (!read_tree_trivial(&common->item->object.oid, + &head_commit->object.oid, + &remoteheads->item->object.oid)) { + ret = merge_trivial(head_commit, remoteheads); + goto done; + } + printf(_("Nope.\n")); + } + } else { + /* + * An octopus. If we can reach all the remote we are up + * to date. + */ + int up_to_date = 1; + struct commit_list *j; + + for (j = remoteheads; j; j = j->next) { + struct commit_list *common_one; + struct commit *common_item; + + /* + * Here we *have* to calculate the individual + * merge_bases again, otherwise "git merge HEAD^ + * HEAD^^" would be missed. + */ + common_one = repo_get_merge_bases(the_repository, + head_commit, + j->item); + common_item = common_one->item; + free_commit_list(common_one); + if (!oideq(&common_item->object.oid, &j->item->object.oid)) { + up_to_date = 0; + break; + } + } + if (up_to_date) { + finish_up_to_date(); + goto done; + } + } + + if (fast_forward == FF_ONLY) + die_ff_impossible(); + + if (autostash) + create_autostash(the_repository, + git_path_merge_autostash(the_repository)); + + /* We are going to make a new commit. */ + git_committer_info(IDENT_STRICT); + + /* + * At this point, we need a real merge. No matter what strategy + * we use, it would operate on the index, possibly affecting the + * working tree, and when resolved cleanly, have the desired + * tree in the index -- this means that the index must be in + * sync with the head commit. The strategies are responsible + * to ensure this. + * + * Stash away the local changes so that we can try more than one + * and/or recover from merge strategies bailing while leaving the + * index and working tree polluted. + */ + if (save_state(&stash)) + oidclr(&stash); + + for (i = 0; i < use_strategies_nr; i++) { + int ret, cnt; + if (i) { + printf(_("Rewinding the tree to pristine...\n")); + restore_state(&head_commit->object.oid, &stash); + } + if (use_strategies_nr != 1) + printf(_("Trying merge strategy %s...\n"), + use_strategies[i]->name); + /* + * Remember which strategy left the state in the working + * tree. + */ + wt_strategy = use_strategies[i]->name; + + ret = try_merge_strategy(wt_strategy, + common, remoteheads, + head_commit); + /* + * The backend exits with 1 when conflicts are + * left to be resolved, with 2 when it does not + * handle the given merge at all. + */ + if (ret < 2) { + if (!ret) { + /* + * This strategy worked; no point in trying + * another. + */ + merge_was_ok = 1; + best_strategy = wt_strategy; + break; + } + cnt = (use_strategies_nr > 1) ? evaluate_result() : 0; + if (best_cnt <= 0 || cnt <= best_cnt) { + best_strategy = wt_strategy; + best_cnt = cnt; + } + } + } + + /* + * If we have a resulting tree, that means the strategy module + * auto resolved the merge cleanly. + */ + if (merge_was_ok && option_commit) { + automerge_was_ok = 1; + ret = finish_automerge(head_commit, head_subsumed, + common, remoteheads, + &result_tree, wt_strategy); + goto done; + } + + /* + * Pick the result from the best strategy and have the user fix + * it up. + */ + if (!best_strategy) { + restore_state(&head_commit->object.oid, &stash); + if (use_strategies_nr > 1) + fprintf(stderr, + _("No merge strategy handled the merge.\n")); + else + fprintf(stderr, _("Merge with strategy %s failed.\n"), + use_strategies[0]->name); + apply_autostash(git_path_merge_autostash(the_repository)); + ret = 2; + goto done; + } else if (best_strategy == wt_strategy) + ; /* We already have its result in the working tree. */ + else { + printf(_("Rewinding the tree to pristine...\n")); + restore_state(&head_commit->object.oid, &stash); + printf(_("Using the %s strategy to prepare resolving by hand.\n"), + best_strategy); + try_merge_strategy(best_strategy, common, remoteheads, + head_commit); + } + + if (squash) { + finish(head_commit, remoteheads, NULL, NULL); + + git_test_write_commit_graph_or_die(); + } else + write_merge_state(remoteheads); + + if (merge_was_ok) + fprintf(stderr, _("Automatic merge went well; " + "stopped before committing as requested\n")); + else + ret = suggest_conflicts(); + if (autostash) + printf(_("When finished, apply stashed changes with `git stash pop`\n")); + +done: + if (!automerge_was_ok) { + free_commit_list(common); + free_commit_list(remoteheads); + } + strbuf_release(&buf); + free(branch_to_free); + discard_index(&the_index); + return ret; +} diff --git a/builtin/mktag.c b/builtin/mktag.c new file mode 100644 index 0000000..d8e0b5a --- /dev/null +++ b/builtin/mktag.c @@ -0,0 +1,113 @@ +#include "builtin.h" +#include "gettext.h" +#include "hex.h" +#include "parse-options.h" +#include "strbuf.h" +#include "tag.h" +#include "replace-object.h" +#include "object-file.h" +#include "object-store-ll.h" +#include "fsck.h" +#include "config.h" + +static char const * const builtin_mktag_usage[] = { + "git mktag", + NULL +}; +static int option_strict = 1; + +static struct fsck_options fsck_options = FSCK_OPTIONS_STRICT; + +static int mktag_fsck_error_func(struct fsck_options *o UNUSED, + const struct object_id *oid UNUSED, + enum object_type object_type UNUSED, + enum fsck_msg_type msg_type, + enum fsck_msg_id msg_id UNUSED, + const char *message) +{ + switch (msg_type) { + case FSCK_WARN: + if (!option_strict) { + fprintf_ln(stderr, _("warning: tag input does not pass fsck: %s"), message); + return 0; + + } + /* fallthrough */ + case FSCK_ERROR: + /* + * We treat both warnings and errors as errors, things + * like missing "tagger" lines are "only" warnings + * under fsck, we've always considered them an error. + */ + fprintf_ln(stderr, _("error: tag input does not pass fsck: %s"), message); + return 1; + default: + BUG(_("%d (FSCK_IGNORE?) should never trigger this callback"), + msg_type); + } +} + +static int verify_object_in_tag(struct object_id *tagged_oid, int *tagged_type) +{ + int ret; + enum object_type type; + unsigned long size; + void *buffer; + const struct object_id *repl; + + buffer = repo_read_object_file(the_repository, tagged_oid, &type, + &size); + if (!buffer) + die(_("could not read tagged object '%s'"), + oid_to_hex(tagged_oid)); + if (type != *tagged_type) + die(_("object '%s' tagged as '%s', but is a '%s' type"), + oid_to_hex(tagged_oid), + type_name(*tagged_type), type_name(type)); + + repl = lookup_replace_object(the_repository, tagged_oid); + ret = check_object_signature(the_repository, repl, buffer, size, + *tagged_type); + free(buffer); + + return ret; +} + +int cmd_mktag(int argc, const char **argv, const char *prefix) +{ + static struct option builtin_mktag_options[] = { + OPT_BOOL(0, "strict", &option_strict, + N_("enable more strict checking")), + OPT_END(), + }; + struct strbuf buf = STRBUF_INIT; + struct object_id tagged_oid; + int tagged_type; + struct object_id result; + + argc = parse_options(argc, argv, prefix, + builtin_mktag_options, + builtin_mktag_usage, 0); + + if (strbuf_read(&buf, 0, 0) < 0) + die_errno(_("could not read from stdin")); + + fsck_options.error_func = mktag_fsck_error_func; + fsck_set_msg_type_from_ids(&fsck_options, FSCK_MSG_EXTRA_HEADER_ENTRY, + FSCK_WARN); + /* config might set fsck.extraHeaderEntry=* again */ + git_config(git_fsck_config, &fsck_options); + if (fsck_tag_standalone(NULL, buf.buf, buf.len, &fsck_options, + &tagged_oid, &tagged_type)) + die(_("tag on stdin did not pass our strict fsck check")); + + if (verify_object_in_tag(&tagged_oid, &tagged_type) < 0) + die(_("tag on stdin did not refer to a valid object")); + + if (write_object_file(buf.buf, buf.len, OBJ_TAG, &result) < 0) + die(_("unable to write tag file")); + + strbuf_release(&buf); + puts(oid_to_hex(&result)); + return 0; +} diff --git a/builtin/mktree.c b/builtin/mktree.c new file mode 100644 index 0000000..9a22d4e --- /dev/null +++ b/builtin/mktree.c @@ -0,0 +1,203 @@ +/* + * GIT - the stupid content tracker + * + * Copyright (c) Junio C Hamano, 2006, 2009 + */ +#include "builtin.h" +#include "gettext.h" +#include "hex.h" +#include "quote.h" +#include "strbuf.h" +#include "tree.h" +#include "parse-options.h" +#include "object-store-ll.h" + +static struct treeent { + unsigned mode; + struct object_id oid; + int len; + char name[FLEX_ARRAY]; +} **entries; +static int alloc, used; + +static void append_to_tree(unsigned mode, struct object_id *oid, char *path) +{ + struct treeent *ent; + size_t len = strlen(path); + if (strchr(path, '/')) + die("path %s contains slash", path); + + FLEX_ALLOC_MEM(ent, name, path, len); + ent->mode = mode; + ent->len = len; + oidcpy(&ent->oid, oid); + + ALLOC_GROW(entries, used + 1, alloc); + entries[used++] = ent; +} + +static int ent_compare(const void *a_, const void *b_) +{ + struct treeent *a = *(struct treeent **)a_; + struct treeent *b = *(struct treeent **)b_; + return base_name_compare(a->name, a->len, a->mode, + b->name, b->len, b->mode); +} + +static void write_tree(struct object_id *oid) +{ + struct strbuf buf; + size_t size; + int i; + + QSORT(entries, used, ent_compare); + for (size = i = 0; i < used; i++) + size += 32 + entries[i]->len; + + strbuf_init(&buf, size); + for (i = 0; i < used; i++) { + struct treeent *ent = entries[i]; + strbuf_addf(&buf, "%o %s%c", ent->mode, ent->name, '\0'); + strbuf_add(&buf, ent->oid.hash, the_hash_algo->rawsz); + } + + write_object_file(buf.buf, buf.len, OBJ_TREE, oid); + strbuf_release(&buf); +} + +static const char *mktree_usage[] = { + "git mktree [-z] [--missing] [--batch]", + NULL +}; + +static void mktree_line(char *buf, int nul_term_line, int allow_missing) +{ + char *ptr, *ntr; + const char *p; + unsigned mode; + enum object_type mode_type; /* object type derived from mode */ + enum object_type obj_type; /* object type derived from sha */ + struct object_info oi = OBJECT_INFO_INIT; + char *path, *to_free = NULL; + struct object_id oid; + + ptr = buf; + /* + * Read non-recursive ls-tree output format: + * mode SP type SP sha1 TAB name + */ + mode = strtoul(ptr, &ntr, 8); + if (ptr == ntr || !ntr || *ntr != ' ') + die("input format error: %s", buf); + ptr = ntr + 1; /* type */ + ntr = strchr(ptr, ' '); + if (!ntr || parse_oid_hex(ntr + 1, &oid, &p) || + *p != '\t') + die("input format error: %s", buf); + + /* It is perfectly normal if we do not have a commit from a submodule */ + if (S_ISGITLINK(mode)) + allow_missing = 1; + + + *ntr++ = 0; /* now at the beginning of SHA1 */ + + path = (char *)p + 1; /* at the beginning of name */ + if (!nul_term_line && path[0] == '"') { + struct strbuf p_uq = STRBUF_INIT; + if (unquote_c_style(&p_uq, path, NULL)) + die("invalid quoting"); + path = to_free = strbuf_detach(&p_uq, NULL); + } + + /* + * Object type is redundantly derivable three ways. + * These should all agree. + */ + mode_type = object_type(mode); + if (mode_type != type_from_string(ptr)) { + die("entry '%s' object type (%s) doesn't match mode type (%s)", + path, ptr, type_name(mode_type)); + } + + /* Check the type of object identified by oid without fetching objects */ + oi.typep = &obj_type; + if (oid_object_info_extended(the_repository, &oid, &oi, + OBJECT_INFO_LOOKUP_REPLACE | + OBJECT_INFO_QUICK | + OBJECT_INFO_SKIP_FETCH_OBJECT) < 0) + obj_type = -1; + + if (obj_type < 0) { + if (allow_missing) { + ; /* no problem - missing objects are presumed to be of the right type */ + } else { + die("entry '%s' object %s is unavailable", path, oid_to_hex(&oid)); + } + } else { + if (obj_type != mode_type) { + /* + * The object exists but is of the wrong type. + * This is a problem regardless of allow_missing + * because the new tree entry will never be correct. + */ + die("entry '%s' object %s is a %s but specified type was (%s)", + path, oid_to_hex(&oid), type_name(obj_type), type_name(mode_type)); + } + } + + append_to_tree(mode, &oid, path); + free(to_free); +} + +int cmd_mktree(int ac, const char **av, const char *prefix) +{ + struct strbuf sb = STRBUF_INIT; + struct object_id oid; + int nul_term_line = 0; + int allow_missing = 0; + int is_batch_mode = 0; + int got_eof = 0; + strbuf_getline_fn getline_fn; + + const struct option option[] = { + OPT_BOOL('z', NULL, &nul_term_line, N_("input is NUL terminated")), + OPT_SET_INT( 0 , "missing", &allow_missing, N_("allow missing objects"), 1), + OPT_SET_INT( 0 , "batch", &is_batch_mode, N_("allow creation of more than one tree"), 1), + OPT_END() + }; + + ac = parse_options(ac, av, prefix, option, mktree_usage, 0); + getline_fn = nul_term_line ? strbuf_getline_nul : strbuf_getline_lf; + + while (!got_eof) { + while (1) { + if (getline_fn(&sb, stdin) == EOF) { + got_eof = 1; + break; + } + if (sb.buf[0] == '\0') { + /* empty lines denote tree boundaries in batch mode */ + if (is_batch_mode) + break; + die("input format error: (blank line only valid in batch mode)"); + } + mktree_line(sb.buf, nul_term_line, allow_missing); + } + if (is_batch_mode && got_eof && used < 1) { + /* + * Execution gets here if the last tree entry is terminated with a + * new-line. The final new-line has been made optional to be + * consistent with the original non-batch behaviour of mktree. + */ + ; /* skip creating an empty tree */ + } else { + write_tree(&oid); + puts(oid_to_hex(&oid)); + fflush(stdout); + } + used=0; /* reset tree entry buffer for re-use in batch mode */ + } + strbuf_release(&sb); + return 0; +} diff --git a/builtin/multi-pack-index.c b/builtin/multi-pack-index.c new file mode 100644 index 0000000..a72aebe --- /dev/null +++ b/builtin/multi-pack-index.c @@ -0,0 +1,291 @@ +#include "builtin.h" +#include "abspath.h" +#include "config.h" +#include "environment.h" +#include "gettext.h" +#include "parse-options.h" +#include "midx.h" +#include "strbuf.h" +#include "trace2.h" +#include "object-store-ll.h" + +#define BUILTIN_MIDX_WRITE_USAGE \ + N_("git multi-pack-index [<options>] write [--preferred-pack=<pack>]" \ + "[--refs-snapshot=<path>]") + +#define BUILTIN_MIDX_VERIFY_USAGE \ + N_("git multi-pack-index [<options>] verify") + +#define BUILTIN_MIDX_EXPIRE_USAGE \ + N_("git multi-pack-index [<options>] expire") + +#define BUILTIN_MIDX_REPACK_USAGE \ + N_("git multi-pack-index [<options>] repack [--batch-size=<size>]") + +static char const * const builtin_multi_pack_index_write_usage[] = { + BUILTIN_MIDX_WRITE_USAGE, + NULL +}; +static char const * const builtin_multi_pack_index_verify_usage[] = { + BUILTIN_MIDX_VERIFY_USAGE, + NULL +}; +static char const * const builtin_multi_pack_index_expire_usage[] = { + BUILTIN_MIDX_EXPIRE_USAGE, + NULL +}; +static char const * const builtin_multi_pack_index_repack_usage[] = { + BUILTIN_MIDX_REPACK_USAGE, + NULL +}; +static char const * const builtin_multi_pack_index_usage[] = { + BUILTIN_MIDX_WRITE_USAGE, + BUILTIN_MIDX_VERIFY_USAGE, + BUILTIN_MIDX_EXPIRE_USAGE, + BUILTIN_MIDX_REPACK_USAGE, + NULL +}; + +static struct opts_multi_pack_index { + char *object_dir; + const char *preferred_pack; + const char *refs_snapshot; + unsigned long batch_size; + unsigned flags; + int stdin_packs; +} opts; + + +static int parse_object_dir(const struct option *opt, const char *arg, + int unset) +{ + char **value = opt->value; + free(*value); + if (unset) + *value = xstrdup(get_object_directory()); + else + *value = real_pathdup(arg, 1); + return 0; +} + +static struct option common_opts[] = { + OPT_CALLBACK(0, "object-dir", &opts.object_dir, + N_("directory"), + N_("object directory containing set of packfile and pack-index pairs"), + parse_object_dir), + OPT_END(), +}; + +static struct option *add_common_options(struct option *prev) +{ + return parse_options_concat(common_opts, prev); +} + +static int git_multi_pack_index_write_config(const char *var, const char *value, + const struct config_context *ctx UNUSED, + void *cb UNUSED) +{ + if (!strcmp(var, "pack.writebitmaphashcache")) { + if (git_config_bool(var, value)) + opts.flags |= MIDX_WRITE_BITMAP_HASH_CACHE; + else + opts.flags &= ~MIDX_WRITE_BITMAP_HASH_CACHE; + } + + if (!strcmp(var, "pack.writebitmaplookuptable")) { + if (git_config_bool(var, value)) + opts.flags |= MIDX_WRITE_BITMAP_LOOKUP_TABLE; + else + opts.flags &= ~MIDX_WRITE_BITMAP_LOOKUP_TABLE; + } + + /* + * We should never make a fall-back call to 'git_default_config', since + * this was already called in 'cmd_multi_pack_index()'. + */ + return 0; +} + +static void read_packs_from_stdin(struct string_list *to) +{ + struct strbuf buf = STRBUF_INIT; + while (strbuf_getline(&buf, stdin) != EOF) + string_list_append(to, buf.buf); + string_list_sort(to); + + strbuf_release(&buf); +} + +static int cmd_multi_pack_index_write(int argc, const char **argv, + const char *prefix) +{ + struct option *options; + static struct option builtin_multi_pack_index_write_options[] = { + OPT_STRING(0, "preferred-pack", &opts.preferred_pack, + N_("preferred-pack"), + N_("pack for reuse when computing a multi-pack bitmap")), + OPT_BIT(0, "bitmap", &opts.flags, N_("write multi-pack bitmap"), + MIDX_WRITE_BITMAP | MIDX_WRITE_REV_INDEX), + OPT_BIT(0, "progress", &opts.flags, + N_("force progress reporting"), MIDX_PROGRESS), + OPT_BOOL(0, "stdin-packs", &opts.stdin_packs, + N_("write multi-pack index containing only given indexes")), + OPT_FILENAME(0, "refs-snapshot", &opts.refs_snapshot, + N_("refs snapshot for selecting bitmap commits")), + OPT_END(), + }; + + opts.flags |= MIDX_WRITE_BITMAP_HASH_CACHE; + + git_config(git_multi_pack_index_write_config, NULL); + + options = add_common_options(builtin_multi_pack_index_write_options); + + trace2_cmd_mode(argv[0]); + + if (isatty(2)) + opts.flags |= MIDX_PROGRESS; + argc = parse_options(argc, argv, prefix, + options, builtin_multi_pack_index_write_usage, + 0); + if (argc) + usage_with_options(builtin_multi_pack_index_write_usage, + options); + + FREE_AND_NULL(options); + + if (opts.stdin_packs) { + struct string_list packs = STRING_LIST_INIT_DUP; + int ret; + + read_packs_from_stdin(&packs); + + ret = write_midx_file_only(opts.object_dir, &packs, + opts.preferred_pack, + opts.refs_snapshot, opts.flags); + + string_list_clear(&packs, 0); + + return ret; + + } + return write_midx_file(opts.object_dir, opts.preferred_pack, + opts.refs_snapshot, opts.flags); +} + +static int cmd_multi_pack_index_verify(int argc, const char **argv, + const char *prefix) +{ + struct option *options; + static struct option builtin_multi_pack_index_verify_options[] = { + OPT_BIT(0, "progress", &opts.flags, + N_("force progress reporting"), MIDX_PROGRESS), + OPT_END(), + }; + options = add_common_options(builtin_multi_pack_index_verify_options); + + trace2_cmd_mode(argv[0]); + + if (isatty(2)) + opts.flags |= MIDX_PROGRESS; + argc = parse_options(argc, argv, prefix, + options, builtin_multi_pack_index_verify_usage, + 0); + if (argc) + usage_with_options(builtin_multi_pack_index_verify_usage, + options); + + FREE_AND_NULL(options); + + return verify_midx_file(the_repository, opts.object_dir, opts.flags); +} + +static int cmd_multi_pack_index_expire(int argc, const char **argv, + const char *prefix) +{ + struct option *options; + static struct option builtin_multi_pack_index_expire_options[] = { + OPT_BIT(0, "progress", &opts.flags, + N_("force progress reporting"), MIDX_PROGRESS), + OPT_END(), + }; + options = add_common_options(builtin_multi_pack_index_expire_options); + + trace2_cmd_mode(argv[0]); + + if (isatty(2)) + opts.flags |= MIDX_PROGRESS; + argc = parse_options(argc, argv, prefix, + options, builtin_multi_pack_index_expire_usage, + 0); + if (argc) + usage_with_options(builtin_multi_pack_index_expire_usage, + options); + + FREE_AND_NULL(options); + + return expire_midx_packs(the_repository, opts.object_dir, opts.flags); +} + +static int cmd_multi_pack_index_repack(int argc, const char **argv, + const char *prefix) +{ + struct option *options; + static struct option builtin_multi_pack_index_repack_options[] = { + OPT_MAGNITUDE(0, "batch-size", &opts.batch_size, + N_("during repack, collect pack-files of smaller size into a batch that is larger than this size")), + OPT_BIT(0, "progress", &opts.flags, + N_("force progress reporting"), MIDX_PROGRESS), + OPT_END(), + }; + + options = add_common_options(builtin_multi_pack_index_repack_options); + + trace2_cmd_mode(argv[0]); + + if (isatty(2)) + opts.flags |= MIDX_PROGRESS; + argc = parse_options(argc, argv, prefix, + options, + builtin_multi_pack_index_repack_usage, + 0); + if (argc) + usage_with_options(builtin_multi_pack_index_repack_usage, + options); + + FREE_AND_NULL(options); + + return midx_repack(the_repository, opts.object_dir, + (size_t)opts.batch_size, opts.flags); +} + +int cmd_multi_pack_index(int argc, const char **argv, + const char *prefix) +{ + int res; + parse_opt_subcommand_fn *fn = NULL; + struct option builtin_multi_pack_index_options[] = { + OPT_SUBCOMMAND("repack", &fn, cmd_multi_pack_index_repack), + OPT_SUBCOMMAND("write", &fn, cmd_multi_pack_index_write), + OPT_SUBCOMMAND("verify", &fn, cmd_multi_pack_index_verify), + OPT_SUBCOMMAND("expire", &fn, cmd_multi_pack_index_expire), + OPT_END(), + }; + struct option *options = parse_options_concat(builtin_multi_pack_index_options, common_opts); + + git_config(git_default_config, NULL); + + if (the_repository && + the_repository->objects && + the_repository->objects->odb) + opts.object_dir = xstrdup(the_repository->objects->odb->path); + + argc = parse_options(argc, argv, prefix, options, + builtin_multi_pack_index_usage, 0); + FREE_AND_NULL(options); + + res = fn(argc, argv, prefix); + + free(opts.object_dir); + return res; +} diff --git a/builtin/mv.c b/builtin/mv.c new file mode 100644 index 0000000..c596515 --- /dev/null +++ b/builtin/mv.c @@ -0,0 +1,576 @@ +/* + * "git mv" builtin command + * + * Copyright (C) 2006 Johannes Schindelin + */ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "abspath.h" +#include "advice.h" +#include "config.h" +#include "environment.h" +#include "gettext.h" +#include "name-hash.h" +#include "object-file.h" +#include "pathspec.h" +#include "lockfile.h" +#include "dir.h" +#include "cache-tree.h" +#include "string-list.h" +#include "parse-options.h" +#include "read-cache-ll.h" +#include "repository.h" +#include "setup.h" +#include "submodule.h" +#include "entry.h" + +static const char * const builtin_mv_usage[] = { + N_("git mv [<options>] <source>... <destination>"), + NULL +}; + +enum update_mode { + WORKING_DIRECTORY = (1 << 1), + INDEX = (1 << 2), + SPARSE = (1 << 3), + SKIP_WORKTREE_DIR = (1 << 4), +}; + +#define DUP_BASENAME 1 +#define KEEP_TRAILING_SLASH 2 + +static const char **internal_prefix_pathspec(const char *prefix, + const char **pathspec, + int count, unsigned flags) +{ + int i; + const char **result; + int prefixlen = prefix ? strlen(prefix) : 0; + ALLOC_ARRAY(result, count + 1); + + /* Create an intermediate copy of the pathspec based on the flags */ + for (i = 0; i < count; i++) { + int length = strlen(pathspec[i]); + int to_copy = length; + char *it; + while (!(flags & KEEP_TRAILING_SLASH) && + to_copy > 0 && is_dir_sep(pathspec[i][to_copy - 1])) + to_copy--; + + it = xmemdupz(pathspec[i], to_copy); + if (flags & DUP_BASENAME) { + result[i] = xstrdup(basename(it)); + free(it); + } else { + result[i] = it; + } + } + result[count] = NULL; + + /* Prefix the pathspec and free the old intermediate strings */ + for (i = 0; i < count; i++) { + const char *match = prefix_path(prefix, prefixlen, result[i]); + free((char *) result[i]); + result[i] = match; + } + + return result; +} + +static const char *add_slash(const char *path) +{ + size_t len = strlen(path); + if (len && path[len - 1] != '/') { + char *with_slash = xmalloc(st_add(len, 2)); + memcpy(with_slash, path, len); + with_slash[len++] = '/'; + with_slash[len] = 0; + return with_slash; + } + return path; +} + +#define SUBMODULE_WITH_GITDIR ((const char *)1) + +static void prepare_move_submodule(const char *src, int first, + const char **submodule_gitfile) +{ + struct strbuf submodule_dotgit = STRBUF_INIT; + if (!S_ISGITLINK(the_index.cache[first]->ce_mode)) + die(_("Directory %s is in index and no submodule?"), src); + if (!is_staging_gitmodules_ok(&the_index)) + die(_("Please stage your changes to .gitmodules or stash them to proceed")); + strbuf_addf(&submodule_dotgit, "%s/.git", src); + *submodule_gitfile = read_gitfile(submodule_dotgit.buf); + if (*submodule_gitfile) + *submodule_gitfile = xstrdup(*submodule_gitfile); + else + *submodule_gitfile = SUBMODULE_WITH_GITDIR; + strbuf_release(&submodule_dotgit); +} + +static int index_range_of_same_dir(const char *src, int length, + int *first_p, int *last_p) +{ + const char *src_w_slash = add_slash(src); + int first, last, len_w_slash = length + 1; + + first = index_name_pos(&the_index, src_w_slash, len_w_slash); + if (first >= 0) + die(_("%.*s is in index"), len_w_slash, src_w_slash); + + first = -1 - first; + for (last = first; last < the_index.cache_nr; last++) { + const char *path = the_index.cache[last]->name; + if (strncmp(path, src_w_slash, len_w_slash)) + break; + } + if (src_w_slash != src) + free((char *)src_w_slash); + *first_p = first; + *last_p = last; + return last - first; +} + +/* + * Given the path of a directory that does not exist on-disk, check whether the + * directory contains any entries in the index with the SKIP_WORKTREE flag + * enabled. + * Return 1 if such index entries exist. + * Return 0 otherwise. + */ +static int empty_dir_has_sparse_contents(const char *name) +{ + int ret = 0; + const char *with_slash = add_slash(name); + int length = strlen(with_slash); + + int pos = index_name_pos(&the_index, with_slash, length); + const struct cache_entry *ce; + + if (pos < 0) { + pos = -pos - 1; + if (pos >= the_index.cache_nr) + goto free_return; + ce = the_index.cache[pos]; + if (strncmp(with_slash, ce->name, length)) + goto free_return; + if (ce_skip_worktree(ce)) + ret = 1; + } + +free_return: + if (with_slash != name) + free((char *)with_slash); + return ret; +} + +int cmd_mv(int argc, const char **argv, const char *prefix) +{ + int i, flags, gitmodules_modified = 0; + int verbose = 0, show_only = 0, force = 0, ignore_errors = 0, ignore_sparse = 0; + struct option builtin_mv_options[] = { + OPT__VERBOSE(&verbose, N_("be verbose")), + OPT__DRY_RUN(&show_only, N_("dry run")), + OPT__FORCE(&force, N_("force move/rename even if target exists"), + PARSE_OPT_NOCOMPLETE), + OPT_BOOL('k', NULL, &ignore_errors, N_("skip move/rename errors")), + OPT_BOOL(0, "sparse", &ignore_sparse, N_("allow updating entries outside of the sparse-checkout cone")), + OPT_END(), + }; + const char **source, **destination, **dest_path, **submodule_gitfile; + const char *dst_w_slash; + const char **src_dir = NULL; + int src_dir_nr = 0, src_dir_alloc = 0; + struct strbuf a_src_dir = STRBUF_INIT; + enum update_mode *modes, dst_mode = 0; + struct stat st, dest_st; + struct string_list src_for_dst = STRING_LIST_INIT_NODUP; + struct lock_file lock_file = LOCK_INIT; + struct cache_entry *ce; + struct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP; + struct string_list dirty_paths = STRING_LIST_INIT_NODUP; + + git_config(git_default_config, NULL); + + argc = parse_options(argc, argv, prefix, builtin_mv_options, + builtin_mv_usage, 0); + if (--argc < 1) + usage_with_options(builtin_mv_usage, builtin_mv_options); + + repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR); + if (repo_read_index(the_repository) < 0) + die(_("index file corrupt")); + + source = internal_prefix_pathspec(prefix, argv, argc, 0); + CALLOC_ARRAY(modes, argc); + + /* + * Keep trailing slash, needed to let + * "git mv file no-such-dir/" error out, except in the case + * "git mv directory no-such-dir/". + */ + flags = KEEP_TRAILING_SLASH; + if (argc == 1 && is_directory(argv[0]) && !is_directory(argv[1])) + flags = 0; + dest_path = internal_prefix_pathspec(prefix, argv + argc, 1, flags); + dst_w_slash = add_slash(dest_path[0]); + submodule_gitfile = xcalloc(argc, sizeof(char *)); + + if (dest_path[0][0] == '\0') + /* special case: "." was normalized to "" */ + destination = internal_prefix_pathspec(dest_path[0], argv, argc, DUP_BASENAME); + else if (!lstat(dest_path[0], &st) && + S_ISDIR(st.st_mode)) { + destination = internal_prefix_pathspec(dst_w_slash, argv, argc, DUP_BASENAME); + } else { + if (!path_in_sparse_checkout(dst_w_slash, &the_index) && + empty_dir_has_sparse_contents(dst_w_slash)) { + destination = internal_prefix_pathspec(dst_w_slash, argv, argc, DUP_BASENAME); + dst_mode = SKIP_WORKTREE_DIR; + } else if (argc != 1) { + die(_("destination '%s' is not a directory"), dest_path[0]); + } else { + destination = dest_path; + /* + * <destination> is a file outside of sparse-checkout + * cone. Insist on cone mode here for backward + * compatibility. We don't want dst_mode to be assigned + * for a file when the repo is using no-cone mode (which + * is deprecated at this point) sparse-checkout. As + * SPARSE here is only considering cone-mode situation. + */ + if (!path_in_cone_mode_sparse_checkout(destination[0], &the_index)) + dst_mode = SPARSE; + } + } + if (dst_w_slash != dest_path[0]) { + free((char *)dst_w_slash); + dst_w_slash = NULL; + } + + /* Checking */ + for (i = 0; i < argc; i++) { + const char *src = source[i], *dst = destination[i]; + int length; + const char *bad = NULL; + int skip_sparse = 0; + + if (show_only) + printf(_("Checking rename of '%s' to '%s'\n"), src, dst); + + length = strlen(src); + if (lstat(src, &st) < 0) { + int pos; + const struct cache_entry *ce; + + pos = index_name_pos(&the_index, src, length); + if (pos < 0) { + const char *src_w_slash = add_slash(src); + if (!path_in_sparse_checkout(src_w_slash, &the_index) && + empty_dir_has_sparse_contents(src)) { + modes[i] |= SKIP_WORKTREE_DIR; + goto dir_check; + } + /* only error if existence is expected. */ + if (!(modes[i] & SPARSE)) + bad = _("bad source"); + goto act_on_entry; + } + ce = the_index.cache[pos]; + if (!ce_skip_worktree(ce)) { + bad = _("bad source"); + goto act_on_entry; + } + if (!ignore_sparse) { + string_list_append(&only_match_skip_worktree, src); + goto act_on_entry; + } + /* Check if dst exists in index */ + if (index_name_pos(&the_index, dst, strlen(dst)) < 0) { + modes[i] |= SPARSE; + goto act_on_entry; + } + if (!force) { + bad = _("destination exists"); + goto act_on_entry; + } + modes[i] |= SPARSE; + goto act_on_entry; + } + if (!strncmp(src, dst, length) && + (dst[length] == 0 || dst[length] == '/')) { + bad = _("can not move directory into itself"); + goto act_on_entry; + } + if (S_ISDIR(st.st_mode) + && lstat(dst, &dest_st) == 0) { + bad = _("destination already exists"); + goto act_on_entry; + } + +dir_check: + if (S_ISDIR(st.st_mode)) { + int j, dst_len, n; + int first = index_name_pos(&the_index, src, length), last; + + if (first >= 0) { + prepare_move_submodule(src, first, + submodule_gitfile + i); + goto act_on_entry; + } else if (index_range_of_same_dir(src, length, + &first, &last) < 1) { + bad = _("source directory is empty"); + goto act_on_entry; + } + + /* last - first >= 1 */ + modes[i] |= WORKING_DIRECTORY; + + ALLOC_GROW(src_dir, src_dir_nr + 1, src_dir_alloc); + src_dir[src_dir_nr++] = src; + + n = argc + last - first; + REALLOC_ARRAY(source, n); + REALLOC_ARRAY(destination, n); + REALLOC_ARRAY(modes, n); + REALLOC_ARRAY(submodule_gitfile, n); + + dst = add_slash(dst); + dst_len = strlen(dst); + + for (j = 0; j < last - first; j++) { + const struct cache_entry *ce = the_index.cache[first + j]; + const char *path = ce->name; + source[argc + j] = path; + destination[argc + j] = + prefix_path(dst, dst_len, path + length + 1); + memset(modes + argc + j, 0, sizeof(enum update_mode)); + modes[argc + j] |= ce_skip_worktree(ce) ? SPARSE : INDEX; + submodule_gitfile[argc + j] = NULL; + } + argc += last - first; + goto act_on_entry; + } + if (!(ce = index_file_exists(&the_index, src, length, 0))) { + bad = _("not under version control"); + goto act_on_entry; + } + if (ce_stage(ce)) { + bad = _("conflicted"); + goto act_on_entry; + } + if (lstat(dst, &st) == 0 && + (!ignore_case || strcasecmp(src, dst))) { + bad = _("destination exists"); + if (force) { + /* + * only files can overwrite each other: + * check both source and destination + */ + if (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) { + if (verbose) + warning(_("overwriting '%s'"), dst); + bad = NULL; + } else + bad = _("Cannot overwrite"); + } + goto act_on_entry; + } + if (string_list_has_string(&src_for_dst, dst)) { + bad = _("multiple sources for the same target"); + goto act_on_entry; + } + if (is_dir_sep(dst[strlen(dst) - 1])) { + bad = _("destination directory does not exist"); + goto act_on_entry; + } + + if (ignore_sparse && + (dst_mode & (SKIP_WORKTREE_DIR | SPARSE)) && + index_entry_exists(&the_index, dst, strlen(dst))) { + bad = _("destination exists in the index"); + if (force) { + if (verbose) + warning(_("overwriting '%s'"), dst); + bad = NULL; + } else { + goto act_on_entry; + } + } + /* + * We check if the paths are in the sparse-checkout + * definition as a very final check, since that + * allows us to point the user to the --sparse + * option as a way to have a successful run. + */ + if (!ignore_sparse && + !path_in_sparse_checkout(src, &the_index)) { + string_list_append(&only_match_skip_worktree, src); + skip_sparse = 1; + } + if (!ignore_sparse && + !path_in_sparse_checkout(dst, &the_index)) { + string_list_append(&only_match_skip_worktree, dst); + skip_sparse = 1; + } + + if (skip_sparse) + goto remove_entry; + + string_list_insert(&src_for_dst, dst); + +act_on_entry: + if (!bad) + continue; + if (!ignore_errors) + die(_("%s, source=%s, destination=%s"), + bad, src, dst); +remove_entry: + if (--argc > 0) { + int n = argc - i; + MOVE_ARRAY(source + i, source + i + 1, n); + MOVE_ARRAY(destination + i, destination + i + 1, n); + MOVE_ARRAY(modes + i, modes + i + 1, n); + MOVE_ARRAY(submodule_gitfile + i, + submodule_gitfile + i + 1, n); + i--; + } + } + + if (only_match_skip_worktree.nr) { + advise_on_updating_sparse_paths(&only_match_skip_worktree); + if (!ignore_errors) + return 1; + } + + for (i = 0; i < argc; i++) { + const char *src = source[i], *dst = destination[i]; + enum update_mode mode = modes[i]; + int pos; + int sparse_and_dirty = 0; + struct checkout state = CHECKOUT_INIT; + state.istate = &the_index; + + if (force) + state.force = 1; + if (show_only || verbose) + printf(_("Renaming %s to %s\n"), src, dst); + if (show_only) + continue; + if (!(mode & (INDEX | SPARSE | SKIP_WORKTREE_DIR)) && + !(dst_mode & (SKIP_WORKTREE_DIR | SPARSE)) && + rename(src, dst) < 0) { + if (ignore_errors) + continue; + die_errno(_("renaming '%s' failed"), src); + } + if (submodule_gitfile[i]) { + if (!update_path_in_gitmodules(src, dst)) + gitmodules_modified = 1; + if (submodule_gitfile[i] != SUBMODULE_WITH_GITDIR) + connect_work_tree_and_git_dir(dst, + submodule_gitfile[i], + 1); + } + + if (mode & (WORKING_DIRECTORY | SKIP_WORKTREE_DIR)) + continue; + + pos = index_name_pos(&the_index, src, strlen(src)); + assert(pos >= 0); + if (!(mode & SPARSE) && !lstat(src, &st)) + sparse_and_dirty = ie_modified(&the_index, + the_index.cache[pos], + &st, + 0); + rename_index_entry_at(&the_index, pos, dst); + + if (ignore_sparse && + core_apply_sparse_checkout && + core_sparse_checkout_cone) { + /* + * NEEDSWORK: we are *not* paying attention to + * "out-to-out" move (<source> is out-of-cone and + * <destination> is out-of-cone) at this point. It + * should be added in a future patch. + */ + if ((mode & SPARSE) && + path_in_sparse_checkout(dst, &the_index)) { + /* from out-of-cone to in-cone */ + int dst_pos = index_name_pos(&the_index, dst, + strlen(dst)); + struct cache_entry *dst_ce = the_index.cache[dst_pos]; + + dst_ce->ce_flags &= ~CE_SKIP_WORKTREE; + + if (checkout_entry(dst_ce, &state, NULL, NULL)) + die(_("cannot checkout %s"), dst_ce->name); + } else if ((dst_mode & (SKIP_WORKTREE_DIR | SPARSE)) && + !(mode & SPARSE) && + !path_in_sparse_checkout(dst, &the_index)) { + /* from in-cone to out-of-cone */ + int dst_pos = index_name_pos(&the_index, dst, + strlen(dst)); + struct cache_entry *dst_ce = the_index.cache[dst_pos]; + + /* + * if src is clean, it will suffice to remove it + */ + if (!sparse_and_dirty) { + dst_ce->ce_flags |= CE_SKIP_WORKTREE; + unlink_or_warn(src); + } else { + /* + * if src is dirty, move it to the + * destination and create leading + * dirs if necessary + */ + char *dst_dup = xstrdup(dst); + string_list_append(&dirty_paths, dst); + safe_create_leading_directories(dst_dup); + FREE_AND_NULL(dst_dup); + rename(src, dst); + } + } + } + } + + /* + * cleanup the empty src_dirs + */ + for (i = 0; i < src_dir_nr; i++) { + int dummy; + strbuf_addstr(&a_src_dir, src_dir[i]); + /* + * if entries under a_src_dir are all moved away, + * recursively remove a_src_dir to cleanup + */ + if (index_range_of_same_dir(a_src_dir.buf, a_src_dir.len, + &dummy, &dummy) < 1) { + remove_dir_recursively(&a_src_dir, 0); + } + strbuf_reset(&a_src_dir); + } + + strbuf_release(&a_src_dir); + free(src_dir); + + if (dirty_paths.nr) + advise_on_moving_dirty_path(&dirty_paths); + + if (gitmodules_modified) + stage_updated_gitmodules(&the_index); + + if (write_locked_index(&the_index, &lock_file, + COMMIT_LOCK | SKIP_IF_UNCHANGED)) + die(_("Unable to write new index file")); + + string_list_clear(&src_for_dst, 0); + string_list_clear(&dirty_paths, 0); + UNLEAK(source); + UNLEAK(dest_path); + free(submodule_gitfile); + free(modes); + return 0; +} diff --git a/builtin/name-rev.c b/builtin/name-rev.c new file mode 100644 index 0000000..2dd1807 --- /dev/null +++ b/builtin/name-rev.c @@ -0,0 +1,681 @@ +#include "builtin.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "repository.h" +#include "config.h" +#include "commit.h" +#include "tag.h" +#include "refs.h" +#include "object-name.h" +#include "pager.h" +#include "parse-options.h" +#include "prio-queue.h" +#include "hash-lookup.h" +#include "commit-slab.h" +#include "commit-graph.h" +#include "wildmatch.h" + +/* + * One day. See the 'name a rev shortly after epoch' test in t6120 when + * changing this value + */ +#define CUTOFF_DATE_SLOP 86400 + +struct rev_name { + const char *tip_name; + timestamp_t taggerdate; + int generation; + int distance; + int from_tag; +}; + +define_commit_slab(commit_rev_name, struct rev_name); + +static timestamp_t generation_cutoff = GENERATION_NUMBER_INFINITY; +static timestamp_t cutoff = TIME_MAX; +static struct commit_rev_name rev_names; + +/* Disable the cutoff checks entirely */ +static void disable_cutoff(void) +{ + generation_cutoff = 0; + cutoff = 0; +} + +/* Cutoff searching any commits older than this one */ +static void set_commit_cutoff(struct commit *commit) +{ + + if (cutoff > commit->date) + cutoff = commit->date; + + if (generation_cutoff) { + timestamp_t generation = commit_graph_generation(commit); + + if (generation_cutoff > generation) + generation_cutoff = generation; + } +} + +/* adjust the commit date cutoff with a slop to allow for slightly incorrect + * commit timestamps in case of clock skew. + */ +static void adjust_cutoff_timestamp_for_slop(void) +{ + if (cutoff) { + /* check for undeflow */ + if (cutoff > TIME_MIN + CUTOFF_DATE_SLOP) + cutoff = cutoff - CUTOFF_DATE_SLOP; + else + cutoff = TIME_MIN; + } +} + +/* Check if a commit is before the cutoff. Prioritize generation numbers + * first, but use the commit timestamp if we lack generation data. + */ +static int commit_is_before_cutoff(struct commit *commit) +{ + if (generation_cutoff < GENERATION_NUMBER_INFINITY) + return generation_cutoff && + commit_graph_generation(commit) < generation_cutoff; + + return commit->date < cutoff; +} + +/* How many generations are maximally preferred over _one_ merge traversal? */ +#define MERGE_TRAVERSAL_WEIGHT 65535 + +static int is_valid_rev_name(const struct rev_name *name) +{ + return name && name->tip_name; +} + +static struct rev_name *get_commit_rev_name(const struct commit *commit) +{ + struct rev_name *name = commit_rev_name_peek(&rev_names, commit); + + return is_valid_rev_name(name) ? name : NULL; +} + +static int effective_distance(int distance, int generation) +{ + return distance + (generation > 0 ? MERGE_TRAVERSAL_WEIGHT : 0); +} + +static int is_better_name(struct rev_name *name, + timestamp_t taggerdate, + int generation, + int distance, + int from_tag) +{ + int name_distance = effective_distance(name->distance, name->generation); + int new_distance = effective_distance(distance, generation); + + /* If both are tags, we prefer the nearer one. */ + if (from_tag && name->from_tag) + return name_distance > new_distance; + + /* Favor a tag over a non-tag. */ + if (name->from_tag != from_tag) + return from_tag; + + /* + * We are now looking at two non-tags. Tiebreak to favor + * shorter hops. + */ + if (name_distance != new_distance) + return name_distance > new_distance; + + /* ... or tiebreak to favor older date */ + if (name->taggerdate != taggerdate) + return name->taggerdate > taggerdate; + + /* keep the current one if we cannot decide */ + return 0; +} + +static struct rev_name *create_or_update_name(struct commit *commit, + timestamp_t taggerdate, + int generation, int distance, + int from_tag) +{ + struct rev_name *name = commit_rev_name_at(&rev_names, commit); + + if (is_valid_rev_name(name) && + !is_better_name(name, taggerdate, generation, distance, from_tag)) + return NULL; + + name->taggerdate = taggerdate; + name->generation = generation; + name->distance = distance; + name->from_tag = from_tag; + + return name; +} + +static char *get_parent_name(const struct rev_name *name, int parent_number) +{ + struct strbuf sb = STRBUF_INIT; + size_t len; + + strip_suffix(name->tip_name, "^0", &len); + if (name->generation > 0) { + strbuf_grow(&sb, len + + 1 + decimal_width(name->generation) + + 1 + decimal_width(parent_number)); + strbuf_addf(&sb, "%.*s~%d^%d", (int)len, name->tip_name, + name->generation, parent_number); + } else { + strbuf_grow(&sb, len + + 1 + decimal_width(parent_number)); + strbuf_addf(&sb, "%.*s^%d", (int)len, name->tip_name, + parent_number); + } + return strbuf_detach(&sb, NULL); +} + +static void name_rev(struct commit *start_commit, + const char *tip_name, timestamp_t taggerdate, + int from_tag, int deref) +{ + struct prio_queue queue; + struct commit *commit; + struct commit **parents_to_queue = NULL; + size_t parents_to_queue_nr, parents_to_queue_alloc = 0; + struct rev_name *start_name; + + repo_parse_commit(the_repository, start_commit); + if (commit_is_before_cutoff(start_commit)) + return; + + start_name = create_or_update_name(start_commit, taggerdate, 0, 0, + from_tag); + if (!start_name) + return; + if (deref) + start_name->tip_name = xstrfmt("%s^0", tip_name); + else + start_name->tip_name = xstrdup(tip_name); + + memset(&queue, 0, sizeof(queue)); /* Use the prio_queue as LIFO */ + prio_queue_put(&queue, start_commit); + + while ((commit = prio_queue_get(&queue))) { + struct rev_name *name = get_commit_rev_name(commit); + struct commit_list *parents; + int parent_number = 1; + + parents_to_queue_nr = 0; + + for (parents = commit->parents; + parents; + parents = parents->next, parent_number++) { + struct commit *parent = parents->item; + struct rev_name *parent_name; + int generation, distance; + + repo_parse_commit(the_repository, parent); + if (commit_is_before_cutoff(parent)) + continue; + + if (parent_number > 1) { + generation = 0; + distance = name->distance + MERGE_TRAVERSAL_WEIGHT; + } else { + generation = name->generation + 1; + distance = name->distance + 1; + } + + parent_name = create_or_update_name(parent, taggerdate, + generation, + distance, from_tag); + if (parent_name) { + if (parent_number > 1) + parent_name->tip_name = + get_parent_name(name, + parent_number); + else + parent_name->tip_name = name->tip_name; + ALLOC_GROW(parents_to_queue, + parents_to_queue_nr + 1, + parents_to_queue_alloc); + parents_to_queue[parents_to_queue_nr] = parent; + parents_to_queue_nr++; + } + } + + /* The first parent must come out first from the prio_queue */ + while (parents_to_queue_nr) + prio_queue_put(&queue, + parents_to_queue[--parents_to_queue_nr]); + } + + clear_prio_queue(&queue); + free(parents_to_queue); +} + +static int subpath_matches(const char *path, const char *filter) +{ + const char *subpath = path; + + while (subpath) { + if (!wildmatch(filter, subpath, 0)) + return subpath - path; + subpath = strchr(subpath, '/'); + if (subpath) + subpath++; + } + return -1; +} + +struct name_ref_data { + int tags_only; + int name_only; + struct string_list ref_filters; + struct string_list exclude_filters; +}; + +static struct tip_table { + struct tip_table_entry { + struct object_id oid; + const char *refname; + struct commit *commit; + timestamp_t taggerdate; + unsigned int from_tag:1; + unsigned int deref:1; + } *table; + int nr; + int alloc; + int sorted; +} tip_table; + +static void add_to_tip_table(const struct object_id *oid, const char *refname, + int shorten_unambiguous, struct commit *commit, + timestamp_t taggerdate, int from_tag, int deref) +{ + char *short_refname = NULL; + + if (shorten_unambiguous) + short_refname = shorten_unambiguous_ref(refname, 0); + else if (skip_prefix(refname, "refs/heads/", &refname)) + ; /* refname already advanced */ + else + skip_prefix(refname, "refs/", &refname); + + ALLOC_GROW(tip_table.table, tip_table.nr + 1, tip_table.alloc); + oidcpy(&tip_table.table[tip_table.nr].oid, oid); + tip_table.table[tip_table.nr].refname = short_refname ? + short_refname : xstrdup(refname); + tip_table.table[tip_table.nr].commit = commit; + tip_table.table[tip_table.nr].taggerdate = taggerdate; + tip_table.table[tip_table.nr].from_tag = from_tag; + tip_table.table[tip_table.nr].deref = deref; + tip_table.nr++; + tip_table.sorted = 0; +} + +static int tipcmp(const void *a_, const void *b_) +{ + const struct tip_table_entry *a = a_, *b = b_; + return oidcmp(&a->oid, &b->oid); +} + +static int cmp_by_tag_and_age(const void *a_, const void *b_) +{ + const struct tip_table_entry *a = a_, *b = b_; + int cmp; + + /* Prefer tags. */ + cmp = b->from_tag - a->from_tag; + if (cmp) + return cmp; + + /* Older is better. */ + if (a->taggerdate < b->taggerdate) + return -1; + return a->taggerdate != b->taggerdate; +} + +static int name_ref(const char *path, const struct object_id *oid, + int flags UNUSED, void *cb_data) +{ + struct object *o = parse_object(the_repository, oid); + struct name_ref_data *data = cb_data; + int can_abbreviate_output = data->tags_only && data->name_only; + int deref = 0; + int from_tag = 0; + struct commit *commit = NULL; + timestamp_t taggerdate = TIME_MAX; + + if (data->tags_only && !starts_with(path, "refs/tags/")) + return 0; + + if (data->exclude_filters.nr) { + struct string_list_item *item; + + for_each_string_list_item(item, &data->exclude_filters) { + if (subpath_matches(path, item->string) >= 0) + return 0; + } + } + + if (data->ref_filters.nr) { + struct string_list_item *item; + int matched = 0; + + /* See if any of the patterns match. */ + for_each_string_list_item(item, &data->ref_filters) { + /* + * Check all patterns even after finding a match, so + * that we can see if a match with a subpath exists. + * When a user asked for 'refs/tags/v*' and 'v1.*', + * both of which match, the user is showing her + * willingness to accept a shortened output by having + * the 'v1.*' in the acceptable refnames, so we + * shouldn't stop when seeing 'refs/tags/v1.4' matches + * 'refs/tags/v*'. We should show it as 'v1.4'. + */ + switch (subpath_matches(path, item->string)) { + case -1: /* did not match */ + break; + case 0: /* matched fully */ + matched = 1; + break; + default: /* matched subpath */ + matched = 1; + can_abbreviate_output = 1; + break; + } + } + + /* If none of the patterns matched, stop now */ + if (!matched) + return 0; + } + + while (o && o->type == OBJ_TAG) { + struct tag *t = (struct tag *) o; + if (!t->tagged) + break; /* broken repository */ + o = parse_object(the_repository, &t->tagged->oid); + deref = 1; + taggerdate = t->date; + } + if (o && o->type == OBJ_COMMIT) { + commit = (struct commit *)o; + from_tag = starts_with(path, "refs/tags/"); + if (taggerdate == TIME_MAX) + taggerdate = commit->date; + } + + add_to_tip_table(oid, path, can_abbreviate_output, commit, taggerdate, + from_tag, deref); + return 0; +} + +static void name_tips(void) +{ + int i; + + /* + * Try to set better names first, so that worse ones spread + * less. + */ + QSORT(tip_table.table, tip_table.nr, cmp_by_tag_and_age); + for (i = 0; i < tip_table.nr; i++) { + struct tip_table_entry *e = &tip_table.table[i]; + if (e->commit) { + name_rev(e->commit, e->refname, e->taggerdate, + e->from_tag, e->deref); + } + } +} + +static const struct object_id *nth_tip_table_ent(size_t ix, const void *table_) +{ + const struct tip_table_entry *table = table_; + return &table[ix].oid; +} + +static const char *get_exact_ref_match(const struct object *o) +{ + int found; + + if (!tip_table.table || !tip_table.nr) + return NULL; + + if (!tip_table.sorted) { + QSORT(tip_table.table, tip_table.nr, tipcmp); + tip_table.sorted = 1; + } + + found = oid_pos(&o->oid, tip_table.table, tip_table.nr, + nth_tip_table_ent); + if (0 <= found) + return tip_table.table[found].refname; + return NULL; +} + +/* may return a constant string or use "buf" as scratch space */ +static const char *get_rev_name(const struct object *o, struct strbuf *buf) +{ + struct rev_name *n; + const struct commit *c; + + if (o->type != OBJ_COMMIT) + return get_exact_ref_match(o); + c = (const struct commit *) o; + n = get_commit_rev_name(c); + if (!n) + return NULL; + + if (!n->generation) + return n->tip_name; + else { + strbuf_reset(buf); + strbuf_addstr(buf, n->tip_name); + strbuf_strip_suffix(buf, "^0"); + strbuf_addf(buf, "~%d", n->generation); + return buf->buf; + } +} + +static void show_name(const struct object *obj, + const char *caller_name, + int always, int allow_undefined, int name_only) +{ + const char *name; + const struct object_id *oid = &obj->oid; + struct strbuf buf = STRBUF_INIT; + + if (!name_only) + printf("%s ", caller_name ? caller_name : oid_to_hex(oid)); + name = get_rev_name(obj, &buf); + if (name) + printf("%s\n", name); + else if (allow_undefined) + printf("undefined\n"); + else if (always) + printf("%s\n", + repo_find_unique_abbrev(the_repository, oid, DEFAULT_ABBREV)); + else + die("cannot describe '%s'", oid_to_hex(oid)); + strbuf_release(&buf); +} + +static char const * const name_rev_usage[] = { + N_("git name-rev [<options>] <commit>..."), + N_("git name-rev [<options>] --all"), + N_("git name-rev [<options>] --annotate-stdin"), + NULL +}; + +static void name_rev_line(char *p, struct name_ref_data *data) +{ + struct strbuf buf = STRBUF_INIT; + int counter = 0; + char *p_start; + const unsigned hexsz = the_hash_algo->hexsz; + + for (p_start = p; *p; p++) { +#define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f')) + if (!ishex(*p)) + counter = 0; + else if (++counter == hexsz && + !ishex(*(p+1))) { + struct object_id oid; + const char *name = NULL; + char c = *(p+1); + int p_len = p - p_start + 1; + + counter = 0; + + *(p+1) = 0; + if (!repo_get_oid(the_repository, p - (hexsz - 1), &oid)) { + struct object *o = + lookup_object(the_repository, &oid); + if (o) + name = get_rev_name(o, &buf); + } + *(p+1) = c; + + if (!name) + continue; + + if (data->name_only) + printf("%.*s%s", p_len - hexsz, p_start, name); + else + printf("%.*s (%s)", p_len, p_start, name); + p_start = p + 1; + } + } + + /* flush */ + if (p_start != p) + fwrite(p_start, p - p_start, 1, stdout); + + strbuf_release(&buf); +} + +int cmd_name_rev(int argc, const char **argv, const char *prefix) +{ + struct object_array revs = OBJECT_ARRAY_INIT; + int all = 0, annotate_stdin = 0, transform_stdin = 0, allow_undefined = 1, always = 0, peel_tag = 0; + struct name_ref_data data = { 0, 0, STRING_LIST_INIT_NODUP, STRING_LIST_INIT_NODUP }; + struct option opts[] = { + OPT_BOOL(0, "name-only", &data.name_only, N_("print only ref-based names (no object names)")), + OPT_BOOL(0, "tags", &data.tags_only, N_("only use tags to name the commits")), + OPT_STRING_LIST(0, "refs", &data.ref_filters, N_("pattern"), + N_("only use refs matching <pattern>")), + OPT_STRING_LIST(0, "exclude", &data.exclude_filters, N_("pattern"), + N_("ignore refs matching <pattern>")), + OPT_GROUP(""), + OPT_BOOL(0, "all", &all, N_("list all commits reachable from all refs")), + OPT_BOOL_F(0, + "stdin", + &transform_stdin, + N_("deprecated: use --annotate-stdin instead"), + PARSE_OPT_HIDDEN), + OPT_BOOL(0, "annotate-stdin", &annotate_stdin, N_("annotate text from stdin")), + OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print `undefined` names (default)")), + OPT_BOOL(0, "always", &always, + N_("show abbreviated commit object as fallback")), + OPT_HIDDEN_BOOL(0, "peel-tag", &peel_tag, + N_("dereference tags in the input (internal use)")), + OPT_END(), + }; + + init_commit_rev_name(&rev_names); + git_config(git_default_config, NULL); + argc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0); + + if (transform_stdin) { + warning("--stdin is deprecated. Please use --annotate-stdin instead, " + "which is functionally equivalent.\n" + "This option will be removed in a future release."); + annotate_stdin = 1; + } + + if (all + annotate_stdin + !!argc > 1) { + error("Specify either a list, or --all, not both!"); + usage_with_options(name_rev_usage, opts); + } + if (all || annotate_stdin) + disable_cutoff(); + + for (; argc; argc--, argv++) { + struct object_id oid; + struct object *object; + struct commit *commit; + + if (repo_get_oid(the_repository, *argv, &oid)) { + fprintf(stderr, "Could not get sha1 for %s. Skipping.\n", + *argv); + continue; + } + + commit = NULL; + object = parse_object(the_repository, &oid); + if (object) { + struct object *peeled = deref_tag(the_repository, + object, *argv, 0); + if (peeled && peeled->type == OBJ_COMMIT) + commit = (struct commit *)peeled; + } + + if (!object) { + fprintf(stderr, "Could not get object for %s. Skipping.\n", + *argv); + continue; + } + + if (commit) + set_commit_cutoff(commit); + + if (peel_tag) { + if (!commit) { + fprintf(stderr, "Could not get commit for %s. Skipping.\n", + *argv); + continue; + } + object = (struct object *)commit; + } + add_object_array(object, *argv, &revs); + } + + adjust_cutoff_timestamp_for_slop(); + + for_each_ref(name_ref, &data); + name_tips(); + + if (annotate_stdin) { + struct strbuf sb = STRBUF_INIT; + + while (strbuf_getline(&sb, stdin) != EOF) { + strbuf_addch(&sb, '\n'); + name_rev_line(sb.buf, &data); + } + strbuf_release(&sb); + } else if (all) { + int i, max; + + max = get_max_object_index(); + for (i = 0; i < max; i++) { + struct object *obj = get_indexed_object(i); + if (!obj || obj->type != OBJ_COMMIT) + continue; + show_name(obj, NULL, + always, allow_undefined, data.name_only); + } + } else { + int i; + for (i = 0; i < revs.nr; i++) + show_name(revs.objects[i].item, revs.objects[i].name, + always, allow_undefined, data.name_only); + } + + UNLEAK(revs); + return 0; +} diff --git a/builtin/notes.c b/builtin/notes.c new file mode 100644 index 0000000..9f38863 --- /dev/null +++ b/builtin/notes.c @@ -0,0 +1,1138 @@ +/* + * Builtin "git notes" + * + * Copyright (c) 2010 Johan Herland <johan@herland.net> + * + * Based on git-notes.sh by Johannes Schindelin, + * and builtin/tag.c by Kristian Høgsberg and Carlos Rica. + */ + +#include "builtin.h" +#include "config.h" +#include "alloc.h" +#include "editor.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "notes.h" +#include "object-name.h" +#include "object-store-ll.h" +#include "path.h" +#include "repository.h" +#include "blob.h" +#include "pretty.h" +#include "refs.h" +#include "exec-cmd.h" +#include "run-command.h" +#include "parse-options.h" +#include "string-list.h" +#include "notes-merge.h" +#include "notes-utils.h" +#include "worktree.h" +#include "write-or-die.h" + +static const char *separator = "\n"; +static const char * const git_notes_usage[] = { + N_("git notes [--ref <notes-ref>] [list [<object>]]"), + N_("git notes [--ref <notes-ref>] add [-f] [--allow-empty] [--[no-]separator|--separator=<paragraph-break>] [--[no-]stripspace] [-m <msg> | -F <file> | (-c | -C) <object>] [<object>]"), + N_("git notes [--ref <notes-ref>] copy [-f] <from-object> <to-object>"), + N_("git notes [--ref <notes-ref>] append [--allow-empty] [--[no-]separator|--separator=<paragraph-break>] [--[no-]stripspace] [-m <msg> | -F <file> | (-c | -C) <object>] [<object>]"), + N_("git notes [--ref <notes-ref>] edit [--allow-empty] [<object>]"), + N_("git notes [--ref <notes-ref>] show [<object>]"), + N_("git notes [--ref <notes-ref>] merge [-v | -q] [-s <strategy>] <notes-ref>"), + "git notes merge --commit [-v | -q]", + "git notes merge --abort [-v | -q]", + N_("git notes [--ref <notes-ref>] remove [<object>...]"), + N_("git notes [--ref <notes-ref>] prune [-n] [-v]"), + N_("git notes [--ref <notes-ref>] get-ref"), + NULL +}; + +static const char * const git_notes_list_usage[] = { + N_("git notes [list [<object>]]"), + NULL +}; + +static const char * const git_notes_add_usage[] = { + N_("git notes add [<options>] [<object>]"), + NULL +}; + +static const char * const git_notes_copy_usage[] = { + N_("git notes copy [<options>] <from-object> <to-object>"), + N_("git notes copy --stdin [<from-object> <to-object>]..."), + NULL +}; + +static const char * const git_notes_append_usage[] = { + N_("git notes append [<options>] [<object>]"), + NULL +}; + +static const char * const git_notes_edit_usage[] = { + N_("git notes edit [<object>]"), + NULL +}; + +static const char * const git_notes_show_usage[] = { + N_("git notes show [<object>]"), + NULL +}; + +static const char * const git_notes_merge_usage[] = { + N_("git notes merge [<options>] <notes-ref>"), + N_("git notes merge --commit [<options>]"), + N_("git notes merge --abort [<options>]"), + NULL +}; + +static const char * const git_notes_remove_usage[] = { + N_("git notes remove [<object>]"), + NULL +}; + +static const char * const git_notes_prune_usage[] = { + N_("git notes prune [<options>]"), + NULL +}; + +static const char * const git_notes_get_ref_usage[] = { + "git notes get-ref", + NULL +}; + +static const char note_template[] = + N_("Write/edit the notes for the following object:"); + +enum notes_stripspace { + UNSPECIFIED = -1, + NO_STRIPSPACE = 0, + STRIPSPACE = 1, +}; + +struct note_msg { + enum notes_stripspace stripspace; + struct strbuf buf; +}; + +struct note_data { + int given; + int use_editor; + int stripspace; + char *edit_path; + struct strbuf buf; + struct note_msg **messages; + size_t msg_nr; + size_t msg_alloc; +}; + +static void free_note_data(struct note_data *d) +{ + if (d->edit_path) { + unlink_or_warn(d->edit_path); + free(d->edit_path); + } + strbuf_release(&d->buf); + + while (d->msg_nr--) { + strbuf_release(&d->messages[d->msg_nr]->buf); + free(d->messages[d->msg_nr]); + } + free(d->messages); +} + +static int list_each_note(const struct object_id *object_oid, + const struct object_id *note_oid, + char *note_path UNUSED, + void *cb_data UNUSED) +{ + printf("%s %s\n", oid_to_hex(note_oid), oid_to_hex(object_oid)); + return 0; +} + +static void copy_obj_to_fd(int fd, const struct object_id *oid) +{ + unsigned long size; + enum object_type type; + char *buf = repo_read_object_file(the_repository, oid, &type, &size); + if (buf) { + if (size) + write_or_die(fd, buf, size); + free(buf); + } +} + +static void write_commented_object(int fd, const struct object_id *object) +{ + struct child_process show = CHILD_PROCESS_INIT; + struct strbuf buf = STRBUF_INIT; + struct strbuf cbuf = STRBUF_INIT; + + /* Invoke "git show --stat --no-notes $object" */ + strvec_pushl(&show.args, "show", "--stat", "--no-notes", + oid_to_hex(object), NULL); + show.no_stdin = 1; + show.out = -1; + show.err = 0; + show.git_cmd = 1; + if (start_command(&show)) + die(_("unable to start 'show' for object '%s'"), + oid_to_hex(object)); + + if (strbuf_read(&buf, show.out, 0) < 0) + die_errno(_("could not read 'show' output")); + strbuf_add_commented_lines(&cbuf, buf.buf, buf.len, comment_line_char); + write_or_die(fd, cbuf.buf, cbuf.len); + + strbuf_release(&cbuf); + strbuf_release(&buf); + + if (finish_command(&show)) + die(_("failed to finish 'show' for object '%s'"), + oid_to_hex(object)); +} + +static void prepare_note_data(const struct object_id *object, struct note_data *d, + const struct object_id *old_note) +{ + if (d->use_editor || !d->given) { + int fd; + struct strbuf buf = STRBUF_INIT; + + /* write the template message before editing: */ + d->edit_path = git_pathdup("NOTES_EDITMSG"); + fd = xopen(d->edit_path, O_CREAT | O_TRUNC | O_WRONLY, 0600); + + if (d->given) + write_or_die(fd, d->buf.buf, d->buf.len); + else if (old_note) + copy_obj_to_fd(fd, old_note); + + strbuf_addch(&buf, '\n'); + strbuf_add_commented_lines(&buf, "\n", strlen("\n"), comment_line_char); + strbuf_add_commented_lines(&buf, _(note_template), strlen(_(note_template)), + comment_line_char); + strbuf_add_commented_lines(&buf, "\n", strlen("\n"), comment_line_char); + write_or_die(fd, buf.buf, buf.len); + + write_commented_object(fd, object); + + close(fd); + strbuf_release(&buf); + strbuf_reset(&d->buf); + + if (launch_editor(d->edit_path, &d->buf, NULL)) { + die(_("please supply the note contents using either -m or -F option")); + } + if (d->stripspace) + strbuf_stripspace(&d->buf, comment_line_char); + } +} + +static void write_note_data(struct note_data *d, struct object_id *oid) +{ + if (write_object_file(d->buf.buf, d->buf.len, OBJ_BLOB, oid)) { + int status = die_message(_("unable to write note object")); + + if (d->edit_path) + die_message(_("the note contents have been left in %s"), + d->edit_path); + exit(status); + } +} + +static void append_separator(struct strbuf *message) +{ + size_t sep_len = 0; + + if (!separator) + return; + else if ((sep_len = strlen(separator)) && separator[sep_len - 1] == '\n') + strbuf_addstr(message, separator); + else + strbuf_addf(message, "%s%s", separator, "\n"); +} + +static void concat_messages(struct note_data *d) +{ + struct strbuf msg = STRBUF_INIT; + size_t i; + + for (i = 0; i < d->msg_nr ; i++) { + if (d->buf.len) + append_separator(&d->buf); + strbuf_add(&msg, d->messages[i]->buf.buf, d->messages[i]->buf.len); + strbuf_addbuf(&d->buf, &msg); + if ((d->stripspace == UNSPECIFIED && + d->messages[i]->stripspace == STRIPSPACE) || + d->stripspace == STRIPSPACE) + strbuf_stripspace(&d->buf, 0); + strbuf_reset(&msg); + } + strbuf_release(&msg); +} + +static int parse_msg_arg(const struct option *opt, const char *arg, int unset) +{ + struct note_data *d = opt->value; + struct note_msg *msg = xmalloc(sizeof(*msg)); + + BUG_ON_OPT_NEG(unset); + + strbuf_init(&msg->buf, strlen(arg)); + strbuf_addstr(&msg->buf, arg); + ALLOC_GROW_BY(d->messages, d->msg_nr, 1, d->msg_alloc); + d->messages[d->msg_nr - 1] = msg; + msg->stripspace = STRIPSPACE; + return 0; +} + +static int parse_file_arg(const struct option *opt, const char *arg, int unset) +{ + struct note_data *d = opt->value; + struct note_msg *msg = xmalloc(sizeof(*msg)); + + BUG_ON_OPT_NEG(unset); + + strbuf_init(&msg->buf , 0); + if (!strcmp(arg, "-")) { + if (strbuf_read(&msg->buf, 0, 1024) < 0) + die_errno(_("cannot read '%s'"), arg); + } else if (strbuf_read_file(&msg->buf, arg, 1024) < 0) + die_errno(_("could not open or read '%s'"), arg); + + ALLOC_GROW_BY(d->messages, d->msg_nr, 1, d->msg_alloc); + d->messages[d->msg_nr - 1] = msg; + msg->stripspace = STRIPSPACE; + return 0; +} + +static int parse_reuse_arg(const struct option *opt, const char *arg, int unset) +{ + struct note_data *d = opt->value; + struct note_msg *msg = xmalloc(sizeof(*msg)); + char *value; + struct object_id object; + enum object_type type; + unsigned long len; + + BUG_ON_OPT_NEG(unset); + + strbuf_init(&msg->buf, 0); + if (repo_get_oid(the_repository, arg, &object)) + die(_("failed to resolve '%s' as a valid ref."), arg); + if (!(value = repo_read_object_file(the_repository, &object, &type, &len))) + die(_("failed to read object '%s'."), arg); + if (type != OBJ_BLOB) { + strbuf_release(&msg->buf); + free(value); + free(msg); + die(_("cannot read note data from non-blob object '%s'."), arg); + } + + strbuf_add(&msg->buf, value, len); + free(value); + + msg->buf.len = len; + ALLOC_GROW_BY(d->messages, d->msg_nr, 1, d->msg_alloc); + d->messages[d->msg_nr - 1] = msg; + msg->stripspace = NO_STRIPSPACE; + return 0; +} + +static int parse_reedit_arg(const struct option *opt, const char *arg, int unset) +{ + struct note_data *d = opt->value; + BUG_ON_OPT_NEG(unset); + d->use_editor = 1; + return parse_reuse_arg(opt, arg, unset); +} + +static int parse_separator_arg(const struct option *opt, const char *arg, + int unset) +{ + if (unset) + *(const char **)opt->value = NULL; + else + *(const char **)opt->value = arg ? arg : "\n"; + return 0; +} + +static int notes_copy_from_stdin(int force, const char *rewrite_cmd) +{ + struct strbuf buf = STRBUF_INIT; + struct notes_rewrite_cfg *c = NULL; + struct notes_tree *t = NULL; + int ret = 0; + const char *msg = "Notes added by 'git notes copy'"; + + if (rewrite_cmd) { + c = init_copy_notes_for_rewrite(rewrite_cmd); + if (!c) + return 0; + } else { + init_notes(NULL, NULL, NULL, NOTES_INIT_WRITABLE); + t = &default_notes_tree; + } + + while (strbuf_getline_lf(&buf, stdin) != EOF) { + struct object_id from_obj, to_obj; + struct strbuf **split; + int err; + + split = strbuf_split(&buf, ' '); + if (!split[0] || !split[1]) + die(_("malformed input line: '%s'."), buf.buf); + strbuf_rtrim(split[0]); + strbuf_rtrim(split[1]); + if (repo_get_oid(the_repository, split[0]->buf, &from_obj)) + die(_("failed to resolve '%s' as a valid ref."), split[0]->buf); + if (repo_get_oid(the_repository, split[1]->buf, &to_obj)) + die(_("failed to resolve '%s' as a valid ref."), split[1]->buf); + + if (rewrite_cmd) + err = copy_note_for_rewrite(c, &from_obj, &to_obj); + else + err = copy_note(t, &from_obj, &to_obj, force, + combine_notes_overwrite); + + if (err) { + error(_("failed to copy notes from '%s' to '%s'"), + split[0]->buf, split[1]->buf); + ret = 1; + } + + strbuf_list_free(split); + } + + if (!rewrite_cmd) { + commit_notes(the_repository, t, msg); + free_notes(t); + } else { + finish_copy_notes_for_rewrite(the_repository, c, msg); + } + strbuf_release(&buf); + return ret; +} + +static struct notes_tree *init_notes_check(const char *subcommand, + int flags) +{ + struct notes_tree *t; + const char *ref; + init_notes(NULL, NULL, NULL, flags); + t = &default_notes_tree; + + ref = (flags & NOTES_INIT_WRITABLE) ? t->update_ref : t->ref; + if (!starts_with(ref, "refs/notes/")) + /* + * TRANSLATORS: the first %s will be replaced by a git + * notes command: 'add', 'merge', 'remove', etc. + */ + die(_("refusing to %s notes in %s (outside of refs/notes/)"), + subcommand, ref); + return t; +} + +static int list(int argc, const char **argv, const char *prefix) +{ + struct notes_tree *t; + struct object_id object; + const struct object_id *note; + int retval = -1; + struct option options[] = { + OPT_END() + }; + + if (argc) + argc = parse_options(argc, argv, prefix, options, + git_notes_list_usage, 0); + + if (1 < argc) { + error(_("too many arguments")); + usage_with_options(git_notes_list_usage, options); + } + + t = init_notes_check("list", 0); + if (argc) { + if (repo_get_oid(the_repository, argv[0], &object)) + die(_("failed to resolve '%s' as a valid ref."), argv[0]); + note = get_note(t, &object); + if (note) { + puts(oid_to_hex(note)); + retval = 0; + } else + retval = error(_("no note found for object %s."), + oid_to_hex(&object)); + } else + retval = for_each_note(t, 0, list_each_note, NULL); + + free_notes(t); + return retval; +} + +static int append_edit(int argc, const char **argv, const char *prefix); + +static int add(int argc, const char **argv, const char *prefix) +{ + int force = 0, allow_empty = 0; + const char *object_ref; + struct notes_tree *t; + struct object_id object, new_note; + const struct object_id *note; + struct note_data d = { .buf = STRBUF_INIT, .stripspace = UNSPECIFIED }; + + struct option options[] = { + OPT_CALLBACK_F('m', "message", &d, N_("message"), + N_("note contents as a string"), PARSE_OPT_NONEG, + parse_msg_arg), + OPT_CALLBACK_F('F', "file", &d, N_("file"), + N_("note contents in a file"), PARSE_OPT_NONEG, + parse_file_arg), + OPT_CALLBACK_F('c', "reedit-message", &d, N_("object"), + N_("reuse and edit specified note object"), PARSE_OPT_NONEG, + parse_reedit_arg), + OPT_CALLBACK_F('C', "reuse-message", &d, N_("object"), + N_("reuse specified note object"), PARSE_OPT_NONEG, + parse_reuse_arg), + OPT_BOOL(0, "allow-empty", &allow_empty, + N_("allow storing empty note")), + OPT__FORCE(&force, N_("replace existing notes"), PARSE_OPT_NOCOMPLETE), + OPT_CALLBACK_F(0, "separator", &separator, + N_("<paragraph-break>"), + N_("insert <paragraph-break> between paragraphs"), + PARSE_OPT_OPTARG, parse_separator_arg), + OPT_BOOL(0, "stripspace", &d.stripspace, + N_("remove unnecessary whitespace")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, git_notes_add_usage, + PARSE_OPT_KEEP_ARGV0); + + if (2 < argc) { + error(_("too many arguments")); + usage_with_options(git_notes_add_usage, options); + } + + if (d.msg_nr) + concat_messages(&d); + d.given = !!d.buf.len; + + object_ref = argc > 1 ? argv[1] : "HEAD"; + + if (repo_get_oid(the_repository, object_ref, &object)) + die(_("failed to resolve '%s' as a valid ref."), object_ref); + + t = init_notes_check("add", NOTES_INIT_WRITABLE); + note = get_note(t, &object); + + if (note) { + if (!force) { + free_notes(t); + if (d.given) { + free_note_data(&d); + return error(_("Cannot add notes. " + "Found existing notes for object %s. " + "Use '-f' to overwrite existing notes"), + oid_to_hex(&object)); + } + /* + * Redirect to "edit" subcommand. + * + * We only end up here if none of -m/-F/-c/-C or -f are + * given. The original args are therefore still in + * argv[0-1]. + */ + argv[0] = "edit"; + return append_edit(argc, argv, prefix); + } + fprintf(stderr, _("Overwriting existing notes for object %s\n"), + oid_to_hex(&object)); + } + + prepare_note_data(&object, &d, note); + if (d.buf.len || allow_empty) { + write_note_data(&d, &new_note); + if (add_note(t, &object, &new_note, combine_notes_overwrite)) + BUG("combine_notes_overwrite failed"); + commit_notes(the_repository, t, + "Notes added by 'git notes add'"); + } else { + fprintf(stderr, _("Removing note for object %s\n"), + oid_to_hex(&object)); + remove_note(t, object.hash); + commit_notes(the_repository, t, + "Notes removed by 'git notes add'"); + } + + free_note_data(&d); + free_notes(t); + return 0; +} + +static int copy(int argc, const char **argv, const char *prefix) +{ + int retval = 0, force = 0, from_stdin = 0; + const struct object_id *from_note, *note; + const char *object_ref; + struct object_id object, from_obj; + struct notes_tree *t; + const char *rewrite_cmd = NULL; + struct option options[] = { + OPT__FORCE(&force, N_("replace existing notes"), PARSE_OPT_NOCOMPLETE), + OPT_BOOL(0, "stdin", &from_stdin, N_("read objects from stdin")), + OPT_STRING(0, "for-rewrite", &rewrite_cmd, N_("command"), + N_("load rewriting config for <command> (implies " + "--stdin)")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, git_notes_copy_usage, + 0); + + if (from_stdin || rewrite_cmd) { + if (argc) { + error(_("too many arguments")); + usage_with_options(git_notes_copy_usage, options); + } else { + return notes_copy_from_stdin(force, rewrite_cmd); + } + } + + if (argc < 1) { + error(_("too few arguments")); + usage_with_options(git_notes_copy_usage, options); + } + if (2 < argc) { + error(_("too many arguments")); + usage_with_options(git_notes_copy_usage, options); + } + + if (repo_get_oid(the_repository, argv[0], &from_obj)) + die(_("failed to resolve '%s' as a valid ref."), argv[0]); + + object_ref = 1 < argc ? argv[1] : "HEAD"; + + if (repo_get_oid(the_repository, object_ref, &object)) + die(_("failed to resolve '%s' as a valid ref."), object_ref); + + t = init_notes_check("copy", NOTES_INIT_WRITABLE); + note = get_note(t, &object); + + if (note) { + if (!force) { + retval = error(_("Cannot copy notes. Found existing " + "notes for object %s. Use '-f' to " + "overwrite existing notes"), + oid_to_hex(&object)); + goto out; + } + fprintf(stderr, _("Overwriting existing notes for object %s\n"), + oid_to_hex(&object)); + } + + from_note = get_note(t, &from_obj); + if (!from_note) { + retval = error(_("missing notes on source object %s. Cannot " + "copy."), oid_to_hex(&from_obj)); + goto out; + } + + if (add_note(t, &object, from_note, combine_notes_overwrite)) + BUG("combine_notes_overwrite failed"); + commit_notes(the_repository, t, + "Notes added by 'git notes copy'"); +out: + free_notes(t); + return retval; +} + +static int append_edit(int argc, const char **argv, const char *prefix) +{ + int allow_empty = 0; + const char *object_ref; + struct notes_tree *t; + struct object_id object, new_note; + const struct object_id *note; + char *logmsg; + const char * const *usage; + struct note_data d = { .buf = STRBUF_INIT, .stripspace = UNSPECIFIED }; + struct option options[] = { + OPT_CALLBACK_F('m', "message", &d, N_("message"), + N_("note contents as a string"), PARSE_OPT_NONEG, + parse_msg_arg), + OPT_CALLBACK_F('F', "file", &d, N_("file"), + N_("note contents in a file"), PARSE_OPT_NONEG, + parse_file_arg), + OPT_CALLBACK_F('c', "reedit-message", &d, N_("object"), + N_("reuse and edit specified note object"), PARSE_OPT_NONEG, + parse_reedit_arg), + OPT_CALLBACK_F('C', "reuse-message", &d, N_("object"), + N_("reuse specified note object"), PARSE_OPT_NONEG, + parse_reuse_arg), + OPT_BOOL(0, "allow-empty", &allow_empty, + N_("allow storing empty note")), + OPT_CALLBACK_F(0, "separator", &separator, + N_("<paragraph-break>"), + N_("insert <paragraph-break> between paragraphs"), + PARSE_OPT_OPTARG, parse_separator_arg), + OPT_BOOL(0, "stripspace", &d.stripspace, + N_("remove unnecessary whitespace")), + OPT_END() + }; + int edit = !strcmp(argv[0], "edit"); + + usage = edit ? git_notes_edit_usage : git_notes_append_usage; + argc = parse_options(argc, argv, prefix, options, usage, + PARSE_OPT_KEEP_ARGV0); + + if (2 < argc) { + error(_("too many arguments")); + usage_with_options(usage, options); + } + + if (d.msg_nr) + concat_messages(&d); + d.given = !!d.buf.len; + + if (d.given && edit) + fprintf(stderr, _("The -m/-F/-c/-C options have been deprecated " + "for the 'edit' subcommand.\n" + "Please use 'git notes add -f -m/-F/-c/-C' instead.\n")); + + object_ref = 1 < argc ? argv[1] : "HEAD"; + + if (repo_get_oid(the_repository, object_ref, &object)) + die(_("failed to resolve '%s' as a valid ref."), object_ref); + + t = init_notes_check(argv[0], NOTES_INIT_WRITABLE); + note = get_note(t, &object); + + prepare_note_data(&object, &d, edit && note ? note : NULL); + + if (note && !edit) { + /* Append buf to previous note contents */ + unsigned long size; + enum object_type type; + struct strbuf buf = STRBUF_INIT; + char *prev_buf = repo_read_object_file(the_repository, note, &type, &size); + + if (prev_buf && size) + strbuf_add(&buf, prev_buf, size); + if (d.buf.len && prev_buf && size) + append_separator(&buf); + strbuf_insert(&d.buf, 0, buf.buf, buf.len); + + free(prev_buf); + strbuf_release(&buf); + } + + if (d.buf.len || allow_empty) { + write_note_data(&d, &new_note); + if (add_note(t, &object, &new_note, combine_notes_overwrite)) + BUG("combine_notes_overwrite failed"); + logmsg = xstrfmt("Notes added by 'git notes %s'", argv[0]); + } else { + fprintf(stderr, _("Removing note for object %s\n"), + oid_to_hex(&object)); + remove_note(t, object.hash); + logmsg = xstrfmt("Notes removed by 'git notes %s'", argv[0]); + } + commit_notes(the_repository, t, logmsg); + + free(logmsg); + free_note_data(&d); + free_notes(t); + return 0; +} + +static int show(int argc, const char **argv, const char *prefix) +{ + const char *object_ref; + struct notes_tree *t; + struct object_id object; + const struct object_id *note; + int retval; + struct option options[] = { + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, git_notes_show_usage, + 0); + + if (1 < argc) { + error(_("too many arguments")); + usage_with_options(git_notes_show_usage, options); + } + + object_ref = argc ? argv[0] : "HEAD"; + + if (repo_get_oid(the_repository, object_ref, &object)) + die(_("failed to resolve '%s' as a valid ref."), object_ref); + + t = init_notes_check("show", 0); + note = get_note(t, &object); + + if (!note) + retval = error(_("no note found for object %s."), + oid_to_hex(&object)); + else { + const char *show_args[3] = {"show", oid_to_hex(note), NULL}; + retval = execv_git_cmd(show_args); + } + free_notes(t); + return retval; +} + +static int merge_abort(struct notes_merge_options *o) +{ + int ret = 0; + + /* + * Remove .git/NOTES_MERGE_PARTIAL and .git/NOTES_MERGE_REF, and call + * notes_merge_abort() to remove .git/NOTES_MERGE_WORKTREE. + */ + + if (delete_ref(NULL, "NOTES_MERGE_PARTIAL", NULL, 0)) + ret += error(_("failed to delete ref NOTES_MERGE_PARTIAL")); + if (delete_ref(NULL, "NOTES_MERGE_REF", NULL, REF_NO_DEREF)) + ret += error(_("failed to delete ref NOTES_MERGE_REF")); + if (notes_merge_abort(o)) + ret += error(_("failed to remove 'git notes merge' worktree")); + return ret; +} + +static int merge_commit(struct notes_merge_options *o) +{ + struct strbuf msg = STRBUF_INIT; + struct object_id oid, parent_oid; + struct notes_tree *t; + struct commit *partial; + struct pretty_print_context pretty_ctx; + void *local_ref_to_free; + int ret; + + /* + * Read partial merge result from .git/NOTES_MERGE_PARTIAL, + * and target notes ref from .git/NOTES_MERGE_REF. + */ + + if (repo_get_oid(the_repository, "NOTES_MERGE_PARTIAL", &oid)) + die(_("failed to read ref NOTES_MERGE_PARTIAL")); + else if (!(partial = lookup_commit_reference(the_repository, &oid))) + die(_("could not find commit from NOTES_MERGE_PARTIAL.")); + else if (repo_parse_commit(the_repository, partial)) + die(_("could not parse commit from NOTES_MERGE_PARTIAL.")); + + if (partial->parents) + oidcpy(&parent_oid, &partial->parents->item->object.oid); + else + oidclr(&parent_oid); + + CALLOC_ARRAY(t, 1); + init_notes(t, "NOTES_MERGE_PARTIAL", combine_notes_overwrite, 0); + + o->local_ref = local_ref_to_free = + resolve_refdup("NOTES_MERGE_REF", 0, &oid, NULL); + if (!o->local_ref) + die(_("failed to resolve NOTES_MERGE_REF")); + + if (notes_merge_commit(o, t, partial, &oid)) + die(_("failed to finalize notes merge")); + + /* Reuse existing commit message in reflog message */ + memset(&pretty_ctx, 0, sizeof(pretty_ctx)); + repo_format_commit_message(the_repository, partial, "%s", &msg, + &pretty_ctx); + strbuf_trim(&msg); + strbuf_insertstr(&msg, 0, "notes: "); + update_ref(msg.buf, o->local_ref, &oid, + is_null_oid(&parent_oid) ? NULL : &parent_oid, + 0, UPDATE_REFS_DIE_ON_ERR); + + free_notes(t); + strbuf_release(&msg); + ret = merge_abort(o); + free(local_ref_to_free); + return ret; +} + +static int git_config_get_notes_strategy(const char *key, + enum notes_merge_strategy *strategy) +{ + char *value; + + if (git_config_get_string(key, &value)) + return 1; + if (parse_notes_merge_strategy(value, strategy)) + git_die_config(key, _("unknown notes merge strategy %s"), value); + + free(value); + return 0; +} + +static int merge(int argc, const char **argv, const char *prefix) +{ + struct strbuf remote_ref = STRBUF_INIT, msg = STRBUF_INIT; + struct object_id result_oid; + struct notes_tree *t; + struct notes_merge_options o; + int do_merge = 0, do_commit = 0, do_abort = 0; + int verbosity = 0, result; + const char *strategy = NULL; + struct option options[] = { + OPT_GROUP(N_("General options")), + OPT__VERBOSITY(&verbosity), + OPT_GROUP(N_("Merge options")), + OPT_STRING('s', "strategy", &strategy, N_("strategy"), + N_("resolve notes conflicts using the given strategy " + "(manual/ours/theirs/union/cat_sort_uniq)")), + OPT_GROUP(N_("Committing unmerged notes")), + OPT_SET_INT_F(0, "commit", &do_commit, + N_("finalize notes merge by committing unmerged notes"), + 1, PARSE_OPT_NONEG), + OPT_GROUP(N_("Aborting notes merge resolution")), + OPT_SET_INT_F(0, "abort", &do_abort, + N_("abort notes merge"), + 1, PARSE_OPT_NONEG), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, + git_notes_merge_usage, 0); + + if (strategy || do_commit + do_abort == 0) + do_merge = 1; + if (do_merge + do_commit + do_abort != 1) { + error(_("cannot mix --commit, --abort or -s/--strategy")); + usage_with_options(git_notes_merge_usage, options); + } + + if (do_merge && argc != 1) { + error(_("must specify a notes ref to merge")); + usage_with_options(git_notes_merge_usage, options); + } else if (!do_merge && argc) { + error(_("too many arguments")); + usage_with_options(git_notes_merge_usage, options); + } + + init_notes_merge_options(the_repository, &o); + o.verbosity = verbosity + NOTES_MERGE_VERBOSITY_DEFAULT; + + if (do_abort) + return merge_abort(&o); + if (do_commit) + return merge_commit(&o); + + o.local_ref = default_notes_ref(); + strbuf_addstr(&remote_ref, argv[0]); + expand_loose_notes_ref(&remote_ref); + o.remote_ref = remote_ref.buf; + + t = init_notes_check("merge", NOTES_INIT_WRITABLE); + + if (strategy) { + if (parse_notes_merge_strategy(strategy, &o.strategy)) { + error(_("unknown -s/--strategy: %s"), strategy); + usage_with_options(git_notes_merge_usage, options); + } + } else { + struct strbuf merge_key = STRBUF_INIT; + const char *short_ref = NULL; + + if (!skip_prefix(o.local_ref, "refs/notes/", &short_ref)) + BUG("local ref %s is outside of refs/notes/", + o.local_ref); + + strbuf_addf(&merge_key, "notes.%s.mergeStrategy", short_ref); + + if (git_config_get_notes_strategy(merge_key.buf, &o.strategy)) + git_config_get_notes_strategy("notes.mergeStrategy", &o.strategy); + + strbuf_release(&merge_key); + } + + strbuf_addf(&msg, "notes: Merged notes from %s into %s", + remote_ref.buf, default_notes_ref()); + strbuf_add(&(o.commit_msg), msg.buf + 7, msg.len - 7); /* skip "notes: " */ + + result = notes_merge(&o, t, &result_oid); + + if (result >= 0) /* Merge resulted (trivially) in result_oid */ + /* Update default notes ref with new commit */ + update_ref(msg.buf, default_notes_ref(), &result_oid, NULL, 0, + UPDATE_REFS_DIE_ON_ERR); + else { /* Merge has unresolved conflicts */ + struct worktree **worktrees; + const struct worktree *wt; + /* Update .git/NOTES_MERGE_PARTIAL with partial merge result */ + update_ref(msg.buf, "NOTES_MERGE_PARTIAL", &result_oid, NULL, + 0, UPDATE_REFS_DIE_ON_ERR); + /* Store ref-to-be-updated into .git/NOTES_MERGE_REF */ + worktrees = get_worktrees(); + wt = find_shared_symref(worktrees, "NOTES_MERGE_REF", + default_notes_ref()); + if (wt) + die(_("a notes merge into %s is already in-progress at %s"), + default_notes_ref(), wt->path); + free_worktrees(worktrees); + if (create_symref("NOTES_MERGE_REF", default_notes_ref(), NULL)) + die(_("failed to store link to current notes ref (%s)"), + default_notes_ref()); + fprintf(stderr, _("Automatic notes merge failed. Fix conflicts in %s " + "and commit the result with 'git notes merge --commit', " + "or abort the merge with 'git notes merge --abort'.\n"), + git_path(NOTES_MERGE_WORKTREE)); + } + + free_notes(t); + strbuf_release(&remote_ref); + strbuf_release(&msg); + return result < 0; /* return non-zero on conflicts */ +} + +#define IGNORE_MISSING 1 + +static int remove_one_note(struct notes_tree *t, const char *name, unsigned flag) +{ + int status; + struct object_id oid; + if (repo_get_oid(the_repository, name, &oid)) + return error(_("Failed to resolve '%s' as a valid ref."), name); + status = remove_note(t, oid.hash); + if (status) + fprintf(stderr, _("Object %s has no note\n"), name); + else + fprintf(stderr, _("Removing note for object %s\n"), name); + return (flag & IGNORE_MISSING) ? 0 : status; +} + +static int remove_cmd(int argc, const char **argv, const char *prefix) +{ + unsigned flag = 0; + int from_stdin = 0; + struct option options[] = { + OPT_BIT(0, "ignore-missing", &flag, + N_("attempt to remove non-existent note is not an error"), + IGNORE_MISSING), + OPT_BOOL(0, "stdin", &from_stdin, + N_("read object names from the standard input")), + OPT_END() + }; + struct notes_tree *t; + int retval = 0; + + argc = parse_options(argc, argv, prefix, options, + git_notes_remove_usage, 0); + + t = init_notes_check("remove", NOTES_INIT_WRITABLE); + + if (!argc && !from_stdin) { + retval = remove_one_note(t, "HEAD", flag); + } else { + while (*argv) { + retval |= remove_one_note(t, *argv, flag); + argv++; + } + } + if (from_stdin) { + struct strbuf sb = STRBUF_INIT; + while (strbuf_getwholeline(&sb, stdin, '\n') != EOF) { + strbuf_rtrim(&sb); + retval |= remove_one_note(t, sb.buf, flag); + } + strbuf_release(&sb); + } + if (!retval) + commit_notes(the_repository, t, + "Notes removed by 'git notes remove'"); + free_notes(t); + return retval; +} + +static int prune(int argc, const char **argv, const char *prefix) +{ + struct notes_tree *t; + int show_only = 0, verbose = 0; + struct option options[] = { + OPT__DRY_RUN(&show_only, N_("do not remove, show only")), + OPT__VERBOSE(&verbose, N_("report pruned notes")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, git_notes_prune_usage, + 0); + + if (argc) { + error(_("too many arguments")); + usage_with_options(git_notes_prune_usage, options); + } + + t = init_notes_check("prune", NOTES_INIT_WRITABLE); + + prune_notes(t, (verbose ? NOTES_PRUNE_VERBOSE : 0) | + (show_only ? NOTES_PRUNE_VERBOSE|NOTES_PRUNE_DRYRUN : 0) ); + if (!show_only) + commit_notes(the_repository, t, + "Notes removed by 'git notes prune'"); + free_notes(t); + return 0; +} + +static int get_ref(int argc, const char **argv, const char *prefix) +{ + struct option options[] = { OPT_END() }; + argc = parse_options(argc, argv, prefix, options, + git_notes_get_ref_usage, 0); + + if (argc) { + error(_("too many arguments")); + usage_with_options(git_notes_get_ref_usage, options); + } + + puts(default_notes_ref()); + return 0; +} + +int cmd_notes(int argc, const char **argv, const char *prefix) +{ + const char *override_notes_ref = NULL; + parse_opt_subcommand_fn *fn = NULL; + struct option options[] = { + OPT_STRING(0, "ref", &override_notes_ref, N_("notes-ref"), + N_("use notes from <notes-ref>")), + OPT_SUBCOMMAND("list", &fn, list), + OPT_SUBCOMMAND("add", &fn, add), + OPT_SUBCOMMAND("copy", &fn, copy), + OPT_SUBCOMMAND("append", &fn, append_edit), + OPT_SUBCOMMAND("edit", &fn, append_edit), + OPT_SUBCOMMAND("show", &fn, show), + OPT_SUBCOMMAND("merge", &fn, merge), + OPT_SUBCOMMAND("remove", &fn, remove_cmd), + OPT_SUBCOMMAND("prune", &fn, prune), + OPT_SUBCOMMAND("get-ref", &fn, get_ref), + OPT_END() + }; + + git_config(git_default_config, NULL); + argc = parse_options(argc, argv, prefix, options, git_notes_usage, + PARSE_OPT_SUBCOMMAND_OPTIONAL); + if (!fn) { + if (argc) { + error(_("unknown subcommand: `%s'"), argv[0]); + usage_with_options(git_notes_usage, options); + } + fn = list; + } + + if (override_notes_ref) { + struct strbuf sb = STRBUF_INIT; + strbuf_addstr(&sb, override_notes_ref); + expand_notes_ref(&sb); + setenv("GIT_NOTES_REF", sb.buf, 1); + strbuf_release(&sb); + } + + return !!fn(argc, argv, prefix); +} diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c new file mode 100644 index 0000000..89a8b5a --- /dev/null +++ b/builtin/pack-objects.c @@ -0,0 +1,4529 @@ +#include "builtin.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "repository.h" +#include "config.h" +#include "attr.h" +#include "object.h" +#include "blob.h" +#include "commit.h" +#include "tag.h" +#include "tree.h" +#include "delta.h" +#include "pack.h" +#include "pack-revindex.h" +#include "csum-file.h" +#include "tree-walk.h" +#include "diff.h" +#include "revision.h" +#include "list-objects.h" +#include "list-objects-filter.h" +#include "list-objects-filter-options.h" +#include "pack-objects.h" +#include "progress.h" +#include "refs.h" +#include "streaming.h" +#include "thread-utils.h" +#include "pack-bitmap.h" +#include "delta-islands.h" +#include "reachable.h" +#include "oid-array.h" +#include "strvec.h" +#include "list.h" +#include "packfile.h" +#include "object-file.h" +#include "object-store-ll.h" +#include "replace-object.h" +#include "dir.h" +#include "midx.h" +#include "trace2.h" +#include "shallow.h" +#include "promisor-remote.h" +#include "pack-mtimes.h" +#include "parse-options.h" + +/* + * Objects we are going to pack are collected in the `to_pack` structure. + * It contains an array (dynamically expanded) of the object data, and a map + * that can resolve SHA1s to their position in the array. + */ +static struct packing_data to_pack; + +static inline struct object_entry *oe_delta( + const struct packing_data *pack, + const struct object_entry *e) +{ + if (!e->delta_idx) + return NULL; + if (e->ext_base) + return &pack->ext_bases[e->delta_idx - 1]; + else + return &pack->objects[e->delta_idx - 1]; +} + +static inline unsigned long oe_delta_size(struct packing_data *pack, + const struct object_entry *e) +{ + if (e->delta_size_valid) + return e->delta_size_; + + /* + * pack->delta_size[] can't be NULL because oe_set_delta_size() + * must have been called when a new delta is saved with + * oe_set_delta(). + * If oe_delta() returns NULL (i.e. default state, which means + * delta_size_valid is also false), then the caller must never + * call oe_delta_size(). + */ + return pack->delta_size[e - pack->objects]; +} + +unsigned long oe_get_size_slow(struct packing_data *pack, + const struct object_entry *e); + +static inline unsigned long oe_size(struct packing_data *pack, + const struct object_entry *e) +{ + if (e->size_valid) + return e->size_; + + return oe_get_size_slow(pack, e); +} + +static inline void oe_set_delta(struct packing_data *pack, + struct object_entry *e, + struct object_entry *delta) +{ + if (delta) + e->delta_idx = (delta - pack->objects) + 1; + else + e->delta_idx = 0; +} + +static inline struct object_entry *oe_delta_sibling( + const struct packing_data *pack, + const struct object_entry *e) +{ + if (e->delta_sibling_idx) + return &pack->objects[e->delta_sibling_idx - 1]; + return NULL; +} + +static inline struct object_entry *oe_delta_child( + const struct packing_data *pack, + const struct object_entry *e) +{ + if (e->delta_child_idx) + return &pack->objects[e->delta_child_idx - 1]; + return NULL; +} + +static inline void oe_set_delta_child(struct packing_data *pack, + struct object_entry *e, + struct object_entry *delta) +{ + if (delta) + e->delta_child_idx = (delta - pack->objects) + 1; + else + e->delta_child_idx = 0; +} + +static inline void oe_set_delta_sibling(struct packing_data *pack, + struct object_entry *e, + struct object_entry *delta) +{ + if (delta) + e->delta_sibling_idx = (delta - pack->objects) + 1; + else + e->delta_sibling_idx = 0; +} + +static inline void oe_set_size(struct packing_data *pack, + struct object_entry *e, + unsigned long size) +{ + if (size < pack->oe_size_limit) { + e->size_ = size; + e->size_valid = 1; + } else { + e->size_valid = 0; + if (oe_get_size_slow(pack, e) != size) + BUG("'size' is supposed to be the object size!"); + } +} + +static inline void oe_set_delta_size(struct packing_data *pack, + struct object_entry *e, + unsigned long size) +{ + if (size < pack->oe_delta_size_limit) { + e->delta_size_ = size; + e->delta_size_valid = 1; + } else { + packing_data_lock(pack); + if (!pack->delta_size) + ALLOC_ARRAY(pack->delta_size, pack->nr_alloc); + packing_data_unlock(pack); + + pack->delta_size[e - pack->objects] = size; + e->delta_size_valid = 0; + } +} + +#define IN_PACK(obj) oe_in_pack(&to_pack, obj) +#define SIZE(obj) oe_size(&to_pack, obj) +#define SET_SIZE(obj,size) oe_set_size(&to_pack, obj, size) +#define DELTA_SIZE(obj) oe_delta_size(&to_pack, obj) +#define DELTA(obj) oe_delta(&to_pack, obj) +#define DELTA_CHILD(obj) oe_delta_child(&to_pack, obj) +#define DELTA_SIBLING(obj) oe_delta_sibling(&to_pack, obj) +#define SET_DELTA(obj, val) oe_set_delta(&to_pack, obj, val) +#define SET_DELTA_EXT(obj, oid) oe_set_delta_ext(&to_pack, obj, oid) +#define SET_DELTA_SIZE(obj, val) oe_set_delta_size(&to_pack, obj, val) +#define SET_DELTA_CHILD(obj, val) oe_set_delta_child(&to_pack, obj, val) +#define SET_DELTA_SIBLING(obj, val) oe_set_delta_sibling(&to_pack, obj, val) + +static const char *pack_usage[] = { + N_("git pack-objects --stdout [<options>] [< <ref-list> | < <object-list>]"), + N_("git pack-objects [<options>] <base-name> [< <ref-list> | < <object-list>]"), + NULL +}; + +static struct pack_idx_entry **written_list; +static uint32_t nr_result, nr_written, nr_seen; +static struct bitmap_index *bitmap_git; +static uint32_t write_layer; + +static int non_empty; +static int reuse_delta = 1, reuse_object = 1; +static int keep_unreachable, unpack_unreachable, include_tag; +static timestamp_t unpack_unreachable_expiration; +static int pack_loose_unreachable; +static int cruft; +static timestamp_t cruft_expiration; +static int local; +static int have_non_local_packs; +static int incremental; +static int ignore_packed_keep_on_disk; +static int ignore_packed_keep_in_core; +static int allow_ofs_delta; +static struct pack_idx_option pack_idx_opts; +static const char *base_name; +static int progress = 1; +static int window = 10; +static unsigned long pack_size_limit; +static int depth = 50; +static int delta_search_threads; +static int pack_to_stdout; +static int sparse; +static int thin; +static int num_preferred_base; +static struct progress *progress_state; + +static struct packed_git *reuse_packfile; +static uint32_t reuse_packfile_objects; +static struct bitmap *reuse_packfile_bitmap; + +static int use_bitmap_index_default = 1; +static int use_bitmap_index = -1; +static int allow_pack_reuse = 1; +static enum { + WRITE_BITMAP_FALSE = 0, + WRITE_BITMAP_QUIET, + WRITE_BITMAP_TRUE, +} write_bitmap_index; +static uint16_t write_bitmap_options = BITMAP_OPT_HASH_CACHE; + +static int exclude_promisor_objects; + +static int use_delta_islands; + +static unsigned long delta_cache_size = 0; +static unsigned long max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE; +static unsigned long cache_max_small_delta_size = 1000; + +static unsigned long window_memory_limit = 0; + +static struct string_list uri_protocols = STRING_LIST_INIT_NODUP; + +enum missing_action { + MA_ERROR = 0, /* fail if any missing objects are encountered */ + MA_ALLOW_ANY, /* silently allow ALL missing objects */ + MA_ALLOW_PROMISOR, /* silently allow all missing PROMISOR objects */ +}; +static enum missing_action arg_missing_action; +static show_object_fn fn_show_object; + +struct configured_exclusion { + struct oidmap_entry e; + char *pack_hash_hex; + char *uri; +}; +static struct oidmap configured_exclusions; + +static struct oidset excluded_by_config; + +/* + * stats + */ +static uint32_t written, written_delta; +static uint32_t reused, reused_delta; + +/* + * Indexed commits + */ +static struct commit **indexed_commits; +static unsigned int indexed_commits_nr; +static unsigned int indexed_commits_alloc; + +static void index_commit_for_bitmap(struct commit *commit) +{ + if (indexed_commits_nr >= indexed_commits_alloc) { + indexed_commits_alloc = (indexed_commits_alloc + 32) * 2; + REALLOC_ARRAY(indexed_commits, indexed_commits_alloc); + } + + indexed_commits[indexed_commits_nr++] = commit; +} + +static void *get_delta(struct object_entry *entry) +{ + unsigned long size, base_size, delta_size; + void *buf, *base_buf, *delta_buf; + enum object_type type; + + buf = repo_read_object_file(the_repository, &entry->idx.oid, &type, + &size); + if (!buf) + die(_("unable to read %s"), oid_to_hex(&entry->idx.oid)); + base_buf = repo_read_object_file(the_repository, + &DELTA(entry)->idx.oid, &type, + &base_size); + if (!base_buf) + die("unable to read %s", + oid_to_hex(&DELTA(entry)->idx.oid)); + delta_buf = diff_delta(base_buf, base_size, + buf, size, &delta_size, 0); + /* + * We successfully computed this delta once but dropped it for + * memory reasons. Something is very wrong if this time we + * recompute and create a different delta. + */ + if (!delta_buf || delta_size != DELTA_SIZE(entry)) + BUG("delta size changed"); + free(buf); + free(base_buf); + return delta_buf; +} + +static unsigned long do_compress(void **pptr, unsigned long size) +{ + git_zstream stream; + void *in, *out; + unsigned long maxsize; + + git_deflate_init(&stream, pack_compression_level); + maxsize = git_deflate_bound(&stream, size); + + in = *pptr; + out = xmalloc(maxsize); + *pptr = out; + + stream.next_in = in; + stream.avail_in = size; + stream.next_out = out; + stream.avail_out = maxsize; + while (git_deflate(&stream, Z_FINISH) == Z_OK) + ; /* nothing */ + git_deflate_end(&stream); + + free(in); + return stream.total_out; +} + +static unsigned long write_large_blob_data(struct git_istream *st, struct hashfile *f, + const struct object_id *oid) +{ + git_zstream stream; + unsigned char ibuf[1024 * 16]; + unsigned char obuf[1024 * 16]; + unsigned long olen = 0; + + git_deflate_init(&stream, pack_compression_level); + + for (;;) { + ssize_t readlen; + int zret = Z_OK; + readlen = read_istream(st, ibuf, sizeof(ibuf)); + if (readlen == -1) + die(_("unable to read %s"), oid_to_hex(oid)); + + stream.next_in = ibuf; + stream.avail_in = readlen; + while ((stream.avail_in || readlen == 0) && + (zret == Z_OK || zret == Z_BUF_ERROR)) { + stream.next_out = obuf; + stream.avail_out = sizeof(obuf); + zret = git_deflate(&stream, readlen ? 0 : Z_FINISH); + hashwrite(f, obuf, stream.next_out - obuf); + olen += stream.next_out - obuf; + } + if (stream.avail_in) + die(_("deflate error (%d)"), zret); + if (readlen == 0) { + if (zret != Z_STREAM_END) + die(_("deflate error (%d)"), zret); + break; + } + } + git_deflate_end(&stream); + return olen; +} + +/* + * we are going to reuse the existing object data as is. make + * sure it is not corrupt. + */ +static int check_pack_inflate(struct packed_git *p, + struct pack_window **w_curs, + off_t offset, + off_t len, + unsigned long expect) +{ + git_zstream stream; + unsigned char fakebuf[4096], *in; + int st; + + memset(&stream, 0, sizeof(stream)); + git_inflate_init(&stream); + do { + in = use_pack(p, w_curs, offset, &stream.avail_in); + stream.next_in = in; + stream.next_out = fakebuf; + stream.avail_out = sizeof(fakebuf); + st = git_inflate(&stream, Z_FINISH); + offset += stream.next_in - in; + } while (st == Z_OK || st == Z_BUF_ERROR); + git_inflate_end(&stream); + return (st == Z_STREAM_END && + stream.total_out == expect && + stream.total_in == len) ? 0 : -1; +} + +static void copy_pack_data(struct hashfile *f, + struct packed_git *p, + struct pack_window **w_curs, + off_t offset, + off_t len) +{ + unsigned char *in; + unsigned long avail; + + while (len) { + in = use_pack(p, w_curs, offset, &avail); + if (avail > len) + avail = (unsigned long)len; + hashwrite(f, in, avail); + offset += avail; + len -= avail; + } +} + +static inline int oe_size_greater_than(struct packing_data *pack, + const struct object_entry *lhs, + unsigned long rhs) +{ + if (lhs->size_valid) + return lhs->size_ > rhs; + if (rhs < pack->oe_size_limit) /* rhs < 2^x <= lhs ? */ + return 1; + return oe_get_size_slow(pack, lhs) > rhs; +} + +/* Return 0 if we will bust the pack-size limit */ +static unsigned long write_no_reuse_object(struct hashfile *f, struct object_entry *entry, + unsigned long limit, int usable_delta) +{ + unsigned long size, datalen; + unsigned char header[MAX_PACK_OBJECT_HEADER], + dheader[MAX_PACK_OBJECT_HEADER]; + unsigned hdrlen; + enum object_type type; + void *buf; + struct git_istream *st = NULL; + const unsigned hashsz = the_hash_algo->rawsz; + + if (!usable_delta) { + if (oe_type(entry) == OBJ_BLOB && + oe_size_greater_than(&to_pack, entry, big_file_threshold) && + (st = open_istream(the_repository, &entry->idx.oid, &type, + &size, NULL)) != NULL) + buf = NULL; + else { + buf = repo_read_object_file(the_repository, + &entry->idx.oid, &type, + &size); + if (!buf) + die(_("unable to read %s"), + oid_to_hex(&entry->idx.oid)); + } + /* + * make sure no cached delta data remains from a + * previous attempt before a pack split occurred. + */ + FREE_AND_NULL(entry->delta_data); + entry->z_delta_size = 0; + } else if (entry->delta_data) { + size = DELTA_SIZE(entry); + buf = entry->delta_data; + entry->delta_data = NULL; + type = (allow_ofs_delta && DELTA(entry)->idx.offset) ? + OBJ_OFS_DELTA : OBJ_REF_DELTA; + } else { + buf = get_delta(entry); + size = DELTA_SIZE(entry); + type = (allow_ofs_delta && DELTA(entry)->idx.offset) ? + OBJ_OFS_DELTA : OBJ_REF_DELTA; + } + + if (st) /* large blob case, just assume we don't compress well */ + datalen = size; + else if (entry->z_delta_size) + datalen = entry->z_delta_size; + else + datalen = do_compress(&buf, size); + + /* + * The object header is a byte of 'type' followed by zero or + * more bytes of length. + */ + hdrlen = encode_in_pack_object_header(header, sizeof(header), + type, size); + + if (type == OBJ_OFS_DELTA) { + /* + * Deltas with relative base contain an additional + * encoding of the relative offset for the delta + * base from this object's position in the pack. + */ + off_t ofs = entry->idx.offset - DELTA(entry)->idx.offset; + unsigned pos = sizeof(dheader) - 1; + dheader[pos] = ofs & 127; + while (ofs >>= 7) + dheader[--pos] = 128 | (--ofs & 127); + if (limit && hdrlen + sizeof(dheader) - pos + datalen + hashsz >= limit) { + if (st) + close_istream(st); + free(buf); + return 0; + } + hashwrite(f, header, hdrlen); + hashwrite(f, dheader + pos, sizeof(dheader) - pos); + hdrlen += sizeof(dheader) - pos; + } else if (type == OBJ_REF_DELTA) { + /* + * Deltas with a base reference contain + * additional bytes for the base object ID. + */ + if (limit && hdrlen + hashsz + datalen + hashsz >= limit) { + if (st) + close_istream(st); + free(buf); + return 0; + } + hashwrite(f, header, hdrlen); + hashwrite(f, DELTA(entry)->idx.oid.hash, hashsz); + hdrlen += hashsz; + } else { + if (limit && hdrlen + datalen + hashsz >= limit) { + if (st) + close_istream(st); + free(buf); + return 0; + } + hashwrite(f, header, hdrlen); + } + if (st) { + datalen = write_large_blob_data(st, f, &entry->idx.oid); + close_istream(st); + } else { + hashwrite(f, buf, datalen); + free(buf); + } + + return hdrlen + datalen; +} + +/* Return 0 if we will bust the pack-size limit */ +static off_t write_reuse_object(struct hashfile *f, struct object_entry *entry, + unsigned long limit, int usable_delta) +{ + struct packed_git *p = IN_PACK(entry); + struct pack_window *w_curs = NULL; + uint32_t pos; + off_t offset; + enum object_type type = oe_type(entry); + off_t datalen; + unsigned char header[MAX_PACK_OBJECT_HEADER], + dheader[MAX_PACK_OBJECT_HEADER]; + unsigned hdrlen; + const unsigned hashsz = the_hash_algo->rawsz; + unsigned long entry_size = SIZE(entry); + + if (DELTA(entry)) + type = (allow_ofs_delta && DELTA(entry)->idx.offset) ? + OBJ_OFS_DELTA : OBJ_REF_DELTA; + hdrlen = encode_in_pack_object_header(header, sizeof(header), + type, entry_size); + + offset = entry->in_pack_offset; + if (offset_to_pack_pos(p, offset, &pos) < 0) + die(_("write_reuse_object: could not locate %s, expected at " + "offset %"PRIuMAX" in pack %s"), + oid_to_hex(&entry->idx.oid), (uintmax_t)offset, + p->pack_name); + datalen = pack_pos_to_offset(p, pos + 1) - offset; + if (!pack_to_stdout && p->index_version > 1 && + check_pack_crc(p, &w_curs, offset, datalen, + pack_pos_to_index(p, pos))) { + error(_("bad packed object CRC for %s"), + oid_to_hex(&entry->idx.oid)); + unuse_pack(&w_curs); + return write_no_reuse_object(f, entry, limit, usable_delta); + } + + offset += entry->in_pack_header_size; + datalen -= entry->in_pack_header_size; + + if (!pack_to_stdout && p->index_version == 1 && + check_pack_inflate(p, &w_curs, offset, datalen, entry_size)) { + error(_("corrupt packed object for %s"), + oid_to_hex(&entry->idx.oid)); + unuse_pack(&w_curs); + return write_no_reuse_object(f, entry, limit, usable_delta); + } + + if (type == OBJ_OFS_DELTA) { + off_t ofs = entry->idx.offset - DELTA(entry)->idx.offset; + unsigned pos = sizeof(dheader) - 1; + dheader[pos] = ofs & 127; + while (ofs >>= 7) + dheader[--pos] = 128 | (--ofs & 127); + if (limit && hdrlen + sizeof(dheader) - pos + datalen + hashsz >= limit) { + unuse_pack(&w_curs); + return 0; + } + hashwrite(f, header, hdrlen); + hashwrite(f, dheader + pos, sizeof(dheader) - pos); + hdrlen += sizeof(dheader) - pos; + reused_delta++; + } else if (type == OBJ_REF_DELTA) { + if (limit && hdrlen + hashsz + datalen + hashsz >= limit) { + unuse_pack(&w_curs); + return 0; + } + hashwrite(f, header, hdrlen); + hashwrite(f, DELTA(entry)->idx.oid.hash, hashsz); + hdrlen += hashsz; + reused_delta++; + } else { + if (limit && hdrlen + datalen + hashsz >= limit) { + unuse_pack(&w_curs); + return 0; + } + hashwrite(f, header, hdrlen); + } + copy_pack_data(f, p, &w_curs, offset, datalen); + unuse_pack(&w_curs); + reused++; + return hdrlen + datalen; +} + +/* Return 0 if we will bust the pack-size limit */ +static off_t write_object(struct hashfile *f, + struct object_entry *entry, + off_t write_offset) +{ + unsigned long limit; + off_t len; + int usable_delta, to_reuse; + + if (!pack_to_stdout) + crc32_begin(f); + + /* apply size limit if limited packsize and not first object */ + if (!pack_size_limit || !nr_written) + limit = 0; + else if (pack_size_limit <= write_offset) + /* + * the earlier object did not fit the limit; avoid + * mistaking this with unlimited (i.e. limit = 0). + */ + limit = 1; + else + limit = pack_size_limit - write_offset; + + if (!DELTA(entry)) + usable_delta = 0; /* no delta */ + else if (!pack_size_limit) + usable_delta = 1; /* unlimited packfile */ + else if (DELTA(entry)->idx.offset == (off_t)-1) + usable_delta = 0; /* base was written to another pack */ + else if (DELTA(entry)->idx.offset) + usable_delta = 1; /* base already exists in this pack */ + else + usable_delta = 0; /* base could end up in another pack */ + + if (!reuse_object) + to_reuse = 0; /* explicit */ + else if (!IN_PACK(entry)) + to_reuse = 0; /* can't reuse what we don't have */ + else if (oe_type(entry) == OBJ_REF_DELTA || + oe_type(entry) == OBJ_OFS_DELTA) + /* check_object() decided it for us ... */ + to_reuse = usable_delta; + /* ... but pack split may override that */ + else if (oe_type(entry) != entry->in_pack_type) + to_reuse = 0; /* pack has delta which is unusable */ + else if (DELTA(entry)) + to_reuse = 0; /* we want to pack afresh */ + else + to_reuse = 1; /* we have it in-pack undeltified, + * and we do not need to deltify it. + */ + + if (!to_reuse) + len = write_no_reuse_object(f, entry, limit, usable_delta); + else + len = write_reuse_object(f, entry, limit, usable_delta); + if (!len) + return 0; + + if (usable_delta) + written_delta++; + written++; + if (!pack_to_stdout) + entry->idx.crc32 = crc32_end(f); + return len; +} + +enum write_one_status { + WRITE_ONE_SKIP = -1, /* already written */ + WRITE_ONE_BREAK = 0, /* writing this will bust the limit; not written */ + WRITE_ONE_WRITTEN = 1, /* normal */ + WRITE_ONE_RECURSIVE = 2 /* already scheduled to be written */ +}; + +static enum write_one_status write_one(struct hashfile *f, + struct object_entry *e, + off_t *offset) +{ + off_t size; + int recursing; + + /* + * we set offset to 1 (which is an impossible value) to mark + * the fact that this object is involved in "write its base + * first before writing a deltified object" recursion. + */ + recursing = (e->idx.offset == 1); + if (recursing) { + warning(_("recursive delta detected for object %s"), + oid_to_hex(&e->idx.oid)); + return WRITE_ONE_RECURSIVE; + } else if (e->idx.offset || e->preferred_base) { + /* offset is non zero if object is written already. */ + return WRITE_ONE_SKIP; + } + + /* if we are deltified, write out base object first. */ + if (DELTA(e)) { + e->idx.offset = 1; /* now recurse */ + switch (write_one(f, DELTA(e), offset)) { + case WRITE_ONE_RECURSIVE: + /* we cannot depend on this one */ + SET_DELTA(e, NULL); + break; + default: + break; + case WRITE_ONE_BREAK: + e->idx.offset = recursing; + return WRITE_ONE_BREAK; + } + } + + e->idx.offset = *offset; + size = write_object(f, e, *offset); + if (!size) { + e->idx.offset = recursing; + return WRITE_ONE_BREAK; + } + written_list[nr_written++] = &e->idx; + + /* make sure off_t is sufficiently large not to wrap */ + if (signed_add_overflows(*offset, size)) + die(_("pack too large for current definition of off_t")); + *offset += size; + return WRITE_ONE_WRITTEN; +} + +static int mark_tagged(const char *path UNUSED, const struct object_id *oid, + int flag UNUSED, void *cb_data UNUSED) +{ + struct object_id peeled; + struct object_entry *entry = packlist_find(&to_pack, oid); + + if (entry) + entry->tagged = 1; + if (!peel_iterated_oid(oid, &peeled)) { + entry = packlist_find(&to_pack, &peeled); + if (entry) + entry->tagged = 1; + } + return 0; +} + +static inline unsigned char oe_layer(struct packing_data *pack, + struct object_entry *e) +{ + if (!pack->layer) + return 0; + return pack->layer[e - pack->objects]; +} + +static inline void add_to_write_order(struct object_entry **wo, + unsigned int *endp, + struct object_entry *e) +{ + if (e->filled || oe_layer(&to_pack, e) != write_layer) + return; + wo[(*endp)++] = e; + e->filled = 1; +} + +static void add_descendants_to_write_order(struct object_entry **wo, + unsigned int *endp, + struct object_entry *e) +{ + int add_to_order = 1; + while (e) { + if (add_to_order) { + struct object_entry *s; + /* add this node... */ + add_to_write_order(wo, endp, e); + /* all its siblings... */ + for (s = DELTA_SIBLING(e); s; s = DELTA_SIBLING(s)) { + add_to_write_order(wo, endp, s); + } + } + /* drop down a level to add left subtree nodes if possible */ + if (DELTA_CHILD(e)) { + add_to_order = 1; + e = DELTA_CHILD(e); + } else { + add_to_order = 0; + /* our sibling might have some children, it is next */ + if (DELTA_SIBLING(e)) { + e = DELTA_SIBLING(e); + continue; + } + /* go back to our parent node */ + e = DELTA(e); + while (e && !DELTA_SIBLING(e)) { + /* we're on the right side of a subtree, keep + * going up until we can go right again */ + e = DELTA(e); + } + if (!e) { + /* done- we hit our original root node */ + return; + } + /* pass it off to sibling at this level */ + e = DELTA_SIBLING(e); + } + }; +} + +static void add_family_to_write_order(struct object_entry **wo, + unsigned int *endp, + struct object_entry *e) +{ + struct object_entry *root; + + for (root = e; DELTA(root); root = DELTA(root)) + ; /* nothing */ + add_descendants_to_write_order(wo, endp, root); +} + +static void compute_layer_order(struct object_entry **wo, unsigned int *wo_end) +{ + unsigned int i, last_untagged; + struct object_entry *objects = to_pack.objects; + + for (i = 0; i < to_pack.nr_objects; i++) { + if (objects[i].tagged) + break; + add_to_write_order(wo, wo_end, &objects[i]); + } + last_untagged = i; + + /* + * Then fill all the tagged tips. + */ + for (; i < to_pack.nr_objects; i++) { + if (objects[i].tagged) + add_to_write_order(wo, wo_end, &objects[i]); + } + + /* + * And then all remaining commits and tags. + */ + for (i = last_untagged; i < to_pack.nr_objects; i++) { + if (oe_type(&objects[i]) != OBJ_COMMIT && + oe_type(&objects[i]) != OBJ_TAG) + continue; + add_to_write_order(wo, wo_end, &objects[i]); + } + + /* + * And then all the trees. + */ + for (i = last_untagged; i < to_pack.nr_objects; i++) { + if (oe_type(&objects[i]) != OBJ_TREE) + continue; + add_to_write_order(wo, wo_end, &objects[i]); + } + + /* + * Finally all the rest in really tight order + */ + for (i = last_untagged; i < to_pack.nr_objects; i++) { + if (!objects[i].filled && oe_layer(&to_pack, &objects[i]) == write_layer) + add_family_to_write_order(wo, wo_end, &objects[i]); + } +} + +static struct object_entry **compute_write_order(void) +{ + uint32_t max_layers = 1; + unsigned int i, wo_end; + + struct object_entry **wo; + struct object_entry *objects = to_pack.objects; + + for (i = 0; i < to_pack.nr_objects; i++) { + objects[i].tagged = 0; + objects[i].filled = 0; + SET_DELTA_CHILD(&objects[i], NULL); + SET_DELTA_SIBLING(&objects[i], NULL); + } + + /* + * Fully connect delta_child/delta_sibling network. + * Make sure delta_sibling is sorted in the original + * recency order. + */ + for (i = to_pack.nr_objects; i > 0;) { + struct object_entry *e = &objects[--i]; + if (!DELTA(e)) + continue; + /* Mark me as the first child */ + e->delta_sibling_idx = DELTA(e)->delta_child_idx; + SET_DELTA_CHILD(DELTA(e), e); + } + + /* + * Mark objects that are at the tip of tags. + */ + for_each_tag_ref(mark_tagged, NULL); + + if (use_delta_islands) { + max_layers = compute_pack_layers(&to_pack); + free_island_marks(); + } + + ALLOC_ARRAY(wo, to_pack.nr_objects); + wo_end = 0; + + for (; write_layer < max_layers; ++write_layer) + compute_layer_order(wo, &wo_end); + + if (wo_end != to_pack.nr_objects) + die(_("ordered %u objects, expected %"PRIu32), + wo_end, to_pack.nr_objects); + + return wo; +} + + +/* + * A reused set of objects. All objects in a chunk have the same + * relative position in the original packfile and the generated + * packfile. + */ + +static struct reused_chunk { + /* The offset of the first object of this chunk in the original + * packfile. */ + off_t original; + /* The difference for "original" minus the offset of the first object of + * this chunk in the generated packfile. */ + off_t difference; +} *reused_chunks; +static int reused_chunks_nr; +static int reused_chunks_alloc; + +static void record_reused_object(off_t where, off_t offset) +{ + if (reused_chunks_nr && reused_chunks[reused_chunks_nr-1].difference == offset) + return; + + ALLOC_GROW(reused_chunks, reused_chunks_nr + 1, + reused_chunks_alloc); + reused_chunks[reused_chunks_nr].original = where; + reused_chunks[reused_chunks_nr].difference = offset; + reused_chunks_nr++; +} + +/* + * Binary search to find the chunk that "where" is in. Note + * that we're not looking for an exact match, just the first + * chunk that contains it (which implicitly ends at the start + * of the next chunk. + */ +static off_t find_reused_offset(off_t where) +{ + int lo = 0, hi = reused_chunks_nr; + while (lo < hi) { + int mi = lo + ((hi - lo) / 2); + if (where == reused_chunks[mi].original) + return reused_chunks[mi].difference; + if (where < reused_chunks[mi].original) + hi = mi; + else + lo = mi + 1; + } + + /* + * The first chunk starts at zero, so we can't have gone below + * there. + */ + assert(lo); + return reused_chunks[lo-1].difference; +} + +static void write_reused_pack_one(size_t pos, struct hashfile *out, + struct pack_window **w_curs) +{ + off_t offset, next, cur; + enum object_type type; + unsigned long size; + + offset = pack_pos_to_offset(reuse_packfile, pos); + next = pack_pos_to_offset(reuse_packfile, pos + 1); + + record_reused_object(offset, offset - hashfile_total(out)); + + cur = offset; + type = unpack_object_header(reuse_packfile, w_curs, &cur, &size); + assert(type >= 0); + + if (type == OBJ_OFS_DELTA) { + off_t base_offset; + off_t fixup; + + unsigned char header[MAX_PACK_OBJECT_HEADER]; + unsigned len; + + base_offset = get_delta_base(reuse_packfile, w_curs, &cur, type, offset); + assert(base_offset != 0); + + /* Convert to REF_DELTA if we must... */ + if (!allow_ofs_delta) { + uint32_t base_pos; + struct object_id base_oid; + + if (offset_to_pack_pos(reuse_packfile, base_offset, &base_pos) < 0) + die(_("expected object at offset %"PRIuMAX" " + "in pack %s"), + (uintmax_t)base_offset, + reuse_packfile->pack_name); + + nth_packed_object_id(&base_oid, reuse_packfile, + pack_pos_to_index(reuse_packfile, base_pos)); + + len = encode_in_pack_object_header(header, sizeof(header), + OBJ_REF_DELTA, size); + hashwrite(out, header, len); + hashwrite(out, base_oid.hash, the_hash_algo->rawsz); + copy_pack_data(out, reuse_packfile, w_curs, cur, next - cur); + return; + } + + /* Otherwise see if we need to rewrite the offset... */ + fixup = find_reused_offset(offset) - + find_reused_offset(base_offset); + if (fixup) { + unsigned char ofs_header[10]; + unsigned i, ofs_len; + off_t ofs = offset - base_offset - fixup; + + len = encode_in_pack_object_header(header, sizeof(header), + OBJ_OFS_DELTA, size); + + i = sizeof(ofs_header) - 1; + ofs_header[i] = ofs & 127; + while (ofs >>= 7) + ofs_header[--i] = 128 | (--ofs & 127); + + ofs_len = sizeof(ofs_header) - i; + + hashwrite(out, header, len); + hashwrite(out, ofs_header + sizeof(ofs_header) - ofs_len, ofs_len); + copy_pack_data(out, reuse_packfile, w_curs, cur, next - cur); + return; + } + + /* ...otherwise we have no fixup, and can write it verbatim */ + } + + copy_pack_data(out, reuse_packfile, w_curs, offset, next - offset); +} + +static size_t write_reused_pack_verbatim(struct hashfile *out, + struct pack_window **w_curs) +{ + size_t pos = 0; + + while (pos < reuse_packfile_bitmap->word_alloc && + reuse_packfile_bitmap->words[pos] == (eword_t)~0) + pos++; + + if (pos) { + off_t to_write; + + written = (pos * BITS_IN_EWORD); + to_write = pack_pos_to_offset(reuse_packfile, written) + - sizeof(struct pack_header); + + /* We're recording one chunk, not one object. */ + record_reused_object(sizeof(struct pack_header), 0); + hashflush(out); + copy_pack_data(out, reuse_packfile, w_curs, + sizeof(struct pack_header), to_write); + + display_progress(progress_state, written); + } + return pos; +} + +static void write_reused_pack(struct hashfile *f) +{ + size_t i = 0; + uint32_t offset; + struct pack_window *w_curs = NULL; + + if (allow_ofs_delta) + i = write_reused_pack_verbatim(f, &w_curs); + + for (; i < reuse_packfile_bitmap->word_alloc; ++i) { + eword_t word = reuse_packfile_bitmap->words[i]; + size_t pos = (i * BITS_IN_EWORD); + + for (offset = 0; offset < BITS_IN_EWORD; ++offset) { + if ((word >> offset) == 0) + break; + + offset += ewah_bit_ctz64(word >> offset); + /* + * Can use bit positions directly, even for MIDX + * bitmaps. See comment in try_partial_reuse() + * for why. + */ + write_reused_pack_one(pos + offset, f, &w_curs); + display_progress(progress_state, ++written); + } + } + + unuse_pack(&w_curs); +} + +static void write_excluded_by_configs(void) +{ + struct oidset_iter iter; + const struct object_id *oid; + + oidset_iter_init(&excluded_by_config, &iter); + while ((oid = oidset_iter_next(&iter))) { + struct configured_exclusion *ex = + oidmap_get(&configured_exclusions, oid); + + if (!ex) + BUG("configured exclusion wasn't configured"); + write_in_full(1, ex->pack_hash_hex, strlen(ex->pack_hash_hex)); + write_in_full(1, " ", 1); + write_in_full(1, ex->uri, strlen(ex->uri)); + write_in_full(1, "\n", 1); + } +} + +static const char no_split_warning[] = N_( +"disabling bitmap writing, packs are split due to pack.packSizeLimit" +); + +static void write_pack_file(void) +{ + uint32_t i = 0, j; + struct hashfile *f; + off_t offset; + uint32_t nr_remaining = nr_result; + time_t last_mtime = 0; + struct object_entry **write_order; + + if (progress > pack_to_stdout) + progress_state = start_progress(_("Writing objects"), nr_result); + ALLOC_ARRAY(written_list, to_pack.nr_objects); + write_order = compute_write_order(); + + do { + unsigned char hash[GIT_MAX_RAWSZ]; + char *pack_tmp_name = NULL; + + if (pack_to_stdout) + f = hashfd_throughput(1, "<stdout>", progress_state); + else + f = create_tmp_packfile(&pack_tmp_name); + + offset = write_pack_header(f, nr_remaining); + + if (reuse_packfile) { + assert(pack_to_stdout); + write_reused_pack(f); + offset = hashfile_total(f); + } + + nr_written = 0; + for (; i < to_pack.nr_objects; i++) { + struct object_entry *e = write_order[i]; + if (write_one(f, e, &offset) == WRITE_ONE_BREAK) + break; + display_progress(progress_state, written); + } + + if (pack_to_stdout) { + /* + * We never fsync when writing to stdout since we may + * not be writing to an actual pack file. For instance, + * the upload-pack code passes a pipe here. Calling + * fsync on a pipe results in unnecessary + * synchronization with the reader on some platforms. + */ + finalize_hashfile(f, hash, FSYNC_COMPONENT_NONE, + CSUM_HASH_IN_STREAM | CSUM_CLOSE); + } else if (nr_written == nr_remaining) { + finalize_hashfile(f, hash, FSYNC_COMPONENT_PACK, + CSUM_HASH_IN_STREAM | CSUM_FSYNC | CSUM_CLOSE); + } else { + /* + * If we wrote the wrong number of entries in the + * header, rewrite it like in fast-import. + */ + + int fd = finalize_hashfile(f, hash, FSYNC_COMPONENT_PACK, 0); + fixup_pack_header_footer(fd, hash, pack_tmp_name, + nr_written, hash, offset); + close(fd); + if (write_bitmap_index) { + if (write_bitmap_index != WRITE_BITMAP_QUIET) + warning(_(no_split_warning)); + write_bitmap_index = 0; + } + } + + if (!pack_to_stdout) { + struct stat st; + struct strbuf tmpname = STRBUF_INIT; + char *idx_tmp_name = NULL; + + /* + * Packs are runtime accessed in their mtime + * order since newer packs are more likely to contain + * younger objects. So if we are creating multiple + * packs then we should modify the mtime of later ones + * to preserve this property. + */ + if (stat(pack_tmp_name, &st) < 0) { + warning_errno(_("failed to stat %s"), pack_tmp_name); + } else if (!last_mtime) { + last_mtime = st.st_mtime; + } else { + struct utimbuf utb; + utb.actime = st.st_atime; + utb.modtime = --last_mtime; + if (utime(pack_tmp_name, &utb) < 0) + warning_errno(_("failed utime() on %s"), pack_tmp_name); + } + + strbuf_addf(&tmpname, "%s-%s.", base_name, + hash_to_hex(hash)); + + if (write_bitmap_index) { + bitmap_writer_set_checksum(hash); + bitmap_writer_build_type_index( + &to_pack, written_list, nr_written); + } + + if (cruft) + pack_idx_opts.flags |= WRITE_MTIMES; + + stage_tmp_packfiles(&tmpname, pack_tmp_name, + written_list, nr_written, + &to_pack, &pack_idx_opts, hash, + &idx_tmp_name); + + if (write_bitmap_index) { + size_t tmpname_len = tmpname.len; + + strbuf_addstr(&tmpname, "bitmap"); + stop_progress(&progress_state); + + bitmap_writer_show_progress(progress); + bitmap_writer_select_commits(indexed_commits, indexed_commits_nr, -1); + if (bitmap_writer_build(&to_pack) < 0) + die(_("failed to write bitmap index")); + bitmap_writer_finish(written_list, nr_written, + tmpname.buf, write_bitmap_options); + write_bitmap_index = 0; + strbuf_setlen(&tmpname, tmpname_len); + } + + rename_tmp_packfile_idx(&tmpname, &idx_tmp_name); + + free(idx_tmp_name); + strbuf_release(&tmpname); + free(pack_tmp_name); + puts(hash_to_hex(hash)); + } + + /* mark written objects as written to previous pack */ + for (j = 0; j < nr_written; j++) { + written_list[j]->offset = (off_t)-1; + } + nr_remaining -= nr_written; + } while (nr_remaining && i < to_pack.nr_objects); + + free(written_list); + free(write_order); + stop_progress(&progress_state); + if (written != nr_result) + die(_("wrote %"PRIu32" objects while expecting %"PRIu32), + written, nr_result); + trace2_data_intmax("pack-objects", the_repository, + "write_pack_file/wrote", nr_result); +} + +static int no_try_delta(const char *path) +{ + static struct attr_check *check; + + if (!check) + check = attr_check_initl("delta", NULL); + git_check_attr(the_repository->index, path, check); + if (ATTR_FALSE(check->items[0].value)) + return 1; + return 0; +} + +/* + * When adding an object, check whether we have already added it + * to our packing list. If so, we can skip. However, if we are + * being asked to excludei t, but the previous mention was to include + * it, make sure to adjust its flags and tweak our numbers accordingly. + * + * As an optimization, we pass out the index position where we would have + * found the item, since that saves us from having to look it up again a + * few lines later when we want to add the new entry. + */ +static int have_duplicate_entry(const struct object_id *oid, + int exclude) +{ + struct object_entry *entry; + + if (reuse_packfile_bitmap && + bitmap_walk_contains(bitmap_git, reuse_packfile_bitmap, oid)) + return 1; + + entry = packlist_find(&to_pack, oid); + if (!entry) + return 0; + + if (exclude) { + if (!entry->preferred_base) + nr_result--; + entry->preferred_base = 1; + } + + return 1; +} + +static int want_found_object(const struct object_id *oid, int exclude, + struct packed_git *p) +{ + if (exclude) + return 1; + if (incremental) + return 0; + + if (!is_pack_valid(p)) + return -1; + + /* + * When asked to do --local (do not include an object that appears in a + * pack we borrow from elsewhere) or --honor-pack-keep (do not include + * an object that appears in a pack marked with .keep), finding a pack + * that matches the criteria is sufficient for us to decide to omit it. + * However, even if this pack does not satisfy the criteria, we need to + * make sure no copy of this object appears in _any_ pack that makes us + * to omit the object, so we need to check all the packs. + * + * We can however first check whether these options can possibly matter; + * if they do not matter we know we want the object in generated pack. + * Otherwise, we signal "-1" at the end to tell the caller that we do + * not know either way, and it needs to check more packs. + */ + + /* + * Objects in packs borrowed from elsewhere are discarded regardless of + * if they appear in other packs that weren't borrowed. + */ + if (local && !p->pack_local) + return 0; + + /* + * Then handle .keep first, as we have a fast(er) path there. + */ + if (ignore_packed_keep_on_disk || ignore_packed_keep_in_core) { + /* + * Set the flags for the kept-pack cache to be the ones we want + * to ignore. + * + * That is, if we are ignoring objects in on-disk keep packs, + * then we want to search through the on-disk keep and ignore + * the in-core ones. + */ + unsigned flags = 0; + if (ignore_packed_keep_on_disk) + flags |= ON_DISK_KEEP_PACKS; + if (ignore_packed_keep_in_core) + flags |= IN_CORE_KEEP_PACKS; + + if (ignore_packed_keep_on_disk && p->pack_keep) + return 0; + if (ignore_packed_keep_in_core && p->pack_keep_in_core) + return 0; + if (has_object_kept_pack(oid, flags)) + return 0; + } + + /* + * At this point we know definitively that either we don't care about + * keep-packs, or the object is not in one. Keep checking other + * conditions... + */ + if (!local || !have_non_local_packs) + return 1; + + /* we don't know yet; keep looking for more packs */ + return -1; +} + +static int want_object_in_pack_one(struct packed_git *p, + const struct object_id *oid, + int exclude, + struct packed_git **found_pack, + off_t *found_offset) +{ + off_t offset; + + if (p == *found_pack) + offset = *found_offset; + else + offset = find_pack_entry_one(oid->hash, p); + + if (offset) { + if (!*found_pack) { + if (!is_pack_valid(p)) + return -1; + *found_offset = offset; + *found_pack = p; + } + return want_found_object(oid, exclude, p); + } + return -1; +} + +/* + * Check whether we want the object in the pack (e.g., we do not want + * objects found in non-local stores if the "--local" option was used). + * + * If the caller already knows an existing pack it wants to take the object + * from, that is passed in *found_pack and *found_offset; otherwise this + * function finds if there is any pack that has the object and returns the pack + * and its offset in these variables. + */ +static int want_object_in_pack(const struct object_id *oid, + int exclude, + struct packed_git **found_pack, + off_t *found_offset) +{ + int want; + struct list_head *pos; + struct multi_pack_index *m; + + if (!exclude && local && has_loose_object_nonlocal(oid)) + return 0; + + /* + * If we already know the pack object lives in, start checks from that + * pack - in the usual case when neither --local was given nor .keep files + * are present we will determine the answer right now. + */ + if (*found_pack) { + want = want_found_object(oid, exclude, *found_pack); + if (want != -1) + return want; + + *found_pack = NULL; + *found_offset = 0; + } + + for (m = get_multi_pack_index(the_repository); m; m = m->next) { + struct pack_entry e; + if (fill_midx_entry(the_repository, oid, &e, m)) { + want = want_object_in_pack_one(e.p, oid, exclude, found_pack, found_offset); + if (want != -1) + return want; + } + } + + list_for_each(pos, get_packed_git_mru(the_repository)) { + struct packed_git *p = list_entry(pos, struct packed_git, mru); + want = want_object_in_pack_one(p, oid, exclude, found_pack, found_offset); + if (!exclude && want > 0) + list_move(&p->mru, + get_packed_git_mru(the_repository)); + if (want != -1) + return want; + } + + if (uri_protocols.nr) { + struct configured_exclusion *ex = + oidmap_get(&configured_exclusions, oid); + int i; + const char *p; + + if (ex) { + for (i = 0; i < uri_protocols.nr; i++) { + if (skip_prefix(ex->uri, + uri_protocols.items[i].string, + &p) && + *p == ':') { + oidset_insert(&excluded_by_config, oid); + return 0; + } + } + } + } + + return 1; +} + +static struct object_entry *create_object_entry(const struct object_id *oid, + enum object_type type, + uint32_t hash, + int exclude, + int no_try_delta, + struct packed_git *found_pack, + off_t found_offset) +{ + struct object_entry *entry; + + entry = packlist_alloc(&to_pack, oid); + entry->hash = hash; + oe_set_type(entry, type); + if (exclude) + entry->preferred_base = 1; + else + nr_result++; + if (found_pack) { + oe_set_in_pack(&to_pack, entry, found_pack); + entry->in_pack_offset = found_offset; + } + + entry->no_try_delta = no_try_delta; + + return entry; +} + +static const char no_closure_warning[] = N_( +"disabling bitmap writing, as some objects are not being packed" +); + +static int add_object_entry(const struct object_id *oid, enum object_type type, + const char *name, int exclude) +{ + struct packed_git *found_pack = NULL; + off_t found_offset = 0; + + display_progress(progress_state, ++nr_seen); + + if (have_duplicate_entry(oid, exclude)) + return 0; + + if (!want_object_in_pack(oid, exclude, &found_pack, &found_offset)) { + /* The pack is missing an object, so it will not have closure */ + if (write_bitmap_index) { + if (write_bitmap_index != WRITE_BITMAP_QUIET) + warning(_(no_closure_warning)); + write_bitmap_index = 0; + } + return 0; + } + + create_object_entry(oid, type, pack_name_hash(name), + exclude, name && no_try_delta(name), + found_pack, found_offset); + return 1; +} + +static int add_object_entry_from_bitmap(const struct object_id *oid, + enum object_type type, + int flags UNUSED, uint32_t name_hash, + struct packed_git *pack, off_t offset) +{ + display_progress(progress_state, ++nr_seen); + + if (have_duplicate_entry(oid, 0)) + return 0; + + if (!want_object_in_pack(oid, 0, &pack, &offset)) + return 0; + + create_object_entry(oid, type, name_hash, 0, 0, pack, offset); + return 1; +} + +struct pbase_tree_cache { + struct object_id oid; + int ref; + int temporary; + void *tree_data; + unsigned long tree_size; +}; + +static struct pbase_tree_cache *(pbase_tree_cache[256]); +static int pbase_tree_cache_ix(const struct object_id *oid) +{ + return oid->hash[0] % ARRAY_SIZE(pbase_tree_cache); +} +static int pbase_tree_cache_ix_incr(int ix) +{ + return (ix+1) % ARRAY_SIZE(pbase_tree_cache); +} + +static struct pbase_tree { + struct pbase_tree *next; + /* This is a phony "cache" entry; we are not + * going to evict it or find it through _get() + * mechanism -- this is for the toplevel node that + * would almost always change with any commit. + */ + struct pbase_tree_cache pcache; +} *pbase_tree; + +static struct pbase_tree_cache *pbase_tree_get(const struct object_id *oid) +{ + struct pbase_tree_cache *ent, *nent; + void *data; + unsigned long size; + enum object_type type; + int neigh; + int my_ix = pbase_tree_cache_ix(oid); + int available_ix = -1; + + /* pbase-tree-cache acts as a limited hashtable. + * your object will be found at your index or within a few + * slots after that slot if it is cached. + */ + for (neigh = 0; neigh < 8; neigh++) { + ent = pbase_tree_cache[my_ix]; + if (ent && oideq(&ent->oid, oid)) { + ent->ref++; + return ent; + } + else if (((available_ix < 0) && (!ent || !ent->ref)) || + ((0 <= available_ix) && + (!ent && pbase_tree_cache[available_ix]))) + available_ix = my_ix; + if (!ent) + break; + my_ix = pbase_tree_cache_ix_incr(my_ix); + } + + /* Did not find one. Either we got a bogus request or + * we need to read and perhaps cache. + */ + data = repo_read_object_file(the_repository, oid, &type, &size); + if (!data) + return NULL; + if (type != OBJ_TREE) { + free(data); + return NULL; + } + + /* We need to either cache or return a throwaway copy */ + + if (available_ix < 0) + ent = NULL; + else { + ent = pbase_tree_cache[available_ix]; + my_ix = available_ix; + } + + if (!ent) { + nent = xmalloc(sizeof(*nent)); + nent->temporary = (available_ix < 0); + } + else { + /* evict and reuse */ + free(ent->tree_data); + nent = ent; + } + oidcpy(&nent->oid, oid); + nent->tree_data = data; + nent->tree_size = size; + nent->ref = 1; + if (!nent->temporary) + pbase_tree_cache[my_ix] = nent; + return nent; +} + +static void pbase_tree_put(struct pbase_tree_cache *cache) +{ + if (!cache->temporary) { + cache->ref--; + return; + } + free(cache->tree_data); + free(cache); +} + +static size_t name_cmp_len(const char *name) +{ + return strcspn(name, "\n/"); +} + +static void add_pbase_object(struct tree_desc *tree, + const char *name, + size_t cmplen, + const char *fullname) +{ + struct name_entry entry; + int cmp; + + while (tree_entry(tree,&entry)) { + if (S_ISGITLINK(entry.mode)) + continue; + cmp = tree_entry_len(&entry) != cmplen ? 1 : + memcmp(name, entry.path, cmplen); + if (cmp > 0) + continue; + if (cmp < 0) + return; + if (name[cmplen] != '/') { + add_object_entry(&entry.oid, + object_type(entry.mode), + fullname, 1); + return; + } + if (S_ISDIR(entry.mode)) { + struct tree_desc sub; + struct pbase_tree_cache *tree; + const char *down = name+cmplen+1; + size_t downlen = name_cmp_len(down); + + tree = pbase_tree_get(&entry.oid); + if (!tree) + return; + init_tree_desc(&sub, tree->tree_data, tree->tree_size); + + add_pbase_object(&sub, down, downlen, fullname); + pbase_tree_put(tree); + } + } +} + +static unsigned *done_pbase_paths; +static int done_pbase_paths_num; +static int done_pbase_paths_alloc; +static int done_pbase_path_pos(unsigned hash) +{ + int lo = 0; + int hi = done_pbase_paths_num; + while (lo < hi) { + int mi = lo + (hi - lo) / 2; + if (done_pbase_paths[mi] == hash) + return mi; + if (done_pbase_paths[mi] < hash) + hi = mi; + else + lo = mi + 1; + } + return -lo-1; +} + +static int check_pbase_path(unsigned hash) +{ + int pos = done_pbase_path_pos(hash); + if (0 <= pos) + return 1; + pos = -pos - 1; + ALLOC_GROW(done_pbase_paths, + done_pbase_paths_num + 1, + done_pbase_paths_alloc); + done_pbase_paths_num++; + if (pos < done_pbase_paths_num) + MOVE_ARRAY(done_pbase_paths + pos + 1, done_pbase_paths + pos, + done_pbase_paths_num - pos - 1); + done_pbase_paths[pos] = hash; + return 0; +} + +static void add_preferred_base_object(const char *name) +{ + struct pbase_tree *it; + size_t cmplen; + unsigned hash = pack_name_hash(name); + + if (!num_preferred_base || check_pbase_path(hash)) + return; + + cmplen = name_cmp_len(name); + for (it = pbase_tree; it; it = it->next) { + if (cmplen == 0) { + add_object_entry(&it->pcache.oid, OBJ_TREE, NULL, 1); + } + else { + struct tree_desc tree; + init_tree_desc(&tree, it->pcache.tree_data, it->pcache.tree_size); + add_pbase_object(&tree, name, cmplen, name); + } + } +} + +static void add_preferred_base(struct object_id *oid) +{ + struct pbase_tree *it; + void *data; + unsigned long size; + struct object_id tree_oid; + + if (window <= num_preferred_base++) + return; + + data = read_object_with_reference(the_repository, oid, + OBJ_TREE, &size, &tree_oid); + if (!data) + return; + + for (it = pbase_tree; it; it = it->next) { + if (oideq(&it->pcache.oid, &tree_oid)) { + free(data); + return; + } + } + + CALLOC_ARRAY(it, 1); + it->next = pbase_tree; + pbase_tree = it; + + oidcpy(&it->pcache.oid, &tree_oid); + it->pcache.tree_data = data; + it->pcache.tree_size = size; +} + +static void cleanup_preferred_base(void) +{ + struct pbase_tree *it; + unsigned i; + + it = pbase_tree; + pbase_tree = NULL; + while (it) { + struct pbase_tree *tmp = it; + it = tmp->next; + free(tmp->pcache.tree_data); + free(tmp); + } + + for (i = 0; i < ARRAY_SIZE(pbase_tree_cache); i++) { + if (!pbase_tree_cache[i]) + continue; + free(pbase_tree_cache[i]->tree_data); + FREE_AND_NULL(pbase_tree_cache[i]); + } + + FREE_AND_NULL(done_pbase_paths); + done_pbase_paths_num = done_pbase_paths_alloc = 0; +} + +/* + * Return 1 iff the object specified by "delta" can be sent + * literally as a delta against the base in "base_sha1". If + * so, then *base_out will point to the entry in our packing + * list, or NULL if we must use the external-base list. + * + * Depth value does not matter - find_deltas() will + * never consider reused delta as the base object to + * deltify other objects against, in order to avoid + * circular deltas. + */ +static int can_reuse_delta(const struct object_id *base_oid, + struct object_entry *delta, + struct object_entry **base_out) +{ + struct object_entry *base; + + /* + * First see if we're already sending the base (or it's explicitly in + * our "excluded" list). + */ + base = packlist_find(&to_pack, base_oid); + if (base) { + if (!in_same_island(&delta->idx.oid, &base->idx.oid)) + return 0; + *base_out = base; + return 1; + } + + /* + * Otherwise, reachability bitmaps may tell us if the receiver has it, + * even if it was buried too deep in history to make it into the + * packing list. + */ + if (thin && bitmap_has_oid_in_uninteresting(bitmap_git, base_oid)) { + if (use_delta_islands) { + if (!in_same_island(&delta->idx.oid, base_oid)) + return 0; + } + *base_out = NULL; + return 1; + } + + return 0; +} + +static void prefetch_to_pack(uint32_t object_index_start) { + struct oid_array to_fetch = OID_ARRAY_INIT; + uint32_t i; + + for (i = object_index_start; i < to_pack.nr_objects; i++) { + struct object_entry *entry = to_pack.objects + i; + + if (!oid_object_info_extended(the_repository, + &entry->idx.oid, + NULL, + OBJECT_INFO_FOR_PREFETCH)) + continue; + oid_array_append(&to_fetch, &entry->idx.oid); + } + promisor_remote_get_direct(the_repository, + to_fetch.oid, to_fetch.nr); + oid_array_clear(&to_fetch); +} + +static void check_object(struct object_entry *entry, uint32_t object_index) +{ + unsigned long canonical_size; + enum object_type type; + struct object_info oi = {.typep = &type, .sizep = &canonical_size}; + + if (IN_PACK(entry)) { + struct packed_git *p = IN_PACK(entry); + struct pack_window *w_curs = NULL; + int have_base = 0; + struct object_id base_ref; + struct object_entry *base_entry; + unsigned long used, used_0; + unsigned long avail; + off_t ofs; + unsigned char *buf, c; + enum object_type type; + unsigned long in_pack_size; + + buf = use_pack(p, &w_curs, entry->in_pack_offset, &avail); + + /* + * We want in_pack_type even if we do not reuse delta + * since non-delta representations could still be reused. + */ + used = unpack_object_header_buffer(buf, avail, + &type, + &in_pack_size); + if (used == 0) + goto give_up; + + if (type < 0) + BUG("invalid type %d", type); + entry->in_pack_type = type; + + /* + * Determine if this is a delta and if so whether we can + * reuse it or not. Otherwise let's find out as cheaply as + * possible what the actual type and size for this object is. + */ + switch (entry->in_pack_type) { + default: + /* Not a delta hence we've already got all we need. */ + oe_set_type(entry, entry->in_pack_type); + SET_SIZE(entry, in_pack_size); + entry->in_pack_header_size = used; + if (oe_type(entry) < OBJ_COMMIT || oe_type(entry) > OBJ_BLOB) + goto give_up; + unuse_pack(&w_curs); + return; + case OBJ_REF_DELTA: + if (reuse_delta && !entry->preferred_base) { + oidread(&base_ref, + use_pack(p, &w_curs, + entry->in_pack_offset + used, + NULL)); + have_base = 1; + } + entry->in_pack_header_size = used + the_hash_algo->rawsz; + break; + case OBJ_OFS_DELTA: + buf = use_pack(p, &w_curs, + entry->in_pack_offset + used, NULL); + used_0 = 0; + c = buf[used_0++]; + ofs = c & 127; + while (c & 128) { + ofs += 1; + if (!ofs || MSB(ofs, 7)) { + error(_("delta base offset overflow in pack for %s"), + oid_to_hex(&entry->idx.oid)); + goto give_up; + } + c = buf[used_0++]; + ofs = (ofs << 7) + (c & 127); + } + ofs = entry->in_pack_offset - ofs; + if (ofs <= 0 || ofs >= entry->in_pack_offset) { + error(_("delta base offset out of bound for %s"), + oid_to_hex(&entry->idx.oid)); + goto give_up; + } + if (reuse_delta && !entry->preferred_base) { + uint32_t pos; + if (offset_to_pack_pos(p, ofs, &pos) < 0) + goto give_up; + if (!nth_packed_object_id(&base_ref, p, + pack_pos_to_index(p, pos))) + have_base = 1; + } + entry->in_pack_header_size = used + used_0; + break; + } + + if (have_base && + can_reuse_delta(&base_ref, entry, &base_entry)) { + oe_set_type(entry, entry->in_pack_type); + SET_SIZE(entry, in_pack_size); /* delta size */ + SET_DELTA_SIZE(entry, in_pack_size); + + if (base_entry) { + SET_DELTA(entry, base_entry); + entry->delta_sibling_idx = base_entry->delta_child_idx; + SET_DELTA_CHILD(base_entry, entry); + } else { + SET_DELTA_EXT(entry, &base_ref); + } + + unuse_pack(&w_curs); + return; + } + + if (oe_type(entry)) { + off_t delta_pos; + + /* + * This must be a delta and we already know what the + * final object type is. Let's extract the actual + * object size from the delta header. + */ + delta_pos = entry->in_pack_offset + entry->in_pack_header_size; + canonical_size = get_size_from_delta(p, &w_curs, delta_pos); + if (canonical_size == 0) + goto give_up; + SET_SIZE(entry, canonical_size); + unuse_pack(&w_curs); + return; + } + + /* + * No choice but to fall back to the recursive delta walk + * with oid_object_info() to find about the object type + * at this point... + */ + give_up: + unuse_pack(&w_curs); + } + + if (oid_object_info_extended(the_repository, &entry->idx.oid, &oi, + OBJECT_INFO_SKIP_FETCH_OBJECT | OBJECT_INFO_LOOKUP_REPLACE) < 0) { + if (repo_has_promisor_remote(the_repository)) { + prefetch_to_pack(object_index); + if (oid_object_info_extended(the_repository, &entry->idx.oid, &oi, + OBJECT_INFO_SKIP_FETCH_OBJECT | OBJECT_INFO_LOOKUP_REPLACE) < 0) + type = -1; + } else { + type = -1; + } + } + oe_set_type(entry, type); + if (entry->type_valid) { + SET_SIZE(entry, canonical_size); + } else { + /* + * Bad object type is checked in prepare_pack(). This is + * to permit a missing preferred base object to be ignored + * as a preferred base. Doing so can result in a larger + * pack file, but the transfer will still take place. + */ + } +} + +static int pack_offset_sort(const void *_a, const void *_b) +{ + const struct object_entry *a = *(struct object_entry **)_a; + const struct object_entry *b = *(struct object_entry **)_b; + const struct packed_git *a_in_pack = IN_PACK(a); + const struct packed_git *b_in_pack = IN_PACK(b); + + /* avoid filesystem trashing with loose objects */ + if (!a_in_pack && !b_in_pack) + return oidcmp(&a->idx.oid, &b->idx.oid); + + if (a_in_pack < b_in_pack) + return -1; + if (a_in_pack > b_in_pack) + return 1; + return a->in_pack_offset < b->in_pack_offset ? -1 : + (a->in_pack_offset > b->in_pack_offset); +} + +/* + * Drop an on-disk delta we were planning to reuse. Naively, this would + * just involve blanking out the "delta" field, but we have to deal + * with some extra book-keeping: + * + * 1. Removing ourselves from the delta_sibling linked list. + * + * 2. Updating our size/type to the non-delta representation. These were + * either not recorded initially (size) or overwritten with the delta type + * (type) when check_object() decided to reuse the delta. + * + * 3. Resetting our delta depth, as we are now a base object. + */ +static void drop_reused_delta(struct object_entry *entry) +{ + unsigned *idx = &to_pack.objects[entry->delta_idx - 1].delta_child_idx; + struct object_info oi = OBJECT_INFO_INIT; + enum object_type type; + unsigned long size; + + while (*idx) { + struct object_entry *oe = &to_pack.objects[*idx - 1]; + + if (oe == entry) + *idx = oe->delta_sibling_idx; + else + idx = &oe->delta_sibling_idx; + } + SET_DELTA(entry, NULL); + entry->depth = 0; + + oi.sizep = &size; + oi.typep = &type; + if (packed_object_info(the_repository, IN_PACK(entry), entry->in_pack_offset, &oi) < 0) { + /* + * We failed to get the info from this pack for some reason; + * fall back to oid_object_info, which may find another copy. + * And if that fails, the error will be recorded in oe_type(entry) + * and dealt with in prepare_pack(). + */ + oe_set_type(entry, + oid_object_info(the_repository, &entry->idx.oid, &size)); + } else { + oe_set_type(entry, type); + } + SET_SIZE(entry, size); +} + +/* + * Follow the chain of deltas from this entry onward, throwing away any links + * that cause us to hit a cycle (as determined by the DFS state flags in + * the entries). + * + * We also detect too-long reused chains that would violate our --depth + * limit. + */ +static void break_delta_chains(struct object_entry *entry) +{ + /* + * The actual depth of each object we will write is stored as an int, + * as it cannot exceed our int "depth" limit. But before we break + * changes based no that limit, we may potentially go as deep as the + * number of objects, which is elsewhere bounded to a uint32_t. + */ + uint32_t total_depth; + struct object_entry *cur, *next; + + for (cur = entry, total_depth = 0; + cur; + cur = DELTA(cur), total_depth++) { + if (cur->dfs_state == DFS_DONE) { + /* + * We've already seen this object and know it isn't + * part of a cycle. We do need to append its depth + * to our count. + */ + total_depth += cur->depth; + break; + } + + /* + * We break cycles before looping, so an ACTIVE state (or any + * other cruft which made its way into the state variable) + * is a bug. + */ + if (cur->dfs_state != DFS_NONE) + BUG("confusing delta dfs state in first pass: %d", + cur->dfs_state); + + /* + * Now we know this is the first time we've seen the object. If + * it's not a delta, we're done traversing, but we'll mark it + * done to save time on future traversals. + */ + if (!DELTA(cur)) { + cur->dfs_state = DFS_DONE; + break; + } + + /* + * Mark ourselves as active and see if the next step causes + * us to cycle to another active object. It's important to do + * this _before_ we loop, because it impacts where we make the + * cut, and thus how our total_depth counter works. + * E.g., We may see a partial loop like: + * + * A -> B -> C -> D -> B + * + * Cutting B->C breaks the cycle. But now the depth of A is + * only 1, and our total_depth counter is at 3. The size of the + * error is always one less than the size of the cycle we + * broke. Commits C and D were "lost" from A's chain. + * + * If we instead cut D->B, then the depth of A is correct at 3. + * We keep all commits in the chain that we examined. + */ + cur->dfs_state = DFS_ACTIVE; + if (DELTA(cur)->dfs_state == DFS_ACTIVE) { + drop_reused_delta(cur); + cur->dfs_state = DFS_DONE; + break; + } + } + + /* + * And now that we've gone all the way to the bottom of the chain, we + * need to clear the active flags and set the depth fields as + * appropriate. Unlike the loop above, which can quit when it drops a + * delta, we need to keep going to look for more depth cuts. So we need + * an extra "next" pointer to keep going after we reset cur->delta. + */ + for (cur = entry; cur; cur = next) { + next = DELTA(cur); + + /* + * We should have a chain of zero or more ACTIVE states down to + * a final DONE. We can quit after the DONE, because either it + * has no bases, or we've already handled them in a previous + * call. + */ + if (cur->dfs_state == DFS_DONE) + break; + else if (cur->dfs_state != DFS_ACTIVE) + BUG("confusing delta dfs state in second pass: %d", + cur->dfs_state); + + /* + * If the total_depth is more than depth, then we need to snip + * the chain into two or more smaller chains that don't exceed + * the maximum depth. Most of the resulting chains will contain + * (depth + 1) entries (i.e., depth deltas plus one base), and + * the last chain (i.e., the one containing entry) will contain + * whatever entries are left over, namely + * (total_depth % (depth + 1)) of them. + * + * Since we are iterating towards decreasing depth, we need to + * decrement total_depth as we go, and we need to write to the + * entry what its final depth will be after all of the + * snipping. Since we're snipping into chains of length (depth + * + 1) entries, the final depth of an entry will be its + * original depth modulo (depth + 1). Any time we encounter an + * entry whose final depth is supposed to be zero, we snip it + * from its delta base, thereby making it so. + */ + cur->depth = (total_depth--) % (depth + 1); + if (!cur->depth) + drop_reused_delta(cur); + + cur->dfs_state = DFS_DONE; + } +} + +static void get_object_details(void) +{ + uint32_t i; + struct object_entry **sorted_by_offset; + + if (progress) + progress_state = start_progress(_("Counting objects"), + to_pack.nr_objects); + + CALLOC_ARRAY(sorted_by_offset, to_pack.nr_objects); + for (i = 0; i < to_pack.nr_objects; i++) + sorted_by_offset[i] = to_pack.objects + i; + QSORT(sorted_by_offset, to_pack.nr_objects, pack_offset_sort); + + for (i = 0; i < to_pack.nr_objects; i++) { + struct object_entry *entry = sorted_by_offset[i]; + check_object(entry, i); + if (entry->type_valid && + oe_size_greater_than(&to_pack, entry, big_file_threshold)) + entry->no_try_delta = 1; + display_progress(progress_state, i + 1); + } + stop_progress(&progress_state); + + /* + * This must happen in a second pass, since we rely on the delta + * information for the whole list being completed. + */ + for (i = 0; i < to_pack.nr_objects; i++) + break_delta_chains(&to_pack.objects[i]); + + free(sorted_by_offset); +} + +/* + * We search for deltas in a list sorted by type, by filename hash, and then + * by size, so that we see progressively smaller and smaller files. + * That's because we prefer deltas to be from the bigger file + * to the smaller -- deletes are potentially cheaper, but perhaps + * more importantly, the bigger file is likely the more recent + * one. The deepest deltas are therefore the oldest objects which are + * less susceptible to be accessed often. + */ +static int type_size_sort(const void *_a, const void *_b) +{ + const struct object_entry *a = *(struct object_entry **)_a; + const struct object_entry *b = *(struct object_entry **)_b; + const enum object_type a_type = oe_type(a); + const enum object_type b_type = oe_type(b); + const unsigned long a_size = SIZE(a); + const unsigned long b_size = SIZE(b); + + if (a_type > b_type) + return -1; + if (a_type < b_type) + return 1; + if (a->hash > b->hash) + return -1; + if (a->hash < b->hash) + return 1; + if (a->preferred_base > b->preferred_base) + return -1; + if (a->preferred_base < b->preferred_base) + return 1; + if (use_delta_islands) { + const int island_cmp = island_delta_cmp(&a->idx.oid, &b->idx.oid); + if (island_cmp) + return island_cmp; + } + if (a_size > b_size) + return -1; + if (a_size < b_size) + return 1; + return a < b ? -1 : (a > b); /* newest first */ +} + +struct unpacked { + struct object_entry *entry; + void *data; + struct delta_index *index; + unsigned depth; +}; + +static int delta_cacheable(unsigned long src_size, unsigned long trg_size, + unsigned long delta_size) +{ + if (max_delta_cache_size && delta_cache_size + delta_size > max_delta_cache_size) + return 0; + + if (delta_size < cache_max_small_delta_size) + return 1; + + /* cache delta, if objects are large enough compared to delta size */ + if ((src_size >> 20) + (trg_size >> 21) > (delta_size >> 10)) + return 1; + + return 0; +} + +/* Protect delta_cache_size */ +static pthread_mutex_t cache_mutex; +#define cache_lock() pthread_mutex_lock(&cache_mutex) +#define cache_unlock() pthread_mutex_unlock(&cache_mutex) + +/* + * Protect object list partitioning (e.g. struct thread_param) and + * progress_state + */ +static pthread_mutex_t progress_mutex; +#define progress_lock() pthread_mutex_lock(&progress_mutex) +#define progress_unlock() pthread_mutex_unlock(&progress_mutex) + +/* + * Access to struct object_entry is unprotected since each thread owns + * a portion of the main object list. Just don't access object entries + * ahead in the list because they can be stolen and would need + * progress_mutex for protection. + */ + +static inline int oe_size_less_than(struct packing_data *pack, + const struct object_entry *lhs, + unsigned long rhs) +{ + if (lhs->size_valid) + return lhs->size_ < rhs; + if (rhs < pack->oe_size_limit) /* rhs < 2^x <= lhs ? */ + return 0; + return oe_get_size_slow(pack, lhs) < rhs; +} + +static inline void oe_set_tree_depth(struct packing_data *pack, + struct object_entry *e, + unsigned int tree_depth) +{ + if (!pack->tree_depth) + CALLOC_ARRAY(pack->tree_depth, pack->nr_alloc); + pack->tree_depth[e - pack->objects] = tree_depth; +} + +/* + * Return the size of the object without doing any delta + * reconstruction (so non-deltas are true object sizes, but deltas + * return the size of the delta data). + */ +unsigned long oe_get_size_slow(struct packing_data *pack, + const struct object_entry *e) +{ + struct packed_git *p; + struct pack_window *w_curs; + unsigned char *buf; + enum object_type type; + unsigned long used, avail, size; + + if (e->type_ != OBJ_OFS_DELTA && e->type_ != OBJ_REF_DELTA) { + packing_data_lock(&to_pack); + if (oid_object_info(the_repository, &e->idx.oid, &size) < 0) + die(_("unable to get size of %s"), + oid_to_hex(&e->idx.oid)); + packing_data_unlock(&to_pack); + return size; + } + + p = oe_in_pack(pack, e); + if (!p) + BUG("when e->type is a delta, it must belong to a pack"); + + packing_data_lock(&to_pack); + w_curs = NULL; + buf = use_pack(p, &w_curs, e->in_pack_offset, &avail); + used = unpack_object_header_buffer(buf, avail, &type, &size); + if (used == 0) + die(_("unable to parse object header of %s"), + oid_to_hex(&e->idx.oid)); + + unuse_pack(&w_curs); + packing_data_unlock(&to_pack); + return size; +} + +static int try_delta(struct unpacked *trg, struct unpacked *src, + unsigned max_depth, unsigned long *mem_usage) +{ + struct object_entry *trg_entry = trg->entry; + struct object_entry *src_entry = src->entry; + unsigned long trg_size, src_size, delta_size, sizediff, max_size, sz; + unsigned ref_depth; + enum object_type type; + void *delta_buf; + + /* Don't bother doing diffs between different types */ + if (oe_type(trg_entry) != oe_type(src_entry)) + return -1; + + /* + * We do not bother to try a delta that we discarded on an + * earlier try, but only when reusing delta data. Note that + * src_entry that is marked as the preferred_base should always + * be considered, as even if we produce a suboptimal delta against + * it, we will still save the transfer cost, as we already know + * the other side has it and we won't send src_entry at all. + */ + if (reuse_delta && IN_PACK(trg_entry) && + IN_PACK(trg_entry) == IN_PACK(src_entry) && + !src_entry->preferred_base && + trg_entry->in_pack_type != OBJ_REF_DELTA && + trg_entry->in_pack_type != OBJ_OFS_DELTA) + return 0; + + /* Let's not bust the allowed depth. */ + if (src->depth >= max_depth) + return 0; + + /* Now some size filtering heuristics. */ + trg_size = SIZE(trg_entry); + if (!DELTA(trg_entry)) { + max_size = trg_size/2 - the_hash_algo->rawsz; + ref_depth = 1; + } else { + max_size = DELTA_SIZE(trg_entry); + ref_depth = trg->depth; + } + max_size = (uint64_t)max_size * (max_depth - src->depth) / + (max_depth - ref_depth + 1); + if (max_size == 0) + return 0; + src_size = SIZE(src_entry); + sizediff = src_size < trg_size ? trg_size - src_size : 0; + if (sizediff >= max_size) + return 0; + if (trg_size < src_size / 32) + return 0; + + if (!in_same_island(&trg->entry->idx.oid, &src->entry->idx.oid)) + return 0; + + /* Load data if not already done */ + if (!trg->data) { + packing_data_lock(&to_pack); + trg->data = repo_read_object_file(the_repository, + &trg_entry->idx.oid, &type, + &sz); + packing_data_unlock(&to_pack); + if (!trg->data) + die(_("object %s cannot be read"), + oid_to_hex(&trg_entry->idx.oid)); + if (sz != trg_size) + die(_("object %s inconsistent object length (%"PRIuMAX" vs %"PRIuMAX")"), + oid_to_hex(&trg_entry->idx.oid), (uintmax_t)sz, + (uintmax_t)trg_size); + *mem_usage += sz; + } + if (!src->data) { + packing_data_lock(&to_pack); + src->data = repo_read_object_file(the_repository, + &src_entry->idx.oid, &type, + &sz); + packing_data_unlock(&to_pack); + if (!src->data) { + if (src_entry->preferred_base) { + static int warned = 0; + if (!warned++) + warning(_("object %s cannot be read"), + oid_to_hex(&src_entry->idx.oid)); + /* + * Those objects are not included in the + * resulting pack. Be resilient and ignore + * them if they can't be read, in case the + * pack could be created nevertheless. + */ + return 0; + } + die(_("object %s cannot be read"), + oid_to_hex(&src_entry->idx.oid)); + } + if (sz != src_size) + die(_("object %s inconsistent object length (%"PRIuMAX" vs %"PRIuMAX")"), + oid_to_hex(&src_entry->idx.oid), (uintmax_t)sz, + (uintmax_t)src_size); + *mem_usage += sz; + } + if (!src->index) { + src->index = create_delta_index(src->data, src_size); + if (!src->index) { + static int warned = 0; + if (!warned++) + warning(_("suboptimal pack - out of memory")); + return 0; + } + *mem_usage += sizeof_delta_index(src->index); + } + + delta_buf = create_delta(src->index, trg->data, trg_size, &delta_size, max_size); + if (!delta_buf) + return 0; + + if (DELTA(trg_entry)) { + /* Prefer only shallower same-sized deltas. */ + if (delta_size == DELTA_SIZE(trg_entry) && + src->depth + 1 >= trg->depth) { + free(delta_buf); + return 0; + } + } + + /* + * Handle memory allocation outside of the cache + * accounting lock. Compiler will optimize the strangeness + * away when NO_PTHREADS is defined. + */ + free(trg_entry->delta_data); + cache_lock(); + if (trg_entry->delta_data) { + delta_cache_size -= DELTA_SIZE(trg_entry); + trg_entry->delta_data = NULL; + } + if (delta_cacheable(src_size, trg_size, delta_size)) { + delta_cache_size += delta_size; + cache_unlock(); + trg_entry->delta_data = xrealloc(delta_buf, delta_size); + } else { + cache_unlock(); + free(delta_buf); + } + + SET_DELTA(trg_entry, src_entry); + SET_DELTA_SIZE(trg_entry, delta_size); + trg->depth = src->depth + 1; + + return 1; +} + +static unsigned int check_delta_limit(struct object_entry *me, unsigned int n) +{ + struct object_entry *child = DELTA_CHILD(me); + unsigned int m = n; + while (child) { + const unsigned int c = check_delta_limit(child, n + 1); + if (m < c) + m = c; + child = DELTA_SIBLING(child); + } + return m; +} + +static unsigned long free_unpacked(struct unpacked *n) +{ + unsigned long freed_mem = sizeof_delta_index(n->index); + free_delta_index(n->index); + n->index = NULL; + if (n->data) { + freed_mem += SIZE(n->entry); + FREE_AND_NULL(n->data); + } + n->entry = NULL; + n->depth = 0; + return freed_mem; +} + +static void find_deltas(struct object_entry **list, unsigned *list_size, + int window, int depth, unsigned *processed) +{ + uint32_t i, idx = 0, count = 0; + struct unpacked *array; + unsigned long mem_usage = 0; + + CALLOC_ARRAY(array, window); + + for (;;) { + struct object_entry *entry; + struct unpacked *n = array + idx; + int j, max_depth, best_base = -1; + + progress_lock(); + if (!*list_size) { + progress_unlock(); + break; + } + entry = *list++; + (*list_size)--; + if (!entry->preferred_base) { + (*processed)++; + display_progress(progress_state, *processed); + } + progress_unlock(); + + mem_usage -= free_unpacked(n); + n->entry = entry; + + while (window_memory_limit && + mem_usage > window_memory_limit && + count > 1) { + const uint32_t tail = (idx + window - count) % window; + mem_usage -= free_unpacked(array + tail); + count--; + } + + /* We do not compute delta to *create* objects we are not + * going to pack. + */ + if (entry->preferred_base) + goto next; + + /* + * If the current object is at pack edge, take the depth the + * objects that depend on the current object into account + * otherwise they would become too deep. + */ + max_depth = depth; + if (DELTA_CHILD(entry)) { + max_depth -= check_delta_limit(entry, 0); + if (max_depth <= 0) + goto next; + } + + j = window; + while (--j > 0) { + int ret; + uint32_t other_idx = idx + j; + struct unpacked *m; + if (other_idx >= window) + other_idx -= window; + m = array + other_idx; + if (!m->entry) + break; + ret = try_delta(n, m, max_depth, &mem_usage); + if (ret < 0) + break; + else if (ret > 0) + best_base = other_idx; + } + + /* + * If we decided to cache the delta data, then it is best + * to compress it right away. First because we have to do + * it anyway, and doing it here while we're threaded will + * save a lot of time in the non threaded write phase, + * as well as allow for caching more deltas within + * the same cache size limit. + * ... + * But only if not writing to stdout, since in that case + * the network is most likely throttling writes anyway, + * and therefore it is best to go to the write phase ASAP + * instead, as we can afford spending more time compressing + * between writes at that moment. + */ + if (entry->delta_data && !pack_to_stdout) { + unsigned long size; + + size = do_compress(&entry->delta_data, DELTA_SIZE(entry)); + if (size < (1U << OE_Z_DELTA_BITS)) { + entry->z_delta_size = size; + cache_lock(); + delta_cache_size -= DELTA_SIZE(entry); + delta_cache_size += entry->z_delta_size; + cache_unlock(); + } else { + FREE_AND_NULL(entry->delta_data); + entry->z_delta_size = 0; + } + } + + /* if we made n a delta, and if n is already at max + * depth, leaving it in the window is pointless. we + * should evict it first. + */ + if (DELTA(entry) && max_depth <= n->depth) + continue; + + /* + * Move the best delta base up in the window, after the + * currently deltified object, to keep it longer. It will + * be the first base object to be attempted next. + */ + if (DELTA(entry)) { + struct unpacked swap = array[best_base]; + int dist = (window + idx - best_base) % window; + int dst = best_base; + while (dist--) { + int src = (dst + 1) % window; + array[dst] = array[src]; + dst = src; + } + array[dst] = swap; + } + + next: + idx++; + if (count + 1 < window) + count++; + if (idx >= window) + idx = 0; + } + + for (i = 0; i < window; ++i) { + free_delta_index(array[i].index); + free(array[i].data); + } + free(array); +} + +/* + * The main object list is split into smaller lists, each is handed to + * one worker. + * + * The main thread waits on the condition that (at least) one of the workers + * has stopped working (which is indicated in the .working member of + * struct thread_params). + * + * When a work thread has completed its work, it sets .working to 0 and + * signals the main thread and waits on the condition that .data_ready + * becomes 1. + * + * The main thread steals half of the work from the worker that has + * most work left to hand it to the idle worker. + */ + +struct thread_params { + pthread_t thread; + struct object_entry **list; + unsigned list_size; + unsigned remaining; + int window; + int depth; + int working; + int data_ready; + pthread_mutex_t mutex; + pthread_cond_t cond; + unsigned *processed; +}; + +static pthread_cond_t progress_cond; + +/* + * Mutex and conditional variable can't be statically-initialized on Windows. + */ +static void init_threaded_search(void) +{ + pthread_mutex_init(&cache_mutex, NULL); + pthread_mutex_init(&progress_mutex, NULL); + pthread_cond_init(&progress_cond, NULL); +} + +static void cleanup_threaded_search(void) +{ + pthread_cond_destroy(&progress_cond); + pthread_mutex_destroy(&cache_mutex); + pthread_mutex_destroy(&progress_mutex); +} + +static void *threaded_find_deltas(void *arg) +{ + struct thread_params *me = arg; + + progress_lock(); + while (me->remaining) { + progress_unlock(); + + find_deltas(me->list, &me->remaining, + me->window, me->depth, me->processed); + + progress_lock(); + me->working = 0; + pthread_cond_signal(&progress_cond); + progress_unlock(); + + /* + * We must not set ->data_ready before we wait on the + * condition because the main thread may have set it to 1 + * before we get here. In order to be sure that new + * work is available if we see 1 in ->data_ready, it + * was initialized to 0 before this thread was spawned + * and we reset it to 0 right away. + */ + pthread_mutex_lock(&me->mutex); + while (!me->data_ready) + pthread_cond_wait(&me->cond, &me->mutex); + me->data_ready = 0; + pthread_mutex_unlock(&me->mutex); + + progress_lock(); + } + progress_unlock(); + /* leave ->working 1 so that this doesn't get more work assigned */ + return NULL; +} + +static void ll_find_deltas(struct object_entry **list, unsigned list_size, + int window, int depth, unsigned *processed) +{ + struct thread_params *p; + int i, ret, active_threads = 0; + + init_threaded_search(); + + if (delta_search_threads <= 1) { + find_deltas(list, &list_size, window, depth, processed); + cleanup_threaded_search(); + return; + } + if (progress > pack_to_stdout) + fprintf_ln(stderr, _("Delta compression using up to %d threads"), + delta_search_threads); + CALLOC_ARRAY(p, delta_search_threads); + + /* Partition the work amongst work threads. */ + for (i = 0; i < delta_search_threads; i++) { + unsigned sub_size = list_size / (delta_search_threads - i); + + /* don't use too small segments or no deltas will be found */ + if (sub_size < 2*window && i+1 < delta_search_threads) + sub_size = 0; + + p[i].window = window; + p[i].depth = depth; + p[i].processed = processed; + p[i].working = 1; + p[i].data_ready = 0; + + /* try to split chunks on "path" boundaries */ + while (sub_size && sub_size < list_size && + list[sub_size]->hash && + list[sub_size]->hash == list[sub_size-1]->hash) + sub_size++; + + p[i].list = list; + p[i].list_size = sub_size; + p[i].remaining = sub_size; + + list += sub_size; + list_size -= sub_size; + } + + /* Start work threads. */ + for (i = 0; i < delta_search_threads; i++) { + if (!p[i].list_size) + continue; + pthread_mutex_init(&p[i].mutex, NULL); + pthread_cond_init(&p[i].cond, NULL); + ret = pthread_create(&p[i].thread, NULL, + threaded_find_deltas, &p[i]); + if (ret) + die(_("unable to create thread: %s"), strerror(ret)); + active_threads++; + } + + /* + * Now let's wait for work completion. Each time a thread is done + * with its work, we steal half of the remaining work from the + * thread with the largest number of unprocessed objects and give + * it to that newly idle thread. This ensure good load balancing + * until the remaining object list segments are simply too short + * to be worth splitting anymore. + */ + while (active_threads) { + struct thread_params *target = NULL; + struct thread_params *victim = NULL; + unsigned sub_size = 0; + + progress_lock(); + for (;;) { + for (i = 0; !target && i < delta_search_threads; i++) + if (!p[i].working) + target = &p[i]; + if (target) + break; + pthread_cond_wait(&progress_cond, &progress_mutex); + } + + for (i = 0; i < delta_search_threads; i++) + if (p[i].remaining > 2*window && + (!victim || victim->remaining < p[i].remaining)) + victim = &p[i]; + if (victim) { + sub_size = victim->remaining / 2; + list = victim->list + victim->list_size - sub_size; + while (sub_size && list[0]->hash && + list[0]->hash == list[-1]->hash) { + list++; + sub_size--; + } + if (!sub_size) { + /* + * It is possible for some "paths" to have + * so many objects that no hash boundary + * might be found. Let's just steal the + * exact half in that case. + */ + sub_size = victim->remaining / 2; + list -= sub_size; + } + target->list = list; + victim->list_size -= sub_size; + victim->remaining -= sub_size; + } + target->list_size = sub_size; + target->remaining = sub_size; + target->working = 1; + progress_unlock(); + + pthread_mutex_lock(&target->mutex); + target->data_ready = 1; + pthread_cond_signal(&target->cond); + pthread_mutex_unlock(&target->mutex); + + if (!sub_size) { + pthread_join(target->thread, NULL); + pthread_cond_destroy(&target->cond); + pthread_mutex_destroy(&target->mutex); + active_threads--; + } + } + cleanup_threaded_search(); + free(p); +} + +static int obj_is_packed(const struct object_id *oid) +{ + return packlist_find(&to_pack, oid) || + (reuse_packfile_bitmap && + bitmap_walk_contains(bitmap_git, reuse_packfile_bitmap, oid)); +} + +static void add_tag_chain(const struct object_id *oid) +{ + struct tag *tag; + + /* + * We catch duplicates already in add_object_entry(), but we'd + * prefer to do this extra check to avoid having to parse the + * tag at all if we already know that it's being packed (e.g., if + * it was included via bitmaps, we would not have parsed it + * previously). + */ + if (obj_is_packed(oid)) + return; + + tag = lookup_tag(the_repository, oid); + while (1) { + if (!tag || parse_tag(tag) || !tag->tagged) + die(_("unable to pack objects reachable from tag %s"), + oid_to_hex(oid)); + + add_object_entry(&tag->object.oid, OBJ_TAG, NULL, 0); + + if (tag->tagged->type != OBJ_TAG) + return; + + tag = (struct tag *)tag->tagged; + } +} + +static int add_ref_tag(const char *tag UNUSED, const struct object_id *oid, + int flag UNUSED, void *cb_data UNUSED) +{ + struct object_id peeled; + + if (!peel_iterated_oid(oid, &peeled) && obj_is_packed(&peeled)) + add_tag_chain(oid); + return 0; +} + +static void prepare_pack(int window, int depth) +{ + struct object_entry **delta_list; + uint32_t i, nr_deltas; + unsigned n; + + if (use_delta_islands) + resolve_tree_islands(the_repository, progress, &to_pack); + + get_object_details(); + + /* + * If we're locally repacking then we need to be doubly careful + * from now on in order to make sure no stealth corruption gets + * propagated to the new pack. Clients receiving streamed packs + * should validate everything they get anyway so no need to incur + * the additional cost here in that case. + */ + if (!pack_to_stdout) + do_check_packed_object_crc = 1; + + if (!to_pack.nr_objects || !window || !depth) + return; + + ALLOC_ARRAY(delta_list, to_pack.nr_objects); + nr_deltas = n = 0; + + for (i = 0; i < to_pack.nr_objects; i++) { + struct object_entry *entry = to_pack.objects + i; + + if (DELTA(entry)) + /* This happens if we decided to reuse existing + * delta from a pack. "reuse_delta &&" is implied. + */ + continue; + + if (!entry->type_valid || + oe_size_less_than(&to_pack, entry, 50)) + continue; + + if (entry->no_try_delta) + continue; + + if (!entry->preferred_base) { + nr_deltas++; + if (oe_type(entry) < 0) + die(_("unable to get type of object %s"), + oid_to_hex(&entry->idx.oid)); + } else { + if (oe_type(entry) < 0) { + /* + * This object is not found, but we + * don't have to include it anyway. + */ + continue; + } + } + + delta_list[n++] = entry; + } + + if (nr_deltas && n > 1) { + unsigned nr_done = 0; + + if (progress) + progress_state = start_progress(_("Compressing objects"), + nr_deltas); + QSORT(delta_list, n, type_size_sort); + ll_find_deltas(delta_list, n, window+1, depth, &nr_done); + stop_progress(&progress_state); + if (nr_done != nr_deltas) + die(_("inconsistency with delta count")); + } + free(delta_list); +} + +static int git_pack_config(const char *k, const char *v, + const struct config_context *ctx, void *cb) +{ + if (!strcmp(k, "pack.window")) { + window = git_config_int(k, v, ctx->kvi); + return 0; + } + if (!strcmp(k, "pack.windowmemory")) { + window_memory_limit = git_config_ulong(k, v, ctx->kvi); + return 0; + } + if (!strcmp(k, "pack.depth")) { + depth = git_config_int(k, v, ctx->kvi); + return 0; + } + if (!strcmp(k, "pack.deltacachesize")) { + max_delta_cache_size = git_config_int(k, v, ctx->kvi); + return 0; + } + if (!strcmp(k, "pack.deltacachelimit")) { + cache_max_small_delta_size = git_config_int(k, v, ctx->kvi); + return 0; + } + if (!strcmp(k, "pack.writebitmaphashcache")) { + if (git_config_bool(k, v)) + write_bitmap_options |= BITMAP_OPT_HASH_CACHE; + else + write_bitmap_options &= ~BITMAP_OPT_HASH_CACHE; + } + + if (!strcmp(k, "pack.writebitmaplookuptable")) { + if (git_config_bool(k, v)) + write_bitmap_options |= BITMAP_OPT_LOOKUP_TABLE; + else + write_bitmap_options &= ~BITMAP_OPT_LOOKUP_TABLE; + } + + if (!strcmp(k, "pack.usebitmaps")) { + use_bitmap_index_default = git_config_bool(k, v); + return 0; + } + if (!strcmp(k, "pack.allowpackreuse")) { + allow_pack_reuse = git_config_bool(k, v); + return 0; + } + if (!strcmp(k, "pack.threads")) { + delta_search_threads = git_config_int(k, v, ctx->kvi); + if (delta_search_threads < 0) + die(_("invalid number of threads specified (%d)"), + delta_search_threads); + if (!HAVE_THREADS && delta_search_threads != 1) { + warning(_("no threads support, ignoring %s"), k); + delta_search_threads = 0; + } + return 0; + } + if (!strcmp(k, "pack.indexversion")) { + pack_idx_opts.version = git_config_int(k, v, ctx->kvi); + if (pack_idx_opts.version > 2) + die(_("bad pack.indexVersion=%"PRIu32), + pack_idx_opts.version); + return 0; + } + if (!strcmp(k, "pack.writereverseindex")) { + if (git_config_bool(k, v)) + pack_idx_opts.flags |= WRITE_REV; + else + pack_idx_opts.flags &= ~WRITE_REV; + return 0; + } + if (!strcmp(k, "uploadpack.blobpackfileuri")) { + struct configured_exclusion *ex = xmalloc(sizeof(*ex)); + const char *oid_end, *pack_end; + /* + * Stores the pack hash. This is not a true object ID, but is + * of the same form. + */ + struct object_id pack_hash; + + if (parse_oid_hex(v, &ex->e.oid, &oid_end) || + *oid_end != ' ' || + parse_oid_hex(oid_end + 1, &pack_hash, &pack_end) || + *pack_end != ' ') + die(_("value of uploadpack.blobpackfileuri must be " + "of the form '<object-hash> <pack-hash> <uri>' (got '%s')"), v); + if (oidmap_get(&configured_exclusions, &ex->e.oid)) + die(_("object already configured in another " + "uploadpack.blobpackfileuri (got '%s')"), v); + ex->pack_hash_hex = xcalloc(1, pack_end - oid_end); + memcpy(ex->pack_hash_hex, oid_end + 1, pack_end - oid_end - 1); + ex->uri = xstrdup(pack_end + 1); + oidmap_put(&configured_exclusions, ex); + } + return git_default_config(k, v, ctx, cb); +} + +/* Counters for trace2 output when in --stdin-packs mode. */ +static int stdin_packs_found_nr; +static int stdin_packs_hints_nr; + +static int add_object_entry_from_pack(const struct object_id *oid, + struct packed_git *p, + uint32_t pos, + void *_data) +{ + off_t ofs; + enum object_type type = OBJ_NONE; + + display_progress(progress_state, ++nr_seen); + + if (have_duplicate_entry(oid, 0)) + return 0; + + ofs = nth_packed_object_offset(p, pos); + if (!want_object_in_pack(oid, 0, &p, &ofs)) + return 0; + + if (p) { + struct rev_info *revs = _data; + struct object_info oi = OBJECT_INFO_INIT; + + oi.typep = &type; + if (packed_object_info(the_repository, p, ofs, &oi) < 0) { + die(_("could not get type of object %s in pack %s"), + oid_to_hex(oid), p->pack_name); + } else if (type == OBJ_COMMIT) { + /* + * commits in included packs are used as starting points for the + * subsequent revision walk + */ + add_pending_oid(revs, NULL, oid, 0); + } + + stdin_packs_found_nr++; + } + + create_object_entry(oid, type, 0, 0, 0, p, ofs); + + return 0; +} + +static void show_commit_pack_hint(struct commit *commit UNUSED, + void *data UNUSED) +{ + /* nothing to do; commits don't have a namehash */ +} + +static void show_object_pack_hint(struct object *object, const char *name, + void *data UNUSED) +{ + struct object_entry *oe = packlist_find(&to_pack, &object->oid); + if (!oe) + return; + + /* + * Our 'to_pack' list was constructed by iterating all objects packed in + * included packs, and so doesn't have a non-zero hash field that you + * would typically pick up during a reachability traversal. + * + * Make a best-effort attempt to fill in the ->hash and ->no_try_delta + * here using a now in order to perhaps improve the delta selection + * process. + */ + oe->hash = pack_name_hash(name); + oe->no_try_delta = name && no_try_delta(name); + + stdin_packs_hints_nr++; +} + +static int pack_mtime_cmp(const void *_a, const void *_b) +{ + struct packed_git *a = ((const struct string_list_item*)_a)->util; + struct packed_git *b = ((const struct string_list_item*)_b)->util; + + /* + * order packs by descending mtime so that objects are laid out + * roughly as newest-to-oldest + */ + if (a->mtime < b->mtime) + return 1; + else if (b->mtime < a->mtime) + return -1; + else + return 0; +} + +static void read_packs_list_from_stdin(void) +{ + struct strbuf buf = STRBUF_INIT; + struct string_list include_packs = STRING_LIST_INIT_DUP; + struct string_list exclude_packs = STRING_LIST_INIT_DUP; + struct string_list_item *item = NULL; + + struct packed_git *p; + struct rev_info revs; + + repo_init_revisions(the_repository, &revs, NULL); + /* + * Use a revision walk to fill in the namehash of objects in the include + * packs. To save time, we'll avoid traversing through objects that are + * in excluded packs. + * + * That may cause us to avoid populating all of the namehash fields of + * all included objects, but our goal is best-effort, since this is only + * an optimization during delta selection. + */ + revs.no_kept_objects = 1; + revs.keep_pack_cache_flags |= IN_CORE_KEEP_PACKS; + revs.blob_objects = 1; + revs.tree_objects = 1; + revs.tag_objects = 1; + revs.ignore_missing_links = 1; + + while (strbuf_getline(&buf, stdin) != EOF) { + if (!buf.len) + continue; + + if (*buf.buf == '^') + string_list_append(&exclude_packs, buf.buf + 1); + else + string_list_append(&include_packs, buf.buf); + + strbuf_reset(&buf); + } + + string_list_sort(&include_packs); + string_list_remove_duplicates(&include_packs, 0); + string_list_sort(&exclude_packs); + string_list_remove_duplicates(&exclude_packs, 0); + + for (p = get_all_packs(the_repository); p; p = p->next) { + const char *pack_name = pack_basename(p); + + if ((item = string_list_lookup(&include_packs, pack_name))) + item->util = p; + if ((item = string_list_lookup(&exclude_packs, pack_name))) + item->util = p; + } + + /* + * Arguments we got on stdin may not even be packs. First + * check that to avoid segfaulting later on in + * e.g. pack_mtime_cmp(), excluded packs are handled below. + * + * Since we first parsed our STDIN and then sorted the input + * lines the pack we error on will be whatever line happens to + * sort first. This is lazy, it's enough that we report one + * bad case here, we don't need to report the first/last one, + * or all of them. + */ + for_each_string_list_item(item, &include_packs) { + struct packed_git *p = item->util; + if (!p) + die(_("could not find pack '%s'"), item->string); + if (!is_pack_valid(p)) + die(_("packfile %s cannot be accessed"), p->pack_name); + } + + /* + * Then, handle all of the excluded packs, marking them as + * kept in-core so that later calls to add_object_entry() + * discards any objects that are also found in excluded packs. + */ + for_each_string_list_item(item, &exclude_packs) { + struct packed_git *p = item->util; + if (!p) + die(_("could not find pack '%s'"), item->string); + p->pack_keep_in_core = 1; + } + + /* + * Order packs by ascending mtime; use QSORT directly to access the + * string_list_item's ->util pointer, which string_list_sort() does not + * provide. + */ + QSORT(include_packs.items, include_packs.nr, pack_mtime_cmp); + + for_each_string_list_item(item, &include_packs) { + struct packed_git *p = item->util; + for_each_object_in_pack(p, + add_object_entry_from_pack, + &revs, + FOR_EACH_OBJECT_PACK_ORDER); + } + + if (prepare_revision_walk(&revs)) + die(_("revision walk setup failed")); + traverse_commit_list(&revs, + show_commit_pack_hint, + show_object_pack_hint, + NULL); + + trace2_data_intmax("pack-objects", the_repository, "stdin_packs_found", + stdin_packs_found_nr); + trace2_data_intmax("pack-objects", the_repository, "stdin_packs_hints", + stdin_packs_hints_nr); + + strbuf_release(&buf); + string_list_clear(&include_packs, 0); + string_list_clear(&exclude_packs, 0); +} + +static void add_cruft_object_entry(const struct object_id *oid, enum object_type type, + struct packed_git *pack, off_t offset, + const char *name, uint32_t mtime) +{ + struct object_entry *entry; + + display_progress(progress_state, ++nr_seen); + + entry = packlist_find(&to_pack, oid); + if (entry) { + if (name) { + entry->hash = pack_name_hash(name); + entry->no_try_delta = no_try_delta(name); + } + } else { + if (!want_object_in_pack(oid, 0, &pack, &offset)) + return; + if (!pack && type == OBJ_BLOB && !has_loose_object(oid)) { + /* + * If a traversed tree has a missing blob then we want + * to avoid adding that missing object to our pack. + * + * This only applies to missing blobs, not trees, + * because the traversal needs to parse sub-trees but + * not blobs. + * + * Note we only perform this check when we couldn't + * already find the object in a pack, so we're really + * limited to "ensure non-tip blobs which don't exist in + * packs do exist via loose objects". Confused? + */ + return; + } + + entry = create_object_entry(oid, type, pack_name_hash(name), + 0, name && no_try_delta(name), + pack, offset); + } + + if (mtime > oe_cruft_mtime(&to_pack, entry)) + oe_set_cruft_mtime(&to_pack, entry, mtime); + return; +} + +static void show_cruft_object(struct object *obj, const char *name, void *data UNUSED) +{ + /* + * if we did not record it earlier, it's at least as old as our + * expiration value. Rather than find it exactly, just use that + * value. This may bump it forward from its real mtime, but it + * will still be "too old" next time we run with the same + * expiration. + * + * if obj does appear in the packing list, this call is a noop (or may + * set the namehash). + */ + add_cruft_object_entry(&obj->oid, obj->type, NULL, 0, name, cruft_expiration); +} + +static void show_cruft_commit(struct commit *commit, void *data) +{ + show_cruft_object((struct object*)commit, NULL, data); +} + +static int cruft_include_check_obj(struct object *obj, void *data UNUSED) +{ + return !has_object_kept_pack(&obj->oid, IN_CORE_KEEP_PACKS); +} + +static int cruft_include_check(struct commit *commit, void *data) +{ + return cruft_include_check_obj((struct object*)commit, data); +} + +static void set_cruft_mtime(const struct object *object, + struct packed_git *pack, + off_t offset, time_t mtime) +{ + add_cruft_object_entry(&object->oid, object->type, pack, offset, NULL, + mtime); +} + +static void mark_pack_kept_in_core(struct string_list *packs, unsigned keep) +{ + struct string_list_item *item = NULL; + for_each_string_list_item(item, packs) { + struct packed_git *p = item->util; + if (!p) + die(_("could not find pack '%s'"), item->string); + p->pack_keep_in_core = keep; + } +} + +static void add_unreachable_loose_objects(void); +static void add_objects_in_unpacked_packs(void); + +static void enumerate_cruft_objects(void) +{ + if (progress) + progress_state = start_progress(_("Enumerating cruft objects"), 0); + + add_objects_in_unpacked_packs(); + add_unreachable_loose_objects(); + + stop_progress(&progress_state); +} + +static void enumerate_and_traverse_cruft_objects(struct string_list *fresh_packs) +{ + struct packed_git *p; + struct rev_info revs; + int ret; + + repo_init_revisions(the_repository, &revs, NULL); + + revs.tag_objects = 1; + revs.tree_objects = 1; + revs.blob_objects = 1; + + revs.include_check = cruft_include_check; + revs.include_check_obj = cruft_include_check_obj; + + revs.ignore_missing_links = 1; + + if (progress) + progress_state = start_progress(_("Enumerating cruft objects"), 0); + ret = add_unseen_recent_objects_to_traversal(&revs, cruft_expiration, + set_cruft_mtime, 1); + stop_progress(&progress_state); + + if (ret) + die(_("unable to add cruft objects")); + + /* + * Re-mark only the fresh packs as kept so that objects in + * unknown packs do not halt the reachability traversal early. + */ + for (p = get_all_packs(the_repository); p; p = p->next) + p->pack_keep_in_core = 0; + mark_pack_kept_in_core(fresh_packs, 1); + + if (prepare_revision_walk(&revs)) + die(_("revision walk setup failed")); + if (progress) + progress_state = start_progress(_("Traversing cruft objects"), 0); + nr_seen = 0; + traverse_commit_list(&revs, show_cruft_commit, show_cruft_object, NULL); + + stop_progress(&progress_state); +} + +static void read_cruft_objects(void) +{ + struct strbuf buf = STRBUF_INIT; + struct string_list discard_packs = STRING_LIST_INIT_DUP; + struct string_list fresh_packs = STRING_LIST_INIT_DUP; + struct packed_git *p; + + ignore_packed_keep_in_core = 1; + + while (strbuf_getline(&buf, stdin) != EOF) { + if (!buf.len) + continue; + + if (*buf.buf == '-') + string_list_append(&discard_packs, buf.buf + 1); + else + string_list_append(&fresh_packs, buf.buf); + } + + string_list_sort(&discard_packs); + string_list_sort(&fresh_packs); + + for (p = get_all_packs(the_repository); p; p = p->next) { + const char *pack_name = pack_basename(p); + struct string_list_item *item; + + item = string_list_lookup(&fresh_packs, pack_name); + if (!item) + item = string_list_lookup(&discard_packs, pack_name); + + if (item) { + item->util = p; + } else { + /* + * This pack wasn't mentioned in either the "fresh" or + * "discard" list, so the caller didn't know about it. + * + * Mark it as kept so that its objects are ignored by + * add_unseen_recent_objects_to_traversal(). We'll + * unmark it before starting the traversal so it doesn't + * halt the traversal early. + */ + p->pack_keep_in_core = 1; + } + } + + mark_pack_kept_in_core(&fresh_packs, 1); + mark_pack_kept_in_core(&discard_packs, 0); + + if (cruft_expiration) + enumerate_and_traverse_cruft_objects(&fresh_packs); + else + enumerate_cruft_objects(); + + strbuf_release(&buf); + string_list_clear(&discard_packs, 0); + string_list_clear(&fresh_packs, 0); +} + +static void read_object_list_from_stdin(void) +{ + char line[GIT_MAX_HEXSZ + 1 + PATH_MAX + 2]; + struct object_id oid; + const char *p; + + for (;;) { + if (!fgets(line, sizeof(line), stdin)) { + if (feof(stdin)) + break; + if (!ferror(stdin)) + BUG("fgets returned NULL, not EOF, not error!"); + if (errno != EINTR) + die_errno("fgets"); + clearerr(stdin); + continue; + } + if (line[0] == '-') { + if (get_oid_hex(line+1, &oid)) + die(_("expected edge object ID, got garbage:\n %s"), + line); + add_preferred_base(&oid); + continue; + } + if (parse_oid_hex(line, &oid, &p)) + die(_("expected object ID, got garbage:\n %s"), line); + + add_preferred_base_object(p + 1); + add_object_entry(&oid, OBJ_NONE, p + 1, 0); + } +} + +static void show_commit(struct commit *commit, void *data UNUSED) +{ + add_object_entry(&commit->object.oid, OBJ_COMMIT, NULL, 0); + + if (write_bitmap_index) + index_commit_for_bitmap(commit); + + if (use_delta_islands) + propagate_island_marks(commit); +} + +static void show_object(struct object *obj, const char *name, + void *data UNUSED) +{ + add_preferred_base_object(name); + add_object_entry(&obj->oid, obj->type, name, 0); + + if (use_delta_islands) { + const char *p; + unsigned depth; + struct object_entry *ent; + + /* the empty string is a root tree, which is depth 0 */ + depth = *name ? 1 : 0; + for (p = strchr(name, '/'); p; p = strchr(p + 1, '/')) + depth++; + + ent = packlist_find(&to_pack, &obj->oid); + if (ent && depth > oe_tree_depth(&to_pack, ent)) + oe_set_tree_depth(&to_pack, ent, depth); + } +} + +static void show_object__ma_allow_any(struct object *obj, const char *name, void *data) +{ + assert(arg_missing_action == MA_ALLOW_ANY); + + /* + * Quietly ignore ALL missing objects. This avoids problems with + * staging them now and getting an odd error later. + */ + if (!has_object(the_repository, &obj->oid, 0)) + return; + + show_object(obj, name, data); +} + +static void show_object__ma_allow_promisor(struct object *obj, const char *name, void *data) +{ + assert(arg_missing_action == MA_ALLOW_PROMISOR); + + /* + * Quietly ignore EXPECTED missing objects. This avoids problems with + * staging them now and getting an odd error later. + */ + if (!has_object(the_repository, &obj->oid, 0) && is_promisor_object(&obj->oid)) + return; + + show_object(obj, name, data); +} + +static int option_parse_missing_action(const struct option *opt UNUSED, + const char *arg, int unset) +{ + assert(arg); + assert(!unset); + + if (!strcmp(arg, "error")) { + arg_missing_action = MA_ERROR; + fn_show_object = show_object; + return 0; + } + + if (!strcmp(arg, "allow-any")) { + arg_missing_action = MA_ALLOW_ANY; + fetch_if_missing = 0; + fn_show_object = show_object__ma_allow_any; + return 0; + } + + if (!strcmp(arg, "allow-promisor")) { + arg_missing_action = MA_ALLOW_PROMISOR; + fetch_if_missing = 0; + fn_show_object = show_object__ma_allow_promisor; + return 0; + } + + die(_("invalid value for '%s': '%s'"), "--missing", arg); + return 0; +} + +static void show_edge(struct commit *commit) +{ + add_preferred_base(&commit->object.oid); +} + +static int add_object_in_unpacked_pack(const struct object_id *oid, + struct packed_git *pack, + uint32_t pos, + void *data UNUSED) +{ + if (cruft) { + off_t offset; + time_t mtime; + + if (pack->is_cruft) { + if (load_pack_mtimes(pack) < 0) + die(_("could not load cruft pack .mtimes")); + mtime = nth_packed_mtime(pack, pos); + } else { + mtime = pack->mtime; + } + offset = nth_packed_object_offset(pack, pos); + + add_cruft_object_entry(oid, OBJ_NONE, pack, offset, + NULL, mtime); + } else { + add_object_entry(oid, OBJ_NONE, "", 0); + } + return 0; +} + +static void add_objects_in_unpacked_packs(void) +{ + if (for_each_packed_object(add_object_in_unpacked_pack, NULL, + FOR_EACH_OBJECT_PACK_ORDER | + FOR_EACH_OBJECT_LOCAL_ONLY | + FOR_EACH_OBJECT_SKIP_IN_CORE_KEPT_PACKS | + FOR_EACH_OBJECT_SKIP_ON_DISK_KEPT_PACKS)) + die(_("cannot open pack index")); +} + +static int add_loose_object(const struct object_id *oid, const char *path, + void *data UNUSED) +{ + enum object_type type = oid_object_info(the_repository, oid, NULL); + + if (type < 0) { + warning(_("loose object at %s could not be examined"), path); + return 0; + } + + if (cruft) { + struct stat st; + if (stat(path, &st) < 0) { + if (errno == ENOENT) + return 0; + return error_errno("unable to stat %s", oid_to_hex(oid)); + } + + add_cruft_object_entry(oid, type, NULL, 0, NULL, + st.st_mtime); + } else { + add_object_entry(oid, type, "", 0); + } + return 0; +} + +/* + * We actually don't even have to worry about reachability here. + * add_object_entry will weed out duplicates, so we just add every + * loose object we find. + */ +static void add_unreachable_loose_objects(void) +{ + for_each_loose_file_in_objdir(get_object_directory(), + add_loose_object, + NULL, NULL, NULL); +} + +static int has_sha1_pack_kept_or_nonlocal(const struct object_id *oid) +{ + static struct packed_git *last_found = (void *)1; + struct packed_git *p; + + p = (last_found != (void *)1) ? last_found : + get_all_packs(the_repository); + + while (p) { + if ((!p->pack_local || p->pack_keep || + p->pack_keep_in_core) && + find_pack_entry_one(oid->hash, p)) { + last_found = p; + return 1; + } + if (p == last_found) + p = get_all_packs(the_repository); + else + p = p->next; + if (p == last_found) + p = p->next; + } + return 0; +} + +/* + * Store a list of sha1s that are should not be discarded + * because they are either written too recently, or are + * reachable from another object that was. + * + * This is filled by get_object_list. + */ +static struct oid_array recent_objects; + +static int loosened_object_can_be_discarded(const struct object_id *oid, + timestamp_t mtime) +{ + if (!unpack_unreachable_expiration) + return 0; + if (mtime > unpack_unreachable_expiration) + return 0; + if (oid_array_lookup(&recent_objects, oid) >= 0) + return 0; + return 1; +} + +static void loosen_unused_packed_objects(void) +{ + struct packed_git *p; + uint32_t i; + uint32_t loosened_objects_nr = 0; + struct object_id oid; + + for (p = get_all_packs(the_repository); p; p = p->next) { + if (!p->pack_local || p->pack_keep || p->pack_keep_in_core) + continue; + + if (open_pack_index(p)) + die(_("cannot open pack index")); + + for (i = 0; i < p->num_objects; i++) { + nth_packed_object_id(&oid, p, i); + if (!packlist_find(&to_pack, &oid) && + !has_sha1_pack_kept_or_nonlocal(&oid) && + !loosened_object_can_be_discarded(&oid, p->mtime)) { + if (force_object_loose(&oid, p->mtime)) + die(_("unable to force loose object")); + loosened_objects_nr++; + } + } + } + + trace2_data_intmax("pack-objects", the_repository, + "loosen_unused_packed_objects/loosened", loosened_objects_nr); +} + +/* + * This tracks any options which pack-reuse code expects to be on, or which a + * reader of the pack might not understand, and which would therefore prevent + * blind reuse of what we have on disk. + */ +static int pack_options_allow_reuse(void) +{ + return allow_pack_reuse && + pack_to_stdout && + !ignore_packed_keep_on_disk && + !ignore_packed_keep_in_core && + (!local || !have_non_local_packs) && + !incremental; +} + +static int get_object_list_from_bitmap(struct rev_info *revs) +{ + if (!(bitmap_git = prepare_bitmap_walk(revs, 0))) + return -1; + + if (pack_options_allow_reuse() && + !reuse_partial_packfile_from_bitmap( + bitmap_git, + &reuse_packfile, + &reuse_packfile_objects, + &reuse_packfile_bitmap)) { + assert(reuse_packfile_objects); + nr_result += reuse_packfile_objects; + nr_seen += reuse_packfile_objects; + display_progress(progress_state, nr_seen); + } + + traverse_bitmap_commit_list(bitmap_git, revs, + &add_object_entry_from_bitmap); + return 0; +} + +static void record_recent_object(struct object *obj, + const char *name UNUSED, + void *data UNUSED) +{ + oid_array_append(&recent_objects, &obj->oid); +} + +static void record_recent_commit(struct commit *commit, void *data UNUSED) +{ + oid_array_append(&recent_objects, &commit->object.oid); +} + +static int mark_bitmap_preferred_tip(const char *refname, + const struct object_id *oid, + int flags UNUSED, + void *data UNUSED) +{ + struct object_id peeled; + struct object *object; + + if (!peel_iterated_oid(oid, &peeled)) + oid = &peeled; + + object = parse_object_or_die(oid, refname); + if (object->type == OBJ_COMMIT) + object->flags |= NEEDS_BITMAP; + + return 0; +} + +static void mark_bitmap_preferred_tips(void) +{ + struct string_list_item *item; + const struct string_list *preferred_tips; + + preferred_tips = bitmap_preferred_tips(the_repository); + if (!preferred_tips) + return; + + for_each_string_list_item(item, preferred_tips) { + for_each_ref_in(item->string, mark_bitmap_preferred_tip, NULL); + } +} + +static void get_object_list(struct rev_info *revs, int ac, const char **av) +{ + struct setup_revision_opt s_r_opt = { + .allow_exclude_promisor_objects = 1, + }; + char line[1000]; + int flags = 0; + int save_warning; + + save_commit_buffer = 0; + setup_revisions(ac, av, revs, &s_r_opt); + + /* make sure shallows are read */ + is_repository_shallow(the_repository); + + save_warning = warn_on_object_refname_ambiguity; + warn_on_object_refname_ambiguity = 0; + + while (fgets(line, sizeof(line), stdin) != NULL) { + int len = strlen(line); + if (len && line[len - 1] == '\n') + line[--len] = 0; + if (!len) + break; + if (*line == '-') { + if (!strcmp(line, "--not")) { + flags ^= UNINTERESTING; + write_bitmap_index = 0; + continue; + } + if (starts_with(line, "--shallow ")) { + struct object_id oid; + if (get_oid_hex(line + 10, &oid)) + die("not an object name '%s'", line + 10); + register_shallow(the_repository, &oid); + use_bitmap_index = 0; + continue; + } + die(_("not a rev '%s'"), line); + } + if (handle_revision_arg(line, revs, flags, REVARG_CANNOT_BE_FILENAME)) + die(_("bad revision '%s'"), line); + } + + warn_on_object_refname_ambiguity = save_warning; + + if (use_bitmap_index && !get_object_list_from_bitmap(revs)) + return; + + if (use_delta_islands) + load_delta_islands(the_repository, progress); + + if (write_bitmap_index) + mark_bitmap_preferred_tips(); + + if (prepare_revision_walk(revs)) + die(_("revision walk setup failed")); + mark_edges_uninteresting(revs, show_edge, sparse); + + if (!fn_show_object) + fn_show_object = show_object; + traverse_commit_list(revs, + show_commit, fn_show_object, + NULL); + + if (unpack_unreachable_expiration) { + revs->ignore_missing_links = 1; + if (add_unseen_recent_objects_to_traversal(revs, + unpack_unreachable_expiration, NULL, 0)) + die(_("unable to add recent objects")); + if (prepare_revision_walk(revs)) + die(_("revision walk setup failed")); + traverse_commit_list(revs, record_recent_commit, + record_recent_object, NULL); + } + + if (keep_unreachable) + add_objects_in_unpacked_packs(); + if (pack_loose_unreachable) + add_unreachable_loose_objects(); + if (unpack_unreachable) + loosen_unused_packed_objects(); + + oid_array_clear(&recent_objects); +} + +static void add_extra_kept_packs(const struct string_list *names) +{ + struct packed_git *p; + + if (!names->nr) + return; + + for (p = get_all_packs(the_repository); p; p = p->next) { + const char *name = basename(p->pack_name); + int i; + + if (!p->pack_local) + continue; + + for (i = 0; i < names->nr; i++) + if (!fspathcmp(name, names->items[i].string)) + break; + + if (i < names->nr) { + p->pack_keep_in_core = 1; + ignore_packed_keep_in_core = 1; + continue; + } + } +} + +static int option_parse_quiet(const struct option *opt, const char *arg, + int unset) +{ + int *val = opt->value; + + BUG_ON_OPT_ARG(arg); + + if (!unset) + *val = 0; + else if (!*val) + *val = 1; + return 0; +} + +static int option_parse_index_version(const struct option *opt, + const char *arg, int unset) +{ + struct pack_idx_option *popts = opt->value; + char *c; + const char *val = arg; + + BUG_ON_OPT_NEG(unset); + + popts->version = strtoul(val, &c, 10); + if (popts->version > 2) + die(_("unsupported index version %s"), val); + if (*c == ',' && c[1]) + popts->off32_limit = strtoul(c+1, &c, 0); + if (*c || popts->off32_limit & 0x80000000) + die(_("bad index version '%s'"), val); + return 0; +} + +static int option_parse_unpack_unreachable(const struct option *opt UNUSED, + const char *arg, int unset) +{ + if (unset) { + unpack_unreachable = 0; + unpack_unreachable_expiration = 0; + } + else { + unpack_unreachable = 1; + if (arg) + unpack_unreachable_expiration = approxidate(arg); + } + return 0; +} + +static int option_parse_cruft_expiration(const struct option *opt UNUSED, + const char *arg, int unset) +{ + if (unset) { + cruft = 0; + cruft_expiration = 0; + } else { + cruft = 1; + if (arg) + cruft_expiration = approxidate(arg); + } + return 0; +} + +int cmd_pack_objects(int argc, const char **argv, const char *prefix) +{ + int use_internal_rev_list = 0; + int shallow = 0; + int all_progress_implied = 0; + struct strvec rp = STRVEC_INIT; + int rev_list_unpacked = 0, rev_list_all = 0, rev_list_reflog = 0; + int rev_list_index = 0; + int stdin_packs = 0; + struct string_list keep_pack_list = STRING_LIST_INIT_NODUP; + struct list_objects_filter_options filter_options = + LIST_OBJECTS_FILTER_INIT; + + struct option pack_objects_options[] = { + OPT_CALLBACK_F('q', "quiet", &progress, NULL, + N_("do not show progress meter"), + PARSE_OPT_NOARG, option_parse_quiet), + OPT_SET_INT(0, "progress", &progress, + N_("show progress meter"), 1), + OPT_SET_INT(0, "all-progress", &progress, + N_("show progress meter during object writing phase"), 2), + OPT_BOOL(0, "all-progress-implied", + &all_progress_implied, + N_("similar to --all-progress when progress meter is shown")), + OPT_CALLBACK_F(0, "index-version", &pack_idx_opts, N_("<version>[,<offset>]"), + N_("write the pack index file in the specified idx format version"), + PARSE_OPT_NONEG, option_parse_index_version), + OPT_MAGNITUDE(0, "max-pack-size", &pack_size_limit, + N_("maximum size of each output pack file")), + OPT_BOOL(0, "local", &local, + N_("ignore borrowed objects from alternate object store")), + OPT_BOOL(0, "incremental", &incremental, + N_("ignore packed objects")), + OPT_INTEGER(0, "window", &window, + N_("limit pack window by objects")), + OPT_MAGNITUDE(0, "window-memory", &window_memory_limit, + N_("limit pack window by memory in addition to object limit")), + OPT_INTEGER(0, "depth", &depth, + N_("maximum length of delta chain allowed in the resulting pack")), + OPT_BOOL(0, "reuse-delta", &reuse_delta, + N_("reuse existing deltas")), + OPT_BOOL(0, "reuse-object", &reuse_object, + N_("reuse existing objects")), + OPT_BOOL(0, "delta-base-offset", &allow_ofs_delta, + N_("use OFS_DELTA objects")), + OPT_INTEGER(0, "threads", &delta_search_threads, + N_("use threads when searching for best delta matches")), + OPT_BOOL(0, "non-empty", &non_empty, + N_("do not create an empty pack output")), + OPT_BOOL(0, "revs", &use_internal_rev_list, + N_("read revision arguments from standard input")), + OPT_SET_INT_F(0, "unpacked", &rev_list_unpacked, + N_("limit the objects to those that are not yet packed"), + 1, PARSE_OPT_NONEG), + OPT_SET_INT_F(0, "all", &rev_list_all, + N_("include objects reachable from any reference"), + 1, PARSE_OPT_NONEG), + OPT_SET_INT_F(0, "reflog", &rev_list_reflog, + N_("include objects referred by reflog entries"), + 1, PARSE_OPT_NONEG), + OPT_SET_INT_F(0, "indexed-objects", &rev_list_index, + N_("include objects referred to by the index"), + 1, PARSE_OPT_NONEG), + OPT_BOOL(0, "stdin-packs", &stdin_packs, + N_("read packs from stdin")), + OPT_BOOL(0, "stdout", &pack_to_stdout, + N_("output pack to stdout")), + OPT_BOOL(0, "include-tag", &include_tag, + N_("include tag objects that refer to objects to be packed")), + OPT_BOOL(0, "keep-unreachable", &keep_unreachable, + N_("keep unreachable objects")), + OPT_BOOL(0, "pack-loose-unreachable", &pack_loose_unreachable, + N_("pack loose unreachable objects")), + OPT_CALLBACK_F(0, "unpack-unreachable", NULL, N_("time"), + N_("unpack unreachable objects newer than <time>"), + PARSE_OPT_OPTARG, option_parse_unpack_unreachable), + OPT_BOOL(0, "cruft", &cruft, N_("create a cruft pack")), + OPT_CALLBACK_F(0, "cruft-expiration", NULL, N_("time"), + N_("expire cruft objects older than <time>"), + PARSE_OPT_OPTARG, option_parse_cruft_expiration), + OPT_BOOL(0, "sparse", &sparse, + N_("use the sparse reachability algorithm")), + OPT_BOOL(0, "thin", &thin, + N_("create thin packs")), + OPT_BOOL(0, "shallow", &shallow, + N_("create packs suitable for shallow fetches")), + OPT_BOOL(0, "honor-pack-keep", &ignore_packed_keep_on_disk, + N_("ignore packs that have companion .keep file")), + OPT_STRING_LIST(0, "keep-pack", &keep_pack_list, N_("name"), + N_("ignore this pack")), + OPT_INTEGER(0, "compression", &pack_compression_level, + N_("pack compression level")), + OPT_BOOL(0, "keep-true-parents", &grafts_keep_true_parents, + N_("do not hide commits by grafts")), + OPT_BOOL(0, "use-bitmap-index", &use_bitmap_index, + N_("use a bitmap index if available to speed up counting objects")), + OPT_SET_INT(0, "write-bitmap-index", &write_bitmap_index, + N_("write a bitmap index together with the pack index"), + WRITE_BITMAP_TRUE), + OPT_SET_INT_F(0, "write-bitmap-index-quiet", + &write_bitmap_index, + N_("write a bitmap index if possible"), + WRITE_BITMAP_QUIET, PARSE_OPT_HIDDEN), + OPT_PARSE_LIST_OBJECTS_FILTER(&filter_options), + OPT_CALLBACK_F(0, "missing", NULL, N_("action"), + N_("handling for missing objects"), PARSE_OPT_NONEG, + option_parse_missing_action), + OPT_BOOL(0, "exclude-promisor-objects", &exclude_promisor_objects, + N_("do not pack objects in promisor packfiles")), + OPT_BOOL(0, "delta-islands", &use_delta_islands, + N_("respect islands during delta compression")), + OPT_STRING_LIST(0, "uri-protocol", &uri_protocols, + N_("protocol"), + N_("exclude any configured uploadpack.blobpackfileuri with this protocol")), + OPT_END(), + }; + + if (DFS_NUM_STATES > (1 << OE_DFS_STATE_BITS)) + BUG("too many dfs states, increase OE_DFS_STATE_BITS"); + + disable_replace_refs(); + + sparse = git_env_bool("GIT_TEST_PACK_SPARSE", -1); + if (the_repository->gitdir) { + prepare_repo_settings(the_repository); + if (sparse < 0) + sparse = the_repository->settings.pack_use_sparse; + } + + reset_pack_idx_option(&pack_idx_opts); + pack_idx_opts.flags |= WRITE_REV; + git_config(git_pack_config, NULL); + if (git_env_bool(GIT_TEST_NO_WRITE_REV_INDEX, 0)) + pack_idx_opts.flags &= ~WRITE_REV; + + progress = isatty(2); + argc = parse_options(argc, argv, prefix, pack_objects_options, + pack_usage, 0); + + if (argc) { + base_name = argv[0]; + argc--; + } + if (pack_to_stdout != !base_name || argc) + usage_with_options(pack_usage, pack_objects_options); + + if (depth < 0) + depth = 0; + if (depth >= (1 << OE_DEPTH_BITS)) { + warning(_("delta chain depth %d is too deep, forcing %d"), + depth, (1 << OE_DEPTH_BITS) - 1); + depth = (1 << OE_DEPTH_BITS) - 1; + } + if (cache_max_small_delta_size >= (1U << OE_Z_DELTA_BITS)) { + warning(_("pack.deltaCacheLimit is too high, forcing %d"), + (1U << OE_Z_DELTA_BITS) - 1); + cache_max_small_delta_size = (1U << OE_Z_DELTA_BITS) - 1; + } + if (window < 0) + window = 0; + + strvec_push(&rp, "pack-objects"); + if (thin) { + use_internal_rev_list = 1; + strvec_push(&rp, shallow + ? "--objects-edge-aggressive" + : "--objects-edge"); + } else + strvec_push(&rp, "--objects"); + + if (rev_list_all) { + use_internal_rev_list = 1; + strvec_push(&rp, "--all"); + } + if (rev_list_reflog) { + use_internal_rev_list = 1; + strvec_push(&rp, "--reflog"); + } + if (rev_list_index) { + use_internal_rev_list = 1; + strvec_push(&rp, "--indexed-objects"); + } + if (rev_list_unpacked && !stdin_packs) { + use_internal_rev_list = 1; + strvec_push(&rp, "--unpacked"); + } + + if (exclude_promisor_objects) { + use_internal_rev_list = 1; + fetch_if_missing = 0; + strvec_push(&rp, "--exclude-promisor-objects"); + } + if (unpack_unreachable || keep_unreachable || pack_loose_unreachable) + use_internal_rev_list = 1; + + if (!reuse_object) + reuse_delta = 0; + if (pack_compression_level == -1) + pack_compression_level = Z_DEFAULT_COMPRESSION; + else if (pack_compression_level < 0 || pack_compression_level > Z_BEST_COMPRESSION) + die(_("bad pack compression level %d"), pack_compression_level); + + if (!delta_search_threads) /* --threads=0 means autodetect */ + delta_search_threads = online_cpus(); + + if (!HAVE_THREADS && delta_search_threads != 1) + warning(_("no threads support, ignoring --threads")); + if (!pack_to_stdout && !pack_size_limit) + pack_size_limit = pack_size_limit_cfg; + if (pack_to_stdout && pack_size_limit) + die(_("--max-pack-size cannot be used to build a pack for transfer")); + if (pack_size_limit && pack_size_limit < 1024*1024) { + warning(_("minimum pack size limit is 1 MiB")); + pack_size_limit = 1024*1024; + } + + if (!pack_to_stdout && thin) + die(_("--thin cannot be used to build an indexable pack")); + + if (keep_unreachable && unpack_unreachable) + die(_("options '%s' and '%s' cannot be used together"), "--keep-unreachable", "--unpack-unreachable"); + if (!rev_list_all || !rev_list_reflog || !rev_list_index) + unpack_unreachable_expiration = 0; + + if (stdin_packs && filter_options.choice) + die(_("cannot use --filter with --stdin-packs")); + + if (stdin_packs && use_internal_rev_list) + die(_("cannot use internal rev list with --stdin-packs")); + + if (cruft) { + if (use_internal_rev_list) + die(_("cannot use internal rev list with --cruft")); + if (stdin_packs) + die(_("cannot use --stdin-packs with --cruft")); + } + + /* + * "soft" reasons not to use bitmaps - for on-disk repack by default we want + * + * - to produce good pack (with bitmap index not-yet-packed objects are + * packed in suboptimal order). + * + * - to use more robust pack-generation codepath (avoiding possible + * bugs in bitmap code and possible bitmap index corruption). + */ + if (!pack_to_stdout) + use_bitmap_index_default = 0; + + if (use_bitmap_index < 0) + use_bitmap_index = use_bitmap_index_default; + + /* "hard" reasons not to use bitmaps; these just won't work at all */ + if (!use_internal_rev_list || (!pack_to_stdout && write_bitmap_index) || is_repository_shallow(the_repository)) + use_bitmap_index = 0; + + if (pack_to_stdout || !rev_list_all) + write_bitmap_index = 0; + + if (use_delta_islands) + strvec_push(&rp, "--topo-order"); + + if (progress && all_progress_implied) + progress = 2; + + add_extra_kept_packs(&keep_pack_list); + if (ignore_packed_keep_on_disk) { + struct packed_git *p; + for (p = get_all_packs(the_repository); p; p = p->next) + if (p->pack_local && p->pack_keep) + break; + if (!p) /* no keep-able packs found */ + ignore_packed_keep_on_disk = 0; + } + if (local) { + /* + * unlike ignore_packed_keep_on_disk above, we do not + * want to unset "local" based on looking at packs, as + * it also covers non-local objects + */ + struct packed_git *p; + for (p = get_all_packs(the_repository); p; p = p->next) { + if (!p->pack_local) { + have_non_local_packs = 1; + break; + } + } + } + + trace2_region_enter("pack-objects", "enumerate-objects", + the_repository); + prepare_packing_data(the_repository, &to_pack); + + if (progress && !cruft) + progress_state = start_progress(_("Enumerating objects"), 0); + if (stdin_packs) { + /* avoids adding objects in excluded packs */ + ignore_packed_keep_in_core = 1; + read_packs_list_from_stdin(); + if (rev_list_unpacked) + add_unreachable_loose_objects(); + } else if (cruft) { + read_cruft_objects(); + } else if (!use_internal_rev_list) { + read_object_list_from_stdin(); + } else { + struct rev_info revs; + + repo_init_revisions(the_repository, &revs, NULL); + list_objects_filter_copy(&revs.filter, &filter_options); + get_object_list(&revs, rp.nr, rp.v); + release_revisions(&revs); + } + cleanup_preferred_base(); + if (include_tag && nr_result) + for_each_tag_ref(add_ref_tag, NULL); + stop_progress(&progress_state); + trace2_region_leave("pack-objects", "enumerate-objects", + the_repository); + + if (non_empty && !nr_result) + goto cleanup; + if (nr_result) { + trace2_region_enter("pack-objects", "prepare-pack", + the_repository); + prepare_pack(window, depth); + trace2_region_leave("pack-objects", "prepare-pack", + the_repository); + } + + trace2_region_enter("pack-objects", "write-pack-file", the_repository); + write_excluded_by_configs(); + write_pack_file(); + trace2_region_leave("pack-objects", "write-pack-file", the_repository); + + if (progress) + fprintf_ln(stderr, + _("Total %"PRIu32" (delta %"PRIu32")," + " reused %"PRIu32" (delta %"PRIu32")," + " pack-reused %"PRIu32), + written, written_delta, reused, reused_delta, + reuse_packfile_objects); + +cleanup: + list_objects_filter_release(&filter_options); + strvec_clear(&rp); + + return 0; +} diff --git a/builtin/pack-redundant.c b/builtin/pack-redundant.c new file mode 100644 index 0000000..4c735ba --- /dev/null +++ b/builtin/pack-redundant.c @@ -0,0 +1,675 @@ +/* +* +* Copyright 2005, Lukas Sandstrom <lukass@etek.chalmers.se> +* +* This file is licensed under the GPL v2. +* +*/ + +#include "builtin.h" +#include "gettext.h" +#include "hex.h" +#include "repository.h" +#include "packfile.h" +#include "object-store-ll.h" + +#define BLKSIZE 512 + +static const char pack_redundant_usage[] = +"git pack-redundant [--verbose] [--alt-odb] (--all | <pack-filename>...)"; + +static int load_all_packs, verbose, alt_odb; + +struct llist_item { + struct llist_item *next; + struct object_id oid; +}; +static struct llist { + struct llist_item *front; + struct llist_item *back; + size_t size; +} *all_objects; /* all objects which must be present in local packfiles */ + +static struct pack_list { + struct pack_list *next; + struct packed_git *pack; + struct llist *unique_objects; + struct llist *remaining_objects; + size_t all_objects_size; +} *local_packs = NULL, *altodb_packs = NULL; + +static struct llist_item *free_nodes; + +static inline void llist_item_put(struct llist_item *item) +{ + item->next = free_nodes; + free_nodes = item; +} + +static inline struct llist_item *llist_item_get(void) +{ + struct llist_item *new_item; + if ( free_nodes ) { + new_item = free_nodes; + free_nodes = free_nodes->next; + } else { + int i = 1; + ALLOC_ARRAY(new_item, BLKSIZE); + for (; i < BLKSIZE; i++) + llist_item_put(&new_item[i]); + } + return new_item; +} + +static inline void llist_init(struct llist **list) +{ + *list = xmalloc(sizeof(struct llist)); + (*list)->front = (*list)->back = NULL; + (*list)->size = 0; +} + +static struct llist * llist_copy(struct llist *list) +{ + struct llist *ret; + struct llist_item *new_item, *old_item, *prev; + + llist_init(&ret); + + if ((ret->size = list->size) == 0) + return ret; + + new_item = ret->front = llist_item_get(); + new_item->oid = list->front->oid; + + old_item = list->front->next; + while (old_item) { + prev = new_item; + new_item = llist_item_get(); + prev->next = new_item; + new_item->oid = old_item->oid; + old_item = old_item->next; + } + new_item->next = NULL; + ret->back = new_item; + + return ret; +} + +static inline struct llist_item *llist_insert(struct llist *list, + struct llist_item *after, + const unsigned char *oid) +{ + struct llist_item *new_item = llist_item_get(); + oidread(&new_item->oid, oid); + new_item->next = NULL; + + if (after) { + new_item->next = after->next; + after->next = new_item; + if (after == list->back) + list->back = new_item; + } else {/* insert in front */ + if (list->size == 0) + list->back = new_item; + else + new_item->next = list->front; + list->front = new_item; + } + list->size++; + return new_item; +} + +static inline struct llist_item *llist_insert_back(struct llist *list, + const unsigned char *oid) +{ + return llist_insert(list, list->back, oid); +} + +static inline struct llist_item *llist_insert_sorted_unique(struct llist *list, + const struct object_id *oid, struct llist_item *hint) +{ + struct llist_item *prev = NULL, *l; + + l = (hint == NULL) ? list->front : hint; + while (l) { + int cmp = oidcmp(&l->oid, oid); + if (cmp > 0) { /* we insert before this entry */ + return llist_insert(list, prev, oid->hash); + } + if (!cmp) { /* already exists */ + return l; + } + prev = l; + l = l->next; + } + /* insert at the end */ + return llist_insert_back(list, oid->hash); +} + +/* returns a pointer to an item in front of sha1 */ +static inline struct llist_item * llist_sorted_remove(struct llist *list, const unsigned char *oid, struct llist_item *hint) +{ + struct llist_item *prev, *l; + +redo_from_start: + l = (hint == NULL) ? list->front : hint; + prev = NULL; + while (l) { + const int cmp = hashcmp(l->oid.hash, oid); + if (cmp > 0) /* not in list, since sorted */ + return prev; + if (!cmp) { /* found */ + if (!prev) { + if (hint != NULL && hint != list->front) { + /* we don't know the previous element */ + hint = NULL; + goto redo_from_start; + } + list->front = l->next; + } else + prev->next = l->next; + if (l == list->back) + list->back = prev; + llist_item_put(l); + list->size--; + return prev; + } + prev = l; + l = l->next; + } + return prev; +} + +/* computes A\B */ +static void llist_sorted_difference_inplace(struct llist *A, + struct llist *B) +{ + struct llist_item *hint, *b; + + hint = NULL; + b = B->front; + + while (b) { + hint = llist_sorted_remove(A, b->oid.hash, hint); + b = b->next; + } +} + +static inline struct pack_list * pack_list_insert(struct pack_list **pl, + struct pack_list *entry) +{ + struct pack_list *p = xmalloc(sizeof(struct pack_list)); + memcpy(p, entry, sizeof(struct pack_list)); + p->next = *pl; + *pl = p; + return p; +} + +static inline size_t pack_list_size(struct pack_list *pl) +{ + size_t ret = 0; + while (pl) { + ret++; + pl = pl->next; + } + return ret; +} + +static struct pack_list * pack_list_difference(const struct pack_list *A, + const struct pack_list *B) +{ + struct pack_list *ret; + const struct pack_list *pl; + + if (!A) + return NULL; + + pl = B; + while (pl != NULL) { + if (A->pack == pl->pack) + return pack_list_difference(A->next, B); + pl = pl->next; + } + ret = xmalloc(sizeof(struct pack_list)); + memcpy(ret, A, sizeof(struct pack_list)); + ret->next = pack_list_difference(A->next, B); + return ret; +} + +static void cmp_two_packs(struct pack_list *p1, struct pack_list *p2) +{ + size_t p1_off = 0, p2_off = 0, p1_step, p2_step; + const unsigned char *p1_base, *p2_base; + struct llist_item *p1_hint = NULL, *p2_hint = NULL; + const unsigned int hashsz = the_hash_algo->rawsz; + + if (!p1->unique_objects) + p1->unique_objects = llist_copy(p1->remaining_objects); + if (!p2->unique_objects) + p2->unique_objects = llist_copy(p2->remaining_objects); + + p1_base = p1->pack->index_data; + p2_base = p2->pack->index_data; + p1_base += 256 * 4 + ((p1->pack->index_version < 2) ? 4 : 8); + p2_base += 256 * 4 + ((p2->pack->index_version < 2) ? 4 : 8); + p1_step = hashsz + ((p1->pack->index_version < 2) ? 4 : 0); + p2_step = hashsz + ((p2->pack->index_version < 2) ? 4 : 0); + + while (p1_off < p1->pack->num_objects * p1_step && + p2_off < p2->pack->num_objects * p2_step) + { + const int cmp = hashcmp(p1_base + p1_off, p2_base + p2_off); + /* cmp ~ p1 - p2 */ + if (cmp == 0) { + p1_hint = llist_sorted_remove(p1->unique_objects, + p1_base + p1_off, + p1_hint); + p2_hint = llist_sorted_remove(p2->unique_objects, + p1_base + p1_off, + p2_hint); + p1_off += p1_step; + p2_off += p2_step; + continue; + } + if (cmp < 0) { /* p1 has the object, p2 doesn't */ + p1_off += p1_step; + } else { /* p2 has the object, p1 doesn't */ + p2_off += p2_step; + } + } +} + +static size_t sizeof_union(struct packed_git *p1, struct packed_git *p2) +{ + size_t ret = 0; + size_t p1_off = 0, p2_off = 0, p1_step, p2_step; + const unsigned char *p1_base, *p2_base; + const unsigned int hashsz = the_hash_algo->rawsz; + + p1_base = p1->index_data; + p2_base = p2->index_data; + p1_base += 256 * 4 + ((p1->index_version < 2) ? 4 : 8); + p2_base += 256 * 4 + ((p2->index_version < 2) ? 4 : 8); + p1_step = hashsz + ((p1->index_version < 2) ? 4 : 0); + p2_step = hashsz + ((p2->index_version < 2) ? 4 : 0); + + while (p1_off < p1->num_objects * p1_step && + p2_off < p2->num_objects * p2_step) + { + int cmp = hashcmp(p1_base + p1_off, p2_base + p2_off); + /* cmp ~ p1 - p2 */ + if (cmp == 0) { + ret++; + p1_off += p1_step; + p2_off += p2_step; + continue; + } + if (cmp < 0) { /* p1 has the object, p2 doesn't */ + p1_off += p1_step; + } else { /* p2 has the object, p1 doesn't */ + p2_off += p2_step; + } + } + return ret; +} + +/* another O(n^2) function ... */ +static size_t get_pack_redundancy(struct pack_list *pl) +{ + struct pack_list *subset; + size_t ret = 0; + + if (!pl) + return 0; + + while ((subset = pl->next)) { + while (subset) { + ret += sizeof_union(pl->pack, subset->pack); + subset = subset->next; + } + pl = pl->next; + } + return ret; +} + +static inline off_t pack_set_bytecount(struct pack_list *pl) +{ + off_t ret = 0; + while (pl) { + ret += pl->pack->pack_size; + ret += pl->pack->index_size; + pl = pl->next; + } + return ret; +} + +static int cmp_remaining_objects(const void *a, const void *b) +{ + struct pack_list *pl_a = *((struct pack_list **)a); + struct pack_list *pl_b = *((struct pack_list **)b); + + if (pl_a->remaining_objects->size == pl_b->remaining_objects->size) { + /* have the same remaining_objects, big pack first */ + if (pl_a->all_objects_size == pl_b->all_objects_size) + return 0; + else if (pl_a->all_objects_size < pl_b->all_objects_size) + return 1; + else + return -1; + } else if (pl_a->remaining_objects->size < pl_b->remaining_objects->size) { + /* sort by remaining objects, more objects first */ + return 1; + } else { + return -1; + } +} + +/* Sort pack_list, greater size of remaining_objects first */ +static void sort_pack_list(struct pack_list **pl) +{ + struct pack_list **ary, *p; + int i; + size_t n = pack_list_size(*pl); + + if (n < 2) + return; + + /* prepare an array of packed_list for easier sorting */ + CALLOC_ARRAY(ary, n); + for (n = 0, p = *pl; p; p = p->next) + ary[n++] = p; + + QSORT(ary, n, cmp_remaining_objects); + + /* link them back again */ + for (i = 0; i < n - 1; i++) + ary[i]->next = ary[i + 1]; + ary[n - 1]->next = NULL; + *pl = ary[0]; + + free(ary); +} + + +static void minimize(struct pack_list **min) +{ + struct pack_list *pl, *unique = NULL, *non_unique = NULL; + struct llist *missing, *unique_pack_objects; + + pl = local_packs; + while (pl) { + if (pl->unique_objects->size) + pack_list_insert(&unique, pl); + else + pack_list_insert(&non_unique, pl); + pl = pl->next; + } + /* find out which objects are missing from the set of unique packs */ + missing = llist_copy(all_objects); + pl = unique; + while (pl) { + llist_sorted_difference_inplace(missing, pl->remaining_objects); + pl = pl->next; + } + + *min = unique; + + /* return if there are no objects missing from the unique set */ + if (missing->size == 0) { + free(missing); + return; + } + + unique_pack_objects = llist_copy(all_objects); + llist_sorted_difference_inplace(unique_pack_objects, missing); + + /* remove unique pack objects from the non_unique packs */ + pl = non_unique; + while (pl) { + llist_sorted_difference_inplace(pl->remaining_objects, unique_pack_objects); + pl = pl->next; + } + + while (non_unique) { + /* sort the non_unique packs, greater size of remaining_objects first */ + sort_pack_list(&non_unique); + if (non_unique->remaining_objects->size == 0) + break; + + pack_list_insert(min, non_unique); + + for (pl = non_unique->next; pl && pl->remaining_objects->size > 0; pl = pl->next) + llist_sorted_difference_inplace(pl->remaining_objects, non_unique->remaining_objects); + + non_unique = non_unique->next; + } +} + +static void load_all_objects(void) +{ + struct pack_list *pl = local_packs; + struct llist_item *hint, *l; + + llist_init(&all_objects); + + while (pl) { + hint = NULL; + l = pl->remaining_objects->front; + while (l) { + hint = llist_insert_sorted_unique(all_objects, + &l->oid, hint); + l = l->next; + } + pl = pl->next; + } + /* remove objects present in remote packs */ + pl = altodb_packs; + while (pl) { + llist_sorted_difference_inplace(all_objects, pl->remaining_objects); + pl = pl->next; + } +} + +/* this scales like O(n^2) */ +static void cmp_local_packs(void) +{ + struct pack_list *subset, *pl = local_packs; + + /* only one packfile */ + if (!pl->next) { + llist_init(&pl->unique_objects); + return; + } + + while ((subset = pl)) { + while ((subset = subset->next)) + cmp_two_packs(pl, subset); + pl = pl->next; + } +} + +static void scan_alt_odb_packs(void) +{ + struct pack_list *local, *alt; + + alt = altodb_packs; + while (alt) { + local = local_packs; + while (local) { + llist_sorted_difference_inplace(local->remaining_objects, + alt->remaining_objects); + local = local->next; + } + alt = alt->next; + } +} + +static struct pack_list * add_pack(struct packed_git *p) +{ + struct pack_list l; + size_t off = 0, step; + const unsigned char *base; + + if (!p->pack_local && !(alt_odb || verbose)) + return NULL; + + l.pack = p; + llist_init(&l.remaining_objects); + + if (open_pack_index(p)) + return NULL; + + base = p->index_data; + base += 256 * 4 + ((p->index_version < 2) ? 4 : 8); + step = the_hash_algo->rawsz + ((p->index_version < 2) ? 4 : 0); + while (off < p->num_objects * step) { + llist_insert_back(l.remaining_objects, base + off); + off += step; + } + l.all_objects_size = l.remaining_objects->size; + l.unique_objects = NULL; + if (p->pack_local) + return pack_list_insert(&local_packs, &l); + else + return pack_list_insert(&altodb_packs, &l); +} + +static struct pack_list * add_pack_file(const char *filename) +{ + struct packed_git *p = get_all_packs(the_repository); + + if (strlen(filename) < 40) + die("Bad pack filename: %s", filename); + + while (p) { + if (strstr(p->pack_name, filename)) + return add_pack(p); + p = p->next; + } + die("Filename %s not found in packed_git", filename); +} + +static void load_all(void) +{ + struct packed_git *p = get_all_packs(the_repository); + + while (p) { + add_pack(p); + p = p->next; + } +} + +int cmd_pack_redundant(int argc, const char **argv, const char *prefix UNUSED) +{ + int i; + int i_still_use_this = 0; + struct pack_list *min = NULL, *red, *pl; + struct llist *ignore; + struct object_id *oid; + char buf[GIT_MAX_HEXSZ + 2]; /* hex hash + \n + \0 */ + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage(pack_redundant_usage); + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (!strcmp(arg, "--")) { + i++; + break; + } + if (!strcmp(arg, "--all")) { + load_all_packs = 1; + continue; + } + if (!strcmp(arg, "--verbose")) { + verbose = 1; + continue; + } + if (!strcmp(arg, "--alt-odb")) { + alt_odb = 1; + continue; + } + if (!strcmp(arg, "--i-still-use-this")) { + i_still_use_this = 1; + continue; + } + if (*arg == '-') + usage(pack_redundant_usage); + else + break; + } + + if (!i_still_use_this) { + fputs(_("'git pack-redundant' is nominated for removal.\n" + "If you still use this command, please add an extra\n" + "option, '--i-still-use-this', on the command line\n" + "and let us know you still use it by sending an e-mail\n" + "to <git@vger.kernel.org>. Thanks.\n"), stderr); + die(_("refusing to run without --i-still-use-this")); + } + + if (load_all_packs) + load_all(); + else + while (*(argv + i) != NULL) + add_pack_file(*(argv + i++)); + + if (!local_packs) + die("Zero packs found!"); + + load_all_objects(); + + if (alt_odb) + scan_alt_odb_packs(); + + /* ignore objects given on stdin */ + llist_init(&ignore); + if (!isatty(0)) { + while (fgets(buf, sizeof(buf), stdin)) { + oid = xmalloc(sizeof(*oid)); + if (get_oid_hex(buf, oid)) + die("Bad object ID on stdin: %s", buf); + llist_insert_sorted_unique(ignore, oid, NULL); + } + } + llist_sorted_difference_inplace(all_objects, ignore); + pl = local_packs; + while (pl) { + llist_sorted_difference_inplace(pl->remaining_objects, ignore); + pl = pl->next; + } + + cmp_local_packs(); + + minimize(&min); + + if (verbose) { + fprintf(stderr, "There are %lu packs available in alt-odbs.\n", + (unsigned long)pack_list_size(altodb_packs)); + fprintf(stderr, "The smallest (bytewise) set of packs is:\n"); + pl = min; + while (pl) { + fprintf(stderr, "\t%s\n", pl->pack->pack_name); + pl = pl->next; + } + fprintf(stderr, "containing %lu duplicate objects " + "with a total size of %lukb.\n", + (unsigned long)get_pack_redundancy(min), + (unsigned long)pack_set_bytecount(min)/1024); + fprintf(stderr, "A total of %lu unique objects were considered.\n", + (unsigned long)all_objects->size); + fprintf(stderr, "Redundant packs (with indexes):\n"); + } + pl = red = pack_list_difference(local_packs, min); + while (pl) { + printf("%s\n%s\n", + sha1_pack_index_name(pl->pack->hash), + pl->pack->pack_name); + pl = pl->next; + } + if (verbose) + fprintf(stderr, "%luMB of redundant packs in total.\n", + (unsigned long)pack_set_bytecount(red)/(1024*1024)); + + return 0; +} diff --git a/builtin/pack-refs.c b/builtin/pack-refs.c new file mode 100644 index 0000000..bcf383c --- /dev/null +++ b/builtin/pack-refs.c @@ -0,0 +1,48 @@ +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "parse-options.h" +#include "refs.h" +#include "repository.h" +#include "revision.h" + +static char const * const pack_refs_usage[] = { + N_("git pack-refs [--all] [--no-prune] [--include <pattern>] [--exclude <pattern>]"), + NULL +}; + +int cmd_pack_refs(int argc, const char **argv, const char *prefix) +{ + unsigned int flags = PACK_REFS_PRUNE; + static struct ref_exclusions excludes = REF_EXCLUSIONS_INIT; + static struct string_list included_refs = STRING_LIST_INIT_NODUP; + struct pack_refs_opts pack_refs_opts = { .exclusions = &excludes, + .includes = &included_refs, + .flags = flags }; + static struct string_list option_excluded_refs = STRING_LIST_INIT_NODUP; + struct string_list_item *item; + + struct option opts[] = { + OPT_BIT(0, "all", &pack_refs_opts.flags, N_("pack everything"), PACK_REFS_ALL), + OPT_BIT(0, "prune", &pack_refs_opts.flags, N_("prune loose refs (default)"), PACK_REFS_PRUNE), + OPT_STRING_LIST(0, "include", pack_refs_opts.includes, N_("pattern"), + N_("references to include")), + OPT_STRING_LIST(0, "exclude", &option_excluded_refs, N_("pattern"), + N_("references to exclude")), + OPT_END(), + }; + git_config(git_default_config, NULL); + if (parse_options(argc, argv, prefix, opts, pack_refs_usage, 0)) + usage_with_options(pack_refs_usage, opts); + + for_each_string_list_item(item, &option_excluded_refs) + add_ref_exclusion(pack_refs_opts.exclusions, item->string); + + if (pack_refs_opts.flags & PACK_REFS_ALL) + string_list_append(pack_refs_opts.includes, "*"); + + if (!pack_refs_opts.includes->nr) + string_list_append(pack_refs_opts.includes, "refs/tags/*"); + + return refs_pack_refs(get_main_ref_store(the_repository), &pack_refs_opts); +} diff --git a/builtin/patch-id.c b/builtin/patch-id.c new file mode 100644 index 0000000..3894d2b --- /dev/null +++ b/builtin/patch-id.c @@ -0,0 +1,243 @@ +#include "builtin.h" +#include "config.h" +#include "diff.h" +#include "gettext.h" +#include "hash.h" +#include "hex.h" +#include "parse-options.h" + +static void flush_current_id(int patchlen, struct object_id *id, struct object_id *result) +{ + if (patchlen) + printf("%s %s\n", oid_to_hex(result), oid_to_hex(id)); +} + +static int remove_space(char *line) +{ + char *src = line; + char *dst = line; + unsigned char c; + + while ((c = *src++) != '\0') { + if (!isspace(c)) + *dst++ = c; + } + return dst - line; +} + +static int scan_hunk_header(const char *p, int *p_before, int *p_after) +{ + static const char digits[] = "0123456789"; + const char *q, *r; + int n; + + q = p + 4; + n = strspn(q, digits); + if (q[n] == ',') { + q += n + 1; + *p_before = atoi(q); + n = strspn(q, digits); + } else { + *p_before = 1; + } + + if (n == 0 || q[n] != ' ' || q[n+1] != '+') + return 0; + + r = q + n + 2; + n = strspn(r, digits); + if (r[n] == ',') { + r += n + 1; + *p_after = atoi(r); + n = strspn(r, digits); + } else { + *p_after = 1; + } + if (n == 0) + return 0; + + return 1; +} + +static int get_one_patchid(struct object_id *next_oid, struct object_id *result, + struct strbuf *line_buf, int stable, int verbatim) +{ + int patchlen = 0, found_next = 0; + int before = -1, after = -1; + int diff_is_binary = 0; + char pre_oid_str[GIT_MAX_HEXSZ + 1], post_oid_str[GIT_MAX_HEXSZ + 1]; + git_hash_ctx ctx; + + the_hash_algo->init_fn(&ctx); + oidclr(result); + + while (strbuf_getwholeline(line_buf, stdin, '\n') != EOF) { + char *line = line_buf->buf; + const char *p = line; + int len; + + /* Possibly skip over the prefix added by "log" or "format-patch" */ + if (!skip_prefix(line, "commit ", &p) && + !skip_prefix(line, "From ", &p) && + starts_with(line, "\\ ") && 12 < strlen(line)) { + if (verbatim) + the_hash_algo->update_fn(&ctx, line, strlen(line)); + continue; + } + + if (!get_oid_hex(p, next_oid)) { + found_next = 1; + break; + } + + /* Ignore commit comments */ + if (!patchlen && !starts_with(line, "diff ")) + continue; + + /* Parsing diff header? */ + if (before == -1) { + if (starts_with(line, "GIT binary patch") || + starts_with(line, "Binary files")) { + diff_is_binary = 1; + before = 0; + the_hash_algo->update_fn(&ctx, pre_oid_str, + strlen(pre_oid_str)); + the_hash_algo->update_fn(&ctx, post_oid_str, + strlen(post_oid_str)); + if (stable) + flush_one_hunk(result, &ctx); + continue; + } else if (skip_prefix(line, "index ", &p)) { + char *oid1_end = strstr(line, ".."); + char *oid2_end = NULL; + if (oid1_end) + oid2_end = strstr(oid1_end, " "); + if (!oid2_end) + oid2_end = line + strlen(line) - 1; + if (oid1_end != NULL && oid2_end != NULL) { + *oid1_end = *oid2_end = '\0'; + strlcpy(pre_oid_str, p, GIT_MAX_HEXSZ + 1); + strlcpy(post_oid_str, oid1_end + 2, GIT_MAX_HEXSZ + 1); + } + continue; + } else if (starts_with(line, "--- ")) + before = after = 1; + else if (!isalpha(line[0])) + break; + } + + if (diff_is_binary) { + if (starts_with(line, "diff ")) { + diff_is_binary = 0; + before = -1; + } + continue; + } + + /* Looking for a valid hunk header? */ + if (before == 0 && after == 0) { + if (starts_with(line, "@@ -")) { + /* Parse next hunk, but ignore line numbers. */ + scan_hunk_header(line, &before, &after); + continue; + } + + /* Split at the end of the patch. */ + if (!starts_with(line, "diff ")) + break; + + /* Else we're parsing another header. */ + if (stable) + flush_one_hunk(result, &ctx); + before = after = -1; + } + + /* If we get here, we're inside a hunk. */ + if (line[0] == '-' || line[0] == ' ') + before--; + if (line[0] == '+' || line[0] == ' ') + after--; + + /* Add line to hash algo (possibly removing whitespace) */ + len = verbatim ? strlen(line) : remove_space(line); + patchlen += len; + the_hash_algo->update_fn(&ctx, line, len); + } + + if (!found_next) + oidclr(next_oid); + + flush_one_hunk(result, &ctx); + + return patchlen; +} + +static void generate_id_list(int stable, int verbatim) +{ + struct object_id oid, n, result; + int patchlen; + struct strbuf line_buf = STRBUF_INIT; + + oidclr(&oid); + while (!feof(stdin)) { + patchlen = get_one_patchid(&n, &result, &line_buf, stable, verbatim); + flush_current_id(patchlen, &oid, &result); + oidcpy(&oid, &n); + } + strbuf_release(&line_buf); +} + +static const char *const patch_id_usage[] = { + N_("git patch-id [--stable | --unstable | --verbatim]"), NULL +}; + +struct patch_id_opts { + int stable; + int verbatim; +}; + +static int git_patch_id_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + struct patch_id_opts *opts = cb; + + if (!strcmp(var, "patchid.stable")) { + opts->stable = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "patchid.verbatim")) { + opts->verbatim = git_config_bool(var, value); + return 0; + } + + return git_default_config(var, value, ctx, cb); +} + +int cmd_patch_id(int argc, const char **argv, const char *prefix) +{ + /* if nothing is set, default to unstable */ + struct patch_id_opts config = {0, 0}; + int opts = 0; + struct option builtin_patch_id_options[] = { + OPT_CMDMODE(0, "unstable", &opts, + N_("use the unstable patch-id algorithm"), 1), + OPT_CMDMODE(0, "stable", &opts, + N_("use the stable patch-id algorithm"), 2), + OPT_CMDMODE(0, "verbatim", &opts, + N_("don't strip whitespace from the patch"), 3), + OPT_END() + }; + + git_config(git_patch_id_config, &config); + + /* verbatim implies stable */ + if (config.verbatim) + config.stable = 1; + + argc = parse_options(argc, argv, prefix, builtin_patch_id_options, + patch_id_usage, 0); + + generate_id_list(opts ? opts > 1 : config.stable, + opts ? opts == 3 : config.verbatim); + return 0; +} diff --git a/builtin/prune-packed.c b/builtin/prune-packed.c new file mode 100644 index 0000000..ca3578e --- /dev/null +++ b/builtin/prune-packed.c @@ -0,0 +1,32 @@ +#include "builtin.h" +#include "gettext.h" +#include "parse-options.h" +#include "prune-packed.h" + +static const char * const prune_packed_usage[] = { + "git prune-packed [-n | --dry-run] [-q | --quiet]", + NULL +}; + +int cmd_prune_packed(int argc, const char **argv, const char *prefix) +{ + int opts = isatty(2) ? PRUNE_PACKED_VERBOSE : 0; + const struct option prune_packed_options[] = { + OPT_BIT('n', "dry-run", &opts, N_("dry run"), + PRUNE_PACKED_DRY_RUN), + OPT_NEGBIT('q', "quiet", &opts, N_("be quiet"), + PRUNE_PACKED_VERBOSE), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, prune_packed_options, + prune_packed_usage, 0); + + if (argc > 0) + usage_msg_opt(_("too many arguments"), + prune_packed_usage, + prune_packed_options); + + prune_packed_objects(opts); + return 0; +} diff --git a/builtin/prune.c b/builtin/prune.c new file mode 100644 index 0000000..57fe314 --- /dev/null +++ b/builtin/prune.c @@ -0,0 +1,212 @@ +#include "builtin.h" +#include "commit.h" +#include "diff.h" +#include "dir.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "revision.h" +#include "reachable.h" +#include "parse-options.h" +#include "path.h" +#include "progress.h" +#include "prune-packed.h" +#include "replace-object.h" +#include "object-file.h" +#include "object-name.h" +#include "object-store-ll.h" +#include "shallow.h" + +static const char * const prune_usage[] = { + N_("git prune [-n] [-v] [--progress] [--expire <time>] [--] [<head>...]"), + NULL +}; +static int show_only; +static int verbose; +static timestamp_t expire; +static int show_progress = -1; + +static int prune_tmp_file(const char *fullpath) +{ + struct stat st; + if (lstat(fullpath, &st)) + return error("Could not stat '%s'", fullpath); + if (st.st_mtime > expire) + return 0; + if (S_ISDIR(st.st_mode)) { + if (show_only || verbose) + printf("Removing stale temporary directory %s\n", fullpath); + if (!show_only) { + struct strbuf remove_dir_buf = STRBUF_INIT; + + strbuf_addstr(&remove_dir_buf, fullpath); + remove_dir_recursively(&remove_dir_buf, 0); + strbuf_release(&remove_dir_buf); + } + } else { + if (show_only || verbose) + printf("Removing stale temporary file %s\n", fullpath); + if (!show_only) + unlink_or_warn(fullpath); + } + return 0; +} + +static void perform_reachability_traversal(struct rev_info *revs) +{ + static int initialized; + struct progress *progress = NULL; + + if (initialized) + return; + + if (show_progress) + progress = start_delayed_progress(_("Checking connectivity"), 0); + mark_reachable_objects(revs, 1, expire, progress); + stop_progress(&progress); + initialized = 1; +} + +static int is_object_reachable(const struct object_id *oid, + struct rev_info *revs) +{ + struct object *obj; + + perform_reachability_traversal(revs); + + obj = lookup_object(the_repository, oid); + return obj && (obj->flags & SEEN); +} + +static int prune_object(const struct object_id *oid, const char *fullpath, + void *data) +{ + struct rev_info *revs = data; + struct stat st; + + if (is_object_reachable(oid, revs)) + return 0; + + if (lstat(fullpath, &st)) { + /* report errors, but do not stop pruning */ + error("Could not stat '%s'", fullpath); + return 0; + } + if (st.st_mtime > expire) + return 0; + if (show_only || verbose) { + enum object_type type = oid_object_info(the_repository, oid, + NULL); + printf("%s %s\n", oid_to_hex(oid), + (type > 0) ? type_name(type) : "unknown"); + } + if (!show_only) + unlink_or_warn(fullpath); + return 0; +} + +static int prune_cruft(const char *basename, const char *path, + void *data UNUSED) +{ + if (starts_with(basename, "tmp_obj_")) + prune_tmp_file(path); + else + fprintf(stderr, "bad sha1 file: %s\n", path); + return 0; +} + +static int prune_subdir(unsigned int nr UNUSED, const char *path, + void *data UNUSED) +{ + if (!show_only) + rmdir(path); + return 0; +} + +/* + * Write errors (particularly out of space) can result in + * failed temporary packs (and more rarely indexes and other + * files beginning with "tmp_") accumulating in the object + * and the pack directories. + */ +static void remove_temporary_files(const char *path) +{ + DIR *dir; + struct dirent *de; + + dir = opendir(path); + if (!dir) { + if (errno != ENOENT) + fprintf(stderr, "Unable to open directory %s: %s\n", + path, strerror(errno)); + return; + } + while ((de = readdir(dir)) != NULL) + if (starts_with(de->d_name, "tmp_")) + prune_tmp_file(mkpath("%s/%s", path, de->d_name)); + closedir(dir); +} + +int cmd_prune(int argc, const char **argv, const char *prefix) +{ + struct rev_info revs; + int exclude_promisor_objects = 0; + const struct option options[] = { + OPT__DRY_RUN(&show_only, N_("do not remove, show only")), + OPT__VERBOSE(&verbose, N_("report pruned objects")), + OPT_BOOL(0, "progress", &show_progress, N_("show progress")), + OPT_EXPIRY_DATE(0, "expire", &expire, + N_("expire objects older than <time>")), + OPT_BOOL(0, "exclude-promisor-objects", &exclude_promisor_objects, + N_("limit traversal to objects outside promisor packfiles")), + OPT_END() + }; + char *s; + + expire = TIME_MAX; + save_commit_buffer = 0; + disable_replace_refs(); + repo_init_revisions(the_repository, &revs, prefix); + + argc = parse_options(argc, argv, prefix, options, prune_usage, 0); + + if (repository_format_precious_objects) + die(_("cannot prune in a precious-objects repo")); + + while (argc--) { + struct object_id oid; + const char *name = *argv++; + + if (!repo_get_oid(the_repository, name, &oid)) { + struct object *object = parse_object_or_die(&oid, + name); + add_pending_object(&revs, object, ""); + } + else + die("unrecognized argument: %s", name); + } + + if (show_progress == -1) + show_progress = isatty(2); + if (exclude_promisor_objects) { + fetch_if_missing = 0; + revs.exclude_promisor_objects = 1; + } + + for_each_loose_file_in_objdir(get_object_directory(), prune_object, + prune_cruft, prune_subdir, &revs); + + prune_packed_objects(show_only ? PRUNE_PACKED_DRY_RUN : 0); + remove_temporary_files(get_object_directory()); + s = mkpathdup("%s/pack", get_object_directory()); + remove_temporary_files(s); + free(s); + + if (is_repository_shallow(the_repository)) { + perform_reachability_traversal(&revs); + prune_shallow(show_only ? PRUNE_SHOW_ONLY : 0); + } + + release_revisions(&revs); + return 0; +} diff --git a/builtin/pull.c b/builtin/pull.c new file mode 100644 index 0000000..be2b2c9 --- /dev/null +++ b/builtin/pull.c @@ -0,0 +1,1160 @@ +/* + * Builtin "git pull" + * + * Based on git-pull.sh by Junio C Hamano + * + * Fetch one or more remote refs and merge it/them into the current HEAD. + */ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "advice.h" +#include "config.h" +#include "gettext.h" +#include "hex.h" +#include "merge.h" +#include "object-name.h" +#include "parse-options.h" +#include "exec-cmd.h" +#include "run-command.h" +#include "oid-array.h" +#include "remote.h" +#include "dir.h" +#include "path.h" +#include "read-cache-ll.h" +#include "rebase.h" +#include "refs.h" +#include "refspec.h" +#include "revision.h" +#include "submodule.h" +#include "submodule-config.h" +#include "tempfile.h" +#include "lockfile.h" +#include "wt-status.h" +#include "commit-reach.h" +#include "sequencer.h" +#include "packfile.h" + +/** + * Parses the value of --rebase. If value is a false value, returns + * REBASE_FALSE. If value is a true value, returns REBASE_TRUE. If value is + * "merges", returns REBASE_MERGES. If value is a invalid value, dies with + * a fatal error if fatal is true, otherwise returns REBASE_INVALID. + */ +static enum rebase_type parse_config_rebase(const char *key, const char *value, + int fatal) +{ + enum rebase_type v = rebase_parse_value(value); + if (v != REBASE_INVALID) + return v; + + if (fatal) + die(_("invalid value for '%s': '%s'"), key, value); + else + error(_("invalid value for '%s': '%s'"), key, value); + + return REBASE_INVALID; +} + +/** + * Callback for --rebase, which parses arg with parse_config_rebase(). + */ +static int parse_opt_rebase(const struct option *opt, const char *arg, int unset) +{ + enum rebase_type *value = opt->value; + + if (arg) + *value = parse_config_rebase("--rebase", arg, 0); + else + *value = unset ? REBASE_FALSE : REBASE_TRUE; + return *value == REBASE_INVALID ? -1 : 0; +} + +static const char * const pull_usage[] = { + N_("git pull [<options>] [<repository> [<refspec>...]]"), + NULL +}; + +/* Shared options */ +static int opt_verbosity; +static char *opt_progress; +static int recurse_submodules = RECURSE_SUBMODULES_DEFAULT; +static int recurse_submodules_cli = RECURSE_SUBMODULES_DEFAULT; + +/* Options passed to git-merge or git-rebase */ +static enum rebase_type opt_rebase = -1; +static char *opt_diffstat; +static char *opt_log; +static char *opt_signoff; +static char *opt_squash; +static char *opt_commit; +static char *opt_edit; +static char *cleanup_arg; +static char *opt_ff; +static char *opt_verify_signatures; +static char *opt_verify; +static int opt_autostash = -1; +static int config_autostash; +static int check_trust_level = 1; +static struct strvec opt_strategies = STRVEC_INIT; +static struct strvec opt_strategy_opts = STRVEC_INIT; +static char *opt_gpg_sign; +static int opt_allow_unrelated_histories; + +/* Options passed to git-fetch */ +static char *opt_all; +static char *opt_append; +static char *opt_upload_pack; +static int opt_force; +static char *opt_tags; +static char *opt_prune; +static char *max_children; +static int opt_dry_run; +static char *opt_keep; +static char *opt_depth; +static char *opt_unshallow; +static char *opt_update_shallow; +static char *opt_refmap; +static char *opt_ipv4; +static char *opt_ipv6; +static int opt_show_forced_updates = -1; +static char *set_upstream; +static struct strvec opt_fetch = STRVEC_INIT; + +static struct option pull_options[] = { + /* Shared options */ + OPT__VERBOSITY(&opt_verbosity), + OPT_PASSTHRU(0, "progress", &opt_progress, NULL, + N_("force progress reporting"), + PARSE_OPT_NOARG), + OPT_CALLBACK_F(0, "recurse-submodules", + &recurse_submodules_cli, N_("on-demand"), + N_("control for recursive fetching of submodules"), + PARSE_OPT_OPTARG, option_fetch_parse_recurse_submodules), + + /* Options passed to git-merge or git-rebase */ + OPT_GROUP(N_("Options related to merging")), + OPT_CALLBACK_F('r', "rebase", &opt_rebase, + "(false|true|merges|interactive)", + N_("incorporate changes by rebasing rather than merging"), + PARSE_OPT_OPTARG, parse_opt_rebase), + OPT_PASSTHRU('n', NULL, &opt_diffstat, NULL, + N_("do not show a diffstat at the end of the merge"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG), + OPT_PASSTHRU(0, "stat", &opt_diffstat, NULL, + N_("show a diffstat at the end of the merge"), + PARSE_OPT_NOARG), + OPT_PASSTHRU(0, "summary", &opt_diffstat, NULL, + N_("(synonym to --stat)"), + PARSE_OPT_NOARG | PARSE_OPT_HIDDEN), + OPT_PASSTHRU(0, "log", &opt_log, N_("n"), + N_("add (at most <n>) entries from shortlog to merge commit message"), + PARSE_OPT_OPTARG), + OPT_PASSTHRU(0, "signoff", &opt_signoff, NULL, + N_("add a Signed-off-by trailer"), + PARSE_OPT_OPTARG), + OPT_PASSTHRU(0, "squash", &opt_squash, NULL, + N_("create a single commit instead of doing a merge"), + PARSE_OPT_NOARG), + OPT_PASSTHRU(0, "commit", &opt_commit, NULL, + N_("perform a commit if the merge succeeds (default)"), + PARSE_OPT_NOARG), + OPT_PASSTHRU(0, "edit", &opt_edit, NULL, + N_("edit message before committing"), + PARSE_OPT_NOARG), + OPT_CLEANUP(&cleanup_arg), + OPT_PASSTHRU(0, "ff", &opt_ff, NULL, + N_("allow fast-forward"), + PARSE_OPT_NOARG), + OPT_PASSTHRU(0, "ff-only", &opt_ff, NULL, + N_("abort if fast-forward is not possible"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG), + OPT_PASSTHRU(0, "verify", &opt_verify, NULL, + N_("control use of pre-merge-commit and commit-msg hooks"), + PARSE_OPT_NOARG), + OPT_PASSTHRU(0, "verify-signatures", &opt_verify_signatures, NULL, + N_("verify that the named commit has a valid GPG signature"), + PARSE_OPT_NOARG), + OPT_BOOL(0, "autostash", &opt_autostash, + N_("automatically stash/stash pop before and after")), + OPT_PASSTHRU_ARGV('s', "strategy", &opt_strategies, N_("strategy"), + N_("merge strategy to use"), + 0), + OPT_PASSTHRU_ARGV('X', "strategy-option", &opt_strategy_opts, + N_("option=value"), + N_("option for selected merge strategy"), + 0), + OPT_PASSTHRU('S', "gpg-sign", &opt_gpg_sign, N_("key-id"), + N_("GPG sign commit"), + PARSE_OPT_OPTARG), + OPT_SET_INT(0, "allow-unrelated-histories", + &opt_allow_unrelated_histories, + N_("allow merging unrelated histories"), 1), + + /* Options passed to git-fetch */ + OPT_GROUP(N_("Options related to fetching")), + OPT_PASSTHRU(0, "all", &opt_all, NULL, + N_("fetch from all remotes"), + PARSE_OPT_NOARG), + OPT_PASSTHRU('a', "append", &opt_append, NULL, + N_("append to .git/FETCH_HEAD instead of overwriting"), + PARSE_OPT_NOARG), + OPT_PASSTHRU(0, "upload-pack", &opt_upload_pack, N_("path"), + N_("path to upload pack on remote end"), + 0), + OPT__FORCE(&opt_force, N_("force overwrite of local branch"), 0), + OPT_PASSTHRU('t', "tags", &opt_tags, NULL, + N_("fetch all tags and associated objects"), + PARSE_OPT_NOARG), + OPT_PASSTHRU('p', "prune", &opt_prune, NULL, + N_("prune remote-tracking branches no longer on remote"), + PARSE_OPT_NOARG), + OPT_PASSTHRU('j', "jobs", &max_children, N_("n"), + N_("number of submodules pulled in parallel"), + PARSE_OPT_OPTARG), + OPT_BOOL(0, "dry-run", &opt_dry_run, + N_("dry run")), + OPT_PASSTHRU('k', "keep", &opt_keep, NULL, + N_("keep downloaded pack"), + PARSE_OPT_NOARG), + OPT_PASSTHRU(0, "depth", &opt_depth, N_("depth"), + N_("deepen history of shallow clone"), + 0), + OPT_PASSTHRU_ARGV(0, "shallow-since", &opt_fetch, N_("time"), + N_("deepen history of shallow repository based on time"), + 0), + OPT_PASSTHRU_ARGV(0, "shallow-exclude", &opt_fetch, N_("revision"), + N_("deepen history of shallow clone, excluding rev"), + 0), + OPT_PASSTHRU_ARGV(0, "deepen", &opt_fetch, N_("n"), + N_("deepen history of shallow clone"), + 0), + OPT_PASSTHRU(0, "unshallow", &opt_unshallow, NULL, + N_("convert to a complete repository"), + PARSE_OPT_NONEG | PARSE_OPT_NOARG), + OPT_PASSTHRU(0, "update-shallow", &opt_update_shallow, NULL, + N_("accept refs that update .git/shallow"), + PARSE_OPT_NOARG), + OPT_PASSTHRU(0, "refmap", &opt_refmap, N_("refmap"), + N_("specify fetch refmap"), + PARSE_OPT_NONEG), + OPT_PASSTHRU_ARGV('o', "server-option", &opt_fetch, + N_("server-specific"), + N_("option to transmit"), + 0), + OPT_PASSTHRU('4', "ipv4", &opt_ipv4, NULL, + N_("use IPv4 addresses only"), + PARSE_OPT_NOARG), + OPT_PASSTHRU('6', "ipv6", &opt_ipv6, NULL, + N_("use IPv6 addresses only"), + PARSE_OPT_NOARG), + OPT_PASSTHRU_ARGV(0, "negotiation-tip", &opt_fetch, N_("revision"), + N_("report that we have only objects reachable from this object"), + 0), + OPT_BOOL(0, "show-forced-updates", &opt_show_forced_updates, + N_("check for forced-updates on all updated branches")), + OPT_PASSTHRU(0, "set-upstream", &set_upstream, NULL, + N_("set upstream for git pull/fetch"), + PARSE_OPT_NOARG), + + OPT_END() +}; + +/** + * Pushes "-q" or "-v" switches into arr to match the opt_verbosity level. + */ +static void argv_push_verbosity(struct strvec *arr) +{ + int verbosity; + + for (verbosity = opt_verbosity; verbosity > 0; verbosity--) + strvec_push(arr, "-v"); + + for (verbosity = opt_verbosity; verbosity < 0; verbosity++) + strvec_push(arr, "-q"); +} + +/** + * Pushes "-f" switches into arr to match the opt_force level. + */ +static void argv_push_force(struct strvec *arr) +{ + int force = opt_force; + while (force-- > 0) + strvec_push(arr, "-f"); +} + +/** + * Sets the GIT_REFLOG_ACTION environment variable to the concatenation of argv + */ +static void set_reflog_message(int argc, const char **argv) +{ + int i; + struct strbuf msg = STRBUF_INIT; + + for (i = 0; i < argc; i++) { + if (i) + strbuf_addch(&msg, ' '); + strbuf_addstr(&msg, argv[i]); + } + + setenv("GIT_REFLOG_ACTION", msg.buf, 0); + + strbuf_release(&msg); +} + +/** + * If pull.ff is unset, returns NULL. If pull.ff is "true", returns "--ff". If + * pull.ff is "false", returns "--no-ff". If pull.ff is "only", returns + * "--ff-only". Otherwise, if pull.ff is set to an invalid value, die with an + * error. + */ +static const char *config_get_ff(void) +{ + const char *value; + + if (git_config_get_value("pull.ff", &value)) + return NULL; + + switch (git_parse_maybe_bool(value)) { + case 0: + return "--no-ff"; + case 1: + return "--ff"; + } + + if (!strcmp(value, "only")) + return "--ff-only"; + + die(_("invalid value for '%s': '%s'"), "pull.ff", value); +} + +/** + * Returns the default configured value for --rebase. It first looks for the + * value of "branch.$curr_branch.rebase", where $curr_branch is the current + * branch, and if HEAD is detached or the configuration key does not exist, + * looks for the value of "pull.rebase". If both configuration keys do not + * exist, returns REBASE_FALSE. + */ +static enum rebase_type config_get_rebase(int *rebase_unspecified) +{ + struct branch *curr_branch = branch_get("HEAD"); + const char *value; + + if (curr_branch) { + char *key = xstrfmt("branch.%s.rebase", curr_branch->name); + + if (!git_config_get_value(key, &value)) { + enum rebase_type ret = parse_config_rebase(key, value, 1); + free(key); + return ret; + } + + free(key); + } + + if (!git_config_get_value("pull.rebase", &value)) + return parse_config_rebase("pull.rebase", value, 1); + + *rebase_unspecified = 1; + + return REBASE_FALSE; +} + +/** + * Read config variables. + */ +static int git_pull_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + if (!strcmp(var, "rebase.autostash")) { + config_autostash = git_config_bool(var, value); + return 0; + } else if (!strcmp(var, "submodule.recurse")) { + recurse_submodules = git_config_bool(var, value) ? + RECURSE_SUBMODULES_ON : RECURSE_SUBMODULES_OFF; + return 0; + } else if (!strcmp(var, "gpg.mintrustlevel")) { + check_trust_level = 0; + } + + return git_default_config(var, value, ctx, cb); +} + +/** + * Appends merge candidates from FETCH_HEAD that are not marked not-for-merge + * into merge_heads. + */ +static void get_merge_heads(struct oid_array *merge_heads) +{ + const char *filename = git_path_fetch_head(the_repository); + FILE *fp; + struct strbuf sb = STRBUF_INIT; + struct object_id oid; + + fp = xfopen(filename, "r"); + while (strbuf_getline_lf(&sb, fp) != EOF) { + const char *p; + if (parse_oid_hex(sb.buf, &oid, &p)) + continue; /* invalid line: does not start with object ID */ + if (starts_with(p, "\tnot-for-merge\t")) + continue; /* ref is not-for-merge */ + oid_array_append(merge_heads, &oid); + } + fclose(fp); + strbuf_release(&sb); +} + +/** + * Used by die_no_merge_candidates() as a for_each_remote() callback to + * retrieve the name of the remote if the repository only has one remote. + */ +static int get_only_remote(struct remote *remote, void *cb_data) +{ + const char **remote_name = cb_data; + + if (*remote_name) + return -1; + + *remote_name = remote->name; + return 0; +} + +/** + * Dies with the appropriate reason for why there are no merge candidates: + * + * 1. We fetched from a specific remote, and a refspec was given, but it ended + * up not fetching anything. This is usually because the user provided a + * wildcard refspec which had no matches on the remote end. + * + * 2. We fetched from a non-default remote, but didn't specify a branch to + * merge. We can't use the configured one because it applies to the default + * remote, thus the user must specify the branches to merge. + * + * 3. We fetched from the branch's or repo's default remote, but: + * + * a. We are not on a branch, so there will never be a configured branch to + * merge with. + * + * b. We are on a branch, but there is no configured branch to merge with. + * + * 4. We fetched from the branch's or repo's default remote, but the configured + * branch to merge didn't get fetched. (Either it doesn't exist, or wasn't + * part of the configured fetch refspec.) + */ +static void NORETURN die_no_merge_candidates(const char *repo, const char **refspecs) +{ + struct branch *curr_branch = branch_get("HEAD"); + const char *remote = curr_branch ? curr_branch->remote_name : NULL; + + if (*refspecs) { + if (opt_rebase) + fprintf_ln(stderr, _("There is no candidate for rebasing against among the refs that you just fetched.")); + else + fprintf_ln(stderr, _("There are no candidates for merging among the refs that you just fetched.")); + fprintf_ln(stderr, _("Generally this means that you provided a wildcard refspec which had no\n" + "matches on the remote end.")); + } else if (repo && curr_branch && (!remote || strcmp(repo, remote))) { + fprintf_ln(stderr, _("You asked to pull from the remote '%s', but did not specify\n" + "a branch. Because this is not the default configured remote\n" + "for your current branch, you must specify a branch on the command line."), + repo); + } else if (!curr_branch) { + fprintf_ln(stderr, _("You are not currently on a branch.")); + if (opt_rebase) + fprintf_ln(stderr, _("Please specify which branch you want to rebase against.")); + else + fprintf_ln(stderr, _("Please specify which branch you want to merge with.")); + fprintf_ln(stderr, _("See git-pull(1) for details.")); + fprintf(stderr, "\n"); + fprintf_ln(stderr, " git pull %s %s", _("<remote>"), _("<branch>")); + fprintf(stderr, "\n"); + } else if (!curr_branch->merge_nr) { + const char *remote_name = NULL; + + if (for_each_remote(get_only_remote, &remote_name) || !remote_name) + remote_name = _("<remote>"); + + fprintf_ln(stderr, _("There is no tracking information for the current branch.")); + if (opt_rebase) + fprintf_ln(stderr, _("Please specify which branch you want to rebase against.")); + else + fprintf_ln(stderr, _("Please specify which branch you want to merge with.")); + fprintf_ln(stderr, _("See git-pull(1) for details.")); + fprintf(stderr, "\n"); + fprintf_ln(stderr, " git pull %s %s", _("<remote>"), _("<branch>")); + fprintf(stderr, "\n"); + fprintf_ln(stderr, _("If you wish to set tracking information for this branch you can do so with:")); + fprintf(stderr, "\n"); + fprintf_ln(stderr, " git branch --set-upstream-to=%s/%s %s\n", + remote_name, _("<branch>"), curr_branch->name); + } else + fprintf_ln(stderr, _("Your configuration specifies to merge with the ref '%s'\n" + "from the remote, but no such ref was fetched."), + *curr_branch->merge_name); + exit(1); +} + +/** + * Parses argv into [<repo> [<refspecs>...]], returning their values in `repo` + * as a string and `refspecs` as a null-terminated array of strings. If `repo` + * is not provided in argv, it is set to NULL. + */ +static void parse_repo_refspecs(int argc, const char **argv, const char **repo, + const char ***refspecs) +{ + if (argc > 0) { + *repo = *argv++; + argc--; + } else + *repo = NULL; + *refspecs = argv; +} + +/** + * Runs git-fetch, returning its exit status. `repo` and `refspecs` are the + * repository and refspecs to fetch, or NULL if they are not provided. + */ +static int run_fetch(const char *repo, const char **refspecs) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + + strvec_pushl(&cmd.args, "fetch", "--update-head-ok", NULL); + + /* Shared options */ + argv_push_verbosity(&cmd.args); + if (opt_progress) + strvec_push(&cmd.args, opt_progress); + + /* Options passed to git-fetch */ + if (opt_all) + strvec_push(&cmd.args, opt_all); + if (opt_append) + strvec_push(&cmd.args, opt_append); + if (opt_upload_pack) + strvec_push(&cmd.args, opt_upload_pack); + argv_push_force(&cmd.args); + if (opt_tags) + strvec_push(&cmd.args, opt_tags); + if (opt_prune) + strvec_push(&cmd.args, opt_prune); + if (recurse_submodules_cli != RECURSE_SUBMODULES_DEFAULT) + switch (recurse_submodules_cli) { + case RECURSE_SUBMODULES_ON: + strvec_push(&cmd.args, "--recurse-submodules=on"); + break; + case RECURSE_SUBMODULES_OFF: + strvec_push(&cmd.args, "--recurse-submodules=no"); + break; + case RECURSE_SUBMODULES_ON_DEMAND: + strvec_push(&cmd.args, "--recurse-submodules=on-demand"); + break; + default: + BUG("submodule recursion option not understood"); + } + if (max_children) + strvec_push(&cmd.args, max_children); + if (opt_dry_run) + strvec_push(&cmd.args, "--dry-run"); + if (opt_keep) + strvec_push(&cmd.args, opt_keep); + if (opt_depth) + strvec_push(&cmd.args, opt_depth); + if (opt_unshallow) + strvec_push(&cmd.args, opt_unshallow); + if (opt_update_shallow) + strvec_push(&cmd.args, opt_update_shallow); + if (opt_refmap) + strvec_push(&cmd.args, opt_refmap); + if (opt_ipv4) + strvec_push(&cmd.args, opt_ipv4); + if (opt_ipv6) + strvec_push(&cmd.args, opt_ipv6); + if (opt_show_forced_updates > 0) + strvec_push(&cmd.args, "--show-forced-updates"); + else if (opt_show_forced_updates == 0) + strvec_push(&cmd.args, "--no-show-forced-updates"); + if (set_upstream) + strvec_push(&cmd.args, set_upstream); + strvec_pushv(&cmd.args, opt_fetch.v); + + if (repo) { + strvec_push(&cmd.args, repo); + strvec_pushv(&cmd.args, refspecs); + } else if (*refspecs) + BUG("refspecs without repo?"); + cmd.git_cmd = 1; + cmd.close_object_store = 1; + return run_command(&cmd); +} + +/** + * "Pulls into void" by branching off merge_head. + */ +static int pull_into_void(const struct object_id *merge_head, + const struct object_id *curr_head) +{ + if (opt_verify_signatures) { + struct commit *commit; + + commit = lookup_commit(the_repository, merge_head); + if (!commit) + die(_("unable to access commit %s"), + oid_to_hex(merge_head)); + + verify_merge_signature(commit, opt_verbosity, + check_trust_level); + } + + /* + * Two-way merge: we treat the index as based on an empty tree, + * and try to fast-forward to HEAD. This ensures we will not lose + * index/worktree changes that the user already made on the unborn + * branch. + */ + if (checkout_fast_forward(the_repository, + the_hash_algo->empty_tree, + merge_head, 0)) + return 1; + + if (update_ref("initial pull", "HEAD", merge_head, curr_head, 0, UPDATE_REFS_DIE_ON_ERR)) + return 1; + + return 0; +} + +static int rebase_submodules(void) +{ + struct child_process cp = CHILD_PROCESS_INIT; + + cp.git_cmd = 1; + cp.no_stdin = 1; + strvec_pushl(&cp.args, "submodule", "update", + "--recursive", "--rebase", NULL); + argv_push_verbosity(&cp.args); + + return run_command(&cp); +} + +static int update_submodules(void) +{ + struct child_process cp = CHILD_PROCESS_INIT; + + cp.git_cmd = 1; + cp.no_stdin = 1; + strvec_pushl(&cp.args, "submodule", "update", + "--recursive", "--checkout", NULL); + argv_push_verbosity(&cp.args); + + return run_command(&cp); +} + +/** + * Runs git-merge, returning its exit status. + */ +static int run_merge(void) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + + strvec_pushl(&cmd.args, "merge", NULL); + + /* Shared options */ + argv_push_verbosity(&cmd.args); + if (opt_progress) + strvec_push(&cmd.args, opt_progress); + + /* Options passed to git-merge */ + if (opt_diffstat) + strvec_push(&cmd.args, opt_diffstat); + if (opt_log) + strvec_push(&cmd.args, opt_log); + if (opt_signoff) + strvec_push(&cmd.args, opt_signoff); + if (opt_squash) + strvec_push(&cmd.args, opt_squash); + if (opt_commit) + strvec_push(&cmd.args, opt_commit); + if (opt_edit) + strvec_push(&cmd.args, opt_edit); + if (cleanup_arg) + strvec_pushf(&cmd.args, "--cleanup=%s", cleanup_arg); + if (opt_ff) + strvec_push(&cmd.args, opt_ff); + if (opt_verify) + strvec_push(&cmd.args, opt_verify); + if (opt_verify_signatures) + strvec_push(&cmd.args, opt_verify_signatures); + strvec_pushv(&cmd.args, opt_strategies.v); + strvec_pushv(&cmd.args, opt_strategy_opts.v); + if (opt_gpg_sign) + strvec_push(&cmd.args, opt_gpg_sign); + if (opt_autostash == 0) + strvec_push(&cmd.args, "--no-autostash"); + else if (opt_autostash == 1) + strvec_push(&cmd.args, "--autostash"); + if (opt_allow_unrelated_histories > 0) + strvec_push(&cmd.args, "--allow-unrelated-histories"); + + strvec_push(&cmd.args, "FETCH_HEAD"); + cmd.git_cmd = 1; + return run_command(&cmd); +} + +/** + * Returns remote's upstream branch for the current branch. If remote is NULL, + * the current branch's configured default remote is used. Returns NULL if + * `remote` does not name a valid remote, HEAD does not point to a branch, + * remote is not the branch's configured remote or the branch does not have any + * configured upstream branch. + */ +static const char *get_upstream_branch(const char *remote) +{ + struct remote *rm; + struct branch *curr_branch; + const char *curr_branch_remote; + + rm = remote_get(remote); + if (!rm) + return NULL; + + curr_branch = branch_get("HEAD"); + if (!curr_branch) + return NULL; + + curr_branch_remote = remote_for_branch(curr_branch, NULL); + assert(curr_branch_remote); + + if (strcmp(curr_branch_remote, rm->name)) + return NULL; + + return branch_get_upstream(curr_branch, NULL); +} + +/** + * Derives the remote-tracking branch from the remote and refspec. + * + * FIXME: The current implementation assumes the default mapping of + * refs/heads/<branch_name> to refs/remotes/<remote_name>/<branch_name>. + */ +static const char *get_tracking_branch(const char *remote, const char *refspec) +{ + struct refspec_item spec; + const char *spec_src; + const char *merge_branch; + + refspec_item_init_or_die(&spec, refspec, REFSPEC_FETCH); + spec_src = spec.src; + if (!*spec_src || !strcmp(spec_src, "HEAD")) + spec_src = "HEAD"; + else if (skip_prefix(spec_src, "heads/", &spec_src)) + ; + else if (skip_prefix(spec_src, "refs/heads/", &spec_src)) + ; + else if (starts_with(spec_src, "refs/") || + starts_with(spec_src, "tags/") || + starts_with(spec_src, "remotes/")) + spec_src = ""; + + if (*spec_src) { + if (!strcmp(remote, ".")) + merge_branch = mkpath("refs/heads/%s", spec_src); + else + merge_branch = mkpath("refs/remotes/%s/%s", remote, spec_src); + } else + merge_branch = NULL; + + refspec_item_clear(&spec); + return merge_branch; +} + +/** + * Given the repo and refspecs, sets fork_point to the point at which the + * current branch forked from its remote-tracking branch. Returns 0 on success, + * -1 on failure. + */ +static int get_rebase_fork_point(struct object_id *fork_point, const char *repo, + const char *refspec) +{ + int ret; + struct branch *curr_branch; + const char *remote_branch; + struct child_process cp = CHILD_PROCESS_INIT; + struct strbuf sb = STRBUF_INIT; + + curr_branch = branch_get("HEAD"); + if (!curr_branch) + return -1; + + if (refspec) + remote_branch = get_tracking_branch(repo, refspec); + else + remote_branch = get_upstream_branch(repo); + + if (!remote_branch) + return -1; + + strvec_pushl(&cp.args, "merge-base", "--fork-point", + remote_branch, curr_branch->name, NULL); + cp.no_stdin = 1; + cp.no_stderr = 1; + cp.git_cmd = 1; + + ret = capture_command(&cp, &sb, GIT_MAX_HEXSZ); + if (ret) + goto cleanup; + + ret = get_oid_hex(sb.buf, fork_point); + if (ret) + goto cleanup; + +cleanup: + strbuf_release(&sb); + return ret ? -1 : 0; +} + +/** + * Sets merge_base to the octopus merge base of curr_head, merge_head and + * fork_point. Returns 0 if a merge base is found, 1 otherwise. + */ +static int get_octopus_merge_base(struct object_id *merge_base, + const struct object_id *curr_head, + const struct object_id *merge_head, + const struct object_id *fork_point) +{ + struct commit_list *revs = NULL, *result; + + commit_list_insert(lookup_commit_reference(the_repository, curr_head), + &revs); + commit_list_insert(lookup_commit_reference(the_repository, merge_head), + &revs); + if (!is_null_oid(fork_point)) + commit_list_insert(lookup_commit_reference(the_repository, fork_point), + &revs); + + result = get_octopus_merge_bases(revs); + free_commit_list(revs); + reduce_heads_replace(&result); + + if (!result) + return 1; + + oidcpy(merge_base, &result->item->object.oid); + free_commit_list(result); + return 0; +} + +/** + * Given the current HEAD oid, the merge head returned from git-fetch and the + * fork point calculated by get_rebase_fork_point(), compute the <newbase> and + * <upstream> arguments to use for the upcoming git-rebase invocation. + */ +static int get_rebase_newbase_and_upstream(struct object_id *newbase, + struct object_id *upstream, + const struct object_id *curr_head, + const struct object_id *merge_head, + const struct object_id *fork_point) +{ + struct object_id oct_merge_base; + + if (!get_octopus_merge_base(&oct_merge_base, curr_head, merge_head, fork_point)) + if (!is_null_oid(fork_point) && oideq(&oct_merge_base, fork_point)) + fork_point = NULL; + + if (fork_point && !is_null_oid(fork_point)) + oidcpy(upstream, fork_point); + else + oidcpy(upstream, merge_head); + + oidcpy(newbase, merge_head); + + return 0; +} + +/** + * Given the <newbase> and <upstream> calculated by + * get_rebase_newbase_and_upstream(), runs git-rebase with the + * appropriate arguments and returns its exit status. + */ +static int run_rebase(const struct object_id *newbase, + const struct object_id *upstream) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + + strvec_push(&cmd.args, "rebase"); + + /* Shared options */ + argv_push_verbosity(&cmd.args); + + /* Options passed to git-rebase */ + if (opt_rebase == REBASE_MERGES) + strvec_push(&cmd.args, "--rebase-merges"); + else if (opt_rebase == REBASE_INTERACTIVE) + strvec_push(&cmd.args, "--interactive"); + if (opt_diffstat) + strvec_push(&cmd.args, opt_diffstat); + strvec_pushv(&cmd.args, opt_strategies.v); + strvec_pushv(&cmd.args, opt_strategy_opts.v); + if (opt_gpg_sign) + strvec_push(&cmd.args, opt_gpg_sign); + if (opt_signoff) + strvec_push(&cmd.args, opt_signoff); + if (opt_autostash == 0) + strvec_push(&cmd.args, "--no-autostash"); + else if (opt_autostash == 1) + strvec_push(&cmd.args, "--autostash"); + if (opt_verify_signatures && + !strcmp(opt_verify_signatures, "--verify-signatures")) + warning(_("ignoring --verify-signatures for rebase")); + + strvec_push(&cmd.args, "--onto"); + strvec_push(&cmd.args, oid_to_hex(newbase)); + + strvec_push(&cmd.args, oid_to_hex(upstream)); + + cmd.git_cmd = 1; + return run_command(&cmd); +} + +static int get_can_ff(struct object_id *orig_head, + struct oid_array *merge_heads) +{ + int ret; + struct commit_list *list = NULL; + struct commit *merge_head, *head; + struct object_id *orig_merge_head; + + if (merge_heads->nr > 1) + return 0; + + orig_merge_head = &merge_heads->oid[0]; + head = lookup_commit_reference(the_repository, orig_head); + commit_list_insert(head, &list); + merge_head = lookup_commit_reference(the_repository, orig_merge_head); + ret = repo_is_descendant_of(the_repository, merge_head, list); + free_commit_list(list); + return ret; +} + +/* + * Is orig_head a descendant of _all_ merge_heads? + * Unfortunately is_descendant_of() cannot be used as it asks + * if orig_head is a descendant of at least one of them. + */ +static int already_up_to_date(struct object_id *orig_head, + struct oid_array *merge_heads) +{ + int i; + struct commit *ours; + + ours = lookup_commit_reference(the_repository, orig_head); + for (i = 0; i < merge_heads->nr; i++) { + struct commit_list *list = NULL; + struct commit *theirs; + int ok; + + theirs = lookup_commit_reference(the_repository, &merge_heads->oid[i]); + commit_list_insert(theirs, &list); + ok = repo_is_descendant_of(the_repository, ours, list); + free_commit_list(list); + if (!ok) + return 0; + } + return 1; +} + +static void show_advice_pull_non_ff(void) +{ + advise(_("You have divergent branches and need to specify how to reconcile them.\n" + "You can do so by running one of the following commands sometime before\n" + "your next pull:\n" + "\n" + " git config pull.rebase false # merge\n" + " git config pull.rebase true # rebase\n" + " git config pull.ff only # fast-forward only\n" + "\n" + "You can replace \"git config\" with \"git config --global\" to set a default\n" + "preference for all repositories. You can also pass --rebase, --no-rebase,\n" + "or --ff-only on the command line to override the configured default per\n" + "invocation.\n")); +} + +int cmd_pull(int argc, const char **argv, const char *prefix) +{ + const char *repo, **refspecs; + struct oid_array merge_heads = OID_ARRAY_INIT; + struct object_id orig_head, curr_head; + struct object_id rebase_fork_point; + int rebase_unspecified = 0; + int can_ff; + int divergent; + int ret; + + if (!getenv("GIT_REFLOG_ACTION")) + set_reflog_message(argc, argv); + + git_config(git_pull_config, NULL); + if (the_repository->gitdir) { + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + } + + argc = parse_options(argc, argv, prefix, pull_options, pull_usage, 0); + + if (recurse_submodules_cli != RECURSE_SUBMODULES_DEFAULT) + recurse_submodules = recurse_submodules_cli; + + if (cleanup_arg) + /* + * this only checks the validity of cleanup_arg; we don't need + * a valid value for use_editor + */ + get_cleanup_mode(cleanup_arg, 0); + + parse_repo_refspecs(argc, argv, &repo, &refspecs); + + if (!opt_ff) { + opt_ff = xstrdup_or_null(config_get_ff()); + /* + * A subtle point: opt_ff was set on the line above via + * reading from config. opt_rebase, in contrast, is set + * before this point via command line options. The setting + * of opt_rebase via reading from config (using + * config_get_rebase()) does not happen until later. We + * are relying on the next if-condition happening before + * the config_get_rebase() call so that an explicit + * "--rebase" can override a config setting of + * pull.ff=only. + */ + if (opt_rebase >= 0 && opt_ff && !strcmp(opt_ff, "--ff-only")) + opt_ff = "--ff"; + } + + if (opt_rebase < 0) + opt_rebase = config_get_rebase(&rebase_unspecified); + + if (repo_read_index_unmerged(the_repository)) + die_resolve_conflict("pull"); + + if (file_exists(git_path_merge_head(the_repository))) + die_conclude_merge(); + + if (repo_get_oid(the_repository, "HEAD", &orig_head)) + oidclr(&orig_head); + + if (opt_rebase) { + if (opt_autostash == -1) + opt_autostash = config_autostash; + + if (is_null_oid(&orig_head) && !is_index_unborn(&the_index)) + die(_("Updating an unborn branch with changes added to the index.")); + + if (!opt_autostash) + require_clean_work_tree(the_repository, + N_("pull with rebase"), + _("Please commit or stash them."), 1, 0); + + if (get_rebase_fork_point(&rebase_fork_point, repo, *refspecs)) + oidclr(&rebase_fork_point); + } + + if (run_fetch(repo, refspecs)) + return 1; + + if (opt_dry_run) + return 0; + + if (repo_get_oid(the_repository, "HEAD", &curr_head)) + oidclr(&curr_head); + + if (!is_null_oid(&orig_head) && !is_null_oid(&curr_head) && + !oideq(&orig_head, &curr_head)) { + /* + * The fetch involved updating the current branch. + * + * The working tree and the index file are still based on + * orig_head commit, but we are merging into curr_head. + * Update the working tree to match curr_head. + */ + + warning(_("fetch updated the current branch head.\n" + "fast-forwarding your working tree from\n" + "commit %s."), oid_to_hex(&orig_head)); + + if (checkout_fast_forward(the_repository, &orig_head, + &curr_head, 0)) + die(_("Cannot fast-forward your working tree.\n" + "After making sure that you saved anything precious from\n" + "$ git diff %s\n" + "output, run\n" + "$ git reset --hard\n" + "to recover."), oid_to_hex(&orig_head)); + } + + get_merge_heads(&merge_heads); + + if (!merge_heads.nr) + die_no_merge_candidates(repo, refspecs); + + if (is_null_oid(&orig_head)) { + if (merge_heads.nr > 1) + die(_("Cannot merge multiple branches into empty head.")); + ret = pull_into_void(merge_heads.oid, &curr_head); + goto cleanup; + } + if (merge_heads.nr > 1) { + if (opt_rebase) + die(_("Cannot rebase onto multiple branches.")); + if (opt_ff && !strcmp(opt_ff, "--ff-only")) + die(_("Cannot fast-forward to multiple branches.")); + } + + can_ff = get_can_ff(&orig_head, &merge_heads); + divergent = !can_ff && !already_up_to_date(&orig_head, &merge_heads); + + /* ff-only takes precedence over rebase */ + if (opt_ff && !strcmp(opt_ff, "--ff-only")) { + if (divergent) + die_ff_impossible(); + opt_rebase = REBASE_FALSE; + } + /* If no action specified and we can't fast forward, then warn. */ + if (!opt_ff && rebase_unspecified && divergent) { + show_advice_pull_non_ff(); + die(_("Need to specify how to reconcile divergent branches.")); + } + + if (opt_rebase) { + struct object_id newbase; + struct object_id upstream; + get_rebase_newbase_and_upstream(&newbase, &upstream, &curr_head, + merge_heads.oid, &rebase_fork_point); + + if ((recurse_submodules == RECURSE_SUBMODULES_ON || + recurse_submodules == RECURSE_SUBMODULES_ON_DEMAND) && + submodule_touches_in_range(the_repository, &upstream, &curr_head)) + die(_("cannot rebase with locally recorded submodule modifications")); + + if (can_ff) { + /* we can fast-forward this without invoking rebase */ + opt_ff = "--ff-only"; + ret = run_merge(); + } else { + ret = run_rebase(&newbase, &upstream); + } + + if (!ret && (recurse_submodules == RECURSE_SUBMODULES_ON || + recurse_submodules == RECURSE_SUBMODULES_ON_DEMAND)) + ret = rebase_submodules(); + + goto cleanup; + } else { + ret = run_merge(); + if (!ret && (recurse_submodules == RECURSE_SUBMODULES_ON || + recurse_submodules == RECURSE_SUBMODULES_ON_DEMAND)) + ret = update_submodules(); + goto cleanup; + } + +cleanup: + oid_array_clear(&merge_heads); + return ret; +} diff --git a/builtin/push.c b/builtin/push.c new file mode 100644 index 0000000..2e70838 --- /dev/null +++ b/builtin/push.c @@ -0,0 +1,708 @@ +/* + * "git push" + */ +#include "builtin.h" +#include "advice.h" +#include "branch.h" +#include "config.h" +#include "environment.h" +#include "gettext.h" +#include "refs.h" +#include "refspec.h" +#include "run-command.h" +#include "remote.h" +#include "transport.h" +#include "parse-options.h" +#include "pkt-line.h" +#include "repository.h" +#include "submodule.h" +#include "submodule-config.h" +#include "send-pack.h" +#include "trace2.h" +#include "color.h" + +static const char * const push_usage[] = { + N_("git push [<options>] [<repository> [<refspec>...]]"), + NULL, +}; + +static int push_use_color = -1; +static char push_colors[][COLOR_MAXLEN] = { + GIT_COLOR_RESET, + GIT_COLOR_RED, /* ERROR */ +}; + +enum color_push { + PUSH_COLOR_RESET = 0, + PUSH_COLOR_ERROR = 1 +}; + +static int parse_push_color_slot(const char *slot) +{ + if (!strcasecmp(slot, "reset")) + return PUSH_COLOR_RESET; + if (!strcasecmp(slot, "error")) + return PUSH_COLOR_ERROR; + return -1; +} + +static const char *push_get_color(enum color_push ix) +{ + if (want_color_stderr(push_use_color)) + return push_colors[ix]; + return ""; +} + +static int thin = 1; +static int deleterefs; +static const char *receivepack; +static int verbosity; +static int progress = -1; +static int recurse_submodules = RECURSE_SUBMODULES_DEFAULT; +static enum transport_family family; + +static struct push_cas_option cas; + +static struct refspec rs = REFSPEC_INIT_PUSH; + +static struct string_list push_options_config = STRING_LIST_INIT_DUP; + +static void refspec_append_mapped(struct refspec *refspec, const char *ref, + struct remote *remote, struct ref *matched) +{ + const char *branch_name; + + if (remote->push.nr) { + struct refspec_item query; + memset(&query, 0, sizeof(struct refspec_item)); + query.src = matched->name; + if (!query_refspecs(&remote->push, &query) && query.dst) { + refspec_appendf(refspec, "%s%s:%s", + query.force ? "+" : "", + query.src, query.dst); + return; + } + } + + if (push_default == PUSH_DEFAULT_UPSTREAM && + skip_prefix(matched->name, "refs/heads/", &branch_name)) { + struct branch *branch = branch_get(branch_name); + if (branch->merge_nr == 1 && branch->merge[0]->src) { + refspec_appendf(refspec, "%s:%s", + ref, branch->merge[0]->src); + return; + } + } + + refspec_append(refspec, ref); +} + +static void set_refspecs(const char **refs, int nr, const char *repo) +{ + struct remote *remote = NULL; + struct ref *local_refs = NULL; + int i; + + for (i = 0; i < nr; i++) { + const char *ref = refs[i]; + if (!strcmp("tag", ref)) { + if (nr <= ++i) + die(_("tag shorthand without <tag>")); + ref = refs[i]; + if (deleterefs) + refspec_appendf(&rs, ":refs/tags/%s", ref); + else + refspec_appendf(&rs, "refs/tags/%s", ref); + } else if (deleterefs) { + if (strchr(ref, ':') || !*ref) + die(_("--delete only accepts plain target ref names")); + refspec_appendf(&rs, ":%s", ref); + } else if (!strchr(ref, ':')) { + struct ref *matched = NULL; + + /* lazily grab local_refs */ + if (!local_refs) + local_refs = get_local_heads(); + + /* Does "ref" uniquely name our ref? */ + if (count_refspec_match(ref, local_refs, &matched) != 1) { + refspec_append(&rs, ref); + } else { + /* lazily grab remote */ + if (!remote) + remote = remote_get(repo); + if (!remote) + BUG("must get a remote for repo '%s'", repo); + + refspec_append_mapped(&rs, ref, remote, matched); + } + } else + refspec_append(&rs, ref); + } + free_refs(local_refs); +} + +static int push_url_of_remote(struct remote *remote, const char ***url_p) +{ + if (remote->pushurl_nr) { + *url_p = remote->pushurl; + return remote->pushurl_nr; + } + *url_p = remote->url; + return remote->url_nr; +} + +static NORETURN void die_push_simple(struct branch *branch, + struct remote *remote) +{ + /* + * There's no point in using shorten_unambiguous_ref here, + * as the ambiguity would be on the remote side, not what + * we have locally. Plus, this is supposed to be the simple + * mode. If the user is doing something crazy like setting + * upstream to a non-branch, we should probably be showing + * them the big ugly fully qualified ref. + */ + const char *advice_pushdefault_maybe = ""; + const char *advice_automergesimple_maybe = ""; + const char *short_upstream = branch->merge[0]->src; + + skip_prefix(short_upstream, "refs/heads/", &short_upstream); + + /* + * Don't show advice for people who explicitly set + * push.default. + */ + if (push_default == PUSH_DEFAULT_UNSPECIFIED) + advice_pushdefault_maybe = _("\n" + "To choose either option permanently, " + "see push.default in 'git help config'.\n"); + if (git_branch_track != BRANCH_TRACK_SIMPLE) + advice_automergesimple_maybe = _("\n" + "To avoid automatically configuring " + "an upstream branch when its name\n" + "won't match the local branch, see option " + "'simple' of branch.autoSetupMerge\n" + "in 'git help config'.\n"); + die(_("The upstream branch of your current branch does not match\n" + "the name of your current branch. To push to the upstream branch\n" + "on the remote, use\n" + "\n" + " git push %s HEAD:%s\n" + "\n" + "To push to the branch of the same name on the remote, use\n" + "\n" + " git push %s HEAD\n" + "%s%s"), + remote->name, short_upstream, + remote->name, advice_pushdefault_maybe, + advice_automergesimple_maybe); +} + +static const char message_detached_head_die[] = + N_("You are not currently on a branch.\n" + "To push the history leading to the current (detached HEAD)\n" + "state now, use\n" + "\n" + " git push %s HEAD:<name-of-remote-branch>\n"); + +static const char *get_upstream_ref(int flags, struct branch *branch, const char *remote_name) +{ + if (branch->merge_nr == 0 && (flags & TRANSPORT_PUSH_AUTO_UPSTREAM)) { + /* if missing, assume same; set_upstream will be defined later */ + return branch->refname; + } + + if (!branch->merge_nr || !branch->merge || !branch->remote_name) { + const char *advice_autosetup_maybe = ""; + if (!(flags & TRANSPORT_PUSH_AUTO_UPSTREAM)) { + advice_autosetup_maybe = _("\n" + "To have this happen automatically for " + "branches without a tracking\n" + "upstream, see 'push.autoSetupRemote' " + "in 'git help config'.\n"); + } + die(_("The current branch %s has no upstream branch.\n" + "To push the current branch and set the remote as upstream, use\n" + "\n" + " git push --set-upstream %s %s\n" + "%s"), + branch->name, + remote_name, + branch->name, + advice_autosetup_maybe); + } + if (branch->merge_nr != 1) + die(_("The current branch %s has multiple upstream branches, " + "refusing to push."), branch->name); + + return branch->merge[0]->src; +} + +static void setup_default_push_refspecs(int *flags, struct remote *remote) +{ + struct branch *branch; + const char *dst; + int same_remote; + + switch (push_default) { + case PUSH_DEFAULT_MATCHING: + refspec_append(&rs, ":"); + return; + + case PUSH_DEFAULT_NOTHING: + die(_("You didn't specify any refspecs to push, and " + "push.default is \"nothing\".")); + return; + default: + break; + } + + branch = branch_get(NULL); + if (!branch) + die(_(message_detached_head_die), remote->name); + + dst = branch->refname; + same_remote = !strcmp(remote->name, remote_for_branch(branch, NULL)); + + switch (push_default) { + default: + case PUSH_DEFAULT_UNSPECIFIED: + case PUSH_DEFAULT_SIMPLE: + if (!same_remote) + break; + if (strcmp(branch->refname, get_upstream_ref(*flags, branch, remote->name))) + die_push_simple(branch, remote); + break; + + case PUSH_DEFAULT_UPSTREAM: + if (!same_remote) + die(_("You are pushing to remote '%s', which is not the upstream of\n" + "your current branch '%s', without telling me what to push\n" + "to update which remote branch."), + remote->name, branch->name); + dst = get_upstream_ref(*flags, branch, remote->name); + break; + + case PUSH_DEFAULT_CURRENT: + break; + } + + /* + * this is a default push - if auto-upstream is enabled and there is + * no upstream defined, then set it (with options 'simple', 'upstream', + * and 'current'). + */ + if ((*flags & TRANSPORT_PUSH_AUTO_UPSTREAM) && branch->merge_nr == 0) + *flags |= TRANSPORT_PUSH_SET_UPSTREAM; + + refspec_appendf(&rs, "%s:%s", branch->refname, dst); +} + +static const char message_advice_pull_before_push[] = + N_("Updates were rejected because the tip of your current branch is behind\n" + "its remote counterpart. If you want to integrate the remote changes,\n" + "use 'git pull' before pushing again.\n" + "See the 'Note about fast-forwards' in 'git push --help' for details."); + +static const char message_advice_checkout_pull_push[] = + N_("Updates were rejected because a pushed branch tip is behind its remote\n" + "counterpart. If you want to integrate the remote changes, use 'git pull'\n" + "before pushing again.\n" + "See the 'Note about fast-forwards' in 'git push --help' for details."); + +static const char message_advice_ref_fetch_first[] = + N_("Updates were rejected because the remote contains work that you do not\n" + "have locally. This is usually caused by another repository pushing to\n" + "the same ref. If you want to integrate the remote changes, use\n" + "'git pull' before pushing again.\n" + "See the 'Note about fast-forwards' in 'git push --help' for details."); + +static const char message_advice_ref_already_exists[] = + N_("Updates were rejected because the tag already exists in the remote."); + +static const char message_advice_ref_needs_force[] = + N_("You cannot update a remote ref that points at a non-commit object,\n" + "or update a remote ref to make it point at a non-commit object,\n" + "without using the '--force' option.\n"); + +static const char message_advice_ref_needs_update[] = + N_("Updates were rejected because the tip of the remote-tracking branch has\n" + "been updated since the last checkout. If you want to integrate the\n" + "remote changes, use 'git pull' before pushing again.\n" + "See the 'Note about fast-forwards' in 'git push --help' for details."); + +static void advise_pull_before_push(void) +{ + if (!advice_enabled(ADVICE_PUSH_NON_FF_CURRENT) || !advice_enabled(ADVICE_PUSH_UPDATE_REJECTED)) + return; + advise(_(message_advice_pull_before_push)); +} + +static void advise_checkout_pull_push(void) +{ + if (!advice_enabled(ADVICE_PUSH_NON_FF_MATCHING) || !advice_enabled(ADVICE_PUSH_UPDATE_REJECTED)) + return; + advise(_(message_advice_checkout_pull_push)); +} + +static void advise_ref_already_exists(void) +{ + if (!advice_enabled(ADVICE_PUSH_ALREADY_EXISTS) || !advice_enabled(ADVICE_PUSH_UPDATE_REJECTED)) + return; + advise(_(message_advice_ref_already_exists)); +} + +static void advise_ref_fetch_first(void) +{ + if (!advice_enabled(ADVICE_PUSH_FETCH_FIRST) || !advice_enabled(ADVICE_PUSH_UPDATE_REJECTED)) + return; + advise(_(message_advice_ref_fetch_first)); +} + +static void advise_ref_needs_force(void) +{ + if (!advice_enabled(ADVICE_PUSH_NEEDS_FORCE) || !advice_enabled(ADVICE_PUSH_UPDATE_REJECTED)) + return; + advise(_(message_advice_ref_needs_force)); +} + +static void advise_ref_needs_update(void) +{ + if (!advice_enabled(ADVICE_PUSH_REF_NEEDS_UPDATE) || !advice_enabled(ADVICE_PUSH_UPDATE_REJECTED)) + return; + advise(_(message_advice_ref_needs_update)); +} + +static int push_with_options(struct transport *transport, struct refspec *rs, + int flags) +{ + int err; + unsigned int reject_reasons; + char *anon_url = transport_anonymize_url(transport->url); + + transport_set_verbosity(transport, verbosity, progress); + transport->family = family; + + if (receivepack) + transport_set_option(transport, + TRANS_OPT_RECEIVEPACK, receivepack); + transport_set_option(transport, TRANS_OPT_THIN, thin ? "yes" : NULL); + + if (!is_empty_cas(&cas)) { + if (!transport->smart_options) + die("underlying transport does not support --%s option", + CAS_OPT_NAME); + transport->smart_options->cas = &cas; + } + + if (verbosity > 0) + fprintf(stderr, _("Pushing to %s\n"), anon_url); + trace2_region_enter("push", "transport_push", the_repository); + err = transport_push(the_repository, transport, + rs, flags, &reject_reasons); + trace2_region_leave("push", "transport_push", the_repository); + if (err != 0) { + fprintf(stderr, "%s", push_get_color(PUSH_COLOR_ERROR)); + error(_("failed to push some refs to '%s'"), anon_url); + fprintf(stderr, "%s", push_get_color(PUSH_COLOR_RESET)); + } + + err |= transport_disconnect(transport); + free(anon_url); + if (!err) + return 0; + + if (reject_reasons & REJECT_NON_FF_HEAD) { + advise_pull_before_push(); + } else if (reject_reasons & REJECT_NON_FF_OTHER) { + advise_checkout_pull_push(); + } else if (reject_reasons & REJECT_ALREADY_EXISTS) { + advise_ref_already_exists(); + } else if (reject_reasons & REJECT_FETCH_FIRST) { + advise_ref_fetch_first(); + } else if (reject_reasons & REJECT_NEEDS_FORCE) { + advise_ref_needs_force(); + } else if (reject_reasons & REJECT_REF_NEEDS_UPDATE) { + advise_ref_needs_update(); + } + + return 1; +} + +static int do_push(int flags, + const struct string_list *push_options, + struct remote *remote) +{ + int i, errs; + const char **url; + int url_nr; + struct refspec *push_refspec = &rs; + + if (push_options->nr) + flags |= TRANSPORT_PUSH_OPTIONS; + + if (!push_refspec->nr && !(flags & TRANSPORT_PUSH_ALL)) { + if (remote->push.nr) { + push_refspec = &remote->push; + } else if (!(flags & TRANSPORT_PUSH_MIRROR)) + setup_default_push_refspecs(&flags, remote); + } + errs = 0; + url_nr = push_url_of_remote(remote, &url); + if (url_nr) { + for (i = 0; i < url_nr; i++) { + struct transport *transport = + transport_get(remote, url[i]); + if (flags & TRANSPORT_PUSH_OPTIONS) + transport->push_options = push_options; + if (push_with_options(transport, push_refspec, flags)) + errs++; + } + } else { + struct transport *transport = + transport_get(remote, NULL); + if (flags & TRANSPORT_PUSH_OPTIONS) + transport->push_options = push_options; + if (push_with_options(transport, push_refspec, flags)) + errs++; + } + return !!errs; +} + +static int option_parse_recurse_submodules(const struct option *opt, + const char *arg, int unset) +{ + int *recurse_submodules = opt->value; + + if (unset) + *recurse_submodules = RECURSE_SUBMODULES_OFF; + else { + if (!strcmp(arg, "only-is-on-demand")) { + if (*recurse_submodules == RECURSE_SUBMODULES_ONLY) { + warning(_("recursing into submodule with push.recurseSubmodules=only; using on-demand instead")); + *recurse_submodules = RECURSE_SUBMODULES_ON_DEMAND; + } + } else { + *recurse_submodules = parse_push_recurse_submodules_arg(opt->long_name, arg); + } + } + + return 0; +} + +static void set_push_cert_flags(int *flags, int v) +{ + switch (v) { + case SEND_PACK_PUSH_CERT_NEVER: + *flags &= ~(TRANSPORT_PUSH_CERT_ALWAYS | TRANSPORT_PUSH_CERT_IF_ASKED); + break; + case SEND_PACK_PUSH_CERT_ALWAYS: + *flags |= TRANSPORT_PUSH_CERT_ALWAYS; + *flags &= ~TRANSPORT_PUSH_CERT_IF_ASKED; + break; + case SEND_PACK_PUSH_CERT_IF_ASKED: + *flags |= TRANSPORT_PUSH_CERT_IF_ASKED; + *flags &= ~TRANSPORT_PUSH_CERT_ALWAYS; + break; + } +} + + +static int git_push_config(const char *k, const char *v, + const struct config_context *ctx, void *cb) +{ + const char *slot_name; + int *flags = cb; + + if (!strcmp(k, "push.followtags")) { + if (git_config_bool(k, v)) + *flags |= TRANSPORT_PUSH_FOLLOW_TAGS; + else + *flags &= ~TRANSPORT_PUSH_FOLLOW_TAGS; + return 0; + } else if (!strcmp(k, "push.autosetupremote")) { + if (git_config_bool(k, v)) + *flags |= TRANSPORT_PUSH_AUTO_UPSTREAM; + return 0; + } else if (!strcmp(k, "push.gpgsign")) { + const char *value; + if (!git_config_get_value("push.gpgsign", &value)) { + switch (git_parse_maybe_bool(value)) { + case 0: + set_push_cert_flags(flags, SEND_PACK_PUSH_CERT_NEVER); + break; + case 1: + set_push_cert_flags(flags, SEND_PACK_PUSH_CERT_ALWAYS); + break; + default: + if (value && !strcasecmp(value, "if-asked")) + set_push_cert_flags(flags, SEND_PACK_PUSH_CERT_IF_ASKED); + else + return error(_("invalid value for '%s'"), k); + } + } + } else if (!strcmp(k, "push.recursesubmodules")) { + const char *value; + if (!git_config_get_value("push.recursesubmodules", &value)) + recurse_submodules = parse_push_recurse_submodules_arg(k, value); + } else if (!strcmp(k, "submodule.recurse")) { + int val = git_config_bool(k, v) ? + RECURSE_SUBMODULES_ON_DEMAND : RECURSE_SUBMODULES_OFF; + recurse_submodules = val; + } else if (!strcmp(k, "push.pushoption")) { + if (!v) + return config_error_nonbool(k); + else + if (!*v) + string_list_clear(&push_options_config, 0); + else + string_list_append(&push_options_config, v); + return 0; + } else if (!strcmp(k, "color.push")) { + push_use_color = git_config_colorbool(k, v); + return 0; + } else if (skip_prefix(k, "color.push.", &slot_name)) { + int slot = parse_push_color_slot(slot_name); + if (slot < 0) + return 0; + if (!v) + return config_error_nonbool(k); + return color_parse(v, push_colors[slot]); + } else if (!strcmp(k, "push.useforceifincludes")) { + if (git_config_bool(k, v)) + *flags |= TRANSPORT_PUSH_FORCE_IF_INCLUDES; + else + *flags &= ~TRANSPORT_PUSH_FORCE_IF_INCLUDES; + return 0; + } + + return git_default_config(k, v, ctx, NULL); +} + +int cmd_push(int argc, const char **argv, const char *prefix) +{ + int flags = 0; + int tags = 0; + int push_cert = -1; + int rc; + const char *repo = NULL; /* default repository */ + struct string_list push_options_cmdline = STRING_LIST_INIT_DUP; + struct string_list *push_options; + const struct string_list_item *item; + struct remote *remote; + + struct option options[] = { + OPT__VERBOSITY(&verbosity), + OPT_STRING( 0 , "repo", &repo, N_("repository"), N_("repository")), + OPT_BIT( 0 , "all", &flags, N_("push all branches"), TRANSPORT_PUSH_ALL), + OPT_ALIAS( 0 , "branches", "all"), + OPT_BIT( 0 , "mirror", &flags, N_("mirror all refs"), + (TRANSPORT_PUSH_MIRROR|TRANSPORT_PUSH_FORCE)), + OPT_BOOL('d', "delete", &deleterefs, N_("delete refs")), + OPT_BOOL( 0 , "tags", &tags, N_("push tags (can't be used with --all or --branches or --mirror)")), + OPT_BIT('n' , "dry-run", &flags, N_("dry run"), TRANSPORT_PUSH_DRY_RUN), + OPT_BIT( 0, "porcelain", &flags, N_("machine-readable output"), TRANSPORT_PUSH_PORCELAIN), + OPT_BIT('f', "force", &flags, N_("force updates"), TRANSPORT_PUSH_FORCE), + OPT_CALLBACK_F(0, CAS_OPT_NAME, &cas, N_("<refname>:<expect>"), + N_("require old value of ref to be at this value"), + PARSE_OPT_OPTARG | PARSE_OPT_LITERAL_ARGHELP, parseopt_push_cas_option), + OPT_BIT(0, TRANS_OPT_FORCE_IF_INCLUDES, &flags, + N_("require remote updates to be integrated locally"), + TRANSPORT_PUSH_FORCE_IF_INCLUDES), + OPT_CALLBACK(0, "recurse-submodules", &recurse_submodules, "(check|on-demand|no)", + N_("control recursive pushing of submodules"), option_parse_recurse_submodules), + OPT_BOOL_F( 0 , "thin", &thin, N_("use thin pack"), PARSE_OPT_NOCOMPLETE), + OPT_STRING( 0 , "receive-pack", &receivepack, "receive-pack", N_("receive pack program")), + OPT_STRING( 0 , "exec", &receivepack, "receive-pack", N_("receive pack program")), + OPT_BIT('u', "set-upstream", &flags, N_("set upstream for git pull/status"), + TRANSPORT_PUSH_SET_UPSTREAM), + OPT_BOOL(0, "progress", &progress, N_("force progress reporting")), + OPT_BIT(0, "prune", &flags, N_("prune locally removed refs"), + TRANSPORT_PUSH_PRUNE), + OPT_BIT(0, "no-verify", &flags, N_("bypass pre-push hook"), TRANSPORT_PUSH_NO_HOOK), + OPT_BIT(0, "follow-tags", &flags, N_("push missing but relevant tags"), + TRANSPORT_PUSH_FOLLOW_TAGS), + OPT_CALLBACK_F(0, "signed", &push_cert, "(yes|no|if-asked)", N_("GPG sign the push"), + PARSE_OPT_OPTARG, option_parse_push_signed), + OPT_BIT(0, "atomic", &flags, N_("request atomic transaction on remote side"), TRANSPORT_PUSH_ATOMIC), + OPT_STRING_LIST('o', "push-option", &push_options_cmdline, N_("server-specific"), N_("option to transmit")), + OPT_IPVERSION(&family), + OPT_END() + }; + + packet_trace_identity("push"); + git_config(git_push_config, &flags); + argc = parse_options(argc, argv, prefix, options, push_usage, 0); + push_options = (push_options_cmdline.nr + ? &push_options_cmdline + : &push_options_config); + set_push_cert_flags(&flags, push_cert); + + if (deleterefs && (tags || (flags & (TRANSPORT_PUSH_ALL | TRANSPORT_PUSH_MIRROR)))) + die(_("options '%s' and '%s' cannot be used together"), "--delete", "--all/--branches/--mirror/--tags"); + if (deleterefs && argc < 2) + die(_("--delete doesn't make sense without any refs")); + + if (recurse_submodules == RECURSE_SUBMODULES_CHECK) + flags |= TRANSPORT_RECURSE_SUBMODULES_CHECK; + else if (recurse_submodules == RECURSE_SUBMODULES_ON_DEMAND) + flags |= TRANSPORT_RECURSE_SUBMODULES_ON_DEMAND; + else if (recurse_submodules == RECURSE_SUBMODULES_ONLY) + flags |= TRANSPORT_RECURSE_SUBMODULES_ONLY; + + if (tags) + refspec_append(&rs, "refs/tags/*"); + + if (argc > 0) { + repo = argv[0]; + set_refspecs(argv + 1, argc - 1, repo); + } + + remote = pushremote_get(repo); + if (!remote) { + if (repo) + die(_("bad repository '%s'"), repo); + die(_("No configured push destination.\n" + "Either specify the URL from the command-line or configure a remote repository using\n" + "\n" + " git remote add <name> <url>\n" + "\n" + "and then push using the remote name\n" + "\n" + " git push <name>\n")); + } + + if (remote->mirror) + flags |= (TRANSPORT_PUSH_MIRROR|TRANSPORT_PUSH_FORCE); + + if (flags & TRANSPORT_PUSH_ALL) { + if (tags) + die(_("options '%s' and '%s' cannot be used together"), "--all", "--tags"); + if (argc >= 2) + die(_("--all can't be combined with refspecs")); + } + if (flags & TRANSPORT_PUSH_MIRROR) { + if (tags) + die(_("options '%s' and '%s' cannot be used together"), "--mirror", "--tags"); + if (argc >= 2) + die(_("--mirror can't be combined with refspecs")); + } + if ((flags & TRANSPORT_PUSH_ALL) && (flags & TRANSPORT_PUSH_MIRROR)) + die(_("options '%s' and '%s' cannot be used together"), "--all", "--mirror"); + + if (!is_empty_cas(&cas) && (flags & TRANSPORT_PUSH_FORCE_IF_INCLUDES)) + cas.use_force_if_includes = 1; + + for_each_string_list_item(item, push_options) + if (strchr(item->string, '\n')) + die(_("push options must not have new line characters")); + + rc = do_push(flags, push_options, remote); + string_list_clear(&push_options_cmdline, 0); + string_list_clear(&push_options_config, 0); + if (rc == -1) + usage_with_options(push_usage, options); + else + return rc; +} diff --git a/builtin/range-diff.c b/builtin/range-diff.c new file mode 100644 index 0000000..e455a47 --- /dev/null +++ b/builtin/range-diff.c @@ -0,0 +1,159 @@ +#include "builtin.h" +#include "gettext.h" +#include "object-name.h" +#include "parse-options.h" +#include "range-diff.h" +#include "config.h" +#include "repository.h" +#include "revision.h" + +static const char * const builtin_range_diff_usage[] = { +N_("git range-diff [<options>] <old-base>..<old-tip> <new-base>..<new-tip>"), +N_("git range-diff [<options>] <old-tip>...<new-tip>"), +N_("git range-diff [<options>] <base> <old-tip> <new-tip>"), +NULL +}; + +int cmd_range_diff(int argc, const char **argv, const char *prefix) +{ + struct diff_options diffopt = { NULL }; + struct strvec other_arg = STRVEC_INIT; + struct range_diff_options range_diff_opts = { + .creation_factor = RANGE_DIFF_CREATION_FACTOR_DEFAULT, + .diffopt = &diffopt, + .other_arg = &other_arg + }; + int simple_color = -1, left_only = 0, right_only = 0; + struct option range_diff_options[] = { + OPT_INTEGER(0, "creation-factor", + &range_diff_opts.creation_factor, + N_("percentage by which creation is weighted")), + OPT_BOOL(0, "no-dual-color", &simple_color, + N_("use simple diff colors")), + OPT_PASSTHRU_ARGV(0, "notes", &other_arg, + N_("notes"), N_("passed to 'git log'"), + PARSE_OPT_OPTARG), + OPT_BOOL(0, "left-only", &left_only, + N_("only emit output related to the first range")), + OPT_BOOL(0, "right-only", &right_only, + N_("only emit output related to the second range")), + OPT_END() + }; + struct option *options; + int i, dash_dash = -1, res = 0; + struct strbuf range1 = STRBUF_INIT, range2 = STRBUF_INIT; + struct object_id oid; + const char *three_dots = NULL; + + git_config(git_diff_ui_config, NULL); + + repo_diff_setup(the_repository, &diffopt); + + options = add_diff_options(range_diff_options, &diffopt); + argc = parse_options(argc, argv, prefix, options, + builtin_range_diff_usage, PARSE_OPT_KEEP_DASHDASH); + + diff_setup_done(&diffopt); + + /* force color when --dual-color was used */ + if (!simple_color) + diffopt.use_color = 1; + + for (i = 0; i < argc; i++) + if (!strcmp(argv[i], "--")) { + dash_dash = i; + break; + } + + if (dash_dash == 3 || + (dash_dash < 0 && argc > 2 && + !repo_get_oid_committish(the_repository, argv[0], &oid) && + !repo_get_oid_committish(the_repository, argv[1], &oid) && + !repo_get_oid_committish(the_repository, argv[2], &oid))) { + if (dash_dash < 0) + ; /* already validated arguments */ + else if (repo_get_oid_committish(the_repository, argv[0], &oid)) + usage_msg_optf(_("not a revision: '%s'"), + builtin_range_diff_usage, options, + argv[0]); + else if (repo_get_oid_committish(the_repository, argv[1], &oid)) + usage_msg_optf(_("not a revision: '%s'"), + builtin_range_diff_usage, options, + argv[1]); + else if (repo_get_oid_committish(the_repository, argv[2], &oid)) + usage_msg_optf(_("not a revision: '%s'"), + builtin_range_diff_usage, options, + argv[2]); + + strbuf_addf(&range1, "%s..%s", argv[0], argv[1]); + strbuf_addf(&range2, "%s..%s", argv[0], argv[2]); + + strvec_pushv(&other_arg, argv + + (dash_dash < 0 ? 3 : dash_dash)); + } else if (dash_dash == 2 || + (dash_dash < 0 && argc > 1 && + is_range_diff_range(argv[0]) && + is_range_diff_range(argv[1]))) { + if (dash_dash < 0) + ; /* already validated arguments */ + else if (!is_range_diff_range(argv[0])) + usage_msg_optf(_("not a commit range: '%s'"), + builtin_range_diff_usage, options, + argv[0]); + else if (!is_range_diff_range(argv[1])) + usage_msg_optf(_("not a commit range: '%s'"), + builtin_range_diff_usage, options, + argv[1]); + + strbuf_addstr(&range1, argv[0]); + strbuf_addstr(&range2, argv[1]); + + strvec_pushv(&other_arg, argv + + (dash_dash < 0 ? 2 : dash_dash)); + } else if (dash_dash == 1 || + (dash_dash < 0 && argc > 0 && + (three_dots = strstr(argv[0], "...")))) { + const char *a, *b; + int a_len; + + if (dash_dash < 0) + ; /* already validated arguments */ + else if (!(three_dots = strstr(argv[0], "..."))) + usage_msg_optf(_("not a symmetric range: '%s'"), + builtin_range_diff_usage, options, + argv[0]); + + if (three_dots == argv[0]) { + a = "HEAD"; + a_len = strlen(a); + } else { + a = argv[0]; + a_len = (int)(three_dots - a); + } + + if (three_dots[3]) + b = three_dots + 3; + else + b = "HEAD"; + + strbuf_addf(&range1, "%s..%.*s", b, a_len, a); + strbuf_addf(&range2, "%.*s..%s", a_len, a, b); + + strvec_pushv(&other_arg, argv + + (dash_dash < 0 ? 1 : dash_dash)); + } else + usage_msg_opt(_("need two commit ranges"), + builtin_range_diff_usage, options); + FREE_AND_NULL(options); + + range_diff_opts.dual_color = simple_color < 1; + range_diff_opts.left_only = left_only; + range_diff_opts.right_only = right_only; + res = show_range_diff(range1.buf, range2.buf, &range_diff_opts); + + strvec_clear(&other_arg); + strbuf_release(&range1); + strbuf_release(&range2); + + return res; +} diff --git a/builtin/read-tree.c b/builtin/read-tree.c new file mode 100644 index 0000000..8196ca9 --- /dev/null +++ b/builtin/read-tree.c @@ -0,0 +1,289 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ + +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "hex.h" +#include "lockfile.h" +#include "object.h" +#include "object-name.h" +#include "tree.h" +#include "tree-walk.h" +#include "cache-tree.h" +#include "unpack-trees.h" +#include "dir.h" +#include "parse-options.h" +#include "repository.h" +#include "resolve-undo.h" +#include "setup.h" +#include "sparse-index.h" +#include "submodule.h" +#include "submodule-config.h" + +static int nr_trees; +static int read_empty; +static struct tree *trees[MAX_UNPACK_TREES]; + +static int list_tree(struct object_id *oid) +{ + struct tree *tree; + + if (nr_trees >= MAX_UNPACK_TREES) + die("I cannot read more than %d trees", MAX_UNPACK_TREES); + tree = parse_tree_indirect(oid); + if (!tree) + return -1; + trees[nr_trees++] = tree; + return 0; +} + +static const char * const read_tree_usage[] = { + N_("git read-tree [(-m [--trivial] [--aggressive] | --reset | --prefix=<prefix>)\n" + " [-u | -i]] [--index-output=<file>] [--no-sparse-checkout]\n" + " (--empty | <tree-ish1> [<tree-ish2> [<tree-ish3>]])"), + NULL +}; + +static int index_output_cb(const struct option *opt UNUSED, const char *arg, + int unset) +{ + BUG_ON_OPT_NEG(unset); + set_alternate_index_output(arg); + return 0; +} + +static int exclude_per_directory_cb(const struct option *opt, const char *arg, + int unset) +{ + struct unpack_trees_options *opts; + + BUG_ON_OPT_NEG(unset); + + opts = (struct unpack_trees_options *)opt->value; + + if (!opts->update) + die("--exclude-per-directory is meaningless unless -u"); + if (strcmp(arg, ".gitignore")) + die("--exclude-per-directory argument must be .gitignore"); + return 0; +} + +static void debug_stage(const char *label, const struct cache_entry *ce, + struct unpack_trees_options *o) +{ + printf("%s ", label); + if (!ce) + printf("(missing)\n"); + else if (ce == o->df_conflict_entry) + printf("(conflict)\n"); + else + printf("%06o #%d %s %.8s\n", + ce->ce_mode, ce_stage(ce), ce->name, + oid_to_hex(&ce->oid)); +} + +static int debug_merge(const struct cache_entry * const *stages, + struct unpack_trees_options *o) +{ + int i; + + printf("* %d-way merge\n", o->internal.merge_size); + debug_stage("index", stages[0], o); + for (i = 1; i <= o->internal.merge_size; i++) { + char buf[24]; + xsnprintf(buf, sizeof(buf), "ent#%d", i); + debug_stage(buf, stages[i], o); + } + return 0; +} + +static int git_read_tree_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + if (!strcmp(var, "submodule.recurse")) + return git_default_submodule_config(var, value, cb); + + return git_default_config(var, value, ctx, cb); +} + +int cmd_read_tree(int argc, const char **argv, const char *cmd_prefix) +{ + int i, stage = 0; + struct object_id oid; + struct tree_desc t[MAX_UNPACK_TREES]; + struct unpack_trees_options opts; + int prefix_set = 0; + struct lock_file lock_file = LOCK_INIT; + const struct option read_tree_options[] = { + OPT__SUPER_PREFIX(&opts.super_prefix), + OPT_CALLBACK_F(0, "index-output", NULL, N_("file"), + N_("write resulting index to <file>"), + PARSE_OPT_NONEG, index_output_cb), + OPT_BOOL(0, "empty", &read_empty, + N_("only empty the index")), + OPT__VERBOSE(&opts.verbose_update, N_("be verbose")), + OPT_GROUP(N_("Merging")), + OPT_BOOL('m', NULL, &opts.merge, + N_("perform a merge in addition to a read")), + OPT_BOOL(0, "trivial", &opts.trivial_merges_only, + N_("3-way merge if no file level merging required")), + OPT_BOOL(0, "aggressive", &opts.aggressive, + N_("3-way merge in presence of adds and removes")), + OPT_BOOL(0, "reset", &opts.reset, + N_("same as -m, but discard unmerged entries")), + { OPTION_STRING, 0, "prefix", &opts.prefix, N_("<subdirectory>/"), + N_("read the tree into the index under <subdirectory>/"), + PARSE_OPT_NONEG }, + OPT_BOOL('u', NULL, &opts.update, + N_("update working tree with merge result")), + OPT_CALLBACK_F(0, "exclude-per-directory", &opts, + N_("gitignore"), + N_("allow explicitly ignored files to be overwritten"), + PARSE_OPT_NONEG, exclude_per_directory_cb), + OPT_BOOL('i', NULL, &opts.index_only, + N_("don't check the working tree after merging")), + OPT__DRY_RUN(&opts.dry_run, N_("don't update the index or the work tree")), + OPT_BOOL(0, "no-sparse-checkout", &opts.skip_sparse_checkout, + N_("skip applying sparse checkout filter")), + OPT_BOOL(0, "debug-unpack", &opts.internal.debug_unpack, + N_("debug unpack-trees")), + OPT_CALLBACK_F(0, "recurse-submodules", NULL, + "checkout", "control recursive updating of submodules", + PARSE_OPT_OPTARG, option_parse_recurse_submodules_worktree_updater), + OPT__QUIET(&opts.quiet, N_("suppress feedback messages")), + OPT_END() + }; + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = -1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + + git_config(git_read_tree_config, NULL); + + argc = parse_options(argc, argv, cmd_prefix, read_tree_options, + read_tree_usage, 0); + + prefix_set = opts.prefix ? 1 : 0; + if (1 < opts.merge + opts.reset + prefix_set) + die("Which one? -m, --reset, or --prefix?"); + + /* Prefix should not start with a directory separator */ + if (opts.prefix && opts.prefix[0] == '/') + die("Invalid prefix, prefix cannot start with '/'"); + + if (opts.reset) + opts.reset = UNPACK_RESET_OVERWRITE_UNTRACKED; + + 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); + + /* + * NEEDSWORK + * + * The old index should be read anyway even if we're going to + * destroy all index entries because we still need to preserve + * certain information such as index version or split-index + * mode. + */ + + if (opts.reset || opts.merge || opts.prefix) { + if (repo_read_index_unmerged(the_repository) && (opts.prefix || opts.merge)) + die(_("You need to resolve your current index first")); + stage = opts.merge = 1; + } + resolve_undo_clear_index(&the_index); + + for (i = 0; i < argc; i++) { + const char *arg = argv[i]; + + if (repo_get_oid(the_repository, arg, &oid)) + die("Not a valid object name %s", arg); + if (list_tree(&oid) < 0) + die("failed to unpack tree object %s", arg); + stage++; + } + if (!nr_trees && !read_empty && !opts.merge) + warning("read-tree: emptying the index with no arguments is deprecated; use --empty"); + else if (nr_trees > 0 && read_empty) + die("passing trees as arguments contradicts --empty"); + + if (1 < opts.index_only + opts.update) + die("-u and -i at the same time makes no sense"); + if ((opts.update || opts.index_only) && !opts.merge) + die("%s is meaningless without -m, --reset, or --prefix", + opts.update ? "-u" : "-i"); + if (opts.update && !opts.reset) + opts.preserve_ignored = 0; + /* otherwise, opts.preserve_ignored is irrelevant */ + if (opts.merge && !opts.index_only) + setup_work_tree(); + + if (opts.skip_sparse_checkout) + ensure_full_index(&the_index); + + if (opts.merge) { + switch (stage - 1) { + case 0: + die("you must specify at least one tree to merge"); + break; + case 1: + opts.fn = opts.prefix ? bind_merge : oneway_merge; + break; + case 2: + opts.fn = twoway_merge; + opts.initial_checkout = is_index_unborn(&the_index); + break; + case 3: + default: + opts.fn = threeway_merge; + break; + } + + if (stage - 1 >= 3) + opts.head_idx = stage - 2; + else + opts.head_idx = 1; + } + + if (opts.internal.debug_unpack) + opts.fn = debug_merge; + + /* If we're going to prime_cache_tree later, skip cache tree update */ + if (nr_trees == 1 && !opts.prefix) + opts.skip_cache_tree_update = 1; + + cache_tree_free(&the_index.cache_tree); + for (i = 0; i < nr_trees; i++) { + struct tree *tree = trees[i]; + parse_tree(tree); + init_tree_desc(t+i, tree->buffer, tree->size); + } + if (unpack_trees(nr_trees, t, &opts)) + return 128; + + if (opts.internal.debug_unpack || opts.dry_run) + return 0; /* do not write the index out */ + + /* + * When reading only one tree (either the most basic form, + * "-m ent" or "--reset ent" form), we can obtain a fully + * valid cache-tree because the index must match exactly + * what came from the tree. + */ + if (nr_trees == 1 && !opts.prefix) + prime_cache_tree(the_repository, + the_repository->index, + trees[0]); + + if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK)) + die("unable to write new index file"); + return 0; +} diff --git a/builtin/rebase.c b/builtin/rebase.c new file mode 100644 index 0000000..043c65d --- /dev/null +++ b/builtin/rebase.c @@ -0,0 +1,1860 @@ +/* + * "git rebase" builtin command + * + * Copyright (c) 2018 Pratik Karki + */ + +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "abspath.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "run-command.h" +#include "exec-cmd.h" +#include "strvec.h" +#include "dir.h" +#include "packfile.h" +#include "refs.h" +#include "quote.h" +#include "config.h" +#include "cache-tree.h" +#include "unpack-trees.h" +#include "lockfile.h" +#include "object-file.h" +#include "object-name.h" +#include "parse-options.h" +#include "path.h" +#include "commit.h" +#include "diff.h" +#include "wt-status.h" +#include "revision.h" +#include "commit-reach.h" +#include "rerere.h" +#include "branch.h" +#include "sequencer.h" +#include "rebase-interactive.h" +#include "reset.h" +#include "trace2.h" +#include "hook.h" + +static char const * const builtin_rebase_usage[] = { + N_("git rebase [-i] [options] [--exec <cmd>] " + "[--onto <newbase> | --keep-base] [<upstream> [<branch>]]"), + N_("git rebase [-i] [options] [--exec <cmd>] [--onto <newbase>] " + "--root [<branch>]"), + "git rebase --continue | --abort | --skip | --edit-todo", + NULL +}; + +static GIT_PATH_FUNC(path_squash_onto, "rebase-merge/squash-onto") +static GIT_PATH_FUNC(path_interactive, "rebase-merge/interactive") +static GIT_PATH_FUNC(apply_dir, "rebase-apply") +static GIT_PATH_FUNC(merge_dir, "rebase-merge") + +enum rebase_type { + REBASE_UNSPECIFIED = -1, + REBASE_APPLY, + REBASE_MERGE +}; + +enum empty_type { + EMPTY_UNSPECIFIED = -1, + EMPTY_DROP, + EMPTY_KEEP, + EMPTY_ASK +}; + +enum action { + ACTION_NONE = 0, + ACTION_CONTINUE, + ACTION_SKIP, + ACTION_ABORT, + ACTION_QUIT, + ACTION_EDIT_TODO, + ACTION_SHOW_CURRENT_PATCH +}; + +static const char *action_names[] = { + "undefined", + "continue", + "skip", + "abort", + "quit", + "edit_todo", + "show_current_patch" +}; + +struct rebase_options { + enum rebase_type type; + enum empty_type empty; + const char *default_backend; + const char *state_dir; + struct commit *upstream; + const char *upstream_name; + const char *upstream_arg; + char *head_name; + struct commit *orig_head; + struct commit *onto; + const char *onto_name; + const char *revisions; + const char *switch_to; + int root, root_with_onto; + struct object_id *squash_onto; + struct commit *restrict_revision; + int dont_finish_rebase; + enum { + REBASE_NO_QUIET = 1<<0, + REBASE_VERBOSE = 1<<1, + REBASE_DIFFSTAT = 1<<2, + REBASE_FORCE = 1<<3, + REBASE_INTERACTIVE_EXPLICIT = 1<<4, + } flags; + struct strvec git_am_opts; + enum action action; + char *reflog_action; + int signoff; + int allow_rerere_autoupdate; + int keep_empty; + int autosquash; + char *gpg_sign_opt; + int autostash; + int committer_date_is_author_date; + int ignore_date; + struct string_list exec; + int allow_empty_message; + int rebase_merges, rebase_cousins; + char *strategy; + struct string_list strategy_opts; + struct strbuf git_format_patch_opt; + int reschedule_failed_exec; + int reapply_cherry_picks; + int fork_point; + int update_refs; + int config_autosquash; + int config_rebase_merges; + int config_update_refs; +}; + +#define REBASE_OPTIONS_INIT { \ + .type = REBASE_UNSPECIFIED, \ + .empty = EMPTY_UNSPECIFIED, \ + .keep_empty = 1, \ + .default_backend = "merge", \ + .flags = REBASE_NO_QUIET, \ + .git_am_opts = STRVEC_INIT, \ + .exec = STRING_LIST_INIT_NODUP, \ + .git_format_patch_opt = STRBUF_INIT, \ + .fork_point = -1, \ + .reapply_cherry_picks = -1, \ + .allow_empty_message = 1, \ + .autosquash = -1, \ + .config_autosquash = -1, \ + .rebase_merges = -1, \ + .config_rebase_merges = -1, \ + .update_refs = -1, \ + .config_update_refs = -1, \ + .strategy_opts = STRING_LIST_INIT_NODUP,\ + } + +static struct replay_opts get_replay_opts(const struct rebase_options *opts) +{ + struct replay_opts replay = REPLAY_OPTS_INIT; + + replay.action = REPLAY_INTERACTIVE_REBASE; + replay.strategy = NULL; + sequencer_init_config(&replay); + + replay.signoff = opts->signoff; + replay.allow_ff = !(opts->flags & REBASE_FORCE); + if (opts->allow_rerere_autoupdate) + replay.allow_rerere_auto = opts->allow_rerere_autoupdate; + replay.allow_empty = 1; + replay.allow_empty_message = opts->allow_empty_message; + replay.drop_redundant_commits = (opts->empty == EMPTY_DROP); + replay.keep_redundant_commits = (opts->empty == EMPTY_KEEP); + replay.quiet = !(opts->flags & REBASE_NO_QUIET); + replay.verbose = opts->flags & REBASE_VERBOSE; + replay.reschedule_failed_exec = opts->reschedule_failed_exec; + replay.committer_date_is_author_date = + opts->committer_date_is_author_date; + replay.ignore_date = opts->ignore_date; + replay.gpg_sign = xstrdup_or_null(opts->gpg_sign_opt); + replay.reflog_action = xstrdup(opts->reflog_action); + if (opts->strategy) + replay.strategy = xstrdup_or_null(opts->strategy); + else if (!replay.strategy && replay.default_strategy) { + replay.strategy = replay.default_strategy; + replay.default_strategy = NULL; + } + + for (size_t i = 0; i < opts->strategy_opts.nr; i++) + strvec_push(&replay.xopts, opts->strategy_opts.items[i].string); + + if (opts->squash_onto) { + oidcpy(&replay.squash_onto, opts->squash_onto); + replay.have_squash_onto = 1; + } + + return replay; +} + +static int edit_todo_file(unsigned flags) +{ + const char *todo_file = rebase_path_todo(); + struct todo_list todo_list = TODO_LIST_INIT, + new_todo = TODO_LIST_INIT; + int res = 0; + + if (strbuf_read_file(&todo_list.buf, todo_file, 0) < 0) + return error_errno(_("could not read '%s'."), todo_file); + + strbuf_stripspace(&todo_list.buf, comment_line_char); + res = edit_todo_list(the_repository, &todo_list, &new_todo, NULL, NULL, flags); + if (!res && todo_list_write_to_file(the_repository, &new_todo, todo_file, + NULL, NULL, -1, flags & ~(TODO_LIST_SHORTEN_IDS))) + res = error_errno(_("could not write '%s'"), todo_file); + + todo_list_release(&todo_list); + todo_list_release(&new_todo); + + return res; +} + +static int get_revision_ranges(struct commit *upstream, struct commit *onto, + struct object_id *orig_head, char **revisions, + char **shortrevisions) +{ + struct commit *base_rev = upstream ? upstream : onto; + const char *shorthead; + + *revisions = xstrfmt("%s...%s", oid_to_hex(&base_rev->object.oid), + oid_to_hex(orig_head)); + + shorthead = repo_find_unique_abbrev(the_repository, orig_head, + DEFAULT_ABBREV); + + if (upstream) { + const char *shortrev; + + shortrev = repo_find_unique_abbrev(the_repository, + &base_rev->object.oid, + DEFAULT_ABBREV); + + *shortrevisions = xstrfmt("%s..%s", shortrev, shorthead); + } else + *shortrevisions = xstrdup(shorthead); + + return 0; +} + +static int init_basic_state(struct replay_opts *opts, const char *head_name, + struct commit *onto, + const struct object_id *orig_head) +{ + FILE *interactive; + + if (!is_directory(merge_dir()) && mkdir_in_gitdir(merge_dir())) + return error_errno(_("could not create temporary %s"), merge_dir()); + + delete_reflog("REBASE_HEAD"); + + interactive = fopen(path_interactive(), "w"); + if (!interactive) + return error_errno(_("could not mark as interactive")); + fclose(interactive); + + return write_basic_state(opts, head_name, onto, orig_head); +} + +static int do_interactive_rebase(struct rebase_options *opts, unsigned flags) +{ + int ret = -1; + char *revisions = NULL, *shortrevisions = NULL; + struct strvec make_script_args = STRVEC_INIT; + struct todo_list todo_list = TODO_LIST_INIT; + struct replay_opts replay = get_replay_opts(opts); + + if (get_revision_ranges(opts->upstream, opts->onto, &opts->orig_head->object.oid, + &revisions, &shortrevisions)) + goto cleanup; + + if (init_basic_state(&replay, + opts->head_name ? opts->head_name : "detached HEAD", + opts->onto, &opts->orig_head->object.oid)) + goto cleanup; + + if (!opts->upstream && opts->squash_onto) + write_file(path_squash_onto(), "%s\n", + oid_to_hex(opts->squash_onto)); + + strvec_pushl(&make_script_args, "", revisions, NULL); + if (opts->restrict_revision) + strvec_pushf(&make_script_args, "^%s", + oid_to_hex(&opts->restrict_revision->object.oid)); + + ret = sequencer_make_script(the_repository, &todo_list.buf, + make_script_args.nr, make_script_args.v, + flags); + + if (ret) + error(_("could not generate todo list")); + else { + discard_index(&the_index); + if (todo_list_parse_insn_buffer(the_repository, todo_list.buf.buf, + &todo_list)) + BUG("unusable todo list"); + + ret = complete_action(the_repository, &replay, flags, + shortrevisions, opts->onto_name, opts->onto, + &opts->orig_head->object.oid, &opts->exec, + opts->autosquash, opts->update_refs, &todo_list); + } + +cleanup: + replay_opts_release(&replay); + free(revisions); + free(shortrevisions); + todo_list_release(&todo_list); + strvec_clear(&make_script_args); + + return ret; +} + +static int run_sequencer_rebase(struct rebase_options *opts) +{ + unsigned flags = 0; + int abbreviate_commands = 0, ret = 0; + + git_config_get_bool("rebase.abbreviatecommands", &abbreviate_commands); + + flags |= opts->keep_empty ? TODO_LIST_KEEP_EMPTY : 0; + flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0; + flags |= opts->rebase_merges ? TODO_LIST_REBASE_MERGES : 0; + flags |= opts->rebase_cousins > 0 ? TODO_LIST_REBASE_COUSINS : 0; + flags |= opts->root_with_onto ? TODO_LIST_ROOT_WITH_ONTO : 0; + flags |= opts->reapply_cherry_picks ? TODO_LIST_REAPPLY_CHERRY_PICKS : 0; + flags |= opts->flags & REBASE_NO_QUIET ? TODO_LIST_WARN_SKIPPED_CHERRY_PICKS : 0; + + switch (opts->action) { + case ACTION_NONE: { + if (!opts->onto && !opts->upstream) + die(_("a base commit must be provided with --upstream or --onto")); + + ret = do_interactive_rebase(opts, flags); + break; + } + case ACTION_SKIP: { + struct string_list merge_rr = STRING_LIST_INIT_DUP; + + rerere_clear(the_repository, &merge_rr); + } + /* fallthrough */ + case ACTION_CONTINUE: { + struct replay_opts replay_opts = get_replay_opts(opts); + + ret = sequencer_continue(the_repository, &replay_opts); + replay_opts_release(&replay_opts); + break; + } + case ACTION_EDIT_TODO: + ret = edit_todo_file(flags); + break; + case ACTION_SHOW_CURRENT_PATCH: { + struct child_process cmd = CHILD_PROCESS_INIT; + + cmd.git_cmd = 1; + strvec_pushl(&cmd.args, "show", "REBASE_HEAD", "--", NULL); + ret = run_command(&cmd); + + break; + } + default: + BUG("invalid command '%d'", opts->action); + } + + return ret; +} + +static int is_merge(struct rebase_options *opts) +{ + return opts->type == REBASE_MERGE; +} + +static void imply_merge(struct rebase_options *opts, const char *option) +{ + switch (opts->type) { + case REBASE_APPLY: + die(_("%s requires the merge backend"), option); + break; + case REBASE_MERGE: + break; + default: + opts->type = REBASE_MERGE; /* implied */ + break; + } +} + +/* Returns the filename prefixed by the state_dir */ +static const char *state_dir_path(const char *filename, struct rebase_options *opts) +{ + static struct strbuf path = STRBUF_INIT; + static size_t prefix_len; + + if (!prefix_len) { + strbuf_addf(&path, "%s/", opts->state_dir); + prefix_len = path.len; + } + + strbuf_setlen(&path, prefix_len); + strbuf_addstr(&path, filename); + return path.buf; +} + +/* Initialize the rebase options from the state directory. */ +static int read_basic_state(struct rebase_options *opts) +{ + struct strbuf head_name = STRBUF_INIT; + struct strbuf buf = STRBUF_INIT; + struct object_id oid; + + if (!read_oneliner(&head_name, state_dir_path("head-name", opts), + READ_ONELINER_WARN_MISSING) || + !read_oneliner(&buf, state_dir_path("onto", opts), + READ_ONELINER_WARN_MISSING)) + return -1; + opts->head_name = starts_with(head_name.buf, "refs/") ? + xstrdup(head_name.buf) : NULL; + strbuf_release(&head_name); + if (get_oid_hex(buf.buf, &oid) || + !(opts->onto = lookup_commit_object(the_repository, &oid))) + return error(_("invalid onto: '%s'"), buf.buf); + + /* + * We always write to orig-head, but interactive rebase used to write to + * head. Fall back to reading from head to cover for the case that the + * user upgraded git with an ongoing interactive rebase. + */ + strbuf_reset(&buf); + if (file_exists(state_dir_path("orig-head", opts))) { + if (!read_oneliner(&buf, state_dir_path("orig-head", opts), + READ_ONELINER_WARN_MISSING)) + return -1; + } else if (!read_oneliner(&buf, state_dir_path("head", opts), + READ_ONELINER_WARN_MISSING)) + return -1; + if (get_oid_hex(buf.buf, &oid) || + !(opts->orig_head = lookup_commit_object(the_repository, &oid))) + return error(_("invalid orig-head: '%s'"), buf.buf); + + if (file_exists(state_dir_path("quiet", opts))) + opts->flags &= ~REBASE_NO_QUIET; + else + opts->flags |= REBASE_NO_QUIET; + + if (file_exists(state_dir_path("verbose", opts))) + opts->flags |= REBASE_VERBOSE; + + if (file_exists(state_dir_path("signoff", opts))) { + opts->signoff = 1; + opts->flags |= REBASE_FORCE; + } + + if (file_exists(state_dir_path("allow_rerere_autoupdate", opts))) { + strbuf_reset(&buf); + if (!read_oneliner(&buf, state_dir_path("allow_rerere_autoupdate", opts), + READ_ONELINER_WARN_MISSING)) + return -1; + if (!strcmp(buf.buf, "--rerere-autoupdate")) + opts->allow_rerere_autoupdate = RERERE_AUTOUPDATE; + else if (!strcmp(buf.buf, "--no-rerere-autoupdate")) + opts->allow_rerere_autoupdate = RERERE_NOAUTOUPDATE; + else + warning(_("ignoring invalid allow_rerere_autoupdate: " + "'%s'"), buf.buf); + } + + if (file_exists(state_dir_path("gpg_sign_opt", opts))) { + strbuf_reset(&buf); + if (!read_oneliner(&buf, state_dir_path("gpg_sign_opt", opts), + READ_ONELINER_WARN_MISSING)) + return -1; + free(opts->gpg_sign_opt); + opts->gpg_sign_opt = xstrdup(buf.buf); + } + + strbuf_release(&buf); + + return 0; +} + +static int rebase_write_basic_state(struct rebase_options *opts) +{ + write_file(state_dir_path("head-name", opts), "%s", + opts->head_name ? opts->head_name : "detached HEAD"); + write_file(state_dir_path("onto", opts), "%s", + opts->onto ? oid_to_hex(&opts->onto->object.oid) : ""); + write_file(state_dir_path("orig-head", opts), "%s", + oid_to_hex(&opts->orig_head->object.oid)); + if (!(opts->flags & REBASE_NO_QUIET)) + write_file(state_dir_path("quiet", opts), "%s", ""); + if (opts->flags & REBASE_VERBOSE) + write_file(state_dir_path("verbose", opts), "%s", ""); + if (opts->allow_rerere_autoupdate > 0) + write_file(state_dir_path("allow_rerere_autoupdate", opts), + "-%s-rerere-autoupdate", + opts->allow_rerere_autoupdate == RERERE_AUTOUPDATE ? + "" : "-no"); + if (opts->gpg_sign_opt) + write_file(state_dir_path("gpg_sign_opt", opts), "%s", + opts->gpg_sign_opt); + if (opts->signoff) + write_file(state_dir_path("signoff", opts), "--signoff"); + + return 0; +} + +static int finish_rebase(struct rebase_options *opts) +{ + struct strbuf dir = STRBUF_INIT; + int ret = 0; + + delete_ref(NULL, "REBASE_HEAD", NULL, REF_NO_DEREF); + unlink(git_path_auto_merge(the_repository)); + apply_autostash(state_dir_path("autostash", opts)); + /* + * We ignore errors in 'git maintenance run --auto', since the + * user should see them. + */ + run_auto_maintenance(!(opts->flags & (REBASE_NO_QUIET|REBASE_VERBOSE))); + if (opts->type == REBASE_MERGE) { + struct replay_opts replay = REPLAY_OPTS_INIT; + + replay.action = REPLAY_INTERACTIVE_REBASE; + ret = sequencer_remove_state(&replay); + replay_opts_release(&replay); + } else { + strbuf_addstr(&dir, opts->state_dir); + if (remove_dir_recursively(&dir, 0)) + ret = error(_("could not remove '%s'"), + opts->state_dir); + strbuf_release(&dir); + } + + return ret; +} + +static int move_to_original_branch(struct rebase_options *opts) +{ + struct strbuf branch_reflog = STRBUF_INIT, head_reflog = STRBUF_INIT; + struct reset_head_opts ropts = { 0 }; + int ret; + + if (!opts->head_name) + return 0; /* nothing to move back to */ + + if (!opts->onto) + BUG("move_to_original_branch without onto"); + + strbuf_addf(&branch_reflog, "%s (finish): %s onto %s", + opts->reflog_action, + opts->head_name, oid_to_hex(&opts->onto->object.oid)); + strbuf_addf(&head_reflog, "%s (finish): returning to %s", + opts->reflog_action, opts->head_name); + ropts.branch = opts->head_name; + ropts.flags = RESET_HEAD_REFS_ONLY; + ropts.branch_msg = branch_reflog.buf; + ropts.head_msg = head_reflog.buf; + ret = reset_head(the_repository, &ropts); + + strbuf_release(&branch_reflog); + strbuf_release(&head_reflog); + return ret; +} + +static const char *resolvemsg = +N_("Resolve all conflicts manually, mark them as resolved with\n" +"\"git add/rm <conflicted_files>\", then run \"git rebase --continue\".\n" +"You can instead skip this commit: run \"git rebase --skip\".\n" +"To abort and get back to the state before \"git rebase\", run " +"\"git rebase --abort\"."); + +static int run_am(struct rebase_options *opts) +{ + struct child_process am = CHILD_PROCESS_INIT; + struct child_process format_patch = CHILD_PROCESS_INIT; + struct strbuf revisions = STRBUF_INIT; + int status; + char *rebased_patches; + + am.git_cmd = 1; + strvec_push(&am.args, "am"); + strvec_pushf(&am.env, GIT_REFLOG_ACTION_ENVIRONMENT "=%s (pick)", + opts->reflog_action); + if (opts->action == ACTION_CONTINUE) { + strvec_push(&am.args, "--resolved"); + strvec_pushf(&am.args, "--resolvemsg=%s", resolvemsg); + if (opts->gpg_sign_opt) + strvec_push(&am.args, opts->gpg_sign_opt); + status = run_command(&am); + if (status) + return status; + + return move_to_original_branch(opts); + } + if (opts->action == ACTION_SKIP) { + strvec_push(&am.args, "--skip"); + strvec_pushf(&am.args, "--resolvemsg=%s", resolvemsg); + status = run_command(&am); + if (status) + return status; + + return move_to_original_branch(opts); + } + if (opts->action == ACTION_SHOW_CURRENT_PATCH) { + strvec_push(&am.args, "--show-current-patch"); + return run_command(&am); + } + + strbuf_addf(&revisions, "%s...%s", + oid_to_hex(opts->root ? + /* this is now equivalent to !opts->upstream */ + &opts->onto->object.oid : + &opts->upstream->object.oid), + oid_to_hex(&opts->orig_head->object.oid)); + + rebased_patches = xstrdup(git_path("rebased-patches")); + format_patch.out = open(rebased_patches, + O_WRONLY | O_CREAT | O_TRUNC, 0666); + if (format_patch.out < 0) { + status = error_errno(_("could not open '%s' for writing"), + rebased_patches); + free(rebased_patches); + strvec_clear(&am.args); + return status; + } + + format_patch.git_cmd = 1; + strvec_pushl(&format_patch.args, "format-patch", "-k", "--stdout", + "--full-index", "--cherry-pick", "--right-only", + "--default-prefix", "--no-renames", + "--no-cover-letter", "--pretty=mboxrd", "--topo-order", + "--no-base", NULL); + if (opts->git_format_patch_opt.len) + strvec_split(&format_patch.args, + opts->git_format_patch_opt.buf); + strvec_push(&format_patch.args, revisions.buf); + if (opts->restrict_revision) + strvec_pushf(&format_patch.args, "^%s", + oid_to_hex(&opts->restrict_revision->object.oid)); + + status = run_command(&format_patch); + if (status) { + struct reset_head_opts ropts = { 0 }; + unlink(rebased_patches); + free(rebased_patches); + strvec_clear(&am.args); + + ropts.oid = &opts->orig_head->object.oid; + ropts.branch = opts->head_name; + ropts.default_reflog_action = opts->reflog_action; + reset_head(the_repository, &ropts); + error(_("\ngit encountered an error while preparing the " + "patches to replay\n" + "these revisions:\n" + "\n %s\n\n" + "As a result, git cannot rebase them."), + opts->revisions); + + strbuf_release(&revisions); + return status; + } + strbuf_release(&revisions); + + am.in = open(rebased_patches, O_RDONLY); + if (am.in < 0) { + status = error_errno(_("could not open '%s' for reading"), + rebased_patches); + free(rebased_patches); + strvec_clear(&am.args); + return status; + } + + strvec_pushv(&am.args, opts->git_am_opts.v); + strvec_push(&am.args, "--rebasing"); + strvec_pushf(&am.args, "--resolvemsg=%s", resolvemsg); + strvec_push(&am.args, "--patch-format=mboxrd"); + if (opts->allow_rerere_autoupdate == RERERE_AUTOUPDATE) + strvec_push(&am.args, "--rerere-autoupdate"); + else if (opts->allow_rerere_autoupdate == RERERE_NOAUTOUPDATE) + strvec_push(&am.args, "--no-rerere-autoupdate"); + if (opts->gpg_sign_opt) + strvec_push(&am.args, opts->gpg_sign_opt); + status = run_command(&am); + unlink(rebased_patches); + free(rebased_patches); + + if (!status) { + return move_to_original_branch(opts); + } + + if (is_directory(opts->state_dir)) + rebase_write_basic_state(opts); + + return status; +} + +static int run_specific_rebase(struct rebase_options *opts) +{ + int status; + + if (opts->type == REBASE_MERGE) { + /* Run sequencer-based rebase */ + setenv("GIT_CHERRY_PICK_HELP", resolvemsg, 1); + if (!(opts->flags & REBASE_INTERACTIVE_EXPLICIT)) { + setenv("GIT_SEQUENCE_EDITOR", ":", 1); + opts->autosquash = 0; + } + if (opts->gpg_sign_opt) { + /* remove the leading "-S" */ + char *tmp = xstrdup(opts->gpg_sign_opt + 2); + free(opts->gpg_sign_opt); + opts->gpg_sign_opt = tmp; + } + + status = run_sequencer_rebase(opts); + } else if (opts->type == REBASE_APPLY) + status = run_am(opts); + else + BUG("Unhandled rebase type %d", opts->type); + + if (opts->dont_finish_rebase) + ; /* do nothing */ + else if (opts->type == REBASE_MERGE) + ; /* merge backend cleans up after itself */ + else if (status == 0) { + if (!file_exists(state_dir_path("stopped-sha", opts))) + finish_rebase(opts); + } else if (status == 2) { + struct strbuf dir = STRBUF_INIT; + + apply_autostash(state_dir_path("autostash", opts)); + strbuf_addstr(&dir, opts->state_dir); + remove_dir_recursively(&dir, 0); + strbuf_release(&dir); + die("Nothing to do"); + } + + return status ? -1 : 0; +} + +static void parse_rebase_merges_value(struct rebase_options *options, const char *value) +{ + if (!strcmp("no-rebase-cousins", value)) + options->rebase_cousins = 0; + else if (!strcmp("rebase-cousins", value)) + options->rebase_cousins = 1; + else + die(_("Unknown rebase-merges mode: %s"), value); +} + +static int rebase_config(const char *var, const char *value, + const struct config_context *ctx, void *data) +{ + struct rebase_options *opts = data; + + if (!strcmp(var, "rebase.stat")) { + if (git_config_bool(var, value)) + opts->flags |= REBASE_DIFFSTAT; + else + opts->flags &= ~REBASE_DIFFSTAT; + return 0; + } + + if (!strcmp(var, "rebase.autosquash")) { + opts->config_autosquash = git_config_bool(var, value); + return 0; + } + + if (!strcmp(var, "commit.gpgsign")) { + free(opts->gpg_sign_opt); + opts->gpg_sign_opt = git_config_bool(var, value) ? + xstrdup("-S") : NULL; + return 0; + } + + if (!strcmp(var, "rebase.autostash")) { + opts->autostash = git_config_bool(var, value); + return 0; + } + + if (!strcmp(var, "rebase.rebasemerges")) { + opts->config_rebase_merges = git_parse_maybe_bool(value); + if (opts->config_rebase_merges < 0) { + opts->config_rebase_merges = 1; + parse_rebase_merges_value(opts, value); + } else { + opts->rebase_cousins = 0; + } + return 0; + } + + if (!strcmp(var, "rebase.updaterefs")) { + opts->config_update_refs = git_config_bool(var, value); + return 0; + } + + if (!strcmp(var, "rebase.reschedulefailedexec")) { + opts->reschedule_failed_exec = git_config_bool(var, value); + return 0; + } + + if (!strcmp(var, "rebase.forkpoint")) { + opts->fork_point = git_config_bool(var, value) ? -1 : 0; + return 0; + } + + if (!strcmp(var, "rebase.backend")) { + return git_config_string(&opts->default_backend, var, value); + } + + return git_default_config(var, value, ctx, data); +} + +static int checkout_up_to_date(struct rebase_options *options) +{ + struct strbuf buf = STRBUF_INIT; + struct reset_head_opts ropts = { 0 }; + int ret = 0; + + strbuf_addf(&buf, "%s: checkout %s", + options->reflog_action, options->switch_to); + ropts.oid = &options->orig_head->object.oid; + ropts.branch = options->head_name; + ropts.flags = RESET_HEAD_RUN_POST_CHECKOUT_HOOK; + if (!ropts.branch) + ropts.flags |= RESET_HEAD_DETACH; + ropts.head_msg = buf.buf; + if (reset_head(the_repository, &ropts) < 0) + ret = error(_("could not switch to %s"), options->switch_to); + strbuf_release(&buf); + + return ret; +} + +/* + * Determines whether the commits in from..to are linear, i.e. contain + * no merge commits. This function *expects* `from` to be an ancestor of + * `to`. + */ +static int is_linear_history(struct commit *from, struct commit *to) +{ + while (to && to != from) { + repo_parse_commit(the_repository, to); + if (!to->parents) + return 1; + if (to->parents->next) + return 0; + to = to->parents->item; + } + return 1; +} + +static int can_fast_forward(struct commit *onto, struct commit *upstream, + struct commit *restrict_revision, + struct commit *head, struct object_id *branch_base) +{ + struct commit_list *merge_bases = NULL; + int res = 0; + + if (is_null_oid(branch_base)) + goto done; /* fill_branch_base() found multiple merge bases */ + + if (!oideq(branch_base, &onto->object.oid)) + goto done; + + if (restrict_revision && !oideq(&restrict_revision->object.oid, branch_base)) + goto done; + + if (!upstream) + goto done; + + merge_bases = repo_get_merge_bases(the_repository, upstream, head); + if (!merge_bases || merge_bases->next) + goto done; + + if (!oideq(&onto->object.oid, &merge_bases->item->object.oid)) + goto done; + + res = 1; + +done: + free_commit_list(merge_bases); + return res && is_linear_history(onto, head); +} + +static void fill_branch_base(struct rebase_options *options, + struct object_id *branch_base) +{ + struct commit_list *merge_bases = NULL; + + merge_bases = repo_get_merge_bases(the_repository, options->onto, + options->orig_head); + if (!merge_bases || merge_bases->next) + oidcpy(branch_base, null_oid()); + else + oidcpy(branch_base, &merge_bases->item->object.oid); + + free_commit_list(merge_bases); +} + +static int parse_opt_am(const struct option *opt, const char *arg, int unset) +{ + struct rebase_options *opts = opt->value; + + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + + if (opts->type != REBASE_UNSPECIFIED && opts->type != REBASE_APPLY) + die(_("apply options and merge options cannot be used together")); + + opts->type = REBASE_APPLY; + + return 0; +} + +/* -i followed by -m is still -i */ +static int parse_opt_merge(const struct option *opt, const char *arg, int unset) +{ + struct rebase_options *opts = opt->value; + + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + + if (opts->type != REBASE_UNSPECIFIED && opts->type != REBASE_MERGE) + die(_("apply options and merge options cannot be used together")); + + opts->type = REBASE_MERGE; + + return 0; +} + +/* -i followed by -r is still explicitly interactive, but -r alone is not */ +static int parse_opt_interactive(const struct option *opt, const char *arg, + int unset) +{ + struct rebase_options *opts = opt->value; + + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + + if (opts->type != REBASE_UNSPECIFIED && opts->type != REBASE_MERGE) + die(_("apply options and merge options cannot be used together")); + + opts->type = REBASE_MERGE; + opts->flags |= REBASE_INTERACTIVE_EXPLICIT; + + return 0; +} + +static enum empty_type parse_empty_value(const char *value) +{ + if (!strcasecmp(value, "drop")) + return EMPTY_DROP; + else if (!strcasecmp(value, "keep")) + return EMPTY_KEEP; + else if (!strcasecmp(value, "ask")) + return EMPTY_ASK; + + die(_("unrecognized empty type '%s'; valid values are \"drop\", \"keep\", and \"ask\"."), value); +} + +static int parse_opt_keep_empty(const struct option *opt, const char *arg, + int unset) +{ + struct rebase_options *opts = opt->value; + + BUG_ON_OPT_ARG(arg); + + imply_merge(opts, unset ? "--no-keep-empty" : "--keep-empty"); + opts->keep_empty = !unset; + return 0; +} + +static int parse_opt_empty(const struct option *opt, const char *arg, int unset) +{ + struct rebase_options *options = opt->value; + enum empty_type value = parse_empty_value(arg); + + BUG_ON_OPT_NEG(unset); + + options->empty = value; + return 0; +} + +static int parse_opt_rebase_merges(const struct option *opt, const char *arg, int unset) +{ + struct rebase_options *options = opt->value; + + options->rebase_merges = !unset; + options->rebase_cousins = 0; + + if (arg) { + if (!*arg) { + warning(_("--rebase-merges with an empty string " + "argument is deprecated and will stop " + "working in a future version of Git. Use " + "--rebase-merges without an argument " + "instead, which does the same thing.")); + return 0; + } + parse_rebase_merges_value(options, arg); + } + + return 0; +} + +static void NORETURN error_on_missing_default_upstream(void) +{ + struct branch *current_branch = branch_get(NULL); + + printf(_("%s\n" + "Please specify which branch you want to rebase against.\n" + "See git-rebase(1) for details.\n" + "\n" + " git rebase '<branch>'\n" + "\n"), + current_branch ? _("There is no tracking information for " + "the current branch.") : + _("You are not currently on a branch.")); + + if (current_branch) { + const char *remote = current_branch->remote_name; + + if (!remote) + remote = _("<remote>"); + + printf(_("If you wish to set tracking information for this " + "branch you can do so with:\n" + "\n" + " git branch --set-upstream-to=%s/<branch> %s\n" + "\n"), + remote, current_branch->name); + } + exit(1); +} + +static int check_exec_cmd(const char *cmd) +{ + if (strchr(cmd, '\n')) + return error(_("exec commands cannot contain newlines")); + + /* Does the command consist purely of whitespace? */ + if (!cmd[strspn(cmd, " \t\r\f\v")]) + return error(_("empty exec command")); + + return 0; +} + +int cmd_rebase(int argc, const char **argv, const char *prefix) +{ + struct rebase_options options = REBASE_OPTIONS_INIT; + const char *branch_name; + int ret, flags, total_argc, in_progress = 0; + int keep_base = 0; + int ok_to_skip_pre_rebase = 0; + struct strbuf msg = STRBUF_INIT; + struct strbuf revisions = STRBUF_INIT; + struct strbuf buf = STRBUF_INIT; + struct object_id branch_base; + int ignore_whitespace = 0; + const char *gpg_sign = NULL; + struct object_id squash_onto; + char *squash_onto_name = NULL; + char *keep_base_onto_name = NULL; + int reschedule_failed_exec = -1; + int allow_preemptive_ff = 1; + int preserve_merges_selected = 0; + struct reset_head_opts ropts = { 0 }; + struct option builtin_rebase_options[] = { + OPT_STRING(0, "onto", &options.onto_name, + N_("revision"), + N_("rebase onto given branch instead of upstream")), + OPT_BOOL(0, "keep-base", &keep_base, + N_("use the merge-base of upstream and branch as the current base")), + OPT_BOOL(0, "no-verify", &ok_to_skip_pre_rebase, + N_("allow pre-rebase hook to run")), + OPT_NEGBIT('q', "quiet", &options.flags, + N_("be quiet. implies --no-stat"), + REBASE_NO_QUIET | REBASE_VERBOSE | REBASE_DIFFSTAT), + OPT_BIT('v', "verbose", &options.flags, + N_("display a diffstat of what changed upstream"), + REBASE_NO_QUIET | REBASE_VERBOSE | REBASE_DIFFSTAT), + {OPTION_NEGBIT, 'n', "no-stat", &options.flags, NULL, + N_("do not show diffstat of what changed upstream"), + PARSE_OPT_NOARG, NULL, REBASE_DIFFSTAT }, + OPT_BOOL(0, "signoff", &options.signoff, + N_("add a Signed-off-by trailer to each commit")), + OPT_BOOL(0, "committer-date-is-author-date", + &options.committer_date_is_author_date, + N_("make committer date match author date")), + OPT_BOOL(0, "reset-author-date", &options.ignore_date, + N_("ignore author date and use current date")), + OPT_HIDDEN_BOOL(0, "ignore-date", &options.ignore_date, + N_("synonym of --reset-author-date")), + OPT_PASSTHRU_ARGV('C', NULL, &options.git_am_opts, N_("n"), + N_("passed to 'git apply'"), 0), + OPT_BOOL(0, "ignore-whitespace", &ignore_whitespace, + N_("ignore changes in whitespace")), + OPT_PASSTHRU_ARGV(0, "whitespace", &options.git_am_opts, + N_("action"), N_("passed to 'git apply'"), 0), + OPT_BIT('f', "force-rebase", &options.flags, + N_("cherry-pick all commits, even if unchanged"), + REBASE_FORCE), + OPT_BIT(0, "no-ff", &options.flags, + N_("cherry-pick all commits, even if unchanged"), + REBASE_FORCE), + OPT_CMDMODE(0, "continue", &options.action, N_("continue"), + ACTION_CONTINUE), + OPT_CMDMODE(0, "skip", &options.action, + N_("skip current patch and continue"), ACTION_SKIP), + OPT_CMDMODE(0, "abort", &options.action, + N_("abort and check out the original branch"), + ACTION_ABORT), + OPT_CMDMODE(0, "quit", &options.action, + N_("abort but keep HEAD where it is"), ACTION_QUIT), + OPT_CMDMODE(0, "edit-todo", &options.action, N_("edit the todo list " + "during an interactive rebase"), ACTION_EDIT_TODO), + OPT_CMDMODE(0, "show-current-patch", &options.action, + N_("show the patch file being applied or merged"), + ACTION_SHOW_CURRENT_PATCH), + OPT_CALLBACK_F(0, "apply", &options, NULL, + N_("use apply strategies to rebase"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, + parse_opt_am), + OPT_CALLBACK_F('m', "merge", &options, NULL, + N_("use merging strategies to rebase"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, + parse_opt_merge), + OPT_CALLBACK_F('i', "interactive", &options, NULL, + N_("let the user edit the list of commits to rebase"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, + parse_opt_interactive), + OPT_SET_INT_F('p', "preserve-merges", &preserve_merges_selected, + N_("(REMOVED) was: try to recreate merges " + "instead of ignoring them"), + 1, PARSE_OPT_HIDDEN), + OPT_RERERE_AUTOUPDATE(&options.allow_rerere_autoupdate), + OPT_CALLBACK_F(0, "empty", &options, "(drop|keep|ask)", + N_("how to handle commits that become empty"), + PARSE_OPT_NONEG, parse_opt_empty), + OPT_CALLBACK_F('k', "keep-empty", &options, NULL, + N_("keep commits which start empty"), + PARSE_OPT_NOARG | PARSE_OPT_HIDDEN, + parse_opt_keep_empty), + OPT_BOOL(0, "autosquash", &options.autosquash, + N_("move commits that begin with " + "squash!/fixup! under -i")), + OPT_BOOL(0, "update-refs", &options.update_refs, + N_("update branches that point to commits " + "that are being rebased")), + { OPTION_STRING, 'S', "gpg-sign", &gpg_sign, N_("key-id"), + N_("GPG-sign commits"), + PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + OPT_AUTOSTASH(&options.autostash), + OPT_STRING_LIST('x', "exec", &options.exec, N_("exec"), + N_("add exec lines after each commit of the " + "editable list")), + OPT_BOOL_F(0, "allow-empty-message", + &options.allow_empty_message, + N_("allow rebasing commits with empty messages"), + PARSE_OPT_HIDDEN), + OPT_CALLBACK_F('r', "rebase-merges", &options, N_("mode"), + N_("try to rebase merges instead of skipping them"), + PARSE_OPT_OPTARG, parse_opt_rebase_merges), + OPT_BOOL(0, "fork-point", &options.fork_point, + N_("use 'merge-base --fork-point' to refine upstream")), + OPT_STRING('s', "strategy", &options.strategy, + N_("strategy"), N_("use the given merge strategy")), + OPT_STRING_LIST('X', "strategy-option", &options.strategy_opts, + N_("option"), + N_("pass the argument through to the merge " + "strategy")), + OPT_BOOL(0, "root", &options.root, + N_("rebase all reachable commits up to the root(s)")), + OPT_BOOL(0, "reschedule-failed-exec", + &reschedule_failed_exec, + N_("automatically re-schedule any `exec` that fails")), + OPT_BOOL(0, "reapply-cherry-picks", &options.reapply_cherry_picks, + N_("apply all changes, even those already present upstream")), + OPT_END(), + }; + int i; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(builtin_rebase_usage, + builtin_rebase_options); + + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + git_config(rebase_config, &options); + /* options.gpg_sign_opt will be either "-S" or NULL */ + gpg_sign = options.gpg_sign_opt ? "" : NULL; + FREE_AND_NULL(options.gpg_sign_opt); + + strbuf_reset(&buf); + strbuf_addf(&buf, "%s/applying", apply_dir()); + if(file_exists(buf.buf)) + die(_("It looks like 'git am' is in progress. Cannot rebase.")); + + if (is_directory(apply_dir())) { + options.type = REBASE_APPLY; + options.state_dir = apply_dir(); + } else if (is_directory(merge_dir())) { + strbuf_reset(&buf); + strbuf_addf(&buf, "%s/rewritten", merge_dir()); + if (!(options.action == ACTION_ABORT) && is_directory(buf.buf)) { + die(_("`rebase --preserve-merges` (-p) is no longer supported.\n" + "Use `git rebase --abort` to terminate current rebase.\n" + "Or downgrade to v2.33, or earlier, to complete the rebase.")); + } else { + strbuf_reset(&buf); + strbuf_addf(&buf, "%s/interactive", merge_dir()); + options.type = REBASE_MERGE; + if (file_exists(buf.buf)) + options.flags |= REBASE_INTERACTIVE_EXPLICIT; + } + options.state_dir = merge_dir(); + } + + if (options.type != REBASE_UNSPECIFIED) + in_progress = 1; + + total_argc = argc; + argc = parse_options(argc, argv, prefix, + builtin_rebase_options, + builtin_rebase_usage, 0); + + if (preserve_merges_selected) + die(_("--preserve-merges was replaced by --rebase-merges\n" + "Note: Your `pull.rebase` configuration may also be set to 'preserve',\n" + "which is no longer supported; use 'merges' instead")); + + if (options.action != ACTION_NONE && total_argc != 2) { + usage_with_options(builtin_rebase_usage, + builtin_rebase_options); + } + + if (argc > 2) + usage_with_options(builtin_rebase_usage, + builtin_rebase_options); + + if (keep_base) { + if (options.onto_name) + die(_("options '%s' and '%s' cannot be used together"), "--keep-base", "--onto"); + if (options.root) + die(_("options '%s' and '%s' cannot be used together"), "--keep-base", "--root"); + /* + * --keep-base defaults to --no-fork-point to keep the + * base the same. + */ + if (options.fork_point < 0) + options.fork_point = 0; + } + if (options.root && options.fork_point > 0) + die(_("options '%s' and '%s' cannot be used together"), "--root", "--fork-point"); + + if (options.action != ACTION_NONE && !in_progress) + die(_("No rebase in progress?")); + + if (options.action == ACTION_EDIT_TODO && !is_merge(&options)) + die(_("The --edit-todo action can only be used during " + "interactive rebase.")); + + if (trace2_is_enabled()) { + if (is_merge(&options)) + trace2_cmd_mode("interactive"); + else if (options.exec.nr) + trace2_cmd_mode("interactive-exec"); + else + trace2_cmd_mode(action_names[options.action]); + } + + options.reflog_action = getenv(GIT_REFLOG_ACTION_ENVIRONMENT); + options.reflog_action = + xstrdup(options.reflog_action ? options.reflog_action : "rebase"); + + switch (options.action) { + case ACTION_CONTINUE: { + struct object_id head; + struct lock_file lock_file = LOCK_INIT; + int fd; + + /* Sanity check */ + if (repo_get_oid(the_repository, "HEAD", &head)) + die(_("Cannot read HEAD")); + + fd = repo_hold_locked_index(the_repository, &lock_file, 0); + if (repo_read_index(the_repository) < 0) + die(_("could not read index")); + refresh_index(the_repository->index, REFRESH_QUIET, NULL, NULL, + NULL); + if (0 <= fd) + repo_update_index_if_able(the_repository, &lock_file); + rollback_lock_file(&lock_file); + + if (has_unstaged_changes(the_repository, 1)) { + puts(_("You must edit all merge conflicts and then\n" + "mark them as resolved using git add")); + exit(1); + } + if (read_basic_state(&options)) + exit(1); + goto run_rebase; + } + case ACTION_SKIP: { + struct string_list merge_rr = STRING_LIST_INIT_DUP; + + rerere_clear(the_repository, &merge_rr); + string_list_clear(&merge_rr, 1); + ropts.flags = RESET_HEAD_HARD; + if (reset_head(the_repository, &ropts) < 0) + die(_("could not discard worktree changes")); + remove_branch_state(the_repository, 0); + if (read_basic_state(&options)) + exit(1); + goto run_rebase; + } + case ACTION_ABORT: { + struct string_list merge_rr = STRING_LIST_INIT_DUP; + struct strbuf head_msg = STRBUF_INIT; + + rerere_clear(the_repository, &merge_rr); + string_list_clear(&merge_rr, 1); + + if (read_basic_state(&options)) + exit(1); + + strbuf_addf(&head_msg, "%s (abort): returning to %s", + options.reflog_action, + options.head_name ? options.head_name + : oid_to_hex(&options.orig_head->object.oid)); + ropts.oid = &options.orig_head->object.oid; + ropts.head_msg = head_msg.buf; + ropts.branch = options.head_name; + ropts.flags = RESET_HEAD_HARD; + if (reset_head(the_repository, &ropts) < 0) + die(_("could not move back to %s"), + oid_to_hex(&options.orig_head->object.oid)); + strbuf_release(&head_msg); + remove_branch_state(the_repository, 0); + ret = finish_rebase(&options); + goto cleanup; + } + case ACTION_QUIT: { + save_autostash(state_dir_path("autostash", &options)); + if (options.type == REBASE_MERGE) { + struct replay_opts replay = REPLAY_OPTS_INIT; + + replay.action = REPLAY_INTERACTIVE_REBASE; + ret = sequencer_remove_state(&replay); + replay_opts_release(&replay); + } else { + strbuf_reset(&buf); + strbuf_addstr(&buf, options.state_dir); + ret = remove_dir_recursively(&buf, 0); + if (ret) + error(_("could not remove '%s'"), + options.state_dir); + } + goto cleanup; + } + case ACTION_EDIT_TODO: + options.dont_finish_rebase = 1; + goto run_rebase; + case ACTION_SHOW_CURRENT_PATCH: + options.dont_finish_rebase = 1; + goto run_rebase; + case ACTION_NONE: + break; + default: + BUG("action: %d", options.action); + } + + /* Make sure no rebase is in progress */ + if (in_progress) { + const char *last_slash = strrchr(options.state_dir, '/'); + const char *state_dir_base = + last_slash ? last_slash + 1 : options.state_dir; + const char *cmd_live_rebase = + "git rebase (--continue | --abort | --skip)"; + strbuf_reset(&buf); + strbuf_addf(&buf, "rm -fr \"%s\"", options.state_dir); + die(_("It seems that there is already a %s directory, and\n" + "I wonder if you are in the middle of another rebase. " + "If that is the\n" + "case, please try\n\t%s\n" + "If that is not the case, please\n\t%s\n" + "and run me again. I am stopping in case you still " + "have something\n" + "valuable there.\n"), + state_dir_base, cmd_live_rebase, buf.buf); + } + + if ((options.flags & REBASE_INTERACTIVE_EXPLICIT) || + (options.action != ACTION_NONE) || + (options.exec.nr > 0) || + (options.autosquash == -1 && options.config_autosquash == 1) || + options.autosquash == 1) { + allow_preemptive_ff = 0; + } + if (options.committer_date_is_author_date || options.ignore_date) + options.flags |= REBASE_FORCE; + + for (i = 0; i < options.git_am_opts.nr; i++) { + const char *option = options.git_am_opts.v[i], *p; + if (!strcmp(option, "--whitespace=fix") || + !strcmp(option, "--whitespace=strip")) + allow_preemptive_ff = 0; + else if (skip_prefix(option, "-C", &p)) { + while (*p) + if (!isdigit(*(p++))) + die(_("switch `C' expects a " + "numerical value")); + } else if (skip_prefix(option, "--whitespace=", &p)) { + if (*p && strcmp(p, "warn") && strcmp(p, "nowarn") && + strcmp(p, "error") && strcmp(p, "error-all")) + die("Invalid whitespace option: '%s'", p); + } + } + + for (i = 0; i < options.exec.nr; i++) + if (check_exec_cmd(options.exec.items[i].string)) + exit(1); + + if (!(options.flags & REBASE_NO_QUIET)) + strvec_push(&options.git_am_opts, "-q"); + + if (options.empty != EMPTY_UNSPECIFIED) + imply_merge(&options, "--empty"); + + if (options.reapply_cherry_picks < 0) + /* + * We default to --no-reapply-cherry-picks unless + * --keep-base is given; when --keep-base is given, we want + * to default to --reapply-cherry-picks. + */ + options.reapply_cherry_picks = keep_base; + else if (!keep_base) + /* + * The apply backend always searches for and drops cherry + * picks. This is often not wanted with --keep-base, so + * --keep-base allows --reapply-cherry-picks to be + * simulated by altering the upstream such that + * cherry-picks cannot be detected and thus all commits are + * reapplied. Thus, --[no-]reapply-cherry-picks is + * supported when --keep-base is specified, but not when + * --keep-base is left out. + */ + imply_merge(&options, options.reapply_cherry_picks ? + "--reapply-cherry-picks" : + "--no-reapply-cherry-picks"); + + if (gpg_sign) + options.gpg_sign_opt = xstrfmt("-S%s", gpg_sign); + + if (options.exec.nr) + imply_merge(&options, "--exec"); + + if (options.type == REBASE_APPLY) { + if (ignore_whitespace) + strvec_push(&options.git_am_opts, + "--ignore-whitespace"); + if (options.committer_date_is_author_date) + strvec_push(&options.git_am_opts, + "--committer-date-is-author-date"); + if (options.ignore_date) + strvec_push(&options.git_am_opts, "--ignore-date"); + } else { + /* REBASE_MERGE */ + if (ignore_whitespace) { + string_list_append(&options.strategy_opts, + "ignore-space-change"); + } + } + + if (options.strategy_opts.nr && !options.strategy) + options.strategy = "ort"; + + if (options.strategy) { + options.strategy = xstrdup(options.strategy); + imply_merge(&options, "--strategy"); + } + + if (options.root && !options.onto_name) + imply_merge(&options, "--root without --onto"); + + if (isatty(2) && options.flags & REBASE_NO_QUIET) + strbuf_addstr(&options.git_format_patch_opt, " --progress"); + + if (options.git_am_opts.nr || options.type == REBASE_APPLY) { + /* all am options except -q are compatible only with --apply */ + for (i = options.git_am_opts.nr - 1; i >= 0; i--) + if (strcmp(options.git_am_opts.v[i], "-q")) + break; + + if (i >= 0 || options.type == REBASE_APPLY) { + if (is_merge(&options)) + die(_("apply options and merge options " + "cannot be used together")); + else if (options.autosquash == -1 && options.config_autosquash == 1) + die(_("apply options are incompatible with rebase.autoSquash. Consider adding --no-autosquash")); + else if (options.rebase_merges == -1 && options.config_rebase_merges == 1) + die(_("apply options are incompatible with rebase.rebaseMerges. Consider adding --no-rebase-merges")); + else if (options.update_refs == -1 && options.config_update_refs == 1) + die(_("apply options are incompatible with rebase.updateRefs. Consider adding --no-update-refs")); + else + options.type = REBASE_APPLY; + } + } + + if (options.update_refs == 1) + imply_merge(&options, "--update-refs"); + options.update_refs = (options.update_refs >= 0) ? options.update_refs : + ((options.config_update_refs >= 0) ? options.config_update_refs : 0); + + if (options.rebase_merges == 1) + imply_merge(&options, "--rebase-merges"); + options.rebase_merges = (options.rebase_merges >= 0) ? options.rebase_merges : + ((options.config_rebase_merges >= 0) ? options.config_rebase_merges : 0); + + if (options.autosquash == 1) + imply_merge(&options, "--autosquash"); + options.autosquash = (options.autosquash >= 0) ? options.autosquash : + ((options.config_autosquash >= 0) ? options.config_autosquash : 0); + + if (options.type == REBASE_UNSPECIFIED) { + if (!strcmp(options.default_backend, "merge")) + options.type = REBASE_MERGE; + else if (!strcmp(options.default_backend, "apply")) + options.type = REBASE_APPLY; + else + die(_("Unknown rebase backend: %s"), + options.default_backend); + } + + if (options.type == REBASE_MERGE && + !options.strategy && + getenv("GIT_TEST_MERGE_ALGORITHM")) + options.strategy = xstrdup(getenv("GIT_TEST_MERGE_ALGORITHM")); + + switch (options.type) { + case REBASE_MERGE: + options.state_dir = merge_dir(); + break; + case REBASE_APPLY: + options.state_dir = apply_dir(); + break; + default: + BUG("options.type was just set above; should be unreachable."); + } + + if (options.empty == EMPTY_UNSPECIFIED) { + if (options.flags & REBASE_INTERACTIVE_EXPLICIT) + options.empty = EMPTY_ASK; + else if (options.exec.nr > 0) + options.empty = EMPTY_KEEP; + else + options.empty = EMPTY_DROP; + } + if (reschedule_failed_exec > 0 && !is_merge(&options)) + die(_("--reschedule-failed-exec requires " + "--exec or --interactive")); + if (reschedule_failed_exec >= 0) + options.reschedule_failed_exec = reschedule_failed_exec; + + if (options.signoff) { + strvec_push(&options.git_am_opts, "--signoff"); + options.flags |= REBASE_FORCE; + } + + if (!options.root) { + if (argc < 1) { + struct branch *branch; + + branch = branch_get(NULL); + options.upstream_name = branch_get_upstream(branch, + NULL); + if (!options.upstream_name) + error_on_missing_default_upstream(); + if (options.fork_point < 0) + options.fork_point = 1; + } else { + options.upstream_name = argv[0]; + argc--; + argv++; + if (!strcmp(options.upstream_name, "-")) + options.upstream_name = "@{-1}"; + } + options.upstream = + lookup_commit_reference_by_name(options.upstream_name); + if (!options.upstream) + die(_("invalid upstream '%s'"), options.upstream_name); + options.upstream_arg = options.upstream_name; + } else { + if (!options.onto_name) { + if (commit_tree("", 0, the_hash_algo->empty_tree, NULL, + &squash_onto, NULL, NULL) < 0) + die(_("Could not create new root commit")); + options.squash_onto = &squash_onto; + options.onto_name = squash_onto_name = + xstrdup(oid_to_hex(&squash_onto)); + } else + options.root_with_onto = 1; + + options.upstream_name = NULL; + options.upstream = NULL; + if (argc > 1) + usage_with_options(builtin_rebase_usage, + builtin_rebase_options); + options.upstream_arg = "--root"; + } + + /* + * If the branch to rebase is given, that is the branch we will rebase + * branch_name -- branch/commit being rebased, or + * HEAD (already detached) + * orig_head -- commit object name of tip of the branch before rebasing + * head_name -- refs/heads/<that-branch> or NULL (detached HEAD) + */ + if (argc == 1) { + /* Is it "rebase other branchname" or "rebase other commit"? */ + struct object_id branch_oid; + branch_name = argv[0]; + options.switch_to = argv[0]; + + /* Is it a local branch? */ + strbuf_reset(&buf); + strbuf_addf(&buf, "refs/heads/%s", branch_name); + if (!read_ref(buf.buf, &branch_oid)) { + die_if_checked_out(buf.buf, 1); + options.head_name = xstrdup(buf.buf); + options.orig_head = + lookup_commit_object(the_repository, + &branch_oid); + /* If not is it a valid ref (branch or commit)? */ + } else { + options.orig_head = + lookup_commit_reference_by_name(branch_name); + options.head_name = NULL; + } + if (!options.orig_head) + die(_("no such branch/commit '%s'"), branch_name); + } else if (argc == 0) { + /* Do not need to switch branches, we are already on it. */ + options.head_name = + xstrdup_or_null(resolve_ref_unsafe("HEAD", 0, NULL, + &flags)); + if (!options.head_name) + die(_("No such ref: %s"), "HEAD"); + if (flags & REF_ISSYMREF) { + if (!skip_prefix(options.head_name, + "refs/heads/", &branch_name)) + branch_name = options.head_name; + + } else { + FREE_AND_NULL(options.head_name); + branch_name = "HEAD"; + } + options.orig_head = lookup_commit_reference_by_name("HEAD"); + if (!options.orig_head) + die(_("Could not resolve HEAD to a commit")); + } else + BUG("unexpected number of arguments left to parse"); + + /* Make sure the branch to rebase onto is valid. */ + if (keep_base) { + strbuf_reset(&buf); + strbuf_addstr(&buf, options.upstream_name); + strbuf_addstr(&buf, "..."); + strbuf_addstr(&buf, branch_name); + options.onto_name = keep_base_onto_name = xstrdup(buf.buf); + } else if (!options.onto_name) + options.onto_name = options.upstream_name; + if (strstr(options.onto_name, "...")) { + if (repo_get_oid_mb(the_repository, options.onto_name, &branch_base) < 0) { + if (keep_base) + die(_("'%s': need exactly one merge base with branch"), + options.upstream_name); + else + die(_("'%s': need exactly one merge base"), + options.onto_name); + } + options.onto = lookup_commit_or_die(&branch_base, + options.onto_name); + } else { + options.onto = + lookup_commit_reference_by_name(options.onto_name); + if (!options.onto) + die(_("Does not point to a valid commit '%s'"), + options.onto_name); + fill_branch_base(&options, &branch_base); + } + + if (keep_base && options.reapply_cherry_picks) + options.upstream = options.onto; + + if (options.fork_point > 0) + options.restrict_revision = + get_fork_point(options.upstream_name, options.orig_head); + + if (repo_read_index(the_repository) < 0) + die(_("could not read index")); + + if (options.autostash) + create_autostash(the_repository, + state_dir_path("autostash", &options)); + + + if (require_clean_work_tree(the_repository, "rebase", + _("Please commit or stash them."), 1, 1)) { + ret = -1; + goto cleanup; + } + + /* + * Now we are rebasing commits upstream..orig_head (or with --root, + * everything leading up to orig_head) on top of onto. + */ + + /* + * Check if we are already based on onto with linear history, + * in which case we could fast-forward without replacing the commits + * with new commits recreated by replaying their changes. + */ + if (allow_preemptive_ff && + can_fast_forward(options.onto, options.upstream, options.restrict_revision, + options.orig_head, &branch_base)) { + int flag; + + if (!(options.flags & REBASE_FORCE)) { + /* Lazily switch to the target branch if needed... */ + if (options.switch_to) { + ret = checkout_up_to_date(&options); + if (ret) + goto cleanup; + } + + if (!(options.flags & REBASE_NO_QUIET)) + ; /* be quiet */ + else if (!strcmp(branch_name, "HEAD") && + resolve_ref_unsafe("HEAD", 0, NULL, &flag)) + puts(_("HEAD is up to date.")); + else + printf(_("Current branch %s is up to date.\n"), + branch_name); + ret = finish_rebase(&options); + goto cleanup; + } else if (!(options.flags & REBASE_NO_QUIET)) + ; /* be quiet */ + else if (!strcmp(branch_name, "HEAD") && + resolve_ref_unsafe("HEAD", 0, NULL, &flag)) + puts(_("HEAD is up to date, rebase forced.")); + else + printf(_("Current branch %s is up to date, rebase " + "forced.\n"), branch_name); + } + + /* If a hook exists, give it a chance to interrupt*/ + if (!ok_to_skip_pre_rebase && + run_hooks_l("pre-rebase", options.upstream_arg, + argc ? argv[0] : NULL, NULL)) + die(_("The pre-rebase hook refused to rebase.")); + + if (options.flags & REBASE_DIFFSTAT) { + struct diff_options opts; + + if (options.flags & REBASE_VERBOSE) { + if (is_null_oid(&branch_base)) + printf(_("Changes to %s:\n"), + oid_to_hex(&options.onto->object.oid)); + else + printf(_("Changes from %s to %s:\n"), + oid_to_hex(&branch_base), + oid_to_hex(&options.onto->object.oid)); + } + + /* We want color (if set), but no pager */ + repo_diff_setup(the_repository, &opts); + init_diffstat_widths(&opts); + opts.output_format |= + DIFF_FORMAT_SUMMARY | DIFF_FORMAT_DIFFSTAT; + opts.detect_rename = DIFF_DETECT_RENAME; + diff_setup_done(&opts); + diff_tree_oid(is_null_oid(&branch_base) ? + the_hash_algo->empty_tree : &branch_base, + &options.onto->object.oid, "", &opts); + diffcore_std(&opts); + diff_flush(&opts); + } + + if (is_merge(&options)) + goto run_rebase; + + /* Detach HEAD and reset the tree */ + if (options.flags & REBASE_NO_QUIET) + printf(_("First, rewinding head to replay your work on top of " + "it...\n")); + + strbuf_addf(&msg, "%s (start): checkout %s", + options.reflog_action, options.onto_name); + ropts.oid = &options.onto->object.oid; + ropts.orig_head = &options.orig_head->object.oid, + ropts.flags = RESET_HEAD_DETACH | RESET_ORIG_HEAD | + RESET_HEAD_RUN_POST_CHECKOUT_HOOK; + ropts.head_msg = msg.buf; + ropts.default_reflog_action = options.reflog_action; + if (reset_head(the_repository, &ropts)) + die(_("Could not detach HEAD")); + strbuf_release(&msg); + + /* + * If the onto is a proper descendant of the tip of the branch, then + * we just fast-forwarded. + */ + if (oideq(&branch_base, &options.orig_head->object.oid)) { + printf(_("Fast-forwarded %s to %s.\n"), + branch_name, options.onto_name); + move_to_original_branch(&options); + ret = finish_rebase(&options); + goto cleanup; + } + + strbuf_addf(&revisions, "%s..%s", + options.root ? oid_to_hex(&options.onto->object.oid) : + (options.restrict_revision ? + oid_to_hex(&options.restrict_revision->object.oid) : + oid_to_hex(&options.upstream->object.oid)), + oid_to_hex(&options.orig_head->object.oid)); + + options.revisions = revisions.buf; + +run_rebase: + ret = run_specific_rebase(&options); + +cleanup: + strbuf_release(&buf); + strbuf_release(&revisions); + free(options.reflog_action); + free(options.head_name); + strvec_clear(&options.git_am_opts); + free(options.gpg_sign_opt); + string_list_clear(&options.exec, 0); + free(options.strategy); + string_list_clear(&options.strategy_opts, 0); + strbuf_release(&options.git_format_patch_opt); + free(squash_onto_name); + free(keep_base_onto_name); + return !!ret; +} diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c new file mode 100644 index 0000000..8c4f0cb --- /dev/null +++ b/builtin/receive-pack.c @@ -0,0 +1,2628 @@ +#include "builtin.h" +#include "abspath.h" +#include "repository.h" +#include "config.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "lockfile.h" +#include "pack.h" +#include "refs.h" +#include "pkt-line.h" +#include "sideband.h" +#include "run-command.h" +#include "hook.h" +#include "exec-cmd.h" +#include "commit.h" +#include "object.h" +#include "remote.h" +#include "connect.h" +#include "string-list.h" +#include "oid-array.h" +#include "connected.h" +#include "strvec.h" +#include "version.h" +#include "tag.h" +#include "gpg-interface.h" +#include "sigchain.h" +#include "fsck.h" +#include "tmp-objdir.h" +#include "oidset.h" +#include "packfile.h" +#include "object-name.h" +#include "object-store-ll.h" +#include "path.h" +#include "protocol.h" +#include "commit-reach.h" +#include "server-info.h" +#include "trace.h" +#include "trace2.h" +#include "worktree.h" +#include "shallow.h" +#include "parse-options.h" + +static const char * const receive_pack_usage[] = { + N_("git receive-pack <git-dir>"), + NULL +}; + +enum deny_action { + DENY_UNCONFIGURED, + DENY_IGNORE, + DENY_WARN, + DENY_REFUSE, + DENY_UPDATE_INSTEAD +}; + +static int deny_deletes; +static int deny_non_fast_forwards; +static enum deny_action deny_current_branch = DENY_UNCONFIGURED; +static enum deny_action deny_delete_current = DENY_UNCONFIGURED; +static int receive_fsck_objects = -1; +static int transfer_fsck_objects = -1; +static struct strbuf fsck_msg_types = STRBUF_INIT; +static int receive_unpack_limit = -1; +static int transfer_unpack_limit = -1; +static int advertise_atomic_push = 1; +static int advertise_push_options; +static int advertise_sid; +static int unpack_limit = 100; +static off_t max_input_size; +static int report_status; +static int report_status_v2; +static int use_sideband; +static int use_atomic; +static int use_push_options; +static int quiet; +static int prefer_ofs_delta = 1; +static int auto_update_server_info; +static int auto_gc = 1; +static int reject_thin; +static int stateless_rpc; +static const char *service_dir; +static const char *head_name; +static void *head_name_to_free; +static int sent_capabilities; +static int shallow_update; +static const char *alt_shallow_file; +static struct strbuf push_cert = STRBUF_INIT; +static struct object_id push_cert_oid; +static struct signature_check sigcheck; +static const char *push_cert_nonce; +static const char *cert_nonce_seed; +static struct strvec hidden_refs = STRVEC_INIT; + +static const char *NONCE_UNSOLICITED = "UNSOLICITED"; +static const char *NONCE_BAD = "BAD"; +static const char *NONCE_MISSING = "MISSING"; +static const char *NONCE_OK = "OK"; +static const char *NONCE_SLOP = "SLOP"; +static const char *nonce_status; +static long nonce_stamp_slop; +static timestamp_t nonce_stamp_slop_limit; +static struct ref_transaction *transaction; + +static enum { + KEEPALIVE_NEVER = 0, + KEEPALIVE_AFTER_NUL, + KEEPALIVE_ALWAYS +} use_keepalive; +static int keepalive_in_sec = 5; + +static struct tmp_objdir *tmp_objdir; + +static struct proc_receive_ref { + unsigned int want_add:1, + want_delete:1, + want_modify:1, + negative_ref:1; + char *ref_prefix; + struct proc_receive_ref *next; +} *proc_receive_ref; + +static void proc_receive_ref_append(const char *prefix); + +static enum deny_action parse_deny_action(const char *var, const char *value) +{ + if (value) { + if (!strcasecmp(value, "ignore")) + return DENY_IGNORE; + if (!strcasecmp(value, "warn")) + return DENY_WARN; + if (!strcasecmp(value, "refuse")) + return DENY_REFUSE; + if (!strcasecmp(value, "updateinstead")) + return DENY_UPDATE_INSTEAD; + } + if (git_config_bool(var, value)) + return DENY_REFUSE; + return DENY_IGNORE; +} + +static int receive_pack_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + int status = parse_hide_refs_config(var, value, "receive", &hidden_refs); + + if (status) + return status; + + if (strcmp(var, "receive.denydeletes") == 0) { + deny_deletes = git_config_bool(var, value); + return 0; + } + + if (strcmp(var, "receive.denynonfastforwards") == 0) { + deny_non_fast_forwards = git_config_bool(var, value); + return 0; + } + + if (strcmp(var, "receive.unpacklimit") == 0) { + receive_unpack_limit = git_config_int(var, value, ctx->kvi); + return 0; + } + + if (strcmp(var, "transfer.unpacklimit") == 0) { + transfer_unpack_limit = git_config_int(var, value, ctx->kvi); + return 0; + } + + if (strcmp(var, "receive.fsck.skiplist") == 0) { + const char *path; + + if (git_config_pathname(&path, var, value)) + return 1; + strbuf_addf(&fsck_msg_types, "%cskiplist=%s", + fsck_msg_types.len ? ',' : '=', path); + free((char *)path); + return 0; + } + + if (skip_prefix(var, "receive.fsck.", &var)) { + if (is_valid_msg_type(var, value)) + strbuf_addf(&fsck_msg_types, "%c%s=%s", + fsck_msg_types.len ? ',' : '=', var, value); + else + warning("skipping unknown msg id '%s'", var); + return 0; + } + + if (strcmp(var, "receive.fsckobjects") == 0) { + receive_fsck_objects = git_config_bool(var, value); + return 0; + } + + if (strcmp(var, "transfer.fsckobjects") == 0) { + transfer_fsck_objects = git_config_bool(var, value); + return 0; + } + + if (!strcmp(var, "receive.denycurrentbranch")) { + deny_current_branch = parse_deny_action(var, value); + return 0; + } + + if (strcmp(var, "receive.denydeletecurrent") == 0) { + deny_delete_current = parse_deny_action(var, value); + return 0; + } + + if (strcmp(var, "repack.usedeltabaseoffset") == 0) { + prefer_ofs_delta = git_config_bool(var, value); + return 0; + } + + if (strcmp(var, "receive.updateserverinfo") == 0) { + auto_update_server_info = git_config_bool(var, value); + return 0; + } + + if (strcmp(var, "receive.autogc") == 0) { + auto_gc = git_config_bool(var, value); + return 0; + } + + if (strcmp(var, "receive.shallowupdate") == 0) { + shallow_update = git_config_bool(var, value); + return 0; + } + + if (strcmp(var, "receive.certnonceseed") == 0) + return git_config_string(&cert_nonce_seed, var, value); + + if (strcmp(var, "receive.certnonceslop") == 0) { + nonce_stamp_slop_limit = git_config_ulong(var, value, ctx->kvi); + return 0; + } + + if (strcmp(var, "receive.advertiseatomic") == 0) { + advertise_atomic_push = git_config_bool(var, value); + return 0; + } + + if (strcmp(var, "receive.advertisepushoptions") == 0) { + advertise_push_options = git_config_bool(var, value); + return 0; + } + + if (strcmp(var, "receive.keepalive") == 0) { + keepalive_in_sec = git_config_int(var, value, ctx->kvi); + return 0; + } + + if (strcmp(var, "receive.maxinputsize") == 0) { + max_input_size = git_config_int64(var, value, ctx->kvi); + return 0; + } + + if (strcmp(var, "receive.procreceiverefs") == 0) { + if (!value) + return config_error_nonbool(var); + proc_receive_ref_append(value); + return 0; + } + + if (strcmp(var, "transfer.advertisesid") == 0) { + advertise_sid = git_config_bool(var, value); + return 0; + } + + return git_default_config(var, value, ctx, cb); +} + +static void show_ref(const char *path, const struct object_id *oid) +{ + if (sent_capabilities) { + packet_write_fmt(1, "%s %s\n", oid_to_hex(oid), path); + } else { + struct strbuf cap = STRBUF_INIT; + + strbuf_addstr(&cap, + "report-status report-status-v2 delete-refs side-band-64k quiet"); + if (advertise_atomic_push) + strbuf_addstr(&cap, " atomic"); + if (prefer_ofs_delta) + strbuf_addstr(&cap, " ofs-delta"); + if (push_cert_nonce) + strbuf_addf(&cap, " push-cert=%s", push_cert_nonce); + if (advertise_push_options) + strbuf_addstr(&cap, " push-options"); + if (advertise_sid) + strbuf_addf(&cap, " session-id=%s", trace2_session_id()); + strbuf_addf(&cap, " object-format=%s", the_hash_algo->name); + strbuf_addf(&cap, " agent=%s", git_user_agent_sanitized()); + packet_write_fmt(1, "%s %s%c%s\n", + oid_to_hex(oid), path, 0, cap.buf); + strbuf_release(&cap); + sent_capabilities = 1; + } +} + +static int show_ref_cb(const char *path_full, const struct object_id *oid, + int flag UNUSED, void *data) +{ + struct oidset *seen = data; + const char *path = strip_namespace(path_full); + + if (ref_is_hidden(path, path_full, &hidden_refs)) + return 0; + + /* + * Advertise refs outside our current namespace as ".have" + * refs, so that the client can use them to minimize data + * transfer but will otherwise ignore them. + */ + if (!path) { + if (oidset_insert(seen, oid)) + return 0; + path = ".have"; + } else { + oidset_insert(seen, oid); + } + show_ref(path, oid); + return 0; +} + +static void show_one_alternate_ref(const struct object_id *oid, + void *data) +{ + struct oidset *seen = data; + + if (oidset_insert(seen, oid)) + return; + + show_ref(".have", oid); +} + +static void write_head_info(void) +{ + static struct oidset seen = OIDSET_INIT; + + refs_for_each_fullref_in(get_main_ref_store(the_repository), "", + hidden_refs_to_excludes(&hidden_refs), + show_ref_cb, &seen); + for_each_alternate_ref(show_one_alternate_ref, &seen); + oidset_clear(&seen); + if (!sent_capabilities) + show_ref("capabilities^{}", null_oid()); + + advertise_shallow_grafts(1); + + /* EOF */ + packet_flush(1); +} + +#define RUN_PROC_RECEIVE_SCHEDULED 1 +#define RUN_PROC_RECEIVE_RETURNED 2 +struct command { + struct command *next; + const char *error_string; + struct ref_push_report *report; + unsigned int skip_update:1, + did_not_exist:1, + run_proc_receive:2; + int index; + struct object_id old_oid; + struct object_id new_oid; + char ref_name[FLEX_ARRAY]; /* more */ +}; + +static void proc_receive_ref_append(const char *prefix) +{ + struct proc_receive_ref *ref_pattern; + char *p; + int len; + + CALLOC_ARRAY(ref_pattern, 1); + p = strchr(prefix, ':'); + if (p) { + while (prefix < p) { + if (*prefix == 'a') + ref_pattern->want_add = 1; + else if (*prefix == 'd') + ref_pattern->want_delete = 1; + else if (*prefix == 'm') + ref_pattern->want_modify = 1; + else if (*prefix == '!') + ref_pattern->negative_ref = 1; + prefix++; + } + prefix++; + } else { + ref_pattern->want_add = 1; + ref_pattern->want_delete = 1; + ref_pattern->want_modify = 1; + } + len = strlen(prefix); + while (len && prefix[len - 1] == '/') + len--; + ref_pattern->ref_prefix = xmemdupz(prefix, len); + if (!proc_receive_ref) { + proc_receive_ref = ref_pattern; + } else { + struct proc_receive_ref *end; + + end = proc_receive_ref; + while (end->next) + end = end->next; + end->next = ref_pattern; + } +} + +static int proc_receive_ref_matches(struct command *cmd) +{ + struct proc_receive_ref *p; + + if (!proc_receive_ref) + return 0; + + for (p = proc_receive_ref; p; p = p->next) { + const char *match = p->ref_prefix; + const char *remains; + + if (!p->want_add && is_null_oid(&cmd->old_oid)) + continue; + else if (!p->want_delete && is_null_oid(&cmd->new_oid)) + continue; + else if (!p->want_modify && + !is_null_oid(&cmd->old_oid) && + !is_null_oid(&cmd->new_oid)) + continue; + + if (skip_prefix(cmd->ref_name, match, &remains) && + (!*remains || *remains == '/')) { + if (!p->negative_ref) + return 1; + } else if (p->negative_ref) { + return 1; + } + } + return 0; +} + +static void report_message(const char *prefix, const char *err, va_list params) +{ + int sz; + char msg[4096]; + + sz = xsnprintf(msg, sizeof(msg), "%s", prefix); + sz += vsnprintf(msg + sz, sizeof(msg) - sz, err, params); + if (sz > (sizeof(msg) - 1)) + sz = sizeof(msg) - 1; + msg[sz++] = '\n'; + + if (use_sideband) + send_sideband(1, 2, msg, sz, use_sideband); + else + xwrite(2, msg, sz); +} + +__attribute__((format (printf, 1, 2))) +static void rp_warning(const char *err, ...) +{ + va_list params; + va_start(params, err); + report_message("warning: ", err, params); + va_end(params); +} + +__attribute__((format (printf, 1, 2))) +static void rp_error(const char *err, ...) +{ + va_list params; + va_start(params, err); + report_message("error: ", err, params); + va_end(params); +} + +static int copy_to_sideband(int in, int out UNUSED, void *arg UNUSED) +{ + char data[128]; + int keepalive_active = 0; + + if (keepalive_in_sec <= 0) + use_keepalive = KEEPALIVE_NEVER; + if (use_keepalive == KEEPALIVE_ALWAYS) + keepalive_active = 1; + + while (1) { + ssize_t sz; + + if (keepalive_active) { + struct pollfd pfd; + int ret; + + pfd.fd = in; + pfd.events = POLLIN; + ret = poll(&pfd, 1, 1000 * keepalive_in_sec); + + if (ret < 0) { + if (errno == EINTR) + continue; + else + break; + } else if (ret == 0) { + /* no data; send a keepalive packet */ + static const char buf[] = "0005\1"; + write_or_die(1, buf, sizeof(buf) - 1); + continue; + } /* else there is actual data to read */ + } + + sz = xread(in, data, sizeof(data)); + if (sz <= 0) + break; + + if (use_keepalive == KEEPALIVE_AFTER_NUL && !keepalive_active) { + const char *p = memchr(data, '\0', sz); + if (p) { + /* + * The NUL tells us to start sending keepalives. Make + * sure we send any other data we read along + * with it. + */ + keepalive_active = 1; + send_sideband(1, 2, data, p - data, use_sideband); + send_sideband(1, 2, p + 1, sz - (p - data + 1), use_sideband); + continue; + } + } + + /* + * Either we're not looking for a NUL signal, or we didn't see + * it yet; just pass along the data. + */ + send_sideband(1, 2, data, sz, use_sideband); + } + close(in); + return 0; +} + +static void hmac_hash(unsigned char *out, + const char *key_in, size_t key_len, + const char *text, size_t text_len) +{ + unsigned char key[GIT_MAX_BLKSZ]; + unsigned char k_ipad[GIT_MAX_BLKSZ]; + unsigned char k_opad[GIT_MAX_BLKSZ]; + int i; + git_hash_ctx ctx; + + /* RFC 2104 2. (1) */ + memset(key, '\0', GIT_MAX_BLKSZ); + if (the_hash_algo->blksz < key_len) { + the_hash_algo->init_fn(&ctx); + the_hash_algo->update_fn(&ctx, key_in, key_len); + the_hash_algo->final_fn(key, &ctx); + } else { + memcpy(key, key_in, key_len); + } + + /* RFC 2104 2. (2) & (5) */ + for (i = 0; i < sizeof(key); i++) { + k_ipad[i] = key[i] ^ 0x36; + k_opad[i] = key[i] ^ 0x5c; + } + + /* RFC 2104 2. (3) & (4) */ + the_hash_algo->init_fn(&ctx); + the_hash_algo->update_fn(&ctx, k_ipad, sizeof(k_ipad)); + the_hash_algo->update_fn(&ctx, text, text_len); + the_hash_algo->final_fn(out, &ctx); + + /* RFC 2104 2. (6) & (7) */ + the_hash_algo->init_fn(&ctx); + the_hash_algo->update_fn(&ctx, k_opad, sizeof(k_opad)); + the_hash_algo->update_fn(&ctx, out, the_hash_algo->rawsz); + the_hash_algo->final_fn(out, &ctx); +} + +static char *prepare_push_cert_nonce(const char *path, timestamp_t stamp) +{ + struct strbuf buf = STRBUF_INIT; + unsigned char hash[GIT_MAX_RAWSZ]; + + strbuf_addf(&buf, "%s:%"PRItime, path, stamp); + hmac_hash(hash, buf.buf, buf.len, cert_nonce_seed, strlen(cert_nonce_seed)); + strbuf_release(&buf); + + /* RFC 2104 5. HMAC-SHA1 or HMAC-SHA256 */ + strbuf_addf(&buf, "%"PRItime"-%.*s", stamp, (int)the_hash_algo->hexsz, hash_to_hex(hash)); + return strbuf_detach(&buf, NULL); +} + +static char *find_header(const char *msg, size_t len, const char *key, + const char **next_line) +{ + size_t out_len; + const char *val = find_header_mem(msg, len, key, &out_len); + + if (!val) + return NULL; + + if (next_line) + *next_line = val + out_len + 1; + + return xmemdupz(val, out_len); +} + +/* + * Return zero if a and b are equal up to n bytes and nonzero if they are not. + * This operation is guaranteed to run in constant time to avoid leaking data. + */ +static int constant_memequal(const char *a, const char *b, size_t n) +{ + int res = 0; + size_t i; + + for (i = 0; i < n; i++) + res |= a[i] ^ b[i]; + return res; +} + +static const char *check_nonce(const char *buf, size_t len) +{ + char *nonce = find_header(buf, len, "nonce", NULL); + timestamp_t stamp, ostamp; + char *bohmac, *expect = NULL; + const char *retval = NONCE_BAD; + size_t noncelen; + + if (!nonce) { + retval = NONCE_MISSING; + goto leave; + } else if (!push_cert_nonce) { + retval = NONCE_UNSOLICITED; + goto leave; + } else if (!strcmp(push_cert_nonce, nonce)) { + retval = NONCE_OK; + goto leave; + } + + if (!stateless_rpc) { + /* returned nonce MUST match what we gave out earlier */ + retval = NONCE_BAD; + goto leave; + } + + /* + * In stateless mode, we may be receiving a nonce issued by + * another instance of the server that serving the same + * repository, and the timestamps may not match, but the + * nonce-seed and dir should match, so we can recompute and + * report the time slop. + * + * In addition, when a nonce issued by another instance has + * timestamp within receive.certnonceslop seconds, we pretend + * as if we issued that nonce when reporting to the hook. + */ + + /* nonce is concat(<seconds-since-epoch>, "-", <hmac>) */ + if (*nonce <= '0' || '9' < *nonce) { + retval = NONCE_BAD; + goto leave; + } + stamp = parse_timestamp(nonce, &bohmac, 10); + if (bohmac == nonce || bohmac[0] != '-') { + retval = NONCE_BAD; + goto leave; + } + + noncelen = strlen(nonce); + expect = prepare_push_cert_nonce(service_dir, stamp); + if (noncelen != strlen(expect)) { + /* This is not even the right size. */ + retval = NONCE_BAD; + goto leave; + } + if (constant_memequal(expect, nonce, noncelen)) { + /* Not what we would have signed earlier */ + retval = NONCE_BAD; + goto leave; + } + + /* + * By how many seconds is this nonce stale? Negative value + * would mean it was issued by another server with its clock + * skewed in the future. + */ + ostamp = parse_timestamp(push_cert_nonce, NULL, 10); + nonce_stamp_slop = (long)ostamp - (long)stamp; + + if (nonce_stamp_slop_limit && + labs(nonce_stamp_slop) <= nonce_stamp_slop_limit) { + /* + * Pretend as if the received nonce (which passes the + * HMAC check, so it is not a forged by third-party) + * is what we issued. + */ + free((void *)push_cert_nonce); + push_cert_nonce = xstrdup(nonce); + retval = NONCE_OK; + } else { + retval = NONCE_SLOP; + } + +leave: + free(nonce); + free(expect); + return retval; +} + +/* + * Return 1 if there is no push_cert or if the push options in push_cert are + * the same as those in the argument; 0 otherwise. + */ +static int check_cert_push_options(const struct string_list *push_options) +{ + const char *buf = push_cert.buf; + int len = push_cert.len; + + char *option; + const char *next_line; + int options_seen = 0; + + int retval = 1; + + if (!len) + return 1; + + while ((option = find_header(buf, len, "push-option", &next_line))) { + len -= (next_line - buf); + buf = next_line; + options_seen++; + if (options_seen > push_options->nr + || strcmp(option, + push_options->items[options_seen - 1].string)) { + retval = 0; + goto leave; + } + free(option); + } + + if (options_seen != push_options->nr) + retval = 0; + +leave: + free(option); + return retval; +} + +static void prepare_push_cert_sha1(struct child_process *proc) +{ + static int already_done; + + if (!push_cert.len) + return; + + if (!already_done) { + int bogs /* beginning_of_gpg_sig */; + + already_done = 1; + if (write_object_file(push_cert.buf, push_cert.len, OBJ_BLOB, + &push_cert_oid)) + oidclr(&push_cert_oid); + + memset(&sigcheck, '\0', sizeof(sigcheck)); + + bogs = parse_signed_buffer(push_cert.buf, push_cert.len); + sigcheck.payload = xmemdupz(push_cert.buf, bogs); + sigcheck.payload_len = bogs; + check_signature(&sigcheck, push_cert.buf + bogs, + push_cert.len - bogs); + + nonce_status = check_nonce(push_cert.buf, bogs); + } + if (!is_null_oid(&push_cert_oid)) { + strvec_pushf(&proc->env, "GIT_PUSH_CERT=%s", + oid_to_hex(&push_cert_oid)); + strvec_pushf(&proc->env, "GIT_PUSH_CERT_SIGNER=%s", + sigcheck.signer ? sigcheck.signer : ""); + strvec_pushf(&proc->env, "GIT_PUSH_CERT_KEY=%s", + sigcheck.key ? sigcheck.key : ""); + strvec_pushf(&proc->env, "GIT_PUSH_CERT_STATUS=%c", + sigcheck.result); + if (push_cert_nonce) { + strvec_pushf(&proc->env, + "GIT_PUSH_CERT_NONCE=%s", + push_cert_nonce); + strvec_pushf(&proc->env, + "GIT_PUSH_CERT_NONCE_STATUS=%s", + nonce_status); + if (nonce_status == NONCE_SLOP) + strvec_pushf(&proc->env, + "GIT_PUSH_CERT_NONCE_SLOP=%ld", + nonce_stamp_slop); + } + } +} + +struct receive_hook_feed_state { + struct command *cmd; + struct ref_push_report *report; + int skip_broken; + struct strbuf buf; + const struct string_list *push_options; +}; + +typedef int (*feed_fn)(void *, const char **, size_t *); +static int run_and_feed_hook(const char *hook_name, feed_fn feed, + struct receive_hook_feed_state *feed_state) +{ + struct child_process proc = CHILD_PROCESS_INIT; + struct async muxer; + int code; + const char *hook_path = find_hook(hook_name); + + if (!hook_path) + return 0; + + strvec_push(&proc.args, hook_path); + proc.in = -1; + proc.stdout_to_stderr = 1; + proc.trace2_hook_name = hook_name; + + if (feed_state->push_options) { + size_t i; + for (i = 0; i < feed_state->push_options->nr; i++) + strvec_pushf(&proc.env, + "GIT_PUSH_OPTION_%"PRIuMAX"=%s", + (uintmax_t)i, + feed_state->push_options->items[i].string); + strvec_pushf(&proc.env, "GIT_PUSH_OPTION_COUNT=%"PRIuMAX"", + (uintmax_t)feed_state->push_options->nr); + } else + strvec_pushf(&proc.env, "GIT_PUSH_OPTION_COUNT"); + + if (tmp_objdir) + strvec_pushv(&proc.env, tmp_objdir_env(tmp_objdir)); + + if (use_sideband) { + memset(&muxer, 0, sizeof(muxer)); + muxer.proc = copy_to_sideband; + muxer.in = -1; + code = start_async(&muxer); + if (code) + return code; + proc.err = muxer.in; + } + + prepare_push_cert_sha1(&proc); + + code = start_command(&proc); + if (code) { + if (use_sideband) + finish_async(&muxer); + return code; + } + + sigchain_push(SIGPIPE, SIG_IGN); + + while (1) { + const char *buf; + size_t n; + if (feed(feed_state, &buf, &n)) + break; + if (write_in_full(proc.in, buf, n) < 0) + break; + } + close(proc.in); + if (use_sideband) + finish_async(&muxer); + + sigchain_pop(SIGPIPE); + + return finish_command(&proc); +} + +static int feed_receive_hook(void *state_, const char **bufp, size_t *sizep) +{ + struct receive_hook_feed_state *state = state_; + struct command *cmd = state->cmd; + + while (cmd && + state->skip_broken && (cmd->error_string || cmd->did_not_exist)) + cmd = cmd->next; + if (!cmd) + return -1; /* EOF */ + if (!bufp) + return 0; /* OK, can feed something. */ + strbuf_reset(&state->buf); + if (!state->report) + state->report = cmd->report; + if (state->report) { + struct object_id *old_oid; + struct object_id *new_oid; + const char *ref_name; + + old_oid = state->report->old_oid ? state->report->old_oid : &cmd->old_oid; + new_oid = state->report->new_oid ? state->report->new_oid : &cmd->new_oid; + ref_name = state->report->ref_name ? state->report->ref_name : cmd->ref_name; + strbuf_addf(&state->buf, "%s %s %s\n", + oid_to_hex(old_oid), oid_to_hex(new_oid), + ref_name); + state->report = state->report->next; + if (!state->report) + state->cmd = cmd->next; + } else { + strbuf_addf(&state->buf, "%s %s %s\n", + oid_to_hex(&cmd->old_oid), oid_to_hex(&cmd->new_oid), + cmd->ref_name); + state->cmd = cmd->next; + } + if (bufp) { + *bufp = state->buf.buf; + *sizep = state->buf.len; + } + return 0; +} + +static int run_receive_hook(struct command *commands, + const char *hook_name, + int skip_broken, + const struct string_list *push_options) +{ + struct receive_hook_feed_state state; + int status; + + strbuf_init(&state.buf, 0); + state.cmd = commands; + state.skip_broken = skip_broken; + state.report = NULL; + if (feed_receive_hook(&state, NULL, NULL)) + return 0; + state.cmd = commands; + state.push_options = push_options; + status = run_and_feed_hook(hook_name, feed_receive_hook, &state); + strbuf_release(&state.buf); + return status; +} + +static int run_update_hook(struct command *cmd) +{ + struct child_process proc = CHILD_PROCESS_INIT; + int code; + const char *hook_path = find_hook("update"); + + if (!hook_path) + return 0; + + strvec_push(&proc.args, hook_path); + strvec_push(&proc.args, cmd->ref_name); + strvec_push(&proc.args, oid_to_hex(&cmd->old_oid)); + strvec_push(&proc.args, oid_to_hex(&cmd->new_oid)); + + proc.no_stdin = 1; + proc.stdout_to_stderr = 1; + proc.err = use_sideband ? -1 : 0; + proc.trace2_hook_name = "update"; + + code = start_command(&proc); + if (code) + return code; + if (use_sideband) + copy_to_sideband(proc.err, -1, NULL); + return finish_command(&proc); +} + +static struct command *find_command_by_refname(struct command *list, + const char *refname) +{ + for (; list; list = list->next) + if (!strcmp(list->ref_name, refname)) + return list; + return NULL; +} + +static int read_proc_receive_report(struct packet_reader *reader, + struct command *commands, + struct strbuf *errmsg) +{ + struct command *cmd; + struct command *hint = NULL; + struct ref_push_report *report = NULL; + int new_report = 0; + int code = 0; + int once = 0; + int response = 0; + + for (;;) { + struct object_id old_oid, new_oid; + const char *head; + const char *refname; + char *p; + enum packet_read_status status; + + status = packet_reader_read(reader); + if (status != PACKET_READ_NORMAL) { + /* Check whether proc-receive exited abnormally */ + if (status == PACKET_READ_EOF && !response) { + strbuf_addstr(errmsg, "proc-receive exited abnormally"); + return -1; + } + break; + } + response++; + + head = reader->line; + p = strchr(head, ' '); + if (!p) { + strbuf_addf(errmsg, "proc-receive reported incomplete status line: '%s'\n", head); + code = -1; + continue; + } + *p++ = '\0'; + if (!strcmp(head, "option")) { + const char *key, *val; + + if (!hint || !(report || new_report)) { + if (!once++) + strbuf_addstr(errmsg, "proc-receive reported 'option' without a matching 'ok/ng' directive\n"); + code = -1; + continue; + } + if (new_report) { + if (!hint->report) { + CALLOC_ARRAY(hint->report, 1); + report = hint->report; + } else { + report = hint->report; + while (report->next) + report = report->next; + report->next = xcalloc(1, sizeof(struct ref_push_report)); + report = report->next; + } + new_report = 0; + } + key = p; + p = strchr(key, ' '); + if (p) + *p++ = '\0'; + val = p; + if (!strcmp(key, "refname")) + report->ref_name = xstrdup_or_null(val); + else if (!strcmp(key, "old-oid") && val && + !parse_oid_hex(val, &old_oid, &val)) + report->old_oid = oiddup(&old_oid); + else if (!strcmp(key, "new-oid") && val && + !parse_oid_hex(val, &new_oid, &val)) + report->new_oid = oiddup(&new_oid); + else if (!strcmp(key, "forced-update")) + report->forced_update = 1; + else if (!strcmp(key, "fall-through")) + /* Fall through, let 'receive-pack' to execute it. */ + hint->run_proc_receive = 0; + continue; + } + + report = NULL; + new_report = 0; + refname = p; + p = strchr(refname, ' '); + if (p) + *p++ = '\0'; + if (strcmp(head, "ok") && strcmp(head, "ng")) { + strbuf_addf(errmsg, "proc-receive reported bad status '%s' on ref '%s'\n", + head, refname); + code = -1; + continue; + } + + /* first try searching at our hint, falling back to all refs */ + if (hint) + hint = find_command_by_refname(hint, refname); + if (!hint) + hint = find_command_by_refname(commands, refname); + if (!hint) { + strbuf_addf(errmsg, "proc-receive reported status on unknown ref: %s\n", + refname); + code = -1; + continue; + } + if (!hint->run_proc_receive) { + strbuf_addf(errmsg, "proc-receive reported status on unexpected ref: %s\n", + refname); + code = -1; + continue; + } + hint->run_proc_receive |= RUN_PROC_RECEIVE_RETURNED; + if (!strcmp(head, "ng")) { + if (p) + hint->error_string = xstrdup(p); + else + hint->error_string = "failed"; + code = -1; + continue; + } + new_report = 1; + } + + for (cmd = commands; cmd; cmd = cmd->next) + if (cmd->run_proc_receive && !cmd->error_string && + !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED)) { + cmd->error_string = "proc-receive failed to report status"; + code = -1; + } + return code; +} + +static int run_proc_receive_hook(struct command *commands, + const struct string_list *push_options) +{ + struct child_process proc = CHILD_PROCESS_INIT; + struct async muxer; + struct command *cmd; + struct packet_reader reader; + struct strbuf cap = STRBUF_INIT; + struct strbuf errmsg = STRBUF_INIT; + int hook_use_push_options = 0; + int version = 0; + int code; + const char *hook_path = find_hook("proc-receive"); + + if (!hook_path) { + rp_error("cannot find hook 'proc-receive'"); + return -1; + } + + strvec_push(&proc.args, hook_path); + proc.in = -1; + proc.out = -1; + proc.trace2_hook_name = "proc-receive"; + + if (use_sideband) { + memset(&muxer, 0, sizeof(muxer)); + muxer.proc = copy_to_sideband; + muxer.in = -1; + code = start_async(&muxer); + if (code) + return code; + proc.err = muxer.in; + } else { + proc.err = 0; + } + + code = start_command(&proc); + if (code) { + if (use_sideband) + finish_async(&muxer); + return code; + } + + sigchain_push(SIGPIPE, SIG_IGN); + + /* Version negotiaton */ + packet_reader_init(&reader, proc.out, NULL, 0, + PACKET_READ_CHOMP_NEWLINE | + PACKET_READ_GENTLE_ON_EOF); + if (use_atomic) + strbuf_addstr(&cap, " atomic"); + if (use_push_options) + strbuf_addstr(&cap, " push-options"); + if (cap.len) { + code = packet_write_fmt_gently(proc.in, "version=1%c%s\n", '\0', cap.buf + 1); + strbuf_release(&cap); + } else { + code = packet_write_fmt_gently(proc.in, "version=1\n"); + } + if (!code) + code = packet_flush_gently(proc.in); + + if (!code) + for (;;) { + int linelen; + enum packet_read_status status; + + status = packet_reader_read(&reader); + if (status != PACKET_READ_NORMAL) { + /* Check whether proc-receive exited abnormally */ + if (status == PACKET_READ_EOF) + code = -1; + break; + } + + if (reader.pktlen > 8 && starts_with(reader.line, "version=")) { + version = atoi(reader.line + 8); + linelen = strlen(reader.line); + if (linelen < reader.pktlen) { + const char *feature_list = reader.line + linelen + 1; + if (parse_feature_request(feature_list, "push-options")) + hook_use_push_options = 1; + } + } + } + + if (code) { + strbuf_addstr(&errmsg, "fail to negotiate version with proc-receive hook"); + goto cleanup; + } + + switch (version) { + case 0: + /* fallthrough */ + case 1: + break; + default: + strbuf_addf(&errmsg, "proc-receive version '%d' is not supported", + version); + code = -1; + goto cleanup; + } + + /* Send commands */ + for (cmd = commands; cmd; cmd = cmd->next) { + if (!cmd->run_proc_receive || cmd->skip_update || cmd->error_string) + continue; + code = packet_write_fmt_gently(proc.in, "%s %s %s", + oid_to_hex(&cmd->old_oid), + oid_to_hex(&cmd->new_oid), + cmd->ref_name); + if (code) + break; + } + if (!code) + code = packet_flush_gently(proc.in); + if (code) { + strbuf_addstr(&errmsg, "fail to write commands to proc-receive hook"); + goto cleanup; + } + + /* Send push options */ + if (hook_use_push_options) { + struct string_list_item *item; + + for_each_string_list_item(item, push_options) { + code = packet_write_fmt_gently(proc.in, "%s", item->string); + if (code) + break; + } + if (!code) + code = packet_flush_gently(proc.in); + if (code) { + strbuf_addstr(&errmsg, + "fail to write push-options to proc-receive hook"); + goto cleanup; + } + } + + /* Read result from proc-receive */ + code = read_proc_receive_report(&reader, commands, &errmsg); + +cleanup: + close(proc.in); + close(proc.out); + if (use_sideband) + finish_async(&muxer); + if (finish_command(&proc)) + code = -1; + if (errmsg.len >0) { + char *p = errmsg.buf; + + p += errmsg.len - 1; + if (*p == '\n') + *p = '\0'; + rp_error("%s", errmsg.buf); + strbuf_release(&errmsg); + } + sigchain_pop(SIGPIPE); + + return code; +} + +static char *refuse_unconfigured_deny_msg = + N_("By default, updating the current branch in a non-bare repository\n" + "is denied, because it will make the index and work tree inconsistent\n" + "with what you pushed, and will require 'git reset --hard' to match\n" + "the work tree to HEAD.\n" + "\n" + "You can set the 'receive.denyCurrentBranch' configuration variable\n" + "to 'ignore' or 'warn' in the remote repository to allow pushing into\n" + "its current branch; however, this is not recommended unless you\n" + "arranged to update its work tree to match what you pushed in some\n" + "other way.\n" + "\n" + "To squelch this message and still keep the default behaviour, set\n" + "'receive.denyCurrentBranch' configuration variable to 'refuse'."); + +static void refuse_unconfigured_deny(void) +{ + rp_error("%s", _(refuse_unconfigured_deny_msg)); +} + +static char *refuse_unconfigured_deny_delete_current_msg = + N_("By default, deleting the current branch is denied, because the next\n" + "'git clone' won't result in any file checked out, causing confusion.\n" + "\n" + "You can set 'receive.denyDeleteCurrent' configuration variable to\n" + "'warn' or 'ignore' in the remote repository to allow deleting the\n" + "current branch, with or without a warning message.\n" + "\n" + "To squelch this message, you can set it to 'refuse'."); + +static void refuse_unconfigured_deny_delete_current(void) +{ + rp_error("%s", _(refuse_unconfigured_deny_delete_current_msg)); +} + +static const struct object_id *command_singleton_iterator(void *cb_data); +static int update_shallow_ref(struct command *cmd, struct shallow_info *si) +{ + struct shallow_lock shallow_lock = SHALLOW_LOCK_INIT; + struct oid_array extra = OID_ARRAY_INIT; + struct check_connected_options opt = CHECK_CONNECTED_INIT; + uint32_t mask = 1 << (cmd->index % 32); + int i; + + trace_printf_key(&trace_shallow, + "shallow: update_shallow_ref %s\n", cmd->ref_name); + for (i = 0; i < si->shallow->nr; i++) + if (si->used_shallow[i] && + (si->used_shallow[i][cmd->index / 32] & mask) && + !delayed_reachability_test(si, i)) + oid_array_append(&extra, &si->shallow->oid[i]); + + opt.env = tmp_objdir_env(tmp_objdir); + setup_alternate_shallow(&shallow_lock, &opt.shallow_file, &extra); + if (check_connected(command_singleton_iterator, cmd, &opt)) { + rollback_shallow_file(the_repository, &shallow_lock); + oid_array_clear(&extra); + return -1; + } + + commit_shallow_file(the_repository, &shallow_lock); + + /* + * Make sure setup_alternate_shallow() for the next ref does + * not lose these new roots.. + */ + for (i = 0; i < extra.nr; i++) + register_shallow(the_repository, &extra.oid[i]); + + si->shallow_ref[cmd->index] = 0; + oid_array_clear(&extra); + return 0; +} + +/* + * NEEDSWORK: we should consolidate various implementions of "are we + * on an unborn branch?" test into one, and make the unified one more + * robust. !get_sha1() based check used here and elsewhere would not + * allow us to tell an unborn branch from corrupt ref, for example. + * For the purpose of fixing "deploy-to-update does not work when + * pushing into an empty repository" issue, this should suffice for + * now. + */ +static int head_has_history(void) +{ + struct object_id oid; + + return !repo_get_oid(the_repository, "HEAD", &oid); +} + +static const char *push_to_deploy(unsigned char *sha1, + struct strvec *env, + const char *work_tree) +{ + struct child_process child = CHILD_PROCESS_INIT; + + strvec_pushl(&child.args, "update-index", "-q", "--ignore-submodules", + "--refresh", NULL); + strvec_pushv(&child.env, env->v); + child.dir = work_tree; + child.no_stdin = 1; + child.stdout_to_stderr = 1; + child.git_cmd = 1; + if (run_command(&child)) + return "Up-to-date check failed"; + + /* run_command() does not clean up completely; reinitialize */ + child_process_init(&child); + strvec_pushl(&child.args, "diff-files", "--quiet", + "--ignore-submodules", "--", NULL); + strvec_pushv(&child.env, env->v); + child.dir = work_tree; + child.no_stdin = 1; + child.stdout_to_stderr = 1; + child.git_cmd = 1; + if (run_command(&child)) + return "Working directory has unstaged changes"; + + child_process_init(&child); + strvec_pushl(&child.args, "diff-index", "--quiet", "--cached", + "--ignore-submodules", + /* diff-index with either HEAD or an empty tree */ + head_has_history() ? "HEAD" : empty_tree_oid_hex(), + "--", NULL); + strvec_pushv(&child.env, env->v); + child.no_stdin = 1; + child.no_stdout = 1; + child.stdout_to_stderr = 0; + child.git_cmd = 1; + if (run_command(&child)) + return "Working directory has staged changes"; + + child_process_init(&child); + strvec_pushl(&child.args, "read-tree", "-u", "-m", hash_to_hex(sha1), + NULL); + strvec_pushv(&child.env, env->v); + child.dir = work_tree; + child.no_stdin = 1; + child.no_stdout = 1; + child.stdout_to_stderr = 0; + child.git_cmd = 1; + if (run_command(&child)) + return "Could not update working tree to new HEAD"; + + return NULL; +} + +static const char *push_to_checkout_hook = "push-to-checkout"; + +static const char *push_to_checkout(unsigned char *hash, + int *invoked_hook, + struct strvec *env, + const char *work_tree) +{ + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + opt.invoked_hook = invoked_hook; + + strvec_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree)); + strvec_pushv(&opt.env, env->v); + strvec_push(&opt.args, hash_to_hex(hash)); + if (run_hooks_opt(push_to_checkout_hook, &opt)) + return "push-to-checkout hook declined"; + else + return NULL; +} + +static const char *update_worktree(unsigned char *sha1, const struct worktree *worktree) +{ + const char *retval, *git_dir; + struct strvec env = STRVEC_INIT; + int invoked_hook; + + if (!worktree || !worktree->path) + BUG("worktree->path must be non-NULL"); + + if (worktree->is_bare) + return "denyCurrentBranch = updateInstead needs a worktree"; + git_dir = get_worktree_git_dir(worktree); + + strvec_pushf(&env, "GIT_DIR=%s", absolute_path(git_dir)); + + retval = push_to_checkout(sha1, &invoked_hook, &env, worktree->path); + if (!invoked_hook) + retval = push_to_deploy(sha1, &env, worktree->path); + + strvec_clear(&env); + return retval; +} + +static const char *update(struct command *cmd, struct shallow_info *si) +{ + const char *name = cmd->ref_name; + struct strbuf namespaced_name_buf = STRBUF_INIT; + static char *namespaced_name; + const char *ret; + struct object_id *old_oid = &cmd->old_oid; + struct object_id *new_oid = &cmd->new_oid; + int do_update_worktree = 0; + struct worktree **worktrees = get_worktrees(); + const struct worktree *worktree = + find_shared_symref(worktrees, "HEAD", name); + + /* only refs/... are allowed */ + if (!starts_with(name, "refs/") || + check_refname_format(name + 5, is_null_oid(new_oid) ? + REFNAME_ALLOW_ONELEVEL : 0)) { + rp_error("refusing to update funny ref '%s' remotely", name); + ret = "funny refname"; + goto out; + } + + strbuf_addf(&namespaced_name_buf, "%s%s", get_git_namespace(), name); + free(namespaced_name); + namespaced_name = strbuf_detach(&namespaced_name_buf, NULL); + + if (worktree && !worktree->is_bare) { + switch (deny_current_branch) { + case DENY_IGNORE: + break; + case DENY_WARN: + rp_warning("updating the current branch"); + break; + case DENY_REFUSE: + case DENY_UNCONFIGURED: + rp_error("refusing to update checked out branch: %s", name); + if (deny_current_branch == DENY_UNCONFIGURED) + refuse_unconfigured_deny(); + ret = "branch is currently checked out"; + goto out; + case DENY_UPDATE_INSTEAD: + /* pass -- let other checks intervene first */ + do_update_worktree = 1; + break; + } + } + + if (!is_null_oid(new_oid) && !repo_has_object_file(the_repository, new_oid)) { + error("unpack should have generated %s, " + "but I can't find it!", oid_to_hex(new_oid)); + ret = "bad pack"; + goto out; + } + + if (!is_null_oid(old_oid) && is_null_oid(new_oid)) { + if (deny_deletes && starts_with(name, "refs/heads/")) { + rp_error("denying ref deletion for %s", name); + ret = "deletion prohibited"; + goto out; + } + + if (worktree || (head_name && !strcmp(namespaced_name, head_name))) { + switch (deny_delete_current) { + case DENY_IGNORE: + break; + case DENY_WARN: + rp_warning("deleting the current branch"); + break; + case DENY_REFUSE: + case DENY_UNCONFIGURED: + case DENY_UPDATE_INSTEAD: + if (deny_delete_current == DENY_UNCONFIGURED) + refuse_unconfigured_deny_delete_current(); + rp_error("refusing to delete the current branch: %s", name); + ret = "deletion of the current branch prohibited"; + goto out; + default: + ret = "Invalid denyDeleteCurrent setting"; + goto out; + } + } + } + + if (deny_non_fast_forwards && !is_null_oid(new_oid) && + !is_null_oid(old_oid) && + starts_with(name, "refs/heads/")) { + struct object *old_object, *new_object; + struct commit *old_commit, *new_commit; + + old_object = parse_object(the_repository, old_oid); + new_object = parse_object(the_repository, new_oid); + + if (!old_object || !new_object || + old_object->type != OBJ_COMMIT || + new_object->type != OBJ_COMMIT) { + error("bad sha1 objects for %s", name); + ret = "bad ref"; + goto out; + } + old_commit = (struct commit *)old_object; + new_commit = (struct commit *)new_object; + if (!repo_in_merge_bases(the_repository, old_commit, new_commit)) { + rp_error("denying non-fast-forward %s" + " (you should pull first)", name); + ret = "non-fast-forward"; + goto out; + } + } + if (run_update_hook(cmd)) { + rp_error("hook declined to update %s", name); + ret = "hook declined"; + goto out; + } + + if (do_update_worktree) { + ret = update_worktree(new_oid->hash, worktree); + if (ret) + goto out; + } + + if (is_null_oid(new_oid)) { + struct strbuf err = STRBUF_INIT; + if (!parse_object(the_repository, old_oid)) { + old_oid = NULL; + if (ref_exists(name)) { + rp_warning("allowing deletion of corrupt ref"); + } else { + rp_warning("deleting a non-existent ref"); + cmd->did_not_exist = 1; + } + } + if (ref_transaction_delete(transaction, + namespaced_name, + old_oid, + 0, "push", &err)) { + rp_error("%s", err.buf); + ret = "failed to delete"; + } else { + ret = NULL; /* good */ + } + strbuf_release(&err); + } + else { + struct strbuf err = STRBUF_INIT; + if (shallow_update && si->shallow_ref[cmd->index] && + update_shallow_ref(cmd, si)) { + ret = "shallow error"; + goto out; + } + + if (ref_transaction_update(transaction, + namespaced_name, + new_oid, old_oid, + 0, "push", + &err)) { + rp_error("%s", err.buf); + ret = "failed to update ref"; + } else { + ret = NULL; /* good */ + } + strbuf_release(&err); + } + +out: + free_worktrees(worktrees); + return ret; +} + +static void run_update_post_hook(struct command *commands) +{ + struct command *cmd; + struct child_process proc = CHILD_PROCESS_INIT; + const char *hook; + + hook = find_hook("post-update"); + if (!hook) + return; + + for (cmd = commands; cmd; cmd = cmd->next) { + if (cmd->error_string || cmd->did_not_exist) + continue; + if (!proc.args.nr) + strvec_push(&proc.args, hook); + strvec_push(&proc.args, cmd->ref_name); + } + if (!proc.args.nr) + return; + + proc.no_stdin = 1; + proc.stdout_to_stderr = 1; + proc.err = use_sideband ? -1 : 0; + proc.trace2_hook_name = "post-update"; + + if (!start_command(&proc)) { + if (use_sideband) + copy_to_sideband(proc.err, -1, NULL); + finish_command(&proc); + } +} + +static void check_aliased_update_internal(struct command *cmd, + struct string_list *list, + const char *dst_name, int flag) +{ + struct string_list_item *item; + struct command *dst_cmd; + + if (!(flag & REF_ISSYMREF)) + return; + + if (!dst_name) { + rp_error("refusing update to broken symref '%s'", cmd->ref_name); + cmd->skip_update = 1; + cmd->error_string = "broken symref"; + return; + } + dst_name = strip_namespace(dst_name); + + if (!(item = string_list_lookup(list, dst_name))) + return; + + cmd->skip_update = 1; + + dst_cmd = (struct command *) item->util; + + if (oideq(&cmd->old_oid, &dst_cmd->old_oid) && + oideq(&cmd->new_oid, &dst_cmd->new_oid)) + return; + + dst_cmd->skip_update = 1; + + rp_error("refusing inconsistent update between symref '%s' (%s..%s) and" + " its target '%s' (%s..%s)", + cmd->ref_name, + repo_find_unique_abbrev(the_repository, &cmd->old_oid, DEFAULT_ABBREV), + repo_find_unique_abbrev(the_repository, &cmd->new_oid, DEFAULT_ABBREV), + dst_cmd->ref_name, + repo_find_unique_abbrev(the_repository, &dst_cmd->old_oid, DEFAULT_ABBREV), + repo_find_unique_abbrev(the_repository, &dst_cmd->new_oid, DEFAULT_ABBREV)); + + cmd->error_string = dst_cmd->error_string = + "inconsistent aliased update"; +} + +static void check_aliased_update(struct command *cmd, struct string_list *list) +{ + struct strbuf buf = STRBUF_INIT; + const char *dst_name; + int flag; + + strbuf_addf(&buf, "%s%s", get_git_namespace(), cmd->ref_name); + dst_name = resolve_ref_unsafe(buf.buf, 0, NULL, &flag); + check_aliased_update_internal(cmd, list, dst_name, flag); + strbuf_release(&buf); +} + +static void check_aliased_updates(struct command *commands) +{ + struct command *cmd; + struct string_list ref_list = STRING_LIST_INIT_NODUP; + + for (cmd = commands; cmd; cmd = cmd->next) { + struct string_list_item *item = + string_list_append(&ref_list, cmd->ref_name); + item->util = (void *)cmd; + } + string_list_sort(&ref_list); + + for (cmd = commands; cmd; cmd = cmd->next) { + if (!cmd->error_string) + check_aliased_update(cmd, &ref_list); + } + + string_list_clear(&ref_list, 0); +} + +static const struct object_id *command_singleton_iterator(void *cb_data) +{ + struct command **cmd_list = cb_data; + struct command *cmd = *cmd_list; + + if (!cmd || is_null_oid(&cmd->new_oid)) + return NULL; + *cmd_list = NULL; /* this returns only one */ + return &cmd->new_oid; +} + +static void set_connectivity_errors(struct command *commands, + struct shallow_info *si) +{ + struct command *cmd; + + for (cmd = commands; cmd; cmd = cmd->next) { + struct command *singleton = cmd; + struct check_connected_options opt = CHECK_CONNECTED_INIT; + + if (shallow_update && si->shallow_ref[cmd->index]) + /* to be checked in update_shallow_ref() */ + continue; + + opt.env = tmp_objdir_env(tmp_objdir); + if (!check_connected(command_singleton_iterator, &singleton, + &opt)) + continue; + + cmd->error_string = "missing necessary objects"; + } +} + +struct iterate_data { + struct command *cmds; + struct shallow_info *si; +}; + +static const struct object_id *iterate_receive_command_list(void *cb_data) +{ + struct iterate_data *data = cb_data; + struct command **cmd_list = &data->cmds; + struct command *cmd = *cmd_list; + + for (; cmd; cmd = cmd->next) { + if (shallow_update && data->si->shallow_ref[cmd->index]) + /* to be checked in update_shallow_ref() */ + continue; + if (!is_null_oid(&cmd->new_oid) && !cmd->skip_update) { + *cmd_list = cmd->next; + return &cmd->new_oid; + } + } + return NULL; +} + +static void reject_updates_to_hidden(struct command *commands) +{ + struct strbuf refname_full = STRBUF_INIT; + size_t prefix_len; + struct command *cmd; + + strbuf_addstr(&refname_full, get_git_namespace()); + prefix_len = refname_full.len; + + for (cmd = commands; cmd; cmd = cmd->next) { + if (cmd->error_string) + continue; + + strbuf_setlen(&refname_full, prefix_len); + strbuf_addstr(&refname_full, cmd->ref_name); + + if (!ref_is_hidden(cmd->ref_name, refname_full.buf, &hidden_refs)) + continue; + if (is_null_oid(&cmd->new_oid)) + cmd->error_string = "deny deleting a hidden ref"; + else + cmd->error_string = "deny updating a hidden ref"; + } + + strbuf_release(&refname_full); +} + +static int should_process_cmd(struct command *cmd) +{ + return !cmd->error_string && !cmd->skip_update; +} + +static void BUG_if_skipped_connectivity_check(struct command *commands, + struct shallow_info *si) +{ + struct command *cmd; + + for (cmd = commands; cmd; cmd = cmd->next) { + if (should_process_cmd(cmd) && si->shallow_ref[cmd->index]) + bug("connectivity check has not been run on ref %s", + cmd->ref_name); + } + BUG_if_bug("connectivity check skipped???"); +} + +static void execute_commands_non_atomic(struct command *commands, + struct shallow_info *si) +{ + struct command *cmd; + struct strbuf err = STRBUF_INIT; + + for (cmd = commands; cmd; cmd = cmd->next) { + if (!should_process_cmd(cmd) || cmd->run_proc_receive) + continue; + + transaction = ref_transaction_begin(&err); + if (!transaction) { + rp_error("%s", err.buf); + strbuf_reset(&err); + cmd->error_string = "transaction failed to start"; + continue; + } + + cmd->error_string = update(cmd, si); + + if (!cmd->error_string + && ref_transaction_commit(transaction, &err)) { + rp_error("%s", err.buf); + strbuf_reset(&err); + cmd->error_string = "failed to update ref"; + } + ref_transaction_free(transaction); + } + strbuf_release(&err); +} + +static void execute_commands_atomic(struct command *commands, + struct shallow_info *si) +{ + struct command *cmd; + struct strbuf err = STRBUF_INIT; + const char *reported_error = "atomic push failure"; + + transaction = ref_transaction_begin(&err); + if (!transaction) { + rp_error("%s", err.buf); + strbuf_reset(&err); + reported_error = "transaction failed to start"; + goto failure; + } + + for (cmd = commands; cmd; cmd = cmd->next) { + if (!should_process_cmd(cmd) || cmd->run_proc_receive) + continue; + + cmd->error_string = update(cmd, si); + + if (cmd->error_string) + goto failure; + } + + if (ref_transaction_commit(transaction, &err)) { + rp_error("%s", err.buf); + reported_error = "atomic transaction failed"; + goto failure; + } + goto cleanup; + +failure: + for (cmd = commands; cmd; cmd = cmd->next) + if (!cmd->error_string) + cmd->error_string = reported_error; + +cleanup: + ref_transaction_free(transaction); + strbuf_release(&err); +} + +static void execute_commands(struct command *commands, + const char *unpacker_error, + struct shallow_info *si, + const struct string_list *push_options) +{ + struct check_connected_options opt = CHECK_CONNECTED_INIT; + struct command *cmd; + struct iterate_data data; + struct async muxer; + int err_fd = 0; + int run_proc_receive = 0; + + if (unpacker_error) { + for (cmd = commands; cmd; cmd = cmd->next) + cmd->error_string = "unpacker error"; + return; + } + + if (use_sideband) { + memset(&muxer, 0, sizeof(muxer)); + muxer.proc = copy_to_sideband; + muxer.in = -1; + if (!start_async(&muxer)) + err_fd = muxer.in; + /* ...else, continue without relaying sideband */ + } + + data.cmds = commands; + data.si = si; + opt.err_fd = err_fd; + opt.progress = err_fd && !quiet; + opt.env = tmp_objdir_env(tmp_objdir); + opt.exclude_hidden_refs_section = "receive"; + + if (check_connected(iterate_receive_command_list, &data, &opt)) + set_connectivity_errors(commands, si); + + if (use_sideband) + finish_async(&muxer); + + reject_updates_to_hidden(commands); + + /* + * Try to find commands that have special prefix in their reference names, + * and mark them to run an external "proc-receive" hook later. + */ + if (proc_receive_ref) { + for (cmd = commands; cmd; cmd = cmd->next) { + if (!should_process_cmd(cmd)) + continue; + + if (proc_receive_ref_matches(cmd)) { + cmd->run_proc_receive = RUN_PROC_RECEIVE_SCHEDULED; + run_proc_receive = 1; + } + } + } + + if (run_receive_hook(commands, "pre-receive", 0, push_options)) { + for (cmd = commands; cmd; cmd = cmd->next) { + if (!cmd->error_string) + cmd->error_string = "pre-receive hook declined"; + } + return; + } + + /* + * If there is no command ready to run, should return directly to destroy + * temporary data in the quarantine area. + */ + for (cmd = commands; cmd && cmd->error_string; cmd = cmd->next) + ; /* nothing */ + if (!cmd) + return; + + /* + * Now we'll start writing out refs, which means the objects need + * to be in their final positions so that other processes can see them. + */ + if (tmp_objdir_migrate(tmp_objdir) < 0) { + for (cmd = commands; cmd; cmd = cmd->next) { + if (!cmd->error_string) + cmd->error_string = "unable to migrate objects to permanent storage"; + } + return; + } + tmp_objdir = NULL; + + check_aliased_updates(commands); + + free(head_name_to_free); + head_name = head_name_to_free = resolve_refdup("HEAD", 0, NULL, NULL); + + if (run_proc_receive && + run_proc_receive_hook(commands, push_options)) + for (cmd = commands; cmd; cmd = cmd->next) + if (!cmd->error_string && + !(cmd->run_proc_receive & RUN_PROC_RECEIVE_RETURNED) && + (cmd->run_proc_receive || use_atomic)) + cmd->error_string = "fail to run proc-receive hook"; + + if (use_atomic) + execute_commands_atomic(commands, si); + else + execute_commands_non_atomic(commands, si); + + if (shallow_update) + BUG_if_skipped_connectivity_check(commands, si); +} + +static struct command **queue_command(struct command **tail, + const char *line, + int linelen) +{ + struct object_id old_oid, new_oid; + struct command *cmd; + const char *refname; + int reflen; + const char *p; + + if (parse_oid_hex(line, &old_oid, &p) || + *p++ != ' ' || + parse_oid_hex(p, &new_oid, &p) || + *p++ != ' ') + die("protocol error: expected old/new/ref, got '%s'", line); + + refname = p; + reflen = linelen - (p - line); + FLEX_ALLOC_MEM(cmd, ref_name, refname, reflen); + oidcpy(&cmd->old_oid, &old_oid); + oidcpy(&cmd->new_oid, &new_oid); + *tail = cmd; + return &cmd->next; +} + +static void free_commands(struct command *commands) +{ + while (commands) { + struct command *next = commands->next; + + free(commands); + commands = next; + } +} + +static void queue_commands_from_cert(struct command **tail, + struct strbuf *push_cert) +{ + const char *boc, *eoc; + + if (*tail) + die("protocol error: got both push certificate and unsigned commands"); + + boc = strstr(push_cert->buf, "\n\n"); + if (!boc) + die("malformed push certificate %.*s", 100, push_cert->buf); + else + boc += 2; + eoc = push_cert->buf + parse_signed_buffer(push_cert->buf, push_cert->len); + + while (boc < eoc) { + const char *eol = memchr(boc, '\n', eoc - boc); + tail = queue_command(tail, boc, eol ? eol - boc : eoc - boc); + boc = eol ? eol + 1 : eoc; + } +} + +static struct command *read_head_info(struct packet_reader *reader, + struct oid_array *shallow) +{ + struct command *commands = NULL; + struct command **p = &commands; + for (;;) { + int linelen; + + if (packet_reader_read(reader) != PACKET_READ_NORMAL) + break; + + if (reader->pktlen > 8 && starts_with(reader->line, "shallow ")) { + struct object_id oid; + if (get_oid_hex(reader->line + 8, &oid)) + die("protocol error: expected shallow sha, got '%s'", + reader->line + 8); + oid_array_append(shallow, &oid); + continue; + } + + linelen = strlen(reader->line); + if (linelen < reader->pktlen) { + const char *feature_list = reader->line + linelen + 1; + const char *hash = NULL; + const char *client_sid; + size_t len = 0; + if (parse_feature_request(feature_list, "report-status")) + report_status = 1; + if (parse_feature_request(feature_list, "report-status-v2")) + report_status_v2 = 1; + if (parse_feature_request(feature_list, "side-band-64k")) + use_sideband = LARGE_PACKET_MAX; + if (parse_feature_request(feature_list, "quiet")) + quiet = 1; + if (advertise_atomic_push + && parse_feature_request(feature_list, "atomic")) + use_atomic = 1; + if (advertise_push_options + && parse_feature_request(feature_list, "push-options")) + use_push_options = 1; + hash = parse_feature_value(feature_list, "object-format", &len, NULL); + if (!hash) { + hash = hash_algos[GIT_HASH_SHA1].name; + len = strlen(hash); + } + if (xstrncmpz(the_hash_algo->name, hash, len)) + die("error: unsupported object format '%s'", hash); + client_sid = parse_feature_value(feature_list, "session-id", &len, NULL); + if (client_sid) { + char *sid = xstrndup(client_sid, len); + trace2_data_string("transfer", NULL, "client-sid", client_sid); + free(sid); + } + } + + if (!strcmp(reader->line, "push-cert")) { + int true_flush = 0; + int saved_options = reader->options; + reader->options &= ~PACKET_READ_CHOMP_NEWLINE; + + for (;;) { + packet_reader_read(reader); + if (reader->status == PACKET_READ_FLUSH) { + true_flush = 1; + break; + } + if (reader->status != PACKET_READ_NORMAL) { + die("protocol error: got an unexpected packet"); + } + if (!strcmp(reader->line, "push-cert-end\n")) + break; /* end of cert */ + strbuf_addstr(&push_cert, reader->line); + } + reader->options = saved_options; + + if (true_flush) + break; + continue; + } + + p = queue_command(p, reader->line, linelen); + } + + if (push_cert.len) + queue_commands_from_cert(p, &push_cert); + + return commands; +} + +static void read_push_options(struct packet_reader *reader, + struct string_list *options) +{ + while (1) { + if (packet_reader_read(reader) != PACKET_READ_NORMAL) + break; + + string_list_append(options, reader->line); + } +} + +static const char *parse_pack_header(struct pack_header *hdr) +{ + switch (read_pack_header(0, hdr)) { + case PH_ERROR_EOF: + return "eof before pack header was fully read"; + + case PH_ERROR_PACK_SIGNATURE: + return "protocol error (pack signature mismatch detected)"; + + case PH_ERROR_PROTOCOL: + return "protocol error (pack version unsupported)"; + + default: + return "unknown error in parse_pack_header"; + + case 0: + return NULL; + } +} + +static struct tempfile *pack_lockfile; + +static void push_header_arg(struct strvec *args, struct pack_header *hdr) +{ + strvec_pushf(args, "--pack_header=%"PRIu32",%"PRIu32, + ntohl(hdr->hdr_version), ntohl(hdr->hdr_entries)); +} + +static const char *unpack(int err_fd, struct shallow_info *si) +{ + struct pack_header hdr; + const char *hdr_err; + int status; + struct child_process child = CHILD_PROCESS_INIT; + int fsck_objects = (receive_fsck_objects >= 0 + ? receive_fsck_objects + : transfer_fsck_objects >= 0 + ? transfer_fsck_objects + : 0); + + hdr_err = parse_pack_header(&hdr); + if (hdr_err) { + if (err_fd > 0) + close(err_fd); + return hdr_err; + } + + if (si->nr_ours || si->nr_theirs) { + alt_shallow_file = setup_temporary_shallow(si->shallow); + strvec_push(&child.args, "--shallow-file"); + strvec_push(&child.args, alt_shallow_file); + } + + tmp_objdir = tmp_objdir_create("incoming"); + if (!tmp_objdir) { + if (err_fd > 0) + close(err_fd); + return "unable to create temporary object directory"; + } + strvec_pushv(&child.env, tmp_objdir_env(tmp_objdir)); + + /* + * Normally we just pass the tmp_objdir environment to the child + * processes that do the heavy lifting, but we may need to see these + * objects ourselves to set up shallow information. + */ + tmp_objdir_add_as_alternate(tmp_objdir); + + if (ntohl(hdr.hdr_entries) < unpack_limit) { + strvec_push(&child.args, "unpack-objects"); + push_header_arg(&child.args, &hdr); + if (quiet) + strvec_push(&child.args, "-q"); + if (fsck_objects) + strvec_pushf(&child.args, "--strict%s", + fsck_msg_types.buf); + if (max_input_size) + strvec_pushf(&child.args, "--max-input-size=%"PRIuMAX, + (uintmax_t)max_input_size); + child.no_stdout = 1; + child.err = err_fd; + child.git_cmd = 1; + status = run_command(&child); + if (status) + return "unpack-objects abnormal exit"; + } else { + char hostname[HOST_NAME_MAX + 1]; + char *lockfile; + + strvec_pushl(&child.args, "index-pack", "--stdin", NULL); + push_header_arg(&child.args, &hdr); + + if (xgethostname(hostname, sizeof(hostname))) + xsnprintf(hostname, sizeof(hostname), "localhost"); + strvec_pushf(&child.args, + "--keep=receive-pack %"PRIuMAX" on %s", + (uintmax_t)getpid(), + hostname); + + if (!quiet && err_fd) + strvec_push(&child.args, "--show-resolving-progress"); + if (use_sideband) + strvec_push(&child.args, "--report-end-of-input"); + if (fsck_objects) + strvec_pushf(&child.args, "--strict%s", + fsck_msg_types.buf); + if (!reject_thin) + strvec_push(&child.args, "--fix-thin"); + if (max_input_size) + strvec_pushf(&child.args, "--max-input-size=%"PRIuMAX, + (uintmax_t)max_input_size); + child.out = -1; + child.err = err_fd; + child.git_cmd = 1; + status = start_command(&child); + if (status) + return "index-pack fork failed"; + + lockfile = index_pack_lockfile(child.out, NULL); + if (lockfile) { + pack_lockfile = register_tempfile(lockfile); + free(lockfile); + } + close(child.out); + + status = finish_command(&child); + if (status) + return "index-pack abnormal exit"; + reprepare_packed_git(the_repository); + } + return NULL; +} + +static const char *unpack_with_sideband(struct shallow_info *si) +{ + struct async muxer; + const char *ret; + + if (!use_sideband) + return unpack(0, si); + + use_keepalive = KEEPALIVE_AFTER_NUL; + memset(&muxer, 0, sizeof(muxer)); + muxer.proc = copy_to_sideband; + muxer.in = -1; + if (start_async(&muxer)) + return NULL; + + ret = unpack(muxer.in, si); + + finish_async(&muxer); + return ret; +} + +static void prepare_shallow_update(struct shallow_info *si) +{ + int i, j, k, bitmap_size = DIV_ROUND_UP(si->ref->nr, 32); + + ALLOC_ARRAY(si->used_shallow, si->shallow->nr); + assign_shallow_commits_to_refs(si, si->used_shallow, NULL); + + CALLOC_ARRAY(si->need_reachability_test, si->shallow->nr); + CALLOC_ARRAY(si->reachable, si->shallow->nr); + CALLOC_ARRAY(si->shallow_ref, si->ref->nr); + + for (i = 0; i < si->nr_ours; i++) + si->need_reachability_test[si->ours[i]] = 1; + + for (i = 0; i < si->shallow->nr; i++) { + if (!si->used_shallow[i]) + continue; + for (j = 0; j < bitmap_size; j++) { + if (!si->used_shallow[i][j]) + continue; + si->need_reachability_test[i]++; + for (k = 0; k < 32; k++) + if (si->used_shallow[i][j] & (1U << k)) + si->shallow_ref[j * 32 + k]++; + } + + /* + * true for those associated with some refs and belong + * in "ours" list aka "step 7 not done yet" + */ + si->need_reachability_test[i] = + si->need_reachability_test[i] > 1; + } + + /* + * keep hooks happy by forcing a temporary shallow file via + * env variable because we can't add --shallow-file to every + * command. check_connected() will be done with + * true .git/shallow though. + */ + setenv(GIT_SHALLOW_FILE_ENVIRONMENT, alt_shallow_file, 1); +} + +static void update_shallow_info(struct command *commands, + struct shallow_info *si, + struct oid_array *ref) +{ + struct command *cmd; + int *ref_status; + remove_nonexistent_theirs_shallow(si); + if (!si->nr_ours && !si->nr_theirs) { + shallow_update = 0; + return; + } + + for (cmd = commands; cmd; cmd = cmd->next) { + if (is_null_oid(&cmd->new_oid)) + continue; + oid_array_append(ref, &cmd->new_oid); + cmd->index = ref->nr - 1; + } + si->ref = ref; + + if (shallow_update) { + prepare_shallow_update(si); + return; + } + + ALLOC_ARRAY(ref_status, ref->nr); + assign_shallow_commits_to_refs(si, NULL, ref_status); + for (cmd = commands; cmd; cmd = cmd->next) { + if (is_null_oid(&cmd->new_oid)) + continue; + if (ref_status[cmd->index]) { + cmd->error_string = "shallow update not allowed"; + cmd->skip_update = 1; + } + } + free(ref_status); +} + +static void report(struct command *commands, const char *unpack_status) +{ + struct command *cmd; + struct strbuf buf = STRBUF_INIT; + + packet_buf_write(&buf, "unpack %s\n", + unpack_status ? unpack_status : "ok"); + for (cmd = commands; cmd; cmd = cmd->next) { + if (!cmd->error_string) + packet_buf_write(&buf, "ok %s\n", + cmd->ref_name); + else + packet_buf_write(&buf, "ng %s %s\n", + cmd->ref_name, cmd->error_string); + } + packet_buf_flush(&buf); + + if (use_sideband) + send_sideband(1, 1, buf.buf, buf.len, use_sideband); + else + write_or_die(1, buf.buf, buf.len); + strbuf_release(&buf); +} + +static void report_v2(struct command *commands, const char *unpack_status) +{ + struct command *cmd; + struct strbuf buf = STRBUF_INIT; + struct ref_push_report *report; + + packet_buf_write(&buf, "unpack %s\n", + unpack_status ? unpack_status : "ok"); + for (cmd = commands; cmd; cmd = cmd->next) { + int count = 0; + + if (cmd->error_string) { + packet_buf_write(&buf, "ng %s %s\n", + cmd->ref_name, + cmd->error_string); + continue; + } + packet_buf_write(&buf, "ok %s\n", + cmd->ref_name); + for (report = cmd->report; report; report = report->next) { + if (count++ > 0) + packet_buf_write(&buf, "ok %s\n", + cmd->ref_name); + if (report->ref_name) + packet_buf_write(&buf, "option refname %s\n", + report->ref_name); + if (report->old_oid) + packet_buf_write(&buf, "option old-oid %s\n", + oid_to_hex(report->old_oid)); + if (report->new_oid) + packet_buf_write(&buf, "option new-oid %s\n", + oid_to_hex(report->new_oid)); + if (report->forced_update) + packet_buf_write(&buf, "option forced-update\n"); + } + } + packet_buf_flush(&buf); + + if (use_sideband) + send_sideband(1, 1, buf.buf, buf.len, use_sideband); + else + write_or_die(1, buf.buf, buf.len); + strbuf_release(&buf); +} + +static int delete_only(struct command *commands) +{ + struct command *cmd; + for (cmd = commands; cmd; cmd = cmd->next) { + if (!is_null_oid(&cmd->new_oid)) + return 0; + } + return 1; +} + +int cmd_receive_pack(int argc, const char **argv, const char *prefix) +{ + int advertise_refs = 0; + struct command *commands; + struct oid_array shallow = OID_ARRAY_INIT; + struct oid_array ref = OID_ARRAY_INIT; + struct shallow_info si; + struct packet_reader reader; + + struct option options[] = { + OPT__QUIET(&quiet, N_("quiet")), + OPT_HIDDEN_BOOL(0, "stateless-rpc", &stateless_rpc, NULL), + OPT_HIDDEN_BOOL(0, "http-backend-info-refs", &advertise_refs, NULL), + OPT_ALIAS(0, "advertise-refs", "http-backend-info-refs"), + OPT_HIDDEN_BOOL(0, "reject-thin-pack-for-testing", &reject_thin, NULL), + OPT_END() + }; + + packet_trace_identity("receive-pack"); + + argc = parse_options(argc, argv, prefix, options, receive_pack_usage, 0); + + if (argc > 1) + usage_msg_opt(_("too many arguments"), receive_pack_usage, options); + if (argc == 0) + usage_msg_opt(_("you must specify a directory"), receive_pack_usage, options); + + service_dir = argv[0]; + + setup_path(); + + if (!enter_repo(service_dir, 0)) + die("'%s' does not appear to be a git repository", service_dir); + + git_config(receive_pack_config, NULL); + if (cert_nonce_seed) + push_cert_nonce = prepare_push_cert_nonce(service_dir, time(NULL)); + + if (0 <= receive_unpack_limit) + unpack_limit = receive_unpack_limit; + else if (0 <= transfer_unpack_limit) + unpack_limit = transfer_unpack_limit; + + switch (determine_protocol_version_server()) { + case protocol_v2: + /* + * push support for protocol v2 has not been implemented yet, + * so ignore the request to use v2 and fallback to using v0. + */ + break; + case protocol_v1: + /* + * v1 is just the original protocol with a version string, + * so just fall through after writing the version string. + */ + if (advertise_refs || !stateless_rpc) + packet_write_fmt(1, "version 1\n"); + + /* fallthrough */ + case protocol_v0: + break; + case protocol_unknown_version: + BUG("unknown protocol version"); + } + + if (advertise_refs || !stateless_rpc) { + write_head_info(); + } + if (advertise_refs) + return 0; + + packet_reader_init(&reader, 0, NULL, 0, + PACKET_READ_CHOMP_NEWLINE | + PACKET_READ_DIE_ON_ERR_PACKET); + + if ((commands = read_head_info(&reader, &shallow))) { + const char *unpack_status = NULL; + struct string_list push_options = STRING_LIST_INIT_DUP; + + if (use_push_options) + read_push_options(&reader, &push_options); + if (!check_cert_push_options(&push_options)) { + struct command *cmd; + for (cmd = commands; cmd; cmd = cmd->next) + cmd->error_string = "inconsistent push options"; + } + + prepare_shallow_info(&si, &shallow); + if (!si.nr_ours && !si.nr_theirs) + shallow_update = 0; + if (!delete_only(commands)) { + unpack_status = unpack_with_sideband(&si); + update_shallow_info(commands, &si, &ref); + } + use_keepalive = KEEPALIVE_ALWAYS; + execute_commands(commands, unpack_status, &si, + &push_options); + delete_tempfile(&pack_lockfile); + sigchain_push(SIGPIPE, SIG_IGN); + if (report_status_v2) + report_v2(commands, unpack_status); + else if (report_status) + report(commands, unpack_status); + sigchain_pop(SIGPIPE); + run_receive_hook(commands, "post-receive", 1, + &push_options); + run_update_post_hook(commands); + free_commands(commands); + string_list_clear(&push_options, 0); + if (auto_gc) { + struct child_process proc = CHILD_PROCESS_INIT; + + proc.no_stdin = 1; + proc.stdout_to_stderr = 1; + proc.err = use_sideband ? -1 : 0; + proc.git_cmd = proc.close_object_store = 1; + strvec_pushl(&proc.args, "gc", "--auto", "--quiet", + NULL); + + if (!start_command(&proc)) { + if (use_sideband) + copy_to_sideband(proc.err, -1, NULL); + finish_command(&proc); + } + } + if (auto_update_server_info) + update_server_info(0); + clear_shallow_info(&si); + } + if (use_sideband) + packet_flush(1); + oid_array_clear(&shallow); + oid_array_clear(&ref); + strvec_clear(&hidden_refs); + free((void *)push_cert_nonce); + return 0; +} diff --git a/builtin/reflog.c b/builtin/reflog.c new file mode 100644 index 0000000..6e490f8 --- /dev/null +++ b/builtin/reflog.c @@ -0,0 +1,435 @@ +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "repository.h" +#include "revision.h" +#include "reachable.h" +#include "wildmatch.h" +#include "worktree.h" +#include "reflog.h" +#include "parse-options.h" + +#define BUILTIN_REFLOG_SHOW_USAGE \ + N_("git reflog [show] [<log-options>] [<ref>]") + +#define BUILTIN_REFLOG_EXPIRE_USAGE \ + N_("git reflog expire [--expire=<time>] [--expire-unreachable=<time>]\n" \ + " [--rewrite] [--updateref] [--stale-fix]\n" \ + " [--dry-run | -n] [--verbose] [--all [--single-worktree] | <refs>...]") + +#define BUILTIN_REFLOG_DELETE_USAGE \ + N_("git reflog delete [--rewrite] [--updateref]\n" \ + " [--dry-run | -n] [--verbose] <ref>@{<specifier>}...") + +#define BUILTIN_REFLOG_EXISTS_USAGE \ + N_("git reflog exists <ref>") + +static const char *const reflog_show_usage[] = { + BUILTIN_REFLOG_SHOW_USAGE, + NULL, +}; + +static const char *const reflog_expire_usage[] = { + BUILTIN_REFLOG_EXPIRE_USAGE, + NULL +}; + +static const char *const reflog_delete_usage[] = { + BUILTIN_REFLOG_DELETE_USAGE, + NULL +}; + +static const char *const reflog_exists_usage[] = { + BUILTIN_REFLOG_EXISTS_USAGE, + NULL, +}; + +static const char *const reflog_usage[] = { + BUILTIN_REFLOG_SHOW_USAGE, + BUILTIN_REFLOG_EXPIRE_USAGE, + BUILTIN_REFLOG_DELETE_USAGE, + BUILTIN_REFLOG_EXISTS_USAGE, + NULL +}; + +static timestamp_t default_reflog_expire; +static timestamp_t default_reflog_expire_unreachable; + +struct worktree_reflogs { + struct worktree *worktree; + struct string_list reflogs; +}; + +static int collect_reflog(const char *ref, const struct object_id *oid UNUSED, + int flags UNUSED, void *cb_data) +{ + struct worktree_reflogs *cb = cb_data; + struct worktree *worktree = cb->worktree; + struct strbuf newref = STRBUF_INIT; + + /* + * Avoid collecting the same shared ref multiple times because + * they are available via all worktrees. + */ + if (!worktree->is_current && + parse_worktree_ref(ref, NULL, NULL, NULL) == REF_WORKTREE_SHARED) + return 0; + + strbuf_worktree_ref(worktree, &newref, ref); + string_list_append_nodup(&cb->reflogs, strbuf_detach(&newref, NULL)); + + return 0; +} + +static struct reflog_expire_cfg { + struct reflog_expire_cfg *next; + timestamp_t expire_total; + timestamp_t expire_unreachable; + char pattern[FLEX_ARRAY]; +} *reflog_expire_cfg, **reflog_expire_cfg_tail; + +static struct reflog_expire_cfg *find_cfg_ent(const char *pattern, size_t len) +{ + struct reflog_expire_cfg *ent; + + if (!reflog_expire_cfg_tail) + reflog_expire_cfg_tail = &reflog_expire_cfg; + + for (ent = reflog_expire_cfg; ent; ent = ent->next) + if (!strncmp(ent->pattern, pattern, len) && + ent->pattern[len] == '\0') + return ent; + + FLEX_ALLOC_MEM(ent, pattern, pattern, len); + *reflog_expire_cfg_tail = ent; + reflog_expire_cfg_tail = &(ent->next); + return ent; +} + +/* expiry timer slot */ +#define EXPIRE_TOTAL 01 +#define EXPIRE_UNREACH 02 + +static int reflog_expire_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + const char *pattern, *key; + size_t pattern_len; + timestamp_t expire; + int slot; + struct reflog_expire_cfg *ent; + + if (parse_config_key(var, "gc", &pattern, &pattern_len, &key) < 0) + return git_default_config(var, value, ctx, cb); + + if (!strcmp(key, "reflogexpire")) { + slot = EXPIRE_TOTAL; + if (git_config_expiry_date(&expire, var, value)) + return -1; + } else if (!strcmp(key, "reflogexpireunreachable")) { + slot = EXPIRE_UNREACH; + if (git_config_expiry_date(&expire, var, value)) + return -1; + } else + return git_default_config(var, value, ctx, cb); + + if (!pattern) { + switch (slot) { + case EXPIRE_TOTAL: + default_reflog_expire = expire; + break; + case EXPIRE_UNREACH: + default_reflog_expire_unreachable = expire; + break; + } + return 0; + } + + ent = find_cfg_ent(pattern, pattern_len); + if (!ent) + return -1; + switch (slot) { + case EXPIRE_TOTAL: + ent->expire_total = expire; + break; + case EXPIRE_UNREACH: + ent->expire_unreachable = expire; + break; + } + return 0; +} + +static void set_reflog_expiry_param(struct cmd_reflog_expire_cb *cb, const char *ref) +{ + struct reflog_expire_cfg *ent; + + if (cb->explicit_expiry == (EXPIRE_TOTAL|EXPIRE_UNREACH)) + return; /* both given explicitly -- nothing to tweak */ + + for (ent = reflog_expire_cfg; ent; ent = ent->next) { + if (!wildmatch(ent->pattern, ref, 0)) { + if (!(cb->explicit_expiry & EXPIRE_TOTAL)) + cb->expire_total = ent->expire_total; + if (!(cb->explicit_expiry & EXPIRE_UNREACH)) + cb->expire_unreachable = ent->expire_unreachable; + return; + } + } + + /* + * If unconfigured, make stash never expire + */ + if (!strcmp(ref, "refs/stash")) { + if (!(cb->explicit_expiry & EXPIRE_TOTAL)) + cb->expire_total = 0; + if (!(cb->explicit_expiry & EXPIRE_UNREACH)) + cb->expire_unreachable = 0; + return; + } + + /* Nothing matched -- use the default value */ + if (!(cb->explicit_expiry & EXPIRE_TOTAL)) + cb->expire_total = default_reflog_expire; + if (!(cb->explicit_expiry & EXPIRE_UNREACH)) + cb->expire_unreachable = default_reflog_expire_unreachable; +} + +static int expire_unreachable_callback(const struct option *opt, + const char *arg, + int unset) +{ + struct cmd_reflog_expire_cb *cmd = opt->value; + + BUG_ON_OPT_NEG(unset); + + if (parse_expiry_date(arg, &cmd->expire_unreachable)) + die(_("invalid timestamp '%s' given to '--%s'"), + arg, opt->long_name); + + cmd->explicit_expiry |= EXPIRE_UNREACH; + return 0; +} + +static int expire_total_callback(const struct option *opt, + const char *arg, + int unset) +{ + struct cmd_reflog_expire_cb *cmd = opt->value; + + BUG_ON_OPT_NEG(unset); + + if (parse_expiry_date(arg, &cmd->expire_total)) + die(_("invalid timestamp '%s' given to '--%s'"), + arg, opt->long_name); + + cmd->explicit_expiry |= EXPIRE_TOTAL; + return 0; +} + +static int cmd_reflog_show(int argc, const char **argv, const char *prefix) +{ + struct option options[] = { + OPT_END() + }; + + parse_options(argc, argv, prefix, options, reflog_show_usage, + PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_ARGV0 | + PARSE_OPT_KEEP_UNKNOWN_OPT); + + return cmd_log_reflog(argc, argv, prefix); +} + +static int cmd_reflog_expire(int argc, const char **argv, const char *prefix) +{ + struct cmd_reflog_expire_cb cmd = { 0 }; + timestamp_t now = time(NULL); + int i, status, do_all, single_worktree = 0; + unsigned int flags = 0; + int verbose = 0; + reflog_expiry_should_prune_fn *should_prune_fn = should_expire_reflog_ent; + const struct option options[] = { + OPT_BIT(0, "dry-run", &flags, N_("do not actually prune any entries"), + EXPIRE_REFLOGS_DRY_RUN), + OPT_BIT(0, "rewrite", &flags, + N_("rewrite the old SHA1 with the new SHA1 of the entry that now precedes it"), + EXPIRE_REFLOGS_REWRITE), + OPT_BIT(0, "updateref", &flags, + N_("update the reference to the value of the top reflog entry"), + EXPIRE_REFLOGS_UPDATE_REF), + OPT_BOOL(0, "verbose", &verbose, N_("print extra information on screen")), + OPT_CALLBACK_F(0, "expire", &cmd, N_("timestamp"), + N_("prune entries older than the specified time"), + PARSE_OPT_NONEG, + expire_total_callback), + OPT_CALLBACK_F(0, "expire-unreachable", &cmd, N_("timestamp"), + N_("prune entries older than <time> that are not reachable from the current tip of the branch"), + PARSE_OPT_NONEG, + expire_unreachable_callback), + OPT_BOOL(0, "stale-fix", &cmd.stalefix, + N_("prune any reflog entries that point to broken commits")), + OPT_BOOL(0, "all", &do_all, N_("process the reflogs of all references")), + OPT_BOOL(0, "single-worktree", &single_worktree, + N_("limits processing to reflogs from the current worktree only")), + OPT_END() + }; + + default_reflog_expire_unreachable = now - 30 * 24 * 3600; + default_reflog_expire = now - 90 * 24 * 3600; + git_config(reflog_expire_config, NULL); + + save_commit_buffer = 0; + do_all = status = 0; + + cmd.explicit_expiry = 0; + cmd.expire_total = default_reflog_expire; + cmd.expire_unreachable = default_reflog_expire_unreachable; + + argc = parse_options(argc, argv, prefix, options, reflog_expire_usage, 0); + + if (verbose) + should_prune_fn = should_expire_reflog_ent_verbose; + + /* + * We can trust the commits and objects reachable from refs + * even in older repository. We cannot trust what's reachable + * from reflog if the repository was pruned with older git. + */ + if (cmd.stalefix) { + struct rev_info revs; + + repo_init_revisions(the_repository, &revs, prefix); + revs.do_not_die_on_missing_objects = 1; + revs.ignore_missing = 1; + revs.ignore_missing_links = 1; + if (verbose) + printf(_("Marking reachable objects...")); + mark_reachable_objects(&revs, 0, 0, NULL); + release_revisions(&revs); + if (verbose) + putchar('\n'); + } + + if (do_all) { + struct worktree_reflogs collected = { + .reflogs = STRING_LIST_INIT_DUP, + }; + struct string_list_item *item; + struct worktree **worktrees, **p; + + worktrees = get_worktrees(); + for (p = worktrees; *p; p++) { + if (single_worktree && !(*p)->is_current) + continue; + collected.worktree = *p; + refs_for_each_reflog(get_worktree_ref_store(*p), + collect_reflog, &collected); + } + free_worktrees(worktrees); + + for_each_string_list_item(item, &collected.reflogs) { + struct expire_reflog_policy_cb cb = { + .cmd = cmd, + .dry_run = !!(flags & EXPIRE_REFLOGS_DRY_RUN), + }; + + set_reflog_expiry_param(&cb.cmd, item->string); + status |= reflog_expire(item->string, flags, + reflog_expiry_prepare, + should_prune_fn, + reflog_expiry_cleanup, + &cb); + } + string_list_clear(&collected.reflogs, 0); + } + + for (i = 0; i < argc; i++) { + char *ref; + struct expire_reflog_policy_cb cb = { .cmd = cmd }; + + if (!dwim_log(argv[i], strlen(argv[i]), NULL, &ref)) { + status |= error(_("%s points nowhere!"), argv[i]); + continue; + } + set_reflog_expiry_param(&cb.cmd, ref); + status |= reflog_expire(ref, flags, + reflog_expiry_prepare, + should_prune_fn, + reflog_expiry_cleanup, + &cb); + free(ref); + } + return status; +} + +static int cmd_reflog_delete(int argc, const char **argv, const char *prefix) +{ + int i, status = 0; + unsigned int flags = 0; + int verbose = 0; + + const struct option options[] = { + OPT_BIT(0, "dry-run", &flags, N_("do not actually prune any entries"), + EXPIRE_REFLOGS_DRY_RUN), + OPT_BIT(0, "rewrite", &flags, + N_("rewrite the old SHA1 with the new SHA1 of the entry that now precedes it"), + EXPIRE_REFLOGS_REWRITE), + OPT_BIT(0, "updateref", &flags, + N_("update the reference to the value of the top reflog entry"), + EXPIRE_REFLOGS_UPDATE_REF), + OPT_BOOL(0, "verbose", &verbose, N_("print extra information on screen")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, reflog_delete_usage, 0); + + if (argc < 1) + return error(_("no reflog specified to delete")); + + for (i = 0; i < argc; i++) + status |= reflog_delete(argv[i], flags, verbose); + + return status; +} + +static int cmd_reflog_exists(int argc, const char **argv, const char *prefix) +{ + struct option options[] = { + OPT_END() + }; + const char *refname; + + argc = parse_options(argc, argv, prefix, options, reflog_exists_usage, + 0); + if (!argc) + usage_with_options(reflog_exists_usage, options); + + refname = argv[0]; + if (check_refname_format(refname, REFNAME_ALLOW_ONELEVEL)) + die(_("invalid ref format: %s"), refname); + return !reflog_exists(refname); +} + +/* + * main "reflog" + */ + +int cmd_reflog(int argc, const char **argv, const char *prefix) +{ + parse_opt_subcommand_fn *fn = NULL; + struct option options[] = { + OPT_SUBCOMMAND("show", &fn, cmd_reflog_show), + OPT_SUBCOMMAND("expire", &fn, cmd_reflog_expire), + OPT_SUBCOMMAND("delete", &fn, cmd_reflog_delete), + OPT_SUBCOMMAND("exists", &fn, cmd_reflog_exists), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, reflog_usage, + PARSE_OPT_SUBCOMMAND_OPTIONAL | + PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_ARGV0 | + PARSE_OPT_KEEP_UNKNOWN_OPT); + if (fn) + return fn(argc - 1, argv + 1, prefix); + else + return cmd_log_reflog(argc, argv, prefix); +} diff --git a/builtin/remote-ext.c b/builtin/remote-ext.c new file mode 100644 index 0000000..282782e --- /dev/null +++ b/builtin/remote-ext.c @@ -0,0 +1,206 @@ +#include "builtin.h" +#include "transport.h" +#include "run-command.h" +#include "pkt-line.h" + +static const char usage_msg[] = + "git remote-ext <remote> <url>"; + +/* + * URL syntax: + * 'command [arg1 [arg2 [...]]]' Invoke command with given arguments. + * Special characters: + * '% ': Literal space in argument. + * '%%': Literal percent sign. + * '%S': Name of service (git-upload-pack/git-upload-archive/ + * git-receive-pack. + * '%s': Same as \s, but with possible git- prefix stripped. + * '%G': Only allowed as first 'character' of argument. Do not pass this + * Argument to command, instead send this as name of repository + * in in-line git://-style request (also activates sending this + * style of request). + * '%V': Only allowed as first 'character' of argument. Used in + * conjunction with '%G': Do not pass this argument to command, + * instead send this as vhost in git://-style request (note: does + * not activate sending git:// style request). + */ + +static char *git_req; +static char *git_req_vhost; + +static char *strip_escapes(const char *str, const char *service, + const char **next) +{ + size_t rpos = 0; + int escape = 0; + char special = 0; + const char *service_noprefix = service; + struct strbuf ret = STRBUF_INIT; + + skip_prefix(service_noprefix, "git-", &service_noprefix); + + /* Pass the service to command. */ + setenv("GIT_EXT_SERVICE", service, 1); + setenv("GIT_EXT_SERVICE_NOPREFIX", service_noprefix, 1); + + /* Scan the length of argument. */ + while (str[rpos] && (escape || str[rpos] != ' ')) { + if (escape) { + switch (str[rpos]) { + case ' ': + case '%': + case 's': + case 'S': + break; + case 'G': + case 'V': + special = str[rpos]; + if (rpos == 1) + break; + /* fallthrough */ + default: + die("Bad remote-ext placeholder '%%%c'.", + str[rpos]); + } + escape = 0; + } else + escape = (str[rpos] == '%'); + rpos++; + } + if (escape && !str[rpos]) + die("remote-ext command has incomplete placeholder"); + *next = str + rpos; + if (**next == ' ') + ++*next; /* Skip over space */ + + /* + * Do the actual placeholder substitution. The string will be short + * enough not to overflow integers. + */ + rpos = special ? 2 : 0; /* Skip first 2 bytes in specials. */ + escape = 0; + while (str[rpos] && (escape || str[rpos] != ' ')) { + if (escape) { + switch (str[rpos]) { + case ' ': + case '%': + strbuf_addch(&ret, str[rpos]); + break; + case 's': + strbuf_addstr(&ret, service_noprefix); + break; + case 'S': + strbuf_addstr(&ret, service); + break; + } + escape = 0; + } else + switch (str[rpos]) { + case '%': + escape = 1; + break; + default: + strbuf_addch(&ret, str[rpos]); + break; + } + rpos++; + } + switch (special) { + case 'G': + git_req = strbuf_detach(&ret, NULL); + return NULL; + case 'V': + git_req_vhost = strbuf_detach(&ret, NULL); + return NULL; + default: + return strbuf_detach(&ret, NULL); + } +} + +static void parse_argv(struct strvec *out, const char *arg, const char *service) +{ + while (*arg) { + char *expanded = strip_escapes(arg, service, &arg); + if (expanded) + strvec_push(out, expanded); + free(expanded); + } +} + +static void send_git_request(int stdin_fd, const char *serv, const char *repo, + const char *vhost) +{ + if (!vhost) + packet_write_fmt(stdin_fd, "%s %s%c", serv, repo, 0); + else + packet_write_fmt(stdin_fd, "%s %s%chost=%s%c", serv, repo, 0, + vhost, 0); +} + +static int run_child(const char *arg, const char *service) +{ + int r; + struct child_process child = CHILD_PROCESS_INIT; + + child.in = -1; + child.out = -1; + child.err = 0; + parse_argv(&child.args, arg, service); + + if (start_command(&child) < 0) + die("Can't run specified command"); + + if (git_req) + send_git_request(child.in, service, git_req, git_req_vhost); + + r = bidirectional_transfer_loop(child.out, child.in); + if (!r) + r = finish_command(&child); + else + finish_command(&child); + return r; +} + +#define MAXCOMMAND 4096 + +static int command_loop(const char *child) +{ + char buffer[MAXCOMMAND]; + + while (1) { + size_t i; + const char *arg; + + if (!fgets(buffer, MAXCOMMAND - 1, stdin)) { + if (ferror(stdin)) + die("Command input error"); + exit(0); + } + /* Strip end of line characters. */ + i = strlen(buffer); + while (i > 0 && isspace(buffer[i - 1])) + buffer[--i] = 0; + + if (!strcmp(buffer, "capabilities")) { + printf("*connect\n\n"); + fflush(stdout); + } else if (skip_prefix(buffer, "connect ", &arg)) { + printf("\n"); + fflush(stdout); + return run_child(child, arg); + } else { + fprintf(stderr, "Bad command"); + return 1; + } + } +} + +int cmd_remote_ext(int argc, const char **argv, const char *prefix) +{ + BUG_ON_NON_EMPTY_PREFIX(prefix); + + if (argc != 3) + usage(usage_msg); + + return command_loop(argv[2]); +} diff --git a/builtin/remote-fd.c b/builtin/remote-fd.c new file mode 100644 index 0000000..9020fab --- /dev/null +++ b/builtin/remote-fd.c @@ -0,0 +1,84 @@ +#include "builtin.h" +#include "transport.h" + +static const char usage_msg[] = + "git remote-fd <remote> <url>"; + +/* + * URL syntax: + * 'fd::<inoutfd>[/<anything>]' Read/write socket pair + * <inoutfd>. + * 'fd::<infd>,<outfd>[/<anything>]' Read pipe <infd> and write + * pipe <outfd>. + * [foo] indicates 'foo' is optional. <anything> is any string. + * + * The data output to <outfd>/<inoutfd> should be passed unmolested to + * git-receive-pack/git-upload-pack/git-upload-archive and output of + * git-receive-pack/git-upload-pack/git-upload-archive should be passed + * unmolested to <infd>/<inoutfd>. + * + */ + +#define MAXCOMMAND 4096 + +static void command_loop(int input_fd, int output_fd) +{ + char buffer[MAXCOMMAND]; + + while (1) { + size_t i; + if (!fgets(buffer, MAXCOMMAND - 1, stdin)) { + if (ferror(stdin)) + die("Input error"); + return; + } + /* Strip end of line characters. */ + i = strlen(buffer); + while (i > 0 && isspace(buffer[i - 1])) + buffer[--i] = 0; + + if (!strcmp(buffer, "capabilities")) { + printf("*connect\n\n"); + fflush(stdout); + } else if (starts_with(buffer, "connect ")) { + printf("\n"); + fflush(stdout); + if (bidirectional_transfer_loop(input_fd, + output_fd)) + die("Copying data between file descriptors failed"); + return; + } else { + die("Bad command: %s", buffer); + } + } +} + +int cmd_remote_fd(int argc, const char **argv, const char *prefix) +{ + int input_fd = -1; + int output_fd = -1; + char *end; + + BUG_ON_NON_EMPTY_PREFIX(prefix); + + if (argc != 3) + usage(usage_msg); + + input_fd = (int)strtoul(argv[2], &end, 10); + + if ((end == argv[2]) || (*end != ',' && *end != '/' && *end)) + die("Bad URL syntax"); + + if (*end == '/' || !*end) { + output_fd = input_fd; + } else { + char *end2; + output_fd = (int)strtoul(end + 1, &end2, 10); + + if ((end2 == end + 1) || (*end2 != '/' && *end2)) + die("Bad URL syntax"); + } + + command_loop(input_fd, output_fd); + return 0; +} diff --git a/builtin/remote.c b/builtin/remote.c new file mode 100644 index 0000000..d91bbe7 --- /dev/null +++ b/builtin/remote.c @@ -0,0 +1,1788 @@ +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "parse-options.h" +#include "path.h" +#include "transport.h" +#include "remote.h" +#include "string-list.h" +#include "strbuf.h" +#include "run-command.h" +#include "rebase.h" +#include "refs.h" +#include "refspec.h" +#include "object-store-ll.h" +#include "strvec.h" +#include "commit-reach.h" +#include "progress.h" + +static const char * const builtin_remote_usage[] = { + "git remote [-v | --verbose]", + N_("git remote add [-t <branch>] [-m <master>] [-f] [--tags | --no-tags] [--mirror=<fetch|push>] <name> <url>"), + N_("git remote rename [--[no-]progress] <old> <new>"), + N_("git remote remove <name>"), + N_("git remote set-head <name> (-a | --auto | -d | --delete | <branch>)"), + N_("git remote [-v | --verbose] show [-n] <name>"), + N_("git remote prune [-n | --dry-run] <name>"), + N_("git remote [-v | --verbose] update [-p | --prune] [(<group> | <remote>)...]"), + N_("git remote set-branches [--add] <name> <branch>..."), + N_("git remote get-url [--push] [--all] <name>"), + N_("git remote set-url [--push] <name> <newurl> [<oldurl>]"), + N_("git remote set-url --add <name> <newurl>"), + N_("git remote set-url --delete <name> <url>"), + NULL +}; + +static const char * const builtin_remote_add_usage[] = { + N_("git remote add [<options>] <name> <url>"), + NULL +}; + +static const char * const builtin_remote_rename_usage[] = { + N_("git remote rename [--[no-]progress] <old> <new>"), + NULL +}; + +static const char * const builtin_remote_rm_usage[] = { + N_("git remote remove <name>"), + NULL +}; + +static const char * const builtin_remote_sethead_usage[] = { + N_("git remote set-head <name> (-a | --auto | -d | --delete | <branch>)"), + NULL +}; + +static const char * const builtin_remote_setbranches_usage[] = { + N_("git remote set-branches <name> <branch>..."), + N_("git remote set-branches --add <name> <branch>..."), + NULL +}; + +static const char * const builtin_remote_show_usage[] = { + N_("git remote show [<options>] <name>"), + NULL +}; + +static const char * const builtin_remote_prune_usage[] = { + N_("git remote prune [<options>] <name>"), + NULL +}; + +static const char * const builtin_remote_update_usage[] = { + N_("git remote update [<options>] [<group> | <remote>]..."), + NULL +}; + +static const char * const builtin_remote_geturl_usage[] = { + N_("git remote get-url [--push] [--all] <name>"), + NULL +}; + +static const char * const builtin_remote_seturl_usage[] = { + N_("git remote set-url [--push] <name> <newurl> [<oldurl>]"), + N_("git remote set-url --add <name> <newurl>"), + N_("git remote set-url --delete <name> <url>"), + NULL +}; + +#define GET_REF_STATES (1<<0) +#define GET_HEAD_NAMES (1<<1) +#define GET_PUSH_REF_STATES (1<<2) + +static int verbose; + +static int fetch_remote(const char *name) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + + strvec_push(&cmd.args, "fetch"); + if (verbose) + strvec_push(&cmd.args, "-v"); + strvec_push(&cmd.args, name); + cmd.git_cmd = 1; + printf_ln(_("Updating %s"), name); + if (run_command(&cmd)) + return error(_("Could not fetch %s"), name); + return 0; +} + +enum { + TAGS_UNSET = 0, + TAGS_DEFAULT = 1, + TAGS_SET = 2 +}; + +#define MIRROR_NONE 0 +#define MIRROR_FETCH 1 +#define MIRROR_PUSH 2 +#define MIRROR_BOTH (MIRROR_FETCH|MIRROR_PUSH) + +static void add_branch(const char *key, const char *branchname, + const char *remotename, int mirror, struct strbuf *tmp) +{ + strbuf_reset(tmp); + strbuf_addch(tmp, '+'); + if (mirror) + strbuf_addf(tmp, "refs/%s:refs/%s", + branchname, branchname); + else + strbuf_addf(tmp, "refs/heads/%s:refs/remotes/%s/%s", + branchname, remotename, branchname); + git_config_set_multivar(key, tmp->buf, "^$", 0); +} + +static const char mirror_advice[] = +N_("--mirror is dangerous and deprecated; please\n" + "\t use --mirror=fetch or --mirror=push instead"); + +static int parse_mirror_opt(const struct option *opt, const char *arg, int not) +{ + unsigned *mirror = opt->value; + if (not) + *mirror = MIRROR_NONE; + else if (!arg) { + warning("%s", _(mirror_advice)); + *mirror = MIRROR_BOTH; + } + else if (!strcmp(arg, "fetch")) + *mirror = MIRROR_FETCH; + else if (!strcmp(arg, "push")) + *mirror = MIRROR_PUSH; + else + return error(_("unknown mirror argument: %s"), arg); + return 0; +} + +static int add(int argc, const char **argv, const char *prefix) +{ + int fetch = 0, fetch_tags = TAGS_DEFAULT; + unsigned mirror = MIRROR_NONE; + struct string_list track = STRING_LIST_INIT_NODUP; + const char *master = NULL; + struct remote *remote; + struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT; + const char *name, *url; + int i; + + struct option options[] = { + OPT_BOOL('f', "fetch", &fetch, N_("fetch the remote branches")), + OPT_SET_INT(0, "tags", &fetch_tags, + N_("import all tags and associated objects when fetching\n" + "or do not fetch any tag at all (--no-tags)"), + TAGS_SET), + OPT_STRING_LIST('t', "track", &track, N_("branch"), + N_("branch(es) to track")), + OPT_STRING('m', "master", &master, N_("branch"), N_("master branch")), + OPT_CALLBACK_F(0, "mirror", &mirror, "(push|fetch)", + N_("set up remote as a mirror to push to or fetch from"), + PARSE_OPT_OPTARG | PARSE_OPT_COMP_ARG, parse_mirror_opt), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, + builtin_remote_add_usage, 0); + + if (argc != 2) + usage_with_options(builtin_remote_add_usage, options); + + if (mirror && master) + die(_("specifying a master branch makes no sense with --mirror")); + if (mirror && !(mirror & MIRROR_FETCH) && track.nr) + die(_("specifying branches to track makes sense only with fetch mirrors")); + + name = argv[0]; + url = argv[1]; + + remote = remote_get(name); + if (remote_is_configured(remote, 1)) { + error(_("remote %s already exists."), name); + exit(3); + } + + if (!valid_remote_name(name)) + die(_("'%s' is not a valid remote name"), name); + + strbuf_addf(&buf, "remote.%s.url", name); + git_config_set(buf.buf, url); + + if (!mirror || mirror & MIRROR_FETCH) { + strbuf_reset(&buf); + strbuf_addf(&buf, "remote.%s.fetch", name); + if (track.nr == 0) + string_list_append(&track, "*"); + for (i = 0; i < track.nr; i++) { + add_branch(buf.buf, track.items[i].string, + name, mirror, &buf2); + } + } + + if (mirror & MIRROR_PUSH) { + strbuf_reset(&buf); + strbuf_addf(&buf, "remote.%s.mirror", name); + git_config_set(buf.buf, "true"); + } + + if (fetch_tags != TAGS_DEFAULT) { + strbuf_reset(&buf); + strbuf_addf(&buf, "remote.%s.tagOpt", name); + git_config_set(buf.buf, + fetch_tags == TAGS_SET ? "--tags" : "--no-tags"); + } + + if (fetch && fetch_remote(name)) + return 1; + + if (master) { + strbuf_reset(&buf); + strbuf_addf(&buf, "refs/remotes/%s/HEAD", name); + + strbuf_reset(&buf2); + strbuf_addf(&buf2, "refs/remotes/%s/%s", name, master); + + if (create_symref(buf.buf, buf2.buf, "remote add")) + return error(_("Could not setup master '%s'"), master); + } + + strbuf_release(&buf); + strbuf_release(&buf2); + string_list_clear(&track, 0); + + return 0; +} + +struct branch_info { + char *remote_name; + struct string_list merge; + enum rebase_type rebase; + char *push_remote_name; +}; + +static struct string_list branch_list = STRING_LIST_INIT_NODUP; + +static const char *abbrev_ref(const char *name, const char *prefix) +{ + skip_prefix(name, prefix, &name); + return name; +} +#define abbrev_branch(name) abbrev_ref((name), "refs/heads/") + +static int config_read_branches(const char *key, const char *value, + const struct config_context *ctx UNUSED, + void *data UNUSED) +{ + const char *orig_key = key; + char *name; + struct string_list_item *item; + struct branch_info *info; + enum { REMOTE, MERGE, REBASE, PUSH_REMOTE } type; + size_t key_len; + + if (!starts_with(key, "branch.")) + return 0; + + key += strlen("branch."); + if (strip_suffix(key, ".remote", &key_len)) + type = REMOTE; + else if (strip_suffix(key, ".merge", &key_len)) + type = MERGE; + else if (strip_suffix(key, ".rebase", &key_len)) + type = REBASE; + else if (strip_suffix(key, ".pushremote", &key_len)) + type = PUSH_REMOTE; + else + return 0; + name = xmemdupz(key, key_len); + + item = string_list_insert(&branch_list, name); + + if (!item->util) + item->util = xcalloc(1, sizeof(struct branch_info)); + info = item->util; + switch (type) { + case REMOTE: + if (info->remote_name) + warning(_("more than one %s"), orig_key); + info->remote_name = xstrdup(value); + break; + case MERGE: { + char *space = strchr(value, ' '); + value = abbrev_branch(value); + while (space) { + char *merge; + merge = xstrndup(value, space - value); + string_list_append(&info->merge, merge); + value = abbrev_branch(space + 1); + space = strchr(value, ' '); + } + string_list_append(&info->merge, xstrdup(value)); + break; + } + case REBASE: + /* + * Consider invalid values as false and check the + * truth value with >= REBASE_TRUE. + */ + info->rebase = rebase_parse_value(value); + if (info->rebase == REBASE_INVALID) + warning(_("unhandled branch.%s.rebase=%s; assuming " + "'true'"), name, value); + break; + case PUSH_REMOTE: + if (info->push_remote_name) + warning(_("more than one %s"), orig_key); + info->push_remote_name = xstrdup(value); + break; + default: + BUG("unexpected type=%d", type); + } + + return 0; +} + +static void read_branches(void) +{ + if (branch_list.nr) + return; + git_config(config_read_branches, NULL); +} + +struct ref_states { + struct remote *remote; + struct string_list new_refs, skipped, stale, tracked, heads, push; + int queried; +}; + +#define REF_STATES_INIT { \ + .new_refs = STRING_LIST_INIT_DUP, \ + .skipped = STRING_LIST_INIT_DUP, \ + .stale = STRING_LIST_INIT_DUP, \ + .tracked = STRING_LIST_INIT_DUP, \ + .heads = STRING_LIST_INIT_DUP, \ + .push = STRING_LIST_INIT_DUP, \ +} + +static int get_ref_states(const struct ref *remote_refs, struct ref_states *states) +{ + struct ref *fetch_map = NULL, **tail = &fetch_map; + struct ref *ref, *stale_refs; + int i; + + for (i = 0; i < states->remote->fetch.nr; i++) + if (get_fetch_map(remote_refs, &states->remote->fetch.items[i], &tail, 1)) + die(_("Could not get fetch map for refspec %s"), + states->remote->fetch.raw[i]); + + for (ref = fetch_map; ref; ref = ref->next) { + if (omit_name_by_refspec(ref->name, &states->remote->fetch)) + string_list_append(&states->skipped, abbrev_branch(ref->name)); + else if (!ref->peer_ref || !ref_exists(ref->peer_ref->name)) + string_list_append(&states->new_refs, abbrev_branch(ref->name)); + else + string_list_append(&states->tracked, abbrev_branch(ref->name)); + } + stale_refs = get_stale_heads(&states->remote->fetch, fetch_map); + for (ref = stale_refs; ref; ref = ref->next) { + struct string_list_item *item = + string_list_append(&states->stale, abbrev_branch(ref->name)); + item->util = xstrdup(ref->name); + } + free_refs(stale_refs); + free_refs(fetch_map); + + string_list_sort(&states->new_refs); + string_list_sort(&states->skipped); + string_list_sort(&states->tracked); + string_list_sort(&states->stale); + + return 0; +} + +struct push_info { + char *dest; + int forced; + enum { + PUSH_STATUS_CREATE = 0, + PUSH_STATUS_DELETE, + PUSH_STATUS_UPTODATE, + PUSH_STATUS_FASTFORWARD, + PUSH_STATUS_OUTOFDATE, + PUSH_STATUS_NOTQUERIED + } status; +}; + +static int get_push_ref_states(const struct ref *remote_refs, + struct ref_states *states) +{ + struct remote *remote = states->remote; + struct ref *ref, *local_refs, *push_map; + if (remote->mirror) + return 0; + + local_refs = get_local_heads(); + push_map = copy_ref_list(remote_refs); + + match_push_refs(local_refs, &push_map, &remote->push, MATCH_REFS_NONE); + + for (ref = push_map; ref; ref = ref->next) { + struct string_list_item *item; + struct push_info *info; + + if (!ref->peer_ref) + continue; + oidcpy(&ref->new_oid, &ref->peer_ref->new_oid); + + item = string_list_append(&states->push, + abbrev_branch(ref->peer_ref->name)); + item->util = xcalloc(1, sizeof(struct push_info)); + info = item->util; + info->forced = ref->force; + info->dest = xstrdup(abbrev_branch(ref->name)); + + if (is_null_oid(&ref->new_oid)) { + info->status = PUSH_STATUS_DELETE; + } else if (oideq(&ref->old_oid, &ref->new_oid)) + info->status = PUSH_STATUS_UPTODATE; + else if (is_null_oid(&ref->old_oid)) + info->status = PUSH_STATUS_CREATE; + else if (repo_has_object_file(the_repository, &ref->old_oid) && + ref_newer(&ref->new_oid, &ref->old_oid)) + info->status = PUSH_STATUS_FASTFORWARD; + else + info->status = PUSH_STATUS_OUTOFDATE; + } + free_refs(local_refs); + free_refs(push_map); + return 0; +} + +static int get_push_ref_states_noquery(struct ref_states *states) +{ + int i; + struct remote *remote = states->remote; + struct string_list_item *item; + struct push_info *info; + + if (remote->mirror) + return 0; + + if (!remote->push.nr) { + item = string_list_append(&states->push, _("(matching)")); + info = item->util = xcalloc(1, sizeof(struct push_info)); + info->status = PUSH_STATUS_NOTQUERIED; + info->dest = xstrdup(item->string); + } + for (i = 0; i < remote->push.nr; i++) { + const struct refspec_item *spec = &remote->push.items[i]; + if (spec->matching) + item = string_list_append(&states->push, _("(matching)")); + else if (strlen(spec->src)) + item = string_list_append(&states->push, spec->src); + else + item = string_list_append(&states->push, _("(delete)")); + + info = item->util = xcalloc(1, sizeof(struct push_info)); + info->forced = spec->force; + info->status = PUSH_STATUS_NOTQUERIED; + info->dest = xstrdup(spec->dst ? spec->dst : item->string); + } + return 0; +} + +static int get_head_names(const struct ref *remote_refs, struct ref_states *states) +{ + struct ref *ref, *matches; + struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map; + struct refspec_item refspec; + + memset(&refspec, 0, sizeof(refspec)); + refspec.force = 0; + refspec.pattern = 1; + refspec.src = refspec.dst = "refs/heads/*"; + get_fetch_map(remote_refs, &refspec, &fetch_map_tail, 0); + matches = guess_remote_head(find_ref_by_name(remote_refs, "HEAD"), + fetch_map, 1); + for (ref = matches; ref; ref = ref->next) + string_list_append(&states->heads, abbrev_branch(ref->name)); + + free_refs(fetch_map); + free_refs(matches); + + return 0; +} + +struct known_remote { + struct known_remote *next; + struct remote *remote; +}; + +struct known_remotes { + struct remote *to_delete; + struct known_remote *list; +}; + +static int add_known_remote(struct remote *remote, void *cb_data) +{ + struct known_remotes *all = cb_data; + struct known_remote *r; + + if (!strcmp(all->to_delete->name, remote->name)) + return 0; + + r = xmalloc(sizeof(*r)); + r->remote = remote; + r->next = all->list; + all->list = r; + return 0; +} + +struct branches_for_remote { + struct remote *remote; + struct string_list *branches, *skipped; + struct known_remotes *keep; +}; + +static int add_branch_for_removal(const char *refname, + const struct object_id *oid UNUSED, + int flags UNUSED, void *cb_data) +{ + struct branches_for_remote *branches = cb_data; + struct refspec_item refspec; + struct known_remote *kr; + + memset(&refspec, 0, sizeof(refspec)); + refspec.dst = (char *)refname; + if (remote_find_tracking(branches->remote, &refspec)) + return 0; + + /* don't delete a branch if another remote also uses it */ + for (kr = branches->keep->list; kr; kr = kr->next) { + memset(&refspec, 0, sizeof(refspec)); + refspec.dst = (char *)refname; + if (!remote_find_tracking(kr->remote, &refspec)) + return 0; + } + + /* don't delete non-remote-tracking refs */ + if (!starts_with(refname, "refs/remotes/")) { + /* advise user how to delete local branches */ + if (starts_with(refname, "refs/heads/")) + string_list_append(branches->skipped, + abbrev_branch(refname)); + /* silently skip over other non-remote refs */ + return 0; + } + + string_list_append(branches->branches, refname); + + return 0; +} + +struct rename_info { + const char *old_name; + const char *new_name; + struct string_list *remote_branches; + uint32_t symrefs_nr; +}; + +static int read_remote_branches(const char *refname, + const struct object_id *oid UNUSED, + int flags UNUSED, void *cb_data) +{ + struct rename_info *rename = cb_data; + struct strbuf buf = STRBUF_INIT; + struct string_list_item *item; + int flag; + const char *symref; + + strbuf_addf(&buf, "refs/remotes/%s/", rename->old_name); + if (starts_with(refname, buf.buf)) { + item = string_list_append(rename->remote_branches, refname); + symref = resolve_ref_unsafe(refname, RESOLVE_REF_READING, + NULL, &flag); + if (symref && (flag & REF_ISSYMREF)) { + item->util = xstrdup(symref); + rename->symrefs_nr++; + } else { + item->util = NULL; + } + } + strbuf_release(&buf); + + return 0; +} + +static int migrate_file(struct remote *remote) +{ + struct strbuf buf = STRBUF_INIT; + int i; + + strbuf_addf(&buf, "remote.%s.url", remote->name); + for (i = 0; i < remote->url_nr; i++) + git_config_set_multivar(buf.buf, remote->url[i], "^$", 0); + strbuf_reset(&buf); + strbuf_addf(&buf, "remote.%s.push", remote->name); + for (i = 0; i < remote->push.raw_nr; i++) + git_config_set_multivar(buf.buf, remote->push.raw[i], "^$", 0); + strbuf_reset(&buf); + strbuf_addf(&buf, "remote.%s.fetch", remote->name); + for (i = 0; i < remote->fetch.raw_nr; i++) + git_config_set_multivar(buf.buf, remote->fetch.raw[i], "^$", 0); + if (remote->origin == REMOTE_REMOTES) + unlink_or_warn(git_path("remotes/%s", remote->name)); + else if (remote->origin == REMOTE_BRANCHES) + unlink_or_warn(git_path("branches/%s", remote->name)); + strbuf_release(&buf); + + return 0; +} + +struct push_default_info +{ + const char *old_name; + enum config_scope scope; + struct strbuf origin; + int linenr; +}; + +static int config_read_push_default(const char *key, const char *value, + const struct config_context *ctx, void *cb) +{ + const struct key_value_info *kvi = ctx->kvi; + + struct push_default_info* info = cb; + if (strcmp(key, "remote.pushdefault") || + !value || strcmp(value, info->old_name)) + return 0; + + info->scope = kvi->scope; + strbuf_reset(&info->origin); + strbuf_addstr(&info->origin, config_origin_type_name(kvi->origin_type)); + info->linenr = kvi->linenr; + + return 0; +} + +static void handle_push_default(const char* old_name, const char* new_name) +{ + struct push_default_info push_default = { + old_name, CONFIG_SCOPE_UNKNOWN, STRBUF_INIT, -1 }; + git_config(config_read_push_default, &push_default); + if (push_default.scope >= CONFIG_SCOPE_COMMAND) + ; /* pass */ + else if (push_default.scope >= CONFIG_SCOPE_LOCAL) { + int result = git_config_set_gently("remote.pushDefault", + new_name); + if (new_name && result && result != CONFIG_NOTHING_SET) + die(_("could not set '%s'"), "remote.pushDefault"); + else if (!new_name && result && result != CONFIG_NOTHING_SET) + die(_("could not unset '%s'"), "remote.pushDefault"); + } else if (push_default.scope >= CONFIG_SCOPE_SYSTEM) { + /* warn */ + warning(_("The %s configuration remote.pushDefault in:\n" + "\t%s:%d\n" + "now names the non-existent remote '%s'"), + config_scope_name(push_default.scope), + push_default.origin.buf, push_default.linenr, + old_name); + } +} + + +static int mv(int argc, const char **argv, const char *prefix) +{ + int show_progress = isatty(2); + struct option options[] = { + OPT_BOOL(0, "progress", &show_progress, N_("force progress reporting")), + OPT_END() + }; + struct remote *oldremote, *newremote; + struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT, buf3 = STRBUF_INIT, + old_remote_context = STRBUF_INIT; + struct string_list remote_branches = STRING_LIST_INIT_DUP; + struct rename_info rename; + int i, refs_renamed_nr = 0, refspec_updated = 0; + struct progress *progress = NULL; + + argc = parse_options(argc, argv, prefix, options, + builtin_remote_rename_usage, 0); + + if (argc != 2) + usage_with_options(builtin_remote_rename_usage, options); + + rename.old_name = argv[0]; + rename.new_name = argv[1]; + rename.remote_branches = &remote_branches; + rename.symrefs_nr = 0; + + oldremote = remote_get(rename.old_name); + if (!remote_is_configured(oldremote, 1)) { + error(_("No such remote: '%s'"), rename.old_name); + exit(2); + } + + if (!strcmp(rename.old_name, rename.new_name) && oldremote->origin != REMOTE_CONFIG) + return migrate_file(oldremote); + + newremote = remote_get(rename.new_name); + if (remote_is_configured(newremote, 1)) { + error(_("remote %s already exists."), rename.new_name); + exit(3); + } + + if (!valid_remote_name(rename.new_name)) + die(_("'%s' is not a valid remote name"), rename.new_name); + + strbuf_addf(&buf, "remote.%s", rename.old_name); + strbuf_addf(&buf2, "remote.%s", rename.new_name); + if (git_config_rename_section(buf.buf, buf2.buf) < 1) + return error(_("Could not rename config section '%s' to '%s'"), + buf.buf, buf2.buf); + + if (oldremote->fetch.raw_nr) { + strbuf_reset(&buf); + strbuf_addf(&buf, "remote.%s.fetch", rename.new_name); + git_config_set_multivar(buf.buf, NULL, NULL, CONFIG_FLAGS_MULTI_REPLACE); + strbuf_addf(&old_remote_context, ":refs/remotes/%s/", rename.old_name); + for (i = 0; i < oldremote->fetch.raw_nr; i++) { + char *ptr; + + strbuf_reset(&buf2); + strbuf_addstr(&buf2, oldremote->fetch.raw[i]); + ptr = strstr(buf2.buf, old_remote_context.buf); + if (ptr) { + refspec_updated = 1; + strbuf_splice(&buf2, + ptr-buf2.buf + strlen(":refs/remotes/"), + strlen(rename.old_name), rename.new_name, + strlen(rename.new_name)); + } else + warning(_("Not updating non-default fetch refspec\n" + "\t%s\n" + "\tPlease update the configuration manually if necessary."), + buf2.buf); + + git_config_set_multivar(buf.buf, buf2.buf, "^$", 0); + } + } + + read_branches(); + for (i = 0; i < branch_list.nr; i++) { + struct string_list_item *item = branch_list.items + i; + struct branch_info *info = item->util; + if (info->remote_name && !strcmp(info->remote_name, rename.old_name)) { + strbuf_reset(&buf); + strbuf_addf(&buf, "branch.%s.remote", item->string); + git_config_set(buf.buf, rename.new_name); + } + if (info->push_remote_name && !strcmp(info->push_remote_name, rename.old_name)) { + strbuf_reset(&buf); + strbuf_addf(&buf, "branch.%s.pushRemote", item->string); + git_config_set(buf.buf, rename.new_name); + } + } + + if (!refspec_updated) + return 0; + + /* + * First remove symrefs, then rename the rest, finally create + * the new symrefs. + */ + for_each_ref(read_remote_branches, &rename); + if (show_progress) { + /* + * Count symrefs twice, since "renaming" them is done by + * deleting and recreating them in two separate passes. + */ + progress = start_progress(_("Renaming remote references"), + rename.remote_branches->nr + rename.symrefs_nr); + } + for (i = 0; i < remote_branches.nr; i++) { + struct string_list_item *item = remote_branches.items + i; + struct strbuf referent = STRBUF_INIT; + + if (refs_read_symbolic_ref(get_main_ref_store(the_repository), item->string, + &referent)) + continue; + if (delete_ref(NULL, item->string, NULL, REF_NO_DEREF)) + die(_("deleting '%s' failed"), item->string); + + strbuf_release(&referent); + display_progress(progress, ++refs_renamed_nr); + } + for (i = 0; i < remote_branches.nr; i++) { + struct string_list_item *item = remote_branches.items + i; + + if (item->util) + continue; + strbuf_reset(&buf); + strbuf_addstr(&buf, item->string); + strbuf_splice(&buf, strlen("refs/remotes/"), strlen(rename.old_name), + rename.new_name, strlen(rename.new_name)); + strbuf_reset(&buf2); + strbuf_addf(&buf2, "remote: renamed %s to %s", + item->string, buf.buf); + if (rename_ref(item->string, buf.buf, buf2.buf)) + die(_("renaming '%s' failed"), item->string); + display_progress(progress, ++refs_renamed_nr); + } + for (i = 0; i < remote_branches.nr; i++) { + struct string_list_item *item = remote_branches.items + i; + + if (!item->util) + continue; + strbuf_reset(&buf); + strbuf_addstr(&buf, item->string); + strbuf_splice(&buf, strlen("refs/remotes/"), strlen(rename.old_name), + rename.new_name, strlen(rename.new_name)); + strbuf_reset(&buf2); + strbuf_addstr(&buf2, item->util); + strbuf_splice(&buf2, strlen("refs/remotes/"), strlen(rename.old_name), + rename.new_name, strlen(rename.new_name)); + strbuf_reset(&buf3); + strbuf_addf(&buf3, "remote: renamed %s to %s", + item->string, buf.buf); + if (create_symref(buf.buf, buf2.buf, buf3.buf)) + die(_("creating '%s' failed"), buf.buf); + display_progress(progress, ++refs_renamed_nr); + } + stop_progress(&progress); + string_list_clear(&remote_branches, 1); + + handle_push_default(rename.old_name, rename.new_name); + + return 0; +} + +static int rm(int argc, const char **argv, const char *prefix) +{ + struct option options[] = { + OPT_END() + }; + struct remote *remote; + struct strbuf buf = STRBUF_INIT; + struct known_remotes known_remotes = { NULL, NULL }; + struct string_list branches = STRING_LIST_INIT_DUP; + struct string_list skipped = STRING_LIST_INIT_DUP; + struct branches_for_remote cb_data; + int i, result; + + memset(&cb_data, 0, sizeof(cb_data)); + cb_data.branches = &branches; + cb_data.skipped = &skipped; + cb_data.keep = &known_remotes; + + argc = parse_options(argc, argv, prefix, options, + builtin_remote_rm_usage, 0); + if (argc != 1) + usage_with_options(builtin_remote_rm_usage, options); + + remote = remote_get(argv[0]); + if (!remote_is_configured(remote, 1)) { + error(_("No such remote: '%s'"), argv[0]); + exit(2); + } + + known_remotes.to_delete = remote; + for_each_remote(add_known_remote, &known_remotes); + + read_branches(); + for (i = 0; i < branch_list.nr; i++) { + struct string_list_item *item = branch_list.items + i; + struct branch_info *info = item->util; + if (info->remote_name && !strcmp(info->remote_name, remote->name)) { + const char *keys[] = { "remote", "merge", NULL }, **k; + for (k = keys; *k; k++) { + strbuf_reset(&buf); + strbuf_addf(&buf, "branch.%s.%s", + item->string, *k); + result = git_config_set_gently(buf.buf, NULL); + if (result && result != CONFIG_NOTHING_SET) + die(_("could not unset '%s'"), buf.buf); + } + } + if (info->push_remote_name && !strcmp(info->push_remote_name, remote->name)) { + strbuf_reset(&buf); + strbuf_addf(&buf, "branch.%s.pushremote", item->string); + result = git_config_set_gently(buf.buf, NULL); + if (result && result != CONFIG_NOTHING_SET) + die(_("could not unset '%s'"), buf.buf); + } + } + + /* + * We cannot just pass a function to for_each_ref() which deletes + * the branches one by one, since for_each_ref() relies on cached + * refs, which are invalidated when deleting a branch. + */ + cb_data.remote = remote; + result = for_each_ref(add_branch_for_removal, &cb_data); + strbuf_release(&buf); + + if (!result) + result = delete_refs("remote: remove", &branches, REF_NO_DEREF); + string_list_clear(&branches, 0); + + if (skipped.nr) { + fprintf_ln(stderr, + Q_("Note: A branch outside the refs/remotes/ hierarchy was not removed;\n" + "to delete it, use:", + "Note: Some branches outside the refs/remotes/ hierarchy were not removed;\n" + "to delete them, use:", + skipped.nr)); + for (i = 0; i < skipped.nr; i++) + fprintf(stderr, " git branch -d %s\n", + skipped.items[i].string); + } + string_list_clear(&skipped, 0); + + if (!result) { + strbuf_addf(&buf, "remote.%s", remote->name); + if (git_config_rename_section(buf.buf, NULL) < 1) + return error(_("Could not remove config section '%s'"), buf.buf); + + handle_push_default(remote->name, NULL); + } + + return result; +} + +static void clear_push_info(void *util, const char *string UNUSED) +{ + struct push_info *info = util; + free(info->dest); + free(info); +} + +static void free_remote_ref_states(struct ref_states *states) +{ + string_list_clear(&states->new_refs, 0); + string_list_clear(&states->skipped, 0); + string_list_clear(&states->stale, 1); + string_list_clear(&states->tracked, 0); + string_list_clear(&states->heads, 0); + string_list_clear_func(&states->push, clear_push_info); +} + +static int append_ref_to_tracked_list(const char *refname, + const struct object_id *oid UNUSED, + int flags, void *cb_data) +{ + struct ref_states *states = cb_data; + struct refspec_item refspec; + + if (flags & REF_ISSYMREF) + return 0; + + memset(&refspec, 0, sizeof(refspec)); + refspec.dst = (char *)refname; + if (!remote_find_tracking(states->remote, &refspec)) + string_list_append(&states->tracked, abbrev_branch(refspec.src)); + + return 0; +} + +static int get_remote_ref_states(const char *name, + struct ref_states *states, + int query) +{ + states->remote = remote_get(name); + if (!states->remote) + return error(_("No such remote: '%s'"), name); + + read_branches(); + + if (query) { + struct transport *transport; + const struct ref *remote_refs; + + transport = transport_get(states->remote, states->remote->url_nr > 0 ? + states->remote->url[0] : NULL); + remote_refs = transport_get_remote_refs(transport, NULL); + + states->queried = 1; + if (query & GET_REF_STATES) + get_ref_states(remote_refs, states); + if (query & GET_HEAD_NAMES) + get_head_names(remote_refs, states); + if (query & GET_PUSH_REF_STATES) + get_push_ref_states(remote_refs, states); + transport_disconnect(transport); + } else { + for_each_ref(append_ref_to_tracked_list, states); + string_list_sort(&states->tracked); + get_push_ref_states_noquery(states); + } + + return 0; +} + +struct show_info { + struct string_list list; + struct ref_states states; + int width, width2; + int any_rebase; +}; + +#define SHOW_INFO_INIT { \ + .list = STRING_LIST_INIT_DUP, \ + .states = REF_STATES_INIT, \ +} + +static int add_remote_to_show_info(struct string_list_item *item, void *cb_data) +{ + struct show_info *info = cb_data; + int n = strlen(item->string); + if (n > info->width) + info->width = n; + string_list_insert(&info->list, item->string); + return 0; +} + +static int show_remote_info_item(struct string_list_item *item, void *cb_data) +{ + struct show_info *info = cb_data; + struct ref_states *states = &info->states; + const char *name = item->string; + + if (states->queried) { + const char *fmt = "%s"; + const char *arg = ""; + if (string_list_has_string(&states->new_refs, name)) { + fmt = _(" new (next fetch will store in remotes/%s)"); + arg = states->remote->name; + } else if (string_list_has_string(&states->tracked, name)) + arg = _(" tracked"); + else if (string_list_has_string(&states->skipped, name)) + arg = _(" skipped"); + else if (string_list_has_string(&states->stale, name)) + arg = _(" stale (use 'git remote prune' to remove)"); + else + arg = _(" ???"); + printf(" %-*s", info->width, name); + printf(fmt, arg); + printf("\n"); + } else + printf(" %s\n", name); + + return 0; +} + +static int add_local_to_show_info(struct string_list_item *branch_item, void *cb_data) +{ + struct show_info *show_info = cb_data; + struct ref_states *states = &show_info->states; + struct branch_info *branch_info = branch_item->util; + struct string_list_item *item; + int n; + + if (!branch_info->merge.nr || !branch_info->remote_name || + strcmp(states->remote->name, branch_info->remote_name)) + return 0; + if ((n = strlen(branch_item->string)) > show_info->width) + show_info->width = n; + if (branch_info->rebase >= REBASE_TRUE) + show_info->any_rebase = 1; + + item = string_list_insert(&show_info->list, branch_item->string); + item->util = branch_info; + + return 0; +} + +static int show_local_info_item(struct string_list_item *item, void *cb_data) +{ + struct show_info *show_info = cb_data; + struct branch_info *branch_info = item->util; + struct string_list *merge = &branch_info->merge; + int width = show_info->width + 4; + int i; + + if (branch_info->rebase >= REBASE_TRUE && branch_info->merge.nr > 1) { + error(_("invalid branch.%s.merge; cannot rebase onto > 1 branch"), + item->string); + return 0; + } + + printf(" %-*s ", show_info->width, item->string); + if (branch_info->rebase >= REBASE_TRUE) { + const char *msg; + if (branch_info->rebase == REBASE_INTERACTIVE) + msg = _("rebases interactively onto remote %s"); + else if (branch_info->rebase == REBASE_MERGES) + msg = _("rebases interactively (with merges) onto " + "remote %s"); + else + msg = _("rebases onto remote %s"); + printf_ln(msg, merge->items[0].string); + return 0; + } else if (show_info->any_rebase) { + printf_ln(_(" merges with remote %s"), merge->items[0].string); + width++; + } else { + printf_ln(_("merges with remote %s"), merge->items[0].string); + } + for (i = 1; i < merge->nr; i++) + printf(_("%-*s and with remote %s\n"), width, "", + merge->items[i].string); + + return 0; +} + +static int add_push_to_show_info(struct string_list_item *push_item, void *cb_data) +{ + struct show_info *show_info = cb_data; + struct push_info *push_info = push_item->util; + struct string_list_item *item; + int n; + if ((n = strlen(push_item->string)) > show_info->width) + show_info->width = n; + if ((n = strlen(push_info->dest)) > show_info->width2) + show_info->width2 = n; + item = string_list_append(&show_info->list, push_item->string); + item->util = push_item->util; + return 0; +} + +/* + * Sorting comparison for a string list that has push_info + * structs in its util field + */ +static int cmp_string_with_push(const void *va, const void *vb) +{ + const struct string_list_item *a = va; + const struct string_list_item *b = vb; + const struct push_info *a_push = a->util; + const struct push_info *b_push = b->util; + int cmp = strcmp(a->string, b->string); + return cmp ? cmp : strcmp(a_push->dest, b_push->dest); +} + +static int show_push_info_item(struct string_list_item *item, void *cb_data) +{ + struct show_info *show_info = cb_data; + struct push_info *push_info = item->util; + const char *src = item->string, *status = NULL; + + switch (push_info->status) { + case PUSH_STATUS_CREATE: + status = _("create"); + break; + case PUSH_STATUS_DELETE: + status = _("delete"); + src = _("(none)"); + break; + case PUSH_STATUS_UPTODATE: + status = _("up to date"); + break; + case PUSH_STATUS_FASTFORWARD: + status = _("fast-forwardable"); + break; + case PUSH_STATUS_OUTOFDATE: + status = _("local out of date"); + break; + case PUSH_STATUS_NOTQUERIED: + break; + } + if (status) { + if (push_info->forced) + printf_ln(_(" %-*s forces to %-*s (%s)"), show_info->width, src, + show_info->width2, push_info->dest, status); + else + printf_ln(_(" %-*s pushes to %-*s (%s)"), show_info->width, src, + show_info->width2, push_info->dest, status); + } else { + if (push_info->forced) + printf_ln(_(" %-*s forces to %s"), show_info->width, src, + push_info->dest); + else + printf_ln(_(" %-*s pushes to %s"), show_info->width, src, + push_info->dest); + } + return 0; +} + +static int get_one_entry(struct remote *remote, void *priv) +{ + struct string_list *list = priv; + struct strbuf remote_info_buf = STRBUF_INIT; + const char **url; + int i, url_nr; + + if (remote->url_nr > 0) { + struct strbuf promisor_config = STRBUF_INIT; + const char *partial_clone_filter = NULL; + + strbuf_addf(&promisor_config, "remote.%s.partialclonefilter", remote->name); + strbuf_addf(&remote_info_buf, "%s (fetch)", remote->url[0]); + if (!git_config_get_string_tmp(promisor_config.buf, &partial_clone_filter)) + strbuf_addf(&remote_info_buf, " [%s]", partial_clone_filter); + + strbuf_release(&promisor_config); + string_list_append(list, remote->name)->util = + strbuf_detach(&remote_info_buf, NULL); + } else + string_list_append(list, remote->name)->util = NULL; + if (remote->pushurl_nr) { + url = remote->pushurl; + url_nr = remote->pushurl_nr; + } else { + url = remote->url; + url_nr = remote->url_nr; + } + for (i = 0; i < url_nr; i++) + { + strbuf_addf(&remote_info_buf, "%s (push)", url[i]); + string_list_append(list, remote->name)->util = + strbuf_detach(&remote_info_buf, NULL); + } + + return 0; +} + +static int show_all(void) +{ + struct string_list list = STRING_LIST_INIT_DUP; + int result; + + result = for_each_remote(get_one_entry, &list); + + if (!result) { + int i; + + string_list_sort(&list); + for (i = 0; i < list.nr; i++) { + struct string_list_item *item = list.items + i; + if (verbose) + printf("%s\t%s\n", item->string, + item->util ? (const char *)item->util : ""); + else { + if (i && !strcmp((item - 1)->string, item->string)) + continue; + printf("%s\n", item->string); + } + } + } + string_list_clear(&list, 1); + return result; +} + +static int show(int argc, const char **argv, const char *prefix) +{ + int no_query = 0, result = 0, query_flag = 0; + struct option options[] = { + OPT_BOOL('n', NULL, &no_query, N_("do not query remotes")), + OPT_END() + }; + struct show_info info = SHOW_INFO_INIT; + + argc = parse_options(argc, argv, prefix, options, + builtin_remote_show_usage, + 0); + + if (argc < 1) + return show_all(); + + if (!no_query) + query_flag = (GET_REF_STATES | GET_HEAD_NAMES | GET_PUSH_REF_STATES); + + for (; argc; argc--, argv++) { + int i; + const char **url; + int url_nr; + + get_remote_ref_states(*argv, &info.states, query_flag); + + printf_ln(_("* remote %s"), *argv); + printf_ln(_(" Fetch URL: %s"), info.states.remote->url_nr > 0 ? + info.states.remote->url[0] : _("(no URL)")); + if (info.states.remote->pushurl_nr) { + url = info.states.remote->pushurl; + url_nr = info.states.remote->pushurl_nr; + } else { + url = info.states.remote->url; + url_nr = info.states.remote->url_nr; + } + for (i = 0; i < url_nr; i++) + /* + * TRANSLATORS: the colon ':' should align + * with the one in " Fetch URL: %s" + * translation. + */ + printf_ln(_(" Push URL: %s"), url[i]); + if (!i) + printf_ln(_(" Push URL: %s"), _("(no URL)")); + if (no_query) + printf_ln(_(" HEAD branch: %s"), _("(not queried)")); + else if (!info.states.heads.nr) + printf_ln(_(" HEAD branch: %s"), _("(unknown)")); + else if (info.states.heads.nr == 1) + printf_ln(_(" HEAD branch: %s"), info.states.heads.items[0].string); + else { + printf(_(" HEAD branch (remote HEAD is ambiguous," + " may be one of the following):\n")); + for (i = 0; i < info.states.heads.nr; i++) + printf(" %s\n", info.states.heads.items[i].string); + } + + /* remote branch info */ + info.width = 0; + for_each_string_list(&info.states.new_refs, add_remote_to_show_info, &info); + for_each_string_list(&info.states.skipped, add_remote_to_show_info, &info); + for_each_string_list(&info.states.tracked, add_remote_to_show_info, &info); + for_each_string_list(&info.states.stale, add_remote_to_show_info, &info); + if (info.list.nr) + printf_ln(Q_(" Remote branch:%s", + " Remote branches:%s", + info.list.nr), + no_query ? _(" (status not queried)") : ""); + for_each_string_list(&info.list, show_remote_info_item, &info); + string_list_clear(&info.list, 0); + + /* git pull info */ + info.width = 0; + info.any_rebase = 0; + for_each_string_list(&branch_list, add_local_to_show_info, &info); + if (info.list.nr) + printf_ln(Q_(" Local branch configured for 'git pull':", + " Local branches configured for 'git pull':", + info.list.nr)); + for_each_string_list(&info.list, show_local_info_item, &info); + string_list_clear(&info.list, 0); + + /* git push info */ + if (info.states.remote->mirror) + printf_ln(_(" Local refs will be mirrored by 'git push'")); + + info.width = info.width2 = 0; + for_each_string_list(&info.states.push, add_push_to_show_info, &info); + QSORT(info.list.items, info.list.nr, cmp_string_with_push); + if (info.list.nr) + printf_ln(Q_(" Local ref configured for 'git push'%s:", + " Local refs configured for 'git push'%s:", + info.list.nr), + no_query ? _(" (status not queried)") : ""); + for_each_string_list(&info.list, show_push_info_item, &info); + string_list_clear(&info.list, 0); + + free_remote_ref_states(&info.states); + } + + return result; +} + +static int set_head(int argc, const char **argv, const char *prefix) +{ + int i, opt_a = 0, opt_d = 0, result = 0; + struct strbuf buf = STRBUF_INIT, buf2 = STRBUF_INIT; + char *head_name = NULL; + + struct option options[] = { + OPT_BOOL('a', "auto", &opt_a, + N_("set refs/remotes/<name>/HEAD according to remote")), + OPT_BOOL('d', "delete", &opt_d, + N_("delete refs/remotes/<name>/HEAD")), + OPT_END() + }; + argc = parse_options(argc, argv, prefix, options, + builtin_remote_sethead_usage, 0); + if (argc) + strbuf_addf(&buf, "refs/remotes/%s/HEAD", argv[0]); + + if (!opt_a && !opt_d && argc == 2) { + head_name = xstrdup(argv[1]); + } else if (opt_a && !opt_d && argc == 1) { + struct ref_states states = REF_STATES_INIT; + get_remote_ref_states(argv[0], &states, GET_HEAD_NAMES); + if (!states.heads.nr) + result |= error(_("Cannot determine remote HEAD")); + else if (states.heads.nr > 1) { + result |= error(_("Multiple remote HEAD branches. " + "Please choose one explicitly with:")); + for (i = 0; i < states.heads.nr; i++) + fprintf(stderr, " git remote set-head %s %s\n", + argv[0], states.heads.items[i].string); + } else + head_name = xstrdup(states.heads.items[0].string); + free_remote_ref_states(&states); + } else if (opt_d && !opt_a && argc == 1) { + if (delete_ref(NULL, buf.buf, NULL, REF_NO_DEREF)) + result |= error(_("Could not delete %s"), buf.buf); + } else + usage_with_options(builtin_remote_sethead_usage, options); + + if (head_name) { + strbuf_addf(&buf2, "refs/remotes/%s/%s", argv[0], head_name); + /* make sure it's valid */ + if (!ref_exists(buf2.buf)) + result |= error(_("Not a valid ref: %s"), buf2.buf); + else if (create_symref(buf.buf, buf2.buf, "remote set-head")) + result |= error(_("Could not setup %s"), buf.buf); + else if (opt_a) + printf("%s/HEAD set to %s\n", argv[0], head_name); + free(head_name); + } + + strbuf_release(&buf); + strbuf_release(&buf2); + return result; +} + +static int prune_remote(const char *remote, int dry_run) +{ + int result = 0; + struct ref_states states = REF_STATES_INIT; + struct string_list refs_to_prune = STRING_LIST_INIT_NODUP; + struct string_list_item *item; + const char *dangling_msg = dry_run + ? _(" %s will become dangling!") + : _(" %s has become dangling!"); + + get_remote_ref_states(remote, &states, GET_REF_STATES); + + if (!states.stale.nr) { + free_remote_ref_states(&states); + return 0; + } + + printf_ln(_("Pruning %s"), remote); + printf_ln(_("URL: %s"), + states.remote->url_nr + ? states.remote->url[0] + : _("(no URL)")); + + for_each_string_list_item(item, &states.stale) + string_list_append(&refs_to_prune, item->util); + string_list_sort(&refs_to_prune); + + if (!dry_run) + result |= delete_refs("remote: prune", &refs_to_prune, 0); + + for_each_string_list_item(item, &states.stale) { + const char *refname = item->util; + + if (dry_run) + printf_ln(_(" * [would prune] %s"), + abbrev_ref(refname, "refs/remotes/")); + else + printf_ln(_(" * [pruned] %s"), + abbrev_ref(refname, "refs/remotes/")); + } + + warn_dangling_symrefs(stdout, dangling_msg, &refs_to_prune); + + string_list_clear(&refs_to_prune, 0); + free_remote_ref_states(&states); + return result; +} + +static int prune(int argc, const char **argv, const char *prefix) +{ + int dry_run = 0, result = 0; + struct option options[] = { + OPT__DRY_RUN(&dry_run, N_("dry run")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, + builtin_remote_prune_usage, 0); + + if (argc < 1) + usage_with_options(builtin_remote_prune_usage, options); + + for (; argc; argc--, argv++) + result |= prune_remote(*argv, dry_run); + + return result; +} + +static int get_remote_default(const char *key, const char *value UNUSED, + const struct config_context *ctx UNUSED, + void *priv) +{ + if (strcmp(key, "remotes.default") == 0) { + int *found = priv; + *found = 1; + } + return 0; +} + +static int update(int argc, const char **argv, const char *prefix) +{ + int i, prune = -1; + struct option options[] = { + OPT_BOOL('p', "prune", &prune, + N_("prune remotes after fetching")), + OPT_END() + }; + struct child_process cmd = CHILD_PROCESS_INIT; + int default_defined = 0; + + argc = parse_options(argc, argv, prefix, options, + builtin_remote_update_usage, + PARSE_OPT_KEEP_ARGV0); + + strvec_push(&cmd.args, "fetch"); + + if (prune != -1) + strvec_push(&cmd.args, prune ? "--prune" : "--no-prune"); + if (verbose) + strvec_push(&cmd.args, "-v"); + strvec_push(&cmd.args, "--multiple"); + if (argc < 2) + strvec_push(&cmd.args, "default"); + for (i = 1; i < argc; i++) + strvec_push(&cmd.args, argv[i]); + + if (strcmp(cmd.args.v[cmd.args.nr-1], "default") == 0) { + git_config(get_remote_default, &default_defined); + if (!default_defined) { + strvec_pop(&cmd.args); + strvec_push(&cmd.args, "--all"); + } + } + + cmd.git_cmd = 1; + return run_command(&cmd); +} + +static int remove_all_fetch_refspecs(const char *key) +{ + return git_config_set_multivar_gently(key, NULL, NULL, + CONFIG_FLAGS_MULTI_REPLACE); +} + +static void add_branches(struct remote *remote, const char **branches, + const char *key) +{ + const char *remotename = remote->name; + int mirror = remote->mirror; + struct strbuf refspec = STRBUF_INIT; + + for (; *branches; branches++) + add_branch(key, *branches, remotename, mirror, &refspec); + + strbuf_release(&refspec); +} + +static int set_remote_branches(const char *remotename, const char **branches, + int add_mode) +{ + struct strbuf key = STRBUF_INIT; + struct remote *remote; + + strbuf_addf(&key, "remote.%s.fetch", remotename); + + remote = remote_get(remotename); + if (!remote_is_configured(remote, 1)) { + error(_("No such remote '%s'"), remotename); + exit(2); + } + + if (!add_mode && remove_all_fetch_refspecs(key.buf)) { + strbuf_release(&key); + return 1; + } + add_branches(remote, branches, key.buf); + + strbuf_release(&key); + return 0; +} + +static int set_branches(int argc, const char **argv, const char *prefix) +{ + int add_mode = 0; + struct option options[] = { + OPT_BOOL('\0', "add", &add_mode, N_("add branch")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, + builtin_remote_setbranches_usage, 0); + if (argc == 0) { + error(_("no remote specified")); + usage_with_options(builtin_remote_setbranches_usage, options); + } + argv[argc] = NULL; + + return set_remote_branches(argv[0], argv + 1, add_mode); +} + +static int get_url(int argc, const char **argv, const char *prefix) +{ + int i, push_mode = 0, all_mode = 0; + const char *remotename = NULL; + struct remote *remote; + const char **url; + int url_nr; + struct option options[] = { + OPT_BOOL('\0', "push", &push_mode, + N_("query push URLs rather than fetch URLs")), + OPT_BOOL('\0', "all", &all_mode, + N_("return all URLs")), + OPT_END() + }; + argc = parse_options(argc, argv, prefix, options, + builtin_remote_geturl_usage, 0); + + if (argc != 1) + usage_with_options(builtin_remote_geturl_usage, options); + + remotename = argv[0]; + + remote = remote_get(remotename); + if (!remote_is_configured(remote, 1)) { + error(_("No such remote '%s'"), remotename); + exit(2); + } + + url_nr = 0; + if (push_mode) { + url = remote->pushurl; + url_nr = remote->pushurl_nr; + } + /* else fetch mode */ + + /* Use the fetch URL when no push URLs were found or requested. */ + if (!url_nr) { + url = remote->url; + url_nr = remote->url_nr; + } + + if (!url_nr) + die(_("no URLs configured for remote '%s'"), remotename); + + if (all_mode) { + for (i = 0; i < url_nr; i++) + printf_ln("%s", url[i]); + } else { + printf_ln("%s", *url); + } + + return 0; +} + +static int set_url(int argc, const char **argv, const char *prefix) +{ + int i, push_mode = 0, add_mode = 0, delete_mode = 0; + int matches = 0, negative_matches = 0; + const char *remotename = NULL; + const char *newurl = NULL; + const char *oldurl = NULL; + struct remote *remote; + regex_t old_regex; + const char **urlset; + int urlset_nr; + struct strbuf name_buf = STRBUF_INIT; + struct option options[] = { + OPT_BOOL('\0', "push", &push_mode, + N_("manipulate push URLs")), + OPT_BOOL('\0', "add", &add_mode, + N_("add URL")), + OPT_BOOL('\0', "delete", &delete_mode, + N_("delete URLs")), + OPT_END() + }; + argc = parse_options(argc, argv, prefix, options, + builtin_remote_seturl_usage, + PARSE_OPT_KEEP_ARGV0); + + if (add_mode && delete_mode) + die(_("--add --delete doesn't make sense")); + + if (argc < 3 || argc > 4 || ((add_mode || delete_mode) && argc != 3)) + usage_with_options(builtin_remote_seturl_usage, options); + + remotename = argv[1]; + newurl = argv[2]; + if (argc > 3) + oldurl = argv[3]; + + if (delete_mode) + oldurl = newurl; + + remote = remote_get(remotename); + if (!remote_is_configured(remote, 1)) { + error(_("No such remote '%s'"), remotename); + exit(2); + } + + if (push_mode) { + strbuf_addf(&name_buf, "remote.%s.pushurl", remotename); + urlset = remote->pushurl; + urlset_nr = remote->pushurl_nr; + } else { + strbuf_addf(&name_buf, "remote.%s.url", remotename); + urlset = remote->url; + urlset_nr = remote->url_nr; + } + + /* Special cases that add new entry. */ + if ((!oldurl && !delete_mode) || add_mode) { + if (add_mode) + git_config_set_multivar(name_buf.buf, newurl, + "^$", 0); + else + git_config_set(name_buf.buf, newurl); + goto out; + } + + /* Old URL specified. Demand that one matches. */ + if (regcomp(&old_regex, oldurl, REG_EXTENDED)) + die(_("Invalid old URL pattern: %s"), oldurl); + + for (i = 0; i < urlset_nr; i++) + if (!regexec(&old_regex, urlset[i], 0, NULL, 0)) + matches++; + else + negative_matches++; + if (!delete_mode && !matches) + die(_("No such URL found: %s"), oldurl); + if (delete_mode && !negative_matches && !push_mode) + die(_("Will not delete all non-push URLs")); + + regfree(&old_regex); + + if (!delete_mode) + git_config_set_multivar(name_buf.buf, newurl, oldurl, 0); + else + git_config_set_multivar(name_buf.buf, NULL, oldurl, + CONFIG_FLAGS_MULTI_REPLACE); +out: + strbuf_release(&name_buf); + return 0; +} + +int cmd_remote(int argc, const char **argv, const char *prefix) +{ + parse_opt_subcommand_fn *fn = NULL; + struct option options[] = { + OPT__VERBOSE(&verbose, N_("be verbose; must be placed before a subcommand")), + OPT_SUBCOMMAND("add", &fn, add), + OPT_SUBCOMMAND("rename", &fn, mv), + OPT_SUBCOMMAND_F("rm", &fn, rm, PARSE_OPT_NOCOMPLETE), + OPT_SUBCOMMAND("remove", &fn, rm), + OPT_SUBCOMMAND("set-head", &fn, set_head), + OPT_SUBCOMMAND("set-branches", &fn, set_branches), + OPT_SUBCOMMAND("get-url", &fn, get_url), + OPT_SUBCOMMAND("set-url", &fn, set_url), + OPT_SUBCOMMAND("show", &fn, show), + OPT_SUBCOMMAND("prune", &fn, prune), + OPT_SUBCOMMAND("update", &fn, update), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, builtin_remote_usage, + PARSE_OPT_SUBCOMMAND_OPTIONAL); + + if (fn) { + return !!fn(argc, argv, prefix); + } else { + if (argc) { + error(_("unknown subcommand: `%s'"), argv[0]); + usage_with_options(builtin_remote_usage, options); + } + return !!show_all(); + } +} diff --git a/builtin/repack.c b/builtin/repack.c new file mode 100644 index 0000000..edaee4d --- /dev/null +++ b/builtin/repack.c @@ -0,0 +1,1538 @@ +#include "builtin.h" +#include "config.h" +#include "dir.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "parse-options.h" +#include "path.h" +#include "run-command.h" +#include "server-info.h" +#include "sigchain.h" +#include "strbuf.h" +#include "string-list.h" +#include "strvec.h" +#include "midx.h" +#include "packfile.h" +#include "prune-packed.h" +#include "object-store-ll.h" +#include "promisor-remote.h" +#include "shallow.h" +#include "pack.h" +#include "pack-bitmap.h" +#include "refs.h" +#include "list-objects-filter-options.h" + +#define ALL_INTO_ONE 1 +#define LOOSEN_UNREACHABLE 2 +#define PACK_CRUFT 4 + +#define DELETE_PACK 1 +#define RETAIN_PACK 2 + +static int pack_everything; +static int delta_base_offset = 1; +static int pack_kept_objects = -1; +static int write_bitmaps = -1; +static int use_delta_islands; +static int run_update_server_info = 1; +static char *packdir, *packtmp_name, *packtmp; + +static const char *const git_repack_usage[] = { + N_("git repack [<options>]"), + NULL +}; + +static const char incremental_bitmap_conflict_error[] = N_( +"Incremental repacks are incompatible with bitmap indexes. Use\n" +"--no-write-bitmap-index or disable the pack.writeBitmaps configuration." +); + +struct pack_objects_args { + const char *window; + const char *window_memory; + const char *depth; + const char *threads; + unsigned long max_pack_size; + int no_reuse_delta; + int no_reuse_object; + int quiet; + int local; + struct list_objects_filter_options filter_options; +}; + +static int repack_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + struct pack_objects_args *cruft_po_args = cb; + if (!strcmp(var, "repack.usedeltabaseoffset")) { + delta_base_offset = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "repack.packkeptobjects")) { + pack_kept_objects = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "repack.writebitmaps") || + !strcmp(var, "pack.writebitmaps")) { + write_bitmaps = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "repack.usedeltaislands")) { + use_delta_islands = git_config_bool(var, value); + return 0; + } + if (strcmp(var, "repack.updateserverinfo") == 0) { + run_update_server_info = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "repack.cruftwindow")) + return git_config_string(&cruft_po_args->window, var, value); + if (!strcmp(var, "repack.cruftwindowmemory")) + return git_config_string(&cruft_po_args->window_memory, var, value); + if (!strcmp(var, "repack.cruftdepth")) + return git_config_string(&cruft_po_args->depth, var, value); + if (!strcmp(var, "repack.cruftthreads")) + return git_config_string(&cruft_po_args->threads, var, value); + return git_default_config(var, value, ctx, cb); +} + +struct existing_packs { + struct string_list kept_packs; + struct string_list non_kept_packs; + struct string_list cruft_packs; +}; + +#define EXISTING_PACKS_INIT { \ + .kept_packs = STRING_LIST_INIT_DUP, \ + .non_kept_packs = STRING_LIST_INIT_DUP, \ + .cruft_packs = STRING_LIST_INIT_DUP, \ +} + +static int has_existing_non_kept_packs(const struct existing_packs *existing) +{ + return existing->non_kept_packs.nr || existing->cruft_packs.nr; +} + +static void pack_mark_for_deletion(struct string_list_item *item) +{ + item->util = (void*)((uintptr_t)item->util | DELETE_PACK); +} + +static void pack_unmark_for_deletion(struct string_list_item *item) +{ + item->util = (void*)((uintptr_t)item->util & ~DELETE_PACK); +} + +static int pack_is_marked_for_deletion(struct string_list_item *item) +{ + return (uintptr_t)item->util & DELETE_PACK; +} + +static void pack_mark_retained(struct string_list_item *item) +{ + item->util = (void*)((uintptr_t)item->util | RETAIN_PACK); +} + +static int pack_is_retained(struct string_list_item *item) +{ + return (uintptr_t)item->util & RETAIN_PACK; +} + +static void mark_packs_for_deletion_1(struct string_list *names, + struct string_list *list) +{ + struct string_list_item *item; + const int hexsz = the_hash_algo->hexsz; + + for_each_string_list_item(item, list) { + char *sha1; + size_t len = strlen(item->string); + if (len < hexsz) + continue; + sha1 = item->string + len - hexsz; + + if (pack_is_retained(item)) { + pack_unmark_for_deletion(item); + } else if (!string_list_has_string(names, sha1)) { + /* + * Mark this pack for deletion, which ensures + * that this pack won't be included in a MIDX + * (if `--write-midx` was given) and that we + * will actually delete this pack (if `-d` was + * given). + */ + pack_mark_for_deletion(item); + } + } +} + +static void retain_cruft_pack(struct existing_packs *existing, + struct packed_git *cruft) +{ + struct strbuf buf = STRBUF_INIT; + struct string_list_item *item; + + strbuf_addstr(&buf, pack_basename(cruft)); + strbuf_strip_suffix(&buf, ".pack"); + + item = string_list_lookup(&existing->cruft_packs, buf.buf); + if (!item) + BUG("could not find cruft pack '%s'", pack_basename(cruft)); + + pack_mark_retained(item); + strbuf_release(&buf); +} + +static void mark_packs_for_deletion(struct existing_packs *existing, + struct string_list *names) + +{ + mark_packs_for_deletion_1(names, &existing->non_kept_packs); + mark_packs_for_deletion_1(names, &existing->cruft_packs); +} + +static void remove_redundant_pack(const char *dir_name, const char *base_name) +{ + struct strbuf buf = STRBUF_INIT; + struct multi_pack_index *m = get_local_multi_pack_index(the_repository); + strbuf_addf(&buf, "%s.pack", base_name); + if (m && midx_contains_pack(m, buf.buf)) + clear_midx_file(the_repository); + strbuf_insertf(&buf, 0, "%s/", dir_name); + unlink_pack_path(buf.buf, 1); + strbuf_release(&buf); +} + +static void remove_redundant_packs_1(struct string_list *packs) +{ + struct string_list_item *item; + for_each_string_list_item(item, packs) { + if (!pack_is_marked_for_deletion(item)) + continue; + remove_redundant_pack(packdir, item->string); + } +} + +static void remove_redundant_existing_packs(struct existing_packs *existing) +{ + remove_redundant_packs_1(&existing->non_kept_packs); + remove_redundant_packs_1(&existing->cruft_packs); +} + +static void existing_packs_release(struct existing_packs *existing) +{ + string_list_clear(&existing->kept_packs, 0); + string_list_clear(&existing->non_kept_packs, 0); + string_list_clear(&existing->cruft_packs, 0); +} + +/* + * Adds all packs hex strings (pack-$HASH) to either packs->non_kept + * or packs->kept based on whether each pack has a corresponding + * .keep file or not. Packs without a .keep file are not to be kept + * if we are going to pack everything into one file. + */ +static void collect_pack_filenames(struct existing_packs *existing, + const struct string_list *extra_keep) +{ + struct packed_git *p; + struct strbuf buf = STRBUF_INIT; + + for (p = get_all_packs(the_repository); p; p = p->next) { + int i; + const char *base; + + if (!p->pack_local) + continue; + + base = pack_basename(p); + + for (i = 0; i < extra_keep->nr; i++) + if (!fspathcmp(base, extra_keep->items[i].string)) + break; + + strbuf_reset(&buf); + strbuf_addstr(&buf, base); + strbuf_strip_suffix(&buf, ".pack"); + + if ((extra_keep->nr > 0 && i < extra_keep->nr) || p->pack_keep) + string_list_append(&existing->kept_packs, buf.buf); + else if (p->is_cruft) + string_list_append(&existing->cruft_packs, buf.buf); + else + string_list_append(&existing->non_kept_packs, buf.buf); + } + + string_list_sort(&existing->kept_packs); + string_list_sort(&existing->non_kept_packs); + string_list_sort(&existing->cruft_packs); + strbuf_release(&buf); +} + +static void prepare_pack_objects(struct child_process *cmd, + const struct pack_objects_args *args, + const char *out) +{ + strvec_push(&cmd->args, "pack-objects"); + if (args->window) + strvec_pushf(&cmd->args, "--window=%s", args->window); + if (args->window_memory) + strvec_pushf(&cmd->args, "--window-memory=%s", args->window_memory); + if (args->depth) + strvec_pushf(&cmd->args, "--depth=%s", args->depth); + if (args->threads) + strvec_pushf(&cmd->args, "--threads=%s", args->threads); + if (args->max_pack_size) + strvec_pushf(&cmd->args, "--max-pack-size=%lu", args->max_pack_size); + if (args->no_reuse_delta) + strvec_pushf(&cmd->args, "--no-reuse-delta"); + if (args->no_reuse_object) + strvec_pushf(&cmd->args, "--no-reuse-object"); + if (args->local) + strvec_push(&cmd->args, "--local"); + if (args->quiet) + strvec_push(&cmd->args, "--quiet"); + if (delta_base_offset) + strvec_push(&cmd->args, "--delta-base-offset"); + strvec_push(&cmd->args, out); + cmd->git_cmd = 1; + cmd->out = -1; +} + +/* + * Write oid to the given struct child_process's stdin, starting it first if + * necessary. + */ +static int write_oid(const struct object_id *oid, + struct packed_git *pack UNUSED, + uint32_t pos UNUSED, void *data) +{ + struct child_process *cmd = data; + + if (cmd->in == -1) { + if (start_command(cmd)) + die(_("could not start pack-objects to repack promisor objects")); + } + + xwrite(cmd->in, oid_to_hex(oid), the_hash_algo->hexsz); + xwrite(cmd->in, "\n", 1); + return 0; +} + +static struct { + const char *name; + unsigned optional:1; +} exts[] = { + {".pack"}, + {".rev", 1}, + {".mtimes", 1}, + {".bitmap", 1}, + {".promisor", 1}, + {".idx"}, +}; + +struct generated_pack_data { + struct tempfile *tempfiles[ARRAY_SIZE(exts)]; +}; + +static struct generated_pack_data *populate_pack_exts(const char *name) +{ + struct stat statbuf; + struct strbuf path = STRBUF_INIT; + struct generated_pack_data *data = xcalloc(1, sizeof(*data)); + int i; + + for (i = 0; i < ARRAY_SIZE(exts); i++) { + strbuf_reset(&path); + strbuf_addf(&path, "%s-%s%s", packtmp, name, exts[i].name); + + if (stat(path.buf, &statbuf)) + continue; + + data->tempfiles[i] = register_tempfile(path.buf); + } + + strbuf_release(&path); + return data; +} + +static int has_pack_ext(const struct generated_pack_data *data, + const char *ext) +{ + int i; + for (i = 0; i < ARRAY_SIZE(exts); i++) { + if (strcmp(exts[i].name, ext)) + continue; + return !!data->tempfiles[i]; + } + BUG("unknown pack extension: '%s'", ext); +} + +static void repack_promisor_objects(const struct pack_objects_args *args, + struct string_list *names) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + FILE *out; + struct strbuf line = STRBUF_INIT; + + prepare_pack_objects(&cmd, args, packtmp); + cmd.in = -1; + + /* + * NEEDSWORK: Giving pack-objects only the OIDs without any ordering + * hints may result in suboptimal deltas in the resulting pack. See if + * the OIDs can be sent with fake paths such that pack-objects can use a + * {type -> existing pack order} ordering when computing deltas instead + * of a {type -> size} ordering, which may produce better deltas. + */ + for_each_packed_object(write_oid, &cmd, + FOR_EACH_OBJECT_PROMISOR_ONLY); + + if (cmd.in == -1) { + /* No packed objects; cmd was never started */ + child_process_clear(&cmd); + return; + } + + close(cmd.in); + + out = xfdopen(cmd.out, "r"); + while (strbuf_getline_lf(&line, out) != EOF) { + struct string_list_item *item; + char *promisor_name; + + if (line.len != the_hash_algo->hexsz) + die(_("repack: Expecting full hex object ID lines only from pack-objects.")); + item = string_list_append(names, line.buf); + + /* + * pack-objects creates the .pack and .idx files, but not the + * .promisor file. Create the .promisor file, which is empty. + * + * NEEDSWORK: fetch-pack sometimes generates non-empty + * .promisor files containing the ref names and associated + * hashes at the point of generation of the corresponding + * packfile, but this would not preserve their contents. Maybe + * concatenate the contents of all .promisor files instead of + * just creating a new empty file. + */ + promisor_name = mkpathdup("%s-%s.promisor", packtmp, + line.buf); + write_promisor_file(promisor_name, NULL, 0); + + item->util = populate_pack_exts(item->string); + + free(promisor_name); + } + fclose(out); + if (finish_command(&cmd)) + die(_("could not finish pack-objects to repack promisor objects")); +} + +struct pack_geometry { + struct packed_git **pack; + uint32_t pack_nr, pack_alloc; + uint32_t split; + + int split_factor; +}; + +static uint32_t geometry_pack_weight(struct packed_git *p) +{ + if (open_pack_index(p)) + die(_("cannot open index for %s"), p->pack_name); + return p->num_objects; +} + +static int geometry_cmp(const void *va, const void *vb) +{ + uint32_t aw = geometry_pack_weight(*(struct packed_git **)va), + bw = geometry_pack_weight(*(struct packed_git **)vb); + + if (aw < bw) + return -1; + if (aw > bw) + return 1; + return 0; +} + +static void init_pack_geometry(struct pack_geometry *geometry, + struct existing_packs *existing, + const struct pack_objects_args *args) +{ + struct packed_git *p; + struct strbuf buf = STRBUF_INIT; + + for (p = get_all_packs(the_repository); p; p = p->next) { + if (args->local && !p->pack_local) + /* + * When asked to only repack local packfiles we skip + * over any packfiles that are borrowed from alternate + * object directories. + */ + continue; + + if (!pack_kept_objects) { + /* + * Any pack that has its pack_keep bit set will + * appear in existing->kept_packs below, but + * this saves us from doing a more expensive + * check. + */ + if (p->pack_keep) + continue; + + /* + * The pack may be kept via the --keep-pack + * option; check 'existing->kept_packs' to + * determine whether to ignore it. + */ + strbuf_reset(&buf); + strbuf_addstr(&buf, pack_basename(p)); + strbuf_strip_suffix(&buf, ".pack"); + + if (string_list_has_string(&existing->kept_packs, buf.buf)) + continue; + } + if (p->is_cruft) + continue; + + ALLOC_GROW(geometry->pack, + geometry->pack_nr + 1, + geometry->pack_alloc); + + geometry->pack[geometry->pack_nr] = p; + geometry->pack_nr++; + } + + QSORT(geometry->pack, geometry->pack_nr, geometry_cmp); + strbuf_release(&buf); +} + +static void split_pack_geometry(struct pack_geometry *geometry) +{ + uint32_t i; + uint32_t split; + off_t total_size = 0; + + if (!geometry->pack_nr) { + geometry->split = geometry->pack_nr; + return; + } + + /* + * First, count the number of packs (in descending order of size) which + * already form a geometric progression. + */ + for (i = geometry->pack_nr - 1; i > 0; i--) { + struct packed_git *ours = geometry->pack[i]; + struct packed_git *prev = geometry->pack[i - 1]; + + if (unsigned_mult_overflows(geometry->split_factor, + geometry_pack_weight(prev))) + die(_("pack %s too large to consider in geometric " + "progression"), + prev->pack_name); + + if (geometry_pack_weight(ours) < + geometry->split_factor * geometry_pack_weight(prev)) + break; + } + + split = i; + + if (split) { + /* + * Move the split one to the right, since the top element in the + * last-compared pair can't be in the progression. Only do this + * when we split in the middle of the array (otherwise if we got + * to the end, then the split is in the right place). + */ + split++; + } + + /* + * Then, anything to the left of 'split' must be in a new pack. But, + * creating that new pack may cause packs in the heavy half to no longer + * form a geometric progression. + * + * Compute an expected size of the new pack, and then determine how many + * packs in the heavy half need to be joined into it (if any) to restore + * the geometric progression. + */ + for (i = 0; i < split; i++) { + struct packed_git *p = geometry->pack[i]; + + if (unsigned_add_overflows(total_size, geometry_pack_weight(p))) + die(_("pack %s too large to roll up"), p->pack_name); + total_size += geometry_pack_weight(p); + } + for (i = split; i < geometry->pack_nr; i++) { + struct packed_git *ours = geometry->pack[i]; + + if (unsigned_mult_overflows(geometry->split_factor, + total_size)) + die(_("pack %s too large to roll up"), ours->pack_name); + + if (geometry_pack_weight(ours) < + geometry->split_factor * total_size) { + if (unsigned_add_overflows(total_size, + geometry_pack_weight(ours))) + die(_("pack %s too large to roll up"), + ours->pack_name); + + split++; + total_size += geometry_pack_weight(ours); + } else + break; + } + + geometry->split = split; +} + +static struct packed_git *get_preferred_pack(struct pack_geometry *geometry) +{ + uint32_t i; + + if (!geometry) { + /* + * No geometry means either an all-into-one repack (in which + * case there is only one pack left and it is the largest) or an + * incremental one. + * + * If repacking incrementally, then we could check the size of + * all packs to determine which should be preferred, but leave + * this for later. + */ + return NULL; + } + if (geometry->split == geometry->pack_nr) + return NULL; + + /* + * The preferred pack is the largest pack above the split line. In + * other words, it is the largest pack that does not get rolled up in + * the geometric repack. + */ + for (i = geometry->pack_nr; i > geometry->split; i--) + /* + * A pack that is not local would never be included in a + * multi-pack index. We thus skip over any non-local packs. + */ + if (geometry->pack[i - 1]->pack_local) + return geometry->pack[i - 1]; + + return NULL; +} + +static void geometry_remove_redundant_packs(struct pack_geometry *geometry, + struct string_list *names, + struct existing_packs *existing) +{ + struct strbuf buf = STRBUF_INIT; + uint32_t i; + + for (i = 0; i < geometry->split; i++) { + struct packed_git *p = geometry->pack[i]; + if (string_list_has_string(names, hash_to_hex(p->hash))) + continue; + + strbuf_reset(&buf); + strbuf_addstr(&buf, pack_basename(p)); + strbuf_strip_suffix(&buf, ".pack"); + + if ((p->pack_keep) || + (string_list_has_string(&existing->kept_packs, buf.buf))) + continue; + + remove_redundant_pack(packdir, buf.buf); + } + + strbuf_release(&buf); +} + +static void free_pack_geometry(struct pack_geometry *geometry) +{ + if (!geometry) + return; + + free(geometry->pack); +} + +struct midx_snapshot_ref_data { + struct tempfile *f; + struct oidset seen; + int preferred; +}; + +static int midx_snapshot_ref_one(const char *refname UNUSED, + const struct object_id *oid, + int flag UNUSED, void *_data) +{ + struct midx_snapshot_ref_data *data = _data; + struct object_id peeled; + + if (!peel_iterated_oid(oid, &peeled)) + oid = &peeled; + + if (oidset_insert(&data->seen, oid)) + return 0; /* already seen */ + + if (oid_object_info(the_repository, oid, NULL) != OBJ_COMMIT) + return 0; + + fprintf(data->f->fp, "%s%s\n", data->preferred ? "+" : "", + oid_to_hex(oid)); + + return 0; +} + +static void midx_snapshot_refs(struct tempfile *f) +{ + struct midx_snapshot_ref_data data; + const struct string_list *preferred = bitmap_preferred_tips(the_repository); + + data.f = f; + data.preferred = 0; + oidset_init(&data.seen, 0); + + if (!fdopen_tempfile(f, "w")) + die(_("could not open tempfile %s for writing"), + get_tempfile_path(f)); + + if (preferred) { + struct string_list_item *item; + + data.preferred = 1; + for_each_string_list_item(item, preferred) + for_each_ref_in(item->string, midx_snapshot_ref_one, &data); + data.preferred = 0; + } + + for_each_ref(midx_snapshot_ref_one, &data); + + if (close_tempfile_gently(f)) { + int save_errno = errno; + delete_tempfile(&f); + errno = save_errno; + die_errno(_("could not close refs snapshot tempfile")); + } + + oidset_clear(&data.seen); +} + +static void midx_included_packs(struct string_list *include, + struct existing_packs *existing, + struct string_list *names, + struct pack_geometry *geometry) +{ + struct string_list_item *item; + + for_each_string_list_item(item, &existing->kept_packs) + string_list_insert(include, xstrfmt("%s.idx", item->string)); + for_each_string_list_item(item, names) + string_list_insert(include, xstrfmt("pack-%s.idx", item->string)); + if (geometry->split_factor) { + struct strbuf buf = STRBUF_INIT; + uint32_t i; + for (i = geometry->split; i < geometry->pack_nr; i++) { + struct packed_git *p = geometry->pack[i]; + + /* + * The multi-pack index never refers to packfiles part + * of an alternate object database, so we skip these. + * While git-multi-pack-index(1) would silently ignore + * them anyway, this allows us to skip executing the + * command completely when we have only non-local + * packfiles. + */ + if (!p->pack_local) + continue; + + strbuf_addstr(&buf, pack_basename(p)); + strbuf_strip_suffix(&buf, ".pack"); + strbuf_addstr(&buf, ".idx"); + + string_list_insert(include, strbuf_detach(&buf, NULL)); + } + } else { + for_each_string_list_item(item, &existing->non_kept_packs) { + if (pack_is_marked_for_deletion(item)) + continue; + string_list_insert(include, xstrfmt("%s.idx", item->string)); + } + } + + for_each_string_list_item(item, &existing->cruft_packs) { + /* + * When doing a --geometric repack, there is no need to check + * for deleted packs, since we're by definition not doing an + * ALL_INTO_ONE repack (hence no packs will be deleted). + * Otherwise we must check for and exclude any packs which are + * enqueued for deletion. + * + * So we could omit the conditional below in the --geometric + * case, but doing so is unnecessary since no packs are marked + * as pending deletion (since we only call + * `mark_packs_for_deletion()` when doing an all-into-one + * repack). + */ + if (pack_is_marked_for_deletion(item)) + continue; + string_list_insert(include, xstrfmt("%s.idx", item->string)); + } +} + +static int write_midx_included_packs(struct string_list *include, + struct pack_geometry *geometry, + struct string_list *names, + const char *refs_snapshot, + int show_progress, int write_bitmaps) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + struct string_list_item *item; + struct packed_git *preferred = get_preferred_pack(geometry); + FILE *in; + int ret; + + if (!include->nr) + return 0; + + cmd.in = -1; + cmd.git_cmd = 1; + + strvec_push(&cmd.args, "multi-pack-index"); + strvec_pushl(&cmd.args, "write", "--stdin-packs", NULL); + + if (show_progress) + strvec_push(&cmd.args, "--progress"); + else + strvec_push(&cmd.args, "--no-progress"); + + if (write_bitmaps) + strvec_push(&cmd.args, "--bitmap"); + + if (preferred) + strvec_pushf(&cmd.args, "--preferred-pack=%s", + pack_basename(preferred)); + else if (names->nr) { + /* The largest pack was repacked, meaning that either + * one or two packs exist depending on whether the + * repository has a cruft pack or not. + * + * Select the non-cruft one as preferred to encourage + * pack-reuse among packs containing reachable objects + * over unreachable ones. + * + * (Note we could write multiple packs here if + * `--max-pack-size` was given, but any one of them + * will suffice, so pick the first one.) + */ + for_each_string_list_item(item, names) { + struct generated_pack_data *data = item->util; + if (has_pack_ext(data, ".mtimes")) + continue; + + strvec_pushf(&cmd.args, "--preferred-pack=pack-%s.pack", + item->string); + break; + } + } else { + /* + * No packs were kept, and no packs were written. The + * only thing remaining are .keep packs (unless + * --pack-kept-objects was given). + * + * Set the `--preferred-pack` arbitrarily here. + */ + ; + } + + if (refs_snapshot) + strvec_pushf(&cmd.args, "--refs-snapshot=%s", refs_snapshot); + + ret = start_command(&cmd); + if (ret) + return ret; + + in = xfdopen(cmd.in, "w"); + for_each_string_list_item(item, include) + fprintf(in, "%s\n", item->string); + fclose(in); + + return finish_command(&cmd); +} + +static void remove_redundant_bitmaps(struct string_list *include, + const char *packdir) +{ + struct strbuf path = STRBUF_INIT; + struct string_list_item *item; + size_t packdir_len; + + strbuf_addstr(&path, packdir); + strbuf_addch(&path, '/'); + packdir_len = path.len; + + /* + * Remove any pack bitmaps corresponding to packs which are now + * included in the MIDX. + */ + for_each_string_list_item(item, include) { + strbuf_addstr(&path, item->string); + strbuf_strip_suffix(&path, ".idx"); + strbuf_addstr(&path, ".bitmap"); + + if (unlink(path.buf) && errno != ENOENT) + warning_errno(_("could not remove stale bitmap: %s"), + path.buf); + + strbuf_setlen(&path, packdir_len); + } + strbuf_release(&path); +} + +static int finish_pack_objects_cmd(struct child_process *cmd, + struct string_list *names, + int local) +{ + FILE *out; + struct strbuf line = STRBUF_INIT; + + out = xfdopen(cmd->out, "r"); + while (strbuf_getline_lf(&line, out) != EOF) { + struct string_list_item *item; + + if (line.len != the_hash_algo->hexsz) + die(_("repack: Expecting full hex object ID lines only " + "from pack-objects.")); + /* + * Avoid putting packs written outside of the repository in the + * list of names. + */ + if (local) { + item = string_list_append(names, line.buf); + item->util = populate_pack_exts(line.buf); + } + } + fclose(out); + + strbuf_release(&line); + + return finish_command(cmd); +} + +static int write_filtered_pack(const struct pack_objects_args *args, + const char *destination, + const char *pack_prefix, + struct existing_packs *existing, + struct string_list *names) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + struct string_list_item *item; + FILE *in; + int ret; + const char *caret; + const char *scratch; + int local = skip_prefix(destination, packdir, &scratch); + + prepare_pack_objects(&cmd, args, destination); + + strvec_push(&cmd.args, "--stdin-packs"); + + if (!pack_kept_objects) + strvec_push(&cmd.args, "--honor-pack-keep"); + for_each_string_list_item(item, &existing->kept_packs) + strvec_pushf(&cmd.args, "--keep-pack=%s", item->string); + + cmd.in = -1; + + ret = start_command(&cmd); + if (ret) + return ret; + + /* + * Here 'names' contains only the pack(s) that were just + * written, which is exactly the packs we want to keep. Also + * 'existing_kept_packs' already contains the packs in + * 'keep_pack_list'. + */ + in = xfdopen(cmd.in, "w"); + for_each_string_list_item(item, names) + fprintf(in, "^%s-%s.pack\n", pack_prefix, item->string); + for_each_string_list_item(item, &existing->non_kept_packs) + fprintf(in, "%s.pack\n", item->string); + for_each_string_list_item(item, &existing->cruft_packs) + fprintf(in, "%s.pack\n", item->string); + caret = pack_kept_objects ? "" : "^"; + for_each_string_list_item(item, &existing->kept_packs) + fprintf(in, "%s%s.pack\n", caret, item->string); + fclose(in); + + return finish_pack_objects_cmd(&cmd, names, local); +} + +static int existing_cruft_pack_cmp(const void *va, const void *vb) +{ + struct packed_git *a = *(struct packed_git **)va; + struct packed_git *b = *(struct packed_git **)vb; + + if (a->pack_size < b->pack_size) + return -1; + if (a->pack_size > b->pack_size) + return 1; + return 0; +} + +static void collapse_small_cruft_packs(FILE *in, size_t max_size, + struct existing_packs *existing) +{ + struct packed_git **existing_cruft, *p; + struct strbuf buf = STRBUF_INIT; + size_t total_size = 0; + size_t existing_cruft_nr = 0; + size_t i; + + ALLOC_ARRAY(existing_cruft, existing->cruft_packs.nr); + + for (p = get_all_packs(the_repository); p; p = p->next) { + if (!(p->is_cruft && p->pack_local)) + continue; + + strbuf_reset(&buf); + strbuf_addstr(&buf, pack_basename(p)); + strbuf_strip_suffix(&buf, ".pack"); + + if (!string_list_has_string(&existing->cruft_packs, buf.buf)) + continue; + + if (existing_cruft_nr >= existing->cruft_packs.nr) + BUG("too many cruft packs (found %"PRIuMAX", but knew " + "of %"PRIuMAX")", + (uintmax_t)existing_cruft_nr + 1, + (uintmax_t)existing->cruft_packs.nr); + existing_cruft[existing_cruft_nr++] = p; + } + + QSORT(existing_cruft, existing_cruft_nr, existing_cruft_pack_cmp); + + for (i = 0; i < existing_cruft_nr; i++) { + size_t proposed; + + p = existing_cruft[i]; + proposed = st_add(total_size, p->pack_size); + + if (proposed <= max_size) { + total_size = proposed; + fprintf(in, "-%s\n", pack_basename(p)); + } else { + retain_cruft_pack(existing, p); + fprintf(in, "%s\n", pack_basename(p)); + } + } + + for (i = 0; i < existing->non_kept_packs.nr; i++) + fprintf(in, "-%s.pack\n", + existing->non_kept_packs.items[i].string); + + strbuf_release(&buf); + free(existing_cruft); +} + +static int write_cruft_pack(const struct pack_objects_args *args, + const char *destination, + const char *pack_prefix, + const char *cruft_expiration, + struct string_list *names, + struct existing_packs *existing) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + struct string_list_item *item; + FILE *in; + int ret; + const char *scratch; + int local = skip_prefix(destination, packdir, &scratch); + + prepare_pack_objects(&cmd, args, destination); + + strvec_push(&cmd.args, "--cruft"); + if (cruft_expiration) + strvec_pushf(&cmd.args, "--cruft-expiration=%s", + cruft_expiration); + + strvec_push(&cmd.args, "--honor-pack-keep"); + strvec_push(&cmd.args, "--non-empty"); + + cmd.in = -1; + + ret = start_command(&cmd); + if (ret) + return ret; + + /* + * names has a confusing double use: it both provides the list + * of just-written new packs, and accepts the name of the cruft + * pack we are writing. + * + * By the time it is read here, it contains only the pack(s) + * that were just written, which is exactly the set of packs we + * want to consider kept. + * + * If `--expire-to` is given, the double-use served by `names` + * ensures that the pack written to `--expire-to` excludes any + * objects contained in the cruft pack. + */ + in = xfdopen(cmd.in, "w"); + for_each_string_list_item(item, names) + fprintf(in, "%s-%s.pack\n", pack_prefix, item->string); + if (args->max_pack_size && !cruft_expiration) { + collapse_small_cruft_packs(in, args->max_pack_size, existing); + } else { + for_each_string_list_item(item, &existing->non_kept_packs) + fprintf(in, "-%s.pack\n", item->string); + for_each_string_list_item(item, &existing->cruft_packs) + fprintf(in, "-%s.pack\n", item->string); + } + for_each_string_list_item(item, &existing->kept_packs) + fprintf(in, "%s.pack\n", item->string); + fclose(in); + + return finish_pack_objects_cmd(&cmd, names, local); +} + +static const char *find_pack_prefix(const char *packdir, const char *packtmp) +{ + const char *pack_prefix; + if (!skip_prefix(packtmp, packdir, &pack_prefix)) + die(_("pack prefix %s does not begin with objdir %s"), + packtmp, packdir); + if (*pack_prefix == '/') + pack_prefix++; + return pack_prefix; +} + +int cmd_repack(int argc, const char **argv, const char *prefix) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + struct string_list_item *item; + struct string_list names = STRING_LIST_INIT_DUP; + struct existing_packs existing = EXISTING_PACKS_INIT; + struct pack_geometry geometry = { 0 }; + struct tempfile *refs_snapshot = NULL; + int i, ext, ret; + int show_progress; + + /* variables to be filled by option parsing */ + int delete_redundant = 0; + const char *unpack_unreachable = NULL; + int keep_unreachable = 0; + struct string_list keep_pack_list = STRING_LIST_INIT_NODUP; + struct pack_objects_args po_args = {NULL}; + struct pack_objects_args cruft_po_args = {NULL}; + int write_midx = 0; + const char *cruft_expiration = NULL; + const char *expire_to = NULL; + const char *filter_to = NULL; + + struct option builtin_repack_options[] = { + OPT_BIT('a', NULL, &pack_everything, + N_("pack everything in a single pack"), ALL_INTO_ONE), + OPT_BIT('A', NULL, &pack_everything, + N_("same as -a, and turn unreachable objects loose"), + LOOSEN_UNREACHABLE | ALL_INTO_ONE), + OPT_BIT(0, "cruft", &pack_everything, + N_("same as -a, pack unreachable cruft objects separately"), + PACK_CRUFT), + OPT_STRING(0, "cruft-expiration", &cruft_expiration, N_("approxidate"), + N_("with --cruft, expire objects older than this")), + OPT_MAGNITUDE(0, "max-cruft-size", &cruft_po_args.max_pack_size, + N_("with --cruft, limit the size of new cruft packs")), + OPT_BOOL('d', NULL, &delete_redundant, + N_("remove redundant packs, and run git-prune-packed")), + OPT_BOOL('f', NULL, &po_args.no_reuse_delta, + N_("pass --no-reuse-delta to git-pack-objects")), + OPT_BOOL('F', NULL, &po_args.no_reuse_object, + N_("pass --no-reuse-object to git-pack-objects")), + OPT_NEGBIT('n', NULL, &run_update_server_info, + N_("do not run git-update-server-info"), 1), + OPT__QUIET(&po_args.quiet, N_("be quiet")), + OPT_BOOL('l', "local", &po_args.local, + N_("pass --local to git-pack-objects")), + OPT_BOOL('b', "write-bitmap-index", &write_bitmaps, + N_("write bitmap index")), + OPT_BOOL('i', "delta-islands", &use_delta_islands, + N_("pass --delta-islands to git-pack-objects")), + OPT_STRING(0, "unpack-unreachable", &unpack_unreachable, N_("approxidate"), + N_("with -A, do not loosen objects older than this")), + OPT_BOOL('k', "keep-unreachable", &keep_unreachable, + N_("with -a, repack unreachable objects")), + OPT_STRING(0, "window", &po_args.window, N_("n"), + N_("size of the window used for delta compression")), + OPT_STRING(0, "window-memory", &po_args.window_memory, N_("bytes"), + N_("same as the above, but limit memory size instead of entries count")), + OPT_STRING(0, "depth", &po_args.depth, N_("n"), + N_("limits the maximum delta depth")), + OPT_STRING(0, "threads", &po_args.threads, N_("n"), + N_("limits the maximum number of threads")), + OPT_MAGNITUDE(0, "max-pack-size", &po_args.max_pack_size, + N_("maximum size of each packfile")), + OPT_PARSE_LIST_OBJECTS_FILTER(&po_args.filter_options), + OPT_BOOL(0, "pack-kept-objects", &pack_kept_objects, + N_("repack objects in packs marked with .keep")), + OPT_STRING_LIST(0, "keep-pack", &keep_pack_list, N_("name"), + N_("do not repack this pack")), + OPT_INTEGER('g', "geometric", &geometry.split_factor, + N_("find a geometric progression with factor <N>")), + OPT_BOOL('m', "write-midx", &write_midx, + N_("write a multi-pack index of the resulting packs")), + OPT_STRING(0, "expire-to", &expire_to, N_("dir"), + N_("pack prefix to store a pack containing pruned objects")), + OPT_STRING(0, "filter-to", &filter_to, N_("dir"), + N_("pack prefix to store a pack containing filtered out objects")), + OPT_END() + }; + + list_objects_filter_init(&po_args.filter_options); + + git_config(repack_config, &cruft_po_args); + + argc = parse_options(argc, argv, prefix, builtin_repack_options, + git_repack_usage, 0); + + if (delete_redundant && repository_format_precious_objects) + die(_("cannot delete packs in a precious-objects repo")); + + if (keep_unreachable && + (unpack_unreachable || (pack_everything & LOOSEN_UNREACHABLE))) + die(_("options '%s' and '%s' cannot be used together"), "--keep-unreachable", "-A"); + + if (pack_everything & PACK_CRUFT) { + pack_everything |= ALL_INTO_ONE; + + if (unpack_unreachable || (pack_everything & LOOSEN_UNREACHABLE)) + die(_("options '%s' and '%s' cannot be used together"), "--cruft", "-A"); + if (keep_unreachable) + die(_("options '%s' and '%s' cannot be used together"), "--cruft", "-k"); + } + + if (write_bitmaps < 0) { + if (!write_midx && + (!(pack_everything & ALL_INTO_ONE) || !is_bare_repository())) + write_bitmaps = 0; + } else if (write_bitmaps && + git_env_bool(GIT_TEST_MULTI_PACK_INDEX, 0) && + git_env_bool(GIT_TEST_MULTI_PACK_INDEX_WRITE_BITMAP, 0)) { + write_bitmaps = 0; + } + if (pack_kept_objects < 0) + pack_kept_objects = write_bitmaps > 0 && !write_midx; + + if (write_bitmaps && !(pack_everything & ALL_INTO_ONE) && !write_midx) + die(_(incremental_bitmap_conflict_error)); + + if (write_bitmaps && po_args.local && has_alt_odb(the_repository)) { + /* + * When asked to do a local repack, but we have + * packfiles that are inherited from an alternate, then + * we cannot guarantee that the multi-pack-index would + * have full coverage of all objects. We thus disable + * writing bitmaps in that case. + */ + warning(_("disabling bitmap writing, as some objects are not being packed")); + write_bitmaps = 0; + } + + if (write_midx && write_bitmaps) { + struct strbuf path = STRBUF_INIT; + + strbuf_addf(&path, "%s/%s_XXXXXX", get_object_directory(), + "bitmap-ref-tips"); + + refs_snapshot = xmks_tempfile(path.buf); + midx_snapshot_refs(refs_snapshot); + + strbuf_release(&path); + } + + packdir = mkpathdup("%s/pack", get_object_directory()); + packtmp_name = xstrfmt(".tmp-%d-pack", (int)getpid()); + packtmp = mkpathdup("%s/%s", packdir, packtmp_name); + + collect_pack_filenames(&existing, &keep_pack_list); + + if (geometry.split_factor) { + if (pack_everything) + die(_("options '%s' and '%s' cannot be used together"), "--geometric", "-A/-a"); + init_pack_geometry(&geometry, &existing, &po_args); + split_pack_geometry(&geometry); + } + + prepare_pack_objects(&cmd, &po_args, packtmp); + + show_progress = !po_args.quiet && isatty(2); + + strvec_push(&cmd.args, "--keep-true-parents"); + if (!pack_kept_objects) + strvec_push(&cmd.args, "--honor-pack-keep"); + for (i = 0; i < keep_pack_list.nr; i++) + strvec_pushf(&cmd.args, "--keep-pack=%s", + keep_pack_list.items[i].string); + strvec_push(&cmd.args, "--non-empty"); + if (!geometry.split_factor) { + /* + * We need to grab all reachable objects, including those that + * are reachable from reflogs and the index. + * + * When repacking into a geometric progression of packs, + * however, we ask 'git pack-objects --stdin-packs', and it is + * not about packing objects based on reachability but about + * repacking all the objects in specified packs and loose ones + * (indeed, --stdin-packs is incompatible with these options). + */ + strvec_push(&cmd.args, "--all"); + strvec_push(&cmd.args, "--reflog"); + strvec_push(&cmd.args, "--indexed-objects"); + } + if (repo_has_promisor_remote(the_repository)) + strvec_push(&cmd.args, "--exclude-promisor-objects"); + if (!write_midx) { + if (write_bitmaps > 0) + strvec_push(&cmd.args, "--write-bitmap-index"); + else if (write_bitmaps < 0) + strvec_push(&cmd.args, "--write-bitmap-index-quiet"); + } + if (use_delta_islands) + strvec_push(&cmd.args, "--delta-islands"); + + if (pack_everything & ALL_INTO_ONE) { + repack_promisor_objects(&po_args, &names); + + if (has_existing_non_kept_packs(&existing) && + delete_redundant && + !(pack_everything & PACK_CRUFT)) { + for_each_string_list_item(item, &names) { + strvec_pushf(&cmd.args, "--keep-pack=%s-%s.pack", + packtmp_name, item->string); + } + if (unpack_unreachable) { + strvec_pushf(&cmd.args, + "--unpack-unreachable=%s", + unpack_unreachable); + } else if (pack_everything & LOOSEN_UNREACHABLE) { + strvec_push(&cmd.args, + "--unpack-unreachable"); + } else if (keep_unreachable) { + strvec_push(&cmd.args, "--keep-unreachable"); + strvec_push(&cmd.args, "--pack-loose-unreachable"); + } + } + } else if (geometry.split_factor) { + strvec_push(&cmd.args, "--stdin-packs"); + strvec_push(&cmd.args, "--unpacked"); + } else { + strvec_push(&cmd.args, "--unpacked"); + strvec_push(&cmd.args, "--incremental"); + } + + if (po_args.filter_options.choice) + strvec_pushf(&cmd.args, "--filter=%s", + expand_list_objects_filter_spec(&po_args.filter_options)); + else if (filter_to) + die(_("option '%s' can only be used along with '%s'"), "--filter-to", "--filter"); + + if (geometry.split_factor) + cmd.in = -1; + else + cmd.no_stdin = 1; + + ret = start_command(&cmd); + if (ret) + goto cleanup; + + if (geometry.split_factor) { + FILE *in = xfdopen(cmd.in, "w"); + /* + * The resulting pack should contain all objects in packs that + * are going to be rolled up, but exclude objects in packs which + * are being left alone. + */ + for (i = 0; i < geometry.split; i++) + fprintf(in, "%s\n", pack_basename(geometry.pack[i])); + for (i = geometry.split; i < geometry.pack_nr; i++) + fprintf(in, "^%s\n", pack_basename(geometry.pack[i])); + fclose(in); + } + + ret = finish_pack_objects_cmd(&cmd, &names, 1); + if (ret) + goto cleanup; + + if (!names.nr && !po_args.quiet) + printf_ln(_("Nothing new to pack.")); + + if (pack_everything & PACK_CRUFT) { + const char *pack_prefix = find_pack_prefix(packdir, packtmp); + + if (!cruft_po_args.window) + cruft_po_args.window = po_args.window; + if (!cruft_po_args.window_memory) + cruft_po_args.window_memory = po_args.window_memory; + if (!cruft_po_args.depth) + cruft_po_args.depth = po_args.depth; + if (!cruft_po_args.threads) + cruft_po_args.threads = po_args.threads; + if (!cruft_po_args.max_pack_size) + cruft_po_args.max_pack_size = po_args.max_pack_size; + + cruft_po_args.local = po_args.local; + cruft_po_args.quiet = po_args.quiet; + + ret = write_cruft_pack(&cruft_po_args, packtmp, pack_prefix, + cruft_expiration, &names, + &existing); + if (ret) + goto cleanup; + + if (delete_redundant && expire_to) { + /* + * If `--expire-to` is given with `-d`, it's possible + * that we're about to prune some objects. With cruft + * packs, pruning is implicit: any objects from existing + * packs that weren't picked up by new packs are removed + * when their packs are deleted. + * + * Generate an additional cruft pack, with one twist: + * `names` now includes the name of the cruft pack + * written in the previous step. So the contents of + * _this_ cruft pack exclude everything contained in the + * existing cruft pack (that is, all of the unreachable + * objects which are no older than + * `--cruft-expiration`). + * + * To make this work, cruft_expiration must become NULL + * so that this cruft pack doesn't actually prune any + * objects. If it were non-NULL, this call would always + * generate an empty pack (since every object not in the + * cruft pack generated above will have an mtime older + * than the expiration). + */ + ret = write_cruft_pack(&cruft_po_args, expire_to, + pack_prefix, + NULL, + &names, + &existing); + if (ret) + goto cleanup; + } + } + + if (po_args.filter_options.choice) { + if (!filter_to) + filter_to = packtmp; + + ret = write_filtered_pack(&po_args, + filter_to, + find_pack_prefix(packdir, packtmp), + &existing, + &names); + if (ret) + goto cleanup; + } + + string_list_sort(&names); + + close_object_store(the_repository->objects); + + /* + * Ok we have prepared all new packfiles. + */ + for_each_string_list_item(item, &names) { + struct generated_pack_data *data = item->util; + + for (ext = 0; ext < ARRAY_SIZE(exts); ext++) { + char *fname; + + fname = mkpathdup("%s/pack-%s%s", + packdir, item->string, exts[ext].name); + + if (data->tempfiles[ext]) { + const char *fname_old = get_tempfile_path(data->tempfiles[ext]); + struct stat statbuffer; + + if (!stat(fname_old, &statbuffer)) { + statbuffer.st_mode &= ~(S_IWUSR | S_IWGRP | S_IWOTH); + chmod(fname_old, statbuffer.st_mode); + } + + if (rename_tempfile(&data->tempfiles[ext], fname)) + die_errno(_("renaming pack to '%s' failed"), fname); + } else if (!exts[ext].optional) + die(_("pack-objects did not write a '%s' file for pack %s-%s"), + exts[ext].name, packtmp, item->string); + else if (unlink(fname) < 0 && errno != ENOENT) + die_errno(_("could not unlink: %s"), fname); + + free(fname); + } + } + /* End of pack replacement. */ + + if (delete_redundant && pack_everything & ALL_INTO_ONE) + mark_packs_for_deletion(&existing, &names); + + if (write_midx) { + struct string_list include = STRING_LIST_INIT_NODUP; + midx_included_packs(&include, &existing, &names, &geometry); + + ret = write_midx_included_packs(&include, &geometry, &names, + refs_snapshot ? get_tempfile_path(refs_snapshot) : NULL, + show_progress, write_bitmaps > 0); + + if (!ret && write_bitmaps) + remove_redundant_bitmaps(&include, packdir); + + string_list_clear(&include, 0); + + if (ret) + goto cleanup; + } + + reprepare_packed_git(the_repository); + + if (delete_redundant) { + int opts = 0; + remove_redundant_existing_packs(&existing); + + if (geometry.split_factor) + geometry_remove_redundant_packs(&geometry, &names, + &existing); + if (show_progress) + opts |= PRUNE_PACKED_VERBOSE; + prune_packed_objects(opts); + + if (!keep_unreachable && + (!(pack_everything & LOOSEN_UNREACHABLE) || + unpack_unreachable) && + is_repository_shallow(the_repository)) + prune_shallow(PRUNE_QUICK); + } + + if (run_update_server_info) + update_server_info(0); + + if (git_env_bool(GIT_TEST_MULTI_PACK_INDEX, 0)) { + unsigned flags = 0; + if (git_env_bool(GIT_TEST_MULTI_PACK_INDEX_WRITE_BITMAP, 0)) + flags |= MIDX_WRITE_BITMAP | MIDX_WRITE_REV_INDEX; + write_midx_file(get_object_directory(), NULL, NULL, flags); + } + +cleanup: + string_list_clear(&names, 1); + existing_packs_release(&existing); + free_pack_geometry(&geometry); + list_objects_filter_release(&po_args.filter_options); + + return ret; +} diff --git a/builtin/replace.c b/builtin/replace.c new file mode 100644 index 0000000..da59600 --- /dev/null +++ b/builtin/replace.c @@ -0,0 +1,634 @@ +/* + * Builtin "git replace" + * + * Copyright (c) 2008 Christian Couder <chriscool@tuxfamily.org> + * + * Based on builtin/tag.c by Kristian Høgsberg <krh@redhat.com> + * and Carlos Rica <jasampler@gmail.com> that was itself based on + * git-tag.sh and mktag.c by Linus Torvalds. + */ + +#include "builtin.h" +#include "config.h" +#include "editor.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "refs.h" +#include "parse-options.h" +#include "path.h" +#include "run-command.h" +#include "object-file.h" +#include "object-name.h" +#include "object-store-ll.h" +#include "replace-object.h" +#include "repository.h" +#include "tag.h" +#include "wildmatch.h" + +static const char * const git_replace_usage[] = { + N_("git replace [-f] <object> <replacement>"), + N_("git replace [-f] --edit <object>"), + N_("git replace [-f] --graft <commit> [<parent>...]"), + "git replace [-f] --convert-graft-file", + N_("git replace -d <object>..."), + N_("git replace [--format=<format>] [-l [<pattern>]]"), + NULL +}; + +enum replace_format { + REPLACE_FORMAT_SHORT, + REPLACE_FORMAT_MEDIUM, + REPLACE_FORMAT_LONG +}; + +struct show_data { + const char *pattern; + enum replace_format format; +}; + +static int show_reference(struct repository *r, const char *refname, + const struct object_id *oid, + int flag UNUSED, void *cb_data) +{ + struct show_data *data = cb_data; + + if (!wildmatch(data->pattern, refname, 0)) { + if (data->format == REPLACE_FORMAT_SHORT) + printf("%s\n", refname); + else if (data->format == REPLACE_FORMAT_MEDIUM) + printf("%s -> %s\n", refname, oid_to_hex(oid)); + else { /* data->format == REPLACE_FORMAT_LONG */ + struct object_id object; + enum object_type obj_type, repl_type; + + if (repo_get_oid(r, refname, &object)) + return error(_("failed to resolve '%s' as a valid ref"), refname); + + obj_type = oid_object_info(r, &object, NULL); + repl_type = oid_object_info(r, oid, NULL); + + printf("%s (%s) -> %s (%s)\n", refname, type_name(obj_type), + oid_to_hex(oid), type_name(repl_type)); + } + } + + return 0; +} + +static int list_replace_refs(const char *pattern, const char *format) +{ + struct show_data data; + + if (!pattern) + pattern = "*"; + data.pattern = pattern; + + if (format == NULL || *format == '\0' || !strcmp(format, "short")) + data.format = REPLACE_FORMAT_SHORT; + else if (!strcmp(format, "medium")) + data.format = REPLACE_FORMAT_MEDIUM; + else if (!strcmp(format, "long")) + data.format = REPLACE_FORMAT_LONG; + /* + * Please update _git_replace() in git-completion.bash when + * you add new format + */ + else + return error(_("invalid replace format '%s'\n" + "valid formats are 'short', 'medium' and 'long'"), + format); + + for_each_replace_ref(the_repository, show_reference, (void *)&data); + + return 0; +} + +typedef int (*each_replace_name_fn)(const char *name, const char *ref, + const struct object_id *oid); + +static int for_each_replace_name(const char **argv, each_replace_name_fn fn) +{ + const char **p, *full_hex; + struct strbuf ref = STRBUF_INIT; + size_t base_len; + int had_error = 0; + struct object_id oid; + const char *git_replace_ref_base = ref_namespace[NAMESPACE_REPLACE].ref; + + strbuf_addstr(&ref, git_replace_ref_base); + base_len = ref.len; + + for (p = argv; *p; p++) { + if (repo_get_oid(the_repository, *p, &oid)) { + error("failed to resolve '%s' as a valid ref", *p); + had_error = 1; + continue; + } + + strbuf_setlen(&ref, base_len); + strbuf_addstr(&ref, oid_to_hex(&oid)); + full_hex = ref.buf + base_len; + + if (read_ref(ref.buf, &oid)) { + error(_("replace ref '%s' not found"), full_hex); + had_error = 1; + continue; + } + if (fn(full_hex, ref.buf, &oid)) + had_error = 1; + } + strbuf_release(&ref); + return had_error; +} + +static int delete_replace_ref(const char *name, const char *ref, + const struct object_id *oid) +{ + if (delete_ref(NULL, ref, oid, 0)) + return 1; + printf_ln(_("Deleted replace ref '%s'"), name); + return 0; +} + +static int check_ref_valid(struct object_id *object, + struct object_id *prev, + struct strbuf *ref, + int force) +{ + const char *git_replace_ref_base = ref_namespace[NAMESPACE_REPLACE].ref; + + strbuf_reset(ref); + strbuf_addf(ref, "%s%s", git_replace_ref_base, oid_to_hex(object)); + if (check_refname_format(ref->buf, 0)) + return error(_("'%s' is not a valid ref name"), ref->buf); + + if (read_ref(ref->buf, prev)) + oidclr(prev); + else if (!force) + return error(_("replace ref '%s' already exists"), ref->buf); + return 0; +} + +static int replace_object_oid(const char *object_ref, + struct object_id *object, + const char *replace_ref, + struct object_id *repl, + int force) +{ + struct object_id prev; + enum object_type obj_type, repl_type; + struct strbuf ref = STRBUF_INIT; + struct ref_transaction *transaction; + struct strbuf err = STRBUF_INIT; + int res = 0; + + obj_type = oid_object_info(the_repository, object, NULL); + repl_type = oid_object_info(the_repository, repl, NULL); + if (!force && obj_type != repl_type) + return error(_("Objects must be of the same type.\n" + "'%s' points to a replaced object of type '%s'\n" + "while '%s' points to a replacement object of " + "type '%s'."), + object_ref, type_name(obj_type), + replace_ref, type_name(repl_type)); + + if (check_ref_valid(object, &prev, &ref, force)) { + strbuf_release(&ref); + return -1; + } + + transaction = ref_transaction_begin(&err); + if (!transaction || + ref_transaction_update(transaction, ref.buf, repl, &prev, + 0, NULL, &err) || + ref_transaction_commit(transaction, &err)) + res = error("%s", err.buf); + + ref_transaction_free(transaction); + strbuf_release(&ref); + return res; +} + +static int replace_object(const char *object_ref, const char *replace_ref, int force) +{ + struct object_id object, repl; + + if (repo_get_oid(the_repository, object_ref, &object)) + return error(_("failed to resolve '%s' as a valid ref"), + object_ref); + if (repo_get_oid(the_repository, replace_ref, &repl)) + return error(_("failed to resolve '%s' as a valid ref"), + replace_ref); + + return replace_object_oid(object_ref, &object, replace_ref, &repl, force); +} + +/* + * Write the contents of the object named by "sha1" to the file "filename". + * If "raw" is true, then the object's raw contents are printed according to + * "type". Otherwise, we pretty-print the contents for human editing. + */ +static int export_object(const struct object_id *oid, enum object_type type, + int raw, const char *filename) +{ + struct child_process cmd = CHILD_PROCESS_INIT; + int fd; + + fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666); + if (fd < 0) + return error_errno(_("unable to open %s for writing"), filename); + + strvec_push(&cmd.args, "--no-replace-objects"); + strvec_push(&cmd.args, "cat-file"); + if (raw) + strvec_push(&cmd.args, type_name(type)); + else + strvec_push(&cmd.args, "-p"); + strvec_push(&cmd.args, oid_to_hex(oid)); + cmd.git_cmd = 1; + cmd.out = fd; + + if (run_command(&cmd)) + return error(_("cat-file reported failure")); + return 0; +} + +/* + * Read a previously-exported (and possibly edited) object back from "filename", + * interpreting it as "type", and writing the result to the object database. + * The sha1 of the written object is returned via sha1. + */ +static int import_object(struct object_id *oid, enum object_type type, + int raw, const char *filename) +{ + int fd; + + fd = open(filename, O_RDONLY); + if (fd < 0) + return error_errno(_("unable to open %s for reading"), filename); + + if (!raw && type == OBJ_TREE) { + struct child_process cmd = CHILD_PROCESS_INIT; + struct strbuf result = STRBUF_INIT; + + strvec_push(&cmd.args, "mktree"); + cmd.git_cmd = 1; + cmd.in = fd; + cmd.out = -1; + + if (start_command(&cmd)) { + close(fd); + return error(_("unable to spawn mktree")); + } + + if (strbuf_read(&result, cmd.out, the_hash_algo->hexsz + 1) < 0) { + error_errno(_("unable to read from mktree")); + close(fd); + close(cmd.out); + return -1; + } + close(cmd.out); + + if (finish_command(&cmd)) { + strbuf_release(&result); + return error(_("mktree reported failure")); + } + if (get_oid_hex(result.buf, oid) < 0) { + strbuf_release(&result); + return error(_("mktree did not return an object name")); + } + + strbuf_release(&result); + } else { + struct stat st; + int flags = HASH_FORMAT_CHECK | HASH_WRITE_OBJECT; + + if (fstat(fd, &st) < 0) { + error_errno(_("unable to fstat %s"), filename); + close(fd); + return -1; + } + if (index_fd(the_repository->index, oid, fd, &st, type, NULL, flags) < 0) + return error(_("unable to write object to database")); + /* index_fd close()s fd for us */ + } + + /* + * No need to close(fd) here; both run-command and index-fd + * will have done it for us. + */ + return 0; +} + +static int edit_and_replace(const char *object_ref, int force, int raw) +{ + char *tmpfile; + enum object_type type; + struct object_id old_oid, new_oid, prev; + struct strbuf ref = STRBUF_INIT; + + if (repo_get_oid(the_repository, object_ref, &old_oid) < 0) + return error(_("not a valid object name: '%s'"), object_ref); + + type = oid_object_info(the_repository, &old_oid, NULL); + if (type < 0) + return error(_("unable to get object type for %s"), + oid_to_hex(&old_oid)); + + if (check_ref_valid(&old_oid, &prev, &ref, force)) { + strbuf_release(&ref); + return -1; + } + strbuf_release(&ref); + + tmpfile = git_pathdup("REPLACE_EDITOBJ"); + if (export_object(&old_oid, type, raw, tmpfile)) { + free(tmpfile); + return -1; + } + if (launch_editor(tmpfile, NULL, NULL) < 0) { + free(tmpfile); + return error(_("editing object file failed")); + } + if (import_object(&new_oid, type, raw, tmpfile)) { + free(tmpfile); + return -1; + } + free(tmpfile); + + if (oideq(&old_oid, &new_oid)) + return error(_("new object is the same as the old one: '%s'"), oid_to_hex(&old_oid)); + + return replace_object_oid(object_ref, &old_oid, "replacement", &new_oid, force); +} + +static int replace_parents(struct strbuf *buf, int argc, const char **argv) +{ + struct strbuf new_parents = STRBUF_INIT; + const char *parent_start, *parent_end; + int i; + const unsigned hexsz = the_hash_algo->hexsz; + + /* find existing parents */ + parent_start = buf->buf; + parent_start += hexsz + 6; /* "tree " + "hex sha1" + "\n" */ + parent_end = parent_start; + + while (starts_with(parent_end, "parent ")) + parent_end += hexsz + 8; /* "parent " + "hex sha1" + "\n" */ + + /* prepare new parents */ + for (i = 0; i < argc; i++) { + struct object_id oid; + struct commit *commit; + + if (repo_get_oid(the_repository, argv[i], &oid) < 0) { + strbuf_release(&new_parents); + return error(_("not a valid object name: '%s'"), + argv[i]); + } + commit = lookup_commit_reference(the_repository, &oid); + if (!commit) { + strbuf_release(&new_parents); + return error(_("could not parse %s as a commit"), argv[i]); + } + strbuf_addf(&new_parents, "parent %s\n", oid_to_hex(&commit->object.oid)); + } + + /* replace existing parents with new ones */ + strbuf_splice(buf, parent_start - buf->buf, parent_end - parent_start, + new_parents.buf, new_parents.len); + + strbuf_release(&new_parents); + return 0; +} + +struct check_mergetag_data { + int argc; + const char **argv; +}; + +static int check_one_mergetag(struct commit *commit UNUSED, + struct commit_extra_header *extra, + void *data) +{ + struct check_mergetag_data *mergetag_data = (struct check_mergetag_data *)data; + const char *ref = mergetag_data->argv[0]; + struct object_id tag_oid; + struct tag *tag; + int i; + + hash_object_file(the_hash_algo, extra->value, extra->len, + OBJ_TAG, &tag_oid); + tag = lookup_tag(the_repository, &tag_oid); + if (!tag) + return error(_("bad mergetag in commit '%s'"), ref); + if (parse_tag_buffer(the_repository, tag, extra->value, extra->len)) + return error(_("malformed mergetag in commit '%s'"), ref); + + /* iterate over new parents */ + for (i = 1; i < mergetag_data->argc; i++) { + struct object_id oid; + if (repo_get_oid(the_repository, mergetag_data->argv[i], &oid) < 0) + return error(_("not a valid object name: '%s'"), + mergetag_data->argv[i]); + if (oideq(get_tagged_oid(tag), &oid)) + return 0; /* found */ + } + + return error(_("original commit '%s' contains mergetag '%s' that is " + "discarded; use --edit instead of --graft"), ref, + oid_to_hex(&tag_oid)); +} + +static int check_mergetags(struct commit *commit, int argc, const char **argv) +{ + struct check_mergetag_data mergetag_data; + + mergetag_data.argc = argc; + mergetag_data.argv = argv; + return for_each_mergetag(check_one_mergetag, commit, &mergetag_data); +} + +static int create_graft(int argc, const char **argv, int force, int gentle) +{ + struct object_id old_oid, new_oid; + const char *old_ref = argv[0]; + struct commit *commit; + struct strbuf buf = STRBUF_INIT; + const char *buffer; + unsigned long size; + + if (repo_get_oid(the_repository, old_ref, &old_oid) < 0) + return error(_("not a valid object name: '%s'"), old_ref); + commit = lookup_commit_reference(the_repository, &old_oid); + if (!commit) + return error(_("could not parse %s"), old_ref); + + buffer = repo_get_commit_buffer(the_repository, commit, &size); + strbuf_add(&buf, buffer, size); + repo_unuse_commit_buffer(the_repository, commit, buffer); + + if (replace_parents(&buf, argc - 1, &argv[1]) < 0) { + strbuf_release(&buf); + return -1; + } + + if (remove_signature(&buf)) { + warning(_("the original commit '%s' has a gpg signature"), old_ref); + warning(_("the signature will be removed in the replacement commit!")); + } + + if (check_mergetags(commit, argc, argv)) { + strbuf_release(&buf); + return -1; + } + + if (write_object_file(buf.buf, buf.len, OBJ_COMMIT, &new_oid)) { + strbuf_release(&buf); + return error(_("could not write replacement commit for: '%s'"), + old_ref); + } + + strbuf_release(&buf); + + if (oideq(&commit->object.oid, &new_oid)) { + if (gentle) { + warning(_("graft for '%s' unnecessary"), + oid_to_hex(&commit->object.oid)); + return 0; + } + return error(_("new commit is the same as the old one: '%s'"), + oid_to_hex(&commit->object.oid)); + } + + return replace_object_oid(old_ref, &commit->object.oid, + "replacement", &new_oid, force); +} + +static int convert_graft_file(int force) +{ + const char *graft_file = get_graft_file(the_repository); + FILE *fp = fopen_or_warn(graft_file, "r"); + struct strbuf buf = STRBUF_INIT, err = STRBUF_INIT; + struct strvec args = STRVEC_INIT; + + if (!fp) + return -1; + + no_graft_file_deprecated_advice = 1; + while (strbuf_getline(&buf, fp) != EOF) { + if (*buf.buf == '#') + continue; + + strvec_split(&args, buf.buf); + if (args.nr && create_graft(args.nr, args.v, force, 1)) + strbuf_addf(&err, "\n\t%s", buf.buf); + strvec_clear(&args); + } + fclose(fp); + + strbuf_release(&buf); + + if (!err.len) + return unlink_or_warn(graft_file); + + warning(_("could not convert the following graft(s):\n%s"), err.buf); + strbuf_release(&err); + + return -1; +} + +int cmd_replace(int argc, const char **argv, const char *prefix) +{ + int force = 0; + int raw = 0; + const char *format = NULL; + enum { + MODE_UNSPECIFIED = 0, + MODE_LIST, + MODE_DELETE, + MODE_EDIT, + MODE_GRAFT, + MODE_CONVERT_GRAFT_FILE, + MODE_REPLACE + } cmdmode = MODE_UNSPECIFIED; + struct option options[] = { + OPT_CMDMODE('l', "list", &cmdmode, N_("list replace refs"), MODE_LIST), + OPT_CMDMODE('d', "delete", &cmdmode, N_("delete replace refs"), MODE_DELETE), + OPT_CMDMODE('e', "edit", &cmdmode, N_("edit existing object"), MODE_EDIT), + OPT_CMDMODE('g', "graft", &cmdmode, N_("change a commit's parents"), MODE_GRAFT), + OPT_CMDMODE(0, "convert-graft-file", &cmdmode, N_("convert existing graft file"), MODE_CONVERT_GRAFT_FILE), + OPT_BOOL_F('f', "force", &force, N_("replace the ref if it exists"), + PARSE_OPT_NOCOMPLETE), + OPT_BOOL(0, "raw", &raw, N_("do not pretty-print contents for --edit")), + OPT_STRING(0, "format", &format, N_("format"), N_("use this format")), + OPT_END() + }; + + disable_replace_refs(); + git_config(git_default_config, NULL); + + argc = parse_options(argc, argv, prefix, options, git_replace_usage, 0); + + if (!cmdmode) + cmdmode = argc ? MODE_REPLACE : MODE_LIST; + + if (format && cmdmode != MODE_LIST) + usage_msg_opt(_("--format cannot be used when not listing"), + git_replace_usage, options); + + if (force && + cmdmode != MODE_REPLACE && + cmdmode != MODE_EDIT && + cmdmode != MODE_GRAFT && + cmdmode != MODE_CONVERT_GRAFT_FILE) + usage_msg_opt(_("-f only makes sense when writing a replacement"), + git_replace_usage, options); + + if (raw && cmdmode != MODE_EDIT) + usage_msg_opt(_("--raw only makes sense with --edit"), + git_replace_usage, options); + + switch (cmdmode) { + case MODE_DELETE: + if (argc < 1) + usage_msg_opt(_("-d needs at least one argument"), + git_replace_usage, options); + return for_each_replace_name(argv, delete_replace_ref); + + case MODE_REPLACE: + if (argc != 2) + usage_msg_opt(_("bad number of arguments"), + git_replace_usage, options); + return replace_object(argv[0], argv[1], force); + + case MODE_EDIT: + if (argc != 1) + usage_msg_opt(_("-e needs exactly one argument"), + git_replace_usage, options); + return edit_and_replace(argv[0], force, raw); + + case MODE_GRAFT: + if (argc < 1) + usage_msg_opt(_("-g needs at least one argument"), + git_replace_usage, options); + return create_graft(argc, argv, force, 0); + + case MODE_CONVERT_GRAFT_FILE: + if (argc != 0) + usage_msg_opt(_("--convert-graft-file takes no argument"), + git_replace_usage, options); + return !!convert_graft_file(force); + + case MODE_LIST: + if (argc > 1) + usage_msg_opt(_("only one pattern can be given with -l"), + git_replace_usage, options); + return list_replace_refs(argv[0], format); + + default: + BUG("invalid cmdmode %d", (int)cmdmode); + } +} diff --git a/builtin/rerere.c b/builtin/rerere.c new file mode 100644 index 0000000..07a9d37 --- /dev/null +++ b/builtin/rerere.c @@ -0,0 +1,119 @@ +#include "builtin.h" +#include "config.h" +#include "dir.h" +#include "gettext.h" +#include "parse-options.h" +#include "repository.h" +#include "string-list.h" +#include "rerere.h" +#include "xdiff/xdiff.h" +#include "xdiff-interface.h" +#include "pathspec.h" + +static const char * const rerere_usage[] = { + N_("git rerere [clear | forget <pathspec>... | diff | status | remaining | gc]"), + NULL, +}; + +static int outf(void *dummy UNUSED, mmbuffer_t *ptr, int nbuf) +{ + int i; + for (i = 0; i < nbuf; i++) + if (write_in_full(1, ptr[i].ptr, ptr[i].size) < 0) + return -1; + return 0; +} + +static int diff_two(const char *file1, const char *label1, + const char *file2, const char *label2) +{ + xpparam_t xpp; + xdemitconf_t xecfg; + xdemitcb_t ecb = { .out_line = outf }; + mmfile_t minus, plus; + int ret; + + if (read_mmfile(&minus, file1) || read_mmfile(&plus, file2)) + return -1; + + printf("--- a/%s\n+++ b/%s\n", label1, label2); + fflush(stdout); + memset(&xpp, 0, sizeof(xpp)); + xpp.flags = 0; + memset(&xecfg, 0, sizeof(xecfg)); + xecfg.ctxlen = 3; + ret = xdi_diff(&minus, &plus, &xpp, &xecfg, &ecb); + + free(minus.ptr); + free(plus.ptr); + return ret; +} + +int cmd_rerere(int argc, const char **argv, const char *prefix) +{ + struct string_list merge_rr = STRING_LIST_INIT_DUP; + int i, autoupdate = -1, flags = 0; + + struct option options[] = { + OPT_SET_INT(0, "rerere-autoupdate", &autoupdate, + N_("register clean resolutions in index"), 1), + OPT_END(), + }; + + argc = parse_options(argc, argv, prefix, options, rerere_usage, 0); + + git_config(git_xmerge_config, NULL); + + if (autoupdate == 1) + flags = RERERE_AUTOUPDATE; + if (autoupdate == 0) + flags = RERERE_NOAUTOUPDATE; + + if (argc < 1) + return repo_rerere(the_repository, flags); + + if (!strcmp(argv[0], "forget")) { + struct pathspec pathspec; + if (argc < 2) + warning(_("'git rerere forget' without paths is deprecated")); + parse_pathspec(&pathspec, 0, PATHSPEC_PREFER_CWD, + prefix, argv + 1); + return rerere_forget(the_repository, &pathspec); + } + + if (!strcmp(argv[0], "clear")) { + rerere_clear(the_repository, &merge_rr); + } else if (!strcmp(argv[0], "gc")) + rerere_gc(the_repository, &merge_rr); + else if (!strcmp(argv[0], "status")) { + if (setup_rerere(the_repository, &merge_rr, + flags | RERERE_READONLY) < 0) + return 0; + for (i = 0; i < merge_rr.nr; i++) + printf("%s\n", merge_rr.items[i].string); + } else if (!strcmp(argv[0], "remaining")) { + rerere_remaining(the_repository, &merge_rr); + for (i = 0; i < merge_rr.nr; i++) { + if (merge_rr.items[i].util != RERERE_RESOLVED) + printf("%s\n", merge_rr.items[i].string); + else + /* prepare for later call to + * string_list_clear() */ + merge_rr.items[i].util = NULL; + } + } else if (!strcmp(argv[0], "diff")) { + if (setup_rerere(the_repository, &merge_rr, + flags | RERERE_READONLY) < 0) + return 0; + for (i = 0; i < merge_rr.nr; i++) { + const char *path = merge_rr.items[i].string; + const struct rerere_id *id = merge_rr.items[i].util; + if (diff_two(rerere_path(id, "preimage"), path, path, path)) + die(_("unable to generate diff for '%s'"), rerere_path(id, NULL)); + } + } else + usage_with_options(rerere_usage, options); + + string_list_clear(&merge_rr, 1); + return 0; +} diff --git a/builtin/reset.c b/builtin/reset.c new file mode 100644 index 0000000..4b018d2 --- /dev/null +++ b/builtin/reset.c @@ -0,0 +1,522 @@ +/* + * "git reset" builtin command + * + * Copyright (c) 2007 Carlos Rica + * + * Based on git-reset.sh, which is + * + * Copyright (c) 2005, 2006 Linus Torvalds and Junio C Hamano + */ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "advice.h" +#include "config.h" +#include "environment.h" +#include "gettext.h" +#include "hash.h" +#include "hex.h" +#include "lockfile.h" +#include "tag.h" +#include "object.h" +#include "pretty.h" +#include "run-command.h" +#include "refs.h" +#include "diff.h" +#include "diffcore.h" +#include "tree.h" +#include "branch.h" +#include "object-name.h" +#include "parse-options.h" +#include "path.h" +#include "unpack-trees.h" +#include "cache-tree.h" +#include "setup.h" +#include "sparse-index.h" +#include "submodule.h" +#include "submodule-config.h" +#include "trace.h" +#include "trace2.h" +#include "dir.h" +#include "add-interactive.h" + +#define REFRESH_INDEX_DELAY_WARNING_IN_MS (2 * 1000) + +static const char * const git_reset_usage[] = { + N_("git reset [--mixed | --soft | --hard | --merge | --keep] [-q] [<commit>]"), + N_("git reset [-q] [<tree-ish>] [--] <pathspec>..."), + N_("git reset [-q] [--pathspec-from-file [--pathspec-file-nul]] [<tree-ish>]"), + N_("git reset --patch [<tree-ish>] [--] [<pathspec>...]"), + NULL +}; + +enum reset_type { MIXED, SOFT, HARD, MERGE, KEEP, NONE }; +static const char *reset_type_names[] = { + N_("mixed"), N_("soft"), N_("hard"), N_("merge"), N_("keep"), NULL +}; + +static inline int is_merge(void) +{ + return !access(git_path_merge_head(the_repository), F_OK); +} + +static int reset_index(const char *ref, const struct object_id *oid, int reset_type, int quiet) +{ + int i, nr = 0; + struct tree_desc desc[2]; + struct tree *tree; + struct unpack_trees_options opts; + int ret = -1; + + memset(&opts, 0, sizeof(opts)); + opts.head_idx = 1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.fn = oneway_merge; + opts.merge = 1; + init_checkout_metadata(&opts.meta, ref, oid, NULL); + if (!quiet) + opts.verbose_update = 1; + switch (reset_type) { + case KEEP: + case MERGE: + opts.update = 1; + opts.preserve_ignored = 0; /* FIXME: !overwrite_ignore */ + break; + case HARD: + opts.update = 1; + opts.reset = UNPACK_RESET_OVERWRITE_UNTRACKED; + opts.skip_cache_tree_update = 1; + break; + case MIXED: + opts.reset = UNPACK_RESET_PROTECT_UNTRACKED; + opts.skip_cache_tree_update = 1; + /* but opts.update=0, so working tree not updated */ + break; + default: + BUG("invalid reset_type passed to reset_index"); + } + + repo_read_index_unmerged(the_repository); + + if (reset_type == KEEP) { + struct object_id head_oid; + if (repo_get_oid(the_repository, "HEAD", &head_oid)) + return error(_("You do not have a valid HEAD.")); + if (!fill_tree_descriptor(the_repository, desc + nr, &head_oid)) + return error(_("Failed to find tree of HEAD.")); + nr++; + opts.fn = twoway_merge; + } + + if (!fill_tree_descriptor(the_repository, desc + nr, oid)) { + error(_("Failed to find tree of %s."), oid_to_hex(oid)); + goto out; + } + nr++; + + if (unpack_trees(nr, desc, &opts)) + goto out; + + if (reset_type == MIXED || reset_type == HARD) { + tree = parse_tree_indirect(oid); + prime_cache_tree(the_repository, the_repository->index, tree); + } + + ret = 0; + +out: + for (i = 0; i < nr; i++) + free((void *)desc[i].buffer); + return ret; +} + +static void print_new_head_line(struct commit *commit) +{ + struct strbuf buf = STRBUF_INIT; + + printf(_("HEAD is now at %s"), + repo_find_unique_abbrev(the_repository, &commit->object.oid, DEFAULT_ABBREV)); + + pp_commit_easy(CMIT_FMT_ONELINE, commit, &buf); + if (buf.len > 0) + printf(" %s", buf.buf); + putchar('\n'); + strbuf_release(&buf); +} + +static void update_index_from_diff(struct diff_queue_struct *q, + struct diff_options *opt UNUSED, + void *data) +{ + int i; + int intent_to_add = *(int *)data; + + for (i = 0; i < q->nr; i++) { + int pos; + struct diff_filespec *one = q->queue[i]->one; + int is_in_reset_tree = one->mode && !is_null_oid(&one->oid); + struct cache_entry *ce; + + if (!is_in_reset_tree && !intent_to_add) { + remove_file_from_index(&the_index, one->path); + continue; + } + + ce = make_cache_entry(&the_index, one->mode, &one->oid, one->path, + 0, 0); + + /* + * If the file 1) corresponds to an existing index entry with + * skip-worktree set, or 2) does not exist in the index but is + * outside the sparse checkout definition, add a skip-worktree bit + * to the new index entry. Note that a sparse index will be expanded + * if this entry is outside the sparse cone - this is necessary + * to properly construct the reset sparse directory. + */ + pos = index_name_pos(&the_index, one->path, strlen(one->path)); + if ((pos >= 0 && ce_skip_worktree(the_index.cache[pos])) || + (pos < 0 && !path_in_sparse_checkout(one->path, &the_index))) + ce->ce_flags |= CE_SKIP_WORKTREE; + + if (!ce) + die(_("make_cache_entry failed for path '%s'"), + one->path); + if (!is_in_reset_tree) { + ce->ce_flags |= CE_INTENT_TO_ADD; + set_object_name_for_intent_to_add_entry(ce); + } + add_index_entry(&the_index, ce, + ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE); + } +} + +static int read_from_tree(const struct pathspec *pathspec, + struct object_id *tree_oid, + int intent_to_add) +{ + struct diff_options opt; + + memset(&opt, 0, sizeof(opt)); + copy_pathspec(&opt.pathspec, pathspec); + opt.output_format = DIFF_FORMAT_CALLBACK; + opt.format_callback = update_index_from_diff; + opt.format_callback_data = &intent_to_add; + opt.flags.override_submodule_config = 1; + opt.flags.recursive = 1; + opt.repo = the_repository; + opt.change = diff_change; + opt.add_remove = diff_addremove; + + if (pathspec->nr && pathspec_needs_expanded_index(&the_index, pathspec)) + ensure_full_index(&the_index); + + if (do_diff_cache(tree_oid, &opt)) + return 1; + diffcore_std(&opt); + diff_flush(&opt); + + return 0; +} + +static void set_reflog_message(struct strbuf *sb, const char *action, + const char *rev) +{ + const char *rla = getenv("GIT_REFLOG_ACTION"); + + strbuf_reset(sb); + if (rla) + strbuf_addf(sb, "%s: %s", rla, action); + else if (rev) + strbuf_addf(sb, "reset: moving to %s", rev); + else + strbuf_addf(sb, "reset: %s", action); +} + +static void die_if_unmerged_cache(int reset_type) +{ + if (is_merge() || unmerged_index(&the_index)) + die(_("Cannot do a %s reset in the middle of a merge."), + _(reset_type_names[reset_type])); + +} + +static void parse_args(struct pathspec *pathspec, + const char **argv, const char *prefix, + int patch_mode, + const char **rev_ret) +{ + const char *rev = "HEAD"; + struct object_id unused; + /* + * Possible arguments are: + * + * git reset [-opts] [<rev>] + * git reset [-opts] <tree> [<paths>...] + * git reset [-opts] <tree> -- [<paths>...] + * git reset [-opts] -- [<paths>...] + * git reset [-opts] <paths>... + * + * At this point, argv points immediately after [-opts]. + */ + + if (argv[0]) { + if (!strcmp(argv[0], "--")) { + argv++; /* reset to HEAD, possibly with paths */ + } else if (argv[1] && !strcmp(argv[1], "--")) { + rev = argv[0]; + argv += 2; + } + /* + * Otherwise, argv[0] could be either <rev> or <paths> and + * has to be unambiguous. If there is a single argument, it + * can not be a tree + */ + else if ((!argv[1] && !repo_get_oid_committish(the_repository, argv[0], &unused)) || + (argv[1] && !repo_get_oid_treeish(the_repository, argv[0], &unused))) { + /* + * Ok, argv[0] looks like a commit/tree; it should not + * be a filename. + */ + verify_non_filename(prefix, argv[0]); + rev = *argv++; + } else { + /* Otherwise we treat this as a filename */ + verify_filename(prefix, argv[0], 1); + } + } + *rev_ret = rev; + + parse_pathspec(pathspec, 0, + PATHSPEC_PREFER_FULL | + (patch_mode ? PATHSPEC_PREFIX_ORIGIN : 0), + prefix, argv); +} + +static int reset_refs(const char *rev, const struct object_id *oid) +{ + int update_ref_status; + struct strbuf msg = STRBUF_INIT; + struct object_id *orig = NULL, oid_orig, + *old_orig = NULL, oid_old_orig; + + if (!repo_get_oid(the_repository, "ORIG_HEAD", &oid_old_orig)) + old_orig = &oid_old_orig; + if (!repo_get_oid(the_repository, "HEAD", &oid_orig)) { + orig = &oid_orig; + set_reflog_message(&msg, "updating ORIG_HEAD", NULL); + update_ref(msg.buf, "ORIG_HEAD", orig, old_orig, 0, + UPDATE_REFS_MSG_ON_ERR); + } else if (old_orig) + delete_ref(NULL, "ORIG_HEAD", old_orig, 0); + set_reflog_message(&msg, "updating HEAD", rev); + update_ref_status = update_ref(msg.buf, "HEAD", oid, orig, 0, + UPDATE_REFS_MSG_ON_ERR); + strbuf_release(&msg); + return update_ref_status; +} + +static int git_reset_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + if (!strcmp(var, "submodule.recurse")) + return git_default_submodule_config(var, value, cb); + + return git_default_config(var, value, ctx, cb); +} + +int cmd_reset(int argc, const char **argv, const char *prefix) +{ + int reset_type = NONE, update_ref_status = 0, quiet = 0; + int no_refresh = 0; + int patch_mode = 0, pathspec_file_nul = 0, unborn; + const char *rev; + char *pathspec_from_file = NULL; + struct object_id oid; + struct pathspec pathspec; + int intent_to_add = 0; + const struct option options[] = { + OPT__QUIET(&quiet, N_("be quiet, only report errors")), + OPT_BOOL(0, "no-refresh", &no_refresh, + N_("skip refreshing the index after reset")), + OPT_SET_INT_F(0, "mixed", &reset_type, + N_("reset HEAD and index"), + MIXED, PARSE_OPT_NONEG), + OPT_SET_INT_F(0, "soft", &reset_type, + N_("reset only HEAD"), + SOFT, PARSE_OPT_NONEG), + OPT_SET_INT_F(0, "hard", &reset_type, + N_("reset HEAD, index and working tree"), + HARD, PARSE_OPT_NONEG), + OPT_SET_INT_F(0, "merge", &reset_type, + N_("reset HEAD, index and working tree"), + MERGE, PARSE_OPT_NONEG), + OPT_SET_INT_F(0, "keep", &reset_type, + N_("reset HEAD but keep local changes"), + KEEP, PARSE_OPT_NONEG), + OPT_CALLBACK_F(0, "recurse-submodules", NULL, + "reset", "control recursive updating of submodules", + PARSE_OPT_OPTARG, + option_parse_recurse_submodules_worktree_updater), + OPT_BOOL('p', "patch", &patch_mode, N_("select hunks interactively")), + OPT_BOOL('N', "intent-to-add", &intent_to_add, + N_("record only the fact that removed paths will be added later")), + OPT_PATHSPEC_FROM_FILE(&pathspec_from_file), + OPT_PATHSPEC_FILE_NUL(&pathspec_file_nul), + OPT_END() + }; + + git_config(git_reset_config, NULL); + + argc = parse_options(argc, argv, prefix, options, git_reset_usage, + PARSE_OPT_KEEP_DASHDASH); + parse_args(&pathspec, argv, prefix, patch_mode, &rev); + + if (pathspec_from_file) { + if (patch_mode) + die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--patch"); + + if (pathspec.nr) + die(_("'%s' and pathspec arguments cannot be used together"), "--pathspec-from-file"); + + parse_pathspec_file(&pathspec, 0, + PATHSPEC_PREFER_FULL, + prefix, pathspec_from_file, pathspec_file_nul); + } else if (pathspec_file_nul) { + die(_("the option '%s' requires '%s'"), "--pathspec-file-nul", "--pathspec-from-file"); + } + + unborn = !strcmp(rev, "HEAD") && repo_get_oid(the_repository, "HEAD", + &oid); + if (unborn) { + /* reset on unborn branch: treat as reset to empty tree */ + oidcpy(&oid, the_hash_algo->empty_tree); + } else if (!pathspec.nr && !patch_mode) { + struct commit *commit; + if (repo_get_oid_committish(the_repository, rev, &oid)) + die(_("Failed to resolve '%s' as a valid revision."), rev); + commit = lookup_commit_reference(the_repository, &oid); + if (!commit) + die(_("Could not parse object '%s'."), rev); + oidcpy(&oid, &commit->object.oid); + } else { + struct tree *tree; + if (repo_get_oid_treeish(the_repository, rev, &oid)) + die(_("Failed to resolve '%s' as a valid tree."), rev); + tree = parse_tree_indirect(&oid); + if (!tree) + die(_("Could not parse object '%s'."), rev); + oidcpy(&oid, &tree->object.oid); + } + + if (patch_mode) { + if (reset_type != NONE) + die(_("options '%s' and '%s' cannot be used together"), "--patch", "--{hard,mixed,soft}"); + trace2_cmd_mode("patch-interactive"); + update_ref_status = !!run_add_p(the_repository, ADD_P_RESET, rev, + &pathspec); + goto cleanup; + } + + /* git reset tree [--] paths... can be used to + * load chosen paths from the tree into the index without + * affecting the working tree nor HEAD. */ + if (pathspec.nr) { + if (reset_type == MIXED) + warning(_("--mixed with paths is deprecated; use 'git reset -- <paths>' instead.")); + else if (reset_type != NONE) + die(_("Cannot do %s reset with paths."), + _(reset_type_names[reset_type])); + } + if (reset_type == NONE) + reset_type = MIXED; /* by default */ + + if (pathspec.nr) + trace2_cmd_mode("path"); + else + trace2_cmd_mode(reset_type_names[reset_type]); + + if (reset_type != SOFT && (reset_type != MIXED || get_git_work_tree())) + setup_work_tree(); + + if (reset_type == MIXED && is_bare_repository()) + die(_("%s reset is not allowed in a bare repository"), + _(reset_type_names[reset_type])); + + if (intent_to_add && reset_type != MIXED) + die(_("the option '%s' requires '%s'"), "-N", "--mixed"); + + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + if (repo_read_index(the_repository) < 0) + die(_("index file corrupt")); + + /* Soft reset does not touch the index file nor the working tree + * at all, but requires them in a good order. Other resets reset + * the index file to the tree object we are switching to. */ + if (reset_type == SOFT || reset_type == KEEP) + die_if_unmerged_cache(reset_type); + + if (reset_type != SOFT) { + struct lock_file lock = LOCK_INIT; + repo_hold_locked_index(the_repository, &lock, + LOCK_DIE_ON_ERROR); + if (reset_type == MIXED) { + int flags = quiet ? REFRESH_QUIET : REFRESH_IN_PORCELAIN; + if (read_from_tree(&pathspec, &oid, intent_to_add)) { + update_ref_status = 1; + goto cleanup; + } + the_index.updated_skipworktree = 1; + if (!no_refresh && get_git_work_tree()) { + uint64_t t_begin, t_delta_in_ms; + + t_begin = getnanotime(); + refresh_index(&the_index, flags, NULL, NULL, + _("Unstaged changes after reset:")); + t_delta_in_ms = (getnanotime() - t_begin) / 1000000; + if (!quiet && advice_enabled(ADVICE_RESET_NO_REFRESH_WARNING) && t_delta_in_ms > REFRESH_INDEX_DELAY_WARNING_IN_MS) { + advise(_("It took %.2f seconds to refresh the index after reset. You can use\n" + "'--no-refresh' to avoid this."), t_delta_in_ms / 1000.0); + } + } + } else { + struct object_id dummy; + char *ref = NULL; + int err; + + repo_dwim_ref(the_repository, rev, strlen(rev), + &dummy, &ref, 0); + if (ref && !starts_with(ref, "refs/")) + FREE_AND_NULL(ref); + + err = reset_index(ref, &oid, reset_type, quiet); + if (reset_type == KEEP && !err) + err = reset_index(ref, &oid, MIXED, quiet); + if (err) + die(_("Could not reset index file to revision '%s'."), rev); + free(ref); + } + + if (write_locked_index(&the_index, &lock, COMMIT_LOCK)) + die(_("Could not write new index file.")); + } + + if (!pathspec.nr && !unborn) { + /* Any resets without paths update HEAD to the head being + * switched to, saving the previous head in ORIG_HEAD before. */ + update_ref_status = reset_refs(rev, &oid); + + if (reset_type == HARD && !update_ref_status && !quiet) + print_new_head_line(lookup_commit_reference(the_repository, &oid)); + } + if (!pathspec.nr) + remove_branch_state(the_repository, 0); + + discard_index(&the_index); + +cleanup: + clear_pathspec(&pathspec); + free(pathspec_from_file); + return update_ref_status; +} diff --git a/builtin/rev-list.c b/builtin/rev-list.c new file mode 100644 index 0000000..181353d --- /dev/null +++ b/builtin/rev-list.c @@ -0,0 +1,801 @@ +#include "builtin.h" +#include "config.h" +#include "commit.h" +#include "diff.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "revision.h" +#include "list-objects.h" +#include "list-objects-filter.h" +#include "list-objects-filter-options.h" +#include "object.h" +#include "object-name.h" +#include "object-file.h" +#include "object-store-ll.h" +#include "pack.h" +#include "pack-bitmap.h" +#include "log-tree.h" +#include "graph.h" +#include "bisect.h" +#include "progress.h" +#include "reflog-walk.h" +#include "oidset.h" +#include "packfile.h" + +static const char rev_list_usage[] = +"git rev-list [<options>] <commit>... [--] [<path>...]\n" +"\n" +" limiting output:\n" +" --max-count=<n>\n" +" --max-age=<epoch>\n" +" --min-age=<epoch>\n" +" --sparse\n" +" --no-merges\n" +" --min-parents=<n>\n" +" --no-min-parents\n" +" --max-parents=<n>\n" +" --no-max-parents\n" +" --remove-empty\n" +" --all\n" +" --branches\n" +" --tags\n" +" --remotes\n" +" --stdin\n" +" --exclude-hidden=[fetch|receive|uploadpack]\n" +" --quiet\n" +" ordering output:\n" +" --topo-order\n" +" --date-order\n" +" --reverse\n" +" formatting output:\n" +" --parents\n" +" --children\n" +" --objects | --objects-edge\n" +" --disk-usage[=human]\n" +" --unpacked\n" +" --header | --pretty\n" +" --[no-]object-names\n" +" --abbrev=<n> | --no-abbrev\n" +" --abbrev-commit\n" +" --left-right\n" +" --count\n" +" special purpose:\n" +" --bisect\n" +" --bisect-vars\n" +" --bisect-all" +; + +static struct progress *progress; +static unsigned progress_counter; + +static struct oidset omitted_objects; +static int arg_print_omitted; /* print objects omitted by filter */ + +static struct oidset missing_objects; +enum missing_action { + MA_ERROR = 0, /* fail if any missing objects are encountered */ + MA_ALLOW_ANY, /* silently allow ALL missing objects */ + MA_PRINT, /* print ALL missing objects in special section */ + MA_ALLOW_PROMISOR, /* silently allow all missing PROMISOR objects */ +}; +static enum missing_action arg_missing_action; + +/* display only the oid of each object encountered */ +static int arg_show_object_names = 1; + +#define DEFAULT_OIDSET_SIZE (16*1024) + +static int show_disk_usage; +static off_t total_disk_usage; +static int human_readable; + +static off_t get_object_disk_usage(struct object *obj) +{ + off_t size; + struct object_info oi = OBJECT_INFO_INIT; + oi.disk_sizep = &size; + if (oid_object_info_extended(the_repository, &obj->oid, &oi, 0) < 0) + die(_("unable to get disk usage of %s"), oid_to_hex(&obj->oid)); + return size; +} + +static inline void finish_object__ma(struct object *obj) +{ + /* + * Whether or not we try to dynamically fetch missing objects + * from the server, we currently DO NOT have the object. We + * can either print, allow (ignore), or conditionally allow + * (ignore) them. + */ + switch (arg_missing_action) { + case MA_ERROR: + die("missing %s object '%s'", + type_name(obj->type), oid_to_hex(&obj->oid)); + return; + + case MA_ALLOW_ANY: + return; + + case MA_PRINT: + oidset_insert(&missing_objects, &obj->oid); + return; + + case MA_ALLOW_PROMISOR: + if (is_promisor_object(&obj->oid)) + return; + die("unexpected missing %s object '%s'", + type_name(obj->type), oid_to_hex(&obj->oid)); + return; + + default: + BUG("unhandled missing_action"); + return; + } +} + +static void finish_commit(struct commit *commit) +{ + free_commit_list(commit->parents); + commit->parents = NULL; + free_commit_buffer(the_repository->parsed_objects, + commit); +} + +static void show_commit(struct commit *commit, void *data) +{ + struct rev_list_info *info = data; + struct rev_info *revs = info->revs; + + display_progress(progress, ++progress_counter); + + if (revs->do_not_die_on_missing_objects && + oidset_contains(&revs->missing_commits, &commit->object.oid)) { + finish_object__ma(&commit->object); + return; + } + + if (show_disk_usage) + total_disk_usage += get_object_disk_usage(&commit->object); + + if (info->flags & REV_LIST_QUIET) { + finish_commit(commit); + return; + } + + graph_show_commit(revs->graph); + + if (revs->count) { + if (commit->object.flags & PATCHSAME) + revs->count_same++; + else if (commit->object.flags & SYMMETRIC_LEFT) + revs->count_left++; + else + revs->count_right++; + finish_commit(commit); + return; + } + + if (info->show_timestamp) + printf("%"PRItime" ", commit->date); + if (info->header_prefix) + fputs(info->header_prefix, stdout); + + if (revs->include_header) { + if (!revs->graph) + fputs(get_revision_mark(revs, commit), stdout); + if (revs->abbrev_commit && revs->abbrev) + fputs(repo_find_unique_abbrev(the_repository, &commit->object.oid, revs->abbrev), + stdout); + else + fputs(oid_to_hex(&commit->object.oid), stdout); + } + if (revs->print_parents) { + struct commit_list *parents = commit->parents; + while (parents) { + printf(" %s", oid_to_hex(&parents->item->object.oid)); + parents = parents->next; + } + } + if (revs->children.name) { + struct commit_list *children; + + children = lookup_decoration(&revs->children, &commit->object); + while (children) { + printf(" %s", oid_to_hex(&children->item->object.oid)); + children = children->next; + } + } + show_decorations(revs, commit); + if (revs->commit_format == CMIT_FMT_ONELINE) + putchar(' '); + else if (revs->include_header) + putchar('\n'); + + if (revs->verbose_header) { + struct strbuf buf = STRBUF_INIT; + struct pretty_print_context ctx = {0}; + ctx.abbrev = revs->abbrev; + ctx.date_mode = revs->date_mode; + ctx.date_mode_explicit = revs->date_mode_explicit; + ctx.fmt = revs->commit_format; + ctx.output_encoding = get_log_output_encoding(); + ctx.color = revs->diffopt.use_color; + pretty_print_commit(&ctx, commit, &buf); + if (buf.len) { + if (revs->commit_format != CMIT_FMT_ONELINE) + graph_show_oneline(revs->graph); + + graph_show_commit_msg(revs->graph, stdout, &buf); + + /* + * Add a newline after the commit message. + * + * Usually, this newline produces a blank + * padding line between entries, in which case + * we need to add graph padding on this line. + * + * However, the commit message may not end in a + * newline. In this case the newline simply + * ends the last line of the commit message, + * and we don't need any graph output. (This + * always happens with CMIT_FMT_ONELINE, and it + * happens with CMIT_FMT_USERFORMAT when the + * format doesn't explicitly end in a newline.) + */ + if (buf.len && buf.buf[buf.len - 1] == '\n') + graph_show_padding(revs->graph); + putchar(info->hdr_termination); + } else { + /* + * If the message buffer is empty, just show + * the rest of the graph output for this + * commit. + */ + if (graph_show_remainder(revs->graph)) + putchar('\n'); + if (revs->commit_format == CMIT_FMT_ONELINE) + putchar('\n'); + } + strbuf_release(&buf); + } else { + if (graph_show_remainder(revs->graph)) + putchar('\n'); + } + maybe_flush_or_die(stdout, "stdout"); + finish_commit(commit); +} + +static int finish_object(struct object *obj, const char *name UNUSED, + void *cb_data) +{ + struct rev_list_info *info = cb_data; + if (oid_object_info_extended(the_repository, &obj->oid, NULL, 0) < 0) { + finish_object__ma(obj); + return 1; + } + if (info->revs->verify_objects && !obj->parsed && obj->type != OBJ_COMMIT) + parse_object(the_repository, &obj->oid); + return 0; +} + +static void show_object(struct object *obj, const char *name, void *cb_data) +{ + struct rev_list_info *info = cb_data; + struct rev_info *revs = info->revs; + + if (finish_object(obj, name, cb_data)) + return; + display_progress(progress, ++progress_counter); + if (show_disk_usage) + total_disk_usage += get_object_disk_usage(obj); + if (info->flags & REV_LIST_QUIET) + return; + + if (revs->count) { + /* + * The object count is always accumulated in the .count_right + * field for traversal that is not a left-right traversal, + * and cmd_rev_list() made sure that a .count request that + * wants to count non-commit objects, which is handled by + * the show_object() callback, does not ask for .left_right. + */ + revs->count_right++; + return; + } + + if (arg_show_object_names) + show_object_with_name(stdout, obj, name); + else + printf("%s\n", oid_to_hex(&obj->oid)); +} + +static void show_edge(struct commit *commit) +{ + printf("-%s\n", oid_to_hex(&commit->object.oid)); +} + +static void print_var_str(const char *var, const char *val) +{ + printf("%s='%s'\n", var, val); +} + +static void print_var_int(const char *var, int val) +{ + printf("%s=%d\n", var, val); +} + +static int show_bisect_vars(struct rev_list_info *info, int reaches, int all) +{ + int cnt, flags = info->flags; + char hex[GIT_MAX_HEXSZ + 1] = ""; + struct commit_list *tried; + struct rev_info *revs = info->revs; + + if (!revs->commits) + return 1; + + revs->commits = filter_skipped(revs->commits, &tried, + flags & BISECT_SHOW_ALL, + NULL, NULL); + + /* + * revs->commits can reach "reaches" commits among + * "all" commits. If it is good, then there are + * (all-reaches) commits left to be bisected. + * On the other hand, if it is bad, then the set + * to bisect is "reaches". + * A bisect set of size N has (N-1) commits further + * to test, as we already know one bad one. + */ + cnt = all - reaches; + if (cnt < reaches) + cnt = reaches; + + if (revs->commits) + oid_to_hex_r(hex, &revs->commits->item->object.oid); + + if (flags & BISECT_SHOW_ALL) { + traverse_commit_list(revs, show_commit, show_object, info); + printf("------\n"); + } + + print_var_str("bisect_rev", hex); + print_var_int("bisect_nr", cnt - 1); + print_var_int("bisect_good", all - reaches - 1); + print_var_int("bisect_bad", reaches - 1); + print_var_int("bisect_all", all); + print_var_int("bisect_steps", estimate_bisect_steps(all)); + + return 0; +} + +static int show_object_fast( + const struct object_id *oid, + enum object_type type UNUSED, + int exclude UNUSED, + uint32_t name_hash UNUSED, + struct packed_git *found_pack UNUSED, + off_t found_offset UNUSED) +{ + fprintf(stdout, "%s\n", oid_to_hex(oid)); + return 1; +} + +static void print_disk_usage(off_t size) +{ + struct strbuf sb = STRBUF_INIT; + if (human_readable) + strbuf_humanise_bytes(&sb, size); + else + strbuf_addf(&sb, "%"PRIuMAX, (uintmax_t)size); + puts(sb.buf); + strbuf_release(&sb); +} + +static inline int parse_missing_action_value(const char *value) +{ + if (!strcmp(value, "error")) { + arg_missing_action = MA_ERROR; + return 1; + } + + if (!strcmp(value, "allow-any")) { + arg_missing_action = MA_ALLOW_ANY; + fetch_if_missing = 0; + return 1; + } + + if (!strcmp(value, "print")) { + arg_missing_action = MA_PRINT; + fetch_if_missing = 0; + return 1; + } + + if (!strcmp(value, "allow-promisor")) { + arg_missing_action = MA_ALLOW_PROMISOR; + fetch_if_missing = 0; + return 1; + } + + return 0; +} + +static int try_bitmap_count(struct rev_info *revs, + int filter_provided_objects) +{ + uint32_t commit_count = 0, + tag_count = 0, + tree_count = 0, + blob_count = 0; + int max_count; + struct bitmap_index *bitmap_git; + + /* This function only handles counting, not general traversal. */ + if (!revs->count) + return -1; + + /* + * A bitmap result can't know left/right, etc, because we don't + * actually traverse. + */ + if (revs->left_right || revs->cherry_mark) + return -1; + + /* + * If we're counting reachable objects, we can't handle a max count of + * commits to traverse, since we don't know which objects go with which + * commit. + */ + if (revs->max_count >= 0 && + (revs->tag_objects || revs->tree_objects || revs->blob_objects)) + return -1; + + /* + * This must be saved before doing any walking, since the revision + * machinery will count it down to zero while traversing. + */ + max_count = revs->max_count; + + bitmap_git = prepare_bitmap_walk(revs, filter_provided_objects); + if (!bitmap_git) + return -1; + + count_bitmap_commit_list(bitmap_git, &commit_count, + revs->tree_objects ? &tree_count : NULL, + revs->blob_objects ? &blob_count : NULL, + revs->tag_objects ? &tag_count : NULL); + if (max_count >= 0 && max_count < commit_count) + commit_count = max_count; + + printf("%d\n", commit_count + tree_count + blob_count + tag_count); + free_bitmap_index(bitmap_git); + return 0; +} + +static int try_bitmap_traversal(struct rev_info *revs, + int filter_provided_objects) +{ + struct bitmap_index *bitmap_git; + + /* + * We can't use a bitmap result with a traversal limit, since the set + * of commits we'd get would be essentially random. + */ + if (revs->max_count >= 0) + return -1; + + bitmap_git = prepare_bitmap_walk(revs, filter_provided_objects); + if (!bitmap_git) + return -1; + + traverse_bitmap_commit_list(bitmap_git, revs, &show_object_fast); + free_bitmap_index(bitmap_git); + return 0; +} + +static int try_bitmap_disk_usage(struct rev_info *revs, + int filter_provided_objects) +{ + struct bitmap_index *bitmap_git; + off_t size_from_bitmap; + + if (!show_disk_usage) + return -1; + + bitmap_git = prepare_bitmap_walk(revs, filter_provided_objects); + if (!bitmap_git) + return -1; + + size_from_bitmap = get_disk_usage_from_bitmap(bitmap_git, revs); + print_disk_usage(size_from_bitmap); + return 0; +} + +int cmd_rev_list(int argc, const char **argv, const char *prefix) +{ + struct rev_info revs; + struct rev_list_info info; + struct setup_revision_opt s_r_opt = { + .allow_exclude_promisor_objects = 1, + }; + int i; + int bisect_list = 0; + int bisect_show_vars = 0; + int bisect_find_all = 0; + int use_bitmap_index = 0; + int filter_provided_objects = 0; + const char *show_progress = NULL; + int ret = 0; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage(rev_list_usage); + + git_config(git_default_config, NULL); + repo_init_revisions(the_repository, &revs, prefix); + revs.abbrev = DEFAULT_ABBREV; + revs.commit_format = CMIT_FMT_UNSPECIFIED; + revs.include_header = 1; + + /* + * Scan the argument list before invoking setup_revisions(), so that we + * know if fetch_if_missing needs to be set to 0. + * + * "--exclude-promisor-objects" acts as a pre-filter on missing objects + * by not crossing the boundary from realized objects to promisor + * objects. + * + * Let "--missing" to conditionally set fetch_if_missing. + */ + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (!strcmp(arg, "--exclude-promisor-objects")) { + fetch_if_missing = 0; + revs.exclude_promisor_objects = 1; + break; + } + } + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (skip_prefix(arg, "--missing=", &arg)) { + if (revs.exclude_promisor_objects) + die(_("options '%s' and '%s' cannot be used together"), "--exclude-promisor-objects", "--missing"); + if (parse_missing_action_value(arg)) + break; + } + } + + if (arg_missing_action) + revs.do_not_die_on_missing_objects = 1; + + argc = setup_revisions(argc, argv, &revs, &s_r_opt); + + memset(&info, 0, sizeof(info)); + info.revs = &revs; + if (revs.bisect) + bisect_list = 1; + + if (revs.diffopt.flags.quick) + info.flags |= REV_LIST_QUIET; + for (i = 1 ; i < argc; i++) { + const char *arg = argv[i]; + + if (!strcmp(arg, "--header")) { + revs.verbose_header = 1; + continue; + } + if (!strcmp(arg, "--timestamp")) { + info.show_timestamp = 1; + continue; + } + if (!strcmp(arg, "--bisect")) { + bisect_list = 1; + continue; + } + if (!strcmp(arg, "--bisect-all")) { + bisect_list = 1; + bisect_find_all = 1; + info.flags |= BISECT_SHOW_ALL; + revs.show_decorations = 1; + continue; + } + if (!strcmp(arg, "--bisect-vars")) { + bisect_list = 1; + bisect_show_vars = 1; + continue; + } + if (!strcmp(arg, "--use-bitmap-index")) { + use_bitmap_index = 1; + continue; + } + if (!strcmp(arg, "--test-bitmap")) { + test_bitmap_walk(&revs); + goto cleanup; + } + if (skip_prefix(arg, "--progress=", &arg)) { + show_progress = arg; + continue; + } + if (!strcmp(arg, "--filter-provided-objects")) { + filter_provided_objects = 1; + continue; + } + if (!strcmp(arg, "--filter-print-omitted")) { + arg_print_omitted = 1; + continue; + } + + if (!strcmp(arg, "--exclude-promisor-objects")) + continue; /* already handled above */ + if (skip_prefix(arg, "--missing=", &arg)) + continue; /* already handled above */ + + if (!strcmp(arg, ("--no-object-names"))) { + arg_show_object_names = 0; + continue; + } + + if (!strcmp(arg, ("--object-names"))) { + arg_show_object_names = 1; + continue; + } + + if (!strcmp(arg, ("--commit-header"))) { + revs.include_header = 1; + continue; + } + + if (!strcmp(arg, ("--no-commit-header"))) { + revs.include_header = 0; + continue; + } + + if (skip_prefix(arg, "--disk-usage", &arg)) { + if (*arg == '=') { + if (!strcmp(++arg, "human")) { + human_readable = 1; + } else + die(_("invalid value for '%s': '%s', the only allowed format is '%s'"), + "--disk-usage=<format>", arg, "human"); + } else if (*arg) { + /* + * Arguably should goto a label to continue chain of ifs? + * Doesn't matter unless we try to add --disk-usage-foo + * afterwards. + */ + usage(rev_list_usage); + } + show_disk_usage = 1; + info.flags |= REV_LIST_QUIET; + continue; + } + + usage(rev_list_usage); + + } + if (revs.commit_format != CMIT_FMT_USERFORMAT) + revs.include_header = 1; + if (revs.commit_format != CMIT_FMT_UNSPECIFIED) { + /* The command line has a --pretty */ + info.hdr_termination = '\n'; + if (revs.commit_format == CMIT_FMT_ONELINE || !revs.include_header) + info.header_prefix = ""; + else + info.header_prefix = "commit "; + } + else if (revs.verbose_header) + /* Only --header was specified */ + revs.commit_format = CMIT_FMT_RAW; + + if ((!revs.commits && reflog_walk_empty(revs.reflog_info) && + (!(revs.tag_objects || revs.tree_objects || revs.blob_objects) && + !revs.pending.nr) && + !revs.rev_input_given && !revs.read_from_stdin) || + revs.diff) + usage(rev_list_usage); + + if (revs.show_notes) + die(_("rev-list does not support display of notes")); + + if (revs.count && + (revs.tag_objects || revs.tree_objects || revs.blob_objects) && + (revs.left_right || revs.cherry_mark)) + die(_("marked counting and '%s' cannot be used together"), "--objects"); + + save_commit_buffer = (revs.verbose_header || + revs.grep_filter.pattern_list || + revs.grep_filter.header_list); + if (bisect_list) + revs.limited = 1; + + if (show_progress) + progress = start_delayed_progress(show_progress, 0); + + if (use_bitmap_index) { + if (!try_bitmap_count(&revs, filter_provided_objects)) + goto cleanup; + if (!try_bitmap_disk_usage(&revs, filter_provided_objects)) + goto cleanup; + if (!try_bitmap_traversal(&revs, filter_provided_objects)) + goto cleanup; + } + + if (prepare_revision_walk(&revs)) + die("revision walk setup failed"); + if (revs.tree_objects) + mark_edges_uninteresting(&revs, show_edge, 0); + + if (bisect_list) { + int reaches, all; + unsigned bisect_flags = 0; + + if (bisect_find_all) + bisect_flags |= FIND_BISECTION_ALL; + + if (revs.first_parent_only) + bisect_flags |= FIND_BISECTION_FIRST_PARENT_ONLY; + + find_bisection(&revs.commits, &reaches, &all, bisect_flags); + + if (bisect_show_vars) { + ret = show_bisect_vars(&info, reaches, all); + goto cleanup; + } + } + + if (filter_provided_objects) { + struct commit_list *c; + for (i = 0; i < revs.pending.nr; i++) { + struct object_array_entry *pending = revs.pending.objects + i; + pending->item->flags |= NOT_USER_GIVEN; + } + for (c = revs.commits; c; c = c->next) + c->item->object.flags |= NOT_USER_GIVEN; + } + + if (arg_print_omitted) + oidset_init(&omitted_objects, DEFAULT_OIDSET_SIZE); + if (arg_missing_action == MA_PRINT) + oidset_init(&missing_objects, DEFAULT_OIDSET_SIZE); + + traverse_commit_list_filtered( + &revs, show_commit, show_object, &info, + (arg_print_omitted ? &omitted_objects : NULL)); + + if (arg_print_omitted) { + struct oidset_iter iter; + struct object_id *oid; + oidset_iter_init(&omitted_objects, &iter); + while ((oid = oidset_iter_next(&iter))) + printf("~%s\n", oid_to_hex(oid)); + oidset_clear(&omitted_objects); + } + if (arg_missing_action == MA_PRINT) { + struct oidset_iter iter; + struct object_id *oid; + oidset_iter_init(&missing_objects, &iter); + while ((oid = oidset_iter_next(&iter))) + printf("?%s\n", oid_to_hex(oid)); + oidset_clear(&missing_objects); + } + + stop_progress(&progress); + + if (revs.count) { + if (revs.left_right && revs.cherry_mark) + printf("%d\t%d\t%d\n", revs.count_left, revs.count_right, revs.count_same); + else if (revs.left_right) + printf("%d\t%d\n", revs.count_left, revs.count_right); + else if (revs.cherry_mark) + printf("%d\t%d\n", revs.count_left + revs.count_right, revs.count_same); + else + printf("%d\n", revs.count_left + revs.count_right); + } + + if (show_disk_usage) + print_disk_usage(total_disk_usage); + +cleanup: + release_revisions(&revs); + return ret; +} diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c new file mode 100644 index 0000000..fde8861 --- /dev/null +++ b/builtin/rev-parse.c @@ -0,0 +1,1112 @@ +/* + * rev-parse.c + * + * Copyright (C) Linus Torvalds, 2005 + */ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "abspath.h" +#include "config.h" +#include "commit.h" +#include "environment.h" +#include "gettext.h" +#include "hash.h" +#include "hex.h" +#include "refs.h" +#include "quote.h" +#include "object-name.h" +#include "parse-options.h" +#include "path.h" +#include "diff.h" +#include "read-cache-ll.h" +#include "revision.h" +#include "setup.h" +#include "split-index.h" +#include "submodule.h" +#include "commit-reach.h" +#include "shallow.h" + +#define DO_REVS 1 +#define DO_NOREV 2 +#define DO_FLAGS 4 +#define DO_NONFLAGS 8 +static int filter = ~0; + +static const char *def; + +#define NORMAL 0 +#define REVERSED 1 +static int show_type = NORMAL; + +#define SHOW_SYMBOLIC_ASIS 1 +#define SHOW_SYMBOLIC_FULL 2 +static int symbolic; +static int abbrev; +static int abbrev_ref; +static int abbrev_ref_strict; +static int output_sq; + +static int stuck_long; +static struct ref_exclusions ref_excludes = REF_EXCLUSIONS_INIT; + +/* + * Some arguments are relevant "revision" arguments, + * others are about output format or other details. + * This sorts it all out. + */ +static int is_rev_argument(const char *arg) +{ + static const char *rev_args[] = { + "--all", + "--bisect", + "--dense", + "--branches=", + "--branches", + "--header", + "--ignore-missing", + "--max-age=", + "--max-count=", + "--min-age=", + "--no-merges", + "--min-parents=", + "--no-min-parents", + "--max-parents=", + "--no-max-parents", + "--objects", + "--objects-edge", + "--parents", + "--pretty", + "--remotes=", + "--remotes", + "--glob=", + "--sparse", + "--tags=", + "--tags", + "--topo-order", + "--date-order", + "--unpacked", + NULL + }; + const char **p = rev_args; + + /* accept -<digit>, like traditional "head" */ + if ((*arg == '-') && isdigit(arg[1])) + return 1; + + for (;;) { + const char *str = *p++; + int len; + if (!str) + return 0; + len = strlen(str); + if (!strcmp(arg, str) || + (str[len-1] == '=' && !strncmp(arg, str, len))) + return 1; + } +} + +/* Output argument as a string, either SQ or normal */ +static void show(const char *arg) +{ + if (output_sq) { + int sq = '\'', ch; + + putchar(sq); + while ((ch = *arg++)) { + if (ch == sq) + fputs("'\\'", stdout); + putchar(ch); + } + putchar(sq); + putchar(' '); + } + else + puts(arg); +} + +/* Like show(), but with a negation prefix according to type */ +static void show_with_type(int type, const char *arg) +{ + if (type != show_type) + putchar('^'); + show(arg); +} + +/* Output a revision, only if filter allows it */ +static void show_rev(int type, const struct object_id *oid, const char *name) +{ + if (!(filter & DO_REVS)) + return; + def = NULL; + + if ((symbolic || abbrev_ref) && name) { + if (symbolic == SHOW_SYMBOLIC_FULL || abbrev_ref) { + struct object_id discard; + char *full; + + switch (repo_dwim_ref(the_repository, name, + strlen(name), &discard, &full, + 0)) { + case 0: + /* + * Not found -- not a ref. We could + * emit "name" here, but symbolic-full + * users are interested in finding the + * refs spelled in full, and they would + * need to filter non-refs if we did so. + */ + break; + case 1: /* happy */ + if (abbrev_ref) { + char *old = full; + full = shorten_unambiguous_ref(full, + abbrev_ref_strict); + free(old); + } + show_with_type(type, full); + break; + default: /* ambiguous */ + error("refname '%s' is ambiguous", name); + break; + } + free(full); + } else { + show_with_type(type, name); + } + } + else if (abbrev) + show_with_type(type, + repo_find_unique_abbrev(the_repository, oid, abbrev)); + else + show_with_type(type, oid_to_hex(oid)); +} + +/* Output a flag, only if filter allows it. */ +static int show_flag(const char *arg) +{ + if (!(filter & DO_FLAGS)) + return 0; + if (filter & (is_rev_argument(arg) ? DO_REVS : DO_NOREV)) { + show(arg); + return 1; + } + return 0; +} + +static int show_default(void) +{ + const char *s = def; + + if (s) { + struct object_id oid; + + def = NULL; + if (!repo_get_oid(the_repository, s, &oid)) { + show_rev(NORMAL, &oid, s); + return 1; + } + } + return 0; +} + +static int show_reference(const char *refname, const struct object_id *oid, + int flag UNUSED, void *cb_data UNUSED) +{ + if (ref_excluded(&ref_excludes, refname)) + return 0; + show_rev(NORMAL, oid, refname); + return 0; +} + +static int anti_reference(const char *refname, const struct object_id *oid, + int flag UNUSED, void *cb_data UNUSED) +{ + show_rev(REVERSED, oid, refname); + return 0; +} + +static int show_abbrev(const struct object_id *oid, void *cb_data UNUSED) +{ + show_rev(NORMAL, oid, NULL); + return 0; +} + +static void show_datestring(const char *flag, const char *datestr) +{ + char *buffer; + + /* date handling requires both flags and revs */ + if ((filter & (DO_FLAGS | DO_REVS)) != (DO_FLAGS | DO_REVS)) + return; + buffer = xstrfmt("%s%"PRItime, flag, approxidate(datestr)); + show(buffer); + free(buffer); +} + +static int show_file(const char *arg, int output_prefix) +{ + show_default(); + if ((filter & (DO_NONFLAGS|DO_NOREV)) == (DO_NONFLAGS|DO_NOREV)) { + if (output_prefix) { + const char *prefix = startup_info->prefix; + char *fname = prefix_filename(prefix, arg); + show(fname); + free(fname); + } else + show(arg); + return 1; + } + return 0; +} + +static int try_difference(const char *arg) +{ + char *dotdot; + struct object_id start_oid; + struct object_id end_oid; + const char *end; + const char *start; + int symmetric; + static const char head_by_default[] = "HEAD"; + + if (!(dotdot = strstr(arg, ".."))) + return 0; + end = dotdot + 2; + start = arg; + symmetric = (*end == '.'); + + *dotdot = 0; + end += symmetric; + + if (!*end) + end = head_by_default; + if (dotdot == arg) + start = head_by_default; + + if (start == head_by_default && end == head_by_default && + !symmetric) { + /* + * Just ".."? That is not a range but the + * pathspec for the parent directory. + */ + *dotdot = '.'; + return 0; + } + + if (!repo_get_oid_committish(the_repository, start, &start_oid) && !repo_get_oid_committish(the_repository, end, &end_oid)) { + show_rev(NORMAL, &end_oid, end); + show_rev(symmetric ? NORMAL : REVERSED, &start_oid, start); + if (symmetric) { + struct commit_list *exclude; + struct commit *a, *b; + a = lookup_commit_reference(the_repository, &start_oid); + b = lookup_commit_reference(the_repository, &end_oid); + if (!a || !b) { + *dotdot = '.'; + return 0; + } + exclude = repo_get_merge_bases(the_repository, a, b); + while (exclude) { + struct commit *commit = pop_commit(&exclude); + show_rev(REVERSED, &commit->object.oid, NULL); + } + } + *dotdot = '.'; + return 1; + } + *dotdot = '.'; + return 0; +} + +static int try_parent_shorthands(const char *arg) +{ + char *dotdot; + struct object_id oid; + struct commit *commit; + struct commit_list *parents; + int parent_number; + int include_rev = 0; + int include_parents = 0; + int exclude_parent = 0; + + if ((dotdot = strstr(arg, "^!"))) { + include_rev = 1; + if (dotdot[2]) + return 0; + } else if ((dotdot = strstr(arg, "^@"))) { + include_parents = 1; + if (dotdot[2]) + return 0; + } else if ((dotdot = strstr(arg, "^-"))) { + include_rev = 1; + exclude_parent = 1; + + if (dotdot[2]) { + char *end; + exclude_parent = strtoul(dotdot + 2, &end, 10); + if (*end != '\0' || !exclude_parent) + return 0; + } + } else + return 0; + + *dotdot = 0; + if (repo_get_oid_committish(the_repository, arg, &oid) || + !(commit = lookup_commit_reference(the_repository, &oid))) { + *dotdot = '^'; + return 0; + } + + if (exclude_parent && + exclude_parent > commit_list_count(commit->parents)) { + *dotdot = '^'; + return 0; + } + + if (include_rev) + show_rev(NORMAL, &oid, arg); + for (parents = commit->parents, parent_number = 1; + parents; + parents = parents->next, parent_number++) { + char *name = NULL; + + if (exclude_parent && parent_number != exclude_parent) + continue; + + if (symbolic) + name = xstrfmt("%s^%d", arg, parent_number); + show_rev(include_parents ? NORMAL : REVERSED, + &parents->item->object.oid, name); + free(name); + } + + *dotdot = '^'; + return 1; +} + +static int parseopt_dump(const struct option *o, const char *arg, int unset) +{ + struct strbuf *parsed = o->value; + if (unset) + strbuf_addf(parsed, " --no-%s", o->long_name); + else if (o->short_name && (o->long_name == NULL || !stuck_long)) + strbuf_addf(parsed, " -%c", o->short_name); + else + strbuf_addf(parsed, " --%s", o->long_name); + if (arg) { + if (!stuck_long) + strbuf_addch(parsed, ' '); + else if (o->long_name) + strbuf_addch(parsed, '='); + sq_quote_buf(parsed, arg); + } + return 0; +} + +static const char *skipspaces(const char *s) +{ + while (isspace(*s)) + s++; + return s; +} + +static char *findspace(const char *s) +{ + for (; *s; s++) + if (isspace(*s)) + return (char*)s; + return NULL; +} + +static int cmd_parseopt(int argc, const char **argv, const char *prefix) +{ + static int keep_dashdash = 0, stop_at_non_option = 0; + static char const * const parseopt_usage[] = { + N_("git rev-parse --parseopt [<options>] -- [<args>...]"), + NULL + }; + static struct option parseopt_opts[] = { + OPT_BOOL(0, "keep-dashdash", &keep_dashdash, + N_("keep the `--` passed as an arg")), + OPT_BOOL(0, "stop-at-non-option", &stop_at_non_option, + N_("stop parsing after the " + "first non-option argument")), + OPT_BOOL(0, "stuck-long", &stuck_long, + N_("output in stuck long form")), + OPT_END(), + }; + static const char * const flag_chars = "*=?!"; + + struct strbuf sb = STRBUF_INIT, parsed = STRBUF_INIT; + const char **usage = NULL; + struct option *opts = NULL; + int onb = 0, osz = 0, unb = 0, usz = 0; + + strbuf_addstr(&parsed, "set --"); + argc = parse_options(argc, argv, prefix, parseopt_opts, parseopt_usage, + PARSE_OPT_KEEP_DASHDASH); + if (argc < 1 || strcmp(argv[0], "--")) + usage_with_options(parseopt_usage, parseopt_opts); + + /* get the usage up to the first line with a -- on it */ + for (;;) { + if (strbuf_getline(&sb, stdin) == EOF) + die(_("premature end of input")); + ALLOC_GROW(usage, unb + 1, usz); + if (!strcmp("--", sb.buf)) { + if (unb < 1) + die(_("no usage string given before the `--' separator")); + usage[unb] = NULL; + break; + } + usage[unb++] = strbuf_detach(&sb, NULL); + } + + /* parse: (<short>|<short>,<long>|<long>)[*=?!]*<arghint>? SP+ <help> */ + while (strbuf_getline(&sb, stdin) != EOF) { + const char *s; + char *help; + struct option *o; + + if (!sb.len) + continue; + + ALLOC_GROW(opts, onb + 1, osz); + memset(opts + onb, 0, sizeof(opts[onb])); + + o = &opts[onb++]; + help = findspace(sb.buf); + if (!help || sb.buf == help) { + o->type = OPTION_GROUP; + o->help = xstrdup(skipspaces(sb.buf)); + continue; + } + + *help = '\0'; + + o->type = OPTION_CALLBACK; + o->help = xstrdup(skipspaces(help+1)); + o->value = &parsed; + o->flags = PARSE_OPT_NOARG; + o->callback = &parseopt_dump; + + /* name(s) */ + s = strpbrk(sb.buf, flag_chars); + if (!s) + s = help; + + if (s == sb.buf) + die(_("missing opt-spec before option flags")); + + if (s - sb.buf == 1) /* short option only */ + o->short_name = *sb.buf; + else if (sb.buf[1] != ',') /* long option only */ + o->long_name = xmemdupz(sb.buf, s - sb.buf); + else { + o->short_name = *sb.buf; + o->long_name = xmemdupz(sb.buf + 2, s - sb.buf - 2); + } + + /* flags */ + while (s < help) { + switch (*s++) { + case '=': + o->flags &= ~PARSE_OPT_NOARG; + continue; + case '?': + o->flags &= ~PARSE_OPT_NOARG; + o->flags |= PARSE_OPT_OPTARG; + continue; + case '!': + o->flags |= PARSE_OPT_NONEG; + continue; + case '*': + o->flags |= PARSE_OPT_HIDDEN; + continue; + } + s--; + break; + } + + if (s < help) + o->argh = xmemdupz(s, help - s); + } + strbuf_release(&sb); + + /* put an OPT_END() */ + ALLOC_GROW(opts, onb + 1, osz); + memset(opts + onb, 0, sizeof(opts[onb])); + argc = parse_options(argc, argv, prefix, opts, usage, + (keep_dashdash ? PARSE_OPT_KEEP_DASHDASH : 0) | + (stop_at_non_option ? PARSE_OPT_STOP_AT_NON_OPTION : 0) | + PARSE_OPT_SHELL_EVAL); + + strbuf_addstr(&parsed, " --"); + sq_quote_argv(&parsed, argv); + puts(parsed.buf); + strbuf_release(&parsed); + return 0; +} + +static int cmd_sq_quote(int argc, const char **argv) +{ + struct strbuf buf = STRBUF_INIT; + + if (argc) + sq_quote_argv(&buf, argv); + printf("%s\n", buf.buf); + strbuf_release(&buf); + + return 0; +} + +static void die_no_single_rev(int quiet) +{ + if (quiet) + exit(1); + else + die(_("Needed a single revision")); +} + +static const char builtin_rev_parse_usage[] = +N_("git rev-parse --parseopt [<options>] -- [<args>...]\n" + " or: git rev-parse --sq-quote [<arg>...]\n" + " or: git rev-parse [<options>] [<arg>...]\n" + "\n" + "Run \"git rev-parse --parseopt -h\" for more information on the first usage."); + +/* + * Parse "opt" or "opt=<value>", setting value respectively to either + * NULL or the string after "=". + */ +static int opt_with_value(const char *arg, const char *opt, const char **value) +{ + if (skip_prefix(arg, opt, &arg)) { + if (!*arg) { + *value = NULL; + return 1; + } + if (*arg++ == '=') { + *value = arg; + return 1; + } + } + return 0; +} + +static void handle_ref_opt(const char *pattern, const char *prefix) +{ + if (pattern) + for_each_glob_ref_in(show_reference, pattern, prefix, NULL); + else + for_each_ref_in(prefix, show_reference, NULL); + clear_ref_exclusions(&ref_excludes); +} + +enum format_type { + /* We would like a relative path. */ + FORMAT_RELATIVE, + /* We would like a canonical absolute path. */ + FORMAT_CANONICAL, + /* We would like the default behavior. */ + FORMAT_DEFAULT, +}; + +enum default_type { + /* Our default is a relative path. */ + DEFAULT_RELATIVE, + /* Our default is a relative path if there's a shared root. */ + DEFAULT_RELATIVE_IF_SHARED, + /* Our default is a canonical absolute path. */ + DEFAULT_CANONICAL, + /* Our default is not to modify the item. */ + DEFAULT_UNMODIFIED, +}; + +static void print_path(const char *path, const char *prefix, enum format_type format, enum default_type def) +{ + char *cwd = NULL; + /* + * We don't ever produce a relative path if prefix is NULL, so set the + * prefix to the current directory so that we can produce a relative + * path whenever possible. If we're using RELATIVE_IF_SHARED mode, then + * we want an absolute path unless the two share a common prefix, so don't + * set it in that case, since doing so causes a relative path to always + * be produced if possible. + */ + if (!prefix && (format != FORMAT_DEFAULT || def != DEFAULT_RELATIVE_IF_SHARED)) + prefix = cwd = xgetcwd(); + if (format == FORMAT_DEFAULT && def == DEFAULT_UNMODIFIED) { + puts(path); + } else if (format == FORMAT_RELATIVE || + (format == FORMAT_DEFAULT && def == DEFAULT_RELATIVE)) { + /* + * In order for relative_path to work as expected, we need to + * make sure that both paths are absolute paths. If we don't, + * we can end up with an unexpected absolute path that the user + * didn't want. + */ + struct strbuf buf = STRBUF_INIT, realbuf = STRBUF_INIT, prefixbuf = STRBUF_INIT; + if (!is_absolute_path(path)) { + strbuf_realpath_forgiving(&realbuf, path, 1); + path = realbuf.buf; + } + if (!is_absolute_path(prefix)) { + strbuf_realpath_forgiving(&prefixbuf, prefix, 1); + prefix = prefixbuf.buf; + } + puts(relative_path(path, prefix, &buf)); + strbuf_release(&buf); + strbuf_release(&realbuf); + strbuf_release(&prefixbuf); + } else if (format == FORMAT_DEFAULT && def == DEFAULT_RELATIVE_IF_SHARED) { + struct strbuf buf = STRBUF_INIT; + puts(relative_path(path, prefix, &buf)); + strbuf_release(&buf); + } else { + struct strbuf buf = STRBUF_INIT; + strbuf_realpath_forgiving(&buf, path, 1); + puts(buf.buf); + strbuf_release(&buf); + } + free(cwd); +} + +int cmd_rev_parse(int argc, const char **argv, const char *prefix) +{ + int i, as_is = 0, verify = 0, quiet = 0, revs_count = 0, type = 0; + int did_repo_setup = 0; + int has_dashdash = 0; + int output_prefix = 0; + struct object_id oid; + unsigned int flags = 0; + const char *name = NULL; + struct object_context unused; + struct strbuf buf = STRBUF_INIT; + const int hexsz = the_hash_algo->hexsz; + int seen_end_of_options = 0; + enum format_type format = FORMAT_DEFAULT; + + if (argc > 1 && !strcmp("--parseopt", argv[1])) + return cmd_parseopt(argc - 1, argv + 1, prefix); + + if (argc > 1 && !strcmp("--sq-quote", argv[1])) + return cmd_sq_quote(argc - 2, argv + 2); + + if (argc > 1 && !strcmp("-h", argv[1])) + usage(builtin_rev_parse_usage); + + for (i = 1; i < argc; i++) { + if (!strcmp(argv[i], "--")) { + has_dashdash = 1; + break; + } + } + + /* No options; just report on whether we're in a git repo or not. */ + if (argc == 1) { + setup_git_directory(); + git_config(git_default_config, NULL); + return 0; + } + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (as_is) { + if (show_file(arg, output_prefix) && as_is < 2) + verify_filename(prefix, arg, 0); + continue; + } + + if (!seen_end_of_options) { + if (!strcmp(arg, "--local-env-vars")) { + int i; + for (i = 0; local_repo_env[i]; i++) + printf("%s\n", local_repo_env[i]); + continue; + } + if (!strcmp(arg, "--resolve-git-dir")) { + const char *gitdir = argv[++i]; + if (!gitdir) + die(_("--resolve-git-dir requires an argument")); + gitdir = resolve_gitdir(gitdir); + if (!gitdir) + die(_("not a gitdir '%s'"), argv[i]); + puts(gitdir); + continue; + } + } + + /* The rest of the options require a git repository. */ + if (!did_repo_setup) { + prefix = setup_git_directory(); + git_config(git_default_config, NULL); + did_repo_setup = 1; + + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + } + + if (!strcmp(arg, "--")) { + as_is = 2; + /* Pass on the "--" if we show anything but files.. */ + if (filter & (DO_FLAGS | DO_REVS)) + show_file(arg, 0); + continue; + } + + if (!seen_end_of_options && *arg == '-') { + if (!strcmp(arg, "--git-path")) { + if (!argv[i + 1]) + die(_("--git-path requires an argument")); + strbuf_reset(&buf); + print_path(git_path("%s", argv[i + 1]), prefix, + format, + DEFAULT_RELATIVE_IF_SHARED); + i++; + continue; + } + if (!strcmp(arg,"-n")) { + if (++i >= argc) + die(_("-n requires an argument")); + if ((filter & DO_FLAGS) && (filter & DO_REVS)) { + show(arg); + show(argv[i]); + } + continue; + } + if (starts_with(arg, "-n")) { + if ((filter & DO_FLAGS) && (filter & DO_REVS)) + show(arg); + continue; + } + if (opt_with_value(arg, "--path-format", &arg)) { + if (!arg) + die(_("--path-format requires an argument")); + if (!strcmp(arg, "absolute")) { + format = FORMAT_CANONICAL; + } else if (!strcmp(arg, "relative")) { + format = FORMAT_RELATIVE; + } else { + die(_("unknown argument to --path-format: %s"), arg); + } + continue; + } + if (!strcmp(arg, "--default")) { + def = argv[++i]; + if (!def) + die(_("--default requires an argument")); + continue; + } + if (!strcmp(arg, "--prefix")) { + prefix = argv[++i]; + if (!prefix) + die(_("--prefix requires an argument")); + startup_info->prefix = prefix; + output_prefix = 1; + continue; + } + if (!strcmp(arg, "--revs-only")) { + filter &= ~DO_NOREV; + continue; + } + if (!strcmp(arg, "--no-revs")) { + filter &= ~DO_REVS; + continue; + } + if (!strcmp(arg, "--flags")) { + filter &= ~DO_NONFLAGS; + continue; + } + if (!strcmp(arg, "--no-flags")) { + filter &= ~DO_FLAGS; + continue; + } + if (!strcmp(arg, "--verify")) { + filter &= ~(DO_FLAGS|DO_NOREV); + verify = 1; + continue; + } + if (!strcmp(arg, "--quiet") || !strcmp(arg, "-q")) { + quiet = 1; + flags |= GET_OID_QUIETLY; + continue; + } + if (opt_with_value(arg, "--short", &arg)) { + filter &= ~(DO_FLAGS|DO_NOREV); + verify = 1; + abbrev = DEFAULT_ABBREV; + if (!arg) + continue; + abbrev = strtoul(arg, NULL, 10); + if (abbrev < MINIMUM_ABBREV) + abbrev = MINIMUM_ABBREV; + else if (hexsz <= abbrev) + abbrev = hexsz; + continue; + } + if (!strcmp(arg, "--sq")) { + output_sq = 1; + continue; + } + if (!strcmp(arg, "--not")) { + show_type ^= REVERSED; + continue; + } + if (!strcmp(arg, "--symbolic")) { + symbolic = SHOW_SYMBOLIC_ASIS; + continue; + } + if (!strcmp(arg, "--symbolic-full-name")) { + symbolic = SHOW_SYMBOLIC_FULL; + continue; + } + if (opt_with_value(arg, "--abbrev-ref", &arg)) { + abbrev_ref = 1; + abbrev_ref_strict = warn_ambiguous_refs; + if (arg) { + if (!strcmp(arg, "strict")) + abbrev_ref_strict = 1; + else if (!strcmp(arg, "loose")) + abbrev_ref_strict = 0; + else + die(_("unknown mode for --abbrev-ref: %s"), + arg); + } + continue; + } + if (!strcmp(arg, "--all")) { + for_each_ref(show_reference, NULL); + clear_ref_exclusions(&ref_excludes); + continue; + } + if (skip_prefix(arg, "--disambiguate=", &arg)) { + repo_for_each_abbrev(the_repository, arg, + show_abbrev, NULL); + continue; + } + if (!strcmp(arg, "--bisect")) { + for_each_fullref_in("refs/bisect/bad", show_reference, NULL); + for_each_fullref_in("refs/bisect/good", anti_reference, NULL); + continue; + } + if (opt_with_value(arg, "--branches", &arg)) { + if (ref_excludes.hidden_refs_configured) + return error(_("--exclude-hidden cannot be used together with --branches")); + handle_ref_opt(arg, "refs/heads/"); + continue; + } + if (opt_with_value(arg, "--tags", &arg)) { + if (ref_excludes.hidden_refs_configured) + return error(_("--exclude-hidden cannot be used together with --tags")); + handle_ref_opt(arg, "refs/tags/"); + continue; + } + if (skip_prefix(arg, "--glob=", &arg)) { + handle_ref_opt(arg, NULL); + continue; + } + if (opt_with_value(arg, "--remotes", &arg)) { + if (ref_excludes.hidden_refs_configured) + return error(_("--exclude-hidden cannot be used together with --remotes")); + handle_ref_opt(arg, "refs/remotes/"); + continue; + } + if (skip_prefix(arg, "--exclude=", &arg)) { + add_ref_exclusion(&ref_excludes, arg); + continue; + } + if (skip_prefix(arg, "--exclude-hidden=", &arg)) { + exclude_hidden_refs(&ref_excludes, arg); + continue; + } + if (!strcmp(arg, "--show-toplevel")) { + const char *work_tree = get_git_work_tree(); + if (work_tree) + print_path(work_tree, prefix, format, DEFAULT_UNMODIFIED); + else + die(_("this operation must be run in a work tree")); + continue; + } + if (!strcmp(arg, "--show-superproject-working-tree")) { + struct strbuf superproject = STRBUF_INIT; + if (get_superproject_working_tree(&superproject)) + print_path(superproject.buf, prefix, format, DEFAULT_UNMODIFIED); + strbuf_release(&superproject); + continue; + } + if (!strcmp(arg, "--show-prefix")) { + if (prefix) + puts(prefix); + else + putchar('\n'); + continue; + } + if (!strcmp(arg, "--show-cdup")) { + const char *pfx = prefix; + if (!is_inside_work_tree()) { + const char *work_tree = + get_git_work_tree(); + if (work_tree) + printf("%s\n", work_tree); + continue; + } + while (pfx) { + pfx = strchr(pfx, '/'); + if (pfx) { + pfx++; + printf("../"); + } + } + putchar('\n'); + continue; + } + if (!strcmp(arg, "--git-dir") || + !strcmp(arg, "--absolute-git-dir")) { + const char *gitdir = getenv(GIT_DIR_ENVIRONMENT); + char *cwd; + int len; + enum format_type wanted = format; + if (arg[2] == 'g') { /* --git-dir */ + if (gitdir) { + print_path(gitdir, prefix, format, DEFAULT_UNMODIFIED); + continue; + } + if (!prefix) { + print_path(".git", prefix, format, DEFAULT_UNMODIFIED); + continue; + } + } else { /* --absolute-git-dir */ + wanted = FORMAT_CANONICAL; + if (!gitdir && !prefix) + gitdir = ".git"; + if (gitdir) { + struct strbuf realpath = STRBUF_INIT; + strbuf_realpath(&realpath, gitdir, 1); + puts(realpath.buf); + strbuf_release(&realpath); + continue; + } + } + cwd = xgetcwd(); + len = strlen(cwd); + strbuf_reset(&buf); + strbuf_addf(&buf, "%s%s.git", cwd, len && cwd[len-1] != '/' ? "/" : ""); + free(cwd); + print_path(buf.buf, prefix, wanted, DEFAULT_CANONICAL); + continue; + } + if (!strcmp(arg, "--git-common-dir")) { + print_path(get_git_common_dir(), prefix, format, DEFAULT_RELATIVE_IF_SHARED); + continue; + } + if (!strcmp(arg, "--is-inside-git-dir")) { + printf("%s\n", is_inside_git_dir() ? "true" + : "false"); + continue; + } + if (!strcmp(arg, "--is-inside-work-tree")) { + printf("%s\n", is_inside_work_tree() ? "true" + : "false"); + continue; + } + if (!strcmp(arg, "--is-bare-repository")) { + printf("%s\n", is_bare_repository() ? "true" + : "false"); + continue; + } + if (!strcmp(arg, "--is-shallow-repository")) { + printf("%s\n", + is_repository_shallow(the_repository) ? "true" + : "false"); + continue; + } + if (!strcmp(arg, "--shared-index-path")) { + if (repo_read_index(the_repository) < 0) + die(_("Could not read the index")); + if (the_index.split_index) { + const struct object_id *oid = &the_index.split_index->base_oid; + const char *path = git_path("sharedindex.%s", oid_to_hex(oid)); + print_path(path, prefix, format, DEFAULT_RELATIVE); + } + continue; + } + if (skip_prefix(arg, "--since=", &arg)) { + show_datestring("--max-age=", arg); + continue; + } + if (skip_prefix(arg, "--after=", &arg)) { + show_datestring("--max-age=", arg); + continue; + } + if (skip_prefix(arg, "--before=", &arg)) { + show_datestring("--min-age=", arg); + continue; + } + if (skip_prefix(arg, "--until=", &arg)) { + show_datestring("--min-age=", arg); + continue; + } + if (opt_with_value(arg, "--show-object-format", &arg)) { + const char *val = arg ? arg : "storage"; + + if (strcmp(val, "storage") && + strcmp(val, "input") && + strcmp(val, "output")) + die(_("unknown mode for --show-object-format: %s"), + arg); + puts(the_hash_algo->name); + continue; + } + if (!strcmp(arg, "--end-of-options")) { + seen_end_of_options = 1; + if (filter & (DO_FLAGS | DO_REVS)) + show_file(arg, 0); + continue; + } + if (show_flag(arg) && verify) + die_no_single_rev(quiet); + continue; + } + + /* Not a flag argument */ + if (try_difference(arg)) + continue; + if (try_parent_shorthands(arg)) + continue; + name = arg; + type = NORMAL; + if (*arg == '^') { + name++; + type = REVERSED; + } + if (!get_oid_with_context(the_repository, name, + flags, &oid, &unused)) { + if (verify) + revs_count++; + else + show_rev(type, &oid, name); + continue; + } + if (verify) + die_no_single_rev(quiet); + if (has_dashdash) + die(_("bad revision '%s'"), arg); + as_is = 1; + if (!show_file(arg, output_prefix)) + continue; + verify_filename(prefix, arg, 1); + } + strbuf_release(&buf); + if (verify) { + if (revs_count == 1) { + show_rev(type, &oid, name); + return 0; + } else if (revs_count == 0 && show_default()) + return 0; + die_no_single_rev(quiet); + } else + show_default(); + return 0; +} diff --git a/builtin/revert.c b/builtin/revert.c new file mode 100644 index 0000000..e6f9a1a --- /dev/null +++ b/builtin/revert.c @@ -0,0 +1,256 @@ +#include "git-compat-util.h" +#include "config.h" +#include "builtin.h" +#include "parse-options.h" +#include "diff.h" +#include "gettext.h" +#include "repository.h" +#include "revision.h" +#include "rerere.h" +#include "dir.h" +#include "sequencer.h" +#include "branch.h" + +/* + * This implements the builtins revert and cherry-pick. + * + * Copyright (c) 2007 Johannes E. Schindelin + * + * Based on git-revert.sh, which is + * + * Copyright (c) 2005 Linus Torvalds + * Copyright (c) 2005 Junio C Hamano + */ + +static const char * const revert_usage[] = { + N_("git revert [--[no-]edit] [-n] [-m <parent-number>] [-s] [-S[<keyid>]] <commit>..."), + N_("git revert (--continue | --skip | --abort | --quit)"), + NULL +}; + +static const char * const cherry_pick_usage[] = { + N_("git cherry-pick [--edit] [-n] [-m <parent-number>] [-s] [-x] [--ff]\n" + " [-S[<keyid>]] <commit>..."), + N_("git cherry-pick (--continue | --skip | --abort | --quit)"), + NULL +}; + +static const char *action_name(const struct replay_opts *opts) +{ + return opts->action == REPLAY_REVERT ? "revert" : "cherry-pick"; +} + +static const char * const *revert_or_cherry_pick_usage(struct replay_opts *opts) +{ + return opts->action == REPLAY_REVERT ? revert_usage : cherry_pick_usage; +} + +static int option_parse_m(const struct option *opt, + const char *arg, int unset) +{ + struct replay_opts *replay = opt->value; + char *end; + + if (unset) { + replay->mainline = 0; + return 0; + } + + replay->mainline = strtol(arg, &end, 10); + if (*end || replay->mainline <= 0) + return error(_("option `%s' expects a number greater than zero"), + opt->long_name); + + return 0; +} + +LAST_ARG_MUST_BE_NULL +static void verify_opt_compatible(const char *me, const char *base_opt, ...) +{ + const char *this_opt; + va_list ap; + + va_start(ap, base_opt); + while ((this_opt = va_arg(ap, const char *))) { + if (va_arg(ap, int)) + break; + } + va_end(ap); + + if (this_opt) + die(_("%s: %s cannot be used with %s"), me, this_opt, base_opt); +} + +static int run_sequencer(int argc, const char **argv, const char *prefix, + struct replay_opts *opts) +{ + const char * const * usage_str = revert_or_cherry_pick_usage(opts); + const char *me = action_name(opts); + const char *cleanup_arg = NULL; + int cmd = 0; + struct option base_options[] = { + OPT_CMDMODE(0, "quit", &cmd, N_("end revert or cherry-pick sequence"), 'q'), + OPT_CMDMODE(0, "continue", &cmd, N_("resume revert or cherry-pick sequence"), 'c'), + OPT_CMDMODE(0, "abort", &cmd, N_("cancel revert or cherry-pick sequence"), 'a'), + OPT_CMDMODE(0, "skip", &cmd, N_("skip current commit and continue"), 's'), + OPT_CLEANUP(&cleanup_arg), + OPT_BOOL('n', "no-commit", &opts->no_commit, N_("don't automatically commit")), + OPT_BOOL('e', "edit", &opts->edit, N_("edit the commit message")), + OPT_NOOP_NOARG('r', NULL), + OPT_BOOL('s', "signoff", &opts->signoff, N_("add a Signed-off-by trailer")), + OPT_CALLBACK('m', "mainline", opts, N_("parent-number"), + N_("select mainline parent"), option_parse_m), + OPT_RERERE_AUTOUPDATE(&opts->allow_rerere_auto), + OPT_STRING(0, "strategy", &opts->strategy, N_("strategy"), N_("merge strategy")), + OPT_STRVEC('X', "strategy-option", &opts->xopts, N_("option"), + N_("option for merge strategy")), + { OPTION_STRING, 'S', "gpg-sign", &opts->gpg_sign, N_("key-id"), + N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" }, + OPT_END() + }; + struct option *options = base_options; + + if (opts->action == REPLAY_PICK) { + struct option cp_extra[] = { + OPT_BOOL('x', NULL, &opts->record_origin, N_("append commit name")), + OPT_BOOL(0, "ff", &opts->allow_ff, N_("allow fast-forward")), + OPT_BOOL(0, "allow-empty", &opts->allow_empty, N_("preserve initially empty commits")), + OPT_BOOL(0, "allow-empty-message", &opts->allow_empty_message, N_("allow commits with empty messages")), + OPT_BOOL(0, "keep-redundant-commits", &opts->keep_redundant_commits, N_("keep redundant, empty commits")), + OPT_END(), + }; + options = parse_options_concat(options, cp_extra); + } else if (opts->action == REPLAY_REVERT) { + struct option cp_extra[] = { + OPT_BOOL(0, "reference", &opts->commit_use_reference, + N_("use the 'reference' format to refer to commits")), + OPT_END(), + }; + options = parse_options_concat(options, cp_extra); + } + + argc = parse_options(argc, argv, prefix, options, usage_str, + PARSE_OPT_KEEP_ARGV0 | + PARSE_OPT_KEEP_UNKNOWN_OPT); + + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + /* implies allow_empty */ + if (opts->keep_redundant_commits) + opts->allow_empty = 1; + + if (cleanup_arg) { + opts->default_msg_cleanup = get_cleanup_mode(cleanup_arg, 1); + opts->explicit_cleanup = 1; + } + + /* Check for incompatible command line arguments */ + if (cmd) { + char *this_operation; + if (cmd == 'q') + this_operation = "--quit"; + else if (cmd == 'c') + this_operation = "--continue"; + else if (cmd == 's') + this_operation = "--skip"; + else { + assert(cmd == 'a'); + this_operation = "--abort"; + } + + verify_opt_compatible(me, this_operation, + "--no-commit", opts->no_commit, + "--signoff", opts->signoff, + "--mainline", opts->mainline, + "--strategy", opts->strategy ? 1 : 0, + "--strategy-option", opts->xopts.nr ? 1 : 0, + "-x", opts->record_origin, + "--ff", opts->allow_ff, + "--rerere-autoupdate", opts->allow_rerere_auto == RERERE_AUTOUPDATE, + "--no-rerere-autoupdate", opts->allow_rerere_auto == RERERE_NOAUTOUPDATE, + NULL); + } + + if (!opts->strategy && opts->default_strategy) { + opts->strategy = opts->default_strategy; + opts->default_strategy = NULL; + } + + if (opts->allow_ff) + verify_opt_compatible(me, "--ff", + "--signoff", opts->signoff, + "--no-commit", opts->no_commit, + "-x", opts->record_origin, + "--edit", opts->edit > 0, + NULL); + + if (cmd) { + opts->revs = NULL; + } else { + struct setup_revision_opt s_r_opt; + opts->revs = xmalloc(sizeof(*opts->revs)); + repo_init_revisions(the_repository, opts->revs, NULL); + opts->revs->no_walk = 1; + opts->revs->unsorted_input = 1; + if (argc < 2) + usage_with_options(usage_str, options); + if (!strcmp(argv[1], "-")) + argv[1] = "@{-1}"; + memset(&s_r_opt, 0, sizeof(s_r_opt)); + s_r_opt.assume_dashdash = 1; + argc = setup_revisions(argc, argv, opts->revs, &s_r_opt); + } + + if (argc > 1) + usage_with_options(usage_str, options); + + /* These option values will be free()d */ + opts->gpg_sign = xstrdup_or_null(opts->gpg_sign); + opts->strategy = xstrdup_or_null(opts->strategy); + if (!opts->strategy && getenv("GIT_TEST_MERGE_ALGORITHM")) + opts->strategy = xstrdup(getenv("GIT_TEST_MERGE_ALGORITHM")); + free(options); + + if (cmd == 'q') { + int ret = sequencer_remove_state(opts); + if (!ret) + remove_branch_state(the_repository, 0); + return ret; + } + if (cmd == 'c') + return sequencer_continue(the_repository, opts); + if (cmd == 'a') + return sequencer_rollback(the_repository, opts); + if (cmd == 's') + return sequencer_skip(the_repository, opts); + return sequencer_pick_revisions(the_repository, opts); +} + +int cmd_revert(int argc, const char **argv, const char *prefix) +{ + struct replay_opts opts = REPLAY_OPTS_INIT; + int res; + + opts.action = REPLAY_REVERT; + sequencer_init_config(&opts); + res = run_sequencer(argc, argv, prefix, &opts); + if (res < 0) + die(_("revert failed")); + replay_opts_release(&opts); + return res; +} + +int cmd_cherry_pick(int argc, const char **argv, const char *prefix) +{ + struct replay_opts opts = REPLAY_OPTS_INIT; + int res; + + opts.action = REPLAY_PICK; + sequencer_init_config(&opts); + res = run_sequencer(argc, argv, prefix, &opts); + if (res < 0) + die(_("cherry-pick failed")); + replay_opts_release(&opts); + return res; +} diff --git a/builtin/rm.c b/builtin/rm.c new file mode 100644 index 0000000..dff819a --- /dev/null +++ b/builtin/rm.c @@ -0,0 +1,444 @@ +/* + * "git rm" builtin command + * + * Copyright (C) Linus Torvalds 2006 + */ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "advice.h" +#include "config.h" +#include "lockfile.h" +#include "dir.h" +#include "cache-tree.h" +#include "gettext.h" +#include "hash.h" +#include "tree-walk.h" +#include "object-name.h" +#include "parse-options.h" +#include "read-cache.h" +#include "repository.h" +#include "string-list.h" +#include "setup.h" +#include "sparse-index.h" +#include "submodule.h" +#include "pathspec.h" + +static const char * const builtin_rm_usage[] = { + N_("git rm [-f | --force] [-n] [-r] [--cached] [--ignore-unmatch]\n" + " [--quiet] [--pathspec-from-file=<file> [--pathspec-file-nul]]\n" + " [--] [<pathspec>...]"), + NULL +}; + +static struct { + int nr, alloc; + struct { + const char *name; + char is_submodule; + } *entry; +} list; + +static int get_ours_cache_pos(const char *path, int pos) +{ + int i = -pos - 1; + + while ((i < the_index.cache_nr) && !strcmp(the_index.cache[i]->name, path)) { + if (ce_stage(the_index.cache[i]) == 2) + return i; + i++; + } + return -1; +} + +static void print_error_files(struct string_list *files_list, + const char *main_msg, + const char *hints_msg, + int *errs) +{ + if (files_list->nr) { + int i; + struct strbuf err_msg = STRBUF_INIT; + + strbuf_addstr(&err_msg, main_msg); + for (i = 0; i < files_list->nr; i++) + strbuf_addf(&err_msg, + "\n %s", + files_list->items[i].string); + if (advice_enabled(ADVICE_RM_HINTS)) + strbuf_addstr(&err_msg, hints_msg); + *errs = error("%s", err_msg.buf); + strbuf_release(&err_msg); + } +} + +static void submodules_absorb_gitdir_if_needed(void) +{ + int i; + for (i = 0; i < list.nr; i++) { + const char *name = list.entry[i].name; + int pos; + const struct cache_entry *ce; + + pos = index_name_pos(&the_index, name, strlen(name)); + if (pos < 0) { + pos = get_ours_cache_pos(name, pos); + if (pos < 0) + continue; + } + ce = the_index.cache[pos]; + + if (!S_ISGITLINK(ce->ce_mode) || + !file_exists(ce->name) || + is_empty_dir(name)) + continue; + + if (!submodule_uses_gitfile(name)) + absorb_git_dir_into_superproject(name, NULL); + } +} + +static int check_local_mod(struct object_id *head, int index_only) +{ + /* + * Items in list are already sorted in the cache order, + * so we could do this a lot more efficiently by using + * tree_desc based traversal if we wanted to, but I am + * lazy, and who cares if removal of files is a tad + * slower than the theoretical maximum speed? + */ + int i, no_head; + int errs = 0; + struct string_list files_staged = STRING_LIST_INIT_NODUP; + struct string_list files_cached = STRING_LIST_INIT_NODUP; + struct string_list files_local = STRING_LIST_INIT_NODUP; + + no_head = is_null_oid(head); + for (i = 0; i < list.nr; i++) { + struct stat st; + int pos; + const struct cache_entry *ce; + const char *name = list.entry[i].name; + struct object_id oid; + unsigned short mode; + int local_changes = 0; + int staged_changes = 0; + + pos = index_name_pos(&the_index, name, strlen(name)); + if (pos < 0) { + /* + * Skip unmerged entries except for populated submodules + * that could lose history when removed. + */ + pos = get_ours_cache_pos(name, pos); + if (pos < 0) + continue; + + if (!S_ISGITLINK(the_index.cache[pos]->ce_mode) || + is_empty_dir(name)) + continue; + } + ce = the_index.cache[pos]; + + if (lstat(ce->name, &st) < 0) { + if (!is_missing_file_error(errno)) + warning_errno(_("failed to stat '%s'"), ce->name); + /* It already vanished from the working tree */ + continue; + } + else if (S_ISDIR(st.st_mode)) { + /* if a file was removed and it is now a + * directory, that is the same as ENOENT as + * far as git is concerned; we do not track + * directories unless they are submodules. + */ + if (!S_ISGITLINK(ce->ce_mode)) + continue; + } + + /* + * "rm" of a path that has changes need to be treated + * carefully not to allow losing local changes + * accidentally. A local change could be (1) file in + * work tree is different since the index; and/or (2) + * the user staged a content that is different from + * the current commit in the index. + * + * In such a case, you would need to --force the + * removal. However, "rm --cached" (remove only from + * the index) is safe if the index matches the file in + * the work tree or the HEAD commit, as it means that + * the content being removed is available elsewhere. + */ + + /* + * Is the index different from the file in the work tree? + * If it's a submodule, is its work tree modified? + */ + if (ie_match_stat(&the_index, ce, &st, 0) || + (S_ISGITLINK(ce->ce_mode) && + bad_to_remove_submodule(ce->name, + SUBMODULE_REMOVAL_DIE_ON_ERROR | + SUBMODULE_REMOVAL_IGNORE_IGNORED_UNTRACKED))) + local_changes = 1; + + /* + * Is the index different from the HEAD commit? By + * definition, before the very initial commit, + * anything staged in the index is treated by the same + * way as changed from the HEAD. + */ + if (no_head + || get_tree_entry(the_repository, head, name, &oid, &mode) + || ce->ce_mode != create_ce_mode(mode) + || !oideq(&ce->oid, &oid)) + staged_changes = 1; + + /* + * If the index does not match the file in the work + * tree and if it does not match the HEAD commit + * either, (1) "git rm" without --cached definitely + * will lose information; (2) "git rm --cached" will + * lose information unless it is about removing an + * "intent to add" entry. + */ + if (local_changes && staged_changes) { + if (!index_only || !ce_intent_to_add(ce)) + string_list_append(&files_staged, name); + } + else if (!index_only) { + if (staged_changes) + string_list_append(&files_cached, name); + if (local_changes) + string_list_append(&files_local, name); + } + } + print_error_files(&files_staged, + Q_("the following file has staged content different " + "from both the\nfile and the HEAD:", + "the following files have staged content different" + " from both the\nfile and the HEAD:", + files_staged.nr), + _("\n(use -f to force removal)"), + &errs); + string_list_clear(&files_staged, 0); + print_error_files(&files_cached, + Q_("the following file has changes " + "staged in the index:", + "the following files have changes " + "staged in the index:", files_cached.nr), + _("\n(use --cached to keep the file," + " or -f to force removal)"), + &errs); + string_list_clear(&files_cached, 0); + + print_error_files(&files_local, + Q_("the following file has local modifications:", + "the following files have local modifications:", + files_local.nr), + _("\n(use --cached to keep the file," + " or -f to force removal)"), + &errs); + string_list_clear(&files_local, 0); + + return errs; +} + +static int show_only = 0, force = 0, index_only = 0, recursive = 0, quiet = 0; +static int ignore_unmatch = 0, pathspec_file_nul; +static int include_sparse; +static char *pathspec_from_file; + +static struct option builtin_rm_options[] = { + OPT__DRY_RUN(&show_only, N_("dry run")), + OPT__QUIET(&quiet, N_("do not list removed files")), + OPT_BOOL( 0 , "cached", &index_only, N_("only remove from the index")), + OPT__FORCE(&force, N_("override the up-to-date check"), PARSE_OPT_NOCOMPLETE), + OPT_BOOL('r', NULL, &recursive, N_("allow recursive removal")), + OPT_BOOL( 0 , "ignore-unmatch", &ignore_unmatch, + N_("exit with a zero status even if nothing matched")), + OPT_BOOL(0, "sparse", &include_sparse, N_("allow updating entries outside of the sparse-checkout cone")), + OPT_PATHSPEC_FROM_FILE(&pathspec_from_file), + OPT_PATHSPEC_FILE_NUL(&pathspec_file_nul), + OPT_END(), +}; + +int cmd_rm(int argc, const char **argv, const char *prefix) +{ + struct lock_file lock_file = LOCK_INIT; + int i, ret = 0; + struct pathspec pathspec; + char *seen; + + git_config(git_default_config, NULL); + + argc = parse_options(argc, argv, prefix, builtin_rm_options, + builtin_rm_usage, 0); + + parse_pathspec(&pathspec, 0, + PATHSPEC_PREFER_CWD, + 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, 0, + PATHSPEC_PREFER_CWD, + 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 (!pathspec.nr) + die(_("No pathspec was given. Which files should I remove?")); + + if (!index_only) + setup_work_tree(); + + 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); + + if (repo_read_index(the_repository) < 0) + die(_("index file corrupt")); + + refresh_index(&the_index, REFRESH_QUIET|REFRESH_UNMERGED, &pathspec, NULL, NULL); + + seen = xcalloc(pathspec.nr, 1); + + if (pathspec_needs_expanded_index(&the_index, &pathspec)) + ensure_full_index(&the_index); + + for (i = 0; i < the_index.cache_nr; i++) { + const 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_path_match(&the_index, ce, &pathspec, seen)) + continue; + ALLOC_GROW(list.entry, list.nr + 1, list.alloc); + list.entry[list.nr].name = xstrdup(ce->name); + list.entry[list.nr].is_submodule = S_ISGITLINK(ce->ce_mode); + if (list.entry[list.nr++].is_submodule && + !is_staging_gitmodules_ok(&the_index)) + die(_("please stage your changes to .gitmodules or stash them to proceed")); + } + + if (pathspec.nr) { + const char *original; + int seen_any = 0; + char *skip_worktree_seen = NULL; + struct string_list only_match_skip_worktree = STRING_LIST_INIT_NODUP; + + for (i = 0; i < pathspec.nr; i++) { + original = pathspec.items[i].original; + if (seen[i]) + seen_any = 1; + else if (ignore_unmatch) + continue; + else if (!include_sparse && + matches_skip_worktree(&pathspec, i, &skip_worktree_seen)) + string_list_append(&only_match_skip_worktree, original); + else + die(_("pathspec '%s' did not match any files"), original); + + if (!recursive && seen[i] == MATCHED_RECURSIVELY) + die(_("not removing '%s' recursively without -r"), + *original ? original : "."); + } + + if (only_match_skip_worktree.nr) { + advise_on_updating_sparse_paths(&only_match_skip_worktree); + ret = 1; + } + free(skip_worktree_seen); + string_list_clear(&only_match_skip_worktree, 0); + + if (!seen_any) + exit(ret); + } + clear_pathspec(&pathspec); + free(seen); + + if (!index_only) + submodules_absorb_gitdir_if_needed(); + + /* + * If not forced, the file, the index and the HEAD (if exists) + * must match; but the file can already been removed, since + * this sequence is a natural "novice" way: + * + * rm F; git rm F + * + * Further, if HEAD commit exists, "diff-index --cached" must + * report no changes unless forced. + */ + if (!force) { + struct object_id oid; + if (repo_get_oid(the_repository, "HEAD", &oid)) + oidclr(&oid); + if (check_local_mod(&oid, index_only)) + exit(1); + } + + /* + * First remove the names from the index: we won't commit + * the index unless all of them succeed. + */ + for (i = 0; i < list.nr; i++) { + const char *path = list.entry[i].name; + if (!quiet) + printf("rm '%s'\n", path); + + if (remove_file_from_index(&the_index, path)) + die(_("git rm: unable to remove %s"), path); + } + + if (show_only) + return 0; + + /* + * Then, unless we used "--cached", remove the filenames from + * the workspace. If we fail to remove the first one, we + * abort the "git rm" (but once we've successfully removed + * any file at all, we'll go ahead and commit to it all: + * by then we've already committed ourselves and can't fail + * in the middle) + */ + if (!index_only) { + int removed = 0, gitmodules_modified = 0; + struct strbuf buf = STRBUF_INIT; + int flag = force ? REMOVE_DIR_PURGE_ORIGINAL_CWD : 0; + for (i = 0; i < list.nr; i++) { + const char *path = list.entry[i].name; + if (list.entry[i].is_submodule) { + strbuf_reset(&buf); + strbuf_addstr(&buf, path); + if (remove_dir_recursively(&buf, flag)) + die(_("could not remove '%s'"), path); + + removed = 1; + if (!remove_path_from_gitmodules(path)) + gitmodules_modified = 1; + continue; + } + if (!remove_path(path)) { + removed = 1; + continue; + } + if (!removed) + die_errno("git rm: '%s'", path); + } + strbuf_release(&buf); + if (gitmodules_modified) + stage_updated_gitmodules(&the_index); + } + + if (write_locked_index(&the_index, &lock_file, + COMMIT_LOCK | SKIP_IF_UNCHANGED)) + die(_("Unable to write new index file")); + + return ret; +} diff --git a/builtin/send-pack.c b/builtin/send-pack.c new file mode 100644 index 0000000..cd6d9e4 --- /dev/null +++ b/builtin/send-pack.c @@ -0,0 +1,347 @@ +#include "builtin.h" +#include "config.h" +#include "commit.h" +#include "hex.h" +#include "refs.h" +#include "pkt-line.h" +#include "sideband.h" +#include "run-command.h" +#include "remote.h" +#include "connect.h" +#include "send-pack.h" +#include "quote.h" +#include "transport.h" +#include "version.h" +#include "oid-array.h" +#include "gpg-interface.h" +#include "gettext.h" +#include "protocol.h" +#include "parse-options.h" +#include "write-or-die.h" + +static const char * const send_pack_usage[] = { + N_("git send-pack [--mirror] [--dry-run] [--force]\n" + " [--receive-pack=<git-receive-pack>]\n" + " [--verbose] [--thin] [--atomic]\n" + " [--[no-]signed | --signed=(true|false|if-asked)]\n" + " [<host>:]<directory> (--all | <ref>...)"), + NULL, +}; + +static struct send_pack_args args; + +static void print_helper_status(struct ref *ref) +{ + struct strbuf buf = STRBUF_INIT; + struct ref_push_report *report; + + for (; ref; ref = ref->next) { + const char *msg = NULL; + const char *res; + int count = 0; + + switch(ref->status) { + case REF_STATUS_NONE: + res = "error"; + msg = "no match"; + break; + + case REF_STATUS_OK: + res = "ok"; + break; + + case REF_STATUS_UPTODATE: + res = "ok"; + msg = "up to date"; + break; + + case REF_STATUS_REJECT_NONFASTFORWARD: + res = "error"; + msg = "non-fast forward"; + break; + + case REF_STATUS_REJECT_FETCH_FIRST: + res = "error"; + msg = "fetch first"; + break; + + case REF_STATUS_REJECT_NEEDS_FORCE: + res = "error"; + msg = "needs force"; + break; + + case REF_STATUS_REJECT_STALE: + res = "error"; + msg = "stale info"; + break; + + case REF_STATUS_REJECT_REMOTE_UPDATED: + res = "error"; + msg = "remote ref updated since checkout"; + break; + + case REF_STATUS_REJECT_ALREADY_EXISTS: + res = "error"; + msg = "already exists"; + break; + + case REF_STATUS_REJECT_NODELETE: + case REF_STATUS_REMOTE_REJECT: + res = "error"; + break; + + case REF_STATUS_EXPECTING_REPORT: + res = "error"; + msg = "expecting report"; + break; + + default: + continue; + } + + strbuf_reset(&buf); + strbuf_addf(&buf, "%s %s", res, ref->name); + if (ref->remote_status) + msg = ref->remote_status; + if (msg) { + strbuf_addch(&buf, ' '); + quote_two_c_style(&buf, "", msg, 0); + } + strbuf_addch(&buf, '\n'); + + if (ref->status == REF_STATUS_OK) { + for (report = ref->report; report; report = report->next) { + if (count++ > 0) + strbuf_addf(&buf, "ok %s\n", ref->name); + if (report->ref_name) + strbuf_addf(&buf, "option refname %s\n", + report->ref_name); + if (report->old_oid) + strbuf_addf(&buf, "option old-oid %s\n", + oid_to_hex(report->old_oid)); + if (report->new_oid) + strbuf_addf(&buf, "option new-oid %s\n", + oid_to_hex(report->new_oid)); + if (report->forced_update) + strbuf_addstr(&buf, "option forced-update\n"); + } + } + write_or_die(1, buf.buf, buf.len); + } + strbuf_release(&buf); +} + +static int send_pack_config(const char *k, const char *v, + const struct config_context *ctx, void *cb) +{ + if (!strcmp(k, "push.gpgsign")) { + const char *value; + if (!git_config_get_value("push.gpgsign", &value)) { + switch (git_parse_maybe_bool(value)) { + case 0: + args.push_cert = SEND_PACK_PUSH_CERT_NEVER; + break; + case 1: + args.push_cert = SEND_PACK_PUSH_CERT_ALWAYS; + break; + default: + if (value && !strcasecmp(value, "if-asked")) + args.push_cert = SEND_PACK_PUSH_CERT_IF_ASKED; + else + return error(_("invalid value for '%s'"), k); + } + } + } + return git_default_config(k, v, ctx, cb); +} + +int cmd_send_pack(int argc, const char **argv, const char *prefix) +{ + struct refspec rs = REFSPEC_INIT_PUSH; + const char *remote_name = NULL; + struct remote *remote = NULL; + const char *dest = NULL; + int fd[2]; + struct child_process *conn; + struct oid_array extra_have = OID_ARRAY_INIT; + struct oid_array shallow = OID_ARRAY_INIT; + struct ref *remote_refs, *local_refs; + int ret; + int helper_status = 0; + int send_all = 0; + int verbose = 0; + const char *receivepack = "git-receive-pack"; + unsigned dry_run = 0; + unsigned send_mirror = 0; + unsigned force_update = 0; + unsigned quiet = 0; + int push_cert = 0; + struct string_list push_options = STRING_LIST_INIT_NODUP; + unsigned use_thin_pack = 0; + unsigned atomic = 0; + unsigned stateless_rpc = 0; + int flags; + unsigned int reject_reasons; + int progress = -1; + int from_stdin = 0; + struct push_cas_option cas = {0}; + int force_if_includes = 0; + struct packet_reader reader; + + struct option options[] = { + OPT__VERBOSITY(&verbose), + OPT_STRING(0, "receive-pack", &receivepack, "receive-pack", N_("receive pack program")), + OPT_STRING(0, "exec", &receivepack, "receive-pack", N_("receive pack program")), + OPT_STRING(0, "remote", &remote_name, "remote", N_("remote name")), + OPT_BOOL(0, "all", &send_all, N_("push all refs")), + OPT_BOOL('n' , "dry-run", &dry_run, N_("dry run")), + OPT_BOOL(0, "mirror", &send_mirror, N_("mirror all refs")), + OPT_BOOL('f', "force", &force_update, N_("force updates")), + OPT_CALLBACK_F(0, "signed", &push_cert, "(yes|no|if-asked)", N_("GPG sign the push"), + PARSE_OPT_OPTARG, option_parse_push_signed), + OPT_STRING_LIST(0, "push-option", &push_options, + N_("server-specific"), + N_("option to transmit")), + OPT_BOOL(0, "progress", &progress, N_("force progress reporting")), + OPT_BOOL(0, "thin", &use_thin_pack, N_("use thin pack")), + OPT_BOOL(0, "atomic", &atomic, N_("request atomic transaction on remote side")), + OPT_BOOL(0, "stateless-rpc", &stateless_rpc, N_("use stateless RPC protocol")), + OPT_BOOL(0, "stdin", &from_stdin, N_("read refs from stdin")), + OPT_BOOL(0, "helper-status", &helper_status, N_("print status from remote helper")), + OPT_CALLBACK_F(0, CAS_OPT_NAME, &cas, N_("<refname>:<expect>"), + N_("require old value of ref to be at this value"), + PARSE_OPT_OPTARG, parseopt_push_cas_option), + OPT_BOOL(0, TRANS_OPT_FORCE_IF_INCLUDES, &force_if_includes, + N_("require remote updates to be integrated locally")), + OPT_END() + }; + + git_config(send_pack_config, NULL); + argc = parse_options(argc, argv, prefix, options, send_pack_usage, 0); + if (argc > 0) { + dest = argv[0]; + refspec_appendn(&rs, argv + 1, argc - 1); + } + + if (!dest) + usage_with_options(send_pack_usage, options); + + args.verbose = verbose; + args.dry_run = dry_run; + args.send_mirror = send_mirror; + args.force_update = force_update; + args.quiet = quiet; + args.push_cert = push_cert; + args.progress = progress; + args.use_thin_pack = use_thin_pack; + args.atomic = atomic; + args.stateless_rpc = stateless_rpc; + args.push_options = push_options.nr ? &push_options : NULL; + args.url = dest; + + if (from_stdin) { + if (args.stateless_rpc) { + const char *buf; + while ((buf = packet_read_line(0, NULL))) + refspec_append(&rs, buf); + } else { + struct strbuf line = STRBUF_INIT; + while (strbuf_getline(&line, stdin) != EOF) + refspec_append(&rs, line.buf); + strbuf_release(&line); + } + } + + /* + * --all and --mirror are incompatible; neither makes sense + * with any refspecs. + */ + if ((rs.nr > 0 && (send_all || args.send_mirror)) || + (send_all && args.send_mirror)) + usage_with_options(send_pack_usage, options); + + if (remote_name) { + remote = remote_get(remote_name); + if (!remote_has_url(remote, dest)) { + die("Destination %s is not a uri for %s", + dest, remote_name); + } + } + + if (progress == -1) + progress = !args.quiet && isatty(2); + args.progress = progress; + + if (args.stateless_rpc) { + conn = NULL; + fd[0] = 0; + fd[1] = 1; + } else { + conn = git_connect(fd, dest, "git-receive-pack", receivepack, + args.verbose ? CONNECT_VERBOSE : 0); + } + + packet_reader_init(&reader, fd[0], NULL, 0, + PACKET_READ_CHOMP_NEWLINE | + PACKET_READ_GENTLE_ON_EOF | + PACKET_READ_DIE_ON_ERR_PACKET); + + switch (discover_version(&reader)) { + case protocol_v2: + die("support for protocol v2 not implemented yet"); + break; + case protocol_v1: + case protocol_v0: + get_remote_heads(&reader, &remote_refs, REF_NORMAL, + &extra_have, &shallow); + break; + case protocol_unknown_version: + BUG("unknown protocol version"); + } + + local_refs = get_local_heads(); + + flags = MATCH_REFS_NONE; + + if (send_all) + flags |= MATCH_REFS_ALL; + if (args.send_mirror) + flags |= MATCH_REFS_MIRROR; + + /* match them up */ + if (match_push_refs(local_refs, &remote_refs, &rs, flags)) + return -1; + + if (!is_empty_cas(&cas)) + apply_push_cas(&cas, remote, remote_refs); + + if (!is_empty_cas(&cas) && force_if_includes) + cas.use_force_if_includes = 1; + + set_ref_status_for_push(remote_refs, args.send_mirror, + args.force_update); + + ret = send_pack(&args, fd, conn, remote_refs, &extra_have); + + if (helper_status) + print_helper_status(remote_refs); + + close(fd[1]); + close(fd[0]); + + ret |= finish_connect(conn); + + if (!helper_status) + transport_print_push_status(dest, remote_refs, args.verbose, 0, &reject_reasons); + + if (!args.dry_run && remote) { + struct ref *ref; + for (ref = remote_refs; ref; ref = ref->next) + transport_update_tracking_ref(remote, ref, args.verbose); + } + + if (!ret && !transport_refs_pushed(remote_refs)) + fprintf(stderr, "Everything up-to-date\n"); + + return ret; +} diff --git a/builtin/shortlog.c b/builtin/shortlog.c new file mode 100644 index 0000000..1307ed2 --- /dev/null +++ b/builtin/shortlog.c @@ -0,0 +1,521 @@ +#include "builtin.h" +#include "config.h" +#include "commit.h" +#include "diff.h" +#include "environment.h" +#include "gettext.h" +#include "string-list.h" +#include "repository.h" +#include "revision.h" +#include "utf8.h" +#include "mailmap.h" +#include "setup.h" +#include "shortlog.h" +#include "parse-options.h" +#include "trailer.h" +#include "strmap.h" + +static char const * const shortlog_usage[] = { + N_("git shortlog [<options>] [<revision-range>] [[--] <path>...]"), + N_("git log --pretty=short | git shortlog [<options>]"), + NULL +}; + +/* + * The util field of our string_list_items will contain one of two things: + * + * - if --summary is not in use, it will point to a string list of the + * oneline subjects assigned to this author + * + * - if --summary is in use, we don't need that list; we only need to know + * its size. So we abuse the pointer slot to store our integer counter. + * + * This macro accesses the latter. + */ +#define UTIL_TO_INT(x) ((intptr_t)(x)->util) + +static int compare_by_counter(const void *a1, const void *a2) +{ + const struct string_list_item *i1 = a1, *i2 = a2; + return UTIL_TO_INT(i2) - UTIL_TO_INT(i1); +} + +static int compare_by_list(const void *a1, const void *a2) +{ + const struct string_list_item *i1 = a1, *i2 = a2; + const struct string_list *l1 = i1->util, *l2 = i2->util; + + if (l1->nr < l2->nr) + return 1; + else if (l1->nr == l2->nr) + return 0; + else + return -1; +} + +static void insert_one_record(struct shortlog *log, + const char *ident, + const char *oneline) +{ + struct string_list_item *item; + + item = string_list_insert(&log->list, ident); + + if (log->summary) + item->util = (void *)(UTIL_TO_INT(item) + 1); + else { + char *buffer; + struct strbuf subject = STRBUF_INIT; + const char *eol; + + /* Skip any leading whitespace, including any blank lines. */ + while (*oneline && isspace(*oneline)) + oneline++; + eol = strchr(oneline, '\n'); + if (!eol) + eol = oneline + strlen(oneline); + if (starts_with(oneline, "[PATCH")) { + char *eob = strchr(oneline, ']'); + if (eob && (!eol || eob < eol)) + oneline = eob + 1; + } + while (*oneline && isspace(*oneline) && *oneline != '\n') + oneline++; + format_subject(&subject, oneline, " "); + buffer = strbuf_detach(&subject, NULL); + + if (!item->util) { + item->util = xmalloc(sizeof(struct string_list)); + string_list_init_nodup(item->util); + } + string_list_append(item->util, buffer); + } +} + +static int parse_ident(struct shortlog *log, + struct strbuf *out, const char *in) +{ + const char *mailbuf, *namebuf; + size_t namelen, maillen; + struct ident_split ident; + + if (split_ident_line(&ident, in, strlen(in))) + return -1; + + namebuf = ident.name_begin; + mailbuf = ident.mail_begin; + namelen = ident.name_end - ident.name_begin; + maillen = ident.mail_end - ident.mail_begin; + + map_user(&log->mailmap, &mailbuf, &maillen, &namebuf, &namelen); + strbuf_add(out, namebuf, namelen); + if (log->email) + strbuf_addf(out, " <%.*s>", (int)maillen, mailbuf); + + return 0; +} + +static void read_from_stdin(struct shortlog *log) +{ + struct strbuf ident = STRBUF_INIT; + struct strbuf mapped_ident = STRBUF_INIT; + struct strbuf oneline = STRBUF_INIT; + static const char *author_match[2] = { "Author: ", "author " }; + static const char *committer_match[2] = { "Commit: ", "committer " }; + const char **match; + + if (HAS_MULTI_BITS(log->groups)) + die(_("using multiple --group options with stdin is not supported")); + + switch (log->groups) { + case SHORTLOG_GROUP_AUTHOR: + match = author_match; + break; + case SHORTLOG_GROUP_COMMITTER: + match = committer_match; + break; + case SHORTLOG_GROUP_TRAILER: + die(_("using %s with stdin is not supported"), "--group=trailer"); + case SHORTLOG_GROUP_FORMAT: + die(_("using %s with stdin is not supported"), "--group=format"); + default: + BUG("unhandled shortlog group"); + } + + while (strbuf_getline_lf(&ident, stdin) != EOF) { + const char *v; + if (!skip_prefix(ident.buf, match[0], &v) && + !skip_prefix(ident.buf, match[1], &v)) + continue; + while (strbuf_getline_lf(&oneline, stdin) != EOF && + oneline.len) + ; /* discard headers */ + while (strbuf_getline_lf(&oneline, stdin) != EOF && + !oneline.len) + ; /* discard blanks */ + + strbuf_reset(&mapped_ident); + if (parse_ident(log, &mapped_ident, v) < 0) + continue; + + insert_one_record(log, mapped_ident.buf, oneline.buf); + } + strbuf_release(&ident); + strbuf_release(&mapped_ident); + strbuf_release(&oneline); +} + +static void insert_records_from_trailers(struct shortlog *log, + struct strset *dups, + struct commit *commit, + struct pretty_print_context *ctx, + const char *oneline) +{ + struct trailer_iterator iter; + const char *commit_buffer, *body; + struct strbuf ident = STRBUF_INIT; + + if (!log->trailers.nr) + return; + + /* + * Using repo_format_commit_message("%B") would be simpler here, but + * this saves us copying the message. + */ + commit_buffer = repo_logmsg_reencode(the_repository, commit, NULL, + ctx->output_encoding); + body = strstr(commit_buffer, "\n\n"); + if (!body) + return; + + trailer_iterator_init(&iter, body); + while (trailer_iterator_advance(&iter)) { + const char *value = iter.val.buf; + + if (!string_list_has_string(&log->trailers, iter.key.buf)) + continue; + + strbuf_reset(&ident); + if (!parse_ident(log, &ident, value)) + value = ident.buf; + + if (!strset_add(dups, value)) + continue; + insert_one_record(log, value, oneline); + } + trailer_iterator_release(&iter); + + strbuf_release(&ident); + repo_unuse_commit_buffer(the_repository, commit, commit_buffer); +} + +static int shortlog_needs_dedup(const struct shortlog *log) +{ + return HAS_MULTI_BITS(log->groups) || log->format.nr > 1 || log->trailers.nr; +} + +static void insert_records_from_format(struct shortlog *log, + struct strset *dups, + struct commit *commit, + struct pretty_print_context *ctx, + const char *oneline) +{ + struct strbuf buf = STRBUF_INIT; + struct string_list_item *item; + + for_each_string_list_item(item, &log->format) { + strbuf_reset(&buf); + + repo_format_commit_message(the_repository, commit, + item->string, &buf, ctx); + + if (!shortlog_needs_dedup(log) || strset_add(dups, buf.buf)) + insert_one_record(log, buf.buf, oneline); + } + + strbuf_release(&buf); +} + +void shortlog_add_commit(struct shortlog *log, struct commit *commit) +{ + struct strbuf oneline = STRBUF_INIT; + struct strset dups = STRSET_INIT; + struct pretty_print_context ctx = {0}; + const char *oneline_str; + + ctx.fmt = CMIT_FMT_USERFORMAT; + ctx.abbrev = log->abbrev; + ctx.print_email_subject = 1; + ctx.date_mode = log->date_mode; + ctx.output_encoding = get_log_output_encoding(); + + if (!log->summary) { + if (log->user_format) + pretty_print_commit(&ctx, commit, &oneline); + else + repo_format_commit_message(the_repository, commit, + "%s", &oneline, &ctx); + } + oneline_str = oneline.len ? oneline.buf : "<none>"; + + insert_records_from_trailers(log, &dups, commit, &ctx, oneline_str); + insert_records_from_format(log, &dups, commit, &ctx, oneline_str); + + strset_clear(&dups); + strbuf_release(&oneline); +} + +static void get_from_rev(struct rev_info *rev, struct shortlog *log) +{ + struct commit *commit; + + if (prepare_revision_walk(rev)) + die(_("revision walk setup failed")); + while ((commit = get_revision(rev)) != NULL) + shortlog_add_commit(log, commit); +} + +static int parse_uint(char const **arg, int comma, int defval) +{ + unsigned long ul; + int ret; + char *endp; + + ul = strtoul(*arg, &endp, 10); + if (*endp && *endp != comma) + return -1; + if (ul > INT_MAX) + return -1; + ret = *arg == endp ? defval : (int)ul; + *arg = *endp ? endp + 1 : endp; + return ret; +} + +static const char wrap_arg_usage[] = "-w[<width>[,<indent1>[,<indent2>]]]"; +#define DEFAULT_WRAPLEN 76 +#define DEFAULT_INDENT1 6 +#define DEFAULT_INDENT2 9 + +static int parse_wrap_args(const struct option *opt, const char *arg, int unset) +{ + struct shortlog *log = opt->value; + + log->wrap_lines = !unset; + if (unset) + return 0; + if (!arg) { + log->wrap = DEFAULT_WRAPLEN; + log->in1 = DEFAULT_INDENT1; + log->in2 = DEFAULT_INDENT2; + return 0; + } + + log->wrap = parse_uint(&arg, ',', DEFAULT_WRAPLEN); + log->in1 = parse_uint(&arg, ',', DEFAULT_INDENT1); + log->in2 = parse_uint(&arg, '\0', DEFAULT_INDENT2); + if (log->wrap < 0 || log->in1 < 0 || log->in2 < 0) + return error(wrap_arg_usage); + if (log->wrap && + ((log->in1 && log->wrap <= log->in1) || + (log->in2 && log->wrap <= log->in2))) + return error(wrap_arg_usage); + return 0; +} + +static int parse_group_option(const struct option *opt, const char *arg, int unset) +{ + struct shortlog *log = opt->value; + const char *field; + + if (unset) { + log->groups = 0; + string_list_clear(&log->trailers, 0); + string_list_clear(&log->format, 0); + } else if (!strcasecmp(arg, "author")) + log->groups |= SHORTLOG_GROUP_AUTHOR; + else if (!strcasecmp(arg, "committer")) + log->groups |= SHORTLOG_GROUP_COMMITTER; + else if (skip_prefix(arg, "trailer:", &field)) { + log->groups |= SHORTLOG_GROUP_TRAILER; + string_list_append(&log->trailers, field); + } else if (skip_prefix(arg, "format:", &field)) { + log->groups |= SHORTLOG_GROUP_FORMAT; + string_list_append(&log->format, field); + } else if (strchr(arg, '%')) { + log->groups |= SHORTLOG_GROUP_FORMAT; + string_list_append(&log->format, arg); + } else { + return error(_("unknown group type: %s"), arg); + } + + return 0; +} + + +void shortlog_init(struct shortlog *log) +{ + memset(log, 0, sizeof(*log)); + + read_mailmap(&log->mailmap); + + log->list.strdup_strings = 1; + log->wrap = DEFAULT_WRAPLEN; + log->in1 = DEFAULT_INDENT1; + log->in2 = DEFAULT_INDENT2; + log->trailers.strdup_strings = 1; + log->trailers.cmp = strcasecmp; + log->format.strdup_strings = 1; +} + +void shortlog_finish_setup(struct shortlog *log) +{ + if (log->groups & SHORTLOG_GROUP_AUTHOR) + string_list_append(&log->format, + log->email ? "%aN <%aE>" : "%aN"); + if (log->groups & SHORTLOG_GROUP_COMMITTER) + string_list_append(&log->format, + log->email ? "%cN <%cE>" : "%cN"); + + string_list_sort(&log->trailers); +} + +int cmd_shortlog(int argc, const char **argv, const char *prefix) +{ + struct shortlog log = { STRING_LIST_INIT_NODUP }; + struct rev_info rev; + int nongit = !startup_info->have_repository; + + const struct option options[] = { + OPT_BIT('c', "committer", &log.groups, + N_("group by committer rather than author"), + SHORTLOG_GROUP_COMMITTER), + OPT_BOOL('n', "numbered", &log.sort_by_number, + N_("sort output according to the number of commits per author")), + OPT_BOOL('s', "summary", &log.summary, + N_("suppress commit descriptions, only provides commit count")), + OPT_BOOL('e', "email", &log.email, + N_("show the email address of each author")), + OPT_CALLBACK_F('w', NULL, &log, N_("<w>[,<i1>[,<i2>]]"), + N_("linewrap output"), PARSE_OPT_OPTARG, + &parse_wrap_args), + OPT_CALLBACK(0, "group", &log, N_("field"), + N_("group by field"), parse_group_option), + OPT_END(), + }; + + struct parse_opt_ctx_t ctx; + + git_config(git_default_config, NULL); + shortlog_init(&log); + repo_init_revisions(the_repository, &rev, prefix); + parse_options_start(&ctx, argc, argv, prefix, options, + PARSE_OPT_KEEP_DASHDASH | PARSE_OPT_KEEP_ARGV0); + + for (;;) { + switch (parse_options_step(&ctx, options, shortlog_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: + goto parse_done; + } + parse_revision_opt(&rev, &ctx, options, shortlog_usage); + } +parse_done: + revision_opts_finish(&rev); + argc = parse_options_end(&ctx); + + if (nongit && argc > 1) { + error(_("too many arguments given outside repository")); + usage_with_options(shortlog_usage, options); + } + + if (setup_revisions(argc, argv, &rev, NULL) != 1) { + error(_("unrecognized argument: %s"), argv[1]); + usage_with_options(shortlog_usage, options); + } + + log.user_format = rev.commit_format == CMIT_FMT_USERFORMAT; + log.abbrev = rev.abbrev; + log.file = rev.diffopt.file; + log.date_mode = rev.date_mode; + + if (!log.groups) + log.groups = SHORTLOG_GROUP_AUTHOR; + shortlog_finish_setup(&log); + + /* assume HEAD if from a tty */ + if (!nongit && !rev.pending.nr && isatty(0)) + add_head_to_pending(&rev); + if (rev.pending.nr == 0) { + if (isatty(0)) + fprintf(stderr, _("(reading log message from standard input)\n")); + read_from_stdin(&log); + } + else + get_from_rev(&rev, &log); + + release_revisions(&rev); + + shortlog_output(&log); + if (log.file != stdout) + fclose(log.file); + return 0; +} + +static void add_wrapped_shortlog_msg(struct strbuf *sb, const char *s, + const struct shortlog *log) +{ + strbuf_add_wrapped_text(sb, s, log->in1, log->in2, log->wrap); + strbuf_addch(sb, '\n'); +} + +void shortlog_output(struct shortlog *log) +{ + size_t i, j; + struct strbuf sb = STRBUF_INIT; + + if (log->sort_by_number) + STABLE_QSORT(log->list.items, log->list.nr, + log->summary ? compare_by_counter : compare_by_list); + for (i = 0; i < log->list.nr; i++) { + const struct string_list_item *item = &log->list.items[i]; + if (log->summary) { + fprintf(log->file, "%6d\t%s\n", + (int)UTIL_TO_INT(item), item->string); + } else { + struct string_list *onelines = item->util; + fprintf(log->file, "%s (%"PRIuMAX"):\n", + item->string, (uintmax_t)onelines->nr); + for (j = onelines->nr; j >= 1; j--) { + const char *msg = onelines->items[j - 1].string; + + if (log->wrap_lines) { + strbuf_reset(&sb); + add_wrapped_shortlog_msg(&sb, msg, log); + fwrite(sb.buf, sb.len, 1, log->file); + } + else + fprintf(log->file, " %s\n", msg); + } + putc('\n', log->file); + onelines->strdup_strings = 1; + string_list_clear(onelines, 0); + free(onelines); + } + + log->list.items[i].util = NULL; + } + + strbuf_release(&sb); + log->list.strdup_strings = 1; + string_list_clear(&log->list, 1); + clear_mailmap(&log->mailmap); + string_list_clear(&log->format, 0); +} diff --git a/builtin/show-branch.c b/builtin/show-branch.c new file mode 100644 index 0000000..b01ec76 --- /dev/null +++ b/builtin/show-branch.c @@ -0,0 +1,972 @@ +#include "builtin.h" +#include "config.h" +#include "environment.h" +#include "gettext.h" +#include "hash.h" +#include "hex.h" +#include "pretty.h" +#include "refs.h" +#include "color.h" +#include "strvec.h" +#include "object-name.h" +#include "parse-options.h" +#include "repository.h" +#include "dir.h" +#include "commit-slab.h" +#include "date.h" +#include "wildmatch.h" + +static const char* show_branch_usage[] = { + N_("git show-branch [-a | --all] [-r | --remotes] [--topo-order | --date-order]\n" + " [--current] [--color[=<when>] | --no-color] [--sparse]\n" + " [--more=<n> | --list | --independent | --merge-base]\n" + " [--no-name | --sha1-name] [--topics]\n" + " [(<rev> | <glob>)...]"), + N_("git show-branch (-g | --reflog)[=<n>[,<base>]] [--list] [<ref>]"), + NULL +}; + +static int showbranch_use_color = -1; + +static struct strvec default_args = STRVEC_INIT; + +/* + * TODO: convert this use of commit->object.flags to commit-slab + * instead to store a pointer to ref name directly. Then use the same + * UNINTERESTING definition from revision.h here. + */ +#define UNINTERESTING 01 + +#define REV_SHIFT 2 +#define MAX_REVS (FLAG_BITS - REV_SHIFT) /* should not exceed bits_per_int - REV_SHIFT */ + +#define DEFAULT_REFLOG 4 + +static const char *get_color_code(int idx) +{ + if (want_color(showbranch_use_color)) + return column_colors_ansi[idx % column_colors_ansi_max]; + return ""; +} + +static const char *get_color_reset_code(void) +{ + if (want_color(showbranch_use_color)) + return GIT_COLOR_RESET; + return ""; +} + +static struct commit *interesting(struct commit_list *list) +{ + while (list) { + struct commit *commit = list->item; + list = list->next; + if (commit->object.flags & UNINTERESTING) + continue; + return commit; + } + return NULL; +} + +struct commit_name { + const char *head_name; /* which head's ancestor? */ + int generation; /* how many parents away from head_name */ +}; + +define_commit_slab(commit_name_slab, struct commit_name *); +static struct commit_name_slab name_slab; + +static struct commit_name *commit_to_name(struct commit *commit) +{ + return *commit_name_slab_at(&name_slab, commit); +} + + +/* Name the commit as nth generation ancestor of head_name; + * we count only the first-parent relationship for naming purposes. + */ +static void name_commit(struct commit *commit, const char *head_name, int nth) +{ + struct commit_name *name; + + name = *commit_name_slab_at(&name_slab, commit); + if (!name) { + name = xmalloc(sizeof(*name)); + *commit_name_slab_at(&name_slab, commit) = name; + } + name->head_name = head_name; + name->generation = nth; +} + +/* Parent is the first parent of the commit. We may name it + * as (n+1)th generation ancestor of the same head_name as + * commit is nth generation ancestor of, if that generation + * number is better than the name it already has. + */ +static void name_parent(struct commit *commit, struct commit *parent) +{ + struct commit_name *commit_name = commit_to_name(commit); + struct commit_name *parent_name = commit_to_name(parent); + if (!commit_name) + return; + if (!parent_name || + commit_name->generation + 1 < parent_name->generation) + name_commit(parent, commit_name->head_name, + commit_name->generation + 1); +} + +static int name_first_parent_chain(struct commit *c) +{ + int i = 0; + while (c) { + struct commit *p; + if (!commit_to_name(c)) + break; + if (!c->parents) + break; + p = c->parents->item; + if (!commit_to_name(p)) { + name_parent(c, p); + i++; + } + else + break; + c = p; + } + return i; +} + +static void name_commits(struct commit_list *list, + struct commit **rev, + char **ref_name, + int num_rev) +{ + struct commit_list *cl; + struct commit *c; + int i; + + /* First give names to the given heads */ + for (cl = list; cl; cl = cl->next) { + c = cl->item; + if (commit_to_name(c)) + continue; + for (i = 0; i < num_rev; i++) { + if (rev[i] == c) { + name_commit(c, ref_name[i], 0); + break; + } + } + } + + /* Then commits on the first parent ancestry chain */ + do { + i = 0; + for (cl = list; cl; cl = cl->next) { + i += name_first_parent_chain(cl->item); + } + } while (i); + + /* Finally, any unnamed commits */ + do { + i = 0; + for (cl = list; cl; cl = cl->next) { + struct commit_list *parents; + struct commit_name *n; + int nth; + c = cl->item; + if (!commit_to_name(c)) + continue; + n = commit_to_name(c); + parents = c->parents; + nth = 0; + while (parents) { + struct commit *p = parents->item; + struct strbuf newname = STRBUF_INIT; + parents = parents->next; + nth++; + if (commit_to_name(p)) + continue; + switch (n->generation) { + case 0: + strbuf_addstr(&newname, n->head_name); + break; + case 1: + strbuf_addf(&newname, "%s^", n->head_name); + break; + default: + strbuf_addf(&newname, "%s~%d", + n->head_name, n->generation); + break; + } + if (nth == 1) + strbuf_addch(&newname, '^'); + else + strbuf_addf(&newname, "^%d", nth); + name_commit(p, strbuf_detach(&newname, NULL), 0); + i++; + name_first_parent_chain(p); + } + } + } while (i); +} + +static int mark_seen(struct commit *commit, struct commit_list **seen_p) +{ + if (!commit->object.flags) { + commit_list_insert(commit, seen_p); + return 1; + } + return 0; +} + +static void join_revs(struct commit_list **list_p, + struct commit_list **seen_p, + int num_rev, int extra) +{ + int all_mask = ((1u << (REV_SHIFT + num_rev)) - 1); + int all_revs = all_mask & ~((1u << REV_SHIFT) - 1); + + while (*list_p) { + struct commit_list *parents; + int still_interesting = !!interesting(*list_p); + struct commit *commit = pop_commit(list_p); + int flags = commit->object.flags & all_mask; + + if (!still_interesting && extra <= 0) + break; + + mark_seen(commit, seen_p); + if ((flags & all_revs) == all_revs) + flags |= UNINTERESTING; + parents = commit->parents; + + while (parents) { + struct commit *p = parents->item; + int this_flag = p->object.flags; + parents = parents->next; + if ((this_flag & flags) == flags) + continue; + repo_parse_commit(the_repository, p); + if (mark_seen(p, seen_p) && !still_interesting) + extra--; + p->object.flags |= flags; + commit_list_insert_by_date(p, list_p); + } + } + + /* + * Postprocess to complete well-poisoning. + * + * At this point we have all the commits we have seen in + * seen_p list. Mark anything that can be reached from + * uninteresting commits not interesting. + */ + for (;;) { + int changed = 0; + struct commit_list *s; + for (s = *seen_p; s; s = s->next) { + struct commit *c = s->item; + struct commit_list *parents; + + if (((c->object.flags & all_revs) != all_revs) && + !(c->object.flags & UNINTERESTING)) + continue; + + /* The current commit is either a merge base or + * already uninteresting one. Mark its parents + * as uninteresting commits _only_ if they are + * already parsed. No reason to find new ones + * here. + */ + parents = c->parents; + while (parents) { + struct commit *p = parents->item; + parents = parents->next; + if (!(p->object.flags & UNINTERESTING)) { + p->object.flags |= UNINTERESTING; + changed = 1; + } + } + } + if (!changed) + break; + } +} + +static void show_one_commit(struct commit *commit, int no_name) +{ + struct strbuf pretty = STRBUF_INIT; + const char *pretty_str = "(unavailable)"; + struct commit_name *name = commit_to_name(commit); + + if (commit->object.parsed) { + pp_commit_easy(CMIT_FMT_ONELINE, commit, &pretty); + pretty_str = pretty.buf; + } + skip_prefix(pretty_str, "[PATCH] ", &pretty_str); + + if (!no_name) { + if (name && name->head_name) { + printf("[%s", name->head_name); + if (name->generation) { + if (name->generation == 1) + printf("^"); + else + printf("~%d", name->generation); + } + printf("] "); + } + else + printf("[%s] ", + repo_find_unique_abbrev(the_repository, &commit->object.oid, + DEFAULT_ABBREV)); + } + puts(pretty_str); + strbuf_release(&pretty); +} + +static char *ref_name[MAX_REVS + 1]; +static int ref_name_cnt; + +static const char *find_digit_prefix(const char *s, int *v) +{ + const char *p; + int ver; + char ch; + + for (p = s, ver = 0; + '0' <= (ch = *p) && ch <= '9'; + p++) + ver = ver * 10 + ch - '0'; + *v = ver; + return p; +} + + +static int version_cmp(const char *a, const char *b) +{ + while (1) { + int va, vb; + + a = find_digit_prefix(a, &va); + b = find_digit_prefix(b, &vb); + if (va != vb) + return va - vb; + + while (1) { + int ca = *a; + int cb = *b; + if ('0' <= ca && ca <= '9') + ca = 0; + if ('0' <= cb && cb <= '9') + cb = 0; + if (ca != cb) + return ca - cb; + if (!ca) + break; + a++; + b++; + } + if (!*a && !*b) + return 0; + } +} + +static int compare_ref_name(const void *a_, const void *b_) +{ + const char * const*a = a_, * const*b = b_; + return version_cmp(*a, *b); +} + +static void sort_ref_range(int bottom, int top) +{ + QSORT(ref_name + bottom, top - bottom, compare_ref_name); +} + +static int append_ref(const char *refname, const struct object_id *oid, + int allow_dups) +{ + struct commit *commit = lookup_commit_reference_gently(the_repository, + oid, 1); + int i; + + if (!commit) + return 0; + + if (!allow_dups) { + /* Avoid adding the same thing twice */ + for (i = 0; i < ref_name_cnt; i++) + if (!strcmp(refname, ref_name[i])) + return 0; + } + if (MAX_REVS <= ref_name_cnt) { + warning(Q_("ignoring %s; cannot handle more than %d ref", + "ignoring %s; cannot handle more than %d refs", + MAX_REVS), refname, MAX_REVS); + return 0; + } + ref_name[ref_name_cnt++] = xstrdup(refname); + ref_name[ref_name_cnt] = NULL; + return 0; +} + +static int append_head_ref(const char *refname, const struct object_id *oid, + int flag UNUSED, void *cb_data UNUSED) +{ + struct object_id tmp; + int ofs = 11; + if (!starts_with(refname, "refs/heads/")) + return 0; + /* If both heads/foo and tags/foo exists, get_sha1 would + * get confused. + */ + if (repo_get_oid(the_repository, refname + ofs, &tmp) || !oideq(&tmp, oid)) + ofs = 5; + return append_ref(refname + ofs, oid, 0); +} + +static int append_remote_ref(const char *refname, const struct object_id *oid, + int flag UNUSED, void *cb_data UNUSED) +{ + struct object_id tmp; + int ofs = 13; + if (!starts_with(refname, "refs/remotes/")) + return 0; + /* If both heads/foo and tags/foo exists, get_sha1 would + * get confused. + */ + if (repo_get_oid(the_repository, refname + ofs, &tmp) || !oideq(&tmp, oid)) + ofs = 5; + return append_ref(refname + ofs, oid, 0); +} + +static int append_tag_ref(const char *refname, const struct object_id *oid, + int flag UNUSED, void *cb_data UNUSED) +{ + if (!starts_with(refname, "refs/tags/")) + return 0; + return append_ref(refname + 5, oid, 0); +} + +static const char *match_ref_pattern = NULL; +static int match_ref_slash = 0; + +static int append_matching_ref(const char *refname, const struct object_id *oid, + int flag, void *cb_data) +{ + /* we want to allow pattern hold/<asterisk> to show all + * branches under refs/heads/hold/, and v0.99.9? to show + * refs/tags/v0.99.9a and friends. + */ + const char *tail; + int slash = count_slashes(refname); + for (tail = refname; *tail && match_ref_slash < slash; ) + if (*tail++ == '/') + slash--; + if (!*tail) + return 0; + if (wildmatch(match_ref_pattern, tail, 0)) + return 0; + if (starts_with(refname, "refs/heads/")) + return append_head_ref(refname, oid, flag, cb_data); + if (starts_with(refname, "refs/tags/")) + return append_tag_ref(refname, oid, flag, cb_data); + return append_ref(refname, oid, 0); +} + +static void snarf_refs(int head, int remotes) +{ + if (head) { + int orig_cnt = ref_name_cnt; + + for_each_ref(append_head_ref, NULL); + sort_ref_range(orig_cnt, ref_name_cnt); + } + if (remotes) { + int orig_cnt = ref_name_cnt; + + for_each_ref(append_remote_ref, NULL); + sort_ref_range(orig_cnt, ref_name_cnt); + } +} + +static int rev_is_head(const char *head, const char *name) +{ + if (!head) + return 0; + skip_prefix(head, "refs/heads/", &head); + if (!skip_prefix(name, "refs/heads/", &name)) + skip_prefix(name, "heads/", &name); + return !strcmp(head, name); +} + +static int show_merge_base(struct commit_list *seen, int num_rev) +{ + int all_mask = ((1u << (REV_SHIFT + num_rev)) - 1); + int all_revs = all_mask & ~((1u << REV_SHIFT) - 1); + int exit_status = 1; + + while (seen) { + struct commit *commit = pop_commit(&seen); + int flags = commit->object.flags & all_mask; + if (!(flags & UNINTERESTING) && + ((flags & all_revs) == all_revs)) { + puts(oid_to_hex(&commit->object.oid)); + exit_status = 0; + commit->object.flags |= UNINTERESTING; + } + } + return exit_status; +} + +static int show_independent(struct commit **rev, + int num_rev, + unsigned int *rev_mask) +{ + int i; + + for (i = 0; i < num_rev; i++) { + struct commit *commit = rev[i]; + unsigned int flag = rev_mask[i]; + + if (commit->object.flags == flag) + puts(oid_to_hex(&commit->object.oid)); + commit->object.flags |= UNINTERESTING; + } + return 0; +} + +static void append_one_rev(const char *av) +{ + struct object_id revkey; + if (!repo_get_oid(the_repository, av, &revkey)) { + append_ref(av, &revkey, 0); + return; + } + if (strpbrk(av, "*?[")) { + /* glob style match */ + int saved_matches = ref_name_cnt; + + match_ref_pattern = av; + match_ref_slash = count_slashes(av); + for_each_ref(append_matching_ref, NULL); + if (saved_matches == ref_name_cnt && + ref_name_cnt < MAX_REVS) + error(_("no matching refs with %s"), av); + sort_ref_range(saved_matches, ref_name_cnt); + return; + } + die("bad sha1 reference %s", av); +} + +static int git_show_branch_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + if (!strcmp(var, "showbranch.default")) { + if (!value) + return config_error_nonbool(var); + /* + * default_arg is now passed to parse_options(), so we need to + * mimic the real argv a bit better. + */ + if (!default_args.nr) + strvec_push(&default_args, "show-branch"); + strvec_push(&default_args, value); + return 0; + } + + if (!strcmp(var, "color.showbranch")) { + showbranch_use_color = git_config_colorbool(var, value); + return 0; + } + + if (git_color_config(var, value, cb) < 0) + return -1; + + return git_default_config(var, value, ctx, cb); +} + +static int omit_in_dense(struct commit *commit, struct commit **rev, int n) +{ + /* If the commit is tip of the named branches, do not + * omit it. + * Otherwise, if it is a merge that is reachable from only one + * tip, it is not that interesting. + */ + int i, flag, count; + for (i = 0; i < n; i++) + if (rev[i] == commit) + return 0; + flag = commit->object.flags; + for (i = count = 0; i < n; i++) { + if (flag & (1u << (i + REV_SHIFT))) + count++; + } + if (count == 1) + return 1; + return 0; +} + +static int reflog = 0; + +static int parse_reflog_param(const struct option *opt, const char *arg, + int unset) +{ + char *ep; + const char **base = (const char **)opt->value; + BUG_ON_OPT_NEG(unset); + if (!arg) + arg = ""; + reflog = strtoul(arg, &ep, 10); + if (*ep == ',') + *base = ep + 1; + else if (*ep) + return error("unrecognized reflog param '%s'", arg); + else + *base = NULL; + if (reflog <= 0) + reflog = DEFAULT_REFLOG; + return 0; +} + +int cmd_show_branch(int ac, const char **av, const char *prefix) +{ + struct commit *rev[MAX_REVS], *commit; + char *reflog_msg[MAX_REVS]; + struct commit_list *list = NULL, *seen = NULL; + unsigned int rev_mask[MAX_REVS]; + int num_rev, i, extra = 0; + int all_heads = 0, all_remotes = 0; + int all_mask, all_revs; + enum rev_sort_order sort_order = REV_SORT_IN_GRAPH_ORDER; + char *head; + struct object_id head_oid; + int merge_base = 0; + int independent = 0; + int no_name = 0; + int sha1_name = 0; + int shown_merge_point = 0; + int with_current_branch = 0; + int head_at = -1; + int topics = 0; + int sparse = 0; + const char *reflog_base = NULL; + struct option builtin_show_branch_options[] = { + OPT_BOOL('a', "all", &all_heads, + N_("show remote-tracking and local branches")), + OPT_BOOL('r', "remotes", &all_remotes, + N_("show remote-tracking branches")), + OPT__COLOR(&showbranch_use_color, + N_("color '*!+-' corresponding to the branch")), + { OPTION_INTEGER, 0, "more", &extra, N_("n"), + N_("show <n> more commits after the common ancestor"), + PARSE_OPT_OPTARG, NULL, (intptr_t)1 }, + OPT_SET_INT(0, "list", &extra, N_("synonym to more=-1"), -1), + OPT_BOOL(0, "no-name", &no_name, N_("suppress naming strings")), + OPT_BOOL(0, "current", &with_current_branch, + N_("include the current branch")), + OPT_BOOL(0, "sha1-name", &sha1_name, + N_("name commits with their object names")), + OPT_BOOL(0, "merge-base", &merge_base, + N_("show possible merge bases")), + OPT_BOOL(0, "independent", &independent, + N_("show refs unreachable from any other ref")), + OPT_SET_INT_F(0, "topo-order", &sort_order, + N_("show commits in topological order"), + REV_SORT_IN_GRAPH_ORDER, PARSE_OPT_NONEG), + OPT_BOOL(0, "topics", &topics, + N_("show only commits not on the first branch")), + OPT_SET_INT(0, "sparse", &sparse, + N_("show merges reachable from only one tip"), 1), + OPT_SET_INT_F(0, "date-order", &sort_order, + N_("topologically sort, maintaining date order " + "where possible"), + REV_SORT_BY_COMMIT_DATE, PARSE_OPT_NONEG), + OPT_CALLBACK_F('g', "reflog", &reflog_base, N_("<n>[,<base>]"), + N_("show <n> most recent ref-log entries starting at " + "base"), + PARSE_OPT_OPTARG | PARSE_OPT_NONEG, + parse_reflog_param), + OPT_END() + }; + + init_commit_name_slab(&name_slab); + + git_config(git_show_branch_config, NULL); + + /* If nothing is specified, try the default first */ + if (ac == 1 && default_args.nr) { + ac = default_args.nr; + av = default_args.v; + } + + ac = parse_options(ac, av, prefix, builtin_show_branch_options, + show_branch_usage, PARSE_OPT_STOP_AT_NON_OPTION); + if (all_heads) + all_remotes = 1; + + if (extra || reflog) { + /* "listing" mode is incompatible with + * independent nor merge-base modes. + */ + if (independent || merge_base) + usage_with_options(show_branch_usage, + builtin_show_branch_options); + if (reflog && ((0 < extra) || all_heads || all_remotes)) + /* + * Asking for --more in reflog mode does not + * make sense. --list is Ok. + * + * Also --all and --remotes do not make sense either. + */ + die(_("options '%s' and '%s' cannot be used together"), "--reflog", + "--all/--remotes/--independent/--merge-base"); + } + + if (with_current_branch && reflog) + die(_("options '%s' and '%s' cannot be used together"), + "--reflog", "--current"); + + /* If nothing is specified, show all branches by default */ + if (ac <= topics && all_heads + all_remotes == 0) + all_heads = 1; + + if (reflog) { + struct object_id oid; + char *ref; + int base = 0; + unsigned int flags = 0; + + if (ac == 0) { + static const char *fake_av[2]; + + fake_av[0] = resolve_refdup("HEAD", + RESOLVE_REF_READING, &oid, + NULL); + fake_av[1] = NULL; + av = fake_av; + ac = 1; + if (!*av) + die(_("no branches given, and HEAD is not valid")); + } + if (ac != 1) + die(_("--reflog option needs one branch name")); + + if (MAX_REVS < reflog) + die(Q_("only %d entry can be shown at one time.", + "only %d entries can be shown at one time.", + MAX_REVS), MAX_REVS); + if (!repo_dwim_ref(the_repository, *av, strlen(*av), &oid, + &ref, 0)) + die(_("no such ref %s"), *av); + + /* Has the base been specified? */ + if (reflog_base) { + char *ep; + base = strtoul(reflog_base, &ep, 10); + if (*ep) { + /* Ah, that is a date spec... */ + timestamp_t at; + at = approxidate(reflog_base); + read_ref_at(get_main_ref_store(the_repository), + ref, flags, at, -1, &oid, NULL, + NULL, NULL, &base); + } + } + + for (i = 0; i < reflog; i++) { + char *logmsg; + char *nth_desc; + const char *msg; + char *end; + timestamp_t timestamp; + int tz; + + if (read_ref_at(get_main_ref_store(the_repository), + ref, flags, 0, base + i, &oid, &logmsg, + ×tamp, &tz, NULL)) { + reflog = i; + break; + } + + end = strchr(logmsg, '\n'); + if (end) + *end = '\0'; + + msg = (*logmsg == '\0') ? "(none)" : logmsg; + reflog_msg[i] = xstrfmt("(%s) %s", + show_date(timestamp, tz, + DATE_MODE(RELATIVE)), + msg); + free(logmsg); + + nth_desc = xstrfmt("%s@{%d}", *av, base+i); + append_ref(nth_desc, &oid, 1); + free(nth_desc); + } + free(ref); + } + else { + while (0 < ac) { + append_one_rev(*av); + ac--; av++; + } + if (all_heads + all_remotes) + snarf_refs(all_heads, all_remotes); + } + + head = resolve_refdup("HEAD", RESOLVE_REF_READING, + &head_oid, NULL); + + if (with_current_branch && head) { + int has_head = 0; + for (i = 0; !has_head && i < ref_name_cnt; i++) { + /* We are only interested in adding the branch + * HEAD points at. + */ + if (rev_is_head(head, ref_name[i])) + has_head++; + } + if (!has_head) { + const char *name = head; + skip_prefix(name, "refs/heads/", &name); + append_one_rev(name); + } + } + + if (!ref_name_cnt) { + fprintf(stderr, "No revs to be shown.\n"); + exit(0); + } + + for (num_rev = 0; ref_name[num_rev]; num_rev++) { + struct object_id revkey; + unsigned int flag = 1u << (num_rev + REV_SHIFT); + + if (MAX_REVS <= num_rev) + die(Q_("cannot handle more than %d rev.", + "cannot handle more than %d revs.", + MAX_REVS), MAX_REVS); + if (repo_get_oid(the_repository, ref_name[num_rev], &revkey)) + die(_("'%s' is not a valid ref."), ref_name[num_rev]); + commit = lookup_commit_reference(the_repository, &revkey); + if (!commit) + die(_("cannot find commit %s (%s)"), + ref_name[num_rev], oid_to_hex(&revkey)); + repo_parse_commit(the_repository, commit); + mark_seen(commit, &seen); + + /* rev#0 uses bit REV_SHIFT, rev#1 uses bit REV_SHIFT+1, + * and so on. REV_SHIFT bits from bit 0 are used for + * internal bookkeeping. + */ + commit->object.flags |= flag; + if (commit->object.flags == flag) + commit_list_insert_by_date(commit, &list); + rev[num_rev] = commit; + } + for (i = 0; i < num_rev; i++) + rev_mask[i] = rev[i]->object.flags; + + if (0 <= extra) + join_revs(&list, &seen, num_rev, extra); + + commit_list_sort_by_date(&seen); + + if (merge_base) + return show_merge_base(seen, num_rev); + + if (independent) + return show_independent(rev, num_rev, rev_mask); + + /* Show list; --more=-1 means list-only */ + if (1 < num_rev || extra < 0) { + for (i = 0; i < num_rev; i++) { + int j; + int is_head = rev_is_head(head, ref_name[i]) && + oideq(&head_oid, &rev[i]->object.oid); + if (extra < 0) + printf("%c [%s] ", + is_head ? '*' : ' ', ref_name[i]); + else { + for (j = 0; j < i; j++) + putchar(' '); + printf("%s%c%s [%s] ", + get_color_code(i), + is_head ? '*' : '!', + get_color_reset_code(), ref_name[i]); + } + + if (!reflog) { + /* header lines never need name */ + show_one_commit(rev[i], 1); + } + else + puts(reflog_msg[i]); + + if (is_head) + head_at = i; + } + if (0 <= extra) { + for (i = 0; i < num_rev; i++) + putchar('-'); + putchar('\n'); + } + } + if (extra < 0) + exit(0); + + /* Sort topologically */ + sort_in_topological_order(&seen, sort_order); + + /* Give names to commits */ + if (!sha1_name && !no_name) + name_commits(seen, rev, ref_name, num_rev); + + all_mask = ((1u << (REV_SHIFT + num_rev)) - 1); + all_revs = all_mask & ~((1u << REV_SHIFT) - 1); + + while (seen) { + struct commit *commit = pop_commit(&seen); + int this_flag = commit->object.flags; + int is_merge_point = ((this_flag & all_revs) == all_revs); + + shown_merge_point |= is_merge_point; + + if (1 < num_rev) { + int is_merge = !!(commit->parents && + commit->parents->next); + if (topics && + !is_merge_point && + (this_flag & (1u << REV_SHIFT))) + continue; + if (!sparse && is_merge && + omit_in_dense(commit, rev, num_rev)) + continue; + for (i = 0; i < num_rev; i++) { + int mark; + if (!(this_flag & (1u << (i + REV_SHIFT)))) + mark = ' '; + else if (is_merge) + mark = '-'; + else if (i == head_at) + mark = '*'; + else + mark = '+'; + if (mark == ' ') + putchar(mark); + else + printf("%s%c%s", + get_color_code(i), + mark, get_color_reset_code()); + } + putchar(' '); + } + show_one_commit(commit, no_name); + + if (shown_merge_point && --extra < 0) + break; + } + free(head); + return 0; +} diff --git a/builtin/show-index.c b/builtin/show-index.c new file mode 100644 index 0000000..540dc3d --- /dev/null +++ b/builtin/show-index.c @@ -0,0 +1,111 @@ +#include "builtin.h" +#include "gettext.h" +#include "hash.h" +#include "hex.h" +#include "pack.h" +#include "parse-options.h" +#include "repository.h" + +static const char *const show_index_usage[] = { + "git show-index [--object-format=<hash-algorithm>]", + NULL +}; + +int cmd_show_index(int argc, const char **argv, const char *prefix) +{ + int i; + unsigned nr; + unsigned int version; + static unsigned int top_index[256]; + unsigned hashsz; + const char *hash_name = NULL; + int hash_algo; + const struct option show_index_options[] = { + OPT_STRING(0, "object-format", &hash_name, N_("hash-algorithm"), + N_("specify the hash algorithm to use")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, show_index_options, show_index_usage, 0); + + if (hash_name) { + hash_algo = hash_algo_by_name(hash_name); + if (hash_algo == GIT_HASH_UNKNOWN) + die(_("Unknown hash algorithm")); + repo_set_hash_algo(the_repository, hash_algo); + } + + hashsz = the_hash_algo->rawsz; + + if (fread(top_index, 2 * 4, 1, stdin) != 1) + die("unable to read header"); + if (top_index[0] == htonl(PACK_IDX_SIGNATURE)) { + version = ntohl(top_index[1]); + if (version < 2 || version > 2) + die("unknown index version"); + if (fread(top_index, 256 * 4, 1, stdin) != 1) + die("unable to read index"); + } else { + version = 1; + if (fread(&top_index[2], 254 * 4, 1, stdin) != 1) + die("unable to read index"); + } + nr = 0; + for (i = 0; i < 256; i++) { + unsigned n = ntohl(top_index[i]); + if (n < nr) + die("corrupt index file"); + nr = n; + } + if (version == 1) { + for (i = 0; i < nr; i++) { + unsigned int offset, entry[(GIT_MAX_RAWSZ + 4) / sizeof(unsigned int)]; + + if (fread(entry, 4 + hashsz, 1, stdin) != 1) + die("unable to read entry %u/%u", i, nr); + offset = ntohl(entry[0]); + printf("%u %s\n", offset, hash_to_hex((void *)(entry+1))); + } + } else { + unsigned off64_nr = 0; + struct { + struct object_id oid; + uint32_t crc; + uint32_t off; + } *entries; + ALLOC_ARRAY(entries, nr); + for (i = 0; i < nr; i++) { + if (fread(entries[i].oid.hash, hashsz, 1, stdin) != 1) + die("unable to read sha1 %u/%u", i, nr); + entries[i].oid.algo = hash_algo_by_ptr(the_hash_algo); + } + for (i = 0; i < nr; i++) + if (fread(&entries[i].crc, 4, 1, stdin) != 1) + die("unable to read crc %u/%u", i, nr); + for (i = 0; i < nr; i++) + if (fread(&entries[i].off, 4, 1, stdin) != 1) + die("unable to read 32b offset %u/%u", i, nr); + for (i = 0; i < nr; i++) { + uint64_t offset; + uint32_t off = ntohl(entries[i].off); + if (!(off & 0x80000000)) { + offset = off; + } else { + uint32_t off64[2]; + if ((off & 0x7fffffff) != off64_nr) + die("inconsistent 64b offset index"); + if (fread(off64, 8, 1, stdin) != 1) + die("unable to read 64b offset %u", off64_nr); + offset = (((uint64_t)ntohl(off64[0])) << 32) | + ntohl(off64[1]); + off64_nr++; + } + printf("%" PRIuMAX " %s (%08"PRIx32")\n", + (uintmax_t) offset, + oid_to_hex(&entries[i].oid), + ntohl(entries[i].crc)); + } + free(entries); + } + return 0; +} diff --git a/builtin/show-ref.c b/builtin/show-ref.c new file mode 100644 index 0000000..7aac525 --- /dev/null +++ b/builtin/show-ref.c @@ -0,0 +1,330 @@ +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "hex.h" +#include "refs/refs-internal.h" +#include "object-name.h" +#include "object-store-ll.h" +#include "object.h" +#include "tag.h" +#include "string-list.h" +#include "parse-options.h" + +static const char * const show_ref_usage[] = { + N_("git show-ref [--head] [-d | --dereference]\n" + " [-s | --hash[=<n>]] [--abbrev[=<n>]] [--tags]\n" + " [--heads] [--] [<pattern>...]"), + N_("git show-ref --verify [-q | --quiet] [-d | --dereference]\n" + " [-s | --hash[=<n>]] [--abbrev[=<n>]]\n" + " [--] [<ref>...]"), + N_("git show-ref --exclude-existing[=<pattern>]"), + N_("git show-ref --exists <ref>"), + NULL +}; + +struct show_one_options { + int quiet; + int hash_only; + int abbrev; + int deref_tags; +}; + +static void show_one(const struct show_one_options *opts, + const char *refname, const struct object_id *oid) +{ + const char *hex; + struct object_id peeled; + + if (!repo_has_object_file(the_repository, oid)) + die("git show-ref: bad ref %s (%s)", refname, + oid_to_hex(oid)); + + if (opts->quiet) + return; + + hex = repo_find_unique_abbrev(the_repository, oid, opts->abbrev); + if (opts->hash_only) + printf("%s\n", hex); + else + printf("%s %s\n", hex, refname); + + if (!opts->deref_tags) + return; + + if (!peel_iterated_oid(oid, &peeled)) { + hex = repo_find_unique_abbrev(the_repository, &peeled, opts->abbrev); + printf("%s %s^{}\n", hex, refname); + } +} + +struct show_ref_data { + const struct show_one_options *show_one_opts; + const char **patterns; + int found_match; + int show_head; +}; + +static int show_ref(const char *refname, const struct object_id *oid, + int flag UNUSED, void *cbdata) +{ + struct show_ref_data *data = cbdata; + + if (data->show_head && !strcmp(refname, "HEAD")) + goto match; + + if (data->patterns) { + int reflen = strlen(refname); + const char **p = data->patterns, *m; + while ((m = *p++) != NULL) { + int len = strlen(m); + if (len > reflen) + continue; + if (memcmp(m, refname + reflen - len, len)) + continue; + if (len == reflen) + goto match; + if (refname[reflen - len - 1] == '/') + goto match; + } + return 0; + } + +match: + data->found_match++; + + show_one(data->show_one_opts, refname, oid); + + return 0; +} + +static int add_existing(const char *refname, + const struct object_id *oid UNUSED, + int flag UNUSED, void *cbdata) +{ + struct string_list *list = (struct string_list *)cbdata; + string_list_insert(list, refname); + return 0; +} + +struct exclude_existing_options { + /* + * We need an explicit `enabled` field because it is perfectly valid + * for `pattern` to be `NULL` even if `--exclude-existing` was given. + */ + int enabled; + const char *pattern; +}; + +/* + * read "^(?:<anything>\s)?<refname>(?:\^\{\})?$" from the standard input, + * and + * (1) strip "^{}" at the end of line if any; + * (2) ignore if match is provided and does not head-match refname; + * (3) warn if refname is not a well-formed refname and skip; + * (4) ignore if refname is a ref that exists in the local repository; + * (5) otherwise output the line. + */ +static int cmd_show_ref__exclude_existing(const struct exclude_existing_options *opts) +{ + struct string_list existing_refs = STRING_LIST_INIT_DUP; + char buf[1024]; + int patternlen = opts->pattern ? strlen(opts->pattern) : 0; + + for_each_ref(add_existing, &existing_refs); + while (fgets(buf, sizeof(buf), stdin)) { + char *ref; + int len = strlen(buf); + + if (len > 0 && buf[len - 1] == '\n') + buf[--len] = '\0'; + if (3 <= len && !strcmp(buf + len - 3, "^{}")) { + len -= 3; + buf[len] = '\0'; + } + for (ref = buf + len; buf < ref; ref--) + if (isspace(ref[-1])) + break; + if (opts->pattern) { + int reflen = buf + len - ref; + if (reflen < patternlen) + continue; + if (strncmp(ref, opts->pattern, patternlen)) + continue; + } + if (check_refname_format(ref, 0)) { + warning("ref '%s' ignored", ref); + continue; + } + if (!string_list_has_string(&existing_refs, ref)) { + printf("%s\n", buf); + } + } + + string_list_clear(&existing_refs, 0); + return 0; +} + +static int cmd_show_ref__verify(const struct show_one_options *show_one_opts, + const char **refs) +{ + if (!refs || !*refs) + die("--verify requires a reference"); + + while (*refs) { + struct object_id oid; + + if ((starts_with(*refs, "refs/") || !strcmp(*refs, "HEAD")) && + !read_ref(*refs, &oid)) { + show_one(show_one_opts, *refs, &oid); + } + else if (!show_one_opts->quiet) + die("'%s' - not a valid ref", *refs); + else + return 1; + refs++; + } + + return 0; +} + +struct patterns_options { + int show_head; + int heads_only; + int tags_only; +}; + +static int cmd_show_ref__patterns(const struct patterns_options *opts, + const struct show_one_options *show_one_opts, + const char **patterns) +{ + struct show_ref_data show_ref_data = { + .show_one_opts = show_one_opts, + .show_head = opts->show_head, + }; + + if (patterns && *patterns) + show_ref_data.patterns = patterns; + + if (opts->show_head) + head_ref(show_ref, &show_ref_data); + if (opts->heads_only || opts->tags_only) { + if (opts->heads_only) + for_each_fullref_in("refs/heads/", show_ref, &show_ref_data); + if (opts->tags_only) + for_each_fullref_in("refs/tags/", show_ref, &show_ref_data); + } else { + for_each_ref(show_ref, &show_ref_data); + } + if (!show_ref_data.found_match) + return 1; + + return 0; +} + +static int cmd_show_ref__exists(const char **refs) +{ + struct strbuf unused_referent = STRBUF_INIT; + struct object_id unused_oid; + unsigned int unused_type; + int failure_errno = 0; + const char *ref; + int ret = 0; + + if (!refs || !*refs) + die("--exists requires a reference"); + ref = *refs++; + if (*refs) + die("--exists requires exactly one reference"); + + if (refs_read_raw_ref(get_main_ref_store(the_repository), ref, + &unused_oid, &unused_referent, &unused_type, + &failure_errno)) { + if (failure_errno == ENOENT) { + error(_("reference does not exist")); + ret = 2; + } else { + errno = failure_errno; + error_errno(_("failed to look up reference")); + ret = 1; + } + + goto out; + } + +out: + strbuf_release(&unused_referent); + return ret; +} + +static int hash_callback(const struct option *opt, const char *arg, int unset) +{ + struct show_one_options *opts = opt->value; + struct option abbrev_opt = *opt; + + opts->hash_only = 1; + /* Use full length SHA1 if no argument */ + if (!arg) + return 0; + + abbrev_opt.value = &opts->abbrev; + return parse_opt_abbrev_cb(&abbrev_opt, arg, unset); +} + +static int exclude_existing_callback(const struct option *opt, const char *arg, + int unset) +{ + struct exclude_existing_options *opts = opt->value; + BUG_ON_OPT_NEG(unset); + opts->enabled = 1; + opts->pattern = arg; + return 0; +} + +int cmd_show_ref(int argc, const char **argv, const char *prefix) +{ + struct exclude_existing_options exclude_existing_opts = {0}; + struct patterns_options patterns_opts = {0}; + struct show_one_options show_one_opts = {0}; + int verify = 0, exists = 0; + const struct option show_ref_options[] = { + OPT_BOOL(0, "tags", &patterns_opts.tags_only, N_("only show tags (can be combined with heads)")), + OPT_BOOL(0, "heads", &patterns_opts.heads_only, N_("only show heads (can be combined with tags)")), + OPT_BOOL(0, "exists", &exists, N_("check for reference existence without resolving")), + OPT_BOOL(0, "verify", &verify, N_("stricter reference checking, " + "requires exact ref path")), + OPT_HIDDEN_BOOL('h', NULL, &patterns_opts.show_head, + N_("show the HEAD reference, even if it would be filtered out")), + OPT_BOOL(0, "head", &patterns_opts.show_head, + N_("show the HEAD reference, even if it would be filtered out")), + OPT_BOOL('d', "dereference", &show_one_opts.deref_tags, + N_("dereference tags into object IDs")), + OPT_CALLBACK_F('s', "hash", &show_one_opts, N_("n"), + N_("only show SHA1 hash using <n> digits"), + PARSE_OPT_OPTARG, &hash_callback), + OPT__ABBREV(&show_one_opts.abbrev), + OPT__QUIET(&show_one_opts.quiet, + N_("do not print results to stdout (useful with --verify)")), + OPT_CALLBACK_F(0, "exclude-existing", &exclude_existing_opts, + N_("pattern"), N_("show refs from stdin that aren't in local repository"), + PARSE_OPT_OPTARG | PARSE_OPT_NONEG, exclude_existing_callback), + OPT_END() + }; + + git_config(git_default_config, NULL); + + argc = parse_options(argc, argv, prefix, show_ref_options, + show_ref_usage, 0); + + if ((!!exclude_existing_opts.enabled + !!verify + !!exists) > 1) + die(_("only one of '%s', '%s' or '%s' can be given"), + "--exclude-existing", "--verify", "--exists"); + + if (exclude_existing_opts.enabled) + return cmd_show_ref__exclude_existing(&exclude_existing_opts); + else if (verify) + return cmd_show_ref__verify(&show_one_opts, argv); + else if (exists) + return cmd_show_ref__exists(argv); + else + return cmd_show_ref__patterns(&patterns_opts, &show_one_opts, argv); +} diff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c new file mode 100644 index 0000000..5c8ffb1 --- /dev/null +++ b/builtin/sparse-checkout.c @@ -0,0 +1,1048 @@ +#include "builtin.h" +#include "config.h" +#include "dir.h" +#include "environment.h" +#include "gettext.h" +#include "object-file.h" +#include "object-name.h" +#include "parse-options.h" +#include "pathspec.h" +#include "repository.h" +#include "run-command.h" +#include "strbuf.h" +#include "string-list.h" +#include "cache-tree.h" +#include "lockfile.h" +#include "resolve-undo.h" +#include "unpack-trees.h" +#include "wt-status.h" +#include "quote.h" +#include "setup.h" +#include "sparse-index.h" +#include "worktree.h" + +static const char *empty_base = ""; + +static char const * const builtin_sparse_checkout_usage[] = { + N_("git sparse-checkout (init | list | set | add | reapply | disable | check-rules) [<options>]"), + NULL +}; + +static void write_patterns_to_file(FILE *fp, struct pattern_list *pl) +{ + int i; + + for (i = 0; i < pl->nr; i++) { + struct path_pattern *p = pl->patterns[i]; + + if (p->flags & PATTERN_FLAG_NEGATIVE) + fprintf(fp, "!"); + + fprintf(fp, "%s", p->pattern); + + if (p->flags & PATTERN_FLAG_MUSTBEDIR) + fprintf(fp, "/"); + + fprintf(fp, "\n"); + } +} + +static char const * const builtin_sparse_checkout_list_usage[] = { + "git sparse-checkout list", + NULL +}; + +static int sparse_checkout_list(int argc, const char **argv, const char *prefix) +{ + static struct option builtin_sparse_checkout_list_options[] = { + OPT_END(), + }; + struct pattern_list pl; + char *sparse_filename; + int res; + + setup_work_tree(); + if (!core_apply_sparse_checkout) + die(_("this worktree is not sparse")); + + argc = parse_options(argc, argv, prefix, + builtin_sparse_checkout_list_options, + builtin_sparse_checkout_list_usage, 0); + + memset(&pl, 0, sizeof(pl)); + + pl.use_cone_patterns = core_sparse_checkout_cone; + + sparse_filename = get_sparse_checkout_filename(); + res = add_patterns_from_file_to_list(sparse_filename, "", 0, &pl, NULL, 0); + free(sparse_filename); + + if (res < 0) { + warning(_("this worktree is not sparse (sparse-checkout file may not exist)")); + return 0; + } + + if (pl.use_cone_patterns) { + int i; + struct pattern_entry *pe; + struct hashmap_iter iter; + struct string_list sl = STRING_LIST_INIT_DUP; + + hashmap_for_each_entry(&pl.recursive_hashmap, &iter, pe, ent) { + /* pe->pattern starts with "/", skip it */ + string_list_insert(&sl, pe->pattern + 1); + } + + string_list_sort(&sl); + + for (i = 0; i < sl.nr; i++) { + quote_c_style(sl.items[i].string, NULL, stdout, 0); + printf("\n"); + } + + return 0; + } + + write_patterns_to_file(stdout, &pl); + clear_pattern_list(&pl); + + return 0; +} + +static void clean_tracked_sparse_directories(struct repository *r) +{ + int i, was_full = 0; + struct strbuf path = STRBUF_INIT; + size_t pathlen; + struct string_list_item *item; + struct string_list sparse_dirs = STRING_LIST_INIT_DUP; + + /* + * If we are not using cone mode patterns, then we cannot + * delete directories outside of the sparse cone. + */ + if (!r || !r->index || !r->worktree) + return; + if (init_sparse_checkout_patterns(r->index) || + !r->index->sparse_checkout_patterns->use_cone_patterns) + return; + + /* + * Use the sparse index as a data structure to assist finding + * directories that are safe to delete. This conversion to a + * sparse index will not delete directories that contain + * conflicted entries or submodules. + */ + if (r->index->sparse_index == INDEX_EXPANDED) { + /* + * If something, such as a merge conflict or other concern, + * prevents us from converting to a sparse index, then do + * not try deleting files. + */ + if (convert_to_sparse(r->index, SPARSE_INDEX_MEMORY_ONLY)) + return; + was_full = 1; + } + + strbuf_addstr(&path, r->worktree); + strbuf_complete(&path, '/'); + pathlen = path.len; + + /* + * Collect directories that have gone out of scope but also + * exist on disk, so there is some work to be done. We need to + * store the entries in a list before exploring, since that might + * expand the sparse-index again. + */ + for (i = 0; i < r->index->cache_nr; i++) { + struct cache_entry *ce = r->index->cache[i]; + + if (S_ISSPARSEDIR(ce->ce_mode) && + repo_file_exists(r, ce->name)) + string_list_append(&sparse_dirs, ce->name); + } + + for_each_string_list_item(item, &sparse_dirs) { + struct dir_struct dir = DIR_INIT; + struct pathspec p = { 0 }; + struct strvec s = STRVEC_INIT; + + strbuf_setlen(&path, pathlen); + strbuf_addstr(&path, item->string); + + dir.flags |= DIR_SHOW_IGNORED_TOO; + + setup_standard_excludes(&dir); + strvec_push(&s, path.buf); + + parse_pathspec(&p, PATHSPEC_GLOB, 0, NULL, s.v); + fill_directory(&dir, r->index, &p); + + if (dir.nr) { + warning(_("directory '%s' contains untracked files," + " but is not in the sparse-checkout cone"), + item->string); + } else if (remove_dir_recursively(&path, 0)) { + /* + * Removal is "best effort". If something blocks + * the deletion, then continue with a warning. + */ + warning(_("failed to remove directory '%s'"), + item->string); + } + + strvec_clear(&s); + clear_pathspec(&p); + dir_clear(&dir); + } + + string_list_clear(&sparse_dirs, 0); + strbuf_release(&path); + + if (was_full) + ensure_full_index(r->index); +} + +static int update_working_directory(struct pattern_list *pl) +{ + enum update_sparsity_result result; + struct unpack_trees_options o; + struct lock_file lock_file = LOCK_INIT; + struct repository *r = the_repository; + + /* If no branch has been checked out, there are no updates to make. */ + if (is_index_unborn(r->index)) + return UPDATE_SPARSITY_SUCCESS; + + r->index->sparse_checkout_patterns = pl; + + memset(&o, 0, sizeof(o)); + o.verbose_update = isatty(2); + o.update = 1; + o.head_idx = -1; + o.src_index = r->index; + o.dst_index = r->index; + o.skip_sparse_checkout = 0; + + setup_work_tree(); + + repo_hold_locked_index(r, &lock_file, LOCK_DIE_ON_ERROR); + + setup_unpack_trees_porcelain(&o, "sparse-checkout"); + result = update_sparsity(&o, pl); + clear_unpack_trees_porcelain(&o); + + if (result == UPDATE_SPARSITY_WARNINGS) + /* + * We don't do any special handling of warnings from untracked + * files in the way or dirty entries that can't be removed. + */ + result = UPDATE_SPARSITY_SUCCESS; + if (result == UPDATE_SPARSITY_SUCCESS) + write_locked_index(r->index, &lock_file, COMMIT_LOCK); + else + rollback_lock_file(&lock_file); + + clean_tracked_sparse_directories(r); + + r->index->sparse_checkout_patterns = NULL; + return result; +} + +static char *escaped_pattern(char *pattern) +{ + char *p = pattern; + struct strbuf final = STRBUF_INIT; + + while (*p) { + if (is_glob_special(*p)) + strbuf_addch(&final, '\\'); + + strbuf_addch(&final, *p); + p++; + } + + return strbuf_detach(&final, NULL); +} + +static void write_cone_to_file(FILE *fp, struct pattern_list *pl) +{ + int i; + struct pattern_entry *pe; + struct hashmap_iter iter; + struct string_list sl = STRING_LIST_INIT_DUP; + struct strbuf parent_pattern = STRBUF_INIT; + + hashmap_for_each_entry(&pl->parent_hashmap, &iter, pe, ent) { + if (hashmap_get_entry(&pl->recursive_hashmap, pe, ent, NULL)) + continue; + + if (!hashmap_contains_parent(&pl->recursive_hashmap, + pe->pattern, + &parent_pattern)) + string_list_insert(&sl, pe->pattern); + } + + string_list_sort(&sl); + string_list_remove_duplicates(&sl, 0); + + fprintf(fp, "/*\n!/*/\n"); + + for (i = 0; i < sl.nr; i++) { + char *pattern = escaped_pattern(sl.items[i].string); + + if (strlen(pattern)) + fprintf(fp, "%s/\n!%s/*/\n", pattern, pattern); + free(pattern); + } + + string_list_clear(&sl, 0); + + hashmap_for_each_entry(&pl->recursive_hashmap, &iter, pe, ent) { + if (!hashmap_contains_parent(&pl->recursive_hashmap, + pe->pattern, + &parent_pattern)) + string_list_insert(&sl, pe->pattern); + } + + strbuf_release(&parent_pattern); + + string_list_sort(&sl); + string_list_remove_duplicates(&sl, 0); + + for (i = 0; i < sl.nr; i++) { + char *pattern = escaped_pattern(sl.items[i].string); + fprintf(fp, "%s/\n", pattern); + free(pattern); + } +} + +static int write_patterns_and_update(struct pattern_list *pl) +{ + char *sparse_filename; + FILE *fp; + int fd; + struct lock_file lk = LOCK_INIT; + int result; + + sparse_filename = get_sparse_checkout_filename(); + + if (safe_create_leading_directories(sparse_filename)) + die(_("failed to create directory for sparse-checkout file")); + + fd = hold_lock_file_for_update(&lk, sparse_filename, + LOCK_DIE_ON_ERROR); + free(sparse_filename); + + result = update_working_directory(pl); + if (result) { + rollback_lock_file(&lk); + clear_pattern_list(pl); + update_working_directory(NULL); + return result; + } + + fp = xfdopen(fd, "w"); + + if (core_sparse_checkout_cone) + write_cone_to_file(fp, pl); + else + write_patterns_to_file(fp, pl); + + fflush(fp); + commit_lock_file(&lk); + + clear_pattern_list(pl); + + return 0; +} + +enum sparse_checkout_mode { + MODE_NO_PATTERNS = 0, + MODE_ALL_PATTERNS = 1, + MODE_CONE_PATTERNS = 2, +}; + +static int set_config(enum sparse_checkout_mode mode) +{ + /* Update to use worktree config, if not already. */ + if (init_worktree_config(the_repository)) { + error(_("failed to initialize worktree config")); + return 1; + } + + if (repo_config_set_worktree_gently(the_repository, + "core.sparseCheckout", + mode ? "true" : "false") || + repo_config_set_worktree_gently(the_repository, + "core.sparseCheckoutCone", + mode == MODE_CONE_PATTERNS ? + "true" : "false")) + return 1; + + if (mode == MODE_NO_PATTERNS) + return set_sparse_index_config(the_repository, 0); + + return 0; +} + +static enum sparse_checkout_mode update_cone_mode(int *cone_mode) { + /* If not specified, use previous definition of cone mode */ + if (*cone_mode == -1 && core_apply_sparse_checkout) + *cone_mode = core_sparse_checkout_cone; + + /* Set cone/non-cone mode appropriately */ + core_apply_sparse_checkout = 1; + if (*cone_mode == 1 || *cone_mode == -1) { + core_sparse_checkout_cone = 1; + return MODE_CONE_PATTERNS; + } + core_sparse_checkout_cone = 0; + return MODE_ALL_PATTERNS; +} + +static int update_modes(int *cone_mode, int *sparse_index) +{ + int mode, record_mode; + + /* Determine if we need to record the mode; ensure sparse checkout on */ + record_mode = (*cone_mode != -1) || !core_apply_sparse_checkout; + + mode = update_cone_mode(cone_mode); + if (record_mode && set_config(mode)) + return 1; + + /* Set sparse-index/non-sparse-index mode if specified */ + if (*sparse_index >= 0) { + if (set_sparse_index_config(the_repository, *sparse_index) < 0) + die(_("failed to modify sparse-index config")); + + /* force an index rewrite */ + repo_read_index(the_repository); + the_repository->index->updated_workdir = 1; + + if (!*sparse_index) + ensure_full_index(the_repository->index); + } + + return 0; +} + +static char const * const builtin_sparse_checkout_init_usage[] = { + "git sparse-checkout init [--cone] [--[no-]sparse-index]", + NULL +}; + +static struct sparse_checkout_init_opts { + int cone_mode; + int sparse_index; +} init_opts; + +static int sparse_checkout_init(int argc, const char **argv, const char *prefix) +{ + struct pattern_list pl; + char *sparse_filename; + int res; + struct object_id oid; + struct strbuf pattern = STRBUF_INIT; + + static struct option builtin_sparse_checkout_init_options[] = { + OPT_BOOL(0, "cone", &init_opts.cone_mode, + N_("initialize the sparse-checkout in cone mode")), + OPT_BOOL(0, "sparse-index", &init_opts.sparse_index, + N_("toggle the use of a sparse index")), + OPT_END(), + }; + + setup_work_tree(); + repo_read_index(the_repository); + + init_opts.cone_mode = -1; + init_opts.sparse_index = -1; + + argc = parse_options(argc, argv, prefix, + builtin_sparse_checkout_init_options, + builtin_sparse_checkout_init_usage, 0); + + if (update_modes(&init_opts.cone_mode, &init_opts.sparse_index)) + return 1; + + memset(&pl, 0, sizeof(pl)); + + sparse_filename = get_sparse_checkout_filename(); + res = add_patterns_from_file_to_list(sparse_filename, "", 0, &pl, NULL, 0); + + /* If we already have a sparse-checkout file, use it. */ + if (res >= 0) { + free(sparse_filename); + return update_working_directory(NULL); + } + + if (repo_get_oid(the_repository, "HEAD", &oid)) { + FILE *fp; + + /* assume we are in a fresh repo, but update the sparse-checkout file */ + if (safe_create_leading_directories(sparse_filename)) + die(_("unable to create leading directories of %s"), + sparse_filename); + fp = xfopen(sparse_filename, "w"); + if (!fp) + die(_("failed to open '%s'"), sparse_filename); + + free(sparse_filename); + fprintf(fp, "/*\n!/*/\n"); + fclose(fp); + return 0; + } + + strbuf_addstr(&pattern, "/*"); + add_pattern(strbuf_detach(&pattern, NULL), empty_base, 0, &pl, 0); + strbuf_addstr(&pattern, "!/*/"); + add_pattern(strbuf_detach(&pattern, NULL), empty_base, 0, &pl, 0); + pl.use_cone_patterns = init_opts.cone_mode; + + return write_patterns_and_update(&pl); +} + +static void insert_recursive_pattern(struct pattern_list *pl, struct strbuf *path) +{ + struct pattern_entry *e = xmalloc(sizeof(*e)); + e->patternlen = path->len; + e->pattern = strbuf_detach(path, NULL); + hashmap_entry_init(&e->ent, fspathhash(e->pattern)); + + hashmap_add(&pl->recursive_hashmap, &e->ent); + + while (e->patternlen) { + char *slash = strrchr(e->pattern, '/'); + char *oldpattern = e->pattern; + size_t newlen; + + if (!slash || slash == e->pattern) + break; + + newlen = slash - e->pattern; + e = xmalloc(sizeof(struct pattern_entry)); + e->patternlen = newlen; + e->pattern = xstrndup(oldpattern, newlen); + hashmap_entry_init(&e->ent, fspathhash(e->pattern)); + + if (!hashmap_get_entry(&pl->parent_hashmap, e, ent, NULL)) + hashmap_add(&pl->parent_hashmap, &e->ent); + } +} + +static void strbuf_to_cone_pattern(struct strbuf *line, struct pattern_list *pl) +{ + strbuf_trim(line); + + strbuf_trim_trailing_dir_sep(line); + + if (strbuf_normalize_path(line)) + die(_("could not normalize path %s"), line->buf); + + if (!line->len) + return; + + if (line->buf[0] != '/') + strbuf_insertstr(line, 0, "/"); + + insert_recursive_pattern(pl, line); +} + +static void add_patterns_from_input(struct pattern_list *pl, + int argc, const char **argv, + FILE *file) +{ + int i; + if (core_sparse_checkout_cone) { + struct strbuf line = STRBUF_INIT; + + hashmap_init(&pl->recursive_hashmap, pl_hashmap_cmp, NULL, 0); + hashmap_init(&pl->parent_hashmap, pl_hashmap_cmp, NULL, 0); + pl->use_cone_patterns = 1; + + if (file) { + struct strbuf unquoted = STRBUF_INIT; + while (!strbuf_getline(&line, file)) { + if (line.buf[0] == '"') { + strbuf_reset(&unquoted); + if (unquote_c_style(&unquoted, line.buf, NULL)) + die(_("unable to unquote C-style string '%s'"), + line.buf); + + strbuf_swap(&unquoted, &line); + } + + strbuf_to_cone_pattern(&line, pl); + } + + strbuf_release(&unquoted); + } else { + for (i = 0; i < argc; i++) { + strbuf_setlen(&line, 0); + strbuf_addstr(&line, argv[i]); + strbuf_to_cone_pattern(&line, pl); + } + } + } else { + if (file) { + struct strbuf line = STRBUF_INIT; + + while (!strbuf_getline(&line, file)) { + size_t len; + char *buf = strbuf_detach(&line, &len); + add_pattern(buf, empty_base, 0, pl, 0); + } + } else { + for (i = 0; i < argc; i++) + add_pattern(argv[i], empty_base, 0, pl, 0); + } + } +} + +enum modify_type { + REPLACE, + ADD, +}; + +static void add_patterns_cone_mode(int argc, const char **argv, + struct pattern_list *pl, + int use_stdin) +{ + struct strbuf buffer = STRBUF_INIT; + struct pattern_entry *pe; + struct hashmap_iter iter; + struct pattern_list existing; + char *sparse_filename = get_sparse_checkout_filename(); + + add_patterns_from_input(pl, argc, argv, + use_stdin ? stdin : NULL); + + memset(&existing, 0, sizeof(existing)); + existing.use_cone_patterns = core_sparse_checkout_cone; + + if (add_patterns_from_file_to_list(sparse_filename, "", 0, + &existing, NULL, 0)) + die(_("unable to load existing sparse-checkout patterns")); + free(sparse_filename); + + if (!existing.use_cone_patterns) + die(_("existing sparse-checkout patterns do not use cone mode")); + + hashmap_for_each_entry(&existing.recursive_hashmap, &iter, pe, ent) { + if (!hashmap_contains_parent(&pl->recursive_hashmap, + pe->pattern, &buffer) || + !hashmap_contains_parent(&pl->parent_hashmap, + pe->pattern, &buffer)) { + strbuf_reset(&buffer); + strbuf_addstr(&buffer, pe->pattern); + insert_recursive_pattern(pl, &buffer); + } + } + + clear_pattern_list(&existing); + strbuf_release(&buffer); +} + +static void add_patterns_literal(int argc, const char **argv, + struct pattern_list *pl, + int use_stdin) +{ + char *sparse_filename = get_sparse_checkout_filename(); + if (add_patterns_from_file_to_list(sparse_filename, "", 0, + pl, NULL, 0)) + die(_("unable to load existing sparse-checkout patterns")); + free(sparse_filename); + add_patterns_from_input(pl, argc, argv, use_stdin ? stdin : NULL); +} + +static int modify_pattern_list(int argc, const char **argv, int use_stdin, + enum modify_type m) +{ + int result; + int changed_config = 0; + struct pattern_list *pl = xcalloc(1, sizeof(*pl)); + + switch (m) { + case ADD: + if (core_sparse_checkout_cone) + add_patterns_cone_mode(argc, argv, pl, use_stdin); + else + add_patterns_literal(argc, argv, pl, use_stdin); + break; + + case REPLACE: + add_patterns_from_input(pl, argc, argv, + use_stdin ? stdin : NULL); + break; + } + + if (!core_apply_sparse_checkout) { + set_config(MODE_ALL_PATTERNS); + core_apply_sparse_checkout = 1; + changed_config = 1; + } + + result = write_patterns_and_update(pl); + + if (result && changed_config) + set_config(MODE_NO_PATTERNS); + + clear_pattern_list(pl); + free(pl); + return result; +} + +static void sanitize_paths(int argc, const char **argv, + const char *prefix, int skip_checks) +{ + int i; + + if (!argc) + return; + + if (prefix && *prefix && core_sparse_checkout_cone) { + /* + * The args are not pathspecs, so unfortunately we + * cannot imitate how cmd_add() uses parse_pathspec(). + */ + int prefix_len = strlen(prefix); + + for (i = 0; i < argc; i++) + argv[i] = prefix_path(prefix, prefix_len, argv[i]); + } + + if (skip_checks) + return; + + if (prefix && *prefix && !core_sparse_checkout_cone) + die(_("please run from the toplevel directory in non-cone mode")); + + if (core_sparse_checkout_cone) { + for (i = 0; i < argc; i++) { + if (argv[i][0] == '/') + die(_("specify directories rather than patterns (no leading slash)")); + if (argv[i][0] == '!') + die(_("specify directories rather than patterns. If your directory starts with a '!', pass --skip-checks")); + if (strpbrk(argv[i], "*?[]")) + die(_("specify directories rather than patterns. If your directory really has any of '*?[]\\' in it, pass --skip-checks")); + } + } + + for (i = 0; i < argc; i++) { + struct cache_entry *ce; + struct index_state *index = the_repository->index; + int pos = index_name_pos(index, argv[i], strlen(argv[i])); + + if (pos < 0) + continue; + ce = index->cache[pos]; + if (S_ISSPARSEDIR(ce->ce_mode)) + continue; + + if (core_sparse_checkout_cone) + die(_("'%s' is not a directory; to treat it as a directory anyway, rerun with --skip-checks"), argv[i]); + else + warning(_("pass a leading slash before paths such as '%s' if you want a single file (see NON-CONE PROBLEMS in the git-sparse-checkout manual)."), argv[i]); + } +} + +static char const * const builtin_sparse_checkout_add_usage[] = { + N_("git sparse-checkout add [--skip-checks] (--stdin | <patterns>)"), + NULL +}; + +static struct sparse_checkout_add_opts { + int skip_checks; + int use_stdin; +} add_opts; + +static int sparse_checkout_add(int argc, const char **argv, const char *prefix) +{ + static struct option builtin_sparse_checkout_add_options[] = { + OPT_BOOL_F(0, "skip-checks", &add_opts.skip_checks, + N_("skip some sanity checks on the given paths that might give false positives"), + PARSE_OPT_NONEG), + OPT_BOOL(0, "stdin", &add_opts.use_stdin, + N_("read patterns from standard in")), + OPT_END(), + }; + + setup_work_tree(); + if (!core_apply_sparse_checkout) + die(_("no sparse-checkout to add to")); + + repo_read_index(the_repository); + + argc = parse_options(argc, argv, prefix, + builtin_sparse_checkout_add_options, + builtin_sparse_checkout_add_usage, + PARSE_OPT_KEEP_UNKNOWN_OPT); + + sanitize_paths(argc, argv, prefix, add_opts.skip_checks); + + return modify_pattern_list(argc, argv, add_opts.use_stdin, ADD); +} + +static char const * const builtin_sparse_checkout_set_usage[] = { + N_("git sparse-checkout set [--[no-]cone] [--[no-]sparse-index] [--skip-checks] (--stdin | <patterns>)"), + NULL +}; + +static struct sparse_checkout_set_opts { + int cone_mode; + int sparse_index; + int skip_checks; + int use_stdin; +} set_opts; + +static int sparse_checkout_set(int argc, const char **argv, const char *prefix) +{ + int default_patterns_nr = 2; + const char *default_patterns[] = {"/*", "!/*/", NULL}; + + static struct option builtin_sparse_checkout_set_options[] = { + OPT_BOOL(0, "cone", &set_opts.cone_mode, + N_("initialize the sparse-checkout in cone mode")), + OPT_BOOL(0, "sparse-index", &set_opts.sparse_index, + N_("toggle the use of a sparse index")), + OPT_BOOL_F(0, "skip-checks", &set_opts.skip_checks, + N_("skip some sanity checks on the given paths that might give false positives"), + PARSE_OPT_NONEG), + OPT_BOOL_F(0, "stdin", &set_opts.use_stdin, + N_("read patterns from standard in"), + PARSE_OPT_NONEG), + OPT_END(), + }; + + setup_work_tree(); + repo_read_index(the_repository); + + set_opts.cone_mode = -1; + set_opts.sparse_index = -1; + + argc = parse_options(argc, argv, prefix, + builtin_sparse_checkout_set_options, + builtin_sparse_checkout_set_usage, + PARSE_OPT_KEEP_UNKNOWN_OPT); + + if (update_modes(&set_opts.cone_mode, &set_opts.sparse_index)) + return 1; + + /* + * Cone mode automatically specifies the toplevel directory. For + * non-cone mode, if nothing is specified, manually select just the + * top-level directory (much as 'init' would do). + */ + if (!core_sparse_checkout_cone && argc == 0) { + argv = default_patterns; + argc = default_patterns_nr; + } else { + sanitize_paths(argc, argv, prefix, set_opts.skip_checks); + } + + return modify_pattern_list(argc, argv, set_opts.use_stdin, REPLACE); +} + +static char const * const builtin_sparse_checkout_reapply_usage[] = { + "git sparse-checkout reapply [--[no-]cone] [--[no-]sparse-index]", + NULL +}; + +static struct sparse_checkout_reapply_opts { + int cone_mode; + int sparse_index; +} reapply_opts; + +static int sparse_checkout_reapply(int argc, const char **argv, + const char *prefix) +{ + static struct option builtin_sparse_checkout_reapply_options[] = { + OPT_BOOL(0, "cone", &reapply_opts.cone_mode, + N_("initialize the sparse-checkout in cone mode")), + OPT_BOOL(0, "sparse-index", &reapply_opts.sparse_index, + N_("toggle the use of a sparse index")), + OPT_END(), + }; + + setup_work_tree(); + if (!core_apply_sparse_checkout) + die(_("must be in a sparse-checkout to reapply sparsity patterns")); + + reapply_opts.cone_mode = -1; + reapply_opts.sparse_index = -1; + + argc = parse_options(argc, argv, prefix, + builtin_sparse_checkout_reapply_options, + builtin_sparse_checkout_reapply_usage, 0); + + repo_read_index(the_repository); + + if (update_modes(&reapply_opts.cone_mode, &reapply_opts.sparse_index)) + return 1; + + return update_working_directory(NULL); +} + +static char const * const builtin_sparse_checkout_disable_usage[] = { + "git sparse-checkout disable", + NULL +}; + +static int sparse_checkout_disable(int argc, const char **argv, + const char *prefix) +{ + static struct option builtin_sparse_checkout_disable_options[] = { + OPT_END(), + }; + struct pattern_list pl; + struct strbuf match_all = STRBUF_INIT; + + /* + * We do not exit early if !core_apply_sparse_checkout; due to the + * ability for users to manually muck things up between + * direct editing of .git/info/sparse-checkout + * running read-tree -m u HEAD or update-index --skip-worktree + * direct toggling of config options + * users might end up with an index with SKIP_WORKTREE bit set on + * some files and not know how to undo it. So, here we just + * forcibly return to a dense checkout regardless of initial state. + */ + + setup_work_tree(); + argc = parse_options(argc, argv, prefix, + builtin_sparse_checkout_disable_options, + builtin_sparse_checkout_disable_usage, 0); + + repo_read_index(the_repository); + + memset(&pl, 0, sizeof(pl)); + hashmap_init(&pl.recursive_hashmap, pl_hashmap_cmp, NULL, 0); + hashmap_init(&pl.parent_hashmap, pl_hashmap_cmp, NULL, 0); + pl.use_cone_patterns = 0; + core_apply_sparse_checkout = 1; + + strbuf_addstr(&match_all, "/*"); + add_pattern(strbuf_detach(&match_all, NULL), empty_base, 0, &pl, 0); + + prepare_repo_settings(the_repository); + the_repository->settings.sparse_index = 0; + + if (update_working_directory(&pl)) + die(_("error while refreshing working directory")); + + clear_pattern_list(&pl); + return set_config(MODE_NO_PATTERNS); +} + +static char const * const builtin_sparse_checkout_check_rules_usage[] = { + N_("git sparse-checkout check-rules [-z] [--skip-checks]" + "[--[no-]cone] [--rules-file <file>]"), + NULL +}; + +static struct sparse_checkout_check_rules_opts { + int cone_mode; + int null_termination; + char *rules_file; +} check_rules_opts; + +static int check_rules(struct pattern_list *pl, int null_terminated) { + struct strbuf line = STRBUF_INIT; + struct strbuf unquoted = STRBUF_INIT; + char *path; + int line_terminator = null_terminated ? 0 : '\n'; + strbuf_getline_fn getline_fn = null_terminated ? strbuf_getline_nul + : strbuf_getline; + the_repository->index->sparse_checkout_patterns = pl; + while (!getline_fn(&line, stdin)) { + path = line.buf; + if (!null_terminated && line.buf[0] == '"') { + strbuf_reset(&unquoted); + if (unquote_c_style(&unquoted, line.buf, NULL)) + die(_("unable to unquote C-style string '%s'"), + line.buf); + + path = unquoted.buf; + } + + if (path_in_sparse_checkout(path, the_repository->index)) + write_name_quoted(path, stdout, line_terminator); + } + strbuf_release(&line); + strbuf_release(&unquoted); + + return 0; +} + +static int sparse_checkout_check_rules(int argc, const char **argv, const char *prefix) +{ + static struct option builtin_sparse_checkout_check_rules_options[] = { + OPT_BOOL('z', NULL, &check_rules_opts.null_termination, + N_("terminate input and output files by a NUL character")), + OPT_BOOL(0, "cone", &check_rules_opts.cone_mode, + N_("when used with --rules-file interpret patterns as cone mode patterns")), + OPT_FILENAME(0, "rules-file", &check_rules_opts.rules_file, + N_("use patterns in <file> instead of the current ones.")), + OPT_END(), + }; + + FILE *fp; + int ret; + struct pattern_list pl = {0}; + char *sparse_filename; + check_rules_opts.cone_mode = -1; + + argc = parse_options(argc, argv, prefix, + builtin_sparse_checkout_check_rules_options, + builtin_sparse_checkout_check_rules_usage, + PARSE_OPT_KEEP_UNKNOWN_OPT); + + if (check_rules_opts.rules_file && check_rules_opts.cone_mode < 0) + check_rules_opts.cone_mode = 1; + + update_cone_mode(&check_rules_opts.cone_mode); + pl.use_cone_patterns = core_sparse_checkout_cone; + if (check_rules_opts.rules_file) { + fp = xfopen(check_rules_opts.rules_file, "r"); + add_patterns_from_input(&pl, argc, argv, fp); + fclose(fp); + } else { + sparse_filename = get_sparse_checkout_filename(); + if (add_patterns_from_file_to_list(sparse_filename, "", 0, &pl, + NULL, 0)) + die(_("unable to load existing sparse-checkout patterns")); + free(sparse_filename); + } + + ret = check_rules(&pl, check_rules_opts.null_termination); + clear_pattern_list(&pl); + return ret; +} + +int cmd_sparse_checkout(int argc, const char **argv, const char *prefix) +{ + parse_opt_subcommand_fn *fn = NULL; + struct option builtin_sparse_checkout_options[] = { + OPT_SUBCOMMAND("list", &fn, sparse_checkout_list), + OPT_SUBCOMMAND("init", &fn, sparse_checkout_init), + OPT_SUBCOMMAND("set", &fn, sparse_checkout_set), + OPT_SUBCOMMAND("add", &fn, sparse_checkout_add), + OPT_SUBCOMMAND("reapply", &fn, sparse_checkout_reapply), + OPT_SUBCOMMAND("disable", &fn, sparse_checkout_disable), + OPT_SUBCOMMAND("check-rules", &fn, sparse_checkout_check_rules), + OPT_END(), + }; + + argc = parse_options(argc, argv, prefix, + builtin_sparse_checkout_options, + builtin_sparse_checkout_usage, 0); + + git_config(git_default_config, NULL); + + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + return fn(argc, argv, prefix); +} diff --git a/builtin/stash.c b/builtin/stash.c new file mode 100644 index 0000000..4a6771c --- /dev/null +++ b/builtin/stash.c @@ -0,0 +1,1887 @@ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "abspath.h" +#include "config.h" +#include "environment.h" +#include "gettext.h" +#include "hash.h" +#include "hex.h" +#include "object-name.h" +#include "parse-options.h" +#include "refs.h" +#include "lockfile.h" +#include "cache-tree.h" +#include "unpack-trees.h" +#include "merge-recursive.h" +#include "merge-ort-wrappers.h" +#include "strvec.h" +#include "run-command.h" +#include "dir.h" +#include "entry.h" +#include "preload-index.h" +#include "read-cache.h" +#include "rerere.h" +#include "revision.h" +#include "setup.h" +#include "sparse-index.h" +#include "log-tree.h" +#include "diffcore.h" +#include "exec-cmd.h" +#include "reflog.h" +#include "add-interactive.h" + +#define INCLUDE_ALL_FILES 2 + +#define BUILTIN_STASH_LIST_USAGE \ + N_("git stash list [<log-options>]") +#define BUILTIN_STASH_SHOW_USAGE \ + N_("git stash show [-u | --include-untracked | --only-untracked] [<diff-options>] [<stash>]") +#define BUILTIN_STASH_DROP_USAGE \ + N_("git stash drop [-q | --quiet] [<stash>]") +#define BUILTIN_STASH_POP_USAGE \ + N_("git stash pop [--index] [-q | --quiet] [<stash>]") +#define BUILTIN_STASH_APPLY_USAGE \ + N_("git stash apply [--index] [-q | --quiet] [<stash>]") +#define BUILTIN_STASH_BRANCH_USAGE \ + N_("git stash branch <branchname> [<stash>]") +#define BUILTIN_STASH_STORE_USAGE \ + N_("git stash store [(-m | --message) <message>] [-q | --quiet] <commit>") +#define BUILTIN_STASH_PUSH_USAGE \ + N_("git stash [push [-p | --patch] [-S | --staged] [-k | --[no-]keep-index] [-q | --quiet]\n" \ + " [-u | --include-untracked] [-a | --all] [(-m | --message) <message>]\n" \ + " [--pathspec-from-file=<file> [--pathspec-file-nul]]\n" \ + " [--] [<pathspec>...]]") +#define BUILTIN_STASH_SAVE_USAGE \ + N_("git stash save [-p | --patch] [-S | --staged] [-k | --[no-]keep-index] [-q | --quiet]\n" \ + " [-u | --include-untracked] [-a | --all] [<message>]") +#define BUILTIN_STASH_CREATE_USAGE \ + N_("git stash create [<message>]") +#define BUILTIN_STASH_CLEAR_USAGE \ + "git stash clear" + +static const char * const git_stash_usage[] = { + BUILTIN_STASH_LIST_USAGE, + BUILTIN_STASH_SHOW_USAGE, + BUILTIN_STASH_DROP_USAGE, + BUILTIN_STASH_POP_USAGE, + BUILTIN_STASH_APPLY_USAGE, + BUILTIN_STASH_BRANCH_USAGE, + BUILTIN_STASH_PUSH_USAGE, + BUILTIN_STASH_SAVE_USAGE, + BUILTIN_STASH_CLEAR_USAGE, + BUILTIN_STASH_CREATE_USAGE, + BUILTIN_STASH_STORE_USAGE, + NULL +}; + +static const char * const git_stash_list_usage[] = { + BUILTIN_STASH_LIST_USAGE, + NULL +}; + +static const char * const git_stash_show_usage[] = { + BUILTIN_STASH_SHOW_USAGE, + NULL +}; + +static const char * const git_stash_drop_usage[] = { + BUILTIN_STASH_DROP_USAGE, + NULL +}; + +static const char * const git_stash_pop_usage[] = { + BUILTIN_STASH_POP_USAGE, + NULL +}; + +static const char * const git_stash_apply_usage[] = { + BUILTIN_STASH_APPLY_USAGE, + NULL +}; + +static const char * const git_stash_branch_usage[] = { + BUILTIN_STASH_BRANCH_USAGE, + NULL +}; + +static const char * const git_stash_clear_usage[] = { + BUILTIN_STASH_CLEAR_USAGE, + NULL +}; + +static const char * const git_stash_store_usage[] = { + BUILTIN_STASH_STORE_USAGE, + NULL +}; + +static const char * const git_stash_push_usage[] = { + BUILTIN_STASH_PUSH_USAGE, + NULL +}; + +static const char * const git_stash_save_usage[] = { + BUILTIN_STASH_SAVE_USAGE, + NULL +}; + +static const char ref_stash[] = "refs/stash"; +static struct strbuf stash_index_path = STRBUF_INIT; + +/* + * w_commit is set to the commit containing the working tree + * b_commit is set to the base commit + * i_commit is set to the commit containing the index tree + * u_commit is set to the commit containing the untracked files tree + * w_tree is set to the working tree + * b_tree is set to the base tree + * i_tree is set to the index tree + * u_tree is set to the untracked files tree + */ +struct stash_info { + struct object_id w_commit; + struct object_id b_commit; + struct object_id i_commit; + struct object_id u_commit; + struct object_id w_tree; + struct object_id b_tree; + struct object_id i_tree; + struct object_id u_tree; + struct strbuf revision; + int is_stash_ref; + int has_u; +}; + +#define STASH_INFO_INIT { \ + .revision = STRBUF_INIT, \ +} + +static void free_stash_info(struct stash_info *info) +{ + strbuf_release(&info->revision); +} + +static void assert_stash_like(struct stash_info *info, const char *revision) +{ + if (get_oidf(&info->b_commit, "%s^1", revision) || + get_oidf(&info->w_tree, "%s:", revision) || + get_oidf(&info->b_tree, "%s^1:", revision) || + get_oidf(&info->i_tree, "%s^2:", revision)) + die(_("'%s' is not a stash-like commit"), revision); +} + +static int get_stash_info(struct stash_info *info, int argc, const char **argv) +{ + int ret; + char *end_of_rev; + char *expanded_ref; + const char *revision; + const char *commit = NULL; + struct object_id dummy; + struct strbuf symbolic = STRBUF_INIT; + + if (argc > 1) { + int i; + struct strbuf refs_msg = STRBUF_INIT; + + for (i = 0; i < argc; i++) + strbuf_addf(&refs_msg, " '%s'", argv[i]); + + fprintf_ln(stderr, _("Too many revisions specified:%s"), + refs_msg.buf); + strbuf_release(&refs_msg); + + return -1; + } + + if (argc == 1) + commit = argv[0]; + + if (!commit) { + if (!ref_exists(ref_stash)) { + fprintf_ln(stderr, _("No stash entries found.")); + return -1; + } + + strbuf_addf(&info->revision, "%s@{0}", ref_stash); + } else if (strspn(commit, "0123456789") == strlen(commit)) { + strbuf_addf(&info->revision, "%s@{%s}", ref_stash, commit); + } else { + strbuf_addstr(&info->revision, commit); + } + + revision = info->revision.buf; + + if (repo_get_oid(the_repository, revision, &info->w_commit)) + return error(_("%s is not a valid reference"), revision); + + assert_stash_like(info, revision); + + info->has_u = !get_oidf(&info->u_tree, "%s^3:", revision); + + end_of_rev = strchrnul(revision, '@'); + strbuf_add(&symbolic, revision, end_of_rev - revision); + + ret = repo_dwim_ref(the_repository, symbolic.buf, symbolic.len, + &dummy, &expanded_ref, 0); + strbuf_release(&symbolic); + switch (ret) { + case 0: /* Not found, but valid ref */ + info->is_stash_ref = 0; + break; + case 1: + info->is_stash_ref = !strcmp(expanded_ref, ref_stash); + break; + default: /* Invalid or ambiguous */ + break; + } + + free(expanded_ref); + return !(ret == 0 || ret == 1); +} + +static int do_clear_stash(void) +{ + struct object_id obj; + if (repo_get_oid(the_repository, ref_stash, &obj)) + return 0; + + return delete_ref(NULL, ref_stash, &obj, 0); +} + +static int clear_stash(int argc, const char **argv, const char *prefix) +{ + struct option options[] = { + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, + git_stash_clear_usage, + PARSE_OPT_STOP_AT_NON_OPTION); + + if (argc) + return error(_("git stash clear with arguments is " + "unimplemented")); + + return do_clear_stash(); +} + +static int reset_tree(struct object_id *i_tree, int update, int reset) +{ + int nr_trees = 1; + struct unpack_trees_options opts; + struct tree_desc t[MAX_UNPACK_TREES]; + struct tree *tree; + struct lock_file lock_file = LOCK_INIT; + + repo_read_index_preload(the_repository, NULL, 0); + if (refresh_index(&the_index, REFRESH_QUIET, NULL, NULL, NULL)) + return -1; + + repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR); + + memset(&opts, 0, sizeof(opts)); + + tree = parse_tree_indirect(i_tree); + if (parse_tree(tree)) + return -1; + + init_tree_desc(t, tree->buffer, tree->size); + + opts.head_idx = 1; + opts.src_index = &the_index; + opts.dst_index = &the_index; + opts.merge = 1; + opts.reset = reset ? UNPACK_RESET_PROTECT_UNTRACKED : 0; + opts.update = update; + if (update) + opts.preserve_ignored = 0; /* FIXME: !overwrite_ignore */ + opts.fn = oneway_merge; + + if (unpack_trees(nr_trees, t, &opts)) + return -1; + + if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK)) + return error(_("unable to write new index file")); + + return 0; +} + +static int diff_tree_binary(struct strbuf *out, struct object_id *w_commit) +{ + struct child_process cp = CHILD_PROCESS_INIT; + const char *w_commit_hex = oid_to_hex(w_commit); + + /* + * Diff-tree would not be very hard to replace with a native function, + * however it should be done together with apply_cached. + */ + cp.git_cmd = 1; + strvec_pushl(&cp.args, "diff-tree", "--binary", NULL); + strvec_pushf(&cp.args, "%s^2^..%s^2", w_commit_hex, w_commit_hex); + + return pipe_command(&cp, NULL, 0, out, 0, NULL, 0); +} + +static int apply_cached(struct strbuf *out) +{ + struct child_process cp = CHILD_PROCESS_INIT; + + /* + * Apply currently only reads either from stdin or a file, thus + * apply_all_patches would have to be updated to optionally take a + * buffer. + */ + cp.git_cmd = 1; + strvec_pushl(&cp.args, "apply", "--cached", NULL); + return pipe_command(&cp, out->buf, out->len, NULL, 0, NULL, 0); +} + +static int reset_head(void) +{ + struct child_process cp = CHILD_PROCESS_INIT; + + /* + * Reset is overall quite simple, however there is no current public + * API for resetting. + */ + cp.git_cmd = 1; + strvec_pushl(&cp.args, "reset", "--quiet", "--refresh", NULL); + + return run_command(&cp); +} + +static int is_path_a_directory(const char *path) +{ + /* + * This function differs from abspath.c:is_directory() in that + * here we use lstat() instead of stat(); we do not want to + * follow symbolic links here. + */ + struct stat st; + return (!lstat(path, &st) && S_ISDIR(st.st_mode)); +} + +static void add_diff_to_buf(struct diff_queue_struct *q, + struct diff_options *options UNUSED, + void *data) +{ + int i; + + for (i = 0; i < q->nr; i++) { + if (is_path_a_directory(q->queue[i]->one->path)) + continue; + + strbuf_addstr(data, q->queue[i]->one->path); + + /* NUL-terminate: will be fed to update-index -z */ + strbuf_addch(data, '\0'); + } +} + +static int restore_untracked(struct object_id *u_tree) +{ + int res; + struct child_process cp = CHILD_PROCESS_INIT; + + /* + * We need to run restore files from a given index, but without + * affecting the current index, so we use GIT_INDEX_FILE with + * run_command to fork processes that will not interfere. + */ + cp.git_cmd = 1; + strvec_push(&cp.args, "read-tree"); + strvec_push(&cp.args, oid_to_hex(u_tree)); + strvec_pushf(&cp.env, "GIT_INDEX_FILE=%s", + stash_index_path.buf); + if (run_command(&cp)) { + remove_path(stash_index_path.buf); + return -1; + } + + child_process_init(&cp); + cp.git_cmd = 1; + strvec_pushl(&cp.args, "checkout-index", "--all", NULL); + strvec_pushf(&cp.env, "GIT_INDEX_FILE=%s", + stash_index_path.buf); + + res = run_command(&cp); + remove_path(stash_index_path.buf); + return res; +} + +static void unstage_changes_unless_new(struct object_id *orig_tree) +{ + /* + * When we enter this function, there has been a clean merge of + * relevant trees, and the merge logic always stages whatever merges + * cleanly. We want to unstage those changes, unless it corresponds + * to a file that didn't exist as of orig_tree. + * + * However, if any SKIP_WORKTREE path is modified relative to + * orig_tree, then we want to clear the SKIP_WORKTREE bit and write + * it to the worktree before unstaging. + */ + + struct checkout state = CHECKOUT_INIT; + struct diff_options diff_opts; + struct lock_file lock = LOCK_INIT; + int i; + + /* If any entries have skip_worktree set, we'll have to check 'em out */ + state.force = 1; + state.quiet = 1; + state.refresh_cache = 1; + state.istate = &the_index; + + /* + * Step 1: get a difference between orig_tree (which corresponding + * to the index before a merge was run) and the current index + * (reflecting the changes brought in by the merge). + */ + repo_diff_setup(the_repository, &diff_opts); + diff_opts.flags.recursive = 1; + diff_opts.detect_rename = 0; + diff_opts.output_format = DIFF_FORMAT_NO_OUTPUT; + diff_setup_done(&diff_opts); + + do_diff_cache(orig_tree, &diff_opts); + diffcore_std(&diff_opts); + + /* Iterate over the paths that changed due to the merge... */ + for (i = 0; i < diff_queued_diff.nr; i++) { + struct diff_filepair *p; + struct cache_entry *ce; + int pos; + + /* Look up the path's position in the current index. */ + p = diff_queued_diff.queue[i]; + pos = index_name_pos(&the_index, p->two->path, + strlen(p->two->path)); + + /* + * Step 2: Place changes in the working tree + * + * Stash is about restoring changes *to the working tree*. + * So if the merge successfully got a new version of some + * path, but left it out of the working tree, then clear the + * SKIP_WORKTREE bit and write it to the working tree. + */ + if (pos >= 0 && ce_skip_worktree(the_index.cache[pos])) { + struct stat st; + + ce = the_index.cache[pos]; + if (!lstat(ce->name, &st)) { + /* Conflicting path present; relocate it */ + struct strbuf new_path = STRBUF_INIT; + int fd; + + strbuf_addf(&new_path, + "%s.stash.XXXXXX", ce->name); + fd = xmkstemp(new_path.buf); + close(fd); + printf(_("WARNING: Untracked file in way of " + "tracked file! Renaming\n " + " %s -> %s\n" + " to make room.\n"), + ce->name, new_path.buf); + if (rename(ce->name, new_path.buf)) + die("Failed to move %s to %s\n", + ce->name, new_path.buf); + strbuf_release(&new_path); + } + checkout_entry(ce, &state, NULL, NULL); + ce->ce_flags &= ~CE_SKIP_WORKTREE; + } + + /* + * Step 3: "unstage" changes, as long as they are still tracked + */ + if (p->one->oid_valid) { + /* + * Path existed in orig_tree; restore index entry + * from that tree in order to "unstage" the changes. + */ + int option = ADD_CACHE_OK_TO_REPLACE; + if (pos < 0) + option = ADD_CACHE_OK_TO_ADD; + + ce = make_cache_entry(&the_index, + p->one->mode, + &p->one->oid, + p->one->path, + 0, 0); + add_index_entry(&the_index, ce, option); + } + } + diff_flush(&diff_opts); + + /* + * Step 4: write the new index to disk + */ + repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR); + if (write_locked_index(&the_index, &lock, + COMMIT_LOCK | SKIP_IF_UNCHANGED)) + die(_("Unable to write index.")); +} + +static int do_apply_stash(const char *prefix, struct stash_info *info, + int index, int quiet) +{ + int clean, ret; + int has_index = index; + struct merge_options o; + struct object_id c_tree; + struct object_id index_tree; + struct tree *head, *merge, *merge_base; + struct lock_file lock = LOCK_INIT; + + repo_read_index_preload(the_repository, NULL, 0); + if (repo_refresh_and_write_index(the_repository, REFRESH_QUIET, 0, 0, + NULL, NULL, NULL)) + return -1; + + if (write_index_as_tree(&c_tree, &the_index, get_index_file(), 0, + NULL)) + return error(_("cannot apply a stash in the middle of a merge")); + + if (index) { + if (oideq(&info->b_tree, &info->i_tree) || + oideq(&c_tree, &info->i_tree)) { + has_index = 0; + } else { + struct strbuf out = STRBUF_INIT; + + if (diff_tree_binary(&out, &info->w_commit)) { + strbuf_release(&out); + return error(_("could not generate diff %s^!."), + oid_to_hex(&info->w_commit)); + } + + ret = apply_cached(&out); + strbuf_release(&out); + if (ret) + return error(_("conflicts in index. " + "Try without --index.")); + + discard_index(&the_index); + repo_read_index(the_repository); + if (write_index_as_tree(&index_tree, &the_index, + get_index_file(), 0, NULL)) + return error(_("could not save index tree")); + + reset_head(); + discard_index(&the_index); + repo_read_index(the_repository); + } + } + + init_merge_options(&o, the_repository); + + o.branch1 = "Updated upstream"; + o.branch2 = "Stashed changes"; + o.ancestor = "Stash base"; + + if (oideq(&info->b_tree, &c_tree)) + o.branch1 = "Version stash was based on"; + + if (quiet) + o.verbosity = 0; + + if (o.verbosity >= 3) + printf_ln(_("Merging %s with %s"), o.branch1, o.branch2); + + head = lookup_tree(o.repo, &c_tree); + merge = lookup_tree(o.repo, &info->w_tree); + merge_base = lookup_tree(o.repo, &info->b_tree); + + repo_hold_locked_index(o.repo, &lock, LOCK_DIE_ON_ERROR); + clean = merge_ort_nonrecursive(&o, head, merge, merge_base); + + /* + * If 'clean' >= 0, reverse the value for 'ret' so 'ret' is 0 when the + * merge was clean, and nonzero if the merge was unclean or encountered + * an error. + */ + ret = clean >= 0 ? !clean : clean; + + if (ret < 0) + rollback_lock_file(&lock); + else if (write_locked_index(o.repo->index, &lock, + COMMIT_LOCK | SKIP_IF_UNCHANGED)) + ret = error(_("could not write index")); + + if (ret) { + repo_rerere(the_repository, 0); + + if (index) + fprintf_ln(stderr, _("Index was not unstashed.")); + + goto restore_untracked; + } + + if (has_index) { + if (reset_tree(&index_tree, 0, 0)) + ret = -1; + } else { + unstage_changes_unless_new(&c_tree); + } + +restore_untracked: + if (info->has_u && restore_untracked(&info->u_tree)) + ret = error(_("could not restore untracked files from stash")); + + if (!quiet) { + struct child_process cp = CHILD_PROCESS_INIT; + + /* + * Status is quite simple and could be replaced with calls to + * wt_status in the future, but it adds complexities which may + * require more tests. + */ + cp.git_cmd = 1; + cp.dir = prefix; + strvec_pushf(&cp.env, GIT_WORK_TREE_ENVIRONMENT"=%s", + absolute_path(get_git_work_tree())); + strvec_pushf(&cp.env, GIT_DIR_ENVIRONMENT"=%s", + absolute_path(get_git_dir())); + strvec_push(&cp.args, "status"); + run_command(&cp); + } + + return ret; +} + +static int apply_stash(int argc, const char **argv, const char *prefix) +{ + int ret = -1; + int quiet = 0; + int index = 0; + struct stash_info info = STASH_INFO_INIT; + struct option options[] = { + OPT__QUIET(&quiet, N_("be quiet, only report errors")), + OPT_BOOL(0, "index", &index, + N_("attempt to recreate the index")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, + git_stash_apply_usage, 0); + + if (get_stash_info(&info, argc, argv)) + goto cleanup; + + ret = do_apply_stash(prefix, &info, index, quiet); +cleanup: + free_stash_info(&info); + return ret; +} + +static int reject_reflog_ent(struct object_id *ooid UNUSED, + struct object_id *noid UNUSED, + const char *email UNUSED, + timestamp_t timestamp UNUSED, + int tz UNUSED, const char *message UNUSED, + void *cb_data UNUSED) +{ + return 1; +} + +static int reflog_is_empty(const char *refname) +{ + return !for_each_reflog_ent(refname, reject_reflog_ent, NULL); +} + +static int do_drop_stash(struct stash_info *info, int quiet) +{ + if (!reflog_delete(info->revision.buf, + EXPIRE_REFLOGS_REWRITE | EXPIRE_REFLOGS_UPDATE_REF, + 0)) { + if (!quiet) + printf_ln(_("Dropped %s (%s)"), info->revision.buf, + oid_to_hex(&info->w_commit)); + } else { + return error(_("%s: Could not drop stash entry"), + info->revision.buf); + } + + if (reflog_is_empty(ref_stash)) + do_clear_stash(); + + return 0; +} + +static int get_stash_info_assert(struct stash_info *info, int argc, + const char **argv) +{ + int ret = get_stash_info(info, argc, argv); + + if (ret < 0) + return ret; + + if (!info->is_stash_ref) + return error(_("'%s' is not a stash reference"), info->revision.buf); + + return 0; +} + +static int drop_stash(int argc, const char **argv, const char *prefix) +{ + int ret = -1; + int quiet = 0; + struct stash_info info = STASH_INFO_INIT; + struct option options[] = { + OPT__QUIET(&quiet, N_("be quiet, only report errors")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, + git_stash_drop_usage, 0); + + if (get_stash_info_assert(&info, argc, argv)) + goto cleanup; + + ret = do_drop_stash(&info, quiet); +cleanup: + free_stash_info(&info); + return ret; +} + +static int pop_stash(int argc, const char **argv, const char *prefix) +{ + int ret = -1; + int index = 0; + int quiet = 0; + struct stash_info info = STASH_INFO_INIT; + struct option options[] = { + OPT__QUIET(&quiet, N_("be quiet, only report errors")), + OPT_BOOL(0, "index", &index, + N_("attempt to recreate the index")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, + git_stash_pop_usage, 0); + + if (get_stash_info_assert(&info, argc, argv)) + goto cleanup; + + if ((ret = do_apply_stash(prefix, &info, index, quiet))) + printf_ln(_("The stash entry is kept in case " + "you need it again.")); + else + ret = do_drop_stash(&info, quiet); + +cleanup: + free_stash_info(&info); + return ret; +} + +static int branch_stash(int argc, const char **argv, const char *prefix) +{ + int ret = -1; + const char *branch = NULL; + struct stash_info info = STASH_INFO_INIT; + struct child_process cp = CHILD_PROCESS_INIT; + struct option options[] = { + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, + git_stash_branch_usage, 0); + + if (!argc) { + fprintf_ln(stderr, _("No branch name specified")); + return -1; + } + + branch = argv[0]; + + if (get_stash_info(&info, argc - 1, argv + 1)) + goto cleanup; + + cp.git_cmd = 1; + strvec_pushl(&cp.args, "checkout", "-b", NULL); + strvec_push(&cp.args, branch); + strvec_push(&cp.args, oid_to_hex(&info.b_commit)); + ret = run_command(&cp); + if (!ret) + ret = do_apply_stash(prefix, &info, 1, 0); + if (!ret && info.is_stash_ref) + ret = do_drop_stash(&info, 0); + +cleanup: + free_stash_info(&info); + return ret; +} + +static int list_stash(int argc, const char **argv, const char *prefix) +{ + struct child_process cp = CHILD_PROCESS_INIT; + struct option options[] = { + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, + git_stash_list_usage, + PARSE_OPT_KEEP_UNKNOWN_OPT); + + if (!ref_exists(ref_stash)) + return 0; + + cp.git_cmd = 1; + strvec_pushl(&cp.args, "log", "--format=%gd: %gs", "-g", + "--first-parent", NULL); + strvec_pushv(&cp.args, argv); + strvec_push(&cp.args, ref_stash); + strvec_push(&cp.args, "--"); + return run_command(&cp); +} + +static int show_stat = 1; +static int show_patch; +static int show_include_untracked; + +static int git_stash_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + if (!strcmp(var, "stash.showstat")) { + show_stat = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "stash.showpatch")) { + show_patch = git_config_bool(var, value); + return 0; + } + if (!strcmp(var, "stash.showincludeuntracked")) { + show_include_untracked = git_config_bool(var, value); + return 0; + } + return git_diff_basic_config(var, value, ctx, cb); +} + +static void diff_include_untracked(const struct stash_info *info, struct diff_options *diff_opt) +{ + const struct object_id *oid[] = { &info->w_commit, &info->u_tree }; + struct tree *tree[ARRAY_SIZE(oid)]; + struct tree_desc tree_desc[ARRAY_SIZE(oid)]; + struct unpack_trees_options unpack_tree_opt = { 0 }; + int i; + + for (i = 0; i < ARRAY_SIZE(oid); i++) { + tree[i] = parse_tree_indirect(oid[i]); + if (parse_tree(tree[i]) < 0) + die(_("failed to parse tree")); + init_tree_desc(&tree_desc[i], tree[i]->buffer, tree[i]->size); + } + + unpack_tree_opt.head_idx = -1; + unpack_tree_opt.src_index = &the_index; + unpack_tree_opt.dst_index = &the_index; + unpack_tree_opt.merge = 1; + unpack_tree_opt.fn = stash_worktree_untracked_merge; + + if (unpack_trees(ARRAY_SIZE(tree_desc), tree_desc, &unpack_tree_opt)) + die(_("failed to unpack trees")); + + do_diff_cache(&info->b_commit, diff_opt); +} + +static int show_stash(int argc, const char **argv, const char *prefix) +{ + int i; + int ret = -1; + struct stash_info info = STASH_INFO_INIT; + struct rev_info rev; + struct strvec stash_args = STRVEC_INIT; + struct strvec revision_args = STRVEC_INIT; + enum { + UNTRACKED_NONE, + UNTRACKED_INCLUDE, + UNTRACKED_ONLY + } show_untracked = show_include_untracked ? UNTRACKED_INCLUDE : UNTRACKED_NONE; + struct option options[] = { + OPT_SET_INT('u', "include-untracked", &show_untracked, + N_("include untracked files in the stash"), + UNTRACKED_INCLUDE), + OPT_SET_INT_F(0, "only-untracked", &show_untracked, + N_("only show untracked files in the stash"), + UNTRACKED_ONLY, PARSE_OPT_NONEG), + OPT_END() + }; + int do_usage = 0; + + init_diff_ui_defaults(); + git_config(git_diff_ui_config, NULL); + repo_init_revisions(the_repository, &rev, prefix); + + argc = parse_options(argc, argv, prefix, options, git_stash_show_usage, + PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT | + PARSE_OPT_KEEP_DASHDASH); + + strvec_push(&revision_args, argv[0]); + for (i = 1; i < argc; i++) { + if (argv[i][0] != '-') + strvec_push(&stash_args, argv[i]); + else + strvec_push(&revision_args, argv[i]); + } + + if (get_stash_info(&info, stash_args.nr, stash_args.v)) + goto cleanup; + + /* + * The config settings are applied only if there are not passed + * any options. + */ + if (revision_args.nr == 1) { + if (show_stat) + rev.diffopt.output_format = DIFF_FORMAT_DIFFSTAT; + + if (show_patch) + rev.diffopt.output_format |= DIFF_FORMAT_PATCH; + + if (!show_stat && !show_patch) { + ret = 0; + goto cleanup; + } + } + + argc = setup_revisions(revision_args.nr, revision_args.v, &rev, NULL); + if (argc > 1) + goto usage; + if (!rev.diffopt.output_format) { + rev.diffopt.output_format = DIFF_FORMAT_PATCH; + diff_setup_done(&rev.diffopt); + } + + rev.diffopt.flags.recursive = 1; + setup_diff_pager(&rev.diffopt); + switch (show_untracked) { + case UNTRACKED_NONE: + diff_tree_oid(&info.b_commit, &info.w_commit, "", &rev.diffopt); + break; + case UNTRACKED_ONLY: + if (info.has_u) + diff_root_tree_oid(&info.u_tree, "", &rev.diffopt); + break; + case UNTRACKED_INCLUDE: + if (info.has_u) + diff_include_untracked(&info, &rev.diffopt); + else + diff_tree_oid(&info.b_commit, &info.w_commit, "", &rev.diffopt); + break; + } + log_tree_diff_flush(&rev); + + ret = diff_result_code(&rev.diffopt); +cleanup: + strvec_clear(&stash_args); + free_stash_info(&info); + release_revisions(&rev); + if (do_usage) + usage_with_options(git_stash_show_usage, options); + return ret; +usage: + do_usage = 1; + goto cleanup; +} + +static int do_store_stash(const struct object_id *w_commit, const char *stash_msg, + int quiet) +{ + struct stash_info info; + char revision[GIT_MAX_HEXSZ]; + + oid_to_hex_r(revision, w_commit); + assert_stash_like(&info, revision); + + if (!stash_msg) + stash_msg = "Created via \"git stash store\"."; + + if (update_ref(stash_msg, ref_stash, w_commit, NULL, + REF_FORCE_CREATE_REFLOG, + quiet ? UPDATE_REFS_QUIET_ON_ERR : + UPDATE_REFS_MSG_ON_ERR)) { + if (!quiet) { + fprintf_ln(stderr, _("Cannot update %s with %s"), + ref_stash, oid_to_hex(w_commit)); + } + return -1; + } + + return 0; +} + +static int store_stash(int argc, const char **argv, const char *prefix) +{ + int quiet = 0; + const char *stash_msg = NULL; + struct object_id obj; + struct object_context dummy; + struct option options[] = { + OPT__QUIET(&quiet, N_("be quiet")), + OPT_STRING('m', "message", &stash_msg, "message", + N_("stash message")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, + git_stash_store_usage, + PARSE_OPT_KEEP_UNKNOWN_OPT); + + if (argc != 1) { + if (!quiet) + fprintf_ln(stderr, _("\"git stash store\" requires one " + "<commit> argument")); + return -1; + } + + if (get_oid_with_context(the_repository, + argv[0], quiet ? GET_OID_QUIETLY : 0, &obj, + &dummy)) { + if (!quiet) + fprintf_ln(stderr, _("Cannot update %s with %s"), + ref_stash, argv[0]); + return -1; + } + + return do_store_stash(&obj, stash_msg, quiet); +} + +static void add_pathspecs(struct strvec *args, + const struct pathspec *ps) { + int i; + + for (i = 0; i < ps->nr; i++) + strvec_push(args, ps->items[i].original); +} + +/* + * `untracked_files` will be filled with the names of untracked files. + * The return value is: + * + * = 0 if there are not any untracked files + * > 0 if there are untracked files + */ +static int get_untracked_files(const struct pathspec *ps, int include_untracked, + struct strbuf *untracked_files) +{ + int i; + int found = 0; + struct dir_struct dir = DIR_INIT; + + if (include_untracked != INCLUDE_ALL_FILES) + setup_standard_excludes(&dir); + + fill_directory(&dir, the_repository->index, ps); + for (i = 0; i < dir.nr; i++) { + struct dir_entry *ent = dir.entries[i]; + found++; + strbuf_addstr(untracked_files, ent->name); + /* NUL-terminate: will be fed to update-index -z */ + strbuf_addch(untracked_files, '\0'); + } + + dir_clear(&dir); + return found; +} + +/* + * The return value of `check_changes_tracked_files()` can be: + * + * < 0 if there was an error + * = 0 if there are no changes. + * > 0 if there are changes. + */ +static int check_changes_tracked_files(const struct pathspec *ps) +{ + struct rev_info rev; + struct object_id dummy; + int ret = 0; + + /* No initial commit. */ + if (repo_get_oid(the_repository, "HEAD", &dummy)) + return -1; + + if (repo_read_index(the_repository) < 0) + return -1; + + repo_init_revisions(the_repository, &rev, NULL); + copy_pathspec(&rev.prune_data, ps); + + rev.diffopt.flags.quick = 1; + rev.diffopt.flags.ignore_submodules = 1; + rev.abbrev = 0; + + add_head_to_pending(&rev); + diff_setup_done(&rev.diffopt); + + run_diff_index(&rev, DIFF_INDEX_CACHED); + if (diff_result_code(&rev.diffopt)) { + ret = 1; + goto done; + } + + run_diff_files(&rev, 0); + if (diff_result_code(&rev.diffopt)) { + ret = 1; + goto done; + } + +done: + release_revisions(&rev); + return ret; +} + +/* + * The function will fill `untracked_files` with the names of untracked files + * It will return 1 if there were any changes and 0 if there were not. + */ +static int check_changes(const struct pathspec *ps, int include_untracked, + struct strbuf *untracked_files) +{ + int ret = 0; + if (check_changes_tracked_files(ps)) + ret = 1; + + if (include_untracked && get_untracked_files(ps, include_untracked, + untracked_files)) + ret = 1; + + return ret; +} + +static int save_untracked_files(struct stash_info *info, struct strbuf *msg, + struct strbuf files) +{ + int ret = 0; + struct strbuf untracked_msg = STRBUF_INIT; + struct child_process cp_upd_index = CHILD_PROCESS_INIT; + struct index_state istate = INDEX_STATE_INIT(the_repository); + + cp_upd_index.git_cmd = 1; + strvec_pushl(&cp_upd_index.args, "update-index", "-z", "--add", + "--remove", "--stdin", NULL); + strvec_pushf(&cp_upd_index.env, "GIT_INDEX_FILE=%s", + stash_index_path.buf); + + strbuf_addf(&untracked_msg, "untracked files on %s\n", msg->buf); + if (pipe_command(&cp_upd_index, files.buf, files.len, NULL, 0, + NULL, 0)) { + ret = -1; + goto done; + } + + if (write_index_as_tree(&info->u_tree, &istate, stash_index_path.buf, 0, + NULL)) { + ret = -1; + goto done; + } + + if (commit_tree(untracked_msg.buf, untracked_msg.len, + &info->u_tree, NULL, &info->u_commit, NULL, NULL)) { + ret = -1; + goto done; + } + +done: + release_index(&istate); + strbuf_release(&untracked_msg); + remove_path(stash_index_path.buf); + return ret; +} + +static int stash_staged(struct stash_info *info, struct strbuf *out_patch, + int quiet) +{ + int ret = 0; + struct child_process cp_diff_tree = CHILD_PROCESS_INIT; + struct index_state istate = INDEX_STATE_INIT(the_repository); + + if (write_index_as_tree(&info->w_tree, &istate, the_repository->index_file, + 0, NULL)) { + ret = -1; + goto done; + } + + cp_diff_tree.git_cmd = 1; + strvec_pushl(&cp_diff_tree.args, "diff-tree", "-p", "-U1", "HEAD", + oid_to_hex(&info->w_tree), "--", NULL); + if (pipe_command(&cp_diff_tree, NULL, 0, out_patch, 0, NULL, 0)) { + ret = -1; + goto done; + } + + if (!out_patch->len) { + if (!quiet) + fprintf_ln(stderr, _("No staged changes")); + ret = 1; + } + +done: + release_index(&istate); + return ret; +} + +static int stash_patch(struct stash_info *info, const struct pathspec *ps, + struct strbuf *out_patch, int quiet) +{ + int ret = 0; + struct child_process cp_read_tree = CHILD_PROCESS_INIT; + struct child_process cp_diff_tree = CHILD_PROCESS_INIT; + struct index_state istate = INDEX_STATE_INIT(the_repository); + char *old_index_env = NULL, *old_repo_index_file; + + remove_path(stash_index_path.buf); + + cp_read_tree.git_cmd = 1; + strvec_pushl(&cp_read_tree.args, "read-tree", "HEAD", NULL); + strvec_pushf(&cp_read_tree.env, "GIT_INDEX_FILE=%s", + stash_index_path.buf); + if (run_command(&cp_read_tree)) { + ret = -1; + goto done; + } + + /* Find out what the user wants. */ + old_repo_index_file = the_repository->index_file; + the_repository->index_file = stash_index_path.buf; + old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT)); + setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1); + + ret = !!run_add_p(the_repository, ADD_P_STASH, NULL, ps); + + the_repository->index_file = old_repo_index_file; + if (old_index_env && *old_index_env) + setenv(INDEX_ENVIRONMENT, old_index_env, 1); + else + unsetenv(INDEX_ENVIRONMENT); + FREE_AND_NULL(old_index_env); + + /* State of the working tree. */ + if (write_index_as_tree(&info->w_tree, &istate, stash_index_path.buf, 0, + NULL)) { + ret = -1; + goto done; + } + + cp_diff_tree.git_cmd = 1; + strvec_pushl(&cp_diff_tree.args, "diff-tree", "-p", "-U1", "HEAD", + oid_to_hex(&info->w_tree), "--", NULL); + if (pipe_command(&cp_diff_tree, NULL, 0, out_patch, 0, NULL, 0)) { + ret = -1; + goto done; + } + + if (!out_patch->len) { + if (!quiet) + fprintf_ln(stderr, _("No changes selected")); + ret = 1; + } + +done: + release_index(&istate); + remove_path(stash_index_path.buf); + return ret; +} + +static int stash_working_tree(struct stash_info *info, const struct pathspec *ps) +{ + int ret = 0; + struct rev_info rev; + struct child_process cp_upd_index = CHILD_PROCESS_INIT; + struct strbuf diff_output = STRBUF_INIT; + struct index_state istate = INDEX_STATE_INIT(the_repository); + + repo_init_revisions(the_repository, &rev, NULL); + copy_pathspec(&rev.prune_data, ps); + + set_alternate_index_output(stash_index_path.buf); + if (reset_tree(&info->i_tree, 0, 0)) { + ret = -1; + goto done; + } + set_alternate_index_output(NULL); + + rev.diffopt.output_format = DIFF_FORMAT_CALLBACK; + rev.diffopt.format_callback = add_diff_to_buf; + rev.diffopt.format_callback_data = &diff_output; + + if (repo_read_index_preload(the_repository, &rev.diffopt.pathspec, 0) < 0) { + ret = -1; + goto done; + } + + add_pending_object(&rev, parse_object(the_repository, &info->b_commit), + ""); + run_diff_index(&rev, 0); + + cp_upd_index.git_cmd = 1; + strvec_pushl(&cp_upd_index.args, "update-index", + "--ignore-skip-worktree-entries", + "-z", "--add", "--remove", "--stdin", NULL); + strvec_pushf(&cp_upd_index.env, "GIT_INDEX_FILE=%s", + stash_index_path.buf); + + if (pipe_command(&cp_upd_index, diff_output.buf, diff_output.len, + NULL, 0, NULL, 0)) { + ret = -1; + goto done; + } + + if (write_index_as_tree(&info->w_tree, &istate, stash_index_path.buf, 0, + NULL)) { + ret = -1; + goto done; + } + +done: + release_index(&istate); + release_revisions(&rev); + strbuf_release(&diff_output); + remove_path(stash_index_path.buf); + return ret; +} + +static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_buf, + int include_untracked, int patch_mode, int only_staged, + struct stash_info *info, struct strbuf *patch, + int quiet) +{ + int ret = 0; + int flags = 0; + int untracked_commit_option = 0; + const char *head_short_sha1 = NULL; + const char *branch_ref = NULL; + const char *branch_name = "(no branch)"; + struct commit *head_commit = NULL; + struct commit_list *parents = NULL; + struct strbuf msg = STRBUF_INIT; + struct strbuf commit_tree_label = STRBUF_INIT; + struct strbuf untracked_files = STRBUF_INIT; + + prepare_fallback_ident("git stash", "git@stash"); + + repo_read_index_preload(the_repository, NULL, 0); + if (repo_refresh_and_write_index(the_repository, REFRESH_QUIET, 0, 0, + NULL, NULL, NULL) < 0) { + ret = -1; + goto done; + } + + if (repo_get_oid(the_repository, "HEAD", &info->b_commit)) { + if (!quiet) + fprintf_ln(stderr, _("You do not have " + "the initial commit yet")); + ret = -1; + goto done; + } else { + head_commit = lookup_commit(the_repository, &info->b_commit); + } + + if (!check_changes(ps, include_untracked, &untracked_files)) { + ret = 1; + goto done; + } + + branch_ref = resolve_ref_unsafe("HEAD", 0, NULL, &flags); + if (flags & REF_ISSYMREF) + skip_prefix(branch_ref, "refs/heads/", &branch_name); + head_short_sha1 = repo_find_unique_abbrev(the_repository, + &head_commit->object.oid, + DEFAULT_ABBREV); + strbuf_addf(&msg, "%s: %s ", branch_name, head_short_sha1); + pp_commit_easy(CMIT_FMT_ONELINE, head_commit, &msg); + + strbuf_addf(&commit_tree_label, "index on %s\n", msg.buf); + commit_list_insert(head_commit, &parents); + if (write_index_as_tree(&info->i_tree, &the_index, get_index_file(), 0, + NULL) || + commit_tree(commit_tree_label.buf, commit_tree_label.len, + &info->i_tree, parents, &info->i_commit, NULL, NULL)) { + if (!quiet) + fprintf_ln(stderr, _("Cannot save the current " + "index state")); + ret = -1; + goto done; + } + + if (include_untracked) { + if (save_untracked_files(info, &msg, untracked_files)) { + if (!quiet) + fprintf_ln(stderr, _("Cannot save " + "the untracked files")); + ret = -1; + goto done; + } + untracked_commit_option = 1; + } + if (patch_mode) { + ret = stash_patch(info, ps, patch, quiet); + if (ret < 0) { + if (!quiet) + fprintf_ln(stderr, _("Cannot save the current " + "worktree state")); + goto done; + } else if (ret > 0) { + goto done; + } + } else if (only_staged) { + ret = stash_staged(info, patch, quiet); + if (ret < 0) { + if (!quiet) + fprintf_ln(stderr, _("Cannot save the current " + "staged state")); + goto done; + } else if (ret > 0) { + goto done; + } + } else { + if (stash_working_tree(info, ps)) { + if (!quiet) + fprintf_ln(stderr, _("Cannot save the current " + "worktree state")); + ret = -1; + goto done; + } + } + + if (!stash_msg_buf->len) + strbuf_addf(stash_msg_buf, "WIP on %s", msg.buf); + else + strbuf_insertf(stash_msg_buf, 0, "On %s: ", branch_name); + + /* + * `parents` will be empty after calling `commit_tree()`, so there is + * no need to call `free_commit_list()` + */ + parents = NULL; + if (untracked_commit_option) + commit_list_insert(lookup_commit(the_repository, + &info->u_commit), + &parents); + commit_list_insert(lookup_commit(the_repository, &info->i_commit), + &parents); + commit_list_insert(head_commit, &parents); + + if (commit_tree(stash_msg_buf->buf, stash_msg_buf->len, &info->w_tree, + parents, &info->w_commit, NULL, NULL)) { + if (!quiet) + fprintf_ln(stderr, _("Cannot record " + "working tree state")); + ret = -1; + goto done; + } + +done: + strbuf_release(&commit_tree_label); + strbuf_release(&msg); + strbuf_release(&untracked_files); + return ret; +} + +static int create_stash(int argc, const char **argv, const char *prefix UNUSED) +{ + int ret; + struct strbuf stash_msg_buf = STRBUF_INIT; + struct stash_info info = STASH_INFO_INIT; + struct pathspec ps; + + /* Starting with argv[1], since argv[0] is "create" */ + strbuf_join_argv(&stash_msg_buf, argc - 1, ++argv, ' '); + + memset(&ps, 0, sizeof(ps)); + if (!check_changes_tracked_files(&ps)) + return 0; + + ret = do_create_stash(&ps, &stash_msg_buf, 0, 0, 0, &info, + NULL, 0); + if (!ret) + printf_ln("%s", oid_to_hex(&info.w_commit)); + + free_stash_info(&info); + strbuf_release(&stash_msg_buf); + return ret; +} + +static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int quiet, + int keep_index, int patch_mode, int include_untracked, int only_staged) +{ + int ret = 0; + struct stash_info info = STASH_INFO_INIT; + struct strbuf patch = STRBUF_INIT; + struct strbuf stash_msg_buf = STRBUF_INIT; + struct strbuf untracked_files = STRBUF_INIT; + + if (patch_mode && keep_index == -1) + keep_index = 1; + + if (patch_mode && include_untracked) { + fprintf_ln(stderr, _("Can't use --patch and --include-untracked" + " or --all at the same time")); + ret = -1; + goto done; + } + + /* --patch overrides --staged */ + if (patch_mode) + only_staged = 0; + + if (only_staged && include_untracked) { + fprintf_ln(stderr, _("Can't use --staged and --include-untracked" + " or --all at the same time")); + ret = -1; + goto done; + } + + repo_read_index_preload(the_repository, NULL, 0); + if (!include_untracked && ps->nr) { + int i; + char *ps_matched = xcalloc(ps->nr, 1); + + /* TODO: audit for interaction with sparse-index. */ + ensure_full_index(&the_index); + for (i = 0; i < the_index.cache_nr; i++) + ce_path_match(&the_index, the_index.cache[i], ps, + ps_matched); + + if (report_path_error(ps_matched, ps)) { + fprintf_ln(stderr, _("Did you forget to 'git add'?")); + ret = -1; + free(ps_matched); + goto done; + } + free(ps_matched); + } + + if (repo_refresh_and_write_index(the_repository, REFRESH_QUIET, 0, 0, + NULL, NULL, NULL)) { + ret = -1; + goto done; + } + + if (!check_changes(ps, include_untracked, &untracked_files)) { + if (!quiet) + printf_ln(_("No local changes to save")); + goto done; + } + + if (!reflog_exists(ref_stash) && do_clear_stash()) { + ret = -1; + if (!quiet) + fprintf_ln(stderr, _("Cannot initialize stash")); + goto done; + } + + if (stash_msg) + strbuf_addstr(&stash_msg_buf, stash_msg); + if (do_create_stash(ps, &stash_msg_buf, include_untracked, patch_mode, only_staged, + &info, &patch, quiet)) { + ret = -1; + goto done; + } + + if (do_store_stash(&info.w_commit, stash_msg_buf.buf, 1)) { + ret = -1; + if (!quiet) + fprintf_ln(stderr, _("Cannot save the current status")); + goto done; + } + + if (!quiet) + printf_ln(_("Saved working directory and index state %s"), + stash_msg_buf.buf); + + if (!(patch_mode || only_staged)) { + if (include_untracked && !ps->nr) { + struct child_process cp = CHILD_PROCESS_INIT; + + cp.git_cmd = 1; + if (startup_info->original_cwd) { + cp.dir = startup_info->original_cwd; + strvec_pushf(&cp.env, "%s=%s", + GIT_WORK_TREE_ENVIRONMENT, + the_repository->worktree); + } + strvec_pushl(&cp.args, "clean", "--force", + "--quiet", "-d", ":/", NULL); + if (include_untracked == INCLUDE_ALL_FILES) + strvec_push(&cp.args, "-x"); + if (run_command(&cp)) { + ret = -1; + goto done; + } + } + discard_index(&the_index); + if (ps->nr) { + struct child_process cp_add = CHILD_PROCESS_INIT; + struct child_process cp_diff = CHILD_PROCESS_INIT; + struct child_process cp_apply = CHILD_PROCESS_INIT; + struct strbuf out = STRBUF_INIT; + + cp_add.git_cmd = 1; + strvec_push(&cp_add.args, "add"); + if (!include_untracked) + strvec_push(&cp_add.args, "-u"); + if (include_untracked == INCLUDE_ALL_FILES) + strvec_push(&cp_add.args, "--force"); + strvec_push(&cp_add.args, "--"); + add_pathspecs(&cp_add.args, ps); + if (run_command(&cp_add)) { + ret = -1; + goto done; + } + + cp_diff.git_cmd = 1; + strvec_pushl(&cp_diff.args, "diff-index", "-p", + "--cached", "--binary", "HEAD", "--", + NULL); + add_pathspecs(&cp_diff.args, ps); + if (pipe_command(&cp_diff, NULL, 0, &out, 0, NULL, 0)) { + ret = -1; + goto done; + } + + cp_apply.git_cmd = 1; + strvec_pushl(&cp_apply.args, "apply", "--index", + "-R", NULL); + if (pipe_command(&cp_apply, out.buf, out.len, NULL, 0, + NULL, 0)) { + ret = -1; + goto done; + } + } else { + struct child_process cp = CHILD_PROCESS_INIT; + cp.git_cmd = 1; + /* BUG: this nukes untracked files in the way */ + strvec_pushl(&cp.args, "reset", "--hard", "-q", + "--no-recurse-submodules", NULL); + if (run_command(&cp)) { + ret = -1; + goto done; + } + } + + if (keep_index == 1 && !is_null_oid(&info.i_tree)) { + struct child_process cp = CHILD_PROCESS_INIT; + + cp.git_cmd = 1; + strvec_pushl(&cp.args, "checkout", "--no-overlay", + oid_to_hex(&info.i_tree), "--", NULL); + if (!ps->nr) + strvec_push(&cp.args, ":/"); + else + add_pathspecs(&cp.args, ps); + if (run_command(&cp)) { + ret = -1; + goto done; + } + } + goto done; + } else { + struct child_process cp = CHILD_PROCESS_INIT; + + cp.git_cmd = 1; + strvec_pushl(&cp.args, "apply", "-R", NULL); + + if (pipe_command(&cp, patch.buf, patch.len, NULL, 0, NULL, 0)) { + if (!quiet) + fprintf_ln(stderr, _("Cannot remove " + "worktree changes")); + ret = -1; + goto done; + } + + if (keep_index < 1) { + struct child_process cp = CHILD_PROCESS_INIT; + + cp.git_cmd = 1; + strvec_pushl(&cp.args, "reset", "-q", "--refresh", "--", + NULL); + add_pathspecs(&cp.args, ps); + if (run_command(&cp)) { + ret = -1; + goto done; + } + } + goto done; + } + +done: + strbuf_release(&patch); + free_stash_info(&info); + strbuf_release(&stash_msg_buf); + strbuf_release(&untracked_files); + return ret; +} + +static int push_stash(int argc, const char **argv, const char *prefix, + int push_assumed) +{ + int force_assume = 0; + int keep_index = -1; + int only_staged = 0; + int patch_mode = 0; + int include_untracked = 0; + int quiet = 0; + int pathspec_file_nul = 0; + const char *stash_msg = NULL; + const char *pathspec_from_file = NULL; + struct pathspec ps; + struct option options[] = { + OPT_BOOL('k', "keep-index", &keep_index, + N_("keep index")), + OPT_BOOL('S', "staged", &only_staged, + N_("stash staged changes only")), + OPT_BOOL('p', "patch", &patch_mode, + N_("stash in patch mode")), + OPT__QUIET(&quiet, N_("quiet mode")), + OPT_BOOL('u', "include-untracked", &include_untracked, + N_("include untracked files in stash")), + OPT_SET_INT('a', "all", &include_untracked, + N_("include ignore files"), 2), + OPT_STRING('m', "message", &stash_msg, N_("message"), + N_("stash message")), + OPT_PATHSPEC_FROM_FILE(&pathspec_from_file), + OPT_PATHSPEC_FILE_NUL(&pathspec_file_nul), + OPT_END() + }; + int ret; + + if (argc) { + force_assume = !strcmp(argv[0], "-p"); + argc = parse_options(argc, argv, prefix, options, + push_assumed ? git_stash_usage : + git_stash_push_usage, + PARSE_OPT_KEEP_DASHDASH); + } + + if (argc) { + if (!strcmp(argv[0], "--")) { + argc--; + argv++; + } else if (push_assumed && !force_assume) { + die("subcommand wasn't specified; 'push' can't be assumed due to unexpected token '%s'", + argv[0]); + } + } + + parse_pathspec(&ps, 0, PATHSPEC_PREFER_FULL | PATHSPEC_PREFIX_ORIGIN, + prefix, argv); + + if (pathspec_from_file) { + if (patch_mode) + die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--patch"); + + if (only_staged) + die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--staged"); + + if (ps.nr) + die(_("'%s' and pathspec arguments cannot be used together"), "--pathspec-from-file"); + + parse_pathspec_file(&ps, 0, + PATHSPEC_PREFER_FULL | PATHSPEC_PREFIX_ORIGIN, + prefix, pathspec_from_file, pathspec_file_nul); + } else if (pathspec_file_nul) { + die(_("the option '%s' requires '%s'"), "--pathspec-file-nul", "--pathspec-from-file"); + } + + ret = do_push_stash(&ps, stash_msg, quiet, keep_index, patch_mode, + include_untracked, only_staged); + clear_pathspec(&ps); + return ret; +} + +static int push_stash_unassumed(int argc, const char **argv, const char *prefix) +{ + return push_stash(argc, argv, prefix, 0); +} + +static int save_stash(int argc, const char **argv, const char *prefix) +{ + int keep_index = -1; + int only_staged = 0; + int patch_mode = 0; + int include_untracked = 0; + int quiet = 0; + int ret = 0; + const char *stash_msg = NULL; + struct pathspec ps; + struct strbuf stash_msg_buf = STRBUF_INIT; + struct option options[] = { + OPT_BOOL('k', "keep-index", &keep_index, + N_("keep index")), + OPT_BOOL('S', "staged", &only_staged, + N_("stash staged changes only")), + OPT_BOOL('p', "patch", &patch_mode, + N_("stash in patch mode")), + OPT__QUIET(&quiet, N_("quiet mode")), + OPT_BOOL('u', "include-untracked", &include_untracked, + N_("include untracked files in stash")), + OPT_SET_INT('a', "all", &include_untracked, + N_("include ignore files"), 2), + OPT_STRING('m', "message", &stash_msg, "message", + N_("stash message")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, + git_stash_save_usage, + PARSE_OPT_KEEP_DASHDASH); + + if (argc) + stash_msg = strbuf_join_argv(&stash_msg_buf, argc, argv, ' '); + + memset(&ps, 0, sizeof(ps)); + ret = do_push_stash(&ps, stash_msg, quiet, keep_index, + patch_mode, include_untracked, only_staged); + + strbuf_release(&stash_msg_buf); + return ret; +} + +int cmd_stash(int argc, const char **argv, const char *prefix) +{ + pid_t pid = getpid(); + const char *index_file; + struct strvec args = STRVEC_INIT; + parse_opt_subcommand_fn *fn = NULL; + struct option options[] = { + OPT_SUBCOMMAND("apply", &fn, apply_stash), + OPT_SUBCOMMAND("clear", &fn, clear_stash), + OPT_SUBCOMMAND("drop", &fn, drop_stash), + OPT_SUBCOMMAND("pop", &fn, pop_stash), + OPT_SUBCOMMAND("branch", &fn, branch_stash), + OPT_SUBCOMMAND("list", &fn, list_stash), + OPT_SUBCOMMAND("show", &fn, show_stash), + OPT_SUBCOMMAND("store", &fn, store_stash), + OPT_SUBCOMMAND("create", &fn, create_stash), + OPT_SUBCOMMAND("push", &fn, push_stash_unassumed), + OPT_SUBCOMMAND_F("save", &fn, save_stash, PARSE_OPT_NOCOMPLETE), + OPT_END() + }; + + git_config(git_stash_config, NULL); + + argc = parse_options(argc, argv, prefix, options, git_stash_usage, + PARSE_OPT_SUBCOMMAND_OPTIONAL | + PARSE_OPT_KEEP_UNKNOWN_OPT | + PARSE_OPT_KEEP_DASHDASH); + + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + index_file = get_index_file(); + strbuf_addf(&stash_index_path, "%s.stash.%" PRIuMAX, index_file, + (uintmax_t)pid); + + if (fn) + return !!fn(argc, argv, prefix); + else if (!argc) + return !!push_stash_unassumed(0, NULL, prefix); + + /* Assume 'stash push' */ + strvec_push(&args, "push"); + strvec_pushv(&args, argv); + return !!push_stash(args.nr, args.v, prefix, 1); +} diff --git a/builtin/stripspace.c b/builtin/stripspace.c new file mode 100644 index 0000000..7b700a9 --- /dev/null +++ b/builtin/stripspace.c @@ -0,0 +1,69 @@ +#include "builtin.h" +#include "config.h" +#include "environment.h" +#include "gettext.h" +#include "parse-options.h" +#include "setup.h" +#include "strbuf.h" +#include "write-or-die.h" + +static void comment_lines(struct strbuf *buf) +{ + char *msg; + size_t len; + + msg = strbuf_detach(buf, &len); + strbuf_add_commented_lines(buf, msg, len, comment_line_char); + free(msg); +} + +static const char * const stripspace_usage[] = { + "git stripspace [-s | --strip-comments]", + "git stripspace [-c | --comment-lines]", + NULL +}; + +enum stripspace_mode { + STRIP_DEFAULT = 0, + STRIP_COMMENTS, + COMMENT_LINES +}; + +int cmd_stripspace(int argc, const char **argv, const char *prefix) +{ + struct strbuf buf = STRBUF_INIT; + enum stripspace_mode mode = STRIP_DEFAULT; + int nongit; + + const struct option options[] = { + OPT_CMDMODE('s', "strip-comments", &mode, + N_("skip and remove all lines starting with comment character"), + STRIP_COMMENTS), + OPT_CMDMODE('c', "comment-lines", &mode, + N_("prepend comment character and space to each line"), + COMMENT_LINES), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, stripspace_usage, 0); + if (argc) + usage_with_options(stripspace_usage, options); + + if (mode == STRIP_COMMENTS || mode == COMMENT_LINES) { + setup_git_directory_gently(&nongit); + git_config(git_default_config, NULL); + } + + if (strbuf_read(&buf, 0, 1024) < 0) + die_errno("could not read the input"); + + if (mode == STRIP_DEFAULT || mode == STRIP_COMMENTS) + strbuf_stripspace(&buf, + mode == STRIP_COMMENTS ? comment_line_char : '\0'); + else + comment_lines(&buf); + + write_or_die(1, buf.buf, buf.len); + strbuf_release(&buf); + return 0; +} diff --git a/builtin/submodule--helper.c b/builtin/submodule--helper.c new file mode 100644 index 0000000..cce4645 --- /dev/null +++ b/builtin/submodule--helper.c @@ -0,0 +1,3429 @@ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "abspath.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "repository.h" +#include "config.h" +#include "parse-options.h" +#include "quote.h" +#include "path.h" +#include "pathspec.h" +#include "preload-index.h" +#include "dir.h" +#include "read-cache.h" +#include "setup.h" +#include "sparse-index.h" +#include "submodule.h" +#include "submodule-config.h" +#include "string-list.h" +#include "run-command.h" +#include "remote.h" +#include "refs.h" +#include "refspec.h" +#include "connect.h" +#include "revision.h" +#include "diffcore.h" +#include "diff.h" +#include "object-file.h" +#include "object-name.h" +#include "object-store-ll.h" +#include "advice.h" +#include "branch.h" +#include "list-objects-filter-options.h" + +#define OPT_QUIET (1 << 0) +#define OPT_CACHED (1 << 1) +#define OPT_RECURSIVE (1 << 2) +#define OPT_FORCE (1 << 3) + +typedef void (*each_submodule_fn)(const struct cache_entry *list_item, + void *cb_data); + +static int repo_get_default_remote(struct repository *repo, char **default_remote) +{ + char *dest = NULL; + struct strbuf sb = STRBUF_INIT; + struct ref_store *store = get_main_ref_store(repo); + const char *refname = refs_resolve_ref_unsafe(store, "HEAD", 0, NULL, + NULL); + + if (!refname) + return die_message(_("No such ref: %s"), "HEAD"); + + /* detached HEAD */ + if (!strcmp(refname, "HEAD")) { + *default_remote = xstrdup("origin"); + return 0; + } + + if (!skip_prefix(refname, "refs/heads/", &refname)) + return die_message(_("Expecting a full ref name, got %s"), + refname); + + strbuf_addf(&sb, "branch.%s.remote", refname); + if (repo_config_get_string(repo, sb.buf, &dest)) + *default_remote = xstrdup("origin"); + else + *default_remote = dest; + + strbuf_release(&sb); + return 0; +} + +static int get_default_remote_submodule(const char *module_path, char **default_remote) +{ + struct repository subrepo; + int ret; + + if (repo_submodule_init(&subrepo, the_repository, module_path, + null_oid()) < 0) + return die_message(_("could not get a repository handle for submodule '%s'"), + module_path); + ret = repo_get_default_remote(&subrepo, default_remote); + repo_clear(&subrepo); + + return ret; +} + +static char *get_default_remote(void) +{ + char *default_remote; + int code = repo_get_default_remote(the_repository, &default_remote); + + if (code) + exit(code); + + return default_remote; +} + +static char *resolve_relative_url(const char *rel_url, const char *up_path, int quiet) +{ + char *remoteurl, *resolved_url; + char *remote = get_default_remote(); + struct strbuf remotesb = STRBUF_INIT; + + strbuf_addf(&remotesb, "remote.%s.url", remote); + if (git_config_get_string(remotesb.buf, &remoteurl)) { + if (!quiet) + warning(_("could not look up configuration '%s'. " + "Assuming this repository is its own " + "authoritative upstream."), + remotesb.buf); + remoteurl = xgetcwd(); + } + resolved_url = relative_url(remoteurl, rel_url, up_path); + + free(remote); + free(remoteurl); + strbuf_release(&remotesb); + + return resolved_url; +} + +/* the result should be freed by the caller. */ +static char *get_submodule_displaypath(const char *path, const char *prefix, + const char *super_prefix) +{ + if (prefix && super_prefix) { + BUG("cannot have prefix '%s' and superprefix '%s'", + prefix, super_prefix); + } else if (prefix) { + struct strbuf sb = STRBUF_INIT; + char *displaypath = xstrdup(relative_path(path, prefix, &sb)); + strbuf_release(&sb); + return displaypath; + } else if (super_prefix) { + return xstrfmt("%s%s", super_prefix, path); + } else { + return xstrdup(path); + } +} + +static char *compute_rev_name(const char *sub_path, const char* object_id) +{ + struct strbuf sb = STRBUF_INIT; + const char ***d; + + static const char *describe_bare[] = { NULL }; + + static const char *describe_tags[] = { "--tags", NULL }; + + static const char *describe_contains[] = { "--contains", NULL }; + + static const char *describe_all_always[] = { "--all", "--always", NULL }; + + static const char **describe_argv[] = { describe_bare, describe_tags, + describe_contains, + describe_all_always, NULL }; + + for (d = describe_argv; *d; d++) { + struct child_process cp = CHILD_PROCESS_INIT; + prepare_submodule_repo_env(&cp.env); + cp.dir = sub_path; + cp.git_cmd = 1; + cp.no_stderr = 1; + + strvec_push(&cp.args, "describe"); + strvec_pushv(&cp.args, *d); + strvec_push(&cp.args, object_id); + + if (!capture_command(&cp, &sb, 0)) { + strbuf_strip_suffix(&sb, "\n"); + return strbuf_detach(&sb, NULL); + } + } + + strbuf_release(&sb); + return NULL; +} + +struct module_list { + const struct cache_entry **entries; + int alloc, nr; +}; +#define MODULE_LIST_INIT { 0 } + +static void module_list_release(struct module_list *ml) +{ + free(ml->entries); +} + +static int module_list_compute(const char **argv, + const char *prefix, + struct pathspec *pathspec, + struct module_list *list) +{ + int i, result = 0; + char *ps_matched = NULL; + + parse_pathspec(pathspec, 0, + PATHSPEC_PREFER_FULL, + prefix, argv); + + if (pathspec->nr) + ps_matched = xcalloc(pathspec->nr, 1); + + if (repo_read_index(the_repository) < 0) + die(_("index file corrupt")); + + for (i = 0; i < the_index.cache_nr; i++) { + const struct cache_entry *ce = the_index.cache[i]; + + if (!match_pathspec(&the_index, pathspec, ce->name, ce_namelen(ce), + 0, ps_matched, 1) || + !S_ISGITLINK(ce->ce_mode)) + continue; + + ALLOC_GROW(list->entries, list->nr + 1, list->alloc); + list->entries[list->nr++] = ce; + while (i + 1 < the_index.cache_nr && + !strcmp(ce->name, the_index.cache[i + 1]->name)) + /* + * Skip entries with the same name in different stages + * to make sure an entry is returned only once. + */ + i++; + } + + if (ps_matched && report_path_error(ps_matched, pathspec)) + result = -1; + + free(ps_matched); + + return result; +} + +static void module_list_active(struct module_list *list) +{ + int i; + struct module_list active_modules = MODULE_LIST_INIT; + + for (i = 0; i < list->nr; i++) { + const struct cache_entry *ce = list->entries[i]; + + if (!is_submodule_active(the_repository, ce->name)) + continue; + + ALLOC_GROW(active_modules.entries, + active_modules.nr + 1, + active_modules.alloc); + active_modules.entries[active_modules.nr++] = ce; + } + + module_list_release(list); + *list = active_modules; +} + +static char *get_up_path(const char *path) +{ + int i; + struct strbuf sb = STRBUF_INIT; + + for (i = count_slashes(path); i; i--) + strbuf_addstr(&sb, "../"); + + /* + * Check if 'path' ends with slash or not + * for having the same output for dir/sub_dir + * and dir/sub_dir/ + */ + if (!is_dir_sep(path[strlen(path) - 1])) + strbuf_addstr(&sb, "../"); + + return strbuf_detach(&sb, NULL); +} + +static void for_each_listed_submodule(const struct module_list *list, + each_submodule_fn fn, void *cb_data) +{ + int i; + + for (i = 0; i < list->nr; i++) + fn(list->entries[i], cb_data); +} + +struct foreach_cb { + int argc; + const char **argv; + const char *prefix; + const char *super_prefix; + int quiet; + int recursive; +}; +#define FOREACH_CB_INIT { 0 } + +static void runcommand_in_submodule_cb(const struct cache_entry *list_item, + void *cb_data) +{ + struct foreach_cb *info = cb_data; + const char *path = list_item->name; + const struct object_id *ce_oid = &list_item->oid; + const struct submodule *sub; + struct child_process cp = CHILD_PROCESS_INIT; + char *displaypath; + + displaypath = get_submodule_displaypath(path, info->prefix, + info->super_prefix); + + sub = submodule_from_path(the_repository, null_oid(), path); + + if (!sub) + die(_("No url found for submodule path '%s' in .gitmodules"), + displaypath); + + if (!is_submodule_populated_gently(path, NULL)) + goto cleanup; + + prepare_submodule_repo_env(&cp.env); + + /* + * For the purpose of executing <command> in the submodule, + * separate shell is used for the purpose of running the + * child process. + */ + cp.use_shell = 1; + cp.dir = path; + + /* + * NEEDSWORK: the command currently has access to the variables $name, + * $sm_path, $displaypath, $sha1 and $toplevel only when the command + * contains a single argument. This is done for maintaining a faithful + * translation from shell script. + */ + if (info->argc == 1) { + char *toplevel = xgetcwd(); + struct strbuf sb = STRBUF_INIT; + + strvec_pushf(&cp.env, "name=%s", sub->name); + strvec_pushf(&cp.env, "sm_path=%s", path); + strvec_pushf(&cp.env, "displaypath=%s", displaypath); + strvec_pushf(&cp.env, "sha1=%s", + oid_to_hex(ce_oid)); + strvec_pushf(&cp.env, "toplevel=%s", toplevel); + + /* + * Since the path variable was accessible from the script + * before porting, it is also made available after porting. + * The environment variable "PATH" has a very special purpose + * on windows. And since environment variables are + * case-insensitive in windows, it interferes with the + * existing PATH variable. Hence, to avoid that, we expose + * path via the args strvec and not via env. + */ + sq_quote_buf(&sb, path); + strvec_pushf(&cp.args, "path=%s; %s", + sb.buf, info->argv[0]); + strbuf_release(&sb); + free(toplevel); + } else { + strvec_pushv(&cp.args, info->argv); + } + + if (!info->quiet) + printf(_("Entering '%s'\n"), displaypath); + + if (info->argv[0] && run_command(&cp)) + die(_("run_command returned non-zero status for %s\n."), + displaypath); + + if (info->recursive) { + struct child_process cpr = CHILD_PROCESS_INIT; + + cpr.git_cmd = 1; + cpr.dir = path; + prepare_submodule_repo_env(&cpr.env); + + strvec_pushl(&cpr.args, "submodule--helper", "foreach", "--recursive", + NULL); + strvec_pushl(&cpr.args, "--super-prefix", NULL); + strvec_pushf(&cpr.args, "%s/", displaypath); + + if (info->quiet) + strvec_push(&cpr.args, "--quiet"); + + strvec_push(&cpr.args, "--"); + strvec_pushv(&cpr.args, info->argv); + + if (run_command(&cpr)) + die(_("run_command returned non-zero status while " + "recursing in the nested submodules of %s\n."), + displaypath); + } + +cleanup: + free(displaypath); +} + +static int module_foreach(int argc, const char **argv, const char *prefix) +{ + struct foreach_cb info = FOREACH_CB_INIT; + struct pathspec pathspec = { 0 }; + struct module_list list = MODULE_LIST_INIT; + struct option module_foreach_options[] = { + OPT__SUPER_PREFIX(&info.super_prefix), + OPT__QUIET(&info.quiet, N_("suppress output of entering each submodule command")), + OPT_BOOL(0, "recursive", &info.recursive, + N_("recurse into nested submodules")), + OPT_END() + }; + const char *const git_submodule_helper_usage[] = { + N_("git submodule foreach [--quiet] [--recursive] [--] <command>"), + NULL + }; + int ret = 1; + + argc = parse_options(argc, argv, prefix, module_foreach_options, + git_submodule_helper_usage, 0); + + if (module_list_compute(NULL, prefix, &pathspec, &list) < 0) + goto cleanup; + + info.argc = argc; + info.argv = argv; + info.prefix = prefix; + + for_each_listed_submodule(&list, runcommand_in_submodule_cb, &info); + + ret = 0; +cleanup: + module_list_release(&list); + clear_pathspec(&pathspec); + return ret; +} + +static int starts_with_dot_slash(const char *const path) +{ + return path_match_flags(path, PATH_MATCH_STARTS_WITH_DOT_SLASH | + PATH_MATCH_XPLATFORM); +} + +static int starts_with_dot_dot_slash(const char *const path) +{ + return path_match_flags(path, PATH_MATCH_STARTS_WITH_DOT_DOT_SLASH | + PATH_MATCH_XPLATFORM); +} + +struct init_cb { + const char *prefix; + const char *super_prefix; + unsigned int flags; +}; +#define INIT_CB_INIT { 0 } + +static void init_submodule(const char *path, const char *prefix, + const char *super_prefix, + unsigned int flags) +{ + const struct submodule *sub; + struct strbuf sb = STRBUF_INIT; + const char *upd; + char *url = NULL, *displaypath; + + displaypath = get_submodule_displaypath(path, prefix, super_prefix); + + sub = submodule_from_path(the_repository, null_oid(), path); + + if (!sub) + die(_("No url found for submodule path '%s' in .gitmodules"), + displaypath); + + /* + * NEEDSWORK: In a multi-working-tree world, this needs to be + * set in the per-worktree config. + * + * Set active flag for the submodule being initialized + */ + if (!is_submodule_active(the_repository, path)) { + strbuf_addf(&sb, "submodule.%s.active", sub->name); + git_config_set_gently(sb.buf, "true"); + strbuf_reset(&sb); + } + + /* + * Copy url setting when it is not set yet. + * To look up the url in .git/config, we must not fall back to + * .gitmodules, so look it up directly. + */ + strbuf_addf(&sb, "submodule.%s.url", sub->name); + if (git_config_get_string(sb.buf, &url)) { + if (!sub->url) + die(_("No url found for submodule path '%s' in .gitmodules"), + displaypath); + + url = xstrdup(sub->url); + + /* Possibly a url relative to parent */ + if (starts_with_dot_dot_slash(url) || + starts_with_dot_slash(url)) { + char *oldurl = url; + + url = resolve_relative_url(oldurl, NULL, 0); + free(oldurl); + } + + if (git_config_set_gently(sb.buf, url)) + die(_("Failed to register url for submodule path '%s'"), + displaypath); + if (!(flags & OPT_QUIET)) + fprintf(stderr, + _("Submodule '%s' (%s) registered for path '%s'\n"), + sub->name, url, displaypath); + } + strbuf_reset(&sb); + + /* Copy "update" setting when it is not set yet */ + strbuf_addf(&sb, "submodule.%s.update", sub->name); + if (git_config_get_string_tmp(sb.buf, &upd) && + sub->update_strategy.type != SM_UPDATE_UNSPECIFIED) { + if (sub->update_strategy.type == SM_UPDATE_COMMAND) { + fprintf(stderr, _("warning: command update mode suggested for submodule '%s'\n"), + sub->name); + upd = "none"; + } else { + upd = submodule_update_type_to_string(sub->update_strategy.type); + } + + if (git_config_set_gently(sb.buf, upd)) + die(_("Failed to register update mode for submodule path '%s'"), displaypath); + } + strbuf_release(&sb); + free(displaypath); + free(url); +} + +static void init_submodule_cb(const struct cache_entry *list_item, void *cb_data) +{ + struct init_cb *info = cb_data; + + init_submodule(list_item->name, info->prefix, info->super_prefix, + info->flags); +} + +static int module_init(int argc, const char **argv, const char *prefix) +{ + struct init_cb info = INIT_CB_INIT; + struct pathspec pathspec = { 0 }; + struct module_list list = MODULE_LIST_INIT; + int quiet = 0; + struct option module_init_options[] = { + OPT__QUIET(&quiet, N_("suppress output for initializing a submodule")), + OPT_END() + }; + const char *const git_submodule_helper_usage[] = { + N_("git submodule init [<options>] [<path>]"), + NULL + }; + int ret = 1; + + argc = parse_options(argc, argv, prefix, module_init_options, + git_submodule_helper_usage, 0); + + if (module_list_compute(argv, prefix, &pathspec, &list) < 0) + goto cleanup; + + /* + * If there are no path args and submodule.active is set then, + * by default, only initialize 'active' modules. + */ + if (!argc && !git_config_get("submodule.active")) + module_list_active(&list); + + info.prefix = prefix; + if (quiet) + info.flags |= OPT_QUIET; + + for_each_listed_submodule(&list, init_submodule_cb, &info); + + ret = 0; +cleanup: + module_list_release(&list); + clear_pathspec(&pathspec); + return ret; +} + +struct status_cb { + const char *prefix; + const char *super_prefix; + unsigned int flags; +}; +#define STATUS_CB_INIT { 0 } + +static void print_status(unsigned int flags, char state, const char *path, + const struct object_id *oid, const char *displaypath) +{ + if (flags & OPT_QUIET) + return; + + printf("%c%s %s", state, oid_to_hex(oid), displaypath); + + if (state == ' ' || state == '+') { + char *name = compute_rev_name(path, oid_to_hex(oid)); + + if (name) + printf(" (%s)", name); + free(name); + } + + printf("\n"); +} + +static int handle_submodule_head_ref(const char *refname UNUSED, + const struct object_id *oid, + int flags UNUSED, + void *cb_data) +{ + struct object_id *output = cb_data; + + if (oid) + oidcpy(output, oid); + + return 0; +} + +static void status_submodule(const char *path, const struct object_id *ce_oid, + unsigned int ce_flags, const char *prefix, + const char *super_prefix, unsigned int flags) +{ + char *displaypath; + struct strvec diff_files_args = STRVEC_INIT; + struct rev_info rev = REV_INFO_INIT; + struct strbuf buf = STRBUF_INIT; + const char *git_dir; + struct setup_revision_opt opt = { + .free_removed_argv_elements = 1, + }; + + if (!submodule_from_path(the_repository, null_oid(), path)) + die(_("no submodule mapping found in .gitmodules for path '%s'"), + path); + + displaypath = get_submodule_displaypath(path, prefix, super_prefix); + + if ((CE_STAGEMASK & ce_flags) >> CE_STAGESHIFT) { + print_status(flags, 'U', path, null_oid(), displaypath); + goto cleanup; + } + + strbuf_addf(&buf, "%s/.git", path); + git_dir = read_gitfile(buf.buf); + if (!git_dir) + git_dir = buf.buf; + + if (!is_submodule_active(the_repository, path) || + !is_git_directory(git_dir)) { + print_status(flags, '-', path, ce_oid, displaypath); + strbuf_release(&buf); + goto cleanup; + } + strbuf_release(&buf); + + strvec_pushl(&diff_files_args, "diff-files", + "--ignore-submodules=dirty", "--quiet", "--", + path, NULL); + + git_config(git_diff_basic_config, NULL); + + repo_init_revisions(the_repository, &rev, NULL); + rev.abbrev = 0; + setup_revisions(diff_files_args.nr, diff_files_args.v, &rev, &opt); + run_diff_files(&rev, 0); + + if (!diff_result_code(&rev.diffopt)) { + print_status(flags, ' ', path, ce_oid, + displaypath); + } else if (!(flags & OPT_CACHED)) { + struct object_id oid; + struct ref_store *refs = get_submodule_ref_store(path); + + if (!refs) { + print_status(flags, '-', path, ce_oid, displaypath); + goto cleanup; + } + if (refs_head_ref(refs, handle_submodule_head_ref, &oid)) + die(_("could not resolve HEAD ref inside the " + "submodule '%s'"), path); + + print_status(flags, '+', path, &oid, displaypath); + } else { + print_status(flags, '+', path, ce_oid, displaypath); + } + + if (flags & OPT_RECURSIVE) { + struct child_process cpr = CHILD_PROCESS_INIT; + + cpr.git_cmd = 1; + cpr.dir = path; + prepare_submodule_repo_env(&cpr.env); + + strvec_pushl(&cpr.args, "submodule--helper", "status", + "--recursive", NULL); + strvec_push(&cpr.args, "--super-prefix"); + strvec_pushf(&cpr.args, "%s/", displaypath); + + if (flags & OPT_CACHED) + strvec_push(&cpr.args, "--cached"); + + if (flags & OPT_QUIET) + strvec_push(&cpr.args, "--quiet"); + + if (run_command(&cpr)) + die(_("failed to recurse into submodule '%s'"), path); + } + +cleanup: + strvec_clear(&diff_files_args); + free(displaypath); + release_revisions(&rev); +} + +static void status_submodule_cb(const struct cache_entry *list_item, + void *cb_data) +{ + struct status_cb *info = cb_data; + + status_submodule(list_item->name, &list_item->oid, list_item->ce_flags, + info->prefix, info->super_prefix, info->flags); +} + +static int module_status(int argc, const char **argv, const char *prefix) +{ + struct status_cb info = STATUS_CB_INIT; + struct pathspec pathspec = { 0 }; + struct module_list list = MODULE_LIST_INIT; + int quiet = 0; + struct option module_status_options[] = { + OPT__SUPER_PREFIX(&info.super_prefix), + OPT__QUIET(&quiet, N_("suppress submodule status output")), + OPT_BIT(0, "cached", &info.flags, N_("use commit stored in the index instead of the one stored in the submodule HEAD"), OPT_CACHED), + OPT_BIT(0, "recursive", &info.flags, N_("recurse into nested submodules"), OPT_RECURSIVE), + OPT_END() + }; + const char *const git_submodule_helper_usage[] = { + N_("git submodule status [--quiet] [--cached] [--recursive] [<path>...]"), + NULL + }; + int ret = 1; + + argc = parse_options(argc, argv, prefix, module_status_options, + git_submodule_helper_usage, 0); + + if (module_list_compute(argv, prefix, &pathspec, &list) < 0) + goto cleanup; + + info.prefix = prefix; + if (quiet) + info.flags |= OPT_QUIET; + + for_each_listed_submodule(&list, status_submodule_cb, &info); + + ret = 0; +cleanup: + module_list_release(&list); + clear_pathspec(&pathspec); + return ret; +} + +struct module_cb { + unsigned int mod_src; + unsigned int mod_dst; + struct object_id oid_src; + struct object_id oid_dst; + char status; + char *sm_path; +}; +#define MODULE_CB_INIT { 0 } + +static void module_cb_release(struct module_cb *mcb) +{ + free(mcb->sm_path); +} + +struct module_cb_list { + struct module_cb **entries; + int alloc, nr; +}; +#define MODULE_CB_LIST_INIT { 0 } + +static void module_cb_list_release(struct module_cb_list *mcbl) +{ + int i; + + for (i = 0; i < mcbl->nr; i++) { + struct module_cb *mcb = mcbl->entries[i]; + + module_cb_release(mcb); + free(mcb); + } + free(mcbl->entries); +} + +struct summary_cb { + int argc; + const char **argv; + const char *prefix; + const char *super_prefix; + unsigned int cached: 1; + unsigned int for_status: 1; + unsigned int files: 1; + int summary_limit; +}; +#define SUMMARY_CB_INIT { 0 } + +enum diff_cmd { + DIFF_INDEX, + DIFF_FILES +}; + +static char *verify_submodule_committish(const char *sm_path, + const char *committish) +{ + struct child_process cp_rev_parse = CHILD_PROCESS_INIT; + struct strbuf result = STRBUF_INIT; + + cp_rev_parse.git_cmd = 1; + cp_rev_parse.dir = sm_path; + prepare_submodule_repo_env(&cp_rev_parse.env); + strvec_pushl(&cp_rev_parse.args, "rev-parse", "-q", "--short", NULL); + strvec_pushf(&cp_rev_parse.args, "%s^0", committish); + strvec_push(&cp_rev_parse.args, "--"); + + if (capture_command(&cp_rev_parse, &result, 0)) + return NULL; + + strbuf_trim_trailing_newline(&result); + return strbuf_detach(&result, NULL); +} + +static void print_submodule_summary(struct summary_cb *info, const char *errmsg, + int total_commits, const char *displaypath, + const char *src_abbrev, const char *dst_abbrev, + struct module_cb *p) +{ + if (p->status == 'T') { + if (S_ISGITLINK(p->mod_dst)) + printf(_("* %s %s(blob)->%s(submodule)"), + displaypath, src_abbrev, dst_abbrev); + else + printf(_("* %s %s(submodule)->%s(blob)"), + displaypath, src_abbrev, dst_abbrev); + } else { + printf("* %s %s...%s", + displaypath, src_abbrev, dst_abbrev); + } + + if (total_commits < 0) + printf(":\n"); + else + printf(" (%d):\n", total_commits); + + if (errmsg) { + printf(_("%s"), errmsg); + } else if (total_commits > 0) { + struct child_process cp_log = CHILD_PROCESS_INIT; + + cp_log.git_cmd = 1; + cp_log.dir = p->sm_path; + prepare_submodule_repo_env(&cp_log.env); + strvec_pushl(&cp_log.args, "log", NULL); + + if (S_ISGITLINK(p->mod_src) && S_ISGITLINK(p->mod_dst)) { + if (info->summary_limit > 0) + strvec_pushf(&cp_log.args, "-%d", + info->summary_limit); + + strvec_pushl(&cp_log.args, "--pretty= %m %s", + "--first-parent", NULL); + strvec_pushf(&cp_log.args, "%s...%s", + src_abbrev, dst_abbrev); + } else if (S_ISGITLINK(p->mod_dst)) { + strvec_pushl(&cp_log.args, "--pretty= > %s", + "-1", dst_abbrev, NULL); + } else { + strvec_pushl(&cp_log.args, "--pretty= < %s", + "-1", src_abbrev, NULL); + } + run_command(&cp_log); + } + printf("\n"); +} + +static void generate_submodule_summary(struct summary_cb *info, + struct module_cb *p) +{ + char *displaypath, *src_abbrev = NULL, *dst_abbrev; + int missing_src = 0, missing_dst = 0; + struct strbuf errmsg = STRBUF_INIT; + int total_commits = -1; + + if (!info->cached && oideq(&p->oid_dst, null_oid())) { + if (S_ISGITLINK(p->mod_dst)) { + struct ref_store *refs = get_submodule_ref_store(p->sm_path); + + if (refs) + refs_head_ref(refs, handle_submodule_head_ref, &p->oid_dst); + } else if (S_ISLNK(p->mod_dst) || S_ISREG(p->mod_dst)) { + struct stat st; + int fd = open(p->sm_path, O_RDONLY); + + if (fd < 0 || fstat(fd, &st) < 0 || + index_fd(&the_index, &p->oid_dst, fd, &st, OBJ_BLOB, + p->sm_path, 0)) + error(_("couldn't hash object from '%s'"), p->sm_path); + } else { + /* for a submodule removal (mode:0000000), don't warn */ + if (p->mod_dst) + warning(_("unexpected mode %o\n"), p->mod_dst); + } + } + + if (S_ISGITLINK(p->mod_src)) { + if (p->status != 'D') + src_abbrev = verify_submodule_committish(p->sm_path, + oid_to_hex(&p->oid_src)); + if (!src_abbrev) { + missing_src = 1; + /* + * As `rev-parse` failed, we fallback to getting + * the abbreviated hash using oid_src. We do + * this as we might still need the abbreviated + * hash in cases like a submodule type change, etc. + */ + src_abbrev = xstrndup(oid_to_hex(&p->oid_src), 7); + } + } else { + /* + * The source does not point to a submodule. + * So, we fallback to getting the abbreviation using + * oid_src as we might still need the abbreviated + * hash in cases like submodule add, etc. + */ + src_abbrev = xstrndup(oid_to_hex(&p->oid_src), 7); + } + + if (S_ISGITLINK(p->mod_dst)) { + dst_abbrev = verify_submodule_committish(p->sm_path, + oid_to_hex(&p->oid_dst)); + if (!dst_abbrev) { + missing_dst = 1; + /* + * As `rev-parse` failed, we fallback to getting + * the abbreviated hash using oid_dst. We do + * this as we might still need the abbreviated + * hash in cases like a submodule type change, etc. + */ + dst_abbrev = xstrndup(oid_to_hex(&p->oid_dst), 7); + } + } else { + /* + * The destination does not point to a submodule. + * So, we fallback to getting the abbreviation using + * oid_dst as we might still need the abbreviated + * hash in cases like a submodule removal, etc. + */ + dst_abbrev = xstrndup(oid_to_hex(&p->oid_dst), 7); + } + + displaypath = get_submodule_displaypath(p->sm_path, info->prefix, + info->super_prefix); + + if (!missing_src && !missing_dst) { + struct child_process cp_rev_list = CHILD_PROCESS_INIT; + struct strbuf sb_rev_list = STRBUF_INIT; + + strvec_pushl(&cp_rev_list.args, "rev-list", + "--first-parent", "--count", NULL); + if (S_ISGITLINK(p->mod_src) && S_ISGITLINK(p->mod_dst)) + strvec_pushf(&cp_rev_list.args, "%s...%s", + src_abbrev, dst_abbrev); + else + strvec_push(&cp_rev_list.args, S_ISGITLINK(p->mod_src) ? + src_abbrev : dst_abbrev); + strvec_push(&cp_rev_list.args, "--"); + + cp_rev_list.git_cmd = 1; + cp_rev_list.dir = p->sm_path; + prepare_submodule_repo_env(&cp_rev_list.env); + + if (!capture_command(&cp_rev_list, &sb_rev_list, 0)) + total_commits = atoi(sb_rev_list.buf); + + strbuf_release(&sb_rev_list); + } else { + /* + * Don't give error msg for modification whose dst is not + * submodule, i.e., deleted or changed to blob + */ + if (S_ISGITLINK(p->mod_dst)) { + if (missing_src && missing_dst) { + strbuf_addf(&errmsg, " Warn: %s doesn't contain commits %s and %s\n", + displaypath, oid_to_hex(&p->oid_src), + oid_to_hex(&p->oid_dst)); + } else { + strbuf_addf(&errmsg, " Warn: %s doesn't contain commit %s\n", + displaypath, missing_src ? + oid_to_hex(&p->oid_src) : + oid_to_hex(&p->oid_dst)); + } + } + } + + print_submodule_summary(info, errmsg.len ? errmsg.buf : NULL, + total_commits, displaypath, src_abbrev, + dst_abbrev, p); + + free(displaypath); + free(src_abbrev); + free(dst_abbrev); + strbuf_release(&errmsg); +} + +static void prepare_submodule_summary(struct summary_cb *info, + struct module_cb_list *list) +{ + int i; + for (i = 0; i < list->nr; i++) { + const struct submodule *sub; + struct module_cb *p = list->entries[i]; + struct strbuf sm_gitdir = STRBUF_INIT; + + if (p->status == 'D' || p->status == 'T') { + generate_submodule_summary(info, p); + continue; + } + + if (info->for_status && p->status != 'A' && + (sub = submodule_from_path(the_repository, + null_oid(), p->sm_path))) { + char *config_key = NULL; + const char *value; + int ignore_all = 0; + + config_key = xstrfmt("submodule.%s.ignore", + sub->name); + if (!git_config_get_string_tmp(config_key, &value)) + ignore_all = !strcmp(value, "all"); + else if (sub->ignore) + ignore_all = !strcmp(sub->ignore, "all"); + + free(config_key); + if (ignore_all) + continue; + } + + /* Also show added or modified modules which are checked out */ + strbuf_addstr(&sm_gitdir, p->sm_path); + if (is_nonbare_repository_dir(&sm_gitdir)) + generate_submodule_summary(info, p); + strbuf_release(&sm_gitdir); + } +} + +static void submodule_summary_callback(struct diff_queue_struct *q, + struct diff_options *options UNUSED, + void *data) +{ + int i; + struct module_cb_list *list = data; + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + struct module_cb *temp; + + if (!S_ISGITLINK(p->one->mode) && !S_ISGITLINK(p->two->mode)) + continue; + temp = (struct module_cb*)malloc(sizeof(struct module_cb)); + temp->mod_src = p->one->mode; + temp->mod_dst = p->two->mode; + temp->oid_src = p->one->oid; + temp->oid_dst = p->two->oid; + temp->status = p->status; + temp->sm_path = xstrdup(p->one->path); + + ALLOC_GROW(list->entries, list->nr + 1, list->alloc); + list->entries[list->nr++] = temp; + } +} + +static const char *get_diff_cmd(enum diff_cmd diff_cmd) +{ + switch (diff_cmd) { + case DIFF_INDEX: return "diff-index"; + case DIFF_FILES: return "diff-files"; + default: BUG("bad diff_cmd value %d", diff_cmd); + } +} + +static int compute_summary_module_list(struct object_id *head_oid, + struct summary_cb *info, + enum diff_cmd diff_cmd) +{ + struct strvec diff_args = STRVEC_INIT; + struct rev_info rev; + struct setup_revision_opt opt = { + .free_removed_argv_elements = 1, + }; + struct module_cb_list list = MODULE_CB_LIST_INIT; + int ret = 0; + + strvec_push(&diff_args, get_diff_cmd(diff_cmd)); + if (info->cached) + strvec_push(&diff_args, "--cached"); + strvec_pushl(&diff_args, "--ignore-submodules=dirty", "--raw", NULL); + if (head_oid) + strvec_push(&diff_args, oid_to_hex(head_oid)); + strvec_push(&diff_args, "--"); + if (info->argc) + strvec_pushv(&diff_args, info->argv); + + git_config(git_diff_basic_config, NULL); + repo_init_revisions(the_repository, &rev, info->prefix); + rev.abbrev = 0; + precompose_argv_prefix(diff_args.nr, diff_args.v, NULL); + setup_revisions(diff_args.nr, diff_args.v, &rev, &opt); + rev.diffopt.output_format = DIFF_FORMAT_NO_OUTPUT | DIFF_FORMAT_CALLBACK; + rev.diffopt.format_callback = submodule_summary_callback; + rev.diffopt.format_callback_data = &list; + + if (!info->cached) { + if (diff_cmd == DIFF_INDEX) + setup_work_tree(); + if (repo_read_index_preload(the_repository, &rev.diffopt.pathspec, 0) < 0) { + perror("repo_read_index_preload"); + ret = -1; + goto cleanup; + } + } else if (repo_read_index(the_repository) < 0) { + perror("repo_read_cache"); + ret = -1; + goto cleanup; + } + + if (diff_cmd == DIFF_INDEX) + run_diff_index(&rev, info->cached ? DIFF_INDEX_CACHED : 0); + else + run_diff_files(&rev, 0); + prepare_submodule_summary(info, &list); +cleanup: + strvec_clear(&diff_args); + release_revisions(&rev); + module_cb_list_release(&list); + return ret; +} + +static int module_summary(int argc, const char **argv, const char *prefix) +{ + struct summary_cb info = SUMMARY_CB_INIT; + int cached = 0; + int for_status = 0; + int files = 0; + int summary_limit = -1; + enum diff_cmd diff_cmd = DIFF_INDEX; + struct object_id head_oid; + int ret; + struct option module_summary_options[] = { + OPT_BOOL(0, "cached", &cached, + N_("use the commit stored in the index instead of the submodule HEAD")), + OPT_BOOL(0, "files", &files, + N_("compare the commit in the index with that in the submodule HEAD")), + OPT_BOOL(0, "for-status", &for_status, + N_("skip submodules with 'ignore_config' value set to 'all'")), + OPT_INTEGER('n', "summary-limit", &summary_limit, + N_("limit the summary size")), + OPT_END() + }; + const char *const git_submodule_helper_usage[] = { + N_("git submodule summary [<options>] [<commit>] [--] [<path>]"), + NULL + }; + + argc = parse_options(argc, argv, prefix, module_summary_options, + git_submodule_helper_usage, 0); + + if (!summary_limit) + return 0; + + if (!repo_get_oid(the_repository, argc ? argv[0] : "HEAD", &head_oid)) { + if (argc) { + argv++; + argc--; + } + } else if (!argc || !strcmp(argv[0], "HEAD")) { + /* before the first commit: compare with an empty tree */ + oidcpy(&head_oid, the_hash_algo->empty_tree); + if (argc) { + argv++; + argc--; + } + } else { + if (repo_get_oid(the_repository, "HEAD", &head_oid)) + die(_("could not fetch a revision for HEAD")); + } + + if (files) { + if (cached) + die(_("options '%s' and '%s' cannot be used together"), "--cached", "--files"); + diff_cmd = DIFF_FILES; + } + + info.argc = argc; + info.argv = argv; + info.prefix = prefix; + info.cached = !!cached; + info.files = !!files; + info.for_status = !!for_status; + info.summary_limit = summary_limit; + + ret = compute_summary_module_list((diff_cmd == DIFF_INDEX) ? &head_oid : NULL, + &info, diff_cmd); + return ret; +} + +struct sync_cb { + const char *prefix; + const char *super_prefix; + unsigned int flags; +}; +#define SYNC_CB_INIT { 0 } + +static void sync_submodule(const char *path, const char *prefix, + const char *super_prefix, unsigned int flags) +{ + const struct submodule *sub; + char *remote_key = NULL; + char *sub_origin_url, *super_config_url, *displaypath, *default_remote; + struct strbuf sb = STRBUF_INIT; + char *sub_config_path = NULL; + int code; + + if (!is_submodule_active(the_repository, path)) + return; + + sub = submodule_from_path(the_repository, null_oid(), path); + + if (sub && sub->url) { + if (starts_with_dot_dot_slash(sub->url) || + starts_with_dot_slash(sub->url)) { + char *up_path = get_up_path(path); + + sub_origin_url = resolve_relative_url(sub->url, up_path, 1); + super_config_url = resolve_relative_url(sub->url, NULL, 1); + free(up_path); + } else { + sub_origin_url = xstrdup(sub->url); + super_config_url = xstrdup(sub->url); + } + } else { + sub_origin_url = xstrdup(""); + super_config_url = xstrdup(""); + } + + displaypath = get_submodule_displaypath(path, prefix, super_prefix); + + if (!(flags & OPT_QUIET)) + printf(_("Synchronizing submodule url for '%s'\n"), + displaypath); + + strbuf_reset(&sb); + strbuf_addf(&sb, "submodule.%s.url", sub->name); + if (git_config_set_gently(sb.buf, super_config_url)) + die(_("failed to register url for submodule path '%s'"), + displaypath); + + if (!is_submodule_populated_gently(path, NULL)) + goto cleanup; + + strbuf_reset(&sb); + code = get_default_remote_submodule(path, &default_remote); + if (code) + exit(code); + + remote_key = xstrfmt("remote.%s.url", default_remote); + free(default_remote); + + submodule_to_gitdir(&sb, path); + strbuf_addstr(&sb, "/config"); + + if (git_config_set_in_file_gently(sb.buf, remote_key, sub_origin_url)) + die(_("failed to update remote for submodule '%s'"), + path); + + if (flags & OPT_RECURSIVE) { + struct child_process cpr = CHILD_PROCESS_INIT; + + cpr.git_cmd = 1; + cpr.dir = path; + prepare_submodule_repo_env(&cpr.env); + + strvec_pushl(&cpr.args, "submodule--helper", "sync", + "--recursive", NULL); + strvec_push(&cpr.args, "--super-prefix"); + strvec_pushf(&cpr.args, "%s/", displaypath); + + + if (flags & OPT_QUIET) + strvec_push(&cpr.args, "--quiet"); + + if (run_command(&cpr)) + die(_("failed to recurse into submodule '%s'"), + path); + } + +cleanup: + free(super_config_url); + free(sub_origin_url); + strbuf_release(&sb); + free(remote_key); + free(displaypath); + free(sub_config_path); +} + +static void sync_submodule_cb(const struct cache_entry *list_item, void *cb_data) +{ + struct sync_cb *info = cb_data; + + sync_submodule(list_item->name, info->prefix, info->super_prefix, + info->flags); +} + +static int module_sync(int argc, const char **argv, const char *prefix) +{ + struct sync_cb info = SYNC_CB_INIT; + struct pathspec pathspec = { 0 }; + struct module_list list = MODULE_LIST_INIT; + int quiet = 0; + int recursive = 0; + struct option module_sync_options[] = { + OPT__SUPER_PREFIX(&info.super_prefix), + OPT__QUIET(&quiet, N_("suppress output of synchronizing submodule url")), + OPT_BOOL(0, "recursive", &recursive, + N_("recurse into nested submodules")), + OPT_END() + }; + const char *const git_submodule_helper_usage[] = { + N_("git submodule sync [--quiet] [--recursive] [<path>]"), + NULL + }; + int ret = 1; + + argc = parse_options(argc, argv, prefix, module_sync_options, + git_submodule_helper_usage, 0); + + if (module_list_compute(argv, prefix, &pathspec, &list) < 0) + goto cleanup; + + info.prefix = prefix; + if (quiet) + info.flags |= OPT_QUIET; + if (recursive) + info.flags |= OPT_RECURSIVE; + + for_each_listed_submodule(&list, sync_submodule_cb, &info); + + ret = 0; +cleanup: + module_list_release(&list); + clear_pathspec(&pathspec); + return ret; +} + +struct deinit_cb { + const char *prefix; + unsigned int flags; +}; +#define DEINIT_CB_INIT { 0 } + +static void deinit_submodule(const char *path, const char *prefix, + unsigned int flags) +{ + const struct submodule *sub; + char *displaypath = NULL; + struct child_process cp_config = CHILD_PROCESS_INIT; + struct strbuf sb_config = STRBUF_INIT; + char *sub_git_dir = xstrfmt("%s/.git", path); + + sub = submodule_from_path(the_repository, null_oid(), path); + + if (!sub || !sub->name) + goto cleanup; + + displaypath = get_submodule_displaypath(path, prefix, NULL); + + /* remove the submodule work tree (unless the user already did it) */ + if (is_directory(path)) { + struct strbuf sb_rm = STRBUF_INIT; + const char *format; + + if (is_directory(sub_git_dir)) { + if (!(flags & OPT_QUIET)) + warning(_("Submodule work tree '%s' contains a .git " + "directory. This will be replaced with a " + ".git file by using absorbgitdirs."), + displaypath); + + absorb_git_dir_into_superproject(path, NULL); + + } + + if (!(flags & OPT_FORCE)) { + struct child_process cp_rm = CHILD_PROCESS_INIT; + + cp_rm.git_cmd = 1; + strvec_pushl(&cp_rm.args, "rm", "-qn", + path, NULL); + + if (run_command(&cp_rm)) + die(_("Submodule work tree '%s' contains local " + "modifications; use '-f' to discard them"), + displaypath); + } + + strbuf_addstr(&sb_rm, path); + + if (!remove_dir_recursively(&sb_rm, 0)) + format = _("Cleared directory '%s'\n"); + else + format = _("Could not remove submodule work tree '%s'\n"); + + if (!(flags & OPT_QUIET)) + printf(format, displaypath); + + submodule_unset_core_worktree(sub); + + strbuf_release(&sb_rm); + } + + if (mkdir(path, 0777)) + printf(_("could not create empty submodule directory %s"), + displaypath); + + cp_config.git_cmd = 1; + strvec_pushl(&cp_config.args, "config", "--get-regexp", NULL); + strvec_pushf(&cp_config.args, "submodule.%s\\.", sub->name); + + /* remove the .git/config entries (unless the user already did it) */ + if (!capture_command(&cp_config, &sb_config, 0) && sb_config.len) { + char *sub_key = xstrfmt("submodule.%s", sub->name); + + /* + * remove the whole section so we have a clean state when + * the user later decides to init this submodule again + */ + git_config_rename_section_in_file(NULL, sub_key, NULL); + if (!(flags & OPT_QUIET)) + printf(_("Submodule '%s' (%s) unregistered for path '%s'\n"), + sub->name, sub->url, displaypath); + free(sub_key); + } + +cleanup: + free(displaypath); + free(sub_git_dir); + strbuf_release(&sb_config); +} + +static void deinit_submodule_cb(const struct cache_entry *list_item, + void *cb_data) +{ + struct deinit_cb *info = cb_data; + deinit_submodule(list_item->name, info->prefix, info->flags); +} + +static int module_deinit(int argc, const char **argv, const char *prefix) +{ + struct deinit_cb info = DEINIT_CB_INIT; + struct pathspec pathspec = { 0 }; + struct module_list list = MODULE_LIST_INIT; + int quiet = 0; + int force = 0; + int all = 0; + struct option module_deinit_options[] = { + OPT__QUIET(&quiet, N_("suppress submodule status output")), + OPT__FORCE(&force, N_("remove submodule working trees even if they contain local changes"), 0), + OPT_BOOL(0, "all", &all, N_("unregister all submodules")), + OPT_END() + }; + const char *const git_submodule_helper_usage[] = { + N_("git submodule deinit [--quiet] [-f | --force] [--all | [--] [<path>...]]"), + NULL + }; + int ret = 1; + + argc = parse_options(argc, argv, prefix, module_deinit_options, + git_submodule_helper_usage, 0); + + if (all && argc) { + error("pathspec and --all are incompatible"); + usage_with_options(git_submodule_helper_usage, + module_deinit_options); + } + + if (!argc && !all) + die(_("Use '--all' if you really want to deinitialize all submodules")); + + if (module_list_compute(argv, prefix, &pathspec, &list) < 0) + goto cleanup; + + info.prefix = prefix; + if (quiet) + info.flags |= OPT_QUIET; + if (force) + info.flags |= OPT_FORCE; + + for_each_listed_submodule(&list, deinit_submodule_cb, &info); + + ret = 0; +cleanup: + module_list_release(&list); + clear_pathspec(&pathspec); + return ret; +} + +struct module_clone_data { + const char *prefix; + const char *path; + const char *name; + const char *url; + const char *depth; + struct list_objects_filter_options *filter_options; + unsigned int quiet: 1; + unsigned int progress: 1; + unsigned int dissociate: 1; + unsigned int require_init: 1; + int single_branch; +}; +#define MODULE_CLONE_DATA_INIT { \ + .single_branch = -1, \ +} + +struct submodule_alternate_setup { + const char *submodule_name; + enum SUBMODULE_ALTERNATE_ERROR_MODE { + SUBMODULE_ALTERNATE_ERROR_DIE, + SUBMODULE_ALTERNATE_ERROR_INFO, + SUBMODULE_ALTERNATE_ERROR_IGNORE + } error_mode; + struct string_list *reference; +}; +#define SUBMODULE_ALTERNATE_SETUP_INIT { \ + .error_mode = SUBMODULE_ALTERNATE_ERROR_IGNORE, \ +} + +static const char alternate_error_advice[] = N_( +"An alternate computed from a superproject's alternate is invalid.\n" +"To allow Git to clone without an alternate in such a case, set\n" +"submodule.alternateErrorStrategy to 'info' or, equivalently, clone with\n" +"'--reference-if-able' instead of '--reference'." +); + +static int add_possible_reference_from_superproject( + struct object_directory *odb, void *sas_cb) +{ + struct submodule_alternate_setup *sas = sas_cb; + size_t len; + + /* + * If the alternate object store is another repository, try the + * standard layout with .git/(modules/<name>)+/objects + */ + if (strip_suffix(odb->path, "/objects", &len)) { + struct repository alternate; + char *sm_alternate; + struct strbuf sb = STRBUF_INIT; + struct strbuf err = STRBUF_INIT; + strbuf_add(&sb, odb->path, len); + + if (repo_init(&alternate, sb.buf, NULL) < 0) + die(_("could not get a repository handle for gitdir '%s'"), + sb.buf); + + /* + * We need to end the new path with '/' to mark it as a dir, + * otherwise a submodule name containing '/' will be broken + * as the last part of a missing submodule reference would + * be taken as a file name. + */ + strbuf_reset(&sb); + submodule_name_to_gitdir(&sb, &alternate, sas->submodule_name); + strbuf_addch(&sb, '/'); + repo_clear(&alternate); + + sm_alternate = compute_alternate_path(sb.buf, &err); + if (sm_alternate) { + char *p = strbuf_detach(&sb, NULL); + + string_list_append(sas->reference, p)->util = p; + free(sm_alternate); + } else { + switch (sas->error_mode) { + case SUBMODULE_ALTERNATE_ERROR_DIE: + if (advice_enabled(ADVICE_SUBMODULE_ALTERNATE_ERROR_STRATEGY_DIE)) + advise(_(alternate_error_advice)); + die(_("submodule '%s' cannot add alternate: %s"), + sas->submodule_name, err.buf); + case SUBMODULE_ALTERNATE_ERROR_INFO: + fprintf_ln(stderr, _("submodule '%s' cannot add alternate: %s"), + sas->submodule_name, err.buf); + case SUBMODULE_ALTERNATE_ERROR_IGNORE: + ; /* nothing */ + } + } + strbuf_release(&sb); + } + + return 0; +} + +static void prepare_possible_alternates(const char *sm_name, + struct string_list *reference) +{ + char *sm_alternate = NULL, *error_strategy = NULL; + struct submodule_alternate_setup sas = SUBMODULE_ALTERNATE_SETUP_INIT; + + git_config_get_string("submodule.alternateLocation", &sm_alternate); + if (!sm_alternate) + return; + + git_config_get_string("submodule.alternateErrorStrategy", &error_strategy); + + if (!error_strategy) + error_strategy = xstrdup("die"); + + sas.submodule_name = sm_name; + sas.reference = reference; + if (!strcmp(error_strategy, "die")) + sas.error_mode = SUBMODULE_ALTERNATE_ERROR_DIE; + else if (!strcmp(error_strategy, "info")) + sas.error_mode = SUBMODULE_ALTERNATE_ERROR_INFO; + else if (!strcmp(error_strategy, "ignore")) + sas.error_mode = SUBMODULE_ALTERNATE_ERROR_IGNORE; + else + die(_("Value '%s' for submodule.alternateErrorStrategy is not recognized"), error_strategy); + + if (!strcmp(sm_alternate, "superproject")) + foreach_alt_odb(add_possible_reference_from_superproject, &sas); + else if (!strcmp(sm_alternate, "no")) + ; /* do nothing */ + else + die(_("Value '%s' for submodule.alternateLocation is not recognized"), sm_alternate); + + free(sm_alternate); + free(error_strategy); +} + +static char *clone_submodule_sm_gitdir(const char *name) +{ + struct strbuf sb = STRBUF_INIT; + char *sm_gitdir; + + submodule_name_to_gitdir(&sb, the_repository, name); + sm_gitdir = absolute_pathdup(sb.buf); + strbuf_release(&sb); + + return sm_gitdir; +} + +static int clone_submodule(const struct module_clone_data *clone_data, + struct string_list *reference) +{ + char *p; + char *sm_gitdir = clone_submodule_sm_gitdir(clone_data->name); + char *sm_alternate = NULL, *error_strategy = NULL; + struct child_process cp = CHILD_PROCESS_INIT; + const char *clone_data_path = clone_data->path; + char *to_free = NULL; + + if (!is_absolute_path(clone_data->path)) + clone_data_path = to_free = xstrfmt("%s/%s", get_git_work_tree(), + clone_data->path); + + if (validate_submodule_git_dir(sm_gitdir, clone_data->name) < 0) + die(_("refusing to create/use '%s' in another submodule's " + "git dir"), sm_gitdir); + + if (!file_exists(sm_gitdir)) { + if (safe_create_leading_directories_const(sm_gitdir) < 0) + die(_("could not create directory '%s'"), sm_gitdir); + + prepare_possible_alternates(clone_data->name, reference); + + strvec_push(&cp.args, "clone"); + strvec_push(&cp.args, "--no-checkout"); + if (clone_data->quiet) + strvec_push(&cp.args, "--quiet"); + if (clone_data->progress) + strvec_push(&cp.args, "--progress"); + if (clone_data->depth && *(clone_data->depth)) + strvec_pushl(&cp.args, "--depth", clone_data->depth, NULL); + if (reference->nr) { + struct string_list_item *item; + + for_each_string_list_item(item, reference) + strvec_pushl(&cp.args, "--reference", + item->string, NULL); + } + if (clone_data->dissociate) + strvec_push(&cp.args, "--dissociate"); + if (sm_gitdir && *sm_gitdir) + strvec_pushl(&cp.args, "--separate-git-dir", sm_gitdir, NULL); + if (clone_data->filter_options && clone_data->filter_options->choice) + strvec_pushf(&cp.args, "--filter=%s", + expand_list_objects_filter_spec( + clone_data->filter_options)); + if (clone_data->single_branch >= 0) + strvec_push(&cp.args, clone_data->single_branch ? + "--single-branch" : + "--no-single-branch"); + + strvec_push(&cp.args, "--"); + strvec_push(&cp.args, clone_data->url); + strvec_push(&cp.args, clone_data_path); + + cp.git_cmd = 1; + prepare_submodule_repo_env(&cp.env); + cp.no_stdin = 1; + + if(run_command(&cp)) + die(_("clone of '%s' into submodule path '%s' failed"), + clone_data->url, clone_data_path); + } else { + char *path; + + if (clone_data->require_init && !access(clone_data_path, X_OK) && + !is_empty_dir(clone_data_path)) + die(_("directory not empty: '%s'"), clone_data_path); + if (safe_create_leading_directories_const(clone_data_path) < 0) + die(_("could not create directory '%s'"), clone_data_path); + path = xstrfmt("%s/index", sm_gitdir); + unlink_or_warn(path); + free(path); + } + + connect_work_tree_and_git_dir(clone_data_path, sm_gitdir, 0); + + p = git_pathdup_submodule(clone_data_path, "config"); + if (!p) + die(_("could not get submodule directory for '%s'"), clone_data_path); + + /* setup alternateLocation and alternateErrorStrategy in the cloned submodule if needed */ + git_config_get_string("submodule.alternateLocation", &sm_alternate); + if (sm_alternate) + git_config_set_in_file(p, "submodule.alternateLocation", + sm_alternate); + git_config_get_string("submodule.alternateErrorStrategy", &error_strategy); + if (error_strategy) + git_config_set_in_file(p, "submodule.alternateErrorStrategy", + error_strategy); + + free(sm_alternate); + free(error_strategy); + + free(sm_gitdir); + free(p); + free(to_free); + return 0; +} + +static int module_clone(int argc, const char **argv, const char *prefix) +{ + int dissociate = 0, quiet = 0, progress = 0, require_init = 0; + struct module_clone_data clone_data = MODULE_CLONE_DATA_INIT; + struct string_list reference = STRING_LIST_INIT_NODUP; + struct list_objects_filter_options filter_options = + LIST_OBJECTS_FILTER_INIT; + + struct option module_clone_options[] = { + OPT_STRING(0, "prefix", &clone_data.prefix, + N_("path"), + N_("alternative anchor for relative paths")), + OPT_STRING(0, "path", &clone_data.path, + N_("path"), + N_("where the new submodule will be cloned to")), + OPT_STRING(0, "name", &clone_data.name, + N_("string"), + N_("name of the new submodule")), + OPT_STRING(0, "url", &clone_data.url, + N_("string"), + N_("url where to clone the submodule from")), + OPT_STRING_LIST(0, "reference", &reference, + N_("repo"), + N_("reference repository")), + OPT_BOOL(0, "dissociate", &dissociate, + N_("use --reference only while cloning")), + OPT_STRING(0, "depth", &clone_data.depth, + N_("string"), + N_("depth for shallow clones")), + OPT__QUIET(&quiet, "suppress output for cloning a submodule"), + OPT_BOOL(0, "progress", &progress, + N_("force cloning progress")), + OPT_BOOL(0, "require-init", &require_init, + N_("disallow cloning into non-empty directory")), + OPT_BOOL(0, "single-branch", &clone_data.single_branch, + N_("clone only one branch, HEAD or --branch")), + OPT_PARSE_LIST_OBJECTS_FILTER(&filter_options), + OPT_END() + }; + const char *const git_submodule_helper_usage[] = { + N_("git submodule--helper clone [--prefix=<path>] [--quiet] " + "[--reference <repository>] [--name <name>] [--depth <depth>] " + "[--single-branch] [--filter <filter-spec>] " + "--url <url> --path <path>"), + NULL + }; + + argc = parse_options(argc, argv, prefix, module_clone_options, + git_submodule_helper_usage, 0); + + clone_data.dissociate = !!dissociate; + clone_data.quiet = !!quiet; + clone_data.progress = !!progress; + clone_data.require_init = !!require_init; + clone_data.filter_options = &filter_options; + + if (argc || !clone_data.url || !clone_data.path || !*(clone_data.path)) + usage_with_options(git_submodule_helper_usage, + module_clone_options); + + clone_submodule(&clone_data, &reference); + list_objects_filter_release(&filter_options); + string_list_clear(&reference, 1); + return 0; +} + +static int determine_submodule_update_strategy(struct repository *r, + int just_cloned, + const char *path, + enum submodule_update_type update, + struct submodule_update_strategy *out) +{ + const struct submodule *sub = submodule_from_path(r, null_oid(), path); + char *key; + const char *val; + int ret; + + key = xstrfmt("submodule.%s.update", sub->name); + + if (update) { + out->type = update; + } else if (!repo_config_get_string_tmp(r, key, &val)) { + if (parse_submodule_update_strategy(val, out) < 0) { + ret = die_message(_("Invalid update mode '%s' configured for submodule path '%s'"), + val, path); + goto cleanup; + } + } else if (sub->update_strategy.type != SM_UPDATE_UNSPECIFIED) { + if (sub->update_strategy.type == SM_UPDATE_COMMAND) + BUG("how did we read update = !command from .gitmodules?"); + out->type = sub->update_strategy.type; + out->command = sub->update_strategy.command; + } else + out->type = SM_UPDATE_CHECKOUT; + + if (just_cloned && + (out->type == SM_UPDATE_MERGE || + out->type == SM_UPDATE_REBASE || + out->type == SM_UPDATE_NONE)) + out->type = SM_UPDATE_CHECKOUT; + + ret = 0; +cleanup: + free(key); + return ret; +} + +struct update_clone_data { + const struct submodule *sub; + struct object_id oid; + unsigned just_cloned; +}; + +struct submodule_update_clone { + /* index into 'update_data.list', the list of submodules to look into for cloning */ + int current; + + /* configuration parameters which are passed on to the children */ + const struct update_data *update_data; + + /* to be consumed by update_submodule() */ + struct update_clone_data *update_clone; + int update_clone_nr; int update_clone_alloc; + + /* If we want to stop as fast as possible and return an error */ + unsigned quickstop : 1; + + /* failed clones to be retried again */ + const struct cache_entry **failed_clones; + int failed_clones_nr, failed_clones_alloc; +}; +#define SUBMODULE_UPDATE_CLONE_INIT { 0 } + +static void submodule_update_clone_release(struct submodule_update_clone *suc) +{ + free(suc->update_clone); + free(suc->failed_clones); +} + +struct update_data { + const char *prefix; + const char *super_prefix; + char *displaypath; + enum submodule_update_type update_default; + struct object_id suboid; + struct string_list references; + struct submodule_update_strategy update_strategy; + struct list_objects_filter_options *filter_options; + struct module_list list; + int depth; + int max_jobs; + int single_branch; + int recommend_shallow; + unsigned int require_init; + unsigned int force; + unsigned int quiet; + unsigned int nofetch; + unsigned int remote; + unsigned int progress; + unsigned int dissociate; + unsigned int init; + unsigned int warn_if_uninitialized; + unsigned int recursive; + + /* copied over from update_clone_data */ + struct object_id oid; + unsigned int just_cloned; + const char *sm_path; +}; +#define UPDATE_DATA_INIT { \ + .update_strategy = SUBMODULE_UPDATE_STRATEGY_INIT, \ + .list = MODULE_LIST_INIT, \ + .recommend_shallow = -1, \ + .references = STRING_LIST_INIT_DUP, \ + .single_branch = -1, \ + .max_jobs = 1, \ +} + +static void update_data_release(struct update_data *ud) +{ + free(ud->displaypath); + module_list_release(&ud->list); +} + +static void next_submodule_warn_missing(struct submodule_update_clone *suc, + struct strbuf *out, const char *displaypath) +{ + /* + * Only mention uninitialized submodules when their + * paths have been specified. + */ + if (suc->update_data->warn_if_uninitialized) { + strbuf_addf(out, + _("Submodule path '%s' not initialized"), + displaypath); + strbuf_addch(out, '\n'); + strbuf_addstr(out, + _("Maybe you want to use 'update --init'?")); + strbuf_addch(out, '\n'); + } +} + +/** + * Determine whether 'ce' needs to be cloned. If so, prepare the 'child' to + * run the clone. Returns 1 if 'ce' needs to be cloned, 0 otherwise. + */ +static int prepare_to_clone_next_submodule(const struct cache_entry *ce, + struct child_process *child, + struct submodule_update_clone *suc, + struct strbuf *out) +{ + const struct submodule *sub = NULL; + const char *url = NULL; + const char *update_string; + enum submodule_update_type update_type; + char *key; + const struct update_data *ud = suc->update_data; + char *displaypath = get_submodule_displaypath(ce->name, ud->prefix, + ud->super_prefix); + struct strbuf sb = STRBUF_INIT; + int needs_cloning = 0; + int need_free_url = 0; + + if (ce_stage(ce)) { + strbuf_addf(out, _("Skipping unmerged submodule %s"), displaypath); + strbuf_addch(out, '\n'); + goto cleanup; + } + + sub = submodule_from_path(the_repository, null_oid(), ce->name); + + if (!sub) { + next_submodule_warn_missing(suc, out, displaypath); + goto cleanup; + } + + key = xstrfmt("submodule.%s.update", sub->name); + if (!repo_config_get_string_tmp(the_repository, key, &update_string)) { + update_type = parse_submodule_update_type(update_string); + } else { + update_type = sub->update_strategy.type; + } + free(key); + + if (suc->update_data->update_strategy.type == SM_UPDATE_NONE + || (suc->update_data->update_strategy.type == SM_UPDATE_UNSPECIFIED + && update_type == SM_UPDATE_NONE)) { + strbuf_addf(out, _("Skipping submodule '%s'"), displaypath); + strbuf_addch(out, '\n'); + goto cleanup; + } + + /* Check if the submodule has been initialized. */ + if (!is_submodule_active(the_repository, ce->name)) { + next_submodule_warn_missing(suc, out, displaypath); + goto cleanup; + } + + strbuf_reset(&sb); + strbuf_addf(&sb, "submodule.%s.url", sub->name); + if (repo_config_get_string_tmp(the_repository, sb.buf, &url)) { + if (sub->url && (starts_with_dot_slash(sub->url) || + starts_with_dot_dot_slash(sub->url))) { + url = resolve_relative_url(sub->url, NULL, 0); + need_free_url = 1; + } else + url = sub->url; + } + + if (!url) + die(_("cannot clone submodule '%s' without a URL"), sub->name); + + strbuf_reset(&sb); + strbuf_addf(&sb, "%s/.git", ce->name); + needs_cloning = !file_exists(sb.buf); + + ALLOC_GROW(suc->update_clone, suc->update_clone_nr + 1, + suc->update_clone_alloc); + oidcpy(&suc->update_clone[suc->update_clone_nr].oid, &ce->oid); + suc->update_clone[suc->update_clone_nr].just_cloned = needs_cloning; + suc->update_clone[suc->update_clone_nr].sub = sub; + suc->update_clone_nr++; + + if (!needs_cloning) + goto cleanup; + + child->git_cmd = 1; + child->no_stdin = 1; + child->stdout_to_stderr = 1; + child->err = -1; + strvec_push(&child->args, "submodule--helper"); + strvec_push(&child->args, "clone"); + if (suc->update_data->progress) + strvec_push(&child->args, "--progress"); + if (suc->update_data->quiet) + strvec_push(&child->args, "--quiet"); + if (suc->update_data->prefix) + strvec_pushl(&child->args, "--prefix", suc->update_data->prefix, NULL); + if (suc->update_data->recommend_shallow && sub->recommend_shallow == 1) + strvec_push(&child->args, "--depth=1"); + else if (suc->update_data->depth) + strvec_pushf(&child->args, "--depth=%d", suc->update_data->depth); + if (suc->update_data->filter_options && suc->update_data->filter_options->choice) + strvec_pushf(&child->args, "--filter=%s", + expand_list_objects_filter_spec(suc->update_data->filter_options)); + if (suc->update_data->require_init) + strvec_push(&child->args, "--require-init"); + strvec_pushl(&child->args, "--path", sub->path, NULL); + strvec_pushl(&child->args, "--name", sub->name, NULL); + strvec_pushl(&child->args, "--url", url, NULL); + if (suc->update_data->references.nr) { + struct string_list_item *item; + + for_each_string_list_item(item, &suc->update_data->references) + strvec_pushl(&child->args, "--reference", item->string, NULL); + } + if (suc->update_data->dissociate) + strvec_push(&child->args, "--dissociate"); + if (suc->update_data->single_branch >= 0) + strvec_push(&child->args, suc->update_data->single_branch ? + "--single-branch" : + "--no-single-branch"); + +cleanup: + free(displaypath); + strbuf_release(&sb); + if (need_free_url) + free((void*)url); + + return needs_cloning; +} + +static int update_clone_get_next_task(struct child_process *child, + struct strbuf *err, + void *suc_cb, + void **idx_task_cb) +{ + struct submodule_update_clone *suc = suc_cb; + const struct cache_entry *ce; + int index; + + for (; suc->current < suc->update_data->list.nr; suc->current++) { + ce = suc->update_data->list.entries[suc->current]; + if (prepare_to_clone_next_submodule(ce, child, suc, err)) { + int *p = xmalloc(sizeof(*p)); + + *p = suc->current; + *idx_task_cb = p; + suc->current++; + return 1; + } + } + + /* + * The loop above tried cloning each submodule once, now try the + * stragglers again, which we can imagine as an extension of the + * entry list. + */ + index = suc->current - suc->update_data->list.nr; + if (index < suc->failed_clones_nr) { + int *p; + + ce = suc->failed_clones[index]; + if (!prepare_to_clone_next_submodule(ce, child, suc, err)) { + suc->current ++; + strbuf_addstr(err, "BUG: submodule considered for " + "cloning, doesn't need cloning " + "any more?\n"); + return 0; + } + p = xmalloc(sizeof(*p)); + *p = suc->current; + *idx_task_cb = p; + suc->current ++; + return 1; + } + + return 0; +} + +static int update_clone_start_failure(struct strbuf *err UNUSED, + void *suc_cb, + void *idx_task_cb UNUSED) +{ + struct submodule_update_clone *suc = suc_cb; + + suc->quickstop = 1; + return 1; +} + +static int update_clone_task_finished(int result, + struct strbuf *err, + void *suc_cb, + void *idx_task_cb) +{ + const struct cache_entry *ce; + struct submodule_update_clone *suc = suc_cb; + int *idxP = idx_task_cb; + int idx = *idxP; + + free(idxP); + + if (!result) + return 0; + + if (idx < suc->update_data->list.nr) { + ce = suc->update_data->list.entries[idx]; + strbuf_addf(err, _("Failed to clone '%s'. Retry scheduled"), + ce->name); + strbuf_addch(err, '\n'); + ALLOC_GROW(suc->failed_clones, + suc->failed_clones_nr + 1, + suc->failed_clones_alloc); + suc->failed_clones[suc->failed_clones_nr++] = ce; + return 0; + } else { + idx -= suc->update_data->list.nr; + ce = suc->failed_clones[idx]; + strbuf_addf(err, _("Failed to clone '%s' a second time, aborting"), + ce->name); + strbuf_addch(err, '\n'); + suc->quickstop = 1; + return 1; + } + + return 0; +} + +static int git_update_clone_config(const char *var, const char *value, + const struct config_context *ctx, + void *cb) +{ + int *max_jobs = cb; + + if (!strcmp(var, "submodule.fetchjobs")) + *max_jobs = parse_submodule_fetchjobs(var, value, ctx->kvi); + return 0; +} + +static int is_tip_reachable(const char *path, const struct object_id *oid) +{ + struct child_process cp = CHILD_PROCESS_INIT; + struct strbuf rev = STRBUF_INIT; + char *hex = oid_to_hex(oid); + + cp.git_cmd = 1; + cp.dir = path; + cp.no_stderr = 1; + strvec_pushl(&cp.args, "rev-list", "-n", "1", hex, "--not", "--all", NULL); + + prepare_submodule_repo_env(&cp.env); + + if (capture_command(&cp, &rev, GIT_MAX_HEXSZ + 1) || rev.len) + return 0; + + return 1; +} + +static int fetch_in_submodule(const char *module_path, int depth, int quiet, + const struct object_id *oid) +{ + struct child_process cp = CHILD_PROCESS_INIT; + + prepare_submodule_repo_env(&cp.env); + cp.git_cmd = 1; + cp.dir = module_path; + + strvec_push(&cp.args, "fetch"); + if (quiet) + strvec_push(&cp.args, "--quiet"); + if (depth) + strvec_pushf(&cp.args, "--depth=%d", depth); + if (oid) { + char *hex = oid_to_hex(oid); + char *remote = get_default_remote(); + + strvec_pushl(&cp.args, remote, hex, NULL); + free(remote); + } + + return run_command(&cp); +} + +static int run_update_command(const struct update_data *ud, int subforce) +{ + struct child_process cp = CHILD_PROCESS_INIT; + char *oid = oid_to_hex(&ud->oid); + int ret; + + switch (ud->update_strategy.type) { + case SM_UPDATE_CHECKOUT: + cp.git_cmd = 1; + strvec_pushl(&cp.args, "checkout", "-q", NULL); + if (subforce) + strvec_push(&cp.args, "-f"); + break; + case SM_UPDATE_REBASE: + cp.git_cmd = 1; + strvec_push(&cp.args, "rebase"); + if (ud->quiet) + strvec_push(&cp.args, "--quiet"); + break; + case SM_UPDATE_MERGE: + cp.git_cmd = 1; + strvec_push(&cp.args, "merge"); + if (ud->quiet) + strvec_push(&cp.args, "--quiet"); + break; + case SM_UPDATE_COMMAND: + cp.use_shell = 1; + strvec_push(&cp.args, ud->update_strategy.command); + break; + default: + BUG("unexpected update strategy type: %d", + ud->update_strategy.type); + } + strvec_push(&cp.args, oid); + + cp.dir = ud->sm_path; + prepare_submodule_repo_env(&cp.env); + if ((ret = run_command(&cp))) { + switch (ud->update_strategy.type) { + case SM_UPDATE_CHECKOUT: + die_message(_("Unable to checkout '%s' in submodule path '%s'"), + oid, ud->displaypath); + /* No "ret" assignment, use "git checkout"'s */ + break; + case SM_UPDATE_REBASE: + ret = die_message(_("Unable to rebase '%s' in submodule path '%s'"), + oid, ud->displaypath); + break; + case SM_UPDATE_MERGE: + ret = die_message(_("Unable to merge '%s' in submodule path '%s'"), + oid, ud->displaypath); + break; + case SM_UPDATE_COMMAND: + ret = die_message(_("Execution of '%s %s' failed in submodule path '%s'"), + ud->update_strategy.command, oid, ud->displaypath); + break; + default: + BUG("unexpected update strategy type: %d", + ud->update_strategy.type); + } + + return ret; + } + + if (ud->quiet) + return 0; + + switch (ud->update_strategy.type) { + case SM_UPDATE_CHECKOUT: + printf(_("Submodule path '%s': checked out '%s'\n"), + ud->displaypath, oid); + break; + case SM_UPDATE_REBASE: + printf(_("Submodule path '%s': rebased into '%s'\n"), + ud->displaypath, oid); + break; + case SM_UPDATE_MERGE: + printf(_("Submodule path '%s': merged in '%s'\n"), + ud->displaypath, oid); + break; + case SM_UPDATE_COMMAND: + printf(_("Submodule path '%s': '%s %s'\n"), + ud->displaypath, ud->update_strategy.command, oid); + break; + default: + BUG("unexpected update strategy type: %d", + ud->update_strategy.type); + } + + return 0; +} + +static int run_update_procedure(const struct update_data *ud) +{ + int subforce = is_null_oid(&ud->suboid) || ud->force; + + if (!ud->nofetch) { + /* + * Run fetch only if `oid` isn't present or it + * is not reachable from a ref. + */ + if (!is_tip_reachable(ud->sm_path, &ud->oid) && + fetch_in_submodule(ud->sm_path, ud->depth, ud->quiet, NULL) && + !ud->quiet) + fprintf_ln(stderr, + _("Unable to fetch in submodule path '%s'; " + "trying to directly fetch %s:"), + ud->displaypath, oid_to_hex(&ud->oid)); + /* + * Now we tried the usual fetch, but `oid` may + * not be reachable from any of the refs. + */ + if (!is_tip_reachable(ud->sm_path, &ud->oid) && + fetch_in_submodule(ud->sm_path, ud->depth, ud->quiet, &ud->oid)) + return die_message(_("Fetched in submodule path '%s', but it did not " + "contain %s. Direct fetching of that commit failed."), + ud->displaypath, oid_to_hex(&ud->oid)); + } + + return run_update_command(ud, subforce); +} + +static int remote_submodule_branch(const char *path, const char **branch) +{ + const struct submodule *sub; + char *key; + *branch = NULL; + + sub = submodule_from_path(the_repository, null_oid(), path); + if (!sub) + return die_message(_("could not initialize submodule at path '%s'"), + path); + + key = xstrfmt("submodule.%s.branch", sub->name); + if (repo_config_get_string_tmp(the_repository, key, branch)) + *branch = sub->branch; + free(key); + + if (!*branch) { + *branch = "HEAD"; + return 0; + } + + if (!strcmp(*branch, ".")) { + const char *refname = resolve_ref_unsafe("HEAD", 0, NULL, NULL); + + if (!refname) + return die_message(_("No such ref: %s"), "HEAD"); + + /* detached HEAD */ + if (!strcmp(refname, "HEAD")) + return die_message(_("Submodule (%s) branch configured to inherit " + "branch from superproject, but the superproject " + "is not on any branch"), sub->name); + + if (!skip_prefix(refname, "refs/heads/", &refname)) + return die_message(_("Expecting a full ref name, got %s"), + refname); + + *branch = refname; + return 0; + } + + /* Our "branch" is coming from repo_config_get_string_tmp() */ + return 0; +} + +static int ensure_core_worktree(const char *path) +{ + const char *cw; + struct repository subrepo; + + if (repo_submodule_init(&subrepo, the_repository, path, null_oid())) + return die_message(_("could not get a repository handle for submodule '%s'"), + path); + + if (!repo_config_get_string_tmp(&subrepo, "core.worktree", &cw)) { + char *cfg_file, *abs_path; + const char *rel_path; + struct strbuf sb = STRBUF_INIT; + + cfg_file = repo_git_path(&subrepo, "config"); + + abs_path = absolute_pathdup(path); + rel_path = relative_path(abs_path, subrepo.gitdir, &sb); + + git_config_set_in_file(cfg_file, "core.worktree", rel_path); + + free(cfg_file); + free(abs_path); + strbuf_release(&sb); + } + + repo_clear(&subrepo); + return 0; +} + +static const char *submodule_update_type_to_label(enum submodule_update_type type) +{ + switch (type) { + case SM_UPDATE_CHECKOUT: + return "checkout"; + case SM_UPDATE_MERGE: + return "merge"; + case SM_UPDATE_REBASE: + return "rebase"; + case SM_UPDATE_UNSPECIFIED: + case SM_UPDATE_NONE: + case SM_UPDATE_COMMAND: + break; + } + BUG("unreachable with type %d", type); +} + +static void update_data_to_args(const struct update_data *update_data, + struct strvec *args) +{ + enum submodule_update_type update_type = update_data->update_default; + + strvec_pushl(args, "submodule--helper", "update", "--recursive", NULL); + if (update_data->displaypath) { + strvec_push(args, "--super-prefix"); + strvec_pushf(args, "%s/", update_data->displaypath); + } + strvec_pushf(args, "--jobs=%d", update_data->max_jobs); + if (update_data->quiet) + strvec_push(args, "--quiet"); + if (update_data->force) + strvec_push(args, "--force"); + if (update_data->init) + strvec_push(args, "--init"); + if (update_data->remote) + strvec_push(args, "--remote"); + if (update_data->nofetch) + strvec_push(args, "--no-fetch"); + if (update_data->dissociate) + strvec_push(args, "--dissociate"); + if (update_data->progress) + strvec_push(args, "--progress"); + if (update_data->require_init) + strvec_push(args, "--require-init"); + if (update_data->depth) + strvec_pushf(args, "--depth=%d", update_data->depth); + if (update_type != SM_UPDATE_UNSPECIFIED) + strvec_pushf(args, "--%s", + submodule_update_type_to_label(update_type)); + + if (update_data->references.nr) { + struct string_list_item *item; + + for_each_string_list_item(item, &update_data->references) + strvec_pushl(args, "--reference", item->string, NULL); + } + if (update_data->filter_options && update_data->filter_options->choice) + strvec_pushf(args, "--filter=%s", + expand_list_objects_filter_spec( + update_data->filter_options)); + if (update_data->recommend_shallow == 0) + strvec_push(args, "--no-recommend-shallow"); + else if (update_data->recommend_shallow == 1) + strvec_push(args, "--recommend-shallow"); + if (update_data->single_branch >= 0) + strvec_push(args, update_data->single_branch ? + "--single-branch" : + "--no-single-branch"); +} + +static int update_submodule(struct update_data *update_data) +{ + int ret; + + ret = determine_submodule_update_strategy(the_repository, + update_data->just_cloned, + update_data->sm_path, + update_data->update_default, + &update_data->update_strategy); + if (ret) + return ret; + + if (update_data->just_cloned) + oidcpy(&update_data->suboid, null_oid()); + else if (resolve_gitlink_ref(update_data->sm_path, "HEAD", &update_data->suboid)) + return die_message(_("Unable to find current revision in submodule path '%s'"), + update_data->displaypath); + + if (update_data->remote) { + char *remote_name; + const char *branch; + char *remote_ref; + int code; + + code = get_default_remote_submodule(update_data->sm_path, &remote_name); + if (code) + return code; + code = remote_submodule_branch(update_data->sm_path, &branch); + if (code) + return code; + remote_ref = xstrfmt("refs/remotes/%s/%s", remote_name, branch); + + free(remote_name); + + if (!update_data->nofetch) { + if (fetch_in_submodule(update_data->sm_path, update_data->depth, + 0, NULL)) + return die_message(_("Unable to fetch in submodule path '%s'"), + update_data->sm_path); + } + + if (resolve_gitlink_ref(update_data->sm_path, remote_ref, &update_data->oid)) + return die_message(_("Unable to find %s revision in submodule path '%s'"), + remote_ref, update_data->sm_path); + + free(remote_ref); + } + + if (!oideq(&update_data->oid, &update_data->suboid) || update_data->force) { + ret = run_update_procedure(update_data); + if (ret) + return ret; + } + + if (update_data->recursive) { + struct child_process cp = CHILD_PROCESS_INIT; + struct update_data next = *update_data; + + next.prefix = NULL; + oidcpy(&next.oid, null_oid()); + oidcpy(&next.suboid, null_oid()); + + cp.dir = update_data->sm_path; + cp.git_cmd = 1; + prepare_submodule_repo_env(&cp.env); + update_data_to_args(&next, &cp.args); + + ret = run_command(&cp); + if (ret) + die_message(_("Failed to recurse into submodule path '%s'"), + update_data->displaypath); + return ret; + } + + return 0; +} + +static int update_submodules(struct update_data *update_data) +{ + int i, ret = 0; + struct submodule_update_clone suc = SUBMODULE_UPDATE_CLONE_INIT; + const struct run_process_parallel_opts opts = { + .tr2_category = "submodule", + .tr2_label = "parallel/update", + + .processes = update_data->max_jobs, + + .get_next_task = update_clone_get_next_task, + .start_failure = update_clone_start_failure, + .task_finished = update_clone_task_finished, + .data = &suc, + }; + + suc.update_data = update_data; + run_processes_parallel(&opts); + + /* + * We saved the output and put it out all at once now. + * That means: + * - the listener does not have to interleave their (checkout) + * work with our fetching. The writes involved in a + * checkout involve more straightforward sequential I/O. + * - the listener can avoid doing any work if fetching failed. + */ + if (suc.quickstop) { + ret = 1; + goto cleanup; + } + + for (i = 0; i < suc.update_clone_nr; i++) { + struct update_clone_data ucd = suc.update_clone[i]; + int code; + + oidcpy(&update_data->oid, &ucd.oid); + update_data->just_cloned = ucd.just_cloned; + update_data->sm_path = ucd.sub->path; + + code = ensure_core_worktree(update_data->sm_path); + if (code) + goto fail; + + update_data->displaypath = get_submodule_displaypath( + update_data->sm_path, update_data->prefix, + update_data->super_prefix); + code = update_submodule(update_data); + FREE_AND_NULL(update_data->displaypath); +fail: + if (!code) + continue; + ret = code; + if (ret == 128) + goto cleanup; + } + +cleanup: + submodule_update_clone_release(&suc); + string_list_clear(&update_data->references, 0); + return ret; +} + +static int module_update(int argc, const char **argv, const char *prefix) +{ + struct pathspec pathspec = { 0 }; + struct pathspec pathspec2 = { 0 }; + struct update_data opt = UPDATE_DATA_INIT; + struct list_objects_filter_options filter_options = + LIST_OBJECTS_FILTER_INIT; + int ret; + struct option module_update_options[] = { + OPT__SUPER_PREFIX(&opt.super_prefix), + OPT__FORCE(&opt.force, N_("force checkout updates"), 0), + OPT_BOOL(0, "init", &opt.init, + N_("initialize uninitialized submodules before update")), + OPT_BOOL(0, "remote", &opt.remote, + N_("use SHA-1 of submodule's remote tracking branch")), + OPT_BOOL(0, "recursive", &opt.recursive, + N_("traverse submodules recursively")), + OPT_BOOL('N', "no-fetch", &opt.nofetch, + N_("don't fetch new objects from the remote site")), + OPT_SET_INT(0, "checkout", &opt.update_default, + N_("use the 'checkout' update strategy (default)"), + SM_UPDATE_CHECKOUT), + OPT_SET_INT('m', "merge", &opt.update_default, + N_("use the 'merge' update strategy"), + SM_UPDATE_MERGE), + OPT_SET_INT('r', "rebase", &opt.update_default, + N_("use the 'rebase' update strategy"), + SM_UPDATE_REBASE), + OPT_STRING_LIST(0, "reference", &opt.references, N_("repo"), + N_("reference repository")), + OPT_BOOL(0, "dissociate", &opt.dissociate, + N_("use --reference only while cloning")), + OPT_INTEGER(0, "depth", &opt.depth, + N_("create a shallow clone truncated to the " + "specified number of revisions")), + OPT_INTEGER('j', "jobs", &opt.max_jobs, + N_("parallel jobs")), + OPT_BOOL(0, "recommend-shallow", &opt.recommend_shallow, + N_("whether the initial clone should follow the shallow recommendation")), + OPT__QUIET(&opt.quiet, N_("don't print cloning progress")), + OPT_BOOL(0, "progress", &opt.progress, + N_("force cloning progress")), + OPT_BOOL(0, "require-init", &opt.require_init, + N_("disallow cloning into non-empty directory, implies --init")), + OPT_BOOL(0, "single-branch", &opt.single_branch, + N_("clone only one branch, HEAD or --branch")), + OPT_PARSE_LIST_OBJECTS_FILTER(&filter_options), + OPT_END() + }; + const char *const git_submodule_helper_usage[] = { + N_("git submodule [--quiet] update" + " [--init [--filter=<filter-spec>]] [--remote]" + " [-N|--no-fetch] [-f|--force]" + " [--checkout|--merge|--rebase]" + " [--[no-]recommend-shallow] [--reference <repository>]" + " [--recursive] [--[no-]single-branch] [--] [<path>...]"), + NULL + }; + + update_clone_config_from_gitmodules(&opt.max_jobs); + git_config(git_update_clone_config, &opt.max_jobs); + + argc = parse_options(argc, argv, prefix, module_update_options, + git_submodule_helper_usage, 0); + + if (opt.require_init) + opt.init = 1; + + if (filter_options.choice && !opt.init) { + usage_with_options(git_submodule_helper_usage, + module_update_options); + } + + opt.filter_options = &filter_options; + opt.prefix = prefix; + + if (opt.update_default) + opt.update_strategy.type = opt.update_default; + + if (module_list_compute(argv, prefix, &pathspec, &opt.list) < 0) { + ret = 1; + goto cleanup; + } + + if (pathspec.nr) + opt.warn_if_uninitialized = 1; + + if (opt.init) { + struct module_list list = MODULE_LIST_INIT; + struct init_cb info = INIT_CB_INIT; + + if (module_list_compute(argv, opt.prefix, + &pathspec2, &list) < 0) { + module_list_release(&list); + ret = 1; + goto cleanup; + } + + /* + * If there are no path args and submodule.active is set then, + * by default, only initialize 'active' modules. + */ + if (!argc && !git_config_get("submodule.active")) + module_list_active(&list); + + info.prefix = opt.prefix; + info.super_prefix = opt.super_prefix; + if (opt.quiet) + info.flags |= OPT_QUIET; + + for_each_listed_submodule(&list, init_submodule_cb, &info); + module_list_release(&list); + } + + ret = update_submodules(&opt); +cleanup: + update_data_release(&opt); + list_objects_filter_release(&filter_options); + clear_pathspec(&pathspec); + clear_pathspec(&pathspec2); + return ret; +} + +static int push_check(int argc, const char **argv, const char *prefix UNUSED) +{ + struct remote *remote; + const char *superproject_head; + char *head; + int detached_head = 0; + struct object_id head_oid; + + if (argc < 3) + die("submodule--helper push-check requires at least 2 arguments"); + + /* + * superproject's resolved head ref. + * if HEAD then the superproject is in a detached head state, otherwise + * it will be the resolved head ref. + */ + superproject_head = argv[1]; + argv++; + argc--; + /* Get the submodule's head ref and determine if it is detached */ + head = resolve_refdup("HEAD", 0, &head_oid, NULL); + if (!head) + die(_("Failed to resolve HEAD as a valid ref.")); + if (!strcmp(head, "HEAD")) + detached_head = 1; + + /* + * The remote must be configured. + * This is to avoid pushing to the exact same URL as the parent. + */ + remote = pushremote_get(argv[1]); + if (!remote || remote->origin == REMOTE_UNCONFIGURED) + die("remote '%s' not configured", argv[1]); + + /* Check the refspec */ + if (argc > 2) { + int i; + struct ref *local_refs = get_local_heads(); + struct refspec refspec = REFSPEC_INIT_PUSH; + + refspec_appendn(&refspec, argv + 2, argc - 2); + + for (i = 0; i < refspec.nr; i++) { + const struct refspec_item *rs = &refspec.items[i]; + + if (rs->pattern || rs->matching) + continue; + + /* LHS must match a single ref */ + switch (count_refspec_match(rs->src, local_refs, NULL)) { + case 1: + break; + case 0: + /* + * If LHS matches 'HEAD' then we need to ensure + * that it matches the same named branch + * checked out in the superproject. + */ + if (!strcmp(rs->src, "HEAD")) { + if (!detached_head && + !strcmp(head, superproject_head)) + break; + die("HEAD does not match the named branch in the superproject"); + } + /* fallthrough */ + default: + die("src refspec '%s' must name a ref", + rs->src); + } + } + refspec_clear(&refspec); + } + free(head); + + return 0; +} + +static int absorb_git_dirs(int argc, const char **argv, const char *prefix) +{ + int i; + struct pathspec pathspec = { 0 }; + struct module_list list = MODULE_LIST_INIT; + const char *super_prefix = NULL; + struct option embed_gitdir_options[] = { + OPT__SUPER_PREFIX(&super_prefix), + OPT_END() + }; + const char *const git_submodule_helper_usage[] = { + N_("git submodule absorbgitdirs [<options>] [<path>...]"), + NULL + }; + int ret = 1; + + argc = parse_options(argc, argv, prefix, embed_gitdir_options, + git_submodule_helper_usage, 0); + + if (module_list_compute(argv, prefix, &pathspec, &list) < 0) + goto cleanup; + + for (i = 0; i < list.nr; i++) + absorb_git_dir_into_superproject(list.entries[i]->name, + super_prefix); + + ret = 0; +cleanup: + clear_pathspec(&pathspec); + module_list_release(&list); + return ret; +} + +static int module_set_url(int argc, const char **argv, const char *prefix) +{ + int quiet = 0, ret; + const char *newurl; + const char *path; + char *config_name; + struct option options[] = { + OPT__QUIET(&quiet, N_("suppress output for setting url of a submodule")), + OPT_END() + }; + const char *const usage[] = { + N_("git submodule set-url [--quiet] <path> <newurl>"), + NULL + }; + const struct submodule *sub; + + argc = parse_options(argc, argv, prefix, options, usage, 0); + + if (argc != 2 || !(path = argv[0]) || !(newurl = argv[1])) + usage_with_options(usage, options); + + sub = submodule_from_path(the_repository, null_oid(), path); + + if (!sub) + die(_("no submodule mapping found in .gitmodules for path '%s'"), + path); + + config_name = xstrfmt("submodule.%s.url", sub->name); + ret = config_set_in_gitmodules_file_gently(config_name, newurl); + + if (!ret) { + repo_read_gitmodules(the_repository, 0); + sync_submodule(sub->path, prefix, NULL, quiet ? OPT_QUIET : 0); + } + + free(config_name); + return !!ret; +} + +static int module_set_branch(int argc, const char **argv, const char *prefix) +{ + int opt_default = 0, ret; + const char *opt_branch = NULL; + const char *path; + char *config_name; + struct option options[] = { + /* + * We accept the `quiet` option for uniformity across subcommands, + * though there is nothing to make less verbose in this subcommand. + */ + OPT_NOOP_NOARG('q', "quiet"), + + OPT_BOOL('d', "default", &opt_default, + N_("set the default tracking branch to master")), + OPT_STRING('b', "branch", &opt_branch, N_("branch"), + N_("set the default tracking branch")), + OPT_END() + }; + const char *const usage[] = { + N_("git submodule set-branch [-q|--quiet] (-d|--default) <path>"), + N_("git submodule set-branch [-q|--quiet] (-b|--branch) <branch> <path>"), + NULL + }; + const struct submodule *sub; + + argc = parse_options(argc, argv, prefix, options, usage, 0); + + if (!opt_branch && !opt_default) + die(_("--branch or --default required")); + + if (opt_branch && opt_default) + die(_("options '%s' and '%s' cannot be used together"), "--branch", "--default"); + + if (argc != 1 || !(path = argv[0])) + usage_with_options(usage, options); + + sub = submodule_from_path(the_repository, null_oid(), path); + + if (!sub) + die(_("no submodule mapping found in .gitmodules for path '%s'"), + path); + + config_name = xstrfmt("submodule.%s.branch", sub->name); + ret = config_set_in_gitmodules_file_gently(config_name, opt_branch); + + free(config_name); + return !!ret; +} + +static int module_create_branch(int argc, const char **argv, const char *prefix) +{ + enum branch_track track; + int quiet = 0, force = 0, reflog = 0, dry_run = 0; + struct option options[] = { + OPT__QUIET(&quiet, N_("print only error messages")), + OPT__FORCE(&force, N_("force creation"), 0), + OPT_BOOL(0, "create-reflog", &reflog, + N_("create the branch's reflog")), + OPT_CALLBACK_F('t', "track", &track, "(direct|inherit)", + N_("set branch tracking configuration"), + PARSE_OPT_OPTARG, + parse_opt_tracking_mode), + OPT__DRY_RUN(&dry_run, + N_("show whether the branch would be created")), + OPT_END() + }; + const char *const usage[] = { + N_("git submodule--helper create-branch [-f|--force] [--create-reflog] [-q|--quiet] [-t|--track] [-n|--dry-run] <name> <start-oid> <start-name>"), + NULL + }; + + git_config(git_default_config, NULL); + track = git_branch_track; + argc = parse_options(argc, argv, prefix, options, usage, 0); + + if (argc != 3) + usage_with_options(usage, options); + + if (!quiet && !dry_run) + printf_ln(_("creating branch '%s'"), argv[0]); + + create_branches_recursively(the_repository, argv[0], argv[1], argv[2], + force, reflog, quiet, track, dry_run); + return 0; +} + +struct add_data { + const char *prefix; + const char *branch; + const char *reference_path; + char *sm_path; + const char *sm_name; + const char *repo; + const char *realrepo; + int depth; + unsigned int force: 1; + unsigned int quiet: 1; + unsigned int progress: 1; + unsigned int dissociate: 1; +}; +#define ADD_DATA_INIT { .depth = -1 } + +static void append_fetch_remotes(struct strbuf *msg, const char *git_dir_path) +{ + struct child_process cp_remote = CHILD_PROCESS_INIT; + struct strbuf sb_remote_out = STRBUF_INIT; + + cp_remote.git_cmd = 1; + strvec_pushf(&cp_remote.env, + "GIT_DIR=%s", git_dir_path); + strvec_push(&cp_remote.env, "GIT_WORK_TREE=."); + strvec_pushl(&cp_remote.args, "remote", "-v", NULL); + if (!capture_command(&cp_remote, &sb_remote_out, 0)) { + char *next_line; + char *line = sb_remote_out.buf; + + while ((next_line = strchr(line, '\n')) != NULL) { + size_t len = next_line - line; + + if (strip_suffix_mem(line, &len, " (fetch)")) + strbuf_addf(msg, " %.*s\n", (int)len, line); + line = next_line + 1; + } + } + + strbuf_release(&sb_remote_out); +} + +static int add_submodule(const struct add_data *add_data) +{ + char *submod_gitdir_path; + struct module_clone_data clone_data = MODULE_CLONE_DATA_INIT; + struct string_list reference = STRING_LIST_INIT_NODUP; + int ret = -1; + + /* perhaps the path already exists and is already a git repo, else clone it */ + if (is_directory(add_data->sm_path)) { + struct strbuf sm_path = STRBUF_INIT; + strbuf_addstr(&sm_path, add_data->sm_path); + submod_gitdir_path = xstrfmt("%s/.git", add_data->sm_path); + if (is_nonbare_repository_dir(&sm_path)) + printf(_("Adding existing repo at '%s' to the index\n"), + add_data->sm_path); + else + die(_("'%s' already exists and is not a valid git repo"), + add_data->sm_path); + strbuf_release(&sm_path); + free(submod_gitdir_path); + } else { + struct child_process cp = CHILD_PROCESS_INIT; + + submod_gitdir_path = xstrfmt(".git/modules/%s", add_data->sm_name); + + if (is_directory(submod_gitdir_path)) { + if (!add_data->force) { + struct strbuf msg = STRBUF_INIT; + char *die_msg; + + strbuf_addf(&msg, _("A git directory for '%s' is found " + "locally with remote(s):\n"), + add_data->sm_name); + + append_fetch_remotes(&msg, submod_gitdir_path); + free(submod_gitdir_path); + + strbuf_addf(&msg, _("If you want to reuse this local git " + "directory instead of cloning again from\n" + " %s\n" + "use the '--force' option. If the local git " + "directory is not the correct repo\n" + "or you are unsure what this means choose " + "another name with the '--name' option."), + add_data->realrepo); + + die_msg = strbuf_detach(&msg, NULL); + die("%s", die_msg); + } else { + printf(_("Reactivating local git directory for " + "submodule '%s'\n"), add_data->sm_name); + } + } + free(submod_gitdir_path); + + clone_data.prefix = add_data->prefix; + clone_data.path = add_data->sm_path; + clone_data.name = add_data->sm_name; + clone_data.url = add_data->realrepo; + clone_data.quiet = add_data->quiet; + clone_data.progress = add_data->progress; + if (add_data->reference_path) { + char *p = xstrdup(add_data->reference_path); + + string_list_append(&reference, p)->util = p; + } + clone_data.dissociate = add_data->dissociate; + if (add_data->depth >= 0) + clone_data.depth = xstrfmt("%d", add_data->depth); + + if (clone_submodule(&clone_data, &reference)) + goto cleanup; + + prepare_submodule_repo_env(&cp.env); + cp.git_cmd = 1; + cp.dir = add_data->sm_path; + /* + * NOTE: we only get here if add_data->force is true, so + * passing --force to checkout is reasonable. + */ + strvec_pushl(&cp.args, "checkout", "-f", "-q", NULL); + + if (add_data->branch) { + strvec_pushl(&cp.args, "-B", add_data->branch, NULL); + strvec_pushf(&cp.args, "origin/%s", add_data->branch); + } + + if (run_command(&cp)) + die(_("unable to checkout submodule '%s'"), add_data->sm_path); + } + ret = 0; +cleanup: + string_list_clear(&reference, 1); + return ret; +} + +static int config_submodule_in_gitmodules(const char *name, const char *var, const char *value) +{ + char *key; + int ret; + + if (!is_writing_gitmodules_ok()) + die(_("please make sure that the .gitmodules file is in the working tree")); + + key = xstrfmt("submodule.%s.%s", name, var); + ret = config_set_in_gitmodules_file_gently(key, value); + free(key); + + return ret; +} + +static void configure_added_submodule(struct add_data *add_data) +{ + char *key; + struct child_process add_submod = CHILD_PROCESS_INIT; + struct child_process add_gitmodules = CHILD_PROCESS_INIT; + + key = xstrfmt("submodule.%s.url", add_data->sm_name); + git_config_set_gently(key, add_data->realrepo); + free(key); + + add_submod.git_cmd = 1; + strvec_pushl(&add_submod.args, "add", + "--no-warn-embedded-repo", NULL); + if (add_data->force) + strvec_push(&add_submod.args, "--force"); + strvec_pushl(&add_submod.args, "--", add_data->sm_path, NULL); + + if (run_command(&add_submod)) + die(_("Failed to add submodule '%s'"), add_data->sm_path); + + if (config_submodule_in_gitmodules(add_data->sm_name, "path", add_data->sm_path) || + config_submodule_in_gitmodules(add_data->sm_name, "url", add_data->repo)) + die(_("Failed to register submodule '%s'"), add_data->sm_path); + + if (add_data->branch) { + if (config_submodule_in_gitmodules(add_data->sm_name, + "branch", add_data->branch)) + die(_("Failed to register submodule '%s'"), add_data->sm_path); + } + + add_gitmodules.git_cmd = 1; + strvec_pushl(&add_gitmodules.args, + "add", "--force", "--", ".gitmodules", NULL); + + if (run_command(&add_gitmodules)) + die(_("Failed to register submodule '%s'"), add_data->sm_path); + + /* + * NEEDSWORK: In a multi-working-tree world this needs to be + * set in the per-worktree config. + */ + /* + * NEEDSWORK: In the longer run, we need to get rid of this + * pattern of querying "submodule.active" before calling + * is_submodule_active(), since that function needs to find + * out the value of "submodule.active" again anyway. + */ + if (!git_config_get("submodule.active")) { + /* + * If the submodule being added isn't already covered by the + * current configured pathspec, set the submodule's active flag + */ + if (!is_submodule_active(the_repository, add_data->sm_path)) { + key = xstrfmt("submodule.%s.active", add_data->sm_name); + git_config_set_gently(key, "true"); + free(key); + } + } else { + key = xstrfmt("submodule.%s.active", add_data->sm_name); + git_config_set_gently(key, "true"); + free(key); + } +} + +static void die_on_index_match(const char *path, int force) +{ + struct pathspec ps; + const char *args[] = { path, NULL }; + parse_pathspec(&ps, 0, PATHSPEC_PREFER_CWD, NULL, args); + + if (repo_read_index_preload(the_repository, NULL, 0) < 0) + die(_("index file corrupt")); + + if (ps.nr) { + int i; + char *ps_matched = xcalloc(ps.nr, 1); + + /* TODO: audit for interaction with sparse-index. */ + ensure_full_index(&the_index); + + /* + * Since there is only one pathspec, we just need to + * check ps_matched[0] to know if a cache entry matched. + */ + for (i = 0; i < the_index.cache_nr; i++) { + ce_path_match(&the_index, the_index.cache[i], &ps, + ps_matched); + + if (ps_matched[0]) { + if (!force) + die(_("'%s' already exists in the index"), + path); + if (!S_ISGITLINK(the_index.cache[i]->ce_mode)) + die(_("'%s' already exists in the index " + "and is not a submodule"), path); + break; + } + } + free(ps_matched); + } + clear_pathspec(&ps); +} + +static void die_on_repo_without_commits(const char *path) +{ + struct strbuf sb = STRBUF_INIT; + strbuf_addstr(&sb, path); + if (is_nonbare_repository_dir(&sb)) { + struct object_id oid; + if (resolve_gitlink_ref(path, "HEAD", &oid) < 0) + die(_("'%s' does not have a commit checked out"), path); + } + strbuf_release(&sb); +} + +static int module_add(int argc, const char **argv, const char *prefix) +{ + int force = 0, quiet = 0, progress = 0, dissociate = 0; + struct add_data add_data = ADD_DATA_INIT; + char *to_free = NULL; + struct option options[] = { + OPT_STRING('b', "branch", &add_data.branch, N_("branch"), + N_("branch of repository to add as submodule")), + OPT__FORCE(&force, N_("allow adding an otherwise ignored submodule path"), + PARSE_OPT_NOCOMPLETE), + OPT__QUIET(&quiet, N_("print only error messages")), + OPT_BOOL(0, "progress", &progress, N_("force cloning progress")), + OPT_STRING(0, "reference", &add_data.reference_path, N_("repository"), + N_("reference repository")), + OPT_BOOL(0, "dissociate", &dissociate, N_("borrow the objects from reference repositories")), + OPT_STRING(0, "name", &add_data.sm_name, N_("name"), + N_("sets the submodule's name to the given string " + "instead of defaulting to its path")), + OPT_INTEGER(0, "depth", &add_data.depth, N_("depth for shallow clones")), + OPT_END() + }; + const char *const usage[] = { + N_("git submodule add [<options>] [--] <repository> [<path>]"), + NULL + }; + struct strbuf sb = STRBUF_INIT; + int ret = 1; + + argc = parse_options(argc, argv, prefix, options, usage, 0); + + if (!is_writing_gitmodules_ok()) + die(_("please make sure that the .gitmodules file is in the working tree")); + + if (prefix && *prefix && + add_data.reference_path && !is_absolute_path(add_data.reference_path)) + add_data.reference_path = xstrfmt("%s%s", prefix, add_data.reference_path); + + if (argc == 0 || argc > 2) + usage_with_options(usage, options); + + add_data.repo = argv[0]; + if (argc == 1) + add_data.sm_path = git_url_basename(add_data.repo, 0, 0); + else + add_data.sm_path = xstrdup(argv[1]); + + if (prefix && *prefix && !is_absolute_path(add_data.sm_path)) { + char *sm_path = add_data.sm_path; + + add_data.sm_path = xstrfmt("%s%s", prefix, sm_path); + free(sm_path); + } + + if (starts_with_dot_dot_slash(add_data.repo) || + starts_with_dot_slash(add_data.repo)) { + if (prefix) + die(_("Relative path can only be used from the toplevel " + "of the working tree")); + + /* dereference source url relative to parent's url */ + to_free = resolve_relative_url(add_data.repo, NULL, 1); + add_data.realrepo = to_free; + } else if (is_dir_sep(add_data.repo[0]) || strchr(add_data.repo, ':')) { + add_data.realrepo = add_data.repo; + } else { + die(_("repo URL: '%s' must be absolute or begin with ./|../"), + add_data.repo); + } + + /* + * normalize path: + * multiple //; leading ./; /./; /../; + */ + normalize_path_copy(add_data.sm_path, add_data.sm_path); + strip_dir_trailing_slashes(add_data.sm_path); + + die_on_index_match(add_data.sm_path, force); + die_on_repo_without_commits(add_data.sm_path); + + if (!force) { + struct child_process cp = CHILD_PROCESS_INIT; + + cp.git_cmd = 1; + cp.no_stdout = 1; + strvec_pushl(&cp.args, "add", "--dry-run", "--ignore-missing", + "--no-warn-embedded-repo", add_data.sm_path, NULL); + if ((ret = pipe_command(&cp, NULL, 0, NULL, 0, &sb, 0))) { + strbuf_complete_line(&sb); + fputs(sb.buf, stderr); + goto cleanup; + } + } + + if(!add_data.sm_name) + add_data.sm_name = add_data.sm_path; + + if (check_submodule_name(add_data.sm_name)) + die(_("'%s' is not a valid submodule name"), add_data.sm_name); + + add_data.prefix = prefix; + add_data.force = !!force; + add_data.quiet = !!quiet; + add_data.progress = !!progress; + add_data.dissociate = !!dissociate; + + if (add_submodule(&add_data)) + goto cleanup; + configure_added_submodule(&add_data); + + ret = 0; +cleanup: + free(add_data.sm_path); + free(to_free); + strbuf_release(&sb); + + return ret; +} + +int cmd_submodule__helper(int argc, const char **argv, const char *prefix) +{ + parse_opt_subcommand_fn *fn = NULL; + const char *const usage[] = { + N_("git submodule--helper <command>"), + NULL + }; + struct option options[] = { + OPT_SUBCOMMAND("clone", &fn, module_clone), + OPT_SUBCOMMAND("add", &fn, module_add), + OPT_SUBCOMMAND("update", &fn, module_update), + OPT_SUBCOMMAND("foreach", &fn, module_foreach), + OPT_SUBCOMMAND("init", &fn, module_init), + OPT_SUBCOMMAND("status", &fn, module_status), + OPT_SUBCOMMAND("sync", &fn, module_sync), + OPT_SUBCOMMAND("deinit", &fn, module_deinit), + OPT_SUBCOMMAND("summary", &fn, module_summary), + OPT_SUBCOMMAND("push-check", &fn, push_check), + OPT_SUBCOMMAND("absorbgitdirs", &fn, absorb_git_dirs), + OPT_SUBCOMMAND("set-url", &fn, module_set_url), + OPT_SUBCOMMAND("set-branch", &fn, module_set_branch), + OPT_SUBCOMMAND("create-branch", &fn, module_create_branch), + OPT_END() + }; + argc = parse_options(argc, argv, prefix, options, usage, 0); + + return fn(argc, argv, prefix); +} diff --git a/builtin/symbolic-ref.c b/builtin/symbolic-ref.c new file mode 100644 index 0000000..c9defe4 --- /dev/null +++ b/builtin/symbolic-ref.c @@ -0,0 +1,88 @@ +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "refs.h" +#include "parse-options.h" +#include "strbuf.h" + +static const char * const git_symbolic_ref_usage[] = { + N_("git symbolic-ref [-m <reason>] <name> <ref>"), + N_("git symbolic-ref [-q] [--short] [--no-recurse] <name>"), + N_("git symbolic-ref --delete [-q] <name>"), + NULL +}; + +static int check_symref(const char *HEAD, int quiet, int shorten, int recurse, int print) +{ + int resolve_flags, flag; + const char *refname; + + resolve_flags = (recurse ? 0 : RESOLVE_REF_NO_RECURSE); + refname = resolve_ref_unsafe(HEAD, resolve_flags, NULL, &flag); + + if (!refname) + die("No such ref: %s", HEAD); + else if (!(flag & REF_ISSYMREF)) { + if (!quiet) + die("ref %s is not a symbolic ref", HEAD); + else + return 1; + } + if (print) { + char *to_free = NULL; + if (shorten) + refname = to_free = shorten_unambiguous_ref(refname, 0); + puts(refname); + free(to_free); + } + return 0; +} + +int cmd_symbolic_ref(int argc, const char **argv, const char *prefix) +{ + int quiet = 0, delete = 0, shorten = 0, recurse = 1, ret = 0; + const char *msg = NULL; + struct option options[] = { + OPT__QUIET(&quiet, + N_("suppress error message for non-symbolic (detached) refs")), + OPT_BOOL('d', "delete", &delete, N_("delete symbolic ref")), + OPT_BOOL(0, "short", &shorten, N_("shorten ref output")), + OPT_BOOL(0, "recurse", &recurse, N_("recursively dereference (default)")), + OPT_STRING('m', NULL, &msg, N_("reason"), N_("reason of the update")), + OPT_END(), + }; + + git_config(git_default_config, NULL); + argc = parse_options(argc, argv, prefix, options, + git_symbolic_ref_usage, 0); + if (msg && !*msg) + die("Refusing to perform update with empty message"); + + if (delete) { + if (argc != 1) + usage_with_options(git_symbolic_ref_usage, options); + ret = check_symref(argv[0], 1, 0, 0, 0); + if (ret) + die("Cannot delete %s, not a symbolic ref", argv[0]); + if (!strcmp(argv[0], "HEAD")) + die("deleting '%s' is not allowed", argv[0]); + return delete_ref(NULL, argv[0], NULL, REF_NO_DEREF); + } + + switch (argc) { + case 1: + ret = check_symref(argv[0], quiet, shorten, recurse, 1); + break; + case 2: + if (!strcmp(argv[0], "HEAD") && + !starts_with(argv[1], "refs/")) + die("Refusing to point HEAD outside of refs/"); + if (check_refname_format(argv[1], REFNAME_ALLOW_ONELEVEL) < 0) + die("Refusing to set '%s' to invalid ref '%s'", argv[0], argv[1]); + ret = !!create_symref(argv[0], argv[1], msg); + break; + default: + usage_with_options(git_symbolic_ref_usage, options); + } + return ret; +} diff --git a/builtin/tag.c b/builtin/tag.c new file mode 100644 index 0000000..3918eac --- /dev/null +++ b/builtin/tag.c @@ -0,0 +1,670 @@ +/* + * Builtin "git tag" + * + * Copyright (c) 2007 Kristian Høgsberg <krh@redhat.com>, + * Carlos Rica <jasampler@gmail.com> + * Based on git-tag.sh and mktag.c by Linus Torvalds. + */ + +#include "builtin.h" +#include "advice.h" +#include "config.h" +#include "editor.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "refs.h" +#include "object-name.h" +#include "object-store-ll.h" +#include "path.h" +#include "tag.h" +#include "run-command.h" +#include "parse-options.h" +#include "diff.h" +#include "revision.h" +#include "gpg-interface.h" +#include "oid-array.h" +#include "column.h" +#include "ref-filter.h" +#include "date.h" +#include "write-or-die.h" + +static const char * const git_tag_usage[] = { + N_("git tag [-a | -s | -u <key-id>] [-f] [-m <msg> | -F <file>] [-e]\n" + " <tagname> [<commit> | <object>]"), + N_("git tag -d <tagname>..."), + N_("git tag [-n[<num>]] -l [--contains <commit>] [--no-contains <commit>]\n" + " [--points-at <object>] [--column[=<options>] | --no-column]\n" + " [--create-reflog] [--sort=<key>] [--format=<format>]\n" + " [--merged <commit>] [--no-merged <commit>] [<pattern>...]"), + N_("git tag -v [--format=<format>] <tagname>..."), + NULL +}; + +static unsigned int colopts; +static int force_sign_annotate; +static int config_sign_tag = -1; /* unspecified */ +static int omit_empty = 0; + +static int list_tags(struct ref_filter *filter, struct ref_sorting *sorting, + struct ref_format *format) +{ + struct ref_array array; + struct strbuf output = STRBUF_INIT; + struct strbuf err = STRBUF_INIT; + char *to_free = NULL; + int i; + + memset(&array, 0, sizeof(array)); + + if (filter->lines == -1) + filter->lines = 0; + + if (!format->format) { + if (filter->lines) { + to_free = xstrfmt("%s %%(contents:lines=%d)", + "%(align:15)%(refname:lstrip=2)%(end)", + filter->lines); + format->format = to_free; + } else + format->format = "%(refname:lstrip=2)"; + } + + if (verify_ref_format(format)) + die(_("unable to parse format string")); + filter->with_commit_tag_algo = 1; + filter_refs(&array, filter, FILTER_REFS_TAGS); + filter_ahead_behind(the_repository, format, &array); + ref_array_sort(sorting, &array); + + for (i = 0; i < array.nr; i++) { + strbuf_reset(&output); + strbuf_reset(&err); + if (format_ref_array_item(array.items[i], format, &output, &err)) + die("%s", err.buf); + fwrite(output.buf, 1, output.len, stdout); + if (output.len || !omit_empty) + putchar('\n'); + } + + strbuf_release(&err); + strbuf_release(&output); + ref_array_clear(&array); + free(to_free); + + return 0; +} + +typedef int (*each_tag_name_fn)(const char *name, const char *ref, + const struct object_id *oid, void *cb_data); + +static int for_each_tag_name(const char **argv, each_tag_name_fn fn, + void *cb_data) +{ + const char **p; + struct strbuf ref = STRBUF_INIT; + int had_error = 0; + struct object_id oid; + + for (p = argv; *p; p++) { + strbuf_reset(&ref); + strbuf_addf(&ref, "refs/tags/%s", *p); + if (read_ref(ref.buf, &oid)) { + error(_("tag '%s' not found."), *p); + had_error = 1; + continue; + } + if (fn(*p, ref.buf, &oid, cb_data)) + had_error = 1; + } + strbuf_release(&ref); + return had_error; +} + +static int collect_tags(const char *name UNUSED, const char *ref, + const struct object_id *oid, void *cb_data) +{ + struct string_list *ref_list = cb_data; + + string_list_append(ref_list, ref); + ref_list->items[ref_list->nr - 1].util = oiddup(oid); + return 0; +} + +static int delete_tags(const char **argv) +{ + int result; + struct string_list refs_to_delete = STRING_LIST_INIT_DUP; + struct string_list_item *item; + + result = for_each_tag_name(argv, collect_tags, (void *)&refs_to_delete); + if (delete_refs(NULL, &refs_to_delete, REF_NO_DEREF)) + result = 1; + + for_each_string_list_item(item, &refs_to_delete) { + const char *name = item->string; + struct object_id *oid = item->util; + if (!ref_exists(name)) + printf(_("Deleted tag '%s' (was %s)\n"), + item->string + 10, + repo_find_unique_abbrev(the_repository, oid, DEFAULT_ABBREV)); + + free(oid); + } + string_list_clear(&refs_to_delete, 0); + return result; +} + +static int verify_tag(const char *name, const char *ref UNUSED, + const struct object_id *oid, void *cb_data) +{ + int flags; + struct ref_format *format = cb_data; + flags = GPG_VERIFY_VERBOSE; + + if (format->format) + flags = GPG_VERIFY_OMIT_STATUS; + + if (gpg_verify_tag(oid, name, flags)) + return -1; + + if (format->format) + pretty_print_ref(name, oid, format); + + return 0; +} + +static int do_sign(struct strbuf *buffer) +{ + return sign_buffer(buffer, buffer, get_signing_key()); +} + +static const char tag_template[] = + N_("\nWrite a message for tag:\n %s\n" + "Lines starting with '%c' will be ignored.\n"); + +static const char tag_template_nocleanup[] = + N_("\nWrite a message for tag:\n %s\n" + "Lines starting with '%c' will be kept; you may remove them" + " yourself if you want to.\n"); + +static int git_tag_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + if (!strcmp(var, "tag.gpgsign")) { + config_sign_tag = git_config_bool(var, value); + return 0; + } + + if (!strcmp(var, "tag.sort")) { + if (!value) + return config_error_nonbool(var); + string_list_append(cb, value); + return 0; + } + + if (!strcmp(var, "tag.forcesignannotated")) { + force_sign_annotate = git_config_bool(var, value); + return 0; + } + + if (starts_with(var, "column.")) + return git_column_config(var, value, "tag", &colopts); + + if (git_color_config(var, value, cb) < 0) + return -1; + + return git_default_config(var, value, ctx, cb); +} + +static void write_tag_body(int fd, const struct object_id *oid) +{ + unsigned long size; + enum object_type type; + char *buf, *sp, *orig; + struct strbuf payload = STRBUF_INIT; + struct strbuf signature = STRBUF_INIT; + + orig = buf = repo_read_object_file(the_repository, oid, &type, &size); + if (!buf) + return; + if (parse_signature(buf, size, &payload, &signature)) { + buf = payload.buf; + size = payload.len; + } + /* skip header */ + sp = strstr(buf, "\n\n"); + + if (!sp || !size || type != OBJ_TAG) { + free(buf); + return; + } + sp += 2; /* skip the 2 LFs */ + write_or_die(fd, sp, buf + size - sp); + + free(orig); + strbuf_release(&payload); + strbuf_release(&signature); +} + +static int build_tag_object(struct strbuf *buf, int sign, struct object_id *result) +{ + if (sign && do_sign(buf) < 0) + return error(_("unable to sign the tag")); + if (write_object_file(buf->buf, buf->len, OBJ_TAG, result) < 0) + return error(_("unable to write tag file")); + return 0; +} + +struct create_tag_options { + unsigned int message_given:1; + unsigned int use_editor:1; + unsigned int sign; + enum { + CLEANUP_NONE, + CLEANUP_SPACE, + CLEANUP_ALL + } cleanup_mode; +}; + +static const char message_advice_nested_tag[] = + N_("You have created a nested tag. The object referred to by your new tag is\n" + "already a tag. If you meant to tag the object that it points to, use:\n" + "\n" + "\tgit tag -f %s %s^{}"); + +static void create_tag(const struct object_id *object, const char *object_ref, + const char *tag, + struct strbuf *buf, struct create_tag_options *opt, + struct object_id *prev, struct object_id *result, char *path) +{ + enum object_type type; + struct strbuf header = STRBUF_INIT; + + type = oid_object_info(the_repository, object, NULL); + if (type <= OBJ_NONE) + die(_("bad object type.")); + + if (type == OBJ_TAG) + advise_if_enabled(ADVICE_NESTED_TAG, _(message_advice_nested_tag), + tag, object_ref); + + strbuf_addf(&header, + "object %s\n" + "type %s\n" + "tag %s\n" + "tagger %s\n\n", + oid_to_hex(object), + type_name(type), + tag, + git_committer_info(IDENT_STRICT)); + + if (!opt->message_given || opt->use_editor) { + int fd; + + /* write the template message before editing: */ + fd = xopen(path, O_CREAT | O_TRUNC | O_WRONLY, 0600); + + if (opt->message_given) { + write_or_die(fd, buf->buf, buf->len); + strbuf_reset(buf); + } else if (!is_null_oid(prev)) { + write_tag_body(fd, prev); + } else { + struct strbuf buf = STRBUF_INIT; + strbuf_addch(&buf, '\n'); + if (opt->cleanup_mode == CLEANUP_ALL) + strbuf_commented_addf(&buf, comment_line_char, + _(tag_template), tag, comment_line_char); + else + strbuf_commented_addf(&buf, comment_line_char, + _(tag_template_nocleanup), tag, comment_line_char); + write_or_die(fd, buf.buf, buf.len); + strbuf_release(&buf); + } + close(fd); + + if (launch_editor(path, buf, NULL)) { + fprintf(stderr, + _("Please supply the message using either -m or -F option.\n")); + exit(1); + } + } + + if (opt->cleanup_mode != CLEANUP_NONE) + strbuf_stripspace(buf, + opt->cleanup_mode == CLEANUP_ALL ? comment_line_char : '\0'); + + if (!opt->message_given && !buf->len) + die(_("no tag message?")); + + strbuf_insert(buf, 0, header.buf, header.len); + strbuf_release(&header); + + if (build_tag_object(buf, opt->sign, result) < 0) { + if (path) + fprintf(stderr, _("The tag message has been left in %s\n"), + path); + exit(128); + } +} + +static void create_reflog_msg(const struct object_id *oid, struct strbuf *sb) +{ + enum object_type type; + struct commit *c; + char *buf; + unsigned long size; + int subject_len = 0; + const char *subject_start; + + char *rla = getenv("GIT_REFLOG_ACTION"); + if (rla) { + strbuf_addstr(sb, rla); + } else { + strbuf_addstr(sb, "tag: tagging "); + strbuf_add_unique_abbrev(sb, oid, DEFAULT_ABBREV); + } + + strbuf_addstr(sb, " ("); + type = oid_object_info(the_repository, oid, NULL); + switch (type) { + default: + strbuf_addstr(sb, "object of unknown type"); + break; + case OBJ_COMMIT: + if ((buf = repo_read_object_file(the_repository, oid, &type, &size))) { + subject_len = find_commit_subject(buf, &subject_start); + strbuf_insert(sb, sb->len, subject_start, subject_len); + } else { + strbuf_addstr(sb, "commit object"); + } + free(buf); + + if ((c = lookup_commit_reference(the_repository, oid))) + strbuf_addf(sb, ", %s", show_date(c->date, 0, DATE_MODE(SHORT))); + break; + case OBJ_TREE: + strbuf_addstr(sb, "tree object"); + break; + case OBJ_BLOB: + strbuf_addstr(sb, "blob object"); + break; + case OBJ_TAG: + strbuf_addstr(sb, "other tag object"); + break; + } + strbuf_addch(sb, ')'); +} + +struct msg_arg { + int given; + struct strbuf buf; +}; + +static int parse_msg_arg(const struct option *opt, const char *arg, int unset) +{ + struct msg_arg *msg = opt->value; + + BUG_ON_OPT_NEG(unset); + + if (!arg) + return -1; + if (msg->buf.len) + strbuf_addstr(&(msg->buf), "\n\n"); + strbuf_addstr(&(msg->buf), arg); + msg->given = 1; + return 0; +} + +static int strbuf_check_tag_ref(struct strbuf *sb, const char *name) +{ + if (name[0] == '-') + return -1; + + strbuf_reset(sb); + strbuf_addf(sb, "refs/tags/%s", name); + + return check_refname_format(sb->buf, 0); +} + +int cmd_tag(int argc, const char **argv, const char *prefix) +{ + struct strbuf buf = STRBUF_INIT; + struct strbuf ref = STRBUF_INIT; + struct strbuf reflog_msg = STRBUF_INIT; + struct object_id object, prev; + const char *object_ref, *tag; + struct create_tag_options opt; + char *cleanup_arg = NULL; + int create_reflog = 0; + int annotate = 0, force = 0; + int cmdmode = 0, create_tag_object = 0; + char *msgfile = NULL; + const char *keyid = NULL; + struct msg_arg msg = { .buf = STRBUF_INIT }; + struct ref_transaction *transaction; + struct strbuf err = STRBUF_INIT; + struct ref_filter filter = REF_FILTER_INIT; + struct ref_sorting *sorting; + struct string_list sorting_options = STRING_LIST_INIT_DUP; + struct ref_format format = REF_FORMAT_INIT; + int icase = 0; + int edit_flag = 0; + struct option options[] = { + OPT_CMDMODE('l', "list", &cmdmode, N_("list tag names"), 'l'), + { OPTION_INTEGER, 'n', NULL, &filter.lines, N_("n"), + N_("print <n> lines of each tag message"), + PARSE_OPT_OPTARG, NULL, 1 }, + OPT_CMDMODE('d', "delete", &cmdmode, N_("delete tags"), 'd'), + OPT_CMDMODE('v', "verify", &cmdmode, N_("verify tags"), 'v'), + + OPT_GROUP(N_("Tag creation options")), + OPT_BOOL('a', "annotate", &annotate, + N_("annotated tag, needs a message")), + OPT_CALLBACK_F('m', "message", &msg, N_("message"), + N_("tag message"), PARSE_OPT_NONEG, parse_msg_arg), + OPT_FILENAME('F', "file", &msgfile, N_("read message from file")), + OPT_BOOL('e', "edit", &edit_flag, N_("force edit of tag message")), + OPT_BOOL('s', "sign", &opt.sign, N_("annotated and GPG-signed tag")), + OPT_CLEANUP(&cleanup_arg), + OPT_STRING('u', "local-user", &keyid, N_("key-id"), + N_("use another key to sign the tag")), + OPT__FORCE(&force, N_("replace the tag if exists"), 0), + OPT_BOOL(0, "create-reflog", &create_reflog, N_("create a reflog")), + + OPT_GROUP(N_("Tag listing options")), + OPT_COLUMN(0, "column", &colopts, N_("show tag list in columns")), + OPT_CONTAINS(&filter.with_commit, N_("print only tags that contain the commit")), + OPT_NO_CONTAINS(&filter.no_commit, N_("print only tags that don't contain the commit")), + OPT_WITH(&filter.with_commit, N_("print only tags that contain the commit")), + OPT_WITHOUT(&filter.no_commit, N_("print only tags that don't contain the commit")), + OPT_MERGED(&filter, N_("print only tags that are merged")), + OPT_NO_MERGED(&filter, N_("print only tags that are not merged")), + OPT_BOOL(0, "omit-empty", &omit_empty, + N_("do not output a newline after empty formatted refs")), + OPT_REF_SORT(&sorting_options), + { + OPTION_CALLBACK, 0, "points-at", &filter.points_at, N_("object"), + N_("print only tags of the object"), PARSE_OPT_LASTARG_DEFAULT, + parse_opt_object_name, (intptr_t) "HEAD" + }, + OPT_STRING( 0 , "format", &format.format, N_("format"), + N_("format to use for the output")), + OPT__COLOR(&format.use_color, N_("respect format colors")), + OPT_BOOL('i', "ignore-case", &icase, N_("sorting and filtering are case insensitive")), + OPT_END() + }; + int ret = 0; + const char *only_in_list = NULL; + char *path = NULL; + + setup_ref_filter_porcelain_msg(); + + git_config(git_tag_config, &sorting_options); + + memset(&opt, 0, sizeof(opt)); + filter.lines = -1; + opt.sign = -1; + + argc = parse_options(argc, argv, prefix, options, git_tag_usage, 0); + + if (!cmdmode) { + if (argc == 0) + cmdmode = 'l'; + else if (filter.with_commit || filter.no_commit || + filter.reachable_from || filter.unreachable_from || + filter.points_at.nr || filter.lines != -1) + cmdmode = 'l'; + } + + if (cmdmode == 'l') + setup_auto_pager("tag", 1); + + if (opt.sign == -1) + opt.sign = cmdmode ? 0 : config_sign_tag > 0; + + if (keyid) { + opt.sign = 1; + set_signing_key(keyid); + } + create_tag_object = (opt.sign || annotate || msg.given || msgfile); + + if ((create_tag_object || force) && (cmdmode != 0)) + usage_with_options(git_tag_usage, options); + + finalize_colopts(&colopts, -1); + if (cmdmode == 'l' && filter.lines != -1) { + if (explicitly_enable_column(colopts)) + die(_("options '%s' and '%s' cannot be used together"), "--column", "-n"); + colopts = 0; + } + sorting = ref_sorting_options(&sorting_options); + ref_sorting_set_sort_flags_all(sorting, REF_SORTING_ICASE, icase); + filter.ignore_case = icase; + if (cmdmode == 'l') { + if (column_active(colopts)) { + struct column_options copts; + memset(&copts, 0, sizeof(copts)); + copts.padding = 2; + run_column_filter(colopts, &copts); + } + filter.name_patterns = argv; + ret = list_tags(&filter, sorting, &format); + if (column_active(colopts)) + stop_column_filter(); + goto cleanup; + } + if (filter.lines != -1) + only_in_list = "-n"; + else if (filter.with_commit) + only_in_list = "--contains"; + else if (filter.no_commit) + only_in_list = "--no-contains"; + else if (filter.points_at.nr) + only_in_list = "--points-at"; + else if (filter.reachable_from) + only_in_list = "--merged"; + else if (filter.unreachable_from) + only_in_list = "--no-merged"; + if (only_in_list) + die(_("the '%s' option is only allowed in list mode"), only_in_list); + if (cmdmode == 'd') { + ret = delete_tags(argv); + goto cleanup; + } + if (cmdmode == 'v') { + if (format.format && verify_ref_format(&format)) + usage_with_options(git_tag_usage, options); + ret = for_each_tag_name(argv, verify_tag, &format); + goto cleanup; + } + + if (msg.given || msgfile) { + if (msg.given && msgfile) + die(_("options '%s' and '%s' cannot be used together"), "-F", "-m"); + if (msg.given) + strbuf_addbuf(&buf, &(msg.buf)); + else { + if (!strcmp(msgfile, "-")) { + if (strbuf_read(&buf, 0, 1024) < 0) + die_errno(_("cannot read '%s'"), msgfile); + } else { + if (strbuf_read_file(&buf, msgfile, 1024) < 0) + die_errno(_("could not open or read '%s'"), + msgfile); + } + } + } + + tag = argv[0]; + + object_ref = argc == 2 ? argv[1] : "HEAD"; + if (argc > 2) + die(_("too many arguments")); + + if (repo_get_oid(the_repository, object_ref, &object)) + die(_("Failed to resolve '%s' as a valid ref."), object_ref); + + if (strbuf_check_tag_ref(&ref, tag)) + die(_("'%s' is not a valid tag name."), tag); + + if (read_ref(ref.buf, &prev)) + oidclr(&prev); + else if (!force) + die(_("tag '%s' already exists"), tag); + + opt.message_given = msg.given || msgfile; + opt.use_editor = edit_flag; + + if (!cleanup_arg || !strcmp(cleanup_arg, "strip")) + opt.cleanup_mode = CLEANUP_ALL; + else if (!strcmp(cleanup_arg, "verbatim")) + opt.cleanup_mode = CLEANUP_NONE; + else if (!strcmp(cleanup_arg, "whitespace")) + opt.cleanup_mode = CLEANUP_SPACE; + else + die(_("Invalid cleanup mode %s"), cleanup_arg); + + create_reflog_msg(&object, &reflog_msg); + + if (create_tag_object) { + if (force_sign_annotate && !annotate) + opt.sign = 1; + path = git_pathdup("TAG_EDITMSG"); + create_tag(&object, object_ref, tag, &buf, &opt, &prev, &object, + path); + } + + transaction = ref_transaction_begin(&err); + if (!transaction || + ref_transaction_update(transaction, ref.buf, &object, &prev, + create_reflog ? REF_FORCE_CREATE_REFLOG : 0, + reflog_msg.buf, &err) || + ref_transaction_commit(transaction, &err)) { + if (path) + fprintf(stderr, + _("The tag message has been left in %s\n"), + path); + die("%s", err.buf); + } + if (path) { + unlink_or_warn(path); + free(path); + } + ref_transaction_free(transaction); + if (force && !is_null_oid(&prev) && !oideq(&prev, &object)) + printf(_("Updated tag '%s' (was %s)\n"), tag, + repo_find_unique_abbrev(the_repository, &prev, DEFAULT_ABBREV)); + +cleanup: + ref_sorting_release(sorting); + ref_filter_clear(&filter); + strbuf_release(&buf); + strbuf_release(&ref); + strbuf_release(&reflog_msg); + strbuf_release(&msg.buf); + strbuf_release(&err); + free(msgfile); + return ret; +} diff --git a/builtin/unpack-file.c b/builtin/unpack-file.c new file mode 100644 index 0000000..c129e2b --- /dev/null +++ b/builtin/unpack-file.c @@ -0,0 +1,41 @@ +#include "builtin.h" +#include "config.h" +#include "hex.h" +#include "object-name.h" +#include "object-store-ll.h" + +static char *create_temp_file(struct object_id *oid) +{ + static char path[50]; + void *buf; + enum object_type type; + unsigned long size; + int fd; + + buf = repo_read_object_file(the_repository, oid, &type, &size); + if (!buf || type != OBJ_BLOB) + die("unable to read blob object %s", oid_to_hex(oid)); + + xsnprintf(path, sizeof(path), ".merge_file_XXXXXX"); + fd = xmkstemp(path); + if (write_in_full(fd, buf, size) < 0) + die_errno("unable to write temp-file"); + close(fd); + free(buf); + return path; +} + +int cmd_unpack_file(int argc, const char **argv, const char *prefix UNUSED) +{ + struct object_id oid; + + if (argc != 2 || !strcmp(argv[1], "-h")) + usage("git unpack-file <blob>"); + if (repo_get_oid(the_repository, argv[1], &oid)) + die("Not a valid object name %s", argv[1]); + + git_config(git_default_config, NULL); + + puts(create_temp_file(&oid)); + return 0; +} diff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c new file mode 100644 index 0000000..fef7423 --- /dev/null +++ b/builtin/unpack-objects.c @@ -0,0 +1,696 @@ +#include "builtin.h" +#include "bulk-checkin.h" +#include "config.h" +#include "environment.h" +#include "gettext.h" +#include "git-zlib.h" +#include "hex.h" +#include "object-store-ll.h" +#include "object.h" +#include "delta.h" +#include "pack.h" +#include "blob.h" +#include "commit.h" +#include "replace-object.h" +#include "strbuf.h" +#include "tag.h" +#include "tree.h" +#include "tree-walk.h" +#include "progress.h" +#include "decorate.h" +#include "fsck.h" + +static int dry_run, quiet, recover, has_errors, strict; +static const char unpack_usage[] = "git unpack-objects [-n] [-q] [-r] [--strict]"; + +/* We always read in 4kB chunks. */ +static unsigned char buffer[4096]; +static unsigned int offset, len; +static off_t consumed_bytes; +static off_t max_input_size; +static git_hash_ctx ctx; +static struct fsck_options fsck_options = FSCK_OPTIONS_STRICT; +static struct progress *progress; + +/* + * When running under --strict mode, objects whose reachability are + * suspect are kept in core without getting written in the object + * store. + */ +struct obj_buffer { + char *buffer; + unsigned long size; +}; + +static struct decoration obj_decorate; + +static struct obj_buffer *lookup_object_buffer(struct object *base) +{ + return lookup_decoration(&obj_decorate, base); +} + +static void add_object_buffer(struct object *object, char *buffer, unsigned long size) +{ + struct obj_buffer *obj; + CALLOC_ARRAY(obj, 1); + obj->buffer = buffer; + obj->size = size; + if (add_decoration(&obj_decorate, object, obj)) + die("object %s tried to add buffer twice!", oid_to_hex(&object->oid)); +} + +/* + * Make sure at least "min" bytes are available in the buffer, and + * return the pointer to the buffer. + */ +static void *fill(int min) +{ + if (min <= len) + return buffer + offset; + if (min > sizeof(buffer)) + die("cannot fill %d bytes", min); + if (offset) { + the_hash_algo->update_fn(&ctx, buffer, offset); + memmove(buffer, buffer + offset, len); + offset = 0; + } + do { + ssize_t ret = xread(0, buffer + len, sizeof(buffer) - len); + if (ret <= 0) { + if (!ret) + die("early EOF"); + die_errno("read error on input"); + } + len += ret; + } while (len < min); + return buffer; +} + +static void use(int bytes) +{ + if (bytes > len) + die("used more bytes than were available"); + len -= bytes; + offset += bytes; + + /* make sure off_t is sufficiently large not to wrap */ + if (signed_add_overflows(consumed_bytes, bytes)) + die("pack too large for current definition of off_t"); + consumed_bytes += bytes; + if (max_input_size && consumed_bytes > max_input_size) + die(_("pack exceeds maximum allowed size")); + display_throughput(progress, consumed_bytes); +} + +/* + * Decompress zstream from the standard input into a newly + * allocated buffer of specified size and return the buffer. + * The caller is responsible to free the returned buffer. + * + * But for dry_run mode, "get_data()" is only used to check the + * integrity of data, and the returned buffer is not used at all. + * Therefore, in dry_run mode, "get_data()" will release the small + * allocated buffer which is reused to hold temporary zstream output + * and return NULL instead of returning garbage data. + */ +static void *get_data(unsigned long size) +{ + git_zstream stream; + unsigned long bufsize = dry_run && size > 8192 ? 8192 : size; + void *buf = xmallocz(bufsize); + + memset(&stream, 0, sizeof(stream)); + + stream.next_out = buf; + stream.avail_out = bufsize; + stream.next_in = fill(1); + stream.avail_in = len; + git_inflate_init(&stream); + + for (;;) { + int ret = git_inflate(&stream, 0); + use(len - stream.avail_in); + if (stream.total_out == size && ret == Z_STREAM_END) + break; + if (ret != Z_OK) { + error("inflate returned %d", ret); + FREE_AND_NULL(buf); + if (!recover) + exit(1); + has_errors = 1; + break; + } + stream.next_in = fill(1); + stream.avail_in = len; + if (dry_run) { + /* reuse the buffer in dry_run mode */ + stream.next_out = buf; + stream.avail_out = bufsize > size - stream.total_out ? + size - stream.total_out : + bufsize; + } + } + git_inflate_end(&stream); + if (dry_run) + FREE_AND_NULL(buf); + return buf; +} + +struct delta_info { + struct object_id base_oid; + unsigned nr; + off_t base_offset; + unsigned long size; + void *delta; + struct delta_info *next; +}; + +static struct delta_info *delta_list; + +static void add_delta_to_list(unsigned nr, const struct object_id *base_oid, + off_t base_offset, + void *delta, unsigned long size) +{ + struct delta_info *info = xmalloc(sizeof(*info)); + + oidcpy(&info->base_oid, base_oid); + info->base_offset = base_offset; + info->size = size; + info->delta = delta; + info->nr = nr; + info->next = delta_list; + delta_list = info; +} + +struct obj_info { + off_t offset; + struct object_id oid; + struct object *obj; +}; + +/* Remember to update object flag allocation in object.h */ +#define FLAG_OPEN (1u<<20) +#define FLAG_WRITTEN (1u<<21) + +static struct obj_info *obj_list; +static unsigned nr_objects; + +/* + * Called only from check_object() after it verified this object + * is Ok. + */ +static void write_cached_object(struct object *obj, struct obj_buffer *obj_buf) +{ + struct object_id oid; + + if (write_object_file(obj_buf->buffer, obj_buf->size, + obj->type, &oid) < 0) + die("failed to write object %s", oid_to_hex(&obj->oid)); + obj->flags |= FLAG_WRITTEN; +} + +/* + * At the very end of the processing, write_rest() scans the objects + * that have reachability requirements and calls this function. + * Verify its reachability and validity recursively and write it out. + */ +static int check_object(struct object *obj, enum object_type type, + void *data UNUSED, + struct fsck_options *options UNUSED) +{ + struct obj_buffer *obj_buf; + + if (!obj) + return 1; + + if (obj->flags & FLAG_WRITTEN) + return 0; + + if (type != OBJ_ANY && obj->type != type) + die("object type mismatch"); + + if (!(obj->flags & FLAG_OPEN)) { + unsigned long size; + int type = oid_object_info(the_repository, &obj->oid, &size); + if (type != obj->type || type <= 0) + die("object of unexpected type"); + obj->flags |= FLAG_WRITTEN; + return 0; + } + + obj_buf = lookup_object_buffer(obj); + if (!obj_buf) + die("Whoops! Cannot find object '%s'", oid_to_hex(&obj->oid)); + if (fsck_object(obj, obj_buf->buffer, obj_buf->size, &fsck_options)) + die("fsck error in packed object"); + fsck_options.walk = check_object; + if (fsck_walk(obj, NULL, &fsck_options)) + die("Error on reachable objects of %s", oid_to_hex(&obj->oid)); + write_cached_object(obj, obj_buf); + return 0; +} + +static void write_rest(void) +{ + unsigned i; + for (i = 0; i < nr_objects; i++) { + if (obj_list[i].obj) + check_object(obj_list[i].obj, OBJ_ANY, NULL, NULL); + } +} + +static void added_object(unsigned nr, enum object_type type, + void *data, unsigned long size); + +/* + * Write out nr-th object from the list, now we know the contents + * of it. Under --strict, this buffers structured objects in-core, + * to be checked at the end. + */ +static void write_object(unsigned nr, enum object_type type, + void *buf, unsigned long size) +{ + if (!strict) { + if (write_object_file(buf, size, type, + &obj_list[nr].oid) < 0) + die("failed to write object"); + added_object(nr, type, buf, size); + free(buf); + obj_list[nr].obj = NULL; + } else if (type == OBJ_BLOB) { + struct blob *blob; + if (write_object_file(buf, size, type, + &obj_list[nr].oid) < 0) + die("failed to write object"); + added_object(nr, type, buf, size); + free(buf); + + blob = lookup_blob(the_repository, &obj_list[nr].oid); + if (blob) + blob->object.flags |= FLAG_WRITTEN; + else + die("invalid blob object"); + obj_list[nr].obj = NULL; + } else { + struct object *obj; + int eaten; + hash_object_file(the_hash_algo, buf, size, type, + &obj_list[nr].oid); + added_object(nr, type, buf, size); + obj = parse_object_buffer(the_repository, &obj_list[nr].oid, + type, size, buf, + &eaten); + if (!obj) + die("invalid %s", type_name(type)); + add_object_buffer(obj, buf, size); + obj->flags |= FLAG_OPEN; + obj_list[nr].obj = obj; + } +} + +static void resolve_delta(unsigned nr, enum object_type type, + void *base, unsigned long base_size, + void *delta, unsigned long delta_size) +{ + void *result; + unsigned long result_size; + + result = patch_delta(base, base_size, + delta, delta_size, + &result_size); + if (!result) + die("failed to apply delta"); + free(delta); + write_object(nr, type, result, result_size); +} + +/* + * We now know the contents of an object (which is nr-th in the pack); + * resolve all the deltified objects that are based on it. + */ +static void added_object(unsigned nr, enum object_type type, + void *data, unsigned long size) +{ + struct delta_info **p = &delta_list; + struct delta_info *info; + + while ((info = *p) != NULL) { + if (oideq(&info->base_oid, &obj_list[nr].oid) || + info->base_offset == obj_list[nr].offset) { + *p = info->next; + p = &delta_list; + resolve_delta(info->nr, type, data, size, + info->delta, info->size); + free(info); + continue; + } + p = &info->next; + } +} + +static void unpack_non_delta_entry(enum object_type type, unsigned long size, + unsigned nr) +{ + void *buf = get_data(size); + + if (buf) + write_object(nr, type, buf, size); +} + +struct input_zstream_data { + git_zstream *zstream; + unsigned char buf[8192]; + int status; +}; + +static const void *feed_input_zstream(struct input_stream *in_stream, + unsigned long *readlen) +{ + struct input_zstream_data *data = in_stream->data; + git_zstream *zstream = data->zstream; + void *in = fill(1); + + if (in_stream->is_finished) { + *readlen = 0; + return NULL; + } + + zstream->next_out = data->buf; + zstream->avail_out = sizeof(data->buf); + zstream->next_in = in; + zstream->avail_in = len; + + data->status = git_inflate(zstream, 0); + + in_stream->is_finished = data->status != Z_OK; + use(len - zstream->avail_in); + *readlen = sizeof(data->buf) - zstream->avail_out; + + return data->buf; +} + +static void stream_blob(unsigned long size, unsigned nr) +{ + git_zstream zstream = { 0 }; + struct input_zstream_data data = { 0 }; + struct input_stream in_stream = { + .read = feed_input_zstream, + .data = &data, + }; + struct obj_info *info = &obj_list[nr]; + + data.zstream = &zstream; + git_inflate_init(&zstream); + + if (stream_loose_object(&in_stream, size, &info->oid)) + die(_("failed to write object in stream")); + + if (data.status != Z_STREAM_END) + die(_("inflate returned (%d)"), data.status); + git_inflate_end(&zstream); + + if (strict) { + struct blob *blob = lookup_blob(the_repository, &info->oid); + + if (!blob) + die(_("invalid blob object from stream")); + blob->object.flags |= FLAG_WRITTEN; + } + info->obj = NULL; +} + +static int resolve_against_held(unsigned nr, const struct object_id *base, + void *delta_data, unsigned long delta_size) +{ + struct object *obj; + struct obj_buffer *obj_buffer; + obj = lookup_object(the_repository, base); + if (!obj) + return 0; + obj_buffer = lookup_object_buffer(obj); + if (!obj_buffer) + return 0; + resolve_delta(nr, obj->type, obj_buffer->buffer, + obj_buffer->size, delta_data, delta_size); + return 1; +} + +static void unpack_delta_entry(enum object_type type, unsigned long delta_size, + unsigned nr) +{ + void *delta_data, *base; + unsigned long base_size; + struct object_id base_oid; + + if (type == OBJ_REF_DELTA) { + oidread(&base_oid, fill(the_hash_algo->rawsz)); + use(the_hash_algo->rawsz); + delta_data = get_data(delta_size); + if (!delta_data) + return; + if (repo_has_object_file(the_repository, &base_oid)) + ; /* Ok we have this one */ + else if (resolve_against_held(nr, &base_oid, + delta_data, delta_size)) + return; /* we are done */ + else { + /* cannot resolve yet --- queue it */ + oidclr(&obj_list[nr].oid); + add_delta_to_list(nr, &base_oid, 0, delta_data, delta_size); + return; + } + } else { + unsigned base_found = 0; + unsigned char *pack, c; + off_t base_offset; + unsigned lo, mid, hi; + + pack = fill(1); + c = *pack; + use(1); + base_offset = c & 127; + while (c & 128) { + base_offset += 1; + if (!base_offset || MSB(base_offset, 7)) + die("offset value overflow for delta base object"); + pack = fill(1); + c = *pack; + use(1); + base_offset = (base_offset << 7) + (c & 127); + } + base_offset = obj_list[nr].offset - base_offset; + if (base_offset <= 0 || base_offset >= obj_list[nr].offset) + die("offset value out of bound for delta base object"); + + delta_data = get_data(delta_size); + if (!delta_data) + return; + lo = 0; + hi = nr; + while (lo < hi) { + mid = lo + (hi - lo) / 2; + if (base_offset < obj_list[mid].offset) { + hi = mid; + } else if (base_offset > obj_list[mid].offset) { + lo = mid + 1; + } else { + oidcpy(&base_oid, &obj_list[mid].oid); + base_found = !is_null_oid(&base_oid); + break; + } + } + if (!base_found) { + /* + * The delta base object is itself a delta that + * has not been resolved yet. + */ + oidclr(&obj_list[nr].oid); + add_delta_to_list(nr, null_oid(), base_offset, + delta_data, delta_size); + return; + } + } + + if (resolve_against_held(nr, &base_oid, delta_data, delta_size)) + return; + + base = repo_read_object_file(the_repository, &base_oid, &type, + &base_size); + if (!base) { + error("failed to read delta-pack base object %s", + oid_to_hex(&base_oid)); + if (!recover) + exit(1); + has_errors = 1; + return; + } + resolve_delta(nr, type, base, base_size, delta_data, delta_size); + free(base); +} + +static void unpack_one(unsigned nr) +{ + unsigned shift; + unsigned char *pack; + unsigned long size, c; + enum object_type type; + + obj_list[nr].offset = consumed_bytes; + + pack = fill(1); + c = *pack; + use(1); + type = (c >> 4) & 7; + size = (c & 15); + shift = 4; + while (c & 0x80) { + pack = fill(1); + c = *pack; + use(1); + size += (c & 0x7f) << shift; + shift += 7; + } + + switch (type) { + case OBJ_BLOB: + if (!dry_run && size > big_file_threshold) { + stream_blob(size, nr); + return; + } + /* fallthrough */ + case OBJ_COMMIT: + case OBJ_TREE: + case OBJ_TAG: + unpack_non_delta_entry(type, size, nr); + return; + case OBJ_REF_DELTA: + case OBJ_OFS_DELTA: + unpack_delta_entry(type, size, nr); + return; + default: + error("bad object type %d", type); + has_errors = 1; + if (recover) + return; + exit(1); + } +} + +static void unpack_all(void) +{ + int i; + struct pack_header *hdr = fill(sizeof(struct pack_header)); + + nr_objects = ntohl(hdr->hdr_entries); + + if (ntohl(hdr->hdr_signature) != PACK_SIGNATURE) + die("bad pack file"); + if (!pack_version_ok(hdr->hdr_version)) + die("unknown pack file version %"PRIu32, + ntohl(hdr->hdr_version)); + use(sizeof(struct pack_header)); + + if (!quiet) + progress = start_progress(_("Unpacking objects"), nr_objects); + CALLOC_ARRAY(obj_list, nr_objects); + begin_odb_transaction(); + for (i = 0; i < nr_objects; i++) { + unpack_one(i); + display_progress(progress, i + 1); + } + end_odb_transaction(); + stop_progress(&progress); + + if (delta_list) + die("unresolved deltas left after unpacking"); +} + +int cmd_unpack_objects(int argc, const char **argv, const char *prefix UNUSED) +{ + int i; + struct object_id oid; + git_hash_ctx tmp_ctx; + + disable_replace_refs(); + + git_config(git_default_config, NULL); + + quiet = !isatty(2); + + for (i = 1 ; i < argc; i++) { + const char *arg = argv[i]; + + if (*arg == '-') { + if (!strcmp(arg, "-n")) { + dry_run = 1; + continue; + } + if (!strcmp(arg, "-q")) { + quiet = 1; + continue; + } + if (!strcmp(arg, "-r")) { + recover = 1; + continue; + } + if (!strcmp(arg, "--strict")) { + strict = 1; + continue; + } + if (skip_prefix(arg, "--strict=", &arg)) { + strict = 1; + fsck_set_msg_types(&fsck_options, arg); + continue; + } + if (starts_with(arg, "--pack_header=")) { + struct pack_header *hdr; + char *c; + + hdr = (struct pack_header *)buffer; + hdr->hdr_signature = htonl(PACK_SIGNATURE); + hdr->hdr_version = htonl(strtoul(arg + 14, &c, 10)); + if (*c != ',') + die("bad %s", arg); + hdr->hdr_entries = htonl(strtoul(c + 1, &c, 10)); + if (*c) + die("bad %s", arg); + len = sizeof(*hdr); + continue; + } + if (skip_prefix(arg, "--max-input-size=", &arg)) { + max_input_size = strtoumax(arg, NULL, 10); + continue; + } + usage(unpack_usage); + } + + /* We don't take any non-flag arguments now.. Maybe some day */ + usage(unpack_usage); + } + the_hash_algo->init_fn(&ctx); + unpack_all(); + the_hash_algo->update_fn(&ctx, buffer, offset); + the_hash_algo->init_fn(&tmp_ctx); + the_hash_algo->clone_fn(&tmp_ctx, &ctx); + the_hash_algo->final_oid_fn(&oid, &tmp_ctx); + if (strict) { + write_rest(); + if (fsck_finish(&fsck_options)) + die(_("fsck error in pack objects")); + } + if (!hasheq(fill(the_hash_algo->rawsz), oid.hash)) + die("final sha1 did not match"); + use(the_hash_algo->rawsz); + + /* Write the last part of the buffer to stdout */ + while (len) { + int ret = xwrite(1, buffer + offset, len); + if (ret <= 0) + break; + len -= ret; + offset += ret; + } + + /* All done */ + return has_errors; +} diff --git a/builtin/update-index.c b/builtin/update-index.c new file mode 100644 index 0000000..7bcaa14 --- /dev/null +++ b/builtin/update-index.c @@ -0,0 +1,1250 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "bulk-checkin.h" +#include "config.h" +#include "environment.h" +#include "gettext.h" +#include "hash.h" +#include "hex.h" +#include "lockfile.h" +#include "quote.h" +#include "cache-tree.h" +#include "tree-walk.h" +#include "object-file.h" +#include "refs.h" +#include "resolve-undo.h" +#include "parse-options.h" +#include "pathspec.h" +#include "dir.h" +#include "read-cache.h" +#include "repository.h" +#include "setup.h" +#include "sparse-index.h" +#include "split-index.h" +#include "symlinks.h" +#include "fsmonitor.h" +#include "write-or-die.h" + +/* + * Default to not allowing changes to the list of files. The + * tool doesn't actually care, but this makes it harder to add + * files to the revision control by mistake by doing something + * like "git update-index *" and suddenly having all the object + * files be revision controlled. + */ +static int allow_add; +static int allow_remove; +static int allow_replace; +static int info_only; +static int force_remove; +static int verbose; +static int mark_valid_only; +static int mark_skip_worktree_only; +static int mark_fsmonitor_only; +static int ignore_skip_worktree_entries; +#define MARK_FLAG 1 +#define UNMARK_FLAG 2 +static struct strbuf mtime_dir = STRBUF_INIT; + +/* Untracked cache mode */ +enum uc_mode { + UC_UNSPECIFIED = -1, + UC_DISABLE = 0, + UC_ENABLE, + UC_TEST, + UC_FORCE +}; + +__attribute__((format (printf, 1, 2))) +static void report(const char *fmt, ...) +{ + va_list vp; + + if (!verbose) + return; + + /* + * It is possible, though unlikely, that a caller could use the verbose + * output to synchronize with addition of objects to the object + * database. The current implementation of ODB transactions leaves + * objects invisible while a transaction is active, so flush the + * transaction here before reporting a change made by update-index. + */ + flush_odb_transaction(); + va_start(vp, fmt); + vprintf(fmt, vp); + putchar('\n'); + va_end(vp); +} + +static void remove_test_directory(void) +{ + if (mtime_dir.len) + remove_dir_recursively(&mtime_dir, 0); +} + +static const char *get_mtime_path(const char *path) +{ + static struct strbuf sb = STRBUF_INIT; + strbuf_reset(&sb); + strbuf_addf(&sb, "%s/%s", mtime_dir.buf, path); + return sb.buf; +} + +static void xmkdir(const char *path) +{ + path = get_mtime_path(path); + if (mkdir(path, 0700)) + die_errno(_("failed to create directory %s"), path); +} + +static int xstat_mtime_dir(struct stat *st) +{ + if (stat(mtime_dir.buf, st)) + die_errno(_("failed to stat %s"), mtime_dir.buf); + return 0; +} + +static int create_file(const char *path) +{ + int fd; + path = get_mtime_path(path); + fd = xopen(path, O_CREAT | O_RDWR, 0644); + return fd; +} + +static void xunlink(const char *path) +{ + path = get_mtime_path(path); + if (unlink(path)) + die_errno(_("failed to delete file %s"), path); +} + +static void xrmdir(const char *path) +{ + path = get_mtime_path(path); + if (rmdir(path)) + die_errno(_("failed to delete directory %s"), path); +} + +static void avoid_racy(void) +{ + /* + * not use if we could usleep(10) if USE_NSEC is defined. The + * field nsec could be there, but the OS could choose to + * ignore it? + */ + sleep(1); +} + +static int test_if_untracked_cache_is_supported(void) +{ + struct stat st; + struct stat_data base; + int fd, ret = 0; + char *cwd; + + strbuf_addstr(&mtime_dir, "mtime-test-XXXXXX"); + if (!mkdtemp(mtime_dir.buf)) + die_errno("Could not make temporary directory"); + + cwd = xgetcwd(); + fprintf(stderr, _("Testing mtime in '%s' "), cwd); + free(cwd); + + atexit(remove_test_directory); + xstat_mtime_dir(&st); + fill_stat_data(&base, &st); + fputc('.', stderr); + + avoid_racy(); + fd = create_file("newfile"); + xstat_mtime_dir(&st); + if (!match_stat_data(&base, &st)) { + close(fd); + fputc('\n', stderr); + fprintf_ln(stderr,_("directory stat info does not " + "change after adding a new file")); + goto done; + } + fill_stat_data(&base, &st); + fputc('.', stderr); + + avoid_racy(); + xmkdir("new-dir"); + xstat_mtime_dir(&st); + if (!match_stat_data(&base, &st)) { + close(fd); + fputc('\n', stderr); + fprintf_ln(stderr, _("directory stat info does not change " + "after adding a new directory")); + goto done; + } + fill_stat_data(&base, &st); + fputc('.', stderr); + + avoid_racy(); + write_or_die(fd, "data", 4); + close(fd); + xstat_mtime_dir(&st); + if (match_stat_data(&base, &st)) { + fputc('\n', stderr); + fprintf_ln(stderr, _("directory stat info changes " + "after updating a file")); + goto done; + } + fputc('.', stderr); + + avoid_racy(); + close(create_file("new-dir/new")); + xstat_mtime_dir(&st); + if (match_stat_data(&base, &st)) { + fputc('\n', stderr); + fprintf_ln(stderr, _("directory stat info changes after " + "adding a file inside subdirectory")); + goto done; + } + fputc('.', stderr); + + avoid_racy(); + xunlink("newfile"); + xstat_mtime_dir(&st); + if (!match_stat_data(&base, &st)) { + fputc('\n', stderr); + fprintf_ln(stderr, _("directory stat info does not " + "change after deleting a file")); + goto done; + } + fill_stat_data(&base, &st); + fputc('.', stderr); + + avoid_racy(); + xunlink("new-dir/new"); + xrmdir("new-dir"); + xstat_mtime_dir(&st); + if (!match_stat_data(&base, &st)) { + fputc('\n', stderr); + fprintf_ln(stderr, _("directory stat info does not " + "change after deleting a directory")); + goto done; + } + + if (rmdir(mtime_dir.buf)) + die_errno(_("failed to delete directory %s"), mtime_dir.buf); + fprintf_ln(stderr, _(" OK")); + ret = 1; + +done: + strbuf_release(&mtime_dir); + return ret; +} + +static int mark_ce_flags(const char *path, int flag, int mark) +{ + int namelen = strlen(path); + int pos = index_name_pos(&the_index, path, namelen); + if (0 <= pos) { + mark_fsmonitor_invalid(&the_index, the_index.cache[pos]); + if (mark) + the_index.cache[pos]->ce_flags |= flag; + else + the_index.cache[pos]->ce_flags &= ~flag; + the_index.cache[pos]->ce_flags |= CE_UPDATE_IN_BASE; + cache_tree_invalidate_path(&the_index, path); + the_index.cache_changed |= CE_ENTRY_CHANGED; + return 0; + } + return -1; +} + +static int remove_one_path(const char *path) +{ + if (!allow_remove) + return error("%s: does not exist and --remove not passed", path); + if (remove_file_from_index(&the_index, path)) + return error("%s: cannot remove from the index", path); + return 0; +} + +/* + * Handle a path that couldn't be lstat'ed. It's either: + * - missing file (ENOENT or ENOTDIR). That's ok if we're + * supposed to be removing it and the removal actually + * succeeds. + * - permission error. That's never ok. + */ +static int process_lstat_error(const char *path, int err) +{ + if (is_missing_file_error(err)) + return remove_one_path(path); + return error("lstat(\"%s\"): %s", path, strerror(err)); +} + +static int add_one_path(const struct cache_entry *old, const char *path, int len, struct stat *st) +{ + int option; + struct cache_entry *ce; + + /* Was the old index entry already up-to-date? */ + if (old && !ce_stage(old) && !ie_match_stat(&the_index, old, st, 0)) + return 0; + + ce = make_empty_cache_entry(&the_index, len); + memcpy(ce->name, path, len); + ce->ce_flags = create_ce_flags(0); + ce->ce_namelen = len; + fill_stat_cache_info(&the_index, ce, st); + ce->ce_mode = ce_mode_from_stat(old, st->st_mode); + + if (index_path(&the_index, &ce->oid, path, st, + info_only ? 0 : HASH_WRITE_OBJECT)) { + discard_cache_entry(ce); + return -1; + } + option = allow_add ? ADD_CACHE_OK_TO_ADD : 0; + option |= allow_replace ? ADD_CACHE_OK_TO_REPLACE : 0; + if (add_index_entry(&the_index, ce, option)) { + discard_cache_entry(ce); + return error("%s: cannot add to the index - missing --add option?", path); + } + return 0; +} + +/* + * Handle a path that was a directory. Four cases: + * + * - it's already a gitlink in the index, and we keep it that + * way, and update it if we can (if we cannot find the HEAD, + * we're going to keep it unchanged in the index!) + * + * - it's a *file* in the index, in which case it should be + * removed as a file if removal is allowed, since it doesn't + * exist as such any more. If removal isn't allowed, it's + * an error. + * + * (NOTE! This is old and arguably fairly strange behaviour. + * We might want to make this an error unconditionally, and + * use "--force-remove" if you actually want to force removal). + * + * - it used to exist as a subdirectory (ie multiple files with + * this particular prefix) in the index, in which case it's wrong + * to try to update it as a directory. + * + * - it doesn't exist at all in the index, but it is a valid + * git directory, and it should be *added* as a gitlink. + */ +static int process_directory(const char *path, int len, struct stat *st) +{ + struct object_id oid; + int pos = index_name_pos(&the_index, path, len); + + /* Exact match: file or existing gitlink */ + if (pos >= 0) { + const struct cache_entry *ce = the_index.cache[pos]; + if (S_ISGITLINK(ce->ce_mode)) { + + /* Do nothing to the index if there is no HEAD! */ + if (resolve_gitlink_ref(path, "HEAD", &oid) < 0) + return 0; + + return add_one_path(ce, path, len, st); + } + /* Should this be an unconditional error? */ + return remove_one_path(path); + } + + /* Inexact match: is there perhaps a subdirectory match? */ + pos = -pos-1; + while (pos < the_index.cache_nr) { + const struct cache_entry *ce = the_index.cache[pos++]; + + if (strncmp(ce->name, path, len)) + break; + if (ce->name[len] > '/') + break; + if (ce->name[len] < '/') + continue; + + /* Subdirectory match - error out */ + return error("%s: is a directory - add individual files instead", path); + } + + /* No match - should we add it as a gitlink? */ + if (!resolve_gitlink_ref(path, "HEAD", &oid)) + return add_one_path(NULL, path, len, st); + + /* Error out. */ + return error("%s: is a directory - add files inside instead", path); +} + +static int process_path(const char *path, struct stat *st, int stat_errno) +{ + int pos, len; + const struct cache_entry *ce; + + len = strlen(path); + if (has_symlink_leading_path(path, len)) + return error("'%s' is beyond a symbolic link", path); + + pos = index_name_pos(&the_index, path, len); + ce = pos < 0 ? NULL : the_index.cache[pos]; + if (ce && ce_skip_worktree(ce)) { + /* + * working directory version is assumed "good" + * so updating it does not make sense. + * On the other hand, removing it from index should work + */ + if (!ignore_skip_worktree_entries && allow_remove && + remove_file_from_index(&the_index, path)) + return error("%s: cannot remove from the index", path); + return 0; + } + + /* + * First things first: get the stat information, to decide + * what to do about the pathname! + */ + if (stat_errno) + return process_lstat_error(path, stat_errno); + + if (S_ISDIR(st->st_mode)) + return process_directory(path, len, st); + + return add_one_path(ce, path, len, st); +} + +static int add_cacheinfo(unsigned int mode, const struct object_id *oid, + const char *path, int stage) +{ + int len, option; + struct cache_entry *ce; + + if (!verify_path(path, mode)) + return error("Invalid path '%s'", path); + + len = strlen(path); + ce = make_empty_cache_entry(&the_index, len); + + oidcpy(&ce->oid, oid); + memcpy(ce->name, path, len); + ce->ce_flags = create_ce_flags(stage); + ce->ce_namelen = len; + ce->ce_mode = create_ce_mode(mode); + if (assume_unchanged) + ce->ce_flags |= CE_VALID; + option = allow_add ? ADD_CACHE_OK_TO_ADD : 0; + option |= allow_replace ? ADD_CACHE_OK_TO_REPLACE : 0; + if (add_index_entry(&the_index, ce, option)) + return error("%s: cannot add to the index - missing --add option?", + path); + report("add '%s'", path); + return 0; +} + +static void chmod_path(char flip, const char *path) +{ + int pos; + struct cache_entry *ce; + + pos = index_name_pos(&the_index, path, strlen(path)); + if (pos < 0) + goto fail; + ce = the_index.cache[pos]; + if (chmod_index_entry(&the_index, ce, flip) < 0) + goto fail; + + report("chmod %cx '%s'", flip, path); + return; + fail: + die("git update-index: cannot chmod %cx '%s'", flip, path); +} + +static void update_one(const char *path) +{ + int stat_errno = 0; + struct stat st; + + if (mark_valid_only || mark_skip_worktree_only || force_remove || + mark_fsmonitor_only) + st.st_mode = 0; + else if (lstat(path, &st) < 0) { + st.st_mode = 0; + stat_errno = errno; + } /* else stat is valid */ + + if (!verify_path(path, st.st_mode)) { + fprintf(stderr, "Ignoring path %s\n", path); + return; + } + if (mark_valid_only) { + if (mark_ce_flags(path, CE_VALID, mark_valid_only == MARK_FLAG)) + die("Unable to mark file %s", path); + return; + } + if (mark_skip_worktree_only) { + if (mark_ce_flags(path, CE_SKIP_WORKTREE, mark_skip_worktree_only == MARK_FLAG)) + die("Unable to mark file %s", path); + return; + } + if (mark_fsmonitor_only) { + if (mark_ce_flags(path, CE_FSMONITOR_VALID, mark_fsmonitor_only == MARK_FLAG)) + die("Unable to mark file %s", path); + return; + } + + if (force_remove) { + if (remove_file_from_index(&the_index, path)) + die("git update-index: unable to remove %s", path); + report("remove '%s'", path); + return; + } + if (process_path(path, &st, stat_errno)) + die("Unable to process path %s", path); + report("add '%s'", path); +} + +static void read_index_info(int nul_term_line) +{ + const int hexsz = the_hash_algo->hexsz; + struct strbuf buf = STRBUF_INIT; + struct strbuf uq = STRBUF_INIT; + strbuf_getline_fn getline_fn; + + getline_fn = nul_term_line ? strbuf_getline_nul : strbuf_getline_lf; + while (getline_fn(&buf, stdin) != EOF) { + char *ptr, *tab; + char *path_name; + struct object_id oid; + unsigned int mode; + unsigned long ul; + int stage; + + /* This reads lines formatted in one of three formats: + * + * (1) mode SP sha1 TAB path + * The first format is what "git apply --index-info" + * reports, and used to reconstruct a partial tree + * that is used for phony merge base tree when falling + * back on 3-way merge. + * + * (2) mode SP type SP sha1 TAB path + * The second format is to stuff "git ls-tree" output + * into the index file. + * + * (3) mode SP sha1 SP stage TAB path + * This format is to put higher order stages into the + * index file and matches "git ls-files --stage" output. + */ + errno = 0; + ul = strtoul(buf.buf, &ptr, 8); + if (ptr == buf.buf || *ptr != ' ' + || errno || (unsigned int) ul != ul) + goto bad_line; + mode = ul; + + tab = strchr(ptr, '\t'); + if (!tab || tab - ptr < hexsz + 1) + goto bad_line; + + if (tab[-2] == ' ' && '0' <= tab[-1] && tab[-1] <= '3') { + stage = tab[-1] - '0'; + ptr = tab + 1; /* point at the head of path */ + tab = tab - 2; /* point at tail of sha1 */ + } + else { + stage = 0; + ptr = tab + 1; /* point at the head of path */ + } + + if (get_oid_hex(tab - hexsz, &oid) || + tab[-(hexsz + 1)] != ' ') + goto bad_line; + + path_name = ptr; + if (!nul_term_line && path_name[0] == '"') { + strbuf_reset(&uq); + if (unquote_c_style(&uq, path_name, NULL)) { + die("git update-index: bad quoting of path name"); + } + path_name = uq.buf; + } + + if (!verify_path(path_name, mode)) { + fprintf(stderr, "Ignoring path %s\n", path_name); + continue; + } + + if (!mode) { + /* mode == 0 means there is no such path -- remove */ + if (remove_file_from_index(&the_index, path_name)) + die("git update-index: unable to remove %s", + ptr); + } + else { + /* mode ' ' sha1 '\t' name + * ptr[-1] points at tab, + * ptr[-41] is at the beginning of sha1 + */ + ptr[-(hexsz + 2)] = ptr[-1] = 0; + if (add_cacheinfo(mode, &oid, path_name, stage)) + die("git update-index: unable to update %s", + path_name); + } + continue; + + bad_line: + die("malformed index info %s", buf.buf); + } + strbuf_release(&buf); + strbuf_release(&uq); +} + +static const char * const update_index_usage[] = { + N_("git update-index [<options>] [--] [<file>...]"), + NULL +}; + +static struct cache_entry *read_one_ent(const char *which, + struct object_id *ent, const char *path, + int namelen, int stage) +{ + unsigned short mode; + struct object_id oid; + struct cache_entry *ce; + + if (get_tree_entry(the_repository, ent, path, &oid, &mode)) { + if (which) + error("%s: not in %s branch.", path, which); + return NULL; + } + if (!the_index.sparse_index && mode == S_IFDIR) { + if (which) + error("%s: not a blob in %s branch.", path, which); + return NULL; + } + ce = make_empty_cache_entry(&the_index, namelen); + + oidcpy(&ce->oid, &oid); + memcpy(ce->name, path, namelen); + ce->ce_flags = create_ce_flags(stage); + ce->ce_namelen = namelen; + ce->ce_mode = create_ce_mode(mode); + return ce; +} + +static int unresolve_one(const char *path) +{ + struct string_list_item *item; + int res = 0; + + if (!the_index.resolve_undo) + return res; + item = string_list_lookup(the_index.resolve_undo, path); + if (!item) + return res; /* no resolve-undo record for the path */ + res = unmerge_index_entry(&the_index, path, item->util, 0); + FREE_AND_NULL(item->util); + return res; +} + +static int do_unresolve(int ac, const char **av, + const char *prefix, int prefix_length) +{ + int i; + int err = 0; + + for (i = 1; i < ac; i++) { + const char *arg = av[i]; + char *p = prefix_path(prefix, prefix_length, arg); + err |= unresolve_one(p); + free(p); + } + return err; +} + +static int do_reupdate(const char **paths, + const char *prefix) +{ + /* Read HEAD and run update-index on paths that are + * merged and already different between index and HEAD. + */ + int pos; + int has_head = 1; + struct pathspec pathspec; + struct object_id head_oid; + + parse_pathspec(&pathspec, 0, + PATHSPEC_PREFER_CWD, + prefix, paths); + + if (read_ref("HEAD", &head_oid)) + /* If there is no HEAD, that means it is an initial + * commit. Update everything in the index. + */ + has_head = 0; + redo: + for (pos = 0; pos < the_index.cache_nr; pos++) { + const struct cache_entry *ce = the_index.cache[pos]; + struct cache_entry *old = NULL; + int save_nr; + char *path; + + if (ce_stage(ce) || !ce_path_match(&the_index, ce, &pathspec, NULL)) + continue; + if (has_head) + old = read_one_ent(NULL, &head_oid, + ce->name, ce_namelen(ce), 0); + if (old && ce->ce_mode == old->ce_mode && + oideq(&ce->oid, &old->oid)) { + discard_cache_entry(old); + continue; /* unchanged */ + } + + /* At this point, we know the contents of the sparse directory are + * modified with respect to HEAD, so we expand the index and restart + * to process each path individually + */ + if (S_ISSPARSEDIR(ce->ce_mode)) { + ensure_full_index(&the_index); + goto redo; + } + + /* Be careful. The working tree may not have the + * path anymore, in which case, under 'allow_remove', + * or worse yet 'allow_replace', active_nr may decrease. + */ + save_nr = the_index.cache_nr; + path = xstrdup(ce->name); + update_one(path); + free(path); + discard_cache_entry(old); + if (save_nr != the_index.cache_nr) + goto redo; + } + clear_pathspec(&pathspec); + return 0; +} + +struct refresh_params { + unsigned int flags; + int *has_errors; +}; + +static int refresh(struct refresh_params *o, unsigned int flag) +{ + setup_work_tree(); + repo_read_index(the_repository); + *o->has_errors |= refresh_index(&the_index, o->flags | flag, NULL, + NULL, NULL); + if (has_racy_timestamp(&the_index)) { + /* + * Even if nothing else has changed, updating the file + * increases the chance that racy timestamps become + * non-racy, helping future run-time performance. + * We do that even in case of "errors" returned by + * refresh_index() as these are no actual errors. + * cmd_status() does the same. + */ + the_index.cache_changed |= SOMETHING_CHANGED; + } + return 0; +} + +static int refresh_callback(const struct option *opt, + const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + return refresh(opt->value, 0); +} + +static int really_refresh_callback(const struct option *opt, + const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + return refresh(opt->value, REFRESH_REALLY); +} + +static int chmod_callback(const struct option *opt, + const char *arg, int unset) +{ + char *flip = opt->value; + BUG_ON_OPT_NEG(unset); + if ((arg[0] != '-' && arg[0] != '+') || arg[1] != 'x' || arg[2]) + return error("option 'chmod' expects \"+x\" or \"-x\""); + *flip = arg[0]; + return 0; +} + +static int resolve_undo_clear_callback(const struct option *opt UNUSED, + const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + resolve_undo_clear_index(&the_index); + return 0; +} + +static int parse_new_style_cacheinfo(const char *arg, + unsigned int *mode, + struct object_id *oid, + const char **path) +{ + unsigned long ul; + char *endp; + const char *p; + + if (!arg) + return -1; + + errno = 0; + ul = strtoul(arg, &endp, 8); + if (errno || endp == arg || *endp != ',' || (unsigned int) ul != ul) + return -1; /* not a new-style cacheinfo */ + *mode = ul; + endp++; + if (parse_oid_hex(endp, oid, &p) || *p != ',') + return -1; + *path = p + 1; + return 0; +} + +static enum parse_opt_result cacheinfo_callback( + struct parse_opt_ctx_t *ctx, const struct option *opt UNUSED, + const char *arg, int unset) +{ + struct object_id oid; + unsigned int mode; + const char *path; + + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + + if (!parse_new_style_cacheinfo(ctx->argv[1], &mode, &oid, &path)) { + if (add_cacheinfo(mode, &oid, path, 0)) + die("git update-index: --cacheinfo cannot add %s", path); + ctx->argv++; + ctx->argc--; + return 0; + } + if (ctx->argc <= 3) + return error("option 'cacheinfo' expects <mode>,<sha1>,<path>"); + if (strtoul_ui(*++ctx->argv, 8, &mode) || + get_oid_hex(*++ctx->argv, &oid) || + add_cacheinfo(mode, &oid, *++ctx->argv, 0)) + die("git update-index: --cacheinfo cannot add %s", *ctx->argv); + ctx->argc -= 3; + return 0; +} + +static enum parse_opt_result stdin_cacheinfo_callback( + struct parse_opt_ctx_t *ctx, const struct option *opt, + const char *arg, int unset) +{ + int *nul_term_line = opt->value; + + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + + if (ctx->argc != 1) + return error("option '%s' must be the last argument", opt->long_name); + allow_add = allow_replace = allow_remove = 1; + read_index_info(*nul_term_line); + return 0; +} + +static enum parse_opt_result stdin_callback( + struct parse_opt_ctx_t *ctx, const struct option *opt, + const char *arg, int unset) +{ + int *read_from_stdin = opt->value; + + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + + if (ctx->argc != 1) + return error("option '%s' must be the last argument", opt->long_name); + *read_from_stdin = 1; + return 0; +} + +static enum parse_opt_result unresolve_callback( + struct parse_opt_ctx_t *ctx, const struct option *opt, + const char *arg, int unset) +{ + int *has_errors = opt->value; + const char *prefix = startup_info->prefix; + + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + + /* consume remaining arguments. */ + *has_errors = do_unresolve(ctx->argc, ctx->argv, + prefix, prefix ? strlen(prefix) : 0); + if (*has_errors) + the_index.cache_changed = 0; + + ctx->argv += ctx->argc - 1; + ctx->argc = 1; + return 0; +} + +static enum parse_opt_result reupdate_callback( + struct parse_opt_ctx_t *ctx, const struct option *opt, + const char *arg, int unset) +{ + int *has_errors = opt->value; + const char *prefix = startup_info->prefix; + + BUG_ON_OPT_NEG(unset); + BUG_ON_OPT_ARG(arg); + + /* consume remaining arguments. */ + setup_work_tree(); + *has_errors = do_reupdate(ctx->argv + 1, prefix); + if (*has_errors) + the_index.cache_changed = 0; + + ctx->argv += ctx->argc - 1; + ctx->argc = 1; + return 0; +} + +int cmd_update_index(int argc, const char **argv, const char *prefix) +{ + int newfd, entries, has_errors = 0, nul_term_line = 0; + enum uc_mode untracked_cache = UC_UNSPECIFIED; + int read_from_stdin = 0; + int prefix_length = prefix ? strlen(prefix) : 0; + int preferred_index_format = 0; + char set_executable_bit = 0; + struct refresh_params refresh_args = {0, &has_errors}; + int lock_error = 0; + int split_index = -1; + int force_write = 0; + int fsmonitor = -1; + struct lock_file lock_file = LOCK_INIT; + struct parse_opt_ctx_t ctx; + strbuf_getline_fn getline_fn; + int parseopt_state = PARSE_OPT_UNKNOWN; + struct repository *r = the_repository; + struct option options[] = { + OPT_BIT('q', NULL, &refresh_args.flags, + N_("continue refresh even when index needs update"), + REFRESH_QUIET), + OPT_BIT(0, "ignore-submodules", &refresh_args.flags, + N_("refresh: ignore submodules"), + REFRESH_IGNORE_SUBMODULES), + OPT_SET_INT(0, "add", &allow_add, + N_("do not ignore new files"), 1), + OPT_SET_INT(0, "replace", &allow_replace, + N_("let files replace directories and vice-versa"), 1), + OPT_SET_INT(0, "remove", &allow_remove, + N_("notice files missing from worktree"), 1), + OPT_BIT(0, "unmerged", &refresh_args.flags, + N_("refresh even if index contains unmerged entries"), + REFRESH_UNMERGED), + OPT_CALLBACK_F(0, "refresh", &refresh_args, NULL, + N_("refresh stat information"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, + refresh_callback), + OPT_CALLBACK_F(0, "really-refresh", &refresh_args, NULL, + N_("like --refresh, but ignore assume-unchanged setting"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, + really_refresh_callback), + {OPTION_LOWLEVEL_CALLBACK, 0, "cacheinfo", NULL, + N_("<mode>,<object>,<path>"), + N_("add the specified entry to the index"), + PARSE_OPT_NOARG | /* disallow --cacheinfo=<mode> form */ + PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, + NULL, 0, + cacheinfo_callback}, + OPT_CALLBACK_F(0, "chmod", &set_executable_bit, "(+|-)x", + N_("override the executable bit of the listed files"), + PARSE_OPT_NONEG, + chmod_callback), + {OPTION_SET_INT, 0, "assume-unchanged", &mark_valid_only, NULL, + N_("mark files as \"not changing\""), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG}, + {OPTION_SET_INT, 0, "no-assume-unchanged", &mark_valid_only, NULL, + N_("clear assumed-unchanged bit"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG}, + {OPTION_SET_INT, 0, "skip-worktree", &mark_skip_worktree_only, NULL, + N_("mark files as \"index-only\""), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG}, + {OPTION_SET_INT, 0, "no-skip-worktree", &mark_skip_worktree_only, NULL, + N_("clear skip-worktree bit"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG}, + OPT_BOOL(0, "ignore-skip-worktree-entries", &ignore_skip_worktree_entries, + N_("do not touch index-only entries")), + OPT_SET_INT(0, "info-only", &info_only, + N_("add to index only; do not add content to object database"), 1), + OPT_SET_INT(0, "force-remove", &force_remove, + N_("remove named paths even if present in worktree"), 1), + OPT_BOOL('z', NULL, &nul_term_line, + N_("with --stdin: input lines are terminated by null bytes")), + {OPTION_LOWLEVEL_CALLBACK, 0, "stdin", &read_from_stdin, NULL, + N_("read list of paths to be updated from standard input"), + PARSE_OPT_NONEG | PARSE_OPT_NOARG, + NULL, 0, stdin_callback}, + {OPTION_LOWLEVEL_CALLBACK, 0, "index-info", &nul_term_line, NULL, + N_("add entries from standard input to the index"), + PARSE_OPT_NONEG | PARSE_OPT_NOARG, + NULL, 0, stdin_cacheinfo_callback}, + {OPTION_LOWLEVEL_CALLBACK, 0, "unresolve", &has_errors, NULL, + N_("repopulate stages #2 and #3 for the listed paths"), + PARSE_OPT_NONEG | PARSE_OPT_NOARG, + NULL, 0, unresolve_callback}, + {OPTION_LOWLEVEL_CALLBACK, 'g', "again", &has_errors, NULL, + N_("only update entries that differ from HEAD"), + PARSE_OPT_NONEG | PARSE_OPT_NOARG, + NULL, 0, reupdate_callback}, + OPT_BIT(0, "ignore-missing", &refresh_args.flags, + N_("ignore files missing from worktree"), + REFRESH_IGNORE_MISSING), + OPT_SET_INT(0, "verbose", &verbose, + N_("report actions to standard output"), 1), + OPT_CALLBACK_F(0, "clear-resolve-undo", NULL, NULL, + N_("(for porcelains) forget saved unresolved conflicts"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, + resolve_undo_clear_callback), + OPT_INTEGER(0, "index-version", &preferred_index_format, + N_("write index in this format")), + OPT_SET_INT(0, "show-index-version", &preferred_index_format, + N_("report on-disk index format version"), -1), + OPT_BOOL(0, "split-index", &split_index, + N_("enable or disable split index")), + OPT_BOOL(0, "untracked-cache", &untracked_cache, + N_("enable/disable untracked cache")), + OPT_SET_INT(0, "test-untracked-cache", &untracked_cache, + N_("test if the filesystem supports untracked cache"), UC_TEST), + OPT_SET_INT(0, "force-untracked-cache", &untracked_cache, + N_("enable untracked cache without testing the filesystem"), UC_FORCE), + OPT_SET_INT(0, "force-write-index", &force_write, + N_("write out the index even if is not flagged as changed"), 1), + OPT_BOOL(0, "fsmonitor", &fsmonitor, + N_("enable or disable file system monitor")), + {OPTION_SET_INT, 0, "fsmonitor-valid", &mark_fsmonitor_only, NULL, + N_("mark files as fsmonitor valid"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG}, + {OPTION_SET_INT, 0, "no-fsmonitor-valid", &mark_fsmonitor_only, NULL, + N_("clear fsmonitor valid bit"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG}, + OPT_END() + }; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(update_index_usage, options); + + git_config(git_default_config, NULL); + + prepare_repo_settings(r); + the_repository->settings.command_requires_full_index = 0; + + /* we will diagnose later if it turns out that we need to update it */ + newfd = repo_hold_locked_index(the_repository, &lock_file, 0); + if (newfd < 0) + lock_error = errno; + + entries = repo_read_index(the_repository); + if (entries < 0) + die("cache corrupted"); + + the_index.updated_skipworktree = 1; + + /* + * Custom copy of parse_options() because we want to handle + * filename arguments as they come. + */ + parse_options_start(&ctx, argc, argv, prefix, + options, PARSE_OPT_STOP_AT_NON_OPTION); + + /* + * Allow the object layer to optimize adding multiple objects in + * a batch. + */ + begin_odb_transaction(); + while (ctx.argc) { + if (parseopt_state != PARSE_OPT_DONE) + parseopt_state = parse_options_step(&ctx, options, + update_index_usage); + if (!ctx.argc) + break; + switch (parseopt_state) { + case PARSE_OPT_HELP: + case PARSE_OPT_ERROR: + exit(129); + case PARSE_OPT_COMPLETE: + exit(0); + case PARSE_OPT_NON_OPTION: + case PARSE_OPT_DONE: + { + const char *path = ctx.argv[0]; + char *p; + + setup_work_tree(); + p = prefix_path(prefix, prefix_length, path); + update_one(p); + if (set_executable_bit) + chmod_path(set_executable_bit, p); + free(p); + ctx.argc--; + ctx.argv++; + break; + } + case PARSE_OPT_UNKNOWN: + if (ctx.argv[0][1] == '-') + error("unknown option '%s'", ctx.argv[0] + 2); + else + error("unknown switch '%c'", *ctx.opt); + usage_with_options(update_index_usage, options); + } + } + argc = parse_options_end(&ctx); + + getline_fn = nul_term_line ? strbuf_getline_nul : strbuf_getline_lf; + if (preferred_index_format) { + if (preferred_index_format < 0) { + printf(_("%d\n"), the_index.version); + } else if (preferred_index_format < INDEX_FORMAT_LB || + INDEX_FORMAT_UB < preferred_index_format) { + die("index-version %d not in range: %d..%d", + preferred_index_format, + INDEX_FORMAT_LB, INDEX_FORMAT_UB); + } else { + if (the_index.version != preferred_index_format) + the_index.cache_changed |= SOMETHING_CHANGED; + report(_("index-version: was %d, set to %d"), + the_index.version, preferred_index_format); + the_index.version = preferred_index_format; + } + } + + if (read_from_stdin) { + struct strbuf buf = STRBUF_INIT; + struct strbuf unquoted = STRBUF_INIT; + + setup_work_tree(); + 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); + update_one(p); + if (set_executable_bit) + chmod_path(set_executable_bit, p); + free(p); + } + strbuf_release(&unquoted); + strbuf_release(&buf); + } + + /* + * By now we have added all of the new objects + */ + end_odb_transaction(); + + if (split_index > 0) { + if (git_config_get_split_index() == 0) + warning(_("core.splitIndex is set to false; " + "remove or change it, if you really want to " + "enable split index")); + if (the_index.split_index) + the_index.cache_changed |= SPLIT_INDEX_ORDERED; + else + add_split_index(&the_index); + } else if (!split_index) { + if (git_config_get_split_index() == 1) + warning(_("core.splitIndex is set to true; " + "remove or change it, if you really want to " + "disable split index")); + remove_split_index(&the_index); + } + + prepare_repo_settings(r); + switch (untracked_cache) { + case UC_UNSPECIFIED: + break; + case UC_DISABLE: + if (r->settings.core_untracked_cache == UNTRACKED_CACHE_WRITE) + warning(_("core.untrackedCache is set to true; " + "remove or change it, if you really want to " + "disable the untracked cache")); + remove_untracked_cache(&the_index); + report(_("Untracked cache disabled")); + break; + case UC_TEST: + setup_work_tree(); + return !test_if_untracked_cache_is_supported(); + case UC_ENABLE: + case UC_FORCE: + if (r->settings.core_untracked_cache == UNTRACKED_CACHE_REMOVE) + warning(_("core.untrackedCache is set to false; " + "remove or change it, if you really want to " + "enable the untracked cache")); + add_untracked_cache(&the_index); + report(_("Untracked cache enabled for '%s'"), get_git_work_tree()); + break; + default: + BUG("bad untracked_cache value: %d", untracked_cache); + } + + if (fsmonitor > 0) { + enum fsmonitor_mode fsm_mode = fsm_settings__get_mode(r); + enum fsmonitor_reason reason = fsm_settings__get_reason(r); + + /* + * The user wants to turn on FSMonitor using the command + * line argument. (We don't know (or care) whether that + * is the IPC or HOOK version.) + * + * Use one of the __get routines to force load the FSMonitor + * config settings into the repo-settings. That will detect + * whether the file system is compatible so that we can stop + * here with a nice error message. + */ + if (reason > FSMONITOR_REASON_OK) + die("%s", + fsm_settings__get_incompatible_msg(r, reason)); + + if (fsm_mode == FSMONITOR_MODE_DISABLED) { + warning(_("core.fsmonitor is unset; " + "set it if you really want to " + "enable fsmonitor")); + } + add_fsmonitor(&the_index); + report(_("fsmonitor enabled")); + } else if (!fsmonitor) { + enum fsmonitor_mode fsm_mode = fsm_settings__get_mode(r); + if (fsm_mode > FSMONITOR_MODE_DISABLED) + warning(_("core.fsmonitor is set; " + "remove it if you really want to " + "disable fsmonitor")); + remove_fsmonitor(&the_index); + report(_("fsmonitor disabled")); + } + + if (the_index.cache_changed || force_write) { + if (newfd < 0) { + if (refresh_args.flags & REFRESH_QUIET) + exit(128); + unable_to_lock_die(get_index_file(), lock_error); + } + if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK)) + die("Unable to write new index file"); + } + + rollback_lock_file(&lock_file); + + return has_errors ? 1 : 0; +} diff --git a/builtin/update-ref.c b/builtin/update-ref.c new file mode 100644 index 0000000..c0c4e65 --- /dev/null +++ b/builtin/update-ref.c @@ -0,0 +1,582 @@ +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "hash.h" +#include "refs.h" +#include "object-name.h" +#include "parse-options.h" +#include "quote.h" +#include "repository.h" +#include "strvec.h" + +static const char * const git_update_ref_usage[] = { + N_("git update-ref [<options>] -d <refname> [<old-val>]"), + N_("git update-ref [<options>] <refname> <new-val> [<old-val>]"), + N_("git update-ref [<options>] --stdin [-z]"), + NULL +}; + +static char line_termination = '\n'; +static unsigned int update_flags; +static unsigned int default_flags; +static unsigned create_reflog_flag; +static const char *msg; + +/* + * Parse one whitespace- or NUL-terminated, possibly C-quoted argument + * and append the result to arg. Return a pointer to the terminator. + * Die if there is an error in how the argument is C-quoted. This + * function is only used if not -z. + */ +static const char *parse_arg(const char *next, struct strbuf *arg) +{ + if (*next == '"') { + const char *orig = next; + + if (unquote_c_style(arg, next, &next)) + die("badly quoted argument: %s", orig); + if (*next && !isspace(*next)) + die("unexpected character after quoted argument: %s", orig); + } else { + while (*next && !isspace(*next)) + strbuf_addch(arg, *next++); + } + + return next; +} + +/* + * Parse the reference name immediately after "command SP". If not + * -z, then handle C-quoting. Return a pointer to a newly allocated + * string containing the name of the reference, or NULL if there was + * an error. Update *next to point at the character that terminates + * the argument. Die if C-quoting is malformed or the reference name + * is invalid. + */ +static char *parse_refname(const char **next) +{ + struct strbuf ref = STRBUF_INIT; + + if (line_termination) { + /* Without -z, use the next argument */ + *next = parse_arg(*next, &ref); + } else { + /* With -z, use everything up to the next NUL */ + strbuf_addstr(&ref, *next); + *next += ref.len; + } + + if (!ref.len) { + strbuf_release(&ref); + return NULL; + } + + if (check_refname_format(ref.buf, REFNAME_ALLOW_ONELEVEL)) + die("invalid ref format: %s", ref.buf); + + return strbuf_detach(&ref, NULL); +} + +/* + * The value being parsed is <oldvalue> (as opposed to <newvalue>; the + * difference affects which error messages are generated): + */ +#define PARSE_SHA1_OLD 0x01 + +/* + * For backwards compatibility, accept an empty string for update's + * <newvalue> in binary mode to be equivalent to specifying zeros. + */ +#define PARSE_SHA1_ALLOW_EMPTY 0x02 + +/* + * Parse an argument separator followed by the next argument, if any. + * If there is an argument, convert it to a SHA-1, write it to sha1, + * set *next to point at the character terminating the argument, and + * return 0. If there is no argument at all (not even the empty + * string), return 1 and leave *next unchanged. If the value is + * provided but cannot be converted to a SHA-1, die. flags can + * include PARSE_SHA1_OLD and/or PARSE_SHA1_ALLOW_EMPTY. + */ +static int parse_next_oid(const char **next, const char *end, + struct object_id *oid, + const char *command, const char *refname, + int flags) +{ + struct strbuf arg = STRBUF_INIT; + int ret = 0; + + if (*next == end) + goto eof; + + if (line_termination) { + /* Without -z, consume SP and use next argument */ + if (!**next || **next == line_termination) + return 1; + if (**next != ' ') + die("%s %s: expected SP but got: %s", + command, refname, *next); + (*next)++; + *next = parse_arg(*next, &arg); + if (arg.len) { + if (repo_get_oid(the_repository, arg.buf, oid)) + goto invalid; + } else { + /* Without -z, an empty value means all zeros: */ + oidclr(oid); + } + } else { + /* With -z, read the next NUL-terminated line */ + if (**next) + die("%s %s: expected NUL but got: %s", + command, refname, *next); + (*next)++; + if (*next == end) + goto eof; + strbuf_addstr(&arg, *next); + *next += arg.len; + + if (arg.len) { + if (repo_get_oid(the_repository, arg.buf, oid)) + goto invalid; + } else if (flags & PARSE_SHA1_ALLOW_EMPTY) { + /* With -z, treat an empty value as all zeros: */ + warning("%s %s: missing <newvalue>, treating as zero", + command, refname); + oidclr(oid); + } else { + /* + * With -z, an empty non-required value means + * unspecified: + */ + ret = 1; + } + } + + strbuf_release(&arg); + + return ret; + + invalid: + die(flags & PARSE_SHA1_OLD ? + "%s %s: invalid <oldvalue>: %s" : + "%s %s: invalid <newvalue>: %s", + command, refname, arg.buf); + + eof: + die(flags & PARSE_SHA1_OLD ? + "%s %s: unexpected end of input when reading <oldvalue>" : + "%s %s: unexpected end of input when reading <newvalue>", + command, refname); +} + + +/* + * The following five parse_cmd_*() functions parse the corresponding + * command. In each case, next points at the character following the + * command name and the following space. They each return a pointer + * to the character terminating the command, and die with an + * explanatory message if there are any parsing problems. All of + * these functions handle either text or binary format input, + * depending on how line_termination is set. + */ + +static void parse_cmd_update(struct ref_transaction *transaction, + const char *next, const char *end) +{ + struct strbuf err = STRBUF_INIT; + char *refname; + struct object_id new_oid, old_oid; + int have_old; + + refname = parse_refname(&next); + if (!refname) + die("update: missing <ref>"); + + if (parse_next_oid(&next, end, &new_oid, "update", refname, + PARSE_SHA1_ALLOW_EMPTY)) + die("update %s: missing <newvalue>", refname); + + have_old = !parse_next_oid(&next, end, &old_oid, "update", refname, + PARSE_SHA1_OLD); + + if (*next != line_termination) + die("update %s: extra input: %s", refname, next); + + if (ref_transaction_update(transaction, refname, + &new_oid, have_old ? &old_oid : NULL, + update_flags | create_reflog_flag, + msg, &err)) + die("%s", err.buf); + + update_flags = default_flags; + free(refname); + strbuf_release(&err); +} + +static void parse_cmd_create(struct ref_transaction *transaction, + const char *next, const char *end) +{ + struct strbuf err = STRBUF_INIT; + char *refname; + struct object_id new_oid; + + refname = parse_refname(&next); + if (!refname) + die("create: missing <ref>"); + + if (parse_next_oid(&next, end, &new_oid, "create", refname, 0)) + die("create %s: missing <newvalue>", refname); + + if (is_null_oid(&new_oid)) + die("create %s: zero <newvalue>", refname); + + if (*next != line_termination) + die("create %s: extra input: %s", refname, next); + + if (ref_transaction_create(transaction, refname, &new_oid, + update_flags | create_reflog_flag, + msg, &err)) + die("%s", err.buf); + + update_flags = default_flags; + free(refname); + strbuf_release(&err); +} + +static void parse_cmd_delete(struct ref_transaction *transaction, + const char *next, const char *end) +{ + struct strbuf err = STRBUF_INIT; + char *refname; + struct object_id old_oid; + int have_old; + + refname = parse_refname(&next); + if (!refname) + die("delete: missing <ref>"); + + if (parse_next_oid(&next, end, &old_oid, "delete", refname, + PARSE_SHA1_OLD)) { + have_old = 0; + } else { + if (is_null_oid(&old_oid)) + die("delete %s: zero <oldvalue>", refname); + have_old = 1; + } + + if (*next != line_termination) + die("delete %s: extra input: %s", refname, next); + + if (ref_transaction_delete(transaction, refname, + have_old ? &old_oid : NULL, + update_flags, msg, &err)) + die("%s", err.buf); + + update_flags = default_flags; + free(refname); + strbuf_release(&err); +} + +static void parse_cmd_verify(struct ref_transaction *transaction, + const char *next, const char *end) +{ + struct strbuf err = STRBUF_INIT; + char *refname; + struct object_id old_oid; + + refname = parse_refname(&next); + if (!refname) + die("verify: missing <ref>"); + + if (parse_next_oid(&next, end, &old_oid, "verify", refname, + PARSE_SHA1_OLD)) + oidclr(&old_oid); + + if (*next != line_termination) + die("verify %s: extra input: %s", refname, next); + + if (ref_transaction_verify(transaction, refname, &old_oid, + update_flags, &err)) + die("%s", err.buf); + + update_flags = default_flags; + free(refname); + strbuf_release(&err); +} + +static void report_ok(const char *command) +{ + fprintf(stdout, "%s: ok\n", command); + fflush(stdout); +} + +static void parse_cmd_option(struct ref_transaction *transaction UNUSED, + const char *next, const char *end UNUSED) +{ + const char *rest; + if (skip_prefix(next, "no-deref", &rest) && *rest == line_termination) + update_flags |= REF_NO_DEREF; + else + die("option unknown: %s", next); +} + +static void parse_cmd_start(struct ref_transaction *transaction UNUSED, + const char *next, const char *end UNUSED) +{ + if (*next != line_termination) + die("start: extra input: %s", next); + report_ok("start"); +} + +static void parse_cmd_prepare(struct ref_transaction *transaction, + const char *next, const char *end UNUSED) +{ + struct strbuf error = STRBUF_INIT; + if (*next != line_termination) + die("prepare: extra input: %s", next); + if (ref_transaction_prepare(transaction, &error)) + die("prepare: %s", error.buf); + report_ok("prepare"); +} + +static void parse_cmd_abort(struct ref_transaction *transaction, + const char *next, const char *end UNUSED) +{ + struct strbuf error = STRBUF_INIT; + if (*next != line_termination) + die("abort: extra input: %s", next); + if (ref_transaction_abort(transaction, &error)) + die("abort: %s", error.buf); + report_ok("abort"); +} + +static void parse_cmd_commit(struct ref_transaction *transaction, + const char *next, const char *end UNUSED) +{ + struct strbuf error = STRBUF_INIT; + if (*next != line_termination) + die("commit: extra input: %s", next); + if (ref_transaction_commit(transaction, &error)) + die("commit: %s", error.buf); + report_ok("commit"); + ref_transaction_free(transaction); +} + +enum update_refs_state { + /* Non-transactional state open for updates. */ + UPDATE_REFS_OPEN, + /* A transaction has been started. */ + UPDATE_REFS_STARTED, + /* References are locked and ready for commit */ + UPDATE_REFS_PREPARED, + /* Transaction has been committed or closed. */ + UPDATE_REFS_CLOSED, +}; + +static const struct parse_cmd { + const char *prefix; + void (*fn)(struct ref_transaction *, const char *, const char *); + unsigned args; + enum update_refs_state state; +} command[] = { + { "update", parse_cmd_update, 3, UPDATE_REFS_OPEN }, + { "create", parse_cmd_create, 2, UPDATE_REFS_OPEN }, + { "delete", parse_cmd_delete, 2, UPDATE_REFS_OPEN }, + { "verify", parse_cmd_verify, 2, UPDATE_REFS_OPEN }, + { "option", parse_cmd_option, 1, UPDATE_REFS_OPEN }, + { "start", parse_cmd_start, 0, UPDATE_REFS_STARTED }, + { "prepare", parse_cmd_prepare, 0, UPDATE_REFS_PREPARED }, + { "abort", parse_cmd_abort, 0, UPDATE_REFS_CLOSED }, + { "commit", parse_cmd_commit, 0, UPDATE_REFS_CLOSED }, +}; + +static void update_refs_stdin(void) +{ + struct strbuf input = STRBUF_INIT, err = STRBUF_INIT; + enum update_refs_state state = UPDATE_REFS_OPEN; + struct ref_transaction *transaction; + int i, j; + + transaction = ref_transaction_begin(&err); + if (!transaction) + die("%s", err.buf); + + /* Read each line dispatch its command */ + while (!strbuf_getwholeline(&input, stdin, line_termination)) { + const struct parse_cmd *cmd = NULL; + + if (*input.buf == line_termination) + die("empty command in input"); + else if (isspace(*input.buf)) + die("whitespace before command: %s", input.buf); + + for (i = 0; i < ARRAY_SIZE(command); i++) { + const char *prefix = command[i].prefix; + char c; + + if (!starts_with(input.buf, prefix)) + continue; + + /* + * If the command has arguments, verify that it's + * followed by a space. Otherwise, it shall be followed + * by a line terminator. + */ + c = command[i].args ? ' ' : line_termination; + if (input.buf[strlen(prefix)] != c) + continue; + + cmd = &command[i]; + break; + } + if (!cmd) + die("unknown command: %s", input.buf); + + /* + * Read additional arguments if NUL-terminated. Do not raise an + * error in case there is an early EOF to let the command + * handle missing arguments with a proper error message. + */ + for (j = 1; line_termination == '\0' && j < cmd->args; j++) + if (strbuf_appendwholeline(&input, stdin, line_termination)) + break; + + switch (state) { + case UPDATE_REFS_OPEN: + case UPDATE_REFS_STARTED: + if (state == UPDATE_REFS_STARTED && cmd->state == UPDATE_REFS_STARTED) + die("cannot restart ongoing transaction"); + /* Do not downgrade a transaction to a non-transaction. */ + if (cmd->state >= state) + state = cmd->state; + break; + case UPDATE_REFS_PREPARED: + if (cmd->state != UPDATE_REFS_CLOSED) + die("prepared transactions can only be closed"); + state = cmd->state; + break; + case UPDATE_REFS_CLOSED: + if (cmd->state != UPDATE_REFS_STARTED) + die("transaction is closed"); + + /* + * Open a new transaction if we're currently closed and + * get a "start". + */ + state = cmd->state; + transaction = ref_transaction_begin(&err); + if (!transaction) + die("%s", err.buf); + + break; + } + + cmd->fn(transaction, input.buf + strlen(cmd->prefix) + !!cmd->args, + input.buf + input.len); + } + + switch (state) { + case UPDATE_REFS_OPEN: + /* Commit by default if no transaction was requested. */ + if (ref_transaction_commit(transaction, &err)) + die("%s", err.buf); + ref_transaction_free(transaction); + break; + case UPDATE_REFS_STARTED: + case UPDATE_REFS_PREPARED: + /* If using a transaction, we want to abort it. */ + if (ref_transaction_abort(transaction, &err)) + die("%s", err.buf); + break; + case UPDATE_REFS_CLOSED: + /* Otherwise no need to do anything, the transaction was closed already. */ + break; + } + + strbuf_release(&err); + strbuf_release(&input); +} + +int cmd_update_ref(int argc, const char **argv, const char *prefix) +{ + const char *refname, *oldval; + struct object_id oid, oldoid; + int delete = 0, no_deref = 0, read_stdin = 0, end_null = 0; + int create_reflog = 0; + struct option options[] = { + OPT_STRING( 'm', NULL, &msg, N_("reason"), N_("reason of the update")), + OPT_BOOL('d', NULL, &delete, N_("delete the reference")), + OPT_BOOL( 0 , "no-deref", &no_deref, + N_("update <refname> not the one it points to")), + OPT_BOOL('z', NULL, &end_null, N_("stdin has NUL-terminated arguments")), + OPT_BOOL( 0 , "stdin", &read_stdin, N_("read updates from stdin")), + OPT_BOOL( 0 , "create-reflog", &create_reflog, N_("create a reflog")), + OPT_END(), + }; + + git_config(git_default_config, NULL); + argc = parse_options(argc, argv, prefix, options, git_update_ref_usage, + 0); + if (msg && !*msg) + die("Refusing to perform update with empty message."); + + create_reflog_flag = create_reflog ? REF_FORCE_CREATE_REFLOG : 0; + + if (no_deref) { + default_flags = REF_NO_DEREF; + update_flags = default_flags; + } + + if (read_stdin) { + if (delete || argc > 0) + usage_with_options(git_update_ref_usage, options); + if (end_null) + line_termination = '\0'; + update_refs_stdin(); + return 0; + } + + if (end_null) + usage_with_options(git_update_ref_usage, options); + + if (delete) { + if (argc < 1 || argc > 2) + usage_with_options(git_update_ref_usage, options); + refname = argv[0]; + oldval = argv[1]; + } else { + const char *value; + if (argc < 2 || argc > 3) + usage_with_options(git_update_ref_usage, options); + refname = argv[0]; + value = argv[1]; + oldval = argv[2]; + if (repo_get_oid(the_repository, value, &oid)) + die("%s: not a valid SHA1", value); + } + + if (oldval) { + if (!*oldval) + /* + * The empty string implies that the reference + * must not already exist: + */ + oidclr(&oldoid); + else if (repo_get_oid(the_repository, oldval, &oldoid)) + die("%s: not a valid old SHA1", oldval); + } + + if (delete) + /* + * For purposes of backwards compatibility, we treat + * NULL_SHA1 as "don't care" here: + */ + return delete_ref(msg, refname, + (oldval && !is_null_oid(&oldoid)) ? &oldoid : NULL, + default_flags); + else + return update_ref(msg, refname, &oid, oldval ? &oldoid : NULL, + default_flags | create_reflog_flag, + UPDATE_REFS_DIE_ON_ERR); +} diff --git a/builtin/update-server-info.c b/builtin/update-server-info.c new file mode 100644 index 0000000..1dc3971 --- /dev/null +++ b/builtin/update-server-info.c @@ -0,0 +1,27 @@ +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "parse-options.h" +#include "server-info.h" + +static const char * const update_server_info_usage[] = { + "git update-server-info [-f | --force]", + NULL +}; + +int cmd_update_server_info(int argc, const char **argv, const char *prefix) +{ + int force = 0; + struct option options[] = { + OPT__FORCE(&force, N_("update the info files from scratch"), 0), + OPT_END() + }; + + git_config(git_default_config, NULL); + argc = parse_options(argc, argv, prefix, options, + update_server_info_usage, 0); + if (argc > 0) + usage_with_options(update_server_info_usage, options); + + return !!update_server_info(force); +} diff --git a/builtin/upload-archive.c b/builtin/upload-archive.c new file mode 100644 index 0000000..1b09e5e --- /dev/null +++ b/builtin/upload-archive.c @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2006 Franck Bui-Huu + */ +#include "builtin.h" +#include "archive.h" +#include "path.h" +#include "pkt-line.h" +#include "sideband.h" +#include "repository.h" +#include "run-command.h" +#include "strvec.h" + +static const char upload_archive_usage[] = + "git upload-archive <repository>"; + +static const char deadchild[] = +"git upload-archive: archiver died with error"; + +#define MAX_ARGS (64) + +int cmd_upload_archive_writer(int argc, const char **argv, const char *prefix) +{ + struct strvec sent_argv = STRVEC_INIT; + const char *arg_cmd = "argument "; + + if (argc != 2 || !strcmp(argv[1], "-h")) + usage(upload_archive_usage); + + if (!enter_repo(argv[1], 0)) + die("'%s' does not appear to be a git repository", argv[1]); + + init_archivers(); + + /* put received options in sent_argv[] */ + strvec_push(&sent_argv, "git-upload-archive"); + for (;;) { + char *buf = packet_read_line(0, NULL); + if (!buf) + break; /* got a flush */ + if (sent_argv.nr > MAX_ARGS) + die("Too many options (>%d)", MAX_ARGS - 1); + + if (!starts_with(buf, arg_cmd)) + die("'argument' token or flush expected"); + strvec_push(&sent_argv, buf + strlen(arg_cmd)); + } + + /* parse all options sent by the client */ + return write_archive(sent_argv.nr, sent_argv.v, prefix, + the_repository, NULL, 1); +} + +__attribute__((format (printf, 1, 2))) +static void error_clnt(const char *fmt, ...) +{ + struct strbuf buf = STRBUF_INIT; + va_list params; + + va_start(params, fmt); + strbuf_vaddf(&buf, fmt, params); + va_end(params); + send_sideband(1, 3, buf.buf, buf.len, LARGE_PACKET_MAX); + die("sent error to the client: %s", buf.buf); +} + +static ssize_t process_input(int child_fd, int band) +{ + char buf[16384]; + ssize_t sz = read(child_fd, buf, sizeof(buf)); + if (sz < 0) { + if (errno != EAGAIN && errno != EINTR) + error_clnt("read error: %s\n", strerror(errno)); + return sz; + } + send_sideband(1, band, buf, sz, LARGE_PACKET_MAX); + return sz; +} + +int cmd_upload_archive(int argc, const char **argv, const char *prefix) +{ + struct child_process writer = CHILD_PROCESS_INIT; + + BUG_ON_NON_EMPTY_PREFIX(prefix); + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage(upload_archive_usage); + + /* + * Set up sideband subprocess. + * + * We (parent) monitor and read from child, sending its fd#1 and fd#2 + * multiplexed out to our fd#1. If the child dies, we tell the other + * end over channel #3. + */ + writer.out = writer.err = -1; + writer.git_cmd = 1; + strvec_push(&writer.args, "upload-archive--writer"); + strvec_pushv(&writer.args, argv + 1); + if (start_command(&writer)) { + int err = errno; + packet_write_fmt(1, "NACK unable to spawn subprocess\n"); + die("upload-archive: %s", strerror(err)); + } + + packet_write_fmt(1, "ACK\n"); + packet_flush(1); + + while (1) { + struct pollfd pfd[2]; + + pfd[0].fd = writer.out; + pfd[0].events = POLLIN; + pfd[1].fd = writer.err; + pfd[1].events = POLLIN; + if (poll(pfd, 2, -1) < 0) { + if (errno != EINTR) { + error_errno("poll failed resuming"); + sleep(1); + } + continue; + } + if (pfd[1].revents & POLLIN) + /* Status stream ready */ + if (process_input(pfd[1].fd, 2)) + continue; + if (pfd[0].revents & POLLIN) + /* Data stream ready */ + if (process_input(pfd[0].fd, 1)) + continue; + + if (finish_command(&writer)) + error_clnt("%s", deadchild); + packet_flush(1); + break; + } + return 0; +} diff --git a/builtin/upload-pack.c b/builtin/upload-pack.c new file mode 100644 index 0000000..9b021ef --- /dev/null +++ b/builtin/upload-pack.c @@ -0,0 +1,77 @@ +#include "builtin.h" +#include "exec-cmd.h" +#include "gettext.h" +#include "pkt-line.h" +#include "parse-options.h" +#include "path.h" +#include "protocol.h" +#include "replace-object.h" +#include "upload-pack.h" +#include "serve.h" + +static const char * const upload_pack_usage[] = { + N_("git-upload-pack [--[no-]strict] [--timeout=<n>] [--stateless-rpc]\n" + " [--advertise-refs] <directory>"), + NULL +}; + +int cmd_upload_pack(int argc, const char **argv, const char *prefix) +{ + const char *dir; + int strict = 0; + int advertise_refs = 0; + int stateless_rpc = 0; + int timeout = 0; + struct option options[] = { + OPT_BOOL(0, "stateless-rpc", &stateless_rpc, + N_("quit after a single request/response exchange")), + OPT_HIDDEN_BOOL(0, "http-backend-info-refs", &advertise_refs, + N_("serve up the info/refs for git-http-backend")), + OPT_ALIAS(0, "advertise-refs", "http-backend-info-refs"), + OPT_BOOL(0, "strict", &strict, + N_("do not try <directory>/.git/ if <directory> is no Git directory")), + OPT_INTEGER(0, "timeout", &timeout, + N_("interrupt transfer after <n> seconds of inactivity")), + OPT_END() + }; + + packet_trace_identity("upload-pack"); + disable_replace_refs(); + + argc = parse_options(argc, argv, prefix, options, upload_pack_usage, 0); + + if (argc != 1) + usage_with_options(upload_pack_usage, options); + + setup_path(); + + dir = argv[0]; + + if (!enter_repo(dir, strict)) + die("'%s' does not appear to be a git repository", dir); + + switch (determine_protocol_version_server()) { + case protocol_v2: + if (advertise_refs) + protocol_v2_advertise_capabilities(); + else + protocol_v2_serve_loop(stateless_rpc); + break; + case protocol_v1: + /* + * v1 is just the original protocol with a version string, + * so just fall through after writing the version string. + */ + if (advertise_refs || !stateless_rpc) + packet_write_fmt(1, "version 1\n"); + + /* fallthrough */ + case protocol_v0: + upload_pack(advertise_refs, stateless_rpc, timeout); + break; + case protocol_unknown_version: + BUG("unknown protocol version"); + } + + return 0; +} diff --git a/builtin/var.c b/builtin/var.c new file mode 100644 index 0000000..8cf7dd9 --- /dev/null +++ b/builtin/var.c @@ -0,0 +1,239 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Eric Biederman, 2005 + */ +#include "builtin.h" +#include "attr.h" +#include "config.h" +#include "editor.h" +#include "ident.h" +#include "pager.h" +#include "refs.h" +#include "path.h" +#include "strbuf.h" + +static const char var_usage[] = "git var (-l | <variable>)"; + +static char *committer(int ident_flag) +{ + return xstrdup_or_null(git_committer_info(ident_flag)); +} + +static char *author(int ident_flag) +{ + return xstrdup_or_null(git_author_info(ident_flag)); +} + +static char *editor(int ident_flag UNUSED) +{ + return xstrdup_or_null(git_editor()); +} + +static char *sequence_editor(int ident_flag UNUSED) +{ + return xstrdup_or_null(git_sequence_editor()); +} + +static char *pager(int ident_flag UNUSED) +{ + const char *pgm = git_pager(1); + + if (!pgm) + pgm = "cat"; + return xstrdup(pgm); +} + +static char *default_branch(int ident_flag UNUSED) +{ + return xstrdup_or_null(git_default_branch_name(1)); +} + +static char *shell_path(int ident_flag UNUSED) +{ + return xstrdup(SHELL_PATH); +} + +static char *git_attr_val_system(int ident_flag UNUSED) +{ + if (git_attr_system_is_enabled()) { + char *file = xstrdup(git_attr_system_file()); + normalize_path_copy(file, file); + return file; + } + return NULL; +} + +static char *git_attr_val_global(int ident_flag UNUSED) +{ + char *file = xstrdup_or_null(git_attr_global_file()); + if (file) { + normalize_path_copy(file, file); + return file; + } + return NULL; +} + +static char *git_config_val_system(int ident_flag UNUSED) +{ + if (git_config_system()) { + char *file = git_system_config(); + normalize_path_copy(file, file); + return file; + } + return NULL; +} + +static char *git_config_val_global(int ident_flag UNUSED) +{ + struct strbuf buf = STRBUF_INIT; + char *user, *xdg; + size_t unused; + + git_global_config(&user, &xdg); + if (xdg && *xdg) { + normalize_path_copy(xdg, xdg); + strbuf_addf(&buf, "%s\n", xdg); + } + if (user && *user) { + normalize_path_copy(user, user); + strbuf_addf(&buf, "%s\n", user); + } + free(xdg); + free(user); + strbuf_trim_trailing_newline(&buf); + if (buf.len == 0) { + strbuf_release(&buf); + return NULL; + } + return strbuf_detach(&buf, &unused); +} + +struct git_var { + const char *name; + char *(*read)(int); + int multivalued; +}; +static struct git_var git_vars[] = { + { + .name = "GIT_COMMITTER_IDENT", + .read = committer, + }, + { + .name = "GIT_AUTHOR_IDENT", + .read = author, + }, + { + .name = "GIT_EDITOR", + .read = editor, + }, + { + .name = "GIT_SEQUENCE_EDITOR", + .read = sequence_editor, + }, + { + .name = "GIT_PAGER", + .read = pager, + }, + { + .name = "GIT_DEFAULT_BRANCH", + .read = default_branch, + }, + { + .name = "GIT_SHELL_PATH", + .read = shell_path, + }, + { + .name = "GIT_ATTR_SYSTEM", + .read = git_attr_val_system, + }, + { + .name = "GIT_ATTR_GLOBAL", + .read = git_attr_val_global, + }, + { + .name = "GIT_CONFIG_SYSTEM", + .read = git_config_val_system, + }, + { + .name = "GIT_CONFIG_GLOBAL", + .read = git_config_val_global, + .multivalued = 1, + }, + { + .name = "", + .read = NULL, + }, +}; + +static void list_vars(void) +{ + struct git_var *ptr; + char *val; + + for (ptr = git_vars; ptr->read; ptr++) + if ((val = ptr->read(0))) { + if (ptr->multivalued && *val) { + struct string_list list = STRING_LIST_INIT_DUP; + int i; + + string_list_split(&list, val, '\n', -1); + for (i = 0; i < list.nr; i++) + printf("%s=%s\n", ptr->name, list.items[i].string); + string_list_clear(&list, 0); + } else { + printf("%s=%s\n", ptr->name, val); + } + free(val); + } +} + +static const struct git_var *get_git_var(const char *var) +{ + struct git_var *ptr; + for (ptr = git_vars; ptr->read; ptr++) { + if (strcmp(var, ptr->name) == 0) { + return ptr; + } + } + return NULL; +} + +static int show_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + if (value) + printf("%s=%s\n", var, value); + else + printf("%s\n", var); + return git_default_config(var, value, ctx, cb); +} + +int cmd_var(int argc, const char **argv, const char *prefix UNUSED) +{ + const struct git_var *git_var; + char *val; + + if (argc != 2) + usage(var_usage); + + if (strcmp(argv[1], "-l") == 0) { + git_config(show_config, NULL); + list_vars(); + return 0; + } + git_config(git_default_config, NULL); + + git_var = get_git_var(argv[1]); + if (!git_var) + usage(var_usage); + + val = git_var->read(IDENT_STRICT); + if (!val) + return 1; + + printf("%s\n", val); + free(val); + + return 0; +} diff --git a/builtin/verify-commit.c b/builtin/verify-commit.c new file mode 100644 index 0000000..9680b58 --- /dev/null +++ b/builtin/verify-commit.c @@ -0,0 +1,83 @@ +/* + * Builtin "git commit-commit" + * + * Copyright (c) 2014 Michael J Gruber <git@drmicha.warpmail.net> + * + * Based on git-verify-tag + */ +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "object-name.h" +#include "object-store-ll.h" +#include "repository.h" +#include "commit.h" +#include "run-command.h" +#include "parse-options.h" +#include "gpg-interface.h" + +static const char * const verify_commit_usage[] = { + N_("git verify-commit [-v | --verbose] [--raw] <commit>..."), + NULL +}; + +static int run_gpg_verify(struct commit *commit, unsigned flags) +{ + struct signature_check signature_check; + int ret; + + memset(&signature_check, 0, sizeof(signature_check)); + + ret = check_commit_signature(commit, &signature_check); + print_signature_buffer(&signature_check, flags); + + signature_check_clear(&signature_check); + return ret; +} + +static int verify_commit(const char *name, unsigned flags) +{ + struct object_id oid; + struct object *obj; + + if (repo_get_oid(the_repository, name, &oid)) + return error("commit '%s' not found.", name); + + obj = parse_object(the_repository, &oid); + if (!obj) + return error("%s: unable to read file.", name); + if (obj->type != OBJ_COMMIT) + return error("%s: cannot verify a non-commit object of type %s.", + name, type_name(obj->type)); + + return run_gpg_verify((struct commit *)obj, flags); +} + +int cmd_verify_commit(int argc, const char **argv, const char *prefix) +{ + int i = 1, verbose = 0, had_error = 0; + unsigned flags = 0; + const struct option verify_commit_options[] = { + OPT__VERBOSE(&verbose, N_("print commit contents")), + OPT_BIT(0, "raw", &flags, N_("print raw gpg status output"), GPG_VERIFY_RAW), + OPT_END() + }; + + git_config(git_default_config, NULL); + + argc = parse_options(argc, argv, prefix, verify_commit_options, + verify_commit_usage, PARSE_OPT_KEEP_ARGV0); + if (argc <= i) + usage_with_options(verify_commit_usage, verify_commit_options); + + if (verbose) + flags |= GPG_VERIFY_VERBOSE; + + /* sometimes the program was terminated because this signal + * was received in the process of writing the gpg input: */ + signal(SIGPIPE, SIG_IGN); + while (i < argc) + if (verify_commit(argv[i++], flags)) + had_error = 1; + return had_error; +} diff --git a/builtin/verify-pack.c b/builtin/verify-pack.c new file mode 100644 index 0000000..011dddd --- /dev/null +++ b/builtin/verify-pack.c @@ -0,0 +1,91 @@ +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "run-command.h" +#include "parse-options.h" +#include "strbuf.h" + +#define VERIFY_PACK_VERBOSE 01 +#define VERIFY_PACK_STAT_ONLY 02 + +static int verify_one_pack(const char *path, unsigned int flags, const char *hash_algo) +{ + struct child_process index_pack = CHILD_PROCESS_INIT; + struct strvec *argv = &index_pack.args; + struct strbuf arg = STRBUF_INIT; + int verbose = flags & VERIFY_PACK_VERBOSE; + int stat_only = flags & VERIFY_PACK_STAT_ONLY; + int err; + + strvec_push(argv, "index-pack"); + + if (stat_only) + strvec_push(argv, "--verify-stat-only"); + else if (verbose) + strvec_push(argv, "--verify-stat"); + else + strvec_push(argv, "--verify"); + + if (hash_algo) + strvec_pushf(argv, "--object-format=%s", hash_algo); + + /* + * In addition to "foo.pack" we accept "foo.idx" and "foo"; + * normalize these forms to "foo.pack" for "index-pack --verify". + */ + strbuf_addstr(&arg, path); + if (strbuf_strip_suffix(&arg, ".idx") || + !ends_with(arg.buf, ".pack")) + strbuf_addstr(&arg, ".pack"); + strvec_push(argv, arg.buf); + + index_pack.git_cmd = 1; + + err = run_command(&index_pack); + + if (verbose || stat_only) { + if (err) + printf("%s: bad\n", arg.buf); + else { + if (!stat_only) + printf("%s: ok\n", arg.buf); + } + } + strbuf_release(&arg); + + return err; +} + +static const char * const verify_pack_usage[] = { + N_("git verify-pack [-v | --verbose] [-s | --stat-only] [--] <pack>.idx..."), + NULL +}; + +int cmd_verify_pack(int argc, const char **argv, const char *prefix) +{ + int err = 0; + unsigned int flags = 0; + const char *object_format = NULL; + int i; + const struct option verify_pack_options[] = { + OPT_BIT('v', "verbose", &flags, N_("verbose"), + VERIFY_PACK_VERBOSE), + OPT_BIT('s', "stat-only", &flags, N_("show statistics only"), + VERIFY_PACK_STAT_ONLY), + OPT_STRING(0, "object-format", &object_format, N_("hash"), + N_("specify the hash algorithm to use")), + OPT_END() + }; + + git_config(git_default_config, NULL); + argc = parse_options(argc, argv, prefix, verify_pack_options, + verify_pack_usage, 0); + if (argc < 1) + usage_with_options(verify_pack_usage, verify_pack_options); + for (i = 0; i < argc; i++) { + if (verify_one_pack(argv[i], flags, object_format)) + err = 1; + } + + return err; +} diff --git a/builtin/verify-tag.c b/builtin/verify-tag.c new file mode 100644 index 0000000..d875327 --- /dev/null +++ b/builtin/verify-tag.c @@ -0,0 +1,70 @@ +/* + * Builtin "git verify-tag" + * + * Copyright (c) 2007 Carlos Rica <jasampler@gmail.com> + * + * Based on git-verify-tag.sh + */ +#include "builtin.h" +#include "config.h" +#include "gettext.h" +#include "tag.h" +#include "run-command.h" +#include "object-name.h" +#include "parse-options.h" +#include "gpg-interface.h" +#include "ref-filter.h" + +static const char * const verify_tag_usage[] = { + N_("git verify-tag [-v | --verbose] [--format=<format>] [--raw] <tag>..."), + NULL +}; + +int cmd_verify_tag(int argc, const char **argv, const char *prefix) +{ + int i = 1, verbose = 0, had_error = 0; + unsigned flags = 0; + struct ref_format format = REF_FORMAT_INIT; + const struct option verify_tag_options[] = { + OPT__VERBOSE(&verbose, N_("print tag contents")), + OPT_BIT(0, "raw", &flags, N_("print raw gpg status output"), GPG_VERIFY_RAW), + OPT_STRING(0, "format", &format.format, N_("format"), N_("format to use for the output")), + OPT_END() + }; + + git_config(git_default_config, NULL); + + argc = parse_options(argc, argv, prefix, verify_tag_options, + verify_tag_usage, PARSE_OPT_KEEP_ARGV0); + if (argc <= i) + usage_with_options(verify_tag_usage, verify_tag_options); + + if (verbose) + flags |= GPG_VERIFY_VERBOSE; + + if (format.format) { + if (verify_ref_format(&format)) + usage_with_options(verify_tag_usage, + verify_tag_options); + flags |= GPG_VERIFY_OMIT_STATUS; + } + + while (i < argc) { + struct object_id oid; + const char *name = argv[i++]; + + if (repo_get_oid(the_repository, name, &oid)) { + had_error = !!error("tag '%s' not found.", name); + continue; + } + + if (gpg_verify_tag(&oid, name, flags)) { + had_error = 1; + continue; + } + + if (format.format) + pretty_print_ref(name, &oid, &format); + } + return had_error; +} diff --git a/builtin/worktree.c b/builtin/worktree.c new file mode 100644 index 0000000..62b7e26 --- /dev/null +++ b/builtin/worktree.c @@ -0,0 +1,1416 @@ +#include "builtin.h" +#include "abspath.h" +#include "advice.h" +#include "checkout.h" +#include "config.h" +#include "copy.h" +#include "dir.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "object-file.h" +#include "object-name.h" +#include "parse-options.h" +#include "path.h" +#include "strvec.h" +#include "branch.h" +#include "read-cache-ll.h" +#include "refs.h" +#include "remote.h" +#include "repository.h" +#include "run-command.h" +#include "hook.h" +#include "sigchain.h" +#include "submodule.h" +#include "utf8.h" +#include "worktree.h" +#include "quote.h" + +#define BUILTIN_WORKTREE_ADD_USAGE \ + N_("git worktree add [-f] [--detach] [--checkout] [--lock [--reason <string>]]\n" \ + " [--orphan] [(-b | -B) <new-branch>] <path> [<commit-ish>]") + +#define BUILTIN_WORKTREE_LIST_USAGE \ + N_("git worktree list [-v | --porcelain [-z]]") +#define BUILTIN_WORKTREE_LOCK_USAGE \ + N_("git worktree lock [--reason <string>] <worktree>") +#define BUILTIN_WORKTREE_MOVE_USAGE \ + N_("git worktree move <worktree> <new-path>") +#define BUILTIN_WORKTREE_PRUNE_USAGE \ + N_("git worktree prune [-n] [-v] [--expire <expire>]") +#define BUILTIN_WORKTREE_REMOVE_USAGE \ + N_("git worktree remove [-f] <worktree>") +#define BUILTIN_WORKTREE_REPAIR_USAGE \ + N_("git worktree repair [<path>...]") +#define BUILTIN_WORKTREE_UNLOCK_USAGE \ + N_("git worktree unlock <worktree>") + +#define WORKTREE_ADD_DWIM_ORPHAN_INFER_TEXT \ + _("No possible source branch, inferring '--orphan'") + +#define WORKTREE_ADD_ORPHAN_WITH_DASH_B_HINT_TEXT \ + _("If you meant to create a worktree containing a new orphan branch\n" \ + "(branch with no commits) for this repository, you can do so\n" \ + "using the --orphan flag:\n" \ + "\n" \ + " git worktree add --orphan -b %s %s\n") + +#define WORKTREE_ADD_ORPHAN_NO_DASH_B_HINT_TEXT \ + _("If you meant to create a worktree containing a new orphan branch\n" \ + "(branch with no commits) for this repository, you can do so\n" \ + "using the --orphan flag:\n" \ + "\n" \ + " git worktree add --orphan %s\n") + +static const char * const git_worktree_usage[] = { + BUILTIN_WORKTREE_ADD_USAGE, + BUILTIN_WORKTREE_LIST_USAGE, + BUILTIN_WORKTREE_LOCK_USAGE, + BUILTIN_WORKTREE_MOVE_USAGE, + BUILTIN_WORKTREE_PRUNE_USAGE, + BUILTIN_WORKTREE_REMOVE_USAGE, + BUILTIN_WORKTREE_REPAIR_USAGE, + BUILTIN_WORKTREE_UNLOCK_USAGE, + NULL +}; + +static const char * const git_worktree_add_usage[] = { + BUILTIN_WORKTREE_ADD_USAGE, + NULL, +}; + +static const char * const git_worktree_list_usage[] = { + BUILTIN_WORKTREE_LIST_USAGE, + NULL +}; + +static const char * const git_worktree_lock_usage[] = { + BUILTIN_WORKTREE_LOCK_USAGE, + NULL +}; + +static const char * const git_worktree_move_usage[] = { + BUILTIN_WORKTREE_MOVE_USAGE, + NULL +}; + +static const char * const git_worktree_prune_usage[] = { + BUILTIN_WORKTREE_PRUNE_USAGE, + NULL +}; + +static const char * const git_worktree_remove_usage[] = { + BUILTIN_WORKTREE_REMOVE_USAGE, + NULL +}; + +static const char * const git_worktree_repair_usage[] = { + BUILTIN_WORKTREE_REPAIR_USAGE, + NULL +}; + +static const char * const git_worktree_unlock_usage[] = { + BUILTIN_WORKTREE_UNLOCK_USAGE, + NULL +}; + +struct add_opts { + int force; + int detach; + int quiet; + int checkout; + int orphan; + const char *keep_locked; +}; + +static int show_only; +static int verbose; +static int guess_remote; +static timestamp_t expire; + +static int git_worktree_config(const char *var, const char *value, + const struct config_context *ctx, void *cb) +{ + if (!strcmp(var, "worktree.guessremote")) { + guess_remote = git_config_bool(var, value); + return 0; + } + + return git_default_config(var, value, ctx, cb); +} + +static int delete_git_dir(const char *id) +{ + struct strbuf sb = STRBUF_INIT; + int ret; + + strbuf_addstr(&sb, git_common_path("worktrees/%s", id)); + ret = remove_dir_recursively(&sb, 0); + if (ret < 0 && errno == ENOTDIR) + ret = unlink(sb.buf); + if (ret) + error_errno(_("failed to delete '%s'"), sb.buf); + strbuf_release(&sb); + return ret; +} + +static void delete_worktrees_dir_if_empty(void) +{ + rmdir(git_path("worktrees")); /* ignore failed removal */ +} + +static void prune_worktree(const char *id, const char *reason) +{ + if (show_only || verbose) + fprintf_ln(stderr, _("Removing %s/%s: %s"), "worktrees", id, reason); + if (!show_only) + delete_git_dir(id); +} + +static int prune_cmp(const void *a, const void *b) +{ + const struct string_list_item *x = a; + const struct string_list_item *y = b; + int c; + + if ((c = fspathcmp(x->string, y->string))) + return c; + /* + * paths same; prune_dupes() removes all but the first worktree entry + * having the same path, so sort main worktree ('util' is NULL) above + * linked worktrees ('util' not NULL) since main worktree can't be + * removed + */ + if (!x->util) + return -1; + if (!y->util) + return 1; + /* paths same; sort by .git/worktrees/<id> */ + return strcmp(x->util, y->util); +} + +static void prune_dups(struct string_list *l) +{ + int i; + + QSORT(l->items, l->nr, prune_cmp); + for (i = 1; i < l->nr; i++) { + if (!fspathcmp(l->items[i].string, l->items[i - 1].string)) + prune_worktree(l->items[i].util, "duplicate entry"); + } +} + +static void prune_worktrees(void) +{ + struct strbuf reason = STRBUF_INIT; + struct strbuf main_path = STRBUF_INIT; + struct string_list kept = STRING_LIST_INIT_DUP; + DIR *dir = opendir(git_path("worktrees")); + struct dirent *d; + if (!dir) + return; + while ((d = readdir_skip_dot_and_dotdot(dir)) != NULL) { + char *path; + strbuf_reset(&reason); + if (should_prune_worktree(d->d_name, &reason, &path, expire)) + prune_worktree(d->d_name, reason.buf); + else if (path) + string_list_append_nodup(&kept, path)->util = xstrdup(d->d_name); + } + closedir(dir); + + strbuf_add_absolute_path(&main_path, get_git_common_dir()); + /* massage main worktree absolute path to match 'gitdir' content */ + strbuf_strip_suffix(&main_path, "/."); + string_list_append_nodup(&kept, strbuf_detach(&main_path, NULL)); + prune_dups(&kept); + string_list_clear(&kept, 1); + + if (!show_only) + delete_worktrees_dir_if_empty(); + strbuf_release(&reason); +} + +static int prune(int ac, const char **av, const char *prefix) +{ + struct option options[] = { + OPT__DRY_RUN(&show_only, N_("do not remove, show only")), + OPT__VERBOSE(&verbose, N_("report pruned working trees")), + OPT_EXPIRY_DATE(0, "expire", &expire, + N_("expire working trees older than <time>")), + OPT_END() + }; + + expire = TIME_MAX; + ac = parse_options(ac, av, prefix, options, git_worktree_prune_usage, + 0); + if (ac) + usage_with_options(git_worktree_prune_usage, options); + prune_worktrees(); + return 0; +} + +static char *junk_work_tree; +static char *junk_git_dir; +static int is_junk; +static pid_t junk_pid; + +static void remove_junk(void) +{ + struct strbuf sb = STRBUF_INIT; + if (!is_junk || getpid() != junk_pid) + return; + if (junk_git_dir) { + strbuf_addstr(&sb, junk_git_dir); + remove_dir_recursively(&sb, 0); + strbuf_reset(&sb); + } + if (junk_work_tree) { + strbuf_addstr(&sb, junk_work_tree); + remove_dir_recursively(&sb, 0); + } + strbuf_release(&sb); +} + +static void remove_junk_on_signal(int signo) +{ + remove_junk(); + sigchain_pop(signo); + raise(signo); +} + +static const char *worktree_basename(const char *path, int *olen) +{ + const char *name; + int len; + + len = strlen(path); + while (len && is_dir_sep(path[len - 1])) + len--; + + for (name = path + len - 1; name > path; name--) + if (is_dir_sep(*name)) { + name++; + break; + } + + *olen = len; + return name; +} + +/* check that path is viable location for worktree */ +static void check_candidate_path(const char *path, + int force, + struct worktree **worktrees, + const char *cmd) +{ + struct worktree *wt; + int locked; + + if (file_exists(path) && !is_empty_dir(path)) + die(_("'%s' already exists"), path); + + wt = find_worktree_by_path(worktrees, path); + if (!wt) + return; + + locked = !!worktree_lock_reason(wt); + if ((!locked && force) || (locked && force > 1)) { + if (delete_git_dir(wt->id)) + die(_("unusable worktree destination '%s'"), path); + return; + } + + if (locked) + die(_("'%s' is a missing but locked worktree;\nuse '%s -f -f' to override, or 'unlock' and 'prune' or 'remove' to clear"), path, cmd); + else + die(_("'%s' is a missing but already registered worktree;\nuse '%s -f' to override, or 'prune' or 'remove' to clear"), path, cmd); +} + +static void copy_sparse_checkout(const char *worktree_git_dir) +{ + char *from_file = git_pathdup("info/sparse-checkout"); + char *to_file = xstrfmt("%s/info/sparse-checkout", worktree_git_dir); + + if (file_exists(from_file)) { + if (safe_create_leading_directories(to_file) || + copy_file(to_file, from_file, 0666)) + error(_("failed to copy '%s' to '%s'; sparse-checkout may not work correctly"), + from_file, to_file); + } + + free(from_file); + free(to_file); +} + +static void copy_filtered_worktree_config(const char *worktree_git_dir) +{ + char *from_file = git_pathdup("config.worktree"); + char *to_file = xstrfmt("%s/config.worktree", worktree_git_dir); + + if (file_exists(from_file)) { + struct config_set cs = { { 0 } }; + int bare; + + if (safe_create_leading_directories(to_file) || + copy_file(to_file, from_file, 0666)) { + error(_("failed to copy worktree config from '%s' to '%s'"), + from_file, to_file); + goto worktree_copy_cleanup; + } + + git_configset_init(&cs); + git_configset_add_file(&cs, from_file); + + if (!git_configset_get_bool(&cs, "core.bare", &bare) && + bare && + git_config_set_multivar_in_file_gently( + to_file, "core.bare", NULL, "true", 0)) + error(_("failed to unset '%s' in '%s'"), + "core.bare", to_file); + if (!git_configset_get(&cs, "core.worktree") && + git_config_set_in_file_gently(to_file, + "core.worktree", NULL)) + error(_("failed to unset '%s' in '%s'"), + "core.worktree", to_file); + + git_configset_clear(&cs); + } + +worktree_copy_cleanup: + free(from_file); + free(to_file); +} + +static int checkout_worktree(const struct add_opts *opts, + struct strvec *child_env) +{ + struct child_process cp = CHILD_PROCESS_INIT; + cp.git_cmd = 1; + strvec_pushl(&cp.args, "reset", "--hard", "--no-recurse-submodules", NULL); + if (opts->quiet) + strvec_push(&cp.args, "--quiet"); + strvec_pushv(&cp.env, child_env->v); + return run_command(&cp); +} + +static int make_worktree_orphan(const char * ref, const struct add_opts *opts, + struct strvec *child_env) +{ + struct strbuf symref = STRBUF_INIT; + struct child_process cp = CHILD_PROCESS_INIT; + + validate_new_branchname(ref, &symref, 0); + strvec_pushl(&cp.args, "symbolic-ref", "HEAD", symref.buf, NULL); + if (opts->quiet) + strvec_push(&cp.args, "--quiet"); + strvec_pushv(&cp.env, child_env->v); + strbuf_release(&symref); + cp.git_cmd = 1; + return run_command(&cp); +} + +static int add_worktree(const char *path, const char *refname, + const struct add_opts *opts) +{ + struct strbuf sb_git = STRBUF_INIT, sb_repo = STRBUF_INIT; + struct strbuf sb = STRBUF_INIT, realpath = STRBUF_INIT; + const char *name; + struct child_process cp = CHILD_PROCESS_INIT; + struct strvec child_env = STRVEC_INIT; + unsigned int counter = 0; + int len, ret; + struct strbuf symref = STRBUF_INIT; + struct commit *commit = NULL; + int is_branch = 0; + struct strbuf sb_name = STRBUF_INIT; + struct worktree **worktrees; + + worktrees = get_worktrees(); + check_candidate_path(path, opts->force, worktrees, "add"); + free_worktrees(worktrees); + worktrees = NULL; + + /* is 'refname' a branch or commit? */ + if (!opts->detach && !strbuf_check_branch_ref(&symref, refname) && + ref_exists(symref.buf)) { + is_branch = 1; + if (!opts->force) + die_if_checked_out(symref.buf, 0); + } + commit = lookup_commit_reference_by_name(refname); + if (!commit && !opts->orphan) + die(_("invalid reference: %s"), refname); + + name = worktree_basename(path, &len); + strbuf_add(&sb, name, path + len - name); + sanitize_refname_component(sb.buf, &sb_name); + if (!sb_name.len) + BUG("How come '%s' becomes empty after sanitization?", sb.buf); + strbuf_reset(&sb); + name = sb_name.buf; + git_path_buf(&sb_repo, "worktrees/%s", name); + len = sb_repo.len; + if (safe_create_leading_directories_const(sb_repo.buf)) + die_errno(_("could not create leading directories of '%s'"), + sb_repo.buf); + + while (mkdir(sb_repo.buf, 0777)) { + counter++; + if ((errno != EEXIST) || !counter /* overflow */) + die_errno(_("could not create directory of '%s'"), + sb_repo.buf); + strbuf_setlen(&sb_repo, len); + strbuf_addf(&sb_repo, "%d", counter); + } + name = strrchr(sb_repo.buf, '/') + 1; + + junk_pid = getpid(); + atexit(remove_junk); + sigchain_push_common(remove_junk_on_signal); + + junk_git_dir = xstrdup(sb_repo.buf); + is_junk = 1; + + /* + * lock the incomplete repo so prune won't delete it, unlock + * after the preparation is over. + */ + strbuf_addf(&sb, "%s/locked", sb_repo.buf); + if (opts->keep_locked) + write_file(sb.buf, "%s", opts->keep_locked); + else + write_file(sb.buf, _("initializing")); + + strbuf_addf(&sb_git, "%s/.git", path); + if (safe_create_leading_directories_const(sb_git.buf)) + die_errno(_("could not create leading directories of '%s'"), + sb_git.buf); + junk_work_tree = xstrdup(path); + + strbuf_reset(&sb); + strbuf_addf(&sb, "%s/gitdir", sb_repo.buf); + strbuf_realpath(&realpath, sb_git.buf, 1); + write_file(sb.buf, "%s", realpath.buf); + strbuf_realpath(&realpath, get_git_common_dir(), 1); + write_file(sb_git.buf, "gitdir: %s/worktrees/%s", + realpath.buf, name); + /* + * This is to keep resolve_ref() happy. We need a valid HEAD + * or is_git_directory() will reject the directory. Any value which + * looks like an object ID will do since it will be immediately + * replaced by the symbolic-ref or update-ref invocation in the new + * worktree. + */ + strbuf_reset(&sb); + strbuf_addf(&sb, "%s/HEAD", sb_repo.buf); + write_file(sb.buf, "%s", oid_to_hex(null_oid())); + strbuf_reset(&sb); + strbuf_addf(&sb, "%s/commondir", sb_repo.buf); + write_file(sb.buf, "../.."); + + /* + * If the current worktree has sparse-checkout enabled, then copy + * the sparse-checkout patterns from the current worktree. + */ + if (core_apply_sparse_checkout) + copy_sparse_checkout(sb_repo.buf); + + /* + * If we are using worktree config, then copy all current config + * values from the current worktree into the new one, that way the + * new worktree behaves the same as this one. + */ + if (the_repository->repository_format_worktree_config) + copy_filtered_worktree_config(sb_repo.buf); + + strvec_pushf(&child_env, "%s=%s", GIT_DIR_ENVIRONMENT, sb_git.buf); + strvec_pushf(&child_env, "%s=%s", GIT_WORK_TREE_ENVIRONMENT, path); + cp.git_cmd = 1; + + if (!is_branch && commit) { + strvec_pushl(&cp.args, "update-ref", "HEAD", + oid_to_hex(&commit->object.oid), NULL); + } else { + strvec_pushl(&cp.args, "symbolic-ref", "HEAD", + symref.buf, NULL); + if (opts->quiet) + strvec_push(&cp.args, "--quiet"); + } + + strvec_pushv(&cp.env, child_env.v); + ret = run_command(&cp); + if (ret) + goto done; + + if (opts->orphan && + (ret = make_worktree_orphan(refname, opts, &child_env))) + goto done; + + if (opts->checkout && + (ret = checkout_worktree(opts, &child_env))) + goto done; + + is_junk = 0; + FREE_AND_NULL(junk_work_tree); + FREE_AND_NULL(junk_git_dir); + +done: + if (ret || !opts->keep_locked) { + strbuf_reset(&sb); + strbuf_addf(&sb, "%s/locked", sb_repo.buf); + unlink_or_warn(sb.buf); + } + + /* + * Hook failure does not warrant worktree deletion, so run hook after + * is_junk is cleared, but do return appropriate code when hook fails. + */ + if (!ret && opts->checkout && !opts->orphan) { + struct run_hooks_opt opt = RUN_HOOKS_OPT_INIT; + + strvec_pushl(&opt.env, "GIT_DIR", "GIT_WORK_TREE", NULL); + strvec_pushl(&opt.args, + oid_to_hex(null_oid()), + oid_to_hex(&commit->object.oid), + "1", + NULL); + opt.dir = path; + + ret = run_hooks_opt("post-checkout", &opt); + } + + strvec_clear(&child_env); + strbuf_release(&sb); + strbuf_release(&symref); + strbuf_release(&sb_repo); + strbuf_release(&sb_git); + strbuf_release(&sb_name); + strbuf_release(&realpath); + return ret; +} + +static void print_preparing_worktree_line(int detach, + const char *branch, + const char *new_branch, + int force_new_branch) +{ + if (force_new_branch) { + struct commit *commit = lookup_commit_reference_by_name(new_branch); + if (!commit) + fprintf_ln(stderr, _("Preparing worktree (new branch '%s')"), new_branch); + else + fprintf_ln(stderr, _("Preparing worktree (resetting branch '%s'; was at %s)"), + new_branch, + repo_find_unique_abbrev(the_repository, &commit->object.oid, DEFAULT_ABBREV)); + } else if (new_branch) { + fprintf_ln(stderr, _("Preparing worktree (new branch '%s')"), new_branch); + } else { + struct strbuf s = STRBUF_INIT; + if (!detach && !strbuf_check_branch_ref(&s, branch) && + ref_exists(s.buf)) + fprintf_ln(stderr, _("Preparing worktree (checking out '%s')"), + branch); + else { + struct commit *commit = lookup_commit_reference_by_name(branch); + if (!commit) + BUG(_("unreachable: invalid reference: %s"), branch); + fprintf_ln(stderr, _("Preparing worktree (detached HEAD %s)"), + repo_find_unique_abbrev(the_repository, &commit->object.oid, DEFAULT_ABBREV)); + } + strbuf_release(&s); + } +} + +/** + * Callback to short circuit iteration over refs on the first reference + * corresponding to a valid oid. + * + * Returns 0 on failure and non-zero on success. + */ +static int first_valid_ref(const char *refname UNUSED, + const struct object_id *oid UNUSED, + int flags UNUSED, + void *cb_data UNUSED) +{ + return 1; +} + +/** + * Verifies HEAD and determines whether there exist any valid local references. + * + * - Checks whether HEAD points to a valid reference. + * + * - Checks whether any valid local branches exist. + * + * - Emits a warning if there exist any valid branches but HEAD does not point + * to a valid reference. + * + * Returns 1 if any of the previous checks are true, otherwise returns 0. + */ +static int can_use_local_refs(const struct add_opts *opts) +{ + if (head_ref(first_valid_ref, NULL)) { + return 1; + } else if (for_each_branch_ref(first_valid_ref, NULL)) { + if (!opts->quiet) { + struct strbuf path = STRBUF_INIT; + struct strbuf contents = STRBUF_INIT; + + strbuf_add_real_path(&path, get_worktree_git_dir(NULL)); + strbuf_addstr(&path, "/HEAD"); + strbuf_read_file(&contents, path.buf, 64); + strbuf_stripspace(&contents, 0); + strbuf_strip_suffix(&contents, "\n"); + + warning(_("HEAD points to an invalid (or orphaned) reference.\n" + "HEAD path: '%s'\n" + "HEAD contents: '%s'"), + path.buf, contents.buf); + strbuf_release(&path); + strbuf_release(&contents); + } + return 1; + } + return 0; +} + +/** + * Reports whether the necessary flags were set and whether the repository has + * remote references to attempt DWIM tracking of upstream branches. + * + * 1. Checks that `--guess-remote` was used or `worktree.guessRemote = true`. + * + * 2. Checks whether any valid remote branches exist. + * + * 3. Checks that there exists at least one remote and emits a warning/error + * if both checks 1. and 2. are false (can be bypassed with `--force`). + * + * Returns 1 if checks 1. and 2. are true, otherwise 0. + */ +static int can_use_remote_refs(const struct add_opts *opts) +{ + if (!guess_remote) { + return 0; + } else if (for_each_remote_ref(first_valid_ref, NULL)) { + return 1; + } else if (!opts->force && remote_get(NULL)) { + die(_("No local or remote refs exist despite at least one remote\n" + "present, stopping; use 'add -f' to override or fetch a remote first")); + } + return 0; +} + +/** + * Determines whether `--orphan` should be inferred in the evaluation of + * `worktree add path/` or `worktree add -b branch path/` and emits an error + * if the supplied arguments would produce an illegal combination when the + * `--orphan` flag is included. + * + * `opts` and `opt_track` contain the other options & flags supplied to the + * command. + * + * remote determines whether to check `can_use_remote_refs()` or not. This + * is primarily to differentiate between the basic `add` DWIM and `add -b`. + * + * Returns 1 when inferring `--orphan`, 0 otherwise, and emits an error when + * `--orphan` is inferred but doing so produces an illegal combination of + * options and flags. Additionally produces an error when remote refs are + * checked and the repo is in a state that looks like the user added a remote + * but forgot to fetch (and did not override the warning with -f). + */ +static int dwim_orphan(const struct add_opts *opts, int opt_track, int remote) +{ + if (can_use_local_refs(opts)) { + return 0; + } else if (remote && can_use_remote_refs(opts)) { + return 0; + } else if (!opts->quiet) { + fprintf_ln(stderr, WORKTREE_ADD_DWIM_ORPHAN_INFER_TEXT); + } + + if (opt_track) { + die(_("'%s' and '%s' cannot be used together"), "--orphan", + "--track"); + } else if (!opts->checkout) { + die(_("'%s' and '%s' cannot be used together"), "--orphan", + "--no-checkout"); + } + return 1; +} + +static const char *dwim_branch(const char *path, const char **new_branch) +{ + int n; + int branch_exists; + const char *s = worktree_basename(path, &n); + const char *branchname = xstrndup(s, n); + struct strbuf ref = STRBUF_INIT; + + UNLEAK(branchname); + + branch_exists = !strbuf_check_branch_ref(&ref, branchname) && + ref_exists(ref.buf); + strbuf_release(&ref); + if (branch_exists) + return branchname; + + *new_branch = branchname; + if (guess_remote) { + struct object_id oid; + const char *remote = + unique_tracking_name(*new_branch, &oid, NULL); + return remote; + } + return NULL; +} + +static int add(int ac, const char **av, const char *prefix) +{ + struct add_opts opts; + const char *new_branch_force = NULL; + char *path; + const char *branch; + const char *new_branch = NULL; + const char *opt_track = NULL; + const char *lock_reason = NULL; + int keep_locked = 0; + int used_new_branch_options; + struct option options[] = { + OPT__FORCE(&opts.force, + N_("checkout <branch> even if already checked out in other worktree"), + PARSE_OPT_NOCOMPLETE), + OPT_STRING('b', NULL, &new_branch, N_("branch"), + N_("create a new branch")), + OPT_STRING('B', NULL, &new_branch_force, N_("branch"), + N_("create or reset a branch")), + OPT_BOOL(0, "orphan", &opts.orphan, N_("create unborn/orphaned branch")), + OPT_BOOL('d', "detach", &opts.detach, N_("detach HEAD at named commit")), + OPT_BOOL(0, "checkout", &opts.checkout, N_("populate the new working tree")), + OPT_BOOL(0, "lock", &keep_locked, N_("keep the new working tree locked")), + OPT_STRING(0, "reason", &lock_reason, N_("string"), + N_("reason for locking")), + OPT__QUIET(&opts.quiet, N_("suppress progress reporting")), + OPT_PASSTHRU(0, "track", &opt_track, NULL, + N_("set up tracking mode (see git-branch(1))"), + PARSE_OPT_NOARG | PARSE_OPT_OPTARG), + OPT_BOOL(0, "guess-remote", &guess_remote, + N_("try to match the new branch name with a remote-tracking branch")), + OPT_END() + }; + int ret; + + memset(&opts, 0, sizeof(opts)); + opts.checkout = 1; + ac = parse_options(ac, av, prefix, options, git_worktree_add_usage, 0); + if (!!opts.detach + !!new_branch + !!new_branch_force > 1) + die(_("options '%s', '%s', and '%s' cannot be used together"), "-b", "-B", "--detach"); + if (opts.detach && opts.orphan) + die(_("options '%s', and '%s' cannot be used together"), + "--orphan", "--detach"); + if (opts.orphan && opt_track) + die(_("'%s' and '%s' cannot be used together"), "--orphan", "--track"); + if (opts.orphan && !opts.checkout) + die(_("'%s' and '%s' cannot be used together"), "--orphan", + "--no-checkout"); + if (opts.orphan && ac == 2) + die(_("'%s' and '%s' cannot be used together"), "--orphan", + _("<commit-ish>")); + if (lock_reason && !keep_locked) + die(_("the option '%s' requires '%s'"), "--reason", "--lock"); + if (lock_reason) + opts.keep_locked = lock_reason; + else if (keep_locked) + opts.keep_locked = _("added with --lock"); + + if (ac < 1 || ac > 2) + usage_with_options(git_worktree_add_usage, options); + + path = prefix_filename(prefix, av[0]); + branch = ac < 2 ? "HEAD" : av[1]; + used_new_branch_options = new_branch || new_branch_force; + + if (!strcmp(branch, "-")) + branch = "@{-1}"; + + if (new_branch_force) { + struct strbuf symref = STRBUF_INIT; + + new_branch = new_branch_force; + + if (!opts.force && + !strbuf_check_branch_ref(&symref, new_branch) && + ref_exists(symref.buf)) + die_if_checked_out(symref.buf, 0); + strbuf_release(&symref); + } + + if (opts.orphan && !new_branch) { + int n; + const char *s = worktree_basename(path, &n); + new_branch = xstrndup(s, n); + } else if (opts.orphan) { + // No-op + } else if (opts.detach) { + // Check HEAD + if (!strcmp(branch, "HEAD")) + can_use_local_refs(&opts); + } else if (ac < 2 && new_branch) { + // DWIM: Infer --orphan when repo has no refs. + opts.orphan = dwim_orphan(&opts, !!opt_track, 0); + } else if (ac < 2) { + // DWIM: Guess branch name from path. + const char *s = dwim_branch(path, &new_branch); + if (s) + branch = s; + + // DWIM: Infer --orphan when repo has no refs. + opts.orphan = (!s) && dwim_orphan(&opts, !!opt_track, 1); + } else if (ac == 2) { + struct object_id oid; + struct commit *commit; + const char *remote; + + commit = lookup_commit_reference_by_name(branch); + if (!commit) { + remote = unique_tracking_name(branch, &oid, NULL); + if (remote) { + new_branch = branch; + branch = remote; + } + } + + if (!strcmp(branch, "HEAD")) + can_use_local_refs(&opts); + + } + + if (!opts.orphan && !lookup_commit_reference_by_name(branch)) { + int attempt_hint = !opts.quiet && (ac < 2); + if (attempt_hint && used_new_branch_options) { + advise_if_enabled(ADVICE_WORKTREE_ADD_ORPHAN, + WORKTREE_ADD_ORPHAN_WITH_DASH_B_HINT_TEXT, + new_branch, path); + } else if (attempt_hint) { + advise_if_enabled(ADVICE_WORKTREE_ADD_ORPHAN, + WORKTREE_ADD_ORPHAN_NO_DASH_B_HINT_TEXT, path); + } + die(_("invalid reference: %s"), branch); + } + + if (!opts.quiet) + print_preparing_worktree_line(opts.detach, branch, new_branch, !!new_branch_force); + + if (opts.orphan) { + branch = new_branch; + } else if (new_branch) { + struct child_process cp = CHILD_PROCESS_INIT; + cp.git_cmd = 1; + strvec_push(&cp.args, "branch"); + if (new_branch_force) + strvec_push(&cp.args, "--force"); + if (opts.quiet) + strvec_push(&cp.args, "--quiet"); + strvec_push(&cp.args, new_branch); + strvec_push(&cp.args, branch); + if (opt_track) + strvec_push(&cp.args, opt_track); + if (run_command(&cp)) + return -1; + branch = new_branch; + } else if (opt_track) { + die(_("--[no-]track can only be used if a new branch is created")); + } + + ret = add_worktree(path, branch, &opts); + free(path); + return ret; +} + +static void show_worktree_porcelain(struct worktree *wt, int line_terminator) +{ + const char *reason; + + printf("worktree %s%c", wt->path, line_terminator); + if (wt->is_bare) + printf("bare%c", line_terminator); + else { + printf("HEAD %s%c", oid_to_hex(&wt->head_oid), line_terminator); + if (wt->is_detached) + printf("detached%c", line_terminator); + else if (wt->head_ref) + printf("branch %s%c", wt->head_ref, line_terminator); + } + + reason = worktree_lock_reason(wt); + if (reason) { + fputs("locked", stdout); + if (*reason) { + fputc(' ', stdout); + write_name_quoted(reason, stdout, line_terminator); + } else { + fputc(line_terminator, stdout); + } + } + + reason = worktree_prune_reason(wt, expire); + if (reason) + printf("prunable %s%c", reason, line_terminator); + + fputc(line_terminator, stdout); +} + +static void show_worktree(struct worktree *wt, int path_maxlen, int abbrev_len) +{ + struct strbuf sb = STRBUF_INIT; + int cur_path_len = strlen(wt->path); + int path_adj = cur_path_len - utf8_strwidth(wt->path); + const char *reason; + + strbuf_addf(&sb, "%-*s ", 1 + path_maxlen + path_adj, wt->path); + if (wt->is_bare) + strbuf_addstr(&sb, "(bare)"); + else { + strbuf_addf(&sb, "%-*s ", abbrev_len, + repo_find_unique_abbrev(the_repository, &wt->head_oid, DEFAULT_ABBREV)); + if (wt->is_detached) + strbuf_addstr(&sb, "(detached HEAD)"); + else if (wt->head_ref) { + char *ref = shorten_unambiguous_ref(wt->head_ref, 0); + strbuf_addf(&sb, "[%s]", ref); + free(ref); + } else + strbuf_addstr(&sb, "(error)"); + } + + reason = worktree_lock_reason(wt); + if (verbose && reason && *reason) + strbuf_addf(&sb, "\n\tlocked: %s", reason); + else if (reason) + strbuf_addstr(&sb, " locked"); + + reason = worktree_prune_reason(wt, expire); + if (verbose && reason) + strbuf_addf(&sb, "\n\tprunable: %s", reason); + else if (reason) + strbuf_addstr(&sb, " prunable"); + + printf("%s\n", sb.buf); + strbuf_release(&sb); +} + +static void measure_widths(struct worktree **wt, int *abbrev, int *maxlen) +{ + int i; + + for (i = 0; wt[i]; i++) { + int sha1_len; + int path_len = strlen(wt[i]->path); + + if (path_len > *maxlen) + *maxlen = path_len; + sha1_len = strlen(repo_find_unique_abbrev(the_repository, &wt[i]->head_oid, *abbrev)); + if (sha1_len > *abbrev) + *abbrev = sha1_len; + } +} + +static int pathcmp(const void *a_, const void *b_) +{ + const struct worktree *const *a = a_; + const struct worktree *const *b = b_; + return fspathcmp((*a)->path, (*b)->path); +} + +static void pathsort(struct worktree **wt) +{ + int n = 0; + struct worktree **p = wt; + + while (*p++) + n++; + QSORT(wt, n, pathcmp); +} + +static int list(int ac, const char **av, const char *prefix) +{ + int porcelain = 0; + int line_terminator = '\n'; + + struct option options[] = { + OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")), + OPT__VERBOSE(&verbose, N_("show extended annotations and reasons, if available")), + OPT_EXPIRY_DATE(0, "expire", &expire, + N_("add 'prunable' annotation to worktrees older than <time>")), + OPT_SET_INT('z', NULL, &line_terminator, + N_("terminate records with a NUL character"), '\0'), + OPT_END() + }; + + expire = TIME_MAX; + ac = parse_options(ac, av, prefix, options, git_worktree_list_usage, 0); + if (ac) + usage_with_options(git_worktree_list_usage, options); + else if (verbose && porcelain) + die(_("options '%s' and '%s' cannot be used together"), "--verbose", "--porcelain"); + else if (!line_terminator && !porcelain) + die(_("the option '%s' requires '%s'"), "-z", "--porcelain"); + else { + struct worktree **worktrees = get_worktrees(); + int path_maxlen = 0, abbrev = DEFAULT_ABBREV, i; + + /* sort worktrees by path but keep main worktree at top */ + pathsort(worktrees + 1); + + if (!porcelain) + measure_widths(worktrees, &abbrev, &path_maxlen); + + for (i = 0; worktrees[i]; i++) { + if (porcelain) + show_worktree_porcelain(worktrees[i], + line_terminator); + else + show_worktree(worktrees[i], path_maxlen, abbrev); + } + free_worktrees(worktrees); + } + return 0; +} + +static int lock_worktree(int ac, const char **av, const char *prefix) +{ + const char *reason = "", *old_reason; + struct option options[] = { + OPT_STRING(0, "reason", &reason, N_("string"), + N_("reason for locking")), + OPT_END() + }; + struct worktree **worktrees, *wt; + + ac = parse_options(ac, av, prefix, options, git_worktree_lock_usage, 0); + if (ac != 1) + usage_with_options(git_worktree_lock_usage, options); + + worktrees = get_worktrees(); + wt = find_worktree(worktrees, prefix, av[0]); + if (!wt) + die(_("'%s' is not a working tree"), av[0]); + if (is_main_worktree(wt)) + die(_("The main working tree cannot be locked or unlocked")); + + old_reason = worktree_lock_reason(wt); + if (old_reason) { + if (*old_reason) + die(_("'%s' is already locked, reason: %s"), + av[0], old_reason); + die(_("'%s' is already locked"), av[0]); + } + + write_file(git_common_path("worktrees/%s/locked", wt->id), + "%s", reason); + free_worktrees(worktrees); + return 0; +} + +static int unlock_worktree(int ac, const char **av, const char *prefix) +{ + struct option options[] = { + OPT_END() + }; + struct worktree **worktrees, *wt; + int ret; + + ac = parse_options(ac, av, prefix, options, git_worktree_unlock_usage, 0); + if (ac != 1) + usage_with_options(git_worktree_unlock_usage, options); + + worktrees = get_worktrees(); + wt = find_worktree(worktrees, prefix, av[0]); + if (!wt) + die(_("'%s' is not a working tree"), av[0]); + if (is_main_worktree(wt)) + die(_("The main working tree cannot be locked or unlocked")); + if (!worktree_lock_reason(wt)) + die(_("'%s' is not locked"), av[0]); + ret = unlink_or_warn(git_common_path("worktrees/%s/locked", wt->id)); + free_worktrees(worktrees); + return ret; +} + +static void validate_no_submodules(const struct worktree *wt) +{ + struct index_state istate = INDEX_STATE_INIT(the_repository); + struct strbuf path = STRBUF_INIT; + int i, found_submodules = 0; + + if (is_directory(worktree_git_path(wt, "modules"))) { + /* + * There could be false positives, e.g. the "modules" + * directory exists but is empty. But it's a rare case and + * this simpler check is probably good enough for now. + */ + found_submodules = 1; + } else if (read_index_from(&istate, worktree_git_path(wt, "index"), + get_worktree_git_dir(wt)) > 0) { + for (i = 0; i < istate.cache_nr; i++) { + struct cache_entry *ce = istate.cache[i]; + int err; + + if (!S_ISGITLINK(ce->ce_mode)) + continue; + + strbuf_reset(&path); + strbuf_addf(&path, "%s/%s", wt->path, ce->name); + if (!is_submodule_populated_gently(path.buf, &err)) + continue; + + found_submodules = 1; + break; + } + } + discard_index(&istate); + strbuf_release(&path); + + if (found_submodules) + die(_("working trees containing submodules cannot be moved or removed")); +} + +static int move_worktree(int ac, const char **av, const char *prefix) +{ + int force = 0; + struct option options[] = { + OPT__FORCE(&force, + N_("force move even if worktree is dirty or locked"), + PARSE_OPT_NOCOMPLETE), + OPT_END() + }; + struct worktree **worktrees, *wt; + struct strbuf dst = STRBUF_INIT; + struct strbuf errmsg = STRBUF_INIT; + const char *reason = NULL; + char *path; + + ac = parse_options(ac, av, prefix, options, git_worktree_move_usage, + 0); + if (ac != 2) + usage_with_options(git_worktree_move_usage, options); + + path = prefix_filename(prefix, av[1]); + strbuf_addstr(&dst, path); + free(path); + + worktrees = get_worktrees(); + wt = find_worktree(worktrees, prefix, av[0]); + if (!wt) + die(_("'%s' is not a working tree"), av[0]); + if (is_main_worktree(wt)) + die(_("'%s' is a main working tree"), av[0]); + if (is_directory(dst.buf)) { + const char *sep = find_last_dir_sep(wt->path); + + if (!sep) + die(_("could not figure out destination name from '%s'"), + wt->path); + strbuf_trim_trailing_dir_sep(&dst); + strbuf_addstr(&dst, sep); + } + check_candidate_path(dst.buf, force, worktrees, "move"); + + validate_no_submodules(wt); + + if (force < 2) + reason = worktree_lock_reason(wt); + if (reason) { + if (*reason) + die(_("cannot move a locked working tree, lock reason: %s\nuse 'move -f -f' to override or unlock first"), + reason); + die(_("cannot move a locked working tree;\nuse 'move -f -f' to override or unlock first")); + } + if (validate_worktree(wt, &errmsg, 0)) + die(_("validation failed, cannot move working tree: %s"), + errmsg.buf); + strbuf_release(&errmsg); + + if (rename(wt->path, dst.buf) == -1) + die_errno(_("failed to move '%s' to '%s'"), wt->path, dst.buf); + + update_worktree_location(wt, dst.buf); + + strbuf_release(&dst); + free_worktrees(worktrees); + return 0; +} + +/* + * Note, "git status --porcelain" is used to determine if it's safe to + * delete a whole worktree. "git status" does not ignore user + * configuration, so if a normal "git status" shows "clean" for the + * user, then it's ok to remove it. + * + * This assumption may be a bad one. We may want to ignore + * (potentially bad) user settings and only delete a worktree when + * it's absolutely safe to do so from _our_ point of view because we + * know better. + */ +static void check_clean_worktree(struct worktree *wt, + const char *original_path) +{ + struct child_process cp; + char buf[1]; + int ret; + + /* + * Until we sort this out, all submodules are "dirty" and + * will abort this function. + */ + validate_no_submodules(wt); + + child_process_init(&cp); + strvec_pushf(&cp.env, "%s=%s/.git", + GIT_DIR_ENVIRONMENT, wt->path); + strvec_pushf(&cp.env, "%s=%s", + GIT_WORK_TREE_ENVIRONMENT, wt->path); + strvec_pushl(&cp.args, "status", + "--porcelain", "--ignore-submodules=none", + NULL); + cp.git_cmd = 1; + cp.dir = wt->path; + cp.out = -1; + ret = start_command(&cp); + if (ret) + die_errno(_("failed to run 'git status' on '%s'"), + original_path); + ret = xread(cp.out, buf, sizeof(buf)); + if (ret) + die(_("'%s' contains modified or untracked files, use --force to delete it"), + original_path); + close(cp.out); + ret = finish_command(&cp); + if (ret) + die_errno(_("failed to run 'git status' on '%s', code %d"), + original_path, ret); +} + +static int delete_git_work_tree(struct worktree *wt) +{ + struct strbuf sb = STRBUF_INIT; + int ret = 0; + + strbuf_addstr(&sb, wt->path); + if (remove_dir_recursively(&sb, 0)) { + error_errno(_("failed to delete '%s'"), sb.buf); + ret = -1; + } + strbuf_release(&sb); + return ret; +} + +static int remove_worktree(int ac, const char **av, const char *prefix) +{ + int force = 0; + struct option options[] = { + OPT__FORCE(&force, + N_("force removal even if worktree is dirty or locked"), + PARSE_OPT_NOCOMPLETE), + OPT_END() + }; + struct worktree **worktrees, *wt; + struct strbuf errmsg = STRBUF_INIT; + const char *reason = NULL; + int ret = 0; + + ac = parse_options(ac, av, prefix, options, git_worktree_remove_usage, 0); + if (ac != 1) + usage_with_options(git_worktree_remove_usage, options); + + worktrees = get_worktrees(); + wt = find_worktree(worktrees, prefix, av[0]); + if (!wt) + die(_("'%s' is not a working tree"), av[0]); + if (is_main_worktree(wt)) + die(_("'%s' is a main working tree"), av[0]); + if (force < 2) + reason = worktree_lock_reason(wt); + if (reason) { + if (*reason) + die(_("cannot remove a locked working tree, lock reason: %s\nuse 'remove -f -f' to override or unlock first"), + reason); + die(_("cannot remove a locked working tree;\nuse 'remove -f -f' to override or unlock first")); + } + if (validate_worktree(wt, &errmsg, WT_VALIDATE_WORKTREE_MISSING_OK)) + die(_("validation failed, cannot remove working tree: %s"), + errmsg.buf); + strbuf_release(&errmsg); + + if (file_exists(wt->path)) { + if (!force) + check_clean_worktree(wt, av[0]); + + ret |= delete_git_work_tree(wt); + } + /* + * continue on even if ret is non-zero, there's no going back + * from here. + */ + ret |= delete_git_dir(wt->id); + delete_worktrees_dir_if_empty(); + + free_worktrees(worktrees); + return ret; +} + +static void report_repair(int iserr, const char *path, const char *msg, void *cb_data) +{ + if (!iserr) { + fprintf_ln(stderr, _("repair: %s: %s"), msg, path); + } else { + int *exit_status = (int *)cb_data; + fprintf_ln(stderr, _("error: %s: %s"), msg, path); + *exit_status = 1; + } +} + +static int repair(int ac, const char **av, const char *prefix) +{ + const char **p; + const char *self[] = { ".", NULL }; + struct option options[] = { + OPT_END() + }; + int rc = 0; + + ac = parse_options(ac, av, prefix, options, git_worktree_repair_usage, 0); + p = ac > 0 ? av : self; + for (; *p; p++) + repair_worktree_at_path(*p, report_repair, &rc); + repair_worktrees(report_repair, &rc); + return rc; +} + +int cmd_worktree(int ac, const char **av, const char *prefix) +{ + parse_opt_subcommand_fn *fn = NULL; + struct option options[] = { + OPT_SUBCOMMAND("add", &fn, add), + OPT_SUBCOMMAND("prune", &fn, prune), + OPT_SUBCOMMAND("list", &fn, list), + OPT_SUBCOMMAND("lock", &fn, lock_worktree), + OPT_SUBCOMMAND("unlock", &fn, unlock_worktree), + OPT_SUBCOMMAND("move", &fn, move_worktree), + OPT_SUBCOMMAND("remove", &fn, remove_worktree), + OPT_SUBCOMMAND("repair", &fn, repair), + OPT_END() + }; + + git_config(git_worktree_config, NULL); + + if (!prefix) + prefix = ""; + + ac = parse_options(ac, av, prefix, options, git_worktree_usage, 0); + + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + return fn(ac, av, prefix); +} diff --git a/builtin/write-tree.c b/builtin/write-tree.c new file mode 100644 index 0000000..66e83d0 --- /dev/null +++ b/builtin/write-tree.c @@ -0,0 +1,64 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#define USE_THE_INDEX_VARIABLE +#include "builtin.h" +#include "config.h" +#include "environment.h" +#include "gettext.h" +#include "hex.h" +#include "tree.h" +#include "cache-tree.h" +#include "parse-options.h" +#include "repository.h" + +static const char * const write_tree_usage[] = { + N_("git write-tree [--missing-ok] [--prefix=<prefix>/]"), + NULL +}; + +int cmd_write_tree(int argc, const char **argv, const char *cmd_prefix) +{ + int flags = 0, ret; + const char *tree_prefix = NULL; + struct object_id oid; + const char *me = "git-write-tree"; + struct option write_tree_options[] = { + OPT_BIT(0, "missing-ok", &flags, N_("allow missing objects"), + WRITE_TREE_MISSING_OK), + OPT_STRING(0, "prefix", &tree_prefix, N_("<prefix>/"), + N_("write tree object for a subdirectory <prefix>")), + { OPTION_BIT, 0, "ignore-cache-tree", &flags, NULL, + N_("only useful for debugging"), + PARSE_OPT_HIDDEN | PARSE_OPT_NOARG, NULL, + WRITE_TREE_IGNORE_CACHE_TREE }, + OPT_END() + }; + + git_config(git_default_config, NULL); + argc = parse_options(argc, argv, cmd_prefix, write_tree_options, + write_tree_usage, 0); + + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + + ret = write_index_as_tree(&oid, &the_index, get_index_file(), flags, + tree_prefix); + switch (ret) { + case 0: + printf("%s\n", oid_to_hex(&oid)); + break; + case WRITE_TREE_UNREADABLE_INDEX: + die("%s: error reading the index", me); + break; + case WRITE_TREE_UNMERGED_INDEX: + die("%s: error building trees", me); + break; + case WRITE_TREE_PREFIX_ERROR: + die("%s: prefix %s not found", me, tree_prefix); + break; + } + return ret; +} |