Listen to git commits for a specific repo and run a shell command
1fn help() {
2 println!(
3 "Help: tangled-on-commit
4Listen for commits on a specified repository and execute a shell command.
5
6CLI Arguments:
7 `tangled-on-commit (-h | --help)`
8 Displays this message
9
10 `tangled-on-commit`
11 No specified handle, repo, or command. Falls back to config/env
12
13 `tangled-on-commit SHELL`
14 Uses config/env for handle and repo
15
16 `tangled-on-commit @HANDLE SHELL`
17 Uses config/env for repo
18
19 `tangled-on-commit REPO SHELL`
20 Uses config/env for handle
21
22 `tangled-on-commit @HANDLE/REPO SHELL`
23 `tangled-on-commit HANDLE REPO SHELL`
24 No config/env
25
26JSON:
27 Loads the file `tangled-on-commit.json` from cwd if it exists.
28 Reads keys \"handle\", \"repo_name\", and \"shell\".
29 Unknown keys are ignored and any key can be ommitted
30 JSON is used if the arguments aren't passed to the CLI
31
32Env:
33 Loads the environment variables `TANGLED_ON_COMMIT_HANDLE` and `TANGLED_ON_COMMIT_REPO_NAME`
34 Shell cannot be set by environment variables.
35 Env variables are used if relevant keys are ommitted an arguments aren't passed to the CLI.
36"
37 )
38}
39
40#[derive(Debug)]
41pub struct Config {
42 pub handle: String,
43 pub repo_name: String,
44 pub shell: String,
45}
46
47pub fn load_config() -> Result<Config, ()> {
48 // load config from cli args if present
49 // if omitted, fallback to a local `tangled-on-commit.json` file
50 // if key omitted or file not found, fall back to env
51 // if env omitted, error out and quit
52 // note: shell is not loaded from env (to avoid the user unknowingly executing scripts)
53
54 // if any args are `-h` || `--help` display help and quit
55 for arg in std::env::args() {
56 if arg == "-h" || arg == "--help" {
57 help();
58 return Err(());
59 }
60 }
61
62 // if 0 args are passed, skip
63 // if 1 arg is passed, its the shell command
64 // if 2 args are passed:
65 // if arg 1 starts in @ its a handle
66 // if it contains a / the contents after the slash is the reponame
67 // else its the reponame
68 // arg 2 is always the shell command
69 // if 3 args are passed its handle repo shell
70 // if more args are passed its an error
71
72 let mut handle: Option<String> = None;
73 let mut repo_name: Option<String> = None;
74 let mut shell: Option<String> = None;
75 match std::env::args().collect::<Vec<_>>().len() {
76 // 0 args (std env args includes this script)
77 1 => {}
78 2 => {
79 shell = Some(
80 std::env::args()
81 .last()
82 .expect("Invalid state: 2 `Some` std::env::args() but found no Some values"),
83 )
84 }
85 3 => {
86 // load args and consume first
87 let mut args = std::env::args();
88 args.next();
89
90 if let Some(val) = args.next() {
91 if val.starts_with("@") {
92 if val.contains("/") {
93 let entries: Vec<_> = val.split("/").collect();
94 if entries.len() != 2 {
95 println!("Invalid argument: `{}` is malformed", val);
96 return Err(());
97 }
98 handle = Some(entries[0][1..].to_string());
99 repo_name = Some(entries[1].to_string());
100 } else {
101 handle = Some(val[1..].to_string());
102 }
103 } else {
104 repo_name = Some(val)
105 };
106 }
107 shell = Some(
108 args.next()
109 .expect("Invalid state: 3 `Some` std::env::args() but only found 2"),
110 );
111 }
112 4 => {
113 // load args and consume first
114 let mut args = std::env::args();
115 args.next();
116
117 handle = Some(
118 args.next()
119 .expect("Invalid state: 4 `Some` std::env::args() but only found 1"),
120 );
121 repo_name = Some(
122 args.next()
123 .expect("Invalid state: 4 `Some` std::env::args() but only found 2"),
124 );
125 shell = Some(
126 args.next()
127 .expect("Invalid state: 4 `Some` std::env::args() but only found 3"),
128 );
129 }
130 _ => {
131 // err
132 }
133 }
134
135 if let Some(ref handle) = handle
136 && let Some(ref repo_name) = repo_name
137 && let Some(ref shell) = shell
138 {
139 return Ok(Config {
140 handle: handle.to_string(),
141 repo_name: repo_name.to_string(),
142 shell: shell.to_string(),
143 });
144 }
145
146 // now load config
147 if let Ok(file) = std::fs::read_to_string("./tangled-on-commit.json") {
148 if let Ok(parsed) = json::parse(&file) {
149 if handle.is_none()
150 && let Some(json_handle) = parsed["handle"].as_str()
151 {
152 handle = Some(String::from(json_handle))
153 }
154 if repo_name.is_none()
155 && let Some(json_repo_name) = parsed["repo_name"].as_str()
156 {
157 repo_name = Some(String::from(json_repo_name))
158 }
159 if shell.is_none()
160 && let Some(json_shell) = parsed["shell"].as_str()
161 {
162 shell = Some(String::from(json_shell))
163 }
164 }
165 }
166
167 if let Some(ref handle) = handle
168 && let Some(ref repo_name) = repo_name
169 && let Some(ref shell) = shell
170 {
171 return Ok(Config {
172 handle: handle.to_string(),
173 repo_name: repo_name.to_string(),
174 shell: shell.to_string(),
175 });
176 }
177
178 // now load from env
179 if handle.is_none()
180 && let Ok(env_handle) = std::env::var("TANGLED_ON_COMMIT_HANDLE")
181 {
182 handle = Some(String::from(env_handle))
183 }
184 if repo_name.is_none()
185 && let Ok(env_repo_name) = std::env::var("TANGLED_ON_COMMIT_REPO_NAME")
186 {
187 repo_name = Some(String::from(env_repo_name))
188 }
189
190 if let Some(ref handle) = handle
191 && let Some(ref repo_name) = repo_name
192 && let Some(ref shell) = shell
193 {
194 return Ok(Config {
195 handle: handle.to_string(),
196 repo_name: repo_name.to_string(),
197 shell: shell.to_string(),
198 });
199 }
200
201 // couldnt resolve every value
202 // print an error and quit
203 println!("Unable to find a value for every setting. Missing values for:");
204 if handle.is_none() {
205 println!("- handle");
206 }
207 if repo_name.is_none() {
208 println!("- repo_name");
209 }
210 if shell.is_none() {
211 println!("- shell");
212 }
213 println!("\nRun `tangled-on-commit --help` to see the help message");
214 return Err(());
215}