├── .gitignore ├── fixtures ├── blob.git-file ├── blob.git-file.plaintext └── blob.git-file.unpacked ├── Cargo.toml ├── src ├── model │ ├── mod.rs │ ├── commit.rs │ ├── object.rs │ ├── tree_node.rs │ ├── tree.rs │ ├── hashable.rs │ └── blob.rs └── main.rs ├── .travis.yml ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /fixtures/blob.git-file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/themasch/rust-git/HEAD/fixtures/blob.git-file -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "git" 3 | version = "0.1.0" 4 | authors = ["Mark Schmale "] 5 | 6 | [dependencies] 7 | sha1="0.1.1" 8 | -------------------------------------------------------------------------------- /src/model/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod hashable; 2 | pub mod blob; 3 | pub mod tree_node; 4 | pub mod tree; 5 | pub mod object; 6 | 7 | pub use model::blob::Blob; 8 | pub use model::tree::Tree; 9 | pub use model::object::Object; 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: rust 3 | rust: 4 | - beta 5 | - stable 6 | 7 | before_script: 8 | - | 9 | pip install 'travis-cargo<0.2' --user && 10 | export PATH=$HOME/.local/bin:$PATH 11 | 12 | addons: 13 | apt: 14 | packages: 15 | - libcurl4-openssl-dev 16 | - libelf-dev 17 | - libdw-dev 18 | 19 | after_success: 20 | - | 21 | travis-cargo --only stable coveralls --no-sudo 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git in rust [![Build Status](https://travis-ci.org/themasch/rust-git.svg?branch=master)](https://travis-ci.org/themasch/rust-git)[![Coverage Status](https://coveralls.io/repos/github/themasch/rust-git/badge.svg?branch=master)](https://coveralls.io/github/themasch/rust-git?branch=master) 2 | (I'm afraid the coverage tingy is lying) 3 | 4 | I'm pretty sure this project will not be completed. But its a nice way to learn rust 5 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [root] 2 | name = "git" 3 | version = "0.1.0" 4 | dependencies = [ 5 | "sha1 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 6 | ] 7 | 8 | [[package]] 9 | name = "byteorder" 10 | version = "0.5.1" 11 | source = "registry+https://github.com/rust-lang/crates.io-index" 12 | 13 | [[package]] 14 | name = "sha1" 15 | version = "0.1.1" 16 | source = "registry+https://github.com/rust-lang/crates.io-index" 17 | dependencies = [ 18 | "byteorder 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 19 | ] 20 | 21 | -------------------------------------------------------------------------------- /fixtures/blob.git-file.plaintext: -------------------------------------------------------------------------------- 1 | [root] 2 | name = "git" 3 | version = "0.1.0" 4 | dependencies = [ 5 | "sha1 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 6 | ] 7 | 8 | [[package]] 9 | name = "byteorder" 10 | version = "0.5.1" 11 | source = "registry+https://github.com/rust-lang/crates.io-index" 12 | 13 | [[package]] 14 | name = "sha1" 15 | version = "0.1.1" 16 | source = "registry+https://github.com/rust-lang/crates.io-index" 17 | dependencies = [ 18 | "byteorder 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 19 | ] 20 | 21 | -------------------------------------------------------------------------------- /fixtures/blob.git-file.unpacked: -------------------------------------------------------------------------------- 1 | blob 449[root] 2 | name = "git" 3 | version = "0.1.0" 4 | dependencies = [ 5 | "sha1 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 6 | ] 7 | 8 | [[package]] 9 | name = "byteorder" 10 | version = "0.5.1" 11 | source = "registry+https://github.com/rust-lang/crates.io-index" 12 | 13 | [[package]] 14 | name = "sha1" 15 | version = "0.1.1" 16 | source = "registry+https://github.com/rust-lang/crates.io-index" 17 | dependencies = [ 18 | "byteorder 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 19 | ] 20 | 21 | -------------------------------------------------------------------------------- /src/model/commit.rs: -------------------------------------------------------------------------------- 1 | 2 | /// Format: 3 | /// "commit "[length]\0 4 | /// "tree "[tree hash in hex ascii]\0 5 | /// "parent "[parent commit hash in hex ascii]\0 (repeat for each parent)\0x0A 6 | /// "author "[author name in utf8]" <"[author email]"> "[unix timestamp]" "[timezone]\0x0A 7 | /// "commiter "[commiter name in utf8]" <"[author email]"> "[unix timestamp]" "[timezone]\0x0A 8 | /// \0x0A 9 | /// [commit message] 10 | /// TODO: gpg signing? other infos 11 | pub struct Commit { 12 | pub tree: Rc, 13 | pub parents: Vec>, 14 | pub author: (Contact, TimeInfo), 15 | pub committer: (Contact, TimeInfo) 16 | } 17 | -------------------------------------------------------------------------------- /src/model/object.rs: -------------------------------------------------------------------------------- 1 | use model::Blob; 2 | use model::Tree; 3 | use model::hashable::Hashable; 4 | 5 | #[derive(Debug)] 6 | pub enum Object { 7 | Blob(Blob), 8 | Tree(Tree), 9 | } 10 | 11 | impl From for Object { 12 | fn from(blob: Blob) -> Object { 13 | Object::Blob(blob) 14 | } 15 | } 16 | 17 | impl From for Object { 18 | fn from(tree: Tree) -> Object { 19 | Object::Tree(tree) 20 | } 21 | } 22 | 23 | impl Hashable for Object { 24 | fn get_hash_content(&self) -> Vec { 25 | match self { 26 | &Object::Blob(ref b) => b.get_hash_content(), 27 | &Object::Tree(ref t) => t.get_hash_content() 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/model/tree_node.rs: -------------------------------------------------------------------------------- 1 | use model::object::Object; 2 | 3 | #[derive(Debug)] 4 | pub struct TreeNode { 5 | pub mode: u16, 6 | pub object: Box, 7 | pub name: String 8 | } 9 | 10 | impl TreeNode { 11 | pub fn new(node: Box, mode: u16, name: String) -> TreeNode { 12 | TreeNode { object: node, mode: mode, name: name } 13 | } 14 | } 15 | 16 | #[cfg(test)] 17 | use model::blob::Blob; 18 | 19 | #[test] 20 | fn test_create_tree_node() { 21 | let x = Object::from(Blob::new(String::from("asdf").into_bytes())); 22 | let node = TreeNode::new(Box::new(x), 0x1234, String::from("filena.me")); 23 | 24 | match *node.object { 25 | Object::Blob(x) => { 26 | assert_eq!(x.content_length(), 4); 27 | }, 28 | _ => assert!(false) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod model; 2 | 3 | use model::Object; 4 | use model::Blob; 5 | use model::Tree; 6 | use model::hashable::Hashable; 7 | use model::tree_node::TreeNode; 8 | use std::str; 9 | 10 | use std::io; 11 | use std::io::prelude::*; 12 | use std::fs::File; 13 | 14 | 15 | fn read_object(data: Vec) -> Result { 16 | match str::from_utf8(&data[0..4]).unwrap() { 17 | "blob" => { 18 | let blub = Blob::from(data); 19 | 20 | println!("{:?}", str::from_utf8(&blub.get_content())); 21 | println!("{:?}", blub.hash()); 22 | 23 | Ok(Object::from(blub)) 24 | }, 25 | _ => { 26 | Err(String::from("unknown object")) 27 | } 28 | } 29 | } 30 | 31 | fn main() { 32 | 33 | println!("reading ./fixtures/blob.git-file.unpacked", ); 34 | let mut f = File::open("./fixtures/blob.git-file.unpacked").unwrap(); 35 | let mut buffer = Vec::new(); 36 | // read the whole file 37 | f.read_to_end(&mut buffer); 38 | 39 | println!("{:?}", read_object(buffer)); 40 | } 41 | -------------------------------------------------------------------------------- /src/model/tree.rs: -------------------------------------------------------------------------------- 1 | use model::hashable::Hashable; 2 | use model::tree_node::TreeNode; 3 | 4 | #[derive(Debug)] 5 | pub struct Tree { 6 | entries: Vec 7 | } 8 | 9 | impl Tree { 10 | pub fn new() -> Tree { 11 | Tree { entries: Vec::new() } 12 | } 13 | 14 | pub fn add(&mut self, obj: TreeNode) -> &Tree { 15 | self.entries.push(obj); 16 | self 17 | } 18 | } 19 | 20 | impl Hashable for Tree { 21 | // this is terrible slow. and not just slow, its just pure terrible 22 | fn get_hash_content(&self) -> Vec { 23 | let buffer = self.entries.iter() 24 | .fold(Vec::new(), | mut buffer, entry | { 25 | //format: [mode in ascii octal]0x20[name as utf8 string]\0x00[sha1 hash] 26 | buffer.extend_from_slice(format!("{:o}", entry.mode).as_bytes()); 27 | buffer.push(0x20); 28 | buffer.extend_from_slice(entry.name.as_bytes()); 29 | buffer.push(0x00); 30 | buffer.extend((*entry.object).hash()); 31 | 32 | return buffer; 33 | }); 34 | let mut result = format!("blob {}", buffer.len()).into_bytes(); 35 | result.push(0); 36 | result.extend(buffer); 37 | 38 | return result; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/model/hashable.rs: -------------------------------------------------------------------------------- 1 | extern crate sha1; 2 | use std::fmt::Write; 3 | use std::fmt; 4 | 5 | pub trait Hashable { 6 | /// returns the complete object content, including the metadata 7 | fn get_hash_content(&self) -> Vec; 8 | 9 | /// returns the hash for this object 10 | fn hash(&self) -> Vec { 11 | let mut sha = sha1::Sha1::new(); 12 | sha.update(&self.get_hash_content()); 13 | sha.digest() 14 | } 15 | } 16 | 17 | impl fmt::Debug for Hashable { 18 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 19 | let mut s = String::new(); 20 | for byte in self.hash() { 21 | write!(&mut s, "{:X} ", byte).unwrap(); 22 | } 23 | 24 | write!(f, "Hashable {{ hash: {} }}", s) 25 | } 26 | } 27 | 28 | #[test] 29 | pub fn test_mock() { 30 | struct HashableMock { 31 | text: String 32 | } 33 | 34 | impl Hashable for HashableMock { 35 | fn get_hash_content(&self) -> Vec { 36 | self.text.clone().into_bytes() 37 | } 38 | } 39 | 40 | let test = HashableMock { text: String::from("The quick brown fox jumps over the lazy dog") }; 41 | let expected = vec![47, 212, 225, 198, 122, 45, 40, 252, 237, 132, 158, 225, 187, 118, 231, 57, 27, 147, 235, 18]; 42 | let result = test.hash(); 43 | assert_eq!(result.len(), expected.len()); 44 | assert_eq!(result, expected); 45 | } 46 | -------------------------------------------------------------------------------- /src/model/blob.rs: -------------------------------------------------------------------------------- 1 | use model::hashable::Hashable; 2 | 3 | #[derive(Debug,Clone)] 4 | pub struct Blob { 5 | content: Vec 6 | } 7 | 8 | impl Blob { 9 | pub fn new(content: Vec) -> Blob { 10 | Blob { content: content } 11 | } 12 | 13 | pub fn content_length(&self) -> usize { 14 | self.content.len() 15 | } 16 | 17 | pub fn get_content(&self) -> Vec { 18 | self.content.clone() 19 | } 20 | 21 | pub fn get_metadata(&self) -> Vec { 22 | format!("blob {}", self.content_length()).into_bytes() 23 | } 24 | } 25 | 26 | 27 | /// this does not care if the content is a blob! use with caution 28 | /// 29 | impl From> for Blob { 30 | fn from(data: Vec) -> Blob { 31 | 32 | // find \0x00 33 | let mut start = 4; 34 | for &byte in &data[4..] { 35 | start += 1; 36 | if byte == 0 { 37 | break; 38 | } 39 | } 40 | 41 | let mut vec = Vec::new(); 42 | vec.extend_from_slice(&data[start..]); 43 | Blob::new(vec) 44 | } 45 | } 46 | 47 | impl Hashable for Blob { 48 | fn get_hash_content(&self) -> Vec { 49 | let metadata = self.get_metadata(); 50 | let content = self.get_content(); 51 | let mut vec = Vec::with_capacity(metadata.len() + content.len() + 1); 52 | vec.extend(metadata); 53 | vec.push(0x00); 54 | vec.extend(content); 55 | 56 | vec 57 | } 58 | } 59 | 60 | #[test] 61 | fn create_blob() { 62 | let x = Blob::new(String::from("asdf").into_bytes()); 63 | assert_eq!(4, x.content_length()); 64 | } 65 | 66 | #[test] 67 | fn test_metadata() { 68 | let x = Blob::new(String::from("asdf").into_bytes()); 69 | assert_eq!(String::from("blob 4").into_bytes(), x.get_metadata()); 70 | } 71 | 72 | #[test] 73 | fn hash_blob() { 74 | let test = Blob::new(String::from("The quick brown fox jumps over the lazy dog").into_bytes()); 75 | let expected = vec![255, 59, 182, 57, 72, 180, 178, 71, 150, 210, 172, 210, 89, 145, 95, 42, 157, 151, 38, 56]; 76 | let result = test.hash(); 77 | assert_eq!(result.len(),20); 78 | assert_eq!(result, expected); 79 | } 80 | 81 | #[test] 82 | fn test_blob_from() { 83 | let mut vec = Vec::new(); 84 | vec.extend_from_slice(include_bytes!("../../fixtures/blob.git-file.unpacked")); 85 | let blob = Blob::from(vec); 86 | let expect = vec![255, 235, 158, 73, 149, 185, 14, 166, 198, 50, 151, 41, 87, 111, 6, 240, 88, 208, 228, 187]; 87 | let result = blob.hash(); 88 | assert_eq!(result.len(),20); 89 | assert_eq!(result, expect); 90 | } 91 | --------------------------------------------------------------------------------