1use crate::{
4 AtlasStore, DbError, DbResult, WorktreeUsageSnapshot, WorktreeUsageSyncState, telemetry,
5};
6use projectatlas_core::{
7 CanonicalProjectRoot, MAX_GIT_WORKTREE_REGISTRATIONS, graph::ProjectInstanceId,
8};
9use rusqlite::{Connection, OptionalExtension, Row, params};
10use std::fmt;
11use std::path::Path;
12
13pub const MAIN_WORKTREE_ALIAS: &str = "main";
15pub const MAX_WORKTREE_ALIAS_BYTES: usize = 64;
17const MAX_WORKTREE_REGISTRATION_PATH_BYTES: usize = 128 * 1_024;
19const GIT_ADMINISTRATIVE_IDENTITY_BYTES: usize = 64;
21
22#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
24pub struct WorktreeAlias(String);
25
26impl WorktreeAlias {
27 pub fn parse(value: &str) -> DbResult<Self> {
34 if value.is_empty() {
35 return invalid_alias(value, "alias is empty");
36 }
37 if value.len() > MAX_WORKTREE_ALIAS_BYTES {
38 return invalid_alias(value, "alias exceeds 64 UTF-8 bytes");
39 }
40 if value == MAIN_WORKTREE_ALIAS {
41 return invalid_alias(value, "main is reserved for the control atlas");
42 }
43 let mut bytes = value.bytes();
44 let first = bytes.next().ok_or_else(|| DbError::InvalidWorktreeAlias {
45 alias: value.to_string(),
46 reason: "alias is empty",
47 })?;
48 if !first.is_ascii_lowercase() && !first.is_ascii_digit() {
49 return invalid_alias(value, "alias must start with a lowercase letter or digit");
50 }
51 if !bytes.all(|byte| {
52 byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'.' | b'_' | b'-')
53 }) {
54 return invalid_alias(value, "alias contains unsupported characters");
55 }
56 Ok(Self(value.to_string()))
57 }
58
59 #[must_use]
61 pub fn as_str(&self) -> &str {
62 &self.0
63 }
64}
65
66impl fmt::Display for WorktreeAlias {
67 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
68 formatter.write_str(self.as_str())
69 }
70}
71
72#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub enum WorktreeRegistrationState {
75 Active,
77 Retired,
79}
80
81impl WorktreeRegistrationState {
82 #[must_use]
84 pub const fn as_str(self) -> &'static str {
85 match self {
86 Self::Active => "active",
87 Self::Retired => "retired",
88 }
89 }
90
91 fn parse(value: &str) -> DbResult<Self> {
93 match value {
94 "active" => Ok(Self::Active),
95 "retired" => Ok(Self::Retired),
96 _ => Err(DbError::WorktreeRegistrationRow {
97 reason: "unknown registration state",
98 }),
99 }
100 }
101}
102
103#[derive(Clone, Debug, Eq, PartialEq)]
105pub struct WorktreeRegistration {
106 pub registration_id: i64,
108 pub alias: WorktreeAlias,
110 pub state: WorktreeRegistrationState,
112 pub git_common_directory: String,
115 pub git_common_directory_identity: CanonicalProjectRoot,
117 pub git_administrative_directory: String,
120 pub git_administrative_directory_identity: CanonicalProjectRoot,
122 pub git_administrative_identity: String,
124 pub last_root: String,
127 pub last_root_identity: CanonicalProjectRoot,
129 pub project_instance_id: Option<ProjectInstanceId>,
131 pub accepted_telemetry_revision: u64,
133 pub created_at_epoch: u64,
135 pub retired_at_epoch: Option<u64>,
137}
138
139pub struct ActiveWorktreeRegistrationGuard<'transaction> {
145 connection: &'transaction Connection,
147 registration: WorktreeRegistration,
149}
150
151impl ActiveWorktreeRegistrationGuard<'_> {
152 #[must_use]
154 pub const fn registration(&self) -> &WorktreeRegistration {
155 &self.registration
156 }
157
158 pub fn bind_project(
165 &mut self,
166 root: &Path,
167 project_instance_id: ProjectInstanceId,
168 ) -> DbResult<WorktreeRegistration> {
169 let root = worktree_identity("root", root)?;
170 let bound = bind_registration_project(
171 self.connection,
172 &self.registration,
173 &root,
174 project_instance_id,
175 )?;
176 self.registration = bound.clone();
177 Ok(bound)
178 }
179
180 pub fn bind_project_with_usage_snapshot(
187 &mut self,
188 root: &Path,
189 project_instance_id: ProjectInstanceId,
190 snapshot: &WorktreeUsageSnapshot,
191 ) -> DbResult<(WorktreeRegistration, WorktreeUsageSyncState)> {
192 let root = worktree_identity("root", root)?;
193 let bound = bind_registration_project(
194 self.connection,
195 &self.registration,
196 &root,
197 project_instance_id,
198 )?;
199 let synchronized = telemetry::synchronize_worktree_usage_snapshot(
200 self.connection,
201 bound.registration_id,
202 snapshot,
203 )?;
204 let bound = load_by_id(self.connection, bound.registration_id)?;
205 self.registration = bound.clone();
206 Ok((bound, synchronized))
207 }
208
209 pub fn synchronize_usage_snapshot(
216 &mut self,
217 snapshot: &WorktreeUsageSnapshot,
218 ) -> DbResult<WorktreeUsageSyncState> {
219 let synchronized = telemetry::synchronize_worktree_usage_snapshot(
220 self.connection,
221 self.registration.registration_id,
222 snapshot,
223 )?;
224 self.registration = load_by_id(self.connection, self.registration.registration_id)?;
225 Ok(synchronized)
226 }
227
228 pub fn retire(&mut self, retired_at_epoch: u64) -> DbResult<WorktreeRegistration> {
235 let retired = retire_registration(
236 self.connection,
237 &self.registration,
238 epoch_to_sqlite(retired_at_epoch)?,
239 )?;
240 self.registration = retired.clone();
241 Ok(retired)
242 }
243
244 pub fn retire_with_usage_snapshot(
251 &mut self,
252 root: &Path,
253 project_instance_id: ProjectInstanceId,
254 snapshot: &WorktreeUsageSnapshot,
255 retired_at_epoch: u64,
256 ) -> DbResult<(WorktreeRegistration, WorktreeUsageSyncState)> {
257 let retired_at_epoch = epoch_to_sqlite(retired_at_epoch)?;
258 let (bound, synchronized) =
259 self.bind_project_with_usage_snapshot(root, project_instance_id, snapshot)?;
260 let retired = retire_registration(self.connection, &bound, retired_at_epoch)?;
261 self.registration = retired.clone();
262 Ok((retired, synchronized))
263 }
264}
265
266struct PersistedWorktreeRegistration {
268 registration_id: i64,
270 alias: String,
272 state: String,
274 git_common_directory: String,
276 git_common_directory_identity: Vec<u8>,
278 git_administrative_directory: String,
280 git_administrative_directory_identity: Vec<u8>,
282 git_administrative_identity: String,
284 last_root: String,
286 last_root_identity: Vec<u8>,
288 project_instance_id: Option<Vec<u8>>,
290 accepted_telemetry_revision: i64,
292 created_at_epoch: i64,
294 retired_at_epoch: Option<i64>,
296}
297
298impl AtlasStore {
299 #[allow(clippy::too_many_arguments)]
311 pub fn register_worktree(
312 &self,
313 alias: &WorktreeAlias,
314 git_common_directory: &Path,
315 git_administrative_directory: &Path,
316 git_administrative_identity: &str,
317 root: &Path,
318 project_instance_id: Option<ProjectInstanceId>,
319 created_at_epoch: u64,
320 ) -> DbResult<WorktreeRegistration> {
321 let git_common_directory_identity =
322 worktree_identity("git_common_directory", git_common_directory)?;
323 let git_administrative_directory_identity =
324 worktree_identity("git_administrative_directory", git_administrative_directory)?;
325 let git_administrative_identity =
326 validated_administrative_identity(git_administrative_identity)?;
327 let root_identity = worktree_identity("root", root)?;
328 let git_common_directory = native_path_projection(&git_common_directory_identity)?;
329 let git_administrative_directory =
330 native_path_projection(&git_administrative_directory_identity)?;
331 let root = native_path_projection(&root_identity)?;
332 let created_at_epoch = epoch_to_sqlite(created_at_epoch)?;
333 let project_bytes = project_instance_id.map(ProjectInstanceId::as_bytes);
334 let common_identity_bytes = git_common_directory_identity.encode()?;
335 let administrative_identity_bytes = git_administrative_directory_identity.encode()?;
336 let root_identity_bytes = root_identity.encode()?;
337
338 self.with_validated_write(|transaction| {
339 if let Some(existing) = load_active_by_alias(transaction, alias.as_str())? {
340 if existing.git_administrative_directory_identity
341 != git_administrative_directory_identity
342 {
343 return Err(DbError::WorktreeRegistrationConflict {
344 field: "alias",
345 value: alias.to_string(),
346 });
347 }
348 if existing.git_administrative_identity != git_administrative_identity {
349 return Err(DbError::WorktreeRegistrationConflict {
350 field: "git_administrative_identity",
351 value: git_administrative_identity,
352 });
353 }
354 if identities_conflict(existing.project_instance_id, project_instance_id) {
355 return Err(DbError::WorktreeRegistrationConflict {
356 field: "project_instance_id",
357 value: project_instance_id
358 .map_or_else(String::new, |value| value.to_string()),
359 });
360 }
361 if let Some(project_bytes) = project_bytes.as_ref()
362 && project_identity_exists_for_other(
363 transaction,
364 Some(existing.registration_id),
365 project_bytes.as_slice(),
366 )?
367 {
368 return Err(DbError::WorktreeRegistrationConflict {
369 field: "project_instance_id",
370 value: project_instance_id
371 .map_or_else(String::new, |value| value.to_string()),
372 });
373 }
374 if native_root_identity_exists_for_other(
375 transaction,
376 Some(existing.registration_id),
377 root_identity_bytes.as_slice(),
378 )? {
379 return Err(DbError::WorktreeRegistrationConflict {
380 field: "root",
381 value: root,
382 });
383 }
384 transaction.execute(
385 "UPDATE worktree_registrations
386 SET git_common_directory = ?1, git_common_directory_identity = ?2,
387 last_root = ?3, last_root_identity = ?4,
388 project_instance_id = COALESCE(project_instance_id, ?5)
389 WHERE registration_id = ?6",
390 params![
391 git_common_directory,
392 common_identity_bytes.as_slice(),
393 root,
394 root_identity_bytes.as_slice(),
395 project_bytes.as_ref().map(<[u8; 16]>::as_slice),
396 existing.registration_id,
397 ],
398 )?;
399 return load_by_id(transaction, existing.registration_id);
400 }
401
402 let retired_id = load_matching_retired_id(
403 transaction,
404 &administrative_identity_bytes,
405 &git_administrative_identity,
406 project_bytes.as_ref(),
407 )?;
408 if active_git_identity_exists(
409 transaction,
410 &administrative_identity_bytes,
411 &git_administrative_identity,
412 )? {
413 return Err(DbError::WorktreeRegistrationConflict {
414 field: "git_or_project_identity",
415 value: git_administrative_directory,
416 });
417 }
418 if let Some(project_bytes) = project_bytes.as_ref()
419 && project_identity_exists_for_other(
420 transaction,
421 retired_id,
422 project_bytes.as_slice(),
423 )?
424 {
425 return Err(DbError::WorktreeRegistrationConflict {
426 field: "project_instance_id",
427 value: project_instance_id.map_or_else(String::new, |value| value.to_string()),
428 });
429 }
430 if native_root_identity_exists_for_other(
431 transaction,
432 retired_id,
433 root_identity_bytes.as_slice(),
434 )? {
435 return Err(DbError::WorktreeRegistrationConflict {
436 field: "root",
437 value: root,
438 });
439 }
440 let registration_id = if let Some(registration_id) = retired_id {
441 transaction.execute(
442 "UPDATE worktree_registrations
443 SET alias = ?1, state = 'active', git_common_directory = ?2,
444 git_common_directory_identity = ?3,
445 git_administrative_directory = ?4,
446 git_administrative_directory_identity = ?5,
447 git_administrative_identity = ?6, last_root = ?7,
448 last_root_identity = ?8, project_instance_id = ?9,
449 retired_at_epoch = NULL
450 WHERE registration_id = ?10",
451 params![
452 alias.as_str(),
453 git_common_directory,
454 common_identity_bytes.as_slice(),
455 git_administrative_directory,
456 administrative_identity_bytes.as_slice(),
457 git_administrative_identity,
458 root,
459 root_identity_bytes.as_slice(),
460 project_bytes.as_ref().map(<[u8; 16]>::as_slice),
461 registration_id,
462 ],
463 )?;
464 registration_id
465 } else {
466 let registration_count = transaction.query_row(
467 "SELECT COUNT(*) FROM worktree_registrations",
468 [],
469 |row| row.get::<_, i64>(0),
470 )?;
471 if usize::try_from(registration_count).map_err(|_source| {
472 DbError::WorktreeRegistrationRow {
473 reason: "negative registration count",
474 }
475 })? >= MAX_GIT_WORKTREE_REGISTRATIONS
476 {
477 return Err(DbError::WorktreeRegistrationCapacity {
478 limit: MAX_GIT_WORKTREE_REGISTRATIONS,
479 });
480 }
481 transaction.execute(
482 "INSERT INTO worktree_registrations(
483 alias, state, git_common_directory, git_common_directory_identity,
484 git_administrative_directory, git_administrative_directory_identity,
485 git_administrative_identity, last_root, last_root_identity,
486 project_instance_id, created_at_epoch
487 ) VALUES(?1, 'active', ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
488 params![
489 alias.as_str(),
490 git_common_directory,
491 common_identity_bytes.as_slice(),
492 git_administrative_directory,
493 administrative_identity_bytes.as_slice(),
494 git_administrative_identity,
495 root,
496 root_identity_bytes.as_slice(),
497 project_bytes.as_ref().map(<[u8; 16]>::as_slice),
498 created_at_epoch,
499 ],
500 )?;
501 transaction.last_insert_rowid()
502 };
503 load_by_id(transaction, registration_id)
504 })
505 }
506
507 #[allow(clippy::too_many_arguments)]
514 pub fn register_worktree_with_usage_snapshot(
515 &self,
516 alias: &WorktreeAlias,
517 git_common_directory: &Path,
518 git_administrative_directory: &Path,
519 git_administrative_identity: &str,
520 root: &Path,
521 project_instance_id: ProjectInstanceId,
522 snapshot: &WorktreeUsageSnapshot,
523 created_at_epoch: u64,
524 ) -> DbResult<(WorktreeRegistration, WorktreeUsageSyncState)> {
525 self.with_validated_write(|transaction| {
526 let registration = self.register_worktree(
527 alias,
528 git_common_directory,
529 git_administrative_directory,
530 git_administrative_identity,
531 root,
532 Some(project_instance_id),
533 created_at_epoch,
534 )?;
535 let synchronization = telemetry::synchronize_worktree_usage_snapshot(
536 transaction,
537 registration.registration_id,
538 snapshot,
539 )?;
540 Ok((
541 load_by_id(transaction, registration.registration_id)?,
542 synchronization,
543 ))
544 })
545 }
546
547 pub fn refresh_worktree_root(
554 &self,
555 registration: &WorktreeRegistration,
556 root: &Path,
557 ) -> DbResult<WorktreeRegistration> {
558 let root_identity = worktree_identity("root", root)?;
559 let root = native_path_projection(&root_identity)?;
560 let root_identity_bytes = root_identity.encode()?;
561 self.with_validated_write(|transaction| {
562 let updated = transaction.execute(
563 "UPDATE worktree_registrations
564 SET last_root = ?1, last_root_identity = ?2
565 WHERE registration_id = ?3 AND alias = ?4 AND state = 'active'",
566 params![
567 root,
568 root_identity_bytes.as_slice(),
569 registration.registration_id,
570 registration.alias.as_str()
571 ],
572 )?;
573 if updated != 1 {
574 return Err(DbError::WorktreeRegistrationNotFound {
575 alias: registration.alias.to_string(),
576 });
577 }
578 load_by_id(transaction, registration.registration_id)
579 })
580 }
581
582 pub fn worktree_registration(&self, alias: &WorktreeAlias) -> DbResult<WorktreeRegistration> {
589 load_active_by_alias(&self.connection, alias.as_str())?.ok_or_else(|| {
590 DbError::WorktreeRegistrationNotFound {
591 alias: alias.to_string(),
592 }
593 })
594 }
595
596 pub fn worktree_registrations(
602 &self,
603 include_retired: bool,
604 ) -> DbResult<Vec<WorktreeRegistration>> {
605 let mut statement = self.connection.prepare(
606 "SELECT registration_id, alias, state, git_common_directory,
607 git_common_directory_identity,
608 git_administrative_directory, git_administrative_directory_identity,
609 git_administrative_identity, last_root, last_root_identity,
610 project_instance_id,
611 accepted_telemetry_revision, created_at_epoch, retired_at_epoch
612 FROM worktree_registrations
613 WHERE state = 'active' OR ?1
614 ORDER BY CASE state WHEN 'active' THEN 0 ELSE 1 END, alias, registration_id
615 LIMIT ?2",
616 )?;
617 let limit = i64::try_from(MAX_GIT_WORKTREE_REGISTRATIONS + 1).map_err(|_source| {
618 DbError::WorktreeRegistrationRow {
619 reason: "registration bound exceeds SQLite integer range",
620 }
621 })?;
622 let rows = statement.query_map(params![include_retired, limit], persisted_registration)?;
623 let registrations = rows
624 .map(|row| row.map_err(DbError::from).and_then(try_registration))
625 .collect::<DbResult<Vec<_>>>()?;
626 if registrations.len() > MAX_GIT_WORKTREE_REGISTRATIONS {
627 return Err(DbError::WorktreeRegistrationCapacity {
628 limit: MAX_GIT_WORKTREE_REGISTRATIONS,
629 });
630 }
631 Ok(registrations)
632 }
633
634 pub fn with_matching_active_worktree_catalog<T>(
643 &self,
644 expected: &[WorktreeRegistration],
645 operation: impl FnOnce() -> DbResult<T>,
646 ) -> DbResult<Option<T>> {
647 self.with_validated_write(|_| {
648 let current = self.worktree_registrations(false)?;
649 if current.len() != expected.len()
650 || current.iter().zip(expected).any(|(current, expected)| {
651 current.registration_id != expected.registration_id
652 || current.alias != expected.alias
653 || current.project_instance_id != expected.project_instance_id
654 })
655 {
656 return Ok(None);
657 }
658 operation().map(Some)
659 })
660 }
661
662 pub fn with_active_worktree_registration<T>(
674 &self,
675 registration_id: i64,
676 alias: &WorktreeAlias,
677 operation: impl FnOnce(&mut ActiveWorktreeRegistrationGuard<'_>) -> DbResult<T>,
678 ) -> DbResult<T> {
679 self.with_validated_write(|transaction| {
680 let registration = load_by_id(transaction, registration_id)?;
681 if registration.state != WorktreeRegistrationState::Active
682 || registration.alias != *alias
683 {
684 return Err(DbError::WorktreeRegistrationNotFound {
685 alias: alias.to_string(),
686 });
687 }
688 operation(&mut ActiveWorktreeRegistrationGuard {
689 connection: transaction,
690 registration,
691 })
692 })
693 }
694
695 pub fn with_unbound_worktree_registration<T, E>(
707 &self,
708 registration_id: i64,
709 alias: &WorktreeAlias,
710 operation: impl FnOnce(&WorktreeRegistration) -> Result<T, E>,
711 ) -> DbResult<Result<T, E>> {
712 self.with_active_worktree_registration(registration_id, alias, |guard| {
713 if guard.registration().project_instance_id.is_some() {
714 return Err(DbError::WorktreeRegistrationConflict {
715 field: "project_instance_id",
716 value: guard
717 .registration()
718 .project_instance_id
719 .map_or_else(String::new, |value| value.to_string()),
720 });
721 }
722 Ok(operation(guard.registration()))
723 })
724 }
725
726 pub fn export_worktree_usage_snapshot(&self) -> DbResult<WorktreeUsageSnapshot> {
734 telemetry::export_worktree_usage_snapshot(&self.connection)
735 }
736
737 pub fn with_exclusive_worktree_usage_snapshot<T>(
748 &self,
749 operation: impl FnOnce(&WorktreeUsageSnapshot) -> DbResult<T>,
750 ) -> DbResult<T> {
751 let binding = self.captured_project_binding()?;
752 self.with_telemetry_connection(|connection| {
753 crate::with_validated_native_write_transaction(
754 connection,
755 Some(&binding.project_root_identity),
756 Some(binding.project_instance_id),
757 |transaction| {
758 let snapshot = telemetry::export_worktree_usage_snapshot(transaction)?;
759 operation(&snapshot)
760 },
761 )
762 })
763 }
764
765 pub fn synchronize_worktree_usage(
774 &self,
775 alias: &WorktreeAlias,
776 snapshot: &WorktreeUsageSnapshot,
777 ) -> DbResult<WorktreeUsageSyncState> {
778 let registration = self.worktree_registration(alias)?;
779 self.with_validated_write(|transaction| {
780 telemetry::synchronize_worktree_usage_snapshot(
781 transaction,
782 registration.registration_id,
783 snapshot,
784 )
785 })
786 }
787
788 pub fn bind_worktree_project(
796 &self,
797 registration_id: i64,
798 alias: &WorktreeAlias,
799 root: &Path,
800 project_instance_id: ProjectInstanceId,
801 ) -> DbResult<WorktreeRegistration> {
802 self.with_active_worktree_registration(registration_id, alias, |guard| {
803 guard.bind_project(root, project_instance_id)
804 })
805 }
806
807 pub fn retire_worktree(
817 &self,
818 registration_id: i64,
819 alias: &WorktreeAlias,
820 retired_at_epoch: u64,
821 ) -> DbResult<WorktreeRegistration> {
822 let retired_at_epoch = epoch_to_sqlite(retired_at_epoch)?;
823 self.with_validated_write(|transaction| {
824 let existing = load_by_id(transaction, registration_id)?;
825 if existing.state != WorktreeRegistrationState::Active || existing.alias != *alias {
826 return Err(DbError::WorktreeRegistrationNotFound {
827 alias: alias.to_string(),
828 });
829 }
830 retire_registration(transaction, &existing, retired_at_epoch)
831 })
832 }
833
834 pub fn retire_worktree_with_usage_snapshot(
842 &self,
843 registration_id: i64,
844 alias: &WorktreeAlias,
845 root: &Path,
846 project_instance_id: ProjectInstanceId,
847 snapshot: &WorktreeUsageSnapshot,
848 retired_at_epoch: u64,
849 ) -> DbResult<(WorktreeRegistration, WorktreeUsageSyncState)> {
850 self.with_active_worktree_registration(registration_id, alias, |guard| {
851 guard.retire_with_usage_snapshot(root, project_instance_id, snapshot, retired_at_epoch)
852 })
853 }
854}
855
856fn bind_registration_project(
858 connection: &Connection,
859 registration: &WorktreeRegistration,
860 root: &CanonicalProjectRoot,
861 project_instance_id: ProjectInstanceId,
862) -> DbResult<WorktreeRegistration> {
863 if identities_conflict(registration.project_instance_id, Some(project_instance_id)) {
864 return Err(DbError::WorktreeRegistrationConflict {
865 field: "project_instance_id",
866 value: project_instance_id.to_string(),
867 });
868 }
869 let project_bytes = project_instance_id.as_bytes();
870 if project_identity_exists_for_other(
871 connection,
872 Some(registration.registration_id),
873 project_bytes.as_slice(),
874 )? {
875 return Err(DbError::WorktreeRegistrationConflict {
876 field: "project_instance_id",
877 value: project_instance_id.to_string(),
878 });
879 }
880 let root_display = native_path_projection(root)?;
881 let root_identity = root.encode()?;
882 let updated = connection.execute(
883 "UPDATE worktree_registrations
884 SET last_root = ?1, last_root_identity = ?2, project_instance_id = ?3
885 WHERE registration_id = ?4 AND alias = ?5 AND state = 'active'",
886 params![
887 root_display,
888 root_identity.as_slice(),
889 project_bytes.as_slice(),
890 registration.registration_id,
891 registration.alias.as_str()
892 ],
893 )?;
894 if updated != 1 {
895 return Err(DbError::WorktreeRegistrationNotFound {
896 alias: registration.alias.to_string(),
897 });
898 }
899 load_by_id(connection, registration.registration_id)
900}
901
902fn retire_registration(
904 connection: &Connection,
905 registration: &WorktreeRegistration,
906 retired_at_epoch: i64,
907) -> DbResult<WorktreeRegistration> {
908 if retired_at_epoch < epoch_to_sqlite(registration.created_at_epoch)? {
909 return Err(DbError::WorktreeRegistrationRow {
910 reason: "retirement time precedes creation time",
911 });
912 }
913 let updated = connection.execute(
914 "UPDATE worktree_registrations
915 SET state = 'retired', retired_at_epoch = ?1
916 WHERE registration_id = ?2 AND alias = ?3 AND state = 'active'",
917 params![
918 retired_at_epoch,
919 registration.registration_id,
920 registration.alias.as_str()
921 ],
922 )?;
923 if updated != 1 {
924 return Err(DbError::WorktreeRegistrationNotFound {
925 alias: registration.alias.to_string(),
926 });
927 }
928 load_by_id(connection, registration.registration_id)
929}
930
931fn invalid_alias<T>(value: &str, reason: &'static str) -> DbResult<T> {
933 Err(DbError::InvalidWorktreeAlias {
934 alias: value.to_string(),
935 reason,
936 })
937}
938
939fn worktree_identity(field: &'static str, path: &Path) -> DbResult<CanonicalProjectRoot> {
941 let byte_len = path.as_os_str().as_encoded_bytes().len();
942 if !path.is_absolute() || byte_len > MAX_WORKTREE_REGISTRATION_PATH_BYTES {
943 return Err(DbError::InvalidWorktreeRegistrationPath {
944 field,
945 path: path.to_string_lossy().into_owned(),
946 });
947 }
948 if path.exists() {
953 CanonicalProjectRoot::from_path(path).map_err(DbError::from)
954 } else {
955 CanonicalProjectRoot::from_persisted_path(path.to_path_buf()).map_err(DbError::from)
956 }
957}
958
959fn native_path_projection(identity: &CanonicalProjectRoot) -> DbResult<String> {
961 identity.display_string().or_else(|_| {
962 let encoded = identity.encode()?;
963 Ok(format!(
964 "native-path-unavailable:{}",
965 blake3::hash(&encoded).to_hex()
966 ))
967 })
968}
969
970fn validated_administrative_identity(value: &str) -> DbResult<String> {
972 if value.len() != GIT_ADMINISTRATIVE_IDENTITY_BYTES
973 || !value
974 .bytes()
975 .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase())
976 {
977 return Err(DbError::WorktreeRegistrationRow {
978 reason: "invalid Git administrative identity",
979 });
980 }
981 Ok(value.to_string())
982}
983
984fn epoch_to_sqlite(value: u64) -> DbResult<i64> {
986 i64::try_from(value).map_err(|_source| DbError::WorktreeRegistrationRow {
987 reason: "epoch exceeds SQLite integer range",
988 })
989}
990
991fn identities_conflict(
993 existing: Option<ProjectInstanceId>,
994 requested: Option<ProjectInstanceId>,
995) -> bool {
996 matches!((existing, requested), (Some(left), Some(right)) if left != right)
997}
998
999fn persisted_registration(row: &Row<'_>) -> rusqlite::Result<PersistedWorktreeRegistration> {
1001 Ok(PersistedWorktreeRegistration {
1002 registration_id: row.get(0)?,
1003 alias: row.get(1)?,
1004 state: row.get(2)?,
1005 git_common_directory: row.get(3)?,
1006 git_common_directory_identity: row.get(4)?,
1007 git_administrative_directory: row.get(5)?,
1008 git_administrative_directory_identity: row.get(6)?,
1009 git_administrative_identity: row.get(7)?,
1010 last_root: row.get(8)?,
1011 last_root_identity: row.get(9)?,
1012 project_instance_id: row.get(10)?,
1013 accepted_telemetry_revision: row.get(11)?,
1014 created_at_epoch: row.get(12)?,
1015 retired_at_epoch: row.get(13)?,
1016 })
1017}
1018
1019fn try_registration(row: PersistedWorktreeRegistration) -> DbResult<WorktreeRegistration> {
1021 let git_common_directory_identity =
1022 CanonicalProjectRoot::decode(&row.git_common_directory_identity)?;
1023 let git_administrative_directory_identity =
1024 CanonicalProjectRoot::decode(&row.git_administrative_directory_identity)?;
1025 let last_root_identity = CanonicalProjectRoot::decode(&row.last_root_identity)?;
1026 let project_instance_id = row
1027 .project_instance_id
1028 .map(|value| {
1029 let bytes: [u8; 16] =
1030 value
1031 .try_into()
1032 .map_err(|value: Vec<u8>| DbError::InvalidBlobLength {
1033 field: "worktree_registrations.project_instance_id",
1034 expected: 16,
1035 found: value.len(),
1036 })?;
1037 ProjectInstanceId::from_bytes(bytes).map_err(DbError::from)
1038 })
1039 .transpose()?;
1040 let accepted_telemetry_revision =
1041 u64::try_from(row.accepted_telemetry_revision).map_err(|_source| {
1042 DbError::WorktreeRegistrationRow {
1043 reason: "negative accepted telemetry revision",
1044 }
1045 })?;
1046 let created_at_epoch = u64::try_from(row.created_at_epoch).map_err(|_source| {
1047 DbError::WorktreeRegistrationRow {
1048 reason: "negative creation time",
1049 }
1050 })?;
1051 let retired_at_epoch = row
1052 .retired_at_epoch
1053 .map(|value| {
1054 u64::try_from(value).map_err(|_source| DbError::WorktreeRegistrationRow {
1055 reason: "negative retirement time",
1056 })
1057 })
1058 .transpose()?;
1059 Ok(WorktreeRegistration {
1060 registration_id: row.registration_id,
1061 alias: WorktreeAlias::parse(&row.alias)?,
1062 state: WorktreeRegistrationState::parse(&row.state)?,
1063 git_common_directory: row.git_common_directory,
1064 git_common_directory_identity,
1065 git_administrative_directory: row.git_administrative_directory,
1066 git_administrative_directory_identity,
1067 git_administrative_identity: validated_administrative_identity(
1068 &row.git_administrative_identity,
1069 )?,
1070 last_root: row.last_root,
1071 last_root_identity,
1072 project_instance_id,
1073 accepted_telemetry_revision,
1074 created_at_epoch,
1075 retired_at_epoch,
1076 })
1077}
1078
1079const REGISTRATION_SELECT: &str = "SELECT registration_id, alias, state, git_common_directory,
1081 git_common_directory_identity, git_administrative_directory,
1082 git_administrative_directory_identity, git_administrative_identity,
1083 last_root, last_root_identity, project_instance_id,
1084 accepted_telemetry_revision, created_at_epoch, retired_at_epoch
1085 FROM worktree_registrations";
1086
1087fn load_active_by_alias(
1089 connection: &Connection,
1090 alias: &str,
1091) -> DbResult<Option<WorktreeRegistration>> {
1092 let sql = format!(
1093 "{REGISTRATION_SELECT} INDEXED BY idx_worktree_registrations_active_alias
1094 WHERE state = 'active' AND alias = ?1"
1095 );
1096 let row = connection
1097 .query_row(&sql, [alias], persisted_registration)
1098 .optional()?;
1099 row.map(try_registration).transpose()
1100}
1101
1102fn load_by_id(connection: &Connection, registration_id: i64) -> DbResult<WorktreeRegistration> {
1104 let sql = format!("{REGISTRATION_SELECT} WHERE registration_id = ?1");
1105 let row = connection.query_row(&sql, [registration_id], persisted_registration)?;
1106 try_registration(row)
1107}
1108
1109fn active_git_identity_exists(
1111 connection: &Connection,
1112 administrative_directory: &[u8],
1113 administrative_identity: &str,
1114) -> DbResult<bool> {
1115 let administrative_identity_exists = connection.query_row(
1116 "SELECT EXISTS(
1117 SELECT 1 FROM worktree_registrations
1118 INDEXED BY idx_worktree_registrations_active_native_administrative_directory
1119 WHERE state = 'active' AND git_administrative_directory_identity = ?1
1120 )",
1121 [administrative_directory],
1122 |row| row.get::<_, bool>(0),
1123 )?;
1124 if administrative_identity_exists {
1125 return Ok(true);
1126 }
1127 let lifecycle_identity_exists = connection.query_row(
1128 "SELECT EXISTS(
1129 SELECT 1 FROM worktree_registrations
1130 INDEXED BY idx_worktree_registrations_active_administrative_identity
1131 WHERE state = 'active' AND git_administrative_identity = ?1
1132 )",
1133 [administrative_identity],
1134 |row| row.get::<_, bool>(0),
1135 )?;
1136 if lifecycle_identity_exists {
1137 return Ok(true);
1138 }
1139 Ok(false)
1140}
1141
1142fn project_identity_exists_for_other(
1144 connection: &Connection,
1145 registration_id: Option<i64>,
1146 project_instance_id: &[u8],
1147) -> DbResult<bool> {
1148 let found = connection.query_row(
1149 "SELECT EXISTS(
1150 SELECT 1 FROM worktree_registrations
1151 WHERE registration_id IS NOT ?1 AND project_instance_id = ?2
1152 )",
1153 params![registration_id, project_instance_id],
1154 |row| row.get::<_, bool>(0),
1155 )?;
1156 Ok(found)
1157}
1158
1159fn native_root_identity_exists_for_other(
1161 connection: &Connection,
1162 registration_id: Option<i64>,
1163 root_identity: &[u8],
1164) -> DbResult<bool> {
1165 connection
1166 .query_row(
1167 "SELECT EXISTS(
1168 SELECT 1 FROM worktree_registrations
1169 INDEXED BY idx_worktree_registrations_active_native_root
1170 WHERE state = 'active'
1171 AND registration_id IS NOT ?1
1172 AND last_root_identity = ?2
1173 )",
1174 params![registration_id, root_identity],
1175 |row| row.get::<_, bool>(0),
1176 )
1177 .map_err(DbError::from)
1178}
1179
1180fn load_matching_retired_id(
1182 connection: &Connection,
1183 administrative_directory: &[u8],
1184 administrative_identity: &str,
1185 project_instance_id: Option<&[u8; 16]>,
1186) -> DbResult<Option<i64>> {
1187 connection
1188 .query_row(
1189 "SELECT registration_id
1190 FROM worktree_registrations
1191 WHERE state = 'retired'
1192 AND git_administrative_directory_identity = ?1
1193 AND git_administrative_identity = ?2
1194 AND project_instance_id IS ?3
1195 ORDER BY registration_id DESC
1196 LIMIT 1",
1197 params![
1198 administrative_directory,
1199 administrative_identity,
1200 project_instance_id.map(<[u8; 16]>::as_slice),
1201 ],
1202 |row| row.get(0),
1203 )
1204 .optional()
1205 .map_err(DbError::from)
1206}
1207
1208#[cfg(test)]
1209mod tests {
1210 use super::*;
1211 use std::error::Error;
1212 use std::fmt::Debug;
1213 use std::fs;
1214 use std::io;
1215 #[cfg(unix)]
1216 use std::os::unix::ffi::OsStringExt;
1217 use std::time::Duration;
1218
1219 fn require(condition: bool, message: &str) -> Result<(), Box<dyn Error>> {
1221 if condition {
1222 Ok(())
1223 } else {
1224 Err(io::Error::other(message).into())
1225 }
1226 }
1227
1228 fn sqlite_writer_busy(error: &DbError) -> bool {
1230 matches!(
1231 error,
1232 DbError::Sqlite(rusqlite::Error::SqliteFailure(code, _))
1233 if matches!(
1234 code.code,
1235 rusqlite::ErrorCode::DatabaseBusy | rusqlite::ErrorCode::DatabaseLocked
1236 )
1237 )
1238 }
1239
1240 fn require_eq<T>(actual: &T, expected: &T, label: &str) -> Result<(), Box<dyn Error>>
1242 where
1243 T: Debug + PartialEq,
1244 {
1245 if actual == expected {
1246 Ok(())
1247 } else {
1248 Err(io::Error::other(format!(
1249 "{label} mismatch: expected {expected:?}, found {actual:?}"
1250 ))
1251 .into())
1252 }
1253 }
1254
1255 fn identity(byte: u8) -> Result<ProjectInstanceId, Box<dyn Error>> {
1256 Ok(ProjectInstanceId::from_bytes([byte; 16])?)
1257 }
1258
1259 fn administrative_identity(byte: u8) -> String {
1260 format!("{byte:02x}").repeat(32)
1261 }
1262
1263 #[test]
1264 fn aliases_reject_reserved_or_ambiguous_shapes() -> Result<(), Box<dyn Error>> {
1265 for invalid in ["", "main", "Issue-430", "issue 430", "-issue", "ä"] {
1266 if WorktreeAlias::parse(invalid).is_ok() {
1267 return Err(format!("invalid alias was accepted: {invalid:?}").into());
1268 }
1269 }
1270 let alias = WorktreeAlias::parse("issue-430.fix")?;
1271 require_eq(&alias.as_str(), &"issue-430.fix", "valid alias")?;
1272 Ok(())
1273 }
1274
1275 #[cfg(unix)]
1276 #[test]
1277 fn registry_round_trips_non_utf8_identity_paths_without_lossy_keys()
1278 -> Result<(), Box<dyn Error>> {
1279 let temp = tempfile::tempdir()?;
1280 let control = temp.path().join("control");
1281 let common = temp.path().join("common.git");
1282 let administrative = common.join("worktrees").join("linked");
1283 let root = temp.path().join("linked");
1284 let invalid_common = temp
1285 .path()
1286 .join(std::ffi::OsString::from_vec(b"common-\xff".to_vec()));
1287 let invalid_administrative = temp
1288 .path()
1289 .join(std::ffi::OsString::from_vec(b"admin-\xff".to_vec()));
1290 let invalid_root = temp
1291 .path()
1292 .join(std::ffi::OsString::from_vec(b"root-\xff".to_vec()));
1293 let invalid_administrative_two = temp
1294 .path()
1295 .join(std::ffi::OsString::from_vec(b"admin-\xfe".to_vec()));
1296 fs::create_dir_all(&control)?;
1297 for path in [
1298 &common,
1299 &administrative,
1300 &root,
1301 &invalid_common,
1302 &invalid_administrative,
1303 &invalid_root,
1304 &invalid_administrative_two,
1305 ] {
1306 fs::create_dir_all(path)?;
1307 }
1308 let store = AtlasStore::open_for_project(&control.join("projectatlas.db"), &control)?;
1309 let second_root = temp.path().join("second");
1312 let third_administrative = common.join("worktrees/third");
1313 fs::create_dir_all(&second_root)?;
1314 fs::create_dir_all(&third_administrative)?;
1315 let mut rows = Vec::new();
1316 for (alias, common_path, administrative_path, root_path, identity) in [
1317 (
1318 "nonutf-common",
1319 invalid_common.as_path(),
1320 administrative.as_path(),
1321 root.as_path(),
1322 1,
1323 ),
1324 (
1325 "nonutf-admin",
1326 common.as_path(),
1327 invalid_administrative.as_path(),
1328 second_root.as_path(),
1329 2,
1330 ),
1331 (
1332 "nonutf-root",
1333 common.as_path(),
1334 third_administrative.as_path(),
1335 invalid_root.as_path(),
1336 3,
1337 ),
1338 ] {
1339 rows.push(store.register_worktree(
1340 &WorktreeAlias::parse(alias)?,
1341 common_path,
1342 administrative_path,
1343 &administrative_identity(identity),
1344 root_path,
1345 None,
1346 1,
1347 )?);
1348 }
1349 for row in &rows {
1350 require(
1351 row.git_common_directory_identity.encode()?.len() >= 3
1352 && row.git_administrative_directory_identity.encode()?.len() >= 3
1353 && row.last_root_identity.encode()?.len() >= 3,
1354 "native identity codec bytes were not persisted",
1355 )?;
1356 require(
1357 row.git_common_directory.contains("native-path-unavailable")
1358 || row
1359 .git_administrative_directory
1360 .contains("native-path-unavailable")
1361 || row.last_root.contains("native-path-unavailable"),
1362 "non-UTF-8 display projection was not typed as unavailable",
1363 )?;
1364 }
1365 require(
1366 matches!(
1367 store.register_worktree(
1368 &WorktreeAlias::parse("duplicate-native-admin")?,
1369 &common,
1370 &invalid_administrative,
1371 &administrative_identity(4),
1372 &temp.path().join("duplicate-root"),
1373 None,
1374 1,
1375 ),
1376 Err(DbError::WorktreeRegistrationConflict { .. })
1377 ),
1378 "duplicate native administrative identity was accepted",
1379 )?;
1380 fs::create_dir_all(temp.path().join("distinct-root"))?;
1381 store.register_worktree(
1382 &WorktreeAlias::parse("distinct-native-admin")?,
1383 &common,
1384 &invalid_administrative_two,
1385 &administrative_identity(5),
1386 &temp.path().join("distinct-root"),
1387 None,
1388 1,
1389 )?;
1390 let reopened = AtlasStore::open_for_project(&control.join("projectatlas.db"), &control)?;
1391 require_eq(
1392 &reopened.worktree_registrations(false)?.len(),
1393 &4,
1394 "native rows",
1395 )?;
1396 require(
1397 reopened
1398 .worktree_registrations(false)?
1399 .iter()
1400 .any(|row| row.last_root_identity.display_string().is_err()),
1401 "native non-UTF-8 root did not remain lossless after reopen",
1402 )
1403 }
1404
1405 #[test]
1406 fn registry_reuses_matching_history_and_keeps_retired_alias_history()
1407 -> Result<(), Box<dyn Error>> {
1408 let temp = tempfile::tempdir()?;
1409 let control = temp.path().join("control");
1410 let common = temp.path().join("common.git");
1411 let first_admin = common.join("worktrees/first");
1412 let second_admin = common.join("worktrees/second");
1413 let first_root = temp.path().join("first");
1414 let second_root = temp.path().join("second");
1415 for path in [
1416 &control,
1417 &first_admin,
1418 &second_admin,
1419 &first_root,
1420 &second_root,
1421 ] {
1422 fs::create_dir_all(path)?;
1423 }
1424 let database = control.join("projectatlas.db");
1425 let store = AtlasStore::open_for_project(&database, &control)?;
1426 let alias = WorktreeAlias::parse("issue-430")?;
1427 let first = store.register_worktree(
1428 &alias,
1429 &common,
1430 &first_admin,
1431 &administrative_identity(1),
1432 &first_root,
1433 Some(identity(1)?),
1434 10,
1435 )?;
1436 let idempotent = store.register_worktree(
1437 &alias,
1438 &common,
1439 &first_admin,
1440 &administrative_identity(1),
1441 &first_root,
1442 Some(identity(1)?),
1443 11,
1444 )?;
1445 require_eq(
1446 &first.registration_id,
1447 &idempotent.registration_id,
1448 "idempotent registration identity",
1449 )?;
1450 require(
1451 matches!(
1452 store.register_worktree(
1453 &alias,
1454 &common,
1455 &second_admin,
1456 &administrative_identity(2),
1457 &second_root,
1458 Some(identity(2)?),
1459 12,
1460 ),
1461 Err(DbError::WorktreeRegistrationConflict { field: "alias", .. })
1462 ),
1463 "active alias conflict was not rejected",
1464 )?;
1465
1466 let retired = store.retire_worktree(first.registration_id, &alias, 20)?;
1467 require_eq(
1468 &retired.state,
1469 &WorktreeRegistrationState::Retired,
1470 "retired state",
1471 )?;
1472 require(
1473 matches!(
1474 store.refresh_worktree_root(&first, &second_root),
1475 Err(DbError::WorktreeRegistrationNotFound { .. })
1476 ),
1477 "stale root refresh reactivated a retired registration",
1478 )?;
1479 require_eq(
1480 &store
1481 .worktree_registrations(true)?
1482 .into_iter()
1483 .find(|registration| registration.registration_id == first.registration_id)
1484 .ok_or_else(|| io::Error::other("retired registration history is missing"))?
1485 .state,
1486 &WorktreeRegistrationState::Retired,
1487 "state after stale root refresh",
1488 )?;
1489 let replacement = store.register_worktree(
1490 &alias,
1491 &common,
1492 &first_admin,
1493 &administrative_identity(2),
1494 &second_root,
1495 None,
1496 21,
1497 )?;
1498 require(
1499 replacement.registration_id != first.registration_id,
1500 "replacement reused unrelated retired history",
1501 )?;
1502 require(
1503 matches!(
1504 store.bind_worktree_project(
1505 first.registration_id,
1506 &alias,
1507 &first_root,
1508 identity(1)?
1509 ),
1510 Err(DbError::WorktreeRegistrationNotFound { .. })
1511 ),
1512 "stale bind targeted a replacement registration after alias reuse",
1513 )?;
1514 require(
1515 matches!(
1516 store.retire_worktree(first.registration_id, &alias, 22),
1517 Err(DbError::WorktreeRegistrationNotFound { .. })
1518 ),
1519 "stale retirement targeted a replacement registration after alias reuse",
1520 )?;
1521 let replacement = store.worktree_registration(&alias)?;
1522 require(
1523 replacement.project_instance_id.is_none()
1524 && replacement.last_root
1525 == native_path_projection(&worktree_identity("root", &second_root)?)?,
1526 "stale bind changed the replacement registration",
1527 )?;
1528 let all = store.worktree_registrations(true)?;
1529 require_eq(&all.len(), &2, "retained registration count")?;
1530 require_eq(
1531 &all.first()
1532 .ok_or_else(|| io::Error::other("active registration is missing"))?
1533 .state,
1534 &WorktreeRegistrationState::Active,
1535 "active row ordering",
1536 )?;
1537 require_eq(
1538 &all.get(1)
1539 .ok_or_else(|| io::Error::other("retired registration is missing"))?
1540 .state,
1541 &WorktreeRegistrationState::Retired,
1542 "retired row ordering",
1543 )?;
1544 require(
1545 store
1546 .worktree_registrations(false)?
1547 .iter()
1548 .all(|row| row.state == WorktreeRegistrationState::Active),
1549 "active-only list returned retired history",
1550 )?;
1551 Ok(())
1552 }
1553
1554 #[test]
1555 fn active_registration_guard_rolls_back_nested_binding_and_rechecks_unbound_state()
1556 -> Result<(), Box<dyn Error>> {
1557 let temp = tempfile::tempdir()?;
1558 let control_root = temp.path().join("control");
1559 let common = temp.path().join("common.git");
1560 let admin = common.join("worktrees/guarded");
1561 let root = temp.path().join("guarded");
1562 for path in [&control_root, &admin, &root] {
1563 fs::create_dir_all(path)?;
1564 }
1565 let store =
1566 AtlasStore::open_for_project(&control_root.join("projectatlas.db"), &control_root)?;
1567 let alias = WorktreeAlias::parse("guarded")?;
1568 let project = identity(1)?;
1569 let registration = store.register_worktree(
1570 &alias,
1571 &common,
1572 &admin,
1573 &administrative_identity(1),
1574 &root,
1575 None,
1576 1,
1577 )?;
1578
1579 let rejected = store.with_active_worktree_registration(
1580 registration.registration_id,
1581 &alias,
1582 |guard| {
1583 guard.bind_project(&root, project)?;
1584 Err::<(), _>(DbError::WorktreeRegistrationConflict {
1585 field: "test_operation",
1586 value: "rollback".to_string(),
1587 })
1588 },
1589 );
1590 require(
1591 matches!(
1592 rejected,
1593 Err(DbError::WorktreeRegistrationConflict {
1594 field: "test_operation",
1595 ..
1596 })
1597 ),
1598 "guarded callback failure was not returned",
1599 )?;
1600 require(
1601 store
1602 .worktree_registration(&alias)?
1603 .project_instance_id
1604 .is_none(),
1605 "failed guarded callback committed its nested binding",
1606 )?;
1607
1608 store.with_active_worktree_registration(registration.registration_id, &alias, |guard| {
1609 guard.bind_project(&root, project).map(|_| ())
1610 })?;
1611 let reset_callback_ran = std::cell::Cell::new(false);
1612 require(
1613 matches!(
1614 store.with_unbound_worktree_registration(
1615 registration.registration_id,
1616 &alias,
1617 |_registration| {
1618 reset_callback_ran.set(true);
1619 Ok::<(), io::Error>(())
1620 }
1621 ),
1622 Err(DbError::WorktreeRegistrationConflict {
1623 field: "project_instance_id",
1624 ..
1625 })
1626 ) && !reset_callback_ran.get(),
1627 "bound registration entered the guarded reset callback",
1628 )
1629 }
1630
1631 #[test]
1632 fn active_registration_guard_serializes_bind_and_reset_across_connections()
1633 -> Result<(), Box<dyn Error>> {
1634 let temp = tempfile::tempdir()?;
1635 let control_root = temp.path().join("control");
1636 let common = temp.path().join("common.git");
1637 let first_root = temp.path().join("first");
1638 let second_root = temp.path().join("second");
1639 for path in [&control_root, &first_root, &second_root] {
1640 fs::create_dir_all(path)?;
1641 }
1642 let database = control_root.join("projectatlas.db");
1643 let store = AtlasStore::open_for_project(&database, &control_root)?;
1644 let contender = AtlasStore::open_for_project(&database, &control_root)?;
1645 contender.connection.busy_timeout(Duration::ZERO)?;
1646 let bind_wins = WorktreeAlias::parse("bind-wins")?;
1647 let bind_registration = store.register_worktree(
1648 &bind_wins,
1649 &common,
1650 &common.join("worktrees/bind-wins"),
1651 &administrative_identity(1),
1652 &first_root,
1653 None,
1654 1,
1655 )?;
1656 let bind_project = identity(1)?;
1657 store.with_active_worktree_registration(
1658 bind_registration.registration_id,
1659 &bind_wins,
1660 |guard| {
1661 let blocked = contender.with_unbound_worktree_registration(
1662 bind_registration.registration_id,
1663 &bind_wins,
1664 |_registration| Ok::<(), io::Error>(()),
1665 );
1666 if !blocked.as_ref().is_err_and(sqlite_writer_busy) {
1667 return Err(DbError::WorktreeRegistrationRow {
1668 reason: "reset contender did not reach the held SQLite writer lock",
1669 });
1670 }
1671 guard.bind_project(&first_root, bind_project).map(|_| ())
1672 },
1673 )?;
1674 let reset_callback_ran = std::cell::Cell::new(false);
1675 require(
1676 matches!(
1677 contender.with_unbound_worktree_registration(
1678 bind_registration.registration_id,
1679 &bind_wins,
1680 |_registration| {
1681 reset_callback_ran.set(true);
1682 Ok::<(), io::Error>(())
1683 },
1684 ),
1685 Err(DbError::WorktreeRegistrationConflict {
1686 field: "project_instance_id",
1687 ..
1688 })
1689 ) && !reset_callback_ran.get(),
1690 "reset did not reload the binding committed by the winning writer",
1691 )?;
1692
1693 let reset_wins = WorktreeAlias::parse("reset-wins")?;
1694 let reset_registration = store.register_worktree(
1695 &reset_wins,
1696 &common,
1697 &common.join("worktrees/reset-wins"),
1698 &administrative_identity(2),
1699 &second_root,
1700 None,
1701 2,
1702 )?;
1703 let reset_project = identity(2)?;
1704 let target_database = second_root.join("projectatlas.db");
1705 fs::write(&target_database, b"captured atlas")?;
1706 store.with_unbound_worktree_registration(
1707 reset_registration.registration_id,
1708 &reset_wins,
1709 |_registration| {
1710 let blocked = contender.with_active_worktree_registration(
1711 reset_registration.registration_id,
1712 &reset_wins,
1713 |guard| guard.bind_project(&second_root, reset_project).map(|_| ()),
1714 );
1715 if !blocked.as_ref().is_err_and(sqlite_writer_busy) {
1716 return Err(io::Error::other(
1717 "bind contender did not reach the held SQLite writer lock",
1718 ));
1719 }
1720 fs::remove_file(&target_database)
1721 },
1722 )??;
1723 let late_bind_result = contender.with_active_worktree_registration(
1724 reset_registration.registration_id,
1725 &reset_wins,
1726 |guard| {
1727 if !target_database.is_file() {
1728 return Ok(Err("target atlas is missing"));
1729 }
1730 guard.bind_project(&second_root, reset_project)?;
1731 Ok(Ok(()))
1732 },
1733 )?;
1734 require(
1735 matches!(late_bind_result, Err("target atlas is missing")),
1736 "late bind did not recheck the reset target after writer exclusion",
1737 )?;
1738 require(
1739 store
1740 .worktree_registration(&reset_wins)?
1741 .project_instance_id
1742 .is_none()
1743 && !target_database.exists(),
1744 "reset-wins interleaving bound or recreated the deleted target atlas",
1745 )
1746 }
1747
1748 #[test]
1749 fn failed_final_sync_rolls_back_project_binding_and_retirement() -> Result<(), Box<dyn Error>> {
1750 let temp = tempfile::tempdir()?;
1751 let control_root = temp.path().join("control");
1752 let common = temp.path().join("common.git");
1753 let admin = common.join("worktrees/issue-430");
1754 let original_root = temp.path().join("original");
1755 let moved_root = temp.path().join("moved");
1756 let other_root = temp.path().join("other");
1757 for path in [
1758 &control_root,
1759 &admin,
1760 &original_root,
1761 &moved_root,
1762 &other_root,
1763 ] {
1764 fs::create_dir_all(path)?;
1765 }
1766
1767 let control =
1768 AtlasStore::open_for_project(&control_root.join("projectatlas.db"), &control_root)?;
1769 let other = AtlasStore::open_for_project(&other_root.join("projectatlas.db"), &other_root)?;
1770 let mismatched_snapshot = other.export_worktree_usage_snapshot()?;
1771 let target_project = if mismatched_snapshot.project_instance_id() == identity(7)? {
1772 identity(8)?
1773 } else {
1774 identity(7)?
1775 };
1776 let alias = WorktreeAlias::parse("issue-430")?;
1777 let before = control.register_worktree(
1778 &alias,
1779 &common,
1780 &admin,
1781 &administrative_identity(1),
1782 &original_root,
1783 None,
1784 10,
1785 )?;
1786
1787 require(
1788 matches!(
1789 control.retire_worktree_with_usage_snapshot(
1790 before.registration_id,
1791 &alias,
1792 &moved_root,
1793 target_project,
1794 &mismatched_snapshot,
1795 20,
1796 ),
1797 Err(DbError::WorktreeTelemetryProjectMismatch { .. })
1798 ),
1799 "mismatched final snapshot was not rejected",
1800 )?;
1801 require_eq(
1802 &control.worktree_registration(&alias)?,
1803 &before,
1804 "active registration after failed final synchronization",
1805 )?;
1806 Ok(())
1807 }
1808
1809 #[test]
1810 fn registration_and_initial_usage_snapshot_commit_atomically() -> Result<(), Box<dyn Error>> {
1811 let temp = tempfile::tempdir()?;
1812 let control_root = temp.path().join("control");
1813 let local_root = temp.path().join("local");
1814 let common = temp.path().join("common.git");
1815 let admin = common.join("worktrees/local");
1816 for path in [&control_root, &local_root, &admin] {
1817 fs::create_dir_all(path)?;
1818 }
1819 let control =
1820 AtlasStore::open_for_project(&control_root.join("projectatlas.db"), &control_root)?;
1821 let local = AtlasStore::open_for_project(&local_root.join("projectatlas.db"), &local_root)?;
1822 local.record_usage(&projectatlas_core::telemetry::usage_from_estimates(
1823 "atomic-registration",
1824 "atlas_overview",
1825 None,
1826 None,
1827 100,
1828 20,
1829 ))?;
1830 let snapshot = local.export_worktree_usage_snapshot()?;
1831 let project = snapshot.project_instance_id();
1832 let mismatched_project = if project == identity(7)? {
1833 identity(8)?
1834 } else {
1835 identity(7)?
1836 };
1837 let alias = WorktreeAlias::parse("local")?;
1838
1839 require(
1840 matches!(
1841 control.register_worktree_with_usage_snapshot(
1842 &alias,
1843 &common,
1844 &admin,
1845 &administrative_identity(1),
1846 &local_root,
1847 mismatched_project,
1848 &snapshot,
1849 1,
1850 ),
1851 Err(DbError::WorktreeTelemetryProjectMismatch { .. })
1852 ),
1853 "failed initial snapshot synchronization made a registration visible",
1854 )?;
1855 require(
1856 matches!(
1857 control.worktree_registration(&alias),
1858 Err(DbError::WorktreeRegistrationNotFound { .. })
1859 ) && control.repository_token_overview()?.calls == 0,
1860 "failed initial snapshot synchronization changed registration or aggregate state",
1861 )?;
1862
1863 let (registration, synchronization) = control.register_worktree_with_usage_snapshot(
1864 &alias,
1865 &common,
1866 &admin,
1867 &administrative_identity(1),
1868 &local_root,
1869 project,
1870 &snapshot,
1871 1,
1872 )?;
1873 require(
1874 registration.project_instance_id == Some(project)
1875 && registration.accepted_telemetry_revision == snapshot.revision()
1876 && synchronization == WorktreeUsageSyncState::Synchronized
1877 && control.repository_token_overview()?.calls == 1,
1878 "successful initial registration exposed incomplete aggregate state",
1879 )
1880 }
1881
1882 #[test]
1883 fn deferred_binding_and_initial_usage_snapshot_commit_atomically() -> Result<(), Box<dyn Error>>
1884 {
1885 let temp = tempfile::tempdir()?;
1886 let control_root = temp.path().join("control");
1887 let local_root = temp.path().join("local");
1888 let common = temp.path().join("common.git");
1889 let admin = common.join("worktrees/local");
1890 for path in [&control_root, &local_root, &admin] {
1891 fs::create_dir_all(path)?;
1892 }
1893 let control =
1894 AtlasStore::open_for_project(&control_root.join("projectatlas.db"), &control_root)?;
1895 let local = AtlasStore::open_for_project(&local_root.join("projectatlas.db"), &local_root)?;
1896 local.record_usage(&projectatlas_core::telemetry::usage_from_estimates(
1897 "deferred-binding",
1898 "atlas_overview",
1899 None,
1900 None,
1901 100,
1902 20,
1903 ))?;
1904 let snapshot = local.export_worktree_usage_snapshot()?;
1905 let project = snapshot.project_instance_id();
1906 let mismatched_project = if project == identity(7)? {
1907 identity(8)?
1908 } else {
1909 identity(7)?
1910 };
1911 let alias = WorktreeAlias::parse("local")?;
1912 let registration = control.register_worktree(
1913 &alias,
1914 &common,
1915 &admin,
1916 &administrative_identity(1),
1917 &local_root,
1918 None,
1919 1,
1920 )?;
1921
1922 let rejected = control.with_active_worktree_registration(
1923 registration.registration_id,
1924 &alias,
1925 |guard| {
1926 guard.bind_project_with_usage_snapshot(&local_root, mismatched_project, &snapshot)
1927 },
1928 );
1929 require(
1930 matches!(
1931 rejected,
1932 Err(DbError::WorktreeTelemetryProjectMismatch { .. })
1933 ) && control
1934 .worktree_registration(&alias)?
1935 .project_instance_id
1936 .is_none()
1937 && control.repository_token_overview()?.calls == 0,
1938 "failed deferred synchronization committed a project binding or aggregate",
1939 )?;
1940
1941 let (bound, synchronization) = control.with_active_worktree_registration(
1942 registration.registration_id,
1943 &alias,
1944 |guard| guard.bind_project_with_usage_snapshot(&local_root, project, &snapshot),
1945 )?;
1946 require(
1947 bound.project_instance_id == Some(project)
1948 && bound.accepted_telemetry_revision == snapshot.revision()
1949 && synchronization == WorktreeUsageSyncState::Synchronized
1950 && control.repository_token_overview()?.calls == 1,
1951 "successful deferred binding exposed incomplete aggregate state",
1952 )
1953 }
1954
1955 #[test]
1956 fn retired_project_identity_cannot_bind_another_registration() -> Result<(), Box<dyn Error>> {
1957 let temp = tempfile::tempdir()?;
1958 let control = temp.path().join("control");
1959 let common = temp.path().join("common.git");
1960 fs::create_dir_all(&control)?;
1961 let store = AtlasStore::open_for_project(&control.join("projectatlas.db"), &control)?;
1962 let original = WorktreeAlias::parse("original")?;
1963 let original_registration = store.register_worktree(
1964 &original,
1965 &common,
1966 &common.join("worktrees/original"),
1967 &administrative_identity(1),
1968 &temp.path().join("original"),
1969 Some(identity(1)?),
1970 1,
1971 )?;
1972 store.retire_worktree(original_registration.registration_id, &original, 2)?;
1973
1974 let unbound = WorktreeAlias::parse("unbound")?;
1975 let unbound_registration = store.register_worktree(
1976 &unbound,
1977 &common,
1978 &common.join("worktrees/unbound"),
1979 &administrative_identity(2),
1980 &temp.path().join("unbound"),
1981 None,
1982 3,
1983 )?;
1984 for result in [
1985 store
1986 .bind_worktree_project(
1987 unbound_registration.registration_id,
1988 &unbound,
1989 &temp.path().join("unbound"),
1990 identity(1)?,
1991 )
1992 .map(|_| ()),
1993 store
1994 .register_worktree(
1995 &unbound,
1996 &common,
1997 &common.join("worktrees/unbound"),
1998 &administrative_identity(2),
1999 &temp.path().join("unbound"),
2000 Some(identity(1)?),
2001 4,
2002 )
2003 .map(|_| ()),
2004 store
2005 .register_worktree(
2006 &WorktreeAlias::parse("direct")?,
2007 &common,
2008 &common.join("worktrees/direct"),
2009 &administrative_identity(3),
2010 &temp.path().join("direct"),
2011 Some(identity(1)?),
2012 4,
2013 )
2014 .map(|_| ()),
2015 ] {
2016 require(
2017 matches!(
2018 result,
2019 Err(DbError::WorktreeRegistrationConflict {
2020 field: "project_instance_id",
2021 ..
2022 })
2023 ),
2024 "retired project identity was rebound to another registration",
2025 )?;
2026 }
2027 require(
2028 store
2029 .worktree_registration(&unbound)?
2030 .project_instance_id
2031 .is_none(),
2032 "failed retired-identity binding changed the active registration",
2033 )
2034 }
2035
2036 #[test]
2037 fn registry_capacity_rejects_new_history_without_changing_existing_rows()
2038 -> Result<(), Box<dyn Error>> {
2039 let temp = tempfile::tempdir()?;
2040 let control = temp.path().join("control");
2041 fs::create_dir_all(&control)?;
2042 let store = AtlasStore::open_for_project(&control.join("projectatlas.db"), &control)?;
2043 store.connection.execute_batch(
2044 "WITH RECURSIVE registrations(value) AS (
2045 SELECT 0
2046 UNION ALL
2047 SELECT value + 1 FROM registrations WHERE value + 1 < 1024
2048 )
2049 INSERT INTO worktree_registrations(
2050 alias, state, git_common_directory, git_administrative_directory,
2051 git_administrative_identity, last_root, created_at_epoch, retired_at_epoch
2052 )
2053 SELECT printf('retired-%04d', value), 'retired', '/common',
2054 printf('/common/worktrees/%04d', value),
2055 printf('%064x', value + 1), printf('/worktrees/%04d', value), 0, 0
2056 FROM registrations;",
2057 )?;
2058 let alias = WorktreeAlias::parse("overflow")?;
2059 require(
2060 matches!(
2061 store.register_worktree(
2062 &alias,
2063 &temp.path().join("common"),
2064 &temp.path().join("common/worktrees/overflow"),
2065 &administrative_identity(3),
2066 &temp.path().join("overflow"),
2067 None,
2068 1,
2069 ),
2070 Err(DbError::WorktreeRegistrationCapacity {
2071 limit: MAX_GIT_WORKTREE_REGISTRATIONS
2072 })
2073 ),
2074 "registration capacity did not reject new history",
2075 )?;
2076 require_eq(
2077 &store.worktree_registrations(true)?.len(),
2078 &MAX_GIT_WORKTREE_REGISTRATIONS,
2079 "catalog after capacity rejection",
2080 )?;
2081 Ok(())
2082 }
2083
2084 #[test]
2085 fn schema_twentytwo_worktree_identity_migration_backfills_and_retries_atomically()
2086 -> Result<(), Box<dyn Error>> {
2087 let temp = tempfile::tempdir()?;
2088 let base = temp.path().canonicalize()?;
2089 let control = base.join("control");
2090 let common = base.join("common.git");
2091 let administrative = common.join("worktrees/legacy");
2092 let root = base.join("legacy");
2093 for path in [&control, &common, &administrative, &root] {
2094 fs::create_dir_all(path)?;
2095 }
2096 let database = control.join("projectatlas.db");
2097 let store = AtlasStore::open_for_project(&database, &control)?;
2098 let common_display = projectatlas_core::normalize_native_path_display(&common);
2099 let administrative_display =
2100 projectatlas_core::normalize_native_path_display(&administrative);
2101 let root_display = projectatlas_core::normalize_native_path_display(&root);
2102 store.connection.execute(
2103 "INSERT INTO worktree_registrations(
2104 alias, state, git_common_directory, git_administrative_directory,
2105 git_administrative_identity, last_root, created_at_epoch
2106 ) VALUES('legacy', 'active', ?1, ?2, ?3, ?4, 1)",
2107 params![
2108 common_display,
2109 administrative_display,
2110 administrative_identity(9),
2111 root_display,
2112 ],
2113 )?;
2114 crate::schema::drop_worktree_native_identity_schema(&store.connection)?;
2115 store.connection.execute(
2116 "UPDATE metadata SET value = '22' WHERE key = 'schema_version'",
2117 [],
2118 )?;
2119 drop(store);
2120
2121 let migrated = AtlasStore::open_for_project(&database, &control)?;
2122 let registration = migrated.worktree_registration(&WorktreeAlias::parse("legacy")?)?;
2123 require_eq(
2124 ®istration.git_common_directory_identity,
2125 &CanonicalProjectRoot::from_path(&common)?,
2126 "migrated common identity",
2127 )?;
2128 require_eq(
2129 ®istration.git_administrative_directory_identity,
2130 &CanonicalProjectRoot::from_path(&administrative)?,
2131 "migrated administrative identity",
2132 )?;
2133 require_eq(
2134 ®istration.last_root_identity,
2135 &CanonicalProjectRoot::from_path(&root)?,
2136 "migrated root identity",
2137 )?;
2138 drop(migrated);
2139
2140 let failed_database = control.join("failed-projectatlas.db");
2141 let failed = AtlasStore::open_for_project(&failed_database, &control)?;
2142 failed.connection.execute(
2143 "INSERT INTO worktree_registrations(
2144 alias, state, git_common_directory, git_administrative_directory,
2145 git_administrative_identity, last_root, created_at_epoch
2146 ) VALUES('legacy', 'active', ?1, ?2, ?3, ?4, 1)",
2147 params![
2148 common_display,
2149 administrative_display,
2150 administrative_identity(10),
2151 root_display,
2152 ],
2153 )?;
2154 crate::schema::drop_worktree_native_identity_schema(&failed.connection)?;
2155 failed.connection.execute_batch(
2156 "UPDATE metadata SET value = '22' WHERE key = 'schema_version';
2157 UPDATE worktree_registrations SET last_root = 'relative';",
2158 )?;
2159 let failed_database_path = failed_database;
2160 drop(failed);
2161 let migration_result = AtlasStore::open_for_project(&failed_database_path, &control);
2162 require(
2163 matches!(&migration_result, Err(DbError::ProjectRootIdentity(_))),
2164 &format!(
2165 "injected native identity migration failure was not returned: {:?}",
2166 migration_result.as_ref().err().map(ToString::to_string)
2167 ),
2168 )?;
2169 let inspect = Connection::open(&failed_database_path)?;
2170 let marker = inspect.query_row(
2171 "SELECT value FROM metadata WHERE key = 'schema_version'",
2172 [],
2173 |row| row.get::<_, String>(0),
2174 )?;
2175 require_eq(&marker, &"22".to_string(), "failed migration marker")?;
2176 require_eq(
2177 &inspect.query_row(
2178 "SELECT COUNT(*) FROM pragma_table_info('worktree_registrations')
2179 WHERE name = 'last_root_identity'",
2180 [],
2181 |row| row.get::<_, i64>(0),
2182 )?,
2183 &0,
2184 "failed migration native columns",
2185 )?;
2186 inspect.execute(
2187 "UPDATE worktree_registrations SET last_root = ?1",
2188 [root_display.as_str()],
2189 )?;
2190 let retried = AtlasStore::open_for_project(&failed_database_path, &control)?;
2191 require_eq(
2192 &retried.worktree_registrations(false)?.len(),
2193 &1,
2194 "retried migration registration",
2195 )?;
2196 Ok(())
2197 }
2198
2199 #[test]
2200 fn schema_twentytwo_retired_history_survives_path_reuse_as_files() -> Result<(), Box<dyn Error>>
2201 {
2202 let temp = tempfile::tempdir()?;
2203 let base = CanonicalProjectRoot::from_path(temp.path())?.into_path();
2204 let control = base.join("control");
2205 let common = base.join("common.git");
2206 let administrative = common.join("worktrees/legacy");
2207 let root = base.join("legacy");
2208 for path in [&control, &common, &administrative, &root] {
2209 fs::create_dir_all(path)?;
2210 }
2211 for field in [
2212 "git_common_directory",
2213 "git_administrative_directory",
2214 "last_root",
2215 ] {
2216 let reused = base.join(field);
2217 fs::create_dir(&reused)?;
2218 let historical = CanonicalProjectRoot::from_path(&reused)?;
2219 fs::remove_dir(&reused)?;
2220 fs::write(&reused, "unrelated replacement file")?;
2221 let paths = [
2222 if field == "git_common_directory" {
2223 &reused
2224 } else {
2225 &common
2226 },
2227 if field == "git_administrative_directory" {
2228 &reused
2229 } else {
2230 &administrative
2231 },
2232 if field == "last_root" { &reused } else { &root },
2233 ];
2234 let database = control.join(format!("reused-{field}.db"));
2235 let store = AtlasStore::open_for_project(&database, &control)?;
2236 store.connection.execute(
2237 "INSERT INTO worktree_registrations(
2238 alias, state, git_common_directory, git_administrative_directory,
2239 git_administrative_identity, last_root, created_at_epoch
2240 ) VALUES('legacy', 'active', ?1, ?2, ?3, ?4, 1)",
2241 params![
2242 projectatlas_core::normalize_native_path_display(paths[0]),
2243 projectatlas_core::normalize_native_path_display(paths[1]),
2244 administrative_identity(21),
2245 projectatlas_core::normalize_native_path_display(paths[2]),
2246 ],
2247 )?;
2248 crate::schema::drop_worktree_native_identity_schema(&store.connection)?;
2249 store.connection.execute(
2250 "UPDATE metadata SET value = '22' WHERE key = 'schema_version'",
2251 [],
2252 )?;
2253 drop(store);
2254 require(
2255 matches!(
2256 AtlasStore::open_for_project(&database, &control),
2257 Err(DbError::ProjectRootIdentity(_))
2258 ),
2259 "active registration admitted a replacement file",
2260 )?;
2261 let inspect = Connection::open(&database)?;
2262 require_eq(
2263 &inspect.query_row(
2264 "SELECT value FROM metadata WHERE key = 'schema_version'",
2265 [],
2266 |row| row.get::<_, String>(0),
2267 )?,
2268 &"22".to_string(),
2269 "active path failure rolled back migration",
2270 )?;
2271 inspect.execute_batch(
2272 "UPDATE worktree_registrations SET state = 'retired', retired_at_epoch = 2;",
2273 )?;
2274 drop(inspect);
2275 let migrated = AtlasStore::open_for_project(&database, &control)?;
2276 let rows = migrated.worktree_registrations(true)?;
2277 require_eq(&rows.len(), &1, "retained historical row")?;
2278 let row = &rows[0];
2279 let recovered = match field {
2280 "git_common_directory" => &row.git_common_directory_identity,
2281 "git_administrative_directory" => &row.git_administrative_directory_identity,
2282 _ => &row.last_root_identity,
2283 };
2284 require_eq(
2285 recovered,
2286 &historical,
2287 "historical native identity after path reuse",
2288 )?;
2289 require_eq(
2290 &row.state,
2291 &WorktreeRegistrationState::Retired,
2292 "retained retirement state",
2293 )?;
2294 require_eq(
2295 &fs::read_to_string(&reused)?,
2296 &"unrelated replacement file".to_string(),
2297 "replacement file untouched",
2298 )?;
2299 drop(migrated);
2300 let reopened = AtlasStore::open_for_project(&database, &control)?;
2301 require_eq(
2302 &reopened.worktree_registrations(true)?,
2303 &rows,
2304 "retired migration survives reopen",
2305 )?;
2306 }
2307 Ok(())
2308 }
2309
2310 #[cfg(any(unix, windows))]
2311 #[test]
2312 fn schema_twentytwo_history_does_not_follow_replacement_directory_links()
2313 -> Result<(), Box<dyn Error>> {
2314 let temp = tempfile::tempdir()?;
2315 let base = temp.path().canonicalize()?;
2316 let target = base.join("unrelated");
2317 fs::create_dir(&target)?;
2318 fs::write(target.join("sentinel"), "unrelated contents")?;
2319 for replaced_field in 0..3 {
2320 let fixture = base.join(format!("field-{replaced_field}"));
2321 fs::create_dir(&fixture)?;
2322 let paths = [
2323 fixture.join("common"),
2324 fixture.join("admin"),
2325 fixture.join("root"),
2326 ];
2327 for path in &paths {
2328 fs::create_dir(path)?;
2329 }
2330 let expected = paths
2331 .iter()
2332 .map(|path| CanonicalProjectRoot::from_path(path))
2333 .collect::<Result<Vec<_>, _>>()?;
2334 fs::rename(&paths[replaced_field], fixture.join("moved"))?;
2335 #[cfg(unix)]
2336 std::os::unix::fs::symlink(&target, &paths[replaced_field])?;
2337 #[cfg(windows)]
2338 {
2339 let output = std::process::Command::new("cmd")
2340 .args(["/D", "/C", "mklink", "/J"])
2341 .arg(&paths[replaced_field])
2342 .arg(&target)
2343 .output()?;
2344 require(
2345 output.status.success(),
2346 &format!("junction fixture failed: {output:?}"),
2347 )?;
2348 }
2349 for state in ["active", "retired"] {
2350 let database = fixture.join(format!("{state}.db"));
2351 let store = AtlasStore::open_for_project(&database, &fixture)?;
2352 crate::schema::drop_worktree_native_identity_schema(&store.connection)?;
2353 store.connection.execute(
2354 "INSERT INTO worktree_registrations(
2355 alias, state, git_common_directory, git_administrative_directory,
2356 git_administrative_identity, last_root, created_at_epoch, retired_at_epoch
2357 ) VALUES('legacy', ?1, ?2, ?3, ?4, ?5, 1, ?6)",
2358 params![
2359 state,
2360 projectatlas_core::normalize_native_path_display(&paths[0]),
2361 projectatlas_core::normalize_native_path_display(&paths[1]),
2362 administrative_identity(22),
2363 projectatlas_core::normalize_native_path_display(&paths[2]),
2364 (state == "retired").then_some(2),
2365 ],
2366 )?;
2367 if replaced_field == 2 {
2368 let other_admin = fixture.join("other-admin");
2369 fs::create_dir_all(&other_admin)?;
2370 store.connection.execute(
2371 "INSERT INTO worktree_registrations(
2372 alias, state, git_common_directory, git_administrative_directory,
2373 git_administrative_identity, last_root, created_at_epoch
2374 ) VALUES('other', 'active', ?1, ?2, ?3, ?4, 1)",
2375 params![
2376 projectatlas_core::normalize_native_path_display(&paths[0]),
2377 projectatlas_core::normalize_native_path_display(&other_admin),
2378 administrative_identity(23),
2379 projectatlas_core::normalize_native_path_display(&target),
2380 ],
2381 )?;
2382 }
2383 store.connection.execute(
2384 "UPDATE metadata SET value = '22' WHERE key = 'schema_version'",
2385 [],
2386 )?;
2387 drop(store);
2388 let migrated = AtlasStore::open_for_project(&database, &fixture)?;
2389 let rows = migrated.worktree_registrations(true)?;
2390 require_eq(
2391 &rows.len(),
2392 &(1 + usize::from(replaced_field == 2)),
2393 "historical registrations preserved without a false collision",
2394 )?;
2395 let alias = WorktreeAlias::parse("legacy")?;
2396 let row = rows
2397 .iter()
2398 .find(|row| row.alias == alias)
2399 .ok_or_else(|| io::Error::other("legacy registration missing"))?;
2400 for (actual, expected) in [
2401 &row.git_common_directory_identity,
2402 &row.git_administrative_directory_identity,
2403 &row.last_root_identity,
2404 ]
2405 .into_iter()
2406 .zip(&expected)
2407 {
2408 require_eq(
2409 actual,
2410 expected,
2411 "migration must not adopt a replacement target",
2412 )?;
2413 }
2414 drop(migrated);
2415 let reopened = AtlasStore::open_for_project(&database, &fixture)?;
2416 require_eq(
2417 &reopened.worktree_registrations(true)?,
2418 &rows,
2419 "stable migrated history",
2420 )?;
2421 }
2422 }
2423 require_eq(
2424 &fs::read_to_string(target.join("sentinel"))?,
2425 &"unrelated contents".to_string(),
2426 "replacement target untouched",
2427 )?;
2428 Ok(())
2429 }
2430
2431 #[cfg(windows)]
2432 #[test]
2433 fn schema_twentytwo_worktree_identity_migration_rejects_unprovable_verbatim_paths()
2434 -> Result<(), Box<dyn Error>> {
2435 let temp = tempfile::tempdir()?;
2436 let base = temp.path().canonicalize()?;
2437 let control = base.join("control");
2438 let common = base.join("common.git");
2439 let administrative = common.join("worktrees/legacy");
2440 let root = base.join("legacy");
2441 for path in [&control, &common, &administrative, &root] {
2442 fs::create_dir_all(path)?;
2443 }
2444 let common_display = projectatlas_core::normalize_native_path_display(&common);
2445 let administrative_display =
2446 projectatlas_core::normalize_native_path_display(&administrative);
2447 let root_display = projectatlas_core::normalize_native_path_display(&root);
2448 let missing_display =
2449 projectatlas_core::normalize_native_path_display(base.join("a".repeat(240)));
2450 let suffix_units = r"\.projectatlas\projectatlas.db".encode_utf16().count();
2451 let prefix = projectatlas_core::normalize_native_path_display(&base);
2452 let suffix_threshold_display = format!(
2453 "{prefix}/{}",
2454 "b".repeat(260 - suffix_units - prefix.encode_utf16().count() - 1)
2455 );
2456 let ambiguous_common_display = format!("{common_display}.");
2457 let ambiguous_administrative_display = format!("{administrative_display}.");
2458 let ambiguous_root_display = format!("{root_display}.");
2459
2460 for (case, field, rejected_display) in [
2461 ("missing-common", "git_common_directory", &missing_display),
2462 (
2463 "missing-administrative",
2464 "git_administrative_directory",
2465 &missing_display,
2466 ),
2467 ("missing-root", "last_root", &missing_display),
2468 (
2469 "suffix-common",
2470 "git_common_directory",
2471 &suffix_threshold_display,
2472 ),
2473 (
2474 "suffix-administrative",
2475 "git_administrative_directory",
2476 &suffix_threshold_display,
2477 ),
2478 ("suffix-root", "last_root", &suffix_threshold_display),
2479 (
2480 "ambiguous-common",
2481 "git_common_directory",
2482 &ambiguous_common_display,
2483 ),
2484 (
2485 "ambiguous-administrative",
2486 "git_administrative_directory",
2487 &ambiguous_administrative_display,
2488 ),
2489 ("ambiguous-root", "last_root", &ambiguous_root_display),
2490 ] {
2491 let database = control.join(format!("unprovable-{case}.db"));
2492 let store = AtlasStore::open_for_project(&database, &control)?;
2493 let common_value = if field == "git_common_directory" {
2494 rejected_display
2495 } else {
2496 &common_display
2497 };
2498 let administrative_value = if field == "git_administrative_directory" {
2499 rejected_display
2500 } else {
2501 &administrative_display
2502 };
2503 let root_value = if field == "last_root" {
2504 rejected_display
2505 } else {
2506 &root_display
2507 };
2508 store.connection.execute(
2509 "INSERT INTO worktree_registrations(
2510 alias, state, git_common_directory, git_administrative_directory,
2511 git_administrative_identity, last_root, created_at_epoch
2512 ) VALUES('legacy', 'active', ?1, ?2, ?3, ?4, 1)",
2513 params![
2514 common_value,
2515 administrative_value,
2516 administrative_identity(20),
2517 root_value,
2518 ],
2519 )?;
2520 crate::schema::drop_worktree_native_identity_schema(&store.connection)?;
2521 store.connection.execute(
2522 "UPDATE metadata SET value = '22' WHERE key = 'schema_version'",
2523 [],
2524 )?;
2525 drop(store);
2526
2527 let migration_result = AtlasStore::open_for_project(&database, &control);
2528 require(
2529 matches!(
2530 &migration_result,
2531 Err(DbError::WorktreeRegistrationMigrationIdentityUnavailable {
2532 field: failed_field,
2533 registration_id: 1,
2534 }) if failed_field == &field
2535 ),
2536 &format!(
2537 "unprovable {field} migration returned the wrong result: {:?}",
2538 migration_result.as_ref().err().map(ToString::to_string)
2539 ),
2540 )?;
2541
2542 let inspect = Connection::open(&database)?;
2543 let marker = inspect.query_row(
2544 "SELECT value FROM metadata WHERE key = 'schema_version'",
2545 [],
2546 |row| row.get::<_, String>(0),
2547 )?;
2548 require_eq(&marker, &"22".to_string(), "failed migration marker")?;
2549 require_eq(
2550 &inspect.query_row(
2551 "SELECT COUNT(*) FROM pragma_table_info('worktree_registrations')
2552 WHERE name IN (
2553 'git_common_directory_identity',
2554 'git_administrative_directory_identity',
2555 'last_root_identity'
2556 )",
2557 [],
2558 |row| row.get::<_, i64>(0),
2559 )?,
2560 &0,
2561 "failed migration native columns",
2562 )?;
2563 require_eq(
2564 &inspect.query_row(
2565 "SELECT COUNT(*) FROM sqlite_schema
2566 WHERE type = 'index' AND name IN (
2567 'idx_worktree_registrations_active_native_administrative_directory',
2568 'idx_worktree_registrations_active_native_root'
2569 )",
2570 [],
2571 |row| row.get::<_, i64>(0),
2572 )?,
2573 &0,
2574 "failed migration native indexes",
2575 )?;
2576 let legacy_paths = inspect.query_row(
2577 "SELECT git_common_directory, git_administrative_directory, last_root
2578 FROM worktree_registrations WHERE registration_id = 1",
2579 [],
2580 |row| {
2581 Ok((
2582 row.get::<_, String>(0)?,
2583 row.get::<_, String>(1)?,
2584 row.get::<_, String>(2)?,
2585 ))
2586 },
2587 )?;
2588 require(
2589 [&legacy_paths.0, &legacy_paths.1, &legacy_paths.2]
2590 .into_iter()
2591 .any(|path| path == rejected_display),
2592 "failed migration changed the unprovable legacy path",
2593 )?;
2594 let repair = match field {
2595 "git_common_directory" => common_display.as_str(),
2596 "git_administrative_directory" => administrative_display.as_str(),
2597 _ => root_display.as_str(),
2598 };
2599 inspect.execute(
2600 &format!("UPDATE worktree_registrations SET {field} = ?1"),
2601 [repair],
2602 )?;
2603 drop(inspect);
2604
2605 let retried = AtlasStore::open_for_project(&database, &control)?;
2606 let registration = retried.worktree_registration(&WorktreeAlias::parse("legacy")?)?;
2607 require_eq(
2608 ®istration.git_common_directory_identity,
2609 &CanonicalProjectRoot::from_path(&common)?,
2610 "repaired common identity",
2611 )?;
2612 require_eq(
2613 ®istration.git_administrative_directory_identity,
2614 &CanonicalProjectRoot::from_path(&administrative)?,
2615 "repaired administrative identity",
2616 )?;
2617 require_eq(
2618 ®istration.last_root_identity,
2619 &CanonicalProjectRoot::from_path(&root)?,
2620 "repaired root identity",
2621 )?;
2622 }
2623 Ok(())
2624 }
2625
2626 #[test]
2627 fn schema_twentytwo_worktree_identity_migration_rejects_legacy_collisions_atomically()
2628 -> Result<(), Box<dyn Error>> {
2629 let temp = tempfile::tempdir()?;
2630 let control = temp.path().join("control");
2631 let common = temp.path().join("common.git");
2632 let administrative_a = common.join("worktrees/legacy-a");
2633 let administrative_b = common.join("worktrees/legacy-b");
2634 let administrative_distinct = common.join("worktrees/distinct");
2635 let collision_root = temp.path().join("legacy-\u{fffd}-root");
2636 let repaired_root = temp.path().join("repaired-root");
2637 let unaffected_root = temp.path().join("unaffected-root");
2638 for path in [
2639 &control,
2640 &common,
2641 &administrative_a,
2642 &administrative_b,
2643 &administrative_distinct,
2644 &collision_root,
2645 &repaired_root,
2646 &unaffected_root,
2647 ] {
2648 fs::create_dir_all(path)?;
2649 }
2650
2651 let database = control.join("projectatlas.db");
2652 let store = AtlasStore::open_for_project(&database, &control)?;
2653 let common_display = projectatlas_core::normalize_native_path_display(&common);
2654 let collision_display = projectatlas_core::normalize_native_path_display(&collision_root);
2655 let repaired_display = projectatlas_core::normalize_native_path_display(&repaired_root);
2656 let unaffected_display = projectatlas_core::normalize_native_path_display(&unaffected_root);
2657 crate::schema::drop_worktree_native_identity_schema(&store.connection)?;
2658 store.connection.execute(
2659 "UPDATE metadata SET value = '22' WHERE key = 'schema_version'",
2660 [],
2661 )?;
2662 for (alias, administrative, root, identity) in [
2663 (
2664 "legacy-a",
2665 &administrative_a,
2666 &collision_display,
2667 administrative_identity(11),
2668 ),
2669 (
2670 "legacy-b",
2671 &administrative_b,
2672 &collision_display,
2673 administrative_identity(12),
2674 ),
2675 (
2676 "distinct",
2677 &administrative_distinct,
2678 &unaffected_display,
2679 administrative_identity(13),
2680 ),
2681 ] {
2682 let administrative_display =
2683 projectatlas_core::normalize_native_path_display(administrative);
2684 store.connection.execute(
2685 "INSERT INTO worktree_registrations(
2686 alias, state, git_common_directory, git_administrative_directory,
2687 git_administrative_identity, last_root, created_at_epoch
2688 ) VALUES(?1, 'active', ?2, ?3, ?4, ?5, 1)",
2689 params![
2690 alias,
2691 common_display,
2692 administrative_display,
2693 identity,
2694 root,
2695 ],
2696 )?;
2697 }
2698 drop(store);
2699
2700 let migration_result = AtlasStore::open_for_project(&database, &control);
2701 require(
2702 matches!(
2703 &migration_result,
2704 Err(DbError::WorktreeRegistrationMigrationConflict {
2705 field: "last_root_identity",
2706 first_registration_id: 1,
2707 second_registration_id: 2,
2708 })
2709 ),
2710 &format!(
2711 "legacy native identity collision was not rejected deterministically: {:?}",
2712 migration_result.as_ref().err().map(ToString::to_string)
2713 ),
2714 )?;
2715
2716 let inspect = Connection::open(&database)?;
2717 let marker = inspect.query_row(
2718 "SELECT value FROM metadata WHERE key = 'schema_version'",
2719 [],
2720 |row| row.get::<_, String>(0),
2721 )?;
2722 require_eq(&marker, &"22".to_string(), "collision migration marker")?;
2723 require_eq(
2724 &inspect.query_row(
2725 "SELECT COUNT(*) FROM pragma_table_info('worktree_registrations')
2726 WHERE name IN (
2727 'git_common_directory_identity',
2728 'git_administrative_directory_identity',
2729 'last_root_identity'
2730 )",
2731 [],
2732 |row| row.get::<_, i64>(0),
2733 )?,
2734 &0,
2735 "collision migration native columns",
2736 )?;
2737 require_eq(
2738 &inspect.query_row(
2739 "SELECT COUNT(*) FROM sqlite_schema
2740 WHERE type = 'index' AND name IN (
2741 'idx_worktree_registrations_active_native_administrative_directory',
2742 'idx_worktree_registrations_active_native_root'
2743 )",
2744 [],
2745 |row| row.get::<_, i64>(0),
2746 )?,
2747 &0,
2748 "collision migration native indexes",
2749 )?;
2750 require_eq(
2751 &inspect.query_row(
2752 "SELECT COUNT(*) FROM worktree_registrations
2753 WHERE state = 'active'",
2754 [],
2755 |row| row.get::<_, i64>(0),
2756 )?,
2757 &3,
2758 "collision migration registered rows",
2759 )?;
2760 let legacy_rows = inspect
2761 .prepare(
2762 "SELECT alias, last_root FROM worktree_registrations
2763 ORDER BY registration_id",
2764 )?
2765 .query_map([], |row| {
2766 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
2767 })?
2768 .collect::<Result<Vec<_>, _>>()?;
2769 require_eq(
2770 &legacy_rows,
2771 &vec![
2772 ("legacy-a".to_string(), collision_display.clone()),
2773 ("legacy-b".to_string(), collision_display.clone()),
2774 ("distinct".to_string(), unaffected_display.clone()),
2775 ],
2776 "collision migration preserved legacy rows",
2777 )?;
2778 inspect.execute(
2779 "UPDATE worktree_registrations SET last_root = ?1 WHERE alias = 'legacy-b'",
2780 [repaired_display.as_str()],
2781 )?;
2782 drop(inspect);
2783
2784 let retried = AtlasStore::open_for_project(&database, &control)?;
2785 let legacy_a = retried.worktree_registration(&WorktreeAlias::parse("legacy-a")?)?;
2786 let legacy_b = retried.worktree_registration(&WorktreeAlias::parse("legacy-b")?)?;
2787 let distinct = retried.worktree_registration(&WorktreeAlias::parse("distinct")?)?;
2788 require_eq(
2789 &legacy_a.last_root,
2790 &collision_display,
2791 "collision migration first row",
2792 )?;
2793 require_eq(
2794 &legacy_b.last_root,
2795 &repaired_display,
2796 "collision migration repaired row",
2797 )?;
2798 require_eq(
2799 &distinct.last_root,
2800 &unaffected_display,
2801 "collision migration unaffected row",
2802 )?;
2803 require(
2804 legacy_a.last_root_identity != legacy_b.last_root_identity
2805 && legacy_b.last_root_identity != distinct.last_root_identity
2806 && legacy_a.last_root_identity != distinct.last_root_identity,
2807 "collision migration did not preserve distinct native identities",
2808 )?;
2809 require_eq(
2810 &retried.worktree_registrations(false)?.len(),
2811 &3,
2812 "collision migration retry registration count",
2813 )?;
2814 require_eq(
2815 &retried.connection.query_row(
2816 "SELECT COUNT(*) FROM sqlite_schema
2817 WHERE type = 'index' AND name IN (
2818 'idx_worktree_registrations_active_native_administrative_directory',
2819 'idx_worktree_registrations_active_native_root'
2820 )",
2821 [],
2822 |row| row.get::<_, i64>(0),
2823 )?,
2824 &2,
2825 "collision migration native indexes after retry",
2826 )?;
2827 for (identity, index, column) in [
2828 (
2829 legacy_a.git_administrative_directory_identity,
2830 "idx_worktree_registrations_active_native_administrative_directory",
2831 "git_administrative_directory_identity",
2832 ),
2833 (
2834 legacy_a.last_root_identity,
2835 "idx_worktree_registrations_active_native_root",
2836 "last_root_identity",
2837 ),
2838 ] {
2839 let query = format!(
2840 "EXPLAIN QUERY PLAN
2841 SELECT registration_id FROM worktree_registrations
2842 INDEXED BY {index}
2843 WHERE state = 'active' AND {column} = ?1"
2844 );
2845 let plan = retried
2846 .connection
2847 .prepare(&query)?
2848 .query_map(params![identity.encode()?], |row| row.get::<_, String>(3))?
2849 .collect::<Result<Vec<_>, _>>()?;
2850 require(
2851 plan.iter().any(|detail| detail.contains(index)),
2852 &format!("retry query plan omitted {index}: {plan:?}"),
2853 )?;
2854 }
2855 Ok(())
2856 }
2857
2858 #[test]
2859 fn hot_registry_and_aggregate_lookups_use_owning_indexes() -> Result<(), Box<dyn Error>> {
2860 let connection = Connection::open_in_memory()?;
2861 crate::schema::initialize(&connection, None)?;
2862 for (sql, index) in [
2863 (
2864 "EXPLAIN QUERY PLAN
2865 SELECT registration_id FROM worktree_registrations
2866 INDEXED BY idx_worktree_registrations_active_alias
2867 WHERE state = 'active' AND alias = 'issue-430'",
2868 "idx_worktree_registrations_active_alias",
2869 ),
2870 (
2871 "EXPLAIN QUERY PLAN
2872 SELECT registration_id FROM worktree_registrations
2873 INDEXED BY idx_worktree_registrations_active_administrative_directory
2874 WHERE state = 'active' AND git_administrative_directory = 'admin'",
2875 "idx_worktree_registrations_active_administrative_directory",
2876 ),
2877 (
2878 "EXPLAIN QUERY PLAN
2879 SELECT registration_id FROM worktree_registrations
2880 INDEXED BY idx_worktree_registrations_active_administrative_identity
2881 WHERE state = 'active' AND git_administrative_identity = 'identity'",
2882 "idx_worktree_registrations_active_administrative_identity",
2883 ),
2884 (
2885 "EXPLAIN QUERY PLAN
2886 SELECT registration_id FROM worktree_registrations
2887 INDEXED BY idx_worktree_registrations_active_native_administrative_directory
2888 WHERE state = 'active' AND git_administrative_directory_identity = zeroblob(3)",
2889 "idx_worktree_registrations_active_native_administrative_directory",
2890 ),
2891 (
2892 "EXPLAIN QUERY PLAN
2893 SELECT registration_id FROM worktree_registrations
2894 INDEXED BY idx_worktree_registrations_active_native_root
2895 WHERE state = 'active' AND last_root_identity = zeroblob(3)",
2896 "idx_worktree_registrations_active_native_root",
2897 ),
2898 (
2899 "EXPLAIN QUERY PLAN
2900 SELECT registration_id FROM worktree_registrations
2901 INDEXED BY idx_worktree_registrations_active_project
2902 WHERE state = 'active' AND project_instance_id = zeroblob(16)",
2903 "idx_worktree_registrations_active_project",
2904 ),
2905 (
2906 "EXPLAIN QUERY PLAN
2907 SELECT registration_id FROM worktree_usage_aggregates
2908 INDEXED BY idx_worktree_usage_aggregates_day_registration
2909 WHERE day_epoch = -1 AND source_kind = 'synchronized'",
2910 "idx_worktree_usage_aggregates_day_registration",
2911 ),
2912 ] {
2913 let plan = connection
2914 .prepare(sql)?
2915 .query_map([], |row| row.get::<_, String>(3))?
2916 .collect::<Result<Vec<_>, _>>()?;
2917 require(
2918 plan.iter().any(|detail| detail.contains(index)),
2919 &format!("query plan omitted {index}: {plan:?}"),
2920 )?;
2921 }
2922 Ok(())
2923 }
2924}