Skip to content

Add Bitbucket adapter - #125

Open
HarshMN2345 wants to merge 25 commits into
mainfrom
feat-bitbucket-adapter
Open

Add Bitbucket adapter#125
HarshMN2345 wants to merge 25 commits into
mainfrom
feat-bitbucket-adapter

Conversation

@HarshMN2345

@HarshMN2345 HarshMN2345 commented Jul 29, 2026

Copy link
Copy Markdown
Member

Adds a Bitbucket Cloud adapter (API 2.0), implementing every abstract on Adapter/Git: repositories, source/tree/content, branches, tags, commits, build statuses, pull requests, comments, webhooks, clone commands and webhook event parsing. Authenticates with a Bearer access token (OAuth 2.0, workspace or repository token), passed through initializeVariables(accessToken:) like the GitLab and Gitea adapters.

Endpoint shapes were taken from Bitbucket's published OpenAPI spec, including the POST /src form-field contract (file paths are sent as /-prefixed field names so a file named message isn't read as commit metadata) and its documented empty-repo / new-branch behavior.

Where Bitbucket doesn't fit the shared interface

Each of these is implemented and documented in place:

  • No numeric repository ids. id is normalized to workspace/slug, which is what its API routes on and what getRepositoryName() accepts. getOwnerName() therefore ignores $repositoryId and resolves the token's own workspace.
  • Webhooks are UUID-keyed, so Git::createWebhook() now returns int|string and deleteWebhook() takes what it returned.
  • No language statistics — only a hand-set language field, reported when present.
  • Push payloads carry no file lists, so affectedFiles is always empty.
  • Archive downloads come from the browser host, not the API host, and are answered directly rather than redirected to a signed URL, so getRepositoryPresignedUrl() embeds the credential as HTTP basic userinfo. GitHub returns its redirect target instead, and the ?access_token= query form GitLab and Gitea use was removed from Bitbucket in CHANGE-3052.

Responses are normalized onto the keys the other adapters report: private, pushed_at, number on pull requests, lowercased PR state, and commit states mapped both ways between Bitbucket's vocabulary and the shared one.

Tests

BitbucketTest follows the pattern main now uses: the shared contract lives in Base, and an adapter's own file declares what it is and only tests what is true of it alone. It declares 7 tests of its own and runs 108 in total — 101 of them inherited.

Bitbucket declares the parts of the contract it does not offer, rather than overriding tests to say so: $supportsCheckRuns, $supportsNamespaceListing, $supportsRepositoryLanguages, $supportsUserLookup, $supportsWebhookDelivery, $supportsInstallationRepository, $resolvesOwnerFromRepositoryId and $reportsAffectedFilesInPushEvent. Those skip the shared tests for the parts it does not offer.

What stays Bitbucket's own: workspaces (its grouping in place of namespaces), UUID-keyed webhooks, user lookup by UUID, multi-ref pushes through getEvents(), its event-to-action mapping, tag pushes not being reported as branches, the linked-vs-raw commit author, and a build status written without a URL. The hand-written getEvent assertions are gone — the class supplies pushPayload() and pullRequestPayload() builders and the shared assertions cover them.

Three additions to Base, all defaulting to full support so the gap is the adapter's to declare:

  • $supportsPresignedUrls and $reportsAffectedFilesInPushEvent, new capability flags (Bitbucket declares only the latter -- it does support presigned urls).
  • repositoryIdOf(), overridable, because Base otherwise asserts a repository id is numeric.
  • EVENT_* payload facts are read through static:: so an adapter can restate one; Bitbucket restates the repository id as workspace/slug.

testWebhookPullRequestEvent also now skips on $supportsWebhookDelivery rather than only on pull request support, which is what actually stops Bitbucket Cloud from reaching the local request-catcher.

One adapter fix came out of running the shared tests: getEvent() returned an empty event for a malformed payload instead of throwing, as the other adapters do.

composer lint and composer check (PHPStan level 8) pass. The suite is still credential-gated and the secrets are not set on this repo, so the bitbucket CI job currently skips all 112 tests and passes without asserting anything — it needs TESTS_BITBUCKET_ACCESS_TOKEN, and optionally TESTS_BITBUCKET_WORKSPACE, whose workspace needs at least one project since Bitbucket assigns every new repository to one.

Comment thread src/VCS/Adapter/Git/Bitbucket.php Outdated
Comment thread src/VCS/Adapter/Git/Bitbucket.php Outdated
Comment thread src/VCS/Adapter/Git/Bitbucket.php Outdated
Comment thread src/VCS/Adapter/Git/Bitbucket.php Outdated
@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a Bitbucket Cloud adapter and extends the shared VCS contract to support provider-specific webhook identifiers and batched webhook events.

  • Implements Bitbucket repository, source, branch, tag, commit, pull-request, status, webhook, archive, clone, and event operations.
  • Adds Bitbucket integration tests, CI configuration, capability flags, and documentation.
  • Moves webhook creation and deletion into the common adapter contract and introduces getEvents() for batched deliveries.

Confidence Score: 2/5

This PR is not safe to merge until webhook recovery stops returning identifiers from incomplete listings and the outstanding webhook event-loss, orphaning, and credential-exposure failures are addressed.

A failed later webhook-list page can make UUID recovery select the wrong hook, unresolved UUID recovery can leave active hooks unmanaged, getEvent still discards later refs in multi-ref pushes, and archive and clone outputs still expose the access token.

Files Needing Attention: src/VCS/Adapter/Git/Bitbucket.php

Important Files Changed

Filename Overview
src/VCS/Adapter/Git/Bitbucket.php Implements the Bitbucket adapter, but webhook UUID recovery can trust an incomplete listing and several previously reported webhook and credential-handling failures remain.
src/VCS/Adapter.php Extends the shared contract with string webhook identifiers, deletion, and batched event parsing.
tests/VCS/Adapter/BitbucketTest.php Adds Bitbucket-specific contract coverage, though credential-gated execution and skipped webhook delivery leave important remote behavior unverified.
tests/VCS/Base.php Generalizes shared adapter tests for opaque repository and webhook identifiers, capability flags, and batched-event differences.
src/VCS/Adapter/Git/GitHub.php Adds webhook deletion required by the expanded shared adapter contract.
src/VCS/Adapter/Git/GitLab.php Adds webhook deletion required by the expanded shared adapter contract.
src/VCS/Adapter/Git/Gitea.php Adds webhook deletion inherited by Gitea-compatible providers.

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
### Issue 1
src/VCS/Adapter/Git/Bitbucket.php:1035-1037
**Partial hook list selects wrong UUID**

When a later page of the hook listing fails after an earlier page contains one matching URL, this branch treats that partial result as complete and returns the older hook's UUID. Subsequent cleanup deletes the older webhook while the newly created webhook remains active and unmanaged.

```suggestion
            if (($responseHeaders['status-code'] ?? 0) >= 400) {
                return null;
            }
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (26): Last reviewed commit: "fix: return the routable slug and a real..." | Re-trigger Greptile

@HarshMN2345

Copy link
Copy Markdown
Member Author

@greptile fixed the first three in aaa8801.

  1. repositoryId was inconsistent, good catch. A bitbucket repo uuid isn't routable on its own (the api wants workspace + repo in the path), so getEvent now reports full_name, which is what getRepositoryName and the normalized id already use. Added a test that pulls the id out of the event and feeds it straight into getRepositoryName so the two can't drift apart again.

  2. Agreed, creating the hook and then returning 0 was the worst of both. createWebhook now throws before creating anything and points at createRepositoryWebhook, which returns the uuid that deleteWebhook takes. Same approach as GitHub::createTag. Widening the interface to return string would be nicer but it touches all 5 adapters and their tests, so I'd rather do that separately if we want it.

  3. Added getEvents(), which returns one event per ref in payload order. getEvent() returns the first so the shared interface doesn't change. While doing this I noticed tags arrive in the same changes array and were being reported as branches (branch = v1.0.0, url pointing at /branch/v1.0.0), so those are skipped now and a tag only push returns [].

  4. Not changing this one here. The token in the clone url is how every adapter in this library already works, GitLab embeds oauth2:{token}@ and Gitea embeds {owner}:{token}@, and Base::testGenerateCloneCommand asserts that command shape. Making bitbucket the only one that does it differently wouldn't remove the exposure anywhere else. If we want GIT_ASKPASS or a credential helper it should be one change across all the adapters.

Comment thread src/VCS/Adapter/Git/Bitbucket.php Outdated
@greptile-apps

greptile-apps Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

All three fixes look correct from the diff and tests.

1 — repositoryId: getEvent() now reports full_name (workspace/slug) and testGetEventReportsResolvableRepositoryId closes the loop by feeding the emitted id straight into getRepositoryName(). Solid.

2 — createWebhook: Throwing before creating anything is the right call. The testCreateWebhookThroughSharedInterfaceIsRefused / testCreateWebhook pairing makes the intended path unambiguous — callers get a clear error if they hit the wrong entry point.

3 — getEvents() + tag skipping: The multi-ref test (2 branches + 1 tag → 2 events) and the tag-only test ([]) cover both cases cleanly. Having getEvent() delegate to getEvents()[0] rather than duplicating the parsing logic is the right structure.

4 — Clone URL: Fair point, and I agree it should be a single cross-adapter change if pursued. Happy to drop that comment.

No remaining blockers from my side on these four items.

Bitbucket identifies a hook by UUID, which an int return cannot carry, so
createWebhook() threw and a separate createRepositoryWebhook() returned the
UUID. Widening the return to int|string lets Bitbucket implement the same
method every other adapter does.
Bitbucket declares the parts of the contract it does not offer - presigned
archive urls, check runs, namespace listing, computed languages, user
lookup by handle, webhook delivery to the catcher, and file lists in a
push event - and keeps only the tests describing itself: workspaces, uuid
webhooks and user lookups, multi-ref pushes, and its action mapping.

The event payload builders replace hand-written getEvent tests, so the
shared assertions cover them, and Base's repository id is now overridable
because Bitbucket routes on "workspace/slug" rather than a numeric id.

getEvent() returned an empty event for a malformed payload rather than
throwing, as the other adapters do.
@HarshMN2345
HarshMN2345 force-pushed the feat-bitbucket-adapter branch from aaa8801 to fe8e115 Compare July 30, 2026 11:26
@HarshMN2345

Copy link
Copy Markdown
Member Author

@greptile This matches the existing pattern in GitHub::generateCloneCommand() and GitLab::generateCloneCommand() both embed the access token in the git remote add origin URL the same way, so this isn't a regression specific to Bitbucket. Fixing it properly (e.g. via a credential helper instead of an embedded URL) would need to happen across all three adapters together worth tracking as a separate follow-up rather than a Bitbucket-only patch here.

@greptile-apps

greptile-apps Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

That's a fair point. The token-in-URL pattern is consistent across GitHub, GitLab, and now Bitbucket — this PR isn't introducing anything new. A proper fix (credential helper, GIT_ASKPASS, or similar) would need to be a coordinated change across all three adapters to avoid inconsistency.

Agreed — worth tracking as a repo-level follow-up rather than blocking here.

@HarshMN2345

Copy link
Copy Markdown
Member Author

Re the two Greptile findings:

Clone command exposes the access token — this matches the existing pattern in GitHub::generateCloneCommand() and GitLab::generateCloneCommand(), both of which embed the token in the git remote add origin URL the same way. It's not a Bitbucket-specific regression; fixing it properly (e.g. via a credential helper instead of an embedded URL) would need to happen across all three adapters together, so I'd track that as a separate follow-up rather than a Bitbucket-only patch here.

getEvent() drops later branch changes — this one was real. Bitbucket::getEvent() intentionally reports only the first of a batched push (getEvents() has the rest), but getEvents() wasn't part of the shared Adapter contract, so a consumer coded against the standard interface would silently lose every branch after the first. Fixed in 2ffe0a4 by promoting getEvents() onto Adapter with a default that wraps getEvent():

public function getEvents(string $event, string $payload): array
{
    return [$this->getEvent($event, $payload)];
}

Every other adapter gets this for free with no behavior change; Bitbucket already overrides it correctly.

Comment thread src/VCS/Adapter/Git/Bitbucket.php Outdated
Same bug as the OAuth2 side, independently: username/nickname aren't
workspace identifiers for accounts migrated to Atlassian's unified
identity, and the account UUID doesn't double as the workspace UUID.
Silently returned zero repositories rather than an error, since
searchRepositories() treats any 4xx as an empty result.

Resolves the workspace via /user/workspaces -- the endpoint
Atlassian's migration guidance names as the replacement for the
cross-workspace /workspaces listing CHANGE-2770 removed -- falling
back to the old username/nickname behavior only if that call fails.
resolveRef() above already catches Exception and converts it to
FileNotFound() for the expected "file/ref doesn't exist" case, but the
two call() invocations below it didn't -- so a 404 whose response
body doesn't decode as JSON (a real, common case: not every repo has
package.json) propagated as an uncaught fatal, crashing the whole
Swoole worker (confirmed live) rather than resulting in a normal
FileNotFound for this one lookup.
Was missing entirely, throwing the base Git class's "not supported"
default -- confirmed live, this crashed VCS site/function deployments
outright (Compute/Base.php calls it unconditionally for every
provider). Bitbucket's archive download lives on the browser host,
not the API host, and only supports zip/gz/bz2 (no "tarball"
extension distinct from gz). Auth is URL-embedded the same way
generateCloneCommand() already does it, since this URL is handed off
for a plain download rather than called with a bearer header.
Comment thread src/VCS/Adapter/Git/Bitbucket.php Outdated
…igned urls

Follow-ups from review of the presigned-url commit:

- Archive extension was `.gz`; verified against a live repo that
  `.tar.gz` serves the same gzipped tarball and matches the shared
  contract's default $presignedTarballFragment, so no override is
  needed.
- Ref is now encoded keeping slashes, as Gitea and GitHub do, so
  nested branch names (feature/foo) resolve.
- Documented why the credential travels as basic userinfo rather than
  the query parameter GitLab and Gitea use: Bitbucket answers this
  directly instead of redirecting to a signed url (so GitHub's
  redirect-following approach isn't available), and its
  ?access_token= form was removed in CHANGE-3052.
- createWebhook() threw away a missing uuid, returning '' and leaving
  the caller unable to delete the hook it just created. Now throws,
  mirroring GitHub::createWebhook()'s guard.
- Dropped $supportsPresignedUrls = false so the two shared contract
  tests run instead of asserting the method throws -- it no longer
  does. Folded the tag-only push case into the multi-ref test rather
  than keeping a separate test for the same rule.
The method name collided with Base::testGetUser(), so PHPUnit ran only
Bitbucket's override and never reached Base's skipUnlessSupported()
check -- meaning $supportsUserLookup = false correctly skipped
testGetUserWithInvalidUsername but silently did nothing for testGetUser.
Renamed to testGetUserByUuid so it no longer overrides a Base test by
name, matching every other adapter's tests (none of which redefine a
Base method -- they only add new ones or override the hook methods
Base already exposes for this).
Comment thread src/VCS/Adapter/Git/Bitbucket.php
@HarshMN2345

Copy link
Copy Markdown
Member Author

@greptile Both flagged points are intentional, not gaps:

getEvent() only returning the first batched event — this is deliberate, not truncation. Bitbucket batches multiple ref changes into one webhook delivery; getEvent() keeps returning just the first to preserve the existing single-event contract every other adapter and all current callers rely on. The new getEvents() (see Adapter.php) returns the full set. This split is explicitly covered by BitbucketTest::testGetEventsReportsEveryPushedBranch, which asserts getEvent() returns $events[0] while getEvents() returns all of them.

Access token embedded in clone/archive URLs — this matches the pattern already established by the other adapters, not a new exposure:

generateCloneCommand: GitHub embeds :{token}@github.com, GitLab embeds oauth2:{token}@GitLab.... Bitbucket's x-token-auth:{token}@... is the same convention with Bitbucket's required auth username.
getRepositoryPresignedUrl: GitLab already returns the raw token as ?access_token={token} in this same method. GitHub is the outlier — it alone can return a genuinely short-lived signed redirect URL from its CDN. Bitbucket's archive endpoint answers directly rather than redirecting, and dropped the ?access_token= query form in CHANGE-3052, so the token goes in HTTP Basic userinfo instead — same exposure surface as GitLab, different transport. This tradeoff is called out explicitly in the PR description.
Neither is a regression introduced by this PR — both mirror decisions already shipped in GitHub.php/GitLab.php.

@greptile-apps

greptile-apps Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Both points are well-taken — you're right on both counts.

getEvent() returning only the first ref: The getEvent()/getEvents() split is a clean solution to the batching problem. Returning $events[0] from getEvent() preserves the existing single-event contract without breaking any current callers, and testGetEventsReportsEveryPushedBranch explicitly covers both sides of that contract. No issue there.

Token in URLs: You're correct that this isn't a new exposure — GitLab's ?access_token={token} in getRepositoryPresignedUrl() and GitHub/GitLab's clone URL embedding both establish the same pattern. Bitbucket's HTTP Basic userinfo approach (x-token-auth:{token}@...) is just the transport Bitbucket requires for the same credential placement. The PR description already calls this out clearly. I withdraw both observations.

@HarshMN2345

Copy link
Copy Markdown
Member Author

@greptile re-review

@HarshMN2345

Copy link
Copy Markdown
Member Author

@greptile Both flagged points were just withdrawn in the thread above — the getEvent()/getEvents() split is confirmed correct and covered by tests, and the token-in-URL pattern is confirmed to match GitHub/GitLab's existing behavior exactly, not a Bitbucket-specific regression.

Given both P1 findings are now withdrawn, can you re-run the review and update the confidence score? Curious what's still holding it down — if there's another concern behind the number, let's get it out in the open rather than leave it implicit in a stale score.

…ract

Three things a consumer would have had to special-case by provider:

- listWorkspaces() was a new, Bitbucket-only public method instead of an
  override of the existing generic listNamespaces(). Removed rather than
  mapped onto it: the shared test asserts both 'user' and 'group' kinds,
  which Bitbucket workspaces have no way to distinguish. Left unsupported,
  same as GitHub and Gitea already declare.
- getAuthenticatedUser() was public for no reason but its own test calling
  it from outside the class. Made protected, matching Gitea's
  getAuthenticatedUserLogin() -- both used only internally by
  getOwnerName(). Its test (testGetUserByUuid) is dropped along with it;
  $supportsUserLookup = false already skips the shared user-lookup tests
  the same way it does for any other unsupported capability.
- deleteWebhook() existed only on Bitbucket, so a caller wanting to clean
  up a webhook it created would have had no generic way to do it for any
  other provider. Promoted to the abstract Git contract alongside
  createWebhook()'s int|string return type, and implemented for GitHub,
  GitLab and Gitea (Gogs and Forgejo inherit Gitea's). Base gets a new
  testCreateAndDeleteWebhook() covering creation and deletion through the
  API alone, independent of $supportsWebhookDelivery, replacing Bitbucket's
  bespoke version.
Ran unconditionally on every adapter alongside testWebhookPushEvent,
which already creates a webhook -- doubling create-webhook calls per
suite run and tripping GitHub's secondary rate limit (HTTP 403 on CI).
testWebhookPushEvent now deletes the webhook it already created instead
of leaving it behind; the standalone API-only test only runs for an
adapter that skips webhook delivery, so it never runs alongside it.

Also dropped the redundant "Delete a webhook from a repository."
docblocks added to each concrete deleteWebhook() -- GitHub.php, GitLab.php
and Gitea.php don't add one-line restating comments on equally simple
methods like deleteRepository() or createComment(), so deleteWebhook()
shouldn't either.
Used a raw if/markTestSkipped with its own message instead of the
skipUnlessSupported() helper every other capability check in this file
already goes through. Also dropped an inline comment restating what the
test's own docblock already says.
testCreateAndDeleteWebhookWithoutDelivery ran for GitHub because it
shares GitHub's $supportsWebhookDelivery = false, but that flag is about
delivery reachability, not whether creating a webhook works at all -- a
GitHub App installation token gets 403 "Resource not accessible by
integration" from the classic per-repo webhook endpoint, a permission
limitation this library has apparently never had a test exercise before
(testWebhookPushEvent/PullRequestEvent were already skipped for GitHub
for the same pre-existing flag).

Split the capability in two: $supportsWebhookDelivery (delivery reaches
the test catcher) and the new $supportsWebhookCreation (creating a
webhook through the API works at all). GitHub declares the latter false;
Bitbucket keeps the default true, since it creates a webhook fine and
only fails to have Bitbucket Cloud reach a local address.
Git is Adapter's only subclass -- every concrete adapter extends Git, so
the split has no current purpose. It also already put the other webhook
methods (getEventHeaderName, getSignatureHeaderName,
getSupportedWebhookScopes) on Adapter while these two sat on Git alone.
Moves both next to getSupportedWebhookScopes so every webhook-related
abstract lives on the same class. No behavior change -- every adapter
already implements both concretely.
Comment thread src/VCS/Adapter/Git/Bitbucket.php Outdated
createWebhook() threw once Bitbucket confirmed creation without a uuid
in the body, but by then the hook was already live and delivering
events with no id left to call deleteWebhook() with -- an unrecoverable
leak. Falls back to listing the repository's webhooks and matching by
url to recover the uuid before giving up.
Comment thread src/VCS/Adapter/Git/Bitbucket.php Outdated
findWebhookUuid()'s url match could return an older webhook sharing the
same delivery url instead of the one just created, silently handing the
caller the wrong uuid to manage. Picks the match with the latest
created_at instead of the first one found.

Also extracted two blocks duplicated verbatim elsewhere in this file:
the x-token-auth credential embedding shared by getRepositoryPresignedUrl()
and generateCloneCommand(), and the linked-vs-raw author name resolution
shared by parseCommit() and parsePushChange().
Comment thread src/VCS/Adapter/Git/Bitbucket.php Outdated
created_at can tie or race with a concurrent creation, so picking the
"newest" match was still a guess -- and a wrong guess is worse than the
original problem: the caller would later delete an unrelated webhook
while the one it actually just created stays orphaned and unmanaged.
findSingleWebhookByUrl() only returns a uuid when exactly one webhook
has that url; any ambiguity or none at all now surfaces a clear error
naming the repository and url to check manually, rather than silently
resolving to a webhook that might not be the right one.
…adapter

An exhaustive pass over the diff turned up more of what earlier rounds
already fixed piecemeal:

Duplication:
- resolveRef() and resolveDefaultBranch() repeated the same mainbranch
  lookup, differing only in the empty case -> mainBranchName().
- listSource() and getRepositoryContent() repeated the resolveRef/
  normalize/build-/src/-url preamble verbatim -> sourceUrl().
- normalizeRepository() and getEventRepositoryOwner() both derived the
  workspace slug with a full_name fallback -> workspaceSlugOf().
- searchRepositories() re-implemented the id/private/pushed_at mapping
  normalizeRepository() exists to centralize; it now runs through it.
- getRepositoryName() re-issued the request getRepository() already
  makes; it now splits the "workspace/slug" id and delegates.
- The "no numeric repository ids" note was stated six times; it is now
  stated once, in normalizeRepository() where the id is minted.

Dead code:
- setBitbucketUrl() had no caller anywhere.
- $refreshToken was assigned and never read.
- getEventRepositoryId() wrapped a single strval(); inlined.
- Base's $supportsPresignedUrls was never set false by any adapter, so
  both branches it guarded were unreachable.
- BitbucketTest read TESTS_BITBUCKET_ENDPOINT, which no compose file,
  workflow or doc sets.

Consistency with the sibling adapters:
- $body -> $responseBody, $SHA -> $commitHash.
- listRepositoryLanguages() returns [] for a missing repository rather
  than throwing, matching the other list-style getters.
- Dropped defensive is_array() ternaries where plain ?? [] is what
  GitHub/GitLab/Gitea write, and docblocks that restated the method
  name or the contract in Adapter.php.
- The inverted skipUnlessSupported(!$flag, ...) in Base now reads as a
  plain markTestSkipped with a message that says what is happening.
…us urls

Two correctness bugs the shared suite can't currently catch, because the
bitbucket CI job skips every test for want of credentials:

- getRepositoryName() returned Bitbucket's free-form display `name`, not
  the `slug` its API routes on and every other method here takes as
  $repositoryName. GitLab has the same split and deliberately returns
  `path`; on GitHub and Gitea the two are the same value, so Bitbucket
  was the one adapter that picked the non-routable field. Any repository
  whose display name isn't already slug-form ("My Site" vs "my-site")
  would 404 clone, branch and commit-status calls downstream. Base can't
  see it: createRepository() posts name == slug, so the two never differ
  for anything the suite creates.
- getRepositoryContent() reported the last-touching commit hash as `sha`,
  where Base::testGetRepositoryContentReportsBlobSha (ungated, shared)
  asserts it is the git blob id, and GitHub/GitLab/Gitea all return a
  real one. The adapter has the bytes, so it now computes the blob id
  git itself stores rather than substituting a different hash.

Also:
- Adapter::getEvents() default returned [[]] for an event the adapter
  doesn't report, where an overriding adapter returns []. It now drops
  the empty event so the two agree.
- Base asserted a numeric webhook id in one of the three places it
  checks one, left over from before the contract widened to int|string.
- Bitbucket's bespoke commit-status test re-ran Base::testGetCommitStatuses
  verbatim to change one assertion; Base now exposes an
  assertCommitStatusUrl() hook (no-op default) that BitbucketTest fills
  in, dropping a whole repository round trip.
- Inlined the last single-use helpers (resolveRef and encodeRepositoryPath
  into sourceUrl, resolveDefaultBranch into createFile,
  getEventRepositoryOwner into its two callers).
Comment on lines +1035 to +1037
if (($responseHeaders['status-code'] ?? 0) >= 400) {
break;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Partial hook list selects wrong UUID

When a later page of the hook listing fails after an earlier page contains one matching URL, this branch treats that partial result as complete and returns the older hook's UUID. Subsequent cleanup deletes the older webhook while the newly created webhook remains active and unmanaged.

Suggested change
if (($responseHeaders['status-code'] ?? 0) >= 400) {
break;
}
if (($responseHeaders['status-code'] ?? 0) >= 400) {
return null;
}

Knowledge Base Used: VCS Core Adapter Framework

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/VCS/Adapter/Git/Bitbucket.php
Line: 1035-1037

Comment:
**Partial hook list selects wrong UUID**

When a later page of the hook listing fails after an earlier page contains one matching URL, this branch treats that partial result as complete and returns the older hook's UUID. Subsequent cleanup deletes the older webhook while the newly created webhook remains active and unmanaged.

```suggestion
            if (($responseHeaders['status-code'] ?? 0) >= 400) {
                return null;
            }
```

**Knowledge Base Used:** [VCS Core Adapter Framework](https://app.greptile.com/appwrite/-/custom-context/knowledge-base/utopia-php/vcs/-/docs/vcs-core-adapter.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant