Skip to content

Commit cbab0cf

Browse files
committed
Create [TODO] XXX issues upon every commit
1 parent cac167b commit cbab0cf

File tree

6 files changed

+258
-0
lines changed

6 files changed

+258
-0
lines changed

acceptance/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ require (
3737
github.com/kylelemons/godebug v1.1.0 // indirect
3838
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
3939
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
40+
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
4041
go.opencensus.io v0.24.0 // indirect
4142
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
4243
go.opentelemetry.io/otel v1.24.0 // indirect

acceptance/go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,14 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
105105
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
106106
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
107107
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
108+
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI=
109+
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs=
108110
github.com/sethvargo/go-githubactions v1.2.0 h1:Gbr36trCAj6uq7Rx1DolY1NTIg0wnzw3/N5WHdKIjME=
109111
github.com/sethvargo/go-githubactions v1.2.0/go.mod h1:7/4WeHgYfSz9U5vwuToCK9KPnELVHAhGtRwLREOQV80=
110112
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
111113
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
112114
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
115+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
113116
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
114117
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
115118
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=

acceptance/main.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/databrickslabs/sandbox/acceptance/notify"
1616
"github.com/databrickslabs/sandbox/acceptance/redaction"
1717
"github.com/databrickslabs/sandbox/acceptance/testenv"
18+
"github.com/databrickslabs/sandbox/acceptance/todos"
1819
"github.com/databrickslabs/sandbox/go-libs/env"
1920
"github.com/databrickslabs/sandbox/go-libs/github"
2021
"github.com/databrickslabs/sandbox/go-libs/slack"
@@ -34,6 +35,10 @@ func run(ctx context.Context, opts ...githubactions.Option) error {
3435
return fmt.Errorf("boilerplate: %w", err)
3536
}
3637
a := &acceptance{Boilerplate: b}
38+
err = a.syncTodos(ctx)
39+
if err != nil {
40+
return fmt.Errorf("sync todos: %w", err)
41+
}
3742
alert, err := a.trigger(ctx)
3843
if err != nil {
3944
return fmt.Errorf("trigger: %w", err)
@@ -45,6 +50,22 @@ type acceptance struct {
4550
*boilerplate.Boilerplate
4651
}
4752

53+
func (a *acceptance) syncTodos(ctx context.Context) error {
54+
directory, _, err := a.getProject()
55+
if err != nil {
56+
return fmt.Errorf("project: %w", err)
57+
}
58+
techDebt, err := todos.New(ctx, a.GitHub, directory)
59+
if err != nil {
60+
return fmt.Errorf("tech debt: %w", err)
61+
}
62+
err = techDebt.Create(ctx)
63+
if err != nil {
64+
return fmt.Errorf("create: %w", err)
65+
}
66+
return nil
67+
}
68+
4869
func (a *acceptance) trigger(ctx context.Context) (*notify.Notification, error) {
4970
vaultURI := a.Action.GetInput("vault_uri")
5071
directory, project, err := a.getProject()

acceptance/todos/finder.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package todos
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"regexp"
9+
"strings"
10+
11+
"github.com/databrickslabs/sandbox/go-libs/fileset"
12+
"github.com/databrickslabs/sandbox/go-libs/git"
13+
"github.com/databrickslabs/sandbox/go-libs/github"
14+
)
15+
16+
func New(ctx context.Context, gh *github.GitHubClient, root string) (*Finder, error) {
17+
fs, err := fileset.RecursiveChildren(root)
18+
if err != nil {
19+
return nil, fmt.Errorf("fileset: %w", err)
20+
}
21+
raw, err := os.ReadFile(filepath.Join(root, ".gitignore"))
22+
if err != nil {
23+
return nil, fmt.Errorf("read .gitignore: %w", err)
24+
}
25+
ignorer := newIncluder(append(strings.Split(string(raw), "\n"), ".git/", "*.gif", "*.png"))
26+
var scope fileset.FileSet
27+
for _, file := range fs {
28+
include, _ := ignorer.IgnoreFile(file.Relative)
29+
if !include {
30+
continue
31+
}
32+
scope = append(scope, file)
33+
}
34+
checkout, err := git.NewCheckout(ctx, root)
35+
if err != nil {
36+
return nil, fmt.Errorf("git: %w", err)
37+
}
38+
return &Finder{
39+
fs: scope,
40+
git: checkout,
41+
gh: gh,
42+
}, nil
43+
}
44+
45+
type Finder struct {
46+
fs fileset.FileSet
47+
git *git.Checkout
48+
gh *github.GitHubClient
49+
}
50+
51+
type Todo struct {
52+
message string
53+
link string
54+
}
55+
56+
func (f *Finder) Create(ctx context.Context) error {
57+
todos, err := f.allTodos(ctx)
58+
if err != nil {
59+
return fmt.Errorf("all todos: %w", err)
60+
}
61+
seen, err := f.seenIssues(ctx)
62+
if err != nil {
63+
return fmt.Errorf("seen issues: %w", err)
64+
}
65+
for _, todo := range todos {
66+
if seen[todo.message] {
67+
continue
68+
}
69+
if err := f.createIssue(ctx, todo); err != nil {
70+
return fmt.Errorf("create issue: %w", err)
71+
}
72+
}
73+
return nil
74+
}
75+
76+
func (f *Finder) createIssue(ctx context.Context, todo Todo) error {
77+
org, repo, ok := f.git.OrgAndRepo()
78+
if !ok {
79+
return fmt.Errorf("git org and repo")
80+
}
81+
_, err := f.gh.CreateIssue(ctx, org, repo, github.NewIssue{
82+
Title: fmt.Sprintf("[TODO] %s", todo.message),
83+
Body: fmt.Sprintf("See: %s", todo.link),
84+
Labels: []string{"tech debt"},
85+
})
86+
if err != nil {
87+
return fmt.Errorf("create issue: %w", err)
88+
}
89+
return nil
90+
}
91+
92+
func (f *Finder) seenIssues(ctx context.Context) (map[string]bool, error) {
93+
org, repo, ok := f.git.OrgAndRepo()
94+
if !ok {
95+
return nil, fmt.Errorf("git org and repo")
96+
}
97+
seen := map[string]bool{}
98+
it := f.gh.ListRepositoryIssues(ctx, org, repo, &github.ListIssues{
99+
State: "all",
100+
Labels: []string{"tech debt"},
101+
})
102+
for it.HasNext(ctx) {
103+
issue, err := it.Next(ctx)
104+
if err != nil {
105+
return nil, fmt.Errorf("next: %w", err)
106+
}
107+
if !strings.HasPrefix(issue.Title, "[TODO] ") {
108+
continue
109+
}
110+
norm := strings.TrimPrefix(issue.Title, "[TODO] ")
111+
seen[norm] = true
112+
}
113+
return seen, nil
114+
}
115+
116+
func (f *Finder) allTodos(ctx context.Context) ([]Todo, error) {
117+
prefix, err := f.prefix(ctx)
118+
if err != nil {
119+
return nil, fmt.Errorf("prefix: %w", err)
120+
}
121+
var todos []Todo
122+
needle := regexp.MustCompile(`TODO:(.*)`)
123+
for _, v := range f.fs {
124+
raw, err := v.Raw()
125+
if err != nil {
126+
return nil, fmt.Errorf("%s: %w", v.Relative, err)
127+
}
128+
lines := strings.Split(string(raw), "\n")
129+
for i, line := range lines {
130+
if !needle.MatchString(line) {
131+
continue
132+
}
133+
todos = append(todos, Todo{
134+
message: strings.TrimSpace(needle.FindStringSubmatch(line)[1]),
135+
link: fmt.Sprintf("%s/%s#L%d-L%d", prefix, v.Relative, i, i+5),
136+
})
137+
}
138+
}
139+
return todos, nil
140+
}
141+
142+
func (f *Finder) prefix(ctx context.Context) (string, error) {
143+
org, repo, ok := f.git.OrgAndRepo()
144+
if !ok {
145+
return "", fmt.Errorf("git org and repo")
146+
}
147+
commits, err := f.git.History(ctx)
148+
if err != nil {
149+
return "", fmt.Errorf("git history: %w", err)
150+
}
151+
// example: https://github.com/databrickslabs/ucx/blob/69a0cf8ce3450680dc222150f500d84a1eb523fc/src/databricks/labs/ucx/azure/access.py#L25-L35
152+
prefix := fmt.Sprintf("https://github.com/%s/%s/blob/%s", org, repo, commits[0].Sha)
153+
return prefix, nil
154+
}

acceptance/todos/finder_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package todos
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/databrickslabs/sandbox/go-libs/github"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestXXX(t *testing.T) {
12+
ctx := context.Background()
13+
14+
gh := github.NewClient(&github.GitHubConfig{})
15+
f, err := New(ctx, gh, "/Users/serge.smertin/git/labs/remorph")
16+
assert.NoError(t, err)
17+
18+
err = f.Create(ctx)
19+
assert.NoError(t, err)
20+
}

acceptance/todos/ignorer.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// copied from https://github.com/databricks/cli/blob/71cf426755260afc9152b41d231b9d0add495497/libs/fileset/ignorer.go
2+
3+
package todos
4+
5+
import (
6+
ignore "github.com/sabhiram/go-gitignore"
7+
)
8+
9+
// Ignorer is the interface for what determines if a path
10+
// in the [FileSet] must be ignored or not.
11+
type Ignorer interface {
12+
IgnoreFile(path string) (bool, error)
13+
IgnoreDirectory(path string) (bool, error)
14+
}
15+
16+
// nopIgnorer implements an [Ignorer] that doesn't ignore anything.
17+
type nopIgnorer struct{}
18+
19+
func (nopIgnorer) IgnoreFile(path string) (bool, error) {
20+
return false, nil
21+
}
22+
23+
func (nopIgnorer) IgnoreDirectory(path string) (bool, error) {
24+
return false, nil
25+
}
26+
27+
type includer struct {
28+
matcher *ignore.GitIgnore
29+
}
30+
31+
func newIncluder(includes []string) *includer {
32+
matcher := ignore.CompileIgnoreLines(includes...)
33+
return &includer{
34+
matcher,
35+
}
36+
}
37+
38+
func (i *includer) IgnoreFile(path string) (bool, error) {
39+
return i.ignore(path), nil
40+
}
41+
42+
// In the context of 'include' functionality, the Ignorer logic appears to be reversed:
43+
// For patterns like 'foo/bar/' which intends to match directories only, we still need to traverse into the directory for potential file matches.
44+
// Ignoring the directory entirely isn't an option, especially when dealing with patterns like 'foo/bar/*.go'.
45+
// While this pattern doesn't target directories, it does match all Go files within them and ignoring directories not matching the pattern
46+
// Will result in missing file matches.
47+
// During the tree traversal process, we call 'IgnoreDirectory' on ".", "./foo", and "./foo/bar",
48+
// all while applying the 'foo/bar/*.go' pattern. To handle this situation effectively, it requires to make the code more complex.
49+
// This could mean generating various prefix patterns to facilitate the exclusion of directories from traversal.
50+
// It's worth noting that, in this particular case, opting for a simpler logic results in a performance trade-off.
51+
func (i *includer) IgnoreDirectory(path string) (bool, error) {
52+
return false, nil
53+
}
54+
55+
func (i *includer) ignore(path string) bool {
56+
matched := i.matcher.MatchesPath(path)
57+
// If matched, do not ignore the file because we want to include it
58+
return !matched
59+
}

0 commit comments

Comments
 (0)