# Forgejo Labels System

## Overview

Forgejo has a two-tier label system:

1. **Repo-level labels** — Scoped to a single repository. Created and managed via `/repos/{owner}/{repo}/labels`.
2. **Org-level labels** — Scoped to an organization. Created via `/orgs/{org}/labels` and automatically inherited by ALL repositories in that organization.

Both types coexist. When viewing an issue, you see repo labels AND org labels together. They have separate ID spaces — an org label with `id: 5` is a different label than a repo label with `id: 5`.

## Label Object Structure

Every label (repo or org) has the same shape:

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Unique identifier (unique within its scope) |
| `name` | string | Display name (e.g., `"bug"`, `"State/Open"`) |
| `color` | string | 6-character hex color **without** `#` prefix (e.g., `"00aabb"`) |
| `description` | string | Optional description |
| `exclusive` | boolean | Whether this label is part of an exclusive group |
| `is_archived` | boolean | Whether this label is archived (hidden from new selections) |
| `url` | string | API URL for this label |

## Exclusive Labels

When `exclusive` is `true` and the label name contains a `/` separator, Forgejo treats the prefix as a **scope group**. Only one label from a scope group can be active on an issue at a time.

**Example exclusive label set:**
- `Priority/Low` (exclusive: true)
- `Priority/Medium` (exclusive: true)
- `Priority/High` (exclusive: true)
- `State/Open` (exclusive: true)
- `State/In Progress` (exclusive: true)
- `State/Closed` (exclusive: true)

**Behavior:** Applying `Priority/High` to an issue that already has `Priority/Low` will automatically remove `Priority/Low`. The scope is the part before the `/` — so `Priority/*` labels conflict with each other, but NOT with `State/*` labels.

**Important:** The automatic removal only works through the Forgejo UI. When using the API, you must manage exclusive label conflicts yourself:
- Use `PUT` (replace all labels) instead of `POST` (add labels) for safer exclusive label handling
- Or explicitly remove the conflicting label before adding the new one

## Applying Labels: IDs vs Names

Labels can be referenced in two ways depending on the endpoint:

- **Label management endpoints** (`GET/PATCH/DELETE /labels/{id}`) use the **integer ID**
- **Issue label endpoints** (`POST/PUT/DELETE /issues/{index}/labels`) accept the **IssueLabelsOption** body, where the `labels` field takes EITHER:
  - An array of **integer IDs**: `{"labels": [1, 5, 10]}`
  - An array of **string names**: `{"labels": ["bug", "enhancement"]}`
  - Do NOT mix types in the same array

## Decision Tree for Label Operations

```
What do you want to do?
│
├─ Manage label definitions (CRUD)?
│  ├─ For a single repo?
│  │  └─ Use /repos/{owner}/{repo}/labels endpoints
│  │     See: repo-labels.md
│  └─ For an entire org?
│     └─ Use /orgs/{org}/labels endpoints
│        See: org-labels.md
│
├─ Apply/remove labels on an issue or PR?
│  └─ Use /repos/{owner}/{repo}/issues/{index}/labels endpoints
│     See: issue-pr-labels.md
│     │
│     ├─ Adding labels to existing set?
│     │  └─ POST with IssueLabelsOption
│     ├─ Replacing ALL labels?
│     │  └─ PUT with IssueLabelsOption (safest for exclusive labels)
│     ├─ Removing one label?
│     │  └─ DELETE /labels/{identifier}
│     └─ Removing all labels?
│        └─ DELETE /labels with empty body or IssueLabelsOption
│
├─ Read labels on an issue or PR?
│  └─ GET the issue/PR itself — labels are in the response `labels` array
│     There is no separate "list issue labels" endpoint
│
└─ Use a predefined label template?
   └─ Use /label/templates endpoints
      See: label-templates.md
```

## Related Files

- [repo-labels.md](./repo-labels.md) — CRUD for repo-level labels
- [org-labels.md](./org-labels.md) — CRUD for org-level labels
- [issue-pr-labels.md](./issue-pr-labels.md) — Applying/removing labels on issues and PRs
- [label-templates.md](./label-templates.md) — Predefined label template sets
