Skip to content

Commit aeb744d

Browse files
authored
Merge pull request #159 from Byte-Barn/feat/fetcher-retry-and-repo-update
Feat/fetcher retry and repo update
2 parents 242efd8 + 57fcd34 commit aeb744d

File tree

10 files changed

+105
-62
lines changed

10 files changed

+105
-62
lines changed

.github/workflows/publish-npm.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ jobs:
128128
console.error(`Run: npm install -g ${pkg}`);
129129
process.exit(1);
130130
} else {
131-
throw e;
131+
process.exit(e.status || 1);
132132
}
133133
}
134134
EOF

src/commands/gitignore/preview.rs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::utils::pretty_print;
33
use crate::utils::progress;
44
use crate::utils::remote::Fetcher;
55

6-
use super::{GITHUB_RAW_BASE, ensure_gitignore_cache, find_template_in_cache};
6+
use super::{ensure_gitignore_cache, find_template_in_cache, GITHUB_RAW_BASE};
77

88
#[derive(clap::Args, Debug, Clone)]
99
pub struct PreviewArgs {
@@ -14,6 +14,10 @@ pub struct PreviewArgs {
1414
/// Update the gitignore cache
1515
#[arg(long = "update-cache")]
1616
pub update_cache: bool,
17+
18+
/// Disable colored output
19+
#[arg(long = "no-color")]
20+
pub no_color: bool,
1721
}
1822

1923
impl super::Runnable for PreviewArgs {
@@ -28,17 +32,21 @@ impl super::Runnable for PreviewArgs {
2832
let cache = ensure_gitignore_cache(&mut cache_manager, self.update_cache)?;
2933

3034
for template_name in &self.args {
31-
preview_single_template(template_name, &cache)?;
35+
preview_single_template(template_name, &cache, self.no_color)?;
3236
}
3337

3438
Ok(())
3539
}
3640
}
3741

38-
fn preview_single_template(template: &str, cache: &super::Cache<String>) -> anyhow::Result<()> {
42+
fn preview_single_template(
43+
template: &str,
44+
cache: &super::Cache<String>,
45+
no_color: bool,
46+
) -> anyhow::Result<()> {
3947
// normalize template if it has the .gitignore ext
4048
let template = template.strip_suffix(".gitignore").unwrap_or(template);
41-
49+
4250
// Find the template path in cache
4351
let template_path = find_template_in_cache(template, cache)?;
4452

@@ -51,7 +59,10 @@ fn preview_single_template(template: &str, cache: &super::Cache<String>) -> anyh
5159
pb.set_message(msg);
5260
pb.finish_and_clear();
5361

54-
println!("\n === Preview: {} === \n", template);
55-
pretty_print::print_highlighted("gitignore", &content);
62+
if no_color {
63+
println!("{}", content);
64+
} else {
65+
pretty_print::print_highlighted("gitignore", &content);
66+
}
5667
Ok(())
5768
}

src/commands/issue/mod.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ pub mod list;
77
pub mod preview;
88

99
// Global constants - these can stay in the main module file
10-
const GITHUB_RAW_BASE: &str =
11-
"https://raw.githubusercontent.com/rafaeljohn9/gitcraft/main/templates";
10+
const GITHUB_RAW_BASE: &str = "https://raw.githubusercontent.com/Byte-Barn/gitcraft/main/templates";
1211

1312
#[derive(Subcommand)]
1413
pub enum Command {

src/commands/issue/preview.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ use super::GITHUB_RAW_BASE;
88
pub struct PreviewArgs {
99
#[arg(allow_hyphen_values = true)]
1010
pub templates: Vec<String>,
11+
12+
/// Disable colored output
13+
#[arg(long = "no-color")]
14+
pub no_color: bool,
1115
}
1216

1317
impl super::Runnable for PreviewArgs {
@@ -19,14 +23,14 @@ impl super::Runnable for PreviewArgs {
1923
}
2024

2125
for template_name in &self.templates {
22-
preview_single_template(template_name)?;
26+
preview_single_template(template_name, self.no_color)?;
2327
}
2428

2529
Ok(())
2630
}
2731
}
2832

29-
fn preview_single_template(template: &str) -> anyhow::Result<()> {
33+
fn preview_single_template(template: &str, no_color: bool) -> anyhow::Result<()> {
3034
let fetcher = Fetcher::new();
3135
let url = format!("{}/issue-templates/{}.yml", GITHUB_RAW_BASE, template);
3236

@@ -36,6 +40,10 @@ fn preview_single_template(template: &str) -> anyhow::Result<()> {
3640
pb.set_message(msg);
3741
pb.finish_and_clear();
3842

39-
pretty_print::print_highlighted("yml", &content);
43+
if no_color {
44+
println!("{}", content);
45+
} else {
46+
pretty_print::print_highlighted("yml", &content);
47+
}
4048
Ok(())
4149
}

src/commands/license/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ fn ensure_spdx_license_cache(
5151
let should_update = cache_manager
5252
.should_update_cache::<serde_json::Value>(SPDX_CACHE_NAME, CACHE_MAX_AGE_SECONDS)?;
5353

54-
if !should_update || !update_cache {
54+
if !should_update && !update_cache {
5555
let cache = cache_manager.load_cache(SPDX_CACHE_NAME)?;
5656
// Only print if running in verbose/debug mode (not implemented here)
5757
// e.g., println!("Loaded license template cache ({} templates)", cache.entries.len());
@@ -94,7 +94,7 @@ fn ensure_github_api_license_cache(
9494
CACHE_MAX_AGE_SECONDS,
9595
)?;
9696

97-
if !should_update || update_cache {
97+
if !should_update && !update_cache {
9898
let cache = cache_manager.load_cache(GITHUB_LICENSES_CACHE_NAME)?;
9999
// Only print if running in verbose/debug mode (not implemented here)
100100
// e.g., println!("Loaded GitHub licenses cache ({} licenses)", cache.entries.len());

src/commands/license/preview.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use colored::*;
22

33
use super::{
4-
CHOOSEALICENSE_RAW_BASE_URL, SPDX_LICENSE_DETAILS_BASE_URL, SPDX_LICENSE_LIST_URL,
5-
ensure_spdx_license_cache,
4+
ensure_spdx_license_cache, CHOOSEALICENSE_RAW_BASE_URL, SPDX_LICENSE_DETAILS_BASE_URL,
5+
SPDX_LICENSE_LIST_URL,
66
};
77

88
use crate::utils::cache::{Cache, CacheManager};

src/commands/pr/mod.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ pub mod list;
77
pub mod preview;
88

99
// Global constants - these can stay in the main module file
10-
const GITHUB_RAW_BASE: &str =
11-
"https://raw.githubusercontent.com/rafaeljohn9/gitcraft/main/templates";
10+
const GITHUB_RAW_BASE: &str = "https://raw.githubusercontent.com/Byte-Barn/gitcraft/main/templates";
1211

1312
#[derive(Subcommand)]
1413
pub enum Command {

src/commands/pr/preview.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ use super::GITHUB_RAW_BASE;
88
pub struct PreviewArgs {
99
#[arg(help = "PR template names to preview")]
1010
pub args: Vec<String>,
11+
12+
/// Disable colored output
13+
#[arg(long = "no-color")]
14+
pub no_color: bool,
1115
}
1216

1317
impl super::Runnable for PreviewArgs {
@@ -19,14 +23,14 @@ impl super::Runnable for PreviewArgs {
1923
}
2024

2125
for template_name in &self.args {
22-
preview_single_template(template_name)?;
26+
preview_single_template(template_name, self.no_color)?;
2327
}
2428

2529
Ok(())
2630
}
2731
}
2832

29-
fn preview_single_template(template: &str) -> anyhow::Result<()> {
33+
fn preview_single_template(template: &str, no_color: bool) -> anyhow::Result<()> {
3034
let fetcher = Fetcher::new();
3135
let url = format!("{}/pr-templates/{}.md", GITHUB_RAW_BASE, template);
3236

@@ -36,6 +40,11 @@ fn preview_single_template(template: &str) -> anyhow::Result<()> {
3640
pb.set_message(msg);
3741
pb.finish_and_clear();
3842

39-
pretty_print::print_highlighted("md", &content);
43+
println!("\n === Preview: {} === \n", template);
44+
if no_color {
45+
println!("{}", content);
46+
} else {
47+
pretty_print::print_highlighted("md", &content);
48+
}
4049
Ok(())
4150
}

src/utils/remote.rs

Lines changed: 53 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,45 +18,64 @@ impl Fetcher {
1818
}
1919
}
2020

21-
/// Fetch raw content from a URL
21+
/// Fetch raw content from a URL with retry logic
2222
pub fn fetch_content(&self, url: &str) -> anyhow::Result<String> {
23-
let response = self
24-
.client
25-
.get(url)
26-
.send()
27-
.map_err(|e| anyhow!("Failed to fetch from {}: {}", url, e))?;
28-
29-
if !response.status().is_success() {
30-
return Err(anyhow!(
31-
"Request failed with status {}: {}",
32-
response.status(),
33-
url
34-
));
35-
}
36-
37-
response
38-
.text()
39-
.map_err(|e| anyhow!("Failed to read response: {}", e))
23+
self.retry(|| self.client.get(url).send())
24+
.and_then(|response| {
25+
if !response.status().is_success() {
26+
return Err(anyhow!(
27+
"Failed to fetch from {}: HTTP {} ({})",
28+
url,
29+
response.status().as_u16(),
30+
response.status().canonical_reason().unwrap_or("Unknown")
31+
));
32+
}
33+
Ok(response)
34+
})
35+
.and_then(|response| {
36+
response
37+
.text()
38+
.map_err(|e| anyhow!("Failed to read response: {}", e))
39+
})
4040
}
4141

42-
/// Fetch and parse JSON from a URL
42+
/// Fetch and parse JSON from a URL with retry logic
4343
pub fn fetch_json(&self, url: &str) -> anyhow::Result<serde_json::Value> {
44-
let response = self
45-
.client
46-
.get(url)
47-
.send()
48-
.map_err(|e| anyhow!("Failed to fetch JSON from {}: {}", url, e))?;
44+
self.retry(|| self.client.get(url).send())
45+
.and_then(|response| {
46+
if !response.status().is_success() {
47+
return Err(anyhow!(
48+
"JSON request failed with status {}: {}",
49+
response.status(),
50+
url
51+
));
52+
}
53+
Ok(response)
54+
})
55+
.and_then(|response| {
56+
response
57+
.json()
58+
.map_err(|e| anyhow!("Failed to parse JSON: {}", e))
59+
})
60+
}
4961

50-
if !response.status().is_success() {
51-
return Err(anyhow!(
52-
"JSON request failed with status {}: {}",
53-
response.status(),
54-
url
55-
));
62+
/// Retry logic with exponential backoff (max 3 attempts)
63+
fn retry<F>(&self, mut f: F) -> anyhow::Result<reqwest::blocking::Response>
64+
where
65+
F: FnMut() -> Result<reqwest::blocking::Response, reqwest::Error>,
66+
{
67+
let mut attempts = 0;
68+
loop {
69+
match f() {
70+
Ok(response) => return Ok(response),
71+
Err(e) => {
72+
attempts += 1;
73+
if attempts >= 3 {
74+
return Err(anyhow!("Request failed after 3 attempts: {}", e));
75+
}
76+
std::thread::sleep(Duration::from_millis(100 * (2_u64.pow(attempts - 1))));
77+
}
78+
}
5679
}
57-
58-
response
59-
.json()
60-
.map_err(|e| anyhow!("Failed to parse JSON: {}", e))
6180
}
6281
}

tests/integration/issue_tests.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,7 @@ fn test_issue_add_invalid_type() {
130130
cmd.args(&["add", "issue", "invalid-template"])
131131
.assert()
132132
.failure()
133-
.stderr(
134-
predicate::str::contains("Request failed").or(predicate::str::contains("not found")),
135-
);
133+
.stderr(predicate::str::contains("Failed").or(predicate::str::contains("not found")));
136134
}
137135

138136
#[test]
@@ -301,9 +299,7 @@ fn test_issue_preview_invalid_id() {
301299
cmd.args(&["preview", "issue", "not-a-template"])
302300
.assert()
303301
.failure()
304-
.stderr(
305-
predicate::str::contains("Request failed").or(predicate::str::contains("not found")),
306-
);
302+
.stderr(predicate::str::contains("Failed").or(predicate::str::contains("not found")));
307303
}
308304

309305
// -------- HELP COMMAND TEST --------
@@ -318,7 +314,9 @@ fn test_issue_help_command() {
318314
.assert()
319315
.success()
320316
.stdout(predicate::str::contains("Add an issue template"))
321-
.stdout(predicate::str::contains("Usage: gitcraft add issue-template"))
317+
.stdout(predicate::str::contains(
318+
"Usage: gitcraft add issue-template",
319+
))
322320
.stdout(predicate::str::contains("--dir"))
323321
.stdout(predicate::str::contains("--force"))
324322
.stdout(predicate::str::contains("-o, --output"));

0 commit comments

Comments
 (0)