From bd5fbcfb293119aa68f724a2cf011a44edb01375 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 31 Oct 2026 07:51:43 -0700 Subject: [PATCH] feat(migrations): add repository migration from bitbucket.org (#38607) Adds migration from bitbucket.org as the Cloud variant of the Bitbucket entry, in the web UI and via `POST /repos/migrate` with `service=bitbucket`. It migrates the repository and pull requests with their comments and reviews. Bitbucket Cloud has no releases, and Atlassian removed its issues and wikis on 2026-08-20 (https://community.atlassian.com/forums/Bitbucket-articles/Announcing-sunset-of-Bitbucket-Issues-and-Wikis/ba-p/3193882). - Private repositories need an API or access token, which also raises the rate limit. - Rate-limited requests back off and retry, Bitbucket sends no `Retry-After`. - Tests replay recorded responses, set `BITBUCKET_READ_TOKEN` to re-record. Co-authored-by: silverwind --- modules/structs/repo.go | 19 +- options/locale/locale_en-US.json | 4 +- services/convert/utils.go | 2 + services/convert/utils_test.go | 2 + ...ositories%2Fatlassian%2Faui%2Fpullrequests | 37 ++ ...assian%2Faui%2Fpullrequests%2F1%2Fcomments | 35 ++ ..._%2F2.0%2Frepositories%2Fgitea%2Ftest_repo | 37 ++ ...itories%2Fgitea%2Ftest_repo%2Fpullrequests | 37 ++ services/migrations/bitbucket.go | 382 ++++++++++++++++++ services/migrations/bitbucket_test.go | 161 ++++++++ services/migrations/bitbucketserver.go | 2 +- services/migrations/gitea_uploader.go | 59 ++- services/migrations/gitea_uploader_test.go | 40 ++ services/migrations/http_client.go | 8 +- services/migrations/http_client_test.go | 7 +- templates/repo/migrate/bitbucket.tmpl | 50 +++ templates/repo/migrate/bitbucketserver.tmpl | 49 +-- templates/repo/migrate/migrate.tmpl | 6 +- templates/swagger/v1-openapi3.generated.json | 3 +- templates/swagger/v1-swagger.generated.json | 3 +- tests/integration/repo_migrate_test.go | 11 + 21 files changed, 874 insertions(+), 80 deletions(-) create mode 100644 services/migrations/_mock_data/TestBitbucketDownloadRepo/GET_%2F2.0%2Frepositories%2Fatlassian%2Faui%2Fpullrequests create mode 100644 services/migrations/_mock_data/TestBitbucketDownloadRepo/GET_%2F2.0%2Frepositories%2Fatlassian%2Faui%2Fpullrequests%2F1%2Fcomments create mode 100644 services/migrations/_mock_data/TestBitbucketDownloadRepo/GET_%2F2.0%2Frepositories%2Fgitea%2Ftest_repo create mode 100644 services/migrations/_mock_data/TestBitbucketDownloadRepo/GET_%2F2.0%2Frepositories%2Fgitea%2Ftest_repo%2Fpullrequests create mode 100644 services/migrations/bitbucket.go create mode 100644 services/migrations/bitbucket_test.go create mode 100644 templates/repo/migrate/bitbucket.tmpl diff --git a/modules/structs/repo.go b/modules/structs/repo.go index 46c62744701..7e92bebe57b 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -359,6 +359,7 @@ const ( CodeCommitService // 9 codecommit service BitbucketServerService // 10 bitbucket server (data center) service ForgejoService // 11 forgejo service + BitbucketService // 12 bitbucket.org service ) // Name represents the service type's name @@ -390,6 +391,8 @@ func (gt GitServiceType) Title() string { return "Bitbucket Server" case ForgejoService: return "Forgejo" + case BitbucketService: + return "Bitbucket" case PlainGitService: return "Git" } @@ -399,21 +402,12 @@ func (gt GitServiceType) Title() string { // Variants returns the hosting options of gt's provider, which share one entry on the migrate page func (gt GitServiceType) Variants() []GitServiceType { switch gt { - case BitbucketServerService: - return []GitServiceType{BitbucketServerService} + case BitbucketService, BitbucketServerService: + return []GitServiceType{BitbucketService, BitbucketServerService} } return nil } -// ProviderTitle returns the title of gt's entry on the migrate page -func (gt GitServiceType) ProviderTitle() string { - switch gt { - case BitbucketServerService: - return "Bitbucket" - } - return gt.Title() -} - // MigrateRepoOptions options for migrating repository's // this is used to interact with api v1 type MigrateRepoOptions struct { @@ -426,7 +420,7 @@ type MigrateRepoOptions struct { // required: true RepoName string `json:"repo_name" binding:"Required;AlphaDashDot;MaxSize(100)"` - // enum: ["git","github","gitea","gitlab","gogs","onedev","gitbucket","codebase","codecommit","bitbucketserver","forgejo"] + // enum: ["git","github","gitea","gitlab","gogs","onedev","gitbucket","codebase","codecommit","bitbucketserver","forgejo","bitbucket"] Service string `json:"service"` AuthUsername string `json:"auth_username"` AuthPassword string `json:"auth_password"` @@ -464,6 +458,7 @@ var SupportedFullGitService = []GitServiceType{ GithubService, GitlabService, GiteaService, + BitbucketService, ForgejoService, GogsService, OneDevService, diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 06fe859b86d..53153e51b19 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -1125,7 +1125,9 @@ "repo.migrate.onedev.description": "Migrate data from code.onedev.io or other OneDev instances.", "repo.migrate.codebase.description": "Migrate data from codebasehq.com.", "repo.migrate.gitbucket.description": "Migrate data from GitBucket instances.", - "repo.migrate.bitbucketserver.description": "Migrate data from Bitbucket Data Center or Server instances.", + "repo.migrate.bitbucket.description": "Migrate data from bitbucket.org or Bitbucket Data Center and Server instances.", + "repo.migrate.bitbucket.variant": "Cloud (bitbucket.org)", + "repo.migrate.bitbucket.token_desc": "An Atlassian API token or a repository or workspace access token with read access to repositories and pull requests. Required for private repositories and to raise the API rate limit.", "repo.migrate.bitbucketserver.variant": "Data Center / Server", "repo.migrate.hosting": "Hosting", "repo.migrate.bitbucketserver.token_desc": "HTTP access token. If set, it is used instead of the password above. The username defaults to the owner of the token.", diff --git a/services/convert/utils.go b/services/convert/utils.go index 59a4ff008da..3cfbb5f1bae 100644 --- a/services/convert/utils.go +++ b/services/convert/utils.go @@ -44,6 +44,8 @@ func ToGitServiceType(value string) structs.GitServiceType { return structs.BitbucketServerService case "forgejo": return structs.ForgejoService + case "bitbucket": + return structs.BitbucketService default: return structs.PlainGitService } diff --git a/services/convert/utils_test.go b/services/convert/utils_test.go index f207513ea1b..1ec23b444d2 100644 --- a/services/convert/utils_test.go +++ b/services/convert/utils_test.go @@ -42,6 +42,8 @@ func TestToGitServiceType(t *testing.T) { typ: "bitbucketserver", enum: 10, }, { typ: "forgejo", enum: 11, + }, { + typ: "bitbucket", enum: 12, }} for _, test := range tc { assert.EqualValues(t, test.enum, ToGitServiceType(test.typ)) diff --git a/services/migrations/_mock_data/TestBitbucketDownloadRepo/GET_%2F2.0%2Frepositories%2Fatlassian%2Faui%2Fpullrequests b/services/migrations/_mock_data/TestBitbucketDownloadRepo/GET_%2F2.0%2Frepositories%2Fatlassian%2Faui%2Fpullrequests new file mode 100644 index 00000000000..9c16456db8f --- /dev/null +++ b/services/migrations/_mock_data/TestBitbucketDownloadRepo/GET_%2F2.0%2Frepositories%2Fatlassian%2Faui%2Fpullrequests @@ -0,0 +1,37 @@ +Age: 0 +Atl-Request-Id: 9e13d533-e742-492f-9491-fa70d6b2eeda +Atl-Traceid: 9e13d533e742492f9491fa70d6b2eeda +Cache-Control: max-age=900 +Content-Type: application/json; charset=utf-8 +Etag: W/"d60eb01065df32f229cf7996bf21f149" +Server-Timing: cdn-upstream-layer;desc="EDGE",cdn-upstream-dns;dur=0,cdn-upstream-connect;dur=0,cdn-upstream-fbl;dur=576,atl-edge;dur=469,atl-edge-internal;dur=8,atl-edge-upstream;dur=461,atl-edge-pop;desc="aws-us-east-1",cdn-cache-miss,cdn-pop;desc="VIE50-P1",cdn-rid;desc="G_Q7MMqPH7SRDgKt6XL84kKmqanLAXMocfBKJ_CNFWkF3gHAW_8SCw==",cdn-downstream-fbl;dur=580 +Strict-Transport-Security: max-age=63072000; includeSubDomains; preload +Vary: Authorization, origin +Vary: Accept-Encoding +Via: 1.1 9127bf22c332a88edd7d5939b5870d1e.cloudfront.net (CloudFront) +X-Accepted-Oauth-Scopes: pullrequest +X-Amz-Cf-Id: G_Q7MMqPH7SRDgKt6XL84kKmqanLAXMocfBKJ_CNFWkF3gHAW_8SCw== +X-Amz-Cf-Pop: VIE50-P1 +X-Asap-Succeeded: True +X-B3-Spanid: 54f13123cfd4da4c +X-B3-Traceid: 9e13d533e742492f9491fa70d6b2eeda +X-Cache: Miss from cloudfront +X-Content-Type-Options: nosniff +X-Credential-Type: unauthenticated_identity +X-Dc-Location: Micros-3 +X-Envoy-Attempt-Count: 1 +X-Frame-Options: SAMEORIGIN +X-Render-Time: 0.43766093254089355 +X-Request-Count: 2930 +X-Served-By: 38e679aebd5f +X-Static-Version: 23055ebb54a0 +X-Usage-Input-Ops: 256 +X-Usage-Output-Ops: 0 +X-Usage-System-Time: 0.012903 +X-Usage-User-Time: 0.182562 +X-Used-Mesh: False +X-Version: 23055ebb54a0 +X-View-Name: bitbucket.apps.pullrequests.api.v20.handlers.RepositoryPullRequestsHandler +X-Xss-Protection: 1; mode=block + +{"values": [{"comment_count": 25, "task_count": 0, "type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "description": "As discussed at FERM. Will put this on master first then backport as required.", "rendered": {}, "state": "MERGED", "draft": false, "merge_commit": {"type": "commit", "hash": "108d70a532dd8b98166237b54a961958e92f8a96", "date": "2011-11-02T05:23:30+00:00", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/commit/108d70a532dd8b98166237b54a961958e92f8a96"}, "html": {"href": "https://bitbucket.org/atlassian/aui/commits/108d70a532dd8b98166237b54a961958e92f8a96"}}}, "diff_type": 1, "latest_excluded_files_version": "8d74170f-e08f-41d8-8d65-ce063fa86348", "close_source_branch": false, "closed_by": {"display_name": "Jaie Wilson", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B4035f76b-7370-4a33-86e5-bc906d201811%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/557058:5271fe35-ce1c-4d9e-bb7c-93d777bf230d/36a76baa-05bc-4e76-a42b-5f3ce2a3fd1c/128"}, "html": {"href": "https://bitbucket.org/%7B4035f76b-7370-4a33-86e5-bc906d201811%7D/"}}, "type": "user", "uuid": "{4035f76b-7370-4a33-86e5-bc906d201811}", "account_id": "557058:5271fe35-ce1c-4d9e-bb7c-93d777bf230d", "nickname": "Jaie Wilson"}, "author": {"display_name": "Ben Buchanan", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351/259de5c8-61ca-4d67-b7fc-41deb8e8bbe9/128"}, "html": {"href": "https://bitbucket.org/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D/"}}, "type": "user", "uuid": "{5881db19-c8eb-4cd0-af9b-1a5b3e378e17}", "account_id": "557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351", "nickname": "200ok"}, "reason": "", "created_on": "2011-11-02T05:10:57.655043+00:00", "updated_on": "2011-11-02T05:23:32.012180+00:00", "closed_on": "2011-11-02T05:23:31.438428+00:00", "destination": {"branch": {"name": "master"}, "commit": {"type": "commit", "hash": "3d83e9aa790ef44d2ed02eb6708bec7b28e410d3", "date": "2011-10-27T02:59:21+00:00", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/commit/3d83e9aa790ef44d2ed02eb6708bec7b28e410d3"}, "html": {"href": "https://bitbucket.org/atlassian/aui/commits/3d83e9aa790ef44d2ed02eb6708bec7b28e410d3"}}}, "repository": {"type": "repository", "full_name": "atlassian/aui", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui"}, "html": {"href": "https://bitbucket.org/atlassian/aui"}, "avatar": {"href": "https://bytebucket.org/ravatar/%7B585074de-7b60-4fd1-81ed-e0bc7fafbda5%7D?ts=86317"}}, "name": "aui", "uuid": "{585074de-7b60-4fd1-81ed-e0bc7fafbda5}"}}, "source": {"branch": {"name": "master", "links": {}}, "commit": null, "repository": null}, "reviewers": [], "participants": [{"type": "participant", "user": {"display_name": "Jason Berry", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B2a7482f5-ccf1-4c98-89f2-4b5c8434601d%7D"}, "avatar": {"href": "https://secure.gravatar.com/avatar/5664c95b41e5fc913d9c53861e6ea4c6?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FJB-6.png"}, "html": {"href": "https://bitbucket.org/%7B2a7482f5-ccf1-4c98-89f2-4b5c8434601d%7D/"}}, "type": "user", "uuid": "{2a7482f5-ccf1-4c98-89f2-4b5c8434601d}", "account_id": "557058:d42d7454-f9de-4bdf-9584-9abfc1c8102f", "nickname": "Jason Berry"}, "role": "PARTICIPANT", "approved": false, "state": null, "participated_on": "2011-11-03T02:54:51.194329+00:00"}, {"type": "participant", "user": {"display_name": "Ben Buchanan", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351/259de5c8-61ca-4d67-b7fc-41deb8e8bbe9/128"}, "html": {"href": "https://bitbucket.org/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D/"}}, "type": "user", "uuid": "{5881db19-c8eb-4cd0-af9b-1a5b3e378e17}", "account_id": "557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351", "nickname": "200ok"}, "role": "PARTICIPANT", "approved": false, "state": null, "participated_on": "2011-11-03T02:36:54.859631+00:00"}], "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}, "commits": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/commits"}, "approve": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/approve"}, "request-changes": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/request-changes"}, "diff": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/diff/atlassian/aui:108d70a532dd%0D3d83e9aa790e?from_pullrequest_id=1&exclude_files=8d74170f-e08f-41d8-8d65-ce063fa86348"}, "diffstat": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/diffstat/atlassian/aui:108d70a532dd%0D3d83e9aa790e?from_pullrequest_id=1&exclude_files=8d74170f-e08f-41d8-8d65-ce063fa86348"}, "comments": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments"}, "activity": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/activity"}, "merge": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/merge"}, "decline": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/decline"}, "statuses": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/statuses"}, "mergeability_checks": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/mergeability/checks"}}, "properties": {}, "summary": {"type": "rendered", "raw": "As discussed at FERM. Will put this on master first then backport as required.", "markup": "markdown", "html": "

As discussed at FERM. Will put this on master first then backport as required.

"}, "merge_in_progress": null, "ai_contribution": false, "queued": false, "queue_in_progress": null}], "pagelen": 1, "size": 5474, "page": 1, "next": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests?fields=%2Bvalues.participants%2C%2Bvalues.%2A.commit.date%2C%2Bvalues.merge_commit.date&page=2&pagelen=1&sort=created_on&state=OPEN&state=MERGED&state=DECLINED&state=SUPERSEDED"} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestBitbucketDownloadRepo/GET_%2F2.0%2Frepositories%2Fatlassian%2Faui%2Fpullrequests%2F1%2Fcomments b/services/migrations/_mock_data/TestBitbucketDownloadRepo/GET_%2F2.0%2Frepositories%2Fatlassian%2Faui%2Fpullrequests%2F1%2Fcomments new file mode 100644 index 00000000000..cd1f64fc8f3 --- /dev/null +++ b/services/migrations/_mock_data/TestBitbucketDownloadRepo/GET_%2F2.0%2Frepositories%2Fatlassian%2Faui%2Fpullrequests%2F1%2Fcomments @@ -0,0 +1,35 @@ +Age: 0 +Atl-Request-Id: 754633c8-d49c-413e-a9dd-b6a319700346 +Atl-Traceid: 754633c8d49c413ea9ddb6a319700346 +Cache-Control: max-age=900 +Content-Type: application/json; charset=utf-8 +Server-Timing: cdn-upstream-layer;desc="EDGE",cdn-upstream-dns;dur=0,cdn-upstream-connect;dur=0,cdn-upstream-fbl;dur=863,atl-edge;dur=757,atl-edge-internal;dur=8,atl-edge-upstream;dur=749,atl-edge-pop;desc="aws-us-east-1",cdn-cache-miss,cdn-pop;desc="VIE50-P1",cdn-rid;desc="iJzf36UVx1dY6Cd0i0iUYl5PS470YK0TolLZy8YYoTl7QCLDqcc-Lw==",cdn-downstream-fbl;dur=866 +Strict-Transport-Security: max-age=63072000; includeSubDomains; preload +Vary: Authorization, origin, Accept-Encoding +Via: 1.1 9127bf22c332a88edd7d5939b5870d1e.cloudfront.net (CloudFront) +X-Accepted-Oauth-Scopes: pullrequest +X-Amz-Cf-Id: iJzf36UVx1dY6Cd0i0iUYl5PS470YK0TolLZy8YYoTl7QCLDqcc-Lw== +X-Amz-Cf-Pop: VIE50-P1 +X-Asap-Succeeded: True +X-B3-Spanid: cec56d2b24e72a51 +X-B3-Traceid: 754633c8d49c413ea9ddb6a319700346 +X-Cache: Miss from cloudfront +X-Content-Type-Options: nosniff +X-Credential-Type: unauthenticated_identity +X-Dc-Location: Micros-3 +X-Envoy-Attempt-Count: 1 +X-Frame-Options: SAMEORIGIN +X-Render-Time: 0.7233684062957764 +X-Request-Count: 3456 +X-Served-By: 4525283b180c +X-Static-Version: 23055ebb54a0 +X-Usage-Input-Ops: 64 +X-Usage-Output-Ops: 48 +X-Usage-System-Time: 0.020775 +X-Usage-User-Time: 0.435677 +X-Used-Mesh: False +X-Version: 23055ebb54a0 +X-View-Name: bitbucket.apps.pullrequests.api.v20.handlers.PullRequestCommentsV20Handler +X-Xss-Protection: 1; mode=block + +{"values": [{"id": 752, "created_on": "2011-11-02T23:26:25.284238+00:00", "updated_on": "2011-11-02T23:26:25.301897+00:00", "content": {"type": "rendered", "raw": "I think we should leave this out of the layout css\n.aui-layout #header input[type='search'] {\n margin: 2px 0 0 0;\n padding: 0;\n}\nNot all products use the search field and it renders differently in different browsers. Probably something best left for the products to style.", "markup": "creole", "html": "

I think we should leave this out of the layout css\n.aui-layout #header input[type='search'] {\n margin: 2px 0 0 0;\n padding: 0;\n}\nNot all products use the search field and it renders differently in different browsers. Probably something best left for the products to style.

\n"}, "user": {"display_name": "Sean Curtis", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7Bdf80b26c-31cb-4dc7-826a-443fdce4b6e7%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/SC-0.png"}, "html": {"href": "https://bitbucket.org/%7Bdf80b26c-31cb-4dc7-826a-443fdce4b6e7%7D/"}}, "type": "user", "uuid": "{df80b26c-31cb-4dc7-826a-443fdce4b6e7}", "account_id": "557057:abdc4f56-58c1-44db-a647-4531f709c366", "nickname": "Sean Curtis"}, "deleted": false, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/752"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-752"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 753, "created_on": "2011-11-02T23:29:27.650347+00:00", "updated_on": "2011-11-02T23:29:27.666716+00:00", "content": {"type": "rendered", "raw": "We don't have a search in Bamboo so it didn't bother me.", "markup": "creole", "html": "

We don't have a search in Bamboo so it didn't bother me.

\n"}, "user": {"display_name": "Jason Berry", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B2a7482f5-ccf1-4c98-89f2-4b5c8434601d%7D"}, "avatar": {"href": "https://secure.gravatar.com/avatar/5664c95b41e5fc913d9c53861e6ea4c6?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FJB-6.png"}, "html": {"href": "https://bitbucket.org/%7B2a7482f5-ccf1-4c98-89f2-4b5c8434601d%7D/"}}, "type": "user", "uuid": "{2a7482f5-ccf1-4c98-89f2-4b5c8434601d}", "account_id": "557058:d42d7454-f9de-4bdf-9584-9abfc1c8102f", "nickname": "Jason Berry"}, "deleted": false, "parent": {"id": 752, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/752"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-752"}}}, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/753"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-753"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 754, "created_on": "2011-11-02T23:29:35.097456+00:00", "updated_on": "2011-11-02T23:29:35.114656+00:00", "content": {"type": "rendered", "raw": "#content > header h2 is not prefixed with .aui-layout", "markup": "creole", "html": "
  1. content > header h2 is not prefixed with .aui-layout
\n"}, "user": {"display_name": "Sean Curtis", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7Bdf80b26c-31cb-4dc7-826a-443fdce4b6e7%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/SC-0.png"}, "html": {"href": "https://bitbucket.org/%7Bdf80b26c-31cb-4dc7-826a-443fdce4b6e7%7D/"}}, "type": "user", "uuid": "{df80b26c-31cb-4dc7-826a-443fdce4b6e7}", "account_id": "557057:abdc4f56-58c1-44db-a647-4531f709c366", "nickname": "Sean Curtis"}, "deleted": false, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/754"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-754"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 755, "created_on": "2011-11-02T23:30:19.550217+00:00", "updated_on": "2011-11-02T23:46:49.915012+00:00", "content": {"type": "rendered", "raw": "I'm cool with these changes.\n\nEdit: Thought we were just commenting on the changeset, not everything else too. Adding other comments.", "markup": "creole", "html": "

I'm cool with these changes.

\n

Edit: Thought we were just commenting on the changeset, not everything else too. Adding other comments.

\n"}, "user": {"display_name": "Jason Berry", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B2a7482f5-ccf1-4c98-89f2-4b5c8434601d%7D"}, "avatar": {"href": "https://secure.gravatar.com/avatar/5664c95b41e5fc913d9c53861e6ea4c6?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FJB-6.png"}, "html": {"href": "https://bitbucket.org/%7B2a7482f5-ccf1-4c98-89f2-4b5c8434601d%7D/"}}, "type": "user", "uuid": "{2a7482f5-ccf1-4c98-89f2-4b5c8434601d}", "account_id": "557058:d42d7454-f9de-4bdf-9584-9abfc1c8102f", "nickname": "Jason Berry"}, "deleted": false, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/755"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-755"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 756, "created_on": "2011-11-02T23:30:32.800786+00:00", "updated_on": "2011-11-02T23:34:46.589784+00:00", "content": {"type": "rendered", "raw": "Actually probably best to remove lines 69-80 of layout unless we want to push the content-body/container pattern into aui", "markup": "creole", "html": "

Actually probably best to remove lines 69-80 of layout unless we want to push the content-body/container pattern into aui

\n"}, "user": {"display_name": "Sean Curtis", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7Bdf80b26c-31cb-4dc7-826a-443fdce4b6e7%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/SC-0.png"}, "html": {"href": "https://bitbucket.org/%7Bdf80b26c-31cb-4dc7-826a-443fdce4b6e7%7D/"}}, "type": "user", "uuid": "{df80b26c-31cb-4dc7-826a-443fdce4b6e7}", "account_id": "557057:abdc4f56-58c1-44db-a647-4531f709c366", "nickname": "Sean Curtis"}, "deleted": false, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/756"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-756"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 757, "created_on": "2011-11-02T23:32:32.578051+00:00", "updated_on": "2011-11-02T23:32:32.603691+00:00", "content": {"type": "rendered", "raw": "Good catch.", "markup": "creole", "html": "

Good catch.

\n"}, "user": {"display_name": "Jason Berry", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B2a7482f5-ccf1-4c98-89f2-4b5c8434601d%7D"}, "avatar": {"href": "https://secure.gravatar.com/avatar/5664c95b41e5fc913d9c53861e6ea4c6?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FJB-6.png"}, "html": {"href": "https://bitbucket.org/%7B2a7482f5-ccf1-4c98-89f2-4b5c8434601d%7D/"}}, "type": "user", "uuid": "{2a7482f5-ccf1-4c98-89f2-4b5c8434601d}", "account_id": "557058:d42d7454-f9de-4bdf-9584-9abfc1c8102f", "nickname": "Jason Berry"}, "deleted": false, "parent": {"id": 754, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/754"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-754"}}}, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/757"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-757"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 758, "created_on": "2011-11-02T23:32:59.736009+00:00", "updated_on": "2011-11-02T23:32:59.763220+00:00", "content": {"type": "rendered", "raw": "Is that a JIRA-only thing?", "markup": "creole", "html": "

Is that a JIRA-only thing?

\n"}, "user": {"display_name": "Jason Berry", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B2a7482f5-ccf1-4c98-89f2-4b5c8434601d%7D"}, "avatar": {"href": "https://secure.gravatar.com/avatar/5664c95b41e5fc913d9c53861e6ea4c6?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FJB-6.png"}, "html": {"href": "https://bitbucket.org/%7B2a7482f5-ccf1-4c98-89f2-4b5c8434601d%7D/"}}, "type": "user", "uuid": "{2a7482f5-ccf1-4c98-89f2-4b5c8434601d}", "account_id": "557058:d42d7454-f9de-4bdf-9584-9abfc1c8102f", "nickname": "Jason Berry"}, "deleted": false, "parent": {"id": 756, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/756"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-756"}}}, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/758"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-758"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 759, "created_on": "2011-11-02T23:34:20.820273+00:00", "updated_on": "2011-11-02T23:34:38.887386+00:00", "content": {"type": "rendered", "raw": "Add line breaks to the comma-separated selectors on line 200 of layout", "markup": "creole", "html": "

Add line breaks to the comma-separated selectors on line 200 of layout

\n"}, "user": {"display_name": "Sean Curtis", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7Bdf80b26c-31cb-4dc7-826a-443fdce4b6e7%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/SC-0.png"}, "html": {"href": "https://bitbucket.org/%7Bdf80b26c-31cb-4dc7-826a-443fdce4b6e7%7D/"}}, "type": "user", "uuid": "{df80b26c-31cb-4dc7-826a-443fdce4b6e7}", "account_id": "557057:abdc4f56-58c1-44db-a647-4531f709c366", "nickname": "Sean Curtis"}, "deleted": false, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/759"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-759"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 760, "created_on": "2011-11-02T23:36:34.206229+00:00", "updated_on": "2011-11-02T23:40:28.289157+00:00", "content": {"type": "rendered", "raw": "Selectors on the following lines can be merged:\n213/248\n217/252\n227/273\n231/277\n221/235/256/281", "markup": "creole", "html": "

Selectors on the following lines can be merged:\n213/248\n217/252\n227/273\n231/277\n221/235/256/281

\n"}, "user": {"display_name": "Sean Curtis", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7Bdf80b26c-31cb-4dc7-826a-443fdce4b6e7%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/SC-0.png"}, "html": {"href": "https://bitbucket.org/%7Bdf80b26c-31cb-4dc7-826a-443fdce4b6e7%7D/"}}, "type": "user", "uuid": "{df80b26c-31cb-4dc7-826a-443fdce4b6e7}", "account_id": "557057:abdc4f56-58c1-44db-a647-4531f709c366", "nickname": "Sean Curtis"}, "deleted": false, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/760"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-760"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 761, "created_on": "2011-11-02T23:42:38.533879+00:00", "updated_on": "2011-11-02T23:42:38.563418+00:00", "content": {"type": "rendered", "raw": "Line 351 is unnecessary - already included above on line 171", "markup": "creole", "html": "

Line 351 is unnecessary - already included above on line 171

\n"}, "user": {"display_name": "Sean Curtis", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7Bdf80b26c-31cb-4dc7-826a-443fdce4b6e7%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/SC-0.png"}, "html": {"href": "https://bitbucket.org/%7Bdf80b26c-31cb-4dc7-826a-443fdce4b6e7%7D/"}}, "type": "user", "uuid": "{df80b26c-31cb-4dc7-826a-443fdce4b6e7}", "account_id": "557057:abdc4f56-58c1-44db-a647-4531f709c366", "nickname": "Sean Curtis"}, "deleted": false, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/761"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-761"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 762, "created_on": "2011-11-02T23:43:46.838639+00:00", "updated_on": "2011-11-02T23:45:04.824380+00:00", "content": {"type": "rendered", "raw": "While we're commenting wildly, here are some things I needed to add/override in Bamboo:\n{{{\n#!css\n\n.aui-theme-default #header .local .secondary > ul > li > a:hover {\n background: #fff;\n color: #333;\n}\n.aui-theme-default #header .local .secondary > ul > li.selected,\n.aui-theme-default #header .local .secondary > ul > li.selected a {\n background: #f0f0f0;\n color: #333;\n}\n.aui-theme-default #header .aui-dropdown {\n min-width: 80px;\n}\n.aui-theme-default #header .aui-dropdown .item-link {\n color: #333;\n display: block;\n padding: 0.4em 8px;\n text-decoration: none;\n}\n.aui-theme-default #header .global .secondary > ul {\n padding-top: 2px;\n}\n.aui-theme-default #header .global .secondary > ul > li > .aui-dd-trigger {\n cursor: pointer;\n display: block;\n padding: 4px;\n white-space: nowrap;\n}\n.aui-theme-default #header .global .secondary > ul > li.active > .aui-dd-trigger,\n.aui-theme-default #header .global .secondary > ul > li > .aui-dd-trigger:hover {\n background: #326ca6;\n color: #fff;\n}\n\n}}}", "markup": "creole", "html": "

While we're commenting wildly, here are some things I needed to add/override in Bamboo:

\n
.aui-theme-default #header .local .secondary > ul > li > a:hover {\n    background: #fff;\n    color: #333;\n}\n.aui-theme-default #header .local .secondary > ul > li.selected,\n.aui-theme-default #header .local .secondary > ul > li.selected a {\n    background: #f0f0f0;\n    color: #333;\n}\n.aui-theme-default #header .aui-dropdown {\n    min-width: 80px;\n}\n.aui-theme-default #header .aui-dropdown .item-link {\n    color: #333;\n    display: block;\n    padding: 0.4em 8px;\n    text-decoration: none;\n}\n.aui-theme-default #header .global .secondary > ul {\n    padding-top: 2px;\n}\n.aui-theme-default #header .global .secondary > ul > li > .aui-dd-trigger {\n    cursor: pointer;\n    display: block;\n    padding: 4px;\n    white-space: nowrap;\n}\n.aui-theme-default #header .global .secondary > ul > li.active > .aui-dd-trigger,\n.aui-theme-default #header .global .secondary > ul > li > .aui-dd-trigger:hover {\n    background: #326ca6;\n    color: #fff;\n}\n
\n\n"}, "user": {"display_name": "Jason Berry", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B2a7482f5-ccf1-4c98-89f2-4b5c8434601d%7D"}, "avatar": {"href": "https://secure.gravatar.com/avatar/5664c95b41e5fc913d9c53861e6ea4c6?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FJB-6.png"}, "html": {"href": "https://bitbucket.org/%7B2a7482f5-ccf1-4c98-89f2-4b5c8434601d%7D/"}}, "type": "user", "uuid": "{2a7482f5-ccf1-4c98-89f2-4b5c8434601d}", "account_id": "557058:d42d7454-f9de-4bdf-9584-9abfc1c8102f", "nickname": "Jason Berry"}, "deleted": false, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/762"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-762"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 764, "created_on": "2011-11-02T23:52:07.431466+00:00", "updated_on": "2011-11-02T23:52:07.439817+00:00", "content": {"type": "rendered", "raw": "....yeah, I miss Crucible already.", "markup": "creole", "html": "

....yeah, I miss Crucible already.

\n"}, "user": {"display_name": "Ben Buchanan", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351/259de5c8-61ca-4d67-b7fc-41deb8e8bbe9/128"}, "html": {"href": "https://bitbucket.org/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D/"}}, "type": "user", "uuid": "{5881db19-c8eb-4cd0-af9b-1a5b3e378e17}", "account_id": "557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351", "nickname": "200ok"}, "deleted": false, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/764"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-764"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 765, "created_on": "2011-11-03T00:23:46.862166+00:00", "updated_on": "2011-11-03T00:23:56.624317+00:00", "content": {"type": "rendered", "raw": "That << issue 326 >> crap shouldn't be there, but I couldn't figure out how to stop bitbucket screwing with my hex colours.", "markup": "creole", "html": "

That << issue 326 >> crap shouldn't be there, but I couldn't figure out how to stop bitbucket screwing with my hex colours.

\n"}, "user": {"display_name": "Jason Berry", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B2a7482f5-ccf1-4c98-89f2-4b5c8434601d%7D"}, "avatar": {"href": "https://secure.gravatar.com/avatar/5664c95b41e5fc913d9c53861e6ea4c6?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FJB-6.png"}, "html": {"href": "https://bitbucket.org/%7B2a7482f5-ccf1-4c98-89f2-4b5c8434601d%7D/"}}, "type": "user", "uuid": "{2a7482f5-ccf1-4c98-89f2-4b5c8434601d}", "account_id": "557058:d42d7454-f9de-4bdf-9584-9abfc1c8102f", "nickname": "Jason Berry"}, "deleted": false, "parent": {"id": 762, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/762"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-762"}}}, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/765"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-765"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 766, "created_on": "2011-11-03T02:26:01.663708+00:00", "updated_on": "2011-11-03T02:26:01.673821+00:00", "content": {"type": "rendered", "raw": "It's a bit random, will remove.", "markup": "creole", "html": "

It's a bit random, will remove.

\n"}, "user": {"display_name": "Ben Buchanan", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351/259de5c8-61ca-4d67-b7fc-41deb8e8bbe9/128"}, "html": {"href": "https://bitbucket.org/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D/"}}, "type": "user", "uuid": "{5881db19-c8eb-4cd0-af9b-1a5b3e378e17}", "account_id": "557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351", "nickname": "200ok"}, "deleted": false, "parent": {"id": 752, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/752"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-752"}}}, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/766"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-766"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 767, "created_on": "2011-11-03T02:26:13.154822+00:00", "updated_on": "2011-11-03T02:26:13.163969+00:00", "content": {"type": "rendered", "raw": "fixed", "markup": "creole", "html": "

fixed

\n"}, "user": {"display_name": "Ben Buchanan", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351/259de5c8-61ca-4d67-b7fc-41deb8e8bbe9/128"}, "html": {"href": "https://bitbucket.org/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D/"}}, "type": "user", "uuid": "{5881db19-c8eb-4cd0-af9b-1a5b3e378e17}", "account_id": "557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351", "nickname": "200ok"}, "deleted": false, "parent": {"id": 754, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/754"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-754"}}}, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/767"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-767"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 768, "created_on": "2011-11-03T02:30:04.262166+00:00", "updated_on": "2011-11-03T02:30:04.272712+00:00", "content": {"type": "rendered", "raw": "Did intend to just do the changeset, but realistically the actual goal is to get this component improved for better usage.", "markup": "creole", "html": "

Did intend to just do the changeset, but realistically the actual goal is to get this component improved for better usage.

\n"}, "user": {"display_name": "Ben Buchanan", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351/259de5c8-61ca-4d67-b7fc-41deb8e8bbe9/128"}, "html": {"href": "https://bitbucket.org/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D/"}}, "type": "user", "uuid": "{5881db19-c8eb-4cd0-af9b-1a5b3e378e17}", "account_id": "557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351", "nickname": "200ok"}, "deleted": false, "parent": {"id": 755, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/755"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-755"}}}, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/768"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-768"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 769, "created_on": "2011-11-03T02:30:50.938714+00:00", "updated_on": "2011-11-03T02:30:50.947724+00:00", "content": {"type": "rendered", "raw": "yeah I'm already screwed here, decoupled comments pretty much guarantee PRs can't be reviews... :)", "markup": "creole", "html": "

yeah I'm already screwed here, decoupled comments pretty much guarantee PRs can't be reviews... :)

\n"}, "user": {"display_name": "Ben Buchanan", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351/259de5c8-61ca-4d67-b7fc-41deb8e8bbe9/128"}, "html": {"href": "https://bitbucket.org/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D/"}}, "type": "user", "uuid": "{5881db19-c8eb-4cd0-af9b-1a5b3e378e17}", "account_id": "557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351", "nickname": "200ok"}, "deleted": false, "parent": {"id": 761, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/761"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-761"}}}, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/769"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-769"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 770, "created_on": "2011-11-03T02:34:01.708172+00:00", "updated_on": "2011-11-03T02:34:01.717375+00:00", "content": {"type": "rendered", "raw": "would you prefer to update or remove these styles from aui?", "markup": "creole", "html": "

would you prefer to update or remove these styles from aui?

\n"}, "user": {"display_name": "Ben Buchanan", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351/259de5c8-61ca-4d67-b7fc-41deb8e8bbe9/128"}, "html": {"href": "https://bitbucket.org/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D/"}}, "type": "user", "uuid": "{5881db19-c8eb-4cd0-af9b-1a5b3e378e17}", "account_id": "557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351", "nickname": "200ok"}, "deleted": false, "parent": {"id": 762, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/762"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-762"}}}, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/770"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-770"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 771, "created_on": "2011-11-03T02:36:35.520198+00:00", "updated_on": "2011-11-03T02:36:35.532629+00:00", "content": {"type": "rendered", "raw": "done", "markup": "creole", "html": "

done

\n"}, "user": {"display_name": "Ben Buchanan", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351/259de5c8-61ca-4d67-b7fc-41deb8e8bbe9/128"}, "html": {"href": "https://bitbucket.org/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D/"}}, "type": "user", "uuid": "{5881db19-c8eb-4cd0-af9b-1a5b3e378e17}", "account_id": "557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351", "nickname": "200ok"}, "deleted": false, "parent": {"id": 760, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/760"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-760"}}}, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/771"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-771"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 772, "created_on": "2011-11-03T02:36:43.794399+00:00", "updated_on": "2011-11-03T02:36:43.805188+00:00", "content": {"type": "rendered", "raw": "ok done", "markup": "creole", "html": "

ok done

\n"}, "user": {"display_name": "Ben Buchanan", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351/259de5c8-61ca-4d67-b7fc-41deb8e8bbe9/128"}, "html": {"href": "https://bitbucket.org/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D/"}}, "type": "user", "uuid": "{5881db19-c8eb-4cd0-af9b-1a5b3e378e17}", "account_id": "557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351", "nickname": "200ok"}, "deleted": false, "parent": {"id": 761, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/761"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-761"}}}, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/772"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-772"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 773, "created_on": "2011-11-03T02:36:54.849374+00:00", "updated_on": "2011-11-03T02:36:54.859631+00:00", "content": {"type": "rendered", "raw": "presuming you meant hte headings, done", "markup": "creole", "html": "

presuming you meant hte headings, done

\n"}, "user": {"display_name": "Ben Buchanan", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351/259de5c8-61ca-4d67-b7fc-41deb8e8bbe9/128"}, "html": {"href": "https://bitbucket.org/%7B5881db19-c8eb-4cd0-af9b-1a5b3e378e17%7D/"}}, "type": "user", "uuid": "{5881db19-c8eb-4cd0-af9b-1a5b3e378e17}", "account_id": "557058:6af744d8-ea74-40ff-9fbb-c7af27dc5351", "nickname": "200ok"}, "deleted": false, "parent": {"id": 759, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/759"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-759"}}}, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/773"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-773"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 774, "created_on": "2011-11-03T02:54:51.178808+00:00", "updated_on": "2011-11-03T02:54:51.194329+00:00", "content": {"type": "rendered", "raw": "I'd probably like to add them (or something like it) to AUI. They should be fairly standard cross-product. The only reason they weren't in there is because JIRA has a custom dropdown.\n\nCertainly the \"#header .aui-dropdown\" and \".global .secondary\" stuff should consider being added - it's the AUI dropdown at the top-right for the user. The \".local .secondary\" stuff maybe not so much.", "markup": "creole", "html": "

I'd probably like to add them (or something like it) to AUI. They should be fairly standard cross-product. The only reason they weren't in there is because JIRA has a custom dropdown.

\n

Certainly the \"#header .aui-dropdown\" and \".global .secondary\" stuff should consider being added - it's the AUI dropdown at the top-right for the user. The \".local .secondary\" stuff maybe not so much.

\n"}, "user": {"display_name": "Jason Berry", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B2a7482f5-ccf1-4c98-89f2-4b5c8434601d%7D"}, "avatar": {"href": "https://secure.gravatar.com/avatar/5664c95b41e5fc913d9c53861e6ea4c6?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FJB-6.png"}, "html": {"href": "https://bitbucket.org/%7B2a7482f5-ccf1-4c98-89f2-4b5c8434601d%7D/"}}, "type": "user", "uuid": "{2a7482f5-ccf1-4c98-89f2-4b5c8434601d}", "account_id": "557058:d42d7454-f9de-4bdf-9584-9abfc1c8102f", "nickname": "Jason Berry"}, "deleted": false, "parent": {"id": 770, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/770"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-770"}}}, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/774"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-774"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 775, "created_on": "2011-11-03T04:21:32.272584+00:00", "updated_on": "2011-11-03T04:21:32.293398+00:00", "content": {"type": "rendered", "raw": "Well I'm happy for other products to use it but if so we'd probably have to aui-fy that pattern. It'd be a nice opt-in thing for people to use if they wanted...", "markup": "creole", "html": "

Well I'm happy for other products to use it but if so we'd probably have to aui-fy that pattern. It'd be a nice opt-in thing for people to use if they wanted...

\n"}, "user": {"display_name": "Sean Curtis", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7Bdf80b26c-31cb-4dc7-826a-443fdce4b6e7%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/SC-0.png"}, "html": {"href": "https://bitbucket.org/%7Bdf80b26c-31cb-4dc7-826a-443fdce4b6e7%7D/"}}, "type": "user", "uuid": "{df80b26c-31cb-4dc7-826a-443fdce4b6e7}", "account_id": "557057:abdc4f56-58c1-44db-a647-4531f709c366", "nickname": "Sean Curtis"}, "deleted": false, "parent": {"id": 758, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/758"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-758"}}}, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/775"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-775"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 776, "created_on": "2011-11-03T04:22:49.970685+00:00", "updated_on": "2011-11-03T04:22:49.996783+00:00", "content": {"type": "rendered", "raw": "I don't see the point of including the styles in this experimental css file if they're not aui sanctioned things. As it stands these styles will duplicate/tread on the toes of what is currently a JIRA-specific pattern.", "markup": "creole", "html": "

I don't see the point of including the styles in this experimental css file if they're not aui sanctioned things. As it stands these styles will duplicate/tread on the toes of what is currently a JIRA-specific pattern.

\n"}, "user": {"display_name": "Sean Curtis", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7Bdf80b26c-31cb-4dc7-826a-443fdce4b6e7%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/SC-0.png"}, "html": {"href": "https://bitbucket.org/%7Bdf80b26c-31cb-4dc7-826a-443fdce4b6e7%7D/"}}, "type": "user", "uuid": "{df80b26c-31cb-4dc7-826a-443fdce4b6e7}", "account_id": "557057:abdc4f56-58c1-44db-a647-4531f709c366", "nickname": "Sean Curtis"}, "deleted": false, "parent": {"id": 758, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/758"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-758"}}}, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/776"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-776"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}, {"id": 777, "created_on": "2011-11-03T04:23:26.777979+00:00", "updated_on": "2011-11-03T04:23:26.796730+00:00", "content": {"type": "rendered", "raw": "Heh sorry - should have been more explicit with which file I was commenting on. ", "markup": "creole", "html": "

Heh sorry - should have been more explicit with which file I was commenting on.

\n"}, "user": {"display_name": "Sean Curtis", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7Bdf80b26c-31cb-4dc7-826a-443fdce4b6e7%7D"}, "avatar": {"href": "https://avatar-management--avatars.us-west-2.prod.public.atl-paas.net/initials/SC-0.png"}, "html": {"href": "https://bitbucket.org/%7Bdf80b26c-31cb-4dc7-826a-443fdce4b6e7%7D/"}}, "type": "user", "uuid": "{df80b26c-31cb-4dc7-826a-443fdce4b6e7}", "account_id": "557057:abdc4f56-58c1-44db-a647-4531f709c366", "nickname": "Sean Curtis"}, "deleted": false, "parent": {"id": 769, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/769"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-769"}}}, "pending": false, "type": "pullrequest_comment", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1/comments/777"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1/_/diff#comment-777"}}, "pullrequest": {"type": "pullrequest", "id": 1, "title": "Heading size change and split typography to separate resource", "draft": false, "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/atlassian/aui/pullrequests/1"}, "html": {"href": "https://bitbucket.org/atlassian/aui/pull-requests/1"}}, "queued": false}}], "pagelen": 100, "size": 25, "page": 1} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestBitbucketDownloadRepo/GET_%2F2.0%2Frepositories%2Fgitea%2Ftest_repo b/services/migrations/_mock_data/TestBitbucketDownloadRepo/GET_%2F2.0%2Frepositories%2Fgitea%2Ftest_repo new file mode 100644 index 00000000000..d708fc08682 --- /dev/null +++ b/services/migrations/_mock_data/TestBitbucketDownloadRepo/GET_%2F2.0%2Frepositories%2Fgitea%2Ftest_repo @@ -0,0 +1,37 @@ +Age: 0 +Atl-Request-Id: 9179a08c-393b-4ef2-9b63-0202f377b154 +Atl-Traceid: 9179a08c393b4ef29b630202f377b154 +Cache-Control: max-age=900 +Content-Type: application/json; charset=utf-8 +Etag: W/"0055c235ff1216676265b36c65aa2ea4" +Server-Timing: cdn-upstream-layer;desc="EDGE",cdn-upstream-dns;dur=1,cdn-upstream-connect;dur=0,cdn-upstream-fbl;dur=218,atl-edge;dur=115,atl-edge-internal;dur=8,atl-edge-upstream;dur=107,atl-edge-pop;desc="aws-us-east-1",cdn-cache-miss,cdn-pop;desc="VIE50-P1",cdn-rid;desc="ZezuZh_FtSEXhi0VZc38GYlAyYNS_twFTfXWN25B7YXwzL80tJAUFg==",cdn-downstream-fbl;dur=222 +Strict-Transport-Security: max-age=63072000; includeSubDomains; preload +Vary: Authorization, origin +Vary: Accept-Encoding +Via: 1.1 9127bf22c332a88edd7d5939b5870d1e.cloudfront.net (CloudFront) +X-Accepted-Oauth-Scopes: repository +X-Amz-Cf-Id: ZezuZh_FtSEXhi0VZc38GYlAyYNS_twFTfXWN25B7YXwzL80tJAUFg== +X-Amz-Cf-Pop: VIE50-P1 +X-Asap-Succeeded: True +X-B3-Spanid: e5d99cd1c4db9972 +X-B3-Traceid: 9179a08c393b4ef29b630202f377b154 +X-Cache: Miss from cloudfront +X-Content-Type-Options: nosniff +X-Credential-Type: unauthenticated_identity +X-Dc-Location: Micros-3 +X-Envoy-Attempt-Count: 1 +X-Frame-Options: SAMEORIGIN +X-Render-Time: 0.08297491073608398 +X-Request-Count: 4191 +X-Served-By: b23789207f7b +X-Static-Version: 23055ebb54a0 +X-Usage-Input-Ops: 0 +X-Usage-Output-Ops: 0 +X-Usage-System-Time: 0.000423 +X-Usage-User-Time: 0.055612 +X-Used-Mesh: False +X-Version: 23055ebb54a0 +X-View-Name: bitbucket.apps.repo2.api.v20.repo.RepositoryHandler +X-Xss-Protection: 1; mode=block + +{"type": "repository", "full_name": "gitea/test_repo", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo"}, "html": {"href": "https://bitbucket.org/gitea/test_repo"}, "avatar": {"href": "https://bytebucket.org/ravatar/%7B7b764bc5-c3f3-4910-a90b-4cdc229a66b7%7D?ts=default"}, "pullrequests": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/pullrequests"}, "commits": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/commits"}, "forks": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/forks"}, "watchers": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/watchers"}, "branches": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/refs/branches"}, "tags": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/refs/tags"}, "downloads": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/downloads"}, "source": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/src"}, "clone": [{"name": "https", "href": "https://bitbucket.org/gitea/test_repo.git"}, {"name": "ssh", "href": "git@ssh.bitbucket.org:gitea/test_repo.git"}], "hooks": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/hooks"}}, "name": "test_repo", "slug": "test_repo", "description": "", "scm": "git", "website": null, "owner": {"display_name": "gitea", "links": {"self": {"href": "https://api.bitbucket.org/2.0/workspaces/%7B5e516e72-8608-4d3e-9ca4-969288daed2a%7D"}, "avatar": {"href": "https://bitbucket.org/account/gitea4/avatar/"}, "html": {"href": "https://bitbucket.org/%7B5e516e72-8608-4d3e-9ca4-969288daed2a%7D/"}}, "type": "team", "uuid": "{5e516e72-8608-4d3e-9ca4-969288daed2a}", "username": "gitea4"}, "workspace": {"type": "workspace", "uuid": "{5e516e72-8608-4d3e-9ca4-969288daed2a}", "name": "gitea", "slug": "gitea", "links": {"avatar": {"href": "https://bitbucket.org/workspaces/gitea/avatar/?ts=1791398183"}, "html": {"href": "https://bitbucket.org/gitea/"}, "self": {"href": "https://api.bitbucket.org/2.0/workspaces/gitea"}}}, "is_private": false, "project": {"type": "project", "key": "TES", "uuid": "{94fee3ab-3d66-4ba7-a36a-134aab7a1e84}", "name": "test_repo", "links": {"self": {"href": "https://api.bitbucket.org/2.0/workspaces/gitea/projects/TES"}, "html": {"href": "https://bitbucket.org/gitea/workspace/projects/TES"}, "avatar": {"href": "https://bitbucket.org/gitea/workspace/projects/TES/avatar/32?ts=1791398116"}}}, "fork_policy": "allow_forks", "created_on": "2026-10-07T18:35:17.202781+00:00", "updated_on": "2026-10-07T18:40:40.122213+00:00", "size": 3862, "language": "", "uuid": "{7b764bc5-c3f3-4910-a90b-4cdc229a66b7}", "mainbranch": {"name": "master", "type": "branch"}, "override_settings": {"default_merge_strategy": true, "branching_model": true}, "parent": null, "enforced_signed_commits": null} \ No newline at end of file diff --git a/services/migrations/_mock_data/TestBitbucketDownloadRepo/GET_%2F2.0%2Frepositories%2Fgitea%2Ftest_repo%2Fpullrequests b/services/migrations/_mock_data/TestBitbucketDownloadRepo/GET_%2F2.0%2Frepositories%2Fgitea%2Ftest_repo%2Fpullrequests new file mode 100644 index 00000000000..a402508944e --- /dev/null +++ b/services/migrations/_mock_data/TestBitbucketDownloadRepo/GET_%2F2.0%2Frepositories%2Fgitea%2Ftest_repo%2Fpullrequests @@ -0,0 +1,37 @@ +Age: 0 +Atl-Request-Id: 1ac56c02-99bf-4e23-8e43-4a6dc0ac64f2 +Atl-Traceid: 1ac56c0299bf4e238e434a6dc0ac64f2 +Cache-Control: max-age=900 +Content-Type: application/json; charset=utf-8 +Etag: W/"d1616edb0f00570d57f256a059ae2cf9" +Server-Timing: cdn-upstream-layer;desc="EDGE",cdn-upstream-dns;dur=0,cdn-upstream-connect;dur=0,cdn-upstream-fbl;dur=518,atl-edge;dur=416,atl-edge-internal;dur=8,atl-edge-upstream;dur=408,atl-edge-pop;desc="aws-us-east-1",cdn-cache-miss,cdn-pop;desc="VIE50-P1",cdn-rid;desc="wignpm5GNER8JwK555KBwCkS1LqBl5iNxmE1d4WmhjcJBL7TxNOwCg==",cdn-downstream-fbl;dur=522 +Strict-Transport-Security: max-age=63072000; includeSubDomains; preload +Vary: Authorization, origin +Vary: Accept-Encoding +Via: 1.1 9127bf22c332a88edd7d5939b5870d1e.cloudfront.net (CloudFront) +X-Accepted-Oauth-Scopes: pullrequest +X-Amz-Cf-Id: wignpm5GNER8JwK555KBwCkS1LqBl5iNxmE1d4WmhjcJBL7TxNOwCg== +X-Amz-Cf-Pop: VIE50-P1 +X-Asap-Succeeded: True +X-B3-Spanid: 0c509dea12a6a26e +X-B3-Traceid: 1ac56c0299bf4e238e434a6dc0ac64f2 +X-Cache: Miss from cloudfront +X-Content-Type-Options: nosniff +X-Credential-Type: unauthenticated_identity +X-Dc-Location: Micros-3 +X-Envoy-Attempt-Count: 1 +X-Frame-Options: SAMEORIGIN +X-Render-Time: 0.38138413429260254 +X-Request-Count: 532 +X-Served-By: e9d9f85586a3 +X-Static-Version: 23055ebb54a0 +X-Usage-Input-Ops: 24 +X-Usage-Output-Ops: 0 +X-Usage-System-Time: 0.008078 +X-Usage-User-Time: 0.140643 +X-Used-Mesh: False +X-Version: 23055ebb54a0 +X-View-Name: bitbucket.apps.pullrequests.api.v20.handlers.RepositoryPullRequestsHandler +X-Xss-Protection: 1; mode=block + +{"values": [{"comment_count": 0, "task_count": 0, "type": "pullrequest", "id": 1, "title": "Update LICENSE", "description": "do not merge this PR", "rendered": {}, "state": "OPEN", "draft": false, "merge_commit": null, "diff_type": 2, "latest_excluded_files_version": "", "close_source_branch": false, "closed_by": null, "author": {"display_name": "Lunny Xiao", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B84d46ffe-e0fa-4d3c-973f-b3ce77db43f6%7D"}, "avatar": {"href": "https://secure.gravatar.com/avatar/cfb7b37d5a38d236e451a9f8e1a29fc6?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FLX-1.png"}, "html": {"href": "https://bitbucket.org/%7B84d46ffe-e0fa-4d3c-973f-b3ce77db43f6%7D/"}}, "type": "user", "uuid": "{84d46ffe-e0fa-4d3c-973f-b3ce77db43f6}", "account_id": "712020:07927e50-b64a-4fc3-a57d-dcf6945b70e6", "nickname": "Lunny Xiao"}, "reason": "", "created_on": "2026-10-07T18:39:55.551762+00:00", "updated_on": "2026-10-07T18:40:40.122213+00:00", "closed_on": null, "destination": {"branch": {"name": "master"}, "commit": {"type": "commit", "hash": "c59c9b451acca9d106cc19d61d87afe3fbbb8b83", "date": "2019-11-28T08:53:41+00:00", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/commit/c59c9b451acca9d106cc19d61d87afe3fbbb8b83"}, "html": {"href": "https://bitbucket.org/gitea/test_repo/commits/c59c9b451acca9d106cc19d61d87afe3fbbb8b83"}}}, "repository": {"type": "repository", "full_name": "gitea/test_repo", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo"}, "html": {"href": "https://bitbucket.org/gitea/test_repo"}, "avatar": {"href": "https://bytebucket.org/ravatar/%7B7b764bc5-c3f3-4910-a90b-4cdc229a66b7%7D?ts=default"}}, "name": "test_repo", "uuid": "{7b764bc5-c3f3-4910-a90b-4cdc229a66b7}"}}, "source": {"branch": {"name": "feat/test", "links": {}}, "commit": {"type": "commit", "hash": "9f733b96b98a4175276edf6a2e1231489c3bdd23", "date": "2019-11-28T15:56:02+00:00", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/commit/9f733b96b98a4175276edf6a2e1231489c3bdd23"}, "html": {"href": "https://bitbucket.org/gitea/test_repo/commits/9f733b96b98a4175276edf6a2e1231489c3bdd23"}}}, "repository": {"type": "repository", "full_name": "gitea/test_repo", "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo"}, "html": {"href": "https://bitbucket.org/gitea/test_repo"}, "avatar": {"href": "https://bytebucket.org/ravatar/%7B7b764bc5-c3f3-4910-a90b-4cdc229a66b7%7D?ts=default"}}, "name": "test_repo", "uuid": "{7b764bc5-c3f3-4910-a90b-4cdc229a66b7}"}}, "reviewers": [], "participants": [{"type": "participant", "user": {"display_name": "Lunny Xiao", "links": {"self": {"href": "https://api.bitbucket.org/2.0/users/%7B84d46ffe-e0fa-4d3c-973f-b3ce77db43f6%7D"}, "avatar": {"href": "https://secure.gravatar.com/avatar/cfb7b37d5a38d236e451a9f8e1a29fc6?d=https%3A%2F%2Favatar-management--avatars.us-west-2.prod.public.atl-paas.net%2Finitials%2FLX-1.png"}, "html": {"href": "https://bitbucket.org/%7B84d46ffe-e0fa-4d3c-973f-b3ce77db43f6%7D/"}}, "type": "user", "uuid": "{84d46ffe-e0fa-4d3c-973f-b3ce77db43f6}", "account_id": "712020:07927e50-b64a-4fc3-a57d-dcf6945b70e6", "nickname": "Lunny Xiao"}, "role": "PARTICIPANT", "approved": true, "state": "approved", "participated_on": "2026-10-07T18:40:40.122213+00:00"}], "links": {"self": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/pullrequests/1"}, "html": {"href": "https://bitbucket.org/gitea/test_repo/pull-requests/1"}, "commits": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/pullrequests/1/commits"}, "approve": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/pullrequests/1/approve"}, "request-changes": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/pullrequests/1/request-changes"}, "diff": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/diff/gitea/test_repo:9f733b96b98a%0Dc59c9b451acc?from_pullrequest_id=1&topic=true"}, "diffstat": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/diffstat/gitea/test_repo:9f733b96b98a%0Dc59c9b451acc?from_pullrequest_id=1&topic=true"}, "comments": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/pullrequests/1/comments"}, "activity": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/pullrequests/1/activity"}, "merge": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/pullrequests/1/merge"}, "decline": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/pullrequests/1/decline"}, "statuses": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/pullrequests/1/statuses"}, "mergeability_checks": {"href": "https://api.bitbucket.org/2.0/repositories/gitea/test_repo/pullrequests/1/mergeability/checks"}}, "properties": {}, "summary": {"type": "rendered", "raw": "do not merge this PR", "markup": "markdown", "html": "

do not merge this PR

"}, "merge_in_progress": null, "ai_contribution": false, "queued": false, "queue_in_progress": null}], "pagelen": 10, "size": 1, "page": 1} \ No newline at end of file diff --git a/services/migrations/bitbucket.go b/services/migrations/bitbucket.go new file mode 100644 index 00000000000..13309609d0d --- /dev/null +++ b/services/migrations/bitbucket.go @@ -0,0 +1,382 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package migrations + +import ( + "cmp" + "context" + "fmt" + "hash/fnv" + "net/http" + "net/url" + "slices" + "strconv" + "strings" + "time" + + user_model "gitea.dev/models/user" + "gitea.dev/modules/json" + base "gitea.dev/modules/migration" + "gitea.dev/modules/structs" + "gitea.dev/modules/util" +) + +var ( + _ base.Downloader = &BitbucketDownloader{} + _ base.DownloaderFactory = &BitbucketDownloaderFactory{} +) + +const bitbucketWebURL = "https://bitbucket.org" + +func init() { + RegisterDownloaderFactory(&BitbucketDownloaderFactory{}) +} + +type BitbucketDownloaderFactory struct{} + +func (f *BitbucketDownloaderFactory) New(_ context.Context, opts base.MigrateOptions) (base.Downloader, error) { + cloneURL, err := url.Parse(opts.CloneAddr) + if err != nil { + return nil, err + } + fields := strings.Split(strings.Trim(cloneURL.Path, "/"), "/") + if !strings.EqualFold(cloneURL.Hostname(), "bitbucket.org") || len(fields) < 2 { // the token goes to api.bitbucket.org, so no other host's URL is accepted + return nil, fmt.Errorf("invalid Bitbucket Cloud URL %q, expected https://bitbucket.org//", cloneURL.Redacted()) + } + return NewBitbucketDownloader("https://api.bitbucket.org/2.0", fields[0], strings.TrimSuffix(fields[1], ".git"), cmp.Or(opts.AuthToken, opts.AuthPassword)), nil +} + +func (f *BitbucketDownloaderFactory) GitServiceType() structs.GitServiceType { + return structs.BitbucketService +} + +type bitbucketPage[T any] struct { + Values []T `json:"values"` + Next string `json:"next"` +} + +type bitbucketUser struct { + UUID string `json:"uuid"` + AccountID string `json:"account_id"` + Nickname string `json:"nickname"` + DisplayName string `json:"display_name"` +} + +type bitbucketBranch struct { + Name string `json:"name"` +} + +type bitbucketRepository struct { + Slug string `json:"slug"` + FullName string `json:"full_name"` + Description string `json:"description"` + IsPrivate bool `json:"is_private"` + Website string `json:"website"` + MainBranch bitbucketBranch `json:"mainbranch"` +} + +type bitbucketCommit struct { + Hash string `json:"hash"` +} + +type bitbucketPRSide struct { + Branch bitbucketBranch `json:"branch"` + Commit bitbucketCommit `json:"commit"` + Repository bitbucketRepository `json:"repository"` +} + +type bitbucketParticipant struct { + User bitbucketUser `json:"user"` + Approved bool `json:"approved"` + State string `json:"state"` + ParticipatedOn time.Time `json:"participated_on"` +} + +type bitbucketPullRequest struct { + ID int64 `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + State string `json:"state"` + Draft bool `json:"draft"` + CommentCount int `json:"comment_count"` + Author bitbucketUser `json:"author"` + Source bitbucketPRSide `json:"source"` + Destination bitbucketPRSide `json:"destination"` + MergeCommit bitbucketCommit `json:"merge_commit"` + Participants []bitbucketParticipant `json:"participants"` + Created time.Time `json:"created_on"` + Updated time.Time `json:"updated_on"` + ClosedOn time.Time `json:"closed_on"` // zero while open; updated_on also moves on later activity +} + +type bitbucketComment struct { + Content struct { + Raw string `json:"raw"` + } `json:"content"` + User bitbucketUser `json:"user"` + Created time.Time `json:"created_on"` + Updated time.Time `json:"updated_on"` + Deleted bool `json:"deleted"` + Inline *struct { + Path string `json:"path"` + From int `json:"from"` + To int `json:"to"` + } `json:"inline"` + Parent *struct{} `json:"parent"` + Resolution *struct{} `json:"resolution"` +} + +// isLineComment is false for file comments, which carry neither line +func (c *bitbucketComment) isLineComment() bool { + return c.Inline != nil && (c.Inline.From > 0 || c.Inline.To > 0) +} + +// bitbucketPRContext carries what GetComments and GetReviews share, so the comments of each pull request are fetched once +type bitbucketPRContext struct { + pr bitbucketPullRequest + comments []bitbucketComment +} + +type BitbucketDownloader struct { + base.NullDownloader + client *http.Client + apiRepoURL string + workspace string + repoSlug string +} + +func NewBitbucketDownloader(apiBaseURL, workspace, repoSlug, token string) *BitbucketDownloader { + return &BitbucketDownloader{ + // Bitbucket Cloud answers 429 without Retry-After and refills its limits over a rolling hour + client: &http.Client{Transport: authTransport(retryAfterTransport{RoundTripper: getMigrationTransport(), backoff: time.Minute}, apiBaseURL, util.Iif(token != "", "Bearer "+token, ""))}, + apiRepoURL: apiBaseURL + "/repositories/" + url.PathEscape(workspace) + "/" + url.PathEscape(repoSlug), + workspace: workspace, + repoSlug: repoSlug, + } +} + +func (b *BitbucketDownloader) String() string { + return fmt.Sprintf("migration from bitbucket cloud %s/%s", b.workspace, b.repoSlug) +} + +func (b *BitbucketDownloader) LogString() string { + if b == nil { + return "" + } + return fmt.Sprintf("", b.workspace, b.repoSlug) +} + +// FormatCloneURL adds the token into the clone URL, git accepts API and access tokens with the static user name +func (b *BitbucketDownloader) FormatCloneURL(opts base.MigrateOptions, remoteAddr string) (string, error) { + if token := cmp.Or(opts.AuthToken, opts.AuthPassword); token != "" { + opts.AuthUsername, opts.AuthPassword, opts.AuthToken = "x-token-auth", token, "" + } + return b.NullDownloader.FormatCloneURL(opts, remoteAddr) +} + +func (b *BitbucketDownloader) doAPI(ctx context.Context, path string, query url.Values, out any) error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, b.apiRepoURL+path, nil) + if err != nil { + return err + } + req.URL.RawQuery = query.Encode() + req.Header.Set("Accept", "application/json") + resp, err := b.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return &bbsStatusError{Status: resp.StatusCode, URL: req.URL.String()} + } + return json.NewDecoder(resp.Body).Decode(out) +} + +func bitbucketUserID(user bitbucketUser) int64 { + hasher := fnv.New64a() + _, _ = hasher.Write([]byte(cmp.Or(user.AccountID, user.UUID, bitbucketUserName(user)))) + return int64(hasher.Sum64() & 0x7fffffffffffffff) +} + +func bitbucketUserName(user bitbucketUser) string { + return cmp.Or(user.Nickname, user.DisplayName, user_model.GhostUserName) +} + +func bitbucketCloneURL(owner, repo string) string { + return fmt.Sprintf("%s/%s/%s.git", bitbucketWebURL, url.PathEscape(owner), url.PathEscape(repo)) +} + +func (b *BitbucketDownloader) GetRepoInfo(ctx context.Context) (*base.Repository, error) { + var repo bitbucketRepository + if err := b.doAPI(ctx, "", nil, &repo); err != nil { + return nil, err + } + owner, name, _ := strings.Cut(repo.FullName, "/") + return &base.Repository{ + Owner: owner, + Name: repo.Slug, + IsPrivate: repo.IsPrivate, + Description: repo.Description, + Website: repo.Website, + OriginalURL: fmt.Sprintf("%s/%s/%s", bitbucketWebURL, url.PathEscape(owner), url.PathEscape(name)), + CloneURL: bitbucketCloneURL(owner, name), + DefaultBranch: repo.MainBranch.Name, + }, nil +} + +func (b *BitbucketDownloader) pullRequestComments(ctx context.Context, reviewable base.Reviewable) (*bitbucketPRContext, error) { + prCtx, ok := reviewable.GetContext().(*bitbucketPRContext) + if !ok { + return nil, fmt.Errorf("pullRequestComments: unexpected context %+v", reviewable.GetContext()) + } + if prCtx.comments != nil || prCtx.pr.CommentCount == 0 { + return prCtx, nil + } + comments := make([]bitbucketComment, 0, prCtx.pr.CommentCount) + for page := 1; ; page++ { + var resp bitbucketPage[bitbucketComment] + query := url.Values{"pagelen": {"100"}, "page": {strconv.Itoa(page)}} + if err := b.doAPI(ctx, fmt.Sprintf("/pullrequests/%d/comments", prCtx.pr.ID), query, &resp); err != nil { + return nil, err + } + comments = append(comments, resp.Values...) + if resp.Next == "" { + prCtx.comments = comments + return prCtx, nil + } + } +} + +func (b *BitbucketDownloader) GetComments(ctx context.Context, commentable base.Commentable) ([]*base.Comment, bool, error) { + prCtx, err := b.pullRequestComments(ctx, commentable) + if err != nil { + return nil, false, err + } + comments := make([]*base.Comment, 0, len(prCtx.comments)) + for _, comment := range prCtx.comments { + if comment.Deleted || comment.isLineComment() { + continue + } + content := comment.Content.Raw + if comment.Inline != nil { + content = fmt.Sprintf("Comment on `%s`:\n\n%s", comment.Inline.Path, content) + } + comments = append(comments, &base.Comment{ + IssueIndex: commentable.GetLocalIndex(), + PosterID: bitbucketUserID(comment.User), + PosterName: bitbucketUserName(comment.User), + Created: comment.Created, + Updated: comment.Updated, + Content: content, + }) + } + return comments, true, nil +} + +func (b *BitbucketDownloader) GetReviews(ctx context.Context, reviewable base.Reviewable) ([]*base.Review, error) { + prCtx, err := b.pullRequestComments(ctx, reviewable) + if err != nil { + return nil, err + } + + var reviews []*base.Review + for _, comment := range prCtx.comments { + if comment.Deleted || !comment.isLineComment() { + continue + } + // every comment becomes its own review so that replies keep their author + reviews = append(reviews, &base.Review{ + IssueIndex: reviewable.GetLocalIndex(), + ReviewerID: bitbucketUserID(comment.User), + ReviewerName: bitbucketUserName(comment.User), + CreatedAt: comment.Created, + State: base.ReviewStateCommented, + Comments: []*base.ReviewComment{{ + Content: comment.Content.Raw, + TreePath: comment.Inline.Path, + Line: cmp.Or(comment.Inline.To, -comment.Inline.From), // a negative line addresses the old side of the diff + CommitID: prCtx.pr.Source.Commit.Hash, + UpdatedAt: comment.Updated, + Resolved: comment.Parent == nil && comment.Resolution != nil, + }}, + }) + } + + for _, participant := range prCtx.pr.Participants { + var state string + switch { + case participant.State == "changes_requested": + state = base.ReviewStateChangesRequested + case participant.Approved || participant.State == "approved": + state = base.ReviewStateApproved + default: + continue + } + reviews = append(reviews, &base.Review{ + IssueIndex: reviewable.GetLocalIndex(), + ReviewerID: bitbucketUserID(participant.User), + ReviewerName: bitbucketUserName(participant.User), + CreatedAt: cmp.Or(participant.ParticipatedOn, prCtx.pr.Updated), + State: state, + }) + } + slices.SortStableFunc(reviews, func(a, b *base.Review) int { return a.CreatedAt.Compare(b.CreatedAt) }) + return reviews, nil +} + +func (b *BitbucketDownloader) GetPullRequests(ctx context.Context, page, perPage int) ([]*base.PullRequest, bool, error) { + var resp bitbucketPage[bitbucketPullRequest] + query := url.Values{ + "fields": {"+values.participants,+values.*.commit.date,+values.merge_commit.date"}, // expanding the commits gives full instead of 12-char hashes + "pagelen": {strconv.Itoa(perPage)}, + "page": {strconv.Itoa(page)}, + "sort": {"created_on"}, + "state": {"OPEN", "MERGED", "DECLINED", "SUPERSEDED"}, + } + if err := b.doAPI(ctx, "/pullrequests", query, &resp); err != nil { + return nil, false, err + } + + pullRequests := make([]*base.PullRequest, 0, len(resp.Values)) + for _, pr := range resp.Values { + state := util.Iif(pr.State == "OPEN", "open", "closed") + merged := pr.State == "MERGED" + closedTime := cmp.Or(pr.ClosedOn, pr.Updated) + headOwner, headRepo, _ := strings.Cut(pr.Source.Repository.FullName, "/") + baseOwner, baseRepo, _ := strings.Cut(pr.Destination.Repository.FullName, "/") + pullRequests = append(pullRequests, &base.PullRequest{ + Number: pr.ID, + Title: pr.Title, + PosterID: bitbucketUserID(pr.Author), + PosterName: bitbucketUserName(pr.Author), + Content: pr.Description, + State: state, + Created: pr.Created, + Updated: pr.Updated, + IsDraft: pr.Draft, + Closed: util.Iif(state == "closed", &closedTime, nil), + Merged: merged, + MergedTime: util.Iif(merged, &closedTime, nil), + MergeCommitSHA: pr.MergeCommit.Hash, + Head: base.PullRequestBranch{ + CloneURL: util.Iif(headOwner != "" && headRepo != "", bitbucketCloneURL(headOwner, headRepo), ""), + Ref: pr.Source.Branch.Name, + SHA: pr.Source.Commit.Hash, + OwnerName: headOwner, + RepoName: headRepo, + }, + Base: base.PullRequestBranch{ + Ref: pr.Destination.Branch.Name, + SHA: pr.Destination.Commit.Hash, + OwnerName: baseOwner, + RepoName: baseRepo, + }, + ForeignIndex: pr.ID, + Context: &bitbucketPRContext{pr: pr}, + }) + + _ = CheckAndEnsureSafePR(pullRequests[len(pullRequests)-1], bitbucketWebURL, b) + } + return pullRequests, resp.Next == "", nil +} diff --git a/services/migrations/bitbucket_test.go b/services/migrations/bitbucket_test.go new file mode 100644 index 00000000000..5a4621ff08f --- /dev/null +++ b/services/migrations/bitbucket_test.go @@ -0,0 +1,161 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package migrations + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "os" + "strings" + "sync/atomic" + "testing" + "time" + + "gitea.dev/models/unittest" + base "gitea.dev/modules/migration" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBitbucketDownloadRepo(t *testing.T) { + token := os.Getenv("BITBUCKET_READ_TOKEN") + mockServer := unittest.NewMockWebServer(t, "https://api.bitbucket.org", "_mock_data/TestBitbucketDownloadRepo", token != "", unittest.MockServerOptions{ + FixtureName: func(r *http.Request, _ []byte) string { return "GET_" + url.QueryEscape(r.URL.Path) }, + }) + ctx := t.Context() + + downloader := NewBitbucketDownloader(mockServer.URL+"/2.0", "gitea", "test_repo", token) + repo, err := downloader.GetRepoInfo(ctx) + require.NoError(t, err) + assertRepositoryEqual(t, &base.Repository{ + Name: "test_repo", + Owner: "gitea", + CloneURL: "https://bitbucket.org/gitea/test_repo.git", + OriginalURL: "https://bitbucket.org/gitea/test_repo", + DefaultBranch: "master", + }, repo) + + prs, isEnd, err := downloader.GetPullRequests(ctx, 1, 10) + require.NoError(t, err) + assert.True(t, isEnd) + assertPullRequestsEqual(t, []*base.PullRequest{{ + Number: 1, + PosterID: 5793964208530042003, + PosterName: "Lunny Xiao", + Title: "Update LICENSE", + Content: "do not merge this PR", + State: "open", + Created: time.Date(2026, 10, 7, 18, 39, 55, 551762000, time.UTC), + Updated: time.Date(2026, 10, 7, 18, 40, 40, 122213000, time.UTC), + Head: base.PullRequestBranch{ + CloneURL: "https://bitbucket.org/gitea/test_repo.git", + Ref: "feat/test", + SHA: "9f733b96b98a4175276edf6a2e1231489c3bdd23", + OwnerName: "gitea", + RepoName: "test_repo", + }, + Base: base.PullRequestBranch{ + Ref: "master", + SHA: "c59c9b451acca9d106cc19d61d87afe3fbbb8b83", + OwnerName: "gitea", + RepoName: "test_repo", + }, + }}, prs) + + downloader = NewBitbucketDownloader(mockServer.URL+"/2.0", "atlassian", "aui", token) + prs, isEnd, err = downloader.GetPullRequests(ctx, 1, 1) + require.NoError(t, err) + assert.False(t, isEnd) + require.Len(t, prs, 1) + assert.Equal(t, "closed", prs[0].State) + assert.True(t, prs[0].Merged) + assert.NotNil(t, prs[0].MergedTime) + assert.Len(t, prs[0].MergeCommitSHA, 40) + assert.Empty(t, prs[0].Head.SHA) + assert.Len(t, prs[0].Base.SHA, 40) + + comments, isEnd, err := downloader.GetComments(ctx, prs[0]) + require.NoError(t, err) + assert.True(t, isEnd) + assert.Len(t, comments, 25) +} + +func TestBitbucketDownloader(t *testing.T) { + headSHA := strings.Repeat("a", 40) + var commentRequests atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "Bearer tok", r.Header.Get("Authorization")) + switch r.URL.Path { + case "/2.0/repositories/w/r/pullrequests": + _, _ = w.Write([]byte(`{"values":[{"id":3,"state":"OPEN","draft":true,"comment_count":5,"updated_on":"2024-01-03T00:00:00Z","source":{"commit":{"hash":"` + headSHA + `"},"repository":{"full_name":"w/r"}},"participants":[ + {"user":{"nickname":"alice"},"approved":true,"participated_on":"2024-01-02T00:00:00Z"}, + {"user":{"nickname":"bob"},"state":"changes_requested"}, + {"user":{"nickname":"carol"}}]}]}`)) + case "/2.0/repositories/w/r/pullrequests/3/comments": + commentRequests.Add(1) + _, _ = w.Write([]byte(`{"values":[ + {"id":1,"content":{"raw":"general"},"user":{"nickname":"alice"}}, + {"id":2,"content":{"raw":"inline"},"user":{"nickname":"bob"},"inline":{"path":"a.txt","to":5},"resolution":{}}, + {"id":3,"content":{"raw":"old side"},"user":{"nickname":"bob"},"inline":{"path":"a.txt","from":7},"parent":{"id":2}}, + {"id":4,"content":{"raw":"gone"},"user":{"nickname":"bob"},"deleted":true}, + {"id":5,"content":{"raw":"file"},"user":{"nickname":"bob"},"inline":{"path":"b.txt"}}]}`)) + default: + http.NotFound(w, r) + } + })) + defer server.Close() + + downloader := NewBitbucketDownloader(server.URL+"/2.0", "w", "r", "tok") + prs, _, err := downloader.GetPullRequests(t.Context(), 1, 10) + require.NoError(t, err) + require.Len(t, prs, 1) + assert.True(t, prs[0].IsDraft) + + comments, _, err := downloader.GetComments(t.Context(), prs[0]) + require.NoError(t, err) + require.Len(t, comments, 2) + assert.Equal(t, "general", comments[0].Content) + assert.Equal(t, "Comment on `b.txt`:\n\nfile", comments[1].Content) + + reviews, err := downloader.GetReviews(t.Context(), prs[0]) + require.NoError(t, err) + assert.EqualValues(t, 1, commentRequests.Load()) + var inline, states []string + for _, review := range reviews { + if len(review.Comments) > 0 { + reviewComment := review.Comments[0] + inline = append(inline, fmt.Sprintf("%s:%d:%v", reviewComment.TreePath, reviewComment.Line, reviewComment.Resolved)) + assert.Equal(t, headSHA, reviewComment.CommitID) + } else { + states = append(states, review.ReviewerName+":"+review.State) + } + } + assert.ElementsMatch(t, []string{"a.txt:5:true", "a.txt:-7:false"}, inline) + assert.ElementsMatch(t, []string{"alice:APPROVED", "bob:CHANGES_REQUESTED"}, states) + + cloneURL, err := downloader.FormatCloneURL(base.MigrateOptions{AuthUsername: "me@example.com", AuthPassword: "tok"}, "https://bitbucket.org/w/r") + require.NoError(t, err) + assert.Equal(t, "https://x-token-auth:tok@bitbucket.org/w/r", cloneURL) + + _, err = (&BitbucketDownloaderFactory{}).New(t.Context(), base.MigrateOptions{CloneAddr: "https://git.example.com/w/r.git"}) + assert.ErrorContains(t, err, "invalid Bitbucket Cloud URL") +} + +func TestBitbucketPullRequestClosedTime(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"values":[{"id":1,"state":"MERGED","created_on":"2024-01-01T00:00:00Z","updated_on":"2024-02-01T00:00:00Z","closed_on":"2024-01-05T00:00:00Z"}, + {"id":2,"state":"DECLINED","created_on":"2024-01-01T00:00:00Z","updated_on":"2024-02-01T00:00:00Z"}]}`)) + })) + defer server.Close() + + prs, _, err := NewBitbucketDownloader(server.URL+"/2.0", "w", "r", "").GetPullRequests(t.Context(), 1, 10) + require.NoError(t, err) + require.Len(t, prs, 2) + assert.Equal(t, time.Date(2024, 1, 5, 0, 0, 0, 0, time.UTC), *prs[0].MergedTime) + assert.Equal(t, *prs[0].MergedTime, *prs[0].Closed) + assert.Equal(t, time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC), *prs[1].Closed) // falls back to updated_on +} diff --git a/services/migrations/bitbucketserver.go b/services/migrations/bitbucketserver.go index 3787d32eea6..1f3a4cf0e74 100644 --- a/services/migrations/bitbucketserver.go +++ b/services/migrations/bitbucketserver.go @@ -286,7 +286,7 @@ func (e *bbsStatusError) isAuthError() bool { } func (e *bbsStatusError) Error() string { - msg := fmt.Sprintf("bitbucket server API %s returned status %d", e.URL, e.Status) + msg := fmt.Sprintf("bitbucket API %s returned status %d", e.URL, e.Status) if e.isAuthError() { msg += ", check the credentials and the permission to read the repository" } diff --git a/services/migrations/gitea_uploader.go b/services/migrations/gitea_uploader.go index c490b43a038..6b3ba1eecef 100644 --- a/services/migrations/gitea_uploader.go +++ b/services/migrations/gitea_uploader.go @@ -5,6 +5,7 @@ package migrations import ( + "cmp" "context" "errors" "fmt" @@ -43,6 +44,7 @@ type GiteaLocalUploader struct { doer *user_model.User repoOwner string repoName string + cloneURL string // clone address of the source, may carry credentials repo *repo_model.Repository labels map[string]*issues_model.Label milestones map[string]int64 @@ -118,6 +120,7 @@ func (g *GiteaLocalUploader) CreateRepo(ctx context.Context, repo *base.Reposito r.Description = repo.Description r.Website = repo.Website + g.cloneURL = repo.CloneURL r, err = repo_service.MigrateRepositoryGitData(ctx, owner, r, base.MigrateOptions{ RepoName: g.repoName, Description: repo.Description, @@ -601,6 +604,24 @@ func (g *GiteaLocalUploader) commitExists(ctx context.Context, sha string) bool return err == nil } +// fetchCommit tries to get a missing head commit of a closed pull request by its SHA from the head repository, +// e.g. a squash-merged pull request whose source branch is gone and which the host does not expose as a ref +func (g *GiteaLocalUploader) fetchCommit(ctx context.Context, pr *base.PullRequest) bool { + remote := pr.Head.CloneURL + if !pr.IsForkPullRequest() && g.cloneURL != "" { + remote = g.cloneURL // the source repository itself, with the migration credentials + } + if remote == "" { + return false + } + _, _, err := gitcmd.NewCommand("fetch", "--no-tags").AddDashesAndList(remote, pr.Head.SHA).WithRepo(g.repo).RunStdString(ctx) + if err != nil { + log.Warn("PR #%d fetching head commit %s failed: %v", pr.Number, pr.Head.SHA, util.SanitizeCredentialURLs(err.Error())) + return false + } + return g.commitExists(ctx, pr.Head.SHA) +} + func (g *GiteaLocalUploader) updateGitForPullRequest(ctx context.Context, pr *base.PullRequest) (head string, err error) { // SECURITY: this pr must have been must have been ensured safe if !pr.EnsuredSafe { @@ -739,7 +760,7 @@ func (g *GiteaLocalUploader) updateGitForPullRequest(ctx context.Context, pr *ba if pr.Head.SHA == "" { // The SHA is empty log.Warn("Empty reference, no pull head for PR #%d in %s/%s", pr.Number, g.repoOwner, g.repoName) - } else if !g.commitExists(ctx, pr.Head.SHA) { + } else if !g.commitExists(ctx, pr.Head.SHA) && !g.fetchCommit(ctx, pr) { // Git update-ref remove bad references with a relative path log.Warn("Deprecated local head %s for PR #%d in %s/%s, removing %s", pr.Head.SHA, pr.Number, g.repoOwner, g.repoName, pr.GetGitHeadRefName()) } else if err = git.UpdateRef(ctx, g.repo, pr.GetGitHeadRefName(), pr.Head.SHA); err != nil { @@ -874,6 +895,7 @@ func convertReviewState(state string) issues_model.ReviewType { // CreateReviews create pull request reviews of currently migrated issues func (g *GiteaLocalUploader) CreateReviews(ctx context.Context, reviews ...*base.Review) error { cms := make([]*issues_model.Review, 0, len(reviews)) + var fallbackComments []*issues_model.Comment for _, review := range reviews { var issue *issues_model.Issue issue, ok := g.issues[review.IssueIndex] @@ -910,15 +932,33 @@ func (g *GiteaLocalUploader) CreateReviews(ctx context.Context, reviews ...*base } g.prCache[issue.ID] = pr } + headCommitID := "" if pr.MergeBase == "" { - // No mergebase -> no basis for any patches - log.Warn("PR #%d in %s/%s: does not have a merge base, all review comments will be ignored", pr.Index, g.repoOwner, g.repoName) - continue + log.Warn("PR #%d in %s/%s: does not have a merge base, review comments will be added as plain comments", pr.Index, g.repoOwner, g.repoName) + } else if id, err := g.gitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName()); err != nil { + log.Warn("PR #%d GetRefCommitID[%s] in %s/%s: %v, review comments will be added as plain comments", pr.Index, pr.GetGitHeadRefName(), g.repoOwner, g.repoName, err) + } else { + headCommitID = id } - headCommitID, err := g.gitRepo.GetRefCommitID(ctx, pr.GetGitHeadRefName()) - if err != nil { - log.Warn("PR #%d GetRefCommitID[%s] in %s/%s: %v, all review comments will be ignored", pr.Index, pr.GetGitHeadRefName(), g.repoOwner, g.repoName, err) + if headCommitID == "" { + // without a diff to anchor to, keep the text as plain comments instead of an empty review + for _, comment := range review.Comments { + c := issues_model.Comment{ + Type: issues_model.CommentTypeComment, + IssueID: issue.ID, + Content: fmt.Sprintf("Comment on `%s` line %d:\n\n%s", util.PathJoinRel(comment.TreePath), max(comment.Line, -comment.Line), comment.Content), + CreatedUnix: timeutil.TimeStamp(cmp.Or(comment.CreatedAt, review.CreatedAt).Unix()), + UpdatedUnix: timeutil.TimeStamp(cmp.Or(comment.UpdatedAt, comment.CreatedAt, review.CreatedAt).Unix()), + } + if err := g.remapUser(ctx, review, &c); err != nil { + return err + } + fallbackComments = append(fallbackComments, &c) + } + if len(review.Comments) > 0 && cm.Type == issues_model.ReviewTypeComment && cm.Content == "" { + cms = cms[:len(cms)-1] + } continue } @@ -972,7 +1012,10 @@ func (g *GiteaLocalUploader) CreateReviews(ctx context.Context, reviews ...*base } } - return issues_model.InsertReviews(ctx, cms) + if err := issues_model.InsertReviews(ctx, cms); err != nil { + return err + } + return issues_model.InsertIssueComments(ctx, fallbackComments) } // Rollback when migrating failed, this will rollback all the changes. diff --git a/services/migrations/gitea_uploader_test.go b/services/migrations/gitea_uploader_test.go index 933a45e579f..7bb910d0a2f 100644 --- a/services/migrations/gitea_uploader_test.go +++ b/services/migrations/gitea_uploader_test.go @@ -349,3 +349,43 @@ func TestGiteaUploadForkPullRequestHeadInRepository(t *testing.T) { require.NoError(t, err) assert.Empty(t, strings.TrimSpace(remotes), "no remote is added for a local head") } + +func TestGiteaUploadReviewCommentsWithoutHead(t *testing.T) { + unittest.PrepareTestEnv(t) + ctx := t.Context() + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + pull := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{ID: 1}) + require.NoError(t, pull.LoadIssue(ctx)) + + uploader := NewGiteaLocalUploader(ctx, doer, repo.OwnerName, repo.Name) + var err error + uploader.repo = repo + uploader.gitRepo, err = git.OpenRepository(ctx, repo) + require.NoError(t, err) + defer uploader.Close() + uploader.issues[pull.Issue.Index] = pull.Issue + require.NoError(t, git.RemoveRef(ctx, repo, pull.GetGitHeadRefName())) + + opts := issues_model.FindReviewOptions{IssueID: pull.Issue.ID} + reviewsBefore, err := issues_model.CountReviews(ctx, opts) + require.NoError(t, err) + + require.NoError(t, uploader.CreateReviews(ctx, &base.Review{ + IssueIndex: pull.Issue.Index, + ReviewerID: doer.ID, + State: base.ReviewStateCommented, + Comments: []*base.ReviewComment{{Content: "lost anchor", TreePath: "README.md", Line: -3}}, + })) + + comments, err := issues_model.FindComments(ctx, &issues_model.FindCommentsOptions{IssueID: pull.Issue.ID, Type: issues_model.CommentTypeComment}) + require.NoError(t, err) + var contents []string + for _, c := range comments { + contents = append(contents, c.Content) + } + assert.Contains(t, contents, "Comment on `README.md` line 3:\n\nlost anchor") + reviewsAfter, err := issues_model.CountReviews(ctx, opts) + require.NoError(t, err) + assert.Equal(t, reviewsBefore, reviewsAfter) // no empty review is left behind +} diff --git a/services/migrations/http_client.go b/services/migrations/http_client.go index 7e41fb8685b..7f793f904f9 100644 --- a/services/migrations/http_client.go +++ b/services/migrations/http_client.go @@ -19,7 +19,7 @@ import ( // getMigrationHTTPClient returns the shared migration client, so downloads from one host reuse its connections var getMigrationHTTPClient = sync.OnceValue(func() *http.Client { - return &http.Client{Transport: retryAfterTransport{getMigrationTransport()}} + return &http.Client{Transport: retryAfterTransport{RoundTripper: getMigrationTransport()}} }) var getMigrationTransport = sync.OnceValue(newMigrationTransport) @@ -30,7 +30,7 @@ func newMigrationHTTPClient(baseURL, authorization string) *http.Client { // NewMigrationHTTPTransport returns a HTTP transport for migration, enforcing the migration policy on its direct dials. func NewMigrationHTTPTransport() http.RoundTripper { - return retryAfterTransport{newMigrationTransport()} + return retryAfterTransport{RoundTripper: newMigrationTransport()} } func newMigrationTransport() *http.Transport { @@ -41,6 +41,7 @@ func newMigrationTransport() *http.Transport { type retryAfterTransport struct { http.RoundTripper + backoff time.Duration // first wait for a 429 without Retry-After, doubled per retry, zero gives up } func (t retryAfterTransport) RoundTrip(req *http.Request) (*http.Response, error) { @@ -54,6 +55,9 @@ func (t retryAfterTransport) RoundTrip(req *http.Request) (*http.Response, error return resp, nil } delay, ok := parseRetryAfter(resp.Header.Get("Retry-After")) + if !ok && t.backoff > 0 && resp.StatusCode == http.StatusTooManyRequests { + delay, ok = t.backoff< time.Hour-waited { return resp, nil } diff --git a/services/migrations/http_client_test.go b/services/migrations/http_client_test.go index 746e147e82c..af62f22b554 100644 --- a/services/migrations/http_client_test.go +++ b/services/migrations/http_client_test.go @@ -26,20 +26,23 @@ func TestMigrationHTTPTransportRetryAfter(t *testing.T) { case 2: w.Header().Set("Retry-After", time.Now().UTC().Format(http.TimeFormat)) w.WriteHeader(http.StatusForbidden) + case 3: + w.WriteHeader(http.StatusTooManyRequests) default: _, _ = io.Copy(w, r.Body) } })) defer server.Close() - resp, err := getMigrationHTTPClient().Post(server.URL, "text/plain", strings.NewReader("payload")) + client := &http.Client{Transport: retryAfterTransport{RoundTripper: getMigrationTransport(), backoff: time.Nanosecond}} + resp, err := client.Post(server.URL, "text/plain", strings.NewReader("payload")) require.NoError(t, err) defer resp.Body.Close() body, err := io.ReadAll(resp.Body) require.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) assert.Equal(t, "payload", string(body)) - assert.EqualValues(t, 3, requests.Load()) + assert.EqualValues(t, 4, requests.Load()) } func TestAuthTransportSendsCredentialsToSameHostOnly(t *testing.T) { diff --git a/templates/repo/migrate/bitbucket.tmpl b/templates/repo/migrate/bitbucket.tmpl new file mode 100644 index 00000000000..805fb86fa4b --- /dev/null +++ b/templates/repo/migrate/bitbucket.tmpl @@ -0,0 +1,50 @@ +{{template "base/head" .}} +
+
+

+ {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} +

+
+ {{template "base/alert" .}} +
+ {{template "repo/migrate/common-init" (dict "ServiceType" .service)}} + {{template "repo/migrate/variants" .}} + + {{template "repo/migrate/common-clone-addr"}} + + {{if ne .service.Name "bitbucket"}} +
+ + +
+
+ + +
+ {{end}} +
+ + + {{ctx.Locale.Tr (printf "repo.migrate.%s.token_desc" .service.Name)}} +
+ + {{template "repo/migrate/common-options" .}} + +
+
+ +
+ + +
+
+
+ +
+ + {{template "repo/migrate/common-migrated-repo"}} +
+
+
+
+{{template "base/footer" .}} diff --git a/templates/repo/migrate/bitbucketserver.tmpl b/templates/repo/migrate/bitbucketserver.tmpl index bc0ad74619f..225d750b6bc 100644 --- a/templates/repo/migrate/bitbucketserver.tmpl +++ b/templates/repo/migrate/bitbucketserver.tmpl @@ -1,48 +1 @@ -{{template "base/head" .}} -
-
-

- {{ctx.Locale.Tr "repo.migrate.migrate" .service.Title}} -

-
- {{template "base/alert" .}} -
- {{template "repo/migrate/common-init" (dict "ServiceType" .service)}} - {{template "repo/migrate/variants" .}} - - {{template "repo/migrate/common-clone-addr"}} - -
- - -
-
- - -
-
- - - {{ctx.Locale.Tr "repo.migrate.bitbucketserver.token_desc"}} -
- - {{template "repo/migrate/common-options" .}} - -
-
- -
- - -
-
-
- -
- - {{template "repo/migrate/common-migrated-repo"}} -
-
-
-
-{{template "base/footer" .}} +{{template "repo/migrate/bitbucket" .}} diff --git a/templates/repo/migrate/migrate.tmpl b/templates/repo/migrate/migrate.tmpl index fb921f68a1d..b786204983d 100644 --- a/templates/repo/migrate/migrate.tmpl +++ b/templates/repo/migrate/migrate.tmpl @@ -8,16 +8,14 @@ {{if eq .Name "github"}} {{svg "octicon-mark-github" 184 "tw-p-4"}} - {{else if eq .Name "bitbucketserver"}} - {{svg "gitea-bitbucket" 184 "tw-p-4"}} - {{else if or (eq .Name "gitlab") (eq .Name "gitbucket") (eq .Name "forgejo")}} + {{else if or (eq .Name "gitlab") (eq .Name "gitbucket") (eq .Name "forgejo") (eq .Name "bitbucket")}} {{svg (printf "gitea-%s" .Name) 184 "tw-p-4"}} {{else}} {{svg (printf "gitea-%s" .Name) 184}} {{end}}
- {{.ProviderTitle}} + {{.Title}}
{{ctx.Locale.Tr (printf "repo.migrate.%s.description" .Name)}} diff --git a/templates/swagger/v1-openapi3.generated.json b/templates/swagger/v1-openapi3.generated.json index ee1230ce0a6..7502806197d 100644 --- a/templates/swagger/v1-openapi3.generated.json +++ b/templates/swagger/v1-openapi3.generated.json @@ -7977,7 +7977,8 @@ "codebase", "codecommit", "bitbucketserver", - "forgejo" + "forgejo", + "bitbucket" ], "type": "string", "x-go-name": "Service" diff --git a/templates/swagger/v1-swagger.generated.json b/templates/swagger/v1-swagger.generated.json index 5a97a354502..84231c3d429 100644 --- a/templates/swagger/v1-swagger.generated.json +++ b/templates/swagger/v1-swagger.generated.json @@ -30472,7 +30472,8 @@ "codebase", "codecommit", "bitbucketserver", - "forgejo" + "forgejo", + "bitbucket" ], "x-go-name": "Service" }, diff --git a/tests/integration/repo_migrate_test.go b/tests/integration/repo_migrate_test.go index b8d90839fc4..c2c03a5176b 100644 --- a/tests/integration/repo_migrate_test.go +++ b/tests/integration/repo_migrate_test.go @@ -54,4 +54,15 @@ func TestRepoMigrate(t *testing.T) { assert.Equal(t, 1, entries.Length()) assert.Equal(t, "Bitbucket", strings.TrimSpace(entries.Text())) }) + + t.Run("BitbucketVariants", func(t *testing.T) { + for _, service := range structs.BitbucketService.Variants() { + resp := session.MakeRequest(t, NewRequest(t, "GET", fmt.Sprintf("/repo/migrate?service_type=%d", service)), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + assert.Equal(t, fmt.Sprint(service), htmlDoc.GetInputValueByName("service")) + assert.Equal(t, 2, htmlDoc.Find(".ui.compact.menu a.item").Length()) + assert.Contains(t, htmlDoc.Find(".ui.compact.menu a.item.active").AttrOr("href", ""), fmt.Sprintf("service_type=%d&", service)) + assert.Equal(t, service == structs.BitbucketServerService, htmlDoc.Find(`input[name="auth_username"]`).Length() == 1) + } + }) }