loading up the forgejo repo on tangled to test page performance
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactor internal API for git commands, use meaningful messages instead of "Internal Server Error" (#23687)

# Why this PR comes

At first, I'd like to help users like #23636 (there are a lot)

The unclear "Internal Server Error" is quite anonying, scare users,
frustrate contributors, nobody knows what happens.

So, it's always good to provide meaningful messages to end users (of
course, do not leak sensitive information).

When I started working on the "response message to end users", I found
that the related code has a lot of technical debt. A lot of copy&paste
code, unclear fields and usages.

So I think it's good to make everything clear.

# Tech Backgrounds

Gitea has many sub-commands, some are used by admins, some are used by
SSH servers or Git Hooks. Many sub-commands use "internal API" to
communicate with Gitea web server.

Before, Gitea server always use `StatusCode + Json "err" field` to
return messages.

* The CLI sub-commands: they expect to show all error related messages
to site admin
* The Serv/Hook sub-commands (for git clients): they could only show
safe messages to end users, the error log could only be recorded by
"SSHLog" to Gitea web server.

In the old design, it assumes that:

* If the StatusCode is 500 (in some functions), then the "err" field is
error log, shouldn't be exposed to git client.
* If the StatusCode is 40x, then the "err" field could be exposed. And
some functions always read the "err" no matter what the StatusCode is.

The old code is not strict, and it's difficult to distinguish the
messages clearly and then output them correctly.

# This PR

To help to remove duplicate code and make everything clear, this PR
introduces `ResponseExtra` and `requestJSONResp`.

* `ResponseExtra` is a struct which contains "extra" information of a
internal API response, including StatusCode, UserMsg, Error
* `requestJSONResp` is a generic function which can be used for all
cases to help to simplify the calls.
* Remove all `map["err"]`, always use `private.Response{Err}` to
construct error messages.
* User messages and error messages are separated clearly, the `fail` and
`handleCliResponseExtra` will output correct messages.
* Replace all `Internal Server Error` messages with meaningful (still
safe) messages.

This PR saves more than 300 lines, while makes the git client messages
more clear.

Many gitea-serv/git-hook related essential functions are covered by
tests.

---------

Co-authored-by: delvh <dev.lh@web.de>

authored by

wxiaoguang
delvh
and committed by
GitHub
f4538791 79e7a6ec

+546 -850
+2 -2
build/generate-emoji.go
··· 60 60 // generate data 61 61 buf, err := generate() 62 62 if err != nil { 63 - log.Fatal(err) 63 + log.Fatalf("generate err: %v", err) 64 64 } 65 65 66 66 // write 67 67 err = os.WriteFile(*flagOut, buf, 0o644) 68 68 if err != nil { 69 - log.Fatal(err) 69 + log.Fatalf("WriteFile err: %v", err) 70 70 } 71 71 } 72 72
+63 -81
cmd/hook.go
··· 6 6 import ( 7 7 "bufio" 8 8 "bytes" 9 + "context" 9 10 "fmt" 10 11 "io" 11 - "net/http" 12 12 "os" 13 13 "strconv" 14 14 "strings" ··· 167 167 ctx, cancel := installSignals() 168 168 defer cancel() 169 169 170 - setup("hooks/pre-receive.log", c.Bool("debug")) 170 + setup(ctx, c.Bool("debug")) 171 171 172 172 if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { 173 173 if setting.OnlyAllowPushIfGiteaEnvironmentSet { 174 - return fail(`Rejecting changes as Gitea environment not set. 174 + return fail(ctx, `Rejecting changes as Gitea environment not set. 175 175 If you are pushing over SSH you must push with a key managed by 176 176 Gitea or set your environment appropriately.`, "") 177 177 } ··· 257 257 hookOptions.OldCommitIDs = oldCommitIDs 258 258 hookOptions.NewCommitIDs = newCommitIDs 259 259 hookOptions.RefFullNames = refFullNames 260 - statusCode, msg := private.HookPreReceive(ctx, username, reponame, hookOptions) 261 - switch statusCode { 262 - case http.StatusOK: 263 - // no-op 264 - case http.StatusInternalServerError: 265 - return fail("Internal Server Error", msg) 266 - default: 267 - return fail(msg, "") 260 + extra := private.HookPreReceive(ctx, username, reponame, hookOptions) 261 + if extra.HasError() { 262 + return fail(ctx, extra.UserMsg, "HookPreReceive(batch) failed: %v", extra.Error) 268 263 } 269 264 count = 0 270 265 lastline = 0 ··· 285 280 286 281 fmt.Fprintf(out, " Checking %d references\n", count) 287 282 288 - statusCode, msg := private.HookPreReceive(ctx, username, reponame, hookOptions) 289 - switch statusCode { 290 - case http.StatusInternalServerError: 291 - return fail("Internal Server Error", msg) 292 - case http.StatusForbidden: 293 - return fail(msg, "") 283 + extra := private.HookPreReceive(ctx, username, reponame, hookOptions) 284 + if extra.HasError() { 285 + return fail(ctx, extra.UserMsg, "HookPreReceive(last) failed: %v", extra.Error) 294 286 } 295 287 } else if lastline > 0 { 296 288 fmt.Fprintf(out, "\n") ··· 309 301 ctx, cancel := installSignals() 310 302 defer cancel() 311 303 312 - setup("hooks/post-receive.log", c.Bool("debug")) 304 + setup(ctx, c.Bool("debug")) 313 305 314 306 // First of all run update-server-info no matter what 315 307 if _, _, err := git.NewCommand(ctx, "update-server-info").RunStdString(nil); err != nil { ··· 323 315 324 316 if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { 325 317 if setting.OnlyAllowPushIfGiteaEnvironmentSet { 326 - return fail(`Rejecting changes as Gitea environment not set. 318 + return fail(ctx, `Rejecting changes as Gitea environment not set. 327 319 If you are pushing over SSH you must push with a key managed by 328 320 Gitea or set your environment appropriately.`, "") 329 321 } ··· 394 386 hookOptions.OldCommitIDs = oldCommitIDs 395 387 hookOptions.NewCommitIDs = newCommitIDs 396 388 hookOptions.RefFullNames = refFullNames 397 - resp, err := private.HookPostReceive(ctx, repoUser, repoName, hookOptions) 398 - if resp == nil { 389 + resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions) 390 + if extra.HasError() { 399 391 _ = dWriter.Close() 400 392 hookPrintResults(results) 401 - return fail("Internal Server Error", err) 393 + return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error) 402 394 } 403 395 wasEmpty = wasEmpty || resp.RepoWasEmpty 404 396 results = append(results, resp.Results...) ··· 409 401 if count == 0 { 410 402 if wasEmpty && masterPushed { 411 403 // We need to tell the repo to reset the default branch to master 412 - err := private.SetDefaultBranch(ctx, repoUser, repoName, "master") 413 - if err != nil { 414 - return fail("Internal Server Error", "SetDefaultBranch failed with Error: %v", err) 404 + extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master") 405 + if extra.HasError() { 406 + return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error) 415 407 } 416 408 } 417 409 fmt.Fprintf(out, "Processed %d references in total\n", total) ··· 427 419 428 420 fmt.Fprintf(out, " Processing %d references\n", count) 429 421 430 - resp, err := private.HookPostReceive(ctx, repoUser, repoName, hookOptions) 422 + resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions) 431 423 if resp == nil { 432 424 _ = dWriter.Close() 433 425 hookPrintResults(results) 434 - return fail("Internal Server Error", err) 426 + return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error) 435 427 } 436 428 wasEmpty = wasEmpty || resp.RepoWasEmpty 437 429 results = append(results, resp.Results...) ··· 440 432 441 433 if wasEmpty && masterPushed { 442 434 // We need to tell the repo to reset the default branch to master 443 - err := private.SetDefaultBranch(ctx, repoUser, repoName, "master") 444 - if err != nil { 445 - return fail("Internal Server Error", "SetDefaultBranch failed with Error: %v", err) 435 + extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master") 436 + if extra.HasError() { 437 + return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error) 446 438 } 447 439 } 448 440 _ = dWriter.Close() ··· 485 477 } 486 478 487 479 func runHookProcReceive(c *cli.Context) error { 488 - setup("hooks/proc-receive.log", c.Bool("debug")) 480 + ctx, cancel := installSignals() 481 + defer cancel() 482 + 483 + setup(ctx, c.Bool("debug")) 489 484 490 485 if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { 491 486 if setting.OnlyAllowPushIfGiteaEnvironmentSet { 492 - return fail(`Rejecting changes as Gitea environment not set. 487 + return fail(ctx, `Rejecting changes as Gitea environment not set. 493 488 If you are pushing over SSH you must push with a key managed by 494 489 Gitea or set your environment appropriately.`, "") 495 490 } 496 491 return nil 497 492 } 498 493 499 - ctx, cancel := installSignals() 500 - defer cancel() 501 - 502 494 if git.CheckGitVersionAtLeast("2.29") != nil { 503 - return fail("Internal Server Error", "git not support proc-receive.") 495 + return fail(ctx, "No proc-receive support", "current git version doesn't support proc-receive.") 504 496 } 505 497 506 498 reader := bufio.NewReader(os.Stdin) ··· 515 507 // H: PKT-LINE(version=1\0push-options...) 516 508 // H: flush-pkt 517 509 518 - rs, err := readPktLine(reader, pktLineTypeData) 510 + rs, err := readPktLine(ctx, reader, pktLineTypeData) 519 511 if err != nil { 520 512 return err 521 513 } ··· 530 522 531 523 index := bytes.IndexByte(rs.Data, byte(0)) 532 524 if index >= len(rs.Data) { 533 - return fail("Internal Server Error", "pkt-line: format error "+fmt.Sprint(rs.Data)) 525 + return fail(ctx, "Protocol: format error", "pkt-line: format error "+fmt.Sprint(rs.Data)) 534 526 } 535 527 536 528 if index < 0 { 537 529 if len(rs.Data) == 10 && rs.Data[9] == '\n' { 538 530 index = 9 539 531 } else { 540 - return fail("Internal Server Error", "pkt-line: format error "+fmt.Sprint(rs.Data)) 532 + return fail(ctx, "Protocol: format error", "pkt-line: format error "+fmt.Sprint(rs.Data)) 541 533 } 542 534 } 543 535 544 536 if string(rs.Data[0:index]) != VersionHead { 545 - return fail("Internal Server Error", "Received unsupported version: %s", string(rs.Data[0:index])) 537 + return fail(ctx, "Protocol: version error", "Received unsupported version: %s", string(rs.Data[0:index])) 546 538 } 547 539 requestOptions = strings.Split(string(rs.Data[index+1:]), " ") 548 540 ··· 555 547 } 556 548 response = append(response, '\n') 557 549 558 - _, err = readPktLine(reader, pktLineTypeFlush) 550 + _, err = readPktLine(ctx, reader, pktLineTypeFlush) 559 551 if err != nil { 560 552 return err 561 553 } 562 554 563 - err = writeDataPktLine(os.Stdout, response) 555 + err = writeDataPktLine(ctx, os.Stdout, response) 564 556 if err != nil { 565 557 return err 566 558 } 567 559 568 - err = writeFlushPktLine(os.Stdout) 560 + err = writeFlushPktLine(ctx, os.Stdout) 569 561 if err != nil { 570 562 return err 571 563 } ··· 588 580 589 581 for { 590 582 // note: pktLineTypeUnknow means pktLineTypeFlush and pktLineTypeData all allowed 591 - rs, err = readPktLine(reader, pktLineTypeUnknow) 583 + rs, err = readPktLine(ctx, reader, pktLineTypeUnknow) 592 584 if err != nil { 593 585 return err 594 586 } ··· 609 601 610 602 if hasPushOptions { 611 603 for { 612 - rs, err = readPktLine(reader, pktLineTypeUnknow) 604 + rs, err = readPktLine(ctx, reader, pktLineTypeUnknow) 613 605 if err != nil { 614 606 return err 615 607 } ··· 626 618 } 627 619 628 620 // 3. run hook 629 - resp, err := private.HookProcReceive(ctx, repoUser, repoName, hookOptions) 630 - if err != nil { 631 - return fail("Internal Server Error", "run proc-receive hook failed :%v", err) 621 + resp, extra := private.HookProcReceive(ctx, repoUser, repoName, hookOptions) 622 + if extra.HasError() { 623 + return fail(ctx, extra.UserMsg, "HookProcReceive failed: %v", extra.Error) 632 624 } 633 625 634 626 // 4. response result to service ··· 649 641 650 642 for _, rs := range resp.Results { 651 643 if len(rs.Err) > 0 { 652 - err = writeDataPktLine(os.Stdout, []byte("ng "+rs.OriginalRef+" "+rs.Err)) 644 + err = writeDataPktLine(ctx, os.Stdout, []byte("ng "+rs.OriginalRef+" "+rs.Err)) 653 645 if err != nil { 654 646 return err 655 647 } ··· 657 649 } 658 650 659 651 if rs.IsNotMatched { 660 - err = writeDataPktLine(os.Stdout, []byte("ok "+rs.OriginalRef)) 652 + err = writeDataPktLine(ctx, os.Stdout, []byte("ok "+rs.OriginalRef)) 661 653 if err != nil { 662 654 return err 663 655 } 664 - err = writeDataPktLine(os.Stdout, []byte("option fall-through")) 656 + err = writeDataPktLine(ctx, os.Stdout, []byte("option fall-through")) 665 657 if err != nil { 666 658 return err 667 659 } 668 660 continue 669 661 } 670 662 671 - err = writeDataPktLine(os.Stdout, []byte("ok "+rs.OriginalRef)) 663 + err = writeDataPktLine(ctx, os.Stdout, []byte("ok "+rs.OriginalRef)) 672 664 if err != nil { 673 665 return err 674 666 } 675 - err = writeDataPktLine(os.Stdout, []byte("option refname "+rs.Ref)) 667 + err = writeDataPktLine(ctx, os.Stdout, []byte("option refname "+rs.Ref)) 676 668 if err != nil { 677 669 return err 678 670 } 679 671 if rs.OldOID != git.EmptySHA { 680 - err = writeDataPktLine(os.Stdout, []byte("option old-oid "+rs.OldOID)) 672 + err = writeDataPktLine(ctx, os.Stdout, []byte("option old-oid "+rs.OldOID)) 681 673 if err != nil { 682 674 return err 683 675 } 684 676 } 685 - err = writeDataPktLine(os.Stdout, []byte("option new-oid "+rs.NewOID)) 677 + err = writeDataPktLine(ctx, os.Stdout, []byte("option new-oid "+rs.NewOID)) 686 678 if err != nil { 687 679 return err 688 680 } 689 681 if rs.IsForcePush { 690 - err = writeDataPktLine(os.Stdout, []byte("option forced-update")) 682 + err = writeDataPktLine(ctx, os.Stdout, []byte("option forced-update")) 691 683 if err != nil { 692 684 return err 693 685 } 694 686 } 695 687 } 696 - err = writeFlushPktLine(os.Stdout) 688 + err = writeFlushPktLine(ctx, os.Stdout) 697 689 698 690 return err 699 691 } ··· 718 710 Data []byte 719 711 } 720 712 721 - func readPktLine(in *bufio.Reader, requestType pktLineType) (*gitPktLine, error) { 713 + func readPktLine(ctx context.Context, in *bufio.Reader, requestType pktLineType) (*gitPktLine, error) { 722 714 var ( 723 715 err error 724 716 r *gitPktLine ··· 729 721 for i := 0; i < 4; i++ { 730 722 lengthBytes[i], err = in.ReadByte() 731 723 if err != nil { 732 - return nil, fail("Internal Server Error", "Pkt-Line: read stdin failed : %v", err) 724 + return nil, fail(ctx, "Protocol: stdin error", "Pkt-Line: read stdin failed : %v", err) 733 725 } 734 726 } 735 727 736 728 r = new(gitPktLine) 737 729 r.Length, err = strconv.ParseUint(string(lengthBytes), 16, 32) 738 730 if err != nil { 739 - return nil, fail("Internal Server Error", "Pkt-Line format is wrong :%v", err) 731 + return nil, fail(ctx, "Protocol: format parse error", "Pkt-Line format is wrong :%v", err) 740 732 } 741 733 742 734 if r.Length == 0 { 743 735 if requestType == pktLineTypeData { 744 - return nil, fail("Internal Server Error", "Pkt-Line format is wrong") 736 + return nil, fail(ctx, "Protocol: format data error", "Pkt-Line format is wrong") 745 737 } 746 738 r.Type = pktLineTypeFlush 747 739 return r, nil 748 740 } 749 741 750 742 if r.Length <= 4 || r.Length > 65520 || requestType == pktLineTypeFlush { 751 - return nil, fail("Internal Server Error", "Pkt-Line format is wrong") 743 + return nil, fail(ctx, "Protocol: format length error", "Pkt-Line format is wrong") 752 744 } 753 745 754 746 r.Data = make([]byte, r.Length-4) 755 747 for i := range r.Data { 756 748 r.Data[i], err = in.ReadByte() 757 749 if err != nil { 758 - return nil, fail("Internal Server Error", "Pkt-Line: read stdin failed : %v", err) 750 + return nil, fail(ctx, "Protocol: data error", "Pkt-Line: read stdin failed : %v", err) 759 751 } 760 752 } 761 753 ··· 764 756 return r, nil 765 757 } 766 758 767 - func writeFlushPktLine(out io.Writer) error { 759 + func writeFlushPktLine(ctx context.Context, out io.Writer) error { 768 760 l, err := out.Write([]byte("0000")) 769 - if err != nil { 770 - return fail("Internal Server Error", "Pkt-Line response failed: %v", err) 771 - } 772 - if l != 4 { 773 - return fail("Internal Server Error", "Pkt-Line response failed: %v", err) 761 + if err != nil || l != 4 { 762 + return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err) 774 763 } 775 - 776 764 return nil 777 765 } 778 766 779 - func writeDataPktLine(out io.Writer, data []byte) error { 767 + func writeDataPktLine(ctx context.Context, out io.Writer, data []byte) error { 780 768 hexchar := []byte("0123456789abcdef") 781 769 hex := func(n uint64) byte { 782 770 return hexchar[(n)&15] ··· 790 778 tmp[3] = hex(length) 791 779 792 780 lr, err := out.Write(tmp) 793 - if err != nil { 794 - return fail("Internal Server Error", "Pkt-Line response failed: %v", err) 795 - } 796 - if lr != 4 { 797 - return fail("Internal Server Error", "Pkt-Line response failed: %v", err) 781 + if err != nil || lr != 4 { 782 + return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err) 798 783 } 799 784 800 785 lr, err = out.Write(data) 801 - if err != nil { 802 - return fail("Internal Server Error", "Pkt-Line response failed: %v", err) 803 - } 804 - if int(length-4) != lr { 805 - return fail("Internal Server Error", "Pkt-Line response failed: %v", err) 786 + if err != nil || int(length-4) != lr { 787 + return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err) 806 788 } 807 789 808 790 return nil
+6 -4
cmd/hook_test.go
··· 6 6 import ( 7 7 "bufio" 8 8 "bytes" 9 + "context" 9 10 "strings" 10 11 "testing" 11 12 ··· 14 15 15 16 func TestPktLine(t *testing.T) { 16 17 // test read 18 + ctx := context.Background() 17 19 s := strings.NewReader("0000") 18 20 r := bufio.NewReader(s) 19 - result, err := readPktLine(r, pktLineTypeFlush) 21 + result, err := readPktLine(ctx, r, pktLineTypeFlush) 20 22 assert.NoError(t, err) 21 23 assert.Equal(t, pktLineTypeFlush, result.Type) 22 24 23 25 s = strings.NewReader("0006a\n") 24 26 r = bufio.NewReader(s) 25 - result, err = readPktLine(r, pktLineTypeData) 27 + result, err = readPktLine(ctx, r, pktLineTypeData) 26 28 assert.NoError(t, err) 27 29 assert.Equal(t, pktLineTypeData, result.Type) 28 30 assert.Equal(t, []byte("a\n"), result.Data) 29 31 30 32 // test write 31 33 w := bytes.NewBuffer([]byte{}) 32 - err = writeFlushPktLine(w) 34 + err = writeFlushPktLine(ctx, w) 33 35 assert.NoError(t, err) 34 36 assert.Equal(t, []byte("0000"), w.Bytes()) 35 37 36 38 w.Reset() 37 - err = writeDataPktLine(w, []byte("a\nb")) 39 + err = writeDataPktLine(ctx, w, []byte("a\nb")) 38 40 assert.NoError(t, err) 39 41 assert.Equal(t, []byte("0007a\nb"), w.Bytes()) 40 42 }
+5 -4
cmd/keys.go
··· 64 64 ctx, cancel := installSignals() 65 65 defer cancel() 66 66 67 - setup("keys.log", false) 67 + setup(ctx, false) 68 68 69 - authorizedString, err := private.AuthorizedPublicKeyByContent(ctx, content) 70 - if err != nil { 71 - return err 69 + authorizedString, extra := private.AuthorizedPublicKeyByContent(ctx, content) 70 + // do not use handleCliResponseExtra or cli.NewExitError, if it exists immediately, it breaks some tests like Test_CmdKeys 71 + if extra.Error != nil { 72 + return extra.Error 72 73 } 73 74 fmt.Println(strings.TrimSpace(authorizedString)) 74 75 return nil
+4 -8
cmd/mailer.go
··· 5 5 6 6 import ( 7 7 "fmt" 8 - "net/http" 9 8 10 9 "code.gitea.io/gitea/modules/private" 11 10 "code.gitea.io/gitea/modules/setting" ··· 43 42 } 44 43 } 45 44 46 - status, message := private.SendEmail(ctx, subject, body, nil) 47 - if status != http.StatusOK { 48 - fmt.Printf("error: %s\n", message) 49 - return nil 45 + respText, extra := private.SendEmail(ctx, subject, body, nil) 46 + if extra.HasError() { 47 + return handleCliResponseExtra(extra) 50 48 } 51 - 52 - fmt.Printf("Success: %s\n", message) 53 - 49 + _, _ = fmt.Printf("Sent %s email(s) to all users\n", respText) 54 50 return nil 55 51 }
+12 -37
cmd/manager.go
··· 4 4 package cmd 5 5 6 6 import ( 7 - "fmt" 8 - "net/http" 9 7 "os" 10 8 "time" 11 9 ··· 103 101 ctx, cancel := installSignals() 104 102 defer cancel() 105 103 106 - setup("manager", c.Bool("debug")) 107 - statusCode, msg := private.Shutdown(ctx) 108 - switch statusCode { 109 - case http.StatusInternalServerError: 110 - return fail("InternalServerError", msg) 111 - } 112 - 113 - fmt.Fprintln(os.Stdout, msg) 114 - return nil 104 + setup(ctx, c.Bool("debug")) 105 + extra := private.Shutdown(ctx) 106 + return handleCliResponseExtra(extra) 115 107 } 116 108 117 109 func runRestart(c *cli.Context) error { 118 110 ctx, cancel := installSignals() 119 111 defer cancel() 120 112 121 - setup("manager", c.Bool("debug")) 122 - statusCode, msg := private.Restart(ctx) 123 - switch statusCode { 124 - case http.StatusInternalServerError: 125 - return fail("InternalServerError", msg) 126 - } 127 - 128 - fmt.Fprintln(os.Stdout, msg) 129 - return nil 113 + setup(ctx, c.Bool("debug")) 114 + extra := private.Restart(ctx) 115 + return handleCliResponseExtra(extra) 130 116 } 131 117 132 118 func runFlushQueues(c *cli.Context) error { 133 119 ctx, cancel := installSignals() 134 120 defer cancel() 135 121 136 - setup("manager", c.Bool("debug")) 137 - statusCode, msg := private.FlushQueues(ctx, c.Duration("timeout"), c.Bool("non-blocking")) 138 - switch statusCode { 139 - case http.StatusInternalServerError: 140 - return fail("InternalServerError", msg) 141 - } 142 - 143 - fmt.Fprintln(os.Stdout, msg) 144 - return nil 122 + setup(ctx, c.Bool("debug")) 123 + extra := private.FlushQueues(ctx, c.Duration("timeout"), c.Bool("non-blocking")) 124 + return handleCliResponseExtra(extra) 145 125 } 146 126 147 127 func runProcesses(c *cli.Context) error { 148 128 ctx, cancel := installSignals() 149 129 defer cancel() 150 130 151 - setup("manager", c.Bool("debug")) 152 - statusCode, msg := private.Processes(ctx, os.Stdout, c.Bool("flat"), c.Bool("no-system"), c.Bool("stacktraces"), c.Bool("json"), c.String("cancel")) 153 - switch statusCode { 154 - case http.StatusInternalServerError: 155 - return fail("InternalServerError", msg) 156 - } 157 - 158 - return nil 131 + setup(ctx, c.Bool("debug")) 132 + extra := private.Processes(ctx, os.Stdout, c.Bool("flat"), c.Bool("no-system"), c.Bool("stacktraces"), c.Bool("json"), c.String("cancel")) 133 + return handleCliResponseExtra(extra) 159 134 }
+36 -57
cmd/manager_logging.go
··· 5 5 6 6 import ( 7 7 "fmt" 8 - "net/http" 9 8 "os" 10 9 11 10 "code.gitea.io/gitea/modules/log" ··· 191 190 ) 192 191 193 192 func runRemoveLogger(c *cli.Context) error { 194 - setup("manager", c.Bool("debug")) 193 + ctx, cancel := installSignals() 194 + defer cancel() 195 + 196 + setup(ctx, c.Bool("debug")) 195 197 group := c.String("group") 196 198 if len(group) == 0 { 197 199 group = log.DEFAULT 198 200 } 199 201 name := c.Args().First() 200 - ctx, cancel := installSignals() 201 - defer cancel() 202 202 203 - statusCode, msg := private.RemoveLogger(ctx, group, name) 204 - switch statusCode { 205 - case http.StatusInternalServerError: 206 - return fail("InternalServerError", msg) 207 - } 208 - 209 - fmt.Fprintln(os.Stdout, msg) 210 - return nil 203 + extra := private.RemoveLogger(ctx, group, name) 204 + return handleCliResponseExtra(extra) 211 205 } 212 206 213 207 func runAddSMTPLogger(c *cli.Context) error { 214 - setup("manager", c.Bool("debug")) 208 + ctx, cancel := installSignals() 209 + defer cancel() 210 + 211 + setup(ctx, c.Bool("debug")) 215 212 vals := map[string]interface{}{} 216 213 mode := "smtp" 217 214 if c.IsSet("host") { ··· 242 239 } 243 240 244 241 func runAddConnLogger(c *cli.Context) error { 245 - setup("manager", c.Bool("debug")) 242 + ctx, cancel := installSignals() 243 + defer cancel() 244 + 245 + setup(ctx, c.Bool("debug")) 246 246 vals := map[string]interface{}{} 247 247 mode := "conn" 248 248 vals["net"] = "tcp" ··· 269 269 } 270 270 271 271 func runAddFileLogger(c *cli.Context) error { 272 - setup("manager", c.Bool("debug")) 272 + ctx, cancel := installSignals() 273 + defer cancel() 274 + 275 + setup(ctx, c.Bool("debug")) 273 276 vals := map[string]interface{}{} 274 277 mode := "file" 275 278 if c.IsSet("filename") { ··· 299 302 } 300 303 301 304 func runAddConsoleLogger(c *cli.Context) error { 302 - setup("manager", c.Bool("debug")) 305 + ctx, cancel := installSignals() 306 + defer cancel() 307 + 308 + setup(ctx, c.Bool("debug")) 303 309 vals := map[string]interface{}{} 304 310 mode := "console" 305 311 if c.IsSet("stderr") && c.Bool("stderr") { ··· 338 344 ctx, cancel := installSignals() 339 345 defer cancel() 340 346 341 - statusCode, msg := private.AddLogger(ctx, group, name, mode, vals) 342 - switch statusCode { 343 - case http.StatusInternalServerError: 344 - return fail("InternalServerError", msg) 345 - } 346 - 347 - fmt.Fprintln(os.Stdout, msg) 348 - return nil 347 + extra := private.AddLogger(ctx, group, name, mode, vals) 348 + return handleCliResponseExtra(extra) 349 349 } 350 350 351 351 func runPauseLogging(c *cli.Context) error { 352 352 ctx, cancel := installSignals() 353 353 defer cancel() 354 354 355 - setup("manager", c.Bool("debug")) 356 - statusCode, msg := private.PauseLogging(ctx) 357 - switch statusCode { 358 - case http.StatusInternalServerError: 359 - return fail("InternalServerError", msg) 360 - } 361 - 362 - fmt.Fprintln(os.Stdout, msg) 355 + setup(ctx, c.Bool("debug")) 356 + userMsg := private.PauseLogging(ctx) 357 + _, _ = fmt.Fprintln(os.Stdout, userMsg) 363 358 return nil 364 359 } 365 360 ··· 367 362 ctx, cancel := installSignals() 368 363 defer cancel() 369 364 370 - setup("manager", c.Bool("debug")) 371 - statusCode, msg := private.ResumeLogging(ctx) 372 - switch statusCode { 373 - case http.StatusInternalServerError: 374 - return fail("InternalServerError", msg) 375 - } 376 - 377 - fmt.Fprintln(os.Stdout, msg) 365 + setup(ctx, c.Bool("debug")) 366 + userMsg := private.ResumeLogging(ctx) 367 + _, _ = fmt.Fprintln(os.Stdout, userMsg) 378 368 return nil 379 369 } 380 370 ··· 382 372 ctx, cancel := installSignals() 383 373 defer cancel() 384 374 385 - setup("manager", c.Bool("debug")) 386 - statusCode, msg := private.ReleaseReopenLogging(ctx) 387 - switch statusCode { 388 - case http.StatusInternalServerError: 389 - return fail("InternalServerError", msg) 390 - } 391 - 392 - fmt.Fprintln(os.Stdout, msg) 375 + setup(ctx, c.Bool("debug")) 376 + userMsg := private.ReleaseReopenLogging(ctx) 377 + _, _ = fmt.Fprintln(os.Stdout, userMsg) 393 378 return nil 394 379 } 395 380 396 381 func runSetLogSQL(c *cli.Context) error { 397 382 ctx, cancel := installSignals() 398 383 defer cancel() 399 - setup("manager", c.Bool("debug")) 384 + setup(ctx, c.Bool("debug")) 400 385 401 - statusCode, msg := private.SetLogSQL(ctx, !c.Bool("off")) 402 - switch statusCode { 403 - case http.StatusInternalServerError: 404 - return fail("InternalServerError", msg) 405 - } 406 - 407 - fmt.Fprintln(os.Stdout, msg) 408 - return nil 386 + extra := private.SetLogSQL(ctx, !c.Bool("off")) 387 + return handleCliResponseExtra(extra) 409 388 }
+2 -10
cmd/restore_repo.go
··· 4 4 package cmd 5 5 6 6 import ( 7 - "errors" 8 - "net/http" 9 7 "strings" 10 8 11 - "code.gitea.io/gitea/modules/log" 12 9 "code.gitea.io/gitea/modules/private" 13 10 "code.gitea.io/gitea/modules/setting" 14 11 ··· 60 57 if s := c.String("units"); s != "" { 61 58 units = strings.Split(s, ",") 62 59 } 63 - statusCode, errStr := private.RestoreRepo( 60 + extra := private.RestoreRepo( 64 61 ctx, 65 62 c.String("repo_dir"), 66 63 c.String("owner_name"), ··· 68 65 units, 69 66 c.Bool("validation"), 70 67 ) 71 - if statusCode == http.StatusOK { 72 - return nil 73 - } 74 - 75 - log.Fatal("Failed to restore repository: %v", errStr) 76 - return errors.New(errStr) 68 + return handleCliResponseExtra(extra) 77 69 }
+56 -41
cmd/serv.go
··· 7 7 import ( 8 8 "context" 9 9 "fmt" 10 - "net/http" 11 10 "net/url" 12 11 "os" 13 12 "os/exec" ··· 16 15 "strconv" 17 16 "strings" 18 17 "time" 18 + "unicode" 19 19 20 20 asymkey_model "code.gitea.io/gitea/models/asymkey" 21 21 git_model "code.gitea.io/gitea/models/git" ··· 55 55 }, 56 56 } 57 57 58 - func setup(logPath string, debug bool) { 58 + func setup(ctx context.Context, debug bool) { 59 59 _ = log.DelLogger("console") 60 60 if debug { 61 61 _ = log.NewLogger(1000, "console", "console", `{"level":"trace","stacktracelevel":"NONE","stderr":true}`) ··· 72 72 // `[repository]` `ROOT` is a relative path and $GITEA_WORK_DIR isn't passed to the SSH connection. 73 73 if _, err := os.Stat(setting.RepoRootPath); err != nil { 74 74 if os.IsNotExist(err) { 75 - _ = fail("Incorrect configuration, no repository directory.", "Directory `[repository].ROOT` %q was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository].ROOT` an absolute value.", setting.RepoRootPath) 75 + _ = fail(ctx, "Incorrect configuration, no repository directory.", "Directory `[repository].ROOT` %q was not found, please check if $GITEA_WORK_DIR is passed to the SSH connection or make `[repository].ROOT` an absolute value.", setting.RepoRootPath) 76 76 } else { 77 - _ = fail("Incorrect configuration, repository directory is inaccessible", "Directory `[repository].ROOT` %q is inaccessible. err: %v", setting.RepoRootPath, err) 77 + _ = fail(ctx, "Incorrect configuration, repository directory is inaccessible", "Directory `[repository].ROOT` %q is inaccessible. err: %v", setting.RepoRootPath, err) 78 78 } 79 79 return 80 80 } 81 81 82 82 if err := git.InitSimple(context.Background()); err != nil { 83 - _ = fail("Failed to init git", "Failed to init git, err: %v", err) 83 + _ = fail(ctx, "Failed to init git", "Failed to init git, err: %v", err) 84 84 } 85 85 } 86 86 ··· 94 94 alphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`) 95 95 ) 96 96 97 - func fail(userMessage, logMessage string, args ...interface{}) error { 97 + // fail prints message to stdout, it's mainly used for git serv and git hook commands. 98 + // The output will be passed to git client and shown to user. 99 + func fail(ctx context.Context, userMessage, logMsgFmt string, args ...interface{}) error { 100 + if userMessage == "" { 101 + userMessage = "Internal Server Error (no specific error)" 102 + } 103 + 98 104 // There appears to be a chance to cause a zombie process and failure to read the Exit status 99 105 // if nothing is outputted on stdout. 100 106 _, _ = fmt.Fprintln(os.Stdout, "") 101 107 _, _ = fmt.Fprintln(os.Stderr, "Gitea:", userMessage) 102 108 103 - if len(logMessage) > 0 { 109 + if logMsgFmt != "" { 110 + logMsg := fmt.Sprintf(logMsgFmt, args...) 104 111 if !setting.IsProd { 105 - _, _ = fmt.Fprintf(os.Stderr, logMessage+"\n", args...) 112 + _, _ = fmt.Fprintln(os.Stderr, "Gitea:", logMsg) 113 + } 114 + if userMessage != "" { 115 + if unicode.IsPunct(rune(userMessage[len(userMessage)-1])) { 116 + logMsg = userMessage + " " + logMsg 117 + } else { 118 + logMsg = userMessage + ". " + logMsg 119 + } 106 120 } 121 + _ = private.SSHLog(ctx, true, logMsg) 107 122 } 108 - ctx, cancel := installSignals() 109 - defer cancel() 123 + return cli.NewExitError("", 1) 124 + } 110 125 111 - if len(logMessage) > 0 { 112 - _ = private.SSHLog(ctx, true, fmt.Sprintf(logMessage+": ", args...)) 126 + // handleCliResponseExtra handles the extra response from the cli sub-commands 127 + // If there is a user message it will be printed to stdout 128 + // If the command failed it will return an error (the error will be printed by cli framework) 129 + func handleCliResponseExtra(extra private.ResponseExtra) error { 130 + if extra.UserMsg != "" { 131 + _, _ = fmt.Fprintln(os.Stdout, extra.UserMsg) 113 132 } 114 - return cli.NewExitError("", 1) 133 + if extra.HasError() { 134 + return cli.NewExitError(extra.Error, 1) 135 + } 136 + return nil 115 137 } 116 138 117 139 func runServ(c *cli.Context) error { ··· 119 141 defer cancel() 120 142 121 143 // FIXME: This needs to internationalised 122 - setup("serv.log", c.Bool("debug")) 144 + setup(ctx, c.Bool("debug")) 123 145 124 146 if setting.SSH.Disabled { 125 147 println("Gitea: SSH has been disabled") ··· 135 157 136 158 keys := strings.Split(c.Args()[0], "-") 137 159 if len(keys) != 2 || keys[0] != "key" { 138 - return fail("Key ID format error", "Invalid key argument: %s", c.Args()[0]) 160 + return fail(ctx, "Key ID format error", "Invalid key argument: %s", c.Args()[0]) 139 161 } 140 162 keyID, err := strconv.ParseInt(keys[1], 10, 64) 141 163 if err != nil { 142 - return fail("Key ID format error", "Invalid key argument: %s", c.Args()[1]) 164 + return fail(ctx, "Key ID parsing error", "Invalid key argument: %s", c.Args()[1]) 143 165 } 144 166 145 167 cmd := os.Getenv("SSH_ORIGINAL_COMMAND") 146 168 if len(cmd) == 0 { 147 169 key, user, err := private.ServNoCommand(ctx, keyID) 148 170 if err != nil { 149 - return fail("Internal error", "Failed to check provided key: %v", err) 171 + return fail(ctx, "Key check failed", "Failed to check provided key: %v", err) 150 172 } 151 173 switch key.Type { 152 174 case asymkey_model.KeyTypeDeploy: ··· 164 186 165 187 words, err := shellquote.Split(cmd) 166 188 if err != nil { 167 - return fail("Error parsing arguments", "Failed to parse arguments: %v", err) 189 + return fail(ctx, "Error parsing arguments", "Failed to parse arguments: %v", err) 168 190 } 169 191 170 192 if len(words) < 2 { ··· 175 197 return nil 176 198 } 177 199 } 178 - return fail("Too few arguments", "Too few arguments in cmd: %s", cmd) 200 + return fail(ctx, "Too few arguments", "Too few arguments in cmd: %s", cmd) 179 201 } 180 202 181 203 verb := words[0] ··· 187 209 var lfsVerb string 188 210 if verb == lfsAuthenticateVerb { 189 211 if !setting.LFS.StartServer { 190 - return fail("Unknown git command", "LFS authentication request over SSH denied, LFS support is disabled") 212 + return fail(ctx, "Unknown git command", "LFS authentication request over SSH denied, LFS support is disabled") 191 213 } 192 214 193 215 if len(words) > 2 { ··· 200 222 201 223 rr := strings.SplitN(repoPath, "/", 2) 202 224 if len(rr) != 2 { 203 - return fail("Invalid repository path", "Invalid repository path: %v", repoPath) 225 + return fail(ctx, "Invalid repository path", "Invalid repository path: %v", repoPath) 204 226 } 205 227 206 228 username := strings.ToLower(rr[0]) 207 229 reponame := strings.ToLower(strings.TrimSuffix(rr[1], ".git")) 208 230 209 231 if alphaDashDotPattern.MatchString(reponame) { 210 - return fail("Invalid repo name", "Invalid repo name: %s", reponame) 232 + return fail(ctx, "Invalid repo name", "Invalid repo name: %s", reponame) 211 233 } 212 234 213 235 if c.Bool("enable-pprof") { 214 236 if err := os.MkdirAll(setting.PprofDataPath, os.ModePerm); err != nil { 215 - return fail("Error while trying to create PPROF_DATA_PATH", "Error while trying to create PPROF_DATA_PATH: %v", err) 237 + return fail(ctx, "Error while trying to create PPROF_DATA_PATH", "Error while trying to create PPROF_DATA_PATH: %v", err) 216 238 } 217 239 218 240 stopCPUProfiler, err := pprof.DumpCPUProfileForUsername(setting.PprofDataPath, username) 219 241 if err != nil { 220 - return fail("Internal Server Error", "Unable to start CPU profile: %v", err) 242 + return fail(ctx, "Unable to start CPU profiler", "Unable to start CPU profile: %v", err) 221 243 } 222 244 defer func() { 223 245 stopCPUProfiler() 224 246 err := pprof.DumpMemProfileForUsername(setting.PprofDataPath, username) 225 247 if err != nil { 226 - _ = fail("Internal Server Error", "Unable to dump Mem Profile: %v", err) 248 + _ = fail(ctx, "Unable to dump Mem profile", "Unable to dump Mem Profile: %v", err) 227 249 } 228 250 }() 229 251 } 230 252 231 253 requestedMode, has := allowedCommands[verb] 232 254 if !has { 233 - return fail("Unknown git command", "Unknown git command %s", verb) 255 + return fail(ctx, "Unknown git command", "Unknown git command %s", verb) 234 256 } 235 257 236 258 if verb == lfsAuthenticateVerb { ··· 239 261 } else if lfsVerb == "download" { 240 262 requestedMode = perm.AccessModeRead 241 263 } else { 242 - return fail("Unknown LFS verb", "Unknown lfs verb %s", lfsVerb) 264 + return fail(ctx, "Unknown LFS verb", "Unknown lfs verb %s", lfsVerb) 243 265 } 244 266 } 245 267 246 - results, err := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb) 247 - if err != nil { 248 - if private.IsErrServCommand(err) { 249 - errServCommand := err.(private.ErrServCommand) 250 - if errServCommand.StatusCode != http.StatusInternalServerError { 251 - return fail("Unauthorized", "%s", errServCommand.Error()) 252 - } 253 - return fail("Internal Server Error", "%s", errServCommand.Error()) 254 - } 255 - return fail("Internal Server Error", "%s", err.Error()) 268 + results, extra := private.ServCommand(ctx, keyID, username, reponame, requestedMode, verb, lfsVerb) 269 + if extra.HasError() { 270 + return fail(ctx, extra.UserMsg, "ServCommand failed: %s", extra.Error) 256 271 } 257 272 258 273 // LFS token authentication ··· 274 289 // Sign and get the complete encoded token as a string using the secret 275 290 tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes) 276 291 if err != nil { 277 - return fail("Internal error", "Failed to sign JWT token: %v", err) 292 + return fail(ctx, "Failed to sign JWT Token", "Failed to sign JWT token: %v", err) 278 293 } 279 294 280 295 tokenAuthentication := &git_model.LFSTokenResponse{ ··· 286 301 enc := json.NewEncoder(os.Stdout) 287 302 err = enc.Encode(tokenAuthentication) 288 303 if err != nil { 289 - return fail("Internal error", "Failed to encode LFS json response: %v", err) 304 + return fail(ctx, "Failed to encode LFS json response", "Failed to encode LFS json response: %v", err) 290 305 } 291 306 return nil 292 307 } ··· 332 347 gitcmd.Env = append(gitcmd.Env, git.CommonCmdServEnvs()...) 333 348 334 349 if err = gitcmd.Run(); err != nil { 335 - return fail("Internal error", "Failed to execute git command: %v", err) 350 + return fail(ctx, "Failed to execute git command", "Failed to execute git command: %v", err) 336 351 } 337 352 338 353 // Update user key activity. 339 354 if results.KeyID > 0 { 340 355 if err = private.UpdatePublicKeyInRepo(ctx, results.KeyID, results.RepoID); err != nil { 341 - return fail("Internal error", "UpdatePublicKeyInRepo: %v", err) 356 + return fail(ctx, "Failed to update public key", "UpdatePublicKeyInRepo: %v", err) 342 357 } 343 358 } 344 359
+12 -2
modules/httplib/httplib.go
··· 8 8 "bytes" 9 9 "context" 10 10 "crypto/tls" 11 + "fmt" 11 12 "io" 12 13 "net" 13 14 "net/http" ··· 64 65 // SetTimeout sets connect time out and read-write time out for BeegoRequest. 65 66 func (r *Request) SetTimeout(connectTimeout, readWriteTimeout time.Duration) *Request { 66 67 r.setting.ConnectTimeout = connectTimeout 68 + r.setting.ReadWriteTimeout = readWriteTimeout 69 + return r 70 + } 71 + 72 + func (r *Request) SetReadWriteTimeout(readWriteTimeout time.Duration) *Request { 67 73 r.setting.ReadWriteTimeout = readWriteTimeout 68 74 return r 69 75 } ··· 138 144 r.Body(paramBody) 139 145 } 140 146 141 - url, err := url.Parse(r.url) 147 + var err error 148 + r.req.URL, err = url.Parse(r.url) 142 149 if err != nil { 143 150 return nil, err 144 151 } 145 - r.req.URL = url 146 152 147 153 trans := r.setting.Transport 148 154 if trans == nil { ··· 194 200 return conn, nil 195 201 } 196 202 } 203 + 204 + func (r *Request) GoString() string { 205 + return fmt.Sprintf("%s %s", r.req.Method, r.url) 206 + }
+21 -104
modules/private/hook.go
··· 5 5 6 6 import ( 7 7 "context" 8 - "errors" 9 8 "fmt" 10 - "net/http" 11 9 "net/url" 12 10 "strconv" 13 11 "time" 14 12 15 - "code.gitea.io/gitea/modules/json" 16 13 "code.gitea.io/gitea/modules/setting" 17 14 ) 18 15 ··· 99 96 } 100 97 101 98 // HookPreReceive check whether the provided commits are allowed 102 - func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (int, string) { 103 - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s", 104 - url.PathEscape(ownerName), 105 - url.PathEscape(repoName), 106 - ) 107 - req := newInternalRequest(ctx, reqURL, "POST") 108 - req = req.Header("Content-Type", "application/json") 109 - jsonBytes, _ := json.Marshal(opts) 110 - req.Body(jsonBytes) 111 - req.SetTimeout(60*time.Second, time.Duration(60+len(opts.OldCommitIDs))*time.Second) 112 - resp, err := req.Response() 113 - if err != nil { 114 - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) 115 - } 116 - defer resp.Body.Close() 117 - 118 - if resp.StatusCode != http.StatusOK { 119 - return resp.StatusCode, decodeJSONError(resp).Err 120 - } 121 - 122 - return http.StatusOK, "" 99 + func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) ResponseExtra { 100 + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) 101 + req := newInternalRequest(ctx, reqURL, "POST", opts) 102 + req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) 103 + _, extra := requestJSONResp(req, &responseText{}) 104 + return extra 123 105 } 124 106 125 107 // HookPostReceive updates services and users 126 - func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookPostReceiveResult, string) { 127 - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/post-receive/%s/%s", 128 - url.PathEscape(ownerName), 129 - url.PathEscape(repoName), 130 - ) 131 - 132 - req := newInternalRequest(ctx, reqURL, "POST") 133 - req = req.Header("Content-Type", "application/json") 134 - req.SetTimeout(60*time.Second, time.Duration(60+len(opts.OldCommitIDs))*time.Second) 135 - jsonBytes, _ := json.Marshal(opts) 136 - req.Body(jsonBytes) 137 - resp, err := req.Response() 138 - if err != nil { 139 - return nil, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) 140 - } 141 - defer resp.Body.Close() 142 - 143 - if resp.StatusCode != http.StatusOK { 144 - return nil, decodeJSONError(resp).Err 145 - } 146 - res := &HookPostReceiveResult{} 147 - _ = json.NewDecoder(resp.Body).Decode(res) 148 - 149 - return res, "" 108 + func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookPostReceiveResult, ResponseExtra) { 109 + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/post-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) 110 + req := newInternalRequest(ctx, reqURL, "POST", opts) 111 + req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) 112 + return requestJSONResp(req, &HookPostReceiveResult{}) 150 113 } 151 114 152 115 // HookProcReceive proc-receive hook 153 - func HookProcReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookProcReceiveResult, error) { 154 - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/proc-receive/%s/%s", 155 - url.PathEscape(ownerName), 156 - url.PathEscape(repoName), 157 - ) 116 + func HookProcReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HookProcReceiveResult, ResponseExtra) { 117 + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/proc-receive/%s/%s", url.PathEscape(ownerName), url.PathEscape(repoName)) 158 118 159 - req := newInternalRequest(ctx, reqURL, "POST") 160 - req = req.Header("Content-Type", "application/json") 161 - req.SetTimeout(60*time.Second, time.Duration(60+len(opts.OldCommitIDs))*time.Second) 162 - jsonBytes, _ := json.Marshal(opts) 163 - req.Body(jsonBytes) 164 - resp, err := req.Response() 165 - if err != nil { 166 - return nil, fmt.Errorf("Unable to contact gitea: %w", err) 167 - } 168 - defer resp.Body.Close() 169 - 170 - if resp.StatusCode != http.StatusOK { 171 - return nil, errors.New(decodeJSONError(resp).Err) 172 - } 173 - res := &HookProcReceiveResult{} 174 - _ = json.NewDecoder(resp.Body).Decode(res) 175 - 176 - return res, nil 119 + req := newInternalRequest(ctx, reqURL, "POST", opts) 120 + req.SetReadWriteTimeout(time.Duration(60+len(opts.OldCommitIDs)) * time.Second) 121 + return requestJSONResp(req, &HookProcReceiveResult{}) 177 122 } 178 123 179 124 // SetDefaultBranch will set the default branch to the provided branch for the provided repository 180 - func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) error { 125 + func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) ResponseExtra { 181 126 reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/set-default-branch/%s/%s/%s", 182 127 url.PathEscape(ownerName), 183 128 url.PathEscape(repoName), 184 129 url.PathEscape(branch), 185 130 ) 186 131 req := newInternalRequest(ctx, reqURL, "POST") 187 - req = req.Header("Content-Type", "application/json") 188 - 189 - req.SetTimeout(60*time.Second, 60*time.Second) 190 - resp, err := req.Response() 191 - if err != nil { 192 - return fmt.Errorf("Unable to contact gitea: %w", err) 193 - } 194 - defer resp.Body.Close() 195 - if resp.StatusCode != http.StatusOK { 196 - return fmt.Errorf("Error returned from gitea: %v", decodeJSONError(resp).Err) 197 - } 198 - return nil 132 + return requestJSONUserMsg(req, "") 199 133 } 200 134 201 135 // SSHLog sends ssh error log response 202 136 func SSHLog(ctx context.Context, isErr bool, msg string) error { 203 137 reqURL := setting.LocalURL + "api/internal/ssh/log" 204 - req := newInternalRequest(ctx, reqURL, "POST") 205 - req = req.Header("Content-Type", "application/json") 206 - 207 - jsonBytes, _ := json.Marshal(&SSHLogOption{ 208 - IsError: isErr, 209 - Message: msg, 210 - }) 211 - req.Body(jsonBytes) 212 - 213 - req.SetTimeout(60*time.Second, 60*time.Second) 214 - resp, err := req.Response() 215 - if err != nil { 216 - return fmt.Errorf("unable to contact gitea: %w", err) 217 - } 218 - 219 - defer resp.Body.Close() 220 - if resp.StatusCode != http.StatusOK { 221 - return fmt.Errorf("Error returned from gitea: %v", decodeJSONError(resp).Err) 222 - } 223 - return nil 138 + req := newInternalRequest(ctx, reqURL, "POST", &SSHLogOption{IsError: isErr, Message: msg}) 139 + _, extra := requestJSONResp(req, &responseText{}) 140 + return extra.Error 224 141 }
+29 -27
modules/private/internal.go
··· 11 11 "net/http" 12 12 "os" 13 13 "strings" 14 + "time" 14 15 15 16 "code.gitea.io/gitea/modules/httplib" 16 17 "code.gitea.io/gitea/modules/json" ··· 19 20 "code.gitea.io/gitea/modules/setting" 20 21 ) 21 22 22 - func newRequest(ctx context.Context, url, method, sourceIP string) *httplib.Request { 23 - if setting.InternalToken == "" { 24 - log.Fatal(`The INTERNAL_TOKEN setting is missing from the configuration file: %q. 25 - Ensure you are running in the correct environment or set the correct configuration file with -c.`, setting.CustomConf) 26 - } 27 - return httplib.NewRequest(url, method). 28 - SetContext(ctx). 29 - Header("X-Real-IP", sourceIP). 30 - Header("Authorization", fmt.Sprintf("Bearer %s", setting.InternalToken)) 31 - } 32 - 33 - // Response internal request response 23 + // Response is used for internal request response (for user message and error message) 34 24 type Response struct { 35 - Err string `json:"err"` 36 - } 37 - 38 - func decodeJSONError(resp *http.Response) *Response { 39 - var res Response 40 - err := json.NewDecoder(resp.Body).Decode(&res) 41 - if err != nil { 42 - res.Err = err.Error() 43 - } 44 - return &res 25 + Err string `json:"err,omitempty"` // server-side error log message, it won't be exposed to end users 26 + UserMsg string `json:"user_msg,omitempty"` // meaningful error message for end users, it will be shown in git client's output. 45 27 } 46 28 47 29 func getClientIP() string { ··· 52 34 return strings.Fields(sshConnEnv)[0] 53 35 } 54 36 55 - func newInternalRequest(ctx context.Context, url, method string) *httplib.Request { 56 - req := newRequest(ctx, url, method, getClientIP()).SetTLSClientConfig(&tls.Config{ 57 - InsecureSkipVerify: true, 58 - ServerName: setting.Domain, 59 - }) 37 + func newInternalRequest(ctx context.Context, url, method string, body ...any) *httplib.Request { 38 + if setting.InternalToken == "" { 39 + log.Fatal(`The INTERNAL_TOKEN setting is missing from the configuration file: %q. 40 + Ensure you are running in the correct environment or set the correct configuration file with -c.`, setting.CustomConf) 41 + } 42 + 43 + req := httplib.NewRequest(url, method). 44 + SetContext(ctx). 45 + Header("X-Real-IP", getClientIP()). 46 + Header("Authorization", fmt.Sprintf("Bearer %s", setting.InternalToken)). 47 + SetTLSClientConfig(&tls.Config{ 48 + InsecureSkipVerify: true, 49 + ServerName: setting.Domain, 50 + }) 51 + 60 52 if setting.Protocol == setting.HTTPUnix { 61 53 req.SetTransport(&http.Transport{ 62 54 DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { ··· 90 82 }, 91 83 }) 92 84 } 85 + 86 + if len(body) == 1 { 87 + req.Header("Content-Type", "application/json") 88 + jsonBytes, _ := json.Marshal(body[0]) 89 + req.Body(jsonBytes) 90 + } else if len(body) > 1 { 91 + log.Fatal("Too many arguments for newInternalRequest") 92 + } 93 + 94 + req.SetTimeout(10*time.Second, 60*time.Second) 93 95 return req 94 96 }
+6 -29
modules/private/key.go
··· 6 6 import ( 7 7 "context" 8 8 "fmt" 9 - "io" 10 - "net/http" 11 9 12 10 "code.gitea.io/gitea/modules/setting" 13 11 ) ··· 16 14 func UpdatePublicKeyInRepo(ctx context.Context, keyID, repoID int64) error { 17 15 // Ask for running deliver hook and test pull request tasks. 18 16 reqURL := setting.LocalURL + fmt.Sprintf("api/internal/ssh/%d/update/%d", keyID, repoID) 19 - resp, err := newInternalRequest(ctx, reqURL, "POST").Response() 20 - if err != nil { 21 - return err 22 - } 23 - 24 - defer resp.Body.Close() 25 - 26 - // All 2XX status codes are accepted and others will return an error 27 - if resp.StatusCode/100 != 2 { 28 - return fmt.Errorf("Failed to update public key: %s", decodeJSONError(resp).Err) 29 - } 30 - return nil 17 + req := newInternalRequest(ctx, reqURL, "POST") 18 + _, extra := requestJSONResp(req, &responseText{}) 19 + return extra.Error 31 20 } 32 21 33 22 // AuthorizedPublicKeyByContent searches content as prefix (leak e-mail part) 34 23 // and returns public key found. 35 - func AuthorizedPublicKeyByContent(ctx context.Context, content string) (string, error) { 24 + func AuthorizedPublicKeyByContent(ctx context.Context, content string) (string, ResponseExtra) { 36 25 // Ask for running deliver hook and test pull request tasks. 37 26 reqURL := setting.LocalURL + "api/internal/ssh/authorized_keys" 38 27 req := newInternalRequest(ctx, reqURL, "POST") 39 28 req.Param("content", content) 40 - resp, err := req.Response() 41 - if err != nil { 42 - return "", err 43 - } 44 - 45 - defer resp.Body.Close() 46 - 47 - // All 2XX status codes are accepted and others will return an error 48 - if resp.StatusCode != http.StatusOK { 49 - return "", fmt.Errorf("Failed to update public key: %s", decodeJSONError(resp).Err) 50 - } 51 - bs, err := io.ReadAll(resp.Body) 52 - 53 - return string(bs), err 29 + resp, extra := requestJSONResp(req, &responseText{}) 30 + return resp.Text, extra 54 31 }
+5 -29
modules/private/mail.go
··· 5 5 6 6 import ( 7 7 "context" 8 - "fmt" 9 - "io" 10 - "net/http" 11 8 12 - "code.gitea.io/gitea/modules/json" 13 9 "code.gitea.io/gitea/modules/setting" 14 10 ) 15 11 ··· 21 17 } 22 18 23 19 // SendEmail calls the internal SendEmail function 24 - // 25 20 // It accepts a list of usernames. 26 21 // If DB contains these users it will send the email to them. 27 - // 28 - // If to list == nil its supposed to send an email to every 29 - // user present in DB 30 - func SendEmail(ctx context.Context, subject, message string, to []string) (int, string) { 22 + // If to list == nil, it's supposed to send emails to every user present in DB 23 + func SendEmail(ctx context.Context, subject, message string, to []string) (string, ResponseExtra) { 31 24 reqURL := setting.LocalURL + "api/internal/mail/send" 32 25 33 - req := newInternalRequest(ctx, reqURL, "POST") 34 - req = req.Header("Content-Type", "application/json") 35 - jsonBytes, _ := json.Marshal(Email{ 26 + req := newInternalRequest(ctx, reqURL, "POST", Email{ 36 27 Subject: subject, 37 28 Message: message, 38 29 To: to, 39 30 }) 40 - req.Body(jsonBytes) 41 - resp, err := req.Response() 42 - if err != nil { 43 - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) 44 - } 45 - defer resp.Body.Close() 46 31 47 - body, err := io.ReadAll(resp.Body) 48 - if err != nil { 49 - return http.StatusInternalServerError, fmt.Sprintf("Response body error: %v", err.Error()) 50 - } 51 - 52 - users := fmt.Sprintf("%d", len(to)) 53 - if len(to) == 0 { 54 - users = "all" 55 - } 56 - 57 - return http.StatusOK, fmt.Sprintf("Sent %s email(s) to %s users", body, users) 32 + resp, extra := requestJSONResp(req, &responseText{}) 33 + return resp.Text, extra 58 34 }
+26 -145
modules/private/manager.go
··· 12 12 "strconv" 13 13 "time" 14 14 15 - "code.gitea.io/gitea/modules/json" 16 15 "code.gitea.io/gitea/modules/setting" 17 16 ) 18 17 19 18 // Shutdown calls the internal shutdown function 20 - func Shutdown(ctx context.Context) (int, string) { 19 + func Shutdown(ctx context.Context) ResponseExtra { 21 20 reqURL := setting.LocalURL + "api/internal/manager/shutdown" 22 - 23 21 req := newInternalRequest(ctx, reqURL, "POST") 24 - resp, err := req.Response() 25 - if err != nil { 26 - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) 27 - } 28 - defer resp.Body.Close() 29 - 30 - if resp.StatusCode != http.StatusOK { 31 - return resp.StatusCode, decodeJSONError(resp).Err 32 - } 33 - 34 - return http.StatusOK, "Shutting down" 22 + return requestJSONUserMsg(req, "Shutting down") 35 23 } 36 24 37 25 // Restart calls the internal restart function 38 - func Restart(ctx context.Context) (int, string) { 26 + func Restart(ctx context.Context) ResponseExtra { 39 27 reqURL := setting.LocalURL + "api/internal/manager/restart" 40 - 41 28 req := newInternalRequest(ctx, reqURL, "POST") 42 - resp, err := req.Response() 43 - if err != nil { 44 - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) 45 - } 46 - defer resp.Body.Close() 47 - 48 - if resp.StatusCode != http.StatusOK { 49 - return resp.StatusCode, decodeJSONError(resp).Err 50 - } 51 - 52 - return http.StatusOK, "Restarting" 29 + return requestJSONUserMsg(req, "Restarting") 53 30 } 54 31 55 32 // FlushOptions represents the options for the flush call ··· 59 36 } 60 37 61 38 // FlushQueues calls the internal flush-queues function 62 - func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) (int, string) { 39 + func FlushQueues(ctx context.Context, timeout time.Duration, nonBlocking bool) ResponseExtra { 63 40 reqURL := setting.LocalURL + "api/internal/manager/flush-queues" 64 - 65 - req := newInternalRequest(ctx, reqURL, "POST") 41 + req := newInternalRequest(ctx, reqURL, "POST", FlushOptions{Timeout: timeout, NonBlocking: nonBlocking}) 66 42 if timeout > 0 { 67 - req.SetTimeout(timeout+10*time.Second, timeout+10*time.Second) 68 - } 69 - req = req.Header("Content-Type", "application/json") 70 - jsonBytes, _ := json.Marshal(FlushOptions{ 71 - Timeout: timeout, 72 - NonBlocking: nonBlocking, 73 - }) 74 - req.Body(jsonBytes) 75 - resp, err := req.Response() 76 - if err != nil { 77 - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) 43 + req.SetReadWriteTimeout(timeout + 10*time.Second) 78 44 } 79 - defer resp.Body.Close() 80 - 81 - if resp.StatusCode != http.StatusOK { 82 - return resp.StatusCode, decodeJSONError(resp).Err 83 - } 84 - 85 - return http.StatusOK, "Flushed" 45 + return requestJSONUserMsg(req, "Flushed") 86 46 } 87 47 88 48 // PauseLogging pauses logging 89 - func PauseLogging(ctx context.Context) (int, string) { 49 + func PauseLogging(ctx context.Context) ResponseExtra { 90 50 reqURL := setting.LocalURL + "api/internal/manager/pause-logging" 91 - 92 51 req := newInternalRequest(ctx, reqURL, "POST") 93 - resp, err := req.Response() 94 - if err != nil { 95 - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) 96 - } 97 - defer resp.Body.Close() 98 - 99 - if resp.StatusCode != http.StatusOK { 100 - return resp.StatusCode, decodeJSONError(resp).Err 101 - } 102 - 103 - return http.StatusOK, "Logging Paused" 52 + return requestJSONUserMsg(req, "Logging Paused") 104 53 } 105 54 106 55 // ResumeLogging resumes logging 107 - func ResumeLogging(ctx context.Context) (int, string) { 56 + func ResumeLogging(ctx context.Context) ResponseExtra { 108 57 reqURL := setting.LocalURL + "api/internal/manager/resume-logging" 109 - 110 58 req := newInternalRequest(ctx, reqURL, "POST") 111 - resp, err := req.Response() 112 - if err != nil { 113 - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) 114 - } 115 - defer resp.Body.Close() 116 - 117 - if resp.StatusCode != http.StatusOK { 118 - return resp.StatusCode, decodeJSONError(resp).Err 119 - } 120 - 121 - return http.StatusOK, "Logging Restarted" 59 + return requestJSONUserMsg(req, "Logging Restarted") 122 60 } 123 61 124 62 // ReleaseReopenLogging releases and reopens logging files 125 - func ReleaseReopenLogging(ctx context.Context) (int, string) { 63 + func ReleaseReopenLogging(ctx context.Context) ResponseExtra { 126 64 reqURL := setting.LocalURL + "api/internal/manager/release-and-reopen-logging" 127 - 128 65 req := newInternalRequest(ctx, reqURL, "POST") 129 - resp, err := req.Response() 130 - if err != nil { 131 - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) 132 - } 133 - defer resp.Body.Close() 134 - 135 - if resp.StatusCode != http.StatusOK { 136 - return resp.StatusCode, decodeJSONError(resp).Err 137 - } 138 - 139 - return http.StatusOK, "Logging Restarted" 66 + return requestJSONUserMsg(req, "Logging Restarted") 140 67 } 141 68 142 69 // SetLogSQL sets database logging 143 - func SetLogSQL(ctx context.Context, on bool) (int, string) { 70 + func SetLogSQL(ctx context.Context, on bool) ResponseExtra { 144 71 reqURL := setting.LocalURL + "api/internal/manager/set-log-sql?on=" + strconv.FormatBool(on) 145 - 146 72 req := newInternalRequest(ctx, reqURL, "POST") 147 - resp, err := req.Response() 148 - if err != nil { 149 - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) 150 - } 151 - defer resp.Body.Close() 152 - 153 - if resp.StatusCode != http.StatusOK { 154 - return resp.StatusCode, decodeJSONError(resp).Err 155 - } 156 - 157 - return http.StatusOK, "Log SQL setting set" 73 + return requestJSONUserMsg(req, "Log SQL setting set") 158 74 } 159 75 160 76 // LoggerOptions represents the options for the add logger call ··· 166 82 } 167 83 168 84 // AddLogger adds a logger 169 - func AddLogger(ctx context.Context, group, name, mode string, config map[string]interface{}) (int, string) { 85 + func AddLogger(ctx context.Context, group, name, mode string, config map[string]interface{}) ResponseExtra { 170 86 reqURL := setting.LocalURL + "api/internal/manager/add-logger" 171 - 172 - req := newInternalRequest(ctx, reqURL, "POST") 173 - req = req.Header("Content-Type", "application/json") 174 - jsonBytes, _ := json.Marshal(LoggerOptions{ 87 + req := newInternalRequest(ctx, reqURL, "POST", LoggerOptions{ 175 88 Group: group, 176 89 Name: name, 177 90 Mode: mode, 178 91 Config: config, 179 92 }) 180 - req.Body(jsonBytes) 181 - resp, err := req.Response() 182 - if err != nil { 183 - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) 184 - } 185 - defer resp.Body.Close() 186 - 187 - if resp.StatusCode != http.StatusOK { 188 - return resp.StatusCode, decodeJSONError(resp).Err 189 - } 190 - 191 - return http.StatusOK, "Added" 93 + return requestJSONUserMsg(req, "Added") 192 94 } 193 95 194 96 // RemoveLogger removes a logger 195 - func RemoveLogger(ctx context.Context, group, name string) (int, string) { 97 + func RemoveLogger(ctx context.Context, group, name string) ResponseExtra { 196 98 reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/remove-logger/%s/%s", url.PathEscape(group), url.PathEscape(name)) 197 - 198 99 req := newInternalRequest(ctx, reqURL, "POST") 199 - resp, err := req.Response() 200 - if err != nil { 201 - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) 202 - } 203 - defer resp.Body.Close() 204 - 205 - if resp.StatusCode != http.StatusOK { 206 - return resp.StatusCode, decodeJSONError(resp).Err 207 - } 208 - 209 - return http.StatusOK, "Removed" 100 + return requestJSONUserMsg(req, "Removed") 210 101 } 211 102 212 103 // Processes return the current processes from this gitea instance 213 - func Processes(ctx context.Context, out io.Writer, flat, noSystem, stacktraces, json bool, cancel string) (int, string) { 104 + func Processes(ctx context.Context, out io.Writer, flat, noSystem, stacktraces, json bool, cancel string) ResponseExtra { 214 105 reqURL := setting.LocalURL + fmt.Sprintf("api/internal/manager/processes?flat=%t&no-system=%t&stacktraces=%t&json=%t&cancel-pid=%s", flat, noSystem, stacktraces, json, url.QueryEscape(cancel)) 215 106 216 107 req := newInternalRequest(ctx, reqURL, "GET") 217 - resp, err := req.Response() 218 - if err != nil { 219 - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) 108 + callback := func(resp *http.Response, extra *ResponseExtra) { 109 + _, extra.Error = io.Copy(out, resp.Body) 220 110 } 221 - defer resp.Body.Close() 222 - 223 - if resp.StatusCode != http.StatusOK { 224 - return resp.StatusCode, decodeJSONError(resp).Err 225 - } 226 - 227 - _, err = io.Copy(out, resp.Body) 228 - if err != nil { 229 - return http.StatusInternalServerError, err.Error() 230 - } 231 - return http.StatusOK, "" 111 + _, extra := requestJSONResp(req, &callback) 112 + return extra 232 113 }
+135
modules/private/request.go
··· 1 + // Copyright 2023 The Gitea Authors. All rights reserved. 2 + // SPDX-License-Identifier: MIT 3 + 4 + package private 5 + 6 + import ( 7 + "fmt" 8 + "io" 9 + "net/http" 10 + "unicode" 11 + 12 + "code.gitea.io/gitea/modules/httplib" 13 + "code.gitea.io/gitea/modules/json" 14 + ) 15 + 16 + // responseText is used to get the response as text, instead of parsing it as JSON. 17 + type responseText struct { 18 + Text string 19 + } 20 + 21 + // ResponseExtra contains extra information about the response, especially for error responses. 22 + type ResponseExtra struct { 23 + StatusCode int 24 + UserMsg string 25 + Error error 26 + } 27 + 28 + type responseCallback func(resp *http.Response, extra *ResponseExtra) 29 + 30 + func (re *ResponseExtra) HasError() bool { 31 + return re.Error != nil 32 + } 33 + 34 + type responseError struct { 35 + statusCode int 36 + errorString string 37 + } 38 + 39 + func (re responseError) Error() string { 40 + if re.errorString == "" { 41 + return fmt.Sprintf("internal API error response, status=%d", re.statusCode) 42 + } 43 + return fmt.Sprintf("internal API error response, status=%d, err=%s", re.statusCode, re.errorString) 44 + } 45 + 46 + // requestJSONUserMsg sends a request to the gitea server and then parses the response. 47 + // If the status code is not 2xx, or any error occurs, the ResponseExtra.Error field is guaranteed to be non-nil, 48 + // and the ResponseExtra.UserMsg field will be set to a message for the end user. 49 + // 50 + // * If the "res" is a struct pointer, the response will be parsed as JSON 51 + // * If the "res" is responseText pointer, the response will be stored as text in it 52 + // * If the "res" is responseCallback pointer, the callback function should set the ResponseExtra fields accordingly 53 + func requestJSONResp[T any](req *httplib.Request, res *T) (ret *T, extra ResponseExtra) { 54 + resp, err := req.Response() 55 + if err != nil { 56 + extra.UserMsg = "Internal Server Connection Error" 57 + extra.Error = fmt.Errorf("unable to contact gitea %q: %w", req.GoString(), err) 58 + return nil, extra 59 + } 60 + defer resp.Body.Close() 61 + 62 + extra.StatusCode = resp.StatusCode 63 + 64 + // if the status code is not 2xx, try to parse the error response 65 + if resp.StatusCode/100 != 2 { 66 + var respErr Response 67 + if err := json.NewDecoder(resp.Body).Decode(&respErr); err != nil { 68 + extra.UserMsg = "Internal Server Error Decoding Failed" 69 + extra.Error = fmt.Errorf("unable to decode error response %q: %w", req.GoString(), err) 70 + return nil, extra 71 + } 72 + extra.UserMsg = respErr.UserMsg 73 + if extra.UserMsg == "" { 74 + extra.UserMsg = "Internal Server Error (no message for end users)" 75 + } 76 + extra.Error = responseError{statusCode: resp.StatusCode, errorString: respErr.Err} 77 + return res, extra 78 + } 79 + 80 + // now, the StatusCode must be 2xx 81 + var v any = res 82 + if respText, ok := v.(*responseText); ok { 83 + // get the whole response as a text string 84 + bs, err := io.ReadAll(resp.Body) 85 + if err != nil { 86 + extra.UserMsg = "Internal Server Response Reading Failed" 87 + extra.Error = fmt.Errorf("unable to read response %q: %w", req.GoString(), err) 88 + return nil, extra 89 + } 90 + respText.Text = string(bs) 91 + return res, extra 92 + } else if callback, ok := v.(*responseCallback); ok { 93 + // pass the response to callback, and let the callback update the ResponseExtra 94 + extra.StatusCode = resp.StatusCode 95 + (*callback)(resp, &extra) 96 + return nil, extra 97 + } else if err := json.NewDecoder(resp.Body).Decode(res); err != nil { 98 + // decode the response into the given struct 99 + extra.UserMsg = "Internal Server Response Decoding Failed" 100 + extra.Error = fmt.Errorf("unable to decode response %q: %w", req.GoString(), err) 101 + return nil, extra 102 + } 103 + 104 + if respMsg, ok := v.(*Response); ok { 105 + // if the "res" is Response structure, try to get the UserMsg from it and update the ResponseExtra 106 + extra.UserMsg = respMsg.UserMsg 107 + if respMsg.Err != "" { 108 + // usually this shouldn't happen, because the StatusCode is 2xx, there should be no error. 109 + // but we still handle the "err" response, in case some people return error messages by status code 200. 110 + extra.Error = responseError{statusCode: resp.StatusCode, errorString: respMsg.Err} 111 + } 112 + } 113 + 114 + return res, extra 115 + } 116 + 117 + // requestJSONUserMsg sends a request to the gitea server and then parses the response as private.Response 118 + // If the request succeeds, the successMsg will be used as part of ResponseExtra.UserMsg. 119 + func requestJSONUserMsg(req *httplib.Request, successMsg string) ResponseExtra { 120 + resp, extra := requestJSONResp(req, &Response{}) 121 + if extra.HasError() { 122 + return extra 123 + } 124 + if resp.UserMsg == "" { 125 + extra.UserMsg = successMsg // if UserMsg is empty, then use successMsg as userMsg 126 + } else if successMsg != "" { 127 + // else, now UserMsg is not empty, if successMsg is not empty, then append successMsg to UserMsg 128 + if unicode.IsPunct(rune(extra.UserMsg[len(extra.UserMsg)-1])) { 129 + extra.UserMsg = extra.UserMsg + " " + successMsg 130 + } else { 131 + extra.UserMsg = extra.UserMsg + ". " + successMsg 132 + } 133 + } 134 + return extra 135 + }
+4 -30
modules/private/restore_repo.go
··· 6 6 import ( 7 7 "context" 8 8 "fmt" 9 - "io" 10 - "net/http" 11 9 "time" 12 10 13 - "code.gitea.io/gitea/modules/json" 14 11 "code.gitea.io/gitea/modules/setting" 15 12 ) 16 13 ··· 24 21 } 25 22 26 23 // RestoreRepo calls the internal RestoreRepo function 27 - func RestoreRepo(ctx context.Context, repoDir, ownerName, repoName string, units []string, validation bool) (int, string) { 24 + func RestoreRepo(ctx context.Context, repoDir, ownerName, repoName string, units []string, validation bool) ResponseExtra { 28 25 reqURL := setting.LocalURL + "api/internal/restore_repo" 29 26 30 - req := newInternalRequest(ctx, reqURL, "POST") 31 - req.SetTimeout(3*time.Second, 0) // since the request will spend much time, don't timeout 32 - req = req.Header("Content-Type", "application/json") 33 - jsonBytes, _ := json.Marshal(RestoreParams{ 27 + req := newInternalRequest(ctx, reqURL, "POST", RestoreParams{ 34 28 RepoDir: repoDir, 35 29 OwnerName: ownerName, 36 30 RepoName: repoName, 37 31 Units: units, 38 32 Validation: validation, 39 33 }) 40 - req.Body(jsonBytes) 41 - resp, err := req.Response() 42 - if err != nil { 43 - return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v, could you confirm it's running?", err.Error()) 44 - } 45 - defer resp.Body.Close() 46 - 47 - if resp.StatusCode != http.StatusOK { 48 - ret := struct { 49 - Err string `json:"err"` 50 - }{} 51 - body, err := io.ReadAll(resp.Body) 52 - if err != nil { 53 - return http.StatusInternalServerError, fmt.Sprintf("Response body error: %v", err.Error()) 54 - } 55 - if err := json.Unmarshal(body, &ret); err != nil { 56 - return http.StatusInternalServerError, fmt.Sprintf("Response body Unmarshal error: %v", err.Error()) 57 - } 58 - return http.StatusInternalServerError, ret.Err 59 - } 60 - 61 - return http.StatusOK, fmt.Sprintf("Restore repo %s/%s successfully", ownerName, repoName) 34 + req.SetTimeout(3*time.Second, 0) // since the request will spend much time, don't timeout 35 + return requestJSONUserMsg(req, fmt.Sprintf("Restore repo %s/%s successfully", ownerName, repoName)) 62 36 }
+10 -55
modules/private/serv.go
··· 6 6 import ( 7 7 "context" 8 8 "fmt" 9 - "net/http" 10 9 "net/url" 11 10 12 11 asymkey_model "code.gitea.io/gitea/models/asymkey" 13 12 "code.gitea.io/gitea/models/perm" 14 13 user_model "code.gitea.io/gitea/models/user" 15 - "code.gitea.io/gitea/modules/json" 16 14 "code.gitea.io/gitea/modules/setting" 17 15 ) 18 16 ··· 24 22 25 23 // ServNoCommand returns information about the provided key 26 24 func ServNoCommand(ctx context.Context, keyID int64) (*asymkey_model.PublicKey, *user_model.User, error) { 27 - reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/none/%d", 28 - keyID) 29 - resp, err := newInternalRequest(ctx, reqURL, "GET").Response() 30 - if err != nil { 31 - return nil, nil, err 32 - } 33 - defer resp.Body.Close() 34 - if resp.StatusCode != http.StatusOK { 35 - return nil, nil, fmt.Errorf("%s", decodeJSONError(resp).Err) 36 - } 37 - 38 - var keyAndOwner KeyAndOwner 39 - if err := json.NewDecoder(resp.Body).Decode(&keyAndOwner); err != nil { 40 - return nil, nil, err 25 + reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/none/%d", keyID) 26 + req := newInternalRequest(ctx, reqURL, "GET") 27 + keyAndOwner, extra := requestJSONResp(req, &KeyAndOwner{}) 28 + if extra.HasError() { 29 + return nil, nil, extra.Error 41 30 } 42 31 return keyAndOwner.Key, keyAndOwner.Owner, nil 43 32 } ··· 56 45 RepoID int64 57 46 } 58 47 59 - // ErrServCommand is an error returned from ServCommmand. 60 - type ErrServCommand struct { 61 - Results ServCommandResults 62 - Err string 63 - StatusCode int 64 - } 65 - 66 - func (err ErrServCommand) Error() string { 67 - return err.Err 68 - } 69 - 70 - // IsErrServCommand checks if an error is a ErrServCommand. 71 - func IsErrServCommand(err error) bool { 72 - _, ok := err.(ErrServCommand) 73 - return ok 74 - } 75 - 76 48 // ServCommand preps for a serv call 77 - func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, mode perm.AccessMode, verbs ...string) (*ServCommandResults, error) { 49 + func ServCommand(ctx context.Context, keyID int64, ownerName, repoName string, mode perm.AccessMode, verbs ...string) (*ServCommandResults, ResponseExtra) { 78 50 reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/command/%d/%s/%s?mode=%d", 79 51 keyID, 80 52 url.PathEscape(ownerName), 81 53 url.PathEscape(repoName), 82 - mode) 54 + mode, 55 + ) 83 56 for _, verb := range verbs { 84 57 if verb != "" { 85 58 reqURL += fmt.Sprintf("&verb=%s", url.QueryEscape(verb)) 86 59 } 87 60 } 88 - 89 - resp, err := newInternalRequest(ctx, reqURL, "GET").Response() 90 - if err != nil { 91 - return nil, err 92 - } 93 - defer resp.Body.Close() 94 - 95 - if resp.StatusCode != http.StatusOK { 96 - var errServCommand ErrServCommand 97 - if err := json.NewDecoder(resp.Body).Decode(&errServCommand); err != nil { 98 - return nil, err 99 - } 100 - errServCommand.StatusCode = resp.StatusCode 101 - return nil, errServCommand 102 - } 103 - var results ServCommandResults 104 - if err := json.NewDecoder(resp.Body).Decode(&results); err != nil { 105 - return nil, err 106 - } 107 - return &results, nil 61 + req := newInternalRequest(ctx, reqURL, "GET") 62 + return requestJSONResp(req, &ServCommandResults{}) 108 63 }
-14
routers/private/default_branch.go
··· 1 1 // Copyright 2021 The Gitea Authors. All rights reserved. 2 2 // SPDX-License-Identifier: MIT 3 3 4 - // Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. 5 4 package private 6 5 7 6 import ( ··· 13 12 "code.gitea.io/gitea/modules/git" 14 13 "code.gitea.io/gitea/modules/private" 15 14 ) 16 - 17 - // ________ _____ .__ __ 18 - // \______ \ _____/ ____\____ __ __| |_/ |_ 19 - // | | \_/ __ \ __\\__ \ | | \ |\ __\ 20 - // | ` \ ___/| | / __ \| | / |_| | 21 - // /_______ /\___ >__| (____ /____/|____/__| 22 - // \/ \/ \/ 23 - // __________ .__ 24 - // \______ \____________ ____ ____ | |__ 25 - // | | _/\_ __ \__ \ / \_/ ___\| | \ 26 - // | | \ | | \// __ \| | \ \___| Y \ 27 - // |______ / |__| (____ /___| /\___ >___| / 28 - // \/ \/ \/ \/ \/ 29 15 30 16 // SetDefaultBranch updates the default branch 31 17 func SetDefaultBranch(ctx *gitea_context.PrivateContext) {
-1
routers/private/hook_post_receive.go
··· 1 1 // Copyright 2021 The Gitea Authors. All rights reserved. 2 2 // SPDX-License-Identifier: MIT 3 3 4 - // Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. 5 4 package private 6 5 7 6 import (
+19 -20
routers/private/hook_pre_receive.go
··· 1 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 2 // SPDX-License-Identifier: MIT 3 3 4 - // Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. 5 4 package private 6 5 7 6 import ( ··· 69 68 if ctx.Written() { 70 69 return false 71 70 } 72 - ctx.JSON(http.StatusForbidden, map[string]interface{}{ 73 - "err": "User permission denied for writing.", 71 + ctx.JSON(http.StatusForbidden, private.Response{ 72 + UserMsg: "User permission denied for writing.", 74 73 }) 75 74 return false 76 75 } ··· 95 94 if ctx.Written() { 96 95 return false 97 96 } 98 - ctx.JSON(http.StatusForbidden, map[string]interface{}{ 99 - "err": "User permission denied for creating pull-request.", 97 + ctx.JSON(http.StatusForbidden, private.Response{ 98 + UserMsg: "User permission denied for creating pull-request.", 100 99 }) 101 100 return false 102 101 } ··· 151 150 if branchName == repo.DefaultBranch && newCommitID == git.EmptySHA { 152 151 log.Warn("Forbidden: Branch: %s is the default branch in %-v and cannot be deleted", branchName, repo) 153 152 ctx.JSON(http.StatusForbidden, private.Response{ 154 - Err: fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName), 153 + UserMsg: fmt.Sprintf("branch %s is the default branch and cannot be deleted", branchName), 155 154 }) 156 155 return 157 156 } ··· 179 178 if newCommitID == git.EmptySHA { 180 179 log.Warn("Forbidden: Branch: %s in %-v is protected from deletion", branchName, repo) 181 180 ctx.JSON(http.StatusForbidden, private.Response{ 182 - Err: fmt.Sprintf("branch %s is protected from deletion", branchName), 181 + UserMsg: fmt.Sprintf("branch %s is protected from deletion", branchName), 183 182 }) 184 183 return 185 184 } ··· 196 195 } else if len(output) > 0 { 197 196 log.Warn("Forbidden: Branch: %s in %-v is protected from force push", branchName, repo) 198 197 ctx.JSON(http.StatusForbidden, private.Response{ 199 - Err: fmt.Sprintf("branch %s is protected from force push", branchName), 198 + UserMsg: fmt.Sprintf("branch %s is protected from force push", branchName), 200 199 }) 201 200 return 202 201 ··· 217 216 unverifiedCommit := err.(*errUnverifiedCommit).sha 218 217 log.Warn("Forbidden: Branch: %s in %-v is protected from unverified commit %s", branchName, repo, unverifiedCommit) 219 218 ctx.JSON(http.StatusForbidden, private.Response{ 220 - Err: fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit), 219 + UserMsg: fmt.Sprintf("branch %s is protected from unverified commit %s", branchName, unverifiedCommit), 221 220 }) 222 221 return 223 222 } ··· 272 271 if changedProtectedfiles { 273 272 log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) 274 273 ctx.JSON(http.StatusForbidden, private.Response{ 275 - Err: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), 274 + UserMsg: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), 276 275 }) 277 276 return 278 277 } ··· 297 296 // Or we're simply not able to push to this protected branch 298 297 log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v", ctx.opts.UserID, branchName, repo) 299 298 ctx.JSON(http.StatusForbidden, private.Response{ 300 - Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName), 299 + UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s", branchName), 301 300 }) 302 301 return 303 302 } ··· 333 332 if !allowedMerge { 334 333 log.Warn("Forbidden: User %d is not allowed to push to protected branch: %s in %-v and is not allowed to merge pr #%d", ctx.opts.UserID, branchName, repo, pr.Index) 335 334 ctx.JSON(http.StatusForbidden, private.Response{ 336 - Err: fmt.Sprintf("Not allowed to push to protected branch %s", branchName), 335 + UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s", branchName), 337 336 }) 338 337 return 339 338 } ··· 347 346 if changedProtectedfiles { 348 347 log.Warn("Forbidden: Branch: %s in %-v is protected from changing file %s", branchName, repo, protectedFilePath) 349 348 ctx.JSON(http.StatusForbidden, private.Response{ 350 - Err: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), 349 + UserMsg: fmt.Sprintf("branch %s is protected from changing file %s", branchName, protectedFilePath), 351 350 }) 352 351 return 353 352 } ··· 357 356 if models.IsErrDisallowedToMerge(err) { 358 357 log.Warn("Forbidden: User %d is not allowed push to protected branch %s in %-v and pr #%d is not ready to be merged: %s", ctx.opts.UserID, branchName, repo, pr.Index, err.Error()) 359 358 ctx.JSON(http.StatusForbidden, private.Response{ 360 - Err: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, ctx.opts.PullRequestID, err.Error()), 359 + UserMsg: fmt.Sprintf("Not allowed to push to protected branch %s and pr #%d is not ready to be merged: %s", branchName, ctx.opts.PullRequestID, err.Error()), 361 360 }) 362 361 return 363 362 } ··· 400 399 if !isAllowed { 401 400 log.Warn("Forbidden: Tag %s in %-v is protected", tagName, ctx.Repo.Repository) 402 401 ctx.JSON(http.StatusForbidden, private.Response{ 403 - Err: fmt.Sprintf("Tag %s is protected", tagName), 402 + UserMsg: fmt.Sprintf("Tag %s is protected", tagName), 404 403 }) 405 404 return 406 405 } ··· 412 411 } 413 412 414 413 if ctx.Repo.Repository.IsEmpty { 415 - ctx.JSON(http.StatusForbidden, map[string]interface{}{ 416 - "err": "Can't create pull request for an empty repository.", 414 + ctx.JSON(http.StatusForbidden, private.Response{ 415 + UserMsg: "Can't create pull request for an empty repository.", 417 416 }) 418 417 return 419 418 } 420 419 421 420 if ctx.opts.IsWiki { 422 - ctx.JSON(http.StatusForbidden, map[string]interface{}{ 423 - "err": "Pull requests are not supported on the wiki.", 421 + ctx.JSON(http.StatusForbidden, private.Response{ 422 + UserMsg: "Pull requests are not supported on the wiki.", 424 423 }) 425 424 return 426 425 } ··· 443 442 444 443 if !baseBranchExist { 445 444 ctx.JSON(http.StatusForbidden, private.Response{ 446 - Err: fmt.Sprintf("Unexpected ref: %s", refFullName), 445 + UserMsg: fmt.Sprintf("Unexpected ref: %s", refFullName), 447 446 }) 448 447 return 449 448 }
+2 -3
routers/private/hook_proc_receive.go
··· 1 1 // Copyright 2021 The Gitea Authors. All rights reserved. 2 2 // SPDX-License-Identifier: MIT 3 3 4 - // Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. 5 4 package private 6 5 7 6 import ( ··· 30 29 ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error()) 31 30 } else { 32 31 log.Error(err.Error()) 33 - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ 34 - "Err": err.Error(), 32 + ctx.JSON(http.StatusInternalServerError, private.Response{ 33 + Err: err.Error(), 35 34 }) 36 35 } 37 36
-14
routers/private/hook_verification.go
··· 1 1 // Copyright 2021 The Gitea Authors. All rights reserved. 2 2 // SPDX-License-Identifier: MIT 3 3 4 - // Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. 5 4 package private 6 5 7 6 import ( ··· 16 15 "code.gitea.io/gitea/modules/log" 17 16 ) 18 17 19 - // _________ .__ __ 20 - // \_ ___ \ ____ _____ _____ |__|/ |_ 21 - // / \ \/ / _ \ / \ / \| \ __\ 22 - // \ \___( <_> ) Y Y \ Y Y \ || | 23 - // \______ /\____/|__|_| /__|_| /__||__| 24 - // \/ \/ \/ 25 - // ____ ____ .__ _____.__ __ .__ 26 - // \ \ / /___________|__|/ ____\__| ____ _____ _/ |_|__| ____ ____ 27 - // \ Y // __ \_ __ \ \ __\| |/ ___\\__ \\ __\ |/ _ \ / \ 28 - // \ /\ ___/| | \/ || | | \ \___ / __ \| | | ( <_> ) | \ 29 - // \___/ \___ >__| |__||__| |__|\___ >____ /__| |__|\____/|___| / 30 - // \/ \/ \/ \/ 31 - // 32 18 // This file contains commit verification functions for refs passed across in hooks 33 19 34 20 func verifyCommits(oldCommitID, newCommitID string, repo *git.Repository, env []string) error {
+1 -1
routers/private/internal.go
··· 1 1 // Copyright 2017 The Gitea Authors. All rights reserved. 2 2 // SPDX-License-Identifier: MIT 3 3 4 - // Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. 4 + // Package private contains all internal routes. The package name "internal" isn't usable because Golang reserves it for disabling cross-package usage. 5 5 package private 6 6 7 7 import (
+6 -20
routers/private/internal_repo.go
··· 1 1 // Copyright 2021 The Gitea Authors. All rights reserved. 2 2 // SPDX-License-Identifier: MIT 3 3 4 - // Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. 5 4 package private 6 5 7 6 import ( ··· 13 12 gitea_context "code.gitea.io/gitea/modules/context" 14 13 "code.gitea.io/gitea/modules/git" 15 14 "code.gitea.io/gitea/modules/log" 15 + "code.gitea.io/gitea/modules/private" 16 16 ) 17 17 18 - // __________ 19 - // \______ \ ____ ______ ____ 20 - // | _// __ \\____ \ / _ \ 21 - // | | \ ___/| |_> > <_> ) 22 - // |____|_ /\___ > __/ \____/ 23 - // \/ \/|__| 24 - // _____ .__ __ 25 - // / _ \ ______ _____|__| ____ ____ _____ ____ _____/ |_ 26 - // / /_\ \ / ___// ___/ |/ ___\ / \ / \_/ __ \ / \ __\ 27 - // / | \\___ \ \___ \| / /_/ > | \ Y Y \ ___/| | \ | 28 - // \____|__ /____ >____ >__\___ /|___| /__|_| /\___ >___| /__| 29 - // \/ \/ \/ /_____/ \/ \/ \/ \/ 30 - 31 - // This file contains common functions relating to setting the Repository for the 32 - // internal routes 18 + // This file contains common functions relating to setting the Repository for the internal routes 33 19 34 20 // RepoAssignment assigns the repository and gitrepository to the private context 35 21 func RepoAssignment(ctx *gitea_context.PrivateContext) context.CancelFunc { ··· 45 31 gitRepo, err := git.OpenRepository(ctx, repo.RepoPath()) 46 32 if err != nil { 47 33 log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err) 48 - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ 49 - "Err": fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err), 34 + ctx.JSON(http.StatusInternalServerError, private.Response{ 35 + Err: fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err), 50 36 }) 51 37 return nil 52 38 } ··· 71 57 repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, ownerName, repoName) 72 58 if err != nil { 73 59 log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err) 74 - ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ 75 - "Err": fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err), 60 + ctx.JSON(http.StatusInternalServerError, private.Response{ 61 + Err: fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err), 76 62 }) 77 63 return nil 78 64 }
-1
routers/private/key.go
··· 1 1 // Copyright 2018 The Gitea Authors. All rights reserved. 2 2 // SPDX-License-Identifier: MIT 3 3 4 - // Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. 5 4 package private 6 5 7 6 import (
+2 -2
routers/private/manager.go
··· 31 31 } 32 32 }() 33 33 ctx.JSON(http.StatusAccepted, private.Response{ 34 - Err: "Flushing", 34 + UserMsg: "Flushing", 35 35 }) 36 36 return 37 37 } 38 38 err := queue.GetManager().FlushAll(ctx, opts.Timeout) 39 39 if err != nil { 40 40 ctx.JSON(http.StatusRequestTimeout, private.Response{ 41 - Err: fmt.Sprintf("%v", err), 41 + UserMsg: fmt.Sprintf("%v", err), 42 42 }) 43 43 } 44 44 ctx.PlainText(http.StatusOK, "success")
+1 -1
routers/private/manager_windows.go
··· 16 16 // Restart is not implemented for Windows based servers as they can't fork 17 17 func Restart(ctx *context.PrivateContext) { 18 18 ctx.JSON(http.StatusNotImplemented, private.Response{ 19 - Err: "windows servers cannot be gracefully restarted - shutdown and restart manually", 19 + UserMsg: "windows servers cannot be gracefully restarted - shutdown and restart manually", 20 20 }) 21 21 } 22 22
+57 -84
routers/private/serv.go
··· 1 1 // Copyright 2019 The Gitea Authors. All rights reserved. 2 2 // SPDX-License-Identifier: MIT 3 3 4 - // Package private includes all internal routes. The package name internal is ideal but Golang is not allowed, so we use private as package name instead. 5 4 package private 6 5 7 6 import ( ··· 29 28 keyID := ctx.ParamsInt64(":keyid") 30 29 if keyID <= 0 { 31 30 ctx.JSON(http.StatusBadRequest, private.Response{ 32 - Err: fmt.Sprintf("Bad key id: %d", keyID), 31 + UserMsg: fmt.Sprintf("Bad key id: %d", keyID), 33 32 }) 34 33 } 35 34 results := private.KeyAndOwner{} ··· 38 37 if err != nil { 39 38 if asymkey_model.IsErrKeyNotExist(err) { 40 39 ctx.JSON(http.StatusUnauthorized, private.Response{ 41 - Err: fmt.Sprintf("Cannot find key: %d", keyID), 40 + UserMsg: fmt.Sprintf("Cannot find key: %d", keyID), 42 41 }) 43 42 return 44 43 } ··· 55 54 if err != nil { 56 55 if user_model.IsErrUserNotExist(err) { 57 56 ctx.JSON(http.StatusUnauthorized, private.Response{ 58 - Err: fmt.Sprintf("Cannot find owner with id: %d for key: %d", key.OwnerID, keyID), 57 + UserMsg: fmt.Sprintf("Cannot find owner with id: %d for key: %d", key.OwnerID, keyID), 59 58 }) 60 59 return 61 60 } ··· 67 66 } 68 67 if !user.IsActive || user.ProhibitLogin { 69 68 ctx.JSON(http.StatusForbidden, private.Response{ 70 - Err: "Your account is disabled.", 69 + UserMsg: "Your account is disabled.", 71 70 }) 72 71 return 73 72 } ··· 113 112 if user_model.IsErrUserNotExist(err) { 114 113 // User is fetching/cloning a non-existent repository 115 114 log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr()) 116 - ctx.JSON(http.StatusNotFound, private.ErrServCommand{ 117 - Results: results, 118 - Err: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), 115 + ctx.JSON(http.StatusNotFound, private.Response{ 116 + UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), 119 117 }) 120 118 return 121 119 } 122 120 log.Error("Unable to get repository owner: %s/%s Error: %v", results.OwnerName, results.RepoName, err) 123 - ctx.JSON(http.StatusForbidden, private.ErrServCommand{ 124 - Results: results, 125 - Err: fmt.Sprintf("Unable to get repository owner: %s/%s %v", results.OwnerName, results.RepoName, err), 121 + ctx.JSON(http.StatusForbidden, private.Response{ 122 + UserMsg: fmt.Sprintf("Unable to get repository owner: %s/%s %v", results.OwnerName, results.RepoName, err), 126 123 }) 127 124 return 128 125 } 129 126 if !owner.IsOrganization() && !owner.IsActive { 130 - ctx.JSON(http.StatusForbidden, private.ErrServCommand{ 131 - Results: results, 132 - Err: "Repository cannot be accessed, you could retry it later", 127 + ctx.JSON(http.StatusForbidden, private.Response{ 128 + UserMsg: "Repository cannot be accessed, you could retry it later", 133 129 }) 134 130 return 135 131 } ··· 144 140 if verb == "git-upload-pack" { 145 141 // User is fetching/cloning a non-existent repository 146 142 log.Warn("Failed authentication attempt (cannot find repository: %s/%s) from %s", results.OwnerName, results.RepoName, ctx.RemoteAddr()) 147 - ctx.JSON(http.StatusNotFound, private.ErrServCommand{ 148 - Results: results, 149 - Err: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), 143 + ctx.JSON(http.StatusNotFound, private.Response{ 144 + UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), 150 145 }) 151 146 return 152 147 } 153 148 } 154 149 } else { 155 150 log.Error("Unable to get repository: %s/%s Error: %v", results.OwnerName, results.RepoName, err) 156 - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ 157 - Results: results, 158 - Err: fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err), 151 + ctx.JSON(http.StatusInternalServerError, private.Response{ 152 + Err: fmt.Sprintf("Unable to get repository: %s/%s %v", results.OwnerName, results.RepoName, err), 159 153 }) 160 154 return 161 155 } ··· 167 161 results.RepoID = repo.ID 168 162 169 163 if repo.IsBeingCreated() { 170 - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ 171 - Results: results, 172 - Err: "Repository is being created, you could retry after it finished", 164 + ctx.JSON(http.StatusInternalServerError, private.Response{ 165 + Err: "Repository is being created, you could retry after it finished", 173 166 }) 174 167 return 175 168 } 176 169 177 170 if repo.IsBroken() { 178 - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ 179 - Results: results, 180 - Err: "Repository is in a broken state", 171 + ctx.JSON(http.StatusInternalServerError, private.Response{ 172 + Err: "Repository is in a broken state", 181 173 }) 182 174 return 183 175 } 184 176 185 177 // We can shortcut at this point if the repo is a mirror 186 178 if mode > perm.AccessModeRead && repo.IsMirror { 187 - ctx.JSON(http.StatusForbidden, private.ErrServCommand{ 188 - Results: results, 189 - Err: fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName), 179 + ctx.JSON(http.StatusForbidden, private.Response{ 180 + UserMsg: fmt.Sprintf("Mirror Repository %s/%s is read-only", results.OwnerName, results.RepoName), 190 181 }) 191 182 return 192 183 } ··· 196 187 key, err := asymkey_model.GetPublicKeyByID(keyID) 197 188 if err != nil { 198 189 if asymkey_model.IsErrKeyNotExist(err) { 199 - ctx.JSON(http.StatusNotFound, private.ErrServCommand{ 200 - Results: results, 201 - Err: fmt.Sprintf("Cannot find key: %d", keyID), 190 + ctx.JSON(http.StatusNotFound, private.Response{ 191 + UserMsg: fmt.Sprintf("Cannot find key: %d", keyID), 202 192 }) 203 193 return 204 194 } 205 195 log.Error("Unable to get public key: %d Error: %v", keyID, err) 206 - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ 207 - Results: results, 208 - Err: fmt.Sprintf("Unable to get key: %d Error: %v", keyID, err), 196 + ctx.JSON(http.StatusInternalServerError, private.Response{ 197 + Err: fmt.Sprintf("Unable to get key: %d Error: %v", keyID, err), 209 198 }) 210 199 return 211 200 } ··· 215 204 216 205 // If repo doesn't exist, deploy key doesn't make sense 217 206 if !repoExist && key.Type == asymkey_model.KeyTypeDeploy { 218 - ctx.JSON(http.StatusNotFound, private.ErrServCommand{ 219 - Results: results, 220 - Err: fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName), 207 + ctx.JSON(http.StatusNotFound, private.Response{ 208 + UserMsg: fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName), 221 209 }) 222 210 return 223 211 } ··· 232 220 deployKey, err = asymkey_model.GetDeployKeyByRepo(ctx, key.ID, repo.ID) 233 221 if err != nil { 234 222 if asymkey_model.IsErrDeployKeyNotExist(err) { 235 - ctx.JSON(http.StatusNotFound, private.ErrServCommand{ 236 - Results: results, 237 - Err: fmt.Sprintf("Public (Deploy) Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), 223 + ctx.JSON(http.StatusNotFound, private.Response{ 224 + UserMsg: fmt.Sprintf("Public (Deploy) Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), 238 225 }) 239 226 return 240 227 } 241 228 log.Error("Unable to get deploy for public (deploy) key: %d in %-v Error: %v", key.ID, repo, err) 242 - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ 243 - Results: results, 244 - Err: fmt.Sprintf("Unable to get Deploy Key for Public Key: %d:%s in %s/%s.", key.ID, key.Name, results.OwnerName, results.RepoName), 229 + ctx.JSON(http.StatusInternalServerError, private.Response{ 230 + Err: fmt.Sprintf("Unable to get Deploy Key for Public Key: %d:%s in %s/%s.", key.ID, key.Name, results.OwnerName, results.RepoName), 245 231 }) 246 232 return 247 233 } ··· 262 248 user, err = user_model.GetUserByID(ctx, key.OwnerID) 263 249 if err != nil { 264 250 if user_model.IsErrUserNotExist(err) { 265 - ctx.JSON(http.StatusUnauthorized, private.ErrServCommand{ 266 - Results: results, 267 - Err: fmt.Sprintf("Public Key: %d:%s owner %d does not exist.", key.ID, key.Name, key.OwnerID), 251 + ctx.JSON(http.StatusUnauthorized, private.Response{ 252 + UserMsg: fmt.Sprintf("Public Key: %d:%s owner %d does not exist.", key.ID, key.Name, key.OwnerID), 268 253 }) 269 254 return 270 255 } 271 256 log.Error("Unable to get owner: %d for public key: %d:%s Error: %v", key.OwnerID, key.ID, key.Name, err) 272 - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ 273 - Results: results, 274 - Err: fmt.Sprintf("Unable to get Owner: %d for Deploy Key: %d:%s in %s/%s.", key.OwnerID, key.ID, key.Name, ownerName, repoName), 257 + ctx.JSON(http.StatusInternalServerError, private.Response{ 258 + Err: fmt.Sprintf("Unable to get Owner: %d for Deploy Key: %d:%s in %s/%s.", key.OwnerID, key.ID, key.Name, ownerName, repoName), 275 259 }) 276 260 return 277 261 } 278 262 279 263 if !user.IsActive || user.ProhibitLogin { 280 264 ctx.JSON(http.StatusForbidden, private.Response{ 281 - Err: "Your account is disabled.", 265 + UserMsg: "Your account is disabled.", 282 266 }) 283 267 return 284 268 } ··· 291 275 292 276 // Don't allow pushing if the repo is archived 293 277 if repoExist && mode > perm.AccessModeRead && repo.IsArchived { 294 - ctx.JSON(http.StatusUnauthorized, private.ErrServCommand{ 295 - Results: results, 296 - Err: fmt.Sprintf("Repo: %s/%s is archived.", results.OwnerName, results.RepoName), 278 + ctx.JSON(http.StatusUnauthorized, private.Response{ 279 + UserMsg: fmt.Sprintf("Repo: %s/%s is archived.", results.OwnerName, results.RepoName), 297 280 }) 298 281 return 299 282 } ··· 307 290 setting.Service.RequireSignInView) { 308 291 if key.Type == asymkey_model.KeyTypeDeploy { 309 292 if deployKey.Mode < mode { 310 - ctx.JSON(http.StatusUnauthorized, private.ErrServCommand{ 311 - Results: results, 312 - Err: fmt.Sprintf("Deploy Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), 293 + ctx.JSON(http.StatusUnauthorized, private.Response{ 294 + UserMsg: fmt.Sprintf("Deploy Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName), 313 295 }) 314 296 return 315 297 } ··· 322 304 perm, err := access_model.GetUserRepoPermission(ctx, repo, user) 323 305 if err != nil { 324 306 log.Error("Unable to get permissions for %-v with key %d in %-v Error: %v", user, key.ID, repo, err) 325 - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ 326 - Results: results, 327 - Err: fmt.Sprintf("Unable to get permissions for user %d:%s with key %d in %s/%s Error: %v", user.ID, user.Name, key.ID, results.OwnerName, results.RepoName, err), 307 + ctx.JSON(http.StatusInternalServerError, private.Response{ 308 + Err: fmt.Sprintf("Unable to get permissions for user %d:%s with key %d in %s/%s Error: %v", user.ID, user.Name, key.ID, results.OwnerName, results.RepoName, err), 328 309 }) 329 310 return 330 311 } ··· 333 314 334 315 if userMode < mode { 335 316 log.Warn("Failed authentication attempt for %s with key %s (not authorized to %s %s/%s) from %s", user.Name, key.Name, modeString, ownerName, repoName, ctx.RemoteAddr()) 336 - ctx.JSON(http.StatusUnauthorized, private.ErrServCommand{ 337 - Results: results, 338 - Err: fmt.Sprintf("User: %d:%s with Key: %d:%s is not authorized to %s %s/%s.", user.ID, user.Name, key.ID, key.Name, modeString, ownerName, repoName), 317 + ctx.JSON(http.StatusUnauthorized, private.Response{ 318 + UserMsg: fmt.Sprintf("User: %d:%s with Key: %d:%s is not authorized to %s %s/%s.", user.ID, user.Name, key.ID, key.Name, modeString, ownerName, repoName), 339 319 }) 340 320 return 341 321 } ··· 346 326 if !repoExist { 347 327 owner, err := user_model.GetUserByName(ctx, ownerName) 348 328 if err != nil { 349 - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ 350 - Results: results, 351 - Err: fmt.Sprintf("Unable to get owner: %s %v", results.OwnerName, err), 329 + ctx.JSON(http.StatusInternalServerError, private.Response{ 330 + Err: fmt.Sprintf("Unable to get owner: %s %v", results.OwnerName, err), 352 331 }) 353 332 return 354 333 } 355 334 356 335 if owner.IsOrganization() && !setting.Repository.EnablePushCreateOrg { 357 - ctx.JSON(http.StatusForbidden, private.ErrServCommand{ 358 - Results: results, 359 - Err: "Push to create is not enabled for organizations.", 336 + ctx.JSON(http.StatusForbidden, private.Response{ 337 + UserMsg: "Push to create is not enabled for organizations.", 360 338 }) 361 339 return 362 340 } 363 341 if !owner.IsOrganization() && !setting.Repository.EnablePushCreateUser { 364 - ctx.JSON(http.StatusForbidden, private.ErrServCommand{ 365 - Results: results, 366 - Err: "Push to create is not enabled for users.", 342 + ctx.JSON(http.StatusForbidden, private.Response{ 343 + UserMsg: "Push to create is not enabled for users.", 367 344 }) 368 345 return 369 346 } ··· 371 348 repo, err = repo_service.PushCreateRepo(ctx, user, owner, results.RepoName) 372 349 if err != nil { 373 350 log.Error("pushCreateRepo: %v", err) 374 - ctx.JSON(http.StatusNotFound, private.ErrServCommand{ 375 - Results: results, 376 - Err: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), 351 + ctx.JSON(http.StatusNotFound, private.Response{ 352 + UserMsg: fmt.Sprintf("Cannot find repository: %s/%s", results.OwnerName, results.RepoName), 377 353 }) 378 354 return 379 355 } ··· 384 360 // Ensure the wiki is enabled before we allow access to it 385 361 if _, err := repo.GetUnit(ctx, unit.TypeWiki); err != nil { 386 362 if repo_model.IsErrUnitTypeNotExist(err) { 387 - ctx.JSON(http.StatusForbidden, private.ErrServCommand{ 388 - Results: results, 389 - Err: "repository wiki is disabled", 363 + ctx.JSON(http.StatusForbidden, private.Response{ 364 + UserMsg: "repository wiki is disabled", 390 365 }) 391 366 return 392 367 } 393 368 log.Error("Failed to get the wiki unit in %-v Error: %v", repo, err) 394 - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ 395 - Results: results, 396 - Err: fmt.Sprintf("Failed to get the wiki unit in %s/%s Error: %v", ownerName, repoName, err), 369 + ctx.JSON(http.StatusInternalServerError, private.Response{ 370 + Err: fmt.Sprintf("Failed to get the wiki unit in %s/%s Error: %v", ownerName, repoName, err), 397 371 }) 398 372 return 399 373 } ··· 401 375 // Finally if we're trying to touch the wiki we should init it 402 376 if err = wiki_service.InitWiki(ctx, repo); err != nil { 403 377 log.Error("Failed to initialize the wiki in %-v Error: %v", repo, err) 404 - ctx.JSON(http.StatusInternalServerError, private.ErrServCommand{ 405 - Results: results, 406 - Err: fmt.Sprintf("Failed to initialize the wiki in %s/%s Error: %v", ownerName, repoName, err), 378 + ctx.JSON(http.StatusInternalServerError, private.Response{ 379 + Err: fmt.Sprintf("Failed to initialize the wiki in %s/%s Error: %v", ownerName, repoName, err), 407 380 }) 408 381 return 409 382 }
+24 -24
tests/integration/api_private_serv_test.go
··· 43 43 defer cancel() 44 44 45 45 // Can push to a repo we own 46 - results, err := private.ServCommand(ctx, 1, "user2", "repo1", perm.AccessModeWrite, "git-upload-pack", "") 47 - assert.NoError(t, err) 46 + results, extra := private.ServCommand(ctx, 1, "user2", "repo1", perm.AccessModeWrite, "git-upload-pack", "") 47 + assert.NoError(t, extra.Error) 48 48 assert.False(t, results.IsWiki) 49 49 assert.Zero(t, results.DeployKeyID) 50 50 assert.Equal(t, int64(1), results.KeyID) ··· 56 56 assert.Equal(t, int64(1), results.RepoID) 57 57 58 58 // Cannot push to a private repo we're not associated with 59 - results, err = private.ServCommand(ctx, 1, "user15", "big_test_private_1", perm.AccessModeWrite, "git-upload-pack", "") 60 - assert.Error(t, err) 59 + results, extra = private.ServCommand(ctx, 1, "user15", "big_test_private_1", perm.AccessModeWrite, "git-upload-pack", "") 60 + assert.Error(t, extra.Error) 61 61 assert.Empty(t, results) 62 62 63 63 // Cannot pull from a private repo we're not associated with 64 - results, err = private.ServCommand(ctx, 1, "user15", "big_test_private_1", perm.AccessModeRead, "git-upload-pack", "") 65 - assert.Error(t, err) 64 + results, extra = private.ServCommand(ctx, 1, "user15", "big_test_private_1", perm.AccessModeRead, "git-upload-pack", "") 65 + assert.Error(t, extra.Error) 66 66 assert.Empty(t, results) 67 67 68 68 // Can pull from a public repo we're not associated with 69 - results, err = private.ServCommand(ctx, 1, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "") 70 - assert.NoError(t, err) 69 + results, extra = private.ServCommand(ctx, 1, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "") 70 + assert.NoError(t, extra.Error) 71 71 assert.False(t, results.IsWiki) 72 72 assert.Zero(t, results.DeployKeyID) 73 73 assert.Equal(t, int64(1), results.KeyID) ··· 79 79 assert.Equal(t, int64(17), results.RepoID) 80 80 81 81 // Cannot push to a public repo we're not associated with 82 - results, err = private.ServCommand(ctx, 1, "user15", "big_test_public_1", perm.AccessModeWrite, "git-upload-pack", "") 83 - assert.Error(t, err) 82 + results, extra = private.ServCommand(ctx, 1, "user15", "big_test_public_1", perm.AccessModeWrite, "git-upload-pack", "") 83 + assert.Error(t, extra.Error) 84 84 assert.Empty(t, results) 85 85 86 86 // Add reading deploy key ··· 88 88 assert.NoError(t, err) 89 89 90 90 // Can pull from repo we're a deploy key for 91 - results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_1", perm.AccessModeRead, "git-upload-pack", "") 92 - assert.NoError(t, err) 91 + results, extra = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_1", perm.AccessModeRead, "git-upload-pack", "") 92 + assert.NoError(t, extra.Error) 93 93 assert.False(t, results.IsWiki) 94 94 assert.NotZero(t, results.DeployKeyID) 95 95 assert.Equal(t, deployKey.KeyID, results.KeyID) ··· 101 101 assert.Equal(t, int64(19), results.RepoID) 102 102 103 103 // Cannot push to a private repo with reading key 104 - results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_1", perm.AccessModeWrite, "git-upload-pack", "") 105 - assert.Error(t, err) 104 + results, extra = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_1", perm.AccessModeWrite, "git-upload-pack", "") 105 + assert.Error(t, extra.Error) 106 106 assert.Empty(t, results) 107 107 108 108 // Cannot pull from a private repo we're not associated with 109 - results, err = private.ServCommand(ctx, deployKey.ID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "") 110 - assert.Error(t, err) 109 + results, extra = private.ServCommand(ctx, deployKey.ID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "") 110 + assert.Error(t, extra.Error) 111 111 assert.Empty(t, results) 112 112 113 113 // Cannot pull from a public repo we're not associated with 114 - results, err = private.ServCommand(ctx, deployKey.ID, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "") 115 - assert.Error(t, err) 114 + results, extra = private.ServCommand(ctx, deployKey.ID, "user15", "big_test_public_1", perm.AccessModeRead, "git-upload-pack", "") 115 + assert.Error(t, extra.Error) 116 116 assert.Empty(t, results) 117 117 118 118 // Add writing deploy key ··· 120 120 assert.NoError(t, err) 121 121 122 122 // Cannot push to a private repo with reading key 123 - results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_1", perm.AccessModeWrite, "git-upload-pack", "") 124 - assert.Error(t, err) 123 + results, extra = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_1", perm.AccessModeWrite, "git-upload-pack", "") 124 + assert.Error(t, extra.Error) 125 125 assert.Empty(t, results) 126 126 127 127 // Can pull from repo we're a writing deploy key for 128 - results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "") 129 - assert.NoError(t, err) 128 + results, extra = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_2", perm.AccessModeRead, "git-upload-pack", "") 129 + assert.NoError(t, extra.Error) 130 130 assert.False(t, results.IsWiki) 131 131 assert.NotZero(t, results.DeployKeyID) 132 132 assert.Equal(t, deployKey.KeyID, results.KeyID) ··· 138 138 assert.Equal(t, int64(20), results.RepoID) 139 139 140 140 // Can push to repo we're a writing deploy key for 141 - results, err = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_2", perm.AccessModeWrite, "git-upload-pack", "") 142 - assert.NoError(t, err) 141 + results, extra = private.ServCommand(ctx, deployKey.KeyID, "user15", "big_test_private_2", perm.AccessModeWrite, "git-upload-pack", "") 142 + assert.NoError(t, extra.Error) 143 143 assert.False(t, results.IsWiki) 144 144 assert.NotZero(t, results.DeployKeyID) 145 145 assert.Equal(t, deployKey.KeyID, results.KeyID)