Replace deprecated API in Bitbucket Cloud importer

Overview

Atlassian has announced the deprecation of cross-workspace Bitbucket Cloud REST API endpoints, with removal scheduled for 2026-02-27 (19 days from now).

⚠️ REMOVAL DATE: February 27, 2026

GitLab's Bitbucket Cloud importer currently uses one of the deprecated endpoints and must be updated to use the new workspace-scoped alternatives before the deadline.

Atlassian Announcements

Impact Assessment

Deprecated Endpoints Being Removed

Endpoint Replacement GitLab Usage
GET /2.0/repositories GET /2.0/repositories/{workspace} + GET /2.0/user/workspaces USED in lib/bitbucket/client.rb:117
GET /2.0/workspaces GET /2.0/user/workspaces Not used

What will break:

  • Users will be unable to see their Bitbucket Cloud repositories in the GitLab import UI
  • The repository listing step during Bitbucket Cloud import will fail

What will continue working:

  • /2.0/workspaces/{workspace}/members endpoint (used for user mapping) is NOT deprecated
  • All other import functionality (issues, pull requests, etc.) uses repository-scoped endpoints that are unaffected

Affected Code

Primary file:

  • lib/bitbucket/client.rb - repos() method (lines 117-122)

Caller:

  • app/controllers/import/bitbucket_controller.rb - bitbucket_repos method (lines 154-158)

Current implementation:

def repos(filter: nil, limit: nil, after_cursor: nil)
  path = "/repositories?role=member&sort=created_on"
  path += "&q=name~\"#{filter}\"" if filter

  get_collection(path, :repo, page_number: nil, limit: limit, after_cursor: after_cursor)
end

Suggested Implementation Plan

Core Changes

1. Add Workspace Representation

Click to expand

Create lib/bitbucket/representation/workspace.rb:

# frozen_string_literal: true

module Bitbucket
  module Representation
    class Workspace < Representation::Base
      def slug
        raw['slug']
      end
      
      def name
        raw['name']
      end
      
      def uuid
        raw['uuid']
      end
    end
  end
end

2. Create Multi-Workspace Collection Wrapper

Create lib/bitbucket/multi_workspace_collection.rb:

# frozen_string_literal: true

module Bitbucket
  class MultiWorkspaceCollection
    attr_reader :items
    
    def initialize(client, filter:, limit:, after_cursor:)
      @items = fetch_all_repos(client, filter, limit, after_cursor)
      @has_more = false # Simplified: no cross-workspace pagination initially
    end
    
    def page_info
      {
        has_next_page: @has_more,
        start_cursor: nil,
        end_cursor: nil
      }
    end
    
    def to_a
      @items
    end
    
    private
    
    def fetch_all_repos(client, filter, limit, after_cursor)
      workspaces = client.send(:fetch_workspaces).to_a
      all_repos = []
      
      workspaces.each do |workspace|
        repos = client.send(:fetch_workspace_repos, workspace.slug, filter, limit, after_cursor).to_a
        all_repos.concat(repos)
        break if limit && all_repos.size >= limit
      end
      
      all_repos.first(limit || all_repos.size)
    end
  end
end

3. Update Bitbucket Client

Modify lib/bitbucket/client.rb:

# Update existing method
def repos(filter: nil, limit: nil, after_cursor: nil)
  MultiWorkspaceCollection.new(self, filter: filter, limit: limit, after_cursor: after_cursor)
end

# Add new private methods
private

def fetch_workspaces
  path = "/user/workspaces"
  get_collection(path, :workspace, page_number: nil, limit: nil)
end

def fetch_workspace_repos(workspace_slug, filter, limit, after_cursor)
  path = "/repositories/#{workspace_slug}?sort=created_on"
  path += "&q=name~\"#{filter}\"" if filter
  get_collection(path, :repo, page_number: nil, limit: limit, after_cursor: after_cursor)
end

Testing

  1. Add unit tests:

    • spec/lib/bitbucket/representation/workspace_spec.rb
    • spec/lib/bitbucket/multi_workspace_collection_spec.rb
    • Update spec/lib/bitbucket/client_spec.rb
  2. Add integration tests:

    • Test multi-workspace repository fetching
    • Test filtering across workspaces
    • Test pagination behavior
  3. Manual testing:

    • Test import flow with multiple workspaces
    • Test with single workspace
    • Test with no accessible workspaces

Known Limitations & Trade-offs

Pagination Complexity

The new implementation aggregates repositories from multiple workspaces, which changes pagination behavior:

  • Before: Single API call with cursor-based pagination
  • After: Multiple API calls (1 for workspaces + N for each workspace's repos)

Initial implementation: Simplified pagination (fetch all, return subset) Future enhancement: Implement proper cross-workspace cursor pagination

Performance Considerations

  • Increased API calls: N+1 pattern (1 workspace call + N repository calls)
  • Rate limiting: Higher chance of hitting Bitbucket API rate limits
  • Latency: Slower response time when users have many workspaces

Mitigation: Consider caching workspace list, implementing parallel requests in future iterations

Critical deadline: Changes must be in production by 2026-02-25 to allow for rollback if needed.

Acceptance Criteria

  • GET /2.0/repositories endpoint is no longer used
  • GET /2.0/user/workspaces endpoint is implemented
  • GET /2.0/repositories/{workspace} endpoint is implemented
  • Repository listing in import UI works with multiple workspaces
  • Filtering works across all workspaces
  • Pagination maintains existing UI behavior
  • All existing tests pass
  • New tests added for workspace functionality
  • Documentation updated if needed
  • Changes deployed to production before 2026-02-27
  • Backport for GitLab 18.9 merged in time for 18.9.1
  • Assessment of whether we need to backport further
  • Bitbucket Cloud importer: lib/bitbucket/
  • Import controller: app/controllers/import/bitbucket_controller.rb
  • User importer (uses non-deprecated workspace endpoint): lib/gitlab/bitbucket_import/importers/users_importer.rb
Edited by George Koltsov