Skip to content

Git exec(): stdout and stderr are concatenated into a single buffer, so callers cannot tell them apart #85

@joewalker

Description

@joewalker

Observed behavior

exec() in src/util/git.ts pushes both stdout and stderr chunks into the same outputs array, then resolves with the joined string on success (or rejects with it on failure):

const outputs = [] as Array<string>;
cmdProcess.stdout?.on('data', (data: Buffer): void => {
  outputs.push(String(data));
});
cmdProcess.stderr?.on('data', /* ... */ (data: Buffer): void => {
  outputs.push(String(data));
});
cmdProcess.on('close', (code: number): void => {
  if (code === 0) {
    resolve(outputs.join(''));
  } else {
    reject(new Error(outputs.join('')));
  }
});

Consequences:

  1. On success, callers receive a string that may contain stderr noise (progress, warnings) interleaved with stdout. Tests like expect(result).toContain('Test commit') happen to work, but anything that parses the resolved string can be fooled by stderr text.
  2. The interleaving order is non-deterministic and can corrupt structured output (e.g. --porcelain lines split by a stderr message).

Expected behavior

stdout and stderr should be captured separately. Resolve with stdout only; use stderr (with a stdout fallback) in the error message.

Suggested fix

const stdout = [] as Array<string>;
const stderr = [] as Array<string>;
cmdProcess.stdout?.on('data', (data: Buffer) => { stdout.push(String(data)); });
cmdProcess.stderr?.on('data', (data: Buffer) => { stderr.push(String(data)); });
cmdProcess.on('close', (code, signal) => {
  if (code === 0) {
    resolve(stdout.join(''));
  } else {
    const detail = (stderr.join('') || stdout.join('')).trim();
    reject(new Error(detail || `git exited with code ${code}, signal ${signal}`));
  }
});

Also note that String(Buffer) (used in both branches) is unsafe for streams that may split a multi-byte UTF-8 character across two data events. Switching to stdout.setEncoding('utf8') and concatenating strings, or buffering and decoding once at the end, avoids that.

Metadata

Metadata

Assignees

No one assigned

    Labels

    S4Clean-ups or nits with low behavioral riskbugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions