Skip to content

Commit dd32e8a

Browse files
committed
feat: ssh support
1 parent 51b26ed commit dd32e8a

File tree

6 files changed

+94
-21
lines changed

6 files changed

+94
-21
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ toml = "0.8.19"
1313
tracing = "0.1.41"
1414
tracing-indicatif = "0.3.9"
1515
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
16+
url = "2.5.4"

rust/patchable/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ toml.workspace = true
1414
tracing.workspace = true
1515
tracing-indicatif.workspace = true
1616
tracing-subscriber.workspace = true
17+
url.workspace = true

rust/patchable/src/main.rs

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ use snafu::{OptionExt, ResultExt as _, Snafu};
1313
use tracing_indicatif::IndicatifLayer;
1414
use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _};
1515

16+
use crate::utils::setup_git_credentials;
17+
1618
#[derive(clap::Parser)]
1719
struct ProductVersion {
1820
/// The product name slug (such as druid)
@@ -164,6 +166,10 @@ enum Cmd {
164166
/// Check out the base commit, without applying patches
165167
#[clap(long)]
166168
base_only: bool,
169+
170+
/// Use SSH for git operations
171+
#[clap(long)]
172+
ssh: bool,
167173
},
168174

169175
/// Export the patches from the source tree at docker-images/<PRODUCT>/patchable-work/worktree/<VERSION>
@@ -185,8 +191,13 @@ enum Cmd {
185191
#[clap(long)]
186192
base: String,
187193

194+
/// Mirror the product version to the default mirror repository
188195
#[clap(long)]
189196
mirror: bool,
197+
198+
/// Use SSH for git operations
199+
#[clap(long)]
200+
ssh: bool,
190201
},
191202

192203
/// Shows the patch directory for a given product version
@@ -235,6 +246,9 @@ pub enum Error {
235246
path: PathBuf,
236247
},
237248

249+
#[snafu(display("failed to rewrite URL for SSH: {source}"))]
250+
UrlRewrite { source: utils::UrlRewriteError },
251+
238252
#[snafu(display(
239253
"mirroring requested, but default-mirror is not configured in product configuration"
240254
))]
@@ -326,7 +340,7 @@ fn main() -> Result<()> {
326340
}
327341
};
328342
match opts.cmd {
329-
Cmd::Checkout { pv, base_only } => {
343+
Cmd::Checkout { pv, base_only, ssh } => {
330344
let ctx = ProductVersionContext {
331345
pv,
332346
images_repo_root,
@@ -337,13 +351,20 @@ fn main() -> Result<()> {
337351
let product_repo = repo::ensure_bare_repo(&product_repo_root)
338352
.context(OpenProductRepoForCheckoutSnafu)?;
339353

354+
let mut upstream = version_config.mirror.unwrap_or_else(|| {
355+
tracing::warn!("this product version is not mirrored, re-init it with --mirror before merging it");
356+
product_config.upstream
357+
});
358+
359+
if ssh {
360+
upstream =
361+
utils::rewrite_git_https_url_to_ssh(&upstream).context(UrlRewriteSnafu)?;
362+
}
363+
340364
let base_commit = repo::resolve_and_fetch_commitish(
341365
&product_repo,
342366
&version_config.base.to_string(),
343-
version_config.mirror.as_deref().unwrap_or_else(|| {
344-
tracing::warn!("this product version is not mirrored, re-init it with --mirror before merging it");
345-
&product_config.upstream
346-
}),
367+
&upstream,
347368
)
348369
.context(FetchBaseCommitSnafu)?;
349370
let base_branch = ctx.base_branch();
@@ -453,7 +474,12 @@ fn main() -> Result<()> {
453474
);
454475
}
455476

456-
Cmd::Init { pv, base, mirror } => {
477+
Cmd::Init {
478+
pv,
479+
base,
480+
mirror,
481+
ssh,
482+
} => {
457483
let ctx = ProductVersionContext {
458484
pv,
459485
images_repo_root,
@@ -468,7 +494,11 @@ fn main() -> Result<()> {
468494
.context(OpenProductRepoForCheckoutSnafu)?;
469495

470496
let config = ctx.load_product_config()?;
471-
let upstream = config.upstream;
497+
let upstream = if ssh {
498+
utils::rewrite_git_https_url_to_ssh(&config.upstream).context(UrlRewriteSnafu)?
499+
} else {
500+
config.upstream
501+
};
472502

473503
// --base can be a reference, but patchable.toml should always have a resolved commit id,
474504
// so that it cannot be changed under our feet (without us knowing so, anyway...).
@@ -478,10 +508,13 @@ fn main() -> Result<()> {
478508
tracing::info!(?base, base.commit = ?base_commit, "resolved base commit");
479509

480510
let mirror_url = if mirror {
481-
let mirror_url = config
511+
let mut mirror_url = config
482512
.default_mirror
483513
.context(InitMirrorNotConfiguredSnafu)?;
484-
514+
if ssh {
515+
mirror_url =
516+
utils::rewrite_git_https_url_to_ssh(&mirror_url).context(UrlRewriteSnafu)?
517+
};
485518
// Add mirror remote
486519
let mut mirror_remote =
487520
product_repo
@@ -492,15 +525,7 @@ fn main() -> Result<()> {
492525

493526
// Push the base commit to the mirror
494527
tracing::info!(commit = %base_commit, base = base, url = mirror_url, "pushing commit to mirror");
495-
let mut callbacks = git2::RemoteCallbacks::new();
496-
callbacks.credentials(|url, username_from_url, _allowed_types| {
497-
git2::Cred::credential_helper(
498-
&git2::Config::open_default()
499-
.expect("failed to open default Git configuration"), // Use default git config,
500-
url,
501-
username_from_url,
502-
)
503-
});
528+
let mut callbacks = setup_git_credentials();
504529

505530
// Add progress tracking for push operation
506531
let (span_push, mut quant_push) =

rust/patchable/src/repo.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
use std::path::{self, Path, PathBuf};
22

33
use git2::{
4-
FetchOptions, ObjectType, Oid, RemoteCallbacks, Repository, RepositoryInitOptions,
4+
FetchOptions, ObjectType, Oid, Repository, RepositoryInitOptions,
55
WorktreeAddOptions,
66
};
77
use snafu::{ResultExt, Snafu};
88

99
use crate::{
1010
error::{self, CommitRef},
11-
utils::setup_progress_tracking,
11+
utils::{setup_git_credentials, setup_progress_tracking},
1212
};
1313

1414
#[derive(Debug, Snafu)]
@@ -157,7 +157,7 @@ pub fn resolve_and_fetch_commitish(
157157
let _ = span_recv.enter();
158158
let _ = span_index.enter();
159159

160-
let mut callbacks = RemoteCallbacks::new();
160+
let mut callbacks = setup_git_credentials();
161161
callbacks.transfer_progress(move |progress| {
162162
quant_recv.update_span_progress(
163163
progress.received_objects(),

rust/patchable/src/utils.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,36 @@
11
use std::path::Path;
22

33
use git2::Repository;
4+
use snafu::{OptionExt as _, ResultExt as _, Snafu};
45
use tracing::Span;
56
use tracing_indicatif::{span_ext::IndicatifSpanExt, style::ProgressStyle};
7+
use url::Url;
8+
9+
/// Errors that can occur during URL rewriting.
10+
#[derive(Debug, Snafu)]
11+
pub enum UrlRewriteError {
12+
#[snafu(display("Failed to parse URL {url:?}: {source}"))]
13+
ParseUrl { source: url::ParseError, url: String },
14+
#[snafu(display("URL {url:?} has no host component"))]
15+
NoHostInUrl { url: String },
16+
}
17+
18+
/// Rewrites a given URL to an SSH-style Git URL
19+
/// For example, `https://github.com/user/repo.git` becomes `[email protected]:user/repo.git`.
20+
pub fn rewrite_git_https_url_to_ssh(
21+
original_url: &str,
22+
) -> Result<String, UrlRewriteError> {
23+
let parsed_url = Url::parse(original_url).context(ParseUrlSnafu {
24+
url: original_url,
25+
})?;
26+
27+
let host = parsed_url.host_str().context(NoHostInUrlSnafu {
28+
url: original_url,
29+
})?;
30+
let path = parsed_url.path().trim_start_matches('/');
31+
32+
Ok(format!("git@{}:{}", host, path))
33+
}
634

735
/// Runs a function whenever a `value` changes "enough".
836
///
@@ -97,3 +125,20 @@ pub fn setup_progress_tracking(span: tracing::Span) -> (tracing::Span, Quantizer
97125
let quantizer = Quantizer::percent();
98126
(span, quantizer)
99127
}
128+
129+
/// Basic configuration of credentials for Git operations.
130+
pub fn setup_git_credentials<'a>() -> git2::RemoteCallbacks<'a> {
131+
let mut callbacks = git2::RemoteCallbacks::new();
132+
callbacks.credentials(|url, username_from_url, allowed_types| {
133+
if allowed_types.contains(git2::CredentialType::SSH_KEY) {
134+
git2::Cred::ssh_key_from_agent(username_from_url.unwrap_or("git"))
135+
} else {
136+
git2::Cred::credential_helper(
137+
&git2::Config::open_default().expect("failed to open default Git configuration"),
138+
url,
139+
username_from_url,
140+
)
141+
}
142+
});
143+
callbacks
144+
}

0 commit comments

Comments
 (0)