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 return Err(());
96 }
97 handle = Some(entries[0][1..].to_string());
98 repo_name = Some(entries[1].to_string());
99 } else {
100 handle = Some(val[1..].to_string());
101 }
102 } else {
103 repo_name = Some(val)
104 };
105 }
106 shell = Some(
107 args.next()
108 .expect("Invalid state: 3 `Some` std::env::args() but only found 2"),
109 );
110 }
111 4 => {
112 // load args and consume first
113 let mut args = std::env::args();
114 args.next();
115
116 handle = Some(
117 args.next()
118 .expect("Invalid state: 4 `Some` std::env::args() but only found 1"),
119 );
120 repo_name = Some(
121 args.next()
122 .expect("Invalid state: 4 `Some` std::env::args() but only found 2"),
123 );
124 shell = Some(
125 args.next()
126 .expect("Invalid state: 4 `Some` std::env::args() but only found 3"),
127 );
128 }
129 _ => {
130 // err
131 }
132 }
133
134 if let Some(ref handle) = handle
135 && let Some(ref repo_name) = repo_name
136 && let Some(ref shell) = shell
137 {
138 return Ok(Config {
139 handle: handle.to_string(),
140 repo_name: repo_name.to_string(),
141 shell: shell.to_string(),
142 });
143 }
144
145 // now load config
146 if let Ok(file) = std::fs::read_to_string("./tangled-on-commit.json") {
147 if let Ok(parsed) = json::parse(&file) {
148 if handle.is_none()
149 && let Some(json_handle) = parsed["handle"].as_str()
150 {
151 handle = Some(String::from(json_handle))
152 }
153 if repo_name.is_none()
154 && let Some(json_repo_name) = parsed["repo_name"].as_str()
155 {
156 repo_name = Some(String::from(json_repo_name))
157 }
158 if shell.is_none()
159 && let Some(json_shell) = parsed["shell"].as_str()
160 {
161 shell = Some(String::from(json_shell))
162 }
163 }
164 }
165
166 if let Some(ref handle) = handle
167 && let Some(ref repo_name) = repo_name
168 && let Some(ref shell) = shell
169 {
170 return Ok(Config {
171 handle: handle.to_string(),
172 repo_name: repo_name.to_string(),
173 shell: shell.to_string(),
174 });
175 }
176
177 // now load from env
178 if handle.is_none()
179 && let Ok(env_handle) = std::env::var("TANGLED_ON_COMMIT_HANDLE")
180 {
181 handle = Some(String::from(env_handle))
182 }
183 if repo_name.is_none()
184 && let Ok(env_repo_name) = std::env::var("TANGLED_ON_COMMIT_REPO_NAME")
185 {
186 repo_name = Some(String::from(env_repo_name))
187 }
188
189 if let Some(ref handle) = handle
190 && let Some(ref repo_name) = repo_name
191 && let Some(ref shell) = shell
192 {
193 return Ok(Config {
194 handle: handle.to_string(),
195 repo_name: repo_name.to_string(),
196 shell: shell.to_string(),
197 });
198 }
199
200 // couldnt resolve every value
201 // print an error and quit
202 println!("Unable to find a value for every setting. Missing values for:");
203 if handle.is_none() {
204 println!("- handle");
205 }
206 if repo_name.is_none() {
207 println!("- repo_name");
208 }
209 if shell.is_none() {
210 println!("- shell");
211 }
212 println!("\nRun `tangled-on-commit --help` to see the help message");
213 return Err(());
214}