Compare commits

..

1 Commits

Author SHA256 Message Date
Jan Zerebecki
a6a07f5cd5 Build in obs directly from this repo 2025-04-01 17:23:40 +02:00
1914 changed files with 7434 additions and 189674 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,52 +0,0 @@
name: Integration tests
on:
push:
branches: ['main']
pull_request:
workflow_dispatch:
env:
HOME: /var/lib/gitea-runner
REPO_URL: http://src.opensuse.org//git-workflow/autogits.git
jobs:
t:
runs-on: linux-x86_64
steps:
- name: whoami
run: whoami
- name: pwd
run: pwd
- name: vars
run: |
set | grep GITEA_
- name: Clone
run: |
git clone -q ${{ env.REPO_URL }}
- name: Checkout
run: |
echo ${{ gitea.ref }}
git fetch origin ${{ gitea.ref }}
git checkout FETCH_HEAD
working-directory: ./autogits
- name: Prepare binaries
run: make build
working-directory: ./autogits
- name: Prepare images
run: make build
working-directory: ./autogits/integration
- name: Make sure the pod is down
run: make down
working-directory: ./autogits/integration
- name: Start images
run: make up
working-directory: ./autogits/integration
- name: Run tests
run: py.test-3.11 -v tests
working-directory: ./autogits/integration
- name: Make sure the pod is down
if: always()
run: make down
working-directory: ./autogits/integration

11
.gitignore vendored
View File

@@ -1,8 +1,5 @@
mock
node_modules
*.obscpio
autogits-tmp.tar.zst
*.osc
*.conf
!/integration/**/*.conf
/integration/gitea-data
/integration/gitea-logs
/integration/rabbitmq-data
/integration/workflow-pr-repos
__pycache__/

View File

@@ -1,4 +0,0 @@
MODULES := devel-importer utils/hujson utils/maintainer-update gitea-events-rabbitmq-publisher gitea_status_proxy group-review obs-forward-bot obs-staging-bot obs-status-service workflow-direct workflow-pr
build:
for m in $(MODULES); do go build -C $$m -buildmode=pie || exit 1 ; done

View File

@@ -5,15 +5,11 @@ The bots that drive Git Workflow for package management
* devel-importer -- helper to import an OBS devel project into a Gitea organization
* gitea-events-rabbitmq-publisher -- takes all events from a Gitea organization (webhook) and publishes it on a RabbitMQ instance
* gitea-status-proxy -- allows bots without code owner permission to set Gitea's commit status
* group-review -- group review proxy
* hujson -- translates JWCC (json with commas and comments) to Standard JSON
* obs-forward-bot -- forwards PR as OBS sr (TODO)
* obs-staging-bot -- build bot for a PR
* obs-status-service -- report build status of an OBS project as an SVG
* workflow-pr -- keeps PR to _ObsPrj consistent with a PR to a package update
* workflow-direct -- update _ObsPrj based on direct pushes and repo creations/removals from organization
* staging-utils -- review tooling for PR (TODO)
* staging-utils -- review tooling for PR
- list PR
- merge PR
- split PR
@@ -23,18 +19,7 @@ The bots that drive Git Workflow for package management
Bugs
----
Report bugs to issue tracker at https://src.opensuse.org/git-workflow/autogits
Report bugs to issue tracker at https://src.opensuse.org/adamm/autogits
Build Status
------------
Devel project build status (`main` branch):
![devel:Factory:git-workflow](https://br.opensuse.org/status/devel:Factory:git-workflow/autogits)
`staging` branch build status:
![Staging Build Status](https://br.opensuse.org/status/home:adamm:autogits/autogits)

16
_service Normal file
View File

@@ -0,0 +1,16 @@
<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="subdir">gitea-events-rabbitmq-publisher</param>
<param name="vendorname">vendor-gitea-events-rabbitmq-publisher</param>
</service>
</services>

View File

@@ -17,307 +17,60 @@
Name: autogits
Version: 1
Version: 0
Release: 0
Summary: GitWorkflow utilities
License: GPL-2.0-or-later
URL: https://src.opensuse.org/adamm/autogits
BuildRequires: git
Source1: vendor-gitea-events-rabbitmq-publisher.tar.zst
BuildRequires: golang-packaging
BuildRequires: systemd-rpm-macros
BuildRequires: go
BuildRequires: zstd
%{?systemd_ordering}
%description
Git Workflow tooling and utilities enabling automated handing of OBS projects
as git repositories
%package devel-importer
Summary: Imports devel projects from obs to git
%description -n autogits-devel-importer
Command-line tool to import devel projects from obs to git
%package doc
Summary: Common documentation files
BuildArch: noarch
%description -n autogits-doc
Common documentation files
%package gitea-events-rabbitmq-publisher
%package -n gitea-events-rabbitmq-publisher
Summary: Publishes Gitea webhook data via RabbitMQ
%description gitea-events-rabbitmq-publisher
%description -n gitea-events-rabbitmq-publisher
Listens on an HTTP socket and publishes Gitea events on a RabbitMQ instance
with a topic
<scope>.src.$organization.$webhook_type.[$webhook_action_type]
%package gitea-status-proxy
Summary: Proxy for setting commit status in Gitea
%description gitea-status-proxy
Setting commit status requires code write access token. This proxy
is middleware that delegates status setting without access to other APIs
%package group-review
Summary: Reviews of groups defined in ProjectGit
%description group-review
Is used to handle reviews associated with groups defined in the
ProjectGit.
%package obs-forward-bot
Summary: obs-forward-bot
%description obs-forward-bot
%package obs-staging-bot
Summary: Build a PR against a ProjectGit, if review is requested
%description obs-staging-bot
Build a PR against a ProjectGit, if review is requested.
%package obs-status-service
Summary: Reports build status of OBS service as an easily to produce SVG
%description obs-status-service
Reports build status of OBS service as an easily to produce SVG
%package utils
Summary: HuJSON to JSON parser
Provides: hujson
Provides: /usr/bin/hujson
%description utils
HuJSON to JSON parser, using stdin -> stdout pipe
%package workflow-direct
Summary: Keep ProjectGit in sync for a devel project
Requires: openssh-clients
Requires: git-core
%description workflow-direct
Keep ProjectGit in sync with packages in the organization of a devel project
%package workflow-pr
Summary: Keeps ProjectGit PR in-sync with a PackageGit PR
Requires: openssh-clients
Requires: git-core
%description workflow-pr
Keeps ProjectGit PR in-sync with a PackageGit PR
%prep
cp -r /home/abuild/rpmbuild/SOURCES/* ./
cd gitea-events-rabbitmq-publisher && tar x --zstd -f %{SOURCE1}
%build
go build \
-C devel-importer \
-buildmode=pie
go build \
-C utils/hujson \
-buildmode=pie
go build \
-C utils/maintainer-update \
-buildmode=pie
go build \
-C gitea-events-rabbitmq-publisher \
-mod=vendor \
-buildmode=pie
go build \
-C gitea_status_proxy \
-buildmode=pie
go build \
-C group-review \
-buildmode=pie
go build \
-C obs-forward-bot \
-buildmode=pie
go build \
-C obs-staging-bot \
-buildmode=pie
go build \
-C obs-status-service \
-buildmode=pie
go build \
-C workflow-direct \
-buildmode=pie
go build \
-C workflow-pr \
-buildmode=pie
%check
go test -C common -v
go test -C group-review -v
go test -C obs-staging-bot -v
go test -C obs-status-service -v
go test -C workflow-direct -v
go test -C utils/maintainer-update
# TODO build fails
#go test -C workflow-pr -v
%install
install -D -m0755 devel-importer/devel-importer %{buildroot}%{_bindir}/devel-importer
install -D -m0755 gitea-events-rabbitmq-publisher/gitea-events-rabbitmq-publisher %{buildroot}%{_bindir}/gitea-events-rabbitmq-publisher
install -D -m0644 systemd/gitea-events-rabbitmq-publisher.service %{buildroot}%{_unitdir}/gitea-events-rabbitmq-publisher.service
install -D -m0755 gitea_status_proxy/gitea_status_proxy %{buildroot}%{_bindir}/gitea_status_proxy
install -D -m0755 group-review/group-review %{buildroot}%{_bindir}/group-review
install -D -m0644 systemd/group-review@.service %{buildroot}%{_unitdir}/group-review@.service
install -D -m0755 obs-forward-bot/obs-forward-bot %{buildroot}%{_bindir}/obs-forward-bot
install -D -m0755 obs-staging-bot/obs-staging-bot %{buildroot}%{_bindir}/obs-staging-bot
install -D -m0644 systemd/obs-staging-bot.service %{buildroot}%{_unitdir}/obs-staging-bot.service
install -D -m0755 obs-status-service/obs-status-service %{buildroot}%{_bindir}/obs-status-service
install -D -m0644 systemd/obs-status-service.service %{buildroot}%{_unitdir}/obs-status-service.service
install -D -m0755 workflow-direct/workflow-direct %{buildroot}%{_bindir}/workflow-direct
install -D -m0644 systemd/workflow-direct@.service %{buildroot}%{_unitdir}/workflow-direct@.service
install -D -m0644 systemd/workflow-direct.target %{buildroot}%{_unitdir}/workflow-direct.target
install -D -m0755 workflow-pr/workflow-pr %{buildroot}%{_bindir}/workflow-pr
install -D -m0644 systemd/workflow-pr@.service %{buildroot}%{_unitdir}/workflow-pr@.service
install -D -m0644 systemd/workflow-pr.target %{buildroot}%{_unitdir}/workflow-pr.target
install -D -m0755 utils/hujson/hujson %{buildroot}%{_bindir}/hujson
install -D -m0755 utils/maintainer-update/maintainer-update %{buildroot}%{_bindir}/maintainer-update
%pre gitea-events-rabbitmq-publisher
%pre -n gitea-events-rabbitmq-publisher
%service_add_pre gitea-events-rabbitmq-publisher.service
%post gitea-events-rabbitmq-publisher
%post -n gitea-events-rabbitmq-publisher
%service_add_post gitea-events-rabbitmq-publisher.service
%preun gitea-events-rabbitmq-publisher
%preun -n gitea-events-rabbitmq-publisher
%service_del_preun gitea-events-rabbitmq-publisher.service
%postun gitea-events-rabbitmq-publisher
%postun -n gitea-events-rabbitmq-publisher
%service_del_postun gitea-events-rabbitmq-publisher.service
%pre group-review
%service_add_pre group-review@.service
%post group-review
%service_add_post group-review@.service
%preun group-review
%service_del_preun group-review@.service
%postun group-review
%service_del_postun group-review@.service
%pre obs-staging-bot
%service_add_pre obs-staging-bot.service
%post obs-staging-bot
%service_add_post obs-staging-bot.service
%preun obs-staging-bot
%service_del_preun obs-staging-bot.service
%postun obs-staging-bot
%service_del_postun obs-staging-bot.service
%pre obs-status-service
%service_add_pre obs-status-service.service
%post obs-status-service
%service_add_post obs-status-service.service
%preun obs-status-service
%service_del_preun obs-status-service.service
%postun obs-status-service
%service_del_postun obs-status-service.service
%pre workflow-direct
%service_add_pre workflow-direct.target
%post workflow-direct
%service_add_post workflow-direct.target
%preun workflow-direct
%service_del_preun workflow-direct.target
%postun workflow-direct
%service_del_postun workflow-direct.target
%pre workflow-pr
%service_add_pre workflow-pr.target
%post workflow-pr
%service_add_post workflow-pr.target
%preun workflow-pr
%service_del_preun workflow-pr.target
%postun workflow-pr
%service_del_postun workflow-pr.target
%files devel-importer
%license COPYING
%doc devel-importer/README.md
%{_bindir}/devel-importer
%files doc
%license COPYING
%doc doc/README.md
%doc doc/workflows.md
%files gitea-events-rabbitmq-publisher
%files -n gitea-events-rabbitmq-publisher
%license COPYING
%doc gitea-events-rabbitmq-publisher/README.md
%{_bindir}/gitea-events-rabbitmq-publisher
%{_unitdir}/gitea-events-rabbitmq-publisher.service
%files gitea-status-proxy
%license COPYING
%{_bindir}/gitea_status_proxy
%files group-review
%license COPYING
%doc group-review/README.md
%{_bindir}/group-review
%{_unitdir}/group-review@.service
%files obs-forward-bot
%license COPYING
%{_bindir}/obs-forward-bot
%files obs-staging-bot
%license COPYING
%doc obs-staging-bot/README.md
%{_bindir}/obs-staging-bot
%{_unitdir}/obs-staging-bot.service
%files obs-status-service
%license COPYING
%doc obs-status-service/README.md
%{_bindir}/obs-status-service
%{_unitdir}/obs-status-service.service
%files utils
%license COPYING
%{_bindir}/hujson
%{_bindir}/maintainer-update
%files workflow-direct
%license COPYING
%doc workflow-direct/README.md
%{_bindir}/workflow-direct
%{_unitdir}/workflow-direct@.service
%{_unitdir}/workflow-direct.target
%files workflow-pr
%license COPYING
%doc workflow-pr/README.md
%{_bindir}/workflow-pr
%{_unitdir}/workflow-pr@.service
%{_unitdir}/workflow-pr.target
%changelog

15
bots-common/Makefile Normal file
View File

@@ -0,0 +1,15 @@
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 mock_gitea_utils.go
go generate
build: api
go build

File diff suppressed because it is too large Load Diff

View File

@@ -11,14 +11,14 @@ import (
"strings"
)
const PrPattern = "PR: %s/%s!%d"
const PrPattern = "PR: %s/%s#%d"
type BasicPR struct {
Org, Repo string
Num int64
}
var validOrgAndRepoRx *regexp.Regexp = regexp.MustCompile("^[A-Za-z0-9_\\.-]+$")
var validOrgAndRepoRx *regexp.Regexp = regexp.MustCompile("^[A-Za-z0-9_-]+$")
func parsePrLine(line string) (BasicPR, error) {
var ret BasicPR
@@ -36,14 +36,10 @@ func parsePrLine(line string) (BasicPR, error) {
return ret, errors.New("missing / separator")
}
repo := strings.SplitN(org[1], "!", 2)
repo := strings.SplitN(org[1], "#", 2)
ret.Repo = repo[0]
if len(repo) != 2 {
repo = strings.SplitN(org[1], "#", 2)
ret.Repo = repo[0]
}
if len(repo) != 2 {
return ret, errors.New("Missing ! or # separator")
return ret, errors.New("Missing # separator")
}
// Gitea requires that each org and repo be [A-Za-z0-9_-]+

View File

@@ -14,7 +14,6 @@ func newStringScanner(s string) *bufio.Scanner {
}
func TestAssociatedPRScanner(t *testing.T) {
common.SetTestLogger(t)
testTable := []struct {
name string
input string
@@ -35,7 +34,7 @@ func TestAssociatedPRScanner(t *testing.T) {
},
{
"Multiple PRs",
"Some header of the issue\n\nFollowed by some description\nPR: test/foo#4\n\nPR: test/goo!5\n",
"Some header of the issue\n\nFollowed by some description\nPR: test/foo#4\n\nPR: test/goo#5\n",
[]common.BasicPR{
{Org: "test", Repo: "foo", Num: 4},
{Org: "test", Repo: "goo", Num: 5},
@@ -96,7 +95,6 @@ func TestAssociatedPRScanner(t *testing.T) {
}
func TestAppendingPRsToDescription(t *testing.T) {
common.SetTestLogger(t)
testTable := []struct {
name string
desc string
@@ -109,7 +107,7 @@ func TestAppendingPRsToDescription(t *testing.T) {
[]common.BasicPR{
{Org: "a", Repo: "b", Num: 100},
},
"something\n\nPR: a/b!100",
"something\n\nPR: a/b#100",
},
{
"Append multiple PR to end of description",
@@ -121,7 +119,7 @@ func TestAppendingPRsToDescription(t *testing.T) {
{Org: "b", Repo: "b", Num: 100},
{Org: "c", Repo: "b", Num: 100},
},
"something\n\nPR: a1/b!100\nPR: a1/c!100\nPR: a1/c!101\nPR: b/b!100\nPR: c/b!100",
"something\n\nPR: a1/b#100\nPR: a1/c#100\nPR: a1/c#101\nPR: b/b#100\nPR: c/b#100",
},
{
"Append multiple sorted PR to end of description and remove dups",
@@ -135,7 +133,7 @@ func TestAppendingPRsToDescription(t *testing.T) {
{Org: "a1", Repo: "c", Num: 101},
{Org: "a1", Repo: "b", Num: 100},
},
"something\n\nPR: a1/b!100\nPR: a1/c!100\nPR: a1/c!101\nPR: b/b!100\nPR: c/b!100",
"something\n\nPR: a1/b#100\nPR: a1/c#100\nPR: a1/c#101\nPR: b/b#100\nPR: c/b#100",
},
}

130
bots-common/config.go Normal file
View File

@@ -0,0 +1,130 @@
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"
"log"
"os"
"strings"
)
type ConfigFile struct {
GitProjectName []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
}
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{}
if err := json.Unmarshal(data, &config.GitProjectName); 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)
}
func ReadWorkflowConfig(gitea Gitea, 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, "workflow.config")
if err != nil {
return nil, fmt.Errorf("Error fetching 'workflow.config': %w", err)
}
var config AutogitConfig
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("Error parsing config file: %w", err)
}
config.GitProjectName = a[0] + "/" + prjGitRepo
if len(branch) > 0 {
config.GitProjectName = config.GitProjectName + "#" + branch
}
if len(config.Organization) < 1 {
config.Organization = a[0]
}
log.Println(config)
return &config, nil
}
func ResolveWorkflowConfigs(gitea Gitea, config *ConfigFile) (AutogitConfigs, error) {
configs := make([]*AutogitConfig, 0, len(config.GitProjectName))
for _, git_project := range config.GitProjectName {
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 {
for _, c := range configs {
if c.Organization == org && c.Branch == branch {
return c
}
}
return nil
}

View File

@@ -19,14 +19,9 @@ package common
*/
const (
GiteaTokenEnv = "GITEA_TOKEN"
GiteaHostEnv = "GITEA_HOST"
ObsUserEnv = "OBS_USER"
ObsPasswordEnv = "OBS_PASSWORD"
ObsSshkeyEnv = "OBS_SSHKEY"
ObsSshkeyFileEnv = "OBS_SSHKEYFILE"
ObsApiEnv = "OBS_API"
ObsWebEnv = "OBS_WEB"
GiteaTokenEnv = "GITEA_TOKEN"
ObsUserEnv = "OBS_USER"
ObsPasswordEnv = "OBS_PASSWORD"
DefaultGitPrj = "_ObsPrj"
PrjLinksFile = "links.json"
@@ -36,6 +31,3 @@ const (
TopicApp = "src"
)
// when set, pushing to remote does not happen, and other remote side-effects should also not happen
var IsDryRun bool

View File

@@ -1731,246 +1731,3 @@ const requestedReviewJSON = `{
"commit_id": "",
"review": null
}`
const requestStatusJSON=`{
"commit": {
"id": "e637d86cbbdd438edbf60148e28f9d75a74d51b27b01f75610f247cd18394c8e",
"message": "Update nodejs-common.changes\n",
"url": "https://src.opensuse.org/autogits/nodejs-common/commit/e637d86cbbdd438edbf60148e28f9d75a74d51b27b01f75610f247cd18394c8e",
"author": {
"name": "Adam Majer",
"email": "adamm@noreply.src.opensuse.org",
"username": "adamm"
},
"committer": {
"name": "Adam Majer",
"email": "adamm@noreply.src.opensuse.org",
"username": "adamm"
},
"verification": null,
"timestamp": "2025-09-16T12:41:02+02:00",
"added": [],
"removed": [],
"modified": [
"nodejs-common.changes"
]
},
"context": "test",
"created_at": "2025-09-16T10:50:32Z",
"description": "",
"id": 21663,
"repository": {
"id": 90520,
"owner": {
"id": 983,
"login": "autogits",
"login_name": "",
"source_id": 0,
"full_name": "",
"email": "",
"avatar_url": "https://src.opensuse.org/avatars/80a61ef3a14c3c22f0b8b1885d1a75d4",
"html_url": "https://src.opensuse.org/autogits",
"language": "",
"is_admin": false,
"last_login": "0001-01-01T00:00:00Z",
"created": "2024-06-20T09:46:37+02:00",
"restricted": false,
"active": false,
"prohibit_login": false,
"location": "",
"website": "",
"description": "",
"visibility": "public",
"followers_count": 0,
"following_count": 0,
"starred_repos_count": 0,
"username": "autogits"
},
"name": "nodejs-common",
"full_name": "autogits/nodejs-common",
"description": "",
"empty": false,
"private": false,
"fork": true,
"template": false,
"parent": {
"id": 62649,
"owner": {
"id": 64,
"login": "pool",
"login_name": "",
"source_id": 0,
"full_name": "",
"email": "",
"avatar_url": "https://src.opensuse.org/avatars/b10a8c0bede9eb4ea771b04db3149f28",
"html_url": "https://src.opensuse.org/pool",
"language": "",
"is_admin": false,
"last_login": "0001-01-01T00:00:00Z",
"created": "2023-03-01T14:41:17+01:00",
"restricted": false,
"active": false,
"prohibit_login": false,
"location": "",
"website": "",
"description": "",
"visibility": "public",
"followers_count": 2,
"following_count": 0,
"starred_repos_count": 0,
"username": "pool"
},
"name": "nodejs-common",
"full_name": "pool/nodejs-common",
"description": "",
"empty": false,
"private": false,
"fork": false,
"template": false,
"mirror": false,
"size": 134,
"language": "",
"languages_url": "https://src.opensuse.org/api/v1/repos/pool/nodejs-common/languages",
"html_url": "https://src.opensuse.org/pool/nodejs-common",
"url": "https://src.opensuse.org/api/v1/repos/pool/nodejs-common",
"link": "",
"ssh_url": "gitea@src.opensuse.org:pool/nodejs-common.git",
"clone_url": "https://src.opensuse.org/pool/nodejs-common.git",
"original_url": "",
"website": "",
"stars_count": 0,
"forks_count": 3,
"watchers_count": 12,
"open_issues_count": 0,
"open_pr_counter": 0,
"release_counter": 0,
"default_branch": "factory",
"archived": false,
"created_at": "2024-06-17T17:08:45+02:00",
"updated_at": "2025-08-21T21:58:31+02:00",
"archived_at": "1970-01-01T01:00:00+01:00",
"permissions": {
"admin": true,
"push": true,
"pull": true
},
"has_issues": true,
"internal_tracker": {
"enable_time_tracker": false,
"allow_only_contributors_to_track_time": true,
"enable_issue_dependencies": true
},
"has_wiki": false,
"has_pull_requests": true,
"has_projects": false,
"projects_mode": "all",
"has_releases": false,
"has_packages": false,
"has_actions": false,
"ignore_whitespace_conflicts": false,
"allow_merge_commits": true,
"allow_rebase": true,
"allow_rebase_explicit": true,
"allow_squash_merge": true,
"allow_fast_forward_only_merge": true,
"allow_rebase_update": true,
"allow_manual_merge": true,
"autodetect_manual_merge": true,
"default_delete_branch_after_merge": false,
"default_merge_style": "merge",
"default_allow_maintainer_edit": false,
"avatar_url": "",
"internal": false,
"mirror_interval": "",
"object_format_name": "sha256",
"mirror_updated": "0001-01-01T00:00:00Z",
"topics": [],
"licenses": []
},
"mirror": false,
"size": 143,
"language": "",
"languages_url": "https://src.opensuse.org/api/v1/repos/autogits/nodejs-common/languages",
"html_url": "https://src.opensuse.org/autogits/nodejs-common",
"url": "https://src.opensuse.org/api/v1/repos/autogits/nodejs-common",
"link": "",
"ssh_url": "gitea@src.opensuse.org:autogits/nodejs-common.git",
"clone_url": "https://src.opensuse.org/autogits/nodejs-common.git",
"original_url": "",
"website": "",
"stars_count": 0,
"forks_count": 1,
"watchers_count": 4,
"open_issues_count": 0,
"open_pr_counter": 1,
"release_counter": 0,
"default_branch": "factory",
"archived": false,
"created_at": "2024-07-01T13:29:03+02:00",
"updated_at": "2025-09-16T12:41:03+02:00",
"archived_at": "1970-01-01T01:00:00+01:00",
"permissions": {
"admin": true,
"push": true,
"pull": true
},
"has_issues": false,
"has_wiki": false,
"has_pull_requests": true,
"has_projects": false,
"projects_mode": "all",
"has_releases": false,
"has_packages": false,
"has_actions": false,
"ignore_whitespace_conflicts": false,
"allow_merge_commits": true,
"allow_rebase": true,
"allow_rebase_explicit": true,
"allow_squash_merge": true,
"allow_fast_forward_only_merge": true,
"allow_rebase_update": true,
"allow_manual_merge": true,
"autodetect_manual_merge": true,
"default_delete_branch_after_merge": false,
"default_merge_style": "merge",
"default_allow_maintainer_edit": false,
"avatar_url": "",
"internal": false,
"mirror_interval": "",
"object_format_name": "sha256",
"mirror_updated": "0001-01-01T00:00:00Z",
"topics": [],
"licenses": [
"MIT"
]
},
"sender": {
"id": 129,
"login": "adamm",
"login_name": "",
"source_id": 0,
"full_name": "Adam Majer",
"email": "adamm@noreply.src.opensuse.org",
"avatar_url": "https://src.opensuse.org/avatars/3e8917bfbf04293f7c20c28cacd83dae2ba9b78a6c6a9a1bedf14c683d8a3763",
"html_url": "https://src.opensuse.org/adamm",
"language": "",
"is_admin": false,
"last_login": "0001-01-01T00:00:00Z",
"created": "2023-07-21T16:43:48+02:00",
"restricted": false,
"active": false,
"prohibit_login": false,
"location": "",
"website": "",
"description": "",
"visibility": "public",
"followers_count": 1,
"following_count": 0,
"starred_repos_count": 0,
"username": "adamm"
},
"sha": "e637d86cbbdd438edbf60148e28f9d75a74d51b27b01f75610f247cd18394c8e",
"state": "pending",
"target_url": "https://src.opensuse.org/",
"updated_at": "2025-09-16T10:50:32Z"
}`

925
bots-common/git_utils.go Normal file
View File

@@ -0,0 +1,925 @@
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"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
)
//go:generate mockgen -source=git_utils.go -destination=mock/git_utils.go -typed
type GitSubmoduleLister interface {
GitSubmoduleList(gitPath, commitId string) (submoduleList map[string]string, err error)
GitSubmoduleCommitId(cwd, packageName, commitId string) (subCommitId string, valid bool)
}
type GitStatusLister interface {
GitStatus(cwd string) ([]GitStatusData, error)
}
type Git interface {
GitParseCommits(cwd string, commitIDs []string) (parsedCommits []GitCommit, err error)
GitCatFile(cwd, commitId, filename string) (data []byte, err error)
GetPath() string
GitBranchHead(gitDir, branchName string) (string, error)
io.Closer
GitSubmoduleLister
GitStatusLister
GitExecWithOutputOrPanic(cwd string, params ...string) string
GitExecOrPanic(cwd string, params ...string)
GitExec(cwd string, params ...string) error
GitExecWithOutput(cwd string, params ...string) (string, error)
}
type GitHandlerImpl struct {
DebugLogger bool
GitPath string
GitCommiter string
GitEmail string
}
func (s *GitHandlerImpl) GetPath() string {
return s.GitPath
}
type GitHandlerGenerator interface {
CreateGitHandler(git_author, email, prjName string) (Git, error)
ReadExistingPath(git_author, email, gitPath string) (Git, error)
}
type GitHandlerGeneratorImpl struct{}
func (s *GitHandlerGeneratorImpl) CreateGitHandler(git_author, email, prj_name string) (Git, error) {
gitPath, err := os.MkdirTemp("", prj_name)
if err != nil {
return nil, fmt.Errorf("Cannot create temp dir: %w", err)
}
if err = os.Chmod(gitPath, 0700); err != nil {
return nil, fmt.Errorf("Cannot fix permissions of temp dir: %w", err)
}
return s.ReadExistingPath(git_author, email, gitPath)
}
func (*GitHandlerGeneratorImpl) ReadExistingPath(git_author, email, gitPath string) (Git, error) {
git := &GitHandlerImpl{
GitCommiter: git_author,
GitPath: gitPath,
}
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 (e *GitHandlerImpl) GitBranchHead(gitDir, branchName string) (string, error) {
id, err := e.GitExecWithOutput(gitDir, "rev-list", "-1", branchName)
if err != nil {
return "", fmt.Errorf("Can't find default remote branch: %s", branchName)
}
return strings.TrimSpace(id), nil
}
func (e *GitHandlerImpl) 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 *GitHandlerImpl) GitExecWithOutputOrPanic(cwd string, params ...string) string {
out, err := e.GitExecWithOutput(cwd, params...)
if err != nil {
log.Panicln("git command failed:", params, "@", cwd, "err:", err)
}
return out
}
func (e *GitHandlerImpl) GitExecOrPanic(cwd string, params ...string) {
if err := e.GitExec(cwd, params...); err != nil {
log.Panicln("git command failed:", params, "@", cwd, "err:", err)
}
}
func (e *GitHandlerImpl) GitExec(cwd string, params ...string) error {
_, err := e.GitExecWithOutput(cwd, params...)
return err
}
var ExtraGitParams []string
func (e *GitHandlerImpl) GitExecWithOutput(cwd string, params ...string) (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",
}
if len(ExtraGitParams) > 0 {
cmd.Env = append(cmd.Env, ExtraGitParams...)
}
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 string(out), 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 GitCommit struct {
Tree string
Msg string
}
type GitTreeEntry struct {
name string
mode int
hash string
size int
}
type GitTree struct {
items []GitTreeEntry
}
func (t *GitTreeEntry) isSubmodule() bool {
return (t.mode & 0170000) == 0160000
}
func (t *GitTreeEntry) isTree() bool {
return (t.mode & 0170000) == 0040000
}
func (t *GitTreeEntry) 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) (GitCommit, error) {
hdr, err := parseGitMsg(data)
if err != nil {
return GitCommit{}, err
} else if hdr.itemType != "commit" {
return GitCommit{}, fmt.Errorf("expected commit but parsed %s", hdr.itemType)
}
var c GitCommit
l := hdr.size
for {
hdr, err := parseGitCommitHdr(data)
if err != nil {
return GitCommit{}, 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) (GitTreeEntry, error) {
var e GitTreeEntry
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) (GitTree, error) {
hdr, err := parseGitMsg(data)
if err != nil {
return GitTree{}, err
}
// max capacity to length of hash
t := GitTree{items: make([]GitTreeEntry, 0, hdr.size/len(hdr.hash))}
parsedLen := 0
for parsedLen < hdr.size {
entry, err := parseTreeEntry(data, len(hdr.hash)/2)
if err != nil {
return GitTree{}, 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
}
func (e *GitHandlerImpl) GitParseCommits(cwd string, commitIDs []string) (parsedCommits []GitCommit, err error) {
var done sync.Mutex
done.Lock()
data_in, data_out := ChanIO{make(chan byte, 256)}, ChanIO{make(chan byte, 70)}
parsedCommits = make([]GitCommit, 0, len(commitIDs))
go func() {
defer done.Unlock()
defer close(data_out.ch)
for _, id := range commitIDs {
data_out.Write([]byte(id))
data_out.ch <- '\x00'
c, e := parseGitCommit(data_in.ch)
if e != nil {
err = fmt.Errorf("Error parsing git commit: %w", e)
return
}
parsedCommits = append(parsedCommits, c)
}
}()
cmd := exec.Command("/usr/bin/git", "cat-file", "--batch", "-Z")
cmd.Env = []string{
"GIT_CEILING_DIRECTORIES=" + e.GitPath,
"GIT_LFS_SKIP_SMUDGE=1",
"GIT_CONFIG_GLOBAL=/dev/null",
}
cmd.Dir = filepath.Join(e.GitPath, 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
}
// TODO: support sub-trees
func (e *GitHandlerImpl) 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_LFS_SKIP_SMUDGE=1",
"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
}
// return (filename) -> (hash) map for all submodules
// TODO: recursive? map different orgs, not just assume '.' for path
func (e *GitHandlerImpl) GitSubmoduleList(gitPath, 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 GitCommit
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 GitTree
tree, err = parseGitTree(data_in.ch)
if err != nil {
err = fmt.Errorf("Error parsing git tree: %w", err)
return
}
for _, te := range tree.items {
if te.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_LFS_SKIP_SMUDGE=1",
"GIT_CONFIG_GLOBAL=/dev/null",
}
cmd.Dir = filepath.Join(e.GitPath, gitPath)
cmd.Stdout = &data_in
cmd.Stdin = &data_out
cmd.Stderr = writeFunc(func(data []byte) (int, error) {
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 *GitHandlerImpl) GitSubmoduleCommitId(cwd, packageName, commitId string) (subCommitId string, valid bool) {
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 func() {
if recover() != nil {
subCommitId = "wrong"
commitId = "ok"
valid = false
}
}()
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_LFS_SKIP_SMUDGE=1",
"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)
}
const (
GitStatus_Untracked = 0
GitStatus_Modified = 1
GitStatus_Ignored = 2
GitStatus_Unmerged = 3 // States[0..3] -- Stage1, Stage2, Stage3 of merge objects
GitStatus_Renamed = 4 // orig name in States[0]
)
type GitStatusData struct {
Path string
Status int
States [3]string
}
func parseGitStatusHexString(data io.ByteReader) (string, error) {
str := make([]byte, 0, 32)
for {
c, err := data.ReadByte()
if err != nil {
return "", err
}
switch {
case c == 0 || c == ' ':
return string(str), nil
case c >= 'a' && c <= 'f':
case c >= 'A' && c <= 'F':
case c >= '0' && c <= '9':
default:
return "", errors.New("Invalid character in hex string:" + string(c))
}
str = append(str, c)
}
}
func parseGitStatusString(data io.ByteReader) (string, error) {
str := make([]byte, 0, 100)
for {
c, err := data.ReadByte()
if err != nil {
return "", errors.New("Unexpected EOF. Expected NUL string term")
}
if c == 0 {
return string(str), nil
}
str = append(str, c)
}
}
func skipGitStatusEntry(data io.ByteReader, skipSpaceLen int) error {
for skipSpaceLen > 0 {
c, err := data.ReadByte()
if err != nil {
return err
}
if c == ' ' {
skipSpaceLen--
}
}
return nil
}
func parseSingleStatusEntry(data io.ByteReader) (*GitStatusData, error) {
ret := GitStatusData{}
statusType, err := data.ReadByte()
if err != nil {
return nil, nil
}
switch statusType {
case '1':
var err error
if err = skipGitStatusEntry(data, 8); err != nil {
return nil, err
}
ret.Status = GitStatus_Modified
ret.Path, err = parseGitStatusString(data)
if err != nil {
return nil, err
}
case '2':
var err error
if err = skipGitStatusEntry(data, 9); err != nil {
return nil, err
}
ret.Status = GitStatus_Renamed
ret.Path, err = parseGitStatusString(data)
if err != nil {
return nil, err
}
ret.States[0], err = parseGitStatusString(data)
if err != nil {
return nil, err
}
case '?':
var err error
if err = skipGitStatusEntry(data, 1); err != nil {
return nil, err
}
ret.Status = GitStatus_Untracked
ret.Path, err = parseGitStatusString(data)
if err != nil {
return nil, err
}
case '!':
var err error
if err = skipGitStatusEntry(data, 1); err != nil {
return nil, err
}
ret.Status = GitStatus_Ignored
ret.Path, err = parseGitStatusString(data)
if err != nil {
return nil, err
}
case 'u':
var err error
if err = skipGitStatusEntry(data, 7); err != nil {
return nil, err
}
if ret.States[0], err = parseGitStatusHexString(data); err != nil {
return nil, err
}
if ret.States[1], err = parseGitStatusHexString(data); err != nil {
return nil, err
}
if ret.States[2], err = parseGitStatusHexString(data); err != nil {
return nil, err
}
ret.Status = GitStatus_Unmerged
ret.Path, err = parseGitStatusString(data)
if err != nil {
return nil, err
}
default:
return nil, errors.New("Invalid status type" + string(statusType))
}
return &ret, nil
}
func parseGitStatusData(data io.ByteReader) ([]GitStatusData, error) {
ret := make([]GitStatusData, 0, 10)
for {
data, err := parseSingleStatusEntry(data)
if err != nil {
return nil, err
} else if data == nil {
break
}
ret = append(ret, *data)
}
return ret, nil
}
func (e *GitHandlerImpl) GitStatus(cwd string) (ret []GitStatusData, err error) {
if e.DebugLogger {
log.Println("getting git-status()")
}
cmd := exec.Command("/usr/bin/git", "status", "--porcelain=2", "-z")
cmd.Env = []string{
"GIT_CEILING_DIRECTORIES=" + e.GitPath,
"GIT_LFS_SKIP_SMUDGE=1",
"GIT_CONFIG_GLOBAL=/dev/null",
}
cmd.Dir = filepath.Join(e.GitPath, cwd)
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)
}
out, err := cmd.Output()
if err != nil {
log.Printf("Error running command %v, err: %v", cmd.Args, err)
}
return parseGitStatusData(bufio.NewReader(bytes.NewReader(out)))
}

View File

@@ -24,131 +24,12 @@ import (
"os"
"os/exec"
"path"
"path/filepath"
"slices"
"strings"
"testing"
"time"
)
func TestGitClone(t *testing.T) {
SetTestLogger(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 TestGitCloneCommitID(t *testing.T) {
SetTestLogger(t)
execPath, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
d := t.TempDir()
if err := os.Chdir(d); err != nil {
t.Fatal(err)
}
defer os.Chdir(execPath)
cmd := exec.Command(path.Join(execPath, "test_repo_setup.sh"))
if out, err := cmd.CombinedOutput(); err != nil {
t.Log(string(out))
t.Fatal(err)
}
gh, err := AllocateGitWorkTree(d, "Test", "test@example.com")
if err != nil {
t.Fatal(err)
}
g, err := gh.CreateGitHandler("org")
if err != nil {
t.Fatal(err)
}
// Get a commit ID from pkgA
remoteUrl := "file://" + d + "/pkgA"
out, err := exec.Command("git", "-C", path.Join(d, "pkgA"), "rev-parse", "main").Output()
if err != nil {
t.Fatal(err)
}
commitID := strings.TrimSpace(string(out))
repo := "pkgAcloneCommitID"
if _, err := g.GitClone(repo, commitID, remoteUrl); err != nil {
t.Skip("TODO: Add GitClone CommitID support")
t.Fatalf("GitClone failed with commit ID: %v", err)
}
// Verify we are at the right commit
head, err := g.GitBranchHead(repo, commitID)
if err != nil {
t.Fatalf("GitBranchHead failed: %v", err)
}
if head != commitID {
t.Errorf("Expected head %s, got %s", commitID, head)
}
}
func TestGitMsgParsing(t *testing.T) {
SetTestLogger(t)
t.Run("tree message with size 56", func(t *testing.T) {
const hdr = "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f98 tree 56\x00"
@@ -227,7 +108,6 @@ func TestGitMsgParsing(t *testing.T) {
}
func TestGitCommitParsing(t *testing.T) {
SetTestLogger(t)
t.Run("parse valid commit message", func(t *testing.T) {
const commitData = "f40888ea4515fe2e8eea617a16f5f50a45f652d894de3ad181d58de3aafb8f99 commit 253\000" +
`tree e20033df9f18780756ba4a96dbc7eb1a626253961039cb674156f266ba7a4e53
@@ -256,7 +136,7 @@ committer Adam Majer <amajer@suse.com> 1720709149 +0200
})
t.Run("parse multiline headers", func(t *testing.T) {
const commitData = "cae5831ab48470ff060a5aaa12eb6e5a7acaf91e commit 1492\000" +
const commitData = "cae5831ab48470ff060a5aaa12eb6e5a7acaf91e commit 1491\x00" +
`tree 1f9c8fe8099615d6d3921528402ac53f09213b02
parent e08a654fae0ecc91678819e0b62a2e014bad3339
author Yagiz Nizipli <yagiz@nizipli.com> 1720967314 -0400
@@ -288,7 +168,7 @@ 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"
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>` + "\x00"
ch := make(chan byte, 5000)
for _, b := range []byte(commitData) {
@@ -309,51 +189,6 @@ Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>` + "\000"
}
})
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"
@@ -408,37 +243,9 @@ dummy change, don't merge
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) {
SetTestLogger(t)
func TestCommitTreeParsingOfHead(t *testing.T) {
gitDir := t.TempDir()
testDir, _ := os.Getwd()
var commitId string
@@ -449,63 +256,15 @@ func TestCommitTreeParsing(t *testing.T) {
commitId = commitId + strings.TrimSpace(string(data))
return len(data), nil
})
cmd.Stderr = os.Stderr
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()
h := GitHandlerImpl{
GitPath: gitDir,
}
id, ok := h.GitSubmoduleCommitId("", "nodejs21", commitId)
if !ok {
t.Error("failed parse")
@@ -516,9 +275,9 @@ func TestCommitTreeParsing(t *testing.T) {
})
t.Run("reads README.md", func(t *testing.T) {
h, _ := gh.ReadExistingPath(".")
defer h.Close()
h := GitHandlerImpl{
GitPath: gitDir,
}
data, err := h.GitCatFile("", commitId, "README.md")
if err != nil {
t.Errorf("failed parse: %v", err)
@@ -529,8 +288,9 @@ func TestCommitTreeParsing(t *testing.T) {
})
t.Run("read HEAD", func(t *testing.T) {
h, _ := gh.ReadExistingPath(".")
defer h.Close()
h := GitHandlerImpl{
GitPath: gitDir,
}
data, err := h.GitSubmoduleList("", "HEAD")
if err != nil {
@@ -547,7 +307,6 @@ func TestCommitTreeParsing(t *testing.T) {
}
func TestGitStatusParse(t *testing.T) {
SetTestLogger(t)
testData := []struct {
name string
data []byte
@@ -614,8 +373,6 @@ func TestGitStatusParse(t *testing.T) {
Path: ".gitmodules",
Status: GitStatus_Unmerged,
States: [3]string{"587ec403f01113f2629da538f6e14b84781f70ac59c41aeedd978ea8b1253a76", "d23eb05d9ca92883ab9f4d28f3ec90c05f667f3a5c8c8e291bd65e03bac9ae3c", "087b1d5f22dbf0aa4a879fff27fff03568b334c90daa5f2653f4a7961e24ea33"},
SubmoduleChanges: "N...",
},
},
},
@@ -623,13 +380,14 @@ func TestGitStatusParse(t *testing.T) {
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",
{
Path: "pkgQ",
Status: GitStatus_Renamed,
States: [3]string{"pkgC"},
},
{
Path: ".gitmodules",
{
Path: ".gitmodules",
Status: GitStatus_Modified,
},
},
@@ -654,108 +412,3 @@ func TestGitStatusParse(t *testing.T) {
})
}
}
func TestGitDirectoryListRepro(t *testing.T) {
SetTestLogger(t)
d := t.TempDir()
// Setup a mock environment for GitHandlerImpl
gh, err := AllocateGitWorkTree(d, "Test", "test@example.com")
if err != nil {
t.Fatal(err)
}
org := "repo-org"
repoName := "test-repo"
repoPath := filepath.Join(d, org, repoName)
err = os.MkdirAll(repoPath, 0755)
if err != nil {
t.Fatal(err)
}
runGit := func(args ...string) {
cmd := exec.Command("git", args...)
cmd.Dir = repoPath
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git %v failed: %v\n%s", args, err, out)
}
}
runGit("init", "-b", "main", "--object-format=sha256")
runGit("config", "user.email", "test@example.com")
runGit("config", "user.name", "test")
// Create a directory and a file
err = os.Mkdir(filepath.Join(repoPath, "subdir"), 0755)
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(filepath.Join(repoPath, "subdir", "file.txt"), []byte("hello"), 0644)
if err != nil {
t.Fatal(err)
}
runGit("add", "subdir/file.txt")
runGit("commit", "-m", "add subdir")
// Now create the handler
g, err := gh.CreateGitHandler(org)
if err != nil {
t.Fatal(err)
}
// Call GitDirectoryList
dirs, err := g.GitDirectoryList(repoName, "HEAD")
if err != nil {
t.Fatal(err)
}
t.Logf("Directories found: %v", dirs)
if len(dirs) == 0 {
t.Error("No directories found, but 'subdir' should be there")
}
if _, ok := dirs["subdir"]; !ok {
t.Errorf("Expected 'subdir' in directory list, got %v", dirs)
}
}
func TestGitDeadlockFix(t *testing.T) {
gitDir := t.TempDir()
testDir, _ := os.Getwd()
cmd := exec.Command("/usr/bin/bash", path.Join(testDir, "tsetup.sh"))
cmd.Dir = gitDir
_, err := cmd.CombinedOutput()
gh, err := AllocateGitWorkTree(gitDir, "Test", "test@example.com")
if err != nil {
t.Fatal(err)
}
h, err := gh.ReadExistingPath(".")
if err != nil {
t.Fatal(err)
}
defer h.Close()
// Use a blob ID to trigger error in GitParseCommits
// This ensures that the function returns error immediately and doesn't deadlock
blobId := "81aba862107f1e2f5312e165453955485f424612f313d6c2fb1b31fef9f82a14"
done := make(chan error)
go func() {
_, err := h.GitParseCommits("", []string{blobId})
done <- err
}()
select {
case err := <-done:
if err == nil {
t.Error("Expected error from GitParseCommits with blob ID, got nil")
} else {
// This is expected
t.Logf("Got expected error: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("GitParseCommits deadlocked! Fix is NOT working.")
}
}

Some files were not shown because too many files have changed in this diff Show More