Merge branch 'jh/status-v2-porcelain'
authorJunio C Hamano <gitster@pobox.com>
Fri, 9 Sep 2016 04:49:50 +0000 (21:49 -0700)
committerJunio C Hamano <gitster@pobox.com>
Fri, 9 Sep 2016 04:49:50 +0000 (21:49 -0700)
Enhance "git status --porcelain" output by collecting more data on
the state of the index and the working tree files, which may
further be used to teach git-prompt (in contrib/) to make fewer
calls to git.

* jh/status-v2-porcelain:
status: unit tests for --porcelain=v2
test-lib-functions.sh: add lf_to_nul helper
git-status.txt: describe --porcelain=v2 format
status: print branch info with --porcelain=v2 --branch
status: print per-file porcelain v2 status data
status: collect per-file data for --porcelain=v2
status: support --porcelain[=<version>]
status: cleanup API to wt_status_print
status: rename long-format print routines

Documentation/git-status.txt
builtin/commit.c
t/t7060-wtstatus.sh
t/t7064-wtstatus-pv2.sh [new file with mode: 0755]
t/test-lib-functions.sh
wt-status.c
wt-status.h
index e1e8f57cdd217b43b9b04bc54381e9b155d9cbde..725065ef2d7b6b3c7c6c29a434a8996a7e1ae559 100644 (file)
@@ -32,11 +32,14 @@ OPTIONS
 --branch::
        Show the branch and tracking info even in short-format.
 
---porcelain::
+--porcelain[=<version>]::
        Give the output in an easy-to-parse format for scripts.
        This is similar to the short output, but will remain stable
        across Git versions and regardless of user configuration. See
        below for details.
++
+The version parameter is used to specify the format version.
+This is optional and defaults to the original version 'v1' format.
 
 --long::
        Give the output in the long-format. This is the default.
@@ -96,7 +99,7 @@ configuration variable documented in linkgit:git-config[1].
 
 -z::
        Terminate entries with NUL, instead of LF.  This implies
-       the `--porcelain` output format if no other format is given.
+       the `--porcelain=v1` output format if no other format is given.
 
 --column[=<options>]::
 --no-column::
@@ -180,12 +183,12 @@ in which case `XY` are `!!`.
 
 If -b is used the short-format status is preceded by a line
 
-## branchname tracking info
+    ## branchname tracking info
 
-Porcelain Format
-~~~~~~~~~~~~~~~~
+Porcelain Format Version 1
+~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-The porcelain format is similar to the short format, but is guaranteed
+Version 1 porcelain format is similar to the short format, but is guaranteed
 not to change in a backwards-incompatible way between Git versions or
 based on user configuration. This makes it ideal for parsing by scripts.
 The description of the short format above also describes the porcelain
@@ -207,6 +210,124 @@ field from the first filename).  Third, filenames containing special
 characters are not specially formatted; no quoting or
 backslash-escaping is performed.
 
+Porcelain Format Version 2
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Version 2 format adds more detailed information about the state of
+the worktree and changed items.  Version 2 also defines an extensible
+set of easy to parse optional headers.
+
+Header lines start with "#" and are added in response to specific
+command line arguments.  Parsers should ignore headers they
+don't recognize.
+
+### Branch Headers
+
+If `--branch` is given, a series of header lines are printed with
+information about the current branch.
+
+    Line                                     Notes
+    ------------------------------------------------------------
+    # branch.oid <commit> | (initial)        Current commit.
+    # branch.head <branch> | (detached)      Current branch.
+    # branch.upstream <upstream_branch>      If upstream is set.
+    # branch.ab +<ahead> -<behind>           If upstream is set and
+                                            the commit is present.
+    ------------------------------------------------------------
+
+### Changed Tracked Entries
+
+Following the headers, a series of lines are printed for tracked
+entries.  One of three different line formats may be used to describe
+an entry depending on the type of change.  Tracked entries are printed
+in an undefined order; parsers should allow for a mixture of the 3
+line types in any order.
+
+Ordinary changed entries have the following format:
+
+    1 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <path>
+
+Renamed or copied entries have the following format:
+
+    2 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <X><score> <path><sep><origPath>
+
+    Field       Meaning
+    --------------------------------------------------------
+    <XY>        A 2 character field containing the staged and
+               unstaged XY values described in the short format,
+               with unchanged indicated by a "." rather than
+               a space.
+    <sub>       A 4 character field describing the submodule state.
+               "N..." when the entry is not a submodule.
+               "S<c><m><u>" when the entry is a submodule.
+               <c> is "C" if the commit changed; otherwise ".".
+               <m> is "M" if it has tracked changes; otherwise ".".
+               <u> is "U" if there are untracked changes; otherwise ".".
+    <mH>        The octal file mode in HEAD.
+    <mI>        The octal file mode in the index.
+    <mW>        The octal file mode in the worktree.
+    <hH>        The object name in HEAD.
+    <hI>        The object name in the index.
+    <X><score>  The rename or copy score (denoting the percentage
+               of similarity between the source and target of the
+               move or copy). For example "R100" or "C75".
+    <path>      The pathname.  In a renamed/copied entry, this
+               is the path in the index and in the working tree.
+    <sep>       When the `-z` option is used, the 2 pathnames are separated
+               with a NUL (ASCII 0x00) byte; otherwise, a tab (ASCII 0x09)
+               byte separates them.
+    <origPath>  The pathname in the commit at HEAD.  This is only
+               present in a renamed/copied entry, and tells
+               where the renamed/copied contents came from.
+    --------------------------------------------------------
+
+Unmerged entries have the following format; the first character is
+a "u" to distinguish from ordinary changed entries.
+
+    u <xy> <sub> <m1> <m2> <m3> <mW> <h1> <h2> <h3> <path>
+
+    Field       Meaning
+    --------------------------------------------------------
+    <XY>        A 2 character field describing the conflict type
+               as described in the short format.
+    <sub>       A 4 character field describing the submodule state
+               as described above.
+    <m1>        The octal file mode in stage 1.
+    <m2>        The octal file mode in stage 2.
+    <m3>        The octal file mode in stage 3.
+    <mW>        The octal file mode in the worktree.
+    <h1>        The object name in stage 1.
+    <h2>        The object name in stage 2.
+    <h3>        The object name in stage 3.
+    <path>      The pathname.
+    --------------------------------------------------------
+
+### Other Items
+
+Following the tracked entries (and if requested), a series of
+lines will be printed for untracked and then ignored items
+found in the worktree.
+
+Untracked items have the following format:
+
+    ? <path>
+
+Ignored items have the following format:
+
+    ! <path>
+
+### Pathname Format Notes and -z
+
+When the `-z` option is given, pathnames are printed as is and
+without any quoting and lines are terminated with a NUL (ASCII 0x00)
+byte.
+
+Otherwise, all pathnames will be "C-quoted" if they contain any tab,
+linefeed, double quote, or backslash characters. In C-quoting, these
+characters will be replaced with the corresponding C-style escape
+sequences and the resulting pathname will be double quoted.
+
+
 CONFIGURATION
 -------------
 
index 77e3dc849419e697abe8f2f4c7d753cce17634a8..bb9f79b6ef4b5cd01cafac551bb6665a433334aa 100644 (file)
@@ -142,14 +142,24 @@ static int show_ignored_in_status, have_option_m;
 static const char *only_include_assumed;
 static struct strbuf message = STRBUF_INIT;
 
-static enum status_format {
-       STATUS_FORMAT_NONE = 0,
-       STATUS_FORMAT_LONG,
-       STATUS_FORMAT_SHORT,
-       STATUS_FORMAT_PORCELAIN,
+static enum wt_status_format status_format = STATUS_FORMAT_UNSPECIFIED;
 
-       STATUS_FORMAT_UNSPECIFIED
-} status_format = STATUS_FORMAT_UNSPECIFIED;
+static int opt_parse_porcelain(const struct option *opt, const char *arg, int unset)
+{
+       enum wt_status_format *value = (enum wt_status_format *)opt->value;
+       if (unset)
+               *value = STATUS_FORMAT_NONE;
+       else if (!arg)
+               *value = STATUS_FORMAT_PORCELAIN;
+       else if (!strcmp(arg, "v1") || !strcmp(arg, "1"))
+               *value = STATUS_FORMAT_PORCELAIN;
+       else if (!strcmp(arg, "v2") || !strcmp(arg, "2"))
+               *value = STATUS_FORMAT_PORCELAIN_V2;
+       else
+               die("unsupported porcelain version '%s'", arg);
+
+       return 0;
+}
 
 static int opt_parse_m(const struct option *opt, const char *arg, int unset)
 {
@@ -500,24 +510,13 @@ static int run_status(FILE *fp, const char *index_file, const char *prefix, int
        s->fp = fp;
        s->nowarn = nowarn;
        s->is_initial = get_sha1(s->reference, sha1) ? 1 : 0;
+       if (!s->is_initial)
+               hashcpy(s->sha1_commit, sha1);
+       s->status_format = status_format;
+       s->ignore_submodule_arg = ignore_submodule_arg;
 
        wt_status_collect(s);
-
-       switch (status_format) {
-       case STATUS_FORMAT_SHORT:
-               wt_shortstatus_print(s);
-               break;
-       case STATUS_FORMAT_PORCELAIN:
-               wt_porcelain_print(s);
-               break;
-       case STATUS_FORMAT_UNSPECIFIED:
-               die("BUG: finalize_deferred_config() should have been called");
-               break;
-       case STATUS_FORMAT_NONE:
-       case STATUS_FORMAT_LONG:
-               wt_status_print(s);
-               break;
-       }
+       wt_status_print(s);
 
        return s->commitable;
 }
@@ -1099,7 +1098,7 @@ static const char *read_commit_message(const char *name)
  * is not in effect here.
  */
 static struct status_deferred_config {
-       enum status_format status_format;
+       enum wt_status_format status_format;
        int show_branch;
 } status_deferred_config = {
        STATUS_FORMAT_UNSPECIFIED,
@@ -1109,6 +1108,7 @@ static struct status_deferred_config {
 static void finalize_deferred_config(struct wt_status *s)
 {
        int use_deferred_config = (status_format != STATUS_FORMAT_PORCELAIN &&
+                                  status_format != STATUS_FORMAT_PORCELAIN_V2 &&
                                   !s->null_termination);
 
        if (s->null_termination) {
@@ -1336,9 +1336,9 @@ int cmd_status(int argc, const char **argv, const char *prefix)
                            N_("show status concisely"), STATUS_FORMAT_SHORT),
                OPT_BOOL('b', "branch", &s.show_branch,
                         N_("show branch information")),
-               OPT_SET_INT(0, "porcelain", &status_format,
-                           N_("machine-readable output"),
-                           STATUS_FORMAT_PORCELAIN),
+               { OPTION_CALLBACK, 0, "porcelain", &status_format,
+                 N_("version"), N_("machine-readable output"),
+                 PARSE_OPT_OPTARG, opt_parse_porcelain },
                OPT_SET_INT(0, "long", &status_format,
                            N_("show status in long format (default)"),
                            STATUS_FORMAT_LONG),
@@ -1380,7 +1380,13 @@ int cmd_status(int argc, const char **argv, const char *prefix)
        fd = hold_locked_index(&index_lock, 0);
 
        s.is_initial = get_sha1(s.reference, sha1) ? 1 : 0;
+       if (!s.is_initial)
+               hashcpy(s.sha1_commit, sha1);
+
        s.ignore_submodule_arg = ignore_submodule_arg;
+       s.status_format = status_format;
+       s.verbose = verbose;
+
        wt_status_collect(&s);
 
        if (0 <= fd)
@@ -1389,23 +1395,7 @@ int cmd_status(int argc, const char **argv, const char *prefix)
        if (s.relative_paths)
                s.prefix = prefix;
 
-       switch (status_format) {
-       case STATUS_FORMAT_SHORT:
-               wt_shortstatus_print(&s);
-               break;
-       case STATUS_FORMAT_PORCELAIN:
-               wt_porcelain_print(&s);
-               break;
-       case STATUS_FORMAT_UNSPECIFIED:
-               die("BUG: finalize_deferred_config() should have been called");
-               break;
-       case STATUS_FORMAT_NONE:
-       case STATUS_FORMAT_LONG:
-               s.verbose = verbose;
-               s.ignore_submodule_arg = ignore_submodule_arg;
-               wt_status_print(&s);
-               break;
-       }
+       wt_status_print(&s);
        return 0;
 }
 
index 4d17363a926c8938c6c8d78e997a53ffb81b3c6c..53cf42fac19c83f2da7de1cd5b6c4ca0cf6e5ccf 100755 (executable)
@@ -232,4 +232,25 @@ test_expect_success 'status --branch with detached HEAD' '
        test_i18ncmp expected actual
 '
 
+## Duplicate the above test and verify --porcelain=v1 arg parsing.
+test_expect_success 'status --porcelain=v1 --branch with detached HEAD' '
+       git reset --hard &&
+       git checkout master^0 &&
+       git status --branch --porcelain=v1 >actual &&
+       cat >expected <<-EOF &&
+       ## HEAD (no branch)
+       ?? .gitconfig
+       ?? actual
+       ?? expect
+       ?? expected
+       ?? mdconflict/
+       EOF
+       test_i18ncmp expected actual
+'
+
+## Verify parser error on invalid --porcelain argument.
+test_expect_success 'status --porcelain=bogus' '
+       test_must_fail git status --porcelain=bogus
+'
+
 test_done
diff --git a/t/t7064-wtstatus-pv2.sh b/t/t7064-wtstatus-pv2.sh
new file mode 100755 (executable)
index 0000000..3012a4d
--- /dev/null
@@ -0,0 +1,593 @@
+#!/bin/sh
+
+test_description='git status --porcelain=v2
+
+This test exercises porcelain V2 output for git status.'
+
+
+. ./test-lib.sh
+
+
+test_expect_success setup '
+       test_tick &&
+       git config core.autocrlf false &&
+       echo x >file_x &&
+       echo y >file_y &&
+       echo z >file_z &&
+       mkdir dir1 &&
+       echo a >dir1/file_a &&
+       echo b >dir1/file_b
+'
+
+test_expect_success 'before initial commit, nothing added, only untracked' '
+       cat >expect <<-EOF &&
+       # branch.oid (initial)
+       # branch.head master
+       ? actual
+       ? dir1/
+       ? expect
+       ? file_x
+       ? file_y
+       ? file_z
+       EOF
+
+       git status --porcelain=v2 --branch --untracked-files=normal >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'before initial commit, things added' '
+       git add file_x file_y file_z dir1 &&
+       OID_A=$(git hash-object -t blob -- dir1/file_a) &&
+       OID_B=$(git hash-object -t blob -- dir1/file_b) &&
+       OID_X=$(git hash-object -t blob -- file_x) &&
+       OID_Y=$(git hash-object -t blob -- file_y) &&
+       OID_Z=$(git hash-object -t blob -- file_z) &&
+
+       cat >expect <<-EOF &&
+       # branch.oid (initial)
+       # branch.head master
+       1 A. N... 000000 100644 100644 $_z40 $OID_A dir1/file_a
+       1 A. N... 000000 100644 100644 $_z40 $OID_B dir1/file_b
+       1 A. N... 000000 100644 100644 $_z40 $OID_X file_x
+       1 A. N... 000000 100644 100644 $_z40 $OID_Y file_y
+       1 A. N... 000000 100644 100644 $_z40 $OID_Z file_z
+       ? actual
+       ? expect
+       EOF
+
+       git status --porcelain=v2 --branch --untracked-files=all >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'before initial commit, things added (-z)' '
+       lf_to_nul >expect <<-EOF &&
+       # branch.oid (initial)
+       # branch.head master
+       1 A. N... 000000 100644 100644 $_z40 $OID_A dir1/file_a
+       1 A. N... 000000 100644 100644 $_z40 $OID_B dir1/file_b
+       1 A. N... 000000 100644 100644 $_z40 $OID_X file_x
+       1 A. N... 000000 100644 100644 $_z40 $OID_Y file_y
+       1 A. N... 000000 100644 100644 $_z40 $OID_Z file_z
+       ? actual
+       ? expect
+       EOF
+
+       git status -z --porcelain=v2 --branch --untracked-files=all >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'make first commit, comfirm HEAD oid and branch' '
+       git commit -m initial &&
+       H0=$(git rev-parse HEAD) &&
+       cat >expect <<-EOF &&
+       # branch.oid $H0
+       # branch.head master
+       ? actual
+       ? expect
+       EOF
+
+       git status --porcelain=v2 --branch --untracked-files=all >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'after first commit, create unstaged changes' '
+       echo x >>file_x &&
+       OID_X1=$(git hash-object -t blob -- file_x) &&
+       rm file_z &&
+       H0=$(git rev-parse HEAD) &&
+
+       cat >expect <<-EOF &&
+       # branch.oid $H0
+       # branch.head master
+       1 .M N... 100644 100644 100644 $OID_X $OID_X file_x
+       1 .D N... 100644 100644 000000 $OID_Z $OID_Z file_z
+       ? actual
+       ? expect
+       EOF
+
+       git status --porcelain=v2 --branch --untracked-files=all >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'after first commit but omit untracked files and branch' '
+       cat >expect <<-EOF &&
+       1 .M N... 100644 100644 100644 $OID_X $OID_X file_x
+       1 .D N... 100644 100644 000000 $OID_Z $OID_Z file_z
+       EOF
+
+       git status --porcelain=v2 --untracked-files=no >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'after first commit, stage existing changes' '
+       git add file_x &&
+       git rm file_z &&
+       H0=$(git rev-parse HEAD) &&
+
+       cat >expect <<-EOF &&
+       # branch.oid $H0
+       # branch.head master
+       1 M. N... 100644 100644 100644 $OID_X $OID_X1 file_x
+       1 D. N... 100644 000000 000000 $OID_Z $_z40 file_z
+       ? actual
+       ? expect
+       EOF
+
+       git status --porcelain=v2 --branch --untracked-files=all >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'rename causes 2 path lines' '
+       git mv file_y renamed_y &&
+       H0=$(git rev-parse HEAD) &&
+
+       q_to_tab >expect <<-EOF &&
+       # branch.oid $H0
+       # branch.head master
+       1 M. N... 100644 100644 100644 $OID_X $OID_X1 file_x
+       1 D. N... 100644 000000 000000 $OID_Z $_z40 file_z
+       2 R. N... 100644 100644 100644 $OID_Y $OID_Y R100 renamed_yQfile_y
+       ? actual
+       ? expect
+       EOF
+
+       git status --porcelain=v2 --branch --untracked-files=all >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'rename causes 2 path lines (-z)' '
+       H0=$(git rev-parse HEAD) &&
+
+       ## Lines use NUL path separator and line terminator, so double transform here.
+       q_to_nul <<-EOF | lf_to_nul >expect &&
+       # branch.oid $H0
+       # branch.head master
+       1 M. N... 100644 100644 100644 $OID_X $OID_X1 file_x
+       1 D. N... 100644 000000 000000 $OID_Z $_z40 file_z
+       2 R. N... 100644 100644 100644 $OID_Y $OID_Y R100 renamed_yQfile_y
+       ? actual
+       ? expect
+       EOF
+
+       git status --porcelain=v2 --branch --untracked-files=all -z >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'make second commit, confirm clean and new HEAD oid' '
+       git commit -m second &&
+       H1=$(git rev-parse HEAD) &&
+
+       cat >expect <<-EOF &&
+       # branch.oid $H1
+       # branch.head master
+       ? actual
+       ? expect
+       EOF
+
+       git status --porcelain=v2 --branch --untracked-files=all >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'confirm ignored files are not printed' '
+       test_when_finished "rm -f x.ign .gitignore" &&
+       echo x.ign >.gitignore &&
+       echo "ignore me" >x.ign &&
+
+       cat >expect <<-EOF &&
+       ? .gitignore
+       ? actual
+       ? expect
+       EOF
+
+       git status --porcelain=v2 --untracked-files=all >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'ignored files are printed with --ignored' '
+       test_when_finished "rm -f x.ign .gitignore" &&
+       echo x.ign >.gitignore &&
+       echo "ignore me" >x.ign &&
+
+       cat >expect <<-EOF &&
+       ? .gitignore
+       ? actual
+       ? expect
+       ! x.ign
+       EOF
+
+       git status --porcelain=v2 --ignored --untracked-files=all >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'create and commit permanent ignore file' '
+       cat >.gitignore <<-EOF &&
+       actual*
+       expect*
+       EOF
+
+       git add .gitignore &&
+       git commit -m ignore_trash &&
+       H1=$(git rev-parse HEAD) &&
+
+       cat >expect <<-EOF &&
+       # branch.oid $H1
+       # branch.head master
+       EOF
+
+       git status --porcelain=v2 --branch >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'verify --intent-to-add output' '
+       test_when_finished "git rm -f intent1.add intent2.add" &&
+       touch intent1.add &&
+       echo test >intent2.add &&
+
+       git add --intent-to-add intent1.add intent2.add &&
+
+       cat >expect <<-EOF &&
+       1 AM N... 000000 100644 100644 $_z40 $EMPTY_BLOB intent1.add
+       1 AM N... 000000 100644 100644 $_z40 $EMPTY_BLOB intent2.add
+       EOF
+
+       git status --porcelain=v2 >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'verify AA (add-add) conflict' '
+       test_when_finished "git reset --hard" &&
+
+       git branch AA_A master &&
+       git checkout AA_A &&
+       echo "Branch AA_A" >conflict.txt &&
+       OID_AA_A=$(git hash-object -t blob -- conflict.txt) &&
+       git add conflict.txt &&
+       git commit -m "branch aa_a" &&
+
+       git branch AA_B master &&
+       git checkout AA_B &&
+       echo "Branch AA_B" >conflict.txt &&
+       OID_AA_B=$(git hash-object -t blob -- conflict.txt) &&
+       git add conflict.txt &&
+       git commit -m "branch aa_b" &&
+
+       git branch AA_M AA_B &&
+       git checkout AA_M &&
+       test_must_fail git merge AA_A &&
+
+       HM=$(git rev-parse HEAD) &&
+
+       cat >expect <<-EOF &&
+       # branch.oid $HM
+       # branch.head AA_M
+       u AA N... 000000 100644 100644 100644 $_z40 $OID_AA_B $OID_AA_A conflict.txt
+       EOF
+
+       git status --porcelain=v2 --branch --untracked-files=all >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'verify UU (edit-edit) conflict' '
+       test_when_finished "git reset --hard" &&
+
+       git branch UU_ANC master &&
+       git checkout UU_ANC &&
+       echo "Ancestor" >conflict.txt &&
+       OID_UU_ANC=$(git hash-object -t blob -- conflict.txt) &&
+       git add conflict.txt &&
+       git commit -m "UU_ANC" &&
+
+       git branch UU_A UU_ANC &&
+       git checkout UU_A &&
+       echo "Branch UU_A" >conflict.txt &&
+       OID_UU_A=$(git hash-object -t blob -- conflict.txt) &&
+       git add conflict.txt &&
+       git commit -m "branch uu_a" &&
+
+       git branch UU_B UU_ANC &&
+       git checkout UU_B &&
+       echo "Branch UU_B" >conflict.txt &&
+       OID_UU_B=$(git hash-object -t blob -- conflict.txt) &&
+       git add conflict.txt &&
+       git commit -m "branch uu_b" &&
+
+       git branch UU_M UU_B &&
+       git checkout UU_M &&
+       test_must_fail git merge UU_A &&
+
+       HM=$(git rev-parse HEAD) &&
+
+       cat >expect <<-EOF &&
+       # branch.oid $HM
+       # branch.head UU_M
+       u UU N... 100644 100644 100644 100644 $OID_UU_ANC $OID_UU_B $OID_UU_A conflict.txt
+       EOF
+
+       git status --porcelain=v2 --branch --untracked-files=all >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'verify upstream fields in branch header' '
+       git checkout master &&
+       test_when_finished "rm -rf sub_repo" &&
+       git clone . sub_repo &&
+       (
+               ## Confirm local master tracks remote master.
+               cd sub_repo &&
+               HUF=$(git rev-parse HEAD) &&
+
+               cat >expect <<-EOF &&
+               # branch.oid $HUF
+               # branch.head master
+               # branch.upstream origin/master
+               # branch.ab +0 -0
+               EOF
+
+               git status --porcelain=v2 --branch --untracked-files=all >actual &&
+               test_cmp expect actual &&
+
+               ## Test ahead/behind.
+               echo xyz >file_xyz &&
+               git add file_xyz &&
+               git commit -m xyz &&
+
+               HUF=$(git rev-parse HEAD) &&
+
+               cat >expect <<-EOF &&
+               # branch.oid $HUF
+               # branch.head master
+               # branch.upstream origin/master
+               # branch.ab +1 -0
+               EOF
+
+               git status --porcelain=v2 --branch --untracked-files=all >actual &&
+               test_cmp expect actual &&
+
+               ## Repeat the above but without --branch.
+               cat >expect <<-EOF &&
+               EOF
+
+               git status --porcelain=v2 --untracked-files=all >actual &&
+               test_cmp expect actual &&
+
+               ## Test upstream-gone case. Fake this by pointing origin/master at
+               ## a non-existing commit.
+               OLD=$(git rev-parse origin/master) &&
+               NEW=$_z40 &&
+               mv .git/packed-refs .git/old-packed-refs &&
+               sed "s/$OLD/$NEW/g" <.git/old-packed-refs >.git/packed-refs &&
+
+               HUF=$(git rev-parse HEAD) &&
+
+               cat >expect <<-EOF &&
+               # branch.oid $HUF
+               # branch.head master
+               # branch.upstream origin/master
+               EOF
+
+               git status --porcelain=v2 --branch --untracked-files=all >actual &&
+               test_cmp expect actual
+       )
+'
+
+test_expect_success 'create and add submodule, submodule appears clean (A. S...)' '
+       git checkout master &&
+       git clone . sub_repo &&
+       git clone . super_repo &&
+       (       cd super_repo &&
+               git submodule add ../sub_repo sub1 &&
+
+               ## Confirm stage/add of clean submodule.
+               HMOD=$(git hash-object -t blob -- .gitmodules) &&
+               HSUP=$(git rev-parse HEAD) &&
+               HSUB=$HSUP &&
+
+               cat >expect <<-EOF &&
+               # branch.oid $HSUP
+               # branch.head master
+               # branch.upstream origin/master
+               # branch.ab +0 -0
+               1 A. N... 000000 100644 100644 $_z40 $HMOD .gitmodules
+               1 A. S... 000000 160000 160000 $_z40 $HSUB sub1
+               EOF
+
+               git status --porcelain=v2 --branch --untracked-files=all >actual &&
+               test_cmp expect actual
+       )
+'
+
+test_expect_success 'untracked changes in added submodule (AM S..U)' '
+       (       cd super_repo &&
+               ## create untracked file in the submodule.
+               (       cd sub1 &&
+                       echo "xxxx" >file_in_sub
+               ) &&
+
+               HMOD=$(git hash-object -t blob -- .gitmodules) &&
+               HSUP=$(git rev-parse HEAD) &&
+               HSUB=$HSUP &&
+
+               cat >expect <<-EOF &&
+               # branch.oid $HSUP
+               # branch.head master
+               # branch.upstream origin/master
+               # branch.ab +0 -0
+               1 A. N... 000000 100644 100644 $_z40 $HMOD .gitmodules
+               1 AM S..U 000000 160000 160000 $_z40 $HSUB sub1
+               EOF
+
+               git status --porcelain=v2 --branch --untracked-files=all >actual &&
+               test_cmp expect actual
+       )
+'
+
+test_expect_success 'staged changes in added submodule (AM S.M.)' '
+       (       cd super_repo &&
+               ## stage the changes in the submodule.
+               (       cd sub1 &&
+                       git add file_in_sub
+               ) &&
+
+               HMOD=$(git hash-object -t blob -- .gitmodules) &&
+               HSUP=$(git rev-parse HEAD) &&
+               HSUB=$HSUP &&
+
+               cat >expect <<-EOF &&
+               # branch.oid $HSUP
+               # branch.head master
+               # branch.upstream origin/master
+               # branch.ab +0 -0
+               1 A. N... 000000 100644 100644 $_z40 $HMOD .gitmodules
+               1 AM S.M. 000000 160000 160000 $_z40 $HSUB sub1
+               EOF
+
+               git status --porcelain=v2 --branch --untracked-files=all >actual &&
+               test_cmp expect actual
+       )
+'
+
+test_expect_success 'staged and unstaged changes in added (AM S.M.)' '
+       (       cd super_repo &&
+               (       cd sub1 &&
+                       ## make additional unstaged changes (on the same file) in the submodule.
+                       ## This does not cause us to get S.MU (because the submodule does not report
+                       ## a "?" line for the unstaged changes).
+                       echo "more changes" >>file_in_sub
+               ) &&
+
+               HMOD=$(git hash-object -t blob -- .gitmodules) &&
+               HSUP=$(git rev-parse HEAD) &&
+               HSUB=$HSUP &&
+
+               cat >expect <<-EOF &&
+               # branch.oid $HSUP
+               # branch.head master
+               # branch.upstream origin/master
+               # branch.ab +0 -0
+               1 A. N... 000000 100644 100644 $_z40 $HMOD .gitmodules
+               1 AM S.M. 000000 160000 160000 $_z40 $HSUB sub1
+               EOF
+
+               git status --porcelain=v2 --branch --untracked-files=all >actual &&
+               test_cmp expect actual
+       )
+'
+
+test_expect_success 'staged and untracked changes in added submodule (AM S.MU)' '
+       (       cd super_repo &&
+               (       cd sub1 &&
+                       ## stage new changes in tracked file.
+                       git add file_in_sub &&
+                       ## create new untracked file.
+                       echo "yyyy" >>another_file_in_sub
+               ) &&
+
+               HMOD=$(git hash-object -t blob -- .gitmodules) &&
+               HSUP=$(git rev-parse HEAD) &&
+               HSUB=$HSUP &&
+
+               cat >expect <<-EOF &&
+               # branch.oid $HSUP
+               # branch.head master
+               # branch.upstream origin/master
+               # branch.ab +0 -0
+               1 A. N... 000000 100644 100644 $_z40 $HMOD .gitmodules
+               1 AM S.MU 000000 160000 160000 $_z40 $HSUB sub1
+               EOF
+
+               git status --porcelain=v2 --branch --untracked-files=all >actual &&
+               test_cmp expect actual
+       )
+'
+
+test_expect_success 'commit within the submodule appears as new commit in super (AM SC..)' '
+       (       cd super_repo &&
+               (       cd sub1 &&
+                       ## Make a new commit in the submodule.
+                       git add file_in_sub &&
+                       rm -f another_file_in_sub &&
+                       git commit -m "new commit"
+               ) &&
+
+               HMOD=$(git hash-object -t blob -- .gitmodules) &&
+               HSUP=$(git rev-parse HEAD) &&
+               HSUB=$HSUP &&
+
+               cat >expect <<-EOF &&
+               # branch.oid $HSUP
+               # branch.head master
+               # branch.upstream origin/master
+               # branch.ab +0 -0
+               1 A. N... 000000 100644 100644 $_z40 $HMOD .gitmodules
+               1 AM SC.. 000000 160000 160000 $_z40 $HSUB sub1
+               EOF
+
+               git status --porcelain=v2 --branch --untracked-files=all >actual &&
+               test_cmp expect actual
+       )
+'
+
+test_expect_success 'stage submodule in super and commit' '
+       (       cd super_repo &&
+               ## Stage the new submodule commit in the super.
+               git add sub1 &&
+               ## Commit the super so that the sub no longer appears as added.
+               git commit -m "super commit" &&
+
+               HSUP=$(git rev-parse HEAD) &&
+
+               cat >expect <<-EOF &&
+               # branch.oid $HSUP
+               # branch.head master
+               # branch.upstream origin/master
+               # branch.ab +1 -0
+               EOF
+
+               git status --porcelain=v2 --branch --untracked-files=all >actual &&
+               test_cmp expect actual
+       )
+'
+
+test_expect_success 'make unstaged changes in existing submodule (.M S.M.)' '
+       (       cd super_repo &&
+               (       cd sub1 &&
+                       echo "zzzz" >>file_in_sub
+               ) &&
+
+               HSUP=$(git rev-parse HEAD) &&
+               HSUB=$(cd sub1 && git rev-parse HEAD) &&
+
+               cat >expect <<-EOF &&
+               # branch.oid $HSUP
+               # branch.head master
+               # branch.upstream origin/master
+               # branch.ab +1 -0
+               1 .M S.M. 160000 160000 160000 $HSUB $HSUB sub1
+               EOF
+
+               git status --porcelain=v2 --branch --untracked-files=all >actual &&
+               test_cmp expect actual
+       )
+'
+
+test_done
index 4f7eadb5963e7d698f9ee6e265b1657e543cdb44..fdaeb3a96bed361f53030cb6ea5c6bdde445163f 100644 (file)
@@ -81,6 +81,10 @@ test_decode_color () {
        '
 }
 
+lf_to_nul () {
+       perl -pe 'y/\012/\000/'
+}
+
 nul_to_q () {
        perl -pe 'y/\000/Q/'
 }
index 6225a2d89f2c38bea6ed64295a28e7e07d19522c..539aac15a37adcce6429c4d82f401a7db17fac28 100644 (file)
@@ -139,7 +139,7 @@ void wt_status_prepare(struct wt_status *s)
        s->display_comment_prefix = 0;
 }
 
-static void wt_status_print_unmerged_header(struct wt_status *s)
+static void wt_longstatus_print_unmerged_header(struct wt_status *s)
 {
        int i;
        int del_mod_conflict = 0;
@@ -191,7 +191,7 @@ static void wt_status_print_unmerged_header(struct wt_status *s)
        status_printf_ln(s, c, "%s", "");
 }
 
-static void wt_status_print_cached_header(struct wt_status *s)
+static void wt_longstatus_print_cached_header(struct wt_status *s)
 {
        const char *c = color(WT_STATUS_HEADER, s);
 
@@ -207,9 +207,9 @@ static void wt_status_print_cached_header(struct wt_status *s)
        status_printf_ln(s, c, "%s", "");
 }
 
-static void wt_status_print_dirty_header(struct wt_status *s,
-                                        int has_deleted,
-                                        int has_dirty_submodules)
+static void wt_longstatus_print_dirty_header(struct wt_status *s,
+                                            int has_deleted,
+                                            int has_dirty_submodules)
 {
        const char *c = color(WT_STATUS_HEADER, s);
 
@@ -226,9 +226,9 @@ static void wt_status_print_dirty_header(struct wt_status *s,
        status_printf_ln(s, c, "%s", "");
 }
 
-static void wt_status_print_other_header(struct wt_status *s,
-                                        const char *what,
-                                        const char *how)
+static void wt_longstatus_print_other_header(struct wt_status *s,
+                                            const char *what,
+                                            const char *how)
 {
        const char *c = color(WT_STATUS_HEADER, s);
        status_printf_ln(s, c, "%s:", what);
@@ -238,7 +238,7 @@ static void wt_status_print_other_header(struct wt_status *s,
        status_printf_ln(s, c, "%s", "");
 }
 
-static void wt_status_print_trailer(struct wt_status *s)
+static void wt_longstatus_print_trailer(struct wt_status *s)
 {
        status_printf_ln(s, color(WT_STATUS_HEADER, s), "%s", "");
 }
@@ -304,8 +304,8 @@ static int maxwidth(const char *(*label)(int), int minval, int maxval)
        return result;
 }
 
-static void wt_status_print_unmerged_data(struct wt_status *s,
-                                         struct string_list_item *it)
+static void wt_longstatus_print_unmerged_data(struct wt_status *s,
+                                             struct string_list_item *it)
 {
        const char *c = color(WT_STATUS_UNMERGED, s);
        struct wt_status_change_data *d = it->util;
@@ -331,9 +331,9 @@ static void wt_status_print_unmerged_data(struct wt_status *s,
        strbuf_release(&onebuf);
 }
 
-static void wt_status_print_change_data(struct wt_status *s,
-                                       int change_type,
-                                       struct string_list_item *it)
+static void wt_longstatus_print_change_data(struct wt_status *s,
+                                           int change_type,
+                                           struct string_list_item *it)
 {
        struct wt_status_change_data *d = it->util;
        const char *c = color(change_type, s);
@@ -378,7 +378,7 @@ static void wt_status_print_change_data(struct wt_status *s,
                status = d->worktree_status;
                break;
        default:
-               die("BUG: unhandled change_type %d in wt_status_print_change_data",
+               die("BUG: unhandled change_type %d in wt_longstatus_print_change_data",
                    change_type);
        }
 
@@ -434,6 +434,31 @@ static void wt_status_collect_changed_cb(struct diff_queue_struct *q,
                if (S_ISGITLINK(p->two->mode))
                        d->new_submodule_commits = !!oidcmp(&p->one->oid,
                                                            &p->two->oid);
+
+               switch (p->status) {
+               case DIFF_STATUS_ADDED:
+                       die("BUG: worktree status add???");
+                       break;
+
+               case DIFF_STATUS_DELETED:
+                       d->mode_index = p->one->mode;
+                       oidcpy(&d->oid_index, &p->one->oid);
+                       /* mode_worktree is zero for a delete. */
+                       break;
+
+               case DIFF_STATUS_MODIFIED:
+               case DIFF_STATUS_TYPE_CHANGED:
+               case DIFF_STATUS_UNMERGED:
+                       d->mode_index = p->one->mode;
+                       d->mode_worktree = p->two->mode;
+                       oidcpy(&d->oid_index, &p->one->oid);
+                       break;
+
+               case DIFF_STATUS_UNKNOWN:
+                       die("BUG: worktree status unknown???");
+                       break;
+               }
+
        }
 }
 
@@ -479,12 +504,36 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,
                if (!d->index_status)
                        d->index_status = p->status;
                switch (p->status) {
+               case DIFF_STATUS_ADDED:
+                       /* Leave {mode,oid}_head zero for an add. */
+                       d->mode_index = p->two->mode;
+                       oidcpy(&d->oid_index, &p->two->oid);
+                       break;
+               case DIFF_STATUS_DELETED:
+                       d->mode_head = p->one->mode;
+                       oidcpy(&d->oid_head, &p->one->oid);
+                       /* Leave {mode,oid}_index zero for a delete. */
+                       break;
+
                case DIFF_STATUS_COPIED:
                case DIFF_STATUS_RENAMED:
                        d->head_path = xstrdup(p->one->path);
+                       d->score = p->score * 100 / MAX_SCORE;
+                       /* fallthru */
+               case DIFF_STATUS_MODIFIED:
+               case DIFF_STATUS_TYPE_CHANGED:
+                       d->mode_head = p->one->mode;
+                       d->mode_index = p->two->mode;
+                       oidcpy(&d->oid_head, &p->one->oid);
+                       oidcpy(&d->oid_index, &p->two->oid);
                        break;
                case DIFF_STATUS_UNMERGED:
                        d->stagemask = unmerged_mask(p->two->path);
+                       /*
+                        * Don't bother setting {mode,oid}_{head,index} since the print
+                        * code will output the stage values directly and not use the
+                        * values in these fields.
+                        */
                        break;
                }
        }
@@ -565,9 +614,17 @@ static void wt_status_collect_changes_initial(struct wt_status *s)
                if (ce_stage(ce)) {
                        d->index_status = DIFF_STATUS_UNMERGED;
                        d->stagemask |= (1 << (ce_stage(ce) - 1));
-               }
-               else
+                       /*
+                        * Don't bother setting {mode,oid}_{head,index} since the print
+                        * code will output the stage values directly and not use the
+                        * values in these fields.
+                        */
+               } else {
                        d->index_status = DIFF_STATUS_ADDED;
+                       /* Leave {mode,oid}_head zero for adds. */
+                       d->mode_index = ce->ce_mode;
+                       hashcpy(d->oid_index.hash, ce->sha1);
+               }
        }
 }
 
@@ -627,7 +684,7 @@ void wt_status_collect(struct wt_status *s)
        wt_status_collect_untracked(s);
 }
 
-static void wt_status_print_unmerged(struct wt_status *s)
+static void wt_longstatus_print_unmerged(struct wt_status *s)
 {
        int shown_header = 0;
        int i;
@@ -640,17 +697,17 @@ static void wt_status_print_unmerged(struct wt_status *s)
                if (!d->stagemask)
                        continue;
                if (!shown_header) {
-                       wt_status_print_unmerged_header(s);
+                       wt_longstatus_print_unmerged_header(s);
                        shown_header = 1;
                }
-               wt_status_print_unmerged_data(s, it);
+               wt_longstatus_print_unmerged_data(s, it);
        }
        if (shown_header)
-               wt_status_print_trailer(s);
+               wt_longstatus_print_trailer(s);
 
 }
 
-static void wt_status_print_updated(struct wt_status *s)
+static void wt_longstatus_print_updated(struct wt_status *s)
 {
        int shown_header = 0;
        int i;
@@ -664,14 +721,14 @@ static void wt_status_print_updated(struct wt_status *s)
                    d->index_status == DIFF_STATUS_UNMERGED)
                        continue;
                if (!shown_header) {
-                       wt_status_print_cached_header(s);
+                       wt_longstatus_print_cached_header(s);
                        s->commitable = 1;
                        shown_header = 1;
                }
-               wt_status_print_change_data(s, WT_STATUS_UPDATED, it);
+               wt_longstatus_print_change_data(s, WT_STATUS_UPDATED, it);
        }
        if (shown_header)
-               wt_status_print_trailer(s);
+               wt_longstatus_print_trailer(s);
 }
 
 /*
@@ -703,7 +760,7 @@ static int wt_status_check_worktree_changes(struct wt_status *s,
        return changes;
 }
 
-static void wt_status_print_changed(struct wt_status *s)
+static void wt_longstatus_print_changed(struct wt_status *s)
 {
        int i, dirty_submodules;
        int worktree_changes = wt_status_check_worktree_changes(s, &dirty_submodules);
@@ -711,7 +768,7 @@ static void wt_status_print_changed(struct wt_status *s)
        if (!worktree_changes)
                return;
 
-       wt_status_print_dirty_header(s, worktree_changes < 0, dirty_submodules);
+       wt_longstatus_print_dirty_header(s, worktree_changes < 0, dirty_submodules);
 
        for (i = 0; i < s->change.nr; i++) {
                struct wt_status_change_data *d;
@@ -721,12 +778,12 @@ static void wt_status_print_changed(struct wt_status *s)
                if (!d->worktree_status ||
                    d->worktree_status == DIFF_STATUS_UNMERGED)
                        continue;
-               wt_status_print_change_data(s, WT_STATUS_CHANGED, it);
+               wt_longstatus_print_change_data(s, WT_STATUS_CHANGED, it);
        }
-       wt_status_print_trailer(s);
+       wt_longstatus_print_trailer(s);
 }
 
-static void wt_status_print_submodule_summary(struct wt_status *s, int uncommitted)
+static void wt_longstatus_print_submodule_summary(struct wt_status *s, int uncommitted)
 {
        struct child_process sm_summary = CHILD_PROCESS_INIT;
        struct strbuf cmd_stdout = STRBUF_INIT;
@@ -772,10 +829,10 @@ static void wt_status_print_submodule_summary(struct wt_status *s, int uncommitt
        strbuf_release(&summary);
 }
 
-static void wt_status_print_other(struct wt_status *s,
-                                 struct string_list *l,
-                                 const char *what,
-                                 const char *how)
+static void wt_longstatus_print_other(struct wt_status *s,
+                                     struct string_list *l,
+                                     const char *what,
+                                     const char *how)
 {
        int i;
        struct strbuf buf = STRBUF_INIT;
@@ -785,7 +842,7 @@ static void wt_status_print_other(struct wt_status *s,
        if (!l->nr)
                return;
 
-       wt_status_print_other_header(s, what, how);
+       wt_longstatus_print_other_header(s, what, how);
 
        for (i = 0; i < l->nr; i++) {
                struct string_list_item *it;
@@ -845,7 +902,7 @@ void wt_status_add_cut_line(FILE *fp)
        strbuf_release(&buf);
 }
 
-static void wt_status_print_verbose(struct wt_status *s)
+static void wt_longstatus_print_verbose(struct wt_status *s)
 {
        struct rev_info rev;
        struct setup_revision_opt opt;
@@ -878,7 +935,7 @@ static void wt_status_print_verbose(struct wt_status *s)
        if (s->verbose > 1 && s->commitable) {
                /* print_updated() printed a header, so do we */
                if (s->fp != stdout)
-                       wt_status_print_trailer(s);
+                       wt_longstatus_print_trailer(s);
                status_printf_ln(s, c, _("Changes to be committed:"));
                rev.diffopt.a_prefix = "c/";
                rev.diffopt.b_prefix = "i/";
@@ -896,7 +953,7 @@ static void wt_status_print_verbose(struct wt_status *s)
        }
 }
 
-static void wt_status_print_tracking(struct wt_status *s)
+static void wt_longstatus_print_tracking(struct wt_status *s)
 {
        struct strbuf sb = STRBUF_INIT;
        const char *cp, *ep, *branch_name;
@@ -962,7 +1019,7 @@ static void show_merge_in_progress(struct wt_status *s,
                        status_printf_ln(s, color,
                                _("  (use \"git commit\" to conclude merge)"));
        }
-       wt_status_print_trailer(s);
+       wt_longstatus_print_trailer(s);
 }
 
 static void show_am_in_progress(struct wt_status *s,
@@ -983,7 +1040,7 @@ static void show_am_in_progress(struct wt_status *s,
                status_printf_ln(s, color,
                        _("  (use \"git am --abort\" to restore the original branch)"));
        }
-       wt_status_print_trailer(s);
+       wt_longstatus_print_trailer(s);
 }
 
 static char *read_line_from_git_path(const char *filename)
@@ -1207,7 +1264,7 @@ static void show_rebase_in_progress(struct wt_status *s,
                                _("  (use \"git rebase --continue\" once you are satisfied with your changes)"));
                }
        }
-       wt_status_print_trailer(s);
+       wt_longstatus_print_trailer(s);
 }
 
 static void show_cherry_pick_in_progress(struct wt_status *s,
@@ -1226,7 +1283,7 @@ static void show_cherry_pick_in_progress(struct wt_status *s,
                status_printf_ln(s, color,
                        _("  (use \"git cherry-pick --abort\" to cancel the cherry-pick operation)"));
        }
-       wt_status_print_trailer(s);
+       wt_longstatus_print_trailer(s);
 }
 
 static void show_revert_in_progress(struct wt_status *s,
@@ -1245,7 +1302,7 @@ static void show_revert_in_progress(struct wt_status *s,
                status_printf_ln(s, color,
                        _("  (use \"git revert --abort\" to cancel the revert operation)"));
        }
-       wt_status_print_trailer(s);
+       wt_longstatus_print_trailer(s);
 }
 
 static void show_bisect_in_progress(struct wt_status *s,
@@ -1262,7 +1319,7 @@ static void show_bisect_in_progress(struct wt_status *s,
        if (s->hints)
                status_printf_ln(s, color,
                        _("  (use \"git bisect reset\" to get back to the original branch)"));
-       wt_status_print_trailer(s);
+       wt_longstatus_print_trailer(s);
 }
 
 /*
@@ -1432,8 +1489,8 @@ void wt_status_get_state(struct wt_status_state *state,
                wt_status_get_detached_from(state);
 }
 
-static void wt_status_print_state(struct wt_status *s,
-                                 struct wt_status_state *state)
+static void wt_longstatus_print_state(struct wt_status *s,
+                                     struct wt_status_state *state)
 {
        const char *state_color = color(WT_STATUS_HEADER, s);
        if (state->merge_in_progress)
@@ -1450,7 +1507,7 @@ static void wt_status_print_state(struct wt_status *s,
                show_bisect_in_progress(s, state, state_color);
 }
 
-void wt_status_print(struct wt_status *s)
+static void wt_longstatus_print(struct wt_status *s)
 {
        const char *branch_color = color(WT_STATUS_ONBRANCH, s);
        const char *branch_status_color = color(WT_STATUS_HEADER, s);
@@ -1487,10 +1544,10 @@ void wt_status_print(struct wt_status *s)
                status_printf_more(s, branch_status_color, "%s", on_what);
                status_printf_more(s, branch_color, "%s\n", branch_name);
                if (!s->is_initial)
-                       wt_status_print_tracking(s);
+                       wt_longstatus_print_tracking(s);
        }
 
-       wt_status_print_state(s, &state);
+       wt_longstatus_print_state(s, &state);
        free(state.branch);
        free(state.onto);
        free(state.detached_from);
@@ -1501,19 +1558,19 @@ void wt_status_print(struct wt_status *s)
                status_printf_ln(s, color(WT_STATUS_HEADER, s), "%s", "");
        }
 
-       wt_status_print_updated(s);
-       wt_status_print_unmerged(s);
-       wt_status_print_changed(s);
+       wt_longstatus_print_updated(s);
+       wt_longstatus_print_unmerged(s);
+       wt_longstatus_print_changed(s);
        if (s->submodule_summary &&
            (!s->ignore_submodule_arg ||
             strcmp(s->ignore_submodule_arg, "all"))) {
-               wt_status_print_submodule_summary(s, 0);  /* staged */
-               wt_status_print_submodule_summary(s, 1);  /* unstaged */
+               wt_longstatus_print_submodule_summary(s, 0);  /* staged */
+               wt_longstatus_print_submodule_summary(s, 1);  /* unstaged */
        }
        if (s->show_untracked_files) {
-               wt_status_print_other(s, &s->untracked, _("Untracked files"), "add");
+               wt_longstatus_print_other(s, &s->untracked, _("Untracked files"), "add");
                if (s->show_ignored_files)
-                       wt_status_print_other(s, &s->ignored, _("Ignored files"), "add -f");
+                       wt_longstatus_print_other(s, &s->ignored, _("Ignored files"), "add -f");
                if (advice_status_u_option && 2000 < s->untracked_in_ms) {
                        status_printf_ln(s, GIT_COLOR_NORMAL, "%s", "");
                        status_printf_ln(s, GIT_COLOR_NORMAL,
@@ -1528,7 +1585,7 @@ void wt_status_print(struct wt_status *s)
                        ? _(" (use -u option to show untracked files)") : "");
 
        if (s->verbose)
-               wt_status_print_verbose(s);
+               wt_longstatus_print_verbose(s);
        if (!s->commitable) {
                if (s->amend)
                        status_printf_ln(s, GIT_COLOR_NORMAL, _("No changes"));
@@ -1717,7 +1774,7 @@ static void wt_shortstatus_print_tracking(struct wt_status *s)
        fputc(s->null_termination ? '\0' : '\n', s->fp);
 }
 
-void wt_shortstatus_print(struct wt_status *s)
+static void wt_shortstatus_print(struct wt_status *s)
 {
        int i;
 
@@ -1749,7 +1806,7 @@ void wt_shortstatus_print(struct wt_status *s)
        }
 }
 
-void wt_porcelain_print(struct wt_status *s)
+static void wt_porcelain_print(struct wt_status *s)
 {
        s->use_color = 0;
        s->relative_paths = 0;
@@ -1757,3 +1814,398 @@ void wt_porcelain_print(struct wt_status *s)
        s->no_gettext = 1;
        wt_shortstatus_print(s);
 }
+
+/*
+ * Print branch information for porcelain v2 output.  These lines
+ * are printed when the '--branch' parameter is given.
+ *
+ *    # branch.oid <commit><eol>
+ *    # branch.head <head><eol>
+ *   [# branch.upstream <upstream><eol>
+ *   [# branch.ab +<ahead> -<behind><eol>]]
+ *
+ *      <commit> ::= the current commit hash or the the literal
+ *                   "(initial)" to indicate an initialized repo
+ *                   with no commits.
+ *
+ *        <head> ::= <branch_name> the current branch name or
+ *                   "(detached)" literal when detached head or
+ *                   "(unknown)" when something is wrong.
+ *
+ *    <upstream> ::= the upstream branch name, when set.
+ *
+ *       <ahead> ::= integer ahead value, when upstream set
+ *                   and the commit is present (not gone).
+ *
+ *      <behind> ::= integer behind value, when upstream set
+ *                   and commit is present.
+ *
+ *
+ * The end-of-line is defined by the -z flag.
+ *
+ *                 <eol> ::= NUL when -z,
+ *                           LF when NOT -z.
+ *
+ */
+static void wt_porcelain_v2_print_tracking(struct wt_status *s)
+{
+       struct branch *branch;
+       const char *base;
+       const char *branch_name;
+       struct wt_status_state state;
+       int ab_info, nr_ahead, nr_behind;
+       char eol = s->null_termination ? '\0' : '\n';
+
+       memset(&state, 0, sizeof(state));
+       wt_status_get_state(&state, s->branch && !strcmp(s->branch, "HEAD"));
+
+       fprintf(s->fp, "# branch.oid %s%c",
+                       (s->is_initial ? "(initial)" : sha1_to_hex(s->sha1_commit)),
+                       eol);
+
+       if (!s->branch)
+               fprintf(s->fp, "# branch.head %s%c", "(unknown)", eol);
+       else {
+               if (!strcmp(s->branch, "HEAD")) {
+                       fprintf(s->fp, "# branch.head %s%c", "(detached)", eol);
+
+                       if (state.rebase_in_progress || state.rebase_interactive_in_progress)
+                               branch_name = state.onto;
+                       else if (state.detached_from)
+                               branch_name = state.detached_from;
+                       else
+                               branch_name = "";
+               } else {
+                       branch_name = NULL;
+                       skip_prefix(s->branch, "refs/heads/", &branch_name);
+
+                       fprintf(s->fp, "# branch.head %s%c", branch_name, eol);
+               }
+
+               /* Lookup stats on the upstream tracking branch, if set. */
+               branch = branch_get(branch_name);
+               base = NULL;
+               ab_info = (stat_tracking_info(branch, &nr_ahead, &nr_behind, &base) == 0);
+               if (base) {
+                       base = shorten_unambiguous_ref(base, 0);
+                       fprintf(s->fp, "# branch.upstream %s%c", base, eol);
+                       free((char *)base);
+
+                       if (ab_info)
+                               fprintf(s->fp, "# branch.ab +%d -%d%c", nr_ahead, nr_behind, eol);
+               }
+       }
+
+       free(state.branch);
+       free(state.onto);
+       free(state.detached_from);
+}
+
+/*
+ * Convert various submodule status values into a
+ * fixed-length string of characters in the buffer provided.
+ */
+static void wt_porcelain_v2_submodule_state(
+       struct wt_status_change_data *d,
+       char sub[5])
+{
+       if (S_ISGITLINK(d->mode_head) ||
+               S_ISGITLINK(d->mode_index) ||
+               S_ISGITLINK(d->mode_worktree)) {
+               sub[0] = 'S';
+               sub[1] = d->new_submodule_commits ? 'C' : '.';
+               sub[2] = (d->dirty_submodule & DIRTY_SUBMODULE_MODIFIED) ? 'M' : '.';
+               sub[3] = (d->dirty_submodule & DIRTY_SUBMODULE_UNTRACKED) ? 'U' : '.';
+       } else {
+               sub[0] = 'N';
+               sub[1] = '.';
+               sub[2] = '.';
+               sub[3] = '.';
+       }
+       sub[4] = 0;
+}
+
+/*
+ * Fix-up changed entries before we print them.
+ */
+static void wt_porcelain_v2_fix_up_changed(
+       struct string_list_item *it,
+       struct wt_status *s)
+{
+       struct wt_status_change_data *d = it->util;
+
+       if (!d->index_status) {
+               /*
+                * This entry is unchanged in the index (relative to the head).
+                * Therefore, the collect_updated_cb was never called for this
+                * entry (during the head-vs-index scan) and so the head column
+                * fields were never set.
+                *
+                * We must have data for the index column (from the
+                * index-vs-worktree scan (otherwise, this entry should not be
+                * in the list of changes)).
+                *
+                * Copy index column fields to the head column, so that our
+                * output looks complete.
+                */
+               assert(d->mode_head == 0);
+               d->mode_head = d->mode_index;
+               oidcpy(&d->oid_head, &d->oid_index);
+       }
+
+       if (!d->worktree_status) {
+               /*
+                * This entry is unchanged in the worktree (relative to the index).
+                * Therefore, the collect_changed_cb was never called for this entry
+                * (during the index-vs-worktree scan) and so the worktree column
+                * fields were never set.
+                *
+                * We must have data for the index column (from the head-vs-index
+                * scan).
+                *
+                * Copy the index column fields to the worktree column so that
+                * our output looks complete.
+                *
+                * Note that we only have a mode field in the worktree column
+                * because the scan code tries really hard to not have to compute it.
+                */
+               assert(d->mode_worktree == 0);
+               d->mode_worktree = d->mode_index;
+       }
+}
+
+/*
+ * Print porcelain v2 info for tracked entries with changes.
+ */
+static void wt_porcelain_v2_print_changed_entry(
+       struct string_list_item *it,
+       struct wt_status *s)
+{
+       struct wt_status_change_data *d = it->util;
+       struct strbuf buf_index = STRBUF_INIT;
+       struct strbuf buf_head = STRBUF_INIT;
+       const char *path_index = NULL;
+       const char *path_head = NULL;
+       char key[3];
+       char submodule_token[5];
+       char sep_char, eol_char;
+
+       wt_porcelain_v2_fix_up_changed(it, s);
+       wt_porcelain_v2_submodule_state(d, submodule_token);
+
+       key[0] = d->index_status ? d->index_status : '.';
+       key[1] = d->worktree_status ? d->worktree_status : '.';
+       key[2] = 0;
+
+       if (s->null_termination) {
+               /*
+                * In -z mode, we DO NOT C-quote pathnames.  Current path is ALWAYS first.
+                * A single NUL character separates them.
+                */
+               sep_char = '\0';
+               eol_char = '\0';
+               path_index = it->string;
+               path_head = d->head_path;
+       } else {
+               /*
+                * Path(s) are C-quoted if necessary. Current path is ALWAYS first.
+                * The source path is only present when necessary.
+                * A single TAB separates them (because paths can contain spaces
+                * which are not escaped and C-quoting does escape TAB characters).
+                */
+               sep_char = '\t';
+               eol_char = '\n';
+               path_index = quote_path(it->string, s->prefix, &buf_index);
+               if (d->head_path)
+                       path_head = quote_path(d->head_path, s->prefix, &buf_head);
+       }
+
+       if (path_head)
+               fprintf(s->fp, "2 %s %s %06o %06o %06o %s %s %c%d %s%c%s%c",
+                               key, submodule_token,
+                               d->mode_head, d->mode_index, d->mode_worktree,
+                               oid_to_hex(&d->oid_head), oid_to_hex(&d->oid_index),
+                               key[0], d->score,
+                               path_index, sep_char, path_head, eol_char);
+       else
+               fprintf(s->fp, "1 %s %s %06o %06o %06o %s %s %s%c",
+                               key, submodule_token,
+                               d->mode_head, d->mode_index, d->mode_worktree,
+                               oid_to_hex(&d->oid_head), oid_to_hex(&d->oid_index),
+                               path_index, eol_char);
+
+       strbuf_release(&buf_index);
+       strbuf_release(&buf_head);
+}
+
+/*
+ * Print porcelain v2 status info for unmerged entries.
+ */
+static void wt_porcelain_v2_print_unmerged_entry(
+       struct string_list_item *it,
+       struct wt_status *s)
+{
+       struct wt_status_change_data *d = it->util;
+       const struct cache_entry *ce;
+       struct strbuf buf_index = STRBUF_INIT;
+       const char *path_index = NULL;
+       int pos, stage, sum;
+       struct {
+               int mode;
+               struct object_id oid;
+       } stages[3];
+       char *key;
+       char submodule_token[5];
+       char unmerged_prefix = 'u';
+       char eol_char = s->null_termination ? '\0' : '\n';
+
+       wt_porcelain_v2_submodule_state(d, submodule_token);
+
+       switch (d->stagemask) {
+       case 1: key = "DD"; break; /* both deleted */
+       case 2: key = "AU"; break; /* added by us */
+       case 3: key = "UD"; break; /* deleted by them */
+       case 4: key = "UA"; break; /* added by them */
+       case 5: key = "DU"; break; /* deleted by us */
+       case 6: key = "AA"; break; /* both added */
+       case 7: key = "UU"; break; /* both modified */
+       default:
+               die("BUG: unhandled unmerged status %x", d->stagemask);
+       }
+
+       /*
+        * Disregard d.aux.porcelain_v2 data that we accumulated
+        * for the head and index columns during the scans and
+        * replace with the actual stage data.
+        *
+        * Note that this is a last-one-wins for each the individual
+        * stage [123] columns in the event of multiple cache entries
+        * for same stage.
+        */
+       memset(stages, 0, sizeof(stages));
+       sum = 0;
+       pos = cache_name_pos(it->string, strlen(it->string));
+       assert(pos < 0);
+       pos = -pos-1;
+       while (pos < active_nr) {
+               ce = active_cache[pos++];
+               stage = ce_stage(ce);
+               if (strcmp(ce->name, it->string) || !stage)
+                       break;
+               stages[stage - 1].mode = ce->ce_mode;
+               hashcpy(stages[stage - 1].oid.hash, ce->sha1);
+               sum |= (1 << (stage - 1));
+       }
+       if (sum != d->stagemask)
+               die("BUG: observed stagemask 0x%x != expected stagemask 0x%x", sum, d->stagemask);
+
+       if (s->null_termination)
+               path_index = it->string;
+       else
+               path_index = quote_path(it->string, s->prefix, &buf_index);
+
+       fprintf(s->fp, "%c %s %s %06o %06o %06o %06o %s %s %s %s%c",
+                       unmerged_prefix, key, submodule_token,
+                       stages[0].mode, /* stage 1 */
+                       stages[1].mode, /* stage 2 */
+                       stages[2].mode, /* stage 3 */
+                       d->mode_worktree,
+                       oid_to_hex(&stages[0].oid), /* stage 1 */
+                       oid_to_hex(&stages[1].oid), /* stage 2 */
+                       oid_to_hex(&stages[2].oid), /* stage 3 */
+                       path_index,
+                       eol_char);
+
+       strbuf_release(&buf_index);
+}
+
+/*
+ * Print porcelain V2 status info for untracked and ignored entries.
+ */
+static void wt_porcelain_v2_print_other(
+       struct string_list_item *it,
+       struct wt_status *s,
+       char prefix)
+{
+       struct strbuf buf = STRBUF_INIT;
+       const char *path;
+       char eol_char;
+
+       if (s->null_termination) {
+               path = it->string;
+               eol_char = '\0';
+       } else {
+               path = quote_path(it->string, s->prefix, &buf);
+               eol_char = '\n';
+       }
+
+       fprintf(s->fp, "%c %s%c", prefix, path, eol_char);
+
+       strbuf_release(&buf);
+}
+
+/*
+ * Print porcelain V2 status.
+ *
+ * [<v2_branch>]
+ * [<v2_changed_items>]*
+ * [<v2_unmerged_items>]*
+ * [<v2_untracked_items>]*
+ * [<v2_ignored_items>]*
+ *
+ */
+static void wt_porcelain_v2_print(struct wt_status *s)
+{
+       struct wt_status_change_data *d;
+       struct string_list_item *it;
+       int i;
+
+       if (s->show_branch)
+               wt_porcelain_v2_print_tracking(s);
+
+       for (i = 0; i < s->change.nr; i++) {
+               it = &(s->change.items[i]);
+               d = it->util;
+               if (!d->stagemask)
+                       wt_porcelain_v2_print_changed_entry(it, s);
+       }
+
+       for (i = 0; i < s->change.nr; i++) {
+               it = &(s->change.items[i]);
+               d = it->util;
+               if (d->stagemask)
+                       wt_porcelain_v2_print_unmerged_entry(it, s);
+       }
+
+       for (i = 0; i < s->untracked.nr; i++) {
+               it = &(s->untracked.items[i]);
+               wt_porcelain_v2_print_other(it, s, '?');
+       }
+
+       for (i = 0; i < s->ignored.nr; i++) {
+               it = &(s->ignored.items[i]);
+               wt_porcelain_v2_print_other(it, s, '!');
+       }
+}
+
+void wt_status_print(struct wt_status *s)
+{
+       switch (s->status_format) {
+       case STATUS_FORMAT_SHORT:
+               wt_shortstatus_print(s);
+               break;
+       case STATUS_FORMAT_PORCELAIN:
+               wt_porcelain_print(s);
+               break;
+       case STATUS_FORMAT_PORCELAIN_V2:
+               wt_porcelain_v2_print(s);
+               break;
+       case STATUS_FORMAT_UNSPECIFIED:
+               die("BUG: finalize_deferred_config() should have been called");
+               break;
+       case STATUS_FORMAT_NONE:
+       case STATUS_FORMAT_LONG:
+               wt_longstatus_print(s);
+               break;
+       }
+}
index 2ca93f6957f69cd5652ddc764f5afe81b598a2a9..e40183770700cb593d4e4f2bf914a5b6eff0bb09 100644 (file)
@@ -38,11 +38,24 @@ struct wt_status_change_data {
        int worktree_status;
        int index_status;
        int stagemask;
+       int score;
+       int mode_head, mode_index, mode_worktree;
+       struct object_id oid_head, oid_index;
        char *head_path;
        unsigned dirty_submodule       : 2;
        unsigned new_submodule_commits : 1;
 };
 
+enum wt_status_format {
+       STATUS_FORMAT_NONE = 0,
+       STATUS_FORMAT_LONG,
+       STATUS_FORMAT_SHORT,
+       STATUS_FORMAT_PORCELAIN,
+       STATUS_FORMAT_PORCELAIN_V2,
+
+       STATUS_FORMAT_UNSPECIFIED
+};
+
 struct wt_status {
        int is_initial;
        char *branch;
@@ -66,6 +79,9 @@ struct wt_status {
        int show_branch;
        int hints;
 
+       enum wt_status_format status_format;
+       unsigned char sha1_commit[GIT_SHA1_RAWSZ]; /* when not Initial */
+
        /* These are computed during processing of the individual sections */
        int commitable;
        int workdir_dirty;
@@ -107,9 +123,6 @@ int wt_status_check_rebase(const struct worktree *wt,
 int wt_status_check_bisect(const struct worktree *wt,
                           struct wt_status_state *state);
 
-void wt_shortstatus_print(struct wt_status *s);
-void wt_porcelain_print(struct wt_status *s);
-
 __attribute__((format (printf, 3, 4)))
 void status_printf_ln(struct wt_status *s, const char *color, const char *fmt, ...);
 __attribute__((format (printf, 3, 4)))