git_internal/internal/object/
tree.rs1use std::fmt::Display;
18
19use colored::Colorize;
20use encoding_rs::GBK;
21
22use crate::{
23 errors::GitError,
24 hash::{ObjectHash, get_hash_kind},
25 internal::object::{ObjectTrait, ObjectType},
26};
27
28#[derive(
33 PartialEq,
34 Eq,
35 Debug,
36 Clone,
37 Copy,
38 serde::Serialize,
39 serde::Deserialize,
40 Hash,
41 rkyv::Archive,
42 rkyv::Serialize,
43 rkyv::Deserialize,
44)]
45pub enum TreeItemMode {
46 Blob,
47 BlobExecutable,
48 Tree,
49 Commit,
50 Link,
51}
52
53impl Display for TreeItemMode {
54 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
55 let _print = match *self {
56 TreeItemMode::Blob => "blob",
57 TreeItemMode::BlobExecutable => "blob executable",
58 TreeItemMode::Tree => "tree",
59 TreeItemMode::Commit => "commit",
60 TreeItemMode::Link => "link",
61 };
62
63 write!(f, "{}", String::from(_print).blue())
64 }
65}
66
67impl TreeItemMode {
68 pub fn tree_item_type_from_bytes(mode: &[u8]) -> Result<TreeItemMode, GitError> {
96 Ok(match mode {
97 b"40000" => TreeItemMode::Tree,
98 b"100644" => TreeItemMode::Blob,
99 b"100755" => TreeItemMode::BlobExecutable,
100 b"120000" => TreeItemMode::Link,
101 b"160000" => TreeItemMode::Commit,
102 b"100664" => TreeItemMode::Blob,
103 b"100640" => TreeItemMode::Blob,
104 _ => {
105 return Err(GitError::InvalidTreeItem(
106 String::from_utf8(mode.to_vec()).unwrap(),
107 ));
108 }
109 })
110 }
111
112 pub fn to_bytes(self) -> &'static [u8] {
117 match self {
118 TreeItemMode::Blob => b"100644",
119 TreeItemMode::BlobExecutable => b"100755",
120 TreeItemMode::Link => b"120000",
121 TreeItemMode::Tree => b"40000",
122 TreeItemMode::Commit => b"160000",
123 }
124 }
125}
126
127#[derive(
146 PartialEq,
147 Eq,
148 Debug,
149 Clone,
150 serde::Serialize,
151 serde::Deserialize,
152 Hash,
153 rkyv::Archive,
154 rkyv::Serialize,
155 rkyv::Deserialize,
156)]
157pub struct TreeItem {
158 pub mode: TreeItemMode,
159 pub id: ObjectHash,
160 pub name: String,
161}
162
163impl Display for TreeItem {
164 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
165 write!(
166 f,
167 "{} {} {}",
168 self.mode,
169 self.name,
170 self.id.to_string().blue()
171 )
172 }
173}
174
175impl TreeItem {
176 pub fn new(mode: TreeItemMode, id: ObjectHash, name: String) -> Self {
178 TreeItem { mode, id, name }
179 }
180
181 pub fn from_bytes(bytes: &[u8]) -> Result<Self, GitError> {
188 let mut parts = bytes.splitn(2, |b| *b == b' ');
189 let mode = parts.next().unwrap();
190 let rest = parts.next().unwrap();
191 let mut parts = rest.splitn(2, |b| *b == b'\0');
192 let raw_name = parts.next().unwrap();
193 let id = parts.next().unwrap();
194
195 let name = if String::from_utf8(raw_name.to_vec()).is_ok() {
196 String::from_utf8(raw_name.to_vec()).unwrap()
197 } else {
198 let (decoded, _, had_errors) = GBK.decode(raw_name);
199 if had_errors {
200 return Err(GitError::InvalidTreeItem(format!(
201 "Unsupported raw format: {raw_name:?}"
202 )));
203 } else {
204 decoded.to_string()
205 }
206 };
207 Ok(TreeItem {
208 mode: TreeItemMode::tree_item_type_from_bytes(mode)?,
209 id: ObjectHash::from_bytes(id).unwrap(),
210 name,
211 })
212 }
213
214 pub fn to_data(&self) -> Vec<u8> {
229 let mut bytes = Vec::new();
230
231 bytes.extend_from_slice(self.mode.to_bytes());
232 bytes.push(b' ');
233 bytes.extend_from_slice(self.name.as_bytes());
234 bytes.push(b'\0');
235 bytes.extend_from_slice(&self.id.to_data());
236
237 bytes
238 }
239
240 pub fn is_tree(&self) -> bool {
242 self.mode == TreeItemMode::Tree
243 }
244
245 pub fn is_blob(&self) -> bool {
247 self.mode == TreeItemMode::Blob
248 }
249}
250
251#[derive(
254 Eq,
255 Debug,
256 Clone,
257 serde::Serialize,
258 serde::Deserialize,
259 rkyv::Archive,
260 rkyv::Serialize,
261 rkyv::Deserialize,
262)]
263pub struct Tree {
264 pub id: ObjectHash,
265 pub tree_items: Vec<TreeItem>,
266}
267
268impl PartialEq for Tree {
269 fn eq(&self, other: &Self) -> bool {
270 self.id == other.id
271 }
272}
273
274impl Display for Tree {
275 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
276 writeln!(f, "Tree: {}", self.id.to_string().blue())?;
277 for item in &self.tree_items {
278 writeln!(f, "{item}")?;
279 }
280 Ok(())
281 }
282}
283
284impl Tree {
285 pub fn from_tree_items(tree_items: Vec<TreeItem>) -> Result<Self, GitError> {
287 if tree_items.is_empty() {
288 return Err(GitError::EmptyTreeItems(
289 "When export tree object to meta, the items is empty"
290 .parse()
291 .unwrap(),
292 ));
293 }
294 let mut data = Vec::new();
295 for item in &tree_items {
296 data.extend_from_slice(item.to_data().as_slice());
297 }
298
299 Ok(Tree {
300 id: ObjectHash::from_type_and_data(ObjectType::Tree, &data),
301 tree_items,
302 })
303 }
304
305 pub fn rehash(&mut self) {
307 let mut data = Vec::new();
308 for item in &self.tree_items {
309 data.extend_from_slice(item.to_data().as_slice());
310 }
311 self.id = ObjectHash::from_type_and_data(ObjectType::Tree, &data);
312 }
313}
314
315impl TryFrom<&[u8]> for Tree {
316 type Error = GitError;
317 fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
318 let h = ObjectHash::from_type_and_data(ObjectType::Tree, data);
319 Tree::from_bytes(data, h)
320 }
321}
322impl ObjectTrait for Tree {
323 fn from_bytes(data: &[u8], hash: ObjectHash) -> Result<Self, GitError>
324 where
325 Self: Sized,
326 {
327 let mut tree_items = Vec::new();
328 let mut i = 0;
329 while i < data.len() {
330 if let Some(index) = memchr::memchr(0x00, &data[i..]) {
332 let next = i + index + get_hash_kind().size() + 1; if next > data.len() {
335 return Err(GitError::InvalidTreeObject);
336 } let item_data = &data[i..next];
339 let tree_item = TreeItem::from_bytes(item_data)?;
340
341 tree_items.push(tree_item);
342
343 i = next;
344 } else {
345 return Err(GitError::InvalidTreeObject);
347 }
348 }
349
350 Ok(Tree {
351 id: hash,
352 tree_items,
353 })
354 }
355
356 fn get_type(&self) -> ObjectType {
357 ObjectType::Tree
358 }
359
360 fn get_size(&self) -> usize {
361 self.to_data().map(|data| data.len()).unwrap_or(0)
362 }
363
364 fn to_data(&self) -> Result<Vec<u8>, GitError> {
365 let mut data: Vec<u8> = Vec::new();
366
367 for item in &self.tree_items {
368 data.extend_from_slice(item.to_data().as_slice());
369 }
371
372 Ok(data)
373 }
374}
375
376#[cfg(test)]
377mod tests {
378
379 use std::str::FromStr;
380
381 use crate::{
382 hash::{HashKind, ObjectHash, set_hash_kind_for_test},
383 internal::object::tree::{Tree, TreeItem, TreeItemMode},
384 };
385
386 fn tree_item_round_trip(kind: HashKind, id_hex: &str) {
388 let _guard = set_hash_kind_for_test(kind);
389 let item = TreeItem::new(
390 TreeItemMode::Blob,
391 ObjectHash::from_str(id_hex).unwrap(),
392 "hello-world".to_string(),
393 );
394
395 let bytes = item.to_data();
396 let parsed = TreeItem::from_bytes(&bytes).unwrap();
397
398 assert_eq!(parsed.mode, TreeItemMode::Blob);
399 assert_eq!(parsed.id, item.id);
400 assert_eq!(parsed.name, item.name);
401 }
402
403 #[test]
405 fn tree_item_round_trip_sha1() {
406 tree_item_round_trip(HashKind::Sha1, "8ab686eafeb1f44702738c8b0f24f2567c36da6d");
407 }
408
409 #[test]
411 fn tree_item_round_trip_sha256() {
412 tree_item_round_trip(
413 HashKind::Sha256,
414 "2cf8d83d9ee29543b34a87727421fdecb7e3f3a183d337639025de576db9ebb4",
415 );
416 }
417
418 fn tree_round_trip(kind: HashKind, items: Vec<(&str, &str)>, expected_id: &str) {
420 let _guard = set_hash_kind_for_test(kind);
421 let tree_items = items
422 .into_iter()
423 .map(|(name, id_hex)| {
424 TreeItem::new(
425 TreeItemMode::Blob,
426 ObjectHash::from_str(id_hex).unwrap(),
427 name.to_string(),
428 )
429 })
430 .collect::<Vec<_>>();
431 let tree = Tree::from_tree_items(tree_items).unwrap();
432 assert_eq!(tree.id.to_string(), expected_id);
433 }
434
435 #[test]
437 fn tree_from_items_sha1() {
438 tree_round_trip(
439 HashKind::Sha1,
440 vec![("hello-world", "17288789afffb273c8c394bc65e87d899b92897b")],
441 "cf99336fa61439a2f074c7e6de1c1a05579550e2",
442 );
443 }
444
445 #[test]
447 fn tree_from_items_sha256() {
448 tree_round_trip(
449 HashKind::Sha256,
450 vec![
451 (
452 "a.txt",
453 "2cf8d83d9ee29543b34a87727421fdecb7e3f3a183d337639025de576db9ebb4",
454 ),
455 (
456 "b.txt",
457 "fc2593998f8e1dec9c3a8be11557888134dad90ef5c7a2d6236ed75534c7698e",
458 ),
459 (
460 "c.txt",
461 "21513dcb4d6f9eb247db3b4c52158395d94f809cbaa2630bd2a7a474d9b39fab",
462 ),
463 (
464 "hello-world",
465 "2cf8d83d9ee29543b34a87727421fdecb7e3f3a183d337639025de576db9ebb4",
466 ),
467 (
468 "message.txt",
469 "9ba9ae56288652bf32f074f922e37d3e95df8920b3cdfc053309595b8f86cbc6",
470 ),
471 ],
472 "d712a36aadfb47cabc7aaa90cf9e515773ba3bfc1fe3783730b387ce15c49261",
473 );
474 }
475}