Skip to content

Commit 27c46b2

Browse files
committed
feat: support forking when initializing
1 parent d0d95fd commit 27c46b2

File tree

2 files changed

+120
-5
lines changed

2 files changed

+120
-5
lines changed

rust/patchable/src/main.rs

Lines changed: 118 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ use std::{fs::File, io::Write, path::PathBuf};
99

1010
use git2::{Oid, Repository};
1111
use serde::{Deserialize, Serialize};
12-
use snafu::{OptionExt, ResultExt as _, Snafu};
13-
use tracing_indicatif::IndicatifLayer;
12+
use snafu::{ensure, OptionExt, ResultExt as _, Snafu};
13+
use tracing_indicatif::{span_ext::IndicatifSpanExt, IndicatifLayer};
1414
use tracing_subscriber::{layer::SubscriberExt as _, util::SubscriberInitExt as _};
1515

1616
#[derive(clap::Parser)]
@@ -149,6 +149,11 @@ enum Cmd {
149149
/// Refs (such as tags and branches) will be resolved to commit IDs.
150150
#[clap(long)]
151151
base: String,
152+
153+
/// Assume a fork exists at stackabletech/<repo_name> and push the base ref to it.
154+
/// The fork URL will be stored in patchable.toml instead of the original upstream.
155+
#[clap(long)]
156+
forked: bool,
152157
},
153158

154159
/// Shows the patch directory for a given product version
@@ -197,6 +202,23 @@ pub enum Error {
197202
path: PathBuf,
198203
},
199204

205+
#[snafu(display("failed to parse upstream URL {url:?} to extract repository name"))]
206+
ParseUpstreamUrl { url: String },
207+
#[snafu(display("failed to add temporary fork remote for {url:?}"))]
208+
AddForkRemote { source: git2::Error, url: String },
209+
#[snafu(display("failed to push commit {commit} (as {refspec}) to fork {url:?}"))]
210+
PushToFork {
211+
source: git2::Error,
212+
url: String,
213+
refspec: String,
214+
commit: Oid,
215+
},
216+
#[snafu(display("failed to delete remote {name}"))]
217+
DeleteRemote {
218+
source: git2::Error,
219+
name: String,
220+
},
221+
200222
#[snafu(display("failed to find images repository"))]
201223
FindImagesRepo { source: repo::Error },
202224
#[snafu(display("images repository has no work directory"))]
@@ -287,7 +309,7 @@ fn main() -> Result<()> {
287309
let base_commit = repo::resolve_and_fetch_commitish(
288310
&product_repo,
289311
&config.base.to_string(),
290-
&config.upstream,
312+
&config.upstream
291313
)
292314
.context(FetchBaseCommitSnafu)?;
293315
let base_branch = ctx.base_branch();
@@ -397,7 +419,12 @@ fn main() -> Result<()> {
397419
);
398420
}
399421

400-
Cmd::Init { pv, upstream, base } => {
422+
Cmd::Init {
423+
pv,
424+
upstream,
425+
base,
426+
forked,
427+
} => {
401428
let ctx = ProductVersionContext {
402429
pv,
403430
images_repo_root,
@@ -414,8 +441,94 @@ fn main() -> Result<()> {
414441
// --base can be a reference, but patchable.toml should always have a resolved commit id,
415442
// so that it cannot be changed under our feet (without us knowing so, anyway...).
416443
tracing::info!(?base, "resolving base commit-ish");
417-
let base_commit = repo::resolve_and_fetch_commitish(&product_repo, &base, &upstream)
444+
445+
let (base_commit, upstream) = if forked {
446+
// Parse e.g. "https://github.com/apache/druid.git" into "druid"
447+
let repo_name = upstream.split('/').last().map(|repo| repo.trim_end_matches(".git")).context(ParseUpstreamUrlSnafu { url: &upstream })?;
448+
449+
ensure!(!repo_name.is_empty(), ParseUpstreamUrlSnafu { url: &upstream });
450+
451+
let fork_url = format!("https://github.com/stackabletech/{}.git", repo_name);
452+
tracing::info!(%fork_url, "using fork repository");
453+
454+
// Fetch from original upstream using a temporary remote name
455+
tracing::info!(upstream = upstream, %base, "fetching base ref from original upstream");
456+
let base_commit_oid = repo::resolve_and_fetch_commitish(
457+
&product_repo,
458+
&base,
459+
&upstream
460+
)
418461
.context(FetchBaseCommitSnafu)?;
462+
463+
tracing::info!(commit = %base_commit_oid, "fetched base commit OID");
464+
465+
// Add fork remote
466+
let temp_fork_remote = "patchable_fork_push";
467+
let mut fork_remote = product_repo
468+
.remote(temp_fork_remote, &fork_url)
469+
.context(AddForkRemoteSnafu { url: fork_url.clone() })?;
470+
471+
// Push the base commit to the fork
472+
tracing::info!(commit = %base_commit_oid, base = base, url = fork_url, "pushing commit to fork");
473+
let mut callbacks = git2::RemoteCallbacks::new();
474+
callbacks.credentials(|_url, username_from_url, _allowed_types| {
475+
git2::Cred::credential_helper(
476+
&git2::Config::open_default().unwrap(), // Use default git config
477+
_url,
478+
username_from_url,
479+
)
480+
});
481+
482+
// Add progress tracking for push operation
483+
let span_push = tracing::info_span!("pushing");
484+
span_push.pb_set_style(&utils::progress_bar_style());
485+
let _ = span_push.enter();
486+
let mut quant_push = utils::Quantizer::percent();
487+
callbacks.push_transfer_progress(move |current, total, _| {
488+
if total > 0 {
489+
quant_push.update_span_progress(current, total, &span_push);
490+
}
491+
});
492+
493+
let mut push_options = git2::PushOptions::new();
494+
push_options.remote_callbacks(callbacks);
495+
496+
// Check if the reference is a tag or branch by inspecting the git repository
497+
let refspec = {
498+
let tag_ref = format!("refs/tags/{}", base);
499+
let is_tag = product_repo
500+
.find_reference(&tag_ref)
501+
.is_ok();
502+
503+
if is_tag {
504+
// It's a tag
505+
format!("{}:refs/tags/{}", base_commit_oid, base)
506+
} else {
507+
// Assume it's a branch as default behavior
508+
format!("{}:refs/heads/{}", base_commit_oid, base)
509+
}
510+
};
511+
512+
tracing::info!(refspec = refspec, "constructed push refspec");
513+
514+
fork_remote
515+
.push(&[&refspec], Some(&mut push_options))
516+
.context(PushToForkSnafu {
517+
url: fork_url.clone(),
518+
refspec: &refspec,
519+
commit: base_commit_oid,
520+
})?;
521+
522+
product_repo.remote_delete(temp_fork_remote)
523+
.context(DeleteRemoteSnafu { name: temp_fork_remote.to_string() })?;
524+
525+
tracing::info!("successfully pushed base ref to fork");
526+
527+
(base_commit_oid, fork_url)
528+
} else {
529+
(repo::resolve_and_fetch_commitish(&product_repo, &base, &upstream).context(FetchBaseCommitSnafu)?, upstream)
530+
};
531+
419532
tracing::info!(?base, base.commit = ?base_commit, "resolved base commit");
420533

421534
tracing::info!("saving configuration");

rust/patchable/src/repo.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ pub fn resolve_and_fetch_commitish(
181181
Some(
182182
FetchOptions::new()
183183
.update_fetchhead(true)
184+
// download_tags is needed to later determine whether `commitish` is a tag or not
185+
.download_tags(git2::AutotagOption::Auto)
184186
.remote_callbacks(callbacks)
185187
// TODO: could be 1, CLI option maybe?
186188
.depth(0),

0 commit comments

Comments
 (0)