Distribution

Run and manage assignment distribution with Divekit.

Overview

divekit distribute creates individualized repositories from an existing distribution config. In a typical run it:

  • reads .divekit/distributions/<name>/config.json
  • creates repositories in the configured target group(s)
  • individualizes content for each UUID
  • assigns members and permissions
  • writes state files: remotes.json and individuals.json

If the evaluation pipeline is enabled, Divekit also creates the linked eval repositories.

Prerequisites

divekit init is a one-time setup step and does not need to be repeated before every distribution.

Before running distribution, make sure you have:

  1. A distribution config in .divekit/distributions/<distribution>/config.json
  2. Working authentication (divekit auth or divekit config hosts)
  3. A valid members file referenced by members.path

Repository names are derived from the distribution name by default. If config.json does not set name, Divekit uses {{distribution}}-{{uuid}}, so a distribution named ST2M4 creates repositories such as ST2M4-b8520e99-.... Set name only if you need a custom repository naming pattern. The Repository Name Templates reference lists all supported group data, functions, and formatting options.

When the evaluation pipeline is enabled, the eval repositories use the same repository name template as the work repositories. The target context comes from the surrounding GitLab group/path layout, not from an automatic -eval suffix in the project name.

Run Distribution

Use the distribution name directly:

divekit distribute --distribution <distribution-name>

Or short form:

divekit distribute -d <distribution-name>

Without -d, Divekit prompts for a distribution interactively.

Prepare Members and Groups

Divekit can create or update the configured members file from the interactive multiline prompt, --members-file, or stdin. In the simple format, each non-empty line creates one repository group:

tom
ada
john
ben

This produces four groups with one GitLab username each. Put comma-separated usernames on one row when several people should share one repository:

alice,bob
john,ada

This produces two groups: alice/bob and john/ada. Because the first row does not contain a recognized header such as username or member, it is treated as member data and is not skipped.

CSV, TSV, and Excel files can include a header row. Use username, member, members, user, login, or provider_username for GitLab usernames. Numbered forms such as member1,member2 or username1,username2 create multi-member groups from one row. A metadata-only first row such as fruits is not enough to trigger header mode; include at least one recognized member column such as username, member, or email.

email is only a fallback member column when no username/member column exists. If both email and username exist, Divekit uses username for GitLab access and keeps email as group metadata.

email,username,fruit
tom@example.com,tom,banana
ada@example.com,ada,apple
john@example.com,john,apple

This produces three groups with one GitLab username each: tom, ada, and john. The fruit column is stored as groups[].metadata.fruit, so it can be used in repository name templates with {{metadata "fruit"}} or {{.Metadata.fruit}}.

Columns named label, group, group_label, repository, repository_label, repo, repo_label, or title become the group label instead of metadata. Rows with the same uuid or the same label are merged into one repository group.

Resolve Different GitLab Usernames

If distribution cannot assign a person because the local member name does not match the GitLab username, set a member alias and rerun the distribution command:

divekit members alias set -d ST2M4 myname myname_extern
divekit distribute -d ST2M4

The alias command keeps the GitLab username in members.json and stores the local display name as metadata. Commands such as members list, overview, and the GUI then show both names as myname (@myname_extern), while GitLab operations continue to use myname_extern.

For details, see Member Aliases.

Exclude Members From a Distribution

If a members file was created from a previous milestone or passcheck result, you can remove individual users before or after creating repositories:

divekit members exclude -d ST2M4 alice bob

For longer exclusion lists, put one username per line into a file:

divekit members exclude -d ST2M4 --members-file excluded.txt

Preview the result first with --dry-run:

divekit members exclude -d ST2M4 --members-file excluded.txt --dry-run

members exclude shows a confirmation plan, writes a new members file, preserves the UUIDs of remaining groups, removes empty groups, and updates config.json so the distribution points to the filtered file. The original members file remains available for audit or rollback.

If the distribution already has work-target remotes, members exclude also revokes the excluded users from the matching GitLab projects after confirmation. It does not delete already created repositories. Use --dry-run to inspect the local filtering and planned remote membership changes without writing files or changing GitLab.

Update Configured Members

Use members add when late-joining users should be added to the configured members file before creating or updating repositories:

divekit members add -d ST2M4 alice bob
divekit members add -d ST2M4 --members-file additional-members.txt

Use members filter --include to keep only a known set, for example users who passed an earlier milestone:

divekit members filter -d ST2M4 --include --members-file passed-members.txt

The include mode is also available through the keep, retain, and intersect aliases:

divekit members keep -d ST2M4 --members-file passed-members.txt

Use members filter --exclude when you want the same local filtering behavior as members exclude while keeping the command shape explicit:

divekit members filter -d ST2M4 --exclude --members-file skipped-members.txt

These commands write a new members file, update config.json to point at it, preserve existing UUIDs, remove empty groups, and support --dry-run. When users are removed and work-target remotes already exist, Divekit also plans matching GitLab membership revocations after confirmation. When users are added to a distribution with existing work-target remotes, run divekit distribute --add or divekit members sync afterwards to assign them to repositories.

When an interactive divekit distribute run finds an existing configured members file, it asks how to proceed:

  • Keep: use the configured members as they are.
  • Add: append newcomers only. Removing configured members is rejected in this mode.
  • Edit: open the current groups in the multiline editor. Existing groups are shown as comma-separated rows, so changing alice,bob to alice,carol changes group membership instead of only editing a flat member list.
  • Fresh: start with an empty member list and reset distribution state.

When you choose Edit in divekit distribute and confirm the changed groups, existing repositories are matched by UUID. Divekit assigns the current members to each UUID’s work project and removes users who were previously configured for that UUID but were moved to another UUID or removed from the configured file. It does not delete repositories and it does not remove arbitrary users that were added manually outside the configured members file. Eval targets and runs with --no-member-assignment do not perform membership changes.

Manual edits to members.json only give Divekit the current desired member state. Use the interactive distribute edit flow, divekit members exclude, or divekit members filter when you want Divekit to plan remote membership removals from an existing distribution.

Update Member Roles

Use members role to change the GitLab access level of existing members on the work target. Downgrades also revoke overprivileged Project Access Tokens on affected work projects:

divekit members role -d ST2M4 guest

If no access level is passed, Divekit prompts for one interactively:

divekit members role -d ST2M4

The legacy top-level command divekit role still works for compatibility, but it prints a deprecation warning. Use divekit members role for new scripts and documentation.

Example Flow (Current)

# Initial rollout
divekit distribute -d ST2M4

# Filter configured members before creating repos
divekit members exclude -d ST2M4 --members-file excluded.txt

# Add late-joining members to the configured members file
divekit members add -d ST2M4 --members-file new-members.txt

# Restrict member access after a deadline
divekit members role -d ST2M4 guest

# Assign newly configured members without recreating existing repos
divekit distribute -d ST2M4 --add

# Rebuild state files and re-run while preserving stable repository indexes
divekit distribute -d ST2M4 --fresh

# Preview only (no remote changes)
divekit distribute -d ST2M4 --dry-run

# Write the distribution to local files instead of GitLab
divekit distribute -d ST2M4 -p local

Useful options for advanced runs:

  • --provider simulated (same idea as --dry-run)
  • --provider local or -p local to create a local filesystem distribution
  • --workers <n> and --rate-limit <n> for large cohorts
  • --warn-unresolved-tokens auto|true|false for unresolved token warnings
  • --dangerously-delete-group-projects for destructive reset scenarios

Retrofitting Existing Eval Repositories

For evaluation-pipeline distributions, Divekit disables GitLab’s unique Pages domain setting on eval repositories during divekit distribute. That keeps Pages URLs predictable for course material and generated links.

If a distribution already exists, you can reconcile the stored eval remotes afterwards without patching files:

divekit patch -d ST2M4 --disable-eval-pages-unique-domain

You can also combine this with normal file patches:

divekit patch -d ST2M4 README.md --disable-eval-pages-unique-domain

Behavior notes:

  • the flag only affects the eval target
  • it is safe to run on existing distributions
  • --dry-run still shows the planned action without changing remote state

Local Distribution Output

Use the local provider when you want to inspect or test generated repositories without writing to GitLab:

divekit distribute -d ST2M4 -p local

Divekit writes the generated local distribution under:

.divekit/local-data/<distribution>/local/<run-id>/
├── work/<project>/repo
├── eval/<project>/repo
└── remotes.json

These repositories are local filesystem outputs, not GitLab projects. Commands such as divekit run can discover them and run scripts against them.

Outputs and Follow-Up

After a successful run, check:

  1. .divekit/distributions/<distribution>/remotes.json for UUID → repository mapping
  2. .divekit/distributions/<distribution>/individuals.json for concrete variation selections
  3. divekit overview -d <distribution> for status overview

divekit overview may read cached member data. Use divekit overview -d <distribution> --no-cache when you need fresh member data from GitLab. The older overview --fresh spelling is deprecated; keep --fresh for divekit distribute only, where it resets generated distribution state before re-running.

For GitLab-backed repositories, divekit fetch -d <distribution> creates timestamped snapshots under .divekit/local-data/<distribution>/gitlab/<timestamp>/. divekit run --fetch-first uses the same storage before executing a script.

When a long-running operation is interrupted, Divekit now reports the partial progress before exiting. distribute, fetch, patch, snapshot, run, and purge show how many items completed, failed, were skipped, or were not attempted where that information is available. For distribute, already completed remote mappings and individuals are persisted so follow-up commands can continue from the recorded state.

Create Research Snapshots

Use divekit snapshot when you need a research or archival copy of distributed work repositories without the original Git commit history.

# Interactive: select distribution, snapshot remote, and snapshot root group
divekit snapshot

# Explicit target group
divekit snapshot -d ST2M4 --target-group research/course-2026

# Preview the target projects without cloning, creating groups, or pushing
divekit snapshot -d ST2M4 --dry-run

# Keep local staging repositories after upload
divekit snapshot -d ST2M4 --output-dir ./snapshot-staging

For each selected work repository, Divekit clones the current source state, removes .git, initializes a new repository, creates one neutral initial commit, and pushes that repository into the snapshot target group. By default the commit author is Divekit <divekit@invalid.local> and the target branch is main.

The command deliberately removes Git history and commit metadata only. It does not scrub file contents, file paths, UUIDs, repository names, or other data that is present in the repository tree. Review the generated plan before confirming, and avoid name patterns that include usernames unless that exposure is intended.

Snapshot target resolution works like this:

  • --target-group writes below that GitLab group.
  • snapshot.groupId in .divekit/distributions/<distribution>/config.json stores the configured snapshot root group used by interactive runs.
  • Without snapshot.groupId, Divekit derives a snapshot location from the distribution group layout. For distribution-subgroups, it writes below the layout root as <distribution>/snapshot/<label>. For legacy layouts, it writes below snapshot/<label> next to the work group.

In interactive mode, Divekit asks for the snapshot remote when multiple host aliases are configured, then asks for the snapshot root group before showing the confirmation plan. If you enter a different root group, Divekit resolves it to a GitLab group ID and stores it as snapshot.groupId for the distribution.

By default local staging uses a temporary directory and is removed after the run. Set --output-dir only when you want to inspect or keep the local rewritten repositories after they have been pushed. Snapshot does not write a manifest file and does not update remotes.json.

Useful options:

  • --uuid or --user limits the selected work repositories.
  • --name-pattern controls target project names. The default is {{distribution}}-{{uuid}}.
  • --on-existing skip|overwrite|fail controls how existing target projects are handled.
  • --source-ref clones a specific source branch or tag instead of the default source branch.
  • --workers and --rate-limit control concurrency for larger cohorts.

Delete Distributed Repositories

Use divekit purge when you intentionally want to remove repositories recorded for a distribution. The command builds and prints a deletion plan before it asks for confirmation.

# Interactive: inspect the plan and confirm at the prompt
divekit purge -d ST2M4

# Scripted destructive run: confirmation must be explicit
divekit purge -d ST2M4 --yes

Plain --non-interactive is not enough for purge, because deleting remote repositories requires an explicit confirmation. Use --yes only in scripts where the selected distribution and host are already constrained.

Patch Existing Repositories

Use divekit patch when repositories have already been distributed and you need to apply focused follow-up changes.

# Choose files from a searchable interactive picker
divekit patch

# Create or update files from the origin repository
divekit patch -d ST2M4 README.md docs/changelog.txt

# Equivalent explicit form for file patches
divekit patch files -d ST2M4 README.md docs/changelog.txt

# Rewrite patch files even when the target content already matches the reference
divekit patch -d ST2M4 README.md --force

# Delete a remote file even if it no longer exists locally
divekit patch -d ST2M4 --delete obsolete-file.txt

# Delete all files below a remote directory in every distributed repository
divekit patch -d ST2M4 --delete-dir old-material

# Move a remote file
divekit patch -d ST2M4 --move old/path.txt:new/path.txt

When no positional file or explicit operation is supplied in an interactive terminal, Divekit lists Git-visible project files in a searchable multi-select picker. Changed and untracked files appear first. The picker excludes .git/ and .divekit/; selecting no files cancels the patch. Non-interactive runs must continue to provide files or an explicit operation such as --delete.

Patch paths are repository-relative. Positional files are read from the current origin repository and individualized before being committed. By default Divekit skips file patches when the distributed repository already matches the reference content; add --force when the file should still be written and committed, for example after repairing derived state around a patch. --delete, --delete-dir, and --move operate on the distributed repositories directly, so they also work for files that were accidentally distributed but are not present in the origin repository anymore.

When patched files affect hashed solution tables, Divekit also regenerates the derived work-repository hash artifact. For example, changing src/test/resources/E1.solution_evalrepo.md or src/test/resources/E1.config.json causes the matching src/test/resources/E1.hashed.md file to be updated in the work repositories.

--delete-dir expands the directory separately in each target repository and deletes all files below that path. Missing delete paths and missing move sources are skipped, which keeps cleanup patches idempotent across groups whose repositories may differ slightly. The interactive confirmation plan is based on one group as a preview; execution resolves these operations per project.

File patches do not reconcile GitLab project membership. Use the interactive divekit distribute edit flow or the member filtering commands when regrouping or removing configured members should also assign and revoke work-project members. divekit patch still individualizes patched files by UUID using the current configured members when available.

Repair Repository Names and Paths

Use divekit patch name when repositories were distributed with outdated GitLab project names or URL paths and you want to reconcile them with the current config.json naming rule.

# Interactive: select distribution and choose whether to patch name, URL path, or both
divekit patch name

# Preview the plan for one distribution
divekit patch name -d ST2M4 --scope both

# Apply non-interactively
divekit patch name -d ST2M4 --scope both --apply --non-interactive

The expected repository name is calculated from config.json field name. If name is missing, Divekit uses the default {{distribution}}-{{uuid}}. The URL path is the GitLab-normalized slug for that expected name. Group labels and metadata from the current members file can be part of the template, for example {{label}}, {{slug}}, or {{metadata "fruit"}}. When the template uses {{index}}, the patch reuses persisted assignments or allocates missing ones before reconciling names and URL paths.

Scopes:

  • name: update only the GitLab display name
  • path: update only the GitLab URL path
  • both: update display name and URL path

When URL paths change, Divekit also refreshes remotes.json, reconciles repository linking variables, keeps inbound token scopes current, and re-patches origin files that contain Pages URL tokens such as PagesURL, PagesUrl, PageUrl, or pagesUrl. Hard-coded old URLs without those tokens are not rewritten automatically.

divekit patch name does not rename local distribution folders.

Next Steps