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

1  2 
Documentation/git-commit.txt
builtin/commit.c
builtin/log.c
cache.h
commit.c
environment.c
t/t3415-rebase-autosquash.sh
index ec7b577b85ae5a320b1006380f5abd03946ba44b,6e4c220f205a8f4dd29527acf7b637c0e855239b..b586c0f442afd33c2875af26b83bb261de7361b0
@@@ -9,10 -9,10 +9,10 @@@ SYNOPSI
  --------
  [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>...]
++         [--status | --no-status] [-i | -o] [--] [<file>...]
  
  DESCRIPTION
  -----------
@@@ -70,6 -70,19 +70,19 @@@ OPTION
        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.
diff --combined builtin/commit.c
index 4fd1a1692f867e54eddd406d505e584ee0451cf8,05c2c8129e71b2b6e61ff38528b7292acf21993c..6d867d4018b38cbb6ba36d963ee5af10721bab0e
@@@ -69,6 -69,7 +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;
@@@ -114,8 -115,8 +115,8 @@@ static int opt_parse_m(const struct opt
  }
  
  static struct option builtin_commit_options[] = {
 -      OPT__QUIET(&quiet),
 -      OPT__VERBOSE(&verbose),
 +      OPT__QUIET(&quiet, "suppress summary after successful commit"),
 +      OPT__VERBOSE(&verbose, "show diff in commit message template"),
  
        OPT_GROUP("Commit message options"),
        OPT_FILENAME('F', "file", &logfile, "read log from file"),
        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 +568,25 @@@ static int prepare_to_commit(const cha
        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";
                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");
        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 +905,7 @@@ static int parse_and_validate_options(i
        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;
                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
                 */
                if (use_message_buffer == NULL)
                        use_message_buffer = xstrdup(commit->buffer);
-               if (enc != utf8)
-                       free(enc);
        }
  
        if (!!also + !!only + !!all + !!interactive > 1)
@@@ -1048,7 -1075,7 +1075,7 @@@ int cmd_status(int argc, const char **a
        int fd;
        unsigned char sha1[20];
        static struct option builtin_status_options[] = {
 -              OPT__VERBOSE(&verbose),
 +              OPT__VERBOSE(&verbose, "be verbose"),
                OPT_SET_INT('s', "short", &status_format,
                            "show status concisely", STATUS_FORMAT_SHORT),
                OPT_BOOLEAN('b', "branch", &status_show_branch,
diff --combined builtin/log.c
index d0297a1c5e563bc99cb9cd3d52f7017348980ec9,90e05aca9dc0f980f13b8beaf3865cb86c5669a8..4191d9c4e55c0745b96e8f93161ea10fb4ec7e0d
@@@ -329,8 -329,7 +329,7 @@@ static void show_tagger(char *buf, int 
        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);
  }
@@@ -1365,7 -1364,7 +1364,7 @@@ int cmd_cherry(int argc, const char **a
  
        struct option options[] = {
                OPT__ABBREV(&abbrev),
 -              OPT__VERBOSE(&verbose),
 +              OPT__VERBOSE(&verbose, "be verbose"),
                OPT_END()
        };
  
diff --combined cache.h
index e56a0a23ae9e81ea4b691c6d365491a6ecb41f62,7d498056b243afe3f828d178d42821f0667de72d..e83bc2d3bb20287b42be06edd56dfd525ddfbaa9
+++ b/cache.h
@@@ -445,7 -445,7 +445,7 @@@ extern int init_db(const char *template
   * at least 'nr' entries; the number of entries currently allocated
   * is 'alloc', using the standard growing factor alloc_nr() macro.
   *
 - * DO NOT USE any expression with side-effect for 'x' or 'alloc'.
 + * DO NOT USE any expression with side-effect for 'x', 'nr', or 'alloc'.
   */
  #define ALLOC_GROW(x, nr, alloc) \
        do { \
@@@ -545,7 -545,6 +545,7 @@@ extern int assume_unchanged
  extern int prefer_symlink_refs;
  extern int log_all_ref_updates;
  extern int warn_ambiguous_refs;
 +extern int unique_abbrev_extra_length;
  extern int shared_repository;
  extern const char *apply_default_whitespace;
  extern const char *apply_default_ignorewhitespace;
@@@ -860,7 -859,7 +860,7 @@@ struct cache_def 
  
  extern int has_symlink_leading_path(const char *name, int len);
  extern int threaded_has_symlink_leading_path(struct cache_def *, const char *, int);
 -extern int has_symlink_or_noent_leading_path(const char *name, int len);
 +extern int check_leading_path(const char *name, int len);
  extern int has_dirs_only_path(const char *name, int len, int prefix_len);
  extern void schedule_dir_for_removal(const char *name, int len);
  extern void remove_scheduled_dirs(void);
@@@ -1004,6 -1003,9 +1004,9 @@@ extern int git_env_bool(const char *, i
  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)
diff --combined commit.c
index 2d9265d9fe1e13c0295f5ff33c0cb26201f5fce0,5ed9ccd723b9992e9d331df8d7ad6122fd661a5f..b21335ee4c775d82901fb6b7dc785d63244f2bdd
+++ b/commit.c
@@@ -49,6 -49,19 +49,19 @@@ struct commit *lookup_commit(const unsi
        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;
@@@ -137,8 -150,12 +150,8 @@@ struct commit_graft *read_graft_line(ch
                buf[--len] = '\0';
        if (buf[0] == '#' || buf[0] == '\0')
                return NULL;
 -      if ((len + 1) % 41) {
 -      bad_graft_data:
 -              error("bad graft data: %s", buf);
 -              free(graft);
 -              return NULL;
 -      }
 +      if ((len + 1) % 41)
 +              goto bad_graft_data;
        i = (len + 1) / 41 - 1;
        graft = xmalloc(sizeof(*graft) + 20 * i);
        graft->nr_parent = i;
                        goto bad_graft_data;
        }
        return graft;
 +
 +bad_graft_data:
 +      error("bad graft data: %s", buf);
 +      free(graft);
 +      return NULL;
  }
  
  static int read_graft_file(const char *graft_file)
diff --combined environment.c
index 92e16b19b22ac6a973b3f7a9eab834a4d596b566,a9d44a28a75cbba39e04561e5ea3ae4f1b9f51d3..76e4dee273340e01855ffbce355741ed5d369009
@@@ -21,7 -21,6 +21,7 @@@ int prefer_symlink_refs
  int is_bare_repository_cfg = -1; /* unspecified */
  int log_all_ref_updates = -1; /* unspecified */
  int warn_ambiguous_refs = 1;
 +int unique_abbrev_extra_length;
  int repository_format_version;
  const char *git_commit_encoding;
  const char *git_log_output_encoding;
@@@ -193,3 -192,14 +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 ca16b70373b5595f4a5e3c460638c7aa2edb8663,0028533e0bfb544598ca6dc0de489bf9948b4eb4..b38be8e93723991d717b6b7fb690560efb58c36d
@@@ -14,6 -14,7 +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 +22,7 @@@
        git tag base
  '
  
- test_auto_fixup() {
+ test_auto_fixup () {
        git reset --hard base &&
        echo 1 >file1 &&
        git add -u &&
@@@ -50,7 -51,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 &&
@@@ -94,78 -95,28 +95,102 @@@ test_expect_success 'misspelled auto sq
        test 0 = $(git rev-list final-missquash...HEAD | wc -l)
  '
  
 +test_expect_success 'auto squash that matches 2 commits' '
 +      git reset --hard base &&
 +      echo 4 >file4 &&
 +      git add file4 &&
 +      test_tick &&
 +      git commit -m "first new commit" &&
 +      echo 1 >file1 &&
 +      git add -u &&
 +      test_tick &&
 +      git commit -m "squash! first" &&
 +      git tag final-multisquash &&
 +      test_tick &&
 +      git rebase --autosquash -i HEAD~4 &&
 +      git log --oneline >actual &&
 +      test 4 = $(wc -l <actual) &&
 +      git diff --exit-code final-multisquash &&
 +      test 1 = "$(git cat-file blob HEAD^^:file1)" &&
 +      test 2 = $(git cat-file commit HEAD^^ | grep first | wc -l) &&
 +      test 1 = $(git cat-file commit HEAD | grep first | wc -l)
 +'
 +
 +test_expect_success 'auto squash that matches a commit after the squash' '
 +      git reset --hard base &&
 +      echo 1 >file1 &&
 +      git add -u &&
 +      test_tick &&
 +      git commit -m "squash! third" &&
 +      echo 4 >file4 &&
 +      git add file4 &&
 +      test_tick &&
 +      git commit -m "third commit" &&
 +      git tag final-presquash &&
 +      test_tick &&
 +      git rebase --autosquash -i HEAD~4 &&
 +      git log --oneline >actual &&
 +      test 5 = $(wc -l <actual) &&
 +      git diff --exit-code final-presquash &&
 +      test 0 = "$(git cat-file blob HEAD^^:file1)" &&
 +      test 1 = "$(git cat-file blob HEAD^:file1)" &&
 +      test 1 = $(git cat-file commit HEAD | grep third | wc -l) &&
 +      test 1 = $(git cat-file commit HEAD^ | grep third | wc -l)
 +'
 +test_expect_success 'auto squash that matches a sha1' '
 +      git reset --hard base &&
 +      echo 1 >file1 &&
 +      git add -u &&
 +      test_tick &&
 +      git commit -m "squash! $(git rev-parse --short HEAD^)" &&
 +      git tag final-shasquash &&
 +      test_tick &&
 +      git rebase --autosquash -i HEAD^^^ &&
 +      git log --oneline >actual &&
 +      test 3 = $(wc -l <actual) &&
 +      git diff --exit-code final-shasquash &&
 +      test 1 = "$(git cat-file blob HEAD^:file1)" &&
 +      test 1 = $(git cat-file commit HEAD^ | grep squash | wc -l)
 +'
 +
 +test_expect_success 'auto squash that matches longer sha1' '
 +      git reset --hard base &&
 +      echo 1 >file1 &&
 +      git add -u &&
 +      test_tick &&
 +      git commit -m "squash! $(git rev-parse --short=11 HEAD^)" &&
 +      git tag final-longshasquash &&
 +      test_tick &&
 +      git rebase --autosquash -i HEAD^^^ &&
 +      git log --oneline >actual &&
 +      test 3 = $(wc -l <actual) &&
 +      git diff --exit-code final-longshasquash &&
 +      test 1 = "$(git cat-file blob HEAD^:file1)" &&
 +      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