Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
da9e13b
feat(codec): support tag signature encoding
mroczect Aug 5, 2026
1f01d5e
feat(branch): add list branches command
mroczect Aug 5, 2026
2fc3e98
feat(command): register new command modules
mroczect Aug 5, 2026
fec82bd
feat(tag): add signing support for tags
mroczect Aug 5, 2026
75f8ffb
feat(domain): add signature field to tag
mroczect Aug 5, 2026
7803b8c
feat(error): add Unsupported error variant
mroczect Aug 5, 2026
3c64e57
feat(gc): implement garbage collection removal
mroczect Aug 5, 2026
f79c161
feat(lib): re-export crypto and reflog modules
mroczect Aug 5, 2026
ff4ec54
feat(merge): add merge strategy trait
mroczect Aug 5, 2026
b20cb34
feat(storage): add all_hashes and remove methods
mroczect Aug 5, 2026
a21c6b4
feat(storage): add reflog and object removal
mroczect Aug 5, 2026
af520c8
feat(storage): extend ObjectStore with all_hashes and remove
mroczect Aug 5, 2026
a5b4737
test(tag): update test for signer option
mroczect Aug 5, 2026
793d17b
feat(describe): add describe command
mroczect Aug 5, 2026
75d535d
feat(fsck): add fsck command
mroczect Aug 5, 2026
6631014
feat(log): add log graph command
mroczect Aug 5, 2026
a6ddcbb
feat(merge): add octopus merge command
mroczect Aug 5, 2026
79c558e
feat(rebase): add rebase command
mroczect Aug 5, 2026
8259728
feat(show): add show command
mroczect Aug 5, 2026
decc4d9
feat(tag): add verify tag command
mroczect Aug 5, 2026
9551a60
feat(crypto): add signer and verifier traits
mroczect Aug 5, 2026
d1050d9
feat(merge): add MergeStrategy trait
mroczect Aug 5, 2026
3840aeb
feat(reflog): add reflog store trait
mroczect Aug 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions src/codec/binary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ impl Encoder for BinaryEncoder {
}

fn encode_tag(&self, tag: &Tag, buf: &mut Vec<u8>) -> Result<(), VctrlError> {
buf.push(1u8);
buf.push(2u8);
buf.extend_from_slice(tag.target.as_bytes());
write_user(&tag.tagger, buf)?;
let ts = tag.timestamp.timestamp();
Expand All @@ -106,6 +106,14 @@ impl Encoder for BinaryEncoder {
.map_err(|_| VctrlError::Other("tag message too long".into()))?;
buf.extend_from_slice(&msg_len.to_be_bytes());
buf.extend_from_slice(msg);
if let Some(sig) = &tag.signature {
let sig_len = u32::try_from(sig.len())
.map_err(|_| VctrlError::Other("signature too long".into()))?;
buf.extend_from_slice(&sig_len.to_be_bytes());
buf.extend_from_slice(sig);
} else {
buf.extend_from_slice(&0u32.to_be_bytes());
}
Ok(())
}
}
Expand Down Expand Up @@ -298,7 +306,7 @@ impl Decoder for BinaryDecoder {
let version = cursor
.read_u8()
.map_err(|e| VctrlError::Other(e.to_string()))?;
if version != 1 {
if version != 1 && version != 2 {
return Err(VctrlError::Other("unsupported tag version".into()));
}
let mut target_hash = [0u8; 64];
Expand Down Expand Up @@ -328,11 +336,33 @@ impl Decoder for BinaryDecoder {
.read_exact(&mut msg_bytes)
.map_err(|e| VctrlError::Other(e.to_string()))?;
let message = String::from_utf8(msg_bytes).map_err(|e| VctrlError::Other(e.to_string()))?;

let signature = if version >= 2 {
let sig_len = cursor
.read_u32::<BigEndian>()
.map_err(|e| VctrlError::Other(e.to_string()))?;
if sig_len > MAX_SIG_LEN {
return Err(VctrlError::Corrupted("signature too long".into()));
}
if sig_len == 0 {
None
} else {
let mut sig = vec![0u8; sig_len as usize];
cursor
.read_exact(&mut sig)
.map_err(|e| VctrlError::Other(e.to_string()))?;
Some(sig)
}
} else {
None
};

Ok(Tag {
target,
tagger,
timestamp,
message,
signature,
})
}
}
Expand Down
24 changes: 24 additions & 0 deletions src/command/branch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,27 @@ impl Command for SetHead {
}
}
}

pub struct ListBranches;

impl Command for ListBranches {
type Output = Vec<(String, Hash, bool)>;

fn execute(
&self,
_store: &mut dyn ObjectStore,
refs: &mut dyn RefStore,
) -> Result<Vec<(String, Hash, bool)>, VctrlError> {
let active_branch = refs.head_ref_name()?;
let branch_refs = refs.list_refs("refs/heads/")?;
let mut result = Vec::new();
for ref_name in branch_refs {
if let Some(hash) = refs.get_ref(&ref_name)? {
let short_name = ref_name.trim_start_matches("refs/heads/").to_string();
let is_active = Some(ref_name.clone()) == active_branch;
result.push((short_name, hash, is_active));
}
}
Ok(result)
}
}
86 changes: 86 additions & 0 deletions src/command/describe.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use crate::command::Command;
use crate::domain::hash::Hash;
use crate::domain::object::Object;
use crate::error::VctrlError;
use crate::storage::traits::{ObjectStore, RefStore};
use std::collections::{HashSet, VecDeque};

pub struct Describe {
pub commit_hash: Hash,
pub max_commits_to_search: usize,
}

impl Command for Describe {
type Output = Option<String>;

fn execute(
&self,
store: &mut dyn ObjectStore,
refs: &mut dyn RefStore,
) -> Result<Option<String>, VctrlError> {
let tag_refs = refs.list_refs("refs/tags/")?;
let mut tag_map: Vec<(Hash, String)> = Vec::new();
for ref_name in &tag_refs {
if let Some(hash) = refs.get_ref(ref_name)? {
let commit_hash = match store.get(&hash)? {
Some(Object::Tag(t)) => t.target,
Some(Object::Commit(_)) => hash,
_ => continue,
};
let tag_name = ref_name.trim_start_matches("refs/tags/").to_string();
tag_map.push((commit_hash, tag_name));
}
}

if tag_map.is_empty() {
return Ok(None);
}

let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back((self.commit_hash, 0usize));
visited.insert(self.commit_hash);

let mut found: Option<(String, usize)> = None;

while let Some((hash, dist)) = queue.pop_front() {
for (commit_hash, tag_name) in &tag_map {
if *commit_hash == hash && (found.is_none() || dist < found.as_ref().unwrap().1) {
found = Some((tag_name.clone(), dist));
}
}
if found.is_some() {
break;
}

if dist >= self.max_commits_to_search {
continue;
}

if let Some(Object::Commit(commit)) = store.get(&hash)? {
for parent in &commit.parents {
if visited.insert(*parent) {
queue.push_back((*parent, dist + 1));
}
}
}
}

if let Some((tag_name, dist)) = found {
let mut desc = tag_name;
if dist > 0 {
desc.push_str(&format!("-{}", dist));
}
let short_hash = self.commit_hash.to_hex();
let short = if short_hash.len() >= 8 {
&short_hash[..8]
} else {
&short_hash
};
desc.push_str(&format!("-g{}", short));
Ok(Some(desc))
} else {
Ok(None)
}
}
}
32 changes: 32 additions & 0 deletions src/command/fsck.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use crate::codec::Encoder;
use crate::command::Command;
use crate::error::VctrlError;
use crate::hashing::Hasher;
use crate::storage::traits::{ObjectStore, ObjectStoreExt, RefStore};

pub struct Fsck {
pub encoder: Box<dyn Encoder>,
pub hasher: Box<dyn Hasher>,
}

impl Command for Fsck {
type Output = Vec<VctrlError>;

fn execute(
&self,
store: &mut dyn ObjectStore,
_refs: &mut dyn RefStore,
) -> Result<Vec<VctrlError>, VctrlError> {
let hashes = match store.all_hashes() {
Ok(h) => h,
Err(e) => return Ok(vec![e]),
};
let mut errors = Vec::new();
for hash in hashes {
if let Err(e) = store.get_verified(&hash, self.encoder.as_ref(), self.hasher.as_ref()) {
errors.push(e);
}
}
Ok(errors)
}
}
69 changes: 69 additions & 0 deletions src/command/log_graph.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use crate::command::Command;
use crate::domain::commit::Commit;
use crate::domain::hash::Hash;
use crate::domain::user::UserID;
use crate::error::VctrlError;
use crate::revwalk::RevWalk;
use crate::storage::traits::{ObjectStore, RefStore};
use chrono::{DateTime, Utc};
use std::collections::HashMap;

#[derive(Debug, Clone)]
pub struct GraphCommit {
pub hash: Hash,
pub message: String,
pub author: UserID,
pub timestamp: DateTime<Utc>,
pub parent_indices: Vec<usize>,
}

pub struct LogGraph {
pub head: Hash,
pub encoder: Box<dyn crate::codec::Encoder>,
pub hasher: Box<dyn crate::hashing::Hasher>,
}

impl Command for LogGraph {
type Output = Vec<GraphCommit>;

fn execute(
&self,
store: &mut dyn ObjectStore,
_refs: &mut dyn RefStore,
) -> Result<Vec<GraphCommit>, VctrlError> {
let walk = RevWalk::new(store, &[self.head])?;
let commits: Vec<Commit> = walk.collect::<Result<Vec<_>, _>>()?;

let mut hashes = Vec::with_capacity(commits.len());
for c in &commits {
let mut buf = Vec::new();
self.encoder.encode_commit(c, &mut buf)?;
let hash = self.hasher.hash_commit_encoded(&buf);
hashes.push(hash);
}

let hash_to_idx: HashMap<Hash, usize> =
hashes.iter().enumerate().map(|(i, h)| (*h, i)).collect();

let graph_commits = commits
.into_iter()
.enumerate()
.map(|(i, c)| {
let parent_indices = c
.parents
.iter()
.filter_map(|p| hash_to_idx.get(p).copied())
.collect();
GraphCommit {
hash: hashes[i],
message: c.message,
author: c.author,
timestamp: c.timestamp,
parent_indices,
}
})
.collect();

Ok(graph_commits)
}
}
15 changes: 14 additions & 1 deletion src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,20 @@ pub mod merge_branch;
pub use merge_branch::*;
pub mod blame;
pub mod diff;
pub mod log_graph;
pub mod stash;

pub use log_graph::*;
pub mod verify_tag;
pub use verify_tag::*;
pub mod describe;
pub use describe::*;
pub mod octopus_merge;
pub use octopus_merge::*;
pub mod rebase;
pub use rebase::*;
pub mod fsck;
pub use fsck::*;
pub mod show;
pub use blame::*;
pub use branch::*;
pub use checkout::*;
Expand All @@ -24,6 +36,7 @@ pub use diff::*;
pub use log::*;
pub use merge::*;
pub use revert::*;
pub use show::*;
pub use stash::*;
pub use tag_cmd::*;
pub use verify_commit::*;
Expand Down
Loading
Loading