···558558 sourceBranch := r.FormValue("sourceBranch")559559 patch := r.FormValue("patch")560560561561- // Validate required fields for all PR types562562- if title == "" || body == "" || targetBranch == "" {563563- s.pages.Notice(w, "pull", "Title, body and target branch are required.")561561+ if targetBranch == "" {562562+ s.pages.Notice(w, "pull", "Target branch is required.")564563 return565564 }566565567566 us, err := NewUnsignedClient(f.Knot, s.config.Dev)568567 if err != nil {569569- log.Println("failed to create unsigned client to %s: %v", f.Knot, err)568568+ log.Printf("failed to create unsigned client to %s: %v", f.Knot, err)570569 s.pages.Notice(w, "pull", "Failed to create a pull request. Try again later.")571570 return572571 }···815816 }816817817818 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId))819819+}820820+821821+func (s *State) ValidatePatch(w http.ResponseWriter, r *http.Request) {822822+ _, err := fullyResolvedRepo(r)823823+ if err != nil {824824+ log.Println("failed to get repo and knot", err)825825+ return826826+ }827827+828828+ patch := r.FormValue("patch")829829+ if patch == "" {830830+ s.pages.Notice(w, "patch-error", "Patch is required.")831831+ return832832+ }833833+834834+ if patch == "" || !patchutil.IsPatchValid(patch) {835835+ s.pages.Notice(w, "patch-error", "Invalid patch format. Please provide a valid git diff or format-patch.")836836+ return837837+ }838838+839839+ if patchutil.IsFormatPatch(patch) {840840+ s.pages.Notice(w, "patch-preview", "Format patch detected. Title and description are optional; if left out, they will be extracted from the first commit.")841841+ } else {842842+ s.pages.Notice(w, "patch-preview", "Regular diff detected. Please provide a title and description.")843843+ }818844}819845820846func (s *State) PatchUploadFragment(w http.ResponseWriter, r *http.Request) {