Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ the hub. See [Set up a hub's platform](guide/setup.md).
| `probe` | jobs any hub branch can start | — | exits 2 when the job can see a write credential or signing key |
| `setup github|gitlab [--dry-run] ...` | a maintainer's machine | `TOUCHMARK_HUB_TOKEN` | creates the reader and writer, stores the keys so only the default branch sees the write key, protects the hub; changes only what differs; exit 3 when a step is left to you |
| `migrate --from-multi-gitter FILE [--id ID] [--writer LOGIN]` | anywhere | reader, optional | prints `hub.yml`, `targets.yml` and `operations.yml` for a hub that replaces a multi-gitter setup on GitLab |
| `status [--hub DIR] [--dir DIR] [--repo REF] [--packs a,b]` | a target checkout | none | lists every managed path and its state; changes nothing |
| `status [--hub DIR] [--dir DIR] [--repo REF] [--packs a,b] [--assume-opt-in]` | a target checkout | none | lists every managed path and its state; changes nothing |
| `apply [status flags] [--adopt GLOB]... [--dry-run]` | a target checkout | none | applies the packs to the working tree; `--adopt` takes `local` files back |
| `manifest [--hub DIR]` | hub | none | prints the ownership manifest as JSON |
| `schema hub|targets|opt-in|operations|report|doctor|setup` | anywhere | none | prints a JSON Schema |
Expand Down
18 changes: 14 additions & 4 deletions docs/guide/local.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ touchmark apply --hub ~/src/engineering-assets --adopt '.agents/guidelines/**'
```

`--adopt` takes a glob and repeats. `ignore` in the opt-in file is stronger: an ignored
path is never adopted.
path is never adopted. In a repository the hub subscribes through an `org:` or `group:`
entry with `opt_in: assumed`, and that has no opt-in file, add `--assume-opt-in` (below).

## Organisations, groups and explicit packs

Expand All @@ -77,9 +78,18 @@ without the opt-in file:
- without `--repo` or an `origin` remote, touchmark cannot tell, and warns when
`targets.yml` subscribes anything.

An opt-in file in the checkout settles it either way; with `enabled: false` in it,
`status` prints `opted out` and `apply` writes nothing. In `--format json` the target's
`opt_in` says which: `file`, `assumed`, `opted-out` or `none`.
`--assume-opt-in` settles the last two cases here: a target without the opt-in file
counts as opted in, as such an entry would make it. `status` prints `opted in by
--assume-opt-in`, and `apply` writes the packs of `targets.yml`:

```sh
touchmark apply --hub ~/src/engineering-assets --assume-opt-in --adopt AGENTS.md
```

An opt-in file in the checkout settles it either way, whatever the flag; with
`enabled: false` in it, `status` prints `opted out` and `apply` writes nothing. In
`--format json` the target's `opt_in` says which: `file`, `assumed`, `flag` (opted in
by `--assume-opt-in`), `opted-out` or `none`.

## Try a pack before committing it

Expand Down
4 changes: 3 additions & 1 deletion docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,11 +238,12 @@ Flags:
<!-- generated: help status -->

```text
Usage: touchmark status [--hub DIR] [--dir DIR] [--repo REF] [--packs a,b] [--worktree] [--format text|json]
Usage: touchmark status [--hub DIR] [--dir DIR] [--repo REF] [--packs a,b] [--assume-opt-in] [--worktree] [--format text|json]

List every managed path of the target and its state; changes nothing.

Flags:
--assume-opt-in count the target as opted in when it has no opt-in file, as an org or group entry with opt_in: assumed that a local run cannot resolve would
--dir DIR the target working tree DIR (default: the current directory)
--format FORMAT output FORMAT: text or json (default text)
--hub DIR the hub checkout DIR (default $TOUCHMARK_HUB)
Expand All @@ -264,6 +265,7 @@ Apply the packs to the target's working tree.

Flags:
--adopt GLOB overwrite local files matching GLOB with the pack version (repeatable)
--assume-opt-in count the target as opted in when it has no opt-in file, as an org or group entry with opt_in: assumed that a local run cannot resolve would
--dir DIR the target working tree DIR (default: the current directory)
--dry-run print what apply would change without writing
--format FORMAT output FORMAT: text or json (default text)
Expand Down
3 changes: 2 additions & 1 deletion docs/reference/output.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,8 @@ appears in it. See [Set up a hub's platform](../guide/setup.md).
`--format json` prints the hub (its directory, `id` and commit), the target (its
directory, its reference in `targets.yml`, its opt-in file, whether it opted in, and
`opt_in`, which says why: `file`, the opt-in file is there; `assumed`, there is none and
a `repo:` entry with `opt_in: assumed` subscribes it; `opted-out`, the file says
a `repo:` entry with `opt_in: assumed` subscribes it; `flag`, there is none and
`--assume-opt-in` counts it as opted in; `opted-out`, the file says
`enabled: false`; `none`, neither), the
pack selection and where each pack came from, one entry per managed path with its
`state`, `action`, `pack` and blob ids, a `summary` counting each state, and `warnings`:
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func commands() []*command {
flags: probeFlags, run: runProbe},
migrateCommand(),
setupCommand(),
{name: "status", synopsis: "[--hub DIR] [--dir DIR] [--repo REF] [--packs a,b] [--worktree] [--format text|json]",
{name: "status", synopsis: "[--hub DIR] [--dir DIR] [--repo REF] [--packs a,b] [--assume-opt-in] [--worktree] [--format text|json]",
summary: "list every managed path of the target and its state; changes nothing",
flags: statusFlags, run: runStatus},
{name: "apply", synopsis: "[status flags] [--adopt GLOB]... [--dry-run]",
Expand Down
4 changes: 4 additions & 0 deletions internal/cli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ type options struct {
worktree bool
dryRun bool
format string
// assumeOptIn is status's and apply's --assume-opt-in (target.assume).
assumeOptIn bool
// only, hubFP and strict are plan's (plan.go).
only refList
hubFP fingerprintFlag
Expand Down Expand Up @@ -169,6 +171,8 @@ func (f *flagSet) targetFlags() {
f.fs.StringVar(&f.o.dir, "dir", "", "the target working tree `DIR` (default: the current directory)")
f.fs.StringVar(&f.o.repo, "repo", "", "the target in targets.yml, as `OWNER/NAME` or PROVIDER:PATH (default: from the origin remote)")
f.fs.Var(&f.o.packs, "packs", "comma-separated `PACKS` to apply instead of the selection from targets.yml and the opt-in file")
f.fs.BoolVar(&f.o.assumeOptIn, "assume-opt-in", false,
"count the target as opted in when it has no opt-in file, as an org or group entry with opt_in: assumed that a local run cannot resolve would")
}

func checkFlags() (*flagSet, *options) {
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func (s *syncRun) load(ctx context.Context) error {
if err := t.resolveRef(ctx, s.o.repo, h); err != nil {
return err
}
if err := t.assume(h); err != nil {
if err := t.assume(h, s.o.assumeOptIn); err != nil {
return err
}
s.hub, s.tgt = h, t
Expand Down
41 changes: 27 additions & 14 deletions internal/cli/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,30 +113,43 @@ func (t *target) readOptIn(name string) error {
// It then gets the packs of targets.yml, as if it had an empty opt-in
// file. An org or group entry with opt_in: assumed that may hold the
// target is a warning: only the platform tells whether it selects it, so
// the target stays not opted in here, and plan has the answer.
func (t *target) assume(h *hub) error {
// the target stays not opted in here, and plan has the answer. flag
// (--assume-opt-in) settles that here instead: the target counts as
// opted in, as such an entry would make it. An opt-in file, enabled:
// false included, still wins over the flag.
func (t *target) assume(h *hub, flag bool) error {
if t.optInState != report.OptInNone {
return nil
}
var warning string
if !t.refKnown {
if h.targets != nil && (h.targets.Defaults.OptIn == config.OptInAssumed ||
slices.ContainsFunc(h.targets.Targets, func(e config.Entry) bool { return e.OptIn == config.OptInAssumed })) {
t.warnings = append(t.warnings, "targets.yml subscribes some repositories without an opt-in file (opt_in: assumed), "+
"and this one's name is unknown (no --repo and no origin remote): pass --repo to tell whether it is one of them")
warning = "targets.yml subscribes some repositories without an opt-in file (opt_in: assumed), " +
"and this one's name is unknown (no --repo and no origin remote): pass --repo to tell whether it is one of them, " +
"or --assume-opt-in to count it as one"
}
} else {
a, err := config.Assume(h.cfg, h.targets, t.ref)
if err != nil {
return configError(err)
}
if a.Assumed {
t.optIn, t.optInState = &config.OptIn{Version: 1}, report.OptInAssumed
return nil
}
if len(a.Unresolved) > 0 {
warning = fmt.Sprintf("targets.yml may subscribe this repository without an opt-in file: %s with opt_in: assumed "+
"cannot be resolved here, so it counts as not opted in; plan tells, and --assume-opt-in or an opt-in file decides it here",
strings.Join(a.Unresolved, ", "))
}
return nil
}
a, err := config.Assume(h.cfg, h.targets, t.ref)
if err != nil {
return configError(err)
}
if a.Assumed {
t.optIn, t.optInState = &config.OptIn{Version: 1}, report.OptInAssumed
if flag {
t.optIn, t.optInState = &config.OptIn{Version: 1}, report.OptInFlag
return nil
}
if len(a.Unresolved) > 0 {
t.warnings = append(t.warnings, fmt.Sprintf("targets.yml may subscribe this repository without an opt-in file: %s with opt_in: assumed "+
"cannot be resolved here, so it counts as not opted in; plan tells, and an opt-in file decides it here", strings.Join(a.Unresolved, ", ")))
if warning != "" {
t.warnings = append(t.warnings, warning)
}
return nil
}
Expand Down
26 changes: 26 additions & 0 deletions internal/cli/targets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,28 @@ func TestAssumedOptIn(t *testing.T) {
if rep := unknown.report("status"); rep.Target.OptedIn || len(rep.Warnings) != 1 || !strings.Contains(rep.Warnings[0], "pass --repo") {
t.Errorf("a target without a name: %+v, warnings %q", rep.Target, rep.Warnings)
}

// --assume-opt-in settles what the org entry leaves open: the library
// counts as opted in, with the packs of targets.yml and no warning, and
// apply writes them. So does a target without a name.
libY := newRepo(t, false)
flagged := newScenario(t, "assumed", h, libY, "--repo", "acme/lib-y", "--assume-opt-in")
flagged.golden("status-flag.txt", flagged.text("status"))
if rep := flagged.report("status"); !rep.Target.OptedIn || rep.Target.OptIn != report.OptInFlag || len(rep.Warnings) != 0 {
t.Errorf("acme/lib-y with --assume-opt-in: %+v, warnings %q", rep.Target, rep.Warnings)
}
flagged.json("apply")
if tree := libY.tree(); !strings.Contains(tree, "AGENTS.md") {
t.Errorf("apply --assume-opt-in wrote no packs:\n%s", tree)
}
unnamed := newScenario(t, "assumed", h, newPlainDir(t), "--assume-opt-in")
if rep := unnamed.report("status"); !rep.Target.OptedIn || rep.Target.OptIn != report.OptInFlag || len(rep.Warnings) != 0 {
t.Errorf("a target without a name, with --assume-opt-in: %+v, warnings %q", rep.Target, rep.Warnings)
}
// A repo: entry that subscribes the target still says so.
if rep := newScenario(t, "assumed", h, newRepo(t, false), "--repo", "acme/svc", "--assume-opt-in").report("status"); rep.Target.OptIn != report.OptInAssumed {
t.Errorf("acme/svc with --assume-opt-in: opt_in %q, want %q", rep.Target.OptIn, report.OptInAssumed)
}
}

// An opt-in file with enabled: false opts the target out, whatever
Expand All @@ -62,6 +84,10 @@ func TestOptedOut(t *testing.T) {
s := newScenario(t, "opted-out", h, tg, "--repo", "acme/svc")
s.golden("status.txt", s.text("status"))
s.golden("apply.json", s.json("apply"))
if rep := newScenario(t, "opted-out", h, tg, "--repo", "acme/svc", "--assume-opt-in").report("status"); rep.Target.OptedIn ||
rep.Target.OptIn != report.OptInDisabled {
t.Errorf("enabled: false with --assume-opt-in: %+v", rep.Target)
}
if tree := tg.tree(); strings.Contains(tree, "AGENTS.md") {
t.Errorf("apply wrote into a target that opted out:\n%s", tree)
}
Expand Down
10 changes: 10 additions & 0 deletions internal/cli/testdata/golden/assumed/status-flag.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
touchmark status · hub acme-eng @ $COMMIT · target acme/lib-y
opted in by --assume-opt-in: .engineering-assets.yml not found in $TARGET
packs: base (defaults)

create missing .github/pull_request_template.md base
create missing AGENTS.md base
create missing docs/guide.md base

missing 3
3 changes to apply: run touchmark apply
8 changes: 6 additions & 2 deletions internal/report/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ type Target struct {
// when it could not be determined.
Ref string `json:"ref"`
OptInFile string `json:"opt_in_file"`
// OptedIn is set when the target gets packs: OptIn is OptInFile or
// OptInAssumed.
// OptedIn is set when the target gets packs: OptIn is OptInFile,
// OptInAssumed or OptInFlag.
OptedIn bool `json:"opted_in"`
// OptIn says why the target is opted in or not: one of the OptIn*
// values.
Expand All @@ -51,6 +51,10 @@ const (
// OptInAssumed: the target has no opt-in file, and a repo: entry of
// targets.yml with opt_in: assumed subscribes it.
OptInAssumed = "assumed"
// OptInFlag: the target has no opt-in file, and --assume-opt-in counts
// it as opted in, as an org or group entry with opt_in: assumed that
// a local run cannot resolve would.
OptInFlag = "flag"
// OptInDisabled: the opt-in file says enabled: false.
OptInDisabled = "opted-out"
// OptInNone: the target has no opt-in file, and no repo: entry
Expand Down
2 changes: 2 additions & 0 deletions internal/report/text.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ func (s *Sync) WriteText(w io.Writer) error {
return p.err
case s.Target.OptIn == OptInAssumed:
p.linef("opted in by targets.yml (opt_in: assumed): %s not found in %s", s.Target.OptInFile, s.Target.Root)
case s.Target.OptIn == OptInFlag:
p.linef("opted in by --assume-opt-in: %s not found in %s", s.Target.OptInFile, s.Target.Root)
}
if s.Selection != nil {
p.linef("packs: %s", packsLine(s.Selection))
Expand Down
Loading