@@ -10,28 +10,42 @@ import (
1010 "time"
1111)
1212
13- // UnescapeChars reverses escaped characters.
13+ // UnescapeChars reverses escaped characters in quoted output from Git .
1414func UnescapeChars (in []byte ) []byte {
15- if bytes .ContainsAny (in , " \\ \t " ) {
15+ if ! bytes .ContainsRune (in , '\\' ) {
1616 return in
1717 }
1818
19- out := bytes .Replace (in , escapedSlash , regularSlash , - 1 )
20- out = bytes .Replace (out , escapedTab , regularTab , - 1 )
19+ out := make ([]byte , 0 , len (in ))
20+ for i := 0 ; i < len (in ); i ++ {
21+ if in [i ] == '\\' && i + 1 < len (in ) {
22+ switch in [i + 1 ] {
23+ case '\\' :
24+ out = append (out , '\\' )
25+ i ++
26+ case '"' :
27+ out = append (out , '"' )
28+ i ++
29+ case 't' :
30+ out = append (out , '\t' )
31+ i ++
32+ case 'n' :
33+ out = append (out , '\n' )
34+ i ++
35+ default :
36+ out = append (out , in [i ])
37+ }
38+ } else {
39+ out = append (out , in [i ])
40+ }
41+ }
2142 return out
2243}
2344
24- // Predefine []byte variables to avoid runtime allocations.
25- var (
26- escapedSlash = []byte (`\\` )
27- regularSlash = []byte (`\` )
28- escapedTab = []byte (`\t` )
29- regularTab = []byte ("\t " )
30- )
31-
3245// parseTree parses tree information from the (uncompressed) raw data of the
33- // tree object.
34- func parseTree (t * Tree , data []byte ) ([]* TreeEntry , error ) {
46+ // tree object. The lineTerminator specifies the character used to separate
47+ // entries ('\n' for normal output, '\x00' for verbatim output).
48+ func parseTree (t * Tree , data []byte , lineTerminator byte ) ([]* TreeEntry , error ) {
3549 entries := make ([]* TreeEntry , 0 , 10 )
3650 l := len (data )
3751 pos := 0
@@ -70,9 +84,7 @@ func parseTree(t *Tree, data []byte) ([]*TreeEntry, error) {
7084 entry .id = id
7185 pos += step + 1 // Skip half of SHA1.
7286
73- step = bytes .IndexByte (data [pos :], '\n' )
74-
75- // In case entry name is surrounded by double quotes(it happens only in git-shell).
87+ step = bytes .IndexByte (data [pos :], lineTerminator )
7688 if data [pos ] == '"' {
7789 entry .name = string (UnescapeChars (data [pos + 1 : pos + step - 1 ]))
7890 } else {
@@ -89,12 +101,15 @@ func parseTree(t *Tree, data []byte) ([]*TreeEntry, error) {
89101//
90102// Docs: https://git-scm.com/docs/git-ls-tree
91103type LsTreeOptions struct {
104+ // Verbatim outputs filenames unquoted using the -z flag. This avoids issues
105+ // with special characters in filenames that would otherwise be quoted by Git.
106+ Verbatim bool
92107 // The timeout duration before giving up for each shell command execution. The
93108 // default timeout duration will be used when not supplied.
94109 //
95110 // Deprecated: Use CommandOptions.Timeout instead.
96111 Timeout time.Duration
97- // The additional options to be passed to the underlying git .
112+ // The additional options to be passed to the underlying Git .
98113 CommandOptions
99114}
100115
@@ -121,15 +136,23 @@ func (r *Repository) LsTree(treeID string, opts ...LsTreeOptions) (*Tree, error)
121136 repo : r ,
122137 }
123138
124- stdout , err := NewCommand ("ls-tree" ).
139+ cmd := NewCommand ("ls-tree" )
140+ if opt .Verbatim {
141+ cmd .AddArgs ("-z" )
142+ }
143+ stdout , err := cmd .
125144 AddOptions (opt .CommandOptions ).
126145 AddArgs (treeID ).
127146 RunInDirWithTimeout (opt .Timeout , r .path )
128147 if err != nil {
129148 return nil , err
130149 }
131150
132- t .entries , err = parseTree (t , stdout )
151+ lineTerminator := byte ('\n' )
152+ if opt .Verbatim {
153+ lineTerminator = 0
154+ }
155+ t .entries , err = parseTree (t , stdout , lineTerminator )
133156 if err != nil {
134157 return nil , err
135158 }
0 commit comments