Compare commits
250 Commits
0.0.1
...
local_repo
Author | SHA256 | Date | |
---|---|---|---|
fa61af0db6 | |||
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,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,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
|
||||
}
|
@@ -7,7 +7,8 @@ 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
|
||||
api: gitea-generated/client/gitea_api_client.go mock_gitea_utils.go
|
||||
go generate
|
||||
|
||||
build: api
|
||||
go build
|
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
246
common/config.go
Normal file
246
common/config.go
Normal file
@@ -0,0 +1,246 @@
|
||||
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
|
||||
}
|
||||
|
||||
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 PartiallyParseWorkflowConfig(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 := PartiallyParseWorkflowConfig(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
|
||||
|
||||
// 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
|
||||
}
|
||||
if err := json.Unmarshal(data, &staging); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &staging, nil
|
||||
}
|
130
common/config_test.go
Normal file
130
common/config_test.go
Normal file
@@ -0,0 +1,130 @@
|
||||
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 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)
|
||||
|
||||
_, err := common.ReadWorkflowConfig(gitea, "foo/bar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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,11 +19,14 @@ 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"
|
@@ -1,4 +1,4 @@
|
||||
package common
|
||||
package common_test
|
||||
|
||||
/*
|
||||
* This file is part of Autogits.
|
1013
common/git_utils.go
Normal file
1013
common/git_utils.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -19,13 +19,78 @@ package common
|
||||
*/
|
||||
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
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"
|
||||
@@ -240,9 +305,36 @@ Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>` + "\x00"
|
||||
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 TestCommitTreeParsingOfHead(t *testing.T) {
|
||||
func TestCommitTreeParsing(t *testing.T) {
|
||||
gitDir := t.TempDir()
|
||||
testDir, _ := os.Getwd()
|
||||
var commitId string
|
||||
@@ -257,11 +349,58 @@ func TestCommitTreeParsingOfHead(t *testing.T) {
|
||||
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 := GitHandler{
|
||||
GitPath: gitDir,
|
||||
}
|
||||
h, _ := gh.ReadExistingPath(".")
|
||||
defer h.Close()
|
||||
|
||||
id, ok := h.GitSubmoduleCommitId("", "nodejs21", commitId)
|
||||
if !ok {
|
||||
t.Error("failed parse")
|
||||
@@ -272,9 +411,9 @@ func TestCommitTreeParsingOfHead(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("reads README.md", func(t *testing.T) {
|
||||
h := GitHandler{
|
||||
GitPath: gitDir,
|
||||
}
|
||||
h, _ := gh.ReadExistingPath(".")
|
||||
defer h.Close()
|
||||
|
||||
data, err := h.GitCatFile("", commitId, "README.md")
|
||||
if err != nil {
|
||||
t.Errorf("failed parse: %v", err)
|
||||
@@ -285,9 +424,8 @@ func TestCommitTreeParsingOfHead(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("read HEAD", func(t *testing.T) {
|
||||
h := GitHandler{
|
||||
GitPath: gitDir,
|
||||
}
|
||||
h, _ := gh.ReadExistingPath(".")
|
||||
defer h.Close()
|
||||
|
||||
data, err := h.GitSubmoduleList("", "HEAD")
|
||||
if err != nil {
|
||||
@@ -302,3 +440,109 @@ func TestCommitTreeParsingOfHead(t *testing.T) {
|
||||
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"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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