Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

186 linesCodeBlameRaw
1//! What every command starts from: the settings, the host, the repository,
2//! and how to print.
3
4use serde_json::Value;
5
6use crate::auth::{self, Host};
7use crate::cli::Global;
8use crate::client::Client;
9use crate::config::Config;
10use crate::error::{Error, Result};
11use crate::output::Output;
12use crate::repo::{self, Repo};
13
14pub struct Ctx {
15 pub config: Config,
16 pub global: Global,
17 pub out: Output,
18}
19
20impl 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
147pub 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.
160fn 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, &copy.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}