mod builders;
mod database;
pub use builders::{
BranchBuilder, BranchesBuilder, CommitBuilder, FileEntry, FileQuery, FileType, FilesBuilder,
FindBuilder, FindResult, FsBuilder, FsOperation, FsOpsBuilder, FsPreview, HistoryBuilder,
Permissions, TagBuilder, TagsBuilder, UserBuilder, UsersBuilder,
};
pub use database::Database;
use crate::artifact::{blob, manifest};
use crate::error::{FossilError, Result};
use crate::hash;
use crate::sync::SyncBuilder;
use chrono::Utc;
use std::path::Path;
pub struct Repository {
db: Database,
}
#[derive(Debug, Clone)]
pub struct CheckIn {
pub rid: i64,
pub hash: String,
pub timestamp: String,
pub user: String,
pub comment: String,
pub parents: Vec<String>,
pub branch: Option<String>,
}
#[derive(Debug, Clone)]
pub struct FileInfo {
pub name: String,
pub hash: String,
pub permissions: Option<String>,
pub size: Option<usize>,
}
impl Repository {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
let db = Database::open(path)?;
Ok(Self { db })
}
pub fn open_rw<P: AsRef<Path>>(path: P) -> Result<Self> {
let db = Database::open_rw(path)?;
Ok(Self { db })
}
pub fn init<P: AsRef<Path>>(path: P) -> Result<Self> {
let db = Database::init(path)?;
Ok(Self { db })
}
pub fn files(&self) -> FilesBuilder<'_> {
FilesBuilder::new(self)
}
pub fn commit_builder(&self) -> CommitBuilder<'_> {
CommitBuilder::new(self)
}
pub fn branches(&self) -> BranchesBuilder<'_> {
BranchesBuilder::new(self)
}
pub fn tags(&self) -> TagsBuilder<'_> {
TagsBuilder::new(self)
}
pub fn history(&self) -> HistoryBuilder<'_> {
HistoryBuilder::new(self)
}
pub fn users(&self) -> UsersBuilder<'_> {
UsersBuilder::new(self)
}
pub fn sync(&self) -> SyncBuilder<'_> {
SyncBuilder::new(self)
}
pub fn fs(&self) -> FsOpsBuilder<'_> {
FsOpsBuilder::new(self)
}
#[cfg(feature = "git-import")]
pub fn git_import(&self) -> crate::tools::GitImportBuilder<'_> {
crate::tools::GitImportBuilder::new(self)
}
pub fn project_code(&self) -> Result<String> {
self.db.get_project_code()
}
pub fn project_name(&self) -> Result<Option<String>> {
self.db.get_project_name()
}
pub fn database(&self) -> &Database {
&self.db
}
pub fn rebuild(&self) -> Result<()> {
self.db.connection().execute("DELETE FROM leaf", [])?;
self.db.connection().execute(
"INSERT INTO leaf SELECT rid FROM blob WHERE rid NOT IN (SELECT pid FROM plink)
AND rid IN (SELECT objid FROM event WHERE type='ci')",
[],
)?;
Ok(())
}
pub(crate) fn get_checkin_internal(&self, hash: &str) -> Result<CheckIn> {
let rid = self.db.get_rid_by_hash(hash)?;
let full_hash = self.db.get_hash_by_rid(rid)?;
let content = blob::get_artifact_content(&self.db, rid)?;
let manifest = manifest::parse_manifest(&content)?;
Ok(CheckIn {
rid,
hash: full_hash,
timestamp: manifest.timestamp,
user: manifest.user,
comment: manifest.comment,
parents: manifest.parents,
branch: None,
})
}
pub(crate) fn branch_tip_internal(&self, branch: &str) -> Result<CheckIn> {
let rid = if branch == "trunk" {
self.db.get_trunk_tip()?
} else {
self.db.get_branch_tip(branch)?
};
let hash = self.db.get_hash_by_rid(rid)?;
self.get_checkin_internal(&hash)
}
pub(crate) fn recent_checkins_internal(&self, limit: usize) -> Result<Vec<CheckIn>> {
let raw = self.db.get_recent_checkins(limit)?;
let mut result = Vec::with_capacity(raw.len());
for (rid, hash, _mtime, user, comment) in raw {
let content = blob::get_artifact_content(&self.db, rid)?;
let manifest = manifest::parse_manifest(&content)?;
result.push(CheckIn {
rid,
hash,
timestamp: manifest.timestamp,
user,
comment,
parents: manifest.parents,
branch: None,
});
}
Ok(result)
}
pub(crate) fn list_files_internal(&self, checkin_hash: &str) -> Result<Vec<FileInfo>> {
let rid = self.db.get_rid_by_hash(checkin_hash)?;
let files = self.db.get_files_for_manifest(rid)?;
Ok(files
.into_iter()
.map(|(name, hash)| FileInfo {
name,
hash,
permissions: None,
size: None,
})
.collect())
}
pub(crate) fn read_file_internal(&self, checkin_hash: &str, path: &str) -> Result<Vec<u8>> {
let rid = self.db.get_rid_by_hash(checkin_hash)?;
let file_hash = self.db.get_file_hash_from_manifest(rid, path)?;
blob::get_artifact_by_hash(&self.db, &file_hash)
}
pub(crate) fn find_files_internal(
&self,
checkin_hash: &str,
pattern: &str,
) -> Result<Vec<FileInfo>> {
let all_files = self.list_files_internal(checkin_hash)?;
let glob_pattern =
glob::Pattern::new(pattern).map_err(|e| FossilError::InvalidArtifact(e.to_string()))?;
Ok(all_files
.into_iter()
.filter(|f| glob_pattern.matches(&f.name))
.collect())
}
pub(crate) fn list_directory_internal(
&self,
checkin_hash: &str,
dir: &str,
) -> Result<Vec<FileInfo>> {
let all_files = self.list_files_internal(checkin_hash)?;
let dir = dir.trim_end_matches('/');
Ok(all_files
.into_iter()
.filter(|f| {
if dir.is_empty() {
!f.name.contains('/')
} else {
f.name.starts_with(&format!("{}/", dir))
&& !f.name[dir.len() + 1..].contains('/')
}
})
.collect())
}
pub(crate) fn list_subdirs_internal(
&self,
checkin_hash: &str,
dir: &str,
) -> Result<Vec<String>> {
let all_files = self.list_files_internal(checkin_hash)?;
let dir = dir.trim_end_matches('/');
let prefix = if dir.is_empty() {
String::new()
} else {
format!("{}/", dir)
};
let mut subdirs: Vec<String> = all_files
.into_iter()
.filter_map(|f| {
if f.name.starts_with(&prefix) {
let rest = &f.name[prefix.len()..];
if let Some(idx) = rest.find('/') {
return Some(rest[..idx].to_string());
}
}
None
})
.collect();
subdirs.sort();
subdirs.dedup();
Ok(subdirs)
}
pub(crate) fn list_branches_internal(&self) -> Result<Vec<String>> {
self.db.list_branches()
}
pub(crate) fn list_tags_internal(&self) -> Result<Vec<String>> {
let mut stmt = self.db.connection().prepare(
"SELECT DISTINCT substr(tagname, 5) FROM tag
WHERE tagname LIKE 'sym-%'
AND substr(tagname, 5) NOT IN (SELECT value FROM tagxref WHERE tagid IN
(SELECT tagid FROM tag WHERE tagname = 'branch'))",
)?;
let tags: Vec<String> = stmt
.query_map([], |row| row.get(0))?
.filter_map(|r| r.ok())
.collect();
Ok(tags)
}
pub(crate) fn get_tag_checkin_internal(&self, tag_name: &str) -> Result<String> {
let tag_full = format!("sym-{}", tag_name);
let hash: String = self.db.connection().query_row(
"SELECT b.uuid FROM blob b
JOIN tagxref x ON x.rid = b.rid
JOIN tag t ON t.tagid = x.tagid
WHERE t.tagname = ?1
ORDER BY x.mtime DESC LIMIT 1",
rusqlite::params![tag_full],
|row| row.get(0),
)?;
Ok(hash)
}
pub(crate) fn commit_internal(
&self,
files: &[(&str, &[u8])],
comment: &str,
user: &str,
parent_hash: Option<&str>,
branch: Option<&str>,
) -> Result<String> {
self.db.begin_transaction()?;
let result = self.commit_inner(files, comment, user, parent_hash, branch);
match result {
Ok(hash) => {
self.db.commit_transaction()?;
Ok(hash)
}
Err(e) => {
self.db.rollback_transaction()?;
Err(e)
}
}
}
fn commit_inner(
&self,
files: &[(&str, &[u8])],
comment: &str,
user: &str,
parent_hash: Option<&str>,
branch: Option<&str>,
) -> Result<String> {
let now = Utc::now();
let timestamp = now.format("%Y-%m-%dT%H:%M:%S%.3f").to_string();
let mtime = now.timestamp() as f64 / 86400.0 + 2440587.5;
let mut sorted_files: Vec<(&str, &[u8])> = files.to_vec();
sorted_files.sort_by(|a, b| a.0.cmp(&b.0));
let mut blobs_to_insert: Vec<(Vec<u8>, String, i64)> =
Vec::with_capacity(sorted_files.len());
let mut file_entries: Vec<(String, String)> = Vec::with_capacity(sorted_files.len());
let mut r_hasher = md5::Context::new();
for (name, content) in &sorted_files {
let file_hash = hash::sha3_256_hex(content);
let compressed = blob::compress(content)?;
blobs_to_insert.push((compressed, file_hash.clone(), content.len() as i64));
file_entries.push((name.to_string(), file_hash));
r_hasher.consume(content);
}
let blob_refs: Vec<(&[u8], &str, i64)> = blobs_to_insert
.iter()
.map(|(c, h, s)| (c.as_slice(), h.as_str(), *s))
.collect();
let file_rids = self.db.insert_blobs(&blob_refs)?;
let r_hash = format!("{:x}", r_hasher.compute());
let mut manifest_lines: Vec<String> = Vec::new();
let escaped_comment = manifest::encode_fossil_string(comment);
manifest_lines.push(format!("C {}", escaped_comment));
manifest_lines.push(format!("D {}", timestamp));
for (name, file_hash) in &file_entries {
let escaped_name = manifest::encode_fossil_string(name);
manifest_lines.push(format!("F {} {}", escaped_name, file_hash));
}
if let Some(parent) = parent_hash {
manifest_lines.push(format!("P {}", parent));
}
manifest_lines.push(format!("R {}", r_hash));
let branch_name = branch.unwrap_or("trunk");
if parent_hash.is_none() || branch.is_some() {
manifest_lines.push(format!("T *branch * {}", branch_name));
manifest_lines.push(format!("T *sym-{} *", branch_name));
}
manifest_lines.push(format!("U {}", user));
let manifest_without_z = manifest_lines.join("\n") + "\n";
let z_hash = format!("{:x}", md5::compute(manifest_without_z.as_bytes()));
manifest_lines.push(format!("Z {}", z_hash));
let manifest_content = manifest_lines.join("\n") + "\n";
let manifest_bytes = manifest_content.as_bytes();
let manifest_hash = hash::sha3_256_hex(manifest_bytes);
let manifest_compressed = blob::compress(manifest_bytes)?;
let manifest_rid = self.db.insert_blob(
&manifest_compressed,
&manifest_hash,
manifest_bytes.len() as i64,
)?;
self.db
.insert_event("ci", manifest_rid, mtime, user, comment)?;
if let Some(parent) = parent_hash {
let parent_rid = self.db.get_rid_by_hash(parent)?;
self.db.insert_plink(parent_rid, manifest_rid, mtime)?;
} else {
self.db.insert_leaf(manifest_rid)?;
}
let branch_tag_id = self.db.get_or_create_tag("branch")?;
self.db
.insert_tagxref(branch_tag_id, 2, manifest_rid, mtime, Some(branch_name))?;
let sym_tag_id = self.db.get_or_create_tag(&format!("sym-{}", branch_name))?;
self.db
.insert_tagxref(sym_tag_id, 2, manifest_rid, mtime, None)?;
if branch.is_some() && parent_hash.is_some() {
let parent_rid = self.db.get_rid_by_hash(parent_hash.unwrap())?;
if let Ok(parent_branch) = self.get_checkin_branch(parent_rid) {
if parent_branch != branch_name {
let old_sym_tag_id = self
.db
.get_or_create_tag(&format!("sym-{}", parent_branch))?;
self.db
.insert_tagxref(old_sym_tag_id, 0, manifest_rid, mtime, None)?;
}
}
}
let names: Vec<&str> = file_entries.iter().map(|(n, _)| n.as_str()).collect();
let fnid_map = self.db.get_or_create_filenames(&names)?;
let mlink_entries: Vec<(i64, i64)> = file_entries
.iter()
.map(|(name, file_hash)| {
let fnid = fnid_map.get(name).copied().unwrap_or(0);
let frid = file_rids.get(file_hash).copied().unwrap_or(0);
(frid, fnid)
})
.collect();
self.db.insert_mlinks(manifest_rid, &mlink_entries)?;
Ok(manifest_hash)
}
fn get_checkin_branch(&self, rid: i64) -> Result<String> {
let branch: String = self.db.connection().query_row(
"SELECT value FROM tagxref WHERE rid = ?1 AND tagid = (SELECT tagid FROM tag WHERE tagname = 'branch')",
rusqlite::params![rid],
|row| row.get(0),
)?;
Ok(branch)
}
pub(crate) fn create_branch_internal(
&self,
branch_name: &str,
parent_hash: &str,
user: &str,
) -> Result<String> {
let comment = format!("Create new branch named \"{}\"", branch_name);
let parent_files = self.list_files_internal(parent_hash)?;
let mut files_content: Vec<(String, Vec<u8>)> = Vec::new();
for file in &parent_files {
let content = self.read_file_internal(parent_hash, &file.name)?;
files_content.push((file.name.clone(), content));
}
let files: Vec<(&str, &[u8])> = files_content
.iter()
.map(|(n, c)| (n.as_str(), c.as_slice()))
.collect();
self.commit_internal(&files, &comment, user, Some(parent_hash), Some(branch_name))
}
pub(crate) fn add_tag_internal(
&self,
tag_name: &str,
checkin_hash: &str,
user: &str,
) -> Result<String> {
let rid = self.db.get_rid_by_hash(checkin_hash)?;
let full_hash = self.db.get_hash_by_rid(rid)?;
let now = Utc::now();
let timestamp = now.format("%Y-%m-%dT%H:%M:%S").to_string();
let mtime = now.timestamp() as f64 / 86400.0 + 2440587.5;
let mut lines: Vec<String> = Vec::new();
lines.push(format!("D {}", timestamp));
lines.push(format!("T +sym-{} {}", tag_name, full_hash));
lines.push(format!("U {}", user));
let content_without_z = lines.join("\n") + "\n";
let z_hash = format!("{:x}", md5::compute(content_without_z.as_bytes()));
lines.push(format!("Z {}", z_hash));
let control_content = lines.join("\n") + "\n";
let control_bytes = control_content.as_bytes();
let control_hash = hash::sha3_256_hex(control_bytes);
let control_compressed = blob::compress(control_bytes)?;
let control_rid = self.db.insert_blob(
&control_compressed,
&control_hash,
control_bytes.len() as i64,
)?;
self.db.insert_event(
"g",
control_rid,
mtime,
user,
&format!("Add tag {}", tag_name),
)?;
let tag_id = self.db.get_or_create_tag(&format!("sym-{}", tag_name))?;
self.db.insert_tagxref(tag_id, 1, rid, mtime, None)?;
Ok(control_hash)
}
pub(crate) fn create_user_internal(
&self,
login: &str,
password: &str,
capabilities: &str,
) -> Result<()> {
self.db.create_user(login, password, capabilities)
}
pub(crate) fn set_user_capabilities_internal(
&self,
login: &str,
capabilities: &str,
) -> Result<()> {
self.db.set_user_capabilities(login, capabilities)
}
pub(crate) fn get_user_capabilities_internal(&self, login: &str) -> Result<Option<String>> {
self.db.get_user_capabilities(login)
}
pub(crate) fn list_users_internal(&self) -> Result<Vec<(String, String)>> {
self.db.list_users()
}
}