initial
This commit is contained in:
commit
b5079bdfda
9 changed files with 1964 additions and 0 deletions
2
.env.template
Normal file
2
.env.template
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
FORGEJO_URL = "https://git.rpi-virtuell.de"
|
||||
FORGEJO_TOKEN = "dein-token"
|
||||
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
.git/
|
||||
.venv/
|
||||
__pycache__
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.pyw
|
||||
*.pyz
|
||||
*.pyzw
|
||||
*.pyc
|
||||
*.log
|
||||
|
||||
|
||||
/forgejo_issues/*.md
|
||||
.env
|
||||
82
README.WIN.md
Normal file
82
README.WIN.md
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# Einrichtung einer geplanten Aufgabe in Windows für automatische Updates
|
||||
|
||||
Um das Forgejo-to-Markdown Update-Skript regelmäßig automatisch auszuführen, kannst du den Windows Task Scheduler (Aufgabenplanung) verwenden. Hier ist eine Schritt-für-Schritt-Anleitung:
|
||||
|
||||
## Vorbereitung
|
||||
|
||||
1. Speichere die beiden Skripte in einem Ordner deiner Wahl:
|
||||
- `forgejo_exporter.py` (Python-Skript)
|
||||
- `update_forgejo.bat` (Batch-Skript)
|
||||
- `.env`
|
||||
|
||||
2. Stelle sicher, dass Python installiert ist und der `python`-Befehl im Pfad verfügbar ist
|
||||
|
||||
## Erstellen einer geplanten Aufgabe
|
||||
|
||||
1. Öffne die **Aufgabenplanung**:
|
||||
- Drücke `Win + R`, gib `taskschd.msc` ein und drücke Enter
|
||||
- Oder suche im Startmenü nach "Aufgabenplanung"
|
||||
|
||||
2. Klicke im rechten Bereich auf **Aufgabe erstellen...**
|
||||
|
||||
3. Im Tab **Allgemein**:
|
||||
- Gib einen **Namen** ein (z.B. "Forgejo Issues Update")
|
||||
- Optional: Füge eine Beschreibung hinzu
|
||||
- Wähle **Mit höchsten Privilegien ausführen** (falls dein Benutzer über Administratorrechte verfügt)
|
||||
- Wähle unter "Konfigurieren für:" dein Betriebssystem
|
||||
|
||||
4. Im Tab **Trigger**:
|
||||
- Klicke auf **Neu...**
|
||||
- Wähle unter "Beginnen" die Option **Nach einem Zeitplan**
|
||||
- Stelle die Häufigkeit ein (z.B. **Täglich** um 3:00 Uhr)
|
||||
- Aktiviere **Aktiviert**
|
||||
- Klicke auf **OK**
|
||||
|
||||
5. Im Tab **Aktionen**:
|
||||
- Klicke auf **Neu...**
|
||||
- Aktion: **Programm starten**
|
||||
- Programm/Skript: Gib den vollständigen Pfad zu `update_forgejo.bat` ein oder klicke auf **Durchsuchen...** um die Datei auszuwählen
|
||||
- Starten in: Gib den Ordner ein, in dem sich das Skript befindet
|
||||
- Klicke auf **OK**
|
||||
|
||||
6. Im Tab **Bedingungen**:
|
||||
- Du kannst hier optionale Einstellungen vornehmen, z.B. nur ausführen, wenn der Computer am Netzwerk angeschlossen ist
|
||||
|
||||
7. Im Tab **Einstellungen**:
|
||||
- Wähle **Aufgabe so bald wie möglich nach einem verpassten Start ausführen**
|
||||
- Wähle **Bei Bedarf aufwecken**
|
||||
- Klicke auf **OK**, um die Aufgabe zu erstellen
|
||||
|
||||
## Testen der geplanten Aufgabe
|
||||
|
||||
1. Finde deine neu erstellte Aufgabe in der Liste
|
||||
2. Rechtsklick auf die Aufgabe und wähle **Ausführen**
|
||||
3. Überprüfe, ob die Aufgabe erfolgreich ausgeführt wurde:
|
||||
- Prüfe das Logfile (`forgejo_update.log`)
|
||||
- Überprüfe, ob neue Dateien im Ausgabeordner erstellt wurden
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
Falls die geplante Aufgabe nicht wie erwartet funktioniert:
|
||||
|
||||
1. **Problem**: Die Aufgabe wird gestartet, aber Python-Fehler treten auf
|
||||
- **Lösung**: Überprüfe, ob alle erforderlichen Python-Pakete installiert sind (`pip install requests`)
|
||||
|
||||
2. **Problem**: Die Aufgabe wird gar nicht ausgeführt
|
||||
- **Lösung**: Überprüfe die Ereignisanzeige (Event Viewer) auf Fehler
|
||||
|
||||
3. **Problem**: Das Skript kann nicht auf die Ausgabeordner zugreifen
|
||||
- **Lösung**: Stelle sicher, dass der Benutzer, unter dem die Aufgabe ausgeführt wird, Schreibrechte für den Zielordner hat
|
||||
|
||||
4. **Problem**: Das Batch-Skript wird nicht gefunden
|
||||
- **Lösung**: Verwende absolute Pfade im Task Scheduler anstelle von relativen Pfaden
|
||||
|
||||
## Alternative: Ausführen beim Windows-Start
|
||||
|
||||
Wenn du möchtest, dass das Skript bei jedem Windows-Start ausgeführt wird:
|
||||
|
||||
1. Erstelle eine Verknüpfung zu `update_forgejo.bat`
|
||||
2. Drücke `Win + R`, gib `shell:startup` ein und drücke Enter
|
||||
3. Kopiere die Verknüpfung in den geöffneten Ordner
|
||||
|
||||
Dies führt das Skript jedes Mal aus, wenn du dich bei Windows anmeldest.
|
||||
128
README.md
Normal file
128
README.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# Forgejo Issues zu Markdown Export - Anleitung
|
||||
|
||||
Diese Anleitung erklärt, wie du den Forgejo-to-Markdown Exporter einrichten und verwenden kannst, um Issues in Markdown-Dateien zu exportieren und sie für Knowledge-Management-Tools wie MSTY zu nutzen.
|
||||
|
||||
## Überblick
|
||||
|
||||
Der Exporter bietet folgende Funktionen:
|
||||
|
||||
- Export aller Issues eines Repositories oder einer Organisation in Markdown-Dateien
|
||||
- Inkrementelle Updates (nur geänderte Issues werden aktualisiert)
|
||||
- Einbeziehung von Kommentaren und Metadaten
|
||||
- Formatierung als strukturierte Markdown-Dokumente
|
||||
- Erstellung einer Indexdatei für den einfachen Zugriff
|
||||
|
||||
## Installation
|
||||
|
||||
### Voraussetzungen
|
||||
|
||||
- Python 3.11 oder höher
|
||||
- Internetverbindung zum Forgejo-Server
|
||||
- Berechtigungen zum Lesen der Forgejo-Issues
|
||||
|
||||
### Einrichtung
|
||||
|
||||
1. Speichere das Skript
|
||||
Am besten installierst du den Exporter über den befehl `git clone https://git.rpi-virtuell.de/joachim-happel/git_issue_importer.git` danach wechselst eu in das Verzeichnis mit `cd git_issue_importer`
|
||||
|
||||
2. Kopiere die Datei .env.template nach .env und setze die URl zu deiner forgejo Instanz:
|
||||
|
||||
```text
|
||||
FORGEJO_URL = "https://git.rpi-virtuell.de"
|
||||
FORGEJO_TOKEN = "dein-token"
|
||||
```
|
||||
den Token kannst du unter https://git.rpi-virtuell.de/user/settings/applications generieren
|
||||
|
||||
3. Installiere die erforderlichen Abhängigkeiten:
|
||||
|
||||
```bash
|
||||
pip install requests python-dotenv
|
||||
```
|
||||
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Grundlegende Verwendung
|
||||
|
||||
Führe das Skript mit den erforderlichen Parametern aus:
|
||||
|
||||
```bash
|
||||
python forgejo_exporter.py --owner "Comenius-Institut" --repo "FOERBICO" --output "./forgejo_issues"
|
||||
```
|
||||
|
||||
### Alle verfügbaren Parameter
|
||||
|
||||
```
|
||||
--owner : Repository-Besitzer (Benutzer oder Organisation) [erforderlich]
|
||||
--repo : Repository-Name oder 'all' für alle Repositories [erforderlich]
|
||||
--output : Ausgabeordner für Markdown-Dateien [Standard: ./forgejo_issues]
|
||||
--comments : Kommentare einbeziehen [optional Flag]
|
||||
--closed : Geschlossene Issues einbeziehen [optional Flag]
|
||||
```
|
||||
|
||||
### Beispiele
|
||||
|
||||
**Export eines einzelnen Repositories mit Kommentaren:**
|
||||
```bash
|
||||
python forgejo_exporter.py --owner "Comenius-Institut" --repo "FOERBICO" --output "./forgejo_issues" --comments
|
||||
```
|
||||
|
||||
**Export aller Repositories einer Organisation inkl. geschlossener Issues:**
|
||||
```bash
|
||||
python forgejo_exporter.py --owner "Comenius-Institut" --repo "all" --output "./forgejo_issues" --closed
|
||||
```
|
||||
|
||||
**Export mit allen Optionen:**
|
||||
```bash
|
||||
python forgejo_exporter.py --owner "Comenius-Institut" --repo "FOERBICO" --output "./forgejo_issues" --comments --closed
|
||||
```
|
||||
|
||||
## Automatisierung
|
||||
|
||||
Um die Aktualisierung zu automatisieren, kannst du das bereitgestellte Bash-Skript verwenden:
|
||||
|
||||
1. Speichere das Bash-Skript als `update_forgejo.sh`
|
||||
2. Mache es ausführbar: `chmod +x update_forgejo.sh`
|
||||
3. Passe die Einstellungen im Skript an (Repository-Besitzer, Repository-Name)
|
||||
4. Führe es manuell aus oder richte einen Cron-Job ein
|
||||
|
||||
### Einrichtung eines Cron-Jobs für regelmäßige Updates
|
||||
|
||||
Öffne die Crontab-Konfiguration:
|
||||
```bash
|
||||
crontab -e
|
||||
```
|
||||
|
||||
Füge eine Zeile für tägliche Aktualisierungen hinzu:
|
||||
```
|
||||
# Aktualisiere Forgejo-Issues jeden Tag um 3:00 Uhr
|
||||
0 3 * * * /pfad/zu/update_forgejo.sh
|
||||
```
|
||||
|
||||
## Ausgabestruktur
|
||||
|
||||
Nach der Ausführung des Skripts enthält der Ausgabeordner folgende Dateien:
|
||||
|
||||
- `index.md`: Eine Übersicht aller Issues, gruppiert nach Repository
|
||||
- `_metadata.json`: Metadaten für inkrementelle Updates (nicht löschen!)
|
||||
- Einzelne Markdown-Dateien für jedes Issue, mit dem Namensformat:
|
||||
`{repository_owner}_{repository_name}__issue_{number}_{title}.md`
|
||||
|
||||
### Struktur einer Issue-Datei
|
||||
|
||||
Jede generierte Markdown-Datei enthält:
|
||||
|
||||
1. **Titel und Issue-ID**
|
||||
2. **Metadaten** (Repository, Autor, Status, Daten, URL)
|
||||
3. **Labels** (falls vorhanden)
|
||||
4. **Milestone** (falls vorhanden)
|
||||
5. **Beschreibung** des Issues
|
||||
6. **Kommentare** (wenn mit `--comments` aktiviert)
|
||||
|
||||
## Integration mit Knowledge-Management-Tools
|
||||
|
||||
### Verwendung mit MSTY
|
||||
|
||||
1. Wähle den Ausgabeordner als Knowledge Stack in MSTY
|
||||
2. Klicke auf Compose (kann einige Minuten dauern)
|
||||
3. Führe das Update-Skript und Compose regelmäßig aus, um die Wissensdatenbank aktuell zu halten
|
||||
459
forgejo_exporter.py
Normal file
459
forgejo_exporter.py
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import datetime
|
||||
import requests
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Any, Optional
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
# Konfiguration
|
||||
FORGEJO_URL = os.getenv("FORGEJO_URL", "https://forgejo.de")
|
||||
API_BASE = f"{FORGEJO_URL}/api/v1"
|
||||
|
||||
# Für die Authentifizierung (falls erforderlich)
|
||||
TOKEN = os.getenv("FORGEJO_TOKEN")
|
||||
|
||||
class ForgejoMarkdownExporter:
|
||||
def __init__(
|
||||
self,
|
||||
repo_owner: str,
|
||||
repo_name: str,
|
||||
output_dir: str,
|
||||
include_comments: bool = True,
|
||||
include_closed: bool = True,
|
||||
fetch_all_repos: bool = False
|
||||
):
|
||||
self.repo_owner = repo_owner
|
||||
self.repo_name = repo_name
|
||||
self.output_dir = output_dir
|
||||
self.include_comments = include_comments
|
||||
self.include_closed = include_closed
|
||||
self.fetch_all_repos = fetch_all_repos
|
||||
self.metadata_file = os.path.join(output_dir, "_metadata.json")
|
||||
|
||||
# Erstelle den Ausgabeordner, falls er nicht existiert
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
def get_headers(self) -> Dict[str, str]:
|
||||
"""Gibt die HTTP-Header für die API-Anfragen zurück"""
|
||||
headers = {
|
||||
"User-Agent": "ForgejoMarkdownExporter/1.0",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
|
||||
# Füge Token hinzu, falls vorhanden
|
||||
if "TOKEN" in globals() and TOKEN:
|
||||
headers["Authorization"] = f"token {TOKEN}"
|
||||
|
||||
return headers
|
||||
|
||||
def fetch_repos(self) -> List[Dict[str, Any]]:
|
||||
"""Ruft alle Repositories des angegebenen Besitzers ab"""
|
||||
if not self.fetch_all_repos:
|
||||
# Wenn wir nur ein spezifisches Repo wollen
|
||||
return [{
|
||||
"owner": {"username": self.repo_owner},
|
||||
"name": self.repo_name,
|
||||
"full_name": f"{self.repo_owner}/{self.repo_name}"
|
||||
}]
|
||||
|
||||
repos_url = f"{API_BASE}/orgs/{self.repo_owner}/repos"
|
||||
try:
|
||||
response = requests.get(repos_url, headers=self.get_headers())
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Abrufen der Repositories: {e}")
|
||||
# Fallback auf ein einzelnes Repo
|
||||
return [{
|
||||
"owner": {"username": self.repo_owner},
|
||||
"name": self.repo_name,
|
||||
"full_name": f"{self.repo_owner}/{self.repo_name}"
|
||||
}]
|
||||
|
||||
def fetch_issues(self, repo_full_name: str) -> List[Dict[str, Any]]:
|
||||
"""Ruft alle Issues eines Repositories ab"""
|
||||
issues = []
|
||||
page = 1
|
||||
per_page = 50
|
||||
|
||||
while True:
|
||||
issues_url = f"{API_BASE}/repos/{repo_full_name}/issues?page={page}&per_page={per_page}&state={'all' if self.include_closed else 'open'}"
|
||||
try:
|
||||
response = requests.get(issues_url, headers=self.get_headers())
|
||||
response.raise_for_status()
|
||||
page_issues = response.json()
|
||||
|
||||
if not page_issues:
|
||||
break
|
||||
|
||||
issues.extend(page_issues)
|
||||
page += 1
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Abrufen der Issues für {repo_full_name}: {e}")
|
||||
break
|
||||
|
||||
return issues
|
||||
|
||||
def fetch_comments(self, repo_full_name: str, issue_number: int) -> List[Dict[str, Any]]:
|
||||
"""Ruft alle Kommentare zu einem Issue ab"""
|
||||
if not self.include_comments:
|
||||
return []
|
||||
|
||||
comments_url = f"{API_BASE}/repos/{repo_full_name}/issues/{issue_number}/comments"
|
||||
try:
|
||||
response = requests.get(comments_url, headers=self.get_headers())
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Abrufen der Kommentare für Issue #{issue_number}: {e}")
|
||||
return []
|
||||
|
||||
def generate_markdown(self, issue: Dict[str, Any], comments: List[Dict[str, Any]], repo_full_name: str) -> str:
|
||||
"""Erstellt Markdown-Inhalt für ein Issue"""
|
||||
# Basisinformationen
|
||||
issue_number = issue.get('number', 'unknown')
|
||||
title = issue.get('title', 'No Title')
|
||||
state = issue.get('state', 'unknown')
|
||||
created_at = issue.get('created_at', 'unknown')
|
||||
updated_at = issue.get('updated_at', 'unknown')
|
||||
html_url = issue.get('html_url', '')
|
||||
|
||||
# Benutzerinformationen
|
||||
author = "Unbekannt"
|
||||
if issue.get('user') and issue['user'].get('username'):
|
||||
author = issue['user']['username']
|
||||
|
||||
# Erstelle Markdown-Header
|
||||
markdown = f"# [{repo_full_name}] Issue #{issue_number}: {title}\n\n"
|
||||
|
||||
# Metadaten-Bereich
|
||||
markdown += "## Metadaten\n\n"
|
||||
markdown += f"- **Issue ID:** {issue_number}\n"
|
||||
markdown += f"- **Repository:** {repo_full_name}\n"
|
||||
markdown += f"- **Autor:** {author}\n"
|
||||
markdown += f"- **Status:** {state}\n"
|
||||
markdown += f"- **Erstellt am:** {created_at}\n"
|
||||
markdown += f"- **Aktualisiert am:** {updated_at}\n"
|
||||
markdown += f"- **URL:** {html_url}\n\n"
|
||||
|
||||
# Labels
|
||||
if issue.get('labels') and len(issue['labels']) > 0:
|
||||
markdown += "## Labels\n\n"
|
||||
for label in issue['labels']:
|
||||
markdown += f"- {label.get('name', 'Unbekanntes Label')}\n"
|
||||
markdown += "\n"
|
||||
|
||||
# Milestone
|
||||
if issue.get('milestone'):
|
||||
milestone = issue['milestone']
|
||||
markdown += "## Milestone\n\n"
|
||||
markdown += f"- **Titel:** {milestone.get('title', 'Kein Titel')}\n"
|
||||
markdown += f"- **Status:** {milestone.get('state', 'unbekannt')}\n"
|
||||
|
||||
if milestone.get('due_on'):
|
||||
markdown += f"- **Fälligkeitsdatum:** {milestone.get('due_on')}\n"
|
||||
|
||||
if milestone.get('description'):
|
||||
markdown += f"\n**Beschreibung:**\n\n{milestone.get('description')}\n\n"
|
||||
else:
|
||||
markdown += "\n"
|
||||
|
||||
# Inhalt des Issues
|
||||
markdown += "## Beschreibung\n\n"
|
||||
if issue.get('body'):
|
||||
markdown += f"{issue['body']}\n\n"
|
||||
else:
|
||||
markdown += "_Keine Beschreibung vorhanden._\n\n"
|
||||
|
||||
# Kommentare
|
||||
if comments and len(comments) > 0:
|
||||
markdown += "## Kommentare\n\n"
|
||||
for i, comment in enumerate(comments, 1):
|
||||
user = "Unbekannt"
|
||||
if comment.get('user') and comment['user'].get('username'):
|
||||
user = comment['user']['username']
|
||||
|
||||
created_at = comment.get('created_at', 'Unbekanntes Datum')
|
||||
body = comment.get('body', 'Kein Inhalt')
|
||||
|
||||
markdown += f"### Kommentar {i} von {user} am {created_at}\n\n"
|
||||
markdown += f"{body}\n\n"
|
||||
markdown += "---\n\n"
|
||||
|
||||
return markdown
|
||||
|
||||
def save_issue_to_file(self, issue: Dict[str, Any], repo_full_name: str) -> str:
|
||||
"""Speichert ein Issue als Markdown-Datei und gibt den Dateipfad zurück"""
|
||||
issue_number = issue.get('number', 'unknown')
|
||||
title = issue.get('title', 'No Title').replace('/', '-').replace('\\', '-')
|
||||
|
||||
# Erstelle einen sicheren Dateinamen
|
||||
safe_title = ''.join(c if c.isalnum() or c in [' ', '-', '_'] else '_' for c in title)
|
||||
filename = f"{repo_full_name.replace('/', '_')}__issue_{issue_number}_{safe_title[:50]}.md"
|
||||
filepath = os.path.join(self.output_dir, filename)
|
||||
|
||||
# Hole Kommentare
|
||||
comments = self.fetch_comments(repo_full_name, issue_number)
|
||||
|
||||
# Erstelle Markdown
|
||||
markdown_content = self.generate_markdown(issue, comments, repo_full_name)
|
||||
|
||||
# Speichere in Datei
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(markdown_content)
|
||||
|
||||
print(f"Issue #{issue_number} gespeichert: {filepath}")
|
||||
return filepath
|
||||
|
||||
def calculate_issue_hash(self, issue: Dict[str, Any], comments: List[Dict[str, Any]]) -> str:
|
||||
"""Berechnet einen Hash für ein Issue und seine Kommentare zur Erkennung von Änderungen"""
|
||||
# Kombiniere relevante Daten für den Hash
|
||||
hash_data = {
|
||||
"issue_id": issue.get('id'),
|
||||
"title": issue.get('title'),
|
||||
"body": issue.get('body'),
|
||||
"state": issue.get('state'),
|
||||
"updated_at": issue.get('updated_at'),
|
||||
"comments": [
|
||||
{
|
||||
"id": comment.get('id'),
|
||||
"body": comment.get('body'),
|
||||
"updated_at": comment.get('updated_at')
|
||||
}
|
||||
for comment in comments
|
||||
]
|
||||
}
|
||||
|
||||
# Erstelle einen JSON-String und hashe ihn
|
||||
json_str = json.dumps(hash_data, sort_keys=True)
|
||||
return hashlib.md5(json_str.encode('utf-8')).hexdigest()
|
||||
|
||||
def load_metadata(self) -> Dict[str, Any]:
|
||||
"""Lädt Metadaten aus einer Datei, falls vorhanden"""
|
||||
if not os.path.exists(self.metadata_file):
|
||||
return {"issues": {}, "last_update": None}
|
||||
|
||||
try:
|
||||
with open(self.metadata_file, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Laden der Metadaten: {e}")
|
||||
return {"issues": {}, "last_update": None}
|
||||
|
||||
def save_metadata(self, metadata: Dict[str, Any]):
|
||||
"""Speichert Metadaten in einer Datei"""
|
||||
try:
|
||||
with open(self.metadata_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Speichern der Metadaten: {e}")
|
||||
|
||||
def export(self) -> int:
|
||||
"""Exportiert alle Issues und gibt die Anzahl der aktualisierten Issues zurück"""
|
||||
start_time = datetime.datetime.now()
|
||||
|
||||
# Lade vorherige Metadaten
|
||||
metadata = self.load_metadata()
|
||||
issue_hashes = metadata.get("issues", {})
|
||||
|
||||
# Hole Repositories
|
||||
repos = self.fetch_repos()
|
||||
|
||||
# Zähler für Statistiken
|
||||
total_issues = 0
|
||||
new_issues = 0
|
||||
updated_issues = 0
|
||||
unchanged_issues = 0
|
||||
|
||||
# Liste der aktuellen Issue-IDs für Bereinigung
|
||||
current_issue_ids = []
|
||||
|
||||
# Verarbeite jedes Repository
|
||||
for repo in repos:
|
||||
repo_full_name = repo.get('full_name')
|
||||
if not repo_full_name:
|
||||
continue
|
||||
|
||||
print(f"\nVerarbeite Repository: {repo_full_name}")
|
||||
|
||||
# Hole Issues
|
||||
issues = self.fetch_issues(repo_full_name)
|
||||
print(f" {len(issues)} Issues gefunden")
|
||||
|
||||
# Verarbeite jedes Issue
|
||||
for issue in issues:
|
||||
issue_id = str(issue.get('id', ''))
|
||||
if not issue_id:
|
||||
continue
|
||||
|
||||
total_issues += 1
|
||||
current_issue_ids.append(issue_id)
|
||||
|
||||
# Hole Kommentare
|
||||
comments = self.fetch_comments(repo_full_name, issue.get('number', 0))
|
||||
|
||||
# Berechne Hash zur Erkennung von Änderungen
|
||||
current_hash = self.calculate_issue_hash(issue, comments)
|
||||
|
||||
# Prüfe, ob sich das Issue geändert hat
|
||||
if issue_id in issue_hashes and issue_hashes[issue_id]["hash"] == current_hash:
|
||||
unchanged_issues += 1
|
||||
print(f" Issue #{issue.get('number')} unverändert - überspringe")
|
||||
continue
|
||||
|
||||
# Issue ist neu oder hat sich geändert
|
||||
filepath = self.save_issue_to_file(issue, repo_full_name)
|
||||
|
||||
# Aktualisiere die Metadaten
|
||||
if issue_id in issue_hashes:
|
||||
updated_issues += 1
|
||||
else:
|
||||
new_issues += 1
|
||||
|
||||
issue_hashes[issue_id] = {
|
||||
"hash": current_hash,
|
||||
"number": issue.get('number'),
|
||||
"repo": repo_full_name,
|
||||
"title": issue.get('title'),
|
||||
"file": os.path.basename(filepath),
|
||||
"updated_at": issue.get('updated_at'),
|
||||
"state": issue.get('state')
|
||||
}
|
||||
|
||||
# Entferne gelöschte Issues aus der Metadatendatei
|
||||
deleted_issues = []
|
||||
for issue_id in list(issue_hashes.keys()):
|
||||
if issue_id not in current_issue_ids:
|
||||
# Lösche die zugehörige Datei
|
||||
file_path = os.path.join(self.output_dir, issue_hashes[issue_id].get("file", ""))
|
||||
if os.path.exists(file_path):
|
||||
try:
|
||||
os.remove(file_path)
|
||||
print(f"Gelöschtes Issue entfernt: {file_path}")
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Löschen der Datei {file_path}: {e}")
|
||||
|
||||
deleted_issues.append(issue_id)
|
||||
del issue_hashes[issue_id]
|
||||
|
||||
# Aktualisiere Metadaten
|
||||
metadata["issues"] = issue_hashes
|
||||
metadata["last_update"] = datetime.datetime.now().isoformat()
|
||||
self.save_metadata(metadata)
|
||||
|
||||
# Statistiken ausgeben
|
||||
end_time = datetime.datetime.now()
|
||||
duration = (end_time - start_time).total_seconds()
|
||||
|
||||
print("\n" + "="*50)
|
||||
print(f"Export abgeschlossen in {duration:.2f} Sekunden")
|
||||
print(f"Verarbeitete Issues: {total_issues}")
|
||||
print(f"Neue Issues: {new_issues}")
|
||||
print(f"Aktualisierte Issues: {updated_issues}")
|
||||
print(f"Unveränderte Issues: {unchanged_issues}")
|
||||
print(f"Gelöschte Issues: {len(deleted_issues)}")
|
||||
print("="*50)
|
||||
|
||||
# Erstelle eine index.md mit Zusammenfassung
|
||||
self.create_index_file(total_issues, new_issues, updated_issues, deleted_issues)
|
||||
|
||||
return new_issues + updated_issues
|
||||
|
||||
def create_index_file(self, total: int, new: int, updated: int, deleted: List[str]):
|
||||
"""Erstellt eine Index-Datei mit einer Übersicht aller Issues"""
|
||||
index_path = os.path.join(self.output_dir, "index.md")
|
||||
|
||||
# Lade Metadaten
|
||||
metadata = self.load_metadata()
|
||||
issues = metadata.get("issues", {})
|
||||
last_update = metadata.get("last_update", "Unbekannt")
|
||||
|
||||
try:
|
||||
with open(index_path, 'w', encoding='utf-8') as f:
|
||||
f.write(f"# Forgejo Issues Übersicht\n\n")
|
||||
f.write(f"Letzte Aktualisierung: {last_update}\n\n")
|
||||
|
||||
# Statistiken
|
||||
f.write("## Statistiken\n\n")
|
||||
f.write(f"- **Gesamtzahl der Issues:** {total}\n")
|
||||
f.write(f"- **Neue Issues bei letzter Aktualisierung:** {new}\n")
|
||||
f.write(f"- **Aktualisierte Issues bei letzter Aktualisierung:** {updated}\n")
|
||||
f.write(f"- **Gelöschte Issues bei letzter Aktualisierung:** {len(deleted)}\n\n")
|
||||
|
||||
# Gruppiere nach Repository
|
||||
repos = {}
|
||||
for issue_id, issue_data in issues.items():
|
||||
repo = issue_data.get("repo", "Unbekannt")
|
||||
if repo not in repos:
|
||||
repos[repo] = []
|
||||
repos[repo].append(issue_data)
|
||||
|
||||
# Sortiere Issues nach Nummer innerhalb jedes Repos
|
||||
for repo in repos:
|
||||
repos[repo].sort(key=lambda x: x.get("number", 0))
|
||||
|
||||
# Liste alle Issues nach Repository gruppiert auf
|
||||
f.write("## Issues nach Repository\n\n")
|
||||
|
||||
for repo, repo_issues in sorted(repos.items()):
|
||||
f.write(f"### {repo}\n\n")
|
||||
|
||||
# Offene Issues
|
||||
open_issues = [i for i in repo_issues if i.get("state") == "open"]
|
||||
if open_issues:
|
||||
f.write("#### Offene Issues\n\n")
|
||||
for issue in open_issues:
|
||||
number = issue.get("number", "?")
|
||||
title = issue.get("title", "Kein Titel")
|
||||
file = issue.get("file", "")
|
||||
f.write(f"- [#{number}: {title}]({file})\n")
|
||||
f.write("\n")
|
||||
|
||||
# Geschlossene Issues
|
||||
closed_issues = [i for i in repo_issues if i.get("state") == "closed"]
|
||||
if closed_issues:
|
||||
f.write("#### Geschlossene Issues\n\n")
|
||||
for issue in closed_issues:
|
||||
number = issue.get("number", "?")
|
||||
title = issue.get("title", "Kein Titel")
|
||||
file = issue.get("file", "")
|
||||
f.write(f"- [#{number}: {title}]({file})\n")
|
||||
f.write("\n")
|
||||
|
||||
f.write("\n")
|
||||
|
||||
print(f"Index-Datei erstellt: {index_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Erstellen der Index-Datei: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Exportiert Forgejo-Issues in Markdown-Dateien")
|
||||
parser.add_argument("--owner", required=True, help="Repository-Besitzer (Benutzer oder Organisation)")
|
||||
parser.add_argument("--repo", required=True, help="Repository-Name oder 'all' für alle Repositories")
|
||||
parser.add_argument("--output", default="./forgejo_issues", help="Ausgabeordner für Markdown-Dateien")
|
||||
parser.add_argument("--comments", action="store_true", help="Kommentare einbeziehen")
|
||||
parser.add_argument("--closed", action="store_true", help="Geschlossene Issues einbeziehen")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
fetch_all = args.repo.lower() == "all"
|
||||
repo_name = "" if fetch_all else args.repo
|
||||
|
||||
exporter = ForgejoMarkdownExporter(
|
||||
repo_owner=args.owner,
|
||||
repo_name=repo_name,
|
||||
output_dir=args.output,
|
||||
include_comments=args.comments,
|
||||
include_closed=args.closed,
|
||||
fetch_all_repos=fetch_all
|
||||
)
|
||||
|
||||
num_updated = exporter.export()
|
||||
print(f"\nErgebnis: {num_updated} Issues wurden aktualisiert oder neu hinzugefügt.")
|
||||
1
forgejo_issues/___placeholder
Normal file
1
forgejo_issues/___placeholder
Normal file
|
|
@ -0,0 +1 @@
|
|||
.
|
||||
1157
forgejo_issues/_metadata.json
Normal file
1157
forgejo_issues/_metadata.json
Normal file
File diff suppressed because it is too large
Load diff
59
update.bat
Normal file
59
update.bat
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
@echo off
|
||||
:: Automatisches Update-Skript für Forgejo Issues (Windows)
|
||||
:: Dieses Skript kann als geplante Aufgabe eingerichtet werden, um Issues regelmäßig zu aktualisieren
|
||||
|
||||
:: Konfiguration
|
||||
set SCRIPT_DIR=%~dp0
|
||||
set PYTHON_SCRIPT=%SCRIPT_DIR%forgejo_exporter.py
|
||||
set OUTPUT_DIR=%SCRIPT_DIR%forgejo_issues
|
||||
set LOG_FILE=%SCRIPT_DIR%forgejo_update.log
|
||||
|
||||
:: Forgejo-Einstellungen
|
||||
set REPO_OWNER=Comenius-Institut
|
||||
set REPO_NAME=FOERBICO
|
||||
:: Setze REPO_NAME=all um alle Repositories des Besitzers zu exportieren
|
||||
|
||||
:: Optionen
|
||||
set INCLUDE_COMMENTS=true
|
||||
set INCLUDE_CLOSED=true
|
||||
|
||||
:: Funktion zur Protokollierung
|
||||
echo [%date% %time%] Starte Forgejo-Issues Update >> "%LOG_FILE%"
|
||||
|
||||
:: Prüfe, ob das Python-Skript existiert
|
||||
if not exist "%PYTHON_SCRIPT%" (
|
||||
echo [%date% %time%] FEHLER: Python-Skript nicht gefunden: %PYTHON_SCRIPT% >> "%LOG_FILE%"
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: Erstelle den Ausgabeordner, falls er nicht existiert
|
||||
if not exist "%OUTPUT_DIR%" mkdir "%OUTPUT_DIR%"
|
||||
|
||||
:: Baue die Befehlszeile
|
||||
cd "%SCRIPT_DIR%"
|
||||
set CMD=python "forgejo_exporter.py" --owner "%REPO_OWNER%" --repo "%REPO_NAME%" --output "%OUTPUT_DIR%"
|
||||
|
||||
if "%INCLUDE_COMMENTS%"=="true" (
|
||||
set CMD=%CMD% --comments
|
||||
)
|
||||
|
||||
if "%INCLUDE_CLOSED%"=="true" (
|
||||
set CMD=%CMD% --closed
|
||||
)
|
||||
|
||||
:: Führe das Skript aus
|
||||
echo [%date% %time%] Führe Befehl aus: %CMD% >> "%LOG_FILE%"
|
||||
%CMD% >> "%LOG_FILE%" 2>&1
|
||||
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo [%date% %time%] Update erfolgreich abgeschlossen >> "%LOG_FILE%"
|
||||
) else (
|
||||
echo [%date% %time%] FEHLER: Update fehlgeschlagen mit Exit-Code %ERRORLEVEL% >> "%LOG_FILE%"
|
||||
)
|
||||
|
||||
echo [%date% %time%] Update beendet >> "%LOG_FILE%"
|
||||
echo ================================================== >> "%LOG_FILE%"
|
||||
|
||||
:: Ausgabe auf der Konsole, wenn manuell ausgeführt
|
||||
echo Forgejo Issues Update abgeschlossen.
|
||||
echo Siehe Logdatei für Details: %LOG_FILE%
|
||||
61
update.sh
Normal file
61
update.sh
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Automatisches Update-Skript für Forgejo Issues
|
||||
# Dieses Skript kann als Cron-Job ausgeführt werden, um Issues regelmäßig zu aktualisieren
|
||||
|
||||
# Konfiguration
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
PYTHON_SCRIPT="$SCRIPT_DIR/forgejo_exporter.py"
|
||||
OUTPUT_DIR="$SCRIPT_DIR/forgejo_issues"
|
||||
LOG_FILE="$SCRIPT_DIR/forgejo_update.log"
|
||||
|
||||
# Forgejo-Einstellungen
|
||||
REPO_OWNER="Comenius-Institut" # Ändere dies zu deinem Repository-Besitzer
|
||||
REPO_NAME="FOERBICO" # Ändere dies zu deinem Repository-Namen
|
||||
# Setze REPO_NAME="all" um alle Repositories des Besitzers zu exportieren
|
||||
|
||||
# Optionen
|
||||
INCLUDE_COMMENTS=true # Auf true setzen, um Kommentare einzubeziehen
|
||||
INCLUDE_CLOSED=true # Auf true setzen, um geschlossene Issues einzubeziehen
|
||||
|
||||
# Funktion zur Protokollierung
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
# Starte die Protokollierung
|
||||
log "Starte Forgejo-Issues Update"
|
||||
|
||||
# Prüfe, ob das Python-Skript existiert
|
||||
if [ ! -f "$PYTHON_SCRIPT" ]; then
|
||||
log "FEHLER: Python-Skript nicht gefunden: $PYTHON_SCRIPT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Erstelle den Ausgabeordner, falls er nicht existiert
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Baue die Befehlszeile
|
||||
CMD="python3 $PYTHON_SCRIPT --owner $REPO_OWNER --repo $REPO_NAME --output $OUTPUT_DIR"
|
||||
|
||||
if [ "$INCLUDE_COMMENTS" = true ]; then
|
||||
CMD="$CMD --comments"
|
||||
fi
|
||||
|
||||
if [ "$INCLUDE_CLOSED" = true ]; then
|
||||
CMD="$CMD --closed"
|
||||
fi
|
||||
|
||||
# Führe das Skript aus
|
||||
log "Führe Befehl aus: $CMD"
|
||||
eval $CMD >> "$LOG_FILE" 2>&1
|
||||
RESULT=$?
|
||||
|
||||
if [ $RESULT -eq 0 ]; then
|
||||
log "Update erfolgreich abgeschlossen"
|
||||
else
|
||||
log "FEHLER: Update fehlgeschlagen mit Exit-Code $RESULT"
|
||||
fi
|
||||
|
||||
log "Update beendet"
|
||||
echo "=================================================="
|
||||
Loading…
Add table
Add a link
Reference in a new issue