Compare commits
1 Commits
| Author | SHA256 | Date | |
|---|---|---|---|
| 3d8671a7fe |
@@ -1,34 +0,0 @@
|
|||||||
name: go-generate-check
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ['main']
|
|
||||||
paths:
|
|
||||||
- '**.go'
|
|
||||||
- '**.mod'
|
|
||||||
- '**.sum'
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- '**.go'
|
|
||||||
- '**.mod'
|
|
||||||
- '**.sum'
|
|
||||||
workflow_dispatch:
|
|
||||||
jobs:
|
|
||||||
go-generate-check:
|
|
||||||
name: go-generate-check
|
|
||||||
container:
|
|
||||||
image: registry.opensuse.org/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
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
name: go-generate-push
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
jobs:
|
|
||||||
go-generate-push:
|
|
||||||
name: go-generate-push
|
|
||||||
container:
|
|
||||||
image: registry.opensuse.org/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
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
name: go-vendor-check
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ['main']
|
|
||||||
paths:
|
|
||||||
- '**.mod'
|
|
||||||
- '**.sum'
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- '**.mod'
|
|
||||||
- '**.sum'
|
|
||||||
workflow_dispatch:
|
|
||||||
jobs:
|
|
||||||
go-generate-check:
|
|
||||||
name: go-vendor-check
|
|
||||||
container:
|
|
||||||
image: registry.opensuse.org/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
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
name: go-generate-push
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
jobs:
|
|
||||||
go-generate-push:
|
|
||||||
name: go-generate-push
|
|
||||||
container:
|
|
||||||
image: registry.opensuse.org/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
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,2 +1,5 @@
|
|||||||
|
mock
|
||||||
|
node_modules
|
||||||
|
*.obscpio
|
||||||
|
autogits-tmp.tar.zst
|
||||||
*.osc
|
*.osc
|
||||||
*.conf
|
|
||||||
|
|||||||
19
README.md
19
README.md
@@ -5,15 +5,11 @@ The bots that drive Git Workflow for package management
|
|||||||
|
|
||||||
* devel-importer -- helper to import an OBS devel project into a Gitea organization
|
* devel-importer -- helper to import an OBS devel project into a Gitea organization
|
||||||
* gitea-events-rabbitmq-publisher -- takes all events from a Gitea organization (webhook) and publishes it on a RabbitMQ instance
|
* gitea-events-rabbitmq-publisher -- takes all events from a Gitea organization (webhook) and publishes it on a RabbitMQ instance
|
||||||
* gitea-status-proxy -- allows bots without code owner permission to set Gitea's commit status
|
|
||||||
* group-review -- group review proxy
|
|
||||||
* hujson -- translates JWCC (json with commas and comments) to Standard JSON
|
|
||||||
* obs-forward-bot -- forwards PR as OBS sr (TODO)
|
|
||||||
* obs-staging-bot -- build bot for a PR
|
* obs-staging-bot -- build bot for a PR
|
||||||
* obs-status-service -- report build status of an OBS project as an SVG
|
* obs-status-service -- report build status of an OBS project as an SVG
|
||||||
* workflow-pr -- keeps PR to _ObsPrj consistent with a PR to a package update
|
* workflow-pr -- keeps PR to _ObsPrj consistent with a PR to a package update
|
||||||
* workflow-direct -- update _ObsPrj based on direct pushes and repo creations/removals from organization
|
* workflow-direct -- update _ObsPrj based on direct pushes and repo creations/removals from organization
|
||||||
* staging-utils -- review tooling for PR (TODO)
|
* staging-utils -- review tooling for PR
|
||||||
- list PR
|
- list PR
|
||||||
- merge PR
|
- merge PR
|
||||||
- split PR
|
- split PR
|
||||||
@@ -23,18 +19,7 @@ The bots that drive Git Workflow for package management
|
|||||||
Bugs
|
Bugs
|
||||||
----
|
----
|
||||||
|
|
||||||
Report bugs to issue tracker at https://src.opensuse.org/git-workflow/autogits
|
Report bugs to issue tracker at https://src.opensuse.org/adamm/autogits
|
||||||
|
|
||||||
|
|
||||||
Build Status
|
|
||||||
------------
|
|
||||||
|
|
||||||
Devel project build status (`main` branch):
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
`staging` branch build status:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
15
_service
Normal file
15
_service
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
239
autogits.spec
239
autogits.spec
@@ -17,14 +17,15 @@
|
|||||||
|
|
||||||
|
|
||||||
Name: autogits
|
Name: autogits
|
||||||
Version: 1
|
Version: 0
|
||||||
Release: 0
|
Release: 0
|
||||||
Summary: GitWorkflow utilities
|
Summary: GitWorkflow utilities
|
||||||
License: GPL-2.0-or-later
|
License: GPL-2.0-or-later
|
||||||
URL: https://src.opensuse.org/adamm/autogits
|
URL: https://src.opensuse.org/adamm/autogits
|
||||||
BuildRequires: git
|
Source1: vendor.tar.zst
|
||||||
|
BuildRequires: golang-packaging
|
||||||
BuildRequires: systemd-rpm-macros
|
BuildRequires: systemd-rpm-macros
|
||||||
BuildRequires: go
|
BuildRequires: zstd
|
||||||
%{?systemd_ordering}
|
%{?systemd_ordering}
|
||||||
|
|
||||||
%description
|
%description
|
||||||
@@ -32,268 +33,160 @@ Git Workflow tooling and utilities enabling automated handing of OBS projects
|
|||||||
as git repositories
|
as git repositories
|
||||||
|
|
||||||
|
|
||||||
%package devel-importer
|
%package -n gitea-events-rabbitmq-publisher
|
||||||
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 doc
|
|
||||||
Summary: Common documentation files
|
|
||||||
BuildArch: noarch
|
|
||||||
|
|
||||||
%description -n autogits-doc
|
|
||||||
Common documentation files
|
|
||||||
|
|
||||||
|
|
||||||
%package gitea-events-rabbitmq-publisher
|
|
||||||
Summary: Publishes Gitea webhook data via RabbitMQ
|
Summary: Publishes Gitea webhook data via RabbitMQ
|
||||||
|
|
||||||
%description gitea-events-rabbitmq-publisher
|
%description -n gitea-events-rabbitmq-publisher
|
||||||
Listens on an HTTP socket and publishes Gitea events on a RabbitMQ instance
|
Listens on an HTTP socket and publishes Gitea events on a RabbitMQ instance
|
||||||
with a topic
|
with a topic
|
||||||
<scope>.src.$organization.$webhook_type.[$webhook_action_type]
|
<scope>.src.$organization.$webhook_type.[$webhook_action_type]
|
||||||
|
|
||||||
|
|
||||||
%package gitea-status-proxy
|
%package -n doc
|
||||||
Summary: Proxy for setting commit status in Gitea
|
Summary: Common documentation files
|
||||||
|
|
||||||
%description gitea-status-proxy
|
%description -n doc
|
||||||
Setting commit status requires code write access token. This proxy
|
Common documentation files
|
||||||
is middleware that delegates status setting without access to other APIs
|
|
||||||
|
|
||||||
%package group-review
|
|
||||||
|
%package -n devel-importer
|
||||||
|
Summary: Imports devel projects from obs to git
|
||||||
|
|
||||||
|
%description -n devel-importer
|
||||||
|
Command-line tool to import devel projects from obs to git
|
||||||
|
|
||||||
|
|
||||||
|
%package -n group-review
|
||||||
Summary: Reviews of groups defined in ProjectGit
|
Summary: Reviews of groups defined in ProjectGit
|
||||||
|
|
||||||
%description group-review
|
%description -n group-review
|
||||||
Is used to handle reviews associated with groups defined in the
|
Is used to handle reviews associated with groups defined in the
|
||||||
ProjectGit.
|
ProjectGit.
|
||||||
|
|
||||||
|
|
||||||
%package obs-forward-bot
|
%package -n obs-staging-bot
|
||||||
Summary: obs-forward-bot
|
|
||||||
|
|
||||||
%description obs-forward-bot
|
|
||||||
|
|
||||||
|
|
||||||
%package obs-staging-bot
|
|
||||||
Summary: Build a PR against a ProjectGit, if review is requested
|
Summary: Build a PR against a ProjectGit, if review is requested
|
||||||
|
|
||||||
%description obs-staging-bot
|
%description -n obs-staging-bot
|
||||||
Build a PR against a ProjectGit, if review is requested.
|
Build a PR against a ProjectGit, if review is requested.
|
||||||
|
|
||||||
|
|
||||||
%package obs-status-service
|
%package -n obs-status-service
|
||||||
Summary: Reports build status of OBS service as an easily to produce SVG
|
Summary: Reports build status of OBS service as an easily to produce SVG
|
||||||
|
|
||||||
%description obs-status-service
|
%description -n obs-status-service
|
||||||
Reports build status of OBS service as an easily to produce SVG
|
Reports build status of OBS service as an easily to produce SVG
|
||||||
|
|
||||||
|
|
||||||
%package utils
|
%package -n workflow-direct
|
||||||
Summary: HuJSON to JSON parser
|
|
||||||
Provides: hujson
|
|
||||||
Provides: /usr/bin/hujson
|
|
||||||
|
|
||||||
%description utils
|
|
||||||
HuJSON to JSON parser, using stdin -> stdout pipe
|
|
||||||
|
|
||||||
|
|
||||||
%package workflow-direct
|
|
||||||
Summary: Keep ProjectGit in sync for a devel project
|
Summary: Keep ProjectGit in sync for a devel project
|
||||||
Requires: openssh-clients
|
|
||||||
Requires: git-core
|
|
||||||
|
|
||||||
%description workflow-direct
|
%description -n workflow-direct
|
||||||
Keep ProjectGit in sync with packages in the organization of a devel project
|
Keep ProjectGit in sync with packages in the organization of a devel project
|
||||||
|
|
||||||
|
|
||||||
%package workflow-pr
|
%package -n workflow-pr
|
||||||
Summary: Keeps ProjectGit PR in-sync with a PackageGit PR
|
Summary: Keeps ProjectGit PR in-sync with a PackageGit PR
|
||||||
Requires: openssh-clients
|
|
||||||
Requires: git-core
|
|
||||||
|
|
||||||
%description workflow-pr
|
%description -n workflow-pr
|
||||||
Keeps ProjectGit PR in-sync with a PackageGit PR
|
Keeps ProjectGit PR in-sync with a PackageGit PR
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
%prep
|
%prep
|
||||||
cp -r /home/abuild/rpmbuild/SOURCES/* ./
|
cp -r /home/abuild/rpmbuild/SOURCES/* ./
|
||||||
|
tar x --zstd -f %{SOURCE1}
|
||||||
|
|
||||||
%build
|
%build
|
||||||
go build \
|
|
||||||
-C devel-importer \
|
|
||||||
-buildmode=pie
|
|
||||||
go build \
|
|
||||||
-C utils/hujson \
|
|
||||||
-buildmode=pie
|
|
||||||
go build \
|
go build \
|
||||||
-C gitea-events-rabbitmq-publisher \
|
-C gitea-events-rabbitmq-publisher \
|
||||||
|
-mod=vendor \
|
||||||
-buildmode=pie
|
-buildmode=pie
|
||||||
go build \
|
go build \
|
||||||
-C gitea_status_proxy \
|
-C devel-importer \
|
||||||
|
-mod=vendor \
|
||||||
-buildmode=pie
|
-buildmode=pie
|
||||||
go build \
|
go build \
|
||||||
-C group-review \
|
-C group-review \
|
||||||
-buildmode=pie
|
-mod=vendor \
|
||||||
go build \
|
|
||||||
-C obs-forward-bot \
|
|
||||||
-buildmode=pie
|
-buildmode=pie
|
||||||
go build \
|
go build \
|
||||||
-C obs-staging-bot \
|
-C obs-staging-bot \
|
||||||
|
-mod=vendor \
|
||||||
-buildmode=pie
|
-buildmode=pie
|
||||||
go build \
|
go build \
|
||||||
-C obs-status-service \
|
-C obs-status-service \
|
||||||
|
-mod=vendor \
|
||||||
-buildmode=pie
|
-buildmode=pie
|
||||||
go build \
|
#go build \
|
||||||
-C workflow-direct \
|
# -C workflow-direct \
|
||||||
-buildmode=pie
|
# -mod=vendor \
|
||||||
go build \
|
# -buildmode=pie
|
||||||
-C workflow-pr \
|
#go build \
|
||||||
-buildmode=pie
|
# -C workflow-pr \
|
||||||
|
# -mod=vendor \
|
||||||
%check
|
# -buildmode=pie
|
||||||
go test -C common -v
|
|
||||||
go test -C group-review -v
|
|
||||||
go test -C obs-staging-bot -v
|
|
||||||
go test -C obs-status-service -v
|
|
||||||
go test -C workflow-direct -v
|
|
||||||
# TODO build fails
|
|
||||||
#go test -C workflow-pr -v
|
|
||||||
|
|
||||||
%install
|
%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 -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 -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 devel-importer/devel-importer %{buildroot}%{_bindir}/devel-importer
|
||||||
install -D -m0755 group-review/group-review %{buildroot}%{_bindir}/group-review
|
install -D -m0755 group-review/group-review %{buildroot}%{_bindir}/group-review
|
||||||
install -D -m0644 systemd/group-review@.service %{buildroot}%{_unitdir}/group-review@.service
|
|
||||||
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 -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 obs-status-service/obs-status-service %{buildroot}%{_bindir}/obs-status-service
|
||||||
install -D -m0644 systemd/obs-status-service.service %{buildroot}%{_unitdir}/obs-status-service.service
|
#install -D -m0755 workflow-direct/workflow-direct %{buildroot}%{_bindir}/workflow-direct
|
||||||
install -D -m0755 workflow-direct/workflow-direct %{buildroot}%{_bindir}/workflow-direct
|
#install -D -m0755 workflow-pr/workflow-pr %{buildroot}%{_bindir}/workflow-pr
|
||||||
install -D -m0644 systemd/workflow-direct@.service %{buildroot}%{_unitdir}/workflow-direct@.service
|
|
||||||
install -D -m0755 workflow-pr/workflow-pr %{buildroot}%{_bindir}/workflow-pr
|
|
||||||
install -D -m0755 utils/hujson/hujson %{buildroot}%{_bindir}/hujson
|
|
||||||
|
|
||||||
%pre gitea-events-rabbitmq-publisher
|
%pre -n gitea-events-rabbitmq-publisher
|
||||||
%service_add_pre gitea-events-rabbitmq-publisher.service
|
%service_add_pre gitea-events-rabbitmq-publisher.service
|
||||||
|
|
||||||
%post gitea-events-rabbitmq-publisher
|
%post -n gitea-events-rabbitmq-publisher
|
||||||
%service_add_post gitea-events-rabbitmq-publisher.service
|
%service_add_post gitea-events-rabbitmq-publisher.service
|
||||||
|
|
||||||
%preun gitea-events-rabbitmq-publisher
|
%preun -n gitea-events-rabbitmq-publisher
|
||||||
%service_del_preun gitea-events-rabbitmq-publisher.service
|
%service_del_preun gitea-events-rabbitmq-publisher.service
|
||||||
|
|
||||||
%postun gitea-events-rabbitmq-publisher
|
%postun -n gitea-events-rabbitmq-publisher
|
||||||
%service_del_postun gitea-events-rabbitmq-publisher.service
|
%service_del_postun gitea-events-rabbitmq-publisher.service
|
||||||
|
|
||||||
%pre group-review
|
%files -n gitea-events-rabbitmq-publisher
|
||||||
%service_add_pre group-review@.service
|
|
||||||
|
|
||||||
%post group-review
|
|
||||||
%service_add_post group-review@.service
|
|
||||||
|
|
||||||
%preun group-review
|
|
||||||
%service_del_preun group-review@.service
|
|
||||||
|
|
||||||
%postun group-review
|
|
||||||
%service_del_postun group-review@.service
|
|
||||||
|
|
||||||
%pre obs-staging-bot
|
|
||||||
%service_add_pre obs-staging-bot.service
|
|
||||||
|
|
||||||
%post obs-staging-bot
|
|
||||||
%service_add_post obs-staging-bot.service
|
|
||||||
|
|
||||||
%preun obs-staging-bot
|
|
||||||
%service_del_preun obs-staging-bot.service
|
|
||||||
|
|
||||||
%postun obs-staging-bot
|
|
||||||
%service_del_postun obs-staging-bot.service
|
|
||||||
|
|
||||||
%pre obs-status-service
|
|
||||||
%service_add_pre obs-status-service.service
|
|
||||||
|
|
||||||
%post obs-status-service
|
|
||||||
%service_add_post obs-status-service.service
|
|
||||||
|
|
||||||
%preun obs-status-service
|
|
||||||
%service_del_preun obs-status-service.service
|
|
||||||
|
|
||||||
%postun obs-status-service
|
|
||||||
%service_del_postun obs-status-service.service
|
|
||||||
|
|
||||||
%pre workflow-pr
|
|
||||||
%service_add_pre workflow-direct@.service
|
|
||||||
|
|
||||||
%post workflow-pr
|
|
||||||
%service_add_post workflow-direct@.service
|
|
||||||
|
|
||||||
%preun workflow-pr
|
|
||||||
%service_del_preun workflow-direct@.service
|
|
||||||
|
|
||||||
%postun workflow-pr
|
|
||||||
%service_del_postun workflow-direct@.service
|
|
||||||
|
|
||||||
%files devel-importer
|
|
||||||
%license COPYING
|
|
||||||
%doc devel-importer/README.md
|
|
||||||
%{_bindir}/devel-importer
|
|
||||||
|
|
||||||
%files doc
|
|
||||||
%license COPYING
|
|
||||||
%doc doc/README.md
|
|
||||||
%doc doc/workflows.md
|
|
||||||
|
|
||||||
%files gitea-events-rabbitmq-publisher
|
|
||||||
%license COPYING
|
%license COPYING
|
||||||
%doc gitea-events-rabbitmq-publisher/README.md
|
%doc gitea-events-rabbitmq-publisher/README.md
|
||||||
%{_bindir}/gitea-events-rabbitmq-publisher
|
%{_bindir}/gitea-events-rabbitmq-publisher
|
||||||
%{_unitdir}/gitea-events-rabbitmq-publisher.service
|
%{_unitdir}/gitea-events-rabbitmq-publisher.service
|
||||||
|
|
||||||
%files gitea-status-proxy
|
%files -n doc
|
||||||
%license COPYING
|
%license COPYING
|
||||||
%{_bindir}/gitea_status_proxy
|
%doc doc/README.md
|
||||||
|
%doc doc/workflows.md
|
||||||
|
|
||||||
%files group-review
|
%files -n devel-importer
|
||||||
|
%license COPYING
|
||||||
|
%doc devel-importer/README.md
|
||||||
|
%{_bindir}/devel-importer
|
||||||
|
|
||||||
|
%files -n group-review
|
||||||
%license COPYING
|
%license COPYING
|
||||||
%doc group-review/README.md
|
%doc group-review/README.md
|
||||||
%{_bindir}/group-review
|
%{_bindir}/group-review
|
||||||
%{_unitdir}/group-review@.service
|
|
||||||
|
|
||||||
%files obs-forward-bot
|
%files -n obs-staging-bot
|
||||||
%license COPYING
|
|
||||||
%{_bindir}/obs-forward-bot
|
|
||||||
|
|
||||||
%files obs-staging-bot
|
|
||||||
%license COPYING
|
%license COPYING
|
||||||
%doc obs-staging-bot/README.md
|
%doc obs-staging-bot/README.md
|
||||||
%{_bindir}/obs-staging-bot
|
%{_bindir}/obs-staging-bot
|
||||||
%{_unitdir}/obs-staging-bot.service
|
|
||||||
|
|
||||||
%files obs-status-service
|
%files -n obs-status-service
|
||||||
%license COPYING
|
%license COPYING
|
||||||
%doc obs-status-service/README.md
|
%doc obs-status-service/README.md
|
||||||
%{_bindir}/obs-status-service
|
%{_bindir}/obs-status-service
|
||||||
%{_unitdir}/obs-status-service.service
|
|
||||||
|
|
||||||
%files utils
|
%files -n workflow-direct
|
||||||
%license COPYING
|
|
||||||
%{_bindir}/hujson
|
|
||||||
|
|
||||||
%files workflow-direct
|
|
||||||
%license COPYING
|
%license COPYING
|
||||||
%doc workflow-direct/README.md
|
%doc workflow-direct/README.md
|
||||||
%{_bindir}/workflow-direct
|
#%{_bindir}/workflow-direct
|
||||||
%{_unitdir}/workflow-direct@.service
|
|
||||||
|
|
||||||
%files workflow-pr
|
%files -n workflow-pr
|
||||||
%license COPYING
|
%license COPYING
|
||||||
%doc workflow-pr/README.md
|
%doc workflow-pr/README.md
|
||||||
%{_bindir}/workflow-pr
|
#%{_bindir}/workflow-pr
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const PrPattern = "PR: %s/%s!%d"
|
const PrPattern = "PR: %s/%s#%d"
|
||||||
|
|
||||||
type BasicPR struct {
|
type BasicPR struct {
|
||||||
Org, Repo string
|
Org, Repo string
|
||||||
@@ -36,14 +36,10 @@ func parsePrLine(line string) (BasicPR, error) {
|
|||||||
return ret, errors.New("missing / separator")
|
return ret, errors.New("missing / separator")
|
||||||
}
|
}
|
||||||
|
|
||||||
repo := strings.SplitN(org[1], "!", 2)
|
repo := strings.SplitN(org[1], "#", 2)
|
||||||
ret.Repo = repo[0]
|
ret.Repo = repo[0]
|
||||||
if len(repo) != 2 {
|
if len(repo) != 2 {
|
||||||
repo = strings.SplitN(org[1], "#", 2)
|
return ret, errors.New("Missing # separator")
|
||||||
ret.Repo = repo[0]
|
|
||||||
}
|
|
||||||
if len(repo) != 2 {
|
|
||||||
return ret, errors.New("Missing ! or # separator")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gitea requires that each org and repo be [A-Za-z0-9_-]+
|
// Gitea requires that each org and repo be [A-Za-z0-9_-]+
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func TestAssociatedPRScanner(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Multiple PRs",
|
"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{
|
[]common.BasicPR{
|
||||||
{Org: "test", Repo: "foo", Num: 4},
|
{Org: "test", Repo: "foo", Num: 4},
|
||||||
{Org: "test", Repo: "goo", Num: 5},
|
{Org: "test", Repo: "goo", Num: 5},
|
||||||
@@ -107,7 +107,7 @@ func TestAppendingPRsToDescription(t *testing.T) {
|
|||||||
[]common.BasicPR{
|
[]common.BasicPR{
|
||||||
{Org: "a", Repo: "b", Num: 100},
|
{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",
|
"Append multiple PR to end of description",
|
||||||
@@ -119,7 +119,7 @@ func TestAppendingPRsToDescription(t *testing.T) {
|
|||||||
{Org: "b", Repo: "b", Num: 100},
|
{Org: "b", Repo: "b", Num: 100},
|
||||||
{Org: "c", 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",
|
"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: "c", Num: 101},
|
||||||
{Org: "a1", Repo: "b", Num: 100},
|
{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",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tailscale/hujson"
|
"github.com/tailscale/hujson"
|
||||||
@@ -36,9 +35,6 @@ import (
|
|||||||
const (
|
const (
|
||||||
ProjectConfigFile = "workflow.config"
|
ProjectConfigFile = "workflow.config"
|
||||||
StagingConfigFile = "staging.config"
|
StagingConfigFile = "staging.config"
|
||||||
|
|
||||||
Permission_ForceMerge = "force-merge"
|
|
||||||
Permission_Group = "release-engineering"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigFile struct {
|
type ConfigFile struct {
|
||||||
@@ -47,49 +43,23 @@ type ConfigFile struct {
|
|||||||
|
|
||||||
type ReviewGroup struct {
|
type ReviewGroup struct {
|
||||||
Name string
|
Name string
|
||||||
Silent bool // will not request reviews from group members
|
|
||||||
Reviewers []string
|
Reviewers []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type QAConfig struct {
|
type QAConfig struct {
|
||||||
Name string
|
Name string
|
||||||
Origin string
|
Origin string
|
||||||
BuildDisableRepos []string // which repos to build disable in the new project
|
|
||||||
}
|
|
||||||
|
|
||||||
type Permissions struct {
|
|
||||||
Permission string
|
|
||||||
Members []string
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
Label_StagingAuto = "staging/Auto"
|
|
||||||
Label_ReviewPending = "review/Pending"
|
|
||||||
Label_ReviewDone = "review/Done"
|
|
||||||
)
|
|
||||||
|
|
||||||
func LabelKey(tag_value string) string {
|
|
||||||
// capitalize first letter and remove /
|
|
||||||
if len(tag_value) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.ToUpper(tag_value[0:1]) + strings.ReplaceAll(tag_value[1:], "/", "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AutogitConfig struct {
|
type AutogitConfig struct {
|
||||||
Workflows []string // [pr, direct, test]
|
Workflows []string // [pr, direct, test]
|
||||||
Organization string
|
Organization string
|
||||||
GitProjectName string // Organization/GitProjectName.git is PrjGit
|
GitProjectName string // Organization/GitProjectName.git is PrjGit
|
||||||
Branch string // branch name of PkgGit that aligns with PrjGit submodules
|
Branch string // branch name of PkgGit that aligns with PrjGit submodules
|
||||||
Reviewers []string // only used by `pr` workflow
|
Reviewers []string // only used by `pr` workflow
|
||||||
Permissions []*Permissions // only used by `pr` workflow
|
ReviewGroups []ReviewGroup
|
||||||
ReviewGroups []*ReviewGroup
|
|
||||||
Committers []string // group in addition to Reviewers and Maintainers that can order the bot around, mostly as helper for factory-maintainers
|
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
|
|
||||||
|
|
||||||
Labels map[string]string // list of tags, if not default, to apply
|
|
||||||
|
|
||||||
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
|
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
|
ManualMergeProject bool // require merge of ProjectGit PRs with "Merge OK" by ProjectMaintainers and/or reviewers
|
||||||
}
|
}
|
||||||
@@ -205,8 +175,6 @@ func (configs AutogitConfigs) GetPrjGitConfig(org, repo, branch string) *Autogit
|
|||||||
if c.GitProjectName == prjgit {
|
if c.GitProjectName == prjgit {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
}
|
|
||||||
for _, c := range configs {
|
|
||||||
if c.Organization == org && c.Branch == branch {
|
if c.Organization == org && c.Branch == branch {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
@@ -215,27 +183,6 @@ func (configs AutogitConfigs) GetPrjGitConfig(org, repo, branch string) *Autogit
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *AutogitConfig) HasPermission(user, permission string) bool {
|
|
||||||
if config == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range config.Permissions {
|
|
||||||
if p.Permission == permission {
|
|
||||||
if slices.Contains(p.Members, user) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, m := range p.Members {
|
|
||||||
if members, err := config.GetReviewGroupMembers(m); err == nil && slices.Contains(members, user) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config *AutogitConfig) GetReviewGroupMembers(reviewer string) ([]string, error) {
|
func (config *AutogitConfig) GetReviewGroupMembers(reviewer string) ([]string, error) {
|
||||||
for _, g := range config.ReviewGroups {
|
for _, g := range config.ReviewGroups {
|
||||||
if g.Name == reviewer {
|
if g.Name == reviewer {
|
||||||
@@ -246,19 +193,10 @@ func (config *AutogitConfig) GetReviewGroupMembers(reviewer string) ([]string, e
|
|||||||
return nil, errors.New("User " + reviewer + " not found as group reviewer for " + config.GitProjectName)
|
return nil, errors.New("User " + reviewer + " not found as group reviewer for " + config.GitProjectName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *AutogitConfig) GetReviewGroup(reviewer string) (*ReviewGroup, error) {
|
|
||||||
for _, g := range config.ReviewGroups {
|
|
||||||
if g.Name == reviewer {
|
|
||||||
return g, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, errors.New("User " + reviewer + " not found as group reviewer for " + config.GitProjectName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (config *AutogitConfig) GetPrjGit() (string, string, string) {
|
func (config *AutogitConfig) GetPrjGit() (string, string, string) {
|
||||||
org := config.Organization
|
org := config.Organization
|
||||||
repo := DefaultGitPrj
|
repo := DefaultGitPrj
|
||||||
branch := ""
|
branch := "master"
|
||||||
|
|
||||||
a := strings.Split(config.GitProjectName, "/")
|
a := strings.Split(config.GitProjectName, "/")
|
||||||
if len(a[0]) > 0 {
|
if len(a[0]) > 0 {
|
||||||
@@ -282,9 +220,6 @@ func (config *AutogitConfig) GetPrjGit() (string, string, string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(branch) == 0 {
|
|
||||||
panic("branch for project is undefined. Should not happend." + org + "/" + repo)
|
|
||||||
}
|
|
||||||
return org, repo, branch
|
return org, repo, branch
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,14 +227,6 @@ func (config *AutogitConfig) GetRemoteBranch() string {
|
|||||||
return "origin_" + config.Branch
|
return "origin_" + config.Branch
|
||||||
}
|
}
|
||||||
|
|
||||||
func (config *AutogitConfig) Label(label string) string {
|
|
||||||
if t, found := config.Labels[LabelKey(label)]; found {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
return label
|
|
||||||
}
|
|
||||||
|
|
||||||
type StagingConfig struct {
|
type StagingConfig struct {
|
||||||
ObsProject string
|
ObsProject string
|
||||||
RebuildAll bool
|
RebuildAll bool
|
||||||
@@ -312,9 +239,6 @@ type StagingConfig struct {
|
|||||||
|
|
||||||
func ParseStagingConfig(data []byte) (*StagingConfig, error) {
|
func ParseStagingConfig(data []byte) (*StagingConfig, error) {
|
||||||
var staging StagingConfig
|
var staging StagingConfig
|
||||||
if len(data) == 0 {
|
|
||||||
return nil, errors.New("non-existent config file.")
|
|
||||||
}
|
|
||||||
data, err := hujson.Standardize(data)
|
data, err := hujson.Standardize(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -10,67 +10,6 @@ import (
|
|||||||
mock_common "src.opensuse.org/autogits/common/mock"
|
mock_common "src.opensuse.org/autogits/common/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLabelKey(t *testing.T) {
|
|
||||||
tests := map[string]string{
|
|
||||||
"": "",
|
|
||||||
"foo": "Foo",
|
|
||||||
"foo/bar": "Foobar",
|
|
||||||
"foo/Bar": "FooBar",
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range tests {
|
|
||||||
if c := common.LabelKey(k); c != v {
|
|
||||||
t.Error("expected", v, "got", c, "input", k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigLabelParser(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
json string
|
|
||||||
label_value string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty",
|
|
||||||
json: "{}",
|
|
||||||
label_value: "path/String",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "defined",
|
|
||||||
json: `{"Labels": {"foo": "bar", "PathString": "moo/Label"}}`,
|
|
||||||
label_value: "moo/Label",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "undefined",
|
|
||||||
json: `{"Labels": {"foo": "bar", "NotPathString": "moo/Label"}}`,
|
|
||||||
label_value: "path/String",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
repo := models.Repository{
|
|
||||||
DefaultBranch: "master",
|
|
||||||
}
|
|
||||||
|
|
||||||
ctl := gomock.NewController(t)
|
|
||||||
gitea := mock_common.NewMockGiteaFileContentAndRepoFetcher(ctl)
|
|
||||||
gitea.EXPECT().GetRepositoryFileContent("foo", "bar", "", "workflow.config").Return([]byte(test.json), "abc", nil)
|
|
||||||
gitea.EXPECT().GetRepository("foo", "bar").Return(&repo, nil)
|
|
||||||
|
|
||||||
config, err := common.ReadWorkflowConfig(gitea, "foo/bar")
|
|
||||||
if err != nil || config == nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if l := config.Label("path/String"); l != test.label_value {
|
|
||||||
t.Error("Expecting", test.label_value, "got", l)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProjectConfigMatcher(t *testing.T) {
|
func TestProjectConfigMatcher(t *testing.T) {
|
||||||
configs := common.AutogitConfigs{
|
configs := common.AutogitConfigs{
|
||||||
{
|
{
|
||||||
@@ -82,15 +21,6 @@ func TestProjectConfigMatcher(t *testing.T) {
|
|||||||
Branch: "main",
|
Branch: "main",
|
||||||
GitProjectName: "test/prjgit#main",
|
GitProjectName: "test/prjgit#main",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Organization: "test",
|
|
||||||
Branch: "main",
|
|
||||||
GitProjectName: "test/bar#never_match",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Organization: "test",
|
|
||||||
GitProjectName: "test/bar#main",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -120,20 +50,6 @@ func TestProjectConfigMatcher(t *testing.T) {
|
|||||||
branch: "main",
|
branch: "main",
|
||||||
config: 1,
|
config: 1,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "prjgit only match",
|
|
||||||
org: "test",
|
|
||||||
repo: "bar",
|
|
||||||
branch: "main",
|
|
||||||
config: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "non-default branch match",
|
|
||||||
org: "test",
|
|
||||||
repo: "bar",
|
|
||||||
branch: "something_main",
|
|
||||||
config: -1,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
@@ -189,15 +105,10 @@ func TestConfigWorkflowParser(t *testing.T) {
|
|||||||
if config.ManualMergeOnly != false {
|
if config.ManualMergeOnly != false {
|
||||||
t.Fatal("This should be false")
|
t.Fatal("This should be false")
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Label("foobar") != "foobar" {
|
|
||||||
t.Fatal("undefined label should return default value")
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: should test ReadWorkflowConfig as it will always set prjgit completely
|
|
||||||
func TestProjectGitParser(t *testing.T) {
|
func TestProjectGitParser(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -208,21 +119,20 @@ func TestProjectGitParser(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "repo only",
|
name: "repo only",
|
||||||
prjgit: "repo.git#master",
|
prjgit: "repo.git",
|
||||||
org: "org",
|
org: "org",
|
||||||
branch: "br",
|
branch: "br",
|
||||||
res: [3]string{"org", "repo.git", "master"},
|
res: [3]string{"org", "repo.git", "master"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "default",
|
name: "default",
|
||||||
org: "org",
|
org: "org",
|
||||||
prjgit: "org/_ObsPrj#master",
|
res: [3]string{"org", common.DefaultGitPrj, "master"},
|
||||||
res: [3]string{"org", common.DefaultGitPrj, "master"},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "repo with branch",
|
name: "repo with branch",
|
||||||
org: "org2",
|
org: "org2",
|
||||||
prjgit: "org2/repo.git#somebranch",
|
prjgit: "repo.git#somebranch",
|
||||||
res: [3]string{"org2", "repo.git", "somebranch"},
|
res: [3]string{"org2", "repo.git", "somebranch"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -239,25 +149,25 @@ func TestProjectGitParser(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "repo org and empty branch",
|
name: "repo org and empty branch",
|
||||||
org: "org3",
|
org: "org3",
|
||||||
prjgit: "oorg/foo.bar#master",
|
prjgit: "oorg/foo.bar#",
|
||||||
res: [3]string{"oorg", "foo.bar", "master"},
|
res: [3]string{"oorg", "foo.bar", "master"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "only branch defined",
|
name: "only branch defined",
|
||||||
org: "org3",
|
org: "org3",
|
||||||
prjgit: "org3/_ObsPrj#mybranch",
|
prjgit: "#mybranch",
|
||||||
res: [3]string{"org3", "_ObsPrj", "mybranch"},
|
res: [3]string{"org3", "_ObsPrj", "mybranch"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "only org and branch defined",
|
name: "only org and branch defined",
|
||||||
org: "org3",
|
org: "org3",
|
||||||
prjgit: "org1/_ObsPrj#mybranch",
|
prjgit: "org1/#mybranch",
|
||||||
res: [3]string{"org1", "_ObsPrj", "mybranch"},
|
res: [3]string{"org1", "_ObsPrj", "mybranch"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "empty org and repo",
|
name: "empty org and repo",
|
||||||
org: "org3",
|
org: "org3",
|
||||||
prjgit: "org3/repo#master",
|
prjgit: "/repo#",
|
||||||
res: [3]string{"org3", "repo", "master"},
|
res: [3]string{"org3", "repo", "master"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -278,67 +188,3 @@ func TestProjectGitParser(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfigPermissions(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
permission string
|
|
||||||
user string
|
|
||||||
config *common.AutogitConfig
|
|
||||||
result bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "NoPermissions",
|
|
||||||
permission: common.Permission_ForceMerge,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "NoPermissions",
|
|
||||||
permission: common.Permission_Group,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Regular permission ForcePush",
|
|
||||||
permission: common.Permission_ForceMerge,
|
|
||||||
result: true,
|
|
||||||
user: "user",
|
|
||||||
config: &common.AutogitConfig{
|
|
||||||
Permissions: []*common.Permissions{
|
|
||||||
&common.Permissions{
|
|
||||||
Permission: common.Permission_ForceMerge,
|
|
||||||
Members: []string{"user"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "User is part of a group",
|
|
||||||
permission: common.Permission_ForceMerge,
|
|
||||||
result: true,
|
|
||||||
user: "user",
|
|
||||||
config: &common.AutogitConfig{
|
|
||||||
Permissions: []*common.Permissions{
|
|
||||||
&common.Permissions{
|
|
||||||
Permission: common.Permission_ForceMerge,
|
|
||||||
Members: []string{"group"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ReviewGroups: []*common.ReviewGroup{
|
|
||||||
&common.ReviewGroup{
|
|
||||||
Name: "group",
|
|
||||||
Reviewers: []string{"some", "members", "including", "user"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
if r := test.config.HasPermission(test.user, test.permission); r != test.result {
|
|
||||||
t.Error("Expecting", test.result, "but got opposite")
|
|
||||||
}
|
|
||||||
if r := test.config.HasPermission(test.user+test.user, test.permission); r {
|
|
||||||
t.Error("Expecting false for fake user, but got opposite")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1731,246 +1731,3 @@ const requestedReviewJSON = `{
|
|||||||
"commit_id": "",
|
"commit_id": "",
|
||||||
"review": null
|
"review": null
|
||||||
}`
|
}`
|
||||||
|
|
||||||
const requestStatusJSON=`{
|
|
||||||
"commit": {
|
|
||||||
"id": "e637d86cbbdd438edbf60148e28f9d75a74d51b27b01f75610f247cd18394c8e",
|
|
||||||
"message": "Update nodejs-common.changes\n",
|
|
||||||
"url": "https://src.opensuse.org/autogits/nodejs-common/commit/e637d86cbbdd438edbf60148e28f9d75a74d51b27b01f75610f247cd18394c8e",
|
|
||||||
"author": {
|
|
||||||
"name": "Adam Majer",
|
|
||||||
"email": "adamm@noreply.src.opensuse.org",
|
|
||||||
"username": "adamm"
|
|
||||||
},
|
|
||||||
"committer": {
|
|
||||||
"name": "Adam Majer",
|
|
||||||
"email": "adamm@noreply.src.opensuse.org",
|
|
||||||
"username": "adamm"
|
|
||||||
},
|
|
||||||
"verification": null,
|
|
||||||
"timestamp": "2025-09-16T12:41:02+02:00",
|
|
||||||
"added": [],
|
|
||||||
"removed": [],
|
|
||||||
"modified": [
|
|
||||||
"nodejs-common.changes"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"context": "test",
|
|
||||||
"created_at": "2025-09-16T10:50:32Z",
|
|
||||||
"description": "",
|
|
||||||
"id": 21663,
|
|
||||||
"repository": {
|
|
||||||
"id": 90520,
|
|
||||||
"owner": {
|
|
||||||
"id": 983,
|
|
||||||
"login": "autogits",
|
|
||||||
"login_name": "",
|
|
||||||
"source_id": 0,
|
|
||||||
"full_name": "",
|
|
||||||
"email": "",
|
|
||||||
"avatar_url": "https://src.opensuse.org/avatars/80a61ef3a14c3c22f0b8b1885d1a75d4",
|
|
||||||
"html_url": "https://src.opensuse.org/autogits",
|
|
||||||
"language": "",
|
|
||||||
"is_admin": false,
|
|
||||||
"last_login": "0001-01-01T00:00:00Z",
|
|
||||||
"created": "2024-06-20T09:46:37+02:00",
|
|
||||||
"restricted": false,
|
|
||||||
"active": false,
|
|
||||||
"prohibit_login": false,
|
|
||||||
"location": "",
|
|
||||||
"website": "",
|
|
||||||
"description": "",
|
|
||||||
"visibility": "public",
|
|
||||||
"followers_count": 0,
|
|
||||||
"following_count": 0,
|
|
||||||
"starred_repos_count": 0,
|
|
||||||
"username": "autogits"
|
|
||||||
},
|
|
||||||
"name": "nodejs-common",
|
|
||||||
"full_name": "autogits/nodejs-common",
|
|
||||||
"description": "",
|
|
||||||
"empty": false,
|
|
||||||
"private": false,
|
|
||||||
"fork": true,
|
|
||||||
"template": false,
|
|
||||||
"parent": {
|
|
||||||
"id": 62649,
|
|
||||||
"owner": {
|
|
||||||
"id": 64,
|
|
||||||
"login": "pool",
|
|
||||||
"login_name": "",
|
|
||||||
"source_id": 0,
|
|
||||||
"full_name": "",
|
|
||||||
"email": "",
|
|
||||||
"avatar_url": "https://src.opensuse.org/avatars/b10a8c0bede9eb4ea771b04db3149f28",
|
|
||||||
"html_url": "https://src.opensuse.org/pool",
|
|
||||||
"language": "",
|
|
||||||
"is_admin": false,
|
|
||||||
"last_login": "0001-01-01T00:00:00Z",
|
|
||||||
"created": "2023-03-01T14:41:17+01:00",
|
|
||||||
"restricted": false,
|
|
||||||
"active": false,
|
|
||||||
"prohibit_login": false,
|
|
||||||
"location": "",
|
|
||||||
"website": "",
|
|
||||||
"description": "",
|
|
||||||
"visibility": "public",
|
|
||||||
"followers_count": 2,
|
|
||||||
"following_count": 0,
|
|
||||||
"starred_repos_count": 0,
|
|
||||||
"username": "pool"
|
|
||||||
},
|
|
||||||
"name": "nodejs-common",
|
|
||||||
"full_name": "pool/nodejs-common",
|
|
||||||
"description": "",
|
|
||||||
"empty": false,
|
|
||||||
"private": false,
|
|
||||||
"fork": false,
|
|
||||||
"template": false,
|
|
||||||
"mirror": false,
|
|
||||||
"size": 134,
|
|
||||||
"language": "",
|
|
||||||
"languages_url": "https://src.opensuse.org/api/v1/repos/pool/nodejs-common/languages",
|
|
||||||
"html_url": "https://src.opensuse.org/pool/nodejs-common",
|
|
||||||
"url": "https://src.opensuse.org/api/v1/repos/pool/nodejs-common",
|
|
||||||
"link": "",
|
|
||||||
"ssh_url": "gitea@src.opensuse.org:pool/nodejs-common.git",
|
|
||||||
"clone_url": "https://src.opensuse.org/pool/nodejs-common.git",
|
|
||||||
"original_url": "",
|
|
||||||
"website": "",
|
|
||||||
"stars_count": 0,
|
|
||||||
"forks_count": 3,
|
|
||||||
"watchers_count": 12,
|
|
||||||
"open_issues_count": 0,
|
|
||||||
"open_pr_counter": 0,
|
|
||||||
"release_counter": 0,
|
|
||||||
"default_branch": "factory",
|
|
||||||
"archived": false,
|
|
||||||
"created_at": "2024-06-17T17:08:45+02:00",
|
|
||||||
"updated_at": "2025-08-21T21:58:31+02:00",
|
|
||||||
"archived_at": "1970-01-01T01:00:00+01:00",
|
|
||||||
"permissions": {
|
|
||||||
"admin": true,
|
|
||||||
"push": true,
|
|
||||||
"pull": true
|
|
||||||
},
|
|
||||||
"has_issues": true,
|
|
||||||
"internal_tracker": {
|
|
||||||
"enable_time_tracker": false,
|
|
||||||
"allow_only_contributors_to_track_time": true,
|
|
||||||
"enable_issue_dependencies": true
|
|
||||||
},
|
|
||||||
"has_wiki": false,
|
|
||||||
"has_pull_requests": true,
|
|
||||||
"has_projects": false,
|
|
||||||
"projects_mode": "all",
|
|
||||||
"has_releases": false,
|
|
||||||
"has_packages": false,
|
|
||||||
"has_actions": false,
|
|
||||||
"ignore_whitespace_conflicts": false,
|
|
||||||
"allow_merge_commits": true,
|
|
||||||
"allow_rebase": true,
|
|
||||||
"allow_rebase_explicit": true,
|
|
||||||
"allow_squash_merge": true,
|
|
||||||
"allow_fast_forward_only_merge": true,
|
|
||||||
"allow_rebase_update": true,
|
|
||||||
"allow_manual_merge": true,
|
|
||||||
"autodetect_manual_merge": true,
|
|
||||||
"default_delete_branch_after_merge": false,
|
|
||||||
"default_merge_style": "merge",
|
|
||||||
"default_allow_maintainer_edit": false,
|
|
||||||
"avatar_url": "",
|
|
||||||
"internal": false,
|
|
||||||
"mirror_interval": "",
|
|
||||||
"object_format_name": "sha256",
|
|
||||||
"mirror_updated": "0001-01-01T00:00:00Z",
|
|
||||||
"topics": [],
|
|
||||||
"licenses": []
|
|
||||||
},
|
|
||||||
"mirror": false,
|
|
||||||
"size": 143,
|
|
||||||
"language": "",
|
|
||||||
"languages_url": "https://src.opensuse.org/api/v1/repos/autogits/nodejs-common/languages",
|
|
||||||
"html_url": "https://src.opensuse.org/autogits/nodejs-common",
|
|
||||||
"url": "https://src.opensuse.org/api/v1/repos/autogits/nodejs-common",
|
|
||||||
"link": "",
|
|
||||||
"ssh_url": "gitea@src.opensuse.org:autogits/nodejs-common.git",
|
|
||||||
"clone_url": "https://src.opensuse.org/autogits/nodejs-common.git",
|
|
||||||
"original_url": "",
|
|
||||||
"website": "",
|
|
||||||
"stars_count": 0,
|
|
||||||
"forks_count": 1,
|
|
||||||
"watchers_count": 4,
|
|
||||||
"open_issues_count": 0,
|
|
||||||
"open_pr_counter": 1,
|
|
||||||
"release_counter": 0,
|
|
||||||
"default_branch": "factory",
|
|
||||||
"archived": false,
|
|
||||||
"created_at": "2024-07-01T13:29:03+02:00",
|
|
||||||
"updated_at": "2025-09-16T12:41:03+02:00",
|
|
||||||
"archived_at": "1970-01-01T01:00:00+01:00",
|
|
||||||
"permissions": {
|
|
||||||
"admin": true,
|
|
||||||
"push": true,
|
|
||||||
"pull": true
|
|
||||||
},
|
|
||||||
"has_issues": false,
|
|
||||||
"has_wiki": false,
|
|
||||||
"has_pull_requests": true,
|
|
||||||
"has_projects": false,
|
|
||||||
"projects_mode": "all",
|
|
||||||
"has_releases": false,
|
|
||||||
"has_packages": false,
|
|
||||||
"has_actions": false,
|
|
||||||
"ignore_whitespace_conflicts": false,
|
|
||||||
"allow_merge_commits": true,
|
|
||||||
"allow_rebase": true,
|
|
||||||
"allow_rebase_explicit": true,
|
|
||||||
"allow_squash_merge": true,
|
|
||||||
"allow_fast_forward_only_merge": true,
|
|
||||||
"allow_rebase_update": true,
|
|
||||||
"allow_manual_merge": true,
|
|
||||||
"autodetect_manual_merge": true,
|
|
||||||
"default_delete_branch_after_merge": false,
|
|
||||||
"default_merge_style": "merge",
|
|
||||||
"default_allow_maintainer_edit": false,
|
|
||||||
"avatar_url": "",
|
|
||||||
"internal": false,
|
|
||||||
"mirror_interval": "",
|
|
||||||
"object_format_name": "sha256",
|
|
||||||
"mirror_updated": "0001-01-01T00:00:00Z",
|
|
||||||
"topics": [],
|
|
||||||
"licenses": [
|
|
||||||
"MIT"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"sender": {
|
|
||||||
"id": 129,
|
|
||||||
"login": "adamm",
|
|
||||||
"login_name": "",
|
|
||||||
"source_id": 0,
|
|
||||||
"full_name": "Adam Majer",
|
|
||||||
"email": "adamm@noreply.src.opensuse.org",
|
|
||||||
"avatar_url": "https://src.opensuse.org/avatars/3e8917bfbf04293f7c20c28cacd83dae2ba9b78a6c6a9a1bedf14c683d8a3763",
|
|
||||||
"html_url": "https://src.opensuse.org/adamm",
|
|
||||||
"language": "",
|
|
||||||
"is_admin": false,
|
|
||||||
"last_login": "0001-01-01T00:00:00Z",
|
|
||||||
"created": "2023-07-21T16:43:48+02:00",
|
|
||||||
"restricted": false,
|
|
||||||
"active": false,
|
|
||||||
"prohibit_login": false,
|
|
||||||
"location": "",
|
|
||||||
"website": "",
|
|
||||||
"description": "",
|
|
||||||
"visibility": "public",
|
|
||||||
"followers_count": 1,
|
|
||||||
"following_count": 0,
|
|
||||||
"starred_repos_count": 0,
|
|
||||||
"username": "adamm"
|
|
||||||
},
|
|
||||||
"sha": "e637d86cbbdd438edbf60148e28f9d75a74d51b27b01f75610f247cd18394c8e",
|
|
||||||
"state": "pending",
|
|
||||||
"target_url": "https://src.opensuse.org/",
|
|
||||||
"updated_at": "2025-09-16T10:50:32Z"
|
|
||||||
}`
|
|
||||||
|
|||||||
@@ -40,10 +40,6 @@ type GitSubmoduleLister interface {
|
|||||||
GitSubmoduleCommitId(cwd, packageName, commitId string) (subCommitId string, valid bool)
|
GitSubmoduleCommitId(cwd, packageName, commitId string) (subCommitId string, valid bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
type GitDirectoryLister interface {
|
|
||||||
GitDirectoryList(gitPath, commitId string) (dirlist map[string]string, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type GitStatusLister interface {
|
type GitStatusLister interface {
|
||||||
GitStatus(cwd string) ([]GitStatusData, error)
|
GitStatus(cwd string) ([]GitStatusData, error)
|
||||||
}
|
}
|
||||||
@@ -65,14 +61,12 @@ type Git interface {
|
|||||||
io.Closer
|
io.Closer
|
||||||
|
|
||||||
GitSubmoduleLister
|
GitSubmoduleLister
|
||||||
GitDirectoryLister
|
|
||||||
GitStatusLister
|
GitStatusLister
|
||||||
|
|
||||||
GitExecWithOutputOrPanic(cwd string, params ...string) string
|
GitExecWithOutputOrPanic(cwd string, params ...string) string
|
||||||
GitExecOrPanic(cwd string, params ...string)
|
GitExecOrPanic(cwd string, params ...string)
|
||||||
GitExec(cwd string, params ...string) error
|
GitExec(cwd string, params ...string) error
|
||||||
GitExecWithOutput(cwd string, params ...string) (string, error)
|
GitExecWithOutput(cwd string, params ...string) (string, error)
|
||||||
GitExecQuietOrPanic(cwd string, params ...string)
|
|
||||||
|
|
||||||
GitDiffLister
|
GitDiffLister
|
||||||
}
|
}
|
||||||
@@ -82,8 +76,7 @@ type GitHandlerImpl struct {
|
|||||||
GitCommiter string
|
GitCommiter string
|
||||||
GitEmail string
|
GitEmail string
|
||||||
|
|
||||||
lock *sync.Mutex
|
lock *sync.Mutex
|
||||||
quiet bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *GitHandlerImpl) GetPath() string {
|
func (s *GitHandlerImpl) GetPath() string {
|
||||||
@@ -218,7 +211,7 @@ func (e *GitHandlerImpl) GitClone(repo, branch, remoteUrl string) (string, error
|
|||||||
return "", fmt.Errorf("Cannot parse remote URL: %w", err)
|
return "", fmt.Errorf("Cannot parse remote URL: %w", err)
|
||||||
}
|
}
|
||||||
remoteBranch := "HEAD"
|
remoteBranch := "HEAD"
|
||||||
if len(branch) == 0 && remoteUrlComp != nil && remoteUrlComp.Commit != "HEAD" {
|
if len(branch) == 0 && remoteUrlComp != nil {
|
||||||
branch = remoteUrlComp.Commit
|
branch = remoteUrlComp.Commit
|
||||||
remoteBranch = branch
|
remoteBranch = branch
|
||||||
} else if len(branch) > 0 {
|
} else if len(branch) > 0 {
|
||||||
@@ -247,51 +240,46 @@ func (e *GitHandlerImpl) GitClone(repo, branch, remoteUrl string) (string, error
|
|||||||
|
|
||||||
// check if we have submodule to deinit
|
// check if we have submodule to deinit
|
||||||
if list, _ := e.GitSubmoduleList(repo, "HEAD"); len(list) > 0 {
|
if list, _ := e.GitSubmoduleList(repo, "HEAD"); len(list) > 0 {
|
||||||
e.GitExecQuietOrPanic(repo, "submodule", "deinit", "--all", "--force")
|
e.GitExecOrPanic(repo, "submodule", "deinit", "--all", "--force")
|
||||||
}
|
}
|
||||||
|
|
||||||
e.GitExecOrPanic(repo, "fetch", "--prune", remoteName, remoteBranch)
|
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)
|
|
||||||
return remoteName, fmt.Errorf("Cannot read HEAD of remote %s", remoteName)
|
|
||||||
}
|
|
||||||
|
|
||||||
refs := string(refsBytes)
|
refsBytes, err := os.ReadFile(path.Join(e.GitPath, repo, ".git/refs/remotes", remoteName, "HEAD"))
|
||||||
if refs[0:5] != "ref: " {
|
if err != nil {
|
||||||
LogError("Unexpected format of remote HEAD ref:", refs)
|
LogError("Cannot read HEAD of remote", remoteName)
|
||||||
return remoteName, fmt.Errorf("Unexpected format of remote HEAD ref: %s", refs)
|
return remoteName, fmt.Errorf("Cannot read HEAD of remote %s", remoteName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refs := string(refsBytes)
|
||||||
|
if refs[0:5] != "ref: " {
|
||||||
|
LogError("Unexpected format of remote HEAD ref:", refs)
|
||||||
|
return remoteName, fmt.Errorf("Unexpected format of remote HEAD ref: %s", refs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(branch) == 0 || branch == "HEAD" {
|
||||||
|
remoteRef = strings.TrimSpace(refs[5:])
|
||||||
|
branch = remoteRef[strings.LastIndex(remoteRef, "/")+1:]
|
||||||
|
LogDebug("remoteRef", remoteRef)
|
||||||
|
LogDebug("branch", branch)
|
||||||
|
}
|
||||||
|
|
||||||
if len(branch) == 0 || branch == "HEAD" {
|
|
||||||
remoteRef = strings.TrimSpace(refs[5:])
|
|
||||||
branch = remoteRef[strings.LastIndex(remoteRef, "/")+1:]
|
|
||||||
LogDebug("remoteRef", remoteRef)
|
|
||||||
LogDebug("branch", branch)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
args := []string{"fetch", "--prune", remoteName, branch}
|
args := []string{"fetch", "--prune", remoteName, branch}
|
||||||
if strings.TrimSpace(e.GitExecWithOutputOrPanic(repo, "rev-parse", "--is-shallow-repository")) == "true" {
|
if strings.TrimSpace(e.GitExecWithOutputOrPanic(repo, "rev-parse", "--is-shallow-repository")) == "true" {
|
||||||
args = slices.Insert(args, 1, "--unshallow")
|
args = slices.Insert(args, 1, "--unshallow")
|
||||||
}
|
}
|
||||||
e.GitExecOrPanic(repo, args...)
|
e.GitExecOrPanic(repo, args...)
|
||||||
return remoteName, e.GitExec(repo, "checkout", "-f", "--track", "-B", branch, remoteRef)
|
return remoteName, e.GitExec(repo, "checkout", "--track", "-B", branch, remoteRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *GitHandlerImpl) GitBranchHead(gitDir, branchName string) (string, error) {
|
func (e *GitHandlerImpl) GitBranchHead(gitDir, branchName string) (string, error) {
|
||||||
id, err := e.GitExecWithOutput(gitDir, "show-ref", "--heads", "--hash", branchName)
|
id, err := e.GitExecWithOutput(gitDir, "show-ref", "--hash", "--verify", "refs/heads/"+branchName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("Can't find default branch: %s", branchName)
|
return "", fmt.Errorf("Can't find default branch: %s", branchName)
|
||||||
}
|
}
|
||||||
|
|
||||||
id = strings.TrimSpace(SplitLines(id)[0])
|
return strings.TrimSpace(id), nil
|
||||||
if len(id) < 10 {
|
|
||||||
return "", fmt.Errorf("Can't find branch: %s", branchName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return id, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *GitHandlerImpl) GitRemoteHead(gitDir, remote, branchName string) (string, error) {
|
func (e *GitHandlerImpl) GitRemoteHead(gitDir, remote, branchName string) (string, error) {
|
||||||
@@ -350,10 +338,6 @@ var ExtraGitParams []string
|
|||||||
|
|
||||||
func (e *GitHandlerImpl) GitExecWithOutput(cwd string, params ...string) (string, error) {
|
func (e *GitHandlerImpl) GitExecWithOutput(cwd string, params ...string) (string, error) {
|
||||||
cmd := exec.Command("/usr/bin/git", params...)
|
cmd := exec.Command("/usr/bin/git", params...)
|
||||||
var identityFile string
|
|
||||||
if i := os.Getenv("AUTOGITS_IDENTITY_FILE"); len(i) > 0 {
|
|
||||||
identityFile = " -i " + i
|
|
||||||
}
|
|
||||||
cmd.Env = []string{
|
cmd.Env = []string{
|
||||||
"GIT_CEILING_DIRECTORIES=" + e.GitPath,
|
"GIT_CEILING_DIRECTORIES=" + e.GitPath,
|
||||||
"GIT_CONFIG_GLOBAL=/dev/null",
|
"GIT_CONFIG_GLOBAL=/dev/null",
|
||||||
@@ -361,8 +345,7 @@ func (e *GitHandlerImpl) GitExecWithOutput(cwd string, params ...string) (string
|
|||||||
"GIT_COMMITTER_NAME=" + e.GitCommiter,
|
"GIT_COMMITTER_NAME=" + e.GitCommiter,
|
||||||
"EMAIL=not@exist@src.opensuse.org",
|
"EMAIL=not@exist@src.opensuse.org",
|
||||||
"GIT_LFS_SKIP_SMUDGE=1",
|
"GIT_LFS_SKIP_SMUDGE=1",
|
||||||
"GIT_LFS_SKIP_PUSH=1",
|
"GIT_SSH_COMMAND=/usr/bin/ssh -o StrictHostKeyChecking=yes",
|
||||||
"GIT_SSH_COMMAND=/usr/bin/ssh -o StrictHostKeyChecking=yes" + identityFile,
|
|
||||||
}
|
}
|
||||||
if len(ExtraGitParams) > 0 {
|
if len(ExtraGitParams) > 0 {
|
||||||
cmd.Env = append(cmd.Env, ExtraGitParams...)
|
cmd.Env = append(cmd.Env, ExtraGitParams...)
|
||||||
@@ -372,9 +355,7 @@ func (e *GitHandlerImpl) GitExecWithOutput(cwd string, params ...string) (string
|
|||||||
|
|
||||||
LogDebug("git execute @", cwd, ":", cmd.Args)
|
LogDebug("git execute @", cwd, ":", cmd.Args)
|
||||||
out, err := cmd.CombinedOutput()
|
out, err := cmd.CombinedOutput()
|
||||||
if !e.quiet {
|
LogDebug(string(out))
|
||||||
LogDebug(string(out))
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LogError("git", cmd.Args, " error:", err)
|
LogError("git", cmd.Args, " error:", err)
|
||||||
return "", fmt.Errorf("error executing: git %#v \n%s\n err: %w", cmd.Args, out, err)
|
return "", fmt.Errorf("error executing: git %#v \n%s\n err: %w", cmd.Args, out, err)
|
||||||
@@ -383,13 +364,6 @@ func (e *GitHandlerImpl) GitExecWithOutput(cwd string, params ...string) (string
|
|||||||
return string(out), nil
|
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 {
|
type ChanIO struct {
|
||||||
ch chan byte
|
ch chan byte
|
||||||
}
|
}
|
||||||
@@ -787,80 +761,6 @@ func (e *GitHandlerImpl) GitCatFile(cwd, commitId, filename string) (data []byte
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// return (filename) -> (hash) map for all submodules
|
|
||||||
func (e *GitHandlerImpl) GitDirectoryList(gitPath, commitId string) (directoryList map[string]string, err error) {
|
|
||||||
var done sync.Mutex
|
|
||||||
directoryList = make(map[string]string)
|
|
||||||
|
|
||||||
done.Lock()
|
|
||||||
data_in, data_out := ChanIO{make(chan byte)}, ChanIO{make(chan byte)}
|
|
||||||
|
|
||||||
LogDebug("Getting directory for:", commitId)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer done.Unlock()
|
|
||||||
defer close(data_out.ch)
|
|
||||||
|
|
||||||
data_out.Write([]byte(commitId))
|
|
||||||
data_out.ch <- '\x00'
|
|
||||||
var c GitCommit
|
|
||||||
c, err = parseGitCommit(data_in.ch)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("Error parsing git commit. Err: %w", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
trees := make(map[string]string)
|
|
||||||
trees[""] = c.Tree
|
|
||||||
|
|
||||||
for len(trees) > 0 {
|
|
||||||
for p, tree := range trees {
|
|
||||||
delete(trees, p)
|
|
||||||
|
|
||||||
data_out.Write([]byte(tree))
|
|
||||||
data_out.ch <- '\x00'
|
|
||||||
var tree GitTree
|
|
||||||
tree, err = parseGitTree(data_in.ch)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("Error parsing git tree: %w", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, te := range tree.items {
|
|
||||||
if te.isTree() {
|
|
||||||
directoryList[p+te.name] = te.hash
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
cmd := exec.Command("/usr/bin/git", "cat-file", "--batch", "-Z")
|
|
||||||
cmd.Env = []string{
|
|
||||||
"GIT_CEILING_DIRECTORIES=" + e.GitPath,
|
|
||||||
"GIT_LFS_SKIP_SMUDGE=1",
|
|
||||||
"GIT_CONFIG_GLOBAL=/dev/null",
|
|
||||||
}
|
|
||||||
cmd.Dir = filepath.Join(e.GitPath, gitPath)
|
|
||||||
cmd.Stdout = &data_in
|
|
||||||
cmd.Stdin = &data_out
|
|
||||||
cmd.Stderr = writeFunc(func(data []byte) (int, error) {
|
|
||||||
LogError(string(data))
|
|
||||||
return len(data), nil
|
|
||||||
})
|
|
||||||
LogDebug("command run:", cmd.Args)
|
|
||||||
if e := cmd.Run(); e != nil {
|
|
||||||
LogError(e)
|
|
||||||
close(data_in.ch)
|
|
||||||
close(data_out.ch)
|
|
||||||
return directoryList, e
|
|
||||||
}
|
|
||||||
|
|
||||||
done.Lock()
|
|
||||||
return directoryList, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// return (filename) -> (hash) map for all submodules
|
// return (filename) -> (hash) map for all submodules
|
||||||
func (e *GitHandlerImpl) GitSubmoduleList(gitPath, commitId string) (submoduleList map[string]string, err error) {
|
func (e *GitHandlerImpl) GitSubmoduleList(gitPath, commitId string) (submoduleList map[string]string, err error) {
|
||||||
var done sync.Mutex
|
var done sync.Mutex
|
||||||
|
|||||||
@@ -392,7 +392,6 @@ func TestCommitTreeParsing(t *testing.T) {
|
|||||||
commitId = commitId + strings.TrimSpace(string(data))
|
commitId = commitId + strings.TrimSpace(string(data))
|
||||||
return len(data), nil
|
return len(data), nil
|
||||||
})
|
})
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
t.Fatal(err.Error())
|
t.Fatal(err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
transport "github.com/go-openapi/runtime/client"
|
transport "github.com/go-openapi/runtime/client"
|
||||||
@@ -67,14 +66,6 @@ const (
|
|||||||
ReviewStateUnknown models.ReviewStateType = ""
|
ReviewStateUnknown models.ReviewStateType = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
type GiteaLabelGetter interface {
|
|
||||||
GetLabels(org, repo string, idx int64) ([]*models.Label, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type GiteaLabelSettter interface {
|
|
||||||
SetLabels(org, repo string, idx int64, labels []string) ([]*models.Label, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type GiteaTimelineFetcher interface {
|
type GiteaTimelineFetcher interface {
|
||||||
GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error)
|
GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error)
|
||||||
}
|
}
|
||||||
@@ -100,10 +91,9 @@ type GiteaPRUpdater interface {
|
|||||||
UpdatePullRequest(org, repo string, num int64, options *models.EditPullRequestOption) (*models.PullRequest, error)
|
UpdatePullRequest(org, repo string, num int64, options *models.EditPullRequestOption) (*models.PullRequest, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type GiteaPRTimelineReviewFetcher interface {
|
type GiteaPRTimelineFetcher interface {
|
||||||
GiteaPRFetcher
|
GiteaPRFetcher
|
||||||
GiteaTimelineFetcher
|
GiteaTimelineFetcher
|
||||||
GiteaReviewFetcher
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GiteaCommitFetcher interface {
|
type GiteaCommitFetcher interface {
|
||||||
@@ -129,16 +119,10 @@ type GiteaPRChecker interface {
|
|||||||
GiteaMaintainershipReader
|
GiteaMaintainershipReader
|
||||||
}
|
}
|
||||||
|
|
||||||
type GiteaReviewFetcherAndRequesterAndUnrequester interface {
|
type GiteaReviewFetcherAndRequester interface {
|
||||||
GiteaReviewTimelineFetcher
|
GiteaReviewTimelineFetcher
|
||||||
GiteaCommentFetcher
|
GiteaCommentFetcher
|
||||||
GiteaReviewRequester
|
GiteaReviewRequester
|
||||||
GiteaReviewUnrequester
|
|
||||||
}
|
|
||||||
|
|
||||||
type GiteaUnreviewTimelineFetcher interface {
|
|
||||||
GiteaTimelineFetcher
|
|
||||||
GiteaReviewUnrequester
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GiteaReviewRequester interface {
|
type GiteaReviewRequester interface {
|
||||||
@@ -176,10 +160,6 @@ type GiteaCommitStatusGetter interface {
|
|||||||
GetCommitStatus(org, repo, hash string) ([]*models.CommitStatus, error)
|
GetCommitStatus(org, repo, hash string) ([]*models.CommitStatus, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type GiteaMerger interface {
|
|
||||||
ManualMergePR(org, repo string, id int64, commitid string, delBranch bool) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type Gitea interface {
|
type Gitea interface {
|
||||||
GiteaComment
|
GiteaComment
|
||||||
GiteaRepoFetcher
|
GiteaRepoFetcher
|
||||||
@@ -188,7 +168,6 @@ type Gitea interface {
|
|||||||
GiteaReviewer
|
GiteaReviewer
|
||||||
GiteaPRFetcher
|
GiteaPRFetcher
|
||||||
GiteaPRUpdater
|
GiteaPRUpdater
|
||||||
GiteaMerger
|
|
||||||
GiteaCommitFetcher
|
GiteaCommitFetcher
|
||||||
GiteaReviewFetcher
|
GiteaReviewFetcher
|
||||||
GiteaCommentFetcher
|
GiteaCommentFetcher
|
||||||
@@ -198,8 +177,7 @@ type Gitea interface {
|
|||||||
GiteaCommitStatusGetter
|
GiteaCommitStatusGetter
|
||||||
GiteaCommitStatusSetter
|
GiteaCommitStatusSetter
|
||||||
GiteaSetRepoOptions
|
GiteaSetRepoOptions
|
||||||
GiteaLabelGetter
|
GiteaTimelineFetcher
|
||||||
GiteaLabelSettter
|
|
||||||
|
|
||||||
GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error)
|
GetNotifications(Type string, since *time.Time) ([]*models.NotificationThread, error)
|
||||||
GetDoneNotifications(Type string, page int64) ([]*models.NotificationThread, error)
|
GetDoneNotifications(Type string, page int64) ([]*models.NotificationThread, error)
|
||||||
@@ -207,7 +185,7 @@ type Gitea interface {
|
|||||||
GetOrganization(orgName string) (*models.Organization, error)
|
GetOrganization(orgName string) (*models.Organization, error)
|
||||||
GetOrganizationRepositories(orgName string) ([]*models.Repository, error)
|
GetOrganizationRepositories(orgName string) ([]*models.Repository, error)
|
||||||
CreateRepositoryIfNotExist(git Git, org, repoName string) (*models.Repository, error)
|
CreateRepositoryIfNotExist(git Git, org, repoName string) (*models.Repository, error)
|
||||||
CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error, bool)
|
CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error)
|
||||||
GetPullRequestFileContent(pr *models.PullRequest, path string) ([]byte, string, error)
|
GetPullRequestFileContent(pr *models.PullRequest, path string) ([]byte, string, error)
|
||||||
GetRecentPullRequests(org, repo, branch string) ([]*models.PullRequest, error)
|
GetRecentPullRequests(org, repo, branch string) ([]*models.PullRequest, error)
|
||||||
GetRecentCommits(org, repo, branch string, commitNo int64) ([]*models.Commit, error)
|
GetRecentCommits(org, repo, branch string, commitNo int64) ([]*models.Commit, error)
|
||||||
@@ -255,11 +233,6 @@ func (gitea *GiteaTransport) GetPullRequest(org, project string, num int64) (*mo
|
|||||||
gitea.transport.DefaultAuthentication,
|
gitea.transport.DefaultAuthentication,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
LogError(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return pr.Payload, err
|
return pr.Payload, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,36 +246,9 @@ func (gitea *GiteaTransport) UpdatePullRequest(org, repo string, num int64, opti
|
|||||||
gitea.transport.DefaultAuthentication,
|
gitea.transport.DefaultAuthentication,
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
LogError(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return pr.Payload, err
|
return pr.Payload, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gitea *GiteaTransport) ManualMergePR(org, repo string, num int64, commitid string, delBranch bool) error {
|
|
||||||
manual_merge := "manually-merged"
|
|
||||||
_, err := gitea.client.Repository.RepoMergePullRequest(
|
|
||||||
repository.NewRepoMergePullRequestParams().
|
|
||||||
WithOwner(org).
|
|
||||||
WithRepo(repo).
|
|
||||||
WithIndex(num).
|
|
||||||
WithBody(&models.MergePullRequestForm{
|
|
||||||
Do: &manual_merge,
|
|
||||||
DeleteBranchAfterMerge: delBranch,
|
|
||||||
HeadCommitID: commitid,
|
|
||||||
}), gitea.transport.DefaultAuthentication,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
LogError(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gitea *GiteaTransport) GetPullRequests(org, repo string) ([]*models.PullRequest, error) {
|
func (gitea *GiteaTransport) GetPullRequests(org, repo string) ([]*models.PullRequest, error) {
|
||||||
var page, limit int64
|
var page, limit int64
|
||||||
|
|
||||||
@@ -327,9 +273,6 @@ func (gitea *GiteaTransport) GetPullRequests(org, repo string) ([]*models.PullRe
|
|||||||
return nil, fmt.Errorf("cannot fetch PR list for %s / %s : %w", org, repo, err)
|
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)
|
prs = slices.Concat(prs, req.Payload)
|
||||||
if len(req.Payload) < int(limit) {
|
if len(req.Payload) < int(limit) {
|
||||||
break
|
break
|
||||||
@@ -352,11 +295,11 @@ func (gitea *GiteaTransport) GetCommitStatus(org, repo, hash string) ([]*models.
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
if len(r.Payload) == 0 {
|
|
||||||
|
res = append(res, r.Payload...)
|
||||||
|
if len(r.Payload) < int(limit) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
res = append(res, r.Payload...)
|
|
||||||
page++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res, nil
|
return res, nil
|
||||||
@@ -417,10 +360,10 @@ func (gitea *GiteaTransport) GetPullRequestReviews(org, project string, PRnum in
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(reviews.Payload) == 0 {
|
allReviews = slices.Concat(allReviews, reviews.Payload)
|
||||||
|
if len(reviews.Payload) < int(limit) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
allReviews = slices.Concat(allReviews, reviews.Payload)
|
|
||||||
page++
|
page++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,30 +427,6 @@ func (gitea *GiteaTransport) SetRepoOptions(owner, repo string, manual_merge boo
|
|||||||
return ok.Payload, err
|
return ok.Payload, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gitea *GiteaTransport) GetLabels(owner, repo string, idx int64) ([]*models.Label, error) {
|
|
||||||
ret, err := gitea.client.Issue.IssueGetLabels(issue.NewIssueGetLabelsParams().WithOwner(owner).WithRepo(repo).WithIndex(idx), gitea.transport.DefaultAuthentication)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return ret.Payload, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gitea *GiteaTransport) SetLabels(owner, repo string, idx int64, labels []string) ([]*models.Label, error) {
|
|
||||||
interfaceLabels := make([]interface{}, len(labels))
|
|
||||||
for i, l := range labels {
|
|
||||||
interfaceLabels[i] = l
|
|
||||||
}
|
|
||||||
|
|
||||||
ret, err := gitea.client.Issue.IssueAddLabel(issue.NewIssueAddLabelParams().WithOwner(owner).WithRepo(repo).WithIndex(idx).WithBody(&models.IssueLabelsOption{Labels: interfaceLabels}),
|
|
||||||
gitea.transport.DefaultAuthentication)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret.Payload, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
GiteaNotificationType_Pull = "Pull"
|
GiteaNotificationType_Pull = "Pull"
|
||||||
)
|
)
|
||||||
@@ -534,9 +453,6 @@ func (gitea *GiteaTransport) GetNotifications(Type string, since *time.Time) ([]
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(list.Payload) == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
ret = slices.Concat(ret, list.Payload)
|
ret = slices.Concat(ret, list.Payload)
|
||||||
if len(list.Payload) < int(bigLimit) {
|
if len(list.Payload) < int(bigLimit) {
|
||||||
break
|
break
|
||||||
@@ -685,7 +601,7 @@ func (gitea *GiteaTransport) CreateRepositoryIfNotExist(git Git, org, repoName s
|
|||||||
return repo.Payload, nil
|
return repo.Payload, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gitea *GiteaTransport) CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error, bool) {
|
func (gitea *GiteaTransport) CreatePullRequestIfNotExist(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error) {
|
||||||
prOptions := models.CreatePullRequestOption{
|
prOptions := models.CreatePullRequestOption{
|
||||||
Base: targetId,
|
Base: targetId,
|
||||||
Head: srcId,
|
Head: srcId,
|
||||||
@@ -694,14 +610,10 @@ func (gitea *GiteaTransport) CreatePullRequestIfNotExist(repo *models.Repository
|
|||||||
}
|
}
|
||||||
|
|
||||||
if pr, err := gitea.client.Repository.RepoGetPullRequestByBaseHead(
|
if pr, err := gitea.client.Repository.RepoGetPullRequestByBaseHead(
|
||||||
repository.NewRepoGetPullRequestByBaseHeadParams().
|
repository.NewRepoGetPullRequestByBaseHeadParams().WithOwner(repo.Owner.UserName).WithRepo(repo.Name).WithBase(targetId).WithHead(srcId),
|
||||||
WithOwner(repo.Owner.UserName).
|
|
||||||
WithRepo(repo.Name).
|
|
||||||
WithBase(targetId).
|
|
||||||
WithHead(srcId),
|
|
||||||
gitea.transport.DefaultAuthentication,
|
gitea.transport.DefaultAuthentication,
|
||||||
); err == nil && pr.Payload.State == "open" {
|
); err == nil {
|
||||||
return pr.Payload, nil, false
|
return pr.Payload, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
pr, err := gitea.client.Repository.RepoCreatePullRequest(
|
pr, err := gitea.client.Repository.RepoCreatePullRequest(
|
||||||
@@ -715,10 +627,10 @@ func (gitea *GiteaTransport) CreatePullRequestIfNotExist(repo *models.Repository
|
|||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Cannot create pull request. %w", err), true
|
return nil, fmt.Errorf("Cannot create pull request. %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pr.GetPayload(), nil, true
|
return pr.GetPayload(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gitea *GiteaTransport) RequestReviews(pr *models.PullRequest, reviewers ...string) ([]*models.PullReview, error) {
|
func (gitea *GiteaTransport) RequestReviews(pr *models.PullRequest, reviewers ...string) ([]*models.PullReview, error) {
|
||||||
@@ -805,79 +717,39 @@ func (gitea *GiteaTransport) AddComment(pr *models.PullRequest, comment string)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type TimelineCacheData struct {
|
|
||||||
data []*models.TimelineComment
|
|
||||||
lastCheck time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
var giteaTimelineCache map[string]TimelineCacheData = make(map[string]TimelineCacheData)
|
|
||||||
var giteaTimelineCacheMutex sync.RWMutex
|
|
||||||
|
|
||||||
// returns timeline in reverse chronological create order
|
|
||||||
func (gitea *GiteaTransport) GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error) {
|
func (gitea *GiteaTransport) GetTimeline(org, repo string, idx int64) ([]*models.TimelineComment, error) {
|
||||||
page := int64(1)
|
page := int64(1)
|
||||||
resCount := 1
|
resCount := 1
|
||||||
|
|
||||||
prID := fmt.Sprintf("%s/%s!%d", org, repo, idx)
|
retData := []*models.TimelineComment{}
|
||||||
giteaTimelineCacheMutex.RLock()
|
|
||||||
TimelineCache, IsCached := giteaTimelineCache[prID]
|
|
||||||
var LastCachedTime strfmt.DateTime
|
|
||||||
if IsCached {
|
|
||||||
l := len(TimelineCache.data)
|
|
||||||
if l > 0 {
|
|
||||||
LastCachedTime = TimelineCache.data[0].Updated
|
|
||||||
}
|
|
||||||
|
|
||||||
// cache data for 5 seconds
|
|
||||||
if TimelineCache.lastCheck.Add(time.Second*5).Compare(time.Now()) > 0 {
|
|
||||||
giteaTimelineCacheMutex.RUnlock()
|
|
||||||
return TimelineCache.data, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
giteaTimelineCacheMutex.RUnlock()
|
|
||||||
|
|
||||||
giteaTimelineCacheMutex.Lock()
|
|
||||||
defer giteaTimelineCacheMutex.Unlock()
|
|
||||||
|
|
||||||
for resCount > 0 {
|
for resCount > 0 {
|
||||||
opts := issue.NewIssueGetCommentsAndTimelineParams().WithOwner(org).WithRepo(repo).WithIndex(idx).WithPage(&page)
|
res, err := gitea.client.Issue.IssueGetCommentsAndTimeline(
|
||||||
if !LastCachedTime.IsZero() {
|
issue.NewIssueGetCommentsAndTimelineParams().
|
||||||
opts = opts.WithSince(&LastCachedTime)
|
WithOwner(org).
|
||||||
}
|
WithRepo(repo).
|
||||||
res, err := gitea.client.Issue.IssueGetCommentsAndTimeline(opts, gitea.transport.DefaultAuthentication)
|
WithIndex(idx).
|
||||||
|
WithPage(&page),
|
||||||
|
gitea.transport.DefaultAuthentication,
|
||||||
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if resCount = len(res.Payload); resCount == 0 {
|
resCount = len(res.Payload)
|
||||||
break
|
LogDebug("page:", page, "len:", resCount)
|
||||||
}
|
|
||||||
|
|
||||||
for _, d := range res.Payload {
|
|
||||||
if d != nil {
|
|
||||||
if time.Time(d.Created).Compare(time.Time(LastCachedTime)) > 0 {
|
|
||||||
// created after last check, so we append here
|
|
||||||
TimelineCache.data = append(TimelineCache.data, d)
|
|
||||||
} else {
|
|
||||||
// we need something updated in the timeline, maybe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if resCount < 10 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
page++
|
page++
|
||||||
|
|
||||||
|
retData = append(retData, res.Payload...)
|
||||||
}
|
}
|
||||||
LogDebug("timeline", prID, "# timeline:", len(TimelineCache.data))
|
LogDebug("total results:", len(retData))
|
||||||
slices.SortFunc(TimelineCache.data, func(a, b *models.TimelineComment) int {
|
|
||||||
|
slices.SortFunc(retData, func(a, b *models.TimelineComment) int {
|
||||||
return time.Time(b.Created).Compare(time.Time(a.Created))
|
return time.Time(b.Created).Compare(time.Time(a.Created))
|
||||||
})
|
})
|
||||||
|
|
||||||
TimelineCache.lastCheck = time.Now()
|
return retData, nil
|
||||||
giteaTimelineCache[prID] = TimelineCache
|
|
||||||
|
|
||||||
return TimelineCache.data, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gitea *GiteaTransport) GetRepositoryFileContent(org, repo, hash, path string) ([]byte, string, error) {
|
func (gitea *GiteaTransport) GetRepositoryFileContent(org, repo, hash, path string) ([]byte, string, error) {
|
||||||
|
|||||||
324
common/listen.go
Normal file
324
common/listen.go
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Autogits.
|
||||||
|
*
|
||||||
|
* Copyright © 2024 SUSE LLC
|
||||||
|
*
|
||||||
|
* Autogits is free software: you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU General Public License as published by the Free Software
|
||||||
|
* Foundation, either version 2 of the License, or (at your option) any later
|
||||||
|
* version.
|
||||||
|
*
|
||||||
|
* Autogits is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
||||||
|
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with
|
||||||
|
* Foobar. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"runtime/debug"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
rabbitmq "github.com/rabbitmq/amqp091-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
const RequestType_CreateBrachTag = "create"
|
||||||
|
const RequestType_DeleteBranchTag = "delete"
|
||||||
|
const RequestType_Fork = "fork"
|
||||||
|
const RequestType_Issue = "issues"
|
||||||
|
const RequestType_IssueAssign = "issue_assign"
|
||||||
|
const RequestType_IssueComment = "issue_comment"
|
||||||
|
const RequestType_IssueLabel = "issue_label"
|
||||||
|
const RequestType_IssueMilestone = "issue_milestone"
|
||||||
|
const RequestType_Push = "push"
|
||||||
|
const RequestType_Repository = "repository"
|
||||||
|
const RequestType_Release = "release"
|
||||||
|
const RequestType_PR = "pull_request"
|
||||||
|
const RequestType_PRAssign = "pull_request_assign"
|
||||||
|
const RequestType_PRLabel = "pull_request_label"
|
||||||
|
const RequestType_PRComment = "pull_request_comment"
|
||||||
|
const RequestType_PRMilestone = "pull_request_milestone"
|
||||||
|
const RequestType_PRSync = "pull_request_sync"
|
||||||
|
const RequestType_PRReviewAccepted = "pull_request_review_approved"
|
||||||
|
const RequestType_PRReviewRejected = "pull_request_review_rejected"
|
||||||
|
const RequestType_PRReviewRequest = "pull_request_review_request"
|
||||||
|
const RequestType_PRReviewComment = "pull_request_review_comment"
|
||||||
|
const RequestType_Wiki = "wiki"
|
||||||
|
|
||||||
|
type RequestProcessor interface {
|
||||||
|
ProcessFunc(*Request) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListenDefinitions struct {
|
||||||
|
RabbitURL *url.URL // amqps://user:password@host/queue
|
||||||
|
|
||||||
|
GitAuthor string
|
||||||
|
Handlers map[string]RequestProcessor
|
||||||
|
Orgs []string
|
||||||
|
|
||||||
|
topics []string
|
||||||
|
topicSubChanges chan string // +topic = subscribe, -topic = unsubscribe
|
||||||
|
}
|
||||||
|
|
||||||
|
type RabbitMessage rabbitmq.Delivery
|
||||||
|
|
||||||
|
func (l *ListenDefinitions) processTopicChanges(ch *rabbitmq.Channel, queueName string) {
|
||||||
|
for {
|
||||||
|
topic, ok := <-l.topicSubChanges
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
LogDebug(" topic change:", topic)
|
||||||
|
switch topic[0] {
|
||||||
|
case '+':
|
||||||
|
if err := ch.QueueBind(queueName, topic[1:], "pubsub", false, nil); err != nil {
|
||||||
|
LogError(err)
|
||||||
|
}
|
||||||
|
case '-':
|
||||||
|
if err := ch.QueueUnbind(queueName, topic[1:], "pubsub", nil); err != nil {
|
||||||
|
LogError(err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
LogInfo("Ignoring unknown topic change:", topic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ListenDefinitions) processRabbitMQ(msgCh chan<- RabbitMessage) error {
|
||||||
|
queueName := l.RabbitURL.Path
|
||||||
|
l.RabbitURL.Path = ""
|
||||||
|
|
||||||
|
if len(queueName) > 0 && queueName[0] == '/' {
|
||||||
|
queueName = queueName[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
connection, err := rabbitmq.DialTLS(l.RabbitURL.String(), &tls.Config{
|
||||||
|
ServerName: l.RabbitURL.Hostname(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Cannot connect to %s . Err: %w", l.RabbitURL.Hostname(), err)
|
||||||
|
}
|
||||||
|
defer connection.Close()
|
||||||
|
|
||||||
|
ch, err := connection.Channel()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Cannot create a channel. Err: %w", err)
|
||||||
|
}
|
||||||
|
defer ch.Close()
|
||||||
|
|
||||||
|
if err = ch.ExchangeDeclarePassive("pubsub", "topic", true, false, false, false, nil); err != nil {
|
||||||
|
return fmt.Errorf("Cannot find pubsub exchange? Err: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var q rabbitmq.Queue
|
||||||
|
if len(queueName) == 0 {
|
||||||
|
q, err = ch.QueueDeclare("", false, true, true, false, nil)
|
||||||
|
} else {
|
||||||
|
q, err = ch.QueueDeclarePassive(queueName, true, false, true, false, nil)
|
||||||
|
if err != nil {
|
||||||
|
LogInfo("queue not found .. trying to create it:", err)
|
||||||
|
if ch.IsClosed() {
|
||||||
|
ch, err = connection.Channel()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Channel cannot be re-opened. Err: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
q, err = ch.QueueDeclare(queueName, true, false, true, false, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
LogInfo("can't create persistent queue ... falling back to temporaty queue:", err)
|
||||||
|
if ch.IsClosed() {
|
||||||
|
ch, err = connection.Channel()
|
||||||
|
return fmt.Errorf("Channel cannot be re-opened. Err: %w", err)
|
||||||
|
}
|
||||||
|
q, err = ch.QueueDeclare("", false, true, true, false, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Cannot declare queue. Err: %w", err)
|
||||||
|
}
|
||||||
|
// log.Printf("queue: %s:%d", q.Name, q.Consumers)
|
||||||
|
|
||||||
|
LogDebug(" -- listening to topics:")
|
||||||
|
l.topicSubChanges = make(chan string)
|
||||||
|
defer close(l.topicSubChanges)
|
||||||
|
go l.processTopicChanges(ch, q.Name)
|
||||||
|
|
||||||
|
for _, topic := range l.topics {
|
||||||
|
l.topicSubChanges <- "+" + topic
|
||||||
|
}
|
||||||
|
|
||||||
|
msgs, err := ch.Consume(q.Name, "", true, true, false, false, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Cannot start consumer. Err: %w", err)
|
||||||
|
}
|
||||||
|
// log.Printf("queue: %s:%d", q.Name, q.Consumers)
|
||||||
|
|
||||||
|
for {
|
||||||
|
msg, ok := <-msgs
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("channel/connection closed?\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
msgCh <- RabbitMessage(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ListenDefinitions) connectAndProcessRabbitMQ(ch chan<- RabbitMessage) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
LogError(r)
|
||||||
|
LogError("'crash' RabbitMQ worker. Recovering... reconnecting...")
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
go l.connectAndProcessRabbitMQ(ch)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
err := l.processRabbitMQ(ch)
|
||||||
|
if err != nil {
|
||||||
|
LogError("Error in RabbitMQ connection. %#v", err)
|
||||||
|
LogInfo("Reconnecting in 2 seconds...")
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ListenDefinitions) connectToRabbitMQ() chan RabbitMessage {
|
||||||
|
ch := make(chan RabbitMessage, 100)
|
||||||
|
go l.connectAndProcessRabbitMQ(ch)
|
||||||
|
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProcessEvent(f RequestProcessor, request *Request) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
LogError("panic caught")
|
||||||
|
if err, ok := r.(error); !ok {
|
||||||
|
LogError(err)
|
||||||
|
}
|
||||||
|
LogError(string(debug.Stack()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := f.ProcessFunc(request); err != nil {
|
||||||
|
LogError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ListenDefinitions) generateTopics() []string {
|
||||||
|
topics := make([]string, 0, len(l.Handlers)*len(l.Orgs))
|
||||||
|
scope := "suse"
|
||||||
|
if l.RabbitURL.Hostname() == "rabbit.opensuse.org" {
|
||||||
|
scope = "opensuse"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, org := range l.Orgs {
|
||||||
|
for requestType, _ := range l.Handlers {
|
||||||
|
topics = append(topics, fmt.Sprintf("%s.src.%s.%s.#", scope, org, requestType))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.Sort(topics)
|
||||||
|
return slices.Compact(topics)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ListenDefinitions) UpdateTopics() {
|
||||||
|
newTopics := l.generateTopics()
|
||||||
|
|
||||||
|
j := 0
|
||||||
|
next_new_topic:
|
||||||
|
for i := 0; i < len(newTopics); i++ {
|
||||||
|
topic := newTopics[i]
|
||||||
|
|
||||||
|
for j < len(l.topics) {
|
||||||
|
cmp := strings.Compare(topic, l.topics[j])
|
||||||
|
|
||||||
|
if cmp == 0 {
|
||||||
|
j++
|
||||||
|
continue next_new_topic
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmp < 0 {
|
||||||
|
l.topicSubChanges <- "+" + topic
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
l.topicSubChanges <- "-" + l.topics[j]
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
|
||||||
|
if j == len(l.topics) {
|
||||||
|
l.topicSubChanges <- "+" + topic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for j < len(l.topics) {
|
||||||
|
l.topicSubChanges <- "-" + l.topics[j]
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
|
||||||
|
l.topics = newTopics
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ListenDefinitions) ProcessRabbitMQEvents() error {
|
||||||
|
LogInfo("RabbitMQ connection:", l.RabbitURL.String())
|
||||||
|
LogDebug("# Handlers:", len(l.Handlers))
|
||||||
|
LogDebug("# Orgs:", len(l.Orgs))
|
||||||
|
|
||||||
|
l.RabbitURL.User = url.UserPassword(rabbitUser, rabbitPassword)
|
||||||
|
l.topics = l.generateTopics()
|
||||||
|
ch := l.connectToRabbitMQ()
|
||||||
|
|
||||||
|
for {
|
||||||
|
msg, ok := <-ch
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
LogDebug("event:", msg.RoutingKey)
|
||||||
|
|
||||||
|
route := strings.Split(msg.RoutingKey, ".")
|
||||||
|
if len(route) > 3 {
|
||||||
|
reqType := route[3]
|
||||||
|
org := route[2]
|
||||||
|
|
||||||
|
if !slices.Contains(l.Orgs, org) {
|
||||||
|
LogInfo("Got event for unhandeled org:", org)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
LogDebug("org:", org, "type:", reqType)
|
||||||
|
if handler, found := l.Handlers[reqType]; found {
|
||||||
|
/* h, err := CreateRequestHandler()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Cannot create request handler", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
req, err := ParseRequestJSON(reqType, msg.Body)
|
||||||
|
if err != nil {
|
||||||
|
LogError("Error parsing request JSON:", err)
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
LogDebug("processing req", req.Type)
|
||||||
|
// h.Request = req
|
||||||
|
ProcessEvent(handler, req)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,13 +50,11 @@ func TestListenDefinitionsTopicUpdate(t *testing.T) {
|
|||||||
u, _ := url.Parse("amqps://rabbit.example.com")
|
u, _ := url.Parse("amqps://rabbit.example.com")
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
l := &RabbitMQGiteaEventsProcessor{
|
l := ListenDefinitions{
|
||||||
Orgs: test.orgs1,
|
Orgs: test.orgs1,
|
||||||
Handlers: make(map[string]RequestProcessor),
|
Handlers: make(map[string]RequestProcessor),
|
||||||
c: &RabbitConnection{
|
topicSubChanges: make(chan string, len(test.topicDelta)*10),
|
||||||
RabbitURL: u,
|
RabbitURL: u,
|
||||||
topicSubChanges: make(chan string, len(test.topicDelta)*10),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
slices.Sort(test.topicDelta)
|
slices.Sort(test.topicDelta)
|
||||||
@@ -66,11 +64,11 @@ func TestListenDefinitionsTopicUpdate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
changes := []string{}
|
changes := []string{}
|
||||||
l.c.UpdateTopics(l)
|
l.UpdateTopics()
|
||||||
a:
|
a:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case c := <-l.c.topicSubChanges:
|
case c := <-l.topicSubChanges:
|
||||||
changes = append(changes, c)
|
changes = append(changes, c)
|
||||||
default:
|
default:
|
||||||
changes = []string{}
|
changes = []string{}
|
||||||
@@ -80,13 +78,13 @@ func TestListenDefinitionsTopicUpdate(t *testing.T) {
|
|||||||
|
|
||||||
l.Orgs = test.orgs2
|
l.Orgs = test.orgs2
|
||||||
|
|
||||||
l.c.UpdateTopics(l)
|
l.UpdateTopics()
|
||||||
changes = []string{}
|
changes = []string{}
|
||||||
|
|
||||||
b:
|
b:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case c := <-l.c.topicSubChanges:
|
case c := <-l.topicSubChanges:
|
||||||
changes = append(changes, c)
|
changes = append(changes, c)
|
||||||
default:
|
default:
|
||||||
slices.Sort(changes)
|
slices.Sort(changes)
|
||||||
@@ -63,10 +63,6 @@ func SetLoggingLevel(ll LogLevel) {
|
|||||||
logLevel = ll
|
logLevel = ll
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetLoggingLevel() LogLevel {
|
|
||||||
return logLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetLoggingLevelFromString(ll string) error {
|
func SetLoggingLevelFromString(ll string) error {
|
||||||
switch ll {
|
switch ll {
|
||||||
case "info":
|
case "info":
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ import (
|
|||||||
//go:generate mockgen -source=maintainership.go -destination=mock/maintainership.go -typed
|
//go:generate mockgen -source=maintainership.go -destination=mock/maintainership.go -typed
|
||||||
|
|
||||||
type MaintainershipData interface {
|
type MaintainershipData interface {
|
||||||
ListProjectMaintainers(OptionalGroupExpansion []*ReviewGroup) []string
|
ListProjectMaintainers() []string
|
||||||
ListPackageMaintainers(Pkg string, OptionalGroupExpasion []*ReviewGroup) []string
|
ListPackageMaintainers(pkg string) []string
|
||||||
|
|
||||||
IsApproved(Pkg string, Reviews []*models.PullReview, Submitter string, ReviewGroups []*ReviewGroup) bool
|
IsApproved(pkg string, reviews []*models.PullReview, submitter string) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProjectKey = ""
|
const ProjectKey = ""
|
||||||
@@ -70,7 +70,7 @@ func FetchProjectMaintainershipData(gitea GiteaMaintainershipReader, org, prjGit
|
|||||||
return m, err
|
return m, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (data *MaintainershipMap) ListProjectMaintainers(groups []*ReviewGroup) []string {
|
func (data *MaintainershipMap) ListProjectMaintainers() []string {
|
||||||
if data == nil {
|
if data == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -80,11 +80,6 @@ func (data *MaintainershipMap) ListProjectMaintainers(groups []*ReviewGroup) []s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// expands groups
|
|
||||||
for _, g := range groups {
|
|
||||||
m = g.ExpandMaintainers(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +96,7 @@ func parsePkgDirData(pkg string, data []byte) []string {
|
|||||||
return pkgMaintainers
|
return pkgMaintainers
|
||||||
}
|
}
|
||||||
|
|
||||||
func (data *MaintainershipMap) ListPackageMaintainers(pkg string, groups []*ReviewGroup) []string {
|
func (data *MaintainershipMap) ListPackageMaintainers(pkg string) []string {
|
||||||
if data == nil {
|
if data == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -116,7 +111,7 @@ func (data *MaintainershipMap) ListPackageMaintainers(pkg string, groups []*Revi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prjMaintainers := data.ListProjectMaintainers(nil)
|
prjMaintainers := data.ListProjectMaintainers()
|
||||||
|
|
||||||
prjMaintainer:
|
prjMaintainer:
|
||||||
for _, prjm := range prjMaintainers {
|
for _, prjm := range prjMaintainers {
|
||||||
@@ -128,20 +123,15 @@ prjMaintainer:
|
|||||||
pkgMaintainers = append(pkgMaintainers, prjm)
|
pkgMaintainers = append(pkgMaintainers, prjm)
|
||||||
}
|
}
|
||||||
|
|
||||||
// expands groups
|
|
||||||
for _, g := range groups {
|
|
||||||
pkgMaintainers = g.ExpandMaintainers(pkgMaintainers)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pkgMaintainers
|
return pkgMaintainers
|
||||||
}
|
}
|
||||||
|
|
||||||
func (data *MaintainershipMap) IsApproved(pkg string, reviews []*models.PullReview, submitter string, groups []*ReviewGroup) bool {
|
func (data *MaintainershipMap) IsApproved(pkg string, reviews []*models.PullReview, submitter string) bool {
|
||||||
var reviewers []string
|
var reviewers []string
|
||||||
if pkg != ProjectKey {
|
if pkg != ProjectKey {
|
||||||
reviewers = data.ListPackageMaintainers(pkg, groups)
|
reviewers = data.ListPackageMaintainers(pkg)
|
||||||
} else {
|
} else {
|
||||||
reviewers = data.ListProjectMaintainers(groups)
|
reviewers = data.ListProjectMaintainers()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(reviewers) == 0 {
|
if len(reviewers) == 0 {
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ func TestMaintainership(t *testing.T) {
|
|||||||
maintainersFile []byte
|
maintainersFile []byte
|
||||||
maintainersFileErr error
|
maintainersFileErr error
|
||||||
|
|
||||||
groups []*common.ReviewGroup
|
|
||||||
|
|
||||||
maintainersDir map[string][]byte
|
maintainersDir map[string][]byte
|
||||||
}{
|
}{
|
||||||
/* PACKAGE MAINTAINERS */
|
/* PACKAGE MAINTAINERS */
|
||||||
@@ -53,22 +51,6 @@ func TestMaintainership(t *testing.T) {
|
|||||||
maintainers: []string{"user1", "user2", "user3"},
|
maintainers: []string{"user1", "user2", "user3"},
|
||||||
packageName: "pkg",
|
packageName: "pkg",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "Multiple package maintainers and groups",
|
|
||||||
maintainersFile: []byte(`{"pkg": ["user1", "user2", "g2"], "": ["g2", "user1", "user3"]}`),
|
|
||||||
maintainersDir: map[string][]byte{
|
|
||||||
"_project": []byte(`{"": ["user1", "user3", "g2"]}`),
|
|
||||||
"pkg": []byte(`{"pkg": ["user1", "g2", "user2"]}`),
|
|
||||||
},
|
|
||||||
maintainers: []string{"user1", "user2", "user3", "user5"},
|
|
||||||
packageName: "pkg",
|
|
||||||
groups: []*common.ReviewGroup{
|
|
||||||
{
|
|
||||||
Name: "g2",
|
|
||||||
Reviewers: []string{"user1", "user5"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "No package maintainers and only project maintainer",
|
name: "No package maintainers and only project maintainer",
|
||||||
maintainersFile: []byte(`{"pkg2": ["user1", "user2"], "": ["user1", "user3"]}`),
|
maintainersFile: []byte(`{"pkg2": ["user1", "user2"], "": ["user1", "user3"]}`),
|
||||||
@@ -156,9 +138,9 @@ func TestMaintainership(t *testing.T) {
|
|||||||
|
|
||||||
var m []string
|
var m []string
|
||||||
if len(test.packageName) > 0 {
|
if len(test.packageName) > 0 {
|
||||||
m = maintainers.ListPackageMaintainers(test.packageName, test.groups)
|
m = maintainers.ListPackageMaintainers(test.packageName)
|
||||||
} else {
|
} else {
|
||||||
m = maintainers.ListProjectMaintainers(test.groups)
|
m = maintainers.ListProjectMaintainers()
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(m) != len(test.maintainers) {
|
if len(m) != len(test.maintainers) {
|
||||||
@@ -225,7 +207,7 @@ func TestMaintainershipFileWrite(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "2 project maintainers and 2 single package maintainers",
|
name: "2 project maintainers and 2 single package maintainers",
|
||||||
maintainers: map[string][]string{
|
maintainers: map[string][]string{
|
||||||
"": {"two", "one"},
|
"": {"two", "one"},
|
||||||
"pkg1": {},
|
"pkg1": {},
|
||||||
"foo": {"four", "byte"},
|
"foo": {"four", "byte"},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Manifest struct {
|
|
||||||
Subdirectories []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manifest) SubdirForPackage(pkg string) string {
|
|
||||||
if m == nil {
|
|
||||||
return pkg
|
|
||||||
}
|
|
||||||
|
|
||||||
idx := -1
|
|
||||||
matchLen := 0
|
|
||||||
basePkg := path.Base(pkg)
|
|
||||||
lowercasePkg := strings.ToLower(basePkg)
|
|
||||||
|
|
||||||
for i, sub := range m.Subdirectories {
|
|
||||||
basename := strings.ToLower(path.Base(sub))
|
|
||||||
if strings.HasPrefix(lowercasePkg, basename) && matchLen < len(basename) {
|
|
||||||
idx = i
|
|
||||||
matchLen = len(basename)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if idx > -1 {
|
|
||||||
return path.Join(m.Subdirectories[idx], basePkg)
|
|
||||||
}
|
|
||||||
return pkg
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadManifestFile(filename string) (*Manifest, error) {
|
|
||||||
data, err := os.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ParseManifestFile(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseManifestFile(data []byte) (*Manifest, error) {
|
|
||||||
ret := &Manifest{}
|
|
||||||
err := yaml.Unmarshal(data, ret)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package common_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"src.opensuse.org/autogits/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestManifestSubdirAssignments(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
Name string
|
|
||||||
ManifestContent string
|
|
||||||
Packages []string
|
|
||||||
ManifestLocations []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "empty manifest",
|
|
||||||
Packages: []string{"atom", "blarg", "Foobar", "X-Ray", "boost", "NodeJS"},
|
|
||||||
ManifestLocations: []string{"atom", "blarg", "Foobar", "X-Ray", "boost", "NodeJS"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "only few subdirs manifest",
|
|
||||||
ManifestContent: "subdirectories:\n - a\n - b",
|
|
||||||
Packages: []string{"atom", "blarg", "Foobar", "X-Ray", "Boost", "NodeJS"},
|
|
||||||
ManifestLocations: []string{"a/atom", "b/blarg", "Foobar", "X-Ray", "b/Boost", "NodeJS"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "multilayer subdirs manifest",
|
|
||||||
ManifestContent: "subdirectories:\n - a\n - b\n - libs/boo",
|
|
||||||
Packages: []string{"atom", "blarg", "Foobar", "X-Ray", "Boost", "NodeJS"},
|
|
||||||
ManifestLocations: []string{"a/atom", "b/blarg", "Foobar", "X-Ray", "libs/boo/Boost", "NodeJS"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "multilayer subdirs manifest with trailing /",
|
|
||||||
ManifestContent: "subdirectories:\n - a\n - b\n - libs/boo/\n - somedir/Node/",
|
|
||||||
Packages: []string{"atom", "blarg", "Foobar", "X-Ray", "Boost", "NodeJS", "foobar/node2"},
|
|
||||||
ManifestLocations: []string{"a/atom", "b/blarg", "Foobar", "X-Ray", "libs/boo/Boost", "somedir/Node/NodeJS", "somedir/Node/node2"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.Name, func(t *testing.T) {
|
|
||||||
m, err := common.ParseManifestFile([]byte(test.ManifestContent))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, pkg := range test.Packages {
|
|
||||||
expected := test.ManifestLocations[i]
|
|
||||||
if l := m.SubdirForPackage(pkg); l != expected {
|
|
||||||
t.Error("Expected:", expected, "but got:", l)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
// Code generated by MockGen. DO NOT EDIT.
|
|
||||||
// Source: config.go
|
|
||||||
//
|
|
||||||
// Generated by this command:
|
|
||||||
//
|
|
||||||
// mockgen -source=config.go -destination=mock/config.go -typed
|
|
||||||
//
|
|
||||||
|
|
||||||
// Package mock_common is a generated GoMock package.
|
|
||||||
package mock_common
|
|
||||||
|
|
||||||
import (
|
|
||||||
reflect "reflect"
|
|
||||||
|
|
||||||
gomock "go.uber.org/mock/gomock"
|
|
||||||
models "src.opensuse.org/autogits/common/gitea-generated/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockGiteaFileContentAndRepoFetcher is a mock of GiteaFileContentAndRepoFetcher interface.
|
|
||||||
type MockGiteaFileContentAndRepoFetcher struct {
|
|
||||||
ctrl *gomock.Controller
|
|
||||||
recorder *MockGiteaFileContentAndRepoFetcherMockRecorder
|
|
||||||
isgomock struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockGiteaFileContentAndRepoFetcherMockRecorder is the mock recorder for MockGiteaFileContentAndRepoFetcher.
|
|
||||||
type MockGiteaFileContentAndRepoFetcherMockRecorder struct {
|
|
||||||
mock *MockGiteaFileContentAndRepoFetcher
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockGiteaFileContentAndRepoFetcher creates a new mock instance.
|
|
||||||
func NewMockGiteaFileContentAndRepoFetcher(ctrl *gomock.Controller) *MockGiteaFileContentAndRepoFetcher {
|
|
||||||
mock := &MockGiteaFileContentAndRepoFetcher{ctrl: ctrl}
|
|
||||||
mock.recorder = &MockGiteaFileContentAndRepoFetcherMockRecorder{mock}
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
|
||||||
func (m *MockGiteaFileContentAndRepoFetcher) EXPECT() *MockGiteaFileContentAndRepoFetcherMockRecorder {
|
|
||||||
return m.recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepository mocks base method.
|
|
||||||
func (m *MockGiteaFileContentAndRepoFetcher) GetRepository(org, repo string) (*models.Repository, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetRepository", org, repo)
|
|
||||||
ret0, _ := ret[0].(*models.Repository)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepository indicates an expected call of GetRepository.
|
|
||||||
func (mr *MockGiteaFileContentAndRepoFetcherMockRecorder) GetRepository(org, repo any) *MockGiteaFileContentAndRepoFetcherGetRepositoryCall {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepository", reflect.TypeOf((*MockGiteaFileContentAndRepoFetcher)(nil).GetRepository), org, repo)
|
|
||||||
return &MockGiteaFileContentAndRepoFetcherGetRepositoryCall{Call: call}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockGiteaFileContentAndRepoFetcherGetRepositoryCall wrap *gomock.Call
|
|
||||||
type MockGiteaFileContentAndRepoFetcherGetRepositoryCall struct {
|
|
||||||
*gomock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return rewrite *gomock.Call.Return
|
|
||||||
func (c *MockGiteaFileContentAndRepoFetcherGetRepositoryCall) Return(arg0 *models.Repository, arg1 error) *MockGiteaFileContentAndRepoFetcherGetRepositoryCall {
|
|
||||||
c.Call = c.Call.Return(arg0, arg1)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do rewrite *gomock.Call.Do
|
|
||||||
func (c *MockGiteaFileContentAndRepoFetcherGetRepositoryCall) Do(f func(string, string) (*models.Repository, error)) *MockGiteaFileContentAndRepoFetcherGetRepositoryCall {
|
|
||||||
c.Call = c.Call.Do(f)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
|
||||||
func (c *MockGiteaFileContentAndRepoFetcherGetRepositoryCall) DoAndReturn(f func(string, string) (*models.Repository, error)) *MockGiteaFileContentAndRepoFetcherGetRepositoryCall {
|
|
||||||
c.Call = c.Call.DoAndReturn(f)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepositoryFileContent mocks base method.
|
|
||||||
func (m *MockGiteaFileContentAndRepoFetcher) GetRepositoryFileContent(org, repo, hash, path string) ([]byte, string, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "GetRepositoryFileContent", org, repo, hash, path)
|
|
||||||
ret0, _ := ret[0].([]byte)
|
|
||||||
ret1, _ := ret[1].(string)
|
|
||||||
ret2, _ := ret[2].(error)
|
|
||||||
return ret0, ret1, ret2
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepositoryFileContent indicates an expected call of GetRepositoryFileContent.
|
|
||||||
func (mr *MockGiteaFileContentAndRepoFetcherMockRecorder) GetRepositoryFileContent(org, repo, hash, path any) *MockGiteaFileContentAndRepoFetcherGetRepositoryFileContentCall {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRepositoryFileContent", reflect.TypeOf((*MockGiteaFileContentAndRepoFetcher)(nil).GetRepositoryFileContent), org, repo, hash, path)
|
|
||||||
return &MockGiteaFileContentAndRepoFetcherGetRepositoryFileContentCall{Call: call}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockGiteaFileContentAndRepoFetcherGetRepositoryFileContentCall wrap *gomock.Call
|
|
||||||
type MockGiteaFileContentAndRepoFetcherGetRepositoryFileContentCall struct {
|
|
||||||
*gomock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return rewrite *gomock.Call.Return
|
|
||||||
func (c *MockGiteaFileContentAndRepoFetcherGetRepositoryFileContentCall) Return(arg0 []byte, arg1 string, arg2 error) *MockGiteaFileContentAndRepoFetcherGetRepositoryFileContentCall {
|
|
||||||
c.Call = c.Call.Return(arg0, arg1, arg2)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do rewrite *gomock.Call.Do
|
|
||||||
func (c *MockGiteaFileContentAndRepoFetcherGetRepositoryFileContentCall) Do(f func(string, string, string, string) ([]byte, string, error)) *MockGiteaFileContentAndRepoFetcherGetRepositoryFileContentCall {
|
|
||||||
c.Call = c.Call.Do(f)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
|
||||||
func (c *MockGiteaFileContentAndRepoFetcherGetRepositoryFileContentCall) DoAndReturn(f func(string, string, string, string) ([]byte, string, error)) *MockGiteaFileContentAndRepoFetcherGetRepositoryFileContentCall {
|
|
||||||
c.Call = c.Call.DoAndReturn(f)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,156 +0,0 @@
|
|||||||
// Code generated by MockGen. DO NOT EDIT.
|
|
||||||
// Source: maintainership.go
|
|
||||||
//
|
|
||||||
// Generated by this command:
|
|
||||||
//
|
|
||||||
// mockgen -source=maintainership.go -destination=mock/maintainership.go -typed
|
|
||||||
//
|
|
||||||
|
|
||||||
// Package mock_common is a generated GoMock package.
|
|
||||||
package mock_common
|
|
||||||
|
|
||||||
import (
|
|
||||||
reflect "reflect"
|
|
||||||
|
|
||||||
gomock "go.uber.org/mock/gomock"
|
|
||||||
common "src.opensuse.org/autogits/common"
|
|
||||||
models "src.opensuse.org/autogits/common/gitea-generated/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockMaintainershipData is a mock of MaintainershipData interface.
|
|
||||||
type MockMaintainershipData struct {
|
|
||||||
ctrl *gomock.Controller
|
|
||||||
recorder *MockMaintainershipDataMockRecorder
|
|
||||||
isgomock struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockMaintainershipDataMockRecorder is the mock recorder for MockMaintainershipData.
|
|
||||||
type MockMaintainershipDataMockRecorder struct {
|
|
||||||
mock *MockMaintainershipData
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockMaintainershipData creates a new mock instance.
|
|
||||||
func NewMockMaintainershipData(ctrl *gomock.Controller) *MockMaintainershipData {
|
|
||||||
mock := &MockMaintainershipData{ctrl: ctrl}
|
|
||||||
mock.recorder = &MockMaintainershipDataMockRecorder{mock}
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
|
||||||
func (m *MockMaintainershipData) EXPECT() *MockMaintainershipDataMockRecorder {
|
|
||||||
return m.recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsApproved mocks base method.
|
|
||||||
func (m *MockMaintainershipData) IsApproved(Pkg string, Reviews []*models.PullReview, Submitter string, ReviewGroups []*common.ReviewGroup) bool {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "IsApproved", Pkg, Reviews, Submitter, ReviewGroups)
|
|
||||||
ret0, _ := ret[0].(bool)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsApproved indicates an expected call of IsApproved.
|
|
||||||
func (mr *MockMaintainershipDataMockRecorder) IsApproved(Pkg, Reviews, Submitter, ReviewGroups any) *MockMaintainershipDataIsApprovedCall {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsApproved", reflect.TypeOf((*MockMaintainershipData)(nil).IsApproved), Pkg, Reviews, Submitter, ReviewGroups)
|
|
||||||
return &MockMaintainershipDataIsApprovedCall{Call: call}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockMaintainershipDataIsApprovedCall wrap *gomock.Call
|
|
||||||
type MockMaintainershipDataIsApprovedCall struct {
|
|
||||||
*gomock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return rewrite *gomock.Call.Return
|
|
||||||
func (c *MockMaintainershipDataIsApprovedCall) Return(arg0 bool) *MockMaintainershipDataIsApprovedCall {
|
|
||||||
c.Call = c.Call.Return(arg0)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do rewrite *gomock.Call.Do
|
|
||||||
func (c *MockMaintainershipDataIsApprovedCall) Do(f func(string, []*models.PullReview, string, []*common.ReviewGroup) bool) *MockMaintainershipDataIsApprovedCall {
|
|
||||||
c.Call = c.Call.Do(f)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
|
||||||
func (c *MockMaintainershipDataIsApprovedCall) DoAndReturn(f func(string, []*models.PullReview, string, []*common.ReviewGroup) bool) *MockMaintainershipDataIsApprovedCall {
|
|
||||||
c.Call = c.Call.DoAndReturn(f)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListPackageMaintainers mocks base method.
|
|
||||||
func (m *MockMaintainershipData) ListPackageMaintainers(Pkg string, OptionalGroupExpasion []*common.ReviewGroup) []string {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "ListPackageMaintainers", Pkg, OptionalGroupExpasion)
|
|
||||||
ret0, _ := ret[0].([]string)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListPackageMaintainers indicates an expected call of ListPackageMaintainers.
|
|
||||||
func (mr *MockMaintainershipDataMockRecorder) ListPackageMaintainers(Pkg, OptionalGroupExpasion any) *MockMaintainershipDataListPackageMaintainersCall {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPackageMaintainers", reflect.TypeOf((*MockMaintainershipData)(nil).ListPackageMaintainers), Pkg, OptionalGroupExpasion)
|
|
||||||
return &MockMaintainershipDataListPackageMaintainersCall{Call: call}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockMaintainershipDataListPackageMaintainersCall wrap *gomock.Call
|
|
||||||
type MockMaintainershipDataListPackageMaintainersCall struct {
|
|
||||||
*gomock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return rewrite *gomock.Call.Return
|
|
||||||
func (c *MockMaintainershipDataListPackageMaintainersCall) Return(arg0 []string) *MockMaintainershipDataListPackageMaintainersCall {
|
|
||||||
c.Call = c.Call.Return(arg0)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do rewrite *gomock.Call.Do
|
|
||||||
func (c *MockMaintainershipDataListPackageMaintainersCall) Do(f func(string, []*common.ReviewGroup) []string) *MockMaintainershipDataListPackageMaintainersCall {
|
|
||||||
c.Call = c.Call.Do(f)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
|
||||||
func (c *MockMaintainershipDataListPackageMaintainersCall) DoAndReturn(f func(string, []*common.ReviewGroup) []string) *MockMaintainershipDataListPackageMaintainersCall {
|
|
||||||
c.Call = c.Call.DoAndReturn(f)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListProjectMaintainers mocks base method.
|
|
||||||
func (m *MockMaintainershipData) ListProjectMaintainers(OptionalGroupExpansion []*common.ReviewGroup) []string {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "ListProjectMaintainers", OptionalGroupExpansion)
|
|
||||||
ret0, _ := ret[0].([]string)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListProjectMaintainers indicates an expected call of ListProjectMaintainers.
|
|
||||||
func (mr *MockMaintainershipDataMockRecorder) ListProjectMaintainers(OptionalGroupExpansion any) *MockMaintainershipDataListProjectMaintainersCall {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListProjectMaintainers", reflect.TypeOf((*MockMaintainershipData)(nil).ListProjectMaintainers), OptionalGroupExpansion)
|
|
||||||
return &MockMaintainershipDataListProjectMaintainersCall{Call: call}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockMaintainershipDataListProjectMaintainersCall wrap *gomock.Call
|
|
||||||
type MockMaintainershipDataListProjectMaintainersCall struct {
|
|
||||||
*gomock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return rewrite *gomock.Call.Return
|
|
||||||
func (c *MockMaintainershipDataListProjectMaintainersCall) Return(arg0 []string) *MockMaintainershipDataListProjectMaintainersCall {
|
|
||||||
c.Call = c.Call.Return(arg0)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do rewrite *gomock.Call.Do
|
|
||||||
func (c *MockMaintainershipDataListProjectMaintainersCall) Do(f func([]*common.ReviewGroup) []string) *MockMaintainershipDataListProjectMaintainersCall {
|
|
||||||
c.Call = c.Call.Do(f)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
|
||||||
func (c *MockMaintainershipDataListProjectMaintainersCall) DoAndReturn(f func([]*common.ReviewGroup) []string) *MockMaintainershipDataListProjectMaintainersCall {
|
|
||||||
c.Call = c.Call.DoAndReturn(f)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
// Code generated by MockGen. DO NOT EDIT.
|
|
||||||
// Source: obs_utils.go
|
|
||||||
//
|
|
||||||
// Generated by this command:
|
|
||||||
//
|
|
||||||
// mockgen -source=obs_utils.go -destination=mock/obs_utils.go -typed
|
|
||||||
//
|
|
||||||
|
|
||||||
// Package mock_common is a generated GoMock package.
|
|
||||||
package mock_common
|
|
||||||
|
|
||||||
import (
|
|
||||||
reflect "reflect"
|
|
||||||
|
|
||||||
gomock "go.uber.org/mock/gomock"
|
|
||||||
common "src.opensuse.org/autogits/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockObsStatusFetcherWithState is a mock of ObsStatusFetcherWithState interface.
|
|
||||||
type MockObsStatusFetcherWithState struct {
|
|
||||||
ctrl *gomock.Controller
|
|
||||||
recorder *MockObsStatusFetcherWithStateMockRecorder
|
|
||||||
isgomock struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockObsStatusFetcherWithStateMockRecorder is the mock recorder for MockObsStatusFetcherWithState.
|
|
||||||
type MockObsStatusFetcherWithStateMockRecorder struct {
|
|
||||||
mock *MockObsStatusFetcherWithState
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockObsStatusFetcherWithState creates a new mock instance.
|
|
||||||
func NewMockObsStatusFetcherWithState(ctrl *gomock.Controller) *MockObsStatusFetcherWithState {
|
|
||||||
mock := &MockObsStatusFetcherWithState{ctrl: ctrl}
|
|
||||||
mock.recorder = &MockObsStatusFetcherWithStateMockRecorder{mock}
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
|
||||||
func (m *MockObsStatusFetcherWithState) EXPECT() *MockObsStatusFetcherWithStateMockRecorder {
|
|
||||||
return m.recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildStatusWithState mocks base method.
|
|
||||||
func (m *MockObsStatusFetcherWithState) BuildStatusWithState(project string, opts *common.BuildResultOptions, packages ...string) (*common.BuildResultList, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
varargs := []any{project, opts}
|
|
||||||
for _, a := range packages {
|
|
||||||
varargs = append(varargs, a)
|
|
||||||
}
|
|
||||||
ret := m.ctrl.Call(m, "BuildStatusWithState", varargs...)
|
|
||||||
ret0, _ := ret[0].(*common.BuildResultList)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildStatusWithState indicates an expected call of BuildStatusWithState.
|
|
||||||
func (mr *MockObsStatusFetcherWithStateMockRecorder) BuildStatusWithState(project, opts any, packages ...any) *MockObsStatusFetcherWithStateBuildStatusWithStateCall {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
varargs := append([]any{project, opts}, packages...)
|
|
||||||
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BuildStatusWithState", reflect.TypeOf((*MockObsStatusFetcherWithState)(nil).BuildStatusWithState), varargs...)
|
|
||||||
return &MockObsStatusFetcherWithStateBuildStatusWithStateCall{Call: call}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockObsStatusFetcherWithStateBuildStatusWithStateCall wrap *gomock.Call
|
|
||||||
type MockObsStatusFetcherWithStateBuildStatusWithStateCall struct {
|
|
||||||
*gomock.Call
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return rewrite *gomock.Call.Return
|
|
||||||
func (c *MockObsStatusFetcherWithStateBuildStatusWithStateCall) Return(arg0 *common.BuildResultList, arg1 error) *MockObsStatusFetcherWithStateBuildStatusWithStateCall {
|
|
||||||
c.Call = c.Call.Return(arg0, arg1)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do rewrite *gomock.Call.Do
|
|
||||||
func (c *MockObsStatusFetcherWithStateBuildStatusWithStateCall) Do(f func(string, *common.BuildResultOptions, ...string) (*common.BuildResultList, error)) *MockObsStatusFetcherWithStateBuildStatusWithStateCall {
|
|
||||||
c.Call = c.Call.Do(f)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoAndReturn rewrite *gomock.Call.DoAndReturn
|
|
||||||
func (c *MockObsStatusFetcherWithStateBuildStatusWithStateCall) DoAndReturn(f func(string, *common.BuildResultOptions, ...string) (*common.BuildResultList, error)) *MockObsStatusFetcherWithStateBuildStatusWithStateCall {
|
|
||||||
c.Call = c.Call.DoAndReturn(f)
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
@@ -127,12 +127,10 @@ type ProjectMeta struct {
|
|||||||
Groups []GroupRepoMeta `xml:"group"`
|
Groups []GroupRepoMeta `xml:"group"`
|
||||||
Repositories []RepositoryMeta `xml:"repository"`
|
Repositories []RepositoryMeta `xml:"repository"`
|
||||||
|
|
||||||
BuildFlags Flags `xml:"build"`
|
BuildFlags Flags `xml:"build"`
|
||||||
PublicFlags Flags `xml:"publish"`
|
PublicFlags Flags `xml:"publish"`
|
||||||
DebugFlags Flags `xml:"debuginfo"`
|
DebugFlags Flags `xml:"debuginfo"`
|
||||||
UseForBuild Flags `xml:"useforbuild"`
|
UseForBuild Flags `xml:"useforbuild"`
|
||||||
Access Flags `xml:"access"`
|
|
||||||
SourceAccess Flags `xml:"sourceaccess"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PackageMeta struct {
|
type PackageMeta struct {
|
||||||
@@ -142,12 +140,6 @@ type PackageMeta struct {
|
|||||||
ScmSync string `xml:"scmsync"`
|
ScmSync string `xml:"scmsync"`
|
||||||
Persons []PersonRepoMeta `xml:"person"`
|
Persons []PersonRepoMeta `xml:"person"`
|
||||||
Groups []GroupRepoMeta `xml:"group"`
|
Groups []GroupRepoMeta `xml:"group"`
|
||||||
|
|
||||||
BuildFlags Flags `xml:"build"`
|
|
||||||
PublicFlags Flags `xml:"publish"`
|
|
||||||
DebugFlags Flags `xml:"debuginfo"`
|
|
||||||
UseForBuild Flags `xml:"useforbuild"`
|
|
||||||
SourceAccess Flags `xml:"sourceaccess"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserMeta struct {
|
type UserMeta struct {
|
||||||
@@ -570,58 +562,25 @@ func (c *ObsClient) DeleteProject(project string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ObsClient) BuildLog(prj, pkg, repo, arch string) (io.ReadCloser, error) {
|
|
||||||
url := c.baseUrl.JoinPath("build", prj, repo, arch, pkg, "_log")
|
|
||||||
query := url.Query()
|
|
||||||
query.Add("nostream", "1")
|
|
||||||
query.Add("start", "0")
|
|
||||||
url.RawQuery = query.Encode()
|
|
||||||
res, err := c.ObsRequestRaw("GET", url.String(), nil)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.Body, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PackageBuildStatus struct {
|
type PackageBuildStatus struct {
|
||||||
Package string `xml:"package,attr"`
|
Package string `xml:"package,attr"`
|
||||||
Code string `xml:"code,attr"`
|
Code string `xml:"code,attr"`
|
||||||
Details string `xml:"details"`
|
Details string `xml:"details"`
|
||||||
|
|
||||||
LastUpdate time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func PackageBuildStatusComp(A, B *PackageBuildStatus) int {
|
|
||||||
return strings.Compare(A.Package, B.Package)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BuildResult struct {
|
type BuildResult struct {
|
||||||
XMLName xml.Name `xml:"result" json:"xml,omitempty"`
|
Project string `xml:"project,attr"`
|
||||||
Project string `xml:"project,attr"`
|
Repository string `xml:"repository,attr"`
|
||||||
Repository string `xml:"repository,attr"`
|
Arch string `xml:"arch,attr"`
|
||||||
Arch string `xml:"arch,attr"`
|
Code string `xml:"code,attr"`
|
||||||
Code string `xml:"code,attr"`
|
Dirty bool `xml:"dirty,attr"`
|
||||||
Dirty bool `xml:"dirty,attr,omitempty"`
|
ScmSync string `xml:"scmsync"`
|
||||||
ScmSync string `xml:"scmsync,omitempty"`
|
ScmInfo string `xml:"scminfo"`
|
||||||
ScmInfo string `xml:"scminfo,omitempty"`
|
Status []PackageBuildStatus `xml:"status"`
|
||||||
Status []*PackageBuildStatus `xml:"status"`
|
Binaries []BinaryList `xml:"binarylist"`
|
||||||
Binaries []BinaryList `xml:"binarylist,omitempty"`
|
|
||||||
|
|
||||||
LastUpdate time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func BuildResultComp(A, B *BuildResult) int {
|
|
||||||
if cmp := strings.Compare(A.Project, B.Project); cmp != 0 {
|
|
||||||
return cmp
|
|
||||||
}
|
|
||||||
if cmp := strings.Compare(A.Repository, B.Repository); cmp != 0 {
|
|
||||||
return cmp
|
|
||||||
}
|
|
||||||
return strings.Compare(A.Arch, B.Arch)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Binary struct {
|
type Binary struct {
|
||||||
@@ -636,9 +595,9 @@ type BinaryList struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type BuildResultList struct {
|
type BuildResultList struct {
|
||||||
XMLName xml.Name `xml:"resultlist"`
|
XMLName xml.Name `xml:"resultlist"`
|
||||||
State string `xml:"state,attr"`
|
State string `xml:"state,attr"`
|
||||||
Result []*BuildResult `xml:"result"`
|
Result []BuildResult `xml:"result"`
|
||||||
|
|
||||||
isLastBuild bool
|
isLastBuild bool
|
||||||
}
|
}
|
||||||
|
|||||||
313
common/pr.go
313
common/pr.go
@@ -9,7 +9,6 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"src.opensuse.org/autogits/common/gitea-generated/client/repository"
|
|
||||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,50 +22,7 @@ type PRSet struct {
|
|||||||
PRs []*PRInfo
|
PRs []*PRInfo
|
||||||
Config *AutogitConfig
|
Config *AutogitConfig
|
||||||
|
|
||||||
BotUser string
|
BotUser string
|
||||||
HasAutoStaging bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (prinfo *PRInfo) PRComponents() (org string, repo string, idx int64) {
|
|
||||||
org = prinfo.PR.Base.Repo.Owner.UserName
|
|
||||||
repo = prinfo.PR.Base.Repo.Name
|
|
||||||
idx = prinfo.PR.Index
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (prinfo *PRInfo) RemoveReviewers(gitea GiteaUnreviewTimelineFetcher, Reviewers []string, BotUser string) {
|
|
||||||
org, repo, idx := prinfo.PRComponents()
|
|
||||||
tl, err := gitea.GetTimeline(org, repo, idx)
|
|
||||||
if err != nil {
|
|
||||||
LogError("Failed to fetch timeline for", PRtoString(prinfo.PR), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// find review request for each reviewer
|
|
||||||
ReviewersToUnrequest := Reviewers
|
|
||||||
ReviewersAlreadyChecked := []string{}
|
|
||||||
|
|
||||||
for _, tlc := range tl {
|
|
||||||
if tlc.Type == TimelineCommentType_ReviewRequested && tlc.Assignee != nil {
|
|
||||||
user := tlc.Assignee.UserName
|
|
||||||
|
|
||||||
if idx := slices.Index(ReviewersToUnrequest, user); idx >= 0 && !slices.Contains(ReviewersAlreadyChecked, user) {
|
|
||||||
if tlc.User != nil && tlc.User.UserName == BotUser {
|
|
||||||
ReviewersAlreadyChecked = append(ReviewersAlreadyChecked, user)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ReviewersToUnrequest = slices.Delete(ReviewersToUnrequest, idx, idx+1)
|
|
||||||
if len(Reviewers) == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LogDebug("Unrequesting reviewes for", PRtoString(prinfo.PR), ReviewersToUnrequest)
|
|
||||||
err = gitea.UnrequestReview(org, repo, idx, ReviewersToUnrequest...)
|
|
||||||
if err != nil {
|
|
||||||
LogError("Failed to unrequest reviewers for", PRtoString(prinfo.PR), err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func readPRData(gitea GiteaPRFetcher, pr *models.PullRequest, currentSet []*PRInfo, config *AutogitConfig) ([]*PRInfo, error) {
|
func readPRData(gitea GiteaPRFetcher, pr *models.PullRequest, currentSet []*PRInfo, config *AutogitConfig) ([]*PRInfo, error) {
|
||||||
@@ -99,15 +55,14 @@ func readPRData(gitea GiteaPRFetcher, pr *models.PullRequest, currentSet []*PRIn
|
|||||||
|
|
||||||
var Timeline_RefIssueNotFound error = errors.New("RefIssue not found on the timeline")
|
var Timeline_RefIssueNotFound error = errors.New("RefIssue not found on the timeline")
|
||||||
|
|
||||||
func LastPrjGitRefOnTimeline(botUser string, gitea GiteaPRTimelineReviewFetcher, org, repo string, num int64, config *AutogitConfig) (*models.PullRequest, error) {
|
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)
|
timeline, err := gitea.GetTimeline(org, repo, num)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LogError("Failed to fetch timeline for", org, repo, "#", num, err)
|
LogError("Failed to fetch timeline for", org, repo, "#", num, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
prjGitOrg, prjGitRepo, prjGitBranch := config.GetPrjGit()
|
|
||||||
|
|
||||||
for idx := len(timeline) - 1; idx >= 0; idx-- {
|
for idx := len(timeline) - 1; idx >= 0; idx-- {
|
||||||
item := timeline[idx]
|
item := timeline[idx]
|
||||||
issue := item.RefIssue
|
issue := item.RefIssue
|
||||||
@@ -117,32 +72,9 @@ func LastPrjGitRefOnTimeline(botUser string, gitea GiteaPRTimelineReviewFetcher,
|
|||||||
issue.Repository.Owner == prjGitOrg &&
|
issue.Repository.Owner == prjGitOrg &&
|
||||||
issue.Repository.Name == prjGitRepo {
|
issue.Repository.Name == prjGitRepo {
|
||||||
|
|
||||||
if !config.NoProjectGitPR {
|
lines := SplitLines(item.RefIssue.Body)
|
||||||
if issue.User.UserName != botUser {
|
for _, line := range lines {
|
||||||
continue
|
if strings.TrimSpace(line) == prRefLine {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pr, err := gitea.GetPullRequest(prjGitOrg, prjGitRepo, issue.Index)
|
|
||||||
if err != nil {
|
|
||||||
switch err.(type) {
|
|
||||||
case *repository.RepoGetPullRequestNotFound: // deleted?
|
|
||||||
continue
|
|
||||||
default:
|
|
||||||
LogDebug("PrjGit RefIssue fetch error from timeline", issue.Index, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LogDebug("found ref PR on timeline:", PRtoString(pr))
|
|
||||||
if pr.Base.Name != prjGitBranch {
|
|
||||||
LogDebug(" -> not matching:", pr.Base.Name, prjGitBranch)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
_, 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)
|
LogDebug("Found PrjGit PR in Timeline:", issue.Index)
|
||||||
|
|
||||||
// found prjgit PR in timeline. Return it
|
// found prjgit PR in timeline. Return it
|
||||||
@@ -156,7 +88,7 @@ func LastPrjGitRefOnTimeline(botUser string, gitea GiteaPRTimelineReviewFetcher,
|
|||||||
return nil, Timeline_RefIssueNotFound
|
return nil, Timeline_RefIssueNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func FetchPRSet(user string, gitea GiteaPRTimelineReviewFetcher, org, repo string, num int64, config *AutogitConfig) (*PRSet, error) {
|
func FetchPRSet(user string, gitea GiteaPRTimelineFetcher, org, repo string, num int64, config *AutogitConfig) (*PRSet, error) {
|
||||||
var pr *models.PullRequest
|
var pr *models.PullRequest
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@@ -166,7 +98,7 @@ func FetchPRSet(user string, gitea GiteaPRTimelineReviewFetcher, org, repo strin
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if pr, err = LastPrjGitRefOnTimeline(user, gitea, org, repo, num, config); err != nil && err != Timeline_RefIssueNotFound {
|
if pr, err = LastPrjGitRefOnTimeline(gitea, org, repo, num, prjGitOrg, prjGitRepo); err != nil && err != Timeline_RefIssueNotFound {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,15 +114,6 @@ func FetchPRSet(user string, gitea GiteaPRTimelineReviewFetcher, org, repo strin
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, pr := range prs {
|
|
||||||
org, repo, idx := pr.PRComponents()
|
|
||||||
reviews, err := FetchGiteaReviews(gitea, org, repo, idx)
|
|
||||||
if err != nil {
|
|
||||||
LogError("Error fetching reviews for", PRtoString(pr.PR), ":", err)
|
|
||||||
}
|
|
||||||
pr.Reviews = reviews
|
|
||||||
}
|
|
||||||
|
|
||||||
return &PRSet{
|
return &PRSet{
|
||||||
PRs: prs,
|
PRs: prs,
|
||||||
Config: config,
|
Config: config,
|
||||||
@@ -198,12 +121,6 @@ func FetchPRSet(user string, gitea GiteaPRTimelineReviewFetcher, org, repo strin
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (prset *PRSet) RemoveReviewers(gitea GiteaUnreviewTimelineFetcher, reviewers []string) {
|
|
||||||
for _, prinfo := range prset.PRs {
|
|
||||||
prinfo.RemoveReviewers(gitea, reviewers, prset.BotUser)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rs *PRSet) Find(pr *models.PullRequest) (*PRInfo, bool) {
|
func (rs *PRSet) Find(pr *models.PullRequest) (*PRInfo, bool) {
|
||||||
for _, p := range rs.PRs {
|
for _, p := range rs.PRs {
|
||||||
if p.PR.Base.RepoID == pr.Base.RepoID &&
|
if p.PR.Base.RepoID == pr.Base.RepoID &&
|
||||||
@@ -289,144 +206,67 @@ next_rs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, pr := range prjpr_set {
|
for _, pr := range prjpr_set {
|
||||||
if strings.EqualFold(prinfo.PR.Base.Repo.Owner.UserName, pr.Org) && strings.EqualFold(prinfo.PR.Base.Repo.Name, pr.Repo) && prinfo.PR.Index == pr.Num {
|
if prinfo.PR.Base.Repo.Owner.UserName == pr.Org && prinfo.PR.Base.Repo.Name == pr.Repo && prinfo.PR.Index == pr.Num {
|
||||||
continue next_rs
|
continue next_rs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LogDebug(" PR: ", PRtoString(prinfo.PR), "not found in project git PRSet")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *PRSet) FindMissingAndExtraReviewers(maintainers MaintainershipData, idx int) (missing, extra []string) {
|
func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequester, maintainers MaintainershipData) error {
|
||||||
configReviewers := ParseReviewers(rs.Config.Reviewers)
|
configReviewers := ParseReviewers(rs.Config.Reviewers)
|
||||||
|
|
||||||
// remove reviewers that were already requested and are not stale
|
for _, pr := range rs.PRs {
|
||||||
prjMaintainers := maintainers.ListProjectMaintainers(nil)
|
reviewers := []string{}
|
||||||
LogDebug("project maintainers:", prjMaintainers)
|
|
||||||
|
|
||||||
pr := rs.PRs[idx]
|
if rs.IsPrjGitPR(pr.PR) {
|
||||||
if rs.IsPrjGitPR(pr.PR) {
|
reviewers = slices.Concat(configReviewers.Prj, configReviewers.PrjOptional)
|
||||||
missing = slices.Concat(configReviewers.Prj, configReviewers.PrjOptional)
|
LogDebug("PrjGit submitter:", pr.PR.User.UserName)
|
||||||
if rs.HasAutoStaging {
|
if len(rs.PRs) == 1 {
|
||||||
missing = append(missing, Bot_BuildReview)
|
reviewers = slices.Concat(reviewers, maintainers.ListProjectMaintainers())
|
||||||
}
|
}
|
||||||
LogDebug("PrjGit submitter:", pr.PR.User.UserName)
|
|
||||||
// only need project maintainer reviews if:
|
|
||||||
// * not created by a bot and has other PRs, or
|
|
||||||
// * not created by maintainer
|
|
||||||
noReviewPRCreators := prjMaintainers
|
|
||||||
if len(rs.PRs) > 1 {
|
|
||||||
noReviewPRCreators = append(noReviewPRCreators, rs.BotUser)
|
|
||||||
}
|
|
||||||
if slices.Contains(noReviewPRCreators, pr.PR.User.UserName) || pr.Reviews.IsReviewedByOneOf(prjMaintainers...) {
|
|
||||||
LogDebug("Project already reviewed by a project maintainer, remove rest")
|
|
||||||
// do not remove reviewers if they are also maintainers
|
|
||||||
prjMaintainers = slices.DeleteFunc(prjMaintainers, func(m string) bool { return slices.Contains(missing, m) })
|
|
||||||
extra = slices.Concat(prjMaintainers, []string{rs.BotUser})
|
|
||||||
} else {
|
} else {
|
||||||
// if bot not created PrjGit or prj maintainer, we need to add project reviewers here
|
pkg := pr.PR.Base.Repo.Name
|
||||||
if slices.Contains(noReviewPRCreators, pr.PR.User.UserName) {
|
reviewers = slices.Concat(configReviewers.Pkg, maintainers.ListProjectMaintainers(), maintainers.ListPackageMaintainers(pkg), configReviewers.PkgOptional)
|
||||||
LogDebug("No need for project maintainers")
|
}
|
||||||
extra = slices.Concat(prjMaintainers, []string{rs.BotUser})
|
|
||||||
|
slices.Sort(reviewers)
|
||||||
|
reviewers = slices.Compact(reviewers)
|
||||||
|
|
||||||
|
// submitters do not need to review their own work
|
||||||
|
if idx := slices.Index(reviewers, pr.PR.User.UserName); idx != -1 {
|
||||||
|
reviewers = slices.Delete(reviewers, idx, idx+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
LogDebug("PR: ", pr.PR.Base.Repo.Name, pr.PR.Index)
|
||||||
|
LogDebug("reviewers for PR:", reviewers)
|
||||||
|
|
||||||
|
// remove reviewers that were already requested and are not stale
|
||||||
|
reviews, err := FetchGiteaReviews(gitea, reviewers, pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
|
||||||
|
if err != nil {
|
||||||
|
LogError("Error fetching reviews:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := 0; idx < len(reviewers); {
|
||||||
|
user := reviewers[idx]
|
||||||
|
if reviews.HasPendingReviewBy(user) || reviews.IsReviewedBy(user) {
|
||||||
|
reviewers = slices.Delete(reviewers, idx, idx+1)
|
||||||
|
LogDebug("removing reviewer:", user)
|
||||||
} else {
|
} else {
|
||||||
LogDebug("Adding prjMaintainers to PrjGit")
|
idx++
|
||||||
missing = append(missing, prjMaintainers...)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
pkg := pr.PR.Base.Repo.Name
|
|
||||||
pkgMaintainers := maintainers.ListPackageMaintainers(pkg, nil)
|
|
||||||
Maintainers := slices.Concat(prjMaintainers, pkgMaintainers)
|
|
||||||
noReviewPkgPRCreators := pkgMaintainers
|
|
||||||
|
|
||||||
LogDebug("packakge maintainers:", Maintainers)
|
// get maintainers associated with the PR too
|
||||||
|
if len(reviewers) > 0 {
|
||||||
missing = slices.Concat(configReviewers.Pkg, configReviewers.PkgOptional)
|
LogDebug("Requesting reviews from:", reviewers)
|
||||||
if slices.Contains(noReviewPkgPRCreators, pr.PR.User.UserName) || pr.Reviews.IsReviewedByOneOf(Maintainers...) {
|
|
||||||
// submitter is maintainer or already reviewed
|
|
||||||
LogDebug("Package reviewed by maintainer (or subitter is maintainer), remove the rest of them")
|
|
||||||
// do not remove reviewers if they are also maintainers
|
|
||||||
Maintainers = slices.DeleteFunc(Maintainers, func(m string) bool { return slices.Contains(missing, m) })
|
|
||||||
extra = slices.Concat(Maintainers, []string{rs.BotUser})
|
|
||||||
} else {
|
|
||||||
// maintainer review is missing
|
|
||||||
LogDebug("Adding package maintainers to package git")
|
|
||||||
missing = append(missing, pkgMaintainers...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
slices.Sort(missing)
|
|
||||||
missing = slices.Compact(missing)
|
|
||||||
|
|
||||||
slices.Sort(extra)
|
|
||||||
extra = slices.Compact(extra)
|
|
||||||
|
|
||||||
// submitters cannot review their own work
|
|
||||||
if idx := slices.Index(missing, pr.PR.User.UserName); idx != -1 {
|
|
||||||
missing = slices.Delete(missing, idx, idx+1)
|
|
||||||
}
|
|
||||||
|
|
||||||
LogDebug("PR: ", PRtoString(pr.PR))
|
|
||||||
LogDebug(" preliminary add reviewers for PR:", missing)
|
|
||||||
LogDebug(" preliminary rm reviewers for PR:", extra)
|
|
||||||
|
|
||||||
// remove missing reviewers that are already done or already pending
|
|
||||||
for idx := 0; idx < len(missing); {
|
|
||||||
user := missing[idx]
|
|
||||||
if pr.Reviews.HasPendingReviewBy(user) || pr.Reviews.IsReviewedBy(user) {
|
|
||||||
missing = slices.Delete(missing, idx, idx+1)
|
|
||||||
LogDebug(" removing done/pending reviewer:", user)
|
|
||||||
} else {
|
|
||||||
idx++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove extra reviews that are actually only pending, and only pending by us
|
|
||||||
for idx := 0; idx < len(extra); {
|
|
||||||
user := extra[idx]
|
|
||||||
rr := pr.Reviews.FindReviewRequester(user)
|
|
||||||
if rr != nil && rr.User.UserName == rs.BotUser && pr.Reviews.HasPendingReviewBy(user) {
|
|
||||||
// good to remove this review
|
|
||||||
idx++
|
|
||||||
} else {
|
|
||||||
// this review should not be considered as extra by us
|
|
||||||
LogDebug(" - cannot find? to remove", user)
|
|
||||||
if rr != nil {
|
|
||||||
LogDebug(" ", rr.User.UserName, "vs.", rs.BotUser, pr.Reviews.HasPendingReviewBy(user))
|
|
||||||
}
|
|
||||||
extra = slices.Delete(extra, idx, idx+1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LogDebug(" add reviewers for PR:", missing)
|
|
||||||
LogDebug(" rm reviewers for PR:", extra)
|
|
||||||
|
|
||||||
return missing, extra
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequesterAndUnrequester, maintainers MaintainershipData) error {
|
|
||||||
for idx, pr := range rs.PRs {
|
|
||||||
missingReviewers, extraReviewers := rs.FindMissingAndExtraReviewers(maintainers, idx)
|
|
||||||
|
|
||||||
if len(missingReviewers) > 0 {
|
|
||||||
LogDebug(" Requesting reviews from:", missingReviewers)
|
|
||||||
if !IsDryRun {
|
if !IsDryRun {
|
||||||
for _, r := range missingReviewers {
|
for _, r := range reviewers {
|
||||||
if _, err := gitea.RequestReviews(pr.PR, r); err != nil {
|
if _, err := gitea.RequestReviews(pr.PR, r); err != nil {
|
||||||
LogError("Cannot create reviews on", PRtoString(pr.PR), "for user:", r, err)
|
LogError("Cannot create reviews on", fmt.Sprintf("%s/%s#%d for [%s]", pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index, strings.Join(reviewers, ", ")), err)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(extraReviewers) > 0 {
|
|
||||||
LogDebug(" UnRequesting reviews from:", extraReviewers)
|
|
||||||
if !IsDryRun {
|
|
||||||
for _, r := range extraReviewers {
|
|
||||||
org, repo, idx := pr.PRComponents()
|
|
||||||
if err := gitea.UnrequestReview(org, repo, idx, r); err != nil {
|
|
||||||
LogError("Cannot unrequest reviews on", PRtoString(pr.PR), "for user:", r, err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -435,29 +275,21 @@ func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequesterAndUnreques
|
|||||||
return nil
|
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 {
|
func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData) bool {
|
||||||
configReviewers := ParseReviewers(rs.Config.Reviewers)
|
configReviewers := ParseReviewers(rs.Config.Reviewers)
|
||||||
|
|
||||||
is_manually_reviewed_ok := false
|
is_manually_reviewed_ok := false
|
||||||
|
|
||||||
if need_manual_review := rs.Config.ManualMergeOnly || rs.Config.ManualMergeProject; need_manual_review {
|
if need_manual_review := rs.Config.ManualMergeOnly || rs.Config.ManualMergeProject; need_manual_review {
|
||||||
// Groups are expanded here because any group member can issue "merge ok" to the BotUser
|
|
||||||
groups := rs.Config.ReviewGroups
|
|
||||||
prjgit, err := rs.GetPrjGitPR()
|
prjgit, err := rs.GetPrjGitPR()
|
||||||
if err == nil && prjgit != nil {
|
if err == nil && prjgit != nil {
|
||||||
reviewers := slices.Concat(configReviewers.Prj, maintainers.ListProjectMaintainers(groups))
|
reviewers := slices.Concat(configReviewers.Prj, maintainers.ListProjectMaintainers())
|
||||||
LogDebug("Fetching reviews for", prjgit.PR.Base.Repo.Owner.UserName, prjgit.PR.Base.Repo.Name, prjgit.PR.Index)
|
LogDebug("Fetching reviews for", prjgit.PR.Base.Repo.Owner.UserName, prjgit.PR.Base.Repo.Name, prjgit.PR.Index)
|
||||||
r, err := FetchGiteaReviews(gitea, prjgit.PR.Base.Repo.Owner.UserName, prjgit.PR.Base.Repo.Name, prjgit.PR.Index)
|
r, err := FetchGiteaReviews(gitea, reviewers, prjgit.PR.Base.Repo.Owner.UserName, prjgit.PR.Base.Repo.Name, prjgit.PR.Index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LogError("Cannot fetch gita reaviews for PR:", err)
|
LogError("Cannot fetch gita reaviews for PR:", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
r.RequestedReviewers = reviewers
|
|
||||||
prjgit.Reviews = r
|
prjgit.Reviews = r
|
||||||
if prjgit.Reviews.IsManualMergeOK() {
|
if prjgit.Reviews.IsManualMergeOK() {
|
||||||
is_manually_reviewed_ok = true
|
is_manually_reviewed_ok = true
|
||||||
@@ -471,14 +303,13 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
|
|||||||
}
|
}
|
||||||
|
|
||||||
pkg := pr.PR.Base.Repo.Name
|
pkg := pr.PR.Base.Repo.Name
|
||||||
reviewers := slices.Concat(configReviewers.Pkg, maintainers.ListPackageMaintainers(pkg, groups))
|
reviewers := slices.Concat(configReviewers.Pkg, maintainers.ListPackageMaintainers(pkg))
|
||||||
LogDebug("Fetching reviews for", pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
|
LogDebug("Fetching reviews for", pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
|
||||||
r, err := FetchGiteaReviews(gitea, pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
|
r, err := FetchGiteaReviews(gitea, reviewers, pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LogError("Cannot fetch gita reaviews for PR:", err)
|
LogError("Cannot fetch gita reaviews for PR:", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
r.RequestedReviewers = reviewers
|
|
||||||
pr.Reviews = r
|
pr.Reviews = r
|
||||||
if !pr.Reviews.IsManualMergeOK() {
|
if !pr.Reviews.IsManualMergeOK() {
|
||||||
LogInfo("Not approved manual merge. PR:", pr.PR.URL)
|
LogInfo("Not approved manual merge. PR:", pr.PR.URL)
|
||||||
@@ -500,9 +331,6 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
|
|||||||
var pkg string
|
var pkg string
|
||||||
if rs.IsPrjGitPR(pr.PR) {
|
if rs.IsPrjGitPR(pr.PR) {
|
||||||
reviewers = configReviewers.Prj
|
reviewers = configReviewers.Prj
|
||||||
if rs.HasAutoStaging {
|
|
||||||
reviewers = append(reviewers, Bot_BuildReview)
|
|
||||||
}
|
|
||||||
pkg = ""
|
pkg = ""
|
||||||
} else {
|
} else {
|
||||||
reviewers = configReviewers.Pkg
|
reviewers = configReviewers.Pkg
|
||||||
@@ -514,25 +342,20 @@ func (rs *PRSet) IsApproved(gitea GiteaPRChecker, maintainers MaintainershipData
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
r, err := FetchGiteaReviews(gitea, pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
|
r, err := FetchGiteaReviews(gitea, reviewers, pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
LogError("Cannot fetch gitea reaviews for PR:", err)
|
LogError("Cannot fetch gita reaviews for PR:", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
r.RequestedReviewers = reviewers
|
|
||||||
|
|
||||||
is_manually_reviewed_ok = r.IsApproved()
|
is_manually_reviewed_ok = r.IsApproved()
|
||||||
LogDebug("PR to", pr.PR.Base.Repo.Name, "reviewed?", is_manually_reviewed_ok)
|
LogDebug(pr.PR.Base.Repo.Name, is_manually_reviewed_ok)
|
||||||
if !is_manually_reviewed_ok {
|
if !is_manually_reviewed_ok {
|
||||||
if GetLoggingLevel() > LogLevelInfo {
|
|
||||||
LogDebug("missing reviewers:", r.MissingReviews())
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if need_maintainer_review := !rs.IsPrjGitPR(pr.PR) || pr.PR.User.UserName != rs.BotUser; need_maintainer_review {
|
if need_maintainer_review := !rs.IsPrjGitPR(pr.PR) || pr.PR.User.UserName != rs.BotUser; need_maintainer_review {
|
||||||
// Do not expand groups here, as the group-review-bot will ACK if group has reviewed.
|
if is_manually_reviewed_ok = maintainers.IsApproved(pkg, r.reviews, pr.PR.User.UserName); !is_manually_reviewed_ok {
|
||||||
if is_manually_reviewed_ok = maintainers.IsApproved(pkg, r.Reviews, pr.PR.User.UserName, nil); !is_manually_reviewed_ok {
|
|
||||||
LogDebug(" not approved?", pkg)
|
LogDebug(" not approved?", pkg)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -550,8 +373,7 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
|
|||||||
}
|
}
|
||||||
prjgit := prjgit_info.PR
|
prjgit := prjgit_info.PR
|
||||||
|
|
||||||
_, _, prjgitBranch := rs.Config.GetPrjGit()
|
remote, err := git.GitClone(DefaultGitPrj, rs.Config.Branch, prjgit.Base.Repo.SSHURL)
|
||||||
remote, err := git.GitClone(DefaultGitPrj, prjgitBranch, prjgit.Base.Repo.SSHURL)
|
|
||||||
PanicOnError(err)
|
PanicOnError(err)
|
||||||
git.GitExecOrPanic(DefaultGitPrj, "fetch", remote, prjgit.Head.Sha)
|
git.GitExecOrPanic(DefaultGitPrj, "fetch", remote, prjgit.Head.Sha)
|
||||||
|
|
||||||
@@ -568,7 +390,7 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
|
|||||||
panic("FIXME")
|
panic("FIXME")
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
msg := fmt.Sprintf("Merging\n\nPR: %s/%s!%d", prjgit.Base.Repo.Owner.UserName, prjgit.Base.Repo.Name, prjgit.Index)
|
msg := fmt.Sprintf("Merging\n\nPR: %s/%s#%d", prjgit.Base.Repo.Owner.UserName, prjgit.Base.Repo.Name, prjgit.Index)
|
||||||
|
|
||||||
err = git.GitExec(DefaultGitPrj, "merge", "--no-ff", "-m", msg, prjgit.Head.Sha)
|
err = git.GitExec(DefaultGitPrj, "merge", "--no-ff", "-m", msg, prjgit.Head.Sha)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -580,7 +402,6 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
|
|||||||
// we can only resolve conflicts with .gitmodules
|
// we can only resolve conflicts with .gitmodules
|
||||||
for _, s := range status {
|
for _, s := range status {
|
||||||
if s.Status == GitStatus_Unmerged {
|
if s.Status == GitStatus_Unmerged {
|
||||||
panic("Can't handle conflicts yet")
|
|
||||||
if s.Path != ".gitmodules" {
|
if s.Path != ".gitmodules" {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -671,15 +492,7 @@ func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
|
|||||||
if rs.IsPrjGitPR(prinfo.PR) {
|
if rs.IsPrjGitPR(prinfo.PR) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
br := rs.Config.Branch
|
prinfo.RemoteName, err = git.GitClone(repo.Name, rs.Config.Branch, repo.SSHURL)
|
||||||
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)
|
PanicOnError(err)
|
||||||
git.GitExecOrPanic(repo.Name, "fetch", prinfo.RemoteName, head.Sha)
|
git.GitExecOrPanic(repo.Name, "fetch", prinfo.RemoteName, head.Sha)
|
||||||
git.GitExecOrPanic(repo.Name, "merge", "--ff", head.Sha)
|
git.GitExecOrPanic(repo.Name, "merge", "--ff", head.Sha)
|
||||||
|
|||||||
48
common/pr_conflict_resolution.go
Normal file
48
common/pr_conflict_resolution.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var UnknownParser error = errors.New("Cannot parse path")
|
||||||
|
|
||||||
|
type PRConflictResolver interface {
|
||||||
|
/*
|
||||||
|
stage_content -> { merge_base (stage1), head (stage2), merge_head (stage3) }
|
||||||
|
*/
|
||||||
|
Resolve(path string, stage_contents [3]string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolvers []PRConflictResolver = []PRConflictResolver{
|
||||||
|
&submodule_conflict_resolver{},
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveMergeConflict(path string, file_contents [3]string) error {
|
||||||
|
for _, r := range resolvers {
|
||||||
|
if err := r.Resolve(path, file_contents); err != UnknownParser {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return UnknownParser
|
||||||
|
}
|
||||||
|
|
||||||
|
type submodule_conflict_resolver struct{}
|
||||||
|
|
||||||
|
func (*submodule_conflict_resolver) Resolve(path string, stage [3]string) error {
|
||||||
|
if path != ".gitmodules" {
|
||||||
|
return UnknownParser
|
||||||
|
}
|
||||||
|
return UnknownParser
|
||||||
|
}
|
||||||
|
|
||||||
|
type changes_file_resolver struct{}
|
||||||
|
|
||||||
|
func (*changes_file_resolver) Resolve(path string, stage [3]string) error {
|
||||||
|
if !strings.HasSuffix(path, ".changes") {
|
||||||
|
return UnknownParser
|
||||||
|
}
|
||||||
|
|
||||||
|
return UnknownParser
|
||||||
|
}
|
||||||
10
common/pr_conflict_resolution_test.go
Normal file
10
common/pr_conflict_resolution_test.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package common_test
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func ResolveSubmoduleConflicts(t *testing.T) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveChangesFileConflict(t *testing.T) {
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,6 +2,7 @@ package common_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
@@ -14,23 +15,22 @@ import (
|
|||||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||||
mock_common "src.opensuse.org/autogits/common/mock"
|
mock_common "src.opensuse.org/autogits/common/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
func TestCockpit(t *testing.T) {
|
func TestCockpit(t *testing.T) {
|
||||||
common.SetLoggingLevel(common.LogLevelDebug)
|
common.SetLoggingLevel(common.LogLevelDebug)
|
||||||
gitea := common.AllocateGiteaTransport("https://src.opensuse.org")
|
gitea := common.AllocateGiteaTransport("https://src.opensuse.org")
|
||||||
tl, err := gitea.GetTimeline("cockpit", "cockpit", 29)
|
tl, err := gitea.GetTimeline("cockpit", "cockpit", 29)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("Fail to timeline", err)
|
t.Fatal("Fail to timeline", err)
|
||||||
}
|
|
||||||
t.Log(tl)
|
|
||||||
r, err := common.FetchGiteaReviews(gitea, []string{}, "cockpit", "cockpit", 29)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Error:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Error(r)
|
|
||||||
}
|
}
|
||||||
|
t.Log(tl)
|
||||||
|
r, err := common.FetchGiteaReviews(gitea, []string{}, "cockpit", "cockpit", 29)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Error:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Error(r)
|
||||||
|
}
|
||||||
*/
|
*/
|
||||||
func reviewsToTimeline(reviews []*models.PullReview) []*models.TimelineComment {
|
func reviewsToTimeline(reviews []*models.PullReview) []*models.TimelineComment {
|
||||||
timeline := make([]*models.TimelineComment, len(reviews))
|
timeline := make([]*models.TimelineComment, len(reviews))
|
||||||
@@ -48,13 +48,11 @@ func reviewsToTimeline(reviews []*models.PullReview) []*models.TimelineComment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPR(t *testing.T) {
|
func TestPR(t *testing.T) {
|
||||||
return
|
|
||||||
|
|
||||||
baseConfig := common.AutogitConfig{
|
baseConfig := common.AutogitConfig{
|
||||||
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
||||||
Branch: "branch",
|
Branch: "branch",
|
||||||
Organization: "foo",
|
Organization: "foo",
|
||||||
GitProjectName: "foo/barPrj#master",
|
GitProjectName: "barPrj",
|
||||||
}
|
}
|
||||||
|
|
||||||
type prdata struct {
|
type prdata struct {
|
||||||
@@ -75,7 +73,7 @@ func TestPR(t *testing.T) {
|
|||||||
consistentSet bool
|
consistentSet bool
|
||||||
prjGitPRIndex int
|
prjGitPRIndex int
|
||||||
|
|
||||||
reviewSetFetcher func(*mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error)
|
reviewSetFetcher func(*mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Error fetching PullRequest",
|
name: "Error fetching PullRequest",
|
||||||
@@ -147,7 +145,7 @@ func TestPR(t *testing.T) {
|
|||||||
prjGitPRIndex: 0,
|
prjGitPRIndex: 0,
|
||||||
consistentSet: true,
|
consistentSet: true,
|
||||||
reviewed: true,
|
reviewed: true,
|
||||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &baseConfig)
|
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &baseConfig)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -179,7 +177,7 @@ func TestPR(t *testing.T) {
|
|||||||
prjGitPRIndex: 0,
|
prjGitPRIndex: 0,
|
||||||
consistentSet: true,
|
consistentSet: true,
|
||||||
reviewed: false,
|
reviewed: false,
|
||||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &baseConfig)
|
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &baseConfig)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -207,7 +205,7 @@ func TestPR(t *testing.T) {
|
|||||||
prjGitPRIndex: 0,
|
prjGitPRIndex: 0,
|
||||||
consistentSet: true,
|
consistentSet: true,
|
||||||
reviewed: false,
|
reviewed: false,
|
||||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
||||||
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
||||||
Branch: "branch",
|
Branch: "branch",
|
||||||
@@ -241,7 +239,7 @@ func TestPR(t *testing.T) {
|
|||||||
prjGitPRIndex: 0,
|
prjGitPRIndex: 0,
|
||||||
consistentSet: true,
|
consistentSet: true,
|
||||||
reviewed: true,
|
reviewed: true,
|
||||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
||||||
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
||||||
Branch: "branch",
|
Branch: "branch",
|
||||||
@@ -275,7 +273,7 @@ func TestPR(t *testing.T) {
|
|||||||
prjGitPRIndex: 0,
|
prjGitPRIndex: 0,
|
||||||
consistentSet: true,
|
consistentSet: true,
|
||||||
reviewed: true,
|
reviewed: true,
|
||||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
||||||
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
||||||
Branch: "branch",
|
Branch: "branch",
|
||||||
@@ -311,7 +309,7 @@ func TestPR(t *testing.T) {
|
|||||||
prjGitPRIndex: 0,
|
prjGitPRIndex: 0,
|
||||||
consistentSet: true,
|
consistentSet: true,
|
||||||
reviewed: false,
|
reviewed: false,
|
||||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
||||||
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
||||||
Branch: "branch",
|
Branch: "branch",
|
||||||
@@ -346,7 +344,7 @@ func TestPR(t *testing.T) {
|
|||||||
prjGitPRIndex: 0,
|
prjGitPRIndex: 0,
|
||||||
consistentSet: true,
|
consistentSet: true,
|
||||||
reviewed: true,
|
reviewed: true,
|
||||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
||||||
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
||||||
Branch: "branch",
|
Branch: "branch",
|
||||||
@@ -388,7 +386,7 @@ func TestPR(t *testing.T) {
|
|||||||
prjGitPRIndex: 0,
|
prjGitPRIndex: 0,
|
||||||
consistentSet: true,
|
consistentSet: true,
|
||||||
reviewed: true,
|
reviewed: true,
|
||||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
||||||
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
||||||
Branch: "branch",
|
Branch: "branch",
|
||||||
@@ -430,7 +428,7 @@ func TestPR(t *testing.T) {
|
|||||||
prjGitPRIndex: 0,
|
prjGitPRIndex: 0,
|
||||||
consistentSet: true,
|
consistentSet: true,
|
||||||
reviewed: false,
|
reviewed: false,
|
||||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
||||||
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
||||||
Branch: "branch",
|
Branch: "branch",
|
||||||
@@ -473,7 +471,7 @@ func TestPR(t *testing.T) {
|
|||||||
prjGitPRIndex: 0,
|
prjGitPRIndex: 0,
|
||||||
consistentSet: true,
|
consistentSet: true,
|
||||||
reviewed: false,
|
reviewed: false,
|
||||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||||
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
return common.FetchPRSet("test", mock, "foo", "barPrj", 42, &common.AutogitConfig{
|
||||||
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
Reviewers: []string{"+super1", "*super2", "m1", "-m2"},
|
||||||
Branch: "branch",
|
Branch: "branch",
|
||||||
@@ -500,7 +498,7 @@ func TestPR(t *testing.T) {
|
|||||||
prjGitPRIndex: 0,
|
prjGitPRIndex: 0,
|
||||||
consistentSet: true,
|
consistentSet: true,
|
||||||
reviewed: true,
|
reviewed: true,
|
||||||
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineReviewFetcher) (*common.PRSet, error) {
|
reviewSetFetcher: func(mock *mock_common.MockGiteaPRTimelineFetcher) (*common.PRSet, error) {
|
||||||
config := common.AutogitConfig{
|
config := common.AutogitConfig{
|
||||||
Reviewers: []string{"+super1", "*super2", "m1", "-m2", "~*bot"},
|
Reviewers: []string{"+super1", "*super2", "m1", "-m2", "~*bot"},
|
||||||
Branch: "branch",
|
Branch: "branch",
|
||||||
@@ -515,7 +513,7 @@ func TestPR(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
ctl := gomock.NewController(t)
|
ctl := gomock.NewController(t)
|
||||||
pr_mock := mock_common.NewMockGiteaPRTimelineReviewFetcher(ctl)
|
pr_mock := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
|
||||||
review_mock := mock_common.NewMockGiteaPRChecker(ctl)
|
review_mock := mock_common.NewMockGiteaPRChecker(ctl)
|
||||||
// reviewer_mock := mock_common.NewMockGiteaReviewRequester(ctl)
|
// reviewer_mock := mock_common.NewMockGiteaReviewRequester(ctl)
|
||||||
|
|
||||||
@@ -608,9 +606,9 @@ func TestPR(t *testing.T) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
maintainers := mock_common.NewMockMaintainershipData(ctl)
|
maintainers := mock_common.NewMockMaintainershipData(ctl)
|
||||||
maintainers.EXPECT().ListPackageMaintainers(gomock.Any(), gomock.Any()).Return([]string{}).AnyTimes()
|
maintainers.EXPECT().ListPackageMaintainers(gomock.Any()).Return([]string{}).AnyTimes()
|
||||||
maintainers.EXPECT().ListProjectMaintainers(gomock.Any()).Return([]string{}).AnyTimes()
|
maintainers.EXPECT().ListProjectMaintainers().Return([]string{}).AnyTimes()
|
||||||
maintainers.EXPECT().IsApproved(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(true).AnyTimes()
|
maintainers.EXPECT().IsApproved(gomock.Any(), gomock.Any(), gomock.Any()).Return(true).AnyTimes()
|
||||||
|
|
||||||
if isApproved := res.IsApproved(review_mock, maintainers); isApproved != test.reviewed {
|
if isApproved := res.IsApproved(review_mock, maintainers); isApproved != test.reviewed {
|
||||||
t.Error("expected reviewed to be NOT", isApproved)
|
t.Error("expected reviewed to be NOT", isApproved)
|
||||||
@@ -619,514 +617,288 @@ func TestPR(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFindMissingAndExtraReviewers(t *testing.T) {
|
func TestPRAssignReviewers(t *testing.T) {
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
config common.AutogitConfig
|
||||||
reviewers []struct {
|
reviewers []struct {
|
||||||
org, repo string
|
org, repo string
|
||||||
num int64
|
num int64
|
||||||
reviewer string
|
reviewer string
|
||||||
}
|
}
|
||||||
|
|
||||||
prset *common.PRSet
|
pkgReviews []*models.PullReview
|
||||||
maintainers common.MaintainershipData
|
pkgTimeline []*models.TimelineComment
|
||||||
|
prjReviews []*models.PullReview
|
||||||
|
prjTimeline []*models.TimelineComment
|
||||||
|
|
||||||
noAutoStaging bool
|
expectedReviewerCall [2][]string
|
||||||
|
|
||||||
expected_missing_reviewers [][]string
|
|
||||||
expected_extra_reviewers [][]string
|
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "No reviewers",
|
name: "No reviewers",
|
||||||
prset: &common.PRSet{
|
config: common.AutogitConfig{
|
||||||
PRs: []*common.PRInfo{
|
GitProjectName: "repo",
|
||||||
{
|
Organization: "org",
|
||||||
PR: &models.PullRequest{
|
Branch: "main",
|
||||||
User: &models.User{UserName: "foo"},
|
Reviewers: []string{},
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Config: &common.AutogitConfig{
|
|
||||||
GitProjectName: "prg/repo#main",
|
|
||||||
Organization: "org",
|
|
||||||
Branch: "main",
|
|
||||||
Reviewers: []string{},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{}},
|
expectedReviewerCall: [2][]string{{"autogits_obs_staging_bot"}, {"prjmaintainer", "pkgmaintainer"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "One project reviewer only",
|
name: "One project reviewer only",
|
||||||
prset: &common.PRSet{
|
config: common.AutogitConfig{
|
||||||
PRs: []*common.PRInfo{
|
GitProjectName: "repo",
|
||||||
{
|
Organization: "org",
|
||||||
PR: &models.PullRequest{
|
Branch: "main",
|
||||||
User: &models.User{UserName: "foo"},
|
Reviewers: []string{"-user1"},
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
PR: &models.PullRequest{
|
|
||||||
User: &models.User{UserName: "foo"},
|
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Config: &common.AutogitConfig{
|
|
||||||
GitProjectName: "prg/repo#main",
|
|
||||||
Organization: "org",
|
|
||||||
Branch: "main",
|
|
||||||
Reviewers: []string{"-user1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{}},
|
|
||||||
|
|
||||||
expected_missing_reviewers: [][]string{
|
|
||||||
[]string{},
|
|
||||||
[]string{"autogits_obs_staging_bot", "user1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "One project reviewer only and no auto staging",
|
|
||||||
noAutoStaging: true,
|
|
||||||
prset: &common.PRSet{
|
|
||||||
PRs: []*common.PRInfo{
|
|
||||||
{
|
|
||||||
PR: &models.PullRequest{
|
|
||||||
User: &models.User{UserName: "foo"},
|
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
PR: &models.PullRequest{
|
|
||||||
User: &models.User{UserName: "foo"},
|
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Config: &common.AutogitConfig{
|
|
||||||
GitProjectName: "prg/repo#main",
|
|
||||||
Organization: "org",
|
|
||||||
Branch: "main",
|
|
||||||
Reviewers: []string{"-user1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{}},
|
|
||||||
|
|
||||||
expected_missing_reviewers: [][]string{
|
|
||||||
nil,
|
|
||||||
{"user1"},
|
|
||||||
},
|
},
|
||||||
|
expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"prjmaintainer", "pkgmaintainer"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "One project reviewer and one pkg reviewer only",
|
name: "One project reviewer and one pkg reviewer only",
|
||||||
prset: &common.PRSet{
|
config: common.AutogitConfig{
|
||||||
PRs: []*common.PRInfo{
|
GitProjectName: "repo",
|
||||||
{
|
Organization: "org",
|
||||||
PR: &models.PullRequest{
|
Branch: "main",
|
||||||
User: &models.User{UserName: "foo"},
|
Reviewers: []string{"-user1", "user2"},
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
PR: &models.PullRequest{
|
|
||||||
User: &models.User{UserName: "foo"},
|
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Config: &common.AutogitConfig{
|
|
||||||
GitProjectName: "prg/repo#main",
|
|
||||||
Organization: "org",
|
|
||||||
Branch: "main",
|
|
||||||
Reviewers: []string{"-user1", "user2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{}},
|
|
||||||
|
|
||||||
expected_missing_reviewers: [][]string{
|
|
||||||
[]string{"user2"},
|
|
||||||
[]string{"autogits_obs_staging_bot", "user1"},
|
|
||||||
},
|
},
|
||||||
|
expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"user2", "prjmaintainer", "pkgmaintainer"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "No need to get reviews of submitter reviewer",
|
name: "No need to get reviews of submitter",
|
||||||
prset: &common.PRSet{
|
config: common.AutogitConfig{
|
||||||
PRs: []*common.PRInfo{
|
GitProjectName: "repo",
|
||||||
{
|
Organization: "org",
|
||||||
PR: &models.PullRequest{
|
Branch: "main",
|
||||||
User: &models.User{UserName: "submitter"},
|
Reviewers: []string{"-user1", "submitter"},
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{
|
|
||||||
Reviews: []*models.PullReview{{State: common.ReviewStateRequestReview, User: &models.User{UserName: "m1"}}},
|
|
||||||
RequestedReviewers: []string{"m1"},
|
|
||||||
FullTimeline: []*models.TimelineComment{
|
|
||||||
{User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "m1"}, Type: common.TimelineCommentType_ReviewRequested},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
PR: &models.PullRequest{
|
|
||||||
User: &models.User{UserName: "foo"},
|
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Config: &common.AutogitConfig{
|
|
||||||
GitProjectName: "prg/repo#main",
|
|
||||||
Organization: "org",
|
|
||||||
Branch: "main",
|
|
||||||
Reviewers: []string{"-user1", "submitter"},
|
|
||||||
},
|
|
||||||
BotUser: "bot",
|
|
||||||
},
|
|
||||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"m1", "submitter"}}},
|
|
||||||
|
|
||||||
expected_missing_reviewers: [][]string{
|
|
||||||
nil,
|
|
||||||
{"autogits_obs_staging_bot", "user1"},
|
|
||||||
},
|
|
||||||
expected_extra_reviewers: [][]string{
|
|
||||||
{"m1"},
|
|
||||||
},
|
},
|
||||||
|
expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"prjmaintainer", "pkgmaintainer"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "No need to get reviews of submitter maintainer",
|
name: "Reviews are done",
|
||||||
prset: &common.PRSet{
|
config: common.AutogitConfig{
|
||||||
PRs: []*common.PRInfo{
|
GitProjectName: "repo",
|
||||||
{
|
Organization: "org",
|
||||||
PR: &models.PullRequest{
|
Branch: "main",
|
||||||
User: &models.User{UserName: "submitter"},
|
Reviewers: []string{"-user1", "user2"},
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
},
|
||||||
},
|
pkgReviews: []*models.PullReview{
|
||||||
Reviews: &common.PRReviews{},
|
{
|
||||||
},
|
State: common.ReviewStateApproved,
|
||||||
{
|
User: &models.User{UserName: "user2"},
|
||||||
PR: &models.PullRequest{
|
|
||||||
User: &models.User{UserName: "foo"},
|
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Config: &common.AutogitConfig{
|
{
|
||||||
GitProjectName: "prg/repo#main",
|
State: common.ReviewStateApproved,
|
||||||
Organization: "org",
|
User: &models.User{UserName: "pkgmaintainer"},
|
||||||
Branch: "main",
|
},
|
||||||
Reviewers: []string{"-user1", "submitter"},
|
{
|
||||||
|
State: common.ReviewStatePending,
|
||||||
|
User: &models.User{UserName: "prjmaintainer"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"submitter"}}},
|
prjReviews: []*models.PullReview{
|
||||||
|
{
|
||||||
expected_missing_reviewers: [][]string{
|
State: common.ReviewStateRequestChanges,
|
||||||
[]string{},
|
User: &models.User{UserName: "user1"},
|
||||||
[]string{"autogits_obs_staging_bot", "user1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "Add reviewer if also maintainer where review by maintainer is not needed",
|
|
||||||
prset: &common.PRSet{
|
|
||||||
PRs: []*common.PRInfo{
|
|
||||||
{
|
|
||||||
PR: &models.PullRequest{
|
|
||||||
User: &models.User{UserName: "submitter"},
|
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
PR: &models.PullRequest{
|
|
||||||
User: &models.User{UserName: "bot"},
|
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Config: &common.AutogitConfig{
|
{
|
||||||
GitProjectName: "prg/repo#main",
|
State: common.ReviewStateRequestReview,
|
||||||
Organization: "org",
|
User: &models.User{UserName: "autogits_obs_staging_bot"},
|
||||||
Branch: "main",
|
|
||||||
Reviewers: []string{"-user1", "submitter", "*reviewer"},
|
|
||||||
},
|
},
|
||||||
BotUser: "bot",
|
|
||||||
},
|
},
|
||||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"submitter", "reviewer"}, "": []string{"reviewer"}}},
|
expectedReviewerCall: [2][]string{},
|
||||||
|
|
||||||
expected_missing_reviewers: [][]string{
|
|
||||||
[]string{"reviewer"},
|
|
||||||
[]string{"autogits_obs_staging_bot", "reviewer", "user1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "Dont remove reviewer if also maintainer",
|
|
||||||
prset: &common.PRSet{
|
|
||||||
PRs: []*common.PRInfo{
|
|
||||||
{
|
|
||||||
PR: &models.PullRequest{
|
|
||||||
User: &models.User{UserName: "submitter"},
|
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{
|
|
||||||
Reviews: []*models.PullReview{{State: common.ReviewStateRequestReview, User: &models.User{UserName: "reviewer"}}},
|
|
||||||
RequestedReviewers: []string{"reviewer"},
|
|
||||||
FullTimeline: []*models.TimelineComment{{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "reviewer"}}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
PR: &models.PullRequest{
|
|
||||||
User: &models.User{UserName: "bot"},
|
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{
|
|
||||||
Reviews: []*models.PullReview{{State: common.ReviewStateRequestReview, User: &models.User{UserName: "reviewer"}}},
|
|
||||||
RequestedReviewers: []string{"reviewer"},
|
|
||||||
FullTimeline: []*models.TimelineComment{{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "reviewer"}}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Config: &common.AutogitConfig{
|
|
||||||
GitProjectName: "prg/repo#main",
|
|
||||||
Organization: "org",
|
|
||||||
Branch: "main",
|
|
||||||
Reviewers: []string{"-user1", "submitter", "*reviewer"},
|
|
||||||
},
|
|
||||||
BotUser: "bot",
|
|
||||||
},
|
|
||||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"submitter", "reviewer"}, "": []string{"reviewer"}}},
|
|
||||||
|
|
||||||
expected_missing_reviewers: [][]string{
|
|
||||||
[]string{},
|
|
||||||
[]string{"autogits_obs_staging_bot", "user1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: "Extra project reviewer on the package",
|
|
||||||
prset: &common.PRSet{
|
|
||||||
PRs: []*common.PRInfo{
|
|
||||||
{
|
|
||||||
PR: &models.PullRequest{
|
|
||||||
User: &models.User{UserName: "submitter"},
|
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{
|
|
||||||
Reviews: []*models.PullReview{
|
|
||||||
{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
|
|
||||||
{State: common.ReviewStateApproved, User: &models.User{UserName: "pkgmaintainer"}},
|
|
||||||
{State: common.ReviewStatePending, User: &models.User{UserName: "prjmaintainer"}},
|
|
||||||
},
|
|
||||||
RequestedReviewers: []string{"user2", "pkgmaintainer", "prjmaintainer"},
|
|
||||||
FullTimeline: []*models.TimelineComment{
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "user2"}},
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgmaintainer"}},
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prjmaintainer"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
PR: &models.PullRequest{
|
|
||||||
User: &models.User{UserName: "bot"},
|
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{
|
|
||||||
Reviews: []*models.PullReview{
|
|
||||||
{State: common.ReviewStateRequestChanges, User: &models.User{UserName: "user1"}},
|
|
||||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "autogits_obs_staging_bot"}},
|
|
||||||
},
|
|
||||||
RequestedReviewers: []string{"user1", "autogits_obs_staging_bot"},
|
|
||||||
FullTimeline: []*models.TimelineComment{
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "user1"}},
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "autogits_obs_staging_bot"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Config: &common.AutogitConfig{
|
|
||||||
GitProjectName: "prg/repo#main",
|
|
||||||
Organization: "org",
|
|
||||||
Branch: "main",
|
|
||||||
Reviewers: []string{"-user1", "submitter"},
|
|
||||||
},
|
|
||||||
BotUser: "bot",
|
|
||||||
},
|
|
||||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"pkgmaintainer"}, "": {"prjmaintainer"}}},
|
|
||||||
|
|
||||||
expected_missing_reviewers: [][]string{},
|
|
||||||
expected_extra_reviewers: [][]string{{"prjmaintainer"}},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Extra project reviewers on the package and project",
|
name: "Stale review is not done, re-request it",
|
||||||
prset: &common.PRSet{
|
config: common.AutogitConfig{
|
||||||
PRs: []*common.PRInfo{
|
GitProjectName: "repo",
|
||||||
{
|
Organization: "org",
|
||||||
PR: &models.PullRequest{
|
Branch: "main",
|
||||||
User: &models.User{UserName: "submitter"},
|
Reviewers: []string{"-user1", "user2"},
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{
|
|
||||||
Reviews: []*models.PullReview{
|
|
||||||
{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
|
|
||||||
{State: common.ReviewStateApproved, User: &models.User{UserName: "pkgmaintainer"}},
|
|
||||||
{State: common.ReviewStatePending, User: &models.User{UserName: "prjmaintainer"}},
|
|
||||||
{State: common.ReviewStatePending, User: &models.User{UserName: "pkgm1"}},
|
|
||||||
{State: common.ReviewStatePending, User: &models.User{UserName: "pkgm2"}},
|
|
||||||
{State: common.ReviewStatePending, User: &models.User{UserName: "prj1"}},
|
|
||||||
{State: common.ReviewStatePending, User: &models.User{UserName: "prj2"}},
|
|
||||||
{State: common.ReviewStatePending, User: &models.User{UserName: "someother"}},
|
|
||||||
},
|
|
||||||
RequestedReviewers: []string{"user2", "pkgmaintainer", "prjmaintainer", "pkgm1", "pkgm2", "someother", "prj1", "prj2"},
|
|
||||||
FullTimeline: []*models.TimelineComment{
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgmaintainer"}},
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prjmaintainer"}},
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj1"}},
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj2"}},
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgm1"}},
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgm2"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
PR: &models.PullRequest{
|
|
||||||
User: &models.User{UserName: "bot"},
|
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prg"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{
|
|
||||||
Reviews: []*models.PullReview{
|
|
||||||
{State: common.ReviewStateRequestChanges, User: &models.User{UserName: "user1"}},
|
|
||||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "autogits_obs_staging_bot"}},
|
|
||||||
{State: common.ReviewStatePending, User: &models.User{UserName: "prj1"}},
|
|
||||||
{State: common.ReviewStatePending, User: &models.User{UserName: "prj2"}},
|
|
||||||
},
|
|
||||||
RequestedReviewers: []string{"user1", "autogits_obs_staging_bot", "prj1", "prj2"},
|
|
||||||
FullTimeline: []*models.TimelineComment{
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "autogits_obs_staging_bot"}},
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj1"}},
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj2"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Config: &common.AutogitConfig{
|
|
||||||
GitProjectName: "prg/repo#main",
|
|
||||||
Organization: "org",
|
|
||||||
Branch: "main",
|
|
||||||
Reviewers: []string{"-user1", "submitter"},
|
|
||||||
},
|
|
||||||
BotUser: "bot",
|
|
||||||
},
|
},
|
||||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"pkgmaintainer", "pkgm1", "pkgm2"}, "": {"prjmaintainer", "prj1", "prj2"}}},
|
pkgReviews: []*models.PullReview{
|
||||||
|
{
|
||||||
expected_missing_reviewers: [][]string{},
|
State: common.ReviewStateApproved,
|
||||||
expected_extra_reviewers: [][]string{{"pkgm1", "pkgm2", "prj1", "prj2", "prjmaintainer"}, {"prj1", "prj2"}},
|
User: &models.User{UserName: "user2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
State: common.ReviewStatePending,
|
||||||
|
User: &models.User{UserName: "prjmaintainer"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prjReviews: []*models.PullReview{
|
||||||
|
{
|
||||||
|
State: common.ReviewStateRequestChanges,
|
||||||
|
User: &models.User{UserName: "user1"},
|
||||||
|
Stale: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
State: common.ReviewStateRequestReview,
|
||||||
|
Stale: true,
|
||||||
|
User: &models.User{UserName: "autogits_obs_staging_bot"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"pkgmaintainer"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "No extra project reviewers on the package and project (all pending)",
|
name: "Stale optional review is not done, re-request it",
|
||||||
prset: &common.PRSet{
|
config: common.AutogitConfig{
|
||||||
PRs: []*common.PRInfo{
|
GitProjectName: "repo",
|
||||||
{
|
Organization: "org",
|
||||||
PR: &models.PullRequest{
|
Branch: "main",
|
||||||
User: &models.User{UserName: "submitter"},
|
Reviewers: []string{"-user1", "user2", "~bot"},
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "pkg", Owner: &models.User{UserName: "org"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{
|
|
||||||
Reviews: []*models.PullReview{
|
|
||||||
{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
|
|
||||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "pkgmaintainer"}},
|
|
||||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "prjmaintainer"}},
|
|
||||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "pkgm1"}},
|
|
||||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "prj1"}},
|
|
||||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "someother"}},
|
|
||||||
},
|
|
||||||
RequestedReviewers: []string{"user2", "pkgmaintainer", "prjmaintainer", "pkgm1", "someother", "prj1"},
|
|
||||||
FullTimeline: []*models.TimelineComment{
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgm1"}},
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "pkgmaintainer"}},
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prjmaintainer"}},
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj1"}},
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "!bot"}, Assignee: &models.User{UserName: "someother"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
PR: &models.PullRequest{
|
|
||||||
User: &models.User{UserName: "bot"},
|
|
||||||
Base: &models.PRBranchInfo{Name: "main", Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "prj"}}},
|
|
||||||
},
|
|
||||||
Reviews: &common.PRReviews{
|
|
||||||
Reviews: []*models.PullReview{
|
|
||||||
{State: common.ReviewStateRequestChanges, User: &models.User{UserName: "user1"}},
|
|
||||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "autogits_obs_staging_bot"}},
|
|
||||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "prj1"}},
|
|
||||||
},
|
|
||||||
RequestedReviewers: []string{"user1", "autogits_obs_staging_bot", "prj1"},
|
|
||||||
FullTimeline: []*models.TimelineComment{
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "autogits_obs_staging_bot"}},
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "bot"}, Assignee: &models.User{UserName: "prj1"}},
|
|
||||||
{Type: common.TimelineCommentType_ReviewRequested, User: &models.User{UserName: "!bot"}, Assignee: &models.User{UserName: "user1"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Config: &common.AutogitConfig{
|
|
||||||
GitProjectName: "prj/repo#main",
|
|
||||||
Organization: "org",
|
|
||||||
Branch: "main",
|
|
||||||
Reviewers: []string{"-user1", "submitter"},
|
|
||||||
},
|
|
||||||
BotUser: "bot",
|
|
||||||
},
|
},
|
||||||
maintainers: &common.MaintainershipMap{Data: map[string][]string{"pkg": []string{"pkgmaintainer", "pkgm1", "pkgm2"}, "": {"prjmaintainer", "prj1", "prj2"}}},
|
pkgReviews: []*models.PullReview{
|
||||||
|
{
|
||||||
expected_missing_reviewers: [][]string{{"pkgm2", "prj2"}},
|
State: common.ReviewStateApproved,
|
||||||
expected_extra_reviewers: [][]string{{}, {"prj1"}},
|
User: &models.User{UserName: "bot"},
|
||||||
|
Stale: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
State: common.ReviewStateApproved,
|
||||||
|
User: &models.User{UserName: "user2"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
State: common.ReviewStatePending,
|
||||||
|
User: &models.User{UserName: "prjmaintainer"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prjReviews: []*models.PullReview{
|
||||||
|
{
|
||||||
|
State: common.ReviewStateRequestChanges,
|
||||||
|
User: &models.User{UserName: "user1"},
|
||||||
|
Stale: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
State: common.ReviewStateRequestReview,
|
||||||
|
Stale: true,
|
||||||
|
User: &models.User{UserName: "autogits_obs_staging_bot"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedReviewerCall: [2][]string{{"user1", "autogits_obs_staging_bot"}, {"pkgmaintainer", "bot"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
test.prset.HasAutoStaging = !test.noAutoStaging
|
ctl := gomock.NewController(t)
|
||||||
for idx, pr := range test.prset.PRs {
|
pr_mock := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
|
||||||
missing, extra := test.prset.FindMissingAndExtraReviewers(test.maintainers, idx)
|
review_mock := mock_common.NewMockGiteaReviewFetcherAndRequester(ctl)
|
||||||
|
maintainership_mock := mock_common.NewMockMaintainershipData(ctl)
|
||||||
|
|
||||||
// avoid nil dereference below, by adding empty array elements
|
if test.pkgTimeline == nil {
|
||||||
if idx >= len(test.expected_missing_reviewers) {
|
test.pkgTimeline = reviewsToTimeline(test.pkgReviews)
|
||||||
test.expected_missing_reviewers = append(test.expected_missing_reviewers, nil)
|
}
|
||||||
}
|
if test.prjTimeline == nil {
|
||||||
if idx >= len(test.expected_extra_reviewers) {
|
test.prjTimeline = reviewsToTimeline(test.prjReviews)
|
||||||
test.expected_extra_reviewers = append(test.expected_extra_reviewers, nil)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
slices.Sort(test.expected_extra_reviewers[idx])
|
pr_mock.EXPECT().GetPullRequest("other", "pkgrepo", int64(1)).Return(&models.PullRequest{
|
||||||
slices.Sort(test.expected_missing_reviewers[idx])
|
Body: "Some description is here",
|
||||||
if slices.Compare(missing, test.expected_missing_reviewers[idx]) != 0 {
|
User: &models.User{UserName: "submitter"},
|
||||||
t.Error("Expected missing reviewers for", common.PRtoString(pr.PR), ":", test.expected_missing_reviewers[idx], "but have:", missing)
|
RequestedReviewers: []*models.User{},
|
||||||
}
|
Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "pkgrepo", Owner: &models.User{UserName: "other"}}},
|
||||||
|
Head: &models.PRBranchInfo{},
|
||||||
|
Index: 1,
|
||||||
|
}, nil)
|
||||||
|
review_mock.EXPECT().GetPullRequestReviews("other", "pkgrepo", int64(1)).Return(test.pkgReviews, nil)
|
||||||
|
review_mock.EXPECT().GetTimeline("other", "pkgrepo", int64(1)).Return(test.pkgTimeline, nil)
|
||||||
|
pr_mock.EXPECT().GetPullRequest("org", "repo", int64(1)).Return(&models.PullRequest{
|
||||||
|
Body: fmt.Sprintf(common.PrPattern, "other", "pkgrepo", 1),
|
||||||
|
User: &models.User{UserName: "bot1"},
|
||||||
|
RequestedReviewers: []*models.User{{UserName: "main_reviewer"}},
|
||||||
|
Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "org"}}},
|
||||||
|
Head: &models.PRBranchInfo{},
|
||||||
|
Index: 42,
|
||||||
|
}, nil)
|
||||||
|
review_mock.EXPECT().GetPullRequestReviews("org", "repo", int64(42)).Return(test.prjReviews, nil)
|
||||||
|
review_mock.EXPECT().GetTimeline("org", "repo", int64(42)).Return(test.prjTimeline, nil)
|
||||||
|
|
||||||
if slices.Compare(extra, test.expected_extra_reviewers[idx]) != 0 {
|
maintainership_mock.EXPECT().ListProjectMaintainers().Return([]string{"prjmaintainer"}).AnyTimes()
|
||||||
t.Error("Expected reviewers to remove for", common.PRtoString(pr.PR), ":", test.expected_extra_reviewers[idx], "but have:", extra)
|
maintainership_mock.EXPECT().ListPackageMaintainers("pkgrepo").Return([]string{"pkgmaintainer"}).AnyTimes()
|
||||||
|
|
||||||
|
prs, _ := common.FetchPRSet("test", pr_mock, "other", "pkgrepo", int64(1), &test.config)
|
||||||
|
if len(prs.PRs) != 2 {
|
||||||
|
t.Fatal("PRs not fetched")
|
||||||
|
}
|
||||||
|
for _, pr := range prs.PRs {
|
||||||
|
r := test.expectedReviewerCall[0]
|
||||||
|
if !prs.IsPrjGitPR(pr.PR) {
|
||||||
|
r = test.expectedReviewerCall[1]
|
||||||
|
}
|
||||||
|
slices.Sort(r)
|
||||||
|
for _, reviewer := range r {
|
||||||
|
review_mock.EXPECT().RequestReviews(pr.PR, reviewer).Return(nil, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
prs.AssignReviewers(review_mock, maintainership_mock)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
prjgit_tests := []struct {
|
||||||
|
name string
|
||||||
|
config common.AutogitConfig
|
||||||
|
reviewers []struct {
|
||||||
|
org, repo string
|
||||||
|
num int64
|
||||||
|
reviewer string
|
||||||
|
}
|
||||||
|
|
||||||
|
prjReviews []*models.PullReview
|
||||||
|
|
||||||
|
expectedReviewerCall [2][]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "PrjMaintainers in prjgit review when not part of pkg set",
|
||||||
|
config: common.AutogitConfig{
|
||||||
|
GitProjectName: "repo",
|
||||||
|
Organization: "org",
|
||||||
|
Branch: "main",
|
||||||
|
Reviewers: []string{},
|
||||||
|
},
|
||||||
|
expectedReviewerCall: [2][]string{{"autogits_obs_staging_bot", "prjmaintainer"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range prjgit_tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
ctl := gomock.NewController(t)
|
||||||
|
pr_mock := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
|
||||||
|
review_mock := mock_common.NewMockGiteaReviewFetcherAndRequester(ctl)
|
||||||
|
maintainership_mock := mock_common.NewMockMaintainershipData(ctl)
|
||||||
|
|
||||||
|
pr_mock.EXPECT().GetPullRequest("org", "repo", int64(1)).Return(&models.PullRequest{
|
||||||
|
Body: "Some description is here",
|
||||||
|
User: &models.User{UserName: "submitter"},
|
||||||
|
RequestedReviewers: []*models.User{},
|
||||||
|
Base: &models.PRBranchInfo{Repo: &models.Repository{Name: "repo", Owner: &models.User{UserName: "org"}}},
|
||||||
|
Head: &models.PRBranchInfo{},
|
||||||
|
Index: 1,
|
||||||
|
}, nil)
|
||||||
|
review_mock.EXPECT().GetPullRequestReviews("org", "repo", int64(1)).Return(test.prjReviews, nil)
|
||||||
|
review_mock.EXPECT().GetTimeline("org", "repo", int64(1)).Return(nil, nil)
|
||||||
|
|
||||||
|
maintainership_mock.EXPECT().ListProjectMaintainers().Return([]string{"prjmaintainer"}).AnyTimes()
|
||||||
|
|
||||||
|
prs, _ := common.FetchPRSet("test", pr_mock, "org", "repo", int64(1), &test.config)
|
||||||
|
if len(prs.PRs) != 1 {
|
||||||
|
t.Fatal("PRs not fetched")
|
||||||
|
}
|
||||||
|
for _, pr := range prs.PRs {
|
||||||
|
r := test.expectedReviewerCall[0]
|
||||||
|
if !prs.IsPrjGitPR(pr.PR) {
|
||||||
|
t.Fatal("only prjgit pr here")
|
||||||
|
}
|
||||||
|
for _, reviewer := range r {
|
||||||
|
review_mock.EXPECT().RequestReviews(pr.PR, reviewer).Return(nil, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prs.AssignReviewers(review_mock, maintainership_mock)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPRMerge(t *testing.T) {
|
func TestPRMerge(t *testing.T) {
|
||||||
t.Skip("FAIL: No PrjGit PR found, missing calls")
|
|
||||||
repoDir := t.TempDir()
|
repoDir := t.TempDir()
|
||||||
|
|
||||||
cwd, _ := os.Getwd()
|
cwd, _ := os.Getwd()
|
||||||
@@ -1151,7 +923,7 @@ func TestPRMerge(t *testing.T) {
|
|||||||
|
|
||||||
config := &common.AutogitConfig{
|
config := &common.AutogitConfig{
|
||||||
Organization: "org",
|
Organization: "org",
|
||||||
GitProjectName: "org/prj#master",
|
GitProjectName: "prj",
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -1203,7 +975,7 @@ func TestPRMerge(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
ctl := gomock.NewController(t)
|
ctl := gomock.NewController(t)
|
||||||
mock := mock_common.NewMockGiteaPRTimelineReviewFetcher(ctl)
|
mock := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
|
||||||
reviewUnrequestMock := mock_common.NewMockGiteaReviewUnrequester(ctl)
|
reviewUnrequestMock := mock_common.NewMockGiteaReviewUnrequester(ctl)
|
||||||
|
|
||||||
reviewUnrequestMock.EXPECT().UnrequestReview(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
|
reviewUnrequestMock.EXPECT().UnrequestReview(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
|
||||||
@@ -1231,7 +1003,6 @@ func TestPRMerge(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPRChanges(t *testing.T) {
|
func TestPRChanges(t *testing.T) {
|
||||||
t.Skip("FAIL: unexpected calls, missing calls")
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
PRs []*models.PullRequest
|
PRs []*models.PullRequest
|
||||||
@@ -1262,7 +1033,7 @@ func TestPRChanges(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
ctl := gomock.NewController(t)
|
ctl := gomock.NewController(t)
|
||||||
mock_fetcher := mock_common.NewMockGiteaPRTimelineReviewFetcher(ctl)
|
mock_fetcher := mock_common.NewMockGiteaPRTimelineFetcher(ctl)
|
||||||
mock_fetcher.EXPECT().GetPullRequest("org", "prjgit", int64(42)).Return(test.PrjPRs, nil)
|
mock_fetcher.EXPECT().GetPullRequest("org", "prjgit", int64(42)).Return(test.PrjPRs, nil)
|
||||||
for _, pr := range test.PRs {
|
for _, pr := range test.PRs {
|
||||||
mock_fetcher.EXPECT().GetPullRequest(pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Index).Return(pr, nil)
|
mock_fetcher.EXPECT().GetPullRequest(pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Index).Return(pr, nil)
|
||||||
|
|||||||
@@ -1,238 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of Autogits.
|
|
||||||
*
|
|
||||||
* Copyright © 2024 SUSE LLC
|
|
||||||
*
|
|
||||||
* Autogits is free software: you can redistribute it and/or modify it under
|
|
||||||
* the terms of the GNU General Public License as published by the Free Software
|
|
||||||
* Foundation, either version 2 of the License, or (at your option) any later
|
|
||||||
* version.
|
|
||||||
*
|
|
||||||
* Autogits is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
|
||||||
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with
|
|
||||||
* Foobar. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
rabbitmq "github.com/rabbitmq/amqp091-go"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RabbitConnection struct {
|
|
||||||
RabbitURL *url.URL // amqps://user:password@host/queue
|
|
||||||
|
|
||||||
queueName string
|
|
||||||
ch *rabbitmq.Channel
|
|
||||||
|
|
||||||
topics []string
|
|
||||||
topicSubChanges chan string // +topic = subscribe, -topic = unsubscribe
|
|
||||||
}
|
|
||||||
|
|
||||||
type RabbitProcessor interface {
|
|
||||||
GenerateTopics() []string
|
|
||||||
|
|
||||||
Connection() *RabbitConnection
|
|
||||||
ProcessRabbitMessage(msg RabbitMessage) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type RabbitMessage rabbitmq.Delivery
|
|
||||||
|
|
||||||
func (l *RabbitConnection) ProcessTopicChanges() {
|
|
||||||
for {
|
|
||||||
topic, ok := <-l.topicSubChanges
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
LogDebug(" topic change:", topic)
|
|
||||||
switch topic[0] {
|
|
||||||
case '+':
|
|
||||||
if err := l.ch.QueueBind(l.queueName, topic[1:], "pubsub", false, nil); err != nil {
|
|
||||||
LogError(err)
|
|
||||||
}
|
|
||||||
case '-':
|
|
||||||
if err := l.ch.QueueUnbind(l.queueName, topic[1:], "pubsub", nil); err != nil {
|
|
||||||
LogError(err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
LogInfo("Ignoring unknown topic change:", topic)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *RabbitConnection) ProcessRabbitMQ(msgCh chan<- RabbitMessage) error {
|
|
||||||
queueName := l.RabbitURL.Path
|
|
||||||
l.RabbitURL.Path = ""
|
|
||||||
|
|
||||||
if len(queueName) > 0 && queueName[0] == '/' {
|
|
||||||
queueName = queueName[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
connection, err := rabbitmq.DialTLS(l.RabbitURL.String(), &tls.Config{
|
|
||||||
ServerName: l.RabbitURL.Hostname(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Cannot connect to %s . Err: %w", l.RabbitURL.Hostname(), err)
|
|
||||||
}
|
|
||||||
defer connection.Close()
|
|
||||||
|
|
||||||
l.ch, err = connection.Channel()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Cannot create a channel. Err: %w", err)
|
|
||||||
}
|
|
||||||
defer l.ch.Close()
|
|
||||||
|
|
||||||
if err = l.ch.ExchangeDeclarePassive("pubsub", "topic", true, false, false, false, nil); err != nil {
|
|
||||||
return fmt.Errorf("Cannot find pubsub exchange? Err: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var q rabbitmq.Queue
|
|
||||||
if len(queueName) == 0 {
|
|
||||||
q, err = l.ch.QueueDeclare("", false, true, true, false, nil)
|
|
||||||
} else {
|
|
||||||
q, err = l.ch.QueueDeclarePassive(queueName, true, false, true, false, nil)
|
|
||||||
if err != nil {
|
|
||||||
LogInfo("queue not found .. trying to create it:", err)
|
|
||||||
if l.ch.IsClosed() {
|
|
||||||
l.ch, err = connection.Channel()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Channel cannot be re-opened. Err: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
q, err = l.ch.QueueDeclare(queueName, true, false, true, false, nil)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
LogInfo("can't create persistent queue ... falling back to temporaty queue:", err)
|
|
||||||
if l.ch.IsClosed() {
|
|
||||||
l.ch, err = connection.Channel()
|
|
||||||
return fmt.Errorf("Channel cannot be re-opened. Err: %w", err)
|
|
||||||
}
|
|
||||||
q, err = l.ch.QueueDeclare("", false, true, true, false, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Cannot declare queue. Err: %w", err)
|
|
||||||
}
|
|
||||||
// log.Printf("queue: %s:%d", q.Name, q.Consumers)
|
|
||||||
|
|
||||||
LogDebug(" -- listening to topics:")
|
|
||||||
l.topicSubChanges = make(chan string)
|
|
||||||
defer close(l.topicSubChanges)
|
|
||||||
go l.ProcessTopicChanges()
|
|
||||||
|
|
||||||
for _, topic := range l.topics {
|
|
||||||
l.topicSubChanges <- "+" + topic
|
|
||||||
}
|
|
||||||
|
|
||||||
msgs, err := l.ch.Consume(q.Name, "", true, true, false, false, nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Cannot start consumer. Err: %w", err)
|
|
||||||
}
|
|
||||||
// log.Printf("queue: %s:%d", q.Name, q.Consumers)
|
|
||||||
|
|
||||||
for {
|
|
||||||
msg, ok := <-msgs
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("channel/connection closed?\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
msgCh <- RabbitMessage(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *RabbitConnection) ConnectAndProcessRabbitMQ(ch chan<- RabbitMessage) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
LogError(r)
|
|
||||||
LogError("'crash' RabbitMQ worker. Recovering... reconnecting...")
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
go l.ConnectAndProcessRabbitMQ(ch)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
err := l.ProcessRabbitMQ(ch)
|
|
||||||
if err != nil {
|
|
||||||
LogError("Error in RabbitMQ connection:", err)
|
|
||||||
LogInfo("Reconnecting in 2 seconds...")
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *RabbitConnection) ConnectToRabbitMQ(processor RabbitProcessor) <-chan RabbitMessage {
|
|
||||||
LogInfo("RabbitMQ connection:", l.RabbitURL.String())
|
|
||||||
|
|
||||||
l.RabbitURL.User = url.UserPassword(rabbitUser, rabbitPassword)
|
|
||||||
l.topics = processor.GenerateTopics()
|
|
||||||
|
|
||||||
ch := make(chan RabbitMessage, 100)
|
|
||||||
go l.ConnectAndProcessRabbitMQ(ch)
|
|
||||||
|
|
||||||
return ch
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *RabbitConnection) UpdateTopics(processor RabbitProcessor) {
|
|
||||||
newTopics := processor.GenerateTopics()
|
|
||||||
|
|
||||||
j := 0
|
|
||||||
next_new_topic:
|
|
||||||
for i := 0; i < len(newTopics); i++ {
|
|
||||||
topic := newTopics[i]
|
|
||||||
|
|
||||||
for j < len(l.topics) {
|
|
||||||
cmp := strings.Compare(topic, l.topics[j])
|
|
||||||
|
|
||||||
if cmp == 0 {
|
|
||||||
j++
|
|
||||||
continue next_new_topic
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmp < 0 {
|
|
||||||
l.topicSubChanges <- "+" + topic
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
l.topicSubChanges <- "-" + l.topics[j]
|
|
||||||
j++
|
|
||||||
}
|
|
||||||
|
|
||||||
if j == len(l.topics) {
|
|
||||||
l.topicSubChanges <- "+" + topic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for j < len(l.topics) {
|
|
||||||
l.topicSubChanges <- "-" + l.topics[j]
|
|
||||||
j++
|
|
||||||
}
|
|
||||||
|
|
||||||
l.topics = newTopics
|
|
||||||
}
|
|
||||||
|
|
||||||
func ProcessRabbitMQEvents(processor RabbitProcessor) error {
|
|
||||||
ch := processor.Connection().ConnectToRabbitMQ(processor)
|
|
||||||
|
|
||||||
for {
|
|
||||||
msg, ok := <-ch
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
LogDebug("event:", msg.RoutingKey)
|
|
||||||
if err := processor.ProcessRabbitMessage(msg); err != nil {
|
|
||||||
LogError("Error processing", msg.RoutingKey, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of Autogits.
|
|
||||||
*
|
|
||||||
* Copyright © 2024 SUSE LLC
|
|
||||||
*
|
|
||||||
* Autogits is free software: you can redistribute it and/or modify it under
|
|
||||||
* the terms of the GNU General Public License as published by the Free Software
|
|
||||||
* Foundation, either version 2 of the License, or (at your option) any later
|
|
||||||
* version.
|
|
||||||
*
|
|
||||||
* Autogits is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
|
||||||
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with
|
|
||||||
* Foobar. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"runtime/debug"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const RequestType_CreateBrachTag = "create"
|
|
||||||
const RequestType_DeleteBranchTag = "delete"
|
|
||||||
const RequestType_Fork = "fork"
|
|
||||||
const RequestType_Issue = "issues"
|
|
||||||
const RequestType_IssueAssign = "issue_assign"
|
|
||||||
const RequestType_IssueComment = "issue_comment"
|
|
||||||
const RequestType_IssueLabel = "issue_label"
|
|
||||||
const RequestType_IssueMilestone = "issue_milestone"
|
|
||||||
const RequestType_Push = "push"
|
|
||||||
const RequestType_Repository = "repository"
|
|
||||||
const RequestType_Release = "release"
|
|
||||||
const RequestType_PR = "pull_request"
|
|
||||||
const RequestType_PRAssign = "pull_request_assign"
|
|
||||||
const RequestType_PRLabel = "pull_request_label"
|
|
||||||
const RequestType_PRComment = "pull_request_comment"
|
|
||||||
const RequestType_PRMilestone = "pull_request_milestone"
|
|
||||||
const RequestType_PRSync = "pull_request_sync"
|
|
||||||
const RequestType_PRReviewAccepted = "pull_request_review_approved"
|
|
||||||
const RequestType_PRReviewRejected = "pull_request_review_rejected"
|
|
||||||
const RequestType_PRReviewRequest = "pull_request_review_request"
|
|
||||||
const RequestType_PRReviewComment = "pull_request_review_comment"
|
|
||||||
const RequestType_Status = "status"
|
|
||||||
const RequestType_Wiki = "wiki"
|
|
||||||
|
|
||||||
type RequestProcessor interface {
|
|
||||||
ProcessFunc(*Request) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type RabbitMQGiteaEventsProcessor struct {
|
|
||||||
Handlers map[string]RequestProcessor
|
|
||||||
Orgs []string
|
|
||||||
|
|
||||||
c *RabbitConnection
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gitea *RabbitMQGiteaEventsProcessor) Connection() *RabbitConnection {
|
|
||||||
if gitea.c == nil {
|
|
||||||
gitea.c = &RabbitConnection{}
|
|
||||||
}
|
|
||||||
return gitea.c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gitea *RabbitMQGiteaEventsProcessor) GenerateTopics() []string {
|
|
||||||
topics := make([]string, 0, len(gitea.Handlers)*len(gitea.Orgs))
|
|
||||||
scope := "suse"
|
|
||||||
if gitea.c.RabbitURL.Hostname() == "rabbit.opensuse.org" {
|
|
||||||
scope = "opensuse"
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, org := range gitea.Orgs {
|
|
||||||
for requestType, _ := range gitea.Handlers {
|
|
||||||
topics = append(topics, fmt.Sprintf("%s.src.%s.%s.#", scope, org, requestType))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
slices.Sort(topics)
|
|
||||||
return slices.Compact(topics)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gitea *RabbitMQGiteaEventsProcessor) ProcessRabbitMessage(msg RabbitMessage) error {
|
|
||||||
route := strings.Split(msg.RoutingKey, ".")
|
|
||||||
if len(route) > 3 {
|
|
||||||
reqType := route[3]
|
|
||||||
org := route[2]
|
|
||||||
|
|
||||||
if !slices.Contains(gitea.Orgs, org) {
|
|
||||||
LogInfo("Got event for unhandeled org:", org)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
LogDebug("org:", org, "type:", reqType)
|
|
||||||
if handler, found := gitea.Handlers[reqType]; found {
|
|
||||||
req, err := ParseRequestJSON(reqType, msg.Body)
|
|
||||||
if err != nil {
|
|
||||||
LogError("Error parsing request JSON:", err)
|
|
||||||
} else {
|
|
||||||
LogDebug("processing req", req.Type)
|
|
||||||
// h.Request = req
|
|
||||||
ProcessEvent(handler, req)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("Invalid routing key: %s", route)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ProcessEvent(f RequestProcessor, request *Request) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
LogError("panic caught")
|
|
||||||
if err, ok := r.(error); !ok {
|
|
||||||
LogError(err)
|
|
||||||
}
|
|
||||||
LogError(string(debug.Stack()))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := f.ProcessFunc(request); err != nil {
|
|
||||||
LogError(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
type RabbitMQObsBuildStatusProcessor struct {
|
|
||||||
c *RabbitConnection
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *RabbitMQObsBuildStatusProcessor) GenerateTopics() []string {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *RabbitMQObsBuildStatusProcessor) Connection() *RabbitConnection {
|
|
||||||
if o.c == nil {
|
|
||||||
o.c = &RabbitConnection{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return o.c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *RabbitMQObsBuildStatusProcessor) ProcessRabbitMessage(msg RabbitMessage) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of Autogits.
|
|
||||||
*
|
|
||||||
* Copyright © 2024 SUSE LLC
|
|
||||||
*
|
|
||||||
* Autogits is free software: you can redistribute it and/or modify it under
|
|
||||||
* the terms of the GNU General Public License as published by the Free Software
|
|
||||||
* Foundation, either version 2 of the License, or (at your option) any later
|
|
||||||
* version.
|
|
||||||
*
|
|
||||||
* Autogits is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
|
|
||||||
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with
|
|
||||||
* Foobar. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Status struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatusWebhookEvent struct {
|
|
||||||
Id uint64
|
|
||||||
Context string
|
|
||||||
Description string
|
|
||||||
Sha string
|
|
||||||
State string
|
|
||||||
TargetUrl string
|
|
||||||
|
|
||||||
Commit Commit
|
|
||||||
Repository Repository
|
|
||||||
Sender *User
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StatusWebhookEvent) GetAction() string {
|
|
||||||
return s.State
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *RequestHandler) ParseStatusRequest(data io.Reader) (*StatusWebhookEvent, error) {
|
|
||||||
action := new(StatusWebhookEvent)
|
|
||||||
err := json.NewDecoder(data).Decode(&action)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Got error while parsing: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
h.StdLogger.Printf("Request status for repo: %s#%s\n", action.Repository.Full_Name, action.Sha)
|
|
||||||
h.Request = &Request{
|
|
||||||
Type: RequestType_Status,
|
|
||||||
Data: action,
|
|
||||||
}
|
|
||||||
|
|
||||||
return action, nil
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package common_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"src.opensuse.org/autogits/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestStatusRequestParsing(t *testing.T) {
|
|
||||||
t.Run("parsing repo creation message", func(t *testing.T) {
|
|
||||||
var h common.RequestHandler
|
|
||||||
|
|
||||||
h.StdLogger, h.ErrLogger = common.CreateStdoutLogger(os.Stdout, os.Stdout)
|
|
||||||
json, err := h.ParseStatusRequest(strings.NewReader(requestStatusJSON))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Can't parse struct: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if json.GetAction() != "pending" {
|
|
||||||
t.Fatalf("json.action is '%#v'", json)
|
|
||||||
}
|
|
||||||
|
|
||||||
if json.Repository.Full_Name != "autogits/nodejs-common" ||
|
|
||||||
json.Repository.Parent == nil ||
|
|
||||||
json.Repository.Parent.Parent != nil ||
|
|
||||||
len(json.Repository.Ssh_Url) < 10 ||
|
|
||||||
json.Repository.Default_Branch != "factory" ||
|
|
||||||
json.Repository.Object_Format_Name != "sha256" {
|
|
||||||
|
|
||||||
t.Fatalf("invalid repository parse: %#v", json.Repository)
|
|
||||||
}
|
|
||||||
|
|
||||||
if json.Sha != "e637d86cbbdd438edbf60148e28f9d75a74d51b27b01f75610f247cd18394c8e" {
|
|
||||||
t.Fatal("Invalid SHA:", json.Sha)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (group *ReviewGroup) ExpandMaintainers(maintainers []string) []string {
|
|
||||||
idx := slices.Index(maintainers, group.Name)
|
|
||||||
if idx == -1 {
|
|
||||||
return maintainers
|
|
||||||
}
|
|
||||||
|
|
||||||
expandedMaintainers := slices.Replace(maintainers, idx, idx+1, group.Reviewers...)
|
|
||||||
slices.Sort(expandedMaintainers)
|
|
||||||
return slices.Compact(expandedMaintainers)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package common_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"slices"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"src.opensuse.org/autogits/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMaintainerGroupReplacer(t *testing.T) {
|
|
||||||
GroupName := "my_group"
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
reviewers []string
|
|
||||||
group_members []string
|
|
||||||
|
|
||||||
output []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "group not maintainer",
|
|
||||||
reviewers: []string{"a", "b"},
|
|
||||||
group_members: []string{"g1", "g2"},
|
|
||||||
output: []string{"a", "b"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "group maintainer",
|
|
||||||
reviewers: []string{"b", "my_group"},
|
|
||||||
group_members: []string{"g1", "g2"},
|
|
||||||
output: []string{"b", "g1", "g2"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "sorted group maintainer",
|
|
||||||
reviewers: []string{"my_group", "b"},
|
|
||||||
group_members: []string{"g1", "g2"},
|
|
||||||
output: []string{"b", "g1", "g2"},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "group maintainer dedup",
|
|
||||||
reviewers: []string{"my_group", "g2", "b"},
|
|
||||||
group_members: []string{"g1", "g2"},
|
|
||||||
output: []string{"b", "g1", "g2"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
g := &common.ReviewGroup{
|
|
||||||
Name: GroupName,
|
|
||||||
Reviewers: test.group_members,
|
|
||||||
}
|
|
||||||
|
|
||||||
expandedList := g.ExpandMaintainers(test.reviewers)
|
|
||||||
if slices.Compare(expandedList, test.output) != 0 {
|
|
||||||
t.Error("Expected:", test.output, "but have", expandedList)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
type Reviewers struct {
|
type Reviewers struct {
|
||||||
Prj []string
|
Prj []string
|
||||||
Pkg []string
|
Pkg []string
|
||||||
@@ -32,5 +36,10 @@ func ParseReviewers(input []string) *Reviewers {
|
|||||||
*pkg = append(*pkg, reviewer)
|
*pkg = append(*pkg, reviewer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !slices.Contains(r.Prj, Bot_BuildReview) {
|
||||||
|
r.Prj = append(r.Prj, Bot_BuildReview)
|
||||||
|
}
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ func TestReviewers(t *testing.T) {
|
|||||||
name: "project and package reviewers",
|
name: "project and package reviewers",
|
||||||
input: []string{"1", "2", "3", "*5", "+6", "-7"},
|
input: []string{"1", "2", "3", "*5", "+6", "-7"},
|
||||||
|
|
||||||
prj: []string{"5", "7"},
|
prj: []string{"5", "7", common.Bot_BuildReview},
|
||||||
pkg: []string{"1", "2", "3", "5", "6"},
|
pkg: []string{"1", "2", "3", "5", "6"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "optional project and package reviewers",
|
name: "optional project and package reviewers",
|
||||||
input: []string{"~1", "2", "3", "~*5", "+6", "-7"},
|
input: []string{"~1", "2", "3", "~*5", "+6", "-7"},
|
||||||
|
|
||||||
prj: []string{"7"},
|
prj: []string{"7", common.Bot_BuildReview},
|
||||||
pkg: []string{"2", "3", "6"},
|
pkg: []string{"2", "3", "6"},
|
||||||
prj_optional: []string{"5"},
|
prj_optional: []string{"5"},
|
||||||
pkg_optional: []string{"1", "5"},
|
pkg_optional: []string{"1", "5"},
|
||||||
|
|||||||
@@ -9,14 +9,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PRReviews struct {
|
type PRReviews struct {
|
||||||
Reviews []*models.PullReview
|
reviews []*models.PullReview
|
||||||
RequestedReviewers []string
|
reviewers []string
|
||||||
Comments []*models.TimelineComment
|
comments []*models.TimelineComment
|
||||||
|
|
||||||
FullTimeline []*models.TimelineComment
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, org, repo string, no int64) (*PRReviews, error) {
|
func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, reviewers []string, org, repo string, no int64) (*PRReviews, error) {
|
||||||
timeline, err := rf.GetTimeline(org, repo, no)
|
timeline, err := rf.GetTimeline(org, repo, no)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -27,14 +25,10 @@ func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, org, repo string, no int64
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
reviews := make([]*models.PullReview, 0, 10)
|
reviews := make([]*models.PullReview, 0, len(reviewers))
|
||||||
needNewReviews := []string{}
|
|
||||||
var comments []*models.TimelineComment
|
var comments []*models.TimelineComment
|
||||||
|
|
||||||
alreadyHaveUserReview := func(user string) bool {
|
alreadyHaveUserReview := func(user string) bool {
|
||||||
if slices.Contains(needNewReviews, user) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, r := range reviews {
|
for _, r := range reviews {
|
||||||
if r.User != nil && r.User.UserName == user {
|
if r.User != nil && r.User.UserName == user {
|
||||||
return true
|
return true
|
||||||
@@ -43,40 +37,32 @@ func FetchGiteaReviews(rf GiteaReviewTimelineFetcher, org, repo string, no int64
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
LogDebug("FetchingGiteaReviews for", org, repo, no)
|
|
||||||
LogDebug("Number of reviews:", len(rawReviews))
|
|
||||||
LogDebug("Number of items in timeline:", len(timeline))
|
|
||||||
|
|
||||||
cutOffIdx := len(timeline)
|
|
||||||
for idx, item := range timeline {
|
for idx, item := range timeline {
|
||||||
if item.Type == TimelineCommentType_Review || item.Type == TimelineCommentType_ReviewRequested {
|
if item.Type == TimelineCommentType_Review {
|
||||||
for _, r := range rawReviews {
|
for _, r := range rawReviews {
|
||||||
if r.ID == item.ReviewID {
|
if r.ID == item.ReviewID {
|
||||||
if !alreadyHaveUserReview(r.User.UserName) {
|
if !alreadyHaveUserReview(r.User.UserName) {
|
||||||
if item.Type == TimelineCommentType_Review && idx > cutOffIdx {
|
reviews = append(reviews, r)
|
||||||
needNewReviews = append(needNewReviews, r.User.UserName)
|
|
||||||
} else {
|
|
||||||
reviews = append(reviews, r)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if item.Type == TimelineCommentType_Comment && cutOffIdx > idx {
|
} else if item.Type == TimelineCommentType_Comment {
|
||||||
comments = append(comments, item)
|
comments = append(comments, item)
|
||||||
} else if item.Type == TimelineCommentType_PushPull && cutOffIdx == len(timeline) {
|
} else if item.Type == TimelineCommentType_PushPull {
|
||||||
LogDebug("cut-off", item.Created, "@", idx)
|
LogDebug("cut-off", item.Created)
|
||||||
cutOffIdx = idx
|
timeline = timeline[0:idx]
|
||||||
|
break
|
||||||
} else {
|
} else {
|
||||||
LogDebug("Unhandled timeline type:", item.Type)
|
LogDebug("Unhandled timeline type:", item.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LogDebug("num comments:", len(comments), "timeline:", len(reviews))
|
LogDebug("num comments:", len(comments), "reviews:", len(reviews), len(timeline))
|
||||||
|
|
||||||
return &PRReviews{
|
return &PRReviews{
|
||||||
Reviews: reviews,
|
reviews: reviews,
|
||||||
Comments: comments,
|
reviewers: reviewers,
|
||||||
FullTimeline: timeline,
|
comments: comments,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,27 +81,23 @@ func bodyCommandManualMergeOK(body string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *PRReviews) IsManualMergeOK() bool {
|
func (r *PRReviews) IsManualMergeOK() bool {
|
||||||
if r == nil {
|
for _, c := range r.comments {
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range r.Comments {
|
|
||||||
if c.Updated != c.Created {
|
if c.Updated != c.Created {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
LogDebug("comment:", c.User.UserName, c.Body)
|
LogDebug("comment:", c.User.UserName, c.Body)
|
||||||
if slices.Contains(r.RequestedReviewers, c.User.UserName) {
|
if slices.Contains(r.reviewers, c.User.UserName) {
|
||||||
if bodyCommandManualMergeOK(c.Body) {
|
if bodyCommandManualMergeOK(c.Body) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range r.Reviews {
|
for _, c := range r.reviews {
|
||||||
if c.Updated != c.Submitted {
|
if c.Updated != c.Submitted {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if slices.Contains(r.RequestedReviewers, c.User.UserName) {
|
if slices.Contains(r.reviewers, c.User.UserName) {
|
||||||
if bodyCommandManualMergeOK(c.Body) {
|
if bodyCommandManualMergeOK(c.Body) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -126,14 +108,11 @@ func (r *PRReviews) IsManualMergeOK() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *PRReviews) IsApproved() bool {
|
func (r *PRReviews) IsApproved() bool {
|
||||||
if r == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
goodReview := true
|
goodReview := true
|
||||||
|
|
||||||
for _, reviewer := range r.RequestedReviewers {
|
for _, reviewer := range r.reviewers {
|
||||||
goodReview = false
|
goodReview = false
|
||||||
for _, review := range r.Reviews {
|
for _, review := range r.reviews {
|
||||||
if review.User.UserName == reviewer && review.State == ReviewStateApproved && !review.Stale && !review.Dismissed {
|
if review.User.UserName == reviewer && review.State == ReviewStateApproved && !review.Stale && !review.Dismissed {
|
||||||
LogDebug(" -- found review: ", review.User.UserName)
|
LogDebug(" -- found review: ", review.User.UserName)
|
||||||
goodReview = true
|
goodReview = true
|
||||||
@@ -149,78 +128,45 @@ func (r *PRReviews) IsApproved() bool {
|
|||||||
return goodReview
|
return goodReview
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *PRReviews) MissingReviews() []string {
|
|
||||||
missing := []string{}
|
|
||||||
if r == nil {
|
|
||||||
return missing
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, reviewer := range r.RequestedReviewers {
|
|
||||||
if !r.IsReviewedBy(reviewer) {
|
|
||||||
missing = append(missing, reviewer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return missing
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PRReviews) FindReviewRequester(reviewer string) *models.TimelineComment {
|
|
||||||
if r == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, r := range r.FullTimeline {
|
|
||||||
if r.Type == TimelineCommentType_ReviewRequested && r.Assignee.UserName == reviewer {
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *PRReviews) HasPendingReviewBy(reviewer string) bool {
|
func (r *PRReviews) HasPendingReviewBy(reviewer string) bool {
|
||||||
if r == nil {
|
if !slices.Contains(r.reviewers, reviewer) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range r.Reviews {
|
isPending := false
|
||||||
if r.User.UserName == reviewer {
|
for _, r := range r.reviews {
|
||||||
|
if r.User.UserName == reviewer && !r.Stale {
|
||||||
switch r.State {
|
switch r.State {
|
||||||
case ReviewStateRequestReview, ReviewStatePending:
|
case ReviewStateApproved:
|
||||||
return true
|
fallthrough
|
||||||
default:
|
case ReviewStateRequestChanges:
|
||||||
return false
|
return false
|
||||||
|
case ReviewStateRequestReview:
|
||||||
|
fallthrough
|
||||||
|
case ReviewStatePending:
|
||||||
|
isPending = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return isPending
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *PRReviews) IsReviewedBy(reviewer string) bool {
|
func (r *PRReviews) IsReviewedBy(reviewer string) bool {
|
||||||
if r == nil {
|
if !slices.Contains(r.reviewers, reviewer) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range r.Reviews {
|
for _, r := range r.reviews {
|
||||||
if r.User.UserName == reviewer && !r.Stale {
|
if r.User.UserName == reviewer && !r.Stale {
|
||||||
switch r.State {
|
switch r.State {
|
||||||
case ReviewStateApproved, ReviewStateRequestChanges:
|
case ReviewStateApproved:
|
||||||
|
return true
|
||||||
|
case ReviewStateRequestChanges:
|
||||||
return true
|
return true
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *PRReviews) IsReviewedByOneOf(reviewers ...string) bool {
|
|
||||||
for _, reviewer := range reviewers {
|
|
||||||
if r.IsReviewedBy(reviewer) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -62,23 +62,11 @@ func TestReviews(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Two reviewer, one stale and pending",
|
name: "Two reviewer, one stale and pending",
|
||||||
reviews: []*models.PullReview{
|
reviews: []*models.PullReview{
|
||||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "user1"}, Stale: true},
|
&models.PullReview{State: common.ReviewStateRequestReview, User: &models.User{UserName: "user1"}, Stale: true},
|
||||||
},
|
},
|
||||||
reviewers: []string{"user1", "user2"},
|
reviewers: []string{"user1", "user2"},
|
||||||
isApproved: false,
|
isApproved: false,
|
||||||
isPendingByTest1: true,
|
isPendingByTest1: false,
|
||||||
isReviewedByTest1: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Two reviewer, one stale and pending, other done",
|
|
||||||
reviews: []*models.PullReview{
|
|
||||||
{State: common.ReviewStateRequestReview, User: &models.User{UserName: "user1"}},
|
|
||||||
{State: common.ReviewStateRequestChanges, User: &models.User{UserName: "user1"}},
|
|
||||||
{State: common.ReviewStateApproved, User: &models.User{UserName: "user2"}},
|
|
||||||
},
|
|
||||||
reviewers: []string{"user1", "user2"},
|
|
||||||
isApproved: false,
|
|
||||||
isPendingByTest1: true,
|
|
||||||
isReviewedByTest1: false,
|
isReviewedByTest1: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -151,7 +139,7 @@ func TestReviews(t *testing.T) {
|
|||||||
rf.EXPECT().GetTimeline("test", "pr", int64(1)).Return(test.timeline, nil)
|
rf.EXPECT().GetTimeline("test", "pr", int64(1)).Return(test.timeline, nil)
|
||||||
rf.EXPECT().GetPullRequestReviews("test", "pr", int64(1)).Return(test.reviews, test.fetchErr)
|
rf.EXPECT().GetPullRequestReviews("test", "pr", int64(1)).Return(test.reviews, test.fetchErr)
|
||||||
|
|
||||||
reviews, err := common.FetchGiteaReviews(rf, "test", "pr", 1)
|
reviews, err := common.FetchGiteaReviews(rf, test.reviewers, "test", "pr", 1)
|
||||||
|
|
||||||
if test.fetchErr != nil {
|
if test.fetchErr != nil {
|
||||||
if err != test.fetchErr {
|
if err != test.fetchErr {
|
||||||
@@ -159,7 +147,6 @@ func TestReviews(t *testing.T) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
reviews.RequestedReviewers = test.reviewers
|
|
||||||
|
|
||||||
if r := reviews.IsApproved(); r != test.isApproved {
|
if r := reviews.IsApproved(); r != test.isApproved {
|
||||||
t.Fatal("Unexpected IsReviewed():", r, "vs. expected", test.isApproved)
|
t.Fatal("Unexpected IsReviewed():", r, "vs. expected", test.isApproved)
|
||||||
|
|||||||
@@ -113,10 +113,6 @@ func (s *Submodule) parseKeyValue(line string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Submodule) ManifestSubmodulePath(manifest *Manifest) string {
|
|
||||||
return manifest.SubdirForPackage(s.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseSubmodulesFile(reader io.Reader) ([]Submodule, error) {
|
func ParseSubmodulesFile(reader io.Reader) ([]Submodule, error) {
|
||||||
data, err := io.ReadAll(reader)
|
data, err := io.ReadAll(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
#!/usr/bin/bash
|
#!/usr/bin/bash
|
||||||
|
|
||||||
git init -q --bare --object-format=sha256
|
git init -q --bare --object-format=sha256
|
||||||
git config user.email test@example.com
|
|
||||||
git config user.name Test
|
|
||||||
export GIT_AUTHOR_DATE=2025-10-27T14:20:07+01:00
|
|
||||||
export GIT_COMMITTER_DATE=2025-10-27T14:20:07+01:00
|
|
||||||
|
|
||||||
# 81aba862107f1e2f5312e165453955485f424612f313d6c2fb1b31fef9f82a14
|
# 81aba862107f1e2f5312e165453955485f424612f313d6c2fb1b31fef9f82a14
|
||||||
blobA=$(echo "help" | git hash-object --stdin -w)
|
blobA=$(echo "help" | git hash-object --stdin -w)
|
||||||
|
|||||||
116
common/utils.go
116
common/utils.go
@@ -27,87 +27,10 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NewRepos struct {
|
|
||||||
Repos []struct {
|
|
||||||
Organization, Repository, Branch string
|
|
||||||
PackageName string
|
|
||||||
}
|
|
||||||
IsMaintainer bool
|
|
||||||
}
|
|
||||||
|
|
||||||
const maintainership_line = "MAINTAINER"
|
|
||||||
|
|
||||||
var true_lines []string = []string{"1", "TRUE", "YES", "OK", "T"}
|
|
||||||
|
|
||||||
func HasSpace(s string) bool {
|
|
||||||
return strings.IndexFunc(s, unicode.IsSpace) >= 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func FindNewReposInIssueBody(body string) *NewRepos {
|
|
||||||
Issues := &NewRepos{}
|
|
||||||
for _, line := range strings.Split(body, "\n") {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if ul := strings.ToUpper(line); strings.HasPrefix(ul, "MAINTAINER") {
|
|
||||||
value := ""
|
|
||||||
if idx := strings.IndexRune(ul, ':'); idx > 0 && len(ul) > idx+2 {
|
|
||||||
value = ul[idx+1:]
|
|
||||||
} else if idx := strings.IndexRune(ul, ' '); idx > 0 && len(ul) > idx+2 {
|
|
||||||
value = ul[idx+1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
if slices.Contains(true_lines, strings.TrimSpace(value)) {
|
|
||||||
Issues.IsMaintainer = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// line = strings.TrimSpace(line)
|
|
||||||
issue := struct{ Organization, Repository, Branch, PackageName string }{}
|
|
||||||
|
|
||||||
branch := strings.Split(line, "#")
|
|
||||||
repo := strings.Split(branch[0], "/")
|
|
||||||
|
|
||||||
if len(branch) == 2 {
|
|
||||||
issue.Branch = strings.TrimSpace(branch[1])
|
|
||||||
}
|
|
||||||
if len(repo) == 2 {
|
|
||||||
issue.Organization = strings.TrimSpace(repo[0])
|
|
||||||
issue.Repository = strings.TrimSpace(repo[1])
|
|
||||||
issue.PackageName = issue.Repository
|
|
||||||
|
|
||||||
if idx := strings.Index(strings.ToUpper(issue.Branch), " AS "); idx > 0 && len(issue.Branch) > idx+5 {
|
|
||||||
issue.PackageName = strings.TrimSpace(issue.Branch[idx+3:])
|
|
||||||
issue.Branch = strings.TrimSpace(issue.Branch[0:idx])
|
|
||||||
}
|
|
||||||
|
|
||||||
if HasSpace(issue.Organization) || HasSpace(issue.Repository) || HasSpace(issue.PackageName) || HasSpace(issue.Branch) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
Issues.Repos = append(Issues.Repos, issue)
|
|
||||||
//PackageNameIdx := strings.Index(strings.ToUpper(line), " AS ")
|
|
||||||
//words := strings.Split(line)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(Issues.Repos) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return Issues
|
|
||||||
}
|
|
||||||
|
|
||||||
func IssueToString(issue *models.Issue) string {
|
|
||||||
if issue == nil {
|
|
||||||
return "(nil)"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s/%s#%d", issue.Repository.Owner, issue.Repository.Name, issue.Index)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SplitLines(str string) []string {
|
func SplitLines(str string) []string {
|
||||||
return SplitStringNoEmpty(str, "\n")
|
return SplitStringNoEmpty(str, "\n")
|
||||||
}
|
}
|
||||||
@@ -131,10 +54,6 @@ func TranslateHttpsToSshUrl(url string) (string, error) {
|
|||||||
url2_len = len(url2)
|
url2_len = len(url2)
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(url) > 10 && (url[0:10] == "gitea@src." || url[0:10] == "ssh://gite") {
|
|
||||||
return url, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(url) > url1_len && url[0:url1_len] == url1 {
|
if len(url) > url1_len && url[0:url1_len] == url1 {
|
||||||
return "ssh://gitea@src.opensuse.org/" + url[url1_len:], nil
|
return "ssh://gitea@src.opensuse.org/" + url[url1_len:], nil
|
||||||
}
|
}
|
||||||
@@ -213,7 +132,7 @@ func PRtoString(pr *models.PullRequest) string {
|
|||||||
return "(null)"
|
return "(null)"
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("%s/%s!%d", pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Index)
|
return fmt.Sprintf("%s/%s#%d", pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Index)
|
||||||
}
|
}
|
||||||
|
|
||||||
type DevelProject struct {
|
type DevelProject struct {
|
||||||
@@ -245,10 +164,9 @@ func FetchDevelProjects() (DevelProjects, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var DevelProjectNotFound = errors.New("Devel project not found")
|
var DevelProjectNotFound = errors.New("Devel project not found")
|
||||||
|
|
||||||
func (d DevelProjects) GetDevelProject(pkg string) (string, error) {
|
func (d DevelProjects) GetDevelProject(pkg string) (string, error) {
|
||||||
for _, item := range d {
|
for _, item := range d {
|
||||||
if item.Package == pkg {
|
if item.Package == pkg {
|
||||||
return item.Project, nil
|
return item.Project, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,33 +174,3 @@ func (d DevelProjects) GetDevelProject(pkg string) (string, error) {
|
|||||||
return "", DevelProjectNotFound
|
return "", DevelProjectNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
var removedBranchNameSuffixes []string = []string{
|
|
||||||
"-rm",
|
|
||||||
"-removed",
|
|
||||||
"-deleted",
|
|
||||||
}
|
|
||||||
|
|
||||||
func findRemovedBranchSuffix(branchName string) string {
|
|
||||||
branchName = strings.ToLower(branchName)
|
|
||||||
|
|
||||||
for _, suffix := range removedBranchNameSuffixes {
|
|
||||||
if len(suffix) < len(branchName) && strings.HasSuffix(branchName, suffix) {
|
|
||||||
return suffix
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsRemovedBranch(branchName string) bool {
|
|
||||||
return len(findRemovedBranchSuffix(branchName)) > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func TrimRemovedBranchSuffix(branchName string) string {
|
|
||||||
suffix := findRemovedBranchSuffix(branchName)
|
|
||||||
if len(suffix) > 0 {
|
|
||||||
return branchName[0 : len(branchName)-len(suffix)]
|
|
||||||
}
|
|
||||||
|
|
||||||
return branchName
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package common_test
|
package common_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"src.opensuse.org/autogits/common"
|
"src.opensuse.org/autogits/common"
|
||||||
@@ -166,142 +165,3 @@ func TestRemoteName(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRemovedBranchName(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
branchName string
|
|
||||||
isRemoved bool
|
|
||||||
regularName string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Empty branch",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Removed suffix only",
|
|
||||||
branchName: "-rm",
|
|
||||||
isRemoved: false,
|
|
||||||
regularName: "-rm",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Capital suffix",
|
|
||||||
branchName: "Foo-Rm",
|
|
||||||
isRemoved: true,
|
|
||||||
regularName: "Foo",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Other suffixes",
|
|
||||||
isRemoved: true,
|
|
||||||
branchName: "Goo-Rm-DeleteD",
|
|
||||||
regularName: "Goo-Rm",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Other suffixes",
|
|
||||||
isRemoved: true,
|
|
||||||
branchName: "main-REMOVED",
|
|
||||||
regularName: "main",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Not removed separator",
|
|
||||||
isRemoved: false,
|
|
||||||
branchName: "main;REMOVED",
|
|
||||||
regularName: "main;REMOVED",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
if r := common.IsRemovedBranch(test.branchName); r != test.isRemoved {
|
|
||||||
t.Error("Expecting isRemoved:", test.isRemoved, "but received", r)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tn := common.TrimRemovedBranchSuffix(test.branchName); tn != test.regularName {
|
|
||||||
t.Error("Expected stripped branch name to be:", test.regularName, "but have:", tn)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewPackageIssueParsing(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
input string
|
|
||||||
issues *common.NewRepos
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Nothing",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Basic repo",
|
|
||||||
input: "org/repo#branch",
|
|
||||||
issues: &common.NewRepos{
|
|
||||||
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
|
|
||||||
{Organization: "org", Repository: "repo", Branch: "branch", PackageName: "repo"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Default branch and junk lines and approval for maintainership",
|
|
||||||
input: "\n\nsome comments\n\norg1/repo2\n\nmaintainership: yes",
|
|
||||||
issues: &common.NewRepos{
|
|
||||||
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
|
|
||||||
{Organization: "org1", Repository: "repo2", Branch: "", PackageName: "repo2"},
|
|
||||||
},
|
|
||||||
IsMaintainer: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Default branch and junk lines and no maintainership",
|
|
||||||
input: "\n\nsome comments\n\norg1/repo2\n\nmaintainership: NEVER",
|
|
||||||
issues: &common.NewRepos{
|
|
||||||
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
|
|
||||||
{Organization: "org1", Repository: "repo2", Branch: "", PackageName: "repo2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "3 repos with comments and maintainership",
|
|
||||||
input: "\n\nsome comments for org1/repo2 are here and more\n\norg1/repo2#master\n org2/repo3#master\n some/repo3#m\nMaintainer ok",
|
|
||||||
issues: &common.NewRepos{
|
|
||||||
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
|
|
||||||
{Organization: "org1", Repository: "repo2", Branch: "master", PackageName: "repo2"},
|
|
||||||
{Organization: "org2", Repository: "repo3", Branch: "master", PackageName: "repo3"},
|
|
||||||
{Organization: "some", Repository: "repo3", Branch: "m", PackageName: "repo3"},
|
|
||||||
},
|
|
||||||
IsMaintainer: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid repos with spaces",
|
|
||||||
input: "or g/repo#branch\norg/r epo#branch\norg/repo#br anch\norg/repo#branch As foo ++",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Valid repos with spaces",
|
|
||||||
input: " org / repo # branch",
|
|
||||||
issues: &common.NewRepos{
|
|
||||||
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
|
|
||||||
{Organization: "org", Repository: "repo", Branch: "branch", PackageName: "repo"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Package name is not repo name",
|
|
||||||
input: " org / repo # branch as repo++ \nmaintainer true",
|
|
||||||
issues: &common.NewRepos{
|
|
||||||
Repos: []struct{ Organization, Repository, Branch, PackageName string }{
|
|
||||||
{Organization: "org", Repository: "repo", Branch: "branch", PackageName: "repo++"},
|
|
||||||
},
|
|
||||||
IsMaintainer: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
issue := common.FindNewReposInIssueBody(test.input)
|
|
||||||
if !reflect.DeepEqual(test.issues, issue) {
|
|
||||||
t.Error("Expected", test.issues, "but have", issue)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
all: ../workflow-direct/workflow-direct
|
|
||||||
cp ../workflow-direct/workflow-direct workflow-direct
|
|
||||||
podman build --pull=always -t workflow-direct workflow-direct
|
|
||||||
|
|
||||||
pr:
|
|
||||||
cp ../workflow-pr/workflow-pr workflow-pr
|
|
||||||
podman build --pull=always -t workflow-pr workflow-pr
|
|
||||||
|
|
||||||
1
containers/workflow-direct/.gitignore
vendored
1
containers/workflow-direct/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
workflow-direct
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
FROM registry.suse.com/bci/bci-base
|
|
||||||
RUN zypper install -y openssh-clients git-core
|
|
||||||
RUN mkdir /root/.ssh
|
|
||||||
RUN mkdir /repos
|
|
||||||
RUN ln -s /data/workflow-direct.key /root/.ssh/id_ed25519
|
|
||||||
RUN ln -s /data/workflow-direct.key.pub /root/.ssh/id_ed25519.pub
|
|
||||||
ADD known_hosts /root/.ssh/known_hosts
|
|
||||||
ADD workflow-direct /srv/workflow-direct
|
|
||||||
ENV AMQP_USERNAME=opensuse
|
|
||||||
ENV AMQP_PASSWORD=opensuse
|
|
||||||
VOLUME /data
|
|
||||||
VOLUME /repos
|
|
||||||
ENTRYPOINT /srv/workflow-direct -config /data/config.json -repo-path /repos -debug -check-on-start
|
|
||||||
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
src.opensuse.org,195.135.223.224 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDJ8V51MVIFUkQqQOdHwC3SP9NPqp1ZWYoEbcjvZ7HhSFi2XF8ALo/h1Mk+q8kT2O75/goeTsKFbcU8zrYFeOh0=
|
|
||||||
src.opensuse.org,195.135.223.224 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCkVeXePin0haffC085V2L0jvILfwbB2Mt1fpVe21QAOcWNM+/jOC5RwtWweV/LigHImB39/KvkuPa9yLoDf+eLhdZQckSSauRfDjxtlKeFLPrfJKSA0XeVJT3kJcOvDT/3ANFhYeBbAUBTAeQt5bi2hHC1twMPbaaEdJ2jiMaIBztFf6aE9K58uoS+7Y2tTv87Mv/7lqoBW6BFMoDmjQFWgjik6ZMCvIM/7bj7AgqHk/rjmr5zKS4ag5wtHtYLm1L3LBmHdj7d0VFsOpPQexIOEnnjzKqlwmAxT6eYJ/t3qgBlT8KRfshBFgEuUZ5GJOC7TOne4PfB0bboPMZzIRo3WE9dPGRR8kAIme8XqhFbmjdJ+WsTjg0Lj+415tIbyRQoNkLtawrJxozvevs6wFEFcA/YG6o03Z577tiLT3WxOguCcD5vrALH48SyZb8jDUtcVgTWMW0to/n63S8JGUNyF7Bkw9HQWUx+GO1cv2GNzKpk22KS5dlNUVGE9E/7Ydc=
|
|
||||||
src.opensuse.org,195.135.223.224 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFKNThLRPznU5Io1KrAYHmYpaoLQEMGM9nwpKyYQCkPx
|
|
||||||
|
|
||||||
1
containers/workflow-pr/.gitignore
vendored
1
containers/workflow-pr/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
workflow-pr
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
FROM registry.suse.com/bci/bci-base
|
|
||||||
RUN zypper install -y openssh-clients git-core
|
|
||||||
RUN mkdir /root/.ssh
|
|
||||||
RUN mkdir /repos
|
|
||||||
RUN ln -s /data/workflow-pr.key /root/.ssh/id_ed25519
|
|
||||||
RUN ln -s /data/workflow-pr.key.pub /root/.ssh/id_ed25519.pub
|
|
||||||
ADD known_hosts /root/.ssh/known_hosts
|
|
||||||
ADD workflow-pr /srv/workflow-pr
|
|
||||||
ENV AMQP_USERNAME=opensuse
|
|
||||||
ENV AMQP_PASSWORD=opensuse
|
|
||||||
VOLUME /data
|
|
||||||
VOLUME /repos
|
|
||||||
ENTRYPOINT /srv/workflow-pr -config /data/config.json -repo-path /repos -debug -check-on-start
|
|
||||||
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
src.opensuse.org,195.135.223.224 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDJ8V51MVIFUkQqQOdHwC3SP9NPqp1ZWYoEbcjvZ7HhSFi2XF8ALo/h1Mk+q8kT2O75/goeTsKFbcU8zrYFeOh0=
|
|
||||||
src.opensuse.org,195.135.223.224 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCkVeXePin0haffC085V2L0jvILfwbB2Mt1fpVe21QAOcWNM+/jOC5RwtWweV/LigHImB39/KvkuPa9yLoDf+eLhdZQckSSauRfDjxtlKeFLPrfJKSA0XeVJT3kJcOvDT/3ANFhYeBbAUBTAeQt5bi2hHC1twMPbaaEdJ2jiMaIBztFf6aE9K58uoS+7Y2tTv87Mv/7lqoBW6BFMoDmjQFWgjik6ZMCvIM/7bj7AgqHk/rjmr5zKS4ag5wtHtYLm1L3LBmHdj7d0VFsOpPQexIOEnnjzKqlwmAxT6eYJ/t3qgBlT8KRfshBFgEuUZ5GJOC7TOne4PfB0bboPMZzIRo3WE9dPGRR8kAIme8XqhFbmjdJ+WsTjg0Lj+415tIbyRQoNkLtawrJxozvevs6wFEFcA/YG6o03Z577tiLT3WxOguCcD5vrALH48SyZb8jDUtcVgTWMW0to/n63S8JGUNyF7Bkw9HQWUx+GO1cv2GNzKpk22KS5dlNUVGE9E/7Ydc=
|
|
||||||
src.opensuse.org,195.135.223.224 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFKNThLRPznU5Io1KrAYHmYpaoLQEMGM9nwpKyYQCkPx
|
|
||||||
|
|
||||||
1
devel-importer/.gitignore
vendored
1
devel-importer/.gitignore
vendored
@@ -2,4 +2,3 @@ devel-importer
|
|||||||
Factory
|
Factory
|
||||||
git
|
git
|
||||||
git-migrated
|
git-migrated
|
||||||
git-importer
|
|
||||||
|
|||||||
@@ -1,239 +0,0 @@
|
|||||||
#!/usr/bin/perl
|
|
||||||
use strict;
|
|
||||||
use warnings;
|
|
||||||
use IPC::Open2;
|
|
||||||
use JSON;
|
|
||||||
|
|
||||||
sub FindFactoryCommit {
|
|
||||||
my ($package) = @_;
|
|
||||||
|
|
||||||
# Execute osc cat and capture output
|
|
||||||
my $osc_cmd = "osc cat openSUSE:Factory $package $package.changes";
|
|
||||||
open( my $osc_fh, "$osc_cmd |" ) or die "Failed to run osc: $!";
|
|
||||||
my $data = do { local $/; <$osc_fh> };
|
|
||||||
close($osc_fh);
|
|
||||||
|
|
||||||
# Calculate size
|
|
||||||
my $size = length($data);
|
|
||||||
|
|
||||||
# Create blob header
|
|
||||||
my $blob = "blob $size\0$data";
|
|
||||||
|
|
||||||
# Open a pipe to openssl to compute the hash
|
|
||||||
my ( $reader, $writer );
|
|
||||||
my $pid = open2( $reader, $writer, "openssl sha256" );
|
|
||||||
|
|
||||||
# Send blob data
|
|
||||||
print $writer $blob;
|
|
||||||
close $writer;
|
|
||||||
|
|
||||||
# Read the hash result and extract it
|
|
||||||
my $hash_line = <$reader>;
|
|
||||||
waitpid( $pid, 0 );
|
|
||||||
my ($hash) = $hash_line =~ /([a-fA-F0-9]{64})/;
|
|
||||||
|
|
||||||
# Run git search command with the hash
|
|
||||||
print("looking for hash: $hash\n");
|
|
||||||
my @hashes;
|
|
||||||
my $git_cmd =
|
|
||||||
"git -C $package rev-list --all pool/HEAD | while read commit; do git -C $package ls-tree \"\$commit\" | grep -q '^100644 blob $hash' && echo \"\$commit\"; done";
|
|
||||||
open( my $git_fh, "$git_cmd |" ) or die "Failed to run git search: $!";
|
|
||||||
while ( my $commit = <$git_fh> ) {
|
|
||||||
chomp $commit;
|
|
||||||
print "Found commit $commit\n";
|
|
||||||
push( @hashes, $commit );
|
|
||||||
}
|
|
||||||
close($git_fh);
|
|
||||||
return @hashes;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub ListPackages {
|
|
||||||
my ($project) = @_;
|
|
||||||
open( my $osc_fh,
|
|
||||||
"curl -s https://src.opensuse.org/openSUSE/Factory/raw/branch/main/pkgs/_meta/devel_packages | awk '{ if ( \$2 == \"$project\" ) print \$1 }' |" )
|
|
||||||
or die "Failed to run curl: $!";
|
|
||||||
my @packages = <$osc_fh>;
|
|
||||||
chomp @packages;
|
|
||||||
close($osc_fh);
|
|
||||||
return @packages;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub FactoryMd5 {
|
|
||||||
my ($package) = @_;
|
|
||||||
my $out = "";
|
|
||||||
|
|
||||||
if (system("osc ls openSUSE:Factory $package | grep -q build.specials.obscpio") == 0) {
|
|
||||||
system("mkdir _extract") == 0 || die "_extract exists or can't make it. Aborting.";
|
|
||||||
chdir("_extract") || die;
|
|
||||||
system("osc cat openSUSE:Factory $package build.specials.obscpio | cpio -dium 2> /dev/null") == 0 || die;
|
|
||||||
system("rm .* 2> /dev/null");
|
|
||||||
open( my $fh, "find -type f -exec /usr/bin/basename {} \\; | xargs md5sum | awk '{print \$1 FS \$2}' | grep -v d41d8cd98f00b204e9800998ecf8427e |") or die;
|
|
||||||
while ( my $l = <$fh>) {
|
|
||||||
$out = $out.$l;
|
|
||||||
}
|
|
||||||
close($fh);
|
|
||||||
chdir("..") && system("rm -rf _extract") == 0 || die;
|
|
||||||
}
|
|
||||||
open( my $fh, "osc ls -v openSUSE:Factory $package | awk '{print \$1 FS \$7}' | grep -v -F '_scmsync.obsinfo\nbuild.specials.obscpio' |") or die;
|
|
||||||
while (my $l = <$fh>) {
|
|
||||||
$out = $out.$l;
|
|
||||||
}
|
|
||||||
close($fh);
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Read project from first argument
|
|
||||||
sub Usage {
|
|
||||||
die "Usage: $0 <OBS Project> [org [package]]";
|
|
||||||
}
|
|
||||||
|
|
||||||
my $project = shift or Usage();
|
|
||||||
my $org = shift;
|
|
||||||
|
|
||||||
if (not defined($org)) {
|
|
||||||
$org = `osc meta prj $project | grep scmsync | sed -e 's,^.*src.opensuse.org/\\(.*\\)/_ObsPrj.*,\\1,'`;
|
|
||||||
chomp($org);
|
|
||||||
}
|
|
||||||
|
|
||||||
my @packages = ListPackages($project);
|
|
||||||
my $pkg = shift;
|
|
||||||
@packages = ($pkg) if defined $pkg;
|
|
||||||
|
|
||||||
my @tomove;
|
|
||||||
my @toremove;
|
|
||||||
|
|
||||||
if ( ! -e $org ) {
|
|
||||||
mkdir($org);
|
|
||||||
}
|
|
||||||
chdir($org);
|
|
||||||
print "Verify packages in /pool for $org package in $project\n";
|
|
||||||
|
|
||||||
my $super_user = $ENV{SUPER};
|
|
||||||
if (defined($super_user)) {
|
|
||||||
$super_user = "-G $super_user";
|
|
||||||
} else {
|
|
||||||
$super_user = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
my @missing;
|
|
||||||
|
|
||||||
# verify that packages in devel project is a fork from pool.
|
|
||||||
for my $pkg ( sort(@packages) ) {
|
|
||||||
my $data = `git obs api /repos/$org/$pkg 2> /dev/null`;
|
|
||||||
if ( length($data) == 0 ) {
|
|
||||||
print "***** Repo missing in $org: $pkg\n";
|
|
||||||
push(@missing, $pkg);
|
|
||||||
next;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
my $repo = decode_json($data);
|
|
||||||
if ( !$repo->{parent}
|
|
||||||
|| $repo->{parent}->{owner}->{username} ne "pool" )
|
|
||||||
{
|
|
||||||
if ( system("git obs api /repos/pool/$pkg > /dev/null 2> /dev/null") == 0 ) {
|
|
||||||
print "=== $pkg NOT A FORK of exiting package\n";
|
|
||||||
push( @toremove, $pkg );
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
print "$pkg NEEDS transfer\n";
|
|
||||||
push( @tomove, $pkg );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( scalar @missing > 0 ) {
|
|
||||||
for my $pkg (@missing) {
|
|
||||||
my $index = 0;
|
|
||||||
$index++ until $packages[$index] eq $pkg;
|
|
||||||
splice(@packages, $index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( scalar @toremove > 0 ) {
|
|
||||||
print "ABORTING. Need repos removed.\n";
|
|
||||||
print "@toremove\n";
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( scalar @tomove > 0 ) {
|
|
||||||
for my $pkg (@tomove) {
|
|
||||||
system("git obs $super_user api -X POST --data '{\"reparent\": true, \"organization\": \"pool\"}' /repos/$org/$pkg/forks") == 0 and
|
|
||||||
system("git clone gitea\@src.opensuse.org:pool/$pkg") == 0 and
|
|
||||||
system("git -C $pkg checkout -B factory HEAD") == 0 and
|
|
||||||
system("git -C $pkg push origin factory") == 0 and
|
|
||||||
system("git obs $super_user api -X PATCH --data '{\"default_branch\": \"factory\"}' /repos/pool/$pkg") == 0
|
|
||||||
or die "Error in creating a pool repo";
|
|
||||||
system("for i in \$(git -C $pkg for-each-ref --format='%(refname:lstrip=3)' refs/remotes/origin/ | grep -v '\\(^HEAD\$\\|^factory\$\\)'); do git -C $pkg push origin :\$i; done") == 0 or die "failed to cull branches";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
print "Verify complete.\n";
|
|
||||||
|
|
||||||
for my $package ( sort(@packages) ) {
|
|
||||||
print " ----- PROCESSING $package\n";
|
|
||||||
my $url = "https://src.opensuse.org/$org/$package.git";
|
|
||||||
my $push_url = "gitea\@src.opensuse.org:pool/$package.git";
|
|
||||||
if ( not -e $package ) {
|
|
||||||
print("cloning...\n");
|
|
||||||
system("git clone --origin pool $url") == 0
|
|
||||||
or die "Can't clone $org/$package";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
print("adding remote...\n");
|
|
||||||
system("git -C $package remote rm pool > /dev/null");
|
|
||||||
system("git -C $package remote add pool $url") == 0
|
|
||||||
or die "Can't add pool for $package";
|
|
||||||
}
|
|
||||||
system("git -C $package remote set-url pool --push $push_url") == 0
|
|
||||||
or die "Can't add push remote for $package";
|
|
||||||
print("fetching remote...\n");
|
|
||||||
system("git -C $package fetch pool") == 0
|
|
||||||
or ( push( @tomove, $package ) and die "Can't fetch pool for $package" );
|
|
||||||
|
|
||||||
my @commits = FindFactoryCommit($package);
|
|
||||||
my $Md5Hashes = FactoryMd5($package);
|
|
||||||
my $c;
|
|
||||||
my $match = 0;
|
|
||||||
for my $commit (@commits) {
|
|
||||||
if ( length($commit) != 64 ) {
|
|
||||||
print("Failed to find factory commit. Aborting.");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
system("git -C $package lfs fetch pool $commit") == 0
|
|
||||||
and system("git -C $package checkout -B factory $commit") == 0
|
|
||||||
and system("git -C $package lfs checkout") == 0
|
|
||||||
and chdir($package)) {
|
|
||||||
|
|
||||||
open(my $fh, "|-", "md5sum -c --quiet") or die $!;
|
|
||||||
print $fh $Md5Hashes;
|
|
||||||
close $fh;
|
|
||||||
if ($? >> 8 != 0) {
|
|
||||||
chdir("..") || die;
|
|
||||||
next;
|
|
||||||
}
|
|
||||||
open($fh, "|-", "awk '{print \$2}' | sort | bash -c \"diff <(ls -1 | sort) -\"") or die $!;
|
|
||||||
print $fh $Md5Hashes;
|
|
||||||
close $fh;
|
|
||||||
my $ec = $? >> 8;
|
|
||||||
chdir("..") || die;
|
|
||||||
|
|
||||||
if ($ec == 0) {
|
|
||||||
$c = $commit;
|
|
||||||
$match = 1;
|
|
||||||
last;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( !$match ) {
|
|
||||||
die "Match not found. Aborting.";
|
|
||||||
}
|
|
||||||
|
|
||||||
system ("git -C $package push -f pool factory");
|
|
||||||
print "$package: $c\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
#!/usr/bin/perl
|
|
||||||
use strict;
|
|
||||||
use warnings;
|
|
||||||
use IPC::Open2;
|
|
||||||
use URI;
|
|
||||||
|
|
||||||
sub FindFactoryCommit {
|
|
||||||
my ($package) = @_;
|
|
||||||
|
|
||||||
# Execute osc cat and capture output
|
|
||||||
my $osc_cmd = "osc cat openSUSE:Factory $package $package.changes";
|
|
||||||
open( my $osc_fh, "$osc_cmd |" ) or die "Failed to run osc: $!";
|
|
||||||
my $data = do { local $/; <$osc_fh> };
|
|
||||||
close($osc_fh);
|
|
||||||
|
|
||||||
# Calculate size
|
|
||||||
my $size = length($data);
|
|
||||||
|
|
||||||
# Create blob header
|
|
||||||
my $blob = "blob $size\0$data";
|
|
||||||
|
|
||||||
# Open a pipe to openssl to compute the hash
|
|
||||||
my ( $reader, $writer );
|
|
||||||
my $pid = open2( $reader, $writer, "openssl sha256" );
|
|
||||||
|
|
||||||
# Send blob data
|
|
||||||
print $writer $blob;
|
|
||||||
close $writer;
|
|
||||||
|
|
||||||
# Read the hash result and extract it
|
|
||||||
my $hash_line = <$reader>;
|
|
||||||
waitpid( $pid, 0 );
|
|
||||||
my ($hash) = $hash_line =~ /([a-fA-F0-9]{64})/;
|
|
||||||
|
|
||||||
# Run git search command with the hash
|
|
||||||
print("looking for hash: $hash\n");
|
|
||||||
my @hashes;
|
|
||||||
my $git_cmd =
|
|
||||||
"git -C $package rev-list --all pool/HEAD | while read commit; do git -C $package ls-tree \"\$commit\" | grep -q '^100644 blob $hash' && echo \"\$commit\"; done";
|
|
||||||
open( my $git_fh, "$git_cmd |" ) or die "Failed to run git search: $!";
|
|
||||||
while ( my $commit = <$git_fh> ) {
|
|
||||||
chomp $commit;
|
|
||||||
print "Found commit $commit\n";
|
|
||||||
push( @hashes, $commit );
|
|
||||||
}
|
|
||||||
close($git_fh);
|
|
||||||
return @hashes;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub FactoryMd5 {
|
|
||||||
my ($package) = @_;
|
|
||||||
my $out = "";
|
|
||||||
|
|
||||||
if (system("osc ls openSUSE:Factory $package | grep -q build.specials.obscpio") == 0) {
|
|
||||||
system("mkdir _extract") == 0 || die "_extract exists or can't make it. Aborting.";
|
|
||||||
chdir("_extract") || die;
|
|
||||||
system("osc cat openSUSE:Factory $package build.specials.obscpio | cpio -dium 2> /dev/null") == 0 || die;
|
|
||||||
system("rm .* 2> /dev/null");
|
|
||||||
open( my $fh, "find -type f -exec /usr/bin/basename {} \\; | xargs md5sum | awk '{print \$1 FS \$2}' | grep -v d41d8cd98f00b204e9800998ecf8427e |") or die;
|
|
||||||
while ( my $l = <$fh>) {
|
|
||||||
$out = $out.$l;
|
|
||||||
}
|
|
||||||
close($fh);
|
|
||||||
chdir("..") && system("rm -rf _extract") == 0 || die;
|
|
||||||
}
|
|
||||||
open( my $fh, "osc ls -v openSUSE:Factory $package | awk '{print \$1 FS \$7}' | grep -v -F '_scmsync.obsinfo\nbuild.specials.obscpio' |") or die;
|
|
||||||
while (my $l = <$fh>) {
|
|
||||||
$out = $out.$l;
|
|
||||||
}
|
|
||||||
close($fh);
|
|
||||||
return $out;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Read project from first argument
|
|
||||||
sub Usage {
|
|
||||||
die "Usage: $0 <OBS Project> <package> <repo>";
|
|
||||||
}
|
|
||||||
|
|
||||||
my $project = shift or Usage();
|
|
||||||
my $pkg = shift;
|
|
||||||
my $repo = shift;
|
|
||||||
|
|
||||||
if (not defined($repo)) {
|
|
||||||
Usage();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
my $meta_url = `osc meta pkg $project $pkg | grep scmsync | sed -e 's,\\s*</\\?scmsync>\\s*,,g'`;
|
|
||||||
chomp($meta_url);
|
|
||||||
if ($meta_url ne $repo) {
|
|
||||||
die "meta not equal to repo for $pkg: $meta_url != $repo";
|
|
||||||
}
|
|
||||||
|
|
||||||
my $u = URI->new($meta_url);
|
|
||||||
die "Only src.opensuse.org is supported" unless $u->scheme =~ /^https?$/ && $u->host eq 'src.opensuse.org';
|
|
||||||
my (undef, $org, $repo_path) = split('/', $u->path);
|
|
||||||
my $branch = $u->fragment;
|
|
||||||
die "Only src.opensuse.org is supported" unless $org;
|
|
||||||
if ($org eq "pool") {
|
|
||||||
print "Already a pool package. We are done.\n";
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
my %params = $u->query_form;
|
|
||||||
delete $params{trackingbranch};
|
|
||||||
die "Unsupported query parameters: " . join(', ', keys %params) if keys %params;
|
|
||||||
|
|
||||||
my @packages = ($pkg) if defined $pkg;
|
|
||||||
|
|
||||||
if ( ! -e $org ) {
|
|
||||||
mkdir($org);
|
|
||||||
}
|
|
||||||
chdir($org);
|
|
||||||
|
|
||||||
my $super_user = $ENV{SUPER};
|
|
||||||
if (defined($super_user)) {
|
|
||||||
$super_user = "-G $super_user";
|
|
||||||
} else {
|
|
||||||
$super_user = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
for my $package ( sort(@packages) ) {
|
|
||||||
print " ----- PROCESSING $package\n";
|
|
||||||
my $url = "https://src.opensuse.org/$org/$repo_path.git";
|
|
||||||
my $push_url = "gitea\@src.opensuse.org:pool/$package.git";
|
|
||||||
if ( not -e $package ) {
|
|
||||||
print("cloning...\n");
|
|
||||||
system("git clone --origin pool $url $package") == 0
|
|
||||||
or die "Can't clone $org/$repo_path";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
print("adding remote...\n");
|
|
||||||
system("git -C $package remote rm pool > /dev/null");
|
|
||||||
system("git -C $package remote add pool $url") == 0
|
|
||||||
or die "Can't add pool for $package";
|
|
||||||
}
|
|
||||||
system("git -C $package remote set-url pool --push $push_url") == 0
|
|
||||||
or die "Can't add push remote for $package";
|
|
||||||
print("fetching remote...\n");
|
|
||||||
system("git -C $package fetch pool") == 0 or die "Can't fetch pool for $package";
|
|
||||||
|
|
||||||
my @commits = FindFactoryCommit($package);
|
|
||||||
my $Md5Hashes = FactoryMd5($package);
|
|
||||||
my $c;
|
|
||||||
my $match = 0;
|
|
||||||
for my $commit (@commits) {
|
|
||||||
if ( length($commit) != 64 ) {
|
|
||||||
print("Failed to find factory commit. Aborting.");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
system("git -C $package lfs fetch pool $commit") == 0
|
|
||||||
and system("git -C $package checkout -B factory $commit") == 0
|
|
||||||
and system("git -C $package lfs checkout") == 0
|
|
||||||
and chdir($package)) {
|
|
||||||
|
|
||||||
open(my $fh, "|-", "md5sum -c --quiet") or die $!;
|
|
||||||
print $fh $Md5Hashes;
|
|
||||||
close $fh;
|
|
||||||
if ($? >> 8 != 0) {
|
|
||||||
chdir("..") || die;
|
|
||||||
next;
|
|
||||||
}
|
|
||||||
open($fh, "|-", "awk '{print \$2}' | sort | bash -c \"diff <(ls -1 | sort) -\"") or die $!;
|
|
||||||
print $fh $Md5Hashes;
|
|
||||||
close $fh;
|
|
||||||
my $ec = $? >> 8;
|
|
||||||
chdir("..") || die;
|
|
||||||
|
|
||||||
if ($ec == 0) {
|
|
||||||
$c = $commit;
|
|
||||||
$match = 1;
|
|
||||||
last;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( !$match ) {
|
|
||||||
die "Match not found. Aborting.";
|
|
||||||
}
|
|
||||||
|
|
||||||
system ("git -C $package push -f pool factory");
|
|
||||||
print "$package: $c\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,25 +1,15 @@
|
|||||||
SystemsManagement
|
|
||||||
Java:packages
|
|
||||||
Kernel:firmware
|
Kernel:firmware
|
||||||
Kernel:kdump
|
Kernel:kdump
|
||||||
devel:gcc
|
|
||||||
devel:languages:clojure
|
devel:languages:clojure
|
||||||
devel:languages:erlang
|
devel:languages:erlang
|
||||||
devel:languages:erlang:Factory
|
devel:languages:erlang:Factory
|
||||||
devel:languages:hare
|
devel:languages:hare
|
||||||
devel:languages:javascript
|
devel:languages:javascript
|
||||||
devel:languages:lua
|
devel:languages:lua
|
||||||
devel:languages:nodejs
|
|
||||||
devel:languages:perl
|
|
||||||
devel:languages:python:Factory
|
|
||||||
devel:languages:python:pytest
|
|
||||||
devel:openSUSE:Factory
|
|
||||||
network:chromium
|
|
||||||
network:dhcp
|
network:dhcp
|
||||||
network:im:whatsapp
|
network:im:whatsapp
|
||||||
network:messaging:xmpp
|
network:messaging:xmpp
|
||||||
science:HPC
|
|
||||||
server:dns
|
|
||||||
systemsmanagement:cockpit
|
systemsmanagement:cockpit
|
||||||
|
systemsmanagement:wbem
|
||||||
X11:lxde
|
X11:lxde
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/usr/bin/bash
|
|
||||||
|
|
||||||
osc api '/search/package?match=scmsync' | ../xml_package_parse | ../find_factory_commit_in_gitpkg.pl
|
|
||||||
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
#!/usr/bin/perl
|
|
||||||
use strict;
|
|
||||||
use warnings;
|
|
||||||
use XML::Parser;
|
|
||||||
|
|
||||||
my $parser = XML::Parser->new(Handlers => {
|
|
||||||
Start => \&handle_start,
|
|
||||||
End => \&handle_end,
|
|
||||||
Char => \&handle_char,
|
|
||||||
});
|
|
||||||
|
|
||||||
my $current_element = '';
|
|
||||||
my $current_package_attrs = {};
|
|
||||||
my $scmsync_content = '';
|
|
||||||
|
|
||||||
my %devel_pkgs;
|
|
||||||
open(my $dfh, "curl -s https://src.opensuse.org/openSUSE/Factory/raw/branch/main/pkgs/_meta/devel_packages |") or die $!;
|
|
||||||
while(<$dfh>) {
|
|
||||||
chomp;
|
|
||||||
$devel_pkgs{$_} = 1;
|
|
||||||
}
|
|
||||||
close($dfh);
|
|
||||||
|
|
||||||
my $xml_content = do { local $/; <STDIN> };
|
|
||||||
$parser->parse($xml_content);
|
|
||||||
|
|
||||||
sub handle_start {
|
|
||||||
my ($expat, $element, %attrs) = @_;
|
|
||||||
$current_element = $element;
|
|
||||||
|
|
||||||
if ($element eq 'package') {
|
|
||||||
$current_package_attrs = \%attrs;
|
|
||||||
}
|
|
||||||
if ($element eq 'scmsync') {
|
|
||||||
$scmsync_content = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sub handle_char {
|
|
||||||
my ($expat, $string) = @_;
|
|
||||||
if ($current_element eq 'scmsync') {
|
|
||||||
$scmsync_content .= $string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sub handle_end {
|
|
||||||
my ($expat, $element) = @_;
|
|
||||||
|
|
||||||
if ($element eq 'scmsync') {
|
|
||||||
my $project = $current_package_attrs->{project};
|
|
||||||
my $name = $current_package_attrs->{name};
|
|
||||||
my $scmsync = $scmsync_content;
|
|
||||||
|
|
||||||
# Use checks
|
|
||||||
$project = '' unless defined $project;
|
|
||||||
$name = '' unless defined $name;
|
|
||||||
$scmsync = '' unless defined $scmsync;
|
|
||||||
|
|
||||||
# Trim
|
|
||||||
$project =~ s/^\s+|\s+$//g;
|
|
||||||
$name =~ s/^\s+|\s+$//g;
|
|
||||||
$scmsync =~ s/^\s+|\s+$//g;
|
|
||||||
|
|
||||||
my $has_error = 0;
|
|
||||||
foreach my $val ($project, $name, $scmsync) {
|
|
||||||
if ($val =~ /\s/) {
|
|
||||||
print STDERR "Error: Value '$val' contains whitespace.\n";
|
|
||||||
$has_error = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unless ($has_error) {
|
|
||||||
if ($devel_pkgs{"$name $project"}) {
|
|
||||||
print "$name $project $scmsync\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Reset current element if we are closing it
|
|
||||||
if ($current_element eq $element) {
|
|
||||||
$current_element = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -298,22 +298,6 @@ func parseRequestJSONOrg(reqType string, data []byte) (org *common.Organization,
|
|||||||
org = pr.Repository.Owner
|
org = pr.Repository.Owner
|
||||||
extraAction = ""
|
extraAction = ""
|
||||||
|
|
||||||
case common.RequestType_Status:
|
|
||||||
status := common.StatusWebhookEvent{}
|
|
||||||
if err = json.Unmarshal(data, &status); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch status.State {
|
|
||||||
case "pending", "success", "error", "failure":
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
err = fmt.Errorf("Unknown Status' state: %s", status.State)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
org = status.Repository.Owner
|
|
||||||
extraAction = status.State
|
|
||||||
|
|
||||||
case common.RequestType_Wiki:
|
case common.RequestType_Wiki:
|
||||||
wiki := common.WikiWebhookEvent{}
|
wiki := common.WikiWebhookEvent{}
|
||||||
if err = json.Unmarshal(data, &wiki); err != nil {
|
if err = json.Unmarshal(data, &wiki); err != nil {
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/tailscale/hujson"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
ForgeEndpoint string `json:"forge_url"`
|
|
||||||
Keys []string `json:"keys"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type contextKey string
|
|
||||||
|
|
||||||
const configKey contextKey = "config"
|
|
||||||
|
|
||||||
func ReadConfig(reader io.Reader) (*Config, error) {
|
|
||||||
data, err := io.ReadAll(reader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error reading config data: %w", err)
|
|
||||||
}
|
|
||||||
config := Config{}
|
|
||||||
data, err = hujson.Standardize(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse json: %w", err)
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &config); err != nil {
|
|
||||||
return nil, fmt.Errorf("error parsing json to api keys and target url: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadConfigFile(filename string) (*Config, error) {
|
|
||||||
file, err := os.Open(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot open config file for reading. err: %w", err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
return ReadConfig(file)
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ConfigMiddleWare(cfg *Config) func(http.Handler) http.Handler {
|
|
||||||
return func(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := context.WithValue(r.Context(), configKey, cfg)
|
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"src.opensuse.org/autogits/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StatusInput struct {
|
|
||||||
Description string `json:"description"`
|
|
||||||
Context string `json:"context"`
|
|
||||||
State string `json:"state"`
|
|
||||||
TargetUrl string `json:"target_url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
configFile := flag.String("config", "", "status proxy config file")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if *configFile == "" {
|
|
||||||
common.LogError("missing required argument config")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
config, err := ReadConfigFile(*configFile)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
common.LogError("Failed to read config file", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
|
|
||||||
mux.Handle("/repos/{owner}/{repo}/statuses/{sha}", ConfigMiddleWare(config)(http.HandlerFunc(StatusProxy)))
|
|
||||||
|
|
||||||
common.LogInfo("server up and listening on :3000")
|
|
||||||
err = http.ListenAndServe(":3000", mux)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
common.LogError("Server failed to start up", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func StatusProxy(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.Method == http.MethodPost {
|
|
||||||
config, ok := r.Context().Value(configKey).(*Config)
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
common.LogDebug("Config missing from context")
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
header := r.Header.Get("Authorization")
|
|
||||||
if header == "" {
|
|
||||||
common.LogDebug("Authorization header not found")
|
|
||||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
token_arr := strings.Split(header, " ")
|
|
||||||
if len(token_arr) != 2 {
|
|
||||||
common.LogDebug("Authorization header malformed")
|
|
||||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.EqualFold(token_arr[0], "token") {
|
|
||||||
common.LogDebug("Token not found in Authorization header")
|
|
||||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
token := token_arr[1]
|
|
||||||
|
|
||||||
if !slices.Contains(config.Keys, token) {
|
|
||||||
common.LogDebug("Provided token is not known")
|
|
||||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
owner := r.PathValue("owner")
|
|
||||||
repo := r.PathValue("repo")
|
|
||||||
sha := r.PathValue("sha")
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
common.LogError("Failed to get config from context, is it set?")
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
posturl := fmt.Sprintf("%s/repos/%s/%s/statuses/%s", config.ForgeEndpoint, owner, repo, sha)
|
|
||||||
decoder := json.NewDecoder(r.Body)
|
|
||||||
var statusinput StatusInput
|
|
||||||
err := decoder.Decode(&statusinput)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
status_payload, err := json.Marshal(statusinput)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
client := &http.Client{}
|
|
||||||
req, err := http.NewRequest("POST", posturl, bytes.NewBuffer(status_payload))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ForgeToken := os.Getenv("GITEA_TOKEN")
|
|
||||||
|
|
||||||
if ForgeToken == "" {
|
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
common.LogError("GITEA_TOKEN was not set, all requests will fail")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header.Add("Content-Type", "application/json")
|
|
||||||
req.Header.Add("Authorization", fmt.Sprintf("token %s", ForgeToken))
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
common.LogError(fmt.Sprintf("Request to forge endpoint failed: %v", err))
|
|
||||||
http.Error(w, http.StatusText(http.StatusBadGateway), http.StatusBadGateway)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(resp.StatusCode)
|
|
||||||
|
|
||||||
/*
|
|
||||||
the commented out section sets every key
|
|
||||||
value from the headers, unsure if this
|
|
||||||
leaks information from gitea
|
|
||||||
|
|
||||||
for k, v := range resp.Header {
|
|
||||||
for _, vv := range v {
|
|
||||||
w.Header().Add(k, vv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
_, err = io.Copy(w, resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
common.LogError("Error copying response body: %v", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# gitea_status_proxy
|
|
||||||
|
|
||||||
Allows bots without code owner permission to set Gitea's commit status
|
|
||||||
|
|
||||||
## Basic usage
|
|
||||||
|
|
||||||
To beging, you need the json config and a Gitea token with permissions to the repository you want to write to.
|
|
||||||
|
|
||||||
Keys should be randomly generated, i.e by using openssl: `openssl rand -base64 48`
|
|
||||||
|
|
||||||
Generate a json config file, with the key generated from running the command above, save as example.json:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"forge_url": "https://src.opensuse.org/api/v1",
|
|
||||||
"keys": ["$YOUR_TOKEN_GOES_HERE"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### start the proxy:
|
|
||||||
|
|
||||||
```
|
|
||||||
GITEA_TOKEN=YOURTOKEN ./gitea_status_proxy -config example.json
|
|
||||||
2025/10/30 12:53:18 [I] server up and listening on :3000
|
|
||||||
```
|
|
||||||
|
|
||||||
Now the proxy should be able to accept requests under: `localhost:3000/repos/{owner}/{repo}/statuses/{sha}`, the token to be used when authenticating to the proxy must be in the `keys` list of the configuration json file (example.json above)
|
|
||||||
|
|
||||||
### example:
|
|
||||||
|
|
||||||
On a separate terminal, you can use curl to post a status to the proxy, if the GITEA_TOKEN has permissions on the target
|
|
||||||
repository, it will result in a new status being set for the given commit
|
|
||||||
|
|
||||||
```
|
|
||||||
curl -X 'POST' \
|
|
||||||
'localhost:3000/repos/szarate/test-actions-gitea/statuses/cd5847c92fb65a628bdd6015f96ee7e569e1ad6e4fc487acc149b52e788262f9' \
|
|
||||||
-H 'accept: application/json' \
|
|
||||||
-H 'Authorization: token $YOUR_TOKEN_GOES_HERE' \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{
|
|
||||||
"context": "Proxy test",
|
|
||||||
"description": "Status posted from the proxy",
|
|
||||||
"state": "success",
|
|
||||||
"target_url": "https://src.opensuse.org"
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
After this you should be able to the results in the pull request, e.g from above: https://src.opensuse.org/szarate/test-actions-gitea/pulls/1
|
|
||||||
5
go.mod
5
go.mod
@@ -10,16 +10,12 @@ require (
|
|||||||
github.com/go-openapi/validate v0.24.0
|
github.com/go-openapi/validate v0.24.0
|
||||||
github.com/opentracing/opentracing-go v1.2.0
|
github.com/opentracing/opentracing-go v1.2.0
|
||||||
github.com/rabbitmq/amqp091-go v1.10.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
|
github.com/tailscale/hujson v0.0.0-20250226034555-ec1d1c113d33
|
||||||
go.uber.org/mock v0.5.0
|
go.uber.org/mock v0.5.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
|
||||||
github.com/go-logr/logr v1.4.1 // indirect
|
github.com/go-logr/logr v1.4.1 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-openapi/analysis v0.23.0 // indirect
|
github.com/go-openapi/analysis v0.23.0 // indirect
|
||||||
@@ -37,4 +33,5 @@ require (
|
|||||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||||
golang.org/x/sync v0.7.0 // indirect
|
golang.org/x/sync v0.7.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
10
go.sum
10
go.sum
@@ -1,16 +1,8 @@
|
|||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
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/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=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
|
||||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
@@ -58,8 +50,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||||
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
|
|
||||||
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
|
||||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
|||||||
@@ -1,65 +1,24 @@
|
|||||||
Group Review Bot
|
Group Review Bot
|
||||||
================
|
================
|
||||||
|
|
||||||
This workaround is mainly needed because Gitea does not track which team member performed a review on behalf of a team.
|
Areas of responsibility
|
||||||
|
-----------------------
|
||||||
|
|
||||||
Main Tasks
|
1. Is used to handle reviews associated with groups defined in the
|
||||||
----------
|
ProjectGit.
|
||||||
|
|
||||||
Awaits a comment in the format “@groupreviewbot-name: approve”, then approves the PR with the comment “<user> approved a review on behalf of <groupreviewbot-name>.”
|
2. Assumes: workflow-pr needs to associate and define the PR set from
|
||||||
|
which the groups.json is read (Base of the PrjGit PR)
|
||||||
|
|
||||||
Target Usage
|
Target Usage
|
||||||
------------
|
------------
|
||||||
|
|
||||||
Projects where policy reviews are required.
|
Projects where policy reviews are required.
|
||||||
|
|
||||||
Configuration
|
|
||||||
--------------
|
|
||||||
|
|
||||||
The bot is configured via the `ReviewGroups` field in the `workflow.config` file, located in the ProjectGit repository.
|
|
||||||
|
|
||||||
See `ReviewGroups` in the [workflow-pr configuration](../workflow-pr/README.md#config-file).
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
...
|
|
||||||
"ReviewGroups": [
|
|
||||||
{
|
|
||||||
"Name": "name of the group user",
|
|
||||||
"Reviewers": ["members", "of", "group"],
|
|
||||||
"Silent": "(true, false) -- if true, do not explicitly require review requests of group members"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Server configuration
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
**Configuration file:**
|
|
||||||
|
|
||||||
| Field | Type | Notes |
|
|
||||||
| ----- | ----- | ----- |
|
|
||||||
| root | Array of string | Format **org/repo\#branch** |
|
|
||||||
|
|
||||||
Requirements
|
Requirements
|
||||||
------------
|
------------
|
||||||
Gitea token with following permissions:
|
* Gitea token to:
|
||||||
- R/W PullRequest
|
+ R/W PullRequest
|
||||||
- R/W Notification
|
+ R/W Notification
|
||||||
- R User
|
+ R User
|
||||||
|
|
||||||
Env Variables
|
|
||||||
-------------
|
|
||||||
The following variables can be used (and override) command line parameters.
|
|
||||||
|
|
||||||
* `AUTOGITS_CONFIG` - config file location
|
|
||||||
* `AUTOGITS_URL` - Gitea URL
|
|
||||||
* `AUTOGITS_RABBITURL` - RabbitMQ url
|
|
||||||
* `AUTOGITS_DEBUG` - when set, debug level logging enabled
|
|
||||||
|
|
||||||
Authentication env variables
|
|
||||||
* `GITEA_TOKEN` - Gitea user token
|
|
||||||
* `AMQP_USERNAME`, `AMQP_PASSWORD` - username and password for rabbitmq
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,69 +5,52 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"src.opensuse.org/autogits/common"
|
"src.opensuse.org/autogits/common"
|
||||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ReviewBot struct {
|
var configs common.AutogitConfigs
|
||||||
configs common.AutogitConfigs
|
var acceptRx *regexp.Regexp
|
||||||
acceptRx *regexp.Regexp
|
var rejectRx *regexp.Regexp
|
||||||
rejectRx *regexp.Regexp
|
var groupName string
|
||||||
groupName string
|
|
||||||
gitea common.Gitea
|
func InitRegex(groupName string) {
|
||||||
|
acceptRx = regexp.MustCompile("\\s*:\\s*LGTM")
|
||||||
|
rejectRx = regexp.MustCompile("\\s*:\\s*")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bot *ReviewBot) InitRegex(newGroupName string) {
|
func ParseReviewLine(reviewText string) (bool, string) {
|
||||||
bot.groupName = newGroupName
|
|
||||||
bot.acceptRx = regexp.MustCompile("^:\\s*(LGTM|approved?)")
|
|
||||||
bot.rejectRx = regexp.MustCompile("^:\\s*")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bot *ReviewBot) ParseReviewLine(reviewText string) (bool, string) {
|
|
||||||
line := strings.TrimSpace(reviewText)
|
line := strings.TrimSpace(reviewText)
|
||||||
groupTextName := "@" + bot.groupName
|
groupTextName := "@" + groupName
|
||||||
glen := len(groupTextName)
|
glen := len(groupTextName)
|
||||||
if len(line) < glen || line[0:glen] != groupTextName {
|
if len(line) < glen || line[0:glen] != groupTextName {
|
||||||
return false, line
|
return false, line
|
||||||
}
|
}
|
||||||
|
|
||||||
l := line[glen:]
|
return true, line[glen:]
|
||||||
for idx, r := range l {
|
|
||||||
if unicode.IsSpace(r) {
|
|
||||||
continue
|
|
||||||
} else if r == ':' {
|
|
||||||
return true, l[idx:]
|
|
||||||
} else {
|
|
||||||
return false, line
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, line
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bot *ReviewBot) ReviewAccepted(reviewText string) bool {
|
func ReviewAccepted(reviewText string) bool {
|
||||||
for _, line := range common.SplitStringNoEmpty(reviewText, "\n") {
|
for _, line := range common.SplitStringNoEmpty(reviewText, "\n") {
|
||||||
if matched, reviewLine := bot.ParseReviewLine(line); matched {
|
if matched, reviewLine := ParseReviewLine(line); matched {
|
||||||
return bot.acceptRx.MatchString(reviewLine)
|
return acceptRx.MatchString(reviewLine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bot *ReviewBot) ReviewRejected(reviewText string) bool {
|
func ReviewRejected(reviewText string) bool {
|
||||||
for _, line := range common.SplitStringNoEmpty(reviewText, "\n") {
|
for _, line := range common.SplitStringNoEmpty(reviewText, "\n") {
|
||||||
if matched, reviewLine := bot.ParseReviewLine(line); matched {
|
if matched, reviewLine := ParseReviewLine(line); matched {
|
||||||
if bot.rejectRx.MatchString(reviewLine) {
|
if rejectRx.MatchString(reviewLine) {
|
||||||
return !bot.acceptRx.MatchString(reviewLine)
|
return !acceptRx.MatchString(reviewLine)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -117,10 +100,10 @@ var commentStrings = []string{
|
|||||||
"change_time_estimate",
|
"change_time_estimate",
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
func (bot *ReviewBot) FindAcceptableReviewInTimeline(user string, timeline []*models.TimelineComment, reviews []*models.PullReview) *models.TimelineComment {
|
func FindAcceptableReviewInTimeline(user string, timeline []*models.TimelineComment, reviews []*models.PullReview) *models.TimelineComment {
|
||||||
for _, t := range timeline {
|
for _, t := range timeline {
|
||||||
if t.Type == common.TimelineCommentType_Comment && t.User.UserName == user && t.Created == t.Updated {
|
if t.Type == common.TimelineCommentType_Comment && t.User.UserName == user && t.Created == t.Updated {
|
||||||
if bot.ReviewAccepted(t.Body) || bot.ReviewRejected(t.Body) {
|
if ReviewAccepted(t.Body) || ReviewRejected(t.Body) {
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,23 +112,13 @@ func (bot *ReviewBot) FindAcceptableReviewInTimeline(user string, timeline []*mo
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bot *ReviewBot) FindOurLastReviewInTimeline(timeline []*models.TimelineComment) *models.TimelineComment {
|
func UnrequestReviews(gitea common.Gitea, org, repo string, id int64, users []string) {
|
||||||
for _, t := range timeline {
|
if err := gitea.UnrequestReview(org, repo, id, users...); err != nil {
|
||||||
if t.Type == common.TimelineCommentType_Review && t.User.UserName == bot.groupName && t.Created == t.Updated {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bot *ReviewBot) UnrequestReviews(org, repo string, id int64, users []string) {
|
|
||||||
if err := bot.gitea.UnrequestReview(org, repo, id, users...); err != nil {
|
|
||||||
common.LogError("Can't remove reviewrs after a review:", err)
|
common.LogError("Can't remove reviewrs after a review:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bot *ReviewBot) ProcessNotifications(notification *models.NotificationThread) {
|
func ProcessNotifications(notification *models.NotificationThread, gitea common.Gitea) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
common.LogInfo("panic cought --- recovered")
|
common.LogInfo("panic cought --- recovered")
|
||||||
@@ -153,7 +126,7 @@ func (bot *ReviewBot) ProcessNotifications(notification *models.NotificationThre
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
rx := regexp.MustCompile(`^/?api/v\d+/repos/(?<org>[_\.a-zA-Z0-9-]+)/(?<project>[_\.a-zA-Z0-9-]+)/(?:issues|pulls)/(?<num>[0-9]+)$`)
|
rx := regexp.MustCompile(`^/?api/v\d+/repos/(?<org>[_a-zA-Z0-9-]+)/(?<project>[_a-zA-Z0-9-]+)/(?:issues|pulls)/(?<num>[0-9]+)$`)
|
||||||
subject := notification.Subject
|
subject := notification.Subject
|
||||||
u, err := url.Parse(notification.Subject.URL)
|
u, err := url.Parse(notification.Subject.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -171,110 +144,99 @@ func (bot *ReviewBot) ProcessNotifications(notification *models.NotificationThre
|
|||||||
repo := match[2]
|
repo := match[2]
|
||||||
id, _ := strconv.ParseInt(match[3], 10, 64)
|
id, _ := strconv.ParseInt(match[3], 10, 64)
|
||||||
|
|
||||||
common.LogInfo("processing:", fmt.Sprintf("%s/%s!%d", org, repo, id))
|
common.LogInfo("processing:", fmt.Sprintf("%s/%s#%d", org, repo, id))
|
||||||
pr, err := bot.gitea.GetPullRequest(org, repo, id)
|
pr, err := gitea.GetPullRequest(org, repo, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(" ** Cannot fetch PR associated with review:", subject.URL, "Error:", err)
|
common.LogError(" ** Cannot fetch PR associated with review:", subject.URL, "Error:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bot.ProcessPR(pr); err == nil && !common.IsDryRun {
|
|
||||||
if err := bot.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 (bot *ReviewBot) ProcessPR(pr *models.PullRequest) error {
|
|
||||||
org := pr.Base.Repo.Owner.UserName
|
|
||||||
repo := pr.Base.Repo.Name
|
|
||||||
id := pr.Index
|
|
||||||
|
|
||||||
found := false
|
found := false
|
||||||
for _, reviewer := range pr.RequestedReviewers {
|
for _, reviewer := range pr.RequestedReviewers {
|
||||||
if reviewer != nil && reviewer.UserName == bot.groupName {
|
if reviewer != nil && reviewer.UserName == groupName {
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
if !found {
|
||||||
common.LogInfo(" review is not requested for", bot.groupName)
|
common.LogInfo(" review is not requested for", groupName)
|
||||||
return nil
|
if !common.IsDryRun {
|
||||||
|
gitea.SetNotificationRead(notification.ID)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config := bot.configs.GetPrjGitConfig(org, repo, pr.Base.Name)
|
config := configs.GetPrjGitConfig(org, repo, pr.Base.Name)
|
||||||
if config == nil {
|
if config == nil {
|
||||||
return fmt.Errorf("Cannot find config for: %s", pr.URL)
|
common.LogError("Cannot find config for:", fmt.Sprintf("%s/%s#%s", org, repo, pr.Base.Name))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if pr.State == "closed" {
|
if pr.State == "closed" {
|
||||||
// dismiss the review
|
// dismiss the review
|
||||||
common.LogInfo(" -- closed request, so nothing to review")
|
common.LogInfo(" -- closed request, so nothing to review")
|
||||||
return nil
|
if !common.IsDryRun {
|
||||||
|
gitea.SetNotificationRead(notification.ID)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
reviews, err := bot.gitea.GetPullRequestReviews(org, repo, id)
|
reviews, err := gitea.GetPullRequestReviews(org, repo, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to fetch reviews for: %v: %w", pr.URL, err)
|
common.LogInfo(" ** No reviews associated with request:", subject.URL, "Error:", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
timeline, err := common.FetchTimelineSinceReviewRequestOrPush(bot.gitea, bot.groupName, pr.Head.Sha, org, repo, id)
|
timeline, err := common.FetchTimelineSinceReviewRequestOrPush(gitea, groupName, pr.Head.Sha, org, repo, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to fetch timeline to review. %w", err)
|
common.LogError(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
groupConfig, err := config.GetReviewGroup(bot.groupName)
|
requestReviewers, err := config.GetReviewGroupMembers(groupName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Failed to fetch review group. %w", err)
|
common.LogError(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// submitter cannot be reviewer
|
// submitter cannot be reviewer
|
||||||
requestReviewers := slices.Clone(groupConfig.Reviewers)
|
|
||||||
requestReviewers = slices.DeleteFunc(requestReviewers, func(u string) bool { return u == pr.User.UserName })
|
requestReviewers = slices.DeleteFunc(requestReviewers, func(u string) bool { return u == pr.User.UserName })
|
||||||
// pr.Head.Sha
|
// pr.Head.Sha
|
||||||
|
|
||||||
for _, reviewer := range requestReviewers {
|
for _, reviewer := range requestReviewers {
|
||||||
if review := bot.FindAcceptableReviewInTimeline(reviewer, timeline, reviews); review != nil {
|
if review := FindAcceptableReviewInTimeline(reviewer, timeline, reviews); review != nil {
|
||||||
if bot.ReviewAccepted(review.Body) {
|
if ReviewAccepted(review.Body) {
|
||||||
if !common.IsDryRun {
|
if !common.IsDryRun {
|
||||||
text := reviewer + " approved a review on behalf of " + bot.groupName
|
gitea.AddReviewComment(pr, common.ReviewStateApproved, "Signed off by: "+reviewer)
|
||||||
if review := bot.FindOurLastReviewInTimeline(timeline); review == nil || review.Body != text {
|
UnrequestReviews(gitea, org, repo, id, requestReviewers)
|
||||||
_, err := bot.gitea.AddReviewComment(pr, common.ReviewStateApproved, text)
|
if !common.IsDryRun {
|
||||||
if err != nil {
|
if err := gitea.SetNotificationRead(notification.ID); err != nil {
|
||||||
common.LogError(" -> failed to write approval comment", err)
|
common.LogDebug(" Cannot set notification as read", err)
|
||||||
}
|
}
|
||||||
bot.UnrequestReviews(org, repo, id, requestReviewers)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
common.LogInfo(" -> approved by", reviewer)
|
common.LogInfo(" -> approved by", reviewer)
|
||||||
common.LogInfo(" review at", review.Created)
|
common.LogInfo(" review at", review.Created)
|
||||||
return nil
|
return
|
||||||
} else if bot.ReviewRejected(review.Body) {
|
} else if ReviewRejected(review.Body) {
|
||||||
if !common.IsDryRun {
|
if !common.IsDryRun {
|
||||||
text := reviewer + " requested changes on behalf of " + bot.groupName + ". See " + review.HTMLURL
|
gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Changes requested. See review by: "+reviewer)
|
||||||
if review := bot.FindOurLastReviewInTimeline(timeline); review == nil || review.Body != text {
|
UnrequestReviews(gitea, org, repo, id, requestReviewers)
|
||||||
_, err := bot.gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, text)
|
if err := gitea.SetNotificationRead(notification.ID); err != nil {
|
||||||
if err != nil {
|
common.LogDebug(" Cannot set notification as read", err)
|
||||||
common.LogError(" -> failed to write rejecting comment", err)
|
|
||||||
}
|
|
||||||
bot.UnrequestReviews(org, repo, id, requestReviewers)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
common.LogInfo(" -> declined by", reviewer)
|
common.LogInfo(" -> declined by", reviewer)
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// request group member reviews, if missing
|
// request group member reviews, if missing
|
||||||
common.LogDebug(" Review incomplete...")
|
common.LogDebug(" Review incomplete...")
|
||||||
if !groupConfig.Silent && len(requestReviewers) > 0 {
|
if len(requestReviewers) > 0 {
|
||||||
common.LogDebug(" Requesting reviews for:", requestReviewers)
|
common.LogDebug(" Requesting reviews for:", requestReviewers)
|
||||||
if !common.IsDryRun {
|
if !common.IsDryRun {
|
||||||
if _, err := bot.gitea.RequestReviews(pr, requestReviewers...); err != nil {
|
if _, err := gitea.RequestReviews(pr, requestReviewers...); err != nil {
|
||||||
common.LogDebug(" -> err:", err)
|
common.LogDebug(" -> err:", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -287,67 +249,40 @@ func (bot *ReviewBot) ProcessPR(pr *models.PullRequest) error {
|
|||||||
// add a helpful comment, if not yet added
|
// add a helpful comment, if not yet added
|
||||||
found_help_comment := false
|
found_help_comment := false
|
||||||
for _, t := range timeline {
|
for _, t := range timeline {
|
||||||
if t.Type == common.TimelineCommentType_Comment && t.User != nil && t.User.UserName == bot.groupName {
|
if t.Type == common.TimelineCommentType_Comment && t.User != nil && t.User.UserName == groupName {
|
||||||
found_help_comment = true
|
found_help_comment = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found_help_comment && !common.IsDryRun {
|
if !found_help_comment && !common.IsDryRun {
|
||||||
helpComment := fmt.Sprintln("Review by", bot.groupName, "represents a group of reviewers:", strings.Join(requestReviewers, ", "), ".\n\n"+
|
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.")
|
||||||
"Do **not** use standard review interface to review on behalf of the group.\n"+
|
gitea.AddComment(pr, helpComment)
|
||||||
"To accept the review on behalf of the group, create the following comment: `@"+bot.groupName+": approve`.\n"+
|
|
||||||
"To request changes on behalf of the group, create the following comment: `@"+bot.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" +
|
|
||||||
"Submitter is member of this review group, hence they are excluded from being one of the reviewers here"
|
|
||||||
}
|
|
||||||
bot.gitea.AddComment(pr, helpComment)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ReviewNotFinished
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bot *ReviewBot) PeriodReviewCheck() {
|
func PeriodReviewCheck(gitea common.Gitea) {
|
||||||
notifications, err := bot.gitea.GetNotifications(common.GiteaNotificationType_Pull, nil)
|
notifications, err := gitea.GetNotifications(common.GiteaNotificationType_Pull, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(" Error fetching unread notifications: %w", err)
|
common.LogError(" Error fetching unread notifications: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, notification := range notifications {
|
for _, notification := range notifications {
|
||||||
bot.ProcessNotifications(notification)
|
ProcessNotifications(notification, gitea)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
giteaUrl := flag.String("gitea-url", "https://src.opensuse.org", "Gitea instance used for reviews")
|
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")
|
rabbitMqHost := flag.String("rabbit-url", "amqps://rabbit.opensuse.org", "RabbitMQ instance where Gitea webhook notifications are sent")
|
||||||
interval := flag.Int64("interval", 10, "Notification polling interval in minutes (min 1 min)")
|
interval := flag.Int64("interval", 5, "Notification polling interval in minutes (min 1 min)")
|
||||||
configFile := flag.String("config", "", "PrjGit listing config file")
|
configFile := flag.String("config", "", "PrjGit listing config file")
|
||||||
logging := flag.String("logging", "info", "Logging level: [none, error, info, debug]")
|
logging := flag.String("logging", "info", "Logging level: [none, error, info, debug]")
|
||||||
flag.BoolVar(&common.IsDryRun, "dry", false, "Dry run, no effect. For debugging")
|
flag.BoolVar(&common.IsDryRun, "dry", false, "Dry run, no effect. For debugging")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if err := common.SetLoggingLevelFromString(*logging); err != nil {
|
|
||||||
common.LogError(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if cf := os.Getenv("AUTOGITS_CONFIG"); len(cf) > 0 {
|
|
||||||
*configFile = cf
|
|
||||||
}
|
|
||||||
if url := os.Getenv("AUTOGITS_URL"); len(url) > 0 {
|
|
||||||
*giteaUrl = url
|
|
||||||
}
|
|
||||||
if url := os.Getenv("AUTOGITS_RABBITURL"); len(url) > 0 {
|
|
||||||
*rabbitMqHost = url
|
|
||||||
}
|
|
||||||
if debug := os.Getenv("AUTOGITS_DEBUG"); len(debug) > 0 {
|
|
||||||
common.SetLoggingLevel(common.LogLevelDebug)
|
|
||||||
}
|
|
||||||
|
|
||||||
args := flag.Args()
|
args := flag.Args()
|
||||||
if len(args) != 1 {
|
if len(args) != 1 {
|
||||||
log.Println(" syntax:")
|
log.Println(" syntax:")
|
||||||
@@ -356,7 +291,7 @@ func main() {
|
|||||||
flag.Usage()
|
flag.Usage()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
targetGroupName := args[0]
|
groupName = args[0]
|
||||||
|
|
||||||
if *configFile == "" {
|
if *configFile == "" {
|
||||||
common.LogError("Missing config file")
|
common.LogError("Missing config file")
|
||||||
@@ -379,35 +314,36 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
giteaTransport := common.AllocateGiteaTransport(*giteaUrl)
|
gitea := common.AllocateGiteaTransport(*giteaUrl)
|
||||||
configs, err := common.ResolveWorkflowConfigs(giteaTransport, configData)
|
configs, err = common.ResolveWorkflowConfigs(gitea, configData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError("Cannot parse workflow configs:", err)
|
common.LogError("Cannot parse workflow configs:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
reviewer, err := giteaTransport.GetCurrentUser()
|
reviewer, err := gitea.GetCurrentUser()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError("Cannot fetch review user:", err)
|
common.LogError("Cannot fetch review user:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := common.SetLoggingLevelFromString(*logging); err != nil {
|
||||||
|
common.LogError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if *interval < 1 {
|
if *interval < 1 {
|
||||||
*interval = 1
|
*interval = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
bot := &ReviewBot{
|
InitRegex(groupName)
|
||||||
gitea: giteaTransport,
|
|
||||||
configs: configs,
|
|
||||||
}
|
|
||||||
bot.InitRegex(targetGroupName)
|
|
||||||
|
|
||||||
common.LogInfo(" ** processing group reviews for group:", bot.groupName)
|
common.LogInfo(" ** processing group reviews for group:", groupName)
|
||||||
common.LogInfo(" ** username in Gitea:", reviewer.UserName)
|
common.LogInfo(" ** username in Gitea:", reviewer.UserName)
|
||||||
common.LogInfo(" ** polling interval:", *interval, "min")
|
common.LogInfo(" ** polling interval:", *interval, "min")
|
||||||
common.LogInfo(" ** connecting to RabbitMQ:", *rabbitMqHost)
|
common.LogInfo(" ** connecting to RabbitMQ:", *rabbitMqHost)
|
||||||
|
|
||||||
if bot.groupName != reviewer.UserName {
|
if groupName != reviewer.UserName {
|
||||||
common.LogError(" ***** Reviewer does not match group name. Aborting. *****")
|
common.LogError(" ***** Reviewer does not match group name. Aborting. *****")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -419,28 +355,22 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config_update := ConfigUpdatePush{
|
config_update := ConfigUpdatePush{
|
||||||
bot: bot,
|
|
||||||
config_modified: make(chan *common.AutogitConfig),
|
config_modified: make(chan *common.AutogitConfig),
|
||||||
}
|
}
|
||||||
|
|
||||||
process_issue_pr := IssueCommentProcessor{
|
configUpdates := &common.ListenDefinitions{
|
||||||
bot: bot,
|
RabbitURL: u,
|
||||||
}
|
Orgs: []string{},
|
||||||
|
|
||||||
configUpdates := &common.RabbitMQGiteaEventsProcessor{
|
|
||||||
Orgs: []string{},
|
|
||||||
Handlers: map[string]common.RequestProcessor{
|
Handlers: map[string]common.RequestProcessor{
|
||||||
common.RequestType_Push: &config_update,
|
common.RequestType_Push: &config_update,
|
||||||
common.RequestType_IssueComment: &process_issue_pr,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
configUpdates.Connection().RabbitURL = u
|
for _, c := range configs {
|
||||||
for _, c := range bot.configs {
|
|
||||||
if org, _, _ := c.GetPrjGit(); !slices.Contains(configUpdates.Orgs, org) {
|
if org, _, _ := c.GetPrjGit(); !slices.Contains(configUpdates.Orgs, org) {
|
||||||
configUpdates.Orgs = append(configUpdates.Orgs, org)
|
configUpdates.Orgs = append(configUpdates.Orgs, org)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
go common.ProcessRabbitMQEvents(configUpdates)
|
go configUpdates.ProcessRabbitMQEvents()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
config_update_loop:
|
config_update_loop:
|
||||||
@@ -448,17 +378,17 @@ func main() {
|
|||||||
select {
|
select {
|
||||||
case configTouched, ok := <-config_update.config_modified:
|
case configTouched, ok := <-config_update.config_modified:
|
||||||
if ok {
|
if ok {
|
||||||
for idx, c := range bot.configs {
|
for idx, c := range configs {
|
||||||
if c == configTouched {
|
if c == configTouched {
|
||||||
org, repo, branch := c.GetPrjGit()
|
org, repo, branch := c.GetPrjGit()
|
||||||
prj := fmt.Sprintf("%s/%s#%s", org, repo, branch)
|
prj := fmt.Sprintf("%s/%s#%s", org, repo, branch)
|
||||||
common.LogInfo("Detected config update for", prj)
|
common.LogInfo("Detected config update for", prj)
|
||||||
|
|
||||||
new_config, err := common.ReadWorkflowConfig(bot.gitea, prj)
|
new_config, err := common.ReadWorkflowConfig(gitea, prj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError("Failed parsing Project config for", prj, err)
|
common.LogError("Failed parsing Project config for", prj, err)
|
||||||
} else {
|
} else {
|
||||||
bot.configs[idx] = new_config
|
configs[idx] = new_config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,7 +398,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bot.PeriodReviewCheck()
|
PeriodReviewCheck(gitea)
|
||||||
time.Sleep(time.Duration(*interval * int64(time.Minute)))
|
time.Sleep(time.Duration(*interval * int64(time.Minute)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,492 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import "testing"
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"go.uber.org/mock/gomock"
|
func TestReviews(t *testing.T) {
|
||||||
"src.opensuse.org/autogits/common"
|
|
||||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
|
||||||
mock_common "src.opensuse.org/autogits/common/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestProcessPR(t *testing.T) {
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
defer ctrl.Finish()
|
|
||||||
|
|
||||||
mockGitea := mock_common.NewMockGitea(ctrl)
|
|
||||||
groupName := "testgroup"
|
|
||||||
|
|
||||||
bot := &ReviewBot{
|
|
||||||
gitea: mockGitea,
|
|
||||||
groupName: groupName,
|
|
||||||
}
|
|
||||||
bot.InitRegex(groupName)
|
|
||||||
|
|
||||||
org := "myorg"
|
|
||||||
repo := "myrepo"
|
|
||||||
prIndex := int64(1)
|
|
||||||
headSha := "abcdef123456"
|
|
||||||
|
|
||||||
pr := &models.PullRequest{
|
|
||||||
Index: prIndex,
|
|
||||||
URL: "http://gitea/pr/1",
|
|
||||||
State: "open",
|
|
||||||
Base: &models.PRBranchInfo{
|
|
||||||
Name: "main",
|
|
||||||
Repo: &models.Repository{
|
|
||||||
Name: repo,
|
|
||||||
Owner: &models.User{
|
|
||||||
UserName: org,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Head: &models.PRBranchInfo{
|
|
||||||
Sha: headSha,
|
|
||||||
},
|
|
||||||
User: &models.User{
|
|
||||||
UserName: "submitter",
|
|
||||||
},
|
|
||||||
RequestedReviewers: []*models.User{
|
|
||||||
{UserName: groupName},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
prjConfig := &common.AutogitConfig{
|
|
||||||
GitProjectName: org + "/" + repo + "#main",
|
|
||||||
ReviewGroups: []*common.ReviewGroup{
|
|
||||||
{
|
|
||||||
Name: groupName,
|
|
||||||
Reviewers: []string{"reviewer1", "reviewer2"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
bot.configs = common.AutogitConfigs{prjConfig}
|
|
||||||
|
|
||||||
t.Run("Review not requested for group", func(t *testing.T) {
|
|
||||||
prNoRequest := *pr
|
|
||||||
prNoRequest.RequestedReviewers = nil
|
|
||||||
err := bot.ProcessPR(&prNoRequest)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected no error, got %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("PR is closed", func(t *testing.T) {
|
|
||||||
prClosed := *pr
|
|
||||||
prClosed.State = "closed"
|
|
||||||
err := bot.ProcessPR(&prClosed)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected no error, got %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Successful Approval", func(t *testing.T) {
|
|
||||||
common.IsDryRun = false
|
|
||||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
|
|
||||||
// reviewer1 approved in timeline
|
|
||||||
timeline := []*models.TimelineComment{
|
|
||||||
{
|
|
||||||
Type: common.TimelineCommentType_Comment,
|
|
||||||
User: &models.User{UserName: "reviewer1"},
|
|
||||||
Body: "@" + groupName + ": approve",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
|
|
||||||
|
|
||||||
expectedText := "reviewer1 approved a review on behalf of " + groupName
|
|
||||||
mockGitea.EXPECT().AddReviewComment(pr, common.ReviewStateApproved, expectedText).Return(nil, nil)
|
|
||||||
mockGitea.EXPECT().UnrequestReview(org, repo, prIndex, gomock.Any()).Return(nil)
|
|
||||||
|
|
||||||
err := bot.ProcessPR(pr)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected nil error, got %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Dry Run - No actions taken", func(t *testing.T) {
|
|
||||||
common.IsDryRun = true
|
|
||||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
|
|
||||||
timeline := []*models.TimelineComment{
|
|
||||||
{
|
|
||||||
Type: common.TimelineCommentType_Comment,
|
|
||||||
User: &models.User{UserName: "reviewer1"},
|
|
||||||
Body: "@" + groupName + ": approve",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
|
|
||||||
|
|
||||||
// No AddReviewComment or UnrequestReview should be called
|
|
||||||
err := bot.ProcessPR(pr)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected nil error, got %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Approval already exists - No new comment", func(t *testing.T) {
|
|
||||||
common.IsDryRun = false
|
|
||||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
|
|
||||||
|
|
||||||
approvalText := "reviewer1 approved a review on behalf of " + groupName
|
|
||||||
timeline := []*models.TimelineComment{
|
|
||||||
{
|
|
||||||
Type: common.TimelineCommentType_Review,
|
|
||||||
User: &models.User{UserName: groupName},
|
|
||||||
Body: approvalText,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: common.TimelineCommentType_Comment,
|
|
||||||
User: &models.User{UserName: "reviewer1"},
|
|
||||||
Body: "@" + groupName + ": approve",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: common.TimelineCommentType_Comment,
|
|
||||||
User: &models.User{UserName: groupName},
|
|
||||||
Body: "Help comment",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
|
|
||||||
|
|
||||||
// No AddReviewComment, UnrequestReview, or AddComment should be called
|
|
||||||
err := bot.ProcessPR(pr)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected nil error, got %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Rejection already exists - No new comment", func(t *testing.T) {
|
|
||||||
common.IsDryRun = false
|
|
||||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
|
|
||||||
|
|
||||||
rejectionText := "reviewer1 requested changes on behalf of " + groupName + ". See http://gitea/comment/123"
|
|
||||||
timeline := []*models.TimelineComment{
|
|
||||||
{
|
|
||||||
Type: common.TimelineCommentType_Review,
|
|
||||||
User: &models.User{UserName: groupName},
|
|
||||||
Body: rejectionText,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: common.TimelineCommentType_Comment,
|
|
||||||
User: &models.User{UserName: "reviewer1"},
|
|
||||||
Body: "@" + groupName + ": decline",
|
|
||||||
HTMLURL: "http://gitea/comment/123",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: common.TimelineCommentType_Comment,
|
|
||||||
User: &models.User{UserName: groupName},
|
|
||||||
Body: "Help comment",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
|
|
||||||
|
|
||||||
err := bot.ProcessPR(pr)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected nil error, got %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Pending review - Help comment already exists", func(t *testing.T) {
|
|
||||||
common.IsDryRun = false
|
|
||||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
|
|
||||||
|
|
||||||
timeline := []*models.TimelineComment{
|
|
||||||
{
|
|
||||||
Type: common.TimelineCommentType_Comment,
|
|
||||||
User: &models.User{UserName: groupName},
|
|
||||||
Body: "Some help comment",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
|
|
||||||
|
|
||||||
// It will try to request reviews
|
|
||||||
mockGitea.EXPECT().RequestReviews(pr, "reviewer1", "reviewer2").Return(nil, nil)
|
|
||||||
|
|
||||||
// AddComment should NOT be called because bot already has a comment in timeline
|
|
||||||
err := bot.ProcessPR(pr)
|
|
||||||
if err != ReviewNotFinished {
|
|
||||||
t.Errorf("Expected ReviewNotFinished error, got %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Submitter is group member - Excluded from review request", func(t *testing.T) {
|
|
||||||
common.IsDryRun = false
|
|
||||||
prSubmitterMember := *pr
|
|
||||||
prSubmitterMember.User = &models.User{UserName: "reviewer1"}
|
|
||||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
|
|
||||||
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(nil, nil)
|
|
||||||
mockGitea.EXPECT().RequestReviews(&prSubmitterMember, "reviewer2").Return(nil, nil)
|
|
||||||
mockGitea.EXPECT().AddComment(&prSubmitterMember, gomock.Any()).Return(nil)
|
|
||||||
err := bot.ProcessPR(&prSubmitterMember)
|
|
||||||
if err != ReviewNotFinished {
|
|
||||||
t.Errorf("Expected ReviewNotFinished error, got %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Successful Rejection", func(t *testing.T) {
|
|
||||||
common.IsDryRun = false
|
|
||||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
|
|
||||||
timeline := []*models.TimelineComment{
|
|
||||||
{
|
|
||||||
Type: common.TimelineCommentType_Comment,
|
|
||||||
User: &models.User{UserName: "reviewer2"},
|
|
||||||
Body: "@" + groupName + ": decline",
|
|
||||||
HTMLURL: "http://gitea/comment/999",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
|
|
||||||
expectedText := "reviewer2 requested changes on behalf of " + groupName + ". See http://gitea/comment/999"
|
|
||||||
mockGitea.EXPECT().AddReviewComment(pr, common.ReviewStateRequestChanges, expectedText).Return(nil, nil)
|
|
||||||
mockGitea.EXPECT().UnrequestReview(org, repo, prIndex, gomock.Any()).Return(nil)
|
|
||||||
err := bot.ProcessPR(pr)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected nil error, got %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Config not found", func(t *testing.T) {
|
|
||||||
bot.configs = common.AutogitConfigs{}
|
|
||||||
err := bot.ProcessPR(pr)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error when config is missing, got nil")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Gitea error in GetPullRequestReviews", func(t *testing.T) {
|
|
||||||
bot.configs = common.AutogitConfigs{prjConfig}
|
|
||||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, fmt.Errorf("gitea error"))
|
|
||||||
err := bot.ProcessPR(pr)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error from gitea, got nil")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProcessNotifications(t *testing.T) {
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
defer ctrl.Finish()
|
|
||||||
|
|
||||||
mockGitea := mock_common.NewMockGitea(ctrl)
|
|
||||||
groupName := "testgroup"
|
|
||||||
|
|
||||||
bot := &ReviewBot{
|
|
||||||
gitea: mockGitea,
|
|
||||||
groupName: groupName,
|
|
||||||
}
|
|
||||||
bot.InitRegex(groupName)
|
|
||||||
|
|
||||||
org := "myorg"
|
|
||||||
repo := "myrepo"
|
|
||||||
prIndex := int64(123)
|
|
||||||
notificationID := int64(456)
|
|
||||||
|
|
||||||
notification := &models.NotificationThread{
|
|
||||||
ID: notificationID,
|
|
||||||
Subject: &models.NotificationSubject{
|
|
||||||
URL: fmt.Sprintf("http://gitea/api/v1/repos/%s/%s/pulls/%d", org, repo, prIndex),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("Notification Success", func(t *testing.T) {
|
|
||||||
common.IsDryRun = false
|
|
||||||
pr := &models.PullRequest{
|
|
||||||
Index: prIndex,
|
|
||||||
Base: &models.PRBranchInfo{
|
|
||||||
Name: "main",
|
|
||||||
Repo: &models.Repository{
|
|
||||||
Name: repo,
|
|
||||||
Owner: &models.User{UserName: org},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
Head: &models.PRBranchInfo{
|
|
||||||
Sha: "headsha",
|
|
||||||
Repo: &models.Repository{
|
|
||||||
Name: repo,
|
|
||||||
Owner: &models.User{UserName: org},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
User: &models.User{UserName: "submitter"},
|
|
||||||
RequestedReviewers: []*models.User{{UserName: groupName}},
|
|
||||||
}
|
|
||||||
|
|
||||||
mockGitea.EXPECT().GetPullRequest(org, repo, prIndex).Return(pr, nil)
|
|
||||||
|
|
||||||
prjConfig := &common.AutogitConfig{
|
|
||||||
GitProjectName: org + "/" + repo + "#main",
|
|
||||||
ReviewGroups: []*common.ReviewGroup{{Name: groupName, Reviewers: []string{"r1"}}},
|
|
||||||
}
|
|
||||||
bot.configs = common.AutogitConfigs{prjConfig}
|
|
||||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, prIndex).Return(nil, nil)
|
|
||||||
timeline := []*models.TimelineComment{
|
|
||||||
{
|
|
||||||
Type: common.TimelineCommentType_Comment,
|
|
||||||
User: &models.User{UserName: "r1"},
|
|
||||||
Body: "@" + groupName + ": approve",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
mockGitea.EXPECT().GetTimeline(org, repo, prIndex).Return(timeline, nil)
|
|
||||||
expectedText := "r1 approved a review on behalf of " + groupName
|
|
||||||
mockGitea.EXPECT().AddReviewComment(pr, common.ReviewStateApproved, expectedText).Return(nil, nil)
|
|
||||||
mockGitea.EXPECT().UnrequestReview(org, repo, prIndex, gomock.Any()).Return(nil)
|
|
||||||
|
|
||||||
mockGitea.EXPECT().SetNotificationRead(notificationID).Return(nil)
|
|
||||||
|
|
||||||
bot.ProcessNotifications(notification)
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Invalid Notification URL", func(t *testing.T) {
|
|
||||||
badNotification := &models.NotificationThread{
|
|
||||||
Subject: &models.NotificationSubject{
|
|
||||||
URL: "http://gitea/invalid/url",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
bot.ProcessNotifications(badNotification)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Gitea error in GetPullRequest", func(t *testing.T) {
|
|
||||||
mockGitea.EXPECT().GetPullRequest(org, repo, prIndex).Return(nil, fmt.Errorf("gitea error"))
|
|
||||||
bot.ProcessNotifications(notification)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReviewApprovalCheck(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
Name string
|
|
||||||
GroupName string
|
|
||||||
InString string
|
|
||||||
Approved bool
|
|
||||||
Rejected bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "Empty String",
|
|
||||||
GroupName: "group",
|
|
||||||
InString: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Random Text",
|
|
||||||
GroupName: "group",
|
|
||||||
InString: "some things LGTM",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Group name with Random Text means disapproval",
|
|
||||||
GroupName: "group",
|
|
||||||
InString: "@group: some things LGTM",
|
|
||||||
Rejected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Bad name with Approval",
|
|
||||||
GroupName: "group2",
|
|
||||||
InString: "@group: LGTM",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Bad name with Approval",
|
|
||||||
GroupName: "group2",
|
|
||||||
InString: "@group: LGTM",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "LGTM approval",
|
|
||||||
GroupName: "group2",
|
|
||||||
InString: "@group2: LGTM",
|
|
||||||
Approved: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "approval",
|
|
||||||
GroupName: "group2",
|
|
||||||
InString: "@group2: approved",
|
|
||||||
Approved: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "approval",
|
|
||||||
GroupName: "group2",
|
|
||||||
InString: "@group2: approve",
|
|
||||||
Approved: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "disapproval",
|
|
||||||
GroupName: "group2",
|
|
||||||
InString: "@group2: disapprove",
|
|
||||||
Rejected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Whitespace before colon",
|
|
||||||
GroupName: "group",
|
|
||||||
InString: "@group : LGTM",
|
|
||||||
Approved: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "No whitespace after colon",
|
|
||||||
GroupName: "group",
|
|
||||||
InString: "@group:LGTM",
|
|
||||||
Approved: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Leading and trailing whitespace on line",
|
|
||||||
GroupName: "group",
|
|
||||||
InString: " @group: LGTM ",
|
|
||||||
Approved: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Multiline: Approved on second line",
|
|
||||||
GroupName: "group",
|
|
||||||
InString: "Random noise\n@group: approved",
|
|
||||||
Approved: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Multiline: Multiple group mentions, first wins",
|
|
||||||
GroupName: "group",
|
|
||||||
InString: "@group: decline\n@group: approve",
|
|
||||||
Rejected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Multiline: Approved on second line",
|
|
||||||
GroupName: "group",
|
|
||||||
InString: "noise\n@group: approve\nmore noise",
|
|
||||||
Approved: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Not at start of line (even with whitespace)",
|
|
||||||
GroupName: "group",
|
|
||||||
InString: "Hello @group: approve",
|
|
||||||
Approved: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Rejecting with reason",
|
|
||||||
GroupName: "group",
|
|
||||||
InString: "@group: decline because of X, Y and Z",
|
|
||||||
Rejected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "No colon after group",
|
|
||||||
GroupName: "group",
|
|
||||||
InString: "@group LGTM",
|
|
||||||
Approved: false,
|
|
||||||
Rejected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "Invalid char after group",
|
|
||||||
GroupName: "group",
|
|
||||||
InString: "@group! LGTM",
|
|
||||||
Approved: false,
|
|
||||||
Rejected: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.Name, func(t *testing.T) {
|
|
||||||
bot := &ReviewBot{}
|
|
||||||
bot.InitRegex(test.GroupName)
|
|
||||||
|
|
||||||
if r := bot.ReviewAccepted(test.InString); r != test.Approved {
|
|
||||||
t.Error("ReviewAccepted() returned", r, "expecting", test.Approved)
|
|
||||||
}
|
|
||||||
if r := bot.ReviewRejected(test.InString); r != test.Rejected {
|
|
||||||
t.Error("ReviewRejected() returned", r, "expecting", test.Rejected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,29 +7,7 @@ import (
|
|||||||
"src.opensuse.org/autogits/common"
|
"src.opensuse.org/autogits/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IssueCommentProcessor struct {
|
|
||||||
bot *ReviewBot
|
|
||||||
}
|
|
||||||
|
|
||||||
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 := s.bot.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 s.bot.ProcessPR(pr)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfigUpdatePush struct {
|
type ConfigUpdatePush struct {
|
||||||
bot *ReviewBot
|
|
||||||
config_modified chan *common.AutogitConfig
|
config_modified chan *common.AutogitConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +27,7 @@ func (s *ConfigUpdatePush) ProcessFunc(req *common.Request) error {
|
|||||||
}
|
}
|
||||||
branch := data.Ref[len(branch_ref):]
|
branch := data.Ref[len(branch_ref):]
|
||||||
|
|
||||||
c := s.bot.configs.GetPrjGitConfig(org, repo, branch)
|
c := configs.GetPrjGitConfig(org, repo, branch)
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -67,7 +45,7 @@ func (s *ConfigUpdatePush) ProcessFunc(req *common.Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if modified_config {
|
if modified_config {
|
||||||
for _, config := range s.bot.configs {
|
for _, config := range configs {
|
||||||
if o, r, _ := config.GetPrjGit(); o == org && r == repo {
|
if o, r, _ := config.GetPrjGit(); o == org && r == repo {
|
||||||
s.config_modified <- config
|
s.config_modified <- config
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"go.uber.org/mock/gomock"
|
|
||||||
"src.opensuse.org/autogits/common"
|
|
||||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
|
||||||
mock_common "src.opensuse.org/autogits/common/mock"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestIssueCommentProcessor(t *testing.T) {
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
defer ctrl.Finish()
|
|
||||||
|
|
||||||
mockGitea := mock_common.NewMockGitea(ctrl)
|
|
||||||
groupName := "testgroup"
|
|
||||||
bot := &ReviewBot{
|
|
||||||
gitea: mockGitea,
|
|
||||||
groupName: groupName,
|
|
||||||
}
|
|
||||||
bot.InitRegex(groupName)
|
|
||||||
|
|
||||||
processor := &IssueCommentProcessor{bot: bot}
|
|
||||||
|
|
||||||
org := "myorg"
|
|
||||||
repo := "myrepo"
|
|
||||||
index := 123
|
|
||||||
|
|
||||||
event := &common.IssueCommentWebhookEvent{
|
|
||||||
Repository: &common.Repository{
|
|
||||||
Name: repo,
|
|
||||||
Owner: &common.Organization{
|
|
||||||
Username: org,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Issue: &common.IssueDetail{
|
|
||||||
Number: index,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
req := &common.Request{
|
|
||||||
Type: common.RequestType_IssueComment,
|
|
||||||
Data: event,
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("Successful Processing", func(t *testing.T) {
|
|
||||||
pr := &models.PullRequest{
|
|
||||||
Index: int64(index),
|
|
||||||
Base: &models.PRBranchInfo{
|
|
||||||
Name: "main",
|
|
||||||
Repo: &models.Repository{
|
|
||||||
Name: repo,
|
|
||||||
Owner: &models.User{UserName: org},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Head: &models.PRBranchInfo{
|
|
||||||
Sha: "headsha",
|
|
||||||
Repo: &models.Repository{
|
|
||||||
Name: repo,
|
|
||||||
Owner: &models.User{UserName: org},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
User: &models.User{UserName: "submitter"},
|
|
||||||
RequestedReviewers: []*models.User{{UserName: groupName}},
|
|
||||||
}
|
|
||||||
|
|
||||||
mockGitea.EXPECT().GetPullRequest(org, repo, int64(index)).Return(pr, nil)
|
|
||||||
|
|
||||||
prjConfig := &common.AutogitConfig{
|
|
||||||
GitProjectName: org + "/" + repo + "#main",
|
|
||||||
ReviewGroups: []*common.ReviewGroup{{Name: groupName, Reviewers: []string{"r1"}}},
|
|
||||||
}
|
|
||||||
bot.configs = common.AutogitConfigs{prjConfig}
|
|
||||||
mockGitea.EXPECT().GetPullRequestReviews(org, repo, int64(index)).Return(nil, nil)
|
|
||||||
mockGitea.EXPECT().GetTimeline(org, repo, int64(index)).Return(nil, nil)
|
|
||||||
mockGitea.EXPECT().RequestReviews(pr, "r1").Return(nil, nil)
|
|
||||||
mockGitea.EXPECT().AddComment(pr, gomock.Any()).Return(nil)
|
|
||||||
|
|
||||||
err := processor.ProcessFunc(req)
|
|
||||||
if err != ReviewNotFinished {
|
|
||||||
t.Errorf("Expected ReviewNotFinished, got %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Gitea error in GetPullRequest", func(t *testing.T) {
|
|
||||||
mockGitea.EXPECT().GetPullRequest(org, repo, int64(index)).Return(nil, fmt.Errorf("gitea error"))
|
|
||||||
err := processor.ProcessFunc(req)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error, got nil")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Wrong Request Type", func(t *testing.T) {
|
|
||||||
wrongReq := &common.Request{Type: common.RequestType_Push}
|
|
||||||
err := processor.ProcessFunc(wrongReq)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error for wrong request type, got nil")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigUpdatePush(t *testing.T) {
|
|
||||||
ctrl := gomock.NewController(t)
|
|
||||||
defer ctrl.Finish()
|
|
||||||
|
|
||||||
groupName := "testgroup"
|
|
||||||
bot := &ReviewBot{
|
|
||||||
groupName: groupName,
|
|
||||||
}
|
|
||||||
bot.InitRegex(groupName)
|
|
||||||
|
|
||||||
configChan := make(chan *common.AutogitConfig, 1)
|
|
||||||
processor := &ConfigUpdatePush{
|
|
||||||
bot: bot,
|
|
||||||
config_modified: configChan,
|
|
||||||
}
|
|
||||||
|
|
||||||
org := "myorg"
|
|
||||||
repo := "myrepo"
|
|
||||||
branch := "main"
|
|
||||||
|
|
||||||
prjConfig := &common.AutogitConfig{
|
|
||||||
GitProjectName: org + "/" + repo + "#" + branch,
|
|
||||||
Organization: org,
|
|
||||||
Branch: branch,
|
|
||||||
}
|
|
||||||
bot.configs = common.AutogitConfigs{prjConfig}
|
|
||||||
|
|
||||||
event := &common.PushWebhookEvent{
|
|
||||||
Ref: "refs/heads/" + branch,
|
|
||||||
Repository: &common.Repository{
|
|
||||||
Name: repo,
|
|
||||||
Owner: &common.Organization{
|
|
||||||
Username: org,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Commits: []common.Commit{
|
|
||||||
{
|
|
||||||
Modified: []string{common.ProjectConfigFile},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
req := &common.Request{
|
|
||||||
Type: common.RequestType_Push,
|
|
||||||
Data: event,
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("Config Modified", func(t *testing.T) {
|
|
||||||
err := processor.ProcessFunc(req)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected nil error, got %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case modified := <-configChan:
|
|
||||||
if modified != prjConfig {
|
|
||||||
t.Errorf("Expected modified config to be %v, got %v", prjConfig, modified)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
t.Error("Expected config modification signal, but none received")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("No Config Modified", func(t *testing.T) {
|
|
||||||
noConfigEvent := *event
|
|
||||||
noConfigEvent.Commits = []common.Commit{{Modified: []string{"README.md"}}}
|
|
||||||
noConfigReq := &common.Request{Type: common.RequestType_Push, Data: &noConfigEvent}
|
|
||||||
|
|
||||||
err := processor.ProcessFunc(noConfigReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected nil error, got %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-configChan:
|
|
||||||
t.Error("Did not expect config modification signal")
|
|
||||||
default:
|
|
||||||
// Success
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Wrong Branch Ref", func(t *testing.T) {
|
|
||||||
wrongBranchEvent := *event
|
|
||||||
wrongBranchEvent.Ref = "refs/tags/v1.0"
|
|
||||||
wrongBranchReq := &common.Request{Type: common.RequestType_Push, Data: &wrongBranchEvent}
|
|
||||||
|
|
||||||
err := processor.ProcessFunc(wrongBranchReq)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected error for wrong branch ref, got nil")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Config Not Found", func(t *testing.T) {
|
|
||||||
bot.configs = common.AutogitConfigs{}
|
|
||||||
err := processor.ProcessFunc(req)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected nil error even if config not found, got %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
Purpose
|
|
||||||
-------
|
|
||||||
|
|
||||||
Forwards PR as an OBS submit request when review requested.
|
|
||||||
Accepts a request when OBS request is accepted.
|
|
||||||
Rejects a request when OBS request is denied.
|
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ func ProcessNotification(notification *models.NotificationThread) {
|
|||||||
repo := match[2]
|
repo := match[2]
|
||||||
id, _ := strconv.ParseInt(match[3], 10, 64)
|
id, _ := strconv.ParseInt(match[3], 10, 64)
|
||||||
|
|
||||||
common.LogInfo("processing:", fmt.Sprintf("%s/%s!%d", org, repo, id))
|
common.LogInfo("processing:", fmt.Sprintf("%s/%s#%d", org, repo, id))
|
||||||
pr, err := Gitea.GetPullRequest(org, repo, id)
|
pr, err := Gitea.GetPullRequest(org, repo, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(" ** Cannot fetch PR associated with review:", subject.URL, "Error:", err)
|
common.LogError(" ** Cannot fetch PR associated with review:", subject.URL, "Error:", err)
|
||||||
|
|||||||
@@ -4,15 +4,11 @@ OBS Staging Bot
|
|||||||
Build a PR against a ProjectGit, if review is requested.
|
Build a PR against a ProjectGit, if review is requested.
|
||||||
|
|
||||||
|
|
||||||
Main Tasks
|
Areas of Responsibility
|
||||||
----------
|
-----------------------
|
||||||
|
|
||||||
* A build in OBS is initiated when a review for this bot is requested.
|
* Monitors Notification API in Gitea for review requests
|
||||||
* The overall build status is reported:
|
* Reviews Package build results in OBS for all changed packages in ProjectGit PR
|
||||||
* Build successful
|
|
||||||
* Build failed
|
|
||||||
* It checks the build status only for the involved packages compared to the last state of the project for all architectures and all flavors.
|
|
||||||
* It adds an svg with detailed building status.
|
|
||||||
|
|
||||||
|
|
||||||
Target Usage
|
Target Usage
|
||||||
@@ -20,53 +16,3 @@ Target Usage
|
|||||||
|
|
||||||
Any project (devel, etc) that accepts PR and wants build results
|
Any project (devel, etc) that accepts PR and wants build results
|
||||||
|
|
||||||
|
|
||||||
Configuration File
|
|
||||||
------------------
|
|
||||||
|
|
||||||
Bot reads `staging.config` from the project git or the PR to the project git.
|
|
||||||
It's a JSON file with following syntax:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"ObsProject": "SUSE:SLFO:1.2",
|
|
||||||
"StagingProject": "SUSE:SLFO:1.2:PullRequest",
|
|
||||||
"QA": [
|
|
||||||
{
|
|
||||||
"Name": "SLES",
|
|
||||||
"Origin": "SUSE:SLFO:Products:SLES:16.0",
|
|
||||||
"BuildDisableRepos": ["product"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
| Field name | Details | Mandatory | Type | Allowed Values | Default |
|
|
||||||
| ----- | ----- | ----- | ----- | ----- | ----- |
|
|
||||||
| *ObsProject* | Product OBS project. Builds in this project will be used to compare to builds based on sources from the PR. | yes | string | `[a-zA-Z0-9-_:]+` | |
|
|
||||||
| *StagingProject* | Used both as base project and prefix for all OBS staging projects. Upon being added as a reviewer to a PrjGit PR, this bot automatically generates an OBS project named *StagingProject:<PR_Number>*. It must be a sub-project of the *ObsProject*. | yes | string | `[a-zA-Z0-9-_:]+` | |
|
|
||||||
| *QA* | Crucial for generating a product build (such as an ISO or FTP tree) that incorporates the packages. | no | array of objects | | |
|
|
||||||
| *QA > Name* | Suffix for the QA OBS staging project. The project is named *StagingProject:<PR_Number>:Name*. | no | string | | |
|
|
||||||
| *QA > Origin* | OBS reference project | no | string | | |
|
|
||||||
| *QA > BuildDisableRepos* | The names of OBS repositories to build-disable, if any. | no | array of strings | | [] |
|
|
||||||
|
|
||||||
|
|
||||||
Details
|
|
||||||
-------
|
|
||||||
|
|
||||||
* **OBS staging projects are deleted** when the relative PrjGit PR is closed or merged.
|
|
||||||
|
|
||||||
* **PrjGit PR - staging project**
|
|
||||||
* The OBS staging project utilizes an **scmsync** tag, configured with the `onlybuild` flag, to exclusively build packages associated with this specific PrjGit PR.
|
|
||||||
* The **build config** is inherited from the PrjGit PR config file (even if unchanged).
|
|
||||||
* The **project meta** creates a standard repository following the StagingProject as a project path.
|
|
||||||
* The base *StagingProject* has the macro **FromScratch:** set in its config, which prevents inheriting the configuration from the included project paths.
|
|
||||||
* The bot copies the project maintainers from *StagingProject* to the specific staging project (*StagingProject:<PR_Number>*).
|
|
||||||
* The bot reports “Build successful” only if the build is successful for all repositories and all architectures.
|
|
||||||
|
|
||||||
* **PrjGit PR - QA staging project**
|
|
||||||
* The QA staging project is meant for building the product; the relative build config is inherited from the `QA > Origin` project.
|
|
||||||
* In this case, the **scmsync** tag is inherited from the `QA > Origin` project.
|
|
||||||
* It is desirable in some cases to avoid building some specific build service repositories when not needed. In this case, `QA > BuildDisableRepos` can be specified.
|
|
||||||
These repositories would be disabled in the project meta when generating the QA project.
|
|
||||||
|
|
||||||
|
|||||||
@@ -47,18 +47,15 @@ const (
|
|||||||
Username = "autogits_obs_staging_bot"
|
Username = "autogits_obs_staging_bot"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var GiteaToken string
|
||||||
var runId uint
|
var runId uint
|
||||||
|
|
||||||
func FetchPrGit(git common.Git, pr *models.PullRequest) error {
|
func FetchPrGit(git common.Git, pr *models.PullRequest) error {
|
||||||
// clone PR head via base (target) repo
|
// clone PR head and base and return path
|
||||||
cloneURL := pr.Base.Repo.CloneURL
|
cloneURL := pr.Head.Repo.CloneURL
|
||||||
|
if GiteaUseSshClone {
|
||||||
// pass our token as user always
|
cloneURL = pr.Head.Repo.SSHURL
|
||||||
user, err := url.Parse(cloneURL)
|
}
|
||||||
common.PanicOnError(err)
|
|
||||||
user.User = url.User(common.GetGiteaToken())
|
|
||||||
cloneURL = user.String()
|
|
||||||
|
|
||||||
if _, err := os.Stat(path.Join(git.GetPath(), pr.Head.Sha)); os.IsNotExist(err) {
|
if _, err := os.Stat(path.Join(git.GetPath(), pr.Head.Sha)); os.IsNotExist(err) {
|
||||||
common.PanicOnError(git.GitExec("", "clone", "--depth", "1", cloneURL, pr.Head.Sha))
|
common.PanicOnError(git.GitExec("", "clone", "--depth", "1", cloneURL, pr.Head.Sha))
|
||||||
common.PanicOnError(git.GitExec(pr.Head.Sha, "fetch", "--depth", "1", "origin", pr.Head.Sha, pr.MergeBase))
|
common.PanicOnError(git.GitExec(pr.Head.Sha, "fetch", "--depth", "1", "origin", pr.Head.Sha, pr.MergeBase))
|
||||||
@@ -109,11 +106,6 @@ const (
|
|||||||
BuildStatusSummaryUnknown = 4
|
BuildStatusSummaryUnknown = 4
|
||||||
)
|
)
|
||||||
|
|
||||||
type DisableFlag struct {
|
|
||||||
XMLName string `xml:"disable"`
|
|
||||||
Name string `xml:"repository,attr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ProcessBuildStatus(project, refProject *common.BuildResultList) BuildStatusSummary {
|
func ProcessBuildStatus(project, refProject *common.BuildResultList) BuildStatusSummary {
|
||||||
if _, finished := refProject.BuildResultSummary(); !finished {
|
if _, finished := refProject.BuildResultSummary(); !finished {
|
||||||
common.LogDebug("refProject not finished building??")
|
common.LogDebug("refProject not finished building??")
|
||||||
@@ -128,7 +120,7 @@ func ProcessBuildStatus(project, refProject *common.BuildResultList) BuildStatus
|
|||||||
// the repositories should be setup equally between the projects. We
|
// the repositories should be setup equally between the projects. We
|
||||||
// need to verify that packages that are building in `refProject` are not
|
// need to verify that packages that are building in `refProject` are not
|
||||||
// failing in the `project`
|
// failing in the `project`
|
||||||
BuildResultSorter := func(a, b *common.BuildResult) int {
|
BuildResultSorter := func(a, b common.BuildResult) int {
|
||||||
if c := strings.Compare(a.Repository, b.Repository); c != 0 {
|
if c := strings.Compare(a.Repository, b.Repository); c != 0 {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
@@ -144,7 +136,7 @@ func ProcessBuildStatus(project, refProject *common.BuildResultList) BuildStatus
|
|||||||
common.LogInfo("New package. Only need some success...")
|
common.LogInfo("New package. Only need some success...")
|
||||||
SomeSuccess := false
|
SomeSuccess := false
|
||||||
for i := 0; i < len(project.Result); i++ {
|
for i := 0; i < len(project.Result); i++ {
|
||||||
repoRes := project.Result[i]
|
repoRes := &project.Result[i]
|
||||||
repoResStatus, ok := common.ObsRepoStatusDetails[repoRes.Code]
|
repoResStatus, ok := common.ObsRepoStatusDetails[repoRes.Code]
|
||||||
if !ok {
|
if !ok {
|
||||||
common.LogDebug("cannot find code:", repoRes.Code)
|
common.LogDebug("cannot find code:", repoRes.Code)
|
||||||
@@ -213,8 +205,8 @@ func ProcessBuildStatus(project, refProject *common.BuildResultList) BuildStatus
|
|||||||
return BuildStatusSummaryFailed
|
return BuildStatusSummaryFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProcessRepoBuildStatus(results, ref []*common.PackageBuildStatus) (status BuildStatusSummary, SomeSuccess bool) {
|
func ProcessRepoBuildStatus(results, ref []common.PackageBuildStatus) (status BuildStatusSummary, SomeSuccess bool) {
|
||||||
PackageBuildStatusSorter := func(a, b *common.PackageBuildStatus) int {
|
PackageBuildStatusSorter := func(a, b common.PackageBuildStatus) int {
|
||||||
return strings.Compare(a.Package, b.Package)
|
return strings.Compare(a.Package, b.Package)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,7 +263,7 @@ func ProcessRepoBuildStatus(results, ref []*common.PackageBuildStatus) (status B
|
|||||||
return BuildStatusSummarySuccess, SomeSuccess
|
return BuildStatusSummarySuccess, SomeSuccess
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingPrj, buildPrj string, stagingMasterPrj string) (*common.ProjectMeta, error) {
|
func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingPrj, buildPrj string) (*common.ProjectMeta, error) {
|
||||||
common.LogDebug("repo content fetching ...")
|
common.LogDebug("repo content fetching ...")
|
||||||
err := FetchPrGit(git, pr)
|
err := FetchPrGit(git, pr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -297,32 +289,7 @@ func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullReque
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// find modified directories and assume they are packages
|
meta, err := ObsClient.GetProjectMeta(buildPrj)
|
||||||
// TODO: use _manifest for this here
|
|
||||||
headDirectories, err := git.GitDirectoryList(dir, pr.Head.Sha)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
baseDirectories, err := git.GitDirectoryList(dir, pr.MergeBase)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for pkg, headOid := range headDirectories {
|
|
||||||
if baseOid, exists := baseDirectories[pkg]; !exists || baseOid != headOid {
|
|
||||||
modifiedOrNew = append(modifiedOrNew, pkg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
if err != nil {
|
||||||
common.LogError("error fetching project meta for", buildPrj, ". Err:", err)
|
common.LogError("error fetching project meta for", buildPrj, ". Err:", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -347,13 +314,13 @@ func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullReque
|
|||||||
urlPkg = append(urlPkg, "onlybuild="+url.QueryEscape(pkg))
|
urlPkg = append(urlPkg, "onlybuild="+url.QueryEscape(pkg))
|
||||||
}
|
}
|
||||||
meta.ScmSync = pr.Head.Repo.CloneURL + "?" + strings.Join(urlPkg, "&") + "#" + pr.Head.Sha
|
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)
|
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
|
||||||
|
// meta.PublicFlags = common.Flags{Contents: "<disable/>"}
|
||||||
|
|
||||||
// Untouched content are flags and involved users. These can be configured
|
meta.Groups = nil
|
||||||
// via the staging project.
|
meta.Persons = nil
|
||||||
|
|
||||||
// set paths to parent project
|
// set paths to parent project
|
||||||
for idx, r := range meta.Repositories {
|
for idx, r := range meta.Repositories {
|
||||||
@@ -382,7 +349,7 @@ func GenerateObsPrjMeta(git common.Git, gitea common.Gitea, pr *models.PullReque
|
|||||||
// stagingProject:$buildProject
|
// stagingProject:$buildProject
|
||||||
// ^- stagingProject:$buildProject:$subProjectName (based on templateProject)
|
// ^- stagingProject:$buildProject:$subProjectName (based on templateProject)
|
||||||
|
|
||||||
func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingProject, templateProject, subProjectName string, buildDisableRepos []string) error {
|
func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, gitea common.Gitea, pr *models.PullRequest, stagingProject, templateProject, subProjectName string) error {
|
||||||
common.LogDebug("Setup QA sub projects")
|
common.LogDebug("Setup QA sub projects")
|
||||||
templateMeta, err := ObsClient.GetProjectMeta(templateProject)
|
templateMeta, err := ObsClient.GetProjectMeta(templateProject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -391,42 +358,6 @@ func CreateQASubProject(stagingConfig *common.StagingConfig, git common.Git, git
|
|||||||
}
|
}
|
||||||
// patch baseMeta to become the new project
|
// patch baseMeta to become the new project
|
||||||
templateMeta.Name = stagingProject + ":" + subProjectName
|
templateMeta.Name = stagingProject + ":" + subProjectName
|
||||||
// freeze tag for now
|
|
||||||
if len(templateMeta.ScmSync) > 0 {
|
|
||||||
repository, err := url.Parse(templateMeta.ScmSync)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
common.LogDebug("getting data for ", repository.EscapedPath())
|
|
||||||
split := strings.Split(repository.EscapedPath(), "/")
|
|
||||||
org, repo := split[1], split[2]
|
|
||||||
|
|
||||||
common.LogDebug("getting commit for ", org, " repo ", repo, " fragment ", repository.Fragment)
|
|
||||||
branch, err := gitea.GetCommit(org, repo, repository.Fragment)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// set expanded commit url
|
|
||||||
repository.Fragment = branch.SHA
|
|
||||||
templateMeta.ScmSync = repository.String()
|
|
||||||
common.LogDebug("Setting scmsync url to ", templateMeta.ScmSync)
|
|
||||||
}
|
|
||||||
// Build-disable repositories if asked
|
|
||||||
if len(buildDisableRepos) > 0 {
|
|
||||||
toDisable := make([]DisableFlag, len(buildDisableRepos))
|
|
||||||
for idx, repositoryName := range buildDisableRepos {
|
|
||||||
toDisable[idx] = DisableFlag{Name: repositoryName}
|
|
||||||
}
|
|
||||||
|
|
||||||
output, err := xml.Marshal(toDisable)
|
|
||||||
if err != nil {
|
|
||||||
common.LogError("error while marshalling, skipping BuildDisableRepos: ", err)
|
|
||||||
} else {
|
|
||||||
templateMeta.BuildFlags.Contents += string(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Cleanup ReleaseTarget and modify affected path entries
|
// Cleanup ReleaseTarget and modify affected path entries
|
||||||
for idx, r := range templateMeta.Repositories {
|
for idx, r := range templateMeta.Repositories {
|
||||||
templateMeta.Repositories[idx].ReleaseTargets = nil
|
templateMeta.Repositories[idx].ReleaseTargets = nil
|
||||||
@@ -483,8 +414,7 @@ func StartOrUpdateBuild(config *common.StagingConfig, git common.Git, gitea comm
|
|||||||
var state RequestModification = RequestModificationSourceChanged
|
var state RequestModification = RequestModificationSourceChanged
|
||||||
if meta == nil {
|
if meta == nil {
|
||||||
// new build
|
// new build
|
||||||
common.LogDebug(" Staging master:", config.StagingProject)
|
meta, err = GenerateObsPrjMeta(git, gitea, pr, obsPrProject, config.ObsProject)
|
||||||
meta, err = GenerateObsPrjMeta(git, gitea, pr, obsPrProject, config.ObsProject, config.StagingProject)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return RequestModificationNoChange, err
|
return RequestModificationNoChange, err
|
||||||
}
|
}
|
||||||
@@ -498,8 +428,6 @@ func StartOrUpdateBuild(config *common.StagingConfig, git common.Git, gitea comm
|
|||||||
} else {
|
} else {
|
||||||
err = ObsClient.SetProjectMeta(meta)
|
err = ObsClient.SetProjectMeta(meta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
x, _ := xml.MarshalIndent(meta, "", " ")
|
|
||||||
common.LogDebug(" meta:", string(x))
|
|
||||||
common.LogError("cannot create meta project:", err)
|
common.LogError("cannot create meta project:", err)
|
||||||
return RequestModificationNoChange, err
|
return RequestModificationNoChange, err
|
||||||
}
|
}
|
||||||
@@ -656,7 +584,7 @@ func CleanupPullNotification(gitea common.Gitea, thread *models.NotificationThre
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !pr.HasMerged && time.Since(time.Time(pr.Closed)) < time.Duration(config.CleanupDelay)*time.Hour {
|
if !pr.HasMerged && time.Since(time.Time(pr.Closed)) < time.Duration(config.CleanupDelay)*time.Hour {
|
||||||
common.LogInfo("Cooldown period for cleanup of", thread.Subject.HTMLURL)
|
common.LogInfo("Cooldown period for cleanup of", thread.URL)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -694,14 +622,6 @@ func CleanupPullNotification(gitea common.Gitea, thread *models.NotificationThre
|
|||||||
return false // cleaned up now, but the cleanup was not aleady done
|
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) {
|
func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, error) {
|
||||||
dir, err := os.MkdirTemp(os.TempDir(), BotName)
|
dir, err := os.MkdirTemp(os.TempDir(), BotName)
|
||||||
common.PanicOnError(err)
|
common.PanicOnError(err)
|
||||||
@@ -764,7 +684,6 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
|
|||||||
stagingConfig, err := common.ParseStagingConfig(data)
|
stagingConfig, err := common.ParseStagingConfig(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError("Error parsing config file", common.StagingConfigFile, err)
|
common.LogError("Error parsing config file", common.StagingConfigFile, err)
|
||||||
return true, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if stagingConfig.ObsProject == "" {
|
if stagingConfig.ObsProject == "" {
|
||||||
@@ -777,7 +696,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
meta, err := ObsClient.GetProjectMeta(stagingConfig.ObsProject)
|
meta, err := ObsClient.GetProjectMeta(stagingConfig.ObsProject)
|
||||||
if err != nil || meta == nil {
|
if err != nil {
|
||||||
common.LogError("Cannot find reference project meta:", stagingConfig.ObsProject, err)
|
common.LogError("Cannot find reference project meta:", stagingConfig.ObsProject, err)
|
||||||
if !IsDryRun {
|
if !IsDryRun {
|
||||||
_, err := gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Cannot fetch reference project meta")
|
_, err := gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Cannot fetch reference project meta")
|
||||||
@@ -838,28 +757,23 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
|
|||||||
common.LogDebug(" # head submodules:", len(headSubmodules))
|
common.LogDebug(" # head submodules:", len(headSubmodules))
|
||||||
common.LogDebug(" # base submodules:", len(baseSubmodules))
|
common.LogDebug(" # base submodules:", len(baseSubmodules))
|
||||||
|
|
||||||
modifiedPackages := make([]string, 0, 16)
|
modifiedOrNew := make([]string, 0, 16)
|
||||||
newPackages := make([]string, 0, 16)
|
|
||||||
if !stagingConfig.RebuildAll {
|
if !stagingConfig.RebuildAll {
|
||||||
for pkg, headOid := range headSubmodules {
|
for pkg, headOid := range headSubmodules {
|
||||||
if baseOid, exists := baseSubmodules[pkg]; !exists || baseOid != headOid {
|
if baseOid, exists := baseSubmodules[pkg]; !exists || baseOid != headOid {
|
||||||
if exists {
|
modifiedOrNew = append(modifiedOrNew, pkg)
|
||||||
modifiedPackages = append(modifiedPackages, pkg)
|
|
||||||
} else {
|
|
||||||
newPackages = append(newPackages, pkg)
|
|
||||||
}
|
|
||||||
common.LogDebug(pkg, ":", baseOid, "->", headOid)
|
common.LogDebug(pkg, ":", baseOid, "->", headOid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(modifiedPackages) == 0 && len(newPackages) == 0 {
|
if len(modifiedOrNew) == 0 {
|
||||||
rebuild_all := false || stagingConfig.RebuildAll
|
rebuild_all := false || stagingConfig.RebuildAll
|
||||||
|
|
||||||
reviews, err := gitea.GetPullRequestReviews(pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Index)
|
reviews, err := gitea.GetPullRequestReviews(pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Index)
|
||||||
common.LogDebug("num reviews:", len(reviews))
|
common.LogDebug("num reviews:", len(reviews))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
rebuild_rx := regexp.MustCompile("^@autogits_obs_staging_bot\\s*:?\\s*(re)?build\\s*all$")
|
rebuild_rx := regexp.MustCompile("^@autogits_obs_staging_bot\\s*:\\s*(re)?build\\s*all$")
|
||||||
done:
|
done:
|
||||||
for _, r := range reviews {
|
for _, r := range reviews {
|
||||||
for _, l := range common.SplitLines(r.Body) {
|
for _, l := range common.SplitLines(r.Body) {
|
||||||
@@ -891,12 +805,7 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
|
|||||||
if !rebuild_all {
|
if !rebuild_all {
|
||||||
common.LogInfo("No package changes detected. Ignoring")
|
common.LogInfo("No package changes detected. Ignoring")
|
||||||
if !IsDryRun {
|
if !IsDryRun {
|
||||||
_, err := gitea.AddReviewComment(pr, common.ReviewStateApproved, "No package changes, not rebuilding project by default, accepting change")
|
_, err = gitea.AddReviewComment(pr, common.ReviewStateComment, "No package changes. Not rebuilding project by default")
|
||||||
if err != nil {
|
|
||||||
common.LogError(err)
|
|
||||||
} else {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
@@ -912,22 +821,6 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
|
|||||||
TargetURL: ObsWebHost + "/project/show/" + stagingProject,
|
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"
|
msg := "Changed source updated for build"
|
||||||
if change == RequestModificationProjectCreated {
|
if change == RequestModificationProjectCreated {
|
||||||
msg = "Build is started in " + ObsWebHost + "/project/show/" +
|
msg = "Build is started in " + ObsWebHost + "/project/show/" +
|
||||||
@@ -936,13 +829,13 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
|
|||||||
if len(stagingConfig.QA) > 0 {
|
if len(stagingConfig.QA) > 0 {
|
||||||
msg = msg + "\nAdditional QA builds: \n"
|
msg = msg + "\nAdditional QA builds: \n"
|
||||||
}
|
}
|
||||||
SetStatus(gitea, org, repo, pr.Head.Sha, status)
|
gitea.SetCommitStatus(pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Head.Sha, status)
|
||||||
|
|
||||||
for _, setup := range stagingConfig.QA {
|
for _, setup := range stagingConfig.QA {
|
||||||
CreateQASubProject(stagingConfig, git, gitea, pr,
|
CreateQASubProject(stagingConfig, git, gitea, pr,
|
||||||
stagingProject,
|
stagingProject,
|
||||||
setup.Origin,
|
setup.Origin,
|
||||||
setup.Name,
|
setup.Name)
|
||||||
setup.BuildDisableRepos)
|
|
||||||
msg = msg + ObsWebHost + "/project/show/" +
|
msg = msg + ObsWebHost + "/project/show/" +
|
||||||
stagingProject + ":" + setup.Name + "\n"
|
stagingProject + ":" + setup.Name + "\n"
|
||||||
}
|
}
|
||||||
@@ -951,44 +844,42 @@ func ProcessPullRequest(gitea common.Gitea, org, repo string, id int64) (bool, e
|
|||||||
gitea.AddComment(pr, msg)
|
gitea.AddComment(pr, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
baseResult, err := ObsClient.LastBuildResults(stagingConfig.ObsProject, modifiedPackages...)
|
baseResult, err := ObsClient.LastBuildResults(stagingConfig.ObsProject, modifiedOrNew...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError("failed fetching ref project status for", stagingConfig.ObsProject, ":", err)
|
common.LogError("failed fetching ref project status for", stagingConfig.ObsProject, ":", err)
|
||||||
}
|
}
|
||||||
stagingResult, err := ObsClient.BuildStatus(stagingProject)
|
stagingResult, err := ObsClient.BuildStatus(stagingProject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError("failed fetching stage project status for", stagingProject, ":", err)
|
common.LogError("failed fetching ref project status for", stagingProject, ":", err)
|
||||||
}
|
}
|
||||||
buildStatus := ProcessBuildStatus(stagingResult, baseResult)
|
buildStatus := ProcessBuildStatus(stagingResult, baseResult)
|
||||||
|
|
||||||
done := false
|
|
||||||
switch buildStatus {
|
switch buildStatus {
|
||||||
case BuildStatusSummarySuccess:
|
case BuildStatusSummarySuccess:
|
||||||
status.Status = common.CommitStatus_Success
|
status.Status = common.CommitStatus_Success
|
||||||
done = true
|
|
||||||
if !IsDryRun {
|
if !IsDryRun {
|
||||||
_, err := gitea.AddReviewComment(pr, common.ReviewStateApproved, "Build successful")
|
_, err := gitea.AddReviewComment(pr, common.ReviewStateApproved, "Build successful")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(err)
|
common.LogError(err)
|
||||||
|
} else {
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case BuildStatusSummaryFailed:
|
case BuildStatusSummaryFailed:
|
||||||
status.Status = common.CommitStatus_Fail
|
status.Status = common.CommitStatus_Fail
|
||||||
done = true
|
|
||||||
if !IsDryRun {
|
if !IsDryRun {
|
||||||
_, err := gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Build failed")
|
_, err := gitea.AddReviewComment(pr, common.ReviewStateRequestChanges, "Build failed")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
common.LogError(err)
|
common.LogError(err)
|
||||||
|
} else {
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
common.LogInfo("Build status:", buildStatus)
|
common.LogInfo("Build status:", buildStatus)
|
||||||
if !IsDryRun {
|
gitea.SetCommitStatus(pr.Base.Repo.Owner.UserName, pr.Base.Repo.Name, pr.Head.Sha, status)
|
||||||
if err = SetStatus(gitea, org, repo, pr.Head.Sha, status); err != nil {
|
|
||||||
return false, err
|
// waiting for build results -- nothing to do
|
||||||
}
|
|
||||||
}
|
|
||||||
return done, nil
|
|
||||||
|
|
||||||
} else if err == NonActionableReviewError || err == NoReviewsFoundError {
|
} else if err == NonActionableReviewError || err == NoReviewsFoundError {
|
||||||
return true, nil
|
return true, nil
|
||||||
@@ -1047,6 +938,7 @@ func PollWorkNotifications(giteaUrl string) {
|
|||||||
|
|
||||||
var ListPullNotificationsOnly bool
|
var ListPullNotificationsOnly bool
|
||||||
var GiteaUrl string
|
var GiteaUrl string
|
||||||
|
var GiteaUseSshClone bool
|
||||||
var ObsWebHost string
|
var ObsWebHost string
|
||||||
var IsDryRun bool
|
var IsDryRun bool
|
||||||
var ProcessPROnly string
|
var ProcessPROnly string
|
||||||
@@ -1070,6 +962,7 @@ func main() {
|
|||||||
ProcessPROnly := flag.String("pr", "", "Process only specific PR and ignore the rest. Use for debugging")
|
ProcessPROnly := flag.String("pr", "", "Process only specific PR and ignore the rest. Use for debugging")
|
||||||
buildRoot := flag.String("build-root", "", "Default build location for staging projects. Default is bot's home project")
|
buildRoot := flag.String("build-root", "", "Default build location for staging projects. Default is bot's home project")
|
||||||
flag.StringVar(&GiteaUrl, "gitea-url", "https://src.opensuse.org", "Gitea instance")
|
flag.StringVar(&GiteaUrl, "gitea-url", "https://src.opensuse.org", "Gitea instance")
|
||||||
|
flag.BoolVar(&GiteaUseSshClone, "use-ssh-clone", false, "enforce cloning via ssh")
|
||||||
obsApiHost := flag.String("obs", "https://api.opensuse.org", "API for OBS instance")
|
obsApiHost := flag.String("obs", "https://api.opensuse.org", "API for OBS instance")
|
||||||
flag.StringVar(&ObsWebHost, "obs-web", "", "Web OBS instance, if not derived from the obs config")
|
flag.StringVar(&ObsWebHost, "obs-web", "", "Web OBS instance, if not derived from the obs config")
|
||||||
flag.BoolVar(&IsDryRun, "dry", false, "Dry-run, don't actually create any build projects or review changes")
|
flag.BoolVar(&IsDryRun, "dry", false, "Dry-run, don't actually create any build projects or review changes")
|
||||||
@@ -1086,7 +979,6 @@ func main() {
|
|||||||
ObsWebHost = ObsWebHostFromApiHost(*obsApiHost)
|
ObsWebHost = ObsWebHostFromApiHost(*obsApiHost)
|
||||||
}
|
}
|
||||||
|
|
||||||
common.LogDebug("OBS Gitea Host:", GiteaUrl)
|
|
||||||
common.LogDebug("OBS Web Host:", ObsWebHost)
|
common.LogDebug("OBS Web Host:", ObsWebHost)
|
||||||
common.LogDebug("OBS API Host:", *obsApiHost)
|
common.LogDebug("OBS API Host:", *obsApiHost)
|
||||||
|
|
||||||
@@ -1104,7 +996,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(*ProcessPROnly) > 0 {
|
if len(*ProcessPROnly) > 0 {
|
||||||
rx := regexp.MustCompile("^([^/#]+)/([^/#]+)#([0-9]+)$")
|
rx := regexp.MustCompile("^(\\w+)/(\\w+)#(\\d+)$")
|
||||||
m := rx.FindStringSubmatch(*ProcessPROnly)
|
m := rx.FindStringSubmatch(*ProcessPROnly)
|
||||||
if m == nil {
|
if m == nil {
|
||||||
common.LogError("Cannot find any PR matches in", *ProcessPROnly)
|
common.LogError("Cannot find any PR matches in", *ProcessPROnly)
|
||||||
|
|||||||
1
obs-status-service/.gitignore
vendored
1
obs-status-service/.gitignore
vendored
@@ -1,2 +1 @@
|
|||||||
obs-status-service
|
obs-status-service
|
||||||
*.svg
|
|
||||||
|
|||||||
@@ -1,60 +1,25 @@
|
|||||||
OBS Status Service
|
OBS Status Service
|
||||||
==================
|
==================
|
||||||
|
|
||||||
Reports build status of OBS service as an easily to produce SVG. Repository
|
Reports build status of OBS service as an easily to produce SVG
|
||||||
results (build results) are cached for 10 seconds and repository listing
|
|
||||||
for OBS instance are cached for 5 minutes -- new repositories take up to
|
|
||||||
5 minutes to be visible.
|
|
||||||
|
|
||||||
Requests for individual build results:
|
Requests for individual build results:
|
||||||
|
/obs:project/package/repo/arch
|
||||||
/status/obs:project/package/repo/arch
|
|
||||||
|
|
||||||
where `repo` and `arch` are optional parameters.
|
|
||||||
|
|
||||||
Requests for project results
|
Requests for project results
|
||||||
|
/obs:project
|
||||||
/status/obs:project
|
|
||||||
|
|
||||||
Get requests for / will also return 404 statu normally. If the Backend redis
|
|
||||||
server is not available, it will return 500
|
|
||||||
|
|
||||||
|
|
||||||
By default, SVG output is generated, suitable for inclusion. But JSON and XML
|
|
||||||
output is possible by setting `Accept:` request header
|
|
||||||
|
|
||||||
| Accept Request Header | Output format
|
|
||||||
|------------------------|---------------------
|
|
||||||
| | SVG image
|
|
||||||
| application/json | JSON data
|
|
||||||
| application/obs+xml | XML output
|
|
||||||
|
|
||||||
|
|
||||||
Areas of Responsibility
|
Areas of Responsibility
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
* Fetch and cache internal data from OBS and present it in usable format:
|
* Monitors RabbitMQ interface for notification of OBS package and project status
|
||||||
+ Generate SVG output for specific OBS project or package
|
* Produces SVG output based on GET request
|
||||||
+ Generate JSON/XML output for automated processing
|
* Cache results (sqlite) and periodically update results from OBS (in case of messages are missing)
|
||||||
* Low-overhead
|
|
||||||
|
|
||||||
|
|
||||||
Target Usage
|
Target Usage
|
||||||
------------
|
------------
|
||||||
|
|
||||||
* inside README.md of package git or project git
|
* README.md of package git or project git
|
||||||
* comment section of a Gitea PR
|
* comment section of a Gitea PR
|
||||||
* automated build result processing
|
|
||||||
|
|
||||||
Running
|
|
||||||
-------
|
|
||||||
|
|
||||||
Default parameters can be changed by env variables
|
|
||||||
|
|
||||||
| Environment variable | Default | Description
|
|
||||||
|---------------------------------|-----------------------------|------------
|
|
||||||
| `OBS_STATUS_SERVICE_OBS_URL` | https://build.opensuse.org | Location for creating build logs and monitor page build results
|
|
||||||
| `OBS_STATUS_SERVICE_LISTEN` | [::1]:8080 | Listening address and port
|
|
||||||
| `OBS_STATUS_SERVICE_CERT` | /run/obs-status-service.pem | Location of certificate file for service
|
|
||||||
| `OBS_STATUS_SERVICE_KEY` | /run/obs-status-service.pem | Location of key file for service
|
|
||||||
| `REDIS` | | OBS's Redis instance URL
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -19,20 +19,11 @@ package main
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"bytes"
|
||||||
"encoding/xml"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"maps"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"src.opensuse.org/autogits/common"
|
"src.opensuse.org/autogits/common"
|
||||||
)
|
)
|
||||||
@@ -42,131 +33,52 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var obs *common.ObsClient
|
var obs *common.ObsClient
|
||||||
|
var debug bool
|
||||||
|
|
||||||
type RepoBuildCounters struct {
|
func LogDebug(v ...any) {
|
||||||
Repository, Arch string
|
if debug {
|
||||||
Status string
|
log.Println(v...)
|
||||||
BuildStatusCounter map[string]int
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProjectStatusSummarySvg(res []*common.BuildResult) []byte {
|
func ProjectStatusSummarySvg(project string) []byte {
|
||||||
if len(res) == 0 {
|
res := GetCurrentStatus(project)
|
||||||
|
if res == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
list := common.BuildResultList{
|
pkgs := res.GetPackageList()
|
||||||
Result: res,
|
|
||||||
}
|
|
||||||
package_names := list.GetPackageList()
|
|
||||||
maxLen := 0
|
maxLen := 0
|
||||||
for _, p := range package_names {
|
for _, p := range pkgs {
|
||||||
maxLen = max(maxLen, len(p))
|
maxLen = max(maxLen, len(p))
|
||||||
}
|
}
|
||||||
|
|
||||||
// width := float32(len(list.Result))*1.5 + float32(maxLen)*0.8
|
width := float32(len(res.Result))*1.5 + float32(maxLen)*0.8
|
||||||
// height := 1.5*float32(maxLen) + 30
|
height := 1.5*float32(maxLen) + 30
|
||||||
ret := NewSvg(SvgType_Project)
|
|
||||||
|
|
||||||
status := make([]RepoBuildCounters, len(res))
|
ret := bytes.Buffer{}
|
||||||
|
ret.WriteString(`<svg version="2.0" width="`)
|
||||||
for i, repo := range res {
|
ret.WriteString(fmt.Sprint(width))
|
||||||
status[i].Arch = repo.Arch
|
ret.WriteString(`em" height="`)
|
||||||
status[i].Repository = repo.Repository
|
ret.WriteString(fmt.Sprint(height))
|
||||||
status[i].Status = repo.Code
|
ret.WriteString(`em" xmlns="http://www.w3.org/2000/svg">`)
|
||||||
status[i].BuildStatusCounter = make(map[string]int)
|
ret.WriteString(`<defs>
|
||||||
|
<g id="f"> <!-- failed -->
|
||||||
for _, pkg := range repo.Status {
|
<rect width="1em" height="1em" fill="#800" />
|
||||||
status[i].BuildStatusCounter[pkg.Code]++
|
</g>
|
||||||
}
|
<g id="s"> <!--succeeded-->
|
||||||
}
|
<rect width="1em" height="1em" fill="#080" />
|
||||||
slices.SortFunc(status, func(a, b RepoBuildCounters) int {
|
</g>
|
||||||
if r := strings.Compare(a.Repository, b.Repository); r != 0 {
|
<g id="buidling"> <!--building-->
|
||||||
return r
|
<rect width="1em" height="1em" fill="#880" />
|
||||||
}
|
</g>
|
||||||
return strings.Compare(a.Arch, b.Arch)
|
</defs>`)
|
||||||
})
|
ret.WriteString(`<use href="#f" x="1em" y="2em"/>`)
|
||||||
repoName := ""
|
ret.WriteString(`</svg>`)
|
||||||
ret.ypos = 3.0
|
return ret.Bytes()
|
||||||
for _, repo := range status {
|
|
||||||
if repo.Repository != repoName {
|
|
||||||
repoName = repo.Repository
|
|
||||||
ret.WriteTitle(repoName)
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.WriteSubtitle(repo.Arch)
|
|
||||||
statuses := slices.Sorted(maps.Keys(repo.BuildStatusCounter))
|
|
||||||
for _, status := range statuses {
|
|
||||||
ret.WriteProjectStatus(res[0].Project, repo.Repository, repo.Arch, status, repo.BuildStatusCounter[status])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret.GenerateSvg()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LinkToBuildlog(R *common.BuildResult, S *common.PackageBuildStatus) string {
|
func PackageStatusSummarySvg(status common.PackageBuildStatus) []byte {
|
||||||
if R != nil && S != nil {
|
|
||||||
switch S.Code {
|
|
||||||
case "succeeded", "failed", "building":
|
|
||||||
return "/buildlog/" + url.PathEscape(R.Project) + "/" + url.PathEscape(S.Package) + "/" + url.PathEscape(R.Repository) + "/" + url.PathEscape(R.Arch)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteExceptPkg(pkg string) func(*common.PackageBuildStatus) bool {
|
|
||||||
return func(item *common.PackageBuildStatus) bool {
|
|
||||||
multibuild_prefix := pkg + ":"
|
|
||||||
return item.Package != pkg && !strings.HasPrefix(item.Package, multibuild_prefix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func PackageStatusSummarySvg(pkg string, res []*common.BuildResult) []byte {
|
|
||||||
// per repo, per arch status bins
|
|
||||||
repo_names := []string{}
|
|
||||||
package_names := []string{}
|
|
||||||
multibuild_prefix := pkg + ":"
|
|
||||||
for _, r := range res {
|
|
||||||
if pos, found := slices.BinarySearchFunc(repo_names, r.Repository, strings.Compare); !found {
|
|
||||||
repo_names = slices.Insert(repo_names, pos, r.Repository)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range r.Status {
|
|
||||||
if p.Package == pkg || strings.HasPrefix(p.Package, multibuild_prefix) {
|
|
||||||
if pos, found := slices.BinarySearchFunc(package_names, p.Package, strings.Compare); !found {
|
|
||||||
package_names = slices.Insert(package_names, pos, p.Package)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ret := NewSvg(SvgType_Package)
|
|
||||||
for _, pkg = range package_names {
|
|
||||||
// if len(package_names) > 1 {
|
|
||||||
ret.WriteTitle(pkg)
|
|
||||||
// }
|
|
||||||
|
|
||||||
for _, name := range repo_names {
|
|
||||||
ret.WriteSubtitle(name)
|
|
||||||
// print all repo arches here and build results
|
|
||||||
for _, r := range res {
|
|
||||||
if r.Repository != name {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range r.Status {
|
|
||||||
if s.Package == pkg {
|
|
||||||
link := LinkToBuildlog(r, s)
|
|
||||||
ret.WritePackageStatus(link, r.Arch, s.Code, s.Details)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret.GenerateSvg()
|
|
||||||
}
|
|
||||||
|
|
||||||
func BuildStatusSvg(repo *common.BuildResult, status *common.PackageBuildStatus) []byte {
|
|
||||||
buildStatus, ok := common.ObsBuildStatusDetails[status.Code]
|
buildStatus, ok := common.ObsBuildStatusDetails[status.Code]
|
||||||
if !ok {
|
if !ok {
|
||||||
buildStatus = common.ObsBuildStatusDetails["error"]
|
buildStatus = common.ObsBuildStatusDetails["error"]
|
||||||
@@ -183,255 +95,72 @@ func BuildStatusSvg(repo *common.BuildResult, status *common.PackageBuildStatus)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildlog := LinkToBuildlog(repo, status)
|
log.Println(status, " -> ", buildStatus)
|
||||||
startTag := ""
|
|
||||||
endTag := ""
|
|
||||||
|
|
||||||
if len(buildlog) > 0 {
|
return []byte(`<svg version="2.0" width="8em" height="1.5em" xmlns="http://www.w3.org/2000/svg">
|
||||||
startTag = "<a href=\"" + buildlog + "\">"
|
<rect width="100%" height="100%" fill="` + fillColor + `"/>
|
||||||
endTag = "</a>"
|
<text x="4em" y="1.1em" text-anchor="middle" fill="` + textColor + `">` + buildStatus.Code + `</text>
|
||||||
}
|
</svg>`)
|
||||||
|
|
||||||
return []byte(`<svg version="2.0" width="8em" height="1.5em" xmlns="http://www.w3.org/2000/svg">` +
|
|
||||||
`<rect width="100%" height="100%" fill="` + fillColor + `"/>` + startTag +
|
|
||||||
`<text x="4em" y="1.1em" text-anchor="middle" fill="` + textColor + `">` + html.EscapeString(buildStatus.Code) + `</text>` + endTag + `</svg>`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteJson(data any, res http.ResponseWriter) {
|
|
||||||
if jsonArray, err := json.MarshalIndent(data, "", " "); err != nil {
|
|
||||||
res.WriteHeader(500)
|
|
||||||
} else {
|
|
||||||
res.Header().Add("size", fmt.Sprint(len(jsonArray)))
|
|
||||||
res.Write(jsonArray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WriteXml(data any, res http.ResponseWriter) {
|
|
||||||
if xmlData, err := xml.MarshalIndent(data, "", " "); err != nil {
|
|
||||||
res.WriteHeader(500)
|
|
||||||
} else {
|
|
||||||
res.Header().Add("size", fmt.Sprint(len(xmlData)))
|
|
||||||
res.Write([]byte("<resultlist>"))
|
|
||||||
res.Write(xmlData)
|
|
||||||
res.Write([]byte("</resultlist>"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var ObsUrl *string
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
obsUrlDef := os.Getenv("OBS_STATUS_SERVICE_OBS_URL")
|
cert := flag.String("cert-file", "", "TLS certificates file")
|
||||||
if len(obsUrlDef) == 0 {
|
key := flag.String("key-file", "", "Private key for the TLS certificate")
|
||||||
obsUrlDef = "https://build.opensuse.org"
|
listen := flag.String("listen", "[::1]:8080", "Listening string")
|
||||||
}
|
|
||||||
listenDef := os.Getenv("OBS_STATUS_SERVICE_LISTEN")
|
|
||||||
if len(listenDef) == 0 {
|
|
||||||
listenDef = "[::1]:8080"
|
|
||||||
}
|
|
||||||
certDef := os.Getenv("OBS_STATUS_SERVICE_CERT")
|
|
||||||
if len(certDef) == 0 {
|
|
||||||
certDef = "/run/obs-status-service.pem"
|
|
||||||
}
|
|
||||||
keyDef := os.Getenv("OBS_STATUS_SERVICE_KEY")
|
|
||||||
if len(keyDef) == 0 {
|
|
||||||
keyDef = certDef
|
|
||||||
}
|
|
||||||
|
|
||||||
cert := flag.String("cert-file", certDef, "TLS certificates file")
|
|
||||||
key := flag.String("key-file", keyDef, "Private key for the TLS certificate")
|
|
||||||
listen := flag.String("listen", listenDef, "Listening string")
|
|
||||||
disableTls := flag.Bool("no-tls", false, "Disable TLS")
|
disableTls := flag.Bool("no-tls", false, "Disable TLS")
|
||||||
ObsUrl = flag.String("obs-url", obsUrlDef, "OBS API endpoint for package buildlog information")
|
obsHost := flag.String("obs-host", "api.opensuse.org", "OBS API endpoint for package status information")
|
||||||
debug := flag.Bool("debug", false, "Enable debug logging")
|
flag.BoolVar(&debug, "debug", false, "Enable debug logging")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *debug {
|
common.PanicOnError(common.RequireObsSecretToken())
|
||||||
common.SetLoggingLevel(common.LogLevelDebug)
|
|
||||||
|
var err error
|
||||||
|
if obs, err = common.NewObsClient(*obsHost); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if redisUrl := os.Getenv("REDIS"); len(redisUrl) > 0 {
|
http.HandleFunc("GET /{Project}", func(res http.ResponseWriter, req *http.Request) {
|
||||||
RedisConnect(redisUrl)
|
res.WriteHeader(http.StatusBadRequest)
|
||||||
} else {
|
|
||||||
common.LogError("REDIS needs to contains URL of the OBS Redis instance with login information")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var rescanRepoError error
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
if rescanRepoError = RescanRepositories(); rescanRepoError != nil {
|
|
||||||
common.LogError("Failed to rescan repositories.", rescanRepoError)
|
|
||||||
}
|
|
||||||
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) {
|
http.HandleFunc("GET /{Project}/{Package}", func(res http.ResponseWriter, req *http.Request) {
|
||||||
mime := ParseMimeHeader(req)
|
/*
|
||||||
obsPrj := req.PathValue("Project")
|
obsPrj := req.PathValue("Project")
|
||||||
common.LogInfo(" GET /status/"+obsPrj, "["+mime.MimeType()+"]")
|
obsPkg := req.PathValue("Package")
|
||||||
|
|
||||||
status := FindAndUpdateProjectResults(obsPrj)
|
status, _ := PackageBuildStatus(obsPrj, obsPkg)
|
||||||
if len(status) == 0 {
|
svg := PackageStatusSummarySvg(status)
|
||||||
res.WriteHeader(404)
|
*/
|
||||||
return
|
|
||||||
}
|
res.Header().Add("content-type", "image/svg+xml")
|
||||||
res.Header().Add("content-type", mime.MimeHeader)
|
//res.Header().Add("size", fmt.Sprint(len(svg)))
|
||||||
if mime.IsSvg() {
|
//res.Write(svg)
|
||||||
svg := ProjectStatusSummarySvg(status)
|
|
||||||
res.Header().Add("size", fmt.Sprint(len(svg)))
|
|
||||||
res.Write(svg)
|
|
||||||
} else if mime.IsJson() {
|
|
||||||
WriteJson(status, res)
|
|
||||||
} else if mime.IsXml() {
|
|
||||||
WriteXml(status, res)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
http.HandleFunc("GET /status/{Project}/{Package}", func(res http.ResponseWriter, req *http.Request) {
|
http.HandleFunc("GET /{Project}/{Package}/{Repository}/{Arch}", func(res http.ResponseWriter, req *http.Request) {
|
||||||
mime := ParseMimeHeader(req)
|
|
||||||
obsPrj := req.PathValue("Project")
|
|
||||||
obsPkg := req.PathValue("Package")
|
|
||||||
common.LogInfo(" GET /status/"+obsPrj+"/"+obsPkg, "["+mime.MimeType()+"]")
|
|
||||||
|
|
||||||
status := slices.Clone(FindAndUpdateProjectResults(obsPrj))
|
|
||||||
for i, s := range status {
|
|
||||||
f := *s
|
|
||||||
f.Status = slices.DeleteFunc(slices.Clone(s.Status), DeleteExceptPkg(obsPkg))
|
|
||||||
status[i] = &f
|
|
||||||
}
|
|
||||||
if len(status) == 0 {
|
|
||||||
res.WriteHeader(404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Header().Add("content-type", mime.MimeHeader)
|
|
||||||
if mime.IsSvg() {
|
|
||||||
svg := PackageStatusSummarySvg(obsPkg, status)
|
|
||||||
|
|
||||||
res.Header().Add("size", fmt.Sprint(len(svg)))
|
|
||||||
res.Write(svg)
|
|
||||||
} else if mime.IsJson() {
|
|
||||||
WriteJson(status, res)
|
|
||||||
} else if mime.IsXml() {
|
|
||||||
WriteXml(status, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
http.HandleFunc("GET /status/{Project}/{Package}/{Repository}", func(res http.ResponseWriter, req *http.Request) {
|
|
||||||
mime := ParseMimeHeader(req)
|
|
||||||
obsPrj := req.PathValue("Project")
|
|
||||||
obsPkg := req.PathValue("Package")
|
|
||||||
repo := req.PathValue("Repository")
|
|
||||||
common.LogInfo(" GET /status/"+obsPrj+"/"+obsPkg, "["+mime.MimeType()+"]")
|
|
||||||
|
|
||||||
status := slices.Clone(FindAndUpdateRepoResults(obsPrj, repo))
|
|
||||||
for i, s := range status {
|
|
||||||
f := *s
|
|
||||||
f.Status = slices.DeleteFunc(slices.Clone(s.Status), DeleteExceptPkg(obsPkg))
|
|
||||||
status[i] = &f
|
|
||||||
}
|
|
||||||
if len(status) == 0 {
|
|
||||||
res.WriteHeader(404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if mime.IsSvg() {
|
|
||||||
svg := PackageStatusSummarySvg(obsPkg, status)
|
|
||||||
res.Header().Add("content-type", mime.MimeHeader)
|
|
||||||
res.Header().Add("size", fmt.Sprint(len(svg)))
|
|
||||||
res.Write(svg)
|
|
||||||
} else if mime.IsJson() {
|
|
||||||
WriteJson(status, res)
|
|
||||||
} else if mime.IsXml() {
|
|
||||||
WriteXml(status, res)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
http.HandleFunc("GET /status/{Project}/{Package}/{Repository}/{Arch}", func(res http.ResponseWriter, req *http.Request) {
|
|
||||||
mime := ParseMimeHeader(req)
|
|
||||||
prj := req.PathValue("Project")
|
prj := req.PathValue("Project")
|
||||||
pkg := req.PathValue("Package")
|
pkg := req.PathValue("Package")
|
||||||
repo := req.PathValue("Repository")
|
repo := req.PathValue("Repository")
|
||||||
arch := req.PathValue("Arch")
|
arch := req.PathValue("Arch")
|
||||||
common.LogInfo(" GET /status/"+prj+"/"+pkg+"/"+repo+"/"+arch, "["+mime.MimeType()+"]")
|
|
||||||
|
|
||||||
res.Header().Add("content-type", mime.MimeHeader)
|
res.Header().Add("content-type", "image/svg+xml")
|
||||||
for _, r := range FindAndUpdateRepoResults(prj, repo) {
|
|
||||||
if r.Arch == arch {
|
prjStatus := GetCurrentStatus(prj)
|
||||||
if idx, found := slices.BinarySearchFunc(r.Status, &common.PackageBuildStatus{Package: pkg}, common.PackageBuildStatusComp); found {
|
if prjStatus == nil {
|
||||||
status := r.Status[idx]
|
return
|
||||||
if mime.IsSvg() {
|
}
|
||||||
res.Write(BuildStatusSvg(r, status))
|
|
||||||
} else if mime.IsJson() {
|
for _, r := range prjStatus.Result {
|
||||||
WriteJson(status, res)
|
if r.Arch == arch && r.Repository == repo {
|
||||||
} else if mime.IsXml() {
|
for _, status := range r.Status {
|
||||||
WriteXml(status, res)
|
if status.Package == pkg {
|
||||||
|
res.Write(PackageStatusSummarySvg(status))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if mime.IsSvg() {
|
|
||||||
res.Write(BuildStatusSvg(nil, &common.PackageBuildStatus{Package: pkg, Code: "unknown"}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
http.HandleFunc("GET /search", func(res http.ResponseWriter, req *http.Request) {
|
|
||||||
common.LogInfo("GET /search?" + 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) {
|
go ProcessUpdates()
|
||||||
prj := req.PathValue("Project")
|
|
||||||
pkg := req.PathValue("Package")
|
|
||||||
repo := req.PathValue("Repository")
|
|
||||||
arch := req.PathValue("Arch")
|
|
||||||
|
|
||||||
res.Header().Add("location", *ObsUrl+"/package/live_build_log/"+url.PathEscape(prj)+"/"+url.PathEscape(pkg)+"/"+url.PathEscape(repo)+"/"+url.PathEscape(arch))
|
|
||||||
res.WriteHeader(307)
|
|
||||||
return
|
|
||||||
|
|
||||||
// status := GetDetailedBuildStatus(prj, pkg, repo, arch)
|
|
||||||
data, err := obs.BuildLog(prj, pkg, repo, arch)
|
|
||||||
if err != nil {
|
|
||||||
res.WriteHeader(http.StatusInternalServerError)
|
|
||||||
common.LogError("Failed to fetch build log for:", prj, pkg, repo, arch, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer data.Close()
|
|
||||||
|
|
||||||
io.Copy(res, data)
|
|
||||||
})
|
|
||||||
|
|
||||||
if *disableTls {
|
if *disableTls {
|
||||||
log.Fatal(http.ListenAndServe(*listen, nil))
|
log.Fatal(http.ListenAndServe(*listen, nil))
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"compress/bzip2"
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"src.opensuse.org/autogits/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestStatusSvg(t *testing.T) {
|
|
||||||
ObsUrl = &[]string{"http://nothing.is.here"}[0]
|
|
||||||
os.WriteFile("teststatus.svg", BuildStatusSvg(nil, &common.PackageBuildStatus{
|
|
||||||
Package: "foo",
|
|
||||||
Code: "succeeded",
|
|
||||||
Details: "more success here",
|
|
||||||
}), 0o777)
|
|
||||||
|
|
||||||
data := []*common.BuildResult{
|
|
||||||
{
|
|
||||||
Project: "project:foo",
|
|
||||||
Repository: "repo1",
|
|
||||||
Arch: "x86_64",
|
|
||||||
Status: []*common.PackageBuildStatus{
|
|
||||||
{
|
|
||||||
Package: "pkg1",
|
|
||||||
Code: "succeeded",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Package: "pkg2",
|
|
||||||
Code: "failed",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Project: "project:foo",
|
|
||||||
Repository: "repo1",
|
|
||||||
Arch: "s390x",
|
|
||||||
Status: []*common.PackageBuildStatus{
|
|
||||||
{
|
|
||||||
Package: "pkg1",
|
|
||||||
Code: "succeeded",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Package: "pkg2",
|
|
||||||
Code: "unresolveable",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Project: "project:foo",
|
|
||||||
Repository: "repo1",
|
|
||||||
Arch: "i586",
|
|
||||||
Status: []*common.PackageBuildStatus{
|
|
||||||
{
|
|
||||||
Package: "pkg1",
|
|
||||||
Code: "succeeded",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Package: "pkg2",
|
|
||||||
Code: "blocked",
|
|
||||||
Details: "foo bar is why",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Project: "project:foo",
|
|
||||||
Repository: "TW",
|
|
||||||
Arch: "s390",
|
|
||||||
Status: []*common.PackageBuildStatus{
|
|
||||||
{
|
|
||||||
Package: "pkg1",
|
|
||||||
Code: "excluded",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Package: "pkg2",
|
|
||||||
Code: "failed",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
os.WriteFile("testpackage.svg", PackageStatusSummarySvg("pkg2", data), 0o777)
|
|
||||||
os.WriteFile("testproject.svg", ProjectStatusSummarySvg(data), 0o777)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFactoryResults(t *testing.T) {
|
|
||||||
data, err := os.Open("factory.results.json.bz2")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Openning factory.results.json.bz2 failed:", err)
|
|
||||||
}
|
|
||||||
UncompressedData, err := io.ReadAll(bzip2.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Reading factory.results.json.bz2 failed:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var results []*common.BuildResult
|
|
||||||
if err := json.Unmarshal(UncompressedData, &results); err != nil {
|
|
||||||
t.Fatal("Failed parsing test data", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// add tests here
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
}{
|
|
||||||
// add test data here
|
|
||||||
{
|
|
||||||
name: "First test",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
// and test code here
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MimeHeader struct {
|
|
||||||
MimeHeader string
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
JsonMime = "application/json"
|
|
||||||
XmlMime = "application/obs+xml"
|
|
||||||
SvgMime = "image/svg+xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
var AcceptedStatusMimes []string = []string{
|
|
||||||
SvgMime,
|
|
||||||
JsonMime,
|
|
||||||
XmlMime,
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseMimeHeader(req *http.Request) *MimeHeader {
|
|
||||||
proposedMimes := req.Header.Values("Accept")
|
|
||||||
mime := MimeHeader{MimeHeader: SvgMime}
|
|
||||||
if len(proposedMimes) == 0 {
|
|
||||||
return &mime
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, m := range proposedMimes {
|
|
||||||
for _, am := range AcceptedStatusMimes {
|
|
||||||
if strings.Contains(m, am) {
|
|
||||||
mime.MimeHeader = am
|
|
||||||
return &mime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &mime
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MimeHeader) IsJson() bool {
|
|
||||||
return m.MimeHeader == JsonMime
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MimeHeader) IsXml() bool {
|
|
||||||
return m.MimeHeader == XmlMime
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MimeHeader) IsSvg() bool {
|
|
||||||
return m.MimeHeader == SvgMime
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MimeHeader) MimeType() string {
|
|
||||||
if m.IsJson() {
|
|
||||||
return JsonMime
|
|
||||||
} else if m.IsXml() {
|
|
||||||
return XmlMime
|
|
||||||
}
|
|
||||||
|
|
||||||
return SvgMime // default
|
|
||||||
}
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
"src.opensuse.org/autogits/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
var RepoStatus []*common.BuildResult = []*common.BuildResult{}
|
|
||||||
var RepoStatusLock *sync.RWMutex = &sync.RWMutex{}
|
|
||||||
|
|
||||||
var redisClient *redis.Client
|
|
||||||
|
|
||||||
func RedisConnect(RedisUrl string) {
|
|
||||||
opts, err := redis.ParseURL(RedisUrl)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
redisClient = redis.NewClient(opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
data, err := redisClient.HGetAll(context.Background(), key).Result()
|
|
||||||
if err != nil {
|
|
||||||
common.LogError("Failed fetching build results for", key, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
reset_time := time.Date(1000, 1, 1, 1, 1, 1, 1, time.Local)
|
|
||||||
for _, pkg := range r.Status {
|
|
||||||
pkg.LastUpdate = reset_time
|
|
||||||
}
|
|
||||||
r.LastUpdate = time.Now()
|
|
||||||
for pkg, result := range data {
|
|
||||||
if strings.HasPrefix(result, "scheduled") {
|
|
||||||
// TODO: lookup where's building
|
|
||||||
result = "building"
|
|
||||||
}
|
|
||||||
|
|
||||||
var idx int
|
|
||||||
var found bool
|
|
||||||
var code string
|
|
||||||
var details string
|
|
||||||
|
|
||||||
if pos := strings.IndexByte(result, ':'); pos > -1 && pos < len(result) {
|
|
||||||
code = result[0:pos]
|
|
||||||
details = result[pos+1:]
|
|
||||||
} else {
|
|
||||||
code = result
|
|
||||||
details = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if idx, found = slices.BinarySearchFunc(r.Status, &common.PackageBuildStatus{Package: pkg}, common.PackageBuildStatusComp); found {
|
|
||||||
res := r.Status[idx]
|
|
||||||
res.LastUpdate = r.LastUpdate
|
|
||||||
res.Code = code
|
|
||||||
res.Details = details
|
|
||||||
} else {
|
|
||||||
r.Status = slices.Insert(r.Status, idx, &common.PackageBuildStatus{
|
|
||||||
Package: pkg,
|
|
||||||
Code: code,
|
|
||||||
Details: details,
|
|
||||||
LastUpdate: r.LastUpdate,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for idx := 0; idx < len(r.Status); {
|
|
||||||
if r.Status[idx].LastUpdate == reset_time {
|
|
||||||
r.Status = slices.Delete(r.Status, idx, idx+1)
|
|
||||||
} else {
|
|
||||||
idx++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func FindProjectResults(project string) []*common.BuildResult {
|
|
||||||
RepoStatusLock.RLock()
|
|
||||||
defer RepoStatusLock.RUnlock()
|
|
||||||
|
|
||||||
return FindProjectResultsNoLock(project)
|
|
||||||
}
|
|
||||||
|
|
||||||
func FindProjectResultsNoLock(project string) []*common.BuildResult {
|
|
||||||
ret := make([]*common.BuildResult, 0, 8)
|
|
||||||
idx, _ := slices.BinarySearchFunc(RepoStatus, &common.BuildResult{Project: project}, common.BuildResultComp)
|
|
||||||
for idx < len(RepoStatus) && RepoStatus[idx].Project == project {
|
|
||||||
ret = append(ret, RepoStatus[idx])
|
|
||||||
idx++
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func FindRepoResults(project, repo string) []*common.BuildResult {
|
|
||||||
RepoStatusLock.RLock()
|
|
||||||
defer RepoStatusLock.RUnlock()
|
|
||||||
|
|
||||||
return FindRepoResultsNoLock(project, repo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func FindRepoResultsNoLock(project, repo string) []*common.BuildResult {
|
|
||||||
ret := make([]*common.BuildResult, 0, 8)
|
|
||||||
idx, _ := slices.BinarySearchFunc(RepoStatus, &common.BuildResult{Project: project, Repository: repo}, common.BuildResultComp)
|
|
||||||
for idx < len(RepoStatus) && RepoStatus[idx].Project == project && RepoStatus[idx].Repository == repo {
|
|
||||||
ret = append(ret, RepoStatus[idx])
|
|
||||||
idx++
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func FindPackages(pkg string) []string {
|
|
||||||
RepoStatusLock.RLock()
|
|
||||||
defer RepoStatusLock.RUnlock()
|
|
||||||
|
|
||||||
return FindPackagesNoLock(pkg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func FindPackagesNoLock(pkg string) []string {
|
|
||||||
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{}
|
|
||||||
now := time.Now()
|
|
||||||
for _, r := range res {
|
|
||||||
if now.Sub(r.LastUpdate).Abs() < time.Second*10 {
|
|
||||||
// 1 update per 10 second for now
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
UpdateResults(r)
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func FindAndUpdateRepoResults(project, repo string) []*common.BuildResult {
|
|
||||||
res := FindRepoResults(project, repo)
|
|
||||||
wg := &sync.WaitGroup{}
|
|
||||||
now := time.Now()
|
|
||||||
for _, r := range res {
|
|
||||||
if now.Sub(r.LastUpdate).Abs() < time.Second*10 {
|
|
||||||
// 1 update per 10 second for now
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
UpdateResults(r)
|
|
||||||
wg.Done()
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func RescanRepositories() error {
|
|
||||||
ctx := context.Background()
|
|
||||||
var cursor uint64
|
|
||||||
var err error
|
|
||||||
|
|
||||||
common.LogDebug("** starting rescanning ...")
|
|
||||||
RepoStatusLock.Lock()
|
|
||||||
for _, repo := range RepoStatus {
|
|
||||||
repo.Dirty = false
|
|
||||||
}
|
|
||||||
RepoStatusLock.Unlock()
|
|
||||||
var count int
|
|
||||||
|
|
||||||
projectsLooked := make([]string, 0, 10000)
|
|
||||||
|
|
||||||
for {
|
|
||||||
var data []string
|
|
||||||
data, cursor, err = redisClient.ScanType(ctx, cursor, "", 1000, "hash").Result()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
wg := &sync.WaitGroup{}
|
|
||||||
RepoStatusLock.Lock()
|
|
||||||
for _, repo := range data {
|
|
||||||
r := strings.Split(repo, "/")
|
|
||||||
if len(r) != 3 || len(r[0]) < 8 || r[0][0:7] != "result." {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
d := &common.BuildResult{
|
|
||||||
Project: r[0][7:],
|
|
||||||
Repository: r[1],
|
|
||||||
Arch: r[2],
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
common.LogDebug(" added a total", count, "repos")
|
|
||||||
count = 0
|
|
||||||
|
|
||||||
RepoStatusLock.Lock()
|
|
||||||
for i := 0; i < len(RepoStatus); {
|
|
||||||
if !RepoStatus[i].Dirty {
|
|
||||||
RepoStatus = slices.Delete(RepoStatus, i, i+1)
|
|
||||||
count++
|
|
||||||
} else {
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RepoStatusLock.Unlock()
|
|
||||||
common.LogDebug(" removed", count, "repos")
|
|
||||||
common.LogDebug(" total repos:", len(RepoStatus))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
82
obs-status-service/status.go
Normal file
82
obs-status-service/status.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"src.opensuse.org/autogits/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
var WatchedRepos []string
|
||||||
|
var mutex sync.Mutex
|
||||||
|
|
||||||
|
var StatusUpdateCh chan StatusUpdateMsg = make(chan StatusUpdateMsg)
|
||||||
|
|
||||||
|
var statusMutex sync.RWMutex
|
||||||
|
var CurrentStatus map[string]*common.BuildResultList = make(map[string]*common.BuildResultList)
|
||||||
|
|
||||||
|
type StatusUpdateMsg struct {
|
||||||
|
ObsProject string
|
||||||
|
Result *common.BuildResultList
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCurrentStatus(project string) *common.BuildResultList {
|
||||||
|
statusMutex.RLock()
|
||||||
|
defer statusMutex.RUnlock()
|
||||||
|
|
||||||
|
if ret, found := CurrentStatus[project]; found {
|
||||||
|
return ret
|
||||||
|
} else {
|
||||||
|
go WatchObsProject(obs, project)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProcessUpdates() {
|
||||||
|
for {
|
||||||
|
msg := <-StatusUpdateCh
|
||||||
|
|
||||||
|
statusMutex.Lock()
|
||||||
|
CurrentStatus[msg.ObsProject] = msg.Result
|
||||||
|
|
||||||
|
drainedChannel:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg = <-StatusUpdateCh:
|
||||||
|
CurrentStatus[msg.ObsProject] = msg.Result
|
||||||
|
default:
|
||||||
|
statusMutex.Unlock()
|
||||||
|
break drainedChannel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WatchObsProject(obs common.ObsStatusFetcherWithState, ObsProject string) {
|
||||||
|
old_state := ""
|
||||||
|
|
||||||
|
mutex.Lock()
|
||||||
|
if pos, found := slices.BinarySearch(WatchedRepos, ObsProject); found {
|
||||||
|
mutex.Unlock()
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
WatchedRepos = slices.Insert(WatchedRepos, pos, ObsProject)
|
||||||
|
mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
LogDebug("+ watching", ObsProject)
|
||||||
|
opts := common.BuildResultOptions{}
|
||||||
|
for {
|
||||||
|
state, err := obs.BuildStatusWithState(ObsProject, &opts)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(" *** Error fetching build for", ObsProject, err)
|
||||||
|
time.Sleep(time.Minute)
|
||||||
|
} else {
|
||||||
|
opts.OldState = state.State
|
||||||
|
LogDebug(" --> update", ObsProject, " => ", old_state)
|
||||||
|
StatusUpdateCh <- StatusUpdateMsg{ObsProject: ObsProject, Result: state}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
obs-status-service/status_test.go
Normal file
34
obs-status-service/status_test.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"go.uber.org/mock/gomock"
|
||||||
|
"src.opensuse.org/autogits/common"
|
||||||
|
mock_common "src.opensuse.org/autogits/common/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWatchObsProject(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
res common.BuildResultList
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "two requests",
|
||||||
|
res: common.BuildResultList{
|
||||||
|
State: "success",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
ctl := gomock.NewController(t)
|
||||||
|
obs := mock_common.NewMockObsStatusFetcherWithState(ctl)
|
||||||
|
|
||||||
|
obs.EXPECT().BuildStatusWithState("test:foo", "").Return(&test.res, nil)
|
||||||
|
|
||||||
|
WatchObsProject(obs, "test:foo")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"html"
|
|
||||||
"net/url"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SvgWriter struct {
|
|
||||||
ypos float64
|
|
||||||
header []byte
|
|
||||||
out bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
SvgType_Package = iota
|
|
||||||
SvgType_Project
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewSvg(SvgType int) *SvgWriter {
|
|
||||||
svg := &SvgWriter{}
|
|
||||||
svg.header = []byte(`<svg version="2.0" overflow="auto" width="40ex" height="`)
|
|
||||||
svg.out.WriteString(`em" xmlns="http://www.w3.org/2000/svg">`)
|
|
||||||
switch SvgType {
|
|
||||||
case SvgType_Package:
|
|
||||||
svg.out.WriteString(`<defs>
|
|
||||||
<g id="s">
|
|
||||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="green" fill="#efe" rx="5" />
|
|
||||||
<text x="2.5ex" y="1.1em">succeeded</text>
|
|
||||||
</g>
|
|
||||||
<g id="f">
|
|
||||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="red" fill="#fee" rx="5" />
|
|
||||||
<text x="5ex" y="1.1em">failed</text>
|
|
||||||
</g>
|
|
||||||
<g id="b">
|
|
||||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="grey" fill="#fbf" rx="5" />
|
|
||||||
<text x="3.75ex" y="1.1em">blocked</text>
|
|
||||||
</g>
|
|
||||||
<g id="broken">
|
|
||||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="grey" fill="#fff" rx="5" />
|
|
||||||
<text x="4.5ex" y="1.1em" stroke="red" fill="red">broken</text>
|
|
||||||
</g>
|
|
||||||
<g id="build">
|
|
||||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="yellow" fill="#664" rx="5" />
|
|
||||||
<text x="3.75ex" y="1.1em" fill="yellow">building</text>
|
|
||||||
</g>
|
|
||||||
<g id="u">
|
|
||||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="yellow" fill="#555" rx="5" />
|
|
||||||
<text x="2ex" y="1.1em" fill="orange">unresolvable</text>
|
|
||||||
</g>
|
|
||||||
<g id="scheduled">
|
|
||||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="blue" fill="none" rx="5" />
|
|
||||||
<text x="3ex" y="1.1em" stroke="none" fill="blue">scheduled</text>
|
|
||||||
</g>
|
|
||||||
<g id="d">
|
|
||||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="grey" fill="none" rx="5" />
|
|
||||||
<text x="4ex" y="1.1em" stroke="none" fill="grey">disabled</text>
|
|
||||||
</g>
|
|
||||||
<g id="e">
|
|
||||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="grey" fill="none" rx="5" />
|
|
||||||
<text x="4ex" y="1.1em" stroke="none" fill="#aaf">excluded</text>
|
|
||||||
</g>
|
|
||||||
<g id="un">
|
|
||||||
<rect width="15ex" height="1.5em" stroke-width="1" stroke="grey" fill="none" rx="5" />
|
|
||||||
<text x="4ex" y="1.1em" stroke="none" fill="grey">unknown</text>
|
|
||||||
</g>
|
|
||||||
<rect id="repotitle" width="100%" height="2em" stroke-width="1" stroke="grey" fill="grey" rx="2" />
|
|
||||||
</defs>`)
|
|
||||||
|
|
||||||
case SvgType_Project:
|
|
||||||
svg.out.WriteString(`<defs>
|
|
||||||
</defs>`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return svg
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svg *SvgWriter) WriteTitle(title string) {
|
|
||||||
svg.out.WriteString(`<text stroke="black" fill="black" x="1ex" y="` + fmt.Sprint(svg.ypos-.5) + `em">` + html.EscapeString(title) + "</text>")
|
|
||||||
svg.ypos += 2.5
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svg *SvgWriter) WriteSubtitle(subtitle string) {
|
|
||||||
svg.out.WriteString(`<use href="#repotitle" y="` + fmt.Sprint(svg.ypos-2) + `em"/>`)
|
|
||||||
svg.out.WriteString(`<text stroke="black" fill="black" x="3ex" y="` + fmt.Sprint(svg.ypos-.6) + `em">` + html.EscapeString(subtitle) + `</text>`)
|
|
||||||
svg.ypos += 2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svg *SvgWriter) WritePackageStatus(loglink, arch, status, detail string) {
|
|
||||||
StatusToSVG := func(S string) string {
|
|
||||||
switch S {
|
|
||||||
case "succeeded":
|
|
||||||
return "s"
|
|
||||||
case "failed":
|
|
||||||
return "f"
|
|
||||||
case "broken", "scheduled":
|
|
||||||
return S
|
|
||||||
case "blocked":
|
|
||||||
return "b"
|
|
||||||
case "building":
|
|
||||||
return "build"
|
|
||||||
case "unresolvable":
|
|
||||||
return "u"
|
|
||||||
case "disabled":
|
|
||||||
return "d"
|
|
||||||
case "excluded":
|
|
||||||
return "e"
|
|
||||||
}
|
|
||||||
return "un"
|
|
||||||
}
|
|
||||||
|
|
||||||
svg.out.WriteString(`<text fill="#113" x="5ex" y="` + fmt.Sprint(svg.ypos-.6) + `em">` + html.EscapeString(arch) + `</text>`)
|
|
||||||
svg.out.WriteString(`<g>`)
|
|
||||||
if len(loglink) > 0 {
|
|
||||||
u, err := url.Parse(loglink)
|
|
||||||
if err == nil {
|
|
||||||
svg.out.WriteString(`<a href="` + u.String() + `" target="_blank" rel="noopener">`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
svg.out.WriteString(`<use href="#` + StatusToSVG(status) + `" x="20ex" y="` + fmt.Sprint(svg.ypos-1.7) + `em"/>`)
|
|
||||||
if len(loglink) > 0 {
|
|
||||||
svg.out.WriteString(`</a>`)
|
|
||||||
}
|
|
||||||
if len(detail) > 0 {
|
|
||||||
svg.out.WriteString(`<title>` + html.EscapeString(detail) + "</title>")
|
|
||||||
}
|
|
||||||
|
|
||||||
svg.out.WriteString("</g>\n")
|
|
||||||
svg.ypos += 2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svg *SvgWriter) WriteProjectStatus(project, repo, arch, status string, count int) {
|
|
||||||
u, err := url.Parse(*ObsUrl + "/project/monitor/" + url.PathEscape(project) + "?defaults=0&" + url.QueryEscape(status) + "=1&arch_" + url.QueryEscape(arch) + "=1&repo_" + url.QueryEscape(strings.ReplaceAll(repo, ".", "_")) + "=1")
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
svg.out.WriteString(`<g><a href="` + u.String() + `" target="_blank" rel="noopener">` + "\n" +
|
|
||||||
`<text fill="#113" x="5ex" y="` + fmt.Sprint(svg.ypos-0.6) + "em\">\n" +
|
|
||||||
html.EscapeString(status+": ") + fmt.Sprint(count) + "</text></a></g>\n")
|
|
||||||
svg.ypos += 2
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svg *SvgWriter) GenerateSvg() []byte {
|
|
||||||
return slices.Concat(svg.header, []byte(fmt.Sprint(svg.ypos)), svg.out.Bytes(), []byte("</svg>"))
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
Reparent Bot
|
|
||||||
============
|
|
||||||
|
|
||||||
To be able to put new parents of repositories as special forks into
|
|
||||||
pool and other projects.
|
|
||||||
|
|
||||||
|
|
||||||
Areas of Responsibilities
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
* monitor issues for Add packages
|
|
||||||
+ issue creator *must be* owner of the repo, OR
|
|
||||||
+ repository must not be a fork
|
|
||||||
* assign organization Owner to review request
|
|
||||||
* reparent the repository and create a PR
|
|
||||||
* remove non-accepted repositories from /pool, if no other
|
|
||||||
branches are relevant here
|
|
||||||
|
|
||||||
Target Usage
|
|
||||||
------------
|
|
||||||
|
|
||||||
* devel and released products
|
|
||||||
|
|
||||||
|
|
||||||
@@ -5,7 +5,7 @@ After=network-online.target
|
|||||||
[Service]
|
[Service]
|
||||||
Type=exec
|
Type=exec
|
||||||
ExecStart=/usr/bin/gitea-events-rabbitmq-publisher
|
ExecStart=/usr/bin/gitea-events-rabbitmq-publisher
|
||||||
EnvironmentFile=-/etc/default/gitea-events-rabbitmq-publisher.env
|
EnvironmentFile=-/etc/sysconfig/gitea-events-rabbitmq-publisher.env
|
||||||
DynamicUser=yes
|
DynamicUser=yes
|
||||||
NoNewPrivileges=yes
|
NoNewPrivileges=yes
|
||||||
ProtectSystem=strict
|
ProtectSystem=strict
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Group Review bot for %i
|
|
||||||
After=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=exec
|
|
||||||
ExecStart=/usr/bin/group-review %i
|
|
||||||
EnvironmentFile=-/etc/default/group-review/%i.env
|
|
||||||
DynamicUser=yes
|
|
||||||
NoNewPrivileges=yes
|
|
||||||
ProtectSystem=strict
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
[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/default/obs-staging-bot.env
|
|
||||||
DynamicUser=yes
|
|
||||||
NoNewPrivileges=yes
|
|
||||||
ProtectSystem=strict
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=OBS build status as SVG service
|
|
||||||
After=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=exec
|
|
||||||
Restart=on-failure
|
|
||||||
ExecStart=/usr/bin/obs-status-service
|
|
||||||
EnvironmentFile=-/etc/default/obs-status-service.env
|
|
||||||
DynamicUser=yes
|
|
||||||
ProtectSystem=strict
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=WorkflowDirect git bot for %i
|
|
||||||
After=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=exec
|
|
||||||
ExecStart=/usr/bin/workflow-direct
|
|
||||||
EnvironmentFile=-/etc/default/%i/workflow-direct.env
|
|
||||||
DynamicUser=yes
|
|
||||||
NoNewPrivileges=yes
|
|
||||||
ProtectSystem=strict
|
|
||||||
RuntimeDirectory=%i
|
|
||||||
# SLES 15 doesn't have HOME set for dynamic users, so we improvise
|
|
||||||
BindReadOnlyPaths=/etc/default/%i/known_hosts:/etc/ssh/ssh_known_hosts /etc/default/%i/config.json:%t/%i/config.json /etc/default/%i/id_ed25519 /etc/default/%i/id_ed25519.pub
|
|
||||||
WorkingDirectory=%t/%i
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
// exists only to import this for go.modules
|
|
||||||
import "go.uber.org/mock/mockgen/model"
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BIN
vendor.tar.zst
LFS
Normal file
BIN
vendor.tar.zst
LFS
Normal file
Binary file not shown.
15
vendor/github.com/asaskevich/govalidator/.gitignore
generated
vendored
15
vendor/github.com/asaskevich/govalidator/.gitignore
generated
vendored
@@ -1,15 +0,0 @@
|
|||||||
bin/
|
|
||||||
.idea/
|
|
||||||
# Binaries for programs and plugins
|
|
||||||
*.exe
|
|
||||||
*.exe~
|
|
||||||
*.dll
|
|
||||||
*.so
|
|
||||||
*.dylib
|
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
|
||||||
*.test
|
|
||||||
|
|
||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
|
||||||
*.out
|
|
||||||
|
|
||||||
12
vendor/github.com/asaskevich/govalidator/.travis.yml
generated
vendored
12
vendor/github.com/asaskevich/govalidator/.travis.yml
generated
vendored
@@ -1,12 +0,0 @@
|
|||||||
language: go
|
|
||||||
dist: xenial
|
|
||||||
go:
|
|
||||||
- '1.10'
|
|
||||||
- '1.11'
|
|
||||||
- '1.12'
|
|
||||||
- '1.13'
|
|
||||||
- 'tip'
|
|
||||||
|
|
||||||
script:
|
|
||||||
- go test -coverpkg=./... -coverprofile=coverage.info -timeout=5s
|
|
||||||
- bash <(curl -s https://codecov.io/bash)
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user