aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-16 20:15:44 +0900
committernsfisis <nsfisis@gmail.com>2026-05-16 20:15:44 +0900
commite813e25cf6e41d89375e842f8abc7d6021d1cb21 (patch)
tree9b9ff7c1b6f1eddf3c0305fab430e7ea6a96ffa9 /crates/shirabe/src
parent73bb568e4ac3ee096c048633173bd0efd0c2214c (diff)
downloadphp-shirabe-e813e25cf6e41d89375e842f8abc7d6021d1cb21.tar.gz
php-shirabe-e813e25cf6e41d89375e842f8abc7d6021d1cb21.tar.zst
php-shirabe-e813e25cf6e41d89375e842f8abc7d6021d1cb21.zip
feat(port): port GitBitbucketDriver.php
Diffstat (limited to 'crates/shirabe/src')
-rw-r--r--crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs853
1 files changed, 853 insertions, 0 deletions
diff --git a/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs b/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs
index 9eb944f..495b427 100644
--- a/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs
+++ b/crates/shirabe/src/repository/vcs/git_bitbucket_driver.rs
@@ -1 +1,854 @@
//! ref: composer/src/Composer/Repository/Vcs/GitBitbucketDriver.php
+
+use anyhow::Result;
+use chrono::{DateTime, Utc};
+use indexmap::IndexMap;
+use shirabe_external_packages::composer::pcre::preg::Preg;
+use shirabe_php_shim::{
+ array_key_exists, array_search_mixed, extension_loaded, http_build_query_mixed, implode,
+ in_array, is_array, sprintf, strpos, InvalidArgumentException, LogicException, PhpMixed,
+ RuntimeException,
+};
+
+use crate::cache::Cache;
+use crate::config::Config;
+use crate::downloader::transport_exception::TransportException;
+use crate::io::io_interface::IOInterface;
+use crate::json::json_file::JsonFile;
+use crate::repository::vcs::git_driver::GitDriver;
+use crate::repository::vcs::vcs_driver::VcsDriver;
+use crate::repository::vcs::vcs_driver_interface::VcsDriverInterface;
+use crate::util::bitbucket::Bitbucket;
+use crate::util::http::response::Response;
+
+#[derive(Debug)]
+pub struct GitBitbucketDriver {
+ pub(crate) inner: VcsDriver,
+ /// @var string
+ pub(crate) owner: String,
+ /// @var string
+ pub(crate) repository: String,
+ /// @var bool
+ has_issues: bool,
+ /// @var ?string
+ root_identifier: Option<String>,
+ /// @var array<int|string, string> Map of tag name to identifier
+ tags: Option<IndexMap<String, String>>,
+ /// @var array<int|string, string> Map of branch name to identifier
+ branches: Option<IndexMap<String, String>>,
+ /// @var string
+ branches_url: String,
+ /// @var string
+ tags_url: String,
+ /// @var string
+ home_url: String,
+ /// @var string
+ website: String,
+ /// @var string
+ clone_https_url: String,
+ /// @var array<string, mixed>
+ repo_data: IndexMap<String, PhpMixed>,
+ /// @var ?VcsDriver
+ pub(crate) fallback_driver: Option<Box<dyn VcsDriverInterface>>,
+ /// @var string|null if set either git or hg
+ vcs_type: Option<String>,
+}
+
+impl GitBitbucketDriver {
+ /// @inheritDoc
+ pub fn initialize(&mut self) -> Result<()> {
+ let matched = Preg::is_match_strict_groups(
+ r"#^https?://bitbucket\.org/([^/]+)/([^/]+?)(?:\.git|/?)?$#i",
+ &self.inner.url,
+ );
+ if matched.is_none() {
+ return Err(InvalidArgumentException {
+ message: sprintf(
+ "The Bitbucket repository URL %s is invalid. It must be the HTTPS URL of a Bitbucket repository.",
+ &[PhpMixed::String(self.inner.url.clone())],
+ ),
+ code: 0,
+ }
+ .into());
+ }
+ let m = matched.unwrap();
+
+ self.owner = m.get(1).cloned().unwrap_or_default();
+ self.repository = m.get(2).cloned().unwrap_or_default();
+ self.inner.origin_url = "bitbucket.org".to_string();
+ self.inner.cache = Some(Cache::new(
+ &*self.inner.io,
+ &implode(
+ "/",
+ &[
+ self.inner
+ .config
+ .get("cache-repo-dir")
+ .as_string()
+ .unwrap_or("")
+ .to_string(),
+ self.inner.origin_url.clone(),
+ self.owner.clone(),
+ self.repository.clone(),
+ ],
+ ),
+ None,
+ ));
+ self.inner
+ .cache
+ .as_mut()
+ .unwrap()
+ .set_read_only(self.inner.config.get("cache-read-only").as_bool().unwrap_or(false));
+
+ Ok(())
+ }
+
+ /// @inheritDoc
+ pub fn get_url(&self) -> String {
+ if let Some(fallback) = self.fallback_driver.as_ref() {
+ return fallback.get_url();
+ }
+
+ self.clone_https_url.clone()
+ }
+
+ /// Attempts to fetch the repository data via the BitBucket API and
+ /// sets some parameters which are used in other methods
+ ///
+ /// @phpstan-impure
+ fn get_repo_data(&mut self) -> Result<bool> {
+ let resource = sprintf(
+ "https://api.bitbucket.org/2.0/repositories/%s/%s?%s",
+ &[
+ PhpMixed::String(self.owner.clone()),
+ PhpMixed::String(self.repository.clone()),
+ PhpMixed::String(http_build_query_mixed(
+ &{
+ let mut m: IndexMap<String, PhpMixed> = IndexMap::new();
+ m.insert(
+ "fields".to_string(),
+ PhpMixed::String("-project,-owner".to_string()),
+ );
+ m
+ },
+ "",
+ "&",
+ )),
+ ],
+ );
+
+ let repo_data = self
+ .fetch_with_oauth_credentials(&resource, true)?
+ .decode_json()?;
+ if self.fallback_driver.is_some() {
+ return Ok(false);
+ }
+ let clone_links = repo_data
+ .get("links")
+ .and_then(|v| match v {
+ PhpMixed::Array(m) => m.get("clone"),
+ _ => None,
+ })
+ .cloned();
+ self.parse_clone_urls(clone_links);
+
+ self.has_issues = !shirabe_php_shim::empty(
+ repo_data.get("has_issues").cloned().as_ref().unwrap_or(&PhpMixed::Null),
+ );
+ self.branches_url = repo_data
+ .get("links")
+ .and_then(|v| match v {
+ PhpMixed::Array(m) => m.get("branches"),
+ _ => None,
+ })
+ .and_then(|v| match v.as_ref() {
+ PhpMixed::Array(m) => m.get("href").and_then(|v| v.as_string()).map(String::from),
+ _ => None,
+ })
+ .unwrap_or_default();
+ self.tags_url = repo_data
+ .get("links")
+ .and_then(|v| match v {
+ PhpMixed::Array(m) => m.get("tags"),
+ _ => None,
+ })
+ .and_then(|v| match v.as_ref() {
+ PhpMixed::Array(m) => m.get("href").and_then(|v| v.as_string()).map(String::from),
+ _ => None,
+ })
+ .unwrap_or_default();
+ self.home_url = repo_data
+ .get("links")
+ .and_then(|v| match v {
+ PhpMixed::Array(m) => m.get("html"),
+ _ => None,
+ })
+ .and_then(|v| match v.as_ref() {
+ PhpMixed::Array(m) => m.get("href").and_then(|v| v.as_string()).map(String::from),
+ _ => None,
+ })
+ .unwrap_or_default();
+ self.website = repo_data
+ .get("website")
+ .and_then(|v| v.as_string())
+ .map(String::from)
+ .unwrap_or_default();
+ self.vcs_type = repo_data
+ .get("scm")
+ .and_then(|v| v.as_string())
+ .map(String::from);
+
+ self.repo_data = repo_data;
+
+ Ok(true)
+ }
+
+ /// @inheritDoc
+ pub fn get_composer_information(
+ &mut self,
+ identifier: &str,
+ ) -> Result<Option<IndexMap<String, PhpMixed>>> {
+ if let Some(fallback) = self.fallback_driver.as_mut() {
+ return fallback.get_composer_information(identifier);
+ }
+
+ if !self.inner.info_cache.contains_key(identifier) {
+ let mut composer: Option<IndexMap<String, PhpMixed>> = None;
+ if self.inner.should_cache(identifier)
+ && {
+ let res = self
+ .inner
+ .cache
+ .as_ref()
+ .and_then(|c| c.read(identifier).ok().flatten());
+ if let Some(res) = res {
+ composer = Some(JsonFile::parse_json(&res, None)?);
+ true
+ } else {
+ false
+ }
+ }
+ {
+ // composer already set above
+ } else {
+ composer = self.inner.get_base_composer_information(identifier)?;
+
+ if self.inner.should_cache(identifier) {
+ self.inner.cache.as_ref().unwrap().write(
+ identifier,
+ &JsonFile::encode(
+ &PhpMixed::Array(
+ composer
+ .clone()
+ .unwrap_or_default()
+ .into_iter()
+ .map(|(k, v)| (k, Box::new(v)))
+ .collect(),
+ ),
+ shirabe_php_shim::JSON_UNESCAPED_UNICODE
+ | shirabe_php_shim::JSON_UNESCAPED_SLASHES,
+ JsonFile::INDENT_DEFAULT,
+ ),
+ )?;
+ }
+ }
+
+ if let Some(mut composer_map) = composer.clone() {
+ // specials for bitbucket
+ if composer_map.contains_key("support")
+ && !is_array(composer_map.get("support").unwrap())
+ {
+ composer_map.insert(
+ "support".to_string(),
+ PhpMixed::Array(IndexMap::new()),
+ );
+ }
+ let support_has_source = composer_map
+ .get("support")
+ .and_then(|v| match v {
+ PhpMixed::Array(m) => Some(m.contains_key("source")),
+ _ => None,
+ })
+ .unwrap_or(false);
+ if !support_has_source {
+ let tags = self.get_tags()?;
+ let branches_for_search = self.get_branches()?;
+ let label = array_search_mixed(
+ &PhpMixed::String(identifier.to_string()),
+ &PhpMixed::Array(
+ tags.iter()
+ .map(|(k, v)| (k.clone(), Box::new(PhpMixed::String(v.clone()))))
+ .collect(),
+ ),
+ false,
+ )
+ .or_else(|| {
+ array_search_mixed(
+ &PhpMixed::String(identifier.to_string()),
+ &PhpMixed::Array(
+ branches_for_search
+ .iter()
+ .map(|(k, v)| {
+ (k.clone(), Box::new(PhpMixed::String(v.clone())))
+ })
+ .collect(),
+ ),
+ false,
+ )
+ })
+ .map(|v| v.as_string().unwrap_or("").to_string())
+ .unwrap_or_else(|| identifier.to_string());
+
+ let tags2 = self.get_tags()?;
+ let branches2 = self.get_branches()?;
+ let mut hash: Option<String> = None;
+ if array_key_exists(&label, &tags2) {
+ hash = tags2.get(&label).cloned();
+ } else if array_key_exists(&label, &branches2) {
+ hash = branches2.get(&label).cloned();
+ }
+
+ let support_entry = composer_map
+ .entry("support".to_string())
+ .or_insert(PhpMixed::Array(IndexMap::new()));
+ if hash.is_none() {
+ if let PhpMixed::Array(support_map) = support_entry {
+ support_map.insert(
+ "source".to_string(),
+ Box::new(PhpMixed::String(sprintf(
+ "https://%s/%s/%s/src",
+ &[
+ PhpMixed::String(self.inner.origin_url.clone()),
+ PhpMixed::String(self.owner.clone()),
+ PhpMixed::String(self.repository.clone()),
+ ],
+ ))),
+ );
+ }
+ } else if let PhpMixed::Array(support_map) = support_entry {
+ support_map.insert(
+ "source".to_string(),
+ Box::new(PhpMixed::String(sprintf(
+ "https://%s/%s/%s/src/%s/?at=%s",
+ &[
+ PhpMixed::String(self.inner.origin_url.clone()),
+ PhpMixed::String(self.owner.clone()),
+ PhpMixed::String(self.repository.clone()),
+ PhpMixed::String(hash.unwrap()),
+ PhpMixed::String(label.clone()),
+ ],
+ ))),
+ );
+ }
+ }
+ let support_has_issues = composer_map
+ .get("support")
+ .and_then(|v| match v {
+ PhpMixed::Array(m) => Some(m.contains_key("issues")),
+ _ => None,
+ })
+ .unwrap_or(false);
+ if !support_has_issues && self.has_issues {
+ let support_entry = composer_map
+ .entry("support".to_string())
+ .or_insert(PhpMixed::Array(IndexMap::new()));
+ if let PhpMixed::Array(support_map) = support_entry {
+ support_map.insert(
+ "issues".to_string(),
+ Box::new(PhpMixed::String(sprintf(
+ "https://%s/%s/%s/issues",
+ &[
+ PhpMixed::String(self.inner.origin_url.clone()),
+ PhpMixed::String(self.owner.clone()),
+ PhpMixed::String(self.repository.clone()),
+ ],
+ ))),
+ );
+ }
+ }
+ if !composer_map.contains_key("homepage") {
+ composer_map.insert(
+ "homepage".to_string(),
+ if self.website.is_empty() {
+ PhpMixed::String(self.home_url.clone())
+ } else {
+ PhpMixed::String(self.website.clone())
+ },
+ );
+ }
+ composer = Some(composer_map);
+ }
+
+ self.inner.info_cache.insert(identifier.to_string(), composer);
+ }
+
+ Ok(self.inner.info_cache.get(identifier).cloned().flatten())
+ }
+
+ /// @inheritDoc
+ pub fn get_file_content(
+ &mut self,
+ file: &str,
+ identifier: &str,
+ ) -> Result<Option<String>> {
+ if let Some(fallback) = self.fallback_driver.as_mut() {
+ return fallback.get_file_content(file, identifier);
+ }
+
+ let mut identifier = identifier.to_string();
+ if strpos(&identifier, "/").is_some() {
+ let branches = self.get_branches()?;
+ if let Some(b) = branches.get(&identifier) {
+ identifier = b.clone();
+ }
+ }
+
+ let resource = sprintf(
+ "https://api.bitbucket.org/2.0/repositories/%s/%s/src/%s/%s",
+ &[
+ PhpMixed::String(self.owner.clone()),
+ PhpMixed::String(self.repository.clone()),
+ PhpMixed::String(identifier),
+ PhpMixed::String(file.to_string()),
+ ],
+ );
+
+ Ok(Some(
+ self.fetch_with_oauth_credentials(&resource, false)?
+ .get_body(),
+ ))
+ }
+
+ /// @inheritDoc
+ pub fn get_change_date(&mut self, identifier: &str) -> Result<Option<DateTime<Utc>>> {
+ if let Some(fallback) = self.fallback_driver.as_mut() {
+ return fallback.get_change_date(identifier);
+ }
+
+ let mut identifier = identifier.to_string();
+ if strpos(&identifier, "/").is_some() {
+ let branches = self.get_branches()?;
+ if let Some(b) = branches.get(&identifier) {
+ identifier = b.clone();
+ }
+ }
+
+ let resource = sprintf(
+ "https://api.bitbucket.org/2.0/repositories/%s/%s/commit/%s?fields=date",
+ &[
+ PhpMixed::String(self.owner.clone()),
+ PhpMixed::String(self.repository.clone()),
+ PhpMixed::String(identifier),
+ ],
+ );
+ let commit = self
+ .fetch_with_oauth_credentials(&resource, false)?
+ .decode_json()?;
+
+ // TODO(phase-b): port PHP `new \DateTimeImmutable($commit['date'])`
+ let date_str = commit
+ .get("date")
+ .and_then(|v| v.as_string())
+ .unwrap_or("");
+ let date: DateTime<Utc> = chrono::DateTime::parse_from_rfc3339(date_str)
+ .map_err(|e| anyhow::anyhow!(e))?
+ .with_timezone(&Utc);
+ Ok(Some(date))
+ }
+
+ /// @inheritDoc
+ pub fn get_source(&self, identifier: &str) -> IndexMap<String, String> {
+ if let Some(fallback) = self.fallback_driver.as_ref() {
+ return fallback.get_source(identifier);
+ }
+
+ let mut m: IndexMap<String, String> = IndexMap::new();
+ m.insert(
+ "type".to_string(),
+ self.vcs_type.clone().unwrap_or_default(),
+ );
+ m.insert("url".to_string(), self.get_url());
+ m.insert("reference".to_string(), identifier.to_string());
+ m
+ }
+
+ /// @inheritDoc
+ pub fn get_dist(&self, identifier: &str) -> Option<IndexMap<String, String>> {
+ if let Some(fallback) = self.fallback_driver.as_ref() {
+ return fallback.get_dist(identifier);
+ }
+
+ let url = sprintf(
+ "https://bitbucket.org/%s/%s/get/%s.zip",
+ &[
+ PhpMixed::String(self.owner.clone()),
+ PhpMixed::String(self.repository.clone()),
+ PhpMixed::String(identifier.to_string()),
+ ],
+ );
+
+ let mut m: IndexMap<String, String> = IndexMap::new();
+ m.insert("type".to_string(), "zip".to_string());
+ m.insert("url".to_string(), url);
+ m.insert("reference".to_string(), identifier.to_string());
+ m.insert("shasum".to_string(), String::new());
+ Some(m)
+ }
+
+ /// @inheritDoc
+ pub fn get_tags(&mut self) -> Result<IndexMap<String, String>> {
+ if let Some(fallback) = self.fallback_driver.as_mut() {
+ return fallback.get_tags();
+ }
+
+ if self.tags.is_none() {
+ let mut tags: IndexMap<String, String> = IndexMap::new();
+ let mut resource = sprintf(
+ "%s?%s",
+ &[
+ PhpMixed::String(self.tags_url.clone()),
+ PhpMixed::String(http_build_query_mixed(
+ &{
+ let mut m: IndexMap<String, PhpMixed> = IndexMap::new();
+ m.insert("pagelen".to_string(), PhpMixed::Int(100));
+ m.insert(
+ "fields".to_string(),
+ PhpMixed::String(
+ "values.name,values.target.hash,next".to_string(),
+ ),
+ );
+ m.insert(
+ "sort".to_string(),
+ PhpMixed::String("-target.date".to_string()),
+ );
+ m
+ },
+ "",
+ "&",
+ )),
+ ],
+ );
+ let mut has_next = true;
+ while has_next {
+ let tags_data = self
+ .fetch_with_oauth_credentials(&resource, false)?
+ .decode_json()?;
+ let values = tags_data.get("values").cloned();
+ if let Some(PhpMixed::List(list)) = values {
+ for data in list {
+ if let PhpMixed::Array(m) = data.as_ref() {
+ let name = m
+ .get("name")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string();
+ let hash = m
+ .get("target")
+ .and_then(|v| match v.as_ref() {
+ PhpMixed::Array(m) => m.get("hash"),
+ _ => None,
+ })
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string();
+ tags.insert(name, hash);
+ }
+ }
+ }
+ if shirabe_php_shim::empty(
+ tags_data.get("next").cloned().as_ref().unwrap_or(&PhpMixed::Null),
+ ) {
+ has_next = false;
+ } else {
+ resource = tags_data
+ .get("next")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string();
+ }
+ }
+
+ self.tags = Some(tags);
+ }
+
+ Ok(self.tags.clone().unwrap_or_default())
+ }
+
+ /// @inheritDoc
+ pub fn get_branches(&mut self) -> Result<IndexMap<String, String>> {
+ if let Some(fallback) = self.fallback_driver.as_mut() {
+ return fallback.get_branches();
+ }
+
+ if self.branches.is_none() {
+ let mut branches: IndexMap<String, String> = IndexMap::new();
+ let mut resource = sprintf(
+ "%s?%s",
+ &[
+ PhpMixed::String(self.branches_url.clone()),
+ PhpMixed::String(http_build_query_mixed(
+ &{
+ let mut m: IndexMap<String, PhpMixed> = IndexMap::new();
+ m.insert("pagelen".to_string(), PhpMixed::Int(100));
+ m.insert(
+ "fields".to_string(),
+ PhpMixed::String(
+ "values.name,values.target.hash,values.heads,next"
+ .to_string(),
+ ),
+ );
+ m.insert(
+ "sort".to_string(),
+ PhpMixed::String("-target.date".to_string()),
+ );
+ m
+ },
+ "",
+ "&",
+ )),
+ ],
+ );
+ let mut has_next = true;
+ while has_next {
+ let branch_data = self
+ .fetch_with_oauth_credentials(&resource, false)?
+ .decode_json()?;
+ let values = branch_data.get("values").cloned();
+ if let Some(PhpMixed::List(list)) = values {
+ for data in list {
+ if let PhpMixed::Array(m) = data.as_ref() {
+ let name = m
+ .get("name")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string();
+ let hash = m
+ .get("target")
+ .and_then(|v| match v.as_ref() {
+ PhpMixed::Array(m) => m.get("hash"),
+ _ => None,
+ })
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string();
+ branches.insert(name, hash);
+ }
+ }
+ }
+ if shirabe_php_shim::empty(
+ branch_data.get("next").cloned().as_ref().unwrap_or(&PhpMixed::Null),
+ ) {
+ has_next = false;
+ } else {
+ resource = branch_data
+ .get("next")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string();
+ }
+ }
+
+ self.branches = Some(branches);
+ }
+
+ Ok(self.branches.clone().unwrap_or_default())
+ }
+
+ /// Get the remote content.
+ ///
+ /// @phpstan-impure
+ fn fetch_with_oauth_credentials(
+ &mut self,
+ url: &str,
+ fetching_repo_data: bool,
+ ) -> Result<Response> {
+ match self.inner.get_contents(url, false) {
+ Ok(r) => Ok(r),
+ Err(e) => {
+ // TODO(phase-b): only handle TransportException
+ let bitbucket_util = Bitbucket::new(
+ &*self.inner.io,
+ &self.inner.config,
+ Some(self.inner.process.clone()),
+ Some(self.inner.http_downloader.clone()),
+ );
+
+ if let Some(te) = e.downcast_ref::<TransportException>() {
+ let code = te.get_code();
+ let in_set = in_array(
+ PhpMixed::Int(code),
+ &PhpMixed::List(vec![
+ Box::new(PhpMixed::Int(403)),
+ Box::new(PhpMixed::Int(404)),
+ ]),
+ true,
+ );
+ if in_set
+ || (401 == code && strpos(te.get_message(), "Could not authenticate against") == Some(0))
+ {
+ if !self.inner.io.has_authentication(&self.inner.origin_url)
+ && bitbucket_util.authorize_oauth(&self.inner.origin_url)
+ {
+ return self.inner.get_contents(url, false);
+ }
+
+ if !self.inner.io.is_interactive() && fetching_repo_data {
+ self.attempt_clone_fallback()?;
+
+ let mut headers: IndexMap<String, PhpMixed> = IndexMap::new();
+ headers.insert(
+ "url".to_string(),
+ PhpMixed::String("dummy".to_string()),
+ );
+ return Ok(Response::new(headers, 200, IndexMap::new(), "null".to_string()));
+ }
+ }
+ }
+
+ Err(e)
+ }
+ }
+ }
+
+ /// Generate an SSH URL
+ fn generate_ssh_url(&self) -> String {
+ format!(
+ "git@{}:{}/{}.git",
+ self.inner.origin_url, self.owner, self.repository
+ )
+ }
+
+ /// @phpstan-impure
+ ///
+ /// @return true
+ /// @throws \RuntimeException
+ fn attempt_clone_fallback(&mut self) -> Result<bool> {
+ match self.setup_fallback_driver(&self.generate_ssh_url()) {
+ Ok(()) => Ok(true),
+ Err(e) => {
+ // TODO(phase-b): only catch RuntimeException
+ self.fallback_driver = None;
+
+ self.inner.io.write_error(&format!(
+ "<error>Failed to clone the {} repository, try running in interactive mode so that you can enter your Bitbucket OAuth consumer credentials</error>",
+ self.generate_ssh_url()
+ ));
+ Err(e)
+ }
+ }
+ }
+
+ fn setup_fallback_driver(&mut self, url: &str) -> Result<()> {
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(url.to_string()));
+ // TODO(phase-b): construct VcsDriver from repo_config / io / config / etc.
+ let mut driver = GitDriver {
+ inner: todo!("phase-b: build VcsDriver for fallback GitDriver"),
+ tags: None,
+ branches: None,
+ root_identifier: None,
+ repo_dir: String::new(),
+ };
+ driver.initialize()?;
+ self.fallback_driver = Some(Box::new(driver));
+ Ok(())
+ }
+
+ /// @param array<array{name: string, href: string}> $cloneLinks
+ fn parse_clone_urls(&mut self, clone_links: Option<Box<PhpMixed>>) {
+ let list = match clone_links.as_deref() {
+ Some(PhpMixed::List(l)) => l.clone(),
+ _ => return,
+ };
+ for clone_link in list {
+ if let PhpMixed::Array(m) = clone_link.as_ref() {
+ if m.get("name").and_then(|v| v.as_string()) == Some("https") {
+ // Format: https://(user@)bitbucket.org/{user}/{repo}
+ // Strip username from URL (only present in clone URL's for private repositories)
+ self.clone_https_url = Preg::replace(
+ r"/https:\/\/([^@]+@)?/",
+ "https://",
+ m.get("href").and_then(|v| v.as_string()).unwrap_or(""),
+ );
+ }
+ }
+ }
+ }
+
+ /// @inheritDoc
+ pub fn get_root_identifier(&mut self) -> Result<String> {
+ if let Some(fallback) = self.fallback_driver.as_mut() {
+ return fallback.get_root_identifier();
+ }
+
+ if self.root_identifier.is_none() {
+ if !self.get_repo_data()? {
+ if self.fallback_driver.is_none() {
+ return Err(LogicException {
+ message:
+ "A fallback driver should be setup if getRepoData returns false"
+ .to_string(),
+ code: 0,
+ }
+ .into());
+ }
+
+ return self.fallback_driver.as_mut().unwrap().get_root_identifier();
+ }
+
+ if self.vcs_type.as_deref() != Some("git") {
+ return Err(RuntimeException {
+ message: format!(
+ "{} does not appear to be a git repository, use {} but remember that Bitbucket no longer supports the mercurial repositories. https://bitbucket.org/blog/sunsetting-mercurial-support-in-bitbucket",
+ self.inner.url, self.clone_https_url
+ ),
+ code: 0,
+ }
+ .into());
+ }
+
+ self.root_identifier = self
+ .repo_data
+ .get("mainbranch")
+ .and_then(|v| match v {
+ PhpMixed::Array(m) => m.get("name"),
+ _ => None,
+ })
+ .and_then(|v| v.as_string())
+ .map(String::from)
+ .or_else(|| Some("master".to_string()));
+ }
+
+ Ok(self.root_identifier.clone().unwrap_or_default())
+ }
+
+ /// @inheritDoc
+ pub fn supports(
+ io: &dyn IOInterface,
+ _config: &Config,
+ url: &str,
+ _deep: bool,
+ ) -> bool {
+ if !Preg::is_match(
+ r"#^https?://bitbucket\.org/([^/]+)/([^/]+?)(\.git|/?)?$#i",
+ url,
+ ) {
+ return false;
+ }
+
+ if !extension_loaded("openssl") {
+ io.write_error(
+ &format!(
+ "Skipping Bitbucket git driver for {} because the OpenSSL PHP extension is missing.",
+ url
+ ),
+ );
+ // PHP: writeError(..., true, IOInterface::VERBOSE)
+ // TODO(phase-b): IOInterface::VERBOSE verbosity argument
+
+ return false;
+ }
+
+ true
+ }
+}