Skip to content

Commit 365eef2

Browse files
authored
Filter out repositories with .batchignore at root (#509)
This implements https://github.com/sourcegraph/sourcegraph/issues/18330 by querying the locations of `.batchignore` files in each repository yielded by the `on` attribute in a batch spec. If locations were found, the repository is ignored. This can be overwritten by using the `-force-override-ignore` flag. Example: given my instance has the following repositories: - `github.com/sourcegraph-testing/zap` - `github.com/sourcegraph-testing/titan` - `github.com/sourcegraph-testing/tidb` - `github.com/sourcegraph-testing/etcd` - `github.com/sourcegraph-testing/batch-changes-testing-ignore` And I use the following batch spec: ```yaml on: - repositoriesMatchingQuery: repohasfile:README.md repo:sourcegraph-testing steps: - run: echo "a horse says 'hello'" >> README.md container: alpine:3 ``` with this change the `batch-changes-testing-ignore` repository will be ignored: - no archive will be downloaded - no steps executed A message is printed that says it's ignored.
1 parent ce6f8a5 commit 365eef2

File tree

7 files changed

+181
-7
lines changed

7 files changed

+181
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ All notable changes to `src-cli` are documented in this file.
1414
### Added
1515

1616
- Extension publishing will now add a `gitHead` property to the extension's manifest. [#500](https://github.com/sourcegraph/src-cli/pull/500)
17+
- `src batch [apply|preview]` now ignore repositories in which a `.batchignore` file exists. The `-force-override-ignore` flag can be used to turn that behaviour off. [#509](https://github.com/sourcegraph/src-cli/pull/509)
1718

1819
### Changed
1920

cmd/src/batch_apply.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Examples:
6969

7070
svc := batches.NewService(&batches.ServiceOpts{
7171
AllowUnsupported: flags.allowUnsupported,
72+
AllowIgnored: flags.allowIgnored,
7273
Client: cfg.apiClient(flags.api, flagSet.Output()),
7374
Workspace: flags.workspace,
7475
})

cmd/src/batch_common.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ var (
3131

3232
type batchApplyFlags struct {
3333
allowUnsupported bool
34+
allowIgnored bool
3435
api *api.Flags
3536
apply bool
3637
cacheDir string
@@ -55,6 +56,10 @@ func newBatchApplyFlags(flagSet *flag.FlagSet, cacheDir, tempDir string) *batchA
5556
&caf.allowUnsupported, "allow-unsupported", false,
5657
"Allow unsupported code hosts.",
5758
)
59+
flagSet.BoolVar(
60+
&caf.allowIgnored, "force-override-ignore", false,
61+
"Do not ignore repositories that have a .batchignore file.",
62+
)
5863
flagSet.BoolVar(
5964
&caf.apply, "apply", false,
6065
"Ignored.",
@@ -238,6 +243,14 @@ func batchExecute(ctx context.Context, out *output.Output, svc *batches.Service,
238243
block.Write(repo.Name)
239244
}
240245
block.Close()
246+
} else if repoSet, ok := err.(batches.IgnoredRepoSet); ok {
247+
batchCompletePending(pending, "Resolved repositories")
248+
249+
block := out.Block(output.Line(" ", output.StyleWarning, "The repositories listed below contain .batchignore files and will be skipped. Use the -force-override-ignore flag to avoid skipping them."))
250+
for repo := range repoSet {
251+
block.Write(repo.Name)
252+
}
253+
block.Close()
241254
} else {
242255
return "", "", errors.Wrap(err, "resolving repositories")
243256
}

cmd/src/batch_preview.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Examples:
4444

4545
svc := batches.NewService(&batches.ServiceOpts{
4646
AllowUnsupported: flags.allowUnsupported,
47+
AllowIgnored: flags.allowIgnored,
4748
Client: cfg.apiClient(flags.api, flagSet.Output()),
4849
Workspace: flags.workspace,
4950
})

internal/batches/errors.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,33 @@ func (e UnsupportedRepoSet) appendRepo(repo *graphql.Repository) {
4444
func (e UnsupportedRepoSet) hasUnsupported() bool {
4545
return len(e) > 0
4646
}
47+
48+
// IgnoredRepoSet provides a set to manage repositories that are on
49+
// unsupported code hosts. This type implements error to allow it to be
50+
// returned directly as an error value if needed.
51+
type IgnoredRepoSet map[*graphql.Repository]struct{}
52+
53+
func (e IgnoredRepoSet) includes(r *graphql.Repository) bool {
54+
_, ok := e[r]
55+
return ok
56+
}
57+
58+
func (e IgnoredRepoSet) Error() string {
59+
repos := []string{}
60+
for repo := range e {
61+
repos = append(repos, repo.Name)
62+
}
63+
64+
return fmt.Sprintf(
65+
"found repositories containing .batchignore files:\n\t%s",
66+
strings.Join(repos, "\n\t"),
67+
)
68+
}
69+
70+
func (e IgnoredRepoSet) appendRepo(repo *graphql.Repository) {
71+
e[repo] = struct{}{}
72+
}
73+
74+
func (e IgnoredRepoSet) hasIgnored() bool {
75+
return len(e) > 0
76+
}

internal/batches/service.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323

2424
type Service struct {
2525
allowUnsupported bool
26+
allowIgnored bool
2627
client api.Client
2728
features featureFlags
2829
imageCache *docker.ImageCache
@@ -31,6 +32,7 @@ type Service struct {
3132

3233
type ServiceOpts struct {
3334
AllowUnsupported bool
35+
AllowIgnored bool
3436
Client api.Client
3537
Workspace string
3638
}
@@ -42,6 +44,7 @@ var (
4244
func NewService(opts *ServiceOpts) *Service {
4345
return &Service{
4446
allowUnsupported: opts.AllowUnsupported,
47+
allowIgnored: opts.AllowIgnored,
4548
client: opts.Client,
4649
imageCache: docker.NewImageCache(),
4750
workspace: opts.Workspace,
@@ -529,6 +532,7 @@ func (svc *Service) ResolveNamespace(ctx context.Context, namespace string) (str
529532
func (svc *Service) ResolveRepositories(ctx context.Context, spec *BatchSpec) ([]*graphql.Repository, error) {
530533
seen := map[string]*graphql.Repository{}
531534
unsupported := UnsupportedRepoSet{}
535+
ignored := IgnoredRepoSet{}
532536

533537
// TODO: this could be trivially parallelised in the future.
534538
for _, on := range spec.On {
@@ -537,6 +541,14 @@ func (svc *Service) ResolveRepositories(ctx context.Context, spec *BatchSpec) ([
537541
return nil, errors.Wrapf(err, "resolving %q", on.String())
538542
}
539543

544+
var repoBatchIgnores map[*graphql.Repository][]string
545+
if !svc.allowIgnored {
546+
repoBatchIgnores, err = svc.FindDirectoriesInRepos(ctx, ".batchignore", repos...)
547+
if err != nil {
548+
return nil, err
549+
}
550+
}
551+
540552
for _, repo := range repos {
541553
if !repo.HasBranch() {
542554
continue
@@ -552,6 +564,12 @@ func (svc *Service) ResolveRepositories(ctx context.Context, spec *BatchSpec) ([
552564
unsupported.appendRepo(repo)
553565
}
554566
}
567+
568+
if !svc.allowIgnored {
569+
if locations, ok := repoBatchIgnores[repo]; ok && len(locations) > 0 {
570+
ignored.appendRepo(repo)
571+
}
572+
}
555573
} else {
556574
// If we've already seen this repository, we overwrite the
557575
// Commit/Branch fields with the latest value we have
@@ -563,7 +581,7 @@ func (svc *Service) ResolveRepositories(ctx context.Context, spec *BatchSpec) ([
563581

564582
final := make([]*graphql.Repository, 0, len(seen))
565583
for _, repo := range seen {
566-
if !unsupported.includes(repo) {
584+
if !unsupported.includes(repo) && !ignored.includes(repo) {
567585
final = append(final, repo)
568586
}
569587
}
@@ -572,6 +590,10 @@ func (svc *Service) ResolveRepositories(ctx context.Context, spec *BatchSpec) ([
572590
return final, unsupported
573591
}
574592

593+
if ignored.hasIgnored() {
594+
return final, ignored
595+
}
596+
575597
return final, nil
576598
}
577599

@@ -678,6 +700,7 @@ func (svc *Service) resolveRepositorySearch(ctx context.Context, query string) (
678700
}
679701
}
680702
}
703+
681704
if ok, err := svc.client.NewRequest(repositorySearchQuery, map[string]interface{}{
682705
"query": setDefaultQueryCount(query),
683706
"queryCommit": false,
@@ -728,7 +751,7 @@ type findDirectoriesResult map[string]struct {
728751
// files matching the given file name in the repository.
729752
// The locations are paths relative to the root of the directory.
730753
// No "/" at the beginning.
731-
// An empty path ("") represents the root directory.
754+
// A dot (".") represents the root directory.
732755
func (svc *Service) FindDirectoriesInRepos(ctx context.Context, fileName string, repos ...*graphql.Repository) (map[*graphql.Repository][]string, error) {
733756
const batchSize = 50
734757

internal/batches/service_test.go

Lines changed: 110 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,18 @@ const testResolveRepositorySearchResult = `{
118118
}
119119
`
120120

121-
func mockGraphQLClient(response string) (client api.Client, done func()) {
121+
func mockGraphQLClient(responses ...string) (client api.Client, done func()) {
122122
mux := http.NewServeMux()
123+
124+
var count int
123125
mux.HandleFunc("/.api/graphql", func(w http.ResponseWriter, r *http.Request) {
124126
w.Header().Set("Content-Type", "application/json")
125-
_, _ = w.Write([]byte(response))
127+
128+
_, _ = w.Write([]byte(responses[count]))
129+
130+
if count < len(responses)-1 {
131+
count += 1
132+
}
126133
})
127134

128135
ts := httptest.NewServer(mux)
@@ -144,7 +151,7 @@ func TestResolveRepositories_Unsupported(t *testing.T) {
144151
}
145152

146153
t.Run("allowUnsupported:true", func(t *testing.T) {
147-
svc := &Service{client: client, allowUnsupported: true}
154+
svc := &Service{client: client, allowUnsupported: true, allowIgnored: true}
148155

149156
repos, err := svc.ResolveRepositories(context.Background(), spec)
150157
if err != nil {
@@ -156,7 +163,7 @@ func TestResolveRepositories_Unsupported(t *testing.T) {
156163
})
157164

158165
t.Run("allowUnsupported:false", func(t *testing.T) {
159-
svc := &Service{client: client, allowUnsupported: false}
166+
svc := &Service{client: client, allowUnsupported: false, allowIgnored: true}
160167

161168
repos, err := svc.ResolveRepositories(context.Background(), spec)
162169
repoSet, ok := err.(UnsupportedRepoSet)
@@ -167,7 +174,7 @@ func TestResolveRepositories_Unsupported(t *testing.T) {
167174
t.Fatalf("wrong number of repos. want=%d, have=%d", 1, len(repoSet))
168175
}
169176
if len(repos) != 3 {
170-
t.Fatalf("wrong number of repos. want=%d, have=%d", 4, len(repos))
177+
t.Fatalf("wrong number of repos. want=%d, have=%d", 3, len(repos))
171178
}
172179
})
173180
}
@@ -216,6 +223,104 @@ const testResolveRepositoriesUnsupported = `{
216223
}
217224
`
218225

226+
func TestResolveRepositories_Ignored(t *testing.T) {
227+
spec := &BatchSpec{
228+
On: []OnQueryOrRepository{
229+
{RepositoriesMatchingQuery: "testquery"},
230+
},
231+
}
232+
233+
t.Run("allowIgnored:true", func(t *testing.T) {
234+
client, done := mockGraphQLClient(testResolveRepositories, testBatchIgnoreInRepos)
235+
defer done()
236+
237+
svc := &Service{client: client, allowIgnored: true}
238+
239+
repos, err := svc.ResolveRepositories(context.Background(), spec)
240+
if err != nil {
241+
t.Fatalf("unexpected error: %s", err)
242+
}
243+
if len(repos) != 3 {
244+
t.Fatalf("wrong number of repos. want=%d, have=%d", 3, len(repos))
245+
}
246+
})
247+
248+
t.Run("allowIgnored:false", func(t *testing.T) {
249+
client, done := mockGraphQLClient(testResolveRepositories, testBatchIgnoreInRepos)
250+
defer done()
251+
252+
svc := &Service{client: client, allowIgnored: false}
253+
254+
repos, err := svc.ResolveRepositories(context.Background(), spec)
255+
ignored, ok := err.(IgnoredRepoSet)
256+
if !ok {
257+
t.Fatalf("err is not IgnoredRepoSet: %s", err)
258+
}
259+
if len(ignored) != 1 {
260+
t.Fatalf("wrong number of ignored repos. want=%d, have=%d", 1, len(ignored))
261+
}
262+
if len(repos) != 2 {
263+
t.Fatalf("wrong number of repos. want=%d, have=%d", 2, len(repos))
264+
}
265+
})
266+
}
267+
268+
const testResolveRepositories = `{
269+
"data": {
270+
"search": {
271+
"results": {
272+
"results": [
273+
{
274+
"__typename": "Repository",
275+
"id": "UmVwb3NpdG9yeToxMw==",
276+
"name": "bitbucket.sgdev.org/SOUR/automation-testing",
277+
"url": "/bitbucket.sgdev.org/SOUR/automation-testing",
278+
"externalRepository": { "serviceType": "bitbucketserver" },
279+
"defaultBranch": { "name": "refs/heads/master", "target": { "oid": "b978d56de5578a935ca0bf07b56528acc99d5a61" } }
280+
},
281+
{
282+
"__typename": "Repository",
283+
"id": "UmVwb3NpdG9yeTo0",
284+
"name": "github.com/sourcegraph/automation-testing",
285+
"url": "/github.com/sourcegraph/automation-testing",
286+
"externalRepository": { "serviceType": "github" },
287+
"defaultBranch": { "name": "refs/heads/master", "target": { "oid": "6ac8a32ecaf6c4dc5ce050b9af51bce3db8efd63" } }
288+
},
289+
{
290+
"__typename": "Repository",
291+
"id": "UmVwb3NpdG9yeTo2MQ==",
292+
"name": "gitlab.sgdev.org/sourcegraph/automation-testing",
293+
"url": "/gitlab.sgdev.org/sourcegraph/automation-testing",
294+
"externalRepository": { "serviceType": "gitlab" },
295+
"defaultBranch": { "name": "refs/heads/master", "target": { "oid": "3b79a5d479d2af9cfe91e0aad4e9dddca7278150" } }
296+
}
297+
]
298+
}
299+
}
300+
}
301+
}
302+
`
303+
304+
const testBatchIgnoreInRepos = `{
305+
"data": {
306+
"repo_0": {
307+
"results": {
308+
"results": [
309+
{
310+
"__typename": "FileMatch",
311+
"file": {
312+
"path": ".batchignore"
313+
}
314+
}
315+
]
316+
}
317+
},
318+
"repo_1": { "results": { "results": [] } },
319+
"repo_2": { "results": { "results": [] } }
320+
}
321+
}
322+
`
323+
219324
func TestService_FindDirectoriesInRepos(t *testing.T) {
220325
client, done := mockGraphQLClient(testFindDirectoriesInRepos)
221326
defer done()

0 commit comments

Comments
 (0)