yarli_cli/yarli-core/src/entities/
worktree_binding.rs1use std::path::PathBuf;
8
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use uuid::Uuid;
12
13use crate::yarli_core::domain::{CorrelationId, EventId, RunId, TaskId, WorktreeId};
14use crate::yarli_core::error::TransitionError;
15use crate::yarli_core::fsm::worktree::WorktreeState;
16
17use super::transition::Transition;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum SubmoduleMode {
23 Locked,
25 AllowFastForward,
27 AllowAny,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct WorktreeBinding {
34 pub id: WorktreeId,
36 pub run_id: RunId,
38 pub task_id: Option<TaskId>,
40 pub state: WorktreeState,
42 pub repo_root: PathBuf,
44 pub worktree_path: PathBuf,
46 pub branch_name: String,
48 pub base_ref: String,
50 pub head_ref: String,
52 pub dirty: bool,
54 pub submodule_state_hash: Option<String>,
56 pub submodule_mode: SubmoduleMode,
58 pub lease_owner: Option<String>,
60 pub correlation_id: CorrelationId,
62 pub created_at: DateTime<Utc>,
64 pub updated_at: DateTime<Utc>,
66}
67
68impl WorktreeBinding {
69 pub fn new(
71 run_id: RunId,
72 repo_root: impl Into<PathBuf>,
73 branch_name: impl Into<String>,
74 base_ref: impl Into<String>,
75 correlation_id: CorrelationId,
76 ) -> Self {
77 let now = Utc::now();
78 let base = base_ref.into();
79 Self {
80 id: Uuid::now_v7(),
81 run_id,
82 task_id: None,
83 state: WorktreeState::WtUnbound,
84 repo_root: repo_root.into(),
85 worktree_path: PathBuf::new(),
86 branch_name: branch_name.into(),
87 base_ref: base.clone(),
88 head_ref: base,
89 dirty: false,
90 submodule_state_hash: None,
91 submodule_mode: SubmoduleMode::Locked,
92 lease_owner: None,
93 correlation_id,
94 created_at: now,
95 updated_at: now,
96 }
97 }
98
99 pub fn with_task(mut self, task_id: TaskId) -> Self {
101 self.task_id = Some(task_id);
102 self
103 }
104
105 pub fn with_submodule_mode(mut self, mode: SubmoduleMode) -> Self {
107 self.submodule_mode = mode;
108 self
109 }
110
111 pub fn set_worktree_path(&mut self, path: impl Into<PathBuf>) {
113 self.worktree_path = path.into();
114 }
115
116 pub fn update_head_ref(&mut self, sha: impl Into<String>) {
118 self.head_ref = sha.into();
119 self.updated_at = Utc::now();
120 }
121
122 pub fn set_dirty(&mut self, dirty: bool) {
124 self.dirty = dirty;
125 self.updated_at = Utc::now();
126 }
127
128 pub fn update_submodule_hash(&mut self, hash: impl Into<String>) {
130 self.submodule_state_hash = Some(hash.into());
131 self.updated_at = Utc::now();
132 }
133
134 pub fn set_lease_owner(&mut self, owner: Option<String>) {
136 self.lease_owner = owner;
137 self.updated_at = Utc::now();
138 }
139
140 pub fn allows_mutations(&self) -> bool {
143 !matches!(
144 self.state,
145 WorktreeState::WtUnbound | WorktreeState::WtClosed | WorktreeState::WtCleanupPending
146 )
147 }
148
149 pub fn transition(
155 &mut self,
156 to: WorktreeState,
157 reason: impl Into<String>,
158 actor: impl Into<String>,
159 causation_id: Option<EventId>,
160 ) -> Result<Transition, TransitionError> {
161 let from = self.state;
162
163 if from.is_terminal() {
164 return Err(TransitionError::TerminalState(format!("{from:?}")));
165 }
166
167 if !from.can_transition_to(to) {
168 return Err(TransitionError::InvalidWorktreeTransition { from, to });
169 }
170
171 let reason_str = reason.into();
172 let actor_str = actor.into();
173
174 self.state = to;
175 self.updated_at = Utc::now();
176
177 Ok(Transition::new(
178 "worktree",
179 self.id,
180 format!("{from:?}"),
181 format!("{to:?}"),
182 reason_str,
183 actor_str,
184 self.correlation_id,
185 causation_id,
186 ))
187 }
188}