Add alpha and beta versions
This commit is contained in:
46
beta1/gitea_analyzer_app/README.md
Normal file
46
beta1/gitea_analyzer_app/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Gitea Repository Analyzer
|
||||
|
||||
Программа с графическим интерфейсом для анализа репозиториев Gitea.
|
||||
|
||||
## Что умеет программа
|
||||
|
||||
- заставка при запуске;
|
||||
- регистрация профиля;
|
||||
- вход по логину и паролю;
|
||||
- сохранение ссылок на репозитории для каждого пользователя;
|
||||
- обновление информации по выбранной ссылке отдельной кнопкой;
|
||||
- вывод автора, соавторов, даты создания, даты изменения;
|
||||
- подсчёт строк Python-кода.
|
||||
|
||||
## Запуск через Python
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Сборка в EXE
|
||||
|
||||
На Windows открой папку проекта и запусти файл:
|
||||
|
||||
```text
|
||||
build_exe.bat
|
||||
```
|
||||
|
||||
После сборки файл появится здесь:
|
||||
|
||||
```text
|
||||
dist/GiteaAnalyzer.exe
|
||||
```
|
||||
|
||||
Его можно перенести на рабочий стол и запускать как обычную программу.
|
||||
|
||||
## Где хранятся аккаунты и ссылки
|
||||
|
||||
Данные сохраняются на компьютере пользователя в папке:
|
||||
|
||||
```text
|
||||
C:\Users\USERNAME\GiteaAnalyzer\users.json
|
||||
```
|
||||
|
||||
Пароли хранятся не обычным текстом, а в виде SHA-256 хеша.
|
||||
BIN
beta1/gitea_analyzer_app/__pycache__/api.cpython-312.pyc
Normal file
BIN
beta1/gitea_analyzer_app/__pycache__/api.cpython-312.pyc
Normal file
Binary file not shown.
BIN
beta1/gitea_analyzer_app/__pycache__/parser.cpython-312.pyc
Normal file
BIN
beta1/gitea_analyzer_app/__pycache__/parser.cpython-312.pyc
Normal file
Binary file not shown.
BIN
beta1/gitea_analyzer_app/__pycache__/storage.cpython-312.pyc
Normal file
BIN
beta1/gitea_analyzer_app/__pycache__/storage.cpython-312.pyc
Normal file
Binary file not shown.
81
beta1/gitea_analyzer_app/api.py
Normal file
81
beta1/gitea_analyzer_app/api.py
Normal file
@@ -0,0 +1,81 @@
|
||||
import base64
|
||||
import requests
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
def split_link(repo_link):
|
||||
"""Разбирает ссылку вида https://gitea.com/user/repo."""
|
||||
repo_link = repo_link.strip().rstrip("/")
|
||||
parsed = urlparse(repo_link)
|
||||
|
||||
if not parsed.scheme or not parsed.netloc:
|
||||
raise ValueError("Введите полную ссылку, например: https://gitea.com/user/repo")
|
||||
|
||||
parts = [p for p in parsed.path.split("/") if p]
|
||||
if len(parts) < 2:
|
||||
raise ValueError("Ссылка должна быть вида: https://site/user/repo")
|
||||
|
||||
base_url = f"{parsed.scheme}://{parsed.netloc}"
|
||||
owner = parts[0]
|
||||
repo = parts[1]
|
||||
return base_url, owner, repo
|
||||
|
||||
|
||||
def _get_json(url):
|
||||
response = requests.get(url, timeout=15)
|
||||
if response.status_code != 200:
|
||||
raise Exception(f"Ошибка запроса: {response.status_code}\n{url}")
|
||||
return response.json()
|
||||
|
||||
|
||||
def get_repo_data(repo_link):
|
||||
base_url, owner, repo = split_link(repo_link)
|
||||
url = f"{base_url}/api/v1/repos/{owner}/{repo}"
|
||||
return _get_json(url)
|
||||
|
||||
|
||||
def get_commits(repo_link):
|
||||
base_url, owner, repo = split_link(repo_link)
|
||||
url = f"{base_url}/api/v1/repos/{owner}/{repo}/commits"
|
||||
data = _get_json(url)
|
||||
if isinstance(data, list):
|
||||
return data
|
||||
return []
|
||||
|
||||
|
||||
def get_code_lines(repo_link):
|
||||
"""Считает строки Python-кода в ветке main/master."""
|
||||
base_url, owner, repo = split_link(repo_link)
|
||||
total_lines = 0
|
||||
|
||||
# У разных репозиториев может быть main или master.
|
||||
for branch in ("main", "master"):
|
||||
tree_url = f"{base_url}/api/v1/repos/{owner}/{repo}/git/trees/{branch}?recursive=1"
|
||||
try:
|
||||
data = _get_json(tree_url)
|
||||
break
|
||||
except Exception:
|
||||
data = None
|
||||
|
||||
if not data:
|
||||
return 0
|
||||
|
||||
for file in data.get("tree", []):
|
||||
if file.get("type") != "blob":
|
||||
continue
|
||||
|
||||
path = file.get("path", "")
|
||||
if not path.endswith(".py"):
|
||||
continue
|
||||
|
||||
file_url = f"{base_url}/api/v1/repos/{owner}/{repo}/contents/{path}"
|
||||
try:
|
||||
file_data = _get_json(file_url)
|
||||
content = file_data.get("content")
|
||||
if content:
|
||||
decoded = base64.b64decode(content).decode("utf-8", errors="ignore")
|
||||
total_lines += len(decoded.splitlines())
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return total_lines
|
||||
11
beta1/gitea_analyzer_app/build_exe.bat
Normal file
11
beta1/gitea_analyzer_app/build_exe.bat
Normal file
@@ -0,0 +1,11 @@
|
||||
@echo off
|
||||
echo Installing libraries...
|
||||
python -m pip install -r requirements.txt
|
||||
|
||||
echo Building EXE...
|
||||
python -m PyInstaller --onefile --windowed --name GiteaAnalyzer main.py
|
||||
|
||||
echo.
|
||||
echo Done! EXE file is here: dist\GiteaAnalyzer.exe
|
||||
echo You can copy it to Desktop.
|
||||
pause
|
||||
345
beta1/gitea_analyzer_app/main.py
Normal file
345
beta1/gitea_analyzer_app/main.py
Normal file
@@ -0,0 +1,345 @@
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
|
||||
from api import get_repo_data, get_commits, get_code_lines
|
||||
from parser import parse_repo_info, parse_authors
|
||||
from storage import (
|
||||
register_user,
|
||||
check_user,
|
||||
get_repositories,
|
||||
add_repository,
|
||||
delete_repository,
|
||||
)
|
||||
|
||||
|
||||
class GiteaAnalyzerApp:
|
||||
def __init__(self):
|
||||
self.root = tk.Tk()
|
||||
|
||||
self.root.title("Gitea Analyzer")
|
||||
self.root.geometry("1200x800")
|
||||
self.root.attributes("-zoomed", True)
|
||||
|
||||
self.current_user = None
|
||||
|
||||
self.bg = "#eef4ff"
|
||||
self.panel = "#ffffff"
|
||||
self.button = "#6aa6ff"
|
||||
|
||||
self.root.configure(bg=self.bg)
|
||||
|
||||
self.show_splash()
|
||||
|
||||
self.root.mainloop()
|
||||
|
||||
def clear(self):
|
||||
for widget in self.root.winfo_children():
|
||||
widget.destroy()
|
||||
|
||||
def make_button(self, parent, text, command, width=25):
|
||||
return tk.Button(
|
||||
parent,
|
||||
text=text,
|
||||
command=command,
|
||||
width=width,
|
||||
bg=self.button,
|
||||
fg="white",
|
||||
activebackground="#4d8ee8",
|
||||
font=("Arial", 11, "bold"),
|
||||
relief="flat",
|
||||
cursor="hand2",
|
||||
pady=6,
|
||||
)
|
||||
|
||||
def show_splash(self):
|
||||
self.clear()
|
||||
|
||||
frame = tk.Frame(self.root, bg=self.bg)
|
||||
frame.pack(expand=True)
|
||||
|
||||
tk.Label(
|
||||
frame,
|
||||
text="Gitea Repository Analyzer",
|
||||
font=("Arial", 28, "bold"),
|
||||
bg=self.bg,
|
||||
fg="#1d3557",
|
||||
).pack(pady=20)
|
||||
|
||||
tk.Label(
|
||||
frame,
|
||||
text="Анализ репозиториев Gitea",
|
||||
font=("Arial", 14),
|
||||
bg=self.bg,
|
||||
).pack(pady=10)
|
||||
|
||||
tk.Label(
|
||||
frame,
|
||||
text="Загрузка...",
|
||||
font=("Arial", 12),
|
||||
bg=self.bg,
|
||||
).pack(pady=30)
|
||||
|
||||
self.root.after(1500, self.show_login)
|
||||
|
||||
def show_login(self):
|
||||
self.clear()
|
||||
|
||||
frame = tk.Frame(self.root, bg=self.panel, padx=40, pady=40)
|
||||
frame.place(relx=0.5, rely=0.5, anchor="center")
|
||||
|
||||
tk.Label(
|
||||
frame,
|
||||
text="Вход",
|
||||
font=("Arial", 22, "bold"),
|
||||
bg=self.panel,
|
||||
).pack(pady=10)
|
||||
|
||||
tk.Label(frame, text="Логин", bg=self.panel).pack(anchor="w")
|
||||
|
||||
login_entry = tk.Entry(frame, width=35, font=("Arial", 12))
|
||||
login_entry.pack(pady=5)
|
||||
|
||||
tk.Label(frame, text="Пароль", bg=self.panel).pack(anchor="w")
|
||||
|
||||
password_entry = tk.Entry(
|
||||
frame,
|
||||
width=35,
|
||||
font=("Arial", 12),
|
||||
show="*",
|
||||
)
|
||||
password_entry.pack(pady=5)
|
||||
|
||||
def login():
|
||||
login_value = login_entry.get().strip()
|
||||
password_value = password_entry.get().strip()
|
||||
|
||||
if not login_value or not password_value:
|
||||
messagebox.showerror(
|
||||
"Ошибка",
|
||||
"Введите логин и пароль",
|
||||
)
|
||||
return
|
||||
|
||||
if check_user(login_value, password_value):
|
||||
self.current_user = login_value
|
||||
self.show_main_screen()
|
||||
else:
|
||||
messagebox.showerror(
|
||||
"Ошибка",
|
||||
"Неверный логин или пароль",
|
||||
)
|
||||
|
||||
self.make_button(frame, "Войти", login).pack(pady=10)
|
||||
|
||||
self.make_button(
|
||||
frame,
|
||||
"Создать профиль",
|
||||
self.show_register,
|
||||
).pack(pady=5)
|
||||
|
||||
def show_register(self):
|
||||
self.clear()
|
||||
|
||||
frame = tk.Frame(self.root, bg=self.panel, padx=40, pady=40)
|
||||
frame.place(relx=0.5, rely=0.5, anchor="center")
|
||||
|
||||
tk.Label(
|
||||
frame,
|
||||
text="Регистрация",
|
||||
font=("Arial", 22, "bold"),
|
||||
bg=self.panel,
|
||||
).pack(pady=10)
|
||||
|
||||
tk.Label(frame, text="Логин", bg=self.panel).pack(anchor="w")
|
||||
|
||||
login_entry = tk.Entry(frame, width=35, font=("Arial", 12))
|
||||
login_entry.pack(pady=5)
|
||||
|
||||
tk.Label(frame, text="Пароль", bg=self.panel).pack(anchor="w")
|
||||
|
||||
password_entry = tk.Entry(
|
||||
frame,
|
||||
width=35,
|
||||
font=("Arial", 12),
|
||||
show="*",
|
||||
)
|
||||
password_entry.pack(pady=5)
|
||||
|
||||
def register():
|
||||
login_value = login_entry.get().strip()
|
||||
password_value = password_entry.get().strip()
|
||||
|
||||
if not login_value or not password_value:
|
||||
messagebox.showerror(
|
||||
"Ошибка",
|
||||
"Введите логин и пароль",
|
||||
)
|
||||
return
|
||||
|
||||
ok, text = register_user(
|
||||
login_value,
|
||||
password_value,
|
||||
)
|
||||
|
||||
if ok:
|
||||
messagebox.showinfo("Готово", text)
|
||||
self.show_login()
|
||||
else:
|
||||
messagebox.showerror("Ошибка", text)
|
||||
|
||||
self.make_button(
|
||||
frame,
|
||||
"Зарегистрироваться",
|
||||
register,
|
||||
).pack(pady=10)
|
||||
|
||||
self.make_button(
|
||||
frame,
|
||||
"Назад",
|
||||
self.show_login,
|
||||
).pack(pady=5)
|
||||
|
||||
def show_main_screen(self):
|
||||
self.clear()
|
||||
|
||||
top = tk.Frame(self.root, bg="#1d3557", height=60)
|
||||
top.pack(fill="x")
|
||||
|
||||
tk.Label(
|
||||
top,
|
||||
text=f"Gitea Analyzer | {self.current_user}",
|
||||
bg="#1d3557",
|
||||
fg="white",
|
||||
font=("Arial", 16, "bold"),
|
||||
).pack(side="left", padx=20, pady=15)
|
||||
|
||||
content = tk.Frame(self.root, bg=self.bg)
|
||||
content.pack(fill="both", expand=True, padx=15, pady=15)
|
||||
|
||||
left = tk.Frame(content, bg=self.panel, padx=15, pady=15)
|
||||
left.pack(side="left", fill="y")
|
||||
|
||||
right = tk.Frame(content, bg=self.panel, padx=15, pady=15)
|
||||
right.pack(side="right", fill="both", expand=True, padx=(15, 0))
|
||||
|
||||
tk.Label(
|
||||
left,
|
||||
text="Репозитории",
|
||||
bg=self.panel,
|
||||
font=("Arial", 15, "bold"),
|
||||
).pack(anchor="w")
|
||||
|
||||
repo_listbox = tk.Listbox(
|
||||
left,
|
||||
width=45,
|
||||
height=22,
|
||||
font=("Arial", 10),
|
||||
)
|
||||
repo_listbox.pack(pady=10)
|
||||
|
||||
repo_entry = tk.Entry(
|
||||
left,
|
||||
width=45,
|
||||
font=("Arial", 10),
|
||||
)
|
||||
repo_entry.pack(pady=5)
|
||||
|
||||
result_text = tk.Text(
|
||||
right,
|
||||
font=("Consolas", 11),
|
||||
wrap="word",
|
||||
)
|
||||
result_text.pack(fill="both", expand=True)
|
||||
|
||||
def refresh_list():
|
||||
repo_listbox.delete(0, tk.END)
|
||||
|
||||
for repo in get_repositories(self.current_user):
|
||||
repo_listbox.insert(tk.END, repo)
|
||||
|
||||
def get_selected_repo():
|
||||
selection = repo_listbox.curselection()
|
||||
|
||||
if not selection:
|
||||
return None
|
||||
|
||||
return repo_listbox.get(selection[0])
|
||||
|
||||
def save_repo():
|
||||
link = repo_entry.get().strip()
|
||||
|
||||
if not link:
|
||||
messagebox.showerror(
|
||||
"Ошибка",
|
||||
"Введите ссылку",
|
||||
)
|
||||
return
|
||||
|
||||
add_repository(self.current_user, link)
|
||||
|
||||
repo_entry.delete(0, tk.END)
|
||||
|
||||
refresh_list()
|
||||
|
||||
def analyze_repo():
|
||||
link = get_selected_repo()
|
||||
|
||||
if not link:
|
||||
messagebox.showerror(
|
||||
"Ошибка",
|
||||
"Выберите репозиторий",
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
result_text.delete("1.0", tk.END)
|
||||
|
||||
repo_data = get_repo_data(link)
|
||||
commits = get_commits(link)
|
||||
lines = get_code_lines(link)
|
||||
|
||||
info = parse_repo_info(repo_data)
|
||||
authors = parse_authors(commits)
|
||||
|
||||
result_text.insert(tk.END, f"Ссылка: {link}\n\n")
|
||||
result_text.insert(tk.END, info)
|
||||
|
||||
result_text.insert(
|
||||
tk.END,
|
||||
"\nСоавторы:\n",
|
||||
)
|
||||
|
||||
for author in authors:
|
||||
result_text.insert(
|
||||
tk.END,
|
||||
f"- {author}\n",
|
||||
)
|
||||
|
||||
result_text.insert(
|
||||
tk.END,
|
||||
f"\nСтрок Python-кода: {lines}\n",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror(
|
||||
"Ошибка",
|
||||
str(e),
|
||||
)
|
||||
|
||||
self.make_button(
|
||||
left,
|
||||
"Сохранить ссылку",
|
||||
save_repo,
|
||||
).pack(pady=5)
|
||||
|
||||
self.make_button(
|
||||
left,
|
||||
"Обновить информацию",
|
||||
analyze_repo,
|
||||
).pack(pady=5)
|
||||
|
||||
refresh_list()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
GiteaAnalyzerApp()
|
||||
22
beta1/gitea_analyzer_app/parser.py
Normal file
22
beta1/gitea_analyzer_app/parser.py
Normal file
@@ -0,0 +1,22 @@
|
||||
def parse_repo_info(data):
|
||||
return (
|
||||
f"Название: {data.get('name', '—')}\n"
|
||||
f"Описание: {data.get('description') or '—'}\n"
|
||||
f"Автор: {data.get('owner', {}).get('login', '—')}\n"
|
||||
f"Создан: {data.get('created_at', '—')}\n"
|
||||
f"Изменён: {data.get('updated_at', '—')}\n"
|
||||
)
|
||||
|
||||
|
||||
def parse_authors(commits):
|
||||
authors = set()
|
||||
|
||||
for commit in commits:
|
||||
try:
|
||||
name = commit["commit"]["author"]["name"]
|
||||
if name:
|
||||
authors.add(name)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return sorted(authors)
|
||||
2
beta1/gitea_analyzer_app/requirements.txt
Normal file
2
beta1/gitea_analyzer_app/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
requests
|
||||
pyinstaller
|
||||
76
beta1/gitea_analyzer_app/storage.py
Normal file
76
beta1/gitea_analyzer_app/storage.py
Normal file
@@ -0,0 +1,76 @@
|
||||
import json
|
||||
import os
|
||||
import hashlib
|
||||
|
||||
APP_DIR = os.path.join(os.path.expanduser("~"), "GiteaAnalyzer")
|
||||
DB_FILE = os.path.join(APP_DIR, "users.json")
|
||||
|
||||
|
||||
def _ensure_app_dir():
|
||||
os.makedirs(APP_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def load_users():
|
||||
_ensure_app_dir()
|
||||
if not os.path.exists(DB_FILE):
|
||||
return {}
|
||||
try:
|
||||
with open(DB_FILE, "r", encoding="utf-8") as file:
|
||||
return json.load(file)
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def save_users(users):
|
||||
_ensure_app_dir()
|
||||
with open(DB_FILE, "w", encoding="utf-8") as file:
|
||||
json.dump(users, file, ensure_ascii=False, indent=4)
|
||||
|
||||
|
||||
def hash_password(password):
|
||||
return hashlib.sha256(password.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def register_user(login, password):
|
||||
users = load_users()
|
||||
if login in users:
|
||||
return False, "Такой пользователь уже существует"
|
||||
users[login] = {
|
||||
"password": hash_password(password),
|
||||
"repositories": []
|
||||
}
|
||||
save_users(users)
|
||||
return True, "Профиль создан"
|
||||
|
||||
|
||||
def check_user(login, password):
|
||||
users = load_users()
|
||||
user = users.get(login)
|
||||
if not user:
|
||||
return False
|
||||
return user.get("password") == hash_password(password)
|
||||
|
||||
|
||||
def get_repositories(login):
|
||||
users = load_users()
|
||||
return users.get(login, {}).get("repositories", [])
|
||||
|
||||
|
||||
def add_repository(login, repo_link):
|
||||
users = load_users()
|
||||
repos = users[login].setdefault("repositories", [])
|
||||
if repo_link not in repos:
|
||||
repos.append(repo_link)
|
||||
save_users(users)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def delete_repository(login, repo_link):
|
||||
users = load_users()
|
||||
repos = users[login].setdefault("repositories", [])
|
||||
if repo_link in repos:
|
||||
repos.remove(repo_link)
|
||||
save_users(users)
|
||||
return True
|
||||
return False
|
||||
Reference in New Issue
Block a user