Compare commits
341 Commits
Author | SHA256 | Date | |
---|---|---|---|
3d8671a7fe | |||
c5db1c83a7 | |||
9f0909621b | |||
b3914b04bd | |||
b43a19189e | |||
01b665230e | |||
1a07d4c541 | |||
22e44dff47 | |||
f9021d08b9 | |||
7a0394e51b | |||
518bc15696 | |||
51873eb048 | |||
4f33ce979c | |||
7cc4db2283 | |||
4d9e2f8cab | |||
ed4f27a19e | |||
e438b5b064 | |||
885bb7e537 | |||
977d75f6e9 | |||
42a9ee48e0 | |||
9333e5c3da | |||
5e29c88dc8 | |||
4f0f101620 | |||
253f009da3 | |||
5e66a14fa9 | |||
e79122e494 | |||
0b4b1a4e21 | |||
0019546e30 | |||
6438a8625a | |||
3928fa6429 | |||
e92ac4a592 | |||
a1520ebfb0 | |||
c8d65a3ae5 | |||
b849a72f31 | |||
568a2f3df8 | |||
30c8b2fe57 | |||
69b0f9a5ed | |||
a283d4f26f | |||
af898a6b8d | |||
b89cdb7664 | |||
d37bfaa9d3 | |||
90cca05b31 | |||
7c229500c1 | |||
290424c4a7 | |||
703fa101a4 | |||
66e4982e2d | |||
09b1c415dd | |||
629b941558 | |||
aa50481c00 | |||
bc714ee22d | |||
b8cc0357a7 | |||
aed0ac3ee9 | |||
cca3575596 | |||
69dcebcf74 | |||
7da9daddd5 | |||
cd0c3bc759 | |||
af096af507 | |||
d150c66427 | |||
3bef967023 | |||
9c3658b33e | |||
6968cbc942 | |||
2cb7a065a9 | |||
35058623a7 | |||
24fe165c46 | |||
1498438fee | |||
4653904ded | |||
bd87bf8ce3 | |||
364c3f4ab7 | |||
fd8b7f1bee | |||
da32adb16b | |||
1b5a0ad0c8 | |||
e78fdf4a09 | |||
0564a50fb5 | |||
4f7db36123 | |||
41d536ea1b | |||
91d915cc28 | |||
c7a300119e | |||
c5c3e1c115 | |||
c93788d0ee | |||
1e46f8d0ab | |||
9963ae90ef | |||
a9225bbd76 | |||
801fff6e22 | |||
b4b0d075be | |||
16c2eb7090 | |||
3264ad1589 | |||
cb64635aea | |||
aeb4c20744 | |||
da1df24666 | |||
6b3c613f14 | |||
eb997e1ae9 | |||
f52d72e04a | |||
23e2566843 | |||
0d0fcef7ac | |||
62a597718b | |||
327cb4ceaf | |||
aac475ad16 | |||
046a60a6ed | |||
dcf964bf7a | |||
bff5f1cab7 | |||
6d1ef184e0 | |||
e30d366f2f | |||
4a2fe06f05 | |||
210e7588f1 | |||
72b100124d | |||
996d36aaa8 | |||
82b5b105b1 | |||
248ec4d03c | |||
faa21f5453 | |||
21c4a7c1e0 | |||
f3f76e7d5b | |||
e341b630a2 | |||
58532b9b60 | |||
a697ccd0ca | |||
4bafe0b4ef | |||
7af2092ae1 | |||
32374f76c1 | |||
9403b563f6 | |||
bd492f8d92 | |||
fbc84d551d | |||
874a120f88 | |||
199396c210 | |||
f0de3ad54a | |||
bfeac63c57 | |||
d65f37739c | |||
5895e3d02c | |||
0e036b5ec6 | |||
1d1602852c | |||
9b5013ee45 | |||
ed815c3ad1 | |||
8645063e8d | |||
2d044d5664 | |||
51ba81f257 | |||
bb7a247f66 | |||
c1f71253a4 | |||
96e1c26600 | |||
9d9964df11 | |||
e257b113b9 | |||
11e0bbaed1 | |||
fb430d8c76 | |||
7ed2a7082d | |||
ba7686189e | |||
9dcd25b69a | |||
881fad36a0 | |||
29906e22d2 | |||
d89c77e22d | |||
f91c61cd20 | |||
06aef50047 | |||
52a5cdea94 | |||
d3f1b36676 | |||
5ea5f05b02 | |||
5877081280 | |||
c4ce974ddf | |||
65c718e73b | |||
a8e6c175c0 | |||
044416cd2a | |||
009cc88d54 | |||
da1f4f4fa0 | |||
cfad21e1a3 | |||
5eb54d40e0 | |||
80ff036acb | |||
|
2ed4f0d05f
|
||
|
23ed9b830d
|
||
|
4604aaeeba
|
||
|
2dfe973c51
|
||
b7625cd4c4 | |||
12e7a071d9 | |||
6409741a12 | |||
78eb9f11e5 | |||
c28f28e852 | |||
72270c57ed | |||
5d6dc75400 | |||
20b02d903c | |||
58dc4927c2 | |||
ce48cbee72 | |||
3bd179bee1 | |||
940e5be2c1 | |||
4a4113aad7 | |||
3ee939db1d | |||
00f4e11f02 | |||
635bdd0f50 | |||
82f7a186a9 | |||
030fa43404 | |||
2ad9f6c179 | |||
80952913c9 | |||
1ce38c9de2 | |||
bbb721c6fa | |||
a50d238715 | |||
463a6e3236 | |||
91ecf88a38 | |||
4f9a99d232 | |||
02d3a2e159 | |||
03370871c4 | |||
1f4e1ac35e | |||
debbee17eb | |||
c63a56bc4e | |||
568346ce3d | |||
a010618764 | |||
a80e04065f | |||
72c2967d1f | |||
1cacb914b4 | |||
517ecbb68a | |||
e1313105d1 | |||
5b84f9d5ce | |||
7c254047a8 | |||
fffdce2c58 | |||
4014747712 | |||
b49df4845a | |||
9bac2e924c | |||
ee6d704e1e | |||
bfeaed40d5 | |||
4dd864c7b6 | |||
205741dde1 | |||
a5acc1e34e | |||
fc2dbab782 | |||
9236fa3ff1 | |||
334fe5553e | |||
9418b33c6c | |||
7a8c84d1a6 | |||
367d606870 | |||
682397975f | |||
b4a1c5dc01 | |||
1c38c2105b | |||
072d7b4825 | |||
9ecda0c58b | |||
8c2cc51a3c | |||
2f38e559d1 | |||
61d9359ce3 | |||
d46ca05346 | |||
a84d55c858 | |||
2cd7307291 | |||
efde2fad69 | |||
e537e5d00c | |||
adffc67ca0 | |||
f0b184f4c3 | |||
656a3bacdf | |||
c0c467d72b | |||
dbee0e8bd3 | |||
c7723fce2d | |||
12a641c86f | |||
73e817d408 | |||
6aa53bdf25 | |||
d5dbb37e18 | |||
5108019db0 | |||
6fc0607823 | |||
c1df08dc59 | |||
92747f0913 | |||
f77e35731c | |||
b9e70132ae | |||
245181ad28 | |||
fbaeddfcd8 | |||
e63a450c5d | |||
8ab35475fc | |||
69776dc5dc | |||
cfe15a0551 | |||
888582a74a | |||
72d5f64f90 | |||
fe2a577b3b | |||
ac6fb96534 | |||
f6bd0c10c0 | |||
50aab4c662 | |||
8c6180a8cf | |||
044241c71e | |||
e057cdf0d3 | |||
7ccbd1deb2 | |||
68ba45ca9c | |||
a7d81d6013 | |||
5f00b10f35 | |||
7433ac1d3a | |||
db766bacc3 | |||
77751ecc46 | |||
a025328fef | |||
0c866e8f89 | |||
2d12899da5 | |||
f4462190c9 | |||
7342dc42e9 | |||
60c0a118c9 | |||
cf101ef3f0 | |||
0331346025 | |||
21f7da2257 | |||
2916ec8da5 | |||
2bc9830a7a | |||
f281986c8f | |||
e56f444960 | |||
b96c4d26ca | |||
2949e23b11 | |||
1d7d0a7b43 | |||
e8e51e21ca | |||
dc96392b40 | |||
c757b50c65 | |||
0a7978569e | |||
463e3e198b | |||
8bedcc5195 | |||
0d9451e92c | |||
a230c2aa52 | |||
0f6cb392d6 | |||
48a889b353 | |||
a672bb85fb | |||
6ecc4ecb3a | |||
881cba862f | |||
77bdf7649a | |||
a0a79dcf4d | |||
3d7336a3a0 | |||
bbdd9eb0be | |||
c48ff699f4 | |||
27014958be | |||
5027e98c04 | |||
e2498afc4d | |||
7234811edc | |||
4692cfbe6f | |||
464e807747 | |||
76f2ae8aec | |||
a0b65ea8f4 | |||
5de077610c | |||
d7bbe5695c | |||
86df1921e0 | |||
9de8cf698f | |||
c955811373 | |||
530318a35b | |||
798f96e364 | |||
11bf2aafcd | |||
3a2c590158 | |||
a47d217ab3 | |||
6b40bf7bbf | |||
d36c0c407f | |||
e71e6f04e8 | |||
7e663964ee | |||
7940a8cc86 | |||
edab8aa9dd | |||
a552f751f0 | |||
b7ec9a9ffb | |||
b0b39726b8 | |||
06228c58f3 | |||
d828467d25 | |||
dd316e20b7 | |||
937664dfba | |||
4c8eae5e7c | |||
c61d648294 | |||
630803246c | |||
69b9e41129 | |||
f8ad932e33 |
21
.gitattributes
vendored
Normal file
21
.gitattributes
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
*.7z filter=lfs diff=lfs merge=lfs -text
|
||||
*.bsp filter=lfs diff=lfs merge=lfs -text
|
||||
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
||||
*.gem filter=lfs diff=lfs merge=lfs -text
|
||||
*.gz filter=lfs diff=lfs merge=lfs -text
|
||||
*.jar filter=lfs diff=lfs merge=lfs -text
|
||||
*.lz filter=lfs diff=lfs merge=lfs -text
|
||||
*.lzma filter=lfs diff=lfs merge=lfs -text
|
||||
*.oxt filter=lfs diff=lfs merge=lfs -text
|
||||
*.pdf filter=lfs diff=lfs merge=lfs -text
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
*.rpm filter=lfs diff=lfs merge=lfs -text
|
||||
*.tbz filter=lfs diff=lfs merge=lfs -text
|
||||
*.tbz2 filter=lfs diff=lfs merge=lfs -text
|
||||
*.tgz filter=lfs diff=lfs merge=lfs -text
|
||||
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||
*.txz filter=lfs diff=lfs merge=lfs -text
|
||||
*.whl filter=lfs diff=lfs merge=lfs -text
|
||||
*.zip filter=lfs diff=lfs merge=lfs -text
|
||||
*.zst filter=lfs diff=lfs merge=lfs -text
|
||||
*.changes merge=merge-changes
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
mock
|
||||
node_modules
|
||||
*.obscpio
|
||||
autogits-tmp.tar.zst
|
||||
*.osc
|
12
README.md
12
README.md
@@ -3,20 +3,18 @@ AutoGits
|
||||
|
||||
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
|
||||
* maintainer-and-policy-bot -- review bot to make sure maintainer signed off on reviews, along with necessary other entities
|
||||
* 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
|
||||
* pr-review -- keeps PR to _ObsPrj consistent with a PR to a package update
|
||||
* prjgit-updater -- update _ObsPrj based on direct pushes and repo creations/removals from organization
|
||||
* staging-utils -- review tooling for PR
|
||||
* 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
|
||||
* staging-utils -- review tooling for PR
|
||||
- list PR
|
||||
- merge PR
|
||||
- split PR
|
||||
- diff PR
|
||||
- accept/reject PR
|
||||
* random -- random utils and tools
|
||||
|
||||
Bugs
|
||||
----
|
||||
|
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>
|
||||
|
10
autogits.changes
Normal file
10
autogits.changes
Normal file
@@ -0,0 +1,10 @@
|
||||
-------------------------------------------------------------------
|
||||
Wed Sep 11 16:00:58 UTC 2024 - Adam Majer <adam.majer@suse.de>
|
||||
|
||||
- enable Authorization bearer token checks
|
||||
|
||||
-------------------------------------------------------------------
|
||||
Wed Sep 11 14:10:18 UTC 2024 - Adam Majer <adam.majer@suse.de>
|
||||
|
||||
- rabbitmq publisher
|
||||
|
142
autogits.spec
142
autogits.spec
@@ -17,12 +17,11 @@
|
||||
|
||||
|
||||
Name: autogits
|
||||
Version: 0.0.1
|
||||
Version: 0
|
||||
Release: 0
|
||||
Summary: GitWorkflow utilities
|
||||
License: GPL-2.0-or-later
|
||||
URL: https://src.opensuse.org/adamm/autogits/
|
||||
Source: https://src.opensuse.org/adamm/autogits/0.0.1.tar.gz
|
||||
URL: https://src.opensuse.org/adamm/autogits
|
||||
Source1: vendor.tar.zst
|
||||
BuildRequires: golang-packaging
|
||||
BuildRequires: systemd-rpm-macros
|
||||
@@ -33,6 +32,7 @@ BuildRequires: zstd
|
||||
Git Workflow tooling and utilities enabling automated handing of OBS projects
|
||||
as git repositories
|
||||
|
||||
|
||||
%package -n gitea-events-rabbitmq-publisher
|
||||
Summary: Publishes Gitea webhook data via RabbitMQ
|
||||
|
||||
@@ -41,18 +41,113 @@ Listens on an HTTP socket and publishes Gitea events on a RabbitMQ instance
|
||||
with a topic
|
||||
<scope>.src.$organization.$webhook_type.[$webhook_action_type]
|
||||
|
||||
|
||||
%package -n doc
|
||||
Summary: Common documentation files
|
||||
|
||||
%description -n doc
|
||||
Common documentation files
|
||||
|
||||
|
||||
%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
|
||||
|
||||
%description -n group-review
|
||||
Is used to handle reviews associated with groups defined in the
|
||||
ProjectGit.
|
||||
|
||||
|
||||
%package -n obs-staging-bot
|
||||
Summary: Build a PR against a ProjectGit, if review is requested
|
||||
|
||||
%description -n obs-staging-bot
|
||||
Build a PR against a ProjectGit, if review is requested.
|
||||
|
||||
|
||||
%package -n obs-status-service
|
||||
Summary: Reports build status of OBS service as an easily to produce SVG
|
||||
|
||||
%description -n obs-status-service
|
||||
Reports build status of OBS service as an easily to produce SVG
|
||||
|
||||
|
||||
%package -n workflow-direct
|
||||
Summary: Keep ProjectGit in sync for a devel project
|
||||
|
||||
%description -n workflow-direct
|
||||
Keep ProjectGit in sync with packages in the organization of a devel project
|
||||
|
||||
|
||||
%package -n workflow-pr
|
||||
Summary: Keeps ProjectGit PR in-sync with a PackageGit PR
|
||||
|
||||
%description -n workflow-pr
|
||||
Keeps ProjectGit PR in-sync with a PackageGit PR
|
||||
|
||||
|
||||
|
||||
%prep
|
||||
%autosetup -p1
|
||||
cp -r /home/abuild/rpmbuild/SOURCES/* ./
|
||||
tar x --zstd -f %{SOURCE1}
|
||||
|
||||
%build
|
||||
go build \
|
||||
-C gitea-events-rabbitmq-publisher \
|
||||
-mod=vendor \
|
||||
-buildmode=pie
|
||||
go build \
|
||||
-C devel-importer \
|
||||
-mod=vendor \
|
||||
-buildmode=pie
|
||||
go build \
|
||||
-C group-review \
|
||||
-mod=vendor \
|
||||
-buildmode=pie
|
||||
go build \
|
||||
-C obs-staging-bot \
|
||||
-mod=vendor \
|
||||
-buildmode=pie
|
||||
go build \
|
||||
-C obs-status-service \
|
||||
-mod=vendor \
|
||||
-buildmode=pie
|
||||
#go build \
|
||||
# -C workflow-direct \
|
||||
# -mod=vendor \
|
||||
# -buildmode=pie
|
||||
#go build \
|
||||
# -C workflow-pr \
|
||||
# -mod=vendor \
|
||||
# -buildmode=pie
|
||||
|
||||
%install
|
||||
install -D -m0755 gitea-events-rabbitmq-publisher/gitea-events-rabbitmq-publisher %{buildroot}%{_bindir}
|
||||
install -D -m0755 systemd/gitea-events-rabbitmq-publisher.service %{buildroot}%{_unitdir}
|
||||
install -D -m0755 gitea-events-rabbitmq-publisher/gitea-events-rabbitmq-publisher %{buildroot}%{_bindir}/gitea-events-rabbitmq-publisher
|
||||
install -D -m0644 systemd/gitea-events-rabbitmq-publisher.service %{buildroot}%{_unitdir}/gitea-events-rabbitmq-publisher.service
|
||||
install -D -m0755 devel-importer/devel-importer %{buildroot}%{_bindir}/devel-importer
|
||||
install -D -m0755 group-review/group-review %{buildroot}%{_bindir}/group-review
|
||||
install -D -m0755 obs-staging-bot/obs-staging-bot %{buildroot}%{_bindir}/obs-staging-bot
|
||||
install -D -m0755 obs-status-service/obs-status-service %{buildroot}%{_bindir}/obs-status-service
|
||||
#install -D -m0755 workflow-direct/workflow-direct %{buildroot}%{_bindir}/workflow-direct
|
||||
#install -D -m0755 workflow-pr/workflow-pr %{buildroot}%{_bindir}/workflow-pr
|
||||
|
||||
%pre -n gitea-events-rabbitmq-publisher
|
||||
%service_add_pre gitea-events-rabbitmq-publisher.service
|
||||
|
||||
%post -n gitea-events-rabbitmq-publisher
|
||||
%service_add_post gitea-events-rabbitmq-publisher.service
|
||||
|
||||
%preun -n gitea-events-rabbitmq-publisher
|
||||
%service_del_preun gitea-events-rabbitmq-publisher.service
|
||||
|
||||
%postun -n gitea-events-rabbitmq-publisher
|
||||
%service_del_postun gitea-events-rabbitmq-publisher.service
|
||||
|
||||
%files -n gitea-events-rabbitmq-publisher
|
||||
%license COPYING
|
||||
@@ -60,5 +155,38 @@ install -D -m0755 systemd/gitea-events-rabbitmq-publisher.service %{buildroot}%{
|
||||
%{_bindir}/gitea-events-rabbitmq-publisher
|
||||
%{_unitdir}/gitea-events-rabbitmq-publisher.service
|
||||
|
||||
%changelog
|
||||
%files -n doc
|
||||
%license COPYING
|
||||
%doc doc/README.md
|
||||
%doc doc/workflows.md
|
||||
|
||||
%files -n devel-importer
|
||||
%license COPYING
|
||||
%doc devel-importer/README.md
|
||||
%{_bindir}/devel-importer
|
||||
|
||||
%files -n group-review
|
||||
%license COPYING
|
||||
%doc group-review/README.md
|
||||
%{_bindir}/group-review
|
||||
|
||||
%files -n obs-staging-bot
|
||||
%license COPYING
|
||||
%doc obs-staging-bot/README.md
|
||||
%{_bindir}/obs-staging-bot
|
||||
|
||||
%files -n obs-status-service
|
||||
%license COPYING
|
||||
%doc obs-status-service/README.md
|
||||
%{_bindir}/obs-status-service
|
||||
|
||||
%files -n workflow-direct
|
||||
%license COPYING
|
||||
%doc workflow-direct/README.md
|
||||
#%{_bindir}/workflow-direct
|
||||
|
||||
%files -n workflow-pr
|
||||
%license COPYING
|
||||
%doc workflow-pr/README.md
|
||||
#%{_bindir}/workflow-pr
|
||||
|
||||
|
@@ -1,14 +0,0 @@
|
||||
all: build
|
||||
|
||||
api.json:
|
||||
curl -o api.json https://src.opensuse.org/swagger.v1.json
|
||||
|
||||
gitea-generated/client/gitea_api_client.go:: api.json
|
||||
[ -d gitea-generated ] || mkdir gitea-generated
|
||||
podman run --rm -v $$(pwd):/api ghcr.io/go-swagger/go-swagger generate client -f /api/api.json -t /api/gitea-generated
|
||||
|
||||
api: gitea-generated/client/gitea_api_client.go
|
||||
|
||||
build: api
|
||||
go build
|
||||
|
@@ -1,74 +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"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type AutogitConfig struct {
|
||||
Workflows []string // [pr, direct, test]
|
||||
Organization string
|
||||
GitProjectName string // Organization/GitProjectName.git is PrjGit
|
||||
Branch string // branch name of PkgGit that aligns with PrjGit submodules
|
||||
}
|
||||
|
||||
func ReadWorkflowConfigs(reader io.Reader) ([]*AutogitConfig, error) {
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error reading config file. err: %w", err)
|
||||
}
|
||||
|
||||
var config []*AutogitConfig
|
||||
if err = json.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("Error parsing config file. err: %w", err)
|
||||
}
|
||||
|
||||
availableWorkflows := []string{"pr", "direct", "test"}
|
||||
for _, workflow := range config {
|
||||
for _, w := range workflow.Workflows {
|
||||
if !slices.Contains(availableWorkflows, w) {
|
||||
return nil, fmt.Errorf(
|
||||
"Invalid Workflow '%s'. Only available workflows are: %s",
|
||||
w, strings.Join(availableWorkflows, " "),
|
||||
)
|
||||
}
|
||||
}
|
||||
if len(workflow.GitProjectName) == 0 {
|
||||
workflow.GitProjectName = DefaultGitPrj
|
||||
}
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func ReadWorkflowConfigsFile(filename string) ([]*AutogitConfig, 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 ReadWorkflowConfigs(file)
|
||||
}
|
@@ -1,705 +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"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type GitHandler struct {
|
||||
DebugLogger bool
|
||||
|
||||
GitPath string
|
||||
GitCommiter string
|
||||
GitEmail string
|
||||
}
|
||||
|
||||
func CreateGitHandler(git_author, email, name string) (*GitHandler, error) {
|
||||
var err error
|
||||
|
||||
git := new(GitHandler)
|
||||
git.GitCommiter = git_author
|
||||
git.GitPath, err = os.MkdirTemp("", name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot create temp dir: %w", err)
|
||||
}
|
||||
|
||||
if err = os.Chmod(git.GitPath, 0700); err != nil {
|
||||
return nil, fmt.Errorf("Cannot fix permissions of temp dir: %w", err)
|
||||
}
|
||||
|
||||
return git, nil
|
||||
}
|
||||
|
||||
//func (h *GitHandler) ProcessBranchList() []string {
|
||||
// if h.HasError() {
|
||||
// return make([]string, 0)
|
||||
// }
|
||||
//
|
||||
// trackedBranches, err := os.ReadFile(path.Join(h.GitPath, DefaultGitPrj, TrackedBranchesFile))
|
||||
// if err != nil {
|
||||
// if errors.Is(err, os.ErrNotExist) {
|
||||
// trackedBranches = []byte("factory")
|
||||
// } else {
|
||||
// h.LogError("file error reading '%s' file in repo", TrackedBranchesFile)
|
||||
// h.Error = err
|
||||
// return make([]string, 0)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return strings.Split(string(trackedBranches), "\n")
|
||||
//}
|
||||
|
||||
type GitReference struct {
|
||||
Branch string
|
||||
Id string
|
||||
}
|
||||
|
||||
type GitReferences struct {
|
||||
refs []GitReference
|
||||
}
|
||||
|
||||
func (refs *GitReferences) addReference(id, branch string) {
|
||||
for _, ref := range refs.refs {
|
||||
if ref.Id == id && ref.Branch == branch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
refs.refs = append(refs.refs, GitReference{Branch: branch, Id: id})
|
||||
}
|
||||
|
||||
func processRefs(gitDir string) ([]GitReference, error) {
|
||||
packedRefsPath := path.Join(gitDir, "packed-refs")
|
||||
stat, err := os.Stat(packedRefsPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if stat.Size() > 10000 || stat.IsDir() {
|
||||
return nil, fmt.Errorf("Funny business with 'packed-refs' in '%s'", gitDir)
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(packedRefsPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var references GitReferences
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
if len(line) < 1 || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
|
||||
splitLine := strings.Split(line, " ")
|
||||
if len(splitLine) != 2 {
|
||||
return nil, fmt.Errorf("Unexpected packaged-refs entry '%#v' in '%s'", splitLine, packedRefsPath)
|
||||
}
|
||||
id, ref := splitLine[0], splitLine[1]
|
||||
const remoteRefPrefix = "refs/remotes/origin/"
|
||||
if ref[0:len(remoteRefPrefix)] != remoteRefPrefix {
|
||||
continue
|
||||
}
|
||||
|
||||
references.addReference(id, ref[len(remoteRefPrefix):])
|
||||
}
|
||||
|
||||
return references.refs, nil
|
||||
}
|
||||
|
||||
func findGitDir(p string) (string, error) {
|
||||
gitFile := path.Join(p, ".git")
|
||||
stat, err := os.Stat(gitFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if stat.IsDir() {
|
||||
return path.Join(p, ".git"), nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(gitFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
refs := strings.Split(line, ":")
|
||||
if len(refs) != 2 {
|
||||
return "", fmt.Errorf("Unknown format of .git file: '%s'\n", line)
|
||||
}
|
||||
|
||||
if refs[0] != "gitdir" {
|
||||
return "", fmt.Errorf("Unknown header of .git file: '%s'\n", refs[0])
|
||||
}
|
||||
|
||||
return path.Join(p, strings.TrimSpace(refs[1])), nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Can't find git subdirectory in '%s'", p)
|
||||
}
|
||||
|
||||
func (e *GitHandler) GitBranchHead(gitDir, branchName string) (string, error) {
|
||||
path, err := findGitDir(path.Join(e.GitPath, gitDir))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error identifying gitdir in `%s`: %w", gitDir, err)
|
||||
}
|
||||
|
||||
refs, err := processRefs(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error finding branches (%s): %w\n", branchName, err)
|
||||
}
|
||||
|
||||
for _, ref := range refs {
|
||||
if ref.Branch == branchName {
|
||||
return ref.Id, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("Can't find default remote branch: %s", branchName)
|
||||
}
|
||||
|
||||
func (e *GitHandler) Close() error {
|
||||
if err := os.RemoveAll(e.GitPath); err != nil {
|
||||
return err
|
||||
}
|
||||
e.GitPath = ""
|
||||
return nil
|
||||
}
|
||||
|
||||
type writeFunc func(data []byte) (int, error)
|
||||
|
||||
func (f writeFunc) Write(data []byte) (int, error) {
|
||||
return f(data)
|
||||
}
|
||||
|
||||
func (h writeFunc) UnmarshalText(text []byte) error {
|
||||
_, err := h.Write(text)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h writeFunc) Close() error {
|
||||
_, err := h.Write(nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *GitHandler) GitExec(cwd string, params ...string) error {
|
||||
cmd := exec.Command("/usr/bin/git", params...)
|
||||
cmd.Env = []string{
|
||||
"GIT_CEILING_DIRECTORIES=" + e.GitPath,
|
||||
"GIT_CONFIG_GLOBAL=/dev/null",
|
||||
"GIT_AUTHOR_NAME=" + e.GitCommiter,
|
||||
"GIT_COMMITTER_NAME=" + e.GitCommiter,
|
||||
"EMAIL=not@exist@src.opensuse.org",
|
||||
"GIT_LFS_SKIP_SMUDGE=1",
|
||||
"GIT_SSH_COMMAND=/usr/bin/ssh -o StrictHostKeyChecking=yes",
|
||||
}
|
||||
cmd.Dir = filepath.Join(e.GitPath, cwd)
|
||||
cmd.Stdin = nil
|
||||
|
||||
if e.DebugLogger {
|
||||
log.Printf("git execute: %#v\n", cmd.Args)
|
||||
}
|
||||
out, err := cmd.CombinedOutput()
|
||||
if e.DebugLogger {
|
||||
log.Println(string(out))
|
||||
}
|
||||
if err != nil {
|
||||
if e.DebugLogger {
|
||||
log.Printf(" *** error: %v\n", err)
|
||||
}
|
||||
return fmt.Errorf("error executing: git %#v \n%s\n err: %w", cmd.Args, out, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ChanIO struct {
|
||||
ch chan byte
|
||||
}
|
||||
|
||||
func (c *ChanIO) Write(p []byte) (int, error) {
|
||||
for _, b := range p {
|
||||
c.ch <- b
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// read at least 1 byte, but don't block if nothing more in channel
|
||||
func (c *ChanIO) Read(data []byte) (idx int, err error) {
|
||||
var ok bool
|
||||
|
||||
data[idx], ok = <-c.ch
|
||||
if !ok {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
idx++
|
||||
|
||||
for len(c.ch) > 0 && idx < len(data) {
|
||||
data[idx], ok = <-c.ch
|
||||
if !ok {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
|
||||
idx++
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type gitMsg struct {
|
||||
hash string
|
||||
itemType string
|
||||
size int
|
||||
}
|
||||
|
||||
type commit struct {
|
||||
Tree string
|
||||
Msg string
|
||||
}
|
||||
|
||||
type tree_entry struct {
|
||||
name string
|
||||
mode int
|
||||
hash string
|
||||
|
||||
size int
|
||||
}
|
||||
|
||||
type tree struct {
|
||||
items []tree_entry
|
||||
}
|
||||
|
||||
func (t *tree_entry) isSubmodule() bool {
|
||||
return (t.mode & 0170000) == 0160000
|
||||
}
|
||||
|
||||
func (t *tree_entry) isTree() bool {
|
||||
return (t.mode & 0170000) == 0040000
|
||||
}
|
||||
|
||||
func (t *tree_entry) isBlob() bool {
|
||||
return !t.isTree() && !t.isSubmodule()
|
||||
}
|
||||
|
||||
func parseGitMsg(data <-chan byte) (gitMsg, error) {
|
||||
var id []byte = make([]byte, 64)
|
||||
var msgType []byte = make([]byte, 16)
|
||||
var size int
|
||||
|
||||
pos := 0
|
||||
for c := <-data; c != ' '; c = <-data {
|
||||
if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') {
|
||||
id[pos] = c
|
||||
pos++
|
||||
} else {
|
||||
return gitMsg{}, fmt.Errorf("Invalid character during object hash parse '%c' at %d", c, pos)
|
||||
}
|
||||
}
|
||||
id = id[:pos]
|
||||
|
||||
pos = 0
|
||||
var c byte
|
||||
for c = <-data; c != ' ' && c != '\x00'; c = <-data {
|
||||
if c >= 'a' && c <= 'z' {
|
||||
msgType[pos] = c
|
||||
pos++
|
||||
} else {
|
||||
return gitMsg{}, fmt.Errorf("Invalid character during object type parse '%c' at %d", c, pos)
|
||||
}
|
||||
}
|
||||
msgType = msgType[:pos]
|
||||
|
||||
switch string(msgType) {
|
||||
case "commit", "tree", "blob":
|
||||
break
|
||||
case "missing":
|
||||
if c != '\x00' {
|
||||
return gitMsg{}, fmt.Errorf("Missing format weird")
|
||||
}
|
||||
return gitMsg{
|
||||
hash: string(id[:]),
|
||||
itemType: "missing",
|
||||
size: 0,
|
||||
}, fmt.Errorf("Object not found: '%s'", string(id))
|
||||
default:
|
||||
return gitMsg{}, fmt.Errorf("Invalid object type: '%s'", string(msgType))
|
||||
}
|
||||
|
||||
for c = <-data; c != '\000'; c = <-data {
|
||||
if c >= '0' && c <= '9' {
|
||||
size = size*10 + (int(c) - '0')
|
||||
} else {
|
||||
return gitMsg{}, fmt.Errorf("Invalid character during object size parse: '%c'", c)
|
||||
}
|
||||
}
|
||||
|
||||
return gitMsg{
|
||||
hash: string(id[:]),
|
||||
itemType: string(msgType),
|
||||
size: size,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseGitCommitHdr(data <-chan byte) ([2]string, error) {
|
||||
hdr := make([]byte, 0, 60)
|
||||
val := make([]byte, 0, 1000)
|
||||
|
||||
c := <-data
|
||||
if c != '\n' { // end of header marker
|
||||
for ; c != ' '; c = <-data {
|
||||
hdr = append(hdr, c)
|
||||
}
|
||||
for c := <-data; c != '\n'; c = <-data {
|
||||
val = append(val, c)
|
||||
}
|
||||
}
|
||||
|
||||
return [2]string{string(hdr), string(val)}, nil
|
||||
}
|
||||
|
||||
func parseGitCommitMsg(data <-chan byte, l int) (string, error) {
|
||||
msg := make([]byte, 0, l)
|
||||
|
||||
for c := <-data; c != '\x00'; c = <-data {
|
||||
msg = append(msg, c)
|
||||
l--
|
||||
}
|
||||
// l--
|
||||
|
||||
if l != 0 {
|
||||
return "", fmt.Errorf("Unexpected data in the git commit msg: l=%d", l)
|
||||
}
|
||||
|
||||
return string(msg), nil
|
||||
}
|
||||
|
||||
func parseGitCommit(data <-chan byte) (commit, error) {
|
||||
hdr, err := parseGitMsg(data)
|
||||
if err != nil {
|
||||
return commit{}, err
|
||||
} else if hdr.itemType != "commit" {
|
||||
return commit{}, fmt.Errorf("expected commit but parsed %s", hdr.itemType)
|
||||
}
|
||||
|
||||
var c commit
|
||||
l := hdr.size
|
||||
for {
|
||||
hdr, err := parseGitCommitHdr(data)
|
||||
if err != nil {
|
||||
return commit{}, nil
|
||||
}
|
||||
|
||||
if len(hdr[0])+len(hdr[1]) == 0 { // hdr end marker
|
||||
break
|
||||
}
|
||||
|
||||
switch hdr[0] {
|
||||
case "tree":
|
||||
c.Tree = hdr[1]
|
||||
}
|
||||
|
||||
l -= len(hdr[0]) + len(hdr[1]) + 2
|
||||
}
|
||||
l--
|
||||
|
||||
c.Msg, err = parseGitCommitMsg(data, l)
|
||||
return c, err
|
||||
}
|
||||
|
||||
func parseTreeEntry(data <-chan byte, hashLen int) (tree_entry, error) {
|
||||
var e tree_entry
|
||||
|
||||
for c := <-data; c != ' '; c = <-data {
|
||||
e.mode = e.mode*8 + int(c-'0')
|
||||
e.size++
|
||||
}
|
||||
e.size++
|
||||
|
||||
name := make([]byte, 0, 128)
|
||||
for c := <-data; c != '\x00'; c = <-data {
|
||||
name = append(name, c)
|
||||
e.size++
|
||||
}
|
||||
e.size++
|
||||
e.name = string(name)
|
||||
|
||||
const hexBinToAscii = "0123456789abcdef"
|
||||
|
||||
hash := make([]byte, 0, hashLen*2)
|
||||
for range hashLen {
|
||||
c := <-data
|
||||
hash = append(hash, hexBinToAscii[((c&0xF0)>>4)], hexBinToAscii[c&0xF])
|
||||
}
|
||||
e.hash = string(hash)
|
||||
e.size += hashLen
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func parseGitTree(data <-chan byte) (tree, error) {
|
||||
|
||||
hdr, err := parseGitMsg(data)
|
||||
if err != nil {
|
||||
return tree{}, err
|
||||
}
|
||||
|
||||
// max capacity to length of hash
|
||||
t := tree{items: make([]tree_entry, 0, hdr.size/len(hdr.hash))}
|
||||
parsedLen := 0
|
||||
for parsedLen < hdr.size {
|
||||
entry, err := parseTreeEntry(data, len(hdr.hash)/2)
|
||||
if err != nil {
|
||||
return tree{}, nil
|
||||
}
|
||||
|
||||
t.items = append(t.items, entry)
|
||||
parsedLen += entry.size
|
||||
}
|
||||
c := <-data // \0 read
|
||||
|
||||
if c != '\x00' {
|
||||
return t, fmt.Errorf("Unexpected character during git tree data read")
|
||||
}
|
||||
|
||||
if parsedLen != hdr.size {
|
||||
return t, fmt.Errorf("Invalid size of git tree data")
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func parseGitBlob(data <-chan byte) ([]byte, error) {
|
||||
hdr, err := parseGitMsg(data)
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
|
||||
d := make([]byte, hdr.size)
|
||||
for l := 0; l < hdr.size; l++ {
|
||||
d[l] = <-data
|
||||
}
|
||||
eob := <-data
|
||||
if eob != '\x00' {
|
||||
return d, fmt.Errorf("invalid byte read in parseGitBlob")
|
||||
}
|
||||
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// TODO: support sub-trees
|
||||
func (e *GitHandler) GitCatFile(cwd, commitId, filename string) (data []byte, err error) {
|
||||
var done sync.Mutex
|
||||
|
||||
done.Lock()
|
||||
data_in, data_out := ChanIO{make(chan byte, 256)}, ChanIO{make(chan byte, 70)}
|
||||
|
||||
go func() {
|
||||
defer done.Unlock()
|
||||
defer close(data_out.ch)
|
||||
|
||||
data_out.Write([]byte(commitId))
|
||||
data_out.ch <- '\x00'
|
||||
c, err := parseGitCommit(data_in.ch)
|
||||
if err != nil {
|
||||
log.Printf("Error parsing git commit: %v\n", err)
|
||||
return
|
||||
}
|
||||
data_out.Write([]byte(c.Tree))
|
||||
data_out.ch <- '\x00'
|
||||
tree, err := parseGitTree(data_in.ch)
|
||||
|
||||
if err != nil {
|
||||
if e.DebugLogger {
|
||||
log.Printf("Error parsing git tree: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for _, te := range tree.items {
|
||||
if te.isBlob() && te.name == filename {
|
||||
data_out.Write([]byte(te.hash))
|
||||
data_out.ch <- '\x00'
|
||||
data, err = parseGitBlob(data_in.ch)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = fmt.Errorf("file not found: '%s'", filename)
|
||||
}()
|
||||
|
||||
cmd := exec.Command("/usr/bin/git", "cat-file", "--batch", "-Z")
|
||||
cmd.Env = []string{
|
||||
"GIT_CEILING_DIRECTORIES=" + e.GitPath,
|
||||
"GIT_CONFIG_GLOBAL=/dev/null",
|
||||
}
|
||||
cmd.Dir = filepath.Join(e.GitPath, cwd)
|
||||
cmd.Stdout = &data_in
|
||||
cmd.Stdin = &data_out
|
||||
cmd.Stderr = writeFunc(func(data []byte) (int, error) {
|
||||
if e.DebugLogger {
|
||||
log.Printf(string(data))
|
||||
}
|
||||
return len(data), nil
|
||||
})
|
||||
if e.DebugLogger {
|
||||
log.Printf("command run: %v\n", cmd.Args)
|
||||
}
|
||||
err = cmd.Run()
|
||||
|
||||
done.Lock()
|
||||
return
|
||||
}
|
||||
|
||||
// return (filename) -> (hash) map for all submodules
|
||||
// TODO: recursive? map different orgs, not just assume '.' for path
|
||||
func (e *GitHandler) GitSubmoduleList(cwd, commitId string) (submoduleList map[string]string, err error) {
|
||||
var done sync.Mutex
|
||||
submoduleList = make(map[string]string)
|
||||
|
||||
done.Lock()
|
||||
data_in, data_out := ChanIO{make(chan byte, 256)}, ChanIO{make(chan byte, 70)}
|
||||
|
||||
go func() {
|
||||
defer done.Unlock()
|
||||
defer close(data_out.ch)
|
||||
|
||||
data_out.Write([]byte(commitId))
|
||||
data_out.ch <- '\x00'
|
||||
var c commit
|
||||
c, err = parseGitCommit(data_in.ch)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("Error parsing git commit. Err: %w", err)
|
||||
return
|
||||
}
|
||||
data_out.Write([]byte(c.Tree))
|
||||
data_out.ch <- '\x00'
|
||||
var tree tree
|
||||
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.isSubmodule() {
|
||||
submoduleList[te.name] = te.hash
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
cmd := exec.Command("/usr/bin/git", "cat-file", "--batch", "-Z")
|
||||
cmd.Env = []string{
|
||||
"GIT_CEILING_DIRECTORIES=" + e.GitPath,
|
||||
"GIT_CONFIG_GLOBAL=/dev/null",
|
||||
}
|
||||
cmd.Dir = filepath.Join(e.GitPath, cwd)
|
||||
cmd.Stdout = &data_in
|
||||
cmd.Stdin = &data_out
|
||||
cmd.Stderr = writeFunc(func(data []byte) (int, error) {
|
||||
if e.DebugLogger {
|
||||
log.Println(string(data))
|
||||
}
|
||||
return len(data), nil
|
||||
})
|
||||
if e.DebugLogger {
|
||||
log.Printf("command run: %v\n", cmd.Args)
|
||||
}
|
||||
err = cmd.Run()
|
||||
|
||||
done.Lock()
|
||||
return submoduleList, err
|
||||
}
|
||||
|
||||
func (e *GitHandler) GitSubmoduleCommitId(cwd, packageName, commitId string) (subCommitId string, valid bool) {
|
||||
defer func() {
|
||||
if recover() != nil {
|
||||
commitId = ""
|
||||
valid = false
|
||||
}
|
||||
}()
|
||||
|
||||
data_in, data_out := ChanIO{make(chan byte, 256)}, ChanIO{make(chan byte, 70)}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
if e.DebugLogger {
|
||||
log.Printf("getting commit id '%s' from git at '%s' with packageName: %s\n", commitId, cwd, packageName)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer close(data_out.ch)
|
||||
|
||||
data_out.Write([]byte(commitId))
|
||||
data_out.ch <- '\x00'
|
||||
c, err := parseGitCommit(data_in.ch)
|
||||
if err != nil {
|
||||
log.Panicf("Error parsing git commit: %v\n", err)
|
||||
}
|
||||
data_out.Write([]byte(c.Tree))
|
||||
data_out.ch <- '\x00'
|
||||
tree, err := parseGitTree(data_in.ch)
|
||||
|
||||
if err != nil {
|
||||
log.Panicf("Error parsing git tree: %v\n", err)
|
||||
}
|
||||
|
||||
for _, te := range tree.items {
|
||||
if te.name == packageName && te.isSubmodule() {
|
||||
subCommitId = te.hash
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
cmd := exec.Command("/usr/bin/git", "cat-file", "--batch", "-Z")
|
||||
cmd.Env = []string{
|
||||
"GIT_CEILING_DIRECTORIES=" + e.GitPath,
|
||||
"GIT_CONFIG_GLOBAL=/dev/null",
|
||||
}
|
||||
cmd.Dir = filepath.Join(e.GitPath, cwd)
|
||||
cmd.Stdout = &data_in
|
||||
cmd.Stdin = &data_out
|
||||
cmd.Stderr = writeFunc(func(data []byte) (int, error) {
|
||||
log.Println(string(data))
|
||||
return len(data), nil
|
||||
})
|
||||
if e.DebugLogger {
|
||||
log.Printf("command run: %v\n", cmd.Args)
|
||||
}
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Printf("Error running command %v, err: %v", cmd.Args, err)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return subCommitId, len(subCommitId) == len(commitId)
|
||||
}
|
@@ -1,304 +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 (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGitMsgParsing(t *testing.T) {
|
||||
t.Run("tree message with size 56", func(t *testing.T) {
|
||||
const hdr = "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f98 tree 56\x00"
|
||||
|
||||
data := make(chan byte, 500)
|
||||
for _, b := range []byte(hdr) {
|
||||
data <- b
|
||||
}
|
||||
gitHdr, err := parseGitMsg(data)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if gitHdr.hash != "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f98" {
|
||||
t.Errorf("Invalid hash %s", gitHdr.hash)
|
||||
}
|
||||
|
||||
if gitHdr.size != 56 {
|
||||
t.Errorf("Invalid msg size: %d", gitHdr.size)
|
||||
}
|
||||
|
||||
if gitHdr.itemType != "tree" {
|
||||
t.Errorf("Invalid msg type: %s", gitHdr.itemType)
|
||||
}
|
||||
})
|
||||
t.Run("commit message with size 256", func(t *testing.T) {
|
||||
const hdr = "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f99 commit 256\x00"
|
||||
|
||||
data := make(chan byte, 500)
|
||||
for _, b := range []byte(hdr) {
|
||||
data <- b
|
||||
}
|
||||
gitHdr, err := parseGitMsg(data)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if gitHdr.hash != "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f99" {
|
||||
t.Errorf("Invalid hash %s", gitHdr.hash)
|
||||
}
|
||||
|
||||
if gitHdr.size != 256 {
|
||||
t.Errorf("Invalid msg size: %d", gitHdr.size)
|
||||
}
|
||||
|
||||
if gitHdr.itemType != "commit" {
|
||||
t.Errorf("Invalid msg type: %s", gitHdr.itemType)
|
||||
}
|
||||
})
|
||||
t.Run("invalid tree message with size 56", func(t *testing.T) {
|
||||
const hdr = "f408r8ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f98 tree 56\x00"
|
||||
|
||||
data := make(chan byte, 500)
|
||||
for _, b := range []byte(hdr) {
|
||||
data <- b
|
||||
}
|
||||
gitHdr, err := parseGitMsg(data)
|
||||
|
||||
if err.Error() != "Invalid character during object hash parse 'r' at 4" {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if gitHdr.hash != "" {
|
||||
t.Errorf("Invalid hash %s", gitHdr.hash)
|
||||
}
|
||||
|
||||
if gitHdr.size != 0 {
|
||||
t.Errorf("Invalid msg size: %d", gitHdr.size)
|
||||
}
|
||||
|
||||
if gitHdr.itemType != "" {
|
||||
t.Errorf("Invalid msg type: %s", gitHdr.itemType)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGitCommitParsing(t *testing.T) {
|
||||
t.Run("parse valid commit message", func(t *testing.T) {
|
||||
const commitData = "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f99 commit 253\000" +
|
||||
`tree e20033df9f18780756ba4a96dbc7eb1a626253961039cb674156f266ba7a4e53
|
||||
parent 429cc2fe02170ca5668f0461928c7e7430c7a17cd64ac298286d7162572a7703
|
||||
author Adam Majer <amajer@suse.com> 1720709149 +0200
|
||||
committer Adam Majer <amajer@suse.com> 1720709149 +0200
|
||||
|
||||
.` + "\000"
|
||||
ch := make(chan byte, 5000)
|
||||
for _, b := range []byte(commitData) {
|
||||
ch <- b
|
||||
}
|
||||
commit, err := parseGitCommit(ch)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if commit.Tree != "e20033df9f18780756ba4a96dbc7eb1a626253961039cb674156f266ba7a4e53" {
|
||||
t.Errorf("Invalid commit object: %#v", commit)
|
||||
}
|
||||
|
||||
if commit.Msg != "." {
|
||||
t.Errorf("Invalid commit msg: '%s'", commit.Msg)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("parse multiline headers", func(t *testing.T) {
|
||||
const commitData = "cae5831ab48470ff060a5aaa12eb6e5a7acaf91e commit 1491\x00" +
|
||||
`tree 1f9c8fe8099615d6d3921528402ac53f09213b02
|
||||
parent e08a654fae0ecc91678819e0b62a2e014bad3339
|
||||
author Yagiz Nizipli <yagiz@nizipli.com> 1720967314 -0400
|
||||
committer GitHub <noreply@github.com> 1720967314 +0200
|
||||
gpgsig -----BEGIN PGP SIGNATURE-----
|
||||
|
||||
wsFcBAABCAAQBQJmk+CSCRC1aQ7uu5UhlAAAQIYQAEQXCl3bUUuegiz5/oitIIF7
|
||||
6xhndcjQIuqY4dONIeOARrGwbKh8OtMHpfJhMRUmvWvXrsTA6P1PWl0YcyyIMzHZ
|
||||
a4sBsWyxA0uSztVywpvksvk6EdMoEXeXrHS3cBxePsH8bI+Pwnsv27PsevEwpyIT
|
||||
reB4zZsoGySFVqf2lnXxG5hSRMYw++BDXSDMZk2BP9BvueRXasJ0lT1c7HlbHepF
|
||||
TWzwyHZ91OhXjrdPY7qLQEEV/frwuM+UrxOPb2e83ZTg81vXFuugURfhHNx4Iu+F
|
||||
LCMvOeaF2vO5yJtMe8+tY1l0Wb8S1aWcGCECN2XCXmmnWxt+yYh2gjqxq3y0DMcz
|
||||
zvg6arQIepDFLkQPZMDlUCIjIJQn4FbAaQAvoyMF8Pi5YmxhRqgo3iOB5SBE8eES
|
||||
63ifZ311izuSdD+o3ObFpzLoTgq62kglwfegZN/X8CzTSIqrT1norYJEbSwkWID1
|
||||
WeRHUHfC7f6N3XK8zeb83zmhBU58ghW9sp5/LcefGMRJmVhBWhjBCpeMUaFHdKhl
|
||||
/dfgPl5gJrrJ+wM3O6iaay0R1Iv4Upe/yXrbQnIgGj/qqgMLEPBY8lzNYimVTLxd
|
||||
2ObrcXnERo3wwxeUgWaAARbEGQjC51DK/2SXxVGUh+IokicsBNRKU7lVWwwFFely
|
||||
ntjtge6Gs9pA5rSIilPH
|
||||
=V1bK
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
|
||||
meta: change email address of anonrig
|
||||
|
||||
PR-URL: https://github.com/nodejs/node/pull/53829
|
||||
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
|
||||
Reviewed-By: James M Snell <jasnell@gmail.com>
|
||||
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
|
||||
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
|
||||
Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
|
||||
Reviewed-By: Richard Lau <rlau@redhat.com>
|
||||
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>` + "\x00"
|
||||
|
||||
ch := make(chan byte, 5000)
|
||||
for _, b := range []byte(commitData) {
|
||||
ch <- b
|
||||
}
|
||||
commit, err := parseGitCommit(ch)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if commit.Tree != "1f9c8fe8099615d6d3921528402ac53f09213b02" {
|
||||
t.Errorf("Invalid commit object: %#v", commit)
|
||||
}
|
||||
|
||||
if commit.Msg[len(commit.Msg)-55:] != "Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>" {
|
||||
t.Errorf("Invalid commit msg: '%s'", commit.Msg[len(commit.Msg)-55:])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("parse tree object", func(t *testing.T) {
|
||||
const treeData = "\x31\x61\x30\x35\x64\x62\x37\x33\x36\x39\x33\x37\x34\x33\x30\x65\x31\x38\x64\x66\x34\x33\x61\x32\x37\x61\x39\x38\x30\x30\x31\x30\x31\x32\x65\x31\x65\x64\x32\x30\x34\x38\x32\x39\x38\x36\x37\x31\x32\x38\x66\x32\x63\x65\x38\x34\x30\x36\x62\x35\x63\x66\x63\x39\x20\x74\x72\x65\x65\x20\x32\x30\x35\x00\x34\x30\x30\x30\x30\x20\x62\x6f\x74\x73\x2d\x63\x6f\x6d\x6d\x6f\x6e\x00\x93\x17\xaa\x47\xf6\xea\x37\xe8\xbc\xe2\x80\x77\x57\x90\xf4\xa8\x01\xd7\xe3\x70\x2f\x84\xfb\xe1\xb0\x0e\x4a\x2c\x1c\x75\x2c\x2b\x34\x30\x30\x30\x30\x20\x6f\x62\x73\x2d\x73\x74\x61\x67\x69\x6e\x67\x2d\x62\x6f\x74\x00\x79\x77\x8b\x28\x7d\x37\x10\x59\xb9\x71\x28\x36\xed\x20\x31\x5f\xfb\xe1\xed\xb5\xba\x4f\x5e\xbb\x65\x65\x68\x23\x77\x32\x58\xfe\x34\x30\x30\x30\x30\x20\x70\x72\x2d\x72\x65\x76\x69\x65\x77\x00\x36\x0d\x45\xcb\x76\xb8\x93\xb3\x21\xba\xfa\xd5\x00\x9d\xfc\x59\xab\x88\xc1\x3c\x81\xcb\x48\x5a\xe0\x29\x29\x0f\xe3\x6b\x3c\x5e\x34\x30\x30\x30\x30\x20\x70\x72\x6a\x67\x69\x74\x2d\x75\x70\x64\x61\x74\x65\x72\x00\xb4\x0b\x1c\xf5\xfb\xec\x9a\xb2\x9f\x48\x3e\x21\x18\x0d\x51\xb7\x98\x6e\x21\x99\x74\x84\x67\x71\x41\x24\x42\xfc\xc9\x04\x12\x99\x00"
|
||||
|
||||
ch := make(chan byte, 1000)
|
||||
for _, b := range []byte(treeData) {
|
||||
ch <- b
|
||||
}
|
||||
|
||||
tree, err := parseGitTree(ch)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
found := false
|
||||
t.Log(tree.items)
|
||||
for _, item := range tree.items {
|
||||
if item.name == "bots-common" && item.hash == "9317aa47f6ea37e8bce280775790f4a801d7e3702f84fbe1b00e4a2c1c752c2b" && item.isTree() {
|
||||
found = true
|
||||
t.Log("found")
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("expected sub-tree not found")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("parse tree object with submodules", func(t *testing.T) {
|
||||
const treeData = "\x34\x38\x34\x66\x31\x62\x65\x65\x63\x39\x35\x63\x61\x36\x62\x38\x36\x30\x64\x30\x64\x37\x63\x35\x65\x34\x38\x37\x31\x36\x36\x62\x30\x38\x65\x31\x61\x34\x35\x36\x37\x36\x62\x31\x37\x64\x39\x65\x65\x66\x32\x39\x39\x39\x65\x39\x64\x37\x34\x63\x62\x63\x64\x36\x20\x74\x72\x65\x65\x20\x33\x34\x39\x00\x31\x30\x30\x36\x34\x34\x20\x2e\x67\x69\x74\x6d\x6f\x64\x75\x6c\x65\x73\x00\xc8\x1e\x14\x29\xc5\xd4\x07\xfc\x32\xf1\xd7\xe2\x64\xee\x88\xfc\xce\xfc\x44\xf7\xae\x2e\xc4\x6e\x2c\x15\x27\x26\x65\xd4\xb8\x78\x31\x30\x30\x36\x34\x34\x20\x52\x45\x41\x44\x4d\x45\x2e\x6d\x64\x00\x90\x22\x84\x13\xbf\xd3\x5c\xfb\x1e\x27\x6b\xd6\x17\x3e\x89\xed\x0f\xc6\x31\x85\x24\x58\x6d\x9d\xf0\x6a\x1e\x17\x57\x19\x8d\xc2\x31\x36\x30\x30\x30\x30\x20\x6d\x69\x6e\x67\x77\x33\x32\x2d\x67\x63\x63\x00\xdc\x55\xb8\x28\x32\x8c\x8e\x49\x4f\x67\x87\x4a\x7d\x8c\x03\xdd\x1c\x6b\x4e\x02\xd1\x6b\x86\xe0\x8e\x47\xd7\x0e\xcd\x79\x96\x80\x31\x36\x30\x30\x30\x30\x20\x6e\x6f\x64\x65\x6a\x73\x2d\x63\x6f\x6d\x6d\x6f\x6e\x00\xd6\xa7\x4c\x08\x40\x6c\xe4\x0c\xc8\xf7\xbf\xf2\xd5\xcf\x30\x90\x87\xa8\x72\x83\x61\xcc\x75\x35\x4b\x08\x62\xba\x50\x81\x93\xb8\x31\x36\x30\x30\x30\x30\x20\x6e\x6f\x64\x65\x6a\x73\x32\x31\x00\x24\xee\x6b\xee\x74\x59\xa3\x86\xda\xda\xbf\x8a\x9f\x6a\xe4\xfa\x15\xc3\xf8\x10\xbf\xa0\x1c\xee\x52\x38\x13\x8a\xa2\x14\xd1\x80\x31\x36\x30\x30\x30\x30\x20\x6e\x6f\x64\x65\x6a\x73\x32\x32\x00\x87\x3a\x32\x3b\x26\x2e\xbb\x3b\xd7\x7b\x25\x92\xb2\xe1\x1b\xdd\x08\xdb\xc7\x21\xcb\xf4\xac\x9f\x97\x63\x7e\x58\xe1\xff\xfc\xe7\x31\x36\x30\x30\x30\x30\x20\x70\x79\x74\x68\x6f\x6e\x33\x31\x31\x00\x35\xc7\x02\xe8\x50\x1e\xed\xeb\x5c\xe4\x3d\x6f\x34\x60\xd1\x1c\x79\x1c\xfe\xfa\xa7\x72\x48\xf0\x8c\xad\x55\xd0\x0c\x37\xe7\x3a\x00"
|
||||
|
||||
ch := make(chan byte, 1000)
|
||||
for _, b := range []byte(treeData) {
|
||||
ch <- b
|
||||
}
|
||||
|
||||
tree, err := parseGitTree(ch)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, item := range tree.items {
|
||||
t.Log(item)
|
||||
if item.name == "nodejs22" && item.hash == "873a323b262ebb3bd77b2592b2e11bdd08dbc721cbf4ac9f97637e58e1fffce7" && item.isSubmodule() {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("expected submodule not found")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommitTreeParsingOfHead(t *testing.T) {
|
||||
gitDir := t.TempDir()
|
||||
testDir, _ := os.Getwd()
|
||||
var commitId string
|
||||
|
||||
cmd := exec.Command("/usr/bin/bash", path.Join(testDir, "tsetup.sh"))
|
||||
cmd.Dir = gitDir
|
||||
cmd.Stdout = writeFunc(func(data []byte) (int, error) {
|
||||
commitId = commitId + strings.TrimSpace(string(data))
|
||||
return len(data), nil
|
||||
})
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
t.Run("reads HEAD and parses the tree", func(t *testing.T) {
|
||||
const nodejs21 = "c678c57007d496a98bec668ae38f2c26a695f94af78012f15d044ccf066ccb41"
|
||||
h := GitHandler{
|
||||
GitPath: gitDir,
|
||||
}
|
||||
id, ok := h.GitSubmoduleCommitId("", "nodejs21", commitId)
|
||||
if !ok {
|
||||
t.Error("failed parse")
|
||||
}
|
||||
if id != nodejs21 {
|
||||
t.Errorf("hash doesn't match: %s vs. expected %s", id, nodejs21)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("reads README.md", func(t *testing.T) {
|
||||
h := GitHandler{
|
||||
GitPath: gitDir,
|
||||
}
|
||||
data, err := h.GitCatFile("", commitId, "README.md")
|
||||
if err != nil {
|
||||
t.Errorf("failed parse: %v", err)
|
||||
}
|
||||
if string(data) != "foo\n" || len(data) != 4 {
|
||||
t.Errorf("Wrong data of len: %d", len(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("read HEAD", func(t *testing.T) {
|
||||
h := GitHandler{
|
||||
GitPath: gitDir,
|
||||
}
|
||||
|
||||
data, err := h.GitSubmoduleList("", "HEAD")
|
||||
if err != nil {
|
||||
t.Error("failed to get submodule list", err)
|
||||
}
|
||||
|
||||
if len(data) != 5 {
|
||||
t.Error("Invalid len of submodules", len(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("try to parse unknown item", func(t *testing.T) {
|
||||
})
|
||||
}
|
@@ -1,104 +0,0 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
package issue
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/go-openapi/runtime"
|
||||
"github.com/go-openapi/strfmt"
|
||||
|
||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||
)
|
||||
|
||||
// IssueSearchIssuesReader is a Reader for the IssueSearchIssues structure.
|
||||
type IssueSearchIssuesReader struct {
|
||||
formats strfmt.Registry
|
||||
}
|
||||
|
||||
// ReadResponse reads a server response into the received o.
|
||||
func (o *IssueSearchIssuesReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
|
||||
switch response.Code() {
|
||||
case 200:
|
||||
result := NewIssueSearchIssuesOK()
|
||||
if err := result.readResponse(response, consumer, o.formats); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
default:
|
||||
return nil, runtime.NewAPIError("[GET /repos/issues/search] issueSearchIssues", response, response.Code())
|
||||
}
|
||||
}
|
||||
|
||||
// NewIssueSearchIssuesOK creates a IssueSearchIssuesOK with default headers values
|
||||
func NewIssueSearchIssuesOK() *IssueSearchIssuesOK {
|
||||
return &IssueSearchIssuesOK{}
|
||||
}
|
||||
|
||||
/*
|
||||
IssueSearchIssuesOK describes a response with status code 200, with default header values.
|
||||
|
||||
IssueList
|
||||
*/
|
||||
type IssueSearchIssuesOK struct {
|
||||
Payload []*models.Issue
|
||||
}
|
||||
|
||||
// IsSuccess returns true when this issue search issues o k response has a 2xx status code
|
||||
func (o *IssueSearchIssuesOK) IsSuccess() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// IsRedirect returns true when this issue search issues o k response has a 3xx status code
|
||||
func (o *IssueSearchIssuesOK) IsRedirect() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsClientError returns true when this issue search issues o k response has a 4xx status code
|
||||
func (o *IssueSearchIssuesOK) IsClientError() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsServerError returns true when this issue search issues o k response has a 5xx status code
|
||||
func (o *IssueSearchIssuesOK) IsServerError() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsCode returns true when this issue search issues o k response a status code equal to that given
|
||||
func (o *IssueSearchIssuesOK) IsCode(code int) bool {
|
||||
return code == 200
|
||||
}
|
||||
|
||||
// Code gets the status code for the issue search issues o k response
|
||||
func (o *IssueSearchIssuesOK) Code() int {
|
||||
return 200
|
||||
}
|
||||
|
||||
func (o *IssueSearchIssuesOK) Error() string {
|
||||
payload, _ := json.Marshal(o.Payload)
|
||||
return fmt.Sprintf("[GET /repos/issues/search][%d] issueSearchIssuesOK %s", 200, payload)
|
||||
}
|
||||
|
||||
func (o *IssueSearchIssuesOK) String() string {
|
||||
payload, _ := json.Marshal(o.Payload)
|
||||
return fmt.Sprintf("[GET /repos/issues/search][%d] issueSearchIssuesOK %s", 200, payload)
|
||||
}
|
||||
|
||||
func (o *IssueSearchIssuesOK) GetPayload() []*models.Issue {
|
||||
return o.Payload
|
||||
}
|
||||
|
||||
func (o *IssueSearchIssuesOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error {
|
||||
|
||||
// response payload
|
||||
if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@@ -1,50 +0,0 @@
|
||||
// Code generated by go-swagger; DO NOT EDIT.
|
||||
|
||||
package models
|
||||
|
||||
// This file was generated by the swagger tool.
|
||||
// Editing this file might prove futile when you re-run the swagger generate command
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/swag"
|
||||
)
|
||||
|
||||
// AddCollaboratorOption AddCollaboratorOption options when adding a user as a collaborator of a repository
|
||||
//
|
||||
// swagger:model AddCollaboratorOption
|
||||
type AddCollaboratorOption struct {
|
||||
|
||||
// permission
|
||||
Permission string `json:"permission,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates this add collaborator option
|
||||
func (m *AddCollaboratorOption) Validate(formats strfmt.Registry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ContextValidate validates this add collaborator option based on context it is used
|
||||
func (m *AddCollaboratorOption) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalBinary interface implementation
|
||||
func (m *AddCollaboratorOption) MarshalBinary() ([]byte, error) {
|
||||
if m == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return swag.WriteJSON(m)
|
||||
}
|
||||
|
||||
// UnmarshalBinary interface implementation
|
||||
func (m *AddCollaboratorOption) UnmarshalBinary(b []byte) error {
|
||||
var res AddCollaboratorOption
|
||||
if err := swag.ReadJSON(b, &res); err != nil {
|
||||
return err
|
||||
}
|
||||
*m = res
|
||||
return nil
|
||||
}
|
@@ -1,487 +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"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
transport "github.com/go-openapi/runtime/client"
|
||||
"github.com/go-openapi/strfmt"
|
||||
apiclient "src.opensuse.org/autogits/common/gitea-generated/client"
|
||||
"src.opensuse.org/autogits/common/gitea-generated/client/notification"
|
||||
"src.opensuse.org/autogits/common/gitea-generated/client/organization"
|
||||
"src.opensuse.org/autogits/common/gitea-generated/client/repository"
|
||||
"src.opensuse.org/autogits/common/gitea-generated/models"
|
||||
)
|
||||
|
||||
const PrPattern = "PR: %s/%s#%d"
|
||||
|
||||
const (
|
||||
// from Gitea
|
||||
// ReviewStateApproved pr is approved
|
||||
ReviewStateApproved models.ReviewStateType = "APPROVED"
|
||||
// ReviewStatePending pr state is pending
|
||||
ReviewStatePending models.ReviewStateType = "PENDING"
|
||||
// ReviewStateComment is a comment review
|
||||
ReviewStateComment models.ReviewStateType = "COMMENT"
|
||||
// ReviewStateRequestChanges changes for pr are requested
|
||||
ReviewStateRequestChanges models.ReviewStateType = "REQUEST_CHANGES"
|
||||
// ReviewStateRequestReview review is requested from user
|
||||
ReviewStateRequestReview models.ReviewStateType = "REQUEST_REVIEW"
|
||||
// ReviewStateUnknown state of pr is unknown
|
||||
ReviewStateUnknown models.ReviewStateType = ""
|
||||
)
|
||||
|
||||
type GiteaTransport struct {
|
||||
transport *transport.Runtime
|
||||
client *apiclient.GiteaAPI
|
||||
}
|
||||
|
||||
func AllocateGiteaTransport(host string) *GiteaTransport {
|
||||
var r GiteaTransport
|
||||
|
||||
r.transport = transport.New(host, apiclient.DefaultBasePath, [](string){"https"})
|
||||
r.transport.DefaultAuthentication = transport.BearerToken(giteaToken)
|
||||
|
||||
r.client = apiclient.New(r.transport, nil)
|
||||
|
||||
return &r
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) GetPullRequestAndReviews(org, project string, num int64) (*models.PullRequest, []*models.PullReview, error) {
|
||||
pr, err := gitea.client.Repository.RepoGetPullRequest(
|
||||
repository.NewRepoGetPullRequestParams().
|
||||
WithDefaults().
|
||||
WithOwner(org).
|
||||
WithRepo(project).
|
||||
WithIndex(num),
|
||||
gitea.transport.DefaultAuthentication,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
limit := int64(1000)
|
||||
reviews, err := gitea.client.Repository.RepoListPullReviews(
|
||||
repository.NewRepoListPullReviewsParams().
|
||||
WithDefaults().
|
||||
WithOwner(org).
|
||||
WithRepo(project).
|
||||
WithIndex(num).
|
||||
WithLimit(&limit),
|
||||
gitea.transport.DefaultAuthentication,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return pr.Payload, reviews.Payload, nil
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) GetPullNotifications(since *time.Time) ([]*models.NotificationThread, error) {
|
||||
bigLimit := int64(100000)
|
||||
|
||||
params := notification.NewNotifyGetListParams().
|
||||
WithDefaults().
|
||||
WithSubjectType([]string{"Pull"}).
|
||||
WithStatusTypes([]string{"unread"}).
|
||||
WithLimit(&bigLimit)
|
||||
|
||||
if since != nil {
|
||||
s := strfmt.DateTime(*since)
|
||||
params.SetSince(&s)
|
||||
}
|
||||
|
||||
list, err := gitea.client.Notification.NotifyGetList(params, gitea.transport.DefaultAuthentication)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return list.Payload, nil
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) SetNotificationRead(notificationId int64) error {
|
||||
_, err := gitea.client.Notification.NotifyReadThread(
|
||||
notification.NewNotifyReadThreadParams().
|
||||
WithDefaults().
|
||||
WithID(fmt.Sprint(notificationId)),
|
||||
gitea.transport.DefaultAuthentication,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error setting notification: %d. Err: %w", notificationId, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) GetOrganization(orgName string) (*models.Organization, error) {
|
||||
org, err := gitea.client.Organization.OrgGet(
|
||||
organization.NewOrgGetParams().WithOrg(orgName),
|
||||
gitea.transport.DefaultAuthentication,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error fetching org: '%s' data. Err: %w", orgName, err)
|
||||
}
|
||||
return org.Payload, nil
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) GetOrganizationRepositories(orgName string) ([]*models.Repository, error) {
|
||||
var page int64
|
||||
repos := make([]*models.Repository, 0, 100)
|
||||
|
||||
page = 1
|
||||
for {
|
||||
ret, err := gitea.client.Organization.OrgListRepos(
|
||||
organization.NewOrgListReposParams().WithOrg(orgName).WithPage(&page),
|
||||
gitea.transport.DefaultAuthentication,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error retrieving repository list for org: '%s'. Err: %w", orgName, err)
|
||||
}
|
||||
|
||||
if len(ret.Payload) == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
repos = append(repos, ret.Payload...)
|
||||
page++
|
||||
}
|
||||
|
||||
return repos, nil
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) CreateRepositoryIfNotExist(git *GitHandler, org Organization, repoName string) (*models.Repository, error) {
|
||||
repo, err := gitea.client.Repository.RepoGet(
|
||||
repository.NewRepoGetParams().WithDefaults().WithOwner(org.Username).WithRepo(repoName),
|
||||
gitea.transport.DefaultAuthentication)
|
||||
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *repository.RepoGetNotFound:
|
||||
repo, err := gitea.client.Organization.CreateOrgRepo(
|
||||
organization.NewCreateOrgRepoParams().WithDefaults().WithBody(
|
||||
&models.CreateRepoOption{
|
||||
AutoInit: false,
|
||||
Name: &repoName,
|
||||
ObjectFormatName: models.CreateRepoOptionObjectFormatNameSha256,
|
||||
},
|
||||
).WithOrg(org.Username),
|
||||
nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *organization.CreateOrgRepoCreated:
|
||||
// weird, but ok, repo created
|
||||
default:
|
||||
return nil, fmt.Errorf("error creating repo '%s' under '%s'. Err: %w", repoName, org.Username, err)
|
||||
}
|
||||
}
|
||||
|
||||
// initialize repository
|
||||
if err = os.Mkdir(filepath.Join(git.GitPath, DefaultGitPrj), 0700); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = git.GitExec(DefaultGitPrj, "init", "--object-format="+repo.Payload.ObjectFormatName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = git.GitExec(DefaultGitPrj, "checkout", "-b", repo.Payload.DefaultBranch); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
readmeFilename := filepath.Join(git.GitPath, DefaultGitPrj, "README.md")
|
||||
{
|
||||
file, _ := os.Create(readmeFilename)
|
||||
defer file.Close()
|
||||
|
||||
io.WriteString(file, ReadmeBoilerplate)
|
||||
}
|
||||
if err = git.GitExec(DefaultGitPrj, "add", "README.md"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = git.GitExec(DefaultGitPrj, "commit", "-m", "Automatic devel project creation"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = git.GitExec(DefaultGitPrj, "remote", "add", "origin", repo.Payload.SSHURL); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return repo.Payload, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("cannot fetch repo data for '%s' / '%s' : %w", org.Username, repoName, err)
|
||||
}
|
||||
}
|
||||
|
||||
return repo.Payload, nil
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) CreatePullRequest(repo *models.Repository, srcId, targetId, title, body string) (*models.PullRequest, error) {
|
||||
prOptions := models.CreatePullRequestOption{
|
||||
Base: repo.DefaultBranch,
|
||||
Head: srcId,
|
||||
Title: title,
|
||||
Body: body,
|
||||
}
|
||||
|
||||
pr, err := gitea.client.Repository.RepoCreatePullRequest(
|
||||
repository.
|
||||
NewRepoCreatePullRequestParams().
|
||||
WithDefaults().
|
||||
WithOwner(repo.Owner.UserName).
|
||||
WithRepo(repo.Name).
|
||||
WithBody(&prOptions),
|
||||
gitea.transport.DefaultAuthentication,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot create pull request. %w", err)
|
||||
}
|
||||
|
||||
return pr.GetPayload(), nil
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) RequestReviews(pr *models.PullRequest, reviewer string) ([]*models.PullReview, error) {
|
||||
reviewOptions := models.PullReviewRequestOptions{
|
||||
Reviewers: []string{reviewer},
|
||||
}
|
||||
|
||||
review, err := gitea.client.Repository.RepoCreatePullReviewRequests(
|
||||
repository.
|
||||
NewRepoCreatePullReviewRequestsParams().
|
||||
WithOwner(pr.Base.Repo.Owner.UserName).
|
||||
WithRepo(pr.Base.Repo.Name).
|
||||
WithIndex(pr.Index).
|
||||
WithBody(&reviewOptions),
|
||||
gitea.transport.DefaultAuthentication,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot create pull request: %w", err)
|
||||
}
|
||||
|
||||
return review.GetPayload(), nil
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) IsReviewed(pr *models.PullRequest) (bool, error) {
|
||||
// TODO: get review from project git
|
||||
reviewers := pr.RequestedReviewers
|
||||
var page, limit int64
|
||||
var reviews []*models.PullReview
|
||||
page = 0
|
||||
limit = 20
|
||||
for {
|
||||
res, err := gitea.client.Repository.RepoListPullReviews(
|
||||
repository.NewRepoListPullReviewsParams().
|
||||
WithOwner(pr.Base.Repo.Owner.UserName).
|
||||
WithRepo(pr.Base.Repo.Name).
|
||||
WithPage(&page).
|
||||
WithLimit(&limit),
|
||||
gitea.transport.DefaultAuthentication)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if res.IsSuccess() {
|
||||
r := res.Payload
|
||||
|
||||
if reviews == nil {
|
||||
reviews = r
|
||||
} else {
|
||||
reviews = append(reviews, r...)
|
||||
}
|
||||
|
||||
if len(r) < int(limit) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slices.Reverse(reviews)
|
||||
|
||||
for _, review := range reviews {
|
||||
if review.Stale || review.Dismissed {
|
||||
continue
|
||||
}
|
||||
|
||||
next_review:
|
||||
for i, reviewer := range reviewers {
|
||||
if review.User.UserName == reviewer.UserName {
|
||||
switch review.State {
|
||||
case ReviewStateApproved:
|
||||
reviewers = slices.Delete(reviewers, i, i)
|
||||
break next_review
|
||||
case ReviewStateRequestChanges:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return len(reviewers) == 0, nil
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) AddReviewComment(pr *models.PullRequest, state models.ReviewStateType, comment string) (*models.PullReview, error) {
|
||||
c, err := gitea.client.Repository.RepoCreatePullReview(
|
||||
repository.NewRepoCreatePullReviewParams().
|
||||
WithDefaults().
|
||||
WithOwner(pr.Base.Repo.Owner.UserName).
|
||||
WithRepo(pr.Base.Repo.Name).
|
||||
WithIndex(pr.Index).
|
||||
WithBody(&models.CreatePullReviewOptions{
|
||||
Event: state,
|
||||
Body: comment,
|
||||
}),
|
||||
gitea.transport.DefaultAuthentication,
|
||||
)
|
||||
/*
|
||||
c, err := client.Repository.RepoSubmitPullReview(
|
||||
repository.NewRepoSubmitPullReviewParams().
|
||||
WithDefaults().
|
||||
WithOwner(pr.Base.Repo.Owner.UserName).
|
||||
WithRepo(pr.Base.Repo.Name).
|
||||
WithIndex(pr.Index).
|
||||
WithID(review.ID).
|
||||
WithBody(&models.SubmitPullReviewOptions{
|
||||
Event: state,
|
||||
Body: comment,
|
||||
}),
|
||||
transport.DefaultAuthentication,
|
||||
)
|
||||
*/
|
||||
|
||||
/* c, err := client.Issue.IssueCreateComment(
|
||||
issue.NewIssueCreateCommentParams().
|
||||
WithDefaults().
|
||||
WithOwner(pr.Base.Repo.Owner.UserName).
|
||||
WithRepo(pr.Base.Repo.Name).
|
||||
WithIndex(pr.Index).
|
||||
WithBody(&models.CreateIssueCommentOption{
|
||||
Body: &comment,
|
||||
}),
|
||||
transport.DefaultAuthentication)
|
||||
*/
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.Payload, nil
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) GetAssociatedPrjGitPR(pr *PullRequestWebhookEvent) (*models.PullRequest, error) {
|
||||
var page, maxSize int64
|
||||
page = 1
|
||||
maxSize = 10000
|
||||
state := "open"
|
||||
prs, err := gitea.client.Repository.RepoListPullRequests(
|
||||
repository.
|
||||
NewRepoListPullRequestsParams().
|
||||
WithDefaults().
|
||||
WithOwner(pr.Repository.Owner.Username).
|
||||
WithRepo(DefaultGitPrj).
|
||||
WithState(&state).
|
||||
WithLimit(&maxSize).
|
||||
WithPage(&page),
|
||||
gitea.transport.DefaultAuthentication)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot fetch PR list for %s / %s : %w", pr.Repository.Owner.Username, pr.Repository.Name, err)
|
||||
}
|
||||
|
||||
prLine := fmt.Sprintf(PrPattern, pr.Repository.Owner.Username, pr.Repository.Name, pr.Number)
|
||||
// h.StdLogger.Printf("attemping to match line: '%s'\n", prLine)
|
||||
|
||||
// payload_processing:
|
||||
for _, pr := range prs.Payload {
|
||||
lines := strings.Split(pr.Body, "\n")
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) == prLine {
|
||||
return pr, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) GetRepositoryFileContent(repo *models.Repository, hash, path string) ([]byte, error) {
|
||||
var retData []byte
|
||||
|
||||
dataOut := writeFunc(func(data []byte) (int, error) {
|
||||
if len(data) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
retData = data
|
||||
return len(data), nil
|
||||
})
|
||||
_, err := gitea.client.Repository.RepoGetRawFile(
|
||||
repository.NewRepoGetRawFileParams().
|
||||
WithOwner(repo.Owner.UserName).
|
||||
WithRepo(repo.Name).
|
||||
WithFilepath(path).
|
||||
WithRef(&hash),
|
||||
gitea.transport.DefaultAuthentication,
|
||||
dataOut,
|
||||
repository.WithContentTypeApplicationOctetStream,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return retData, nil
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) GetPullRequestFileContent(pr *models.PullRequest, path string) ([]byte, error) {
|
||||
return gitea.GetRepositoryFileContent(pr.Head.Repo, pr.Head.Sha, path)
|
||||
}
|
||||
|
||||
func (gitea *GiteaTransport) GetRecentCommits(org, repo, branch string, commitNo int64) ([]*models.Commit, error) {
|
||||
not := false
|
||||
var page int64
|
||||
page = 1
|
||||
commits, err := gitea.client.Repository.RepoGetAllCommits(
|
||||
repository.NewRepoGetAllCommitsParams().
|
||||
WithOwner(org).
|
||||
WithRepo(repo).
|
||||
WithSha(&branch).
|
||||
WithPage(&page).
|
||||
WithStat(¬).
|
||||
WithFiles(¬).
|
||||
WithVerification(¬).
|
||||
WithLimit(&commitNo),
|
||||
gitea.transport.DefaultAuthentication,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return commits.Payload, nil
|
||||
}
|
@@ -1,35 +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"
|
||||
"io"
|
||||
"log"
|
||||
)
|
||||
|
||||
var id uint
|
||||
|
||||
func CreateStdoutLogger(stdout, stderr io.Writer) (*log.Logger, *log.Logger) {
|
||||
id++
|
||||
idStr := fmt.Sprintf("[%d] ", id)
|
||||
stdLogger := log.New(stdout, idStr, log.Lmsgprefix)
|
||||
errLogger := log.New(stderr, idStr, log.Lmsgprefix)
|
||||
return stdLogger, errLogger
|
||||
}
|
@@ -1,504 +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 (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"slices"
|
||||
)
|
||||
|
||||
type ObsClient struct {
|
||||
baseUrl *url.URL
|
||||
client *http.Client
|
||||
user, password string
|
||||
cookie string
|
||||
|
||||
HomeProject string
|
||||
}
|
||||
|
||||
func NewObsClient(host string) (*ObsClient, error) {
|
||||
baseUrl, err := url.Parse("https://" + host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ObsClient{
|
||||
baseUrl: baseUrl,
|
||||
client: &http.Client{},
|
||||
user: obsUser,
|
||||
password: obsPassword,
|
||||
|
||||
HomeProject: fmt.Sprintf("home:%s", obsUser),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type RepositoryPathMeta struct {
|
||||
Project string `xml:"project,attr"`
|
||||
Repository string `xml:"repository,attr"`
|
||||
}
|
||||
|
||||
type RepositoryMeta struct {
|
||||
Name string `xml:"name,attr"`
|
||||
BuildTrigger string `xml:"rebuild,attr"`
|
||||
BlockMode string `xml:"block"`
|
||||
LinkedBuild string `xml:"linkedbuild"`
|
||||
Archs []string `xml:"arch"`
|
||||
Paths []RepositoryPathMeta `xml:"path"`
|
||||
}
|
||||
|
||||
type Flags struct {
|
||||
Contents string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type ProjectMeta struct {
|
||||
XMLName xml.Name `xml:"project"`
|
||||
Name string `xml:"name,attr"`
|
||||
Title string `xml:"title"`
|
||||
Description string `xml:"description"`
|
||||
Url string `xml:"url"`
|
||||
ScmSync string `xml:"scmsync"`
|
||||
Repositories []RepositoryMeta `xml:"repository"`
|
||||
|
||||
BuildFlags Flags `xml:"build"`
|
||||
PublicFlags Flags `xml:"publish"`
|
||||
DebugFlags Flags `xml:"debuginfo"`
|
||||
UseForBuild Flags `xml:"useforbuild"`
|
||||
}
|
||||
|
||||
func parseProjectMeta(data []byte) (*ProjectMeta, error) {
|
||||
var meta ProjectMeta
|
||||
err := xml.Unmarshal(data, &meta)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &meta, nil
|
||||
}
|
||||
|
||||
func (c *ObsClient) GetProjectMeta(project string) (*ProjectMeta, error) {
|
||||
req, err := http.NewRequest("GET", c.baseUrl.JoinPath("source", project, "_meta").String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SetBasicAuth(c.user, c.password)
|
||||
log.Printf("request: %#v", *req.URL)
|
||||
log.Printf("headers: %#v", req.Header)
|
||||
res, err := c.client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch res.StatusCode {
|
||||
case 200:
|
||||
break
|
||||
case 404:
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("Unexpected return code: %d", res.StatusCode)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return parseProjectMeta(data)
|
||||
}
|
||||
|
||||
func ObsSafeProjectName(prjname string) string {
|
||||
if len(prjname) < 1 {
|
||||
return prjname
|
||||
} else if len(prjname) > 200 {
|
||||
prjname = prjname[:199]
|
||||
}
|
||||
|
||||
switch prjname[0] {
|
||||
case '_', '.', ':':
|
||||
prjname = "X" + prjname[1:]
|
||||
// no UTF-8 in OBS :(
|
||||
// prjname = "_" + prjname[1:]
|
||||
// case ':':
|
||||
// prjname = ":" + prjname[1:]
|
||||
// case '.':
|
||||
// prjname = "․" + prjname[1:]
|
||||
}
|
||||
|
||||
return prjname
|
||||
}
|
||||
|
||||
var ValidBlockModes []string
|
||||
var ValidPrjLinkModes []string
|
||||
var ValidTriggerModes []string
|
||||
|
||||
func (c *ObsClient) SetProjectMeta(meta *ProjectMeta) error {
|
||||
|
||||
for _, repo := range meta.Repositories {
|
||||
if len(repo.BlockMode) > 0 && !slices.Contains(ValidBlockModes, repo.BlockMode) {
|
||||
return fmt.Errorf("Invalid repository block mode: '%s'", repo.BlockMode)
|
||||
}
|
||||
if len(repo.BuildTrigger) > 0 && !slices.Contains(ValidTriggerModes, repo.BuildTrigger) {
|
||||
return fmt.Errorf("Invalid repository trigger mode: '%s'", repo.BuildTrigger)
|
||||
}
|
||||
if len(repo.LinkedBuild) > 0 && !slices.Contains(ValidPrjLinkModes, repo.LinkedBuild) {
|
||||
return fmt.Errorf("Invalid linked project rebuild mode: '%s'", repo.LinkedBuild)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("PUT", c.baseUrl.JoinPath("source", meta.Name, "_meta").String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.SetBasicAuth(c.user, c.password)
|
||||
xml, err := xml.Marshal(meta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Body = io.NopCloser(bytes.NewReader(xml))
|
||||
log.Printf("headers: %#v", req.Header)
|
||||
log.Printf("xml: %s", xml)
|
||||
res, err := c.client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch res.StatusCode {
|
||||
case 200:
|
||||
break
|
||||
default:
|
||||
return fmt.Errorf("Unexpected return code: %d", res.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ObsClient) DeleteProject(project string) error {
|
||||
req, err := http.NewRequest("DELETE", c.baseUrl.JoinPath("source", project).String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.SetBasicAuth(c.user, c.password)
|
||||
res, err := c.client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("Unexpected return code: %d", res.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
type PackageBuildStatus struct {
|
||||
Package string `xml:"package,attr"`
|
||||
Code string `xml:"code,attr"`
|
||||
Details string `xml:"details"`
|
||||
}
|
||||
|
||||
type BuildResult struct {
|
||||
Project string `xml:"project,attr"`
|
||||
Repository string `xml:"repository,attr"`
|
||||
Arch string `xml:"arch,attr"`
|
||||
Code string `xml:"code,attr"`
|
||||
Dirty bool `xml:"dirty,attr"`
|
||||
Status []PackageBuildStatus `xml:"status"`
|
||||
Binaries []BinaryList `xml:"binarylist"`
|
||||
}
|
||||
|
||||
type Binary struct {
|
||||
Size uint64 `xml:"size,attr"`
|
||||
Filename string `xml:"filename,attr"`
|
||||
Mtime uint64 `xml:"mtime,attr"`
|
||||
}
|
||||
|
||||
type BinaryList struct {
|
||||
Package string `xml:"package,attr"`
|
||||
Binary []Binary `xml:"binary"`
|
||||
}
|
||||
|
||||
type BuildResultList struct {
|
||||
XMLName xml.Name `xml:"resultlist"`
|
||||
Result []BuildResult `xml:"result"`
|
||||
}
|
||||
|
||||
func (r *BuildResultList) GetPackageList() []string {
|
||||
pkgList := make([]string, 0, 16)
|
||||
|
||||
for _, res := range r.Result {
|
||||
// TODO: enough to iterate over one result set?
|
||||
|
||||
for _, status := range res.Status {
|
||||
if !slices.Contains(pkgList, status.Package) {
|
||||
pkgList = append(pkgList, status.Package)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pkgList
|
||||
}
|
||||
|
||||
func (r *BuildResultList) BuildResultSummary() (success, finished bool) {
|
||||
if r == nil {
|
||||
return true, true
|
||||
}
|
||||
|
||||
finished = len(r.Result) > 0 && len(r.Result[0].Status) > 0
|
||||
success = finished
|
||||
|
||||
for _, resultSet := range r.Result {
|
||||
repoDetail, ok := ObsRepoStatusDetails[resultSet.Code]
|
||||
|
||||
if !ok {
|
||||
panic("Unknown repo result code: " + resultSet.Code)
|
||||
}
|
||||
|
||||
finished = repoDetail.Finished
|
||||
if !finished || resultSet.Dirty {
|
||||
return
|
||||
}
|
||||
|
||||
for _, result := range resultSet.Status {
|
||||
detail, ok := ObsBuildStatusDetails[result.Code]
|
||||
|
||||
if !ok {
|
||||
panic("Unknown result code: " + result.Code)
|
||||
}
|
||||
|
||||
finished = finished && detail.Finished
|
||||
success = success && detail.Success
|
||||
|
||||
if !finished {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var ObsBuildStatusDetails map[string]ObsBuildStatusDetail
|
||||
var ObsRepoStatusDetails map[string]ObsBuildStatusDetail
|
||||
|
||||
type ObsBuildStatusDetail struct {
|
||||
Code string
|
||||
Description string
|
||||
Finished bool
|
||||
Success bool
|
||||
}
|
||||
|
||||
func init() {
|
||||
ValidTriggerModes = []string{"transitive", "direct", "local"}
|
||||
ValidBlockModes = []string{"all", "local", "never"}
|
||||
ValidPrjLinkModes = []string{"off", "localdep", "alldirect", "all"}
|
||||
|
||||
ObsBuildStatusDetails = make(map[string]ObsBuildStatusDetail)
|
||||
ObsRepoStatusDetails = make(map[string]ObsBuildStatusDetail)
|
||||
|
||||
// package status
|
||||
ObsBuildStatusDetails["succeeded"] = ObsBuildStatusDetail{
|
||||
Code: "succeeded",
|
||||
Description: "Package has built successfully and can be used to build further packages.",
|
||||
Finished: true,
|
||||
Success: true,
|
||||
}
|
||||
ObsBuildStatusDetails["failed"] = ObsBuildStatusDetail{
|
||||
Code: "failed",
|
||||
Description: "The package does not build successfully. No packages have been created. Packages that depend on this package will be built using any previously created packages, if they exist.",
|
||||
Finished: true,
|
||||
Success: false,
|
||||
}
|
||||
ObsBuildStatusDetails["unresolvable"] = ObsBuildStatusDetail{
|
||||
Code: "unresolvable",
|
||||
Description: "The build can not begin, because required packages are either missing or not explicitly defined.",
|
||||
Finished: true,
|
||||
Success: false,
|
||||
}
|
||||
ObsBuildStatusDetails["broken"] = ObsBuildStatusDetail{
|
||||
Code: "broken",
|
||||
Description: "The sources either contain no build description (e.g. specfile), automatic source processing failed or a merge conflict does exist.",
|
||||
Finished: true,
|
||||
Success: false,
|
||||
}
|
||||
ObsBuildStatusDetails["blocked"] = ObsBuildStatusDetail{
|
||||
Code: "blocked",
|
||||
Description: "This package waits for other packages to be built. These can be in the same or other projects.",
|
||||
Finished: false,
|
||||
}
|
||||
ObsBuildStatusDetails["scheduled"] = ObsBuildStatusDetail{
|
||||
Code: "scheduled",
|
||||
Description: "A package has been marked for building, but the build has not started yet.",
|
||||
Finished: false,
|
||||
}
|
||||
ObsBuildStatusDetails["dispatching"] = ObsBuildStatusDetail{
|
||||
Code: "dispatching",
|
||||
Description: "A package is being copied to a build host. This is an intermediate state before building.",
|
||||
Finished: false,
|
||||
}
|
||||
ObsBuildStatusDetails["building"] = ObsBuildStatusDetail{
|
||||
Code: "building",
|
||||
Description: "The package is currently being built.",
|
||||
Finished: false,
|
||||
}
|
||||
ObsBuildStatusDetails["signing"] = ObsBuildStatusDetail{
|
||||
Code: "signing",
|
||||
Description: "The package has been built successfully and is assigned to get signed.",
|
||||
Finished: false,
|
||||
}
|
||||
ObsBuildStatusDetails["finished"] = ObsBuildStatusDetail{
|
||||
Code: "finished",
|
||||
Description: "The package has been built and signed, but has not yet been picked up by the scheduler. This is an intermediate state prior to 'succeeded' or 'failed'.",
|
||||
Finished: false,
|
||||
}
|
||||
ObsBuildStatusDetails["disabled"] = ObsBuildStatusDetail{
|
||||
Code: "disabled",
|
||||
Description: "The package has been disabled from building in project or package metadata. Packages that depend on this package will be built using any previously created packages, if they still exist.",
|
||||
Finished: true,
|
||||
Success: true,
|
||||
}
|
||||
ObsBuildStatusDetails["excluded"] = ObsBuildStatusDetail{
|
||||
Code: "excluded",
|
||||
Description: "The package build has been disabled in package build description (for example in the .spec file) or does not provide a matching build description for the target.",
|
||||
Finished: true,
|
||||
Success: true,
|
||||
}
|
||||
ObsBuildStatusDetails["locked"] = ObsBuildStatusDetail{
|
||||
Code: "locked",
|
||||
Description: "The package is frozen",
|
||||
Finished: true,
|
||||
Success: true,
|
||||
}
|
||||
ObsBuildStatusDetails["unknown"] = ObsBuildStatusDetail{
|
||||
Code: "unknown",
|
||||
Description: "The scheduler has not yet evaluated this package. Should be a short intermediate state for new packages.",
|
||||
Finished: false,
|
||||
}
|
||||
|
||||
// repo status
|
||||
ObsRepoStatusDetails["published"] = ObsBuildStatusDetail{
|
||||
Code: "published",
|
||||
Description: "Repository has been published",
|
||||
Finished: true,
|
||||
}
|
||||
ObsRepoStatusDetails["publishing"] = ObsBuildStatusDetail{
|
||||
Code: "publishing",
|
||||
Description: "Repository is being created right now",
|
||||
Finished: true,
|
||||
}
|
||||
ObsRepoStatusDetails["unpublished"] = ObsBuildStatusDetail{
|
||||
Code: "unpublished",
|
||||
Description: "Build finished, but repository publishing is disabled",
|
||||
Finished: true,
|
||||
}
|
||||
ObsRepoStatusDetails["building"] = ObsBuildStatusDetail{
|
||||
Code: "building",
|
||||
Description: "Build jobs exist for the repository",
|
||||
Finished: false,
|
||||
}
|
||||
ObsRepoStatusDetails["finished"] = ObsBuildStatusDetail{
|
||||
Code: "finished",
|
||||
Description: "Build jobs have been processed, new repository is not yet created",
|
||||
Finished: true,
|
||||
}
|
||||
ObsRepoStatusDetails["blocked"] = ObsBuildStatusDetail{
|
||||
Code: "blocked",
|
||||
Description: "No build possible at the moment, waiting for jobs in other repositories",
|
||||
Finished: false,
|
||||
}
|
||||
ObsRepoStatusDetails["broken"] = ObsBuildStatusDetail{
|
||||
Code: "broken",
|
||||
Description: "The repository setup is broken, build or publish not possible",
|
||||
Finished: true,
|
||||
}
|
||||
ObsRepoStatusDetails["scheduling"] = ObsBuildStatusDetail{
|
||||
Code: "scheduling",
|
||||
Description: "The repository state is being calculated right now",
|
||||
Finished: false,
|
||||
}
|
||||
}
|
||||
|
||||
func parseBuildResults(data []byte) (*BuildResultList, error) {
|
||||
result := BuildResultList{}
|
||||
err := xml.Unmarshal(data, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
type ObsProjectNotFound struct {
|
||||
Project string
|
||||
}
|
||||
|
||||
func (obs ObsProjectNotFound) Error() string {
|
||||
return fmt.Sprintf("OBS project is not found: %s", obs.Project)
|
||||
}
|
||||
|
||||
func (c *ObsClient) BuildStatus(project string, packages ...string) (*BuildResultList, error) {
|
||||
u := c.baseUrl.JoinPath("build", project, "_result")
|
||||
query := u.Query()
|
||||
query.Add("view", "status")
|
||||
query.Add("view", "binarylist")
|
||||
query.Add("multibuild", "1")
|
||||
if len(packages) > 0 {
|
||||
query.Add("lastbuild", "1")
|
||||
for _, pkg := range packages {
|
||||
query.Add("package", pkg)
|
||||
}
|
||||
}
|
||||
u.RawQuery = query.Encode()
|
||||
req, err := http.NewRequest("GET", u.String(), nil)
|
||||
log.Print(u.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.SetBasicAuth(c.user, c.password)
|
||||
res, err := c.client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch res.StatusCode {
|
||||
case 200:
|
||||
break
|
||||
case 404:
|
||||
return nil, ObsProjectNotFound{project}
|
||||
default:
|
||||
return nil, fmt.Errorf("Unexpected return code: %d", res.StatusCode)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseBuildResults(data)
|
||||
}
|
17
common/Makefile
Normal file
17
common/Makefile
Normal file
@@ -0,0 +1,17 @@
|
||||
all: build
|
||||
|
||||
api.json::
|
||||
curl -o api.json https://src.opensuse.org/swagger.v1.json
|
||||
|
||||
gitea-generated/client/gitea_api_client.go: api.json
|
||||
[ -d gitea-generated ] || mkdir gitea-generated
|
||||
podman run --rm -v $$(pwd)/..:/api ghcr.io/go-swagger/go-swagger generate client -f /api/common/api.json -t /api/common/gitea-generated
|
||||
|
||||
swagger: gitea-generated/client/gitea_api_client.go
|
||||
|
||||
api:
|
||||
go generate
|
||||
|
||||
build: api
|
||||
go build
|
||||
|
File diff suppressed because it is too large
Load Diff
118
common/associated_pr_scanner.go
Normal file
118
common/associated_pr_scanner.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const PrPattern = "PR: %s/%s#%d"
|
||||
|
||||
type BasicPR struct {
|
||||
Org, Repo string
|
||||
Num int64
|
||||
}
|
||||
|
||||
var validOrgAndRepoRx *regexp.Regexp = regexp.MustCompile("^[A-Za-z0-9_\\.-]+$")
|
||||
|
||||
func parsePrLine(line string) (BasicPR, error) {
|
||||
var ret BasicPR
|
||||
trimmedLine := strings.TrimSpace(line)
|
||||
|
||||
// min size > 9 -> must fit all parameters in th PrPattern with at least one item per parameter
|
||||
if len(trimmedLine) < 9 || trimmedLine[0:4] != "PR: " {
|
||||
return ret, errors.New("Line too short")
|
||||
}
|
||||
|
||||
trimmedLine = trimmedLine[4:]
|
||||
org := strings.SplitN(trimmedLine, "/", 2)
|
||||
ret.Org = org[0]
|
||||
if len(org) != 2 {
|
||||
return ret, errors.New("missing / separator")
|
||||
}
|
||||
|
||||
repo := strings.SplitN(org[1], "#", 2)
|
||||
ret.Repo = repo[0]
|
||||
if len(repo) != 2 {
|
||||
return ret, errors.New("Missing # separator")
|
||||
}
|
||||
|
||||
// Gitea requires that each org and repo be [A-Za-z0-9_-]+
|
||||
var err error
|
||||
if ret.Num, err = strconv.ParseInt(repo[1], 10, 64); err != nil {
|
||||
return ret, errors.New("Invalid number")
|
||||
}
|
||||
|
||||
if !validOrgAndRepoRx.MatchString(repo[0]) || !validOrgAndRepoRx.MatchString(org[0]) {
|
||||
return ret, errors.New("Invalid repo or org character set")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func ExtractDescriptionAndPRs(data *bufio.Scanner) (string, []BasicPR) {
|
||||
prs := make([]BasicPR, 0, 1)
|
||||
var desc strings.Builder
|
||||
|
||||
for data.Scan() {
|
||||
line := data.Text()
|
||||
|
||||
pr, err := parsePrLine(line)
|
||||
if err != nil {
|
||||
desc.WriteString(line)
|
||||
desc.WriteByte('\n')
|
||||
} else {
|
||||
prs = append(prs, pr)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.TrimSpace(desc.String()), prs
|
||||
}
|
||||
|
||||
func prToLine(writer io.Writer, pr BasicPR) {
|
||||
writer.Write([]byte("\n"))
|
||||
fmt.Fprintf(writer, PrPattern, pr.Org, pr.Repo, pr.Num)
|
||||
}
|
||||
|
||||
// returns:
|
||||
// <0 for a<b
|
||||
// >0 for a>b
|
||||
// =0 when equal
|
||||
func compareBasicPRs(a BasicPR, b BasicPR) int {
|
||||
if c := strings.Compare(a.Org, b.Org); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := strings.Compare(a.Repo, b.Repo); c != 0 {
|
||||
return c
|
||||
}
|
||||
|
||||
if a.Num > b.Num {
|
||||
return 1
|
||||
}
|
||||
if a.Num < b.Num {
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func AppendPRsToDescription(desc string, prs []BasicPR) string {
|
||||
var out strings.Builder
|
||||
|
||||
out.WriteString(strings.TrimSpace(desc))
|
||||
out.WriteString("\n")
|
||||
|
||||
slices.SortFunc(prs, compareBasicPRs)
|
||||
prs = slices.Compact(prs)
|
||||
|
||||
for _, pr := range prs {
|
||||
prToLine(&out, pr)
|
||||
}
|
||||
|
||||
return out.String()
|
||||
}
|
149
common/associated_pr_scanner_test.go
Normal file
149
common/associated_pr_scanner_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
package common_test
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"src.opensuse.org/autogits/common"
|
||||
)
|
||||
|
||||
func newStringScanner(s string) *bufio.Scanner {
|
||||
return bufio.NewScanner(strings.NewReader(s))
|
||||
}
|
||||
|
||||
func TestAssociatedPRScanner(t *testing.T) {
|
||||
testTable := []struct {
|
||||
name string
|
||||
input string
|
||||
prs []common.BasicPR
|
||||
desc string
|
||||
}{
|
||||
{
|
||||
"No PRs",
|
||||
"",
|
||||
[]common.BasicPR{},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"Single PRs",
|
||||
"Some header of the issue\n\nFollowed by some description\n\nPR: test/foo#4\n",
|
||||
[]common.BasicPR{{Org: "test", Repo: "foo", Num: 4}},
|
||||
"Some header of the issue\n\nFollowed by some description",
|
||||
},
|
||||
{
|
||||
"Multiple PRs",
|
||||
"Some header of the issue\n\nFollowed by some description\nPR: test/foo#4\n\nPR: test/goo#5\n",
|
||||
[]common.BasicPR{
|
||||
{Org: "test", Repo: "foo", Num: 4},
|
||||
{Org: "test", Repo: "goo", Num: 5},
|
||||
},
|
||||
"Some header of the issue\n\nFollowed by some description",
|
||||
},
|
||||
{
|
||||
"Multiple PRs with whitespace",
|
||||
"Some header of the issue\n\n\tPR: test/goo#5\n\n Followed by some description\n \t PR: test/foo#4\n",
|
||||
[]common.BasicPR{
|
||||
{Org: "test", Repo: "foo", Num: 4},
|
||||
{Org: "test", Repo: "goo", Num: 5},
|
||||
},
|
||||
"Some header of the issue\n\n\n Followed by some description",
|
||||
},
|
||||
{
|
||||
"Multiple PRs with missing names and other special cases to ignore",
|
||||
"Some header of the issue\n\n\n\t PR: foobar#5 \n\t PR: rd/goo5 \n\t PR: test/#5 \n" +
|
||||
"\t PR: /goo#5 \n\t PR: test/goo# \n\t PR: test / goo # 10 \n\tPR: test/gool# 10 \n" +
|
||||
"\t PR: test/goo#5 \n\t\n Followed by some description\n\t PR: test/foo#4 \n\t\n\n",
|
||||
[]common.BasicPR{
|
||||
{
|
||||
Org: "test",
|
||||
Repo: "foo",
|
||||
Num: 4,
|
||||
},
|
||||
{
|
||||
Org: "test",
|
||||
Repo: "goo",
|
||||
Num: 5,
|
||||
},
|
||||
},
|
||||
"Some header of the issue\n\n\n\t PR: foobar#5 \n\t PR: rd/goo5 \n\t PR: test/#5 \n" +
|
||||
"\t PR: /goo#5 \n\t PR: test/goo# \n\t PR: test / goo # 10 \n\tPR: test/gool# 10 \n" +
|
||||
"\t\n Followed by some description",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testTable {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
desc, prs := common.ExtractDescriptionAndPRs(newStringScanner(test.input))
|
||||
if len(prs) != len(test.prs) {
|
||||
t.Error("Unexpected length:", len(prs), "expected:", len(test.prs))
|
||||
return
|
||||
}
|
||||
|
||||
for _, p := range test.prs {
|
||||
if !slices.Contains(prs, p) {
|
||||
t.Error("missing expected PR", p)
|
||||
}
|
||||
}
|
||||
|
||||
if desc != test.desc {
|
||||
t.Error("Desc output", len(desc), "!=", len(test.desc), ":", desc)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendingPRsToDescription(t *testing.T) {
|
||||
testTable := []struct {
|
||||
name string
|
||||
desc string
|
||||
PRs []common.BasicPR
|
||||
output string
|
||||
}{
|
||||
{
|
||||
"Append single PR to end of description",
|
||||
"something",
|
||||
[]common.BasicPR{
|
||||
{Org: "a", Repo: "b", Num: 100},
|
||||
},
|
||||
"something\n\nPR: a/b#100",
|
||||
},
|
||||
{
|
||||
"Append multiple PR to end of description",
|
||||
"something",
|
||||
[]common.BasicPR{
|
||||
{Org: "a1", Repo: "b", Num: 100},
|
||||
{Org: "a1", Repo: "c", Num: 100},
|
||||
{Org: "a1", Repo: "c", Num: 101},
|
||||
{Org: "b", Repo: "b", Num: 100},
|
||||
{Org: "c", Repo: "b", Num: 100},
|
||||
},
|
||||
"something\n\nPR: a1/b#100\nPR: a1/c#100\nPR: a1/c#101\nPR: b/b#100\nPR: c/b#100",
|
||||
},
|
||||
{
|
||||
"Append multiple sorted PR to end of description and remove dups",
|
||||
"something",
|
||||
[]common.BasicPR{
|
||||
{Org: "a1", Repo: "c", Num: 101},
|
||||
{Org: "a1", Repo: "c", Num: 100},
|
||||
{Org: "c", Repo: "b", Num: 100},
|
||||
{Org: "b", Repo: "b", Num: 100},
|
||||
{Org: "a1", Repo: "c", Num: 101},
|
||||
{Org: "a1", Repo: "c", Num: 101},
|
||||
{Org: "a1", Repo: "b", Num: 100},
|
||||
},
|
||||
"something\n\nPR: a1/b#100\nPR: a1/c#100\nPR: a1/c#101\nPR: b/b#100\nPR: c/b#100",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testTable {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
d := common.AppendPRsToDescription(test.desc, test.PRs)
|
||||
if d != test.output {
|
||||
t.Error(len(d), "vs", len(test.output))
|
||||
t.Error("unpected output", d)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
252
common/config.go
Normal file
252
common/config.go
Normal file
@@ -0,0 +1,252 @@
|
||||
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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/tailscale/hujson"
|
||||
)
|
||||
|
||||
//go:generate mockgen -source=config.go -destination=mock/config.go -typed
|
||||
|
||||
const (
|
||||
ProjectConfigFile = "workflow.config"
|
||||
StagingConfigFile = "staging.config"
|
||||
)
|
||||
|
||||
type ConfigFile struct {
|
||||
GitProjectNames []string
|
||||
}
|
||||
|
||||
type ReviewGroup struct {
|
||||
Name string
|
||||
Reviewers []string
|
||||
}
|
||||
|
||||
type QAConfig struct {
|
||||
Name string
|
||||
Origin string
|
||||
}
|
||||
|
||||
type AutogitConfig struct {
|
||||
Workflows []string // [pr, direct, test]
|
||||
Organization string
|
||||
GitProjectName string // Organization/GitProjectName.git is PrjGit
|
||||
Branch string // branch name of PkgGit that aligns with PrjGit submodules
|
||||
Reviewers []string // only used by `pr` workflow
|
||||
ReviewGroups []ReviewGroup
|
||||
Committers []string // group in addition to Reviewers and Maintainers that can order the bot around, mostly as helper for factory-maintainers
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type AutogitConfigs []*AutogitConfig
|
||||
|
||||
func ReadConfig(reader io.Reader) (*ConfigFile, error) {
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error reading config data: %w", err)
|
||||
}
|
||||
|
||||
config := ConfigFile{}
|
||||
data, err = hujson.Standardize(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to parse json: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(data, &config.GitProjectNames); err != nil {
|
||||
return nil, fmt.Errorf("Error parsing Git Project paths: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func ReadConfigFile(filename string) (*ConfigFile, 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)
|
||||
}
|
||||
|
||||
type GiteaFileContentAndRepoFetcher interface {
|
||||
GiteaFileContentReader
|
||||
GiteaRepoFetcher
|
||||
}
|
||||
|
||||
func UnmarshalWorkflowConfig(data []byte) (*AutogitConfig, error) {
|
||||
var config AutogitConfig
|
||||
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 workflow config file: %s: %w", string(data), err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
func ReadWorkflowConfig(gitea GiteaFileContentAndRepoFetcher, git_project string) (*AutogitConfig, error) {
|
||||
hash := strings.Split(git_project, "#")
|
||||
branch := ""
|
||||
if len(hash) > 1 {
|
||||
branch = hash[1]
|
||||
}
|
||||
|
||||
a := strings.Split(hash[0], "/")
|
||||
prjGitRepo := DefaultGitPrj
|
||||
switch len(a) {
|
||||
case 1:
|
||||
case 2:
|
||||
prjGitRepo = a[1]
|
||||
default:
|
||||
return nil, fmt.Errorf("Missing org/repo in projectgit: %s", git_project)
|
||||
}
|
||||
|
||||
data, _, err := gitea.GetRepositoryFileContent(a[0], prjGitRepo, branch, ProjectConfigFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error fetching 'workflow.config' for %s/%s#%s: %w", a[0], prjGitRepo, branch, err)
|
||||
}
|
||||
|
||||
config, err := UnmarshalWorkflowConfig(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(config.Organization) < 1 {
|
||||
config.Organization = a[0]
|
||||
}
|
||||
config.GitProjectName = a[0] + "/" + prjGitRepo
|
||||
if len(branch) == 0 {
|
||||
if r, err := gitea.GetRepository(a[0], prjGitRepo); err == nil {
|
||||
branch = r.DefaultBranch
|
||||
} else {
|
||||
return nil, fmt.Errorf("Failed to read workflow config in %s: %w", git_project, err)
|
||||
}
|
||||
}
|
||||
config.GitProjectName = config.GitProjectName + "#" + branch
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func ResolveWorkflowConfigs(gitea GiteaFileContentAndRepoFetcher, config *ConfigFile) (AutogitConfigs, error) {
|
||||
configs := make([]*AutogitConfig, 0, len(config.GitProjectNames))
|
||||
for _, git_project := range config.GitProjectNames {
|
||||
c, err := ReadWorkflowConfig(gitea, git_project)
|
||||
if err != nil {
|
||||
// can't sync, so ignore for now
|
||||
log.Println(err)
|
||||
} else {
|
||||
configs = append(configs, c)
|
||||
}
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
||||
|
||||
func (configs AutogitConfigs) GetPrjGitConfig(org, repo, branch string) *AutogitConfig {
|
||||
prjgit := org + "/" + repo + "#" + branch
|
||||
for _, c := range configs {
|
||||
if c.GitProjectName == prjgit {
|
||||
return c
|
||||
}
|
||||
if c.Organization == org && c.Branch == branch {
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (config *AutogitConfig) GetReviewGroupMembers(reviewer string) ([]string, error) {
|
||||
for _, g := range config.ReviewGroups {
|
||||
if g.Name == reviewer {
|
||||
return g.Reviewers, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("User " + reviewer + " not found as group reviewer for " + config.GitProjectName)
|
||||
}
|
||||
|
||||
func (config *AutogitConfig) GetPrjGit() (string, string, string) {
|
||||
org := config.Organization
|
||||
repo := DefaultGitPrj
|
||||
branch := "master"
|
||||
|
||||
a := strings.Split(config.GitProjectName, "/")
|
||||
if len(a[0]) > 0 {
|
||||
repo = strings.TrimSpace(a[0])
|
||||
}
|
||||
if len(a) == 2 {
|
||||
if a[0] = strings.TrimSpace(a[0]); len(a[0]) > 0 {
|
||||
org = a[0]
|
||||
}
|
||||
repo = strings.TrimSpace(a[1])
|
||||
}
|
||||
b := strings.Split(repo, "#")
|
||||
if len(b) == 2 {
|
||||
if b[0] = strings.TrimSpace(b[0]); len(b[0]) > 0 {
|
||||
repo = b[0]
|
||||
} else {
|
||||
repo = DefaultGitPrj
|
||||
}
|
||||
if b[1] = strings.TrimSpace(b[1]); len(b[1]) > 0 {
|
||||
branch = strings.TrimSpace(b[1])
|
||||
}
|
||||
}
|
||||
|
||||
return org, repo, branch
|
||||
}
|
||||
|
||||
func (config *AutogitConfig) GetRemoteBranch() string {
|
||||
return "origin_" + config.Branch
|
||||
}
|
||||
|
||||
type StagingConfig struct {
|
||||
ObsProject string
|
||||
RebuildAll bool
|
||||
CleanupDelay int // cleanup delay, in hours, for unmerged closed PRs (def: 48)
|
||||
|
||||
// if set, then only use pull request numbers as unique identifiers
|
||||
StagingProject string
|
||||
QA []QAConfig
|
||||
}
|
||||
|
||||
func ParseStagingConfig(data []byte) (*StagingConfig, error) {
|
||||
var staging StagingConfig
|
||||
data, err := hujson.Standardize(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
staging.CleanupDelay = 48
|
||||
if err := json.Unmarshal(data, &staging); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &staging, nil
|
||||
}
|
190
common/config_test.go
Normal file
190
common/config_test.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package common_test
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"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 TestProjectConfigMatcher(t *testing.T) {
|
||||
configs := common.AutogitConfigs{
|
||||
{
|
||||
Organization: "test",
|
||||
GitProjectName: "test/prjgit#main",
|
||||
},
|
||||
{
|
||||
Organization: "test",
|
||||
Branch: "main",
|
||||
GitProjectName: "test/prjgit#main",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
org string
|
||||
repo string
|
||||
branch string
|
||||
config int
|
||||
}{
|
||||
{
|
||||
name: "invalid match",
|
||||
org: "foo",
|
||||
repo: "bar",
|
||||
config: -1,
|
||||
},
|
||||
{
|
||||
name: "default branch",
|
||||
org: "test",
|
||||
repo: "foo",
|
||||
branch: "",
|
||||
config: 0,
|
||||
},
|
||||
{
|
||||
name: "main branch",
|
||||
org: "test",
|
||||
repo: "foo",
|
||||
branch: "main",
|
||||
config: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c := configs.GetPrjGitConfig(test.org, test.repo, test.branch)
|
||||
if test.config < 0 {
|
||||
if c != nil {
|
||||
t.Fatal("Expected nil. Got:", *c)
|
||||
}
|
||||
} else if config := configs[test.config]; c != config {
|
||||
t.Fatal("Expected", *config, "got", *c)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigWorkflowParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config_json string
|
||||
repo models.Repository
|
||||
}{
|
||||
{
|
||||
name: "Regular workflow file",
|
||||
config_json: `{
|
||||
"Workflows": ["direct", "pr"],
|
||||
"Organization": "testing",
|
||||
"ReviewGroups": [
|
||||
{
|
||||
"Name": "gnuman1",
|
||||
"Reviewers": ["adamm"]
|
||||
}
|
||||
]
|
||||
}`,
|
||||
repo: models.Repository{
|
||||
DefaultBranch: "master",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctl := gomock.NewController(t)
|
||||
gitea := mock_common.NewMockGiteaFileContentAndRepoFetcher(ctl)
|
||||
gitea.EXPECT().GetRepositoryFileContent("foo", "bar", "", "workflow.config").Return([]byte(test.config_json), "abc", nil)
|
||||
gitea.EXPECT().GetRepository("foo", "bar").Return(&test.repo, nil)
|
||||
|
||||
config, err := common.ReadWorkflowConfig(gitea, "foo/bar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if config.ManualMergeOnly != false {
|
||||
t.Fatal("This should be false")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectGitParser(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
prjgit string
|
||||
org string
|
||||
branch string
|
||||
res [3]string
|
||||
}{
|
||||
{
|
||||
name: "repo only",
|
||||
prjgit: "repo.git",
|
||||
org: "org",
|
||||
branch: "br",
|
||||
res: [3]string{"org", "repo.git", "master"},
|
||||
},
|
||||
{
|
||||
name: "default",
|
||||
org: "org",
|
||||
res: [3]string{"org", common.DefaultGitPrj, "master"},
|
||||
},
|
||||
{
|
||||
name: "repo with branch",
|
||||
org: "org2",
|
||||
prjgit: "repo.git#somebranch",
|
||||
res: [3]string{"org2", "repo.git", "somebranch"},
|
||||
},
|
||||
{
|
||||
name: "repo org and branch",
|
||||
org: "org3",
|
||||
prjgit: "oorg/foo.bar#point",
|
||||
res: [3]string{"oorg", "foo.bar", "point"},
|
||||
},
|
||||
{
|
||||
name: "whitespace shouldn't matter",
|
||||
prjgit: " oorg / \nfoo.bar\t # point ",
|
||||
res: [3]string{"oorg", "foo.bar", "point"},
|
||||
},
|
||||
{
|
||||
name: "repo org and empty branch",
|
||||
org: "org3",
|
||||
prjgit: "oorg/foo.bar#",
|
||||
res: [3]string{"oorg", "foo.bar", "master"},
|
||||
},
|
||||
{
|
||||
name: "only branch defined",
|
||||
org: "org3",
|
||||
prjgit: "#mybranch",
|
||||
res: [3]string{"org3", "_ObsPrj", "mybranch"},
|
||||
},
|
||||
{
|
||||
name: "only org and branch defined",
|
||||
org: "org3",
|
||||
prjgit: "org1/#mybranch",
|
||||
res: [3]string{"org1", "_ObsPrj", "mybranch"},
|
||||
},
|
||||
{
|
||||
name: "empty org and repo",
|
||||
org: "org3",
|
||||
prjgit: "/repo#",
|
||||
res: [3]string{"org3", "repo", "master"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
c := &common.AutogitConfig{
|
||||
Organization: test.org,
|
||||
Branch: test.branch,
|
||||
GitProjectName: test.prjgit,
|
||||
}
|
||||
|
||||
i, j, k := c.GetPrjGit()
|
||||
res := []string{i, j, k}
|
||||
if !slices.Equal(res, test.res[:]) {
|
||||
t.Error("Expected", test.res, "but received", res)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@@ -19,14 +19,20 @@ package common
|
||||
*/
|
||||
|
||||
const (
|
||||
GiteaTokenEnv = "GITEA_TOKEN"
|
||||
ObsUserEnv = "OBS_USER"
|
||||
ObsPasswordEnv = "OBS_PASSWORD"
|
||||
GiteaTokenEnv = "GITEA_TOKEN"
|
||||
ObsUserEnv = "OBS_USER"
|
||||
ObsPasswordEnv = "OBS_PASSWORD"
|
||||
ObsSshkeyEnv = "OBS_SSHKEY"
|
||||
ObsSshkeyFileEnv = "OBS_SSHKEYFILE"
|
||||
|
||||
DefaultGitPrj = "_ObsPrj"
|
||||
PrjLinksFile = "links.json"
|
||||
GiteaRequestHeader = "X-Gitea-Event-Type"
|
||||
|
||||
Bot_BuildReview = "autogits_obs_staging_bot"
|
||||
|
||||
TopicApp = "src"
|
||||
)
|
||||
|
||||
// when set, pushing to remote does not happen, and other remote side-effects should also not happen
|
||||
var IsDryRun bool
|
@@ -1,4 +1,4 @@
|
||||
package common
|
||||
package common_test
|
||||
|
||||
/*
|
||||
* This file is part of Autogits.
|
1136
common/git_utils.go
Normal file
1136
common/git_utils.go
Normal file
File diff suppressed because it is too large
Load Diff
597
common/git_utils_test.go
Normal file
597
common/git_utils_test.go
Normal file
@@ -0,0 +1,597 @@
|
||||
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 (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGitClone(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
repo string
|
||||
branch string
|
||||
remoteName string
|
||||
remoteUrl string
|
||||
}{
|
||||
{
|
||||
name: "Basic clone",
|
||||
repo: "pkgAclone",
|
||||
branch: "main",
|
||||
remoteName: "pkgA_main",
|
||||
remoteUrl: "/pkgA",
|
||||
},
|
||||
{
|
||||
name: "Remote branch is non-existent",
|
||||
repo: "pkgAclone",
|
||||
branch: "main_not_here",
|
||||
remoteName: "pkgA_main",
|
||||
remoteUrl: "/pkgA",
|
||||
},
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
execPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d := t.TempDir()
|
||||
os.Chdir(d)
|
||||
defer os.Chdir(execPath)
|
||||
cmd := exec.Command(path.Join(execPath, "test_clone_setup.sh"))
|
||||
if _, err := cmd.Output(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gh, err := AllocateGitWorkTree(d, "Test", "test@example.com")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
g, err := gh.CreateGitHandler("org")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := g.GitClone(test.repo, test.branch, "file://"+d+test.remoteUrl); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
id, err := g.GitBranchHead(test.repo, test.branch)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Fatal(id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitMsgParsing(t *testing.T) {
|
||||
t.Run("tree message with size 56", func(t *testing.T) {
|
||||
const hdr = "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f98 tree 56\x00"
|
||||
|
||||
data := make(chan byte, 500)
|
||||
for _, b := range []byte(hdr) {
|
||||
data <- b
|
||||
}
|
||||
gitHdr, err := parseGitMsg(data)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if gitHdr.hash != "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f98" {
|
||||
t.Errorf("Invalid hash %s", gitHdr.hash)
|
||||
}
|
||||
|
||||
if gitHdr.size != 56 {
|
||||
t.Errorf("Invalid msg size: %d", gitHdr.size)
|
||||
}
|
||||
|
||||
if gitHdr.itemType != "tree" {
|
||||
t.Errorf("Invalid msg type: %s", gitHdr.itemType)
|
||||
}
|
||||
})
|
||||
t.Run("commit message with size 256", func(t *testing.T) {
|
||||
const hdr = "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f99 commit 256\x00"
|
||||
|
||||
data := make(chan byte, 500)
|
||||
for _, b := range []byte(hdr) {
|
||||
data <- b
|
||||
}
|
||||
gitHdr, err := parseGitMsg(data)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if gitHdr.hash != "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f99" {
|
||||
t.Errorf("Invalid hash %s", gitHdr.hash)
|
||||
}
|
||||
|
||||
if gitHdr.size != 256 {
|
||||
t.Errorf("Invalid msg size: %d", gitHdr.size)
|
||||
}
|
||||
|
||||
if gitHdr.itemType != "commit" {
|
||||
t.Errorf("Invalid msg type: %s", gitHdr.itemType)
|
||||
}
|
||||
})
|
||||
t.Run("invalid tree message with size 56", func(t *testing.T) {
|
||||
const hdr = "f408r8ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f98 tree 56\x00"
|
||||
|
||||
data := make(chan byte, 500)
|
||||
for _, b := range []byte(hdr) {
|
||||
data <- b
|
||||
}
|
||||
gitHdr, err := parseGitMsg(data)
|
||||
|
||||
if err.Error() != "Invalid character during object hash parse 'r' at 4" {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if gitHdr.hash != "" {
|
||||
t.Errorf("Invalid hash %s", gitHdr.hash)
|
||||
}
|
||||
|
||||
if gitHdr.size != 0 {
|
||||
t.Errorf("Invalid msg size: %d", gitHdr.size)
|
||||
}
|
||||
|
||||
if gitHdr.itemType != "" {
|
||||
t.Errorf("Invalid msg type: %s", gitHdr.itemType)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGitCommitParsing(t *testing.T) {
|
||||
t.Run("parse valid commit message", func(t *testing.T) {
|
||||
const commitData = "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f99 commit 253\000" +
|
||||
`tree e20033df9f18780756ba4a96dbc7eb1a626253961039cb674156f266ba7a4e53
|
||||
parent 429cc2fe02170ca5668f0461928c7e7430c7a17cd64ac298286d7162572a7703
|
||||
author Adam Majer <amajer@suse.com> 1720709149 +0200
|
||||
committer Adam Majer <amajer@suse.com> 1720709149 +0200
|
||||
|
||||
.` + "\000"
|
||||
ch := make(chan byte, 5000)
|
||||
for _, b := range []byte(commitData) {
|
||||
ch <- b
|
||||
}
|
||||
commit, err := parseGitCommit(ch)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if commit.Tree != "e20033df9f18780756ba4a96dbc7eb1a626253961039cb674156f266ba7a4e53" {
|
||||
t.Errorf("Invalid commit object: %#v", commit)
|
||||
}
|
||||
|
||||
if commit.Msg != "." {
|
||||
t.Errorf("Invalid commit msg: '%s'", commit.Msg)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("parse multiline headers", func(t *testing.T) {
|
||||
const commitData = "cae5831ab48470ff060a5aaa12eb6e5a7acaf91e commit 1492\000" +
|
||||
`tree 1f9c8fe8099615d6d3921528402ac53f09213b02
|
||||
parent e08a654fae0ecc91678819e0b62a2e014bad3339
|
||||
author Yagiz Nizipli <yagiz@nizipli.com> 1720967314 -0400
|
||||
committer GitHub <noreply@github.com> 1720967314 +0200
|
||||
gpgsig -----BEGIN PGP SIGNATURE-----
|
||||
|
||||
wsFcBAABCAAQBQJmk+CSCRC1aQ7uu5UhlAAAQIYQAEQXCl3bUUuegiz5/oitIIF7
|
||||
6xhndcjQIuqY4dONIeOARrGwbKh8OtMHpfJhMRUmvWvXrsTA6P1PWl0YcyyIMzHZ
|
||||
a4sBsWyxA0uSztVywpvksvk6EdMoEXeXrHS3cBxePsH8bI+Pwnsv27PsevEwpyIT
|
||||
reB4zZsoGySFVqf2lnXxG5hSRMYw++BDXSDMZk2BP9BvueRXasJ0lT1c7HlbHepF
|
||||
TWzwyHZ91OhXjrdPY7qLQEEV/frwuM+UrxOPb2e83ZTg81vXFuugURfhHNx4Iu+F
|
||||
LCMvOeaF2vO5yJtMe8+tY1l0Wb8S1aWcGCECN2XCXmmnWxt+yYh2gjqxq3y0DMcz
|
||||
zvg6arQIepDFLkQPZMDlUCIjIJQn4FbAaQAvoyMF8Pi5YmxhRqgo3iOB5SBE8eES
|
||||
63ifZ311izuSdD+o3ObFpzLoTgq62kglwfegZN/X8CzTSIqrT1norYJEbSwkWID1
|
||||
WeRHUHfC7f6N3XK8zeb83zmhBU58ghW9sp5/LcefGMRJmVhBWhjBCpeMUaFHdKhl
|
||||
/dfgPl5gJrrJ+wM3O6iaay0R1Iv4Upe/yXrbQnIgGj/qqgMLEPBY8lzNYimVTLxd
|
||||
2ObrcXnERo3wwxeUgWaAARbEGQjC51DK/2SXxVGUh+IokicsBNRKU7lVWwwFFely
|
||||
ntjtge6Gs9pA5rSIilPH
|
||||
=V1bK
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
|
||||
meta: change email address of anonrig
|
||||
|
||||
PR-URL: https://github.com/nodejs/node/pull/53829
|
||||
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
|
||||
Reviewed-By: James M Snell <jasnell@gmail.com>
|
||||
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
|
||||
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
|
||||
Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
|
||||
Reviewed-By: Richard Lau <rlau@redhat.com>
|
||||
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>` + "\000"
|
||||
|
||||
ch := make(chan byte, 5000)
|
||||
for _, b := range []byte(commitData) {
|
||||
ch <- b
|
||||
}
|
||||
commit, err := parseGitCommit(ch)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if commit.Tree != "1f9c8fe8099615d6d3921528402ac53f09213b02" {
|
||||
t.Errorf("Invalid commit object: %#v", commit)
|
||||
}
|
||||
|
||||
if commit.Msg[len(commit.Msg)-55:] != "Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>" {
|
||||
t.Errorf("Invalid commit msg: '%s'", commit.Msg[len(commit.Msg)-55:])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("parse multiline headers", func(t *testing.T) {
|
||||
const commitData = "c07c52c57a10fb355956df3caad2986613838f149274fbe312ad76560764829d commit 1150\000" + `tree 3e06b280ea056141ed5e8af9794a41ae5281930c45321803eab53a240cb60044
|
||||
parent 19362a2cecb1fd25a89e03611d08ac68dcb1732f9dc0a68a40926356787fa4ca
|
||||
author Adrian Schröter <adrian@suse.de> 1746600403 +0200
|
||||
committer Adrian Schröter <adrian@suse.de> 1746600403 +0200
|
||||
gpgsig-sha256 -----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iQIzBAABCgAdFiEE1QF1zm/pNbvyhgLFkY2MlUwI22cFAmgbAd0ACgkQkY2MlUwI
|
||||
22dxtA//eUCzIqxVdaEnOrFeTyxKig/mCOjaAyctmwr0vXUyElRtjXe4TzVG3QtR
|
||||
uDfhIrKYLZ2tU/0TewTW/4XopWxLuqEzVQLrjuYl7K5P3GoYk52W1yGT0szzm7/i
|
||||
87j4UdRL9YGU/gYO7nSzstcfTP6AcmYzVUoOnwYR0K2vyOVjO4niL3mFXxLkIgIt
|
||||
jd82xcE4JpQz9Yjyq2nDdz4A55kLAwsqY+dOct4oC6bZmj1/JeoGQfPvUsvsQgcI
|
||||
syCHVh0GBxjvSv50V/VPzxQTFMal/TdtvAD4kmP/9RDi/5THzus8Peam8pV0gEIC
|
||||
Q15ZcuLwIsC9i7ifUDYgzLgBBRdpSI0qji4Y6clWULPVjsyghgyfQw1trBSySpC8
|
||||
O1XfajUM+rXyrBLP6kzY+zl/zyzRdJ8JhljmC+SmNuyyEB77Hkn83k0f+aBhhqC2
|
||||
4b3fIsKtwJZ1w6gr6SSz1BottiT9ShQzRaL8iRoF/2l5MkHPR+QFg2J7EIBqCbCQ
|
||||
hFUjdvWAXQBWkkTQlJmLmJBXDOLQg3o6xCbnZM0gPFjZWE7e3Mpky7H0+xPnoeg9
|
||||
ukuvkexXQ6yrdiekA7HRLc76Te/I0m7KDOOWZ3rbJV6uH/3ps4FbLQTZO12AtZ6J
|
||||
n8hYdYfw9yjCxiKUjnEtXtDRe8DJpqv+hO0Wj4MI5gIA2JE2lzY=
|
||||
=Keg5
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
dummy change, don't merge
|
||||
` + "\000"
|
||||
ch := make(chan byte)
|
||||
go func() {
|
||||
for _, b := range []byte(commitData) {
|
||||
ch <- b
|
||||
}
|
||||
}()
|
||||
commit, err := parseGitCommit(ch)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if commit.Tree != "3e06b280ea056141ed5e8af9794a41ae5281930c45321803eab53a240cb60044" {
|
||||
t.Errorf("Invalid commit object: %#v", commit)
|
||||
}
|
||||
|
||||
if commit.Msg != "dummy change, don't merge\n" {
|
||||
t.Errorf("Invalid commit msg: '%s'", commit.Msg)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("parse tree object", func(t *testing.T) {
|
||||
const treeData = "\x31\x61\x30\x35\x64\x62\x37\x33\x36\x39\x33\x37\x34\x33\x30\x65\x31\x38\x64\x66\x34\x33\x61\x32\x37\x61\x39\x38\x30\x30\x31\x30\x31\x32\x65\x31\x65\x64\x32\x30\x34\x38\x32\x39\x38\x36\x37\x31\x32\x38\x66\x32\x63\x65\x38\x34\x30\x36\x62\x35\x63\x66\x63\x39\x20\x74\x72\x65\x65\x20\x32\x30\x35\x00\x34\x30\x30\x30\x30\x20\x62\x6f\x74\x73\x2d\x63\x6f\x6d\x6d\x6f\x6e\x00\x93\x17\xaa\x47\xf6\xea\x37\xe8\xbc\xe2\x80\x77\x57\x90\xf4\xa8\x01\xd7\xe3\x70\x2f\x84\xfb\xe1\xb0\x0e\x4a\x2c\x1c\x75\x2c\x2b\x34\x30\x30\x30\x30\x20\x6f\x62\x73\x2d\x73\x74\x61\x67\x69\x6e\x67\x2d\x62\x6f\x74\x00\x79\x77\x8b\x28\x7d\x37\x10\x59\xb9\x71\x28\x36\xed\x20\x31\x5f\xfb\xe1\xed\xb5\xba\x4f\x5e\xbb\x65\x65\x68\x23\x77\x32\x58\xfe\x34\x30\x30\x30\x30\x20\x70\x72\x2d\x72\x65\x76\x69\x65\x77\x00\x36\x0d\x45\xcb\x76\xb8\x93\xb3\x21\xba\xfa\xd5\x00\x9d\xfc\x59\xab\x88\xc1\x3c\x81\xcb\x48\x5a\xe0\x29\x29\x0f\xe3\x6b\x3c\x5e\x34\x30\x30\x30\x30\x20\x70\x72\x6a\x67\x69\x74\x2d\x75\x70\x64\x61\x74\x65\x72\x00\xb4\x0b\x1c\xf5\xfb\xec\x9a\xb2\x9f\x48\x3e\x21\x18\x0d\x51\xb7\x98\x6e\x21\x99\x74\x84\x67\x71\x41\x24\x42\xfc\xc9\x04\x12\x99\x00"
|
||||
|
||||
ch := make(chan byte, 1000)
|
||||
for _, b := range []byte(treeData) {
|
||||
ch <- b
|
||||
}
|
||||
|
||||
tree, err := parseGitTree(ch)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
found := false
|
||||
t.Log(tree.items)
|
||||
for _, item := range tree.items {
|
||||
if item.name == "bots-common" && item.hash == "9317aa47f6ea37e8bce280775790f4a801d7e3702f84fbe1b00e4a2c1c752c2b" && item.isTree() {
|
||||
found = true
|
||||
t.Log("found")
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("expected sub-tree not found")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("parse tree object with submodules", func(t *testing.T) {
|
||||
const treeData = "\x34\x38\x34\x66\x31\x62\x65\x65\x63\x39\x35\x63\x61\x36\x62\x38\x36\x30\x64\x30\x64\x37\x63\x35\x65\x34\x38\x37\x31\x36\x36\x62\x30\x38\x65\x31\x61\x34\x35\x36\x37\x36\x62\x31\x37\x64\x39\x65\x65\x66\x32\x39\x39\x39\x65\x39\x64\x37\x34\x63\x62\x63\x64\x36\x20\x74\x72\x65\x65\x20\x33\x34\x39\x00\x31\x30\x30\x36\x34\x34\x20\x2e\x67\x69\x74\x6d\x6f\x64\x75\x6c\x65\x73\x00\xc8\x1e\x14\x29\xc5\xd4\x07\xfc\x32\xf1\xd7\xe2\x64\xee\x88\xfc\xce\xfc\x44\xf7\xae\x2e\xc4\x6e\x2c\x15\x27\x26\x65\xd4\xb8\x78\x31\x30\x30\x36\x34\x34\x20\x52\x45\x41\x44\x4d\x45\x2e\x6d\x64\x00\x90\x22\x84\x13\xbf\xd3\x5c\xfb\x1e\x27\x6b\xd6\x17\x3e\x89\xed\x0f\xc6\x31\x85\x24\x58\x6d\x9d\xf0\x6a\x1e\x17\x57\x19\x8d\xc2\x31\x36\x30\x30\x30\x30\x20\x6d\x69\x6e\x67\x77\x33\x32\x2d\x67\x63\x63\x00\xdc\x55\xb8\x28\x32\x8c\x8e\x49\x4f\x67\x87\x4a\x7d\x8c\x03\xdd\x1c\x6b\x4e\x02\xd1\x6b\x86\xe0\x8e\x47\xd7\x0e\xcd\x79\x96\x80\x31\x36\x30\x30\x30\x30\x20\x6e\x6f\x64\x65\x6a\x73\x2d\x63\x6f\x6d\x6d\x6f\x6e\x00\xd6\xa7\x4c\x08\x40\x6c\xe4\x0c\xc8\xf7\xbf\xf2\xd5\xcf\x30\x90\x87\xa8\x72\x83\x61\xcc\x75\x35\x4b\x08\x62\xba\x50\x81\x93\xb8\x31\x36\x30\x30\x30\x30\x20\x6e\x6f\x64\x65\x6a\x73\x32\x31\x00\x24\xee\x6b\xee\x74\x59\xa3\x86\xda\xda\xbf\x8a\x9f\x6a\xe4\xfa\x15\xc3\xf8\x10\xbf\xa0\x1c\xee\x52\x38\x13\x8a\xa2\x14\xd1\x80\x31\x36\x30\x30\x30\x30\x20\x6e\x6f\x64\x65\x6a\x73\x32\x32\x00\x87\x3a\x32\x3b\x26\x2e\xbb\x3b\xd7\x7b\x25\x92\xb2\xe1\x1b\xdd\x08\xdb\xc7\x21\xcb\xf4\xac\x9f\x97\x63\x7e\x58\xe1\xff\xfc\xe7\x31\x36\x30\x30\x30\x30\x20\x70\x79\x74\x68\x6f\x6e\x33\x31\x31\x00\x35\xc7\x02\xe8\x50\x1e\xed\xeb\x5c\xe4\x3d\x6f\x34\x60\xd1\x1c\x79\x1c\xfe\xfa\xa7\x72\x48\xf0\x8c\xad\x55\xd0\x0c\x37\xe7\x3a\x00"
|
||||
|
||||
ch := make(chan byte, 1000)
|
||||
for _, b := range []byte(treeData) {
|
||||
ch <- b
|
||||
}
|
||||
|
||||
tree, err := parseGitTree(ch)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, item := range tree.items {
|
||||
t.Log(item)
|
||||
if item.name == "nodejs22" && item.hash == "873a323b262ebb3bd77b2592b2e11bdd08dbc721cbf4ac9f97637e58e1fffce7" && item.isSubmodule() {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("expected submodule not found")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("parse nested trees with subtrees", func(t *testing.T) {
|
||||
const data = "873a323b262ebb3bd77b2592b2e11bdd08dbc721cbf4ac9f97637e58e1fffce7 tree 1083\x00100644\x20\x2Egitattributes\x00\xD8v\xA95\x87\xC1\xA9\xFCPn\xDD\xD4\x13\x9B\x8E\xD2\xCFs\xBD\x11q\x8A\xAE\x8A\x7Cg\xE2C\x14J\x01\xB0100644\x20\x2Egitignore\x00\xC3\xCD\x8En\x887\x3AJ\xA0P\xEEL\xD4\xF5\xD2v\x9C\xA6v\xC5D\x60\x40\x95\xD1\x0B\xA4\xB8\x86\xD4rE100644\x20COPYING\x00\x12\x2A\x28\xC8\xB9\x5D\x9B\x8A\x23\x1F\xE96\x07\x3F\xA9D\x90\xFD\xCE\x2Bi\x2D\x031\x5C\xCC\xC4fx\x00\xC22100644\x20README\x2Emd\x00\x92D\xF7\xFF\x0E0\x5C\xF2\xAC\x0DA\x06\x92\x0B\xD6z\x3CGh\x00y\x7EW1\xB9a\x8Ch\x215Fa100644\x20_service\x00\xC51\xF2\x12\xF3\x24\x9C\xD9\x9F\x0A\x93Mp\x12\xC1\xF7i\x05\x95\xC5Z\x06\x95i\x3Az\xC3\xF59\x7E\xF8\x1B100644\x20autogits\x2Echanges\x00\xF7\x8D\xBF\x0A\xCB\x5D\xB7y\x8C\xA9\x9C\xEB\x92\xAFd\x2C\x98\x23\x0C\x13\x13\xED\xDE\x5D\xBALD6\x3BR\x5B\xCA100644\x20autogits\x2Espec\x00\xD2\xBC\x20v\xD3\xE5F\xCA\xEE\xEA\x18\xC84\x0D\xA7\xCA\xD8O\xF2\x0A\xAB\x40\x2A\xFAL\x3B\xB4\xE6\x11\xE7o\xD140000\x20common\x00\xE2\xC9dg\xD0\x5D\xD1\xF1\x8ARW\xF0\x96\xD6\x29\x2F\x8F\xD9\xC7\x82\x1A\xB7\xAAw\xB0\xCE\xA8\xFE\xC8\xD7D\xF2100755\x20dev_test_helper\x2Esh\x00\xECY\xDD\xB3rz\x9Fh\xD4\x2E\x85\x02\x13\xF8\xFE\xB57\x8B\x1B6\x8E\x09dC\x1E\xE0\x90\x09\x08\xED\xBD_40000\x20devel\x2Dimporter\x00v\x98\x9B\x92\xD8\x24lu\xFC\xB2d\xC9\xCENb\xEE\x0F\x21\x8B\x92\x88\xDBs\xF8\x2E\xA8\xC8W\x1C\x20\xCF\xD440000\x20doc\x00\x8Akyq\xD0\xCF\xB8\x2F\x80Y\x2F\x11\xF0\x14\xA9\xFE\x96\x14\xE0W\x2C\xCF\xB9\x86\x7E\xFDi\xD7\x1F\x08Q\xFB40000\x20gitea\x2Devents\x2Drabbitmq\x2Dpublisher\x00\x5Cb\x3Fh\xA2\x06\x06\x0Cd\x09\xA5\xD9\xF7\x23\x5C\xF85\xF5\xB8\xBE\x7F\xD4O\x25t\xEF\xCC\xAB\x18\x7C\x0C\xF3100644\x20go\x2Emod\x00j\x85\x0B\x03\xC8\x9F\x9F\x0F\xC8\xE0\x8C\xF7\x3D\xC19\xF7\x12gk\xD6\x18JN\x24\xC0\x1C\xBE\x97oY\x02\x8D100644\x20go\x2Esum\x00h\x88\x2E\x27\xED\xD39\x8D\x12\x0F\x7D\x97\xA2\x5DE\xB9\x82o\x0Cu\xF4l\xA17s\x28\x2BQT\xE6\x12\x9040000\x20group\x2Dreview\x00\x7E\x7B\xB42\x0F\x3B\xC9o\x2C\xE79\x1DR\xE2\xE4i\xAE\xF6u\x90\x09\xD8\xC9c\xE7\xF7\xC7\x92\xFB\xD7\xDD140000\x20obs\x2Dstaging\x2Dbot\x00\x12\xE8\xAF\x09\xD4\x5D\x13\x8D\xC9\x0AvPDc\xB6\x7C\xAC4\xD9\xC5\xD4_\x98i\xBE2\xA7\x25aj\xE2k40000\x20obs\x2Dstatus\x2Dservice\x00MATY\xA3\xFA\xED\x05\xBE\xEB\x2B\x07\x9CN\xA9\xF3SB\x22MlV\xA4\x5D\xDA\x0B\x0F\x23\xA1\xA8z\xD740000\x20systemd\x00\x2D\xE2\x03\x7E\xBD\xEB6\x8F\xC5\x0E\x12\xD4\xBD\x97P\xDD\xA2\x92\xCE6n\x08Q\xCA\xE4\x15\x97\x8F\x26V\x3DW100644\x20vendor\x2Etar\x2Ezst\x00\xD9\x2Es\x03I\x91\x22\x24\xC86q\x91\x95\xEF\xA3\xC9\x3C\x06D\x90w\xAD\xCB\xAE\xEEu2i\xCE\x05\x09u40000\x20workflow\x2Ddirect\x00\x94\xDB\xDFc\xB5A\xD5\x16\xB3\xC3ng\x94J\xE7\x101jYF\x15Q\xE97\xCFg\x14\x12\x28\x3A\xFC\xDB40000\x20workflow\x2Dpr\x00\xC1\xD8Z9\x18\x60\xA2\xE2\xEF\xB0\xFC\xD7\x2Ah\xF07\x0D\xEC\x8A7\x7E\x1A\xAAn\x13\x9C\xEC\x05s\xE8\xBDf\x00"
|
||||
|
||||
ch := make(chan byte, 2000)
|
||||
for _, b := range []byte(data) {
|
||||
ch <- b
|
||||
}
|
||||
|
||||
tree, err := parseGitTree(ch)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, item := range tree.items {
|
||||
t.Log(item)
|
||||
if item.name == "workflow-pr" && item.hash == "c1d85a391860a2e2efb0fcd72a68f0370dec8a377e1aaa6e139cec0573e8bd66" && item.isTree() {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("expected submodule not found")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCommitTreeParsing(t *testing.T) {
|
||||
gitDir := t.TempDir()
|
||||
testDir, _ := os.Getwd()
|
||||
var commitId string
|
||||
|
||||
cmd := exec.Command("/usr/bin/bash", path.Join(testDir, "tsetup.sh"))
|
||||
cmd.Dir = gitDir
|
||||
cmd.Stdout = writeFunc(func(data []byte) (int, error) {
|
||||
commitId = commitId + strings.TrimSpace(string(data))
|
||||
return len(data), nil
|
||||
})
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
gh, err := AllocateGitWorkTree(gitDir, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("GitCatFile commit", func(t *testing.T) {
|
||||
h, _ := gh.ReadExistingPath(".")
|
||||
defer h.Close()
|
||||
|
||||
file, err := h.GitCatFile("", commitId, "help")
|
||||
if err != nil {
|
||||
t.Error("failed", err)
|
||||
}
|
||||
|
||||
if string(file) != "help\n" {
|
||||
t.Error("expected 'help\\n' but got", string(file))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GitCatFile commit", func(t *testing.T) {
|
||||
h, _ := gh.ReadExistingPath(".")
|
||||
defer h.Close()
|
||||
|
||||
file, err := h.GitCatFile("", "HEAD", "help")
|
||||
if err != nil {
|
||||
t.Error("failed", err)
|
||||
}
|
||||
|
||||
if string(file) != "help\n" {
|
||||
t.Error("expected 'help\\n' but got", string(file))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GitCatFile bad commit", func(t *testing.T) {
|
||||
h, _ := gh.ReadExistingPath(".")
|
||||
defer h.Close()
|
||||
|
||||
file, err := h.GitCatFile("", "518b468f391bf01d5d76d497d7cbecfa8b46d185714cf8745800ae18afb21afd", "help")
|
||||
if err == nil {
|
||||
t.Error("expected error, but not nothing")
|
||||
}
|
||||
|
||||
if string(file) != "" {
|
||||
t.Error("expected 'help\\n' but got", file)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("reads HEAD and parses the tree", func(t *testing.T) {
|
||||
const nodejs21 = "c678c57007d496a98bec668ae38f2c26a695f94af78012f15d044ccf066ccb41"
|
||||
h, _ := gh.ReadExistingPath(".")
|
||||
defer h.Close()
|
||||
|
||||
id, ok := h.GitSubmoduleCommitId("", "nodejs21", commitId)
|
||||
if !ok {
|
||||
t.Error("failed parse")
|
||||
}
|
||||
if id != nodejs21 {
|
||||
t.Errorf("hash doesn't match: %s vs. expected %s", id, nodejs21)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("reads README.md", func(t *testing.T) {
|
||||
h, _ := gh.ReadExistingPath(".")
|
||||
defer h.Close()
|
||||
|
||||
data, err := h.GitCatFile("", commitId, "README.md")
|
||||
if err != nil {
|
||||
t.Errorf("failed parse: %v", err)
|
||||
}
|
||||
if string(data) != "foo\n" || len(data) != 4 {
|
||||
t.Errorf("Wrong data of len: %d", len(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("read HEAD", func(t *testing.T) {
|
||||
h, _ := gh.ReadExistingPath(".")
|
||||
defer h.Close()
|
||||
|
||||
data, err := h.GitSubmoduleList("", "HEAD")
|
||||
if err != nil {
|
||||
t.Error("failed to get submodule list", err)
|
||||
}
|
||||
|
||||
if len(data) != 5 {
|
||||
t.Error("Invalid len of submodules", len(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("try to parse unknown item", func(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGitStatusParse(t *testing.T) {
|
||||
testData := []struct {
|
||||
name string
|
||||
data []byte
|
||||
res []GitStatusData
|
||||
}{
|
||||
{
|
||||
name: "Single modified line",
|
||||
data: []byte("1 .M N... 100644 100644 100644 dbe4b3d5a0a2e385f78fd41d726baa20e9190f7b5a2e78cbd4885586832f39e7 dbe4b3d5a0a2e385f78fd41d726baa20e9190f7b5a2e78cbd4885586832f39e7 bots-common/git_utils.go\x00"),
|
||||
res: []GitStatusData{
|
||||
{
|
||||
Path: "bots-common/git_utils.go",
|
||||
Status: GitStatus_Modified,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Untracked entries",
|
||||
data: []byte("1 .M N... 100644 100644 100644 dbe4b3d5a0a2e385f78fd41d726baa20e9190f7b5a2e78cbd4885586832f39e7 dbe4b3d5a0a2e385f78fd41d726baa20e9190f7b5a2e78cbd4885586832f39e7 bots-common/git_utils.go\x00? bots-common/c.out\x00? doc/Makefile\x00"),
|
||||
res: []GitStatusData{
|
||||
{
|
||||
Path: "bots-common/git_utils.go",
|
||||
Status: GitStatus_Modified,
|
||||
},
|
||||
{
|
||||
Path: "bots-common/c.out",
|
||||
Status: GitStatus_Untracked,
|
||||
},
|
||||
{
|
||||
Path: "doc/Makefile",
|
||||
Status: GitStatus_Untracked,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Untracked entries",
|
||||
data: []byte("1 .M N... 100644 100644 100644 dbe4b3d5a0a2e385f78fd41d726baa20e9190f7b5a2e78cbd4885586832f39e7 dbe4b3d5a0a2e385f78fd41d726baa20e9190f7b5a2e78cbd4885586832f39e7 bots-common/git_utils.go\x00? bots-common/c.out\x00! doc/Makefile\x00"),
|
||||
res: []GitStatusData{
|
||||
{
|
||||
Path: "bots-common/git_utils.go",
|
||||
Status: GitStatus_Modified,
|
||||
},
|
||||
{
|
||||
Path: "bots-common/c.out",
|
||||
Status: GitStatus_Untracked,
|
||||
},
|
||||
{
|
||||
Path: "doc/Makefile",
|
||||
Status: GitStatus_Ignored,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Nothing",
|
||||
},
|
||||
{
|
||||
name: "Unmerged .gitmodules during a merge",
|
||||
data: []byte("1 A. S... 000000 160000 160000 0000000000000000000000000000000000000000000000000000000000000000 ed07665aea0522096c88a7555f1fa9009ed0e0bac92de4613c3479516dd3d147 pkgB2\x00u UU N... 100644 100644 100644 100644 587ec403f01113f2629da538f6e14b84781f70ac59c41aeedd978ea8b1253a76 d23eb05d9ca92883ab9f4d28f3ec90c05f667f3a5c8c8e291bd65e03bac9ae3c 087b1d5f22dbf0aa4a879fff27fff03568b334c90daa5f2653f4a7961e24ea33 .gitmodules\x00"),
|
||||
res: []GitStatusData{
|
||||
{
|
||||
Path: "pkgB2",
|
||||
Status: GitStatus_Modified,
|
||||
},
|
||||
{
|
||||
Path: ".gitmodules",
|
||||
Status: GitStatus_Unmerged,
|
||||
States: [3]string{"587ec403f01113f2629da538f6e14b84781f70ac59c41aeedd978ea8b1253a76", "d23eb05d9ca92883ab9f4d28f3ec90c05f667f3a5c8c8e291bd65e03bac9ae3c", "087b1d5f22dbf0aa4a879fff27fff03568b334c90daa5f2653f4a7961e24ea33"},
|
||||
|
||||
SubmoduleChanges: "N...",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Renamed file",
|
||||
data: []byte("1 M. N... 100644 100644 100644 d23eb05d9ca92883ab9f4d28f3ec90c05f667f3a5c8c8e291bd65e03bac9ae3c 896cd09f36d39e782d66ae32dd5614d4f4d83fc689f132aab2dfc019a9f5b6f3 .gitmodules\x002 R. S... 160000 160000 160000 3befe051a34612530acfa84c736d2454278453ec0f78ec028f25d2980f8c3559 3befe051a34612530acfa84c736d2454278453ec0f78ec028f25d2980f8c3559 R100 pkgQ\x00pkgC\x00"),
|
||||
res: []GitStatusData{
|
||||
{
|
||||
Path: "pkgQ",
|
||||
Status: GitStatus_Renamed,
|
||||
States: [3]string{"pkgC"},
|
||||
},
|
||||
{
|
||||
Path: ".gitmodules",
|
||||
Status: GitStatus_Modified,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testData {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
r, err := parseGitStatusData(bufio.NewReader(bytes.NewReader(test.data)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(r) != len(test.res) {
|
||||
t.Fatal("len(r):", len(r), "is not expected", len(test.res))
|
||||
}
|
||||
|
||||
for _, expected := range test.res {
|
||||
if !slices.Contains(r, expected) {
|
||||
t.Fatal("result", r, "doesn't contains expected", expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user