Skip to content

Commit 1a13f5c

Browse files
--wip-- [skip ci]
1 parent 74a313b commit 1a13f5c

9 files changed

Lines changed: 298 additions & 93 deletions

File tree

src/api_client.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,29 @@ nest! {
243243
}
244244
}
245245

246+
nest! {
247+
#[derive(Debug, Deserialize, Serialize)]*
248+
#[serde(rename_all = "camelCase")]*
249+
struct CurrentUserData {
250+
user: Option<pub struct CurrentUserPayload {
251+
pub id: String,
252+
}>,
253+
}
254+
}
255+
246256
impl CodSpeedAPIClient {
257+
/// Check if the current token is valid by querying the user resolver.
258+
pub async fn is_token_valid(&self) -> bool {
259+
let response = self
260+
.gql_client
261+
.query_unwrap::<CurrentUserData>(include_str!("queries/CurrentUser.gql"))
262+
.await;
263+
match response {
264+
Ok(data) => data.user.is_some(),
265+
Err(_) => false,
266+
}
267+
}
268+
247269
pub async fn create_login_session(&self) -> Result<CreateLoginSessionPayload> {
248270
let response = self
249271
.unauthenticated_gql_client

src/cli/auth.rs

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
use std::time::Duration;
22

3-
use crate::{api_client::CodSpeedAPIClient, config::CodSpeedConfig, prelude::*};
3+
use crate::api_client::{CodSpeedAPIClient, GetRepositoryVars};
4+
use crate::cli::run::helpers::{find_repository_root, parse_repository_from_remote, ParsedRepository};
5+
use crate::config::CodSpeedConfig;
6+
use crate::prelude::*;
47
use clap::{Args, Subcommand};
58
use console::style;
9+
use git2::Repository;
610
use tokio::time::{Instant, sleep};
711

12+
use super::status::{check_mark, cross_mark};
13+
814
#[derive(Debug, Args)]
915
pub struct AuthArgs {
1016
#[command(subcommand)]
@@ -15,6 +21,8 @@ pub struct AuthArgs {
1521
enum AuthCommands {
1622
/// Login to CodSpeed
1723
Login,
24+
/// Show the authentication status
25+
Status,
1826
}
1927

2028
pub async fn run(
@@ -24,6 +32,7 @@ pub async fn run(
2432
) -> Result<()> {
2533
match args.command {
2634
AuthCommands::Login => login(api_client, config_name).await?,
35+
AuthCommands::Status => status(api_client).await?,
2736
}
2837
Ok(())
2938
}
@@ -80,3 +89,86 @@ async fn login(api_client: &CodSpeedAPIClient, config_name: Option<&str>) -> Res
8089

8190
Ok(())
8291
}
92+
93+
/// Detect the repository from the git remote of the current directory
94+
fn detect_repository() -> Option<ParsedRepository> {
95+
let current_dir = std::env::current_dir().ok()?;
96+
let root_path = find_repository_root(&current_dir)?;
97+
let git_repository = Repository::open(&root_path).ok()?;
98+
let remote = git_repository.find_remote("origin").ok()?;
99+
let url = remote.url()?;
100+
parse_repository_from_remote(url).ok()
101+
}
102+
103+
fn provider_label(provider: &crate::run_environment::RepositoryProvider) -> &'static str {
104+
match provider {
105+
crate::run_environment::RepositoryProvider::GitHub => "GitHub",
106+
crate::run_environment::RepositoryProvider::GitLab => "GitLab",
107+
crate::run_environment::RepositoryProvider::Project => "Project",
108+
}
109+
}
110+
111+
pub async fn status(api_client: &CodSpeedAPIClient) -> Result<()> {
112+
let config = CodSpeedConfig::load_with_override(None, None)?;
113+
let has_token = config.auth.token.is_some();
114+
let detected_repo = detect_repository();
115+
116+
// 1. Check token validity
117+
let token_valid = has_token && api_client.is_token_valid().await;
118+
119+
info!("{}", style("Authentication").bold());
120+
if token_valid {
121+
info!(" {} Logged in", check_mark());
122+
} else if has_token {
123+
info!(
124+
" {} Token expired (run {} to re-authenticate)",
125+
cross_mark(),
126+
style("codspeed auth login").cyan()
127+
);
128+
} else {
129+
info!(
130+
" {} Not logged in (run {} to authenticate)",
131+
cross_mark(),
132+
style("codspeed auth login").cyan()
133+
);
134+
}
135+
info!("");
136+
137+
// 2. If token is valid and we detected a repo, check repository existence
138+
info!("{}", style("Repository").bold());
139+
match detected_repo {
140+
Some(parsed) => {
141+
let label = provider_label(&parsed.provider);
142+
if token_valid {
143+
let repo_exists = api_client
144+
.get_repository(GetRepositoryVars {
145+
owner: parsed.owner.clone(),
146+
name: parsed.name.clone(),
147+
provider: parsed.provider.clone(),
148+
})
149+
.await
150+
.ok()
151+
.flatten()
152+
.is_some();
153+
if repo_exists {
154+
info!(" {} {}/{} ({})", check_mark(), parsed.owner, parsed.name, label);
155+
} else {
156+
info!(
157+
" {} {}/{} ({}, not found on CodSpeed)",
158+
cross_mark(),
159+
parsed.owner,
160+
parsed.name,
161+
label
162+
);
163+
}
164+
} else {
165+
info!(" {}/{} ({})", parsed.owner, parsed.name, label);
166+
}
167+
}
168+
None => {
169+
info!(" Not inside a git repository");
170+
}
171+
}
172+
173+
Ok(())
174+
}

src/cli/mod.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub(crate) mod run;
44
mod setup;
55
mod shared;
66
mod show;
7+
mod status;
78
mod use_mode;
89

910
pub(crate) use shared::*;
@@ -84,7 +85,9 @@ enum Commands {
8485
/// Manage the CLI authentication state
8586
Auth(auth::AuthArgs),
8687
/// Pre-install the codspeed executors
87-
Setup,
88+
Setup(setup::SetupArgs),
89+
/// Show the overall status of CodSpeed (authentication, tools, system)
90+
Status,
8891
/// Set the codspeed mode for the rest of the shell session
8992
Use(use_mode::UseArgs),
9093
/// Show the codspeed mode previously set in this shell session with `codspeed use`
@@ -137,7 +140,8 @@ pub async fn run() -> Result<()> {
137140
.await?
138141
}
139142
Commands::Auth(args) => auth::run(args, &api_client, cli.config_name.as_deref()).await?,
140-
Commands::Setup => setup::setup(setup_cache_dir).await?,
143+
Commands::Setup(args) => setup::run(args, setup_cache_dir).await?,
144+
Commands::Status => status::run(&api_client).await?,
141145
Commands::Use(args) => use_mode::run(args)?,
142146
Commands::Show => show::run()?,
143147
}
@@ -154,7 +158,7 @@ impl Cli {
154158
config_name: None,
155159
config: None,
156160
setup_cache_dir: None,
157-
command: Commands::Setup,
161+
command: Commands::Setup(setup::SetupArgs::default()),
158162
}
159163
}
160164
}

src/cli/run/helpers/find_repository_root.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ fn _find_repository_root(base_dir: &Path) -> Option<PathBuf> {
2525
}
2626
}
2727

28-
log::warn!("Could not find repository root");
29-
3028
None
3129
}
3230

src/cli/run/helpers/parse_git_remote.rs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::sync::LazyLock;
22

3-
use anyhow::{Result, anyhow};
3+
use anyhow::{Result, anyhow, bail};
4+
5+
use crate::run_environment::RepositoryProvider;
46

57
static REMOTE_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
68
regex::Regex::new(
@@ -16,6 +18,34 @@ pub struct GitRemote {
1618
pub repository: String,
1719
}
1820

21+
/// Parsed repository info including the CodSpeed provider
22+
#[derive(Debug)]
23+
pub struct ParsedRepository {
24+
pub provider: RepositoryProvider,
25+
pub owner: String,
26+
pub name: String,
27+
}
28+
29+
/// Parse a git remote URL and extract the provider, owner, and repository name
30+
pub fn parse_repository_from_remote(remote_url: &str) -> Result<ParsedRepository> {
31+
let GitRemote {
32+
domain,
33+
owner,
34+
repository,
35+
} = parse_git_remote(remote_url)?;
36+
let provider = match domain.as_str() {
37+
"github.com" => RepositoryProvider::GitHub,
38+
"gitlab.com" => RepositoryProvider::GitLab,
39+
domain => bail!("Repository provider {domain} is not supported by CodSpeed"),
40+
};
41+
42+
Ok(ParsedRepository {
43+
provider,
44+
owner,
45+
name: repository,
46+
})
47+
}
48+
1949
pub fn parse_git_remote(remote: &str) -> Result<GitRemote> {
2050
let captures = REMOTE_REGEX.captures(remote).ok_or_else(|| {
2151
anyhow!("Could not extract owner and repository from remote url: {remote}")
@@ -98,4 +128,43 @@ mod tests {
98128
}
99129
"###);
100130
}
131+
132+
#[test]
133+
fn test_parse_repository_from_remote() {
134+
use crate::run_environment::RepositoryProvider;
135+
136+
let remote_urls = [
137+
(
138+
"git@github.com:CodSpeedHQ/codspeed.git",
139+
RepositoryProvider::GitHub,
140+
"CodSpeedHQ",
141+
"codspeed",
142+
),
143+
(
144+
"https://github.com/CodSpeedHQ/codspeed.git",
145+
RepositoryProvider::GitHub,
146+
"CodSpeedHQ",
147+
"codspeed",
148+
),
149+
(
150+
"git@gitlab.com:codspeed/runner.git",
151+
RepositoryProvider::GitLab,
152+
"codspeed",
153+
"runner",
154+
),
155+
(
156+
"https://gitlab.com/codspeed/runner.git",
157+
RepositoryProvider::GitLab,
158+
"codspeed",
159+
"runner",
160+
),
161+
];
162+
for (remote_url, expected_provider, expected_owner, expected_name) in remote_urls.into_iter()
163+
{
164+
let parsed = parse_repository_from_remote(remote_url).unwrap();
165+
assert_eq!(parsed.provider, expected_provider);
166+
assert_eq!(parsed.owner, expected_owner);
167+
assert_eq!(parsed.name, expected_name);
168+
}
169+
}
101170
}

src/cli/setup.rs

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,35 @@
1-
use crate::executor::get_all_executors;
1+
use crate::executor::{ToolInstallStatus, get_all_executors};
22
use crate::prelude::*;
33
use crate::system::SystemInfo;
4+
use clap::{Args, Subcommand};
5+
use console::style;
46
use std::path::Path;
57

6-
pub async fn setup(setup_cache_dir: Option<&Path>) -> Result<()> {
8+
use super::status::{check_mark, cross_mark, warn_mark};
9+
10+
#[derive(Debug, Default, Args)]
11+
pub struct SetupArgs {
12+
#[command(subcommand)]
13+
command: Option<SetupCommands>,
14+
}
15+
16+
#[derive(Debug, Subcommand)]
17+
enum SetupCommands {
18+
/// Show the installation status of CodSpeed tools
19+
Status,
20+
}
21+
22+
pub async fn run(args: SetupArgs, setup_cache_dir: Option<&Path>) -> Result<()> {
23+
match args.command {
24+
None => setup(setup_cache_dir).await,
25+
Some(SetupCommands::Status) => {
26+
status();
27+
Ok(())
28+
}
29+
}
30+
}
31+
32+
async fn setup(setup_cache_dir: Option<&Path>) -> Result<()> {
733
let system_info = SystemInfo::new()?;
834
let executors = get_all_executors();
935
start_group!("Setting up the environment for all executors");
@@ -18,3 +44,31 @@ pub async fn setup(setup_cache_dir: Option<&Path>) -> Result<()> {
1844
end_group!();
1945
Ok(())
2046
}
47+
48+
pub fn status() {
49+
info!("{}", style("Tools").bold());
50+
for executor in get_all_executors() {
51+
let tool_status = executor.tool_status();
52+
match &tool_status.status {
53+
ToolInstallStatus::Installed { version } => {
54+
info!(" {} {} ({})", check_mark(), tool_status.tool_name, version);
55+
}
56+
ToolInstallStatus::IncorrectVersion { version, message } => {
57+
info!(
58+
" {} {} ({}, {})",
59+
warn_mark(),
60+
tool_status.tool_name,
61+
version,
62+
message
63+
);
64+
}
65+
ToolInstallStatus::NotInstalled => {
66+
info!(
67+
" {} {} (not installed)",
68+
cross_mark(),
69+
tool_status.tool_name
70+
);
71+
}
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)