From: phillip.wood123@gmail.com Date: Wed, 30 Jul 2025 10:08:43 GMT Subject: Re: [PATCH] status: add --json output format to git status Message-ID: <2da722a3-2ed0-4f2b-a623-df0dc4aaad77@gmail.com> In-Reply-To: Hi Tachera On 30/07/2025 10:15, Tachera W sasi wrote: > > For v2, I will: >   * Rework the patch to use `json-writer.h` so escaping and JSON > compliance are handled >     properly. That will handle the escaping problems but we still have the problem that filenames and refnames are not guaranteed to be utf-8 encoded. I think the only way to handle that is to base64 encode them so that the caller can retrieve the raw bytes. See https://lore.kernel.org/git/ZvXMSKaUWWA-MG9J@tapette.crustytoothpaste.net/ for a previous discussion about this. >   * Add tests covering filenames with quotes, newlines, and non-UTF-8 > cases. >   * Include tests under `t/` to prevent future regressions. That's great, you should update Documentation/git-status.adoc as well to document the new option and output format Thanks Phillip > Thanks again for your review and for pointing me towards the proper helpers. > > Best regards, > Tachera Sasi > > On Wed, Jul 30, 2025 at 12:01 PM Phillip Wood > wrote: > > On 30/07/2025 07:27, Tach via GitGitGadget wrote: > > From: tacherasasi > > > > > Add a new --json flag to 'git status' that outputs repository state > > in a structured JSON format. This enables reliable machine parsing > > of status information for tools and automation. > > > > The JSON output includes: > > - Branch information (name, detached state, ahead/behind counts) > > - Staged files array > > - Unstaged files array > > - Untracked files array > > - Ignored files array (with --ignored flag) > > > > Implementation details: > > - Add STATUS_FORMAT_JSON to wt_status_format enum > > - Add JSON output option to git status and git commit > > - Implement JSON formatting helpers for arrays and branch info > > - Structure output for easy parsing and future extensibility > > > > Example: > >    $ git status --json > >    { > >      "branch": { > >        "current": "main", > >        "detached": false > >      }, > >      "staged": ["file1.txt"], > >      "unstaged": ["file2.txt"], > >      "untracked": ["file3.txt"] > >    } > > > > This provides a robust alternative to parsing traditional output > > formats, making it easier to build reliable tools and automation > > around Git status information. > > What problem are you having paring the current output? You say this > patch provides a robust alternative but I'm afraid it does not seem to > handle filenames containing newlines or double quotes and it assumes > they are utf-8 encoded. It also seems to print a trailing comma if an > array has more than one item which is not allowed by the json spec. We > already have support for generating json output as documented > json-writer.h which will help with most of that but not the filename > encoding problem. New features should also be accompanied by tests to > prevent future regressions. I'm not opposed to adding json output if it > is robust and handles non utf-8 filenames but it would be helpful to > understand what problems you are having parsing the output of "git > status --porcelain[=v2]". > > Thanks > > Phillip > > > Signed-off-by: Tachera Sasi > > > Signed-off-by: tacherasasi > > > --- > >      status: add --json output format to git status > > > > Published-As: https://github.com/gitgitgadget/git/releases/tag/ > pr-1937%2FtacheraSasi%2Ffeature%2Fjson-status-output-v1 github.com/gitgitgadget/git/releases/tag/ > pr-1937%2FtacheraSasi%2Ffeature%2Fjson-status-output-v1> > > Fetch-It-Via: git fetch https://github.com/gitgitgadget/git > pr-1937/tacheraSasi/feature/ > json-status-output-v1 > > Pull-Request: https://github.com/gitgitgadget/git/pull/1937 > > > > >   builtin/commit.c |  9 ++++- > >   wt-status.c      | 98 +++++++++++++++++++++++++++++++++++++++++ > +++++++ > >   wt-status.h      |  1 + > >   3 files changed, 107 insertions(+), 1 deletion(-) > > > > diff --git a/builtin/commit.c b/builtin/commit.c > > index fba0dded64a..f1db4fdfd9a 100644 > > --- a/builtin/commit.c > > +++ b/builtin/commit.c > > @@ -1540,6 +1540,9 @@ struct repository *repo UNUSED) > >               OPT_SET_INT(0, "long", &status_format, > >                           N_("show status in long format (default)"), > >                           STATUS_FORMAT_LONG), > > +             OPT_SET_INT(0, "json", &status_format, > > +                 N_("show status in JSON format"), > > +                 STATUS_FORMAT_JSON), > >               OPT_BOOL('z', "null", &s.null_termination, > >                        N_("terminate entries with NUL")), > >               { > > @@ -1603,7 +1606,8 @@ struct repository *repo UNUSED) > >                      prefix, argv); > > > >       if (status_format != STATUS_FORMAT_PORCELAIN && > > -         status_format != STATUS_FORMAT_PORCELAIN_V2) > > +         status_format != STATUS_FORMAT_PORCELAIN_V2 && > > +         status_format != STATUS_FORMAT_JSON) > >               progress_flag = REFRESH_PROGRESS; > >       repo_read_index(the_repository); > >       refresh_index(the_repository->index, > > @@ -1735,6 +1739,9 @@ int cmd_commit(int argc, > >               OPT_SET_INT(0, "long", &status_format, > >                           N_("show status in long format (default)"), > >                           STATUS_FORMAT_LONG), > > +             OPT_SET_INT(0, "json", &status_format, > > +                 N_("show status in JSON format"), > > +                 STATUS_FORMAT_JSON), > >               OPT_BOOL('z', "null", &s.null_termination, > >                        N_("terminate entries with NUL")), > >               OPT_BOOL(0, "amend", &amend, N_("amend previous > commit")), > > diff --git a/wt-status.c b/wt-status.c > > index 454601afa15..7192fb4d057 100644 > > --- a/wt-status.c > > +++ b/wt-status.c > > @@ -2564,6 +2564,101 @@ static void wt_porcelain_v2_print(struct > wt_status *s) > >       } > >   } > > > > + > > +static void wt_json_print_string_array(struct wt_status *s, > const char *name, struct string_list *list) > > +{ > > +     int i; > > +     fprintf(s->fp, "  \"%s\": [", name); > > +     for (i = 0; i < list->nr; i++) { > > +             if (i > 0) > > +                     fprintf(s->fp, ", "); > > +             fprintf(s->fp, "\"%s\"", list->items[i].string); > > +     } > > +     fprintf(s->fp, "]"); > > +} > > + > > +static void wt_json_print_change_array(struct wt_status *s, > const char *name, int change_type) > > +{ > > +     int i; > > +     struct string_list files = STRING_LIST_INIT_DUP; > > + > > +     for (i = 0; i < s->change.nr ; i++) { > > +             struct wt_status_change_data *d; > > +             struct string_list_item *it; > > +             it = &(s->change.items[i]); > > +             d = it->util; > > + > > +             if ((change_type == WT_STATUS_UPDATED && d- > >index_status && > > +                  d->index_status != DIFF_STATUS_UNMERGED) || > > +                 (change_type == WT_STATUS_CHANGED && d- > >worktree_status && > > +                  d->worktree_status != DIFF_STATUS_UNMERGED)) { > > +                     string_list_append(&files, it->string); > > +             } > > +     } > > + > > +     wt_json_print_string_array(s, name, &files); > > +     string_list_clear(&files, 0); > > +} > > + > > +static void wt_json_print_branch_info(struct wt_status *s) > > +{ > > +     struct branch *branch; > > +     const char *branch_name; > > +     int ahead = 0, behind = 0; > > + > > +     fprintf(s->fp, "  \"branch\": {\n"); > > + > > +     if (s->branch && !s->is_initial) { > > +             if (!strcmp(s->branch, "HEAD")) { > > +                     fprintf(s->fp, "    \"current\": \"HEAD\",\n"); > > +                     fprintf(s->fp, "    \"detached\": true"); > > +             } else { > > +                     if (skip_prefix(s->branch, "refs/heads/", > &branch_name)) { > > +                             fprintf(s->fp, "    \"current\": > \"%s\",\n", branch_name); > > +                             fprintf(s->fp, "    \"detached\": > false"); > > + > > +                             branch = branch_get(branch_name); > > +                             if (branch && branch->merge && > branch->merge[0] && branch->merge[0]->dst) { > > +                                     if (! > stat_tracking_info(branch, &ahead, &behind, NULL, 0, 0)) { > > +                                             fprintf(s->fp, ", > \n    \"ahead\": %d,\n    \"behind\": %d", ahead, behind); > > +                                     } > > +                             } > > +                     } else { > > +                             fprintf(s->fp, "    \"current\": > \"%s\",\n", s->branch); > > +                             fprintf(s->fp, "    \"detached\": > false"); > > +                     } > > +             } > > +     } else { > > +             fprintf(s->fp, "    \"current\": null,\n"); > > +             fprintf(s->fp, "    \"detached\": false"); > > +     } > > + > > +     fprintf(s->fp, "\n  }"); > > +} > > + > > +static void wt_json_status_print(struct wt_status *s) > > +{ > > +     fprintf(s->fp, "{\n"); > > + > > +     wt_json_print_branch_info(s); > > +     fprintf(s->fp, ",\n"); > > + > > +     wt_json_print_change_array(s, "staged", WT_STATUS_UPDATED); > > +     fprintf(s->fp, ",\n"); > > + > > +     wt_json_print_change_array(s, "unstaged", WT_STATUS_CHANGED); > > +     fprintf(s->fp, ",\n"); > > + > > +     wt_json_print_string_array(s, "untracked", &s->untracked); > > + > > +     if (s->ignored.nr > 0) { > > +             fprintf(s->fp, ",\n"); > > +             wt_json_print_string_array(s, "ignored", &s->ignored); > > +     } > > + > > +     fprintf(s->fp, "\n}\n"); > > +} > > + > >   void wt_status_print(struct wt_status *s) > >   { > >       trace2_data_intmax("status", s->repo, "count/changed", s- > >change.nr ); > > @@ -2583,6 +2678,9 @@ void wt_status_print(struct wt_status *s) > >       case STATUS_FORMAT_PORCELAIN_V2: > >               wt_porcelain_v2_print(s); > >               break; > > +     case STATUS_FORMAT_JSON: > > +             wt_json_status_print(s); > > +             break; > >       case STATUS_FORMAT_UNSPECIFIED: > >               BUG("finalize_deferred_config() should have been > called"); > >               break; > > diff --git a/wt-status.h b/wt-status.h > > index 4e377ce62b8..e929af832b2 100644 > > --- a/wt-status.h > > +++ b/wt-status.h > > @@ -74,6 +74,7 @@ enum wt_status_format { > >       STATUS_FORMAT_SHORT, > >       STATUS_FORMAT_PORCELAIN, > >       STATUS_FORMAT_PORCELAIN_V2, > > +     STATUS_FORMAT_JSON, > > > >       STATUS_FORMAT_UNSPECIFIED > >   }; > > > > base-commit: cb3b40381e1d5ee32dde96521ad7cfd68eb308a6 >