---
title: "Run git and show its output"
audience: "anyone whose terminal tool runs another program"
description: "You'll run `git log --oneline` from an effect, collect what it prints and show it in a pane. A key runs it again."
snippet_caption: "app/routes/page.tsx"
snippet: "useEffect(() => {\n  let read = \"\";\n  const git = system.spawn(\"git\", [\"log\", \"--oneline\", \"-5\"]);\n  git.stdout.on(\"data\", (chunk) => {\n    read = read + chunk;\n  });\n  git.on(\"close\", (code) => {\n    if (code === 0) setLines(read.trim().split(\"\\n\"));\n  });\n}, [runs]);"
---

import { Install } from "@src/ui/Install";

You'll run `git log --oneline` from an effect, collect what it prints and show it in a pane. A key runs it again. It starts from the app you built in [650](https://snapfirers.com/fsr/learn/650-move-focus-between-panes).

<Install captured="0.x" />

## Start a process

An effect or a handler starts a process with `system.spawn(command, args, options)` from `@snapfire/fsr/terminal`. It takes the place of `spawn` from Node's `child_process`, which a terminal page cannot import:

```text
`spawn` from `node:child_process`, which is not FSR's
```

`fsr migrate` rewrites Node's `spawn` to `system.spawn` when it brings an Ink app over, which [680](https://snapfirers.com/fsr/learn/680-bring-an-ink-tool-over) shows.

The process runs with its output piped back to the page. `stdout` and `stderr` give `data` with each chunk. The process gives `exit` and `close` with its exit code. It gives `error` when it cannot start. The options it reads are `cwd`, `env`, `shell` and `stdio: "inherit"`.

## Write the git pane

Add `system` to the `std` import:

```tsx title="app/routes/page.tsx"
import { file, path } from "@snapfire/fsr/host";
import { system } from "@snapfire/fsr/terminal";
```

Then add a pane above `Tasks`:

```tsx title="app/routes/page.tsx"
function GitPane({ runs }: { runs: number }) {
  const [lines, setLines] = useState<string[]>([]);
  const [failure, setFailure] = useState("");

  useEffect(() => {
    let read = "";
    const git = system.spawn("git", ["log", "--oneline", "-5"]);
    git.stdout.on("data", (chunk) => {
      read = read + chunk;
    });
    git.on("close", (code) => {
      if (code === 0) {
        setLines(read.trim().split("\n"));
        setFailure("");
      } else {
        setFailure(`git log exited with ${code}`);
      }
    });
  }, [runs]);

  return (
    <Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={1}>
      <Text bold>git log</Text>
      {failure ? <Text color="red">{failure}</Text> : lines.map((line) => <Text key={line} wrap="truncate">{line}</Text>)}
    </Box>
  );
}
```

`read` is a local of the effect that the `data` listener writes. It keeps its value from one chunk to the next, so by `close` it holds everything git printed. Each run of the effect gets a fresh `read`, so a second run does not add to the first.

Each listener runs as a handler of the component, so it may call the setters. Output can arrive in several chunks, which is why the lines are only split once the process has closed.

`wrap="truncate"` cuts a line at the pane's edge instead of wrapping it.

## Run it again on a key

The effect depends on `runs`, so changing `runs` runs git again. In `Tasks`, add the state below `saving`:

```tsx title="app/routes/page.tsx"
const [runs, setRuns] = useState(0);
```

Add a branch to the page's handler, after the `focus` lines:

```tsx title="app/routes/page.tsx"
if (input === "g") setRuns(runs + 1);
```

Place the pane below the two panes and name the key in the footer:

```tsx title="app/routes/page.tsx" {5}
<Box>
  <ListPane tasks={shown} selected={selected} active={!adding} onMove={move} onToggle={toggle} />
  <DetailPane task={shown[selected]} active={!adding} onRemove={remove} />
</Box>
<GitPane runs={runs} />
```

```tsx title="app/routes/page.tsx"
<Text dimColor>{adding ? "Enter adds, Esc cancels" : "Tab switches panes, a adds, g reruns git"}</Text>
```

## Run it

git runs in the directory you start the app from. Make the project a repository with a few commits:

```shell-session
$ git init -q
$ git commit -q --allow-empty -m "start the task list"
$ git commit -q --allow-empty -m "save tasks to a file"
$ git commit -q --allow-empty -m "add a git pane"
$ fsr dev app --terminal
```

```text
 Tasks tasks.json                                        1 open
╭──────────────────────────────╮╭──────────────────────────────╮
│ > Tag a release              ││ Tag a release                │
│   Answer the open issues     ││ open                         │
│                              ││ x removes it                 │
╰──────────────────────────────╯╰──────────────────────────────╯
╭──────────────────────────────────────────────────────────────╮
│ git log                                                      │
│ 80204f3 add a git pane                                       │
│ 12d479d save tasks to a file                                 │
│ 96f7519 start the task list                                  │
╰──────────────────────────────────────────────────────────────╯
 Tab switches panes, a adds, g reruns git                 saved
```

Your hashes differ. Leave it running and commit from a second terminal:

```shell-session
$ git commit -q --allow-empty -m "rerun git on g"
```

Press `g` in the app. git runs again and the new commit is on top:

```text
╭──────────────────────────────────────────────────────────────╮
│ git log                                                      │
│ 467741c rerun git on g                                       │
│ 80204f3 add a git pane                                       │
│ 12d479d save tasks to a file                                 │
│ 96f7519 start the task list                                  │
╰──────────────────────────────────────────────────────────────╯
```

Started outside a repository, git exits with 128 and the pane says so in red:

```text
│ git log                                                      │
│ git log exited with 128                                      │
```

Next up: [670. Keep time](https://snapfirers.com/fsr/learn/670-keep-time).
