commit 12ee7eee2db2120e851093d9d8ab1a2b9f420af9 Author: 24_OskinEA <24_OskinEA@iux.local> Date: Sun May 24 13:51:49 2026 +0300 Add alpha and beta versions diff --git a/alpha/gitea_simple_app/README.md b/alpha/gitea_simple_app/README.md new file mode 100644 index 0000000..25b524e --- /dev/null +++ b/alpha/gitea_simple_app/README.md @@ -0,0 +1,31 @@ +# Gitea Repository Analyzer + +Программа для анализа репозиториев Gitea. + +## Возможности + +- получение информации о репозитории +- вывод автора +- вывод соавторов +- даты создания и изменения +- подсчёт строк Python-кода + +## Запуск + +```bash +pip install -r requirements.txt +python main.py + +--- + +# 🚀 Как загрузить на Gitea + +--- + +# 1. Создать репозиторий + +На Gitea: +- New Repository +- например: +```text +gitea-analyzer \ No newline at end of file diff --git a/alpha/gitea_simple_app/README_START.md b/alpha/gitea_simple_app/README_START.md new file mode 100644 index 0000000..f431128 --- /dev/null +++ b/alpha/gitea_simple_app/README_START.md @@ -0,0 +1,24 @@ +# Gitea Analyzer Simple + +Простая версия без логина и пароля. + +## Linux / Ubuntu + +```bash +sudo apt install python3-tk -y +pip3 install -r requirements.txt +python3 main.py +``` + +Или: + +```bash +./run_linux.sh +``` + +## Windows + +1. Установить Python. +2. Запустить `run_windows.bat`. + +Для сборки exe запустить `build_windows_exe.bat`. diff --git a/alpha/gitea_simple_app/__pycache__/api.cpython-312.pyc b/alpha/gitea_simple_app/__pycache__/api.cpython-312.pyc new file mode 100644 index 0000000..f8c8b06 Binary files /dev/null and b/alpha/gitea_simple_app/__pycache__/api.cpython-312.pyc differ diff --git a/alpha/gitea_simple_app/__pycache__/parser.cpython-312.pyc b/alpha/gitea_simple_app/__pycache__/parser.cpython-312.pyc new file mode 100644 index 0000000..1efac41 Binary files /dev/null and b/alpha/gitea_simple_app/__pycache__/parser.cpython-312.pyc differ diff --git a/alpha/gitea_simple_app/api.py b/alpha/gitea_simple_app/api.py new file mode 100644 index 0000000..cf8aaa9 --- /dev/null +++ b/alpha/gitea_simple_app/api.py @@ -0,0 +1,63 @@ +import requests +import base64 + + +def split_link(repo_link): + parts = repo_link.replace("https://", "").split("/") + + base_url = "https://" + parts[0] + owner = parts[1] + repo = parts[2] + + return base_url, owner, repo + + +def get_repo_data(repo_link): + base_url, owner, repo = split_link(repo_link) + + url = f"{base_url}/api/v1/repos/{owner}/{repo}" + return requests.get(url).json() + + +def get_commits(repo_link): + base_url, owner, repo = split_link(repo_link) + + url = f"{base_url}/api/v1/repos/{owner}/{repo}/commits" + return requests.get(url).json() + + +def get_code_lines(repo_link): + base_url, owner, repo = split_link(repo_link) + + # получаем список файлов + url = f"{base_url}/api/v1/repos/{owner}/{repo}/git/trees/main?recursive=1" + data = requests.get(url).json() + + total_lines = 0 + + for file in data.get("tree", []): + + if file.get("type") != "blob": + continue + + path = file.get("path", "") + + if not path.endswith(".py"): + continue + + # читаем файл через API (НЕ raw!) + file_url = f"{base_url}/api/v1/repos/{owner}/{repo}/contents/{path}" + r = requests.get(file_url) + + if r.status_code != 200: + continue + + file_data = r.json() + + content = file_data.get("content") + + if content: + decoded = base64.b64decode(content).decode("utf-8", errors="ignore") + total_lines += len(decoded.splitlines()) + + return total_lines \ No newline at end of file diff --git a/alpha/gitea_simple_app/build_windows_exe.bat b/alpha/gitea_simple_app/build_windows_exe.bat new file mode 100644 index 0000000..6965b6a --- /dev/null +++ b/alpha/gitea_simple_app/build_windows_exe.bat @@ -0,0 +1,6 @@ +@echo off +cd /d "%~dp0" +python -m pip install -r requirements.txt +python -m pip install pyinstaller +python -m PyInstaller --onefile --windowed --name GiteaAnalyzer main.py +pause diff --git a/alpha/gitea_simple_app/main.py b/alpha/gitea_simple_app/main.py new file mode 100644 index 0000000..4a87d58 --- /dev/null +++ b/alpha/gitea_simple_app/main.py @@ -0,0 +1,120 @@ +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 + + +class GiteaAnalyzer: + def __init__(self): + self.root = tk.Tk() + self.root.title("Gitea Analyzer") + self.root.geometry("1000x700") + self.root.minsize(850, 600) + self.root.configure(bg="#eef4ff") + + self.build_interface() + self.root.mainloop() + + def build_interface(self): + title = tk.Label( + self.root, + text="Gitea Repository Analyzer", + font=("Arial", 22, "bold"), + bg="#eef4ff", + fg="#1d3557", + ) + title.pack(pady=(20, 5)) + + subtitle = tk.Label( + self.root, + text="Введите ссылку на репозиторий Gitea и получите информацию о нём", + font=("Arial", 12), + bg="#eef4ff", + fg="#333333", + ) + subtitle.pack(pady=(0, 15)) + + input_frame = tk.Frame(self.root, bg="#eef4ff") + input_frame.pack(fill="x", padx=30) + + self.repo_entry = tk.Entry(input_frame, font=("Arial", 12)) + self.repo_entry.pack(side="left", fill="x", expand=True, ipady=6) + self.repo_entry.insert(0, "https://iux-gitea.myddns.me/owner/repo") + self.repo_entry.bind("", self.clear_placeholder) + self.repo_entry.bind("", lambda event: self.analyze()) + + analyze_button = tk.Button( + input_frame, + text="Анализировать", + command=self.analyze, + bg="#4d8ee8", + fg="white", + font=("Arial", 11, "bold"), + relief="flat", + padx=20, + pady=7, + cursor="hand2", + ) + analyze_button.pack(side="left", padx=(10, 0)) + + self.result_text = tk.Text( + self.root, + font=("Consolas", 11), + wrap="word", + state="disabled", + bg="#ffffff", + fg="#111111", + ) + self.result_text.pack(fill="both", expand=True, padx=30, pady=20) + + def clear_placeholder(self, event): + if self.repo_entry.get() == "https://iux-gitea.myddns.me/owner/repo": + self.repo_entry.delete(0, tk.END) + + def set_text(self, text): + self.result_text.config(state="normal") + self.result_text.delete("1.0", tk.END) + self.result_text.insert(tk.END, text) + self.result_text.config(state="disabled") + + def analyze(self): + repo_link = self.repo_entry.get().strip() + + if not repo_link or repo_link == "https://iux-gitea.myddns.me/owner/repo": + messagebox.showerror("Ошибка", "Введите ссылку на репозиторий") + return + + try: + self.set_text("Загрузка данных...\n") + self.root.update() + + repo_data = get_repo_data(repo_link) + commits = get_commits(repo_link) + lines = get_code_lines(repo_link) + + info = parse_repo_info(repo_data) + authors = parse_authors(commits) + + output = [] + output.append(f"Ссылка: {repo_link}\n") + output.append(info) + output.append("\nАвторы / соавторы по коммитам:\n") + + if authors: + for author in sorted(authors): + output.append(f"- {author}\n") + else: + output.append("- нет данных\n") + + output.append(f"\nСтрок Python-кода: {lines}\n") + + self.set_text("".join(output)) + + except Exception as error: + self.set_text("") + messagebox.showerror("Ошибка", f"Не удалось получить данные:\n{error}") + + +if __name__ == "__main__": + GiteaAnalyzer() diff --git a/alpha/gitea_simple_app/parser.py b/alpha/gitea_simple_app/parser.py new file mode 100644 index 0000000..53afaee --- /dev/null +++ b/alpha/gitea_simple_app/parser.py @@ -0,0 +1,20 @@ +def parse_repo_info(data): + return ( + f"Название: {data.get('name', '—')}\n" + f"Описание: {data.get('description', '—')}\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: + authors.add(commit["commit"]["author"]["name"]) + except: + continue + + return authors \ No newline at end of file diff --git a/alpha/gitea_simple_app/requirements.txt b/alpha/gitea_simple_app/requirements.txt new file mode 100644 index 0000000..663bd1f --- /dev/null +++ b/alpha/gitea_simple_app/requirements.txt @@ -0,0 +1 @@ +requests \ No newline at end of file diff --git a/alpha/gitea_simple_app/run_linux.sh b/alpha/gitea_simple_app/run_linux.sh new file mode 100755 index 0000000..30e69fa --- /dev/null +++ b/alpha/gitea_simple_app/run_linux.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd "$(dirname "$0")" +python3 main.py diff --git a/alpha/gitea_simple_app/run_windows.bat b/alpha/gitea_simple_app/run_windows.bat new file mode 100644 index 0000000..481baa4 --- /dev/null +++ b/alpha/gitea_simple_app/run_windows.bat @@ -0,0 +1,4 @@ +@echo off +cd /d "%~dp0" +python main.py +pause diff --git a/beta1/gitea_analyzer_app/README.md b/beta1/gitea_analyzer_app/README.md new file mode 100644 index 0000000..a2bdb65 --- /dev/null +++ b/beta1/gitea_analyzer_app/README.md @@ -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 хеша. diff --git a/beta1/gitea_analyzer_app/__pycache__/api.cpython-312.pyc b/beta1/gitea_analyzer_app/__pycache__/api.cpython-312.pyc new file mode 100644 index 0000000..166febe Binary files /dev/null and b/beta1/gitea_analyzer_app/__pycache__/api.cpython-312.pyc differ diff --git a/beta1/gitea_analyzer_app/__pycache__/parser.cpython-312.pyc b/beta1/gitea_analyzer_app/__pycache__/parser.cpython-312.pyc new file mode 100644 index 0000000..f49f2cc Binary files /dev/null and b/beta1/gitea_analyzer_app/__pycache__/parser.cpython-312.pyc differ diff --git a/beta1/gitea_analyzer_app/__pycache__/storage.cpython-312.pyc b/beta1/gitea_analyzer_app/__pycache__/storage.cpython-312.pyc new file mode 100644 index 0000000..f07fb2a Binary files /dev/null and b/beta1/gitea_analyzer_app/__pycache__/storage.cpython-312.pyc differ diff --git a/beta1/gitea_analyzer_app/api.py b/beta1/gitea_analyzer_app/api.py new file mode 100644 index 0000000..7450567 --- /dev/null +++ b/beta1/gitea_analyzer_app/api.py @@ -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 diff --git a/beta1/gitea_analyzer_app/build_exe.bat b/beta1/gitea_analyzer_app/build_exe.bat new file mode 100644 index 0000000..934c352 --- /dev/null +++ b/beta1/gitea_analyzer_app/build_exe.bat @@ -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 diff --git a/beta1/gitea_analyzer_app/main.py b/beta1/gitea_analyzer_app/main.py new file mode 100644 index 0000000..7b96d7d --- /dev/null +++ b/beta1/gitea_analyzer_app/main.py @@ -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() diff --git a/beta1/gitea_analyzer_app/parser.py b/beta1/gitea_analyzer_app/parser.py new file mode 100644 index 0000000..f00d5d2 --- /dev/null +++ b/beta1/gitea_analyzer_app/parser.py @@ -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) diff --git a/beta1/gitea_analyzer_app/requirements.txt b/beta1/gitea_analyzer_app/requirements.txt new file mode 100644 index 0000000..fd9a31e --- /dev/null +++ b/beta1/gitea_analyzer_app/requirements.txt @@ -0,0 +1,2 @@ +requests +pyinstaller diff --git a/beta1/gitea_analyzer_app/storage.py b/beta1/gitea_analyzer_app/storage.py new file mode 100644 index 0000000..c6c3d44 --- /dev/null +++ b/beta1/gitea_analyzer_app/storage.py @@ -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 diff --git a/beta2/universal_git_analyzer_gui_final2/README.md b/beta2/universal_git_analyzer_gui_final2/README.md new file mode 100644 index 0000000..e0b9962 --- /dev/null +++ b/beta2/universal_git_analyzer_gui_final2/README.md @@ -0,0 +1,38 @@ +# Universal Git Analyzer + +GUI-программа для анализа публичных Git-репозиториев. + +## Что умеет + +- регистрация и вход в локальный профиль; +- сохранение ссылок на репозитории; +- повторный анализ сохранённых ссылок; +- анализ GitHub, Gitea, GitLab, Forgejo и других публичных git-репозиториев; +- вывод автора по первому коммиту, соавторов, дат, количества коммитов и строк. + +## Установка Ubuntu + +```bash +sudo apt update +sudo apt install python3 python3-tk git -y +``` + +## Запуск + +```bash +python3 main.py +``` + +или: + +```bash +./run.sh +``` + +## Примеры ссылок + +```text +https://iux-gitea.myddns.me/24_OskinEA/YP_2026_litvintseva +https://github.com/Flowseal/zapret-discord-youtube +https://git.24unix.net/tracer/gitea-deb.git +``` diff --git a/beta2/universal_git_analyzer_gui_final2/analyzer.py b/beta2/universal_git_analyzer_gui_final2/analyzer.py new file mode 100644 index 0000000..714b57f --- /dev/null +++ b/beta2/universal_git_analyzer_gui_final2/analyzer.py @@ -0,0 +1,99 @@ +import os +import shutil +import subprocess +import tempfile +from datetime import datetime + +CODE_EXTENSIONS = { + '.py', '.js', '.ts', '.java', '.c', '.cpp', '.h', '.hpp', '.cs', '.php', '.rb', '.go', + '.rs', '.kt', '.swift', '.html', '.css', '.scss', '.json', '.xml', '.yaml', '.yml', + '.sh', '.bat', '.sql', '.md' +} + +SKIP_DIRS = {'.git', 'node_modules', '__pycache__', 'venv', '.venv', 'dist', 'build'} + + +def run_cmd(cmd, cwd=None): + result = subprocess.run( + cmd, + cwd=cwd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + timeout=60, + ) + if result.returncode != 0: + raise Exception(result.stderr.strip() or result.stdout.strip()) + return result.stdout.strip() + + +def repo_name_from_url(url): + name = url.rstrip('/').split('/')[-1] + if name.endswith('.git'): + name = name[:-4] + return name or 'repository' + + +def count_code_lines(repo_path): + total = 0 + files_count = 0 + for root, dirs, files in os.walk(repo_path): + dirs[:] = [d for d in dirs if d not in SKIP_DIRS] + for filename in files: + ext = os.path.splitext(filename)[1].lower() + if ext not in CODE_EXTENSIONS: + continue + path = os.path.join(root, filename) + try: + with open(path, 'r', encoding='utf-8', errors='ignore') as f: + total += len(f.readlines()) + files_count += 1 + except Exception: + pass + return total, files_count + + +def analyze_repository(repo_url): + temp_dir = tempfile.mkdtemp(prefix='git_analyzer_') + repo_dir = os.path.join(temp_dir, repo_name_from_url(repo_url)) + try: + run_cmd(['git', 'clone', '--depth', '100', repo_url, repo_dir]) + + repo_name = repo_name_from_url(repo_url) + authors_raw = run_cmd(['git', 'log', '--format=%an'], cwd=repo_dir) + authors = [] + for name in authors_raw.splitlines(): + name = name.strip() + if name and name not in authors: + authors.append(name) + + created = run_cmd(['git', 'log', '--reverse', '--format=%ci', '-1'], cwd=repo_dir) + updated = run_cmd(['git', 'log', '-1', '--format=%ci'], cwd=repo_dir) + commits_count = run_cmd(['git', 'rev-list', '--count', 'HEAD'], cwd=repo_dir) + branch = run_cmd(['git', 'branch', '--show-current'], cwd=repo_dir) + lines, files_count = count_code_lines(repo_dir) + + main_author = authors[0] if authors else 'нет данных' + coauthors = authors[1:] + + text = [] + text.append(f'Ссылка: {repo_url}') + text.append(f'Название репозитория: {repo_name}') + text.append(f'Ветка: {branch or "не определена"}') + text.append('') + text.append(f'Автор по первому коммиту: {main_author}') + text.append('Соавторы по коммитам:') + if coauthors: + for a in coauthors: + text.append(f'- {a}') + else: + text.append('- нет соавторов') + text.append('') + text.append(f'Дата создания по первому коммиту: {created or "нет данных"}') + text.append(f'Дата последнего изменения: {updated or "нет данных"}') + text.append(f'Количество коммитов: {commits_count or "0"}') + text.append(f'Файлов с кодом/текстом: {files_count}') + text.append(f'Строк кода/текста: {lines}') + return '\n'.join(text) + finally: + shutil.rmtree(temp_dir, ignore_errors=True) diff --git a/beta2/universal_git_analyzer_gui_final2/main.py b/beta2/universal_git_analyzer_gui_final2/main.py new file mode 100644 index 0000000..f75a6be --- /dev/null +++ b/beta2/universal_git_analyzer_gui_final2/main.py @@ -0,0 +1,244 @@ +import tkinter as tk +from tkinter import messagebox +import threading + +from analyzer import analyze_repository +from storage import register_user, check_user, get_repositories, add_repository, delete_repository + + +class GitAnalyzerApp: + def __init__(self): + self.root = tk.Tk() + self.root.title('Universal Git Analyzer') + self.root.geometry('1200x750') + self.root.minsize(1000, 650) + self.root.resizable(True, 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=24): + return tk.Button( + parent, + text=text, + command=command, + width=width, + bg=self.button, + fg='white', + activebackground='#4d8ee8', + font=('Arial', 12, '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='Universal Git Analyzer', + font=('Arial', 30, 'bold'), + bg=self.bg, + fg='#1d3557', + ).pack(pady=20) + + tk.Label( + frame, + text='Анализ Git-репозиториев: GitHub, Gitea, GitLab и другие публичные git-ссылки', + font=('Arial', 14), + bg=self.bg, + fg='#333333', + ).pack(pady=10) + + tk.Label(frame, text='Загрузка...', font=('Arial', 12), bg=self.bg, fg='#555555').pack(pady=30) + self.root.after(1200, self.show_login) + + def show_login(self): + self.clear() + frame = tk.Frame(self.root, bg=self.panel, padx=35, pady=35) + 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, font=('Arial', 11)).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, font=('Arial', 11)).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=12) + 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=35, pady=35) + 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, font=('Arial', 11)).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, font=('Arial', 11)).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 + if len(password_value) < 4: + messagebox.showerror('Ошибка', 'Пароль должен быть минимум 4 символа') + 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=12) + 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'Universal Git Analyzer | Пользователь: {self.current_user}', + bg='#1d3557', + fg='white', + font=('Arial', 18, 'bold'), + ).pack(side='left', padx=20, pady=15) + tk.Button(top, text='Выйти', command=self.show_login, bg='#e63946', fg='white', relief='flat', font=('Arial', 12)).pack(side='right', padx=20) + + 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', 22, 'bold')).pack(anchor='w') + repo_listbox = tk.Listbox(left, width=48, height=20, font=('Arial', 12)) + repo_listbox.pack(pady=10) + + tk.Label(left, text='Новая ссылка', bg=self.panel, font=('Arial', 13)).pack(anchor='w') + repo_entry = tk.Entry(left, width=48, font=('Arial', 12)) + repo_entry.pack(pady=5) + + result_text = tk.Text(right, width=65, height=28, font=('Consolas', 13), 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 + if not (link.startswith('https://') or link.startswith('http://') or link.startswith('git@')): + messagebox.showerror('Ошибка', 'Введите нормальную git-ссылку') + return + added = add_repository(self.current_user, link) + if added: + repo_entry.delete(0, tk.END) + refresh_list() + messagebox.showinfo('Готово', 'Ссылка сохранена') + else: + messagebox.showinfo('Информация', 'Эта ссылка уже сохранена') + + def remove_repo(): + link = get_selected_repo() + if not link: + messagebox.showerror('Ошибка', 'Выберите репозиторий') + return + delete_repository(self.current_user, link) + refresh_list() + result_text.delete('1.0', tk.END) + + def analyze_selected(): + link = get_selected_repo() + if not link: + messagebox.showerror('Ошибка', 'Выберите репозиторий') + return + + result_text.delete('1.0', tk.END) + result_text.insert(tk.END, 'Загрузка данных...\n') + + def worker(): + try: + info = analyze_repository(link) + self.root.after(0, lambda: show_result(info)) + except Exception as e: + self.root.after(0, lambda: show_error(str(e))) + + def show_result(info): + result_text.delete('1.0', tk.END) + result_text.insert(tk.END, info) + + def show_error(error): + result_text.delete('1.0', tk.END) + messagebox.showerror('Ошибка', f'Не удалось проанализировать репозиторий:\n{error}') + + threading.Thread(target=worker, daemon=True).start() + + self.make_button(left, 'Сохранить ссылку', save_repo, width=28).pack(pady=5) + self.make_button(left, 'Обновить информацию', analyze_selected, width=28).pack(pady=5) + self.make_button(left, 'Удалить ссылку', remove_repo, width=28).pack(pady=5) + + tk.Label( + left, + text='Поддерживаются публичные git-ссылки:\nGitea, GitHub, GitLab, Forgejo и другие.', + bg=self.panel, + fg='#444444', + font=('Arial', 10), + justify='left', + ).pack(anchor='w', pady=15) + + refresh_list() + + +if __name__ == '__main__': + GitAnalyzerApp() diff --git a/beta2/universal_git_analyzer_gui_final2/requirements.txt b/beta2/universal_git_analyzer_gui_final2/requirements.txt new file mode 100644 index 0000000..32599ff --- /dev/null +++ b/beta2/universal_git_analyzer_gui_final2/requirements.txt @@ -0,0 +1 @@ +# Tkinter входит в python3-tk, внешних библиотек нет diff --git a/beta2/universal_git_analyzer_gui_final2/run.sh b/beta2/universal_git_analyzer_gui_final2/run.sh new file mode 100755 index 0000000..30e69fa --- /dev/null +++ b/beta2/universal_git_analyzer_gui_final2/run.sh @@ -0,0 +1,3 @@ +#!/bin/bash +cd "$(dirname "$0")" +python3 main.py diff --git a/beta2/universal_git_analyzer_gui_final2/storage.py b/beta2/universal_git_analyzer_gui_final2/storage.py new file mode 100644 index 0000000..2161849 --- /dev/null +++ b/beta2/universal_git_analyzer_gui_final2/storage.py @@ -0,0 +1,65 @@ +import json +import os +import hashlib + +DATA_FILE = os.path.join(os.path.dirname(__file__), 'users.json') + + +def load_data(): + if not os.path.exists(DATA_FILE): + return {} + try: + with open(DATA_FILE, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception: + return {} + + +def save_data(data): + with open(DATA_FILE, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=4) + + +def hash_password(password): + return hashlib.sha256(password.encode('utf-8')).hexdigest() + + +def register_user(login, password): + data = load_data() + if login in data: + return False, 'Такой пользователь уже существует' + data[login] = { + 'password': hash_password(password), + 'repositories': [] + } + save_data(data) + return True, 'Профиль создан' + + +def check_user(login, password): + data = load_data() + return login in data and data[login].get('password') == hash_password(password) + + +def get_repositories(login): + data = load_data() + return data.get(login, {}).get('repositories', []) + + +def add_repository(login, repo_link): + data = load_data() + if login not in data: + return False + repos = data[login].setdefault('repositories', []) + if repo_link in repos: + return False + repos.append(repo_link) + save_data(data) + return True + + +def delete_repository(login, repo_link): + data = load_data() + if login in data and repo_link in data[login].get('repositories', []): + data[login]['repositories'].remove(repo_link) + save_data(data)