Skip to content

Commit 53f1b74

Browse files
committed
Allow searching for jira issues (#1)
1 parent b964533 commit 53f1b74

File tree

19 files changed

+868
-0
lines changed

19 files changed

+868
-0
lines changed

models/matera/jira/issue.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package jira
2+
3+
import (
4+
"code.gitea.io/gitea/models/db"
5+
"code.gitea.io/gitea/models/repo"
6+
"code.gitea.io/gitea/models/user"
7+
"code.gitea.io/gitea/modules/log"
8+
"code.gitea.io/gitea/modules/timeutil"
9+
gitea_context "code.gitea.io/gitea/services/context"
10+
"xorm.io/builder"
11+
)
12+
13+
type JiraIssueRelatedCommit struct {
14+
ID int64 `xorm:"pk autoincr"`
15+
RepoID int64 `xorm:"not null index(jira_issue_ticket) index(jira_issue_repo)"`
16+
Ticket string `xorm:"not null index(jira_issue_ticket)"`
17+
SHA string `xorm:"varchar(64) not null"`
18+
CreatedUnix timeutil.TimeStamp `xorm:"not null index(jira_issue_ticket)"`
19+
}
20+
21+
type JiraIssueRelatedCommitKey struct {
22+
SHA string
23+
Ticket string
24+
}
25+
26+
type FindJiraIssueRelatedCommitOptions struct {
27+
db.ListOptions
28+
RepoID int64
29+
}
30+
31+
type FindJiraIssueRelatedCommitByTicketOptions struct {
32+
db.ListOptions
33+
Ticket string
34+
Doer *user.User
35+
}
36+
37+
func (opts FindJiraIssueRelatedCommitOptions) ToConds() builder.Cond {
38+
var cond builder.Cond = builder.Eq{"repo_id": opts.RepoID}
39+
return cond
40+
}
41+
42+
func (opts FindJiraIssueRelatedCommitOptions) ToOrders() string {
43+
return "created_unix DESC, id DESC"
44+
}
45+
46+
func (opts FindJiraIssueRelatedCommitByTicketOptions) ToConds() builder.Cond {
47+
cond := builder.Eq{"ticket": opts.Ticket}
48+
return cond.And(builder.In("repo_id", opts.accessibleRepos()))
49+
}
50+
51+
func (opts FindJiraIssueRelatedCommitByTicketOptions) ToOrders() string {
52+
return "repo_id, created_unix asc"
53+
}
54+
55+
func (opts FindJiraIssueRelatedCommitByTicketOptions) accessibleRepos() *builder.Builder {
56+
searchOpts := repo.SearchRepoOptions{
57+
Actor: opts.Doer,
58+
OwnerID: opts.Doer.ID,
59+
Private: true,
60+
AllPublic: true,
61+
AllLimited: true,
62+
}
63+
64+
return builder.Select("id").From("repository").Where(repo.SearchRepositoryCondition(&searchOpts))
65+
}
66+
67+
type JiraIssue struct {
68+
TicketId string
69+
TotalCommits int
70+
Commits []*JiraIssueRelatedCommit
71+
}
72+
73+
func GetJiraIssueByTicket(ctx *gitea_context.Context, criterion FindJiraIssueRelatedCommitByTicketOptions) (*JiraIssue, error) {
74+
commits := make([]*JiraIssueRelatedCommit, 0, 10)
75+
76+
count, err := db.GetEngine(ctx).Where(criterion.ToConds()).Count(new(JiraIssueRelatedCommit))
77+
78+
if err != nil {
79+
log.Error("Error while trying to count JiraIssueRelatedCommit: %v", err)
80+
return nil, err
81+
}
82+
83+
if count == 0 {
84+
return &JiraIssue{TicketId: criterion.Ticket, Commits: commits, TotalCommits: int(count)}, nil
85+
}
86+
87+
offset, limit := criterion.GetSkipTake()
88+
89+
if err = db.GetEngine(ctx).Where(criterion.ToConds()).OrderBy(criterion.ToOrders()).Limit(limit, offset).Find(&commits); err != nil {
90+
log.Error("Error while trying to fetch JiraIssueRelatedCommit: %v", err)
91+
return nil, err
92+
}
93+
94+
return &JiraIssue{TicketId: criterion.Ticket, Commits: commits, TotalCommits: int(count)}, nil
95+
}
96+
97+
func (issues *JiraIssue) CommitsByRepo() map[int64][]*JiraIssueRelatedCommit {
98+
result := make(map[int64][]*JiraIssueRelatedCommit)
99+
100+
for _, commit := range issues.Commits {
101+
if _, ok := result[commit.RepoID]; !ok {
102+
result[commit.RepoID] = []*JiraIssueRelatedCommit{}
103+
}
104+
result[commit.RepoID] = append(result[commit.RepoID], commit)
105+
}
106+
107+
return result
108+
}
109+
110+
func (commit JiraIssueRelatedCommit) GetKey() JiraIssueRelatedCommitKey {
111+
return JiraIssueRelatedCommitKey{
112+
Ticket: commit.Ticket,
113+
SHA: commit.SHA,
114+
}
115+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package v1_22 //nolint
5+
6+
import (
7+
"fmt"
8+
9+
"code.gitea.io/gitea/modules/timeutil"
10+
"xorm.io/xorm"
11+
)
12+
13+
func CreateJiraIssueRelatedCommitTable(x *xorm.Engine) error {
14+
type JiraIssueRelatedCommit struct {
15+
ID int64 `xorm:"pk autoincr"`
16+
Ticket string `xorm:"varchar(64) not null index(jira_issue_ticket) index(jira_issue_repo)"`
17+
RepoID int64 `xorm:"not null index(jira_issue_ticket)"`
18+
SHA string `xorm:"varchar(64) not null`
19+
CreatedUnix timeutil.TimeStamp `xorm:"not null index(jira_issue_ticket)"`
20+
}
21+
22+
if error := x.Sync(new(JiraIssueRelatedCommit)); error != nil {
23+
return fmt.Errorf("Error creating jira_issue_related_commit table: %w", error)
24+
} else {
25+
return nil
26+
}
27+
}

models/migrations/migrations.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"context"
99
"fmt"
1010

11+
matera_v_1_22 "code.gitea.io/gitea/models/migrations/matera/v_1_22"
1112
"code.gitea.io/gitea/models/migrations/v1_10"
1213
"code.gitea.io/gitea/models/migrations/v1_11"
1314
"code.gitea.io/gitea/models/migrations/v1_12"
@@ -587,6 +588,13 @@ var migrations = []Migration{
587588
NewMigration("Drop wrongly created table o_auth2_application", v1_22.DropWronglyCreatedTable),
588589

589590
// Gitea 1.22.0-rc1 ends at 299
591+
592+
// Start matera extra migrations
593+
594+
// v299 -> v300
595+
NewMigration("Create jira relate issues table", matera_v_1_22.CreateJiraIssueRelatedCommitTable),
596+
597+
// End matera migrations
590598
}
591599

592600
// GetCurrentDBVersion returns the current db version

modules/matera/git/repo_issue.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package git
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"fmt"
7+
"io"
8+
"regexp"
9+
"strconv"
10+
"strings"
11+
12+
jira_issue_models "code.gitea.io/gitea/models/matera/jira"
13+
git_module "code.gitea.io/gitea/modules/git"
14+
"code.gitea.io/gitea/modules/timeutil"
15+
"code.gitea.io/gitea/modules/util"
16+
)
17+
18+
var issueRegex = regexp.MustCompile("^([A-Z]*-\\d+)")
19+
20+
type Parser struct {
21+
scanner *bufio.Scanner
22+
}
23+
24+
func GetIssueRelateCommits(repo *git_module.Repository, page int, pageSize int) ([]*jira_issue_models.JiraIssueRelatedCommit, int, error) {
25+
stdoutReader, stdoutWriter := io.Pipe()
26+
defer stdoutReader.Close()
27+
defer stdoutWriter.Close()
28+
stderr := strings.Builder{}
29+
30+
rc := &git_module.RunOpts{Dir: repo.Path, Stdout: stdoutWriter, Stderr: &stderr}
31+
32+
go func() {
33+
err := git_module.NewCommand(repo.Ctx, "log").
34+
AddOptionFormat("--pretty=format:%s", "%H %at %s").
35+
AddArguments("--all").Run(rc)
36+
if err != nil {
37+
_ = stdoutWriter.CloseWithError(git_module.ConcatenateError(err, stderr.String()))
38+
} else {
39+
_ = stdoutWriter.Close()
40+
}
41+
}()
42+
43+
parser := NewParser(stdoutReader)
44+
commits, err := parser.ParseCommits()
45+
46+
if err != nil {
47+
return nil, 0, err
48+
}
49+
50+
//sortTagsByTime(tags)
51+
commitsTotal := len(commits)
52+
if page != 0 {
53+
commits = util.PaginateSlice(commits, page, pageSize).([]*jira_issue_models.JiraIssueRelatedCommit)
54+
}
55+
56+
return commits, commitsTotal, nil
57+
}
58+
59+
func NewParser(r io.Reader) *Parser {
60+
scanner := bufio.NewScanner(r)
61+
scanner.Buffer(make([]byte, 0, 1024*1024), 1024*1024)
62+
63+
refDelim := make([]byte, 0, 1)
64+
refDelim = append(refDelim, '\n')
65+
66+
// Split input into delimiter-separated "reference blocks".
67+
scanner.Split(
68+
func(data []byte, atEOF bool) (advance int, token []byte, err error) {
69+
// Scan until delimiter, marking end of reference.
70+
delimIdx := bytes.Index(data, refDelim)
71+
if delimIdx >= 0 {
72+
token := data[:delimIdx]
73+
advance := delimIdx + len(refDelim)
74+
return advance, token, nil
75+
}
76+
// If we're at EOF, we have a final, non-terminated reference. Return it.
77+
if atEOF {
78+
return len(data), data, nil
79+
}
80+
// Not yet a full field. Request more data.
81+
return 0, nil, nil
82+
})
83+
84+
return &Parser{
85+
scanner: scanner,
86+
}
87+
}
88+
89+
func (parser *Parser) ParseCommits() ([]*jira_issue_models.JiraIssueRelatedCommit, error) {
90+
var commits []*jira_issue_models.JiraIssueRelatedCommit
91+
for {
92+
commit, hasNext, err := parser.next()
93+
if err != nil {
94+
return nil, fmt.Errorf("GetIssueRelateCommits: parse commit: %w", err)
95+
} else if commit != nil {
96+
commits = append(commits, commit)
97+
}
98+
if !hasNext {
99+
return commits, nil
100+
}
101+
}
102+
}
103+
104+
func (parser *Parser) next() (*jira_issue_models.JiraIssueRelatedCommit, bool, error) {
105+
if !parser.scanner.Scan() {
106+
return nil, false, nil
107+
}
108+
line := parser.scanner.Text()
109+
if line == "" {
110+
return nil, false, nil
111+
} else {
112+
parts := strings.SplitN(line, " ", 3)
113+
ticket := issueRegex.FindString(parts[2])
114+
if ticket == "" {
115+
return nil, true, nil
116+
} else {
117+
timestamp, err := parser.toTimestamp(parts[1])
118+
if err != nil {
119+
return nil, false, err
120+
}
121+
122+
return &jira_issue_models.JiraIssueRelatedCommit{
123+
Ticket: ticket,
124+
SHA: parts[0],
125+
CreatedUnix: timestamp,
126+
}, true, nil
127+
}
128+
}
129+
}
130+
131+
func (parser *Parser) toTimestamp(value string) (timeutil.TimeStamp, error) {
132+
asInt, err := strconv.ParseInt(value, 10, 64)
133+
if err != nil {
134+
return timeutil.TimeStampNow(), err
135+
} else {
136+
return timeutil.TimeStamp(asInt), nil
137+
}
138+
}

0 commit comments

Comments
 (0)