diff --git a/.gitea/workflows/gemini-review.yml b/.gitea/workflows/gemini-review.yml new file mode 100644 index 0000000..a989e50 --- /dev/null +++ b/.gitea/workflows/gemini-review.yml @@ -0,0 +1,32 @@ +name: Gemini Writing Review + +on: + pull_request: + types: [opened, synchronized, reopened] + +jobs: + gemini-review: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Run Gemini Review + env: + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} + GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITEA_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + cd scripts/gemini_review + go build -o gemini_review_bin main.go + cd ../.. + ./scripts/gemini_review/gemini_review_bin diff --git a/.gitea/workflows/word-count.yml b/.gitea/workflows/word-count.yml new file mode 100644 index 0000000..f037306 --- /dev/null +++ b/.gitea/workflows/word-count.yml @@ -0,0 +1,31 @@ +name: Word Count + +on: + push: + branches: [ "main", "master" ] + pull_request: + branches: [ "main", "master" ] + +jobs: + count-words: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Run Gemini Review + env: + GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITEA_URL: ${{ github.server_url }} + GITHUB_REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + cd scripts/wordcount + go build -o wordcount_bin main.go + cd ../.. + ./scripts/wordcount/wordcount_bin diff --git a/Diplomarbeitsbuch-individueller-teil-Konstantin-Hintermayer.md b/Diplomarbeitsbuch-individueller-teil-Konstantin-Hintermayer.md index 4e27d03..7f39709 100644 --- a/Diplomarbeitsbuch-individueller-teil-Konstantin-Hintermayer.md +++ b/Diplomarbeitsbuch-individueller-teil-Konstantin-Hintermayer.md @@ -4,8 +4,30 @@ gitea: none --- # Technologie -## Entwicklung mit Asp.Net (Was ist Blazor? / Was ist Razor? / Kestrel) +## Entwicklung mit ASP.NET (Was ist Blazor? / Was ist Razor? / Kestrel) ## Was ist Oqtane? Architektur von Oqtane? +Oqtane ist ein Framework und CMS zur Entwicklung von Webseiten mithilfe von ASP.NET und Blazor. [^5] Ein Oqtane-System besteht aus mehreren Komponenten. + +In dieser Diplomarbeit fokussieren wir uns hauptsächlich auf `Themes` und `Modules`, aber es gibt auch `Language Packs` und `Pure Extensions`. [^6] + +Ein `Module` (Modul) soll neue Funktionalitäten in das CMS hinzufügen und ein `Theme` soll die ganze Gestaltung der Website (die Shell) festlegen. [^6] + +[^5]: https://www.oqtane.org/#about +[^6]: https://docs.oqtane.org/dev/extensions/index.html + +### Architektur eines Moduls +Ein Modul in Oqtane besteht aus 4 Projekten. Server, Client, Shared und Package. + +Im Server-Projekt liegt Sourcecode, welcher serverseitig ausgeführt werden soll. In der Praxis bedeutet das: alle Repositories, Controller, Manager, Migrationen und Server-Services (entwickelt nach einem Interface definiert im Client) und Server-Startuplogik. + +Im Client-Projekt liegen Code und Razor-Komponenten für den Client. Also Client-Staruplogik, Client-Services (+ Inferfaces dafür, die Services hier sollen lediglich die Server-Services über HTTP aufrufen), Ressourcendateien (.resx), die Komponenten / das User Interface und die Moduldefinitionen für jedes Modul. + +Im Shared-Projekt wird geteilter Sourcecode abgelegt, der server- und clientseitig verwendet wird. In der Praxis bleibt es hierbei bei den EntityFramework-Modellen zum Speichern der Daten im Arbeitsspeicher. + +Im Package Projekt findet man Skripte zum Debuggen und Releasen eines Moduls. Und die NuGet-Spezifikation. +- Beim Debug werden die DLLs, PDBs und statischen Assets wie Skripte und Stylesheets der 3 anderen Projekte in den bereits gebauten Oqtane.Server `oqtane.framework/oqtane.server/bin/debug/net10.0/...` kopiert. +- Beim Release wird ein NuGet-Paket erstellt und unter oqtane.framework/oqtane.server/Packages abgelegt. Dort abgelegte NuGet-Pakete werden beim nächsten Start des Oqtane Servers installiert (DB Migrationen werden gemacht und die Pakete entpackt). + ## Systemarchitektur (Postgres / Oqtane / Nginx ) ```mermaid architecture-beta @@ -27,13 +49,13 @@ architecture-beta ## Dependency injection ### Dependency Inversion Principle [^1] -Das Dependency-Inversion-Principle (DIP / auf Deutsch: Abhängigkeits-Umkehr-Prinzip) ist eines von den 5 `SOLID` Prinzipien in der Softwareentwicklung. +Das Dependency-Inversion-Principle (DIP / auf Deutsch: Abhängigkeits-Umkehr-Prinzip) ist eines von den fünf `SOLID` Prinzipien in der Softwareentwicklung. Das DIP unterscheidet zwischen high-level und low-level Modulen. -- Die high-level Module beschreiben die Applikations- / Buisnesslogik, ohne direkt mit den low-level Modulen zu interagieren, sondern lediglich auf abstraktionen. [^3] -- Die Abstraktionen sollen nicht von Implementierungsdetails abhängig sein, sondern die low-level Implementierung sollen gemäß der Abstraktionsschickt implemetiert werden. [^3] +- Die High-Level-Module beschreiben die Applikations- / Businesslogik, ohne direkt mit den Low-Level-Modulen zu interagieren, sondern lediglich auf Abstraktionen. [^3] +- Die Abstraktionen sollen nicht von Implementierungsdetails abhängig sein, sondern die Low-Level-Implementierung sollen gemäß der Abstraktionsschicht implementiert werden. [^3] -Ausgangslage ist eine Softwarearchitektur im Direct-Dependency-Graph Model. +Ausgangslage ist eine Softwarearchitektur im Direct-Dependency-Graph-Modell. ```mermaid architecture-beta @@ -45,7 +67,7 @@ architecture-beta b:R --> L:c ``` Bei diesem Beispiel ist die Klasse A ein high-level Modul, welches direkt auf die Klasse B referenziert, was das DI-Prinzip verbietet. -Das Problem dabei: Die einzelnen Klassen sind eng gekoppelt, was das austauschen von B mit einer anderen Klasse unmöglich macht. Genau dieses Problem wird vom DIP gelöst. +Das Problem dabei: Die einzelnen Klassen sind eng gekoppelt, was das Austauschen von B mit einer anderen Klasse unmöglich macht. Genau dieses Problem wird vom DIP gelöst. ```mermaid architecture-beta @@ -60,14 +82,14 @@ architecture-beta b:B --> T:ic ic:R <-- L:c ``` -Das high-level Modul ruft lediglich eine Abstraktion eines low-level Moduls auf, welche von einem, oder mehreren low-level Modulen implementiert worden ist. Für das high-level Modul ist es hier egal, welches low-level Modul die Implementierung bereitstellt. Dadurch erhält man einen viel modulareren Aufbau in der Software. Die einzelnen Module sind auch leichter austauschbar, testbar. Genau diese Modularität macht dependency injection möglich. +Das High-Level-Modul ruft lediglich eine Abstraktion eines Low-Level-Moduls auf, welche von einem, oder mehreren Low-Level-Modulen implementiert wurde. Für das High-Level-Modul ist es hier egal, welches Low-Level-Modul die Implementierung bereitstellt. Dadurch erhält man einen viel modulareren Aufbau in der Software. Die einzelnen Module sind auch leichter austauschbar und testbar. Genau diese Modularität macht Dependency Injection möglich. ### Microsoft Dependency Injection Framework -Dependency Injektion ist in .Net genau so wie Konfiguration, Protokollierung und das Optionsmuster ins Framework integriert. [^4] +Dependency Injektion ist in .NET genau so wie Konfiguration, Protokollierung und das Optionsmuster ins Framework integriert. [^4] -Alle Dependencies werden in einem `service container` zur verwaltung registriert. .Net hat einen eingebauten `service container` (eine Implementierung des `IServiceProvider`). [^4] +Alle Dependencies werden in einem `Service-Container` zur Verwaltung registriert. .NET hat einen eingebauten `Service-Container` (eine Implementierung des `IServiceProvider`). [^4] -Das Dependency Injection Framework verwaltet alle Instanzen. Nach Bedarf werden instanzen erstellt, oder wieder entsorgt (sofern das Service nicht mehr gebraucht wird). Beim instanzieren einer Klasse werden alle im Konstruktor erwarteten Dependencies bereitgestellt, bzw. selbst instanziert und dannach bereitgestellt. [^4] +Das Dependency Injection Framework verwaltet alle Instanzen. Nach Bedarf werden Instanzen erstellt, oder wieder entsorgt (sofern das Service nicht mehr gebraucht wird). Beim Instanziieren einer Klasse werden alle im Konstruktor erwarteten Dependencies bereitgestellt, bzw. selbst instanziiert und dannach bereitgestellt. [^4] Hier ein Beispiel aus der Dokumentation von Microsoft: [^4] ```c# @@ -105,13 +127,13 @@ public sealed class Worker(IMessageWriter messageWriter) : BackgroundService } } ``` -Das ist ein simples Beispiel, welches Teile des DI Frameworks zeigt. Wir haben einen Service (Klasse Worker), ein Dependency (Klasse MessageWriter) und eine Abstraktionsebene, von dem Dependency (Interface IMessage Writer). +Das ist ein simples Beispiel, welches Teile des DI Frameworks zeigt. Wir haben einen Service (Klasse Worker), ein Dependency (Klasse MessageWriter) und eine Abstraktionsebene, von dem Dependency (Interface IMessageWriter). -Bei Programstart wird zuerst manuell der `Service Container` erstellt, dannach alle Module registriert (entweder als HostedService, oder als Modul mit einer spezifischen Lifetime (Scoped, Transient, Singleton)). +Bei Programmstart wird zuerst manuell der `Service-Container` erstellt, dannach alle Module registriert (entweder als HostedService, oder als Modul mit einer spezifischen Lifetime (Scoped, Transient, Singleton)). -Mit dem Aufruf von `builder.Build()` wird intern ein Dependency Graph erstellt und mit `host.Run()` wird versucht die Klasse Worker zu instanzieren und zu starten. Nachdem Worker ein Dependency auf IMessageWriter hat wird über den zuvor erstelltem Dependency Graph die implementierung von IMessageWriter gesucht. Jetzt wird MessageWriter instanziert und dem Konstruktor von Worker übergeben, damit seine Dependencies befriedigt werden. +Mit dem Aufruf von `builder.Build()` wird intern ein Dependency Graph erstellt und mit `host.Run()` wird versucht die Klasse Worker zu instanziieren und zu starten. Nachdem Worker ein Dependency auf IMessageWriter hat, wird über den zuvor erstellten Dependency-Graph die Implementierung von IMessageWriter gesucht. Jetzt wird MessageWriter instanziiert und dem Konstruktor von Worker übergeben, damit seine Dependencies befriedigt werden. -So sehen der Abhängigkeitsgraph bei diesem Beispiel aus. +So sieht der Abhängigkeitsgraph bei diesem Beispiel aus. ```mermaid architecture-beta @@ -142,7 +164,7 @@ architecture-beta ## Arbeitszeiteinschätzung (Zeitverzug) ## Teamleitung (Motivation / Downsizing) ## Produktion != Staging -## Sprints und Meetings (in Zukunft ja Asyncron +## Sprints und Meetings (in Zukunft ja asynchron) # Modules ## Mass Mailer diff --git a/scripts/gemini_review/main.go b/scripts/gemini_review/main.go new file mode 100644 index 0000000..ebec13e --- /dev/null +++ b/scripts/gemini_review/main.go @@ -0,0 +1,186 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "os" + "strconv" + "strings" + + "code.gitea.io/sdk/gitea" + "github.com/google/generative-ai-go/genai" + "google.golang.org/api/option" +) + +type Suggestion struct { + LineNumber uint64 `json:"line_number"` + Comment string `json:"comment"` + Snippet string `json:"snippet,omitempty"` +} + +func main() { + apiKey := os.Getenv("GEMINI_API_KEY") + token := os.Getenv("GITEA_TOKEN") + baseURL := os.Getenv("GITEA_URL") + repoFullName := os.Getenv("GITHUB_REPOSITORY") + prNumberStr := os.Getenv("PR_NUMBER") + + if apiKey == "" || token == "" || repoFullName == "" || prNumberStr == "" { + log.Fatal("Missing required environment variables: GEMINI_API_KEY, GITEA_TOKEN, GITHUB_REPOSITORY, PR_NUMBER") + } + + if baseURL == "" { + baseURL = "https://gitea.com" + } + + prNumber, err := strconv.ParseInt(prNumberStr, 10, 64) + if err != nil { + log.Fatalf("Invalid PR_NUMBER: %v", err) + } + + repoParts := strings.Split(repoFullName, "/") + if len(repoParts) != 2 { + log.Fatalf("Invalid GITHUB_REPOSITORY format: %s", repoFullName) + } + owner, repo := repoParts[0], repoParts[1] + + ctx := context.Background() + + // Initialize Gitea Client + client, err := gitea.NewClient(baseURL, gitea.SetToken(token)) + if err != nil { + log.Fatalf("Failed to create Gitea client: %v", err) + } + + // Initialize Gemini Client + geminiClient, err := genai.NewClient(ctx, option.WithAPIKey(apiKey)) + if err != nil { + log.Fatalf("Failed to create Gemini client: %v", err) + } + defer geminiClient.Close() + + model := geminiClient.GenerativeModel("gemini-3-flash-preview") + + // Get PR files + files, _, err := client.ListPullRequestFiles(owner, repo, prNumber, gitea.ListPullRequestFilesOptions{}) + if err != nil { + log.Fatalf("Failed to get PR files: %v", err) + } + + var giteaComments []gitea.CreatePullReviewComment + for _, file := range files { + if !strings.HasSuffix(file.Filename, ".md") { + continue + } + + fmt.Printf("Reviewing file: %s\n", file.Filename) + content, err := readFile(file.Filename) + if err != nil { + fmt.Printf("Error reading file %s: %v\n", file.Filename, err) + continue + } + + suggestions, err := getGeminiReview(ctx, model, content) + if err != nil { + fmt.Printf("Error getting review for %s: %v\n", file.Filename, err) + continue + } + + for _, s := range suggestions { + body := s.Comment + if s.Snippet != "" { + body += fmt.Sprintf("\n\n```suggestion\n%s\n```", s.Snippet) + } + giteaComments = append(giteaComments, gitea.CreatePullReviewComment{ + Path: file.Filename, + Body: body, + NewLineNum: int64(s.LineNumber), + }) + } + } + + if len(giteaComments) > 0 { + _, _, err = client.CreatePullReview(owner, repo, prNumber, gitea.CreatePullReviewOptions{ + State: gitea.ReviewStateRequestChanges, + Body: "### 🤖 Gemini Writing Review\n\nI've found some areas for improvement in the documentation. Please see the inline comments below.", + Comments: giteaComments, + }) + if err != nil { + log.Fatalf("Failed to create PR review: %v", err) + } + fmt.Printf("Successfully created PR review with %d inline comments.\n", len(giteaComments)) + } else { + fmt.Println("No Markdown files to review or no suggestions found.") + } +} + +func readFile(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", err + } + defer f.Close() + + content, err := io.ReadAll(f) + if err != nil { + return "", err + } + return string(content), nil +} + +func getGeminiReview(ctx context.Context, model *genai.GenerativeModel, content string) ([]Suggestion, error) { + prompt := fmt.Sprintf(` +Review the following Markdown content for spelling errors, grammar mistakes, and style improvements. +Analyze the text line by line. + +For each issue found, provide a suggestion in JSON format with: +- "line_number": The 1-indexed line number where the issue occurs. +- "comment": A brief explanation of the problem and the suggested fix. +- "snippet": The corrected text for that line (optional, but highly recommended for spell/grammar fixes). + +Return ONLY a JSON array of suggestions. If no issues are found, return "[]". + +Content with line numbers for reference: +%s +`, addLineNumbers(content)) + + resp, err := model.GenerateContent(ctx, genai.Text(prompt)) + if err != nil { + return nil, err + } + + if len(resp.Candidates) == 0 || len(resp.Candidates[0].Content.Parts) == 0 { + return nil, nil + } + + var rawJS strings.Builder + for _, part := range resp.Candidates[0].Content.Parts { + if text, ok := part.(genai.Text); ok { + rawJS.WriteString(string(text)) + } + } + + // Clean up markdown code blocks if present + js := strings.TrimSpace(rawJS.String()) + js = strings.TrimPrefix(js, "```json") + js = strings.TrimSuffix(js, "```") + js = strings.TrimSpace(js) + + var suggestions []Suggestion + if err := json.Unmarshal([]byte(js), &suggestions); err != nil { + return nil, fmt.Errorf("failed to unmarshal suggestions: %v (raw response: %s)", err, js) + } + + return suggestions, nil +} + +func addLineNumbers(text string) string { + lines := strings.Split(text, "\n") + for i, line := range lines { + lines[i] = fmt.Sprintf("%d: %s", i+1, line) + } + return strings.Join(lines, "\n") +} diff --git a/scripts/go.mod b/scripts/go.mod new file mode 100644 index 0000000..51831ec --- /dev/null +++ b/scripts/go.mod @@ -0,0 +1,44 @@ +module gemini_review + +go 1.25.0 + +require ( + cloud.google.com/go v0.115.0 // indirect + cloud.google.com/go/ai v0.8.0 // indirect + cloud.google.com/go/auth v0.18.2 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/longrunning v0.5.7 // indirect + code.gitea.io/sdk/gitea v0.23.2 // indirect + github.com/42wim/httpsig v1.2.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davidmz/go-pageant v1.0.2 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-fed/httpsig v1.1.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/generative-ai-go v0.20.1 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect + github.com/googleapis/gax-go/v2 v2.17.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/api v0.269.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect + google.golang.org/grpc v1.79.1 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) diff --git a/scripts/go.sum b/scripts/go.sum new file mode 100644 index 0000000..127f03c --- /dev/null +++ b/scripts/go.sum @@ -0,0 +1,89 @@ +cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14= +cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU= +cloud.google.com/go/ai v0.8.0 h1:rXUEz8Wp2OlrM8r1bfmpF2+VKqc1VJpafE3HgzRnD/w= +cloud.google.com/go/ai v0.8.0/go.mod h1:t3Dfk4cM61sytiggo2UyGsDVW3RF1qGZaUKDrZFyqkE= +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU= +cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng= +code.gitea.io/sdk/gitea v0.23.2 h1:iJB1FDmLegwfwjX8gotBDHdPSbk/ZR8V9VmEJaVsJYg= +code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM= +github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs= +github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= +github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= +github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/generative-ai-go v0.20.1 h1:6dEIujpgN2V0PgLhr6c/M1ynRdc7ARtiIDPFzj45uNQ= +github.com/google/generative-ai-go v0.20.1/go.mod h1:TjOnZJmZKzarWbjUJgy+r3Ee7HGBRVLhOIgupnwR4Bg= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= +github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg= +google.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/scripts/wordcount/main.go b/scripts/wordcount/main.go new file mode 100644 index 0000000..851e3a1 --- /dev/null +++ b/scripts/wordcount/main.go @@ -0,0 +1,97 @@ +package main + +import ( + "fmt" + "io" + "log" + "os" + "strconv" + "strings" + + "code.gitea.io/sdk/gitea" +) + +func main() { + token := os.Getenv("GITEA_TOKEN") + baseURL := os.Getenv("GITEA_URL") + repoFullName := os.Getenv("GITHUB_REPOSITORY") + prNumberStr := os.Getenv("PR_NUMBER") + + if token == "" || repoFullName == "" || prNumberStr == "" { + log.Fatal("Missing required environment variables: GITEA_TOKEN, GITHUB_REPOSITORY, PR_NUMBER") + } + + if baseURL == "" { + baseURL = "https://gitea.com" + } + + prNumber, err := strconv.ParseInt(prNumberStr, 10, 64) + if err != nil { + log.Fatalf("Invalid PR_NUMBER: %v", err) + } + + repoParts := strings.Split(repoFullName, "/") + if len(repoParts) != 2 { + log.Fatalf("Invalid GITHUB_REPOSITORY format: %s", repoFullName) + } + owner, repo := repoParts[0], repoParts[1] + + client, err := gitea.NewClient(baseURL, gitea.SetToken(token)) + if err != nil { + log.Fatalf("Failed to create Gitea client: %v", err) + } + + // Get PR files + files, _, err := client.ListPullRequestFiles(owner, repo, prNumber, gitea.ListPullRequestFilesOptions{}) + if err != nil { + log.Fatalf("Failed to get PR files: %v", err) + } + + var counts []string + for _, file := range files { + if !strings.HasSuffix(file.Filename, ".md") { + continue + } + + fmt.Printf("Reviewing file: %s\n", file.Filename) + content, err := readFile(file.Filename) + if err != nil { + fmt.Printf("Error reading file %s: %v\n", file.Filename, err) + continue + } + + count := countWords(content) + counts = append(counts, fmt.Sprintf("#### Word count for `%s`\n\nWord count: %d", file.Filename, count)) + } + + if len(counts) > 0 { + commentBody := "### 🤖 Word Count Report\n\n" + strings.Join(counts, "\n\n---\n\n") + _, _, err = client.CreateIssueComment(owner, repo, prNumber, gitea.CreateIssueCommentOption{ + Body: commentBody, + }) + if err != nil { + log.Fatalf("Failed to post PR comment: %v", err) + } + fmt.Println("Successfully posted review comments.") + } else { + fmt.Println("No Markdown files to review or no suggestions found.") + } +} + +func readFile(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", err + } + defer f.Close() + + content, err := io.ReadAll(f) + if err != nil { + return "", err + } + return string(content), nil +} + +func countWords(text string) int { + return len(strings.Fields(text)) +}