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 CategorizedReferenceName, GitRunInfo, MaybeZeroOid, NonZeroOid, ReferenceName, Repo, Stage,
16 UpdateIndexCommand, WorkingCopySnapshot, update_index,
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 force_detach: bool,
49
50 pub reset: bool,
53
54 pub render_smartlog: bool,
56}
57
58impl Default for CheckOutCommitOptions {
59 fn default() -> Self {
60 Self {
61 additional_args: Default::default(),
62 force_detach: false,
63 reset: false,
64 render_smartlog: true,
65 }
66 }
67}
68
69fn maybe_get_branch_name(
70 current_target: Option<String>,
71 oid: Option<NonZeroOid>,
72 repo: &Repo,
73) -> eyre::Result<Option<String>> {
74 let RepoReferencesSnapshot {
75 head_oid,
76 branch_oid_to_names,
77 ..
78 } = repo.get_references_snapshot()?;
79 let oid = match current_target {
80 Some(_) => oid,
81 None => head_oid,
82 };
83 if current_target.is_some()
84 && ((head_oid.is_some() && head_oid == oid)
85 || current_target == head_oid.map(|o| o.to_string()))
86 {
87 return Ok(current_target);
89 }
90
91 match oid {
94 Some(oid) => match branch_oid_to_names.get(&oid) {
95 Some(branch_names) => match branch_names.iter().exactly_one() {
96 Ok(branch_name) => {
97 let name = CategorizedReferenceName::new(branch_name);
99 Ok(Some(name.render_suffix()))
100 }
101 Err(_) => Ok(current_target),
102 },
103 None => Ok(current_target),
104 },
105 None => Ok(current_target),
106 }
107}
108
109#[instrument]
112pub fn check_out_commit(
113 effects: &Effects,
114 git_run_info: &GitRunInfo,
115 repo: &Repo,
116 event_log_db: &EventLogDb,
117 event_tx_id: EventTransactionId,
118 target: Option<CheckoutTarget>,
119 options: &CheckOutCommitOptions,
120) -> EyreExitOr<()> {
121 let CheckOutCommitOptions {
122 additional_args,
123 force_detach,
124 reset,
125 render_smartlog,
126 } = options;
127
128 let (target, oid) = match target {
129 None => (None, None),
130 Some(CheckoutTarget::Reference(reference_name)) => {
131 let categorized_target = CategorizedReferenceName::new(&reference_name);
132 (Some(categorized_target.render_suffix()), None)
133 }
134 Some(CheckoutTarget::Oid(oid)) => (Some(oid.to_string()), Some(oid)),
135 Some(CheckoutTarget::Unknown(target)) => (Some(target), None),
136 };
137
138 if get_undo_create_snapshots(repo)? {
139 create_snapshot(effects, git_run_info, repo, event_log_db, event_tx_id)?;
140 }
141
142 let target = if get_auto_switch_branches(repo)? && !reset && !force_detach {
143 maybe_get_branch_name(target, oid, repo)?
144 } else {
145 target
146 };
147
148 if *reset {
149 if let Some(target) = &target {
150 try_exit_code!(git_run_info.run(
151 effects,
152 Some(event_tx_id),
153 &["reset", target, "--"]
154 )?);
155 }
156 } else {
157 let checkout_args = {
158 let mut args = vec![OsStr::new("checkout")];
159 if let Some(target) = &target {
160 args.push(OsStr::new(target.as_str()));
161 }
162 args.extend(additional_args.iter().map(OsStr::new));
163 args.push(OsStr::new("--"));
164 args
165 };
166 match git_run_info.run(effects, Some(event_tx_id), checkout_args.as_slice())? {
167 Ok(()) => {}
168 Err(exit_code) => {
169 writeln!(
170 effects.get_output_stream(),
171 "{}",
172 effects.get_glyphs().render(StyledString::styled(
173 match target {
174 Some(target) => format!("Failed to check out commit: {target}"),
175 None => "Failed to check out commit".to_string(),
176 },
177 BaseColor::Red.light()
178 ))?
179 )?;
180 return Ok(Err(exit_code));
181 }
182 }
183 }
184
185 {
188 let head_info = repo.get_head_info()?;
189 if let Some(head_oid) = head_info.oid {
190 let head_commit = repo.find_commit_or_fail(head_oid)?;
191 if let Some(snapshot) = WorkingCopySnapshot::try_from_base_commit(repo, &head_commit)? {
192 try_exit_code!(restore_snapshot(
193 effects,
194 git_run_info,
195 repo,
196 event_tx_id,
197 &snapshot
198 )?);
199 }
200 }
201 }
202
203 if *render_smartlog {
204 try_exit_code!(
205 git_run_info.run_direct_no_wrapping(Some(event_tx_id), &["branchless", "smartlog"])?
206 );
207 }
208 Ok(Ok(()))
209}
210
211pub fn create_snapshot<'repo>(
217 effects: &Effects,
218 git_run_info: &GitRunInfo,
219 repo: &'repo Repo,
220 event_log_db: &EventLogDb,
221 event_tx_id: EventTransactionId,
222) -> eyre::Result<WorkingCopySnapshot<'repo>> {
223 writeln!(
224 effects.get_error_stream(),
225 "branchless: creating working copy snapshot"
226 )?;
227
228 let head_info = repo.get_head_info()?;
229 let index = repo.get_index()?;
230 let (snapshot, _status) =
231 repo.get_status(effects, git_run_info, &index, &head_info, Some(event_tx_id))?;
232 event_log_db.add_events(vec![Event::WorkingCopySnapshot {
233 timestamp: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs_f64(),
234 event_tx_id,
235 head_oid: MaybeZeroOid::from(head_info.oid),
236 commit_oid: snapshot.base_commit.get_oid(),
237 ref_name: head_info.reference_name,
238 }])?;
239 Ok(snapshot)
240}
241
242pub fn restore_snapshot(
252 effects: &Effects,
253 git_run_info: &GitRunInfo,
254 repo: &Repo,
255 event_tx_id: EventTransactionId,
256 snapshot: &WorkingCopySnapshot,
257) -> EyreExitOr<()> {
258 writeln!(
259 effects.get_error_stream(),
260 "branchless: restoring from snapshot"
261 )?;
262
263 try_exit_code!(
266 git_run_info
267 .run(
268 effects,
269 Some(event_tx_id),
270 &["reset", "--hard", "HEAD", "--"]
271 )
272 .wrap_err("Discarding working copy changes")?
273 );
274
275 try_exit_code!(
282 git_run_info
283 .run(
284 effects,
285 Some(event_tx_id),
286 &[
287 "checkout",
288 &snapshot.commit_unstaged.get_oid().to_string(),
289 "--"
290 ],
291 )
292 .wrap_err("Checking out unstaged changes (fail if conflict)")?
293 );
294
295 match &snapshot.head_commit {
298 Some(head_commit) => {
299 try_exit_code!(
300 git_run_info
301 .run(
302 effects,
303 Some(event_tx_id),
304 &["reset", &head_commit.get_oid().to_string(), "--"],
305 )
306 .wrap_err("Update HEAD for unstaged changes")?
307 );
308 }
309 None => {
310 }
312 }
313
314 let update_index_script = {
316 let mut commands = Vec::new();
317 for (stage, commit) in [
318 (Stage::Stage0, &snapshot.commit_stage0),
319 (Stage::Stage1, &snapshot.commit_stage1),
320 (Stage::Stage2, &snapshot.commit_stage2),
321 (Stage::Stage3, &snapshot.commit_stage3),
322 ] {
323 let changed_paths = repo.get_paths_touched_by_commit(commit)?;
324 for path in changed_paths {
325 let tree = commit.get_tree()?;
326 let tree_entry = tree.get_path(&path)?;
327
328 let is_deleted = tree_entry.is_none();
329 if is_deleted {
330 commands.push(UpdateIndexCommand::Delete { path: path.clone() })
331 }
332
333 if let Some(tree_entry) = tree_entry {
334 commands.push(UpdateIndexCommand::Update {
335 path,
336 stage,
337 mode: tree_entry.get_filemode(),
338 oid: tree_entry.get_oid(),
339 })
340 }
341 }
342 }
343 commands
344 };
345 let index = repo.get_index()?;
346 update_index(
347 git_run_info,
348 repo,
349 &index,
350 event_tx_id,
351 &update_index_script,
352 )?;
353
354 if let Some(ref_name) = &snapshot.head_reference_name {
357 let head_oid = match &snapshot.head_commit {
358 Some(head_commit) => MaybeZeroOid::NonZero(head_commit.get_oid()),
359 None => MaybeZeroOid::Zero,
360 };
361 try_exit_code!(
362 git_run_info
363 .run(
364 effects,
365 Some(event_tx_id),
366 &["update-ref", ref_name.as_str(), &head_oid.to_string()],
367 )
368 .context("Restoring snapshot branch")?
369 );
370
371 try_exit_code!(
372 git_run_info
373 .run(
374 effects,
375 Some(event_tx_id),
376 &["symbolic-ref", "HEAD", ref_name.as_str()],
377 )
378 .context("Checking out snapshot branch")?
379 );
380 }
381
382 Ok(Ok(()))
383}