|
| 1 | +--- |
| 2 | +title: Configuration |
| 3 | +weight: 6 |
| 4 | +--- |
| 5 | + |
| 6 | +`internal/config` turns a YAML file into the structs every later stage reads. It is deliberately |
| 7 | +three separate concerns in three files, and only the first has landed: |
| 8 | + |
| 9 | +| File | Concern | Status | |
| 10 | +|---------------|---------------------------------------------------|---------| |
| 11 | +| `config.go` | Find the file, parse it, normalise it | landed | |
| 12 | +| `validate.go` | The rules in the design's validation table | planned | |
| 13 | +| `resolve.go` | Groups → repository sets, `include_groups` cycles | planned | |
| 14 | + |
| 15 | +Nothing in the package touches the network, and nothing in `config.go` rejects a config: an |
| 16 | +invalid colour parses into the struct exactly as written and is `validate.go`'s answer to give. |
| 17 | +Splitting it that way keeps the error a user sees attached to the rule it broke rather than to a |
| 18 | +decoder. |
| 19 | + |
| 20 | +## Finding the file |
| 21 | + |
| 22 | +```go |
| 23 | +func Find(explicit string) (string, error) // path only |
| 24 | +func Load(explicit string) (*Config, error) // Find + LoadFile |
| 25 | +func LoadFile(path string) (*Config, error) // read + Parse, records Config.Path |
| 26 | +func Parse(data []byte) (*Config, error) // decode + normalise, no I/O |
| 27 | +``` |
| 28 | + |
| 29 | +`Find` resolves in this order: |
| 30 | + |
| 31 | +1. `explicit` — the `--config` value, when one was given |
| 32 | +2. `labels.yml` or `labels.yaml` in the working directory |
| 33 | +3. `labels.yml` or `labels.yaml` under `ConfigDir()` — `$XDG_CONFIG_HOME/labelsync` |
| 34 | + |
| 35 | +A `--config` value naming a **directory** is searched the same way as the other two, so |
| 36 | +`--config ~/.config/labelsync` and `--config ~/.config/labelsync/labels.yml` both work. |
| 37 | + |
| 38 | +Two outcomes are sentinels, wrapped with `%w` so `KindOf` can name them in JSON output: |
| 39 | + |
| 40 | +| Situation | Sentinel | |
| 41 | +|-----------------------------------------------|--------------------------| |
| 42 | +| Both spellings in the same directory | `ErrAmbiguousConfigFile` | |
| 43 | +| No config file at the path, or in any of them | `ErrConfigNotFound` | |
| 44 | + |
| 45 | +Ambiguity is checked **per directory, while searching it**. A working directory holding both |
| 46 | +spellings is an error even though the XDG directory holds a perfectly good file — a run whose |
| 47 | +config depends on which name the tool happened to look at first is worse than a run that stops. |
| 48 | +This is the rule specs-cli applies to `project.yml` / `project.yaml`. |
| 49 | + |
| 50 | +A YAML syntax error is not one of the run's failure modes and carries no sentinel. It is wrapped |
| 51 | +with the file it came from, so `KindOf` returns `""` and the message still says where to look. |
| 52 | + |
| 53 | +## Normalisation |
| 54 | + |
| 55 | +`Parse` normalises before it returns, so no later stage has to ask whether a colour still has its |
| 56 | +`#` or whether a label's groups have been filled in. |
| 57 | + |
| 58 | +| Input | Normalised to | |
| 59 | +|---------------------------------|-------------------------------------------------------| |
| 60 | +| `color: "#D73A4A"` | `d73a4a` — one leading `#` stripped, then lowercased | |
| 61 | +| `name: " type: bug "` | `type: bug` — trimmed | |
| 62 | +| `from` / `to` in a rename | Trimmed, because they are matched against label names | |
| 63 | +| A label with no `groups` | A copy of `defaults.groups` | |
| 64 | +| A group with no `skip_archived` | `true` | |
| 65 | +| A group with no `skip_forks` | `true` | |
| 66 | +| A group with no `visibility` | `all` | |
| 67 | + |
| 68 | +Only **one** leading `#` is stripped: `##abc` normalises to `#abc`, which is invalid, which is the |
| 69 | +point. Normalisation tidies, it never repairs — a repaired colour would silently apply a colour |
| 70 | +the file does not name. |
| 71 | + |
| 72 | +Inherited groups are **cloned per label** rather than aliased to the one `defaults.groups` slice. |
| 73 | +Sharing a backing array across every defaulted label is the kind of thing that works until the |
| 74 | +first stage that filters a label's groups in place. |
| 75 | + |
| 76 | +### Why group defaults are applied while decoding |
| 77 | + |
| 78 | +`skip_archived` and `skip_forks` default to `true`, which cannot be done after decoding: once the |
| 79 | +struct is filled in, an explicit `skip_archived: false` and an omitted `skip_archived` are both |
| 80 | +`false` and nothing can tell them apart. So `Group.UnmarshalYAML` decodes over a defaulted value |
| 81 | +instead of over a zero one. |
| 82 | + |
| 83 | +That alone is not quite enough. A group written as a bare key — |
| 84 | + |
| 85 | +```yaml |
| 86 | +groups: |
| 87 | + self: |
| 88 | +``` |
| 89 | + |
| 90 | +— is a YAML null, and `yaml.v3` resolves a null to the zero value *without* consulting the |
| 91 | +target's own `UnmarshalYAML`. The defaults would be zeroed straight back out. `Groups` therefore |
| 92 | +has a decoder of its own that walks the mapping entry by entry and hands a null entry the defaults |
| 93 | +directly. Both paths are pinned by tests. |
| 94 | + |
| 95 | +### Descriptions |
| 96 | + |
| 97 | +`Label.Description` is a plain `string`, not a `*string`, and that is a decision rather than an |
| 98 | +omission. The design defines descriptions as **authoritative**: omitting one *clears* the remote |
| 99 | +description. An omitted description and an explicitly empty one therefore describe the same |
| 100 | +desired state, and nothing downstream would do anything different with the distinction. |
| 101 | + |
| 102 | +If a future mode ever means "leave descriptions alone", that is the change that introduces the |
| 103 | +pointer — not this one. |
| 104 | + |
| 105 | +## Testing |
| 106 | + |
| 107 | +- **Resolution** is table-driven over every branch of the order, including both ambiguity cases |
| 108 | + and both not-found cases, with the working directory and `XDG_CONFIG_HOME` pointed at temporary |
| 109 | + directories. Every error case also asserts `KindOf` still names the sentinel through the |
| 110 | + wrapping. |
| 111 | +- **Normalisation** is pinned by golden files: `testdata/*.yml` in, the normalised struct |
| 112 | + marshalled back to YAML out. A change to any defaulting or tidying rule shows up as a diff in |
| 113 | + the golden rather than as a subtly different plan several stages later. |
| 114 | +
|
| 115 | +```sh |
| 116 | +task test:update # rewrite the goldens, here and in every other golden package |
| 117 | +``` |
0 commit comments