git-module

git clone git://git.lin.moe/fork/git-module.git

  1package git
  2
  3import (
  4	"bytes"
  5	"context"
  6	"fmt"
  7)
  8
  9// UnescapeChars reverses escaped characters in quoted output from Git.
 10func UnescapeChars(in []byte) []byte {
 11	if !bytes.ContainsRune(in, '\\') {
 12		return in
 13	}
 14
 15	out := make([]byte, 0, len(in))
 16	for i := 0; i < len(in); i++ {
 17		if in[i] == '\\' && i+1 < len(in) {
 18			switch in[i+1] {
 19			case '\\':
 20				out = append(out, '\\')
 21				i++
 22			case '"':
 23				out = append(out, '"')
 24				i++
 25			case 't':
 26				out = append(out, '\t')
 27				i++
 28			case 'n':
 29				out = append(out, '\n')
 30				i++
 31			default:
 32				out = append(out, in[i])
 33			}
 34		} else {
 35			out = append(out, in[i])
 36		}
 37	}
 38	return out
 39}
 40
 41// parseTree parses tree information from the (uncompressed) raw data of the
 42// tree object. The lineTerminator specifies the character used to separate
 43// entries ('\n' for normal output, '\x00' for verbatim output).
 44func parseTree(t *Tree, data []byte, lineTerminator byte) ([]*TreeEntry, error) {
 45	entries := make([]*TreeEntry, 0, 10)
 46	l := len(data)
 47	pos := 0
 48	for pos < l {
 49		entry := new(TreeEntry)
 50		entry.parent = t
 51		step := 6
 52		switch string(data[pos : pos+step]) {
 53		case "100644", "100664":
 54			entry.mode = EntryBlob
 55			entry.typ = ObjectBlob
 56		case "100755":
 57			entry.mode = EntryExec
 58			entry.typ = ObjectBlob
 59		case "120000":
 60			entry.mode = EntrySymlink
 61			entry.typ = ObjectBlob
 62		case "160000":
 63			entry.mode = EntryCommit
 64			entry.typ = ObjectCommit
 65
 66			step = 8
 67		case "040000":
 68			entry.mode = EntryTree
 69			entry.typ = ObjectTree
 70		default:
 71			return nil, fmt.Errorf("unknown type: %v", string(data[pos:pos+step]))
 72		}
 73		pos += step + 6 // Skip string type of entry type.
 74
 75		step = bytes.Index(data[pos:], []byte{9}) // index of \t
 76
 77		id, err := NewIDFromString(string(data[pos : pos+step]))
 78		if err != nil {
 79			return nil, err
 80		}
 81		entry.id = id
 82		pos += step + 1 // Skip half of Oid.
 83
 84		step = bytes.IndexByte(data[pos:], lineTerminator)
 85		if data[pos] == '"' {
 86			entry.name = string(UnescapeChars(data[pos+1 : pos+step-1]))
 87		} else {
 88			entry.name = string(data[pos : pos+step])
 89		}
 90
 91		pos += step + 1
 92		entries = append(entries, entry)
 93	}
 94	return entries, nil
 95}
 96
 97// LsTreeOptions contains optional arguments for listing trees.
 98//
 99// Docs: https://git-scm.com/docs/git-ls-tree
100type LsTreeOptions struct {
101	// Verbatim outputs filenames unquoted using the -z flag. This avoids issues
102	// with special characters in filenames that would otherwise be quoted by Git.
103	Verbatim bool
104	CommandOptions
105}
106
107// LsTree returns the tree object in the repository by given tree ID.
108func (r *Repository) LsTree(ctx context.Context, treeID string, opts ...LsTreeOptions) (*Tree, error) {
109	var opt LsTreeOptions
110	if len(opts) > 0 {
111		opt = opts[0]
112	}
113
114	cache, ok := r.cachedTrees.Get(treeID)
115	if ok {
116		logf("Cached tree hit: %s", treeID)
117		return cache.(*Tree), nil
118	}
119
120	var err error
121	treeID, err = r.RevParse(ctx, treeID) //nolint
122	if err != nil {
123		return nil, err
124	}
125	t := &Tree{
126		id:   MustIDFromString(treeID),
127		repo: r,
128	}
129
130	args := []string{"ls-tree"}
131	if opt.Verbatim {
132		args = append(args, "-z")
133	}
134	args = append(args, "--end-of-options", treeID)
135
136	stdout, err := exec(ctx, r.path, args, opt.Envs)
137	if err != nil {
138		return nil, err
139	}
140
141	lineTerminator := byte('\n')
142	if opt.Verbatim {
143		lineTerminator = 0
144	}
145	t.entries, err = parseTree(t, stdout, lineTerminator)
146	if err != nil {
147		return nil, err
148	}
149
150	r.cachedTrees.Set(treeID, t)
151	return t, nil
152}