From 92a2f1c0a1217dafd67dcadd988b72c2c98c8ca9 Mon Sep 17 00:00:00 2001 From: Nikolai Orekhov Date: Fri, 17 Apr 2026 15:30:17 +0300 Subject: [PATCH] added support for Bitbucket Data Center --- Source/MantisSourceGitBasePlugin.class.php | 23 + Source/MantisSourcePlugin.class.php | 6 + Source/lang/strings_english.txt | 2 + Source/pages/checkin.php | 9 +- Source/pages/index.php | 2 +- Source/pages/repo_manage_page.php | 5 +- SourceBitbucketDC/SourceBitbucketDC.php | 679 ++++++++++++++++++++ SourceBitbucketDC/files/test_connection.js | 12 + SourceBitbucketDC/lang/strings_english.txt | 25 + SourceBitbucketDC/pages/test_connection.php | 16 + SourceSVN/SourceSVN.php | 2 +- 11 files changed, 775 insertions(+), 6 deletions(-) create mode 100644 SourceBitbucketDC/SourceBitbucketDC.php create mode 100644 SourceBitbucketDC/files/test_connection.js create mode 100644 SourceBitbucketDC/lang/strings_english.txt create mode 100644 SourceBitbucketDC/pages/test_connection.php diff --git a/Source/MantisSourceGitBasePlugin.class.php b/Source/MantisSourceGitBasePlugin.class.php index 068e83135..5c3d14c61 100644 --- a/Source/MantisSourceGitBasePlugin.class.php +++ b/Source/MantisSourceGitBasePlugin.class.php @@ -41,6 +41,7 @@ abstract class MantisSourceGitBasePlugin extends MantisSourcePlugin * */ const CFG_DEFAULT_PRIMARY_BRANCH = 'git_default_primary_branch'; + const CFG_DEFAULT_URL = 'git_default_url'; /** * Error constants @@ -143,6 +144,22 @@ public function update_config_form() { + + + + + + +
+ + + "> - + diff --git a/Source/pages/repo_manage_page.php b/Source/pages/repo_manage_page.php index 48ec2f2e9..18c23759b 100644 --- a/Source/pages/repo_manage_page.php +++ b/Source/pages/repo_manage_page.php @@ -120,6 +120,7 @@ function convert_to_key_value( $p_array ) { + show_manage_actions( $t_repo ) ?>
@@ -221,17 +222,15 @@ function convert_to_key_value( $p_array ) { $t_mappings[] = new SourceMapping( null, null, null ); foreach( $t_mappings as $t_mapping ) { + $t_branch = str_replace( '.', '_', $t_mapping->branch ); # Since it is not possible to update the branch's name (see #230), # the input field is disabled, except for the 'new mapping' row $t_disabled = 'disabled'; if( is_null( $t_mapping->branch ) ) { - $t_branch = ''; $t_disabled = ''; if( count( $t_mappings ) > 1 ) { echo ''; } - } else { - $t_branch = str_replace( '.', '_', $t_mapping->branch ); } ?> diff --git a/SourceBitbucketDC/SourceBitbucketDC.php b/SourceBitbucketDC/SourceBitbucketDC.php new file mode 100644 index 000000000..a47f9363a --- /dev/null +++ b/SourceBitbucketDC/SourceBitbucketDC.php @@ -0,0 +1,679 @@ +name = plugin_lang_get( 'title' ); + $this->description = plugin_lang_get( 'description' ); + $this->author = 'Nikolai Orekhov'; + $this->contact = 'no@3cx.com'; + $this->url = ''; + } + + public function errors() { + $t_errors = array( + self::ERROR_BITBUCKET_DC_API => plugin_lang_get( 'error_' . self::ERROR_BITBUCKET_DC_API ), + ); + return array_merge( parent::errors(), $t_errors ); + } + + // ----------------------------------------------------------------------- + // Display helpers + // ----------------------------------------------------------------------- + + public function show_type() { + return plugin_lang_get( 'bitbucketdc' ); + } + + public function show_changeset( $p_repo, $p_changeset ) { + return substr( $p_changeset->revision, 0, 8 ); + } + + public function show_file( $p_repo, $p_changeset, $p_file ) { + return $p_file->action . ' - ' . $p_file->filename; + } + + // ----------------------------------------------------------------------- + // URL builders + // ----------------------------------------------------------------------- + + /** + * Web UI base URL for the repository. + * e.g. https://bitbucket.example.com/projects/PROJ/repos/my-repo + */ + private function url_base( $p_repo ) { + $t_base = rtrim( $p_repo->url, '/' ); + $t_project = $p_repo->info['dc_project_key']; + $t_slug = $p_repo->info['dc_repo_slug']; + return "$t_base/projects/$t_project/repos/$t_slug"; + } + + /** + * REST API base URL for the repository. + * e.g. https://bitbucket.example.com/rest/api/1.0/projects/PROJ/repos/my-repo + */ + private function api_base( $p_repo ) { + $t_base = rtrim( $p_repo->url, '/' ); + $t_project = $p_repo->info['dc_project_key']; + $t_slug = $p_repo->info['dc_repo_slug']; + return "$t_base/rest/api/1.0/projects/$t_project/repos/$t_slug"; + } + + public function url_repo( $p_repo, $p_changeset = null ) { + return $this->url_base( $p_repo ) . '/browse'; + } + + public function url_changeset( $p_repo, $p_changeset ) { + return $this->url_base( $p_repo ) . '/commits/' . $p_changeset->revision; + } + + public function url_file( $p_repo, $p_changeset, $p_file ) { + if( $p_file->action === SourceFile::DELETED ) { + return ''; + } + return $this->url_base( $p_repo ) . '/browse/' . $p_file->filename + . '?at=' . $p_changeset->revision; + } + + public function url_diff( $p_repo, $p_changeset, $p_file ) { + if( $p_file->action === SourceFile::DELETED ) { + return ''; + } + return $this->url_base( $p_repo ) . '/diff/' . $p_file->filename + . '?at=' . $p_changeset->revision; + } + + // ----------------------------------------------------------------------- + // Manage page actions + // ----------------------------------------------------------------------- + + public function show_manage_actions( $p_repo ) { + static $s_script_loaded = false; + + $t_url = plugin_page( 'test_connection', true, 'SourceBitbucketDC' ) . '&id=' . $p_repo->id; +?> + +'; + } + } + + /** + * Test connectivity and authentication against the Bitbucket DC API. + * Returns an array with 'ok' (bool) and 'message' (string). + */ + public function test_connection( $p_repo ) { + $t_url = $this->api_base( $p_repo ); + $t_token = $p_repo->info['dc_token'] ?? ''; + + $t_ch = curl_init( $t_url ); + curl_setopt_array( $t_ch, array( + CURLOPT_HTTPHEADER => array( + 'Authorization: Bearer ' . $t_token, + 'Accept: application/json', + ), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_USERAGENT => 'mantisbt/' . MANTIS_VERSION, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_TIMEOUT => 10, + ) ); + $t_data = curl_exec( $t_ch ); + $t_http_code = curl_getinfo( $t_ch, CURLINFO_HTTP_CODE ); + $t_curl_err = curl_error( $t_ch ); + curl_close( $t_ch ); + + if( $t_data === false ) { + return array( 'ok' => false, 'message' => 'cURL error: ' . $t_curl_err ); + } + if( $t_http_code === 401 ) { + return array( 'ok' => false, 'message' => 'Authentication failed (HTTP 401) — check your access token.' ); + } + if( $t_http_code === 404 ) { + return array( 'ok' => false, 'message' => 'Repository not found (HTTP 404) — check project key and repo slug.' ); + } + if( $t_http_code >= 400 ) { + return array( 'ok' => false, 'message' => 'HTTP ' . $t_http_code . ' from Bitbucket DC API.' ); + } + + $t_json = json_decode( $t_data ); + $t_name = $t_json->name ?? '(unknown)'; + return array( 'ok' => true, 'message' => 'Connected successfully to repository: ' . $t_name ); + } + + // ----------------------------------------------------------------------- + // Repository form + // ----------------------------------------------------------------------- + + public function update_repo_form( $p_repo ) { + $t_dc_project_key = $p_repo->info['dc_project_key'] ?? ''; + $t_dc_repo_slug = $p_repo->info['dc_repo_slug'] ?? ''; + $t_dc_token = $p_repo->info['dc_token'] ?? ''; + $t_master_branch = $p_repo->info['master_branch'] ?? $this->get_default_primary_branches(); + +?> + + + + + + +
+ + + + + + + + +
+ + + + + + + + +
+ + + + + + + + +
+ + +validate_branch_list( $f_master_branch ); + + $p_repo->info['dc_project_key'] = strtoupper( gpc_get_string( 'dc_project_key' ) ); + $p_repo->info['dc_repo_slug'] = gpc_get_string( 'dc_repo_slug' ); + $p_repo->info['dc_token'] = gpc_get_string( 'dc_token' ); + $p_repo->info['master_branch'] = $f_master_branch; + + return $p_repo; + } + + // ----------------------------------------------------------------------- + // HTTP / API helpers + // ----------------------------------------------------------------------- + + /** + * Fetch a URL using the configured Bearer token. + * Returns the raw response body, or triggers a plugin error on failure. + */ + private function url_get( $p_repo, $p_url ) { + $t_token = $p_repo->info['dc_token'] ?? ''; + + if( function_exists( 'curl_init' ) ) { + $t_curl_version = curl_version(); + $t_ch = curl_init( $p_url ); + curl_setopt_array( $t_ch, array( + CURLOPT_HTTPHEADER => array( + 'Authorization: Bearer ' . $t_token, + 'Accept: application/json', + ), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_USERAGENT => 'mantisbt/' . MANTIS_VERSION + . ' php-curl/' . $t_curl_version['version'], + CURLOPT_SSL_VERIFYPEER => true, + ) ); + $t_data = curl_exec( $t_ch ); + $t_http_code = curl_getinfo( $t_ch, CURLINFO_HTTP_CODE ); + curl_close( $t_ch ); + + if( $t_data === false ) { + log_event( LOG_PLUGIN, '[SourceBitbucketDC] cURL error fetching ' . $p_url ); + error_parameters( 'cURL error fetching ' . $p_url ); + plugin_error( self::ERROR_BITBUCKET_DC_API ); + } + if( $t_http_code >= 400 ) { + log_event( LOG_PLUGIN, '[SourceBitbucketDC] HTTP ' . $t_http_code . ' from ' . $p_url ); + error_parameters( "HTTP $t_http_code from Bitbucket DC API" ); + plugin_error( self::ERROR_BITBUCKET_DC_API ); + } + return $t_data; + } + + # Fallback: shell curl + $t_url_safe = escapeshellarg( $p_url ); + $t_auth_header = escapeshellarg( 'Authorization: Bearer ' . $t_token ); + return shell_exec( "curl -s -H $t_auth_header $t_url_safe" ); + } + + /** + * Fetch and JSON-decode a Bitbucket DC API URL. + */ + private function api_json( $p_repo, $p_url ) { + $t_data = $this->url_get( $p_repo, $p_url ); + $t_json = json_decode( $t_data ); + if( json_last_error() !== JSON_ERROR_NONE ) { + error_parameters( substr( $t_data, 0, 512 ) ); + plugin_error( self::ERROR_BITBUCKET_DC_API ); + } + return $t_json; + } + + /** + * Fetch all pages from a Bitbucket DC paginated API endpoint. + * + * DC pagination uses isLastPage + nextPageStart. + * + * @param SourceRepo $p_repo + * @param string $p_url Base endpoint URL (no pagination params) + * @param int $p_limit Page size + * @return array Flat array of all 'values' items + */ + private function api_paged( $p_repo, $p_url, $p_limit = 100 ) { + $t_values = array(); + $t_start = 0; + $t_separator = strpos( $p_url, '?' ) !== false ? '&' : '?'; + + do { + $t_page_url = $p_url . $t_separator . 'limit=' . $p_limit . '&start=' . $t_start; + $t_json = $this->api_json( $p_repo, $t_page_url ); + + if( isset( $t_json->values ) ) { + foreach( $t_json->values as $t_item ) { + $t_values[] = $t_item; + } + } + + $t_is_last = $t_json->isLastPage ?? true; + $t_start = $t_json->nextPageStart ?? 0; + } while( !$t_is_last ); + + return $t_values; + } + + // ----------------------------------------------------------------------- + // Webhook handling + // ----------------------------------------------------------------------- + + /** + * Intercept an incoming Bitbucket DC webhook (repo:refs_changed). + * + * Bitbucket DC sends a JSON body; we match the repository by project key + * and repo slug. If matched, returns array('repo' => ..., 'data' => ...) + * for checkin.php to pass to commit(). + */ + public function precommit() { + # Only handle DC webhooks (JSON body, no 'data' POST field) + $t_event = $_SERVER['HTTP_X_EVENT_KEY'] ?? ''; + if( $t_event !== '' && $t_event !== 'repo:refs_changed' ) { + return null; + } + + $t_input = file_get_contents( 'php://input' ); + if( empty( $t_input ) ) { + return null; + } + + $t_json = json_decode( $t_input ); + if( json_last_error() !== JSON_ERROR_NONE ) { + return null; + } + + # Validate it looks like a DC push payload + if( !isset( $t_json->changes ) || !isset( $t_json->repository ) ) { + return null; + } + + $t_repo_slug = $t_json->repository->slug ?? null; + $t_project_key = $t_json->repository->project->key ?? null; + if( is_null( $t_repo_slug ) || is_null( $t_project_key ) ) { + return null; + } + + log_event( LOG_PLUGIN, '[SourceBitbucketDC] Webhook received for ' . $t_project_key . '/' . $t_repo_slug ); + + # Find a MantisBT repository matching the webhook's project + slug + foreach( SourceRepo::load_all() as $t_repo ) { + if( $t_repo->type !== $this->type ) { + continue; + } + $t_cfg_slug = $t_repo->info['dc_repo_slug'] ?? ''; + $t_cfg_project = $t_repo->info['dc_project_key'] ?? ''; + if( strcasecmp( $t_cfg_slug, $t_repo_slug ) === 0 + && strcasecmp( $t_cfg_project, $t_project_key ) === 0 ) { + + plugin_push_current( 'Source' ); + $t_secret = plugin_config_get( 'api_key', '' ); + plugin_pop_current(); + + if( $t_secret !== '' ) { + $t_sig_header = $_SERVER['HTTP_X_HUB_SIGNATURE'] ?? ''; + $t_expected = 'sha256=' . hash_hmac( 'sha256', $t_input, $t_secret ); + if( !hash_equals( $t_expected, $t_sig_header ) ) { + log_event( LOG_PLUGIN, '[SourceBitbucketDC] Invalid webhook signature for repo: ' . $t_repo->name ); + http_response_code( 403 ); + die( 'Invalid webhook signature' ); + } + } + + log_event( LOG_PLUGIN, '[SourceBitbucketDC] Matched repo: ' . $t_repo->name ); + return array( 'repo' => $t_repo, 'data' => $t_json ); + } + } + + log_event( LOG_PLUGIN, '[SourceBitbucketDC] No matching repo found for ' . $t_project_key . '/' . $t_repo_slug ); + return null; + } + + /** + * Process a webhook payload: import new commits for each changed branch. + * + * @param SourceRepo $p_repo + * @param object $p_data Decoded webhook JSON + * @return SourceChangeset[] + */ + public function commit( $p_repo, $p_data ) { + $t_changesets = array(); + + $t_master_list = array_map( 'trim', explode( ',', $p_repo->info['master_branch'] ?? 'master' ) ); + $t_use_all = in_array( '*', $t_master_list ); + + # Build a hash-indexed lookup of commits included in the webhook payload + $t_payload_commits = array(); + if( isset( $p_data->commits ) && is_array( $p_data->commits ) ) { + foreach( $p_data->commits as $t_c ) { + $t_payload_commits[$t_c->id] = $t_c; + } + } + + foreach( $p_data->changes as $t_change ) { + # Skip non-branch refs and deletions + if( ( $t_change->ref->type ?? '' ) !== 'BRANCH' ) { + continue; + } + if( ( $t_change->type ?? '' ) === 'DELETE' ) { + continue; + } + + $t_branch = $t_change->ref->displayId; + + if( !$t_use_all && !$this->branch_matches( $t_branch, $t_master_list ) ) { + log_event( LOG_PLUGIN, '[SourceBitbucketDC] Skipping branch (filtered): ' . $t_branch ); + continue; + } + + $t_from_hash = $t_change->fromHash ?? null; + $t_to_hash = $t_change->toHash ?? null; + + if( is_null( $t_to_hash ) ) { + continue; + } + + # Try to resolve the full commit range from the payload before hitting the API + $t_range = $this->resolve_range_from_payload( $t_payload_commits, $t_from_hash, $t_to_hash ); + + if( $t_range !== null ) { + log_event( LOG_PLUGIN, '[SourceBitbucketDC] Processing branch ' . $t_branch . ' from webhook payload (' . count( $t_range ) . ' commits)' ); + $t_changesets = array_merge( + $t_changesets, + $this->process_commits( $p_repo, $t_range, $t_branch ) + ); + } else { + log_event( LOG_PLUGIN, '[SourceBitbucketDC] Processing branch ' . $t_branch . ' via API (payload insufficient)' ); + $t_changesets = array_merge( + $t_changesets, + $this->import_commits_range( $p_repo, $t_branch, $t_from_hash, $t_to_hash ) + ); + } + } + + log_event( LOG_PLUGIN, '[SourceBitbucketDC] Webhook processed ' . count( $t_changesets ) . ' new changesets for repo: ' . $p_repo->name ); + return $t_changesets; + } + + /** + * Try to resolve commits in range (fromHash, toHash] using only the + * payload commits. Walks the parent chain from toHash back to fromHash. + * Returns an array of commit objects on success, or null if the payload + * does not cover the full range and an API call is needed instead. + * + * @param array $p_payload_commits Hash-indexed payload commits + * @param string|null $p_from_hash Exclusive lower bound + * @param string $p_to_hash Inclusive upper bound + * @return array|null + */ + private function resolve_range_from_payload( $p_payload_commits, $p_from_hash, $p_to_hash ) { + if( empty( $p_payload_commits ) || !isset( $p_payload_commits[$p_to_hash] ) ) { + return null; + } + + $t_zero_hash = '0000000000000000000000000000000000000000'; + $t_result = array(); + $t_current = $p_to_hash; + + while( true ) { + if( $t_current === $p_from_hash || $t_current === $t_zero_hash ) { + break; + } + if( !isset( $p_payload_commits[$t_current] ) ) { + return null; + } + $t_commit = $p_payload_commits[$t_current]; + $t_result[] = $t_commit; + $t_current = $t_commit->parents[0]->id ?? null; + if( is_null( $t_current ) ) { + break; + } + } + + return $t_result; + } + + // ----------------------------------------------------------------------- + // Import + // ----------------------------------------------------------------------- + + /** + * Import commits in the range (fromHash, toHash] for a branch. + * + * @param SourceRepo $p_repo + * @param string $p_branch + * @param string|null $p_from_hash Exclusive lower bound (null = initial push) + * @param string $p_to_hash Inclusive upper bound + * @return SourceChangeset[] + */ + private function import_commits_range( $p_repo, $p_branch, $p_from_hash, $p_to_hash ) { + $t_url = $this->api_base( $p_repo ) . '/commits?until=' . urlencode( $p_to_hash ); + if( !is_null( $p_from_hash ) && $p_from_hash !== '0000000000000000000000000000000000000000' ) { + $t_url .= '&since=' . urlencode( $p_from_hash ); + } + $t_commits = $this->api_paged( $p_repo, $t_url ); + return $this->process_commits( $p_repo, $t_commits, $p_branch ); + } + + /** + * Full import: import all commits from all configured branches. + * + * @param SourceRepo $p_repo + * @return SourceChangeset[] + */ + public function import_full( $p_repo ) { + log_event( LOG_PLUGIN, '[SourceBitbucketDC] Full import started for repo: ' . $p_repo->name ); + + $t_branches_url = $this->api_base( $p_repo ) . '/branches'; + $t_branches = $this->api_paged( $p_repo, $t_branches_url ); + + $t_master_list = array_map( 'trim', explode( ',', $p_repo->info['master_branch'] ?? 'master' ) ); + $t_use_all = in_array( '*', $t_master_list ); + + $t_changesets = array(); + foreach( $t_branches as $t_branch ) { + $t_branch_name = $t_branch->displayId; + if( !$t_use_all && !$this->branch_matches( $t_branch_name, $t_master_list ) ) { + log_event( LOG_PLUGIN, '[SourceBitbucketDC] Skipping branch (filtered): ' . $t_branch_name ); + continue; + } + + log_event( LOG_PLUGIN, '[SourceBitbucketDC] Importing branch: ' . $t_branch_name ); + $t_url = $this->api_base( $p_repo ) . '/commits?until=' + . urlencode( $t_branch->latestCommit ); + $t_commits = $this->api_paged( $p_repo, $t_url ); + $t_changesets = array_merge( + $t_changesets, + $this->process_commits( $p_repo, $t_commits, $t_branch_name ) + ); + } + + log_event( LOG_PLUGIN, '[SourceBitbucketDC] Full import complete for repo: ' . $p_repo->name . ' — ' . count( $t_changesets ) . ' new changesets' ); + return $t_changesets; + } + + protected function validate_branch_list( $p_list ) { + foreach( array_map( 'trim', explode( ',', $p_list ) ) as $t_pattern ) { + if( $t_pattern === '*' || strpbrk( $t_pattern, '*?' ) !== false ) { + continue; + } + $this->ensure_branch_valid( $t_pattern ); + } + } + + private function branch_matches( $p_branch, $p_patterns ) { + foreach( $p_patterns as $t_pattern ) { + if( fnmatch( $t_pattern, $p_branch ) ) { + return true; + } + } + return false; + } + + public function import_latest( $p_repo ) { + return $this->import_full( $p_repo ); + } + + // ----------------------------------------------------------------------- + // Commit processing + // ----------------------------------------------------------------------- + + /** + * Convert an array of DC API commit objects into saved SourceChangesets. + * Skips commits already in the database. + * + * @param SourceRepo $p_repo + * @param array $p_commits DC API commit objects + * @param string $p_branch + * @return SourceChangeset[] + */ + private function process_commits( $p_repo, $p_commits, $p_branch ) { + $t_changesets = array(); + foreach( $p_commits as $t_commit ) { + if( SourceChangeset::exists( $p_repo->id, $t_commit->id ) ) { + log_event( LOG_PLUGIN, '[SourceBitbucketDC] Skipping existing commit: ' . substr( $t_commit->id, 0, 8 ) ); + continue; + } + $t_changeset = $this->json_commit_to_changeset( $p_repo, $t_commit, $p_branch ); + if( !is_null( $t_changeset ) ) { + $t_changeset->save(); + log_event( LOG_PLUGIN, '[SourceBitbucketDC] Saved commit: ' . substr( $t_commit->id, 0, 8 ) . ' on ' . $p_branch . ' by ' . ( $t_commit->author->name ?? 'unknown' ) ); + $t_changesets[] = $t_changeset; + } + } + return $t_changesets; + } + + /** + * Convert a single DC API commit object into a SourceChangeset. + * Also fetches the list of changed files via a second API call. + * + * @param SourceRepo $p_repo + * @param object $p_commit DC commit JSON object + * @param string $p_branch + * @return SourceChangeset + */ + private function json_commit_to_changeset( $p_repo, $p_commit, $p_branch ) { + $t_revision = $p_commit->id; + $t_author = $p_commit->author->name ?? 'unknown'; + $t_author_email = $p_commit->author->emailAddress ?? ''; + # DC timestamps are Unix milliseconds + $t_timestamp = date( 'Y-m-d H:i:s', intval( $p_commit->authorTimestamp / 1000 ) ); + $t_message = $p_commit->message ?? ''; + $t_parent = $p_commit->parents[0]->id ?? ''; + + $t_changeset = new SourceChangeset( + $p_repo->id, $t_revision, $p_branch, $t_timestamp, + $t_author, $t_message + ); + $t_changeset->author_email = $t_author_email; + $t_changeset->parent = $t_parent; + + # Fetch files changed in this commit + $t_changes_url = $this->api_base( $p_repo ) . '/commits/' . $t_revision . '/changes'; + $t_file_items = $this->api_paged( $p_repo, $t_changes_url ); + + foreach( $t_file_items as $t_item ) { + $t_filename = $t_item->path->toString ?? ''; + if( $t_filename === '' ) { + continue; + } + + switch( $t_item->type ?? 'MODIFY' ) { + case 'ADD': $t_action = SourceFile::ADDED; break; + case 'DELETE': $t_action = SourceFile::DELETED; break; + case 'RENAME': $t_action = SourceFile::RENAMED; break; + case 'COPY': $t_action = SourceFile::ADDED; break; + default: $t_action = SourceFile::MODIFIED; break; + } + + $t_changeset->files[] = new SourceFile( $t_changeset->id, '', $t_filename, $t_action ); + } + + return $t_changeset; + } +} diff --git a/SourceBitbucketDC/files/test_connection.js b/SourceBitbucketDC/files/test_connection.js new file mode 100644 index 000000000..0a08e6c32 --- /dev/null +++ b/SourceBitbucketDC/files/test_connection.js @@ -0,0 +1,12 @@ +document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('.bbdc-test-connection').forEach(function(btn) { + btn.addEventListener('click', function() { + btn.disabled = true; + fetch(btn.dataset.url) + .then(function(r) { return r.json(); }) + .then(function(d) { alert(d.message); }) + .catch(function() { alert('Request failed'); }) + .finally(function() { btn.disabled = false; }); + }); + }); +}); diff --git a/SourceBitbucketDC/lang/strings_english.txt b/SourceBitbucketDC/lang/strings_english.txt new file mode 100644 index 000000000..9b937bd59 --- /dev/null +++ b/SourceBitbucketDC/lang/strings_english.txt @@ -0,0 +1,25 @@ +test_connection( $t_repo ); + +header( 'Content-Type: application/json' ); +echo json_encode( $t_result ); diff --git a/SourceSVN/SourceSVN.php b/SourceSVN/SourceSVN.php index 8b36bbf2b..f338618e8 100644 --- a/SourceSVN/SourceSVN.php +++ b/SourceSVN/SourceSVN.php @@ -315,7 +315,7 @@ public function import_full( $p_repo ) { # create parser $t_svninfo_parsed_xml = new SimpleXMLElement($t_svninfo_xml); - $t_max_rev = (int) $t_svninfo_parsed_xml->entry->commit['revision']; + $t_max_rev = (integer) $t_svninfo_parsed_xml->entry->commit['revision']; # this is required because invalid revision number render invalid xml output for svn log if($t_rev > $t_max_rev) {