Coverage Report

Created: 2025-12-31 07:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/git/column.c
Line
Count
Source
1
#define DISABLE_SIGN_COMPARE_WARNINGS
2
3
#include "git-compat-util.h"
4
#include "config.h"
5
#include "column.h"
6
#include "string-list.h"
7
#include "pager.h"
8
#include "parse-options.h"
9
#include "run-command.h"
10
#include "utf8.h"
11
12
0
#define XY2LINEAR(d, x, y) (COL_LAYOUT((d)->colopts) == COL_COLUMN ? \
13
0
          (x) * (d)->rows + (y) : \
14
0
          (y) * (d)->cols + (x))
15
16
struct column_data {
17
  const struct string_list *list;
18
  unsigned int colopts;
19
  struct column_options opts;
20
21
  int rows, cols;
22
  int *len;   /* cell length */
23
  int *width;       /* index to the longest row in column */
24
};
25
26
/* return length of 's' in letters, ANSI escapes stripped */
27
static int item_length(const char *s)
28
0
{
29
0
  return utf8_strnwidth(s, strlen(s), 1);
30
0
}
31
32
/*
33
 * Calculate cell width, rows and cols for a table of equal cells, given
34
 * table width and how many spaces between cells.
35
 */
36
static void layout(struct column_data *data, int *width)
37
0
{
38
0
  int i;
39
40
0
  *width = 0;
41
0
  for (i = 0; i < data->list->nr; i++)
42
0
    if (*width < data->len[i])
43
0
      *width = data->len[i];
44
45
0
  *width += data->opts.padding;
46
47
0
  data->cols = (data->opts.width - strlen(data->opts.indent)) / *width;
48
0
  if (data->cols == 0)
49
0
    data->cols = 1;
50
51
0
  data->rows = DIV_ROUND_UP(data->list->nr, data->cols);
52
0
}
53
54
static void compute_column_width(struct column_data *data)
55
0
{
56
0
  int i, x, y;
57
0
  for (x = 0; x < data->cols; x++) {
58
0
    data->width[x] = XY2LINEAR(data, x, 0);
59
0
    for (y = 0; y < data->rows; y++) {
60
0
      i = XY2LINEAR(data, x, y);
61
0
      if (i < data->list->nr &&
62
0
          data->len[data->width[x]] < data->len[i])
63
0
        data->width[x] = i;
64
0
    }
65
0
  }
66
0
}
67
68
/*
69
 * Shrink all columns by shortening them one row each time (and adding
70
 * more columns along the way). Hopefully the longest cell will be
71
 * moved to the next column, column is shrunk so we have more space
72
 * for new columns. The process ends when the whole thing no longer
73
 * fits in data->total_width.
74
 */
75
static void shrink_columns(struct column_data *data)
76
0
{
77
0
  REALLOC_ARRAY(data->width, data->cols);
78
0
  while (data->rows > 1) {
79
0
    int x, total_width, cols, rows;
80
0
    rows = data->rows;
81
0
    cols = data->cols;
82
83
0
    data->rows--;
84
0
    data->cols = DIV_ROUND_UP(data->list->nr, data->rows);
85
0
    if (data->cols != cols)
86
0
      REALLOC_ARRAY(data->width, data->cols);
87
0
    compute_column_width(data);
88
89
0
    total_width = strlen(data->opts.indent);
90
0
    for (x = 0; x < data->cols; x++) {
91
0
      total_width += data->len[data->width[x]];
92
0
      total_width += data->opts.padding;
93
0
    }
94
0
    if (total_width > data->opts.width) {
95
0
      data->rows = rows;
96
0
      data->cols = cols;
97
0
      break;
98
0
    }
99
0
  }
100
0
  compute_column_width(data);
101
0
}
102
103
/* Display without layout when not enabled */
104
static void display_plain(const struct string_list *list,
105
        const char *indent, const char *nl)
106
0
{
107
0
  int i;
108
109
0
  for (i = 0; i < list->nr; i++)
110
0
    printf("%s%s%s", indent, list->items[i].string, nl);
111
0
}
112
113
/* Print a cell to stdout with all necessary leading/trailing space */
114
static int display_cell(struct column_data *data, int initial_width,
115
      const char *empty_cell, int x, int y)
116
0
{
117
0
  int i, len, newline;
118
119
0
  i = XY2LINEAR(data, x, y);
120
0
  if (i >= data->list->nr)
121
0
    return -1;
122
123
0
  len = data->len[i];
124
0
  if (data->width && data->len[data->width[x]] < initial_width) {
125
    /*
126
     * empty_cell has initial_width chars, if real column
127
     * is narrower, increase len a bit so we fill less
128
     * space.
129
     */
130
0
    len += initial_width - data->len[data->width[x]];
131
0
    len -= data->opts.padding;
132
0
  }
133
134
0
  if (COL_LAYOUT(data->colopts) == COL_COLUMN)
135
0
    newline = i + data->rows >= data->list->nr;
136
0
  else
137
0
    newline = x == data->cols - 1 || i == data->list->nr - 1;
138
139
0
  printf("%s%s%s",
140
0
         x == 0 ? data->opts.indent : "",
141
0
         data->list->items[i].string,
142
0
         newline ? data->opts.nl : empty_cell + len);
143
0
  return 0;
144
0
}
145
146
/* Display COL_COLUMN or COL_ROW */
147
static void display_table(const struct string_list *list,
148
        unsigned int colopts,
149
        const struct column_options *opts)
150
0
{
151
0
  struct column_data data;
152
0
  int x, y, i, initial_width;
153
0
  char *empty_cell;
154
155
0
  memset(&data, 0, sizeof(data));
156
0
  data.list = list;
157
0
  data.colopts = colopts;
158
0
  data.opts = *opts;
159
160
0
  ALLOC_ARRAY(data.len, list->nr);
161
0
  for (i = 0; i < list->nr; i++)
162
0
    data.len[i] = item_length(list->items[i].string);
163
164
0
  layout(&data, &initial_width);
165
166
0
  if (colopts & COL_DENSE)
167
0
    shrink_columns(&data);
168
169
0
  empty_cell = xmallocz(initial_width);
170
0
  memset(empty_cell, ' ', initial_width);
171
0
  for (y = 0; y < data.rows; y++) {
172
0
    for (x = 0; x < data.cols; x++)
173
0
      if (display_cell(&data, initial_width, empty_cell, x, y))
174
0
        break;
175
0
  }
176
177
0
  free(data.len);
178
0
  free(data.width);
179
0
  free(empty_cell);
180
0
}
181
182
void print_columns(const struct string_list *list, unsigned int colopts,
183
       const struct column_options *opts)
184
0
{
185
0
  struct column_options nopts;
186
187
0
  if (opts && (0 > opts->padding))
188
0
    BUG("padding must be non-negative");
189
0
  if (!list->nr)
190
0
    return;
191
0
  assert((colopts & COL_ENABLE_MASK) != COL_AUTO);
192
193
0
  memset(&nopts, 0, sizeof(nopts));
194
0
  nopts.indent = opts && opts->indent ? opts->indent : "";
195
0
  nopts.nl = opts && opts->nl ? opts->nl : "\n";
196
0
  nopts.padding = opts ? opts->padding : 1;
197
0
  nopts.width = opts && opts->width ? opts->width : term_columns() - 1;
198
0
  if (!column_active(colopts)) {
199
0
    display_plain(list, "", "\n");
200
0
    return;
201
0
  }
202
0
  switch (COL_LAYOUT(colopts)) {
203
0
  case COL_PLAIN:
204
0
    display_plain(list, nopts.indent, nopts.nl);
205
0
    break;
206
0
  case COL_ROW:
207
0
  case COL_COLUMN:
208
0
    display_table(list, colopts, &nopts);
209
0
    break;
210
0
  default:
211
0
    BUG("invalid layout mode %d", COL_LAYOUT(colopts));
212
0
  }
213
0
}
214
215
int finalize_colopts(unsigned int *colopts, int stdout_is_tty)
216
0
{
217
0
  if ((*colopts & COL_ENABLE_MASK) == COL_AUTO) {
218
0
    if (stdout_is_tty < 0)
219
0
      stdout_is_tty = isatty(1);
220
0
    *colopts &= ~COL_ENABLE_MASK;
221
0
    if (stdout_is_tty || pager_in_use())
222
0
      *colopts |= COL_ENABLED;
223
0
  }
224
0
  return 0;
225
0
}
226
227
struct colopt {
228
  const char *name;
229
  unsigned int value;
230
  unsigned int mask;
231
};
232
233
0
#define LAYOUT_SET 1
234
0
#define ENABLE_SET 2
235
236
static int parse_option(const char *arg, int len, unsigned int *colopts,
237
      int *group_set)
238
0
{
239
0
  struct colopt opts[] = {
240
0
    { "always", COL_ENABLED,  COL_ENABLE_MASK },
241
0
    { "never",  COL_DISABLED, COL_ENABLE_MASK },
242
0
    { "auto",   COL_AUTO,     COL_ENABLE_MASK },
243
0
    { "plain",  COL_PLAIN,    COL_LAYOUT_MASK },
244
0
    { "column", COL_COLUMN,   COL_LAYOUT_MASK },
245
0
    { "row",    COL_ROW,      COL_LAYOUT_MASK },
246
0
    { "dense",  COL_DENSE,    0 },
247
0
  };
248
0
  int i;
249
250
0
  for (i = 0; i < ARRAY_SIZE(opts); i++) {
251
0
    int set = 1, arg_len = len, name_len;
252
0
    const char *arg_str = arg;
253
254
0
    if (!opts[i].mask) {
255
0
      if (arg_len > 2 && !strncmp(arg_str, "no", 2)) {
256
0
        arg_str += 2;
257
0
        arg_len -= 2;
258
0
        set = 0;
259
0
      }
260
0
    }
261
262
0
    name_len = strlen(opts[i].name);
263
0
    if (arg_len != name_len ||
264
0
        strncmp(arg_str, opts[i].name, name_len))
265
0
      continue;
266
267
0
    switch (opts[i].mask) {
268
0
    case COL_ENABLE_MASK:
269
0
      *group_set |= ENABLE_SET;
270
0
      break;
271
0
    case COL_LAYOUT_MASK:
272
0
      *group_set |= LAYOUT_SET;
273
0
      break;
274
0
    }
275
276
0
    if (opts[i].mask)
277
0
      *colopts = (*colopts & ~opts[i].mask) | opts[i].value;
278
0
    else {
279
0
      if (set)
280
0
        *colopts |= opts[i].value;
281
0
      else
282
0
        *colopts &= ~opts[i].value;
283
0
    }
284
0
    return 0;
285
0
  }
286
287
0
  return error("unsupported option '%s'", arg);
288
0
}
289
290
static int parse_config(unsigned int *colopts, const char *value)
291
0
{
292
0
  const char *sep = " ,";
293
0
  int group_set = 0;
294
295
0
  while (*value) {
296
0
    int len = strcspn(value, sep);
297
0
    if (len) {
298
0
      if (parse_option(value, len, colopts, &group_set))
299
0
        return -1;
300
301
0
      value += len;
302
0
    }
303
0
    value += strspn(value, sep);
304
0
  }
305
  /*
306
   * If none of "always", "never", and "auto" is specified, then setting
307
   * layout implies "always".
308
   *
309
   * Current value in COL_ENABLE_MASK is disregarded. This means if
310
   * you set column.ui = auto and pass --column=row, then "auto"
311
   * will become "always".
312
   */
313
0
  if ((group_set & LAYOUT_SET) && !(group_set & ENABLE_SET))
314
0
    *colopts = (*colopts & ~COL_ENABLE_MASK) | COL_ENABLED;
315
0
  return 0;
316
0
}
317
318
static int column_config(const char *var, const char *value,
319
       const char *key, unsigned int *colopts)
320
0
{
321
0
  if (!value)
322
0
    return config_error_nonbool(var);
323
0
  if (parse_config(colopts, value))
324
0
    return error("invalid column.%s mode %s", key, value);
325
0
  return 0;
326
0
}
327
328
int git_column_config(const char *var, const char *value,
329
          const char *command, unsigned int *colopts)
330
0
{
331
0
  const char *it;
332
333
0
  if (!skip_prefix(var, "column.", &it))
334
0
    return 0;
335
336
0
  if (!strcmp(it, "ui"))
337
0
    return column_config(var, value, "ui", colopts);
338
339
0
  if (command && !strcmp(it, command))
340
0
    return column_config(var, value, it, colopts);
341
342
0
  return 0;
343
0
}
344
345
int parseopt_column_callback(const struct option *opt,
346
           const char *arg, int unset)
347
0
{
348
0
  unsigned int *colopts = opt->value;
349
0
  *colopts |= COL_PARSEOPT;
350
0
  *colopts &= ~COL_ENABLE_MASK;
351
0
  if (unset)   /* --no-column == never */
352
0
    return 0;
353
  /* --column == always unless "arg" states otherwise */
354
0
  *colopts |= COL_ENABLED;
355
0
  if (arg)
356
0
    return parse_config(colopts, arg);
357
358
0
  return 0;
359
0
}
360
361
static int fd_out = -1;
362
static struct child_process column_process = CHILD_PROCESS_INIT;
363
364
int run_column_filter(int colopts, const struct column_options *opts)
365
0
{
366
0
  struct strvec *argv;
367
368
0
  if (opts && (0 > opts->padding))
369
0
    BUG("padding must be non-negative");
370
0
  if (fd_out != -1)
371
0
    return -1;
372
373
0
  child_process_init(&column_process);
374
0
  argv = &column_process.args;
375
376
0
  strvec_push(argv, "column");
377
0
  strvec_pushf(argv, "--raw-mode=%d", colopts);
378
0
  if (opts && opts->width)
379
0
    strvec_pushf(argv, "--width=%d", opts->width);
380
0
  if (opts && opts->indent)
381
0
    strvec_pushf(argv, "--indent=%s", opts->indent);
382
0
  if (opts && opts->padding)
383
0
    strvec_pushf(argv, "--padding=%d", opts->padding);
384
385
0
  fflush(stdout);
386
0
  column_process.in = -1;
387
0
  column_process.out = dup(1);
388
0
  column_process.git_cmd = 1;
389
390
0
  if (start_command(&column_process))
391
0
    return -2;
392
393
0
  fd_out = dup(1);
394
0
  close(1);
395
0
  dup2(column_process.in, 1);
396
0
  close(column_process.in);
397
0
  return 0;
398
0
}
399
400
int stop_column_filter(void)
401
0
{
402
0
  if (fd_out == -1)
403
0
    return -1;
404
405
0
  fflush(stdout);
406
0
  close(1);
407
0
  finish_command(&column_process);
408
0
  dup2(fd_out, 1);
409
0
  close(fd_out);
410
0
  fd_out = -1;
411
0
  return 0;
412
0
}