Add alpha and beta versions
This commit is contained in:
31
alpha/gitea_simple_app/README.md
Normal file
31
alpha/gitea_simple_app/README.md
Normal file
@@ -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
|
||||||
24
alpha/gitea_simple_app/README_START.md
Normal file
24
alpha/gitea_simple_app/README_START.md
Normal file
@@ -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`.
|
||||||
BIN
alpha/gitea_simple_app/__pycache__/api.cpython-312.pyc
Normal file
BIN
alpha/gitea_simple_app/__pycache__/api.cpython-312.pyc
Normal file
Binary file not shown.
BIN
alpha/gitea_simple_app/__pycache__/parser.cpython-312.pyc
Normal file
BIN
alpha/gitea_simple_app/__pycache__/parser.cpython-312.pyc
Normal file
Binary file not shown.
63
alpha/gitea_simple_app/api.py
Normal file
63
alpha/gitea_simple_app/api.py
Normal file
@@ -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
|
||||||
6
alpha/gitea_simple_app/build_windows_exe.bat
Normal file
6
alpha/gitea_simple_app/build_windows_exe.bat
Normal file
@@ -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
|
||||||
120
alpha/gitea_simple_app/main.py
Normal file
120
alpha/gitea_simple_app/main.py
Normal file
@@ -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("<FocusIn>", self.clear_placeholder)
|
||||||
|
self.repo_entry.bind("<Return>", 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()
|
||||||
20
alpha/gitea_simple_app/parser.py
Normal file
20
alpha/gitea_simple_app/parser.py
Normal file
@@ -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
|
||||||
1
alpha/gitea_simple_app/requirements.txt
Normal file
1
alpha/gitea_simple_app/requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
requests
|
||||||
3
alpha/gitea_simple_app/run_linux.sh
Executable file
3
alpha/gitea_simple_app/run_linux.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
python3 main.py
|
||||||
4
alpha/gitea_simple_app/run_windows.bat
Normal file
4
alpha/gitea_simple_app/run_windows.bat
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
@echo off
|
||||||
|
cd /d "%~dp0"
|
||||||
|
python main.py
|
||||||
|
pause
|
||||||
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
|
||||||
38
beta2/universal_git_analyzer_gui_final2/README.md
Normal file
38
beta2/universal_git_analyzer_gui_final2/README.md
Normal file
@@ -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
|
||||||
|
```
|
||||||
99
beta2/universal_git_analyzer_gui_final2/analyzer.py
Normal file
99
beta2/universal_git_analyzer_gui_final2/analyzer.py
Normal file
@@ -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)
|
||||||
244
beta2/universal_git_analyzer_gui_final2/main.py
Normal file
244
beta2/universal_git_analyzer_gui_final2/main.py
Normal file
@@ -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()
|
||||||
1
beta2/universal_git_analyzer_gui_final2/requirements.txt
Normal file
1
beta2/universal_git_analyzer_gui_final2/requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Tkinter входит в python3-tk, внешних библиотек нет
|
||||||
3
beta2/universal_git_analyzer_gui_final2/run.sh
Executable file
3
beta2/universal_git_analyzer_gui_final2/run.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
python3 main.py
|
||||||
65
beta2/universal_git_analyzer_gui_final2/storage.py
Normal file
65
beta2/universal_git_analyzer_gui_final2/storage.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user