diff options
Diffstat (limited to 'builtin')
105 files changed, 53832 insertions, 0 deletions
diff --git a/builtin/add.c b/builtin/add.c new file mode 100644 index 0000000000..a648cf4c56 --- /dev/null +++ b/builtin/add.c @@ -0,0 +1,521 @@ +/* + * "git add" builtin command + * + * Copyright (C) 2006 Linus Torvalds + */ +#include "cache.h" +#include "config.h" +#include "builtin.h" +#include "lockfile.h" +#include "dir.h" +#include "pathspec.h" +#include "exec_cmd.h" +#include "cache-tree.h" +#include "run-command.h" +#include "parse-options.h" +#include "diff.h" +#include "diffcore.h" +#include "revision.h" +#include "bulk-checkin.h" +#include "argv-array.h" +#include "submodule.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; + +struct update_callback_data { + int flags; + int add_errors; +}; + +static void chmod_pathspec(struct pathspec *pathspec, char flip) +{ + int i; + + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce = active_cache[i]; + + if (pathspec && !ce_path_match(ce, pathspec, NULL)) + continue; + + if (chmod_cache_entry(ce, flip) < 0) + fprintf(stderr, "cannot chmod %cx '%s'\n", flip, ce->name); + } +} + +static int fix_unmerged_status(struct diff_filepair *p, + struct update_callback_data *data) +{ + if (p->status != DIFF_STATUS_UNMERGED) + return p->status; + if (!(data->flags & ADD_CACHE_IGNORE_REMOVAL) && !p->two->mode) + /* + * This is not an explicit add request, and the + * path is missing from the working tree (deleted) + */ + return DIFF_STATUS_DELETED; + else + /* + * Either an explicit add request, or path exists + * in the working tree. An attempt to explicitly + * add a path that does not exist in the working tree + * will be caught as an error by the caller immediately. + */ + return DIFF_STATUS_MODIFIED; +} + +static void update_callback(struct diff_queue_struct *q, + struct diff_options *opt, void *cbdata) +{ + int i; + struct update_callback_data *data = cbdata; + + for (i = 0; i < q->nr; i++) { + struct diff_filepair *p = q->queue[i]; + const char *path = p->one->path; + switch (fix_unmerged_status(p, data)) { + default: + die(_("unexpected diff status %c"), p->status); + case DIFF_STATUS_MODIFIED: + case DIFF_STATUS_TYPE_CHANGED: + if (add_file_to_index(&the_index, path, data->flags)) { + if (!(data->flags & ADD_CACHE_IGNORE_ERRORS)) + die(_("updating files failed")); + data->add_errors++; + } + break; + case DIFF_STATUS_DELETED: + if (data->flags & ADD_CACHE_IGNORE_REMOVAL) + break; + if (!(data->flags & ADD_CACHE_PRETEND)) + remove_file_from_index(&the_index, path); + if (data->flags & (ADD_CACHE_PRETEND|ADD_CACHE_VERBOSE)) + printf(_("remove '%s'\n"), path); + break; + } + } +} + +int add_files_to_cache(const char *prefix, + const struct pathspec *pathspec, int flags) +{ + struct update_callback_data data; + struct rev_info rev; + + memset(&data, 0, sizeof(data)); + data.flags = flags; + + init_revisions(&rev, prefix); + setup_revisions(0, NULL, &rev, NULL); + if (pathspec) + copy_pathspec(&rev.prune_data, pathspec); + rev.diffopt.output_format = DIFF_FORMAT_CALLBACK; + rev.diffopt.format_callback = update_callback; + rev.diffopt.format_callback_data = &data; + rev.diffopt.flags |= DIFF_OPT_OVERRIDE_SUBMODULE_CONFIG; + rev.max_count = 0; /* do not compare unmerged paths with stage #2 */ + run_diff_files(&rev, DIFF_RACY_IS_MODIFIED); + clear_pathspec(&rev.prune_data); + return !!data.add_errors; +} + +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(entry, pathspec, prefix, seen)) + *dst++ = entry; + } + dir->nr = dst - dir->entries; + add_pathspec_matches_against_index(pathspec, &the_index, seen); + return seen; +} + +static void refresh(int verbose, const struct pathspec *pathspec) +{ + char *seen; + int i; + + seen = xcalloc(pathspec->nr, 1); + refresh_index(&the_index, verbose ? REFRESH_IN_PORCELAIN : REFRESH_QUIET, + pathspec, seen, _("Unstaged changes after refreshing the index:")); + for (i = 0; i < pathspec->nr; i++) { + if (!seen[i]) + die(_("pathspec '%s' did not match any files"), + pathspec->items[i].match); + } + free(seen); +} + +int run_add_interactive(const char *revision, const char *patch_mode, + const struct pathspec *pathspec) +{ + int status, i; + struct argv_array argv = ARGV_ARRAY_INIT; + + argv_array_push(&argv, "add--interactive"); + if (patch_mode) + argv_array_push(&argv, patch_mode); + if (revision) + argv_array_push(&argv, revision); + argv_array_push(&argv, "--"); + for (i = 0; i < pathspec->nr; i++) + /* pass original pathspec, to be re-parsed */ + argv_array_push(&argv, pathspec->items[i].original); + + status = run_command_v_opt(argv.argv, RUN_GIT_CMD); + argv_array_clear(&argv); + return status; +} + +int interactive_add(int argc, const char **argv, const char *prefix, int patch) +{ + struct pathspec pathspec; + + parse_pathspec(&pathspec, 0, + PATHSPEC_PREFER_FULL | + PATHSPEC_SYMLINK_LEADING_PATH | + PATHSPEC_PREFIX_ORIGIN, + prefix, argv); + + return run_add_interactive(NULL, + patch ? "--patch" : NULL, + &pathspec); +} + +static int edit_patch(int argc, const char **argv, const char *prefix) +{ + char *file = git_pathdup("ADD_EDIT.patch"); + const char *apply_argv[] = { "apply", "--recount", "--cached", + NULL, NULL }; + struct child_process child = CHILD_PROCESS_INIT; + struct rev_info rev; + int out; + struct stat st; + + apply_argv[3] = file; + + git_config(git_diff_basic_config, NULL); /* no "diff" UI options */ + + if (read_cache() < 0) + die(_("Could not read the index")); + + init_revisions(&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; + DIFF_OPT_SET(&rev.diffopt, IGNORE_DIRTY_SUBMODULES); + out = open(file, O_CREAT | O_WRONLY, 0666); + if (out < 0) + die(_("Could not open '%s' for writing."), file); + rev.diffopt.file = xfdopen(out, "w"); + rev.diffopt.close_file = 1; + if (run_diff_files(&rev, 0)) + die(_("Could not write patch")); + + 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; + child.argv = apply_argv; + if (run_command(&child)) + die(_("Could not apply '%s'"), file); + + unlink(file); + free(file); + return 0; +} + +static struct lock_file lock_file; + +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) +{ + /* 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")), + OPT_BOOL('u', "update", &take_worktree_changes, N_("update tracked files")), + 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")), + { OPTION_CALLBACK, 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_STRING( 0 , "chmod", &chmod_arg, N_("(+/-)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_END(), +}; + +static int add_config(const char *var, const char *value, void *cb) +{ + if (!strcmp(var, "add.ignoreerrors") || + !strcmp(var, "add.ignore-errors")) { + ignore_add_errors = git_config_bool(var, value); + return 0; + } + return git_default_config(var, value, 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; + + 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 (advice_add_embedded_repo) { + advise(embedded_advice, name.buf, name.buf); + /* there may be multiple entries; advise only once */ + advice_add_embedded_repo = 0; + } + + strbuf_release(&name); +} + +static int add_files(struct dir_struct *dir, int flags) +{ + int i, exit_status = 0; + + if (dir->ignored_nr) { + fprintf(stderr, _(ignore_error)); + for (i = 0; i < dir->ignored_nr; i++) + fprintf(stderr, "%s\n", dir->ignored[i]->name); + fprintf(stderr, _("Use -f if you really want to add them.\n")); + exit_status = 1; + } + + for (i = 0; i < dir->nr; i++) { + check_embedded_repo(dir->entries[i]->name); + if (add_file_to_index(&the_index, dir->entries[i]->name, flags)) { + if (!ignore_add_errors) + die(_("adding files failed")); + exit_status = 1; + } + } + 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; + int flags; + int add_new_files; + int require_pathspec; + char *seen = NULL; + + 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) + exit(interactive_add(argc - 1, argv + 1, prefix, patch_interactive)); + + if (edit_interactive) + 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(_("-A and -u are mutually incompatible")); + + if (!take_worktree_changes && addremove_explicit < 0 && argc) + /* Turn "git add pathspec..." to "git add -A pathspec..." */ + addremove = 1; + + if (!show_only && ignore_missing) + die(_("Option --ignore-missing can only be used together with --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; + require_pathspec = !(take_worktree_changes || (0 < addremove_explicit)); + + hold_locked_index(&lock_file, LOCK_DIE_ON_ERROR); + + 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 (require_pathspec && argc == 0) { + fprintf(stderr, _("Nothing specified, nothing added.\n")); + fprintf(stderr, _("Maybe you wanted to say 'git add .'?\n")); + return 0; + } + + if (read_cache() < 0) + die(_("index file corrupt")); + + die_in_unpopulated_submodule(&the_index, prefix); + + /* + * Check the "pathspec '%s' did not match any files" block + * below before enabling new magic. + */ + parse_pathspec(&pathspec, 0, + PATHSPEC_PREFER_FULL | + PATHSPEC_SYMLINK_LEADING_PATH, + prefix, argv); + + die_path_inside_submodule(&the_index, &pathspec); + + if (add_new_files) { + int baselen; + + /* Set up the default git porcelain excludes */ + memset(&dir, 0, sizeof(dir)); + 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) { + refresh(verbose, &pathspec); + goto finish; + } + + if (pathspec.nr) { + int i; + + if (!seen) + seen = find_pathspecs_matching_against_index(&pathspec, &the_index); + + /* + * 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] && path[0] && + ((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); + } + } + free(seen); + } + + plug_bulk_checkin(); + + exit_status |= add_files_to_cache(prefix, &pathspec, flags); + + if (add_new_files) + exit_status |= add_files(&dir, flags); + + if (chmod_arg && pathspec.nr) + chmod_pathspec(&pathspec, chmod_arg[0]); + unplug_bulk_checkin(); + +finish: + if (active_cache_changed) { + if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK)) + die(_("Unable to write new index file")); + } + + UNLEAK(pathspec); + UNLEAK(dir); + return exit_status; +} diff --git a/builtin/am.c b/builtin/am.c new file mode 100644 index 0000000000..d7513f5375 --- /dev/null +++ b/builtin/am.c @@ -0,0 +1,2405 @@ +/* + * Builtin "git am" + * + * Based on git-am.sh by Junio C Hamano. + */ +#include "cache.h" +#include "config.h" +#include "builtin.h" +#include "exec_cmd.h" +#include "parse-options.h" +#include "dir.h" +#include "run-command.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 "sequencer.h" +#include "revision.h" +#include "merge-recursive.h" +#include "revision.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" + +/** + * Returns 1 if the file is empty or does not exist, 0 otherwise. + */ +static int is_empty_file(const char *filename) +{ + struct stat st; + + if (stat(filename, &st) < 0) { + if (errno == ENOENT) + return 1; + die_errno(_("could not stat %s"), filename); + } + + return !st.st_size; +} + +/** + * 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 */ +}; + +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 threeway; + int quiet; + int signoff; /* enum signoff_type */ + int utf8; + int keep; /* enum keep_type */ + int message_id; + int scissors; /* enum scissors_type */ + struct argv_array 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; + + argv_array_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); + argv_array_clear(&state->git_apply_opts); +} + +/** + * 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. + */ +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)); +} + +/** + * Take a series of KEY='VALUE' lines where VALUE part is + * sq-quoted, and append <KEY, VALUE> at the end of the string list + */ +static int parse_key_value_squoted(char *buf, struct string_list *list) +{ + while (*buf) { + struct string_list_item *item; + char *np; + char *cp = strchr(buf, '='); + if (!cp) + return -1; + np = strchrnul(cp, '\n'); + *cp++ = '\0'; + item = string_list_append(list, buf); + + buf = np + (*np == '\n'); + *np = '\0'; + cp = sq_dequote(cp); + if (!cp) + return -1; + item->util = xstrdup(cp); + } + return 0; +} + +/** + * 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_author_script(struct am_state *state) +{ + const char *filename = am_path(state, "author-script"); + struct strbuf buf = STRBUF_INIT; + struct string_list kv = STRING_LIST_INIT_DUP; + int retval = -1; /* assume failure */ + int fd; + + assert(!state->author_name); + assert(!state->author_email); + assert(!state->author_date); + + fd = open(filename, O_RDONLY); + if (fd < 0) { + if (errno == ENOENT) + return 0; + die_errno(_("could not open '%s' for reading"), filename); + } + strbuf_read(&buf, fd, 0); + close(fd); + if (parse_key_value_squoted(buf.buf, &kv)) + goto finish; + + if (kv.nr != 3 || + strcmp(kv.items[0].string, "GIT_AUTHOR_NAME") || + strcmp(kv.items[1].string, "GIT_AUTHOR_EMAIL") || + strcmp(kv.items[2].string, "GIT_AUTHOR_DATE")) + goto finish; + state->author_name = kv.items[0].util; + state->author_email = kv.items[1].util; + state->author_date = kv.items[2].util; + retval = 0; +finish: + string_list_clear(&kv, !!retval); + strbuf_release(&buf); + return retval; +} + +/** + * 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) + die("BUG: state file 'next' does not exist"); + state->cur = strtol(sb.buf, NULL, 10); + + if (read_state_file(&sb, state, "last", 1) < 0) + die("BUG: state file 'last' does not exist"); + state->last = strtol(sb.buf, NULL, 10); + + if (read_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, "apply-opt", 1); + argv_array_clear(&state->git_apply_opts); + if (sq_dequote_to_argv_array(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; + + assert(state->msg); + ret = run_hook_le(NULL, "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 child_process cp = CHILD_PROCESS_INIT; + const char *hook = find_hook("post-rewrite"); + int ret; + + if (!hook) + return 0; + + argv_array_push(&cp.args, hook); + argv_array_push(&cp.args, "rebase"); + + cp.in = xopen(am_path(state, "rewritten"), O_RDONLY); + cp.stdout_to_stderr = 1; + + ret = run_command(&cp); + + close(cp.in); + return ret; +} + +/** + * 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; + + if (sb.len != GIT_SHA1_HEXSZ * 2 + 1) { + ret = error(invalid_line, sb.buf); + goto finish; + } + + if (get_oid_hex(sb.buf, &from_obj)) { + ret = error(invalid_line, sb.buf); + goto finish; + } + + if (sb.buf[GIT_SHA1_HEXSZ] != ' ') { + ret = error(invalid_line, sb.buf); + goto finish; + } + + if (get_oid_hex(sb.buf + GIT_SHA1_HEXSZ + 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(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; + + cp.git_cmd = 1; + argv_array_push(&cp.args, "mailsplit"); + argv_array_pushf(&cp.args, "-d%d", state->prec); + argv_array_pushf(&cp.args, "-o%s", state->dir); + argv_array_push(&cp.args, "-b"); + if (keep_cr) + argv_array_push(&cp.args, "--keep-cr"); + if (mboxrd) + argv_array_push(&cp.args, "--mboxrd"); + argv_array_push(&cp.args, "--"); + argv_array_pushv(&cp.args, paths); + + if (capture_command(&cp, &last, 8)) + return -1; + + state->cur = 1; + state->last = strtol(last.buf, NULL, 10); + + return 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) +{ + 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 argv_array patches = ARGV_ARRAY_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 */ + + argv_array_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.argv, keep_cr); + + argv_array_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) +{ + 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: + die("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); + + 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: + die("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: + die("BUG: invalid value for state->scissors"); + } + write_state_text(state, "scissors", str); + + sq_quote_argv(&sb, state->git_apply_opts.argv, 0); + write_state_text(state, "apply-opt", sb.buf); + + if (state->rebasing) + write_state_text(state, "rebasing", ""); + else + write_state_text(state, "applying", ""); + + if (!get_oid("HEAD", &curr_head)) { + write_state_text(state, "abort-safety", oid_to_hex(&curr_head)); + if (!state->rebasing) + update_ref_oid("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")); + + if (!get_oid("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; +} + +/** + * Refresh and write index. + */ +static void refresh_and_write_cache(void) +{ + struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file)); + + hold_locked_index(lock_file, LOCK_DIE_ON_ERROR); + refresh_cache(REFRESH_QUIET); + if (write_locked_index(&the_index, lock_file, COMMIT_LOCK)) + die(_("unable to write index file")); +} + +/** + * Returns 1 if the index differs from HEAD, 0 otherwise. When on an unborn + * branch, returns 1 if there are entries in the index, 0 otherwise. If an + * strbuf is provided, the space-separated list of files that differ will be + * appended to it. + */ +static int index_has_changes(struct strbuf *sb) +{ + struct object_id head; + int i; + + if (!get_oid_tree("HEAD", &head)) { + struct diff_options opt; + + diff_setup(&opt); + DIFF_OPT_SET(&opt, EXIT_WITH_STATUS); + if (!sb) + DIFF_OPT_SET(&opt, QUICK); + do_diff_cache(&head, &opt); + diffcore_std(&opt); + for (i = 0; sb && i < diff_queued_diff.nr; i++) { + if (i) + strbuf_addch(sb, ' '); + strbuf_addstr(sb, diff_queued_diff.queue[i]->two->path); + } + diff_flush(&opt); + return DIFF_OPT_TST(&opt, HAS_CHANGES) != 0; + } else { + for (i = 0; sb && i < active_nr; i++) { + if (i) + strbuf_addch(sb, ' '); + strbuf_addstr(sb, active_cache[i]->name); + } + return !!active_nr; + } +} + +/** + * 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); + 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: + die("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: + die("BUG: invalid value for state->scissors"); + } + + 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); + + /* 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; + } + + if (is_empty_file(am_path(state, "patch"))) { + printf_ln(_("Patch is empty.")); + die_user_resolve(state); + } + + 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 = logmsg_reencode(commit, NULL, get_commit_output_encoding()); + + ident_line = find_commit_header(buffer, "author", &ident_len); + + 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); + unuse_commit_buffer(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"); + init_revisions(&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; + DIFF_OPT_SET(&rev_info.diffopt, BINARY); + DIFF_OPT_SET(&rev_info.diffopt, FULL_INDEX); + 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); +} + +/** + * 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 (!get_oid_tree("HEAD", &head)) + tree = lookup_tree(&head); + else + tree = lookup_tree(&empty_tree_oid); + + fp = xfopen(am_path(state, "patch"), "w"); + init_revisions(&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, 1); +} + +/** + * 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)); + + 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 argv_array apply_paths = ARGV_ARRAY_INIT; + struct argv_array apply_opts = ARGV_ARRAY_INIT; + struct apply_state apply_state; + int res, opts_left; + static struct lock_file lock_file; + int force_apply = 0; + int options = 0; + + if (init_apply_state(&apply_state, NULL, &lock_file)) + die("BUG: init_apply_state() failed"); + + argv_array_push(&apply_opts, "apply"); + argv_array_pushv(&apply_opts, state->git_apply_opts.argv); + + opts_left = apply_parse_options(apply_opts.argc, apply_opts.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)) + die("BUG: check_apply_state() failed"); + + argv_array_push(&apply_paths, am_path(state, "patch")); + + res = apply_all_patches(&apply_state, apply_paths.argc, apply_paths.argv, options); + + argv_array_clear(&apply_paths); + argv_array_clear(&apply_opts); + clear_apply_state(&apply_state); + + if (res) + return res; + + if (index_file) { + /* Reload index as apply_all_patches() will have modified it. */ + discard_cache(); + read_cache_from(index_file); + } + + 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; + argv_array_push(&cp.args, "apply"); + argv_array_pushv(&cp.args, state->git_apply_opts.argv); + argv_array_pushf(&cp.args, "--build-fake-ancestor=%s", index_file); + argv_array_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 (get_oid("HEAD", &our_tree) < 0) + hashcpy(our_tree.hash, EMPTY_TREE_SHA1_BIN); + + if (build_fake_ancestor(state, index_path)) + return error("could not build fake ancestor"); + + discard_cache(); + read_cache_from(index_path); + + if (write_index_as_tree(orig_tree.hash, &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; + const char *diff_filter_str = "--diff-filter=AM"; + + init_revisions(&rev_info, NULL); + rev_info.diffopt.output_format = DIFF_FORMAT_NAME_STATUS; + diff_opt_parse(&rev_info.diffopt, &diff_filter_str, 1, rev_info.prefix); + add_pending_oid(&rev_info, "HEAD", &our_tree, 0); + diff_setup_done(&rev_info.diffopt); + run_diff_index(&rev_info, 1); + } + + 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.hash, &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_cache(); + read_cache(); + + /* + * 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); + + o.branch1 = "HEAD"; + their_tree_name = xstrfmt("%.*s", linelen(state->msg), state->msg); + o.branch2 = their_tree_name; + + if (state->quiet) + o.verbosity = 0; + + if (merge_recursive_generic(&o, &our_tree, &their_tree, 1, bases, &result)) { + rerere(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; + struct strbuf sb = STRBUF_INIT; + + if (run_hook_le(NULL, "pre-applypatch", NULL)) + exit(1); + + if (write_cache_as_tree(tree.hash, 0, NULL)) + die(_("git write-tree failed to write a tree")); + + if (!get_oid_commit("HEAD", &parent)) { + old_oid = &parent; + commit_list_insert(lookup_commit(&parent), &parents); + } else { + old_oid = NULL; + say(state, stderr, _("applying to an empty history")); + } + + author = fmt_ident(state->author_name, state->author_email, + state->ignore_date ? NULL : state->author_date, + IDENT_STRICT); + + if (state->committer_date_is_author_date) + setenv("GIT_COMMITTER_DATE", + state->ignore_date ? "" : state->author_date, 1); + + if (commit_tree(state->msg, state->msg_len, tree.hash, parents, commit.hash, + author, state->sign_commit)) + 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_oid(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_hook_le(NULL, "post-applypatch", NULL); + + 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); + + if (!isatty(0)) + die(_("cannot be interactive without stdin connected to a terminal.")); + + for (;;) { + const char *reply; + + 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. + */ + reply = git_prompt(_("Apply? [y]es/[n]o/[e]dit/[v]iew patch/[a]ccept all: "), PROMPT_ECHO); + + if (!reply) { + continue; + } else 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); + argv_array_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) +{ + const char *argv_gc_auto[] = {"gc", "--auto", NULL}; + struct strbuf sb = STRBUF_INIT; + + unlink(am_path(state, "dirtyindex")); + + refresh_and_write_cache(); + + if (index_has_changes(&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; + + 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; + + if (run_applypatch_msg_hook(state)) + exit(1); + + 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 && !index_has_changes(NULL)) { + say(state, stdout, _("No changes -- Patch already applied.")); + goto next; + } + } + + if (apply_status) { + int advice_amworkdir = 1; + + printf_ln(_("Patch failed at %s %.*s"), msgnum(state), + linelen(state->msg), state->msg); + + git_config_get_bool("advice.amworkdir", &advice_amworkdir); + + if (advice_amworkdir) + printf_ln(_("The copy of the patch that failed is found in: %s"), + am_path(state, "patch")); + + die_user_resolve(state); + } + + do_commit(state); + +next: + am_next(state); + + if (resume) + am_load(state); + resume = 0; + } + + if (!is_empty_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); + close_all_packs(); + run_command_v_opt(argv_gc_auto, RUN_GIT_CMD); + } +} + +/** + * 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. + */ +static void am_resolve(struct am_state *state) +{ + validate_resume_state(state); + + say(state, stdout, _("Applying: %.*s"), linelen(state->msg), state->msg); + + if (!index_has_changes(NULL)) { + 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_cache()) { + 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; + } + + rerere(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; + struct unpack_trees_options opts; + struct tree_desc t[2]; + + if (parse_tree(head) || parse_tree(remote)) + return -1; + + lock_file = xcalloc(1, sizeof(struct lock_file)); + hold_locked_index(lock_file, LOCK_DIE_ON_ERROR); + + refresh_cache(REFRESH_QUIET); + + 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; + 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; + struct unpack_trees_options opts; + struct tree_desc t[1]; + + if (parse_tree(tree)) + return -1; + + lock_file = xcalloc(1, sizeof(struct lock_file)); + hold_locked_index(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)); + + read_cache_unmerged(); + + if (fast_forward_to(head_tree, head_tree, 1)) + return -1; + + if (write_cache_as_tree(index.hash, 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(); + + 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(&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 (get_oid("HEAD", &head)) + hashcpy(head.hash, EMPTY_TREE_SHA1_BIN); + + if (clean_index(&head, &head)) + die(_("failed to clean index")); + + 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 (get_oid("HEAD", &head)) + oidclr(&head); + + if (!oidcmp(&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.hash, NULL); + has_curr_head = curr_branch && !is_null_oid(&curr_head); + if (!has_curr_head) + hashcpy(curr_head.hash, EMPTY_TREE_SHA1_BIN); + + has_orig_head = !get_oid("ORIG_HEAD", &orig_head); + if (!has_orig_head) + hashcpy(orig_head.hash, EMPTY_TREE_SHA1_BIN); + + clean_index(&curr_head, &orig_head); + + if (has_orig_head) + update_ref_oid("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_NODEREF); + + free(curr_branch); + am_destroy(state); +} + +/** + * 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 (!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; + else + return error(_("Invalid value for --patch-format: %s"), arg); + return 0; +} + +enum resume_mode { + RESUME_FALSE = 0, + RESUME_APPLY, + RESUME_RESOLVED, + RESUME_SKIP, + RESUME_ABORT +}; + +static int git_am_config(const char *k, const char *v, void *cb) +{ + int status; + + status = git_gpg_config(k, v, NULL); + if (status) + return status; + + return git_default_config(k, v, NULL); +} + +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_mode resume = RESUME_FALSE; + int in_progress; + + 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_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 line 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")), + { OPTION_SET_INT, 0, "keep-cr", &keep_cr, NULL, + N_("pass --keep-cr flag to git-mailsplit for mbox format"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 1}, + { OPTION_SET_INT, 0, "no-keep-cr", &keep_cr, NULL, + N_("do not pass --keep-cr flag to git-mailsplit independent of am.keepcr"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 0}, + OPT_BOOL('c', "scissors", &state.scissors, + N_("strip everything before a scissors line")), + 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, + N_("continue applying patches after resolving a conflict"), + RESUME_RESOLVED), + OPT_CMDMODE('r', "resolved", &resume, + N_("synonyms for --continue"), + RESUME_RESOLVED), + OPT_CMDMODE(0, "skip", &resume, + N_("skip the current patch"), + RESUME_SKIP), + OPT_CMDMODE(0, "abort", &resume, + N_("restore the original branch and abort the patching operation."), + RESUME_ABORT), + 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_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_am_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 (read_index_preload(&the_index, NULL) < 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 == RESUME_FALSE && !isatty(0))) + die(_("previous rebase directory %s still exists but mbox given."), + state.dir); + + if (resume == RESUME_FALSE) + resume = RESUME_APPLY; + + if (state.signoff == SIGNOFF_EXPLICIT) + am_append_signoff(&state); + } else { + struct argv_array paths = ARGV_ARRAY_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 == RESUME_ABORT) { + 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) + die(_("Resolve operation not in progress, we are not resuming.")); + + for (i = 0; i < argc; i++) { + if (is_absolute_path(argv[i]) || !prefix) + argv_array_push(&paths, argv[i]); + else + argv_array_push(&paths, mkpath("%s/%s", prefix, argv[i])); + } + + am_setup(&state, patch_format, paths.argv, keep_cr); + + argv_array_clear(&paths); + } + + switch (resume) { + case RESUME_FALSE: + am_run(&state, 0); + break; + case RESUME_APPLY: + am_run(&state, 1); + break; + case RESUME_RESOLVED: + am_resolve(&state); + break; + case RESUME_SKIP: + am_skip(&state); + break; + case RESUME_ABORT: + am_abort(&state); + break; + default: + die("BUG: invalid resume value"); + } + + am_state_release(&state); + + return 0; +} diff --git a/builtin/annotate.c b/builtin/annotate.c new file mode 100644 index 0000000000..da413ae0d1 --- /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 "argv-array.h" + +int cmd_annotate(int argc, const char **argv, const char *prefix) +{ + struct argv_array args = ARGV_ARRAY_INIT; + int i; + + argv_array_pushl(&args, "annotate", "-c", NULL); + + for (i = 1; i < argc; i++) { + argv_array_push(&args, argv[i]); + } + + return cmd_blame(args.argc, args.argv, prefix); +} diff --git a/builtin/apply.c b/builtin/apply.c new file mode 100644 index 0000000000..81b9a61c37 --- /dev/null +++ b/builtin/apply.c @@ -0,0 +1,36 @@ +#include "cache.h" +#include "builtin.h" +#include "parse-options.h" +#include "lockfile.h" +#include "apply.h" + +static const char * const apply_usage[] = { + N_("git apply [<options>] [<patch>...]"), + NULL +}; + +static struct lock_file lock_file; + +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, prefix, &lock_file)) + 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 0000000000..f863465a0f --- /dev/null +++ b/builtin/archive.c @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2006 Franck Bui-Huu + * Copyright (c) 2006 Rene Scharfe + */ +#include "cache.h" +#include "builtin.h" +#include "archive.h" +#include "transport.h" +#include "parse-options.h" +#include "pkt-line.h" +#include "sideband.h" + +static void create_output_file(const char *output_file) +{ + int output_fd = open(output_file, O_CREAT | O_WRONLY | O_TRUNC, 0666); + if (output_fd < 0) + die_errno(_("could not create archive file '%s'"), output_file); + 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) +{ + char *buf; + int fd[2], i, rv; + struct transport *transport; + struct remote *_remote; + + _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]); + + buf = packet_read_line(fd[0], NULL); + if (!buf) + die(_("git archive: expected ACK/NAK, got EOF")); + if (strcmp(buf, "ACK")) { + if (starts_with(buf, "NACK ")) + die(_("git archive: NACK %s"), buf + 5); + if (starts_with(buf, "ERR ")) + die(_("remote error: %s"), buf + 4); + die(_("git archive: protocol error")); + } + + if (packet_read_line(fd[0], NULL)) + 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 | \ + PARSE_OPT_NO_INTERNAL_HELP ) + +int cmd_archive(int argc, const char **argv, const char *prefix) +{ + const char *exec = "git-upload-archive"; + const 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); + + if (output) + create_output_file(output); + + if (remote) + return run_remote_archiver(argc, argv, remote, exec, output); + + setvbuf(stderr, NULL, _IOLBF, BUFSIZ); + + return write_archive(argc, argv, prefix, output, 0); +} diff --git a/builtin/bisect--helper.c b/builtin/bisect--helper.c new file mode 100644 index 0000000000..3324229025 --- /dev/null +++ b/builtin/bisect--helper.c @@ -0,0 +1,31 @@ +#include "builtin.h" +#include "cache.h" +#include "parse-options.h" +#include "bisect.h" + +static const char * const git_bisect_helper_usage[] = { + N_("git bisect--helper --next-all [--no-checkout]"), + NULL +}; + +int cmd_bisect__helper(int argc, const char **argv, const char *prefix) +{ + int next_all = 0; + int no_checkout = 0; + struct option options[] = { + OPT_BOOL(0, "next-all", &next_all, + N_("perform 'git bisect next'")), + OPT_BOOL(0, "no-checkout", &no_checkout, + N_("update BISECT_HEAD instead of checking out the current commit")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, + git_bisect_helper_usage, 0); + + if (!next_all) + usage_with_options(git_bisect_helper_usage, options); + + /* next-all */ + return bisect_next_all(prefix, no_checkout); +} diff --git a/builtin/blame.c b/builtin/blame.c new file mode 100644 index 0000000000..67adaef4d8 --- /dev/null +++ b/builtin/blame.c @@ -0,0 +1,960 @@ +/* + * Blame + * + * Copyright (c) 2006, 2014 by its authors + * See COPYING for licensing conditions + */ + +#include "cache.h" +#include "config.h" +#include "builtin.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 "blame.h" + +static char blame_usage[] = N_("git blame [<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 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 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 +#define DEBUG 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; +}; + +/* + * 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_init(struct commit_info *ci) +{ + + strbuf_init(&ci->author, 0); + strbuf_init(&ci->author_mail, 0); + strbuf_init(&ci->author_tz, 0); + strbuf_init(&ci->committer, 0); + strbuf_init(&ci->committer_mail, 0); + strbuf_init(&ci->committer_tz, 0); + strbuf_init(&ci->summary, 0); +} + +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; + + commit_info_init(ret); + + encoding = get_log_output_encoding(); + message = logmsg_reencode(commit, NULL, encoding); + get_ac_line(message, "\nauthor ", + &ret->author, &ret->author_mail, + &ret->author_time, &ret->author_tz); + + if (!detailed) { + unuse_commit_buffer(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)); + + unuse_commit_buffer(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; + + 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 001 +#define OUTPUT_LONG_OBJECT_NAME 002 +#define OUTPUT_RAW_TIMESTAMP 004 +#define OUTPUT_PORCELAIN 010 +#define OUTPUT_SHOW_NAME 020 +#define OUTPUT_SHOW_NUMBER 040 +#define OUTPUT_SHOW_SCORE 0100 +#define OUTPUT_NO_AUTHOR 0200 +#define OUTPUT_SHOW_EMAIL 0400 +#define OUTPUT_LINE_PORCELAIN 01000 + +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 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; + char hex[GIT_MAX_HEXSZ + 1]; + int show_raw_time = !!(opt & OUTPUT_RAW_TIMESTAMP); + + get_commit_info(suspect->commit, &ci, 1); + oid_to_hex_r(hex, &suspect->commit->object.oid); + + cp = blame_nth_line(sb, ent->lno); + for (cnt = 0; cnt < ent->num_lines; cnt++) { + char ch; + int length = (opt & OUTPUT_LONG_OBJECT_NAME) ? GIT_SHA1_HEXSZ : abbrev; + + if (suspect->commit->object.flags & UNINTERESTING) { + if (blank_boundary) + memset(hex, ' ', length); + else if (!(opt & OUTPUT_ANNOTATE_COMPAT)) { + 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); + } + 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 = commit->util; 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(graft, 0); + } + fclose(fp); + strbuf_release(&buf); + return 0; +} + +static int update_auto_abbrev(int auto_abbrev, struct blame_origin *suspect) +{ + const char *uniq = find_unique_abbrev(suspect->commit->object.oid.hash, + 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; + 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, 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 (git_diff_heuristic_config(var, value, cb) < 0) + return -1; + if (userdiff_config(var, value) < 0) + return -1; + + return git_default_config(var, value, cb); +} + +static int blame_copy_callback(const struct option *option, const char *arg, int unset) +{ + int *opt = option->value; + + /* + * -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; + + *opt |= PICKAXE_BLAME_MOVE; + + if (arg) + blame_move_score = parse_score(arg); + return 0; +} + +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; + 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_("Show blank SHA-1 for 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), + + /* + * The following two options are parsed by parse_revision_opt() + * and are only included here to get included in the "-h" + * output: + */ + { OPTION_LOWLEVEL_CALLBACK, 0, "indent-heuristic", NULL, NULL, N_("Use an experimental heuristic to improve diffs"), PARSE_OPT_NOARG, parse_opt_unknown_cb }, + + 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")), + { OPTION_CALLBACK, 'C', NULL, &opt, N_("score"), N_("Find line copies within and across files"), PARSE_OPT_OPTARG, blame_copy_callback }, + { OPTION_CALLBACK, '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_("n,m"), N_("Process only line range n,m, counting from 1")), + 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; + + git_config(git_blame_config, &output_option); + init_revisions(&revs, NULL); + revs.date_mode = blame_date_mode; + DIFF_OPT_SET(&revs.diffopt, ALLOW_TEXTCONV); + DIFF_OPT_SET(&revs.diffopt, FOLLOW_RENAMES); + + 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, blame_opt_usage)) { + case PARSE_OPT_HELP: + exit(129); + 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, blame_opt_usage); + } +parse_done: + no_whole_file_rename = !DIFF_OPT_TST(&revs.diffopt, FOLLOW_RENAMES); + xdl_opts |= revs.diffopt.xdl_opts & XDF_INDENT_HEURISTIC; + DIFF_OPT_CLR(&revs.diffopt, FOLLOW_RENAMES); + argc = parse_options_end(&ctx); + + 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 < GIT_SHA1_HEXSZ) + /* one more abbrev length is needed for the boundary commit */ + abbrev++; + else if (!abbrev) + abbrev = GIT_SHA1_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_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 (DIFF_OPT_TST(&revs.diffopt, 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(blame_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(blame_opt_usage, options); + } + } else { + if (argc < 2) + usage_with_options(blame_opt_usage, options); + path = add_prefix(prefix, argv[argc - 1]); + if (argc == 3 && !file_exists(path)) { /* (2b) */ + path = add_prefix(prefix, argv[1]); + argv[1] = argv[2]; + } + argv[argc - 1] = "--"; + + setup_work_tree(); + if (!file_exists(path)) + die_errno("cannot stat path '%s'", path); + } + + revs.disable_stdin = 1; + setup_revisions(argc, argv, &revs, NULL); + + init_scoreboard(&sb); + sb.revs = &revs; + sb.contents_from = contents_from; + sb.reverse = reverse; + setup_scoreboard(&sb, path, &o); + 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)) + usage(blame_usage); + if (lno < top || ((lno || bottom) && lno < bottom)) + die(Q_("file %s has only %lu line", + "file %s has only %lu lines", + lno), path, lno); + if (bottom < 1) + bottom = 1; + if (top < 1) + 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); + } + + 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; + sb.path = path; + + if (blame_move_score) + sb.move_score = blame_move_score; + if (blame_copy_score) + sb.copy_score = blame_copy_score; + + sb.debug = DEBUG; + 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, NULL); + + sb.found_guilty_entry = &found_guilty_entry; + sb.found_guilty_entry_data = π + if (show_progress) + pi.progress = start_delayed_progress(_("Blaming lines"), sb.num_lines); + + assign_blame(&sb, opt); + + stop_progress(&pi.progress); + + if (!incremental) + setup_pager(); + else + return 0; + + blame_sort_final(&sb); + + blame_coalesce(&sb); + + if (!(output_option & OUTPUT_PORCELAIN)) + find_alignment(&sb, &output_option); + + 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); + } + return 0; +} diff --git a/builtin/branch.c b/builtin/branch.c new file mode 100644 index 0000000000..79dc9181fd --- /dev/null +++ b/builtin/branch.c @@ -0,0 +1,815 @@ +/* + * Builtin "git branch" + * + * Copyright (c) 2006 Kristian Høgsberg <krh@redhat.com> + * Based on git-branch.sh by Junio C Hamano. + */ + +#include "cache.h" +#include "config.h" +#include "color.h" +#include "refs.h" +#include "commit.h" +#include "builtin.h" +#include "remote.h" +#include "parse-options.h" +#include "branch.h" +#include "diff.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" + +static const char * const builtin_branch_usage[] = { + N_("git branch [<options>] [-r | -a] [--merged | --no-merged]"), + N_("git branch [<options>] [-l] [-f] <branch-name> [<start-point>]"), + 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 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 */ +}; +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 +}; + +static struct string_list output = STRING_LIST_INIT_DUP; +static unsigned int colopts; + +static int parse_branch_color_slot(const char *slot) +{ + if (!strcasecmp(slot, "plain")) + return BRANCH_COLOR_PLAIN; + if (!strcasecmp(slot, "reset")) + return BRANCH_COLOR_RESET; + if (!strcasecmp(slot, "remote")) + return BRANCH_COLOR_REMOTE; + if (!strcasecmp(slot, "local")) + return BRANCH_COLOR_LOCAL; + if (!strcasecmp(slot, "current")) + return BRANCH_COLOR_CURRENT; + if (!strcasecmp(slot, "upstream")) + return BRANCH_COLOR_UPSTREAM; + return -1; +} + +static int git_branch_config(const char *var, const char *value, void *cb) +{ + const char *slot_name; + + 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 = parse_branch_color_slot(slot_name); + if (slot < 0) + return 0; + if (!value) + return config_error_nonbool(var); + return color_parse(value, branch_colors[slot]); + } + return git_color_default_config(var, value, 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.hash, NULL)) != NULL) + reference_rev = lookup_commit_reference(&oid); + } + if (!reference_rev) + reference_rev = head_rev; + + merged = in_merge_bases(rev, reference_rev); + + /* + * After the safety valve is fully redefined to "check with + * upstream, if any, otherwise with HEAD", we should just + * return the result of the 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) && + in_merge_bases(rev, head_rev) != 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(oid); + if (!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; + + switch (kinds) { + case FILTER_REFS_REMOTES: + fmt = "refs/remotes/%s"; + /* 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")); + } + + if (!force) { + head_rev = lookup_commit_reference(&head_oid); + if (!head_rev) + die(_("Couldn't look up commit object for HEAD")); + } + 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 struct worktree *wt = + find_shared_symref("HEAD", name); + if (wt) { + error(_("Cannot delete branch '%s' " + "checked out at '%s'"), + bname.buf, wt->path); + ret = 1; + continue; + } + } + + target = resolve_refdup(name, + RESOLVE_REF_READING + | RESOLVE_REF_NO_RECURSE + | RESOLVE_REF_ALLOW_BAD_NAME, + oid.hash, &flags); + if (!target) { + error(remote_branch + ? _("remote-tracking branch '%s' not found.") + : _("branch '%s' not found."), bname.buf); + ret = 1; + continue; + } + + if (!(flags & (REF_ISSYMREF|REF_ISBROKEN)) && + check_branch_commit(bname.buf, name, &oid, head_rev, kinds, + force)) { + ret = 1; + goto next; + } + + if (delete_ref(NULL, name, is_null_oid(&oid) ? NULL : oid.hash, + REF_NODEREF)) { + error(remote_branch + ? _("Error deleting remote-tracking branch '%s'") + : _("Error deleting branch '%s'"), + bname.buf); + ret = 1; + goto next; + } + if (!quiet) { + printf(remote_branch + ? _("Deleted remote-tracking branch %s (was %s).\n") + : _("Deleted branch %s (was %s).\n"), + bname.buf, + (flags & REF_ISBROKEN) ? "broken" + : (flags & REF_ISSYMREF) ? target + : find_unique_abbrev(oid.hash, DEFAULT_ABBREV)); + } + delete_branch_config(bname.buf); + + next: + free(target); + } + + 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 (*s) { + const char *ep = strchrnul(s, '%'); + if (s < ep) + strbuf_add(&buf, s, ep - s); + if (*ep == '%') { + strbuf_addstr(&buf, "%%"); + s = ep + 1; + } else { + s = ep; + } + } + 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) %s%%(end)", + branch_get_color(BRANCH_COLOR_CURRENT), + 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)%%(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) +{ + int i; + struct ref_array array; + int maxwidth = 0; + const char *remote_prefix = ""; + struct strbuf out = STRBUF_INIT; + 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 | FILTER_REFS_INCLUDE_BROKEN); + + 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")); + + ref_array_sort(sorting, &array); + + for (i = 0; i < array.nr; i++) { + format_ref_array_item(array.items[i], format, &out); + 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); + putchar('\n'); + } + strbuf_release(&out); + } + + ref_array_clear(&array); + free(to_free); +} + +static void reject_rebase_or_bisect_branch(const char *target) +{ + struct worktree **worktrees = get_worktrees(0); + 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); + } + + free_worktrees(worktrees); +} + +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; + int recovery = 0; + int clobber_head_ok; + + if (!oldname) { + if (copy) + die(_("cannot copy the current branch while not on any.")); + else + die(_("cannot rename the current branch while not on any.")); + } + + 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); + } + + /* + * A command like "git branch -M currentbranch currentbranch" cannot + * cause the worktree to become inconsistent with HEAD, so allow it. + */ + clobber_head_ok = !strcmp(oldname, newname); + + validate_new_branchname(newname, &newref, force, clobber_head_ok); + + reject_rebase_or_bisect_branch(oldref.buf); + + 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 && 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(_("Copied a misnamed branch '%s' away"), + oldref.buf + 11); + else + warning(_("Renamed a misnamed branch '%s' away"), + oldref.buf + 11); + } + + if (!copy && + replace_each_worktree_head_symref(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", oldref.buf + 11); + strbuf_release(&oldref); + strbuf_addf(&newsection, "branch.%s", newref.buf + 11); + strbuf_release(&newref); + if (!copy && git_config_rename_section(oldsection.buf, newsection.buf) < 0) + die(_("Branch is renamed, but update of config-file failed")); + if (copy && strcmp(oldname, newname) && git_config_copy_section(oldsection.buf, newsection.buf) < 0) + die(_("Branch is copied, but update of config-file failed")); + strbuf_release(&oldsection); + strbuf_release(&newsection); +} + +static GIT_PATH_FUNC(edit_description, "EDIT_DESCRIPTION") + +static int edit_branch_description(const char *branch_name) +{ + struct strbuf buf = STRBUF_INIT; + struct strbuf name = STRBUF_INIT; + + read_branch_desc(&buf, branch_name); + if (!buf.len || buf.buf[buf.len-1] != '\n') + strbuf_addch(&buf, '\n'); + strbuf_commented_addf(&buf, + _("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, 1); + + strbuf_addf(&name, "branch.%s.description", branch_name); + 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) +{ + int delete = 0, rename = 0, copy = 0, force = 0, list = 0; + int reflog = 0, edit_description = 0; + int quiet = 0, unset_upstream = 0; + const char *new_upstream = NULL; + enum branch_track track; + struct ref_filter filter; + int icase = 0; + static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting; + 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_SET_INT('t', "track", &track, N_("set up tracking mode (see git-pull(1))"), + BRANCH_TRACK_EXPLICIT), + { OPTION_SET_INT, 0, "set-upstream", &track, NULL, N_("do not use"), + PARSE_OPT_NOARG | PARSE_OPT_HIDDEN, NULL, BRANCH_TRACK_OVERRIDE }, + 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('r', "remotes", &filter.kind, N_("act on remote-tracking branches"), + FILTER_REFS_REMOTES), + 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('a', "all", &filter.kind, N_("list both remote-tracking and local branches"), + FILTER_REFS_REMOTES | FILTER_REFS_BRANCHES), + 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_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(0, "list", &list, N_("list branch names")), + OPT_BOOL('l', "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")), + 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_CALLBACK(0 , "sort", sorting_tail, N_("key"), + N_("field name to sort on"), &parse_opt_ref_sorting), + { + OPTION_CALLBACK, 0, "points-at", &filter.points_at, N_("object"), + N_("print only branches of the object"), 0, parse_opt_object_name + }, + OPT_BOOL('i', "ignore-case", &icase, N_("sorting and filtering are case insensitive")), + OPT_STRING( 0 , "format", &format.format, N_("format"), N_("format to use for the output")), + OPT_END(), + }; + + setup_ref_filter_porcelain_msg(); + + memset(&filter, 0, sizeof(filter)); + 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, NULL); + + track = git_branch_track; + + head = resolve_refdup("HEAD", 0, head_oid.hash, 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 && !unset_upstream && argc == 0) + list = 1; + + if (filter.with_commit || filter.merge != REF_FILTER_MERGED_NONE || filter.points_at.nr || + filter.no_commit) + list = 1; + + if (!!delete + !!rename + !!copy + !!new_upstream + + list + unset_upstream > 1) + usage_with_options(builtin_branch_usage, options); + + 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(_("--column and --verbose are incompatible")); + colopts = 0; + } + + if (force) { + delete *= 2; + rename *= 2; + copy *= 2; + } + + if (delete) { + if (!argc) + die(_("branch name required")); + return delete_branches(argc, argv, delete > 1, filter.kind, quiet); + } else if (list) { + /* git branch --local 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-tacking + * branches 'refs/remotes/...'. + */ + if (!sorting) + sorting = ref_default_sorting(); + sorting->ignore_case = icase; + print_ref_list(&filter, sorting, &format); + print_columns(&output, colopts, NULL); + string_list_clear(&output, 0); + return 0; + } + else if (edit_description) { + const char *branch_name; + struct strbuf branch_ref = STRBUF_INIT; + + if (!argc) { + if (filter.detached) + die(_("Cannot give description to detached HEAD")); + branch_name = head; + } else if (argc == 1) + branch_name = argv[0]; + 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)) { + strbuf_release(&branch_ref); + + if (!argc) + return error(_("No commit on branch '%s' yet."), + branch_name); + else + return error(_("No branch named '%s'."), + branch_name); + } + strbuf_release(&branch_ref); + + if (edit_branch_description(branch_name)) + return 1; + } else if (copy) { + if (!argc) + die(_("branch name required")); + else if (argc == 1) + copy_or_rename_branch(head, argv[0], 1, copy > 1); + else if (argc == 2) + copy_or_rename_branch(argv[0], argv[1], 1, copy > 1); + else + die(_("too many branches for a copy operation")); + } else if (rename) { + if (!argc) + die(_("branch name required")); + else if (argc == 1) + copy_or_rename_branch(head, argv[0], 0, rename > 1); + else if (argc == 2) + copy_or_rename_branch(argv[0], argv[1], 0, rename > 1); + else + die(_("too many arguments for a rename operation")); + } else if (new_upstream) { + struct branch *branch = branch_get(argv[0]); + + if (argc > 1) + 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)) + die(_("branch '%s' does not exist"), branch->name); + + /* + * create_branch takes care of setting up the tracking + * info and making sure new_upstream is correct + */ + create_branch(branch->name, new_upstream, 0, 0, 0, quiet, BRANCH_TRACK_OVERRIDE); + } else if (unset_upstream) { + struct branch *branch = branch_get(argv[0]); + struct strbuf buf = STRBUF_INIT; + + if (argc > 1) + 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_addf(&buf, "branch.%s.remote", branch->name); + git_config_set_multivar(buf.buf, NULL, NULL, 1); + strbuf_reset(&buf); + strbuf_addf(&buf, "branch.%s.merge", branch->name); + git_config_set_multivar(buf.buf, NULL, NULL, 1); + strbuf_release(&buf); + } else if (argc > 0 && argc <= 2) { + struct branch *branch = branch_get(argv[0]); + + if (!strcmp(argv[0], "HEAD")) + die(_("it does not make sense to create 'HEAD' manually")); + + if (!branch) + die(_("no such branch '%s'"), argv[0]); + + if (filter.kind != FILTER_REFS_BRANCHES) + die(_("-a and -r options to 'git branch' do not make sense with a branch name")); + + if (track == BRANCH_TRACK_OVERRIDE) + die(_("the '--set-upstream' option is no longer supported. Please use '--track' or '--set-upstream-to' instead.")); + + create_branch(argv[0], (argc == 2) ? argv[1] : head, + force, reflog, 0, quiet, track); + + } else + usage_with_options(builtin_branch_usage, options); + + return 0; +} diff --git a/builtin/bundle.c b/builtin/bundle.c new file mode 100644 index 0000000000..d0de59b94f --- /dev/null +++ b/builtin/bundle.c @@ -0,0 +1,67 @@ +#include "builtin.h" +#include "cache.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". + */ + +static const char builtin_bundle_usage[] = + "git bundle create <file> <git-rev-list args>\n" + " or: git bundle verify <file>\n" + " or: git bundle list-heads <file> [<refname>...]\n" + " or: git bundle unbundle <file> [<refname>...]"; + +int cmd_bundle(int argc, const char **argv, const char *prefix) +{ + struct bundle_header header; + const char *cmd, *bundle_file; + int bundle_fd = -1; + + if (argc < 3) + usage(builtin_bundle_usage); + + cmd = argv[1]; + bundle_file = prefix_filename(prefix, argv[2]); + argc -= 2; + argv += 2; + + memset(&header, 0, sizeof(header)); + if (strcmp(cmd, "create") && (bundle_fd = + read_bundle_header(bundle_file, &header)) < 0) + return 1; + + if (!strcmp(cmd, "verify")) { + close(bundle_fd); + if (argc != 1) { + usage(builtin_bundle_usage); + return 1; + } + if (verify_bundle(&header, 1)) + return 1; + fprintf(stderr, _("%s is okay\n"), bundle_file); + return 0; + } + if (!strcmp(cmd, "list-heads")) { + close(bundle_fd); + return !!list_bundle_refs(&header, argc, argv); + } + if (!strcmp(cmd, "create")) { + if (argc < 2) { + usage(builtin_bundle_usage); + return 1; + } + if (!startup_info->have_repository) + die(_("Need a repository to create a bundle.")); + return !!create_bundle(&header, bundle_file, argc, argv); + } else if (!strcmp(cmd, "unbundle")) { + if (!startup_info->have_repository) + die(_("Need a repository to unbundle.")); + return !!unbundle(&header, bundle_fd, 0) || + list_bundle_refs(&header, argc, argv); + } else + usage(builtin_bundle_usage); +} diff --git a/builtin/cat-file.c b/builtin/cat-file.c new file mode 100644 index 0000000000..f5fa4fd75a --- /dev/null +++ b/builtin/cat-file.c @@ -0,0 +1,638 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" +#include "config.h" +#include "builtin.h" +#include "diff.h" +#include "parse-options.h" +#include "userdiff.h" +#include "streaming.h" +#include "tree-walk.h" +#include "sha1-array.h" +#include "packfile.h" + +struct batch_options { + int enabled; + int follow_symlinks; + int print_contents; + int buffer_output; + int all_objects; + int cmdmode; /* may be 'w' or 'c' for --filters or --textconv */ + const char *format; +}; + +static const char *force_path; + +static int filter_object(const char *path, unsigned mode, + const struct object_id *oid, + char **buf, unsigned long *size) +{ + enum object_type type; + + *buf = read_sha1_file(oid->hash, &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; + if (convert_to_working_tree(path, *buf, *size, &strbuf)) { + free(*buf); + *size = strbuf.len; + *buf = strbuf_detach(&strbuf, NULL); + } + } + + return 0; +} + +static int cat_one_file(int opt, const char *exp_type, const char *obj_name, + int unknown_type) +{ + 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; + const char *path = force_path; + + if (unknown_type) + flags |= OBJECT_INFO_ALLOW_UNKNOWN_TYPE; + + if (get_oid_with_context(obj_name, GET_OID_RECORD_PATH, + &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.typename = &sb; + if (sha1_object_info_extended(oid.hash, &oi, flags) < 0) + die("git cat-file: could not get object info"); + if (sb.len) { + printf("%s\n", sb.buf); + strbuf_release(&sb); + return 0; + } + break; + + case 's': + oi.sizep = &size; + if (sha1_object_info_extended(oid.hash, &oi, flags) < 0) + die("git cat-file: could not get object info"); + printf("%lu\n", size); + return 0; + + case 'e': + return !has_object_file(&oid); + + case 'w': + if (!path) + die("git cat-file --filters %s: <object> must be " + "<sha1:path>", obj_name); + + if (filter_object(path, obj_context.mode, + &oid, &buf, &size)) + return -1; + break; + + case 'c': + if (!path) + die("git cat-file --textconv %s: <object> must be <sha1:path>", + obj_name); + + if (textconv_object(path, obj_context.mode, &oid, 1, &buf, &size)) + break; + /* else fallthrough */ + + case 'p': + type = sha1_object_info(oid.hash, 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; + return cmd_ls_tree(2, ls_args, NULL); + } + + if (type == OBJ_BLOB) + return stream_blob_to_fd(1, &oid, NULL, 0); + buf = read_sha1_file(oid.hash, &type, &size); + if (!buf) + die("Cannot read object %s", obj_name); + + /* otherwise just spit out the data */ + break; + + case 0: + if (type_from_string(exp_type) == OBJ_BLOB) { + struct object_id blob_oid; + if (sha1_object_info(oid.hash, NULL) == OBJ_TAG) { + char *buffer = read_sha1_file(oid.hash, &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 (sha1_object_info(blob_oid.hash, NULL) == OBJ_BLOB) + return stream_blob_to_fd(1, &blob_oid, NULL, 0); + /* + * 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(oid.hash, exp_type, &size, NULL); + 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); + free(buf); + free(obj_context.path); + return 0; +} + +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 sha1_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 sha1_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, + void *vdata) +{ + struct expand_data *data = vdata; + + 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, typename(data->type)); + } else if (is_atom("objectsize", atom, len)) { + if (data->mark_query) + data->info.sizep = &data->size; + else + strbuf_addf(sb, "%lu", 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_sha1 = data->delta_base_oid.hash; + else + strbuf_addstr(sb, + oid_to_hex(&data->delta_base_oid)); + } else + die("unknown format element: %.*s", len, atom); +} + +static size_t expand_format(struct strbuf *sb, const char *start, void *data) +{ + const char *end; + + if (*start != '(') + return 0; + end = strchr(start + 1, ')'); + if (!end) + die("format element '%s' does not end in ')'", start); + + expand_atom(sb, start + 1, end - start - 1, data); + + return end - start + 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->cmdmode) { + char *contents; + unsigned long size; + + if (!data->rest) + die("missing path for '%s'", oid_to_hex(oid)); + + if (opt->cmdmode == '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->cmdmode == 'c') { + enum object_type type; + if (!textconv_object(data->rest, 0100644, oid, + 1, &contents, &size)) + contents = read_sha1_file(oid->hash, &type, + &size); + if (!contents) + die("could not convert '%s' %s", + oid_to_hex(oid), data->rest); + } else + die("BUG: invalid cmdmode: %c", opt->cmdmode); + batch_write(opt, contents, size); + free(contents); + } else if (stream_blob_to_fd(1, oid, NULL, 0) < 0) + die("unable to stream %s to stdout", oid_to_hex(oid)); + } + else { + enum object_type type; + unsigned long size; + void *contents; + + contents = read_sha1_file(oid->hash, &type, &size); + 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) + die("object %s changed size!?", oid_to_hex(oid)); + + batch_write(opt, contents, size); + free(contents); + } +} + +static void batch_object_write(const char *obj_name, struct batch_options *opt, + struct expand_data *data) +{ + struct strbuf buf = STRBUF_INIT; + + if (!data->skip_object_info && + sha1_object_info_extended(data->oid.hash, &data->info, + OBJECT_INFO_LOOKUP_REPLACE) < 0) { + printf("%s missing\n", + obj_name ? obj_name : oid_to_hex(&data->oid)); + fflush(stdout); + return; + } + + strbuf_expand(&buf, opt->format, expand_format, data); + strbuf_addch(&buf, '\n'); + batch_write(opt, buf.buf, buf.len); + strbuf_release(&buf); + + if (opt->print_contents) { + print_object_or_die(opt, data); + batch_write(opt, "\n", 1); + } +} + +static void batch_one_object(const char *obj_name, struct batch_options *opt, + struct expand_data *data) +{ + struct object_context ctx; + int flags = opt->follow_symlinks ? GET_OID_FOLLOW_SYMLINKS : 0; + enum follow_symlinks_result result; + + result = get_oid_with_context(obj_name, flags, &data->oid, &ctx); + if (result != FOUND) { + switch (result) { + case MISSING_OBJECT: + printf("%s missing\n", obj_name); + break; + case DANGLING_SYMLINK: + printf("dangling %"PRIuMAX"\n%s\n", + (uintmax_t)strlen(obj_name), obj_name); + break; + case SYMLINK_LOOP: + printf("loop %"PRIuMAX"\n%s\n", + (uintmax_t)strlen(obj_name), obj_name); + break; + case NOT_DIR: + printf("notdir %"PRIuMAX"\n%s\n", + (uintmax_t)strlen(obj_name), obj_name); + break; + default: + die("BUG: unknown get_sha1_with_context result %d\n", + result); + break; + } + fflush(stdout); + return; + } + + if (ctx.mode == 0) { + printf("symlink %"PRIuMAX"\n%s\n", + (uintmax_t)ctx.symlink_path.len, + ctx.symlink_path.buf); + fflush(stdout); + return; + } + + batch_object_write(obj_name, opt, data); +} + +struct object_cb_data { + struct batch_options *opt; + struct expand_data *expand; +}; + +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->opt, data->expand); + return 0; +} + +static int batch_loose_object(const struct object_id *oid, + const char *path, + void *data) +{ + oid_array_append(data, oid); + return 0; +} + +static int batch_packed_object(const struct object_id *oid, + struct packed_git *pack, + uint32_t pos, + void *data) +{ + oid_array_append(data, oid); + return 0; +} + +static int batch_objects(struct batch_options *opt) +{ + struct strbuf buf = STRBUF_INIT; + struct expand_data data; + int save_warning; + int retval = 0; + + if (!opt->format) + opt->format = "%(objectname) %(objecttype) %(objectsize)"; + + /* + * Expand once with our special mark_query flag, which will prime the + * object_info to be handed to sha1_object_info_extended for each + * object. + */ + memset(&data, 0, sizeof(data)); + data.mark_query = 1; + strbuf_expand(&buf, opt->format, expand_format, &data); + data.mark_query = 0; + if (opt->cmdmode) + data.split_on_whitespace = 1; + + if (opt->all_objects) { + struct object_info empty = OBJECT_INFO_INIT; + if (!memcmp(&data.info, &empty, sizeof(empty))) + data.skip_object_info = 1; + } + + /* + * 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->print_contents) + data.info.typep = &data.type; + + if (opt->all_objects) { + struct oid_array sa = OID_ARRAY_INIT; + struct object_cb_data cb; + + for_each_loose_object(batch_loose_object, &sa, 0); + for_each_packed_object(batch_packed_object, &sa, 0); + + cb.opt = opt; + cb.expand = &data; + oid_array_for_each_unique(&sa, batch_object_cb, &cb); + + oid_array_clear(&sa); + 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; + + while (strbuf_getline(&buf, stdin) != 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(buf.buf, " \t"); + if (p) { + while (*p && strchr(" \t", *p)) + *p++ = '\0'; + } + data.rest = p; + } + + batch_one_object(buf.buf, opt, &data); + } + + strbuf_release(&buf); + warn_on_object_refname_ambiguity = save_warning; + return retval; +} + +static const char * const cat_file_usage[] = { + N_("git cat-file (-t [--allow-unknown-type] | -s [--allow-unknown-type] | -e | -p | <type> | --textconv | --filters) [--path=<path>] <object>"), + N_("git cat-file (--batch | --batch-check) [--follow-symlinks] [--textconv | --filters]"), + NULL +}; + +static int git_cat_file_config(const char *var, const char *value, void *cb) +{ + if (userdiff_config(var, value) < 0) + return -1; + + return git_default_config(var, value, cb); +} + +static int batch_option_callback(const struct option *opt, + const char *arg, + int unset) +{ + struct batch_options *bo = opt->value; + + if (bo->enabled) { + return 1; + } + + bo->enabled = 1; + bo->print_contents = !strcmp(opt->long_name, "batch"); + bo->format = arg; + + return 0; +} + +int cmd_cat_file(int argc, const char **argv, const char *prefix) +{ + int opt = 0; + const char *exp_type = NULL, *obj_name = NULL; + struct batch_options batch = {0}; + int unknown_type = 0; + + const struct option options[] = { + OPT_GROUP(N_("<type> can be one of: blob, tree, commit, tag")), + OPT_CMDMODE('t', NULL, &opt, N_("show object type"), 't'), + OPT_CMDMODE('s', NULL, &opt, N_("show object size"), 's'), + OPT_CMDMODE('e', NULL, &opt, + N_("exit with zero when there's no error"), 'e'), + OPT_CMDMODE('p', NULL, &opt, N_("pretty-print object's content"), 'p'), + OPT_CMDMODE(0, "textconv", &opt, + N_("for blob objects, run textconv on object's content"), 'c'), + OPT_CMDMODE(0, "filters", &opt, + N_("for blob objects, run filters on object's content"), 'w'), + OPT_STRING(0, "path", &force_path, N_("blob"), + N_("use a specific path for --textconv/--filters")), + OPT_BOOL(0, "allow-unknown-type", &unknown_type, + N_("allow -s and -t to work with broken/corrupt objects")), + OPT_BOOL(0, "buffer", &batch.buffer_output, N_("buffer --batch output")), + { OPTION_CALLBACK, 0, "batch", &batch, "format", + N_("show info and content of objects fed from the standard input"), + PARSE_OPT_OPTARG, batch_option_callback }, + { OPTION_CALLBACK, 0, "batch-check", &batch, "format", + N_("show info about objects fed from the standard input"), + PARSE_OPT_OPTARG, batch_option_callback }, + OPT_BOOL(0, "follow-symlinks", &batch.follow_symlinks, + N_("follow in-tree symlinks (used with --batch or --batch-check)")), + OPT_BOOL(0, "batch-all-objects", &batch.all_objects, + N_("show all objects with --batch or --batch-check")), + OPT_END() + }; + + git_config(git_cat_file_config, NULL); + + batch.buffer_output = -1; + argc = parse_options(argc, argv, prefix, options, cat_file_usage, 0); + + if (opt) { + if (batch.enabled && (opt == 'c' || opt == 'w')) + batch.cmdmode = opt; + else if (argc == 1) + obj_name = argv[0]; + else + usage_with_options(cat_file_usage, options); + } + if (!opt && !batch.enabled) { + if (argc == 2) { + exp_type = argv[0]; + obj_name = argv[1]; + } else + usage_with_options(cat_file_usage, options); + } + if (batch.enabled) { + if (batch.cmdmode != opt || argc) + usage_with_options(cat_file_usage, options); + if (batch.cmdmode && batch.all_objects) + die("--batch-all-objects cannot be combined with " + "--textconv nor with --filters"); + } + + if ((batch.follow_symlinks || batch.all_objects) && !batch.enabled) { + usage_with_options(cat_file_usage, options); + } + + if (force_path && opt != 'c' && opt != 'w') { + error("--path=<path> needs --textconv or --filters"); + usage_with_options(cat_file_usage, options); + } + + if (force_path && batch.enabled) { + error("--path=<path> incompatible with --batch"); + usage_with_options(cat_file_usage, options); + } + + if (batch.buffer_output < 0) + batch.buffer_output = batch.all_objects; + + if (batch.enabled) + return batch_objects(&batch); + + 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 0000000000..91444dc044 --- /dev/null +++ b/builtin/check-attr.c @@ -0,0 +1,189 @@ +#include "builtin.h" +#include "cache.h" +#include "config.h" +#include "attr.h" +#include "quote.h" +#include "parse-options.h" + +static int all_attrs; +static int cached_attrs; +static int stdin_paths; +static const char * const check_attr_usage[] = { +N_("git check-attr [-a | --all | <attr>...] [--] <pathname>..."), +N_("git check-attr --stdin [-z] [-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_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(full_path, check); + } else { + if (git_check_attr(full_path, check)) + die("git_check_attr died"); + } + 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; + 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); + + if (read_cache() < 0) { + die("invalid cache"); + } + + if (cached_attrs) + git_attr_set_direction(GIT_ATTR_INDEX, NULL); + + 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 (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 0000000000..3e280b9c7a --- /dev/null +++ b/builtin/check-ignore.c @@ -0,0 +1,191 @@ +#include "builtin.h" +#include "cache.h" +#include "config.h" +#include "dir.h" +#include "quote.h" +#include "pathspec.h" +#include "parse-options.h" +#include "submodule.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_exclude(const char *path, struct exclude *exclude) +{ + char *bang = (exclude && exclude->flags & EXC_FLAG_NEGATIVE) ? "!" : ""; + char *slash = (exclude && exclude->flags & EXC_FLAG_MUSTBEDIR) ? "/" : ""; + if (!nul_term_line) { + if (!verbose) { + write_name_quoted(path, stdout, '\n'); + } else { + if (exclude) { + quote_c_style(exclude->el->src, NULL, stdout, 0); + printf(":%d:%s%s%s\t", + exclude->srcpos, + bang, exclude->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 (exclude) + printf("%s%c%d%c%s%s%s%c%s%c", + exclude->el->src, '\0', + exclude->srcpos, '\0', + bang, exclude->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, dtype = DT_UNKNOWN, i; + struct exclude *exclude; + 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); + for (i = 0; i < pathspec.nr; i++) { + full_path = pathspec.items[i].match; + exclude = NULL; + if (!seen[i]) { + exclude = last_exclude_matching(dir, &the_index, + full_path, &dtype); + } + if (!quiet && (exclude || show_non_matching)) + output_exclude(pathspec.items[i].original, exclude); + if (exclude) + num_ignored++; + } + free(seen); + + 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; + + 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 && read_cache() < 0) + die(_("index file corrupt")); + + memset(&dir, 0, sizeof(dir)); + 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"); + } + + clear_directory(&dir); + + return !num_ignored; +} diff --git a/builtin/check-mailmap.c b/builtin/check-mailmap.c new file mode 100644 index 0000000000..cdce144f3b --- /dev/null +++ b/builtin/check-mailmap.c @@ -0,0 +1,67 @@ +#include "builtin.h" +#include "config.h" +#include "mailmap.h" +#include "parse-options.h" +#include "string-list.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, NULL); + + 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 0000000000..6c40ff110b --- /dev/null +++ b/builtin/check-ref-format.c @@ -0,0 +1,89 @@ +/* + * GIT - The information manager from hell + */ + +#include "cache.h" +#include "refs.h" +#include "builtin.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; + int nongit; + + setup_git_directory_gently(&nongit); + if (strbuf_check_branch_ref(&sb, arg)) + die("'%s' is not a valid branch name", arg); + printf("%s\n", sb.buf + 11); + 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; + + 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 = collapse_slashes(refname); + if (check_refname_format(refname, flags)) + return 1; + if (normalize) + printf("%s\n", refname); + + return 0; +} diff --git a/builtin/checkout-index.c b/builtin/checkout-index.c new file mode 100644 index 0000000000..39c8be05dc --- /dev/null +++ b/builtin/checkout-index.c @@ -0,0 +1,258 @@ +/* + * Check-out files from the "current cache directory" + * + * Copyright (C) 2005 Linus Torvalds + * + */ +#include "builtin.h" +#include "config.h" +#include "lockfile.h" +#include "quote.h" +#include "cache-tree.h" +#include "parse-options.h" + +#define CHECKOUT_ALL 4 +static int nul_term_line; +static int checkout_stage; /* default to checkout stage0 */ +static int to_tempfile; +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; + + if (CHECKOUT_ALL == checkout_stage) { + for (i = 1; i < 4; i++) { + if (i > 1) + putchar(' '); + if (topath[i][0]) + fputs(topath[i], stdout); + else + putchar('.'); + } + } else + fputs(topath[checkout_stage], stdout); + + 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 = cache_name_pos(name, namelen); + int has_same_name = 0; + int did_checkout = 0; + int errs = 0; + + if (pos < 0) + pos = -pos - 1; + + while (pos < active_nr) { + struct cache_entry *ce = active_cache[pos]; + if (ce_namelen(ce) != namelen || + memcmp(ce->name, name, namelen)) + break; + has_same_name = 1; + pos++; + 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) < 0) + errs++; + } + + if (did_checkout) { + if (to_tempfile) + write_tempfile_record(name, prefix); + return errs > 0 ? -1 : 0; + } + + if (!state.quiet) { + fprintf(stderr, "git checkout-index: %s ", name); + if (!has_same_name) + fprintf(stderr, "is not in the cache"); + 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 void checkout_all(const char *prefix, int prefix_length) +{ + int i, errs = 0; + struct cache_entry *last_ce = NULL; + + for (i = 0; i < active_nr ; i++) { + struct cache_entry *ce = active_cache[i]; + 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) < 0) + errs++; + last_ce = ce; + } + if (last_ce && to_tempfile) + write_tempfile_record(last_ce->name, prefix); + if (errs) + /* we have already done our error reporting. + * exit with the same code as die(). + */ + exit(128); +} + +static const char * const builtin_checkout_index_usage[] = { + N_("git checkout-index [<options>] [--] [<file>...]"), + NULL +}; + +static struct lock_file lock_file; + +static int option_parse_stage(const struct option *opt, + const char *arg, int unset) +{ + if (!strcmp(arg, "all")) { + to_tempfile = 1; + checkout_stage = CHECKOUT_ALL; + } else { + int ch = arg[0]; + if ('1' <= ch && ch <= '3') + checkout_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; + int newfd = -1; + int all = 0; + int read_from_stdin = 0; + int prefix_length; + int force = 0, quiet = 0, not_new = 0; + int index_opt = 0; + struct option builtin_checkout_index_options[] = { + OPT_BOOL('a', "all", &all, + N_("check out all files in the index")), + OPT__FORCE(&force, N_("force overwrite of existing files")), + 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>")), + { OPTION_CALLBACK, 0, "stage", NULL, "1-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; + + if (read_cache() < 0) { + die("invalid cache"); + } + + argc = parse_options(argc, argv, prefix, builtin_checkout_index_options, + builtin_checkout_index_usage, 0); + 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); + + /* + * 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; + newfd = hold_locked_index(&lock_file, LOCK_DIE_ON_ERROR); + } + + /* 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); + 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); + checkout_file(p, prefix); + free(p); + } + strbuf_release(&unquoted); + strbuf_release(&buf); + } + + if (all) + checkout_all(prefix, prefix_length); + + if (0 <= newfd && + 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 0000000000..fc4f8fd2ea --- /dev/null +++ b/builtin/checkout.c @@ -0,0 +1,1305 @@ +#include "builtin.h" +#include "config.h" +#include "lockfile.h" +#include "parse-options.h" +#include "refs.h" +#include "commit.h" +#include "tree.h" +#include "tree-walk.h" +#include "cache-tree.h" +#include "unpack-trees.h" +#include "dir.h" +#include "run-command.h" +#include "merge-recursive.h" +#include "branch.h" +#include "diff.h" +#include "revision.h" +#include "remote.h" +#include "blob.h" +#include "xdiff-interface.h" +#include "ll-merge.h" +#include "resolve-undo.h" +#include "submodule-config.h" +#include "submodule.h" + +static const char * const checkout_usage[] = { + N_("git checkout [<options>] <branch>"), + N_("git checkout [<options>] [<branch>] -- <file>..."), + NULL, +}; + +struct checkout_opts { + int patch_mode; + int quiet; + int merge; + int force; + int force_detach; + int writeout_stage; + int overwrite_ignore; + int ignore_skipworktree; + int ignore_other_worktrees; + int show_progress; + + 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; + + int branch_exists; + const char *prefix; + struct pathspec pathspec; + struct tree *source_tree; +}; + +static int post_checkout_hook(struct commit *old, struct commit *new, + int changed) +{ + return run_hook_le(NULL, "post-checkout", + oid_to_hex(old ? &old->object.oid : &null_oid), + oid_to_hex(new ? &new->object.oid : &null_oid), + changed ? "1" : "0", NULL); + /* "new" can be NULL when checking out from the index before + a commit exists. */ + +} + +static int update_some(const unsigned char *sha1, struct strbuf *base, + const char *pathname, unsigned mode, int stage, void *context) +{ + int len; + struct cache_entry *ce; + int pos; + + if (S_ISDIR(mode)) + return READ_TREE_RECURSIVE; + + len = base->len + strlen(pathname); + ce = xcalloc(1, cache_entry_size(len)); + hashcpy(ce->oid.hash, sha1); + 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 = cache_name_pos(ce->name, ce->ce_namelen); + if (pos >= 0) { + struct cache_entry *old = active_cache[pos]; + if (ce->ce_mode == old->ce_mode && + !oidcmp(&ce->oid, &old->oid)) { + old->ce_flags |= CE_UPDATE; + free(ce); + return 0; + } + } + + add_cache_entry(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_recursive(tree, "", 0, 0, 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 < active_nr && + !strcmp(active_cache[pos]->name, ce->name)) + ; /* skip */ + return pos; +} + +static int check_stage(int stage, const struct cache_entry *ce, int pos) +{ + while (pos < active_nr && + !strcmp(active_cache[pos]->name, ce->name)) { + if (ce_stage(active_cache[pos]) == stage) + return 0; + pos++; + } + 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 < active_nr) { + ce = active_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) +{ + while (pos < active_nr && + !strcmp(active_cache[pos]->name, ce->name)) { + if (ce_stage(active_cache[pos]) == stage) + return checkout_entry(active_cache[pos], state, NULL); + pos++; + } + 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) +{ + struct cache_entry *ce = active_cache[pos]; + const char *path = ce->name; + mmfile_t ancestor, ours, theirs; + int status; + struct object_id oid; + mmbuffer_t result_buf; + struct object_id threeway[3]; + unsigned mode = 0; + + memset(threeway, 0, sizeof(threeway)); + while (pos < active_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 = active_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]); + + /* + * NEEDSWORK: re-create conflicts from merges with + * merge.renormalize set, too + */ + status = ll_merge(&result_buf, path, &ancestor, "base", + &ours, "ours", &theirs, "theirs", NULL); + free(ancestor.ptr); + free(ours.ptr); + free(theirs.ptr); + if (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_sha1_file(result_buf.ptr, result_buf.size, + blob_type, oid.hash)) + die(_("Unable to add merge result for '%s'"), path); + free(result_buf.ptr); + ce = make_cache_entry(mode, oid.hash, path, 2, 0); + if (!ce) + die(_("make_cache_entry failed for path '%s'"), path); + status = checkout_entry(ce, state, NULL); + free(ce); + return status; +} + +static int checkout_paths(const struct checkout_opts *opts, + const char *revision) +{ + int pos; + struct checkout state = CHECKOUT_INIT; + static char *ps_matched; + struct object_id rev; + struct commit *head; + int errs = 0; + struct lock_file *lock_file; + + 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->force && opts->patch_mode) + die(_("'%s' cannot be used with updating paths"), "-f"); + + if (opts->force_detach) + die(_("'%s' cannot be used with updating paths"), "--detach"); + + if (opts->merge && opts->patch_mode) + die(_("'%s' cannot be used with %s"), "--merge", "--patch"); + + if (opts->force && opts->merge) + die(_("'%s' cannot be used with %s"), "-f", "-m"); + + if (opts->new_branch) + die(_("Cannot update paths and switch to branch '%s' at the same time."), + opts->new_branch); + + if (opts->patch_mode) + return run_add_interactive(revision, "--patch=checkout", + &opts->pathspec); + + lock_file = xcalloc(1, sizeof(struct lock_file)); + + hold_locked_index(lock_file, LOCK_DIE_ON_ERROR); + if (read_cache_preload(&opts->pathspec) < 0) + return error(_("index file corrupt")); + + if (opts->source_tree) + read_tree_some(opts->source_tree, &opts->pathspec); + + ps_matched = xcalloc(opts->pathspec.nr, 1); + + /* + * Make sure all pathspecs participated in locating the paths + * to be checked out. + */ + for (pos = 0; pos < active_nr; pos++) { + struct cache_entry *ce = active_cache[pos]; + ce->ce_flags &= ~CE_MATCHED; + if (!opts->ignore_skipworktree && ce_skip_worktree(ce)) + continue; + if (opts->source_tree && !(ce->ce_flags & CE_UPDATE)) + /* + * "git checkout tree-ish -- path", but this entry + * is in the original index; it will not be checked + * out to the working tree and it does not matter + * if pathspec matched this entry. We will not do + * anything to this entry at all. + */ + continue; + /* + * 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_recursive (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(ce, &opts->pathspec, ps_matched)) + ce->ce_flags |= CE_MATCHED; + } + + if (report_path_error(ps_matched, &opts->pathspec, opts->prefix)) { + free(ps_matched); + return 1; + } + free(ps_matched); + + /* "checkout -m path" to recreate conflicted state */ + if (opts->merge) + unmerge_marked_index(&the_index); + + /* Any unmerged paths? */ + for (pos = 0; pos < active_nr; pos++) { + const struct cache_entry *ce = active_cache[pos]; + if (ce->ce_flags & CE_MATCHED) { + if (!ce_stage(ce)) + continue; + if (opts->force) { + warning(_("path '%s' is unmerged"), ce->name); + } else if (opts->writeout_stage) { + errs |= check_stage(opts->writeout_stage, ce, pos); + } 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 */ + state.force = 1; + state.refresh_cache = 1; + state.istate = &the_index; + + enable_delayed_checkout(&state); + for (pos = 0; pos < active_nr; pos++) { + struct cache_entry *ce = active_cache[pos]; + if (ce->ce_flags & CE_MATCHED) { + if (!ce_stage(ce)) { + errs |= checkout_entry(ce, &state, NULL); + continue; + } + if (opts->writeout_stage) + errs |= checkout_stage(opts->writeout_stage, ce, pos, &state); + else if (opts->merge) + errs |= checkout_merged(pos, &state); + pos = skip_same_name(ce, pos) - 1; + } + } + errs |= finish_delayed_checkout(&state); + + if (write_locked_index(&the_index, lock_file, COMMIT_LOCK)) + die(_("unable to write new index file")); + + read_ref_full("HEAD", 0, rev.hash, NULL); + head = lookup_commit_reference_gently(&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. */ + init_revisions(&rev, NULL); + rev.diffopt.flags = opts->flags; + rev.diffopt.output_format |= DIFF_FORMAT_NAME_STATUS; + diff_setup_done(&rev.diffopt); + add_pending_object(&rev, head, NULL); + run_diff_index(&rev, 0); +} + +static void describe_detached_head(const char *msg, struct commit *commit) +{ + struct strbuf sb = STRBUF_INIT; + if (!parse_commit(commit)) + pp_commit_easy(CMIT_FMT_ONELINE, commit, &sb); + fprintf(stderr, "%s %s... %s\n", msg, + find_unique_abbrev(commit->object.oid.hash, 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 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 = 1; + opts.merge = 1; + opts.fn = oneway_merge; + opts.verbose_update = o->show_progress; + opts.src_index = &the_index; + opts.dst_index = &the_index; + 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; + } +} + +struct branch_info { + const char *name; /* The short name used */ + const char *path; /* The full name of a real branch */ + struct commit *commit; /* The named commit */ + /* + * if not null the branch is detached because it's already + * checked out in this checkout + */ + char *checkout; +}; + +static void setup_branch_path(struct branch_info *branch) +{ + struct strbuf buf = STRBUF_INIT; + + strbuf_branchname(&buf, branch->name, INTERPRET_BRANCH_LOCAL); + if (strcmp(buf.buf, branch->name)) + branch->name = xstrdup(buf.buf); + strbuf_splice(&buf, 0, 0, "refs/heads/", 11); + branch->path = strbuf_detach(&buf, NULL); +} + +static int merge_working_tree(const struct checkout_opts *opts, + struct branch_info *old, + struct branch_info *new, + int *writeout_error) +{ + int ret; + struct lock_file *lock_file = xcalloc(1, sizeof(struct lock_file)); + + hold_locked_index(lock_file, LOCK_DIE_ON_ERROR); + if (read_cache_preload(NULL) < 0) + return error(_("index file corrupt")); + + resolve_undo_clear(); + if (opts->force) { + ret = reset_tree(new->commit->tree, opts, 1, writeout_error); + if (ret) + return ret; + } else { + struct tree_desc trees[2]; + struct tree *tree; + struct unpack_trees_options topts; + + 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"); + + refresh_cache(REFRESH_QUIET); + + if (unmerged_cache()) { + error(_("you need to resolve your current index first")); + return 1; + } + + /* 2-way merge to the new branch */ + topts.initial_checkout = is_cache_unborn(); + topts.update = 1; + topts.merge = 1; + topts.gently = opts->merge && old->commit; + topts.verbose_update = opts->show_progress; + topts.fn = twoway_merge; + if (opts->overwrite_ignore) { + topts.dir = xcalloc(1, sizeof(*topts.dir)); + topts.dir->flags |= DIR_SHOW_IGNORED; + setup_standard_excludes(topts.dir); + } + tree = parse_tree_indirect(old->commit ? + &old->commit->object.oid : + &empty_tree_oid); + init_tree_desc(&trees[0], tree->buffer, tree->size); + tree = parse_tree_indirect(&new->commit->object.oid); + init_tree_desc(&trees[1], tree->buffer, tree->size); + + ret = unpack_trees(2, trees, &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 *result; + struct tree *work; + struct merge_options o; + if (!opts->merge) + return 1; + + /* + * Without old->commit, the below is the same as + * the two-tree unpack we already tried and failed. + */ + if (!old->commit) + return 1; + + /* 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(NULL, NULL, 0); + /* + * NEEDSWORK: carrying over local changes + * when branches have different end-of-line + * normalization (or clean+smudge rules) is + * a pain; plumb in an option to set + * o.renormalize? + */ + init_merge_options(&o); + o.verbosity = 0; + work = write_tree_from_memory(&o); + + ret = reset_tree(new->commit->tree, opts, 1, + writeout_error); + if (ret) + return ret; + o.ancestor = old->name; + o.branch1 = new->name; + o.branch2 = "local"; + ret = merge_trees(&o, new->commit->tree, work, + old->commit->tree, &result); + if (ret < 0) + exit(128); + ret = reset_tree(new->commit->tree, opts, 0, + writeout_error); + strbuf_release(&o.obuf); + if (ret) + return ret; + } + } + + if (!active_cache_tree) + active_cache_tree = cache_tree(); + + if (!cache_tree_fully_valid(active_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->force && !opts->quiet) + show_local_changes(&new->commit->object, &opts->diff_options); + + return 0; +} + +static void report_tracking(struct branch_info *new) +{ + struct strbuf sb = STRBUF_INIT; + struct branch *branch = branch_get(new->name); + + if (!format_tracking_info(branch, &sb)) + return; + fputs(sb.buf, stdout); + strbuf_release(&sb); +} + +static void update_refs_for_switch(const struct checkout_opts *opts, + struct branch_info *old, + struct branch_info *new) +{ + 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, 1, &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(opts->new_branch, new->name, + opts->new_branch_force ? 1 : 0, + opts->new_branch_log, + opts->new_branch_force ? 1 : 0, + opts->quiet, + opts->track); + new->name = opts->new_branch; + setup_branch_path(new); + } + + old_desc = old->name; + if (!old_desc && old->commit) + old_desc = oid_to_hex(&old->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->name); + else + strbuf_insert(&msg, 0, reflog_msg, strlen(reflog_msg)); + + if (!strcmp(new->name, "HEAD") && !new->path && !opts->force_detach) { + /* Nothing to do. */ + } else if (opts->force_detach || !new->path) { /* No longer on any branch. */ + update_ref(msg.buf, "HEAD", new->commit->object.oid.hash, NULL, + REF_NODEREF, UPDATE_REFS_DIE_ON_ERR); + if (!opts->quiet) { + if (old->path && + advice_detached_head && !opts->force_detach) + detach_advice(new->name); + describe_detached_head(_("HEAD is now at"), new->commit); + } + } else if (new->path) { /* Switch branches. */ + if (create_symref("HEAD", new->path, msg.buf) < 0) + die(_("unable to update HEAD")); + if (!opts->quiet) { + if (old->path && !strcmp(new->path, old->path)) { + if (opts->new_branch_force) + fprintf(stderr, _("Reset branch '%s'\n"), + new->name); + else + fprintf(stderr, _("Already on '%s'\n"), + new->name); + } else if (opts->new_branch) { + if (opts->branch_exists) + fprintf(stderr, _("Switched to and reset branch '%s'\n"), new->name); + else + fprintf(stderr, _("Switched to a new branch '%s'\n"), new->name); + } else { + fprintf(stderr, _("Switched to branch '%s'\n"), + new->name); + } + } + if (old->path && old->name) { + if (!ref_exists(old->path) && reflog_exists(old->path)) + delete_reflog(old->path); + } + } + remove_branch_state(); + strbuf_release(&msg); + if (!opts->quiet && + (new->path || (!opts->force_detach && !strcmp(new->name, "HEAD")))) + report_tracking(new); +} + +static int add_pending_uninteresting_ref(const char *refname, + const struct object_id *oid, + int flags, 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.hash, DEFAULT_ABBREV); + strbuf_addch(sb, ' '); + if (!parse_commit(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_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), + find_unique_abbrev(commit->object.oid.hash, 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, struct commit *new) +{ + struct rev_info revs; + struct object *object = &old->object; + struct object_array refs; + + init_revisions(&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); + add_pending_oid(&revs, "HEAD", &new->object.oid, UNINTERESTING); + + /* Save pending objects, so they can be cleaned up later. */ + refs = revs.pending; + revs.leak_pending = 1; + + /* + * prepare_revision_walk (together with .leak_pending = 1) makes us + * the sole owner of the list of pending objects. + */ + if (prepare_revision_walk(&revs)) + die(_("internal error in revision walk")); + if (!(old->object.flags & UNINTERESTING)) + suggest_reattach(old, &revs); + else + describe_detached_head(_("Previous HEAD position was"), old); + + /* Clean up objects used, as they will be reused. */ + clear_commit_marks_for_object_array(&refs, ALL_REV_FLAGS); + + object_array_clear(&refs); +} + +static int switch_branches(const struct checkout_opts *opts, + struct branch_info *new) +{ + int ret = 0; + struct branch_info old; + void *path_to_free; + struct object_id rev; + int flag, writeout_error = 0; + memset(&old, 0, sizeof(old)); + old.path = path_to_free = resolve_refdup("HEAD", 0, rev.hash, &flag); + if (old.path) + old.commit = lookup_commit_reference_gently(&rev, 1); + if (!(flag & REF_ISSYMREF)) + old.path = NULL; + + if (old.path) + skip_prefix(old.path, "refs/heads/", &old.name); + + if (!new->name) { + new->name = "HEAD"; + new->commit = old.commit; + if (!new->commit) + die(_("You are on a branch yet to be born")); + parse_commit_or_die(new->commit); + } + + ret = merge_working_tree(opts, &old, new, &writeout_error); + if (ret) { + free(path_to_free); + return ret; + } + + if (!opts->quiet && !old.path && old.commit && new->commit != old.commit) + orphaned_commit_warning(old.commit, new->commit); + + update_refs_for_switch(opts, &old, new); + + ret = post_checkout_hook(old.commit, new->commit, 1); + free(path_to_free); + return ret || writeout_error; +} + +static int git_checkout_config(const char *var, const char *value, void *cb) +{ + if (!strcmp(var, "diff.ignoresubmodules")) { + struct checkout_opts *opts = cb; + handle_ignore_submodules_arg(&opts->diff_options, value); + return 0; + } + + if (starts_with(var, "submodule.")) + return git_default_submodule_config(var, value, NULL); + + return git_xmerge_config(var, value, NULL); +} + +struct tracking_name_data { + /* const */ char *src_ref; + char *dst_ref; + struct object_id *dst_oid; + int unique; +}; + +static int check_tracking_name(struct remote *remote, void *cb_data) +{ + struct tracking_name_data *cb = cb_data; + struct refspec query; + memset(&query, 0, sizeof(struct refspec)); + query.src = cb->src_ref; + if (remote_find_tracking(remote, &query) || + get_oid(query.dst, cb->dst_oid)) { + free(query.dst); + return 0; + } + if (cb->dst_ref) { + free(query.dst); + cb->unique = 0; + return 0; + } + cb->dst_ref = query.dst; + return 0; +} + +static const char *unique_tracking_name(const char *name, struct object_id *oid) +{ + struct tracking_name_data cb_data = { NULL, NULL, NULL, 1 }; + cb_data.src_ref = xstrfmt("refs/heads/%s", name); + cb_data.dst_oid = oid; + for_each_remote(check_tracking_name, &cb_data); + free(cb_data.src_ref); + if (cb_data.unique) + return cb_data.dst_ref; + free(cb_data.dst_ref); + return NULL; +} + +static int parse_branchname_arg(int argc, const char **argv, + int dwim_new_local_branch_ok, + struct branch_info *new, + struct checkout_opts *opts, + struct object_id *rev) +{ + struct tree **source_tree = &opts->source_tree; + const char **new_branch = &opts->new_branch; + int argcount = 0; + struct object_id branch_rev; + 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 + * and there is a tracking branch whose name is <something> + * in one and only one remote, 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; + + arg = argv[0]; + dash_dash_pos = -1; + for (i = 0; i < argc; i++) { + if (!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); + + if (!strcmp(arg, "-")) + arg = "@{-1}"; + + if (get_oid_mb(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; + + if (!has_dash_dash && + (check_filename(opts->prefix, arg) || !no_wildcard(arg))) + recover_with_dwim = 0; + /* + * Accept "git checkout foo" and "git checkout foo --" + * as candidates for dwim. + */ + if (!(argc == 1 && !has_dash_dash) && + !(argc == 2 && has_dash_dash)) + recover_with_dwim = 0; + + if (recover_with_dwim) { + const char *remote = unique_tracking_name(arg, rev); + 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--; + + new->name = arg; + setup_branch_path(new); + + if (!check_refname_format(new->path, 0) && + !read_ref(new->path, branch_rev.hash)) + oidcpy(rev, &branch_rev); + else + new->path = NULL; /* not an existing branch */ + + new->commit = lookup_commit_reference_gently(rev, 1); + if (!new->commit) { + /* not a commit */ + *source_tree = parse_tree_indirect(rev); + } else { + parse_commit_or_die(new->commit); + *source_tree = new->commit->tree; + } + + if (!*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 { + 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; + + 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 int checkout_branch(struct checkout_opts *opts, + struct branch_info *new) +{ + 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->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->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"); + } 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->name && !new->commit) + die(_("Cannot switch branch to a non-commit '%s'"), + new->name); + + if (new->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->path))) + die_if_checked_out(new->path, 1); + free(head_ref); + } + + if (!new->commit && opts->new_branch) { + struct object_id rev; + int flag; + + if (!read_ref_full("HEAD", 0, rev.hash, &flag) && + (flag & REF_ISSYMREF) && is_null_oid(&rev)) + return switch_unborn_to_new_branch(opts); + } + return switch_branches(opts, new); +} + +int cmd_checkout(int argc, const char **argv, const char *prefix) +{ + struct checkout_opts opts; + struct branch_info new; + char *conflict_style = NULL; + int dwim_new_local_branch = 1; + struct option options[] = { + OPT__QUIET(&opts.quiet, N_("suppress progress reporting")), + 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, "detach", &opts.force_detach, N_("detach HEAD at named commit")), + OPT_SET_INT('t', "track", &opts.track, N_("set upstream info for new branch"), + BRANCH_TRACK_EXPLICIT), + OPT_STRING(0, "orphan", &opts.new_orphan_branch, N_("new-branch"), N_("new unparented branch")), + OPT_SET_INT('2', "ours", &opts.writeout_stage, N_("checkout our version for unmerged files"), + 2), + OPT_SET_INT('3', "theirs", &opts.writeout_stage, N_("checkout their version for unmerged files"), + 3), + OPT__FORCE(&opts.force, N_("force checkout (throw away local modifications)")), + OPT_BOOL('m', "merge", &opts.merge, N_("perform a 3-way merge with the new branch")), + OPT_BOOL(0, "overwrite-ignore", &opts.overwrite_ignore, N_("update ignored files (default)")), + OPT_STRING(0, "conflict", &conflict_style, N_("style"), + N_("conflict style (merge or diff3)")), + 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_HIDDEN_BOOL(0, "guess", &dwim_new_local_branch, + N_("second guess 'git checkout <no-such-branch>'")), + OPT_BOOL(0, "ignore-other-worktrees", &opts.ignore_other_worktrees, + N_("do not check if another worktree is holding the given ref")), + { OPTION_CALLBACK, 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_END(), + }; + + memset(&opts, 0, sizeof(opts)); + memset(&new, 0, sizeof(new)); + opts.overwrite_ignore = 1; + opts.prefix = prefix; + opts.show_progress = -1; + + git_config(git_checkout_config, &opts); + + opts.track = BRANCH_TRACK_UNSPECIFIED; + + argc = parse_options(argc, argv, prefix, options, checkout_usage, + PARSE_OPT_KEEP_DASHDASH); + + if (opts.show_progress < 0) { + if (opts.quiet) + opts.show_progress = 0; + else + opts.show_progress = isatty(2); + } + + if (conflict_style) { + opts.merge = 1; /* implied */ + git_xmerge_config("merge.conflictstyle", conflict_style, NULL); + } + + if ((!!opts.new_branch + !!opts.new_branch_force + !!opts.new_orphan_branch) > 1) + die(_("-b, -B and --orphan are mutually exclusive")); + + /* + * 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/--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 -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 -b")); + 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) { + struct object_id rev; + int dwim_ok = + !opts.patch_mode && + dwim_new_local_branch && + opts.track == BRANCH_TRACK_UNSPECIFIED && + !opts.new_branch; + int n = parse_branchname_arg(argc, argv, dwim_ok, + &new, &opts, &rev); + argv += n; + argc -= n; + } + + 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) + 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 (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.")); + } + + if (opts.new_branch) { + struct strbuf buf = STRBUF_INIT; + + opts.branch_exists = + validate_new_branchname(opts.new_branch, &buf, + !!opts.new_branch_force, + !!opts.new_branch_force); + + strbuf_release(&buf); + } + + UNLEAK(opts); + if (opts.patch_mode || opts.pathspec.nr) + return checkout_paths(&opts, new.name); + else + return checkout_branch(&opts, &new); +} diff --git a/builtin/clean.c b/builtin/clean.c new file mode 100644 index 0000000000..189e20628c --- /dev/null +++ b/builtin/clean.c @@ -0,0 +1,1053 @@ +/* + * "git clean" builtin command + * + * Copyright (C) 2007 Shawn Bohrer + * + * Based on git-clean.sh by Pavel Roskin + */ + +#include "builtin.h" +#include "cache.h" +#include "config.h" +#include "dir.h" +#include "parse-options.h" +#include "string-list.h" +#include "quote.h" +#include "column.h" +#include "color.h" +#include "pathspec.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] [--] <paths>..."), + 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"); + +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 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; +}; + +static int parse_clean_color_slot(const char *var) +{ + if (!strcasecmp(var, "reset")) + return CLEAN_COLOR_RESET; + if (!strcasecmp(var, "plain")) + return CLEAN_COLOR_PLAIN; + if (!strcasecmp(var, "prompt")) + return CLEAN_COLOR_PROMPT; + if (!strcasecmp(var, "header")) + return CLEAN_COLOR_HEADER; + if (!strcasecmp(var, "help")) + return CLEAN_COLOR_HELP; + if (!strcasecmp(var, "error")) + return CLEAN_COLOR_ERROR; + return -1; +} + +static int git_clean_config(const char *var, const char *value, 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 = parse_clean_color_slot(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; + } + + /* inspect the color.ui config variable and others */ + return git_color_default_config(var, value, 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; + 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 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_relative(path->buf, prefix, "ed); + 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_relative(path->buf, prefix, "ed); + 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(dir)) != NULL) { + struct stat st; + if (is_dot_or_dotdot(e->d_name)) + continue; + + strbuf_setlen(path, len); + strbuf_addstr(path, e->d_name); + if (lstat(path->buf, &st)) + ; /* fall thru */ + else if (S_ISDIR(st.st_mode)) { + if (remove_dirs(path, prefix, force_flag, dry_run, quiet, &gone)) + ret = 1; + if (gone) { + quote_path_relative(path->buf, prefix, "ed); + string_list_append(&dels, quoted.buf); + } else + *dir_gone = 0; + continue; + } else { + res = dry_run ? 0 : unlink(path->buf); + if (!res) { + quote_path_relative(path->buf, prefix, "ed); + string_list_append(&dels, quoted.buf); + } else { + int saved_errno = errno; + quote_path_relative(path->buf, prefix, "ed); + 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) { + res = dry_run ? 0 : rmdir(path->buf); + if (!res) + *dir_gone = 1; + else { + int saved_errno = errno; + quote_path_relative(path->buf, prefix, "ed); + 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("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_relative(item->string, NULL, &buf); + 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 git-add--interactive.perl. + * + * 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 (strbuf_getline_lf(&choice, stdin) != EOF) { + strbuf_trim(&choice); + } else { + 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]; + } + + result = xcalloc(st_add(nr, 1), sizeof(int)); + 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; + struct strbuf confirm = STRBUF_INIT; + struct strbuf **ignore_list; + struct string_list_item *item; + struct exclude_list *el; + 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 (strbuf_getline_lf(&confirm, stdin) != EOF) + strbuf_trim(&confirm); + else + putchar('\n'); + + /* quit filter_by_pattern mode if press ENTER or Ctrl-D */ + if (!confirm.len) + break; + + memset(&dir, 0, sizeof(dir)); + el = add_exclude_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_exclude(ignore_list[i]->buf, "", 0, el, -(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); + clear_directory(&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_relative(item->string, NULL, &buf); + /* TRANSLATORS: Make sure to keep [y/N] as is */ + printf(_("Remove %s [y/N]? "), qname); + if (strbuf_getline_lf(&confirm, stdin) != EOF) { + strbuf_trim(&confirm); + } else { + 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; + struct pathspec pathspec; + struct strbuf buf = STRBUF_INIT; + struct string_list exclude_list = STRING_LIST_INIT_NODUP; + struct exclude_list *el; + 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")), + OPT_BOOL('i', "interactive", &interactive, N_("interactive cleaning")), + OPT_BOOL('d', NULL, &remove_directories, + N_("remove whole directories")), + { OPTION_CALLBACK, '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); + + memset(&dir, 0, sizeof(dir)); + if (ignored_only) + dir.flags |= DIR_SHOW_IGNORED; + + if (ignored && ignored_only) + die(_("-x and -X cannot be used together")); + + 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; + + dir.flags |= DIR_SHOW_OTHER_DIRECTORIES; + + if (remove_directories) + dir.flags |= DIR_SHOW_IGNORED_TOO | DIR_KEEP_UNTRACKED_CONTENTS; + + if (read_cache() < 0) + die(_("index file corrupt")); + + if (!ignored) + setup_standard_excludes(&dir); + + el = add_exclude_list(&dir, EXC_CMDL, "--exclude option"); + for (i = 0; i < exclude_list.nr; i++) + add_exclude(exclude_list.items[i].string, "", 0, el, -(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]; + int matches = 0; + struct stat st; + const char *rel; + + if (!cache_name_is_other(ent->name, ent->len)) + continue; + + if (pathspec.nr) + matches = dir_path_match(ent, &pathspec, 0, NULL); + + if (pathspec.nr && !matches) + continue; + + if (lstat(ent->name, &st)) + die_errno("Cannot lstat '%s'", ent->name); + + if (S_ISDIR(st.st_mode) && !remove_directories && + matches != MATCHED_EXACTLY) + continue; + + rel = relative_path(ent->name, prefix, &buf); + string_list_append(&del_list, rel); + } + + for (i = 0; i < dir.nr; i++) + free(dir.entries[i]); + + for (i = 0; i < dir.ignored_nr; i++) + free(dir.ignored[i]); + + if (interactive && del_list.nr > 0) + interactive_main_loop(); + + for_each_string_list_item(item, &del_list) { + struct stat st; + + 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_relative(item->string, NULL, &buf); + 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_relative(item->string, NULL, &buf); + errno = saved_errno; + warning_errno(_(msg_warn_remove_failed), qname); + errors++; + } else if (!quiet) { + qname = quote_path_relative(item->string, NULL, &buf); + printf(dry_run ? _(msg_would_remove) : _(msg_remove), qname); + } + } + strbuf_reset(&abs_path); + } + + strbuf_release(&abs_path); + strbuf_release(&buf); + string_list_clear(&del_list, 0); + string_list_clear(&exclude_list, 0); + return (errors != 0); +} diff --git a/builtin/clone.c b/builtin/clone.c new file mode 100644 index 0000000000..dbddd98f80 --- /dev/null +++ b/builtin/clone.c @@ -0,0 +1,1204 @@ +/* + * 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. + */ + +#include "builtin.h" +#include "config.h" +#include "lockfile.h" +#include "parse-options.h" +#include "fetch-pack.h" +#include "refs.h" +#include "tree.h" +#include "tree-walk.h" +#include "unpack-trees.h" +#include "transport.h" +#include "strbuf.h" +#include "dir.h" +#include "sigchain.h" +#include "branch.h" +#include "remote.h" +#include "run-command.h" +#include "connected.h" +#include "packfile.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 deepen; +static char *option_template, *option_depth, *option_since; +static char *option_origin = 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 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 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('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, "recursive", &option_recurse_submodules, + N_("pathspec"), N_("initialize submodules in the clone"), + PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN, recurse_submodules_cb, + (intptr_t)"." }, + { 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_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_SET_INT('4', "ipv4", &family, N_("use IPv4 addresses only"), + TRANSPORT_FAMILY_IPV4), + OPT_SET_INT('6', "ipv6", &family, N_("use IPv6 addresses only"), + TRANSPORT_FAMILY_IPV6), + 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 char *guess_dir_name(const char *repo, int is_bundle, int is_bare) +{ + const char *end = repo + strlen(repo), *start, *ptr; + size_t len; + char *dir; + + /* + * Skip scheme. + */ + start = strstr(repo, "://"); + if (start == NULL) + start = repo; + else + start += 3; + + /* + * Skip authentication data. The stripping does happen + * greedily, such that we strip up to the last '@' inside + * the host part. + */ + for (ptr = start; ptr < end && !is_dir_sep(*ptr); ptr++) { + if (*ptr == '@') + start = ptr + 1; + } + + /* + * Strip trailing spaces, slashes and /.git + */ + while (start < end && (is_dir_sep(end[-1]) || isspace(end[-1]))) + end--; + if (end - start > 5 && is_dir_sep(end[-5]) && + !strncmp(end - 4, ".git", 4)) { + end -= 5; + while (start < end && is_dir_sep(end[-1])) + end--; + } + + /* + * Strip trailing port number if we've got only a + * hostname (that is, there is no dir separator but a + * colon). This check is required such that we do not + * strip URI's like '/foo/bar:2222.git', which should + * result in a dir '2222' being guessed due to backwards + * compatibility. + */ + if (memchr(start, '/', end - start) == NULL + && memchr(start, ':', end - start) != NULL) { + ptr = end; + while (start < ptr && isdigit(ptr[-1]) && ptr[-1] != ':') + ptr--; + if (start < ptr && ptr[-1] == ':') + end = ptr - 1; + } + + /* + * Find last component. To remain backwards compatible we + * also regard colons as path separators, such that + * cloning a repository 'foo:bar.git' would result in a + * directory 'bar' being guessed. + */ + ptr = end; + while (start < ptr && !is_dir_sep(ptr[-1]) && ptr[-1] != ':') + ptr--; + start = ptr; + + /* + * Strip .{bundle,git}. + */ + len = end - start; + strip_suffix_mem(start, &len, is_bundle ? ".bundle" : ".git"); + + if (!len || (len == 1 && *start == '/')) + die(_("No directory name could be guessed.\n" + "Please specify a directory on the command line")); + + if (is_bare) + dir = xstrfmt("%.*s.git", (int)len, start); + else + dir = xstrndup(start, len); + /* + * Replace sequences of 'control' characters and whitespace + * with one ascii space, remove leading and trailing spaces. + */ + if (*dir) { + char *out = dir; + int prev_space = 1 /* strip leading whitespace */; + for (end = dir; *end; ++end) { + char ch = *end; + if ((unsigned char)ch < '\x20') + ch = '\x20'; + if (isspace(ch)) { + if (prev_space) + continue; + prev_space = 1; + } else + prev_space = 0; + *out++ = ch; + } + *out = '\0'; + if (out > dir && prev_space) + out[-1] = '\0'; + } + return dir; +} + +static void strip_trailing_slashes(char *dir) +{ + char *end = dir + strlen(dir); + + while (dir < end - 1 && is_dir_sep(end[-1])) + end--; + *end = '\0'; +} + +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, struct strbuf *dst, + 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 copy_or_link_directory(struct strbuf *src, struct strbuf *dest, + const char *src_repo, int src_baselen) +{ + struct dirent *de; + struct stat buf; + int src_len, dest_len; + DIR *dir; + + dir = opendir(src->buf); + if (!dir) + die_errno(_("failed to open '%s'"), src->buf); + + if (mkdir(dest->buf, 0777)) { + if (errno != EEXIST) + die_errno(_("failed to create directory '%s'"), dest->buf); + else if (stat(dest->buf, &buf)) + die_errno(_("failed to stat '%s'"), dest->buf); + else if (!S_ISDIR(buf.st_mode)) + die(_("%s exists and is not a directory"), dest->buf); + } + + strbuf_addch(src, '/'); + src_len = src->len; + strbuf_addch(dest, '/'); + dest_len = dest->len; + + while ((de = readdir(dir)) != NULL) { + strbuf_setlen(src, src_len); + strbuf_addstr(src, de->d_name); + strbuf_setlen(dest, dest_len); + strbuf_addstr(dest, de->d_name); + if (stat(src->buf, &buf)) { + warning (_("failed to stat %s\n"), src->buf); + continue; + } + if (S_ISDIR(buf.st_mode)) { + if (de->d_name[0] != '.') + copy_or_link_directory(src, dest, + src_repo, src_baselen); + continue; + } + + /* Files that cannot be copied bit-for-bit... */ + if (!strcmp(src->buf + src_baselen, "/info/alternates")) { + copy_alternates(src, dest, src_repo); + continue; + } + + if (unlink(dest->buf) && errno != ENOENT) + die_errno(_("failed to unlink '%s'"), dest->buf); + if (!option_no_hardlinks) { + if (!link(src->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); + } + closedir(dir); +} + +static void clone_local(const char *src_repo, const char *dest_repo) +{ + if (option_shared) { + struct strbuf alt = STRBUF_INIT; + strbuf_addf(&alt, "%s/objects", src_repo); + 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, src.len); + strbuf_release(&src); + strbuf_release(&dest); + } + + if (0 <= option_verbosity) + fprintf(stderr, _("done.\n")); +} + +static const char *junk_work_tree; +static const char *junk_git_dir; +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 the checkout with 'git checkout -f 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, 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 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 { + get_fetch_map(remote_head, refspec, &tail, 0); + + /* if --branch=tag, pull the requested tag explicitly */ + get_fetch_map(remote_head, tag_refspec, &tail, 0); + } + } else + get_fetch_map(refs, refspec, &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.hash, + 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 (!has_object_file(&ref->old_oid)) + continue; + update_ref(msg, ref->name, ref->old_oid.hash, + NULL, 0, UPDATE_REFS_DIE_ON_ERR); + } +} + +static int iterate_ref_map(void *cb_data, unsigned char sha1[20]) +{ + 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; + /* Returning -1 notes "end of list" to the caller. */ + if (!ref) + return -1; + + hashcpy(sha1, ref->old_oid.hash); + *rm = ref->next; + return 0; +} + +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 *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.hash, NULL, 0, + UPDATE_REFS_DIE_ON_ERR); + install_branch_config(0, head, option_origin, our->name); + } + } else if (our) { + struct commit *c = lookup_commit_reference(&our->old_oid); + /* --branch specifies a non-branch (i.e. tags), detach HEAD */ + update_ref(msg, "HEAD", c->object.oid.hash, + NULL, REF_NODEREF, 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.hash, + NULL, REF_NODEREF, UPDATE_REFS_DIE_ON_ERR); + } +} + +static int checkout(int submodule_progress) +{ + struct object_id oid; + char *head; + struct lock_file *lock_file; + 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.hash, NULL); + if (!head) { + warning(_("remote HEAD refers to nonexistent ref, " + "unable to checkout.\n")); + return 0; + } + if (!strcmp(head, "HEAD")) { + if (advice_detached_head) + detach_advice(oid_to_hex(&oid)); + } else { + if (!starts_with(head, "refs/heads/")) + die(_("HEAD not found below refs/heads!")); + } + free(head); + + /* We need to be in the new work tree for the checkout */ + setup_work_tree(); + + lock_file = xcalloc(1, sizeof(struct lock_file)); + hold_locked_index(lock_file, LOCK_DIE_ON_ERROR); + + memset(&opts, 0, sizeof opts); + opts.update = 1; + opts.merge = 1; + opts.fn = oneway_merge; + opts.verbose_update = (option_verbosity >= 0); + opts.src_index = &the_index; + opts.dst_index = &the_index; + + tree = parse_tree_indirect(&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")); + + if (write_locked_index(&the_index, lock_file, COMMIT_LOCK)) + die(_("unable to write new index file")); + + err |= run_hook_le(NULL, "post-checkout", sha1_to_hex(null_sha1), + oid_to_hex(&oid), "1", NULL); + + if (!err && (option_recurse_submodules.nr > 0)) { + struct argv_array args = ARGV_ARRAY_INIT; + argv_array_pushl(&args, "submodule", "update", "--init", "--recursive", NULL); + + if (option_shallow_submodules == 1) + argv_array_push(&args, "--depth=1"); + + if (max_jobs != -1) + argv_array_pushf(&args, "--jobs=%d", max_jobs); + + if (submodule_progress) + argv_array_push(&args, "--progress"); + + if (option_verbosity < 0) + argv_array_push(&args, "--quiet"); + + err = run_command_v_opt(args.argv, RUN_GIT_CMD); + argv_array_clear(&args); + } + + return err; +} + +static int write_one_config(const char *key, const char *value, void *data) +{ + 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)) + die("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", option_origin); + git_config_set_multivar(key.buf, value.buf, "^$", 0); + strbuf_reset(&key); + + if (option_mirror) { + strbuf_addf(&key, "remote.%s.mirror", option_origin); + git_config_set(key.buf, "true"); + strbuf_reset(&key); + } + } + } + + strbuf_release(&key); + strbuf_release(&value); +} + +static void dissociate_from_references(void) +{ + static const char* argv[] = { "repack", "-a", "-d", NULL }; + char *alternates = git_pathdup("objects/info/alternates"); + + if (!access(alternates, F_OK)) { + if (run_command_v_opt(argv, RUN_GIT_CMD|RUN_COMMAND_NO_STDIN)) + die(_("cannot repack to clean up")); + if (unlink(alternates) && errno != ENOENT) + die_errno(_("cannot unlink temporary alternates file")); + } + free(alternates); +} + +int cmd_clone(int argc, const char **argv, const char *prefix) +{ + int is_bundle = 0, is_local; + struct stat buf; + const char *repo_name, *repo, *work_tree, *git_dir; + char *path, *dir; + int dest_exists; + const struct ref *refs, *remote_head; + const struct ref *remote_head_points_at; + const struct ref *our_head_points_at; + struct ref *mapped_refs; + const struct ref *ref; + struct strbuf key = STRBUF_INIT, value = 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; + + struct refspec *refspec; + const char *fetch_pattern; + + packet_trace_identity("clone"); + 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 (option_origin) + die(_("--bare and --origin %s options are incompatible."), + option_origin); + if (real_git_dir) + die(_("--bare and --separate-git-dir are incompatible.")); + option_no_checkout = 1; + } + + if (!option_origin) + option_origin = "origin"; + + repo_name = argv[0]; + + path = get_repo_path(repo_name, &is_bundle); + if (path) + repo = absolute_pathdup(repo_name); + else if (!strchr(repo_name, ':')) + die(_("repository '%s' does not exist"), repo_name); + else + repo = 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 = guess_dir_name(repo_name, is_bundle, option_bare); + strip_trailing_slashes(dir); + + dest_exists = !stat(dir, &buf); + if (dest_exists && !is_empty_dir(dir)) + die(_("destination path '%s' already exists and is not " + "an empty directory."), dir); + + strbuf_addf(&reflog_msg, "clone: from %s", repo); + + if (option_bare) + work_tree = NULL; + else { + work_tree = getenv("GIT_WORK_TREE"); + if (work_tree && !stat(work_tree, &buf)) + 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 && 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); + } + + junk_git_dir = real_git_dir ? real_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; + + /* 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 (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, INIT_DB_QUIET); + + if (real_git_dir) + git_dir = real_git_dir; + + write_config(&option_config); + + git_config(git_default_config, NULL); + + 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/", option_origin); + } + + strbuf_addf(&value, "+%s*:%s*", src_ref_prefix, branch_top.buf); + strbuf_addf(&key, "remote.%s.url", option_origin); + git_config_set(key.buf, repo); + strbuf_reset(&key); + + if (option_no_tags) { + strbuf_addf(&key, "remote.%s.tagOpt", option_origin); + git_config_set(key.buf, "--no-tags"); + strbuf_reset(&key); + } + + if (option_required_reference.nr || option_optional_reference.nr) + setup_reference(); + + fetch_pattern = value.buf; + refspec = parse_fetch_refspec(1, &fetch_pattern); + + strbuf_reset(&value); + + remote = remote_get(option_origin); + transport = transport_get(remote, remote->url[0]); + transport_set_verbosity(transport, option_verbosity, option_progress); + transport->family = family; + + 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 (!access(mkpath("%s/shallow", path), F_OK)) { + 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->cloning = 1; + + if (!transport->get_refs_list || (!is_local && !transport->fetch)) + die(_("Don't know how to clone %s"), transport->url); + + transport_set_option(transport, TRANS_OPT_KEEP, "yes"); + + 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 (transport->smart_options && !deepen) + transport->smart_options->check_self_contained_and_connected = 1; + + refs = transport_get_remote_refs(transport); + + if (refs) { + mapped_refs = wanted_peer_refs(refs, refspec); + /* + * 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) + transport_fetch_refs(transport, mapped_refs); + + 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, option_origin); + } + else + our_head_points_at = remote_head_points_at; + } + else { + if (option_branch) + die(_("Remote branch %s not found in upstream %s"), + option_branch, option_origin); + + warning(_("You appear to have cloned an empty repository.")); + mapped_refs = NULL; + our_head_points_at = NULL; + remote_head_points_at = NULL; + remote_head = NULL; + option_no_checkout = 1; + if (!option_bare) + install_branch_config(0, "master", option_origin, + "refs/heads/master"); + } + + write_refspec_config(src_ref_prefix, our_head_points_at, + remote_head_points_at, &branch_top); + + if (is_local) + clone_local(path, git_dir); + else if (refs && complete_refs_before_fetch) + transport_fetch_refs(transport, mapped_refs); + + 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, 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); + transport_disconnect(transport); + + if (option_dissociate) { + close_all_packs(); + dissociate_from_references(); + } + + junk_mode = JUNK_LEAVE_REPO; + err = checkout(submodule_progress); + + strbuf_release(&reflog_msg); + strbuf_release(&branch_top); + strbuf_release(&key); + strbuf_release(&value); + junk_mode = JUNK_LEAVE_ALL; + + free(refspec); + return err; +} diff --git a/builtin/column.c b/builtin/column.c new file mode 100644 index 0000000000..0c3223d64b --- /dev/null +++ b/builtin/column.c @@ -0,0 +1,60 @@ +#include "builtin.h" +#include "cache.h" +#include "config.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, 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_INTEGER(0, "nl", &copts.nl, 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.width = term_columns(); + copts.padding = 1; + argc = parse_options(argc, argv, "", 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-tree.c b/builtin/commit-tree.c new file mode 100644 index 0000000000..2177251e24 --- /dev/null +++ b/builtin/commit-tree.c @@ -0,0 +1,129 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" +#include "config.h" +#include "commit.h" +#include "tree.h" +#include "builtin.h" +#include "utf8.h" +#include "gpg-interface.h" + +static const char commit_tree_usage[] = "git commit-tree [(-p <sha1>)...] [-S[<keyid>]] [-m <message>] [-F <file>] <sha1>"; + +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 commit_tree_config(const char *var, const char *value, void *cb) +{ + int status = git_gpg_config(var, value, NULL); + if (status) + return status; + return git_default_config(var, value, cb); +} + +int cmd_commit_tree(int argc, const char **argv, const char *prefix) +{ + int i, got_tree = 0; + struct commit_list *parents = NULL; + struct object_id tree_oid; + struct object_id commit_oid; + struct strbuf buffer = STRBUF_INIT; + + git_config(commit_tree_config, NULL); + + if (argc < 2 || !strcmp(argv[1], "-h")) + usage(commit_tree_usage); + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (!strcmp(arg, "-p")) { + struct object_id oid; + if (argc <= ++i) + usage(commit_tree_usage); + if (get_oid_commit(argv[i], &oid)) + die("Not a valid object name %s", argv[i]); + assert_sha1_type(oid.hash, OBJ_COMMIT); + new_parent(lookup_commit(&oid), &parents); + continue; + } + + if (skip_prefix(arg, "-S", &sign_commit)) + continue; + + if (!strcmp(arg, "--no-gpg-sign")) { + sign_commit = NULL; + continue; + } + + if (!strcmp(arg, "-m")) { + if (argc <= ++i) + usage(commit_tree_usage); + if (buffer.len) + strbuf_addch(&buffer, '\n'); + strbuf_addstr(&buffer, argv[i]); + strbuf_complete_line(&buffer); + continue; + } + + if (!strcmp(arg, "-F")) { + int fd; + + if (argc <= ++i) + usage(commit_tree_usage); + if (buffer.len) + strbuf_addch(&buffer, '\n'); + if (!strcmp(argv[i], "-")) + fd = 0; + else { + fd = open(argv[i], O_RDONLY); + if (fd < 0) + die_errno("git commit-tree: failed to open '%s'", + argv[i]); + } + if (strbuf_read(&buffer, fd, 0) < 0) + die_errno("git commit-tree: failed to read '%s'", + argv[i]); + if (fd && close(fd)) + die_errno("git commit-tree: failed to close '%s'", + argv[i]); + continue; + } + + if (get_oid_tree(arg, &tree_oid)) + die("Not a valid object name %s", arg); + if (got_tree) + die("Cannot give more than one trees"); + got_tree = 1; + } + + 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.hash, parents, + commit_oid.hash, 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 0000000000..d75b3805ea --- /dev/null +++ b/builtin/commit.c @@ -0,0 +1,1831 @@ +/* + * Builtin "git commit" + * + * Copyright (c) 2007 Kristian Høgsberg <krh@redhat.com> + * Based on git-commit.sh by Junio C Hamano and Linus Torvalds + */ + +#include "cache.h" +#include "config.h" +#include "lockfile.h" +#include "cache-tree.h" +#include "color.h" +#include "dir.h" +#include "builtin.h" +#include "diff.h" +#include "diffcore.h" +#include "commit.h" +#include "revision.h" +#include "wt-status.h" +#include "run-command.h" +#include "refs.h" +#include "log-tree.h" +#include "strbuf.h" +#include "utf8.h" +#include "parse-options.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 "notes-utils.h" +#include "mailmap.h" +#include "sigchain.h" + +static const char * const builtin_commit_usage[] = { + N_("git commit [<options>] [--] <pathspec>..."), + NULL +}; + +static const char * const builtin_status_usage[] = { + N_("git status [<options>] [--] <pathspec>..."), + NULL +}; + +static const char implicit_ident_advice_noconfig[] = +N_("Your name and email address were configured automatically based\n" +"on your username and hostname. Please check that they are accurate.\n" +"You can suppress this message by setting them explicitly. Run the\n" +"following command and follow the instructions in your editor to edit\n" +"your configuration file:\n" +"\n" +" git config --global --edit\n" +"\n" +"After doing this, you may fix the identity used for this commit with:\n" +"\n" +" git commit --amend --reset-author\n"); + +static const char implicit_ident_advice_config[] = +N_("Your name and email address were configured automatically based\n" +"on your username and hostname. Please check that they are accurate.\n" +"You can suppress this message by setting them explicitly:\n" +"\n" +" git config --global user.name \"Your Name\"\n" +" git config --global user.email you@example.com\n" +"\n" +"After doing this, you may fix the identity used for this commit with:\n" +"\n" +" git commit --amend --reset-author\n"); + +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_cherry_pick_advice_single[] = +N_("Otherwise, please use 'git reset'\n"); + +static const char empty_cherry_pick_advice_multi[] = +N_("If you wish to skip this commit, use:\n" +"\n" +" git reset\n" +"\n" +"Then \"git cherry-pick --continue\" will resume cherry-picking\n" +"the remaining commits.\n"); + +static GIT_PATH_FUNC(git_path_commit_editmsg, "COMMIT_EDITMSG") + +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, *squash_message; +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; +static char *untracked_files_arg, *force_date, *ignore_submodule_arg; +static char *sign_commit; + +/* + * 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 { + CLEANUP_SPACE, + CLEANUP_NONE, + CLEANUP_SCISSORS, + CLEANUP_ALL +} cleanup_mode; +static const char *cleanup_arg; + +static enum commit_whence whence; +static int sequencer_in_use; +static int use_editor = 1, include_status = 1; +static int show_ignored_in_status, have_option_m; +static struct strbuf message = STRBUF_INIT; + +static enum wt_status_format status_format = STATUS_FORMAT_UNSPECIFIED; + +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 void determine_whence(struct wt_status *s) +{ + if (file_exists(git_path_merge_head())) + whence = FROM_MERGE; + else if (file_exists(git_path_cherry_pick_head())) { + whence = FROM_CHERRY_PICK; + if (file_exists(git_path_seq_dir())) + sequencer_in_use = 1; + } + else + whence = FROM_COMMIT; + if (s) + s->whence = whence; +} + +static void status_init_config(struct wt_status *s, config_fn_t fn) +{ + wt_status_prepare(s); + git_config(fn, s); + determine_whence(s); + init_diff_ui_defaults(); + s->hints = 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 char *prefix, 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 ? max_prefix : prefix); + free(max_prefix); + } + + for (i = 0; i < active_nr; i++) { + const struct cache_entry *ce = active_cache[i]; + struct string_list_item *item; + + if (ce->ce_flags & CE_UPDATE) + continue; + if (!ce_path_match(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, prefix); + 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_cache(p->string, &st, 0)) + die(_("updating files failed")); + } else + remove_file_from_cache(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_cache(); + 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_cache(refresh_flags | REFRESH_IN_PORCELAIN)) + die_resolve_conflict("commit"); +} + +static const char *prepare_index(int argc, 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 (read_cache_preload(&pathspec) < 0) + die(_("index file corrupt")); + + if (interactive) { + char *old_index_env = NULL; + hold_locked_index(&index_lock, LOCK_DIE_ON_ERROR); + + refresh_cache_or_die(refresh_flags); + + if (write_locked_index(&the_index, &index_lock, CLOSE_LOCK)) + die(_("unable to create temporary index")); + + old_index_env = getenv(INDEX_ENVIRONMENT); + setenv(INDEX_ENVIRONMENT, get_lock_file_path(&index_lock), 1); + + if (interactive_add(argc, argv, prefix, patch_interactive) != 0) + die(_("interactive add failed")); + + if (old_index_env && *old_index_env) + setenv(INDEX_ENVIRONMENT, old_index_env, 1); + else + unsetenv(INDEX_ENVIRONMENT); + + discard_cache(); + read_cache_from(get_lock_file_path(&index_lock)); + if (update_main_cache_tree(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, CLOSE_LOCK)) + 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)) { + hold_locked_index(&index_lock, LOCK_DIE_ON_ERROR); + add_files_to_cache(also ? prefix : NULL, &pathspec, 0); + refresh_cache_or_die(refresh_flags); + update_main_cache_tree(WRITE_TREE_SILENT); + if (write_locked_index(&the_index, &index_lock, CLOSE_LOCK)) + 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) { + hold_locked_index(&index_lock, LOCK_DIE_ON_ERROR); + refresh_cache_or_die(refresh_flags); + if (active_cache_changed + || !cache_tree_fully_valid(active_cache_tree)) + update_main_cache_tree(WRITE_TREE_SILENT); + if (active_cache_changed) { + if (write_locked_index(&the_index, &index_lock, + COMMIT_LOCK)) + die(_("unable to write new_index file")); + } else { + rollback_lock_file(&index_lock); + } + 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 (whence == FROM_CHERRY_PICK) + die(_("cannot do a partial commit during a cherry-pick.")); + } + + if (list_paths(&partial, !current_head ? NULL : "HEAD", prefix, &pathspec)) + exit(1); + + discard_cache(); + if (read_cache() < 0) + die(_("cannot read the index")); + + hold_locked_index(&index_lock, LOCK_DIE_ON_ERROR); + add_remove_files(&partial); + refresh_cache(REFRESH_QUIET); + update_main_cache_tree(WRITE_TREE_SILENT); + if (write_locked_index(&the_index, &index_lock, CLOSE_LOCK)) + 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_cache(REFRESH_QUIET); + + if (write_locked_index(&the_index, &false_lock, CLOSE_LOCK)) + die(_("unable to write temporary index file")); + + discard_cache(); + ret = get_lock_file_path(&false_lock); + read_cache_from(ret); +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 = get_oid(s->reference, &oid) ? 1 : 0; + if (!s->is_initial) + hashcpy(s->sha1_commit, oid.hash); + s->status_format = status_format; + s->ignore_submodule_arg = ignore_submodule_arg; + + wt_status_collect(s); + wt_status_print(s); + + return s->commitable; +} + +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) + die("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_addf(&buf, "%.*s", (int)(e - s), 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, 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 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 commitable; + struct strbuf sb = STRBUF_INIT; + const char *hook_arg1 = NULL; + const char *hook_arg2 = NULL; + int clean_message_contents = (cleanup_mode != CLEANUP_NONE); + int old_display_comment_prefix; + + /* This checks and barfs if author is badly specified */ + determine_author_info(author_ident); + + if (!no_verify && run_commit_hook(use_editor, index_file, "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(); + format_commit_message(c, "squash! %s\n\n", &sb, + &ctx); + } + } + + if (have_option_m) { + 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; + commit = lookup_commit_reference_by_name(fixup_message); + if (!commit) + die(_("could not lookup commit %s"), fixup_message); + ctx.output_encoding = get_commit_output_encoding(); + format_commit_message(commit, "fixup! %s\n\n", + &sb, &ctx); + hook_arg1 = "message"; + } else if (!stat(git_path_merge_msg(), &statbuf)) { + /* + * prepend SQUASH_MSG here if it exists and a + * "merge --squash" was originally performed + */ + if (!stat(git_path_squash_msg(), &statbuf)) { + if (strbuf_read_file(&sb, git_path_squash_msg(), 0) < 0) + die_errno(_("could not read SQUASH_MSG")); + hook_arg1 = "squash"; + } else + hook_arg1 = "merge"; + if (strbuf_read_file(&sb, git_path_merge_msg(), 0) < 0) + die_errno(_("could not read MERGE_MSG")); + } else if (!stat(git_path_squash_msg(), &statbuf)) { + if (strbuf_read_file(&sb, git_path_squash_msg(), 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 (whence == FROM_CHERRY_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 == NULL) + 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; + + if (whence != FROM_COMMIT) { + if (cleanup_mode == CLEANUP_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 remove the file\n" + " %s\n" + "and try again.\n") + : _("\n" + "It looks like you may be committing a cherry-pick.\n" + "If this is not correct, please remove the file\n" + " %s\n" + "and try again.\n"), + whence == FROM_MERGE ? + git_path_merge_head() : + git_path_cherry_pick_head()); + } + + fprintf(s->fp, "\n"); + if (cleanup_mode == CLEANUP_ALL) + status_printf(s, GIT_COLOR_NORMAL, + _("Please enter the commit message for your changes." + " Lines starting\nwith '%c' will be ignored, and an empty" + " message aborts the commit.\n"), comment_line_char); + else if (cleanup_mode == CLEANUP_SCISSORS && whence == FROM_COMMIT) + wt_status_add_cut_line(s->fp); + else /* CLEANUP_SPACE, that is. */ + status_printf(s, GIT_COLOR_NORMAL, + _("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"), 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; + commitable = run_status(s->fp, index_file, prefix, 1, s); + s->use_color = saved_color_setting; + } else { + struct object_id oid; + const char *parent = "HEAD"; + + if (!active_nr && read_cache() < 0) + die(_("Cannot read index")); + + if (amend) + parent = "HEAD^1"; + + if (get_oid(parent, &oid)) { + int i, ita_nr = 0; + + for (i = 0; i < active_nr; i++) + if (ce_intent_to_add(active_cache[i])) + ita_nr++; + commitable = active_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. + */ + int diff_flags = DIFF_OPT_OVERRIDE_SUBMODULE_CONFIG; + if (ignore_submodule_arg && + !strcmp(ignore_submodule_arg, "all")) + diff_flags |= DIFF_OPT_IGNORE_SUBMODULES; + commitable = index_differs_from(parent, diff_flags, 1); + } + } + strbuf_release(&committer_ident); + + fclose(s->fp); + + /* + * 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 (!commitable && whence != FROM_MERGE && !allow_empty && + !(amend && is_a_merge(current_head))) { + 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 (whence == FROM_CHERRY_PICK) { + fputs(_(empty_cherry_pick_advice), stderr); + if (!sequencer_in_use) + fputs(_(empty_cherry_pick_advice_single), stderr); + else + fputs(_(empty_cherry_pick_advice_multi), stderr); + } + return 0; + } + + if (!no_verify && find_hook("pre-commit")) { + /* + * Re-read the index as pre-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. + */ + discard_cache(); + } + read_cache_from(index_file); + + if (update_main_cache_tree(0)) { + error(_("Error building trees")); + return 0; + } + + if (run_commit_hook(use_editor, index_file, "prepare-commit-msg", + git_path_commit_editmsg(), hook_arg1, hook_arg2, NULL)) + return 0; + + if (use_editor) { + struct argv_array env = ARGV_ARRAY_INIT; + + argv_array_pushf(&env, "GIT_INDEX_FILE=%s", index_file); + if (launch_editor(git_path_commit_editmsg(), NULL, env.argv)) { + fprintf(stderr, + _("Please supply the message using either -m or -F option.\n")); + exit(1); + } + argv_array_clear(&env); + } + + if (!no_verify && + run_commit_hook(use_editor, index_file, "commit-msg", git_path_commit_editmsg(), NULL)) { + return 0; + } + + return 1; +} + +static int rest_is_empty(struct strbuf *sb, int start) +{ + int i, eol; + const char *nl; + + /* Check if the rest is just whitespace and Signed-off-by's. */ + for (i = start; i < sb->len; i++) { + nl = memchr(sb->buf + i, '\n', sb->len - i); + if (nl) + eol = nl - sb->buf; + else + eol = sb->len; + + if (strlen(sign_off_header) <= eol - i && + starts_with(sb->buf + i, sign_off_header)) { + i = eol; + continue; + } + while (i < eol) + if (!isspace(sb->buf[i++])) + return 0; + } + + return 1; +} + +/* + * Find out if the message in the strbuf contains only whitespace and + * Signed-off-by lines. + */ +static int message_is_empty(struct strbuf *sb) +{ + if (cleanup_mode == CLEANUP_NONE && sb->len) + return 0; + return rest_is_empty(sb, 0); +} + +/* + * See if the user edited the message in the editor or left what + * was in the template intact + */ +static int template_untouched(struct strbuf *sb) +{ + struct strbuf tmpl = STRBUF_INIT; + const char *start; + + if (cleanup_mode == CLEANUP_NONE && sb->len) + return 0; + + if (!template_file || strbuf_read_file(&tmpl, template_file, 0) <= 0) + return 0; + + strbuf_stripspace(&tmpl, cleanup_mode == CLEANUP_ALL); + if (!skip_prefix(sb->buf, tmpl.buf, &start)) + start = sb->buf; + strbuf_release(&tmpl); + return rest_is_empty(sb, start - sb->buf); +} + +static const char *find_author_by_nickname(const char *name) +{ + struct rev_info revs; + struct commit *commit; + struct strbuf buf = STRBUF_INIT; + struct string_list mailmap = STRING_LIST_INIT_NODUP; + const char *av[20]; + int ac = 0; + + init_revisions(&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 = &mailmap; + read_mailmap(revs.mailmap, NULL); + + 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); + format_commit_message(commit, "%aN <%aE>", &buf, &ctx); + clear_mailmap(&mailmap); + return strbuf_detach(&buf, NULL); + } + die(_("--author '%s' is not 'Name <email>' and matches no existing author"), name); +} + + +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; + 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 logmsg_reencode(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; +} status_deferred_config = { + STATUS_FORMAT_UNSPECIFIED, + -1 /* 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(_("--long and -z are incompatible")); + } + + 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; +} + +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) +{ + int f = 0; + + 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(_("Using both --reset-author and --author does not make sense")); + + if (logfile || have_option_m || use_message || fixup_message) + use_editor = 0; + if (0 <= edit_flag) + use_editor = edit_flag; + + /* 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 (whence == FROM_CHERRY_PICK) + die(_("You are in the middle of a cherry-pick -- cannot amend.")); + } + if (fixup_message && squash_message) + die(_("Options --squash and --fixup cannot be used together")); + if (use_message) + f++; + if (edit_message) + f++; + if (fixup_message) + f++; + if (logfile) + f++; + if (f > 1) + die(_("Only one of -c/-C/-F/--fixup can be used.")); + if (have_option_m && f > 0) + die((_("Option -m cannot be combined with -c/-C/-F/--fixup."))); + if (f || 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 && whence != FROM_CHERRY_PICK && 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 (whence == FROM_CHERRY_PICK && !renew_authorship) { + author_message = "CHERRY_PICK_HEAD"; + author_message_buffer = read_commit_message(author_message); + } + + if (patch_interactive) + interactive = 1; + + if (also + only + all + interactive > 1) + die(_("Only one of --include/--only/--all/--interactive/--patch can be used.")); + if (argc == 0 && (also || (only && !amend && !allow_empty))) + die(_("No paths with --include/--only does not make sense.")); + if (!cleanup_arg || !strcmp(cleanup_arg, "default")) + cleanup_mode = use_editor ? CLEANUP_ALL : CLEANUP_SPACE; + else if (!strcmp(cleanup_arg, "verbatim")) + cleanup_mode = CLEANUP_NONE; + else if (!strcmp(cleanup_arg, "whitespace")) + cleanup_mode = CLEANUP_SPACE; + else if (!strcmp(cleanup_arg, "strip")) + cleanup_mode = CLEANUP_ALL; + else if (!strcmp(cleanup_arg, "scissors")) + cleanup_mode = use_editor ? CLEANUP_SCISSORS : CLEANUP_SPACE; + else + die(_("Invalid cleanup mode %s"), cleanup_arg); + + handle_untracked_files_arg(s); + + if (all && argc > 0) + die(_("Paths with -a does not make sense.")); + + if (status_format != STATUS_FORMAT_NONE) + dry_run = 1; + + return argc; +} + +static int dry_run_commit(int argc, const char **argv, const char *prefix, + const struct commit *current_head, struct wt_status *s) +{ + int commitable; + const char *index_file; + + index_file = prepare_index(argc, argv, prefix, current_head, 1); + commitable = run_status(stdout, index_file, prefix, 0, s); + rollback_index_files(); + + return commitable ? 0 : 1; +} + +static int parse_status_slot(const char *slot) +{ + if (!strcasecmp(slot, "header")) + return WT_STATUS_HEADER; + if (!strcasecmp(slot, "branch")) + return WT_STATUS_ONBRANCH; + if (!strcasecmp(slot, "updated") || !strcasecmp(slot, "added")) + return WT_STATUS_UPDATED; + if (!strcasecmp(slot, "changed")) + return WT_STATUS_CHANGED; + if (!strcasecmp(slot, "untracked")) + return WT_STATUS_UNTRACKED; + if (!strcasecmp(slot, "nobranch")) + return WT_STATUS_NOBRANCH; + if (!strcasecmp(slot, "unmerged")) + return WT_STATUS_UNMERGED; + if (!strcasecmp(slot, "localBranch")) + return WT_STATUS_LOCAL_BRANCH; + if (!strcasecmp(slot, "remoteBranch")) + return WT_STATUS_REMOTE_BRANCH; + return -1; +} + +static int git_status_config(const char *k, const char *v, 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, &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.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; + } + return git_diff_ui_config(k, v, NULL); +} + +int cmd_status(int argc, const char **argv, const char *prefix) +{ + static struct wt_status s; + 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")), + { OPTION_CALLBACK, 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" }, + OPT_BOOL(0, "ignored", &show_ignored_in_status, + N_("show ignored files")), + { 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_END(), + }; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(builtin_status_usage, builtin_status_options); + + 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); + if (show_ignored_in_status) + s.show_ignored_files = 1; + parse_pathspec(&s.pathspec, 0, + PATHSPEC_PREFER_FULL, + prefix, argv); + + read_cache_preload(&s.pathspec); + refresh_index(&the_index, REFRESH_QUIET|REFRESH_UNMERGED, &s.pathspec, NULL, NULL); + + if (use_optional_locks()) + fd = hold_locked_index(&index_lock, 0); + else + fd = -1; + + s.is_initial = get_oid(s.reference, &oid) ? 1 : 0; + if (!s.is_initial) + hashcpy(s.sha1_commit, oid.hash); + + s.ignore_submodule_arg = ignore_submodule_arg; + s.status_format = status_format; + s.verbose = verbose; + + wt_status_collect(&s); + + if (0 <= fd) + update_index_if_able(&the_index, &index_lock); + + if (s.relative_paths) + s.prefix = prefix; + + wt_status_print(&s); + return 0; +} + +static const char *implicit_ident_advice(void) +{ + char *user_config = expand_user_path("~/.gitconfig", 0); + char *xdg_config = xdg_config_home("config"); + int config_exists = file_exists(user_config) || file_exists(xdg_config); + + free(user_config); + free(xdg_config); + + if (config_exists) + return _(implicit_ident_advice_config); + else + return _(implicit_ident_advice_noconfig); + +} + +static void print_summary(const char *prefix, const struct object_id *oid, + int initial_commit) +{ + struct rev_info rev; + struct commit *commit; + struct strbuf format = STRBUF_INIT; + const char *head; + struct pretty_print_context pctx = {0}; + struct strbuf author_ident = STRBUF_INIT; + struct strbuf committer_ident = STRBUF_INIT; + + commit = lookup_commit(oid); + if (!commit) + die(_("couldn't look up newly created commit")); + if (parse_commit(commit)) + die(_("could not parse newly created commit")); + + strbuf_addstr(&format, "format:%h] %s"); + + format_commit_message(commit, "%an <%ae>", &author_ident, &pctx); + format_commit_message(commit, "%cn <%ce>", &committer_ident, &pctx); + if (strbuf_cmp(&author_ident, &committer_ident)) { + strbuf_addstr(&format, "\n Author: "); + strbuf_addbuf_percentquote(&format, &author_ident); + } + if (author_date_is_interesting()) { + struct strbuf date = STRBUF_INIT; + format_commit_message(commit, "%ad", &date, &pctx); + strbuf_addstr(&format, "\n Date: "); + strbuf_addbuf_percentquote(&format, &date); + strbuf_release(&date); + } + if (!committer_ident_sufficiently_given()) { + strbuf_addstr(&format, "\n Committer: "); + strbuf_addbuf_percentquote(&format, &committer_ident); + if (advice_implicit_identity) { + strbuf_addch(&format, '\n'); + strbuf_addstr(&format, implicit_ident_advice()); + } + } + strbuf_release(&author_ident); + strbuf_release(&committer_ident); + + init_revisions(&rev, prefix); + setup_revisions(0, NULL, &rev, NULL); + + rev.diff = 1; + rev.diffopt.output_format = + DIFF_FORMAT_SHORTSTAT | DIFF_FORMAT_SUMMARY; + + rev.verbose_header = 1; + rev.show_root_diff = 1; + get_commit_format(format.buf, &rev); + rev.always_show_header = 0; + rev.diffopt.detect_rename = 1; + rev.diffopt.break_opt = 0; + diff_setup_done(&rev.diffopt); + + head = resolve_ref_unsafe("HEAD", 0, NULL, NULL); + if (!strcmp(head, "HEAD")) + head = _("detached HEAD"); + else + skip_prefix(head, "refs/heads/", &head); + printf("[%s%s ", head, initial_commit ? _(" (root-commit)") : ""); + + if (!log_tree_commit(&rev, commit)) { + rev.always_show_header = 1; + rev.use_terminator = 1; + log_tree_commit(&rev, commit); + } + + strbuf_release(&format); +} + +static int git_commit_config(const char *k, const char *v, void *cb) +{ + struct wt_status *s = cb; + int status; + + 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, &is_bool); + return 0; + } + + status = git_gpg_config(k, v, NULL); + if (status) + return status; + return git_status_config(k, v, s); +} + +static int run_rewrite_hook(const struct object_id *oldoid, + const struct object_id *newoid) +{ + struct child_process proc = CHILD_PROCESS_INIT; + const char *argv[3]; + int code; + struct strbuf sb = STRBUF_INIT; + + argv[0] = find_hook("post-rewrite"); + if (!argv[0]) + return 0; + + argv[1] = "amend"; + argv[2] = NULL; + + proc.argv = argv; + proc.in = -1; + proc.stdout_to_stderr = 1; + + code = start_command(&proc); + if (code) + return code; + strbuf_addf(&sb, "%s %s\n", oid_to_hex(oldoid), oid_to_hex(newoid)); + sigchain_push(SIGPIPE, SIG_IGN); + write_in_full(proc.in, sb.buf, sb.len); + close(proc.in); + strbuf_release(&sb); + sigchain_pop(SIGPIPE); + return finish_command(&proc); +} + +int run_commit_hook(int editor_is_used, const char *index_file, const char *name, ...) +{ + struct argv_array hook_env = ARGV_ARRAY_INIT; + va_list args; + int ret; + + argv_array_pushf(&hook_env, "GIT_INDEX_FILE=%s", index_file); + + /* + * Let the hook know that no editor will be launched. + */ + if (!editor_is_used) + argv_array_push(&hook_env, "GIT_EDITOR=:"); + + va_start(args, name); + ret = run_hook_ve(hook_env.argv,name, args); + va_end(args); + argv_array_clear(&hook_env); + + return ret; +} + +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")), + OPT_STRING(0, "fixup", &fixup_message, N_("commit"), N_("use autosquash formatted message to fixup 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_BOOL('s', "signoff", &signoff, N_("add Signed-off-by:")), + OPT_FILENAME('t', "template", &template_file, N_("use specified template file")), + OPT_BOOL('e', "edit", &edit_flag, N_("force edit of commit")), + OPT_STRING(0, "cleanup", &cleanup_arg, N_("default"), N_("how to strip spaces and #comments from message")), + 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_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" }, + /* 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; + char *nl; + struct object_id oid; + struct commit_list *parents = NULL; + struct stat statbuf; + struct commit *current_head = NULL; + struct commit_extra_header *extra = NULL; + struct ref_transaction *transaction; + struct strbuf err = STRBUF_INIT; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(builtin_commit_usage, builtin_commit_options); + + status_init_config(&s, git_commit_config); + s.commit_template = 1; + status_format = STATUS_FORMAT_NONE; /* Ignore status.short */ + s.colopts = 0; + + if (get_oid("HEAD", &oid)) + current_head = NULL; + else { + current_head = lookup_commit_or_die(&oid, "HEAD"); + if (parse_commit(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(argc, argv, prefix, current_head, &s); + index_file = prepare_index(argc, 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)) { + rollback_index_files(); + return 1; + } + + /* 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(), "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(), &statbuf)) { + if (strbuf_read_file(&sb, git_path_merge_mode(), 0) < 0) + die_errno(_("could not read MERGE_MODE")); + if (!strcmp(sb.buf, "no-ff")) + allow_fast_forward = 0; + } + if (allow_fast_forward) + parents = reduce_heads(parents); + } else { + if (!reflog_msg) + reflog_msg = (whence == FROM_CHERRY_PICK) + ? "commit (cherry-pick)" + : "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)); + } + + if (verbose || /* Truncate the message just before the diff, if any. */ + cleanup_mode == CLEANUP_SCISSORS) + strbuf_setlen(&sb, wt_status_locate_end(sb.buf, sb.len)); + if (cleanup_mode != CLEANUP_NONE) + strbuf_stripspace(&sb, cleanup_mode == CLEANUP_ALL); + + if (message_is_empty(&sb) && !allow_empty_message) { + rollback_index_files(); + fprintf(stderr, _("Aborting commit due to empty commit message.\n")); + exit(1); + } + if (template_untouched(&sb) && !allow_empty_message) { + rollback_index_files(); + fprintf(stderr, _("Aborting commit; you did not edit the message.\n")); + exit(1); + } + + if (amend) { + const char *exclude_gpgsig[2] = { "gpgsig", 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, active_cache_tree->oid.hash, + parents, oid.hash, author_ident.buf, sign_commit, extra)) { + rollback_index_files(); + die(_("failed to write commit object")); + } + strbuf_release(&author_ident); + free_commit_extra_headers(extra); + + nl = strchr(sb.buf, '\n'); + if (nl) + strbuf_setlen(&sb, nl + 1 - sb.buf); + else + strbuf_addch(&sb, '\n'); + strbuf_insert(&sb, 0, reflog_msg, strlen(reflog_msg)); + strbuf_insert(&sb, strlen(reflog_msg), ": ", 2); + + transaction = ref_transaction_begin(&err); + if (!transaction || + ref_transaction_update(transaction, "HEAD", oid.hash, + current_head + ? current_head->object.oid.hash : null_sha1, + 0, sb.buf, &err) || + ref_transaction_commit(transaction, &err)) { + rollback_index_files(); + die("%s", err.buf); + } + ref_transaction_free(transaction); + + unlink(git_path_cherry_pick_head()); + unlink(git_path_revert_head()); + unlink(git_path_merge_head()); + unlink(git_path_merge_msg()); + unlink(git_path_merge_mode()); + unlink(git_path_squash_msg()); + + 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 reset HEAD\" to recover.")); + + rerere(0); + run_commit_hook(use_editor, get_index_file(), "post-commit", NULL); + if (amend && !no_post_rewrite) { + struct notes_rewrite_cfg *cfg; + cfg = init_copy_notes_for_rewrite("amend"); + if (cfg) { + /* we are amending, so current_head is not NULL */ + copy_note_for_rewrite(cfg, ¤t_head->object.oid, &oid); + finish_copy_notes_for_rewrite(cfg, "Notes added by 'git commit --amend'"); + } + run_rewrite_hook(¤t_head->object.oid, &oid); + } + if (!quiet) + print_summary(prefix, &oid, !current_head); + + UNLEAK(err); + UNLEAK(sb); + return 0; +} diff --git a/builtin/config.c b/builtin/config.c new file mode 100644 index 0000000000..d13daeeb55 --- /dev/null +++ b/builtin/config.c @@ -0,0 +1,736 @@ +#include "builtin.h" +#include "cache.h" +#include "config.h" +#include "color.h" +#include "parse-options.h" +#include "urlmatch.h" +#include "quote.h" + +static const char *const builtin_config_usage[] = { + N_("git config [<options>]"), + NULL +}; + +static char *key; +static regex_t *key_regexp; +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 struct git_config_source given_config_source; +static int actions, types; +static int end_null; +static int respect_includes_opt = -1; +static struct config_options config_options; +static int show_origin; + +#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) + +#define TYPE_BOOL (1<<0) +#define TYPE_INT (1<<1) +#define TYPE_BOOL_OR_INT (1<<2) +#define TYPE_PATH (1<<3) + +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_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-regex]"), ACTION_GET), + OPT_BIT(0, "get-all", &actions, N_("get all values: key [value-regex]"), ACTION_GET_ALL), + OPT_BIT(0, "get-regexp", &actions, N_("get values for regexp: name-regex [value-regex]"), 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_regex]"), 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-regex]"), ACTION_UNSET), + OPT_BIT(0, "unset-all", &actions, N_("remove all matches: name [value-regex]"), 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_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_BIT(0, "bool", &types, N_("value is \"true\" or \"false\""), TYPE_BOOL), + OPT_BIT(0, "int", &types, N_("value is decimal number"), TYPE_INT), + OPT_BIT(0, "bool-or-int", &types, N_("value is --bool or --int"), TYPE_BOOL_OR_INT), + OPT_BIT(0, "path", &types, N_("value is a path (file or directory name)"), TYPE_PATH), + OPT_GROUP(N_("Other")), + OPT_BOOL('z', "null", &end_null, 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_END(), +}; + +static void check_argc(int argc, int min, int max) { + if (argc >= min && argc <= max) + return; + error("wrong number of arguments"); + usage_with_options(builtin_config_usage, builtin_config_options); +} + +static void show_config_origin(struct strbuf *buf) +{ + const char term = end_null ? '\0' : '\t'; + + strbuf_addstr(buf, current_config_origin_type()); + strbuf_addch(buf, ':'); + if (end_null) + strbuf_addstr(buf, current_config_name()); + else + quote_c_style(current_config_name(), buf, NULL, 0); + strbuf_addch(buf, term); +} + +static int show_all_config(const char *key_, const char *value_, void *cb) +{ + if (show_origin) { + struct strbuf buf = STRBUF_INIT; + show_config_origin(&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_) +{ + if (show_origin) + show_config_origin(buf); + if (show_keys) + strbuf_addstr(buf, key_); + if (!omit_values) { + if (show_keys) + strbuf_addch(buf, key_delim); + + if (types == TYPE_INT) + strbuf_addf(buf, "%"PRId64, + git_config_int64(key_, value_ ? value_ : "")); + else if (types == TYPE_BOOL) + strbuf_addstr(buf, git_config_bool(key_, value_) ? + "true" : "false"); + else if (types == TYPE_BOOL_OR_INT) { + int is_bool, v; + v = git_config_bool_or_int(key_, value_, &is_bool); + if (is_bool) + strbuf_addstr(buf, v ? "true" : "false"); + else + strbuf_addf(buf, "%d", v); + } else if (types == TYPE_PATH) { + const char *v; + if (git_config_pathname(&v, key_, value_) < 0) + return -1; + strbuf_addstr(buf, v); + free((char *)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_, void *cb) +{ + struct strbuf_list *values = cb; + + if (!use_key_regexp && strcmp(key_, key)) + return 0; + if (use_key_regexp && regexec(key_regexp, key_, 0, NULL, 0)) + 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_); +} + +static int get_value(const char *key_, const char *regex_) +{ + 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_) { + 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, &config_options); + + 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) +{ + if (!value) + return NULL; + + if (types == 0 || types == TYPE_PATH) + /* + * 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. + */ + return xstrdup(value); + if (types == TYPE_INT) + return xstrfmt("%"PRId64, git_config_int64(key, value)); + if (types == TYPE_BOOL) + return xstrdup(git_config_bool(key, value) ? "true" : "false"); + if (types == TYPE_BOOL_OR_INT) { + int is_bool, v; + v = git_config_bool_or_int(key, value, &is_bool); + if (!is_bool) + return xstrfmt("%d", v); + else + return xstrdup(v ? "true" : "false"); + } + + die("BUG: cannot normalize type %d", types); +} + +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, void *cb) +{ + 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, &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, + void *cb) +{ + 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, &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; +}; + +static int urlmatch_collect_fn(const char *var, const char *value, 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; + + if (!matched) { + matched = xmalloc(sizeof(*matched)); + strbuf_init(&matched->value, 0); + item->util = matched; + } else { + strbuf_reset(&matched->value); + } + + 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 = { STRING_LIST_INIT_DUP }; + 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, &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); + fwrite(buf.buf, 1, buf.len, stdout); + strbuf_release(&buf); + + strbuf_release(&matched->value); + } + string_list_clear(&config.vars, 1); + 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; + + given_config_source.file = 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 + + !!given_config_source.file + !!given_config_source.blob > 1) { + error("only one config file at a time."); + usage_with_options(builtin_config_usage, builtin_config_options); + } + + if (use_local_config && nongit) + die(_("--local 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; + } + + if (use_global_config) { + char *user_config = expand_user_path("~/.gitconfig", 0); + char *xdg_config = xdg_config_home("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"); + + 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_etc_gitconfig(); + else if (use_local_config) + given_config_source.file = git_pathdup("config"); + 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); + } + + 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_null) { + term = '\0'; + delim = '\n'; + key_delim = '\n'; + } + + if (HAS_MULTI_BITS(types)) { + error("only one type at a time."); + usage_with_options(builtin_config_usage, builtin_config_options); + } + + if ((actions & (ACTION_GET_COLOR|ACTION_GET_COLORBOOL)) && types) { + error("--get-color and variable type are incoherent"); + usage_with_options(builtin_config_usage, builtin_config_options); + } + + if (HAS_MULTI_BITS(actions)) { + error("only one action at a time."); + usage_with_options(builtin_config_usage, builtin_config_options); + } + 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_with_options(builtin_config_usage, builtin_config_options); + } + if (omit_values && + !(actions == ACTION_LIST || actions == ACTION_GET_REGEXP)) { + error("--name-only is only applicable to --list or --get-regexp"); + usage_with_options(builtin_config_usage, builtin_config_options); + } + + 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_with_options(builtin_config_usage, builtin_config_options); + } + + if (actions == ACTION_LIST) { + check_argc(argc, 0, 0); + if (config_with_options(show_all_config, NULL, + &given_config_source, + &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) { + int ret; + check_write(); + check_argc(argc, 2, 2); + value = normalize_value(argv[0], argv[1]); + UNLEAK(value); + 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]); + return ret; + } + else if (actions == ACTION_SET_ALL) { + check_write(); + check_argc(argc, 2, 3); + value = normalize_value(argv[0], argv[1]); + UNLEAK(value); + return git_config_set_multivar_in_file_gently(given_config_source.file, + argv[0], value, argv[2], 0); + } + else if (actions == ACTION_ADD) { + check_write(); + check_argc(argc, 2, 2); + value = normalize_value(argv[0], argv[1]); + UNLEAK(value); + return git_config_set_multivar_in_file_gently(given_config_source.file, + argv[0], value, + CONFIG_REGEX_NONE, 0); + } + else if (actions == ACTION_REPLACE_ALL) { + check_write(); + check_argc(argc, 2, 3); + value = normalize_value(argv[0], argv[1]); + UNLEAK(value); + return git_config_set_multivar_in_file_gently(given_config_source.file, + argv[0], value, argv[2], 1); + } + else if (actions == ACTION_GET) { + check_argc(argc, 1, 2); + return get_value(argv[0], argv[1]); + } + else if (actions == ACTION_GET_ALL) { + do_all = 1; + check_argc(argc, 1, 2); + return get_value(argv[0], argv[1]); + } + 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]); + } + 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], 0); + 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], 1); + } + else if (actions == ACTION_RENAME_SECTION) { + int ret; + 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; + if (ret == 0) + die("No such section!"); + } + else if (actions == ACTION_REMOVE_SECTION) { + int ret; + 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; + if (ret == 0) + die("No such section!"); + } + 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); + } + + return 0; +} diff --git a/builtin/count-objects.c b/builtin/count-objects.c new file mode 100644 index 0000000000..33343818c8 --- /dev/null +++ b/builtin/count-objects.c @@ -0,0 +1,171 @@ +/* + * Builtin "git count-objects". + * + * Copyright (c) 2006 Junio C Hamano + */ + +#include "cache.h" +#include "config.h" +#include "dir.h" +#include "builtin.h" +#include "parse-options.h" +#include "quote.h" +#include "packfile.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) +{ + 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_sha1_pack(oid->hash)) + packed_loose++; + } + return 0; +} + +static int count_cruft(const char *basename, const char *path, void *data) +{ + loose_garbage(path); + return 0; +} + +static int print_alternate(struct alternate_object_database *alt, void *data) +{ + printf("alternate: "); + quote_c_style(alt->path, NULL, stdout, 0); + putchar('\n'); + return 0; +} + +static char const * const count_objects_usage[] = { + N_("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; + if (!packed_git) + prepare_packed_git(); + for (p = packed_git; 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.c b/builtin/credential.c new file mode 100644 index 0000000000..879acfbcda --- /dev/null +++ b/builtin/credential.c @@ -0,0 +1,31 @@ +#include "git-compat-util.h" +#include "credential.h" +#include "builtin.h" + +static const char usage_msg[] = + "git credential [fill|approve|reject]"; + +int cmd_credential(int argc, const char **argv, const char *prefix) +{ + const char *op; + struct credential c = CREDENTIAL_INIT; + + 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 0000000000..29075dbd0f --- /dev/null +++ b/builtin/describe.c @@ -0,0 +1,571 @@ +#include "cache.h" +#include "config.h" +#include "lockfile.h" +#include "commit.h" +#include "tag.h" +#include "refs.h" +#include "builtin.h" +#include "exec_cmd.h" +#include "parse-options.h" +#include "diff.h" +#include "hashmap.h" +#include "argv-array.h" +#include "run-command.h" + +#define SEEN (1u << 0) +#define MAX_TAGS (FLAG_BITS - 1) + +static const char * const describe_usage[] = { + N_("git describe [<options>] [<commit-ish>...]"), + N_("git describe [<options>] --dirty"), + 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 = 10; +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; + +/* 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; + struct object_id oid; + char *path; +}; + +static const char *prio_names[] = { + N_("head"), N_("lightweight"), N_("annotated"), +}; + +static int commit_name_cmp(const void *unused_cmp_data, + const void *entry, + const void *entry_or_key, + const void *peeled) +{ + const struct commit_name *cn1 = entry; + const struct commit_name *cn2 = entry_or_key; + + return oidcmp(&cn1->peeled, peeled ? peeled : &cn2->peeled); +} + +static inline struct commit_name *find_commit_name(const struct object_id *peeled) +{ + return hashmap_get_from_hash(&names, sha1hash(peeled->hash), peeled->hash); +} + +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(&e->oid); + if (!t || parse_tag(t)) + return 1; + e->tag = t; + } + + t = lookup_tag(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, sha1hash(peeled->hash)); + hashmap_add(&names, e); + e->path = NULL; + } + e->tag = tag; + e->prio = prio; + e->name_checked = 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, void *cb_data) +{ + 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_ref(path, peeled.hash)) { + is_annotated = !!oidcmp(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; + parse_commit(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 display_name(struct commit_name *n) +{ + if (n->prio == 2 && !n->tag) { + n->tag = lookup_tag(&n->oid); + if (!n->tag || parse_tag(n->tag)) + die(_("annotated tag %s not available"), n->path); + } + if (n->tag && !n->name_checked) { + if (!n->tag->tag) + die(_("annotated tag %s has no embedded name"), n->path); + if (strcmp(n->tag->tag, all ? n->path + 5 : n->path)) + warning(_("tag '%s' is really '%s' here"), n->tag->tag, n->path); + n->name_checked = 1; + } + + if (n->tag) + printf("%s", n->tag->tag); + else + printf("%s", n->path); +} + +static void show_suffix(int depth, const struct object_id *oid) +{ + printf("-%d-g%s", depth, find_unique_abbrev(oid->hash, abbrev)); +} + +static void describe(const char *arg, int last_one) +{ + struct object_id oid; + 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; + + if (get_oid(arg, &oid)) + die(_("Not a valid object name %s"), arg); + cmit = lookup_commit_reference(&oid); + if (!cmit) + die(_("%s is not a valid '%s' object"), arg, commit_type); + + n = find_commit_name(&cmit->object.oid); + if (n && (tags || all || n->prio == 2)) { + /* + * Exact match to an existing ref. + */ + display_name(n); + if (longformat) + show_suffix(0, n->tag ? &n->tag->tagged->oid : &oid); + if (suffix) + printf("%s", suffix); + printf("\n"); + return; + } + + if (!max_candidates) + die(_("no tag exactly matches '%s'"), oid_to_hex(&cmit->object.oid)); + if (debug) + fprintf(stderr, _("searching to describe %s\n"), arg); + + if (!have_util) { + struct hashmap_iter iter; + struct commit *c; + struct commit_name *n = hashmap_iter_first(&names, &iter); + for (; n; n = hashmap_iter_next(&iter)) { + c = lookup_commit_reference_gently(&n->peeled, 1); + if (c) + c->util = 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; + seen_commits++; + n = c->util; + 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++; + } + if (annotated_cnt && !list) { + if (debug) + fprintf(stderr, _("finished search at %s\n"), + oid_to_hex(&c->object.oid)); + break; + } + while (parents) { + struct commit *p = parents->item; + parse_commit(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 *oid = &cmit->object.oid; + if (always) { + printf("%s", find_unique_abbrev(oid->hash, abbrev)); + if (suffix) + printf("%s", suffix); + printf("\n"); + return; + } + if (unannotated_cnt) + die(_("No annotated tags can describe '%s'.\n" + "However, there were unannotated tags: try --tags."), + oid_to_hex(oid)); + else + die(_("No tags can describe '%s'.\n" + "Try --always, or create some tags."), + oid_to_hex(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)); + } + } + + display_name(all_matches[0].name); + if (abbrev) + show_suffix(all_matches[0].depth, &cmit->object.oid); + if (suffix) + printf("%s", suffix); + printf("\n"); + + if (!last_one) + clear_commit_marks(cmit, -1); +} + +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_SET_INT(0, "exact-match", &max_candidates, + N_("only output exact matches"), 0), + 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(_("--long is incompatible with --abbrev=0")); + + if (contains) { + struct string_list_item *item; + struct argv_array args; + + argv_array_init(&args); + argv_array_pushl(&args, "name-rev", + "--peel-tag", "--name-only", "--no-undefined", + NULL); + if (always) + argv_array_push(&args, "--always"); + if (!all) { + argv_array_push(&args, "--tags"); + for_each_string_list_item(item, &patterns) + argv_array_pushf(&args, "--refs=refs/tags/%s", item->string); + for_each_string_list_item(item, &exclude_patterns) + argv_array_pushf(&args, "--exclude=refs/tags/%s", item->string); + } + if (argc) + argv_array_pushv(&args, argv); + else + argv_array_push(&args, "HEAD"); + return cmd_name_rev(args.argc, args.argv, prefix); + } + + hashmap_init(&names, commit_name_cmp, 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; + argv_array_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) { + static struct lock_file index_lock; + int fd; + + read_cache_preload(NULL); + refresh_index(&the_index, REFRESH_QUIET|REFRESH_UNMERGED, + NULL, NULL, NULL); + fd = hold_locked_index(&index_lock, 0); + if (0 <= fd) + update_index_if_able(&the_index, &index_lock); + + if (!cmd_diff_index(ARRAY_SIZE(diff_index_args) - 1, + diff_index_args, prefix)) + suffix = NULL; + else + suffix = dirty; + } + describe("HEAD", 1); + } else if (dirty) { + die(_("--dirty is incompatible with commit-ishes")); + } else if (broken) { + die(_("--broken is incompatible with commit-ishes")); + } else { + while (argc-- > 0) + describe(*argv++, argc == 0); + } + return 0; +} diff --git a/builtin/diff-files.c b/builtin/diff-files.c new file mode 100644 index 0000000000..e88493ffe5 --- /dev/null +++ b/builtin/diff-files.c @@ -0,0 +1,74 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" +#include "config.h" +#include "diff.h" +#include "commit.h" +#include "revision.h" +#include "builtin.h" +#include "submodule.h" + +static const char diff_files_usage[] = +"git diff-files [-q] [-0 | -1 | -2 | -3 | -c | --cc] [<common-diff-options>] [<path>...]" +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 */ + init_revisions(&rev, prefix); + rev.abbrev = 0; + precompose_argv(argc, argv); + + 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; + + /* + * 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.combine_merges && + (rev.diffopt.output_format & DIFF_FORMAT_PATCH)) + rev.combine_merges = rev.dense_combined_merges = 1; + + if (read_cache_preload(&rev.diffopt.pathspec) < 0) { + perror("read_cache_preload"); + return -1; + } + result = run_diff_files(&rev, options); + return diff_result_code(&rev.diffopt, result); +} diff --git a/builtin/diff-index.c b/builtin/diff-index.c new file mode 100644 index 0000000000..522f4fdffd --- /dev/null +++ b/builtin/diff-index.c @@ -0,0 +1,61 @@ +#include "cache.h" +#include "config.h" +#include "diff.h" +#include "commit.h" +#include "revision.h" +#include "builtin.h" +#include "submodule.h" + +static const char diff_cache_usage[] = +"git diff-index [-m] [--cached] " +"[<common-diff-options>] <tree-ish> [<path>...]" +COMMON_DIFF_OPTIONS_HELP; + +int cmd_diff_index(int argc, const char **argv, const char *prefix) +{ + struct rev_info rev; + int cached = 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 */ + init_revisions(&rev, prefix); + rev.abbrev = 0; + precompose_argv(argc, argv); + + argc = setup_revisions(argc, argv, &rev, NULL); + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + + if (!strcmp(arg, "--cached")) + cached = 1; + else + usage(diff_cache_usage); + } + if (!rev.diffopt.output_format) + rev.diffopt.output_format = DIFF_FORMAT_RAW; + + /* + * 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 (!cached) { + setup_work_tree(); + if (read_cache_preload(&rev.diffopt.pathspec) < 0) { + perror("read_cache_preload"); + return -1; + } + } else if (read_cache() < 0) { + perror("read_cache"); + return -1; + } + result = run_diff_index(&rev, cached); + UNLEAK(rev); + return diff_result_code(&rev.diffopt, result); +} diff --git a/builtin/diff-tree.c b/builtin/diff-tree.c new file mode 100644 index 0000000000..d66499909e --- /dev/null +++ b/builtin/diff-tree.c @@ -0,0 +1,186 @@ +#include "cache.h" +#include "config.h" +#include "diff.h" +#include "commit.h" +#include "log-tree.h" +#include "builtin.h" +#include "submodule.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(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(&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(&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(&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), typename(obj->type)); + return -1; +} + +static const char diff_tree_usage[] = +"git diff-tree [--stdin] [-m] [-c] [--cc] [-s] [-v] [--pretty] [-t] [-r] [--root] " +"[<common-diff-options>] <tree-ish> [<tree-ish>] [<path>...]\n" +" -r diff recursively\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, struct setup_revision_opt *opt) +{ + 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; + int read_stdin = 0; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage(diff_tree_usage); + + git_config(git_diff_basic_config, NULL); /* no "diff" UI options */ + init_revisions(opt, prefix); + 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; + + precompose_argv(argc, argv); + argc = setup_revisions(argc, argv, opt, &s_r_opt); + + while (--argc > 0) { + const char *arg = *++argv; + + if (!strcmp(arg, "--stdin")) { + read_stdin = 1; + continue; + } + usage(diff_tree_usage); + } + + /* + * 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 (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; + + if (opt->diffopt.detect_rename) + opt->diffopt.setup |= (DIFF_SETUP_USE_SIZE_CACHE | + DIFF_SETUP_USE_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; + } + + return diff_result_code(&opt->diffopt, 0); +} diff --git a/builtin/diff.c b/builtin/diff.c new file mode 100644 index 0000000000..f5bbd4d757 --- /dev/null +++ b/builtin/diff.c @@ -0,0 +1,471 @@ +/* + * Builtin "git diff" + * + * Copyright (c) 2006 Junio C Hamano + */ +#include "cache.h" +#include "config.h" +#include "lockfile.h" +#include "color.h" +#include "commit.h" +#include "blob.h" +#include "tag.h" +#include "diff.h" +#include "diffcore.h" +#include "revision.h" +#include "log-tree.h" +#include "builtin.h" +#include "submodule.h" +#include "sha1-array.h" + +#define DIFF_NO_INDEX_EXPLICIT 1 +#define DIFF_NO_INDEX_IMPLICIT 2 + +static const char builtin_diff_usage[] = +"git diff [<options>] [<commit> [<commit>]] [--] [<path>...]"; + +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) && + !oidcmp(old_oid, new_oid) && (old_mode == new_mode)) + return; + + if (DIFF_OPT_TST(opt, 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 int builtin_diff_b_f(struct rev_info *revs, + int argc, const char **argv, + 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); + return 0; +} + +static int builtin_diff_blobs(struct rev_info *revs, + int argc, const char **argv, + struct object_array_entry **blob) +{ + 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); + return 0; +} + +static int builtin_diff_index(struct rev_info *revs, + int argc, const char **argv) +{ + int cached = 0; + while (1 < argc) { + const char *arg = argv[1]; + if (!strcmp(arg, "--cached") || !strcmp(arg, "--staged")) + cached = 1; + 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 (!cached) { + setup_work_tree(); + if (read_cache_preload(&revs->diffopt.pathspec) < 0) { + perror("read_cache_preload"); + return -1; + } + } else if (read_cache() < 0) { + perror("read_cache"); + return -1; + } + return run_diff_index(revs, cached); +} + +static int 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]); + int swap = 0; + + if (argc > 1) + usage(builtin_diff_usage); + + /* + * 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); + return 0; +} + +static int builtin_diff_combined(struct rev_info *revs, + int argc, const char **argv, + struct object_array_entry *ent, + int ents) +{ + struct oid_array parents = OID_ARRAY_INIT; + int i; + + if (argc > 1) + usage(builtin_diff_usage); + + if (!revs->dense_combined_merges && !revs->combine_merges) + revs->dense_combined_merges = revs->combine_merges = 1; + for (i = 1; i < ents; i++) + oid_array_append(&parents, &ent[i].item->oid); + diff_tree_combined(&ent[0].item->oid, &parents, + revs->dense_combined_merges, revs); + oid_array_clear(&parents); + return 0; +} + +static void refresh_index_quietly(void) +{ + struct lock_file *lock_file; + int fd; + + lock_file = xcalloc(1, sizeof(struct lock_file)); + fd = hold_locked_index(lock_file, 0); + if (fd < 0) + return; + discard_cache(); + read_cache(); + refresh_cache(REFRESH_QUIET|REFRESH_UNMERGED); + update_index_if_able(&the_index, lock_file); +} + +static int 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 + return error(_("invalid option: %s"), argv[1]); + 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->combine_merges && + (revs->diffopt.output_format & DIFF_FORMAT_PATCH)) + revs->combine_merges = revs->dense_combined_merges = 1; + + setup_work_tree(); + if (read_cache_preload(&revs->diffopt.pathspec) < 0) { + perror("read_cache_preload"); + return -1; + } + return run_diff_files(revs, options); +} + +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 blobs = 0, paths = 0; + struct object_array_entry *blob[2]; + int nongit = 0, no_index = 0; + int result = 0; + + /* + * We could get N tree-ish in the rev.pending_objects list. + * Also there could be M blobs there, and P pathspecs. + * + * N=0, M=0: + * cache vs files (diff-files) + * 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 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 (!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); + precompose_argv(argc, argv); + + init_revisions(&rev, prefix); + + if (no_index && argc != i + 2) { + if (no_index == DIFF_NO_INDEX_IMPLICIT) { + /* + * There was no --no-index and there were not two + * paths. It is possible that the user intended + * to do an inside-repository operation. + */ + fprintf(stderr, "Not a git repository\n"); + fprintf(stderr, + "To compare two paths outside a working tree:\n"); + } + /* Give the usage message for non-repository usage and exit. */ + usagef("git diff %s <path> <path>", + no_index == DIFF_NO_INDEX_EXPLICIT ? + "--no-index" : "[--no-index]"); + + } + if (no_index) + /* If this is a no-index diff, just run it and exit there. */ + diff_no_index(&rev, argc, argv); + + /* Otherwise, we are doing the usual "git" diff */ + rev.diffopt.skip_stat_unmatch = !!diff_auto_refresh_index; + + /* Scale to real terminal size and respect statGraphWidth config */ + rev.diffopt.stat_width = -1; + rev.diffopt.stat_graph_width = -1; + + /* Default to let external and textconv be used */ + DIFF_OPT_SET(&rev.diffopt, ALLOW_EXTERNAL); + DIFF_OPT_SET(&rev.diffopt, ALLOW_TEXTCONV); + + 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); + } + + DIFF_OPT_SET(&rev.diffopt, RECURSIVE); + + 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(&empty_tree_oid); + add_pending_object(&rev, &tree->object, "HEAD"); + } + break; + } + } + } + + 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(&obj->oid); + obj = deref_tag(obj, NULL, 0); + if (!obj) + die(_("invalid object '%s' given."), name); + if (obj->type == OBJ_COMMIT) + obj = &((struct commit *)obj)->tree->object; + + if (obj->type == OBJ_TREE) { + obj->flags |= flags; + add_object_array(obj, name, &ent); + } 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: + result = builtin_diff_files(&rev, argc, argv); + break; + case 1: + if (paths != 1) + usage(builtin_diff_usage); + result = builtin_diff_b_f(&rev, argc, argv, blob); + break; + case 2: + if (paths) + usage(builtin_diff_usage); + result = 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) + result = builtin_diff_index(&rev, argc, argv); + else if (ent.nr == 2) + result = builtin_diff_tree(&rev, argc, argv, + &ent.objects[0], &ent.objects[1]); + else if (ent.objects[0].item->flags & UNINTERESTING) { + /* + * diff A...B where there is at least one merge base + * between A and B. We have ent.objects[0] == + * merge-base, ent.objects[ents-2] == A, and + * ent.objects[ents-1] == B. Show diff between the + * base and B. Note that we pick one merge base at + * random if there are more than one. + */ + result = builtin_diff_tree(&rev, argc, argv, + &ent.objects[0], + &ent.objects[ent.nr-1]); + } else + result = builtin_diff_combined(&rev, argc, argv, + ent.objects, ent.nr); + result = diff_result_code(&rev.diffopt, result); + if (1 < rev.diffopt.skip_stat_unmatch) + refresh_index_quietly(); + UNLEAK(rev); + UNLEAK(ent); + UNLEAK(blob); + return result; +} diff --git a/builtin/difftool.c b/builtin/difftool.c new file mode 100644 index 0000000000..b2d3ba7539 --- /dev/null +++ b/builtin/difftool.c @@ -0,0 +1,762 @@ +/* + * "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 + */ +#include "cache.h" +#include "config.h" +#include "builtin.h" +#include "run-command.h" +#include "exec_cmd.h" +#include "parse-options.h" +#include "argv-array.h" +#include "strbuf.h" +#include "lockfile.h" +#include "dir.h" + +static char *diff_gui_tool; +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, void *cb) +{ + if (!strcmp(var, "diff.guitool")) { + diff_gui_tool = xstrdup(value); + return 0; + } + + if (!strcmp(var, "difftool.trustexitcode")) { + trust_exit_code = git_config_bool(var, value); + return 0; + } + + return git_default_config(var, value, cb); +} + +static int print_tool_help(void) +{ + const char *argv[] = { "mergetool", "--tool-help=diff", NULL }; + return run_command_v_opt(argv, RUN_GIT_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 (get_oid_hex(++p, oid1)) + return error("expected object ID, got '%s'", p + 1); + p += GIT_SHA1_HEXSZ; + if (*p != ' ') + return error("expected ' ', got '%c'", *p); + if (get_oid_hex(++p, oid2)) + return error("expected object ID, got '%s'", p + 1); + p += GIT_SHA1_HEXSZ; + 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(&wt_oid, fd, &st, OBJ_BLOB, name, 0)) { + if (is_null_oid(oid)) { + oidcpy(oid, &wt_oid); + use = 1; + } else if (!oidcmp(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 *unused_cmp_data, + const void *entry, + const void *entry_or_key, + const void *unused_keydata) +{ + const struct working_tree_entry *a = entry; + const struct working_tree_entry *b = entry_or_key; + 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 *unused_cmp_data, + const void *entry, + const void *entry_or_key, + const void *unused_keydata) +{ + const struct pair_entry *a = entry; + const struct pair_entry *b = entry_or_key; + + 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, strhash(path)); + existing = hashmap_get(map, e, NULL); + if (existing) { + free(e); + e = existing; + } else { + e->left[0] = e->right[0] = '\0'; + hashmap_add(map, e); + } + 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 *unused_cmp_data, + const void *entry, + const void *entry_or_key, + const void *key) +{ + const struct path_entry *a = entry; + const struct path_entry *b = entry_or_key; + + 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 index_env = STRBUF_INIT, buf = STRBUF_INIT; + const char *git_dir = absolute_path(get_git_dir()), *env[] = { + NULL, NULL + }; + FILE *fp; + + strbuf_addf(&index_env, "GIT_INDEX_FILE=%s", index_path); + env[0] = index_env.buf; + + argv_array_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; + update_index.env = env; + /* Ignore any errors of update-index */ + run_command(&update_index); + + argv_array_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; + diff_files.env = env; + 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, strhash(buf.buf)); + hashmap_add(result, entry); + } + fclose(fp); + if (finish_command(&diff_files)) + die("diff-files did not exit properly"); + strbuf_release(&index_env); + strbuf_release(&buf); +} + +static NORETURN void exit_cleanup(const char *tmpdir, int exit_code) +{ + struct strbuf buf = STRBUF_INIT; + strbuf_addstr(&buf, tmpdir); + remove_dir_recursively(&buf, 0); + if (exit_code) + warning(_("failed: %d"), exit_code); + exit(exit_code); +} + +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 = read_sha1_file(oid->hash, &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_cache_entry(mode, oid->hash, path, 0, 0); + ret = checkout_entry(ce, state, NULL); + + free(ce); + return ret; +} + +static int run_dir_diff(const char *extcmd, int symlinks, const char *prefix, + int argc, const char **argv) +{ + char tmpdir[PATH_MAX]; + 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; + char *lbase_dir, *rbase_dir; + size_t ldir_len, rdir_len, wtdir_len; + const char *workdir, *tmp; + int ret = 0, i; + FILE *fp; + struct hashmap working_tree_dups, submodules, symlinks2; + struct hashmap_iter iter; + struct pair_entry *entry; + struct index_state wtindex; + struct checkout lstate, rstate; + int rc, flags = RUN_GIT_CMD, err = 0; + struct child_process child = CHILD_PROCESS_INIT; + const char *helper_argv[] = { "difftool--helper", NULL, NULL, NULL }; + struct hashmap wt_modified, tmp_modified; + int indices_loaded = 0; + + workdir = get_git_work_tree(); + + /* Setup temp directories */ + tmp = getenv("TMPDIR"); + xsnprintf(tmpdir, sizeof(tmpdir), "%s/git-difftool.XXXXXX", tmp ? tmp : "/tmp"); + if (!mkdtemp(tmpdir)) + return error("could not create '%s'", tmpdir); + strbuf_addf(&ldir, "%s/left/", tmpdir); + strbuf_addf(&rdir, "%s/right/", tmpdir); + 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(&wtindex, 0, sizeof(wtindex)); + + 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; + + hashmap_init(&working_tree_dups, working_tree_entry_cmp, NULL, 0); + hashmap_init(&submodules, pair_cmp, NULL, 0); + hashmap_init(&symlinks2, pair_cmp, NULL, 0); + + child.no_stdin = 1; + child.git_cmd = 1; + child.use_shell = 0; + child.clean_on_exit = 1; + child.dir = prefix; + child.out = -1; + argv_array_pushl(&child.args, "diff", "--raw", "--no-abbrev", "-z", + NULL); + for (i = 0; i < argc; i++) + argv_array_push(&child.args, argv[i]); + 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 (!oidcmp(&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, strhash(dst_path)); + if (hashmap_get(&working_tree_dups, entry, NULL)) { + free(entry); + continue; + } + hashmap_add(&working_tree_dups, 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(rmode, roid.hash, + 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_iter_init(&submodules, &iter); + while ((entry = hashmap_iter_next(&iter))) { + if (*entry->left) { + add_path(&ldir, ldir_len, entry->path); + ensure_leading_directories(ldir.buf); + write_file(ldir.buf, "%s", entry->left); + } + if (*entry->right) { + add_path(&rdir, rdir_len, entry->path); + ensure_leading_directories(rdir.buf); + write_file(rdir.buf, "%s", entry->right); + } + } + + /* + * 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_iter_init(&symlinks2, &iter); + while ((entry = hashmap_iter_next(&iter))) { + if (*entry->left) { + add_path(&ldir, ldir_len, entry->path); + ensure_leading_directories(ldir.buf); + write_file(ldir.buf, "%s", entry->left); + } + if (*entry->right) { + add_path(&rdir, rdir_len, entry->path); + ensure_leading_directories(rdir.buf); + write_file(rdir.buf, "%s", entry->right); + } + } + + strbuf_release(&buf); + + strbuf_setlen(&ldir, ldir_len); + helper_argv[1] = ldir.buf; + strbuf_setlen(&rdir, rdir_len); + helper_argv[2] = rdir.buf; + + if (extcmd) { + helper_argv[0] = extcmd; + flags = 0; + } else + setenv("GIT_DIFFTOOL_DIRDIFF", "true", 1); + rc = run_command_v_opt(helper_argv, flags); + + /* + * 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) { + static struct lock_file lock; + strbuf_reset(&buf); + strbuf_addf(&buf, "%s/wtindex", tmpdir); + 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); + rollback_lock_file(&lock); + 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); + warning(_("you may want to cleanup or recover these.")); + exit(1); + } else + exit_cleanup(tmpdir, rc); + +finish: + if (fp) + fclose(fp); + + free(lbase_dir); + free(rbase_dir); + strbuf_release(&ldir); + strbuf_release(&rdir); + strbuf_release(&wtdir); + strbuf_release(&buf); + + return ret; +} + +static int run_file_diff(int prompt, const char *prefix, + int argc, const char **argv) +{ + struct argv_array args = ARGV_ARRAY_INIT; + const char *env[] = { + "GIT_PAGER=", "GIT_EXTERNAL_DIFF=git-difftool--helper", NULL, + NULL + }; + int ret = 0, i; + + if (prompt > 0) + env[2] = "GIT_DIFFTOOL_PROMPT=true"; + else if (!prompt) + env[2] = "GIT_DIFFTOOL_NO_PROMPT=true"; + + + argv_array_push(&args, "diff"); + for (i = 0; i < argc; i++) + argv_array_push(&args, argv[i]); + ret = run_command_v_opt_cd_env(args.argv, RUN_GIT_CMD, prefix, env); + exit(ret); +} + +int cmd_difftool(int argc, const char **argv, const char *prefix) +{ + int use_gui_tool = 0, dir_diff = 0, prompt = -1, symlinks = 0, + tool_help = 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")), + { OPTION_SET_INT, 'y', "no-prompt", &prompt, NULL, + N_("do not prompt before launching a diff tool"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 0}, + { OPTION_SET_INT, 0, "prompt", &prompt, NULL, NULL, + PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_HIDDEN, + NULL, 1 }, + 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_END() + }; + + git_config(difftool_config, NULL); + symlinks = has_symlinks; + + argc = parse_options(argc, argv, prefix, builtin_difftool_options, + builtin_difftool_usage, PARSE_OPT_KEEP_UNKNOWN | + PARSE_OPT_KEEP_DASHDASH); + + if (tool_help) + return print_tool_help(); + + /* NEEDSWORK: once we no longer spawn anything, remove this */ + setenv(GIT_DIR_ENVIRONMENT, absolute_path(get_git_dir()), 1); + setenv(GIT_WORK_TREE_ENVIRONMENT, absolute_path(get_git_work_tree()), 1); + + if (use_gui_tool && diff_gui_tool && *diff_gui_tool) + setenv("GIT_DIFF_TOOL", diff_gui_tool, 1); + else 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. + */ + if (dir_diff) + return run_dir_diff(extcmd, symlinks, prefix, argc, argv); + return run_file_diff(prompt, prefix, argc, argv); +} diff --git a/builtin/fast-export.c b/builtin/fast-export.c new file mode 100644 index 0000000000..2fb60d6d48 --- /dev/null +++ b/builtin/fast-export.c @@ -0,0 +1,1092 @@ +/* + * "git fast-export" builtin command + * + * Copyright (C) 2007 Johannes E. Schindelin + */ +#include "builtin.h" +#include "cache.h" +#include "config.h" +#include "refs.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" + +static const char *fast_export_usage[] = { + N_("git fast-export [rev-list-opts]"), + NULL +}; + +static int progress; +static enum { ABORT, VERBATIM, WARN, WARN_STRIP, STRIP } signed_tag_mode = ABORT; +static enum { ERROR, DROP, REWRITE } tag_of_filtered_mode = ERROR; +static int fake_missing_tagger; +static int use_done_feature; +static int no_data; +static int full_tree; +static struct string_list extra_refs = STRING_LIST_INIT_NODUP; +static struct refspec *refspecs; +static int refspecs_nr; +static int anonymize; + +static int parse_opt_signed_tag_mode(const struct option *opt, + const char *arg, int unset) +{ + if (unset || !strcmp(arg, "abort")) + signed_tag_mode = ABORT; + else if (!strcmp(arg, "verbatim") || !strcmp(arg, "ignore")) + signed_tag_mode = VERBATIM; + else if (!strcmp(arg, "warn")) + signed_tag_mode = WARN; + else if (!strcmp(arg, "warn-strip")) + signed_tag_mode = WARN_STRIP; + else if (!strcmp(arg, "strip")) + signed_tag_mode = 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) +{ + if (unset || !strcmp(arg, "abort")) + tag_of_filtered_mode = ERROR; + else if (!strcmp(arg, "drop")) + tag_of_filtered_mode = DROP; + else if (!strcmp(arg, "rewrite")) + tag_of_filtered_mode = REWRITE; + else + return error("Unknown tag-of-filtered mode: %s", arg); + return 0; +} + +static struct decoration idnums; +static uint32_t last_idnum; + +static int has_unshown_parent(struct commit *commit) +{ + struct commit_list *parent; + + for (parent = commit->parents; parent; parent = parent->next) + if (!(parent->item->object.flags & SHOWN) && + !(parent->item->object.flags & UNINTERESTING)) + return 1; + return 0; +} + +struct anonymized_entry { + struct hashmap_entry hash; + const char *orig; + size_t orig_len; + const char *anon; + size_t anon_len; +}; + +static int anonymized_entry_cmp(const void *unused_cmp_data, + const void *va, const void *vb, + const void *unused_keydata) +{ + const struct anonymized_entry *a = va, *b = vb; + return a->orig_len != b->orig_len || + memcmp(a->orig, b->orig, a->orig_len); +} + +/* + * 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 void *anonymize_mem(struct hashmap *map, + void *(*generate)(const void *, size_t *), + const void *orig, size_t *len) +{ + struct anonymized_entry key, *ret; + + if (!map->cmpfn) + hashmap_init(map, anonymized_entry_cmp, NULL, 0); + + hashmap_entry_init(&key, memhash(orig, *len)); + key.orig = orig; + key.orig_len = *len; + ret = hashmap_get(map, &key, NULL); + + if (!ret) { + ret = xmalloc(sizeof(*ret)); + hashmap_entry_init(&ret->hash, key.hash.hash); + ret->orig = xstrdup(orig); + ret->orig_len = *len; + ret->anon = generate(orig, len); + ret->anon_len = *len; + hashmap_put(map, ret); + } + + *len = ret->anon_len; + 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, + void *(*generate)(const void *, size_t *)) +{ + while (*path) { + const char *end_of_component = strchrnul(path, '/'); + size_t len = end_of_component - path; + const char *c = anonymize_mem(map, generate, path, &len); + strbuf_add(out, c, len); + path = end_of_component; + if (*path) + strbuf_addch(out, *path++); + } +} + +/* Since intptr_t is C99, we do not use it here */ +static inline uint32_t *mark_to_ptr(uint32_t mark) +{ + return ((uint32_t *)NULL) + mark; +} + +static inline uint32_t ptr_to_mark(void * mark) +{ + return (uint32_t *)mark - (uint32_t *)NULL; +} + +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 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(oid->hash); + if (object && object->flags & SHOWN) + return; + + if (anonymize) { + buf = anonymize_blob(&size); + object = (struct object *)lookup_blob(oid); + eaten = 0; + } else { + buf = read_sha1_file(oid->hash, &type, &size); + if (!buf) + die ("Could not read blob %s", oid_to_hex(oid)); + if (check_sha1_signature(oid->hash, buf, size, typename(type)) < 0) + die("sha1 mismatch in blob %s", oid_to_hex(oid)); + object = parse_object_buffer(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"\ndata %lu\n", last_idnum, 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 void *anonymize_path_component(const void *path, size_t *len) +{ + static int counter; + struct strbuf out = STRBUF_INIT; + strbuf_addf(&out, "path%d", counter++); + return strbuf_detach(&out, len); +} + +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 void *generate_fake_oid(const void *old, size_t *len) +{ + static uint32_t counter = 1; /* avoid null sha1 */ + unsigned char *out = xcalloc(GIT_SHA1_RAWSZ, 1); + put_be32(out + GIT_SHA1_RAWSZ - 4, counter++); + return out; +} + +static const unsigned char *anonymize_sha1(const struct object_id *oid) +{ + static struct hashmap sha1s; + size_t len = GIT_SHA1_RAWSZ; + return anonymize_mem(&sha1s, generate_fake_oid, oid, &len); +} + +static void show_filemodify(struct diff_queue_struct *q, + struct diff_options *options, 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 (!oidcmp(&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, + sha1_to_hex(anonymize ? + anonymize_sha1(&spec->oid) : + spec->oid.hash)); + else { + struct object *object = lookup_object(spec->oid.hash); + 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 git_commit_encoding; + bol += strlen(needle); + eol = strchrnul(bol, '\n'); + *eol = '\0'; + return bol; +} + +static void *anonymize_ref_component(const void *old, size_t *len) +{ + static int counter; + struct strbuf out = STRBUF_INIT; + strbuf_addf(&out, "ref%d", counter++); + return strbuf_detach(&out, len); +} + +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; + + /* + * We also leave "master" as a special case, since it does not reveal + * anything interesting. + */ + if (!strcmp(refname, "refs/heads/master")) + return refname; + + 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(const char *old) +{ + static int counter; + return xstrfmt("subject %d\n\nbody\n", counter++); +} + +static struct hashmap idents; +static void *anonymize_ident(const void *old, size_t *len) +{ + static int counter; + struct strbuf out = STRBUF_INIT; + strbuf_addf(&out, "User %d <user%d@example.com>", counter, counter); + counter++; + return strbuf_detach(&out, len); +} + +/* + * 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 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) + die("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_mem(&idents, anonymize_ident, + split.name_begin, &len); + strbuf_add(out, ident, len); + 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 = get_commit_buffer(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 && + !full_tree) { + parse_commit_or_die(commit->parents->item); + diff_tree_oid(&commit->parents->item->tree->object.oid, + &commit->tree->object.oid, "", &rev->diffopt); + } + else + diff_root_tree_oid(&commit->tree->object.oid, + "", &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 = commit->util; + 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(message); + else if (!is_encoding_utf8(encoding)) + reencoded = reencode_string(message, "UTF-8", encoding); + if (!commit->parents) + printf("reset %s\n", refname); + printf("commit %s\nmark :%"PRIu32"\n%.*s\n%.*s\ndata %u\n%s", + refname, last_idnum, + (int)(author_end - author), author, + (int)(committer_end - committer), committer, + (unsigned)(reencoded + ? strlen(reencoded) : message + ? strlen(message) : 0), + reencoded ? reencoded : message ? message : ""); + free(reencoded); + unuse_commit_buffer(commit, commit_buffer); + + for (i = 0, p = commit->parents; p; p = p->next) { + int mark = get_object_mark(&p->item->object); + if (!mark) + continue; + if (i == 0) + printf("from :%d\n", mark); + else + printf("merge :%d\n", mark); + 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 void *anonymize_tag(const void *old, size_t *len) +{ + static int counter; + struct strbuf out = STRBUF_INIT; + strbuf_addf(&out, "tag message %d", counter++); + return strbuf_detach(&out, len); +} + +static void handle_tail(struct object_array *commits, struct rev_info *revs, + struct string_list *paths_of_changed_objects) +{ + struct commit *commit; + while (commits->nr) { + commit = (struct commit *)object_array_pop(commits); + if (has_unshown_parent(commit)) + return; + handle_commit(commit, revs, paths_of_changed_objects); + } +} + +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 = read_sha1_file(tag->object.oid.hash, &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_mem(&tags, anonymize_tag, + message, &message_size); + } + } + + /* handle signed tags */ + if (message) { + const char *signature = strstr(message, + "\n-----BEGIN PGP SIGNATURE-----\n"); + if (signature) + switch(signed_tag_mode) { + case 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 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_COMMIT) { + die ("Tag %s tags unexported %s!", + oid_to_hex(&tag->object.oid), + typename(tagged->type)); + } + p = (struct commit *)tagged; + for (;;) { + if (p->parents && p->parents->next) + break; + if (p->object.flags & UNINTERESTING) + break; + if (!(p->object.flags & TREESAME)) + break; + if (!p->parents) + die ("Can't find replacement commit for tag %s\n", + oid_to_hex(&tag->object.oid)); + p = p->parents->item; + } + tagged_mark = get_object_mark(&p->object); + } + } + + if (starts_with(name, "refs/tags/")) + name += 10; + printf("tag %s\nfrom :%d\n%.*s%sdata %d\n%.*s\n", + name, tagged_mark, + (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(&tag->object.oid); + string_list_append(&extra_refs, full_name)->util = tag; + tag = (struct tag *)tag->tagged; + } + if (!tag) + die("Tag %s points nowhere?", e->name); + return (struct commit *)tag; + break; + } + 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 (dwim_ref(e->name, strlen(e->name), oid.hash, &full_name) != 1) + continue; + + if (refspecs) { + char *private; + private = apply_refspecs(refspecs, refspecs_nr, 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, + typename(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.", + typename(commit->object.type)); + continue; + } + + /* + * This ref will not be updated through a commit, lets make + * sure it gets properly updated eventually. + */ + if (commit->util || commit->object.flags & SHOWN) + string_list_append(&extra_refs, full_name)->util = commit; + if (!commit->util) + commit->util = full_name; + } +} + +static void handle_tags_and_duplicates(void) +{ + struct commit *commit; + int i; + + for (i = extra_refs.nr - 1; i >= 0; i--) { + const char *name = extra_refs.items[i].string; + struct object *object = extra_refs.items[i].util; + 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 = (struct commit *)object; + printf("reset %s\nfrom :%d\n\n", name, + get_object_mark(&commit->object)); + show_progress(); + break; + } + } +} + +static void export_marks(char *file) +{ + unsigned int i; + uint32_t mark; + struct object_decoration *deco = idnums.hash; + 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) +{ + char line[512]; + FILE *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 = sha1_object_info(oid.hash, NULL); + if (type < 0) + die("object not found: %s", oid_to_hex(&oid)); + + if (type != OBJ_COMMIT) + /* only commits */ + continue; + + commit = lookup_commit(&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 *refspec = &refspecs[i]; + if (*refspec->src) + continue; + + printf("reset %s\nfrom %s\n\n", + refspec->dst, sha1_to_hex(null_sha1)); + } +} + +int cmd_fast_export(int argc, const char **argv, const char *prefix) +{ + struct rev_info revs; + struct object_array commits = OBJECT_ARRAY_INIT; + struct commit *commit; + char *export_filename = NULL, *import_filename = 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_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_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_END() + }; + + if (argc == 1) + usage_with_options (fast_export_usage, options); + + /* we handle encodings */ + git_config(git_default_config, NULL); + + init_revisions(&revs, prefix); + revs.topo_order = 1; + revs.show_source = 1; + revs.rewrite_parents = 1; + argc = parse_options(argc, argv, prefix, options, fast_export_usage, + PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN); + argc = setup_revisions(argc, argv, &revs, NULL); + if (argc > 1) + usage_with_options (fast_export_usage, options); + + if (refspecs_list.nr) { + const char **refspecs_str; + int i; + + ALLOC_ARRAY(refspecs_str, refspecs_list.nr); + for (i = 0; i < refspecs_list.nr; i++) + refspecs_str[i] = refspecs_list.items[i].string; + + refspecs_nr = refspecs_list.nr; + refspecs = parse_fetch_refspec(refspecs_nr, refspecs_str); + + string_list_clear(&refspecs_list, 1); + free(refspecs_str); + } + + if (use_done_feature) + printf("feature done\n"); + + if (import_filename) + import_marks(import_filename); + 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.diffopt.format_callback = show_filemodify; + revs.diffopt.format_callback_data = &paths_of_changed_objects; + DIFF_OPT_SET(&revs.diffopt, RECURSIVE); + while ((commit = get_revision(&revs))) { + if (has_unshown_parent(commit)) { + add_object_array(&commit->object, NULL, &commits); + } + else { + handle_commit(commit, &revs, &paths_of_changed_objects); + handle_tail(&commits, &revs, &paths_of_changed_objects); + } + } + + handle_tags_and_duplicates(); + handle_deletes(); + + if (export_filename && lastimportid != last_idnum) + export_marks(export_filename); + + if (use_done_feature) + printf("done\n"); + + free_refspec(refspecs_nr, refspecs); + + return 0; +} diff --git a/builtin/fetch-pack.c b/builtin/fetch-pack.c new file mode 100644 index 0000000000..366b9d13f9 --- /dev/null +++ b/builtin/fetch-pack.c @@ -0,0 +1,231 @@ +#include "builtin.h" +#include "pkt-line.h" +#include "fetch-pack.h" +#include "remote.h" +#include "connect.h" +#include "sha1-array.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; + + if (!get_oid_hex(name, &oid)) { + if (name[GIT_SHA1_HEXSZ] == ' ') { + /* <sha1> <ref>, find refname */ + name += GIT_SHA1_HEXSZ + 1; + } else if (name[GIT_SHA1_HEXSZ] == '\0') { + ; /* <sha1>, leave sha1 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) +{ + 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]; + char *pack_lockfile = NULL; + char **pack_lockfile_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; + + packet_trace_identity("fetch-pack"); + + memset(&args, 0, sizeof(args)); + 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_lockfile_ptr = &pack_lockfile; + 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; + } + 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, args.uploadpack, + flags); + if (!conn) + return args.diag_url ? 0 : 1; + } + get_remote_heads(fd[0], NULL, 0, &ref, 0, NULL, &shallow); + + ref = fetch_pack(&args, fd, conn, ref, dest, sought, nr_sought, + &shallow, pack_lockfile_ptr); + if (pack_lockfile) { + printf("lock %s\n", pack_lockfile); + fflush(stdout); + } + 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 0000000000..225c734924 --- /dev/null +++ b/builtin/fetch.c @@ -0,0 +1,1420 @@ +/* + * "git fetch" + */ +#include "cache.h" +#include "config.h" +#include "refs.h" +#include "commit.h" +#include "builtin.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 "argv-array.h" +#include "utf8.h" +#include "packfile.h" + +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 +}; + +static int fetch_prune_config = -1; /* unspecified */ +static int prune = -1; /* unspecified */ +#define PRUNE_BY_DEFAULT 0 /* do we prune by default? */ + +static int all, append, dry_run, force, keep, multiple, update_head_ok, verbosity, deepen_relative; +static int progress = -1; +static int tags = TAGS_DEFAULT, unshallow, update_shallow, deepen; +static int max_children = 1; +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 const char *submodule_prefix = ""; +static int recurse_submodules = RECURSE_SUBMODULES_DEFAULT; +static int recurse_submodules_default = RECURSE_SUBMODULES_ON_DEMAND; +static int shown_url = 0; +static int refmap_alloc, refmap_nr; +static const char **refmap_array; + +static int git_fetch_config(const char *k, const char *v, void *cb) +{ + if (!strcmp(k, "fetch.prune")) { + fetch_prune_config = 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; + recurse_submodules = r; + } + + if (!strcmp(k, "submodule.fetchjobs")) { + max_children = parse_submodule_fetchjobs(k, v); + return 0; + } else if (!strcmp(k, "fetch.recursesubmodules")) { + recurse_submodules = parse_fetch_recurse_submodules_arg(k, v); + return 0; + } + + return git_default_config(k, v, cb); +} + +static int gitmodules_fetch_config(const char *var, const char *value, void *cb) +{ + if (!strcmp(var, "submodule.fetchjobs")) { + max_children = parse_submodule_fetchjobs(var, value); + return 0; + } else if (!strcmp(var, "fetch.recursesubmodules")) { + recurse_submodules = parse_fetch_recurse_submodules_arg(var, value); + return 0; + } + + return 0; +} + +static int parse_refmap_arg(const struct option *opt, const char *arg, int unset) +{ + ALLOC_GROW(refmap_array, refmap_nr + 1, refmap_alloc); + + /* + * "git fetch --refmap='' origin foo" + * can be used to tell the command not to store anywhere + */ + if (*arg) + refmap_array[refmap_nr++] = arg; + return 0; +} + +static struct option builtin_fetch_options[] = { + OPT__VERBOSITY(&verbosity), + OPT_BOOL(0, "all", &all, + N_("fetch from all remotes")), + OPT_BOOL('a', "append", &append, + N_("append to .git/FETCH_HEAD instead of overwriting")), + 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 branch")), + 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_children, + N_("number of submodules fetched in parallel")), + OPT_BOOL('p', "prune", &prune, + N_("prune remote-tracking branches no longer on remote")), + { OPTION_CALLBACK, 0, "recurse-submodules", &recurse_submodules, 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('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")), + { OPTION_SET_INT, 0, "unshallow", &unshallow, NULL, + N_("convert to a complete repository"), + PARSE_OPT_NONEG | PARSE_OPT_NOARG, NULL, 1 }, + { OPTION_STRING, 0, "submodule-prefix", &submodule_prefix, N_("dir"), + N_("prepend this to submodule path output"), PARSE_OPT_HIDDEN }, + { OPTION_CALLBACK, 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")), + { OPTION_CALLBACK, 0, "refmap", NULL, N_("refmap"), + N_("specify fetch refmap"), PARSE_OPT_NONEG, parse_refmap_arg }, + OPT_SET_INT('4', "ipv4", &family, N_("use IPv4 addresses only"), + TRANSPORT_FAMILY_IPV4), + OPT_SET_INT('6', "ipv6", &family, N_("use IPv6 addresses only"), + TRANSPORT_FAMILY_IPV6), + OPT_END() +}; + +static void unlock_pack(void) +{ + if (gtransport) + transport_unlock_pack(gtransport); + if (gsecondary) + transport_unlock_pack(gsecondary); +} + +static void unlock_pack_on_signal(int signo) +{ + unlock_pack(); + 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 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 int add_existing(const char *refname, const struct object_id *oid, + int flag, void *cbdata) +{ + struct string_list *list = (struct string_list *)cbdata; + struct string_list_item *item = string_list_insert(list, refname); + struct object_id *old_oid = xmalloc(sizeof(*old_oid)); + + oidcpy(old_oid, oid); + item->util = old_oid; + return 0; +} + +static int will_fetch(struct ref **head, const unsigned char *sha1) +{ + struct ref *rm = *head; + while (rm) { + if (!hashcmp(rm->old_oid.hash, sha1)) + return 1; + rm = rm->next; + } + return 0; +} + +static void find_non_local_tags(struct transport *transport, + struct ref **head, + struct ref ***tail) +{ + struct string_list existing_refs = STRING_LIST_INIT_DUP; + struct string_list remote_refs = STRING_LIST_INIT_NODUP; + const struct ref *ref; + struct string_list_item *item = NULL; + + for_each_ref(add_existing, &existing_refs); + for (ref = transport_get_remote_refs(transport); 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 && + !has_object_file_with_flags(&ref->old_oid, + OBJECT_INFO_QUICK) && + !will_fetch(head, ref->old_oid.hash) && + !has_sha1_file_with_flags(item->util, + OBJECT_INFO_QUICK) && + !will_fetch(head, item->util)) + item->util = NULL; + 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 && + !has_sha1_file_with_flags(item->util, OBJECT_INFO_QUICK) && + !will_fetch(head, item->util)) + item->util = NULL; + + item = NULL; + + /* skip duplicates and refs that we already have */ + if (string_list_has_string(&remote_refs, ref->name) || + string_list_has_string(&existing_refs, ref->name)) + continue; + + item = string_list_insert(&remote_refs, ref->name); + item->util = (void *)&ref->old_oid; + } + string_list_clear(&existing_refs, 1); + + /* + * We may have a final lightweight tag that needs to be + * checked to see if it needs fetching. + */ + if (item && + !has_sha1_file_with_flags(item->util, OBJECT_INFO_QUICK) && + !will_fetch(head, item->util)) + item->util = NULL; + + /* + * For all the tags in the remote_refs string list, + * add them to the list of refs to be fetched + */ + for_each_string_list_item(item, &remote_refs) { + /* Unless we have already decided to ignore this item... */ + if (item->util) + { + struct ref *rm = alloc_ref(item->string); + rm->peer_ref = alloc_ref(item->string); + oidcpy(&rm->old_oid, item->util); + **tail = rm; + *tail = &rm->next; + } + } + + string_list_clear(&remote_refs, 0); +} + +static struct ref *get_ref_map(struct transport *transport, + struct refspec *refspecs, int refspec_count, + 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; + + const struct ref *remote_refs = transport_get_remote_refs(transport); + + if (refspec_count) { + struct refspec *fetch_refspec; + int fetch_refspec_nr; + + for (i = 0; i < refspec_count; i++) { + get_fetch_map(remote_refs, &refspecs[i], &tail, 0); + if (refspecs[i].dst && refspecs[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_array) { + fetch_refspec = parse_fetch_refspec(refmap_nr, refmap_array); + fetch_refspec_nr = refmap_nr; + } else { + fetch_refspec = transport->remote->fetch; + fetch_refspec_nr = transport->remote->fetch_refspec_nr; + } + + for (i = 0; i < fetch_refspec_nr; i++) + get_fetch_map(ref_map, &fetch_refspec[i], &oref_tail, 1); + } else if (refmap_array) { + die("--refmap option is only meaningful with command-line refspec(s)."); + } else { + /* Use the defaults */ + struct remote *remote = transport->remote; + struct branch *branch = branch_get(NULL); + int has_merge = branch_has_merge_config(branch); + if (remote && + (remote->fetch_refspec_nr || + /* Note: has_merge implies non-NULL branch->remote_name */ + (has_merge && !strcmp(branch->remote_name, remote->name)))) { + for (i = 0; i < remote->fetch_refspec_nr; i++) { + get_fetch_map(remote_refs, &remote->fetch[i], &tail, 0); + if (remote->fetch[i].dst && + remote->fetch[i].dst[0]) + *autotags = 1; + if (!i && !has_merge && ref_map && + !remote->fetch[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 { + 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(transport, &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; + } + + return ref_remove_duplicates(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, + int check_old) +{ + char *msg; + char *rla = getenv("GIT_REFLOG_ACTION"); + struct ref_transaction *transaction; + struct strbuf err = STRBUF_INIT; + int ret, df_conflict = 0; + + if (dry_run) + return 0; + if (!rla) + rla = default_rla.buf; + msg = xstrfmt("%s: %s", rla, action); + + transaction = ref_transaction_begin(&err); + if (!transaction || + ref_transaction_update(transaction, ref->name, + ref->new_oid.hash, + check_old ? ref->old_oid.hash : NULL, + 0, msg, &err)) + goto fail; + + ret = ref_transaction_commit(transaction, &err); + if (ret) { + df_conflict = (ret == TRANSACTION_NAME_CONFLICT); + goto fail; + } + + ref_transaction_free(transaction); + strbuf_release(&err); + free(msg); + return 0; +fail: + ref_transaction_free(transaction); + error("%s", err.buf); + strbuf_release(&err); + free(msg); + return df_conflict ? STORE_REF_ERROR_DF_CONFLICT + : STORE_REF_ERROR_OTHER; +} + +static int refcol_width = 10; +static int compact_format; + +static void adjust_refcol_width(const struct ref *ref) +{ + int max, rlen, llen, len; + + /* uptodate lines are only shown on high verbosity level */ + if (!verbosity && !oidcmp(&ref->peer_ref->old_oid, &ref->old_oid)) + return; + + max = term_columns(); + rlen = utf8_strwidth(prettify_refname(ref->name)); + + 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) + */ + if (compact_format) { + llen = 0; + max = max * 2 / 3; + } + len = 21 /* flag and summary */ + rlen + 4 /* -> */ + llen; + if (len >= max) + return; + + /* + * Not precise calculation for compact mode because '*' can + * appear on the left hand side of '->' and shrink the column + * back. + */ + if (refcol_width < rlen) + refcol_width = rlen; +} + +static void prepare_format_display(struct ref *ref_map) +{ + struct ref *rm; + const char *format = "full"; + + git_config_get_string_const("fetch.output", &format); + if (!strcasecmp(format, "full")) + compact_format = 0; + else if (!strcasecmp(format, "compact")) + compact_format = 1; + else + die(_("configuration fetch.output contains invalid value %s"), + format); + + for (rm = ref_map; rm; rm = rm->next) { + if (rm->status == REF_STATUS_REJECT_SHALLOW || + !rm->peer_ref || + !strcmp(rm->name, "HEAD")) + continue; + + adjust_refcol_width(rm); + } +} + +static void print_remote_to_local(struct strbuf *display, + const char *remote, const char *local) +{ + strbuf_addf(display, "%-*s -> %s", refcol_width, remote, local); +} + +static int find_and_replace(struct strbuf *haystack, + const char *needle, + const char *placeholder) +{ + const char *p = strstr(haystack->buf, needle); + int plen, nlen; + + if (!p) + return 0; + + if (p > haystack->buf && p[-1] != '/') + return 0; + + plen = strlen(p); + nlen = strlen(needle); + if (plen > nlen && p[nlen] != '/') + return 0; + + strbuf_splice(haystack, p - haystack->buf, nlen, + placeholder, strlen(placeholder)); + return 1; +} + +static void print_compact(struct strbuf *display, + const char *remote, const char *local) +{ + struct strbuf r = STRBUF_INIT; + struct strbuf l = STRBUF_INIT; + + if (!strcmp(remote, local)) { + strbuf_addf(display, "%-*s -> *", 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, r.buf, l.buf); + + strbuf_release(&r); + strbuf_release(&l); +} + +static void format_display(struct strbuf *display, char code, + const char *summary, const char *error, + const char *remote, const char *local, + int summary_width) +{ + int width = (summary_width + strlen(summary) - gettext_width(summary)); + + strbuf_addf(display, "%c %-*s ", code, width, summary); + if (!compact_format) + print_remote_to_local(display, remote, local); + else + print_compact(display, remote, local); + if (error) + strbuf_addf(display, " (%s)", error); +} + +static int update_local_ref(struct ref *ref, + const char *remote, + const struct ref *remote_ref, + struct strbuf *display, + int summary_width) +{ + struct commit *current = NULL, *updated; + enum object_type type; + struct branch *current_branch = branch_get(NULL); + const char *pretty_ref = prettify_refname(ref->name); + + type = sha1_object_info(ref->new_oid.hash, NULL); + if (type < 0) + die(_("object %s not found"), oid_to_hex(&ref->new_oid)); + + if (!oidcmp(&ref->old_oid, &ref->new_oid)) { + if (verbosity > 0) + format_display(display, '=', _("[up to date]"), NULL, + remote, pretty_ref, summary_width); + return 0; + } + + if (current_branch && + !strcmp(ref->name, current_branch->name) && + !(update_head_ok || is_bare_repository()) && + !is_null_oid(&ref->old_oid)) { + /* + * If this is the head, and it's not okay to update + * the head, and the old value of the head isn't empty... + */ + format_display(display, '!', _("[rejected]"), + _("can't fetch in current branch"), + remote, pretty_ref, summary_width); + return 1; + } + + if (!is_null_oid(&ref->old_oid) && + starts_with(ref->name, "refs/tags/")) { + int r; + r = s_update_ref("updating tag", ref, 0); + format_display(display, r ? '!' : 't', _("[tag update]"), + r ? _("unable to update local ref") : NULL, + remote, pretty_ref, summary_width); + return r; + } + + current = lookup_commit_reference_gently(&ref->old_oid, 1); + updated = lookup_commit_reference_gently(&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. + */ + const char *name = remote_ref ? remote_ref->name : ""; + if (starts_with(name, "refs/tags/")) { + msg = "storing tag"; + what = _("[new tag]"); + } else if (starts_with(name, "refs/heads/")) { + msg = "storing head"; + what = _("[new branch]"); + } else { + msg = "storing ref"; + what = _("[new ref]"); + } + + if ((recurse_submodules != RECURSE_SUBMODULES_OFF) && + (recurse_submodules != RECURSE_SUBMODULES_ON)) + check_for_new_submodule_commits(&ref->new_oid); + r = s_update_ref(msg, ref, 0); + format_display(display, r ? '!' : '*', what, + r ? _("unable to update local ref") : NULL, + remote, pretty_ref, summary_width); + return r; + } + + if (in_merge_bases(current, updated)) { + struct strbuf quickref = STRBUF_INIT; + int r; + strbuf_add_unique_abbrev(&quickref, current->object.oid.hash, DEFAULT_ABBREV); + strbuf_addstr(&quickref, ".."); + strbuf_add_unique_abbrev(&quickref, ref->new_oid.hash, DEFAULT_ABBREV); + if ((recurse_submodules != RECURSE_SUBMODULES_OFF) && + (recurse_submodules != RECURSE_SUBMODULES_ON)) + check_for_new_submodule_commits(&ref->new_oid); + r = s_update_ref("fast-forward", ref, 1); + format_display(display, r ? '!' : ' ', quickref.buf, + r ? _("unable to update local ref") : NULL, + remote, pretty_ref, summary_width); + strbuf_release(&quickref); + return r; + } else if (force || ref->force) { + struct strbuf quickref = STRBUF_INIT; + int r; + strbuf_add_unique_abbrev(&quickref, current->object.oid.hash, DEFAULT_ABBREV); + strbuf_addstr(&quickref, "..."); + strbuf_add_unique_abbrev(&quickref, ref->new_oid.hash, DEFAULT_ABBREV); + if ((recurse_submodules != RECURSE_SUBMODULES_OFF) && + (recurse_submodules != RECURSE_SUBMODULES_ON)) + check_for_new_submodule_commits(&ref->new_oid); + r = s_update_ref("forced-update", ref, 1); + format_display(display, r ? '!' : '+', quickref.buf, + r ? _("unable to update local ref") : _("forced update"), + remote, pretty_ref, summary_width); + strbuf_release(&quickref); + return r; + } else { + format_display(display, '!', _("[rejected]"), _("non-fast-forward"), + remote, pretty_ref, summary_width); + return 1; + } +} + +static int iterate_ref_map(void *cb_data, unsigned char sha1[20]) +{ + struct ref **rm = cb_data; + struct ref *ref = *rm; + + while (ref && ref->status == REF_STATUS_REJECT_SHALLOW) + ref = ref->next; + if (!ref) + return -1; /* end of the list */ + *rm = ref->next; + hashcpy(sha1, ref->old_oid.hash); + return 0; +} + +static int store_updated_refs(const char *raw_url, const char *remote_name, + struct ref *ref_map) +{ + FILE *fp; + struct commit *commit; + int url_len, i, rc = 0; + struct strbuf note = STRBUF_INIT; + const char *what, *kind; + struct ref *rm; + char *url; + const char *filename = dry_run ? "/dev/null" : git_path_fetch_head(); + int want_status; + int summary_width = transport_summary_width(ref_map); + + fp = fopen(filename, "a"); + if (!fp) + return error_errno(_("cannot open %s"), filename); + + if (raw_url) + url = transport_anonymize_url(raw_url); + else + url = xstrdup("foreign"); + + rm = ref_map; + if (check_connected(iterate_ref_map, &rm, NULL)) { + rc = error(_("%s did not send all necessary objects\n"), url); + goto abort; + } + + prepare_format_display(ref_map); + + /* + * 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; + const char *merge_status_marker = ""; + + if (rm->status == REF_STATUS_REJECT_SHALLOW) { + if (want_status == FETCH_HEAD_MERGE) + warning(_("reject %s because shallow roots are not allowed to be updated"), + rm->peer_ref ? rm->peer_ref->name : rm->name); + continue; + } + + commit = lookup_commit_reference_gently(&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 (!strcmp(rm->name, "HEAD")) { + kind = ""; + what = ""; + } + else if (starts_with(rm->name, "refs/heads/")) { + kind = "branch"; + what = rm->name + 11; + } + else if (starts_with(rm->name, "refs/tags/")) { + kind = "tag"; + what = rm->name + 10; + } + else if (starts_with(rm->name, "refs/remotes/")) { + kind = "remote-tracking branch"; + what = rm->name + 13; + } + else { + kind = ""; + what = rm->name; + } + + url_len = strlen(url); + for (i = url_len - 1; url[i] == '/' && 0 <= i; i--) + ; + url_len = i + 1; + if (4 < i && !strncmp(".git", url + i - 3, 4)) + url_len = i - 3; + + strbuf_reset(¬e); + if (*what) { + if (*kind) + strbuf_addf(¬e, "%s ", kind); + strbuf_addf(¬e, "'%s' of ", what); + } + switch (rm->fetch_head_status) { + case FETCH_HEAD_NOT_FOR_MERGE: + merge_status_marker = "not-for-merge"; + /* fall-through */ + case FETCH_HEAD_MERGE: + fprintf(fp, "%s\t%s\t%s", + oid_to_hex(&rm->old_oid), + merge_status_marker, + note.buf); + for (i = 0; i < url_len; ++i) + if ('\n' == url[i]) + fputs("\\n", fp); + else + fputc(url[i], fp); + fputc('\n', fp); + break; + default: + /* do not write anything to FETCH_HEAD */ + break; + } + + strbuf_reset(¬e); + if (ref) { + rc |= update_local_ref(ref, what, rm, ¬e, + summary_width); + free(ref); + } else + format_display(¬e, '*', + *kind ? kind : "branch", NULL, + *what ? what : "HEAD", + "FETCH_HEAD", summary_width); + if (note.len) { + if (verbosity >= 0 && !shown_url) { + fprintf(stderr, _("From %.*s\n"), + url_len, url); + shown_url = 1; + } + if (verbosity >= 0) + fprintf(stderr, " %s\n", note.buf); + } + } + } + + 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); + + abort: + strbuf_release(¬e); + free(url); + fclose(fp); + 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 quickfetch(struct ref *ref_map) +{ + struct ref *rm = ref_map; + struct check_connected_options opt = CHECK_CONNECTED_INIT; + + /* + * 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; + opt.quiet = 1; + return check_connected(iterate_ref_map, &rm, &opt); +} + +static int fetch_refs(struct transport *transport, struct ref *ref_map) +{ + int ret = quickfetch(ref_map); + if (ret) + ret = transport_fetch_refs(transport, ref_map); + if (!ret) + ret |= store_updated_refs(transport->url, + transport->remote->name, + ref_map); + transport_unlock_pack(transport); + return ret; +} + +static int prune_refs(struct refspec *refs, int ref_count, struct ref *ref_map, + const char *raw_url) +{ + int url_len, i, result = 0; + struct ref *ref, *stale_refs = get_stale_heads(refs, ref_count, ref_map); + char *url; + int summary_width = transport_summary_width(stale_refs); + const char *dangling_msg = dry_run + ? _(" (%s will become dangling)") + : _(" (%s has become dangling)"); + + if (raw_url) + url = transport_anonymize_url(raw_url); + else + url = xstrdup("foreign"); + + url_len = strlen(url); + for (i = url_len - 1; url[i] == '/' && 0 <= i; i--) + ; + + url_len = i + 1; + if (4 < i && !strncmp(".git", url + i - 3, 4)) + url_len = i - 3; + + if (!dry_run) { + 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) { + for (ref = stale_refs; ref; ref = ref->next) { + struct strbuf sb = STRBUF_INIT; + if (!shown_url) { + fprintf(stderr, _("From %.*s\n"), url_len, url); + shown_url = 1; + } + format_display(&sb, '-', _("[deleted]"), NULL, + _("(none)"), prettify_refname(ref->name), + summary_width); + fprintf(stderr, " %s\n",sb.buf); + strbuf_release(&sb); + warn_dangling_symref(stderr, dangling_msg, ref->name); + } + } + + free(url); + free_refs(stale_refs); + return result; +} + +static void check_not_current_branch(struct ref *ref_map) +{ + struct branch *current_branch = branch_get(NULL); + + if (is_bare_repository() || !current_branch) + return; + + for (; ref_map; ref_map = ref_map->next) + if (ref_map->peer_ref && !strcmp(current_branch->refname, + ref_map->peer_ref->name)) + die(_("Refusing to fetch into current branch %s " + "of non-bare repository"), current_branch->refname); +} + +static int truncate_fetch_head(void) +{ + const char *filename = git_path_fetch_head(); + 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 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"); + return transport; +} + +static void backfill_tags(struct transport *transport, struct ref *ref_map) +{ + int 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); + fetch_refs(transport, ref_map); + + if (gsecondary) { + transport_disconnect(gsecondary); + gsecondary = NULL; + } +} + +static int do_fetch(struct transport *transport, + struct refspec *refs, int ref_count) +{ + struct string_list existing_refs = STRING_LIST_INIT_DUP; + struct ref *ref_map; + struct ref *rm; + int autotags = (transport->remote->fetch_tags == 1); + int retcode = 0; + + for_each_ref(add_existing, &existing_refs); + + if (tags == TAGS_DEFAULT) { + if (transport->remote->fetch_tags == 2) + tags = TAGS_SET; + if (transport->remote->fetch_tags == -1) + tags = TAGS_UNSET; + } + + if (!transport->get_refs_list || !transport->fetch) + die(_("Don't know how to fetch from %s"), transport->url); + + /* if not appending, truncate FETCH_HEAD */ + if (!append && !dry_run) { + retcode = truncate_fetch_head(); + if (retcode) + goto cleanup; + } + + ref_map = get_ref_map(transport, refs, ref_count, tags, &autotags); + if (!update_head_ok) + check_not_current_branch(ref_map); + + for (rm = ref_map; rm; rm = rm->next) { + if (rm->peer_ref) { + struct string_list_item *peer_item = + string_list_lookup(&existing_refs, + rm->peer_ref->name); + if (peer_item) { + struct object_id *old_oid = peer_item->util; + oidcpy(&rm->peer_ref->old_oid, old_oid); + } + } + } + + 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 (ref_count) { + prune_refs(refs, ref_count, ref_map, transport->url); + } else { + prune_refs(transport->remote->fetch, + transport->remote->fetch_refspec_nr, + ref_map, + transport->url); + } + } + if (fetch_refs(transport, ref_map)) { + free_refs(ref_map); + retcode = 1; + goto cleanup; + } + free_refs(ref_map); + + /* if neither --no-tags nor --tags was specified, do automated tag + * following ... */ + if (tags == TAGS_DEFAULT && autotags) { + struct ref **tail = &ref_map; + ref_map = NULL; + find_non_local_tags(transport, &ref_map, &tail); + if (ref_map) + backfill_tags(transport, ref_map); + free_refs(ref_map); + } + + cleanup: + string_list_clear(&existing_refs, 1); + 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, 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 argv_array *argv) +{ + if (dry_run) + argv_array_push(argv, "--dry-run"); + if (prune != -1) + argv_array_push(argv, prune ? "--prune" : "--no-prune"); + if (update_head_ok) + argv_array_push(argv, "--update-head-ok"); + if (force) + argv_array_push(argv, "--force"); + if (keep) + argv_array_push(argv, "--keep"); + if (recurse_submodules == RECURSE_SUBMODULES_ON) + argv_array_push(argv, "--recurse-submodules"); + else if (recurse_submodules == RECURSE_SUBMODULES_ON_DEMAND) + argv_array_push(argv, "--recurse-submodules=on-demand"); + if (tags == TAGS_SET) + argv_array_push(argv, "--tags"); + else if (tags == TAGS_UNSET) + argv_array_push(argv, "--no-tags"); + if (verbosity >= 2) + argv_array_push(argv, "-v"); + if (verbosity >= 1) + argv_array_push(argv, "-v"); + else if (verbosity < 0) + argv_array_push(argv, "-q"); + +} + +static int fetch_multiple(struct string_list *list) +{ + int i, result = 0; + struct argv_array argv = ARGV_ARRAY_INIT; + + if (!append && !dry_run) { + int errcode = truncate_fetch_head(); + if (errcode) + return errcode; + } + + argv_array_pushl(&argv, "fetch", "--append", NULL); + add_options_to_argv(&argv); + + for (i = 0; i < list->nr; i++) { + const char *name = list->items[i].string; + argv_array_push(&argv, name); + if (verbosity >= 0) + printf(_("Fetching %s\n"), name); + if (run_command_v_opt(argv.argv, RUN_GIT_CMD)) { + error(_("Could not fetch %s"), name); + result = 1; + } + argv_array_pop(&argv); + } + + argv_array_clear(&argv); + return result; +} + +static int fetch_one(struct remote *remote, int argc, const char **argv) +{ + static const char **refs = NULL; + struct refspec *refspec; + int ref_nr = 0; + int exit_code; + + 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 <= gtransport->remote->prune) + prune = gtransport->remote->prune; + else if (0 <= fetch_prune_config) + prune = fetch_prune_config; + else + prune = PRUNE_BY_DEFAULT; + } + + if (argc > 0) { + int j = 0; + int i; + refs = xcalloc(st_add(argc, 1), sizeof(const char *)); + for (i = 0; i < argc; i++) { + if (!strcmp(argv[i], "tag")) { + i++; + if (i >= argc) + die(_("You need to specify a tag name.")); + refs[j++] = xstrfmt("refs/tags/%s:refs/tags/%s", + argv[i], argv[i]); + } else + refs[j++] = argv[i]; + } + refs[j] = NULL; + ref_nr = j; + } + + sigchain_push_common(unlock_pack_on_signal); + atexit(unlock_pack); + refspec = parse_fetch_refspec(ref_nr, refs); + exit_code = do_fetch(gtransport, refspec, ref_nr); + free_refspec(ref_nr, refspec); + transport_disconnect(gtransport); + gtransport = NULL; + return exit_code; +} + +int cmd_fetch(int argc, const char **argv, const char *prefix) +{ + int i; + struct string_list list = STRING_LIST_INIT_DUP; + struct remote *remote; + int result = 0; + struct argv_array argv_gc_auto = ARGV_ARRAY_INIT; + + packet_trace_identity("fetch"); + + /* Record the command line for the reflog */ + strbuf_addstr(&default_rla, "fetch"); + for (i = 1; i < argc; i++) + strbuf_addf(&default_rla, " %s", argv[i]); + + config_from_gitmodules(gitmodules_fetch_config, NULL); + git_config(git_fetch_config, NULL); + + argc = parse_options(argc, argv, prefix, + builtin_fetch_options, builtin_fetch_usage, 0); + + if (deepen_relative) { + if (deepen_relative < 0) + die(_("Negative depth in --deepen is not supported")); + if (depth) + die(_("--deepen and --depth are mutually exclusive")); + depth = xstrfmt("%d", deepen_relative); + } + if (unshallow) { + if (depth) + die(_("--depth and --unshallow cannot be used together")); + else if (!is_repository_shallow()) + 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; + + 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); + result = fetch_multiple(&list); + } else if (argc == 0) { + /* No arguments -- use default remote */ + remote = remote_get(NULL); + result = fetch_one(remote, argc, argv); + } 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]); + result = fetch_multiple(&list); + } 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")); + result = fetch_multiple(&list); + } else { + /* Zero or one remotes */ + remote = remote_get(argv[0]); + result = fetch_one(remote, argc-1, argv+1); + } + } + + if (!result && (recurse_submodules != RECURSE_SUBMODULES_OFF)) { + struct argv_array options = ARGV_ARRAY_INIT; + + add_options_to_argv(&options); + result = fetch_populated_submodules(&options, + submodule_prefix, + recurse_submodules, + recurse_submodules_default, + verbosity < 0, + max_children); + argv_array_clear(&options); + } + + string_list_clear(&list, 0); + + close_all_packs(); + + argv_array_pushl(&argv_gc_auto, "gc", "--auto", NULL); + if (verbosity < 0) + argv_array_push(&argv_gc_auto, "--quiet"); + run_command_v_opt(argv_gc_auto.argv, RUN_GIT_CMD); + argv_array_clear(&argv_gc_auto); + + return result; +} diff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c new file mode 100644 index 0000000000..e99b5ddbf9 --- /dev/null +++ b/builtin/fmt-merge-msg.c @@ -0,0 +1,712 @@ +#include "builtin.h" +#include "cache.h" +#include "config.h" +#include "refs.h" +#include "commit.h" +#include "diff.h" +#include "revision.h" +#include "tag.h" +#include "string-list.h" +#include "branch.h" +#include "fmt-merge-msg.h" +#include "gpg-interface.h" + +static const char * const fmt_merge_msg_usage[] = { + N_("git fmt-merge-msg [-m <message>] [--log[=<n>] | --no-log] [--file <file>]"), + NULL +}; + +static int use_branch_desc; + +int fmt_merge_msg_config(const char *key, const char *value, void *cb) +{ + if (!strcmp(key, "merge.log") || !strcmp(key, "merge.summary")) { + int is_bool; + merge_log_config = git_config_bool_or_int(key, value, &is_bool); + if (!is_bool && merge_log_config < 0) + return error("%s: negative length %s", key, value); + if (is_bool && merge_log_config) + merge_log_config = DEFAULT_MERGE_LOG_LEN; + } else if (!strcmp(key, "merge.branchdesc")) { + use_branch_desc = git_config_bool(key, value); + } else { + return git_default_config(key, value, cb); + } + return 0; +} + +/* merge data per repository where the merged tips came from */ +struct src_data { + struct string_list branch, tag, r_branch, generic; + int head_status; +}; + +struct origin_data { + struct object_id oid; + unsigned is_local_branch:1; +}; + +static void init_src_data(struct src_data *data) +{ + data->branch.strdup_strings = 1; + data->tag.strdup_strings = 1; + data->r_branch.strdup_strings = 1; + data->generic.strdup_strings = 1; +} + +static struct string_list srcs = STRING_LIST_INIT_DUP; +static struct string_list origins = STRING_LIST_INIT_DUP; + +struct merge_parents { + int alloc, nr; + struct merge_parent { + struct object_id given; + struct object_id commit; + unsigned char used; + } *item; +}; + +/* + * I know, I know, this is inefficient, but you won't be pulling and merging + * hundreds of heads at a time anyway. + */ +static struct merge_parent *find_merge_parent(struct merge_parents *table, + struct object_id *given, + struct object_id *commit) +{ + int i; + for (i = 0; i < table->nr; i++) { + if (given && oidcmp(&table->item[i].given, given)) + continue; + if (commit && oidcmp(&table->item[i].commit, commit)) + continue; + return &table->item[i]; + } + return NULL; +} + +static void add_merge_parent(struct merge_parents *table, + struct object_id *given, + struct object_id *commit) +{ + if (table->nr && find_merge_parent(table, given, commit)) + return; + ALLOC_GROW(table->item, table->nr + 1, table->alloc); + oidcpy(&table->item[table->nr].given, given); + oidcpy(&table->item[table->nr].commit, commit); + table->item[table->nr].used = 0; + table->nr++; +} + +static int handle_line(char *line, struct merge_parents *merge_parents) +{ + int i, len = strlen(line); + struct origin_data *origin_data; + char *src; + const char *origin; + struct src_data *src_data; + struct string_list_item *item; + int pulling_head = 0; + struct object_id oid; + + if (len < GIT_SHA1_HEXSZ + 3 || line[GIT_SHA1_HEXSZ] != '\t') + return 1; + + if (starts_with(line + GIT_SHA1_HEXSZ + 1, "not-for-merge")) + return 0; + + if (line[GIT_SHA1_HEXSZ + 1] != '\t') + return 2; + + i = get_oid_hex(line, &oid); + if (i) + return 3; + + if (!find_merge_parent(merge_parents, &oid, NULL)) + return 0; /* subsumed by other parents */ + + origin_data = xcalloc(1, sizeof(struct origin_data)); + oidcpy(&origin_data->oid, &oid); + + if (line[len - 1] == '\n') + line[len - 1] = 0; + line += GIT_SHA1_HEXSZ + 2; + + /* + * At this point, line points at the beginning of comment e.g. + * "branch 'frotz' of git://that/repository.git". + * Find the repository name and point it with src. + */ + src = strstr(line, " of "); + if (src) { + *src = 0; + src += 4; + pulling_head = 0; + } else { + src = line; + pulling_head = 1; + } + + item = unsorted_string_list_lookup(&srcs, src); + if (!item) { + item = string_list_append(&srcs, src); + item->util = xcalloc(1, sizeof(struct src_data)); + init_src_data(item->util); + } + src_data = item->util; + + if (pulling_head) { + origin = src; + src_data->head_status |= 1; + } else if (starts_with(line, "branch ")) { + origin_data->is_local_branch = 1; + origin = line + 7; + string_list_append(&src_data->branch, origin); + src_data->head_status |= 2; + } else if (starts_with(line, "tag ")) { + origin = line; + string_list_append(&src_data->tag, origin + 4); + src_data->head_status |= 2; + } else if (skip_prefix(line, "remote-tracking branch ", &origin)) { + string_list_append(&src_data->r_branch, origin); + src_data->head_status |= 2; + } else { + origin = src; + string_list_append(&src_data->generic, line); + src_data->head_status |= 2; + } + + if (!strcmp(".", src) || !strcmp(src, origin)) { + int len = strlen(origin); + if (origin[0] == '\'' && origin[len - 1] == '\'') + origin = xmemdupz(origin + 1, len - 2); + } else + origin = xstrfmt("%s of %s", origin, src); + if (strcmp(".", src)) + origin_data->is_local_branch = 0; + string_list_append(&origins, origin)->util = origin_data; + return 0; +} + +static void print_joined(const char *singular, const char *plural, + struct string_list *list, struct strbuf *out) +{ + if (list->nr == 0) + return; + if (list->nr == 1) { + strbuf_addf(out, "%s%s", singular, list->items[0].string); + } else { + int i; + strbuf_addstr(out, plural); + for (i = 0; i < list->nr - 1; i++) + strbuf_addf(out, "%s%s", i > 0 ? ", " : "", + list->items[i].string); + strbuf_addf(out, " and %s", list->items[list->nr - 1].string); + } +} + +static void add_branch_desc(struct strbuf *out, const char *name) +{ + struct strbuf desc = STRBUF_INIT; + + if (!read_branch_desc(&desc, name)) { + const char *bp = desc.buf; + while (*bp) { + const char *ep = strchrnul(bp, '\n'); + if (*ep) + ep++; + strbuf_addf(out, " : %.*s", (int)(ep - bp), bp); + bp = ep; + } + strbuf_complete_line(out); + } + strbuf_release(&desc); +} + +#define util_as_integral(elem) ((intptr_t)((elem)->util)) + +static void record_person_from_buf(int which, struct string_list *people, + const char *buffer) +{ + char *name_buf, *name, *name_end; + struct string_list_item *elem; + const char *field; + + field = (which == 'a') ? "\nauthor " : "\ncommitter "; + name = strstr(buffer, field); + if (!name) + return; + name += strlen(field); + name_end = strchrnul(name, '<'); + if (*name_end) + name_end--; + while (isspace(*name_end) && name <= name_end) + name_end--; + if (name_end < name) + return; + name_buf = xmemdupz(name, name_end - name + 1); + + elem = string_list_lookup(people, name_buf); + if (!elem) { + elem = string_list_insert(people, name_buf); + elem->util = (void *)0; + } + elem->util = (void*)(util_as_integral(elem) + 1); + free(name_buf); +} + + +static void record_person(int which, struct string_list *people, + struct commit *commit) +{ + const char *buffer = get_commit_buffer(commit, NULL); + record_person_from_buf(which, people, buffer); + unuse_commit_buffer(commit, buffer); +} + +static int cmp_string_list_util_as_integral(const void *a_, const void *b_) +{ + const struct string_list_item *a = a_, *b = b_; + return util_as_integral(b) - util_as_integral(a); +} + +static void add_people_count(struct strbuf *out, struct string_list *people) +{ + if (people->nr == 1) + strbuf_addstr(out, people->items[0].string); + else if (people->nr == 2) + strbuf_addf(out, "%s (%d) and %s (%d)", + people->items[0].string, + (int)util_as_integral(&people->items[0]), + people->items[1].string, + (int)util_as_integral(&people->items[1])); + else if (people->nr) + strbuf_addf(out, "%s (%d) and others", + people->items[0].string, + (int)util_as_integral(&people->items[0])); +} + +static void credit_people(struct strbuf *out, + struct string_list *them, + int kind) +{ + const char *label; + const char *me; + + if (kind == 'a') { + label = "By"; + me = git_author_info(IDENT_NO_DATE); + } else { + label = "Via"; + me = git_committer_info(IDENT_NO_DATE); + } + + if (!them->nr || + (them->nr == 1 && + me && + skip_prefix(me, them->items->string, &me) && + starts_with(me, " <"))) + return; + strbuf_addf(out, "\n%c %s ", comment_line_char, label); + add_people_count(out, them); +} + +static void add_people_info(struct strbuf *out, + struct string_list *authors, + struct string_list *committers) +{ + QSORT(authors->items, authors->nr, + cmp_string_list_util_as_integral); + QSORT(committers->items, committers->nr, + cmp_string_list_util_as_integral); + + credit_people(out, authors, 'a'); + credit_people(out, committers, 'c'); +} + +static void shortlog(const char *name, + struct origin_data *origin_data, + struct commit *head, + struct rev_info *rev, + struct fmt_merge_msg_opts *opts, + struct strbuf *out) +{ + int i, count = 0; + struct commit *commit; + struct object *branch; + struct string_list subjects = STRING_LIST_INIT_DUP; + struct string_list authors = STRING_LIST_INIT_DUP; + struct string_list committers = STRING_LIST_INIT_DUP; + int flags = UNINTERESTING | TREESAME | SEEN | SHOWN | ADDED; + struct strbuf sb = STRBUF_INIT; + const struct object_id *oid = &origin_data->oid; + int limit = opts->shortlog_len; + + branch = deref_tag(parse_object(oid), oid_to_hex(oid), GIT_SHA1_HEXSZ); + if (!branch || branch->type != OBJ_COMMIT) + return; + + setup_revisions(0, NULL, rev, NULL); + add_pending_object(rev, branch, name); + add_pending_object(rev, &head->object, "^HEAD"); + head->object.flags |= UNINTERESTING; + if (prepare_revision_walk(rev)) + die("revision walk setup failed"); + while ((commit = get_revision(rev)) != NULL) { + struct pretty_print_context ctx = {0}; + + if (commit->parents && commit->parents->next) { + /* do not list a merge but count committer */ + if (opts->credit_people) + record_person('c', &committers, commit); + continue; + } + if (!count && opts->credit_people) + /* the 'tip' committer */ + record_person('c', &committers, commit); + if (opts->credit_people) + record_person('a', &authors, commit); + count++; + if (subjects.nr > limit) + continue; + + format_commit_message(commit, "%s", &sb, &ctx); + strbuf_ltrim(&sb); + + if (!sb.len) + string_list_append(&subjects, + oid_to_hex(&commit->object.oid)); + else + string_list_append(&subjects, strbuf_detach(&sb, NULL)); + } + + if (opts->credit_people) + add_people_info(out, &authors, &committers); + if (count > limit) + strbuf_addf(out, "\n* %s: (%d commits)\n", name, count); + else + strbuf_addf(out, "\n* %s:\n", name); + + if (origin_data->is_local_branch && use_branch_desc) + add_branch_desc(out, name); + + for (i = 0; i < subjects.nr; i++) + if (i >= limit) + strbuf_addstr(out, " ...\n"); + else + strbuf_addf(out, " %s\n", subjects.items[i].string); + + clear_commit_marks((struct commit *)branch, flags); + clear_commit_marks(head, flags); + free_commit_list(rev->commits); + rev->commits = NULL; + rev->pending.nr = 0; + + string_list_clear(&authors, 0); + string_list_clear(&committers, 0); + string_list_clear(&subjects, 0); +} + +static void fmt_merge_msg_title(struct strbuf *out, + const char *current_branch) +{ + int i = 0; + char *sep = ""; + + strbuf_addstr(out, "Merge "); + for (i = 0; i < srcs.nr; i++) { + struct src_data *src_data = srcs.items[i].util; + const char *subsep = ""; + + strbuf_addstr(out, sep); + sep = "; "; + + if (src_data->head_status == 1) { + strbuf_addstr(out, srcs.items[i].string); + continue; + } + if (src_data->head_status == 3) { + subsep = ", "; + strbuf_addstr(out, "HEAD"); + } + if (src_data->branch.nr) { + strbuf_addstr(out, subsep); + subsep = ", "; + print_joined("branch ", "branches ", &src_data->branch, + out); + } + if (src_data->r_branch.nr) { + strbuf_addstr(out, subsep); + subsep = ", "; + print_joined("remote-tracking branch ", "remote-tracking branches ", + &src_data->r_branch, out); + } + if (src_data->tag.nr) { + strbuf_addstr(out, subsep); + subsep = ", "; + print_joined("tag ", "tags ", &src_data->tag, out); + } + if (src_data->generic.nr) { + strbuf_addstr(out, subsep); + print_joined("commit ", "commits ", &src_data->generic, + out); + } + if (strcmp(".", srcs.items[i].string)) + strbuf_addf(out, " of %s", srcs.items[i].string); + } + + if (!strcmp("master", current_branch)) + strbuf_addch(out, '\n'); + else + strbuf_addf(out, " into %s\n", current_branch); +} + +static void fmt_tag_signature(struct strbuf *tagbuf, + struct strbuf *sig, + const char *buf, + unsigned long len) +{ + const char *tag_body = strstr(buf, "\n\n"); + if (tag_body) { + tag_body += 2; + strbuf_add(tagbuf, tag_body, buf + len - tag_body); + } + strbuf_complete_line(tagbuf); + if (sig->len) { + strbuf_addch(tagbuf, '\n'); + strbuf_add_commented_lines(tagbuf, sig->buf, sig->len); + } +} + +static void fmt_merge_msg_sigs(struct strbuf *out) +{ + int i, tag_number = 0, first_tag = 0; + struct strbuf tagbuf = STRBUF_INIT; + + for (i = 0; i < origins.nr; i++) { + unsigned char *sha1 = origins.items[i].util; + enum object_type type; + unsigned long size, len; + char *buf = read_sha1_file(sha1, &type, &size); + struct strbuf sig = STRBUF_INIT; + + if (!buf || type != OBJ_TAG) + goto next; + len = parse_signature(buf, size); + + if (size == len) + ; /* merely annotated */ + else if (verify_signed_buffer(buf, len, buf + len, size - len, &sig, NULL)) { + if (!sig.len) + strbuf_addstr(&sig, "gpg verification failed.\n"); + } + + if (!tag_number++) { + fmt_tag_signature(&tagbuf, &sig, buf, len); + first_tag = i; + } else { + if (tag_number == 2) { + struct strbuf tagline = STRBUF_INIT; + strbuf_addch(&tagline, '\n'); + strbuf_add_commented_lines(&tagline, + origins.items[first_tag].string, + strlen(origins.items[first_tag].string)); + strbuf_insert(&tagbuf, 0, tagline.buf, + tagline.len); + strbuf_release(&tagline); + } + strbuf_addch(&tagbuf, '\n'); + strbuf_add_commented_lines(&tagbuf, + origins.items[i].string, + strlen(origins.items[i].string)); + fmt_tag_signature(&tagbuf, &sig, buf, len); + } + strbuf_release(&sig); + next: + free(buf); + } + if (tagbuf.len) { + strbuf_addch(out, '\n'); + strbuf_addbuf(out, &tagbuf); + } + strbuf_release(&tagbuf); +} + +static void find_merge_parents(struct merge_parents *result, + struct strbuf *in, struct object_id *head) +{ + struct commit_list *parents; + struct commit *head_commit; + int pos = 0, i, j; + + parents = NULL; + while (pos < in->len) { + int len; + char *p = in->buf + pos; + char *newline = strchr(p, '\n'); + struct object_id oid; + struct commit *parent; + struct object *obj; + + len = newline ? newline - p : strlen(p); + pos += len + !!newline; + + if (len < GIT_SHA1_HEXSZ + 3 || + get_oid_hex(p, &oid) || + p[GIT_SHA1_HEXSZ] != '\t' || + p[GIT_SHA1_HEXSZ + 1] != '\t') + continue; /* skip not-for-merge */ + /* + * Do not use get_merge_parent() here; we do not have + * "name" here and we do not want to contaminate its + * util field yet. + */ + obj = parse_object(&oid); + parent = (struct commit *)peel_to_type(NULL, 0, obj, OBJ_COMMIT); + if (!parent) + continue; + commit_list_insert(parent, &parents); + add_merge_parent(result, &obj->oid, &parent->object.oid); + } + head_commit = lookup_commit(head); + if (head_commit) + commit_list_insert(head_commit, &parents); + parents = reduce_heads(parents); + + while (parents) { + struct commit *cmit = pop_commit(&parents); + for (i = 0; i < result->nr; i++) + if (!oidcmp(&result->item[i].commit, &cmit->object.oid)) + result->item[i].used = 1; + } + + for (i = j = 0; i < result->nr; i++) { + if (result->item[i].used) { + if (i != j) + result->item[j] = result->item[i]; + j++; + } + } + result->nr = j; +} + +int fmt_merge_msg(struct strbuf *in, struct strbuf *out, + struct fmt_merge_msg_opts *opts) +{ + int i = 0, pos = 0; + struct object_id head_oid; + const char *current_branch; + void *current_branch_to_free; + struct merge_parents merge_parents; + + memset(&merge_parents, 0, sizeof(merge_parents)); + + /* get current branch */ + current_branch = current_branch_to_free = + resolve_refdup("HEAD", RESOLVE_REF_READING, head_oid.hash, NULL); + if (!current_branch) + die("No current branch"); + if (starts_with(current_branch, "refs/heads/")) + current_branch += 11; + + find_merge_parents(&merge_parents, in, &head_oid); + + /* get a line */ + while (pos < in->len) { + int len; + char *newline, *p = in->buf + pos; + + newline = strchr(p, '\n'); + len = newline ? newline - p : strlen(p); + pos += len + !!newline; + i++; + p[len] = 0; + if (handle_line(p, &merge_parents)) + die ("Error in line %d: %.*s", i, len, p); + } + + if (opts->add_title && srcs.nr) + fmt_merge_msg_title(out, current_branch); + + if (origins.nr) + fmt_merge_msg_sigs(out); + + if (opts->shortlog_len) { + struct commit *head; + struct rev_info rev; + + head = lookup_commit_or_die(&head_oid, "HEAD"); + init_revisions(&rev, NULL); + rev.commit_format = CMIT_FMT_ONELINE; + rev.ignore_merges = 1; + rev.limited = 1; + + strbuf_complete_line(out); + + for (i = 0; i < origins.nr; i++) + shortlog(origins.items[i].string, + origins.items[i].util, + head, &rev, opts, out); + } + + strbuf_complete_line(out); + free(current_branch_to_free); + free(merge_parents.item); + return 0; +} + +int cmd_fmt_merge_msg(int argc, const char **argv, const char *prefix) +{ + const char *inpath = NULL; + const char *message = 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_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; + + 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 0000000000..e931be9ce4 --- /dev/null +++ b/builtin/for-each-ref.c @@ -0,0 +1,88 @@ +#include "builtin.h" +#include "cache.h" +#include "config.h" +#include "refs.h" +#include "object.h" +#include "parse-options.h" +#include "ref-filter.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 | --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 = NULL, **sorting_tail = &sorting; + int maxcount = 0, icase = 0; + struct ref_array array; + struct ref_filter filter; + struct ref_format format = REF_FORMAT_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_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_CALLBACK(0 , "sort", sorting_tail, N_("key"), + N_("field name to sort on"), &parse_opt_ref_sorting), + 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_END(), + }; + + memset(&array, 0, sizeof(array)); + memset(&filter, 0, sizeof(filter)); + + 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); + + if (!sorting) + sorting = ref_default_sorting(); + sorting->ignore_case = icase; + filter.ignore_case = icase; + + filter.name_patterns = argv; + filter.match_as_path = 1; + filter_refs(&array, &filter, FILTER_REFS_ALL | FILTER_REFS_INCLUDE_BROKEN); + ref_array_sort(sorting, &array); + + if (!maxcount || array.nr < maxcount) + maxcount = array.nr; + for (i = 0; i < maxcount; i++) + show_ref_array_item(array.items[i], &format); + ref_array_clear(&array); + return 0; +} diff --git a/builtin/fsck.c b/builtin/fsck.c new file mode 100644 index 0000000000..56afe405b8 --- /dev/null +++ b/builtin/fsck.c @@ -0,0 +1,788 @@ +#include "builtin.h" +#include "cache.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" + +#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 struct object_id head_oid; +static const char *head_points_at; +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 + +static const char *describe_object(struct object *obj) +{ + static struct strbuf buf = STRBUF_INIT; + char *name = name_objects ? + lookup_decoration(fsck_walk_options.object_names, obj) : NULL; + + strbuf_reset(&buf); + strbuf_addstr(&buf, oid_to_hex(&obj->oid)); + if (name) + strbuf_addf(&buf, " (%s)", name); + + return buf.buf; +} + +static const char *printable_type(struct object *obj) +{ + const char *ret; + + if (obj->type == OBJ_NONE) { + enum object_type type = sha1_object_info(obj->oid.hash, NULL); + if (type > 0) + object_as_type(obj, type, 0); + } + + ret = typename(obj->type); + if (!ret) + ret = "unknown"; + + return ret; +} + +static int fsck_config(const char *var, const char *value, void *cb) +{ + if (strcmp(var, "fsck.skiplist") == 0) { + const char *path; + struct strbuf sb = STRBUF_INIT; + + if (git_config_pathname(&path, var, value)) + return 1; + strbuf_addf(&sb, "skiplist=%s", path); + free((char *)path); + fsck_set_msg_types(&fsck_obj_options, sb.buf); + strbuf_release(&sb); + return 0; + } + + if (skip_prefix(var, "fsck.", &var)) { + fsck_set_msg_type(&fsck_obj_options, var, value); + return 0; + } + + return git_default_config(var, value, cb); +} + +static void objreport(struct object *obj, const char *msg_type, + const char *err) +{ + fprintf(stderr, "%s in %s %s: %s\n", + msg_type, printable_type(obj), describe_object(obj), err); +} + +static int objerror(struct object *obj, const char *err) +{ + errors_found |= ERROR_OBJECT; + objreport(obj, "error", err); + return -1; +} + +static int fsck_error_func(struct fsck_options *o, + struct object *obj, int type, const char *message) +{ + objreport(obj, (type == FSCK_WARN) ? "warning" : "error", message); + return (type == FSCK_WARN) ? 0 : 1; +} + +static struct object_array pending; + +static int mark_object(struct object *obj, int type, void *data, struct fsck_options *options) +{ + 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("broken link from %7s %s\n", + printable_type(parent), describe_object(parent)); + printf("broken link from %7s %s\n", + (type == OBJ_ANY ? "unknown" : typename(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 (!(obj->flags & HAS_OBJ)) { + if (parent && !has_object_file(&obj->oid)) { + printf("broken link from %7s %s\n", + printable_type(parent), describe_object(parent)); + printf(" to %7s %s\n", + printable_type(obj), describe_object(obj)); + 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) +{ + return fsck_walk(obj, obj, &fsck_walk_options); +} + +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, int type, void *data, struct fsck_options *options) +{ + if (!obj) + return 1; + obj->flags |= USED; + 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 (has_sha1_pack(obj->oid.hash)) + return; /* it is in pack - forget about it */ + printf("missing %s %s\n", printable_type(obj), + describe_object(obj)); + 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("unreachable %s %s\n", printable_type(obj), + describe_object(obj)); + 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("dangling %s %s\n", printable_type(obj), + describe_object(obj)); + if (write_lost_and_found) { + char *filename = git_pathdup("lost-found/%s/%s", + obj->type == OBJ_COMMIT ? "commit" : "other", + describe_object(obj)); + 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)); + 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(stderr, "Checking %s\n", describe_object(obj)); + + 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(); + + /* Look up all the requirements, warn about missing objects.. */ + max = get_max_object_index(); + if (verbose) + fprintf(stderr, "Checking connectivity (%d objects)\n", 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) +{ + int err; + + if (obj->flags & SEEN) + return 0; + obj->flags |= SEEN; + + if (verbose) + fprintf(stderr, "Checking %s %s\n", + printable_type(obj), describe_object(obj)); + + if (fsck_walk(obj, NULL, &fsck_obj_options)) + objerror(obj, "broken links"); + err = fsck_object(obj, NULL, 0, &fsck_obj_options); + if (err) + goto out; + + if (obj->type == OBJ_COMMIT) { + struct commit *commit = (struct commit *) obj; + + if (!commit->parents && show_root) + printf("root %s\n", describe_object(&commit->object)); + } + + if (obj->type == OBJ_TAG) { + struct tag *tag = (struct tag *) obj; + + if (show_tags && tag->tagged) { + printf("tagged %s %s", printable_type(tag->tagged), + describe_object(tag->tagged)); + printf(" (%s) in %s\n", tag->tag, + describe_object(&tag->object)); + } + } + +out: + if (obj->type == OBJ_TREE) + free_tree_buffer((struct tree *)obj); + if (obj->type == OBJ_COMMIT) + free_commit_buffer((struct commit *)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(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); +} + +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(oid->hash); + if (obj && (obj->flags & HAS_OBJ)) { + if (timestamp && name_objects) + add_decoration(fsck_walk_options.object_names, + obj, + xstrfmt("%s@{%"PRItime"}", refname, timestamp)); + obj->flags |= USED; + mark_object_reachable(obj); + } else { + 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, timestamp_t timestamp, int tz, + const char *message, void *cb_data) +{ + const char *refname = cb_data; + + if (verbose) + fprintf(stderr, "Checking reflog %s->%s\n", + 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, + int flag, void *cb_data) +{ + for_each_reflog_ent(logname, fsck_handle_reflog_ent, (void *)logname); + return 0; +} + +static int fsck_handle_ref(const char *refname, const struct object_id *oid, + int flag, void *cb_data) +{ + struct object *obj; + + obj = parse_object(oid); + if (!obj) { + 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; + if (name_objects) + add_decoration(fsck_walk_options.object_names, + obj, xstrdup(refname)); + mark_object_reachable(obj); + + return 0; +} + +static void get_default_heads(void) +{ + if (head_points_at && !is_null_oid(&head_oid)) + fsck_handle_ref("HEAD", &head_oid, 0, NULL); + for_each_rawref(fsck_handle_ref, NULL); + if (include_reflogs) + for_each_reflog(fsck_handle_reflog, NULL); + + /* + * 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(stderr, "notice: No default references\n"); + show_unreachable = 0; + } +} + +static struct object *parse_loose_object(const struct object_id *oid, + const char *path) +{ + struct object *obj; + void *contents; + enum object_type type; + unsigned long size; + int eaten; + + if (read_loose_object(path, oid->hash, &type, &size, &contents) < 0) + return NULL; + + if (!contents && type != OBJ_BLOB) + die("BUG: read_loose_object streamed a non-blob"); + + obj = parse_object_buffer(oid, type, size, contents, &eaten); + + if (!eaten) + free(contents); + return obj; +} + +static int fsck_loose(const struct object_id *oid, const char *path, void *data) +{ + struct object *obj = parse_loose_object(oid, path); + + if (!obj) { + errors_found |= ERROR_OBJECT; + error("%s: object corrupt or missing: %s", + oid_to_hex(oid), path); + return 0; /* keep checking other objects */ + } + + obj->flags &= ~(REACHABLE | SEEN); + obj->flags |= HAS_OBJ; + if (fsck_obj(obj)) + errors_found |= ERROR_OBJECT; + return 0; +} + +static int fsck_cruft(const char *basename, const char *path, void *data) +{ + if (!starts_with(basename, "tmp_obj_")) + fprintf(stderr, "bad sha1 file: %s\n", path); + return 0; +} + +static int fsck_subdir(unsigned int nr, const char *path, void *progress) +{ + display_progress(progress, nr + 1); + return 0; +} + +static void fsck_object_dir(const char *path) +{ + struct progress *progress = NULL; + + if (verbose) + fprintf(stderr, "Checking object directory\n"); + + if (show_progress) + progress = start_progress(_("Checking object directories"), 256); + + for_each_loose_file_in_objdir(path, fsck_loose, fsck_cruft, fsck_subdir, + progress); + display_progress(progress, 256); + stop_progress(&progress); +} + +static int fsck_head_link(void) +{ + int null_is_error = 0; + + if (verbose) + fprintf(stderr, "Checking HEAD link\n"); + + head_points_at = resolve_ref_unsafe("HEAD", 0, head_oid.hash, NULL); + if (!head_points_at) { + errors_found |= ERROR_REFS; + return error("Invalid HEAD"); + } + if (!strcmp(head_points_at, "HEAD")) + /* detached HEAD */ + null_is_error = 1; + else if (!starts_with(head_points_at, "refs/heads/")) { + errors_found |= ERROR_REFS; + return error("HEAD points to something strange (%s)", + head_points_at); + } + if (is_null_oid(&head_oid)) { + if (null_is_error) { + errors_found |= ERROR_REFS; + return error("HEAD: detached HEAD points at nothing"); + } + fprintf(stderr, "notice: HEAD points to an unborn branch (%s)\n", + head_points_at + 11); + } + return 0; +} + +static int fsck_cache_tree(struct cache_tree *it) +{ + int i; + int err = 0; + + if (verbose) + fprintf(stderr, "Checking cache tree\n"); + + if (0 <= it->entry_count) { + struct object *obj = parse_object(&it->oid); + if (!obj) { + error("%s: invalid sha1 pointer in cache-tree", + oid_to_hex(&it->oid)); + errors_found |= ERROR_REFS; + return 1; + } + obj->flags |= USED; + if (name_objects) + add_decoration(fsck_walk_options.object_names, + obj, xstrdup(":")); + 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); + return err; +} + +static void mark_object_for_connectivity(const struct object_id *oid) +{ + struct object *obj = lookup_unknown_object(oid->hash); + obj->flags |= HAS_OBJ; +} + +static int mark_loose_for_connectivity(const struct object_id *oid, + const char *path, + void *data) +{ + mark_object_for_connectivity(oid); + return 0; +} + +static int mark_packed_for_connectivity(const struct object_id *oid, + struct packed_git *pack, + uint32_t pos, + void *data) +{ + mark_object_for_connectivity(oid); + return 0; +} + +static char const * const fsck_usage[] = { + N_("git fsck [<options>] [<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 alternate_object_database *alt; + + errors_found = 0; + check_replace_refs = 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_walk_options.object_names = + xcalloc(1, sizeof(struct decoration)); + + git_config(fsck_config, NULL); + + fsck_head_link(); + if (connectivity_only) { + for_each_loose_object(mark_loose_for_connectivity, NULL, 0); + for_each_packed_object(mark_packed_for_connectivity, NULL, 0); + } else { + fsck_object_dir(get_object_directory()); + + prepare_alt_odb(); + for (alt = alt_odb_list; alt; alt = alt->next) + fsck_object_dir(alt->path); + + if (check_full) { + struct packed_git *p; + uint32_t total = 0, count = 0; + struct progress *progress = NULL; + + prepare_packed_git(); + + if (show_progress) { + for (p = packed_git; p; p = p->next) { + if (open_pack_index(p)) + continue; + total += p->num_objects; + } + + progress = start_progress(_("Checking objects"), total); + } + for (p = packed_git; p; p = p->next) { + /* verify gives error messages itself */ + if (verify_pack(p, fsck_obj_buffer, + progress, count)) + errors_found |= ERROR_PACK; + count += p->num_objects; + } + stop_progress(&progress); + } + } + + for (i = 0; i < argc; i++) { + const char *arg = argv[i]; + struct object_id oid; + if (!get_oid(arg, &oid)) { + struct object *obj = lookup_object(oid.hash); + + if (!obj || !(obj->flags & HAS_OBJ)) { + error("%s: object missing", oid_to_hex(&oid)); + errors_found |= ERROR_OBJECT; + continue; + } + + obj->flags |= USED; + if (name_objects) + add_decoration(fsck_walk_options.object_names, + obj, xstrdup(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) { + verify_index_checksum = 1; + read_cache(); + for (i = 0; i < active_nr; i++) { + unsigned int mode; + struct blob *blob; + struct object *obj; + + mode = active_cache[i]->ce_mode; + if (S_ISGITLINK(mode)) + continue; + blob = lookup_blob(&active_cache[i]->oid); + if (!blob) + continue; + obj = &blob->object; + obj->flags |= USED; + if (name_objects) + add_decoration(fsck_walk_options.object_names, + obj, + xstrfmt(":%s", active_cache[i]->name)); + mark_object_reachable(obj); + } + if (active_cache_tree) + fsck_cache_tree(active_cache_tree); + } + + check_connectivity(); + return errors_found; +} diff --git a/builtin/gc.c b/builtin/gc.c new file mode 100644 index 0000000000..3c5eae0edf --- /dev/null +++ b/builtin/gc.c @@ -0,0 +1,488 @@ +/* + * 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 "config.h" +#include "tempfile.h" +#include "lockfile.h" +#include "parse-options.h" +#include "run-command.h" +#include "sigchain.h" +#include "argv-array.h" +#include "commit.h" +#include "packfile.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 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 struct argv_array pack_refs_cmd = ARGV_ARRAY_INIT; +static struct argv_array reflog = ARGV_ARRAY_INIT; +static struct argv_array repack = ARGV_ARRAY_INIT; +static struct argv_array prune = ARGV_ARRAY_INIT; +static struct argv_array prune_worktrees = ARGV_ARRAY_INIT; +static struct argv_array rerere = ARGV_ARRAY_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_tempfile_path(log_lock.tempfile), + 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 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); + } + + 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_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(git_default_config, NULL); +} + +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; + + if (gc_auto_threshold <= 0) + return 0; + + 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") != 38 || + ent->d_name[38] != '\0') + continue; + if (++num_loose > auto_threshold) { + needed = 1; + break; + } + } + closedir(dir); + return needed; +} + +static int too_many_packs(void) +{ + struct packed_git *p; + int cnt; + + if (gc_auto_pack_limit <= 0) + return 0; + + prepare_packed_git(); + for (cnt = 0, p = packed_git; 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 void add_repack_all_option(void) +{ + if (prune_expire && !strcmp(prune_expire, "now")) + argv_array_push(&repack, "-a"); + else { + argv_array_push(&repack, "-A"); + if (prune_expire) + argv_array_pushf(&repack, "--unpack-unreachable=%s", prune_expire); + } +} + +static void add_repack_incremental_option(void) +{ + argv_array_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()) + add_repack_all_option(); + else if (too_many_loose_objects()) + add_repack_incremental_option(); + else + return 0; + + if (run_hook_le(NULL, "pre-auto-gc", NULL)) + 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) +{ + static struct lock_file lock; + 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 != NULL) + 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; +} + +static int report_last_gc_error(void) +{ + struct strbuf sb = STRBUF_INIT; + int ret = 0; + struct stat st; + char *gc_log_path = git_pathdup("gc.log"); + + if (stat(gc_log_path, &st)) { + if (errno == ENOENT) + goto done; + + ret = error_errno(_("Can't stat %s"), gc_log_path); + goto done; + } + + if (st.st_mtime < gc_log_expire_time) + goto done; + + ret = strbuf_read_file(&sb, gc_log_path, 0); + if (ret > 0) + ret = error(_("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); + strbuf_release(&sb); +done: + free(gc_log_path); + return ret; +} + +static int gc_before_repack(void) +{ + if (pack_refs && run_command_v_opt(pack_refs_cmd.argv, RUN_GIT_CMD)) + return error(FAILED_RUN, pack_refs_cmd.argv[0]); + + if (prune_reflogs && run_command_v_opt(reflog.argv, RUN_GIT_CMD)) + return error(FAILED_RUN, reflog.argv[0]); + + pack_refs = 0; + prune_reflogs = 0; + return 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; + + 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, "aggressive", &aggressive, N_("be more thorough (increased runtime)")), + OPT_BOOL(0, "auto", &auto_gc, N_("enable auto-gc mode")), + OPT_BOOL(0, "force", &force, N_("force running gc even if there may be another gc running")), + OPT_END() + }; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(builtin_gc_usage, builtin_gc_options); + + argv_array_pushl(&pack_refs_cmd, "pack-refs", "--all", "--prune", NULL); + argv_array_pushl(&reflog, "reflog", "expire", "--all", NULL); + argv_array_pushl(&repack, "repack", "-d", "-l", NULL); + argv_array_pushl(&prune, "prune", "--expire", NULL); + argv_array_pushl(&prune_worktrees, "worktree", "prune", "--expire", NULL); + argv_array_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 (aggressive) { + argv_array_push(&repack, "-f"); + if (aggressive_depth > 0) + argv_array_pushf(&repack, "--depth=%d", aggressive_depth); + if (aggressive_window > 0) + argv_array_pushf(&repack, "--window=%d", aggressive_window); + } + if (quiet) + argv_array_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) { + if (report_last_gc_error()) + return -1; + + if (lock_repo_for_gc(force, &pid)) + return 0; + if (gc_before_repack()) + return -1; + delete_tempfile(&pidfile); + + /* + * failure to daemonize is ok, we'll continue + * in foreground + */ + daemonized = !daemonize(); + } + } else + add_repack_all_option(); + + 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); + } + + if (gc_before_repack()) + return -1; + + if (!repository_format_precious_objects) { + if (run_command_v_opt(repack.argv, RUN_GIT_CMD)) + return error(FAILED_RUN, repack.argv[0]); + + if (prune_expire) { + argv_array_push(&prune, prune_expire); + if (quiet) + argv_array_push(&prune, "--no-progress"); + if (run_command_v_opt(prune.argv, RUN_GIT_CMD)) + return error(FAILED_RUN, prune.argv[0]); + } + } + + if (prune_worktrees_expire) { + argv_array_push(&prune_worktrees, prune_worktrees_expire); + if (run_command_v_opt(prune_worktrees.argv, RUN_GIT_CMD)) + return error(FAILED_RUN, prune_worktrees.argv[0]); + } + + if (run_command_v_opt(rerere.argv, RUN_GIT_CMD)) + return error(FAILED_RUN, rerere.argv[0]); + + report_garbage = report_pack_garbage; + reprepare_packed_git(); + if (pack_garbage.nr > 0) + clean_pack_garbage(); + + 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; +} diff --git a/builtin/get-tar-commit-id.c b/builtin/get-tar-commit-id.c new file mode 100644 index 0000000000..2706fcfaf2 --- /dev/null +++ b/builtin/get-tar-commit-id.c @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2005, 2006 Rene Scharfe + */ +#include "cache.h" +#include "commit.h" +#include "tar.h" +#include "builtin.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, const char *prefix) +{ + char buffer[HEADERSIZE]; + struct ustar_header *header = (struct ustar_header *)buffer; + char *content = buffer + RECORDSIZE; + const char *comment; + ssize_t n; + + 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] != 'g') + return 1; + if (!skip_prefix(content, "52 comment=", &comment)) + return 1; + + if (write_in_full(1, comment, 41) < 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 0000000000..2d65f27d01 --- /dev/null +++ b/builtin/grep.c @@ -0,0 +1,1107 @@ +/* + * Builtin "git grep" + * + * Copyright (c) 2006 Junio C Hamano + */ +#include "cache.h" +#include "repository.h" +#include "config.h" +#include "blob.h" +#include "tree.h" +#include "commit.h" +#include "tag.h" +#include "tree-walk.h" +#include "builtin.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 "submodule.h" +#include "submodule-config.h" + +static char const * const grep_usage[] = { + N_("git grep [<options>] [-e] <pattern> [<rev>...] [[--] <path>...]"), + NULL +}; + +static int recurse_submodules; + +#define GREP_NUM_THREADS_DEFAULT 8 +static int num_threads; + +#ifndef NO_PTHREADS +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; + +/* This lock protects all the variables above. */ +static pthread_mutex_t grep_mutex; + +static inline void grep_lock(void) +{ + assert(num_threads); + pthread_mutex_lock(&grep_mutex); +} + +static inline void grep_unlock(void) +{ + assert(num_threads); + 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, enum grep_source_type type, + const char *name, const char *path, const void *id) +{ + grep_lock(); + + while ((todo_end+1) % ARRAY_SIZE(todo) == todo_done) { + pthread_cond_wait(&cond_write, &grep_mutex); + } + + grep_source_init(&todo[todo_end].source, type, name, path, id); + if (opt->binary != GREP_BINARY_TEXT) + grep_source_load_driver(&todo[todo_end].source); + 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 *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(arg); + free(arg); + + 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_read_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; + + for (i = 0; i < ARRAY_SIZE(todo); i++) { + strbuf_init(&todo[i].out, 0); + } + + threads = xcalloc(num_threads, sizeof(*threads)); + for (i = 0; i < num_threads; i++) { + int err; + struct grep_opt *o = grep_opt_dup(opt); + o->output = strbuf_out; + if (i) + o->debug = 0; + 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; + + 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_read_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; + + return hit; +} +#else /* !NO_PTHREADS */ + +static int wait_all(void) +{ + return 0; +} +#endif + +static int grep_cmd_config(const char *var, const char *value, void *cb) +{ + int st = grep_config(var, value, cb); + if (git_color_default_config(var, value, cb) < 0) + st = -1; + + if (!strcmp(var, "grep.threads")) { + num_threads = git_config_int(var, value); + if (num_threads < 0) + die(_("invalid number of threads specified (%d) for %s"), + num_threads, var); +#ifdef NO_PTHREADS + else if (num_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 = 0; + } +#endif + } + + if (!strcmp(var, "submodule.recurse")) + recurse_submodules = git_config_bool(var, value); + + return st; +} + +static void *lock_and_read_oid_file(const struct object_id *oid, enum object_type *type, unsigned long *size) +{ + void *data; + + grep_read_lock(); + data = read_sha1_file(oid->hash, type, size); + grep_read_unlock(); + return data; +} + +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; + + if (opt->relative && opt->prefix_length) { + quote_path_relative(filename + tree_name_len, opt->prefix, &pathbuf); + strbuf_insert(&pathbuf, 0, filename, tree_name_len); + } else { + strbuf_addstr(&pathbuf, filename); + } + +#ifndef NO_PTHREADS + if (num_threads) { + add_work(opt, GREP_SOURCE_OID, pathbuf.buf, path, oid); + strbuf_release(&pathbuf); + return 0; + } else +#endif + { + struct grep_source gs; + int hit; + + grep_source_init(&gs, GREP_SOURCE_OID, pathbuf.buf, path, oid); + strbuf_release(&pathbuf); + 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; + + if (opt->relative && opt->prefix_length) + quote_path_relative(filename, opt->prefix, &buf); + else + strbuf_addstr(&buf, filename); + +#ifndef NO_PTHREADS + if (num_threads) { + add_work(opt, GREP_SOURCE_FILE, buf.buf, filename, filename); + strbuf_release(&buf); + return 0; + } else +#endif + { + struct grep_source gs; + int hit; + + grep_source_init(&gs, GREP_SOURCE_FILE, buf.buf, filename, filename); + strbuf_release(&buf); + 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(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++) + argv_array_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, struct repository *repo, + 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, struct repository *repo); + +static int grep_submodule(struct grep_opt *opt, struct repository *superproject, + const struct pathspec *pathspec, + const struct object_id *oid, + const char *filename, const char *path) +{ + struct repository submodule; + int hit; + + if (!is_submodule_active(superproject, path)) + return 0; + + if (repo_submodule_init(&submodule, superproject, path)) + return 0; + + repo_read_gitmodules(&submodule); + + /* + * NEEDSWORK: This adds the submodule's object directory to the list of + * alternates for the single in-memory object store. This has some bad + * consequences for memory (processed objects will never be freed) and + * performance (this increases the number of pack files git has to pay + * attention to, to the sum of the number of pack files in all the + * repositories processed so far). This can be removed once the object + * store is no longer global and instead is a member of the repository + * object. + */ + add_to_alternates_memory(submodule.objectdir); + + if (oid) { + struct object *object; + struct tree_desc tree; + void *data; + unsigned long size; + struct strbuf base = STRBUF_INIT; + + object = parse_object_or_die(oid, oid_to_hex(oid)); + + grep_read_lock(); + data = read_object_with_reference(object->oid.hash, tree_type, + &size, NULL); + grep_read_unlock(); + + if (!data) + die(_("unable to read tree (%s)"), oid_to_hex(&object->oid)); + + strbuf_addstr(&base, filename); + strbuf_addch(&base, '/'); + + init_tree_desc(&tree, data, size); + hit = grep_tree(opt, pathspec, &tree, &base, base.len, + object->type == OBJ_COMMIT, &submodule); + strbuf_release(&base); + free(data); + } else { + hit = grep_cache(opt, &submodule, pathspec, 1); + } + + repo_clear(&submodule); + return hit; +} + +static int grep_cache(struct grep_opt *opt, struct repository *repo, + const struct pathspec *pathspec, int cached) +{ + 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); + } + + repo_read_index(repo); + + for (nr = 0; nr < repo->index->cache_nr; nr++) { + const struct cache_entry *ce = repo->index->cache[nr]; + strbuf_setlen(&name, name_base_len); + strbuf_addstr(&name, ce->name); + + if (S_ISREG(ce->ce_mode) && + match_pathspec(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) || + ce_skip_worktree(ce)) { + 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(pathspec, name.buf, NULL)) { + hit |= grep_submodule(opt, repo, pathspec, NULL, ce->name, ce->name); + } 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) +{ + 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(&entry, &name, + 0, 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 = lock_and_read_oid_file(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, repo); + free(data); + } else if (recurse_submodules && S_ISGITLINK(entry.mode)) { + hit |= grep_submodule(opt, repo, pathspec, entry.oid, + base->buf, base->buf + tn_len); + } + + 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, + struct repository *repo) +{ + 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; + + grep_read_lock(); + data = read_object_with_reference(obj->oid.hash, tree_type, + &size, NULL); + grep_read_unlock(); + + 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, repo); + strbuf_release(&base); + free(data); + return hit; + } + die(_("unable to grep from object of type %s"), typename(obj->type)); +} + +static int grep_objects(struct grep_opt *opt, const struct pathspec *pathspec, + struct repository *repo, + 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; + real_obj = deref_tag(list->objects[i].item, NULL, 0); + + /* load the gitmodules file for this rev */ + if (recurse_submodules) { + submodule_free(); + gitmodules_config_oid(&real_obj->oid); + } + if (grep_object(opt, pathspec, real_obj, list->objects[i].name, list->objects[i].path, + repo)) { + 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; + int i, hit = 0; + + memset(&dir, 0, sizeof(dir)); + if (!use_index) + dir.flags |= DIR_NO_GITLINKS; + if (exc_std) + setup_standard_excludes(&dir); + + fill_directory(&dir, &the_index, pathspec); + for (i = 0; i < dir.nr; i++) { + if (!dir_path_match(dir.entries[i], pathspec, 0, NULL)) + continue; + hit |= grep_file(opt, dir.entries[i]->name); + if (hit && opt->status_only) + break; + } + 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 = !strcmp(arg, "-"); + FILE *patterns; + int lno = 0; + struct strbuf sb = STRBUF_INIT; + + patterns = from_stdin ? stdin : fopen(arg, "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); + return 0; +} + +static int not_callback(const struct option *opt, const char *arg, int unset) +{ + struct grep_opt *grep_opt = opt->value; + 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; + 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; + 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; + 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; + 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_NODUP; + int i; + int dummy; + int use_index = 1; + int pattern_type_arg = GREP_PATTERN_TYPE_UNSPECIFIED; + 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")), + { OPTION_INTEGER, 0, "max-depth", &opt.max_depth, N_("depth"), + N_("descend at most <depth> levels"), PARSE_OPT_NONEG, + NULL, 1 }, + OPT_GROUP(""), + OPT_SET_INT('E', "extended-regexp", &pattern_type_arg, + N_("use extended POSIX regular expressions"), + GREP_PATTERN_TYPE_ERE), + OPT_SET_INT('G', "basic-regexp", &pattern_type_arg, + N_("use basic POSIX regular expressions (default)"), + GREP_PATTERN_TYPE_BRE), + OPT_SET_INT('F', "fixed-strings", &pattern_type_arg, + N_("interpret patterns as fixed strings"), + GREP_PATTERN_TYPE_FIXED), + OPT_SET_INT('P', "perl-regexp", &pattern_type_arg, + N_("use Perl-compatible regular expressions"), + GREP_PATTERN_TYPE_PCRE), + OPT_GROUP(""), + OPT_BOOL('n', "line-number", &opt.linenum, N_("show line numbers")), + 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('z', "null", &opt.null_following_name, + N_("print NUL after filenames")), + 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), + { OPTION_CALLBACK, 'e', NULL, &opt, N_("pattern"), + N_("match <pattern>"), PARSE_OPT_NONEG, pattern_callback }, + { OPTION_CALLBACK, 0, "and", &opt, NULL, + N_("combine patterns specified with -e"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, and_callback }, + OPT_BOOL(0, "or", &dummy, ""), + { OPTION_CALLBACK, 0, "not", &opt, NULL, "", + PARSE_OPT_NOARG | PARSE_OPT_NONEG, not_callback }, + { OPTION_CALLBACK, '(', NULL, &opt, NULL, "", + PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_NODASH, + open_callback }, + { OPTION_CALLBACK, ')', 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")), + { OPTION_SET_INT, 0, "debug", &opt.debug, NULL, + N_("show parse tree for grep expression"), + PARSE_OPT_NOARG | PARSE_OPT_HIDDEN, NULL, 1 }, + OPT_GROUP(""), + { OPTION_STRING, 'O', "open-files-in-pager", &show_in_pager, + N_("pager"), N_("show matching files in the pager"), + PARSE_OPT_OPTARG, NULL, (intptr_t)default_pager }, + OPT_BOOL(0, "ext-grep", &external_grep_allowed__ignored, + N_("allow calling of grep(1) (ignored by this build)")), + OPT_END() + }; + + init_grep_defaults(); + git_config(grep_cmd_config, NULL); + grep_init(&opt, prefix); + + /* + * 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); + grep_commit_pattern_type(pattern_type_arg, &opt); + + 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(); + } + + /* + * 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.")); + + /* + * 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(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; + +#ifndef NO_PTHREADS + if (list.nr || cached || show_in_pager) + num_threads = 0; + else if (num_threads == 0) + num_threads = GREP_NUM_THREADS_DEFAULT; + else if (num_threads < 0) + die(_("invalid number of threads specified (%d)"), num_threads); + if (num_threads == 1) + num_threads = 0; +#else + if (num_threads) + warning(_("no threads support, ignoring --threads")); + num_threads = 0; +#endif + + if (!num_threads) + /* + * The compiled patterns on the main path are only + * used when not using threading. Otherwise + * start_threads() below calls compile_grep_patterns() + * for each thread. + */ + compile_grep_patterns(&opt); + +#ifndef NO_PTHREADS + if (num_threads) { + 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; + start_threads(&opt); + } +#endif + + 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(&path_list, buf.buf); + strbuf_detach(&buf, NULL); + } + } + + if (recurse_submodules && (!use_index || untracked)) + die(_("option not supported with --recurse-submodules.")); + + if (!show_in_pager && !opt.status_only) + setup_pager(); + + if (!use_index && (untracked || cached)) + die(_("--cached or --untracked cannot be used with --no-index.")); + + 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, the_repository, &pathspec, cached); + } else { + if (cached) + die(_("both --cached and trees are given.")); + + hit = grep_objects(&opt, &pathspec, the_repository, &list); + } + + if (num_threads) + hit |= wait_all(); + if (hit && show_in_pager) + run_pager(&opt, prefix); + clear_pathspec(&pathspec); + free_grep_patterns(&opt); + return !hit; +} diff --git a/builtin/hash-object.c b/builtin/hash-object.c new file mode 100644 index 0000000000..c532ff9320 --- /dev/null +++ b/builtin/hash-object.c @@ -0,0 +1,159 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + * Copyright (C) Junio C Hamano, 2005 + */ +#include "builtin.h" +#include "config.h" +#include "blob.h" +#include "quote.h" +#include "parse-options.h" +#include "exec_cmd.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 = hash_sha1_file_literally(buf.buf, buf.len, type, oid, flags); + 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(&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 = open(path, O_RDONLY); + if (fd < 0) + die_errno("Cannot open '%s'", path); + 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] [--stdin] [--] <file>..."), + N_("git hash-object --stdin-paths"), + 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; + 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, NULL, 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 = xstrdup(prefix_filename(prefix, vpath)); + + 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); + + return 0; +} diff --git a/builtin/help.c b/builtin/help.c new file mode 100644 index 0000000000..d3c8fc4082 --- /dev/null +++ b/builtin/help.c @@ -0,0 +1,513 @@ +/* + * Builtin help command + */ +#include "cache.h" +#include "config.h" +#include "builtin.h" +#include "exec_cmd.h" +#include "parse-options.h" +#include "run-command.h" +#include "column.h" +#include "help.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 +}; + +static const char *html_path; + +static int show_all = 0; +static int show_guides = 0; +static unsigned int colopts; +static enum help_format help_format = HELP_FORMAT_NONE; +static int exclude_guides; +static struct option builtin_help_options[] = { + OPT_BOOL('a', "all", &show_all, N_("print all available commands")), + OPT_HIDDEN_BOOL(0, "exclude-guides", &exclude_guides, N_("exclude guides")), + OPT_BOOL('g', "guides", &show_guides, N_("print list of useful 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_END(), +}; + +static const char * const builtin_help_usage[] = { + N_("git help [--all] [--guides] [--man | --web | --info] [<command>]"), + NULL +}; + +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; + 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; + const char *argv_ec[] = { "emacsclient", "--version", NULL }; + int version; + + /* emacsclient prints its version number on stderr */ + ec_process.argv = argv_ec; + 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; + FLEX_ALLOC_MEM(new, name, name, len); + new->info = xstrdup(value); + new->next = man_viewer_info_list; + man_viewer_info_list = new; +} + +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; + int 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, void *cb) +{ + if (starts_with(var, "column.")) + return git_column_config(var, value, "help", &colopts); + 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, 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 + 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 *git_cmd) +{ + struct man_viewer_list *viewer; + const char *page = cmd_to_page(git_cmd); + 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 *git_cmd) +{ + const char *page = cmd_to_page(git_cmd); + 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 we have a git documentation directory. */ + if (!strstr(html_path, "://")) { + if (stat(mkpath("%s/git.html", html_path), &st) + || !S_ISREG(st.st_mode)) + die("'%s': not a documentation directory.", html_path); + } + + 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 *git_cmd) +{ + const char *page = cmd_to_page(git_cmd); + struct strbuf page_path; /* it leaks but we exec bellow */ + + get_html_page_path(&page_path, page); + + open_html(page_path.buf); +} + +static struct { + const char *name; + const char *help; +} common_guides[] = { + { "attributes", N_("Defining attributes per path") }, + { "everyday", N_("Everyday Git With 20 Commands Or So") }, + { "glossary", N_("A Git glossary") }, + { "ignore", N_("Specifies intentionally untracked files to ignore") }, + { "modules", N_("Defining submodule properties") }, + { "revisions", N_("Specifying revisions and ranges for Git") }, + { "tutorial", N_("A tutorial introduction to Git (for version 1.5.1 or newer)") }, + { "workflows", N_("An overview of recommended workflows with Git") }, +}; + +static void list_common_guides_help(void) +{ + int i, longest = 0; + + for (i = 0; i < ARRAY_SIZE(common_guides); i++) { + if (longest < strlen(common_guides[i].name)) + longest = strlen(common_guides[i].name); + } + + puts(_("The common Git guides are:\n")); + for (i = 0; i < ARRAY_SIZE(common_guides); i++) { + printf(" %s ", common_guides[i].name); + mput_char(' ', longest - strlen(common_guides[i].name)); + puts(_(common_guides[i].help)); + } + putchar('\n'); +} + +static const char *check_git_cmd(const char* cmd) +{ + char *alias; + + if (is_git_command(cmd)) + return cmd; + + alias = alias_lookup(cmd); + if (alias) { + printf_ln(_("'%s' is aliased to '%s'"), cmd, alias); + free(alias); + exit(0); + } + + if (exclude_guides) + return help_unknown_cmd(cmd); + + return cmd; +} + +int cmd_help(int argc, const char **argv, const char *prefix) +{ + int nongit; + enum help_format parsed_help_format; + + argc = parse_options(argc, argv, prefix, builtin_help_options, + builtin_help_usage, 0); + parsed_help_format = help_format; + + if (show_all) { + git_config(git_help_config, NULL); + printf(_("usage: %s%s"), _(git_usage_string), "\n\n"); + load_command_list("git-", &main_cmds, &other_cmds); + list_commands(colopts, &main_cmds, &other_cmds); + } + + if (show_guides) + list_common_guides_help(); + + if (show_all || show_guides) { + printf("%s\n", _(git_more_info_string)); + /* + * We're done. Ignore any remaining args + */ + 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]); + + switch (help_format) { + case HELP_FORMAT_NONE: + case HELP_FORMAT_MAN: + show_man_page(argv[0]); + break; + case HELP_FORMAT_INFO: + show_info_page(argv[0]); + break; + case HELP_FORMAT_WEB: + show_html_page(argv[0]); + break; + } + + return 0; +} diff --git a/builtin/index-pack.c b/builtin/index-pack.c new file mode 100644 index 0000000000..8ec459f522 --- /dev/null +++ b/builtin/index-pack.c @@ -0,0 +1,1817 @@ +#include "builtin.h" +#include "config.h" +#include "delta.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 "streaming.h" +#include "thread-utils.h" +#include "packfile.h" + +static const char index_pack_usage[] = +"git index-pack [-v] [-o <index-file>] [--keep | --keep=<msg>] [--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 { + struct base_data *base; + struct base_data *child; + struct object_entry *obj; + void *data; + unsigned long size; + int ref_first, ref_last; + int ofs_first, ofs_last; +}; + +struct thread_local { +#ifndef NO_PTHREADS + pthread_t thread; +#endif + struct base_data *base_cache; + size_t base_cache_used; + int pack_fd; +}; + +#define FLAG_LINK (1u<<20) +#define FLAG_CHECKED (1u<<21) + +struct ofs_delta_entry { + off_t offset; + int obj_no; +}; + +struct ref_delta_entry { + unsigned char sha1[20]; + 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_STRICT; +static int verbose; +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_SHA_CTX input_ctx; +static uint32_t input_crc32; +static int input_fd, output_fd; +static const char *curr_pack; + +#ifndef NO_PTHREADS + +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_mutex_t type_cas_mutex; +#define type_cas_lock() lock_mutex(&type_cas_mutex) +#define type_cas_unlock() unlock_mutex(&type_cas_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); + pthread_mutex_init(&type_cas_mutex, NULL); + if (show_stat) + pthread_mutex_init(&deepest_delta_mutex, NULL); + pthread_key_create(&key, NULL); + thread_data = xcalloc(nr_threads, sizeof(*thread_data)); + for (i = 0; i < nr_threads; i++) { + thread_data[i].pack_fd = open(curr_pack, O_RDONLY); + if (thread_data[i].pack_fd == -1) + die_errno(_("unable to open %s"), curr_pack); + } + + 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); + pthread_mutex_destroy(&type_cas_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); +} + +#else + +#define read_lock() +#define read_unlock() + +#define counter_lock() +#define counter_unlock() + +#define work_lock() +#define work_unlock() + +#define deepest_delta_lock() +#define deepest_delta_unlock() + +#define type_cas_lock() +#define type_cas_unlock() + +#endif + + +static int mark_link(struct object *obj, int type, void *data, struct fsck_options *options) +{ + 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 = sha1_object_info(obj->oid.hash, &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), + typename(obj->type), typename(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(); + for (i = 0; i < max; i++) + foreign_nr += check_object(get_indexed_object(i)); + 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); + git_SHA1_Update(&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) + die(_("pack exceeds maximum allowed size")); +} + +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 = open(pack_name, O_CREAT|O_EXCL|O_RDWR, 0600); + if (output_fd < 0) + die_errno(_("unable to create '%s'"), pack_name); + } + nothread_data.pack_fd = output_fd; + } else { + input_fd = open(pack_name, O_RDONLY); + if (input_fd < 0) + die_errno(_("cannot open packfile '%s'"), pack_name); + output_fd = -1; + nothread_data.pack_fd = input_fd; + } + git_SHA1_Init(&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)); +} + +static NORETURN void bad_object(off_t offset, const char *format, + ...) __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) +{ +#ifndef NO_PTHREADS + if (threads_active) + return pthread_getspecific(key); + assert(!threads_active && + "This should only be reached when all threads are gone"); +#endif + return ¬hread_data; +} + +#ifndef NO_PTHREADS +static void set_thread_data(struct thread_local *data) +{ + if (threads_active) + pthread_setspecific(key, data); +} +#endif + +static struct base_data *alloc_base_data(void) +{ + struct base_data *base = xcalloc(1, sizeof(struct base_data)); + base->ref_last = -1; + base->ofs_last = -1; + return base; +} + +static void free_base_data(struct base_data *c) +{ + if (c->data) { + FREE_AND_NULL(c->data); + get_thread_data()->base_cache_used -= c->size; + } +} + +static void prune_base_data(struct base_data *retain) +{ + struct base_data *b; + struct thread_local *data = get_thread_data(); + for (b = data->base_cache; + data->base_cache_used > delta_base_cache_limit && b; + b = b->child) { + if (b->data && b != retain) + free_base_data(b); + } +} + +static void link_base_data(struct base_data *base, struct base_data *c) +{ + if (base) + base->child = c; + else + get_thread_data()->base_cache = c; + + c->base = base; + c->child = NULL; + if (c->data) + get_thread_data()->base_cache_used += c->size; + prune_base_data(c); +} + +static void unlink_base_data(struct base_data *c) +{ + struct base_data *base = c->base; + if (base) + base->child = NULL; + else + get_thread_data()->base_cache = NULL; + free_base_data(c); +} + +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, unsigned char *sha1) +{ + static char fixed_buf[8192]; + int status; + git_zstream stream; + void *buf; + git_SHA_CTX c; + char hdr[32]; + int hdrlen; + + if (!is_delta_type(type)) { + hdrlen = xsnprintf(hdr, sizeof(hdr), "%s %lu", typename(type), size) + 1; + git_SHA1_Init(&c); + git_SHA1_Update(&c, hdr, hdrlen); + } else + sha1 = 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 (sha1) + git_SHA1_Update(&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 (sha1) + git_SHA1_Final(sha1, &c); + return buf == fixed_buf ? NULL : buf; +} + +static void *unpack_raw_entry(struct object_entry *obj, + off_t *ofs_offset, + unsigned char *ref_sha1, + unsigned char *sha1) +{ + 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: + hashcpy(ref_sha1, fill(20)); + use(20); + 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, sha1); + 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", + (unsigned int)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, enum object_type type) +{ + 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, + type, 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, + enum object_type type) +{ + int first = find_ofs_delta(offset, type); + 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 unsigned char *sha1, + const unsigned char *sha2, + enum object_type type1, + enum object_type type2) +{ + int cmp = type1 - type2; + if (cmp) + return cmp; + return hashcmp(sha1, sha2); +} + +static int find_ref_delta(const unsigned char *sha1, enum object_type type) +{ + 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(sha1, delta->sha1, + type, 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 unsigned char *sha1, + int *first_index, int *last_index, + enum object_type type) +{ + int first = find_ref_delta(sha1, type); + int last = first; + int end = nr_ref_deltas - 1; + + if (first < 0) { + *first_index = 0; + *last_index = -1; + return; + } + while (first > 0 && !hashcmp(ref_deltas[first - 1].sha1, sha1)) + --first; + while (last < end && !hashcmp(ref_deltas[last + 1].sha1, sha1)) + ++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(entry->idx.oid.hash, &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 = + has_sha1_file_with_flags(oid->hash, 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 = sha1_object_info(oid->hash, &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 = read_sha1_file(oid->hash, &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) { + read_lock(); + if (type == OBJ_BLOB) { + struct blob *blob = lookup_blob(oid); + if (blob) + blob->object.flags |= FLAG_CHECKED; + else + die(_("invalid blob object %s"), oid_to_hex(oid)); + } 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(oid, type, size, buf, + &eaten); + if (!obj) + die(_("invalid %s"), typename(type)); + if (do_fsck_object && + fsck_object(obj, buf, size, &fsck_options)) + die(_("Error in object")); + if (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) + die("BUG: parse_object_buffer transmogrified our buffer"); + } + obj->flags |= FLAG_CHECKED; + } + read_unlock(); + } + + free(new_data); +} + +/* + * This function is part of find_unresolved_deltas(). There are two + * walkers going in the opposite ways. + * + * The first one in find_unresolved_deltas() traverses down from + * parent node to children, deflating nodes along the way. However, + * memory for deflated nodes is limited by delta_base_cache_limit, so + * at some point parent node's deflated content may be freed. + * + * The second walker is this function, which goes from current node up + * to top parent if necessary to deflate the node. In normal + * situation, its parent node would be already deflated, so it just + * needs to apply delta. + * + * In the worst case scenario, parent node is no longer deflated because + * we're running out of delta_base_cache_limit; we need to re-deflate + * parents, possibly up to the top base. + * + * All deflated objects here are subject to be freed if we exceed + * delta_base_cache_limit, just like in find_unresolved_deltas(), we + * just need to make sure the last node is not freed. + */ +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; + get_thread_data()->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")); + get_thread_data()->base_cache_used += c->size; + prune_base_data(c); + } + free(delta); + } + return c->data; +} + +static void resolve_delta(struct object_entry *delta_obj, + struct base_data *base, struct base_data *result) +{ + void *base_data, *delta_data; + + 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); + base_data = get_base_data(base); + result->obj = delta_obj; + 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_sha1_file(result->data, result->size, + typename(delta_obj->real_type), + delta_obj->idx.oid.hash); + sha1_object(result->data, NULL, result->size, delta_obj->real_type, + &delta_obj->idx.oid); + counter_lock(); + nr_resolved_deltas++; + counter_unlock(); +} + +/* + * Standard boolean compare-and-swap: atomically check whether "*type" is + * "want"; if so, swap in "set" and return true. Otherwise, leave it untouched + * and return false. + */ +static int compare_and_swap_type(signed char *type, + enum object_type want, + enum object_type set) +{ + enum object_type old; + + type_cas_lock(); + old = *type; + if (old == want) + *type = set; + type_cas_unlock(); + + return old == want; +} + +static struct base_data *find_unresolved_deltas_1(struct base_data *base, + struct base_data *prev_base) +{ + if (base->ref_last == -1 && base->ofs_last == -1) { + find_ref_delta_children(base->obj->idx.oid.hash, + &base->ref_first, &base->ref_last, + OBJ_REF_DELTA); + + find_ofs_delta_children(base->obj->idx.offset, + &base->ofs_first, &base->ofs_last, + OBJ_OFS_DELTA); + + if (base->ref_last == -1 && base->ofs_last == -1) { + free(base->data); + return NULL; + } + + link_base_data(prev_base, base); + } + + if (base->ref_first <= base->ref_last) { + struct object_entry *child = objects + ref_deltas[base->ref_first].obj_no; + struct base_data *result = alloc_base_data(); + + if (!compare_and_swap_type(&child->real_type, OBJ_REF_DELTA, + base->obj->real_type)) + die("BUG: child->real_type != OBJ_REF_DELTA"); + + resolve_delta(child, base, result); + if (base->ref_first == base->ref_last && base->ofs_last == -1) + free_base_data(base); + + base->ref_first++; + return result; + } + + if (base->ofs_first <= base->ofs_last) { + struct object_entry *child = objects + ofs_deltas[base->ofs_first].obj_no; + struct base_data *result = alloc_base_data(); + + assert(child->real_type == OBJ_OFS_DELTA); + child->real_type = base->obj->real_type; + resolve_delta(child, base, result); + if (base->ofs_first == base->ofs_last) + free_base_data(base); + + base->ofs_first++; + return result; + } + + unlink_base_data(base); + return NULL; +} + +static void find_unresolved_deltas(struct base_data *base) +{ + struct base_data *new_base, *prev_base = NULL; + for (;;) { + new_base = find_unresolved_deltas_1(base, prev_base); + + if (new_base) { + prev_base = base; + base = new_base; + } else { + free(base); + base = prev_base; + if (!base) + return; + prev_base = base->base; + } + } +} + +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 hashcmp(delta_a->sha1, delta_b->sha1); +} + +static void resolve_base(struct object_entry *obj) +{ + struct base_data *base_obj = alloc_base_data(); + base_obj->obj = obj; + base_obj->data = NULL; + find_unresolved_deltas(base_obj); +} + +#ifndef NO_PTHREADS +static void *threaded_second_pass(void *data) +{ + set_thread_data(data); + for (;;) { + int i; + counter_lock(); + display_progress(progress, nr_resolved_deltas); + counter_unlock(); + work_lock(); + while (nr_dispatched < nr_objects && + is_delta_type(objects[nr_dispatched].type)) + nr_dispatched++; + if (nr_dispatched >= nr_objects) { + work_unlock(); + break; + } + i = nr_dispatched++; + work_unlock(); + + resolve_base(&objects[i]); + } + return NULL; +} +#endif + +/* + * 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 *sha1) +{ + int i, nr_delays = 0; + struct ofs_delta_entry *ofs_delta = ofs_deltas; + unsigned char ref_delta_sha1[20]; + struct stat st; + + if (verbose) + progress = start_progress( + 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_sha1, + obj->idx.oid.hash); + 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); + hashcpy(ref_deltas[nr_ref_deltas].sha1, ref_delta_sha1); + 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(); + git_SHA1_Final(sha1, &input_ctx); + if (hashcmp(fill(20), sha1)) + die(_("pack is corrupted (SHA1 mismatch)")); + use(20); + + /* 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); + +#ifndef NO_PTHREADS + nr_dispatched = 0; + 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; + } +#endif + + for (i = 0; i < nr_objects; i++) { + struct object_entry *obj = &objects[i]; + + if (is_delta_type(obj->type)) + continue; + resolve_base(obj); + display_progress(progress, nr_resolved_deltas); + } +} + +/* + * Third pass: + * - append objects to convert thin pack to full pack if required + * - write the final 20-byte SHA-1 + */ +static void fix_unresolved_deltas(struct sha1file *f); +static void conclude_pack(int fix_thin_pack, const char *curr_pack, unsigned char *pack_sha1) +{ + if (nr_ref_deltas + nr_ofs_deltas == nr_resolved_deltas) { + stop_progress(&progress); + /* Flush remaining pack final 20-byte SHA1. */ + flush(); + return; + } + + if (fix_thin_pack) { + struct sha1file *f; + unsigned char read_sha1[20], tail_sha1[20]; + 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 = sha1fd(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); + sha1close(f, tail_sha1, 0); + hashcpy(read_sha1, pack_sha1); + fixup_pack_header_footer(output_fd, pack_sha1, + curr_pack, nr_objects, + read_sha1, consumed_bytes-20); + if (hashcmp(read_sha1, tail_sha1) != 0) + 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 sha1file *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); + sha1write(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 sha1file *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); + sha1write(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); + sha1flush(f); + hashcpy(obj->idx.oid.hash, 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 sha1file *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); + + for (i = 0; i < nr_ref_deltas; i++) { + struct ref_delta_entry *d = sorted_by_pos[i]; + enum object_type type; + struct base_data *base_obj = alloc_base_data(); + + if (objects[d->obj_no].real_type != OBJ_REF_DELTA) + continue; + base_obj->data = read_sha1_file(d->sha1, &type, &base_obj->size); + if (!base_obj->data) + continue; + + if (check_sha1_signature(d->sha1, base_obj->data, + base_obj->size, typename(type))) + die(_("local object %s is corrupt"), sha1_to_hex(d->sha1)); + base_obj->obj = append_obj_to_pack(f, d->sha1, + base_obj->data, base_obj->size, type); + find_unresolved_deltas(base_obj); + display_progress(progress, nr_resolved_deltas); + } + free(sorted_by_pos); +} + +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 *keep_name, const char *keep_msg, + unsigned char *sha1) +{ + const char *report = "pack"; + struct strbuf pack_name = STRBUF_INIT; + struct strbuf index_name = STRBUF_INIT; + struct strbuf keep_name_buf = STRBUF_INIT; + int err; + + if (!from_stdin) { + close(input_fd); + } else { + fsync_or_die(output_fd, curr_pack_name); + err = close(output_fd); + if (err) + die_errno(_("error while closing pack file")); + } + + if (keep_msg) { + int keep_fd, keep_msg_len = strlen(keep_msg); + + if (!keep_name) + keep_name = odb_pack_name(&keep_name_buf, sha1, "keep"); + + keep_fd = odb_pack_keep(keep_name); + if (keep_fd < 0) { + if (errno != EEXIST) + die_errno(_("cannot write keep file '%s'"), + keep_name); + } else { + if (keep_msg_len > 0) { + write_or_die(keep_fd, keep_msg, keep_msg_len); + write_or_die(keep_fd, "\n", 1); + } + if (close(keep_fd) != 0) + die_errno(_("cannot close written keep file '%s'"), + keep_name); + report = "keep"; + } + } + + if (final_pack_name != curr_pack_name) { + if (!final_pack_name) + final_pack_name = odb_pack_name(&pack_name, sha1, "pack"); + if (finalize_object_file(curr_pack_name, final_pack_name)) + die(_("cannot store pack file")); + } else if (from_stdin) + chmod(final_pack_name, 0444); + + if (final_index_name != curr_index_name) { + if (!final_index_name) + final_index_name = odb_pack_name(&index_name, sha1, "idx"); + if (finalize_object_file(curr_index_name, final_index_name)) + die(_("cannot store index file")); + } else + chmod(final_index_name, 0444); + + if (!from_stdin) { + printf("%s\n", sha1_to_hex(sha1)); + } else { + struct strbuf buf = STRBUF_INIT; + + strbuf_addf(&buf, "%s\t%s\n", report, sha1_to_hex(sha1)); + 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(&index_name); + strbuf_release(&pack_name); + strbuf_release(&keep_name_buf); +} + +static int git_index_pack_config(const char *k, const char *v, void *cb) +{ + struct pack_idx_option *opts = cb; + + if (!strcmp(k, "pack.indexversion")) { + opts->version = git_config_int(k, v); + 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); + if (nr_threads < 0) + die(_("invalid number of threads specified (%d)"), + nr_threads); +#ifdef NO_PTHREADS + if (nr_threads != 1) + warning(_("no threads support, ignoring %s"), k); + nr_threads = 1; +#endif + return 0; + } + return git_default_config(k, v, 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 *)p->index_data) + + 2 /* 8-byte header */ + + 256 /* fan out */ + + 5 * p->num_objects /* 20-byte SHA-1 table */ + + 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 + * sha1_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) + chain_histogram = xcalloc(deepest_delta, sizeof(unsigned long)); + + 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 %lu %lu %"PRIuMAX, + oid_to_hex(&obj->idx.oid), + typename(obj->real_type), obj->size, + (unsigned long)(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]); + } +} + +static const char *derive_filename(const char *pack_name, const char *suffix, + struct strbuf *buf) +{ + size_t len; + if (!strip_suffix(pack_name, ".pack", &len)) + die(_("packfile name '%s' does not end with '.pack'"), + pack_name); + strbuf_add(buf, pack_name, len); + strbuf_addstr(buf, suffix); + return buf->buf; +} + +int cmd_index_pack(int argc, const char **argv, const char *prefix) +{ + int i, fix_thin_pack = 0, verify = 0, stat_only = 0; + const char *curr_index; + const char *index_name = NULL, *pack_name = NULL; + const char *keep_name = NULL, *keep_msg = NULL; + struct strbuf index_name_buf = STRBUF_INIT, + keep_name_buf = STRBUF_INIT; + struct pack_idx_entry **idx_objects; + struct pack_idx_option opts; + unsigned char pack_sha1[20]; + unsigned foreign_nr = 1; /* zero is a "good" value, assume bad */ + int report_end_of_input = 0; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage(index_pack_usage); + + check_replace_refs = 0; + fsck_options.walk = mark_link; + + reset_pack_idx_option(&opts); + git_config(git_index_pack_config, &opts); + if (prefix && chdir(prefix)) + die(_("Cannot come back to cwd")); + + 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 (!strcmp(arg, "--strict")) { + strict = 1; + do_fsck_object = 1; + } else if (skip_prefix(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, "--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 (!strcmp(arg, "--keep")) { + keep_msg = ""; + } else if (starts_with(arg, "--keep=")) { + keep_msg = arg + 7; + } 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); +#ifdef NO_PTHREADS + if (nr_threads != 1) + warning(_("no threads support, " + "ignoring %s"), arg); + nr_threads = 1; +#endif + } 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, "--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 + 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(_("--fix-thin cannot be used without --stdin")); + if (from_stdin && !startup_info->have_repository) + die(_("--stdin requires a git repository")); + if (!index_name && pack_name) + index_name = derive_filename(pack_name, ".idx", &index_name_buf); + if (keep_msg && !keep_name && pack_name) + keep_name = derive_filename(pack_name, ".keep", &keep_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; + +#ifndef NO_PTHREADS + if (!nr_threads) { + nr_threads = online_cpus(); + /* An experiment showed that more threads does not mean faster */ + if (nr_threads > 3) + nr_threads = 3; + } +#endif + + curr_pack = open_pack_file(pack_name); + parse_pack_header(); + objects = xcalloc(st_add(nr_objects, 1), sizeof(struct object_entry)); + if (show_stat) + obj_stat = xcalloc(st_add(nr_objects, 1), sizeof(struct object_stat)); + ofs_deltas = xcalloc(nr_objects, sizeof(struct ofs_delta_entry)); + parse_pack_objects(pack_sha1); + if (report_end_of_input) + write_in_full(2, "\0", 1); + resolve_deltas(); + conclude_pack(fix_thin_pack, curr_pack, pack_sha1); + 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_sha1); + free(idx_objects); + + if (!verify) + final(pack_name, curr_pack, + index_name, curr_index, + keep_name, keep_msg, + pack_sha1); + else + close(input_fd); + free(objects); + strbuf_release(&index_name_buf); + strbuf_release(&keep_name_buf); + if (pack_name == NULL) + free((void *) curr_pack); + if (index_name == NULL) + free((void *) curr_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 0000000000..c9b7946bad --- /dev/null +++ b/builtin/init-db.c @@ -0,0 +1,586 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" +#include "config.h" +#include "refs.h" +#include "builtin.h" +#include "exec_cmd.h" +#include "parse-options.h" + +#ifndef DEFAULT_GIT_TEMPLATE_DIR +#define DEFAULT_GIT_TEMPLATE_DIR "/usr/share/git-core/templates" +#endif + +#ifdef NO_TRUSTABLE_FILEMODE +#define TEST_FILEMODE 0 +#else +#define TEST_FILEMODE 1 +#endif + +static int init_is_bare_repository = 0; +static int init_shared_repository = -1; +static const char *init_db_template_dir; + +static void copy_templates_1(struct strbuf *path, struct strbuf *template, + DIR *dir) +{ + size_t path_baselen = path->len; + size_t template_baselen = template->len; + struct dirent *de; + + /* Note: if ".git/hooks" file exists in the repository being + * re-initialized, /etc/core-git/templates/hooks/update would + * cause "git init" to fail here. I think this is sane but + * it means that the set of templates we ship by default, along + * with the way the namespace under .git/ is organized, should + * be really carefully chosen. + */ + safe_create_dir(path->buf, 1); + while ((de = readdir(dir)) != NULL) { + struct stat st_git, st_template; + int exists = 0; + + strbuf_setlen(path, path_baselen); + strbuf_setlen(template, template_baselen); + + if (de->d_name[0] == '.') + continue; + strbuf_addstr(path, de->d_name); + strbuf_addstr(template, de->d_name); + if (lstat(path->buf, &st_git)) { + if (errno != ENOENT) + die_errno(_("cannot stat '%s'"), path->buf); + } + else + exists = 1; + + if (lstat(template->buf, &st_template)) + die_errno(_("cannot stat template '%s'"), template->buf); + + if (S_ISDIR(st_template.st_mode)) { + DIR *subdir = opendir(template->buf); + if (!subdir) + die_errno(_("cannot opendir '%s'"), template->buf); + strbuf_addch(path, '/'); + strbuf_addch(template, '/'); + copy_templates_1(path, template, subdir); + closedir(subdir); + } + else if (exists) + continue; + else if (S_ISLNK(st_template.st_mode)) { + struct strbuf lnk = STRBUF_INIT; + if (strbuf_readlink(&lnk, template->buf, 0) < 0) + die_errno(_("cannot readlink '%s'"), template->buf); + if (symlink(lnk.buf, path->buf)) + die_errno(_("cannot symlink '%s' '%s'"), + lnk.buf, path->buf); + strbuf_release(&lnk); + } + else if (S_ISREG(st_template.st_mode)) { + if (copy_file(path->buf, template->buf, st_template.st_mode)) + die_errno(_("cannot copy '%s' to '%s'"), + template->buf, path->buf); + } + else + error(_("ignoring template %s"), template->buf); + } +} + +static void copy_templates(const char *template_dir) +{ + struct strbuf path = STRBUF_INIT; + struct strbuf template_path = STRBUF_INIT; + size_t template_len; + struct repository_format template_format; + struct strbuf err = STRBUF_INIT; + DIR *dir; + char *to_free = NULL; + + if (!template_dir) + template_dir = getenv(TEMPLATE_DIR_ENVIRONMENT); + if (!template_dir) + template_dir = init_db_template_dir; + if (!template_dir) + template_dir = to_free = system_path(DEFAULT_GIT_TEMPLATE_DIR); + if (!template_dir[0]) { + free(to_free); + return; + } + + strbuf_addstr(&template_path, template_dir); + strbuf_complete(&template_path, '/'); + template_len = template_path.len; + + dir = opendir(template_path.buf); + if (!dir) { + warning(_("templates not found %s"), template_dir); + goto free_return; + } + + /* Make sure that template is from the correct vintage */ + strbuf_addstr(&template_path, "config"); + read_repository_format(&template_format, template_path.buf); + strbuf_setlen(&template_path, template_len); + + /* + * No mention of version at all is OK, but anything else should be + * verified. + */ + if (template_format.version >= 0 && + verify_repository_format(&template_format, &err) < 0) { + warning(_("not copying templates from '%s': %s"), + template_dir, err.buf); + strbuf_release(&err); + goto close_free_return; + } + + strbuf_addstr(&path, get_git_common_dir()); + strbuf_complete(&path, '/'); + copy_templates_1(&path, &template_path, dir); +close_free_return: + closedir(dir); +free_return: + free(to_free); + strbuf_release(&path); + strbuf_release(&template_path); +} + +static int git_init_db_config(const char *k, const char *v, void *cb) +{ + if (!strcmp(k, "init.templatedir")) + return git_config_pathname(&init_db_template_dir, k, v); + + return 0; +} + +/* + * If the git_dir is not directly inside the working tree, then git will not + * find it by default, and we need to set the worktree explicitly. + */ +static int needs_work_tree_config(const char *git_dir, const char *work_tree) +{ + if (!strcmp(work_tree, "/") && !strcmp(git_dir, "/.git")) + return 0; + if (skip_prefix(git_dir, work_tree, &git_dir) && + !strcmp(git_dir, "/.git")) + return 0; + return 1; +} + +static int create_default_files(const char *template_path, + const char *original_git_dir) +{ + struct stat st1; + struct strbuf buf = STRBUF_INIT; + char *path; + char repo_version_string[10]; + char junk[2]; + int reinit; + int filemode; + struct strbuf err = STRBUF_INIT; + + /* Just look for `init.templatedir` */ + git_config(git_init_db_config, NULL); + + /* + * First copy the templates -- we might have the default + * config file there, in which case we would want to read + * from it after installing. + * + * Before reading that config, we also need to clear out any cached + * values (since we've just potentially changed what's available on + * disk). + */ + copy_templates(template_path); + git_config_clear(); + reset_shared_repository(); + git_config(git_default_config, NULL); + + /* + * We must make sure command-line options continue to override any + * values we might have just re-read from the config. + */ + is_bare_repository_cfg = init_is_bare_repository; + if (init_shared_repository != -1) + set_shared_repository(init_shared_repository); + + /* + * We would have created the above under user's umask -- under + * shared-repository settings, we would need to fix them up. + */ + if (get_shared_repository()) { + adjust_shared_perm(get_git_dir()); + } + + /* + * We need to create a "refs" dir in any case so that older + * versions of git can tell that this is a repository. + */ + safe_create_dir(git_path("refs"), 1); + adjust_shared_perm(git_path("refs")); + + if (refs_init_db(&err)) + die("failed to set up refs db: %s", err.buf); + + /* + * Create the default symlink from ".git/HEAD" to the "master" + * branch, if it does not exist yet. + */ + path = git_path_buf(&buf, "HEAD"); + reinit = (!access(path, R_OK) + || readlink(path, junk, sizeof(junk)-1) != -1); + if (!reinit) { + if (create_symref("HEAD", "refs/heads/master", NULL) < 0) + exit(1); + } + + /* This forces creation of new config file */ + xsnprintf(repo_version_string, sizeof(repo_version_string), + "%d", GIT_REPO_VERSION); + git_config_set("core.repositoryformatversion", repo_version_string); + + /* Check filemode trustability */ + path = git_path_buf(&buf, "config"); + filemode = TEST_FILEMODE; + if (TEST_FILEMODE && !lstat(path, &st1)) { + struct stat st2; + filemode = (!chmod(path, st1.st_mode ^ S_IXUSR) && + !lstat(path, &st2) && + st1.st_mode != st2.st_mode && + !chmod(path, st1.st_mode)); + if (filemode && !reinit && (st1.st_mode & S_IXUSR)) + filemode = 0; + } + git_config_set("core.filemode", filemode ? "true" : "false"); + + if (is_bare_repository()) + git_config_set("core.bare", "true"); + else { + const char *work_tree = get_git_work_tree(); + git_config_set("core.bare", "false"); + /* allow template config file to override the default */ + if (log_all_ref_updates == LOG_REFS_UNSET) + git_config_set("core.logallrefupdates", "true"); + if (needs_work_tree_config(original_git_dir, work_tree)) + git_config_set("core.worktree", work_tree); + } + + if (!reinit) { + /* Check if symlink is supported in the work tree */ + path = git_path_buf(&buf, "tXXXXXX"); + if (!close(xmkstemp(path)) && + !unlink(path) && + !symlink("testing", path) && + !lstat(path, &st1) && + S_ISLNK(st1.st_mode)) + unlink(path); /* good */ + else + git_config_set("core.symlinks", "false"); + + /* Check if the filesystem is case-insensitive */ + path = git_path_buf(&buf, "CoNfIg"); + if (!access(path, F_OK)) + git_config_set("core.ignorecase", "true"); + probe_utf8_pathname_composition(); + } + + strbuf_release(&buf); + return reinit; +} + +static void create_object_directory(void) +{ + struct strbuf path = STRBUF_INIT; + size_t baselen; + + strbuf_addstr(&path, get_object_directory()); + baselen = path.len; + + safe_create_dir(path.buf, 1); + + strbuf_setlen(&path, baselen); + strbuf_addstr(&path, "/pack"); + safe_create_dir(path.buf, 1); + + strbuf_setlen(&path, baselen); + strbuf_addstr(&path, "/info"); + safe_create_dir(path.buf, 1); + + strbuf_release(&path); +} + +static void separate_git_dir(const char *git_dir, const char *git_link) +{ + struct stat st; + + if (!stat(git_link, &st)) { + const char *src; + + if (S_ISREG(st.st_mode)) + src = read_gitfile(git_link); + else if (S_ISDIR(st.st_mode)) + src = git_link; + else + die(_("unable to handle file type %d"), (int)st.st_mode); + + if (rename(src, git_dir)) + die_errno(_("unable to move %s to %s"), src, git_dir); + } + + write_file(git_link, "gitdir: %s", git_dir); +} + +int init_db(const char *git_dir, const char *real_git_dir, + const char *template_dir, unsigned int flags) +{ + int reinit; + int exist_ok = flags & INIT_DB_EXIST_OK; + char *original_git_dir = real_pathdup(git_dir, 1); + + if (real_git_dir) { + struct stat st; + + if (!exist_ok && !stat(git_dir, &st)) + die(_("%s already exists"), git_dir); + + if (!exist_ok && !stat(real_git_dir, &st)) + die(_("%s already exists"), real_git_dir); + + set_git_dir(real_path(real_git_dir)); + git_dir = get_git_dir(); + separate_git_dir(git_dir, original_git_dir); + } + else { + set_git_dir(real_path(git_dir)); + git_dir = get_git_dir(); + } + startup_info->have_repository = 1; + + safe_create_dir(git_dir, 0); + + init_is_bare_repository = is_bare_repository(); + + /* Check to see if the repository version is right. + * Note that a newly created repository does not have + * config file, so this will not fail. What we are catching + * is an attempt to reinitialize new repository with an old tool. + */ + check_repository_format(); + + reinit = create_default_files(template_dir, original_git_dir); + + create_object_directory(); + + if (get_shared_repository()) { + char buf[10]; + /* We do not spell "group" and such, so that + * the configuration can be read by older version + * of git. Note, we use octal numbers for new share modes, + * and compatibility values for PERM_GROUP and + * PERM_EVERYBODY. + */ + if (get_shared_repository() < 0) + /* force to the mode value */ + xsnprintf(buf, sizeof(buf), "0%o", -get_shared_repository()); + else if (get_shared_repository() == PERM_GROUP) + xsnprintf(buf, sizeof(buf), "%d", OLD_PERM_GROUP); + else if (get_shared_repository() == PERM_EVERYBODY) + xsnprintf(buf, sizeof(buf), "%d", OLD_PERM_EVERYBODY); + else + die("BUG: invalid value for shared_repository"); + git_config_set("core.sharedrepository", buf); + git_config_set("receive.denyNonFastforwards", "true"); + } + + if (!(flags & INIT_DB_QUIET)) { + int len = strlen(git_dir); + + if (reinit) + printf(get_shared_repository() + ? _("Reinitialized existing shared Git repository in %s%s\n") + : _("Reinitialized existing Git repository in %s%s\n"), + git_dir, len && git_dir[len-1] != '/' ? "/" : ""); + else + printf(get_shared_repository() + ? _("Initialized empty shared Git repository in %s%s\n") + : _("Initialized empty Git repository in %s%s\n"), + git_dir, len && git_dir[len-1] != '/' ? "/" : ""); + } + + free(original_git_dir); + return 0; +} + +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) +{ + *((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>] [--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 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_END() + }; + + argc = parse_options(argc, argv, prefix, init_db_options, init_db_usage, 0); + + if (real_git_dir && !is_absolute_path(real_git_dir)) + real_git_dir = real_pathdup(real_git_dir, 1); + + 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 (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 = getenv(GIT_DIR_ENVIRONMENT); + work_tree = 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; + + 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 (work_tree) + set_git_work_tree(work_tree); + } + + UNLEAK(real_git_dir); + + flags |= INIT_DB_EXIST_OK; + return init_db(git_dir, real_git_dir, template_dir, flags); +} diff --git a/builtin/interpret-trailers.c b/builtin/interpret-trailers.c new file mode 100644 index 0000000000..b742539d4d --- /dev/null +++ b/builtin/interpret-trailers.c @@ -0,0 +1,134 @@ +/* + * Builtin "git interpret-trailers" + * + * Copyright (c) 2013, 2014 Christian Couder <chriscool@tuxfamily.org> + * + */ + +#include "cache.h" +#include "builtin.h" +#include "parse-options.h" +#include "string-list.h" +#include "trailer.h" + +static const char * const git_interpret_trailers_usage[] = { + N_("git interpret-trailers [--in-place] [--trim-empty] [(--trailer <token>[(=|:)<value>])...] [<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) +{ + return trailer_set_where(&where, arg); +} + +static int option_parse_if_exists(const struct option *opt, + const char *arg, int unset) +{ + return trailer_set_if_exists(&if_exists, arg); +} + +static int option_parse_if_missing(const struct option *opt, + const char *arg, int unset) +{ + return trailer_set_if_missing(&if_missing, 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; + 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", NULL, N_("action"), + N_("where to place the new trailer"), option_parse_where), + OPT_CALLBACK(0, "if-exists", NULL, N_("action"), + N_("action if trailer already exists"), option_parse_if_exists), + OPT_CALLBACK(0, "if-missing", NULL, 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 config rules")), + OPT_BOOL(0, "unfold", &opts.unfold, N_("join whitespace-continued values")), + { OPTION_CALLBACK, 0, "parse", &opts, NULL, N_("set parsing options"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, parse_opt_parse }, + OPT_CALLBACK(0, "trailer", &trailers, N_("trailer"), + N_("trailer(s) to add"), option_parse_trailer), + OPT_END() + }; + + 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 0000000000..d81a09051e --- /dev/null +++ b/builtin/log.c @@ -0,0 +1,1955 @@ +/* + * Builtin "git log" and related commands (show, whatchanged) + * + * (C) Copyright 2006 Linus Torvalds + * 2006 Junio Hamano + */ +#include "cache.h" +#include "config.h" +#include "refs.h" +#include "color.h" +#include "commit.h" +#include "diff.h" +#include "revision.h" +#include "log-tree.h" +#include "builtin.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" + +/* 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 decoration_style; +static int decoration_given; +static int use_mailmap_config; +static const char *fmt_patch_subject_prefix = "PATCH"; +static const char *fmt_pretty; + +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 auto_decoration_style(void) +{ + return (isatty(1) || pager_in_use()) ? 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(); + return -1; +} + +static int decorate_callback(const struct option *opt, 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; + + 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_grep_defaults(); + 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) + DIFF_OPT_SET(&rev->diffopt, DEFAULT_FOLLOW_RENAMES); + rev->verbose_header = 1; + DIFF_OPT_SET(&rev->diffopt, RECURSIVE); + rev->diffopt.stat_width = -1; /* use full terminal width */ + rev->diffopt.stat_graph_width = -1; /* respect statGraphWidth config */ + rev->abbrev_commit = default_abbrev_commit; + rev->show_root_diff = default_show_root; + rev->subject_prefix = fmt_patch_subject_prefix; + rev->show_signature = default_show_signature; + DIFF_OPT_SET(&rev->diffopt, ALLOW_TEXTCONV); + + if (default_date_mode) + parse_date_format(default_date_mode, &rev->date_mode); + rev->diffopt.touched_flags = 0; +} + +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 = 0; + static struct line_opt_callback_data line_cb = {NULL, NULL, STRING_LIST_INIT_DUP}; + + 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")), + { OPTION_CALLBACK, 0, "decorate", NULL, NULL, N_("decorate options"), + PARSE_OPT_OPTARG, decorate_callback}, + OPT_CALLBACK('L', NULL, &line_cb, "n,m:file", + N_("Process line range n,m in file, counting from 1"), + 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 | + 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]); + + 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) + init_display_notes(&rev->notes_opt); + + if (rev->diffopt.pickaxe || rev->diffopt.filter || + DIFF_OPT_TST(&rev->diffopt, FOLLOW_RENAMES)) + rev->always_show_header = 0; + + if (source) + rev->show_source = 1; + + if (mailmap) { + rev->mailmap = xcalloc(1, sizeof(struct string_list)); + read_mailmap(rev->mailmap, NULL); + } + + 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 (decoration_style) { + rev->show_decorations = 1; + load_ref_decorations(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); +} + +/* + * This gives a rough estimate for how many commits we + * will print out in the list. + */ +static int estimate_commit_count(struct rev_info *rev, 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, close_file = revs->diffopt.close_file; + int show_header = 1; + + revs->diffopt.close_file = 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(revs, list); + show_early_header(revs, "incomplete", n); + show_header = 0; + } + log_tree_commit(revs, commit); + i--; + break; + case commit_ignore: + break; + case commit_error: + if (close_file) + fclose(revs->diffopt.file); + return; + } + list = list->next; + } + + /* Did we already get enough commits for the early output? */ + if (!i) { + if (close_file) + fclose(revs->diffopt.file); + 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) +{ + show_early_output = log_show_early; +} + +static void setup_early_output(struct rev_info *rev) +{ + 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, rev->commits); + signal(SIGALRM, SIG_IGN); + show_early_header(rev, "done", n); +} + +static int cmd_log_walk(struct rev_info *rev) +{ + struct commit *commit; + int saved_nrl = 0; + int saved_dcctc = 0, close_file = rev->diffopt.close_file; + + if (rev->early_output) + setup_early_output(rev); + + 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 + */ + rev->diffopt.close_file = 0; + 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(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 (close_file) + fclose(rev->diffopt.file); + + if (rev->diffopt.output_format & DIFF_FORMAT_CHECKDIFF && + DIFF_OPT_TST(&rev->diffopt, CHECK_FAILED)) { + return 02; + } + return diff_result_code(&rev->diffopt, 0); +} + +static int git_log_config(const char *var, const char *value, 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, "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.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; + } + + if (grep_config(var, value, cb) < 0) + return -1; + if (git_gpg_config(var, value, cb) < 0) + return -1; + return git_diff_ui_config(var, value, 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); + + init_revisions(&rev, prefix); + 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_walk(&rev); +} + +static void show_tagger(char *buf, int len, 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 (!DIFF_OPT_TOUCHED(&rev->diffopt, ALLOW_TEXTCONV) || + !DIFF_OPT_TST(&rev->diffopt, ALLOW_TEXTCONV)) + return stream_blob_to_fd(1, oid, NULL, 0); + + if (get_oid_with_context(obj_name, GET_OID_RECORD_PATH, + &oidc, &obj_context)) + die(_("Not a valid object name %s"), obj_name); + if (!obj_context.path || + !textconv_object(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 = read_sha1_file(oid->hash, &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; + while (new_offset < size && buf[new_offset++] != '\n') + ; /* do nothing */ + if (starts_with(buf + offset, "tagger ")) + show_tagger(buf + offset + 7, + new_offset - offset - 7, 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 unsigned char *sha1, + struct strbuf *base, + const char *pathname, unsigned mode, int stage, 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, + struct setup_revision_opt *opt) +{ + if (rev->ignore_merges) { + /* There was no "-m" on the command line */ + rev->ignore_merges = 0; + if (!rev->first_parent_only && !rev->combine_merges) { + /* No "--first-parent", "-c", or "--cc" */ + rev->combine_merges = 1; + rev->dense_combined_merges = 1; + } + } + 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; + struct object_array_entry *objects; + struct setup_revision_opt opt; + struct pathspec match_all; + int i, count, ret = 0; + + init_log_defaults(); + git_config(git_log_config, NULL); + + memset(&match_all, 0, sizeof(match_all)); + init_revisions(&rev, prefix); + rev.diff = 1; + rev.always_show_header = 1; + rev.no_walk = REVISION_WALK_NO_WALK_SORTED; + 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_walk(&rev); + + count = rev.pending.nr; + objects = rev.pending.objects; + for (i = 0; i < count && !ret; i++) { + struct object *o = objects[i].item; + const char *name = 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; + + 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(&t->tagged->oid); + if (!o) + ret = error(_("Could not read object %s"), + oid_to_hex(&t->tagged->oid)); + 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_recursive((struct tree *)o, "", 0, 0, &match_all, + show_tree_object, rev.diffopt.file); + rev.shown_one = 1; + break; + case OBJ_COMMIT: + rev.pending.nr = rev.pending.alloc = 0; + rev.pending.objects = NULL; + add_object_array(o, name, &rev.pending); + ret = cmd_log_walk(&rev); + break; + default: + ret = error(_("Unknown type: %d"), o->type); + } + } + free(objects); + return ret; +} + +/* + * 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); + + init_revisions(&rev, prefix); + init_reflog_walk(&rev.reflog_info); + 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_walk(&rev); +} + +static void log_setup_revisions_tweak(struct rev_info *rev, + struct setup_revision_opt *opt) +{ + if (DIFF_OPT_TST(&rev->diffopt, DEFAULT_FOLLOW_RENAMES) && + rev->prune_data.nr == 1) + DIFF_OPT_SET(&rev->diffopt, FOLLOW_RENAMES); + + /* Turn --cc/-c into -p --cc/-c when -p was not given */ + if (!rev->diffopt.output_format && rev->combine_merges) + rev->diffopt.output_format = DIFF_FORMAT_PATCH; + + /* Turn -m on when --cc/-c was given */ + if (rev->combine_merges) + rev->ignore_merges = 0; +} + +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); + + init_revisions(&rev, prefix); + 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_walk(&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'; +} + +#define THREAD_SHALLOW 1 +#define THREAD_DEEP 2 +static int thread; +static int do_signoff; +static int base_auto; +static char *from; +static const char *signature = git_version_string; +static const char *signature_file; +static int config_cover_letter; +static const char *config_output_directory; + +enum { + COVER_UNSET, + COVER_OFF, + COVER_ON, + COVER_AUTO +}; + +static int git_format_config(const char *var, const char *value, 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 + 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; + 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")) { + base_auto = git_config_bool(var, value); + 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; + } + + return git_log_config(var, value, 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; + int suffix_len = strlen(rev->patch_suffix) + 1; + + if (output_directory) { + strbuf_addstr(&filename, output_directory); + if (filename.len >= + PATH_MAX - FORMAT_PATCH_NAME_MAX - suffix_len) { + strbuf_release(&filename); + return error(_("name of output directory is too long")); + } + 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")) == NULL) { + 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(&o1->oid); + c2 = lookup_commit_reference(&o2->oid); + + if ((flags1 & UNINTERESTING) == (flags2 & UNINTERESTING)) + die(_("Not a range.")); + + init_patch_ids(ids); + + /* given a range a..b get all patch ids for b..a */ + init_revisions(&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 void add_branch_description(struct strbuf *buf, const char *branch_name) +{ + struct strbuf desc = STRBUF_INIT; + if (!branch_name || !*branch_name) + return; + read_branch_desc(&desc, branch_name); + if (desc.len) { + strbuf_addch(buf, '\n'); + strbuf_addbuf(buf, &desc); + strbuf_addch(buf, '\n'); + } + strbuf_release(&desc); +} + +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 (dwim_ref(ref, strlen(ref), branch_oid.hash, &full_ref) && + skip_prefix(full_ref, "refs/heads/", &v) && + !oidcmp(tip_oid, &branch_oid)) + branch = xstrdup(v); + free(full_ref); + return branch; +} + +static void make_cover_letter(struct rev_info *rev, int use_stdout, + struct commit *origin, + int nr, struct commit **list, + const char *branch_name, + int quiet) +{ + const char *committer; + const char *body = "*** SUBJECT HERE ***\n\n*** BLURB HERE ***\n"; + const char *msg; + struct shortlog log; + struct strbuf sb = STRBUF_INIT; + int i; + const char *encoding = "UTF-8"; + struct diff_options opts; + 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_stdout && + open_next_file(NULL, rev->numbered_files ? NULL : "cover-letter", rev, quiet)) + return; + + log_write_email_headers(rev, head, &pp.after_subject, &need_8bit_cte); + + for (i = 0; !need_8bit_cte && i < nr; i++) { + const char *buf = get_commit_buffer(list[i], NULL); + if (has_non_ascii(buf)) + need_8bit_cte = 1; + unuse_commit_buffer(list[i], buf); + } + + if (!branch_name) + branch_name = find_branch_name(rev); + + msg = body; + 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); + pp_title_line(&pp, &msg, &sb, encoding, need_8bit_cte); + pp_remainder(&pp, &msg, &sb, 0); + add_branch_description(&sb, branch_name); + fprintf(rev->diffopt.file, "%s\n", sb.buf); + + strbuf_release(&sb); + + shortlog_init(&log); + log.wrap_lines = 1; + log.wrap = 72; + log.in1 = 2; + log.in2 = 4; + log.file = rev->diffopt.file; + 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) + return; + + memcpy(&opts, &rev->diffopt, sizeof(opts)); + opts.output_format = DIFF_FORMAT_SUMMARY | DIFF_FORMAT_DIFFSTAT; + + diff_setup_done(&opts); + + diff_tree_oid(&origin->tree->object.oid, + &head->tree->object.oid, + "", &opts); + diffcore_std(&opts); + diff_flush(&opts); + + fprintf(rev->diffopt.file, "\n"); +} + +static const 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 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) +{ + ((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) +{ + subject_prefix = 1; + ((struct rev_info *)opt->value)->subject_prefix = arg; + return 0; +} + +static int rfc_callback(const struct option *opt, const char *arg, int unset) +{ + return subject_prefix_callback(opt, "RFC PATCH", unset); +} + +static int numbered_cmdline_opt = 0; + +static int numbered_callback(const struct option *opt, const char *arg, + int unset) +{ + *(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) +{ + 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; + if (*dir) + die(_("Two output directories?")); + *dir = arg; + return 0; +} + +static int thread_callback(const struct option *opt, const char *arg, int unset) +{ + int *thread = (int *)opt->value; + if (unset) + *thread = 0; + else if (!arg || !strcmp(arg, "shallow")) + *thread = THREAD_SHALLOW; + else if (!strcmp(arg, "deep")) + *thread = THREAD_DEEP; + 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, 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 to_callback(const struct option *opt, const char *arg, int unset) +{ + if (unset) + string_list_clear(&extra_to, 0); + else + string_list_append(&extra_to, arg); + return 0; +} + +static int cc_callback(const struct option *opt, const char *arg, int unset) +{ + if (unset) + string_list_clear(&extra_cc, 0); + else + string_list_append(&extra_cc, 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; +} + +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; + + if (base_commit && strcmp(base_commit, "auto")) { + base = lookup_commit_reference_by_name(base_commit); + if (!base) + die(_("Unknown commit %s"), base_commit); + } else if ((base_commit && !strcmp(base_commit, "auto")) || base_auto) { + 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 (get_oid(upstream, &oid)) + die(_("Failed to resolve '%s' as a valid ref."), upstream); + commit = lookup_commit_or_die(&oid, "upstream base"); + base_list = get_merge_bases_many(commit, total, list); + /* There should be one and only one merge base. */ + if (!base_list || base_list->next) + die(_("Could not find exact merge base.")); + base = base_list->item; + free_commit_list(base_list); + } else { + 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.")); + } + } + + 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 = get_merge_bases(rev[2 * i], rev[2 * i + 1]); + if (!merge_base || merge_base->next) + die(_("Failed to find exact merge base")); + + rev[i] = merge_base->item; + } + + if (rev_nr % 2) + rev[i] = rev[2 * i]; + rev_nr = DIV_ROUND_UP(rev_nr, 2); + } + + if (!in_merge_bases(base, rev[0])) + die(_("base commit should be the ancestor of revision list")); + + for (i = 0; i < total; i++) { + if (base == list[i]) + die(_("base commit shouldn't be in revision list")); + } + + free(rev); + return base; +} + +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; + int i; + + if (!base) + return; + + diff_setup(&diffopt); + DIFF_OPT_SET(&diffopt, RECURSIVE); + diff_setup_done(&diffopt); + + oidcpy(&bases->base_commit, &base->object.oid); + + init_revisions(&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"); + list[i]->util = (void *)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->util) + 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++; + } +} + +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); +} + +int cmd_format_patch(int argc, const char **argv, const char *prefix) +{ + struct commit *commit; + struct commit **list = NULL; + struct rev_info rev; + 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; + int reroll_count = -1; + char *branch_name = NULL; + char *base_commit = NULL; + struct base_tree_info bases; + int show_progress = 0; + struct progress *progress = NULL; + + const struct option builtin_format_patch_options[] = { + { OPTION_CALLBACK, 'n', "numbered", &numbered, NULL, + N_("use [PATCH n/m] even with a single patch"), + PARSE_OPT_NOARG, numbered_callback }, + { OPTION_CALLBACK, 'N', "no-numbered", &numbered, NULL, + N_("use [PATCH] even with multiple patches"), + PARSE_OPT_NOARG, no_numbered_callback }, + OPT_BOOL('s', "signoff", &do_signoff, N_("add Signed-off-by:")), + 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_INTEGER('v', "reroll-count", &reroll_count, + N_("mark the series as Nth re-roll")), + { OPTION_CALLBACK, 0, "rfc", &rev, NULL, + N_("Use [RFC PATCH] instead of [PATCH]"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, rfc_callback }, + { OPTION_CALLBACK, 0, "subject-prefix", &rev, N_("prefix"), + N_("Use [<prefix>] instead of [PATCH]"), + PARSE_OPT_NONEG, subject_prefix_callback }, + { OPTION_CALLBACK, 'o', "output-directory", &output_directory, + N_("dir"), N_("store resulting files in <dir>"), + PARSE_OPT_NONEG, output_directory_callback }, + { OPTION_CALLBACK, '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")), + { OPTION_SET_INT, 'p', "no-stat", &use_patch_format, NULL, + N_("show patch format instead of default (patch + stat)"), + PARSE_OPT_NONEG | PARSE_OPT_NOARG, NULL, 1}, + OPT_GROUP(N_("Messaging")), + { OPTION_CALLBACK, 0, "add-header", NULL, N_("header"), + N_("add email header"), 0, header_callback }, + { OPTION_CALLBACK, 0, "to", NULL, N_("email"), N_("add To: header"), + 0, to_callback }, + { OPTION_CALLBACK, 0, "cc", NULL, N_("email"), N_("add Cc: header"), + 0, cc_callback }, + { OPTION_CALLBACK, 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>")), + { OPTION_CALLBACK, 0, "attach", &rev, N_("boundary"), + N_("attach the patch"), PARSE_OPT_OPTARG, + attach_callback }, + { OPTION_CALLBACK, 0, "inline", &rev, N_("boundary"), + N_("inline the patch"), + PARSE_OPT_OPTARG | PARSE_OPT_NONEG, + inline_callback }, + { OPTION_CALLBACK, 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_STRING(0, "base", &base_commit, N_("base-commit"), + N_("add prerequisite tree info to the patch series")), + 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_END() + }; + + extra_hdr.strdup_strings = 1; + extra_to.strdup_strings = 1; + extra_cc.strdup_strings = 1; + init_log_defaults(); + git_config(git_format_config, NULL); + init_revisions(&rev, prefix); + rev.commit_format = CMIT_FMT_EMAIL; + rev.expand_tabs_in_log_default = 0; + rev.verbose_header = 1; + rev.diff = 1; + rev.max_parents = 1; + DIFF_OPT_SET(&rev.diffopt, RECURSIVE); + rev.subject_prefix = fmt_patch_subject_prefix; + memset(&s_r_opt, 0, sizeof(s_r_opt)); + s_r_opt.def = "HEAD"; + s_r_opt.revarg_opt = REVARG_COMMITTISH; + + 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 | + PARSE_OPT_KEEP_DASHDASH); + + if (0 < reroll_count) { + struct strbuf sprefix = STRBUF_INIT; + strbuf_addf(&sprefix, "%s v%d", + rev.subject_prefix, reroll_count); + rev.reroll_count = reroll_count; + rev.subject_prefix = strbuf_detach(&sprefix, NULL); + } + + 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 = 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 (_("-n and -k are mutually exclusive.")); + if (keep_subject && subject_prefix) + die (_("--subject-prefix/--rfc and -k are mutually exclusive.")); + 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 (!use_patch_format && + (!rev.diffopt.output_format || + rev.diffopt.output_format == DIFF_FORMAT_PATCH)) + rev.diffopt.output_format = DIFF_FORMAT_DIFFSTAT | DIFF_FORMAT_SUMMARY; + + /* Always generate a patch */ + rev.diffopt.output_format |= DIFF_FORMAT_PATCH; + + rev.zero_commit = zero_commit; + + if (!DIFF_OPT_TST(&rev.diffopt, TEXT) && !no_binary_diff) + DIFF_OPT_SET(&rev.diffopt, BINARY); + + if (rev.show_notes) + init_display_notes(&rev.notes_opt); + + if (!output_directory && !use_stdout) + output_directory = config_output_directory; + + if (!use_stdout) + output_directory = set_outdir(prefix, output_directory); + else + setup_pager(); + + if (output_directory) { + if (rev.diffopt.use_color != GIT_COLOR_ALWAYS) + rev.diffopt.use_color = GIT_COLOR_NEVER; + if (use_stdout) + die(_("standard output, or directory, which one?")); + 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 (oidcmp(&o[0].item->oid, &o[1].item->oid) == 0) + return 0; + } + 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 */ + return 0; + 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 (!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)); + if (base_commit || base_auto) { + struct commit *base = get_base_commit(base_commit, list, nr); + reset_revision_walk(); + prepare_bases(&bases, base, list, nr); + } + + if (in_reply_to || thread || cover_letter) + rev.ref_message_ids = xcalloc(1, sizeof(struct string_list)); + if (in_reply_to) { + const char *msgid = clean_message_id(in_reply_to); + string_list_append(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, use_stdout, + origin, nr, list, branch_name, quiet); + print_bases(&bases, rev.diffopt.file); + print_signature(rev.diffopt.file); + total++; + start_number--; + } + 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(rev.ref_message_ids, + rev.message_id); + } + gen_message_id(&rev, oid_to_hex(&commit->object.oid)); + } + + if (!use_stdout && + 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(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 (!use_stdout) + 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 (!use_stdout) + 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); + return 0; +} + +static int add_pending_commit(const char *arg, struct rev_info *revs, int flags) +{ + struct object_id oid; + if (get_oid(arg, &oid) == 0) { + struct commit *commit = lookup_commit_reference(&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, + find_unique_abbrev(commit->object.oid.hash, abbrev)); + } else { + struct strbuf buf = STRBUF_INIT; + pp_commit_easy(CMIT_FMT_ONELINE, commit, &buf); + fprintf(file, "%c %s %s\n", sign, + find_unique_abbrev(commit->object.oid.hash, 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); + } + } + + init_revisions(&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 (oidcmp(&o[0].item->oid, &o[1].item->oid) == 0) + 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 0000000000..8c713c47ac --- /dev/null +++ b/builtin/ls-files.c @@ -0,0 +1,678 @@ +/* + * 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 + */ +#define NO_THE_INDEX_COMPATIBILITY_MACROS +#include "cache.h" +#include "repository.h" +#include "config.h" +#include "quote.h" +#include "dir.h" +#include "builtin.h" +#include "tree.h" +#include "parse-options.h" +#include "resolve-undo.h" +#include "string-list.h" +#include "pathspec.h" +#include "run-command.h" +#include "submodule.h" +#include "submodule-config.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 line_terminator = '\n'; +static int debug_mode; +static int show_eol; +static int recurse_submodules; + +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 *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(const 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(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 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)) { + 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: %d:%d\n", sd->sd_ctime.sec, sd->sd_ctime.nsec); + printf(" mtime: %d:%d\n", sd->sd_mtime.sec, sd->sd_mtime.nsec); + printf(" dev: %d\tino: %d\n", sd->sd_dev, sd->sd_ino); + printf(" uid: %d\tgid: %d\n", sd->sd_uid, sd->sd_gid); + printf(" size: %d\tflags: %x\n", sd->sd_size, ce->ce_flags); + } +} + +static void show_dir_entry(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 (!dir_path_match(ent, &pathspec, len, ps_matched)) + return; + + fputs(tag, stdout); + write_eolinfo(NULL, NULL, ent->name); + write_name(ent->name); +} + +static void show_other_files(const 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(tag_other, ent); + } +} + +static void show_killed_files(const 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) + die("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(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 submodule; + + if (repo_submodule_init(&submodule, superproject, path)) + return; + + if (repo_read_index(&submodule) < 0) + die("index file corrupt"); + + show_files(&submodule, dir); + + repo_clear(&submodule); +} + +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(&pathspec, fullname, strlen(fullname), + max_prefix_len, ps_matched, + S_ISDIR(ce->ce_mode) || + S_ISGITLINK(ce->ce_mode))) { + tag = get_tag(ce, tag); + + if (!show_stage) { + fputs(tag, stdout); + } else { + printf("%s%06o %s %d\t", + tag, + ce->ce_mode, + find_unique_abbrev(ce->oid.hash, abbrev), + ce_stage(ce)); + } + write_eolinfo(repo->index, ce, fullname); + write_name(fullname); + print_debug(ce); + } +} + +static void show_ru_info(const 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(&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], + find_unique_abbrev(ui->sha1[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) { + for (i = 0; i < repo->index->cache_nr; i++) { + const struct cache_entry *ce = repo->index->cache[i]; + + construct_fullname(&fullname, repo, ce); + + if ((dir->flags & DIR_SHOW_IGNORED) && + !ce_excluded(dir, repo->index, fullname.buf, ce)) + continue; + if (show_unmerged && !ce_stage(ce)) + continue; + if (ce->ce_flags & CE_UPDATE) + continue; + show_ce(repo, dir, ce, fullname.buf, + ce_stage(ce) ? tag_unmerged : + (ce_skip_worktree(ce) ? tag_skip_worktree : + tag_cached)); + } + } + if (show_deleted || show_modified) { + for (i = 0; i < repo->index->cache_nr; i++) { + const struct cache_entry *ce = repo->index->cache[i]; + struct stat st; + int 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 (ce_skip_worktree(ce)) + continue; + err = lstat(fullname.buf, &st); + if (show_deleted && err) + show_ce(repo, dir, ce, fullname.buf, tag_removed); + if (show_modified && ie_modified(repo->index, ce, &st, 0)) + show_ce(repo, dir, ce, fullname.buf, tag_modified); + } + } + + 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 = (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; +} + +/* + * Read the tree specified with --with-tree option + * (typically, HEAD) into stage #1 and then + * squash them down to stage #0. This is used for + * --error-unmatch to list and check the path patterns + * that were given from the command line. We are not + * going to write this index out. + */ +void overlay_tree_on_index(struct index_state *istate, + const char *tree_name, const char *prefix) +{ + struct tree *tree; + struct object_id oid; + struct pathspec pathspec; + struct cache_entry *last_stage0 = NULL; + int i; + + if (get_oid(tree_name, &oid)) + die("tree-ish %s not found.", tree_name); + tree = parse_tree_indirect(&oid); + if (!tree) + die("bad tree-ish %s", tree_name); + + /* Hoist the unmerged entries up to stage #3 to make room */ + for (i = 0; i < istate->cache_nr; i++) { + struct cache_entry *ce = istate->cache[i]; + if (!ce_stage(ce)) + continue; + ce->ce_flags |= CE_STAGEMASK; + } + + if (prefix) { + static const char *(matchbuf[1]); + matchbuf[0] = NULL; + parse_pathspec(&pathspec, PATHSPEC_ALL_MAGIC, + PATHSPEC_PREFER_CWD, prefix, matchbuf); + } else + memset(&pathspec, 0, sizeof(pathspec)); + if (read_tree(tree, 1, &pathspec, istate)) + die("unable to read tree entries %s", tree_name); + + for (i = 0; i < istate->cache_nr; i++) { + struct cache_entry *ce = istate->cache[i]; + switch (ce_stage(ce)) { + case 0: + last_stage0 = ce; + /* fallthru */ + default: + continue; + case 1: + /* + * If there is stage #0 entry for this, we do not + * need to show it. We use CE_UPDATE bit to mark + * such an entry. + */ + if (last_stage0 && + !strcmp(last_stage0->name, ce->name)) + ce->ce_flags |= CE_UPDATE; + } + } +} + +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; + + 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; + + exc_given = 1; + add_excludes_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; + + 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; + const char *max_prefix; + struct dir_struct dir; + struct exclude_list *el; + 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_("paths are separated with 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('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")), + { OPTION_CALLBACK, 'x', "exclude", &exclude_list, N_("pattern"), + N_("skip files matching pattern"), + 0, option_parse_exclude }, + { OPTION_CALLBACK, 'X', "exclude-from", &dir, N_("file"), + N_("exclude patterns are read from <file>"), + 0, 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>")), + { OPTION_CALLBACK, 0, "exclude-standard", &dir, NULL, + N_("add the standard git exclusions"), + PARSE_OPT_NOARG, option_parse_exclude_standard }, + { OPTION_SET_INT, 0, "full-name", &prefix_len, NULL, + N_("make the output relative to the project top directory"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL }, + 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_END() + }; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(ls_files_usage, builtin_ls_files_options); + + memset(&dir, 0, sizeof(dir)); + 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); + el = add_exclude_list(&dir, EXC_CMDL, "--exclude option"); + for (i = 0; i < exclude_list.nr; i++) { + add_exclude(exclude_list.items[i].string, "", 0, el, --exclude_args); + } + if (show_tag || show_valid_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 (dir.exclude_per_dir) + exc_given = 1; + + if (require_work_tree && !is_inside_work_tree()) + setup_work_tree(); + + if (recurse_submodules && + (show_stage || 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) && !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("ls-files --with-tree is incompatible with -s or -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) { + int bad; + bad = report_path_error(ps_matched, &pathspec, prefix); + if (bad) + fprintf(stderr, "Did you forget to 'git add'?\n"); + + return bad ? 1 : 0; + } + + UNLEAK(dir); + return 0; +} diff --git a/builtin/ls-remote.c b/builtin/ls-remote.c new file mode 100644 index 0000000000..c4be98ab9e --- /dev/null +++ b/builtin/ls-remote.c @@ -0,0 +1,116 @@ +#include "builtin.h" +#include "cache.h" +#include "transport.h" +#include "remote.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]\n" + " [--symref] [<repository> [<refs>...]]"), + 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 remote *remote; + struct transport *transport; + const struct ref *ref; + + 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_SET_INT(0, "exit-code", &status, + N_("exit with exit code 2 if no matching refs are found"), 2), + OPT_BOOL(0, "symref", &show_symref_target, + N_("show underlying ref in addition to the object pointed by it")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, ls_remote_usage, + PARSE_OPT_STOP_AT_NON_OPTION); + dest = argv[0]; + + if (argc > 1) { + int i; + pattern = xcalloc(argc, sizeof(const char *)); + for (i = 1; i < argc; i++) + pattern[i - 1] = xstrfmt("*/%s", argv[i]); + } + + 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 != NULL) + transport_set_option(transport, TRANS_OPT_UPLOADPACK, uploadpack); + + ref = transport_get_remote_refs(transport); + if (transport_disconnect(transport)) + return 1; + + if (!dest && !quiet) + fprintf(stderr, "From %s\n", *remote->url); + for ( ; ref; ref = ref->next) { + if (!check_ref_type(ref, flags)) + continue; + if (!tail_match(pattern, ref->name)) + continue; + if (show_symref_target && ref->symref) + printf("ref: %s\t%s\n", ref->symref, ref->name); + printf("%s\t%s\n", oid_to_hex(&ref->old_oid), ref->name); + status = 0; /* we found something */ + } + return status; +} diff --git a/builtin/ls-tree.c b/builtin/ls-tree.c new file mode 100644 index 0000000000..ef965408e8 --- /dev/null +++ b/builtin/ls-tree.c @@ -0,0 +1,188 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" +#include "config.h" +#include "blob.h" +#include "tree.h" +#include "commit.h" +#include "quote.h" +#include "builtin.h" +#include "parse-options.h" +#include "pathspec.h" + +static int line_termination = '\n'; +#define LS_RECURSIVE 1 +#define LS_TREE_ONLY 2 +#define LS_SHOW_TREES 4 +#define LS_NAME_ONLY 8 +#define LS_SHOW_SIZE 16 +static int abbrev; +static int ls_options; +static struct pathspec pathspec; +static int chomp_prefix; +static const char *ls_tree_prefix; + +static const char * const ls_tree_usage[] = { + N_("git ls-tree [<options>] <tree-ish> [<path>...]"), + NULL +}; + +static int show_recursive(const char *base, int baselen, const char *pathname) +{ + int i; + + if (ls_options & LS_RECURSIVE) + return 1; + + if (!pathspec.nr) + return 0; + + for (i = 0; i < pathspec.nr; i++) { + const char *spec = pathspec.items[i].match; + int 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(const unsigned char *sha1, struct strbuf *base, + const char *pathname, unsigned mode, int stage, void *context) +{ + int retval = 0; + int baselen; + const char *type = blob_type; + + if (S_ISGITLINK(mode)) { + /* + * Maybe we want to have some recursive version here? + * + * Something similar to this incomplete example: + * + if (show_subprojects(base, baselen, pathname)) + retval = READ_TREE_RECURSIVE; + * + */ + type = commit_type; + } else if (S_ISDIR(mode)) { + if (show_recursive(base->buf, base->len, pathname)) { + retval = READ_TREE_RECURSIVE; + if (!(ls_options & LS_SHOW_TREES)) + return retval; + } + type = tree_type; + } + else if (ls_options & LS_TREE_ONLY) + return 0; + + if (!(ls_options & LS_NAME_ONLY)) { + if (ls_options & LS_SHOW_SIZE) { + char size_text[24]; + if (!strcmp(type, blob_type)) { + unsigned long size; + if (sha1_object_info(sha1, &size) == OBJ_BAD) + xsnprintf(size_text, sizeof(size_text), + "BAD"); + else + xsnprintf(size_text, sizeof(size_text), + "%lu", size); + } else + xsnprintf(size_text, sizeof(size_text), "-"); + printf("%06o %s %s %7s\t", mode, type, + find_unique_abbrev(sha1, abbrev), + size_text); + } else + printf("%06o %s %s\t", mode, type, + find_unique_abbrev(sha1, abbrev)); + } + baselen = base->len; + strbuf_addstr(base, pathname); + write_name_quoted_relative(base->buf, + chomp_prefix ? ls_tree_prefix : NULL, + stdout, line_termination); + strbuf_setlen(base, baselen); + return retval; +} + +int cmd_ls_tree(int argc, const char **argv, const char *prefix) +{ + struct object_id oid; + struct tree *tree; + int i, full_tree = 0; + const struct option ls_tree_options[] = { + OPT_BIT('d', NULL, &ls_options, N_("only show trees"), + LS_TREE_ONLY), + OPT_BIT('r', NULL, &ls_options, N_("recurse into subtrees"), + LS_RECURSIVE), + OPT_BIT('t', NULL, &ls_options, N_("show trees when recursing"), + LS_SHOW_TREES), + OPT_SET_INT('z', NULL, &line_termination, + N_("terminate entries with NUL byte"), 0), + OPT_BIT('l', "long", &ls_options, N_("include object size"), + LS_SHOW_SIZE), + OPT_BIT(0, "name-only", &ls_options, N_("list only filenames"), + LS_NAME_ONLY), + OPT_BIT(0, "name-status", &ls_options, N_("list only filenames"), + LS_NAME_ONLY), + OPT_SET_INT(0, "full-name", &chomp_prefix, + N_("use full path names"), 0), + OPT_BOOL(0, "full-tree", &full_tree, + N_("list entire tree; not just current directory " + "(implies --full-name)")), + OPT__ABBREV(&abbrev), + OPT_END() + }; + + git_config(git_default_config, NULL); + ls_tree_prefix = prefix; + if (prefix && *prefix) + chomp_prefix = strlen(prefix); + + argc = parse_options(argc, argv, prefix, ls_tree_options, + ls_tree_usage, 0); + if (full_tree) { + ls_tree_prefix = prefix = NULL; + chomp_prefix = 0; + } + /* -d -r should imply -t, but -d by itself should not have to. */ + if ( (LS_TREE_ONLY|LS_RECURSIVE) == + ((LS_TREE_ONLY|LS_RECURSIVE) & ls_options)) + ls_options |= LS_SHOW_TREES; + + if (argc < 1) + usage_with_options(ls_tree_usage, ls_tree_options); + if (get_oid(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(&pathspec, PATHSPEC_ALL_MAGIC & + ~(PATHSPEC_FROMTOP | PATHSPEC_LITERAL), + PATHSPEC_PREFER_CWD, + prefix, argv + 1); + for (i = 0; i < pathspec.nr; i++) + pathspec.items[i].nowildcard_len = pathspec.items[i].len; + pathspec.has_wildcard = 0; + tree = parse_tree_indirect(&oid); + if (!tree) + die("not a tree object"); + return !!read_tree_recursive(tree, "", 0, 0, &pathspec, show_tree, NULL); +} diff --git a/builtin/mailinfo.c b/builtin/mailinfo.c new file mode 100644 index 0000000000..cfb667a594 --- /dev/null +++ b/builtin/mailinfo.c @@ -0,0 +1,65 @@ +/* + * Another stupid program, this one parsing the headers of an + * email to figure out authorship and subject + */ +#include "cache.h" +#include "builtin.h" +#include "utf8.h" +#include "strbuf.h" +#include "mailinfo.h" + +static const char mailinfo_usage[] = + "git mailinfo [-k | -b] [-m | --message-id] [-u | --encoding=<encoding> | -n] [--scissors | --no-scissors] <msg> <patch> < mail >info"; + +int cmd_mailinfo(int argc, const char **argv, const char *prefix) +{ + const char *def_charset; + struct mailinfo mi; + int status; + char *msgfile, *patchfile; + + setup_mailinfo(&mi); + + def_charset = get_commit_output_encoding(); + mi.metainfo_charset = def_charset; + + while (1 < argc && argv[1][0] == '-') { + if (!strcmp(argv[1], "-k")) + mi.keep_subject = 1; + else if (!strcmp(argv[1], "-b")) + mi.keep_non_patch_brackets_in_subject = 1; + else if (!strcmp(argv[1], "-m") || !strcmp(argv[1], "--message-id")) + mi.add_message_id = 1; + else if (!strcmp(argv[1], "-u")) + mi.metainfo_charset = def_charset; + else if (!strcmp(argv[1], "-n")) + mi.metainfo_charset = NULL; + else if (starts_with(argv[1], "--encoding=")) + mi.metainfo_charset = argv[1] + 11; + else if (!strcmp(argv[1], "--scissors")) + mi.use_scissors = 1; + else if (!strcmp(argv[1], "--no-scissors")) + mi.use_scissors = 0; + else if (!strcmp(argv[1], "--no-inbody-headers")) + mi.use_inbody_headers = 0; + else + usage(mailinfo_usage); + argc--; argv++; + } + + if (argc != 3) + usage(mailinfo_usage); + + mi.input = stdin; + mi.output = stdout; + + msgfile = prefix_filename(prefix, argv[1]); + patchfile = prefix_filename(prefix, argv[2]); + + 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 0000000000..664400b816 --- /dev/null +++ b/builtin/mailsplit.c @@ -0,0 +1,369 @@ +/* + * 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 "cache.h" +#include "builtin.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 = open(name, O_WRONLY | O_CREAT | O_EXCL, 0666); + if (fd < 0) + die_errno("cannot open output file '%s'", name); + 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)) == NULL) { + 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 (!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 }; + + 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 0000000000..6dbd167d3b --- /dev/null +++ b/builtin/merge-base.c @@ -0,0 +1,263 @@ +#include "builtin.h" +#include "cache.h" +#include "config.h" +#include "commit.h" +#include "refs.h" +#include "diff.h" +#include "revision.h" +#include "parse-options.h" + +static int show_merge_base(struct commit **rev, int rev_nr, int show_all) +{ + struct commit_list *result; + + result = get_merge_bases_many_dirty(rev[0], rev_nr - 1, rev + 1); + + if (!result) + return 1; + + while (result) { + printf("%s\n", oid_to_hex(&result->item->object.oid)); + if (!show_all) + return 0; + result = result->next; + } + + 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 --independent <commit>..."), + N_("git merge-base --is-ancestor <commit> <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 (get_oid(arg, &revkey)) + die("Not a valid object name %s", arg); + r = lookup_commit_reference(&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; + struct commit_list *result; + int i; + + for (i = count - 1; i >= 0; i--) + commit_list_insert(get_commit_reference(args[i]), &revs); + + result = reduce_heads(revs); + if (!result) + return 1; + + while (result) { + printf("%s\n", oid_to_hex(&result->item->object.oid)); + result = result->next; + } + return 0; +} + +static int handle_octopus(int count, const char **args, int show_all) +{ + struct commit_list *revs = NULL; + struct commit_list *result; + int i; + + for (i = count - 1; i >= 0; i--) + commit_list_insert(get_commit_reference(args[i]), &revs); + + result = reduce_heads(get_octopus_merge_bases(revs)); + + if (!result) + return 1; + + while (result) { + printf("%s\n", oid_to_hex(&result->item->object.oid)); + if (!show_all) + return 0; + result = result->next; + } + + 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 (in_merge_bases(one, two)) + return 0; + else + return 1; +} + +struct rev_collect { + struct commit **commit; + int nr; + int alloc; + unsigned int initial : 1; +}; + +static void add_one_commit(struct object_id *oid, struct rev_collect *revs) +{ + struct commit *commit; + + if (is_null_oid(oid)) + return; + + commit = lookup_commit(oid); + if (!commit || + (commit->object.flags & TMP_MARK) || + parse_commit(commit)) + return; + + ALLOC_GROW(revs->commit, revs->nr + 1, revs->alloc); + revs->commit[revs->nr++] = commit; + commit->object.flags |= TMP_MARK; +} + +static int collect_one_reflog_ent(struct object_id *ooid, struct object_id *noid, + const char *ident, timestamp_t timestamp, + int tz, const char *message, void *cbdata) +{ + struct rev_collect *revs = cbdata; + + if (revs->initial) { + revs->initial = 0; + add_one_commit(ooid, revs); + } + add_one_commit(noid, revs); + return 0; +} + +static int handle_fork_point(int argc, const char **argv) +{ + struct object_id oid; + char *refname; + const char *commitname; + struct rev_collect revs; + struct commit *derived; + struct commit_list *bases; + int i, ret = 0; + + switch (dwim_ref(argv[0], strlen(argv[0]), oid.hash, &refname)) { + case 0: + die("No such ref: '%s'", argv[0]); + case 1: + break; /* good */ + default: + die("Ambiguous refname: '%s'", argv[0]); + } + + commitname = (argc == 2) ? argv[1] : "HEAD"; + if (get_oid(commitname, &oid)) + die("Not a valid object name: '%s'", commitname); + + derived = lookup_commit_reference(&oid); + memset(&revs, 0, sizeof(revs)); + revs.initial = 1; + for_each_reflog_ent(refname, collect_one_reflog_ent, &revs); + + if (!revs.nr && !get_oid(refname, &oid)) + add_one_commit(&oid, &revs); + + for (i = 0; i < revs.nr; i++) + revs.commit[i]->object.flags &= ~TMP_MARK; + + bases = get_merge_bases_many_dirty(derived, revs.nr, revs.commit); + + /* + * There should be one and only one merge base, when we found + * a common ancestor among reflog entries. + */ + if (!bases || bases->next) { + ret = 1; + goto cleanup_return; + } + + /* And the found one must be one of the reflog entries */ + for (i = 0; i < revs.nr; i++) + if (&bases->item->object == &revs.commit[i]->object) + break; /* found */ + if (revs.nr <= i) { + ret = 1; /* not found */ + goto cleanup_return; + } + + printf("%s\n", oid_to_hex(&bases->item->object.oid)); + +cleanup_return: + free_commit_list(bases); + return ret; +} + +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; + + 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("--is-ancestor cannot be used with --all"); + return handle_is_ancestor(argc, argv); + } + + if (cmdmode == 'r' && show_all) + die("--independent cannot be used with --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++); + return show_merge_base(rev, rev_nr, show_all); +} diff --git a/builtin/merge-file.c b/builtin/merge-file.c new file mode 100644 index 0000000000..b08803e611 --- /dev/null +++ b/builtin/merge-file.c @@ -0,0 +1,116 @@ +#include "builtin.h" +#include "cache.h" +#include "config.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; + + 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] = { NULL, NULL, NULL }; + mmfile_t mmfs[3]; + mmbuffer_t result = {NULL, 0}; + xmparam_t xmp = {{0}}; + int ret = 0, i = 0, to_stdout = 0; + int quiet = 0; + struct option options[] = { + OPT_BOOL('p', "stdout", &to_stdout, N_("send results to standard output")), + OPT_SET_INT(0, "diff3", &xmp.style, N_("use a diff3 based merge"), XDL_MERGE_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"); + } + + for (i = 0; i < 3; i++) { + char *fname; + int ret; + + if (!names[i]) + names[i] = argv[i]; + + fname = prefix_filename(prefix, argv[i]); + ret = read_mmfile(mmfs + i, fname); + free(fname); + if (ret) + return -1; + + if (mmfs[i].size > MAX_XDIFF_SIZE || + buffer_is_binary(mmfs[i].ptr, mmfs[i].size)) + return error("Cannot merge binary files: %s", + argv[i]); + } + + xmp.ancestor = names[1]; + xmp.file1 = names[0]; + xmp.file2 = names[2]; + ret = xdl_merge(mmfs + 1, mmfs + 0, mmfs + 2, &xmp, &result); + + for (i = 0; i < 3; i++) + free(mmfs[i].ptr); + + if (ret >= 0) { + 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(result.ptr); + free(fpath); + } + + if (ret > 127) + ret = 127; + + return ret; +} diff --git a/builtin/merge-index.c b/builtin/merge-index.c new file mode 100644 index 0000000000..c99443b095 --- /dev/null +++ b/builtin/merge-index.c @@ -0,0 +1,110 @@ +#include "builtin.h" +#include "run-command.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]; + + if (pos >= active_nr) + die("git merge-index: %s not in the cache", path); + found = 0; + do { + const struct cache_entry *ce = active_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 < active_nr); + if (!found) + die("git merge-index: %s not in the cache", path); + + if (run_command_v_opt(arguments, 0)) { + 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 = cache_name_pos(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; + for (i = 0; i < active_nr; i++) { + const struct cache_entry *ce = active_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) +{ + 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>...])"); + + read_cache(); + + 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 0000000000..684411694f --- /dev/null +++ b/builtin/merge-ours.c @@ -0,0 +1,34 @@ +/* + * 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" + +static const char builtin_merge_ours_usage[] = + "git merge-ours <base>... -- HEAD <remote>..."; + +static const char *diff_index_args[] = { + "diff-index", "--quiet", "--cached", "HEAD", "--", NULL +}; +#define NARGS (ARRAY_SIZE(diff_index_args) - 1) + +int cmd_merge_ours(int argc, const char **argv, const char *prefix) +{ + if (argc == 2 && !strcmp(argv[1], "-h")) + usage(builtin_merge_ours_usage); + + /* + * We need to exit with 2 if the index does not match our HEAD tree, + * because the current index is what we will be committing as the + * merge result. + */ + if (cmd_diff_index(NARGS, diff_index_args, prefix)) + exit(2); + exit(0); +} diff --git a/builtin/merge-recursive.c b/builtin/merge-recursive.c new file mode 100644 index 0000000000..0dd9021958 --- /dev/null +++ b/builtin/merge-recursive.c @@ -0,0 +1,83 @@ +#include "builtin.h" +#include "commit.h" +#include "tag.h" +#include "merge-recursive.h" +#include "xdiff-interface.h" + +static const char builtin_merge_recursive_usage[] = + "git %s <base>... -- <head> <remote> ..."; + +static const char *better_branch_name(const char *branch) +{ + static char githead_env[8 + GIT_SHA1_HEXSZ + 1]; + char *name; + + if (strlen(branch) != GIT_SHA1_HEXSZ) + return branch; + xsnprintf(githead_env, sizeof(githead_env), "GITHEAD_%s", branch); + name = getenv(githead_env); + return name ? name : branch; +} + +int cmd_merge_recursive(int argc, const char **argv, const char *prefix) +{ + const struct object_id *bases[21]; + unsigned bases_count = 0; + int i, failed; + struct object_id h1, h2; + struct merge_options o; + struct commit *result; + + init_merge_options(&o); + 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 (get_oid(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.", + (int)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.")); + + o.branch1 = argv[++i]; + o.branch2 = argv[++i]; + + if (get_oid(o.branch1, &h1)) + die(_("could not resolve ref '%s'"), o.branch1); + if (get_oid(o.branch2, &h2)) + die(_("could not resolve ref '%s'"), o.branch2); + + o.branch1 = better_branch_name(o.branch1); + o.branch2 = 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); + 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 0000000000..d01ddecf66 --- /dev/null +++ b/builtin/merge-tree.c @@ -0,0 +1,379 @@ +#include "builtin.h" +#include "tree-walk.h" +#include "xdiff-interface.h" +#include "blob.h" +#include "exec_cmd.h" +#include "merge-blobs.h" + +static const char merge_tree_usage[] = "git merge-tree <base-tree> <branch1> <branch2>"; + +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 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 read_sha1_file(entry->blob->object.oid.hash, &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(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 read_sha1_file(entry->blob->object.oid.hash, &type, size); + entry = entry->link; + } + return NULL; +} + +static int show_outf(void *priv_, 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; + + xpp.flags = 0; + memset(&xecfg, 0, sizeof(xecfg)); + xecfg.ctxlen = 3; + ecb.outf = show_outf; + ecb.priv = NULL; + + 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 a->oid && + b->oid && + !oidcmp(a->oid, b->oid) && + a->mode == b->mode; +} + +static int both_empty(struct name_entry *a, struct name_entry *b) +{ + return !(a->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(oid); + return res; +} + +static char *traverse_path(const struct traverse_info *info, const struct name_entry *n) +{ + char *path = xmallocz(traverse_path_len(info, n)); + return make_traverse_path(path, info, n); +} + +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]) +{ + 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(t + 0, ENTRY_OID(n + 0)); + buf1 = fill_tree_descriptor(t + 1, ENTRY_OID(n + 1)); + buf2 = fill_tree_descriptor(t + 2, ENTRY_OID(n + 2)); +#undef ENTRY_OID + + 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, unsigned long mask, unsigned long dirmask, 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 (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 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(3, t, &info); +} + +static void *get_tree_descriptor(struct tree_desc *desc, const char *rev) +{ + struct object_id oid; + void *buf; + + if (get_oid(rev, &oid)) + die("unknown rev %s", rev); + buf = fill_tree_descriptor(desc, &oid); + if (!buf) + die("%s is not a tree", rev); + return buf; +} + +int cmd_merge_tree(int argc, const char **argv, const char *prefix) +{ + struct tree_desc t[3]; + void *buf1, *buf2, *buf3; + + if (argc != 4) + usage(merge_tree_usage); + + buf1 = get_tree_descriptor(t+0, argv[1]); + buf2 = get_tree_descriptor(t+1, argv[2]); + buf3 = get_tree_descriptor(t+2, argv[3]); + merge_trees(t, ""); + free(buf1); + free(buf2); + free(buf3); + + show_result(); + return 0; +} diff --git a/builtin/merge.c b/builtin/merge.c new file mode 100644 index 0000000000..ab5ffe85e8 --- /dev/null +++ b/builtin/merge.c @@ -0,0 +1,1588 @@ +/* + * Builtin "git merge" + * + * Copyright (c) 2008 Miklos Vajna <vmiklos@frugalware.org> + * + * Based on git-merge.sh by Junio C Hamano. + */ + +#include "cache.h" +#include "config.h" +#include "parse-options.h" +#include "builtin.h" +#include "lockfile.h" +#include "run-command.h" +#include "diff.h" +#include "refs.h" +#include "commit.h" +#include "diffcore.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-recursive.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" + +#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>...]"), + N_("git merge --abort"), + N_("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 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 const char **xopts; +static size_t xopts_nr, xopts_alloc; +static const char *branch; +static char *branch_mergeoptions; +static int option_renormalize; +static int verbosity; +static int allow_rerere_auto; +static int abort_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 verify_msg = 1; + +static struct strategy all_strategy[] = { + { "recursive", DEFAULT_TWOHEAD | NO_TRIVIAL }, + { "octopus", DEFAULT_OCTOPUS }, + { "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 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 struct strategy *get_strategy(const char *name) +{ + int i; + struct strategy *ret; + static struct cmdnames main_cmds, other_cmds; + static int loaded; + + if (!name) + return NULL; + + 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; 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); + } + + ret = xcalloc(1, sizeof(struct strategy)); + 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, + const char *name, int unset) +{ + if (unset) + return 0; + + append_strategy(get_strategy(name)); + return 0; +} + +static int option_parse_x(const struct option *opt, + const char *arg, int unset) +{ + if (unset) + return 0; + + ALLOC_GROW(xopts, xopts_nr + 1, xopts_alloc); + xopts[xopts_nr++] = xstrdup(arg); + return 0; +} + +static int option_parse_n(const struct option *opt, + const char *arg, int unset) +{ + show_diffstat = unset; + return 0; +} + +static struct option builtin_merge_options[] = { + { OPTION_CALLBACK, 'n', NULL, NULL, NULL, + N_("do not show a diffstat at the end of the merge"), + PARSE_OPT_NOARG, option_parse_n }, + 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_SET_INT(0, "ff", &fast_forward, N_("allow fast-forward (default)"), FF_ALLOW), + { OPTION_SET_INT, 0, "ff-only", &fast_forward, NULL, + N_("abort if fast-forward is not possible"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, FF_ONLY }, + 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", &use_strategies, N_("strategy"), + N_("merge strategy to use"), option_parse_strategy), + OPT_CALLBACK('X', "strategy-option", &xopts, N_("option=value"), + N_("option for selected merge strategy"), option_parse_x), + OPT_CALLBACK('m', "message", &merge_msg, N_("message"), + N_("merge commit message (for a non-fast-forward merge)"), + option_parse_message), + OPT__VERBOSITY(&verbosity), + OPT_BOOL(0, "abort", &abort_current_merge, + N_("abort the current in-progress merge")), + 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_BOOL(0, "overwrite-ignore", &overwrite_ignore, N_("update ignored files (default)")), + OPT_BOOL(0, "signoff", &signoff, N_("add Signed-off-by:")), + OPT_BOOL(0, "verify", &verify_msg, N_("verify commit-msg hook")), + OPT_END() +}; + +/* Cleans up metadata that is uninteresting after a succeeded merge. */ +static void drop_save(void) +{ + unlink(git_path_merge_head()); + unlink(git_path_merge_msg()); + unlink(git_path_merge_mode()); +} + +static int save_state(struct object_id *stash) +{ + int len; + struct child_process cp = CHILD_PROCESS_INIT; + struct strbuf buffer = STRBUF_INIT; + const char *argv[] = {"stash", "create", NULL}; + int rc = -1; + + cp.argv = argv; + 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 (get_oid(buffer.buf, stash)) + die(_("not a valid object: %s"), buffer.buf); + rc = 0; +out: + strbuf_release(&buffer); + return rc; +} + +static void read_empty(unsigned const char *sha1, int verbose) +{ + int i = 0; + const char *args[7]; + + args[i++] = "read-tree"; + if (verbose) + args[i++] = "-v"; + args[i++] = "-m"; + args[i++] = "-u"; + args[i++] = EMPTY_TREE_SHA1_HEX; + args[i++] = sha1_to_hex(sha1); + args[i] = NULL; + + if (run_command_v_opt(args, RUN_GIT_CMD)) + die(_("read-tree failed")); +} + +static void reset_hard(unsigned const char *sha1, int verbose) +{ + int i = 0; + const char *args[6]; + + args[i++] = "read-tree"; + if (verbose) + args[i++] = "-v"; + args[i++] = "--reset"; + args[i++] = "-u"; + args[i++] = sha1_to_hex(sha1); + args[i] = NULL; + + if (run_command_v_opt(args, RUN_GIT_CMD)) + die(_("read-tree failed")); +} + +static void restore_state(const struct object_id *head, + const struct object_id *stash) +{ + struct strbuf sb = STRBUF_INIT; + const char *args[] = { "stash", "apply", NULL, NULL }; + + if (is_null_oid(stash)) + return; + + reset_hard(head->hash, 1); + + args[2] = oid_to_hex(stash); + + /* + * It is OK to ignore error here, for example when there was + * nothing to restore. + */ + run_command_v_opt(args, RUN_GIT_CMD); + + strbuf_release(&sb); + refresh_cache(REFRESH_QUIET); +} + +/* This is called when no merge was necessary. */ +static void finish_up_to_date(const char *msg) +{ + if (verbosity >= 0) + printf("%s%s\n", squash ? _(" (nothing to squash)") : "", msg); + drop_save(); +} + +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")); + + init_revisions(&rev, NULL); + rev.ignore_merges = 1; + 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(), out.buf, out.len); + strbuf_release(&out); +} + +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 { + const char *argv_gc_auto[] = { "gc", "--auto", NULL }; + update_ref(reflog_message.buf, "HEAD", + new_head->hash, head->hash, 0, + UPDATE_REFS_DIE_ON_ERR); + /* + * We ignore errors in 'gc --auto', since the + * user should see them. + */ + close_all_packs(); + run_command_v_opt(argv_gc_auto, RUN_GIT_CMD); + } + } + if (new_head && show_diffstat) { + struct diff_options opts; + diff_setup(&opts); + opts.stat_width = -1; /* use full terminal width */ + opts.stat_graph_width = -1; /* respect statGraphWidth config */ + 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_hook_le(NULL, "post-merge", squash ? "1" : "0", NULL); + + 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 buf = STRBUF_INIT; + struct strbuf bname = STRBUF_INIT; + const char *ptr; + char *found_ref; + 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 (dwim_ref(remote, strlen(remote), branch_head.hash, &found_ref) > 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); + } + + if (remote_head->util) { + struct merge_remote_desc *desc; + 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), + typename(desc->obj->type), + remote); + goto cleanup; + } + } + + strbuf_addf(msg, "%s\t\tcommit '%s'\n", + oid_to_hex(&remote_head->object.oid), remote); +cleanup: + strbuf_release(&buf); + 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, void *cb) +{ + int status; + + if (branch && starts_with(k, "branch.") && + starts_with(k + 7, branch) && + !strcmp(k + 7 + strlen(branch), ".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, "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, "merge.renormalize")) + option_renormalize = git_config_bool(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; + } + + status = fmt_merge_msg_config(k, v, cb); + if (status) + return status; + status = git_gpg_config(k, v, NULL); + if (status) + return status; + return git_diff_ui_config(k, v, 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; + 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(&active_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_cache_as_tree(oid->hash, 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) +{ + static struct lock_file lock; + const char *head_arg = "HEAD"; + + hold_locked_index(&lock, LOCK_DIE_ON_ERROR); + refresh_cache(REFRESH_QUIET); + if (active_cache_changed && + write_locked_index(&the_index, &lock, COMMIT_LOCK)) + return error(_("Unable to write index.")); + rollback_lock_file(&lock); + + if (!strcmp(strategy, "recursive") || !strcmp(strategy, "subtree")) { + 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); + if (!strcmp(strategy, "subtree")) + o.subtree_shift = ""; + + o.renormalize = option_renormalize; + o.show_rename_progress = + show_progress == -1 ? isatty(2) : show_progress; + + for (x = 0; x < xopts_nr; x++) + if (parse_merge_opt(&o, xopts[x])) + die(_("Unknown option for merge-recursive: -X%s"), xopts[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); + + hold_locked_index(&lock, LOCK_DIE_ON_ERROR); + clean = merge_recursive(&o, head, + remoteheads->item, reversed, &result); + if (clean < 0) + exit(128); + if (active_cache_changed && + write_locked_index(&the_index, &lock, COMMIT_LOCK)) + die (_("unable to write %s"), get_index_file()); + rollback_lock_file(&lock); + return clean ? 0 : 1; + } else { + return try_merge_command(strategy, xopts_nr, xopts, + common, head_arg, remoteheads); + } +} + +static void count_diff_files(struct diff_queue_struct *q, + struct diff_options *opt, void *data) +{ + int *count = data; + + (*count) += q->nr; +} + +static int count_unmerged_entries(void) +{ + int i, ret = 0; + + for (i = 0; i < active_nr; i++) + if (ce_stage(active_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(); + 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" + "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; + strbuf_addbuf(&msg, &merge_msg); + strbuf_addch(&msg, '\n'); + if (squash) + BUG("the control must not reach here under --squash"); + if (0 < option_edit) + strbuf_commented_addf(&msg, _(merge_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(), msg.buf, msg.len); + if (run_commit_hook(0 < option_edit, get_index_file(), "prepare-commit-msg", + git_path_merge_msg(), "merge", NULL)) + abort_commit(remoteheads, NULL); + if (0 < option_edit) { + if (launch_editor(git_path_merge_msg(), NULL, NULL)) + abort_commit(remoteheads, NULL); + } + + if (verify_msg && run_commit_hook(0 < option_edit, get_index_file(), + "commit-msg", + git_path_merge_msg(), NULL)) + abort_commit(remoteheads, NULL); + + read_merge_msg(&msg); + strbuf_stripspace(&msg, 0 < option_edit); + 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; + static struct lock_file lock; + + hold_locked_index(&lock, LOCK_DIE_ON_ERROR); + refresh_cache(REFRESH_QUIET); + if (active_cache_changed && + write_locked_index(&the_index, &lock, COMMIT_LOCK)) + return error(_("Unable to write index.")); + rollback_lock_file(&lock); + + 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.hash, parents, + result_commit.hash, NULL, sign_commit)) + die(_("failed to write commit object")); + finish(head, remoteheads, &result_commit, "In-index merge"); + drop_save(); + 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; + + free_commit_list(common); + parents = remoteheads; + if (!head_subsumed || fast_forward == FF_NO) + commit_list_insert(head, &parents); + strbuf_addch(&merge_msg, '\n'); + prepare_to_commit(remoteheads); + if (commit_tree(merge_msg.buf, merge_msg.len, result_tree->hash, parents, + result_commit.hash, 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); + drop_save(); + return 0; +} + +static int suggest_conflicts(void) +{ + const char *filename; + FILE *fp; + struct strbuf msgbuf = STRBUF_INIT; + + filename = git_path_merge_msg(); + fp = xfopen(filename, "a"); + + append_conflicts_hint(&msgbuf); + fputs(msgbuf.buf, fp); + strbuf_release(&msgbuf); + fclose(fp); + rerere(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. */ + init_revisions(&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(); + + 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; + if (c->util && merge_remote_util(c)->obj) { + oid = &merge_remote_util(c)->obj->oid; + } else { + oid = &c->object.oid; + } + strbuf_addf(&buf, "%s\n", oid_to_hex(oid)); + } + write_file_buf(git_path_merge_head(), buf.buf, buf.len); + + strbuf_reset(&buf); + if (fast_forward == FF_NO) + strbuf_addstr(&buf, "no-ff"); + write_file_buf(git_path_merge_mode(), 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(), 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); + + 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); + + 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; + + if (!merge_names) + merge_names = &fetch_head_file; + + filename = git_path_fetch_head(); + fd = open(filename, O_RDONLY); + if (fd < 0) + die_errno(_("could not open '%s' for reading"), filename); + + 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 < GIT_SHA1_HEXSZ + 2 || + get_oid_hex(merge_names->buf + pos, &oid)) + commit = NULL; /* bad */ + else if (memcmp(merge_names->buf + pos + GIT_SHA1_HEXSZ, "\t\t", 2)) + continue; /* not-for-merge */ + else { + char saved = merge_names->buf[pos + GIT_SHA1_HEXSZ]; + merge_names->buf[pos + GIT_SHA1_HEXSZ] = '\0'; + commit = get_merge_parent(merge_names->buf + pos); + merge_names->buf[pos + GIT_SHA1_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; +} + +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, *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); + + /* + * 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.hash, NULL); + if (branch) + skip_prefix(branch, "refs/heads/", &branch); + if (!branch || is_null_oid(&head_oid)) + head_commit = NULL; + else + head_commit = lookup_commit_or_die(&head_oid, "HEAD"); + + init_diff_ui_defaults(); + git_config(git_merge_config, NULL); + + 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}; + + if (orig_argc != 2) + usage_msg_opt(_("--abort expects no arguments"), + builtin_merge_usage, builtin_merge_options); + + if (!file_exists(git_path_merge_head())) + die(_("There is no merge to abort (MERGE_HEAD missing).")); + + /* Invoke 'git reset --merge' */ + ret = cmd_reset(nargc, nargv, prefix); + 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())) + die(_("There is no merge in progress (MERGE_HEAD missing).")); + + /* Invoke 'git commit' */ + ret = cmd_commit(nargc, nargv, prefix); + goto done; + } + + if (read_cache_unmerged()) + die_resolve_conflict("merge"); + + if (file_exists(git_path_merge_head())) { + /* + * There is no unmerged entry, don't advise 'git + * add/rm <file>', just 'git commit'. + */ + if (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 (file_exists(git_path_cherry_pick_head())) { + if (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(); + + if (verbosity < 0) + show_diffstat = 0; + + if (squash) { + if (fast_forward == FF_NO) + die(_("You cannot combine --squash with --no-ff.")); + option_commit = 0; + } + + 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")); + remote_head_oid = &remoteheads->item->object.oid; + read_empty(remote_head_oid->hash, 0); + update_ref("initial pull", "HEAD", remote_head_oid->hash, + 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) { + struct commit *commit = p->item; + char hex[GIT_MAX_HEXSZ + 1]; + struct signature_check signature_check; + memset(&signature_check, 0, sizeof(signature_check)); + + check_commit_signature(commit, &signature_check); + + find_unique_abbrev_r(hex, commit->object.oid.hash, DEFAULT_ABBREV); + switch (signature_check.result) { + case 'G': + break; + case 'U': + die(_("Commit %s has an untrusted GPG signature, " + "allegedly by %s."), hex, signature_check.signer); + case 'B': + die(_("Commit %s has a bad GPG signature " + "allegedly by %s."), hex, signature_check.signer); + default: /* 'N' */ + die(_("Commit %s does not have a GPG signature."), hex); + } + if (verbosity >= 0 && signature_check.result == 'G') + printf(_("Commit %s has a good GPG signature by %s\n"), + hex, signature_check.signer); + + signature_check_clear(&signature_check); + } + } + + 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 && + merge_remote_util(commit) && + merge_remote_util(commit)->obj && + merge_remote_util(commit)->obj->type == OBJ_TAG) + fast_forward = FF_NO; + } + + if (option_edit < 0) + option_edit = default_edit_option(); + + 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 = get_merge_bases(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.hash, + 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(_("Already up to date.")); + goto done; + } else if (fast_forward != FF_NO && !remoteheads->next && + !common->next && + !oidcmp(&common->item->object.oid, &head_commit->object.oid)) { + /* Again the most common case of merging one remote. */ + struct strbuf msg = STRBUF_INIT; + struct commit *commit; + + if (verbosity >= 0) { + printf(_("Updating %s..%s\n"), + find_unique_abbrev(head_commit->object.oid.hash, + DEFAULT_ABBREV), + find_unique_abbrev(remoteheads->item->object.oid.hash, + DEFAULT_ABBREV)); + } + strbuf_addstr(&msg, "Fast-forward"); + if (have_message) + strbuf_addstr(&msg, + " (no commit created; -m option ignored)"); + commit = remoteheads->item; + if (!commit) { + ret = 1; + goto done; + } + + if (checkout_fast_forward(&head_commit->object.oid, + &commit->object.oid, + overwrite_ignore)) { + ret = 1; + goto done; + } + + finish(head_commit, remoteheads, &commit->object.oid, msg.buf); + drop_save(); + 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_cache(REFRESH_QUIET); + if (allow_trivial && fast_forward != FF_ONLY) { + /* 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; + + /* + * Here we *have* to calculate the individual + * merge_bases again, otherwise "git merge HEAD^ + * HEAD^^" would be missed. + */ + common_one = get_merge_bases(head_commit, j->item); + if (oidcmp(&common_one->item->object.oid, &j->item->object.oid)) { + up_to_date = 0; + break; + } + } + if (up_to_date) { + finish_up_to_date(_("Already up to date. Yeeah!")); + goto done; + } + } + + if (fast_forward == FF_ONLY) + die(_("Not possible to fast-forward, aborting.")); + + /* 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. + */ + if (use_strategies_nr == 1 || + /* + * Stash away the local changes so that we can try more than one. + */ + save_state(&stash)) + oidclr(&stash); + + for (i = 0; i < use_strategies_nr; i++) { + int ret; + 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(use_strategies[i]->name, + common, remoteheads, + head_commit); + if (!option_commit && !ret) { + merge_was_ok = 1; + /* + * This is necessary here just to avoid writing + * the tree, but later we will *not* exit with + * status code 1 because merge_was_ok is set. + */ + ret = 1; + } + + if (ret) { + /* + * 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 == 1) { + int cnt = evaluate_result(); + + if (best_cnt <= 0 || cnt <= best_cnt) { + best_strategy = use_strategies[i]->name; + best_cnt = cnt; + } + } + if (merge_was_ok) + break; + else + continue; + } + + /* Automerge succeeded. */ + write_tree_trivial(&result_tree); + automerge_was_ok = 1; + break; + } + + /* + * If we have a resulting tree, that means the strategy module + * auto resolved the merge cleanly. + */ + if (automerge_was_ok) { + 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); + 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 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); + 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(); + +done: + free(branch_to_free); + return ret; +} diff --git a/builtin/mktag.c b/builtin/mktag.c new file mode 100644 index 0000000000..031b750f06 --- /dev/null +++ b/builtin/mktag.c @@ -0,0 +1,174 @@ +#include "builtin.h" +#include "tag.h" + +/* + * A signature file has a very simple fixed format: four lines + * of "object <sha1>" + "type <typename>" + "tag <tagname>" + + * "tagger <committer>", followed by a blank line, a free-form tag + * message and a signature block that git itself doesn't care about, + * but that can be verified with gpg or similar. + * + * The first four lines are guaranteed to be at least 83 bytes: + * "object <sha1>\n" is 48 bytes, "type tag\n" at 9 bytes is the + * shortest possible type-line, "tag .\n" at 6 bytes is the shortest + * single-character-tag line, and "tagger . <> 0 +0000\n" at 20 bytes is + * the shortest possible tagger-line. + */ + +/* + * We refuse to tag something we can't verify. Just because. + */ +static int verify_object(const unsigned char *sha1, const char *expected_type) +{ + int ret = -1; + enum object_type type; + unsigned long size; + void *buffer = read_sha1_file(sha1, &type, &size); + const unsigned char *repl = lookup_replace_object(sha1); + + if (buffer) { + if (type == type_from_string(expected_type)) + ret = check_sha1_signature(repl, buffer, size, expected_type); + free(buffer); + } + return ret; +} + +static int verify_tag(char *buffer, unsigned long size) +{ + int typelen; + char type[20]; + unsigned char sha1[20]; + const char *object, *type_line, *tag_line, *tagger_line, *lb, *rb; + size_t len; + + if (size < 84) + return error("wanna fool me ? you obviously got the size wrong !"); + + buffer[size] = 0; + + /* Verify object line */ + object = buffer; + if (memcmp(object, "object ", 7)) + return error("char%d: does not start with \"object \"", 0); + + if (get_sha1_hex(object + 7, sha1)) + return error("char%d: could not get SHA1 hash", 7); + + /* Verify type line */ + type_line = object + 48; + if (memcmp(type_line - 1, "\ntype ", 6)) + return error("char%d: could not find \"\\ntype \"", 47); + + /* Verify tag-line */ + tag_line = strchr(type_line, '\n'); + if (!tag_line) + return error("char%"PRIuMAX": could not find next \"\\n\"", + (uintmax_t) (type_line - buffer)); + tag_line++; + if (memcmp(tag_line, "tag ", 4) || tag_line[4] == '\n') + return error("char%"PRIuMAX": no \"tag \" found", + (uintmax_t) (tag_line - buffer)); + + /* Get the actual type */ + typelen = tag_line - type_line - strlen("type \n"); + if (typelen >= sizeof(type)) + return error("char%"PRIuMAX": type too long", + (uintmax_t) (type_line+5 - buffer)); + + memcpy(type, type_line+5, typelen); + type[typelen] = 0; + + /* Verify that the object matches */ + if (verify_object(sha1, type)) + return error("char%d: could not verify object %s", 7, sha1_to_hex(sha1)); + + /* Verify the tag-name: we don't allow control characters or spaces in it */ + tag_line += 4; + for (;;) { + unsigned char c = *tag_line++; + if (c == '\n') + break; + if (c > ' ') + continue; + return error("char%"PRIuMAX": could not verify tag name", + (uintmax_t) (tag_line - buffer)); + } + + /* Verify the tagger line */ + tagger_line = tag_line; + + if (memcmp(tagger_line, "tagger ", 7)) + return error("char%"PRIuMAX": could not find \"tagger \"", + (uintmax_t) (tagger_line - buffer)); + + /* + * Check for correct form for name and email + * i.e. " <" followed by "> " on _this_ line + * No angle brackets within the name or email address fields. + * No spaces within the email address field. + */ + tagger_line += 7; + if (!(lb = strstr(tagger_line, " <")) || !(rb = strstr(lb+2, "> ")) || + strpbrk(tagger_line, "<>\n") != lb+1 || + strpbrk(lb+2, "><\n ") != rb) + return error("char%"PRIuMAX": malformed tagger field", + (uintmax_t) (tagger_line - buffer)); + + /* Check for author name, at least one character, space is acceptable */ + if (lb == tagger_line) + return error("char%"PRIuMAX": missing tagger name", + (uintmax_t) (tagger_line - buffer)); + + /* timestamp, 1 or more digits followed by space */ + tagger_line = rb + 2; + if (!(len = strspn(tagger_line, "0123456789"))) + return error("char%"PRIuMAX": missing tag timestamp", + (uintmax_t) (tagger_line - buffer)); + tagger_line += len; + if (*tagger_line != ' ') + return error("char%"PRIuMAX": malformed tag timestamp", + (uintmax_t) (tagger_line - buffer)); + tagger_line++; + + /* timezone, 5 digits [+-]hhmm, max. 1400 */ + if (!((tagger_line[0] == '+' || tagger_line[0] == '-') && + strspn(tagger_line+1, "0123456789") == 4 && + tagger_line[5] == '\n' && atoi(tagger_line+1) <= 1400)) + return error("char%"PRIuMAX": malformed tag timezone", + (uintmax_t) (tagger_line - buffer)); + tagger_line += 6; + + /* Verify the blank line separating the header from the body */ + if (*tagger_line != '\n') + return error("char%"PRIuMAX": trailing garbage in tag header", + (uintmax_t) (tagger_line - buffer)); + + /* The actual stuff afterwards we don't care about.. */ + return 0; +} + +int cmd_mktag(int argc, const char **argv, const char *prefix) +{ + struct strbuf buf = STRBUF_INIT; + unsigned char result_sha1[20]; + + if (argc != 1) + usage("git mktag"); + + if (strbuf_read(&buf, 0, 4096) < 0) { + die_errno("could not read from stdin"); + } + + /* Verify it for some basic sanity: it needs to start with + "object <sha1>\ntype\ntagger " */ + if (verify_tag(buf.buf, buf.len) < 0) + die("invalid tag signature file"); + + if (write_sha1_file(buf.buf, buf.len, tag_type, result_sha1) < 0) + die("unable to write tag file"); + + strbuf_release(&buf); + printf("%s\n", sha1_to_hex(result_sha1)); + return 0; +} diff --git a/builtin/mktree.c b/builtin/mktree.c new file mode 100644 index 0000000000..da0fd8cd70 --- /dev/null +++ b/builtin/mktree.c @@ -0,0 +1,192 @@ +/* + * GIT - the stupid content tracker + * + * Copyright (c) Junio C Hamano, 2006, 2009 + */ +#include "builtin.h" +#include "quote.h" +#include "tree.h" +#include "parse-options.h" + +static struct treeent { + unsigned mode; + unsigned char sha1[20]; + int len; + char name[FLEX_ARRAY]; +} **entries; +static int alloc, used; + +static void append_to_tree(unsigned mode, unsigned char *sha1, 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; + hashcpy(ent->sha1, sha1); + + 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(unsigned char *sha1) +{ + 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->sha1, 20); + } + + write_sha1_file(buf.buf, buf.len, tree_type, sha1); + strbuf_release(&buf); +} + +static const char *mktree_usage[] = { + N_("git mktree [-z] [--missing] [--batch]"), + NULL +}; + +static void mktree_line(char *buf, size_t len, int nul_term_line, int allow_missing) +{ + char *ptr, *ntr; + unsigned mode; + enum object_type mode_type; /* object type derived from mode */ + enum object_type obj_type; /* object type derived from sha */ + char *path, *to_free = NULL; + unsigned char sha1[20]; + + 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 || buf + len <= ntr + 40 || + ntr[41] != '\t' || + get_sha1_hex(ntr + 1, sha1)) + 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 = ntr + 41; /* 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, typename(mode_type)); + } + + /* Check the type of object identified by sha1 */ + obj_type = sha1_object_info(sha1, NULL); + 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, sha1_to_hex(sha1)); + } + } 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, sha1_to_hex(sha1), typename(obj_type), typename(mode_type)); + } + } + + append_to_tree(mode, sha1, path); + free(to_free); +} + +int cmd_mktree(int ac, const char **av, const char *prefix) +{ + struct strbuf sb = STRBUF_INIT; + unsigned char sha1[20]; + 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, sb.len, 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(sha1); + puts(sha1_to_hex(sha1)); + fflush(stdout); + } + used=0; /* reset tree entry buffer for re-use in batch mode */ + } + strbuf_release(&sb); + exit(0); +} diff --git a/builtin/mv.c b/builtin/mv.c new file mode 100644 index 0000000000..ffdd5f01a1 --- /dev/null +++ b/builtin/mv.c @@ -0,0 +1,301 @@ +/* + * "git mv" builtin command + * + * Copyright (C) 2006 Johannes Schindelin + */ +#include "builtin.h" +#include "config.h" +#include "pathspec.h" +#include "lockfile.h" +#include "dir.h" +#include "cache-tree.h" +#include "string-list.h" +#include "parse-options.h" +#include "submodule.h" + +static const char * const builtin_mv_usage[] = { + N_("git mv [<options>] <source>... <destination>"), + NULL +}; + +#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 (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; +} + +static struct lock_file lock_file; +#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(active_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 = cache_name_pos(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 < active_nr; last++) { + const char *path = active_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; +} + +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; + 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")), + OPT_BOOL('k', NULL, &ignore_errors, N_("skip move/rename errors")), + OPT_END(), + }; + const char **source, **destination, **dest_path, **submodule_gitfile; + enum update_mode { BOTH = 0, WORKING_DIRECTORY, INDEX } *modes; + struct stat st; + struct string_list src_for_dst = 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); + + hold_locked_index(&lock_file, LOCK_DIE_ON_ERROR); + if (read_cache() < 0) + die(_("index file corrupt")); + + source = internal_prefix_pathspec(prefix, argv, argc, 0); + modes = xcalloc(argc, sizeof(enum update_mode)); + /* + * 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); + 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)) { + dest_path[0] = add_slash(dest_path[0]); + destination = internal_prefix_pathspec(dest_path[0], argv, argc, DUP_BASENAME); + } else { + if (argc != 1) + die(_("destination '%s' is not a directory"), dest_path[0]); + destination = dest_path; + } + + /* Checking */ + for (i = 0; i < argc; i++) { + const char *src = source[i], *dst = destination[i]; + int length, src_is_dir; + const char *bad = NULL; + + if (show_only) + printf(_("Checking rename of '%s' to '%s'\n"), src, dst); + + length = strlen(src); + if (lstat(src, &st) < 0) + bad = _("bad source"); + else if (!strncmp(src, dst, length) && + (dst[length] == 0 || dst[length] == '/')) { + bad = _("can not move directory into itself"); + } else if ((src_is_dir = S_ISDIR(st.st_mode)) + && lstat(dst, &st) == 0) + bad = _("cannot move directory over file"); + else if (src_is_dir) { + int first = cache_name_pos(src, length), last; + + if (first >= 0) + prepare_move_submodule(src, first, + submodule_gitfile + i); + else if (index_range_of_same_dir(src, length, + &first, &last) < 1) + bad = _("source directory is empty"); + else { /* last - first >= 1 */ + int j, dst_len, n; + + modes[i] = WORKING_DIRECTORY; + 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 char *path = active_cache[first + j]->name; + source[argc + j] = path; + destination[argc + j] = + prefix_path(dst, dst_len, path + length + 1); + modes[argc + j] = INDEX; + submodule_gitfile[argc + j] = NULL; + } + argc += last - first; + } + } else if (cache_name_pos(src, length) < 0) + bad = _("not under version control"); + else 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"); + } + } else if (string_list_has_string(&src_for_dst, dst)) + bad = _("multiple sources for the same target"); + else if (is_dir_sep(dst[strlen(dst) - 1])) + bad = _("destination directory does not exist"); + else + string_list_insert(&src_for_dst, dst); + + if (!bad) + continue; + if (!ignore_errors) + die(_("%s, source=%s, destination=%s"), + bad, src, dst); + if (--argc > 0) { + int n = argc - i; + memmove(source + i, source + i + 1, + n * sizeof(char *)); + memmove(destination + i, destination + i + 1, + n * sizeof(char *)); + memmove(modes + i, modes + i + 1, + n * sizeof(enum update_mode)); + memmove(submodule_gitfile + i, submodule_gitfile + i + 1, + n * sizeof(char *)); + i--; + } + } + + for (i = 0; i < argc; i++) { + const char *src = source[i], *dst = destination[i]; + enum update_mode mode = modes[i]; + int pos; + if (show_only || verbose) + printf(_("Renaming %s to %s\n"), src, dst); + if (show_only) + continue; + if (mode != INDEX && rename(src, dst) < 0) { + if (ignore_errors) + continue; + die_errno(_("renaming '%s' failed"), src); + } + if (submodule_gitfile[i]) { + if (submodule_gitfile[i] != SUBMODULE_WITH_GITDIR) + connect_work_tree_and_git_dir(dst, submodule_gitfile[i]); + if (!update_path_in_gitmodules(src, dst)) + gitmodules_modified = 1; + } + + if (mode == WORKING_DIRECTORY) + continue; + + pos = cache_name_pos(src, strlen(src)); + assert(pos >= 0); + if (!show_only) + rename_cache_entry_at(pos, dst); + } + + if (gitmodules_modified) + stage_updated_gitmodules(); + + if (active_cache_changed && + write_locked_index(&the_index, &lock_file, COMMIT_LOCK)) + die(_("Unable to write new index file")); + + return 0; +} diff --git a/builtin/name-rev.c b/builtin/name-rev.c new file mode 100644 index 0000000000..9e088ebd11 --- /dev/null +++ b/builtin/name-rev.c @@ -0,0 +1,499 @@ +#include "builtin.h" +#include "cache.h" +#include "config.h" +#include "commit.h" +#include "tag.h" +#include "refs.h" +#include "parse-options.h" +#include "sha1-lookup.h" + +#define CUTOFF_DATE_SLOP 86400 /* one day */ + +typedef struct rev_name { + const char *tip_name; + timestamp_t taggerdate; + int generation; + int distance; + int from_tag; +} rev_name; + +static timestamp_t cutoff = TIME_MAX; + +/* How many generations are maximally preferred over _one_ merge traversal? */ +#define MERGE_TRAVERSAL_WEIGHT 65535 + +static int is_better_name(struct rev_name *name, + const char *tip_name, + timestamp_t taggerdate, + int generation, + int distance, + int from_tag) +{ + /* + * When comparing names based on tags, prefer names + * based on the older tag, even if it is farther away. + */ + if (from_tag && name->from_tag) + return (name->taggerdate > taggerdate || + (name->taggerdate == taggerdate && + name->distance > distance)); + + /* + * We know that at least one of them is a non-tag at this point. + * 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 != distance) + return name->distance > 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 void name_rev(struct commit *commit, + const char *tip_name, timestamp_t taggerdate, + int generation, int distance, int from_tag, + int deref) +{ + struct rev_name *name = (struct rev_name *)commit->util; + struct commit_list *parents; + int parent_number = 1; + char *to_free = NULL; + + parse_commit(commit); + + if (commit->date < cutoff) + return; + + if (deref) { + tip_name = to_free = xstrfmt("%s^0", tip_name); + + if (generation) + die("generation: %d, but deref?", generation); + } + + if (name == NULL) { + name = xmalloc(sizeof(rev_name)); + commit->util = name; + goto copy_data; + } else if (is_better_name(name, tip_name, taggerdate, + generation, distance, from_tag)) { +copy_data: + name->tip_name = tip_name; + name->taggerdate = taggerdate; + name->generation = generation; + name->distance = distance; + name->from_tag = from_tag; + } else { + free(to_free); + return; + } + + for (parents = commit->parents; + parents; + parents = parents->next, parent_number++) { + if (parent_number > 1) { + size_t len; + char *new_name; + + strip_suffix(tip_name, "^0", &len); + if (generation > 0) + new_name = xstrfmt("%.*s~%d^%d", (int)len, tip_name, + generation, parent_number); + else + new_name = xstrfmt("%.*s^%d", (int)len, tip_name, + parent_number); + + name_rev(parents->item, new_name, taggerdate, 0, + distance + MERGE_TRAVERSAL_WEIGHT, + from_tag, 0); + } else { + name_rev(parents->item, tip_name, taggerdate, + generation + 1, distance + 1, + from_tag, 0); + } + } +} + +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; +} + +static const char *name_ref_abbrev(const char *refname, int shorten_unambiguous) +{ + if (shorten_unambiguous) + refname = shorten_unambiguous_ref(refname, 0); + else if (starts_with(refname, "refs/heads/")) + refname = refname + 11; + else if (starts_with(refname, "refs/")) + refname = refname + 5; + return refname; +} + +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; + } *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) +{ + refname = name_ref_abbrev(refname, shorten_unambiguous); + + 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 = xstrdup(refname); + 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 name_ref(const char *path, const struct object_id *oid, int flags, void *cb_data) +{ + struct object *o = parse_object(oid); + struct name_ref_data *data = cb_data; + int can_abbreviate_output = data->tags_only && data->name_only; + int deref = 0; + 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; + } + + add_to_tip_table(oid, path, can_abbreviate_output); + + while (o && o->type == OBJ_TAG) { + struct tag *t = (struct tag *) o; + if (!t->tagged) + break; /* broken repository */ + o = parse_object(&t->tagged->oid); + deref = 1; + taggerdate = t->date; + } + if (o && o->type == OBJ_COMMIT) { + struct commit *commit = (struct commit *)o; + int from_tag = starts_with(path, "refs/tags/"); + + if (taggerdate == TIME_MAX) + taggerdate = ((struct commit *)o)->date; + path = name_ref_abbrev(path, can_abbreviate_output); + name_rev(commit, xstrdup(path), taggerdate, 0, 0, + from_tag, deref); + } + return 0; +} + +static const unsigned char *nth_tip_table_ent(size_t ix, void *table_) +{ + struct tip_table_entry *table = table_; + return table[ix].oid.hash; +} + +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 = sha1_pos(o->oid.hash, 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; + struct commit *c; + + if (o->type != OBJ_COMMIT) + return get_exact_ref_match(o); + c = (struct commit *) o; + n = c->util; + if (!n) + return NULL; + + if (!n->generation) + return n->tip_name; + else { + int len = strlen(n->tip_name); + if (len > 2 && !strcmp(n->tip_name + len - 2, "^0")) + len -= 2; + strbuf_reset(buf); + strbuf_addf(buf, "%.*s~%d", len, n->tip_name, 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", find_unique_abbrev(oid->hash, 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>] --stdin"), + NULL +}; + +static void name_rev_line(char *p, struct name_ref_data *data) +{ + struct strbuf buf = STRBUF_INIT; + int forty = 0; + char *p_start; + for (p_start = p; *p; p++) { +#define ishex(x) (isdigit((x)) || ((x) >= 'a' && (x) <= 'f')) + if (!ishex(*p)) + forty = 0; + else if (++forty == GIT_SHA1_HEXSZ && + !ishex(*(p+1))) { + struct object_id oid; + const char *name = NULL; + char c = *(p+1); + int p_len = p - p_start + 1; + + forty = 0; + + *(p+1) = 0; + if (!get_oid(p - (GIT_SHA1_HEXSZ - 1), &oid)) { + struct object *o = + lookup_object(oid.hash); + if (o) + name = get_rev_name(o, &buf); + } + *(p+1) = c; + + if (!name) + continue; + + if (data->name_only) + printf("%.*s%s", p_len - GIT_SHA1_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, 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 names (no SHA-1)")), + 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(0, "stdin", &transform_stdin, N_("read 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")), + { + /* A Hidden OPT_BOOL */ + OPTION_SET_INT, 0, "peel-tag", &peel_tag, NULL, + N_("dereference tags in the input (internal use)"), + PARSE_OPT_NOARG | PARSE_OPT_HIDDEN, NULL, 1, + }, + OPT_END(), + }; + + git_config(git_default_config, NULL); + argc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0); + if (all + transform_stdin + !!argc > 1) { + error("Specify either a list, or --all, not both!"); + usage_with_options(name_rev_usage, opts); + } + if (all || transform_stdin) + cutoff = 0; + + for (; argc; argc--, argv++) { + struct object_id oid; + struct object *object; + struct commit *commit; + + if (get_oid(*argv, &oid)) { + fprintf(stderr, "Could not get sha1 for %s. Skipping.\n", + *argv); + continue; + } + + commit = NULL; + object = parse_object(&oid); + if (object) { + struct object *peeled = deref_tag(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) { + if (cutoff > commit->date) + cutoff = commit->date; + } + + 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); + } + + if (cutoff) + cutoff = cutoff - CUTOFF_DATE_SLOP; + for_each_ref(name_ref, &data); + + if (transform_stdin) { + char buffer[2048]; + + while (!feof(stdin)) { + char *p = fgets(buffer, sizeof(buffer), stdin); + if (!p) + break; + name_rev_line(p, &data); + } + } 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 0000000000..8e54f2d146 --- /dev/null +++ b/builtin/notes.c @@ -0,0 +1,1027 @@ +/* + * 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 "cache.h" +#include "config.h" +#include "builtin.h" +#include "notes.h" +#include "blob.h" +#include "commit.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" + +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] [-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] [-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>"), + N_("git notes merge --commit [-v | -q]"), + N_("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[] = { + N_("git notes get-ref"), + NULL +}; + +static const char note_template[] = + N_("Write/edit the notes for the following object:"); + +struct note_data { + int given; + int use_editor; + char *edit_path; + struct strbuf buf; +}; + +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); +} + +static int list_each_note(const struct object_id *object_oid, + const struct object_id *note_oid, char *note_path, + void *cb_data) +{ + 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 unsigned char *sha1) +{ + unsigned long size; + enum object_type type; + char *buf = read_sha1_file(sha1, &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) +{ + const char *show_args[5] = + {"show", "--stat", "--no-notes", oid_to_hex(object), NULL}; + struct child_process show = CHILD_PROCESS_INIT; + struct strbuf buf = STRBUF_INIT; + struct strbuf cbuf = STRBUF_INIT; + + /* Invoke "git show --stat --no-notes $object" */ + show.argv = show_args; + 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); + 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 unsigned char *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 = open(d->edit_path, O_CREAT | O_TRUNC | O_WRONLY, 0600); + if (fd < 0) + die_errno(_("could not create file '%s'"), d->edit_path); + + 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")); + strbuf_add_commented_lines(&buf, _(note_template), strlen(_(note_template))); + strbuf_addch(&buf, '\n'); + 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")); + } + strbuf_stripspace(&d->buf, 1); + } +} + +static void write_note_data(struct note_data *d, unsigned char *sha1) +{ + if (write_sha1_file(d->buf.buf, d->buf.len, blob_type, sha1)) { + error(_("unable to write note object")); + if (d->edit_path) + error(_("the note contents have been left in %s"), + d->edit_path); + exit(128); + } +} + +static int parse_msg_arg(const struct option *opt, const char *arg, int unset) +{ + struct note_data *d = opt->value; + + strbuf_grow(&d->buf, strlen(arg) + 2); + if (d->buf.len) + strbuf_addch(&d->buf, '\n'); + strbuf_addstr(&d->buf, arg); + strbuf_stripspace(&d->buf, 0); + + d->given = 1; + return 0; +} + +static int parse_file_arg(const struct option *opt, const char *arg, int unset) +{ + struct note_data *d = opt->value; + + if (d->buf.len) + strbuf_addch(&d->buf, '\n'); + if (!strcmp(arg, "-")) { + if (strbuf_read(&d->buf, 0, 1024) < 0) + die_errno(_("cannot read '%s'"), arg); + } else if (strbuf_read_file(&d->buf, arg, 1024) < 0) + die_errno(_("could not open or read '%s'"), arg); + strbuf_stripspace(&d->buf, 0); + + d->given = 1; + return 0; +} + +static int parse_reuse_arg(const struct option *opt, const char *arg, int unset) +{ + struct note_data *d = opt->value; + char *buf; + struct object_id object; + enum object_type type; + unsigned long len; + + if (d->buf.len) + strbuf_addch(&d->buf, '\n'); + + if (get_oid(arg, &object)) + die(_("failed to resolve '%s' as a valid ref."), arg); + if (!(buf = read_sha1_file(object.hash, &type, &len))) { + free(buf); + die(_("failed to read object '%s'."), arg); + } + if (type != OBJ_BLOB) { + free(buf); + die(_("cannot read note data from non-blob object '%s'."), arg); + } + strbuf_add(&d->buf, buf, len); + free(buf); + + d->given = 1; + return 0; +} + +static int parse_reedit_arg(const struct option *opt, const char *arg, int unset) +{ + struct note_data *d = opt->value; + d->use_editor = 1; + return parse_reuse_arg(opt, arg, unset); +} + +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 (get_oid(split[0]->buf, &from_obj)) + die(_("failed to resolve '%s' as a valid ref."), split[0]->buf); + if (get_oid(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(t, msg); + free_notes(t); + } else { + finish_copy_notes_for_rewrite(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 parameters")); + usage_with_options(git_notes_list_usage, options); + } + + t = init_notes_check("list", 0); + if (argc) { + if (get_oid(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 = { 0, 0, NULL, STRBUF_INIT }; + struct option options[] = { + { OPTION_CALLBACK, 'm', "message", &d, N_("message"), + N_("note contents as a string"), PARSE_OPT_NONEG, + parse_msg_arg}, + { OPTION_CALLBACK, 'F', "file", &d, N_("file"), + N_("note contents in a file"), PARSE_OPT_NONEG, + parse_file_arg}, + { OPTION_CALLBACK, 'c', "reedit-message", &d, N_("object"), + N_("reuse and edit specified note object"), PARSE_OPT_NONEG, + parse_reedit_arg}, + { OPTION_CALLBACK, '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")), + OPT_END() + }; + + argc = parse_options(argc, argv, prefix, options, git_notes_add_usage, + PARSE_OPT_KEEP_ARGV0); + + if (2 < argc) { + error(_("too many parameters")); + usage_with_options(git_notes_add_usage, options); + } + + object_ref = argc > 1 ? argv[1] : "HEAD"; + + if (get_oid(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 ? note->hash : NULL); + if (d.buf.len || allow_empty) { + write_note_data(&d, new_note.hash); + if (add_note(t, &object, &new_note, combine_notes_overwrite)) + die("BUG: combine_notes_overwrite failed"); + commit_notes(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(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")), + 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 parameters")); + usage_with_options(git_notes_copy_usage, options); + } else { + return notes_copy_from_stdin(force, rewrite_cmd); + } + } + + if (argc < 2) { + error(_("too few parameters")); + usage_with_options(git_notes_copy_usage, options); + } + if (2 < argc) { + error(_("too many parameters")); + usage_with_options(git_notes_copy_usage, options); + } + + if (get_oid(argv[0], &from_obj)) + die(_("failed to resolve '%s' as a valid ref."), argv[0]); + + object_ref = 1 < argc ? argv[1] : "HEAD"; + + if (get_oid(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)) + die("BUG: combine_notes_overwrite failed"); + commit_notes(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 = { 0, 0, NULL, STRBUF_INIT }; + struct option options[] = { + { OPTION_CALLBACK, 'm', "message", &d, N_("message"), + N_("note contents as a string"), PARSE_OPT_NONEG, + parse_msg_arg}, + { OPTION_CALLBACK, 'F', "file", &d, N_("file"), + N_("note contents in a file"), PARSE_OPT_NONEG, + parse_file_arg}, + { OPTION_CALLBACK, 'c', "reedit-message", &d, N_("object"), + N_("reuse and edit specified note object"), PARSE_OPT_NONEG, + parse_reedit_arg}, + { OPTION_CALLBACK, '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_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 parameters")); + usage_with_options(usage, options); + } + + 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 (get_oid(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->hash : NULL); + + if (note && !edit) { + /* Append buf to previous note contents */ + unsigned long size; + enum object_type type; + char *prev_buf = read_sha1_file(note->hash, &type, &size); + + strbuf_grow(&d.buf, size + 1); + if (d.buf.len && prev_buf && size) + strbuf_insert(&d.buf, 0, "\n", 1); + if (prev_buf && size) + strbuf_insert(&d.buf, 0, prev_buf, size); + free(prev_buf); + } + + if (d.buf.len || allow_empty) { + write_note_data(&d, new_note.hash); + if (add_note(t, &object, &new_note, combine_notes_overwrite)) + die("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(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 parameters")); + usage_with_options(git_notes_show_usage, options); + } + + object_ref = argc ? argv[0] : "HEAD"; + + if (get_oid(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_NODEREF)) + 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 (get_oid("NOTES_MERGE_PARTIAL", &oid)) + die(_("failed to read ref NOTES_MERGE_PARTIAL")); + else if (!(partial = lookup_commit_reference(&oid))) + die(_("could not find commit from NOTES_MERGE_PARTIAL.")); + else if (parse_commit(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); + + t = xcalloc(1, sizeof(struct notes_tree)); + init_notes(t, "NOTES_MERGE_PARTIAL", combine_notes_overwrite, 0); + + o->local_ref = local_ref_to_free = + resolve_refdup("NOTES_MERGE_REF", 0, oid.hash, 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)); + format_commit_message(partial, "%s", &msg, &pretty_ctx); + strbuf_trim(&msg); + strbuf_insert(&msg, 0, "notes: ", 7); + update_ref(msg.buf, o->local_ref, oid.hash, + is_null_oid(&parent_oid) ? NULL : parent_oid.hash, + 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")), + { OPTION_SET_INT, 0, "commit", &do_commit, NULL, + N_("finalize notes merge by committing unmerged notes"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 1}, + OPT_GROUP(N_("Aborting notes merge resolution")), + { OPTION_SET_INT, 0, "abort", &do_abort, NULL, + N_("abort notes merge"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 1}, + 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 parameters")); + usage_with_options(git_notes_merge_usage, options); + } + + init_notes_merge_options(&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)) + die("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.hash, NULL, + 0, UPDATE_REFS_DIE_ON_ERR); + else { /* Merge has unresolved conflicts */ + const struct worktree *wt; + /* Update .git/NOTES_MERGE_PARTIAL with partial merge result */ + update_ref(msg.buf, "NOTES_MERGE_PARTIAL", result_oid.hash, NULL, + 0, UPDATE_REFS_DIE_ON_ERR); + /* Store ref-to-be-updated into .git/NOTES_MERGE_REF */ + wt = find_shared_symref("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); + if (create_symref("NOTES_MERGE_REF", default_notes_ref(), NULL)) + die(_("failed to store link to current notes ref (%s)"), + default_notes_ref()); + printf(_("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 (get_oid(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(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 parameters")); + 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(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 parameters")); + 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) +{ + int result; + const char *override_notes_ref = NULL; + struct option options[] = { + OPT_STRING(0, "ref", &override_notes_ref, N_("notes-ref"), + N_("use notes from <notes-ref>")), + OPT_END() + }; + + git_config(git_default_config, NULL); + argc = parse_options(argc, argv, prefix, options, git_notes_usage, + PARSE_OPT_STOP_AT_NON_OPTION); + + 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); + } + + if (argc < 1 || !strcmp(argv[0], "list")) + result = list(argc, argv, prefix); + else if (!strcmp(argv[0], "add")) + result = add(argc, argv, prefix); + else if (!strcmp(argv[0], "copy")) + result = copy(argc, argv, prefix); + else if (!strcmp(argv[0], "append") || !strcmp(argv[0], "edit")) + result = append_edit(argc, argv, prefix); + else if (!strcmp(argv[0], "show")) + result = show(argc, argv, prefix); + else if (!strcmp(argv[0], "merge")) + result = merge(argc, argv, prefix); + else if (!strcmp(argv[0], "remove")) + result = remove_cmd(argc, argv, prefix); + else if (!strcmp(argv[0], "prune")) + result = prune(argc, argv, prefix); + else if (!strcmp(argv[0], "get-ref")) + result = get_ref(argc, argv, prefix); + else { + result = error(_("unknown subcommand: %s"), argv[0]); + usage_with_options(git_notes_usage, options); + } + + return result ? 1 : 0; +} diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c new file mode 100644 index 0000000000..6e77dfd444 --- /dev/null +++ b/builtin/pack-objects.c @@ -0,0 +1,3103 @@ +#include "builtin.h" +#include "cache.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 "pack-objects.h" +#include "progress.h" +#include "refs.h" +#include "streaming.h" +#include "thread-utils.h" +#include "pack-bitmap.h" +#include "reachable.h" +#include "sha1-array.h" +#include "argv-array.h" +#include "mru.h" +#include "packfile.h" + +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 +}; + +/* + * 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 struct pack_idx_entry **written_list; +static uint32_t nr_result, nr_written; + +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 local; +static int have_non_local_packs; +static int incremental; +static int ignore_packed_keep; +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 num_preferred_base; +static struct progress *progress_state; + +static struct packed_git *reuse_packfile; +static uint32_t reuse_packfile_objects; +static off_t reuse_packfile_offset; + +static int use_bitmap_index_default = 1; +static int use_bitmap_index = -1; +static int write_bitmap_index; +static uint16_t write_bitmap_options; + +static unsigned long delta_cache_size = 0; +static unsigned long max_delta_cache_size = 256 * 1024 * 1024; +static unsigned long cache_max_small_delta_size = 1000; + +static unsigned long window_memory_limit = 0; + +/* + * 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 = read_sha1_file(entry->idx.oid.hash, &type, &size); + if (!buf) + die("unable to read %s", oid_to_hex(&entry->idx.oid)); + base_buf = read_sha1_file(entry->delta->idx.oid.hash, &type, + &base_size); + if (!base_buf) + die("unable to read %s", + oid_to_hex(&entry->delta->idx.oid)); + delta_buf = diff_delta(base_buf, base_size, + buf, size, &delta_size, 0); + if (!delta_buf || delta_size != entry->delta_size) + die("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 sha1file *f, + const unsigned char *sha1) +{ + 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"), sha1_to_hex(sha1)); + + 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); + sha1write(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 sha1file *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; + sha1write(f, in, avail); + offset += avail; + len -= avail; + } +} + +/* Return 0 if we will bust the pack-size limit */ +static unsigned long write_no_reuse_object(struct sha1file *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; + + if (!usable_delta) { + if (entry->type == OBJ_BLOB && + entry->size > big_file_threshold && + (st = open_istream(entry->idx.oid.hash, &type, &size, NULL)) != NULL) + buf = NULL; + else { + buf = read_sha1_file(entry->idx.oid.hash, &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 = entry->delta_size; + buf = entry->delta_data; + entry->delta_data = NULL; + type = (allow_ofs_delta && entry->delta->idx.offset) ? + OBJ_OFS_DELTA : OBJ_REF_DELTA; + } else { + buf = get_delta(entry); + size = entry->delta_size; + type = (allow_ofs_delta && entry->delta->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 - entry->delta->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 + 20 >= limit) { + if (st) + close_istream(st); + free(buf); + return 0; + } + sha1write(f, header, hdrlen); + sha1write(f, dheader + pos, sizeof(dheader) - pos); + hdrlen += sizeof(dheader) - pos; + } else if (type == OBJ_REF_DELTA) { + /* + * Deltas with a base reference contain + * an additional 20 bytes for the base sha1. + */ + if (limit && hdrlen + 20 + datalen + 20 >= limit) { + if (st) + close_istream(st); + free(buf); + return 0; + } + sha1write(f, header, hdrlen); + sha1write(f, entry->delta->idx.oid.hash, 20); + hdrlen += 20; + } else { + if (limit && hdrlen + datalen + 20 >= limit) { + if (st) + close_istream(st); + free(buf); + return 0; + } + sha1write(f, header, hdrlen); + } + if (st) { + datalen = write_large_blob_data(st, f, entry->idx.oid.hash); + close_istream(st); + } else { + sha1write(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 sha1file *f, struct object_entry *entry, + unsigned long limit, int usable_delta) +{ + struct packed_git *p = entry->in_pack; + struct pack_window *w_curs = NULL; + struct revindex_entry *revidx; + off_t offset; + enum object_type type = entry->type; + off_t datalen; + unsigned char header[MAX_PACK_OBJECT_HEADER], + dheader[MAX_PACK_OBJECT_HEADER]; + unsigned hdrlen; + + if (entry->delta) + type = (allow_ofs_delta && entry->delta->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; + revidx = find_pack_revindex(p, offset); + datalen = revidx[1].offset - offset; + if (!pack_to_stdout && p->index_version > 1 && + check_pack_crc(p, &w_curs, offset, datalen, revidx->nr)) { + 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 - entry->delta->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 + 20 >= limit) { + unuse_pack(&w_curs); + return 0; + } + sha1write(f, header, hdrlen); + sha1write(f, dheader + pos, sizeof(dheader) - pos); + hdrlen += sizeof(dheader) - pos; + reused_delta++; + } else if (type == OBJ_REF_DELTA) { + if (limit && hdrlen + 20 + datalen + 20 >= limit) { + unuse_pack(&w_curs); + return 0; + } + sha1write(f, header, hdrlen); + sha1write(f, entry->delta->idx.oid.hash, 20); + hdrlen += 20; + reused_delta++; + } else { + if (limit && hdrlen + datalen + 20 >= limit) { + unuse_pack(&w_curs); + return 0; + } + sha1write(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 sha1file *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 (!entry->delta) + usable_delta = 0; /* no delta */ + else if (!pack_size_limit) + usable_delta = 1; /* unlimited packfile */ + else if (entry->delta->idx.offset == (off_t)-1) + usable_delta = 0; /* base was written to another pack */ + else if (entry->delta->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 (!entry->in_pack) + to_reuse = 0; /* can't reuse what we don't have */ + else if (entry->type == OBJ_REF_DELTA || entry->type == OBJ_OFS_DELTA) + /* check_object() decided it for us ... */ + to_reuse = usable_delta; + /* ... but pack split may override that */ + else if (entry->type != entry->in_pack_type) + to_reuse = 0; /* pack has delta which is unusable */ + else if (entry->delta) + 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 sha1file *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 (e->delta) { + e->idx.offset = 1; /* now recurse */ + switch (write_one(f, e->delta, offset)) { + case WRITE_ONE_RECURSIVE: + /* we cannot depend on this one */ + e->delta = 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, const struct object_id *oid, int flag, + void *cb_data) +{ + unsigned char peeled[20]; + struct object_entry *entry = packlist_find(&to_pack, oid->hash, NULL); + + if (entry) + entry->tagged = 1; + if (!peel_ref(path, peeled)) { + entry = packlist_find(&to_pack, peeled, NULL); + if (entry) + entry->tagged = 1; + } + return 0; +} + +static inline void add_to_write_order(struct object_entry **wo, + unsigned int *endp, + struct object_entry *e) +{ + if (e->filled) + 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 = e->delta_sibling; s; s = s->delta_sibling) { + add_to_write_order(wo, endp, s); + } + } + /* drop down a level to add left subtree nodes if possible */ + if (e->delta_child) { + add_to_order = 1; + e = e->delta_child; + } else { + add_to_order = 0; + /* our sibling might have some children, it is next */ + if (e->delta_sibling) { + e = e->delta_sibling; + continue; + } + /* go back to our parent node */ + e = e->delta; + while (e && !e->delta_sibling) { + /* we're on the right side of a subtree, keep + * going up until we can go right again */ + e = e->delta; + } + if (!e) { + /* done- we hit our original root node */ + return; + } + /* pass it off to sibling at this level */ + e = e->delta_sibling; + } + }; +} + +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; root->delta; root = root->delta) + ; /* nothing */ + add_descendants_to_write_order(wo, endp, root); +} + +static struct object_entry **compute_write_order(void) +{ + unsigned int i, wo_end, last_untagged; + + 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; + objects[i].delta_child = NULL; + objects[i].delta_sibling = 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 (!e->delta) + continue; + /* Mark me as the first child */ + e->delta_sibling = e->delta->delta_child; + e->delta->delta_child = e; + } + + /* + * Mark objects that are at the tip of tags. + */ + for_each_tag_ref(mark_tagged, NULL); + + /* + * Give the objects in the original recency order until + * we see a tagged tip. + */ + ALLOC_ARRAY(wo, to_pack.nr_objects); + for (i = wo_end = 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 (objects[i].type != OBJ_COMMIT && + objects[i].type != 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 (objects[i].type != 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) + add_family_to_write_order(wo, &wo_end, &objects[i]); + } + + if (wo_end != to_pack.nr_objects) + die("ordered %u objects, expected %"PRIu32, wo_end, to_pack.nr_objects); + + return wo; +} + +static off_t write_reused_pack(struct sha1file *f) +{ + unsigned char buffer[8192]; + off_t to_write, total; + int fd; + + if (!is_pack_valid(reuse_packfile)) + die("packfile is invalid: %s", reuse_packfile->pack_name); + + fd = git_open(reuse_packfile->pack_name); + if (fd < 0) + die_errno("unable to open packfile for reuse: %s", + reuse_packfile->pack_name); + + if (lseek(fd, sizeof(struct pack_header), SEEK_SET) == -1) + die_errno("unable to seek in reused packfile"); + + if (reuse_packfile_offset < 0) + reuse_packfile_offset = reuse_packfile->pack_size - 20; + + total = to_write = reuse_packfile_offset - sizeof(struct pack_header); + + while (to_write) { + int read_pack = xread(fd, buffer, sizeof(buffer)); + + if (read_pack <= 0) + die_errno("unable to read from reused packfile"); + + if (read_pack > to_write) + read_pack = to_write; + + sha1write(f, buffer, read_pack); + to_write -= read_pack; + + /* + * We don't know the actual number of objects written, + * only how many bytes written, how many bytes total, and + * how many objects total. So we can fake it by pretending all + * objects we are writing are the same size. This gives us a + * smooth progress meter, and at the end it matches the true + * answer. + */ + written = reuse_packfile_objects * + (((double)(total - to_write)) / total); + display_progress(progress_state, written); + } + + close(fd); + written = reuse_packfile_objects; + display_progress(progress_state, written); + return reuse_packfile_offset - sizeof(struct pack_header); +} + +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 sha1file *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 sha1[20]; + char *pack_tmp_name = NULL; + + if (pack_to_stdout) + f = sha1fd_throughput(1, "<stdout>", progress_state); + else + f = create_tmp_packfile(&pack_tmp_name); + + offset = write_pack_header(f, nr_remaining); + + if (reuse_packfile) { + off_t packfile_size; + assert(pack_to_stdout); + + packfile_size = write_reused_pack(f); + offset += packfile_size; + } + + 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); + } + + /* + * Did we write the wrong # entries in the header? + * If so, rewrite it like in fast-import + */ + if (pack_to_stdout) { + sha1close(f, sha1, CSUM_CLOSE); + } else if (nr_written == nr_remaining) { + sha1close(f, sha1, CSUM_FSYNC); + } else { + int fd = sha1close(f, sha1, 0); + fixup_pack_header_footer(fd, sha1, pack_tmp_name, + nr_written, sha1, offset); + close(fd); + if (write_bitmap_index) { + warning(_(no_split_warning)); + write_bitmap_index = 0; + } + } + + if (!pack_to_stdout) { + struct stat st; + struct strbuf tmpname = STRBUF_INIT; + + /* + * 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-", base_name); + + if (write_bitmap_index) { + bitmap_writer_set_checksum(sha1); + bitmap_writer_build_type_index(written_list, nr_written); + } + + finish_tmp_packfile(&tmpname, pack_tmp_name, + written_list, nr_written, + &pack_idx_opts, sha1); + + if (write_bitmap_index) { + strbuf_addf(&tmpname, "%s.bitmap", sha1_to_hex(sha1)); + + stop_progress(&progress_state); + + bitmap_writer_show_progress(progress); + bitmap_writer_reuse_bitmaps(&to_pack); + bitmap_writer_select_commits(indexed_commits, indexed_commits_nr, -1); + bitmap_writer_build(&to_pack); + bitmap_writer_finish(written_list, nr_written, + tmpname.buf, write_bitmap_options); + write_bitmap_index = 0; + } + + strbuf_release(&tmpname); + free(pack_tmp_name); + puts(sha1_to_hex(sha1)); + } + + /* 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); +} + +static int no_try_delta(const char *path) +{ + static struct attr_check *check; + + if (!check) + check = attr_check_initl("delta", NULL); + if (git_check_attr(path, check)) + return 0; + 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 unsigned char *sha1, + int exclude, + uint32_t *index_pos) +{ + struct object_entry *entry; + + entry = packlist_find(&to_pack, sha1, index_pos); + if (!entry) + return 0; + + if (exclude) { + if (!entry->preferred_base) + nr_result--; + entry->preferred_base = 1; + } + + return 1; +} + +static int want_found_object(int exclude, struct packed_git *p) +{ + if (exclude) + return 1; + if (incremental) + return 0; + + /* + * 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 possible 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. + */ + if (!ignore_packed_keep && + (!local || !have_non_local_packs)) + return 1; + + if (local && !p->pack_local) + return 0; + if (ignore_packed_keep && p->pack_local && p->pack_keep) + return 0; + + /* we don't know yet; keep looking for more packs */ + 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 unsigned char *sha1, + int exclude, + struct packed_git **found_pack, + off_t *found_offset) +{ + struct mru_entry *entry; + int want; + + if (!exclude && local && has_loose_object_nonlocal(sha1)) + 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(exclude, *found_pack); + if (want != -1) + return want; + } + + for (entry = packed_git_mru.head; entry; entry = entry->next) { + struct packed_git *p = entry->item; + off_t offset; + + if (p == *found_pack) + offset = *found_offset; + else + offset = find_pack_entry_one(sha1, p); + + if (offset) { + if (!*found_pack) { + if (!is_pack_valid(p)) + continue; + *found_offset = offset; + *found_pack = p; + } + want = want_found_object(exclude, p); + if (!exclude && want > 0) + mru_mark(&packed_git_mru, entry); + if (want != -1) + return want; + } + } + + return 1; +} + +static void create_object_entry(const unsigned char *sha1, + enum object_type type, + uint32_t hash, + int exclude, + int no_try_delta, + uint32_t index_pos, + struct packed_git *found_pack, + off_t found_offset) +{ + struct object_entry *entry; + + entry = packlist_alloc(&to_pack, sha1, index_pos); + entry->hash = hash; + if (type) + entry->type = type; + if (exclude) + entry->preferred_base = 1; + else + nr_result++; + if (found_pack) { + entry->in_pack = found_pack; + entry->in_pack_offset = found_offset; + } + + entry->no_try_delta = no_try_delta; +} + +static const char no_closure_warning[] = N_( +"disabling bitmap writing, as some objects are not being packed" +); + +static int add_object_entry(const unsigned char *sha1, enum object_type type, + const char *name, int exclude) +{ + struct packed_git *found_pack = NULL; + off_t found_offset = 0; + uint32_t index_pos; + + if (have_duplicate_entry(sha1, exclude, &index_pos)) + return 0; + + if (!want_object_in_pack(sha1, exclude, &found_pack, &found_offset)) { + /* The pack is missing an object, so it will not have closure */ + if (write_bitmap_index) { + warning(_(no_closure_warning)); + write_bitmap_index = 0; + } + return 0; + } + + create_object_entry(sha1, type, pack_name_hash(name), + exclude, name && no_try_delta(name), + index_pos, found_pack, found_offset); + + display_progress(progress_state, nr_result); + return 1; +} + +static int add_object_entry_from_bitmap(const unsigned char *sha1, + enum object_type type, + int flags, uint32_t name_hash, + struct packed_git *pack, off_t offset) +{ + uint32_t index_pos; + + if (have_duplicate_entry(sha1, 0, &index_pos)) + return 0; + + if (!want_object_in_pack(sha1, 0, &pack, &offset)) + return 0; + + create_object_entry(sha1, type, name_hash, 0, 0, index_pos, pack, offset); + + display_progress(progress_state, nr_result); + return 1; +} + +struct pbase_tree_cache { + unsigned char sha1[20]; + 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 unsigned char *sha1) +{ + return sha1[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 unsigned char *sha1) +{ + struct pbase_tree_cache *ent, *nent; + void *data; + unsigned long size; + enum object_type type; + int neigh; + int my_ix = pbase_tree_cache_ix(sha1); + 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 && !hashcmp(ent->sha1, sha1)) { + 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 = read_sha1_file(sha1, &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; + } + hashcpy(nent->sha1, sha1); + 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 int name_cmp_len(const char *name) +{ + int i; + for (i = 0; name[i] && name[i] != '\n' && name[i] != '/'; i++) + ; + return i; +} + +static void add_pbase_object(struct tree_desc *tree, + const char *name, + int 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->hash, + 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; + int downlen = name_cmp_len(down); + + tree = pbase_tree_get(entry.oid->hash); + 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; + int 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.sha1, 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(unsigned char *sha1) +{ + struct pbase_tree *it; + void *data; + unsigned long size; + unsigned char tree_sha1[20]; + + if (window <= num_preferred_base++) + return; + + data = read_object_with_reference(sha1, tree_type, &size, tree_sha1); + if (!data) + return; + + for (it = pbase_tree; it; it = it->next) { + if (!hashcmp(it->pcache.sha1, tree_sha1)) { + free(data); + return; + } + } + + it = xcalloc(1, sizeof(*it)); + it->next = pbase_tree; + pbase_tree = it; + + hashcpy(it->pcache.sha1, tree_sha1); + 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 *this = it; + it = this->next; + free(this->pcache.tree_data); + free(this); + } + + 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; +} + +static void check_object(struct object_entry *entry) +{ + if (entry->in_pack) { + struct packed_git *p = entry->in_pack; + struct pack_window *w_curs = NULL; + const unsigned char *base_ref = NULL; + struct object_entry *base_entry; + unsigned long used, used_0; + unsigned long avail; + off_t ofs; + unsigned char *buf, c; + + 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, + &entry->in_pack_type, + &entry->size); + if (used == 0) + goto give_up; + + /* + * 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. */ + entry->type = entry->in_pack_type; + entry->in_pack_header_size = used; + if (entry->type < OBJ_COMMIT || entry->type > OBJ_BLOB) + goto give_up; + unuse_pack(&w_curs); + return; + case OBJ_REF_DELTA: + if (reuse_delta && !entry->preferred_base) + base_ref = use_pack(p, &w_curs, + entry->in_pack_offset + used, NULL); + entry->in_pack_header_size = used + 20; + 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) { + struct revindex_entry *revidx; + revidx = find_pack_revindex(p, ofs); + if (!revidx) + goto give_up; + base_ref = nth_packed_object_sha1(p, revidx->nr); + } + entry->in_pack_header_size = used + used_0; + break; + } + + if (base_ref && (base_entry = packlist_find(&to_pack, base_ref, NULL))) { + /* + * If base_ref was set above that means we wish to + * reuse delta data, and we even found that base + * in the list of objects we want to pack. Goodie! + * + * 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. + */ + entry->type = entry->in_pack_type; + entry->delta = base_entry; + entry->delta_size = entry->size; + entry->delta_sibling = base_entry->delta_child; + base_entry->delta_child = entry; + unuse_pack(&w_curs); + return; + } + + if (entry->type) { + /* + * 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. + */ + entry->size = get_size_from_delta(p, &w_curs, + entry->in_pack_offset + entry->in_pack_header_size); + if (entry->size == 0) + goto give_up; + unuse_pack(&w_curs); + return; + } + + /* + * No choice but to fall back to the recursive delta walk + * with sha1_object_info() to find about the object type + * at this point... + */ + give_up: + unuse_pack(&w_curs); + } + + entry->type = sha1_object_info(entry->idx.oid.hash, &entry->size); + /* + * The error condition 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; + + /* 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) +{ + struct object_entry **p = &entry->delta->delta_child; + struct object_info oi = OBJECT_INFO_INIT; + + while (*p) { + if (*p == entry) + *p = (*p)->delta_sibling; + else + p = &(*p)->delta_sibling; + } + entry->delta = NULL; + entry->depth = 0; + + oi.sizep = &entry->size; + oi.typep = &entry->type; + if (packed_object_info(entry->in_pack, entry->in_pack_offset, &oi) < 0) { + /* + * We failed to get the info from this pack for some reason; + * fall back to sha1_object_info, which may find another copy. + * And if that fails, the error will be recorded in entry->type + * and dealt with in prepare_pack(). + */ + entry->type = sha1_object_info(entry->idx.oid.hash, + &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 = cur->delta, 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) + die("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 (!cur->delta) { + 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 (cur->delta->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 = cur->delta; + + /* + * 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) + die("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; + + sorted_by_offset = xcalloc(to_pack.nr_objects, sizeof(struct object_entry *)); + 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); + if (big_file_threshold < entry->size) + entry->no_try_delta = 1; + } + + /* + * 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; + + 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 (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; +} + +#ifndef NO_PTHREADS + +static pthread_mutex_t read_mutex; +#define read_lock() pthread_mutex_lock(&read_mutex) +#define read_unlock() pthread_mutex_unlock(&read_mutex) + +static pthread_mutex_t cache_mutex; +#define cache_lock() pthread_mutex_lock(&cache_mutex) +#define cache_unlock() pthread_mutex_unlock(&cache_mutex) + +static pthread_mutex_t progress_mutex; +#define progress_lock() pthread_mutex_lock(&progress_mutex) +#define progress_unlock() pthread_mutex_unlock(&progress_mutex) + +#else + +#define read_lock() (void)0 +#define read_unlock() (void)0 +#define cache_lock() (void)0 +#define cache_unlock() (void)0 +#define progress_lock() (void)0 +#define progress_unlock() (void)0 + +#endif + +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 (trg_entry->type != src_entry->type) + 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 && trg_entry->in_pack && + trg_entry->in_pack == src_entry->in_pack && + !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 = trg_entry->size; + if (!trg_entry->delta) { + max_size = trg_size/2 - 20; + ref_depth = 1; + } else { + max_size = trg_entry->delta_size; + 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 = src_entry->size; + sizediff = src_size < trg_size ? trg_size - src_size : 0; + if (sizediff >= max_size) + return 0; + if (trg_size < src_size / 32) + return 0; + + /* Load data if not already done */ + if (!trg->data) { + read_lock(); + trg->data = read_sha1_file(trg_entry->idx.oid.hash, &type, + &sz); + read_unlock(); + 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 (%lu vs %lu)", + oid_to_hex(&trg_entry->idx.oid), sz, + trg_size); + *mem_usage += sz; + } + if (!src->data) { + read_lock(); + src->data = read_sha1_file(src_entry->idx.oid.hash, &type, + &sz); + read_unlock(); + 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 (%lu vs %lu)", + oid_to_hex(&src_entry->idx.oid), sz, + 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 (trg_entry->delta) { + /* Prefer only shallower same-sized deltas. */ + if (delta_size == trg_entry->delta_size && + 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 -= trg_entry->delta_size; + 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); + } + + trg_entry->delta = src_entry; + trg_entry->delta_size = 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 = me->delta_child; + unsigned int m = n; + while (child) { + unsigned int c = check_delta_limit(child, n + 1); + if (m < c) + m = c; + child = child->delta_sibling; + } + 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 += n->entry->size; + 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; + + array = xcalloc(window, sizeof(struct unpacked)); + + 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) { + 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 (entry->delta_child) { + 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) { + entry->z_delta_size = do_compress(&entry->delta_data, + entry->delta_size); + cache_lock(); + delta_cache_size -= entry->delta_size; + delta_cache_size += entry->z_delta_size; + cache_unlock(); + } + + /* 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 (entry->delta && 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 (entry->delta) { + 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); +} + +#ifndef NO_PTHREADS + +static void try_to_free_from_threads(size_t size) +{ + read_lock(); + release_pack_memory(size); + read_unlock(); +} + +static try_to_free_t old_try_to_free_routine; + +/* + * 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. + */ + +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) +{ + init_recursive_mutex(&read_mutex); + pthread_mutex_init(&cache_mutex, NULL); + pthread_mutex_init(&progress_mutex, NULL); + pthread_cond_init(&progress_cond, NULL); + old_try_to_free_routine = set_try_to_free_routine(try_to_free_from_threads); +} + +static void cleanup_threaded_search(void) +{ + set_try_to_free_routine(old_try_to_free_routine); + pthread_cond_destroy(&progress_cond); + pthread_mutex_destroy(&read_mutex); + 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(stderr, "Delta compression using up to %d threads.\n", + delta_search_threads); + p = xcalloc(delta_search_threads, sizeof(*p)); + + /* 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); +} + +#else +#define ll_find_deltas(l, s, w, d, p) find_deltas(l, &s, w, d, p) +#endif + +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 (packlist_find(&to_pack, oid->hash, NULL)) + return; + + tag = lookup_tag(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.hash, OBJ_TAG, NULL, 0); + + if (tag->tagged->type != OBJ_TAG) + return; + + tag = (struct tag *)tag->tagged; + } +} + +static int add_ref_tag(const char *path, const struct object_id *oid, int flag, void *cb_data) +{ + struct object_id peeled; + + if (starts_with(path, "refs/tags/") && /* is a tag? */ + !peel_ref(path, peeled.hash) && /* peelable? */ + packlist_find(&to_pack, peeled.hash, NULL)) /* object packed? */ + 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; + + 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 (entry->delta) + /* This happens if we decided to reuse existing + * delta from a pack. "reuse_delta &&" is implied. + */ + continue; + + if (entry->size < 50) + continue; + + if (entry->no_try_delta) + continue; + + if (!entry->preferred_base) { + nr_deltas++; + if (entry->type < 0) + die("unable to get type of object %s", + oid_to_hex(&entry->idx.oid)); + } else { + if (entry->type < 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, void *cb) +{ + if (!strcmp(k, "pack.window")) { + window = git_config_int(k, v); + return 0; + } + if (!strcmp(k, "pack.windowmemory")) { + window_memory_limit = git_config_ulong(k, v); + return 0; + } + if (!strcmp(k, "pack.depth")) { + depth = git_config_int(k, v); + return 0; + } + if (!strcmp(k, "pack.deltacachesize")) { + max_delta_cache_size = git_config_int(k, v); + return 0; + } + if (!strcmp(k, "pack.deltacachelimit")) { + cache_max_small_delta_size = git_config_int(k, v); + 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.usebitmaps")) { + use_bitmap_index_default = git_config_bool(k, v); + return 0; + } + if (!strcmp(k, "pack.threads")) { + delta_search_threads = git_config_int(k, v); + if (delta_search_threads < 0) + die("invalid number of threads specified (%d)", + delta_search_threads); +#ifdef NO_PTHREADS + if (delta_search_threads != 1) { + warning("no threads support, ignoring %s", k); + delta_search_threads = 0; + } +#endif + return 0; + } + if (!strcmp(k, "pack.indexversion")) { + pack_idx_opts.version = git_config_int(k, v); + if (pack_idx_opts.version > 2) + die("bad pack.indexversion=%"PRIu32, + pack_idx_opts.version); + return 0; + } + return git_default_config(k, v, cb); +} + +static void read_object_list_from_stdin(void) +{ + char line[40 + 1 + PATH_MAX + 2]; + unsigned char sha1[20]; + + for (;;) { + if (!fgets(line, sizeof(line), stdin)) { + if (feof(stdin)) + break; + if (!ferror(stdin)) + die("fgets returned NULL, not EOF, not error!"); + if (errno != EINTR) + die_errno("fgets"); + clearerr(stdin); + continue; + } + if (line[0] == '-') { + if (get_sha1_hex(line+1, sha1)) + die("expected edge sha1, got garbage:\n %s", + line); + add_preferred_base(sha1); + continue; + } + if (get_sha1_hex(line, sha1)) + die("expected sha1, got garbage:\n %s", line); + + add_preferred_base_object(line+41); + add_object_entry(sha1, 0, line+41, 0); + } +} + +#define OBJECT_ADDED (1u<<20) + +static void show_commit(struct commit *commit, void *data) +{ + add_object_entry(commit->object.oid.hash, OBJ_COMMIT, NULL, 0); + commit->object.flags |= OBJECT_ADDED; + + if (write_bitmap_index) + index_commit_for_bitmap(commit); +} + +static void show_object(struct object *obj, const char *name, void *data) +{ + add_preferred_base_object(name); + add_object_entry(obj->oid.hash, obj->type, name, 0); + obj->flags |= OBJECT_ADDED; +} + +static void show_edge(struct commit *commit) +{ + add_preferred_base(commit->object.oid.hash); +} + +struct in_pack_object { + off_t offset; + struct object *object; +}; + +struct in_pack { + unsigned int alloc; + unsigned int nr; + struct in_pack_object *array; +}; + +static void mark_in_pack_object(struct object *object, struct packed_git *p, struct in_pack *in_pack) +{ + in_pack->array[in_pack->nr].offset = find_pack_entry_one(object->oid.hash, p); + in_pack->array[in_pack->nr].object = object; + in_pack->nr++; +} + +/* + * Compare the objects in the offset order, in order to emulate the + * "git rev-list --objects" output that produced the pack originally. + */ +static int ofscmp(const void *a_, const void *b_) +{ + struct in_pack_object *a = (struct in_pack_object *)a_; + struct in_pack_object *b = (struct in_pack_object *)b_; + + if (a->offset < b->offset) + return -1; + else if (a->offset > b->offset) + return 1; + else + return oidcmp(&a->object->oid, &b->object->oid); +} + +static void add_objects_in_unpacked_packs(struct rev_info *revs) +{ + struct packed_git *p; + struct in_pack in_pack; + uint32_t i; + + memset(&in_pack, 0, sizeof(in_pack)); + + for (p = packed_git; p; p = p->next) { + const unsigned char *sha1; + struct object *o; + + if (!p->pack_local || p->pack_keep) + continue; + if (open_pack_index(p)) + die("cannot open pack index"); + + ALLOC_GROW(in_pack.array, + in_pack.nr + p->num_objects, + in_pack.alloc); + + for (i = 0; i < p->num_objects; i++) { + sha1 = nth_packed_object_sha1(p, i); + o = lookup_unknown_object(sha1); + if (!(o->flags & OBJECT_ADDED)) + mark_in_pack_object(o, p, &in_pack); + o->flags |= OBJECT_ADDED; + } + } + + if (in_pack.nr) { + QSORT(in_pack.array, in_pack.nr, ofscmp); + for (i = 0; i < in_pack.nr; i++) { + struct object *o = in_pack.array[i].object; + add_object_entry(o->oid.hash, o->type, "", 0); + } + } + free(in_pack.array); +} + +static int add_loose_object(const struct object_id *oid, const char *path, + void *data) +{ + enum object_type type = sha1_object_info(oid->hash, NULL); + + if (type < 0) { + warning("loose object at %s could not be examined", path); + return 0; + } + + add_object_entry(oid->hash, 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 unsigned char *sha1) +{ + static struct packed_git *last_found = (void *)1; + struct packed_git *p; + + p = (last_found != (void *)1) ? last_found : packed_git; + + while (p) { + if ((!p->pack_local || p->pack_keep) && + find_pack_entry_one(sha1, p)) { + last_found = p; + return 1; + } + if (p == last_found) + p = packed_git; + 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(struct rev_info *revs) +{ + struct packed_git *p; + uint32_t i; + struct object_id oid; + + for (p = packed_git; p; p = p->next) { + if (!p->pack_local || p->pack_keep) + continue; + + if (open_pack_index(p)) + die("cannot open pack index"); + + for (i = 0; i < p->num_objects; i++) { + nth_packed_object_oid(&oid, p, i); + if (!packlist_find(&to_pack, oid.hash, NULL) && + !has_sha1_pack_kept_or_nonlocal(oid.hash) && + !loosened_object_can_be_discarded(&oid, p->mtime)) + if (force_object_loose(oid.hash, p->mtime)) + die("unable to force loose object"); + } + } +} + +/* + * 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 pack_to_stdout && + allow_ofs_delta && + !ignore_packed_keep && + (!local || !have_non_local_packs) && + !incremental; +} + +static int get_object_list_from_bitmap(struct rev_info *revs) +{ + if (prepare_bitmap_walk(revs) < 0) + return -1; + + if (pack_options_allow_reuse() && + !reuse_partial_packfile_from_bitmap( + &reuse_packfile, + &reuse_packfile_objects, + &reuse_packfile_offset)) { + assert(reuse_packfile_objects); + nr_result += reuse_packfile_objects; + display_progress(progress_state, nr_result); + } + + traverse_bitmap_commit_list(&add_object_entry_from_bitmap); + return 0; +} + +static void record_recent_object(struct object *obj, + const char *name, + void *data) +{ + oid_array_append(&recent_objects, &obj->oid); +} + +static void record_recent_commit(struct commit *commit, void *data) +{ + oid_array_append(&recent_objects, &commit->object.oid); +} + +static void get_object_list(int ac, const char **av) +{ + struct rev_info revs; + char line[1000]; + int flags = 0; + + init_revisions(&revs, NULL); + save_commit_buffer = 0; + setup_revisions(ac, av, &revs, NULL); + + /* make sure shallows are read */ + is_repository_shallow(); + + 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 SHA-1 '%s'", line + 10); + register_shallow(&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); + } + + if (use_bitmap_index && !get_object_list_from_bitmap(&revs)) + return; + + if (prepare_revision_walk(&revs)) + die("revision walk setup failed"); + mark_edges_uninteresting(&revs, show_edge); + traverse_commit_list(&revs, show_commit, show_object, NULL); + + if (unpack_unreachable_expiration) { + revs.ignore_missing_links = 1; + if (add_unseen_recent_objects_to_traversal(&revs, + unpack_unreachable_expiration)) + 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(&revs); + if (pack_loose_unreachable) + add_unreachable_loose_objects(); + if (unpack_unreachable) + loosen_unused_packed_objects(&revs); + + oid_array_clear(&recent_objects); +} + +static int option_parse_index_version(const struct option *opt, + const char *arg, int unset) +{ + char *c; + const char *val = arg; + pack_idx_opts.version = strtoul(val, &c, 10); + if (pack_idx_opts.version > 2) + die(_("unsupported index version %s"), val); + if (*c == ',' && c[1]) + pack_idx_opts.off32_limit = strtoul(c+1, &c, 0); + if (*c || pack_idx_opts.off32_limit & 0x80000000) + die(_("bad index version '%s'"), val); + return 0; +} + +static int option_parse_unpack_unreachable(const struct option *opt, + 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; +} + +int cmd_pack_objects(int argc, const char **argv, const char *prefix) +{ + int use_internal_rev_list = 0; + int thin = 0; + int shallow = 0; + int all_progress_implied = 0; + struct argv_array rp = ARGV_ARRAY_INIT; + int rev_list_unpacked = 0, rev_list_all = 0, rev_list_reflog = 0; + int rev_list_index = 0; + struct option pack_objects_options[] = { + OPT_SET_INT('q', "quiet", &progress, + N_("do not show progress meter"), 0), + 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")), + { OPTION_CALLBACK, 0, "index-version", NULL, N_("version[,offset]"), + N_("write the pack index file in the specified idx format version"), + 0, 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")), + { OPTION_SET_INT, 0, "unpacked", &rev_list_unpacked, NULL, + N_("limit the objects to those that are not yet packed"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 1 }, + { OPTION_SET_INT, 0, "all", &rev_list_all, NULL, + N_("include objects reachable from any reference"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 1 }, + { OPTION_SET_INT, 0, "reflog", &rev_list_reflog, NULL, + N_("include objects referred by reflog entries"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 1 }, + { OPTION_SET_INT, 0, "indexed-objects", &rev_list_index, NULL, + N_("include objects referred to by the index"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 1 }, + 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")), + { OPTION_CALLBACK, 0, "unpack-unreachable", NULL, N_("time"), + N_("unpack unreachable objects newer than <time>"), + PARSE_OPT_OPTARG, option_parse_unpack_unreachable }, + 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, + N_("ignore packs that have companion .keep file")), + OPT_INTEGER(0, "compression", &pack_compression_level, + N_("pack compression level")), + OPT_SET_INT(0, "keep-true-parents", &grafts_replace_parents, + N_("do not hide commits by grafts"), 0), + OPT_BOOL(0, "use-bitmap-index", &use_bitmap_index, + N_("use a bitmap index if available to speed up counting objects")), + OPT_BOOL(0, "write-bitmap-index", &write_bitmap_index, + N_("write a bitmap index together with the pack index")), + OPT_END(), + }; + + check_replace_refs = 0; + + reset_pack_idx_option(&pack_idx_opts); + git_config(git_pack_config, NULL); + + 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); + + argv_array_push(&rp, "pack-objects"); + if (thin) { + use_internal_rev_list = 1; + argv_array_push(&rp, shallow + ? "--objects-edge-aggressive" + : "--objects-edge"); + } else + argv_array_push(&rp, "--objects"); + + if (rev_list_all) { + use_internal_rev_list = 1; + argv_array_push(&rp, "--all"); + } + if (rev_list_reflog) { + use_internal_rev_list = 1; + argv_array_push(&rp, "--reflog"); + } + if (rev_list_index) { + use_internal_rev_list = 1; + argv_array_push(&rp, "--indexed-objects"); + } + if (rev_list_unpacked) { + use_internal_rev_list = 1; + argv_array_push(&rp, "--unpacked"); + } + + 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(); + +#ifdef NO_PTHREADS + if (delta_search_threads != 1) + warning("no threads support, ignoring --threads"); +#endif + 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("--keep-unreachable and --unpack-unreachable are incompatible."); + if (!rev_list_all || !rev_list_reflog || !rev_list_index) + unpack_unreachable_expiration = 0; + + /* + * "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()) + use_bitmap_index = 0; + + if (pack_to_stdout || !rev_list_all) + write_bitmap_index = 0; + + if (progress && all_progress_implied) + progress = 2; + + prepare_packed_git(); + if (ignore_packed_keep) { + struct packed_git *p; + for (p = packed_git; p; p = p->next) + if (p->pack_local && p->pack_keep) + break; + if (!p) /* no keep-able packs found */ + ignore_packed_keep = 0; + } + if (local) { + /* + * unlike ignore_packed_keep 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 = packed_git; p; p = p->next) { + if (!p->pack_local) { + have_non_local_packs = 1; + break; + } + } + } + + if (progress) + progress_state = start_progress(_("Counting objects"), 0); + if (!use_internal_rev_list) + read_object_list_from_stdin(); + else { + get_object_list(rp.argc, rp.argv); + argv_array_clear(&rp); + } + cleanup_preferred_base(); + if (include_tag && nr_result) + for_each_ref(add_ref_tag, NULL); + stop_progress(&progress_state); + + if (non_empty && !nr_result) + return 0; + if (nr_result) + prepare_pack(window, depth); + write_pack_file(); + if (progress) + fprintf(stderr, "Total %"PRIu32" (delta %"PRIu32")," + " reused %"PRIu32" (delta %"PRIu32")\n", + written, written_delta, reused, reused_delta); + return 0; +} diff --git a/builtin/pack-redundant.c b/builtin/pack-redundant.c new file mode 100644 index 0000000000..aaa8136322 --- /dev/null +++ b/builtin/pack-redundant.c @@ -0,0 +1,697 @@ +/* +* +* Copyright 2005, Lukas Sandstrom <lukass@etek.chalmers.se> +* +* This file is licensed under the GPL v2. +* +*/ + +#include "builtin.h" +#include "packfile.h" + +#define BLKSIZE 512 + +static const char pack_redundant_usage[] = +"git pack-redundant [--verbose] [--alt-odb] (--all | <filename.pack>...)"; + +static int load_all_packs, verbose, alt_odb; + +struct llist_item { + struct llist_item *next; + const unsigned char *sha1; +}; +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 *all_objects; +} *local_packs = NULL, *altodb_packs = NULL; + +struct pll { + struct pll *next; + struct pack_list *pl; +}; + +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; + if ( free_nodes ) { + new = free_nodes; + free_nodes = free_nodes->next; + } else { + int i = 1; + ALLOC_ARRAY(new, BLKSIZE); + for (; i < BLKSIZE; i++) + llist_item_put(&new[i]); + } + return new; +} + +static void llist_free(struct llist *list) +{ + while ((list->back = list->front)) { + list->front = list->front->next; + llist_item_put(list->back); + } + free(list); +} + +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, *old, *prev; + + llist_init(&ret); + + if ((ret->size = list->size) == 0) + return ret; + + new = ret->front = llist_item_get(); + new->sha1 = list->front->sha1; + + old = list->front->next; + while (old) { + prev = new; + new = llist_item_get(); + prev->next = new; + new->sha1 = old->sha1; + old = old->next; + } + new->next = NULL; + ret->back = new; + + return ret; +} + +static inline struct llist_item *llist_insert(struct llist *list, + struct llist_item *after, + const unsigned char *sha1) +{ + struct llist_item *new = llist_item_get(); + new->sha1 = sha1; + new->next = NULL; + + if (after != NULL) { + new->next = after->next; + after->next = new; + if (after == list->back) + list->back = new; + } else {/* insert in front */ + if (list->size == 0) + list->back = new; + else + new->next = list->front; + list->front = new; + } + list->size++; + return new; +} + +static inline struct llist_item *llist_insert_back(struct llist *list, + const unsigned char *sha1) +{ + return llist_insert(list, list->back, sha1); +} + +static inline struct llist_item *llist_insert_sorted_unique(struct llist *list, + const unsigned char *sha1, struct llist_item *hint) +{ + struct llist_item *prev = NULL, *l; + + l = (hint == NULL) ? list->front : hint; + while (l) { + int cmp = hashcmp(l->sha1, sha1); + if (cmp > 0) { /* we insert before this entry */ + return llist_insert(list, prev, sha1); + } + if (!cmp) { /* already exists */ + return l; + } + prev = l; + l = l->next; + } + /* insert at the end */ + return llist_insert_back(list, sha1); +} + +/* returns a pointer to an item in front of sha1 */ +static inline struct llist_item * llist_sorted_remove(struct llist *list, const unsigned char *sha1, struct llist_item *hint) +{ + struct llist_item *prev, *l; + +redo_from_start: + l = (hint == NULL) ? list->front : hint; + prev = NULL; + while (l) { + int cmp = hashcmp(l->sha1, sha1); + if (cmp > 0) /* not in list, since sorted */ + return prev; + if (!cmp) { /* found */ + if (prev == NULL) { + 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->sha1, 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 == NULL) + 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) +{ + unsigned long 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; + + 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 = (p1->pack->index_version < 2) ? 24 : 20; + p2_step = (p2->pack->index_version < 2) ? 24 : 20; + + while (p1_off < p1->pack->num_objects * p1_step && + p2_off < p2->pack->num_objects * p2_step) + { + 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 void pll_free(struct pll *l) +{ + struct pll *old; + struct pack_list *opl; + + while (l) { + old = l; + while (l->pl) { + opl = l->pl; + l->pl = opl->next; + free(opl); + } + l = l->next; + free(old); + } +} + +/* all the permutations have to be free()d at the same time, + * since they refer to each other + */ +static struct pll * get_permutations(struct pack_list *list, int n) +{ + struct pll *subset, *ret = NULL, *new_pll = NULL; + + if (list == NULL || pack_list_size(list) < n || n == 0) + return NULL; + + if (n == 1) { + while (list) { + new_pll = xmalloc(sizeof(*new_pll)); + new_pll->pl = NULL; + pack_list_insert(&new_pll->pl, list); + new_pll->next = ret; + ret = new_pll; + list = list->next; + } + return ret; + } + + while (list->next) { + subset = get_permutations(list->next, n - 1); + while (subset) { + new_pll = xmalloc(sizeof(*new_pll)); + new_pll->pl = subset->pl; + pack_list_insert(&new_pll->pl, list); + new_pll->next = ret; + ret = new_pll; + subset = subset->next; + } + list = list->next; + } + return ret; +} + +static int is_superset(struct pack_list *pl, struct llist *list) +{ + struct llist *diff; + + diff = llist_copy(list); + + while (pl) { + llist_sorted_difference_inplace(diff, pl->all_objects); + if (diff->size == 0) { /* we're done */ + llist_free(diff); + return 1; + } + pl = pl->next; + } + llist_free(diff); + return 0; +} + +static size_t sizeof_union(struct packed_git *p1, struct packed_git *p2) +{ + size_t ret = 0; + unsigned long p1_off = 0, p2_off = 0, p1_step, p2_step; + const unsigned char *p1_base, *p2_base; + + 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 = (p1->index_version < 2) ? 24 : 20; + p2_step = (p2->index_version < 2) ? 24 : 20; + + 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 == NULL) + 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 void minimize(struct pack_list **min) +{ + struct pack_list *pl, *unique = NULL, + *non_unique = NULL, *min_perm = NULL; + struct pll *perm, *perm_all, *perm_ok = NULL, *new_perm; + struct llist *missing; + off_t min_perm_size = 0, perm_size; + int n; + + 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->all_objects); + pl = pl->next; + } + + /* return if there are no objects missing from the unique set */ + if (missing->size == 0) { + *min = unique; + free(missing); + return; + } + + /* find the permutations which contain all missing objects */ + for (n = 1; n <= pack_list_size(non_unique) && !perm_ok; n++) { + perm_all = perm = get_permutations(non_unique, n); + while (perm) { + if (is_superset(perm->pl, missing)) { + new_perm = xmalloc(sizeof(struct pll)); + memcpy(new_perm, perm, sizeof(struct pll)); + new_perm->next = perm_ok; + perm_ok = new_perm; + } + perm = perm->next; + } + if (perm_ok) + break; + pll_free(perm_all); + } + if (perm_ok == NULL) + die("Internal error: No complete sets found!"); + + /* find the permutation with the smallest size */ + perm = perm_ok; + while (perm) { + perm_size = pack_set_bytecount(perm->pl); + if (!min_perm_size || min_perm_size > perm_size) { + min_perm_size = perm_size; + min_perm = perm->pl; + } + perm = perm->next; + } + *min = min_perm; + /* add the unique packs to the list */ + pl = unique; + while (pl) { + pack_list_insert(min, pl); + pl = pl->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->all_objects->front; + while (l) { + hint = llist_insert_sorted_unique(all_objects, + l->sha1, 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->all_objects); + pl = pl->next; + } +} + +/* this scales like O(n^2) */ +static void cmp_local_packs(void) +{ + struct pack_list *subset, *pl = local_packs; + + 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->unique_objects, + alt->all_objects); + local = local->next; + } + llist_sorted_difference_inplace(all_objects, alt->all_objects); + alt = alt->next; + } +} + +static struct pack_list * add_pack(struct packed_git *p) +{ + struct pack_list l; + unsigned long off = 0, step; + const unsigned char *base; + + if (!p->pack_local && !(alt_odb || verbose)) + return NULL; + + l.pack = p; + llist_init(&l.all_objects); + + if (open_pack_index(p)) + return NULL; + + base = p->index_data; + base += 256 * 4 + ((p->index_version < 2) ? 4 : 8); + step = (p->index_version < 2) ? 24 : 20; + while (off < p->num_objects * step) { + llist_insert_back(l.all_objects, base + off); + off += step; + } + /* this list will be pruned in cmp_two_packs later */ + l.unique_objects = llist_copy(l.all_objects); + 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 = packed_git; + + 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 = packed_git; + + while (p) { + add_pack(p); + p = p->next; + } +} + +int cmd_pack_redundant(int argc, const char **argv, const char *prefix) +{ + int i; + struct pack_list *min, *red, *pl; + struct llist *ignore; + unsigned char *sha1; + char buf[42]; /* 40 byte sha1 + \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 (*arg == '-') + usage(pack_redundant_usage); + else + break; + } + + prepare_packed_git(); + + if (load_all_packs) + load_all(); + else + while (*(argv + i) != NULL) + add_pack_file(*(argv + i++)); + + if (local_packs == NULL) + die("Zero packs found!"); + + load_all_objects(); + + cmp_local_packs(); + if (alt_odb) + scan_alt_odb_packs(); + + /* ignore objects given on stdin */ + llist_init(&ignore); + if (!isatty(0)) { + while (fgets(buf, sizeof(buf), stdin)) { + sha1 = xmalloc(20); + if (get_sha1_hex(buf, sha1)) + die("Bad sha1 on stdin: %s", buf); + llist_insert_sorted_unique(ignore, sha1, NULL); + } + } + llist_sorted_difference_inplace(all_objects, ignore); + pl = local_packs; + while (pl) { + llist_sorted_difference_inplace(pl->unique_objects, ignore); + pl = pl->next; + } + + 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->sha1), + 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 0000000000..b106a392a4 --- /dev/null +++ b/builtin/pack-refs.c @@ -0,0 +1,21 @@ +#include "builtin.h" +#include "parse-options.h" +#include "refs.h" + +static char const * const pack_refs_usage[] = { + N_("git pack-refs [<options>]"), + NULL +}; + +int cmd_pack_refs(int argc, const char **argv, const char *prefix) +{ + unsigned int flags = PACK_REFS_PRUNE; + struct option opts[] = { + OPT_BIT(0, "all", &flags, N_("pack everything"), PACK_REFS_ALL), + OPT_BIT(0, "prune", &flags, N_("prune loose refs (default)"), PACK_REFS_PRUNE), + OPT_END(), + }; + if (parse_options(argc, argv, prefix, opts, pack_refs_usage, 0)) + usage_with_options(pack_refs_usage, opts); + return refs_pack_refs(get_main_ref_store(), flags); +} diff --git a/builtin/patch-id.c b/builtin/patch-id.c new file mode 100644 index 0000000000..970d0d30b4 --- /dev/null +++ b/builtin/patch-id.c @@ -0,0 +1,199 @@ +#include "builtin.h" +#include "config.h" + +static void flush_current_id(int patchlen, struct object_id *id, struct object_id *result) +{ + char name[50]; + + if (!patchlen) + return; + + memcpy(name, oid_to_hex(id), GIT_SHA1_HEXSZ + 1); + printf("%s %s\n", oid_to_hex(result), name); +} + +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; + n = strspn(q, digits); + } + if (n == 0 || q[n] != ' ' || q[n+1] != '+') + return 0; + + r = q + n + 2; + n = strspn(r, digits); + if (r[n] == ',') { + r += n + 1; + n = strspn(r, digits); + } + if (n == 0) + return 0; + + *p_before = atoi(q); + *p_after = atoi(r); + return 1; +} + +static void flush_one_hunk(struct object_id *result, git_SHA_CTX *ctx) +{ + unsigned char hash[GIT_MAX_RAWSZ]; + unsigned short carry = 0; + int i; + + git_SHA1_Final(hash, ctx); + git_SHA1_Init(ctx); + /* 20-byte sum, with carry */ + for (i = 0; i < GIT_SHA1_RAWSZ; ++i) { + carry += result->hash[i] + hash[i]; + result->hash[i] = carry; + carry >>= 8; + } +} + +static int get_one_patchid(struct object_id *next_oid, struct object_id *result, + struct strbuf *line_buf, int stable) +{ + int patchlen = 0, found_next = 0; + int before = -1, after = -1; + git_SHA_CTX ctx; + + git_SHA1_Init(&ctx); + oidclr(result); + + while (strbuf_getwholeline(line_buf, stdin, '\n') != EOF) { + char *line = line_buf->buf; + const char *p = line; + int len; + + if (!skip_prefix(line, "diff-tree ", &p) && + !skip_prefix(line, "commit ", &p) && + !skip_prefix(line, "From ", &p) && + starts_with(line, "\\ ") && 12 < 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, "index ")) + continue; + else if (starts_with(line, "--- ")) + before = after = 1; + else if (!isalpha(line[0])) + break; + } + + /* 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--; + + /* Compute the sha without whitespace */ + len = remove_space(line); + patchlen += len; + git_SHA1_Update(&ctx, line, len); + } + + if (!found_next) + oidclr(next_oid); + + flush_one_hunk(result, &ctx); + + return patchlen; +} + +static void generate_id_list(int stable) +{ + 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); + flush_current_id(patchlen, &oid, &result); + oidcpy(&oid, &n); + } + strbuf_release(&line_buf); +} + +static const char patch_id_usage[] = "git patch-id [--stable | --unstable]"; + +static int git_patch_id_config(const char *var, const char *value, void *cb) +{ + int *stable = cb; + + if (!strcmp(var, "patchid.stable")) { + *stable = git_config_bool(var, value); + return 0; + } + + return git_default_config(var, value, cb); +} + +int cmd_patch_id(int argc, const char **argv, const char *prefix) +{ + int stable = -1; + + git_config(git_patch_id_config, &stable); + + /* If nothing is set, default to unstable. */ + if (stable < 0) + stable = 0; + + if (argc == 2 && !strcmp(argv[1], "--stable")) + stable = 1; + else if (argc == 2 && !strcmp(argv[1], "--unstable")) + stable = 0; + else if (argc != 1) + usage(patch_id_usage); + + generate_id_list(stable); + return 0; +} diff --git a/builtin/prune-packed.c b/builtin/prune-packed.c new file mode 100644 index 0000000000..419238171d --- /dev/null +++ b/builtin/prune-packed.c @@ -0,0 +1,67 @@ +#include "builtin.h" +#include "cache.h" +#include "progress.h" +#include "parse-options.h" +#include "packfile.h" + +static const char * const prune_packed_usage[] = { + N_("git prune-packed [-n | --dry-run] [-q | --quiet]"), + NULL +}; + +static struct progress *progress; + +static int prune_subdir(unsigned int nr, const char *path, void *data) +{ + int *opts = data; + display_progress(progress, nr + 1); + if (!(*opts & PRUNE_PACKED_DRY_RUN)) + rmdir(path); + return 0; +} + +static int prune_object(const struct object_id *oid, const char *path, + void *data) +{ + int *opts = data; + + if (!has_sha1_pack(oid->hash)) + return 0; + + if (*opts & PRUNE_PACKED_DRY_RUN) + printf("rm -f %s\n", path); + else + unlink_or_warn(path); + return 0; +} + +void prune_packed_objects(int opts) +{ + if (opts & PRUNE_PACKED_VERBOSE) + progress = start_delayed_progress(_("Removing duplicate objects"), 256); + + for_each_loose_file_in_objdir(get_object_directory(), + prune_object, NULL, prune_subdir, &opts); + + /* Ensure we show 100% before finishing progress */ + display_progress(progress, 256); + stop_progress(&progress); +} + +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); + + prune_packed_objects(opts); + return 0; +} diff --git a/builtin/prune.c b/builtin/prune.c new file mode 100644 index 0000000000..cddabf26a9 --- /dev/null +++ b/builtin/prune.c @@ -0,0 +1,158 @@ +#include "cache.h" +#include "commit.h" +#include "diff.h" +#include "revision.h" +#include "builtin.h" +#include "reachable.h" +#include "parse-options.h" +#include "progress.h" + +static const char * const prune_usage[] = { + N_("git prune [-n] [-v] [--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 (show_only || verbose) + printf("Removing stale temporary file %s\n", fullpath); + if (!show_only) + unlink_or_warn(fullpath); + return 0; +} + +static int prune_object(const struct object_id *oid, const char *fullpath, + void *data) +{ + struct stat st; + + /* + * Do we know about this object? + * It must have been reachable + */ + if (lookup_object(oid->hash)) + 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 = sha1_object_info(oid->hash, NULL); + printf("%s %s\n", oid_to_hex(oid), + (type > 0) ? typename(type) : "unknown"); + } + if (!show_only) + unlink_or_warn(fullpath); + return 0; +} + +static int prune_cruft(const char *basename, const char *path, void *data) +{ + 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, const char *path, void *data) +{ + 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) { + fprintf(stderr, "Unable to open directory %s\n", path); + 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; + struct progress *progress = NULL; + 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_END() + }; + char *s; + + expire = TIME_MAX; + save_commit_buffer = 0; + check_replace_refs = 0; + ref_paranoia = 1; + init_revisions(&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 (!get_oid(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 (show_progress) + progress = start_delayed_progress(_("Checking connectivity"), 0); + + mark_reachable_objects(&revs, 1, expire, progress); + stop_progress(&progress); + for_each_loose_file_in_objdir(get_object_directory(), prune_object, + prune_cruft, prune_subdir, NULL); + + 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()) + prune_shallow(show_only); + + return 0; +} diff --git a/builtin/pull.c b/builtin/pull.c new file mode 100644 index 0000000000..6f772e8a22 --- /dev/null +++ b/builtin/pull.c @@ -0,0 +1,940 @@ +/* + * 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. + */ +#include "cache.h" +#include "config.h" +#include "builtin.h" +#include "parse-options.h" +#include "exec_cmd.h" +#include "run-command.h" +#include "sha1-array.h" +#include "remote.h" +#include "dir.h" +#include "refs.h" +#include "revision.h" +#include "submodule.h" +#include "submodule-config.h" +#include "tempfile.h" +#include "lockfile.h" +#include "wt-status.h" + +enum rebase_type { + REBASE_INVALID = -1, + REBASE_FALSE = 0, + REBASE_TRUE, + REBASE_PRESERVE, + REBASE_INTERACTIVE +}; + +/** + * 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 + * "preserve", returns REBASE_PRESERVE. 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) +{ + int v = git_parse_maybe_bool(value); + + if (!v) + return REBASE_FALSE; + else if (v > 0) + return REBASE_TRUE; + else if (!strcmp(value, "preserve")) + return REBASE_PRESERVE; + else if (!strcmp(value, "interactive")) + return REBASE_INTERACTIVE; + + 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; + +/* 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_squash; +static char *opt_commit; +static char *opt_edit; +static char *opt_ff; +static char *opt_verify_signatures; +static int opt_autostash = -1; +static int config_autostash; +static struct argv_array opt_strategies = ARGV_ARRAY_INIT; +static struct argv_array opt_strategy_opts = ARGV_ARRAY_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 struct option pull_options[] = { + /* Shared options */ + OPT__VERBOSITY(&opt_verbosity), + OPT_PASSTHRU(0, "progress", &opt_progress, NULL, + N_("force progress reporting"), + PARSE_OPT_NOARG), + { OPTION_CALLBACK, 0, "recurse-submodules", + &recurse_submodules, 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")), + { OPTION_CALLBACK, 'r', "rebase", &opt_rebase, + "false|true|preserve|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, "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_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-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 rebase")), + 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")), + 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(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_END() +}; + +/** + * Pushes "-q" or "-v" switches into arr to match the opt_verbosity level. + */ +static void argv_push_verbosity(struct argv_array *arr) +{ + int verbosity; + + for (verbosity = opt_verbosity; verbosity > 0; verbosity--) + argv_array_push(arr, "-v"); + + for (verbosity = opt_verbosity; verbosity < 0; verbosity++) + argv_array_push(arr, "-q"); +} + +/** + * Pushes "-f" switches into arr to match the opt_force level. + */ +static void argv_push_force(struct argv_array *arr) +{ + int force = opt_force; + while (force-- > 0) + argv_array_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 pull.ff: %s"), 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(void) +{ + 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); + + return REBASE_FALSE; +} + +/** + * Read config variables. + */ +static int git_pull_config(const char *var, const char *value, 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; + } + return git_default_config(var, value, 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(); + FILE *fp; + struct strbuf sb = STRBUF_INIT; + struct object_id oid; + + fp = xfopen(filename, "r"); + while (strbuf_getline_lf(&sb, fp) != EOF) { + if (get_oid_hex(sb.buf, &oid)) + continue; /* invalid line: does not start with SHA1 */ + if (starts_with(sb.buf + GIT_SHA1_HEXSZ, "\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 argv_array args = ARGV_ARRAY_INIT; + int ret; + + argv_array_pushl(&args, "fetch", "--update-head-ok", NULL); + + /* Shared options */ + argv_push_verbosity(&args); + if (opt_progress) + argv_array_push(&args, opt_progress); + + /* Options passed to git-fetch */ + if (opt_all) + argv_array_push(&args, opt_all); + if (opt_append) + argv_array_push(&args, opt_append); + if (opt_upload_pack) + argv_array_push(&args, opt_upload_pack); + argv_push_force(&args); + if (opt_tags) + argv_array_push(&args, opt_tags); + if (opt_prune) + argv_array_push(&args, opt_prune); + if (recurse_submodules != RECURSE_SUBMODULES_DEFAULT) + switch (recurse_submodules) { + case RECURSE_SUBMODULES_ON: + argv_array_push(&args, "--recurse-submodules=on"); + break; + case RECURSE_SUBMODULES_OFF: + argv_array_push(&args, "--recurse-submodules=no"); + break; + case RECURSE_SUBMODULES_ON_DEMAND: + argv_array_push(&args, "--recurse-submodules=on-demand"); + break; + default: + BUG("submodule recursion option not understood"); + } + if (max_children) + argv_array_push(&args, max_children); + if (opt_dry_run) + argv_array_push(&args, "--dry-run"); + if (opt_keep) + argv_array_push(&args, opt_keep); + if (opt_depth) + argv_array_push(&args, opt_depth); + if (opt_unshallow) + argv_array_push(&args, opt_unshallow); + if (opt_update_shallow) + argv_array_push(&args, opt_update_shallow); + if (opt_refmap) + argv_array_push(&args, opt_refmap); + + if (repo) { + argv_array_push(&args, repo); + argv_array_pushv(&args, refspecs); + } else if (*refspecs) + die("BUG: refspecs without repo?"); + ret = run_command_v_opt(args.argv, RUN_GIT_CMD); + argv_array_clear(&args); + return ret; +} + +/** + * "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) +{ + /* + * 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(&empty_tree_oid, merge_head, 0)) + return 1; + + if (update_ref("initial pull", "HEAD", merge_head->hash, curr_head->hash, 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; + argv_array_pushl(&cp.args, "submodule", "update", + "--recursive", "--rebase", NULL); + + return run_command(&cp); +} + +static int update_submodules(void) +{ + struct child_process cp = CHILD_PROCESS_INIT; + + cp.git_cmd = 1; + cp.no_stdin = 1; + argv_array_pushl(&cp.args, "submodule", "update", + "--recursive", "--checkout", NULL); + + return run_command(&cp); +} + +/** + * Runs git-merge, returning its exit status. + */ +static int run_merge(void) +{ + int ret; + struct argv_array args = ARGV_ARRAY_INIT; + + argv_array_pushl(&args, "merge", NULL); + + /* Shared options */ + argv_push_verbosity(&args); + if (opt_progress) + argv_array_push(&args, opt_progress); + + /* Options passed to git-merge */ + if (opt_diffstat) + argv_array_push(&args, opt_diffstat); + if (opt_log) + argv_array_push(&args, opt_log); + if (opt_squash) + argv_array_push(&args, opt_squash); + if (opt_commit) + argv_array_push(&args, opt_commit); + if (opt_edit) + argv_array_push(&args, opt_edit); + if (opt_ff) + argv_array_push(&args, opt_ff); + if (opt_verify_signatures) + argv_array_push(&args, opt_verify_signatures); + argv_array_pushv(&args, opt_strategies.argv); + argv_array_pushv(&args, opt_strategy_opts.argv); + if (opt_gpg_sign) + argv_array_push(&args, opt_gpg_sign); + if (opt_allow_unrelated_histories > 0) + argv_array_push(&args, "--allow-unrelated-histories"); + + argv_array_push(&args, "FETCH_HEAD"); + ret = run_command_v_opt(args.argv, RUN_GIT_CMD); + argv_array_clear(&args); + return ret; +} + +/** + * 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 *spec; + const char *spec_src; + const char *merge_branch; + + spec = parse_fetch_refspec(1, &refspec); + 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; + + free_refspec(1, 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; + + argv_array_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_SHA1_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(curr_head), &revs); + commit_list_insert(lookup_commit_reference(merge_head), &revs); + if (!is_null_oid(fork_point)) + commit_list_insert(lookup_commit_reference(fork_point), &revs); + + result = reduce_heads(get_octopus_merge_bases(revs)); + free_commit_list(revs); + if (!result) + return 1; + + oidcpy(merge_base, &result->item->object.oid); + return 0; +} + +/** + * Given the current HEAD SHA1, the merge head returned from git-fetch and the + * fork point calculated by get_rebase_fork_point(), runs git-rebase with the + * appropriate arguments and returns its exit status. + */ +static int run_rebase(const struct object_id *curr_head, + const struct object_id *merge_head, + const struct object_id *fork_point) +{ + int ret; + struct object_id oct_merge_base; + struct argv_array args = ARGV_ARRAY_INIT; + + if (!get_octopus_merge_base(&oct_merge_base, curr_head, merge_head, fork_point)) + if (!is_null_oid(fork_point) && !oidcmp(&oct_merge_base, fork_point)) + fork_point = NULL; + + argv_array_push(&args, "rebase"); + + /* Shared options */ + argv_push_verbosity(&args); + + /* Options passed to git-rebase */ + if (opt_rebase == REBASE_PRESERVE) + argv_array_push(&args, "--preserve-merges"); + else if (opt_rebase == REBASE_INTERACTIVE) + argv_array_push(&args, "--interactive"); + if (opt_diffstat) + argv_array_push(&args, opt_diffstat); + argv_array_pushv(&args, opt_strategies.argv); + argv_array_pushv(&args, opt_strategy_opts.argv); + if (opt_gpg_sign) + argv_array_push(&args, opt_gpg_sign); + if (opt_autostash == 0) + argv_array_push(&args, "--no-autostash"); + else if (opt_autostash == 1) + argv_array_push(&args, "--autostash"); + if (opt_verify_signatures && + !strcmp(opt_verify_signatures, "--verify-signatures")) + warning(_("ignoring --verify-signatures for rebase")); + + argv_array_push(&args, "--onto"); + argv_array_push(&args, oid_to_hex(merge_head)); + + if (fork_point && !is_null_oid(fork_point)) + argv_array_push(&args, oid_to_hex(fork_point)); + else + argv_array_push(&args, oid_to_hex(merge_head)); + + ret = run_command_v_opt(args.argv, RUN_GIT_CMD); + argv_array_clear(&args); + return ret; +} + +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 autostash; + + if (!getenv("GIT_REFLOG_ACTION")) + set_reflog_message(argc, argv); + + git_config(git_pull_config, NULL); + + argc = parse_options(argc, argv, prefix, pull_options, pull_usage, 0); + + parse_repo_refspecs(argc, argv, &repo, &refspecs); + + if (!opt_ff) + opt_ff = xstrdup_or_null(config_get_ff()); + + if (opt_rebase < 0) + opt_rebase = config_get_rebase(); + + if (read_cache_unmerged()) + die_resolve_conflict("pull"); + + if (file_exists(git_path_merge_head())) + die_conclude_merge(); + + if (get_oid("HEAD", &orig_head)) + oidclr(&orig_head); + + if (!opt_rebase && opt_autostash != -1) + die(_("--[no-]autostash option is only valid with --rebase.")); + + autostash = config_autostash; + if (opt_rebase) { + if (opt_autostash != -1) + autostash = opt_autostash; + + if (is_null_oid(&orig_head) && !is_cache_unborn()) + die(_("Updating an unborn branch with changes added to the index.")); + + if (!autostash) + require_clean_work_tree(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 (get_oid("HEAD", &curr_head)) + oidclr(&curr_head); + + if (!is_null_oid(&orig_head) && !is_null_oid(&curr_head) && + oidcmp(&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(&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.")); + return pull_into_void(merge_heads.oid, &curr_head); + } + if (opt_rebase && merge_heads.nr > 1) + die(_("Cannot rebase onto multiple branches.")); + + if (opt_rebase) { + int ret = 0; + if ((recurse_submodules == RECURSE_SUBMODULES_ON || + recurse_submodules == RECURSE_SUBMODULES_ON_DEMAND) && + submodule_touches_in_range(&rebase_fork_point, &curr_head)) + die(_("cannot rebase with locally recorded submodule modifications")); + if (!autostash) { + struct commit_list *list = NULL; + struct commit *merge_head, *head; + + head = lookup_commit_reference(&orig_head); + commit_list_insert(head, &list); + merge_head = lookup_commit_reference(&merge_heads.oid[0]); + if (is_descendant_of(merge_head, list)) { + /* we can fast-forward this without invoking rebase */ + opt_ff = "--ff-only"; + ret = run_merge(); + } + } + ret = run_rebase(&curr_head, merge_heads.oid, &rebase_fork_point); + + if (!ret && (recurse_submodules == RECURSE_SUBMODULES_ON || + recurse_submodules == RECURSE_SUBMODULES_ON_DEMAND)) + ret = rebase_submodules(); + + return ret; + } else { + int ret = run_merge(); + if (!ret && (recurse_submodules == RECURSE_SUBMODULES_ON || + recurse_submodules == RECURSE_SUBMODULES_ON_DEMAND)) + ret = update_submodules(); + return ret; + } +} diff --git a/builtin/push.c b/builtin/push.c new file mode 100644 index 0000000000..2ac8104229 --- /dev/null +++ b/builtin/push.c @@ -0,0 +1,597 @@ +/* + * "git push" + */ +#include "cache.h" +#include "config.h" +#include "refs.h" +#include "run-command.h" +#include "builtin.h" +#include "remote.h" +#include "transport.h" +#include "parse-options.h" +#include "submodule.h" +#include "submodule-config.h" +#include "send-pack.h" + +static const char * const push_usage[] = { + N_("git push [<options>] [<repository> [<refspec>...]]"), + NULL, +}; + +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 const char **refspec; +static int refspec_nr; +static int refspec_alloc; + +static void add_refspec(const char *ref) +{ + refspec_nr++; + ALLOC_GROW(refspec, refspec_nr, refspec_alloc); + refspec[refspec_nr-1] = ref; +} + +static const char *map_refspec(const char *ref, + struct remote *remote, struct ref *local_refs) +{ + struct ref *matched = NULL; + + /* Does "ref" uniquely name our ref? */ + if (count_refspec_match(ref, local_refs, &matched) != 1) + return ref; + + if (remote->push) { + struct refspec query; + memset(&query, 0, sizeof(struct refspec)); + query.src = matched->name; + if (!query_refspecs(remote->push, remote->push_refspec_nr, &query) && + query.dst) { + struct strbuf buf = STRBUF_INIT; + strbuf_addf(&buf, "%s%s:%s", + query.force ? "+" : "", + query.src, query.dst); + return strbuf_detach(&buf, NULL); + } + } + + if (push_default == PUSH_DEFAULT_UPSTREAM && + starts_with(matched->name, "refs/heads/")) { + struct branch *branch = branch_get(matched->name + 11); + if (branch->merge_nr == 1 && branch->merge[0]->src) { + struct strbuf buf = STRBUF_INIT; + strbuf_addf(&buf, "%s:%s", + ref, branch->merge[0]->src); + return strbuf_detach(&buf, NULL); + } + } + + return 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)) { + struct strbuf tagref = STRBUF_INIT; + if (nr <= ++i) + die(_("tag shorthand without <tag>")); + ref = refs[i]; + if (deleterefs) + strbuf_addf(&tagref, ":refs/tags/%s", ref); + else + strbuf_addf(&tagref, "refs/tags/%s", ref); + ref = strbuf_detach(&tagref, NULL); + } else if (deleterefs) { + struct strbuf delref = STRBUF_INIT; + if (strchr(ref, ':')) + die(_("--delete only accepts plain target ref names")); + strbuf_addf(&delref, ":%s", ref); + ref = strbuf_detach(&delref, NULL); + } else if (!strchr(ref, ':')) { + if (!remote) { + /* lazily grab remote and local_refs */ + remote = remote_get(repo); + local_refs = get_local_heads(); + } + ref = map_refspec(ref, remote, local_refs); + } + add_refspec(ref); + } +} + +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 int 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_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_maybe = _("\n" + "To choose either option permanently, " + "see push.default in 'git help config'."); + 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 %s\n" + "%s"), + remote->name, short_upstream, + remote->name, branch->name, advice_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 void setup_push_upstream(struct remote *remote, struct branch *branch, + int triangular, int simple) +{ + struct strbuf refspec = STRBUF_INIT; + + if (!branch) + die(_(message_detached_head_die), remote->name); + if (!branch->merge_nr || !branch->merge || !branch->remote_name) + 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"), + branch->name, + remote->name, + branch->name); + if (branch->merge_nr != 1) + die(_("The current branch %s has multiple upstream branches, " + "refusing to push."), branch->name); + if (triangular) + 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); + + if (simple) { + /* Additional safety */ + if (strcmp(branch->refname, branch->merge[0]->src)) + die_push_simple(branch, remote); + } + + strbuf_addf(&refspec, "%s:%s", branch->refname, branch->merge[0]->src); + add_refspec(refspec.buf); +} + +static void setup_push_current(struct remote *remote, struct branch *branch) +{ + struct strbuf refspec = STRBUF_INIT; + + if (!branch) + die(_(message_detached_head_die), remote->name); + strbuf_addf(&refspec, "%s:%s", branch->refname, branch->refname); + add_refspec(refspec.buf); +} + +static int is_workflow_triangular(struct remote *remote) +{ + struct remote *fetch_remote = remote_get(NULL); + return (fetch_remote && fetch_remote != remote); +} + +static void setup_default_push_refspecs(struct remote *remote) +{ + struct branch *branch = branch_get(NULL); + int triangular = is_workflow_triangular(remote); + + switch (push_default) { + default: + case PUSH_DEFAULT_MATCHING: + add_refspec(":"); + break; + + case PUSH_DEFAULT_UNSPECIFIED: + case PUSH_DEFAULT_SIMPLE: + if (triangular) + setup_push_current(remote, branch); + else + setup_push_upstream(remote, branch, triangular, 1); + break; + + case PUSH_DEFAULT_UPSTREAM: + setup_push_upstream(remote, branch, triangular, 0); + break; + + case PUSH_DEFAULT_CURRENT: + setup_push_current(remote, branch); + break; + + case PUSH_DEFAULT_NOTHING: + die(_("You didn't specify any refspecs to push, and " + "push.default is \"nothing\".")); + break; + } +} + +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. Integrate the remote changes (e.g.\n" + "'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. Check out this branch and integrate the remote changes\n" + "(e.g. 'git pull ...') 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\n" + "not have locally. This is usually caused by another repository pushing\n" + "to the same ref. You may want to first integrate the remote changes\n" + "(e.g., '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 void advise_pull_before_push(void) +{ + if (!advice_push_non_ff_current || !advice_push_update_rejected) + return; + advise(_(message_advice_pull_before_push)); +} + +static void advise_checkout_pull_push(void) +{ + if (!advice_push_non_ff_matching || !advice_push_update_rejected) + return; + advise(_(message_advice_checkout_pull_push)); +} + +static void advise_ref_already_exists(void) +{ + if (!advice_push_already_exists || !advice_push_update_rejected) + return; + advise(_(message_advice_ref_already_exists)); +} + +static void advise_ref_fetch_first(void) +{ + if (!advice_push_fetch_first || !advice_push_update_rejected) + return; + advise(_(message_advice_ref_fetch_first)); +} + +static void advise_ref_needs_force(void) +{ + if (!advice_push_needs_force || !advice_push_update_rejected) + return; + advise(_(message_advice_ref_needs_force)); +} + +static int push_with_options(struct transport *transport, int flags) +{ + int err; + unsigned int reject_reasons; + + 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"), transport->url); + err = transport_push(transport, refspec_nr, refspec, flags, + &reject_reasons); + if (err != 0) + error(_("failed to push some refs to '%s'"), transport->url); + + err |= transport_disconnect(transport); + 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(); + } + + return 1; +} + +static int do_push(const char *repo, int flags, + const struct string_list *push_options) +{ + int i, errs; + struct remote *remote = pushremote_get(repo); + const char **url; + int url_nr; + + 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 (push_options->nr) + flags |= TRANSPORT_PUSH_OPTIONS; + + if ((flags & TRANSPORT_PUSH_ALL) && refspec) { + if (!strcmp(*refspec, "refs/tags/*")) + return error(_("--all and --tags are incompatible")); + return error(_("--all can't be combined with refspecs")); + } + + if ((flags & TRANSPORT_PUSH_MIRROR) && refspec) { + if (!strcmp(*refspec, "refs/tags/*")) + return error(_("--mirror and --tags are incompatible")); + return error(_("--mirror can't be combined with refspecs")); + } + + if ((flags & (TRANSPORT_PUSH_ALL|TRANSPORT_PUSH_MIRROR)) == + (TRANSPORT_PUSH_ALL|TRANSPORT_PUSH_MIRROR)) { + return error(_("--all and --mirror are incompatible")); + } + + if (!refspec && !(flags & TRANSPORT_PUSH_ALL)) { + if (remote->push_refspec_nr) { + refspec = remote->push_refspec; + refspec_nr = remote->push_refspec_nr; + } else if (!(flags & TRANSPORT_PUSH_MIRROR)) + setup_default_push_refspecs(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, 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, 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 (arg) + *recurse_submodules = parse_push_recurse_submodules_arg(opt->long_name, arg); + else + die("%s missing parameter", opt->long_name); + + 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, void *cb) +{ + int *flags = cb; + int status; + + status = git_gpg_config(k, v, NULL); + if (status) + return status; + + 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.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; + } + + return git_default_config(k, v, 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 = STRING_LIST_INIT_DUP; + const struct string_list_item *item; + + struct option options[] = { + OPT__VERBOSITY(&verbosity), + OPT_STRING( 0 , "repo", &repo, N_("repository"), N_("repository")), + OPT_BIT( 0 , "all", &flags, N_("push all refs"), TRANSPORT_PUSH_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 --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), + { OPTION_CALLBACK, + 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 }, + { OPTION_CALLBACK, 0, "recurse-submodules", &recurse_submodules, "check|on-demand|no", + N_("control recursive pushing of submodules"), + PARSE_OPT_OPTARG, option_parse_recurse_submodules }, + OPT_BOOL( 0 , "thin", &thin, N_("use thin pack")), + 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), + { OPTION_CALLBACK, + 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, N_("server-specific"), N_("option to transmit")), + OPT_SET_INT('4', "ipv4", &family, N_("use IPv4 addresses only"), + TRANSPORT_FAMILY_IPV4), + OPT_SET_INT('6', "ipv6", &family, N_("use IPv6 addresses only"), + TRANSPORT_FAMILY_IPV6), + OPT_END() + }; + + packet_trace_identity("push"); + git_config(git_push_config, &flags); + argc = parse_options(argc, argv, prefix, options, push_usage, 0); + set_push_cert_flags(&flags, push_cert); + + if (deleterefs && (tags || (flags & (TRANSPORT_PUSH_ALL | TRANSPORT_PUSH_MIRROR)))) + die(_("--delete is incompatible with --all, --mirror and --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) + add_refspec("refs/tags/*"); + + if (argc > 0) { + repo = argv[0]; + set_refspecs(argv + 1, argc - 1, repo); + } + + 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(repo, flags, &push_options); + string_list_clear(&push_options, 0); + if (rc == -1) + usage_with_options(push_usage, options); + else + return rc; +} diff --git a/builtin/read-tree.c b/builtin/read-tree.c new file mode 100644 index 0000000000..bf87a2710b --- /dev/null +++ b/builtin/read-tree.c @@ -0,0 +1,264 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ + +#include "cache.h" +#include "config.h" +#include "lockfile.h" +#include "object.h" +#include "tree.h" +#include "tree-walk.h" +#include "cache-tree.h" +#include "unpack-trees.h" +#include "dir.h" +#include "builtin.h" +#include "parse-options.h" +#include "resolve-undo.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>) [-u [--exclude-per-directory=<gitignore>] | -i]] [--no-sparse-checkout] [--index-output=<file>] (--empty | <tree-ish1> [<tree-ish2> [<tree-ish3>]])"), + NULL +}; + +static int index_output_cb(const struct option *opt, const char *arg, + int unset) +{ + set_alternate_index_output(arg); + return 0; +} + +static int exclude_per_directory_cb(const struct option *opt, const char *arg, + int unset) +{ + struct dir_struct *dir; + struct unpack_trees_options *opts; + + opts = (struct unpack_trees_options *)opt->value; + + if (opts->dir) + die("more than one --exclude-per-directory given."); + + dir = xcalloc(1, sizeof(*opts->dir)); + dir->flags |= DIR_SHOW_IGNORED; + dir->exclude_per_dir = arg; + opts->dir = dir; + /* We do not need to nor want to do read-directory + * here; we are merely interested in reusing the + * per directory ignore stack mechanism. + */ + 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->merge_size); + debug_stage("index", stages[0], o); + for (i = 1; i <= o->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, void *cb) +{ + if (!strcmp(var, "submodule.recurse")) + return git_default_submodule_config(var, value, cb); + + return git_default_config(var, value, cb); +} + +static struct lock_file lock_file; + +int cmd_read_tree(int argc, const char **argv, const char *unused_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; + const struct option read_tree_options[] = { + { OPTION_CALLBACK, 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 | PARSE_OPT_LITERAL_ARGHELP }, + OPT_BOOL('u', NULL, &opts.update, + N_("update working tree with merge result")), + { OPTION_CALLBACK, 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.debug_unpack, + N_("debug unpack-trees")), + { OPTION_CALLBACK, 0, "recurse-submodules", NULL, + "checkout", "control recursive updating of submodules", + PARSE_OPT_OPTARG, option_parse_recurse_submodules_worktree_updater }, + 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, unused_prefix, read_tree_options, + read_tree_usage, 0); + + hold_locked_index(&lock_file, LOCK_DIE_ON_ERROR); + + prefix_set = opts.prefix ? 1 : 0; + if (1 < opts.merge + opts.reset + prefix_set) + die("Which one? -m, --reset, or --prefix?"); + + /* + * 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 (read_cache_unmerged() && (opts.prefix || opts.merge)) + die("You need to resolve your current index first"); + stage = opts.merge = 1; + } + resolve_undo_clear(); + + for (i = 0; i < argc; i++) { + const char *arg = argv[i]; + + if (get_oid(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.dir && !opts.update)) + die("--exclude-per-directory is meaningless unless -u"); + if (opts.merge && !opts.index_only) + setup_work_tree(); + + 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_cache_unborn(); + 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.debug_unpack) + opts.fn = debug_merge; + + cache_tree_free(&active_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.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_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--helper.c b/builtin/rebase--helper.c new file mode 100644 index 0000000000..f8519363a3 --- /dev/null +++ b/builtin/rebase--helper.c @@ -0,0 +1,68 @@ +#include "builtin.h" +#include "cache.h" +#include "config.h" +#include "parse-options.h" +#include "sequencer.h" + +static const char * const builtin_rebase_helper_usage[] = { + N_("git rebase--helper [<options>]"), + NULL +}; + +int cmd_rebase__helper(int argc, const char **argv, const char *prefix) +{ + struct replay_opts opts = REPLAY_OPTS_INIT; + int keep_empty = 0; + enum { + CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_SHA1S, EXPAND_SHA1S, + CHECK_TODO_LIST, SKIP_UNNECESSARY_PICKS, REARRANGE_SQUASH + } command = 0; + struct option options[] = { + OPT_BOOL(0, "ff", &opts.allow_ff, N_("allow fast-forward")), + OPT_BOOL(0, "keep-empty", &keep_empty, N_("keep empty commits")), + OPT_CMDMODE(0, "continue", &command, N_("continue rebase"), + CONTINUE), + OPT_CMDMODE(0, "abort", &command, N_("abort rebase"), + ABORT), + OPT_CMDMODE(0, "make-script", &command, + N_("make rebase script"), MAKE_SCRIPT), + OPT_CMDMODE(0, "shorten-ids", &command, + N_("shorten SHA-1s in the todo list"), SHORTEN_SHA1S), + OPT_CMDMODE(0, "expand-ids", &command, + N_("expand SHA-1s in the todo list"), EXPAND_SHA1S), + OPT_CMDMODE(0, "check-todo-list", &command, + N_("check the todo list"), CHECK_TODO_LIST), + OPT_CMDMODE(0, "skip-unnecessary-picks", &command, + N_("skip unnecessary picks"), SKIP_UNNECESSARY_PICKS), + OPT_CMDMODE(0, "rearrange-squash", &command, + N_("rearrange fixup/squash lines"), REARRANGE_SQUASH), + OPT_END() + }; + + git_config(git_default_config, NULL); + + opts.action = REPLAY_INTERACTIVE_REBASE; + opts.allow_ff = 1; + opts.allow_empty = 1; + + argc = parse_options(argc, argv, NULL, options, + builtin_rebase_helper_usage, PARSE_OPT_KEEP_ARGV0); + + if (command == CONTINUE && argc == 1) + return !!sequencer_continue(&opts); + if (command == ABORT && argc == 1) + return !!sequencer_remove_state(&opts); + if (command == MAKE_SCRIPT && argc > 1) + return !!sequencer_make_script(keep_empty, stdout, argc, argv); + if (command == SHORTEN_SHA1S && argc == 1) + return !!transform_todo_ids(1); + if (command == EXPAND_SHA1S && argc == 1) + return !!transform_todo_ids(0); + if (command == CHECK_TODO_LIST && argc == 1) + return !!check_todo_list(); + if (command == SKIP_UNNECESSARY_PICKS && argc == 1) + return !!skip_unnecessary_picks(); + if (command == REARRANGE_SQUASH && argc == 1) + return !!rearrange_squash(); + usage_with_options(builtin_rebase_helper_usage, options); +} diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c new file mode 100644 index 0000000000..cc48767405 --- /dev/null +++ b/builtin/receive-pack.c @@ -0,0 +1,2029 @@ +#include "builtin.h" +#include "config.h" +#include "lockfile.h" +#include "pack.h" +#include "refs.h" +#include "pkt-line.h" +#include "sideband.h" +#include "run-command.h" +#include "exec_cmd.h" +#include "commit.h" +#include "object.h" +#include "remote.h" +#include "connect.h" +#include "transport.h" +#include "string-list.h" +#include "sha1-array.h" +#include "connected.h" +#include "argv-array.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" + +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 unpack_limit = 100; +static off_t max_input_size; +static int report_status; +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 unsigned char push_cert_sha1[20]; +static struct signature_check sigcheck; +static const char *push_cert_nonce; +static const char *cert_nonce_seed; + +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 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, void *cb) +{ + int status = parse_hide_refs_config(var, value, "receive"); + + 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); + return 0; + } + + if (strcmp(var, "transfer.unpacklimit") == 0) { + transfer_unpack_limit = git_config_int(var, value); + 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); + 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); + return 0; + } + + if (strcmp(var, "receive.maxinputsize") == 0) { + max_input_size = git_config_int64(var, value); + return 0; + } + + return git_default_config(var, value, 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 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"); + 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, void *data) +{ + struct oidset *seen = data; + const char *path = strip_namespace(path_full); + + if (ref_is_hidden(path, path_full)) + 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 char *refname, + 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; + + for_each_ref(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); +} + +struct command { + struct command *next; + const char *error_string; + unsigned int skip_update:1, + did_not_exist:1; + int index; + struct object_id old_oid; + struct object_id new_oid; + char ref_name[FLEX_ARRAY]; /* more */ +}; + +static void rp_error(const char *err, ...) __attribute__((format (printf, 1, 2))); +static void rp_warning(const char *err, ...) __attribute__((format (printf, 1, 2))); + +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); +} + +static void rp_warning(const char *err, ...) +{ + va_list params; + va_start(params, err); + report_message("warning: ", err, params); + va_end(params); +} + +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, void *arg) +{ + 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; +} + +#define HMAC_BLOCK_SIZE 64 + +static void hmac_sha1(unsigned char *out, + const char *key_in, size_t key_len, + const char *text, size_t text_len) +{ + unsigned char key[HMAC_BLOCK_SIZE]; + unsigned char k_ipad[HMAC_BLOCK_SIZE]; + unsigned char k_opad[HMAC_BLOCK_SIZE]; + int i; + git_SHA_CTX ctx; + + /* RFC 2104 2. (1) */ + memset(key, '\0', HMAC_BLOCK_SIZE); + if (HMAC_BLOCK_SIZE < key_len) { + git_SHA1_Init(&ctx); + git_SHA1_Update(&ctx, key_in, key_len); + git_SHA1_Final(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) */ + git_SHA1_Init(&ctx); + git_SHA1_Update(&ctx, k_ipad, sizeof(k_ipad)); + git_SHA1_Update(&ctx, text, text_len); + git_SHA1_Final(out, &ctx); + + /* RFC 2104 2. (6) & (7) */ + git_SHA1_Init(&ctx); + git_SHA1_Update(&ctx, k_opad, sizeof(k_opad)); + git_SHA1_Update(&ctx, out, 20); + git_SHA1_Final(out, &ctx); +} + +static char *prepare_push_cert_nonce(const char *path, timestamp_t stamp) +{ + struct strbuf buf = STRBUF_INIT; + unsigned char sha1[20]; + + strbuf_addf(&buf, "%s:%"PRItime, path, stamp); + hmac_sha1(sha1, buf.buf, buf.len, cert_nonce_seed, strlen(cert_nonce_seed));; + strbuf_release(&buf); + + /* RFC 2104 5. HMAC-SHA1-80 */ + strbuf_addf(&buf, "%"PRItime"-%.*s", stamp, 20, sha1_to_hex(sha1)); + return strbuf_detach(&buf, NULL); +} + +/* + * NEEDSWORK: reuse find_commit_header() from jk/commit-author-parsing + * after dropping "_commit" from its name and possibly moving it out + * of commit.c + */ +static char *find_header(const char *msg, size_t len, const char *key, + const char **next_line) +{ + int key_len = strlen(key); + const char *line = msg; + + while (line && line < msg + len) { + const char *eol = strchrnul(line, '\n'); + + if ((msg + len <= eol) || line == eol) + return NULL; + if (line + key_len < eol && + !memcmp(line, key, key_len) && line[key_len] == ' ') { + int offset = key_len + 1; + if (next_line) + *next_line = *eol ? eol + 1 : eol; + return xmemdupz(line + offset, (eol - line) - offset); + } + line = *eol ? eol + 1 : NULL; + } + return NULL; +} + +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; + + 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; + } + + expect = prepare_push_cert_nonce(service_dir, stamp); + if (strcmp(expect, nonce)) { + /* 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) { + struct strbuf gpg_output = STRBUF_INIT; + struct strbuf gpg_status = STRBUF_INIT; + int bogs /* beginning_of_gpg_sig */; + + already_done = 1; + if (write_sha1_file(push_cert.buf, push_cert.len, "blob", push_cert_sha1)) + hashclr(push_cert_sha1); + + memset(&sigcheck, '\0', sizeof(sigcheck)); + sigcheck.result = 'N'; + + bogs = parse_signature(push_cert.buf, push_cert.len); + if (verify_signed_buffer(push_cert.buf, bogs, + push_cert.buf + bogs, push_cert.len - bogs, + &gpg_output, &gpg_status) < 0) { + ; /* error running gpg */ + } else { + sigcheck.payload = push_cert.buf; + sigcheck.gpg_output = gpg_output.buf; + sigcheck.gpg_status = gpg_status.buf; + parse_gpg_output(&sigcheck); + } + + strbuf_release(&gpg_output); + strbuf_release(&gpg_status); + nonce_status = check_nonce(push_cert.buf, bogs); + } + if (!is_null_sha1(push_cert_sha1)) { + argv_array_pushf(&proc->env_array, "GIT_PUSH_CERT=%s", + sha1_to_hex(push_cert_sha1)); + argv_array_pushf(&proc->env_array, "GIT_PUSH_CERT_SIGNER=%s", + sigcheck.signer ? sigcheck.signer : ""); + argv_array_pushf(&proc->env_array, "GIT_PUSH_CERT_KEY=%s", + sigcheck.key ? sigcheck.key : ""); + argv_array_pushf(&proc->env_array, "GIT_PUSH_CERT_STATUS=%c", + sigcheck.result); + if (push_cert_nonce) { + argv_array_pushf(&proc->env_array, + "GIT_PUSH_CERT_NONCE=%s", + push_cert_nonce); + argv_array_pushf(&proc->env_array, + "GIT_PUSH_CERT_NONCE_STATUS=%s", + nonce_status); + if (nonce_status == NONCE_SLOP) + argv_array_pushf(&proc->env_array, + "GIT_PUSH_CERT_NONCE_SLOP=%ld", + nonce_stamp_slop); + } + } +} + +struct receive_hook_feed_state { + struct command *cmd; + 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; + const char *argv[2]; + int code; + + argv[0] = find_hook(hook_name); + if (!argv[0]) + return 0; + + argv[1] = NULL; + + proc.argv = argv; + proc.in = -1; + proc.stdout_to_stderr = 1; + if (feed_state->push_options) { + int i; + for (i = 0; i < feed_state->push_options->nr; i++) + argv_array_pushf(&proc.env_array, + "GIT_PUSH_OPTION_%d=%s", i, + feed_state->push_options->items[i].string); + argv_array_pushf(&proc.env_array, "GIT_PUSH_OPTION_COUNT=%d", + feed_state->push_options->nr); + } else + argv_array_pushf(&proc.env_array, "GIT_PUSH_OPTION_COUNT"); + + if (tmp_objdir) + argv_array_pushv(&proc.env_array, 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 */ + strbuf_reset(&state->buf); + 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; + 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) +{ + const char *argv[5]; + struct child_process proc = CHILD_PROCESS_INIT; + int code; + + argv[0] = find_hook("update"); + if (!argv[0]) + return 0; + + argv[1] = cmd->ref_name; + argv[2] = oid_to_hex(&cmd->old_oid); + argv[3] = oid_to_hex(&cmd->new_oid); + argv[4] = NULL; + + proc.no_stdin = 1; + proc.stdout_to_stderr = 1; + proc.err = use_sideband ? -1 : 0; + proc.argv = argv; + + code = start_command(&proc); + if (code) + return code; + if (use_sideband) + copy_to_sideband(proc.err, -1, NULL); + return finish_command(&proc); +} + +static int is_ref_checked_out(const char *ref) +{ + if (is_bare_repository()) + return 0; + + if (!head_name) + return 0; + return !strcmp(head_name, ref); +} + +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 int command_singleton_iterator(void *cb_data, unsigned char sha1[20]); +static int update_shallow_ref(struct command *cmd, struct shallow_info *si) +{ + static struct lock_file shallow_lock; + 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_lock_file(&shallow_lock); + oid_array_clear(&extra); + return -1; + } + + commit_lock_file(&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(&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 !get_oid("HEAD", &oid); +} + +static const char *push_to_deploy(unsigned char *sha1, + struct argv_array *env, + const char *work_tree) +{ + const char *update_refresh[] = { + "update-index", "-q", "--ignore-submodules", "--refresh", NULL + }; + const char *diff_files[] = { + "diff-files", "--quiet", "--ignore-submodules", "--", NULL + }; + const char *diff_index[] = { + "diff-index", "--quiet", "--cached", "--ignore-submodules", + NULL, "--", NULL + }; + const char *read_tree[] = { + "read-tree", "-u", "-m", NULL, NULL + }; + struct child_process child = CHILD_PROCESS_INIT; + + child.argv = update_refresh; + child.env = env->argv; + 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); + child.argv = diff_files; + child.env = env->argv; + 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"; + + /* diff-index with either HEAD or an empty tree */ + diff_index[4] = head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX; + + child_process_init(&child); + child.argv = diff_index; + child.env = env->argv; + 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"; + + read_tree[3] = sha1_to_hex(sha1); + child_process_init(&child); + child.argv = read_tree; + child.env = env->argv; + 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 *sha1, + struct argv_array *env, + const char *work_tree) +{ + argv_array_pushf(env, "GIT_WORK_TREE=%s", absolute_path(work_tree)); + if (run_hook_le(env->argv, push_to_checkout_hook, + sha1_to_hex(sha1), NULL)) + return "push-to-checkout hook declined"; + else + return NULL; +} + +static const char *update_worktree(unsigned char *sha1) +{ + const char *retval; + const char *work_tree = git_work_tree_cfg ? git_work_tree_cfg : ".."; + struct argv_array env = ARGV_ARRAY_INIT; + + if (is_bare_repository()) + return "denyCurrentBranch = updateInstead needs a worktree"; + + argv_array_pushf(&env, "GIT_DIR=%s", absolute_path(get_git_dir())); + + if (!find_hook(push_to_checkout_hook)) + retval = push_to_deploy(sha1, &env, work_tree); + else + retval = push_to_checkout(sha1, &env, work_tree); + + argv_array_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; + + /* only refs/... are allowed */ + if (!starts_with(name, "refs/") || check_refname_format(name + 5, 0)) { + rp_error("refusing to create funny ref '%s' remotely", name); + return "funny refname"; + } + + strbuf_addf(&namespaced_name_buf, "%s%s", get_git_namespace(), name); + free(namespaced_name); + namespaced_name = strbuf_detach(&namespaced_name_buf, NULL); + + if (is_ref_checked_out(namespaced_name)) { + 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(); + return "branch is currently checked out"; + case DENY_UPDATE_INSTEAD: + ret = update_worktree(new_oid->hash); + if (ret) + return ret; + break; + } + } + + if (!is_null_oid(new_oid) && !has_object_file(new_oid)) { + error("unpack should have generated %s, " + "but I can't find it!", oid_to_hex(new_oid)); + return "bad pack"; + } + + 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); + return "deletion prohibited"; + } + + if (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); + return "deletion of the current branch prohibited"; + default: + return "Invalid denyDeleteCurrent setting"; + } + } + } + + 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(old_oid); + new_object = parse_object(new_oid); + + if (!old_object || !new_object || + old_object->type != OBJ_COMMIT || + new_object->type != OBJ_COMMIT) { + error("bad sha1 objects for %s", name); + return "bad ref"; + } + old_commit = (struct commit *)old_object; + new_commit = (struct commit *)new_object; + if (!in_merge_bases(old_commit, new_commit)) { + rp_error("denying non-fast-forward %s" + " (you should pull first)", name); + return "non-fast-forward"; + } + } + if (run_update_hook(cmd)) { + rp_error("hook declined to update %s", name); + return "hook declined"; + } + + if (is_null_oid(new_oid)) { + struct strbuf err = STRBUF_INIT; + if (!parse_object(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 ? old_oid->hash : NULL, + 0, "push", &err)) { + rp_error("%s", err.buf); + strbuf_release(&err); + return "failed to delete"; + } + strbuf_release(&err); + return NULL; /* good */ + } + else { + struct strbuf err = STRBUF_INIT; + if (shallow_update && si->shallow_ref[cmd->index] && + update_shallow_ref(cmd, si)) + return "shallow error"; + + if (ref_transaction_update(transaction, + namespaced_name, + new_oid->hash, old_oid->hash, + 0, "push", + &err)) { + rp_error("%s", err.buf); + strbuf_release(&err); + + return "failed to update ref"; + } + strbuf_release(&err); + + return NULL; /* good */ + } +} + +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.argc) + argv_array_push(&proc.args, hook); + argv_array_push(&proc.args, cmd->ref_name); + } + if (!proc.args.argc) + return; + + proc.no_stdin = 1; + proc.stdout_to_stderr = 1; + proc.err = use_sideband ? -1 : 0; + + if (!start_command(&proc)) { + if (use_sideband) + copy_to_sideband(proc.err, -1, NULL); + finish_command(&proc); + } +} + +static void check_aliased_update(struct command *cmd, struct string_list *list) +{ + struct strbuf buf = STRBUF_INIT; + const char *dst_name; + struct string_list_item *item; + struct command *dst_cmd; + int flag; + + strbuf_addf(&buf, "%s%s", get_git_namespace(), cmd->ref_name); + dst_name = resolve_ref_unsafe(buf.buf, 0, NULL, &flag); + strbuf_release(&buf); + + 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)) == NULL) + return; + + cmd->skip_update = 1; + + dst_cmd = (struct command *) item->util; + + if (!oidcmp(&cmd->old_oid, &dst_cmd->old_oid) && + !oidcmp(&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, + find_unique_abbrev(cmd->old_oid.hash, DEFAULT_ABBREV), + find_unique_abbrev(cmd->new_oid.hash, DEFAULT_ABBREV), + dst_cmd->ref_name, + find_unique_abbrev(dst_cmd->old_oid.hash, DEFAULT_ABBREV), + find_unique_abbrev(dst_cmd->new_oid.hash, DEFAULT_ABBREV)); + + cmd->error_string = dst_cmd->error_string = + "inconsistent aliased update"; +} + +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 int command_singleton_iterator(void *cb_data, unsigned char sha1[20]) +{ + struct command **cmd_list = cb_data; + struct command *cmd = *cmd_list; + + if (!cmd || is_null_oid(&cmd->new_oid)) + return -1; /* end of list */ + *cmd_list = NULL; /* this returns only one */ + hashcpy(sha1, cmd->new_oid.hash); + return 0; +} + +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 int iterate_receive_command_list(void *cb_data, unsigned char sha1[20]) +{ + 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) { + hashcpy(sha1, cmd->new_oid.hash); + *cmd_list = cmd->next; + return 0; + } + } + *cmd_list = NULL; + return -1; /* end of list */ +} + +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)) + 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 warn_if_skipped_connectivity_check(struct command *commands, + struct shallow_info *si) +{ + struct command *cmd; + int checked_connectivity = 1; + + for (cmd = commands; cmd; cmd = cmd->next) { + if (should_process_cmd(cmd) && si->shallow_ref[cmd->index]) { + error("BUG: connectivity check has not been run on ref %s", + cmd->ref_name); + checked_connectivity = 0; + } + } + if (!checked_connectivity) + die("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)) + 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)) + 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; + + 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); + 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); + + 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; + } + + /* + * 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 (use_atomic) + execute_commands_atomic(commands, si); + else + execute_commands_non_atomic(commands, si); + + if (shallow_update) + warn_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 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_signature(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 oid_array *shallow) +{ + struct command *commands = NULL; + struct command **p = &commands; + for (;;) { + char *line; + int len, linelen; + + line = packet_read_line(0, &len); + if (!line) + break; + + if (len > 8 && starts_with(line, "shallow ")) { + struct object_id oid; + if (get_oid_hex(line + 8, &oid)) + die("protocol error: expected shallow sha, got '%s'", + line + 8); + oid_array_append(shallow, &oid); + continue; + } + + linelen = strlen(line); + if (linelen < len) { + const char *feature_list = line + linelen + 1; + if (parse_feature_request(feature_list, "report-status")) + report_status = 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; + } + + if (!strcmp(line, "push-cert")) { + int true_flush = 0; + char certbuf[1024]; + + for (;;) { + len = packet_read(0, NULL, NULL, + certbuf, sizeof(certbuf), 0); + if (!len) { + true_flush = 1; + break; + } + if (!strcmp(certbuf, "push-cert-end\n")) + break; /* end of cert */ + strbuf_addstr(&push_cert, certbuf); + } + + if (true_flush) + break; + continue; + } + + p = queue_command(p, line, linelen); + } + + if (push_cert.len) + queue_commands_from_cert(p, &push_cert); + + return commands; +} + +static void read_push_options(struct string_list *options) +{ + while (1) { + char *line; + int len; + + line = packet_read_line(0, &len); + + if (!line) + break; + + string_list_append(options, 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 const char *pack_lockfile; + +static void push_header_arg(struct argv_array *args, struct pack_header *hdr) +{ + argv_array_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); + argv_array_push(&child.args, "--shallow-file"); + argv_array_push(&child.args, alt_shallow_file); + } + + tmp_objdir = tmp_objdir_create(); + if (!tmp_objdir) { + if (err_fd > 0) + close(err_fd); + return "unable to create temporary object directory"; + } + 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) { + argv_array_push(&child.args, "unpack-objects"); + push_header_arg(&child.args, &hdr); + if (quiet) + argv_array_push(&child.args, "-q"); + if (fsck_objects) + argv_array_pushf(&child.args, "--strict%s", + fsck_msg_types.buf); + if (max_input_size) + argv_array_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]; + + argv_array_pushl(&child.args, "index-pack", "--stdin", NULL); + push_header_arg(&child.args, &hdr); + + if (xgethostname(hostname, sizeof(hostname))) + xsnprintf(hostname, sizeof(hostname), "localhost"); + argv_array_pushf(&child.args, + "--keep=receive-pack %"PRIuMAX" on %s", + (uintmax_t)getpid(), + hostname); + + if (!quiet && err_fd) + argv_array_push(&child.args, "--show-resolving-progress"); + if (use_sideband) + argv_array_push(&child.args, "--report-end-of-input"); + if (fsck_objects) + argv_array_pushf(&child.args, "--strict%s", + fsck_msg_types.buf); + if (!reject_thin) + argv_array_push(&child.args, "--fix-thin"); + if (max_input_size) + argv_array_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"; + pack_lockfile = index_pack_lockfile(child.out); + close(child.out); + status = finish_command(&child); + if (status) + return "index-pack abnormal exit"; + reprepare_packed_git(); + } + 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 command *commands, + 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); + + si->need_reachability_test = + xcalloc(si->shallow->nr, sizeof(*si->need_reachability_test)); + si->reachable = + xcalloc(si->shallow->nr, sizeof(*si->reachable)); + si->shallow_ref = xcalloc(si->ref->nr, sizeof(*si->shallow_ref)); + + 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_everything_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(commands, 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 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 option options[] = { + OPT__QUIET(&quiet, N_("quiet")), + OPT_HIDDEN_BOOL(0, "stateless-rpc", &stateless_rpc, NULL), + OPT_HIDDEN_BOOL(0, "advertise-refs", &advertise_refs, NULL), + 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 <= transfer_unpack_limit) + unpack_limit = transfer_unpack_limit; + else if (0 <= receive_unpack_limit) + unpack_limit = receive_unpack_limit; + + if (advertise_refs || !stateless_rpc) { + write_head_info(); + } + if (advertise_refs) + return 0; + + if ((commands = read_head_info(&shallow)) != NULL) { + const char *unpack_status = NULL; + struct string_list push_options = STRING_LIST_INIT_DUP; + + if (use_push_options) + read_push_options(&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); + if (pack_lockfile) + unlink_or_warn(pack_lockfile); + if (report_status) + report(commands, unpack_status); + run_receive_hook(commands, "post-receive", 1, + &push_options); + run_update_post_hook(commands); + string_list_clear(&push_options, 0); + if (auto_gc) { + const char *argv_gc_auto[] = { + "gc", "--auto", "--quiet", NULL, + }; + 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 = 1; + proc.argv = argv_gc_auto; + + close_all_packs(); + 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); + free((void *)push_cert_nonce); + return 0; +} diff --git a/builtin/reflog.c b/builtin/reflog.c new file mode 100644 index 0000000000..2067cca5b1 --- /dev/null +++ b/builtin/reflog.c @@ -0,0 +1,751 @@ +#include "builtin.h" +#include "config.h" +#include "lockfile.h" +#include "commit.h" +#include "refs.h" +#include "dir.h" +#include "tree-walk.h" +#include "diff.h" +#include "revision.h" +#include "reachable.h" + +/* NEEDSWORK: switch to using parse_options */ +static const char reflog_expire_usage[] = +"git reflog expire [--expire=<time>] [--expire-unreachable=<time>] [--rewrite] [--updateref] [--stale-fix] [--dry-run | -n] [--verbose] [--all] <refs>..."; +static const char reflog_delete_usage[] = +"git reflog delete [--rewrite] [--updateref] [--dry-run | -n] [--verbose] <refs>..."; +static const char reflog_exists_usage[] = +"git reflog exists <ref>"; + +static timestamp_t default_reflog_expire; +static timestamp_t default_reflog_expire_unreachable; + +struct cmd_reflog_expire_cb { + struct rev_info revs; + int stalefix; + timestamp_t expire_total; + timestamp_t expire_unreachable; + int recno; +}; + +struct expire_reflog_policy_cb { + enum { + UE_NORMAL, + UE_ALWAYS, + UE_HEAD + } unreachable_expire_kind; + struct commit_list *mark_list; + unsigned long mark_limit; + struct cmd_reflog_expire_cb cmd; + struct commit *tip_commit; + struct commit_list *tips; +}; + +struct collected_reflog { + unsigned char sha1[20]; + char reflog[FLEX_ARRAY]; +}; + +struct collect_reflog_cb { + struct collected_reflog **e; + int alloc; + int nr; +}; + +#define INCOMPLETE (1u<<10) +#define STUDYING (1u<<11) +#define REACHABLE (1u<<12) + +static int tree_is_complete(const struct object_id *oid) +{ + struct tree_desc desc; + struct name_entry entry; + int complete; + struct tree *tree; + + tree = lookup_tree(oid); + if (!tree) + return 0; + if (tree->object.flags & SEEN) + return 1; + if (tree->object.flags & INCOMPLETE) + return 0; + + if (!tree->buffer) { + enum object_type type; + unsigned long size; + void *data = read_sha1_file(oid->hash, &type, &size); + if (!data) { + tree->object.flags |= INCOMPLETE; + return 0; + } + tree->buffer = data; + tree->size = size; + } + init_tree_desc(&desc, tree->buffer, tree->size); + complete = 1; + while (tree_entry(&desc, &entry)) { + if (!has_sha1_file(entry.oid->hash) || + (S_ISDIR(entry.mode) && !tree_is_complete(entry.oid))) { + tree->object.flags |= INCOMPLETE; + complete = 0; + } + } + free_tree_buffer(tree); + + if (complete) + tree->object.flags |= SEEN; + return complete; +} + +static int commit_is_complete(struct commit *commit) +{ + struct object_array study; + struct object_array found; + int is_incomplete = 0; + int i; + + /* early return */ + if (commit->object.flags & SEEN) + return 1; + if (commit->object.flags & INCOMPLETE) + return 0; + /* + * Find all commits that are reachable and are not marked as + * SEEN. Then make sure the trees and blobs contained are + * complete. After that, mark these commits also as SEEN. + * If some of the objects that are needed to complete this + * commit are missing, mark this commit as INCOMPLETE. + */ + memset(&study, 0, sizeof(study)); + memset(&found, 0, sizeof(found)); + add_object_array(&commit->object, NULL, &study); + add_object_array(&commit->object, NULL, &found); + commit->object.flags |= STUDYING; + while (study.nr) { + struct commit *c; + struct commit_list *parent; + + c = (struct commit *)object_array_pop(&study); + if (!c->object.parsed && !parse_object(&c->object.oid)) + c->object.flags |= INCOMPLETE; + + if (c->object.flags & INCOMPLETE) { + is_incomplete = 1; + break; + } + else if (c->object.flags & SEEN) + continue; + for (parent = c->parents; parent; parent = parent->next) { + struct commit *p = parent->item; + if (p->object.flags & STUDYING) + continue; + p->object.flags |= STUDYING; + add_object_array(&p->object, NULL, &study); + add_object_array(&p->object, NULL, &found); + } + } + if (!is_incomplete) { + /* + * make sure all commits in "found" array have all the + * necessary objects. + */ + for (i = 0; i < found.nr; i++) { + struct commit *c = + (struct commit *)found.objects[i].item; + if (!tree_is_complete(&c->tree->object.oid)) { + is_incomplete = 1; + c->object.flags |= INCOMPLETE; + } + } + if (!is_incomplete) { + /* mark all found commits as complete, iow SEEN */ + for (i = 0; i < found.nr; i++) + found.objects[i].item->flags |= SEEN; + } + } + /* clear flags from the objects we traversed */ + for (i = 0; i < found.nr; i++) + found.objects[i].item->flags &= ~STUDYING; + if (is_incomplete) + commit->object.flags |= INCOMPLETE; + else { + /* + * If we come here, we have (1) traversed the ancestry chain + * from the "commit" until we reach SEEN commits (which are + * known to be complete), and (2) made sure that the commits + * encountered during the above traversal refer to trees that + * are complete. Which means that we know *all* the commits + * we have seen during this process are complete. + */ + for (i = 0; i < found.nr; i++) + found.objects[i].item->flags |= SEEN; + } + /* free object arrays */ + object_array_clear(&study); + object_array_clear(&found); + return !is_incomplete; +} + +static int keep_entry(struct commit **it, struct object_id *oid) +{ + struct commit *commit; + + if (is_null_oid(oid)) + return 1; + commit = lookup_commit_reference_gently(oid, 1); + if (!commit) + return 0; + + /* + * Make sure everything in this commit exists. + * + * We have walked all the objects reachable from the refs + * and cache earlier. The commits reachable by this commit + * must meet SEEN commits -- and then we should mark them as + * SEEN as well. + */ + if (!commit_is_complete(commit)) + return 0; + *it = commit; + return 1; +} + +/* + * Starting from commits in the cb->mark_list, mark commits that are + * reachable from them. Stop the traversal at commits older than + * the expire_limit and queue them back, so that the caller can call + * us again to restart the traversal with longer expire_limit. + */ +static void mark_reachable(struct expire_reflog_policy_cb *cb) +{ + struct commit_list *pending; + timestamp_t expire_limit = cb->mark_limit; + struct commit_list *leftover = NULL; + + for (pending = cb->mark_list; pending; pending = pending->next) + pending->item->object.flags &= ~REACHABLE; + + pending = cb->mark_list; + while (pending) { + struct commit_list *parent; + struct commit *commit = pop_commit(&pending); + if (commit->object.flags & REACHABLE) + continue; + if (parse_commit(commit)) + continue; + commit->object.flags |= REACHABLE; + if (commit->date < expire_limit) { + commit_list_insert(commit, &leftover); + continue; + } + commit->object.flags |= REACHABLE; + parent = commit->parents; + while (parent) { + commit = parent->item; + parent = parent->next; + if (commit->object.flags & REACHABLE) + continue; + commit_list_insert(commit, &pending); + } + } + cb->mark_list = leftover; +} + +static int unreachable(struct expire_reflog_policy_cb *cb, struct commit *commit, struct object_id *oid) +{ + /* + * We may or may not have the commit yet - if not, look it + * up using the supplied sha1. + */ + if (!commit) { + if (is_null_oid(oid)) + return 0; + + commit = lookup_commit_reference_gently(oid, 1); + + /* Not a commit -- keep it */ + if (!commit) + return 0; + } + + /* Reachable from the current ref? Don't prune. */ + if (commit->object.flags & REACHABLE) + return 0; + + if (cb->mark_list && cb->mark_limit) { + cb->mark_limit = 0; /* dig down to the root */ + mark_reachable(cb); + } + + return !(commit->object.flags & REACHABLE); +} + +/* + * Return true iff the specified reflog entry should be expired. + */ +static int should_expire_reflog_ent(struct object_id *ooid, struct object_id *noid, + const char *email, timestamp_t timestamp, int tz, + const char *message, void *cb_data) +{ + struct expire_reflog_policy_cb *cb = cb_data; + struct commit *old, *new; + + if (timestamp < cb->cmd.expire_total) + return 1; + + old = new = NULL; + if (cb->cmd.stalefix && + (!keep_entry(&old, ooid) || !keep_entry(&new, noid))) + return 1; + + if (timestamp < cb->cmd.expire_unreachable) { + if (cb->unreachable_expire_kind == UE_ALWAYS) + return 1; + if (unreachable(cb, old, ooid) || unreachable(cb, new, noid)) + return 1; + } + + if (cb->cmd.recno && --(cb->cmd.recno) == 0) + return 1; + + return 0; +} + +static int push_tip_to_list(const char *refname, const struct object_id *oid, + int flags, void *cb_data) +{ + struct commit_list **list = cb_data; + struct commit *tip_commit; + if (flags & REF_ISSYMREF) + return 0; + tip_commit = lookup_commit_reference_gently(oid, 1); + if (!tip_commit) + return 0; + commit_list_insert(tip_commit, list); + return 0; +} + +static void reflog_expiry_prepare(const char *refname, + const struct object_id *oid, + void *cb_data) +{ + struct expire_reflog_policy_cb *cb = cb_data; + + if (!cb->cmd.expire_unreachable || !strcmp(refname, "HEAD")) { + cb->tip_commit = NULL; + cb->unreachable_expire_kind = UE_HEAD; + } else { + cb->tip_commit = lookup_commit_reference_gently(oid, 1); + if (!cb->tip_commit) + cb->unreachable_expire_kind = UE_ALWAYS; + else + cb->unreachable_expire_kind = UE_NORMAL; + } + + if (cb->cmd.expire_unreachable <= cb->cmd.expire_total) + cb->unreachable_expire_kind = UE_ALWAYS; + + cb->mark_list = NULL; + cb->tips = NULL; + if (cb->unreachable_expire_kind != UE_ALWAYS) { + if (cb->unreachable_expire_kind == UE_HEAD) { + struct commit_list *elem; + + for_each_ref(push_tip_to_list, &cb->tips); + for (elem = cb->tips; elem; elem = elem->next) + commit_list_insert(elem->item, &cb->mark_list); + } else { + commit_list_insert(cb->tip_commit, &cb->mark_list); + } + cb->mark_limit = cb->cmd.expire_total; + mark_reachable(cb); + } +} + +static void reflog_expiry_cleanup(void *cb_data) +{ + struct expire_reflog_policy_cb *cb = cb_data; + + if (cb->unreachable_expire_kind != UE_ALWAYS) { + if (cb->unreachable_expire_kind == UE_HEAD) { + struct commit_list *elem; + for (elem = cb->tips; elem; elem = elem->next) + clear_commit_marks(elem->item, REACHABLE); + free_commit_list(cb->tips); + } else { + clear_commit_marks(cb->tip_commit, REACHABLE); + } + } +} + +static int collect_reflog(const char *ref, const struct object_id *oid, int unused, void *cb_data) +{ + struct collected_reflog *e; + struct collect_reflog_cb *cb = cb_data; + + FLEX_ALLOC_STR(e, reflog, ref); + hashcpy(e->sha1, oid->hash); + ALLOC_GROW(cb->e, cb->nr + 1, cb->alloc); + cb->e[cb->nr++] = e; + 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; +} + +static int parse_expire_cfg_value(const char *var, const char *value, timestamp_t *expire) +{ + if (!value) + return config_error_nonbool(var); + if (parse_expiry_date(value, expire)) + return error(_("'%s' for '%s' is not a valid timestamp"), + value, var); + return 0; +} + +/* expiry timer slot */ +#define EXPIRE_TOTAL 01 +#define EXPIRE_UNREACH 02 + +static int reflog_expire_config(const char *var, const char *value, void *cb) +{ + const char *pattern, *key; + int 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, cb); + + if (!strcmp(key, "reflogexpire")) { + slot = EXPIRE_TOTAL; + if (parse_expire_cfg_value(var, value, &expire)) + return -1; + } else if (!strcmp(key, "reflogexpireunreachable")) { + slot = EXPIRE_UNREACH; + if (parse_expire_cfg_value(var, value, &expire)) + return -1; + } else + return git_default_config(var, value, 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, int slot, const char *ref) +{ + struct reflog_expire_cfg *ent; + + if (slot == (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 (!(slot & EXPIRE_TOTAL)) + cb->expire_total = ent->expire_total; + if (!(slot & EXPIRE_UNREACH)) + cb->expire_unreachable = ent->expire_unreachable; + return; + } + } + + /* + * If unconfigured, make stash never expire + */ + if (!strcmp(ref, "refs/stash")) { + if (!(slot & EXPIRE_TOTAL)) + cb->expire_total = 0; + if (!(slot & EXPIRE_UNREACH)) + cb->expire_unreachable = 0; + return; + } + + /* Nothing matched -- use the default value */ + if (!(slot & EXPIRE_TOTAL)) + cb->expire_total = default_reflog_expire; + if (!(slot & EXPIRE_UNREACH)) + cb->expire_unreachable = default_reflog_expire_unreachable; +} + +static int cmd_reflog_expire(int argc, const char **argv, const char *prefix) +{ + struct expire_reflog_policy_cb cb; + timestamp_t now = time(NULL); + int i, status, do_all; + int explicit_expiry = 0; + unsigned int flags = 0; + + 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; + memset(&cb, 0, sizeof(cb)); + + cb.cmd.expire_total = default_reflog_expire; + cb.cmd.expire_unreachable = default_reflog_expire_unreachable; + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (!strcmp(arg, "--dry-run") || !strcmp(arg, "-n")) + flags |= EXPIRE_REFLOGS_DRY_RUN; + else if (starts_with(arg, "--expire=")) { + if (parse_expiry_date(arg + 9, &cb.cmd.expire_total)) + die(_("'%s' is not a valid timestamp"), arg); + explicit_expiry |= EXPIRE_TOTAL; + } + else if (starts_with(arg, "--expire-unreachable=")) { + if (parse_expiry_date(arg + 21, &cb.cmd.expire_unreachable)) + die(_("'%s' is not a valid timestamp"), arg); + explicit_expiry |= EXPIRE_UNREACH; + } + else if (!strcmp(arg, "--stale-fix")) + cb.cmd.stalefix = 1; + else if (!strcmp(arg, "--rewrite")) + flags |= EXPIRE_REFLOGS_REWRITE; + else if (!strcmp(arg, "--updateref")) + flags |= EXPIRE_REFLOGS_UPDATE_REF; + else if (!strcmp(arg, "--all")) + do_all = 1; + else if (!strcmp(arg, "--verbose")) + flags |= EXPIRE_REFLOGS_VERBOSE; + else if (!strcmp(arg, "--")) { + i++; + break; + } + else if (arg[0] == '-') + usage(reflog_expire_usage); + else + break; + } + + /* + * 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 (cb.cmd.stalefix) { + init_revisions(&cb.cmd.revs, prefix); + if (flags & EXPIRE_REFLOGS_VERBOSE) + printf("Marking reachable objects..."); + mark_reachable_objects(&cb.cmd.revs, 0, 0, NULL); + if (flags & EXPIRE_REFLOGS_VERBOSE) + putchar('\n'); + } + + if (do_all) { + struct collect_reflog_cb collected; + int i; + + memset(&collected, 0, sizeof(collected)); + for_each_reflog(collect_reflog, &collected); + for (i = 0; i < collected.nr; i++) { + struct collected_reflog *e = collected.e[i]; + set_reflog_expiry_param(&cb.cmd, explicit_expiry, e->reflog); + status |= reflog_expire(e->reflog, e->sha1, flags, + reflog_expiry_prepare, + should_expire_reflog_ent, + reflog_expiry_cleanup, + &cb); + free(e); + } + free(collected.e); + } + + for (; i < argc; i++) { + char *ref; + unsigned char sha1[20]; + if (!dwim_log(argv[i], strlen(argv[i]), sha1, &ref)) { + status |= error("%s points nowhere!", argv[i]); + continue; + } + set_reflog_expiry_param(&cb.cmd, explicit_expiry, ref); + status |= reflog_expire(ref, sha1, flags, + reflog_expiry_prepare, + should_expire_reflog_ent, + reflog_expiry_cleanup, + &cb); + } + return status; +} + +static int count_reflog_ent(struct object_id *ooid, struct object_id *noid, + const char *email, timestamp_t timestamp, int tz, + const char *message, void *cb_data) +{ + struct expire_reflog_policy_cb *cb = cb_data; + if (!cb->cmd.expire_total || timestamp < cb->cmd.expire_total) + cb->cmd.recno++; + return 0; +} + +static int cmd_reflog_delete(int argc, const char **argv, const char *prefix) +{ + struct expire_reflog_policy_cb cb; + int i, status = 0; + unsigned int flags = 0; + + memset(&cb, 0, sizeof(cb)); + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (!strcmp(arg, "--dry-run") || !strcmp(arg, "-n")) + flags |= EXPIRE_REFLOGS_DRY_RUN; + else if (!strcmp(arg, "--rewrite")) + flags |= EXPIRE_REFLOGS_REWRITE; + else if (!strcmp(arg, "--updateref")) + flags |= EXPIRE_REFLOGS_UPDATE_REF; + else if (!strcmp(arg, "--verbose")) + flags |= EXPIRE_REFLOGS_VERBOSE; + else if (!strcmp(arg, "--")) { + i++; + break; + } + else if (arg[0] == '-') + usage(reflog_delete_usage); + else + break; + } + + if (argc - i < 1) + return error("Nothing to delete?"); + + for ( ; i < argc; i++) { + const char *spec = strstr(argv[i], "@{"); + unsigned char sha1[20]; + char *ep, *ref; + int recno; + + if (!spec) { + status |= error("Not a reflog: %s", argv[i]); + continue; + } + + if (!dwim_log(argv[i], spec - argv[i], sha1, &ref)) { + status |= error("no reflog for '%s'", argv[i]); + continue; + } + + recno = strtoul(spec + 2, &ep, 10); + if (*ep == '}') { + cb.cmd.recno = -recno; + for_each_reflog_ent(ref, count_reflog_ent, &cb); + } else { + cb.cmd.expire_total = approxidate(spec + 2); + for_each_reflog_ent(ref, count_reflog_ent, &cb); + cb.cmd.expire_total = 0; + } + + status |= reflog_expire(ref, sha1, flags, + reflog_expiry_prepare, + should_expire_reflog_ent, + reflog_expiry_cleanup, + &cb); + free(ref); + } + return status; +} + +static int cmd_reflog_exists(int argc, const char **argv, const char *prefix) +{ + int i, start = 0; + + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (!strcmp(arg, "--")) { + i++; + break; + } + else if (arg[0] == '-') + usage(reflog_exists_usage); + else + break; + } + + start = i; + + if (argc - start != 1) + usage(reflog_exists_usage); + + if (check_refname_format(argv[start], REFNAME_ALLOW_ONELEVEL)) + die("invalid ref format: %s", argv[start]); + return !reflog_exists(argv[start]); +} + +/* + * main "reflog" + */ + +static const char reflog_usage[] = +"git reflog [ show | expire | delete | exists ]"; + +int cmd_reflog(int argc, const char **argv, const char *prefix) +{ + if (argc > 1 && !strcmp(argv[1], "-h")) + usage(reflog_usage); + + /* With no command, we default to showing it. */ + if (argc < 2 || *argv[1] == '-') + return cmd_log_reflog(argc, argv, prefix); + + if (!strcmp(argv[1], "show")) + return cmd_log_reflog(argc - 1, argv + 1, prefix); + + if (!strcmp(argv[1], "expire")) + return cmd_reflog_expire(argc - 1, argv + 1, prefix); + + if (!strcmp(argv[1], "delete")) + return cmd_reflog_delete(argc - 1, argv + 1, prefix); + + if (!strcmp(argv[1], "exists")) + return cmd_reflog_exists(argc - 1, argv + 1, prefix); + + return cmd_log_reflog(argc, argv, prefix); +} diff --git a/builtin/remote-ext.c b/builtin/remote-ext.c new file mode 100644 index 0000000000..6a9127a33c --- /dev/null +++ b/builtin/remote-ext.c @@ -0,0 +1,202 @@ +#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 argv_array *out, const char *arg, const char *service) +{ + while (*arg) { + char *expanded = strip_escapes(arg, service, &arg); + if (expanded) + argv_array_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; + 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 (!strncmp(buffer, "connect ", 8)) { + printf("\n"); + fflush(stdout); + return run_child(child, buffer + 8); + } else { + fprintf(stderr, "Bad command"); + return 1; + } + } +} + +int cmd_remote_ext(int argc, const char **argv, const char *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 0000000000..91dfe07e06 --- /dev/null +++ b/builtin/remote-fd.c @@ -0,0 +1,82 @@ +#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 (!strncmp(buffer, "connect ", 8)) { + 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; + + 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 0000000000..4f5cac96b0 --- /dev/null +++ b/builtin/remote.c @@ -0,0 +1,1634 @@ +#include "builtin.h" +#include "config.h" +#include "parse-options.h" +#include "transport.h" +#include "remote.h" +#include "string-list.h" +#include "strbuf.h" +#include "run-command.h" +#include "refs.h" +#include "argv-array.h" + +static const char * const builtin_remote_usage[] = { + N_("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 <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 <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) +{ + const char *argv[] = { "fetch", name, NULL, NULL }; + if (verbose) { + argv[1] = "-v"; + argv[2] = name; + } + printf_ln(_("Updating %s"), name); + if (run_command_v_opt(argv, RUN_GIT_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) +{ + 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"), + TAGS_SET), + OPT_SET_INT(0, NULL, &fetch_tags, + N_("or do not fetch any tag at all (--no-tags)"), TAGS_UNSET), + OPT_STRING_LIST('t', "track", &track, N_("branch"), + N_("branch(es) to track")), + OPT_STRING('m', "master", &master, N_("branch"), N_("master branch")), + { OPTION_CALLBACK, 0, "mirror", &mirror, N_("push|fetch"), + N_("set up remote as a mirror to push to or fetch from"), + PARSE_OPT_OPTARG, parse_mirror_opt }, + OPT_END() + }; + + argc = parse_options(argc, argv, NULL, 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)) + die(_("remote %s already exists."), name); + + strbuf_addf(&buf2, "refs/heads/test:refs/remotes/%s/test", name); + if (!valid_fetch_refspec(buf2.buf)) + 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 { NO_REBASE, NORMAL_REBASE, INTERACTIVE_REBASE } rebase; +}; + +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, void *cb) +{ + if (starts_with(key, "branch.")) { + const char *orig_key = key; + char *name; + struct string_list_item *item; + struct branch_info *info; + enum { REMOTE, MERGE, REBASE } type; + size_t key_len; + + key += 7; + if (strip_suffix(key, ".remote", &key_len)) { + name = xmemdupz(key, key_len); + type = REMOTE; + } else if (strip_suffix(key, ".merge", &key_len)) { + name = xmemdupz(key, key_len); + type = MERGE; + } else if (strip_suffix(key, ".rebase", &key_len)) { + name = xmemdupz(key, key_len); + type = REBASE; + } else + return 0; + + item = string_list_insert(&branch_list, name); + + if (!item->util) + item->util = xcalloc(1, sizeof(struct branch_info)); + info = item->util; + if (type == REMOTE) { + if (info->remote_name) + warning(_("more than one %s"), orig_key); + info->remote_name = xstrdup(value); + } else if (type == 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)); + } else { + int v = git_parse_maybe_bool(value); + if (v >= 0) + info->rebase = v; + else if (!strcmp(value, "preserve")) + info->rebase = NORMAL_REBASE; + else if (!strcmp(value, "interactive")) + info->rebase = INTERACTIVE_REBASE; + } + } + 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, stale, tracked, heads, push; + int queried; +}; + +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_refspec_nr; i++) + if (get_fetch_map(remote_refs, states->remote->fetch + i, &tail, 1)) + die(_("Could not get fetch map for refspec %s"), + states->remote->fetch_refspec[i]); + + states->new.strdup_strings = 1; + states->tracked.strdup_strings = 1; + states->stale.strdup_strings = 1; + for (ref = fetch_map; ref; ref = ref->next) { + if (!ref->peer_ref || !ref_exists(ref->peer_ref->name)) + string_list_append(&states->new, abbrev_branch(ref->name)); + else + string_list_append(&states->tracked, abbrev_branch(ref->name)); + } + stale_refs = get_stale_heads(states->remote->fetch, + states->remote->fetch_refspec_nr, 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); + 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_refspec_nr, + remote->push_refspec, MATCH_REFS_NONE); + + states->push.strdup_strings = 1; + 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 (!oidcmp(&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 (has_object_file(&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; + + states->push.strdup_strings = 1; + if (!remote->push_refspec_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_refspec_nr; i++) { + struct refspec *spec = remote->push + 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 refspec; + + refspec.force = 0; + refspec.pattern = 1; + refspec.src = refspec.dst = "refs/heads/*"; + states->heads.strdup_strings = 1; + 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, int flags, void *cb_data) +{ + struct branches_for_remote *branches = cb_data; + struct refspec 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; + const char *new; + struct string_list *remote_branches; +}; + +static int read_remote_branches(const char *refname, + const struct object_id *oid, int flags, 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); + if (starts_with(refname, buf.buf)) { + item = string_list_append(rename->remote_branches, xstrdup(refname)); + symref = resolve_ref_unsafe(refname, RESOLVE_REF_READING, + NULL, &flag); + if (flag & REF_ISSYMREF) + item->util = xstrdup(symref); + 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_refspec_nr; i++) + git_config_set_multivar(buf.buf, remote->push_refspec[i], "^$", 0); + strbuf_reset(&buf); + strbuf_addf(&buf, "remote.%s.fetch", remote->name); + for (i = 0; i < remote->fetch_refspec_nr; i++) + git_config_set_multivar(buf.buf, remote->fetch_refspec[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; +} + +static int mv(int argc, const char **argv) +{ + struct option options[] = { + 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_NODUP; + struct rename_info rename; + int i, refspec_updated = 0; + + if (argc != 3) + usage_with_options(builtin_remote_rename_usage, options); + + rename.old = argv[1]; + rename.new = argv[2]; + rename.remote_branches = &remote_branches; + + oldremote = remote_get(rename.old); + if (!remote_is_configured(oldremote, 1)) + die(_("No such remote: %s"), rename.old); + + if (!strcmp(rename.old, rename.new) && oldremote->origin != REMOTE_CONFIG) + return migrate_file(oldremote); + + newremote = remote_get(rename.new); + if (remote_is_configured(newremote, 1)) + die(_("remote %s already exists."), rename.new); + + strbuf_addf(&buf, "refs/heads/test:refs/remotes/%s/test", rename.new); + if (!valid_fetch_refspec(buf.buf)) + die(_("'%s' is not a valid remote name"), rename.new); + + strbuf_reset(&buf); + strbuf_addf(&buf, "remote.%s", rename.old); + strbuf_addf(&buf2, "remote.%s", rename.new); + if (git_config_rename_section(buf.buf, buf2.buf) < 1) + return error(_("Could not rename config section '%s' to '%s'"), + buf.buf, buf2.buf); + + strbuf_reset(&buf); + strbuf_addf(&buf, "remote.%s.fetch", rename.new); + git_config_set_multivar(buf.buf, NULL, NULL, 1); + strbuf_addf(&old_remote_context, ":refs/remotes/%s/", rename.old); + for (i = 0; i < oldremote->fetch_refspec_nr; i++) { + char *ptr; + + strbuf_reset(&buf2); + strbuf_addstr(&buf2, oldremote->fetch_refspec[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), rename.new, + strlen(rename.new)); + } 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)) { + strbuf_reset(&buf); + strbuf_addf(&buf, "branch.%s.remote", item->string); + git_config_set(buf.buf, rename.new); + } + } + + if (!refspec_updated) + return 0; + + /* + * First remove symrefs, then rename the rest, finally create + * the new symrefs. + */ + for_each_ref(read_remote_branches, &rename); + for (i = 0; i < remote_branches.nr; i++) { + struct string_list_item *item = remote_branches.items + i; + int flag = 0; + struct object_id oid; + + read_ref_full(item->string, RESOLVE_REF_READING, oid.hash, &flag); + if (!(flag & REF_ISSYMREF)) + continue; + if (delete_ref(NULL, item->string, NULL, REF_NODEREF)) + die(_("deleting '%s' failed"), item->string); + } + 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), + rename.new, strlen(rename.new)); + 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); + } + 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), + rename.new, strlen(rename.new)); + strbuf_reset(&buf2); + strbuf_addstr(&buf2, item->util); + strbuf_splice(&buf2, strlen("refs/remotes/"), strlen(rename.old), + rename.new, strlen(rename.new)); + 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); + } + return 0; +} + +static int rm(int argc, const char **argv) +{ + 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; + + if (argc != 2) + usage_with_options(builtin_remote_rm_usage, options); + + remote = remote_get(argv[1]); + if (!remote_is_configured(remote, 1)) + die(_("No such remote: %s"), argv[1]); + + 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); + } + } + } + + /* + * 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_NODEREF); + 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); + } + + return result; +} + +static void clear_push_info(void *util, const char *string) +{ + 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, 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, int flags, void *cb_data) +{ + struct ref_states *states = cb_data; + struct refspec 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) +{ + struct transport *transport; + const struct ref *remote_refs; + + states->remote = remote_get(name); + if (!states->remote) + return error(_("No such remote: %s"), name); + + read_branches(); + + if (query) { + transport = transport_get(states->remote, states->remote->url_nr > 0 ? + states->remote->url[0] : NULL); + remote_refs = transport_get_remote_refs(transport); + transport_disconnect(transport); + + 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); + } 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; +}; + +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, 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->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) + 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 && 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) { + printf_ln(branch_info->rebase == INTERACTIVE_REBASE + ? _("rebases interactively onto remote %s") + : _("rebases onto remote %s"), 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 url_buf = STRBUF_INIT; + const char **url; + int i, url_nr; + + if (remote->url_nr > 0) { + strbuf_addf(&url_buf, "%s (fetch)", remote->url[0]); + string_list_append(list, remote->name)->util = + strbuf_detach(&url_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(&url_buf, "%s (push)", url[i]); + string_list_append(list, remote->name)->util = + strbuf_detach(&url_buf, NULL); + } + + return 0; +} + +static int show_all(void) +{ + struct string_list list = STRING_LIST_INIT_NODUP; + int result; + + list.strdup_strings = 1; + 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) +{ + 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 ref_states states; + struct string_list info_list = STRING_LIST_INIT_NODUP; + struct show_info info; + + argc = parse_options(argc, argv, NULL, 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); + + memset(&states, 0, sizeof(states)); + memset(&info, 0, sizeof(info)); + info.states = &states; + info.list = &info_list; + for (; argc; argc--, argv++) { + int i; + const char **url; + int url_nr; + + get_remote_ref_states(*argv, &states, query_flag); + + printf_ln(_("* remote %s"), *argv); + printf_ln(_(" Fetch URL: %s"), states.remote->url_nr > 0 ? + states.remote->url[0] : _("(no URL)")); + if (states.remote->pushurl_nr) { + url = states.remote->pushurl; + url_nr = states.remote->pushurl_nr; + } else { + url = states.remote->url; + url_nr = 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 (!states.heads.nr) + printf_ln(_(" HEAD branch: %s"), _("(unknown)")); + else if (states.heads.nr == 1) + printf_ln(_(" HEAD branch: %s"), states.heads.items[0].string); + else { + printf(_(" HEAD branch (remote HEAD is ambiguous," + " may be one of the following):\n")); + for (i = 0; i < states.heads.nr; i++) + printf(" %s\n", states.heads.items[i].string); + } + + /* remote branch info */ + info.width = 0; + for_each_string_list(&states.new, add_remote_to_show_info, &info); + for_each_string_list(&states.tracked, add_remote_to_show_info, &info); + for_each_string_list(&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 (states.remote->mirror) + printf_ln(_(" Local refs will be mirrored by 'git push'")); + + info.width = info.width2 = 0; + for_each_string_list(&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(&states); + } + + return result; +} + +static int set_head(int argc, const char **argv) +{ + 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, NULL, 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; + memset(&states, 0, sizeof(states)); + 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_NODEREF)) + 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); + 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; + 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!"); + + memset(&states, 0, sizeof(states)); + 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) +{ + int dry_run = 0, result = 0; + struct option options[] = { + OPT__DRY_RUN(&dry_run, N_("dry run")), + OPT_END() + }; + + argc = parse_options(argc, argv, NULL, 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, void *priv) +{ + if (strcmp(key, "remotes.default") == 0) { + int *found = priv; + *found = 1; + } + return 0; +} + +static int update(int argc, const char **argv) +{ + int i, prune = -1; + struct option options[] = { + OPT_BOOL('p', "prune", &prune, + N_("prune remotes after fetching")), + OPT_END() + }; + struct argv_array fetch_argv = ARGV_ARRAY_INIT; + int default_defined = 0; + int retval; + + argc = parse_options(argc, argv, NULL, options, builtin_remote_update_usage, + PARSE_OPT_KEEP_ARGV0); + + argv_array_push(&fetch_argv, "fetch"); + + if (prune != -1) + argv_array_push(&fetch_argv, prune ? "--prune" : "--no-prune"); + if (verbose) + argv_array_push(&fetch_argv, "-v"); + argv_array_push(&fetch_argv, "--multiple"); + if (argc < 2) + argv_array_push(&fetch_argv, "default"); + for (i = 1; i < argc; i++) + argv_array_push(&fetch_argv, argv[i]); + + if (strcmp(fetch_argv.argv[fetch_argv.argc-1], "default") == 0) { + git_config(get_remote_default, &default_defined); + if (!default_defined) { + argv_array_pop(&fetch_argv); + argv_array_push(&fetch_argv, "--all"); + } + } + + retval = run_command_v_opt(fetch_argv.argv, RUN_GIT_CMD); + argv_array_clear(&fetch_argv); + return retval; +} + +static int remove_all_fetch_refspecs(const char *remote, const char *key) +{ + return git_config_set_multivar_gently(key, NULL, NULL, 1); +} + +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)) + die(_("No such remote '%s'"), remotename); + + if (!add_mode && remove_all_fetch_refspecs(remotename, 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) +{ + int add_mode = 0; + struct option options[] = { + OPT_BOOL('\0', "add", &add_mode, N_("add branch")), + OPT_END() + }; + + argc = parse_options(argc, argv, NULL, 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) +{ + 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, NULL, 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)) + die(_("No such remote '%s'"), remotename); + + 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) +{ + 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, NULL, 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)) + die(_("No such remote '%s'"), remotename); + + 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, 1); +out: + strbuf_release(&name_buf); + return 0; +} + +int cmd_remote(int argc, const char **argv, const char *prefix) +{ + struct option options[] = { + OPT__VERBOSE(&verbose, N_("be verbose; must be placed before a subcommand")), + OPT_END() + }; + int result; + + argc = parse_options(argc, argv, prefix, options, builtin_remote_usage, + PARSE_OPT_STOP_AT_NON_OPTION); + + if (argc < 1) + result = show_all(); + else if (!strcmp(argv[0], "add")) + result = add(argc, argv); + else if (!strcmp(argv[0], "rename")) + result = mv(argc, argv); + else if (!strcmp(argv[0], "rm") || !strcmp(argv[0], "remove")) + result = rm(argc, argv); + else if (!strcmp(argv[0], "set-head")) + result = set_head(argc, argv); + else if (!strcmp(argv[0], "set-branches")) + result = set_branches(argc, argv); + else if (!strcmp(argv[0], "get-url")) + result = get_url(argc, argv); + else if (!strcmp(argv[0], "set-url")) + result = set_url(argc, argv); + else if (!strcmp(argv[0], "show")) + result = show(argc, argv); + else if (!strcmp(argv[0], "prune")) + result = prune(argc, argv); + else if (!strcmp(argv[0], "update")) + result = update(argc, argv); + else { + error(_("Unknown subcommand: %s"), argv[0]); + usage_with_options(builtin_remote_usage, options); + } + + return result ? 1 : 0; +} diff --git a/builtin/repack.c b/builtin/repack.c new file mode 100644 index 0000000000..f17a68a17d --- /dev/null +++ b/builtin/repack.c @@ -0,0 +1,439 @@ +#include "builtin.h" +#include "cache.h" +#include "config.h" +#include "dir.h" +#include "parse-options.h" +#include "run-command.h" +#include "sigchain.h" +#include "strbuf.h" +#include "string-list.h" +#include "argv-array.h" + +static int delta_base_offset = 1; +static int pack_kept_objects = -1; +static int write_bitmaps; +static char *packdir, *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." +); + + +static int repack_config(const char *var, const char *value, void *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; + } + return git_default_config(var, value, cb); +} + +/* + * Remove temporary $GIT_OBJECT_DIRECTORY/pack/.tmp-$$-pack-* files. + */ +static void remove_temporary_files(void) +{ + struct strbuf buf = STRBUF_INIT; + size_t dirlen, prefixlen; + DIR *dir; + struct dirent *e; + + dir = opendir(packdir); + if (!dir) + return; + + /* Point at the slash at the end of ".../objects/pack/" */ + dirlen = strlen(packdir) + 1; + strbuf_addstr(&buf, packtmp); + /* Hold the length of ".tmp-%d-pack-" */ + prefixlen = buf.len - dirlen; + + while ((e = readdir(dir))) { + if (strncmp(e->d_name, buf.buf + dirlen, prefixlen)) + continue; + strbuf_setlen(&buf, dirlen); + strbuf_addstr(&buf, e->d_name); + unlink(buf.buf); + } + closedir(dir); + strbuf_release(&buf); +} + +static void remove_pack_on_signal(int signo) +{ + remove_temporary_files(); + sigchain_pop(signo); + raise(signo); +} + +/* + * Adds all packs hex strings to the fname list, which do not + * have a corresponding .keep file. + */ +static void get_non_kept_pack_filenames(struct string_list *fname_list) +{ + DIR *dir; + struct dirent *e; + char *fname; + + if (!(dir = opendir(packdir))) + return; + + while ((e = readdir(dir)) != NULL) { + size_t len; + if (!strip_suffix(e->d_name, ".pack", &len)) + continue; + + fname = xmemdupz(e->d_name, len); + + if (!file_exists(mkpath("%s/%s.keep", packdir, fname))) + string_list_append_nodup(fname_list, fname); + else + free(fname); + } + closedir(dir); +} + +static void remove_redundant_pack(const char *dir_name, const char *base_name) +{ + const char *exts[] = {".pack", ".idx", ".keep", ".bitmap"}; + int i; + struct strbuf buf = STRBUF_INIT; + size_t plen; + + strbuf_addf(&buf, "%s/%s", dir_name, base_name); + plen = buf.len; + + for (i = 0; i < ARRAY_SIZE(exts); i++) { + strbuf_setlen(&buf, plen); + strbuf_addstr(&buf, exts[i]); + unlink(buf.buf); + } + strbuf_release(&buf); +} + +#define ALL_INTO_ONE 1 +#define LOOSEN_UNREACHABLE 2 + +int cmd_repack(int argc, const char **argv, const char *prefix) +{ + struct { + const char *name; + unsigned optional:1; + } exts[] = { + {".pack"}, + {".idx"}, + {".bitmap", 1}, + }; + struct child_process cmd = CHILD_PROCESS_INIT; + struct string_list_item *item; + struct string_list names = STRING_LIST_INIT_DUP; + struct string_list rollback = STRING_LIST_INIT_NODUP; + struct string_list existing_packs = STRING_LIST_INIT_DUP; + struct strbuf line = STRBUF_INIT; + int ext, ret, failed; + FILE *out; + + /* variables to be filled by option parsing */ + int pack_everything = 0; + int delete_redundant = 0; + const char *unpack_unreachable = NULL; + int keep_unreachable = 0; + const char *window = NULL, *window_memory = NULL; + const char *depth = NULL; + const char *threads = NULL; + const char *max_pack_size = NULL; + int no_reuse_delta = 0, no_reuse_object = 0; + int no_update_server_info = 0; + int quiet = 0; + int local = 0; + + 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_BOOL('d', NULL, &delete_redundant, + N_("remove redundant packs, and run git-prune-packed")), + OPT_BOOL('f', NULL, &no_reuse_delta, + N_("pass --no-reuse-delta to git-pack-objects")), + OPT_BOOL('F', NULL, &no_reuse_object, + N_("pass --no-reuse-object to git-pack-objects")), + OPT_BOOL('n', NULL, &no_update_server_info, + N_("do not run git-update-server-info")), + OPT__QUIET(&quiet, N_("be quiet")), + OPT_BOOL('l', "local", &local, + N_("pass --local to git-pack-objects")), + OPT_BOOL('b', "write-bitmap-index", &write_bitmaps, + N_("write bitmap index")), + 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", &window, N_("n"), + N_("size of the window used for delta compression")), + OPT_STRING(0, "window-memory", &window_memory, N_("bytes"), + N_("same as the above, but limit memory size instead of entries count")), + OPT_STRING(0, "depth", &depth, N_("n"), + N_("limits the maximum delta depth")), + OPT_STRING(0, "threads", &threads, N_("n"), + N_("limits the maximum number of threads")), + OPT_STRING(0, "max-pack-size", &max_pack_size, N_("bytes"), + N_("maximum size of each packfile")), + OPT_BOOL(0, "pack-kept-objects", &pack_kept_objects, + N_("repack objects in packs marked with .keep")), + OPT_END() + }; + + git_config(repack_config, NULL); + + 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(_("--keep-unreachable and -A are incompatible")); + + if (pack_kept_objects < 0) + pack_kept_objects = write_bitmaps; + + if (write_bitmaps && !(pack_everything & ALL_INTO_ONE)) + die(_(incremental_bitmap_conflict_error)); + + packdir = mkpathdup("%s/pack", get_object_directory()); + packtmp = mkpathdup("%s/.tmp-%d-pack", packdir, (int)getpid()); + + sigchain_push_common(remove_pack_on_signal); + + argv_array_push(&cmd.args, "pack-objects"); + argv_array_push(&cmd.args, "--keep-true-parents"); + if (!pack_kept_objects) + argv_array_push(&cmd.args, "--honor-pack-keep"); + argv_array_push(&cmd.args, "--non-empty"); + argv_array_push(&cmd.args, "--all"); + argv_array_push(&cmd.args, "--reflog"); + argv_array_push(&cmd.args, "--indexed-objects"); + if (window) + argv_array_pushf(&cmd.args, "--window=%s", window); + if (window_memory) + argv_array_pushf(&cmd.args, "--window-memory=%s", window_memory); + if (depth) + argv_array_pushf(&cmd.args, "--depth=%s", depth); + if (threads) + argv_array_pushf(&cmd.args, "--threads=%s", threads); + if (max_pack_size) + argv_array_pushf(&cmd.args, "--max-pack-size=%s", max_pack_size); + if (no_reuse_delta) + argv_array_pushf(&cmd.args, "--no-reuse-delta"); + if (no_reuse_object) + argv_array_pushf(&cmd.args, "--no-reuse-object"); + if (write_bitmaps) + argv_array_push(&cmd.args, "--write-bitmap-index"); + + if (pack_everything & ALL_INTO_ONE) { + get_non_kept_pack_filenames(&existing_packs); + + if (existing_packs.nr && delete_redundant) { + if (unpack_unreachable) { + argv_array_pushf(&cmd.args, + "--unpack-unreachable=%s", + unpack_unreachable); + argv_array_push(&cmd.env_array, "GIT_REF_PARANOIA=1"); + } else if (pack_everything & LOOSEN_UNREACHABLE) { + argv_array_push(&cmd.args, + "--unpack-unreachable"); + } else if (keep_unreachable) { + argv_array_push(&cmd.args, "--keep-unreachable"); + argv_array_push(&cmd.args, "--pack-loose-unreachable"); + } else { + argv_array_push(&cmd.env_array, "GIT_REF_PARANOIA=1"); + } + } + } else { + argv_array_push(&cmd.args, "--unpacked"); + argv_array_push(&cmd.args, "--incremental"); + } + + if (local) + argv_array_push(&cmd.args, "--local"); + if (quiet) + argv_array_push(&cmd.args, "--quiet"); + if (delta_base_offset) + argv_array_push(&cmd.args, "--delta-base-offset"); + + argv_array_push(&cmd.args, packtmp); + + cmd.git_cmd = 1; + cmd.out = -1; + cmd.no_stdin = 1; + + ret = start_command(&cmd); + if (ret) + return ret; + + out = xfdopen(cmd.out, "r"); + while (strbuf_getline_lf(&line, out) != EOF) { + if (line.len != 40) + die("repack: Expecting 40 character sha1 lines only from pack-objects."); + string_list_append(&names, line.buf); + } + fclose(out); + ret = finish_command(&cmd); + if (ret) + return ret; + + if (!names.nr && !quiet) + printf("Nothing new to pack.\n"); + + /* + * Ok we have prepared all new packfiles. + * First see if there are packs of the same name and if so + * if we can move them out of the way (this can happen if we + * repacked immediately after packing fully. + */ + failed = 0; + for_each_string_list_item(item, &names) { + for (ext = 0; ext < ARRAY_SIZE(exts); ext++) { + char *fname, *fname_old; + fname = mkpathdup("%s/pack-%s%s", packdir, + item->string, exts[ext].name); + if (!file_exists(fname)) { + free(fname); + continue; + } + + fname_old = mkpathdup("%s/old-%s%s", packdir, + item->string, exts[ext].name); + if (file_exists(fname_old)) + if (unlink(fname_old)) + failed = 1; + + if (!failed && rename(fname, fname_old)) { + free(fname); + free(fname_old); + failed = 1; + break; + } else { + string_list_append(&rollback, fname); + free(fname_old); + } + } + if (failed) + break; + } + if (failed) { + struct string_list rollback_failure = STRING_LIST_INIT_DUP; + for_each_string_list_item(item, &rollback) { + char *fname, *fname_old; + fname = mkpathdup("%s/%s", packdir, item->string); + fname_old = mkpathdup("%s/old-%s", packdir, item->string); + if (rename(fname_old, fname)) + string_list_append(&rollback_failure, fname); + free(fname); + free(fname_old); + } + + if (rollback_failure.nr) { + int i; + fprintf(stderr, + "WARNING: Some packs in use have been renamed by\n" + "WARNING: prefixing old- to their name, in order to\n" + "WARNING: replace them with the new version of the\n" + "WARNING: file. But the operation failed, and the\n" + "WARNING: attempt to rename them back to their\n" + "WARNING: original names also failed.\n" + "WARNING: Please rename them in %s manually:\n", packdir); + for (i = 0; i < rollback_failure.nr; i++) + fprintf(stderr, "WARNING: old-%s -> %s\n", + rollback_failure.items[i].string, + rollback_failure.items[i].string); + } + exit(1); + } + + /* Now the ones with the same name are out of the way... */ + for_each_string_list_item(item, &names) { + for (ext = 0; ext < ARRAY_SIZE(exts); ext++) { + char *fname, *fname_old; + struct stat statbuffer; + int exists = 0; + fname = mkpathdup("%s/pack-%s%s", + packdir, item->string, exts[ext].name); + fname_old = mkpathdup("%s-%s%s", + packtmp, item->string, exts[ext].name); + if (!stat(fname_old, &statbuffer)) { + statbuffer.st_mode &= ~(S_IWUSR | S_IWGRP | S_IWOTH); + chmod(fname_old, statbuffer.st_mode); + exists = 1; + } + if (exists || !exts[ext].optional) { + if (rename(fname_old, fname)) + die_errno(_("renaming '%s' failed"), fname_old); + } + free(fname); + free(fname_old); + } + } + + /* Remove the "old-" files */ + for_each_string_list_item(item, &names) { + for (ext = 0; ext < ARRAY_SIZE(exts); ext++) { + char *fname; + fname = mkpathdup("%s/old-%s%s", + packdir, + item->string, + exts[ext].name); + if (remove_path(fname)) + warning(_("failed to remove '%s'"), fname); + free(fname); + } + } + + /* End of pack replacement. */ + + if (delete_redundant) { + int opts = 0; + string_list_sort(&names); + for_each_string_list_item(item, &existing_packs) { + char *sha1; + size_t len = strlen(item->string); + if (len < 40) + continue; + sha1 = item->string + len - 40; + if (!string_list_has_string(&names, sha1)) + remove_redundant_pack(packdir, item->string); + } + if (!quiet && isatty(2)) + opts |= PRUNE_PACKED_VERBOSE; + prune_packed_objects(opts); + } + + if (!no_update_server_info) + update_server_info(0); + remove_temporary_files(); + string_list_clear(&names, 0); + string_list_clear(&rollback, 0); + string_list_clear(&existing_packs, 0); + strbuf_release(&line); + + return 0; +} diff --git a/builtin/replace.c b/builtin/replace.c new file mode 100644 index 0000000000..3e71a77152 --- /dev/null +++ b/builtin/replace.c @@ -0,0 +1,505 @@ +/* + * 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 "cache.h" +#include "config.h" +#include "builtin.h" +#include "refs.h" +#include "parse-options.h" +#include "run-command.h" +#include "tag.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>...]"), + 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(const char *refname, const struct object_id *oid, + int flag, 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 (get_oid(refname, &object)) + return error("Failed to resolve '%s' as a valid ref.", refname); + + obj_type = sha1_object_info(object.hash, NULL); + repl_type = sha1_object_info(oid->hash, NULL); + + printf("%s (%s) -> %s (%s)\n", refname, typename(obj_type), + oid_to_hex(oid), typename(repl_type)); + } + } + + return 0; +} + +static int list_replace_refs(const char *pattern, const char *format) +{ + struct show_data data; + + if (pattern == NULL) + 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; + else + die("invalid replace format '%s'\n" + "valid formats are 'short', 'medium' and 'long'\n", + format); + + for_each_replace_ref(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; + + strbuf_addstr(&ref, git_replace_ref_base); + base_len = ref.len; + + for (p = argv; *p; p++) { + if (get_oid(*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.hash)) { + 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->hash, 0)) + return 1; + printf("Deleted replace ref '%s'\n", name); + return 0; +} + +static void check_ref_valid(struct object_id *object, + struct object_id *prev, + struct strbuf *ref, + int force) +{ + strbuf_reset(ref); + strbuf_addf(ref, "%s%s", git_replace_ref_base, oid_to_hex(object)); + if (check_refname_format(ref->buf, 0)) + die("'%s' is not a valid ref name.", ref->buf); + + if (read_ref(ref->buf, prev->hash)) + oidclr(prev); + else if (!force) + die("replace ref '%s' already exists", ref->buf); +} + +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; + + obj_type = sha1_object_info(object->hash, NULL); + repl_type = sha1_object_info(repl->hash, NULL); + if (!force && obj_type != repl_type) + die("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, typename(obj_type), + replace_ref, typename(repl_type)); + + check_ref_valid(object, &prev, &ref, force); + + transaction = ref_transaction_begin(&err); + if (!transaction || + ref_transaction_update(transaction, ref.buf, repl->hash, prev.hash, + 0, NULL, &err) || + ref_transaction_commit(transaction, &err)) + die("%s", err.buf); + + ref_transaction_free(transaction); + strbuf_release(&ref); + return 0; +} + +static int replace_object(const char *object_ref, const char *replace_ref, int force) +{ + struct object_id object, repl; + + if (get_oid(object_ref, &object)) + die("Failed to resolve '%s' as a valid ref.", object_ref); + if (get_oid(replace_ref, &repl)) + die("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 void 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) + die_errno("unable to open %s for writing", filename); + + argv_array_push(&cmd.args, "--no-replace-objects"); + argv_array_push(&cmd.args, "cat-file"); + if (raw) + argv_array_push(&cmd.args, typename(type)); + else + argv_array_push(&cmd.args, "-p"); + argv_array_push(&cmd.args, oid_to_hex(oid)); + cmd.git_cmd = 1; + cmd.out = fd; + + if (run_command(&cmd)) + die("cat-file reported failure"); +} + +/* + * 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 void 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) + die_errno("unable to open %s for reading", filename); + + if (!raw && type == OBJ_TREE) { + const char *argv[] = { "mktree", NULL }; + struct child_process cmd = CHILD_PROCESS_INIT; + struct strbuf result = STRBUF_INIT; + + cmd.argv = argv; + cmd.git_cmd = 1; + cmd.in = fd; + cmd.out = -1; + + if (start_command(&cmd)) + die("unable to spawn mktree"); + + if (strbuf_read(&result, cmd.out, 41) < 0) + die_errno("unable to read from mktree"); + close(cmd.out); + + if (finish_command(&cmd)) + die("mktree reported failure"); + if (get_oid_hex(result.buf, oid) < 0) + die("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) + die_errno("unable to fstat %s", filename); + if (index_fd(oid, fd, &st, type, NULL, flags) < 0) + die("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. + */ +} + +static int edit_and_replace(const char *object_ref, int force, int raw) +{ + char *tmpfile = git_pathdup("REPLACE_EDITOBJ"); + enum object_type type; + struct object_id old, new, prev; + struct strbuf ref = STRBUF_INIT; + + if (get_oid(object_ref, &old) < 0) + die("Not a valid object name: '%s'", object_ref); + + type = sha1_object_info(old.hash, NULL); + if (type < 0) + die("unable to get object type for %s", oid_to_hex(&old)); + + check_ref_valid(&old, &prev, &ref, force); + strbuf_release(&ref); + + export_object(&old, type, raw, tmpfile); + if (launch_editor(tmpfile, NULL, NULL) < 0) + die("editing object file failed"); + import_object(&new, type, raw, tmpfile); + + free(tmpfile); + + if (!oidcmp(&old, &new)) + return error("new object is the same as the old one: '%s'", oid_to_hex(&old)); + + return replace_object_oid(object_ref, &old, "replacement", &new, force); +} + +static void replace_parents(struct strbuf *buf, int argc, const char **argv) +{ + struct strbuf new_parents = STRBUF_INIT; + const char *parent_start, *parent_end; + int i; + + /* find existing parents */ + parent_start = buf->buf; + parent_start += GIT_SHA1_HEXSZ + 6; /* "tree " + "hex sha1" + "\n" */ + parent_end = parent_start; + + while (starts_with(parent_end, "parent ")) + parent_end += 48; /* "parent " + "hex sha1" + "\n" */ + + /* prepare new parents */ + for (i = 0; i < argc; i++) { + struct object_id oid; + if (get_oid(argv[i], &oid) < 0) + die(_("Not a valid object name: '%s'"), argv[i]); + lookup_commit_or_die(&oid, argv[i]); + strbuf_addf(&new_parents, "parent %s\n", oid_to_hex(&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); +} + +struct check_mergetag_data { + int argc; + const char **argv; +}; + +static void check_one_mergetag(struct commit *commit, + 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_sha1_file(extra->value, extra->len, typename(OBJ_TAG), tag_oid.hash); + tag = lookup_tag(&tag_oid); + if (!tag) + die(_("bad mergetag in commit '%s'"), ref); + if (parse_tag_buffer(tag, extra->value, extra->len)) + die(_("malformed mergetag in commit '%s'"), ref); + + /* iterate over new parents */ + for (i = 1; i < mergetag_data->argc; i++) { + struct object_id oid; + if (get_oid(mergetag_data->argv[i], &oid) < 0) + die(_("Not a valid object name: '%s'"), mergetag_data->argv[i]); + if (!oidcmp(&tag->tagged->oid, &oid)) + return; /* found */ + } + + die(_("original commit '%s' contains mergetag '%s' that is discarded; " + "use --edit instead of --graft"), ref, oid_to_hex(&tag_oid)); +} + +static void check_mergetags(struct commit *commit, int argc, const char **argv) +{ + struct check_mergetag_data mergetag_data; + + mergetag_data.argc = argc; + mergetag_data.argv = argv; + for_each_mergetag(check_one_mergetag, commit, &mergetag_data); +} + +static int create_graft(int argc, const char **argv, int force) +{ + struct object_id old, new; + const char *old_ref = argv[0]; + struct commit *commit; + struct strbuf buf = STRBUF_INIT; + const char *buffer; + unsigned long size; + + if (get_oid(old_ref, &old) < 0) + die(_("Not a valid object name: '%s'"), old_ref); + commit = lookup_commit_or_die(&old, old_ref); + + buffer = get_commit_buffer(commit, &size); + strbuf_add(&buf, buffer, size); + unuse_commit_buffer(commit, buffer); + + replace_parents(&buf, argc - 1, &argv[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!")); + } + + check_mergetags(commit, argc, argv); + + if (write_sha1_file(buf.buf, buf.len, commit_type, new.hash)) + die(_("could not write replacement commit for: '%s'"), old_ref); + + strbuf_release(&buf); + + if (!oidcmp(&old, &new)) + return error("new commit is the same as the old one: '%s'", oid_to_hex(&old)); + + return replace_object_oid(old_ref, &old, "replacement", &new, force); +} + +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_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_BOOL('f', "force", &force, N_("replace the ref if it exists")), + 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() + }; + + check_replace_refs = 0; + 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) + 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); + + 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: + die("BUG: invalid cmdmode %d", (int)cmdmode); + } +} diff --git a/builtin/rerere.c b/builtin/rerere.c new file mode 100644 index 0000000000..0bc40298c2 --- /dev/null +++ b/builtin/rerere.c @@ -0,0 +1,117 @@ +#include "builtin.h" +#include "cache.h" +#include "config.h" +#include "dir.h" +#include "parse-options.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 <path>... | status | remaining | diff | gc]"), + NULL, +}; + +static int outf(void *dummy, 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; + 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; + ecb.outf = outf; + 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 rerere(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(&pathspec); + } + + if (!strcmp(argv[0], "clear")) { + rerere_clear(&merge_rr); + } else if (!strcmp(argv[0], "gc")) + rerere_gc(&merge_rr); + else if (!strcmp(argv[0], "status")) { + if (setup_rerere(&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(&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(&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 0000000000..9cd89b2305 --- /dev/null +++ b/builtin/reset.c @@ -0,0 +1,413 @@ +/* + * "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 + */ +#include "builtin.h" +#include "config.h" +#include "lockfile.h" +#include "tag.h" +#include "object.h" +#include "commit.h" +#include "run-command.h" +#include "refs.h" +#include "diff.h" +#include "diffcore.h" +#include "tree.h" +#include "branch.h" +#include "parse-options.h" +#include "unpack-trees.h" +#include "cache-tree.h" +#include "submodule.h" +#include "submodule-config.h" + +static const char * const git_reset_usage[] = { + N_("git reset [--mixed | --soft | --hard | --merge | --keep] [-q] [<commit>]"), + N_("git reset [-q] [<tree-ish>] [--] <paths>..."), + N_("git reset --patch [<tree-ish>] [--] [<paths>...]"), + 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(), F_OK); +} + +static int reset_index(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; + if (!quiet) + opts.verbose_update = 1; + switch (reset_type) { + case KEEP: + case MERGE: + opts.update = 1; + break; + case HARD: + opts.update = 1; + /* fallthrough */ + default: + opts.reset = 1; + } + + read_cache_unmerged(); + + if (reset_type == KEEP) { + struct object_id head_oid; + if (get_oid("HEAD", &head_oid)) + return error(_("You do not have a valid HEAD.")); + if (!fill_tree_descriptor(desc + nr, &head_oid)) + return error(_("Failed to find tree of HEAD.")); + nr++; + opts.fn = twoway_merge; + } + + if (!fill_tree_descriptor(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_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) +{ + const char *hex, *body; + const char *msg; + + hex = find_unique_abbrev(commit->object.oid.hash, DEFAULT_ABBREV); + printf(_("HEAD is now at %s"), hex); + msg = logmsg_reencode(commit, NULL, get_log_output_encoding()); + body = strstr(msg, "\n\n"); + if (body) { + const char *eol; + size_t len; + body = skip_blank_lines(body + 2); + eol = strchr(body, '\n'); + len = eol ? eol - body : strlen(body); + printf(" %.*s\n", (int) len, body); + } + else + printf("\n"); + unuse_commit_buffer(commit, msg); +} + +static void update_index_from_diff(struct diff_queue_struct *q, + struct diff_options *opt, void *data) +{ + int i; + int intent_to_add = *(int *)data; + + for (i = 0; i < q->nr; i++) { + struct diff_filespec *one = q->queue[i]->one; + int is_missing = !(one->mode && !is_null_oid(&one->oid)); + struct cache_entry *ce; + + if (is_missing && !intent_to_add) { + remove_file_from_cache(one->path); + continue; + } + + ce = make_cache_entry(one->mode, one->oid.hash, one->path, + 0, 0); + if (!ce) + die(_("make_cache_entry failed for path '%s'"), + one->path); + if (is_missing) { + ce->ce_flags |= CE_INTENT_TO_ADD; + set_object_name_for_intent_to_add_entry(ce); + } + add_cache_entry(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 |= DIFF_OPT_OVERRIDE_SUBMODULE_CONFIG; + + if (do_diff_cache(tree_oid, &opt)) + return 1; + diffcore_std(&opt); + diff_flush(&opt); + clear_pathspec(&opt.pathspec); + + 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_cache()) + 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] && !get_oid_committish(argv[0], &unused)) || + (argv[1] && !get_oid_treeish(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; + + if (read_cache() < 0) + die(_("index file corrupt")); + + 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 (!get_oid("ORIG_HEAD", &oid_old_orig)) + old_orig = &oid_old_orig; + if (!get_oid("HEAD", &oid_orig)) { + orig = &oid_orig; + set_reflog_message(&msg, "updating ORIG_HEAD", NULL); + update_ref_oid(msg.buf, "ORIG_HEAD", orig, old_orig, 0, + UPDATE_REFS_MSG_ON_ERR); + } else if (old_orig) + delete_ref(NULL, "ORIG_HEAD", old_orig->hash, 0); + set_reflog_message(&msg, "updating HEAD", rev); + update_ref_status = update_ref_oid(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, void *cb) +{ + if (!strcmp(var, "submodule.recurse")) + return git_default_submodule_config(var, value, cb); + + return git_default_config(var, value, cb); +} + +int cmd_reset(int argc, const char **argv, const char *prefix) +{ + int reset_type = NONE, update_ref_status = 0, quiet = 0; + int patch_mode = 0, unborn; + const char *rev; + 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_SET_INT(0, "mixed", &reset_type, + N_("reset HEAD and index"), MIXED), + OPT_SET_INT(0, "soft", &reset_type, N_("reset only HEAD"), SOFT), + OPT_SET_INT(0, "hard", &reset_type, + N_("reset HEAD, index and working tree"), HARD), + OPT_SET_INT(0, "merge", &reset_type, + N_("reset HEAD, index and working tree"), MERGE), + OPT_SET_INT(0, "keep", &reset_type, + N_("reset HEAD but keep local changes"), KEEP), + { OPTION_CALLBACK, 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_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); + + unborn = !strcmp(rev, "HEAD") && get_oid("HEAD", &oid); + if (unborn) { + /* reset on unborn branch: treat as reset to empty tree */ + hashcpy(oid.hash, EMPTY_TREE_SHA1_BIN); + } else if (!pathspec.nr) { + struct commit *commit; + if (get_oid_committish(rev, &oid)) + die(_("Failed to resolve '%s' as a valid revision."), rev); + commit = lookup_commit_reference(&oid); + if (!commit) + die(_("Could not parse object '%s'."), rev); + oidcpy(&oid, &commit->object.oid); + } else { + struct tree *tree; + if (get_oid_treeish(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(_("--patch is incompatible with --{hard,mixed,soft}")); + return run_add_interactive(rev, "--patch=reset", &pathspec); + } + + /* 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 (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(_("-N can only be used with --mixed")); + + /* 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; + hold_locked_index(&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)) + return 1; + if (get_git_work_tree()) + refresh_index(&the_index, flags, NULL, NULL, + _("Unstaged changes after reset:")); + } else { + int err = reset_index(&oid, reset_type, quiet); + if (reset_type == KEEP && !err) + err = reset_index(&oid, MIXED, quiet); + if (err) + die(_("Could not reset index file to revision '%s'."), rev); + } + + 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(&oid)); + } + if (!pathspec.nr) + remove_branch_state(); + + return update_ref_status; +} diff --git a/builtin/rev-list.c b/builtin/rev-list.c new file mode 100644 index 0000000000..c1c74d4a79 --- /dev/null +++ b/builtin/rev-list.c @@ -0,0 +1,423 @@ +#include "cache.h" +#include "config.h" +#include "commit.h" +#include "diff.h" +#include "revision.h" +#include "list-objects.h" +#include "pack.h" +#include "pack-bitmap.h" +#include "builtin.h" +#include "log-tree.h" +#include "graph.h" +#include "bisect.h" +#include "progress.h" +#include "reflog-walk.h" + +static const char rev_list_usage[] = +"git rev-list [OPTION] <commit-id>... [ -- paths... ]\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" +" --quiet\n" +" ordering output:\n" +" --topo-order\n" +" --date-order\n" +" --reverse\n" +" formatting output:\n" +" --parents\n" +" --children\n" +" --objects | --objects-edge\n" +" --unpacked\n" +" --header | --pretty\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 void finish_commit(struct commit *commit, void *data); +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 (info->flags & REV_LIST_QUIET) { + finish_commit(commit, data); + 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, data); + return; + } + + if (info->show_timestamp) + printf("%"PRItime" ", commit->date); + if (info->header_prefix) + fputs(info->header_prefix, stdout); + + if (!revs->graph) + fputs(get_revision_mark(revs, commit), stdout); + if (revs->abbrev_commit && revs->abbrev) + fputs(find_unique_abbrev(commit->object.oid.hash, 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 + putchar('\n'); + + if (revs->verbose_header && get_cached_commit_buffer(commit, NULL)) { + 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, data); +} + +static void finish_commit(struct commit *commit, void *data) +{ + if (commit->parents) { + free_commit_list(commit->parents); + commit->parents = NULL; + } + free_commit_buffer(commit); +} + +static void finish_object(struct object *obj, const char *name, void *cb_data) +{ + struct rev_list_info *info = cb_data; + if (obj->type == OBJ_BLOB && !has_object_file(&obj->oid)) + die("missing blob object '%s'", oid_to_hex(&obj->oid)); + if (info->revs->verify_objects && !obj->parsed && obj->type != OBJ_COMMIT) + parse_object(&obj->oid); +} + +static void show_object(struct object *obj, const char *name, void *cb_data) +{ + struct rev_list_info *info = cb_data; + finish_object(obj, name, cb_data); + display_progress(progress, ++progress_counter); + if (info->flags & REV_LIST_QUIET) + return; + show_object_with_name(stdout, obj, name); +} + +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 unsigned char *sha1, + enum object_type type, + int exclude, + uint32_t name_hash, + struct packed_git *found_pack, + off_t found_offset) +{ + fprintf(stdout, "%s\n", sha1_to_hex(sha1)); + return 1; +} + +int cmd_rev_list(int argc, const char **argv, const char *prefix) +{ + struct rev_info revs; + struct rev_list_info info; + int i; + int bisect_list = 0; + int bisect_show_vars = 0; + int bisect_find_all = 0; + int use_bitmap_index = 0; + const char *show_progress = NULL; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage(rev_list_usage); + + git_config(git_default_config, NULL); + init_revisions(&revs, prefix); + revs.abbrev = DEFAULT_ABBREV; + revs.commit_format = CMIT_FMT_UNSPECIFIED; + argc = setup_revisions(argc, argv, &revs, NULL); + + memset(&info, 0, sizeof(info)); + info.revs = &revs; + if (revs.bisect) + bisect_list = 1; + + if (DIFF_OPT_TST(&revs.diffopt, 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); + return 0; + } + if (skip_prefix(arg, "--progress=", &arg)) { + show_progress = arg; + continue; + } + usage(rev_list_usage); + + } + if (revs.commit_format != CMIT_FMT_UNSPECIFIED) { + /* The command line has a --pretty */ + info.hdr_termination = '\n'; + if (revs.commit_format == CMIT_FMT_ONELINE) + 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.diff) + usage(rev_list_usage); + + if (revs.show_notes) + die(_("rev-list does not support display of notes")); + + 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 && !revs.prune) { + if (revs.count && !revs.left_right && !revs.cherry_mark) { + uint32_t commit_count; + int max_count = revs.max_count; + if (!prepare_bitmap_walk(&revs)) { + count_bitmap_commit_list(&commit_count, NULL, NULL, NULL); + if (max_count >= 0 && max_count < commit_count) + commit_count = max_count; + printf("%d\n", commit_count); + return 0; + } + } else if (revs.max_count < 0 && + revs.tag_objects && revs.tree_objects && revs.blob_objects) { + if (!prepare_bitmap_walk(&revs)) { + traverse_bitmap_commit_list(&show_object_fast); + return 0; + } + } + } + + if (prepare_revision_walk(&revs)) + die("revision walk setup failed"); + if (revs.tree_objects) + mark_edges_uninteresting(&revs, show_edge); + + if (bisect_list) { + int reaches = reaches, all = all; + + revs.commits = find_bisection(revs.commits, &reaches, &all, + bisect_find_all); + + if (bisect_show_vars) + return show_bisect_vars(&info, reaches, all); + } + + traverse_commit_list(&revs, show_commit, show_object, &info); + + 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); + } + + return 0; +} diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c new file mode 100644 index 0000000000..a8d7e6f7ae --- /dev/null +++ b/builtin/rev-parse.c @@ -0,0 +1,956 @@ +/* + * rev-parse.c + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" +#include "config.h" +#include "commit.h" +#include "refs.h" +#include "quote.h" +#include "builtin.h" +#include "parse-options.h" +#include "diff.h" +#include "revision.h" +#include "split-index.h" +#include "submodule.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 string_list *ref_excludes; + +/* + * 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 (dwim_ref(name, strlen(name), discard.hash, &full)) { + 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) + full = shorten_unambiguous_ref(full, + abbrev_ref_strict); + 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, find_unique_abbrev(oid->hash, 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 (!get_oid(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, void *cb_data) +{ + 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, void *cb_data) +{ + show_rev(REVERSED, oid, refname); + return 0; +} + +static int show_abbrev(const struct object_id *oid, void *cb_data) +{ + 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 oid; + struct object_id end; + const char *next; + const char *this; + int symmetric; + static const char head_by_default[] = "HEAD"; + + if (!(dotdot = strstr(arg, ".."))) + return 0; + next = dotdot + 2; + this = arg; + symmetric = (*next == '.'); + + *dotdot = 0; + next += symmetric; + + if (!*next) + next = head_by_default; + if (dotdot == arg) + this = head_by_default; + + if (this == head_by_default && next == head_by_default && + !symmetric) { + /* + * Just ".."? That is not a range but the + * pathspec for the parent directory. + */ + *dotdot = '.'; + return 0; + } + + if (!get_oid_committish(this, &oid) && !get_oid_committish(next, &end)) { + show_rev(NORMAL, &end, next); + show_rev(symmetric ? NORMAL : REVERSED, &oid, this); + if (symmetric) { + struct commit_list *exclude; + struct commit *a, *b; + a = lookup_commit_reference(&oid); + b = lookup_commit_reference(&end); + exclude = get_merge_bases(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 (get_oid_committish(arg, &oid)) { + *dotdot = '^'; + return 0; + } + + commit = lookup_commit_reference(&oid); + 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 == NULL) + s = help; + + 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, 0); + puts(parsed.buf); + return 0; +} + +static int cmd_sq_quote(int argc, const char **argv) +{ + struct strbuf buf = STRBUF_INIT; + + if (argc) + sq_quote_argv(&buf, argv, 0); + 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_exclusion(&ref_excludes); +} + +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; + + 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 (!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; + } + + if (!strcmp(arg, "--git-path")) { + if (!argv[i + 1]) + die("--git-path requires an argument"); + strbuf_reset(&buf); + puts(relative_path(git_path("%s", argv[i + 1]), + prefix, &buf)); + i++; + continue; + } + if (as_is) { + if (show_file(arg, output_prefix) && as_is < 2) + verify_filename(prefix, arg, 0); + 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 (*arg == '-') { + 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 (!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 (40 <= abbrev) + abbrev = 40; + 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); + continue; + } + if (skip_prefix(arg, "--disambiguate=", &arg)) { + for_each_abbrev(arg, show_abbrev, NULL); + continue; + } + if (!strcmp(arg, "--bisect")) { + for_each_fullref_in("refs/bisect/bad", show_reference, NULL, 0); + for_each_fullref_in("refs/bisect/good", anti_reference, NULL, 0); + continue; + } + if (opt_with_value(arg, "--branches", &arg)) { + handle_ref_opt(arg, "refs/heads/"); + continue; + } + if (opt_with_value(arg, "--tags", &arg)) { + 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)) { + handle_ref_opt(arg, "refs/remotes/"); + continue; + } + if (skip_prefix(arg, "--exclude=", &arg)) { + add_ref_exclusion(&ref_excludes, arg); + continue; + } + if (!strcmp(arg, "--show-toplevel")) { + const char *work_tree = get_git_work_tree(); + if (work_tree) + puts(work_tree); + continue; + } + if (!strcmp(arg, "--show-superproject-working-tree")) { + const char *superproject = get_superproject_working_tree(); + if (superproject) + puts(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; + if (arg[2] == 'g') { /* --git-dir */ + if (gitdir) { + puts(gitdir); + continue; + } + if (!prefix) { + puts(".git"); + continue; + } + } else { /* --absolute-git-dir */ + if (!gitdir && !prefix) + gitdir = ".git"; + if (gitdir) { + puts(real_path(gitdir)); + continue; + } + } + cwd = xgetcwd(); + len = strlen(cwd); + printf("%s%s.git\n", cwd, len && cwd[len-1] != '/' ? "/" : ""); + free(cwd); + continue; + } + if (!strcmp(arg, "--git-common-dir")) { + strbuf_reset(&buf); + puts(relative_path(get_git_common_dir(), + prefix, &buf)); + 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() ? "true" + : "false"); + continue; + } + if (!strcmp(arg, "--shared-index-path")) { + if (read_cache() < 0) + die(_("Could not read the index")); + if (the_index.split_index) { + const unsigned char *sha1 = the_index.split_index->base_sha1; + const char *path = git_path("sharedindex.%s", sha1_to_hex(sha1)); + strbuf_reset(&buf); + puts(relative_path(path, prefix, &buf)); + } + 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 (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(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 0000000000..b9d927eb09 --- /dev/null +++ b/builtin/revert.c @@ -0,0 +1,229 @@ +#include "cache.h" +#include "config.h" +#include "builtin.h" +#include "parse-options.h" +#include "diff.h" +#include "revision.h" +#include "rerere.h" +#include "dir.h" +#include "sequencer.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 [<options>] <commit-ish>..."), + N_("git revert <subcommand>"), + NULL +}; + +static const char * const cherry_pick_usage[] = { + N_("git cherry-pick [<options>] <commit-ish>..."), + N_("git cherry-pick <subcommand>"), + 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_x(const struct option *opt, + const char *arg, int unset) +{ + struct replay_opts **opts_ptr = opt->value; + struct replay_opts *opts = *opts_ptr; + + if (unset) + return 0; + + ALLOC_GROW(opts->xopts, opts->xopts_nr + 1, opts->xopts_alloc); + opts->xopts[opts->xopts_nr++] = xstrdup(arg); + return 0; +} + +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 opterror(opt, "expects a number greater than zero", 0); + + 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, struct replay_opts *opts) +{ + const char * const * usage_str = revert_or_cherry_pick_usage(opts); + const char *me = action_name(opts); + 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_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 Signed-off-by:")), + 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_CALLBACK('X', "strategy-option", &opts, N_("option"), + N_("option for merge strategy"), option_parse_x), + { 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); + } + + argc = parse_options(argc, argv, NULL, options, usage_str, + PARSE_OPT_KEEP_ARGV0 | + PARSE_OPT_KEEP_UNKNOWN); + + /* implies allow_empty */ + if (opts->keep_redundant_commits) + opts->allow_empty = 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 { + 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 ? 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->allow_ff) + verify_opt_compatible(me, "--ff", + "--signoff", opts->signoff, + "--no-commit", opts->no_commit, + "-x", opts->record_origin, + "--edit", opts->edit, + NULL); + + if (cmd) { + opts->revs = NULL; + } else { + struct setup_revision_opt s_r_opt; + opts->revs = xmalloc(sizeof(*opts->revs)); + init_revisions(opts->revs, NULL); + opts->revs->no_walk = REVISION_WALK_NO_WALK_UNSORTED; + 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 (cmd == 'q') + return sequencer_remove_state(opts); + if (cmd == 'c') + return sequencer_continue(opts); + if (cmd == 'a') + return sequencer_rollback(opts); + return sequencer_pick_revisions(opts); +} + +int cmd_revert(int argc, const char **argv, const char *prefix) +{ + struct replay_opts opts = REPLAY_OPTS_INIT; + int res; + + if (isatty(0)) + opts.edit = 1; + opts.action = REPLAY_REVERT; + git_config(git_default_config, NULL); + res = run_sequencer(argc, argv, &opts); + if (res < 0) + die(_("revert failed")); + 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; + git_config(git_default_config, NULL); + res = run_sequencer(argc, argv, &opts); + if (res < 0) + die(_("cherry-pick failed")); + return res; +} diff --git a/builtin/rm.c b/builtin/rm.c new file mode 100644 index 0000000000..d91451fea1 --- /dev/null +++ b/builtin/rm.c @@ -0,0 +1,394 @@ +/* + * "git rm" builtin command + * + * Copyright (C) Linus Torvalds 2006 + */ +#include "builtin.h" +#include "config.h" +#include "lockfile.h" +#include "dir.h" +#include "cache-tree.h" +#include "tree-walk.h" +#include "parse-options.h" +#include "string-list.h" +#include "submodule.h" +#include "pathspec.h" + +static const char * const builtin_rm_usage[] = { + N_("git rm [<options>] [--] <file>..."), + 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 < active_nr) && !strcmp(active_cache[i]->name, path)) { + if (ce_stage(active_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_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(const char *prefix) +{ + int i; + for (i = 0; i < list.nr; i++) { + const char *name = list.entry[i].name; + int pos; + const struct cache_entry *ce; + + pos = cache_name_pos(name, strlen(name)); + if (pos < 0) { + pos = get_ours_cache_pos(name, pos); + if (pos < 0) + continue; + } + ce = active_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(prefix, name, + ABSORB_GITDIR_RECURSE_SUBMODULES); + } +} + +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 mode; + int local_changes = 0; + int staged_changes = 0; + + pos = cache_name_pos(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(active_cache[pos]->ce_mode) || + is_empty_dir(name)) + continue; + } + ce = active_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 (ce_match_stat(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(head->hash, name, oid.hash, &mode) + || ce->ce_mode != create_ce_mode(mode) + || oidcmp(&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 struct lock_file lock_file; + +static int show_only = 0, force = 0, index_only = 0, recursive = 0, quiet = 0; +static int ignore_unmatch = 0; + +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")), + 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_END(), +}; + +int cmd_rm(int argc, const char **argv, const char *prefix) +{ + int i; + struct pathspec pathspec; + char *seen; + + git_config(git_default_config, NULL); + + argc = parse_options(argc, argv, prefix, builtin_rm_options, + builtin_rm_usage, 0); + if (!argc) + usage_with_options(builtin_rm_usage, builtin_rm_options); + + if (!index_only) + setup_work_tree(); + + hold_locked_index(&lock_file, LOCK_DIE_ON_ERROR); + + if (read_cache() < 0) + die(_("index file corrupt")); + + parse_pathspec(&pathspec, 0, + PATHSPEC_PREFER_CWD, + prefix, argv); + refresh_index(&the_index, REFRESH_QUIET, &pathspec, NULL, NULL); + + seen = xcalloc(pathspec.nr, 1); + + for (i = 0; i < active_nr; i++) { + const struct cache_entry *ce = active_cache[i]; + if (!ce_path_match(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; + for (i = 0; i < pathspec.nr; i++) { + original = pathspec.items[i].original; + if (!seen[i]) { + if (!ignore_unmatch) { + die(_("pathspec '%s' did not match any files"), + original); + } + } + else { + seen_any = 1; + } + if (!recursive && seen[i] == MATCHED_RECURSIVELY) + die(_("not removing '%s' recursively without -r"), + *original ? original : "."); + } + + if (!seen_any) + exit(0); + } + + if (!index_only) + submodules_absorb_gitdir_if_needed(prefix); + + /* + * 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 (get_oid("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_cache(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; + 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, 0)) + 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(); + } + + if (active_cache_changed) { + if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK)) + die(_("Unable to write new index file")); + } + + return 0; +} diff --git a/builtin/send-pack.c b/builtin/send-pack.c new file mode 100644 index 0000000000..fc4f0bb5fb --- /dev/null +++ b/builtin/send-pack.c @@ -0,0 +1,306 @@ +#include "builtin.h" +#include "config.h" +#include "commit.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 "sha1-array.h" +#include "gpg-interface.h" +#include "gettext.h" + +static const char * const send_pack_usage[] = { + N_("git send-pack [--all | --mirror] [--dry-run] [--force] " + "[--receive-pack=<git-receive-pack>] [--verbose] [--thin] [--atomic] " + "[<host>:]<directory> [<ref>...]\n" + " --all and explicit <ref> specification are mutually exclusive."), + NULL, +}; + +static struct send_pack_args args; + +static void print_helper_status(struct ref *ref) +{ + struct strbuf buf = STRBUF_INIT; + + for (; ref; ref = ref->next) { + const char *msg = NULL; + const char *res; + + 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_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: + 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'); + + write_or_die(1, buf.buf, buf.len); + } + strbuf_release(&buf); +} + +static int send_pack_config(const char *k, const char *v, void *cb) +{ + git_gpg_config(k, v, NULL); + + 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 0; +} + +int cmd_send_pack(int argc, const char **argv, const char *prefix) +{ + int i, nr_refspecs = 0; + const char **refspecs = NULL; + 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}; + + 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")), + { OPTION_CALLBACK, + 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")), + { OPTION_CALLBACK, + 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_END() + }; + + git_config(send_pack_config, NULL); + argc = parse_options(argc, argv, prefix, options, send_pack_usage, 0); + if (argc > 0) { + dest = argv[0]; + refspecs = (const char **)(argv + 1); + nr_refspecs = 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; + + if (from_stdin) { + struct argv_array all_refspecs = ARGV_ARRAY_INIT; + + for (i = 0; i < nr_refspecs; i++) + argv_array_push(&all_refspecs, refspecs[i]); + + if (args.stateless_rpc) { + const char *buf; + while ((buf = packet_read_line(0, NULL))) + argv_array_push(&all_refspecs, buf); + } else { + struct strbuf line = STRBUF_INIT; + while (strbuf_getline(&line, stdin) != EOF) + argv_array_push(&all_refspecs, line.buf); + strbuf_release(&line); + } + + refspecs = all_refspecs.argv; + nr_refspecs = all_refspecs.argc; + } + + /* + * --all and --mirror are incompatible; neither makes sense + * with any refspecs. + */ + if ((nr_refspecs > 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, receivepack, + args.verbose ? CONNECT_VERBOSE : 0); + } + + get_remote_heads(fd[0], NULL, 0, &remote_refs, REF_NORMAL, + &extra_have, &shallow); + + transport_verify_remote_names(nr_refspecs, refspecs); + + 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, nr_refspecs, refspecs, flags)) + return -1; + + if (!is_empty_cas(&cas)) + apply_push_cas(&cas, remote, remote_refs); + + 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 0000000000..e29875b843 --- /dev/null +++ b/builtin/shortlog.c @@ -0,0 +1,369 @@ +#include "builtin.h" +#include "cache.h" +#include "config.h" +#include "commit.h" +#include "diff.h" +#include "string-list.h" +#include "revision.h" +#include "utf8.h" +#include "mailmap.h" +#include "shortlog.h" +#include "parse-options.h" + +static char const * const shortlog_usage[] = { + N_("git shortlog [<options>] [<revision-range>] [[--] [<path>...]]"), + 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 *author, + const char *oneline) +{ + struct string_list_item *item; + + item = string_list_insert(&log->list, author); + + if (log->summary) + item->util = (void *)(UTIL_TO_INT(item) + 1); + else { + const char *dot3 = log->common_repo_prefix; + char *buffer, *p; + 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 (dot3) { + int dot3len = strlen(dot3); + if (dot3len > 5) { + while ((p = strstr(buffer, dot3)) != NULL) { + int taillen = strlen(p) - dot3len; + memcpy(p, "/.../", 5); + memmove(p + 5, p + dot3len, taillen + 1); + } + } + } + + if (item->util == NULL) + item->util = xcalloc(1, sizeof(struct string_list)); + string_list_append(item->util, buffer); + } +} + +static int parse_stdin_author(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 author = STRBUF_INIT; + struct strbuf mapped_author = 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; + + match = log->committer ? committer_match : author_match; + while (strbuf_getline_lf(&author, stdin) != EOF) { + const char *v; + if (!skip_prefix(author.buf, match[0], &v) && + !skip_prefix(author.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_author); + if (parse_stdin_author(log, &mapped_author, v) < 0) + continue; + + insert_one_record(log, mapped_author.buf, oneline.buf); + } + strbuf_release(&author); + strbuf_release(&mapped_author); + strbuf_release(&oneline); +} + +void shortlog_add_commit(struct shortlog *log, struct commit *commit) +{ + struct strbuf author = STRBUF_INIT; + struct strbuf oneline = STRBUF_INIT; + struct pretty_print_context ctx = {0}; + const char *fmt; + + ctx.fmt = CMIT_FMT_USERFORMAT; + ctx.abbrev = log->abbrev; + ctx.print_email_subject = 1; + ctx.date_mode.type = DATE_NORMAL; + ctx.output_encoding = get_log_output_encoding(); + + fmt = log->committer ? + (log->email ? "%cN <%cE>" : "%cN") : + (log->email ? "%aN <%aE>" : "%aN"); + + format_commit_message(commit, fmt, &author, &ctx); + if (!log->summary) { + if (log->user_format) + pretty_print_commit(&ctx, commit, &oneline); + else + format_commit_message(commit, "%s", &oneline, &ctx); + } + + insert_one_record(log, author.buf, oneline.len ? oneline.buf : "<none>"); + + strbuf_release(&author); + 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; +} + +void shortlog_init(struct shortlog *log) +{ + memset(log, 0, sizeof(*log)); + + read_mailmap(&log->mailmap, &log->common_repo_prefix); + + log->list.strdup_strings = 1; + log->wrap = DEFAULT_WRAPLEN; + log->in1 = DEFAULT_INDENT1; + log->in2 = DEFAULT_INDENT2; +} + +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_BOOL('c', "committer", &log.committer, + N_("Group by committer rather than author")), + 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")), + { OPTION_CALLBACK, 'w', NULL, &log, N_("w[,i1[,i2]]"), + N_("Linewrap output"), PARSE_OPT_OPTARG, &parse_wrap_args }, + OPT_END(), + }; + + struct parse_opt_ctx_t ctx; + + git_config(git_default_config, NULL); + shortlog_init(&log); + init_revisions(&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_HELP: + exit(129); + case PARSE_OPT_DONE: + goto parse_done; + } + parse_revision_opt(&rev, &ctx, options, shortlog_usage); + } +parse_done: + argc = parse_options_end(&ctx); + + 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; + + /* 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); + + 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) +{ + int i, j; + struct strbuf sb = STRBUF_INIT; + + if (log->sort_by_number) + 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 (%d):\n", + item->string, onelines->nr); + for (j = onelines->nr - 1; j >= 0; j--) { + const char *msg = onelines->items[j].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); +} diff --git a/builtin/show-branch.c b/builtin/show-branch.c new file mode 100644 index 0000000000..6fa1f62a88 --- /dev/null +++ b/builtin/show-branch.c @@ -0,0 +1,931 @@ +#include "cache.h" +#include "config.h" +#include "commit.h" +#include "refs.h" +#include "builtin.h" +#include "color.h" +#include "argv-array.h" +#include "parse-options.h" +#include "dir.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] [(<rev> | <glob>)...]"), + N_("git show-branch (-g | --reflog)[=<n>[,<base>]] [--list] [<ref>]"), + NULL +}; + +static int showbranch_use_color = -1; + +static struct argv_array default_args = ARGV_ARRAY_INIT; + +#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 */ +}; + +/* 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; + if (!commit->util) + commit->util = xmalloc(sizeof(struct commit_name)); + name = commit->util; + 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->util; + struct commit_name *parent_name = parent->util; + 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 (!c->util) + break; + if (!c->parents) + break; + p = c->parents->item; + if (!p->util) { + 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 (c->util) + 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 (!c->util) + continue; + n = c->util; + parents = c->parents; + nth = 0; + while (parents) { + struct commit *p = parents->item; + struct strbuf newname = STRBUF_INIT; + parents = parents->next; + nth++; + if (p->util) + 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; + parse_commit(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->util; + + 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] ", + find_unique_abbrev(commit->object.oid.hash, + 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(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, void *cb_data) +{ + 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 (get_oid(refname + ofs, &tmp) || oidcmp(&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, void *cb_data) +{ + 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 (get_oid(refname + ofs, &tmp) || oidcmp(&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, void *cb_data) +{ + 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, + unsigned char *head_sha1, unsigned char *sha1) +{ + if (!head || (head_sha1 && sha1 && hashcmp(head_sha1, sha1))) + 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, + char **ref_name, + 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 (!get_oid(av, &revkey)) { + append_ref(av, &revkey, 0); + return; + } + if (strchr(av, '*') || strchr(av, '?') || strchr(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, 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.argc) + argv_array_push(&default_args, "show-branch"); + argv_array_push(&default_args, value); + return 0; + } + + if (!strcmp(var, "color.showbranch")) { + showbranch_use_color = git_config_colorbool(var, value); + return 0; + } + + return git_color_default_config(var, value, 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; + 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 dense = 1; + 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(0, "topo-order", &sort_order, + N_("show commits in topological order"), + REV_SORT_IN_GRAPH_ORDER), + OPT_BOOL(0, "topics", &topics, + N_("show only commits not on the first branch")), + OPT_SET_INT(0, "sparse", &dense, + N_("show merges reachable from only one tip"), 0), + OPT_SET_INT(0, "date-order", &sort_order, + N_("topologically sort, maintaining date order " + "where possible"), + REV_SORT_BY_COMMIT_DATE), + { OPTION_CALLBACK, 'g', "reflog", &reflog_base, N_("<n>[,<base>]"), + N_("show <n> most recent ref-log entries starting at " + "base"), + PARSE_OPT_OPTARG | PARSE_OPT_LITERAL_ARGHELP, + parse_reflog_param }, + OPT_END() + }; + + git_config(git_show_branch_config, NULL); + + /* If nothing is specified, try the default first */ + if (ac == 1 && default_args.argc) { + ac = default_args.argc; + av = default_args.argv; + } + + 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(_("--reflog is incompatible with --all, --remotes, " + "--independent or --merge-base")); + } + + /* 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.hash, 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 (!dwim_ref(*av, strlen(*av), oid.hash, &ref)) + 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(ref, flags, at, -1, oid.hash, NULL, + NULL, NULL, &base); + } + } + + for (i = 0; i < reflog; i++) { + char *logmsg; + char *nth_desc; + const char *msg; + timestamp_t timestamp; + int tz; + + if (read_ref_at(ref, flags, 0, base+i, oid.hash, &logmsg, + ×tamp, &tz, NULL)) { + reflog = i; + break; + } + msg = strchr(logmsg, '\t'); + if (!msg) + msg = "(none)"; + else + msg++; + 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.hash, 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], + head_oid.hash, NULL)) + 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 (get_oid(ref_name[num_rev], &revkey)) + die(_("'%s' is not a valid ref."), ref_name[num_rev]); + commit = lookup_commit_reference(&revkey); + if (!commit) + die(_("cannot find commit %s (%s)"), + ref_name[num_rev], oid_to_hex(&revkey)); + parse_commit(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, ref_name, 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], + head_oid.hash, + rev[i]->object.oid.hash); + 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 (dense && 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 = '+'; + 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; + } + return 0; +} diff --git a/builtin/show-ref.c b/builtin/show-ref.c new file mode 100644 index 0000000000..013d241abc --- /dev/null +++ b/builtin/show-ref.c @@ -0,0 +1,221 @@ +#include "builtin.h" +#include "cache.h" +#include "refs.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 [-q | --quiet] [--verify] [--head] [-d | --dereference] [-s | --hash[=<n>]] [--abbrev[=<n>]] [--tags] [--heads] [--] [<pattern>...]"), + N_("git show-ref --exclude-existing[=<pattern>]"), + NULL +}; + +static int deref_tags, show_head, tags_only, heads_only, found_match, verify, + quiet, hash_only, abbrev, exclude_arg; +static const char **pattern; +static const char *exclude_existing_arg; + +static void show_one(const char *refname, const struct object_id *oid) +{ + const char *hex; + struct object_id peeled; + + if (!has_sha1_file(oid->hash)) + die("git show-ref: bad ref %s (%s)", refname, + oid_to_hex(oid)); + + if (quiet) + return; + + hex = find_unique_abbrev(oid->hash, abbrev); + if (hash_only) + printf("%s\n", hex); + else + printf("%s %s\n", hex, refname); + + if (!deref_tags) + return; + + if (!peel_ref(refname, peeled.hash)) { + hex = find_unique_abbrev(peeled.hash, abbrev); + printf("%s %s^{}\n", hex, refname); + } +} + +static int show_ref(const char *refname, const struct object_id *oid, + int flag, void *cbdata) +{ + if (show_head && !strcmp(refname, "HEAD")) + goto match; + + if (tags_only || heads_only) { + int match; + + match = heads_only && starts_with(refname, "refs/heads/"); + match |= tags_only && starts_with(refname, "refs/tags/"); + if (!match) + return 0; + } + if (pattern) { + int reflen = strlen(refname); + const char **p = pattern, *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: + found_match++; + + show_one(refname, oid); + + return 0; +} + +static int add_existing(const char *refname, const struct object_id *oid, + int flag, void *cbdata) +{ + struct string_list *list = (struct string_list *)cbdata; + string_list_insert(list, refname); + return 0; +} + +/* + * 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 exclude_existing(const char *match) +{ + static struct string_list existing_refs = STRING_LIST_INIT_DUP; + char buf[1024]; + int matchlen = match ? strlen(match) : 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 (match) { + int reflen = buf + len - ref; + if (reflen < matchlen) + continue; + if (strncmp(ref, match, matchlen)) + 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); + } + } + return 0; +} + +static int hash_callback(const struct option *opt, const char *arg, int unset) +{ + hash_only = 1; + /* Use full length SHA1 if no argument */ + if (!arg) + return 0; + return parse_opt_abbrev_cb(opt, arg, unset); +} + +static int exclude_existing_callback(const struct option *opt, const char *arg, + int unset) +{ + exclude_arg = 1; + *(const char **)opt->value = arg; + return 0; +} + +static const struct option show_ref_options[] = { + OPT_BOOL(0, "tags", &tags_only, N_("only show tags (can be combined with heads)")), + OPT_BOOL(0, "heads", &heads_only, N_("only show heads (can be combined with tags)")), + OPT_BOOL(0, "verify", &verify, N_("stricter reference checking, " + "requires exact ref path")), + OPT_HIDDEN_BOOL('h', NULL, &show_head, + N_("show the HEAD reference, even if it would be filtered out")), + OPT_BOOL(0, "head", &show_head, + N_("show the HEAD reference, even if it would be filtered out")), + OPT_BOOL('d', "dereference", &deref_tags, + N_("dereference tags into object IDs")), + { OPTION_CALLBACK, 's', "hash", &abbrev, N_("n"), + N_("only show SHA1 hash using <n> digits"), + PARSE_OPT_OPTARG, &hash_callback }, + OPT__ABBREV(&abbrev), + OPT__QUIET(&quiet, + N_("do not print results to stdout (useful with --verify)")), + { OPTION_CALLBACK, 0, "exclude-existing", &exclude_existing_arg, + N_("pattern"), N_("show refs from stdin that aren't in local repository"), + PARSE_OPT_OPTARG | PARSE_OPT_NONEG, exclude_existing_callback }, + OPT_END() +}; + +int cmd_show_ref(int argc, const char **argv, const char *prefix) +{ + argc = parse_options(argc, argv, prefix, show_ref_options, + show_ref_usage, 0); + + if (exclude_arg) + return exclude_existing(exclude_existing_arg); + + pattern = argv; + if (!*pattern) + pattern = NULL; + + if (verify) { + if (!pattern) + die("--verify requires a reference"); + while (*pattern) { + struct object_id oid; + + if ((starts_with(*pattern, "refs/") || !strcmp(*pattern, "HEAD")) && + !read_ref(*pattern, oid.hash)) { + show_one(*pattern, &oid); + } + else if (!quiet) + die("'%s' - not a valid ref", *pattern); + else + return 1; + pattern++; + } + return 0; + } + + if (show_head) + head_ref(show_ref, NULL); + for_each_ref(show_ref, NULL); + if (!found_match) { + if (verify && !quiet) + die("No match"); + return 1; + } + return 0; +} diff --git a/builtin/stripspace.c b/builtin/stripspace.c new file mode 100644 index 0000000000..bdf0328869 --- /dev/null +++ b/builtin/stripspace.c @@ -0,0 +1,64 @@ +#include "builtin.h" +#include "cache.h" +#include "config.h" +#include "parse-options.h" +#include "strbuf.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); + free(msg); +} + +static const char * const stripspace_usage[] = { + N_("git stripspace [-s | --strip-comments]"), + N_("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; + + 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(NULL); + 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); + 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 0000000000..06ed02f994 --- /dev/null +++ b/builtin/submodule--helper.c @@ -0,0 +1,1286 @@ +#include "builtin.h" +#include "repository.h" +#include "cache.h" +#include "config.h" +#include "parse-options.h" +#include "quote.h" +#include "pathspec.h" +#include "dir.h" +#include "submodule.h" +#include "submodule-config.h" +#include "string-list.h" +#include "run-command.h" +#include "remote.h" +#include "refs.h" +#include "connect.h" + +static char *get_default_remote(void) +{ + char *dest = NULL, *ret; + struct strbuf sb = STRBUF_INIT; + const char *refname = resolve_ref_unsafe("HEAD", 0, NULL, NULL); + + if (!refname) + die(_("No such ref: %s"), "HEAD"); + + /* detached HEAD */ + if (!strcmp(refname, "HEAD")) + return xstrdup("origin"); + + if (!skip_prefix(refname, "refs/heads/", &refname)) + die(_("Expecting a full ref name, got %s"), refname); + + strbuf_addf(&sb, "branch.%s.remote", refname); + if (git_config_get_string(sb.buf, &dest)) + ret = xstrdup("origin"); + else + ret = dest; + + strbuf_release(&sb); + return ret; +} + +static int starts_with_dot_slash(const char *str) +{ + return str[0] == '.' && is_dir_sep(str[1]); +} + +static int starts_with_dot_dot_slash(const char *str) +{ + return str[0] == '.' && str[1] == '.' && is_dir_sep(str[2]); +} + +/* + * Returns 1 if it was the last chop before ':'. + */ +static int chop_last_dir(char **remoteurl, int is_relative) +{ + char *rfind = find_last_dir_sep(*remoteurl); + if (rfind) { + *rfind = '\0'; + return 0; + } + + rfind = strrchr(*remoteurl, ':'); + if (rfind) { + *rfind = '\0'; + return 1; + } + + if (is_relative || !strcmp(".", *remoteurl)) + die(_("cannot strip one component off url '%s'"), + *remoteurl); + + free(*remoteurl); + *remoteurl = xstrdup("."); + return 0; +} + +/* + * The `url` argument is the URL that navigates to the submodule origin + * repo. When relative, this URL is relative to the superproject origin + * URL repo. The `up_path` argument, if specified, is the relative + * path that navigates from the submodule working tree to the superproject + * working tree. Returns the origin URL of the submodule. + * + * Return either an absolute URL or filesystem path (if the superproject + * origin URL is an absolute URL or filesystem path, respectively) or a + * relative file system path (if the superproject origin URL is a relative + * file system path). + * + * When the output is a relative file system path, the path is either + * relative to the submodule working tree, if up_path is specified, or to + * the superproject working tree otherwise. + * + * NEEDSWORK: This works incorrectly on the domain and protocol part. + * remote_url url outcome expectation + * http://a.com/b ../c http://a.com/c as is + * http://a.com/b/ ../c http://a.com/c same as previous line, but + * ignore trailing slash in url + * http://a.com/b ../../c http://c error out + * http://a.com/b ../../../c http:/c error out + * http://a.com/b ../../../../c http:c error out + * http://a.com/b ../../../../../c .:c error out + * NEEDSWORK: Given how chop_last_dir() works, this function is broken + * when a local part has a colon in its path component, too. + */ +static char *relative_url(const char *remote_url, + const char *url, + const char *up_path) +{ + int is_relative = 0; + int colonsep = 0; + char *out; + char *remoteurl = xstrdup(remote_url); + struct strbuf sb = STRBUF_INIT; + size_t len = strlen(remoteurl); + + if (is_dir_sep(remoteurl[len-1])) + remoteurl[len-1] = '\0'; + + if (!url_is_local_not_ssh(remoteurl) || is_absolute_path(remoteurl)) + is_relative = 0; + else { + is_relative = 1; + /* + * Prepend a './' to ensure all relative + * remoteurls start with './' or '../' + */ + if (!starts_with_dot_slash(remoteurl) && + !starts_with_dot_dot_slash(remoteurl)) { + strbuf_reset(&sb); + strbuf_addf(&sb, "./%s", remoteurl); + free(remoteurl); + remoteurl = strbuf_detach(&sb, NULL); + } + } + /* + * When the url starts with '../', remove that and the + * last directory in remoteurl. + */ + while (url) { + if (starts_with_dot_dot_slash(url)) { + url += 3; + colonsep |= chop_last_dir(&remoteurl, is_relative); + } else if (starts_with_dot_slash(url)) + url += 2; + else + break; + } + strbuf_reset(&sb); + strbuf_addf(&sb, "%s%s%s", remoteurl, colonsep ? ":" : "/", url); + if (ends_with(url, "/")) + strbuf_setlen(&sb, sb.len - 1); + free(remoteurl); + + if (starts_with_dot_slash(sb.buf)) + out = xstrdup(sb.buf + 2); + else + out = xstrdup(sb.buf); + strbuf_reset(&sb); + + if (!up_path || !is_relative) + return out; + + strbuf_addf(&sb, "%s%s", up_path, out); + free(out); + return strbuf_detach(&sb, NULL); +} + +static int resolve_relative_url(int argc, const char **argv, const char *prefix) +{ + char *remoteurl = NULL; + char *remote = get_default_remote(); + const char *up_path = NULL; + char *res; + const char *url; + struct strbuf sb = STRBUF_INIT; + + if (argc != 2 && argc != 3) + die("resolve-relative-url only accepts one or two arguments"); + + url = argv[1]; + strbuf_addf(&sb, "remote.%s.url", remote); + free(remote); + + if (git_config_get_string(sb.buf, &remoteurl)) + /* the repository is its own authoritative upstream */ + remoteurl = xgetcwd(); + + if (argc == 3) + up_path = argv[2]; + + res = relative_url(remoteurl, url, up_path); + puts(res); + free(res); + free(remoteurl); + return 0; +} + +static int resolve_relative_url_test(int argc, const char **argv, const char *prefix) +{ + char *remoteurl, *res; + const char *up_path, *url; + + if (argc != 4) + die("resolve-relative-url-test only accepts three arguments: <up_path> <remoteurl> <url>"); + + up_path = argv[1]; + remoteurl = xstrdup(argv[2]); + url = argv[3]; + + if (!strcmp(up_path, "(null)")) + up_path = NULL; + + res = relative_url(remoteurl, url, up_path); + puts(res); + free(res); + free(remoteurl); + return 0; +} + +struct module_list { + const struct cache_entry **entries; + int alloc, nr; +}; +#define MODULE_LIST_INIT { NULL, 0, 0 } + +static int module_list_compute(int argc, 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 (read_cache() < 0) + die(_("index file corrupt")); + + for (i = 0; i < active_nr; i++) { + const struct cache_entry *ce = active_cache[i]; + + if (!match_pathspec(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 < active_nr && + !strcmp(ce->name, active_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, prefix)) + 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; + } + + free(list->entries); + *list = active_modules; +} + +static int module_list(int argc, const char **argv, const char *prefix) +{ + int i; + struct pathspec pathspec; + struct module_list list = MODULE_LIST_INIT; + + struct option module_list_options[] = { + OPT_STRING(0, "prefix", &prefix, + N_("path"), + N_("alternative anchor for relative paths")), + OPT_END() + }; + + const char *const git_submodule_helper_usage[] = { + N_("git submodule--helper list [--prefix=<path>] [<path>...]"), + NULL + }; + + argc = parse_options(argc, argv, prefix, module_list_options, + git_submodule_helper_usage, 0); + + if (module_list_compute(argc, argv, prefix, &pathspec, &list) < 0) + return 1; + + for (i = 0; i < list.nr; i++) { + const struct cache_entry *ce = list.entries[i]; + + if (ce_stage(ce)) + printf("%06o %s U\t", ce->ce_mode, sha1_to_hex(null_sha1)); + else + printf("%06o %s %d\t", ce->ce_mode, + oid_to_hex(&ce->oid), ce_stage(ce)); + + fprintf(stdout, "%s\n", ce->name); + } + return 0; +} + +static void init_submodule(const char *path, const char *prefix, int quiet) +{ + const struct submodule *sub; + struct strbuf sb = STRBUF_INIT; + char *upd = NULL, *url = NULL, *displaypath; + + if (prefix && get_super_prefix()) + die("BUG: cannot have prefix and superprefix"); + else if (prefix) + displaypath = xstrdup(relative_path(path, prefix, &sb)); + else if (get_super_prefix()) { + strbuf_addf(&sb, "%s%s", get_super_prefix(), path); + displaypath = strbuf_detach(&sb, NULL); + } else + displaypath = xstrdup(path); + + sub = submodule_from_path(&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_reset(&sb); + strbuf_addf(&sb, "submodule.%s.active", sub->name); + git_config_set_gently(sb.buf, "true"); + } + + /* + * 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_reset(&sb); + 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 *remoteurl, *relurl; + char *remote = get_default_remote(); + struct strbuf remotesb = STRBUF_INIT; + strbuf_addf(&remotesb, "remote.%s.url", remote); + free(remote); + + if (git_config_get_string(remotesb.buf, &remoteurl)) { + warning(_("could not lookup configuration '%s'. Assuming this repository is its own authoritative upstream."), remotesb.buf); + remoteurl = xgetcwd(); + } + relurl = relative_url(remoteurl, url, NULL); + strbuf_release(&remotesb); + free(remoteurl); + free(url); + url = relurl; + } + + if (git_config_set_gently(sb.buf, url)) + die(_("Failed to register url for submodule path '%s'"), + displaypath); + if (!quiet) + fprintf(stderr, + _("Submodule '%s' (%s) registered for path '%s'\n"), + sub->name, url, displaypath); + } + + /* Copy "update" setting when it is not set yet */ + strbuf_reset(&sb); + strbuf_addf(&sb, "submodule.%s.update", sub->name); + if (git_config_get_string(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 = xstrdup("none"); + } else + upd = xstrdup(submodule_strategy_to_string(&sub->update_strategy)); + + 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); + free(upd); +} + +static int module_init(int argc, const char **argv, const char *prefix) +{ + struct pathspec pathspec; + struct module_list list = MODULE_LIST_INIT; + int quiet = 0; + int i; + + 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--helper init [<path>]"), + NULL + }; + + argc = parse_options(argc, argv, prefix, module_init_options, + git_submodule_helper_usage, 0); + + if (module_list_compute(argc, argv, prefix, &pathspec, &list) < 0) + return 1; + + /* + * If there are no path args and submodule.active is set then, + * by default, only initialize 'active' modules. + */ + if (!argc && git_config_get_value_multi("submodule.active")) + module_list_active(&list); + + for (i = 0; i < list.nr; i++) + init_submodule(list.entries[i]->name, prefix, quiet); + + return 0; +} + +static int module_name(int argc, const char **argv, const char *prefix) +{ + const struct submodule *sub; + + if (argc != 2) + usage(_("git submodule--helper name <path>")); + + sub = submodule_from_path(&null_oid, argv[1]); + + if (!sub) + die(_("no submodule mapping found in .gitmodules for path '%s'"), + argv[1]); + + printf("%s\n", sub->name); + + return 0; +} + +static int clone_submodule(const char *path, const char *gitdir, const char *url, + const char *depth, struct string_list *reference, + int quiet, int progress) +{ + struct child_process cp = CHILD_PROCESS_INIT; + + argv_array_push(&cp.args, "clone"); + argv_array_push(&cp.args, "--no-checkout"); + if (quiet) + argv_array_push(&cp.args, "--quiet"); + if (progress) + argv_array_push(&cp.args, "--progress"); + if (depth && *depth) + argv_array_pushl(&cp.args, "--depth", depth, NULL); + if (reference->nr) { + struct string_list_item *item; + for_each_string_list_item(item, reference) + argv_array_pushl(&cp.args, "--reference", + item->string, NULL); + } + if (gitdir && *gitdir) + argv_array_pushl(&cp.args, "--separate-git-dir", gitdir, NULL); + + argv_array_push(&cp.args, url); + argv_array_push(&cp.args, path); + + cp.git_cmd = 1; + prepare_submodule_repo_env(&cp.env_array); + cp.no_stdin = 1; + + return run_command(&cp); +} + +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 { NULL, \ + SUBMODULE_ALTERNATE_ERROR_IGNORE, NULL } + +static int add_possible_reference_from_superproject( + struct alternate_object_database *alt, void *sas_cb) +{ + struct submodule_alternate_setup *sas = sas_cb; + + /* + * If the alternate object store is another repository, try the + * standard layout with .git/(modules/<name>)+/objects + */ + if (ends_with(alt->path, "/objects")) { + char *sm_alternate; + struct strbuf sb = STRBUF_INIT; + struct strbuf err = STRBUF_INIT; + strbuf_add(&sb, alt->path, strlen(alt->path) - strlen("objects")); + + /* + * 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_addf(&sb, "modules/%s/", sas->submodule_name); + + sm_alternate = compute_alternate_path(sb.buf, &err); + if (sm_alternate) { + string_list_append(sas->reference, xstrdup(sb.buf)); + free(sm_alternate); + } else { + switch (sas->error_mode) { + case SUBMODULE_ALTERNATE_ERROR_DIE: + die(_("submodule '%s' cannot add alternate: %s"), + sas->submodule_name, err.buf); + case SUBMODULE_ALTERNATE_ERROR_INFO: + fprintf(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 int module_clone(int argc, const char **argv, const char *prefix) +{ + const char *name = NULL, *url = NULL, *depth = NULL; + int quiet = 0; + int progress = 0; + char *p, *path = NULL, *sm_gitdir; + struct strbuf sb = STRBUF_INIT; + struct string_list reference = STRING_LIST_INIT_NODUP; + char *sm_alternate = NULL, *error_strategy = NULL; + + struct option module_clone_options[] = { + OPT_STRING(0, "prefix", &prefix, + N_("path"), + N_("alternative anchor for relative paths")), + OPT_STRING(0, "path", &path, + N_("path"), + N_("where the new submodule will be cloned to")), + OPT_STRING(0, "name", &name, + N_("string"), + N_("name of the new submodule")), + OPT_STRING(0, "url", &url, + N_("string"), + N_("url where to clone the submodule from")), + OPT_STRING_LIST(0, "reference", &reference, + N_("repo"), + N_("reference repository")), + OPT_STRING(0, "depth", &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_END() + }; + + const char *const git_submodule_helper_usage[] = { + N_("git submodule--helper clone [--prefix=<path>] [--quiet] " + "[--reference <repository>] [--name <name>] [--depth <depth>] " + "--url <url> --path <path>"), + NULL + }; + + argc = parse_options(argc, argv, prefix, module_clone_options, + git_submodule_helper_usage, 0); + + if (argc || !url || !path || !*path) + usage_with_options(git_submodule_helper_usage, + module_clone_options); + + strbuf_addf(&sb, "%s/modules/%s", get_git_dir(), name); + sm_gitdir = absolute_pathdup(sb.buf); + strbuf_reset(&sb); + + if (!is_absolute_path(path)) { + strbuf_addf(&sb, "%s/%s", get_git_work_tree(), path); + path = strbuf_detach(&sb, NULL); + } else + path = xstrdup(path); + + 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(name, &reference); + + if (clone_submodule(path, sm_gitdir, url, depth, &reference, + quiet, progress)) + die(_("clone of '%s' into submodule path '%s' failed"), + url, path); + } else { + if (safe_create_leading_directories_const(path) < 0) + die(_("could not create directory '%s'"), path); + strbuf_addf(&sb, "%s/index", sm_gitdir); + unlink_or_warn(sb.buf); + strbuf_reset(&sb); + } + + /* Connect module worktree and git dir */ + connect_work_tree_and_git_dir(path, sm_gitdir); + + p = git_pathdup_submodule(path, "config"); + if (!p) + die(_("could not get submodule directory for '%s'"), 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); + + strbuf_release(&sb); + free(sm_gitdir); + free(path); + free(p); + return 0; +} + +struct submodule_update_clone { + /* index into 'list', the list of submodules to look into for cloning */ + int current; + struct module_list list; + unsigned warn_if_uninitialized : 1; + + /* update parameter passed via commandline */ + struct submodule_update_strategy update; + + /* configuration parameters which are passed on to the children */ + int progress; + int quiet; + int recommend_shallow; + struct string_list references; + const char *depth; + const char *recursive_prefix; + const char *prefix; + + /* Machine-readable status lines to be consumed by git-submodule.sh */ + struct string_list projectlines; + + /* 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, MODULE_LIST_INIT, 0, \ + SUBMODULE_UPDATE_STRATEGY_INIT, 0, 0, -1, STRING_LIST_INIT_DUP, \ + NULL, NULL, NULL, \ + STRING_LIST_INIT_DUP, 0, NULL, 0, 0} + + +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->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; + struct strbuf displaypath_sb = STRBUF_INIT; + struct strbuf sb = STRBUF_INIT; + const char *displaypath = NULL; + int needs_cloning = 0; + + if (ce_stage(ce)) { + if (suc->recursive_prefix) + strbuf_addf(&sb, "%s/%s", suc->recursive_prefix, ce->name); + else + strbuf_addstr(&sb, ce->name); + strbuf_addf(out, _("Skipping unmerged submodule %s"), sb.buf); + strbuf_addch(out, '\n'); + goto cleanup; + } + + sub = submodule_from_path(&null_oid, ce->name); + + if (suc->recursive_prefix) + displaypath = relative_path(suc->recursive_prefix, + ce->name, &displaypath_sb); + else + displaypath = 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_const(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.type == SM_UPDATE_NONE + || (suc->update.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_const(the_repository, sb.buf, &url)) + url = sub->url; + + strbuf_reset(&sb); + strbuf_addf(&sb, "%s/.git", ce->name); + needs_cloning = !file_exists(sb.buf); + + strbuf_reset(&sb); + strbuf_addf(&sb, "%06o %s %d %d\t%s\n", ce->ce_mode, + oid_to_hex(&ce->oid), ce_stage(ce), + needs_cloning, ce->name); + string_list_append(&suc->projectlines, sb.buf); + + if (!needs_cloning) + goto cleanup; + + child->git_cmd = 1; + child->no_stdin = 1; + child->stdout_to_stderr = 1; + child->err = -1; + argv_array_push(&child->args, "submodule--helper"); + argv_array_push(&child->args, "clone"); + if (suc->progress) + argv_array_push(&child->args, "--progress"); + if (suc->quiet) + argv_array_push(&child->args, "--quiet"); + if (suc->prefix) + argv_array_pushl(&child->args, "--prefix", suc->prefix, NULL); + if (suc->recommend_shallow && sub->recommend_shallow == 1) + argv_array_push(&child->args, "--depth=1"); + argv_array_pushl(&child->args, "--path", sub->path, NULL); + argv_array_pushl(&child->args, "--name", sub->name, NULL); + argv_array_pushl(&child->args, "--url", url, NULL); + if (suc->references.nr) { + struct string_list_item *item; + for_each_string_list_item(item, &suc->references) + argv_array_pushl(&child->args, "--reference", item->string, NULL); + } + if (suc->depth) + argv_array_push(&child->args, suc->depth); + +cleanup: + strbuf_reset(&displaypath_sb); + strbuf_reset(&sb); + + 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->list.nr; suc->current++) { + ce = suc->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->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, + void *suc_cb, + void *idx_task_cb) +{ + 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->list.nr) { + ce = suc->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->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 gitmodules_update_clone_config(const char *var, const char *value, + void *cb) +{ + int *max_jobs = cb; + if (!strcmp(var, "submodule.fetchjobs")) + *max_jobs = parse_submodule_fetchjobs(var, value); + return 0; +} + +static int update_clone(int argc, const char **argv, const char *prefix) +{ + const char *update = NULL; + int max_jobs = 1; + struct string_list_item *item; + struct pathspec pathspec; + struct submodule_update_clone suc = SUBMODULE_UPDATE_CLONE_INIT; + + struct option module_update_clone_options[] = { + OPT_STRING(0, "prefix", &prefix, + N_("path"), + N_("path into the working tree")), + OPT_STRING(0, "recursive-prefix", &suc.recursive_prefix, + N_("path"), + N_("path into the working tree, across nested " + "submodule boundaries")), + OPT_STRING(0, "update", &update, + N_("string"), + N_("rebase, merge, checkout or none")), + OPT_STRING_LIST(0, "reference", &suc.references, N_("repo"), + N_("reference repository")), + OPT_STRING(0, "depth", &suc.depth, "<depth>", + N_("Create a shallow clone truncated to the " + "specified number of revisions")), + OPT_INTEGER('j', "jobs", &max_jobs, + N_("parallel jobs")), + OPT_BOOL(0, "recommend-shallow", &suc.recommend_shallow, + N_("whether the initial clone should follow the shallow recommendation")), + OPT__QUIET(&suc.quiet, N_("don't print cloning progress")), + OPT_BOOL(0, "progress", &suc.progress, + N_("force cloning progress")), + OPT_END() + }; + + const char *const git_submodule_helper_usage[] = { + N_("git submodule--helper update_clone [--prefix=<path>] [<path>...]"), + NULL + }; + suc.prefix = prefix; + + config_from_gitmodules(gitmodules_update_clone_config, &max_jobs); + git_config(gitmodules_update_clone_config, &max_jobs); + + argc = parse_options(argc, argv, prefix, module_update_clone_options, + git_submodule_helper_usage, 0); + + if (update) + if (parse_submodule_update_strategy(update, &suc.update) < 0) + die(_("bad value for update parameter")); + + if (module_list_compute(argc, argv, prefix, &pathspec, &suc.list) < 0) + return 1; + + if (pathspec.nr) + suc.warn_if_uninitialized = 1; + + run_processes_parallel(max_jobs, + update_clone_get_next_task, + update_clone_start_failure, + update_clone_task_finished, + &suc); + + /* + * 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) + return 1; + + for_each_string_list_item(item, &suc.projectlines) + fprintf(stdout, "%s", item->string); + + return 0; +} + +static int resolve_relative_path(int argc, const char **argv, const char *prefix) +{ + struct strbuf sb = STRBUF_INIT; + if (argc != 3) + die("submodule--helper relative-path takes exactly 2 arguments, got %d", argc); + + printf("%s", relative_path(argv[1], argv[2], &sb)); + strbuf_release(&sb); + return 0; +} + +static const char *remote_submodule_branch(const char *path) +{ + const struct submodule *sub; + const char *branch = NULL; + char *key; + + sub = submodule_from_path(&null_oid, path); + if (!sub) + return NULL; + + key = xstrfmt("submodule.%s.branch", sub->name); + if (repo_config_get_string_const(the_repository, key, &branch)) + branch = sub->branch; + free(key); + + if (!branch) + return "master"; + + if (!strcmp(branch, ".")) { + const char *refname = resolve_ref_unsafe("HEAD", 0, NULL, NULL); + + if (!refname) + die(_("No such ref: %s"), "HEAD"); + + /* detached HEAD */ + if (!strcmp(refname, "HEAD")) + die(_("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)) + die(_("Expecting a full ref name, got %s"), refname); + return refname; + } + + return branch; +} + +static int resolve_remote_submodule_branch(int argc, const char **argv, + const char *prefix) +{ + const char *ret; + struct strbuf sb = STRBUF_INIT; + if (argc != 2) + die("submodule--helper remote-branch takes exactly one arguments, got %d", argc); + + ret = remote_submodule_branch(argv[1]); + if (!ret) + die("submodule %s doesn't exist", argv[1]); + + printf("%s", ret); + strbuf_release(&sb); + return 0; +} + +static int push_check(int argc, const char **argv, const char *prefix) +{ + 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.hash, 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, refspec_nr = argc - 2; + struct ref *local_refs = get_local_heads(); + struct refspec *refspec = parse_push_refspec(refspec_nr, + argv + 2); + + for (i = 0; i < refspec_nr; i++) { + struct refspec *rs = refspec + 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); + } + } + free_refspec(refspec_nr, refspec); + } + free(head); + + return 0; +} + +static int absorb_git_dirs(int argc, const char **argv, const char *prefix) +{ + int i; + struct pathspec pathspec; + struct module_list list = MODULE_LIST_INIT; + unsigned flags = ABSORB_GITDIR_RECURSE_SUBMODULES; + + struct option embed_gitdir_options[] = { + OPT_STRING(0, "prefix", &prefix, + N_("path"), + N_("path into the working tree")), + OPT_BIT(0, "--recursive", &flags, N_("recurse into submodules"), + ABSORB_GITDIR_RECURSE_SUBMODULES), + OPT_END() + }; + + const char *const git_submodule_helper_usage[] = { + N_("git submodule--helper embed-git-dir [<path>...]"), + NULL + }; + + argc = parse_options(argc, argv, prefix, embed_gitdir_options, + git_submodule_helper_usage, 0); + + if (module_list_compute(argc, argv, prefix, &pathspec, &list) < 0) + return 1; + + for (i = 0; i < list.nr; i++) + absorb_git_dir_into_superproject(prefix, + list.entries[i]->name, flags); + + return 0; +} + +static int is_active(int argc, const char **argv, const char *prefix) +{ + if (argc != 2) + die("submodule--helper is-active takes exactly 1 argument"); + + return !is_submodule_active(the_repository, argv[1]); +} + +#define SUPPORT_SUPER_PREFIX (1<<0) + +struct cmd_struct { + const char *cmd; + int (*fn)(int, const char **, const char *); + unsigned option; +}; + +static struct cmd_struct commands[] = { + {"list", module_list, 0}, + {"name", module_name, 0}, + {"clone", module_clone, 0}, + {"update-clone", update_clone, 0}, + {"relative-path", resolve_relative_path, 0}, + {"resolve-relative-url", resolve_relative_url, 0}, + {"resolve-relative-url-test", resolve_relative_url_test, 0}, + {"init", module_init, SUPPORT_SUPER_PREFIX}, + {"remote-branch", resolve_remote_submodule_branch, 0}, + {"push-check", push_check, 0}, + {"absorb-git-dirs", absorb_git_dirs, SUPPORT_SUPER_PREFIX}, + {"is-active", is_active, 0}, +}; + +int cmd_submodule__helper(int argc, const char **argv, const char *prefix) +{ + int i; + if (argc < 2 || !strcmp(argv[1], "-h")) + usage("git submodule--helper <command>"); + + for (i = 0; i < ARRAY_SIZE(commands); i++) { + if (!strcmp(argv[1], commands[i].cmd)) { + if (get_super_prefix() && + !(commands[i].option & SUPPORT_SUPER_PREFIX)) + die(_("%s doesn't support --super-prefix"), + commands[i].cmd); + return commands[i].fn(argc - 1, argv + 1, prefix); + } + } + + die(_("'%s' is not a valid submodule--helper " + "subcommand"), argv[1]); +} diff --git a/builtin/symbolic-ref.c b/builtin/symbolic-ref.c new file mode 100644 index 0000000000..17aabaa679 --- /dev/null +++ b/builtin/symbolic-ref.c @@ -0,0 +1,78 @@ +#include "builtin.h" +#include "config.h" +#include "cache.h" +#include "refs.h" +#include "parse-options.h" + +static const char * const git_symbolic_ref_usage[] = { + N_("git symbolic-ref [<options>] <name> [<ref>]"), + N_("git symbolic-ref -d [-q] <name>"), + NULL +}; + +static int check_symref(const char *HEAD, int quiet, int shorten, int print) +{ + int flag; + const char *refname = resolve_ref_unsafe(HEAD, 0, 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) { + if (shorten) + refname = shorten_unambiguous_ref(refname, 0); + puts(refname); + } + return 0; +} + +int cmd_symbolic_ref(int argc, const char **argv, const char *prefix) +{ + int quiet = 0, delete = 0, shorten = 0, 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_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); + 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_NODEREF); + } + + switch (argc) { + case 1: + ret = check_symref(argv[0], quiet, shorten, 1); + break; + case 2: + if (!strcmp(argv[0], "HEAD") && + !starts_with(argv[1], "refs/")) + die("Refusing to point HEAD outside of refs/"); + 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 0000000000..b38329b593 --- /dev/null +++ b/builtin/tag.c @@ -0,0 +1,562 @@ +/* + * 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 "cache.h" +#include "config.h" +#include "builtin.h" +#include "refs.h" +#include "tag.h" +#include "run-command.h" +#include "parse-options.h" +#include "diff.h" +#include "revision.h" +#include "gpg-interface.h" +#include "sha1-array.h" +#include "column.h" +#include "ref-filter.h" + +static const char * const git_tag_usage[] = { + N_("git tag [-a | -s | -u <key-id>] [-f] [-m <msg> | -F <file>] <tagname> [<head>]"), + N_("git tag -d <tagname>..."), + N_("git tag -l [-n[<num>]] [--contains <commit>] [--no-contains <commit>] [--points-at <object>]" + "\n\t\t[--format=<format>] [--[no-]merged [<commit>]] [<pattern>...]"), + N_("git tag -v [--format=<format>] <tagname>..."), + NULL +}; + +static unsigned int colopts; +static int force_sign_annotate; + +static int list_tags(struct ref_filter *filter, struct ref_sorting *sorting, + struct ref_format *format) +{ + struct ref_array array; + 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); + ref_array_sort(sorting, &array); + + for (i = 0; i < array.nr; i++) + show_ref_array_item(array.items[i], format); + 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, const void *cb_data); + +static int for_each_tag_name(const char **argv, each_tag_name_fn fn, + const 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.hash)) { + 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 delete_tag(const char *name, const char *ref, + const struct object_id *oid, const void *cb_data) +{ + if (delete_ref(NULL, ref, oid->hash, 0)) + return 1; + printf(_("Deleted tag '%s' (was %s)\n"), name, find_unique_abbrev(oid->hash, DEFAULT_ABBREV)); + return 0; +} + +static int verify_tag(const char *name, const char *ref, + const struct object_id *oid, const void *cb_data) +{ + int flags; + const 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->hash, 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, void *cb) +{ + int status; + struct ref_sorting **sorting_tail = (struct ref_sorting **)cb; + + if (!strcmp(var, "tag.sort")) { + if (!value) + return config_error_nonbool(var); + parse_ref_sorting(sorting_tail, value); + return 0; + } + + status = git_gpg_config(var, value, cb); + if (status) + return status; + 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); + return git_color_default_config(var, value, cb); +} + +static void write_tag_body(int fd, const struct object_id *oid) +{ + unsigned long size; + enum object_type type; + char *buf, *sp; + + buf = read_sha1_file(oid->hash, &type, &size); + if (!buf) + return; + /* 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, parse_signature(sp, buf + size - sp)); + + free(buf); +} + +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_sha1_file(buf->buf, buf->len, tag_type, result->hash) < 0) + return error(_("unable to write tag file")); + return 0; +} + +struct create_tag_options { + unsigned int message_given:1; + unsigned int sign; + enum { + CLEANUP_NONE, + CLEANUP_SPACE, + CLEANUP_ALL + } cleanup_mode; +}; + +static void create_tag(const struct object_id *object, const char *tag, + struct strbuf *buf, struct create_tag_options *opt, + struct object_id *prev, struct object_id *result) +{ + enum object_type type; + struct strbuf header = STRBUF_INIT; + char *path = NULL; + + type = sha1_object_info(object->hash, NULL); + if (type <= OBJ_NONE) + die(_("bad object type.")); + + strbuf_addf(&header, + "object %s\n" + "type %s\n" + "tag %s\n" + "tagger %s\n\n", + oid_to_hex(object), + typename(type), + tag, + git_committer_info(IDENT_STRICT)); + + if (!opt->message_given) { + int fd; + + /* write the template message before editing: */ + path = git_pathdup("TAG_EDITMSG"); + fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0600); + if (fd < 0) + die_errno(_("could not create file '%s'"), path); + + 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, _(tag_template), tag, comment_line_char); + else + strbuf_commented_addf(&buf, _(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); + + 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); + } + if (path) { + unlink_or_warn(path); + free(path); + } +} + +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->hash, DEFAULT_ABBREV); + } + + strbuf_addstr(sb, " ("); + type = sha1_object_info(oid->hash, NULL); + switch (type) { + default: + strbuf_addstr(sb, "object of unknown type"); + break; + case OBJ_COMMIT: + if ((buf = read_sha1_file(oid->hash, &type, &size)) != NULL) { + 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(oid)) != NULL) + 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; + + 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; + const char *msgfile = NULL, *keyid = NULL; + struct msg_arg msg = { 0, STRBUF_INIT }; + struct ref_transaction *transaction; + struct strbuf err = STRBUF_INIT; + struct ref_filter filter; + static struct ref_sorting *sorting = NULL, **sorting_tail = &sorting; + struct ref_format format = REF_FORMAT_INIT; + int icase = 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('m', "message", &msg, N_("message"), + N_("tag message"), parse_msg_arg), + OPT_FILENAME('F', "file", &msgfile, N_("read message from file")), + OPT_BOOL('s', "sign", &opt.sign, N_("annotated and GPG-signed tag")), + OPT_STRING(0, "cleanup", &cleanup_arg, N_("mode"), + N_("how to strip spaces and #comments from message")), + 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")), + 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_CALLBACK(0 , "sort", sorting_tail, N_("key"), + N_("field name to sort on"), &parse_opt_ref_sorting), + { + 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() + }; + + setup_ref_filter_porcelain_msg(); + + git_config(git_tag_config, sorting_tail); + + memset(&opt, 0, sizeof(opt)); + memset(&filter, 0, sizeof(filter)); + filter.lines = -1; + + argc = parse_options(argc, argv, prefix, options, git_tag_usage, 0); + + if (keyid) { + opt.sign = 1; + set_signing_key(keyid); + } + create_tag_object = (opt.sign || annotate || msg.given || msgfile); + + if (!cmdmode) { + if (argc == 0) + cmdmode = 'l'; + else if (filter.with_commit || filter.no_commit || + filter.points_at.nr || filter.merge_commit || + filter.lines != -1) + cmdmode = 'l'; + } + + if (cmdmode == 'l') + setup_auto_pager("tag", 1); + + 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(_("--column and -n are incompatible")); + colopts = 0; + } + if (!sorting) + sorting = ref_default_sorting(); + sorting->ignore_case = icase; + filter.ignore_case = icase; + if (cmdmode == 'l') { + int ret; + 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(); + return ret; + } + if (filter.lines != -1) + die(_("-n option is only allowed in list mode")); + if (filter.with_commit) + die(_("--contains option is only allowed in list mode")); + if (filter.no_commit) + die(_("--no-contains option is only allowed in list mode")); + if (filter.points_at.nr) + die(_("--points-at option is only allowed in list mode")); + if (filter.merge_commit) + die(_("--merged and --no-merged options are only allowed in list mode")); + if (cmdmode == 'd') + return for_each_tag_name(argv, delete_tag, NULL); + if (cmdmode == 'v') { + if (format.format && verify_ref_format(&format)) + usage_with_options(git_tag_usage, options); + return for_each_tag_name(argv, verify_tag, &format); + } + + if (msg.given || msgfile) { + if (msg.given && msgfile) + die(_("only one -F or -m option is allowed.")); + 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 params")); + + if (get_oid(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.hash)) + oidclr(&prev); + else if (!force) + die(_("tag '%s' already exists"), tag); + + opt.message_given = msg.given || msgfile; + + 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; + create_tag(&object, tag, &buf, &opt, &prev, &object); + } + + transaction = ref_transaction_begin(&err); + if (!transaction || + ref_transaction_update(transaction, ref.buf, object.hash, prev.hash, + create_reflog ? REF_FORCE_CREATE_REFLOG : 0, + reflog_msg.buf, &err) || + ref_transaction_commit(transaction, &err)) + die("%s", err.buf); + ref_transaction_free(transaction); + if (force && !is_null_oid(&prev) && oidcmp(&prev, &object)) + printf(_("Updated tag '%s' (was %s)\n"), tag, find_unique_abbrev(prev.hash, DEFAULT_ABBREV)); + + UNLEAK(buf); + UNLEAK(ref); + UNLEAK(reflog_msg); + UNLEAK(msg); + UNLEAK(err); + return 0; +} diff --git a/builtin/unpack-file.c b/builtin/unpack-file.c new file mode 100644 index 0000000000..32e0155577 --- /dev/null +++ b/builtin/unpack-file.c @@ -0,0 +1,37 @@ +#include "builtin.h" +#include "config.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 = read_sha1_file(oid->hash, &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); + return path; +} + +int cmd_unpack_file(int argc, const char **argv, const char *prefix) +{ + struct object_id oid; + + if (argc != 2 || !strcmp(argv[1], "-h")) + usage("git unpack-file <sha1>"); + if (get_oid(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 0000000000..62ea264c46 --- /dev/null +++ b/builtin/unpack-objects.c @@ -0,0 +1,588 @@ +#include "builtin.h" +#include "cache.h" +#include "config.h" +#include "object.h" +#include "delta.h" +#include "pack.h" +#include "blob.h" +#include "commit.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_SHA_CTX ctx; +static struct fsck_options fsck_options = FSCK_OPTIONS_STRICT; + +/* + * 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; + obj = xcalloc(1, sizeof(struct obj_buffer)); + 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) { + git_SHA1_Update(&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")); +} + +static void *get_data(unsigned long size) +{ + git_zstream stream; + void *buf = xmallocz(size); + + memset(&stream, 0, sizeof(stream)); + + stream.next_out = buf; + stream.avail_out = size; + 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; + } + git_inflate_end(&stream); + 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; +}; + +#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_sha1_file(obj_buf->buffer, obj_buf->size, typename(obj->type), oid.hash) < 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, int type, void *data, struct fsck_options *options) +{ + 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 = sha1_object_info(obj->oid.hash, &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("Error in 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_sha1_file(buf, size, typename(type), obj_list[nr].oid.hash) < 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_sha1_file(buf, size, typename(type), obj_list[nr].oid.hash) < 0) + die("failed to write object"); + added_object(nr, type, buf, size); + free(buf); + + blob = lookup_blob(&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_sha1_file(buf, size, typename(type), obj_list[nr].oid.hash); + added_object(nr, type, buf, size); + obj = parse_object_buffer(&obj_list[nr].oid, type, size, buf, + &eaten); + if (!obj) + die("invalid %s", typename(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 (!oidcmp(&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 (!dry_run && buf) + write_object(nr, type, buf, size); + else + free(buf); +} + +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(base->hash); + 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) { + hashcpy(base_oid.hash, fill(GIT_SHA1_RAWSZ)); + use(GIT_SHA1_RAWSZ); + delta_data = get_data(delta_size); + if (dry_run || !delta_data) { + free(delta_data); + return; + } + if (has_object_file(&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 (dry_run || !delta_data) { + free(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 = read_sha1_file(base_oid.hash, &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_COMMIT: + case OBJ_TREE: + case OBJ_BLOB: + 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 progress *progress = NULL; + 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); + obj_list = xcalloc(nr_objects, sizeof(*obj_list)); + for (i = 0; i < nr_objects; i++) { + unpack_one(i); + display_progress(progress, i + 1); + } + stop_progress(&progress); + + if (delta_list) + die("unresolved deltas left after unpacking"); +} + +int cmd_unpack_objects(int argc, const char **argv, const char *prefix) +{ + int i; + struct object_id oid; + + check_replace_refs = 0; + + 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); + } + git_SHA1_Init(&ctx); + unpack_all(); + git_SHA1_Update(&ctx, buffer, offset); + git_SHA1_Final(oid.hash, &ctx); + if (strict) + write_rest(); + if (hashcmp(fill(GIT_SHA1_RAWSZ), oid.hash)) + die("final sha1 did not match"); + use(GIT_SHA1_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 0000000000..bf7420b808 --- /dev/null +++ b/builtin/update-index.c @@ -0,0 +1,1162 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "cache.h" +#include "config.h" +#include "lockfile.h" +#include "quote.h" +#include "cache-tree.h" +#include "tree-walk.h" +#include "builtin.h" +#include "refs.h" +#include "resolve-undo.h" +#include "parse-options.h" +#include "pathspec.h" +#include "dir.h" +#include "split-index.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; +#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; + + 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 = open(path, O_CREAT | O_RDWR, 0644); + if (fd < 0) + die_errno(_("failed to create file %s"), path); + 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 = cache_name_pos(path, namelen); + if (0 <= pos) { + if (mark) + active_cache[pos]->ce_flags |= flag; + else + active_cache[pos]->ce_flags &= ~flag; + active_cache[pos]->ce_flags |= CE_UPDATE_IN_BASE; + cache_tree_invalidate_path(&the_index, path); + active_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_cache(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, size; + struct cache_entry *ce; + + /* Was the old index entry already up-to-date? */ + if (old && !ce_stage(old) && !ce_match_stat(old, st, 0)) + return 0; + + size = cache_entry_size(len); + ce = xcalloc(1, size); + memcpy(ce->name, path, len); + ce->ce_flags = create_ce_flags(0); + ce->ce_namelen = len; + fill_stat_cache_info(ce, st); + ce->ce_mode = ce_mode_from_stat(old, st->st_mode); + + if (index_path(&ce->oid, path, st, + info_only ? 0 : HASH_WRITE_OBJECT)) { + free(ce); + return -1; + } + option = allow_add ? ADD_CACHE_OK_TO_ADD : 0; + option |= allow_replace ? ADD_CACHE_OK_TO_REPLACE : 0; + if (add_cache_entry(ce, option)) { + free(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 = cache_name_pos(path, len); + + /* Exact match: file or existing gitlink */ + if (pos >= 0) { + const struct cache_entry *ce = active_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.hash) < 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 < active_nr) { + const struct cache_entry *ce = active_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.hash)) + 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) +{ + int pos, len; + struct stat st; + 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 = cache_name_pos(path, len); + ce = pos < 0 ? NULL : active_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 (allow_remove && remove_file_from_cache(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 (lstat(path, &st) < 0) + return process_lstat_error(path, 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 size, len, option; + struct cache_entry *ce; + + if (!verify_path(path)) + return error("Invalid path '%s'", path); + + len = strlen(path); + size = cache_entry_size(len); + ce = xcalloc(1, size); + + 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_cache_entry(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 = cache_name_pos(path, strlen(path)); + if (pos < 0) + goto fail; + ce = active_cache[pos]; + if (chmod_cache_entry(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) +{ + if (!verify_path(path)) { + 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 (force_remove) { + if (remove_file_from_cache(path)) + die("git update-index: unable to remove %s", path); + report("remove '%s'", path); + return; + } + if (process_path(path)) + die("Unable to process path %s", path); + report("add '%s'", path); +} + +static void read_index_info(int nul_term_line) +{ + 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 < GIT_SHA1_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 - GIT_SHA1_HEXSZ, &oid) || + tab[-(GIT_SHA1_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)) { + fprintf(stderr, "Ignoring path %s\n", path_name); + continue; + } + + if (!mode) { + /* mode == 0 means there is no such path -- remove */ + if (remove_file_from_cache(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[-(GIT_SHA1_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 object_id head_oid; +static struct object_id merge_head_oid; + +static struct cache_entry *read_one_ent(const char *which, + struct object_id *ent, const char *path, + int namelen, int stage) +{ + unsigned mode; + struct object_id oid; + int size; + struct cache_entry *ce; + + if (get_tree_entry(ent->hash, path, oid.hash, &mode)) { + if (which) + error("%s: not in %s branch.", path, which); + return NULL; + } + if (mode == S_IFDIR) { + if (which) + error("%s: not a blob in %s branch.", path, which); + return NULL; + } + size = cache_entry_size(namelen); + ce = xcalloc(1, size); + + 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) +{ + int namelen = strlen(path); + int pos; + int ret = 0; + struct cache_entry *ce_2 = NULL, *ce_3 = NULL; + + /* See if there is such entry in the index. */ + pos = cache_name_pos(path, namelen); + if (0 <= pos) { + /* already merged */ + pos = unmerge_cache_entry_at(pos); + if (pos < active_nr) { + const struct cache_entry *ce = active_cache[pos]; + if (ce_stage(ce) && + ce_namelen(ce) == namelen && + !memcmp(ce->name, path, namelen)) + return 0; + } + /* no resolve-undo information; fall back */ + } else { + /* If there isn't, either it is unmerged, or + * resolved as "removed" by mistake. We do not + * want to do anything in the former case. + */ + pos = -pos-1; + if (pos < active_nr) { + const struct cache_entry *ce = active_cache[pos]; + if (ce_namelen(ce) == namelen && + !memcmp(ce->name, path, namelen)) { + fprintf(stderr, + "%s: skipping still unmerged path.\n", + path); + goto free_return; + } + } + } + + /* Grab blobs from given path from HEAD and MERGE_HEAD, + * stuff HEAD version in stage #2, + * stuff MERGE_HEAD version in stage #3. + */ + ce_2 = read_one_ent("our", &head_oid, path, namelen, 2); + ce_3 = read_one_ent("their", &merge_head_oid, path, namelen, 3); + + if (!ce_2 || !ce_3) { + ret = -1; + goto free_return; + } + if (!oidcmp(&ce_2->oid, &ce_3->oid) && + ce_2->ce_mode == ce_3->ce_mode) { + fprintf(stderr, "%s: identical in both, skipping.\n", + path); + goto free_return; + } + + remove_file_from_cache(path); + if (add_cache_entry(ce_2, ADD_CACHE_OK_TO_ADD)) { + error("%s: cannot add our version to the index.", path); + ret = -1; + goto free_return; + } + if (!add_cache_entry(ce_3, ADD_CACHE_OK_TO_ADD)) + return 0; + error("%s: cannot add their version to the index.", path); + ret = -1; + free_return: + free(ce_2); + free(ce_3); + return ret; +} + +static void read_head_pointers(void) +{ + if (read_ref("HEAD", head_oid.hash)) + die("No HEAD -- no initial commit yet?"); + if (read_ref("MERGE_HEAD", merge_head_oid.hash)) { + fprintf(stderr, "Not in the middle of a merge.\n"); + exit(0); + } +} + +static int do_unresolve(int ac, const char **av, + const char *prefix, int prefix_length) +{ + int i; + int err = 0; + + /* Read HEAD and MERGE_HEAD; if MERGE_HEAD does not exist, we + * are not doing a merge, so exit with success status. + */ + read_head_pointers(); + + 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(int ac, const char **av, + const char *prefix, int prefix_length) +{ + /* 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; + + parse_pathspec(&pathspec, 0, + PATHSPEC_PREFER_CWD, + prefix, av + 1); + + if (read_ref("HEAD", head_oid.hash)) + /* 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 < active_nr; pos++) { + const struct cache_entry *ce = active_cache[pos]; + struct cache_entry *old = NULL; + int save_nr; + char *path; + + if (ce_stage(ce) || !ce_path_match(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 && + !oidcmp(&ce->oid, &old->oid)) { + free(old); + continue; /* unchanged */ + } + /* 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 = active_nr; + path = xstrdup(ce->name); + update_one(path); + free(path); + free(old); + if (save_nr != active_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(); + read_cache_preload(NULL); + *o->has_errors |= refresh_cache(o->flags | flag); + return 0; +} + +static int refresh_callback(const struct option *opt, + const char *arg, int unset) +{ + return refresh(opt->value, 0); +} + +static int really_refresh_callback(const struct option *opt, + const char *arg, int unset) +{ + return refresh(opt->value, REFRESH_REALLY); +} + +static int chmod_callback(const struct option *opt, + const char *arg, int unset) +{ + char *flip = opt->value; + 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, + const char *arg, int unset) +{ + resolve_undo_clear(); + 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; + + 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 (get_oid_hex(endp, oid) || endp[GIT_SHA1_HEXSZ] != ',') + return -1; + *path = endp + GIT_SHA1_HEXSZ + 1; + return 0; +} + +static int cacheinfo_callback(struct parse_opt_ctx_t *ctx, + const struct option *opt, int unset) +{ + struct object_id oid; + unsigned int mode; + const char *path; + + 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 int stdin_cacheinfo_callback(struct parse_opt_ctx_t *ctx, + const struct option *opt, int unset) +{ + int *nul_term_line = opt->value; + + 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 int stdin_callback(struct parse_opt_ctx_t *ctx, + const struct option *opt, int unset) +{ + int *read_from_stdin = opt->value; + + if (ctx->argc != 1) + return error("option '%s' must be the last argument", opt->long_name); + *read_from_stdin = 1; + return 0; +} + +static int unresolve_callback(struct parse_opt_ctx_t *ctx, + const struct option *opt, int flags) +{ + int *has_errors = opt->value; + const char *prefix = startup_info->prefix; + + /* consume remaining arguments. */ + *has_errors = do_unresolve(ctx->argc, ctx->argv, + prefix, prefix ? strlen(prefix) : 0); + if (*has_errors) + active_cache_changed = 0; + + ctx->argv += ctx->argc - 1; + ctx->argc = 1; + return 0; +} + +static int reupdate_callback(struct parse_opt_ctx_t *ctx, + const struct option *opt, int flags) +{ + int *has_errors = opt->value; + const char *prefix = startup_info->prefix; + + /* consume remaining arguments. */ + setup_work_tree(); + *has_errors = do_reupdate(ctx->argc, ctx->argv, + prefix, prefix ? strlen(prefix) : 0); + if (*has_errors) + active_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; + 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 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), + {OPTION_CALLBACK, 0, "refresh", &refresh_args, NULL, + N_("refresh stat information"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, + refresh_callback}, + {OPTION_CALLBACK, 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, + (parse_opt_cb *) cacheinfo_callback}, + {OPTION_CALLBACK, 0, "chmod", &set_executable_bit, N_("(+/-)x"), + N_("override the executable bit of the listed files"), + PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP, + 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_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, + (parse_opt_cb *) 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, + (parse_opt_cb *) 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, + (parse_opt_cb *) unresolve_callback}, + {OPTION_LOWLEVEL_CALLBACK, 'g', "again", &has_errors, NULL, + N_("only update entries that differ from HEAD"), + PARSE_OPT_NONEG | PARSE_OPT_NOARG, + (parse_opt_cb *) 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), + {OPTION_CALLBACK, 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_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_END() + }; + + if (argc == 2 && !strcmp(argv[1], "-h")) + usage_with_options(update_index_usage, options); + + git_config(git_default_config, NULL); + + /* we will diagnose later if it turns out that we need to update it */ + newfd = hold_locked_index(&lock_file, 0); + if (newfd < 0) + lock_error = errno; + + entries = read_cache(); + if (entries < 0) + die("cache corrupted"); + + /* + * 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); + 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: + exit(129); + 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 < 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); + + if (the_index.version != preferred_index_format) + active_cache_changed |= SOMETHING_CHANGED; + 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); + } + + 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); + } + + switch (untracked_cache) { + case UC_UNSPECIFIED: + break; + case UC_DISABLE: + if (git_config_get_untracked_cache() == 1) + 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 (git_config_get_untracked_cache() == 0) + 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: + die("BUG: bad untracked_cache value: %d", untracked_cache); + } + + if (active_cache_changed) { + 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 0000000000..6b90c5dead --- /dev/null +++ b/builtin/update-ref.c @@ -0,0 +1,443 @@ +#include "cache.h" +#include "config.h" +#include "refs.h" +#include "builtin.h" +#include "parse-options.h" +#include "quote.h" +#include "argv-array.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 int update_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(struct strbuf *input, 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(struct strbuf *input, const char **next, + struct object_id *oid, + const char *command, const char *refname, + int flags) +{ + struct strbuf arg = STRBUF_INIT; + int ret = 0; + + if (*next == input->buf + input->len) + 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 (get_oid(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 == input->buf + input->len) + goto eof; + strbuf_addstr(&arg, *next); + *next += arg.len; + + if (arg.len) { + if (get_oid(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 const char *parse_cmd_update(struct ref_transaction *transaction, + struct strbuf *input, const char *next) +{ + struct strbuf err = STRBUF_INIT; + char *refname; + struct object_id new_oid, old_oid; + int have_old; + + refname = parse_refname(input, &next); + if (!refname) + die("update: missing <ref>"); + + if (parse_next_oid(input, &next, &new_oid, "update", refname, + PARSE_SHA1_ALLOW_EMPTY)) + die("update %s: missing <newvalue>", refname); + + have_old = !parse_next_oid(input, &next, &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.hash, have_old ? old_oid.hash : NULL, + update_flags | create_reflog_flag, + msg, &err)) + die("%s", err.buf); + + update_flags = 0; + free(refname); + strbuf_release(&err); + + return next; +} + +static const char *parse_cmd_create(struct ref_transaction *transaction, + struct strbuf *input, const char *next) +{ + struct strbuf err = STRBUF_INIT; + char *refname; + struct object_id new_oid; + + refname = parse_refname(input, &next); + if (!refname) + die("create: missing <ref>"); + + if (parse_next_oid(input, &next, &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.hash, + update_flags | create_reflog_flag, + msg, &err)) + die("%s", err.buf); + + update_flags = 0; + free(refname); + strbuf_release(&err); + + return next; +} + +static const char *parse_cmd_delete(struct ref_transaction *transaction, + struct strbuf *input, const char *next) +{ + struct strbuf err = STRBUF_INIT; + char *refname; + struct object_id old_oid; + int have_old; + + refname = parse_refname(input, &next); + if (!refname) + die("delete: missing <ref>"); + + if (parse_next_oid(input, &next, &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.hash : NULL, + update_flags, msg, &err)) + die("%s", err.buf); + + update_flags = 0; + free(refname); + strbuf_release(&err); + + return next; +} + +static const char *parse_cmd_verify(struct ref_transaction *transaction, + struct strbuf *input, const char *next) +{ + struct strbuf err = STRBUF_INIT; + char *refname; + struct object_id old_oid; + + refname = parse_refname(input, &next); + if (!refname) + die("verify: missing <ref>"); + + if (parse_next_oid(input, &next, &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.hash, + update_flags, &err)) + die("%s", err.buf); + + update_flags = 0; + free(refname); + strbuf_release(&err); + + return next; +} + +static const char *parse_cmd_option(struct strbuf *input, const char *next) +{ + if (!strncmp(next, "no-deref", 8) && next[8] == line_termination) + update_flags |= REF_NODEREF; + else + die("option unknown: %s", next); + return next + 8; +} + +static void update_refs_stdin(struct ref_transaction *transaction) +{ + struct strbuf input = STRBUF_INIT; + const char *next; + + if (strbuf_read(&input, 0, 1000) < 0) + die_errno("could not read from stdin"); + next = input.buf; + /* Read each line dispatch its command */ + while (next < input.buf + input.len) { + if (*next == line_termination) + die("empty command in input"); + else if (isspace(*next)) + die("whitespace before command: %s", next); + else if (starts_with(next, "update ")) + next = parse_cmd_update(transaction, &input, next + 7); + else if (starts_with(next, "create ")) + next = parse_cmd_create(transaction, &input, next + 7); + else if (starts_with(next, "delete ")) + next = parse_cmd_delete(transaction, &input, next + 7); + else if (starts_with(next, "verify ")) + next = parse_cmd_verify(transaction, &input, next + 7); + else if (starts_with(next, "option ")) + next = parse_cmd_option(&input, next + 7); + else + die("unknown command: %s", next); + + next++; + } + + 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; + unsigned int flags = 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 (read_stdin) { + struct strbuf err = STRBUF_INIT; + struct ref_transaction *transaction; + + transaction = ref_transaction_begin(&err); + if (!transaction) + die("%s", err.buf); + if (delete || no_deref || argc > 0) + usage_with_options(git_update_ref_usage, options); + if (end_null) + line_termination = '\0'; + update_refs_stdin(transaction); + if (ref_transaction_commit(transaction, &err)) + die("%s", err.buf); + ref_transaction_free(transaction); + strbuf_release(&err); + 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 (get_oid(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 (get_oid(oldval, &oldoid)) + die("%s: not a valid old SHA1", oldval); + } + + if (no_deref) + flags = REF_NODEREF; + 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.hash : NULL, + flags); + else + return update_ref(msg, refname, oid.hash, oldval ? oldoid.hash : NULL, + 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 0000000000..873070e517 --- /dev/null +++ b/builtin/update-server-info.c @@ -0,0 +1,26 @@ +#include "cache.h" +#include "config.h" +#include "builtin.h" +#include "parse-options.h" + +static const char * const update_server_info_usage[] = { + N_("git update-server-info [--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")), + 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 0000000000..84532ae9a9 --- /dev/null +++ b/builtin/upload-archive.c @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2006 Franck Bui-Huu + */ +#include "cache.h" +#include "builtin.h" +#include "archive.h" +#include "pkt-line.h" +#include "sideband.h" +#include "run-command.h" +#include "argv-array.h" + +static const char upload_archive_usage[] = + "git upload-archive <repo>"; + +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 argv_array sent_argv = ARGV_ARRAY_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]); + + /* put received options in sent_argv[] */ + argv_array_push(&sent_argv, "git-upload-archive"); + for (;;) { + char *buf = packet_read_line(0, NULL); + if (!buf) + break; /* got a flush */ + if (sent_argv.argc > MAX_ARGS) + die("Too many options (>%d)", MAX_ARGS - 1); + + if (!starts_with(buf, arg_cmd)) + die("'argument' token or flush expected"); + argv_array_push(&sent_argv, buf + strlen(arg_cmd)); + } + + /* parse all options sent by the client */ + return write_archive(sent_argv.argc, sent_argv.argv, prefix, 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 = { argv }; + + 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. + */ + argv[0] = "upload-archive--writer"; + writer.out = writer.err = -1; + writer.git_cmd = 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/var.c b/builtin/var.c new file mode 100644 index 0000000000..6c6f46b4ae --- /dev/null +++ b/builtin/var.c @@ -0,0 +1,94 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Eric Biederman, 2005 + */ +#include "builtin.h" +#include "config.h" + +static const char var_usage[] = "git var (-l | <variable>)"; + +static const char *editor(int flag) +{ + const char *pgm = git_editor(); + + if (!pgm && flag & IDENT_STRICT) + die("Terminal is dumb, but EDITOR unset"); + + return pgm; +} + +static const char *pager(int flag) +{ + const char *pgm = git_pager(1); + + if (!pgm) + pgm = "cat"; + return pgm; +} + +struct git_var { + const char *name; + const char *(*read)(int); +}; +static struct git_var git_vars[] = { + { "GIT_COMMITTER_IDENT", git_committer_info }, + { "GIT_AUTHOR_IDENT", git_author_info }, + { "GIT_EDITOR", editor }, + { "GIT_PAGER", pager }, + { "", NULL }, +}; + +static void list_vars(void) +{ + struct git_var *ptr; + const char *val; + + for (ptr = git_vars; ptr->read; ptr++) + if ((val = ptr->read(0))) + printf("%s=%s\n", ptr->name, val); +} + +static const char *read_var(const char *var) +{ + struct git_var *ptr; + const char *val; + val = NULL; + for (ptr = git_vars; ptr->read; ptr++) { + if (strcmp(var, ptr->name) == 0) { + val = ptr->read(IDENT_STRICT); + break; + } + } + return val; +} + +static int show_config(const char *var, const char *value, void *cb) +{ + if (value) + printf("%s=%s\n", var, value); + else + printf("%s\n", var); + return git_default_config(var, value, cb); +} + +int cmd_var(int argc, const char **argv, const char *prefix) +{ + const char *val = NULL; + 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); + val = read_var(argv[1]); + if (!val) + usage(var_usage); + + printf("%s\n", val); + + return 0; +} diff --git a/builtin/verify-commit.c b/builtin/verify-commit.c new file mode 100644 index 0000000000..ba38ac9b15 --- /dev/null +++ b/builtin/verify-commit.c @@ -0,0 +1,95 @@ +/* + * Builtin "git commit-commit" + * + * Copyright (c) 2014 Michael J Gruber <git@drmicha.warpmail.net> + * + * Based on git-verify-tag + */ +#include "cache.h" +#include "config.h" +#include "builtin.h" +#include "commit.h" +#include "run-command.h" +#include <signal.h> +#include "parse-options.h" +#include "gpg-interface.h" + +static const char * const verify_commit_usage[] = { + N_("git verify-commit [-v | --verbose] <commit>..."), + NULL +}; + +static int run_gpg_verify(const struct object_id *oid, const char *buf, unsigned long size, unsigned flags) +{ + struct signature_check signature_check; + int ret; + + memset(&signature_check, 0, sizeof(signature_check)); + + ret = check_commit_signature(lookup_commit(oid), &signature_check); + print_signature_buffer(&signature_check, flags); + + signature_check_clear(&signature_check); + return ret; +} + +static int verify_commit(const char *name, unsigned flags) +{ + enum object_type type; + struct object_id oid; + char *buf; + unsigned long size; + int ret; + + if (get_oid(name, &oid)) + return error("commit '%s' not found.", name); + + buf = read_sha1_file(oid.hash, &type, &size); + if (!buf) + return error("%s: unable to read file.", name); + if (type != OBJ_COMMIT) + return error("%s: cannot verify a non-commit object of type %s.", + name, typename(type)); + + ret = run_gpg_verify(&oid, buf, size, flags); + + free(buf); + return ret; +} + +static int git_verify_commit_config(const char *var, const char *value, void *cb) +{ + int status = git_gpg_config(var, value, cb); + if (status) + return status; + return git_default_config(var, value, cb); +} + +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_verify_commit_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 0000000000..c2a1a5c504 --- /dev/null +++ b/builtin/verify-pack.c @@ -0,0 +1,83 @@ +#include "builtin.h" +#include "cache.h" +#include "config.h" +#include "run-command.h" +#include "parse-options.h" + +#define VERIFY_PACK_VERBOSE 01 +#define VERIFY_PACK_STAT_ONLY 02 + +static int verify_one_pack(const char *path, unsigned int flags) +{ + struct child_process index_pack = CHILD_PROCESS_INIT; + const char *argv[] = {"index-pack", NULL, NULL, NULL }; + struct strbuf arg = STRBUF_INIT; + int verbose = flags & VERIFY_PACK_VERBOSE; + int stat_only = flags & VERIFY_PACK_STAT_ONLY; + int err; + + if (stat_only) + argv[1] = "--verify-stat-only"; + else if (verbose) + argv[1] = "--verify-stat"; + else + argv[1] = "--verify"; + + /* + * 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"); + argv[2] = arg.buf; + + index_pack.argv = argv; + 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>..."), + NULL +}; + +int cmd_verify_pack(int argc, const char **argv, const char *prefix) +{ + int err = 0; + unsigned int flags = 0; + 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_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)) + err = 1; + } + + return err; +} diff --git a/builtin/verify-tag.c b/builtin/verify-tag.c new file mode 100644 index 0000000000..ad7b79fa5c --- /dev/null +++ b/builtin/verify-tag.c @@ -0,0 +1,78 @@ +/* + * Builtin "git verify-tag" + * + * Copyright (c) 2007 Carlos Rica <jasampler@gmail.com> + * + * Based on git-verify-tag.sh + */ +#include "cache.h" +#include "config.h" +#include "builtin.h" +#include "tag.h" +#include "run-command.h" +#include <signal.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>] <tag>..."), + NULL +}; + +static int git_verify_tag_config(const char *var, const char *value, void *cb) +{ + int status = git_gpg_config(var, value, cb); + if (status) + return status; + return git_default_config(var, value, cb); +} + +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_verify_tag_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 (get_oid(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.hash, &format); + } + return had_error; +} diff --git a/builtin/worktree.c b/builtin/worktree.c new file mode 100644 index 0000000000..7b9307aa58 --- /dev/null +++ b/builtin/worktree.c @@ -0,0 +1,577 @@ +#include "cache.h" +#include "config.h" +#include "builtin.h" +#include "dir.h" +#include "parse-options.h" +#include "argv-array.h" +#include "branch.h" +#include "refs.h" +#include "run-command.h" +#include "sigchain.h" +#include "refs.h" +#include "utf8.h" +#include "worktree.h" + +static const char * const worktree_usage[] = { + N_("git worktree add [<options>] <path> [<branch>]"), + N_("git worktree list [<options>]"), + N_("git worktree lock [<options>] <path>"), + N_("git worktree prune [<options>]"), + N_("git worktree unlock <path>"), + NULL +}; + +struct add_opts { + int force; + int detach; + int checkout; + int keep_locked; + const char *new_branch; + int force_new_branch; +}; + +static int show_only; +static int verbose; +static timestamp_t expire; + +static int prune_worktree(const char *id, struct strbuf *reason) +{ + struct stat st; + char *path; + int fd; + size_t len; + ssize_t read_result; + + if (!is_directory(git_path("worktrees/%s", id))) { + strbuf_addf(reason, _("Removing worktrees/%s: not a valid directory"), id); + return 1; + } + if (file_exists(git_path("worktrees/%s/locked", id))) + return 0; + if (stat(git_path("worktrees/%s/gitdir", id), &st)) { + strbuf_addf(reason, _("Removing worktrees/%s: gitdir file does not exist"), id); + return 1; + } + fd = open(git_path("worktrees/%s/gitdir", id), O_RDONLY); + if (fd < 0) { + strbuf_addf(reason, _("Removing worktrees/%s: unable to read gitdir file (%s)"), + id, strerror(errno)); + return 1; + } + len = xsize_t(st.st_size); + path = xmallocz(len); + + read_result = read_in_full(fd, path, len); + if (read_result < 0) { + strbuf_addf(reason, _("Removing worktrees/%s: unable to read gitdir file (%s)"), + id, strerror(errno)); + close(fd); + free(path); + return 1; + } + close(fd); + + if (read_result != len) { + strbuf_addf(reason, + _("Removing worktrees/%s: short read (expected %"PRIuMAX" bytes, read %"PRIuMAX")"), + id, (uintmax_t)len, (uintmax_t)read_result); + free(path); + return 1; + } + while (len && (path[len - 1] == '\n' || path[len - 1] == '\r')) + len--; + if (!len) { + strbuf_addf(reason, _("Removing worktrees/%s: invalid gitdir file"), id); + free(path); + return 1; + } + path[len] = '\0'; + if (!file_exists(path)) { + struct stat st_link; + free(path); + /* + * the repo is moved manually and has not been + * accessed since? + */ + if (!stat(git_path("worktrees/%s/link", id), &st_link) && + st_link.st_nlink > 1) + return 0; + if (st.st_mtime <= expire) { + strbuf_addf(reason, _("Removing worktrees/%s: gitdir file points to non-existent location"), id); + return 1; + } else { + return 0; + } + } + free(path); + return 0; +} + +static void prune_worktrees(void) +{ + struct strbuf reason = STRBUF_INIT; + struct strbuf path = STRBUF_INIT; + DIR *dir = opendir(git_path("worktrees")); + struct dirent *d; + int ret; + if (!dir) + return; + while ((d = readdir(dir)) != NULL) { + if (is_dot_or_dotdot(d->d_name)) + continue; + strbuf_reset(&reason); + if (!prune_worktree(d->d_name, &reason)) + continue; + if (show_only || verbose) + printf("%s\n", reason.buf); + if (show_only) + continue; + git_path_buf(&path, "worktrees/%s", d->d_name); + ret = remove_dir_recursively(&path, 0); + if (ret < 0 && errno == ENOTDIR) + ret = unlink(path.buf); + if (ret) + error_errno(_("failed to remove '%s'"), path.buf); + } + closedir(dir); + if (!show_only) + rmdir(git_path("worktrees")); + strbuf_release(&reason); + strbuf_release(&path); +} + +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, worktree_usage, 0); + if (ac) + usage_with_options(worktree_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; +} + +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; + const char *name; + struct stat st; + struct child_process cp = CHILD_PROCESS_INIT; + struct argv_array child_env = ARGV_ARRAY_INIT; + int counter = 0, len, ret; + struct strbuf symref = STRBUF_INIT; + struct commit *commit = NULL; + + if (file_exists(path) && !is_empty_dir(path)) + die(_("'%s' already exists"), path); + + /* is 'refname' a branch or commit? */ + if (!opts->detach && !strbuf_check_branch_ref(&symref, refname) && + ref_exists(symref.buf)) { /* it's a branch */ + if (!opts->force) + die_if_checked_out(symref.buf, 0); + } else { /* must be a commit */ + commit = lookup_commit_reference_by_name(refname); + if (!commit) + die(_("invalid reference: %s"), refname); + } + + name = worktree_basename(path, &len); + git_path_buf(&sb_repo, "worktrees/%.*s", (int)(path + len - name), 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 (!stat(sb_repo.buf, &st)) { + counter++; + 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); + + if (mkdir(sb_repo.buf, 0777)) + die_errno(_("could not create directory of '%s'"), sb_repo.buf); + 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, "initializing"); + else + write_file(sb.buf, "added with --lock"); + + 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); + write_file(sb.buf, "%s", real_path(sb_git.buf)); + write_file(sb_git.buf, "gitdir: %s/worktrees/%s", + real_path(get_git_common_dir()), 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", sha1_to_hex(null_sha1)); + strbuf_reset(&sb); + strbuf_addf(&sb, "%s/commondir", sb_repo.buf); + write_file(sb.buf, "../.."); + + fprintf_ln(stderr, _("Preparing %s (identifier %s)"), path, name); + + argv_array_pushf(&child_env, "%s=%s", GIT_DIR_ENVIRONMENT, sb_git.buf); + argv_array_pushf(&child_env, "%s=%s", GIT_WORK_TREE_ENVIRONMENT, path); + cp.git_cmd = 1; + + if (commit) + argv_array_pushl(&cp.args, "update-ref", "HEAD", + oid_to_hex(&commit->object.oid), NULL); + else + argv_array_pushl(&cp.args, "symbolic-ref", "HEAD", + symref.buf, NULL); + cp.env = child_env.argv; + ret = run_command(&cp); + if (ret) + goto done; + + if (opts->checkout) { + cp.argv = NULL; + argv_array_clear(&cp.args); + argv_array_pushl(&cp.args, "reset", "--hard", NULL); + cp.env = child_env.argv; + ret = run_command(&cp); + if (ret) + 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); + } + argv_array_clear(&child_env); + strbuf_release(&sb); + strbuf_release(&symref); + strbuf_release(&sb_repo); + strbuf_release(&sb_git); + return ret; +} + +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; + struct option options[] = { + OPT__FORCE(&opts.force, N_("checkout <branch> even if already checked out in other worktree")), + OPT_STRING('b', NULL, &opts.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, "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", &opts.keep_locked, N_("keep the new working tree locked")), + OPT_END() + }; + + memset(&opts, 0, sizeof(opts)); + opts.checkout = 1; + ac = parse_options(ac, av, prefix, options, worktree_usage, 0); + if (!!opts.detach + !!opts.new_branch + !!new_branch_force > 1) + die(_("-b, -B, and --detach are mutually exclusive")); + if (ac < 1 || ac > 2) + usage_with_options(worktree_usage, options); + + path = prefix_filename(prefix, av[0]); + branch = ac < 2 ? "HEAD" : av[1]; + + if (!strcmp(branch, "-")) + branch = "@{-1}"; + + opts.force_new_branch = !!new_branch_force; + if (opts.force_new_branch) { + struct strbuf symref = STRBUF_INIT; + + opts.new_branch = new_branch_force; + + if (!opts.force && + !strbuf_check_branch_ref(&symref, opts.new_branch) && + ref_exists(symref.buf)) + die_if_checked_out(symref.buf, 0); + strbuf_release(&symref); + } + + if (ac < 2 && !opts.new_branch && !opts.detach) { + int n; + const char *s = worktree_basename(path, &n); + opts.new_branch = xstrndup(s, n); + } + + if (opts.new_branch) { + struct child_process cp = CHILD_PROCESS_INIT; + cp.git_cmd = 1; + argv_array_push(&cp.args, "branch"); + if (opts.force_new_branch) + argv_array_push(&cp.args, "--force"); + argv_array_push(&cp.args, opts.new_branch); + argv_array_push(&cp.args, branch); + if (run_command(&cp)) + return -1; + branch = opts.new_branch; + } + + UNLEAK(path); + UNLEAK(opts); + return add_worktree(path, branch, &opts); +} + +static void show_worktree_porcelain(struct worktree *wt) +{ + printf("worktree %s\n", wt->path); + if (wt->is_bare) + printf("bare\n"); + else { + printf("HEAD %s\n", sha1_to_hex(wt->head_sha1)); + if (wt->is_detached) + printf("detached\n"); + else if (wt->head_ref) + printf("branch %s\n", wt->head_ref); + } + printf("\n"); +} + +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); + + 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, + find_unique_abbrev(wt->head_sha1, 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)"); + } + 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(find_unique_abbrev(wt[i]->head_sha1, *abbrev)); + if (sha1_len > *abbrev) + *abbrev = sha1_len; + } +} + +static int list(int ac, const char **av, const char *prefix) +{ + int porcelain = 0; + + struct option options[] = { + OPT_BOOL(0, "porcelain", &porcelain, N_("machine-readable output")), + OPT_END() + }; + + ac = parse_options(ac, av, prefix, options, worktree_usage, 0); + if (ac) + usage_with_options(worktree_usage, options); + else { + struct worktree **worktrees = get_worktrees(GWT_SORT_LINKED); + int path_maxlen = 0, abbrev = DEFAULT_ABBREV, i; + + if (!porcelain) + measure_widths(worktrees, &abbrev, &path_maxlen); + + for (i = 0; worktrees[i]; i++) { + if (porcelain) + show_worktree_porcelain(worktrees[i]); + 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, worktree_usage, 0); + if (ac != 1) + usage_with_options(worktree_usage, options); + + worktrees = get_worktrees(0); + 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 = is_worktree_locked(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, worktree_usage, 0); + if (ac != 1) + usage_with_options(worktree_usage, options); + + worktrees = get_worktrees(0); + 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 (!is_worktree_locked(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; +} + +int cmd_worktree(int ac, const char **av, const char *prefix) +{ + struct option options[] = { + OPT_END() + }; + + git_config(git_default_config, NULL); + + if (ac < 2) + usage_with_options(worktree_usage, options); + if (!prefix) + prefix = ""; + if (!strcmp(av[1], "add")) + return add(ac - 1, av + 1, prefix); + if (!strcmp(av[1], "prune")) + return prune(ac - 1, av + 1, prefix); + if (!strcmp(av[1], "list")) + return list(ac - 1, av + 1, prefix); + if (!strcmp(av[1], "lock")) + return lock_worktree(ac - 1, av + 1, prefix); + if (!strcmp(av[1], "unlock")) + return unlock_worktree(ac - 1, av + 1, prefix); + usage_with_options(worktree_usage, options); +} diff --git a/builtin/write-tree.c b/builtin/write-tree.c new file mode 100644 index 0000000000..bd0a78aa3c --- /dev/null +++ b/builtin/write-tree.c @@ -0,0 +1,57 @@ +/* + * GIT - The information manager from hell + * + * Copyright (C) Linus Torvalds, 2005 + */ +#include "builtin.h" +#include "cache.h" +#include "config.h" +#include "tree.h" +#include "cache-tree.h" +#include "parse-options.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 *unused_prefix) +{ + int flags = 0, ret; + const char *prefix = NULL; + unsigned char sha1[20]; + 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), + { OPTION_STRING, 0, "prefix", &prefix, N_("<prefix>/"), + N_("write tree object for a subdirectory <prefix>") , + PARSE_OPT_LITERAL_ARGHELP }, + { 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, unused_prefix, write_tree_options, + write_tree_usage, 0); + + ret = write_cache_as_tree(sha1, flags, prefix); + switch (ret) { + case 0: + printf("%s\n", sha1_to_hex(sha1)); + 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, prefix); + break; + } + return ret; +} |