1package git23import (4 "bytes"5 "context"6 "fmt"7)89// UnescapeChars reverses escaped characters in quoted output from Git.10func UnescapeChars(in []byte) []byte {11 if !bytes.ContainsRune(in, '\\') {12 return in13 }1415 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 out39}4041// parseTree parses tree information from the (uncompressed) raw data of the42// tree object. The lineTerminator specifies the character used to separate43// 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 := 048 for pos < l {49 entry := new(TreeEntry)50 entry.parent = t51 step := 652 switch string(data[pos : pos+step]) {53 case "100644", "100664":54 entry.mode = EntryBlob55 entry.typ = ObjectBlob56 case "100755":57 entry.mode = EntryExec58 entry.typ = ObjectBlob59 case "120000":60 entry.mode = EntrySymlink61 entry.typ = ObjectBlob62 case "160000":63 entry.mode = EntryCommit64 entry.typ = ObjectCommit6566 step = 867 case "040000":68 entry.mode = EntryTree69 entry.typ = ObjectTree70 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.7475 step = bytes.Index(data[pos:], []byte{9}) // index of \t7677 id, err := NewIDFromString(string(data[pos : pos+step]))78 if err != nil {79 return nil, err80 }81 entry.id = id82 pos += step + 1 // Skip half of Oid.8384 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 }9091 pos += step + 192 entries = append(entries, entry)93 }94 return entries, nil95}9697// LsTreeOptions contains optional arguments for listing trees.98//99// Docs: https://git-scm.com/docs/git-ls-tree100type LsTreeOptions struct {101 // Verbatim outputs filenames unquoted using the -z flag. This avoids issues102 // with special characters in filenames that would otherwise be quoted by Git.103 Verbatim bool104 CommandOptions105}106107// 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 LsTreeOptions110 if len(opts) > 0 {111 opt = opts[0]112 }113114 cache, ok := r.cachedTrees.Get(treeID)115 if ok {116 logf("Cached tree hit: %s", treeID)117 return cache.(*Tree), nil118 }119120 var err error121 treeID, err = r.RevParse(ctx, treeID) //nolint122 if err != nil {123 return nil, err124 }125 t := &Tree{126 id: MustIDFromString(treeID),127 repo: r,128 }129130 args := []string{"ls-tree"}131 if opt.Verbatim {132 args = append(args, "-z")133 }134 args = append(args, "--end-of-options", treeID)135136 stdout, err := exec(ctx, r.path, args, opt.Envs)137 if err != nil {138 return nil, err139 }140141 lineTerminator := byte('\n')142 if opt.Verbatim {143 lineTerminator = 0144 }145 t.entries, err = parseTree(t, stdout, lineTerminator)146 if err != nil {147 return nil, err148 }149150 r.cachedTrees.Set(treeID, t)151 return t, nil152}