Skip to main content

lit/commands/
checkout.rs

1use crate::core::{find_repo_root, read_ref, update_head, write_ref, Object, ObjectHash, Tree};
2use crate::response::CheckoutResponse;
3use crate::storage::{Index, ObjectStore};
4use std::fs;
5use std::path::Path;
6
7pub fn execute(
8    target: String,
9    create_new: bool,
10) -> Result<CheckoutResponse, crate::errors::LitError> {
11    let repo_root = find_repo_root()?;
12
13    if create_new {
14        use crate::core::read_head;
15        let head_hash = read_head(&repo_root)?;
16        write_ref(&repo_root, &format!("heads/{}", target), &head_hash)?;
17        update_head(&repo_root, &target)?;
18        checkout_commit(&repo_root, &ObjectHash::from_hex(head_hash))?;
19        Ok(CheckoutResponse {
20            target,
21            is_new_branch: true,
22            is_detached: false,
23        })
24    } else if let Ok(commit_hash) = read_ref(&repo_root, &format!("heads/{}", target)) {
25        update_head(&repo_root, &target)?;
26        checkout_commit(&repo_root, &ObjectHash::from_hex(commit_hash))?;
27        Ok(CheckoutResponse {
28            target,
29            is_new_branch: false,
30            is_detached: false,
31        })
32    } else {
33        let hash = ObjectHash::from_hex(target.clone());
34        checkout_commit(&repo_root, &hash)?;
35        use crate::core::set_head_detached;
36        set_head_detached(&repo_root, &target)?;
37        Ok(CheckoutResponse {
38            target,
39            is_new_branch: false,
40            is_detached: true,
41        })
42    }
43}
44
45fn checkout_commit(
46    repo_root: &Path,
47    commit_hash: &ObjectHash,
48) -> Result<(), crate::errors::LitError> {
49    let store = ObjectStore::new(repo_root);
50
51    // Read commit
52    let commit = match store.read(commit_hash)? {
53        Object::Commit(c) => c,
54        _ => return Err("Not a commit".into()),
55    };
56
57    // Read tree
58    let tree = match store.read(&commit.tree)? {
59        Object::Tree(t) => t,
60        _ => return Err("Not a tree".into()),
61    };
62
63    // Update the working directory and the index in one walk.
64    //
65    // These were two walks, and only this one recursed. The index was rebuilt
66    // from the root tree's entries alone, keyed by entry name, so a
67    // subdirectory was recorded as though it were a file — `src` with the
68    // subtree's hash — and the blobs beneath it got no entry at all. `status`
69    // then called `fs::read` on a directory and failed outright, so any
70    // repository with a subdirectory was broken by a checkout. Populating the
71    // index from the same recursion that writes the files keeps the two in
72    // step by construction.
73    let mut index = Index::new();
74    checkout_tree(repo_root, &tree, &store, "", &mut index)?;
75    index.save(repo_root)?;
76
77    Ok(())
78}
79
80/// Write a tree to the working directory, recording each blob in `index`
81/// under its full path.
82fn checkout_tree(
83    repo_root: &Path,
84    tree: &Tree,
85    store: &ObjectStore,
86    prefix: &str,
87    index: &mut Index,
88) -> Result<(), crate::errors::LitError> {
89    for entry in &tree.entries {
90        let path = if prefix.is_empty() {
91            entry.name.clone()
92        } else {
93            format!("{}/{}", prefix, entry.name)
94        };
95
96        let full_path = repo_root.join(&path);
97
98        match entry.object_type.as_str() {
99            "blob" => {
100                // Write file
101                let blob = match store.read(&entry.hash)? {
102                    Object::Blob(b) => b,
103                    _ => return Err("Expected blob".into()),
104                };
105
106                if let Some(parent) = full_path.parent() {
107                    fs::create_dir_all(parent)
108                        .map_err(|e| format!("Failed to create directory: {}", e))?;
109                }
110
111                fs::write(&full_path, &blob.content)
112                    .map_err(|e| format!("Failed to write file: {}", e))?;
113
114                index.add(path, entry.hash.to_string(), entry.mode.clone());
115            }
116            "tree" => {
117                // Recursively checkout subtree
118                let subtree = match store.read(&entry.hash)? {
119                    Object::Tree(t) => t,
120                    _ => return Err("Expected tree".into()),
121                };
122
123                checkout_tree(repo_root, &subtree, store, &path, index)?;
124            }
125            _ => {}
126        }
127    }
128
129    Ok(())
130}