feat(git): stage / commit / push from the diff viewer (W4)
The walk-away endgame — review a diff on your phone, then land it without typing
git into a mobile terminal. MVP bounded to per-file stage/unstage, commit, and
push the current branch; discard/checkout/reset deliberately deferred (nothing here
mutates working-tree file contents).
- src/http/git-ops.ts (new): stageFiles/commit/push (execFile, no shell, never
throws). Path containment: every files[] entry realpath-contained under the repo,
argv after `--`, capped at diffMaxFiles. Commit message: empty/over-5000 → 400,
single -m argv. Push: current branch only — has-upstream → plain `git push`;
no-upstream + one remote → `git push -u <remote> <branch>`; 0 remotes → 400,
≥2 → 409, detached HEAD → 400. Remote AND branch read from the repo, never the
client. NEVER --force / +refspec. GIT_TERMINAL_PROMPT=0 + ssh BatchMode fail auth
fast (401). Errors classified to fixed safe strings (raw stderr never surfaced).
- POST /projects/git/{stage,commit,push} — all behind requireAllowedOrigin +
GIT_OPS_ENABLED kill-switch + per-IP rate limits (stage/commit 30/min, push 6/min)
+ isValidGitDir. public/diff.ts: per-file Stage/Unstage + a commit/push bar
(all text via textContent; re-loads the diff on success).
Verified: typecheck + build:web clean, git-ops tests 213 pass (unit covers escape
rejection / empty+overlong commit / push argv upstream-vs-no-upstream / classified
errors), full suite green at --test-timeout=30000.
This commit is contained in:
@@ -2385,3 +2385,87 @@ body.home-open #term {
|
||||
.gp-save-btn:hover {
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
/* ── W4 git write: per-file Stage/Unstage toggle + commit/push bar ─────────── */
|
||||
.df-file-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.df-file-stage {
|
||||
margin-left: auto;
|
||||
flex: 0 0 auto;
|
||||
padding: 3px 10px;
|
||||
font-size: 12px;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 6px;
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
.df-file-stage:hover {
|
||||
background: var(--surface-3);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.df-commitbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding: 10px;
|
||||
border-top: 1px solid var(--border);
|
||||
background: var(--surface-1);
|
||||
}
|
||||
.df-commit-msg {
|
||||
flex: 1 1 220px;
|
||||
min-width: 0;
|
||||
resize: vertical;
|
||||
padding: 8px;
|
||||
font: inherit;
|
||||
color: var(--text);
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 6px;
|
||||
}
|
||||
.df-commit-msg:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-soft);
|
||||
}
|
||||
.df-commit-btn,
|
||||
.df-push-btn {
|
||||
flex: 0 0 auto;
|
||||
padding: 8px 16px;
|
||||
font: inherit;
|
||||
font-weight: 600;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--accent);
|
||||
background: var(--accent);
|
||||
color: #1a1712;
|
||||
cursor: pointer;
|
||||
}
|
||||
.df-push-btn {
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
.df-commit-btn:disabled,
|
||||
.df-push-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.df-op-status {
|
||||
flex: 1 1 100%;
|
||||
font-size: 13px;
|
||||
word-break: break-word;
|
||||
}
|
||||
.df-op-error {
|
||||
color: var(--red);
|
||||
}
|
||||
.df-op-notice {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.df-op-busy {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user