From 09d2b6db8c694107e923afd8c5fdc0543cb7b885 Mon Sep 17 00:00:00 2001 From: SidneyZhang Date: Sun, 1 Feb 2026 12:35:26 +0000 Subject: [PATCH] feat: add auto-push functionality to commit and tag commands --- Cargo.toml | 2 +- src/commands/commit.rs | 53 +++++++++++++++++++++++++++++++++---- src/commands/tag.rs | 13 ++++++++- src/config/manager.rs | 2 +- src/git/mod.rs | 28 +++++++------------- src/i18n/messages.rs | 60 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 131 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9b2074d..aed0421 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "quicommit" -version = "0.1.2" +version = "0.1.4" edition = "2024" authors = ["Sidney Zhang "] description = "A powerful Git assistant tool with AI-powered commit/tag/changelog generation(alpha version)" diff --git a/src/commands/commit.rs b/src/commands/commit.rs index 4a10c52..846cb79 100644 --- a/src/commands/commit.rs +++ b/src/commands/commit.rs @@ -15,7 +15,7 @@ use crate::utils::validators::get_commit_types; #[derive(Parser)] pub struct CommitCommand { /// Commit type - #[arg(short, long)] + #[arg(long)] commit_type: Option, /// Commit scope @@ -39,7 +39,7 @@ pub struct CommitCommand { date: bool, /// Manual input (skip AI generation) - #[arg(short, long)] + #[arg(long)] manual: bool, /// Sign the commit @@ -73,6 +73,14 @@ pub struct CommitCommand { /// Skip interactive prompts #[arg(short = 'y', long)] yes: bool, + + /// Push after committing + #[arg(long)] + push: bool, + + /// Remote to push to + #[arg(long, default_value = "origin")] + remote: String, } impl CommitCommand { @@ -101,6 +109,13 @@ impl CommitCommand { config.commit.format }; + // Auto-add if no files are staged and there are unstaged/untracked changes + if status.staged == 0 && (status.unstaged > 0 || status.untracked > 0) && !self.all { + println!("{}", messages.auto_stage_changes().yellow()); + repo.stage_all()?; + println!("{}", messages.staged_all().green()); + } + // Stage all if requested if self.all { repo.stage_all()?; @@ -175,6 +190,24 @@ impl CommitCommand { println!("{} {}", messages.commit_amended().green().bold(), "successfully"); } + // Push after commit if requested or ask user + if self.push { + println!("\n{}", messages.pushing_commit(&self.remote)); + repo.push(&self.remote, "HEAD")?; + println!("{}", messages.pushed_commit(&self.remote)); + } else if !self.yes && !self.dry_run { + let should_push = Confirm::new() + .with_prompt(messages.push_after_commit()) + .default(false) + .interact()?; + + if should_push { + println!("\n{}", messages.pushing_commit(&self.remote)); + repo.push(&self.remote, "HEAD")?; + println!("{}", messages.pushed_commit(&self.remote)); + } + } + Ok(()) } @@ -184,12 +217,22 @@ impl CommitCommand { } fn create_manual_commit(&self, format: CommitFormat) -> Result { - let commit_type = self.commit_type.clone() - .ok_or_else(|| anyhow::anyhow!("Commit type required for manual commit. Use -t "))?; - let description = self.message.clone() .ok_or_else(|| anyhow::anyhow!("Description required for manual commit. Use -m "))?; + // Try to extract commit type from message if not provided + let commit_type = if let Some(ref ct) = self.commit_type { + ct.clone() + } else { + // Parse from conventional commit format: "type: description" + description + .split(':') + .next() + .unwrap_or("feat") + .trim() + .to_string() + }; + let builder = CommitBuilder::new() .commit_type(commit_type) .description(description) diff --git a/src/commands/tag.rs b/src/commands/tag.rs index 8bfbebb..eecaf4d 100644 --- a/src/commands/tag.rs +++ b/src/commands/tag.rs @@ -158,11 +158,22 @@ impl TagCommand { println!("{} {}", messages.tag_created().green(), tag_name.cyan()); - // Push if requested + // Push if requested or ask user if self.push { println!("{}", messages.pushing_tag(&self.remote)); repo.push(&self.remote, &format!("refs/tags/{}", tag_name))?; println!("{}", messages.pushed_tag(&self.remote)); + } else if !self.yes && !self.dry_run { + let should_push = Confirm::new() + .with_prompt(messages.push_after_tag()) + .default(false) + .interact()?; + + if should_push { + println!("{}", messages.pushing_tag(&self.remote)); + repo.push(&self.remote, &format!("refs/tags/{}", tag_name))?; + println!("{}", messages.pushed_tag(&self.remote)); + } } Ok(()) diff --git a/src/config/manager.rs b/src/config/manager.rs index df8323b..95e958e 100644 --- a/src/config/manager.rs +++ b/src/config/manager.rs @@ -1,4 +1,4 @@ -use super::{AppConfig, GitProfile, TokenConfig, TokenType}; +use super::{AppConfig, GitProfile, TokenConfig}; use anyhow::{bail, Context, Result}; use std::collections::HashMap; use std::path::{Path, PathBuf}; diff --git a/src/git/mod.rs b/src/git/mod.rs index fbccb1e..3a3b261 100644 --- a/src/git/mod.rs +++ b/src/git/mod.rs @@ -265,28 +265,18 @@ impl GitRepo { /// Stage all changes including subdirectories pub fn stage_all(&self) -> Result<()> { - let mut index = self.repo.index()?; + // Use git command for reliable staging (handles all edge cases) + let output = std::process::Command::new("git") + .args(&["add", "-A"]) + .current_dir(&self.path) + .output() + .with_context(|| "Failed to stage changes with git command")?; - fn add_directory_recursive(index: &mut git2::Index, base_dir: &Path, current_dir: &Path) -> Result<()> { - for entry in std::fs::read_dir(current_dir) - .with_context(|| format!("Failed to read directory: {:?}", current_dir))? - { - let entry = entry?; - let path = entry.path(); - - if path.is_file() { - if let Ok(rel_path) = path.strip_prefix(base_dir) { - let _ = index.add_path(rel_path); - } - } else if path.is_dir() { - add_directory_recursive(index, base_dir, &path)?; - } - } - Ok(()) + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + bail!("Failed to stage changes: {}", stderr); } - add_directory_recursive(&mut index, &self.path, &self.path)?; - index.write()?; Ok(()) } diff --git a/src/i18n/messages.rs b/src/i18n/messages.rs index de4e410..32d6b54 100644 --- a/src/i18n/messages.rs +++ b/src/i18n/messages.rs @@ -285,6 +285,66 @@ impl Messages { } } + pub fn auto_stage_changes(&self) -> &str { + match self.language { + Language::English => "No files staged. Auto-staging all changes...", + Language::Chinese => "没有暂存文件。自动暂存所有更改...", + Language::Japanese => "ステージされたファイルがありません。すべての変更を自動ステージ中...", + Language::Korean => "스테이징된 파일이 없습니다. 모든 변경 사항을 자동 스테이징 중...", + Language::Spanish => "No hay archivos preparados. Preparando automáticamente todos los cambios...", + Language::French => "Aucun fichier indexé. Indexation automatique de tous les changements...", + Language::German => "Keine Dateien bereitgestellt. Alle Änderungen werden automatisch bereitgestellt...", + } + } + + pub fn push_after_commit(&self) -> &str { + match self.language { + Language::English => "Push changes to remote?", + Language::Chinese => "推送更改到远程仓库?", + Language::Japanese => "リモートに変更をプッシュしますか?", + Language::Korean => "원격으로 변경 사항을 푸시하시겠습니까?", + Language::Spanish => "¿Enviar cambios al remoto?", + Language::French => "Envoyer les modifications au distant ?", + Language::German => "Änderungen an Remote pushen?", + } + } + + pub fn push_after_tag(&self) -> &str { + match self.language { + Language::English => "Push tag to remote?", + Language::Chinese => "推送标签到远程仓库?", + Language::Japanese => "リモートにタグをプッシュしますか?", + Language::Korean => "원격으로 태그를 푸시하시겠습니까?", + Language::Spanish => "¿Enviar etiqueta al remoto?", + Language::French => "Envoyer l'étiquette au distant ?", + Language::German => "Tag an Remote pushen?", + } + } + + pub fn pushing_commit(&self, remote: &str) -> String { + match self.language { + Language::English => format!("→ Pushing commit to {}...", remote), + Language::Chinese => format!("→ 正在推送提交到 {}...", remote), + Language::Japanese => format!("→ コミットを{}にプッシュ中...", remote), + Language::Korean => format!("→ 커밋을 {}로 푸시 중...", remote), + Language::Spanish => format!("→ Enviando commit a {}...", remote), + Language::French => format!("→ Envoi du commit vers {}...", remote), + Language::German => format!("→ Commit wird an {} gepusht...", remote), + } + } + + pub fn pushed_commit(&self, remote: &str) -> String { + match self.language { + Language::English => format!("✓ Pushed commit to {}", remote), + Language::Chinese => format!("✓ 已推送提交到 {}", remote), + Language::Japanese => format!("✓ コミットを{}にプッシュしました", remote), + Language::Korean => format!("✓ 커밋을 {}로 푸시함", remote), + Language::Spanish => format!("✓ Commit enviado a {}", remote), + Language::French => format!("✓ Commit envoyé à {}", remote), + Language::German => format!("✓ Commit an {} gepusht", remote), + } + } + pub fn ai_analyzing(&self) -> &str { match self.language { Language::English => "🤖 AI is analyzing your changes...",