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
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.
Optional create-on-push
Labels:
enhancementSize: 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 thesingle 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: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, defaultfalseso 1.0.0 behaviour isunchanged:
Where it hooks in
In the adapter's
HandleAsync, after theAuthorizehook and beforeinvoker.InvokeAsync. Ordering matters: authorization gates creation, so a caller canonly create repositories it would have been allowed to push to. In the sample's model
that means a user whose
Reposlist contains"*"or that exact name.Which requests count as a push
Both of these, not just the second —
git pushperforms the ref advertisement first, andif that 404s the push never gets to the POST:
GET /<repo>/info/refs?service=git-receive-packPOST /<repo>/git-receive-packA request for
git-upload-pack(clone, fetch) must never create anything. Cloning aname that does not exist keeps returning 404.
Creation itself
Run
git init --bareusing the already-resolved git executable, then set the config thebackend requires:
http.receivepack = true— without itgit-http-backendrefuses 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.
git-daemon-export-okwhenExportAllisfalse, so a created repository isreachable under either export mode. See
07-export-all-default.
SafeDirectoriesfor thegit initinvocation too, otherwise creation failswith "dubious ownership" in exactly the service-account setup this library already
solves for the backend process.
Naming and concurrency
GitRepositoryPath.TryParse(01). No separate parsing here — that is the whole
reason 01 comes first.
.gitsuffix should follow what the client askedfor, so the URL the client already used keeps working.
repository. A lock keyed on the resolved path, plus tolerating "directory already
exists", is sufficient.
backend and let it produce an empty 500, which is the failure mode the stderr logging
was added to eliminate.
Acceptance criteria
AllowCreateOnPushdefaults tofalse; with it off, a push to an unknown namebehaves exactly as in 1.0.0.
git pushto a name that does not exist creates a bare repository andthe push completes; a subsequent
git cloneof that name succeeds.http.receivepack = true.git clone/git fetchof an unknown name never creates anything, with theoption on or off.
Authorizereturns true; a caller not allowed to pushto that name gets 403 and no directory appears on disk.
access.
SafeDirectoriesset.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.