{"thread":{"id":"63876","subject":"[PATCH] status: add --json output format to git status","startedAt":"2025-07-30T06:27:09Z","lastAt":"2025-08-04T13:54:30Z","messageCount":6,"participants":["Tach via GitGitGadget","Phillip Wood","phillip.wood123@gmail.com","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":null},"messages":[{"id":"522996","messageId":"pull.1937.git.1753856826464.gitgitgadget@gmail.com","threadId":"63876","inReplyTo":null,"subject":"[PATCH] status: add --json output format to git status","fromName":"Tach via GitGitGadget","fromEmail":"gitgitgadget@gmail.com","sentAt":"2025-07-30T06:27:06Z","receivedAt":"2025-07-30T06:27:09Z","isPatch":true,"sender":{"key":"name:Tach","avatar":null},"body":"From: tacherasasi <tacherasasi@gmail.com>\n\nAdd a new --json flag to 'git status' that outputs repository state\nin a structured JSON format. This enables reliable machine parsing\nof status information for tools and automation.\n\nThe JSON output includes:\n- Branch information (name, detached state, ahead/behind counts)\n- Staged files array\n- Unstaged files array\n- Untracked files array\n- Ignored files array (with --ignored flag)\n\nImplementation details:\n- Add STATUS_FORMAT_JSON to wt_status_format enum\n- Add JSON output option to git status and git commit\n- Implement JSON formatting helpers for arrays and branch info\n- Structure output for easy parsing and future extensibility\n\nExample:\n  $ git status --json\n  {\n    \"branch\": {\n      \"current\": \"main\",\n      \"detached\": false\n    },\n    \"staged\": [\"file1.txt\"],\n    \"unstaged\": [\"file2.txt\"],\n    \"untracked\": [\"file3.txt\"]\n  }\n\nThis provides a robust alternative to parsing traditional output\nformats, making it easier to build reliable tools and automation\naround Git status information.\n\nSigned-off-by: Tachera Sasi <tachera@ekilie.com>\nSigned-off-by: tacherasasi <tacherasasi@gmail.com>\n---\n    status: add --json output format to git status\n\nPublished-As: https://github.com/gitgitgadget/git/releases/tag/pr-1937%2FtacheraSasi%2Ffeature%2Fjson-status-output-v1\nFetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1937/tacheraSasi/feature/json-status-output-v1\nPull-Request: https://github.com/gitgitgadget/git/pull/1937\n\n builtin/commit.c |  9 ++++-\n wt-status.c      | 98 ++++++++++++++++++++++++++++++++++++++++++++++++\n wt-status.h      |  1 +\n 3 files changed, 107 insertions(+), 1 deletion(-)\n\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex fba0dded64a..f1db4fdfd9a 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -1540,6 +1540,9 @@ struct repository *repo UNUSED)\n \t\tOPT_SET_INT(0, \"long\", &status_format,\n \t\t\t    N_(\"show status in long format (default)\"),\n \t\t\t    STATUS_FORMAT_LONG),\n+\t\tOPT_SET_INT(0, \"json\", &status_format,\n+\t\t    N_(\"show status in JSON format\"),\n+\t\t    STATUS_FORMAT_JSON),\n \t\tOPT_BOOL('z', \"null\", &s.null_termination,\n \t\t\t N_(\"terminate entries with NUL\")),\n \t\t{\n@@ -1603,7 +1606,8 @@ struct repository *repo UNUSED)\n \t\t       prefix, argv);\n \n \tif (status_format != STATUS_FORMAT_PORCELAIN &&\n-\t    status_format != STATUS_FORMAT_PORCELAIN_V2)\n+\t    status_format != STATUS_FORMAT_PORCELAIN_V2 &&\n+\t    status_format != STATUS_FORMAT_JSON)\n \t\tprogress_flag = REFRESH_PROGRESS;\n \trepo_read_index(the_repository);\n \trefresh_index(the_repository->index,\n@@ -1735,6 +1739,9 @@ int cmd_commit(int argc,\n \t\tOPT_SET_INT(0, \"long\", &status_format,\n \t\t\t    N_(\"show status in long format (default)\"),\n \t\t\t    STATUS_FORMAT_LONG),\n+\t\tOPT_SET_INT(0, \"json\", &status_format,\n+\t\t    N_(\"show status in JSON format\"),\n+\t\t    STATUS_FORMAT_JSON),\n \t\tOPT_BOOL('z', \"null\", &s.null_termination,\n \t\t\t N_(\"terminate entries with NUL\")),\n \t\tOPT_BOOL(0, \"amend\", &amend, N_(\"amend previous commit\")),\ndiff --git a/wt-status.c b/wt-status.c\nindex 454601afa15..7192fb4d057 100644\n--- a/wt-status.c\n+++ b/wt-status.c\n@@ -2564,6 +2564,101 @@ static void wt_porcelain_v2_print(struct wt_status *s)\n \t}\n }\n \n+\n+static void wt_json_print_string_array(struct wt_status *s, const char *name, struct string_list *list)\n+{\n+\tint i;\n+\tfprintf(s->fp, \"  \\\"%s\\\": [\", name);\n+\tfor (i = 0; i < list->nr; i++) {\n+\t\tif (i > 0)\n+\t\t\tfprintf(s->fp, \", \");\n+\t\tfprintf(s->fp, \"\\\"%s\\\"\", list->items[i].string);\n+\t}\n+\tfprintf(s->fp, \"]\");\n+}\n+\n+static void wt_json_print_change_array(struct wt_status *s, const char *name, int change_type)\n+{\n+\tint i;\n+\tstruct string_list files = STRING_LIST_INIT_DUP;\n+\n+\tfor (i = 0; i < s->change.nr; i++) {\n+\t\tstruct wt_status_change_data *d;\n+\t\tstruct string_list_item *it;\n+\t\tit = &(s->change.items[i]);\n+\t\td = it->util;\n+\n+\t\tif ((change_type == WT_STATUS_UPDATED && d->index_status &&\n+\t\t     d->index_status != DIFF_STATUS_UNMERGED) ||\n+\t\t    (change_type == WT_STATUS_CHANGED && d->worktree_status &&\n+\t\t     d->worktree_status != DIFF_STATUS_UNMERGED)) {\n+\t\t\tstring_list_append(&files, it->string);\n+\t\t}\n+\t}\n+\n+\twt_json_print_string_array(s, name, &files);\n+\tstring_list_clear(&files, 0);\n+}\n+\n+static void wt_json_print_branch_info(struct wt_status *s)\n+{\n+\tstruct branch *branch;\n+\tconst char *branch_name;\n+\tint ahead = 0, behind = 0;\n+\n+\tfprintf(s->fp, \"  \\\"branch\\\": {\\n\");\n+\n+\tif (s->branch && !s->is_initial) {\n+\t\tif (!strcmp(s->branch, \"HEAD\")) {\n+\t\t\tfprintf(s->fp, \"    \\\"current\\\": \\\"HEAD\\\",\\n\");\n+\t\t\tfprintf(s->fp, \"    \\\"detached\\\": true\");\n+\t\t} else {\n+\t\t\tif (skip_prefix(s->branch, \"refs/heads/\", &branch_name)) {\n+\t\t\t\tfprintf(s->fp, \"    \\\"current\\\": \\\"%s\\\",\\n\", branch_name);\n+\t\t\t\tfprintf(s->fp, \"    \\\"detached\\\": false\");\n+\n+\t\t\t\tbranch = branch_get(branch_name);\n+\t\t\t\tif (branch && branch->merge && branch->merge[0] && branch->merge[0]->dst) {\n+\t\t\t\t\tif (!stat_tracking_info(branch, &ahead, &behind, NULL, 0, 0)) {\n+\t\t\t\t\t\tfprintf(s->fp, \",\\n    \\\"ahead\\\": %d,\\n    \\\"behind\\\": %d\", ahead, behind);\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t} else {\n+\t\t\t\tfprintf(s->fp, \"    \\\"current\\\": \\\"%s\\\",\\n\", s->branch);\n+\t\t\t\tfprintf(s->fp, \"    \\\"detached\\\": false\");\n+\t\t\t}\n+\t\t}\n+\t} else {\n+\t\tfprintf(s->fp, \"    \\\"current\\\": null,\\n\");\n+\t\tfprintf(s->fp, \"    \\\"detached\\\": false\");\n+\t}\n+\n+\tfprintf(s->fp, \"\\n  }\");\n+}\n+\n+static void wt_json_status_print(struct wt_status *s)\n+{\n+\tfprintf(s->fp, \"{\\n\");\n+\n+\twt_json_print_branch_info(s);\n+\tfprintf(s->fp, \",\\n\");\n+\n+\twt_json_print_change_array(s, \"staged\", WT_STATUS_UPDATED);\n+\tfprintf(s->fp, \",\\n\");\n+\n+\twt_json_print_change_array(s, \"unstaged\", WT_STATUS_CHANGED);\n+\tfprintf(s->fp, \",\\n\");\n+\n+\twt_json_print_string_array(s, \"untracked\", &s->untracked);\n+\n+\tif (s->ignored.nr > 0) {\n+\t\tfprintf(s->fp, \",\\n\");\n+\t\twt_json_print_string_array(s, \"ignored\", &s->ignored);\n+\t}\n+\n+\tfprintf(s->fp, \"\\n}\\n\");\n+}\n+\n void wt_status_print(struct wt_status *s)\n {\n \ttrace2_data_intmax(\"status\", s->repo, \"count/changed\", s->change.nr);\n@@ -2583,6 +2678,9 @@ void wt_status_print(struct wt_status *s)\n \tcase STATUS_FORMAT_PORCELAIN_V2:\n \t\twt_porcelain_v2_print(s);\n \t\tbreak;\n+\tcase STATUS_FORMAT_JSON:\n+\t\twt_json_status_print(s);\n+\t\tbreak;\n \tcase STATUS_FORMAT_UNSPECIFIED:\n \t\tBUG(\"finalize_deferred_config() should have been called\");\n \t\tbreak;\ndiff --git a/wt-status.h b/wt-status.h\nindex 4e377ce62b8..e929af832b2 100644\n--- a/wt-status.h\n+++ b/wt-status.h\n@@ -74,6 +74,7 @@ enum wt_status_format {\n \tSTATUS_FORMAT_SHORT,\n \tSTATUS_FORMAT_PORCELAIN,\n \tSTATUS_FORMAT_PORCELAIN_V2,\n+\tSTATUS_FORMAT_JSON,\n \n \tSTATUS_FORMAT_UNSPECIFIED\n };\n\nbase-commit: cb3b40381e1d5ee32dde96521ad7cfd68eb308a6\n-- \ngitgitgadget\n"},{"id":"523003","messageId":"c4658ae9-007d-4645-9561-febe03dc612d@gmail.com","threadId":"63876","inReplyTo":"pull.1937.git.1753856826464.gitgitgadget@gmail.com","subject":"Re: [PATCH] status: add --json output format to git status","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-07-30T09:01:44Z","receivedAt":"2025-07-30T09:01:52Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 30/07/2025 07:27, Tach via GitGitGadget wrote:\n> From: tacherasasi <tacherasasi@gmail.com>\n> \n> Add a new --json flag to 'git status' that outputs repository state\n> in a structured JSON format. This enables reliable machine parsing\n> of status information for tools and automation.\n> \n> The JSON output includes:\n> - Branch information (name, detached state, ahead/behind counts)\n> - Staged files array\n> - Unstaged files array\n> - Untracked files array\n> - Ignored files array (with --ignored flag)\n> \n> Implementation details:\n> - Add STATUS_FORMAT_JSON to wt_status_format enum\n> - Add JSON output option to git status and git commit\n> - Implement JSON formatting helpers for arrays and branch info\n> - Structure output for easy parsing and future extensibility\n> \n> Example:\n>    $ git status --json\n>    {\n>      \"branch\": {\n>        \"current\": \"main\",\n>        \"detached\": false\n>      },\n>      \"staged\": [\"file1.txt\"],\n>      \"unstaged\": [\"file2.txt\"],\n>      \"untracked\": [\"file3.txt\"]\n>    }\n> \n> This provides a robust alternative to parsing traditional output\n> formats, making it easier to build reliable tools and automation\n> around Git status information.\n\nWhat problem are you having paring the current output? You say this \npatch provides a robust alternative but I'm afraid it does not seem to \nhandle filenames containing newlines or double quotes and it assumes \nthey are utf-8 encoded. It also seems to print a trailing comma if an \narray has more than one item which is not allowed by the json spec. We \nalready have support for generating json output as documented \njson-writer.h which will help with most of that but not the filename \nencoding problem. New features should also be accompanied by tests to \nprevent future regressions. I'm not opposed to adding json output if it \nis robust and handles non utf-8 filenames but it would be helpful to \nunderstand what problems you are having parsing the output of \"git \nstatus --porcelain[=v2]\".\n\nThanks\n\nPhillip\n\n> Signed-off-by: Tachera Sasi <tachera@ekilie.com>\n> Signed-off-by: tacherasasi <tacherasasi@gmail.com>\n> ---\n>      status: add --json output format to git status\n> \n> Published-As: https://github.com/gitgitgadget/git/releases/tag/pr-1937%2FtacheraSasi%2Ffeature%2Fjson-status-output-v1\n> Fetch-It-Via: git fetch https://github.com/gitgitgadget/git pr-1937/tacheraSasi/feature/json-status-output-v1\n> Pull-Request: https://github.com/gitgitgadget/git/pull/1937\n> \n>   builtin/commit.c |  9 ++++-\n>   wt-status.c      | 98 ++++++++++++++++++++++++++++++++++++++++++++++++\n>   wt-status.h      |  1 +\n>   3 files changed, 107 insertions(+), 1 deletion(-)\n> \n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index fba0dded64a..f1db4fdfd9a 100644\n> --- a/builtin/commit.c\n> +++ b/builtin/commit.c\n> @@ -1540,6 +1540,9 @@ struct repository *repo UNUSED)\n>   \t\tOPT_SET_INT(0, \"long\", &status_format,\n>   \t\t\t    N_(\"show status in long format (default)\"),\n>   \t\t\t    STATUS_FORMAT_LONG),\n> +\t\tOPT_SET_INT(0, \"json\", &status_format,\n> +\t\t    N_(\"show status in JSON format\"),\n> +\t\t    STATUS_FORMAT_JSON),\n>   \t\tOPT_BOOL('z', \"null\", &s.null_termination,\n>   \t\t\t N_(\"terminate entries with NUL\")),\n>   \t\t{\n> @@ -1603,7 +1606,8 @@ struct repository *repo UNUSED)\n>   \t\t       prefix, argv);\n>   \n>   \tif (status_format != STATUS_FORMAT_PORCELAIN &&\n> -\t    status_format != STATUS_FORMAT_PORCELAIN_V2)\n> +\t    status_format != STATUS_FORMAT_PORCELAIN_V2 &&\n> +\t    status_format != STATUS_FORMAT_JSON)\n>   \t\tprogress_flag = REFRESH_PROGRESS;\n>   \trepo_read_index(the_repository);\n>   \trefresh_index(the_repository->index,\n> @@ -1735,6 +1739,9 @@ int cmd_commit(int argc,\n>   \t\tOPT_SET_INT(0, \"long\", &status_format,\n>   \t\t\t    N_(\"show status in long format (default)\"),\n>   \t\t\t    STATUS_FORMAT_LONG),\n> +\t\tOPT_SET_INT(0, \"json\", &status_format,\n> +\t\t    N_(\"show status in JSON format\"),\n> +\t\t    STATUS_FORMAT_JSON),\n>   \t\tOPT_BOOL('z', \"null\", &s.null_termination,\n>   \t\t\t N_(\"terminate entries with NUL\")),\n>   \t\tOPT_BOOL(0, \"amend\", &amend, N_(\"amend previous commit\")),\n> diff --git a/wt-status.c b/wt-status.c\n> index 454601afa15..7192fb4d057 100644\n> --- a/wt-status.c\n> +++ b/wt-status.c\n> @@ -2564,6 +2564,101 @@ static void wt_porcelain_v2_print(struct wt_status *s)\n>   \t}\n>   }\n>   \n> +\n> +static void wt_json_print_string_array(struct wt_status *s, const char *name, struct string_list *list)\n> +{\n> +\tint i;\n> +\tfprintf(s->fp, \"  \\\"%s\\\": [\", name);\n> +\tfor (i = 0; i < list->nr; i++) {\n> +\t\tif (i > 0)\n> +\t\t\tfprintf(s->fp, \", \");\n> +\t\tfprintf(s->fp, \"\\\"%s\\\"\", list->items[i].string);\n> +\t}\n> +\tfprintf(s->fp, \"]\");\n> +}\n> +\n> +static void wt_json_print_change_array(struct wt_status *s, const char *name, int change_type)\n> +{\n> +\tint i;\n> +\tstruct string_list files = STRING_LIST_INIT_DUP;\n> +\n> +\tfor (i = 0; i < s->change.nr; i++) {\n> +\t\tstruct wt_status_change_data *d;\n> +\t\tstruct string_list_item *it;\n> +\t\tit = &(s->change.items[i]);\n> +\t\td = it->util;\n> +\n> +\t\tif ((change_type == WT_STATUS_UPDATED && d->index_status &&\n> +\t\t     d->index_status != DIFF_STATUS_UNMERGED) ||\n> +\t\t    (change_type == WT_STATUS_CHANGED && d->worktree_status &&\n> +\t\t     d->worktree_status != DIFF_STATUS_UNMERGED)) {\n> +\t\t\tstring_list_append(&files, it->string);\n> +\t\t}\n> +\t}\n> +\n> +\twt_json_print_string_array(s, name, &files);\n> +\tstring_list_clear(&files, 0);\n> +}\n> +\n> +static void wt_json_print_branch_info(struct wt_status *s)\n> +{\n> +\tstruct branch *branch;\n> +\tconst char *branch_name;\n> +\tint ahead = 0, behind = 0;\n> +\n> +\tfprintf(s->fp, \"  \\\"branch\\\": {\\n\");\n> +\n> +\tif (s->branch && !s->is_initial) {\n> +\t\tif (!strcmp(s->branch, \"HEAD\")) {\n> +\t\t\tfprintf(s->fp, \"    \\\"current\\\": \\\"HEAD\\\",\\n\");\n> +\t\t\tfprintf(s->fp, \"    \\\"detached\\\": true\");\n> +\t\t} else {\n> +\t\t\tif (skip_prefix(s->branch, \"refs/heads/\", &branch_name)) {\n> +\t\t\t\tfprintf(s->fp, \"    \\\"current\\\": \\\"%s\\\",\\n\", branch_name);\n> +\t\t\t\tfprintf(s->fp, \"    \\\"detached\\\": false\");\n> +\n> +\t\t\t\tbranch = branch_get(branch_name);\n> +\t\t\t\tif (branch && branch->merge && branch->merge[0] && branch->merge[0]->dst) {\n> +\t\t\t\t\tif (!stat_tracking_info(branch, &ahead, &behind, NULL, 0, 0)) {\n> +\t\t\t\t\t\tfprintf(s->fp, \",\\n    \\\"ahead\\\": %d,\\n    \\\"behind\\\": %d\", ahead, behind);\n> +\t\t\t\t\t}\n> +\t\t\t\t}\n> +\t\t\t} else {\n> +\t\t\t\tfprintf(s->fp, \"    \\\"current\\\": \\\"%s\\\",\\n\", s->branch);\n> +\t\t\t\tfprintf(s->fp, \"    \\\"detached\\\": false\");\n> +\t\t\t}\n> +\t\t}\n> +\t} else {\n> +\t\tfprintf(s->fp, \"    \\\"current\\\": null,\\n\");\n> +\t\tfprintf(s->fp, \"    \\\"detached\\\": false\");\n> +\t}\n> +\n> +\tfprintf(s->fp, \"\\n  }\");\n> +}\n> +\n> +static void wt_json_status_print(struct wt_status *s)\n> +{\n> +\tfprintf(s->fp, \"{\\n\");\n> +\n> +\twt_json_print_branch_info(s);\n> +\tfprintf(s->fp, \",\\n\");\n> +\n> +\twt_json_print_change_array(s, \"staged\", WT_STATUS_UPDATED);\n> +\tfprintf(s->fp, \",\\n\");\n> +\n> +\twt_json_print_change_array(s, \"unstaged\", WT_STATUS_CHANGED);\n> +\tfprintf(s->fp, \",\\n\");\n> +\n> +\twt_json_print_string_array(s, \"untracked\", &s->untracked);\n> +\n> +\tif (s->ignored.nr > 0) {\n> +\t\tfprintf(s->fp, \",\\n\");\n> +\t\twt_json_print_string_array(s, \"ignored\", &s->ignored);\n> +\t}\n> +\n> +\tfprintf(s->fp, \"\\n}\\n\");\n> +}\n> +\n>   void wt_status_print(struct wt_status *s)\n>   {\n>   \ttrace2_data_intmax(\"status\", s->repo, \"count/changed\", s->change.nr);\n> @@ -2583,6 +2678,9 @@ void wt_status_print(struct wt_status *s)\n>   \tcase STATUS_FORMAT_PORCELAIN_V2:\n>   \t\twt_porcelain_v2_print(s);\n>   \t\tbreak;\n> +\tcase STATUS_FORMAT_JSON:\n> +\t\twt_json_status_print(s);\n> +\t\tbreak;\n>   \tcase STATUS_FORMAT_UNSPECIFIED:\n>   \t\tBUG(\"finalize_deferred_config() should have been called\");\n>   \t\tbreak;\n> diff --git a/wt-status.h b/wt-status.h\n> index 4e377ce62b8..e929af832b2 100644\n> --- a/wt-status.h\n> +++ b/wt-status.h\n> @@ -74,6 +74,7 @@ enum wt_status_format {\n>   \tSTATUS_FORMAT_SHORT,\n>   \tSTATUS_FORMAT_PORCELAIN,\n>   \tSTATUS_FORMAT_PORCELAIN_V2,\n> +\tSTATUS_FORMAT_JSON,\n>   \n>   \tSTATUS_FORMAT_UNSPECIFIED\n>   };\n> \n> base-commit: cb3b40381e1d5ee32dde96521ad7cfd68eb308a6\n\n"},{"id":"523004","messageId":"2da722a3-2ed0-4f2b-a623-df0dc4aaad77@gmail.com","threadId":"63876","inReplyTo":"CA+m0rScismG8VVf8mYS=+_AjwgrgX8GNZrFD+8-0EaeCDYPgbg@mail.gmail.com","subject":"Re: [PATCH] status: add --json output format to git status","fromName":"","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-07-30T10:08:43Z","receivedAt":"2025-07-30T10:08:51Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Tachera\n\nOn 30/07/2025 10:15, Tachera W sasi wrote:\n> \n> For v2, I will:\n>    * Rework the patch to use `json-writer.h` so escaping and JSON \n> compliance are handled\n>      properly.\n\nThat will handle the escaping problems but we still have the problem \nthat filenames and refnames are not guaranteed to be utf-8 encoded. I \nthink the only way to handle that is to base64 encode them so that the \ncaller can retrieve the raw bytes. See \nhttps://lore.kernel.org/git/ZvXMSKaUWWA-MG9J@tapette.crustytoothpaste.net/ \nfor a previous discussion about this.\n\n>    * Add tests covering filenames with quotes, newlines, and non-UTF-8 \n> cases.\n>    * Include tests under `t/` to prevent future regressions.\n\nThat's great, you should update Documentation/git-status.adoc as well to \ndocument the new option and output format\n\nThanks\n\nPhillip\n\n> Thanks again for your review and for pointing me towards the proper helpers.\n> \n> Best regards,\n> Tachera Sasi\n> \n> On Wed, Jul 30, 2025 at 12:01 PM Phillip Wood <phillip.wood123@gmail.com \n> <mailto:phillip.wood123@gmail.com>> wrote:\n> \n>     On 30/07/2025 07:27, Tach via GitGitGadget wrote:\n>      > From: tacherasasi <tacherasasi@gmail.com\n>     <mailto:tacherasasi@gmail.com>>\n>      >\n>      > Add a new --json flag to 'git status' that outputs repository state\n>      > in a structured JSON format. This enables reliable machine parsing\n>      > of status information for tools and automation.\n>      >\n>      > The JSON output includes:\n>      > - Branch information (name, detached state, ahead/behind counts)\n>      > - Staged files array\n>      > - Unstaged files array\n>      > - Untracked files array\n>      > - Ignored files array (with --ignored flag)\n>      >\n>      > Implementation details:\n>      > - Add STATUS_FORMAT_JSON to wt_status_format enum\n>      > - Add JSON output option to git status and git commit\n>      > - Implement JSON formatting helpers for arrays and branch info\n>      > - Structure output for easy parsing and future extensibility\n>      >\n>      > Example:\n>      >    $ git status --json\n>      >    {\n>      >      \"branch\": {\n>      >        \"current\": \"main\",\n>      >        \"detached\": false\n>      >      },\n>      >      \"staged\": [\"file1.txt\"],\n>      >      \"unstaged\": [\"file2.txt\"],\n>      >      \"untracked\": [\"file3.txt\"]\n>      >    }\n>      >\n>      > This provides a robust alternative to parsing traditional output\n>      > formats, making it easier to build reliable tools and automation\n>      > around Git status information.\n> \n>     What problem are you having paring the current output? You say this\n>     patch provides a robust alternative but I'm afraid it does not seem to\n>     handle filenames containing newlines or double quotes and it assumes\n>     they are utf-8 encoded. It also seems to print a trailing comma if an\n>     array has more than one item which is not allowed by the json spec. We\n>     already have support for generating json output as documented\n>     json-writer.h which will help with most of that but not the filename\n>     encoding problem. New features should also be accompanied by tests to\n>     prevent future regressions. I'm not opposed to adding json output if it\n>     is robust and handles non utf-8 filenames but it would be helpful to\n>     understand what problems you are having parsing the output of \"git\n>     status --porcelain[=v2]\".\n> \n>     Thanks\n> \n>     Phillip\n> \n>      > Signed-off-by: Tachera Sasi <tachera@ekilie.com\n>     <mailto:tachera@ekilie.com>>\n>      > Signed-off-by: tacherasasi <tacherasasi@gmail.com\n>     <mailto:tacherasasi@gmail.com>>\n>      > ---\n>      >      status: add --json output format to git status\n>      >\n>      > Published-As: https://github.com/gitgitgadget/git/releases/tag/\n>     pr-1937%2FtacheraSasi%2Ffeature%2Fjson-status-output-v1 <https://\n>     github.com/gitgitgadget/git/releases/tag/\n>     pr-1937%2FtacheraSasi%2Ffeature%2Fjson-status-output-v1>\n>      > Fetch-It-Via: git fetch https://github.com/gitgitgadget/git\n>     <https://github.com/gitgitgadget/git> pr-1937/tacheraSasi/feature/\n>     json-status-output-v1\n>      > Pull-Request: https://github.com/gitgitgadget/git/pull/1937\n>     <https://github.com/gitgitgadget/git/pull/1937>\n>      >\n>      >   builtin/commit.c |  9 ++++-\n>      >   wt-status.c      | 98 +++++++++++++++++++++++++++++++++++++++++\n>     +++++++\n>      >   wt-status.h      |  1 +\n>      >   3 files changed, 107 insertions(+), 1 deletion(-)\n>      >\n>      > diff --git a/builtin/commit.c b/builtin/commit.c\n>      > index fba0dded64a..f1db4fdfd9a 100644\n>      > --- a/builtin/commit.c\n>      > +++ b/builtin/commit.c\n>      > @@ -1540,6 +1540,9 @@ struct repository *repo UNUSED)\n>      >               OPT_SET_INT(0, \"long\", &status_format,\n>      >                           N_(\"show status in long format (default)\"),\n>      >                           STATUS_FORMAT_LONG),\n>      > +             OPT_SET_INT(0, \"json\", &status_format,\n>      > +                 N_(\"show status in JSON format\"),\n>      > +                 STATUS_FORMAT_JSON),\n>      >               OPT_BOOL('z', \"null\", &s.null_termination,\n>      >                        N_(\"terminate entries with NUL\")),\n>      >               {\n>      > @@ -1603,7 +1606,8 @@ struct repository *repo UNUSED)\n>      >                      prefix, argv);\n>      >\n>      >       if (status_format != STATUS_FORMAT_PORCELAIN &&\n>      > -         status_format != STATUS_FORMAT_PORCELAIN_V2)\n>      > +         status_format != STATUS_FORMAT_PORCELAIN_V2 &&\n>      > +         status_format != STATUS_FORMAT_JSON)\n>      >               progress_flag = REFRESH_PROGRESS;\n>      >       repo_read_index(the_repository);\n>      >       refresh_index(the_repository->index,\n>      > @@ -1735,6 +1739,9 @@ int cmd_commit(int argc,\n>      >               OPT_SET_INT(0, \"long\", &status_format,\n>      >                           N_(\"show status in long format (default)\"),\n>      >                           STATUS_FORMAT_LONG),\n>      > +             OPT_SET_INT(0, \"json\", &status_format,\n>      > +                 N_(\"show status in JSON format\"),\n>      > +                 STATUS_FORMAT_JSON),\n>      >               OPT_BOOL('z', \"null\", &s.null_termination,\n>      >                        N_(\"terminate entries with NUL\")),\n>      >               OPT_BOOL(0, \"amend\", &amend, N_(\"amend previous\n>     commit\")),\n>      > diff --git a/wt-status.c b/wt-status.c\n>      > index 454601afa15..7192fb4d057 100644\n>      > --- a/wt-status.c\n>      > +++ b/wt-status.c\n>      > @@ -2564,6 +2564,101 @@ static void wt_porcelain_v2_print(struct\n>     wt_status *s)\n>      >       }\n>      >   }\n>      >\n>      > +\n>      > +static void wt_json_print_string_array(struct wt_status *s,\n>     const char *name, struct string_list *list)\n>      > +{\n>      > +     int i;\n>      > +     fprintf(s->fp, \"  \\\"%s\\\": [\", name);\n>      > +     for (i = 0; i < list->nr; i++) {\n>      > +             if (i > 0)\n>      > +                     fprintf(s->fp, \", \");\n>      > +             fprintf(s->fp, \"\\\"%s\\\"\", list->items[i].string);\n>      > +     }\n>      > +     fprintf(s->fp, \"]\");\n>      > +}\n>      > +\n>      > +static void wt_json_print_change_array(struct wt_status *s,\n>     const char *name, int change_type)\n>      > +{\n>      > +     int i;\n>      > +     struct string_list files = STRING_LIST_INIT_DUP;\n>      > +\n>      > +     for (i = 0; i < s->change.nr <http://change.nr>; i++) {\n>      > +             struct wt_status_change_data *d;\n>      > +             struct string_list_item *it;\n>      > +             it = &(s->change.items[i]);\n>      > +             d = it->util;\n>      > +\n>      > +             if ((change_type == WT_STATUS_UPDATED && d-\n>      >index_status &&\n>      > +                  d->index_status != DIFF_STATUS_UNMERGED) ||\n>      > +                 (change_type == WT_STATUS_CHANGED && d-\n>      >worktree_status &&\n>      > +                  d->worktree_status != DIFF_STATUS_UNMERGED)) {\n>      > +                     string_list_append(&files, it->string);\n>      > +             }\n>      > +     }\n>      > +\n>      > +     wt_json_print_string_array(s, name, &files);\n>      > +     string_list_clear(&files, 0);\n>      > +}\n>      > +\n>      > +static void wt_json_print_branch_info(struct wt_status *s)\n>      > +{\n>      > +     struct branch *branch;\n>      > +     const char *branch_name;\n>      > +     int ahead = 0, behind = 0;\n>      > +\n>      > +     fprintf(s->fp, \"  \\\"branch\\\": {\\n\");\n>      > +\n>      > +     if (s->branch && !s->is_initial) {\n>      > +             if (!strcmp(s->branch, \"HEAD\")) {\n>      > +                     fprintf(s->fp, \"    \\\"current\\\": \\\"HEAD\\\",\\n\");\n>      > +                     fprintf(s->fp, \"    \\\"detached\\\": true\");\n>      > +             } else {\n>      > +                     if (skip_prefix(s->branch, \"refs/heads/\",\n>     &branch_name)) {\n>      > +                             fprintf(s->fp, \"    \\\"current\\\":\n>     \\\"%s\\\",\\n\", branch_name);\n>      > +                             fprintf(s->fp, \"    \\\"detached\\\":\n>     false\");\n>      > +\n>      > +                             branch = branch_get(branch_name);\n>      > +                             if (branch && branch->merge &&\n>     branch->merge[0] && branch->merge[0]->dst) {\n>      > +                                     if (!\n>     stat_tracking_info(branch, &ahead, &behind, NULL, 0, 0)) {\n>      > +                                             fprintf(s->fp, \",\n>     \\n    \\\"ahead\\\": %d,\\n    \\\"behind\\\": %d\", ahead, behind);\n>      > +                                     }\n>      > +                             }\n>      > +                     } else {\n>      > +                             fprintf(s->fp, \"    \\\"current\\\":\n>     \\\"%s\\\",\\n\", s->branch);\n>      > +                             fprintf(s->fp, \"    \\\"detached\\\":\n>     false\");\n>      > +                     }\n>      > +             }\n>      > +     } else {\n>      > +             fprintf(s->fp, \"    \\\"current\\\": null,\\n\");\n>      > +             fprintf(s->fp, \"    \\\"detached\\\": false\");\n>      > +     }\n>      > +\n>      > +     fprintf(s->fp, \"\\n  }\");\n>      > +}\n>      > +\n>      > +static void wt_json_status_print(struct wt_status *s)\n>      > +{\n>      > +     fprintf(s->fp, \"{\\n\");\n>      > +\n>      > +     wt_json_print_branch_info(s);\n>      > +     fprintf(s->fp, \",\\n\");\n>      > +\n>      > +     wt_json_print_change_array(s, \"staged\", WT_STATUS_UPDATED);\n>      > +     fprintf(s->fp, \",\\n\");\n>      > +\n>      > +     wt_json_print_change_array(s, \"unstaged\", WT_STATUS_CHANGED);\n>      > +     fprintf(s->fp, \",\\n\");\n>      > +\n>      > +     wt_json_print_string_array(s, \"untracked\", &s->untracked);\n>      > +\n>      > +     if (s->ignored.nr <http://ignored.nr> > 0) {\n>      > +             fprintf(s->fp, \",\\n\");\n>      > +             wt_json_print_string_array(s, \"ignored\", &s->ignored);\n>      > +     }\n>      > +\n>      > +     fprintf(s->fp, \"\\n}\\n\");\n>      > +}\n>      > +\n>      >   void wt_status_print(struct wt_status *s)\n>      >   {\n>      >       trace2_data_intmax(\"status\", s->repo, \"count/changed\", s-\n>      >change.nr <http://change.nr>);\n>      > @@ -2583,6 +2678,9 @@ void wt_status_print(struct wt_status *s)\n>      >       case STATUS_FORMAT_PORCELAIN_V2:\n>      >               wt_porcelain_v2_print(s);\n>      >               break;\n>      > +     case STATUS_FORMAT_JSON:\n>      > +             wt_json_status_print(s);\n>      > +             break;\n>      >       case STATUS_FORMAT_UNSPECIFIED:\n>      >               BUG(\"finalize_deferred_config() should have been\n>     called\");\n>      >               break;\n>      > diff --git a/wt-status.h b/wt-status.h\n>      > index 4e377ce62b8..e929af832b2 100644\n>      > --- a/wt-status.h\n>      > +++ b/wt-status.h\n>      > @@ -74,6 +74,7 @@ enum wt_status_format {\n>      >       STATUS_FORMAT_SHORT,\n>      >       STATUS_FORMAT_PORCELAIN,\n>      >       STATUS_FORMAT_PORCELAIN_V2,\n>      > +     STATUS_FORMAT_JSON,\n>      >\n>      >       STATUS_FORMAT_UNSPECIFIED\n>      >   };\n>      >\n>      > base-commit: cb3b40381e1d5ee32dde96521ad7cfd68eb308a6\n> \n\n"},{"id":"523007","messageId":"xmqqo6t1n2sm.fsf@gitster.g","threadId":"63876","inReplyTo":"pull.1937.git.1753856826464.gitgitgadget@gmail.com","subject":"Re: [PATCH] status: add --json output format to git status","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-07-30T14:58:33Z","receivedAt":"2025-07-30T14:58:36Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"\"Tach via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n\n> From: tacherasasi <tacherasasi@gmail.com>\n\n> Add a new --json flag to 'git status' that outputs repository state\n> in a structured JSON format. This enables reliable machine parsing\n> of status information for tools and automation.\n\nThe --porcelain format is meant to serve this \"parseable for tools\nand automation to read\" use case.  Was there anything your parser\ncouldn't fathom out of the format?\n\n> This provides a robust alternative to parsing traditional output\n> formats, making it easier to build reliable tools and automation\n> around Git status information.\n\nThe writer is obviously biased ;-) but I find this a bit hand-wavy\nand unconvincing.\n\n> Signed-off-by: Tachera Sasi <tachera@ekilie.com>\n> Signed-off-by: tacherasasi <tacherasasi@gmail.com>\n\nIt is not like two separate people worked on this patch, or somebody\nat ekilie worked on the patch but asked a different person with a\nvery similar name to send it on their behalf, right?\n\nPerhaps update the user.{name,email} on the in-body From: to match\nthe one used on the first Sign-off, and drop the second Sign-off?\n\n> diff --git a/builtin/commit.c b/builtin/commit.c\n> index fba0dded64a..f1db4fdfd9a 100644\n> --- a/builtin/commit.c\n> +++ b/builtin/commit.c\n> @@ -1540,6 +1540,9 @@ struct repository *repo UNUSED)\n>  \t\tOPT_SET_INT(0, \"long\", &status_format,\n>  \t\t\t    N_(\"show status in long format (default)\"),\n>  \t\t\t    STATUS_FORMAT_LONG),\n> +\t\tOPT_SET_INT(0, \"json\", &status_format,\n> +\t\t    N_(\"show status in JSON format\"),\n> +\t\t    STATUS_FORMAT_JSON),\n\nThere are now several ways to set to status_format now.  It could be\na good idea to make a preliminary UI clean-up to introduce a new\noption \"--format={short,long,porcelain,porcelain-v1,porcelain-v2}\"\nas a separate step, declare that any new format will use this new\noption without adding a separate option on its own, before adding\nany new features.  And then on top that, a new format can be\nintroduced without cluttering the set of options.\n\nNot that I am enthused to see json here, given that we have\nintroduced porcelain format for the same purpose eons ago.\n\n> @@ -1603,7 +1606,8 @@ struct repository *repo UNUSED)\n>  \t\t       prefix, argv);\n>  \n>  \tif (status_format != STATUS_FORMAT_PORCELAIN &&\n> -\t    status_format != STATUS_FORMAT_PORCELAIN_V2)\n> +\t    status_format != STATUS_FORMAT_PORCELAIN_V2 &&\n> +\t    status_format != STATUS_FORMAT_JSON)\n>  \t\tprogress_flag = REFRESH_PROGRESS;\n>  \trepo_read_index(the_repository);\n>  \trefresh_index(the_repository->index,\n\nAlso, we might want to tweak the bit-assignment for the\nstatus_format constants to make this part easier to maintain (e.g.,\nones below 100 are for human consumption and gives progress bar and\nones above 100 are for machine consumption and suppresses progress,\nor something like that), again as a separate preliminary clean-up\nstep before adding any new features.\n\n> @@ -1735,6 +1739,9 @@ int cmd_commit(int argc,\n>  \t\tOPT_SET_INT(0, \"long\", &status_format,\n>  \t\t\t    N_(\"show status in long format (default)\"),\n>  \t\t\t    STATUS_FORMAT_LONG),\n> +\t\tOPT_SET_INT(0, \"json\", &status_format,\n> +\t\t    N_(\"show status in JSON format\"),\n> +\t\t    STATUS_FORMAT_JSON),\n\nAnd if we are to clean-up with --format=<name> and build on top,\nthis part would become somewhat different.  The clean-up would\nprobably be somewhat like what opt_parse_porcelain() is doing and\n\"json\" or \"yaml\" or any new format name would join if/else if/\ncascade there.\n\n> diff --git a/wt-status.c b/wt-status.c\n> index 454601afa15..7192fb4d057 100644\n> --- a/wt-status.c\n> +++ b/wt-status.c\n> @@ -2564,6 +2564,101 @@ static void wt_porcelain_v2_print(struct wt_status *s)\n>  \t}\n>  }\n>  \n> +\n> +static void wt_json_print_string_array(struct wt_status *s, const char *name, struct string_list *list)\n> +{\n\nWe seem to have a json write-out library-ish helpers already; is it\ntoo tied to its current use (i.e. trace output) and too inflexible\nto be used here, or does the status output have so specialized and\nunusual need as a normal JSON producing application that we need a\nspecial purpose code here with bunch of fprintf() and manual\nformatting?\n\nThanks.\n"},{"id":"523442","messageId":"d62a30f6-af30-489b-ab55-4c01ed3f60c9@gmail.com","threadId":"63876","inReplyTo":"xmqqo6t1n2sm.fsf@gitster.g","subject":"Re: [PATCH] status: add --json output format to git status","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-08-04T13:49:53Z","receivedAt":"2025-08-04T13:49:59Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 30/07/2025 15:58, Junio C Hamano wrote:\n> \"Tach via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n> \n>> This provides a robust alternative to parsing traditional output\n>> formats, making it easier to build reliable tools and automation\n>> around Git status information.\n> \n> The writer is obviously biased ;-) but I find this a bit hand-wavy\n> and unconvincing.\n\nI agree this isn't a very convincing reason. To me a more convincing \nreason is that virtually every scripting language comes with a way to \nparse json and base64 so if we emit json with path and ref names encoded \nas base64 there is no need for script authors to write a parser to parse \nour porcelain output. To me the fact that we have to encode names using \nbase64 is a sign that json is not particularly well suited to the task \nbut it is widely supported format.\n\nThanks\n\nPhillip\n\n"},{"id":"523444","messageId":"xmqqikj39oq3.fsf@gitster.g","threadId":"63876","inReplyTo":"d62a30f6-af30-489b-ab55-4c01ed3f60c9@gmail.com","subject":"Re: [PATCH] status: add --json output format to git status","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-08-04T13:54:28Z","receivedAt":"2025-08-04T13:54:30Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Phillip Wood <phillip.wood123@gmail.com> writes:\n\n> On 30/07/2025 15:58, Junio C Hamano wrote:\n>> \"Tach via GitGitGadget\" <gitgitgadget@gmail.com> writes:\n>> \n>>> This provides a robust alternative to parsing traditional output\n>>> formats, making it easier to build reliable tools and automation\n>>> around Git status information.\n>> The writer is obviously biased ;-) but I find this a bit hand-wavy\n>> and unconvincing.\n>\n> I agree this isn't a very convincing reason. To me a more convincing\n> reason is that virtually every scripting language comes with a way to\n> parse json and base64 so if we emit json with path and ref names\n> encoded as base64 there is no need for script authors to write a\n> parser to parse our porcelain output. To me the fact that we have to\n> encode names using base64 is a sign that json is not particularly well\n> suited to the task but it is widely supported format.\n\nVery well put.  Thanks.\n"}]}