1use std::collections::BTreeMap;
4
5use crate::error::Result;
6use crate::index::{
7 Index, IndexEntry, MODE_EXECUTABLE, MODE_GITLINK, MODE_REGULAR, MODE_SYMLINK, MODE_TREE,
8};
9use crate::objects::{serialize_tree, tree_entry_cmp, ObjectId, ObjectKind, TreeEntry};
10use crate::odb::Odb;
11
12pub fn write_tree_from_index(odb: &Odb, index: &Index, prefix: &str) -> Result<ObjectId> {
16 let _ = odb.write(ObjectKind::Blob, b"");
21
22 let prefix_bytes = prefix.as_bytes();
23 let mut entries: Vec<&IndexEntry> = index
24 .entries
25 .iter()
26 .filter(|entry| {
27 entry.stage() == 0 && !entry.intent_to_add() && entry.path.starts_with(prefix_bytes)
28 })
29 .collect();
30 entries.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.stage().cmp(&b.stage())));
31 build_tree(odb, &entries, prefix_bytes)
32}
33
34fn build_tree(odb: &Odb, entries: &[&IndexEntry], dir_prefix: &[u8]) -> Result<ObjectId> {
35 let mut children: BTreeMap<Vec<u8>, ChildKind> = BTreeMap::new();
36
37 for entry in entries {
38 let path = &entry.path;
39 let rel = if dir_prefix.is_empty() {
40 path.as_slice()
41 } else {
42 path.strip_prefix(dir_prefix)
43 .and_then(|suffix| suffix.strip_prefix(b"/"))
44 .unwrap_or(path.as_slice())
45 };
46
47 if let Some(slash_pos) = rel.iter().position(|&byte| byte == b'/') {
48 let child_name = rel[..slash_pos].to_vec();
49 let sub_prefix = if dir_prefix.is_empty() {
50 child_name.clone()
51 } else {
52 let mut sub_prefix = dir_prefix.to_vec();
53 sub_prefix.push(b'/');
54 sub_prefix.extend_from_slice(&child_name);
55 sub_prefix
56 };
57 children
58 .entry(child_name)
59 .or_insert_with(|| ChildKind::Tree(sub_prefix, Vec::new()))
60 .push_entry(entry);
61 } else {
62 children
63 .entry(rel.to_vec())
64 .or_insert_with(|| ChildKind::Blob {
65 mode: canonicalize_blob_mode(entry.mode),
66 oid: entry.oid,
67 });
68 }
69 }
70
71 let mut tree_entries = Vec::with_capacity(children.len());
72 for (name, child) in children {
73 match child {
74 ChildKind::Blob { mode, oid } => tree_entries.push(TreeEntry { mode, name, oid }),
75 ChildKind::Tree(sub_prefix, sub_entries) => {
76 let sub_oid = build_tree(odb, &sub_entries, &sub_prefix)?;
77 tree_entries.push(TreeEntry {
78 mode: MODE_TREE,
79 name,
80 oid: sub_oid,
81 });
82 }
83 }
84 }
85
86 tree_entries.sort_by(|a, b| {
87 let a_tree = a.mode == MODE_TREE;
88 let b_tree = b.mode == MODE_TREE;
89 tree_entry_cmp(&a.name, a_tree, &b.name, b_tree)
90 });
91
92 let data = serialize_tree(&tree_entries);
93 odb.write(ObjectKind::Tree, &data)
94}
95
96fn canonicalize_blob_mode(mode: u32) -> u32 {
97 match mode & 0o170000 {
98 0o120000 => MODE_SYMLINK,
99 0o160000 => MODE_GITLINK,
100 0o100000 => {
101 if mode & 0o111 != 0 {
102 MODE_EXECUTABLE
103 } else {
104 MODE_REGULAR
105 }
106 }
107 _ => MODE_REGULAR,
108 }
109}
110
111enum ChildKind<'a> {
112 Blob { mode: u32, oid: ObjectId },
113 Tree(Vec<u8>, Vec<&'a IndexEntry>),
114}
115
116impl<'a> ChildKind<'a> {
117 fn push_entry(&mut self, entry: &'a IndexEntry) {
118 if let Self::Tree(_, entries) = self {
119 entries.push(entry);
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 #![allow(clippy::expect_used, clippy::unwrap_used)]
127
128 use super::*;
129 use crate::index::{IndexEntry, MODE_EXECUTABLE, MODE_REGULAR, MODE_SYMLINK, MODE_TREE};
130 use crate::objects::parse_tree;
131 use tempfile::TempDir;
132
133 fn entry(path: &str, mode: u32, oid: ObjectId) -> IndexEntry {
134 IndexEntry {
135 ctime_sec: 0,
136 ctime_nsec: 0,
137 mtime_sec: 0,
138 mtime_nsec: 0,
139 dev: 0,
140 ino: 0,
141 mode,
142 uid: 0,
143 gid: 0,
144 size: 0,
145 oid,
146 flags: path.len().min(0xFFF) as u16,
147 flags_extended: None,
148 path: path.as_bytes().to_vec(),
149 }
150 }
151
152 #[test]
153 fn writes_sorted_tree_with_canonical_modes() {
154 let temp_dir = TempDir::new().unwrap();
155 let odb = Odb::new(temp_dir.path());
156
157 let oid_a = odb.write(ObjectKind::Blob, b"a").unwrap();
158 let oid_exec = odb.write(ObjectKind::Blob, b"exec").unwrap();
159 let oid_link = odb.write(ObjectKind::Blob, b"target").unwrap();
160
161 let mut index = Index::new();
162 index.add_or_replace(entry("bin/run.sh", 0o100777, oid_exec));
163 index.add_or_replace(entry("link", 0o120777, oid_link));
164 index.add_or_replace(entry("a.txt", 0o100664, oid_a));
165
166 let root_oid = write_tree_from_index(&odb, &index, "").unwrap();
167 let root_tree_obj = odb.read(&root_oid).unwrap();
168 let root_entries = parse_tree(&root_tree_obj.data).unwrap();
169
170 assert_eq!(root_entries.len(), 3);
171 assert_eq!(root_entries[0].name, b"a.txt");
172 assert_eq!(root_entries[0].mode, MODE_REGULAR);
173 assert_eq!(root_entries[1].name, b"bin");
174 assert_eq!(root_entries[1].mode, MODE_TREE);
175 assert_eq!(root_entries[2].name, b"link");
176 assert_eq!(root_entries[2].mode, MODE_SYMLINK);
177
178 let bin_tree_obj = odb.read(&root_entries[1].oid).unwrap();
179 let bin_entries = parse_tree(&bin_tree_obj.data).unwrap();
180 assert_eq!(bin_entries.len(), 1);
181 assert_eq!(bin_entries[0].name, b"run.sh");
182 assert_eq!(bin_entries[0].mode, MODE_EXECUTABLE);
183 }
184}