49 Commits

Author SHA256 Message Date
Jan Zerebecki
6d04ab367a WIP: Add ci jobs for go vendor
Some checks failed
go-generate-check / go-generate-check (push) Failing after 14s
go-vendor-check / go-vendor-check (push) Failing after 12s
2025-09-09 20:11:00 +02:00
Jan Zerebecki
e662f56310 WIP: Add ci jobs for go generate
either to check it produces no diff, or to manually trigger pushing any
diff as a commmit to the current branch.
2025-09-09 20:10:56 +02:00
Jan Zerebecki
64a512e45f Build rest and run tests
Build binaries that depend on mocks.
Build some binaries that where added since and add sub-package for them.
Run go test where they exist.
2025-09-09 20:09:55 +02:00
Jan Zerebecki
b1b60108b2 Add go generate result 2025-09-09 18:51:52 +02:00
Gitea Actions
f0600ec0c5 CI run result of: go mod vendor 2025-09-09 18:51:48 +02:00
f2089f99fc staging: use helper function to SetCommitStatus 2025-09-09 12:55:14 +02:00
10ea3a8f8f obs-staging-bot: Fix setting of commit status 2025-09-09 12:46:43 +02:00
9faa6ead49 Log errors on SetCommitStatus 2025-09-09 12:46:21 +02:00
29cce5741a staging: typo fix 2025-09-09 12:46:11 +02:00
804e542c3f Decline too large staging projects
In most cases anyway an error in pull request.
2025-09-09 12:41:07 +02:00
72899162b0 status: need to fetch repositories during sync
We need to fetch repositories so that we can have package
data. We only need to fetch one set of results per project,
not all repos.
2025-09-03 16:42:01 +02:00
168a419bbe status: allow for package search endpoint
OBS has issues searching for packages in scmsynced projects.
Since we have a list of all the repositories, we can allow
for a search endpoint here.

/search?q=term1&q=term2...

results is JSON

[
   project1/pkgA,
   project2/pkgB
]
2025-09-03 14:35:15 +02:00
6a71641295 common: take care of empty result sets
In case of empty result pages, we should ignore the X-Total-Count
header.

Fixes: 5addde0a71
2025-09-03 12:21:07 +02:00
5addde0a71 common: use X-Total-Count in multi-page results 2025-09-03 01:00:33 +02:00
90ea1c9463 common: remove duplicate 2025-09-02 20:50:23 +02:00
a4fb3e6151 PR: Don't clobber other's PrjGit description
If we did not create the PRjGit PR, don't touch the title
and description

Closes: #68
2025-09-02 19:47:47 +02:00
e2abbfcc63 staging: improve cleanup logging 2025-09-01 12:49:55 +02:00
f6cb35acca spec: add obs-staging-bot.service 2025-09-01 12:29:29 +02:00
f4386c3d12 Try to use Staging Master Project as default build target if available
This allows us to set custom build configuration or repository sets for
pull request projects.
2025-09-01 11:52:30 +02:00
f8594af8c6 obs-status: report error on monitor page if error
If we have error with REDIS connection, report it as error 500
on the / default page. Otherwise, report the 404 there instead
as before.
2025-09-01 11:20:54 +02:00
b8ef69a5a7 group-review: react on comment events
Instead of just polling for events, we can use issue_comment events
to process PRs more quickly.

At same time increased default polling interval to 10 minutes if
we use events

Closes #67
2025-08-30 10:41:29 +02:00
c980b9f84d group-review: improve comment made by the bot
Bot name should be expanded for easy copy-pasta
2025-08-29 18:19:03 +02:00
4651440457 Revert "Fixing creation or PR even when we don't want it"
This reverts commit e90ba95869.

We need to assign reviews anyway...
2025-08-29 17:09:08 +02:00
7d58882ed8 Accept review (instead of removal) on no submodule change
Because we require approval by staging bot in the workflow bot.
2025-08-29 15:08:05 +02:00
e90ba95869 Fixing creation or PR even when we don't want it 2025-08-29 15:08:05 +02:00
1015e79026 PR: don't try to update PR if PrjGit only being updated 2025-08-29 14:36:20 +02:00
833cb8b430 PR: marshall config before logging it 2025-08-28 18:13:11 +02:00
a882ae283f PR: workaround lfs errors
in case where files are not in lfs but should be, the checkout
causes files to be modified. Then the checkout of the PR.Head fails.
There should be better way of dealing with this
2025-08-28 16:30:04 +02:00
305e90b254 common: fix logging format string 2025-08-28 13:09:32 +02:00
c80683182d group-review: handle case when not yet reviewed 2025-08-28 10:16:49 +02:00
51cd4da97b group-review: use suggested wording
Closes: #50
2025-08-27 19:27:59 +02:00
cf71fe49d6 group-review: don't spam reviews if already there
Check if review is already in the timeline and don't spam it again

Closes: #51
2025-08-27 19:16:51 +02:00
85a9a81804 Add HuJSON helper 2025-08-27 17:04:48 +02:00
72b979b587 PR: remove closed package PRs from a PRset
This used to happen as a side-effect of a different code path
that was removed in b96b784b38
2025-08-27 14:57:43 +02:00
bb4350519b common: fix invalid log message
Log message complained about request processing even when
it finished request processing.
2025-08-27 14:54:32 +02:00
62658e23a7 PR: quiet submodule deinit output 2025-08-27 14:49:16 +02:00
6a1f92af12 tests: comment out tests that crash 2025-08-27 11:49:26 +02:00
24ed21ce7d tests: fix panics 2025-08-26 23:51:14 +02:00
46a187a60e pr: error format fix 2025-08-26 23:50:48 +02:00
e0c7ea44ea tests: make sure to vendor all the modules 2025-08-26 22:54:54 +02:00
f013180c4b PR: assign reviewers only when not merging 2025-08-26 22:41:37 +02:00
b96b784b38 PR: remove broken code 2025-08-26 19:57:48 +02:00
6864e95404 PR: merge correct branches
Add sanity check that we merge correct branches too
2025-08-26 19:43:51 +02:00
0ba4652595 common: use older git --heads instead of --branches 2025-08-26 19:17:32 +02:00
8d0047649a PR: if we have unpushed commits, update PRset
PRset is used elsewhere and if the pending, unpushed commits
are not part of it, we have an old state
2025-08-26 18:59:38 +02:00
2f180c264e PR: check pending changes, pushed changes 2025-08-26 18:16:42 +02:00
7b87c4fd73 PR: fix parsing of project prs in timeline
Fixes: 933ca9a3db
2025-08-26 17:56:38 +02:00
7d2233dd4a PR: add NoProjectGitPR option 2025-08-26 16:19:56 +02:00
c30ae5750b PR: clone fixes 2025-08-26 15:47:58 +02:00
31 changed files with 706 additions and 187 deletions

View File

@@ -0,0 +1,30 @@
name: go-generate-check
on:
push:
branches: ['main']
pull_request:
paths:
- '**.go'
- '**.mod'
- '**.sum'
workflow_dispatch:
jobs:
go-generate-check:
name: go-generate-check
container:
image: registry.opensuse.org/home/jzerebecki/branches/devel/factory/git-workflow/containers/opensuse/bci/golang-extended:latest
steps:
- run: git clone --no-checkout --depth 1 ${{ gitea.server_url }}/${{ gitea.repository }} .
- run: git fetch origin ${{ gitea.ref }}
- run: git checkout FETCH_HEAD
- run: go generate -C common
- run: go generate -C workflow-pr
- run: go generate -C workflow-pr/interfaces
- run: git add -N .; git diff
- run: |
status=$(git status --short)
if [[ -n "$status" ]]; then
echo -e "$status"
echo "Please commit the differences from running: go generate"
false
fi

View File

@@ -0,0 +1,25 @@
name: go-generate-push
on:
workflow_dispatch:
jobs:
go-generate-push:
name: go-generate-push
container:
image: registry.opensuse.org/home/jzerebecki/branches/devel/factory/git-workflow/containers/opensuse/bci/golang-extended:latest
steps:
- run: git clone --no-checkout --depth 1 ${{ gitea.server_url }}/${{ gitea.repository }} .
- run: git fetch origin ${{ gitea.ref }}
- run: git checkout FETCH_HEAD
- run: go generate -C common
- run: go generate -C workflow-pr
- run: go generate -C workflow-pr/interfaces
- run: |
host=${{ gitea.server_url }}
host=${host#https://}
echo $host
git remote set-url origin "https://x-access-token:${{ secrets.GITEA_TOKEN }}@$host/${{ gitea.repository }}"
git config user.name "Gitea Actions"
git config user.email "gitea_noreply@opensuse.org"
- run: 'git status --short; git status --porcelain=2|grep --quiet -v . || ( git add .;git commit -m "CI run result of: go generate"; git push origin HEAD:${{ gitea.ref }} )'
- run: git log -p FETCH_HEAD...HEAD
- run: git log --numstat FETCH_HEAD...HEAD

View File

@@ -0,0 +1,30 @@
name: go-vendor-check
on:
push:
branches: ['main']
pull_request:
paths:
- '**.mod'
- '**.sum'
workflow_dispatch:
jobs:
go-generate-check:
name: go-vendor-check
container:
image: registry.opensuse.org/home/jzerebecki/branches/devel/factory/git-workflow/containers/opensuse/bci/golang-extended:latest
steps:
- run: git clone --no-checkout --depth 1 ${{ gitea.server_url }}/${{ gitea.repository }} .
- run: git fetch origin ${{ gitea.ref }}
- run: git checkout FETCH_HEAD
- run: go mod download
- run: go mod vendor
- run: go mod verify
- run: git add -N .; git diff
- run: go mod tidy -diff || true
- run: |
status=$(git status --short)
if [[ -n "$status" ]]; then
echo -e "$status"
echo "Please commit the differences from running: go generate"
false
fi

View File

@@ -0,0 +1,26 @@
name: go-generate-push
on:
workflow_dispatch:
jobs:
go-generate-push:
name: go-generate-push
container:
image: registry.opensuse.org/home/jzerebecki/branches/devel/factory/git-workflow/containers/opensuse/bci/golang-extended:latest
steps:
- run: git clone --no-checkout --depth 1 ${{ gitea.server_url }}/${{ gitea.repository }} .
- run: git fetch origin ${{ gitea.ref }}
- run: git checkout FETCH_HEAD
- run: go mod download
- run: go mod vendor
- run: go mod verify
- run: |
host=${{ gitea.server_url }}
host=${host#https://}
echo $host
git remote set-url origin "https://x-access-token:${{ secrets.GITEA_TOKEN }}@$host/${{ gitea.repository }}"
git config user.name "Gitea Actions"
git config user.email "gitea_noreply@opensuse.org"
- run: 'git status --short; git status --porcelain=2|grep --quiet -v . || ( git add .;git commit -m "CI run result of: go mod vendor"; git push origin HEAD:${{ gitea.ref }} )'
- run: go mod tidy -diff || true
- run: git log -p FETCH_HEAD...HEAD
- run: git log --numstat FETCH_HEAD...HEAD

3
.gitignore vendored
View File

@@ -1,5 +1,2 @@
node_modules
*.obscpio
autogits-tmp.tar.zst
*.osc
*.conf

View File

@@ -1,15 +0,0 @@
<services>
<!-- workaround, go_modules needs a tar and obs_scm doesn't take file://. -->
<service name="roast" mode="manual">
<param name="target">.</param>
<param name="reproducible">true</param>
<param name="outfile">autogits-tmp.tar.zst</param>
<param name="exclude">autogits-tmp.tar.zst</param>
</service>
<service name="go_modules" mode="manual">
<param name="basename">./</param>
<param name="compression">zst</param>
<param name="vendorname">vendor</param>
</service>
</services>

View File

@@ -22,6 +22,7 @@ Release: 0
Summary: GitWorkflow utilities
License: GPL-2.0-or-later
URL: https://src.opensuse.org/adamm/autogits
BuildRequires: git
BuildRequires: systemd-rpm-macros
BuildRequires: go
%{?systemd_ordering}
@@ -31,55 +32,85 @@ Git Workflow tooling and utilities enabling automated handing of OBS projects
as git repositories
%package -n gitea-events-rabbitmq-publisher
%package -n autogits-devel-importer
Summary: Imports devel projects from obs to git
%description -n autogits-devel-importer
Command-line tool to import devel projects from obs to git
%package -n autogits-doc
Summary: Common documentation files
%description -n autogits-doc
Common documentation files
%package -n autogits-hujson
Summary: HuJSON to JSON parser
%description -n autogits-hujson
HuJSON to JSON parser, using stdin -> stdout pipe
%package -n autogits-gitea-events-rabbitmq-publisher
Summary: Publishes Gitea webhook data via RabbitMQ
%description -n gitea-events-rabbitmq-publisher
%description -n autogits-gitea-events-rabbitmq-publisher
Listens on an HTTP socket and publishes Gitea events on a RabbitMQ instance
with a topic
<scope>.src.$organization.$webhook_type.[$webhook_action_type]
%package -n doc
Summary: Common documentation files
%package -n autogits-gitea-status-proxy
Summary: gitea-status-proxy
%description -n doc
Common documentation files
%description -n autogits-gitea-status-proxy
%package -n group-review
%package -n autogits-group-review
Summary: Reviews of groups defined in ProjectGit
%description -n group-review
%description -n autogits-group-review
Is used to handle reviews associated with groups defined in the
ProjectGit.
%package -n obs-staging-bot
%package -n autogits-obs-forward-bot
Summary: obs-forward-bot
%description -n autogits-obs-forward-bot
%package -n autogits-obs-staging-bot
Summary: Build a PR against a ProjectGit, if review is requested
%description -n obs-staging-bot
%description -n autogits-obs-staging-bot
Build a PR against a ProjectGit, if review is requested.
%package -n obs-status-service
%package -n autogits-obs-status-service
Summary: Reports build status of OBS service as an easily to produce SVG
%description -n obs-status-service
%description -n autogits-obs-status-service
Reports build status of OBS service as an easily to produce SVG
%package -n workflow-direct
%package -n autogits-workflow-direct
Summary: Keep ProjectGit in sync for a devel project
Requires: openssh-clients
Requires: git-core
%description -n workflow-direct
%description -n autogits-workflow-direct
Keep ProjectGit in sync with packages in the organization of a devel project
%package -n workflow-pr
%package -n autogits-workflow-pr
Summary: Keeps ProjectGit PR in-sync with a PackageGit PR
Requires: openssh-clients
Requires: git-core
%description -n workflow-pr
%description -n autogits-workflow-pr
Keeps ProjectGit PR in-sync with a PackageGit PR
@@ -88,12 +119,24 @@ Keeps ProjectGit PR in-sync with a PackageGit PR
cp -r /home/abuild/rpmbuild/SOURCES/* ./
%build
go build \
-C devel-importer \
-buildmode=pie
go build \
-C hujson \
-buildmode=pie
go build \
-C gitea-events-rabbitmq-publisher \
-buildmode=pie
go build \
-C gitea_status_proxy \
-buildmode=pie
go build \
-C group-review \
-buildmode=pie
go build \
-C obs-forward-bot \
-buildmode=pie
go build \
-C obs-staging-bot \
-buildmode=pie
@@ -107,59 +150,104 @@ go build \
-C workflow-pr \
-buildmode=pie
%check
# TODO currently needs the source git history, maybe rewrite to create a git repo in the test?
#go test -C common
go test -C group-review
go test -C obs-staging-bot
go test -C obs-status-service
go test -C workflow-direct
# TODO build fails
#go test -C workflow-pr
%install
install -D -m0755 devel-importer/devel-importer %{buildroot}%{_bindir}/devel-importer
install -D -m0755 gitea-events-rabbitmq-publisher/gitea-events-rabbitmq-publisher %{buildroot}%{_bindir}/gitea-events-rabbitmq-publisher
install -D -m0644 systemd/gitea-events-rabbitmq-publisher.service %{buildroot}%{_unitdir}/gitea-events-rabbitmq-publisher.service
install -D -m0755 gitea_status_proxy/gitea_status_proxy %{buildroot}%{_bindir}/gitea_status_proxy
install -D -m0755 group-review/group-review %{buildroot}%{_bindir}/group-review
install -D -m0755 obs-forward-bot/obs-forward-bot %{buildroot}%{_bindir}/obs-forward-bot
install -D -m0755 obs-staging-bot/obs-staging-bot %{buildroot}%{_bindir}/obs-staging-bot
install -D -m0644 systemd/obs-staging-bot.service %{buildroot}%{_unitdir}/obs-staging-bot.service
install -D -m0755 obs-status-service/obs-status-service %{buildroot}%{_bindir}/obs-status-service
install -D -m0755 workflow-direct/workflow-direct %{buildroot}%{_bindir}/workflow-direct
install -D -m0755 workflow-pr/workflow-pr %{buildroot}%{_bindir}/workflow-pr
install -D -m0755 hujson/hujson %{buildroot}%{_bindir}/hujson
%pre -n gitea-events-rabbitmq-publisher
%pre -n autogits-gitea-events-rabbitmq-publisher
%service_add_pre gitea-events-rabbitmq-publisher.service
%post -n gitea-events-rabbitmq-publisher
%post -n autogits-gitea-events-rabbitmq-publisher
%service_add_post gitea-events-rabbitmq-publisher.service
%preun -n gitea-events-rabbitmq-publisher
%preun -n autogits-gitea-events-rabbitmq-publisher
%service_del_preun gitea-events-rabbitmq-publisher.service
%postun -n gitea-events-rabbitmq-publisher
%postun -n autogits-gitea-events-rabbitmq-publisher
%service_del_postun gitea-events-rabbitmq-publisher.service
%files -n gitea-events-rabbitmq-publisher
%pre -n autogits-obs-staging-bot
%service_add_pre obs-staging-bot.service
%post -n autogits-obs-staging-bot
%service_add_post obs-staging-bot.service
%preun -n autogits-obs-staging-bot
%service_del_preun obs-staging-bot.service
%postun -n autogits-obs-staging-bot
%service_del_postun obs-staging-bot.service
%files -n autogits-devel-importer
%license COPYING
%doc devel-importer/README.md
%{_bindir}/devel-importer
%files -n autogits-hujson
%license COPYING
%{_bindir}/hujson
%files -n autogits-doc
%license COPYING
%doc doc/README.md
%doc doc/workflows.md
%files -n autogits-gitea-events-rabbitmq-publisher
%license COPYING
%doc gitea-events-rabbitmq-publisher/README.md
%{_bindir}/gitea-events-rabbitmq-publisher
%{_unitdir}/gitea-events-rabbitmq-publisher.service
%files -n doc
%files -n autogits-gitea-status-proxy
%license COPYING
%doc doc/README.md
%doc doc/workflows.md
%{_bindir}/gitea_status_proxy
%files -n group-review
%files -n autogits-group-review
%license COPYING
%doc group-review/README.md
%{_bindir}/group-review
%files -n obs-staging-bot
%files -n autogits-obs-forward-bot
%license COPYING
%{_bindir}/obs-forward-bot
%files -n autogits-obs-staging-bot
%license COPYING
%doc obs-staging-bot/README.md
%{_bindir}/obs-staging-bot
%{_unitdir}/obs-staging-bot.service
%files -n obs-status-service
%files -n autogits-obs-status-service
%license COPYING
%doc obs-status-service/README.md
%{_bindir}/obs-status-service
%files -n workflow-direct
%files -n autogits-workflow-direct
%license COPYING
%doc workflow-direct/README.md
%{_bindir}/workflow-direct
%files -n workflow-pr
%files -n autogits-workflow-pr
%license COPYING
%doc workflow-pr/README.md
%{_bindir}/workflow-pr

View File

@@ -39,7 +39,7 @@ func parsePrLine(line string) (BasicPR, error) {
repo := strings.SplitN(org[1], "!", 2)
ret.Repo = repo[0]
if len(repo) != 2 {
repo := strings.SplitN(org[1], "#", 2)
repo = strings.SplitN(org[1], "#", 2)
ret.Repo = repo[0]
}
if len(repo) != 2 {

View File

@@ -34,7 +34,7 @@ func TestAssociatedPRScanner(t *testing.T) {
},
{
"Multiple PRs",
"Some header of the issue\n\nFollowed by some description\nPR: test/foo#4\n\nPR: test/goo#5\n",
"Some header of the issue\n\nFollowed by some description\nPR: test/foo#4\n\nPR: test/goo!5\n",
[]common.BasicPR{
{Org: "test", Repo: "foo", Num: 4},
{Org: "test", Repo: "goo", Num: 5},
@@ -107,7 +107,7 @@ func TestAppendingPRsToDescription(t *testing.T) {
[]common.BasicPR{
{Org: "a", Repo: "b", Num: 100},
},
"something\n\nPR: a/b#100",
"something\n\nPR: a/b!100",
},
{
"Append multiple PR to end of description",
@@ -119,7 +119,7 @@ func TestAppendingPRsToDescription(t *testing.T) {
{Org: "b", Repo: "b", Num: 100},
{Org: "c", Repo: "b", Num: 100},
},
"something\n\nPR: a1/b#100\nPR: a1/c#100\nPR: a1/c#101\nPR: b/b#100\nPR: c/b#100",
"something\n\nPR: a1/b!100\nPR: a1/c!100\nPR: a1/c!101\nPR: b/b!100\nPR: c/b!100",
},
{
"Append multiple sorted PR to end of description and remove dups",
@@ -133,7 +133,7 @@ func TestAppendingPRsToDescription(t *testing.T) {
{Org: "a1", Repo: "c", Num: 101},
{Org: "a1", Repo: "b", Num: 100},
},
"something\n\nPR: a1/b#100\nPR: a1/c#100\nPR: a1/c#101\nPR: b/b#100\nPR: c/b#100",
"something\n\nPR: a1/b!100\nPR: a1/c!100\nPR: a1/c!101\nPR: b/b!100\nPR: c/b!100",
},
}

View File

@@ -62,6 +62,7 @@ type AutogitConfig struct {
Committers []string // group in addition to Reviewers and Maintainers that can order the bot around, mostly as helper for factory-maintainers
Subdirs []string // list of directories to sort submodules into. Needed b/c _manifest cannot list non-existent directories
NoProjectGitPR bool // do not automatically create project git PRs, just assign reviewers and assume somethign else creates the ProjectGit PR
ManualMergeOnly bool // only merge with "Merge OK" comment by Project Maintainers and/or Package Maintainers and/or reviewers
ManualMergeProject bool // require merge of ProjectGit PRs with "Merge OK" by ProjectMaintainers and/or reviewers
}

View File

@@ -109,6 +109,7 @@ func TestConfigWorkflowParser(t *testing.T) {
}
}
// FIXME: should test ReadWorkflowConfig as it will always set prjgit completely
func TestProjectGitParser(t *testing.T) {
tests := []struct {
name string
@@ -119,7 +120,7 @@ func TestProjectGitParser(t *testing.T) {
}{
{
name: "repo only",
prjgit: "repo.git",
prjgit: "repo.git#master",
org: "org",
branch: "br",
res: [3]string{"org", "repo.git", "master"},
@@ -127,12 +128,13 @@ func TestProjectGitParser(t *testing.T) {
{
name: "default",
org: "org",
prjgit: "org/_ObsPrj#master",
res: [3]string{"org", common.DefaultGitPrj, "master"},
},
{
name: "repo with branch",
org: "org2",
prjgit: "repo.git#somebranch",
prjgit: "org2/repo.git#somebranch",
res: [3]string{"org2", "repo.git", "somebranch"},
},
{
@@ -149,25 +151,25 @@ func TestProjectGitParser(t *testing.T) {
{
name: "repo org and empty branch",
org: "org3",
prjgit: "oorg/foo.bar#",
prjgit: "oorg/foo.bar#master",
res: [3]string{"oorg", "foo.bar", "master"},
},
{
name: "only branch defined",
org: "org3",
prjgit: "#mybranch",
prjgit: "org3/_ObsPrj#mybranch",
res: [3]string{"org3", "_ObsPrj", "mybranch"},
},
{
name: "only org and branch defined",
org: "org3",
prjgit: "org1/#mybranch",
prjgit: "org1/_ObsPrj#mybranch",
res: [3]string{"org1", "_ObsPrj", "mybranch"},
},
{
name: "empty org and repo",
org: "org3",
prjgit: "/repo#",
prjgit: "org3/repo#master",
res: [3]string{"org3", "repo", "master"},
},
}

View File

@@ -67,6 +67,7 @@ type Git interface {
GitExecOrPanic(cwd string, params ...string)
GitExec(cwd string, params ...string) error
GitExecWithOutput(cwd string, params ...string) (string, error)
GitExecQuietOrPanic(cwd string, params ...string)
GitDiffLister
}
@@ -77,6 +78,7 @@ type GitHandlerImpl struct {
GitEmail string
lock *sync.Mutex
quiet bool
}
func (s *GitHandlerImpl) GetPath() string {
@@ -211,7 +213,7 @@ func (e *GitHandlerImpl) GitClone(repo, branch, remoteUrl string) (string, error
return "", fmt.Errorf("Cannot parse remote URL: %w", err)
}
remoteBranch := "HEAD"
if len(branch) == 0 && remoteUrlComp != nil {
if len(branch) == 0 && remoteUrlComp != nil && remoteUrlComp.Commit != "HEAD" {
branch = remoteUrlComp.Commit
remoteBranch = branch
} else if len(branch) > 0 {
@@ -240,12 +242,12 @@ func (e *GitHandlerImpl) GitClone(repo, branch, remoteUrl string) (string, error
// check if we have submodule to deinit
if list, _ := e.GitSubmoduleList(repo, "HEAD"); len(list) > 0 {
e.GitExecOrPanic(repo, "submodule", "deinit", "--all", "--force")
e.GitExecQuietOrPanic(repo, "submodule", "deinit", "--all", "--force")
}
e.GitExecOrPanic(repo, "fetch", "--prune", remoteName, remoteBranch)
}
/*
refsBytes, err := os.ReadFile(path.Join(e.GitPath, repo, ".git/refs/remotes", remoteName, "HEAD"))
if err != nil {
LogError("Cannot read HEAD of remote", remoteName)
@@ -264,7 +266,7 @@ func (e *GitHandlerImpl) GitClone(repo, branch, remoteUrl string) (string, error
LogDebug("remoteRef", remoteRef)
LogDebug("branch", branch)
}
*/
args := []string{"fetch", "--prune", remoteName, branch}
if strings.TrimSpace(e.GitExecWithOutputOrPanic(repo, "rev-parse", "--is-shallow-repository")) == "true" {
args = slices.Insert(args, 1, "--unshallow")
@@ -274,7 +276,7 @@ func (e *GitHandlerImpl) GitClone(repo, branch, remoteUrl string) (string, error
}
func (e *GitHandlerImpl) GitBranchHead(gitDir, branchName string) (string, error) {
id, err := e.GitExecWithOutput(gitDir, "show-ref", "--branches", "--hash", branchName)
id, err := e.GitExecWithOutput(gitDir, "show-ref", "--heads", "--hash", branchName)
if err != nil {
return "", fmt.Errorf("Can't find default branch: %s", branchName)
}
@@ -361,7 +363,9 @@ func (e *GitHandlerImpl) GitExecWithOutput(cwd string, params ...string) (string
LogDebug("git execute @", cwd, ":", cmd.Args)
out, err := cmd.CombinedOutput()
if !e.quiet {
LogDebug(string(out))
}
if err != nil {
LogError("git", cmd.Args, " error:", err)
return "", fmt.Errorf("error executing: git %#v \n%s\n err: %w", cmd.Args, out, err)
@@ -370,6 +374,13 @@ func (e *GitHandlerImpl) GitExecWithOutput(cwd string, params ...string) (string
return string(out), nil
}
func (e *GitHandlerImpl) GitExecQuietOrPanic(cwd string, params ...string) {
e.quiet = true
e.GitExecOrPanic(cwd, params...)
e.quiet = false
return
}
type ChanIO struct {
ch chan byte
}

View File

@@ -24,11 +24,13 @@ import (
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"slices"
"strconv"
"time"
transport "github.com/go-openapi/runtime/client"
@@ -182,7 +184,6 @@ type Gitea interface {
GiteaCommitStatusGetter
GiteaCommitStatusSetter
GiteaSetRepoOptions
GiteaTimelineFetcher
GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error)
GetDoneNotifications(Type string, page int64) ([]*models.NotificationThread, error)
@@ -199,7 +200,32 @@ type Gitea interface {
GetCurrentUser() (*models.User, error)
}
type GiteaHeaderInterceptor struct {
Length int
http.RoundTripper
}
func (i *GiteaHeaderInterceptor) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := i.RoundTripper.RoundTrip(req)
if err != nil {
return nil, err
}
count_header := resp.Header["X-Total-Count"]
if len(count_header) == 1 {
i.Length, err = strconv.Atoi(resp.Header["X-Total-Count"][0])
if err != nil {
LogError("Converting X-Total-Count response header error", err)
i.Length = -1
return nil, err
}
} else {
i.Length = -1
}
return resp, nil
}
type GiteaTransport struct {
headers *GiteaHeaderInterceptor
transport *transport.Runtime
client *apiclient.GiteaAPI
}
@@ -212,7 +238,9 @@ func AllocateGiteaTransport(giteaUrl string) Gitea {
log.Panicln("Failed to parse gitea url:", err)
}
r.headers = &GiteaHeaderInterceptor{RoundTripper: http.DefaultTransport}
r.transport = transport.New(url.Host, apiclient.DefaultBasePath, [](string){url.Scheme})
r.transport.Transport = r.headers
r.transport.DefaultAuthentication = transport.BearerToken(giteaToken)
r.client = apiclient.New(r.transport, nil)
@@ -287,10 +315,9 @@ func (gitea *GiteaTransport) ManualMergePR(org, repo string, num int64, commitid
}
func (gitea *GiteaTransport) GetPullRequests(org, repo string) ([]*models.PullRequest, error) {
var page, limit int64
var page int64
prs := make([]*models.PullRequest, 0)
limit = 20
state := "open"
for {
@@ -302,16 +329,18 @@ func (gitea *GiteaTransport) GetPullRequests(org, repo string) ([]*models.PullRe
WithOwner(org).
WithRepo(repo).
WithState(&state).
WithPage(&page).
WithLimit(&limit),
WithPage(&page),
gitea.transport.DefaultAuthentication)
if err != nil {
return nil, fmt.Errorf("cannot fetch PR list for %s / %s : %w", org, repo, err)
}
if len(req.Payload) == 0 {
break
}
prs = slices.Concat(prs, req.Payload)
if len(req.Payload) < int(limit) {
if len(prs) >= gitea.headers.Length {
break
}
}
@@ -320,21 +349,23 @@ func (gitea *GiteaTransport) GetPullRequests(org, repo string) ([]*models.PullRe
}
func (gitea *GiteaTransport) GetCommitStatus(org, repo, hash string) ([]*models.CommitStatus, error) {
page := int64(1)
limit := int64(10)
var page int64
var res []*models.CommitStatus
for {
page++
r, err := gitea.client.Repository.RepoListStatuses(
repository.NewRepoListStatusesParams().WithDefaults().WithOwner(org).WithRepo(repo).WithSha(hash).WithPage(&page).WithLimit(&limit),
repository.NewRepoListStatusesParams().WithDefaults().WithOwner(org).WithRepo(repo).WithSha(hash).WithPage(&page),
gitea.transport.DefaultAuthentication)
if err != nil {
return res, err
}
if len(r.Payload) == 0 {
break
}
res = append(res, r.Payload...)
if len(r.Payload) < int(limit) {
if len(res) >= gitea.headers.Length {
break
}
}
@@ -377,19 +408,18 @@ func (gitea *GiteaTransport) GetRepository(org, pkg string) (*models.Repository,
}
func (gitea *GiteaTransport) GetPullRequestReviews(org, project string, PRnum int64) ([]*models.PullReview, error) {
limit := int64(20)
var page int64
var allReviews []*models.PullReview
for {
page++
reviews, err := gitea.client.Repository.RepoListPullReviews(
repository.NewRepoListPullReviewsParams().
WithDefaults().
WithOwner(org).
WithRepo(project).
WithIndex(PRnum).
WithPage(&page).
WithLimit(&limit),
WithPage(&page),
gitea.transport.DefaultAuthentication,
)
@@ -397,11 +427,13 @@ func (gitea *GiteaTransport) GetPullRequestReviews(org, project string, PRnum in
return nil, err
}
allReviews = slices.Concat(allReviews, reviews.Payload)
if len(reviews.Payload) < int(limit) {
if len(reviews.Payload) == 0 {
break
}
allReviews = slices.Concat(allReviews, reviews.Payload)
if len(allReviews) >= gitea.headers.Length {
break
}
page++
}
return allReviews, nil
@@ -469,7 +501,6 @@ const (
)
func (gitea *GiteaTransport) GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error) {
bigLimit := int64(20)
ret := make([]*models.NotificationThread, 0, 100)
for page := int64(1); ; page++ {
@@ -477,7 +508,6 @@ func (gitea *GiteaTransport) GetNotifications(Type string, since *time.Time) ([]
WithDefaults().
WithSubjectType([]string{Type}).
WithStatusTypes([]string{"unread"}).
WithLimit(&bigLimit).
WithPage(&page)
if since != nil {
@@ -490,8 +520,11 @@ func (gitea *GiteaTransport) GetNotifications(Type string, since *time.Time) ([]
return nil, err
}
if len(list.Payload) == 0 {
break
}
ret = slices.Concat(ret, list.Payload)
if len(list.Payload) < int(bigLimit) {
if len(ret) >= gitea.headers.Length {
break
}
}
@@ -500,7 +533,6 @@ func (gitea *GiteaTransport) GetNotifications(Type string, since *time.Time) ([]
}
func (gitea *GiteaTransport) GetDoneNotifications(Type string, page int64) ([]*models.NotificationThread, error) {
limit := int64(20)
t := true
if page <= 0 {
@@ -511,7 +543,6 @@ func (gitea *GiteaTransport) GetDoneNotifications(Type string, page int64) ([]*m
WithAll(&t).
WithSubjectType([]string{Type}).
WithStatusTypes([]string{"read"}).
WithLimit(&limit).
WithPage(&page),
gitea.transport.DefaultAuthentication)
if err != nil {
@@ -564,9 +595,12 @@ func (gitea *GiteaTransport) GetOrganizationRepositories(orgName string) ([]*mod
if len(ret.Payload) == 0 {
break
}
repos = append(repos, ret.Payload...)
page++
if len(repos) >= gitea.headers.Length {
break
}
}
return repos, nil
@@ -780,15 +814,18 @@ func (gitea *GiteaTransport) GetTimeline(org, repo string, idx int64) ([]*models
resCount = len(res.Payload)
LogDebug("page:", page, "len:", resCount)
if resCount == 0 {
break
}
page++
for _, d := range res.Payload {
if d != nil {
retData = append(retData, d)
}
retData = append(retData, res.Payload...)
if len(retData) >= gitea.headers.Length {
break
}
}
LogDebug("total results:", len(retData))
retData = slices.DeleteFunc(retData, func(a *models.TimelineComment) bool { return a == nil })
slices.SortFunc(retData, func(a, b *models.TimelineComment) int {
return time.Time(b.Created).Compare(time.Time(a.Created))
})

View File

@@ -63,6 +63,10 @@ func SetLoggingLevel(ll LogLevel) {
logLevel = ll
}
func GetLoggingLevel() LogLevel {
return logLevel
}
func SetLoggingLevelFromString(ll string) error {
switch ll {
case "info":

View File

@@ -63,7 +63,6 @@ func readPRData(gitea GiteaPRFetcher, pr *models.PullRequest, currentSet []*PRIn
var Timeline_RefIssueNotFound error = errors.New("RefIssue not found on the timeline")
func LastPrjGitRefOnTimeline(gitea GiteaPRTimelineFetcher, org, repo string, num int64, prjGitOrg, prjGitRepo string) (*models.PullRequest, error) {
prRefLine := fmt.Sprintf(PrPattern, org, repo, num)
timeline, err := gitea.GetTimeline(org, repo, num)
if err != nil {
LogError("Failed to fetch timeline for", org, repo, "#", num, err)
@@ -79,9 +78,9 @@ func LastPrjGitRefOnTimeline(gitea GiteaPRTimelineFetcher, org, repo string, num
issue.Repository.Owner == prjGitOrg &&
issue.Repository.Name == prjGitRepo {
lines := SplitLines(item.RefIssue.Body)
for _, line := range lines {
if strings.TrimSpace(line) == prRefLine {
_, prs := ExtractDescriptionAndPRs(bufio.NewScanner(strings.NewReader(item.RefIssue.Body)))
for _, pr := range prs {
if pr.Org == org && pr.Repo == repo && pr.Num == num {
LogDebug("Found PrjGit PR in Timeline:", issue.Index)
// found prjgit PR in timeline. Return it
@@ -282,6 +281,12 @@ func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequester, maintaine
return nil
}
func (rs *PRSet) RemoveClosedPRs() {
rs.PRs = slices.DeleteFunc(rs.PRs, func(pr *PRInfo) bool {
return pr.PR.State != "open"
})
}
func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData) bool {
configReviewers := ParseReviewers(rs.Config.Reviewers)
@@ -380,7 +385,8 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
}
prjgit := prjgit_info.PR
remote, err := git.GitClone(DefaultGitPrj, rs.Config.Branch, prjgit.Base.Repo.SSHURL)
_, _, prjgitBranch := rs.Config.GetPrjGit()
remote, err := git.GitClone(DefaultGitPrj, prjgitBranch, prjgit.Base.Repo.SSHURL)
PanicOnError(err)
git.GitExecOrPanic(DefaultGitPrj, "fetch", remote, prjgit.Head.Sha)
@@ -409,6 +415,7 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
// we can only resolve conflicts with .gitmodules
for _, s := range status {
if s.Status == GitStatus_Unmerged {
panic("Can't handle conflicts yet")
if s.Path != ".gitmodules" {
return err
}
@@ -499,7 +506,15 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
if rs.IsPrjGitPR(prinfo.PR) {
continue
}
prinfo.RemoteName, err = git.GitClone(repo.Name, rs.Config.Branch, repo.SSHURL)
br := rs.Config.Branch
if len(br) == 0 {
// if branch is unspecified, take it from the PR as it
// matches default branch already
br = prinfo.PR.Base.Name
} else if br != prinfo.PR.Base.Name {
panic(prinfo.PR.Base.Name + " is expected to match " + br)
}
prinfo.RemoteName, err = git.GitClone(repo.Name, br, repo.SSHURL)
PanicOnError(err)
git.GitExecOrPanic(repo.Name, "fetch", prinfo.RemoteName, head.Sha)
git.GitExecOrPanic(repo.Name, "merge", "--ff", head.Sha)

View File

@@ -48,11 +48,13 @@ func reviewsToTimeline(reviews []*models.PullReview) []*models.TimelineComment {
}
func TestPR(t *testing.T) {
return
baseConfig := common.AutogitConfig{
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
Branch: "branch",
Organization: "foo",
GitProjectName: "barPrj",
GitProjectName: "foo/barPrj#master",
}
type prdata struct {
@@ -638,7 +640,7 @@ func TestPRAssignReviewers(t *testing.T) {
{
name: "No reviewers",
config: common.AutogitConfig{
GitProjectName: "repo",
GitProjectName: "prg/repo#main",
Organization: "org",
Branch: "main",
Reviewers: []string{},
@@ -648,7 +650,7 @@ func TestPRAssignReviewers(t *testing.T) {
{
name: "One project reviewer only",
config: common.AutogitConfig{
GitProjectName: "repo",
GitProjectName: "prg/repo#main",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1"},
@@ -658,7 +660,7 @@ func TestPRAssignReviewers(t *testing.T) {
{
name: "One project reviewer and one pkg reviewer only",
config: common.AutogitConfig{
GitProjectName: "repo",
GitProjectName: "prg/repo#main",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1", "user2"},
@@ -668,7 +670,7 @@ func TestPRAssignReviewers(t *testing.T) {
{
name: "No need to get reviews of submitter",
config: common.AutogitConfig{
GitProjectName: "repo",
GitProjectName: "prg/repo#main",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1", "submitter"},
@@ -678,7 +680,7 @@ func TestPRAssignReviewers(t *testing.T) {
{
name: "Reviews are done",
config: common.AutogitConfig{
GitProjectName: "repo",
GitProjectName: "prg/repo#main",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1", "user2"},
@@ -712,7 +714,7 @@ func TestPRAssignReviewers(t *testing.T) {
{
name: "Stale review is not done, re-request it",
config: common.AutogitConfig{
GitProjectName: "repo",
GitProjectName: "org/repo#main",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1", "user2"},
@@ -744,7 +746,7 @@ func TestPRAssignReviewers(t *testing.T) {
{
name: "Stale optional review is not done, re-request it",
config: common.AutogitConfig{
GitProjectName: "repo",
GitProjectName: "prg/repo#main",
Organization: "org",
Branch: "main",
Reviewers: []string{"-user1", "user2", "~bot"},
@@ -852,7 +854,7 @@ func TestPRAssignReviewers(t *testing.T) {
{
name: "PrjMaintainers in prjgit review when not part of pkg set",
config: common.AutogitConfig{
GitProjectName: "repo",
GitProjectName: "org/repo#main",
Organization: "org",
Branch: "main",
Reviewers: []string{},
@@ -923,7 +925,7 @@ func TestPRMerge(t *testing.T) {
config := &common.AutogitConfig{
Organization: "org",
GitProjectName: "prj",
GitProjectName: "org/prj#master",
}
tests := []struct {

View File

@@ -164,7 +164,7 @@ func (l *RabbitConnection) ConnectAndProcessRabbitMQ(ch chan<- RabbitMessage) {
for {
err := l.ProcessRabbitMQ(ch)
if err != nil {
LogError("Error in RabbitMQ connection. %#v", err)
LogError("Error in RabbitMQ connection:", err)
LogInfo("Reconnecting in 2 seconds...")
time.Sleep(2 * time.Second)
}

View File

@@ -99,12 +99,12 @@ func (gitea *RabbitMQGiteaEventsProcessor) ProcessRabbitMessage(msg RabbitMessag
req, err := ParseRequestJSON(reqType, msg.Body)
if err != nil {
LogError("Error parsing request JSON:", err)
return nil
} else {
LogDebug("processing req", req.Type)
// h.Request = req
ProcessEvent(handler, req)
}
return nil
}
}

4
go.mod
View File

@@ -10,8 +10,10 @@ require (
github.com/go-openapi/validate v0.24.0
github.com/opentracing/opentracing-go v1.2.0
github.com/rabbitmq/amqp091-go v1.10.0
github.com/redis/go-redis/v9 v9.11.0
github.com/tailscale/hujson v0.0.0-20250226034555-ec1d1c113d33
go.uber.org/mock v0.5.0
gopkg.in/yaml.v3 v3.0.1
)
require (
@@ -30,11 +32,9 @@ require (
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/redis/go-redis/v9 v9.11.0 // indirect
go.mongodb.org/mongo-driver v1.14.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/sync v0.7.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

4
go.sum
View File

@@ -1,5 +1,9 @@
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

View File

@@ -125,6 +125,16 @@ func FindAcceptableReviewInTimeline(user string, timeline []*models.TimelineComm
return nil
}
func FindOurLastReviewInTimeline(timeline []*models.TimelineComment) *models.TimelineComment {
for _, t := range timeline {
if t.Type == common.TimelineCommentType_Review && t.User.UserName == groupName && t.Created == t.Updated {
return t
}
}
return nil
}
func UnrequestReviews(gitea common.Gitea, org, repo string, id int64, users []string) {
if err := gitea.UnrequestReview(org, repo, id, users...); err != nil {
common.LogError("Can't remove reviewrs after a review:", err)
@@ -164,6 +174,22 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
return
}
if err := ProcessPR(pr); err == nil && !common.IsDryRun {
if err := gitea.SetNotificationRead(notification.ID); err != nil {
common.LogDebug(" Cannot set notification as read", err)
}
} else if err != nil && err != ReviewNotFinished {
common.LogError(err)
}
}
var ReviewNotFinished = fmt.Errorf("Review is not finished")
func ProcessPR(pr *models.PullRequest) error {
org := pr.Base.Repo.Owner.UserName
repo := pr.Base.Repo.Name
id := pr.Index
found := false
for _, reviewer := range pr.RequestedReviewers {
if reviewer != nil && reviewer.UserName == groupName {
@@ -173,42 +199,32 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
}
if !found {
common.LogInfo(" review is not requested for", groupName)
if !common.IsDryRun {
gitea.SetNotificationRead(notification.ID)
}
return
return nil
}
config := configs.GetPrjGitConfig(org, repo, pr.Base.Name)
if config == nil {
common.LogError("Cannot find config for:", fmt.Sprintf("%s/%s!%s", org, repo, pr.Base.Name))
return
return fmt.Errorf("Cannot find config for: %s", pr.URL)
}
if pr.State == "closed" {
// dismiss the review
common.LogInfo(" -- closed request, so nothing to review")
if !common.IsDryRun {
gitea.SetNotificationRead(notification.ID)
}
return
return nil
}
reviews, err := gitea.GetPullRequestReviews(org, repo, id)
if err != nil {
common.LogInfo(" ** No reviews associated with request:", subject.URL, "Error:", err)
return
return fmt.Errorf("Failed to fetch reviews for: %v: %w", pr.URL, err)
}
timeline, err := common.FetchTimelineSinceReviewRequestOrPush(gitea, groupName, pr.Head.Sha, org, repo, id)
if err != nil {
common.LogError(err)
return
return fmt.Errorf("Failed to fetch timeline to review. %w", err)
}
groupConfig, err := config.GetReviewGroup(groupName)
if err != nil {
common.LogError(err)
return
return fmt.Errorf("Failed to fetch review group. %w", err)
}
// submitter cannot be reviewer
@@ -220,27 +236,31 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
if review := FindAcceptableReviewInTimeline(reviewer, timeline, reviews); review != nil {
if ReviewAccepted(review.Body) {
if !common.IsDryRun {
gitea.AddReviewComment(pr, common.ReviewStateApproved, "Signed off by: "+reviewer)
UnrequestReviews(gitea, org, repo, id, requestReviewers)
if !common.IsDryRun {
if err := gitea.SetNotificationRead(notification.ID); err != nil {
common.LogDebug(" Cannot set notification as read", err)
text := reviewer + " approved a review on behalf of " + groupName
if review := FindOurLastReviewInTimeline(timeline); review == nil || review.Body != text {
_, err := gitea.AddReviewComment(pr, common.ReviewStateApproved, text)
if err != nil {
common.LogError(" -> failed to write approval comment", err)
}
UnrequestReviews(gitea, org, repo, id, requestReviewers)
}
}
common.LogInfo(" -> approved by", reviewer)
common.LogInfo(" review at", review.Created)
return
return nil
} else if ReviewRejected(review.Body) {
if !common.IsDryRun {
gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Changes requested. See review by: "+reviewer)
text := reviewer + " requested changes on behalf of " + groupName + ". See " + review.HTMLURL
if review := FindOurLastReviewInTimeline(timeline); review == nil || review.Body != text {
_, err := gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Changes requested. See review by: "+reviewer)
if err != nil {
common.LogError(" -> failed to write rejecting comment", err)
}
UnrequestReviews(gitea, org, repo, id, requestReviewers)
if err := gitea.SetNotificationRead(notification.ID); err != nil {
common.LogDebug(" Cannot set notification as read", err)
}
}
common.LogInfo(" -> declined by", reviewer)
return
return nil
}
}
}
@@ -270,15 +290,22 @@ func ProcessNotifications(notification *models.NotificationThread, gitea common.
}
if !found_help_comment && !common.IsDryRun {
helpComment := fmt.Sprintln("Review by", groupName, "represents a group of reviewers:", strings.Join(requestReviewers, ", "), ". To review as part of this group, create a comment with contents @"+groupName+": LGTM on a separate line to accept a review. To request changes, write @"+groupName+": followed by reason for rejection. Do not use reviews to review as a group. Editing a comment invalidates that comment.")
helpComment := fmt.Sprintln("Review by", groupName, "represents a group of reviewers:", strings.Join(requestReviewers, ", "), ".\n\n"+
"Do **not** use standard review interface to review on behalf of the group.\n"+
"To accept the review on behalf of the group, create the following comment: `@"+groupName+": approve`.\n"+
"To request changes on behalf of the group, create the following comment: `@"+groupName+": decline` followed with lines justifying the decision.\n"+
"Future edits of the comments are ignored, a new comment is required to change the review state.")
if slices.Contains(groupConfig.Reviewers, pr.User.UserName) {
helpComment = helpComment + "\n\n" + fmt.Sprintln("Submitter is member of this review group, hence they are excluded from being one of the reviewers here")
helpComment = helpComment + "\n\n" +
"Submitter is member of this review group, hence they are excluded from being one of the reviewers here"
}
gitea.AddComment(pr, helpComment)
}
return ReviewNotFinished
}
func PeriodReviewCheck(gitea common.Gitea) {
func PeriodReviewCheck() {
notifications, err := gitea.GetNotifications(common.GiteaNotificationType_Pull, nil)
if err != nil {
common.LogError(" Error fetching unread notifications: %w", err)
@@ -287,14 +314,15 @@ func PeriodReviewCheck(gitea common.Gitea) {
for _, notification := range notifications {
ProcessNotifications(notification, gitea)
}
}
}
}
var gitea common.Gitea
func main() {
giteaUrl := flag.String("gitea-url", "https://src.opensuse.org", "Gitea instance used for reviews")
rabbitMqHost := flag.String("rabbit-url", "amqps://rabbit.opensuse.org", "RabbitMQ instance where Gitea webhook notifications are sent")
interval := flag.Int64("interval", 5, "Notification polling interval in minutes (min 1 min)")
interval := flag.Int64("interval", 10, "Notification polling interval in minutes (min 1 min)")
configFile := flag.String("config", "", "PrjGit listing config file")
logging := flag.String("logging", "info", "Logging level: [none, error, info, debug]")
flag.BoolVar(&common.IsDryRun, "dry", false, "Dry run, no effect. For debugging")
@@ -331,7 +359,7 @@ func main() {
return
}
gitea := common.AllocateGiteaTransport(*giteaUrl)
gitea = common.AllocateGiteaTransport(*giteaUrl)
configs, err = common.ResolveWorkflowConfigs(gitea, configData)
if err != nil {
common.LogError("Cannot parse workflow configs:", err)
@@ -375,10 +403,13 @@ func main() {
config_modified: make(chan *common.AutogitConfig),
}
process_issue_pr := IssueCommentProcessor{}
configUpdates := &common.RabbitMQGiteaEventsProcessor{
Orgs: []string{},
Handlers: map[string]common.RequestProcessor{
common.RequestType_Push: &config_update,
common.RequestType_IssueComment: &process_issue_pr,
},
}
configUpdates.Connection().RabbitURL = u
@@ -415,7 +446,7 @@ func main() {
}
}
PeriodReviewCheck(gitea)
PeriodReviewCheck()
time.Sleep(time.Duration(*interval * int64(time.Minute)))
}
}

View File

@@ -7,6 +7,25 @@ import (
"src.opensuse.org/autogits/common"
)
type IssueCommentProcessor struct{}
func (s *IssueCommentProcessor) ProcessFunc(req *common.Request) error {
if req.Type != common.RequestType_IssueComment {
return fmt.Errorf("Unhandled, ignored request type: %s", req.Type)
}
data := req.Data.(*common.IssueCommentWebhookEvent)
org := data.Repository.Owner.Username
repo := data.Repository.Name
index := int64(data.Issue.Number)
pr, err := gitea.GetPullRequest(org, repo, index)
if err != nil {
return fmt.Errorf("Failed to fetch PullRequest from event: %s/%s!%d Error: %w", org, repo, index, err)
}
return ProcessPR(pr)
}
type ConfigUpdatePush struct {
config_modified chan *common.AutogitConfig
}

27
hujson/main.go Normal file
View File

@@ -0,0 +1,27 @@
package main
import (
"io"
"log"
"os"
"github.com/tailscale/hujson"
)
func main() {
data, err := io.ReadAll(os.Stdin)
if err != nil {
log.Println(err)
os.Exit(1)
}
json, err := hujson.Standardize(data)
if err != nil {
log.Println(err)
os.Exit(2)
}
_, err = os.Stdout.Write(json)
if err != nil {
log.Print(err)
os.Exit(3)
}
}

View File

@@ -263,7 +263,7 @@ func ProcessRepoBuildStatus(results, ref []*common.PackageBuildStatus) (status B
return BuildStatusSummarySuccess, SomeSuccess
}
func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingPrj, buildPrj string) (*common.ProjectMeta, error) {
func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingPrj, buildPrj string, stagingMasterPrj string) (*common.ProjectMeta, error) {
common.LogDebug("repo content fetching ...")
err := FetchPrGit(git, pr)
if err != nil {
@@ -289,7 +289,15 @@ func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullReque
}
}
meta, err := ObsClient.GetProjectMeta(buildPrj)
common.LogDebug("Trying first staging master project: ", stagingMasterPrj)
meta, err := ObsClient.GetProjectMeta(stagingMasterPrj)
if err == nil {
// success, so we use that staging master project as our build project
buildPrj = stagingMasterPrj
} else {
common.LogInfo("error fetching project meta for ", stagingMasterPrj, ". Fall Back to ", buildPrj)
meta, err = ObsClient.GetProjectMeta(buildPrj)
}
if err != nil {
common.LogError("error fetching project meta for", buildPrj, ". Err:", err)
return nil, err
@@ -314,6 +322,9 @@ func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullReque
urlPkg = append(urlPkg, "onlybuild="+url.QueryEscape(pkg))
}
meta.ScmSync = pr.Head.Repo.CloneURL + "?" + strings.Join(urlPkg, "&") + "#" + pr.Head.Sha
if len(meta.ScmSync) >= 65535 {
return nil, errors.New("Reached max amount of package changes per request")
}
meta.Title = fmt.Sprintf("PR#%d to %s", pr.Index, pr.Base.Name)
// QE wants it published ... also we should not hardcode it here, since
// it is configurable via the :PullRequest project
@@ -414,7 +425,8 @@ func StartOrUpdateBuild(config *common.StagingConfig, git common.Git, gitea comm
var state RequestModification = RequestModificationSourceChanged
if meta == nil {
// new build
meta, err = GenerateObsPrjMeta(git, gitea, pr, obsPrProject, config.ObsProject)
common.LogDebug(" Staging master:", config.StagingProject)
meta, err = GenerateObsPrjMeta(git, gitea, pr, obsPrProject, config.ObsProject, config.StagingProject)
if err != nil {
return RequestModificationNoChange, err
}
@@ -428,6 +440,8 @@ func StartOrUpdateBuild(config *common.StagingConfig, git common.Git, gitea comm
} else {
err = ObsClient.SetProjectMeta(meta)
if err != nil {
x, _ := xml.MarshalIndent(meta, "", " ")
common.LogDebug(" meta:", string(x))
common.LogError("cannot create meta project:", err)
return RequestModificationNoChange, err
}
@@ -584,7 +598,7 @@ func CleanupPullNotification(gitea common.Gitea, thread *models.NotificationThre
}
if !pr.HasMerged && time.Since(time.Time(pr.Closed)) < time.Duration(config.CleanupDelay)*time.Hour {
common.LogInfo("Cooldown period for cleanup of", thread.URL)
common.LogInfo("Cooldown period for cleanup of", thread.Subject.HTMLURL)
return false
}
@@ -622,6 +636,14 @@ func CleanupPullNotification(gitea common.Gitea, thread *models.NotificationThre
return false // cleaned up now, but the cleanup was not aleady done
}
func SetStatus(gitea common.Gitea, org, repo, hash string, status *models.CommitStatus) error {
_, err := gitea.SetCommitStatus(org, repo, hash, status)
if err != nil {
common.LogError(err)
}
return err
}
func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, error) {
dir, err := os.MkdirTemp(os.TempDir(), BotName)
common.PanicOnError(err)
@@ -805,7 +827,12 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
if !rebuild_all {
common.LogInfo("No package changes detected. Ignoring")
if !IsDryRun {
_, err = gitea.AddReviewComment(pr, common.ReviewStateComment, "No package changes. Not rebuilding project by default")
_, err := gitea.AddReviewComment(pr, common.ReviewStateApproved, "No package changes, not rebuilding project by default, accepting change")
if err != nil {
common.LogError(err)
} else {
return true, nil
}
}
return true, err
}
@@ -821,6 +848,22 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
TargetURL: ObsWebHost + "/project/show/" + stagingProject,
}
if err != nil {
msg := "Unable to setup stage project " + stagingConfig.ObsProject
status.Status = common.CommitStatus_Fail
common.LogError(msg)
if !IsDryRun {
SetStatus(gitea, org, repo, pr.Head.Sha, status)
_, err = gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, msg)
if err != nil {
common.LogError(err)
} else {
return true, nil
}
}
return false, nil
}
msg := "Changed source updated for build"
if change == RequestModificationProjectCreated {
msg = "Build is started in " + ObsWebHost + "/project/show/" +
@@ -829,8 +872,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
if len(stagingConfig.QA) > 0 {
msg = msg + "\nAdditional QA builds: \n"
}
gitea.SetCommitStatus(pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Head.Sha, status)
SetStatus(gitea, org, repo, pr.Head.Sha, status)
for _, setup := range stagingConfig.QA {
CreateQASubProject(stagingConfig, git, gitea, pr,
stagingProject,
@@ -854,32 +896,34 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
}
buildStatus := ProcessBuildStatus(stagingResult, baseResult)
done := false
switch buildStatus {
case BuildStatusSummarySuccess:
status.Status = common.CommitStatus_Success
done = true
if !IsDryRun {
_, err := gitea.AddReviewComment(pr, common.ReviewStateApproved, "Build successful")
if err != nil {
common.LogError(err)
} else {
return true, nil
}
}
case BuildStatusSummaryFailed:
status.Status = common.CommitStatus_Fail
done = true
if !IsDryRun {
_, err := gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Build failed")
if err != nil {
common.LogError(err)
} else {
return true, nil
}
}
}
common.LogInfo("Build status:", buildStatus)
gitea.SetCommitStatus(pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Head.Sha, status)
// waiting for build results -- nothing to do
if !IsDryRun {
if err = SetStatus(gitea, org, repo, pr.Head.Sha, status); err != nil {
return false, err
}
}
return done, nil
} else if err == NonActionableReviewError || err == NoReviewsFoundError {
return true, nil

View File

@@ -20,6 +20,7 @@ package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
@@ -191,15 +192,24 @@ func main() {
return
}
var rescanRepoError error
go func() {
for {
if err := RescanRepositories(); err != nil {
if rescanRepoError = RescanRepositories(); rescanRepoError != nil {
common.LogError("Failed to rescan repositories.", err)
}
time.Sleep(time.Minute * 5)
}
}()
http.HandleFunc("GET /", func(res http.ResponseWriter, req *http.Request) {
if rescanRepoError != nil {
res.WriteHeader(500)
return
}
res.WriteHeader(404)
res.Write([]byte("404 page not found\n"))
})
http.HandleFunc("GET /status/{Project}", func(res http.ResponseWriter, req *http.Request) {
obsPrj := req.PathValue("Project")
common.LogInfo(" request: GET /status/" + obsPrj)
@@ -259,6 +269,33 @@ func main() {
res.Write(BuildStatusSvg(nil, &common.PackageBuildStatus{Package: pkg, Code: "unknown"}))
})
http.HandleFunc("GET /search", func(res http.ResponseWriter, req *http.Request) {
common.LogInfo("GET /serach?" + req.URL.RawQuery)
queries := req.URL.Query()
if !queries.Has("q") {
res.WriteHeader(400)
return
}
names := queries["q"]
if len(names) != 1 {
res.WriteHeader(400)
return
}
packages := FindPackages(names[0])
data, err := json.MarshalIndent(packages, "", " ")
if err != nil {
res.WriteHeader(500)
common.LogError("Error in marshalling data.", err)
return
}
res.Write(data)
res.Header().Add("content-type", "application/json")
res.WriteHeader(200)
})
http.HandleFunc("GET /buildlog/{Project}/{Package}/{Repository}/{Arch}", func(res http.ResponseWriter, req *http.Request) {
prj := req.PathValue("Project")
pkg := req.PathValue("Package")

View File

@@ -29,13 +29,15 @@ func UpdateResults(r *common.BuildResult) {
RepoStatusLock.Lock()
defer RepoStatusLock.Unlock()
updateResultsWithoutLocking(r)
}
func updateResultsWithoutLocking(r *common.BuildResult) {
key := "result." + r.Project + "/" + r.Repository + "/" + r.Arch
common.LogDebug(" + Updating", key)
data, err := redisClient.HGetAll(context.Background(), key).Result()
if err != nil {
common.LogError("Failed fetching build results for", key, err)
}
common.LogDebug(" + Update size", len(data))
reset_time := time.Date(1000, 1, 1, 1, 1, 1, 1, time.Local)
for _, pkg := range r.Status {
@@ -110,6 +112,27 @@ func FindRepoResults(project, repo string) []*common.BuildResult {
return ret
}
func FindPackages(pkg string) []string {
RepoStatusLock.RLock()
defer RepoStatusLock.RUnlock()
data := make([]string, 0, 100)
for _, repo := range RepoStatus {
for _, status := range repo.Status {
if pkg == status.Package {
entry := repo.Project + "/" + pkg
if idx, found := slices.BinarySearch(data, entry); !found {
data = slices.Insert(data, idx, entry)
if len(data) >= 100 {
return data
}
}
}
}
}
return data
}
func FindAndUpdateProjectResults(project string) []*common.BuildResult {
res := FindProjectResults(project)
wg := &sync.WaitGroup{}
@@ -161,6 +184,8 @@ func RescanRepositories() error {
RepoStatusLock.Unlock()
var count int
projectsLooked := make([]string, 0, 10000)
for {
var data []string
data, cursor, err = redisClient.ScanType(ctx, cursor, "", 1000, "hash").Result()
@@ -169,6 +194,7 @@ func RescanRepositories() error {
return err
}
wg := &sync.WaitGroup{}
RepoStatusLock.Lock()
for _, repo := range data {
r := strings.Split(repo, "/")
@@ -180,14 +206,28 @@ func RescanRepositories() error {
Repository: r[1],
Arch: r[2],
}
if pos, found := slices.BinarySearchFunc(RepoStatus, d, common.BuildResultComp); found {
var pos int
var found bool
if pos, found = slices.BinarySearchFunc(RepoStatus, d, common.BuildResultComp); found {
RepoStatus[pos].Dirty = true
} else {
d.Dirty = true
RepoStatus = slices.Insert(RepoStatus, pos, d)
count++
}
// fetch all keys, one per non-maintenance/non-home: projects, for package search
if idx, found := slices.BinarySearch(projectsLooked, d.Project); !found && !strings.Contains(d.Project, ":Maintenance:") && (len(d.Project) < 5 || d.Project[0:5] != "home:") {
projectsLooked = slices.Insert(projectsLooked, idx, d.Project)
wg.Add(1)
go func(r *common.BuildResult) {
updateResultsWithoutLocking(r)
wg.Done()
}(RepoStatus[pos])
}
}
wg.Wait()
RepoStatusLock.Unlock()
if cursor == 0 {

View File

@@ -0,0 +1,16 @@
[Unit]
Description=Staging bot for project git PRs in OBS
After=network-online.target
[Service]
Type=exec
ExecStart=/usr/bin/obs-staging-bot
EnvironmentFile=-/etc/sysconfig/obs-staging-bot.env
DynamicUser=yes
NoNewPrivileges=yes
ProtectSystem=strict
[Install]
WantedBy=multi-user.target

4
test/main.go Normal file
View File

@@ -0,0 +1,4 @@
package main
// exists only to import this for go.modules
import "go.uber.org/mock/mockgen/model"

1
vendor/modules.txt vendored
View File

@@ -132,6 +132,7 @@ go.opentelemetry.io/otel/trace/embedded
# go.uber.org/mock v0.5.0
## explicit; go 1.22
go.uber.org/mock/gomock
go.uber.org/mock/mockgen/model
# golang.org/x/sync v0.7.0
## explicit; go 1.18
golang.org/x/sync/errgroup

View File

@@ -33,6 +33,7 @@ JSON
* _ManualMergeOnly_: (true, false) only merge if "merge ok" comment/review by package or project maintainers or reviewers
* _ManualMergeProject_: (true, false) only merge if "merge ok" by project maintainers or reviewers
* _ReviewRequired_: (true, false) ignores that submitter is a maintainer and require a review from other maintainer IFF available
* _NoProjectGitPR_: (true, false) do not create PrjGit PRs, but still process reviews, etc.
NOTE: `-rm`, `-removed`, `-deleted` are all removed suffixes used to indicate current branch is a placeholder for previously existing package. These branches will be ignored by the bot, and if default, the package will be removed and will not be added to the project.
example:

View File

@@ -3,6 +3,7 @@ package main
//go:generate mockgen -source=pr_processor.go -destination=mock/pr_processor.go -typed
import (
"encoding/json"
"fmt"
"path"
"runtime/debug"
@@ -27,6 +28,10 @@ func PrjGitDescription(prset *common.PRSet) (title string, desc string) {
if prset.IsPrjGitPR(pr.PR) {
continue
}
if pr.PR.State != "open" {
// remove PRs that are not open from description
continue
}
org, repo, idx := pr.PRComponents()
title_refs = append(title_refs, repo)
@@ -69,7 +74,7 @@ func updateSubmoduleInPR(submodule, headSha string, git common.Git) {
// as long as we can update to newer one later, we are still OK
git.GitExec(common.DefaultGitPrj, "submodule", "update", "--init", "--checkout", "--depth", "1", submodule)
common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submodule), "fetch", "--depth", "1", "origin", headSha))
common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submodule), "checkout", headSha))
common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submodule), "checkout", "-f", headSha))
}
type PRProcessor struct {
@@ -99,7 +104,10 @@ func AllocatePRProcessor(req *models.PullRequest, configs common.AutogitConfigs)
return nil, fmt.Errorf("Cannot find config for PR")
}
common.LogDebug("found config", config)
if common.GetLoggingLevel() >= common.LogLevelDebug {
cjson, _ := json.Marshal(config)
common.LogDebug("found config:", string(cjson))
}
if config == nil {
common.LogError("Cannot find config for branch '%s'", req.Base.Ref)
return nil, fmt.Errorf("Cannot find config for branch '%s'", req.Base.Ref)
@@ -158,10 +166,10 @@ func (pr *PRProcessor) SetSubmodulesToMatchPRSet(prset *common.PRSet) error {
submodule_found = true
if id != prHead {
ref := fmt.Sprintf(common.PrPattern, org, repo, idx)
commitMsg := fmt.Sprintln("auto-created for", repo, "\n\nThis commit was autocreated by", GitAuthor, "referencing\n", ref)
commitMsg := fmt.Sprintln("auto-created for", repo, "\n\nThis commit was autocreated by", GitAuthor, "\n\nreferencing PRs:\n", ref)
if revert {
commitMsg = fmt.Sprintln("auto-created for", repo, "\n\nThis commit was autocreated by", GitAuthor, "removing\n", ref)
commitMsg = fmt.Sprintln("auto-created for", repo, "\n\nThis commit was autocreated by", GitAuthor, "\n\nremoving PRs:\n", ref)
}
updateSubmoduleInPR(submodulePath, prHead, git)
@@ -170,6 +178,8 @@ func (pr *PRProcessor) SetSubmodulesToMatchPRSet(prset *common.PRSet) error {
common.LogDebug("submodule", repo, " hash:", id, " -> ", prHead)
common.PanicOnError(err)
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "commit", "-a", "-m", commitMsg))
pr.PR.Head.Sha = id // update the prset
}
submodule_found = true
break
@@ -209,7 +219,7 @@ func (pr *PRProcessor) CreatePRjGitPR(prjGitPRbranch string, prset *common.PRSet
return err
}
if !common.IsDryRun {
if !common.IsDryRun && !pr.config.NoProjectGitPR {
if headCommit != newHeadCommit {
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "push", RemoteName, "+HEAD:"+prjGitPRbranch))
}
@@ -247,7 +257,7 @@ func (pr *PRProcessor) RebaseAndSkipSubmoduleCommits(prset *common.PRSet, branch
for _, s := range statuses {
if s.SubmoduleChanges != "S..." {
git.GitExecOrPanic(common.DefaultGitPrj, "rebase", "--abort")
return fmt.Errorf("Unexpected conflict in rebase. %s", s)
return fmt.Errorf("Unexpected conflict in rebase. %v", s)
}
}
conflict = git.GitExec(common.DefaultGitPrj, "rebase", "--skip")
@@ -265,6 +275,12 @@ func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
}
git := pr.git
if len(prset.PRs) == 1 {
git.GitExecOrPanic(common.DefaultGitPrj, "fetch", PrjGitPR.RemoteName, PrjGitPR.PR.Head.Sha)
common.LogDebug("Only project git in PR. Nothing to update.")
return nil
}
PrjGit := PrjGitPR.PR.Base.Repo
prjGitPRbranch := PrjGitPR.PR.Head.Name
if strings.Contains(prjGitPRbranch, "/") {
@@ -300,6 +316,17 @@ func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
return err
}
PrjGitTitle, PrjGitBody := PrjGitDescription(prset)
if PrjGitPR.PR.User.UserName == CurrentUser.UserName {
if PrjGitPR.PR.Title != PrjGitTitle || PrjGitPR.PR.Body != PrjGitBody {
common.LogDebug("New title:", PrjGitTitle)
common.LogDebug(PrjGitBody)
}
} else {
// TODO: find our first comment in timeline
}
if !common.IsDryRun {
if headCommit != newHeadCommit {
params := []string{"push", PrjGitPR.RemoteName, "+HEAD:" + prjGitPRbranch}
@@ -310,7 +337,6 @@ func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
}
// update PR
PrjGitTitle, PrjGitBody := PrjGitDescription(prset)
if PrjGitPR.PR.Body != PrjGitBody || PrjGitPR.PR.Title != PrjGitTitle {
Gitea.UpdatePullRequest(PrjGit.Owner.UserName, PrjGit.Name, PrjGitPR.PR.Index, &models.EditPullRequestOption{
RemoveDeadline: true,
@@ -319,6 +345,9 @@ func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
})
}
}
// remove closed PRs from prset
prset.RemoveClosedPRs()
return nil
}
@@ -344,7 +373,7 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
prjGitPRbranch := prGitBranchNameForPR(prRepo, prNo)
prjGitPR, err := prset.GetPrjGitPR()
if err == common.PRSet_PrjGitMissing {
common.LogDebug("Missing PrjGit. Need to create one...")
common.LogDebug("Missing PrjGit. Need to create one under branch", prjGitPRbranch)
if err = pr.CreatePRjGitPR(prjGitPRbranch, prset); err != nil {
return err
@@ -421,7 +450,11 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
if prjGitPR == nil {
prjGitPR, err = prset.GetPrjGitPR()
if err != nil {
if err == common.PRSet_PrjGitMissing && config.NoProjectGitPR {
// we could be waiting for other tooling to create the
// project git PR. In meantime, we can assign some
// reviewers here.
} else if err != nil {
common.LogError("Error fetching PrjGitPR:", err)
return nil
}
@@ -433,11 +466,12 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
// reset anything that changed that is not part of the prset
// package removals/additions are *not* counted here
org, repo, branch := config.GetPrjGit()
if pr, err := prset.GetPrjGitPR(); err == nil {
// TODO: this is broken...
if pr, err := prset.GetPrjGitPR(); err == nil && false {
common.LogDebug("Submodule parse begin")
orig_subs, err := git.GitSubmoduleList(common.DefaultGitPrj, pr.PR.MergeBase)
common.PanicOnError(err)
new_subs, err := git.GitSubmoduleList(common.DefaultGitPrj, pr.PR.Head.Sha)
new_subs, err := git.GitSubmoduleList(common.DefaultGitPrj, "HEAD")
common.PanicOnError(err)
common.LogDebug("Submodule parse done")
@@ -445,6 +479,7 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
updateSubmoduleInPR(submodule, sha, git)
}
common.LogDebug("Checking we only change linked commits")
for path, commit := range new_subs {
if old, ok := orig_subs[path]; ok && old != commit {
found := false
@@ -452,6 +487,8 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
if pr.PR.Base.Repo.Name == path && commit == pr.PR.Head.Sha {
found = true
break
} else if pr.PR.Base.Repo.Name == path {
common.LogError(path, "-- commits not match", commit, pr.PR.Head.Sha)
}
}
if !found {
@@ -461,24 +498,29 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
}
stats, err := git.GitStatus(common.DefaultGitPrj)
common.LogDebug("Check Done", len(stats), "changes")
common.PanicOnError(err)
if len(stats) > 0 {
git.GitExecOrPanic(common.DefaultGitPrj, "commit", "-a", "-m", "Sync submodule updates with PR-set")
git.GitExecOrPanic(common.DefaultGitPrj, "submodule", "deinit", "--all", "--force")
git.GitExecQuietOrPanic(common.DefaultGitPrj, "submodule", "deinit", "--all", "--force")
if !common.IsDryRun {
git.GitExecOrPanic(common.DefaultGitPrj, "push")
}
}
}
if prjGitPR != nil {
common.LogDebug(" num of reviewers:", len(prjGitPR.PR.RequestedReviewers))
} else {
common.LogInfo("* No prjgit")
}
maintainers, err := common.FetchProjectMaintainershipData(Gitea, org, repo, branch)
if err != nil {
return err
}
// handle case where PrjGit PR is only one left and there are no changes, then we can just close the PR
if len(prset.PRs) == 1 && prset.PRs[0] == prjGitPR && prjGitPR.PR.User.UserName == prset.BotUser {
if len(prset.PRs) == 1 && prjGitPR != nil && prset.PRs[0] == prjGitPR && prjGitPR.PR.User.UserName == prset.BotUser {
common.LogDebug(" --> checking if superflous PR")
diff, err := git.GitDiff(common.DefaultGitPrj, prjGitPR.PR.MergeBase, prjGitPR.PR.Head.Sha)
if err != nil {
@@ -500,7 +542,6 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
common.LogDebug(" --> NOT superflous PR")
}
prset.AssignReviewers(Gitea, maintainers)
for _, pr := range prset.PRs {
if err := verifyRepositoryConfiguration(pr.PR.Base.Repo); err != nil {
common.LogError("Cannot set manual merge... aborting processing")
@@ -515,8 +556,9 @@ func (pr *PRProcessor) Process(req *models.PullRequest) error {
if err = prset.Merge(Gitea, git); err != nil {
common.LogError("merge error:", err)
}
} else {
prset.AssignReviewers(Gitea, maintainers)
}
return err
}