Listen to git commits for a specific repo and run a shell command

Move args and did resolution to seperate files

vielle.dev 652a0992 edfe55f5

verified
Changed files
+274 -255
src
+214
src/args.rs
··· 1 + fn help() { 2 + println!( 3 + "Help: tangled-on-commit 4 + Listen for commits on a specified repository and execute a shell command. 5 + 6 + CLI 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 + 26 + JSON: 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 + 32 + Env: 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)] 41 + pub struct Config { 42 + pub handle: String, 43 + pub repo_name: String, 44 + pub shell: String, 45 + } 46 + 47 + pub 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 + }
+56
src/did.rs
··· 1 + use trust_dns_resolver::Resolver; 2 + use trust_dns_resolver::config::{ResolverConfig, ResolverOpts}; 3 + 4 + #[derive(Debug)] 5 + pub struct DidDoc { 6 + pub did: String, 7 + } 8 + 9 + fn get_txt_did(handle: &String) -> Result<String, ()> { 10 + // create a txt resolver 11 + let resolver = match Resolver::new(ResolverConfig::default(), ResolverOpts::default()) { 12 + Ok(val) => val, 13 + Err(_) => return Err(()), 14 + }; 15 + 16 + // resolve _atproto.handle to a TXT record 17 + let txt_res = match resolver.txt_lookup("_atproto.".to_owned() + &handle) { 18 + Ok(val) => val, 19 + Err(_) => return Err(()), // collect all entries and convert to strings 20 + } 21 + .into_iter() 22 + .map(|x| x.to_string()) 23 + .collect::<Vec<_>>(); 24 + 25 + // filter entries which do not start with `did=` 26 + let did_res = txt_res 27 + .clone() 28 + .extract_if(.., |x| x.starts_with("did=")) 29 + .collect::<Vec<_>>(); 30 + // only 1 did= can exist 31 + // https://atproto.com/specs/handle#:~:text=If%20multiple%20valid%20records%20with%20different%20DIDs%20are%20present,%20resolution%20should%20fail. 32 + if did_res.len() != 1 { 33 + return Err(()); 34 + } 35 + let did = did_res[0].clone(); 36 + 37 + return Ok(did.clone()); 38 + } 39 + 40 + fn get_http_did(handle: &String) -> Result<String, ()> { 41 + return Ok(String::new()); 42 + } 43 + 44 + pub fn get_did(handle: String) -> Result<DidDoc, ()> { 45 + let did = if let Ok(did) = get_txt_did(&handle) { 46 + did 47 + } else { 48 + if let Ok(did) = get_http_did(&handle) { 49 + did 50 + } else { 51 + return Err(()); 52 + } 53 + }; 54 + 55 + return Ok(DidDoc { did }); 56 + }
+4 -255
src/main.rs
··· 1 - use trust_dns_resolver::Resolver; 2 - use trust_dns_resolver::config::{ResolverConfig, ResolverOpts}; 3 - 4 - fn help() { 5 - println!( 6 - "Help: tangled-on-commit 7 - Listen for commits on a specified repository and execute a shell command. 8 - 9 - CLI Arguments: 10 - `tangled-on-commit (-h | --help)` 11 - Displays this message 12 - 13 - `tangled-on-commit` 14 - No specified handle, repo, or command. Falls back to config/env 15 - 16 - `tangled-on-commit SHELL` 17 - Uses config/env for handle and repo 18 - 19 - `tangled-on-commit @HANDLE SHELL` 20 - Uses config/env for repo 21 - 22 - `tangled-on-commit REPO SHELL` 23 - Uses config/env for handle 24 - 25 - `tangled-on-commit @HANDLE/REPO SHELL` 26 - `tangled-on-commit HANDLE REPO SHELL` 27 - No config/env 28 - 29 - JSON: 30 - Loads the file `tangled-on-commit.json` from cwd if it exists. 31 - Reads keys \"handle\", \"repo_name\", and \"shell\". 32 - Unknown keys are ignored and any key can be ommitted 33 - JSON is used if the arguments aren't passed to the CLI 34 - 35 - Env: 36 - Loads the environment variables `TANGLED_ON_COMMIT_HANDLE` and `TANGLED_ON_COMMIT_REPO_NAME` 37 - Shell cannot be set by environment variables. 38 - Env variables are used if relevant keys are ommitted an arguments aren't passed to the CLI. 39 - " 40 - ) 41 - } 42 - 43 - #[derive(Debug)] 44 - struct Config { 45 - handle: String, 46 - repo_name: String, 47 - shell: String, 48 - } 49 - 50 - fn load_config() -> Result<Config, ()> { 51 - // load config from cli args if present 52 - // if omitted, fallback to a local `tangled-on-commit.json` file 53 - // if key omitted or file not found, fall back to env 54 - // if env omitted, error out and quit 55 - // note: shell is not loaded from env (to avoid the user unknowingly executing scripts) 56 - 57 - // if any args are `-h` || `--help` display help and quit 58 - for arg in std::env::args() { 59 - if arg == "-h" || arg == "--help" { 60 - help(); 61 - return Err(()); 62 - } 63 - } 64 - 65 - // if 0 args are passed, skip 66 - // if 1 arg is passed, its the shell command 67 - // if 2 args are passed: 68 - // if arg 1 starts in @ its a handle 69 - // if it contains a / the contents after the slash is the reponame 70 - // else its the reponame 71 - // arg 2 is always the shell command 72 - // if 3 args are passed its handle repo shell 73 - // if more args are passed its an error 74 - 75 - let mut handle: Option<String> = None; 76 - let mut repo_name: Option<String> = None; 77 - let mut shell: Option<String> = None; 78 - match std::env::args().collect::<Vec<_>>().len() { 79 - // 0 args (std env args includes this script) 80 - 1 => {} 81 - 2 => { 82 - shell = Some( 83 - std::env::args() 84 - .last() 85 - .expect("Invalid state: 2 `Some` std::env::args() but found no Some values"), 86 - ) 87 - } 88 - 3 => { 89 - // load args and consume first 90 - let mut args = std::env::args(); 91 - args.next(); 92 - 93 - if let Some(val) = args.next() { 94 - if val.starts_with("@") { 95 - if val.contains("/") { 96 - let entries: Vec<_> = val.split("/").collect(); 97 - if entries.len() != 2 { 98 - return Err(()); 99 - } 100 - handle = Some(entries[0][1..].to_string()); 101 - repo_name = Some(entries[1].to_string()); 102 - } else { 103 - handle = Some(val[1..].to_string()); 104 - } 105 - } else { 106 - repo_name = Some(val) 107 - }; 108 - } 109 - shell = Some( 110 - args.next() 111 - .expect("Invalid state: 3 `Some` std::env::args() but only found 2"), 112 - ); 113 - } 114 - 4 => { 115 - // load args and consume first 116 - let mut args = std::env::args(); 117 - args.next(); 118 - 119 - handle = Some( 120 - args.next() 121 - .expect("Invalid state: 4 `Some` std::env::args() but only found 1"), 122 - ); 123 - repo_name = Some( 124 - args.next() 125 - .expect("Invalid state: 4 `Some` std::env::args() but only found 2"), 126 - ); 127 - shell = Some( 128 - args.next() 129 - .expect("Invalid state: 4 `Some` std::env::args() but only found 3"), 130 - ); 131 - } 132 - _ => { 133 - // err 134 - } 135 - } 136 - 137 - if let Some(ref handle) = handle 138 - && let Some(ref repo_name) = repo_name 139 - && let Some(ref shell) = shell 140 - { 141 - return Ok(Config { 142 - handle: handle.to_string(), 143 - repo_name: repo_name.to_string(), 144 - shell: shell.to_string(), 145 - }); 146 - } 147 - 148 - // now load config 149 - if let Ok(file) = std::fs::read_to_string("./tangled-on-commit.json") { 150 - if let Ok(parsed) = json::parse(&file) { 151 - if handle.is_none() 152 - && let Some(json_handle) = parsed["handle"].as_str() 153 - { 154 - handle = Some(String::from(json_handle)) 155 - } 156 - if repo_name.is_none() 157 - && let Some(json_repo_name) = parsed["repo_name"].as_str() 158 - { 159 - repo_name = Some(String::from(json_repo_name)) 160 - } 161 - if shell.is_none() 162 - && let Some(json_shell) = parsed["shell"].as_str() 163 - { 164 - shell = Some(String::from(json_shell)) 165 - } 166 - } 167 - } 168 - 169 - if let Some(ref handle) = handle 170 - && let Some(ref repo_name) = repo_name 171 - && let Some(ref shell) = shell 172 - { 173 - return Ok(Config { 174 - handle: handle.to_string(), 175 - repo_name: repo_name.to_string(), 176 - shell: shell.to_string(), 177 - }); 178 - } 179 - 180 - // now load from env 181 - if handle.is_none() 182 - && let Ok(env_handle) = std::env::var("TANGLED_ON_COMMIT_HANDLE") 183 - { 184 - handle = Some(String::from(env_handle)) 185 - } 186 - if repo_name.is_none() 187 - && let Ok(env_repo_name) = std::env::var("TANGLED_ON_COMMIT_REPO_NAME") 188 - { 189 - repo_name = Some(String::from(env_repo_name)) 190 - } 191 - 192 - if let Some(ref handle) = handle 193 - && let Some(ref repo_name) = repo_name 194 - && let Some(ref shell) = shell 195 - { 196 - return Ok(Config { 197 - handle: handle.to_string(), 198 - repo_name: repo_name.to_string(), 199 - shell: shell.to_string(), 200 - }); 201 - } 202 - 203 - // couldnt resolve every value 204 - // print an error and quit 205 - println!("Unable to find a value for every setting. Missing values for:"); 206 - if handle.is_none() { 207 - println!("- handle"); 208 - } 209 - if repo_name.is_none() { 210 - println!("- repo_name"); 211 - } 212 - if shell.is_none() { 213 - println!("- shell"); 214 - } 215 - println!("\nRun `tangled-on-commit --help` to see the help message"); 216 - return Err(()); 217 - } 218 - 219 - #[derive(Debug)] 220 - struct DidDoc { 221 - did: String, 222 - } 223 - 224 - fn get_did(handle: String) -> Result<DidDoc, ()> { 225 - // create a txt resolver 226 - let resolver = match Resolver::new(ResolverConfig::default(), ResolverOpts::default()) { 227 - Ok(val) => val, 228 - Err(_) => return Err(()), 229 - }; 230 - 231 - // resolve _atproto.handle to a TXT record 232 - let txt_res = match resolver.txt_lookup("_atproto.".to_owned() + &handle) { 233 - Ok(val) => val, 234 - Err(_) => return Err(()), // collect all entries and convert to strings 235 - } 236 - .into_iter() 237 - .map(|x| x.to_string()) 238 - .collect::<Vec<_>>(); 239 - 240 - // filter entries which do not start with `did=` 241 - let did_res = txt_res 242 - .clone() 243 - .extract_if(.., |x| x.starts_with("did=")) 244 - .collect::<Vec<_>>(); 245 - // only 1 did= can exist 246 - // https://atproto.com/specs/handle#:~:text=If%20multiple%20valid%20records%20with%20different%20DIDs%20are%20present,%20resolution%20should%20fail. 247 - if did_res.len() != 1 { 248 - return Err(()); 249 - } 250 - let did = did_res[0].clone(); 251 - 252 - return Ok(DidDoc { did: did.clone() }); 253 - } 1 + mod args; 2 + mod did; 254 3 255 4 fn main() -> Result<(), ()> { 256 5 // load configuration 257 - let config = match load_config() { 6 + let config = match args::load_config() { 258 7 Ok(res) => res, 259 8 Err(_) => { 260 9 // q ··· 264 13 println!("{:#?}", config); 265 14 266 15 // resolve handle to did 267 - let did_doc = match get_did(config.handle) { 16 + let did_doc = match did::get_did(config.handle) { 268 17 Ok(res) => res, 269 18 Err(_) => { 270 19 // q