rs_git_lib/
lib.rs

1//! # rs-git-lib
2//!
3//! A Rust Native Library for Git
4//!
5#[macro_use]
6extern crate num_derive;
7#[macro_use]
8extern crate nom;
9
10mod delta;
11mod packfile;
12mod store;
13mod transport;
14mod utils;
15
16use crate::packfile::refs::{create_refs, resolve_ref, update_head, Refs};
17use crate::packfile::PackFile;
18use crate::store::commit::Commit;
19use crate::store::object::{GitObject, GitObjectType};
20use crate::store::tree::{EntryMode, Tree, TreeEntry};
21use crate::utils::sha1_hash;
22use byteorder::{BigEndian, WriteBytesExt};
23use rustc_serialize::hex::FromHex;
24use std::fs;
25use std::fs::{File, Permissions};
26use std::io::{Error, ErrorKind, Result as IOResult, Write};
27use std::iter::FromIterator;
28use std::os::unix::fs::MetadataExt;
29use std::os::unix::fs::PermissionsExt;
30use std::path::PathBuf;
31use transport::Transport;
32
33/// A Git Repository
34pub struct Repo {
35    dir: String,
36    refs: Refs,
37    count_objects: usize,
38    pack: Option<PackFile>,
39}
40
41impl Repo {
42    /// clone a git repo
43    /// # Arguments
44    ///
45    /// * `url` - a string that holds de repo url from where we will clone
46    /// * `dir` - an optional string with the path where the cloned repo will be out.
47    /// If None the dir wil be created based on url.
48    ///
49    /// # Examples
50    ///
51    /// ```
52    /// // will write the repo on /tmp/rs-git directory
53    /// use rs_git_lib::Repo;
54    /// let repo = Repo::clone_from("https://github.com/lnds/rs-git-lib.git", Some("/tmp/rs-git".to_string()));
55    /// ```
56    ///
57    pub fn clone_from(url: &str, dir: Option<String>) -> IOResult<Self> {
58        let mut transport = Transport::from_url(url, dir)?;
59        let dir = transport.dir();
60        let refs = transport.discover_refs()?;
61        let mut packfile_parser = transport.fetch_packfile(&refs)?;
62        let packfile = packfile_parser.parse(Some(&dir), None)?;
63        packfile.write(&dir)?;
64        create_refs(&dir, &refs)?;
65        update_head(&dir, &refs)?;
66        let repo = Repo {
67            dir,
68            refs,
69            count_objects: packfile_parser.count_objects(),
70            pack: Some(packfile),
71        };
72        repo.checkout_head()?;
73        Ok(repo)
74    }
75
76    ///
77    /// return references of cloned repo
78    ///
79    /// ```
80    /// use rs_git_lib::Repo;
81    /// let repo = Repo::clone_from("https://github.com/lnds/rs-git-lib.git", Some("/tmp/rs-git".to_string()));
82    /// let refs = repo.unwrap().refs();
83    /// assert_eq!(refs[0].name, "HEAD");
84    /// assert_eq!(refs[1].name, "refs/heads/master");
85    /// ```
86    pub fn refs(self) -> Refs {
87        self.refs
88    }
89
90    ///
91    /// return output directory of cloned repo
92    ///
93    /// ```
94    /// use rs_git_lib::Repo;
95    /// let repo = Repo::clone_from("https://github.com/lnds/rs-git-lib.git", Some("/tmp/rs-git".to_string())).unwrap();
96    /// assert_eq!(repo.dir(), "/tmp/rs-git");
97    /// ```
98    pub fn dir(self) -> String {
99        self.dir
100    }
101
102    ///
103    /// return how many objects are in the repo
104    ///
105    /// ```
106    /// use rs_git_lib::Repo;
107    /// let repo = Repo::clone_from("https://github.com/lnds/rs-git-lib.git", Some("/tmp/rs-git".to_string())).unwrap();
108    /// assert!(repo.count_objects() > 1);
109    /// let repo = Repo::clone_from("https://github.com/lnds/redondeo.git", Some("/tmp/redondeo".to_string())).unwrap();
110    /// assert_eq!(repo.count_objects(), 25);
111    /// ```
112    pub fn count_objects(self) -> usize {
113        self.count_objects
114    }
115
116    ///
117    /// return the list of commit objects from current branch
118    ///
119    /// ```
120    /// use rs_git_lib::Repo;
121    /// let repo = Repo::clone_from("https://github.com/lnds/redondeo.git", Some("/tmp/redondeo".to_string())).unwrap();
122    /// let commits = repo.commits().unwrap();
123    /// assert_eq!(commits.len(), 5);
124    /// assert_eq!(commits[4].as_commit().unwrap().get_message(), "Initial commit".to_string())
125    pub fn commits(&self) -> IOResult<Vec<GitObject>> {
126        let tip = resolve_ref(&self.dir, "HEAD")?;
127        let mut result = Vec::new();
128        let head = self.read_object(&tip)?;
129        if let Some(commit) = head.as_commit() {
130            result.push(head.clone());
131            self.search_parents(&mut result, &commit)?;
132        }
133        Ok(result)
134    }
135
136    fn search_parents(
137        &self,
138        mut vec_of_commits: &mut Vec<GitObject>,
139        commit: &Commit,
140    ) -> IOResult<()> {
141        if commit.has_parents() {
142            for parent in commit.parents.iter() {
143                let obj = self.read_object(parent)?;
144                if obj.object_type == GitObjectType::Commit {
145                    vec_of_commits.push(obj.clone());
146                    self.search_parents(&mut vec_of_commits, &obj.as_commit().unwrap())?;
147                }
148            }
149        }
150        Ok(())
151    }
152
153    fn checkout_head(&self) -> IOResult<()> {
154        let tip = resolve_ref(&self.dir, "HEAD")?;
155        let mut idx = Vec::new();
156        self.walk(&tip)
157            .and_then(|t| self.walk_tree(&self.dir, &t, &mut idx).ok());
158        write_index(&self.dir, &mut idx[..])?;
159        Ok(())
160    }
161
162    fn walk(&self, sha: &str) -> Option<Tree> {
163        self.read_object(sha)
164            .ok()
165            .and_then(|object| match object.object_type {
166                GitObjectType::Commit => object.as_commit().and_then(|c| self.extract_tree(&c)),
167                GitObjectType::Tree => object.as_tree(),
168                _ => None,
169            })
170    }
171
172    fn walk_tree(&self, parent: &str, tree: &Tree, idx: &mut Vec<IndexEntry>) -> IOResult<()> {
173        for entry in &tree.entries {
174            let &TreeEntry {
175                ref path,
176                ref mode,
177                ref sha,
178            } = entry;
179            let mut full_path = PathBuf::new();
180            full_path.push(parent);
181            full_path.push(path);
182            match *mode {
183                EntryMode::SubDirectory => {
184                    fs::create_dir_all(&full_path)?;
185                    let path_str = full_path.to_str().unwrap();
186                    self.walk(sha)
187                        .and_then(|t| self.walk_tree(path_str, &t, idx).ok());
188                }
189                EntryMode::Normal | EntryMode::Executable => {
190                    let object = self.read_object(sha)?;
191                    let mut file = File::create(&full_path)?;
192                    file.write_all(&object.content[..])?;
193                    let meta = file.metadata()?;
194                    let mut perms: Permissions = meta.permissions();
195
196                    let raw_mode = match *mode {
197                        EntryMode::Normal => 33188,
198                        _ => 33261,
199                    };
200                    perms.set_mode(raw_mode);
201                    fs::set_permissions(&full_path, perms)?;
202
203                    let idx_entry = get_index_entry(
204                        &self.dir,
205                        full_path.to_str().unwrap(),
206                        mode.clone(),
207                        sha.clone(),
208                    )?;
209                    idx.push(idx_entry);
210                }
211                ref e => panic!("Unsupported Entry Mode {:?}", e),
212            }
213        }
214        Ok(())
215    }
216
217    pub fn read_object(&self, sha: &str) -> IOResult<GitObject> {
218        // Attempt to read from disk first
219        GitObject::open(&self.dir, sha).or_else(|_| {
220            // If this isn't there, read from the packfile
221            let pack = self
222                .pack
223                .as_ref()
224                .ok_or_else(|| Error::new(ErrorKind::Other, "can't read pack object"))?;
225            pack.find_by_sha(sha).map(|o| o.unwrap())
226        })
227    }
228
229    fn extract_tree(&self, commit: &Commit) -> Option<Tree> {
230        let sha = commit.tree;
231        self.read_tree(sha)
232    }
233
234    fn read_tree(&self, sha: &str) -> Option<Tree> {
235        self.read_object(sha).ok().and_then(|obj| obj.as_tree())
236    }
237}
238
239#[derive(Debug)]
240struct IndexEntry {
241    ctime: i64,
242    mtime: i64,
243    device: i32,
244    inode: u64,
245    mode: u16,
246    uid: u32,
247    gid: u32,
248    size: i64,
249    sha: Vec<u8>,
250    file_mode: EntryMode,
251    path: String,
252}
253
254fn write_index(repo: &str, entries: &mut [IndexEntry]) -> IOResult<()> {
255    let mut path = PathBuf::new();
256    path.push(repo);
257    path.push(".git");
258    path.push("index");
259    let mut idx_file = File::create(path)?;
260    let encoded = encode_index(entries)?;
261    idx_file.write_all(&encoded[..])?;
262    Ok(())
263}
264
265fn encode_index(idx: &mut [IndexEntry]) -> IOResult<Vec<u8>> {
266    let mut encoded = index_header(idx.len())?;
267    idx.sort_by(|a, b| a.path.cmp(&b.path));
268    let entries: Result<Vec<_>, _> = idx.iter().map(|e| encode_entry(e)).collect();
269    let mut encoded_entries = entries?.concat();
270    encoded.append(&mut encoded_entries);
271    let mut hash = sha1_hash(&encoded);
272    encoded.append(&mut hash);
273    Ok(encoded)
274}
275
276fn index_header(num_entries: usize) -> IOResult<Vec<u8>> {
277    let mut header = Vec::with_capacity(12);
278    let magic = 1_145_655_875; // "DIRC"
279    let version: u32 = 2;
280    header.write_u32::<BigEndian>(magic)?;
281    header.write_u32::<BigEndian>(version)?;
282    header.write_u32::<BigEndian>(num_entries as u32)?;
283    Ok(header)
284}
285
286fn encode_entry(entry: &IndexEntry) -> IOResult<Vec<u8>> {
287    let mut buf: Vec<u8> = Vec::with_capacity(62);
288    let &IndexEntry {
289        ctime,
290        mtime,
291        device,
292        inode,
293        mode,
294        uid,
295        gid,
296        size,
297        ..
298    } = entry;
299    let &IndexEntry {
300        ref sha,
301        ref file_mode,
302        ref path,
303        ..
304    } = entry;
305    let flags = (path.len() & 0xFFF) as u16;
306    let (encoded_type, perms) = match *file_mode {
307        EntryMode::Normal | EntryMode::Executable => (8u32, mode as u32),
308        EntryMode::Symlink => (10u32, 0u32),
309        EntryMode::Gitlink => (14u32, 0u32),
310        _ => unreachable!("Tried to create an index entry for a non-indexable object"),
311    };
312    let encoded_mode = (encoded_type << 12) | perms;
313
314    let path_and_padding = {
315        // This is the total length of the index entry file
316        // NUL-terminated and padded with enough NUL bytes to pad
317        // the entry to a multiple of 8 bytes.
318        //
319        // The -2 is because of the amount needed to compensate for the flags
320        // only being 2 bytes.
321        let mut v: Vec<u8> = Vec::from_iter(path.as_bytes().iter().cloned());
322        v.push(0u8);
323        let padding_size = 8 - ((v.len() - 2) % 8);
324        let padding = vec![0u8; padding_size];
325        if padding_size != 8 {
326            v.extend(padding);
327        }
328        v
329    };
330
331    buf.write_u32::<BigEndian>(ctime as u32)?;
332    buf.write_u32::<BigEndian>(0u32)?;
333    buf.write_u32::<BigEndian>(mtime as u32)?;
334    buf.write_u32::<BigEndian>(0u32)?;
335    buf.write_u32::<BigEndian>(device as u32)?;
336    buf.write_u32::<BigEndian>(inode as u32)?;
337    buf.write_u32::<BigEndian>(encoded_mode)?;
338    buf.write_u32::<BigEndian>(uid as u32)?;
339    buf.write_u32::<BigEndian>(gid as u32)?;
340    buf.write_u32::<BigEndian>(size as u32)?;
341    buf.extend_from_slice(&sha);
342    buf.write_u16::<BigEndian>(flags)?;
343    buf.extend(path_and_padding);
344    Ok(buf)
345}
346
347fn get_index_entry(
348    root: &str,
349    path: &str,
350    file_mode: EntryMode,
351    sha: String,
352) -> IOResult<IndexEntry> {
353    let file = File::open(path)?;
354    let meta = file.metadata()?;
355
356    // We need to remove the repo path from the path we save on the index entry
357    // FIXME: This doesn't need to be a path since we just discard it again
358    let relative_path = PathBuf::from(path.trim_start_matches(root).trim_start_matches('/'));
359    let decoded_sha = sha
360        .from_hex()
361        .map_err(|_| Error::new(ErrorKind::Other, "can't decode sha"))?;
362
363    Ok(IndexEntry {
364        ctime: meta.ctime(),
365        mtime: meta.mtime(),
366        device: meta.dev() as i32,
367        inode: meta.ino(),
368        mode: meta.mode() as u16,
369        uid: meta.uid(),
370        gid: meta.gid(),
371        size: meta.size() as i64,
372        sha: decoded_sha,
373        file_mode,
374        path: relative_path.to_str().unwrap().to_owned(),
375    })
376}