Coverage Report

Created: 2026-11-12 06:03

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/git/sideband.c
Line
Count
Source
1
#define USE_THE_REPOSITORY_VARIABLE
2
#define DISABLE_SIGN_COMPARE_WARNINGS
3
4
#include "git-compat-util.h"
5
#include "color.h"
6
#include "config.h"
7
#include "editor.h"
8
#include "gettext.h"
9
#include "sideband.h"
10
#include "help.h"
11
#include "pkt-line.h"
12
#include "write-or-die.h"
13
#include "urlmatch.h"
14
15
struct keyword_entry {
16
  /*
17
   * We use keyword as config key so it should be a single alphanumeric word.
18
   */
19
  const char *keyword;
20
  char color[COLOR_MAXLEN];
21
};
22
23
static struct keyword_entry keywords[] = {
24
  { "hint", GIT_COLOR_YELLOW },
25
  { "warning",  GIT_COLOR_BOLD_YELLOW },
26
  { "success",  GIT_COLOR_BOLD_GREEN },
27
  { "error",  GIT_COLOR_BOLD_RED },
28
};
29
30
static enum {
31
  ALLOW_CONTROL_SEQUENCES_UNSET = -1,
32
  ALLOW_NO_CONTROL_CHARACTERS   = 0,
33
  ALLOW_ANSI_COLOR_SEQUENCES    = 1<<0,
34
  ALLOW_ANSI_CURSOR_MOVEMENTS   = 1<<1,
35
  ALLOW_ANSI_ERASE              = 1<<2,
36
  ALLOW_ALL_CONTROL_CHARACTERS  = 1<<3,
37
  ALLOW_DEFAULT_ANSI_SEQUENCES  = ALLOW_ANSI_COLOR_SEQUENCES
38
} allow_control_characters = ALLOW_CONTROL_SEQUENCES_UNSET;
39
40
static inline int skip_prefix_in_csv(const char *value, const char *prefix,
41
             const char **out)
42
0
{
43
0
  if (!skip_prefix(value, prefix, &value) ||
44
0
      (*value && *value != ','))
45
0
    return 0;
46
0
  *out = value + !!*value;
47
0
  return 1;
48
0
}
49
50
int sideband_allow_control_characters_config(const char *var, const char *value)
51
0
{
52
0
  switch (git_parse_maybe_bool(value)) {
53
0
  case 0:
54
0
    allow_control_characters = ALLOW_NO_CONTROL_CHARACTERS;
55
0
    return 0;
56
0
  case 1:
57
0
    allow_control_characters = ALLOW_ALL_CONTROL_CHARACTERS;
58
0
    return 0;
59
0
  default:
60
0
    break;
61
0
  }
62
63
0
  allow_control_characters = ALLOW_NO_CONTROL_CHARACTERS;
64
0
  while (*value) {
65
0
    if (skip_prefix_in_csv(value, "color", &value))
66
0
      allow_control_characters |= ALLOW_ANSI_COLOR_SEQUENCES;
67
0
    else if (skip_prefix_in_csv(value, "cursor", &value))
68
0
      allow_control_characters |= ALLOW_ANSI_CURSOR_MOVEMENTS;
69
0
    else if (skip_prefix_in_csv(value, "erase", &value))
70
0
      allow_control_characters |= ALLOW_ANSI_ERASE;
71
0
    else if (skip_prefix_in_csv(value, "true", &value))
72
0
      allow_control_characters = ALLOW_ALL_CONTROL_CHARACTERS;
73
0
    else if (skip_prefix_in_csv(value, "false", &value))
74
0
      allow_control_characters = ALLOW_NO_CONTROL_CHARACTERS;
75
0
    else
76
0
      warning(_("unrecognized value for '%s': '%s'"), var, value);
77
0
  }
78
0
  return 0;
79
0
}
80
81
static int sideband_config_callback(const char *var, const char *value,
82
            const struct config_context *ctx UNUSED,
83
            void *data UNUSED)
84
0
{
85
0
  if (!strcmp(var, "sideband.allowcontrolcharacters"))
86
0
    return sideband_allow_control_characters_config(var, value);
87
88
0
  return 0;
89
0
}
90
91
void sideband_apply_url_config(const char *url)
92
0
{
93
0
  struct urlmatch_config config = URLMATCH_CONFIG_INIT;
94
0
  char *normalized_url;
95
96
0
  if (!url)
97
0
    BUG("must not call sideband_apply_url_config(NULL)");
98
99
0
  config.section = "sideband";
100
0
  config.collect_fn = sideband_config_callback;
101
102
0
  normalized_url = url_normalize(url, &config.url);
103
0
  repo_config(the_repository, urlmatch_config_entry, &config);
104
0
  free(normalized_url);
105
0
  string_list_clear(&config.vars, 1);
106
0
  urlmatch_config_release(&config);
107
0
}
108
109
/* Returns a color setting (GIT_COLOR_NEVER, etc). */
110
static enum git_colorbool use_sideband_colors(void)
111
0
{
112
0
  static enum git_colorbool use_sideband_colors_cached = GIT_COLOR_UNKNOWN;
113
114
0
  const char *key = "color.remote";
115
0
  struct strbuf sb = STRBUF_INIT;
116
0
  const char *value;
117
0
  int i;
118
119
0
  if (use_sideband_colors_cached != GIT_COLOR_UNKNOWN)
120
0
    return use_sideband_colors_cached;
121
122
0
  if (allow_control_characters == ALLOW_CONTROL_SEQUENCES_UNSET) {
123
0
    if (!repo_config_get_value(the_repository, "sideband.allowcontrolcharacters", &value))
124
0
      sideband_allow_control_characters_config("sideband.allowcontrolcharacters", value);
125
126
0
    if (allow_control_characters == ALLOW_CONTROL_SEQUENCES_UNSET)
127
0
      allow_control_characters = ALLOW_DEFAULT_ANSI_SEQUENCES;
128
0
  }
129
130
0
  if (!repo_config_get_string_tmp(the_repository, key, &value))
131
0
    use_sideband_colors_cached = git_config_colorbool(key, value);
132
0
  else if (!repo_config_get_string_tmp(the_repository, "color.ui", &value))
133
0
    use_sideband_colors_cached = git_config_colorbool("color.ui", value);
134
0
  else
135
0
    use_sideband_colors_cached = GIT_COLOR_AUTO;
136
137
0
  for (i = 0; i < ARRAY_SIZE(keywords); i++) {
138
0
    strbuf_reset(&sb);
139
0
    strbuf_addf(&sb, "%s.%s", key, keywords[i].keyword);
140
0
    if (repo_config_get_string_tmp(the_repository, sb.buf, &value))
141
0
      continue;
142
0
    color_parse(value, keywords[i].color);
143
0
  }
144
145
0
  strbuf_release(&sb);
146
0
  return use_sideband_colors_cached;
147
0
}
148
149
void list_config_color_sideband_slots(struct string_list *list, const char *prefix)
150
0
{
151
0
  int i;
152
153
0
  for (i = 0; i < ARRAY_SIZE(keywords); i++)
154
0
    list_config_item(list, prefix, keywords[i].keyword);
155
0
}
156
157
static int handle_ansi_sequence(struct strbuf *dest, const char *src, int n)
158
0
{
159
0
  int i;
160
161
  /*
162
   * Valid ANSI color sequences are of the form
163
   *
164
   * ESC [ [<n> [; <n>]*] m
165
   *
166
   * where <n> can be either zero-length, or a decimal number, or a
167
   * series of decimal numbers separated by a colon (for 256-color or
168
   * true-color codes).
169
   *
170
   * These are part of the Select Graphic Rendition sequences which
171
   * contain more than just color sequences, for more details see
172
   * https://en.wikipedia.org/wiki/ANSI_escape_code#SGR.
173
   *
174
   * The cursor movement sequences are:
175
   *
176
   * ESC [ n A - Cursor up n lines (CUU)
177
   * ESC [ n B - Cursor down n lines (CUD)
178
   * ESC [ n C - Cursor forward n columns (CUF)
179
   * ESC [ n D - Cursor back n columns (CUB)
180
   * ESC [ n E - Cursor next line, beginning (CNL)
181
   * ESC [ n F - Cursor previous line, beginning (CPL)
182
   * ESC [ n G - Cursor to column n (CHA)
183
   * ESC [ n ; m H - Cursor position (row n, col m) (CUP)
184
   * ESC [ n ; m f - Same as H (HVP)
185
   *
186
   * The sequences to erase characters are:
187
   *
188
   *
189
   * ESC [ 0 J - Clear from cursor to end of screen (ED)
190
   * ESC [ 1 J - Clear from cursor to beginning of screen (ED)
191
   * ESC [ 2 J - Clear entire screen (ED)
192
   * ESC [ 3 J - Clear entire screen + scrollback (ED) - xterm extension
193
   * ESC [ 0 K - Clear from cursor to end of line (EL)
194
   * ESC [ 1 K - Clear from cursor to beginning of line (EL)
195
   * ESC [ 2 K - Clear entire line (EL)
196
   * ESC [ n M - Delete n lines (DL)
197
   * ESC [ n P - Delete n characters (DCH)
198
   * ESC [ n X - Erase n characters (ECH)
199
   *
200
   * For a comprehensive list of common ANSI Escape sequences, see
201
   * https://www.xfree86.org/current/ctlseqs.html
202
   */
203
204
0
  if (n < 3 || src[0] != '\x1b' || src[1] != '[')
205
0
    return 0;
206
207
0
  for (i = 2; i < n; i++) {
208
0
    if (((allow_control_characters & ALLOW_ANSI_COLOR_SEQUENCES) &&
209
0
         src[i] == 'm') ||
210
0
        ((allow_control_characters & ALLOW_ANSI_CURSOR_MOVEMENTS) &&
211
0
         strchr("ABCDEFGHf", src[i])) ||
212
0
        ((allow_control_characters & ALLOW_ANSI_ERASE) &&
213
0
         strchr("JKMPX", src[i]))) {
214
0
      strbuf_add(dest, src, i + 1);
215
0
      return i;
216
0
    }
217
0
    if (!isdigit(src[i]) && src[i] != ':' && src[i] != ';')
218
0
      break;
219
0
  }
220
221
0
  return 0;
222
0
}
223
224
static void strbuf_add_sanitized(struct strbuf *dest, const char *src, int n)
225
0
{
226
0
  int i;
227
228
0
  if ((allow_control_characters & ALLOW_ALL_CONTROL_CHARACTERS)) {
229
0
    strbuf_add(dest, src, n);
230
0
    return;
231
0
  }
232
233
0
  strbuf_grow(dest, n);
234
0
  for (; n && *src; src++, n--) {
235
0
    if (!iscntrl(*src) || *src == '\t' || *src == '\n') {
236
0
      strbuf_addch(dest, *src);
237
0
    } else if (allow_control_characters != ALLOW_NO_CONTROL_CHARACTERS &&
238
0
         (i = handle_ansi_sequence(dest, src, n))) {
239
0
      src += i;
240
0
      n -= i;
241
0
    } else {
242
0
      strbuf_addch(dest, '^');
243
0
      strbuf_addch(dest, *src == 0x7f ? '?' : 0x40 + *src);
244
0
    }
245
0
  }
246
0
}
247
248
/*
249
 * Optionally highlight one keyword in remote output if it appears at the start
250
 * of the line. This should be called for a single line only, which is
251
 * passed as the first N characters of the SRC array.
252
 *
253
 * It is fine to use "int n" here instead of "size_t n" as all calls to this
254
 * function pass an 'int' parameter. Additionally, the buffer involved in
255
 * storing these 'int' values takes input from a packet via the pkt-line
256
 * interface, which is capable of transferring only 64kB at a time.
257
 */
258
static void maybe_colorize_sideband(struct strbuf *dest, const char *src, int n)
259
0
{
260
0
  int i;
261
262
0
  if (!want_color_stderr(use_sideband_colors())) {
263
0
    strbuf_add_sanitized(dest, src, n);
264
0
    return;
265
0
  }
266
267
0
  while (0 < n && isspace(*src)) {
268
0
    strbuf_addch(dest, *src);
269
0
    src++;
270
0
    n--;
271
0
  }
272
273
0
  for (i = 0; i < ARRAY_SIZE(keywords); i++) {
274
0
    struct keyword_entry *p = keywords + i;
275
0
    int len = strlen(p->keyword);
276
277
0
    if (n < len)
278
0
      continue;
279
    /*
280
     * Match case insensitively, so we colorize output from existing
281
     * servers regardless of the case that they use for their
282
     * messages. We only highlight the word precisely, so
283
     * "successful" stays uncolored.
284
     */
285
0
    if (!strncasecmp(p->keyword, src, len) &&
286
0
        (len == n || !isalnum(src[len]))) {
287
0
      strbuf_addstr(dest, p->color);
288
0
      strbuf_add(dest, src, len);
289
0
      strbuf_addstr(dest, GIT_COLOR_RESET);
290
0
      n -= len;
291
0
      src += len;
292
0
      break;
293
0
    }
294
0
  }
295
296
0
  strbuf_add_sanitized(dest, src, n);
297
0
}
298
299
300
0
#define DISPLAY_PREFIX "remote: "
301
302
0
#define ANSI_PREFIX "\033[K"
303
0
#define DUMB_SUFFIX "        "
304
305
int demultiplex_sideband(const char *me, int status,
306
       char *buf, int len,
307
       int die_on_error,
308
       struct strbuf *scratch,
309
       enum sideband_type *sideband_type)
310
0
{
311
0
  static const char *prefix, *suffix;
312
0
  const char *b, *brk;
313
0
  int band;
314
315
0
  if (!suffix) {
316
0
    if (isatty(2) && !is_terminal_dumb()) {
317
0
      prefix = ANSI_PREFIX DISPLAY_PREFIX;
318
0
      suffix = "";
319
0
    } else {
320
0
      prefix = DISPLAY_PREFIX;
321
0
      suffix = DUMB_SUFFIX;
322
0
    }
323
0
  }
324
325
0
  if (status == PACKET_READ_EOF) {
326
0
    strbuf_addf(scratch,
327
0
          "%s%s: unexpected disconnect while reading sideband packet",
328
0
          scratch->len ? "\n" : "", me);
329
0
    *sideband_type = SIDEBAND_PROTOCOL_ERROR;
330
0
    goto cleanup;
331
0
  }
332
333
0
  if (len < 0)
334
0
    BUG("negative length on non-eof packet read");
335
336
0
  if (len == 0) {
337
0
    if (status == PACKET_READ_NORMAL) {
338
0
      strbuf_addf(scratch,
339
0
            "%s%s: protocol error: missing sideband designator",
340
0
            scratch->len ? "\n" : "", me);
341
0
      *sideband_type = SIDEBAND_PROTOCOL_ERROR;
342
0
    } else {
343
      /* covers flush, delim, etc */
344
0
      *sideband_type = SIDEBAND_FLUSH;
345
0
    }
346
0
    goto cleanup;
347
0
  }
348
349
0
  band = buf[0] & 0xff;
350
0
  buf[len] = '\0';
351
0
  len--;
352
0
  switch (band) {
353
0
  case 3:
354
0
    if (die_on_error)
355
0
      die(_("remote error: %s"), buf + 1);
356
0
    strbuf_addf(scratch, "%s%s", scratch->len ? "\n" : "", prefix);
357
0
    maybe_colorize_sideband(scratch, buf + 1, len);
358
359
0
    *sideband_type = SIDEBAND_REMOTE_ERROR;
360
0
    break;
361
0
  case 2:
362
0
    b = buf + 1;
363
364
    /*
365
     * Append a suffix to each nonempty line to clear the
366
     * end of the screen line.
367
     *
368
     * The output is accumulated in a buffer and
369
     * each line is printed to stderr using
370
     * write(2) to ensure inter-process atomicity.
371
     */
372
0
    while ((brk = strpbrk(b, "\n\r"))) {
373
0
      int linelen = brk - b;
374
375
      /*
376
       * For message across packet boundary, there would have
377
       * a nonempty "scratch" buffer from last call of this
378
       * function, and there may have a leading CR/LF in "buf".
379
       * For this case we should add a clear-to-eol suffix to
380
       * clean leftover letters we previously have written on
381
       * the same line.
382
       */
383
0
      if (scratch->len && !linelen)
384
0
        strbuf_addstr(scratch, suffix);
385
386
0
      if (!scratch->len)
387
0
        strbuf_addstr(scratch, prefix);
388
389
      /*
390
       * A use case that we should not add clear-to-eol suffix
391
       * to empty lines:
392
       *
393
       * For progress reporting we may receive a bunch of
394
       * percentage updates followed by '\r' to remain on the
395
       * same line, and at the end receive a single '\n' to
396
       * move to the next line. We should preserve the final
397
       * status report line by not appending clear-to-eol
398
       * suffix to this single line break.
399
       */
400
0
      if (linelen > 0) {
401
0
        maybe_colorize_sideband(scratch, b, linelen);
402
0
        strbuf_addstr(scratch, suffix);
403
0
      }
404
405
0
      strbuf_addch(scratch, *brk);
406
0
      write_in_full(2, scratch->buf, scratch->len);
407
0
      strbuf_reset(scratch);
408
409
0
      b = brk + 1;
410
0
    }
411
412
0
    if (*b) {
413
0
      if (!scratch->len)
414
0
        strbuf_addstr(scratch, prefix);
415
0
      maybe_colorize_sideband(scratch, b, strlen(b));
416
0
    }
417
0
    return 0;
418
0
  case 1:
419
0
    *sideband_type = SIDEBAND_PRIMARY;
420
0
    return 1;
421
0
  default:
422
0
    strbuf_addf(scratch, "%s%s: protocol error: bad band #%d",
423
0
          scratch->len ? "\n" : "", me, band);
424
0
    *sideband_type = SIDEBAND_PROTOCOL_ERROR;
425
0
    break;
426
0
  }
427
428
0
cleanup:
429
0
  if (die_on_error && *sideband_type == SIDEBAND_PROTOCOL_ERROR)
430
0
    die("%s", scratch->buf);
431
0
  if (scratch->len) {
432
0
    strbuf_addch(scratch, '\n');
433
0
    write_in_full(2, scratch->buf, scratch->len);
434
0
  }
435
0
  strbuf_release(scratch);
436
0
  return 1;
437
0
}
438
439
/*
440
 * fd is connected to the remote side; send the sideband data
441
 * over multiplexed packet stream.
442
 */
443
void send_sideband(int fd, int band, const char *data, ssize_t sz, int packet_max)
444
0
{
445
0
  const char *p = data;
446
447
0
  while (sz) {
448
0
    struct iovec iov[2];
449
0
    unsigned n;
450
0
    char hdr[5];
451
452
0
    n = sz;
453
0
    if (packet_max - 5 < n)
454
0
      n = packet_max - 5;
455
0
    if (0 <= band) {
456
0
      xsnprintf(hdr, sizeof(hdr), "%04x", n + 5);
457
0
      hdr[4] = band;
458
0
      iov[0].iov_base = hdr;
459
0
      iov[0].iov_len = 5;
460
0
    } else {
461
0
      xsnprintf(hdr, sizeof(hdr), "%04x", n + 4);
462
0
      iov[0].iov_base = hdr;
463
0
      iov[0].iov_len = 4;
464
0
    }
465
466
0
    iov[1].iov_base = (void *) p;
467
0
    iov[1].iov_len = n;
468
469
0
    writev_or_die(fd, iov, ARRAY_SIZE(iov));
470
471
0
    p += n;
472
0
    sz -= n;
473
0
  }
474
0
}