-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.rs
367 lines (297 loc) · 9.7 KB
/
main.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
use anyhow::{bail, Context, Result};
use chrono::{DateTime, Local};
use clap::{Parser, Subcommand};
use flate2::write::ZlibEncoder;
use flate2::Compression;
use std::collections::HashSet;
use std::fmt::{Display, Formatter};
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
#[derive(Debug, Parser)] // requires `derive` feature
#[command(version, about)]
pub(crate) struct Arguments {
#[command(subcommand)]
pub(crate) command: Command,
}
#[derive(Debug, Subcommand)]
pub(crate) enum Command {
Init,
Commit,
}
pub const ERROR_EXIT_CODE: i32 = 1;
fn main() {
let args = Arguments::parse();
if let Err(err) = run(args) {
eprintln!("{:?}", err);
std::process::exit(ERROR_EXIT_CODE);
}
}
fn run(args: Arguments) -> Result<()> {
match args.command {
Command::Init => init()?,
Command::Commit => commit()?,
}
Ok(())
}
const NEOGIT_DIRECTORY: &str = ".git";
const NEOGIT_OBJECTS_DIRECTORY: &str = "objects";
const NEOGIT_REFERENCES_DIRECTORY: &str = "refs/heads";
const NEOGIT_INTERNAL_DIRECTORY: [&str; 2] =
[NEOGIT_OBJECTS_DIRECTORY, NEOGIT_REFERENCES_DIRECTORY];
fn init() -> Result<()> {
let root_directory = get_root_directory()?;
let neogit_directory = get_neogit_directory(&root_directory)?;
for internal_directory in NEOGIT_INTERNAL_DIRECTORY.iter() {
let mut internal_directory_path = neogit_directory.clone();
internal_directory_path.push(internal_directory);
std::fs::create_dir_all(&internal_directory_path).context(format!(
"Unable to create the directory {:?}.",
internal_directory_path
))?;
}
Ok(())
}
fn get_root_directory() -> Result<PathBuf> {
std::env::current_dir().context("Unable to open the current directory.")
}
fn get_neogit_directory(root_directory: &PathBuf) -> Result<PathBuf> {
let mut neogit_directory = root_directory.clone();
neogit_directory.push(NEOGIT_DIRECTORY);
Ok(neogit_directory)
}
fn commit() -> Result<()> {
// TODO - To get around adding the full paths to the commits.
let root_directory = PathBuf::from(r".");
let neogit_directory = get_neogit_directory(&root_directory)?;
if !neogit_directory.exists() {
bail!("Not in a Git repository, no '.git' folder.");
}
let mut objects_directory = neogit_directory.clone();
objects_directory.push(NEOGIT_OBJECTS_DIRECTORY);
let tree = get_tree(&root_directory, &objects_directory)?;
let author = Author::new(
"DeveloperC".to_string(),
"[email protected]".to_string(),
Local::now(),
);
let commit = Commit::new(&tree, &author, &author, "feat: initial bootstrapped commit");
write_object_to_filesystem(&objects_directory, &commit)?;
let branch = "main";
write_branch_to_head(&neogit_directory, branch)?;
write_object_id_to_branch(&neogit_directory, commit.get_object_id(), branch)
}
fn get_tree(directory: &PathBuf, objects_directory: &PathBuf) -> Result<Tree> {
let mut blobs: HashSet<Blob> = HashSet::new();
let entries = std::fs::read_dir(directory).context("Unable to read entries in directory.")?;
for entry in entries {
let entry = entry.context("Unable to read entry.")?;
let path = entry.path();
if path.is_file() {
let file_content = std::fs::read_to_string(&path)
.context(format!("Unable to read the content of {:?}.", path))?;
let blob = Blob::new(file_content, path.clone());
blobs.insert(blob);
}
}
// Write all blobs to filesystems.
for blob in blobs.iter() {
write_object_to_filesystem(&objects_directory, blob)?;
}
// Build tree referencing all the blobs.
let tree = Tree::new(blobs);
write_object_to_filesystem(&objects_directory, &tree)?;
Ok(tree)
}
pub fn write_object_to_filesystem(objects_directory: &PathBuf, object: &dyn Object) -> Result<()> {
let (object_directory, object_file) =
get_object_directory_and_file_paths(objects_directory, object.get_object_id());
std::fs::create_dir_all(&object_directory).context(format!(
"Unable to create directory {:?}.",
object_directory
))?;
let mut tmp_object_file = object_directory.clone();
tmp_object_file.push(".tmp");
let mut file = File::create(&tmp_object_file)
.context(format!("Unable to create the file {:?}.", tmp_object_file))?;
let compressed_bytes = get_compressed_bytes(object.get_content())?;
file.write_all(&compressed_bytes).context(format!(
"Unable to write to the file {:?}.",
tmp_object_file
))?;
std::fs::rename(&tmp_object_file, &object_file).context(format!(
"Unable to move the temporary file {:?} to {:?}.",
tmp_object_file, object_file
))
}
fn get_compressed_bytes(content: &[u8]) -> Result<Vec<u8>> {
let mut zlib = ZlibEncoder::new(Vec::new(), Compression::default());
zlib.write_all(content)
.context("Unable to compress content.")?;
zlib.finish().context("Unable to compress content.")
}
fn get_object_directory_and_file_paths(
objects_directory: &PathBuf,
object_id: &[u8],
) -> (PathBuf, PathBuf) {
let object_id_hex = hex::encode(object_id);
let object_directory_name = &object_id_hex[0..2];
let mut object_directory = objects_directory.clone();
object_directory.push(object_directory_name);
let object_file_name = &object_id_hex[2..];
let mut object_file = object_directory.clone();
object_file.push(object_file_name);
(object_directory, object_file)
}
pub fn write_branch_to_head(neogit_directory: &PathBuf, branch: &str) -> Result<()> {
let mut head_file = neogit_directory.clone();
head_file.push("HEAD");
std::fs::write(head_file, format!("ref: refs/heads/{}", branch))
.context("Unable to write to the HEAD file.")
}
pub fn write_object_id_to_branch(
neogit_directory: &PathBuf,
object_id: &[u8],
branch: &str,
) -> Result<()> {
let mut branch_file = neogit_directory.clone();
branch_file.push(format!("{}/{}", NEOGIT_REFERENCES_DIRECTORY, branch));
File::create(&branch_file)
.context("Unable to open the branch reference file.")?
.write_all(hex::encode(object_id).as_ref())
.context("Unable to write to the HEAD file.")
}
use sha1::{Digest, Sha1};
pub trait Object {
fn get_object_id(&self) -> &[u8];
fn get_content(&self) -> &[u8];
}
fn calculate_object_id(content: &[u8]) -> Vec<u8> {
Sha1::digest(&content).to_vec()
}
#[derive(PartialEq, Eq, Hash)]
pub struct Blob {
name: PathBuf,
object_id: Vec<u8>,
content: Vec<u8>,
}
impl Blob {
pub fn new(file_content: String, name: PathBuf) -> Self {
let content = format!("blob {}\x00{}", file_content.len(), file_content)
.as_bytes()
.to_vec();
let blob = Blob {
name,
object_id: calculate_object_id(&content),
content,
};
blob
}
pub fn get_path(&self) -> &PathBuf {
&self.name
}
}
impl Object for Blob {
fn get_object_id(&self) -> &[u8] {
&self.object_id
}
fn get_content(&self) -> &[u8] {
&self.content
}
}
pub struct Tree {
object_id: Vec<u8>,
content: Vec<u8>,
}
impl Tree {
pub fn new(entries: HashSet<Blob>) -> Self {
let mut sorted_entries: Vec<Blob> = entries.into_iter().collect();
sorted_entries.sort_by(|a, b| a.get_path().cmp(b.get_path()));
let mut entries_content: Vec<u8> = vec![];
for entry in &sorted_entries {
let entry_name = entry
.get_path()
.display()
.to_string()
.trim_start_matches("./")
.to_string();
let tree_entry = format!("100644 {}\x00", entry_name);
entries_content.extend(tree_entry.as_bytes());
entries_content.extend(entry.get_object_id());
}
let mut content = format!("tree {}\x00", entries_content.len())
.as_bytes()
.to_vec();
content.extend(entries_content);
let tree = Tree {
object_id: calculate_object_id(&content),
content,
};
tree
}
}
impl Object for Tree {
fn get_object_id(&self) -> &[u8] {
&self.object_id
}
fn get_content(&self) -> &[u8] {
&self.content
}
}
pub struct Author {
name: String,
email: String,
authored_at: DateTime<Local>,
}
impl Author {
pub fn new(name: String, email: String, authored_at: DateTime<Local>) -> Self {
Author {
name,
email,
authored_at,
}
}
}
impl Display for Author {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} <{}> {}",
self.name,
self.email,
self.authored_at.format("%s %z")
)
}
}
pub struct Commit {
object_id: Vec<u8>,
content: Vec<u8>,
}
impl Commit {
pub fn new(tree: &Tree, author: &Author, commiter: &Author, message: &str) -> Self {
let commit_content = format!(
"tree {}\nauthor {}\ncommitter {}\n\n{}",
hex::encode(tree.get_object_id()),
author,
commiter,
message
);
let mut content = format!("commit {}\x00", commit_content.len())
.as_bytes()
.to_vec();
content.extend(commit_content.as_bytes());
let commit = Commit {
object_id: calculate_object_id(&content),
content,
};
commit
}
}
impl Object for Commit {
fn get_object_id(&self) -> &[u8] {
&self.object_id
}
fn get_content(&self) -> &[u8] {
&self.content
}
}