diff --git a/cmd/push.go b/cmd/push.go index 41f64e69..e0ac7229 100644 --- a/cmd/push.go +++ b/cmd/push.go @@ -21,15 +21,13 @@ The daemon injects credentials — workers don't need tokens in their environmen Equivalent to "git push -u origin " 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() @@ -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) } diff --git a/internal/daemon/git_handler.go b/internal/daemon/git_handler.go index a51148bf..84f3c2d2 100644 --- a/internal/daemon/git_handler.go +++ b/internal/daemon/git_handler.go @@ -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) diff --git a/internal/daemon/git_handler_test.go b/internal/daemon/git_handler_test.go index 3d8d5390..17207bb3 100644 --- a/internal/daemon/git_handler_test.go +++ b/internal/daemon/git_handler_test.go @@ -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",