feat:(first commit)created repository and complete 0.1.0

This commit is contained in:
2026-01-30 14:18:32 +08:00
commit 5d4156e5e0
36 changed files with 8686 additions and 0 deletions

590
src/git/mod.rs Normal file
View File

@@ -0,0 +1,590 @@
use anyhow::{bail, Context, Result};
use git2::{Repository, Signature, StatusOptions, DiffOptions};
use std::path::Path;
pub mod changelog;
pub mod commit;
pub mod tag;
pub use changelog::ChangelogGenerator;
pub use commit::CommitBuilder;
pub use tag::TagBuilder;
/// Git repository wrapper
pub struct GitRepo {
repo: Repository,
path: std::path::PathBuf,
}
impl GitRepo {
/// Open a git repository
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref().canonicalize()
.unwrap_or_else(|_| path.as_ref().to_path_buf());
let repo = Repository::open(&path)
.with_context(|| format!("Failed to open git repository: {:?}", path))?;
Ok(Self { repo, path })
}
/// Get repository path
pub fn path(&self) -> &Path {
&self.path
}
/// Get internal git2 repository
pub fn inner(&self) -> &Repository {
&self.repo
}
/// Check if this is a valid git repository
pub fn is_valid(&self) -> bool {
!self.repo.is_bare()
}
/// Check if there are uncommitted changes
pub fn has_changes(&self) -> Result<bool> {
let statuses = self.repo.statuses(Some(
StatusOptions::new()
.include_untracked(true)
.renames_head_to_index(true)
.renames_index_to_workdir(true),
))?;
Ok(!statuses.is_empty())
}
/// Get staged diff
pub fn get_staged_diff(&self) -> Result<String> {
let head = self.repo.head().ok();
let head_tree = head.as_ref()
.and_then(|h| h.peel_to_tree().ok());
let mut index = self.repo.index()?;
let index_tree = index.write_tree()?;
let index_tree = self.repo.find_tree(index_tree)?;
let diff = if let Some(head) = head_tree {
self.repo.diff_tree_to_index(Some(&head), Some(&index), None)?
} else {
self.repo.diff_tree_to_index(None, Some(&index), None)?
};
let mut diff_text = String::new();
diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
if let Ok(content) = std::str::from_utf8(line.content()) {
diff_text.push_str(content);
}
true
})?;
Ok(diff_text)
}
/// Get unstaged diff
pub fn get_unstaged_diff(&self) -> Result<String> {
let diff = self.repo.diff_index_to_workdir(None, None)?;
let mut diff_text = String::new();
diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
if let Ok(content) = std::str::from_utf8(line.content()) {
diff_text.push_str(content);
}
true
})?;
Ok(diff_text)
}
/// Get complete diff (staged + unstaged)
pub fn get_full_diff(&self) -> Result<String> {
let staged = self.get_staged_diff().unwrap_or_default();
let unstaged = self.get_unstaged_diff().unwrap_or_default();
Ok(format!("{}{}", staged, unstaged))
}
/// Get list of changed files
pub fn get_changed_files(&self) -> Result<Vec<String>> {
let statuses = self.repo.statuses(Some(
StatusOptions::new()
.include_untracked(true)
.renames_head_to_index(true)
.renames_index_to_workdir(true),
))?;
let mut files = vec![];
for entry in statuses.iter() {
if let Some(path) = entry.path() {
files.push(path.to_string());
}
}
Ok(files)
}
/// Get list of staged files
pub fn get_staged_files(&self) -> Result<Vec<String>> {
let statuses = self.repo.statuses(Some(
StatusOptions::new()
.include_untracked(false),
))?;
let mut files = vec![];
for entry in statuses.iter() {
let status = entry.status();
if status.is_index_new() || status.is_index_modified() || status.is_index_deleted() || status.is_index_renamed() || status.is_index_typechange() {
if let Some(path) = entry.path() {
files.push(path.to_string());
}
}
}
Ok(files)
}
/// Stage files
pub fn stage_files<P: AsRef<Path>>(&self, paths: &[P]) -> Result<()> {
let mut index = self.repo.index()?;
for path in paths {
index.add_path(path.as_ref())?;
}
index.write()?;
Ok(())
}
/// Stage all changes
pub fn stage_all(&self) -> Result<()> {
let mut index = self.repo.index()?;
// Get list of all files in working directory
let mut paths = Vec::new();
for entry in std::fs::read_dir(".")? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
paths.push(path.to_path_buf());
}
}
for path_buf in paths {
if let Ok(_) = index.add_path(&path_buf) {
// File added successfully
}
}
index.write()?;
Ok(())
}
/// Unstage files
pub fn unstage_files<P: AsRef<Path>>(&self, paths: &[P]) -> Result<()> {
let head = self.repo.head()?;
let head_commit = head.peel_to_commit()?;
let head_tree = head_commit.tree()?;
let mut index = self.repo.index()?;
for path in paths {
// For now, just reset the index to HEAD
// This removes all staged changes
index.clear()?;
}
index.write()?;
Ok(())
}
/// Create a commit
pub fn commit(&self, message: &str, sign: bool) -> Result<git2::Oid> {
let signature = self.repo.signature()?;
let head = self.repo.head().ok();
let parents = if let Some(ref head) = head {
vec![head.peel_to_commit()?]
} else {
vec![]
};
let parent_refs: Vec<&git2::Commit> = parents.iter().collect();
let oid = if sign {
// For GPG signing, we need to use the command-line git
self.commit_signed(message, &signature)?
} else {
let tree_id = self.repo.index()?.write_tree()?;
let tree = self.repo.find_tree(tree_id)?;
self.repo.commit(
Some("HEAD"),
&signature,
&signature,
message,
&tree,
&parent_refs,
)?
};
Ok(oid)
}
/// Create a signed commit using git command
fn commit_signed(&self, message: &str, _signature: &git2::Signature) -> Result<git2::Oid> {
use std::process::Command;
// Write message to temp file
let temp_file = tempfile::NamedTempFile::new()?;
std::fs::write(temp_file.path(), message)?;
// Use git CLI for signed commit
let output = Command::new("git")
.args(&["commit", "-S", "-F", temp_file.path().to_str().unwrap()])
.current_dir(&self.path)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Failed to create signed commit: {}", stderr);
}
// Get the new HEAD
let head = self.repo.head()?;
Ok(head.target().unwrap())
}
/// Get current branch name
pub fn current_branch(&self) -> Result<String> {
let head = self.repo.head()?;
if head.is_branch() {
let name = head.shorthand()
.ok_or_else(|| anyhow::anyhow!("Invalid branch name"))?;
Ok(name.to_string())
} else {
bail!("HEAD is not pointing to a branch")
}
}
/// Get current commit hash (short)
pub fn current_commit_short(&self) -> Result<String> {
let head = self.repo.head()?;
let oid = head.target()
.ok_or_else(|| anyhow::anyhow!("No target for HEAD"))?;
Ok(oid.to_string()[..8].to_string())
}
/// Get current commit hash (full)
pub fn current_commit(&self) -> Result<String> {
let head = self.repo.head()?;
let oid = head.target()
.ok_or_else(|| anyhow::anyhow!("No target for HEAD"))?;
Ok(oid.to_string())
}
/// Get commit history
pub fn get_commits(&self, count: usize) -> Result<Vec<CommitInfo>> {
let mut revwalk = self.repo.revwalk()?;
revwalk.push_head()?;
let mut commits = vec![];
for (i, oid) in revwalk.enumerate() {
if i >= count {
break;
}
let oid = oid?;
let commit = self.repo.find_commit(oid)?;
commits.push(CommitInfo {
id: oid.to_string(),
short_id: oid.to_string()[..8].to_string(),
message: commit.message().unwrap_or("").to_string(),
author: commit.author().name().unwrap_or("").to_string(),
email: commit.author().email().unwrap_or("").to_string(),
time: commit.time().seconds(),
});
}
Ok(commits)
}
/// Get commits between two references
pub fn get_commits_between(&self, from: &str, to: &str) -> Result<Vec<CommitInfo>> {
let from_obj = self.repo.revparse_single(from)?;
let to_obj = self.repo.revparse_single(to)?;
let from_commit = from_obj.peel_to_commit()?;
let to_commit = to_obj.peel_to_commit()?;
let mut revwalk = self.repo.revwalk()?;
revwalk.push(to_commit.id())?;
revwalk.hide(from_commit.id())?;
let mut commits = vec![];
for oid in revwalk {
let oid = oid?;
let commit = self.repo.find_commit(oid)?;
commits.push(CommitInfo {
id: oid.to_string(),
short_id: oid.to_string()[..8].to_string(),
message: commit.message().unwrap_or("").to_string(),
author: commit.author().name().unwrap_or("").to_string(),
email: commit.author().email().unwrap_or("").to_string(),
time: commit.time().seconds(),
});
}
Ok(commits)
}
/// Get tags
pub fn get_tags(&self) -> Result<Vec<TagInfo>> {
let mut tags = vec![];
self.repo.tag_foreach(|oid, name| {
let name = String::from_utf8_lossy(name);
let name = name.strip_prefix("refs/tags/").unwrap_or(&name);
if let Ok(commit) = self.repo.find_commit(oid) {
tags.push(TagInfo {
name: name.to_string(),
target: oid.to_string(),
message: commit.message().unwrap_or("").to_string(),
});
}
true
})?;
Ok(tags)
}
/// Create a tag
pub fn create_tag(&self, name: &str, message: Option<&str>, sign: bool) -> Result<()> {
let head = self.repo.head()?;
let target = head.peel_to_commit()?;
if let Some(msg) = message {
// Annotated tag
let sig = self.repo.signature()?;
if sign {
// Use git CLI for signed tags
self.create_signed_tag(name, msg)?;
} else {
self.repo.tag(
name,
target.as_object(),
&sig,
msg,
false,
)?;
}
} else {
// Lightweight tag
self.repo.tag(
name,
target.as_object(),
&self.repo.signature()?,
"",
false,
)?;
}
Ok(())
}
/// Create signed tag using git CLI
fn create_signed_tag(&self, name: &str, message: &str) -> Result<()> {
use std::process::Command;
let output = Command::new("git")
.args(&["tag", "-s", name, "-m", message])
.current_dir(&self.path)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Failed to create signed tag: {}", stderr);
}
Ok(())
}
/// Delete a tag
pub fn delete_tag(&self, name: &str) -> Result<()> {
self.repo.tag_delete(name)?;
Ok(())
}
/// Push to remote
pub fn push(&self, remote: &str, refspec: &str) -> Result<()> {
use std::process::Command;
let output = Command::new("git")
.args(&["push", remote, refspec])
.current_dir(&self.path)
.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Push failed: {}", stderr);
}
Ok(())
}
/// Get remote URL
pub fn get_remote_url(&self, remote: &str) -> Result<String> {
let remote = self.repo.find_remote(remote)?;
let url = remote.url()
.ok_or_else(|| anyhow::anyhow!("Remote has no URL"))?;
Ok(url.to_string())
}
/// Check if working directory is clean
pub fn is_clean(&self) -> Result<bool> {
Ok(!self.has_changes()?)
}
/// Get repository status summary
pub fn status_summary(&self) -> Result<StatusSummary> {
let statuses = self.repo.statuses(Some(StatusOptions::new().include_untracked(true)))?;
let mut staged = 0;
let mut unstaged = 0;
let mut untracked = 0;
let mut conflicted = 0;
for entry in statuses.iter() {
let status = entry.status();
if status.is_index_new() || status.is_index_modified() ||
status.is_index_deleted() || status.is_index_renamed() ||
status.is_index_typechange() {
staged += 1;
}
if status.is_wt_modified() || status.is_wt_deleted() ||
status.is_wt_renamed() || status.is_wt_typechange() {
unstaged += 1;
}
if status.is_wt_new() {
untracked += 1;
}
if status.is_conflicted() {
conflicted += 1;
}
}
Ok(StatusSummary {
staged,
unstaged,
untracked,
conflicted,
clean: staged == 0 && unstaged == 0 && untracked == 0 && conflicted == 0,
})
}
}
/// Commit information
#[derive(Debug, Clone)]
pub struct CommitInfo {
pub id: String,
pub short_id: String,
pub message: String,
pub author: String,
pub email: String,
pub time: i64,
}
impl CommitInfo {
/// Get commit message subject (first line)
pub fn subject(&self) -> &str {
self.message.lines().next().unwrap_or("")
}
/// Get commit type from conventional commit
pub fn commit_type(&self) -> Option<String> {
let subject = self.subject();
if let Some(colon) = subject.find(':') {
let type_part = &subject[..colon];
let type_name = type_part.split('(').next()?;
Some(type_name.to_string())
} else {
None
}
}
}
/// Tag information
#[derive(Debug, Clone)]
pub struct TagInfo {
pub name: String,
pub target: String,
pub message: String,
}
/// Repository status summary
#[derive(Debug, Clone)]
pub struct StatusSummary {
pub staged: usize,
pub unstaged: usize,
pub untracked: usize,
pub conflicted: usize,
pub clean: bool,
}
impl StatusSummary {
/// Format as human-readable string
pub fn format(&self) -> String {
if self.clean {
"working tree clean".to_string()
} else {
let mut parts = vec![];
if self.staged > 0 {
parts.push(format!("{} staged", self.staged));
}
if self.unstaged > 0 {
parts.push(format!("{} unstaged", self.unstaged));
}
if self.untracked > 0 {
parts.push(format!("{} untracked", self.untracked));
}
if self.conflicted > 0 {
parts.push(format!("{} conflicted", self.conflicted));
}
parts.join(", ")
}
}
}
/// Find git repository starting from path and walking up
pub fn find_repo<P: AsRef<Path>>(start_path: P) -> Result<GitRepo> {
let start_path = start_path.as_ref();
if let Ok(repo) = GitRepo::open(start_path) {
return Ok(repo);
}
let mut current = start_path;
while let Some(parent) = current.parent() {
if let Ok(repo) = GitRepo::open(parent) {
return Ok(repo);
}
current = parent;
}
bail!("No git repository found starting from {:?}", start_path)
}
/// Check if path is inside a git repository
pub fn is_git_repo<P: AsRef<Path>>(path: P) -> bool {
find_repo(path).is_ok()
}