# Issue & PR Labels

Endpoints for applying, replacing, and removing labels on issues and pull requests. In Forgejo, PRs are a superset of issues, so **all issue label endpoints work identically for PRs** — just use the PR's index number.

## Reading Labels on an Issue/PR

### Dedicated GET endpoint

```
GET /repos/{owner}/{repo}/issues/{index}/labels
```

Returns an array of all labels applied to the issue/PR.

```bash
# List labels on an issue using the dedicated endpoint
curl -s \
  -H "Authorization: token ${FORGEJO_PAT}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels"
```

### Alternative: extract from issue object

Labels are also embedded in the issue/PR response object itself:

```bash
# Get an issue — labels are in the response's "labels" array
curl -s \
  -H "Authorization: token ${FORGEJO_PAT}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}" | jq '.labels'
```

Each label in the response array has this shape:
```json
{
  "id": 7,
  "name": "bug",
  "color": "{color}",
  "description": "{label_description}",
  "exclusive": false,
  "is_archived": false,
  "url": "https://{FORGEJO_HOST}/api/v1/repos/{owner}/{repo}/labels/{label_id}"
}
```

## Endpoints

### Add labels to an issue

```
POST /repos/{owner}/{repo}/issues/{index}/labels
```

**Adds** labels to the issue's existing label set. Does not remove any existing labels.

**Body: IssueLabelsOption**

| Field | Type | Description |
|-------|------|-------------|
| `labels` | array | Array of integer IDs **OR** array of string names (do not mix) |
| `updated_at` | string | Optional ISO 8601 timestamp |

```bash
# Add labels by ID
curl -s -X POST \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d '{
    "labels": [1, 5, 10]
  }' \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels"

# Add labels by name
curl -s -X POST \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d '{
    "labels": ["bug", "Priority/High"]
  }' \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels"
```

### Replace ALL labels on an issue

```
PUT /repos/{owner}/{repo}/issues/{index}/labels
```

**Replaces** the entire label set. Any labels not in the new array are removed.

**Body: IssueLabelsOption** (same schema as POST)

This is the **safest method** for managing exclusive labels because you provide the complete desired state rather than incremental changes.

```bash
# Replace all labels — issue will have ONLY these labels afterward
curl -s -X PUT \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d '{
    "labels": ["bug", "Priority/High", "Status/In Progress"]
  }' \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels"

# Replace using IDs
curl -s -X PUT \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d '{
    "labels": [1, 7, 15]
  }' \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels"
```

### Remove ALL labels from an issue

```
DELETE /repos/{owner}/{repo}/issues/{index}/labels
```

Removes every label from the issue.

```bash
curl -s -X DELETE \
  -H "Authorization: token ${FORGEJO_PAT}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels"
```

### Remove a single label from an issue

```
DELETE /repos/{owner}/{repo}/issues/{index}/labels/{identifier}
```

**Path parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `identifier` | integer or string | Label ID (integer) or label name (string, URL-encoded if needed) |

```bash
# Remove by ID
curl -s -X DELETE \
  -H "Authorization: token ${FORGEJO_PAT}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels/{label_id}"

# Remove by name
curl -s -X DELETE \
  -H "Authorization: token ${FORGEJO_PAT}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels/bug"

# Remove by name with slash (URL-encode the slash)
curl -s -X DELETE \
  -H "Authorization: token ${FORGEJO_PAT}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels/Priority%2FHigh"
```

## Handling Exclusive Label Conflicts

The Forgejo API does **not** automatically enforce exclusive label constraints. If you POST `Priority/High` to an issue that already has `Priority/Low`, the issue will end up with **both** labels. You must handle this yourself.

### Strategy 1: Use PUT (Replace) — Recommended

Build the complete desired label set and replace everything at once.

```bash
# Step 1: Get current labels
CURRENT=$(curl -s \
  -H "Authorization: token ${FORGEJO_PAT}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}" | jq '[.labels[].name]')

# Step 2: Filter out conflicting scope, add new label
# (Using jq to remove any "Priority/*" labels, then add "Priority/High")
NEW_LABELS=$(echo "$CURRENT" | jq '[.[] | select(startswith("Priority/") | not)] + ["Priority/High"]')

# Step 3: Replace all labels
curl -s -X PUT \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d "{\"labels\": ${NEW_LABELS}}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels"
```

### Strategy 2: Remove then Add

```bash
# Remove the old exclusive label first
curl -s -X DELETE \
  -H "Authorization: token ${FORGEJO_PAT}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels/Priority%2FLow"

# Then add the new one
curl -s -X POST \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d '{"labels": ["Priority/High"]}' \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels"
```

### Strategy 3: Programmatic Exclusive Label Management

When working with exclusive labels programmatically, extract the scope prefix and remove all labels sharing that prefix before applying the new one:

```bash
# Given: want to apply "State/In Progress" to issue 42
NEW_LABEL="State/In Progress"
SCOPE="${NEW_LABEL%%/*}"  # extracts "State"

# Get current label names
LABELS=$(curl -s \
  -H "Authorization: token ${FORGEJO_PAT}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}" | jq -r '.labels[].name')

# Remove any label with matching scope
echo "$LABELS" | while read -r label; do
  if [[ "$label" == "${SCOPE}/"* && "$label" != "$NEW_LABEL" ]]; then
    encoded=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$label', safe=''))")
    curl -s -X DELETE \
      -H "Authorization: token ${FORGEJO_PAT}" \
      "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels/${encoded}"
  fi
done

# Add the new label
curl -s -X POST \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d "{\"labels\": [\"${NEW_LABEL}\"]}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels"
```

## Working with PR Labels

PRs use the exact same endpoints. Just use the PR number as the `{index}`:

```bash
# Add labels to PR #15
curl -s -X POST \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d '{"labels": ["needs-review", "Priority/Medium"]}' \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels"

# Read labels from PR #15
curl -s \
  -H "Authorization: token ${FORGEJO_PAT}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}" | jq '.labels'
```

## Quick Reference

| Operation | Method | Endpoint | Body |
|-----------|--------|----------|------|
| Read labels | GET | `/repos/{owner}/{repo}/issues/{index}` | — (labels in `.labels`) |
| Add labels | POST | `/repos/{owner}/{repo}/issues/{index}/labels` | `IssueLabelsOption` |
| Replace all labels | PUT | `/repos/{owner}/{repo}/issues/{index}/labels` | `IssueLabelsOption` |
| Remove all labels | DELETE | `/repos/{owner}/{repo}/issues/{index}/labels` | — |
| Remove one label | DELETE | `/repos/{owner}/{repo}/issues/{index}/labels/{id_or_name}` | — |
