A tool to help managing forked repos with their own history
1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::Path;
5
6const CONFIG_FILE: &str = "forkme.toml";
7const LOCK_FILE: &str = "forkme.lock";
8
9#[derive(Debug, Serialize, Deserialize)]
10pub struct Config {
11 pub upstream: Upstream,
12}
13
14#[derive(Debug, Serialize, Deserialize)]
15pub struct Upstream {
16 pub url: String,
17 pub branch: String,
18}
19
20impl Config {
21 pub fn load() -> Result<Self> {
22 Self::load_from(CONFIG_FILE)
23 }
24
25 pub fn load_from<P: AsRef<Path>>(path: P) -> Result<Self> {
26 let content = fs::read_to_string(path.as_ref())
27 .with_context(|| format!("Failed to read {}", path.as_ref().display()))?;
28 toml::from_str(&content).with_context(|| "Failed to parse forkme.toml")
29 }
30
31 pub fn save(&self) -> Result<()> {
32 self.save_to(CONFIG_FILE)
33 }
34
35 pub fn save_to<P: AsRef<Path>>(&self, path: P) -> Result<()> {
36 let content = toml::to_string_pretty(self)?;
37 fs::write(path.as_ref(), content)
38 .with_context(|| format!("Failed to write {}", path.as_ref().display()))?;
39 Ok(())
40 }
41
42 pub fn exists() -> bool {
43 Path::new(CONFIG_FILE).exists()
44 }
45}
46
47pub fn load_lock() -> Result<Option<String>> {
48 let path = Path::new(LOCK_FILE);
49 if !path.exists() {
50 return Ok(None);
51 }
52 let content =
53 fs::read_to_string(path).with_context(|| format!("Failed to read {}", LOCK_FILE))?;
54 let sha = content.trim().to_string();
55 if sha.is_empty() {
56 return Ok(None);
57 }
58 Ok(Some(sha))
59}
60
61pub fn save_lock(sha: &str) -> Result<()> {
62 fs::write(LOCK_FILE, format!("{}\n", sha))
63 .with_context(|| format!("Failed to write {}", LOCK_FILE))?;
64 Ok(())
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70 use tempfile::NamedTempFile;
71
72 #[test]
73 fn test_config_save_and_load() {
74 let temp_file = NamedTempFile::new().unwrap();
75 let path = temp_file.path();
76
77 let config = Config {
78 upstream: Upstream {
79 url: "https://github.com/test/repo.git".to_string(),
80 branch: "main".to_string(),
81 },
82 };
83
84 config.save_to(path).unwrap();
85 let loaded = Config::load_from(path).unwrap();
86
87 assert_eq!(loaded.upstream.url, "https://github.com/test/repo.git");
88 assert_eq!(loaded.upstream.branch, "main");
89 }
90
91 #[test]
92 fn test_config_toml_format() {
93 let temp_file = NamedTempFile::new().unwrap();
94 let path = temp_file.path();
95
96 let config = Config {
97 upstream: Upstream {
98 url: "https://github.com/test/repo.git".to_string(),
99 branch: "develop".to_string(),
100 },
101 };
102
103 config.save_to(path).unwrap();
104 let content = fs::read_to_string(path).unwrap();
105
106 assert!(content.contains("[upstream]"));
107 assert!(content.contains("url = \"https://github.com/test/repo.git\""));
108 assert!(content.contains("branch = \"develop\""));
109 }
110
111 #[test]
112 fn test_load_invalid_toml() {
113 let temp_file = NamedTempFile::new().unwrap();
114 let path = temp_file.path();
115
116 fs::write(path, "invalid toml content {{{").unwrap();
117 let result = Config::load_from(path);
118
119 assert!(result.is_err());
120 }
121
122 #[test]
123 fn test_load_missing_field() {
124 let temp_file = NamedTempFile::new().unwrap();
125 let path = temp_file.path();
126
127 fs::write(path, "[upstream]\nurl = \"test\"").unwrap();
128 let result = Config::load_from(path);
129
130 assert!(result.is_err());
131 }
132}