├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── src ├── bits.rs ├── builtin │ ├── add.rs │ ├── branch.rs │ ├── cat_file.rs │ ├── checkout.rs │ ├── clone.rs │ ├── commit.rs │ ├── config.rs │ ├── diff.rs │ ├── fetch.rs │ ├── hash_object.rs │ ├── init.rs │ ├── log.rs │ ├── ls_files.rs │ ├── merge.rs │ ├── mod.rs │ ├── pull.rs │ ├── push.rs │ ├── read_tree.rs │ ├── remote.rs │ ├── status.rs │ └── write_tree.rs ├── cli.rs ├── index.rs ├── main.rs ├── object.rs ├── refs.rs ├── sha1.rs ├── work_dir.rs └── zlib.rs └── tests ├── branch ├── clone_remote ├── detach_head ├── fetch_remote ├── first_commit ├── full_demo ├── merge_conflict ├── merge_fast_forward ├── merge_non_fast_forward ├── pull_remote └── push_remote /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target 3 | **/*.rs.bk 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "gitrs" 3 | version = "0.1.0" 4 | 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gitrs" 3 | version = "0.1.0" 4 | authors = ["haltode "] 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Thibault Allançon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gitrs 2 | 3 | A small re-implementation of [git](https://git-scm.com/) (a distributed version 4 | control system) written in [Rust](https://www.rust-lang.org/): 5 | 6 | - basic commands: `init`, `config`, `add`, `commit`, `status`, `diff`, `log`. 7 | - branches: `branch`, `checkout`, `merge`. 8 | - remotes: `clone`, `fetch`, `push`, `pull`, `remote`. 9 | - plumbing: `hash-object`, `cat-file`, `ls-files`, `read-tree`, `write-tree`. 10 | 11 | ### Building it 12 | 13 | ```bash 14 | $ cargo build --release 15 | $ cd target/release 16 | $ ./gitrs 17 | ``` 18 | 19 | ### Running it 20 | 21 | ```bash 22 | $ mkdir repo 23 | $ cd repo 24 | $ gitrs init 25 | Initialized empty Git repository in .git 26 | $ gitrs config --add user.name "John Doe" 27 | $ gitrs config --add user.email "john.doe@something.com" 28 | 29 | $ echo 'Hello world!' > file_a 30 | $ gitrs status 31 | new: file_a 32 | $ gitrs add file_a 33 | $ gitrs commit -m "first commit" 34 | [master 5b0cd52] first commit 35 | 36 | $ cd .. 37 | $ gitrs clone repo copy 38 | Initialized empty Git repository in copy/.git 39 | Count: 3 objects 40 | From: /home/haltode/repo 41 | Fast-forward 42 | Cloning into copy 43 | $ cp repo/.git/config copy/.git/config 44 | $ cd copy 45 | $ echo 'new file' > file_b 46 | $ gitrs add file_b 47 | $ gitrs commit -m "second commit" 48 | [master 3940393] second commit 49 | 50 | $ cd ../repo 51 | $ gitrs remote add copy_remote ../copy 52 | $ gitrs branch new_b 53 | $ gitrs pull copy_remote master 54 | Count: 3 objects 55 | From: ../copy 56 | Fast-forward 57 | 58 | $ gitrs checkout new_b 59 | Switched to branch new_b 60 | $ echo 'new line' >> file_a 61 | $ gitrs status 62 | modified: file_a 63 | $ gitrs diff 64 | file_a: 65 | Hello world! 66 | +new_line 67 | $ gitrs add file_a 68 | $ gitrs commit -m "third commit" 69 | [new_b 9e4e36b] third commit 70 | $ gitrs checkout master 71 | Switched to branch master 72 | $ gitrs merge new_b 73 | Merge new_b into master 74 | [master 7b8e051] Merge new_b into master 75 | ``` 76 | 77 | ## Resources used 78 | 79 | - [Pro Git book](https://git-scm.com/book/en/v2) 80 | - [Git docs](https://git-scm.com/docs) 81 | - [gitcore-tutorial](https://git-scm.com/docs/gitcore-tutorial) 82 | - [gitrepository-layout](https://git-scm.com/docs/gitrepository-layout) 83 | - [Git user manual](https://git-scm.com/docs/user-manual.html) 84 | - [Git from the bottom up](https://jwiegley.github.io/git-from-the-bottom-up/) 85 | - [The curious coder’s guide to git](https://matthew-brett.github.io/curious-git/) 86 | 87 | ## Why? 88 | 89 | I wanted a fun project to learn more about the Rust programming language and git 90 | inner workings at the same time. The sole purpose of this project is 91 | educational. As a challenge I also restricted myself to the Rust standard 92 | library, thus re-implementing everything else that I might need such as: sha-1 93 | hash function, zlib compress/decompress functions, etc. This is absurd and 94 | definitely not good practice, but again the only aim was to learn, so every 95 | opportunity is a great excuse to code in Rust! 96 | -------------------------------------------------------------------------------- /src/bits.rs: -------------------------------------------------------------------------------- 1 | pub mod big_endian { 2 | pub fn u64_to_u8(x: u64) -> [u8; 8] { 3 | [ 4 | ((x >> 56) & 0xff) as u8, 5 | ((x >> 48) & 0xff) as u8, 6 | ((x >> 40) & 0xff) as u8, 7 | ((x >> 32) & 0xff) as u8, 8 | ((x >> 24) & 0xff) as u8, 9 | ((x >> 16) & 0xff) as u8, 10 | ((x >> 8) & 0xff) as u8, 11 | (x & 0xff) as u8, 12 | ] 13 | } 14 | 15 | pub fn u32_to_u8(x: u32) -> [u8; 4] { 16 | [ 17 | ((x >> 24) & 0xff) as u8, 18 | ((x >> 16) & 0xff) as u8, 19 | ((x >> 8) & 0xff) as u8, 20 | (x & 0xff) as u8, 21 | ] 22 | } 23 | 24 | pub fn u16_to_u8(x: u16) -> [u8; 2] { 25 | [((x >> 8) & 0xff) as u8, (x & 0xff) as u8] 26 | } 27 | 28 | pub fn u8_to_u32(x: [u8; 4]) -> u32 { 29 | (x[0] as u32) << 24 | (x[1] as u32) << 16 | (x[2] as u32) << 8 | (x[3] as u32) 30 | } 31 | 32 | pub fn u8_slice_to_u32(x: &[u8]) -> u32 { 33 | u8_to_u32([x[0], x[1], x[2], x[3]]) 34 | } 35 | 36 | pub fn u8_to_u16(x: [u8; 2]) -> u16 { 37 | (x[0] as u16) << 8 | (x[1] as u16) 38 | } 39 | 40 | pub fn u8_slice_to_u16(x: &[u8]) -> u16 { 41 | u8_to_u16([x[0], x[1]]) 42 | } 43 | 44 | pub fn u8_slice_to_usize(x: &[u8]) -> usize { 45 | let mut res = 0 as usize; 46 | for &e in x { 47 | res <<= 8; 48 | res += e as usize; 49 | } 50 | return res; 51 | } 52 | } 53 | 54 | pub mod little_endian { 55 | pub fn u16_to_u8(x: u16) -> [u8; 2] { 56 | [(x & 0xff) as u8, ((x >> 8) & 0xff) as u8] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/builtin/add.rs: -------------------------------------------------------------------------------- 1 | use index; 2 | 3 | pub fn cmd_add(args: &[String]) { 4 | if let Err(why) = add(args) { 5 | println!("Could not add paths: {:?}", why); 6 | } 7 | } 8 | 9 | fn add(paths: &[String]) -> Result<(), index::Error> { 10 | let mut entries = index::read_entries()?; 11 | entries.retain(|e| !paths.contains(&e.path)); 12 | 13 | for path in paths { 14 | let entry = index::Entry::new(path)?; 15 | entries.push(entry); 16 | } 17 | 18 | index::write_entries(entries)?; 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /src/builtin/branch.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io; 3 | use std::path::Path; 4 | 5 | use cli; 6 | use refs; 7 | 8 | #[derive(Debug)] 9 | pub enum Error { 10 | HEADNotPointingToCommit, 11 | IoError(io::Error), 12 | } 13 | 14 | impl From for Error { 15 | fn from(e: io::Error) -> Error { 16 | Error::IoError(e) 17 | } 18 | } 19 | 20 | pub fn cmd_branch(args: &[String], flags: &[String]) { 21 | let accepted_flags = ["--list", "-l"]; 22 | if cli::has_known_flags(flags, &accepted_flags) { 23 | let default_val = String::new(); 24 | let name = args.get(0).unwrap_or(&default_val); 25 | let flag = flags.get(0).unwrap_or(&default_val); 26 | if let Err(why) = branch(name, flag) { 27 | println!("Could not use branch: {:?}", why); 28 | } 29 | } 30 | } 31 | 32 | fn branch(name: &str, flag: &str) -> Result<(), Error> { 33 | let cur_branch = refs::read_ref("HEAD")?; 34 | 35 | if flag == "--list" || flag == "-l" || name.is_empty() { 36 | let refs_dir = Path::new(".git").join("refs").join("heads"); 37 | for entry in fs::read_dir(refs_dir)? { 38 | let path = entry?.path(); 39 | if path.is_file() { 40 | let file_name = match path.file_name() { 41 | Some(p) => p.to_str().unwrap(), 42 | None => continue, 43 | }; 44 | if file_name == cur_branch { 45 | println!("* {}", file_name); 46 | } else { 47 | println!(" {}", file_name); 48 | } 49 | } 50 | } 51 | } else if !name.is_empty() { 52 | let cur_hash = refs::get_ref_hash("HEAD")?; 53 | if cur_hash.is_empty() { 54 | return Err(Error::HEADNotPointingToCommit); 55 | } 56 | 57 | refs::write_to_ref(name, &cur_hash)?; 58 | } 59 | 60 | Ok(()) 61 | } 62 | -------------------------------------------------------------------------------- /src/builtin/cat_file.rs: -------------------------------------------------------------------------------- 1 | use std::str; 2 | 3 | use builtin::read_tree; 4 | use cli; 5 | use object; 6 | use object::Object; 7 | 8 | #[derive(Debug)] 9 | pub enum Error { 10 | ObjectError(object::Error), 11 | TreeError(read_tree::Error), 12 | } 13 | 14 | pub fn cmd_cat_file(args: &[String], flags: &[String]) { 15 | let accepted_flags = ["--type", "-t", "--size", "-s", "--print", "-p"]; 16 | if cli::has_known_flags(flags, &accepted_flags) { 17 | if args.is_empty() || flags.is_empty() { 18 | println!("cat-file: command takes 'hash' and 'mode' as arguments."); 19 | } else { 20 | let hash_prefix = &args[0]; 21 | let mode = &flags[0]; 22 | if let Err(why) = cat_file(hash_prefix, mode) { 23 | println!("Cannot retrieve object info: {:?}", why); 24 | } 25 | } 26 | } 27 | } 28 | 29 | pub fn cat_file(hash_prefix: &str, mode: &str) -> Result<(), Error> { 30 | let object = Object::new(hash_prefix).map_err(Error::ObjectError)?; 31 | match mode { 32 | "--type" | "-t" => println!("{}", object.obj_type), 33 | "--size" | "-s" => println!("{}", object.obj_size), 34 | "--print" | "-p" => match object.obj_type.as_str() { 35 | "blob" | "commit" => { 36 | let data = str::from_utf8(&object.data).unwrap(); 37 | println!("{}", data); 38 | } 39 | "tree" => { 40 | let entries = read_tree::read_tree(hash_prefix).map_err(Error::TreeError)?; 41 | for entry in entries { 42 | println!("{:o} {} {}", entry.mode, entry.hash, entry.path); 43 | } 44 | } 45 | tp => println!("unknown object type: {}", tp), 46 | }, 47 | _ => unreachable!(), 48 | } 49 | 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /src/builtin/checkout.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::str; 3 | 4 | use builtin::status; 5 | use object; 6 | use object::Object; 7 | use refs; 8 | use work_dir; 9 | 10 | #[derive(Debug)] 11 | pub enum Error { 12 | AlreadyOnIt, 13 | IoError(io::Error), 14 | ObjectError(object::Error), 15 | ReferenceNotACommit, 16 | WorkDirError(work_dir::Error), 17 | WorkDirNotClean, 18 | } 19 | 20 | impl From for Error { 21 | fn from(e: io::Error) -> Error { 22 | Error::IoError(e) 23 | } 24 | } 25 | 26 | pub fn cmd_checkout(args: &[String]) { 27 | if args.is_empty() { 28 | println!("checkout: command takes a 'ref' argument."); 29 | } else { 30 | let ref_name = &args[0]; 31 | if let Err(why) = checkout(ref_name) { 32 | println!("Could not checkout: {:?}", why); 33 | } 34 | } 35 | } 36 | 37 | fn checkout(ref_name: &str) -> Result<(), Error> { 38 | if !status::is_clean_work_dir() { 39 | return Err(Error::WorkDirNotClean); 40 | } 41 | 42 | let will_detach_head = !refs::is_branch(&ref_name); 43 | let commit = match will_detach_head { 44 | true => ref_name.to_string(), 45 | false => refs::get_ref_hash(&ref_name)?, 46 | }; 47 | 48 | let object = Object::new(&commit).map_err(Error::ObjectError)?; 49 | if object.obj_type != "commit" { 50 | return Err(Error::ReferenceNotACommit); 51 | } 52 | 53 | let head = refs::get_ref_hash("HEAD")?; 54 | if ref_name == head { 55 | return Err(Error::AlreadyOnIt); 56 | } 57 | 58 | work_dir::update_from_commit(&commit).map_err(Error::WorkDirError)?; 59 | refs::write_to_ref("HEAD", ref_name)?; 60 | 61 | if will_detach_head { 62 | println!("Note: checking out {}", ref_name); 63 | println!("You are in detached HEAD state."); 64 | } else { 65 | println!("Switched to branch {}", ref_name); 66 | } 67 | 68 | Ok(()) 69 | } 70 | -------------------------------------------------------------------------------- /src/builtin/clone.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs; 3 | use std::io; 4 | use std::path::Path; 5 | 6 | use builtin::init; 7 | use builtin::pull; 8 | use builtin::remote; 9 | use refs; 10 | 11 | #[derive(Debug)] 12 | pub enum Error { 13 | DirectoryAlreadyExists, 14 | IoError(io::Error), 15 | NotAGitRepository, 16 | PullError(pull::Error), 17 | } 18 | 19 | impl From for Error { 20 | fn from(e: io::Error) -> Error { 21 | Error::IoError(e) 22 | } 23 | } 24 | 25 | pub fn cmd_clone(args: &[String]) { 26 | if args.len() < 2 { 27 | println!("clone: takes 'repository' and 'directory' arguments"); 28 | } else { 29 | let repository = &args[0]; 30 | let directory = &args[1]; 31 | if let Err(why) = clone(&repository, &directory) { 32 | println!("Could not clone: {:?}", why); 33 | } 34 | } 35 | } 36 | 37 | fn clone(repository: &str, directory: &str) -> Result<(), Error> { 38 | let repo_path = Path::new(&repository); 39 | let dir_path = Path::new(&directory); 40 | 41 | if !repo_path.join(".git").exists() { 42 | return Err(Error::NotAGitRepository); 43 | } 44 | if dir_path.exists() { 45 | return Err(Error::DirectoryAlreadyExists); 46 | } 47 | 48 | init::init(&directory)?; 49 | 50 | let absolute_repo_path = fs::canonicalize(&repo_path)?; 51 | let absolute_dir_path = fs::canonicalize(&dir_path)?; 52 | 53 | env::set_current_dir(&absolute_repo_path)?; 54 | let has_commits = refs::get_ref_hash("HEAD").is_ok(); 55 | 56 | env::set_current_dir(&absolute_dir_path)?; 57 | remote::add_remote("origin", absolute_repo_path.to_str().unwrap())?; 58 | if has_commits { 59 | pull::pull("origin", "master").map_err(Error::PullError)?; 60 | } 61 | 62 | println!("Cloning into {}", directory); 63 | Ok(()) 64 | } 65 | -------------------------------------------------------------------------------- /src/builtin/commit.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io; 3 | use std::path::Path; 4 | use std::str; 5 | use std::time::{SystemTime, UNIX_EPOCH}; 6 | 7 | use builtin::config; 8 | use builtin::hash_object; 9 | use builtin::write_tree; 10 | use cli; 11 | use object; 12 | use object::Object; 13 | use refs; 14 | 15 | #[derive(Debug)] 16 | pub enum Error { 17 | CannotGetParentFromCommit, 18 | CannotGetTreeFromCommit, 19 | IoError(io::Error), 20 | NoCommonAncestor, 21 | NothingToCommit, 22 | ObjectError(object::Error), 23 | TreeError(write_tree::Error), 24 | UserConfigIncomplete, 25 | } 26 | 27 | impl From for Error { 28 | fn from(e: io::Error) -> Error { 29 | Error::IoError(e) 30 | } 31 | } 32 | 33 | pub fn cmd_commit(args: &[String], flags: &[String]) { 34 | let accepted_flags = ["--message", "-m"]; 35 | if cli::has_known_flags(flags, &accepted_flags) { 36 | if cli::has_flag(flags, "--message", "-m") { 37 | let message = &args[0]; 38 | if let Err(why) = commit(message) { 39 | println!("Could not commit: {:?}", why); 40 | } 41 | } else { 42 | println!("commit: command needs a '--message' flag."); 43 | } 44 | } 45 | } 46 | 47 | pub fn commit(message: &str) -> Result { 48 | let user = config::Config::new()?; 49 | if user.name.is_empty() || user.email.is_empty() { 50 | return Err(Error::UserConfigIncomplete); 51 | } 52 | let author = format!("{} <{}>", user.name, user.email); 53 | 54 | let commit_tree = write_tree::write_tree().map_err(Error::TreeError)?; 55 | let mut parents = Vec::new(); 56 | 57 | let head = refs::read_ref("HEAD")?; 58 | let has_commits = refs::exists_ref(&head) || refs::is_detached_head(); 59 | if has_commits { 60 | let cur_commit = match refs::get_ref_hash(&head) { 61 | Ok(r) => r, 62 | Err(_) => head.to_string(), 63 | }; 64 | parents.push(cur_commit.to_string()); 65 | 66 | let merge_head = Path::new(".git").join("MERGE_HEAD"); 67 | let is_in_merge = merge_head.exists(); 68 | if is_in_merge { 69 | let mut merge_parent = fs::read_to_string(&merge_head)?; 70 | // Remove '\n' character 71 | merge_parent.pop(); 72 | parents.push(merge_parent); 73 | fs::remove_file(&merge_head)?; 74 | } 75 | 76 | let cur_hash = get_tree_hash(&cur_commit)?; 77 | if commit_tree == cur_hash && !is_in_merge { 78 | println!("On {}", head); 79 | println!("nothing to commit, working tree clean"); 80 | return Err(Error::NothingToCommit); 81 | } 82 | } 83 | 84 | let mut header = format!("tree {}", commit_tree); 85 | for parent in parents { 86 | header.push_str(&format!("\nparent {}", parent)); 87 | } 88 | 89 | let start = SystemTime::now(); 90 | let timestamp = start.duration_since(UNIX_EPOCH).unwrap(); 91 | // I decide where you live, alright?! 92 | let timezone = "+0200"; 93 | let time = format!("{} {}", timestamp.as_secs(), timezone); 94 | 95 | let commit_content = format!( 96 | "{}\n\ 97 | author {} {}\n\ 98 | committer {} {}\n\n\ 99 | {}\n", 100 | header, author, time, author, time, message 101 | ); 102 | 103 | let write = true; 104 | let hash = hash_object::hash_object(commit_content.as_bytes(), "commit", write)?; 105 | 106 | let ref_path = match refs::is_detached_head() { 107 | true => Path::new(".git").join("HEAD"), 108 | false => Path::new(".git").join("refs").join("heads").join(&head), 109 | }; 110 | fs::write(ref_path, format!("{}\n", hash))?; 111 | 112 | println!("[{} {}] {}", head, &hash[..7], message); 113 | Ok(hash) 114 | } 115 | 116 | pub fn get_tree_hash(commit: &str) -> Result { 117 | let object = Object::new(&commit).map_err(Error::ObjectError)?; 118 | let data = str::from_utf8(&object.data).unwrap(); 119 | if !data.starts_with("tree ") || data.len() < 45 { 120 | return Err(Error::CannotGetTreeFromCommit); 121 | } 122 | let tree = data[5..45].to_string(); 123 | Ok(tree) 124 | } 125 | 126 | pub fn get_parents_hashes(commit: &str) -> Result, Error> { 127 | let object = Object::new(&commit).map_err(Error::ObjectError)?; 128 | let data = str::from_utf8(&object.data).unwrap(); 129 | 130 | let mut parents = Vec::new(); 131 | let prefix = "parent "; 132 | for line in data.lines().filter(|l| l.starts_with(prefix)) { 133 | if line.len() != prefix.len() + 40 { 134 | return Err(Error::CannotGetParentFromCommit); 135 | } 136 | 137 | let start = prefix.len(); 138 | let end = start + 40; 139 | let hash = &line[start..end]; 140 | parents.push(hash.to_string()); 141 | } 142 | 143 | Ok(parents) 144 | } 145 | 146 | fn get_ancestors(commit: &str) -> Result, Error> { 147 | let mut ancestors = Vec::new(); 148 | for parent in get_parents_hashes(&commit)? { 149 | ancestors.push(parent.to_string()); 150 | ancestors.extend(get_ancestors(&parent)?); 151 | } 152 | Ok(ancestors) 153 | } 154 | 155 | pub fn is_ancestor(commit1: &str, commit2: &str) -> bool { 156 | let commit1_ancestors = match get_ancestors(&commit1) { 157 | Ok(a) => a, 158 | Err(_) => return false, 159 | }; 160 | 161 | commit1_ancestors.contains(&commit2.to_string()) 162 | } 163 | 164 | pub fn lowest_common_ancestor(commit1: &str, commit2: &str) -> Result { 165 | let commit1_ancestors = get_ancestors(&commit1)?; 166 | let commit2_ancestors = get_ancestors(&commit2)?; 167 | 168 | for ancestor in commit1_ancestors { 169 | if commit2_ancestors.contains(&ancestor) { 170 | return Ok(ancestor); 171 | } 172 | } 173 | 174 | Err(Error::NoCommonAncestor) 175 | } 176 | -------------------------------------------------------------------------------- /src/builtin/config.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io; 3 | use std::path::Path; 4 | 5 | use builtin::remote; 6 | use cli; 7 | 8 | #[derive(Debug)] 9 | pub struct Config { 10 | pub name: String, 11 | pub email: String, 12 | pub remotes: Vec, 13 | } 14 | 15 | impl Config { 16 | pub fn new() -> io::Result { 17 | let mut name = String::new(); 18 | let mut email = String::new(); 19 | let mut remotes = Vec::new(); 20 | let mut cur_section = String::new(); 21 | 22 | let config_file = Path::new(".git").join("config"); 23 | if config_file.exists() { 24 | let data = fs::read_to_string(config_file)?; 25 | for line in data.lines().map(|l| l.trim()) { 26 | if line.starts_with("[") && line.ends_with("]") { 27 | cur_section = line.to_string(); 28 | } 29 | 30 | let elem: Vec<&str> = line.split('=').collect(); 31 | if elem.len() != 2 { 32 | continue; 33 | } 34 | 35 | // [section] 36 | // var = value 37 | let var = elem[0].trim().to_string(); 38 | let value = elem[1].trim().to_string(); 39 | if var == "name" { 40 | name = value; 41 | } else if var == "email" { 42 | email = value; 43 | } else if var == "url" { 44 | let (_, remote_name) = parse_section_name(&cur_section); 45 | remotes.push(remote::Remote { 46 | name: remote_name.to_string(), 47 | url: value, 48 | }); 49 | } 50 | } 51 | } 52 | 53 | Ok(Config { 54 | name: name, 55 | email: email, 56 | remotes: remotes, 57 | }) 58 | } 59 | 60 | pub fn add(&mut self, section: &str, subsection: &str, value: &str) { 61 | match section { 62 | "user" => match subsection { 63 | "name" => self.name = value.to_string(), 64 | "email" => self.email = value.to_string(), 65 | sub => println!("config: unknown user subsection '{}'", sub), 66 | }, 67 | "remote" => { 68 | self.remotes.push(remote::Remote { 69 | name: subsection.to_string(), 70 | url: value.to_string(), 71 | }); 72 | } 73 | sct => println!("config: unknown section '{}'", sct), 74 | } 75 | } 76 | 77 | pub fn get(&self, section: &str, subsection: &str) -> Option { 78 | match section { 79 | "user" => match subsection { 80 | "name" => return Some(self.name.to_string()), 81 | "email" => return Some(self.email.to_string()), 82 | sub => println!("config: unknown user subsection '{}'", sub), 83 | }, 84 | "remote" => { 85 | for remote in &self.remotes { 86 | if remote.name == subsection { 87 | return Some(remote.url.to_string()); 88 | } 89 | } 90 | println!("config: unknown remote '{}'", subsection); 91 | } 92 | sct => println!("config: unknown section '{}'", sct), 93 | } 94 | 95 | None 96 | } 97 | 98 | pub fn unset(&mut self, section: &str, subsection: &str) { 99 | match section { 100 | "user" => match subsection { 101 | "name" => self.name = String::new(), 102 | "email" => self.email = String::new(), 103 | sub => println!("config: unknown user subsection '{}'", sub), 104 | }, 105 | "remote" => self.remotes.retain(|r| r.name != subsection), 106 | sct => println!("config: unknown section '{}'", sct), 107 | } 108 | } 109 | 110 | pub fn write_config(&self) -> io::Result<()> { 111 | let mut config_fmt = format!("[user]\n\tname = {}\n\temail = {}\n", self.name, self.email); 112 | for remote in &self.remotes { 113 | let remote_entry = format!("[remote \"{}\"]\n\turl = {}\n", remote.name, remote.url); 114 | config_fmt = format!("{}{}", config_fmt, remote_entry); 115 | } 116 | 117 | let config_file = Path::new(".git").join("config"); 118 | fs::write(config_file, config_fmt)?; 119 | Ok(()) 120 | } 121 | 122 | pub fn is_empty(&self) -> bool { 123 | self.name.is_empty() && self.email.is_empty() && self.remotes.is_empty() 124 | } 125 | } 126 | 127 | pub fn cmd_config(args: &[String], flags: &[String]) { 128 | let accepted_flags = ["--add", "--get", "--unset", "--list"]; 129 | if cli::has_known_flags(flags, &accepted_flags) { 130 | if flags.is_empty() { 131 | println!("config: command takes option such as '--add', '--list', etc."); 132 | } else { 133 | let default_val = String::new(); 134 | let section = args.get(0).unwrap_or(&default_val); 135 | let value = args.get(1).unwrap_or(&default_val); 136 | let option = &flags[0][2..]; 137 | 138 | if let Err(why) = config(option, section, value) { 139 | println!("Could not use config file: {:?}", why); 140 | } 141 | } 142 | } 143 | } 144 | 145 | pub fn config(option: &str, section: &str, value: &str) -> io::Result<()> { 146 | let mut user = Config::new()?; 147 | let (section, subsection) = parse_section_name(section); 148 | match option { 149 | "add" => user.add(§ion, &subsection, &value), 150 | "get" => { 151 | if let Some(val) = user.get(§ion, &subsection) { 152 | println!("{}", val); 153 | } 154 | } 155 | "unset" => user.unset(§ion, &subsection), 156 | "list" => { 157 | println!("user.name = {}", user.name); 158 | println!("user.email = {}", user.email); 159 | for remote in &user.remotes { 160 | println!("remote.{} = {}", remote.name, remote.url); 161 | } 162 | } 163 | _ => unreachable!(), 164 | } 165 | 166 | let is_modif = option == "add" || option == "unset"; 167 | if is_modif { 168 | user.write_config()?; 169 | } 170 | 171 | Ok(()) 172 | } 173 | 174 | fn parse_section_name(section: &str) -> (String, String) { 175 | match section.starts_with("[") && section.ends_with("]") { 176 | // [section "subsection"] or [section] 177 | true => { 178 | let section = §ion[1..section.len() - 1]; 179 | let space_idx = match section.find(' ') { 180 | Some(i) => i, 181 | None => return (section.to_string(), String::new()), 182 | }; 183 | let (section, subsection) = section.split_at(space_idx); 184 | let subsection = &subsection[2..subsection.len() - 1]; 185 | return (section.to_string(), subsection.to_string()); 186 | } 187 | 188 | // section.subsection or section 189 | false => { 190 | let dot_idx = match section.find('.') { 191 | Some(i) => i, 192 | None => return (section.to_string(), String::new()), 193 | }; 194 | let (section, subsection) = section.split_at(dot_idx); 195 | let mut subsection = &subsection[1..]; 196 | // If input is section.subsection.variable, ignore variable 197 | if let Some(dot_idx) = subsection.find('.') { 198 | subsection = &subsection[..dot_idx]; 199 | } 200 | return (section.to_string(), subsection.to_string()); 201 | } 202 | } 203 | } 204 | 205 | #[cfg(test)] 206 | mod tests { 207 | use builtin::config::parse_section_name; 208 | 209 | #[test] 210 | fn section_parser() { 211 | let exp = (String::from("user"), String::new()); 212 | assert_eq!(exp, parse_section_name("[user]")); 213 | 214 | let exp = (String::from("user"), String::from("name")); 215 | assert_eq!(exp, parse_section_name("user.name")); 216 | 217 | let exp = (String::from("remote"), String::from("name")); 218 | assert_eq!(exp, parse_section_name("[remote \"name\"]")); 219 | 220 | let exp = (String::from("remote"), String::from("name")); 221 | assert_eq!(exp, parse_section_name("remote.name")); 222 | 223 | let exp = (String::from("remote"), String::from("name")); 224 | assert_eq!(exp, parse_section_name("remote.name.url")); 225 | 226 | let exp = (String::from("single"), String::new()); 227 | assert_eq!(exp, parse_section_name("single")); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/builtin/diff.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | use std::fs; 3 | use std::io; 4 | use std::str; 5 | 6 | use index; 7 | use object; 8 | use object::Object; 9 | 10 | #[derive(Debug)] 11 | pub enum Error { 12 | IndexError(index::Error), 13 | IoError(io::Error), 14 | ObjectError(object::Error), 15 | } 16 | 17 | enum State { 18 | Ins, 19 | Del, 20 | Eq, 21 | } 22 | 23 | pub fn cmd_diff(args: &[String]) { 24 | if let Err(why) = diff(args) { 25 | println!("Could not show diff: {:?}", why); 26 | } 27 | } 28 | 29 | fn diff(paths: &[String]) -> Result<(), Error> { 30 | let entries = index::read_entries().map_err(Error::IndexError)?; 31 | for entry in &entries { 32 | let path = &entry.path; 33 | if !paths.is_empty() && !paths.contains(path) { 34 | continue; 35 | } 36 | 37 | let object = Object::new(&entry.hash).map_err(Error::ObjectError)?; 38 | if object.obj_type != "blob" { 39 | continue; 40 | } 41 | 42 | let stored_data = str::from_utf8(&object.data).unwrap(); 43 | let actual_data = fs::read_to_string(path).map_err(Error::IoError)?; 44 | 45 | let stored_lines: Vec<&str> = stored_data.split('\n').collect(); 46 | let actual_lines: Vec<&str> = actual_data.split('\n').collect(); 47 | if stored_lines == actual_lines { 48 | continue; 49 | } 50 | 51 | println!("{}:", path); 52 | for (state, line) in lcs_diff(&stored_lines, &actual_lines) { 53 | let c = match state { 54 | State::Ins => '+', 55 | State::Del => '-', 56 | State::Eq => ' ', 57 | }; 58 | println!("{}{}", c, line); 59 | } 60 | } 61 | 62 | Ok(()) 63 | } 64 | 65 | fn lcs_diff(a: &[&str], b: &[&str]) -> Vec<(State, String)> { 66 | let mut res = Vec::new(); 67 | let lcs = longest_common_subseq(a, b); 68 | let mut i = a.len(); 69 | let mut j = b.len(); 70 | loop { 71 | if i > 0 && j > 0 && a[i - 1] == b[j - 1] { 72 | res.push((State::Eq, a[i - 1].to_string())); 73 | i -= 1; 74 | j -= 1; 75 | } else if j > 0 && (i == 0 || lcs[i][j - 1] >= lcs[i - 1][j]) { 76 | res.push((State::Ins, b[j - 1].to_string())); 77 | j -= 1; 78 | } else if i > 0 && (j == 0 || lcs[i][j - 1] < lcs[i - 1][j]) { 79 | res.push((State::Del, a[i - 1].to_string())); 80 | i -= 1; 81 | } else { 82 | break; 83 | } 84 | } 85 | res.reverse(); 86 | return res; 87 | } 88 | 89 | fn longest_common_subseq(a: &[&str], b: &[&str]) -> Vec> { 90 | let m = a.len() + 1; 91 | let n = b.len() + 1; 92 | let mut lcs = vec![vec![0u32; n]; m]; 93 | for i in 1..m { 94 | for j in 1..n { 95 | if a[i - 1] == b[j - 1] { 96 | lcs[i][j] = lcs[i - 1][j - 1] + 1; 97 | } else { 98 | lcs[i][j] = cmp::max(lcs[i][j - 1], lcs[i - 1][j]); 99 | } 100 | } 101 | } 102 | return lcs; 103 | } 104 | -------------------------------------------------------------------------------- /src/builtin/fetch.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs; 3 | use std::io; 4 | use std::path::Path; 5 | 6 | use builtin::config; 7 | use builtin::hash_object; 8 | use builtin::remote; 9 | use object; 10 | use refs; 11 | 12 | #[derive(Debug)] 13 | pub enum Error { 14 | AlreadyUpToDate, 15 | IoError(io::Error), 16 | ObjectError(object::Error), 17 | RemoteNotAGitRepo, 18 | RemoteNotFound, 19 | } 20 | 21 | impl From for Error { 22 | fn from(e: io::Error) -> Error { 23 | Error::IoError(e) 24 | } 25 | } 26 | 27 | pub fn cmd_fetch(args: &[String]) { 28 | if args.len() < 2 { 29 | println!("fetch: takes 'remote' and 'branch' arguments"); 30 | } else { 31 | let remote = &args[0]; 32 | let branch = &args[1]; 33 | if let Err(why) = fetch(&remote, &branch) { 34 | println!("Could not fetch: {:?}", why); 35 | } 36 | } 37 | } 38 | 39 | pub fn fetch(remote: &str, branch: &str) -> Result<(), Error> { 40 | let user = config::Config::new()?; 41 | let url = match user.remotes.iter().find(|r| r.name == remote) { 42 | Some(r) => r.url.to_string(), 43 | None => return Err(Error::RemoteNotFound), 44 | }; 45 | 46 | let local_dir = env::current_dir()?; 47 | let local_hash = refs::get_ref_hash(&branch)?; 48 | let remote_dir = Path::new(&url); 49 | 50 | env::set_current_dir(&remote_dir)?; 51 | if !Path::new(".git").exists() { 52 | return Err(Error::RemoteNotAGitRepo); 53 | } 54 | let remote_hash = refs::get_ref_hash(&branch)?; 55 | if local_hash == remote_hash { 56 | return Err(Error::AlreadyUpToDate); 57 | } 58 | 59 | let missing = remote::find_remote_missing_objects(&remote_hash, &local_hash); 60 | for obj_hash in &missing { 61 | let obj = object::Object::new(&obj_hash).map_err(Error::ObjectError)?; 62 | 63 | env::set_current_dir(&local_dir)?; 64 | let write = true; 65 | hash_object::hash_object(&obj.data, &obj.obj_type, write)?; 66 | env::set_current_dir(&remote_dir)?; 67 | } 68 | env::set_current_dir(&local_dir)?; 69 | 70 | let rem_dir = Path::new(".git").join("refs").join("remotes").join(&remote); 71 | fs::create_dir_all(&rem_dir)?; 72 | fs::write(rem_dir.join(&branch), format!("{}\n", remote_hash))?; 73 | 74 | let fetch_head = Path::new(".git").join("FETCH_HEAD"); 75 | fs::write( 76 | fetch_head, 77 | format!("{} branch '{}' of {}\n", remote_hash, branch, url), 78 | )?; 79 | 80 | println!("Count: {} objects", missing.len()); 81 | println!("From: {}", url); 82 | Ok(()) 83 | } 84 | -------------------------------------------------------------------------------- /src/builtin/hash_object.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io; 3 | use std::path::Path; 4 | 5 | use cli; 6 | use sha1; 7 | use zlib; 8 | 9 | pub fn cmd_hash_object(args: &[String], flags: &[String]) { 10 | let accepted_flags = ["--type", "-t", "--write", "-w"]; 11 | if cli::has_known_flags(flags, &accepted_flags) { 12 | if args.is_empty() { 13 | println!("hash-object: command takes a 'data' argument."); 14 | } else { 15 | let data = &args[0].as_bytes(); 16 | let obj_type = match cli::has_flag(flags, "--type", "-t") { 17 | true => match &args.get(1) { 18 | Some(t) => t, 19 | None => { 20 | println!("hash-object: missing 'type' argument."); 21 | return; 22 | } 23 | }, 24 | false => "blob", 25 | }; 26 | let write = cli::has_flag(&flags, "--write", "-w"); 27 | 28 | match hash_object(data, &obj_type, write) { 29 | Ok(hash) => println!("{}", hash), 30 | Err(why) => println!("Cannot hash object: {:?}", why), 31 | } 32 | } 33 | } 34 | } 35 | 36 | pub fn hash_object(data: &[u8], obj_type: &str, write: bool) -> io::Result { 37 | let header = format!("{} {}", obj_type, data.len()); 38 | let mut full_data = Vec::new(); 39 | full_data.extend(header.as_bytes()); 40 | full_data.push(0); 41 | full_data.extend(data); 42 | 43 | let hash = sha1::sha1(&full_data); 44 | 45 | if write { 46 | let obj_dir = Path::new(".git").join("objects").join(&hash[..2]); 47 | let obj_path = Path::new(&obj_dir).join(&hash[2..]); 48 | 49 | if !obj_path.exists() { 50 | fs::create_dir_all(&obj_dir)?; 51 | let compressed_data = zlib::compress(full_data); 52 | fs::write(&obj_path, &compressed_data)?; 53 | } 54 | } 55 | 56 | Ok(hash) 57 | } 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use builtin::hash_object::hash_object; 62 | 63 | #[test] 64 | fn short() { 65 | let res = hash_object("this is a test!".as_bytes(), "blob", false).unwrap(); 66 | assert_eq!("ca8d93e91ccd585c740d9a483ab11c428eb085f2", &res); 67 | } 68 | 69 | #[test] 70 | fn long() { 71 | let res = hash_object( 72 | "i3Kyc3VSFY359Szkg9q0thD6aR5pfmn7z2gWexqC0KwM8odmUUei4qFFbMnbm4\ 73 | yhUt5oBWvHv5DoeEvivf2Fq4hp5YsvdccpyyPErD9OPfQBHdrOF6DuMMRXwDuIS\ 74 | bKMMoFYm3XOsA8za5YA5rqV4Pm699eSPYt15ymlRpJ3VPJhUBr34qRZRTxX4Q3m\ 75 | ro7mUXVJPwwJJv44Svs3BwxGsA3EQuddSz1kKEp7JJWWUzBVFaKdTE79soRZr9i\ 76 | eg5976Y1QEZ901aUaO0zfd8V09dWhvM53W0jyMkw4DlLBZlPXXKGjrVCmqwFJ7O\ 77 | 2LyVtiewFZ2uS8YHdftRr2eiVIisVrGoenqpXKBUkng32L3WSJ3Gs6chIshWbKp\ 78 | bLhXmM7JjsioxqtnfA8Vwmhr3IGIYLSyOROv9JPMiwnaBdYpFwGc585ZaEKY1qe\ 79 | IzQbHAleXzh9bMBPG91gkgs4jcNJvRlnzPNZ74fVFKmnf29Ue4UcUNVKDe2cVPq\ 80 | nEmGhLcj6BzJ61CxiJUC4sZqhTtYZGbkV4Rb8FgwVRQUqRq5Zsb1Kh8eYbcyHRV\ 81 | N3ih6wJWruBxixGAMLIseURVEUBnRc3nkYCMVsgkwRVevo8Ehp60Ih7eF4sarMX\ 82 | 6EH8caKYIv5A3SE6Owb6dQqYrbOL7EgXNOnCIwQxhz0aw2p4AYmHC22so8rfGbN\ 83 | C1I95RXd9g38Xg4fm8AJORNGsEx0mVLy6GFLuiZ6KxNXci6wPg2BnZj5Pwg4ywT\ 84 | yuPeiOI1ooBwlNDLqqFxUVzfHeVpVila3PyrMrMSMq0CV" 85 | .as_bytes(), 86 | "blob", 87 | false, 88 | ).unwrap(); 89 | assert_eq!("9a1be2ae6deb625c3e4d821f56016ee582d45fa0", &res); 90 | } 91 | 92 | #[test] 93 | fn multiline() { 94 | let res = hash_object( 95 | "This is a multi-line string litteral 96 | used as a test file sample!\n" 97 | .as_bytes(), 98 | "blob", 99 | false, 100 | ).unwrap(); 101 | assert_eq!("ca1bd6f977c9c4319096dde65ab7824d6d249d12", &res); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/builtin/init.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io; 3 | use std::path::Path; 4 | 5 | pub fn cmd_init(args: &[String]) { 6 | let default_path = String::new(); 7 | let path = args.get(0).unwrap_or(&default_path); 8 | if let Err(why) = init(path) { 9 | println!("Could not initialize git repository: {:?}", why); 10 | } 11 | } 12 | 13 | pub fn init(dir_name: &str) -> io::Result<()> { 14 | if !dir_name.is_empty() { 15 | fs::create_dir(&dir_name)?; 16 | } 17 | 18 | let git_path = Path::new(&dir_name).join(".git"); 19 | fs::create_dir(&git_path)?; 20 | for dir in ["objects", "refs", "refs/heads", "refs/remotes"].iter() { 21 | fs::create_dir(git_path.join(dir))?; 22 | } 23 | fs::write(git_path.join("HEAD"), "ref: refs/heads/master\n")?; 24 | 25 | println!("Initialized empty Git repository in {}", git_path.display()); 26 | Ok(()) 27 | } 28 | -------------------------------------------------------------------------------- /src/builtin/log.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use builtin::cat_file; 4 | use builtin::commit; 5 | use refs; 6 | 7 | #[derive(Debug)] 8 | pub enum Error { 9 | CatFileError(cat_file::Error), 10 | CommitError(commit::Error), 11 | RefError(io::Error), 12 | } 13 | 14 | pub fn cmd_log() { 15 | if let Err(why) = log() { 16 | println!("Cannot go through log: {:?}", why); 17 | } 18 | } 19 | 20 | fn log() -> Result<(), Error> { 21 | let mut commit_hash = refs::get_ref_hash("HEAD").map_err(Error::RefError)?; 22 | loop { 23 | println!("commit {}", commit_hash); 24 | cat_file::cat_file(&commit_hash, "--print").map_err(Error::CatFileError)?; 25 | 26 | let parents = commit::get_parents_hashes(&commit_hash).map_err(Error::CommitError)?; 27 | // Simple linear log, ignore multiple parents 28 | commit_hash = match parents.get(0) { 29 | Some(h) => h.to_string(), 30 | None => break, 31 | }; 32 | } 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /src/builtin/ls_files.rs: -------------------------------------------------------------------------------- 1 | use cli; 2 | use index; 3 | 4 | pub fn cmd_ls_files(flags: &[String]) { 5 | let accepted_flags = ["--stage", "-s"]; 6 | if cli::has_known_flags(flags, &accepted_flags) { 7 | let stage = cli::has_flag(&flags, "--stage", "-s"); 8 | if let Err(why) = ls_files(stage) { 9 | println!("Could not print index files: {:?}", why); 10 | } 11 | } 12 | } 13 | 14 | fn ls_files(stage: bool) -> Result<(), index::Error> { 15 | let entries = index::read_entries()?; 16 | for entry in entries { 17 | if stage { 18 | let stage_nb = (entry.flags >> 12) & 3; 19 | println!( 20 | "{:6o} {} {}\t{}", 21 | entry.mode, entry.hash, stage_nb, entry.path 22 | ); 23 | } else { 24 | println!("{}", entry.path); 25 | } 26 | } 27 | 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /src/builtin/merge.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io; 3 | 4 | use builtin::commit; 5 | use builtin::status; 6 | use object; 7 | use object::Object; 8 | use refs; 9 | use work_dir; 10 | 11 | #[derive(Debug)] 12 | pub enum Error { 13 | AlreadyUpToDate, 14 | CommitError(commit::Error), 15 | IoError(io::Error), 16 | ObjectError(object::Error), 17 | ReferenceNotACommit, 18 | WorkDirError(work_dir::Error), 19 | WorkDirNotClean, 20 | } 21 | 22 | impl From for Error { 23 | fn from(e: io::Error) -> Error { 24 | Error::IoError(e) 25 | } 26 | } 27 | 28 | impl From for Error { 29 | fn from(e: work_dir::Error) -> Error { 30 | Error::WorkDirError(e) 31 | } 32 | } 33 | 34 | pub fn cmd_merge(args: &[String]) { 35 | if args.is_empty() { 36 | println!("merge: command takes a 'ref' argument."); 37 | } else { 38 | let ref_name = &args[0]; 39 | if let Err(why) = merge(ref_name) { 40 | println!("Could not merge: {:?}", why); 41 | } 42 | } 43 | } 44 | 45 | pub fn merge(ref_name: &str) -> Result<(), Error> { 46 | if !status::is_clean_work_dir() { 47 | return Err(Error::WorkDirNotClean); 48 | } 49 | 50 | let cur_commit = refs::get_ref_hash("HEAD")?; 51 | let dst_commit = refs::get_ref_hash(&ref_name)?; 52 | if cur_commit == dst_commit { 53 | return Err(Error::AlreadyUpToDate); 54 | } 55 | 56 | let object = Object::new(&dst_commit).map_err(Error::ObjectError)?; 57 | if object.obj_type != "commit" { 58 | return Err(Error::ReferenceNotACommit); 59 | } 60 | 61 | let cur_branch = refs::read_ref("HEAD")?; 62 | let can_fast_forward = cur_commit.is_empty() || commit::is_ancestor(&dst_commit, &cur_commit); 63 | if can_fast_forward { 64 | work_dir::update_from_commit(&dst_commit)?; 65 | 66 | refs::write_to_ref(&cur_branch, &dst_commit)?; 67 | println!("Fast-forward"); 68 | } else { 69 | work_dir::update_from_merge(&cur_commit, &dst_commit)?; 70 | 71 | refs::write_to_ref("MERGE_HEAD", &dst_commit)?; 72 | let merge_msg = format!("Merge {} into {}", ref_name, cur_branch); 73 | println!("{}", merge_msg); 74 | 75 | let mut has_conflicts = false; 76 | for file in work_dir::get_all_files_path()? { 77 | let data = fs::read_to_string(&file)?; 78 | if data.contains("<<<<<<") { 79 | println!("CONFLICT {}", file); 80 | has_conflicts = true; 81 | } 82 | } 83 | 84 | if !has_conflicts { 85 | commit::commit(&merge_msg).map_err(Error::CommitError)?; 86 | } else { 87 | println!("Conflicts detected, fix them and commit to finish merge."); 88 | } 89 | } 90 | 91 | Ok(()) 92 | } 93 | -------------------------------------------------------------------------------- /src/builtin/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod add; 2 | pub mod branch; 3 | pub mod cat_file; 4 | pub mod checkout; 5 | pub mod clone; 6 | pub mod commit; 7 | pub mod config; 8 | pub mod diff; 9 | pub mod fetch; 10 | pub mod hash_object; 11 | pub mod init; 12 | pub mod log; 13 | pub mod ls_files; 14 | pub mod merge; 15 | pub mod pull; 16 | pub mod push; 17 | pub mod read_tree; 18 | pub mod remote; 19 | pub mod status; 20 | pub mod write_tree; 21 | -------------------------------------------------------------------------------- /src/builtin/pull.rs: -------------------------------------------------------------------------------- 1 | use builtin::fetch; 2 | use builtin::merge; 3 | 4 | #[derive(Debug)] 5 | pub enum Error { 6 | FetchError(fetch::Error), 7 | MergeError(merge::Error), 8 | } 9 | 10 | pub fn cmd_pull(args: &[String]) { 11 | if args.len() < 2 { 12 | println!("pull: takes 'remote' and 'branch' arguments"); 13 | } else { 14 | let remote = &args[0]; 15 | let branch = &args[1]; 16 | if let Err(why) = pull(&remote, &branch) { 17 | println!("Could not pull: {:?}", why); 18 | } 19 | } 20 | } 21 | 22 | pub fn pull(remote: &str, branch: &str) -> Result<(), Error> { 23 | fetch::fetch(&remote, &branch).map_err(Error::FetchError)?; 24 | merge::merge("FETCH_HEAD").map_err(Error::MergeError)?; 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /src/builtin/push.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs; 3 | use std::io; 4 | use std::path::Path; 5 | 6 | use builtin::config; 7 | use builtin::hash_object; 8 | use builtin::remote; 9 | use object; 10 | use refs; 11 | 12 | #[derive(Debug)] 13 | pub enum Error { 14 | AlreadyUpToDate, 15 | IoError(io::Error), 16 | ObjectError(object::Error), 17 | RemoteBranchCurrentlyCheckedOut, 18 | RemoteNotAGitRepo, 19 | RemoteNotFound, 20 | } 21 | 22 | impl From for Error { 23 | fn from(e: io::Error) -> Error { 24 | Error::IoError(e) 25 | } 26 | } 27 | 28 | pub fn cmd_push(args: &[String]) { 29 | if args.len() < 2 { 30 | println!("push: takes 'remote' and 'branch' arguments"); 31 | } else { 32 | let remote = &args[0]; 33 | let branch = &args[1]; 34 | if let Err(why) = push(&remote, &branch) { 35 | println!("Could not push: {:?}", why); 36 | } 37 | } 38 | } 39 | 40 | fn push(remote: &str, branch: &str) -> Result<(), Error> { 41 | let user = config::Config::new()?; 42 | let url = match user.remotes.iter().find(|r| r.name == remote) { 43 | Some(r) => r.url.to_string(), 44 | None => return Err(Error::RemoteNotFound), 45 | }; 46 | 47 | let local_dir = env::current_dir()?; 48 | let local_hash = refs::get_ref_hash(&branch)?; 49 | let remote_dir = Path::new(&url); 50 | 51 | env::set_current_dir(&remote_dir)?; 52 | if !Path::new(".git").exists() { 53 | return Err(Error::RemoteNotAGitRepo); 54 | } else { 55 | let is_bare_repo = config::Config::new()?.is_empty(); 56 | if !is_bare_repo && refs::read_ref("HEAD")? == branch { 57 | return Err(Error::RemoteBranchCurrentlyCheckedOut); 58 | } 59 | } 60 | 61 | let remote_hash = refs::get_ref_hash(&branch)?; 62 | if local_hash == remote_hash { 63 | return Err(Error::AlreadyUpToDate); 64 | } else { 65 | refs::write_to_ref(&branch, &local_hash)?; 66 | } 67 | 68 | env::set_current_dir(&local_dir)?; 69 | let missing = remote::find_remote_missing_objects(&local_hash, &remote_hash); 70 | for obj_hash in &missing { 71 | let obj = object::Object::new(&obj_hash).map_err(Error::ObjectError)?; 72 | 73 | env::set_current_dir(&remote_dir)?; 74 | let write = true; 75 | hash_object::hash_object(&obj.data, &obj.obj_type, write)?; 76 | env::set_current_dir(&local_dir)?; 77 | } 78 | 79 | let rem_dir = Path::new(".git").join("refs").join("remotes").join(&remote); 80 | fs::create_dir_all(&rem_dir)?; 81 | fs::write(rem_dir.join(&branch), format!("{}\n", local_hash))?; 82 | 83 | println!("Count: {} objects", missing.len()); 84 | println!("To: {}", url); 85 | Ok(()) 86 | } 87 | -------------------------------------------------------------------------------- /src/builtin/read_tree.rs: -------------------------------------------------------------------------------- 1 | use std::str; 2 | 3 | use builtin::cat_file; 4 | use object; 5 | use object::Object; 6 | use sha1; 7 | 8 | #[derive(Debug)] 9 | pub struct Entry { 10 | pub mode: u32, 11 | pub path: String, 12 | pub hash: String, 13 | } 14 | 15 | #[derive(Debug)] 16 | pub enum Error { 17 | NotATreeObject, 18 | ObjectError(object::Error), 19 | TreeEntryInvalidHash, 20 | TreeEntryMissingHash, 21 | TreeEntryMissingMode, 22 | TreeEntryMissingPath, 23 | } 24 | 25 | pub fn cmd_read_tree(args: &[String]) { 26 | if args.is_empty() { 27 | println!("read-tree: command takes a 'hash' argument."); 28 | } else { 29 | let hash = &args[0]; 30 | if let Err(why) = cat_file::cat_file(hash, "--print") { 31 | println!("Cannot retrieve object info: {:?}", why); 32 | } 33 | } 34 | } 35 | 36 | pub fn read_tree(hash_prefix: &str) -> Result, Error> { 37 | let object = Object::new(&hash_prefix).map_err(Error::ObjectError)?; 38 | if object.obj_type != "tree" { 39 | return Err(Error::NotATreeObject); 40 | } 41 | 42 | let mut tree = Vec::new(); 43 | let mut start = 0; 44 | while start < object.data.len() { 45 | let end = match object.data[start..].iter().position(|&x| x == 0) { 46 | Some(i) => start + i + 21, 47 | None => break, 48 | }; 49 | let entry = &object.data[start..end]; 50 | 51 | let space_byte = match entry.iter().position(|&x| x == 32) { 52 | Some(i) => i, 53 | None => return Err(Error::TreeEntryMissingMode), 54 | }; 55 | let (mode, entry) = entry.split_at(space_byte); 56 | let entry = &entry[1..]; 57 | 58 | let null_byte = match entry.iter().position(|&x| x == 0) { 59 | Some(i) => i, 60 | None => return Err(Error::TreeEntryMissingPath), 61 | }; 62 | let (path, entry) = entry.split_at(null_byte); 63 | 64 | if entry.len() < 21 { 65 | return Err(Error::TreeEntryMissingHash); 66 | } 67 | let hash = &entry[1..21]; 68 | 69 | let mode_str = str::from_utf8(&mode).unwrap(); 70 | let mode = u32::from_str_radix(&mode_str, 8).unwrap(); 71 | let path = str::from_utf8(&path).unwrap(); 72 | let hash = match sha1::decompress_hash(&hash) { 73 | Some(hash) => hash, 74 | None => return Err(Error::TreeEntryInvalidHash), 75 | }; 76 | 77 | tree.push(Entry { 78 | mode: mode, 79 | path: path.to_string(), 80 | hash: hash.to_string(), 81 | }); 82 | start = end; 83 | } 84 | 85 | Ok(tree) 86 | } 87 | -------------------------------------------------------------------------------- /src/builtin/remote.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use builtin::config; 4 | use object; 5 | 6 | #[derive(Debug)] 7 | pub struct Remote { 8 | pub name: String, 9 | pub url: String, 10 | } 11 | 12 | pub fn cmd_remote(args: &[String]) { 13 | if args.is_empty() { 14 | if let Err(why) = list_remotes() { 15 | println!("Could not list remotes: {}", why); 16 | } 17 | } else { 18 | let cmd = &args[0]; 19 | if cmd == "add" { 20 | if args.len() < 3 { 21 | println!("remote: 'add' command takes 'name' and 'url' arguments"); 22 | } else { 23 | let name = &args[1]; 24 | let url = &args[2]; 25 | if let Err(why) = add_remote(name, url) { 26 | println!("Could not add remote: {}", why); 27 | } 28 | } 29 | } else { 30 | println!("remote: unknown command '{}'", cmd); 31 | } 32 | } 33 | } 34 | 35 | fn list_remotes() -> io::Result<()> { 36 | let user = config::Config::new()?; 37 | for remote in user.remotes { 38 | println!("{} {}", remote.name, remote.url); 39 | } 40 | Ok(()) 41 | } 42 | 43 | pub fn add_remote(name: &str, url: &str) -> io::Result<()> { 44 | let section = format!("remote.{}.url", name); 45 | config::config("add", §ion, url)?; 46 | Ok(()) 47 | } 48 | 49 | pub fn find_remote_missing_objects(local_commit: &str, remote_commit: &str) -> Vec { 50 | let local_objects = object::find_objects_from_commit(&local_commit); 51 | let remote_objects = object::find_objects_from_commit(&remote_commit); 52 | 53 | let mut missing = Vec::new(); 54 | for obj in local_objects { 55 | if !remote_objects.contains(&obj) { 56 | missing.push(obj); 57 | } 58 | } 59 | 60 | missing 61 | } 62 | -------------------------------------------------------------------------------- /src/builtin/status.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io; 3 | 4 | use builtin::hash_object; 5 | use index; 6 | use work_dir; 7 | 8 | #[derive(Debug)] 9 | pub enum Error { 10 | IndexError(index::Error), 11 | IoError(io::Error), 12 | } 13 | 14 | impl From for Error { 15 | fn from(e: io::Error) -> Error { 16 | Error::IoError(e) 17 | } 18 | } 19 | 20 | enum State { 21 | Modified, 22 | New, 23 | Deleted, 24 | } 25 | 26 | pub fn cmd_status() { 27 | match status() { 28 | Ok(changes) => { 29 | for (state, path) in changes { 30 | let s = match state { 31 | State::Modified => "modified", 32 | State::New => "new", 33 | State::Deleted => "deleted", 34 | }; 35 | println!("{}: {}", s, path); 36 | } 37 | } 38 | Err(why) => println!("Could not retrieve status: {:?}", why), 39 | }; 40 | } 41 | 42 | fn status() -> Result, Error> { 43 | let mut status = Vec::new(); 44 | let index = index::read_entries().map_err(Error::IndexError)?; 45 | let files = work_dir::get_all_files_path()?; 46 | for file in &files { 47 | match index.iter().find(|e| file == &e.path) { 48 | Some(e) => { 49 | let file_content = fs::read(&file)?; 50 | let hash = hash_object::hash_object(&file_content, "blob", false)?; 51 | if e.hash != hash { 52 | status.push((State::Modified, file.to_string())); 53 | } 54 | } 55 | None => status.push((State::New, file.to_string())), 56 | }; 57 | } 58 | 59 | for entry in &index { 60 | if files.iter().all(|x| x != &entry.path) { 61 | status.push((State::Deleted, entry.path.to_string())); 62 | } 63 | } 64 | 65 | Ok(status) 66 | } 67 | 68 | pub fn is_clean_work_dir() -> bool { 69 | match status() { 70 | Ok(changes) => changes.is_empty(), 71 | Err(_) => false, 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/builtin/write_tree.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use builtin::hash_object; 4 | use index; 5 | use sha1; 6 | 7 | #[derive(Debug)] 8 | pub enum Error { 9 | HashObjError(io::Error), 10 | IndexError(index::Error), 11 | } 12 | 13 | pub fn cmd_write_tree() { 14 | match write_tree() { 15 | Ok(hash) => println!("{}", hash), 16 | Err(why) => println!("Could not create tree object: {:?}", why), 17 | }; 18 | } 19 | 20 | pub fn write_tree() -> Result { 21 | let mut tree = Vec::new(); 22 | let entries = index::read_entries().map_err(Error::IndexError)?; 23 | for entry in entries { 24 | let tree_entry = format!("{:o} {}\x00", entry.mode, entry.path); 25 | let compressed_hash = match sha1::compress_hash(&entry.hash) { 26 | Some(hash) => hash, 27 | None => continue, 28 | }; 29 | tree.extend(tree_entry.as_bytes()); 30 | tree.extend(compressed_hash); 31 | } 32 | 33 | let write = true; 34 | let hash = hash_object::hash_object(&tree, "tree", write).map_err(Error::HashObjError)?; 35 | Ok(hash) 36 | } 37 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | pub fn has_flag(args: &[String], long_fmt: &str, short_fmt: &str) -> bool { 2 | args.iter().any(|x| x == long_fmt || x == short_fmt) 3 | } 4 | 5 | pub fn has_known_flags(flags: &[String], known_flags: &[&str]) -> bool { 6 | for flag in flags { 7 | let is_known = known_flags.contains(&flag.as_str()); 8 | if !is_known { 9 | println!("unknown flag: {}", flag); 10 | return false; 11 | } 12 | } 13 | return true; 14 | } 15 | 16 | pub fn split_args_from_flags(input: Vec) -> (Vec, Vec) { 17 | let mut args = Vec::new(); 18 | let mut flags = Vec::new(); 19 | for opt in input { 20 | if opt.starts_with("-") { 21 | flags.push(opt); 22 | } else { 23 | args.push(opt); 24 | } 25 | } 26 | (args, flags) 27 | } 28 | -------------------------------------------------------------------------------- /src/index.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io; 3 | use std::os::unix::fs::MetadataExt; 4 | use std::path::Path; 5 | use std::str; 6 | 7 | use bits::big_endian; 8 | use builtin::hash_object; 9 | use sha1; 10 | 11 | #[derive(Debug)] 12 | pub enum Error { 13 | EntryMissingNullByteEnding, 14 | InvalidChecksum, 15 | InvalidHash, 16 | InvalidHeaderSignature, 17 | InvalidIndexVersion, 18 | IoError(io::Error), 19 | } 20 | 21 | impl From for Error { 22 | fn from(e: io::Error) -> Error { 23 | Error::IoError(e) 24 | } 25 | } 26 | 27 | #[derive(Debug, Clone)] 28 | pub struct Entry { 29 | pub ctime_sec: u32, 30 | pub ctime_nan: u32, 31 | pub mtime_sec: u32, 32 | pub mtime_nan: u32, 33 | pub dev: u32, 34 | pub ino: u32, 35 | pub mode: u32, 36 | pub uid: u32, 37 | pub gid: u32, 38 | pub size: u32, 39 | pub hash: String, 40 | pub flags: u16, 41 | pub path: String, 42 | } 43 | 44 | impl Entry { 45 | pub fn new(path: &str) -> Result { 46 | let data = fs::read(&path)?; 47 | let meta = fs::metadata(&path)?; 48 | 49 | let write = true; 50 | let hash = hash_object::hash_object(&data, "blob", write)?; 51 | 52 | Ok(Entry { 53 | ctime_sec: meta.ctime() as u32, 54 | ctime_nan: meta.ctime_nsec() as u32, 55 | mtime_sec: meta.mtime() as u32, 56 | mtime_nan: meta.mtime_nsec() as u32, 57 | dev: meta.dev() as u32, 58 | ino: meta.ino() as u32, 59 | mode: meta.mode(), 60 | uid: meta.uid(), 61 | gid: meta.gid(), 62 | size: meta.size() as u32, 63 | hash: hash, 64 | flags: path.len() as u16, 65 | path: path.to_string(), 66 | }) 67 | } 68 | } 69 | 70 | pub fn read_entries() -> Result, Error> { 71 | let mut entries = Vec::new(); 72 | 73 | let index = Path::new(".git").join("index"); 74 | if !index.exists() { 75 | return Ok(entries); 76 | } 77 | 78 | let bytes = fs::read(index)?; 79 | let signature = str::from_utf8(&bytes[0..4]).unwrap(); 80 | if signature != "DIRC" { 81 | return Err(Error::InvalidHeaderSignature); 82 | } 83 | let version = big_endian::u8_slice_to_u32(&bytes[4..]); 84 | if version != 2 { 85 | return Err(Error::InvalidIndexVersion); 86 | } 87 | 88 | let nb_entries = big_endian::u8_slice_to_u32(&bytes[8..]) as usize; 89 | let mut idx = 12; 90 | for _ in 0..nb_entries { 91 | let mut fields = [0u32; 10]; 92 | for e in 0..10 { 93 | fields[e] = big_endian::u8_slice_to_u32(&bytes[idx..]); 94 | idx += 4; 95 | } 96 | 97 | let hash = sha1::u8_slice_hash_to_hex_str(&bytes[idx..]); 98 | idx += 20; 99 | 100 | let flags = big_endian::u8_slice_to_u16(&bytes[idx..]); 101 | idx += 2; 102 | 103 | let null_idx = match bytes[idx..].iter().position(|&x| x == 0) { 104 | Some(i) => i, 105 | None => return Err(Error::EntryMissingNullByteEnding), 106 | }; 107 | let path = str::from_utf8(&bytes[idx..idx + null_idx]) 108 | .unwrap() 109 | .to_string(); 110 | idx += null_idx; 111 | 112 | let entry_len = 62 + path.len(); 113 | let padding_len = ((entry_len + 8) / 8) * 8 - entry_len; 114 | idx += padding_len; 115 | 116 | entries.push(Entry { 117 | ctime_sec: fields[0], 118 | ctime_nan: fields[1], 119 | mtime_sec: fields[2], 120 | mtime_nan: fields[3], 121 | dev: fields[4], 122 | ino: fields[5], 123 | mode: fields[6], 124 | uid: fields[7], 125 | gid: fields[8], 126 | size: fields[9], 127 | hash: hash, 128 | flags: flags, 129 | path: path, 130 | }); 131 | } 132 | 133 | let checksum = sha1::u8_slice_hash_to_hex_str(&bytes[idx..]); 134 | let actual_hash = sha1::sha1(&bytes[..idx]); 135 | if actual_hash != checksum { 136 | return Err(Error::InvalidChecksum); 137 | } 138 | 139 | Ok(entries) 140 | } 141 | 142 | pub fn write_entries(mut entries: Vec) -> Result<(), Error> { 143 | entries.sort_by(|a, b| a.path.cmp(&b.path)); 144 | 145 | let mut compressed_entries = Vec::new(); 146 | for entry in &entries { 147 | let fields = vec![ 148 | entry.ctime_sec, 149 | entry.ctime_nan, 150 | entry.mtime_sec, 151 | entry.mtime_nan, 152 | entry.dev, 153 | entry.ino, 154 | entry.mode, 155 | entry.uid, 156 | entry.gid, 157 | entry.size, 158 | ]; 159 | 160 | let mut bytes_entry = Vec::new(); 161 | for field in fields { 162 | bytes_entry.extend(&big_endian::u32_to_u8(field)); 163 | } 164 | 165 | let compressed_hash = match sha1::compress_hash(&entry.hash) { 166 | Some(hash) => hash, 167 | None => return Err(Error::InvalidHash), 168 | }; 169 | bytes_entry.extend(&compressed_hash); 170 | bytes_entry.extend(&big_endian::u16_to_u8(entry.flags)); 171 | bytes_entry.extend(entry.path.as_bytes()); 172 | 173 | let entry_len = 62 + entry.path.len(); 174 | let padding_len = ((entry_len + 8) / 8) * 8 - entry_len; 175 | let padding = vec![0u8; padding_len]; 176 | bytes_entry.extend(&padding); 177 | 178 | compressed_entries.extend(&bytes_entry); 179 | } 180 | 181 | // DIRC2 182 | let mut header = vec![68, 73, 82, 67, 0, 0, 0, 2]; 183 | header.extend(&big_endian::u32_to_u8(entries.len() as u32)); 184 | 185 | let mut data = Vec::new(); 186 | data.extend(&header); 187 | data.extend(&compressed_entries); 188 | 189 | let checksum = sha1::sha1(&data); 190 | let compressed_hash = match sha1::compress_hash(&checksum) { 191 | Some(hash) => hash, 192 | None => return Err(Error::InvalidHash), 193 | }; 194 | data.extend(&compressed_hash); 195 | 196 | let index = Path::new(".git").join("index"); 197 | fs::write(index, &data)?; 198 | 199 | Ok(()) 200 | } 201 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod bits; 2 | mod builtin; 3 | mod cli; 4 | mod index; 5 | mod object; 6 | mod refs; 7 | mod sha1; 8 | mod work_dir; 9 | mod zlib; 10 | 11 | use std::env; 12 | use std::path::Path; 13 | 14 | fn main() { 15 | let args: Vec = env::args().collect(); 16 | if args.len() == 1 { 17 | print_help(); 18 | return; 19 | } 20 | 21 | let (args, flags) = cli::split_args_from_flags(args); 22 | let cmd = &args[1]; 23 | let args = &args[2..]; 24 | if cmd != "init" && cmd != "clone" && !Path::new(".git").exists() { 25 | println!("Not a top-level git repository"); 26 | return; 27 | } 28 | 29 | match cmd.as_str() { 30 | "init" => builtin::init::cmd_init(&args), 31 | "hash-object" => builtin::hash_object::cmd_hash_object(&args, &flags), 32 | "cat-file" => builtin::cat_file::cmd_cat_file(&args, &flags), 33 | "ls-files" => builtin::ls_files::cmd_ls_files(&flags), 34 | "status" => builtin::status::cmd_status(), 35 | "diff" => builtin::diff::cmd_diff(&args), 36 | "add" => builtin::add::cmd_add(&args), 37 | "write-tree" => builtin::write_tree::cmd_write_tree(), 38 | "read-tree" => builtin::read_tree::cmd_read_tree(&args), 39 | "commit" => builtin::commit::cmd_commit(&args, &flags), 40 | "config" => builtin::config::cmd_config(&args, &flags), 41 | "log" => builtin::log::cmd_log(), 42 | "branch" => builtin::branch::cmd_branch(&args, &flags), 43 | "checkout" => builtin::checkout::cmd_checkout(&args), 44 | "merge" => builtin::merge::cmd_merge(&args), 45 | "remote" => builtin::remote::cmd_remote(&args), 46 | "push" => builtin::push::cmd_push(&args), 47 | "fetch" => builtin::fetch::cmd_fetch(&args), 48 | "pull" => builtin::pull::cmd_pull(&args), 49 | "clone" => builtin::clone::cmd_clone(&args), 50 | "help" | _ => print_help(), 51 | } 52 | } 53 | 54 | fn print_help() { 55 | println!("Help | list of commands:"); 56 | println!("* basic commands:"); 57 | println!("\tinit: create empty git repository"); 58 | println!("\tconfig: get and set repo options"); 59 | println!("\tadd: add content to the index"); 60 | println!("\tcommit: record changes to the repo"); 61 | println!("\tstatus: show the working dir status"); 62 | println!("\tdiff: show changes between index and working dir"); 63 | println!("\tlog: show commit logs"); 64 | println!("* branches:"); 65 | println!("\tbranch: list or create branches"); 66 | println!("\tcheckout: switch branches"); 67 | println!("\tmerge: merge two branches together"); 68 | println!("* remotes:"); 69 | println!("\tclone: clone a git repo into a new dir"); 70 | println!("\tfetch: retrieve refs and objects from remote"); 71 | println!("\tpush: update remote refs and objects"); 72 | println!("\tpull: fetch and merge from another repo"); 73 | println!("\tremote: get and set repo remotes"); 74 | println!("* plumbing:"); 75 | println!("\thash-object: compute object hash and create storage blob"); 76 | println!("\tcat-file: show content, type, or size of stored objects"); 77 | println!("\tls-files: show files in the index"); 78 | println!("\tread-tree: read tree info from object"); 79 | println!("\twrite-tree: create tree object from index"); 80 | } 81 | -------------------------------------------------------------------------------- /src/object.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io; 3 | use std::path::{Path, PathBuf}; 4 | use std::str; 5 | 6 | use bits::big_endian; 7 | use builtin::commit; 8 | use builtin::read_tree; 9 | use zlib; 10 | 11 | #[derive(Debug)] 12 | pub enum Error { 13 | HashPrefixTooShort, 14 | HeaderMissingNullByte, 15 | HeaderMissingSize, 16 | HeaderMissingType, 17 | IoError(io::Error), 18 | ObjectNotFound, 19 | } 20 | 21 | impl From for Error { 22 | fn from(e: io::Error) -> Error { 23 | Error::IoError(e) 24 | } 25 | } 26 | 27 | #[derive(Debug)] 28 | pub struct Object { 29 | pub obj_type: String, 30 | pub obj_size: usize, 31 | pub data: Vec, 32 | } 33 | 34 | impl Object { 35 | pub fn new(hash_prefix: &str) -> Result { 36 | let path = Object::full_path(hash_prefix)?; 37 | let raw_data = fs::read(path)?; 38 | let data = zlib::decompress(raw_data); 39 | 40 | let header_idx = match data.iter().position(|&x| x == 0) { 41 | Some(i) => i, 42 | None => return Err(Error::HeaderMissingNullByte), 43 | }; 44 | let (header, data) = data.split_at(header_idx); 45 | 46 | // 32 = space character (ASCII) 47 | let mut iter = header.split(|&x| x == 32); 48 | let obj_type = match iter.next() { 49 | Some(tp) => str::from_utf8(&tp).unwrap().to_string(), 50 | None => return Err(Error::HeaderMissingType), 51 | }; 52 | let obj_size = match iter.next() { 53 | Some(sz) => big_endian::u8_slice_to_usize(sz), 54 | None => return Err(Error::HeaderMissingSize), 55 | }; 56 | // Skip the null byte 57 | let data = data[1..].to_vec(); 58 | 59 | Ok(Object { 60 | obj_type, 61 | obj_size, 62 | data, 63 | }) 64 | } 65 | 66 | fn full_path(hash_prefix: &str) -> Result { 67 | if hash_prefix.len() < 2 { 68 | return Err(Error::HashPrefixTooShort); 69 | } 70 | 71 | let (dir, file) = hash_prefix.split_at(2); 72 | let objects = Path::new(".git").join("objects").join(dir); 73 | for f in fs::read_dir(objects)? { 74 | let path = f?.path(); 75 | if let Some(f) = path.file_name() { 76 | if f == file { 77 | return Ok(path.clone()); 78 | } 79 | } 80 | } 81 | 82 | Err(Error::ObjectNotFound) 83 | } 84 | } 85 | 86 | pub fn find_objects_from_commit(commit: &str) -> Vec { 87 | let mut objects = Vec::new(); 88 | objects.push(commit.to_string()); 89 | 90 | if let Ok(tree) = commit::get_tree_hash(&commit) { 91 | objects.extend(find_objects_from_tree(&tree)); 92 | } 93 | 94 | if let Ok(parents) = commit::get_parents_hashes(&commit) { 95 | for parent in parents { 96 | objects.extend(find_objects_from_commit(&parent)); 97 | } 98 | } 99 | 100 | objects 101 | } 102 | 103 | pub fn find_objects_from_tree(tree: &str) -> Vec { 104 | let mut objects = Vec::new(); 105 | objects.push(tree.to_string()); 106 | 107 | if let Ok(entries) = read_tree::read_tree(&tree) { 108 | for entry in entries { 109 | if Path::new(&entry.path).is_dir() { 110 | objects.extend(find_objects_from_tree(&entry.hash)); 111 | } else { 112 | objects.push(entry.hash.to_string()); 113 | } 114 | } 115 | } 116 | 117 | objects 118 | } 119 | -------------------------------------------------------------------------------- /src/refs.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io; 3 | use std::path::Path; 4 | 5 | pub fn read_ref(name: &str) -> io::Result { 6 | let ref_name = full_ref_name(name); 7 | let ref_path = Path::new(".git").join(ref_name); 8 | 9 | if !ref_path.exists() { 10 | return Ok(String::new()); 11 | } 12 | 13 | let mut value = fs::read_to_string(ref_path)?; 14 | // Remove '\n' character 15 | value.pop(); 16 | 17 | let head_prefix = "ref: refs/heads/"; 18 | if value.starts_with(&head_prefix) { 19 | value = value.split_off(head_prefix.len()); 20 | } 21 | 22 | let fetch_mark = " branch "; 23 | if value.contains(fetch_mark) { 24 | // Only retrieve the hash 25 | value.split_off(40); 26 | } 27 | 28 | Ok(value) 29 | } 30 | 31 | pub fn get_ref_hash(name: &str) -> io::Result { 32 | let value = read_ref(name)?; 33 | let is_hash = value.len() == 40 && value.chars().all(|c| c.is_ascii_hexdigit()); 34 | if name == "HEAD" && !is_hash { 35 | return read_ref(&value); 36 | } 37 | 38 | Ok(value) 39 | } 40 | 41 | pub fn write_to_ref(name: &str, value: &str) -> io::Result<()> { 42 | let ref_name = full_ref_name(name); 43 | let ref_path = Path::new(".git").join(ref_name); 44 | 45 | let formated_value = match name == "HEAD" && is_branch(&value) { 46 | true => format!("ref: refs/heads/{}\n", value), 47 | false => format!("{}\n", value), 48 | }; 49 | 50 | fs::write(ref_path, formated_value)?; 51 | Ok(()) 52 | } 53 | 54 | pub fn exists_ref(name: &str) -> bool { 55 | let ref_name = full_ref_name(name); 56 | Path::new(".git").join(ref_name).exists() || is_detached_head() 57 | } 58 | 59 | pub fn is_branch(name: &str) -> bool { 60 | Path::new(".git") 61 | .join("refs") 62 | .join("heads") 63 | .join(&name) 64 | .exists() 65 | } 66 | 67 | pub fn is_detached_head() -> bool { 68 | let head_path = Path::new(".git").join("HEAD"); 69 | let head = match fs::read_to_string(head_path) { 70 | Ok(s) => s, 71 | Err(_) => return false, 72 | }; 73 | !head.starts_with("ref: refs/heads/") 74 | } 75 | 76 | fn full_ref_name(name: &str) -> String { 77 | if name == "HEAD" || name == "FETCH_HEAD" || name == "MERGE_HEAD" 78 | || name.starts_with("refs/heads/") 79 | { 80 | name.to_string() 81 | } else { 82 | format!("refs/heads/{}", name) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/sha1.rs: -------------------------------------------------------------------------------- 1 | use std::char; 2 | 3 | use bits::big_endian; 4 | 5 | fn format_input(input: &[u8]) -> Vec { 6 | let mut fmt_input = Vec::new(); 7 | let input_size = input.len(); 8 | 9 | fmt_input.extend(input); 10 | fmt_input.push(0x80); 11 | 12 | let padding = vec![0; 63 - ((input_size + 8) % 64)]; 13 | fmt_input.extend(padding); 14 | 15 | let input_size_bits = 8 * input_size as u64; 16 | fmt_input.extend_from_slice(&big_endian::u64_to_u8(input_size_bits)); 17 | 18 | fmt_input 19 | } 20 | 21 | pub fn sha1(data: &[u8]) -> String { 22 | let mut states = [ 23 | 0x67452301u32, 24 | 0xefcdab89u32, 25 | 0x98badcfeu32, 26 | 0x10325476u32, 27 | 0xc3d2e1f0u32, 28 | ]; 29 | let mut w = [0u32; 80]; 30 | 31 | for block in format_input(data).chunks(64) { 32 | for i in 0..16 { 33 | w[i] = big_endian::u8_to_u32([ 34 | block[i * 4], 35 | block[i * 4 + 1], 36 | block[i * 4 + 2], 37 | block[i * 4 + 3], 38 | ]); 39 | } 40 | for i in 16..80 { 41 | w[i] = w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]; 42 | w[i] = w[i].rotate_left(1); 43 | } 44 | 45 | let mut a = states[0]; 46 | let mut b = states[1]; 47 | let mut c = states[2]; 48 | let mut d = states[3]; 49 | let mut e = states[4]; 50 | 51 | for i in 0..80 { 52 | let (k, f) = match i { 53 | 0...19 => (0x5a827999, (b & c) | (!b & d)), 54 | 20...39 => (0x6ed9eba1, b ^ c ^ d), 55 | 40...59 => (0x8f1bbcdc, (b & c) | (b & d) | (c & d)), 56 | 60...79 => (0xca62c1d6, b ^ c ^ d), 57 | _ => unreachable!(), 58 | }; 59 | 60 | let tmp = a.rotate_left(5) 61 | .wrapping_add(f) 62 | .wrapping_add(e) 63 | .wrapping_add(k) 64 | .wrapping_add(w[i]); 65 | e = d; 66 | d = c; 67 | c = b.rotate_left(30); 68 | b = a; 69 | a = tmp; 70 | } 71 | 72 | states[0] = states[0].wrapping_add(a); 73 | states[1] = states[1].wrapping_add(b); 74 | states[2] = states[2].wrapping_add(c); 75 | states[3] = states[3].wrapping_add(d); 76 | states[4] = states[4].wrapping_add(e); 77 | } 78 | 79 | u32_hash_to_hex_str(&states) 80 | } 81 | 82 | pub fn compress_hash(hash: &str) -> Option> { 83 | let mut dec_val = Vec::new(); 84 | for c in hash.chars() { 85 | let n = char::to_digit(c, 16)?; 86 | dec_val.push(n as u8); 87 | } 88 | 89 | let mut compressed_hash = Vec::new(); 90 | let mut idx = 0; 91 | for _ in 0..(hash.len() / 2) { 92 | let n = (dec_val[idx] << 4) | dec_val[idx + 1]; 93 | compressed_hash.push(n); 94 | idx += 2; 95 | } 96 | 97 | Some(compressed_hash) 98 | } 99 | 100 | pub fn decompress_hash(hash: &[u8]) -> Option { 101 | let mut decompressed_hash = String::new(); 102 | for &x in hash { 103 | let fst = (x >> 4) & 0x0f; 104 | let snd = x & 0x0f; 105 | 106 | let n1 = char::from_digit(fst as u32, 16)?; 107 | let n2 = char::from_digit(snd as u32, 16)?; 108 | decompressed_hash.push(n1); 109 | decompressed_hash.push(n2); 110 | } 111 | 112 | Some(decompressed_hash) 113 | } 114 | 115 | pub fn u32_hash_to_hex_str(hash: &[u32; 5]) -> String { 116 | hash.iter().map(|b| format!("{:08x}", b)).collect() 117 | } 118 | 119 | pub fn u8_slice_hash_to_hex_str(hash: &[u8]) -> String { 120 | let mut states = [0u32; 5]; 121 | let mut idx = 0; 122 | for s in 0..5 { 123 | states[s] = big_endian::u8_slice_to_u32(&hash[idx..]); 124 | idx += 4; 125 | } 126 | u32_hash_to_hex_str(&states) 127 | } 128 | 129 | #[cfg(test)] 130 | mod tests { 131 | use sha1::sha1; 132 | 133 | #[test] 134 | fn short() { 135 | assert_eq!( 136 | sha1("abc".as_bytes()), 137 | "a9993e364706816aba3e25717850c26c9cd0d89d" 138 | ); 139 | assert_eq!( 140 | sha1("".as_bytes()), 141 | "da39a3ee5e6b4b0d3255bfef95601890afd80709" 142 | ); 143 | assert_eq!( 144 | sha1("The quick brown fox jumps over the lazy dog".as_bytes()), 145 | "2fd4e1c67a2d28fced849ee1bb76e7391b93eb12" 146 | ); 147 | assert_eq!( 148 | sha1("The quick brown fox jumps over the lazy cog".as_bytes()), 149 | "de9f2c7fd25e1b3afad3e85a0bd17d9b100db4b3" 150 | ); 151 | } 152 | 153 | #[test] 154 | fn long() { 155 | assert_eq!( 156 | sha1( 157 | "A4j1pcn9Z8l0jzETQk9hVJjWE5dki7hd4Tk69B2aG60OGdifYMm1BNJ2PnDXz0\ 158 | D5XwT7QzFZ9JLtKaxl0cMndNPbzStb3YRb4lnR94BAlapbQsRqoZBYyctywtx0\ 159 | rkOYPbXboNusdd7PupOR3u1Mu71qNuMTgGO3xbO3YAhG4V8eyGGEBQxlObi0m6\ 160 | jSZ3lNPghdsPDhsuIKfHCUpoSGK0YscCpk6T8zuVadR4KC4vXOfERglIh5ya3r\ 161 | IxKNCXiborW7tLwhCQlqDmKvVG9fyK1fbwxif0R0h8pJQYo64FvF2Ev5EaPBZy\ 162 | p9gfPfW1rvgyiKYHFaVes3cs7HDLku64JmaYk79mSpv8XrQoECOmkXhMjIL8U1\ 163 | gCgpl7ruzkNICaKOc0FoQq5sSyCvH45Cm2qyIrviWVamf4aQ1nE3r7oM2LEpAw\ 164 | l9d46b6x0XvA3lsdw5lHSpJ9nK0xCD95MXkgFJwT2RaNDxYJesQzHJJJcDz1u6\ 165 | 41znwx4K5onTObfaxMLfZe2LHtnvS9uhqD3gbRRVG9DefFxKnr3ZzJfXhVpsJu\ 166 | qiogP96YEUXM6Le1UdUQqghG3fJiNmK4K0Llp1ocMHn7RzCPZpyQJydXMZxTsi\ 167 | Rg9lk1nyTaTeYJVsw375YpMRuV45ZZxMk7RvGEyFhJHYcMEqkzSTh1KVqeUywS\ 168 | RxQBFp4vB3aWAEU2dejEXIbmLrT4dAqcuSs7T9SsgXVzAmVNSmyCB4vtFaRh6o\ 169 | OGseV0gqTNzUNcwTPDCQETlkuq0s3VD9j8m4IQymJ4T8EPgF5oAUgWviOiNwr7\ 170 | JT0GGsNpCa5o1qZy2AbiL8NXRxExhj9aQ5x647O3w2QnylDtbYjCHQpM14obeF\ 171 | OwThnKbKOHfMUuNoOuFYIcadRVjD0tJwTGOiwb4aDH70aFd6eN4Fnu0wHG62UN\ 172 | EtEihhkQZfhohShVWcUO23LuLZj4aBIgY5hGJPZO7IImEYtb49rrZ1687EcvTA\ 173 | LyhMMxMD" 174 | .as_bytes() 175 | ), 176 | "32f3f879a843b8792f1574110d02d66aa04701ad" 177 | ); 178 | } 179 | 180 | #[test] 181 | fn multiline() { 182 | assert_eq!( 183 | sha1( 184 | "This is a multi-line string litteral 185 | used as a test file sample!\n" 186 | .as_bytes() 187 | ), 188 | "6bf58217d47b728b777fa2ea1545787587186fff" 189 | ); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/work_dir.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::fs; 3 | use std::io; 4 | use std::path::PathBuf; 5 | use std::str; 6 | 7 | use builtin::commit; 8 | use builtin::read_tree; 9 | use index; 10 | use object; 11 | use object::Object; 12 | use refs; 13 | 14 | #[derive(Debug)] 15 | pub enum Error { 16 | CommitError(commit::Error), 17 | IndexError(index::Error), 18 | IoError(io::Error), 19 | ObjectError(object::Error), 20 | ReadTreeError(read_tree::Error), 21 | } 22 | 23 | impl From for Error { 24 | fn from(e: commit::Error) -> Error { 25 | Error::CommitError(e) 26 | } 27 | } 28 | impl From for Error { 29 | fn from(e: index::Error) -> Error { 30 | Error::IndexError(e) 31 | } 32 | } 33 | impl From for Error { 34 | fn from(e: io::Error) -> Error { 35 | Error::IoError(e) 36 | } 37 | } 38 | impl From for Error { 39 | fn from(e: object::Error) -> Error { 40 | Error::ObjectError(e) 41 | } 42 | } 43 | impl From for Error { 44 | fn from(e: read_tree::Error) -> Error { 45 | Error::ReadTreeError(e) 46 | } 47 | } 48 | 49 | #[derive(Debug)] 50 | pub struct Change { 51 | pub state: State, 52 | pub path: String, 53 | pub hash: String, 54 | } 55 | 56 | #[derive(Debug, PartialEq)] 57 | pub enum State { 58 | Modified, 59 | New, 60 | Deleted, 61 | Same, 62 | } 63 | 64 | pub fn diff_from_commit(oldest: &str, latest: &str) -> Result, Error> { 65 | let tree_hash = match oldest.is_empty() { 66 | true => String::new(), 67 | false => commit::get_tree_hash(&oldest)?, 68 | }; 69 | let oldest_tree = match tree_hash.is_empty() { 70 | true => Vec::new(), 71 | false => read_tree::read_tree(&tree_hash)?, 72 | }; 73 | let tree_hash = commit::get_tree_hash(&latest)?; 74 | let latest_tree = read_tree::read_tree(&tree_hash)?; 75 | 76 | let mut changes = Vec::new(); 77 | for entry in &latest_tree { 78 | match oldest_tree.iter().find(|e| entry.path == e.path) { 79 | Some(e) => { 80 | let oldest_obj = Object::new(&e.hash)?; 81 | let latest_obj = Object::new(&entry.hash)?; 82 | let state = match oldest_obj.data != latest_obj.data { 83 | true => State::Modified, 84 | false => State::Same, 85 | }; 86 | changes.push(Change { 87 | state: state, 88 | path: e.path.to_string(), 89 | hash: e.hash.to_string(), 90 | }); 91 | } 92 | 93 | None => changes.push(Change { 94 | state: State::New, 95 | path: entry.path.to_string(), 96 | hash: entry.hash.to_string(), 97 | }), 98 | } 99 | } 100 | 101 | for entry in &oldest_tree { 102 | let still_here = latest_tree.iter().any(|e| entry.path == e.path); 103 | if !still_here { 104 | changes.push(Change { 105 | state: State::Deleted, 106 | path: entry.path.to_string(), 107 | hash: entry.hash.to_string(), 108 | }); 109 | } 110 | } 111 | 112 | Ok(changes) 113 | } 114 | 115 | pub fn update_from_commit(commit: &str) -> Result<(), Error> { 116 | let cur_commit = refs::get_ref_hash("HEAD")?; 117 | let changes = diff_from_commit(&cur_commit, &commit)?; 118 | 119 | let mut new_index = Vec::new(); 120 | for change in changes { 121 | update_single_change(&change)?; 122 | if change.state != State::Deleted { 123 | let entry = index::Entry::new(&change.path)?; 124 | new_index.push(entry); 125 | } 126 | } 127 | 128 | index::write_entries(new_index)?; 129 | Ok(()) 130 | } 131 | 132 | pub fn update_from_merge(commit1: &str, commit2: &str) -> Result<(), Error> { 133 | let common_ancestor = commit::lowest_common_ancestor(&commit1, &commit2)?; 134 | let changes1 = diff_from_commit(&common_ancestor, &commit1)?; 135 | let changes2 = diff_from_commit(&common_ancestor, &commit2)?; 136 | 137 | let mut new_index = Vec::new(); 138 | for change in &changes1 { 139 | match changes2.iter().find(|c| c.path == change.path) { 140 | Some(c) => { 141 | let obj1 = Object::new(&change.hash)?; 142 | let obj2 = Object::new(&c.hash)?; 143 | if obj1.data != obj2.data { 144 | // Merge conflict (no merge at all or intelligent conflict 145 | // marker, just mark everything as conflict) 146 | let content1 = str::from_utf8(&obj1.data).unwrap(); 147 | let content2 = str::from_utf8(&obj2.data).unwrap(); 148 | let conflict = format!( 149 | "<<<<<< {}\n{}\n======\n{}\n>>>>>> {}", 150 | commit1, content1, content2, commit2 151 | ); 152 | fs::write(&change.path, conflict)?; 153 | } 154 | 155 | let entry = index::Entry::new(&change.path)?; 156 | new_index.push(entry); 157 | } 158 | None => { 159 | update_single_change(&change)?; 160 | if change.state != State::Deleted { 161 | let entry = index::Entry::new(&change.path)?; 162 | new_index.push(entry); 163 | } 164 | } 165 | } 166 | } 167 | 168 | for change in &changes2 { 169 | let not_seen = changes1.iter().all(|c| c.path != change.path); 170 | if not_seen { 171 | update_single_change(&change)?; 172 | if change.state != State::Deleted { 173 | let entry = index::Entry::new(&change.path)?; 174 | new_index.push(entry); 175 | } 176 | } 177 | } 178 | 179 | index::write_entries(new_index)?; 180 | Ok(()) 181 | } 182 | 183 | fn update_single_change(change: &Change) -> Result<(), Error> { 184 | match change.state { 185 | State::New | State::Modified | State::Same => { 186 | let blob = Object::new(&change.hash)?; 187 | fs::write(&change.path, blob.data)?; 188 | } 189 | State::Deleted => fs::remove_file(&change.path)?, 190 | } 191 | 192 | Ok(()) 193 | } 194 | 195 | pub fn get_all_files_path() -> io::Result> { 196 | let mut files = Vec::new(); 197 | let mut queue = VecDeque::new(); 198 | queue.push_back(PathBuf::from(".")); 199 | 200 | while let Some(dir) = queue.pop_front() { 201 | for entry in fs::read_dir(&dir)? { 202 | let path = entry?.path(); 203 | if path.is_dir() { 204 | if !path.starts_with("./.git") { 205 | queue.push_back(path); 206 | } 207 | } else { 208 | let mut path = path.to_str().unwrap(); 209 | if path.starts_with("./") { 210 | path = &path[2..]; 211 | } 212 | 213 | files.push(path.to_string()); 214 | } 215 | } 216 | } 217 | 218 | Ok(files) 219 | } 220 | -------------------------------------------------------------------------------- /src/zlib.rs: -------------------------------------------------------------------------------- 1 | use bits::{big_endian, little_endian}; 2 | 3 | pub fn compress(input: Vec) -> Vec { 4 | let mut state = Encoder::new(input); 5 | if let Err(why) = state.compress() { 6 | panic!("Error while compressing: {:?}", why); 7 | } 8 | state.output 9 | } 10 | 11 | pub fn decompress(input: Vec) -> Vec { 12 | let mut state = Decoder::new(input); 13 | if let Err(why) = state.decompress() { 14 | panic!("Error while decompressing: {:?}", why); 15 | } 16 | state.output 17 | } 18 | 19 | // Encoder 20 | 21 | #[derive(Debug)] 22 | pub enum EncoderError { 23 | OutOfInput, 24 | } 25 | 26 | pub struct Encoder { 27 | input: Vec, 28 | input_idx: usize, 29 | pub output: Vec, 30 | } 31 | 32 | impl Encoder { 33 | pub fn new(input: Vec) -> Encoder { 34 | Encoder { 35 | input: input, 36 | input_idx: 0, 37 | output: Vec::new(), 38 | } 39 | } 40 | 41 | pub fn compress(&mut self) -> Result<(), EncoderError> { 42 | self.write_header(); 43 | 44 | // :D 45 | let nb_bytes = self.input.len(); 46 | self.non_compressed(nb_bytes)?; 47 | 48 | self.add_adler32_checksum(); 49 | Ok(()) 50 | } 51 | 52 | fn write_header(&mut self) { 53 | // CM = 8 CINFO = 7 FCHECK = 1 FDICT = 0 FLEVEL = 0 54 | self.output.push(0x78); 55 | self.output.push(1); 56 | } 57 | 58 | fn non_compressed(&mut self, nb_bytes: usize) -> Result<(), EncoderError> { 59 | self.output.push(1); 60 | 61 | let start = self.input_idx; 62 | let end = start + nb_bytes; 63 | if end > self.input.len() { 64 | return Err(EncoderError::OutOfInput); 65 | } 66 | 67 | let mut header = Vec::new(); 68 | header.extend_from_slice(&little_endian::u16_to_u8(nb_bytes as u16)); 69 | header.extend_from_slice(&little_endian::u16_to_u8(!nb_bytes as u16)); 70 | 71 | let data = &self.input[start..end]; 72 | 73 | self.output.extend(header); 74 | self.output.extend(data); 75 | 76 | Ok(()) 77 | } 78 | 79 | #[allow(dead_code)] 80 | fn fixed_huffman(&mut self) -> Result<(), EncoderError> { 81 | unimplemented!() 82 | } 83 | 84 | fn add_adler32_checksum(&mut self) { 85 | let mut a: u32 = 1; 86 | let mut b: u32 = 0; 87 | 88 | for byte in 0..self.input.len() { 89 | a = (a + self.input[byte] as u32) % 65521; 90 | b = (b + a) % 65521; 91 | } 92 | 93 | let res = (b << 16) | a; 94 | self.output.extend_from_slice(&big_endian::u32_to_u8(res)); 95 | } 96 | } 97 | 98 | // Decoder 99 | 100 | // * DEFLATE Compressed Data Format Specification version 1.3 101 | // https://tools.ietf.org/html/rfc1951 102 | // * puff: a simple inflate written to specify the deflate format unambiguously 103 | // https://github.com/madler/zlib/blob/master/contrib/puff/puff.c 104 | // * Canonical Huffman code 105 | // https://en.wikipedia.org/wiki/Canonical_Huffman_code 106 | // * ZLIB Compressed Data Format Specification version 3.3 107 | // https://tools.ietf.org/html/rfc1950 108 | // * An Explanation of the Deflate Algorithm 109 | // https://www.zlib.net/feldspar.html 110 | 111 | const MAX_BITS: usize = 15; 112 | const MAX_L_CODES: usize = 286; 113 | const MAX_D_CODES: usize = 30; 114 | const MAX_CODES: usize = MAX_L_CODES + MAX_D_CODES; 115 | const FIX_L_CODES: usize = 288; 116 | 117 | #[derive(Debug)] 118 | pub enum DecoderError { 119 | HuffmanTableTooBig, 120 | InvalidBlockCodeHeader, 121 | InvalidBlockSize, 122 | InvalidBlockType, 123 | InvalidDataHeader, 124 | InvalidDistTooFar, 125 | InvalidFixedCode, 126 | MissingEndOfBlockCode, 127 | OutOfCodes, 128 | OutOfInput, 129 | TooManyCodes, 130 | } 131 | 132 | // Instead of using a classic Huffman code with a tree datastructure, we will 133 | // be using a more compact one: a canonical Huffman code. 134 | struct HuffmanTable { 135 | count: [u16; MAX_BITS + 1], 136 | symbol: [u16; MAX_CODES], 137 | } 138 | 139 | impl HuffmanTable { 140 | // Create the table to decode the canonical Huffman code described by the 141 | // `length` array 142 | fn new(length: &[u16]) -> Result { 143 | let mut table = HuffmanTable { 144 | count: [0; MAX_BITS + 1], 145 | symbol: [0; MAX_CODES], 146 | }; 147 | 148 | for len in 0..length.len() { 149 | table.count[length[len] as usize] += 1; 150 | } 151 | 152 | // Check if the count is valid (one bit = 2x more codes) 153 | let mut codes_left = 1; 154 | for len in 1..(MAX_BITS + 1) { 155 | codes_left <<= 1; 156 | codes_left -= table.count[len] as i32; 157 | if codes_left < 0 { 158 | return Err(DecoderError::TooManyCodes); 159 | } 160 | } 161 | 162 | // Add symbols in sorted order (first by length, then by symbol) by 163 | // generating an offset table 164 | 165 | let mut offset = [0; MAX_BITS + 1]; 166 | for len in 1..MAX_BITS { 167 | offset[len + 1] = offset[len] + table.count[len]; 168 | } 169 | 170 | for sym in 0..length.len() { 171 | let len = length[sym] as usize; 172 | if len != 0 { 173 | table.symbol[offset[len] as usize] = sym as u16; 174 | offset[len as usize] += 1; 175 | } 176 | } 177 | 178 | Ok(table) 179 | } 180 | 181 | fn decode_sym(&self, state: &mut Decoder) -> Result { 182 | let mut code = 0; 183 | let mut first = 0; 184 | let mut index = 0; 185 | for bit in 1..(MAX_BITS + 1) { 186 | code |= state.get_bits(1)?; 187 | let count = self.count[bit]; 188 | if code < first + count { 189 | return Ok(self.symbol[(index + (code - first)) as usize]); 190 | } 191 | index += count; 192 | first += count; 193 | first <<= 1; 194 | code <<= 1; 195 | } 196 | 197 | Err(DecoderError::OutOfCodes) 198 | } 199 | } 200 | 201 | pub struct Decoder { 202 | // We store input data as bytes, but since compressed data blocks are not 203 | // guaranteed to begin on a byte boundary, we need a buffer to hold unused 204 | // bits from previous byte. 205 | input: Vec, 206 | input_idx: usize, 207 | bit_buf: u32, 208 | bit_cnt: u32, 209 | pub output: Vec, 210 | } 211 | 212 | impl Decoder { 213 | pub fn new(input: Vec) -> Decoder { 214 | Decoder { 215 | input: input, 216 | input_idx: 0, 217 | bit_buf: 0, 218 | bit_cnt: 0, 219 | output: Vec::new(), 220 | } 221 | } 222 | 223 | pub fn decompress(&mut self) -> Result<(), DecoderError> { 224 | // Validate header (CM = 8 CINFO = 7 FCHECK = 1 FDICT = 0 FLEVEL = 0) 225 | let cmf = self.get_bits(8)?; 226 | let flg = self.get_bits(8)?; 227 | if cmf != 0x78 || flg != 1 { 228 | return Err(DecoderError::InvalidDataHeader); 229 | } 230 | loop { 231 | let end_of_file = self.get_bits(1)?; 232 | let compress_mode = self.get_bits(2)?; 233 | match compress_mode { 234 | 0 => self.non_compressed(), 235 | 1 => self.fixed_huffman(), 236 | 2 => self.dynamic_huffman(), 237 | 3 => Err(DecoderError::InvalidBlockType), 238 | _ => unreachable!(), 239 | }?; 240 | if end_of_file == 1 { 241 | break; 242 | } 243 | } 244 | 245 | Ok(()) 246 | } 247 | 248 | fn get_bits(&mut self, need: u32) -> Result { 249 | let mut val = self.bit_buf; 250 | while self.bit_cnt < need { 251 | if self.input_idx == self.input.len() { 252 | return Err(DecoderError::OutOfInput); 253 | } 254 | // Load a new byte 255 | let byte = self.input[self.input_idx] as u32; 256 | self.input_idx += 1; 257 | val |= byte << self.bit_cnt; 258 | self.bit_cnt += 8; 259 | } 260 | // Keep only unused bits inside the buffer 261 | self.bit_buf = val >> need; 262 | self.bit_cnt -= need; 263 | // Zero out unwanted bits 264 | Ok((val & ((1 << need) - 1)) as u16) 265 | } 266 | 267 | // RFC 1951 - Section 3.2.4 268 | fn non_compressed(&mut self) -> Result<(), DecoderError> { 269 | // Ignore bits in buffer until next byte boundary (these data blocks 270 | // are byte-aligned) 271 | self.bit_buf = 0; 272 | self.bit_cnt = 0; 273 | 274 | let len = self.get_bits(16)?; 275 | let nlen = self.get_bits(16)?; 276 | if !nlen != len { 277 | return Err(DecoderError::InvalidBlockSize); 278 | } 279 | // Non-compressed mode is as simple as reading `len` bytes 280 | for _ in 0..len { 281 | let byte = self.get_bits(8)? as u8; 282 | self.output.push(byte); 283 | } 284 | 285 | Ok(()) 286 | } 287 | 288 | // RFC 1951 - Section 3.2.5 289 | fn decompress_block( 290 | &mut self, 291 | len_table: &HuffmanTable, 292 | dist_table: &HuffmanTable, 293 | ) -> Result<(), DecoderError> { 294 | const EXTRA_LEN: [u16; 29] = [ 295 | 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 296 | 115, 131, 163, 195, 227, 258, 297 | ]; 298 | const EXTRA_BITS: [u16; 29] = [ 299 | 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 300 | ]; 301 | const EXTRA_DIST: [u16; 30] = [ 302 | 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 303 | 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577, 304 | ]; 305 | const EXTRA_DBITS: [u16; 30] = [ 306 | 0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 307 | 12, 13, 13, 308 | ]; 309 | 310 | loop { 311 | let mut symbol = len_table.decode_sym(self)?; 312 | if symbol == 256 { 313 | // End of block 314 | break; 315 | } else if symbol < 256 { 316 | // Literal 317 | self.output.push(symbol as u8); 318 | } else if symbol < 290 { 319 | // Length/distance pair 320 | 321 | // Get length 322 | symbol -= 257; 323 | if symbol as usize > EXTRA_LEN.len() { 324 | return Err(DecoderError::InvalidFixedCode); 325 | } 326 | let len = 327 | EXTRA_LEN[symbol as usize] + self.get_bits(EXTRA_BITS[symbol as usize] as u32)?; 328 | 329 | // Get distance 330 | symbol = dist_table.decode_sym(self)?; 331 | let dist = EXTRA_DIST[symbol as usize] 332 | + self.get_bits(EXTRA_DBITS[symbol as usize] as u32)?; 333 | 334 | // Copy `len` bytes from `dist` bytes back 335 | let dist = dist as usize; 336 | if dist > self.output.len() { 337 | return Err(DecoderError::InvalidDistTooFar); 338 | } 339 | for _ in 0..len { 340 | let prev = self.output[self.output.len() - dist]; 341 | self.output.push(prev); 342 | } 343 | } 344 | } 345 | 346 | Ok(()) 347 | } 348 | 349 | // RFC 1951 - Section 3.2.6 350 | fn fixed_huffman(&mut self) -> Result<(), DecoderError> { 351 | let mut length = [0u16; FIX_L_CODES]; 352 | for sym in 0..FIX_L_CODES { 353 | length[sym] = match sym { 354 | 0...143 => 8, 355 | 144...255 => 9, 356 | 256...279 => 7, 357 | 280...287 => 8, 358 | _ => unreachable!(), 359 | }; 360 | } 361 | 362 | let dist = [5u16; MAX_D_CODES]; 363 | 364 | let len_table = HuffmanTable::new(&length)?; 365 | let dist_table = HuffmanTable::new(&dist)?; 366 | self.decompress_block(&len_table, &dist_table)?; 367 | 368 | Ok(()) 369 | } 370 | 371 | // RFC 1951 - Section 3.2.7 372 | fn dynamic_huffman(&mut self) -> Result<(), DecoderError> { 373 | // Lengths of each table 374 | let nlen: usize = self.get_bits(5)? as usize + 257; 375 | let ndist: usize = self.get_bits(5)? as usize + 1; 376 | let ncode: usize = self.get_bits(4)? as usize + 4; 377 | if nlen > MAX_L_CODES || ndist > MAX_D_CODES { 378 | return Err(DecoderError::HuffmanTableTooBig); 379 | } 380 | 381 | // Build temporary table to read literal/length/distance afterwards 382 | const ORDER: [usize; 19] = [ 383 | 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 384 | ]; 385 | let mut length = [0; MAX_CODES]; 386 | for idx in 0..ncode { 387 | length[ORDER[idx]] = self.get_bits(3)?; 388 | } 389 | let len_table = HuffmanTable::new(&length)?; 390 | 391 | // Get literal and length/distance 392 | let mut idx: usize = 0; 393 | while idx < nlen + ndist { 394 | let mut symbol = len_table.decode_sym(self)?; 395 | if symbol < 16 { 396 | length[idx] = symbol; 397 | idx += 1; 398 | } else { 399 | let mut len = 0; 400 | if symbol == 16 { 401 | if idx == 0 { 402 | return Err(DecoderError::InvalidBlockCodeHeader); 403 | } 404 | len = length[idx - 1]; 405 | symbol = 3 + self.get_bits(2)?; 406 | } else if symbol == 17 { 407 | symbol = 3 + self.get_bits(3)?; 408 | } else { 409 | symbol = 11 + self.get_bits(7)?; 410 | } 411 | 412 | if idx + symbol as usize > nlen + ndist { 413 | return Err(DecoderError::InvalidBlockCodeHeader); 414 | } 415 | for _ in 0..symbol { 416 | length[idx] = len; 417 | idx += 1; 418 | } 419 | } 420 | } 421 | 422 | if length[256] == 0 { 423 | return Err(DecoderError::MissingEndOfBlockCode); 424 | } 425 | 426 | let len_table = HuffmanTable::new(&length[..nlen])?; 427 | let dist_table = HuffmanTable::new(&length[nlen..])?; 428 | self.decompress_block(&len_table, &dist_table)?; 429 | 430 | Ok(()) 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /tests/branch: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | gitrs="cargo run" 4 | 5 | ./first_commit 6 | cd repo 7 | 8 | $gitrs branch new_b 9 | $gitrs checkout new_b 10 | echo "hey!" > hey 11 | $gitrs add hey 12 | $gitrs commit -m "second commit" 13 | $gitrs checkout master 14 | $gitrs checkout new_b 15 | -------------------------------------------------------------------------------- /tests/clone_remote: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | gitrs="cargo run" 4 | 5 | ./merge_non_fast_forward 6 | rm -rf copy 7 | 8 | $gitrs clone repo copy 9 | -------------------------------------------------------------------------------- /tests/detach_head: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | gitrs="cargo run" 4 | 5 | ./branch 6 | cd repo 7 | 8 | last_commit=`git rev-parse HEAD` 9 | echo "test detach" > detach 10 | $gitrs add detach 11 | $gitrs commit -m "commit detach" 12 | $gitrs checkout $last_commit 13 | $gitrs checkout master 14 | $gitrs checkout $last_commit 15 | -------------------------------------------------------------------------------- /tests/fetch_remote: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | gitrs="cargo run" 4 | 5 | ./first_commit 6 | 7 | rm -rf rem 8 | cp -r repo rem 9 | cd rem 10 | echo "diff" > newf 11 | $gitrs add newf 12 | $gitrs commit -m "new commit" 13 | cd .. 14 | 15 | cd repo 16 | $gitrs config --add remote.new_rem.url ../rem 17 | $gitrs fetch new_rem master 18 | -------------------------------------------------------------------------------- /tests/first_commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | gitrs="cargo run" 4 | 5 | rm -rf repo 6 | mkdir repo 7 | cd repo 8 | 9 | $gitrs init 10 | $gitrs config --add user.name "John Doe" 11 | $gitrs config --add user.email "john.doe@something.com" 12 | echo "hello world!" > hello 13 | $gitrs add hello 14 | $gitrs commit -m "initial commit" 15 | -------------------------------------------------------------------------------- /tests/full_demo: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | gitrs="cargo run" 4 | 5 | rm -rf repo 6 | rm -rf copy 7 | 8 | $gitrs init repo 9 | cd repo 10 | $gitrs config --add user.name "John Doe" 11 | $gitrs config --add user.email "john.doe@something.com" 12 | 13 | echo 'Hello world!' > file_a 14 | $gitrs status 15 | $gitrs add file_a 16 | $gitrs commit -m "first commit" 17 | 18 | cd .. 19 | $gitrs clone repo copy 20 | cp repo/.git/config copy/.git/config 21 | cd copy 22 | echo 'new file' > file_b 23 | $gitrs add file_b 24 | $gitrs commit -m "second commit" 25 | 26 | cd ../repo 27 | $gitrs remote add copy_remote ../copy 28 | $gitrs branch new_b 29 | $gitrs pull copy_remote master 30 | 31 | $gitrs checkout new_b 32 | echo 'new line' >> file_a 33 | $gitrs status 34 | $gitrs diff 35 | $gitrs add file_a 36 | $gitrs commit -m "third commit" 37 | $gitrs checkout master 38 | $gitrs merge new_b 39 | -------------------------------------------------------------------------------- /tests/merge_conflict: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | gitrs="cargo run" 4 | 5 | ./branch 6 | cd repo 7 | 8 | $gitrs checkout master 9 | echo 'conflict' >> hey 10 | $gitrs add hey 11 | $gitrs commit -m "conflict commit" 12 | $gitrs merge new_b 13 | -------------------------------------------------------------------------------- /tests/merge_fast_forward: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | gitrs="cargo run" 4 | 5 | ./branch 6 | cd repo 7 | 8 | $gitrs checkout master 9 | $gitrs merge new_b 10 | -------------------------------------------------------------------------------- /tests/merge_non_fast_forward: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | gitrs="cargo run" 4 | 5 | ./branch 6 | cd repo 7 | 8 | $gitrs checkout master 9 | echo 'diverge' > div 10 | $gitrs add div 11 | $gitrs commit -m "diverge commit" 12 | $gitrs merge new_b 13 | -------------------------------------------------------------------------------- /tests/pull_remote: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | gitrs="cargo run" 4 | 5 | ./first_commit 6 | 7 | rm -rf rem 8 | cp -r repo rem 9 | cd rem 10 | echo "diff" > newf 11 | $gitrs add newf 12 | $gitrs commit -m "new commit" 13 | cd .. 14 | 15 | cd repo 16 | $gitrs config --add remote.new_rem.url ../rem 17 | $gitrs pull new_rem master 18 | -------------------------------------------------------------------------------- /tests/push_remote: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | gitrs="cargo run" 4 | 5 | ./first_commit 6 | 7 | rm -rf rem 8 | mkdir rem 9 | cd rem 10 | $gitrs init 11 | cd .. 12 | 13 | cd repo 14 | $gitrs config --add remote.new_rem.url ../rem 15 | $gitrs push new_rem master 16 | --------------------------------------------------------------------------------