From 98333d54cf34f9bdc3a8e0dc65d16ea579392d03 Mon Sep 17 00:00:00 2001 From: tacherasasi Date: Mon, 23 Jun 2025 15:46:44 +0300 Subject: [PATCH] status: add --json output format to git status 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. Signed-off-by: Tachera Sasi Signed-off-by: tacherasasi --- 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 fba0dded64a718..f1db4fdfd9a34c 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 454601afa15a95..030f3e8fac47ca 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 4e377ce62b8b28..e929af832b297f 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 };