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]
name = "quicommit"
version = "0.1.2"
version = "0.1.4"
edition = "2024"
authors = ["Sidney Zhang <zly@lyzhang.me>"]
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)]
pub struct CommitCommand {
/// Commit type
#[arg(short, long)]
#[arg(long)]
commit_type: Option<String>,
/// 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<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()
.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()
.commit_type(commit_type)
.description(description)

View File

@@ -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(())

View File

@@ -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};

View File

@@ -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(())
}

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 {
match self.language {
Language::English => "🤖 AI is analyzing your changes...",