aboutsummaryrefslogtreecommitdiffstats
path: root/modules/git/commit_reader.go
blob: eb8f4c632276582452ddaaa0e760942c050e4d37 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package git

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
)

const (
	commitHeaderGpgsig       = "gpgsig"
	commitHeaderGpgsigSha256 = "gpgsig-sha256"
)

func assignCommitFields(gitRepo *Repository, commit *Commit, headerKey string, headerValue []byte) error {
	if len(headerValue) > 0 && headerValue[len(headerValue)-1] == '\n' {
		headerValue = headerValue[:len(headerValue)-1] // remove trailing newline
	}
	switch headerKey {
	case "tree":
		objID, err := NewIDFromString(string(headerValue))
		if err != nil {
			return fmt.Errorf("invalid tree ID %q: %w", string(headerValue), err)
		}
		commit.Tree = *NewTree(gitRepo, objID)
	case "parent":
		objID, err := NewIDFromString(string(headerValue))
		if err != nil {
			return fmt.Errorf("invalid parent ID %q: %w", string(headerValue), err)
		}
		commit.Parents = append(commit.Parents, objID)
	case "author":
		commit.Author.Decode(headerValue)
	case "committer":
		commit.Committer.Decode(headerValue)
	case commitHeaderGpgsig, commitHeaderGpgsigSha256:
		// if there are duplicate "gpgsig" and "gpgsig-sha256" headers, then the signature must have already been invalid
		// so we don't need to handle duplicate headers here
		commit.Signature = &CommitSignature{Signature: string(headerValue)}
	}
	return nil
}

// CommitFromReader will generate a Commit from a provided reader
// We need this to interpret commits from cat-file or cat-file --batch
//
// If used as part of a cat-file --batch stream you need to limit the reader to the correct size
func CommitFromReader(gitRepo *Repository, objectID ObjectID, reader io.Reader) (*Commit, error) {
	commit := &Commit{
		ID:        objectID,
		Author:    &Signature{},
		Committer: &Signature{},
	}

	bufReader := bufio.NewReader(reader)
	inHeader := true
	var payloadSB, messageSB bytes.Buffer
	var headerKey string
	var headerValue []byte
	for {
		line, err := bufReader.ReadBytes('\n')
		if err != nil && err != io.EOF {
			return nil, fmt.Errorf("unable to read commit %q: %w", objectID.String(), err)
		}
		if len(line) == 0 {
			break
		}

		if inHeader {
			inHeader = !(len(line) == 1 && line[0] == '\n') // still in header if line is not just a newline
			k, v, _ := bytes.Cut(line, []byte{' '})
			if len(k) != 0 || !inHeader {
				if headerKey != "" {
					if err = assignCommitFields(gitRepo, commit, headerKey, headerValue); err != nil {
						return nil, fmt.Errorf("unable to parse commit %q: %w", objectID.String(), err)
					}
				}
				headerKey = string(k) // it also resets the headerValue to empty string if not inHeader
				headerValue = v
			} else {
				headerValue = append(headerValue, v...)
			}
			if headerKey != commitHeaderGpgsig && headerKey != commitHeaderGpgsigSha256 {
				_, _ = payloadSB.Write(line)
			}
		} else {
			_, _ = messageSB.Write(line)
			_, _ = payloadSB.Write(line)
		}
	}

	commit.CommitMessage = messageSB.String()
	if commit.Signature != nil {
		commit.Signature.Payload = payloadSB.String()
	}
	return commit, nil
}