WS handler forwards caller-named target
apps/backend/internal/agent/handlers/git_handlers.go ↗The handler adds two optional fields and forwards them as PushOptions without interpreting them.
Request struct
type GitPushRequest struct {
SessionID string `json:"session_id"`
Force bool `json:"force"`
SetUpstream bool `json:"set_upstream"`
Repo string `json:"repo,omitempty"`
Remote string `json:"remote,omitempty"`
ExpectedBranch string `json:"expected_branch,omitempty"`
}
Forward as PushOptions
result, err := agentClient.GitPush(ctx, req.Repo, client.PushOptions{
Force: req.Force,
SetUpstream: req.SetUpstream,
Remote: req.Remote,
ExpectedBranch: req.ExpectedBranch,
})
Agentctl client carries PushOptions
apps/backend/internal/agent/runtime/agentctl/git.go ↗The client groups the two new inputs into PushOptions and adds destination fields to the result.
PushOptions and result fields
type PushOptions struct {
Force bool
SetUpstream bool
Remote string
ExpectedBranch string
}
type GitOperationResult struct {
Success bool `json:"success"`
Operation string `json:"operation"`
PushedRemote string `json:"pushed_remote,omitempty"`
PushedBranch string `json:"pushed_branch,omitempty"`
ExpectedBranch string `json:"expected_branch,omitempty"`
CurrentBranch string `json:"current_branch,omitempty"`
BaselinePublished bool `json:"baseline_published,omitempty"`
}
GitPush payload
func (c *Client) GitPush(ctx context.Context, repo string, opts PushOptions) (*GitOperationResult, error) {
payload := struct {
Force bool `json:"force"`
SetUpstream bool `json:"set_upstream"`
Repo string `json:"repo,omitempty"`
Remote string `json:"remote,omitempty"`
ExpectedBranch string `json:"expected_branch,omitempty"`
}{
Force: opts.Force, SetUpstream: opts.SetUpstream,
Repo: repo, Remote: opts.Remote, ExpectedBranch: opts.ExpectedBranch,
}
return c.gitOperation(ctx, "/api/v1/git/push", payload)
}
API layer passes through without logic
apps/backend/internal/agentctl/server/api/git.go ↗The HTTP handler accepts the two new fields and passes them to the operator unchanged.
Request types
type GitPushRequest struct {
Force bool `json:"force"`
SetUpstream bool `json:"set_upstream"`
Repo string `json:"repo,omitempty"`
Remote string `json:"remote,omitempty"`
ExpectedBranch string `json:"expected_branch,omitempty"`
}
type GitPushPreflightRequest struct {
Repo string `json:"repo,omitempty"`
Remote string `json:"remote,omitempty"`
ExpectedBranch string `json:"expected_branch,omitempty"`
}
Handler
result, err := gitOp.Push(c.Request.Context(), process.PushOptions{
Force: req.Force,
SetUpstream: req.SetUpstream,
Remote: req.Remote,
ExpectedBranch: req.ExpectedBranch,
})
Push target resolver and branch checks
apps/backend/internal/agentctl/server/process/git_push_target.go ↗The resolver maps a name or URL to a configured remote, validates the expected branch, and builds the refspec.
Resolve name or URL
func (g *GitOperator) resolvePushTarget(ctx context.Context, target string) (string, *pushRefusal) {
remotes, err := g.configuredRemotes(ctx)
if err != nil {
return "", &pushRefusal{code: pushRemoteConfigUnreadableErrorCode, message: "failed to read the checkout's remote configuration"}
}
if securityutil.IsValidBranchName(target) {
for _, remote := range remotes {
if remote.name == target {
return remote.name, nil
}
}
return "", &pushRefusal{code: pushRemoteNotFoundErrorCode, message: fmt.Sprintf("no configured remote named %q", target)}
}
return resolvePushTargetURL(remotes, target)
}
URL matching and fan-out guard
func resolvePushTargetURL(remotes []remoteConfig, target string) (string, *pushRefusal) {
var matches []string
fanout := false
for _, remote := range remotes {
if len(remote.pushURLs) == 1 && remote.pushURLs[0] == target {
matches = append(matches, remote.name)
continue
}
for _, url := range remote.pushURLs {
if url == target {
fanout = true
break
}
}
}
if len(matches) > 0 {
sort.Strings(matches)
return matches[0], nil
}
if fanout {
return "", &pushRefusal{code: pushRemoteFanoutErrorCode, message: "the requested push URL belongs to a remote that publishes to additional URLs; name the remote instead"}
}
return "", &pushRefusal{code: pushRemoteURLUnmatchedErrorCode, message: "no configured remote publishes to the requested URL"}
}
Detached HEAD detection
func (g *GitOperator) currentBranch(ctx context.Context) (string, error) {
output, err := g.runGitCommand(ctx, "symbolic-ref", "HEAD")
if err == nil {
ref := strings.TrimSpace(output)
if !strings.HasPrefix(ref, "refs/heads/") {
return "", nil
}
return strings.TrimPrefix(ref, "refs/heads/"), nil
}
if _, headErr := g.runGitCommand(ctx, "rev-parse", "--verify", "HEAD"); headErr == nil {
return "", nil
}
return "", fmt.Errorf("failed to read HEAD: %w", err)
}
Push and preflight with double verification
apps/backend/internal/agentctl/server/process/git.go ↗Push verifies the expected branch before and after baseline work, then pushes HEAD to the resolved branch.
Push flow
func (g *GitOperator) Push(ctx context.Context, opts PushOptions) (*GitOperationResult, error) {
opts = opts.normalized()
if !g.tryLock("push") {
return nil, ErrOperationInProgress
}
defer g.unlock()
result := &GitOperationResult{Operation: "push"}
if refusal := g.validateContributionState(ctx, opts.Force); refusal != nil {
refusal.apply(result)
return result, nil
}
plan, refusal := g.resolvePushPlan(ctx, opts, false)
if refusal != nil {
refusal.apply(result)
return result, nil
}
basePublication := emptyRemotePublication{}
if plan.baselineEligible {
basePublication = g.prepareEmptyRemotePublication(ctx, "")
}
shouldSetUpstream := g.resolveSetUpstream(ctx, opts, plan)
if refusal := g.verifyExpectedBranch(ctx, opts.ExpectedBranch, basePublication.published); refusal != nil {
refusal.apply(result)
return result, nil
}
args := []string{"push"}
if shouldSetUpstream { args = append(args, "--set-upstream") }
if opts.Force { args = append(args, "--force-with-lease") }
args = append(args, plan.remote, plan.refspec)
output, err := g.runGitCommand(ctx, args...)
result.Output = combineGitOutputs(basePublication.output, output)
if err != nil { result.Error = err.Error(); return result, nil }
result.Success = true
plan.reportDestination(result)
return result, nil
}
Preflight reports destination
func (g *GitOperator) PushPreflight(ctx context.Context, opts PushOptions) (*GitOperationResult, error) {
opts = opts.normalized()
plan, refusal := g.resolvePushPlan(ctx, opts, true)
if refusal != nil { refusal.apply(result); return result, nil }
output, err := g.runGitCommandWithEnvironment(ctx, contributionPreflightEnvironment,
"push", "--dry-run", "--no-verify", "--porcelain", plan.remote, plan.refspec)
if err != nil { setPushPreflightError(result, err); return result, nil }
result.Success = true
if !plan.routed {
result.PushedRemote = plan.remote
result.PushedBranch = plan.branch
}
return result, nil
}
Strict expected-branch validation
apps/backend/internal/common/securityutil/git.go ↗The validator rejects trailing slash, double slash, and symbolic refs that the permissive allowlist would accept.
Validator
func IsValidExpectedBranchName(branch string) bool {
if strings.HasSuffix(branch, "/") || strings.Contains(branch, "//") {
return false
}
return IsValidBranchName(branch) && !IsGitSymbolicRef(branch)
}