Code

Merge branch 'jk/1.7.0-status'
authorJunio C Hamano <gitster@pobox.com>
Mon, 28 Dec 2009 07:01:32 +0000 (23:01 -0800)
committerJunio C Hamano <gitster@pobox.com>
Mon, 28 Dec 2009 07:01:32 +0000 (23:01 -0800)
* jk/1.7.0-status:
  status/commit: do not suggest "reset HEAD <path>" while merging
  commit/status: "git add <path>" is not necessarily how to resolve
  commit/status: check $GIT_DIR/MERGE_HEAD only once
  t7508-status: test all modes with color
  t7508-status: status --porcelain ignores relative paths setting
  status: reduce duplicated setup code
  status: disable color for porcelain format
  status -s: obey color.status
  builtin-commit: refactor short-status code into wt-status.c
  t7508-status.sh: Add tests for status -s
  status -s: respect the status.relativePaths option
  docs: note that status configuration affects only long format
  commit: support alternate status formats
  status: add --porcelain output format
  status: refactor format option parsing
  status: refactor short-mode printing to its own function
  status: typo fix in usage
  git status: not "commit --dry-run" anymore
  git stat -s: short status output
  git stat: the beginning of "status that is not a dry-run of commit"

Conflicts:
t/t4034-diff-words.sh
wt-status.c

1  2 
Documentation/git-commit.txt
builtin-commit.c
t/t4034-diff-words.sh
t/test-lib.sh
wt-status.c

index d227cec9ba566caa098c36e8c91b19a8a27fcae5,c45fbe4f9753df62299acc687f86dc672ba80860..c97c151ae1fdf53a09c5d3796ea6f6fb98b57b03
@@@ -9,7 -9,7 +9,7 @@@ SYNOPSI
  --------
  [verse]
  'git commit' [-a | --interactive] [-s] [-v] [-u<mode>] [--amend] [--dry-run]
 -         [(-c | -C) <commit>] [-F <file> | -m <msg>] [--dry-run]
 +         [(-c | -C) <commit>] [-F <file> | -m <msg>] [--reset-author]
           [--allow-empty] [--no-verify] [-e] [--author=<author>]
           [--cleanup=<mode>] [--] [[-i | -o ]<file>...]
  
@@@ -69,11 -69,26 +69,25 @@@ OPTION
        Like '-C', but with '-c' the editor is invoked, so that
        the user can further edit the commit message.
  
 ---dry-run::
 -      Do not actually make a commit, but show the list of paths
 -      with updates in the index, paths with changes in the work tree,
 -      and paths that are untracked, similar to the one that is given
 -      in the commit log editor.
 +--reset-author::
 +      When used with -C/-c/--amend options, declare that the
 +      authorship of the resulting commit now belongs of the committer.
 +      This also renews the author timestamp.
  
+ --short::
+       When doing a dry-run, give the output in the short-format. See
+       linkgit:git-status[1] for details. Implies `--dry-run`.
+ --porcelain::
+       When doing a dry-run, give the output in a porcelain-ready
+       format. See linkgit:git-status[1] for details. Implies
+       `--dry-run`.
+ -z::
+       When showing `short` or `porcelain` status output, terminate
+       entries in the status output with NUL, instead of LF. If no
+       format is given, implies the `--porcelain` output format.
  -F <file>::
  --file=<file>::
        Take the commit message from the given file.  Use '-' to
@@@ -328,7 -343,7 +342,7 @@@ ENVIRONMENT AND CONFIGURATION VARIABLE
  The editor used to edit the commit log message will be chosen from the
  GIT_EDITOR environment variable, the core.editor configuration variable, the
  VISUAL environment variable, or the EDITOR environment variable (in that
 -order).
 +order).  See linkgit:git-var[1] for details.
  
  HOOKS
  -----
diff --combined builtin-commit.c
index f54772f74a14e480ae978b38ce3bcd49ee994411,7218454d1e602d28e79edd2c04f436062fa8a5bc..6822aa07ca80ee648fd331a79f54915cd338f8d2
@@@ -24,6 -24,7 +24,7 @@@
  #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>...",
@@@ -35,7 -36,7 +36,7 @@@ static const char * const builtin_statu
        NULL
  };
  
- static unsigned char head_sha1[20], merge_head_sha1[20];
+ 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 */
@@@ -51,7 -52,7 +52,7 @@@ 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;
 +static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;
  static char *untracked_files_arg;
  /*
   * The default commit message cleanup mode will remove the lines
@@@ -71,6 -72,13 +72,13 @@@ static int use_editor = 1, initial_comm
  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;
@@@ -91,9 -99,8 +99,9 @@@ static struct option builtin_commit_opt
        OPT_FILENAME('F', "file", &logfile, "read log from file"),
        OPT_STRING(0, "author", &force_author, "AUTHOR", "override author 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', "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_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"),
        { 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"),
@@@ -306,7 -319,7 +320,7 @@@ static char *prepare_index(int argc, co
         */
        commit_style = COMMIT_PARTIAL;
  
-       if (file_exists(git_path("MERGE_HEAD")))
+       if (in_merge)
                die("cannot do a partial commit during a merge.");
  
        memset(&partial, 0, sizeof(partial));
  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;
  
        s->index_file = index_file;
        s->fp = fp;
        s->nowarn = nowarn;
+       s->is_initial = get_sha1(s->reference, sha1) ? 1 : 0;
  
-       wt_status_print(s);
+       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;
  }
@@@ -382,7 -410,7 +411,7 @@@ static void determine_author_info(void
        email = getenv("GIT_AUTHOR_EMAIL");
        date = getenv("GIT_AUTHOR_DATE");
  
 -      if (use_message) {
 +      if (use_message && !renew_authorship) {
                const char *a, *lb, *rb, *eol;
  
                a = strstr(use_message_buffer, "\nauthor ");
        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)
  {
                for (i = sb.len - 1; i > 0 && sb.buf[i - 1] != '\n'; i--)
                        ; /* do nothing */
                if (prefixcmp(sb.buf + i, sob.buf)) {
 -                      if (prefixcmp(sb.buf + i, sign_off_header))
 +                      if (!i || !ends_rfc2822_footer(&sb))
                                strbuf_addch(&sb, '\n');
                        strbuf_addbuf(&sb, &sob);
                }
@@@ -726,15 -713,28 +755,30 @@@ static const char *find_author_by_nickn
        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, DATE_NORMAL);
 +              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,
        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)
        if (get_sha1("HEAD", head_sha1))
                initial_commit = 1;
  
-       if (!get_sha1("MERGE_HEAD", merge_head_sha1))
-               in_merge = 1;
        /* Sanity check options */
        if (amend && initial_commit)
                die("You have nothing to amend.");
                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";
        else
                die("Invalid cleanup mode %s", cleanup_arg);
  
-       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);
+       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;
  }
  
@@@ -890,7 -878,7 +927,7 @@@ static int parse_status_slot(const cha
                return WT_STATUS_NOBRANCH;
        if (!strcasecmp(var+offset, "unmerged"))
                return WT_STATUS_UNMERGED;
 -      die("bad config variable '%s'", var);
 +      return -1;
  }
  
  static int git_status_config(const char *k, const char *v, void *cb)
        }
        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]);
  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();
+       refresh_cache(REFRESH_QUIET|REFRESH_UNMERGED);
+       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;
  
-       argc = parse_and_validate_options(argc, argv, builtin_status_usage,
-                                         prefix, &s);
-       return dry_run_commit(argc, argv, prefix, &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:
+               s.verbose = verbose;
+               wt_status_print(&s);
+               break;
+       }
+       return 0;
  }
  
  static void print_summary(const char *prefix, const unsigned char *sha1)
                initial_commit ? " (root-commit)" : "");
  
        if (!log_tree_commit(&rev, commit)) {
 +              struct pretty_print_context ctx = {0};
                struct strbuf buf = STRBUF_INIT;
 -              format_commit_message(commit, format + 7, &buf, DATE_NORMAL);
 +              ctx.date_mode = DATE_NORMAL;
 +              format_commit_message(commit, format + 7, &buf, &ctx);
                printf("%s\n", buf.buf);
                strbuf_release(&buf);
        }
@@@ -1007,7 -1037,7 +1090,7 @@@ static int git_commit_config(const cha
        struct wt_status *s = cb;
  
        if (!strcmp(k, "commit.template"))
 -              return git_config_string(&template_file, k, v);
 +              return git_config_pathname(&template_file, k, v);
  
        return git_status_config(k, v, s);
  }
@@@ -1026,10 -1056,11 +1109,11 @@@ int cmd_commit(int argc, const char **a
  
        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) {
diff --combined t/t4034-diff-words.sh
index 1c21276c55400c594f71bc01eba17b0d3820b420,17621dd5a412620299f53eecb7e247b3829e25c5..2e2e103b31332ea2f74de5d5e6e49c00b13dfa8a
@@@ -8,23 -8,12 +8,13 @@@ test_expect_success setup 
  
        git config diff.color.old red
        git config diff.color.new green
 +      git config diff.color.func magenta
  
  '
  
- decrypt_color () {
-       sed \
-               -e 's/.\[1m/<WHITE>/g' \
-               -e 's/.\[31m/<RED>/g' \
-               -e 's/.\[32m/<GREEN>/g' \
-               -e 's/.\[35m/<MAGENTA>/g' \
-               -e 's/.\[36m/<BROWN>/g' \
-               -e 's/.\[m/<RESET>/g'
- }
  word_diff () {
        test_must_fail git diff --no-index "$@" pre post > output &&
-       decrypt_color < output > output.decrypted &&
+       test_decode_color <output >output.decrypted &&
        test_cmp expect output.decrypted
  }
  
@@@ -49,9 -38,9 +39,9 @@@ cat > expect <<\EO
  <WHITE>index 330b04f..5ed8eff 100644<RESET>
  <WHITE>--- a/pre<RESET>
  <WHITE>+++ b/post<RESET>
- <BROWN>@@ -1,3 +1,7 @@<RESET>
+ <CYAN>@@ -1,3 +1,7 @@<RESET>
  <RED>h(4)<RESET><GREEN>h(4),hh[44]<RESET>
 -<RESET>
 +
  a = b + c<RESET>
  
  <GREEN>aa = a<RESET>
@@@ -65,34 -54,14 +55,34 @@@ test_expect_success 'word diff with run
  
  '
  
- <BROWN>@@ -1 +1 @@<RESET>
 +cat > expect <<\EOF
 +<WHITE>diff --git a/pre b/post<RESET>
 +<WHITE>index 330b04f..5ed8eff 100644<RESET>
 +<WHITE>--- a/pre<RESET>
 +<WHITE>+++ b/post<RESET>
- <BROWN>@@ -3,0 +4,4 @@<RESET> <RESET><MAGENTA>a = b + c<RESET>
++<CYAN>@@ -1 +1 @@<RESET>
 +<RED>h(4)<RESET><GREEN>h(4),hh[44]<RESET>
++<CYAN>@@ -3,0 +4,4 @@<RESET> <RESET><MAGENTA>a = b + c<RESET>
 +
 +<GREEN>aa = a<RESET>
 +
 +<GREEN>aeff = aeff * ( aaa )<RESET>
 +EOF
 +
 +test_expect_success 'word diff without context' '
 +
 +      word_diff --color-words --unified=0
 +
 +'
 +
  cat > expect <<\EOF
  <WHITE>diff --git a/pre b/post<RESET>
  <WHITE>index 330b04f..5ed8eff 100644<RESET>
  <WHITE>--- a/pre<RESET>
  <WHITE>+++ b/post<RESET>
- <BROWN>@@ -1,3 +1,7 @@<RESET>
+ <CYAN>@@ -1,3 +1,7 @@<RESET>
  h(4),<GREEN>hh<RESET>[44]
 -<RESET>
 +
  a = b + c<RESET>
  
  <GREEN>aa = a<RESET>
@@@ -126,9 -95,9 +116,9 @@@ cat > expect <<\EO
  <WHITE>index 330b04f..5ed8eff 100644<RESET>
  <WHITE>--- a/pre<RESET>
  <WHITE>+++ b/post<RESET>
- <BROWN>@@ -1,3 +1,7 @@<RESET>
+ <CYAN>@@ -1,3 +1,7 @@<RESET>
  h(4)<GREEN>,hh[44]<RESET>
 -<RESET>
 +
  a = b + c<RESET>
  
  <GREEN>aa = a<RESET>
@@@ -168,9 -137,9 +158,9 @@@ cat > expect <<\EO
  <WHITE>index 330b04f..5ed8eff 100644<RESET>
  <WHITE>--- a/pre<RESET>
  <WHITE>+++ b/post<RESET>
- <BROWN>@@ -1,3 +1,7 @@<RESET>
+ <CYAN>@@ -1,3 +1,7 @@<RESET>
  h(4),<GREEN>hh[44<RESET>]
 -<RESET>
 +
  a = b + c<RESET>
  
  <GREEN>aa = a<RESET>
@@@ -190,7 -159,7 +180,7 @@@ cat > expect <<\EO
  <WHITE>index c29453b..be22f37 100644<RESET>
  <WHITE>--- a/pre<RESET>
  <WHITE>+++ b/post<RESET>
- <BROWN>@@ -1 +1 @@<RESET>
+ <CYAN>@@ -1 +1 @@<RESET>
  aaa (aaa) <GREEN>aaa<RESET>
  EOF
  
@@@ -209,7 -178,7 +199,7 @@@ cat > expect <<\EO
  <WHITE>index 289cb9d..2d06f37 100644<RESET>
  <WHITE>--- a/pre<RESET>
  <WHITE>+++ b/post<RESET>
- <BROWN>@@ -1 +1 @@<RESET>
+ <CYAN>@@ -1 +1 @@<RESET>
  (<RED>:<RESET>
  EOF
  
diff --combined t/test-lib.sh
index 2d523fe0f08dbbc1971f4601f24664cdbce95415,d63ad2d870423cd28b42993bf35fa0257df654a8..142f36f2e919929520d240e56b65f2b69e4b4982
@@@ -30,7 -30,7 +30,7 @@@ TZ=UT
  TERM=dumb
  export LANG LC_ALL PAGER TERM TZ
  EDITOR=:
 -VISUAL=:
 +unset VISUAL
  unset GIT_EDITOR
  unset AUTHOR_DATE
  unset AUTHOR_EMAIL
@@@ -58,7 -58,7 +58,7 @@@ GIT_MERGE_VERBOSITY=
  export GIT_MERGE_VERBOSITY
  export GIT_AUTHOR_EMAIL GIT_AUTHOR_NAME
  export GIT_COMMITTER_EMAIL GIT_COMMITTER_NAME
 -export EDITOR VISUAL
 +export EDITOR
  GIT_TEST_CMP=${GIT_TEST_CMP:-diff -u}
  
  # Protect ourselves from common misconfiguration to export
@@@ -114,9 -114,6 +114,9 @@@ d
                valgrind=t; verbose=t; shift ;;
        --tee)
                shift ;; # was handled already
 +      --root=*)
 +              root=$(expr "z$1" : 'z[^=]*=\(.*\)')
 +              shift ;;
        *)
                echo "error: unknown test option '$1'" >&2; exit 1 ;;
        esac
@@@ -207,10 -204,21 +207,21 @@@ trap 'die' EXI
  test_set_editor () {
        FAKE_EDITOR="$1"
        export FAKE_EDITOR
 -      VISUAL='"$FAKE_EDITOR"'
 -      export VISUAL
 +      EDITOR='"$FAKE_EDITOR"'
 +      export EDITOR
  }
  
+ test_decode_color () {
+       sed     -e 's/.\[1m/<WHITE>/g' \
+               -e 's/.\[31m/<RED>/g' \
+               -e 's/.\[32m/<GREEN>/g' \
+               -e 's/.\[33m/<YELLOW>/g' \
+               -e 's/.\[34m/<BLUE>/g' \
+               -e 's/.\[35m/<MAGENTA>/g' \
+               -e 's/.\[36m/<CYAN>/g' \
+               -e 's/.\[m/<RESET>/g'
+ }
  test_tick () {
        if test -z "${test_tick+set}"
        then
@@@ -632,37 -640,23 +643,37 @@@ GIT_CONFIG_NOSYSTEM=
  GIT_CONFIG_NOGLOBAL=1
  export PATH GIT_EXEC_PATH GIT_TEMPLATE_DIR GIT_CONFIG_NOSYSTEM GIT_CONFIG_NOGLOBAL
  
 +. ../GIT-BUILD-OPTIONS
 +
  GITPERLLIB=$(pwd)/../perl/blib/lib:$(pwd)/../perl/blib/arch/auto/Git
  export GITPERLLIB
  test -d ../templates/blt || {
        error "You haven't built things yet, have you?"
  }
  
 +if test -z "$GIT_TEST_INSTALLED" && test -z "$NO_PYTHON"
 +then
 +      GITPYTHONLIB="$(pwd)/../git_remote_helpers/build/lib"
 +      export GITPYTHONLIB
 +      test -d ../git_remote_helpers/build || {
 +              error "You haven't built git_remote_helpers yet, have you?"
 +      }
 +fi
 +
  if ! test -x ../test-chmtime; then
        echo >&2 'You need to build test-chmtime:'
        echo >&2 'Run "make test-chmtime" in the source (toplevel) directory'
        exit 1
  fi
  
 -. ../GIT-BUILD-OPTIONS
 -
  # Test repository
  test="trash directory.$(basename "$0" .sh)"
 -test ! -z "$debug" || remove_trash="$TEST_DIRECTORY/$test"
 +test -n "$root" && test="$root/$test"
 +case "$test" in
 +/*) TRASH_DIRECTORY="$test" ;;
 + *) TRASH_DIRECTORY="$TEST_DIRECTORY/$test" ;;
 +esac
 +test ! -z "$debug" || remove_trash=$TRASH_DIRECTORY
  rm -fr "$test" || {
        GIT_EXIT_OK=t
        echo >&5 "FATAL: Cannot prepare test area"
        esac
  done
  
 +# Provide an implementation of the 'yes' utility
 +yes () {
 +      if test $# = 0
 +      then
 +              y=y
 +      else
 +              y="$*"
 +      fi
 +
 +      while echo "$y"
 +      do
 +              :
 +      done
 +}
 +
  # Fix some commands on Windows
  case $(uname -s) in
  *MINGW*)
  esac
  
  test -z "$NO_PERL" && test_set_prereq PERL
 +test -z "$NO_PYTHON" && test_set_prereq PYTHON
  
  # test whether the filesystem supports symbolic links
  ln -s x y 2>/dev/null && test -h y 2>/dev/null && test_set_prereq SYMLINKS
diff --combined wt-status.c
index 38eb24536b34e40af4c43eb1631e08624e8993d6,c4589055bb7d6b8e945477d09e0015fe2da349af..5d569880163cccec91fdf1e9d50e03a6b5314d58
@@@ -47,28 -47,29 +47,33 @@@ void wt_status_prepare(struct wt_statu
  static void wt_status_print_unmerged_header(struct wt_status *s)
  {
        const char *c = color(WT_STATUS_HEADER, s);
        color_fprintf_ln(s->fp, c, "# Unmerged paths:");
-       if (!s->is_initial)
 +      if (!advice_status_hints)
 +              return;
+       if (s->in_merge)
+               ;
+       else if (!s->is_initial)
                color_fprintf_ln(s->fp, c, "#   (use \"git reset %s <file>...\" to unstage)", s->reference);
        else
                color_fprintf_ln(s->fp, c, "#   (use \"git rm --cached <file>...\" to unstage)");
-       color_fprintf_ln(s->fp, c, "#   (use \"git add <file>...\" to mark resolution)");
+       color_fprintf_ln(s->fp, c, "#   (use \"git add/rm <file>...\" as appropriate to mark resolution)");
        color_fprintf_ln(s->fp, c, "#");
  }
  
  static void wt_status_print_cached_header(struct wt_status *s)
  {
        const char *c = color(WT_STATUS_HEADER, s);
        color_fprintf_ln(s->fp, c, "# Changes to be committed:");
-       if (!s->is_initial) {
 +      if (!advice_status_hints)
 +              return;
+       if (s->in_merge)
+               ; /* NEEDSWORK: use "git reset --unresolve"??? */
+       else if (!s->is_initial)
                color_fprintf_ln(s->fp, c, "#   (use \"git reset %s <file>...\" to unstage)", s->reference);
-       } else {
+       else
                color_fprintf_ln(s->fp, c, "#   (use \"git rm --cached <file>...\" to unstage)");
-       }
        color_fprintf_ln(s->fp, c, "#");
  }
  
@@@ -76,9 -77,8 +81,10 @@@ static void wt_status_print_dirty_heade
                                         int has_deleted)
  {
        const char *c = color(WT_STATUS_HEADER, s);
        color_fprintf_ln(s->fp, c, "# Changed but not updated:");
 +      if (!advice_status_hints)
 +              return;
        if (!has_deleted)
                color_fprintf_ln(s->fp, c, "#   (use \"git add <file>...\" to update what will be committed)");
        else
@@@ -91,8 -91,6 +97,8 @@@ static void wt_status_print_untracked_h
  {
        const char *c = color(WT_STATUS_HEADER, s);
        color_fprintf_ln(s->fp, c, "# Untracked files:");
 +      if (!advice_status_hints)
 +              return;
        color_fprintf_ln(s->fp, c, "#   (use \"git add <file>...\" to include in what will be committed)");
        color_fprintf_ln(s->fp, c, "#");
  }
@@@ -277,6 -275,7 +283,7 @@@ static void wt_status_collect_changes_w
        rev.diffopt.output_format |= DIFF_FORMAT_CALLBACK;
        rev.diffopt.format_callback = wt_status_collect_changed_cb;
        rev.diffopt.format_callback_data = s;
+       rev.prune_data = s->pathspec;
        run_diff_files(&rev, 0);
  }
  
@@@ -293,6 -292,7 +300,7 @@@ static void wt_status_collect_changes_i
        rev.diffopt.detect_rename = 1;
        rev.diffopt.rename_limit = 200;
        rev.diffopt.break_opt = 0;
+       rev.prune_data = s->pathspec;
        run_diff_index(&rev, 1);
  }
  
@@@ -305,6 -305,8 +313,8 @@@ static void wt_status_collect_changes_i
                struct wt_status_change_data *d;
                struct cache_entry *ce = active_cache[i];
  
+               if (!ce_path_match(ce, s->pathspec))
+                       continue;
                it = string_list_insert(ce->name, &s->change);
                d = it->util;
                if (!d) {
@@@ -334,10 -336,12 +344,12 @@@ static void wt_status_collect_untracked
        setup_standard_excludes(&dir);
  
        fill_directory(&dir, NULL);
 -      for(i = 0; i < dir.nr; i++) {
 +      for (i = 0; i < dir.nr; i++) {
                struct dir_entry *ent = dir.entries[i];
                if (!cache_name_is_other(ent->name, ent->len))
                        continue;
+               if (!match_pathspec(s->pathspec, ent->name, ent->len, 0, NULL))
+                       continue;
                s->workdir_untracked = 1;
                string_list_insert(ent->name, &s->untracked);
        }
@@@ -541,10 -545,8 +553,8 @@@ static void wt_status_print_tracking(st
  
  void wt_status_print(struct wt_status *s)
  {
-       unsigned char sha1[20];
        const char *branch_color = color(WT_STATUS_HEADER, s);
  
-       s->is_initial = get_sha1(s->reference, sha1) ? 1 : 0;
        if (s->branch) {
                const char *on_what = "On branch ";
                const char *branch_name = s->branch;
                        wt_status_print_tracking(s);
        }
  
-       wt_status_collect(s);
        if (s->is_initial) {
                color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "#");
                color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "# Initial commit");
                color_fprintf_ln(s->fp, color(WT_STATUS_HEADER, s), "#");
        }
  
 -      wt_status_print_unmerged(s);
        wt_status_print_updated(s);
 +      wt_status_print_unmerged(s);
        wt_status_print_changed(s);
        if (s->submodule_summary)
                wt_status_print_submodule_summary(s);
                        printf("nothing to commit (working directory clean)\n");
        }
  }
+ static void wt_shortstatus_unmerged(int null_termination, struct string_list_item *it,
+                          struct wt_status *s)
+ {
+       struct wt_status_change_data *d = it->util;
+       const char *how = "??";
+       switch (d->stagemask) {
+       case 1: how = "DD"; break; /* both deleted */
+       case 2: how = "AU"; break; /* added by us */
+       case 3: how = "UD"; break; /* deleted by them */
+       case 4: how = "UA"; break; /* added by them */
+       case 5: how = "DU"; break; /* deleted by us */
+       case 6: how = "AA"; break; /* both added */
+       case 7: how = "UU"; break; /* both modified */
+       }
+       color_fprintf(s->fp, color(WT_STATUS_UNMERGED, s), "%s", how);
+       if (null_termination) {
+               fprintf(stdout, " %s%c", it->string, 0);
+       } else {
+               struct strbuf onebuf = STRBUF_INIT;
+               const char *one;
+               one = quote_path(it->string, -1, &onebuf, s->prefix);
+               printf(" %s\n", one);
+               strbuf_release(&onebuf);
+       }
+ }
+ static void wt_shortstatus_status(int null_termination, struct string_list_item *it,
+                        struct wt_status *s)
+ {
+       struct wt_status_change_data *d = it->util;
+       if (d->index_status)
+               color_fprintf(s->fp, color(WT_STATUS_UPDATED, s), "%c", d->index_status);
+       else
+               putchar(' ');
+       if (d->worktree_status)
+               color_fprintf(s->fp, color(WT_STATUS_CHANGED, s), "%c", d->worktree_status);
+       else
+               putchar(' ');
+       putchar(' ');
+       if (null_termination) {
+               fprintf(stdout, "%s%c", it->string, 0);
+               if (d->head_path)
+                       fprintf(stdout, "%s%c", d->head_path, 0);
+       } else {
+               struct strbuf onebuf = STRBUF_INIT;
+               const char *one;
+               if (d->head_path) {
+                       one = quote_path(d->head_path, -1, &onebuf, s->prefix);
+                       printf("%s -> ", one);
+                       strbuf_release(&onebuf);
+               }
+               one = quote_path(it->string, -1, &onebuf, s->prefix);
+               printf("%s\n", one);
+               strbuf_release(&onebuf);
+       }
+ }
+ static void wt_shortstatus_untracked(int null_termination, struct string_list_item *it,
+                           struct wt_status *s)
+ {
+       if (null_termination) {
+               fprintf(stdout, "?? %s%c", it->string, 0);
+       } else {
+               struct strbuf onebuf = STRBUF_INIT;
+               const char *one;
+               one = quote_path(it->string, -1, &onebuf, s->prefix);
+               color_fprintf(s->fp, color(WT_STATUS_UNTRACKED, s), "??");
+               printf(" %s\n", one);
+               strbuf_release(&onebuf);
+       }
+ }
+ void wt_shortstatus_print(struct wt_status *s, int null_termination)
+ {
+       int i;
+       for (i = 0; i < s->change.nr; i++) {
+               struct wt_status_change_data *d;
+               struct string_list_item *it;
+               it = &(s->change.items[i]);
+               d = it->util;
+               if (d->stagemask)
+                       wt_shortstatus_unmerged(null_termination, it, s);
+               else
+                       wt_shortstatus_status(null_termination, it, s);
+       }
+       for (i = 0; i < s->untracked.nr; i++) {
+               struct string_list_item *it;
+               it = &(s->untracked.items[i]);
+               wt_shortstatus_untracked(null_termination, it, s);
+       }
+ }
+ void wt_porcelain_print(struct wt_status *s, int null_termination)
+ {
+       s->use_color = 0;
+       s->relative_paths = 0;
+       s->prefix = NULL;
+       wt_shortstatus_print(s, null_termination);
+ }