From 7c9597c938aa7e3c13944270c6ffe33c69ba7703 Mon Sep 17 00:00:00 2001
From: Sebastiaan de Schaetzen <sebastiaan.de.schaetzen@gmail.com>
Date: Fri, 25 Apr 2025 10:55:12 +0200
Subject: [PATCH] First proof-of-concept version

---
 .gitignore          |   3 +-
 autobuilder.go      | 213 ++++++++++++++++++++++++++++++++++++++------
 check-up-to-date.sh |  52 +++++++++++
 gitea.go            |  10 ++-
 go.mod              |   3 +
 run.sh              |   2 +
 6 files changed, 249 insertions(+), 34 deletions(-)
 create mode 100644 check-up-to-date.sh
 create mode 100644 go.mod
 create mode 100755 run.sh

diff --git a/.gitignore b/.gitignore
index b835332..00d54de 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
+/.idea
 /venv
 /.env
-/work
\ No newline at end of file
+/work
diff --git a/autobuilder.go b/autobuilder.go
index f1037bb..99147e0 100644
--- a/autobuilder.go
+++ b/autobuilder.go
@@ -1,24 +1,16 @@
 package main
 
 import (
+	"errors"
 	"fmt"
 	"log"
 	"os"
 	"os/exec"
 	"path/filepath"
+	"strconv"
+	"strings"
 )
 
-func cloneRepository(repo Repository, workDir string) error {
-	cmd := exec.Command("git", "clone", repo.CloneURL, filepath.Join(workDir, repo.Name))
-	cmd.Stdout = os.Stdout
-	cmd.Stderr = os.Stderr
-	err := cmd.Run()
-	if err != nil {
-		return fmt.Errorf("failed to clone %s: %w", repo.Name, err)
-	}
-	return nil
-}
-
 func main() {
 	token := os.Getenv("TOKEN")
 	if token == "" {
@@ -47,30 +39,193 @@ func main() {
 		log.Fatalf("Failed to get repositories: %v", err)
 	}
 
-	success := true
+	failedRepos := make([]string, 0)
 	for _, repo := range repos {
-		log.Printf("Checking %s", repo.FullName)
-		hasFile, err := client.hasPKGBUILD(repo)
-
-		if err != nil {
-			log.Printf("Error checking for PKGBUILD: %v", err)
-			success = false
+		if repo.Template || strings.Contains(repo.Name, "skip-autobuild") {
+			// Skip template repositories
 			continue
 		}
-
-		if !hasFile {
-			log.Println("PKGBUILD not found, skipping")
-			continue
+		wasRepoProcessed := processRepo(&client, workDir, repo)
+		if !wasRepoProcessed {
+			failedRepos = append(failedRepos, repo.Name)
 		}
-
-		if err := cloneRepository(repo, workDir); err != nil {
-			success = false
-			log.Printf("Error cloning repository: %v", err)
-		}
-
 	}
 
-	if !success {
+	if len(failedRepos) > 0 {
+		log.Println("The following repos failed to process:")
+		for _, repo := range failedRepos {
+			log.Printf(" - %s", repo)
+		}
 		os.Exit(1)
 	}
 }
+
+func processRepo(client *GiteaClient, workDir string, repo Repository) (success bool) {
+	log.Printf("Checking %s", repo.FullName)
+	hasFile, err := client.hasPKGBUILD(repo)
+
+	if err != nil {
+		log.Printf("Error checking for PKGBUILD: %v", err)
+		return false
+	}
+
+	if !hasFile {
+		log.Println("PKGBUILD not found, skipping")
+		return true
+	}
+
+	repoPath, err := cloneRepository(client.Token, repo, workDir)
+	if err != nil {
+		log.Printf("Error cloning repository: %v", err)
+		return false
+	}
+
+	// Run pre-run script (if it exists)
+	requirePush := false
+	defer func() {
+		if requirePush {
+			err = pushRepository(repoPath)
+			if err != nil {
+				log.Printf("Error pushing repository: %v", err)
+				success = false
+			}
+		}
+	}()
+	preRunScript := filepath.Join(repoPath, "pre-run.sh")
+	if _, err := os.Stat(preRunScript); !os.IsNotExist(err) {
+		cmd := exec.Command("/bin/sh", "./pre-run.sh")
+		cmd.Dir = workDir
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+		err = cmd.Run()
+		if err != nil {
+			log.Printf("Error running pre-run script: %v", err)
+			return false
+		}
+		requirePush = true
+	}
+
+	// Check if the repository is up-to-date
+	upToDate, err := checkUpToDate(repoPath)
+	if err != nil {
+		log.Printf("Error checking if up to date: %v", err)
+		return false
+	}
+	if upToDate {
+		log.Printf("%s is up-to-date, skipping", repo.FullName)
+		return true
+	}
+
+	log.Printf("%s requires bumping, running script", repo.FullName)
+	updated, err := bumpRepository(repoPath)
+	if err != nil {
+		log.Printf("Error bumping repository: %v", err)
+		return false
+	}
+	if updated {
+		requirePush = true
+	}
+	return true
+}
+
+func cloneRepository(token string, repo Repository, workDir string) (string, error) {
+	targetPath := filepath.Join(workDir, repo.Name)
+	cmd := exec.Command("git", "clone", fmt.Sprintf("https://seeseemelk:%s@gitea.seeseepuff.be/archlinux/%s", token, repo.Name), targetPath)
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	err := cmd.Run()
+	if err != nil {
+		return "", fmt.Errorf("failed to clone %s: %w", repo.Name, err)
+	}
+	return targetPath, nil
+}
+
+func checkUpToDate(repoPath string) (bool, error) {
+	log.Println("Checking if up-to-date")
+	cmd := exec.Command("/bin/sh", "./check-up-to-date.sh", repoPath)
+	output, err := cmd.CombinedOutput()
+	log.Println("Command output: ", string(output))
+	if err != nil {
+		return false, fmt.Errorf("failed to inspect %s: %w", repoPath, err)
+	}
+	if strings.Index(string(output), "UP-TO-DATE") >= 0 {
+		return true, nil
+	} else if strings.Index(string(output), "OUT-OF-DATE") >= 0 {
+		return false, nil
+	}
+	return false, errors.New("error parsing command output")
+}
+
+/*
+Finds the line that says "pkgrel=NUMBER" in the repository's PKGBUILD file,
+and increments the number by 1.
+*/
+func bumpRepository(repoPath string) (bool, error) {
+	pkgbuildPath := filepath.Join(repoPath, "PKGBUILD")
+
+	// Read the file
+	content, err := os.ReadFile(pkgbuildPath)
+	if err != nil {
+		log.Fatalf("failed to read %s: %v", pkgbuildPath, err)
+	}
+
+	lines := strings.Split(string(content), "\n")
+	updated := false
+
+	// Iterate through lines to find and update pkgrel
+	for i, line := range lines {
+		if strings.HasPrefix(line, "pkgrel=") {
+			parts := strings.SplitN(line, "=", 2)
+			if len(parts) == 2 {
+				// Parse and increment the pkgrel value
+				pkgrel, err := strconv.Atoi(strings.TrimSpace(parts[1]))
+				if err != nil {
+					log.Fatalf("Failed to parse pkgrel value: %v", err)
+				}
+				lines[i] = fmt.Sprintf("pkgrel=%d", pkgrel+1)
+				updated = true
+				break
+			}
+		}
+	}
+
+	if !updated {
+		return false, fmt.Errorf("pkgrel line not found in PKGBUILD file")
+	}
+
+	// Write the updated content back to the PKGBUILD file
+	err = os.WriteFile(pkgbuildPath, []byte(strings.Join(lines, "\n")), 0644)
+	if err != nil {
+		return false, fmt.Errorf("error writing updated PKGBUILD file: %w", err)
+	}
+
+	log.Println("PKGBUILD updated, creating commit")
+
+	// Create commit
+	cmd := exec.Command("git", "commit", "PKGBUILD", "-m", "Bump pkgrel")
+	cmd.Dir = repoPath
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	err = cmd.Run()
+	if err != nil {
+		return false, fmt.Errorf("failed to commit changes: %w", err)
+	}
+
+	return true, nil
+}
+
+func pushRepository(repoPath string) error {
+	// Push commit
+	log.Println("Pushing commit")
+	cmd := exec.Command("git", "push")
+	cmd.Dir = repoPath
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+	err := cmd.Run()
+	if err != nil {
+		return fmt.Errorf("failed to push changes: %w", err)
+	}
+
+	log.Println("Successfully bumped pkgrel in PKGBUILD")
+	return nil
+}
diff --git a/check-up-to-date.sh b/check-up-to-date.sh
new file mode 100644
index 0000000..72d2a33
--- /dev/null
+++ b/check-up-to-date.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+
+# Exit on any error
+set -e
+
+cd $1
+
+# Source the PKGBUILD in a subshell to avoid polluting the global environment
+(
+    source ./PKGBUILD
+
+    # Convert pkgname to array in case it's a single string
+    pkgnames=("${pkgname[@]}")
+
+    for pkg in "${pkgnames[@]}"; do
+        echo "Checking package: $pkg"
+
+        if pacman -Si "$pkg" &>/dev/null; then
+            echo "Package '$pkg' exists in a repository."
+
+            # Get the package build date
+            pkg_build_date=$(date -d "$(pacman -Si "$pkg" | grep 'Build Date' | cut -d: -f2-)" +%s)
+
+            all_deps=("${depends[@]}" "${makedepends[@]}" "${optdepends[@]}")
+
+            # Check each dependency
+            for dep in "${all_deps[@]}"; do
+                dep_name=$(echo "$dep" | sed 's/[<>=].*//')  # Remove version constraints
+                echo "Querying dependency: $dep_name"
+
+                if pacman -Si "$dep_name" &>/dev/null; then
+                    dep_build_date=$(date -d "$(pacman -Si "$dep_name" | grep 'Build Date' | cut -d: -f2-)" +%s)
+                    if (( dep_build_date >= pkg_build_date )); then
+                        echo "Dependency '$dep_name' has newer or equal build date than '$pkg'."
+                        echo "OUT-OF-DATE"
+                        exit 0
+                    fi
+                else
+                    echo "Dependency '$dep_name' not found in repositories. Skipping."
+                fi
+            done
+
+            echo "All dependencies are older than package '$pkg'."
+            echo "UP-TO-DATE"
+            exit 0
+        else
+            echo "Package '$pkg' does NOT exist in any repository."
+            echo "OUT-OF-DATE"
+            exit 0
+        fi
+    done
+)
diff --git a/gitea.go b/gitea.go
index 3b73a6c..2e23e99 100644
--- a/gitea.go
+++ b/gitea.go
@@ -14,10 +14,12 @@ type GiteaClient struct {
 }
 
 type Repository struct {
-	Name     string `json:"name"`
-	CloneURL string `json:"clone_url"`
-	Owner    Owner  `json:"owner"`
-	FullName string `json:"full_name"`
+	Name     string   `json:"name"`
+	CloneURL string   `json:"clone_url"`
+	Owner    Owner    `json:"owner"`
+	FullName string   `json:"full_name"`
+	Template bool     `json:"template"`
+	Topics   []string `json:"topics"`
 }
 
 type Owner struct {
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..de49306
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module autobuilder
+
+go 1.24.2
diff --git a/run.sh b/run.sh
new file mode 100755
index 0000000..828e31a
--- /dev/null
+++ b/run.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env sh
+docker run -it --platform amd64 --env-file .env --rm -v ./:/app -w /app gitea.seeseepuff.be/archlinux/archlinux