AI Update: تسک ها را آماده کن این ساختار عالیه
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
Reference in New Issue
Block a user