From: Junio C Hamano Date: Wed, 24 Mar 2010 23:26:43 +0000 (-0700) Subject: Merge branch 'tr/notes-display' X-Git-Tag: v1.7.1-rc0~43 X-Git-Url: https://git.lorimer.id.au/gitweb.git/diff_plain/a86ed83cce?hp=-c Merge branch 'tr/notes-display' * tr/notes-display: git-notes(1): add a section about the meaning of history notes: track whether notes_trees were changed at all notes: add shorthand --ref to override GIT_NOTES_REF commit --amend: copy notes to the new commit rebase: support automatic notes copying notes: implement helpers needed for note copying during rewrite notes: implement 'git notes copy --stdin' rebase -i: invoke post-rewrite hook rebase: invoke post-rewrite hook commit --amend: invoke post-rewrite hook Documentation: document post-rewrite hook Support showing notes from more than one notes tree test-lib: unset GIT_NOTES_REF to stop it from influencing tests Conflicts: git-am.sh refs.c --- a86ed83cce0fc0b9a9fe4e17ac1bf4f719028d20 diff --combined Documentation/config.txt index 1dbded0fdc,2e02f1b075..06b2f827b4 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@@ -138,11 -138,6 +138,11 @@@ advice.*: Advice on how to set your identity configuration when your information is guessed from the system username and domain name. Default: true. + + detachedHead:: + Advice shown when you used linkgit::git-checkout[1] to + move to the detach HEAD state, to instruct how to create + a local branch after the fact. Default: true. -- core.fileMode:: @@@ -422,20 -417,6 +422,20 @@@ You probably do not need to adjust thi + Common unit suffixes of 'k', 'm', or 'g' are supported. +core.bigFileThreshold:: + Files larger than this size are stored deflated, without + attempting delta compression. Storing large files without + delta compression avoids excessive memory usage, at the + slight expense of increased disk usage. ++ +Default is 512 MiB on all platforms. This should be reasonable +for most projects as source code and other text files can still +be delta compressed, but larger binary media files won't be. ++ +Common unit suffixes of 'k', 'm', or 'g' are supported. ++ +Currently only linkgit:git-fast-import[1] honors this setting. + core.excludesfile:: In addition to '.gitignore' (per-directory) and '.git/info/exclude', git looks into this file for patterns @@@ -519,10 -500,12 +519,12 @@@ check that makes sure that existing obj core.notesRef:: When showing commit messages, also show notes which are stored in the given ref. This ref is expected to contain files named - after the full SHA-1 of the commit they annotate. + after the full SHA-1 of the commit they annotate. The ref + must be fully qualified. + If such a file exists in the given ref, the referenced blob is read, and - appended to the commit message, separated by a "Notes:" line. If the + appended to the commit message, separated by a "Notes ():" + line (shortened to "Notes:" in the case of "refs/notes/commits"). If the given ref itself does not exist, it is not an error, but means that no notes should be printed. + @@@ -555,13 -538,6 +557,13 @@@ it will be treated as a shell command executed from the top-level directory of a repository, which may not necessarily be the current directory. +am.keepcr:: + If true, git-am will call git-mailsplit for patches in mbox format + with parameter '--keep-cr'. In this case git-mailsplit will + not remove `\r` from lines ending with `\r\n`. Can be overrriden + by giving '--no-keep-cr' from the command line. + See linkgit:git-am[1], linkgit:git-mailsplit[1]. + apply.ignorewhitespace:: When set to 'change', tells 'git apply' to ignore changes in whitespace, in the same way as the '--ignore-space-change' @@@ -690,29 -666,11 +692,29 @@@ color.grep: `never`), never. When set to `true` or `auto`, use color only when the output is written to the terminal. Defaults to `false`. -color.grep.match:: - Use customized color for matches. The value of this variable - may be specified as in color.branch.. It is passed using - the environment variables 'GREP_COLOR' and 'GREP_COLORS' when - calling an external 'grep'. +color.grep.:: + Use customized color for grep colorization. `` specifies which + part of the line to use the specified color, and is one of ++ +-- +`context`;; + non-matching text in context lines (when using `-A`, `-B`, or `-C`) +`filename`;; + filename prefix (when not using `-h`) +`function`;; + function name lines (when using `-p`) +`linenumber`;; + line number prefix (when using `-n`) +`match`;; + matching text +`selected`;; + non-matching text in selected lines +`separator`;; + separators between fields on a line (`:`, `-`, and `=`) + and between hunks (`--`) +-- ++ +The values of these variables may be specified as in color.branch.. color.interactive:: When set to `always`, always use colors for interactive prompts @@@ -760,7 -718,7 +762,7 @@@ color.ui: terminal. When more specific variables of color.* are set, they always take precedence over this setting. Defaults to false. -commit.status +commit.status:: A boolean to enable/disable inclusion of status information in the commit message template when using an editor to prepare the commit message. Defaults to true. @@@ -1230,10 -1188,6 +1232,10 @@@ imap: The configuration variables in the 'imap' section are described in linkgit:git-imap-send[1]. +init.templatedir:: + Specify the directory from which templates will be copied. + (See the "TEMPLATE DIRECTORY" section of linkgit:git-init[1].) + instaweb.browser:: Specify the program that will be used to browse your working repository in gitweb. See linkgit:git-instaweb[1]. @@@ -1334,6 -1288,53 +1336,53 @@@ mergetool.keepTemporaries: mergetool.prompt:: Prompt before each invocation of the merge resolution program. + notes.displayRef:: + The (fully qualified) refname from which to show notes when + showing commit messages. The value of this variable can be set + to a glob, in which case notes from all matching refs will be + shown. You may also specify this configuration variable + several times. A warning will be issued for refs that do not + exist, but a glob that does not match any refs is silently + ignored. + + + This setting can be overridden with the `GIT_NOTES_DISPLAY_REF` + environment variable, which must be a colon separated list of refs or + globs. + + + The effective value of "core.notesRef" (possibly overridden by + GIT_NOTES_REF) is also implicitly added to the list of refs to be + displayed. + + notes.rewrite.:: + When rewriting commits with (currently `amend` or + `rebase`) and this variable is set to `true`, git + automatically copies your notes from the original to the + rewritten commit. Defaults to `true`, but see + "notes.rewriteRef" below. + + + This setting can be overridden with the `GIT_NOTES_REWRITE_REF` + environment variable, which must be a colon separated list of refs or + globs. + + notes.rewriteMode:: + When copying notes during a rewrite (see the + "notes.rewrite." option), determines what to do if + the target commit already has a note. Must be one of + `overwrite`, `concatenate`, or `ignore`. Defaults to + `concatenate`. + + + This setting can be overridden with the `GIT_NOTES_REWRITE_MODE` + environment variable. + + notes.rewriteRef:: + When copying notes during a rewrite, specifies the (fully + qualified) ref whose notes should be copied. The ref may be a + glob, in which case notes in all matching refs will be copied. + You may also specify this configuration several times. + + + Does not have a default value; you must configure this variable to + enable note rewriting. + pack.window:: The size of the window used by linkgit:git-pack-objects[1] when no window size is given on the command line. Defaults to 10. @@@ -1402,13 -1403,10 +1451,13 @@@ you can use linkgit:git-index-pack[1] o the `{asterisk}.idx` file. pack.packSizeLimit:: - The default maximum size of a pack. This setting only affects - packing to a file, i.e. the git:// protocol is unaffected. It - can be overridden by the `\--max-pack-size` option of - linkgit:git-repack[1]. + The maximum size of a pack. This setting only affects + packing to a file when repacking, i.e. the git:// protocol + is unaffected. It can be overridden by the `\--max-pack-size` + option of linkgit:git-repack[1]. The minimum size allowed is + limited to 1 MiB. The default is unlimited. + Common unit suffixes of 'k', 'm', or 'g' are + supported. pager.:: Allows turning on or off pagination of the output of a @@@ -1473,7 -1471,7 +1522,7 @@@ receive.denyCurrentBranch: out of sync with the index and working tree. If set to "warn", print a warning of such a push to stderr, but allow the push to proceed. If set to false or "ignore", allow such pushes with no - message. Defaults to "warn". + message. Defaults to "refuse". receive.denyNonFastForwards:: If set to true, git-receive-pack will deny a ref update which is diff --combined Documentation/git-notes.txt index bef2f3942e,e2701cff1a..4e5113b837 --- a/Documentation/git-notes.txt +++ b/Documentation/git-notes.txt @@@ -10,7 -10,7 +10,7 @@@ SYNOPSI [verse] 'git notes' [list []] 'git notes' add [-f] [-F | -m | (-c | -C) ] [] - 'git notes' copy [-f] + 'git notes' copy [-f] ( --stdin | ) 'git notes' append [-F | -m | (-c | -C) ] [] 'git notes' edit [] 'git notes' show [] @@@ -27,12 -27,17 +27,17 @@@ A typical use of notes is to extend a c to change the commit itself. Such commit notes can be shown by `git log` along with the original commit message. To discern these notes from the message stored in the commit object, the notes are indented like the - message, after an unindented line saying "Notes:". + message, after an unindented line saying "Notes ():" (or + "Notes:" for the default setting). - To disable notes, you have to set the config variable core.notesRef to - the empty string. Alternatively, you can set it to a different ref, - something like "refs/notes/bugzilla". This setting can be overridden - by the environment variable "GIT_NOTES_REF". + This command always manipulates the notes specified in "core.notesRef" + (see linkgit:git-config[1]), which can be overridden by GIT_NOTES_REF. + To change which notes are shown by 'git-log', see the + "notes.displayRef" configuration. + + See the description of "notes.rewrite." in + linkgit:git-config[1] for a way of carrying your notes across commands + that rewrite commits. SUBCOMMANDS @@@ -46,15 -51,25 +51,25 @@@ list: add:: Add notes for a given object (defaults to HEAD). Abort if the - object already has notes, abort. (use `-f` to overwrite an + object already has notes (use `-f` to overwrite an existing note). copy:: Copy the notes for the first object onto the second object. Abort if the second object already has notes, or if the first - objects has none. (use -f to overwrite existing notes to the + object has none (use -f to overwrite existing notes to the second object). This subcommand is equivalent to: `git notes add [-f] -C $(git notes list ) ` + + + In `\--stdin` mode, take lines in the format + + + ---------- + SP [ SP ] LF + ---------- + + + on standard input, and copy the notes from each to its + corresponding . (The optional `` is ignored so that + the command can read the input given to the `post-rewrite` hook.) append:: Append to the notes of an existing object (defaults to HEAD). @@@ -101,6 -116,25 +116,25 @@@ OPTION Like '-C', but with '-c' the editor is invoked, so that the user can further edit the note message. + --ref :: + Manipulate the notes tree in . This overrides both + GIT_NOTES_REF and the "core.notesRef" configuration. The ref + is taken to be in `refs/notes/` if it is not qualified. + + + NOTES + ----- + + Every notes change creates a new commit at the specified notes ref. + You can therefore inspect the history of the notes by invoking, e.g., + `git log -p notes/commits`. + + Currently the commit message only records which operation triggered + the update, and the commit authorship is determined according to the + usual rules (see linkgit:git-commit[1]). These details may change in + the future. + + Author ------ Written by Johannes Schindelin and diff --combined builtin/commit.c index f4c73442cf,0000000000..8dd104ee0b mode 100644,000000..100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@@ -1,1310 -1,0 +1,1355 @@@ +/* + * Builtin "git commit" + * + * Copyright (c) 2007 Kristian Høgsberg + * Based on git-commit.sh by Junio C Hamano and Linus Torvalds + */ + +#include "cache.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" + +static const char * const builtin_commit_usage[] = { + "git commit [options] [--] ...", + NULL +}; + +static const char * const builtin_status_usage[] = { + "git status [options] [--] ...", + NULL +}; + +static const char implicit_ident_advice[] = +"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" +"If the identity used for this commit is wrong, you can fix it with:\n" +"\n" +" git commit --amend --author='Your Name '\n"; + +static unsigned char head_sha1[20]; + +static char *use_message_buffer; +static const char commit_editmsg[] = "COMMIT_EDITMSG"; +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; +static char *edit_message, *use_message; +static char *author_name, *author_email, *author_date; +static int all, edit_flag, also, interactive, only, amend, signoff; +static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship; ++static int no_post_rewrite; +static char *untracked_files_arg, *force_date; +/* + * 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_ALL, +} cleanup_mode; +static char *cleanup_arg; + +static int use_editor = 1, initial_commit, in_merge, include_status = 1; +static const char *only_include_assumed; +static struct strbuf message; + +static int null_termination; +static enum { + STATUS_FORMAT_LONG, + STATUS_FORMAT_SHORT, + STATUS_FORMAT_PORCELAIN, +} status_format = STATUS_FORMAT_LONG; + +static int opt_parse_m(const struct option *opt, const char *arg, int unset) +{ + struct strbuf *buf = opt->value; + if (unset) + strbuf_setlen(buf, 0); + else { + strbuf_addstr(buf, arg); + strbuf_addstr(buf, "\n\n"); + } + return 0; +} + +static struct option builtin_commit_options[] = { + OPT__QUIET(&quiet), + OPT__VERBOSE(&verbose), + + OPT_GROUP("Commit message options"), + OPT_FILENAME('F', "file", &logfile, "read log from file"), + OPT_STRING(0, "author", &force_author, "AUTHOR", "override author for commit"), + OPT_STRING(0, "date", &force_date, "DATE", "override date for commit"), + OPT_CALLBACK('m', "message", &message, "MESSAGE", "specify commit message", opt_parse_m), + OPT_STRING('c', "reedit-message", &edit_message, "COMMIT", "reuse and edit message from specified commit"), + OPT_STRING('C', "reuse-message", &use_message, "COMMIT", "reuse message from specified commit"), + OPT_BOOLEAN(0, "reset-author", &renew_authorship, "the commit is authored by me now (used with -C-c/--amend)"), + OPT_BOOLEAN('s', "signoff", &signoff, "add Signed-off-by:"), + OPT_FILENAME('t', "template", &template_file, "use specified template file"), + OPT_BOOLEAN('e', "edit", &edit_flag, "force edit of commit"), + OPT_STRING(0, "cleanup", &cleanup_arg, "default", "how to strip spaces and #comments from message"), + OPT_BOOLEAN(0, "status", &include_status, "include status in commit message template"), + /* end commit message options */ + + OPT_GROUP("Commit contents options"), + OPT_BOOLEAN('a', "all", &all, "commit all changed files"), + OPT_BOOLEAN('i', "include", &also, "add specified files to index for commit"), + OPT_BOOLEAN(0, "interactive", &interactive, "interactively add files"), + OPT_BOOLEAN('o', "only", &only, "commit only specified files"), + OPT_BOOLEAN('n', "no-verify", &no_verify, "bypass pre-commit hook"), + OPT_BOOLEAN(0, "dry-run", &dry_run, "show what would be committed"), + OPT_SET_INT(0, "short", &status_format, "show status concisely", + STATUS_FORMAT_SHORT), + OPT_SET_INT(0, "porcelain", &status_format, + "show porcelain output format", STATUS_FORMAT_PORCELAIN), + OPT_BOOLEAN('z', "null", &null_termination, + "terminate entries with NUL"), + OPT_BOOLEAN(0, "amend", &amend, "amend previous commit"), ++ OPT_BOOLEAN(0, "no-post-rewrite", &no_post_rewrite, "bypass post-rewrite hook"), + { OPTION_STRING, 'u', "untracked-files", &untracked_files_arg, "mode", "show untracked files, optional modes: all, normal, no. (Default: all)", PARSE_OPT_OPTARG, NULL, (intptr_t)"all" }, + OPT_BOOLEAN(0, "allow-empty", &allow_empty, "ok to record an empty change"), + /* end commit contents options */ + + OPT_END() +}; + +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 char **pattern) +{ + int i; + char *m; + + for (i = 0; pattern[i]; i++) + ; + m = xcalloc(1, i); + + if (with_tree) + overlay_tree_on_cache(with_tree, prefix); + + for (i = 0; i < active_nr; i++) { + struct cache_entry *ce = active_cache[i]; + struct string_list_item *item; + + if (ce->ce_flags & CE_UPDATE) + continue; + if (!match_pathspec(pattern, ce->name, ce_namelen(ce), 0, m)) + continue; + item = string_list_insert(ce->name, list); + if (ce_skip_worktree(ce)) + item->util = item; /* better a valid pointer than a fake one */ + } + + return report_path_error(m, pattern, prefix ? strlen(prefix) : 0); +} + +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(void) +{ + struct tree *tree; + struct unpack_trees_options opts; + struct tree_desc t; + + if (initial_commit) { + 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(head_sha1); + 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 char *prepare_index(int argc, const char **argv, const char *prefix, int is_status) +{ + int fd; + struct string_list partial; + const char **pathspec = NULL; + int refresh_flags = REFRESH_QUIET; + + if (is_status) + refresh_flags |= REFRESH_UNMERGED; + if (interactive) { + if (interactive_add(argc, argv, prefix) != 0) + die("interactive add failed"); + if (read_cache_preload(NULL) < 0) + die("index file corrupt"); + commit_style = COMMIT_AS_IS; + return get_index_file(); + } + + if (*argv) + pathspec = get_pathspec(prefix, argv); + + if (read_cache_preload(pathspec) < 0) + die("index file corrupt"); + + /* + * 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 && *pathspec)) { + int fd = hold_locked_index(&index_lock, 1); + add_files_to_cache(also ? prefix : NULL, pathspec, 0); + refresh_cache_or_die(refresh_flags); + if (write_cache(fd, active_cache, active_nr) || + close_lock_file(&index_lock)) + die("unable to write new_index file"); + commit_style = COMMIT_NORMAL; + return index_lock.filename; + } + + /* + * As-is commit. + * + * (1) return the name of the real index file. + * + * The caller should run hooks on the real index, and run + * hooks on the real index, and create commit from the_index. + * We still need to refresh the index here. + */ + if (!pathspec || !*pathspec) { + fd = hold_locked_index(&index_lock, 1); + refresh_cache_or_die(refresh_flags); + if (write_cache(fd, active_cache, active_nr) || + commit_locked_index(&index_lock)) + die("unable to write new_index file"); + commit_style = COMMIT_AS_IS; + return get_index_file(); + } + + /* + * 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 (in_merge) + die("cannot do a partial commit during a merge."); + + memset(&partial, 0, sizeof(partial)); + partial.strdup_strings = 1; + if (list_paths(&partial, initial_commit ? NULL : "HEAD", prefix, pathspec)) + exit(1); + + discard_cache(); + if (read_cache() < 0) + die("cannot read the index"); + + fd = hold_locked_index(&index_lock, 1); + add_remove_files(&partial); + refresh_cache(REFRESH_QUIET); + if (write_cache(fd, active_cache, active_nr) || + close_lock_file(&index_lock)) + die("unable to write new_index file"); + + fd = hold_lock_file_for_update(&false_lock, + git_path("next-index-%"PRIuMAX, + (uintmax_t) getpid()), + LOCK_DIE_ON_ERROR); + + create_base_index(); + add_remove_files(&partial); + refresh_cache(REFRESH_QUIET); + + if (write_cache(fd, active_cache, active_nr) || + close_lock_file(&false_lock)) + die("unable to write temporary index file"); + + discard_cache(); + read_cache_from(false_lock.filename); + + return false_lock.filename; +} + +static int run_status(FILE *fp, const char *index_file, const char *prefix, int nowarn, + struct wt_status *s) +{ + unsigned char sha1[20]; + + 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_sha1(s->reference, sha1) ? 1 : 0; + + wt_status_collect(s); + + switch (status_format) { + case STATUS_FORMAT_SHORT: + wt_shortstatus_print(s, null_termination); + break; + case STATUS_FORMAT_PORCELAIN: + wt_porcelain_print(s, null_termination); + break; + case STATUS_FORMAT_LONG: + wt_status_print(s); + break; + } + + return s->commitable; +} + +static int is_a_merge(const unsigned char *sha1) +{ + struct commit *commit = lookup_commit(sha1); + if (!commit || parse_commit(commit)) + die("could not parse HEAD commit"); + return !!(commit->parents && commit->parents->next); +} + +static const char sign_off_header[] = "Signed-off-by: "; + +static void determine_author_info(void) +{ + char *name, *email, *date; + + name = getenv("GIT_AUTHOR_NAME"); + email = getenv("GIT_AUTHOR_EMAIL"); + date = getenv("GIT_AUTHOR_DATE"); + + if (use_message && !renew_authorship) { + const char *a, *lb, *rb, *eol; + + a = strstr(use_message_buffer, "\nauthor "); + if (!a) + die("invalid commit: %s", use_message); + + lb = strstr(a + 8, " <"); + rb = strstr(a + 8, "> "); + eol = strchr(a + 8, '\n'); + if (!lb || !rb || !eol) + die("invalid commit: %s", use_message); + + name = xstrndup(a + 8, lb - (a + 8)); + email = xstrndup(lb + 2, rb - (lb + 2)); + date = xstrndup(rb + 2, eol - (rb + 2)); + } + + if (force_author) { + const char *lb = strstr(force_author, " <"); + const char *rb = strchr(force_author, '>'); + + if (!lb || !rb) + die("malformed --author parameter"); + name = xstrndup(force_author, lb - force_author); + email = xstrndup(lb + 2, rb - (lb + 2)); + } + + if (force_date) + date = force_date; + + author_name = name; + author_email = email; + author_date = date; +} + +static int ends_rfc2822_footer(struct strbuf *sb) +{ + int ch; + int hit = 0; + int i, j, k; + int len = sb->len; + int first = 1; + const char *buf = sb->buf; + + for (i = len - 1; i > 0; i--) { + if (hit && buf[i] == '\n') + break; + hit = (buf[i] == '\n'); + } + + while (i < len - 1 && buf[i] == '\n') + i++; + + for (; i < len; i = k) { + for (k = i; k < len && buf[k] != '\n'; k++) + ; /* do nothing */ + k++; + + if ((buf[k] == ' ' || buf[k] == '\t') && !first) + continue; + + first = 0; + + for (j = 0; i + j < len; j++) { + ch = buf[i + j]; + if (ch == ':') + break; + if (isalnum(ch) || + (ch == '-')) + continue; + return 0; + } + } + return 1; +} + +static int prepare_to_commit(const char *index_file, const char *prefix, + struct wt_status *s) +{ + struct stat statbuf; + int commitable, saved_color_setting; + struct strbuf sb = STRBUF_INIT; + char *buffer; + FILE *fp; + const char *hook_arg1 = NULL; + const char *hook_arg2 = NULL; + int ident_shown = 0; + + if (!no_verify && run_hook(index_file, "pre-commit", NULL)) + return 0; + + if (message.len) { + 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) { + buffer = strstr(use_message_buffer, "\n\n"); + if (!buffer || buffer[2] == '\0') + die("commit has empty message"); + strbuf_add(&sb, buffer + 2, strlen(buffer + 2)); + hook_arg1 = "commit"; + hook_arg2 = use_message; + } else if (!stat(git_path("MERGE_MSG"), &statbuf)) { + if (strbuf_read_file(&sb, git_path("MERGE_MSG"), 0) < 0) + die_errno("could not read MERGE_MSG"); + hook_arg1 = "merge"; + } 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 && !stat(template_file, &statbuf)) { + if (strbuf_read_file(&sb, template_file, 0) < 0) + die_errno("could not read '%s'", template_file); + hook_arg1 = "template"; + } + + /* + * This final case does not modify the template message, + * it just sets the argument to the prepare-commit-msg hook. + */ + else if (in_merge) + hook_arg1 = "merge"; + + fp = fopen(git_path(commit_editmsg), "w"); + if (fp == NULL) + die_errno("could not open '%s'", git_path(commit_editmsg)); + + if (cleanup_mode != CLEANUP_NONE) + stripspace(&sb, 0); + + if (signoff) { + struct strbuf sob = STRBUF_INIT; + int i; + + strbuf_addstr(&sob, sign_off_header); + strbuf_addstr(&sob, fmt_name(getenv("GIT_COMMITTER_NAME"), + getenv("GIT_COMMITTER_EMAIL"))); + strbuf_addch(&sob, '\n'); + for (i = sb.len - 1; i > 0 && sb.buf[i - 1] != '\n'; i--) + ; /* do nothing */ + if (prefixcmp(sb.buf + i, sob.buf)) { + if (!i || !ends_rfc2822_footer(&sb)) + strbuf_addch(&sb, '\n'); + strbuf_addbuf(&sb, &sob); + } + strbuf_release(&sob); + } + + if (fwrite(sb.buf, 1, sb.len, fp) < sb.len) + die_errno("could not write commit template"); + + strbuf_release(&sb); + + determine_author_info(); + + /* This checks if committer ident is explicitly given */ + git_committer_info(0); + if (use_editor && include_status) { + char *author_ident; + const char *committer_ident; + + if (in_merge) + fprintf(fp, + "#\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", + git_path("MERGE_HEAD")); + + fprintf(fp, + "\n" + "# Please enter the commit message for your changes."); + if (cleanup_mode == CLEANUP_ALL) + fprintf(fp, + " Lines starting\n" + "# with '#' will be ignored, and an empty" + " message aborts the commit.\n"); + else /* CLEANUP_SPACE, that is. */ + fprintf(fp, + " Lines starting\n" + "# with '#' will be kept; you may remove them" + " yourself if you want to.\n" + "# An empty message aborts the commit.\n"); + if (only_include_assumed) + fprintf(fp, "# %s\n", only_include_assumed); + + author_ident = xstrdup(fmt_name(author_name, author_email)); + committer_ident = fmt_name(getenv("GIT_COMMITTER_NAME"), + getenv("GIT_COMMITTER_EMAIL")); + if (strcmp(author_ident, committer_ident)) + fprintf(fp, + "%s" + "# Author: %s\n", + ident_shown++ ? "" : "#\n", + author_ident); + free(author_ident); + + if (!user_ident_sufficiently_given()) + fprintf(fp, + "%s" + "# Committer: %s\n", + ident_shown++ ? "" : "#\n", + committer_ident); + + if (ident_shown) + fprintf(fp, "#\n"); + + saved_color_setting = s->use_color; + s->use_color = 0; + commitable = run_status(fp, index_file, prefix, 1, s); + s->use_color = saved_color_setting; + } else { + unsigned char sha1[20]; + const char *parent = "HEAD"; + + if (!active_nr && read_cache() < 0) + die("Cannot read index"); + + if (amend) + parent = "HEAD^1"; + + if (get_sha1(parent, sha1)) + commitable = !!active_nr; + else + commitable = index_differs_from(parent, 0); + } + + fclose(fp); + + if (!commitable && !in_merge && !allow_empty && + !(amend && is_a_merge(head_sha1))) { + run_status(stdout, index_file, prefix, 0, s); + return 0; + } + + /* + * 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 (!active_cache_tree) + active_cache_tree = cache_tree(); + if (cache_tree_update(active_cache_tree, + active_cache, active_nr, 0, 0) < 0) { + error("Error building trees"); + return 0; + } + + if (run_hook(index_file, "prepare-commit-msg", + git_path(commit_editmsg), hook_arg1, hook_arg2, NULL)) + return 0; + + if (use_editor) { + char index[PATH_MAX]; + const char *env[2] = { index, NULL }; + snprintf(index, sizeof(index), "GIT_INDEX_FILE=%s", index_file); + if (launch_editor(git_path(commit_editmsg), NULL, env)) { + fprintf(stderr, + "Please supply the message using either -m or -F option.\n"); + exit(1); + } + } + + if (!no_verify && + run_hook(index_file, "commit-msg", git_path(commit_editmsg), NULL)) { + 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) +{ + struct strbuf tmpl = STRBUF_INIT; + const char *nl; + int eol, i, start = 0; + + if (cleanup_mode == CLEANUP_NONE && sb->len) + return 0; + + /* See if the template is just a prefix of the message. */ + if (template_file && strbuf_read_file(&tmpl, template_file, 0) > 0) { + stripspace(&tmpl, cleanup_mode == CLEANUP_ALL); + if (start + tmpl.len <= sb->len && + memcmp(tmpl.buf, sb->buf + start, tmpl.len) == 0) + start += tmpl.len; + } + strbuf_release(&tmpl); + + /* Check if the rest is just whitespace and Signed-of-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 && + !prefixcmp(sb->buf + i, sign_off_header)) { + i = eol; + continue; + } + while (i < eol) + if (!isspace(sb->buf[i++])) + return 0; + } + + return 1; +} + +static const char *find_author_by_nickname(const char *name) +{ + struct rev_info revs; + struct commit *commit; + struct strbuf buf = STRBUF_INIT; + const char *av[20]; + int ac = 0; + + 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); + prepare_revision_walk(&revs); + commit = get_revision(&revs); + if (commit) { + struct pretty_print_context ctx = {0}; + ctx.date_mode = DATE_NORMAL; + strbuf_release(&buf); + format_commit_message(commit, "%an <%ae>", &buf, &ctx); + return strbuf_detach(&buf, NULL); + } + die("No existing author found with '%s'", 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 int parse_and_validate_options(int argc, const char *argv[], + const char * const usage[], + const char *prefix, + struct wt_status *s) +{ + int f = 0; + + argc = parse_options(argc, argv, prefix, builtin_commit_options, usage, + 0); + + 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 || message.len || use_message) + use_editor = 0; + if (edit_flag) + use_editor = 1; + if (!use_editor) + setenv("GIT_EDITOR", ":", 1); + + if (get_sha1("HEAD", head_sha1)) + initial_commit = 1; + + /* Sanity check options */ + if (amend && initial_commit) + die("You have nothing to amend."); + if (amend && in_merge) + die("You are in the middle of a merge -- cannot amend."); + + if (use_message) + f++; + if (edit_message) + f++; + if (logfile) + f++; + if (f > 1) + die("Only one of -c/-C/-F can be used."); + if (message.len && f > 0) + die("Option -m cannot be combined with -c/-C/-F."); + if (edit_message) + use_message = edit_message; + if (amend && !use_message) + use_message = "HEAD"; + if (!use_message && renew_authorship) + die("--reset-author can be used only with -C, -c or --amend."); + if (use_message) { + unsigned char sha1[20]; + static char utf8[] = "UTF-8"; + const char *out_enc; + char *enc, *end; + struct commit *commit; + + if (get_sha1(use_message, sha1)) + die("could not lookup commit %s", use_message); + commit = lookup_commit_reference(sha1); + if (!commit || parse_commit(commit)) + die("could not parse commit %s", use_message); + + enc = strstr(commit->buffer, "\nencoding"); + if (enc) { + end = strchr(enc + 10, '\n'); + enc = xstrndup(enc + 10, end - (enc + 10)); + } else { + enc = utf8; + } + out_enc = git_commit_encoding ? git_commit_encoding : utf8; + + if (strcmp(out_enc, enc)) + use_message_buffer = + reencode_string(commit->buffer, out_enc, enc); + + /* + * If we failed to reencode the buffer, just copy it + * byte for byte so the user can try to fix it up. + * This also handles the case where input and output + * encodings are identical. + */ + if (use_message_buffer == NULL) + use_message_buffer = xstrdup(commit->buffer); + if (enc != utf8) + free(enc); + } + + if (!!also + !!only + !!all + !!interactive > 1) + die("Only one of --include/--only/--all/--interactive can be used."); + if (argc == 0 && (also || (only && !amend))) + die("No paths with --include/--only does not make sense."); + if (argc == 0 && only && amend) + only_include_assumed = "Clever... amending the last one with dirty index."; + if (argc > 0 && !also && !only) + only_include_assumed = "Explicit paths specified without -i nor -o; assuming --only paths..."; + 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 + die("Invalid cleanup mode %s", cleanup_arg); + + handle_untracked_files_arg(s); + + if (all && argc > 0) + die("Paths with -a does not make sense."); + else if (interactive && argc > 0) + die("Paths with --interactive does not make sense."); + + if (null_termination && status_format == STATUS_FORMAT_LONG) + status_format = STATUS_FORMAT_PORCELAIN; + if (status_format != STATUS_FORMAT_LONG) + dry_run = 1; + + return argc; +} + +static int dry_run_commit(int argc, const char **argv, const char *prefix, + struct wt_status *s) +{ + int commitable; + const char *index_file; + + index_file = prepare_index(argc, argv, prefix, 1); + commitable = run_status(stdout, index_file, prefix, 0, s); + rollback_index_files(); + + return commitable ? 0 : 1; +} + +static int parse_status_slot(const char *var, int offset) +{ + if (!strcasecmp(var+offset, "header")) + return WT_STATUS_HEADER; + if (!strcasecmp(var+offset, "updated") + || !strcasecmp(var+offset, "added")) + return WT_STATUS_UPDATED; + if (!strcasecmp(var+offset, "changed")) + return WT_STATUS_CHANGED; + if (!strcasecmp(var+offset, "untracked")) + return WT_STATUS_UNTRACKED; + if (!strcasecmp(var+offset, "nobranch")) + return WT_STATUS_NOBRANCH; + if (!strcasecmp(var+offset, "unmerged")) + return WT_STATUS_UNMERGED; + return -1; +} + +static int git_status_config(const char *k, const char *v, void *cb) +{ + struct wt_status *s = cb; + + 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.color") || !strcmp(k, "color.status")) { + s->use_color = git_config_colorbool(k, v, -1); + return 0; + } + if (!prefixcmp(k, "status.color.") || !prefixcmp(k, "color.status.")) { + int slot = parse_status_slot(k, 13); + if (slot < 0) + return 0; + if (!v) + return config_error_nonbool(k); + color_parse(v, k, s->color_palette[slot]); + return 0; + } + 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) +{ + struct wt_status s; + unsigned char sha1[20]; + static struct option builtin_status_options[] = { + OPT__VERBOSE(&verbose), + OPT_SET_INT('s', "short", &status_format, + "show status concisely", STATUS_FORMAT_SHORT), + OPT_SET_INT(0, "porcelain", &status_format, + "show porcelain output format", + STATUS_FORMAT_PORCELAIN), + OPT_BOOLEAN('z', "null", &null_termination, + "terminate entries with NUL"), + { OPTION_STRING, 'u', "untracked-files", &untracked_files_arg, + "mode", + "show untracked files, optional modes: all, normal, no. (Default: all)", + PARSE_OPT_OPTARG, NULL, (intptr_t)"all" }, + OPT_END(), + }; + + if (null_termination && status_format == STATUS_FORMAT_LONG) + status_format = STATUS_FORMAT_PORCELAIN; + + wt_status_prepare(&s); + git_config(git_status_config, &s); + in_merge = file_exists(git_path("MERGE_HEAD")); + argc = parse_options(argc, argv, prefix, + builtin_status_options, + builtin_status_usage, 0); + handle_untracked_files_arg(&s); + + if (*argv) + s.pathspec = get_pathspec(prefix, argv); + + read_cache_preload(s.pathspec); + refresh_index(&the_index, REFRESH_QUIET|REFRESH_UNMERGED, s.pathspec, NULL, NULL); + s.is_initial = get_sha1(s.reference, sha1) ? 1 : 0; + s.in_merge = in_merge; + wt_status_collect(&s); + + if (s.relative_paths) + s.prefix = prefix; + if (s.use_color == -1) + s.use_color = git_use_color_default; + if (diff_use_color_default == -1) + diff_use_color_default = git_use_color_default; + + switch (status_format) { + case STATUS_FORMAT_SHORT: + wt_shortstatus_print(&s, null_termination); + break; + case STATUS_FORMAT_PORCELAIN: + wt_porcelain_print(&s, null_termination); + break; + case STATUS_FORMAT_LONG: + s.verbose = verbose; + wt_status_print(&s); + break; + } + return 0; +} + +static void print_summary(const char *prefix, const unsigned char *sha1) +{ + struct rev_info rev; + struct commit *commit; + struct strbuf format = STRBUF_INIT; + unsigned char junk_sha1[20]; + const char *head = resolve_ref("HEAD", junk_sha1, 0, NULL); + struct pretty_print_context pctx = {0}; + struct strbuf author_ident = STRBUF_INIT; + struct strbuf committer_ident = STRBUF_INIT; + + commit = lookup_commit(sha1); + if (!commit) + die("couldn't look up newly created commit"); + if (!commit || 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 (!user_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.abbrev = 0; + 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.rename_limit = 100; + rev.diffopt.break_opt = 0; + diff_setup_done(&rev.diffopt); + + printf("[%s%s ", + !prefixcmp(head, "refs/heads/") ? + head + 11 : + !strcmp(head, "HEAD") ? + "detached HEAD" : + head, + initial_commit ? " (root-commit)" : ""); + + if (!log_tree_commit(&rev, commit)) { + struct pretty_print_context ctx = {0}; + struct strbuf buf = STRBUF_INIT; + ctx.date_mode = DATE_NORMAL; + format_commit_message(commit, format.buf + 7, &buf, &ctx); + printf("%s\n", buf.buf); + strbuf_release(&buf); + } + strbuf_release(&format); +} + +static int git_commit_config(const char *k, const char *v, void *cb) +{ + struct wt_status *s = cb; + + if (!strcmp(k, "commit.template")) + return git_config_pathname(&template_file, k, v); + if (!strcmp(k, "commit.status")) { + include_status = git_config_bool(k, v); + return 0; + } + + return git_status_config(k, v, s); +} + ++static const char post_rewrite_hook[] = "hooks/post-rewrite"; ++ ++static int run_rewrite_hook(const unsigned char *oldsha1, ++ const unsigned char *newsha1) ++{ ++ /* oldsha1 SP newsha1 LF NUL */ ++ static char buf[2*40 + 3]; ++ struct child_process proc; ++ const char *argv[3]; ++ int code; ++ size_t n; ++ ++ if (access(git_path(post_rewrite_hook), X_OK) < 0) ++ return 0; ++ ++ argv[0] = git_path(post_rewrite_hook); ++ argv[1] = "amend"; ++ argv[2] = NULL; ++ ++ memset(&proc, 0, sizeof(proc)); ++ proc.argv = argv; ++ proc.in = -1; ++ proc.stdout_to_stderr = 1; ++ ++ code = start_command(&proc); ++ if (code) ++ return code; ++ n = snprintf(buf, sizeof(buf), "%s %s\n", ++ sha1_to_hex(oldsha1), sha1_to_hex(newsha1)); ++ write_in_full(proc.in, buf, n); ++ close(proc.in); ++ return finish_command(&proc); ++} ++ +int cmd_commit(int argc, const char **argv, const char *prefix) +{ + struct strbuf sb = STRBUF_INIT; + const char *index_file, *reflog_msg; + char *nl, *p; + unsigned char commit_sha1[20]; + struct ref_lock *ref_lock; + struct commit_list *parents = NULL, **pptr = &parents; + struct stat statbuf; + int allow_fast_forward = 1; + struct wt_status s; + + wt_status_prepare(&s); + git_config(git_commit_config, &s); + in_merge = file_exists(git_path("MERGE_HEAD")); + s.in_merge = in_merge; + + if (s.use_color == -1) + s.use_color = git_use_color_default; + argc = parse_and_validate_options(argc, argv, builtin_commit_usage, + prefix, &s); + if (dry_run) { + if (diff_use_color_default == -1) + diff_use_color_default = git_use_color_default; + return dry_run_commit(argc, argv, prefix, &s); + } + index_file = prepare_index(argc, argv, prefix, 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, &s)) { + rollback_index_files(); + return 1; + } + + /* Determine parents */ + if (initial_commit) { + reflog_msg = "commit (initial)"; + } else if (amend) { + struct commit_list *c; + struct commit *commit; + + reflog_msg = "commit (amend)"; + commit = lookup_commit(head_sha1); + if (!commit || parse_commit(commit)) + die("could not parse HEAD commit"); + + for (c = commit->parents; c; c = c->next) + pptr = &commit_list_insert(c->item, pptr)->next; + } else if (in_merge) { + struct strbuf m = STRBUF_INIT; + FILE *fp; + + reflog_msg = "commit (merge)"; + pptr = &commit_list_insert(lookup_commit(head_sha1), pptr)->next; + fp = fopen(git_path("MERGE_HEAD"), "r"); + if (fp == NULL) + die_errno("could not open '%s' for reading", + git_path("MERGE_HEAD")); + while (strbuf_getline(&m, fp, '\n') != EOF) { + unsigned char sha1[20]; + if (get_sha1_hex(m.buf, sha1) < 0) + die("Corrupt MERGE_HEAD file (%s)", m.buf); + pptr = &commit_list_insert(lookup_commit(sha1), pptr)->next; + } + 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 { + reflog_msg = "commit"; + pptr = &commit_list_insert(lookup_commit(head_sha1), pptr)->next; + } + + /* 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)); + } + + /* Truncate the message just before the diff, if any. */ + if (verbose) { + p = strstr(sb.buf, "\ndiff --git "); + if (p != NULL) + strbuf_setlen(&sb, p - sb.buf + 1); + } + + if (cleanup_mode != CLEANUP_NONE) + stripspace(&sb, cleanup_mode == CLEANUP_ALL); + if (message_is_empty(&sb)) { + rollback_index_files(); + fprintf(stderr, "Aborting commit due to empty commit message.\n"); + exit(1); + } + + if (commit_tree(sb.buf, active_cache_tree->sha1, parents, commit_sha1, + fmt_ident(author_name, author_email, author_date, + IDENT_ERROR_ON_NO_NAME))) { + rollback_index_files(); + die("failed to write commit object"); + } + + ref_lock = lock_any_ref_for_update("HEAD", + initial_commit ? NULL : head_sha1, + 0); + + 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); + + if (!ref_lock) { + rollback_index_files(); + die("cannot lock HEAD ref"); + } + if (write_ref_sha1(ref_lock, commit_sha1, sb.buf) < 0) { + rollback_index_files(); + die("cannot update HEAD ref"); + } + + 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 or quota is\n" + "not exceeded, and then \"git reset HEAD\" to recover."); + + rerere(0); + run_hook(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) { ++ copy_note_for_rewrite(cfg, head_sha1, commit_sha1); ++ finish_copy_notes_for_rewrite(cfg); ++ } ++ run_rewrite_hook(head_sha1, commit_sha1); ++ } + if (!quiet) + print_summary(prefix, commit_sha1); + + return 0; +} diff --combined builtin/log.c index fade77200f,0000000000..a8dd8c989c mode 100644,000000..100644 --- a/builtin/log.c +++ b/builtin/log.c @@@ -1,1401 -1,0 +1,1406 @@@ +/* + * Builtin "git log" and related commands (show, whatchanged) + * + * (C) Copyright 2006 Linus Torvalds + * 2006 Junio Hamano + */ +#include "cache.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" + +/* Set a default date-time format for git log ("log.date" config variable) */ +static const char *default_date_mode = NULL; + +static int default_show_root = 1; +static const char *fmt_patch_subject_prefix = "PATCH"; +static const char *fmt_pretty; + +static const char * const builtin_log_usage = + "git log [] [..] [[--] ...]\n" + " or: git show [options] ..."; + +static void cmd_log_init(int argc, const char **argv, const char *prefix, + struct rev_info *rev, struct setup_revision_opt *opt) +{ + int i; + int decoration_style = 0; + + rev->abbrev = DEFAULT_ABBREV; + rev->commit_format = CMIT_FMT_DEFAULT; + if (fmt_pretty) + get_commit_format(fmt_pretty, rev); + rev->verbose_header = 1; + DIFF_OPT_SET(&rev->diffopt, RECURSIVE); + rev->show_root_diff = default_show_root; + rev->subject_prefix = fmt_patch_subject_prefix; + DIFF_OPT_SET(&rev->diffopt, ALLOW_TEXTCONV); + + if (default_date_mode) + rev->date_mode = parse_date_format(default_date_mode); + + /* + * Check for -h before setup_revisions(), or "git log -h" will + * fail when run without a git directory. + */ + if (argc == 2 && !strcmp(argv[1], "-h")) + usage(builtin_log_usage); + argc = setup_revisions(argc, argv, rev, opt); + + if (!rev->show_notes_given && !rev->pretty_given) + rev->show_notes = 1; ++ if (rev->show_notes) ++ init_display_notes(&rev->notes_opt); + + if (rev->diffopt.pickaxe || rev->diffopt.filter) + rev->always_show_header = 0; + if (DIFF_OPT_TST(&rev->diffopt, FOLLOW_RENAMES)) { + rev->always_show_header = 0; + if (rev->diffopt.nr_paths != 1) + usage("git logs can only follow renames on one pathname at a time"); + } + for (i = 1; i < argc; i++) { + const char *arg = argv[i]; + if (!strcmp(arg, "--decorate")) { + decoration_style = DECORATE_SHORT_REFS; + } else if (!prefixcmp(arg, "--decorate=")) { + const char *v = skip_prefix(arg, "--decorate="); + if (!strcmp(v, "full")) + decoration_style = DECORATE_FULL_REFS; + else if (!strcmp(v, "short")) + decoration_style = DECORATE_SHORT_REFS; + else + die("invalid --decorate option: %s", arg); + } else if (!strcmp(arg, "--source")) { + rev->show_source = 1; + } else if (!strcmp(arg, "-h")) { + usage(builtin_log_usage); + } else + die("unrecognized argument: %s", arg); + } + if (decoration_style) { + rev->show_decorations = 1; + load_ref_decorations(decoration_style); + } +} + +/* + * 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); + } + printf("Final output: %d %s\n", nr, stage); +} + +static struct itimerval early_output_timer; + +static void log_show_early(struct rev_info *revs, struct commit_list *list) +{ + int i = revs->early_output; + int show_header = 1; + + sort_in_topological_order(&list, revs->lifo); + 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: + return; + } + list = list->next; + } + + /* Did we already get enough commits for the early output? */ + if (!i) + 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; + + 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 + */ + while ((commit = get_revision(rev)) != NULL) { + log_tree_commit(rev, commit); + if (!rev->reflog_info) { + /* we allow cycles in reflog ancestry */ + free(commit->buffer); + commit->buffer = NULL; + } + free_commit_list(commit->parents); + commit->parents = NULL; + } + 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) +{ + 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.date")) + return git_config_string(&default_date_mode, var, value); + if (!strcmp(var, "log.showroot")) { + default_show_root = git_config_bool(var, value); + return 0; + } + 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; + + git_config(git_log_config, NULL); + + if (diff_use_color_default == -1) + diff_use_color_default = git_use_color_default; + + init_revisions(&rev, prefix); + rev.diff = 1; + rev.simplify_history = 0; + memset(&opt, 0, sizeof(opt)); + opt.def = "HEAD"; + 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; + + pp_user_info("Tagger", rev->commit_format, &out, buf, rev->date_mode, + git_log_output_encoding ? + git_log_output_encoding: git_commit_encoding); + printf("%s", out.buf); + strbuf_release(&out); +} + +static int show_object(const unsigned char *sha1, int show_tag_object, + struct rev_info *rev) +{ + unsigned long size; + enum object_type type; + char *buf = read_sha1_file(sha1, &type, &size); + int offset = 0; + + if (!buf) + return error("Could not read object %s", sha1_to_hex(sha1)); + + if (show_tag_object) + while (offset < size && buf[offset] != '\n') { + int new_offset = offset + 1; + while (new_offset < size && buf[new_offset++] != '\n') + ; /* do nothing */ + if (!prefixcmp(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, stdout); + free(buf); + return 0; +} + +static int show_tree_object(const unsigned char *sha1, + const char *base, int baselen, + const char *pathname, unsigned mode, int stage, void *context) +{ + printf("%s%s\n", pathname, S_ISDIR(mode) ? "/" : ""); + return 0; +} + +static void show_rev_tweak_rev(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", nor "--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; + int i, count, ret = 0; + + git_config(git_log_config, NULL); + + if (diff_use_color_default == -1) + diff_use_color_default = git_use_color_default; + + init_revisions(&rev, prefix); + rev.diff = 1; + rev.always_show_header = 1; + rev.no_walk = 1; + memset(&opt, 0, sizeof(opt)); + opt.def = "HEAD"; + opt.tweak = show_rev_tweak_rev; + cmd_log_init(argc, argv, prefix, &rev, &opt); + + 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_object(o->sha1, 0, NULL); + break; + case OBJ_TAG: { + struct tag *t = (struct tag *)o; + + if (rev.shown_one) + putchar('\n'); + printf("%stag %s%s\n", + diff_get_color_opt(&rev.diffopt, DIFF_COMMIT), + t->tag, + diff_get_color_opt(&rev.diffopt, DIFF_RESET)); + ret = show_object(o->sha1, 1, &rev); + rev.shown_one = 1; + if (ret) + break; + o = parse_object(t->tagged->sha1); + if (!o) + ret = error("Could not read object %s", + sha1_to_hex(t->tagged->sha1)); + objects[i].item = o; + i--; + break; + } + case OBJ_TREE: + if (rev.shown_one) + putchar('\n'); + printf("%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, NULL, + show_tree_object, NULL); + 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; + + git_config(git_log_config, NULL); + + if (diff_use_color_default == -1) + diff_use_color_default = git_use_color_default; + + init_revisions(&rev, prefix); + init_reflog_walk(&rev.reflog_info); + rev.abbrev_commit = 1; + rev.verbose_header = 1; + memset(&opt, 0, sizeof(opt)); + opt.def = "HEAD"; + cmd_log_init(argc, argv, prefix, &rev, &opt); + + /* + * This means that we override whatever commit format the user gave + * on the cmd line. Sad, but cmd_log_init() currently doesn't + * allow us to set a different default. + */ + rev.commit_format = CMIT_FMT_ONELINE; + rev.use_terminator = 1; + rev.always_show_header = 1; + + /* + * We get called through "git reflog", so unlike the other log + * routines, we need to set up our pager manually.. + */ + setup_pager(); + + return cmd_log_walk(&rev); +} + +int cmd_log(int argc, const char **argv, const char *prefix) +{ + struct rev_info rev; + struct setup_revision_opt opt; + + git_config(git_log_config, NULL); + + if (diff_use_color_default == -1) + diff_use_color_default = git_use_color_default; + + init_revisions(&rev, prefix); + rev.always_show_header = 1; + memset(&opt, 0, sizeof(opt)); + opt.def = "HEAD"; + 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; +static struct string_list extra_to; +static struct string_list extra_cc; + +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(value + 4, &extra_to); + len -= 4; + } else if (!strncasecmp(value, "cc: ", 4)) { + item = string_list_append(value + 4, &extra_cc); + len -= 4; + } else { + item = string_list_append(value, &extra_hdr); + } + + item->string[len] = '\0'; +} + +#define THREAD_SHALLOW 1 +#define THREAD_DEEP 2 +static int thread = 0; +static int do_signoff = 0; + +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(value, &extra_to); + return 0; + } + if (!strcmp(var, "format.cc")) { + if (!value) + return config_error_nonbool(var); + string_list_append(value, &extra_cc); + return 0; + } + if (!strcmp(var, "diff.color") || !strcmp(var, "color.diff")) { + 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; + } + + return git_log_config(var, value, cb); +} + +static FILE *realstdout = NULL; +static const char *output_directory = NULL; +static int outdir_offset; + +static int reopen_stdout(struct commit *commit, struct rev_info *rev) +{ + struct strbuf filename = STRBUF_INIT; + int suffix_len = strlen(fmt_patch_suffix) + 1; + + if (output_directory) { + strbuf_addstr(&filename, output_directory); + if (filename.len >= + PATH_MAX - FORMAT_PATCH_NAME_MAX - suffix_len) + return error("name of output directory is too long"); + if (filename.buf[filename.len - 1] != '/') + strbuf_addch(&filename, '/'); + } + + get_patch_filename(commit, rev->nr, fmt_patch_suffix, &filename); + + if (!DIFF_OPT_TST(&rev->diffopt, QUICK)) + fprintf(realstdout, "%s\n", filename.buf + outdir_offset); + + if (freopen(filename.buf, "w", stdout) == NULL) + return error("Cannot open patch file %s", filename.buf); + + strbuf_release(&filename); + return 0; +} + +static void get_patch_ids(struct rev_info *rev, struct patch_ids *ids, const char *prefix) +{ + struct rev_info check_rev; + struct commit *commit; + struct object *o1, *o2; + unsigned flags1, flags2; + + if (rev->pending.nr != 2) + die("Need exactly one range."); + + o1 = rev->pending.objects[0].item; + flags1 = o1->flags; + o2 = rev->pending.objects[1].item; + flags2 = o2->flags; + + 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, prefix); + 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) { + /* ignore merges */ + if (commit->parents && commit->parents->next) + continue; + + add_commit_patch_id(commit, ids); + } + + /* reset for next revision walk */ + clear_commit_marks((struct commit *)o1, + SEEN | UNINTERESTING | SHOWN | ADDED); + clear_commit_marks((struct commit *)o2, + SEEN | UNINTERESTING | SHOWN | ADDED); + o1->flags = flags1; + o2->flags = flags2; +} + +static void gen_message_id(struct rev_info *info, char *base) +{ + const char *committer = git_committer_info(IDENT_WARN_ON_NO_NAME); + const char *email_start = strrchr(committer, '<'); + const char *email_end = strrchr(committer, '>'); + struct strbuf buf = STRBUF_INIT; + if (!email_start || !email_end || email_start > email_end - 1) + die("Could not extract email from committer identity."); + strbuf_addf(&buf, "%s.%lu.git.%.*s", base, + (unsigned long) time(NULL), + (int)(email_end - email_start - 1), email_start + 1); + info->message_id = strbuf_detach(&buf, NULL); +} + +static void make_cover_letter(struct rev_info *rev, int use_stdout, + int numbered, int numbered_files, + struct commit *origin, + int nr, struct commit **list, struct commit *head) +{ + const char *committer; + const char *subject_start = NULL; + const char *body = "*** SUBJECT HERE ***\n\n*** BLURB HERE ***\n"; + const char *msg; + const char *extra_headers = rev->extra_headers; + 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 commit *commit = NULL; + + if (rev->commit_format != CMIT_FMT_EMAIL) + die("Cover letter needs email format"); + + committer = git_committer_info(0); + + if (!numbered_files) { + /* + * We fake a commit for the cover letter so we get the filename + * desired. + */ + commit = xcalloc(1, sizeof(*commit)); + commit->buffer = xmalloc(400); + snprintf(commit->buffer, 400, + "tree 0000000000000000000000000000000000000000\n" + "parent %s\n" + "author %s\n" + "committer %s\n\n" + "cover letter\n", + sha1_to_hex(head->object.sha1), committer, committer); + } + + if (!use_stdout && reopen_stdout(commit, rev)) + return; + + if (commit) { + + free(commit->buffer); + free(commit); + } + + log_write_email_headers(rev, head, &subject_start, &extra_headers, + &need_8bit_cte); + + for (i = 0; !need_8bit_cte && i < nr; i++) + if (has_non_ascii(list[i]->buffer)) + need_8bit_cte = 1; + + msg = body; + pp_user_info(NULL, CMIT_FMT_EMAIL, &sb, committer, DATE_RFC2822, + encoding); + pp_title_line(CMIT_FMT_EMAIL, &msg, &sb, subject_start, extra_headers, + encoding, need_8bit_cte); + pp_remainder(CMIT_FMT_EMAIL, &msg, &sb, 0); + printf("%s\n", sb.buf); + + strbuf_release(&sb); + + shortlog_init(&log); + log.wrap_lines = 1; + log.wrap = 72; + log.in1 = 2; + log.in2 = 4; + 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_sha1(origin->tree->object.sha1, + head->tree->object.sha1, + "", &opts); + diffcore_std(&opts); + diff_flush(&opts); + + printf("\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 xstrdup(prefix_filename(prefix, outdir_offset, + output_directory)); +} + +static const char * const builtin_format_patch_usage[] = { + "git format-patch [options] [ | ]", + 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 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(arg, &extra_to); + 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(arg, &extra_cc); + return 0; +} + +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 numbered_files = 0; /* _just_ numbers */ + int ignore_if_in_upstream = 0; + int cover_letter = 0; + int boundary_count = 0; + int no_binary_diff = 0; + struct commit *origin = NULL, *head = NULL; + const char *in_reply_to = NULL; + struct patch_ids ids; + char *add_signoff = NULL; + struct strbuf buf = STRBUF_INIT; + int use_patch_format = 0; + const struct option builtin_format_patch_options[] = { + { OPTION_CALLBACK, 'n', "numbered", &numbered, NULL, + "use [PATCH n/m] even with a single patch", + PARSE_OPT_NOARG, numbered_callback }, + { OPTION_CALLBACK, 'N', "no-numbered", &numbered, NULL, + "use [PATCH] even with multiple patches", + PARSE_OPT_NOARG, no_numbered_callback }, + OPT_BOOLEAN('s', "signoff", &do_signoff, "add Signed-off-by:"), + OPT_BOOLEAN(0, "stdout", &use_stdout, + "print patches to standard out"), + OPT_BOOLEAN(0, "cover-letter", &cover_letter, + "generate a cover letter"), + OPT_BOOLEAN(0, "numbered-files", &numbered_files, + "use simple number sequence for output file names"), + OPT_STRING(0, "suffix", &fmt_patch_suffix, "sfx", + "use instead of '.patch'"), + OPT_INTEGER(0, "start-number", &start_number, + "start numbering patches at instead of 1"), + { OPTION_CALLBACK, 0, "subject-prefix", &rev, "prefix", + "Use [] instead of [PATCH]", + PARSE_OPT_NONEG, subject_prefix_callback }, + { OPTION_CALLBACK, 'o', "output-directory", &output_directory, + "dir", "store resulting files in ", + PARSE_OPT_NONEG, output_directory_callback }, + { OPTION_CALLBACK, 'k', "keep-subject", &rev, NULL, + "don't strip/add [PATCH]", + PARSE_OPT_NOARG | PARSE_OPT_NONEG, keep_callback }, + OPT_BOOLEAN(0, "no-binary", &no_binary_diff, + "don't output binary diffs"), + OPT_BOOLEAN(0, "ignore-if-in-upstream", &ignore_if_in_upstream, + "don't include a patch matching a commit upstream"), + { OPTION_BOOLEAN, 'p', "no-stat", &use_patch_format, NULL, + "show patch format instead of default (patch + stat)", + PARSE_OPT_NONEG | PARSE_OPT_NOARG }, + OPT_GROUP("Messaging"), + { OPTION_CALLBACK, 0, "add-header", NULL, "header", + "add email header", 0, header_callback }, + { OPTION_CALLBACK, 0, "to", NULL, "email", "add To: header", + 0, to_callback }, + { OPTION_CALLBACK, 0, "cc", NULL, "email", "add Cc: header", + 0, cc_callback }, + OPT_STRING(0, "in-reply-to", &in_reply_to, "message-id", + "make first mail a reply to "), + { OPTION_CALLBACK, 0, "attach", &rev, "boundary", + "attach the patch", PARSE_OPT_OPTARG, + attach_callback }, + { OPTION_CALLBACK, 0, "inline", &rev, "boundary", + "inline the patch", + PARSE_OPT_OPTARG | PARSE_OPT_NONEG, + inline_callback }, + { OPTION_CALLBACK, 0, "thread", &thread, "style", + "enable message threading, styles: shallow, deep", + PARSE_OPT_OPTARG, thread_callback }, + OPT_END() + }; + + extra_hdr.strdup_strings = 1; + extra_to.strdup_strings = 1; + extra_cc.strdup_strings = 1; + git_config(git_format_config, NULL); + init_revisions(&rev, prefix); + rev.commit_format = CMIT_FMT_EMAIL; + rev.verbose_header = 1; + rev.diff = 1; + rev.combine_merges = 0; + rev.ignore_merges = 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"; + + 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 (do_signoff) { + const char *committer; + const char *endpos; + committer = git_committer_info(IDENT_ERROR_ON_NO_NAME); + endpos = strchr(committer, '>'); + if (!endpos) + die("bogus committer info %s", committer); + add_signoff = xmemdupz(committer, endpos - committer + 1); + } + + 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 (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 and -k are mutually exclusive."); + + 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; + + 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 (!use_stdout) + output_directory = set_outdir(prefix, output_directory); + + if (output_directory) { + 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) { + 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); + } + /* + * Otherwise, it is "format-patch -22 HEAD", and/or + * "format-patch --root HEAD". The user wants + * get_revision() to do the usual traversal. + */ + } + + /* + * 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 (cover_letter) { + /* remember the range */ + int i; + for (i = 0; i < rev.pending.nr; i++) { + struct object *o = rev.pending.objects[i].item; + if (!(o->flags & UNINTERESTING)) + head = (struct commit *)o; + } + /* We can't generate a cover letter without any patches */ + if (!head) + return 0; + } + + if (ignore_if_in_upstream) + get_patch_ids(&rev, &ids, prefix); + + if (!use_stdout) + realstdout = xfdopen(xdup(1), "w"); + + 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; + } + + /* ignore merges */ + if (commit->parents && commit->parents->next) + continue; + + if (ignore_if_in_upstream && + has_commit_patch_id(commit, &ids)) + continue; + + nr++; + list = xrealloc(list, nr * sizeof(list[0])); + list[nr - 1] = commit; + } + total = nr; + if (!keep_subject && auto_number && total > 1) + numbered = 1; + if (numbered) + rev.total = total + start_number - 1; + 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(msgid, rev.ref_message_ids); + } + rev.numbered_files = numbered_files; + rev.patch_suffix = fmt_patch_suffix; + if (cover_letter) { + if (thread) + gen_message_id(&rev, "cover"); + make_cover_letter(&rev, use_stdout, numbered, numbered_files, + origin, nr, list, head); + total++; + start_number--; + } + rev.add_signoff = add_signoff; + while (0 <= --nr) { + int shown; + 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 . + * + * 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.message_id, + rev.ref_message_ids); + } + gen_message_id(&rev, sha1_to_hex(commit->object.sha1)); + } + + if (!use_stdout && reopen_stdout(numbered_files ? NULL : commit, + &rev)) + die("Failed to create output files"); + shown = log_tree_commit(&rev, commit); + free(commit->buffer); + commit->buffer = NULL; + + /* 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) { + if (rev.mime_boundary) + printf("\n--%s%s--\n\n\n", + mime_boundary_leader, + rev.mime_boundary); + else + printf("-- \n%s\n\n", git_version_string); + } + if (!use_stdout) + fclose(stdout); + } + free(list); + 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) +{ + unsigned char sha1[20]; + if (get_sha1(arg, sha1) == 0) { + struct commit *commit = lookup_commit_reference(sha1); + if (commit) { + commit->object.flags |= flags; + add_pending_object(revs, &commit->object, arg); + return 0; + } + } + return -1; +} + +static const char cherry_usage[] = +"git cherry [-v] [ [ []]]"; +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; + + if (argc > 1 && !strcmp(argv[1], "-v")) { + verbose = 1; + argc--; + argv++; + } + + if (argc > 1 && !strcmp(argv[1], "-h")) + usage(cherry_usage); + + switch (argc) { + case 4: + limit = argv[3]; + /* FALLTHROUGH */ + case 3: + head = argv[2]; + /* FALLTHROUGH */ + case 2: + upstream = argv[1]; + break; + default: + current_branch = branch_get(NULL); + if (!current_branch || !current_branch->merge + || !current_branch->merge[0] + || !current_branch->merge[0]->dst) { + fprintf(stderr, "Could not find a tracked" + " remote branch, please" + " specify manually.\n"); + usage(cherry_usage); + } + + upstream = current_branch->merge[0]->dst; + } + + init_revisions(&revs, prefix); + revs.diff = 1; + revs.combine_merges = 0; + revs.ignore_merges = 1; + DIFF_OPT_SET(&revs.diffopt, RECURSIVE); + + 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 (hashcmp(o[0].item->sha1, o[1].item->sha1) == 0) + return 0; + } + + get_patch_ids(&revs, &ids, prefix); + + 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) { + /* ignore merges */ + if (commit->parents && commit->parents->next) + continue; + + commit_list_insert(commit, &list); + } + + while (list) { + char sign = '+'; + + commit = list->item; + if (has_commit_patch_id(commit, &ids)) + sign = '-'; + + if (verbose) { + struct strbuf buf = STRBUF_INIT; + struct pretty_print_context ctx = {0}; + pretty_print_commit(CMIT_FMT_ONELINE, commit, + &buf, &ctx); + printf("%c %s %s\n", sign, + sha1_to_hex(commit->object.sha1), buf.buf); + strbuf_release(&buf); + } + else { + printf("%c %s\n", sign, + sha1_to_hex(commit->object.sha1)); + } + + list = list->next; + } + + free_patch_ids(&ids); + return 0; +} diff --combined builtin/notes.c index feb710ac4a,0000000000..4543d11311 mode 100644,000000..100644 --- a/builtin/notes.c +++ b/builtin/notes.c @@@ -1,455 -1,0 +1,650 @@@ +/* + * Builtin "git notes" + * + * Copyright (c) 2010 Johan Herland + * + * Based on git-notes.sh by Johannes Schindelin, + * and builtin-tag.c by Kristian Høgsberg and Carlos Rica. + */ + +#include "cache.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" + +static const char * const git_notes_usage[] = { + "git notes [list []]", + "git notes add [-f] [-m | -F | (-c | -C) ] []", + "git notes copy [-f] ", + "git notes append [-m | -F | (-c | -C) ] []", + "git notes edit []", + "git notes show []", + "git notes remove []", + "git notes prune", + NULL +}; + +static const char note_template[] = + "\n" + "#\n" + "# Write/edit the notes for the following object:\n" + "#\n"; + +struct msg_arg { + int given; + int use_editor; + struct strbuf buf; +}; + +static int list_each_note(const unsigned char *object_sha1, + const unsigned char *note_sha1, char *note_path, + void *cb_data) +{ + printf("%s %s\n", sha1_to_hex(note_sha1), sha1_to_hex(object_sha1)); + return 0; +} + +static void write_note_data(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 unsigned char *object) +{ + const char *show_args[5] = + {"show", "--stat", "--no-notes", sha1_to_hex(object), NULL}; + struct child_process show; + struct strbuf buf = STRBUF_INIT; + FILE *show_out; + + /* Invoke "git show --stat --no-notes $object" */ + memset(&show, 0, sizeof(show)); + 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'", + sha1_to_hex(object)); + + /* Open the output as FILE* so strbuf_getline() can be used. */ + show_out = xfdopen(show.out, "r"); + if (show_out == NULL) + die_errno("can't fdopen 'show' output fd"); + + /* Prepend "# " to each output line and write result to 'fd' */ + while (strbuf_getline(&buf, show_out, '\n') != EOF) { + write_or_die(fd, "# ", 2); + write_or_die(fd, buf.buf, buf.len); + write_or_die(fd, "\n", 1); + } + strbuf_release(&buf); + if (fclose(show_out)) + die_errno("failed to close pipe to 'show' for object '%s'", + sha1_to_hex(object)); + if (finish_command(&show)) + die("failed to finish 'show' for object '%s'", + sha1_to_hex(object)); +} + +static void create_note(const unsigned char *object, struct msg_arg *msg, + int append_only, const unsigned char *prev, + unsigned char *result) +{ + char *path = NULL; + + if (msg->use_editor || !msg->given) { + int fd; + + /* write the template message before editing: */ + path = git_pathdup("NOTES_EDITMSG"); + fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0600); + if (fd < 0) + die_errno("could not create file '%s'", path); + + if (msg->given) + write_or_die(fd, msg->buf.buf, msg->buf.len); + else if (prev && !append_only) + write_note_data(fd, prev); + write_or_die(fd, note_template, strlen(note_template)); + + write_commented_object(fd, object); + + close(fd); + strbuf_reset(&(msg->buf)); + + if (launch_editor(path, &(msg->buf), NULL)) { + die("Please supply the note contents using either -m" \ + " or -F option"); + } + stripspace(&(msg->buf), 1); + } + + if (prev && append_only) { + /* Append buf to previous note contents */ + unsigned long size; + enum object_type type; + char *prev_buf = read_sha1_file(prev, &type, &size); + + strbuf_grow(&(msg->buf), size + 1); + if (msg->buf.len && prev_buf && size) + strbuf_insert(&(msg->buf), 0, "\n", 1); + if (prev_buf && size) + strbuf_insert(&(msg->buf), 0, prev_buf, size); + free(prev_buf); + } + + if (!msg->buf.len) { + fprintf(stderr, "Removing note for object %s\n", + sha1_to_hex(object)); + hashclr(result); + } else { + if (write_sha1_file(msg->buf.buf, msg->buf.len, blob_type, result)) { + error("unable to write note object"); + if (path) + error("The note contents has been left in %s", + path); + exit(128); + } + } + + if (path) { + unlink_or_warn(path); + free(path); + } +} + +static int parse_msg_arg(const struct option *opt, const char *arg, int unset) +{ + struct msg_arg *msg = opt->value; + + strbuf_grow(&(msg->buf), strlen(arg) + 2); + if (msg->buf.len) + strbuf_addch(&(msg->buf), '\n'); + strbuf_addstr(&(msg->buf), arg); + stripspace(&(msg->buf), 0); + + msg->given = 1; + return 0; +} + +static int parse_file_arg(const struct option *opt, const char *arg, int unset) +{ + struct msg_arg *msg = opt->value; + + if (msg->buf.len) + strbuf_addch(&(msg->buf), '\n'); + if (!strcmp(arg, "-")) { + if (strbuf_read(&(msg->buf), 0, 1024) < 0) + die_errno("cannot read '%s'", arg); + } else if (strbuf_read_file(&(msg->buf), arg, 1024) < 0) + die_errno("could not open or read '%s'", arg); + stripspace(&(msg->buf), 0); + + msg->given = 1; + return 0; +} + +static int parse_reuse_arg(const struct option *opt, const char *arg, int unset) +{ + struct msg_arg *msg = opt->value; + char *buf; + unsigned char object[20]; + enum object_type type; + unsigned long len; + + if (msg->buf.len) + strbuf_addch(&(msg->buf), '\n'); + + if (get_sha1(arg, object)) + die("Failed to resolve '%s' as a valid ref.", arg); + if (!(buf = read_sha1_file(object, &type, &len)) || !len) { + free(buf); + die("Failed to read object '%s'.", arg);; + } + strbuf_add(&(msg->buf), buf, len); + free(buf); + + msg->given = 1; + return 0; +} + +static int parse_reedit_arg(const struct option *opt, const char *arg, int unset) +{ + struct msg_arg *msg = opt->value; + msg->use_editor = 1; + return parse_reuse_arg(opt, arg, unset); +} + +int commit_notes(struct notes_tree *t, const char *msg) +{ + struct commit_list *parent; + unsigned char tree_sha1[20], prev_commit[20], new_commit[20]; + struct strbuf buf = STRBUF_INIT; + + if (!t) + t = &default_notes_tree; + if (!t->initialized || !t->ref || !*t->ref) + die("Cannot commit uninitialized/unreferenced notes tree"); ++ if (!t->dirty) ++ return 0; /* don't have to commit an unchanged tree */ + + /* Prepare commit message and reflog message */ + strbuf_addstr(&buf, "notes: "); /* commit message starts at index 7 */ + strbuf_addstr(&buf, msg); + if (buf.buf[buf.len - 1] != '\n') + strbuf_addch(&buf, '\n'); /* Make sure msg ends with newline */ + + /* Convert notes tree to tree object */ + if (write_notes_tree(t, tree_sha1)) + die("Failed to write current notes tree to database"); + + /* Create new commit for the tree object */ + if (!read_ref(t->ref, prev_commit)) { /* retrieve parent commit */ + parent = xmalloc(sizeof(*parent)); + parent->item = lookup_commit(prev_commit); + parent->next = NULL; + } else { + hashclr(prev_commit); + parent = NULL; + } + if (commit_tree(buf.buf + 7, tree_sha1, parent, new_commit, NULL)) + die("Failed to commit notes tree to database"); + + /* Update notes ref with new commit */ + update_ref(buf.buf, t->ref, new_commit, prev_commit, 0, DIE_ON_ERR); + + strbuf_release(&buf); + return 0; +} + ++ ++combine_notes_fn *parse_combine_notes_fn(const char *v) ++{ ++ if (!strcasecmp(v, "overwrite")) ++ return combine_notes_overwrite; ++ else if (!strcasecmp(v, "ignore")) ++ return combine_notes_ignore; ++ else if (!strcasecmp(v, "concatenate")) ++ return combine_notes_concatenate; ++ else ++ return NULL; ++} ++ ++static int notes_rewrite_config(const char *k, const char *v, void *cb) ++{ ++ struct notes_rewrite_cfg *c = cb; ++ if (!prefixcmp(k, "notes.rewrite.") && !strcmp(k+14, c->cmd)) { ++ c->enabled = git_config_bool(k, v); ++ return 0; ++ } else if (!c->mode_from_env && !strcmp(k, "notes.rewritemode")) { ++ if (!v) ++ config_error_nonbool(k); ++ c->combine = parse_combine_notes_fn(v); ++ if (!c->combine) { ++ error("Bad notes.rewriteMode value: '%s'", v); ++ return 1; ++ } ++ return 0; ++ } else if (!c->refs_from_env && !strcmp(k, "notes.rewriteref")) { ++ /* note that a refs/ prefix is implied in the ++ * underlying for_each_glob_ref */ ++ if (!prefixcmp(v, "refs/notes/")) ++ string_list_add_refs_by_glob(c->refs, v); ++ else ++ warning("Refusing to rewrite notes in %s" ++ " (outside of refs/notes/)", v); ++ return 0; ++ } ++ ++ return 0; ++} ++ ++ ++struct notes_rewrite_cfg *init_copy_notes_for_rewrite(const char *cmd) ++{ ++ struct notes_rewrite_cfg *c = xmalloc(sizeof(struct notes_rewrite_cfg)); ++ const char *rewrite_mode_env = getenv(GIT_NOTES_REWRITE_MODE_ENVIRONMENT); ++ const char *rewrite_refs_env = getenv(GIT_NOTES_REWRITE_REF_ENVIRONMENT); ++ c->cmd = cmd; ++ c->enabled = 1; ++ c->combine = combine_notes_concatenate; ++ c->refs = xcalloc(1, sizeof(struct string_list)); ++ c->refs->strdup_strings = 1; ++ c->refs_from_env = 0; ++ c->mode_from_env = 0; ++ if (rewrite_mode_env) { ++ c->mode_from_env = 1; ++ c->combine = parse_combine_notes_fn(rewrite_mode_env); ++ if (!c->combine) ++ error("Bad " GIT_NOTES_REWRITE_MODE_ENVIRONMENT ++ " value: '%s'", rewrite_mode_env); ++ } ++ if (rewrite_refs_env) { ++ c->refs_from_env = 1; ++ string_list_add_refs_from_colon_sep(c->refs, rewrite_refs_env); ++ } ++ git_config(notes_rewrite_config, c); ++ if (!c->enabled || !c->refs->nr) { ++ string_list_clear(c->refs, 0); ++ free(c->refs); ++ free(c); ++ return NULL; ++ } ++ c->trees = load_notes_trees(c->refs); ++ string_list_clear(c->refs, 0); ++ free(c->refs); ++ return c; ++} ++ ++int copy_note_for_rewrite(struct notes_rewrite_cfg *c, ++ const unsigned char *from_obj, const unsigned char *to_obj) ++{ ++ int ret = 0; ++ int i; ++ for (i = 0; c->trees[i]; i++) ++ ret = copy_note(c->trees[i], from_obj, to_obj, 1, c->combine) || ret; ++ return ret; ++} ++ ++void finish_copy_notes_for_rewrite(struct notes_rewrite_cfg *c) ++{ ++ int i; ++ for (i = 0; c->trees[i]; i++) { ++ commit_notes(c->trees[i], "Notes added by 'git notes copy'"); ++ free_notes(c->trees[i]); ++ } ++ free(c->trees); ++ free(c); ++} ++ ++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; ++ int ret = 0; ++ ++ if (rewrite_cmd) { ++ c = init_copy_notes_for_rewrite(rewrite_cmd); ++ if (!c) ++ return 0; ++ } else { ++ init_notes(NULL, NULL, NULL, 0); ++ t = &default_notes_tree; ++ } ++ ++ while (strbuf_getline(&buf, stdin, '\n') != EOF) { ++ unsigned char from_obj[20], to_obj[20]; ++ 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_sha1(split[0]->buf, from_obj)) ++ die("Failed to resolve '%s' as a valid ref.", split[0]->buf); ++ if (get_sha1(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, "Notes added by 'git notes copy'"); ++ free_notes(t); ++ } else { ++ finish_copy_notes_for_rewrite(c); ++ } ++ return ret; ++} ++ +int cmd_notes(int argc, const char **argv, const char *prefix) +{ + struct notes_tree *t; + unsigned char object[20], from_obj[20], new_note[20]; + const unsigned char *note; + const char *object_ref; + char logmsg[100]; + + int list = 0, add = 0, copy = 0, append = 0, edit = 0, show = 0, - remove = 0, prune = 0, force = 0; ++ remove = 0, prune = 0, force = 0, from_stdin = 0; + int given_object = 0, i = 1, retval = 0; + struct msg_arg msg = { 0, 0, STRBUF_INIT }; ++ const char *rewrite_cmd = NULL; ++ const char *override_notes_ref = NULL; + struct option options[] = { + OPT_GROUP("Notes contents options"), + { OPTION_CALLBACK, 'm', "message", &msg, "MSG", + "note contents as a string", PARSE_OPT_NONEG, + parse_msg_arg}, + { OPTION_CALLBACK, 'F', "file", &msg, "FILE", + "note contents in a file", PARSE_OPT_NONEG, + parse_file_arg}, + { OPTION_CALLBACK, 'c', "reedit-message", &msg, "OBJECT", + "reuse and edit specified note object", PARSE_OPT_NONEG, + parse_reedit_arg}, + { OPTION_CALLBACK, 'C', "reuse-message", &msg, "OBJECT", + "reuse specified note object", PARSE_OPT_NONEG, + parse_reuse_arg}, + OPT_GROUP("Other options"), + OPT_BOOLEAN('f', "force", &force, "replace existing notes"), ++ OPT_BOOLEAN(0, "stdin", &from_stdin, "read objects from stdin"), ++ OPT_STRING(0, "ref", &override_notes_ref, "notes_ref", ++ "use notes from "), ++ OPT_STRING(0, "for-rewrite", &rewrite_cmd, "command", ++ "load rewriting config for (implies --stdin)"), + OPT_END() + }; + + git_config(git_default_config, NULL); + + argc = parse_options(argc, argv, prefix, options, git_notes_usage, 0); + ++ if (override_notes_ref) { ++ struct strbuf sb = STRBUF_INIT; ++ if (!prefixcmp(override_notes_ref, "refs/notes/")) ++ /* we're happy */; ++ else if (!prefixcmp(override_notes_ref, "notes/")) ++ strbuf_addstr(&sb, "refs/"); ++ else ++ strbuf_addstr(&sb, "refs/notes/"); ++ strbuf_addstr(&sb, override_notes_ref); ++ setenv("GIT_NOTES_REF", sb.buf, 1); ++ strbuf_release(&sb); ++ } ++ + if (argc && !strcmp(argv[0], "list")) + list = 1; + else if (argc && !strcmp(argv[0], "add")) + add = 1; + else if (argc && !strcmp(argv[0], "copy")) + copy = 1; + else if (argc && !strcmp(argv[0], "append")) + append = 1; + else if (argc && !strcmp(argv[0], "edit")) + edit = 1; + else if (argc && !strcmp(argv[0], "show")) + show = 1; + else if (argc && !strcmp(argv[0], "remove")) + remove = 1; + else if (argc && !strcmp(argv[0], "prune")) + prune = 1; + else if (!argc) { + list = 1; /* Default to 'list' if no other subcommand given */ + i = 0; + } + + if (list + add + copy + append + edit + show + remove + prune != 1) + usage_with_options(git_notes_usage, options); + + if (msg.given && !(add || append || edit)) { + error("cannot use -m/-F/-c/-C options with %s subcommand.", + argv[0]); + usage_with_options(git_notes_usage, options); + } + + if (msg.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"); + } + + if (force && !(add || copy)) { + error("cannot use -f option with %s subcommand.", argv[0]); + usage_with_options(git_notes_usage, options); + } + ++ if (!copy && rewrite_cmd) { ++ error("cannot use --for-rewrite with %s subcommand.", argv[0]); ++ usage_with_options(git_notes_usage, options); ++ } ++ if (!copy && from_stdin) { ++ error("cannot use --stdin with %s subcommand.", argv[0]); ++ usage_with_options(git_notes_usage, options); ++ } ++ + if (copy) { + const char *from_ref; ++ if (from_stdin || rewrite_cmd) { ++ if (argc > 1) { ++ error("too many parameters"); ++ usage_with_options(git_notes_usage, options); ++ } else { ++ return notes_copy_from_stdin(force, rewrite_cmd); ++ } ++ } + if (argc < 3) { + error("too few parameters"); + usage_with_options(git_notes_usage, options); + } + from_ref = argv[i++]; + if (get_sha1(from_ref, from_obj)) + die("Failed to resolve '%s' as a valid ref.", from_ref); + } + + given_object = argc > i; + object_ref = given_object ? argv[i++] : "HEAD"; + + if (argc > i || (prune && given_object)) { + error("too many parameters"); + usage_with_options(git_notes_usage, options); + } + + if (get_sha1(object_ref, object)) + die("Failed to resolve '%s' as a valid ref.", object_ref); + + init_notes(NULL, NULL, NULL, 0); + t = &default_notes_tree; + + if (prefixcmp(t->ref, "refs/notes/")) + die("Refusing to %s notes in %s (outside of refs/notes/)", + argv[0], t->ref); + + note = get_note(t, object); + + /* list command */ + + if (list) { + if (given_object) { + if (note) { + puts(sha1_to_hex(note)); + goto end; + } + } else { + retval = for_each_note(t, 0, list_each_note, NULL); + goto end; + } + } + + /* show command */ + + if ((list || show) && !note) { + error("No note found for object %s.", sha1_to_hex(object)); + retval = 1; + goto end; + } else if (show) { + const char *show_args[3] = {"show", sha1_to_hex(note), NULL}; + retval = execv_git_cmd(show_args); + goto end; + } + + /* add/append/edit/remove/prune command */ + + if ((add || copy) && note) { + if (!force) { + error("Cannot %s notes. Found existing notes for object" + " %s. Use '-f' to overwrite existing notes", + argv[0], sha1_to_hex(object)); + retval = 1; + goto end; + } + fprintf(stderr, "Overwriting existing notes for object %s\n", + sha1_to_hex(object)); + } + + if (remove) { + msg.given = 1; + msg.use_editor = 0; + strbuf_reset(&(msg.buf)); + } + + if (prune) { + hashclr(new_note); + prune_notes(t); + goto commit; + } else if (copy) { + const unsigned char *from_note = get_note(t, from_obj); + if (!from_note) { + error("Missing notes on source object %s. Cannot copy.", + sha1_to_hex(from_obj)); + retval = 1; + goto end; + } + hashcpy(new_note, from_note); + } else + create_note(object, &msg, append, note, new_note); + + if (is_null_sha1(new_note)) + remove_note(t, object); + else + add_note(t, object, new_note, combine_notes_overwrite); + +commit: + snprintf(logmsg, sizeof(logmsg), "Notes %s by 'git notes %s'", + is_null_sha1(new_note) ? "removed" : "added", argv[0]); + commit_notes(t, logmsg); + +end: + free_notes(t); + strbuf_release(&(msg.buf)); + return retval; +} diff --combined cache.h index f62db0bd7e,a04f37f14b..2928107bb1 --- a/cache.h +++ b/cache.h @@@ -182,8 -182,6 +182,8 @@@ struct cache_entry /* Only remove in work directory, not index */ #define CE_WT_REMOVE (0x400000) +#define CE_UNPACKED (0x1000000) + /* * Extended on-disk flags */ @@@ -387,16 -385,10 +387,19 @@@ static inline enum object_type object_t #define ATTRIBUTE_MACRO_PREFIX "[attr]" #define GIT_NOTES_REF_ENVIRONMENT "GIT_NOTES_REF" #define GIT_NOTES_DEFAULT_REF "refs/notes/commits" + #define GIT_NOTES_DISPLAY_REF_ENVIRONMENT "GIT_NOTES_DISPLAY_REF" + #define GIT_NOTES_REWRITE_REF_ENVIRONMENT "GIT_NOTES_REWRITE_REF" + #define GIT_NOTES_REWRITE_MODE_ENVIRONMENT "GIT_NOTES_REWRITE_MODE" +/* + * Repository-local GIT_* environment variables + * The array is NULL-terminated to simplify its usage in contexts such + * environment creation or simple walk of the list. + * The number of non-NULL entries is available as a macro. + */ +#define LOCAL_REPO_ENV_SIZE 8 +extern const char *const local_repo_env[LOCAL_REPO_ENV_SIZE + 1]; + extern int is_bare_repository_cfg; extern int is_bare_repository(void); extern int is_inside_git_dir(void); @@@ -564,7 -556,6 +567,7 @@@ enum branch_track BRANCH_TRACK_REMOTE, BRANCH_TRACK_ALWAYS, BRANCH_TRACK_EXPLICIT, + BRANCH_TRACK_OVERRIDE, }; enum rebase_setup_type { @@@ -650,10 -641,6 +653,10 @@@ int git_mkstemp(char *path, size_t n, c int git_mkstemps(char *path, size_t n, const char *template, int suffix_len); +/* set default permissions by passing mode arguments to open(2) */ +int git_mkstemps_mode(char *pattern, int suffix_len, int mode); +int git_mkstemp_mode(char *pattern, int mode); + /* * NOTE NOTE NOTE!! * @@@ -688,7 -675,6 +691,7 @@@ int normalize_path_copy(char *dst, cons int longest_ancestor_length(const char *path, const char *prefix_list); char *strip_path_suffix(const char *path, const char *suffix); int daemon_avoid_alias(const char *path); +int offset_1st_component(const char *path); /* Read and unpack a sha1 file into memory, write memory to a sha1 file */ extern int sha1_object_info(const unsigned char *, unsigned long *); @@@ -776,8 -762,7 +779,8 @@@ const char *show_date_relative(unsigne size_t timebuf_size); int parse_date(const char *date, char *buf, int bufsize); void datestamp(char *buf, int bufsize); -unsigned long approxidate(const char *); +#define approxidate(s) approxidate_careful((s), NULL) +unsigned long approxidate_careful(const char *, int *); unsigned long approxidate_relative(const char *date, const struct timeval *now); enum date_mode parse_date_format(const char *format); @@@ -789,7 -774,7 +792,7 @@@ extern const char *git_committer_info(i extern const char *fmt_ident(const char *name, const char *email, const char *date_str, int); extern const char *fmt_name(const char *name, const char *email); extern const char *git_editor(void); -extern const char *git_pager(void); +extern const char *git_pager(int stdout_is_tty); struct checkout { const char *base_dir; @@@ -891,7 -876,6 +894,7 @@@ struct ref extern struct ref *find_ref_by_name(const struct ref *list, const char *name); #define CONNECT_VERBOSE (1u << 0) +extern char *git_getpass(const char *prompt); extern struct child_process *git_connect(int fd[2], const char *url, const char *prog, int flags); extern int finish_connect(struct child_process *conn); extern int path_match(const char *path, int nr, char **match); diff --combined git-am.sh index 50a292a7da,7644474bca..1056075545 --- a/git-am.sh +++ b/git-am.sh @@@ -15,8 -15,6 +15,8 @@@ q,quiet be quie s,signoff add a Signed-off-by line to the commit message u,utf8 recode into utf8 (default) k,keep pass -k flag to git-mailinfo +keep-cr pass --keep-cr flag to git-mailsplit for mbox format +no-keep-cr do not pass --keep-cr flag to git-mailsplit independent of am.keepcr c,scissors strip everything before a scissors line whitespace= pass it through git-apply ignore-space-change pass it through git-apply @@@ -27,8 -25,7 +27,8 @@@ p= pass it through git-app patch-format= format the patch(es) are in reject pass it through git-apply resolvemsg= override error message when patch failure occurs -r,resolved to be used after a patch failure +continue continue applying patches after resolving a conflict +r,resolved synonyms for --continue skip skip the current patch abort restore the original branch and abort the patching operation. committer-date-is-author-date lie about committer date @@@ -208,8 -205,7 +208,8 @@@ check_patch_format () # discarding the indented remainder of folded lines, # and see if it looks like that they all begin with the # header field names... - sed -n -e '/^$/q' -e '/^[ ]/d' -e p "$1" | + tr -d '\015' <"$1" | + sed -n -e '/^$/q' -e '/^[ ]/d' -e p | sane_egrep -v '^[!-9;-~]+:' >/dev/null || patch_format=mbox fi @@@ -219,12 -215,12 +219,12 @@@ split_patches () { case "$patch_format" in mbox) - case "$rebasing" in - '') - keep_cr= ;; - ?*) - keep_cr=--keep-cr ;; - esac + if test -n "$rebasing" || test t = "$keepcr" + then + keep_cr=--keep-cr + else + keep_cr= + fi git mailsplit -d"$prec" -o"$dotest" -b $keep_cr -- "$@" > "$dotest/last" || clean_abort ;; @@@ -293,18 -289,13 +293,18 @@@ prec=4 dotest="$GIT_DIR/rebase-apply" -sign= utf8=t keep= skip= interactive= resolved= rebasing= abort= +sign= utf8=t keep= keepcr= skip= interactive= resolved= rebasing= abort= resolvemsg= resume= scissors= no_inbody_headers= git_apply_opt= committer_date_is_author_date= ignore_date= allow_rerere_autoupdate= +if test "$(git config --bool --get am.keepcr)" = true +then + keepcr=t +fi + while test $# != 0 do case "$1" in @@@ -326,7 -317,7 +326,7 @@@ scissors=t ;; --no-scissors) scissors=f ;; - -r|--resolved) + -r|--resolved|--continue) resolved=t ;; --skip) skip=t ;; @@@ -355,10 -346,6 +355,10 @@@ allow_rerere_autoupdate="$1" ;; -q|--quiet) GIT_QUIET=t ;; + --keep-cr) + keepcr=t ;; + --no-keep-cr) + keepcr=f ;; --) shift; break ;; *) @@@ -464,7 -451,6 +464,7 @@@ els echo "$sign" >"$dotest/sign" echo "$utf8" >"$dotest/utf8" echo "$keep" >"$dotest/keep" + echo "$keepcr" >"$dotest/keepcr" echo "$scissors" >"$dotest/scissors" echo "$no_inbody_headers" >"$dotest/no_inbody_headers" echo "$GIT_QUIET" >"$dotest/quiet" @@@ -508,12 -494,6 +508,12 @@@ if test "$(cat "$dotest/keep")" = then keep=-k fi +case "$(cat "$dotest/keepcr")" in +t) + keepcr=--keep-cr ;; +f) + keepcr=--no-keep-cr ;; +esac case "$(cat "$dotest/scissors")" in t) scissors=--scissors ;; @@@ -593,6 -573,7 +593,7 @@@ d echo "Patch is empty. Was it split wrong?" stop_here $this } + rm -f "$dotest/original-commit" if test -f "$dotest/rebasing" && commit=$(sed -e 's/^From \([0-9a-f]*\) .*/\1/' \ -e q "$dotest/$msgnum") && @@@ -600,6 -581,7 +601,7 @@@ then git cat-file commit "$commit" | sed -e '1,/^$/d' >"$dotest/msg-clean" + echo "$commit" > "$dotest/original-commit" else { sed -n '/^Subject/ s/Subject: //p' "$dotest/info" @@@ -681,7 -663,10 +683,7 @@@ [eE]*) git_editor "$dotest/final-commit" action=again ;; [vV]*) action=again - : ${GIT_PAGER=$(git var GIT_PAGER)} - : ${LESS=-FRSX} - export LESS - $GIT_PAGER "$dotest/patch" ;; + git_pager "$dotest/patch" ;; *) action=again ;; esac done @@@ -783,6 -768,10 +785,10 @@@ git update-ref -m "$GIT_REFLOG_ACTION: $FIRSTLINE" HEAD $commit $parent || stop_here $this + if test -f "$dotest/original-commit"; then + echo "$(cat "$dotest/original-commit") $commit" >> "$dotest/rewritten" + fi + if test -x "$GIT_DIR"/hooks/post-applypatch then "$GIT_DIR"/hooks/post-applypatch @@@ -791,5 -780,13 +797,12 @@@ go_next done + if test -s "$dotest"/rewritten; then + git notes copy --for-rewrite=rebase < "$dotest"/rewritten + if test -x "$GIT_DIR"/hooks/post-rewrite; then + "$GIT_DIR"/hooks/post-rewrite rebase < "$dotest"/rewritten + fi + fi + -git gc --auto - rm -fr "$dotest" +git gc --auto diff --combined git-rebase--interactive.sh index 3e4fd1456f,f69c062def..415ae72dbc --- a/git-rebase--interactive.sh +++ b/git-rebase--interactive.sh @@@ -96,6 -96,13 +96,13 @@@ AUTHOR_SCRIPT="$DOTEST"/author-scrip # command is processed, this file is deleted. AMEND="$DOTEST"/amend + # For the post-rewrite hook, we make a list of rewritten commits and + # their new sha1s. The rewritten-pending list keeps the sha1s of + # commits that have been processed, but not committed yet, + # e.g. because they are waiting for a 'squash' command. + REWRITTEN_LIST="$DOTEST"/rewritten-list + REWRITTEN_PENDING="$DOTEST"/rewritten-pending + PRESERVE_MERGES= STRATEGY= ONTO= @@@ -198,6 -205,7 +205,7 @@@ make_patch () } die_with_patch () { + echo "$1" > "$DOTEST"/stopped-sha make_patch "$1" git rerere die "$2" @@@ -215,10 -223,10 +223,10 @@@ has_action () # Run command with GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and # GIT_AUTHOR_DATE exported from the current environment. do_with_author () { - GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" \ - GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" \ - GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" \ - "$@" + ( + export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE + "$@" + ) } pick_one () { @@@ -348,6 -356,7 +356,7 @@@ pick_one_preserving_merges () printf "%s\n" "$msg" > "$GIT_DIR"/MERGE_MSG die_with_patch $sha1 "Error redoing merge $sha1" fi + echo "$sha1 $(git rev-parse HEAD^0)" >> "$REWRITTEN_LIST" ;; *) output git cherry-pick "$@" || @@@ -378,7 -387,7 +387,7 @@@ update_squash_messages () sed -e 1d -e '2,/^./{ /^$/d }' <"$SQUASH_MSG".bak - } >$SQUASH_MSG + } >"$SQUASH_MSG" else commit_message HEAD > "$FIXUP_MSG" || die "Cannot write $FIXUP_MSG" COUNT=2 @@@ -387,7 -396,7 +396,7 @@@ echo "# The first commit's message is:" echo cat "$FIXUP_MSG" - } >$SQUASH_MSG + } >"$SQUASH_MSG" fi case $1 in squash) @@@ -403,11 -412,11 +412,11 @@@ echo commit_message $2 | sed -e 's/^/# /' ;; - esac >>$SQUASH_MSG + esac >>"$SQUASH_MSG" } peek_next_command () { - sed -n -e "/^#/d" -e "/^$/d" -e "s/ .*//p" -e "q" < "$TODO" + sed -n -e "/^#/d" -e '/^$/d' -e "s/ .*//p" -e "q" < "$TODO" } # A squash/fixup has failed. Prepare the long version of the squash @@@ -425,6 -434,26 +434,26 @@@ die_failed_squash() die_with_patch $1 "" } + flush_rewritten_pending() { + test -s "$REWRITTEN_PENDING" || return + newsha1="$(git rev-parse HEAD^0)" + sed "s/$/ $newsha1/" < "$REWRITTEN_PENDING" >> "$REWRITTEN_LIST" + rm -f "$REWRITTEN_PENDING" + } + + record_in_rewritten() { + oldsha1="$(git rev-parse $1)" + echo "$oldsha1" >> "$REWRITTEN_PENDING" + + case "$(peek_next_command)" in + squash|s|fixup|f) + ;; + *) + flush_rewritten_pending + ;; + esac + } + do_next () { rm -f "$MSG" "$AUTHOR_SCRIPT" "$AMEND" || exit read command sha1 rest < "$TODO" @@@ -438,6 -467,7 +467,7 @@@ mark_action_done pick_one $sha1 || die_with_patch $sha1 "Could not apply $sha1... $rest" + record_in_rewritten $sha1 ;; reword|r) comment_for_reflog reword @@@ -445,7 -475,8 +475,8 @@@ mark_action_done pick_one $sha1 || die_with_patch $sha1 "Could not apply $sha1... $rest" - git commit --amend + git commit --amend --no-post-rewrite + record_in_rewritten $sha1 ;; edit|e) comment_for_reflog edit @@@ -453,6 -484,7 +484,7 @@@ mark_action_done pick_one $sha1 || die_with_patch $sha1 "Could not apply $sha1... $rest" + echo "$1" > "$DOTEST"/stopped-sha make_patch $sha1 git rev-parse --verify HEAD > "$AMEND" warn "Stopped at $sha1... $rest" @@@ -509,6 -541,7 +541,7 @@@ rm -f "$SQUASH_MSG" "$FIXUP_MSG" ;; esac + record_in_rewritten $sha1 ;; *) warn "Unknown command: $command $sha1 $rest" @@@ -537,6 -570,15 +570,15 @@@ test ! -f "$DOTEST"/verbose || git diff-tree --stat $(cat "$DOTEST"/head)..HEAD } && + { + git notes copy --for-rewrite=rebase < "$REWRITTEN_LIST" || + true # we don't care if this copying failed + } && + if test -x "$GIT_DIR"/hooks/post-rewrite && + test -s "$REWRITTEN_LIST"; then + "$GIT_DIR"/hooks/post-rewrite rebase < "$REWRITTEN_LIST" + true # we don't care if this hook failed + fi && rm -rf "$DOTEST" && git gc --auto && warn "Successfully rebased and updated $HEADNAME." @@@ -571,7 -613,12 +613,12 @@@ skip_unnecessary_picks () esac echo "$command${sha1:+ }$sha1${rest:+ }$rest" >&$fd done <"$TODO" >"$TODO.new" 3>>"$DONE" && - mv -f "$TODO".new "$TODO" || + mv -f "$TODO".new "$TODO" && + case "$(peek_next_command)" in + squash|s|fixup|f) + record_in_rewritten "$ONTO" + ;; + esac || die "Could not skip unnecessary pick commands" } @@@ -685,6 -732,7 +732,7 @@@ first and then run 'git rebase --contin test -n "$amend" && git reset --soft $amend die "Could not commit staged changes." } + record_in_rewritten "$(cat "$DOTEST"/stopped-sha)" fi require_clean_work_tree diff --combined git-rebase.sh index fb4fef7b1d,3a26321faa..e0eb9568f3 --- a/git-rebase.sh +++ b/git-rebase.sh @@@ -79,14 -79,17 +79,15 @@@ continue_merge () then printf "Committed: %0${prec}d " $msgnum fi + echo "$cmt $(git rev-parse HEAD^0)" >> "$dotest/rewritten" else if test -z "$GIT_QUIET" then printf "Already applied: %0${prec}d " $msgnum fi fi - if test -z "$GIT_QUIET" - then - git rev-list --pretty=oneline -1 "$cmt" | sed -e 's/^[^ ]* //' - fi + test -z "$GIT_QUIET" && + GIT_PAGER='' git log --format=%s -1 "$cmt" prev_head=`git rev-parse HEAD^0` # save the resulting commit so we can read-tree on it later @@@ -151,6 -154,11 +152,11 @@@ move_to_original_branch () finish_rb_merge () { move_to_original_branch + git notes copy --for-rewrite=rebase < "$dotest"/rewritten + if test -x "$GIT_DIR"/hooks/post-rewrite && + test -s "$dotest"/rewritten; then + "$GIT_DIR"/hooks/post-rewrite rebase < "$dotest"/rewritten + fi rm -r "$dotest" say All done. } diff --combined notes.c index 07941b7235,0261e7898a..e425e19827 --- a/notes.c +++ b/notes.c @@@ -5,6 -5,8 +5,8 @@@ #include "utf8.h" #include "strbuf.h" #include "tree-walk.h" + #include "string-list.h" + #include "refs.h" /* * Use a non-balancing simple 16-tree structure with struct int_node as @@@ -68,6 -70,9 +70,9 @@@ struct non_note struct notes_tree default_notes_tree; + static struct string_list display_notes_refs; + static struct notes_tree **display_notes_trees; + static void load_subtree(struct notes_tree *t, struct leaf_node *subtree, struct int_node *node, unsigned int n); @@@ -624,8 -629,8 +629,8 @@@ static void write_tree_entry(struct str const char *path, unsigned int path_len, const unsigned char *sha1) { - strbuf_addf(buf, "%06o %.*s%c", mode, path_len, path, '\0'); - strbuf_add(buf, sha1, 20); + strbuf_addf(buf, "%o %.*s%c", mode, path_len, path, '\0'); + strbuf_add(buf, sha1, 20); } static void tree_write_stack_init_subtree(struct tree_write_stack *tws, @@@ -828,6 -833,83 +833,83 @@@ int combine_notes_ignore(unsigned char return 0; } + static int string_list_add_one_ref(const char *path, const unsigned char *sha1, + int flag, void *cb) + { + struct string_list *refs = cb; + if (!unsorted_string_list_has_string(refs, path)) + string_list_append(path, refs); + return 0; + } + + void string_list_add_refs_by_glob(struct string_list *list, const char *glob) + { + if (has_glob_specials(glob)) { + for_each_glob_ref(string_list_add_one_ref, glob, list); + } else { + unsigned char sha1[20]; + if (get_sha1(glob, sha1)) + warning("notes ref %s is invalid", glob); + if (!unsorted_string_list_has_string(list, glob)) + string_list_append(glob, list); + } + } + + void string_list_add_refs_from_colon_sep(struct string_list *list, + const char *globs) + { + struct strbuf globbuf = STRBUF_INIT; + struct strbuf **split; + int i; + + strbuf_addstr(&globbuf, globs); + split = strbuf_split(&globbuf, ':'); + + for (i = 0; split[i]; i++) { + if (!split[i]->len) + continue; + if (split[i]->buf[split[i]->len-1] == ':') + strbuf_setlen(split[i], split[i]->len-1); + string_list_add_refs_by_glob(list, split[i]->buf); + } + + strbuf_list_free(split); + strbuf_release(&globbuf); + } + + static int string_list_add_refs_from_list(struct string_list_item *item, + void *cb) + { + struct string_list *list = cb; + string_list_add_refs_by_glob(list, item->string); + return 0; + } + + static int notes_display_config(const char *k, const char *v, void *cb) + { + int *load_refs = cb; + + if (*load_refs && !strcmp(k, "notes.displayref")) { + if (!v) + config_error_nonbool(k); + string_list_add_refs_by_glob(&display_notes_refs, v); + } + + return 0; + } + + static const char *default_notes_ref(void) + { + const char *notes_ref = NULL; + if (!notes_ref) + notes_ref = getenv(GIT_NOTES_REF_ENVIRONMENT); + if (!notes_ref) + notes_ref = notes_ref_name; /* value of core.notesRef config */ + if (!notes_ref) + notes_ref = GIT_NOTES_DEFAULT_REF; + return notes_ref; + } + void init_notes(struct notes_tree *t, const char *notes_ref, combine_notes_fn combine_notes, int flags) { @@@ -840,11 -922,7 +922,7 @@@ assert(!t->initialized); if (!notes_ref) - notes_ref = getenv(GIT_NOTES_REF_ENVIRONMENT); - if (!notes_ref) - notes_ref = notes_ref_name; /* value of core.notesRef config */ - if (!notes_ref) - notes_ref = GIT_NOTES_DEFAULT_REF; + notes_ref = default_notes_ref(); if (!combine_notes) combine_notes = combine_notes_concatenate; @@@ -855,6 -933,7 +933,7 @@@ t->ref = notes_ref ? xstrdup(notes_ref) : NULL; t->combine_notes = combine_notes; t->initialized = 1; + t->dirty = 0; if (flags & NOTES_INIT_EMPTY || !notes_ref || read_ref(notes_ref, object_sha1)) @@@ -868,6 -947,63 +947,63 @@@ load_subtree(t, &root_tree, t->root, 0); } + struct load_notes_cb_data { + int counter; + struct notes_tree **trees; + }; + + static int load_one_display_note_ref(struct string_list_item *item, + void *cb_data) + { + struct load_notes_cb_data *c = cb_data; + struct notes_tree *t = xcalloc(1, sizeof(struct notes_tree)); + init_notes(t, item->string, combine_notes_ignore, 0); + c->trees[c->counter++] = t; + return 0; + } + + struct notes_tree **load_notes_trees(struct string_list *refs) + { + struct notes_tree **trees; + struct load_notes_cb_data cb_data; + trees = xmalloc((refs->nr+1) * sizeof(struct notes_tree *)); + cb_data.counter = 0; + cb_data.trees = trees; + for_each_string_list(load_one_display_note_ref, refs, &cb_data); + trees[cb_data.counter] = NULL; + return trees; + } + + void init_display_notes(struct display_notes_opt *opt) + { + char *display_ref_env; + int load_config_refs = 0; + display_notes_refs.strdup_strings = 1; + + assert(!display_notes_trees); + + if (!opt || !opt->suppress_default_notes) { + string_list_append(default_notes_ref(), &display_notes_refs); + display_ref_env = getenv(GIT_NOTES_DISPLAY_REF_ENVIRONMENT); + if (display_ref_env) { + string_list_add_refs_from_colon_sep(&display_notes_refs, + display_ref_env); + load_config_refs = 0; + } else + load_config_refs = 1; + } + + git_config(notes_display_config, &load_config_refs); + + if (opt && opt->extra_notes_refs) + for_each_string_list(string_list_add_refs_from_list, + opt->extra_notes_refs, + &display_notes_refs); + + display_notes_trees = load_notes_trees(&display_notes_refs); + string_list_clear(&display_notes_refs, 0); + } + void add_note(struct notes_tree *t, const unsigned char *object_sha1, const unsigned char *note_sha1, combine_notes_fn combine_notes) { @@@ -876,6 -1012,7 +1012,7 @@@ if (!t) t = &default_notes_tree; assert(t->initialized); + t->dirty = 1; if (!combine_notes) combine_notes = t->combine_notes; l = (struct leaf_node *) xmalloc(sizeof(struct leaf_node)); @@@ -891,9 -1028,10 +1028,10 @@@ void remove_note(struct notes_tree *t, if (!t) t = &default_notes_tree; assert(t->initialized); + t->dirty = 1; hashcpy(l.key_sha1, object_sha1); hashclr(l.val_sha1); - return note_tree_remove(t, t->root, 0, &l); + note_tree_remove(t, t->root, 0, &l); } const unsigned char *get_note(struct notes_tree *t, @@@ -1016,8 -1154,18 +1154,18 @@@ void format_note(struct notes_tree *t, if (msglen && msg[msglen - 1] == '\n') msglen--; - if (flags & NOTES_SHOW_HEADER) - strbuf_addstr(sb, "\nNotes:\n"); + if (flags & NOTES_SHOW_HEADER) { + const char *ref = t->ref; + if (!ref || !strcmp(ref, GIT_NOTES_DEFAULT_REF)) { + strbuf_addstr(sb, "\nNotes:\n"); + } else { + if (!prefixcmp(ref, "refs/")) + ref += 5; + if (!prefixcmp(ref, "notes/")) + ref += 6; + strbuf_addf(sb, "\nNotes (%s):\n", ref); + } + } for (msg_p = msg; msg_p < msg + msglen; msg_p += linelen + 1) { linelen = strchrnul(msg_p, '\n') - msg_p; @@@ -1030,3 -1178,31 +1178,31 @@@ free(msg); } + + void format_display_notes(const unsigned char *object_sha1, + struct strbuf *sb, const char *output_encoding, int flags) + { + int i; + assert(display_notes_trees); + for (i = 0; display_notes_trees[i]; i++) + format_note(display_notes_trees[i], object_sha1, sb, + output_encoding, flags); + } + + int copy_note(struct notes_tree *t, + const unsigned char *from_obj, const unsigned char *to_obj, + int force, combine_notes_fn combine_fn) + { + const unsigned char *note = get_note(t, from_obj); + const unsigned char *existing_note = get_note(t, to_obj); + + if (!force && existing_note) + return 1; + + if (note) + add_note(t, to_obj, note, combine_fn); + else if (existing_note) + add_note(t, to_obj, null_sha1, combine_fn); + + return 0; + } diff --combined refs.c index a7518b6f0d,5a860c41eb..0f24c8d5d9 --- a/refs.c +++ b/refs.c @@@ -6,7 -6,6 +6,7 @@@ /* ISSYMREF=01 and ISPACKED=02 are public interfaces */ #define REF_KNOWS_PEELED 04 +#define REF_BROKEN 010 struct ref_list { struct ref_list *next; @@@ -276,10 -275,8 +276,10 @@@ static struct ref_list *get_ref_dir(con list = get_ref_dir(ref, list); continue; } - if (!resolve_ref(ref, sha1, 1, &flag)) + if (!resolve_ref(ref, sha1, 1, &flag)) { hashclr(sha1); + flag |= REF_BROKEN; + } list = add_ref(ref, sha1, flag, list, NULL); } free(ref); @@@ -542,10 -539,10 +542,10 @@@ static int do_one_ref(const char *base { if (strncmp(base, entry->name, trim)) return 0; - /* Is this a "negative ref" that represents a deleted ref? */ - if (is_null_sha1(entry->sha1)) - return 0; + if (!(flags & DO_FOR_EACH_INCLUDE_BROKEN)) { + if (entry->flag & REF_BROKEN) + return 0; /* ignore dangling symref */ if (!has_sha1_file(entry->sha1)) { error("%s does not point to a valid object!", entry->name); return 0; @@@ -698,7 -695,6 +698,6 @@@ int for_each_glob_ref_in(each_ref_fn fn { struct strbuf real_pattern = STRBUF_INIT; struct ref_filter filter; - const char *has_glob_specials; int ret; if (!prefix && prefixcmp(pattern, "refs/")) @@@ -707,8 -703,7 +706,7 @@@ strbuf_addstr(&real_pattern, prefix); strbuf_addstr(&real_pattern, pattern); - has_glob_specials = strpbrk(pattern, "?*["); - if (!has_glob_specials) { + if (!has_glob_specials(pattern)) { /* Append implied '/' '*' if not present. */ if (real_pattern.buf[real_pattern.len - 1] != '/') strbuf_addch(&real_pattern, '/'); @@@ -1577,7 -1572,7 +1575,7 @@@ int for_each_recent_reflog_ent(const ch { const char *logfile; FILE *logfp; - char buf[1024]; + struct strbuf sb = STRBUF_INIT; int ret = 0; logfile = git_path("logs/%s", ref); @@@ -1590,24 -1585,24 +1588,24 @@@ if (fstat(fileno(logfp), &statbuf) || statbuf.st_size < ofs || fseek(logfp, -ofs, SEEK_END) || - fgets(buf, sizeof(buf), logfp)) { + strbuf_getwholeline(&sb, logfp, '\n')) { fclose(logfp); + strbuf_release(&sb); return -1; } } - while (fgets(buf, sizeof(buf), logfp)) { + while (!strbuf_getwholeline(&sb, logfp, '\n')) { unsigned char osha1[20], nsha1[20]; char *email_end, *message; unsigned long timestamp; - int len, tz; + int tz; /* old SP new SP name SP time TAB msg LF */ - len = strlen(buf); - if (len < 83 || buf[len-1] != '\n' || - get_sha1_hex(buf, osha1) || buf[40] != ' ' || - get_sha1_hex(buf + 41, nsha1) || buf[81] != ' ' || - !(email_end = strchr(buf + 82, '>')) || + if (sb.len < 83 || sb.buf[sb.len - 1] != '\n' || + get_sha1_hex(sb.buf, osha1) || sb.buf[40] != ' ' || + get_sha1_hex(sb.buf + 41, nsha1) || sb.buf[81] != ' ' || + !(email_end = strchr(sb.buf + 82, '>')) || email_end[1] != ' ' || !(timestamp = strtoul(email_end + 2, &message, 10)) || !message || message[0] != ' ' || @@@ -1621,13 -1616,11 +1619,13 @@@ message += 6; else message += 7; - ret = fn(osha1, nsha1, buf+82, timestamp, tz, message, cb_data); + ret = fn(osha1, nsha1, sb.buf + 82, timestamp, tz, message, + cb_data); if (ret) break; } fclose(logfp); + strbuf_release(&sb); return ret; } diff --combined revision.c index 0471cd3f7e,1c514d120b..f4b8b38315 --- a/revision.c +++ b/revision.c @@@ -12,6 -12,7 +12,7 @@@ #include "patch-ids.h" #include "decorate.h" #include "log-tree.h" + #include "string-list.h" volatile show_early_output_fn_t show_early_output; @@@ -134,20 -135,10 +135,20 @@@ static void add_pending_object_with_mod { if (revs->no_walk && (obj->flags & UNINTERESTING)) revs->no_walk = 0; - if (revs->reflog_info && obj->type == OBJ_COMMIT && - add_reflog_for_walk(revs->reflog_info, - (struct commit *)obj, name)) - return; + if (revs->reflog_info && obj->type == OBJ_COMMIT) { + struct strbuf buf = STRBUF_INIT; + int len = interpret_branch_name(name, &buf); + int st; + + if (0 < len && name[len] && buf.len) + strbuf_addstr(&buf, name + len); + st = add_reflog_for_walk(revs->reflog_info, + (struct commit *)obj, + buf.buf[0] ? buf.buf: name); + strbuf_release(&buf); + if (st) + return; + } add_object_array_with_mode(obj, name, &revs->pending, mode); } @@@ -278,7 -269,7 +279,7 @@@ static int tree_difference = REV_TREE_S static void file_add_remove(struct diff_options *options, int addremove, unsigned mode, const unsigned char *sha1, - const char *fullpath) + const char *fullpath, unsigned dirty_submodule) { int diff = addremove == '+' ? REV_TREE_NEW : REV_TREE_OLD; @@@ -291,8 -282,7 +292,8 @@@ static void file_change(struct diff_opt unsigned old_mode, unsigned new_mode, const unsigned char *old_sha1, const unsigned char *new_sha1, - const char *fullpath) + const char *fullpath, + unsigned old_dirty_submodule, unsigned new_dirty_submodule) { tree_difference = REV_TREE_DIFFERENT; DIFF_OPT_SET(options, HAS_CHANGES); @@@ -547,9 -537,6 +548,9 @@@ static void cherry_pick_list(struct com right_count++; } + if (!left_count || !right_count) + return; + left_first = left_count < right_count; init_patch_ids(&ids); if (revs->diffopt.nr_paths) { @@@ -826,7 -813,6 +827,7 @@@ void init_revisions(struct rev_info *re revs->grep_filter.status_only = 1; revs->grep_filter.pattern_tail = &(revs->grep_filter.pattern_list); + revs->grep_filter.header_tail = &(revs->grep_filter.header_list); revs->grep_filter.regflags = REG_NEWLINE; diff_setup(&revs->diffopt); @@@ -1191,9 -1177,29 +1192,29 @@@ static int handle_revision_opt(struct r } else if (!strcmp(arg, "--show-notes")) { revs->show_notes = 1; revs->show_notes_given = 1; + } else if (!prefixcmp(arg, "--show-notes=")) { + struct strbuf buf = STRBUF_INIT; + revs->show_notes = 1; + revs->show_notes_given = 1; + if (!revs->notes_opt.extra_notes_refs) + revs->notes_opt.extra_notes_refs = xcalloc(1, sizeof(struct string_list)); + if (!prefixcmp(arg+13, "refs/")) + /* happy */; + else if (!prefixcmp(arg+13, "notes/")) + strbuf_addstr(&buf, "refs/"); + else + strbuf_addstr(&buf, "refs/notes/"); + strbuf_addstr(&buf, arg+13); + string_list_append(strbuf_detach(&buf, NULL), + revs->notes_opt.extra_notes_refs); } else if (!strcmp(arg, "--no-notes")) { revs->show_notes = 0; revs->show_notes_given = 1; + } else if (!strcmp(arg, "--standard-notes")) { + revs->show_notes_given = 1; + revs->notes_opt.suppress_default_notes = 0; + } else if (!strcmp(arg, "--no-standard-notes")) { + revs->notes_opt.suppress_default_notes = 1; } else if (!strcmp(arg, "--oneline")) { revs->verbose_header = 1; get_commit_format("oneline", revs); @@@ -1332,9 -1338,9 +1353,9 @@@ static void append_prune_data(const cha * Returns the number of arguments left that weren't recognized * (which are also moved to the head of the argument list) */ -int setup_revisions(int argc, const char **argv, struct rev_info *revs, const char *def) +int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct setup_revision_opt *opt) { - int i, flags, left, seen_dashdash, read_from_stdin; + int i, flags, left, seen_dashdash, read_from_stdin, got_rev_arg = 0; const char **prune_data = NULL; /* First, search for "--" */ @@@ -1460,20 -1466,16 +1481,20 @@@ append_prune_data(&prune_data, argv + i); break; } + else + got_rev_arg = 1; } if (prune_data) revs->prune_data = get_pathspec(revs->prefix, prune_data); if (revs->def == NULL) - revs->def = def; + revs->def = opt ? opt->def : NULL; + if (opt && opt->tweak) + opt->tweak(revs, opt); if (revs->show_merge) prepare_show_merge(revs); - if (revs->def && !revs->pending.nr) { + if (revs->def && !revs->pending.nr && !got_rev_arg) { unsigned char sha1[20]; struct object *object; unsigned mode; @@@ -1504,8 -1506,11 +1525,8 @@@ if (!revs->full_diff) diff_tree_setup_paths(revs->prune_data, &revs->diffopt); } - if (revs->combine_merges) { + if (revs->combine_merges) revs->ignore_merges = 0; - if (revs->dense_combined_merges && !revs->diffopt.output_format) - revs->diffopt.output_format = DIFF_FORMAT_PATCH; - } revs->diffopt.abbrev = revs->abbrev; if (diff_setup_done(&revs->diffopt) < 0) die("diff_setup_done failed"); @@@ -1806,7 -1811,7 +1827,7 @@@ static int rewrite_parents(struct rev_i static int commit_match(struct commit *commit, struct rev_info *opt) { - if (!opt->grep_filter.pattern_list) + if (!opt->grep_filter.pattern_list && !opt->grep_filter.header_list) return 1; return grep_buffer(&opt->grep_filter, NULL, /* we say nothing, not even filename */ diff --combined revision.h index ceae4cae74,580f6eccee..568f1c98de --- a/revision.h +++ b/revision.h @@@ -3,6 -3,7 +3,7 @@@ #include "parse-options.h" #include "grep.h" + #include "notes.h" #define SEEN (1u<<0) #define UNINTERESTING (1u<<1) @@@ -20,6 -21,7 +21,7 @@@ struct rev_info; struct log_info; + struct string_list; struct rev_info { /* Starting list */ @@@ -126,6 -128,9 +128,9 @@@ struct reflog_walk_info *reflog_info; struct decoration children; struct decoration merge_simplification; + + /* notes-specific options: which refs to show */ + struct display_notes_opt notes_opt; }; #define REV_TREE_SAME 0 @@@ -137,13 -142,8 +142,13 @@@ typedef void (*show_early_output_fn_t)(struct rev_info *, struct commit_list *); extern volatile show_early_output_fn_t show_early_output; +struct setup_revision_opt { + const char *def; + void (*tweak)(struct rev_info *, struct setup_revision_opt *); +}; + extern void init_revisions(struct rev_info *revs, const char *prefix); -extern int setup_revisions(int argc, const char **argv, struct rev_info *revs, const char *def); +extern int setup_revisions(int argc, const char **argv, struct rev_info *revs, struct setup_revision_opt *); extern void parse_revision_opt(struct rev_info *revs, struct parse_opt_ctx_t *ctx, const struct option *options, const char * const usagestr[]); diff --combined t/t3301-notes.sh index 37b96871c5,aeec90a8e9..a4a0b1d6c5 --- a/t/t3301-notes.sh +++ b/t/t3301-notes.sh @@@ -8,7 -8,6 +8,7 @@@ test_description='Test commit notes . ./test-lib.sh cat > fake_editor.sh << \EOF +#!/bin/sh echo "$MSG" > "$1" echo "$MSG" >& 2 EOF @@@ -416,7 -415,7 +416,7 @@@ Date: Thu Apr 7 15:18:13 2005 -070 6th - Notes: + Notes (other): other note EOF @@@ -449,7 -448,139 +449,139 @@@ test_expect_success 'Do not show note w test_cmp expect-not-other output ' + cat > expect-both << EOF + commit 387a89921c73d7ed72cd94d179c1c7048ca47756 + Author: A U Thor + Date: Thu Apr 7 15:18:13 2005 -0700 + + 6th + + Notes: + order test + + Notes (other): + other note + + commit bd1753200303d0a0344be813e504253b3d98e74d + Author: A U Thor + Date: Thu Apr 7 15:17:13 2005 -0700 + + 5th + + Notes: + replacement for deleted note + EOF + + test_expect_success 'Show all notes when notes.displayRef=refs/notes/*' ' + GIT_NOTES_REF=refs/notes/commits git notes add \ + -m"replacement for deleted note" HEAD^ && + GIT_NOTES_REF=refs/notes/commits git notes add -m"order test" && + git config --unset core.notesRef && + git config notes.displayRef "refs/notes/*" && + git log -2 > output && + test_cmp expect-both output + ' + + test_expect_success 'core.notesRef is implicitly in notes.displayRef' ' + git config core.notesRef refs/notes/commits && + git config notes.displayRef refs/notes/other && + git log -2 > output && + test_cmp expect-both output + ' + + test_expect_success 'notes.displayRef can be given more than once' ' + git config --unset core.notesRef && + git config notes.displayRef refs/notes/commits && + git config --add notes.displayRef refs/notes/other && + git log -2 > output && + test_cmp expect-both output + ' + + cat > expect-both-reversed << EOF + commit 387a89921c73d7ed72cd94d179c1c7048ca47756 + Author: A U Thor + Date: Thu Apr 7 15:18:13 2005 -0700 + + 6th + + Notes (other): + other note + + Notes: + order test + EOF + + test_expect_success 'notes.displayRef respects order' ' + git config core.notesRef refs/notes/other && + git config --unset-all notes.displayRef && + git config notes.displayRef refs/notes/commits && + git log -1 > output && + test_cmp expect-both-reversed output + ' + + test_expect_success 'GIT_NOTES_DISPLAY_REF works' ' + git config --unset-all core.notesRef && + git config --unset-all notes.displayRef && + GIT_NOTES_DISPLAY_REF=refs/notes/commits:refs/notes/other \ + git log -2 > output && + test_cmp expect-both output + ' + + cat > expect-none << EOF + commit 387a89921c73d7ed72cd94d179c1c7048ca47756 + Author: A U Thor + Date: Thu Apr 7 15:18:13 2005 -0700 + + 6th + + commit bd1753200303d0a0344be813e504253b3d98e74d + Author: A U Thor + Date: Thu Apr 7 15:17:13 2005 -0700 + + 5th + EOF + + test_expect_success 'GIT_NOTES_DISPLAY_REF overrides config' ' + git config notes.displayRef "refs/notes/*" && + GIT_NOTES_REF= GIT_NOTES_DISPLAY_REF= git log -2 > output && + test_cmp expect-none output + ' + + test_expect_success '--show-notes=* adds to GIT_NOTES_DISPLAY_REF' ' + GIT_NOTES_REF= GIT_NOTES_DISPLAY_REF= git log --show-notes=* -2 > output && + test_cmp expect-both output + ' + + cat > expect-commits << EOF + commit 387a89921c73d7ed72cd94d179c1c7048ca47756 + Author: A U Thor + Date: Thu Apr 7 15:18:13 2005 -0700 + + 6th + + Notes: + order test + EOF + + test_expect_success '--no-standard-notes' ' + git log --no-standard-notes --show-notes=commits -1 > output && + test_cmp expect-commits output + ' + + test_expect_success '--standard-notes' ' + git log --no-standard-notes --show-notes=commits \ + --standard-notes -2 > output && + test_cmp expect-both output + ' + + test_expect_success '--show-notes=ref accumulates' ' + git log --show-notes=other --show-notes=commits \ + --no-standard-notes -1 > output && + test_cmp expect-both-reversed output + ' + test_expect_success 'Allow notes on non-commits (trees, blobs, tags)' ' + git config core.notesRef refs/notes/other && echo "Note on a tree" > expect git notes add -m "Note on a tree" HEAD: && git notes show HEAD: > actual && @@@ -473,7 -604,7 +605,7 @@@ Date: Thu Apr 7 15:19:13 2005 -070 7th - Notes: + Notes (other): other note EOF @@@ -504,7 -635,7 +636,7 @@@ Date: Thu Apr 7 15:21:13 2005 -070 9th - Notes: + Notes (other): yet another note EOF @@@ -534,7 -665,7 +666,7 @@@ Date: Thu Apr 7 15:21:13 2005 -070 9th - Notes: + Notes (other): yet another note $whitespace yet another note @@@ -553,7 -684,7 +685,7 @@@ Date: Thu Apr 7 15:22:13 2005 -070 10th - Notes: + Notes (other): other note EOF @@@ -570,7 -701,7 +702,7 @@@ Date: Thu Apr 7 15:22:13 2005 -070 10th - Notes: + Notes (other): other note $whitespace yet another note @@@ -589,7 -720,7 +721,7 @@@ Date: Thu Apr 7 15:23:13 2005 -070 11th - Notes: + Notes (other): other note $whitespace yet another note @@@ -620,7 -751,7 +752,7 @@@ Date: Thu Apr 7 15:23:13 2005 -070 11th - Notes: + Notes (other): yet another note $whitespace yet another note @@@ -645,4 -776,233 +777,233 @@@ test_expect_success 'cannot copy note f test_must_fail git notes copy HEAD^ HEAD ' + cat > expect << EOF + commit e5d4fb5698d564ab8c73551538ecaf2b0c666185 + Author: A U Thor + Date: Thu Apr 7 15:25:13 2005 -0700 + + 13th + + Notes (other): + yet another note + $whitespace + yet another note + + commit 7038787dfe22a14c3867ce816dbba39845359719 + Author: A U Thor + Date: Thu Apr 7 15:24:13 2005 -0700 + + 12th + + Notes (other): + other note + $whitespace + yet another note + EOF + + test_expect_success 'git notes copy --stdin' ' + (echo $(git rev-parse HEAD~3) $(git rev-parse HEAD^); \ + echo $(git rev-parse HEAD~2) $(git rev-parse HEAD)) | + git notes copy --stdin && + git log -2 > output && + test_cmp expect output && + test "$(git notes list HEAD)" = "$(git notes list HEAD~2)" && + test "$(git notes list HEAD^)" = "$(git notes list HEAD~3)" + ' + + cat > expect << EOF + commit 37a0d4cba38afef96ba54a3ea567e6dac575700b + Author: A U Thor + Date: Thu Apr 7 15:27:13 2005 -0700 + + 15th + + commit be28d8b4d9951ad940d229ee3b0b9ee3b1ec273d + Author: A U Thor + Date: Thu Apr 7 15:26:13 2005 -0700 + + 14th + EOF + + test_expect_success 'git notes copy --for-rewrite (unconfigured)' ' + test_commit 14th && + test_commit 15th && + (echo $(git rev-parse HEAD~3) $(git rev-parse HEAD^); \ + echo $(git rev-parse HEAD~2) $(git rev-parse HEAD)) | + git notes copy --for-rewrite=foo && + git log -2 > output && + test_cmp expect output + ' + + cat > expect << EOF + commit 37a0d4cba38afef96ba54a3ea567e6dac575700b + Author: A U Thor + Date: Thu Apr 7 15:27:13 2005 -0700 + + 15th + + Notes (other): + yet another note + $whitespace + yet another note + + commit be28d8b4d9951ad940d229ee3b0b9ee3b1ec273d + Author: A U Thor + Date: Thu Apr 7 15:26:13 2005 -0700 + + 14th + + Notes (other): + other note + $whitespace + yet another note + EOF + + test_expect_success 'git notes copy --for-rewrite (enabled)' ' + git config notes.rewriteMode overwrite && + git config notes.rewriteRef "refs/notes/*" && + (echo $(git rev-parse HEAD~3) $(git rev-parse HEAD^); \ + echo $(git rev-parse HEAD~2) $(git rev-parse HEAD)) | + git notes copy --for-rewrite=foo && + git log -2 > output && + test_cmp expect output + ' + + test_expect_success 'git notes copy --for-rewrite (disabled)' ' + git config notes.rewrite.bar false && + echo $(git rev-parse HEAD~3) $(git rev-parse HEAD) | + git notes copy --for-rewrite=bar && + git log -2 > output && + test_cmp expect output + ' + + cat > expect << EOF + commit 37a0d4cba38afef96ba54a3ea567e6dac575700b + Author: A U Thor + Date: Thu Apr 7 15:27:13 2005 -0700 + + 15th + + Notes (other): + a fresh note + EOF + + test_expect_success 'git notes copy --for-rewrite (overwrite)' ' + git notes add -f -m"a fresh note" HEAD^ && + echo $(git rev-parse HEAD^) $(git rev-parse HEAD) | + git notes copy --for-rewrite=foo && + git log -1 > output && + test_cmp expect output + ' + + test_expect_success 'git notes copy --for-rewrite (ignore)' ' + git config notes.rewriteMode ignore && + echo $(git rev-parse HEAD^) $(git rev-parse HEAD) | + git notes copy --for-rewrite=foo && + git log -1 > output && + test_cmp expect output + ' + + cat > expect << EOF + commit 37a0d4cba38afef96ba54a3ea567e6dac575700b + Author: A U Thor + Date: Thu Apr 7 15:27:13 2005 -0700 + + 15th + + Notes (other): + a fresh note + another fresh note + EOF + + test_expect_success 'git notes copy --for-rewrite (append)' ' + git notes add -f -m"another fresh note" HEAD^ && + git config notes.rewriteMode concatenate && + echo $(git rev-parse HEAD^) $(git rev-parse HEAD) | + git notes copy --for-rewrite=foo && + git log -1 > output && + test_cmp expect output + ' + + cat > expect << EOF + commit 37a0d4cba38afef96ba54a3ea567e6dac575700b + Author: A U Thor + Date: Thu Apr 7 15:27:13 2005 -0700 + + 15th + + Notes (other): + a fresh note + another fresh note + append 1 + append 2 + EOF + + test_expect_success 'git notes copy --for-rewrite (append two to one)' ' + git notes add -f -m"append 1" HEAD^ && + git notes add -f -m"append 2" HEAD^^ && + (echo $(git rev-parse HEAD^) $(git rev-parse HEAD); + echo $(git rev-parse HEAD^^) $(git rev-parse HEAD)) | + git notes copy --for-rewrite=foo && + git log -1 > output && + test_cmp expect output + ' + + test_expect_success 'git notes copy --for-rewrite (append empty)' ' + git notes remove HEAD^ && + echo $(git rev-parse HEAD^) $(git rev-parse HEAD) | + git notes copy --for-rewrite=foo && + git log -1 > output && + test_cmp expect output + ' + + cat > expect << EOF + commit 37a0d4cba38afef96ba54a3ea567e6dac575700b + Author: A U Thor + Date: Thu Apr 7 15:27:13 2005 -0700 + + 15th + + Notes (other): + replacement note 1 + EOF + + test_expect_success 'GIT_NOTES_REWRITE_MODE works' ' + git notes add -f -m"replacement note 1" HEAD^ && + echo $(git rev-parse HEAD^) $(git rev-parse HEAD) | + GIT_NOTES_REWRITE_MODE=overwrite git notes copy --for-rewrite=foo && + git log -1 > output && + test_cmp expect output + ' + + cat > expect << EOF + commit 37a0d4cba38afef96ba54a3ea567e6dac575700b + Author: A U Thor + Date: Thu Apr 7 15:27:13 2005 -0700 + + 15th + + Notes (other): + replacement note 2 + EOF + + test_expect_success 'GIT_NOTES_REWRITE_REF works' ' + git config notes.rewriteMode overwrite && + git notes add -f -m"replacement note 2" HEAD^ && + git config --unset-all notes.rewriteRef && + echo $(git rev-parse HEAD^) $(git rev-parse HEAD) | + GIT_NOTES_REWRITE_REF=refs/notes/commits:refs/notes/other \ + git notes copy --for-rewrite=foo && + git log -1 > output && + test_cmp expect output + ' + + test_expect_success 'GIT_NOTES_REWRITE_REF overrides config' ' + git config notes.rewriteRef refs/notes/other && + git notes add -f -m"replacement note 3" HEAD^ && + echo $(git rev-parse HEAD^) $(git rev-parse HEAD) | + GIT_NOTES_REWRITE_REF= git notes copy --for-rewrite=foo && + git log -1 > output && + test_cmp expect output + ' test_done diff --combined t/t3400-rebase.sh index 4314ad2d66,cca284004d..dbf7dfba9b --- a/t/t3400-rebase.sh +++ b/t/t3400-rebase.sh @@@ -134,6 -134,10 +134,6 @@@ test_expect_success 'rebase -q is quiet test ! -s output.out ' -q_to_cr () { - tr Q '\015' -} - test_expect_success 'Rebase a commit that sprinkles CRs in' ' ( echo "One" @@@ -151,4 -155,21 +151,21 @@@ git diff --exit-code file-with-cr:CR HEAD:CR ' + test_expect_success 'rebase can copy notes' ' + git config notes.rewrite.rebase true && + git config notes.rewriteRef "refs/notes/*" && + test_commit n1 && + test_commit n2 && + test_commit n3 && + git notes add -m"a note" n3 && + git rebase --onto n1 n2 && + test "a note" = "$(git notes show HEAD)" + ' + + test_expect_success 'rebase -m can copy notes' ' + git reset --hard n3 && + git rebase -m --onto n1 n2 && + test "a note" = "$(git notes show HEAD)" + ' + test_done diff --combined t/test-lib.sh index a0e396a952,806b83292f..c582964b0d --- a/t/test-lib.sh +++ b/t/test-lib.sh @@@ -54,6 -54,10 +54,10 @@@ unset GIT_OBJECT_DIRECTOR unset GIT_CEILING_DIRECTORIES unset SHA1_FILE_DIRECTORIES unset SHA1_FILE_DIRECTORY + unset GIT_NOTES_REF + unset GIT_NOTES_DISPLAY_REF + unset GIT_NOTES_REWRITE_REF + unset GIT_NOTES_REWRITE_MODE GIT_MERGE_VERBOSITY=5 export GIT_MERGE_VERBOSITY export GIT_AUTHOR_EMAIL GIT_AUTHOR_NAME @@@ -65,8 -69,6 +69,8 @@@ GIT_TEST_CMP=${GIT_TEST_CMP:-diff -u # CDPATH into the environment unset CDPATH +unset GREP_OPTIONS + case $(echo $GIT_TRACE |tr "[A-Z]" "[a-z]") in 1|2|true) echo "* warning: Some tests will not work if GIT_TRACE" \ @@@ -76,12 -78,6 +80,12 @@@ ;; esac +# Convenience +# +# A regexp to match 5 and 40 hexdigits +_x05='[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]' +_x40="$_x05$_x05$_x05$_x05$_x05$_x05$_x05$_x05" + # Each test should start with something like this, after copyright notices: # # test_description='Description of this test... @@@ -232,22 -228,6 +236,22 @@@ test_decode_color () -e 's/.\[m//g' } +q_to_nul () { + perl -pe 'y/Q/\000/' +} + +q_to_cr () { + tr Q '\015' +} + +append_cr () { + sed -e 's/$/Q/' | tr Q '\015' +} + +remove_cr () { + tr '\015' Q | sed -e 's/Q$//' +} + test_tick () { if test -z "${test_tick+set}" then