use crate::git::{RefContext, RefType};
#[derive(Debug, Clone)]
pub enum PlatformData {
GitHub {
host: String,
head_owner: String,
head_repo: String,
base_owner: String,
base_repo: String,
},
GitLab {
host: String,
base_owner: String,
base_repo: String,
source_project_id: u64,
target_project_id: u64,
},
}
#[derive(Debug, Clone)]
pub struct RemoteRefInfo {
pub ref_type: RefType,
pub number: u32,
pub title: String,
pub author: String,
pub state: String,
pub draft: bool,
pub source_branch: String,
pub is_cross_repo: bool,
pub url: String,
pub fork_push_url: Option<String>,
pub platform_data: PlatformData,
}
impl RefContext for RemoteRefInfo {
fn ref_type(&self) -> RefType {
self.ref_type
}
fn number(&self) -> u32 {
self.number
}
fn title(&self) -> &str {
&self.title
}
fn author(&self) -> &str {
&self.author
}
fn state(&self) -> &str {
&self.state
}
fn draft(&self) -> bool {
self.draft
}
fn url(&self) -> &str {
&self.url
}
fn source_ref(&self) -> String {
if self.is_cross_repo {
match &self.platform_data {
PlatformData::GitHub { head_owner, .. } => {
format!("{}:{}", head_owner, self.source_branch)
}
PlatformData::GitLab { .. } => {
if let Some(url) = &self.fork_push_url
&& let Some(namespace) = extract_namespace_from_url(url)
{
return format!("{}:{}", namespace, self.source_branch);
}
self.source_branch.clone()
}
}
} else {
self.source_branch.clone()
}
}
}
impl RemoteRefInfo {
pub fn prefixed_local_branch_name(&self) -> Option<String> {
match &self.platform_data {
PlatformData::GitHub { head_owner, .. } => {
Some(format!("{}/{}", head_owner, self.source_branch))
}
PlatformData::GitLab { .. } => None,
}
}
}
fn extract_namespace_from_url(url: &str) -> Option<String> {
if let Some(path) = url.strip_prefix("git@").and_then(|s| s.split(':').nth(1)) {
return extract_namespace_from_path(path);
}
if let Some(rest) = url
.strip_prefix("https://")
.or_else(|| url.strip_prefix("http://"))
{
let path = rest.split('/').skip(1).collect::<Vec<_>>().join("/");
return extract_namespace_from_path(&path);
}
None
}
fn extract_namespace_from_path(path: &str) -> Option<String> {
let path = path.strip_suffix(".git").unwrap_or(path);
let segments: Vec<_> = path.split('/').collect();
if segments.len() < 2 {
return None;
}
Some(segments[..segments.len() - 1].join("/"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_ref_same_repo() {
let info = RemoteRefInfo {
ref_type: RefType::Pr,
number: 101,
title: "Fix bug".to_string(),
author: "alice".to_string(),
state: "open".to_string(),
draft: false,
source_branch: "feature-auth".to_string(),
is_cross_repo: false,
url: "https://github.com/owner/repo/pull/101".to_string(),
fork_push_url: None,
platform_data: PlatformData::GitHub {
host: "github.com".to_string(),
head_owner: "owner".to_string(),
head_repo: "repo".to_string(),
base_owner: "owner".to_string(),
base_repo: "repo".to_string(),
},
};
assert_eq!(info.source_ref(), "feature-auth");
}
#[test]
fn test_source_ref_fork_github() {
let info = RemoteRefInfo {
ref_type: RefType::Pr,
number: 42,
title: "Add feature".to_string(),
author: "contributor".to_string(),
state: "open".to_string(),
draft: false,
source_branch: "feature-fix".to_string(),
is_cross_repo: true,
url: "https://github.com/owner/repo/pull/42".to_string(),
fork_push_url: Some("git@github.com:contributor/repo.git".to_string()),
platform_data: PlatformData::GitHub {
host: "github.com".to_string(),
head_owner: "contributor".to_string(),
head_repo: "repo".to_string(),
base_owner: "owner".to_string(),
base_repo: "repo".to_string(),
},
};
assert_eq!(info.source_ref(), "contributor:feature-fix");
}
#[test]
fn test_source_ref_fork_gitlab() {
let info = RemoteRefInfo {
ref_type: RefType::Mr,
number: 101,
title: "Fix bug".to_string(),
author: "contributor".to_string(),
state: "opened".to_string(),
draft: false,
source_branch: "feature-fix".to_string(),
is_cross_repo: true,
url: "https://gitlab.com/owner/repo/-/merge_requests/101".to_string(),
fork_push_url: Some("git@gitlab.com:contributor/repo.git".to_string()),
platform_data: PlatformData::GitLab {
host: "gitlab.com".to_string(),
base_owner: "owner".to_string(),
base_repo: "repo".to_string(),
source_project_id: 456,
target_project_id: 123,
},
};
assert_eq!(info.source_ref(), "contributor:feature-fix");
}
#[test]
fn test_prefixed_local_branch_name_github() {
let info = RemoteRefInfo {
ref_type: RefType::Pr,
number: 101,
title: "Test".to_string(),
author: "contributor".to_string(),
state: "open".to_string(),
draft: false,
source_branch: "main".to_string(),
is_cross_repo: true,
url: "https://github.com/owner/repo/pull/101".to_string(),
fork_push_url: Some("git@github.com:contributor/repo.git".to_string()),
platform_data: PlatformData::GitHub {
host: "github.com".to_string(),
head_owner: "contributor".to_string(),
head_repo: "repo".to_string(),
base_owner: "owner".to_string(),
base_repo: "repo".to_string(),
},
};
assert_eq!(
info.prefixed_local_branch_name(),
Some("contributor/main".to_string())
);
}
#[test]
fn test_prefixed_local_branch_name_gitlab() {
let info = RemoteRefInfo {
ref_type: RefType::Mr,
number: 101,
title: "Test".to_string(),
author: "contributor".to_string(),
state: "opened".to_string(),
draft: false,
source_branch: "main".to_string(),
is_cross_repo: true,
url: "https://gitlab.com/owner/repo/-/merge_requests/101".to_string(),
fork_push_url: Some("git@gitlab.com:contributor/repo.git".to_string()),
platform_data: PlatformData::GitLab {
host: "gitlab.com".to_string(),
base_owner: "owner".to_string(),
base_repo: "repo".to_string(),
source_project_id: 456,
target_project_id: 123,
},
};
assert_eq!(info.prefixed_local_branch_name(), None);
}
#[test]
fn test_extract_namespace_from_url_ssh() {
assert_eq!(
extract_namespace_from_url("git@gitlab.com:owner/repo.git"),
Some("owner".to_string())
);
assert_eq!(
extract_namespace_from_url("git@github.com:contributor/repo.git"),
Some("contributor".to_string())
);
}
#[test]
fn test_extract_namespace_from_url_https() {
assert_eq!(
extract_namespace_from_url("https://gitlab.com/owner/repo.git"),
Some("owner".to_string())
);
assert_eq!(
extract_namespace_from_url("http://github.com/owner/repo.git"),
Some("owner".to_string())
);
}
#[test]
fn test_extract_namespace_from_url_nested() {
assert_eq!(
extract_namespace_from_url("git@gitlab.com:group/subgroup/repo.git"),
Some("group/subgroup".to_string())
);
assert_eq!(
extract_namespace_from_url("https://gitlab.com/group/subgroup/repo.git"),
Some("group/subgroup".to_string())
);
assert_eq!(
extract_namespace_from_url("git@gitlab.com:org/team/project/repo.git"),
Some("org/team/project".to_string())
);
}
#[test]
fn test_extract_namespace_from_url_invalid() {
assert_eq!(extract_namespace_from_url("invalid-url"), None);
assert_eq!(extract_namespace_from_url(""), None);
assert_eq!(extract_namespace_from_url("git@gitlab.com:repo.git"), None);
}
}