| 1 | //! What every command starts from: the settings, the host, the repository, |
| 2 | //! and how to print. |
| 3 | |
| 4 | use serde_json::Value; |
| 5 | |
| 6 | use crate::auth::{self, Host}; |
| 7 | use crate::cli::Global; |
| 8 | use crate::client::Client; |
| 9 | use crate::config::Config; |
| 10 | use crate::error::{Error, Result}; |
| 11 | use crate::output::Output; |
| 12 | use crate::repo::{self, Repo}; |
| 13 | |
| 14 | pub struct Ctx { |
| 15 | pub config: Config, |
| 16 | pub global: Global, |
| 17 | pub out: Output, |
| 18 | } |
| 19 | |
| 20 | impl Ctx { |
| 21 | pub fn new(global: Global) -> Result<Ctx> { |
| 22 | let config = Config::load()?; |
| 23 | let out = Output::detect(global.json.clone(), global.jq.clone()); |
| 24 | Ok(Ctx { |
| 25 | config, |
| 26 | global, |
| 27 | out, |
| 28 | }) |
| 29 | } |
| 30 | |
| 31 | /// The host named by --hostname or G1T_HOST, or the default. |
| 32 | pub fn host(&self) -> Result<Host> { |
| 33 | auth::resolve(&self.config, self.global.hostname.as_deref()) |
| 34 | } |
| 35 | |
| 36 | pub fn client_for(&self, host: Host) -> Client { |
| 37 | let token = auth::token(&self.config, &host.name, None).map(|t| t.value); |
| 38 | Client::new(host, token) |
| 39 | } |
| 40 | |
| 41 | /// A client for the host commands talk to when no repository is involved. |
| 42 | pub fn client(&self) -> Result<Client> { |
| 43 | Ok(self.client_for(self.host()?)) |
| 44 | } |
| 45 | |
| 46 | /// A client that must have a token. |
| 47 | pub fn signed_in_client(&self) -> Result<Client> { |
| 48 | let client = self.client()?; |
| 49 | if !client.has_token() { |
| 50 | return Err(not_signed_in(&client.host)); |
| 51 | } |
| 52 | Ok(client) |
| 53 | } |
| 54 | |
| 55 | /// The hosts a git remote may name: those signed in to, and g1t.sh. |
| 56 | fn known_hosts(&self) -> Vec<String> { |
| 57 | let mut hosts: Vec<String> = self.config.hosts.keys().cloned().collect(); |
| 58 | hosts.push(crate::config::DEFAULT_HOST.to_owned()); |
| 59 | if let Some(h) = self |
| 60 | .global |
| 61 | .hostname |
| 62 | .as_deref() |
| 63 | .or(std::env::var("G1T_HOST").ok().as_deref()) |
| 64 | && let Ok((name, _)) = auth::parse_hostname(h) |
| 65 | { |
| 66 | hosts.push(name); |
| 67 | } |
| 68 | hosts |
| 69 | } |
| 70 | |
| 71 | /// The repository: --repo, G1T_REPO, or the git remote. |
| 72 | pub fn repo(&self) -> Result<Repo> { |
| 73 | if let Some(flag) = self.global.repo.as_deref().filter(|r| !r.trim().is_empty()) { |
| 74 | let host = self.host()?; |
| 75 | return Repo::parse(flag, &host.name); |
| 76 | } |
| 77 | let remotes = crate::git::remotes(); |
| 78 | if remotes.is_empty() { |
| 79 | return Err(Error::usage( |
| 80 | "Not in a git repository with a g1t remote. Say which repository with --repo OWNER/NAME.", |
| 81 | )); |
| 82 | } |
| 83 | match repo::from_remotes(&remotes, &self.known_hosts()) { |
| 84 | Some((_, found)) => Ok(found), |
| 85 | None => Err(Error::usage( |
| 86 | "None of this repository's remotes is on a g1t host you are signed in to. Pass --repo OWNER/NAME, or --hostname for a self-hosted g1t.", |
| 87 | )), |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | /// The repository and a client for its host. A pull request's working |
| 92 | /// copy (`pulls/<id>`) is resolved to the repository it belongs to. |
| 93 | pub fn repo_client(&self) -> Result<(Repo, Client)> { |
| 94 | let repo = self.repo()?; |
| 95 | let mut host = self.host()?; |
| 96 | if repo.host != host.name { |
| 97 | host = auth::resolve(&self.config, Some(&repo.host))?; |
| 98 | } |
| 99 | let client = self.client_for(host); |
| 100 | if let Some(pull_id) = repo.pull_copy() { |
| 101 | let parent = parent_of_copy(&client, &repo, pull_id)?; |
| 102 | return Ok((parent, client)); |
| 103 | } |
| 104 | Ok((repo, client)) |
| 105 | } |
| 106 | |
| 107 | /// The repository this working copy pushes to, and a client for its |
| 108 | /// host: --repo or G1T_REPO, else the remote named origin. In a clone |
| 109 | /// of a fork, origin is the fork and upstream the repository it came |
| 110 | /// from, which `repo` prefers. Falls back to `repo_client`. |
| 111 | pub fn pushed_repo_client(&self) -> Result<(Repo, Client)> { |
| 112 | if self.global.repo.as_deref().is_some_and(|r| !r.trim().is_empty()) { |
| 113 | return self.repo_client(); |
| 114 | } |
| 115 | let hosts = self.known_hosts(); |
| 116 | let origin = crate::git::remotes() |
| 117 | .into_iter() |
| 118 | .filter(|(name, _)| name == "origin") |
| 119 | .find_map(|(_, url)| repo::parse_remote(&url).filter(|r| hosts.contains(&r.host))); |
| 120 | match origin { |
| 121 | Some(found) if found.pull_copy().is_none() => { |
| 122 | let host = auth::resolve(&self.config, Some(&found.host))?; |
| 123 | Ok((found, self.client_for(host))) |
| 124 | } |
| 125 | _ => self.repo_client(), |
| 126 | } |
| 127 | } |
| 128 | |
| 129 | /// The signed-in account's username on the client's host. |
| 130 | pub fn me(&self, client: &Client) -> Result<String> { |
| 131 | if let Some(user) = self |
| 132 | .config |
| 133 | .host(&client.host.name) |
| 134 | .and_then(|h| h.user.clone()) |
| 135 | && std::env::var_os("G1T_TOKEN").is_none_or(|t| t.is_empty()) |
| 136 | { |
| 137 | return Ok(user); |
| 138 | } |
| 139 | let user = client.op("whoami").send()?; |
| 140 | user["username"] |
| 141 | .as_str() |
| 142 | .map(str::to_owned) |
| 143 | .ok_or_else(|| Error::failed("The API did not say who you are.")) |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | pub fn not_signed_in(host: &Host) -> Error { |
| 148 | let flag = if host.name == crate::config::DEFAULT_HOST { |
| 149 | String::new() |
| 150 | } else { |
| 151 | format!(" --hostname {}", host.name) |
| 152 | }; |
| 153 | Error::auth(format!( |
| 154 | "Not signed in to {}. Run `g1t auth login{flag}`, or set G1T_TOKEN.", |
| 155 | host.name |
| 156 | )) |
| 157 | } |
| 158 | |
| 159 | /// The repository a pull request's working copy was made from. |
| 160 | fn parent_of_copy(client: &Client, copy: &Repo, pull_id: &str) -> Result<Repo> { |
| 161 | let found = client.op("get_repo").repo(copy).send().map_err(|e| { |
| 162 | Error::failed(format!( |
| 163 | "This is pull request {pull_id}'s working copy, and its repository could not be found ({e}). Pass --repo OWNER/NAME." |
| 164 | )) |
| 165 | })?; |
| 166 | let fork_of = &found["fork_of"]; |
| 167 | let parsed = match fork_of { |
| 168 | Value::String(s) => Repo::parse(s, ©.host).ok(), |
| 169 | Value::Object(o) => { |
| 170 | let owner = o |
| 171 | .get("namespace") |
| 172 | .or_else(|| o.get("owner")) |
| 173 | .and_then(Value::as_str); |
| 174 | let name = o.get("name").and_then(Value::as_str); |
| 175 | owner.zip(name).map(|(owner, name)| Repo { |
| 176 | host: copy.host.clone(), |
| 177 | owner: owner.into(), |
| 178 | name: name.into(), |
| 179 | }) |
| 180 | } |
| 181 | _ => None, |
| 182 | }; |
| 183 | parsed.ok_or_else(|| { |
| 184 | Error::usage(format!("This is pull request {pull_id}'s working copy; say which repository it is for with --repo OWNER/NAME.")) |
| 185 | }) |
| 186 | } |