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::
+
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
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 (<refname>):"
+ 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.
+
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'
`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.<slot>. It is passed using
- the environment variables 'GREP_COLOR' and 'GREP_COLORS' when
- calling an external 'grep'.
+color.grep.<slot>::
+ Use customized color for grep colorization. `<slot>` 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.<slot>.
color.interactive::
When set to `always`, always use colors for interactive prompts
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.
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].
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.<command>::
+ When rewriting commits with <command> (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.<command>" 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.
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.<cmd>::
Allows turning on or off pagination of the output of a
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
[verse]
'git notes' [list [<object>]]
'git notes' add [-f] [-F <file> | -m <msg> | (-c | -C) <object>] [<object>]
- 'git notes' copy [-f] <from-object> <to-object>
+ 'git notes' copy [-f] ( --stdin | <from-object> <to-object> )
'git notes' append [-F <file> | -m <msg> | (-c | -C) <object>] [<object>]
'git notes' edit [<object>]
'git notes' show [<object>]
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 (<refname>):" (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.<command>" in
+ linkgit:git-config[1] for a way of carrying your notes across commands
+ that rewrite commits.
SUBCOMMANDS
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 <from-object>) <to-object>`
+ +
+ In `\--stdin` mode, take lines in the format
+ +
+ ----------
+ <from-object> SP <to-object> [ SP <rest> ] LF
+ ----------
+ +
+ on standard input, and copy the notes from each <from-object> to its
+ corresponding <to-object>. (The optional `<rest>` 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).
Like '-C', but with '-c' the editor is invoked, so that
the user can further edit the note message.
+ --ref <ref>::
+ Manipulate the notes tree in <ref>. 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 <johannes.schindelin@gmx.de> and
--- /dev/null
+/*
+ * Builtin "git commit"
+ *
+ * Copyright (c) 2007 Kristian Høgsberg <krh@redhat.com>
+ * Based on git-commit.sh by Junio C Hamano and Linus Torvalds
+ */
+
+#include "cache.h"
+#include "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] [--] <filepattern>...",
+ NULL
+};
+
+static const char * const builtin_status_usage[] = {
+ "git status [options] [--] <filepattern>...",
+ 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 <you@example.com>'\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;
+}
--- /dev/null
+/*
+ * 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 [<options>] [<since>..<until>] [[--] <path>...]\n"
+ " or: git show [options] <object>...";
+
+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] [<since> | <revision range>]",
+ NULL
+};
+
+static int keep_subject = 0;
+
+static int keep_callback(const struct option *opt, const char *arg, int unset)
+{
+ ((struct rev_info *)opt->value)->total = -1;
+ keep_subject = 1;
+ return 0;
+}
+
+static int subject_prefix = 0;
+
+static int subject_prefix_callback(const struct option *opt, const char *arg,
+ int unset)
+{
+ subject_prefix = 1;
+ ((struct rev_info *)opt->value)->subject_prefix = arg;
+ return 0;
+}
+
+static int 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 <sfx> instead of '.patch'"),
+ OPT_INTEGER(0, "start-number", &start_number,
+ "start numbering patches at <n> instead of 1"),
+ { OPTION_CALLBACK, 0, "subject-prefix", &rev, "prefix",
+ "Use [<prefix>] instead of [PATCH]",
+ PARSE_OPT_NONEG, subject_prefix_callback },
+ { OPTION_CALLBACK, 'o', "output-directory", &output_directory,
+ "dir", "store resulting files in <dir>",
+ 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 <message-id>"),
+ { 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 <reply-to>.
+ *
+ * With --cover-letter, make every
+ * mail but the cover letter a reply
+ * to the cover letter. The cover
+ * letter is a reply to the
+ * --in-reply-to, if specified.
+ */
+ if (thread == THREAD_SHALLOW
+ && rev.ref_message_ids->nr > 0
+ && (!cover_letter || rev.nr > 1))
+ free(rev.message_id);
+ else
+ string_list_append(rev.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] [<upstream> [<head> [<limit>]]]";
+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 <upstream> 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;
+}
--- /dev/null
- remove = 0, prune = 0, force = 0;
+/*
+ * Builtin "git notes"
+ *
+ * Copyright (c) 2010 Johan Herland <johan@herland.net>
+ *
+ * Based on git-notes.sh by Johannes Schindelin,
+ * and builtin-tag.c by Kristian Høgsberg and Carlos Rica.
+ */
+
+#include "cache.h"
+#include "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 [<object>]]",
+ "git notes add [-f] [-m <msg> | -F <file> | (-c | -C) <object>] [<object>]",
+ "git notes copy [-f] <from-object> <to-object>",
+ "git notes append [-m <msg> | -F <file> | (-c | -C) <object>] [<object>]",
+ "git notes edit [<object>]",
+ "git notes show [<object>]",
+ "git notes remove [<object>]",
+ "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, 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 <notes_ref>"),
++ OPT_STRING(0, "for-rewrite", &rewrite_cmd, "command",
++ "load rewriting config for <command> (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;
+}
/* Only remove in work directory, not index */
#define CE_WT_REMOVE (0x400000)
+#define CE_UNPACKED (0x1000000)
+
/*
* Extended on-disk flags
*/
#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);
BRANCH_TRACK_REMOTE,
BRANCH_TRACK_ALWAYS,
BRANCH_TRACK_EXPLICIT,
+ BRANCH_TRACK_OVERRIDE,
};
enum rebase_setup_type {
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!!
*
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 *);
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);
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;
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);
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
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
# 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
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
;;
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
scissors=t ;;
--no-scissors)
scissors=f ;;
- -r|--resolved)
+ -r|--resolved|--continue)
resolved=t ;;
--skip)
skip=t ;;
allow_rerere_autoupdate="$1" ;;
-q|--quiet)
GIT_QUIET=t ;;
+ --keep-cr)
+ keepcr=t ;;
+ --no-keep-cr)
+ keepcr=f ;;
--)
shift; break ;;
*)
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"
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 ;;
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") &&
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"
[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
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
go_next
done
-git gc --auto
-
+ 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
+
rm -fr "$dotest"
+git gc --auto
# 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=
}
die_with_patch () {
+ echo "$1" > "$DOTEST"/stopped-sha
make_patch "$1"
git rerere
die "$2"
# 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 () {
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 "$@" ||
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
echo "# The first commit's message is:"
echo
cat "$FIXUP_MSG"
- } >$SQUASH_MSG
+ } >"$SQUASH_MSG"
fi
case $1 in
squash)
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
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"
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
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
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"
rm -f "$SQUASH_MSG" "$FIXUP_MSG"
;;
esac
+ record_in_rewritten $sha1
;;
*)
warn "Unknown command: $command $sha1 $rest"
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."
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"
}
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
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
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.
}
#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
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);
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,
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)
{
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;
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))
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)
{
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));
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,
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;
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;
+ }
/* ISSYMREF=01 and ISPACKED=02 are public interfaces */
#define REF_KNOWS_PEELED 04
+#define REF_BROKEN 010
struct ref_list {
struct ref_list *next;
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);
{
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;
{
struct strbuf real_pattern = STRBUF_INIT;
struct ref_filter filter;
- const char *has_glob_specials;
int ret;
if (!prefix && prefixcmp(pattern, "refs/"))
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, '/');
{
const char *logfile;
FILE *logfp;
- char buf[1024];
+ struct strbuf sb = STRBUF_INIT;
int ret = 0;
logfile = git_path("logs/%s", ref);
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 <email> 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] != ' ' ||
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;
}
#include "patch-ids.h"
#include "decorate.h"
#include "log-tree.h"
+ #include "string-list.h"
volatile show_early_output_fn_t show_early_output;
{
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);
}
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;
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);
right_count++;
}
+ if (!left_count || !right_count)
+ return;
+
left_first = left_count < right_count;
init_patch_ids(&ids);
if (revs->diffopt.nr_paths) {
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);
} 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);
* 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 "--" */
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;
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");
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 */
#include "parse-options.h"
#include "grep.h"
+ #include "notes.h"
#define SEEN (1u<<0)
#define UNINTERESTING (1u<<1)
struct rev_info;
struct log_info;
+ struct string_list;
struct rev_info {
/* Starting list */
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
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[]);
. ./test-lib.sh
cat > fake_editor.sh << \EOF
+#!/bin/sh
echo "$MSG" > "$1"
echo "$MSG" >& 2
EOF
6th
- Notes:
+ Notes (other):
other note
EOF
test_cmp expect-not-other output
'
+ cat > expect-both << EOF
+ commit 387a89921c73d7ed72cd94d179c1c7048ca47756
+ Author: A U Thor <author@example.com>
+ Date: Thu Apr 7 15:18:13 2005 -0700
+
+ 6th
+
+ Notes:
+ order test
+
+ Notes (other):
+ other note
+
+ commit bd1753200303d0a0344be813e504253b3d98e74d
+ Author: A U Thor <author@example.com>
+ 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 <author@example.com>
+ 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 <author@example.com>
+ Date: Thu Apr 7 15:18:13 2005 -0700
+
+ 6th
+
+ commit bd1753200303d0a0344be813e504253b3d98e74d
+ Author: A U Thor <author@example.com>
+ 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 <author@example.com>
+ 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 &&
7th
- Notes:
+ Notes (other):
other note
EOF
9th
- Notes:
+ Notes (other):
yet another note
EOF
9th
- Notes:
+ Notes (other):
yet another note
$whitespace
yet another note
10th
- Notes:
+ Notes (other):
other note
EOF
10th
- Notes:
+ Notes (other):
other note
$whitespace
yet another note
11th
- Notes:
+ Notes (other):
other note
$whitespace
yet another note
11th
- Notes:
+ Notes (other):
yet another note
$whitespace
yet another note
test_must_fail git notes copy HEAD^ HEAD
'
+ cat > expect << EOF
+ commit e5d4fb5698d564ab8c73551538ecaf2b0c666185
+ Author: A U Thor <author@example.com>
+ 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 <author@example.com>
+ 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 <author@example.com>
+ Date: Thu Apr 7 15:27:13 2005 -0700
+
+ 15th
+
+ commit be28d8b4d9951ad940d229ee3b0b9ee3b1ec273d
+ Author: A U Thor <author@example.com>
+ 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 <author@example.com>
+ 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 <author@example.com>
+ 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 <author@example.com>
+ 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 <author@example.com>
+ 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 <author@example.com>
+ 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 <author@example.com>
+ 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 <author@example.com>
+ 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
test ! -s output.out
'
-q_to_cr () {
- tr Q '\015'
-}
-
test_expect_success 'Rebase a commit that sprinkles CRs in' '
(
echo "One"
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
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
# 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" \
;;
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...
-e 's/.\[m/<RESET>/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