feat: add auto-push functionality to commit and tag commands

This commit is contained in:
2026-02-01 12:35:26 +00:00
parent 0cbd975748
commit 09d2b6db8c
6 changed files with 131 additions and 27 deletions

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "quicommit" name = "quicommit"
version = "0.1.2" version = "0.1.4"
edition = "2024" edition = "2024"
authors = ["Sidney Zhang <zly@lyzhang.me>"] authors = ["Sidney Zhang <zly@lyzhang.me>"]
description = "A powerful Git assistant tool with AI-powered commit/tag/changelog generation(alpha version)" description = "A powerful Git assistant tool with AI-powered commit/tag/changelog generation(alpha version)"

View File

@@ -15,7 +15,7 @@ use crate::utils::validators::get_commit_types;
#[derive(Parser)] #[derive(Parser)]
pub struct CommitCommand { pub struct CommitCommand {
/// Commit type /// Commit type
#[arg(short, long)] #[arg(long)]
commit_type: Option<String>, commit_type: Option<String>,
/// Commit scope /// Commit scope
@@ -39,7 +39,7 @@ pub struct CommitCommand {
date: bool, date: bool,
/// Manual input (skip AI generation) /// Manual input (skip AI generation)
#[arg(short, long)] #[arg(long)]
manual: bool, manual: bool,
/// Sign the commit /// Sign the commit
@@ -73,6 +73,14 @@ pub struct CommitCommand {
/// Skip interactive prompts /// Skip interactive prompts
#[arg(short = 'y', long)] #[arg(short = 'y', long)]
yes: bool, yes: bool,
/// Push after committing
#[arg(long)]
push: bool,
/// Remote to push to
#[arg(long, default_value = "origin")]
remote: String,
} }
impl CommitCommand { impl CommitCommand {
@@ -101,6 +109,13 @@ impl CommitCommand {
config.commit.format 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 // Stage all if requested
if self.all { if self.all {
repo.stage_all()?; repo.stage_all()?;
@@ -175,6 +190,24 @@ impl CommitCommand {
println!("{} {}", messages.commit_amended().green().bold(), "successfully"); 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(()) Ok(())
} }
@@ -184,12 +217,22 @@ impl CommitCommand {
} }
fn create_manual_commit(&self, format: CommitFormat) -> Result<String> { fn create_manual_commit(&self, format: CommitFormat) -> Result<String> {
let commit_type = self.commit_type.clone()
.ok_or_else(|| anyhow::anyhow!("Commit type required for manual commit. Use -t <type>"))?;
let description = self.message.clone() let description = self.message.clone()
.ok_or_else(|| anyhow::anyhow!("Description required for manual commit. Use -m <message>"))?; .ok_or_else(|| anyhow::anyhow!("Description required for manual commit. Use -m <message>"))?;
// 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() let builder = CommitBuilder::new()
.commit_type(commit_type) .commit_type(commit_type)
.description(description) .description(description)

View File

@@ -158,11 +158,22 @@ impl TagCommand {
println!("{} {}", messages.tag_created().green(), tag_name.cyan()); println!("{} {}", messages.tag_created().green(), tag_name.cyan());
// Push if requested // Push if requested or ask user
if self.push { if self.push {
println!("{}", messages.pushing_tag(&self.remote)); println!("{}", messages.pushing_tag(&self.remote));
repo.push(&self.remote, &format!("refs/tags/{}", tag_name))?; repo.push(&self.remote, &format!("refs/tags/{}", tag_name))?;
println!("{}", messages.pushed_tag(&self.remote)); 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(()) Ok(())

View File

@@ -1,4 +1,4 @@
use super::{AppConfig, GitProfile, TokenConfig, TokenType}; use super::{AppConfig, GitProfile, TokenConfig};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};

View File

@@ -265,28 +265,18 @@ impl GitRepo {
/// Stage all changes including subdirectories /// Stage all changes including subdirectories
pub fn stage_all(&self) -> Result<()> { 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<()> { if !output.status.success() {
for entry in std::fs::read_dir(current_dir) let stderr = String::from_utf8_lossy(&output.stderr);
.with_context(|| format!("Failed to read directory: {:?}", current_dir))? bail!("Failed to stage changes: {}", stderr);
{
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(())
} }
add_directory_recursive(&mut index, &self.path, &self.path)?;
index.write()?;
Ok(()) Ok(())
} }

View File

@@ -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 { pub fn ai_analyzing(&self) -> &str {
match self.language { match self.language {
Language::English => "🤖 AI is analyzing your changes...", Language::English => "🤖 AI is analyzing your changes...",