1mod builders;
35mod database;
36
37pub use builders::{
38 BranchBuilder, BranchesBuilder, CommitBuilder, FileEntry, FileQuery, FileType, FilesBuilder,
39 FindBuilder, FindResult, FsBuilder, FsOperation, FsOpsBuilder, FsPreview, HistoryBuilder,
40 Permissions, TagBuilder, TagsBuilder, UserBuilder, UsersBuilder,
41};
42pub use database::Database;
43
44use crate::artifact::{blob, manifest};
45use crate::error::{FossilError, Result};
46use crate::hash;
47use crate::sync::SyncBuilder;
48use chrono::Utc;
49use std::path::Path;
50
51pub struct Repository {
72 db: Database,
73}
74
75#[derive(Debug, Clone)]
77pub struct CheckIn {
78 pub rid: i64,
80 pub hash: String,
82 pub timestamp: String,
84 pub user: String,
86 pub comment: String,
88 pub parents: Vec<String>,
90 pub branch: Option<String>,
92}
93
94#[derive(Debug, Clone)]
96pub struct FileInfo {
97 pub name: String,
99 pub hash: String,
101 pub permissions: Option<String>,
103 pub size: Option<usize>,
105}
106
107impl Repository {
108 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
114 let db = Database::open(path)?;
115 Ok(Self { db })
116 }
117
118 pub fn open_rw<P: AsRef<Path>>(path: P) -> Result<Self> {
120 let db = Database::open_rw(path)?;
121 Ok(Self { db })
122 }
123
124 pub fn init<P: AsRef<Path>>(path: P) -> Result<Self> {
126 let db = Database::init(path)?;
127 Ok(Self { db })
128 }
129
130 pub fn files(&self) -> FilesBuilder<'_> {
136 FilesBuilder::new(self)
137 }
138
139 pub fn commit_builder(&self) -> CommitBuilder<'_> {
141 CommitBuilder::new(self)
142 }
143
144 pub fn branches(&self) -> BranchesBuilder<'_> {
146 BranchesBuilder::new(self)
147 }
148
149 pub fn tags(&self) -> TagsBuilder<'_> {
151 TagsBuilder::new(self)
152 }
153
154 pub fn history(&self) -> HistoryBuilder<'_> {
156 HistoryBuilder::new(self)
157 }
158
159 pub fn users(&self) -> UsersBuilder<'_> {
161 UsersBuilder::new(self)
162 }
163
164 pub fn sync(&self) -> SyncBuilder<'_> {
166 SyncBuilder::new(self)
167 }
168
169 pub fn fs(&self) -> FsOpsBuilder<'_> {
171 FsOpsBuilder::new(self)
172 }
173
174 #[cfg(feature = "git-import")]
195 pub fn git_import(&self) -> crate::tools::GitImportBuilder<'_> {
196 crate::tools::GitImportBuilder::new(self)
197 }
198
199 pub fn project_code(&self) -> Result<String> {
205 self.db.get_project_code()
206 }
207
208 pub fn project_name(&self) -> Result<Option<String>> {
210 self.db.get_project_name()
211 }
212
213 pub fn database(&self) -> &Database {
215 &self.db
216 }
217
218 pub fn rebuild(&self) -> Result<()> {
220 self.db.connection().execute("DELETE FROM leaf", [])?;
221 self.db.connection().execute(
222 "INSERT INTO leaf SELECT rid FROM blob WHERE rid NOT IN (SELECT pid FROM plink)
223 AND rid IN (SELECT objid FROM event WHERE type='ci')",
224 [],
225 )?;
226 Ok(())
227 }
228
229 pub(crate) fn get_checkin_internal(&self, hash: &str) -> Result<CheckIn> {
234 let rid = self.db.get_rid_by_hash(hash)?;
235 let full_hash = self.db.get_hash_by_rid(rid)?;
236
237 let content = blob::get_artifact_content(&self.db, rid)?;
238 let manifest = manifest::parse_manifest(&content)?;
239
240 Ok(CheckIn {
241 rid,
242 hash: full_hash,
243 timestamp: manifest.timestamp,
244 user: manifest.user,
245 comment: manifest.comment,
246 parents: manifest.parents,
247 branch: None,
248 })
249 }
250
251 pub(crate) fn branch_tip_internal(&self, branch: &str) -> Result<CheckIn> {
252 let rid = if branch == "trunk" {
253 self.db.get_trunk_tip()?
254 } else {
255 self.db.get_branch_tip(branch)?
256 };
257 let hash = self.db.get_hash_by_rid(rid)?;
258 self.get_checkin_internal(&hash)
259 }
260
261 pub(crate) fn recent_checkins_internal(&self, limit: usize) -> Result<Vec<CheckIn>> {
262 let raw = self.db.get_recent_checkins(limit)?;
263 let mut result = Vec::with_capacity(raw.len());
264
265 for (rid, hash, _mtime, user, comment) in raw {
266 let content = blob::get_artifact_content(&self.db, rid)?;
267 let manifest = manifest::parse_manifest(&content)?;
268
269 result.push(CheckIn {
270 rid,
271 hash,
272 timestamp: manifest.timestamp,
273 user,
274 comment,
275 parents: manifest.parents,
276 branch: None,
277 });
278 }
279
280 Ok(result)
281 }
282
283 pub(crate) fn list_files_internal(&self, checkin_hash: &str) -> Result<Vec<FileInfo>> {
284 let rid = self.db.get_rid_by_hash(checkin_hash)?;
285 let files = self.db.get_files_for_manifest(rid)?;
286
287 Ok(files
288 .into_iter()
289 .map(|(name, hash)| FileInfo {
290 name,
291 hash,
292 permissions: None,
293 size: None,
294 })
295 .collect())
296 }
297
298 pub(crate) fn read_file_internal(&self, checkin_hash: &str, path: &str) -> Result<Vec<u8>> {
299 let rid = self.db.get_rid_by_hash(checkin_hash)?;
300 let file_hash = self.db.get_file_hash_from_manifest(rid, path)?;
301 blob::get_artifact_by_hash(&self.db, &file_hash)
302 }
303
304 pub(crate) fn find_files_internal(
305 &self,
306 checkin_hash: &str,
307 pattern: &str,
308 ) -> Result<Vec<FileInfo>> {
309 let all_files = self.list_files_internal(checkin_hash)?;
310 let glob_pattern =
311 glob::Pattern::new(pattern).map_err(|e| FossilError::InvalidArtifact(e.to_string()))?;
312
313 Ok(all_files
314 .into_iter()
315 .filter(|f| glob_pattern.matches(&f.name))
316 .collect())
317 }
318
319 pub(crate) fn list_directory_internal(
320 &self,
321 checkin_hash: &str,
322 dir: &str,
323 ) -> Result<Vec<FileInfo>> {
324 let all_files = self.list_files_internal(checkin_hash)?;
325 let dir = dir.trim_end_matches('/');
326
327 Ok(all_files
328 .into_iter()
329 .filter(|f| {
330 if dir.is_empty() {
331 !f.name.contains('/')
332 } else {
333 f.name.starts_with(&format!("{}/", dir))
334 && !f.name[dir.len() + 1..].contains('/')
335 }
336 })
337 .collect())
338 }
339
340 pub(crate) fn list_subdirs_internal(
341 &self,
342 checkin_hash: &str,
343 dir: &str,
344 ) -> Result<Vec<String>> {
345 let all_files = self.list_files_internal(checkin_hash)?;
346 let dir = dir.trim_end_matches('/');
347 let prefix = if dir.is_empty() {
348 String::new()
349 } else {
350 format!("{}/", dir)
351 };
352
353 let mut subdirs: Vec<String> = all_files
354 .into_iter()
355 .filter_map(|f| {
356 if f.name.starts_with(&prefix) {
357 let rest = &f.name[prefix.len()..];
358 if let Some(idx) = rest.find('/') {
359 return Some(rest[..idx].to_string());
360 }
361 }
362 None
363 })
364 .collect();
365
366 subdirs.sort();
367 subdirs.dedup();
368 Ok(subdirs)
369 }
370
371 pub(crate) fn list_branches_internal(&self) -> Result<Vec<String>> {
372 self.db.list_branches()
373 }
374
375 pub(crate) fn list_tags_internal(&self) -> Result<Vec<String>> {
376 let mut stmt = self.db.connection().prepare(
377 "SELECT DISTINCT substr(tagname, 5) FROM tag
378 WHERE tagname LIKE 'sym-%'
379 AND substr(tagname, 5) NOT IN (SELECT value FROM tagxref WHERE tagid IN
380 (SELECT tagid FROM tag WHERE tagname = 'branch'))",
381 )?;
382
383 let tags: Vec<String> = stmt
384 .query_map([], |row| row.get(0))?
385 .filter_map(|r| r.ok())
386 .collect();
387
388 Ok(tags)
389 }
390
391 pub(crate) fn get_tag_checkin_internal(&self, tag_name: &str) -> Result<String> {
392 let tag_full = format!("sym-{}", tag_name);
393 let hash: String = self.db.connection().query_row(
394 "SELECT b.uuid FROM blob b
395 JOIN tagxref x ON x.rid = b.rid
396 JOIN tag t ON t.tagid = x.tagid
397 WHERE t.tagname = ?1
398 ORDER BY x.mtime DESC LIMIT 1",
399 rusqlite::params![tag_full],
400 |row| row.get(0),
401 )?;
402 Ok(hash)
403 }
404
405 pub(crate) fn commit_internal(
406 &self,
407 files: &[(&str, &[u8])],
408 comment: &str,
409 user: &str,
410 parent_hash: Option<&str>,
411 branch: Option<&str>,
412 ) -> Result<String> {
413 self.db.begin_transaction()?;
414
415 let result = self.commit_inner(files, comment, user, parent_hash, branch);
416
417 match result {
418 Ok(hash) => {
419 self.db.commit_transaction()?;
420 Ok(hash)
421 }
422 Err(e) => {
423 self.db.rollback_transaction()?;
424 Err(e)
425 }
426 }
427 }
428
429 fn commit_inner(
430 &self,
431 files: &[(&str, &[u8])],
432 comment: &str,
433 user: &str,
434 parent_hash: Option<&str>,
435 branch: Option<&str>,
436 ) -> Result<String> {
437 let now = Utc::now();
438 let timestamp = now.format("%Y-%m-%dT%H:%M:%S%.3f").to_string();
439 let mtime = now.timestamp() as f64 / 86400.0 + 2440587.5;
440
441 let mut sorted_files: Vec<(&str, &[u8])> = files.to_vec();
442 sorted_files.sort_by(|a, b| a.0.cmp(&b.0));
443
444 let mut blobs_to_insert: Vec<(Vec<u8>, String, i64)> =
445 Vec::with_capacity(sorted_files.len());
446 let mut file_entries: Vec<(String, String)> = Vec::with_capacity(sorted_files.len());
447 let mut r_hasher = md5::Context::new();
448
449 for (name, content) in &sorted_files {
450 let file_hash = hash::sha3_256_hex(content);
451 let compressed = blob::compress(content)?;
452 blobs_to_insert.push((compressed, file_hash.clone(), content.len() as i64));
453 file_entries.push((name.to_string(), file_hash));
454 r_hasher.consume(content);
455 }
456
457 let blob_refs: Vec<(&[u8], &str, i64)> = blobs_to_insert
458 .iter()
459 .map(|(c, h, s)| (c.as_slice(), h.as_str(), *s))
460 .collect();
461 let file_rids = self.db.insert_blobs(&blob_refs)?;
462
463 let r_hash = format!("{:x}", r_hasher.compute());
464
465 let mut manifest_lines: Vec<String> = Vec::new();
466
467 let escaped_comment = manifest::encode_fossil_string(comment);
468 manifest_lines.push(format!("C {}", escaped_comment));
469 manifest_lines.push(format!("D {}", timestamp));
470
471 for (name, file_hash) in &file_entries {
472 let escaped_name = manifest::encode_fossil_string(name);
473 manifest_lines.push(format!("F {} {}", escaped_name, file_hash));
474 }
475
476 if let Some(parent) = parent_hash {
477 manifest_lines.push(format!("P {}", parent));
478 }
479
480 manifest_lines.push(format!("R {}", r_hash));
481
482 let branch_name = branch.unwrap_or("trunk");
483 if parent_hash.is_none() || branch.is_some() {
484 manifest_lines.push(format!("T *branch * {}", branch_name));
485 manifest_lines.push(format!("T *sym-{} *", branch_name));
486 }
487
488 manifest_lines.push(format!("U {}", user));
489
490 let manifest_without_z = manifest_lines.join("\n") + "\n";
491 let z_hash = format!("{:x}", md5::compute(manifest_without_z.as_bytes()));
492 manifest_lines.push(format!("Z {}", z_hash));
493
494 let manifest_content = manifest_lines.join("\n") + "\n";
495 let manifest_bytes = manifest_content.as_bytes();
496 let manifest_hash = hash::sha3_256_hex(manifest_bytes);
497 let manifest_compressed = blob::compress(manifest_bytes)?;
498 let manifest_rid = self.db.insert_blob(
499 &manifest_compressed,
500 &manifest_hash,
501 manifest_bytes.len() as i64,
502 )?;
503
504 self.db
505 .insert_event("ci", manifest_rid, mtime, user, comment)?;
506
507 if let Some(parent) = parent_hash {
508 let parent_rid = self.db.get_rid_by_hash(parent)?;
509 self.db.insert_plink(parent_rid, manifest_rid, mtime)?;
510 } else {
511 self.db.insert_leaf(manifest_rid)?;
512 }
513
514 let branch_tag_id = self.db.get_or_create_tag("branch")?;
515 self.db
516 .insert_tagxref(branch_tag_id, 2, manifest_rid, mtime, Some(branch_name))?;
517
518 let sym_tag_id = self.db.get_or_create_tag(&format!("sym-{}", branch_name))?;
519 self.db
520 .insert_tagxref(sym_tag_id, 2, manifest_rid, mtime, None)?;
521
522 if branch.is_some() && parent_hash.is_some() {
523 let parent_rid = self.db.get_rid_by_hash(parent_hash.unwrap())?;
524 if let Ok(parent_branch) = self.get_checkin_branch(parent_rid) {
525 if parent_branch != branch_name {
526 let old_sym_tag_id = self
527 .db
528 .get_or_create_tag(&format!("sym-{}", parent_branch))?;
529 self.db
530 .insert_tagxref(old_sym_tag_id, 0, manifest_rid, mtime, None)?;
531 }
532 }
533 }
534
535 let names: Vec<&str> = file_entries.iter().map(|(n, _)| n.as_str()).collect();
536 let fnid_map = self.db.get_or_create_filenames(&names)?;
537
538 let mlink_entries: Vec<(i64, i64)> = file_entries
539 .iter()
540 .map(|(name, file_hash)| {
541 let fnid = fnid_map.get(name).copied().unwrap_or(0);
542 let frid = file_rids.get(file_hash).copied().unwrap_or(0);
543 (frid, fnid)
544 })
545 .collect();
546 self.db.insert_mlinks(manifest_rid, &mlink_entries)?;
547
548 Ok(manifest_hash)
549 }
550
551 fn get_checkin_branch(&self, rid: i64) -> Result<String> {
552 let branch: String = self.db.connection().query_row(
553 "SELECT value FROM tagxref WHERE rid = ?1 AND tagid = (SELECT tagid FROM tag WHERE tagname = 'branch')",
554 rusqlite::params![rid],
555 |row| row.get(0),
556 )?;
557 Ok(branch)
558 }
559
560 pub(crate) fn create_branch_internal(
561 &self,
562 branch_name: &str,
563 parent_hash: &str,
564 user: &str,
565 ) -> Result<String> {
566 let comment = format!("Create new branch named \"{}\"", branch_name);
567
568 let parent_files = self.list_files_internal(parent_hash)?;
569 let mut files_content: Vec<(String, Vec<u8>)> = Vec::new();
570
571 for file in &parent_files {
572 let content = self.read_file_internal(parent_hash, &file.name)?;
573 files_content.push((file.name.clone(), content));
574 }
575
576 let files: Vec<(&str, &[u8])> = files_content
577 .iter()
578 .map(|(n, c)| (n.as_str(), c.as_slice()))
579 .collect();
580
581 self.commit_internal(&files, &comment, user, Some(parent_hash), Some(branch_name))
582 }
583
584 pub(crate) fn add_tag_internal(
585 &self,
586 tag_name: &str,
587 checkin_hash: &str,
588 user: &str,
589 ) -> Result<String> {
590 let rid = self.db.get_rid_by_hash(checkin_hash)?;
591 let full_hash = self.db.get_hash_by_rid(rid)?;
592 let now = Utc::now();
593 let timestamp = now.format("%Y-%m-%dT%H:%M:%S").to_string();
594 let mtime = now.timestamp() as f64 / 86400.0 + 2440587.5;
595
596 let mut lines: Vec<String> = Vec::new();
597 lines.push(format!("D {}", timestamp));
598 lines.push(format!("T +sym-{} {}", tag_name, full_hash));
599 lines.push(format!("U {}", user));
600
601 let content_without_z = lines.join("\n") + "\n";
602 let z_hash = format!("{:x}", md5::compute(content_without_z.as_bytes()));
603 lines.push(format!("Z {}", z_hash));
604
605 let control_content = lines.join("\n") + "\n";
606 let control_bytes = control_content.as_bytes();
607 let control_hash = hash::sha3_256_hex(control_bytes);
608 let control_compressed = blob::compress(control_bytes)?;
609
610 let control_rid = self.db.insert_blob(
611 &control_compressed,
612 &control_hash,
613 control_bytes.len() as i64,
614 )?;
615
616 self.db.insert_event(
617 "g",
618 control_rid,
619 mtime,
620 user,
621 &format!("Add tag {}", tag_name),
622 )?;
623
624 let tag_id = self.db.get_or_create_tag(&format!("sym-{}", tag_name))?;
625 self.db.insert_tagxref(tag_id, 1, rid, mtime, None)?;
626
627 Ok(control_hash)
628 }
629
630 pub(crate) fn create_user_internal(
631 &self,
632 login: &str,
633 password: &str,
634 capabilities: &str,
635 ) -> Result<()> {
636 self.db.create_user(login, password, capabilities)
637 }
638
639 pub(crate) fn set_user_capabilities_internal(
640 &self,
641 login: &str,
642 capabilities: &str,
643 ) -> Result<()> {
644 self.db.set_user_capabilities(login, capabilities)
645 }
646
647 pub(crate) fn get_user_capabilities_internal(&self, login: &str) -> Result<Option<String>> {
648 self.db.get_user_capabilities(login)
649 }
650
651 pub(crate) fn list_users_internal(&self) -> Result<Vec<(String, String)>> {
652 self.db.list_users()
653 }
654}