1use std::{
31 fmt::Display,
32 path::{Path, PathBuf},
33};
34
35use serde::{Deserialize, Deserializer, Serialize};
36
37#[cfg(not(any(feature = "local", feature = "embedded")))]
38compile_error!(
39 "gixor needs to know where the boilerplates come from: enable `local` to keep clones on the \
40 file system, or `embedded` to compile a snapshot in."
41);
42
43#[cfg(all(feature = "local", feature = "embedded"))]
44compile_error!("The features `local` and `embedded` cannot be enabled at the same time.");
45
46pub mod aliases;
47#[cfg(feature = "local")]
48pub mod gitbridge;
49pub mod repos;
50mod source;
51
52pub type Result<T> = std::result::Result<T, Error>;
54
55#[derive(Debug)]
57pub enum Error {
58 Array(Vec<Error>),
60 Alias(String),
62 AliasNotFound(String),
64 BoilerplateNotFound(String),
66 FileNotFound(PathBuf),
68 Fatal(String),
70 Git(String),
72 IO(std::io::Error),
74 Json(serde_json::Error),
76 RepositoryNotFound(String),
78}
79
80impl Error {
81 pub fn to_err<T>(item: T, errs: Vec<Error>) -> Result<T> {
82 if errs.is_empty() {
83 Ok(item)
84 } else if errs.len() == 1 {
85 Err(errs.into_iter().next().unwrap())
86 } else {
87 Err(Error::Array(errs))
88 }
89 }
90
91 pub fn vec_result_to_result_vec<T>(vec: Vec<Result<T>>) -> Result<Vec<T>> {
95 let mut ok_items = vec![];
96 let mut errs = vec![];
97 for r in vec {
98 match r {
99 Ok(item) => ok_items.push(item),
100 Err(e) => errs.push(e),
101 }
102 }
103 Error::to_err(ok_items, errs)
104 }
105}
106
107impl Display for Error {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 use Error::*;
110 match self {
111 Array(errs) => {
112 for (i, e) in errs.iter().enumerate() {
113 if i > 0 {
114 writeln!(f)?;
115 }
116 write!(f, "{e}")?;
117 }
118 Ok(())
119 }
120 Alias(msg) => write!(f, "{msg}"),
121 AliasNotFound(name) => write!(f, "{name}: alias not found"),
122 BoilerplateNotFound(name) => write!(f, "{name}: boilerplate not found"),
123 FileNotFound(path) => write!(f, "{}: file not found", path.display()),
124 Git(e) => write!(f, "Git error: {e}"),
125 IO(e) => write!(f, "IO error: {e}"),
126 Json(e) => write!(f, "JSON error: {e}"),
127 Fatal(msg) => write!(f, "Fatal error: {msg}"),
128 RepositoryNotFound(name) => write!(f, "{name}: repository not found"),
129 }
130 }
131}
132
133mod routine;
134
135pub fn entries<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
140 log::info!("Find current entries from {}", path.as_ref().display());
141 routine::entries(path)
142}
143
144pub fn find_target_repositories<S: AsRef<str>>(
155 gixor: &Gixor,
156 repository_names: Vec<S>,
157) -> Result<Vec<&repos::Repository>> {
158 log::info!(
159 "find_target_repositories: repository_names={:?}",
160 repository_names
161 .iter()
162 .map(|s| s.as_ref())
163 .collect::<Vec<_>>()
164 );
165 routine::find_target_repositories(gixor, repository_names)
166}
167
168#[derive(Debug, Clone)]
172pub struct Name {
173 pub repository_name: Option<String>,
175 pub boilerplate_name: String,
177}
178
179impl Serialize for Name {
180 fn serialize<S>(&self, serializer: S) -> core::result::Result<S::Ok, S::Error>
181 where
182 S: serde::Serializer,
183 {
184 self.to_string().serialize(serializer)
185 }
186}
187
188impl<'de> Deserialize<'de> for Name {
189 fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
190 where
191 D: Deserializer<'de>,
192 {
193 let s = String::deserialize(deserializer)?;
194 Ok(Name::parse(s))
195 }
196}
197
198impl Display for Name {
199 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200 match &self.repository_name {
201 Some(repo) => write!(f, "{}/{}", repo, self.boilerplate_name),
202 None => write!(f, "{}", self.boilerplate_name),
203 }
204 }
205}
206
207impl From<&str> for Name {
208 fn from(s: &str) -> Self {
209 Name::parse(s)
210 }
211}
212
213impl Name {
215 fn new_of<S: AsRef<str>>(boilerplate_name: S) -> Self {
218 Self {
219 repository_name: None,
220 boilerplate_name: boilerplate_name.as_ref().to_string(),
221 }
222 }
223
224 pub fn new<S: AsRef<str>>(repository_name: S, boilerplate_name: S) -> Self {
226 let boilerplate_name = boilerplate_name.as_ref().to_string();
227 Self {
228 repository_name: Some(repository_name.as_ref().to_string()),
229 boilerplate_name,
230 }
231 }
232
233 pub fn parse<S: AsRef<str>>(name: S) -> Self {
237 let name = name.as_ref();
238 let items = name.split('/').collect::<Vec<_>>();
239 if items.len() >= 2 {
240 Self::new(items[0], items[1])
241 } else {
242 Self::new_of(name)
243 }
244 }
245
246 pub fn parse_all<S: AsRef<str>>(names: Vec<S>) -> Vec<Self> {
249 names.iter().map(Name::parse).collect()
250 }
251
252 pub fn matches(&self, boilerplate: &repos::Boilerplate) -> bool {
254 boilerplate.matches(self)
255 }
256}
257
258pub struct Gixor {
263 config: Config,
264 load_from: PathBuf,
265}
266
267pub trait RepositoryManager {
269 fn len(&self) -> usize;
271 fn is_empty(&self) -> bool;
273 fn repositories(&self) -> impl Iterator<Item = &repos::Repository>;
275 fn repository<N: AsRef<str>>(&self, name: N) -> Option<&repos::Repository>;
277 fn add_repository(&mut self, repo: repos::Repository) -> Result<()>;
279 fn add_repository_of<S: AsRef<str>>(&mut self, url: S) -> Result<()>;
281 fn remove_repository_with<S: AsRef<str>>(&mut self, name: S, keep_repo_dir: bool)
283 -> Result<()>;
284 fn remove_repository<S: AsRef<str>>(&mut self, name: S) -> Result<()>;
286}
287
288pub trait AliasManager {
290 fn iter_aliases(&self) -> impl Iterator<Item = &aliases::Alias>;
292 fn remove_alias<S: AsRef<str>>(&mut self, name: S) -> Result<()>;
294 fn add_alias(&mut self, alias: aliases::Alias) -> Result<()>;
296}
297
298#[cfg(feature = "local")]
301impl Default for Gixor {
302 fn default() -> Self {
313 match dirs::config_dir() {
314 Some(dir) => {
315 let repositories = vec![repos::Repository::default()];
316 let config = Config {
317 repositories,
318 base_path: dir.join("gixor").join("boilerplates"),
319 aliases: None,
320 };
321 Self {
322 config,
323 load_from: dir.join("gixor").join("config.json"),
324 }
325 }
326 None => panic!("Failed to get the config directory"),
327 }
328 }
329}
330
331pub struct GixorFactory {}
333
334impl GixorFactory {
335 #[cfg(feature = "embedded")]
341 pub fn embedded() -> Gixor {
342 Gixor::new(
343 Config {
344 repositories: source::repositories(),
345 base_path: PathBuf::new(),
346 aliases: None,
347 },
348 PathBuf::new(),
349 )
350 }
351
352 #[cfg(feature = "local")]
355 pub fn load_or_default() -> Gixor {
356 match dirs::config_dir() {
357 Some(dir) => {
358 let path = dir.join("gixor").join("config.json");
359 GixorFactory::load(&path).unwrap_or_else(|_| GixorFactory::new_at(path))
360 }
361 None => panic!("Failed to get the config directory"),
362 }
363 }
364
365 pub fn new_at<P: AsRef<Path>>(path: P) -> Gixor {
372 let path = path.as_ref();
373 Gixor::new(
374 Config {
375 repositories: vec![repos::Repository::default()],
376 base_path: path.parent().unwrap_or(Path::new(".")).join("boilerplates"),
377 aliases: None,
378 },
379 path.to_path_buf(),
380 )
381 }
382
383 pub fn load<P: AsRef<Path>>(path: P) -> Result<Gixor> {
388 let path = path.as_ref();
389 match std::fs::File::open(path) {
390 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
391 Err(Error::FileNotFound(path.to_path_buf()))
392 }
393 Err(e) => Err(Error::IO(e)),
394 Ok(f) => match serde_json::from_reader(f) {
395 Ok(config) => Ok(Gixor::new(
396 update_base_path(config, path),
397 path.to_path_buf(),
398 )),
399 Err(e) => Err(Error::Json(e)),
400 },
401 }
402 }
403}
404
405impl Gixor {
406 fn new(config: Config, load_from: PathBuf) -> Self {
407 log::debug!("config path: {load_from:?}");
408 log::debug!("config: {}", serde_json::to_string_pretty(&config).unwrap());
409 Gixor { config, load_from }
410 }
411 pub fn base_path(&self) -> &Path {
413 &self.config.base_path
414 }
415
416 pub fn prepare(&self, no_network: bool) -> Result<()> {
424 self.config.prepare(no_network)
425 }
426
427 pub fn dump(
434 &self,
435 names: Vec<Name>,
436 mut dest: impl std::io::Write,
437 clear_flag: bool,
438 ) -> Result<()> {
439 let content = self.build_gitignore(names, ".gitignore", clear_flag)?;
440 dest.write_all(content.as_bytes()).map_err(Error::IO)?;
441 dest.flush().map_err(Error::IO)
442 }
443
444 pub fn build_gitignore<P: AsRef<Path>>(
454 &self,
455 names: Vec<Name>,
456 dest: P,
457 clear_prologue: bool,
458 ) -> Result<String> {
459 let dest = dest.as_ref();
460 let prologue = if clear_prologue {
461 vec![]
462 } else {
463 let from = if dest == Path::new("-") {
464 PathBuf::from(".gitignore")
465 } else {
466 routine::find_gitignore(dest)
467 };
468 routine::load_prologue(&from)
469 };
470 let boilerplates = routine::find_boilerplates(self, names)?;
471 routine::build_content(boilerplates, prologue, self.base_path())
472 }
473
474 pub fn build_gitignore_with(&self, names: Vec<Name>, current: &str) -> Result<String> {
485 let prologue = routine::prologue_of(current);
486 let boilerplates = routine::find_boilerplates(self, names)?;
487 routine::build_content(boilerplates, prologue, self.base_path())
488 }
489
490 pub fn dump_to<P: AsRef<Path>>(
504 &self,
505 names: Vec<Name>,
506 dest: P,
507 clear_flag: bool,
508 ) -> Result<()> {
509 let p = dest.as_ref();
510 log::info!(
511 "dump {} entries into {} with clear_flag: {clear_flag}.",
512 names.len(),
513 p.display()
514 );
515 let content = self.build_gitignore(names, p, clear_flag)?;
518 if p == Path::new("-") {
519 use std::io::Write;
520 let mut out = std::io::stdout();
521 out.write_all(content.as_bytes()).map_err(Error::IO)?;
522 return out.flush().map_err(Error::IO);
523 }
524 routine::write_atomically(&routine::find_gitignore(p), &content)
525 }
526
527 pub fn store(&self) -> Result<()> {
529 if let Some(parent) = self.load_from.parent()
530 && !parent.as_os_str().is_empty() {
531 std::fs::create_dir_all(parent).map_err(Error::IO)?;
532 }
533 match std::fs::File::create(&self.load_from) {
534 Err(e) => Err(Error::IO(e)),
535 Ok(f) => match serde_json::to_writer(f, &self.config) {
536 Err(e) => Err(Error::Json(e)),
537 Ok(_) => Ok(()),
538 },
539 }
540 }
541
542 pub fn iter(&self) -> impl Iterator<Item = repos::Boilerplate<'_>> {
544 self.config.iter()
545 }
546
547 pub fn find(&self, name: Name) -> Result<Vec<repos::Boilerplate<'_>>> {
549 self.config.find(name)
550 }
551}
552
553impl AliasManager for Gixor {
554 fn iter_aliases(&self) -> impl Iterator<Item = &aliases::Alias> {
555 self.config.iter_aliases()
556 }
557
558 fn remove_alias<S: AsRef<str>>(&mut self, name: S) -> Result<()> {
559 self.config.remove_alias(name)
560 }
561
562 fn add_alias(&mut self, alias: aliases::Alias) -> Result<()> {
563 self.config.add_alias(alias)
564 }
565}
566
567impl RepositoryManager for Gixor {
568 fn len(&self) -> usize {
571 self.config.repositories.len()
572 }
573
574 fn is_empty(&self) -> bool {
576 self.config.repositories.is_empty()
577 }
578
579 fn repository<N: AsRef<str>>(&self, name: N) -> Option<&repos::Repository> {
581 let name = name.as_ref();
582 self.config
583 .repositories
584 .iter()
585 .find(|repo| repo.name == name)
586 }
587
588 fn repositories(&self) -> impl Iterator<Item = &repos::Repository> {
590 self.config.repositories.iter()
591 }
592
593 fn add_repository(&mut self, repo: repos::Repository) -> Result<()> {
595 match repo.clone_repo_to(&self.config.base_path) {
596 Err(e) => Err(e),
597 Ok(_) => {
598 self.config.repositories.push(repo);
599 Ok(())
600 }
601 }
602 }
603
604 fn add_repository_of<S: AsRef<str>>(&mut self, url: S) -> Result<()> {
606 let repo = repos::Repository::new(url);
607 self.add_repository(repo)
608 }
609
610 fn remove_repository_with<S: AsRef<str>>(
613 &mut self,
614 name: S,
615 keep_repo_dir: bool,
616 ) -> Result<()> {
617 let name = name.as_ref();
618 let index = self
619 .config
620 .repositories
621 .iter()
622 .position(|repo| repo.name == name);
623 if let Some(index) = index {
624 let repo = self.config.repositories.remove(index);
625 if !keep_repo_dir {
626 remove_repo_dir(&self.config.base_path, repo)?;
627 }
628 Ok(())
629 } else {
630 Err(Error::Fatal(format!("{name}: repository not found")))
631 }
632 }
633
634 fn remove_repository<S: AsRef<str>>(&mut self, name: S) -> Result<()> {
637 self.remove_repository_with(name, false)
638 }
639}
640
641fn update_base_path(config: Config, path: &Path) -> Config {
642 let parent = path.parent().unwrap_or(Path::new("."));
643 let base_path = config.base_path.clone();
644 let new_base_path = if base_path.is_absolute() || base_path.starts_with(".") {
645 base_path
646 } else {
647 parent.join(base_path)
648 };
649 Config {
650 base_path: new_base_path,
651 repositories: config.repositories,
652 aliases: config.aliases,
653 }
654}
655
656#[derive(Serialize, Deserialize, Debug)]
657#[serde(rename_all = "kebab-case")]
658struct Config {
659 pub(crate) repositories: Vec<repos::Repository>,
660 #[serde(flatten)]
661 pub(crate) aliases: Option<aliases::Aliases>,
662 pub(crate) base_path: PathBuf,
663}
664
665impl Config {
666 fn find(&self, name: Name) -> Result<Vec<repos::Boilerplate<'_>>> {
669 if let Some(r) = aliases::extract_alias(self, &name) {
670 Ok(r)
671 } else {
672 for repo in &self.repositories {
673 if let Some(item) = repo.find(&name, &self.base_path) {
674 log::trace!("{}: found from repository {}", name, item.repository_name());
675 return Ok(vec![item]);
676 }
677 }
678 Err(Error::BoilerplateNotFound(name.boilerplate_name))
679 }
680 }
681
682 fn find_all(&self, names: Vec<Name>) -> Result<Vec<repos::Boilerplate<'_>>> {
685 let r = names
686 .into_iter()
687 .map(|name| self.find(name))
688 .collect::<Result<Vec<_>>>();
689 match r {
690 Ok(v) => Ok(v.into_iter().flatten().collect::<Vec<_>>()),
691 Err(e) => Err(e),
692 }
693 }
694
695 fn iter(&self) -> impl Iterator<Item = repos::Boilerplate<'_>> {
697 self.repositories
698 .iter()
699 .flat_map(move |repo| repo.iter(&self.base_path))
700 }
701
702 fn prepare(&self, no_network: bool) -> Result<()> {
704 let mut errs = vec![];
705 if no_network {
706 log::info!("Network access is disabled.");
707 Ok(())
708 } else {
709 self.repositories.iter().for_each(|repo| {
710 if let Err(e) = repo.prepare(&self.base_path) {
711 errs.push(e);
712 }
713 });
714 Error::to_err((), errs)
715 }
716 }
717}
718
719impl AliasManager for Config {
720 fn iter_aliases(&self) -> impl Iterator<Item = &aliases::Alias> {
721 self.aliases.iter().flat_map(|a| a.iter_aliases())
722 }
723
724 fn remove_alias<S: AsRef<str>>(&mut self, name: S) -> Result<()> {
725 self.aliases.as_mut().map_or(
726 Err(Error::AliasNotFound(name.as_ref().to_string())),
727 |aliases| aliases.remove_alias(name),
728 )
729 }
730
731 fn add_alias(&mut self, alias: aliases::Alias) -> Result<()> {
732 let aliases = self.aliases.get_or_insert_with(aliases::Aliases::default);
733 aliases.add_alias(alias)
734 }
735}
736
737fn remove_repo_dir<P: AsRef<Path>>(base_path: P, repo: repos::Repository) -> Result<()> {
738 let path = base_path.as_ref().join(repo.name);
739 match std::fs::remove_dir_all(&path) {
740 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
743 log::debug!("{}: no directory to remove", path.display());
744 Ok(())
745 }
746 Err(e) => Err(Error::IO(e)),
747 Ok(_) => Ok(()),
748 }
749}
750
751#[cfg(test)]
752mod tests {
753 use super::*;
754
755 fn testdata_dir() -> PathBuf {
762 PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/testdata"))
763 }
764
765 pub(crate) fn config_path() -> PathBuf {
770 let path = testdata_dir().join("config.json");
771 assert!(
772 path.exists(),
773 "{}: the test configuration is missing",
774 path.display()
775 );
776 path
777 }
778
779 pub(crate) fn boilerplates_path() -> PathBuf {
781 testdata_dir().join("boilerplates")
782 }
783
784 pub(crate) fn prepare_once() {
791 static PREPARED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
792 PREPARED.get_or_init(|| {
793 GixorFactory::load(config_path())
794 .unwrap()
795 .prepare(false)
796 .unwrap()
797 });
798 }
799
800 #[test]
801 fn test_vec_result_to_result_vec() {
802 let value = vec![Ok(1), Ok(2), Ok(3)];
803 let result = Error::vec_result_to_result_vec(value).unwrap();
804 assert_eq!(result, vec![1, 2, 3]);
805 }
806
807 #[test]
810 fn load_reports_a_missing_configuration() {
811 let dir = tempfile::tempdir().unwrap();
812 let path = dir.path().join("config.json");
813
814 assert!(matches!(
815 GixorFactory::load(&path),
816 Err(Error::FileNotFound(_))
817 ));
818
819 let gixor = GixorFactory::new_at(&path);
820 assert_eq!(gixor.config.repositories.len(), 1);
821 assert_eq!(gixor.config.base_path, dir.path().join("boilerplates"));
822 assert_eq!(gixor.load_from, path);
823 }
824
825 #[test]
826 fn parse_gixor() {
827 match GixorFactory::load(config_path()) {
828 Err(e) => panic!("Failed to parse the config file: {e}"),
829 Ok(gixor) => {
830 assert_eq!(gixor.config.base_path, boilerplates_path());
831 assert_eq!(gixor.config.repositories.len(), 3);
832 }
833 }
834 }
835
836 #[test]
837 fn test_error_display() {
838 assert_eq!(
839 Error::Json(serde::de::Error::custom("hoge")).to_string(),
840 "JSON error: hoge"
841 );
842 assert_eq!(
843 Error::IO(std::io::Error::new(std::io::ErrorKind::NotFound, "hoge")).to_string(),
844 "IO error: hoge"
845 );
846 assert_eq!(
847 Error::BoilerplateNotFound("name".to_string()).to_string(),
848 "name: boilerplate not found"
849 );
850 assert_eq!(Error::Git("hoge".into()).to_string(), "Git error: hoge");
851 assert_eq!(
852 Error::AliasNotFound("hoge".into()).to_string(),
853 "hoge: alias not found"
854 );
855 assert_eq!(
856 Error::FileNotFound("hoge".into()).to_string(),
857 "hoge: file not found"
858 );
859 assert_eq!(
860 Error::RepositoryNotFound("hoge".into()).to_string(),
861 "hoge: repository not found"
862 );
863 assert_eq!(
864 Error::Fatal("message".to_string()).to_string(),
865 "Fatal error: message"
866 );
867 assert_eq!(
868 Error::Array(vec![
869 Error::Fatal("hoge1".to_string()),
870 Error::Fatal("hoge2".to_string())
871 ])
872 .to_string(),
873 "Fatal error: hoge1\nFatal error: hoge2"
874 );
875 assert_eq!(
876 Error::Alias("hoge: alias not found".to_string()).to_string(),
877 "hoge: alias not found"
878 )
879 }
880
881 #[test]
882 fn test_target_name() {
883 let target = Name::new("tamada", "devcontainer");
884 assert_eq!(target.repository_name, Some("tamada".to_string()));
885 assert_eq!(target.boilerplate_name, "devcontainer");
886
887 let target = Name::parse("tamada/devcontainer");
888 assert_eq!(target.repository_name, Some("tamada".to_string()));
889 assert_eq!(target.boilerplate_name, "devcontainer");
890
891 let target = Name::parse("devcontainer");
892 assert_eq!(target.repository_name, None);
893 assert_eq!(target.boilerplate_name, "devcontainer");
894 }
895
896 #[test]
897 fn test_name_serialize_deserialize() {
898 let name: Name = serde_json::from_str("\"os-list\"").unwrap();
899 assert_eq!(name.repository_name, None);
900 assert_eq!(name.boilerplate_name, "os-list");
901
902 let str = serde_json::to_string(&name).unwrap();
903 assert_eq!(str, "\"os-list\"");
904
905 let name: Name = serde_json::from_str("\"alias/os-list\"").unwrap();
906 assert_eq!(name.repository_name, Some("alias".to_string()));
907 assert_eq!(name.boilerplate_name, "os-list");
908
909 let str = serde_json::to_string(&name).unwrap();
910 assert_eq!(str, "\"alias/os-list\"");
911 }
912
913 #[test]
914 fn test_repository_manager() {
915 let temp_dir = tempfile::tempdir().unwrap();
916 let config_path = temp_dir.path().join("config.json");
917 let mut gixor = Gixor::new(
918 Config {
919 repositories: vec![],
920 base_path: temp_dir.path().join("boilerplates"),
921 aliases: None,
922 },
923 config_path,
924 );
925
926 assert!(gixor.is_empty());
927 assert_eq!(gixor.len(), 0);
928
929 let repo = repos::Repository::default();
930 gixor.add_repository(repo).unwrap();
931
932 assert!(!gixor.is_empty());
933 assert_eq!(gixor.len(), 1);
934 assert!(gixor.repository("default").is_some());
935 assert_eq!(gixor.repositories().count(), 1);
936
937 gixor.remove_repository("default").unwrap();
938 assert!(gixor.is_empty());
939 }
940
941 #[test]
942 fn test_alias_manager() {
943 let mut gixor = Gixor::new(
944 Config {
945 repositories: vec![],
946 base_path: PathBuf::from("."),
947 aliases: None,
948 },
949 PathBuf::from("config.json"),
950 );
951
952 let alias = aliases::Alias::new("web".into(), "web stuff".into(), vec![]);
953 gixor.add_alias(alias).unwrap();
954 assert_eq!(gixor.iter_aliases().count(), 1);
955
956 gixor.remove_alias("web").unwrap();
957 assert_eq!(gixor.iter_aliases().count(), 0);
958 }
959
960 #[test]
961 fn test_gixor_store() {
962 let temp_dir = tempfile::tempdir().unwrap();
963 let config_path = temp_dir.path().join("sub").join("config.json");
964 let gixor = Gixor::new(
965 Config {
966 repositories: vec![],
967 base_path: PathBuf::from("."),
968 aliases: None,
969 },
970 config_path.clone(),
971 );
972
973 gixor.store().unwrap();
974 assert!(config_path.exists());
975 }
976
977 #[test]
978 fn test_update_base_path() {
979 let config = Config {
980 repositories: vec![],
981 base_path: PathBuf::from("boilerplates"),
982 aliases: None,
983 };
984 let path = PathBuf::from("/etc/gixor/config.json");
985 let updated = update_base_path(config, &path);
986 assert_eq!(updated.base_path, PathBuf::from("/etc/gixor/boilerplates"));
987
988 let config2 = Config {
989 repositories: vec![],
990 base_path: PathBuf::from("/absolute/path"),
991 aliases: None,
992 };
993 let updated2 = update_base_path(config2, &path);
994 assert_eq!(updated2.base_path, PathBuf::from("/absolute/path"));
995 }
996}