Skip to content

03 - Optional create-on-push #9

Description

@Zefek

Optional create-on-push

Labels: enhancement
Size: M · Priority: high · Depends on: #7

Motivation

Everything else about this project works without touching the host: one self-contained
executable, one appsettings.json, install as a service. Creating a repository is the
single step that breaks that promise — it requires a shell on the machine. The home
page's empty state says so out loud, samples/GitHttpBackend.Server/Program.cs:199-202:

No repositories found in …. Create one with git init --bare myproject.git inside
that folder.

Pushing to a name that does not exist yet, and having it created, closes that gap: a new
machine's configuration repository can be created from wherever the configuration is
authored, with no remote session.

Proposed change

New opt-in option on GitBackendOptions, default false so 1.0.0 behaviour is
unchanged:

/// <summary>
/// Create a bare repository under <see cref="ProjectRoot"/> when a push targets a name
/// that does not exist yet. Default <c>false</c>.
/// </summary>
public bool AllowCreateOnPush { get; init; }

Where it hooks in

In the adapter's HandleAsync, after the Authorize hook and before
invoker.InvokeAsync. Ordering matters: authorization gates creation, so a caller can
only create repositories it would have been allowed to push to. In the sample's model
that means a user whose Repos list contains "*" or that exact name.

Which requests count as a push

Both of these, not just the second — git push performs the ref advertisement first, and
if that 404s the push never gets to the POST:

  • GET /<repo>/info/refs?service=git-receive-pack
  • POST /<repo>/git-receive-pack

A request for git-upload-pack (clone, fetch) must never create anything. Cloning a
name that does not exist keeps returning 404.

Creation itself

Run git init --bare using the already-resolved git executable, then set the config the
backend requires:

  • http.receivepack = true — without it git-http-backend refuses the push regardless,
    which is already documented in the README's "Enabling push" section. Creating a
    repository that immediately rejects the push that created it would be a trap.
  • Write git-daemon-export-ok when ExportAll is false, so a created repository is
    reachable under either export mode. See
    07-export-all-default.
  • Honour SafeDirectories for the git init invocation too, otherwise creation fails
    with "dubious ownership" in exactly the service-account setup this library already
    solves for the backend process.

Naming and concurrency

  • The repository name comes from GitRepositoryPath.TryParse
    (01). No separate parsing here — that is the whole
    reason 01 comes first.
  • Whether the created directory gets a .git suffix should follow what the client asked
    for, so the URL the client already used keeps working.
  • Two simultaneous pushes to the same new name must not race into a half-created
    repository. A lock keyed on the resolved path, plus tolerating "directory already
    exists", is sufficient.
  • If creation fails, return 500 and log the underlying error — do not fall through to the
    backend and let it produce an empty 500, which is the failure mode the stderr logging
    was added to eliminate.

Acceptance criteria

  • AllowCreateOnPush defaults to false; with it off, a push to an unknown name
    behaves exactly as in 1.0.0.
  • With it on, git push to a name that does not exist creates a bare repository and
    the push completes; a subsequent git clone of that name succeeds.
  • The created repository has http.receivepack = true.
  • git clone / git fetch of an unknown name never creates anything, with the
    option on or off.
  • Creation happens only after Authorize returns true; a caller not allowed to push
    to that name gets 403 and no directory appears on disk.
  • An invalid or traversing name is rejected by 01's validator before any filesystem
    access.
  • An existing repository is never re-initialised or reconfigured.
  • Creation works when the host runs as a service account with SafeDirectories set.
  • Covered by an end-to-end test — see 05-end-to-end-tests.

Notes

Deliberately not in scope: a "create repository" button on the home page. It is a second
UI surface with its own CSRF and validation concerns, and push-to-create already covers
the case that matters. Revisit only if the page grows other write actions.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions