Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions cmd/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,13 @@ The daemon injects credentials — workers don't need tokens in their environmen

Equivalent to "git push -u origin <branch>" but credential-safe for worker sessions.

Pushes to main or master are blocked unconditionally — all changes must go through
a feature branch and PR.
Branch protection is enforced by the remote repository, not by ttal.

With --force, performs a --force-with-lease push (raw --force is never used).
Force-push is also blocked on main/master.

Examples:
ttal push
ttal push --force # force-with-lease; blocked on main/master`,
ttal push --force # force-with-lease`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
ctx, err := pr.ResolveContextWithoutProvider()
Expand Down Expand Up @@ -86,6 +84,6 @@ func currentBranch(workDir string) (string, error) {
}

func init() {
pushCmd.Flags().Bool("force", false, "Force push with --force-with-lease (blocked on main/master)")
pushCmd.Flags().Bool("force", false, "Force push with --force-with-lease")
rootCmd.AddCommand(pushCmd)
}
12 changes: 0 additions & 12 deletions internal/daemon/git_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,13 @@ import (

var gitCommandContext = exec.CommandContext

// isProtectedBranch returns true if the given branch name is protected by policy.
// The list is intentionally small — extending it requires a code change.
func isProtectedBranch(branch string) bool {
return branch == "main" || branch == "master"
}

// handleGitPush executes a git push using daemon-held credentials.
// WorkDir may be a ttal worktree or any registered project directory.
// Credentials are injected via GIT_CONFIG env vars — never via URL embedding or keychain.
func handleGitPush(req GitPushRequest) GitPushResponse {
// Validation order: empty branch → protected-branch policy → credentials
if req.Branch == "" {
return GitPushResponse{Error: "branch must not be empty"}
}
if isProtectedBranch(req.Branch) {
return GitPushResponse{
Error: fmt.Sprintf("push to %s blocked — use a feature branch and PR", req.Branch),
}
}

// Detect remote URL to pick the right token.
remoteURL, err := gitutil.RemoteURL(req.WorkDir)
Expand Down
46 changes: 7 additions & 39 deletions internal/daemon/git_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,56 +488,24 @@ func runGitTestCmd(t *testing.T, workDir string, args ...string) {
}
}

func TestHandleGitPush_ForceOnProtectedBranchBlocked(t *testing.T) {
func TestHandleGitPush_AllowsMainAndMaster(t *testing.T) {
for _, branch := range []string{"main", "master"} {
t.Run(branch, func(t *testing.T) {
resp := handleGitPush(GitPushRequest{
WorkDir: "/tmp/whatever", // never reached
WorkDir: "/tmp/not-a-real-repo",
Branch: branch,
Force: true,
})
if resp.OK {
t.Fatalf("expected OK=false for force push to %s", branch)
oldPolicyErr := fmt.Sprintf("push to %s blocked — use a feature branch and PR", branch)
if resp.Error == oldPolicyErr {
t.Fatalf("expected push to %s to bypass local policy guard, got old policy error", branch)
}
wantErr := fmt.Sprintf("push to %s blocked — use a feature branch and PR", branch)
if resp.Error != wantErr {
t.Errorf("error = %q, want %q", resp.Error, wantErr)
if !strings.Contains(resp.Error, "get remote URL") {
t.Fatalf("expected push to %s to reach remote resolution, got error: %s", branch, resp.Error)
}
})
}
}

func TestIsProtectedBranch(t *testing.T) {
protected := []string{"main", "master"}
allowed := []string{"develop", "feature/x", "release/v1", ""}

for _, b := range protected {
if !isProtectedBranch(b) {
t.Errorf("isProtectedBranch(%q) = false, want true", b)
}
}
for _, b := range allowed {
if isProtectedBranch(b) {
t.Errorf("isProtectedBranch(%q) = true, want false", b)
}
}
}

func TestHandleGitPush_NormalPushToMainBlockedByPolicy(t *testing.T) {
resp := handleGitPush(GitPushRequest{
WorkDir: "/tmp/not-a-real-repo", // will fail at RemoteURL if policy guard is bypassed
Branch: "main",
Force: false,
})
if resp.OK {
t.Fatal("expected push to main to be blocked regardless of force flag")
}
policyErr := "push to main blocked — use a feature branch and PR"
if resp.Error != policyErr {
t.Errorf("expected policy error, got: %q", resp.Error)
}
}

func TestHandleGitPush_ForceWithEmptyBranchReturnsEmptyError(t *testing.T) {
resp := handleGitPush(GitPushRequest{
WorkDir: "/tmp/whatever",
Expand Down
Loading