Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,22 @@ git-review log
git-review log -p
git-review log -pw
git-review log --stat
git-review log --difft
git-review log --hunk
```

This command runs `git log @{u}..HEAD --reverse`, so commits appear in oldest-first order.
Use `-p` or `--patch` to show patches.
Use `--stat` to show diff statistics, with or without patches.
Use `-w` or `--ignore-all-space` to ignore whitespace changes in patches.
Use `--difft` to show patches with [Difftastic](https://difftastic.wilfred.me.uk/), which requires `difft` on `PATH`.
This option implies `--patch` and runs Git with `-c diff.external=difft` and `--ext-diff`.
Use `--hunk` to show patches through `hunk pager`, which requires `hunk` on `PATH`.
This option implies `--patch` and runs Git with `-c core.pager='hunk pager'`.
Git starts the pager only when stdout is a terminal, so redirected output remains unpaged.
You can combine `--hunk` and `--difft` to send Difftastic output through `hunk pager`.
Neither option changes your saved Git configuration.
Git's normal precedence rules still apply, including overrides from `GIT_EXTERNAL_DIFF` and `GIT_PAGER`.
Output uses Git's format and pager without reading or changing review state.
The command does not support `--json` or revision arguments.
Unlike review commands, it uses the upstream range directly and permits merge commits.
Expand Down
20 changes: 16 additions & 4 deletions internal/cli/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,28 @@ import (
)

func (a *application) logCommand() *cobra.Command {
var patch, ignoreAllSpace, stat bool
var patch, ignoreAllSpace, stat, difft, hunk bool
cmd := &cobra.Command{
Use: "log",
Short: "Show commits after the upstream in oldest-first order",
Long: "Run git log @{u}..HEAD --reverse with optional patches, diff statistics, and whitespace filtering.\nOutput uses Git's format and pager, not git-review's review state.",
Long: "Run git log @{u}..HEAD --reverse with optional patches, diff statistics, and whitespace filtering.\nUse --difft for Difftastic or --hunk for hunk pager. Both imply --patch.\nOutput uses Git's format and pager, not git-review's review state.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
if a.json {
return fmt.Errorf("log does not support --json")
}
args := []string{"log", "@{u}..HEAD", "--reverse"}
if patch {
var args []string
if hunk {
args = append(args, "-c", "core.pager=hunk pager")
}
if difft {
args = append(args, "-c", "diff.external=difft")
}
args = append(args, "log", "@{u}..HEAD", "--reverse")
if difft {
args = append(args, "--ext-diff")
}
if patch || difft || hunk {
args = append(args, "-p")
}
if ignoreAllSpace {
Expand All @@ -42,5 +52,7 @@ func (a *application) logCommand() *cobra.Command {
cmd.Flags().BoolVarP(&patch, "patch", "p", false, "Show the patch for each commit")
cmd.Flags().BoolVarP(&ignoreAllSpace, "ignore-all-space", "w", false, "Ignore whitespace changes in patches")
cmd.Flags().BoolVar(&stat, "stat", false, "Show diff statistics for each commit")
cmd.Flags().BoolVar(&difft, "difft", false, "Show patches with difft (requires difft on PATH)")
cmd.Flags().BoolVar(&hunk, "hunk", false, "Show patches with hunk pager (requires hunk on PATH)")
return cmd
}
92 changes: 92 additions & 0 deletions internal/cli/log_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package cli

import (
"bytes"
"context"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"

"github.com/creack/pty"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -69,6 +74,93 @@ func TestLogShowsUpstreamCommitsAndOptionalPatches(t *testing.T) {
require.Empty(t, terminal)
}

func TestLogDifft(t *testing.T) {
dir := featureRepo(t)
require.NoError(t, os.WriteFile(filepath.Join(dir, "example.txt"), []byte("example content\n"), 0o600))
git(t, dir, "add", "example.txt")
git(t, dir, "commit", "-m", "Add example")
git(t, dir, "config", "diff.external", "false")
tools := t.TempDir()
difft := filepath.Join(tools, "difft")
require.NoError(t, os.WriteFile(difft, []byte("#!/bin/sh\nprintf 'External diff: %s\\n' \"$1\"\ncat \"$5\"\n"), 0o700))
t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))

out, terminal, err := execute(t, dir, "log", "-p")
require.NoError(t, err)
require.Empty(t, terminal)
require.Contains(t, out, "+example content\n")
require.NotContains(t, out, "External diff:")

out, terminal, err = execute(t, dir, "log", "--difft")
require.NoError(t, err)
require.Empty(t, terminal)
require.Contains(t, out, "External diff: example.txt\nexample content\n")
require.NotContains(t, out, "diff --git")
require.Equal(t, "false", git(t, dir, "config", "diff.external"))

require.NoError(t, os.WriteFile(difft, []byte("#!/bin/sh\necho 'difft failed' >&2\nexit 1\n"), 0o700))
_, terminal, err = execute(t, dir, "log", "--difft")
require.ErrorContains(t, err, "git log:")
require.Contains(t, terminal, "difft failed")
}

func TestLogHunkPager(t *testing.T) {
binary := buildGitReview(t)
dir := featureRepo(t)
require.NoError(t, os.WriteFile(filepath.Join(dir, "example.txt"), []byte("example content\n"), 0o600))
git(t, dir, "add", "example.txt")
git(t, dir, "commit", "-m", "Add example")
git(t, dir, "config", "core.pager", "false")
git(t, dir, "config", "color.ui", "false")
tools := t.TempDir()
capture := filepath.Join(tools, "pager-output")
require.NoError(t, os.WriteFile(filepath.Join(tools, "hunk"), []byte("#!/bin/sh\n[ \"$1\" = pager ] || exit 1\ncat > \"$HUNK_OUTPUT\"\n"), 0o700))
require.NoError(t, os.WriteFile(filepath.Join(tools, "difft"), []byte("#!/bin/sh\nprintf 'External diff: %s\\n' \"$1\"\ncat \"$5\"\n"), 0o700))
t.Setenv("PATH", tools+string(os.PathListSeparator)+os.Getenv("PATH"))
t.Setenv("HUNK_OUTPUT", capture)
t.Setenv("TERM", "xterm")

out, terminal, err := executeBinary(t, binary, dir, "log", "--hunk")
require.NoError(t, err)
require.Empty(t, terminal)
require.Contains(t, out, "+example content\n")
_, err = os.Stat(capture)
require.ErrorIs(t, err, os.ErrNotExist)

for _, tc := range []struct {
name string
flags []string
want string
}{
{name: "hunk", flags: []string{"--hunk"}, want: "+example content\n"},
{name: "hunk and difft", flags: []string{"--hunk", "--difft"}, want: "External diff: example.txt\nexample content\n"},
} {
t.Run(tc.name, func(t *testing.T) {
master, slave, err := pty.Open()
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, slave.Close())
require.NoError(t, master.Close())
})
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, binary, append([]string{"log"}, tc.flags...)...)
cmd.Dir = dir
cmd.Stdout = slave
var stderr bytes.Buffer
cmd.Stderr = &stderr
// The fake pager writes to capture, so the terminal needs no reader.
require.NoError(t, cmd.Run(), "%s", stderr.String())
saved, err := os.ReadFile(capture)
require.NoError(t, err)
require.Contains(t, string(saved), tc.want)
require.Contains(t, string(saved), "Add example")
require.NoError(t, os.Remove(capture))
})
}
require.Equal(t, "false", git(t, dir, "config", "core.pager"))
}

func TestLogReportsGitAndUsageErrors(t *testing.T) {
dir := featureRepo(t)
git(t, dir, "branch", "--unset-upstream")
Expand Down
Loading