summaryrefslogtreecommitdiffstats
path: root/modules/git/parse.go
blob: 8c2c411db624a0e95e36ee39e2b1dfda8938ec6d (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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package git

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"strconv"
	"strings"

	"code.gitea.io/gitea/modules/log"
)

// ParseTreeEntries parses the output of a `git ls-tree -l` command.
func ParseTreeEntries(data []byte) ([]*TreeEntry, error) {
	return parseTreeEntries(data, nil)
}

var sepSpace = []byte{' '}

func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
	var err error
	entries := make([]*TreeEntry, 0, bytes.Count(data, []byte{'\n'})+1)
	for pos := 0; pos < len(data); {
		// expect line to be of the form:
		// <mode> <type> <sha> <space-padded-size>\t<filename>
		// <mode> <type> <sha>\t<filename>
		posEnd := bytes.IndexByte(data[pos:], '\n')
		if posEnd == -1 {
			posEnd = len(data)
		} else {
			posEnd += pos
		}
		line := data[pos:posEnd]
		posTab := bytes.IndexByte(line, '\t')
		if posTab == -1 {
			return nil, fmt.Errorf("invalid ls-tree output (no tab): %q", line)
		}

		entry := new(TreeEntry)
		entry.ptree = ptree

		entryAttrs := line[:posTab]
		entryName := line[posTab+1:]

		entryMode, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace)
		_ /* entryType */, entryAttrs, _ = bytes.Cut(entryAttrs, sepSpace) // the type is not used, the mode is enough to determine the type
		entryObjectID, entryAttrs, _ := bytes.Cut(entryAttrs, sepSpace)
		if len(entryAttrs) > 0 {
			entrySize := entryAttrs // the last field is the space-padded-size
			entry.size, _ = strconv.ParseInt(strings.TrimSpace(string(entrySize)), 10, 64)
			entry.sized = true
		}

		switch string(entryMode) {
		case "100644":
			entry.entryMode = EntryModeBlob
		case "100755":
			entry.entryMode = EntryModeExec
		case "120000":
			entry.entryMode = EntryModeSymlink
		case "160000":
			entry.entryMode = EntryModeCommit
		case "040000", "040755": // git uses 040000 for tree object, but some users may get 040755 for unknown reasons
			entry.entryMode = EntryModeTree
		default:
			return nil, fmt.Errorf("unknown type: %v", string(entryMode))
		}

		entry.ID, err = NewIDFromString(string(entryObjectID))
		if err != nil {
			return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err)
		}

		if len(entryName) > 0 && entryName[0] == '"' {
			entry.name, err = strconv.Unquote(string(entryName))
			if err != nil {
				return nil, fmt.Errorf("invalid ls-tree output (invalid name): %q, err: %w", line, err)
			}
		} else {
			entry.name = string(entryName)
		}

		pos = posEnd + 1
		entries = append(entries, entry)
	}
	return entries, nil
}

func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd *bufio.Reader, sz int64) ([]*TreeEntry, error) {
	fnameBuf := make([]byte, 4096)
	modeBuf := make([]byte, 40)
	shaBuf := make([]byte, objectFormat.FullLength())
	entries := make([]*TreeEntry, 0, 10)

loop:
	for sz > 0 {
		mode, fname, sha, count, err := ParseTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf)
		if err != nil {
			if err == io.EOF {
				break loop
			}
			return nil, err
		}
		sz -= int64(count)
		entry := new(TreeEntry)
		entry.ptree = ptree

		switch string(mode) {
		case "100644":
			entry.entryMode = EntryModeBlob
		case "100755":
			entry.entryMode = EntryModeExec
		case "120000":
			entry.entryMode = EntryModeSymlink
		case "160000":
			entry.entryMode = EntryModeCommit
		case "40000", "40755": // git uses 40000 for tree object, but some users may get 40755 for unknown reasons
			entry.entryMode = EntryModeTree
		default:
			log.Debug("Unknown mode: %v", string(mode))
			return nil, fmt.Errorf("unknown mode: %v", string(mode))
		}

		entry.ID = objectFormat.MustID(sha)
		entry.name = string(fname)
		entries = append(entries, entry)
	}
	if _, err := rd.Discard(1); err != nil {
		return entries, err
	}

	return entries, nil
}