feat:(first commit)created repository and complete 0.1.0
This commit is contained in:
367
src/git/commit.rs
Normal file
367
src/git/commit.rs
Normal file
@@ -0,0 +1,367 @@
|
||||
use super::GitRepo;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use chrono::Local;
|
||||
|
||||
/// Commit builder for creating commits
|
||||
pub struct CommitBuilder {
|
||||
commit_type: Option<String>,
|
||||
scope: Option<String>,
|
||||
description: Option<String>,
|
||||
body: Option<String>,
|
||||
footer: Option<String>,
|
||||
breaking: bool,
|
||||
sign: bool,
|
||||
amend: bool,
|
||||
no_verify: bool,
|
||||
dry_run: bool,
|
||||
format: crate::config::CommitFormat,
|
||||
}
|
||||
|
||||
impl CommitBuilder {
|
||||
/// Create new commit builder
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
commit_type: None,
|
||||
scope: None,
|
||||
description: None,
|
||||
body: None,
|
||||
footer: None,
|
||||
breaking: false,
|
||||
sign: false,
|
||||
amend: false,
|
||||
no_verify: false,
|
||||
dry_run: false,
|
||||
format: crate::config::CommitFormat::Conventional,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set commit type
|
||||
pub fn commit_type(mut self, commit_type: impl Into<String>) -> Self {
|
||||
self.commit_type = Some(commit_type.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set scope
|
||||
pub fn scope(mut self, scope: impl Into<String>) -> Self {
|
||||
self.scope = Some(scope.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set description
|
||||
pub fn description(mut self, description: impl Into<String>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set body
|
||||
pub fn body(mut self, body: impl Into<String>) -> Self {
|
||||
self.body = Some(body.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set footer
|
||||
pub fn footer(mut self, footer: impl Into<String>) -> Self {
|
||||
self.footer = Some(footer.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Mark as breaking change
|
||||
pub fn breaking(mut self, breaking: bool) -> Self {
|
||||
self.breaking = breaking;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sign the commit
|
||||
pub fn sign(mut self, sign: bool) -> Self {
|
||||
self.sign = sign;
|
||||
self
|
||||
}
|
||||
|
||||
/// Amend previous commit
|
||||
pub fn amend(mut self, amend: bool) -> Self {
|
||||
self.amend = amend;
|
||||
self
|
||||
}
|
||||
|
||||
/// Skip pre-commit hooks
|
||||
pub fn no_verify(mut self, no_verify: bool) -> Self {
|
||||
self.no_verify = no_verify;
|
||||
self
|
||||
}
|
||||
|
||||
/// Dry run (don't actually commit)
|
||||
pub fn dry_run(mut self, dry_run: bool) -> Self {
|
||||
self.dry_run = dry_run;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set commit format
|
||||
pub fn format(mut self, format: crate::config::CommitFormat) -> Self {
|
||||
self.format = format;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build commit message
|
||||
pub fn build_message(&self) -> Result<String> {
|
||||
let commit_type = self.commit_type.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Commit type is required"))?;
|
||||
|
||||
let description = self.description.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Description is required"))?;
|
||||
|
||||
let message = match self.format {
|
||||
crate::config::CommitFormat::Conventional => {
|
||||
crate::utils::formatter::format_conventional_commit(
|
||||
commit_type,
|
||||
self.scope.as_deref(),
|
||||
description,
|
||||
self.body.as_deref(),
|
||||
self.footer.as_deref(),
|
||||
self.breaking,
|
||||
)
|
||||
}
|
||||
crate::config::CommitFormat::Commitlint => {
|
||||
crate::utils::formatter::format_commitlint_commit(
|
||||
commit_type,
|
||||
self.scope.as_deref(),
|
||||
description,
|
||||
self.body.as_deref(),
|
||||
self.footer.as_deref(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
/// Execute commit
|
||||
pub fn execute(&self, repo: &GitRepo) -> Result<Option<String>> {
|
||||
let message = self.build_message()?;
|
||||
|
||||
if self.dry_run {
|
||||
return Ok(Some(message));
|
||||
}
|
||||
|
||||
// Check if there are staged changes
|
||||
let staged_files = repo.get_staged_files()?;
|
||||
if staged_files.is_empty() && !self.amend {
|
||||
bail!("No staged changes to commit. Use 'git add' to stage files first.");
|
||||
}
|
||||
|
||||
// Validate message
|
||||
match self.format {
|
||||
crate::config::CommitFormat::Conventional => {
|
||||
crate::utils::validators::validate_conventional_commit(&message)?;
|
||||
}
|
||||
crate::config::CommitFormat::Commitlint => {
|
||||
crate::utils::validators::validate_commitlint_commit(&message)?;
|
||||
}
|
||||
}
|
||||
|
||||
if self.amend {
|
||||
self.amend_commit(repo, &message)?;
|
||||
} else {
|
||||
repo.commit(&message, self.sign)?;
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn amend_commit(&self, repo: &GitRepo, message: &str) -> Result<()> {
|
||||
use std::process::Command;
|
||||
|
||||
let mut args = vec!["commit", "--amend"];
|
||||
|
||||
if self.no_verify {
|
||||
args.push("--no-verify");
|
||||
}
|
||||
|
||||
args.push("-m");
|
||||
args.push(message);
|
||||
|
||||
if self.sign {
|
||||
args.push("-S");
|
||||
}
|
||||
|
||||
let output = Command::new("git")
|
||||
.args(&args)
|
||||
.current_dir(repo.path())
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
bail!("Failed to amend commit: {}", stderr);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CommitBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a date-based commit message
|
||||
pub fn create_date_commit_message(prefix: Option<&str>) -> String {
|
||||
let now = Local::now();
|
||||
let date_str = now.format("%Y-%m-%d").to_string();
|
||||
|
||||
match prefix {
|
||||
Some(p) => format!("{}: {}", p, date_str),
|
||||
None => format!("chore: update {}", date_str),
|
||||
}
|
||||
}
|
||||
|
||||
/// Commit type suggestions based on diff
|
||||
pub fn suggest_commit_type(diff: &str) -> Vec<&'static str> {
|
||||
let mut suggestions = vec![];
|
||||
|
||||
// Check for test files
|
||||
if diff.contains("test") || diff.contains("spec") || diff.contains("__tests__") {
|
||||
suggestions.push("test");
|
||||
}
|
||||
|
||||
// Check for documentation
|
||||
if diff.contains("README") || diff.contains(".md") || diff.contains("docs/") {
|
||||
suggestions.push("docs");
|
||||
}
|
||||
|
||||
// Check for configuration files
|
||||
if diff.contains("config") || diff.contains(".json") || diff.contains(".yaml") || diff.contains(".toml") {
|
||||
suggestions.push("chore");
|
||||
}
|
||||
|
||||
// Check for dependencies
|
||||
if diff.contains("Cargo.toml") || diff.contains("package.json") || diff.contains("requirements.txt") {
|
||||
suggestions.push("build");
|
||||
}
|
||||
|
||||
// Check for CI
|
||||
if diff.contains(".github/") || diff.contains(".gitlab-") || diff.contains("Jenkinsfile") {
|
||||
suggestions.push("ci");
|
||||
}
|
||||
|
||||
// Default suggestions
|
||||
if suggestions.is_empty() {
|
||||
suggestions.extend(&["feat", "fix", "refactor"]);
|
||||
}
|
||||
|
||||
suggestions
|
||||
}
|
||||
|
||||
/// Parse existing commit message
|
||||
pub fn parse_commit_message(message: &str) -> ParsedCommit {
|
||||
let lines: Vec<&str> = message.lines().collect();
|
||||
|
||||
if lines.is_empty() {
|
||||
return ParsedCommit::default();
|
||||
}
|
||||
|
||||
let first_line = lines[0];
|
||||
|
||||
// Try to parse as conventional commit
|
||||
if let Some(colon_pos) = first_line.find(':') {
|
||||
let type_part = &first_line[..colon_pos];
|
||||
let description = first_line[colon_pos + 1..].trim();
|
||||
|
||||
let breaking = type_part.ends_with('!');
|
||||
let type_part = type_part.trim_end_matches('!');
|
||||
|
||||
let (commit_type, scope) = if let Some(open) = type_part.find('(') {
|
||||
if let Some(close) = type_part.find(')') {
|
||||
let t = &type_part[..open];
|
||||
let s = &type_part[open + 1..close];
|
||||
(Some(t.to_string()), Some(s.to_string()))
|
||||
} else {
|
||||
(Some(type_part.to_string()), None)
|
||||
}
|
||||
} else {
|
||||
(Some(type_part.to_string()), None)
|
||||
};
|
||||
|
||||
// Extract body and footer
|
||||
let mut body_lines = vec![];
|
||||
let mut footer_lines = vec![];
|
||||
let mut in_footer = false;
|
||||
|
||||
for line in &lines[1..] {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if line.starts_with("BREAKING CHANGE:") ||
|
||||
line.starts_with("Closes") ||
|
||||
line.starts_with("Fixes") ||
|
||||
line.starts_with("Refs") ||
|
||||
line.starts_with("Co-authored-by:") {
|
||||
in_footer = true;
|
||||
}
|
||||
|
||||
if in_footer {
|
||||
footer_lines.push(line.to_string());
|
||||
} else {
|
||||
body_lines.push(line.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
return ParsedCommit {
|
||||
commit_type,
|
||||
scope,
|
||||
description: Some(description.to_string()),
|
||||
body: if body_lines.is_empty() { None } else { Some(body_lines.join("\n")) },
|
||||
footer: if footer_lines.is_empty() { None } else { Some(footer_lines.join("\n")) },
|
||||
breaking,
|
||||
};
|
||||
}
|
||||
|
||||
// Non-conventional commit
|
||||
ParsedCommit {
|
||||
description: Some(first_line.to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parsed commit structure
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ParsedCommit {
|
||||
pub commit_type: Option<String>,
|
||||
pub scope: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub body: Option<String>,
|
||||
pub footer: Option<String>,
|
||||
pub breaking: bool,
|
||||
}
|
||||
|
||||
impl ParsedCommit {
|
||||
/// Convert back to commit message
|
||||
pub fn to_message(&self, format: crate::config::CommitFormat) -> String {
|
||||
let commit_type = self.commit_type.as_deref().unwrap_or("chore");
|
||||
let description = self.description.as_deref().unwrap_or("update");
|
||||
|
||||
match format {
|
||||
crate::config::CommitFormat::Conventional => {
|
||||
crate::utils::formatter::format_conventional_commit(
|
||||
commit_type,
|
||||
self.scope.as_deref(),
|
||||
description,
|
||||
self.body.as_deref(),
|
||||
self.footer.as_deref(),
|
||||
self.breaking,
|
||||
)
|
||||
}
|
||||
crate::config::CommitFormat::Commitlint => {
|
||||
crate::utils::formatter::format_commitlint_commit(
|
||||
commit_type,
|
||||
self.scope.as_deref(),
|
||||
description,
|
||||
self.body.as_deref(),
|
||||
self.footer.as_deref(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user