code.gitea.io/gitea@v1.21.7/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: "Delegate commands to corresponding Git hooks", 35 Description: "This should only be called by Git", 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 := false 224 if git.CheckGitVersionAtLeast("2.29") == nil { 225 supportProcReceive = true 226 } 227 228 for scanner.Scan() { 229 // TODO: support news feeds for wiki 230 if isWiki { 231 continue 232 } 233 234 fields := bytes.Fields(scanner.Bytes()) 235 if len(fields) != 3 { 236 continue 237 } 238 239 oldCommitID := string(fields[0]) 240 newCommitID := string(fields[1]) 241 refFullName := git.RefName(fields[2]) 242 total++ 243 lastline++ 244 245 // If the ref is a branch or tag, check if it's protected 246 // if supportProcReceive all ref should be checked because 247 // permission check was delayed 248 if supportProcReceive || refFullName.IsBranch() || refFullName.IsTag() { 249 oldCommitIDs[count] = oldCommitID 250 newCommitIDs[count] = newCommitID 251 refFullNames[count] = refFullName 252 count++ 253 fmt.Fprintf(out, "*") 254 255 if count >= hookBatchSize { 256 fmt.Fprintf(out, " Checking %d references\n", count) 257 258 hookOptions.OldCommitIDs = oldCommitIDs 259 hookOptions.NewCommitIDs = newCommitIDs 260 hookOptions.RefFullNames = refFullNames 261 extra := private.HookPreReceive(ctx, username, reponame, hookOptions) 262 if extra.HasError() { 263 return fail(ctx, extra.UserMsg, "HookPreReceive(batch) failed: %v", extra.Error) 264 } 265 count = 0 266 lastline = 0 267 } 268 } else { 269 fmt.Fprintf(out, ".") 270 } 271 if lastline >= hookBatchSize { 272 fmt.Fprintf(out, "\n") 273 lastline = 0 274 } 275 } 276 277 if count > 0 { 278 hookOptions.OldCommitIDs = oldCommitIDs[:count] 279 hookOptions.NewCommitIDs = newCommitIDs[:count] 280 hookOptions.RefFullNames = refFullNames[:count] 281 282 fmt.Fprintf(out, " Checking %d references\n", count) 283 284 extra := private.HookPreReceive(ctx, username, reponame, hookOptions) 285 if extra.HasError() { 286 return fail(ctx, extra.UserMsg, "HookPreReceive(last) failed: %v", extra.Error) 287 } 288 } else if lastline > 0 { 289 fmt.Fprintf(out, "\n") 290 } 291 292 fmt.Fprintf(out, "Checked %d references in total\n", total) 293 return nil 294 } 295 296 func runHookUpdate(c *cli.Context) error { 297 // Update is empty and is kept only for backwards compatibility 298 return nil 299 } 300 301 func runHookPostReceive(c *cli.Context) error { 302 ctx, cancel := installSignals() 303 defer cancel() 304 305 setup(ctx, c.Bool("debug")) 306 307 // First of all run update-server-info no matter what 308 if _, _, err := git.NewCommand(ctx, "update-server-info").RunStdString(nil); err != nil { 309 return fmt.Errorf("Failed to call 'git update-server-info': %w", err) 310 } 311 312 // Now if we're an internal don't do anything else 313 if isInternal, _ := strconv.ParseBool(os.Getenv(repo_module.EnvIsInternal)); isInternal { 314 return nil 315 } 316 317 if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { 318 if setting.OnlyAllowPushIfGiteaEnvironmentSet { 319 return fail(ctx, `Rejecting changes as Gitea environment not set. 320 If you are pushing over SSH you must push with a key managed by 321 Gitea or set your environment appropriately.`, "") 322 } 323 return nil 324 } 325 326 var out io.Writer 327 var dWriter *delayWriter 328 out = &nilWriter{} 329 if setting.Git.VerbosePush { 330 if setting.Git.VerbosePushDelay > 0 { 331 dWriter = newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay) 332 defer dWriter.Close() 333 out = dWriter 334 } else { 335 out = os.Stdout 336 } 337 } 338 339 // the environment is set by serv command 340 repoUser := os.Getenv(repo_module.EnvRepoUsername) 341 isWiki, _ := strconv.ParseBool(os.Getenv(repo_module.EnvRepoIsWiki)) 342 repoName := os.Getenv(repo_module.EnvRepoName) 343 pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64) 344 pusherName := os.Getenv(repo_module.EnvPusherName) 345 346 hookOptions := private.HookOptions{ 347 UserName: pusherName, 348 UserID: pusherID, 349 GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories), 350 GitObjectDirectory: os.Getenv(private.GitObjectDirectory), 351 GitQuarantinePath: os.Getenv(private.GitQuarantinePath), 352 GitPushOptions: pushOptions(), 353 } 354 oldCommitIDs := make([]string, hookBatchSize) 355 newCommitIDs := make([]string, hookBatchSize) 356 refFullNames := make([]git.RefName, hookBatchSize) 357 count := 0 358 total := 0 359 wasEmpty := false 360 masterPushed := false 361 results := make([]private.HookPostReceiveBranchResult, 0) 362 363 scanner := bufio.NewScanner(os.Stdin) 364 for scanner.Scan() { 365 // TODO: support news feeds for wiki 366 if isWiki { 367 continue 368 } 369 370 fields := bytes.Fields(scanner.Bytes()) 371 if len(fields) != 3 { 372 continue 373 } 374 375 fmt.Fprintf(out, ".") 376 oldCommitIDs[count] = string(fields[0]) 377 newCommitIDs[count] = string(fields[1]) 378 refFullNames[count] = git.RefName(fields[2]) 379 if refFullNames[count] == git.BranchPrefix+"master" && newCommitIDs[count] != git.EmptySHA && count == total { 380 masterPushed = true 381 } 382 count++ 383 total++ 384 385 if count >= hookBatchSize { 386 fmt.Fprintf(out, " Processing %d references\n", count) 387 hookOptions.OldCommitIDs = oldCommitIDs 388 hookOptions.NewCommitIDs = newCommitIDs 389 hookOptions.RefFullNames = refFullNames 390 resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions) 391 if extra.HasError() { 392 _ = dWriter.Close() 393 hookPrintResults(results) 394 return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error) 395 } 396 wasEmpty = wasEmpty || resp.RepoWasEmpty 397 results = append(results, resp.Results...) 398 count = 0 399 } 400 } 401 402 if count == 0 { 403 if wasEmpty && masterPushed { 404 // We need to tell the repo to reset the default branch to master 405 extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master") 406 if extra.HasError() { 407 return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error) 408 } 409 } 410 fmt.Fprintf(out, "Processed %d references in total\n", total) 411 412 _ = dWriter.Close() 413 hookPrintResults(results) 414 return nil 415 } 416 417 hookOptions.OldCommitIDs = oldCommitIDs[:count] 418 hookOptions.NewCommitIDs = newCommitIDs[:count] 419 hookOptions.RefFullNames = refFullNames[:count] 420 421 fmt.Fprintf(out, " Processing %d references\n", count) 422 423 resp, extra := private.HookPostReceive(ctx, repoUser, repoName, hookOptions) 424 if resp == nil { 425 _ = dWriter.Close() 426 hookPrintResults(results) 427 return fail(ctx, extra.UserMsg, "HookPostReceive failed: %v", extra.Error) 428 } 429 wasEmpty = wasEmpty || resp.RepoWasEmpty 430 results = append(results, resp.Results...) 431 432 fmt.Fprintf(out, "Processed %d references in total\n", total) 433 434 if wasEmpty && masterPushed { 435 // We need to tell the repo to reset the default branch to master 436 extra := private.SetDefaultBranch(ctx, repoUser, repoName, "master") 437 if extra.HasError() { 438 return fail(ctx, extra.UserMsg, "SetDefaultBranch failed: %v", extra.Error) 439 } 440 } 441 _ = dWriter.Close() 442 hookPrintResults(results) 443 444 return nil 445 } 446 447 func hookPrintResults(results []private.HookPostReceiveBranchResult) { 448 for _, res := range results { 449 if !res.Message { 450 continue 451 } 452 453 fmt.Fprintln(os.Stderr, "") 454 if res.Create { 455 fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", res.Branch) 456 fmt.Fprintf(os.Stderr, " %s\n", res.URL) 457 } else { 458 fmt.Fprint(os.Stderr, "Visit the existing pull request:\n") 459 fmt.Fprintf(os.Stderr, " %s\n", res.URL) 460 } 461 fmt.Fprintln(os.Stderr, "") 462 os.Stderr.Sync() 463 } 464 } 465 466 func pushOptions() map[string]string { 467 opts := make(map[string]string) 468 if pushCount, err := strconv.Atoi(os.Getenv(private.GitPushOptionCount)); err == nil { 469 for idx := 0; idx < pushCount; idx++ { 470 opt := os.Getenv(fmt.Sprintf("GIT_PUSH_OPTION_%d", idx)) 471 kv := strings.SplitN(opt, "=", 2) 472 if len(kv) == 2 { 473 opts[kv[0]] = kv[1] 474 } 475 } 476 } 477 return opts 478 } 479 480 func runHookProcReceive(c *cli.Context) error { 481 ctx, cancel := installSignals() 482 defer cancel() 483 484 setup(ctx, c.Bool("debug")) 485 486 if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { 487 if setting.OnlyAllowPushIfGiteaEnvironmentSet { 488 return fail(ctx, `Rejecting changes as Gitea environment not set. 489 If you are pushing over SSH you must push with a key managed by 490 Gitea or set your environment appropriately.`, "") 491 } 492 return nil 493 } 494 495 if git.CheckGitVersionAtLeast("2.29") != nil { 496 return fail(ctx, "No proc-receive support", "current git version doesn't support proc-receive.") 497 } 498 499 reader := bufio.NewReader(os.Stdin) 500 repoUser := os.Getenv(repo_module.EnvRepoUsername) 501 repoName := os.Getenv(repo_module.EnvRepoName) 502 pusherID, _ := strconv.ParseInt(os.Getenv(repo_module.EnvPusherID), 10, 64) 503 pusherName := os.Getenv(repo_module.EnvPusherName) 504 505 // 1. Version and features negotiation. 506 // S: PKT-LINE(version=1\0push-options atomic...) / PKT-LINE(version=1\n) 507 // S: flush-pkt 508 // H: PKT-LINE(version=1\0push-options...) 509 // H: flush-pkt 510 511 rs, err := readPktLine(ctx, reader, pktLineTypeData) 512 if err != nil { 513 return err 514 } 515 516 const VersionHead string = "version=1" 517 518 var ( 519 hasPushOptions bool 520 response = []byte(VersionHead) 521 requestOptions []string 522 ) 523 524 index := bytes.IndexByte(rs.Data, byte(0)) 525 if index >= len(rs.Data) { 526 return fail(ctx, "Protocol: format error", "pkt-line: format error "+fmt.Sprint(rs.Data)) 527 } 528 529 if index < 0 { 530 if len(rs.Data) == 10 && rs.Data[9] == '\n' { 531 index = 9 532 } else { 533 return fail(ctx, "Protocol: format error", "pkt-line: format error "+fmt.Sprint(rs.Data)) 534 } 535 } 536 537 if string(rs.Data[0:index]) != VersionHead { 538 return fail(ctx, "Protocol: version error", "Received unsupported version: %s", string(rs.Data[0:index])) 539 } 540 requestOptions = strings.Split(string(rs.Data[index+1:]), " ") 541 542 for _, option := range requestOptions { 543 if strings.HasPrefix(option, "push-options") { 544 response = append(response, byte(0)) 545 response = append(response, []byte("push-options")...) 546 hasPushOptions = true 547 } 548 } 549 response = append(response, '\n') 550 551 _, err = readPktLine(ctx, reader, pktLineTypeFlush) 552 if err != nil { 553 return err 554 } 555 556 err = writeDataPktLine(ctx, os.Stdout, response) 557 if err != nil { 558 return err 559 } 560 561 err = writeFlushPktLine(ctx, os.Stdout) 562 if err != nil { 563 return err 564 } 565 566 // 2. receive commands from server. 567 // S: PKT-LINE(<old-oid> <new-oid> <ref>) 568 // S: ... ... 569 // S: flush-pkt 570 // # [receive push-options] 571 // S: PKT-LINE(push-option) 572 // S: ... ... 573 // S: flush-pkt 574 hookOptions := private.HookOptions{ 575 UserName: pusherName, 576 UserID: pusherID, 577 } 578 hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize) 579 hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize) 580 hookOptions.RefFullNames = make([]git.RefName, 0, hookBatchSize) 581 582 for { 583 // note: pktLineTypeUnknow means pktLineTypeFlush and pktLineTypeData all allowed 584 rs, err = readPktLine(ctx, reader, pktLineTypeUnknow) 585 if err != nil { 586 return err 587 } 588 589 if rs.Type == pktLineTypeFlush { 590 break 591 } 592 t := strings.SplitN(string(rs.Data), " ", 3) 593 if len(t) != 3 { 594 continue 595 } 596 hookOptions.OldCommitIDs = append(hookOptions.OldCommitIDs, t[0]) 597 hookOptions.NewCommitIDs = append(hookOptions.NewCommitIDs, t[1]) 598 hookOptions.RefFullNames = append(hookOptions.RefFullNames, git.RefName(t[2])) 599 } 600 601 hookOptions.GitPushOptions = make(map[string]string) 602 603 if hasPushOptions { 604 for { 605 rs, err = readPktLine(ctx, reader, pktLineTypeUnknow) 606 if err != nil { 607 return err 608 } 609 610 if rs.Type == pktLineTypeFlush { 611 break 612 } 613 614 kv := strings.SplitN(string(rs.Data), "=", 2) 615 if len(kv) == 2 { 616 hookOptions.GitPushOptions[kv[0]] = kv[1] 617 } 618 } 619 } 620 621 // 3. run hook 622 resp, extra := private.HookProcReceive(ctx, repoUser, repoName, hookOptions) 623 if extra.HasError() { 624 return fail(ctx, extra.UserMsg, "HookProcReceive failed: %v", extra.Error) 625 } 626 627 // 4. response result to service 628 // # a. OK, but has an alternate reference. The alternate reference name 629 // # and other status can be given in option directives. 630 // H: PKT-LINE(ok <ref>) 631 // H: PKT-LINE(option refname <refname>) 632 // H: PKT-LINE(option old-oid <old-oid>) 633 // H: PKT-LINE(option new-oid <new-oid>) 634 // H: PKT-LINE(option forced-update) 635 // H: ... ... 636 // H: flush-pkt 637 // # b. NO, I reject it. 638 // H: PKT-LINE(ng <ref> <reason>) 639 // # c. Fall through, let 'receive-pack' to execute it. 640 // H: PKT-LINE(ok <ref>) 641 // H: PKT-LINE(option fall-through) 642 643 for _, rs := range resp.Results { 644 if len(rs.Err) > 0 { 645 err = writeDataPktLine(ctx, os.Stdout, []byte("ng "+rs.OriginalRef.String()+" "+rs.Err)) 646 if err != nil { 647 return err 648 } 649 continue 650 } 651 652 if rs.IsNotMatched { 653 err = writeDataPktLine(ctx, os.Stdout, []byte("ok "+rs.OriginalRef.String())) 654 if err != nil { 655 return err 656 } 657 err = writeDataPktLine(ctx, os.Stdout, []byte("option fall-through")) 658 if err != nil { 659 return err 660 } 661 continue 662 } 663 664 err = writeDataPktLine(ctx, os.Stdout, []byte("ok "+rs.OriginalRef)) 665 if err != nil { 666 return err 667 } 668 err = writeDataPktLine(ctx, os.Stdout, []byte("option refname "+rs.Ref)) 669 if err != nil { 670 return err 671 } 672 if rs.OldOID != git.EmptySHA { 673 err = writeDataPktLine(ctx, os.Stdout, []byte("option old-oid "+rs.OldOID)) 674 if err != nil { 675 return err 676 } 677 } 678 err = writeDataPktLine(ctx, os.Stdout, []byte("option new-oid "+rs.NewOID)) 679 if err != nil { 680 return err 681 } 682 if rs.IsForcePush { 683 err = writeDataPktLine(ctx, os.Stdout, []byte("option forced-update")) 684 if err != nil { 685 return err 686 } 687 } 688 } 689 err = writeFlushPktLine(ctx, os.Stdout) 690 691 return err 692 } 693 694 // git PKT-Line api 695 // pktLineType message type of pkt-line 696 type pktLineType int64 697 698 const ( 699 // UnKnow type 700 pktLineTypeUnknow pktLineType = 0 701 // flush-pkt "0000" 702 pktLineTypeFlush pktLineType = iota 703 // data line 704 pktLineTypeData 705 ) 706 707 // gitPktLine pkt-line api 708 type gitPktLine struct { 709 Type pktLineType 710 Length uint64 711 Data []byte 712 } 713 714 func readPktLine(ctx context.Context, in *bufio.Reader, requestType pktLineType) (*gitPktLine, error) { 715 var ( 716 err error 717 r *gitPktLine 718 ) 719 720 // read prefix 721 lengthBytes := make([]byte, 4) 722 for i := 0; i < 4; i++ { 723 lengthBytes[i], err = in.ReadByte() 724 if err != nil { 725 return nil, fail(ctx, "Protocol: stdin error", "Pkt-Line: read stdin failed : %v", err) 726 } 727 } 728 729 r = new(gitPktLine) 730 r.Length, err = strconv.ParseUint(string(lengthBytes), 16, 32) 731 if err != nil { 732 return nil, fail(ctx, "Protocol: format parse error", "Pkt-Line format is wrong :%v", err) 733 } 734 735 if r.Length == 0 { 736 if requestType == pktLineTypeData { 737 return nil, fail(ctx, "Protocol: format data error", "Pkt-Line format is wrong") 738 } 739 r.Type = pktLineTypeFlush 740 return r, nil 741 } 742 743 if r.Length <= 4 || r.Length > 65520 || requestType == pktLineTypeFlush { 744 return nil, fail(ctx, "Protocol: format length error", "Pkt-Line format is wrong") 745 } 746 747 r.Data = make([]byte, r.Length-4) 748 for i := range r.Data { 749 r.Data[i], err = in.ReadByte() 750 if err != nil { 751 return nil, fail(ctx, "Protocol: data error", "Pkt-Line: read stdin failed : %v", err) 752 } 753 } 754 755 r.Type = pktLineTypeData 756 757 return r, nil 758 } 759 760 func writeFlushPktLine(ctx context.Context, out io.Writer) error { 761 l, err := out.Write([]byte("0000")) 762 if err != nil || l != 4 { 763 return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err) 764 } 765 return nil 766 } 767 768 func writeDataPktLine(ctx context.Context, out io.Writer, data []byte) error { 769 hexchar := []byte("0123456789abcdef") 770 hex := func(n uint64) byte { 771 return hexchar[(n)&15] 772 } 773 774 length := uint64(len(data) + 4) 775 tmp := make([]byte, 4) 776 tmp[0] = hex(length >> 12) 777 tmp[1] = hex(length >> 8) 778 tmp[2] = hex(length >> 4) 779 tmp[3] = hex(length) 780 781 lr, err := out.Write(tmp) 782 if err != nil || lr != 4 { 783 return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err) 784 } 785 786 lr, err = out.Write(data) 787 if err != nil || int(length-4) != lr { 788 return fail(ctx, "Protocol: write error", "Pkt-Line response failed: %v", err) 789 } 790 791 return nil 792 }