code.gitea.io/gitea@v1.22.3/cmd/hook.go (about) 1 // Copyright 2017 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package cmd 5 6 import ( 7 "bufio" 8 "bytes" 9 "context" 10 "fmt" 11 "io" 12 "os" 13 "strconv" 14 "strings" 15 "time" 16 17 "code.gitea.io/gitea/modules/git" 18 "code.gitea.io/gitea/modules/log" 19 "code.gitea.io/gitea/modules/private" 20 repo_module "code.gitea.io/gitea/modules/repository" 21 "code.gitea.io/gitea/modules/setting" 22 23 "github.com/urfave/cli/v2" 24 ) 25 26 const ( 27 hookBatchSize = 30 28 ) 29 30 var ( 31 // CmdHook represents the available hooks sub-command. 32 CmdHook = &cli.Command{ 33 Name: "hook", 34 Usage: "(internal) Should only be called by Git", 35 Description: "Delegate commands to corresponding Git hooks", 36 Before: PrepareConsoleLoggerLevel(log.FATAL), 37 Subcommands: []*cli.Command{ 38 subcmdHookPreReceive, 39 subcmdHookUpdate, 40 subcmdHookPostReceive, 41 subcmdHookProcReceive, 42 }, 43 } 44 45 subcmdHookPreReceive = &cli.Command{ 46 Name: "pre-receive", 47 Usage: "Delegate pre-receive Git hook", 48 Description: "This command should only be called by Git", 49 Action: runHookPreReceive, 50 Flags: []cli.Flag{ 51 &cli.BoolFlag{ 52 Name: "debug", 53 }, 54 }, 55 } 56 subcmdHookUpdate = &cli.Command{ 57 Name: "update", 58 Usage: "Delegate update Git hook", 59 Description: "This command should only be called by Git", 60 Action: runHookUpdate, 61 Flags: []cli.Flag{ 62 &cli.BoolFlag{ 63 Name: "debug", 64 }, 65 }, 66 } 67 subcmdHookPostReceive = &cli.Command{ 68 Name: "post-receive", 69 Usage: "Delegate post-receive Git hook", 70 Description: "This command should only be called by Git", 71 Action: runHookPostReceive, 72 Flags: []cli.Flag{ 73 &cli.BoolFlag{ 74 Name: "debug", 75 }, 76 }, 77 } 78 // Note: new hook since git 2.29 79 subcmdHookProcReceive = &cli.Command{ 80 Name: "proc-receive", 81 Usage: "Delegate proc-receive Git hook", 82 Description: "This command should only be called by Git", 83 Action: runHookProcReceive, 84 Flags: []cli.Flag{ 85 &cli.BoolFlag{ 86 Name: "debug", 87 }, 88 }, 89 } 90 ) 91 92 type delayWriter struct { 93 internal io.Writer 94 buf *bytes.Buffer 95 timer *time.Timer 96 } 97 98 func newDelayWriter(internal io.Writer, delay time.Duration) *delayWriter { 99 timer := time.NewTimer(delay) 100 return &delayWriter{ 101 internal: internal, 102 buf: &bytes.Buffer{}, 103 timer: timer, 104 } 105 } 106 107 func (d *delayWriter) Write(p []byte) (n int, err error) { 108 if d.buf != nil { 109 select { 110 case <-d.timer.C: 111 _, err := d.internal.Write(d.buf.Bytes()) 112 if err != nil { 113 return 0, err 114 } 115 d.buf = nil 116 return d.internal.Write(p) 117 default: 118 return d.buf.Write(p) 119 } 120 } 121 return d.internal.Write(p) 122 } 123 124 func (d *delayWriter) WriteString(s string) (n int, err error) { 125 if d.buf != nil { 126 select { 127 case <-d.timer.C: 128 _, err := d.internal.Write(d.buf.Bytes()) 129 if err != nil { 130 return 0, err 131 } 132 d.buf = nil 133 return d.internal.Write([]byte(s)) 134 default: 135 return d.buf.WriteString(s) 136 } 137 } 138 return d.internal.Write([]byte(s)) 139 } 140 141 func (d *delayWriter) Close() error { 142 if d == nil { 143 return nil 144 } 145 stopped := d.timer.Stop() 146 if stopped || d.buf == nil { 147 return nil 148 } 149 _, err := d.internal.Write(d.buf.Bytes()) 150 d.buf = nil 151 return err 152 } 153 154 type nilWriter struct{} 155 156 func (n *nilWriter) Write(p []byte) (int, error) { 157 return len(p), nil 158 } 159 160 func (n *nilWriter) WriteString(s string) (int, error) { 161 return len(s), nil 162 } 163 164 func runHookPreReceive(c *cli.Context) error { 165 if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal { 166 return nil 167 } 168 ctx, cancel := installSignals() 169 defer cancel() 170 171 setup(ctx, c.Bool("debug")) 172 173 if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { 174 if setting.OnlyAllowPushIfGiteaEnvironmentSet { 175 return fail(ctx, `Rejecting changes as Gitea environment not set. 176 If you are pushing over SSH you must push with a key managed by 177 Gitea or set your environment appropriately.`, "") 178 } 179 return nil 180 } 181 182 // the environment is set by serv command 183 isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki)) 184 username := os.Getenv(repo_module.EnvRepoUsername) 185 reponame := os.Getenv(repo_module.EnvRepoName) 186 userID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64) 187 prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64) 188 deployKeyID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvDeployKeyID), 10, 64) 189 actionPerm, _ := strconv.ParseInt(os.Getenv(repo_module.EnvActionPerm), 10, 64) 190 191 hookOptions := private.HookOptions{ 192 UserID: userID, 193 GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories), 194 GitObjectDirectory: os.Getenv(private.GitObjectDirectory), 195 GitQuarantinePath: os.Getenv(private.GitQuarantinePath), 196 GitPushOptions: pushOptions(), 197 PullRequestID: prID, 198 DeployKeyID: deployKeyID, 199 ActionPerm: int(actionPerm), 200 } 201 202 scanner := bufio.NewScanner(os.Stdin) 203 204 oldCommitIDs := make([]string, hookBatchSize) 205 newCommitIDs := make([]string, hookBatchSize) 206 refFullNames := make([]git.RefName, hookBatchSize) 207 count := 0 208 total := 0 209 lastline := 0 210 211 var out io.Writer 212 out = &nilWriter{} 213 if setting.Git.VerbosePush { 214 if setting.Git.VerbosePushDelay > 0 { 215 dWriter := newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay) 216 defer dWriter.Close() 217 out = dWriter 218 } else { 219 out = os.Stdout 220 } 221 } 222 223 supportProcReceive := git.DefaultFeatures().SupportProcReceive 224 225 for scanner.Scan() { 226 // TODO: support news feeds for wiki 227 if isWiki { 228 continue 229 } 230 231 fields := bytes.Fields(scanner.Bytes()) 232 if len(fields) != 3 { 233 continue 234 } 235 236 oldCommitID := string(fields[0]) 237 newCommitID := string(fields[1]) 238 refFullName := git.RefName(fields[2]) 239 total++ 240 lastline++ 241 242 // If the ref is a branch or tag, check if it's protected 243 // if supportProcReceive all ref should be checked because 244 // permission check was delayed 245 if supportProcReceive || refFullName.IsBranch() || refFullName.IsTag() { 246 oldCommitIDs[count] = oldCommitID 247 newCommitIDs[count] = newCommitID 248 refFullNames[count] = refFullName 249 count++ 250 fmt.Fprintf(out, "*") 251 252 if count >= hookBatchSize { 253 fmt.Fprintf(out, " Checking %d references\n", count) 254 255 hookOptions.OldCommitIDs = oldCommitIDs 256 hookOptions.NewCommitIDs = newCommitIDs 257 hookOptions.RefFullNames = refFullNames 258 extra := private.HookPreReceive(ctx, username, reponame, hookOptions) 259 if extra.HasError() { 260 return fail(ctx, extra.UserMsg, "HookPreReceive(batch) failed: %v", extra.Error) 261 } 262 count = 0 263 lastline = 0 264 } 265 } else { 266 fmt.Fprintf(out, ".") 267 } 268 if lastline >= hookBatchSize { 269 fmt.Fprintf(out, "\n") 270 lastline = 0 271 } 272 } 273 274 if count > 0 { 275 hookOptions.OldCommitIDs = oldCommitIDs[:count] 276 hookOptions.NewCommitIDs = newCommitIDs[:count] 277 hookOptions.RefFullNames = refFullNames[:count] 278 279 fmt.Fprintf(out, " Checking %d references\n", count) 280 281 extra := private.HookPreReceive(ctx, username, reponame, hookOptions) 282 if extra.HasError() { 283 return fail(ctx, extra.UserMsg, "HookPreReceive(last) failed: %v", extra.Error) 284 } 285 } else if lastline > 0 { 286 fmt.Fprintf(out, "\n") 287 } 288 289 fmt.Fprintf(out, "Checked %d references in total\n", total) 290 return nil 291 } 292 293 // runHookUpdate avoid to do heavy operations on update hook because it will be 294 // invoked for every ref update which does not like pre-receive and post-receive 295 func runHookUpdate(c *cli.Context) error { 296 if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal { 297 return nil 298 } 299 300 // Update is empty and is kept only for backwards compatibility 301 if len(os.Args) < 3 { 302 return nil 303 } 304 refName := git.RefName(os.Args[len(os.Args)-3]) 305 if refName.IsPull() { 306 // ignore update to refs/pull/xxx/head, so we don't need to output any information 307 os.Exit(1) 308 } 309 return nil 310 } 311 312 func runHookPostReceive(c *cli.Context) error { 313 ctx, cancel := installSignals() 314 defer cancel() 315 316 setup(ctx, c.Bool("debug")) 317 318 // First of all run update-server-info no matter what 319 if _, _, err := git.NewCommand(ctx, "update-server-info").RunStdString(nil); err != nil { 320 return fmt.Errorf("Failed to call 'git update-server-info': %w", err) 321 } 322 323 // Now if we're an internal don't do anything else 324 if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal { 325 return nil 326 } 327 328 if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { 329 if setting.OnlyAllowPushIfGiteaEnvironmentSet { 330 return fail(ctx, `Rejecting changes as Gitea environment not set. 331 If you are pushing over SSH you must push with a key managed by 332 Gitea or set your environment appropriately.`, "") 333 } 334 return nil 335 } 336 337 var out io.Writer 338 var dWriter *delayWriter 339 out = &nilWriter{} 340 if setting.Git.VerbosePush { 341 if setting.Git.VerbosePushDelay > 0 { 342 dWriter = newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay) 343 defer dWriter.Close() 344 out = dWriter 345 } else { 346 out = os.Stdout 347 } 348 } 349 350 // the environment is set by serv command 351 repoUser := os.Getenv(repo_module.EnvRepoUsername) 352 isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki)) 353 repoName := os.Getenv(repo_module.EnvRepoName) 354 pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64) 355 prID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPRID), 10, 64) 356 pusherName := os.Getenv(repo_module.EnvPusherName) 357 358 hookOptions := private.HookOptions{ 359 UserName: pusherName, 360 UserID: pusherID, 361 GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories), 362 GitObjectDirectory: os.Getenv(private.GitObjectDirectory), 363 GitQuarantinePath: os.Getenv(private.GitQuarantinePath), 364 GitPushOptions: pushOptions(), 365 PullRequestID: prID, 366 PushTrigger: repo_module.PushTrigger(os.Getenv(repo_module.EnvPushTrigger)), 367 } 368 oldCommitIDs := make([]string, hookBatchSize) 369 newCommitIDs := make([]string, hookBatchSize) 370 refFullNames := make([]git.RefName, hookBatchSize) 371 count := 0 372 total := 0 373 wasEmpty := false 374 masterPushed := false 375 results := make([]private.HookPostReceiveBranchResult, 0) 376 377 scanner := bufio.NewScanner(os.Stdin) 378 for scanner.Scan() { 379 // TODO: support news feeds for wiki 380 if isWiki { 381 continue 382 } 383 384 fields := bytes.Fields(scanner.Bytes()) 385 if len(fields) != 3 { 386 continue 387 } 388 389 fmt.Fprintf(out, ".") 390 oldCommitIDs[count] = string(fields[0]) 391 newCommitIDs[count] = string(fields[1]) 392 refFullNames[count] = git.RefName(fields[2]) 393 394 commitID, _ := git.NewIDFromString(newCommitIDs[count]) 395 if refFullNames[count] == git.BranchPrefix+"master" && !commitID.IsZero() && count == total { 396 masterPushed = true 397 } 398 count++ 399 total++ 400 401 if count >= hookBatchSize { 402 fmt.Fprintf(out, " Processing %d references\n", count) 403 hookOptions.OldCommitIDs = oldCommitIDs 404 hookOptions.NewCommitIDs = newCommitIDs 405 hookOptions.RefFullNames = refFullNames 406 resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions) 407 if extra.HasError() { 408 _ = dWriter.Close() 409 hookPrintResults(results) 410 return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error) 411 } 412 wasEmpty = wasEmpty || resp.RepoWasEmpty 413 results = append(results, resp.Results...) 414 count = 0 415 } 416 } 417 418 if count == 0 { 419 if wasEmpty && masterPushed { 420 // We need to tell the repo to reset the default branch to master 421 extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master") 422 if extra.HasError() { 423 return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error) 424 } 425 } 426 fmt.Fprintf(out, "Processed %d references in total\n", total) 427 428 _ = dWriter.Close() 429 hookPrintResults(results) 430 return nil 431 } 432 433 hookOptions.OldCommitIDs = oldCommitIDs[:count] 434 hookOptions.NewCommitIDs = newCommitIDs[:count] 435 hookOptions.RefFullNames = refFullNames[:count] 436 437 fmt.Fprintf(out, " Processing %d references\n", count) 438 439 resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions) 440 if resp == nil { 441 _ = dWriter.Close() 442 hookPrintResults(results) 443 return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error) 444 } 445 wasEmpty = wasEmpty || resp.RepoWasEmpty 446 results = append(results, resp.Results...) 447 448 fmt.Fprintf(out, "Processed %d references in total\n", total) 449 450 if wasEmpty && masterPushed { 451 // We need to tell the repo to reset the default branch to master 452 extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master") 453 if extra.HasError() { 454 return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error) 455 } 456 } 457 _ = dWriter.Close() 458 hookPrintResults(results) 459 460 return nil 461 } 462 463 func hookPrintResults(results []private.HookPostReceiveBranchResult) { 464 for _, res := range results { 465 hookPrintResult(res.Message, res.Create, res.Branch, res.URL) 466 } 467 } 468 469 func hookPrintResult(output, isCreate bool, branch, url string) { 470 if !output { 471 return 472 } 473 fmt.Fprintln(os.Stderr, "") 474 if isCreate { 475 fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", branch) 476 fmt.Fprintf(os.Stderr, " %s\n", url) 477 } else { 478 fmt.Fprint(os.Stderr, "Visit the existing pull request:\n") 479 fmt.Fprintf(os.Stderr, " %s\n", url) 480 } 481 fmt.Fprintln(os.Stderr, "") 482 _ = os.Stderr.Sync() 483 } 484 485 func pushOptions() map[string]string { 486 opts := make(map[string]string) 487 if pushCount, err := strconv.Atoi(os.Getenv(private.GitPushOptionCount)); err == nil { 488 for idx := 0; idx < pushCount; idx++ { 489 opt := os.Getenv(fmt.Sprintf("GIT_PUSH_OPTION_%d", idx)) 490 kv := strings.SplitN(opt, "=", 2) 491 if len(kv) == 2 { 492 opts[kv[0]] = kv[1] 493 } 494 } 495 } 496 return opts 497 } 498 499 func runHookProcReceive(c *cli.Context) error { 500 ctx, cancel := installSignals() 501 defer cancel() 502 503 setup(ctx, c.Bool("debug")) 504 505 if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { 506 if setting.OnlyAllowPushIfGiteaEnvironmentSet { 507 return fail(ctx, `Rejecting changes as Gitea environment not set. 508 If you are pushing over SSH you must push with a key managed by 509 Gitea or set your environment appropriately.`, "") 510 } 511 return nil 512 } 513 514 if !git.DefaultFeatures().SupportProcReceive { 515 return fail(ctx, "No proc-receive support", "current git version doesn't support proc-receive.") 516 } 517 518 reader := bufio.NewReader(os.Stdin) 519 repoUser := os.Getenv(repo_module.EnvRepoUsername) 520 repoName := os.Getenv(repo_module.EnvRepoName) 521 pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64) 522 pusherName := os.Getenv(repo_module.EnvPusherName) 523 524 // 1. Version and features negotiation. 525 // S: PKT-LINE(version=1\0push-options atomic...) / PKT-LINE(version=1\n) 526 // S: flush-pkt 527 // H: PKT-LINE(version=1\0push-options...) 528 // H: flush-pkt 529 530 rs, err := readPktLine(ctx, reader, pktLineTypeData) 531 if err != nil { 532 return err 533 } 534 535 const VersionHead string = "version=1" 536 537 var ( 538 hasPushOptions bool 539 response = []byte(VersionHead) 540 requestOptions []string 541 ) 542 543 index := bytes.IndexByte(rs.Data, byte(0)) 544 if index >= len(rs.Data) { 545 return fail(ctx, "Protocol: format error", "pkt-line: format error "+fmt.Sprint(rs.Data)) 546 } 547 548 if index < 0 { 549 if len(rs.Data) == 10 && rs.Data[9] == '\n' { 550 index = 9 551 } else { 552 return fail(ctx, "Protocol: format error", "pkt-line: format error "+fmt.Sprint(rs.Data)) 553 } 554 } 555 556 if string(rs.Data[0:index]) != VersionHead { 557 return fail(ctx, "Protocol: version error", "Received unsupported version: %s", string(rs.Data[0:index])) 558 } 559 requestOptions = strings.Split(string(rs.Data[index+1:]), " ") 560 561 for _, option := range requestOptions { 562 if strings.HasPrefix(option, "push-options") { 563 response = append(response, byte(0)) 564 response = append(response, []byte("push-options")...) 565 hasPushOptions = true 566 } 567 } 568 response = append(response, '\n') 569 570 _, err = readPktLine(ctx, reader, pktLineTypeFlush) 571 if err != nil { 572 return err 573 } 574 575 err = writeDataPktLine(ctx, os.Stdout, response) 576 if err != nil { 577 return err 578 } 579 580 err = writeFlushPktLine(ctx, os.Stdout) 581 if err != nil { 582 return err 583 } 584 585 // 2. receive commands from server. 586 // S: PKT-LINE(<old-oid> <new-oid> <ref>) 587 // S: ... ... 588 // S: flush-pkt 589 // # [receive push-options] 590 // S: PKT-LINE(push-option) 591 // S: ... ... 592 // S: flush-pkt 593 hookOptions := private.HookOptions{ 594 UserName: pusherName, 595 UserID: pusherID, 596 } 597 hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize) 598 hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize) 599 hookOptions.RefFullNames = make([]git.RefName, 0, hookBatchSize) 600 601 for { 602 // note: pktLineTypeUnknow means pktLineTypeFlush and pktLineTypeData all allowed 603 rs, err = readPktLine(ctx, reader, pktLineTypeUnknow) 604 if err != nil { 605 return err 606 } 607 608 if rs.Type == pktLineTypeFlush { 609 break 610 } 611 t := strings.SplitN(string(rs.Data), " ", 3) 612 if len(t) != 3 { 613 continue 614 } 615 hookOptions.OldCommitIDs = append(hookOptions.OldCommitIDs, t[0]) 616 hookOptions.NewCommitIDs = append(hookOptions.NewCommitIDs, t[1]) 617 hookOptions.RefFullNames = append(hookOptions.RefFullNames, git.RefName(t[2])) 618 } 619 620 hookOptions.GitPushOptions = make(map[string]string) 621 622 if hasPushOptions { 623 for { 624 rs, err = readPktLine(ctx, reader, pktLineTypeUnknow) 625 if err != nil { 626 return err 627 } 628 629 if rs.Type == pktLineTypeFlush { 630 break 631 } 632 633 kv := strings.SplitN(string(rs.Data), "=", 2) 634 if len(kv) == 2 { 635 hookOptions.GitPushOptions[kv[0]] = kv[1] 636 } 637 } 638 } 639 640 // 3. run hook 641 resp, extra := private.HookProcReceive(ctx, repoUser, repoName, hookOptions) 642 if extra.HasError() { 643 return fail(ctx, extra.UserMsg, "HookProcReceive failed: %v", extra.Error) 644 } 645 646 // 4. response result to service 647 // # a. OK, but has an alternate reference. The alternate reference name 648 // # and other status can be given in option directives. 649 // H: PKT-LINE(ok <ref>) 650 // H: PKT-LINE(option refname <refname>) 651 // H: PKT-LINE(option old-oid <old-oid>) 652 // H: PKT-LINE(option new-oid <new-oid>) 653 // H: PKT-LINE(option forced-update) 654 // H: ... ... 655 // H: flush-pkt 656 // # b. NO, I reject it. 657 // H: PKT-LINE(ng <ref> <reason>) 658 // # c. Fall through, let 'receive-pack' to execute it. 659 // H: PKT-LINE(ok <ref>) 660 // H: PKT-LINE(option fall-through) 661 662 for _, rs := range resp.Results { 663 if len(rs.Err) > 0 { 664 err = writeDataPktLine(ctx, os.Stdout, []byte("ng "+rs.OriginalRef.String()+" "+rs.Err)) 665 if err != nil { 666 return err 667 } 668 continue 669 } 670 671 if rs.IsNotMatched { 672 err = writeDataPktLine(ctx, os.Stdout, []byte("ok "+rs.OriginalRef.String())) 673 if err != nil { 674 return err 675 } 676 err = writeDataPktLine(ctx, os.Stdout, []byte("option fall-through")) 677 if err != nil { 678 return err 679 } 680 continue 681 } 682 683 err = writeDataPktLine(ctx, os.Stdout, []byte("ok "+rs.OriginalRef)) 684 if err != nil { 685 return err 686 } 687 err = writeDataPktLine(ctx, os.Stdout, []byte("option refname "+rs.Ref)) 688 if err != nil { 689 return err 690 } 691 commitID, _ := git.NewIDFromString(rs.OldOID) 692 if !commitID.IsZero() { 693 err = writeDataPktLine(ctx, os.Stdout, []byte("option old-oid "+rs.OldOID)) 694 if err != nil { 695 return err 696 } 697 } 698 err = writeDataPktLine(ctx, os.Stdout, []byte("option new-oid "+rs.NewOID)) 699 if err != nil { 700 return err 701 } 702 if rs.IsForcePush { 703 err = writeDataPktLine(ctx, os.Stdout, []byte("option forced-update")) 704 if err != nil { 705 return err 706 } 707 } 708 } 709 err = writeFlushPktLine(ctx, os.Stdout) 710 711 if err == nil { 712 for _, res := range resp.Results { 713 hookPrintResult(res.ShouldShowMessage, res.IsCreatePR, res.HeadBranch, res.URL) 714 } 715 } 716 717 return err 718 } 719 720 // git PKT-Line api 721 // pktLineType message type of pkt-line 722 type pktLineType int64 723 724 const ( 725 // UnKnow type 726 pktLineTypeUnknow pktLineType = 0 727 // flush-pkt "0000" 728 pktLineTypeFlush pktLineType = iota 729 // data line 730 pktLineTypeData 731 ) 732 733 // gitPktLine pkt-line api 734 type gitPktLine struct { 735 Type pktLineType 736 Length uint64 737 Data []byte 738 } 739 740 func readPktLine(ctx context.Context, in *bufio.Reader, requestType pktLineType) (*gitPktLine, error) { 741 var ( 742 err error 743 r *gitPktLine 744 ) 745 746 // read prefix 747 lengthBytes := make([]byte, 4) 748 for i := 0; i < 4; i++ { 749 lengthBytes[i], err = in.ReadByte() 750 if err != nil { 751 return nil, fail(ctx, "Protocol: stdin error", "Pkt-Line: read stdin failed : %v", err) 752 } 753 } 754 755 r = new(gitPktLine) 756 r.Length, err = strconv.ParseUint(string(lengthBytes), 16, 32) 757 if err != nil { 758 return nil, fail(ctx, "Protocol: format parse error", "Pkt-Line format is wrong :%v", err) 759 } 760 761 if r.Length == 0 { 762 if requestType == pktLineTypeData { 763 return nil, fail(ctx, "Protocol: format data error", "Pkt-Line format is wrong") 764 } 765 r.Type = pktLineTypeFlush 766 return r, nil 767 } 768 769 if r.Length <= 4 || r.Length > 65520 || requestType == pktLineTypeFlush { 770 return nil, fail(ctx, "Protocol: format length error", "Pkt-Line format is wrong") 771 } 772 773 r.Data = make([]byte, r.Length-4) 774 for i := range r.Data { 775 r.Data[i], err = in.ReadByte() 776 if err != nil { 777 return nil, fail(ctx, "Protocol: data error", "Pkt-Line: read stdin failed : %v", err) 778 } 779 } 780 781 r.Type = pktLineTypeData 782 783 return r, nil 784 } 785 786 func writeFlushPktLine(ctx context.Context, out io.Writer) error { 787 l, err := out.Write([]byte("0000")) 788 if err != nil || l != 4 { 789 return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err) 790 } 791 return nil 792 } 793 794 func writeDataPktLine(ctx context.Context, out io.Writer, data []byte) error { 795 hexchar := []byte("0123456789abcdef") 796 hex := func(n uint64) byte { 797 return hexchar[(n)&15] 798 } 799 800 length := uint64(len(data) + 4) 801 tmp := make([]byte, 4) 802 tmp[0] = hex(length >> 12) 803 tmp[1] = hex(length >> 8) 804 tmp[2] = hex(length >> 4) 805 tmp[3] = hex(length) 806 807 lr, err := out.Write(tmp) 808 if err != nil || lr != 4 { 809 return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err) 810 } 811 812 lr, err = out.Write(data) 813 if err != nil || int(length-4) != lr { 814 return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err) 815 } 816 817 return nil 818 }