AI Update: تسک ها را آماده کن این ساختار عالیه

This commit is contained in:
Antigravity Bot
2026-08-30 18:36:58 +03:30
parent d5c225ebf6
commit bf4df87e86
7 changed files with 930 additions and 28 deletions
+14
View File
@@ -123,6 +123,13 @@ class Project:
created_at: float = field(default_factory=time.time) created_at: float = field(default_factory=time.time)
description: str = "" description: str = ""
conversation_titles: Dict[str, str] = field(default_factory=dict) conversation_titles: Dict[str, str] = field(default_factory=dict)
active_branch: str = "dev"
ftp_host: Optional[str] = None
ftp_port: int = 21
ftp_user: Optional[str] = None
ftp_password: Optional[str] = None
ftp_path: str = "/"
ftp_tls: bool = False
@dataclass @dataclass
class Session: class Session:
@@ -258,6 +265,13 @@ class SessionManager:
"created_at", "created_at",
"description", "description",
"conversation_titles", "conversation_titles",
"active_branch",
"ftp_host",
"ftp_port",
"ftp_user",
"ftp_password",
"ftp_path",
"ftp_tls",
) )
} }
if "name" not in clean_p: if "name" not in clean_p:
+204
View File
@@ -49,6 +49,7 @@ from formatters import (
format_git_info, format_git_info,
format_git_commits_page, format_git_commits_page,
format_commit_detail_view, format_commit_detail_view,
format_ftp_info,
split_message, split_message,
escape_html, escape_html,
) )
@@ -67,6 +68,7 @@ from invite_manager import (
) )
from backup_manager import backup_manager from backup_manager import backup_manager
from git_manager import git_manager from git_manager import git_manager
from ftp_manager import ftp_manager
from sys_monitor import render_server_hardware_report from sys_monitor import render_server_hardware_report
from usage_monitor import fetch_and_render_usage_report from usage_monitor import fetch_and_render_usage_report
from bot_actions import process_all_ai_actions from bot_actions import process_all_ai_actions
@@ -319,6 +321,10 @@ async def build_git_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
[ [
InlineKeyboardButton("🌐 مشاهده در مرورگر (Gitea)", url=urls["web_url"]), InlineKeyboardButton("🌐 مشاهده در مرورگر (Gitea)", url=urls["web_url"]),
], ],
[
InlineKeyboardButton("🚀 انتشار به پروداکشن (Publish)", callback_data="btn_git_publish"),
InlineKeyboardButton("🚀 دیپلوی FTP", callback_data="btn_ftp_menu"),
],
[ [
InlineKeyboardButton("📜 تاریخچه ۲۰ کامیت اخیر", callback_data="git_hist:1"), InlineKeyboardButton("📜 تاریخچه ۲۰ کامیت اخیر", callback_data="git_hist:1"),
InlineKeyboardButton("↩️ لغو آخرین تغییر (Undo)", callback_data="git_ask_undo_head"), InlineKeyboardButton("↩️ لغو آخرین تغییر (Undo)", callback_data="git_ask_undo_head"),
@@ -341,6 +347,10 @@ async def build_git_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
[ [
InlineKeyboardButton("🌐 Open in Browser (Gitea)", url=urls["web_url"]), InlineKeyboardButton("🌐 Open in Browser (Gitea)", url=urls["web_url"]),
], ],
[
InlineKeyboardButton("🚀 Publish to Production", callback_data="btn_git_publish"),
InlineKeyboardButton("🚀 Deploy via FTP", callback_data="btn_ftp_menu"),
],
[ [
InlineKeyboardButton("📜 Recent Commits (History)", callback_data="git_hist:1"), InlineKeyboardButton("📜 Recent Commits (History)", callback_data="git_hist:1"),
InlineKeyboardButton("↩️ Undo Last Change", callback_data="git_ask_undo_head"), InlineKeyboardButton("↩️ Undo Last Change", callback_data="git_ask_undo_head"),
@@ -362,6 +372,59 @@ async def build_git_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
return text, InlineKeyboardMarkup(keyboard) return text, InlineKeyboardMarkup(keyboard)
async def build_ftp_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
"""Generates the interactive FTP Deployer and configuration view."""
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found."
return msg, InlineKeyboardMarkup([[InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard")]])
text = format_ftp_info(
project_name=curr_proj.name,
host=curr_proj.ftp_host,
port=curr_proj.ftp_port,
user=curr_proj.ftp_user,
path=curr_proj.ftp_path,
tls=curr_proj.ftp_tls,
lang=session.language,
)
if is_fa:
keyboard = [
[
InlineKeyboardButton("🚀 دیپلوی روی FTP (Clean Deploy)", callback_data="btn_ftp_deploy"),
InlineKeyboardButton("🔍 تست اتصال FTP", callback_data="btn_ftp_test"),
],
[
InlineKeyboardButton("🐙 منوی گیت و برنچ‌ها", callback_data="btn_git_menu"),
InlineKeyboardButton("📁 مدیریت پروژه‌ها", callback_data="proj_menu"),
],
[
InlineKeyboardButton("🏠 منوی اصلی", callback_data="btn_dashboard"),
],
]
else:
keyboard = [
[
InlineKeyboardButton("🚀 Deploy via FTP (Clean)", callback_data="btn_ftp_deploy"),
InlineKeyboardButton("🔍 Test FTP Connection", callback_data="btn_ftp_test"),
],
[
InlineKeyboardButton("🐙 Git & Branches Menu", callback_data="btn_git_menu"),
InlineKeyboardButton("📁 Projects Menu", callback_data="proj_menu"),
],
[
InlineKeyboardButton("🏠 Main Dashboard", callback_data="btn_dashboard"),
],
]
return text, InlineKeyboardMarkup(keyboard)
async def build_git_history_menu(chat_id: int, page: int = 1) -> tuple[str, InlineKeyboardMarkup]: async def build_git_history_menu(chat_id: int, page: int = 1) -> tuple[str, InlineKeyboardMarkup]:
"""Generates paginated commit history view.""" """Generates paginated commit history view."""
session = session_manager.get_or_create(chat_id) session = session_manager.get_or_create(chat_id)
@@ -3360,6 +3423,55 @@ async def git_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
text, markup = await build_git_menu(chat_id) text, markup = await build_git_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True) await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
# Command: /publish
@check_auth
async def publish_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
session = session_manager.get_or_create(chat_id)
curr_proj = session_manager.get_current_project(chat_id)
is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
if not curr_proj:
msg = "⚠️ شما هنوز هیچ پروژه‌ای ایجاد نکرده‌اید." if is_fa else "⚠️ No active project found."
await update.message.reply_html(msg)
return
pub_msg = " ".join(context.args).strip() if context.args else f"Manual release published via /publish"
status_msg = await update.message.reply_html("🚀 <i>در حال انتشار تغییرات شاخه dev به شاخه production...</i>" if is_fa else "🚀 <i>Publishing dev branch to production...</i>")
ok, res_msg, p_info = await git_manager.git_publish(curr_proj.workspace, message=pub_msg, repo_name=curr_proj.name)
urls = git_manager.get_repo_urls(curr_proj.name)
buttons = [
[
InlineKeyboardButton("🚀 دیپلوی روی FTP" if is_fa else "🚀 Deploy to FTP", callback_data="btn_ftp_deploy"),
InlineKeyboardButton("🐙 منوی گیت" if is_fa else "🐙 Git Menu", callback_data="btn_git_menu"),
]
]
if ok:
out = (
f"🚀 <b>عملیات انتشار به پروداکشن (Publish to Production) با موفقیت انجام شد:</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🌐 <b>مخزن:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n\n"
f"{res_msg}"
) if is_fa else (
f"🚀 <b>Published to Production Successfully:</b>\n\n"
f"• 📁 <b>Project:</b> <code>{escape_html(curr_proj.name)}</code>\n"
f"• 🌐 <b>Repository:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n\n"
f"{res_msg}"
)
else:
out = f"⚠️ <b>خطا در انتشار:</b>\n{res_msg}"
await status_msg.edit_text(out, parse_mode=constants.ParseMode.HTML, reply_markup=InlineKeyboardMarkup(buttons), disable_web_page_preview=True)
# Command: /ftp
@check_auth
async def ftp_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
chat_id = update.effective_chat.id
text, markup = await build_ftp_menu(chat_id)
await update.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
# Command: /sync # Command: /sync
@check_auth @check_auth
async def sync_command(update: Update, context: ContextTypes.DEFAULT_TYPE): async def sync_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -4574,6 +4686,96 @@ async def callback_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
except Exception: except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True) await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "btn_git_publish":
await query.answer("🚀 در حال انتشار به پروداکشن..." if is_fa else "🚀 Publishing to production...")
if curr_proj:
ok, res_msg, _ = await git_manager.git_publish(curr_proj.workspace, message="Publish via bot button", repo_name=curr_proj.name)
try:
await query.answer("✅ انتشار به پروداکشن با موفقیت انجام شد!" if ok else "⚠️ خطا در انتشار", show_alert=not ok)
except Exception:
pass
text, markup = await build_git_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "btn_ftp_menu":
text, markup = await build_ftp_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "btn_ftp_test":
await query.answer("🔍 در حال بررسی اتصال FTP..." if is_fa else "🔍 Testing FTP connection...")
if curr_proj and curr_proj.ftp_host:
test_ok, test_msg = await ftp_manager.test_connection(
host=curr_proj.ftp_host,
port=curr_proj.ftp_port,
user=curr_proj.ftp_user or "",
password=curr_proj.ftp_password or "",
path=curr_proj.ftp_path or "/",
tls=curr_proj.ftp_tls,
)
try:
alert_text = ("✅ اتصال FTP برقرار است" if test_ok else "❌ خطا در اتصال FTP") + f"\n{test_msg}"
await query.answer(alert_text[:200], show_alert=True)
except Exception:
pass
else:
try:
await query.answer("⚠️ اطلاعات FTP هنوز برای این پروژه تنظیم نشده است.", show_alert=True)
except Exception:
pass
text, markup = await build_ftp_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "btn_ftp_deploy":
await query.answer("🚀 در حال استقرار هوشمند روی FTP..." if is_fa else "🚀 Deploying to FTP...")
if curr_proj and curr_proj.ftp_host:
# Auto publish before deploy
try:
await git_manager.git_publish(curr_proj.workspace, message="Auto-publish before UI FTP deploy", repo_name=curr_proj.name)
except Exception:
pass
res = await ftp_manager.deploy_project(
workspace_path=curr_proj.workspace,
host=curr_proj.ftp_host,
port=curr_proj.ftp_port,
user=curr_proj.ftp_user or "",
password=curr_proj.ftp_password or "",
remote_path=curr_proj.ftp_path or "/",
tls=curr_proj.ftp_tls,
)
if res.get("success"):
files_up = res.get("files_uploaded", 0)
dur = res.get("duration", 0)
try:
await query.answer(f"✅ دیپلوی با موفقیت انجام شد ({files_up} فایل در {dur} ثانیه)", show_alert=True)
except Exception:
pass
else:
err = res.get("error", "Error")
try:
await query.answer(f"❌ خطا در دیپلوی: {err}"[:200], show_alert=True)
except Exception:
pass
else:
try:
await query.answer("⚠️ اطلاعات FTP تنظیم نشده است. ابتدا اطلاعات FTP را ثبت کنید.", show_alert=True)
except Exception:
pass
text, markup = await build_ftp_menu(chat_id)
try:
await query.edit_message_text(text, parse_mode=constants.ParseMode.HTML, reply_markup=markup, disable_web_page_preview=True)
except Exception:
await query.message.reply_html(text, reply_markup=markup, disable_web_page_preview=True)
elif data == "btn_git_sync": elif data == "btn_git_sync":
await query.answer("🔄 در حال همگام‌سازی با Gitea..." if is_fa else "🔄 Syncing with Gitea...") await query.answer("🔄 در حال همگام‌سازی با Gitea..." if is_fa else "🔄 Syncing with Gitea...")
if curr_proj: if curr_proj:
@@ -7034,6 +7236,8 @@ def main():
# Git & Gitea Version Control Handlers # Git & Gitea Version Control Handlers
application.add_handler(CommandHandler(["git", "repo", "repository", "gitea"], git_command)) application.add_handler(CommandHandler(["git", "repo", "repository", "gitea"], git_command))
application.add_handler(CommandHandler(["publish", "gitpublish", "release"], publish_command))
application.add_handler(CommandHandler(["ftp", "ftpdeploy", "deploy"], ftp_command))
application.add_handler(CommandHandler(["sync", "gitsync", "pull"], sync_command)) application.add_handler(CommandHandler(["sync", "gitsync", "pull"], sync_command))
application.add_handler(CommandHandler(["commit", "gitcommit"], commit_command)) application.add_handler(CommandHandler(["commit", "gitcommit"], commit_command))
application.add_handler(CommandHandler(["undo", "revert", "gitundo", "gitrevert", "laghv"], undo_command)) application.add_handler(CommandHandler(["undo", "revert", "gitundo", "gitrevert", "laghv"], undo_command))
+207 -1
View File
@@ -1417,7 +1417,213 @@ async def execute_action(
return badge, side_effects, created_task return badge, side_effects, created_task
# ------------------------------------------------------------- # -------------------------------------------------------------
# 31. SAVE_MEMORY / STORE_MEMORY / SET_MEMORY # 31. GIT_PUBLISH / PUBLISH_PROJECT / PUBLISH
# -------------------------------------------------------------
elif act in ("GIT_PUBLISH", "PUBLISH_PROJECT", "PUBLISH", "MERGE_TO_PROD"):
p_name = attrs.get("project") or attrs.get("name") or current_project_name
proj = session.projects.get(p_name) if p_name else session_manager.get_current_project(chat_id)
if not proj:
return f"\n⚠️ پروژه <code>{escape_html(p_name or '')}</code> یافت نشد." if is_fa else f"\n⚠️ Project <code>{escape_html(p_name or '')}</code> not found.", side_effects, created_task
from git_manager import git_manager
pub_msg = attrs.get("message") or attrs.get("_default") or f"Release published to production"
ok, res_msg, p_info = await git_manager.git_publish(proj.workspace, message=pub_msg, repo_name=proj.name)
urls = git_manager.get_repo_urls(proj.name)
if ok:
badge = (
f"\n\n🚀 <b>عملیات انتشار به پروداکشن (Publish to Production) با موفقیت انجام شد:</b>\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(proj.name)}</code>\n"
f"• 🌐 <b>مخزن تحت وب:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n"
f"• 📋 {res_msg}"
if is_fa else
f"\n\n🚀 <b>Published to Production Successfully:</b>\n"
f"• 📁 <b>Project:</b> <code>{escape_html(proj.name)}</code>\n"
f"• 🌐 <b>Web Repo:</b> <a href=\"{urls['web_url']}\">{urls['web_url']}</a>\n"
f"• 📋 {res_msg}"
)
else:
badge = f"\n⚠️ خطا در انتشار به پروداکشن: {res_msg}"
return badge, side_effects, created_task
# -------------------------------------------------------------
# 32. FTP_CONFIG / SET_FTP / CONFIGURE_FTP
# -------------------------------------------------------------
elif act in ("FTP_CONFIG", "SET_FTP", "CONFIGURE_FTP", "FTP_SETTINGS"):
p_name = attrs.get("project") or attrs.get("name") or current_project_name
proj = session.projects.get(p_name) if p_name else session_manager.get_current_project(chat_id)
if not proj:
return f"\n⚠️ پروژه <code>{escape_html(p_name or '')}</code> یافت نشد." if is_fa else f"\n⚠️ Project <code>{escape_html(p_name or '')}</code> not found.", side_effects, created_task
host = attrs.get("host") or attrs.get("server") or attrs.get("ftp_host")
port_raw = attrs.get("port") or attrs.get("ftp_port") or "21"
user = attrs.get("user") or attrs.get("username") or attrs.get("ftp_user") or ""
password = attrs.get("password") or attrs.get("pass") or attrs.get("ftp_password") or ""
path = attrs.get("path") or attrs.get("dir") or attrs.get("remote_path") or attrs.get("ftp_path") or "/"
tls_raw = attrs.get("tls") or attrs.get("ssl") or attrs.get("ftps") or "false"
tls = str(tls_raw).lower() in ("true", "1", "yes", "on")
try:
port = int(port_raw)
except Exception:
port = 21
if host:
proj.ftp_host = host.strip()
proj.ftp_port = port
proj.ftp_user = user.strip()
proj.ftp_password = password.strip() if password else proj.ftp_password
proj.ftp_path = path.strip()
proj.ftp_tls = tls
session_manager.save()
# Test connection
from ftp_manager import ftp_manager
test_ok, test_msg = await ftp_manager.test_connection(
host=proj.ftp_host,
port=proj.ftp_port,
user=proj.ftp_user,
password=proj.ftp_password or "",
path=proj.ftp_path,
tls=proj.ftp_tls,
)
status_icon = "" if test_ok else "⚠️"
badge = (
f"\n\n⚙️ <b>تنظیمات اتصال FTP برای پروژه <code>{escape_html(proj.name)}</code> ذخیره شد:</b>\n"
f"• 🌐 <b>هاست:</b> <code>{escape_html(proj.ftp_host)}:{proj.ftp_port}</code>\n"
f"• 👤 <b>نام کاربری:</b> <code>{escape_html(proj.ftp_user or '(بدون نام کاربری)')}</code>\n"
f"• 📂 <b>مسیر ریموت:</b> <code>{escape_html(proj.ftp_path)}</code>\n"
f"• 🔒 <b>پروتکل امن (FTPS/TLS):</b> {'بله' if proj.ftp_tls else 'خیر'}\n"
f"{status_icon} <b>نتیجه بررسی اتصال:</b> {test_msg}"
if is_fa else
f"\n\n⚙️ <b>FTP Configuration saved for project <code>{escape_html(proj.name)}</code>:</b>\n"
f"• 🌐 <b>Host:</b> <code>{escape_html(proj.ftp_host)}:{proj.ftp_port}</code>\n"
f"• 👤 <b>User:</b> <code>{escape_html(proj.ftp_user or '(none)')}</code>\n"
f"• 📂 <b>Remote Path:</b> <code>{escape_html(proj.ftp_path)}</code>\n"
f"• 🔒 <b>FTPS/TLS:</b> {'Yes' if proj.ftp_tls else 'No'}\n"
f"{status_icon} <b>Connection test:</b> {test_msg}"
)
else:
# Display current config
curr_host = proj.ftp_host or "(تنظیم نشده)"
badge = (
f"\n\n️ <b>وضعیت تنظیمات FTP پروژه <code>{escape_html(proj.name)}</code>:</b>\n"
f"• 🌐 <b>هاست:</b> <code>{escape_html(curr_host)}:{proj.ftp_port}</code>\n"
f"• 👤 <b>کاربر:</b> <code>{escape_html(proj.ftp_user or '(ندارد)')}</code>\n"
f"• 📂 <b>مسیر:</b> <code>{escape_html(proj.ftp_path or '/')}</code>"
if is_fa else
f"\n\n️ <b>FTP Status for <code>{escape_html(proj.name)}</code>:</b>\n"
f"• 🌐 <b>Host:</b> <code>{escape_html(curr_host)}:{proj.ftp_port}</code>\n"
f"• 👤 <b>User:</b> <code>{escape_html(proj.ftp_user or '(none)')}</code>\n"
f"• 📂 <b>Path:</b> <code>{escape_html(proj.ftp_path or '/')}</code>"
)
return badge, side_effects, created_task
# -------------------------------------------------------------
# 33. FTP_TEST / TEST_FTP
# -------------------------------------------------------------
elif act in ("FTP_TEST", "TEST_FTP", "CHECK_FTP"):
p_name = attrs.get("project") or attrs.get("name") or current_project_name
proj = session.projects.get(p_name) if p_name else session_manager.get_current_project(chat_id)
if not proj:
return f"\n⚠️ پروژه <code>{escape_html(p_name or '')}</code> یافت نشد." if is_fa else f"\n⚠️ Project <code>{escape_html(p_name or '')}</code> not found.", side_effects, created_task
if not proj.ftp_host:
return "\n⚠️ مشخصات FTP برای این پروژه هنوز تنظیم نشده است. می‌توانید با تگ <code>[[FTP_CONFIG: host=\"...\", user=\"...\", pass=\"...\"]]</code> آن را تنظیم کنید." if is_fa else "\n⚠️ FTP credentials are not set for this project.", side_effects, created_task
from ftp_manager import ftp_manager
test_ok, test_msg = await ftp_manager.test_connection(
host=proj.ftp_host,
port=proj.ftp_port,
user=proj.ftp_user or "",
password=proj.ftp_password or "",
path=proj.ftp_path or "/",
tls=proj.ftp_tls,
)
icon = "" if test_ok else ""
badge = (
f"\n\n{icon} <b>تست اتصال FTP پروژه <code>{escape_html(proj.name)}</code>:</b>\n"
f"• 🌐 <b>سرور:</b> <code>{escape_html(proj.ftp_host)}:{proj.ftp_port}</code>\n"
f"• 📂 <b>مسیر:</b> <code>{escape_html(proj.ftp_path or '/')}</code>\n"
f"• 📋 <b>وضعیت:</b> {test_msg}"
if is_fa else
f"\n\n{icon} <b>FTP Test Result for <code>{escape_html(proj.name)}</code>:</b>\n"
f"• 🌐 <b>Server:</b> <code>{escape_html(proj.ftp_host)}:{proj.ftp_port}</code>\n"
f"• 📂 <b>Path:</b> <code>{escape_html(proj.ftp_path or '/')}</code>\n"
f"• 📋 <b>Status:</b> {test_msg}"
)
return badge, side_effects, created_task
# -------------------------------------------------------------
# 34. FTP_DEPLOY / DEPLOY_FTP / DEPLOY_PROJECT
# -------------------------------------------------------------
elif act in ("FTP_DEPLOY", "DEPLOY_FTP", "DEPLOY_PROJECT", "FTP_PUBLISH"):
p_name = attrs.get("project") or attrs.get("name") or current_project_name
proj = session.projects.get(p_name) if p_name else session_manager.get_current_project(chat_id)
if not proj:
return f"\n⚠️ پروژه <code>{escape_html(p_name or '')}</code> یافت نشد." if is_fa else f"\n⚠️ Project <code>{escape_html(p_name or '')}</code> not found.", side_effects, created_task
if not proj.ftp_host:
return "\n⚠️ مشخصات اتصال FTP برای این پروژه یافت نشد. لطفاً ابتدا با تگ <code>[[FTP_CONFIG: ...]]</code> اطلاعات FTP را ثبت کنید." if is_fa else "\n⚠️ FTP configuration not found for this project.", side_effects, created_task
from ftp_manager import ftp_manager
dry_raw = attrs.get("dry_run") or attrs.get("test") or "false"
dry_run = str(dry_raw).lower() in ("true", "1", "yes")
# Auto publish dev -> production before deployment if branch="production"
req_branch = attrs.get("branch") or "production"
if req_branch == "production":
from git_manager import git_manager
try:
await git_manager.git_publish(proj.workspace, message="Auto-publish before FTP deployment", repo_name=proj.name)
except Exception as pe:
logger.warning(f"Auto-publish warning before FTP deploy: {pe}")
res = await ftp_manager.deploy_project(
workspace_path=proj.workspace,
host=proj.ftp_host,
port=proj.ftp_port,
user=proj.ftp_user or "",
password=proj.ftp_password or "",
remote_path=proj.ftp_path or "/",
tls=proj.ftp_tls,
dry_run=dry_run,
)
if res.get("success"):
kb = round(res.get("bytes_transferred", 0) / 1024, 2)
mb = round(kb / 1024, 2)
size_str = f"{mb} مگابایت" if mb >= 1.0 else f"{kb} کیلوبایت"
files_up = res.get("files_uploaded", res.get("files_to_upload", 0))
files_skip = res.get("files_skipped", 0)
duration = res.get("duration", 0)
target_p = res.get("remote_path", "/")
badge = (
f"\n\n🚀 <b>دیپلوی هوشمند FTP با موفقیت انجام شد:</b>\n"
f"• 📁 <b>پروژه:</b> <code>{escape_html(proj.name)}</code>\n"
f"• 🌐 <b>مقصد FTP:</b> <code>{escape_html(proj.ftp_host)}:{proj.ftp_port}</code> (مسیر: <code>{escape_html(target_p)}</code>)\n"
f"• 📤 <b>فایل‌های آپلودشده:</b> <b>{files_up}</b> فایل ({size_str})\n"
f"• 🛡️ <b>فایل‌های زائد/محرمانه نادیده گرفته‌شده:</b> <b>{files_skip}</b> آیتم (Clean Deployment)\n"
f"• ⏱️ <b>مدت زمان انتقال:</b> <code>{duration}</code> ثانیه\n"
f"• ✨ <b>وضعیت:</b> عملیاتی و آماده در سرور مقصد"
if is_fa else
f"\n\n🚀 <b>Smart Clean FTP Deployment Succeeded:</b>\n"
f"• 📁 <b>Project:</b> <code>{escape_html(proj.name)}</code>\n"
f"• 🌐 <b>FTP Target:</b> <code>{escape_html(proj.ftp_host)}:{proj.ftp_port}</code> (Path: <code>{escape_html(target_p)}</code>)\n"
f"• 📤 <b>Files Uploaded:</b> <b>{files_up}</b> ({size_str})\n"
f"• 🛡️ <b>Excluded Junk/Secrets:</b> <b>{files_skip}</b> items (Clean Deployment)\n"
f"• ⏱️ <b>Duration:</b> <code>{duration}s</code>"
)
else:
err = res.get("error", "Unknown error")
badge = f"\n⚠️ <b>خطا در استقرار FTP پروژه:</b> {escape_html(str(err))}" if is_fa else f"\n⚠️ <b>FTP Deployment Failed:</b> {escape_html(str(err))}"
return badge, side_effects, created_task
# -------------------------------------------------------------
# 35. SAVE_MEMORY / STORE_MEMORY / SET_MEMORY
# ------------------------------------------------------------- # -------------------------------------------------------------
elif act in ("SAVE_MEMORY", "STORE_MEMORY", "SET_MEMORY", "ADD_MEMORY"): elif act in ("SAVE_MEMORY", "STORE_MEMORY", "SET_MEMORY", "ADD_MEMORY"):
from memory_manager import memory_manager from memory_manager import memory_manager
+41
View File
@@ -615,3 +615,44 @@ def format_commit_detail_view(
) )
def format_ftp_info(
project_name: str,
host: Optional[str],
port: int = 21,
user: Optional[str] = None,
path: str = "/",
tls: bool = False,
lang: str = "fa",
) -> str:
"""Formats FTP connection and deployment status for Telegram HTML."""
is_fa = (lang or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
clean_p = escape_html(project_name)
host_str = f"<code>{escape_html(host)}:{port}</code>" if host else ("<i>(تنظیم نشده)</i>" if is_fa else "<i>(Not configured)</i>")
user_str = f"<code>{escape_html(user)}</code>" if user else ("<i>(ندارد)</i>" if is_fa else "<i>(None)</i>")
path_str = f"<code>{escape_html(path)}</code>"
tls_str = "بله (FTPS/TLS امن) 🔒" if tls else "خیر (FTP معمولی)"
tls_str_en = "Yes (Secure FTPS/TLS) 🔒" if tls else "No (Standard FTP)"
if is_fa:
return (
f"🚀 <b>تنظیمات استقرار و دیپلوی FTP پروژه</b>\n\n"
f"• 📁 <b>پروژه:</b> <code>{clean_p}</code>\n"
f"• 🌐 <b>سرور / هاست FTP:</b> {host_str}\n"
f"• 👤 <b>نام کاربری:</b> {user_str}\n"
f"• 📂 <b>مسیر مقصد روی هاست:</b> {path_str}\n"
f"• 🔒 <b>پروتکل امن (FTPS):</b> {tls_str}\n\n"
f"💡 <i>در زمان دیپلوی، هوش مصنوعی فایل‌های زائد، کش، سشن‌ها و مخزن گیت (.git) را به صورت خودکار فیلتر کرده و تنها سورس اصلی و تمیز به سرور منتقل می‌شود.</i>"
)
else:
return (
f"🚀 <b>Project FTP Deployment Settings</b>\n\n"
f"• 📁 <b>Project:</b> <code>{clean_p}</code>\n"
f"• 🌐 <b>FTP Host:</b> {host_str}\n"
f"• 👤 <b>Username:</b> {user_str}\n"
f"• 📂 <b>Remote Target Path:</b> {path_str}\n"
f"• 🔒 <b>Secure FTPS/TLS:</b> {tls_str_en}\n\n"
f"💡 <i>During deployment, unnecessary cache, temp, sessions, and .git files are automatically excluded for a clean production upload.</i>"
)
+316
View File
@@ -0,0 +1,316 @@
import os
import sys
import time
import fnmatch
import logging
import asyncio
from pathlib import Path
from typing import Optional, Dict, Any, List, Tuple, Set
import ftplib
import socket
logger = logging.getLogger("AGYFTPManager")
# Standard patterns for junk, temporary, confidential, and unnecessary files that must NEVER be sent to production FTP
DEFAULT_FTP_EXCLUDES = [
# Git and version control
".git",
".git/*",
".gitignore",
".gitattributes",
".gitmodules",
# Environment and secrets (local secrets must not leak to production without explicit setup)
".env",
".env.*",
"*.env",
"*.pem",
"*.key",
# AI & Bot internal session / agent configs
".agents",
".agents/*",
".gemini",
".gemini/*",
"sessions_data",
"sessions_data/*",
"uploads_temp",
"uploads_temp/*",
"*.db-shm",
"*.db-wal",
# Python cache & build
"__pycache__",
"__pycache__/*",
"*.pyc",
"*.pyo",
"*.pyd",
".venv",
".venv/*",
"venv",
"venv/*",
"env",
"env/*",
# Logs & temp files
"*.log",
"tmp",
"tmp/*",
"temp",
"temp/*",
# IDE & OS clutter
".DS_Store",
"Thumbs.db",
"desktop.ini",
".idea",
".idea/*",
".vscode",
".vscode/*",
"*.swp",
"*.swo",
"*~",
]
def should_exclude(rel_path: str, custom_excludes: Optional[List[str]] = None) -> bool:
"""Checks if a relative path matches any exclusion pattern."""
excludes = DEFAULT_FTP_EXCLUDES + (custom_excludes or [])
norm_path = rel_path.replace("\\", "/").strip("/")
parts = norm_path.split("/")
for pattern in excludes:
pat = pattern.replace("\\", "/").strip("/")
# Check against full relative path
if fnmatch.fnmatch(norm_path, pat):
return True
# Check against individual directory / file parts
for part in parts:
if fnmatch.fnmatch(part, pat):
return True
# If pattern is a directory (e.g. .git/* or .git), match if path starts with it
clean_pat = pat.rstrip("/*")
if norm_path == clean_pat or norm_path.startswith(clean_pat + "/"):
return True
return False
class FTPManager:
"""Manages FTP connections, testing, and clean intelligent deployments."""
def _create_ftp_client(
self,
host: str,
port: int = 21,
user: str = "",
password: str = "",
tls: bool = False,
timeout: int = 15,
) -> ftplib.FTP:
"""Helper to create and authenticate an FTP or FTPS client."""
if tls:
ftp = ftplib.FTP_TLS(timeout=timeout)
else:
ftp = ftplib.FTP(timeout=timeout)
ftp.connect(host=host, port=port, timeout=timeout)
ftp.login(user=user or "anonymous", passwd=password or "")
if tls and isinstance(ftp, ftplib.FTP_TLS):
ftp.prot_p() # Secure data connection
return ftp
async def test_connection(
self,
host: str,
port: int = 21,
user: str = "",
password: str = "",
path: str = "/",
tls: bool = False,
timeout: int = 10,
) -> Tuple[bool, str]:
"""Tests FTP credentials and verifies access to the target remote directory."""
if not host or not host.strip():
return False, "آدرس هاست (Host) FTP مشخص نشده است."
def _test():
try:
ftp = self._create_ftp_client(host.strip(), int(port), user.strip(), password, tls, timeout)
# Test CWD to target path if specified
target_path = path.strip() if path else "/"
if target_path and target_path != "/":
try:
ftp.cwd(target_path)
except Exception as e:
pwd = ftp.pwd()
ftp.quit()
return False, f"اتصال به FTP برقرار شد، اما مسیر ریموت «{target_path}» یافت نشد (مسیر فعلی: {pwd}): {e}"
pwd = ftp.pwd()
try:
listing = ftp.nlst()
count = len(listing)
except Exception:
count = 0
ftp.quit()
return True, f"اتصال با موفقیت برقرار شد. مسیر فعلی: <code>{pwd}</code> (تعداد آیتم‌ها: {count})"
except (socket.gaierror, socket.timeout) as e:
return False, f"خطای شبکه / نامعتبر بودن آدرس سرور ({host}:{port}): {e}"
except ftplib.error_perm as e:
return False, f"خطای احراز هویت / دسترسی FTP: {e}"
except Exception as e:
return False, f"خطا در برقراری ارتباط با FTP: {e}"
return await asyncio.to_thread(_test)
async def deploy_project(
self,
workspace_path: str,
host: str,
port: int = 21,
user: str = "",
password: str = "",
remote_path: str = "/",
tls: bool = False,
custom_excludes: Optional[List[str]] = None,
dry_run: bool = False,
) -> Dict[str, Any]:
"""
Deploys files from workspace_path to the remote FTP server cleanly.
Skips all temporary, git, session, and junk files.
"""
ws = Path(workspace_path).expanduser().resolve()
if not ws.exists() or not ws.is_dir():
return {
"success": False,
"error": f"دایرکتوری پروژه در مسیر {workspace_path} یافت نشد.",
"files_uploaded": 0,
"files_skipped": 0,
"bytes_transferred": 0,
"duration": 0.0,
"uploaded_list": [],
}
def _run_deploy():
start_time = time.time()
uploaded_files: List[str] = []
skipped_files: List[str] = []
total_bytes = 0
try:
ftp = self._create_ftp_client(host.strip(), int(port), user.strip(), password, tls, timeout=20)
# Navigate or create remote root path
target_root = (remote_path or "/").strip().replace("\\", "/")
if not target_root.startswith("/"):
target_root = "/" + target_root
target_root = target_root.rstrip("/")
if not target_root:
target_root = "/"
def ensure_remote_dir(r_dir: str):
if r_dir in ("/", ""):
ftp.cwd("/")
return
parts = [p for p in r_dir.split("/") if p]
curr = ""
for p in parts:
curr += "/" + p
try:
ftp.cwd(curr)
except Exception:
try:
ftp.mkd(curr)
ftp.cwd(curr)
except Exception:
pass
ensure_remote_dir(target_root)
# Scan workspace
all_files_to_upload: List[Tuple[Path, str]] = [] # (local_path, rel_path)
for root, dirs, files in os.walk(str(ws)):
rel_dir = os.path.relpath(root, str(ws))
if rel_dir == ".":
rel_dir = ""
# Filter out directories in-place to avoid descending into ignored dirs
dirs_to_keep = []
for d in dirs:
dir_rel = f"{rel_dir}/{d}".strip("/")
if should_exclude(dir_rel, custom_excludes):
skipped_files.append(dir_rel + "/")
else:
dirs_to_keep.append(d)
dirs[:] = dirs_to_keep
for f in files:
file_rel = f"{rel_dir}/{f}".strip("/")
if should_exclude(file_rel, custom_excludes):
skipped_files.append(file_rel)
else:
all_files_to_upload.append((Path(root) / f, file_rel))
if dry_run:
ftp.quit()
return {
"success": True,
"dry_run": True,
"files_to_upload": len(all_files_to_upload),
"files_skipped": len(skipped_files),
"bytes_transferred": sum(p.stat().st_size for p, _ in all_files_to_upload if p.exists()),
"duration": round(time.time() - start_time, 2),
"uploaded_list": [r for _, r in all_files_to_upload[:50]],
"remote_path": target_root,
}
# Upload files
for local_p, rel_p in all_files_to_upload:
if not local_p.exists():
continue
file_size = local_p.stat().st_size
rel_dir = str(Path(rel_p).parent).replace("\\", "/").strip(".")
target_dir = f"{target_root}/{rel_dir}".rstrip("/") if rel_dir else target_root
ensure_remote_dir(target_dir)
file_name = local_p.name
with open(local_p, "rb") as fp:
ftp.storbinary(f"STOR {file_name}", fp)
uploaded_files.append(rel_p)
total_bytes += file_size
ftp.quit()
duration = round(time.time() - start_time, 2)
return {
"success": True,
"dry_run": False,
"files_uploaded": len(uploaded_files),
"files_skipped": len(skipped_files),
"bytes_transferred": total_bytes,
"duration": duration,
"uploaded_list": uploaded_files,
"remote_path": target_root,
}
except Exception as e:
logger.error(f"FTP Deploy error: {e}", exc_info=True)
return {
"success": False,
"error": str(e),
"files_uploaded": len(uploaded_files),
"files_skipped": len(skipped_files),
"bytes_transferred": total_bytes,
"duration": round(time.time() - start_time, 2),
"uploaded_list": uploaded_files,
}
return await asyncio.to_thread(_run_deploy)
ftp_manager = FTPManager()
+140 -19
View File
@@ -159,8 +159,11 @@ class GitManager:
async def init_project_repo(self, workspace_path: str, repo_name: str, owner: str = "root") -> Dict[str, Any]: async def init_project_repo(self, workspace_path: str, repo_name: str, owner: str = "root") -> Dict[str, Any]:
""" """
Initializes git in workspace_path, links with Gitea remote, creates .gitignore, Initializes git in workspace_path with dual branches:
and pushes the initial commit. - `production`: base/production branch
- `dev`: active development branch
Links with Gitea remote, creates .gitignore, pushes initial commits on both branches,
and leaves `dev` as the active branch.
""" """
ws = Path(workspace_path).expanduser().resolve() ws = Path(workspace_path).expanduser().resolve()
os.makedirs(ws, exist_ok=True) os.makedirs(ws, exist_ok=True)
@@ -186,7 +189,7 @@ class GitManager:
try: try:
if is_new: if is_new:
await self._run_cmd(["git", "init", "-b", "main"], cwd=str(ws)) await self._run_cmd(["git", "init", "-b", "production"], cwd=str(ws))
# Configure author # Configure author
await self._run_cmd(["git", "config", "user.name", "Antigravity Bot"], cwd=str(ws)) await self._run_cmd(["git", "config", "user.name", "Antigravity Bot"], cwd=str(ws))
@@ -200,19 +203,36 @@ class GitManager:
else: else:
await self._run_cmd(["git", "remote", "add", "origin", remote_url], cwd=str(ws)) await self._run_cmd(["git", "remote", "add", "origin", remote_url], cwd=str(ws))
# Initial add, commit and push # Check if production branch exists, if on main rename to production
curr_branch = (await self._run_cmd(["git", "branch", "--show-current"], cwd=str(ws))).strip()
if curr_branch in ("main", "master"):
try:
await self._run_cmd(["git", "branch", "-M", "production"], cwd=str(ws))
except Exception:
pass
# Initial add, commit and push to production
status_out = await self._run_cmd(["git", "status", "--porcelain"], cwd=str(ws)) status_out = await self._run_cmd(["git", "status", "--porcelain"], cwd=str(ws))
if status_out.strip() or is_new: if status_out.strip() or is_new:
await self._run_cmd(["git", "add", "-A"], cwd=str(ws)) await self._run_cmd(["git", "add", "-A"], cwd=str(ws))
await self._run_cmd(["git", "commit", "-m", f"Initial commit for {clean_name}"], cwd=str(ws)) await self._run_cmd(["git", "commit", "-m", f"Initial commit for {clean_name}"], cwd=str(ws))
await self._run_cmd(["git", "push", "-u", "origin", "main", "--force"], cwd=str(ws)) await self._run_cmd(["git", "push", "-u", "origin", "production", "--force"], cwd=str(ws))
logger.info(f"Initialized Git repo for {clean_name} at {ws}") # Ensure dev branch exists and is pushed
branches_out = await self._run_cmd(["git", "branch"], cwd=str(ws))
if "dev" not in branches_out:
await self._run_cmd(["git", "checkout", "-b", "dev"], cwd=str(ws))
await self._run_cmd(["git", "push", "-u", "origin", "dev", "--force"], cwd=str(ws))
else:
await self._run_cmd(["git", "checkout", "dev"], cwd=str(ws))
logger.info(f"Initialized Git repo for {clean_name} at {ws} with dev & production branches.")
return { return {
"success": True, "success": True,
"name": clean_name, "name": clean_name,
"web_url": urls["web_url"], "web_url": urls["web_url"],
"clone_url": urls["clone_url"], "clone_url": urls["clone_url"],
"active_branch": "dev",
} }
except Exception as e: except Exception as e:
logger.error(f"Error initializing git repo for {clean_name} in {ws}: {e}") logger.error(f"Error initializing git repo for {clean_name} in {ws}: {e}")
@@ -233,7 +253,7 @@ class GitManager:
porcelain = await self._run_cmd(["git", "status", "--porcelain"], cwd=str(ws)) porcelain = await self._run_cmd(["git", "status", "--porcelain"], cwd=str(ws))
dirty_files = [line.strip() for line in porcelain.splitlines() if line.strip()] dirty_files = [line.strip() for line in porcelain.splitlines() if line.strip()]
branch = (await self._run_cmd(["git", "branch", "--show-current"], cwd=str(ws))).strip() or "main" branch = (await self._run_cmd(["git", "branch", "--show-current"], cwd=str(ws))).strip() or "dev"
# Last commit # Last commit
log_out = await self._run_cmd(["git", "log", "-1", "--format=%h|%an|%ar|%s"], cwd=str(ws)) log_out = await self._run_cmd(["git", "log", "-1", "--format=%h|%an|%ar|%s"], cwd=str(ws))
@@ -258,7 +278,7 @@ class GitManager:
} }
except Exception as e: except Exception as e:
logger.debug(f"Git status error in {ws}: {e}") logger.debug(f"Git status error in {ws}: {e}")
return {"is_git": True, "dirty": False, "files": [], "error": str(e), "branch": "main"} return {"is_git": True, "dirty": False, "files": [], "error": str(e), "branch": "dev"}
async def git_commit_and_push( async def git_commit_and_push(
self, self,
@@ -266,8 +286,9 @@ class GitManager:
message: str = "Auto-commit from Antigravity", message: str = "Auto-commit from Antigravity",
repo_name: Optional[str] = None, repo_name: Optional[str] = None,
owner: str = "root", owner: str = "root",
branch: Optional[str] = None,
) -> Tuple[bool, str]: ) -> Tuple[bool, str]:
"""Stages all changes, commits with message, and pushes to Gitea.""" """Stages all changes, commits with message, and pushes to Gitea on the active branch (default dev)."""
ws = Path(workspace_path).expanduser().resolve() ws = Path(workspace_path).expanduser().resolve()
if not (ws / ".git").exists(): if not (ws / ".git").exists():
name = repo_name or ws.name name = repo_name or ws.name
@@ -276,42 +297,142 @@ class GitManager:
return False, f"Failed to initialize repo: {init_res.get('error')}" return False, f"Failed to initialize repo: {init_res.get('error')}"
try: try:
curr_branch = branch or (await self._run_cmd(["git", "branch", "--show-current"], cwd=str(ws))).strip() or "dev"
# Check for uncommitted changes # Check for uncommitted changes
porcelain = await self._run_cmd(["git", "status", "--porcelain"], cwd=str(ws)) porcelain = await self._run_cmd(["git", "status", "--porcelain"], cwd=str(ws))
if not porcelain.strip(): if not porcelain.strip():
return True, "درخت کاری تمیز است (تغییری برای کامیت وجود نداشت)." return True, f"درخت کاری شاخه <code>{curr_branch}</code> تمیز است (تغییری برای کامیت وجود نداشت)."
await self._run_cmd(["git", "add", "-A"], cwd=str(ws)) await self._run_cmd(["git", "add", "-A"], cwd=str(ws))
clean_msg = message.replace('"', '\\"').replace("\n", " ") clean_msg = message.replace('"', '\\"').replace("\n", " ")
if not clean_msg.strip(): if not clean_msg.strip():
clean_msg = "Updates from Antigravity Bot" clean_msg = f"Updates on {curr_branch} from Antigravity Bot"
commit_out = await self._run_cmd(["git", "commit", "-m", clean_msg], cwd=str(ws)) commit_out = await self._run_cmd(["git", "commit", "-m", clean_msg], cwd=str(ws))
push_out = await self._run_cmd(["git", "push", "origin", "main"], cwd=str(ws)) push_out = await self._run_cmd(["git", "push", "origin", curr_branch], cwd=str(ws))
# Extract hash # Extract hash
log_out = await self._run_cmd(["git", "log", "-1", "--format=%h - %s"], cwd=str(ws)) log_out = await self._run_cmd(["git", "log", "-1", "--format=%h - %s"], cwd=str(ws))
return True, f"✅ تغییرات با موفقیت کامیت و پوش شد:\n<code>{log_out.strip()}</code>" return True, f"✅ تغییرات با موفقیت در شاخه <code>{curr_branch}</code> کامیت و پوش شد:\n<code>{log_out.strip()}</code>"
except Exception as e: except Exception as e:
logger.error(f"Git commit/push error in {ws}: {e}") logger.error(f"Git commit/push error in {ws}: {e}")
return False, f"❌ خطا در کامیت یا پوش: {e}" return False, f"❌ خطا در کامیت یا پوش: {e}"
async def git_pull(self, workspace_path: str) -> Tuple[bool, str]: async def git_pull(self, workspace_path: str, branch: Optional[str] = None) -> Tuple[bool, str]:
"""Pulls latest changes from remote Gitea repository.""" """Pulls latest changes from remote Gitea repository on current or specified branch."""
ws = Path(workspace_path).expanduser().resolve() ws = Path(workspace_path).expanduser().resolve()
if not (ws / ".git").exists(): if not (ws / ".git").exists():
return False, "مخزن گیت برای این مسیر مقداردهی نشده است." return False, "مخزن گیت برای این مسیر مقداردهی نشده است."
try: try:
# Fetch and check curr_branch = branch or (await self._run_cmd(["git", "branch", "--show-current"], cwd=str(ws))).strip() or "dev"
out = await self._run_cmd(["git", "pull", "origin", "main"], cwd=str(ws)) out = await self._run_cmd(["git", "pull", "origin", curr_branch], cwd=str(ws))
return True, f"✅ وضعیت دریافت تغییرات:\n<code>{out.strip()}</code>" return True, f"✅ وضعیت دریافت تغییرات شاخه <code>{curr_branch}</code>:\n<code>{out.strip()}</code>"
except Exception as e: except Exception as e:
logger.warning(f"Git pull warning/error in {ws}: {e}") logger.warning(f"Git pull warning/error in {ws}: {e}")
return False, f"⚠️ خطا در دریافت تغییرات (Pull): {e}" return False, f"⚠️ خطا در دریافت تغییرات (Pull): {e}"
async def git_publish(
self,
workspace_path: str,
message: str = "Publish dev to production",
repo_name: Optional[str] = None,
owner: str = "root",
) -> Tuple[bool, str, Dict[str, Any]]:
"""
Publishes latest changes from `dev` to `production`:
1. Ensures changes on `dev` are committed and pushed.
2. Checks out `production` branch.
3. Merges `dev` into `production`.
4. Pushes `production` to Gitea origin.
5. Switches back to `dev`.
"""
ws = Path(workspace_path).expanduser().resolve()
if not (ws / ".git").exists():
name = repo_name or ws.name
init_res = await self.init_project_repo(str(ws), name, owner=owner)
if not init_res.get("success"):
return False, f"خطا در مقداردهی اولیه مخزن: {init_res.get('error')}", {}
try:
# 1. Commit any pending changes on dev
status = await self.git_status(str(ws))
if status.get("dirty"):
await self.git_commit_and_push(str(ws), message=f"Pre-publish commit: {message}", repo_name=repo_name, owner=owner)
# Ensure we are on dev branch
curr_branch = (await self._run_cmd(["git", "branch", "--show-current"], cwd=str(ws))).strip()
if curr_branch != "dev":
# Ensure dev exists
branches = await self._run_cmd(["git", "branch"], cwd=str(ws))
if "dev" in branches:
await self._run_cmd(["git", "checkout", "dev"], cwd=str(ws))
else:
await self._run_cmd(["git", "checkout", "-b", "dev"], cwd=str(ws))
# Push dev
try:
await self._run_cmd(["git", "push", "origin", "dev"], cwd=str(ws))
except Exception as pe:
logger.warning(f"Push dev warning before publish in {ws}: {pe}")
# Ensure production branch exists
branches = await self._run_cmd(["git", "branch"], cwd=str(ws))
if "production" not in branches:
await self._run_cmd(["git", "branch", "production"], cwd=str(ws))
# 2. Checkout production
await self._run_cmd(["git", "checkout", "production"], cwd=str(ws))
# Pull production if remote exists
try:
await self._run_cmd(["git", "pull", "origin", "production"], cwd=str(ws))
except Exception:
pass
# 3. Merge dev into production
merge_msg = f"Merge branch 'dev' into production: {message}".replace('"', '\\"')
merge_out = await self._run_cmd(["git", "merge", "dev", "--no-edit", "-m", merge_msg], cwd=str(ws))
# 4. Push production to Gitea
push_out = await self._run_cmd(["git", "push", "origin", "production"], cwd=str(ws))
# Get production latest commit
prod_commit = (await self._run_cmd(["git", "log", "-1", "--format=%h|%an|%ar|%s"], cwd=str(ws))).strip()
# 5. Switch back to dev branch
await self._run_cmd(["git", "checkout", "dev"], cwd=str(ws))
commit_parts = prod_commit.split("|", 3) if "|" in prod_commit else [prod_commit, "", "", ""]
commit_hash = commit_parts[0]
commit_text = commit_parts[3] if len(commit_parts) > 3 else prod_commit
res_msg = (
f"🚀 <b>پروژه با موفقیت روی شاخه <code>production</code> پابلیش شد!</b>\n\n"
f"• 🌿 <b>شاخه مبدأ:</b> <code>dev</code>\n"
f"• 🚀 <b>شاخه مقصد:</b> <code>production</code>\n"
f"• 🔖 <b>آخرین کامیت:</b> <code>{commit_hash}</code> - {escape_html(commit_text)}\n"
f"• 🔄 <b>وضعیت کاری:</b> مجدداً به شاخه <code>dev</code> بازگشت داده شد تا توسعه ادامه یابد."
)
return True, res_msg, {
"commit_hash": commit_hash,
"commit_text": commit_text,
"source_branch": "dev",
"target_branch": "production",
}
except Exception as e:
logger.error(f"Error publishing dev to production in {ws}: {e}")
# Ensure we return to dev
try:
await self._run_cmd(["git", "checkout", "dev"], cwd=str(ws))
except Exception:
pass
return False, f"❌ خطا در عملیات پابلیش به شاخه پروداکشن: {e}", {}
async def git_sync(self, workspace_path: str, message: str = "Sync with Gitea", repo_name: Optional[str] = None) -> Tuple[bool, str]: async def git_sync(self, workspace_path: str, message: str = "Sync with Gitea", repo_name: Optional[str] = None) -> Tuple[bool, str]:
"""Performs full sync: Pulls remote changes, then commits and pushes any local changes.""" """Performs full sync: Pulls remote changes, then commits and pushes any local changes on active branch."""
ws = Path(workspace_path).expanduser().resolve() ws = Path(workspace_path).expanduser().resolve()
if not (ws / ".git").exists(): if not (ws / ".git").exists():
name = repo_name or ws.name name = repo_name or ws.name
File diff suppressed because one or more lines are too long