Add alpha and beta versions
This commit is contained in:
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