1use std::ffi::{OsStr, OsString};
4use std::fmt::Write;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7use cursive::theme::BaseColor;
8use cursive::utils::markup::StyledString;
9use eyre::Context;
10use itertools::Itertools;
11use tracing::instrument;
12
13use crate::core::config::get_auto_switch_branches;
14use crate::git::{
15 update_index, CategorizedReferenceName, GitRunInfo, MaybeZeroOid, NonZeroOid, ReferenceName,
16 Repo, Stage, UpdateIndexCommand, WorkingCopySnapshot,
17};
18use crate::try_exit_code;
19use crate::util::EyreExitOr;
20
21use super::config::get_undo_create_snapshots;
22use super::effects::Effects;
23use super::eventlog::{Event, EventLogDb, EventTransactionId};
24use super::repo_ext::{RepoExt, RepoReferencesSnapshot};
25
26#[derive(Clone, Debug)]
28pub enum CheckoutTarget {
29 Oid(NonZeroOid),
31
32 Reference(ReferenceName),
35
36 Unknown(String),
39}
40
41#[derive(Clone, Debug)]
43pub struct CheckOutCommitOptions {
44 pub additional_args: Vec<OsString>,
46
47 pub reset: bool,
50
51 pub render_smartlog: bool,
53}
54
55impl Default for CheckOutCommitOptions {
56 fn default() -> Self {
57 Self {
58 additional_args: Default::default(),
59 reset: false,
60 render_smartlog: true,
61 }
62 }
63}
64
65fn maybe_get_branch_name(
66 current_target: Option<String>,
67 oid: Option<NonZeroOid>,
68 repo: &Repo,
69) -> eyre::Result<Option<String>> {
70 let RepoReferencesSnapshot {
71 head_oid,
72 branch_oid_to_names,
73 ..
74 } = repo.get_references_snapshot()?;
75 let oid = match current_target {
76 Some(_) => oid,
77 None => head_oid,
78 };
79 if current_target.is_some()
80 && ((head_oid.is_some() && head_oid == oid)
81 || current_target == head_oid.map(|o| o.to_string()))
82 {
83 return Ok(current_target);
85 }
86
87 match oid {
90 Some(oid) => match branch_oid_to_names.get(&oid) {
91 Some(branch_names) => match branch_names.iter().exactly_one() {
92 Ok(branch_name) => {
93 let name = CategorizedReferenceName::new(branch_name);
95 Ok(Some(name.render_suffix()))
96 }
97 Err(_) => Ok(current_target),
98 },
99 None => Ok(current_target),
100 },
101 None => Ok(current_target),
102 }
103}
104
105#[instrument]
108pub fn check_out_commit(
109 effects: &Effects,
110 git_run_info: &GitRunInfo,
111 repo: &Repo,
112 event_log_db: &EventLogDb,
113 event_tx_id: EventTransactionId,
114 target: Option<CheckoutTarget>,
115 options: &CheckOutCommitOptions,
116) -> EyreExitOr<()> {
117 let CheckOutCommitOptions {
118 additional_args,
119 reset,
120 render_smartlog,
121 } = options;
122
123 let (target, oid) = match target {
124 None => (None, None),
125 Some(CheckoutTarget::Reference(reference_name)) => {
126 let categorized_target = CategorizedReferenceName::new(&reference_name);
127 (Some(categorized_target.render_suffix()), None)
128 }
129 Some(CheckoutTarget::Oid(oid)) => (Some(oid.to_string()), Some(oid)),
130 Some(CheckoutTarget::Unknown(target)) => (Some(target), None),
131 };
132
133 if get_undo_create_snapshots(repo)? {
134 create_snapshot(effects, git_run_info, repo, event_log_db, event_tx_id)?;
135 }
136
137 let target = if get_auto_switch_branches(repo)? && !reset {
138 maybe_get_branch_name(target, oid, repo)?
139 } else {
140 target
141 };
142
143 if *reset {
144 if let Some(target) = &target {
145 try_exit_code!(git_run_info.run(effects, Some(event_tx_id), &["reset", target])?);
146 }
147 } else {
148 let checkout_args = {
149 let mut args = vec![OsStr::new("checkout")];
150 if let Some(target) = &target {
151 args.push(OsStr::new(target.as_str()));
152 }
153 args.extend(additional_args.iter().map(OsStr::new));
154 args
155 };
156 match git_run_info.run(effects, Some(event_tx_id), checkout_args.as_slice())? {
157 Ok(()) => {}
158 Err(exit_code) => {
159 writeln!(
160 effects.get_output_stream(),
161 "{}",
162 effects.get_glyphs().render(StyledString::styled(
163 match target {
164 Some(target) => format!("Failed to check out commit: {target}"),
165 None => "Failed to check out commit".to_string(),
166 },
167 BaseColor::Red.light()
168 ))?
169 )?;
170 return Ok(Err(exit_code));
171 }
172 }
173 }
174
175 {
178 let head_info = repo.get_head_info()?;
179 if let Some(head_oid) = head_info.oid {
180 let head_commit = repo.find_commit_or_fail(head_oid)?;
181 if let Some(snapshot) = WorkingCopySnapshot::try_from_base_commit(repo, &head_commit)? {
182 try_exit_code!(restore_snapshot(
183 effects,
184 git_run_info,
185 repo,
186 event_tx_id,
187 &snapshot
188 )?);
189 }
190 }
191 }
192
193 if *render_smartlog {
194 try_exit_code!(
195 git_run_info.run_direct_no_wrapping(Some(event_tx_id), &["branchless", "smartlog"])?
196 );
197 }
198 Ok(Ok(()))
199}
200
201pub fn create_snapshot<'repo>(
207 effects: &Effects,
208 git_run_info: &GitRunInfo,
209 repo: &'repo Repo,
210 event_log_db: &EventLogDb,
211 event_tx_id: EventTransactionId,
212) -> eyre::Result<WorkingCopySnapshot<'repo>> {
213 writeln!(
214 effects.get_error_stream(),
215 "branchless: creating working copy snapshot"
216 )?;
217
218 let head_info = repo.get_head_info()?;
219 let index = repo.get_index()?;
220 let (snapshot, _status) =
221 repo.get_status(effects, git_run_info, &index, &head_info, Some(event_tx_id))?;
222 event_log_db.add_events(vec![Event::WorkingCopySnapshot {
223 timestamp: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs_f64(),
224 event_tx_id,
225 head_oid: MaybeZeroOid::from(head_info.oid),
226 commit_oid: snapshot.base_commit.get_oid(),
227 ref_name: head_info.reference_name,
228 }])?;
229 Ok(snapshot)
230}
231
232pub fn restore_snapshot(
242 effects: &Effects,
243 git_run_info: &GitRunInfo,
244 repo: &Repo,
245 event_tx_id: EventTransactionId,
246 snapshot: &WorkingCopySnapshot,
247) -> EyreExitOr<()> {
248 writeln!(
249 effects.get_error_stream(),
250 "branchless: restoring from snapshot"
251 )?;
252
253 try_exit_code!(git_run_info
256 .run(effects, Some(event_tx_id), &["reset", "--hard", "HEAD"])
257 .wrap_err("Discarding working copy changes")?);
258
259 try_exit_code!(git_run_info
266 .run(
267 effects,
268 Some(event_tx_id),
269 &["checkout", &snapshot.commit_unstaged.get_oid().to_string()],
270 )
271 .wrap_err("Checking out unstaged changes (fail if conflict)")?);
272
273 match &snapshot.head_commit {
276 Some(head_commit) => {
277 try_exit_code!(git_run_info
278 .run(
279 effects,
280 Some(event_tx_id),
281 &["reset", &head_commit.get_oid().to_string()],
282 )
283 .wrap_err("Update HEAD for unstaged changes")?);
284 }
285 None => {
286 }
288 }
289
290 let update_index_script = {
292 let mut commands = Vec::new();
293 for (stage, commit) in [
294 (Stage::Stage0, &snapshot.commit_stage0),
295 (Stage::Stage1, &snapshot.commit_stage1),
296 (Stage::Stage2, &snapshot.commit_stage2),
297 (Stage::Stage3, &snapshot.commit_stage3),
298 ] {
299 let changed_paths = repo.get_paths_touched_by_commit(commit)?;
300 for path in changed_paths {
301 let tree = commit.get_tree()?;
302 let tree_entry = tree.get_path(&path)?;
303
304 let is_deleted = tree_entry.is_none();
305 if is_deleted {
306 commands.push(UpdateIndexCommand::Delete { path: path.clone() })
307 }
308
309 if let Some(tree_entry) = tree_entry {
310 commands.push(UpdateIndexCommand::Update {
311 path,
312 stage,
313 mode: tree_entry.get_filemode(),
314 oid: tree_entry.get_oid(),
315 })
316 }
317 }
318 }
319 commands
320 };
321 let index = repo.get_index()?;
322 update_index(
323 git_run_info,
324 repo,
325 &index,
326 event_tx_id,
327 &update_index_script,
328 )?;
329
330 if let Some(ref_name) = &snapshot.head_reference_name {
333 let head_oid = match &snapshot.head_commit {
334 Some(head_commit) => MaybeZeroOid::NonZero(head_commit.get_oid()),
335 None => MaybeZeroOid::Zero,
336 };
337 try_exit_code!(git_run_info
338 .run(
339 effects,
340 Some(event_tx_id),
341 &["update-ref", ref_name.as_str(), &head_oid.to_string()],
342 )
343 .context("Restoring snapshot branch")?);
344
345 try_exit_code!(git_run_info
346 .run(
347 effects,
348 Some(event_tx_id),
349 &["symbolic-ref", "HEAD", ref_name.as_str()],
350 )
351 .context("Checking out snapshot branch")?);
352 }
353
354 Ok(Ok(()))
355}