Merge branch 'pn/commit-autosquash'
authorJunio C Hamano <gitster@pobox.com>
Sat, 4 Dec 2010 00:13:06 +0000 (16:13 -0800)
committerJunio C Hamano <gitster@pobox.com>
Sat, 4 Dec 2010 00:13:06 +0000 (16:13 -0800)
* pn/commit-autosquash:
add tests of commit --squash
commit: --squash option for use with rebase --autosquash
add tests of commit --fixup
commit: --fixup option for use with rebase --autosquash
pretty.c: teach format_commit_message() to reencode the output
commit: helper methods to reduce redundant blocks of code

Conflicts:
Documentation/git-commit.txt
t/t3415-rebase-autosquash.sh

13 files changed:
Documentation/git-commit.txt
builtin/commit.c
builtin/log.c
builtin/mailinfo.c
cache.h
commit.c
commit.h
environment.c
pretty.c
t/t3415-rebase-autosquash.sh
t/t3900-i18n-commit.sh
t/t7500-commit.sh
t/t7500/edit-content [new file with mode: 0755]
index ec7b577b85ae5a320b1006380f5abd03946ba44b..b586c0f442afd33c2875af26b83bb261de7361b0 100644 (file)
@@ -9,10 +9,10 @@ SYNOPSIS
 --------
 [verse]
 'git commit' [-a | --interactive] [-s] [-v] [-u<mode>] [--amend] [--dry-run]
-          [(-c | -C) <commit>] [-F <file> | -m <msg>] [--reset-author]
-          [--allow-empty] [--allow-empty-message] [--no-verify] [-e] [--author=<author>]
-          [--date=<date>] [--cleanup=<mode>] [--status | --no-status]
-          [-i | -o] [--] [<file>...]
+          [(-c | -C | --fixup | --squash) <commit>] [-F <file> | -m <msg>]
+          [--reset-author] [--allow-empty] [--allow-empty-message] [--no-verify]
+          [-e] [--author=<author>] [--date=<date>] [--cleanup=<mode>]
+          [--status | --no-status] [-i | -o] [--] [<file>...]
 
 DESCRIPTION
 -----------
@@ -70,6 +70,19 @@ OPTIONS
        Like '-C', but with '-c' the editor is invoked, so that
        the user can further edit the commit message.
 
+--fixup=<commit>::
+       Construct a commit message for use with `rebase --autosquash`.
+       The commit message will be the subject line from the specified
+       commit with a prefix of "fixup! ".  See linkgit:git-rebase[1]
+       for details.
+
+--squash=<commit>::
+       Construct a commit message for use with `rebase --autosquash`.
+       The commit message subject line is taken from the specified
+       commit with a prefix of "squash! ".  Can be used with additional
+       commit message options (`-m`/`-c`/`-C`/`-F`). See
+       linkgit:git-rebase[1] for details.
+
 --reset-author::
        When used with -C/-c/--amend options, declare that the
        authorship of the resulting commit now belongs of the committer.
index 4fd1a1692f867e54eddd406d505e584ee0451cf8..6d867d4018b38cbb6ba36d963ee5af10721bab0e 100644 (file)
@@ -69,6 +69,7 @@ static enum {
 static const char *logfile, *force_author;
 static const char *template_file;
 static char *edit_message, *use_message;
+static char *fixup_message, *squash_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;
@@ -124,6 +125,8 @@ static struct option builtin_commit_options[] = {
        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_STRING(0, "fixup", &fixup_message, "COMMIT", "use autosquash formatted message to fixup specified commit"),
+       OPT_STRING(0, "squash", &squash_message, "COMMIT", "use autosquash formatted message to squash 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"),
@@ -565,6 +568,25 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
        if (!no_verify && run_hook(index_file, "pre-commit", NULL))
                return 0;
 
+       if (squash_message) {
+               /*
+                * Insert the proper subject line before other commit
+                * message options add their content.
+                */
+               if (use_message && !strcmp(use_message, squash_message))
+                       strbuf_addstr(&sb, "squash! ");
+               else {
+                       struct pretty_print_context ctx = {0};
+                       struct commit *c;
+                       c = lookup_commit_reference_by_name(squash_message);
+                       if (!c)
+                               die("could not lookup commit %s", squash_message);
+                       ctx.output_encoding = get_commit_output_encoding();
+                       format_commit_message(c, "squash! %s\n\n", &sb,
+                                             &ctx);
+               }
+       }
+
        if (message.len) {
                strbuf_addbuf(&sb, &message);
                hook_arg1 = "message";
@@ -586,6 +608,16 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
                strbuf_add(&sb, buffer + 2, strlen(buffer + 2));
                hook_arg1 = "commit";
                hook_arg2 = use_message;
+       } else if (fixup_message) {
+               struct pretty_print_context ctx = {0};
+               struct commit *commit;
+               commit = lookup_commit_reference_by_name(fixup_message);
+               if (!commit)
+                       die("could not lookup commit %s", fixup_message);
+               ctx.output_encoding = get_commit_output_encoding();
+               format_commit_message(commit, "fixup! %s\n\n",
+                                     &sb, &ctx);
+               hook_arg1 = "message";
        } else if (!stat(git_path("MERGE_MSG"), &statbuf)) {
                if (strbuf_read_file(&sb, git_path("MERGE_MSG"), 0) < 0)
                        die_errno("could not read MERGE_MSG");
@@ -607,6 +639,16 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
        else if (in_merge)
                hook_arg1 = "merge";
 
+       if (squash_message) {
+               /*
+                * If squash_commit was used for the commit subject,
+                * then we're possibly hijacking other commit log options.
+                * Reset the hook args to tell the real story.
+                */
+               hook_arg1 = "message";
+               hook_arg2 = "";
+       }
+
        fp = fopen(git_path(commit_editmsg), "w");
        if (fp == NULL)
                die_errno("could not open '%s'", git_path(commit_editmsg));
@@ -863,7 +905,7 @@ static int parse_and_validate_options(int argc, const char *argv[],
        if (force_author && renew_authorship)
                die("Using both --reset-author and --author does not make sense");
 
-       if (logfile || message.len || use_message)
+       if (logfile || message.len || use_message || fixup_message)
                use_editor = 0;
        if (edit_flag)
                use_editor = 1;
@@ -878,48 +920,35 @@ static int parse_and_validate_options(int argc, const char *argv[],
                die("You have nothing to amend.");
        if (amend && in_merge)
                die("You are in the middle of a merge -- cannot amend.");
-
+       if (fixup_message && squash_message)
+               die("Options --squash and --fixup cannot be used together");
        if (use_message)
                f++;
        if (edit_message)
                f++;
+       if (fixup_message)
+               f++;
        if (logfile)
                f++;
        if (f > 1)
-               die("Only one of -c/-C/-F can be used.");
+               die("Only one of -c/-C/-F/--fixup can be used.");
        if (message.len && f > 0)
-               die("Option -m cannot be combined with -c/-C/-F.");
+               die("Option -m cannot be combined with -c/-C/-F/--fixup.");
        if (edit_message)
                use_message = edit_message;
-       if (amend && !use_message)
+       if (amend && !use_message && !fixup_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))
+               commit = lookup_commit_reference_by_name(use_message);
+               if (!commit)
                        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);
+               out_enc = get_commit_output_encoding();
+               use_message_buffer = logmsg_reencode(commit, out_enc);
 
                /*
                 * If we failed to reencode the buffer, just copy it
@@ -929,8 +958,6 @@ static int parse_and_validate_options(int argc, const char *argv[],
                 */
                if (use_message_buffer == NULL)
                        use_message_buffer = xstrdup(commit->buffer);
-               if (enc != utf8)
-                       free(enc);
        }
 
        if (!!also + !!only + !!all + !!interactive > 1)
index d0297a1c5e563bc99cb9cd3d52f7017348980ec9..4191d9c4e55c0745b96e8f93161ea10fb4ec7e0d 100644 (file)
@@ -329,8 +329,7 @@ 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);
+               get_log_output_encoding());
        printf("%s", out.buf);
        strbuf_release(&out);
 }
index 2320d981ceab220e287e42c21f3c9f3be5b3722f..71e6262a87d883a4a82953db5f742bccf99e5c77 100644 (file)
@@ -1032,7 +1032,7 @@ int cmd_mailinfo(int argc, const char **argv, const char *prefix)
         */
        git_config(git_mailinfo_config, NULL);
 
-       def_charset = (git_commit_encoding ? git_commit_encoding : "UTF-8");
+       def_charset = get_commit_output_encoding();
        metainfo_charset = def_charset;
 
        while (1 < argc && argv[1][0] == '-') {
diff --git a/cache.h b/cache.h
index e56a0a23ae9e81ea4b691c6d365491a6ecb41f62..e83bc2d3bb20287b42be06edd56dfd525ddfbaa9 100644 (file)
--- a/cache.h
+++ b/cache.h
@@ -1004,6 +1004,9 @@ extern int git_env_bool(const char *, int);
 extern int git_config_system(void);
 extern int git_config_global(void);
 extern int config_error_nonbool(const char *);
+extern const char *get_log_output_encoding(void);
+extern const char *get_commit_output_encoding(void);
+
 extern const char *config_exclusive_filename;
 
 #define MAX_GITNAME (1000)
index 2d9265d9fe1e13c0295f5ff33c0cb26201f5fce0..b21335ee4c775d82901fb6b7dc785d63244f2bdd 100644 (file)
--- a/commit.c
+++ b/commit.c
@@ -49,6 +49,19 @@ struct commit *lookup_commit(const unsigned char *sha1)
        return check_commit(obj, sha1, 0);
 }
 
+struct commit *lookup_commit_reference_by_name(const char *name)
+{
+       unsigned char sha1[20];
+       struct commit *commit;
+
+       if (get_sha1(name, sha1))
+               return NULL;
+       commit = lookup_commit_reference(sha1);
+       if (!commit || parse_commit(commit))
+               return NULL;
+       return commit;
+}
+
 static unsigned long parse_commit_date(const char *buf, const char *tail)
 {
        const char *dateptr;
index 9113bbe4889d71e824348edcb920110598db18d2..3bfb31b5e0faad6ea04fde9e2f932f54d2e6c235 100644 (file)
--- a/commit.h
+++ b/commit.h
@@ -36,6 +36,7 @@ struct commit *lookup_commit(const unsigned char *sha1);
 struct commit *lookup_commit_reference(const unsigned char *sha1);
 struct commit *lookup_commit_reference_gently(const unsigned char *sha1,
                                              int quiet);
+struct commit *lookup_commit_reference_by_name(const char *name);
 
 int parse_commit_buffer(struct commit *item, void *buffer, unsigned long size);
 
@@ -76,6 +77,7 @@ struct pretty_print_context
        int need_8bit_cte;
        int show_notes;
        struct reflog_walk_info *reflog_info;
+       const char *output_encoding;
 };
 
 struct userformat_want {
@@ -84,6 +86,8 @@ struct userformat_want {
 
 extern int has_non_ascii(const char *text);
 struct rev_info; /* in revision.h, it circularly uses enum cmit_fmt */
+extern char *logmsg_reencode(const struct commit *commit,
+                            const char *output_encoding);
 extern char *reencode_commit_message(const struct commit *commit,
                                     const char **encoding_p);
 extern void get_commit_format(const char *arg, struct rev_info *);
index 92e16b19b22ac6a973b3f7a9eab834a4d596b566..76e4dee273340e01855ffbce355741ed5d369009 100644 (file)
@@ -193,3 +193,14 @@ int set_git_dir(const char *path)
        setup_git_env();
        return 0;
 }
+
+const char *get_log_output_encoding(void)
+{
+       return git_log_output_encoding ? git_log_output_encoding
+               : get_commit_output_encoding();
+}
+
+const char *get_commit_output_encoding(void)
+{
+       return git_commit_encoding ? git_commit_encoding : "UTF-8";
+}
index f85444b27d4815547f7cef542291b1f6616f77d5..85499347514ec3e1d62d6caa9f53b886c556e345 100644 (file)
--- a/pretty.c
+++ b/pretty.c
@@ -403,8 +403,8 @@ static char *replace_encoding_header(char *buf, const char *encoding)
        return strbuf_detach(&tmp, NULL);
 }
 
-static char *logmsg_reencode(const struct commit *commit,
-                            const char *output_encoding)
+char *logmsg_reencode(const struct commit *commit,
+                     const char *output_encoding)
 {
        static const char *utf8 = "UTF-8";
        const char *use_encoding;
@@ -555,6 +555,7 @@ struct format_commit_context {
        const struct pretty_print_context *pretty_ctx;
        unsigned commit_header_parsed:1;
        unsigned commit_message_parsed:1;
+       char *message;
        size_t width, indent1, indent2;
 
        /* These offsets are relative to the start of the commit message. */
@@ -591,7 +592,7 @@ static int add_again(struct strbuf *sb, struct chunk *chunk)
 
 static void parse_commit_header(struct format_commit_context *context)
 {
-       const char *msg = context->commit->buffer;
+       const char *msg = context->message;
        int i;
 
        for (i = 0; msg[i]; i++) {
@@ -677,8 +678,8 @@ const char *format_subject(struct strbuf *sb, const char *msg,
 
 static void parse_commit_message(struct format_commit_context *c)
 {
-       const char *msg = c->commit->buffer + c->message_off;
-       const char *start = c->commit->buffer;
+       const char *msg = c->message + c->message_off;
+       const char *start = c->message;
 
        msg = skip_empty_lines(msg);
        c->subject_off = msg - start;
@@ -741,7 +742,7 @@ static size_t format_commit_one(struct strbuf *sb, const char *placeholder,
 {
        struct format_commit_context *c = context;
        const struct commit *commit = c->commit;
-       const char *msg = commit->buffer;
+       const char *msg = c->message;
        struct commit_list *p;
        int h1, h2;
 
@@ -886,8 +887,7 @@ static size_t format_commit_one(struct strbuf *sb, const char *placeholder,
        case 'N':
                if (c->pretty_ctx->show_notes) {
                        format_display_notes(commit->object.sha1, sb,
-                                   git_log_output_encoding ? git_log_output_encoding
-                                                           : git_commit_encoding, 0);
+                                   get_log_output_encoding(), 0);
                        return 1;
                }
                return 0;
@@ -1012,13 +1012,27 @@ void format_commit_message(const struct commit *commit,
                           const struct pretty_print_context *pretty_ctx)
 {
        struct format_commit_context context;
+       static const char utf8[] = "UTF-8";
+       const char *enc;
+       const char *output_enc = pretty_ctx->output_encoding;
 
        memset(&context, 0, sizeof(context));
        context.commit = commit;
        context.pretty_ctx = pretty_ctx;
        context.wrap_start = sb->len;
+       context.message = commit->buffer;
+       if (output_enc) {
+               enc = get_header(commit, "encoding");
+               enc = enc ? enc : utf8;
+               if (strcmp(enc, output_enc))
+                       context.message = logmsg_reencode(commit, output_enc);
+       }
+
        strbuf_expand(sb, format, format_commit_item, &context);
        rewrap_message_tail(sb, &context, 0, 0, 0);
+
+       if (context.message != commit->buffer)
+               free(context.message);
 }
 
 static void pp_header(enum cmit_fmt fmt,
@@ -1159,11 +1173,7 @@ char *reencode_commit_message(const struct commit *commit, const char **encoding
 {
        const char *encoding;
 
-       encoding = (git_log_output_encoding
-                   ? git_log_output_encoding
-                   : git_commit_encoding);
-       if (!encoding)
-               encoding = "UTF-8";
+       encoding = get_log_output_encoding();
        if (encoding_p)
                *encoding_p = encoding;
        return logmsg_reencode(commit, encoding);
index ca16b70373b5595f4a5e3c460638c7aa2edb8663..b38be8e93723991d717b6b7fb690560efb58c36d 100755 (executable)
@@ -14,6 +14,7 @@ test_expect_success setup '
        git add . &&
        test_tick &&
        git commit -m "first commit" &&
+       git tag first-commit &&
        echo 3 >file3 &&
        git add . &&
        test_tick &&
@@ -21,7 +22,7 @@ test_expect_success setup '
        git tag base
 '
 
-test_auto_fixup() {
+test_auto_fixup () {
        git reset --hard base &&
        echo 1 >file1 &&
        git add -u &&
@@ -50,7 +51,7 @@ test_expect_success 'auto fixup (config)' '
        test_must_fail test_auto_fixup final-fixup-config-false
 '
 
-test_auto_squash() {
+test_auto_squash () {
        git reset --hard base &&
        echo 1 >file1 &&
        git add -u &&
@@ -168,4 +169,28 @@ test_expect_success 'auto squash that matches longer sha1' '
        test 1 = $(git cat-file commit HEAD^ | grep squash | wc -l)
 '
 
+test_auto_commit_flags () {
+       git reset --hard base &&
+       echo 1 >file1 &&
+       git add -u &&
+       test_tick &&
+       git commit --$1 first-commit &&
+       git tag final-commit-$1 &&
+       test_tick &&
+       git rebase --autosquash -i HEAD^^^ &&
+       git log --oneline >actual &&
+       test 3 = $(wc -l <actual) &&
+       git diff --exit-code final-commit-$1 &&
+       test 1 = "$(git cat-file blob HEAD^:file1)" &&
+       test $2 = $(git cat-file commit HEAD^ | grep first | wc -l)
+}
+
+test_expect_success 'use commit --fixup' '
+       test_auto_commit_flags fixup 1
+'
+
+test_expect_success 'use commit --squash' '
+       test_auto_commit_flags squash 2
+'
+
 test_done
index 256c4c970145aa9f59e58ee1b0da4c6281b6d9e5..c06a5ee7660c3fd7ca15860cfb761b2c4d953e08 100755 (executable)
@@ -133,4 +133,33 @@ do
        '
 done
 
+test_commit_autosquash_flags () {
+       H=$1
+       flag=$2
+       test_expect_success "commit --$flag with $H encoding" '
+               git config i18n.commitencoding $H &&
+               git checkout -b $H-$flag C0 &&
+               echo $H >>F &&
+               git commit -a -F "$TEST_DIRECTORY"/t3900/$H.txt &&
+               test_tick &&
+               echo intermediate stuff >>G &&
+               git add G &&
+               git commit -a -m "intermediate commit" &&
+               test_tick &&
+               echo $H $flag >>F &&
+               git commit -a --$flag HEAD~1 $3 &&
+               E=$(git cat-file commit '$H-$flag' |
+                       sed -ne "s/^encoding //p") &&
+               test "z$E" = "z$H" &&
+               git config --unset-all i18n.commitencoding &&
+               git rebase --autosquash -i HEAD^^^ &&
+               git log --oneline >actual &&
+               test 3 = $(wc -l <actual)
+       '
+}
+
+test_commit_autosquash_flags eucJP fixup
+
+test_commit_autosquash_flags ISO-2022-JP squash '-m "squash message"'
+
 test_done
index aa9c577e9e306bce05cd0fe076c56017c1b97e41..162527c2114955df2c55db9bf37db59d05fe75f8 100755 (executable)
@@ -215,4 +215,84 @@ test_expect_success 'Commit a message with --allow-empty-message' '
        commit_msg_is "hello there"
 '
 
+commit_for_rebase_autosquash_setup () {
+       echo "first content line" >>foo &&
+       git add foo &&
+       cat >log <<EOF &&
+target message subject line
+
+target message body line 1
+target message body line 2
+EOF
+       git commit -F log &&
+       echo "second content line" >>foo &&
+       git add foo &&
+       git commit -m "intermediate commit" &&
+       echo "third content line" >>foo &&
+       git add foo
+}
+
+test_expect_success 'commit --fixup provides correct one-line commit message' '
+       commit_for_rebase_autosquash_setup &&
+       git commit --fixup HEAD~1 &&
+       commit_msg_is "fixup! target message subject line"
+'
+
+test_expect_success 'commit --squash works with -F' '
+       commit_for_rebase_autosquash_setup &&
+       echo "log message from file" >msgfile &&
+       git commit --squash HEAD~1 -F msgfile  &&
+       commit_msg_is "squash! target message subject linelog message from file"
+'
+
+test_expect_success 'commit --squash works with -m' '
+       commit_for_rebase_autosquash_setup &&
+       git commit --squash HEAD~1 -m "foo bar\nbaz" &&
+       commit_msg_is "squash! target message subject linefoo bar\nbaz"
+'
+
+test_expect_success 'commit --squash works with -C' '
+       commit_for_rebase_autosquash_setup &&
+       git commit --squash HEAD~1 -C HEAD &&
+       commit_msg_is "squash! target message subject lineintermediate commit"
+'
+
+test_expect_success 'commit --squash works with -c' '
+       commit_for_rebase_autosquash_setup &&
+       test_set_editor "$TEST_DIRECTORY"/t7500/edit-content &&
+       git commit --squash HEAD~1 -c HEAD &&
+       commit_msg_is "squash! target message subject lineedited commit"
+'
+
+test_expect_success 'commit --squash works with -C for same commit' '
+       commit_for_rebase_autosquash_setup &&
+       git commit --squash HEAD -C HEAD &&
+       commit_msg_is "squash! intermediate commit"
+'
+
+test_expect_success 'commit --squash works with -c for same commit' '
+       commit_for_rebase_autosquash_setup &&
+       test_set_editor "$TEST_DIRECTORY"/t7500/edit-content &&
+       git commit --squash HEAD -c HEAD &&
+       commit_msg_is "squash! edited commit"
+'
+
+test_expect_success 'commit --squash works with editor' '
+       commit_for_rebase_autosquash_setup &&
+       test_set_editor "$TEST_DIRECTORY"/t7500/add-content &&
+       git commit --squash HEAD~1 &&
+       commit_msg_is "squash! target message subject linecommit message"
+'
+
+test_expect_success 'invalid message options when using --fixup' '
+       echo changes >>foo &&
+       echo "message" >log &&
+       git add foo &&
+       test_must_fail git commit --fixup HEAD~1 --squash HEAD~2 &&
+       test_must_fail git commit --fixup HEAD~1 -C HEAD~2 &&
+       test_must_fail git commit --fixup HEAD~1 -c HEAD~2 &&
+       test_must_fail git commit --fixup HEAD~1 -m "cmdline message" &&
+       test_must_fail git commit --fixup HEAD~1 -F log
+'
+
 test_done
diff --git a/t/t7500/edit-content b/t/t7500/edit-content
new file mode 100755 (executable)
index 0000000..08db9fd
--- /dev/null
@@ -0,0 +1,4 @@
+#!/bin/sh
+sed -e "s/intermediate/edited/g" <"$1" >"$1-"
+mv "$1-" "$1"
+exit 0