1package catfile 2 3import ( 4 "bufio" 5 "bytes" 6 "context" 7 "fmt" 8 "io" 9 "strings" 10 11 "gitlab.com/gitlab-org/gitaly/v14/internal/git" 12 "gitlab.com/gitlab-org/gitaly/v14/internal/helper" 13 "gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb" 14) 15 16const ( 17 // MaxTagReferenceDepth is the maximum depth of tag references we will dereference 18 MaxTagReferenceDepth = 10 19) 20 21// GetTag looks up a commit by tagID using an existing catfile.Batch instance. When 'trim' is 22// 'true', the tag message will be trimmed to fit in a gRPC message. When 'trimRightNewLine' is 23// 'true', the tag message will be trimmed to remove all '\n' characters from right. note: we pass 24// in the tagName because the tag name from refs/tags may be different than the name found in the 25// actual tag object. We want to use the tagName found in refs/tags 26func GetTag(ctx context.Context, c Batch, tagID git.Revision, tagName string, trimLen, trimRightNewLine bool) (*gitalypb.Tag, error) { 27 tagObj, err := c.Tag(ctx, tagID) 28 if err != nil { 29 return nil, err 30 } 31 32 tag, err := buildAnnotatedTag(ctx, c, tagObj, []byte(tagName), trimLen, trimRightNewLine) 33 if err != nil { 34 return nil, err 35 } 36 37 return tag, nil 38} 39 40// ExtractTagSignature extracts the signature from a content and returns both the signature 41// and the remaining content. If no signature is found, nil as the signature and the entire 42// content are returned. note: tags contain the signature block at the end of the message 43// https://github.com/git/git/blob/master/Documentation/technical/signature-format.txt#L12 44func ExtractTagSignature(content []byte) ([]byte, []byte) { 45 index := bytes.Index(content, []byte("-----BEGIN")) 46 47 if index > 0 { 48 return bytes.TrimSuffix(content[index:], []byte("\n")), content[:index] 49 } 50 return nil, content 51} 52 53type tagHeader struct { 54 oid string 55 tagType string 56 tag string 57 tagger string 58} 59 60func splitRawTag(r io.Reader, trimRightNewLine bool) (*tagHeader, []byte, error) { 61 raw, err := io.ReadAll(r) 62 if err != nil { 63 return nil, nil, err 64 } 65 66 var body []byte 67 split := bytes.SplitN(raw, []byte("\n\n"), 2) 68 if len(split) == 2 { 69 body = split[1] 70 if trimRightNewLine { 71 // Remove trailing newline, if any, to preserve existing behavior the old GitLab tag finding code. 72 // See https://gitlab.com/gitlab-org/gitaly/blob/5e94dc966ac1900c11794b107a77496552591f9b/ruby/lib/gitlab/git/repository.rb#L211. 73 // Maybe this belongs in the FindAllTags handler, or even on the gitlab-ce client side, instead of here? 74 body = bytes.TrimRight(body, "\n") 75 } 76 } 77 78 var header tagHeader 79 s := bufio.NewScanner(bytes.NewReader(split[0])) 80 for s.Scan() { 81 headerSplit := strings.SplitN(s.Text(), " ", 2) 82 if len(headerSplit) != 2 { 83 continue 84 } 85 86 key, value := headerSplit[0], headerSplit[1] 87 switch key { 88 case "object": 89 header.oid = value 90 case "type": 91 header.tagType = value 92 case "tag": 93 header.tag = value 94 case "tagger": 95 header.tagger = value 96 } 97 } 98 99 return &header, body, nil 100} 101 102// ParseTag parses the tag from the given Reader. The tag's tagged commit is not populated. The 103// given object ID shall refer to the tag itself such that the returned Tag structure has the 104// correct OID. 105func ParseTag(r io.Reader, oid git.ObjectID) (*gitalypb.Tag, error) { 106 tag, _, err := parseTag(r, oid, nil, true, true) 107 return tag, err 108} 109 110func parseTag(r io.Reader, oid git.ObjectID, name []byte, trimLen, trimRightNewLine bool) (*gitalypb.Tag, *tagHeader, error) { 111 header, body, err := splitRawTag(r, trimRightNewLine) 112 if err != nil { 113 return nil, nil, err 114 } 115 116 if len(name) == 0 { 117 name = []byte(header.tag) 118 } 119 120 tag := &gitalypb.Tag{ 121 Id: oid.String(), 122 Name: name, 123 MessageSize: int64(len(body)), 124 Message: body, 125 } 126 127 if max := helper.MaxCommitOrTagMessageSize; trimLen && len(body) > max { 128 tag.Message = tag.Message[:max] 129 } 130 131 signature, _ := ExtractTagSignature(body) 132 if signature != nil { 133 length := bytes.Index(signature, []byte("\n")) 134 135 if length > 0 { 136 signature := string(signature[:length]) 137 tag.SignatureType = detectSignatureType(signature) 138 } 139 } 140 141 tag.Tagger = parseCommitAuthor(header.tagger) 142 143 return tag, header, nil 144} 145 146func buildAnnotatedTag(ctx context.Context, b Batch, object *Object, name []byte, trimLen, trimRightNewLine bool) (*gitalypb.Tag, error) { 147 tag, header, err := parseTag(object.Reader, object.ObjectInfo.Oid, name, trimLen, trimRightNewLine) 148 if err != nil { 149 return nil, err 150 } 151 152 switch header.tagType { 153 case "commit": 154 tag.TargetCommit, err = GetCommit(ctx, b, git.Revision(header.oid)) 155 if err != nil { 156 return nil, fmt.Errorf("buildAnnotatedTag error when getting target commit: %v", err) 157 } 158 159 case "tag": 160 tag.TargetCommit, err = dereferenceTag(ctx, b, git.Revision(header.oid)) 161 if err != nil { 162 return nil, fmt.Errorf("buildAnnotatedTag error when dereferencing tag: %v", err) 163 } 164 } 165 166 return tag, nil 167} 168 169// dereferenceTag recursively dereferences annotated tags until it finds a commit. 170// This matches the original behavior in the ruby implementation. 171// we also protect against circular tag references. Even though this is not possible in git, 172// we still want to protect against an infinite looop 173func dereferenceTag(ctx context.Context, b Batch, oid git.Revision) (*gitalypb.GitCommit, error) { 174 for depth := 0; depth < MaxTagReferenceDepth; depth++ { 175 i, err := b.Info(ctx, oid) 176 if err != nil { 177 return nil, err 178 } 179 180 switch i.Type { 181 case "tag": 182 tagObj, err := b.Tag(ctx, oid) 183 if err != nil { 184 return nil, err 185 } 186 187 header, _, err := splitRawTag(tagObj.Reader, true) 188 if err != nil { 189 return nil, err 190 } 191 192 oid = git.Revision(header.oid) 193 continue 194 case "commit": 195 return GetCommit(ctx, b, oid) 196 default: // This current tag points to a tree or a blob 197 return nil, nil 198 } 199 } 200 201 // at this point the tag nesting has gone too deep. We want to return silently here however, as we don't 202 // want to fail the entire request if one tag is nested too deeply. 203 return nil, nil 204} 205