# Bitbucket Client

Manage repositories, pull requests, and interact with Bitbucket's APIs.

## Methods

| Method                                   | Description                                           |
| ---------------------------------------- | ----------------------------------------------------- |
| `apiRequest(options, schema, metadata?)` | Make any Bitbucket API request with schema validation |

## Usage

### List Repositories

```typescript
import { api, z, bitbucket } from "@superblocksteam/sdk-api";

// Integration ID from the integrations panel
const PROD_BITBUCKET = "a1b2c3d4-5678-90ab-cdef-bitbucke0001";

const RepositorySchema = z.object({
  uuid: z.string(),
  name: z.string(),
  full_name: z.string(),
  description: z.string().nullable(),
  is_private: z.boolean(),
  links: z.object({
    html: z.object({ href: z.string() }),
    clone: z.array(z.object({ href: z.string(), name: z.string() })),
  }),
  mainbranch: z.object({ name: z.string() }).optional(),
});

const ListReposResponseSchema = z.object({
  values: z.array(RepositorySchema),
  page: z.number(),
  size: z.number(),
  pagelen: z.number(),
  next: z.string().optional(),
});

export default api({
  name: "BitbucketExample",
  integrations: {
    bitbucket: bitbucket(PROD_BITBUCKET),
  },
  input: z.object({
    workspace: z.string(),
  }),
  output: z.object({
    repos: z.array(z.object({ name: z.string(), url: z.string() })),
  }),
  async run(ctx, { workspace }) {
    const result = await ctx.integrations.bitbucket.apiRequest(
      {
        method: "GET",
        path: `/repositories/${workspace}`,
        params: {
          pagelen: 50,
        },
      },
      { response: ListReposResponseSchema },
    );

    return {
      repos: result.values.map((r) => ({
        name: r.name,
        url: r.links.html.href,
      })),
    };
  },
});
```

### Get Repository

```typescript
const repo = await ctx.integrations.bitbucket.apiRequest(
  {
    method: "GET",
    path: `/repositories/${workspace}/${repoSlug}`,
  },
  { response: RepositorySchema },
);

console.log(`${repo.full_name}: ${repo.is_private ? "private" : "public"}`);
```

### Create a Pull Request

```typescript
const PullRequestSchema = z.object({
  id: z.number(),
  title: z.string(),
  description: z.string().nullable(),
  state: z.string(),
  source: z.object({
    branch: z.object({ name: z.string() }),
  }),
  destination: z.object({
    branch: z.object({ name: z.string() }),
  }),
  links: z.object({
    html: z.object({ href: z.string() }),
  }),
  author: z.object({
    display_name: z.string(),
  }),
});

const pr = await ctx.integrations.bitbucket.apiRequest(
  {
    method: "POST",
    path: `/repositories/${workspace}/${repoSlug}/pullrequests`,
    body: {
      title: "Add new feature",
      description: "This PR adds...",
      source: {
        branch: { name: "feature-branch" },
      },
      destination: {
        branch: { name: "main" },
      },
      close_source_branch: true,
    },
  },
  { response: PullRequestSchema },
);

console.log(`Created PR #${pr.id}: ${pr.links.html.href}`);
```

### List Pull Requests

```typescript
const ListPRsResponseSchema = z.object({
  values: z.array(PullRequestSchema),
  page: z.number(),
  size: z.number(),
  next: z.string().optional(),
});

const result = await ctx.integrations.bitbucket.apiRequest(
  {
    method: "GET",
    path: `/repositories/${workspace}/${repoSlug}/pullrequests`,
    params: {
      state: "OPEN", // OPEN, MERGED, DECLINED, SUPERSEDED
      pagelen: 25,
    },
  },
  { response: ListPRsResponseSchema },
);

result.values.forEach((pr) => {
  console.log(`#${pr.id}: ${pr.title} (${pr.state})`);
});
```

### Merge a Pull Request

```typescript
const MergeResponseSchema = z.object({
  state: z.string(),
  merge_commit: z
    .object({
      hash: z.string(),
    })
    .optional(),
});

const result = await ctx.integrations.bitbucket.apiRequest(
  {
    method: "POST",
    path: `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/merge`,
    body: {
      type: "pullrequest",
      message: "Merge pull request #123",
      close_source_branch: true,
      merge_strategy: "squash", // merge_commit, squash, fast_forward
    },
  },
  { response: MergeResponseSchema },
);
```

### List Commits

```typescript
const CommitSchema = z.object({
  hash: z.string(),
  message: z.string(),
  date: z.string(),
  author: z.object({
    raw: z.string(),
    user: z
      .object({
        display_name: z.string(),
      })
      .optional(),
  }),
});

const ListCommitsResponseSchema = z.object({
  values: z.array(CommitSchema),
  next: z.string().optional(),
});

const result = await ctx.integrations.bitbucket.apiRequest(
  {
    method: "GET",
    path: `/repositories/${workspace}/${repoSlug}/commits`,
    params: {
      branch: "main",
      pagelen: 10,
    },
  },
  { response: ListCommitsResponseSchema },
);
```

### Create Branch

```typescript
const BranchSchema = z.object({
  name: z.string(),
  target: z.object({
    hash: z.string(),
  }),
});

const branch = await ctx.integrations.bitbucket.apiRequest(
  {
    method: "POST",
    path: `/repositories/${workspace}/${repoSlug}/refs/branches`,
    body: {
      name: "feature/new-feature",
      target: {
        hash: "main", // Can be commit hash or branch name
      },
    },
  },
  { response: BranchSchema },
);
```

### Get File Contents

```typescript
// Note: Returns raw file content, not JSON
const content = await ctx.integrations.bitbucket.apiRequest(
  {
    method: "GET",
    path: `/repositories/${workspace}/${repoSlug}/src/main/README.md`,
  },
  { response: z.string() },
);
```

### Add Comment to Pull Request

```typescript
const CommentSchema = z.object({
  id: z.number(),
  content: z.object({
    raw: z.string(),
  }),
  created_on: z.string(),
  user: z.object({
    display_name: z.string(),
  }),
});

const comment = await ctx.integrations.bitbucket.apiRequest(
  {
    method: "POST",
    path: `/repositories/${workspace}/${repoSlug}/pullrequests/${prId}/comments`,
    body: {
      content: {
        raw: "LGTM! :+1:",
      },
    },
  },
  { response: CommentSchema },
);
```

## Trace Metadata

All methods accept an optional `metadata` parameter as the last argument for diagnostics labeling. See the [root SDK README](../../../README.md#trace-metadata) for details.

## Common Pitfalls

### No Specialized Methods

```typescript
// WRONG - These methods do not exist
await bitbucket.createPullRequest({ ... });
await bitbucket.listRepos();

// CORRECT - Use apiRequest
await ctx.integrations.bitbucket.apiRequest(
  { method: "POST", path: `/repositories/${workspace}/${repo}/pullrequests`, body: { ... } },
  { response: PullRequestSchema }
);
```

### API Version in Path

The base URL already includes `/2.0`, so do not repeat it in paths:

```typescript
// CORRECT - No version prefix (base URL already has /2.0)
const path = `/repositories/${workspace}/${repo}`;

// WRONG - Duplicates version from base URL
const path = `/2.0/repositories/${workspace}/${repo}`;
```

### Workspace vs Username

Use workspace slug for API calls:

```typescript
// Workspace slug (preferred)
const path = `/repositories/myworkspace/myrepo`;

// Can also be username for personal repos
const path = `/repositories/myusername/myrepo`;
```

### Pagination

Bitbucket uses `pagelen` and `next` URL:

```typescript
async function getAllPRs(
  bitbucket: BitbucketClient,
  workspace: string,
  repo: string,
) {
  const allPRs: PullRequest[] = [];
  let url = `/repositories/${workspace}/${repo}/pullrequests`;

  while (url) {
    const result = await ctx.integrations.bitbucket.apiRequest(
      {
        method: "GET",
        path: url,
        params: url.includes("?") ? {} : { pagelen: 50 },
      },
      { response: ListPRsResponseSchema },
    );

    allPRs.push(...result.values);
    url = result.next ?? "";
  }

  return allPRs;
}
```

### PR State Values

PR states are uppercase:

```typescript
const states = ["OPEN", "MERGED", "DECLINED", "SUPERSEDED"];

// WRONG
params: {
  state: "open";
}

// CORRECT
params: {
  state: "OPEN";
}
```

### Branch Names in Paths

Branch names with slashes need encoding:

```typescript
// Branch name: feature/my-feature
const branchName = encodeURIComponent("feature/my-feature");
const path = `/repositories/${workspace}/${repo}/src/${branchName}/file.txt`;
```

### Raw File Content

File content endpoints return raw text, not JSON:

```typescript
// This returns raw file content as string
const content = await ctx.integrations.bitbucket.apiRequest(
  {
    method: "GET",
    path: `/repositories/${workspace}/${repo}/src/main/file.txt`,
  },
  { response: z.string() }, // Not a JSON schema
);
```

## Error Handling

```typescript
import { RestApiValidationError } from "@superblocksteam/sdk-api";

try {
  const result = await ctx.integrations.bitbucket.apiRequest(
    { method: "GET", path: `/repositories/${workspace}/${repo}` },
    { response: RepositorySchema },
  );
} catch (error) {
  if (error instanceof RestApiValidationError) {
    console.error("Validation failed:", error.details.zodError);
  }
}
```

## API Reference

- [Bitbucket REST API Documentation](https://developer.atlassian.com/cloud/bitbucket/rest/intro/)
- [Repositories](https://developer.atlassian.com/cloud/bitbucket/rest/api-group-repositories/)
- [Pull Requests](https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/)
- [Commits](https://developer.atlassian.com/cloud/bitbucket/rest/api-group-commits/)
