rif 0.2.0

Impact control system
Documentation
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
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
pub mod rel;
pub mod config;
pub mod history;
pub mod hook;
pub mod meta;

use crate::checker::Checker;
use crate::models::{LoopBranch, ListType};
use crate::utils;
use std::collections::HashSet;
use config::Config;
use rel::Relations;
use history::History;
use meta::Meta;
use crate::RifError;
use std::path::{Path, PathBuf};
use crate::consts::*;

/// Rif struct stores all iformation necessary for rif operations
pub struct Rif {
    config: Config,
    history: History,
    relation: Relations,
    meta: Meta,
    black_list: HashSet<PathBuf>,
    root_path: Option<PathBuf>,
}

impl Rif {
    /// Create new rif struct
    pub fn new(path: Option<impl AsRef<Path>>) -> Result<Self, RifError> {
        let config = Config::read_from_file(path.as_ref())?;
        let black_list = utils::get_black_list(config.git_ignore)?;
        Ok(Self {
            config,
            history: History::read_from_file(path.as_ref())?,
            relation: Relations::read_from_file(path.as_ref())?,
            meta: Meta::read_from_file(path.as_ref())?,
            black_list,
            root_path: path.map(|p| p.as_ref().clone().to_owned()),
        })
    }

    // ==========
    // External methods start

    /// Initiate given directory
    ///
    /// If directory is not supplied, initiates current working directory
    pub fn init(path: Option<impl AsRef<Path>>,create_rif_ignore: bool) -> Result<(), RifError> {
        let path = if let Some(path) = path {
            path.as_ref().to_owned()
        } else { std::env::current_dir()? };

        // Already initiated
        if path.join(RIF_DIECTORY).exists() {
            return Err(RifError::RifIoError(String::from("Directory is already initiated")));
        }

        // Crate root directory
        std::fs::create_dir(path.join(RIF_DIECTORY))?;

        // Rif relation
        let new_relations = Relations::new();
        new_relations.save_to_file(Some(&path))?;
        // Rif history
        let new_rif_history = History::new();
        new_rif_history.save_to_file(Some(&path))?;
        // Rif Config
        let new_config = Config::new();
        new_config.save_to_file(Some(&path))?;
        // Rif meta
        let new_meta = Meta::new();
        new_meta.save_to_file(Some(&path))?;
        println!("Initiated a rif directory \"{}\"", std::env::current_dir()?.display());

        // Also create rifignore file
        if create_rif_ignore {
            std::fs::write(".rifignore",".git")?;
        }
        Ok(())
    }

    /// Add new file 
    ///
    /// Files that modified, newly created, deleted files can be added but non modiifed files can
    /// alos be added with force option
    pub fn add(&mut self, files: &Vec<impl AsRef<Path>>, force: bool) -> Result<(), RifError> {
        for file in files {
            let mut path = file.as_ref().to_owned();

            // If file doesn't exist, simply ignore
            if !path.exists() {
                continue;
            }

            // If given value is "." which means that input value has not been expanded.
            // substitute with current working directory
            if path.to_str().unwrap() == "." {
                path = std::env::current_dir()?;
                self.add_directory(&path)?;
                continue;
            } else if path.is_dir() {
                self.add_directory(&path)?;
                continue;
            }

            // Don't do anything if file is in blacklist
            if self.is_in_black_list(&path) {
                continue;
            }

            // First, file is already inside
            // Second, file is new
            if self.relation.files.contains_key(&path) {
                self.add_old_file(&path, force)?;
            } else {
                self.add_new_file(&path)?;
            }
        } // for loop end

        // Update relation file
        self.relation.save_to_file(self.root_path.as_ref())?;
        self.meta.save_to_file(self.root_path.as_ref())?;
        Ok(())
    }

    /// Revert added files
    ///
    /// No files arugment revert all added files
    pub fn revert(&mut self, files: Option<&Vec<impl AsRef<Path>>>) -> Result<(), RifError> {
        if let Some(files) = files {
            for file in files {
                let path = file.as_ref();
                // Removes single item
                self.meta.remove_add_queue(&path);
            } // for loop end
        } else {
            // No argument, revert everything
            self.meta.clear();
        }

        self.meta.save_to_file(self.root_path.as_ref())?;
        Ok(())
    }

    /// Commit addition to rif struct and check impact
    ///
    /// Message is saved inside history file
    pub fn commit(&mut self, message: Option<&str>) -> Result<(), RifError> {

        // Literaly, commit needs to resolve all deleted files
        if self.relation.get_deleted_files().len() != self.meta.to_be_deleted.len() {
            return Err(RifError::CommitFail("Commit without deleted files are illegal. Rejected".to_owned()))
        }

        // delete
        for file in self.meta.to_be_deleted.clone().iter() {
            self.remove_file(file)?;
        }

        // Register new files
        for file in self.meta.to_be_registerd.clone().into_iter() {
            self.register_new_file(&file, message)?;
        }

        // force updates
        for file in self.meta.to_be_forced.iter() {
            self.relation.update_filestamp_force(&file)?;
        }

        // updates
        for file in self.meta.to_be_added.iter() {
            self.relation.update_filestamp(&file)?;

            // Add message to history
            if let Some(msg) = message {
                self.history.add_history(&file, msg)?;
                self.history.save_to_file(self.root_path.as_ref())?;
            }
        }

        // Check if added files are not empty
        if self.meta.to_be_added_later().count() != 0 {
            self.check_exec()?;
        }

        // Clear meta
        self.meta.clear();

        // Save files
        self.meta.save_to_file(self.root_path.as_ref())?;
        self.relation.save_to_file(self.root_path.as_ref())?;
        self.history.save_to_file(self.root_path.as_ref())?;

        Ok(())
    }
    
    /// Discard file change and updated filestamp
    ///
    /// This cannot be reverted so multiple files are not supported
    pub fn discard(&mut self, file: impl AsRef<Path>) -> Result<(), RifError> {
        self.relation.discard_change(file.as_ref())?;
        self.relation.save_to_file(self.root_path.as_ref())?;
        Ok(())
    }

    // TODO
    // Needs serious consideration
    // Whether new_name exists in real directory
    // Whether sany check is activated so that insane rename should be not executed
    /// Rename rif file 
    ///
    /// If file exist, change the file name in filesystem
    pub fn rename(&mut self, source_name: &str, new_name: &str) -> Result<(), RifError> {
        let source_name = Path::new(source_name);
        let new_name = Path::new(new_name);
        if let Some(_) = self.relation.files.get(new_name) {
            return Err(RifError::RenameFail(format!("Rename target: \"{}\" already exists", new_name.display())));
        }

        // Rename file if it exsits and inside relation files
        if source_name.exists() && self.relation.files.contains_key(source_name) {
            if !new_name.exists() {
                std::fs::rename(source_name, new_name)?;
            } else {
                return Err(RifError::RenameFail("New name already exists".to_owned()));
            }
        }

        self.relation.rename_file(source_name, new_name)?;
        self.relation.save_to_file(self.root_path.as_ref())?;
        Ok(())
    }

    /// Remove file from rif
    pub fn remove(&mut self, files: &Vec<impl AsRef<Path>>) -> Result<(), RifError> {
        for file in files {
            self.remove_file(file.as_ref())?;
        }
        self.relation.save_to_file(self.root_path.as_ref())?;
        self.history.save_to_file(self.root_path.as_ref())?;
        Ok(())
    }

    /// Set reference of file
    pub fn set(&mut self, file: &Path, refs : &Vec<impl AsRef<Path>>) -> Result<(), RifError> {
        let refs: HashSet<PathBuf> = refs.iter().map(|a| a.as_ref().to_owned()).collect();

        self.relation.add_reference(file, &refs)?;
        self.relation.save_to_file(self.root_path.as_ref())?;
        Ok(())
    }

    /// Unset reference of file
    pub fn unset(&mut self, file: &Path, refs : &Vec<impl AsRef<Path>>) -> Result<(), RifError> {
        let refs: HashSet<PathBuf> = refs.iter().map(|a| a.as_ref().to_owned()).collect();

        self.relation.remove_reference(file, &refs)?;
        self.relation.save_to_file(self.root_path.as_ref())?;
        Ok(())
    }

    /// Show current status of rif project
    pub fn status(&mut self, ignore: bool, verbose: bool) -> Result<(), RifError> {
        // Remove deleted files from to be added.
        self.meta.remove_non_exsitent();

        let mut to_be_added_later = self.meta.to_be_added_later().peekable();

        if let Some(_) = to_be_added_later.peek() {
            println!("# Changes to be commited :");
            for item in &self.meta.to_be_registerd {
                let format = format!("    new file : {}", &item.to_str().unwrap());
                println!("{}", utils::green(&format));
            }
            for item in &self.meta.to_be_added {
                let format = format!("    modified : {}", &item.to_str().unwrap());
                println!("{}", utils::green(&format));
            }
            for item in &self.meta.to_be_forced {
                let format = format!("    forced   : {}", &item.to_str().unwrap());
                println!("{}", utils::green(&format));
            }
            for item in &self.meta.to_be_deleted {
                let format = format!("    deleted  : {}", &item.to_str().unwrap());
                println!("{}", utils::green(&format));
            }
            println!("");
        }
    
        println!("# Changed files :");
        self.relation.track_modified_files(self.meta.to_be_added_later())?;

        // Ignore untracked files
        if !ignore {
            // Default black list only includes .rif file for now
            // Currently only check relative paths,or say, stripped path
            println!("\n# Untracked files :");
            self.relation.track_unregistered_files(&self.black_list, &self.meta.to_be_registerd)?;
        }

        if verbose {
            println!("\n# Current rif status:\n---");
            print!("{}", self.relation);
        }

        // Save meta file
        self.meta.save_to_file(self.root_path.as_ref())?;

        Ok(())
    }

    /// Show file informations of rif project
    pub fn list(&self, file : Option<impl AsRef<Path>>, list_type: ListType, depth: Option<usize>) -> Result<(), RifError> {
        if let Some(file) = file {
            // Print relation tree
            self.relation.display_file_depth(file.as_ref(), 0)?;

            // Also print update 
            println!("\n# History : ");
            self.history.print_history(file.as_ref())?;
        }  else { // No file was given
            match list_type {
                ListType::All => {
                    self.relation.display_depth(depth.unwrap_or(0))?;
                }
                ListType::Stale => {
                    self.relation.display_stale_files(depth.unwrap_or(0))?;
                }
                // Updated is not yet added
                _ => (),
            }
        }
        Ok(())
    }

    /// Show data of rif project
    pub fn data(&self, data_type: Option<&str>, compact: bool) -> Result<(), RifError> {
        if let Some(data_type) = data_type {
            match data_type {
                "meta"  => {
                    println!("{:#?}", self.meta);
                }
                "history" => {
                    println!("{:#?}", self.history);
                }
                _ => () // This doesn't happen
            }
        } else {
            if compact {
                println!("{:?}", self.relation);
            } else {
                println!("{:#?}", self.relation);
            }
        }

        Ok(())
    }

    /// Show files that depend on given file
    pub fn depend(&self, file: &Path)  -> Result<(), RifError> {
        let dependes = self.relation.find_depends(file)?;
        println!("Files that depends on \"{}\"", file.display());
        println!("=====");
        for item in dependes {
            println!("{}", utils::green(&item.display().to_string()));
        }
        Ok(())
    }

    /// Check file references
    pub fn check(&mut self) -> Result<(), RifError> {
        if self.relation.get_deleted_files().len() != 0 {
            return Err(RifError::CheckerError("Check with deleted files are illegal. Rejected".to_owned()));
        }

        self.check_exec()?;
        Ok(())
    }

    /// Check sanity of rif proeject
    pub fn sanity(&mut self, fix: bool) -> Result<(), RifError> {
        // NOTE ::: You don't have to manually call sanity check
        // Because read operation always check file sanity after reading a file
        // and return erros if sanity was not assured.
        if fix {
            self.relation.sanity_fix()?;
            self.relation.save_to_file(self.root_path.as_ref())?;
            println!("Sucessfully fixed the rif file");
        } else {
            self.relation.sanity_check()?;
            println!("Sucessfully checked the rif file");
        }
        Ok(())
    }

    // External methods end

    // MISC methods start
    //
    
    /// Check file relations(impact of changes)
    fn check_exec(&mut self) -> Result<(), RifError> {
        // Check relations(impact)
        let mut checker = Checker::with_relations(&self.relation)?;
        let changed_files = checker.check(&mut self.relation)?;

        if changed_files.len() != 0 && self.config.hook.trigger {
            println!("\nHook Output");
            self.config.hook.execute(changed_files)?;
        }

        Ok(())
    }
    
    /// Check if given path is inside black_list
    fn is_in_black_list(&self, path: &Path) -> bool {
        // File is in rif ignore
        if let Some(_) = self.black_list.get(path) {
            // If File is not configurable
            // It's not allowed by the program
            // else it's allowd by the program 
            if BLACK_LIST.to_vec().contains(&path.to_str().unwrap()) {
                eprintln!("File : \"{}\" is not allowed", path.display());
            } else {
                println!("\"{}\" is in rifignore file, which is ignored.", path.display());
            }
            return true;
        } 

        false
    }

    /// Add new file to rif 
    fn add_new_file(&mut self, file: &Path) -> Result<(), RifError> {
        self.meta.to_be_registerd.insert(file.to_owned());
        Ok(())
    }

    /// Remove new file to rif 
    fn remove_file(&mut self, file: &Path) -> Result<(), RifError> {
        self.relation.remove_file(file)?;
        self.history.remove_file(file)?;
        Ok(())
    }

    /// Register a new file
    fn register_new_file(&mut self, file: &Path, message: Option<&str>) -> Result<(), RifError> {
        // Closure to recursively get inside directory and add files
        let mut closure = |entry_path : PathBuf| -> Result<LoopBranch, RifError> {
            let striped_path = utils::relativize_path(&entry_path)?;

            // Early return if file or directory is in black_list
            // Need to check the black_list once more because closure checks nested
            // directory that is not checked in outer for loop
            if let Some(_) = self.black_list.get(&striped_path) {
                if striped_path.is_dir() {
                    return Ok(LoopBranch::Exit);
                } 
                else {
                    return Ok(LoopBranch::Continue);
                }
            }

            if !self.relation.add_file(&striped_path)? { return Ok(LoopBranch::Continue); }
            Ok(LoopBranch::Continue)
        }; // Closure end here 

        // if path is a directory then recusively get into it
        // if path is a file then simply add a file
        if file.is_dir() {
            utils::walk_directory_recursive(file, &mut closure)?;
        } else { 
            let file = utils::relativize_path(file)?;

            // TODO 
            // THis returns bools, is it not needed?
            // File was not added e.g. file already exists
            self.relation.add_file(&file)?;
            self.history.add_history(&file, message.unwrap_or(""))?;
        }
        Ok(())
    }
    
    /// Add directory
    fn add_directory(&mut self, dir: &Path) -> Result<(), RifError> {
        let tracked = self.relation.files.keys().cloned().collect::<Vec<PathBuf>>();
        let modified = self.relation.get_modified_files()?.clone();
        let mut deleted = self.relation.get_deleted_files().clone();
        let mut to_be_deleted = HashSet::new();
        let mut to_be_added = HashSet::new();
        let mut to_be_registerd = HashSet::new();
        // Closure to recursively get inside directory and add files
        let mut closure = |entry_path : PathBuf| -> Result<LoopBranch, RifError> {
            let striped_path = utils::relativize_path(&entry_path)?;
            // Early return if file or directory is in black_list
            // Need to check the black_list once more because closure checks nested
            // directory that is not checked in outer for loop
            if let Some(_) = self.black_list.get(&striped_path) {
                if striped_path.is_dir() {
                    return Ok(LoopBranch::Exit);
                } 
                else {
                    return Ok(LoopBranch::Continue);
                }
            }

            // If directory go inside and don't add the directory
            if striped_path.is_dir() {
                // Only retain deleted files that are not specified in directory
                // or say, paths that don't start with entry path
                deleted.retain(|path| {
                    let is_inside = path.starts_with(&entry_path);
                    // Add to to_be_deleted
                    if is_inside {
                        to_be_deleted.insert(path.to_owned());
                    }
                    // Only retain files that is not inside the directory
                    !is_inside
                });
                return Ok(LoopBranch::Continue);
            } 

            if modified.contains(&striped_path) {
                // Is modified file
                to_be_added.insert(striped_path);
            } else if !tracked.contains(&striped_path) {
                // NOt in a tracking file
                to_be_registerd.insert(striped_path);
            }
            Ok(LoopBranch::Continue)
        }; // Closure end here 

        utils::walk_directory_recursive(dir, &mut closure)?;

        self.meta.to_be_registerd.extend(to_be_registerd);
        self.meta.to_be_added.extend(to_be_added);
        self.meta.to_be_deleted.extend(to_be_deleted);

        Ok(())
    }

    /// Add old file
    fn add_old_file(&mut self, file: &Path, force: bool) -> Result<(), RifError> {
        if file.exists() {
            self.meta.queue_added(file, force);
        } else {
            self.meta.queue_deleted(file);
        }
        Ok(())
    }

    // MISC methods end
}