Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

Commit 9491958

Browse files
authored
gitserver: Add option to allow setting custom context line count (#63840)
This PR adds support for two new arguments in the gitserver API: InterHunkContext and ContextLines. See the inline docstring for the two, but basically this allows to request diffs with a custom amount of context lines and hunk merging. The defaults should be unchanged at `3` for both, according to https://sourcegraph.com/github.com/git/git/-/blob/diff.c?L60. Closes SRC-467 Test plan: Added unit tests.
1 parent 2451aab commit 9491958

File tree

13 files changed

+1207
-990
lines changed

13 files changed

+1207
-990
lines changed

cmd/gitserver/internal/git/gitcli/diff.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package gitcli
33
import (
44
"bufio"
55
"context"
6+
"fmt"
67
"io"
78
"sync"
89

@@ -13,7 +14,7 @@ import (
1314
"github.com/sourcegraph/sourcegraph/lib/errors"
1415
)
1516

16-
func (g *gitCLIBackend) RawDiff(ctx context.Context, base string, head string, typ git.GitDiffComparisonType, paths ...string) (io.ReadCloser, error) {
17+
func (g *gitCLIBackend) RawDiff(ctx context.Context, base string, head string, typ git.GitDiffComparisonType, opts git.RawDiffOpts, paths ...string) (io.ReadCloser, error) {
1718
baseOID, err := g.revParse(ctx, base)
1819
if err != nil {
1920
return nil, err
@@ -58,12 +59,12 @@ func (g *gitCLIBackend) RawDiff(ctx context.Context, base string, head string, t
5859
// we want.
5960
}
6061

61-
args := buildRawDiffArgs(baseOID, headOID, paths)
62+
args := buildRawDiffArgs(opts, baseOID, headOID, paths)
6263

6364
return g.NewCommand(ctx, WithArguments(args...))
6465
}
6566

66-
func buildRawDiffArgs(base, head api.CommitID, paths []string) []string {
67+
func buildRawDiffArgs(opts git.RawDiffOpts, base, head api.CommitID, paths []string) []string {
6768
return append([]string{
6869
// Note: We use git diff-tree instead of git diff because git diff lets
6970
// you diff any arbitrary files on disk, which is a security risk, diffing
@@ -72,7 +73,8 @@ func buildRawDiffArgs(base, head api.CommitID, paths []string) []string {
7273
"--patch",
7374
"--find-renames",
7475
"--full-index",
75-
"--inter-hunk-context=3",
76+
fmt.Sprintf("--inter-hunk-context=%d", opts.InterHunkContext),
77+
fmt.Sprintf("--unified=%d", opts.ContextLines),
7678
"--no-prefix",
7779
string(base),
7880
string(head),

cmd/gitserver/internal/git/gitcli/diff_test.go

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import (
1818
)
1919

2020
func TestGitCLIBackend_RawDiff(t *testing.T) {
21+
defaultOpts := git.RawDiffOpts{
22+
InterHunkContext: 3,
23+
ContextLines: 3,
24+
}
2125
var f1Diff = []byte(`diff --git f f
2226
index a29bdeb434d874c9b1d8969c40c42161b03fafdc..c0d0fb45c382919737f8d0c20aaf57cf89b74af8 100644
2327
--- f
@@ -48,15 +52,15 @@ index 0000000000000000000000000000000000000000..8a6a2d098ecaf90105f1cf2fa90fc460
4852
)
4953

5054
t.Run("streams diff", func(t *testing.T) {
51-
r, err := backend.RawDiff(ctx, "testbase", "HEAD", git.GitDiffComparisonTypeOnlyInHead)
55+
r, err := backend.RawDiff(ctx, "testbase", "HEAD", git.GitDiffComparisonTypeOnlyInHead, defaultOpts)
5256
require.NoError(t, err)
5357
diff, err := io.ReadAll(r)
5458
require.NoError(t, err)
5559
require.NoError(t, r.Close())
5660
require.Equal(t, string(f1Diff), string(diff))
5761
})
5862
t.Run("streams diff intersection", func(t *testing.T) {
59-
r, err := backend.RawDiff(ctx, "testbase", "HEAD", git.GitDiffComparisonTypeIntersection)
63+
r, err := backend.RawDiff(ctx, "testbase", "HEAD", git.GitDiffComparisonTypeIntersection, defaultOpts)
6064
require.NoError(t, err)
6165
diff, err := io.ReadAll(r)
6266
require.NoError(t, err)
@@ -75,14 +79,49 @@ index 0000000000000000000000000000000000000000..8a6a2d098ecaf90105f1cf2fa90fc460
7579
"git commit -m foo --author='Foo Author <foo@sourcegraph.com>'",
7680
)
7781

78-
r, err := backend.RawDiff(ctx, "testbase", "HEAD", git.GitDiffComparisonTypeOnlyInHead, "f2")
82+
r, err := backend.RawDiff(ctx, "testbase", "HEAD", git.GitDiffComparisonTypeOnlyInHead, defaultOpts, "f2")
7983
require.NoError(t, err)
8084
diff, err := io.ReadAll(r)
8185
require.NoError(t, err)
8286
require.NoError(t, r.Close())
8387
// We expect only a diff for f2, not for f.
8488
require.Equal(t, string(f2Diff), string(diff))
8589
})
90+
t.Run("custom context", func(t *testing.T) {
91+
// Prepare repo state:
92+
backend := BackendWithRepoCommands(t,
93+
"echo 'line1\nline2\nline3\nlin4\nline5\nline6\nline7\nline8\n' > f",
94+
"git add f",
95+
"git commit -m foo --author='Foo Author <foo@sourcegraph.com>'",
96+
"git tag testbase",
97+
"echo 'line1.1\nline2\nline3\nlin4\nline5.5\nline6\nline7\nline8\n' > f",
98+
"git add f",
99+
"git commit -m foo --author='Foo Author <foo@sourcegraph.com>'",
100+
)
101+
102+
var expectedDiff = []byte(`diff --git f f
103+
index 0ef51c52043997fdd257a0b77d761e9ca58bcc1f..58692a00a73d1f78df00014edf4ef39ef4ba0019 100644
104+
--- f
105+
+++ f
106+
@@ -1 +1 @@
107+
-line1
108+
+line1.1
109+
@@ -5 +5 @@ lin4
110+
-line5
111+
+line5.5
112+
`)
113+
114+
r, err := backend.RawDiff(ctx, "testbase", "HEAD", git.GitDiffComparisonTypeOnlyInHead, git.RawDiffOpts{
115+
InterHunkContext: 0,
116+
ContextLines: 0,
117+
})
118+
require.NoError(t, err)
119+
diff, err := io.ReadAll(r)
120+
require.NoError(t, err)
121+
require.NoError(t, r.Close())
122+
t.Log(string(diff))
123+
require.Equal(t, string(expectedDiff), string(diff))
124+
})
86125
t.Run("not found revspec", func(t *testing.T) {
87126
// Prepare repo state:
88127
backend := BackendWithRepoCommands(t,
@@ -92,19 +131,19 @@ index 0000000000000000000000000000000000000000..8a6a2d098ecaf90105f1cf2fa90fc460
92131
"git tag test",
93132
)
94133

95-
_, err := backend.RawDiff(ctx, "unknown", "test", git.GitDiffComparisonTypeOnlyInHead)
134+
_, err := backend.RawDiff(ctx, "unknown", "test", git.GitDiffComparisonTypeOnlyInHead, defaultOpts)
96135
require.Error(t, err)
97136
require.True(t, errors.HasType[*gitdomain.RevisionNotFoundError](err))
98137

99-
_, err = backend.RawDiff(ctx, "test", "unknown", git.GitDiffComparisonTypeOnlyInHead)
138+
_, err = backend.RawDiff(ctx, "test", "unknown", git.GitDiffComparisonTypeOnlyInHead, defaultOpts)
100139
require.Error(t, err)
101140
require.True(t, errors.HasType[*gitdomain.RevisionNotFoundError](err))
102141
})
103142
t.Run("files outside repository", func(t *testing.T) {
104143
// We use git-diff-tree, but with git-diff you can diff any files on disk
105144
// which is dangerous. So we have this safeguard test here in place to
106145
// make sure we don't regress on that.
107-
r, err := backend.RawDiff(ctx, "testbase", "HEAD", git.GitDiffComparisonTypeOnlyInHead, "/dev/null", "/etc/hosts")
146+
r, err := backend.RawDiff(ctx, "testbase", "HEAD", git.GitDiffComparisonTypeOnlyInHead, defaultOpts, "/dev/null", "/etc/hosts")
108147
require.NoError(t, err)
109148
_, err = io.ReadAll(r)
110149
require.Error(t, err)
@@ -115,7 +154,7 @@ index 0000000000000000000000000000000000000000..8a6a2d098ecaf90105f1cf2fa90fc460
115154
ctx, cancel := context.WithCancel(ctx)
116155
t.Cleanup(cancel)
117156

118-
r, err := backend.RawDiff(ctx, "testbase", "HEAD", git.GitDiffComparisonTypeOnlyInHead)
157+
r, err := backend.RawDiff(ctx, "testbase", "HEAD", git.GitDiffComparisonTypeOnlyInHead, defaultOpts)
119158
require.NoError(t, err)
120159

121160
cancel()

cmd/gitserver/internal/git/iface.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,11 @@ type GitBackend interface {
8787
RevAtTime(ctx context.Context, revspec string, time time.Time) (api.CommitID, error)
8888
// RawDiff returns the raw git diff for the given range.
8989
// Diffs returned from this function will have the following settings applied:
90-
// - 3 lines of context
90+
// - N lines of context according to opts
9191
// - No a/ b/ prefixes
9292
// - Rename detection
9393
// If either base or head don't exist, a RevisionNotFoundError is returned.
94-
RawDiff(ctx context.Context, base string, head string, typ GitDiffComparisonType, paths ...string) (io.ReadCloser, error)
94+
RawDiff(ctx context.Context, base string, head string, typ GitDiffComparisonType, opts RawDiffOpts, paths ...string) (io.ReadCloser, error)
9595
// ContributorCounts returns the number of commits per contributor in the
9696
// set of commits specified by the options.
9797
// Aggregations are done by email address.
@@ -355,3 +355,17 @@ type ReadDirIterator interface {
355355
// Close closes the iterator.
356356
Close() error
357357
}
358+
359+
// RawDiffOpts contaions extra options for the RawDiff method.
360+
type RawDiffOpts struct {
361+
// InterHunkContext specifies the number of lines to consider for fusing hunks
362+
// together. I.e., when set to 5 and between 2 hunks there are at most 5 lines,
363+
// the 2 hunks will be fused together into a single chunk.
364+
InterHunkContext int
365+
// ContextLines specifies the number of lines of context to show around added/removed
366+
// lines.
367+
// This is the number of lines that will be shown before and after each line that
368+
// has been added/removed. If InterHunkContext is not zero, the context will still
369+
// be fused together with other hunks if they meet the threshold.
370+
ContextLines int
371+
}

cmd/gitserver/internal/git/mock.go

Lines changed: 19 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/gitserver/internal/git/observability.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,14 +316,14 @@ func (hr *observableRefIterator) Close() error {
316316
return err
317317
}
318318

319-
func (b *observableBackend) RawDiff(ctx context.Context, base string, head string, typ GitDiffComparisonType, paths ...string) (_ io.ReadCloser, err error) {
319+
func (b *observableBackend) RawDiff(ctx context.Context, base string, head string, typ GitDiffComparisonType, opts RawDiffOpts, paths ...string) (_ io.ReadCloser, err error) {
320320
ctx, errCollector, endObservation := b.operations.rawDiff.WithErrors(ctx, &err, observation.Args{})
321321
ctx, cancel := context.WithCancel(ctx)
322322
endObservation.OnCancel(ctx, 1, observation.Args{})
323323

324324
concurrentOps.WithLabelValues("RawDiff").Inc()
325325

326-
r, err := b.backend.RawDiff(ctx, base, head, typ, paths...)
326+
r, err := b.backend.RawDiff(ctx, base, head, typ, opts, paths...)
327327
if err != nil {
328328
concurrentOps.WithLabelValues("RawDiff").Dec()
329329
cancel()

cmd/gitserver/internal/server_grpc.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1175,7 +1175,20 @@ func (gs *grpcServer) RawDiff(req *proto.RawDiffRequest, ss proto.GitserverServi
11751175
typ = git.GitDiffComparisonTypeOnlyInHead
11761176
}
11771177

1178-
r, err := backend.RawDiff(ctx, string(req.GetBaseRevSpec()), string(req.GetHeadRevSpec()), typ, paths...)
1178+
opts := git.RawDiffOpts{
1179+
InterHunkContext: 3,
1180+
ContextLines: 3,
1181+
}
1182+
1183+
if req.InterHunkContext != nil {
1184+
opts.InterHunkContext = int(*req.InterHunkContext)
1185+
}
1186+
1187+
if req.ContextLines != nil {
1188+
opts.ContextLines = int(*req.ContextLines)
1189+
}
1190+
1191+
r, err := backend.RawDiff(ctx, string(req.GetBaseRevSpec()), string(req.GetHeadRevSpec()), typ, opts, paths...)
11791192
if err != nil {
11801193
var e *gitdomain.RevisionNotFoundError
11811194
if errors.As(err, &e) {

cmd/gitserver/internal/server_grpc_logger.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,8 @@ func rawDiffRequestToLogFields(req *proto.RawDiffRequest) []log.Field {
887887
log.String("headRevSpec", string(req.GetHeadRevSpec())),
888888
log.String("comparisonType", req.GetComparisonType().String()),
889889
log.Strings("paths", byteSlicesToStrings(req.GetPaths())),
890+
log.Int("interHunkContext", int(req.GetInterHunkContext())),
891+
log.Int("contextLines", int(req.GetContextLines())),
890892
}
891893
}
892894

internal/gitserver/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ go_test(
7777
"//internal/gitserver/v1:gitserver",
7878
"//internal/grpc",
7979
"//lib/errors",
80+
"//lib/pointers",
8081
"//schema",
8182
"@com_github_derision_test_go_mockgen_v2//testutil/require",
8283
"@com_github_google_go_cmp//cmp",

internal/gitserver/client.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,22 @@ type DiffOptions struct {
323323
// NOTE: Rename detection does not work if only the old path or the new path
324324
// is specified in this slice.
325325
Paths []string
326+
327+
// InterHunkContext specifies the number of lines to consider for fusing hunks
328+
// together. I.e., when set to 5 and between 2 hunks there are at most 5 lines,
329+
// the 2 hunks will be fused together into a single chunk.
330+
//
331+
// The default for this is 3.
332+
InterHunkContext *int
333+
334+
// ContextLines specifies the number of lines of context to show around added/removed
335+
// lines.
336+
// This is the number of lines that will be shown before and after each line that
337+
// has been added/removed. If InterHunkContext is not zero, the context will still
338+
// be fused together with other hunks if they meet the threshold.
339+
//
340+
// The default for this is 3.
341+
ContextLines *int
326342
}
327343

328344
type Client interface {

internal/gitserver/commands.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ func (c *clientImplementor) Diff(ctx context.Context, repo api.RepoName, opts Di
4545
Paths: stringsToByteSlices(opts.Paths),
4646
}
4747

48+
if opts.InterHunkContext != nil {
49+
req.InterHunkContext = pointers.Ptr(uint32(*opts.InterHunkContext))
50+
}
51+
52+
if opts.ContextLines != nil {
53+
req.ContextLines = pointers.Ptr(uint32(*opts.ContextLines))
54+
}
55+
4856
// Rare case: the base is the empty tree, in which case we must use ..
4957
// instead of ... as the latter only works for commits.
5058
if opts.Base == DevNullSHA {

0 commit comments

Comments
 (0)