Skip to content

Commit 8de0b0a

Browse files
richmahnlafriks
authored andcommitted
Fixes #2738 - Adds the /git/tags API endpoint (#7138)
* Fixes #2738 - /git/tags API * proper URLs * Adds function comments * Updates swagger * Removes newline from tag message * Removes trailing newline from commit message * Adds integration test * Removed debugging * Adds tests * Fixes bug where multiple tags of same commit show wrong tag name * Fix formatting * Removes unused varaible * Fix to annotated tag function names and response * Update modules/git/repo_tag.go Co-Authored-By: Lauris BH <[email protected]> * Uses TagPrefix * Changes per review, better error handling for getting tag and commit IDs * Fix to getting commit ID * Fix to getting commit ID * Fix to getting commit ID * Fix to getting commit ID
1 parent 23a2ee3 commit 8de0b0a

16 files changed

+551
-85
lines changed

integrations/api_repo_file_create_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ func TestAPICreateFile(t *testing.T) {
146146
var fileResponse api.FileResponse
147147
DecodeJSON(t, resp, &fileResponse)
148148
expectedSHA := "a635aa942442ddfdba07468cf9661c08fbdf0ebf"
149-
expectedHTMLURL := fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/blob/new_branch/new/file%d.txt", fileID)
150-
expectedDownloadURL := fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/raw/branch/new_branch/new/file%d.txt", fileID)
149+
expectedHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/blob/new_branch/new/file%d.txt", fileID)
150+
expectedDownloadURL := fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/new_branch/new/file%d.txt", fileID)
151151
assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA)
152152
assert.EqualValues(t, expectedHTMLURL, fileResponse.Content.HTMLURL)
153153
assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL)

integrations/api_repo_file_update_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,8 @@ func TestAPIUpdateFile(t *testing.T) {
136136
var fileResponse api.FileResponse
137137
DecodeJSON(t, resp, &fileResponse)
138138
expectedSHA := "08bd14b2e2852529157324de9c226b3364e76136"
139-
expectedHTMLURL := fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/blob/new_branch/update/file%d.txt", fileID)
140-
expectedDownloadURL := fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/raw/branch/new_branch/update/file%d.txt", fileID)
139+
expectedHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/blob/new_branch/update/file%d.txt", fileID)
140+
expectedDownloadURL := fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/new_branch/update/file%d.txt", fileID)
141141
assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA)
142142
assert.EqualValues(t, expectedHTMLURL, fileResponse.Content.HTMLURL)
143143
assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL)
@@ -155,8 +155,8 @@ func TestAPIUpdateFile(t *testing.T) {
155155
resp = session.MakeRequest(t, req, http.StatusOK)
156156
DecodeJSON(t, resp, &fileResponse)
157157
expectedSHA = "08bd14b2e2852529157324de9c226b3364e76136"
158-
expectedHTMLURL = fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/blob/master/rename/update/file%d.txt", fileID)
159-
expectedDownloadURL = fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/raw/branch/master/rename/update/file%d.txt", fileID)
158+
expectedHTMLURL = fmt.Sprintf(setting.AppURL+"user2/repo1/blob/master/rename/update/file%d.txt", fileID)
159+
expectedDownloadURL = fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/master/rename/update/file%d.txt", fileID)
160160
assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA)
161161
assert.EqualValues(t, expectedHTMLURL, fileResponse.Content.HTMLURL)
162162
assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL)
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright 2018 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package integrations
6+
7+
import (
8+
"net/http"
9+
"testing"
10+
11+
"code.gitea.io/gitea/models"
12+
"code.gitea.io/gitea/modules/git"
13+
api "code.gitea.io/gitea/modules/structs"
14+
"code.gitea.io/gitea/modules/util"
15+
16+
"github.com/stretchr/testify/assert"
17+
)
18+
19+
func TestAPIGitTags(t *testing.T) {
20+
prepareTestEnv(t)
21+
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
22+
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
23+
// Login as User2.
24+
session := loginUser(t, user.Name)
25+
token := getTokenForLoggedInUser(t, session)
26+
27+
// Set up git config for the tagger
28+
git.NewCommand("config", "user.name", user.Name).RunInDir(repo.RepoPath())
29+
git.NewCommand("config", "user.email", user.Email).RunInDir(repo.RepoPath())
30+
31+
gitRepo, _ := git.OpenRepository(repo.RepoPath())
32+
commit, _ := gitRepo.GetBranchCommit("master")
33+
lTagName := "lightweightTag"
34+
gitRepo.CreateTag(lTagName, commit.ID.String())
35+
36+
aTagName := "annotatedTag"
37+
aTagMessage := "my annotated message"
38+
gitRepo.CreateAnnotatedTag(aTagName, aTagMessage, commit.ID.String())
39+
aTag, _ := gitRepo.GetTag(aTagName)
40+
41+
// SHOULD work for annotated tags
42+
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/tags/%s?token=%s", user.Name, repo.Name, aTag.ID.String(), token)
43+
res := session.MakeRequest(t, req, http.StatusOK)
44+
45+
var tag *api.AnnotatedTag
46+
DecodeJSON(t, res, &tag)
47+
48+
assert.Equal(t, aTagName, tag.Tag)
49+
assert.Equal(t, aTag.ID.String(), tag.SHA)
50+
assert.Equal(t, commit.ID.String(), tag.Object.SHA)
51+
assert.Equal(t, aTagMessage, tag.Message)
52+
assert.Equal(t, user.Name, tag.Tagger.Name)
53+
assert.Equal(t, user.Email, tag.Tagger.Email)
54+
assert.Equal(t, util.URLJoin(repo.APIURL(), "git/tags", aTag.ID.String()), tag.URL)
55+
56+
// Should NOT work for lightweight tags
57+
badReq := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/tags/%s?token=%s", user.Name, repo.Name, commit.ID.String(), token)
58+
session.MakeRequest(t, badReq, http.StatusBadRequest)
59+
}

integrations/api_repo_tags_test.go

+3-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package integrations
66

77
import (
88
"net/http"
9-
"path"
109
"testing"
1110

1211
"code.gitea.io/gitea/models"
@@ -32,7 +31,7 @@ func TestAPIReposGetTags(t *testing.T) {
3231
assert.EqualValues(t, 1, len(tags))
3332
assert.Equal(t, "v1.1", tags[0].Name)
3433
assert.Equal(t, "65f1bf27bc3bf70f64657658635e66094edbcb4d", tags[0].Commit.SHA)
35-
assert.Equal(t, path.Join(setting.AppSubURL, "/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d"), tags[0].Commit.URL)
36-
assert.Equal(t, path.Join(setting.AppSubURL, "/user2/repo1/archive/v1.1.zip"), tags[0].ZipballURL)
37-
assert.Equal(t, path.Join(setting.AppSubURL, "/user2/repo1/archive/v1.1.tar.gz"), tags[0].TarballURL)
34+
assert.Equal(t, setting.AppURL+"api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d", tags[0].Commit.URL)
35+
assert.Equal(t, setting.AppURL+"user2/repo1/archive/v1.1.zip", tags[0].ZipballURL)
36+
assert.Equal(t, setting.AppURL+"user2/repo1/archive/v1.1.tar.gz", tags[0].TarballURL)
3837
}

models/repo_tag.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"code.gitea.io/gitea/modules/git"
99
)
1010

11-
// GetTagsByPath returns repo tags by it's path
11+
// GetTagsByPath returns repo tags by its path
1212
func GetTagsByPath(path string) ([]*git.Tag, error) {
1313
gitRepo, err := git.OpenRepository(path)
1414
if err != nil {

modules/git/repo_ref.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,19 @@ func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) {
3131
if err = refsIter.ForEach(func(ref *plumbing.Reference) error {
3232
if ref.Name() != plumbing.HEAD && !ref.Name().IsRemote() &&
3333
(pattern == "" || strings.HasPrefix(ref.Name().String(), pattern)) {
34+
refType := string(ObjectCommit)
35+
if ref.Name().IsTag() {
36+
// tags can be of type `commit` (lightweight) or `tag` (annotated)
37+
if tagType, _ := repo.GetTagType(SHA1(ref.Hash())); err == nil {
38+
refType = tagType
39+
}
40+
}
3441
r := &Reference{
3542
Name: ref.Name().String(),
3643
Object: SHA1(ref.Hash()),
37-
Type: string(ObjectCommit),
44+
Type: refType,
3845
repo: repo,
3946
}
40-
if ref.Name().IsTag() {
41-
r.Type = string(ObjectTag)
42-
}
4347
refs = append(refs, r)
4448
}
4549
return nil

modules/git/repo_tag.go

+134-13
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package git
77

88
import (
9+
"fmt"
910
"strings"
1011

1112
"github.com/mcuadros/go-version"
@@ -35,34 +36,78 @@ func (repo *Repository) CreateTag(name, revision string) error {
3536
return err
3637
}
3738

39+
// CreateAnnotatedTag create one annotated tag in the repository
40+
func (repo *Repository) CreateAnnotatedTag(name, message, revision string) error {
41+
_, err := NewCommand("tag", "-a", "-m", message, name, revision).RunInDir(repo.Path)
42+
return err
43+
}
44+
3845
func (repo *Repository) getTag(id SHA1) (*Tag, error) {
3946
t, ok := repo.tagCache.Get(id.String())
4047
if ok {
4148
log("Hit cache: %s", id)
42-
return t.(*Tag), nil
49+
tagClone := *t.(*Tag)
50+
return &tagClone, nil
4351
}
4452

45-
// Get tag type
46-
tp, err := NewCommand("cat-file", "-t", id.String()).RunInDir(repo.Path)
53+
// Get tag name
54+
name, err := repo.GetTagNameBySHA(id.String())
55+
if err != nil {
56+
return nil, err
57+
}
58+
59+
tp, err := repo.GetTagType(id)
4760
if err != nil {
4861
return nil, err
4962
}
50-
tp = strings.TrimSpace(tp)
5163

52-
// Tag is a commit.
64+
// Get the commit ID and tag ID (may be different for annotated tag) for the returned tag object
65+
commitIDStr, err := repo.GetTagCommitID(name)
66+
if err != nil {
67+
// every tag should have a commit ID so return all errors
68+
return nil, err
69+
}
70+
commitID, err := NewIDFromString(commitIDStr)
71+
if err != nil {
72+
return nil, err
73+
}
74+
75+
// tagID defaults to the commit ID as the tag ID and then tries to get a tag ID (only annotated tags)
76+
tagID := commitID
77+
if tagIDStr, err := repo.GetTagID(name); err != nil {
78+
// if the err is NotExist then we can ignore and just keep tagID as ID (is lightweight tag)
79+
// all other errors we return
80+
if !IsErrNotExist(err) {
81+
return nil, err
82+
}
83+
} else {
84+
tagID, err = NewIDFromString(tagIDStr)
85+
if err != nil {
86+
return nil, err
87+
}
88+
}
89+
90+
// If type is "commit, the tag is a lightweight tag
5391
if ObjectType(tp) == ObjectCommit {
92+
commit, err := repo.GetCommit(id.String())
93+
if err != nil {
94+
return nil, err
95+
}
5496
tag := &Tag{
55-
ID: id,
56-
Object: id,
57-
Type: string(ObjectCommit),
58-
repo: repo,
97+
Name: name,
98+
ID: tagID,
99+
Object: commitID,
100+
Type: string(ObjectCommit),
101+
Tagger: commit.Committer,
102+
Message: commit.Message(),
103+
repo: repo,
59104
}
60105

61106
repo.tagCache.Set(id.String(), tag)
62107
return tag, nil
63108
}
64109

65-
// Tag with message.
110+
// The tag is an annotated tag with a message.
66111
data, err := NewCommand("cat-file", "-p", id.String()).RunInDirBytes(repo.Path)
67112
if err != nil {
68113
return nil, err
@@ -73,16 +118,57 @@ func (repo *Repository) getTag(id SHA1) (*Tag, error) {
73118
return nil, err
74119
}
75120

121+
tag.Name = name
76122
tag.ID = id
77123
tag.repo = repo
124+
tag.Type = tp
78125

79126
repo.tagCache.Set(id.String(), tag)
80127
return tag, nil
81128
}
82129

130+
// GetTagNameBySHA returns the name of a tag from its tag object SHA or commit SHA
131+
func (repo *Repository) GetTagNameBySHA(sha string) (string, error) {
132+
if len(sha) < 5 {
133+
return "", fmt.Errorf("SHA is too short: %s", sha)
134+
}
135+
136+
stdout, err := NewCommand("show-ref", "--tags", "-d").RunInDir(repo.Path)
137+
if err != nil {
138+
return "", err
139+
}
140+
141+
tagRefs := strings.Split(stdout, "\n")
142+
for _, tagRef := range tagRefs {
143+
if len(strings.TrimSpace(tagRef)) > 0 {
144+
fields := strings.Fields(tagRef)
145+
if strings.HasPrefix(fields[0], sha) && strings.HasPrefix(fields[1], TagPrefix) {
146+
name := fields[1][len(TagPrefix):]
147+
// annotated tags show up twice, their name for commit ID is suffixed with ^{}
148+
name = strings.TrimSuffix(name, "^{}")
149+
return name, nil
150+
}
151+
}
152+
}
153+
return "", ErrNotExist{ID: sha}
154+
}
155+
156+
// GetTagID returns the object ID for a tag (annotated tags have both an object SHA AND a commit SHA)
157+
func (repo *Repository) GetTagID(name string) (string, error) {
158+
stdout, err := NewCommand("show-ref", name).RunInDir(repo.Path)
159+
if err != nil {
160+
return "", err
161+
}
162+
fields := strings.Fields(stdout)
163+
if len(fields) != 2 {
164+
return "", ErrNotExist{ID: name}
165+
}
166+
return fields[0], nil
167+
}
168+
83169
// GetTag returns a Git tag by given name.
84170
func (repo *Repository) GetTag(name string) (*Tag, error) {
85-
idStr, err := repo.GetTagCommitID(name)
171+
idStr, err := repo.GetTagID(name)
86172
if err != nil {
87173
return nil, err
88174
}
@@ -96,7 +182,6 @@ func (repo *Repository) GetTag(name string) (*Tag, error) {
96182
if err != nil {
97183
return nil, err
98184
}
99-
tag.Name = name
100185
return tag, nil
101186
}
102187

@@ -108,7 +193,7 @@ func (repo *Repository) GetTagInfos() ([]*Tag, error) {
108193
return nil, err
109194
}
110195

111-
tagNames := strings.Split(stdout, "\n")
196+
tagNames := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
112197
var tags = make([]*Tag, 0, len(tagNames))
113198
for _, tagName := range tagNames {
114199
tagName = strings.TrimSpace(tagName)
@@ -120,6 +205,7 @@ func (repo *Repository) GetTagInfos() ([]*Tag, error) {
120205
if err != nil {
121206
return nil, err
122207
}
208+
tag.Name = tagName
123209
tags = append(tags, tag)
124210
}
125211
sortTagsByTime(tags)
@@ -150,3 +236,38 @@ func (repo *Repository) GetTags() ([]string, error) {
150236

151237
return tagNames, nil
152238
}
239+
240+
// GetTagType gets the type of the tag, either commit (simple) or tag (annotated)
241+
func (repo *Repository) GetTagType(id SHA1) (string, error) {
242+
// Get tag type
243+
stdout, err := NewCommand("cat-file", "-t", id.String()).RunInDir(repo.Path)
244+
if err != nil {
245+
return "", err
246+
}
247+
if len(stdout) == 0 {
248+
return "", ErrNotExist{ID: id.String()}
249+
}
250+
return strings.TrimSpace(stdout), nil
251+
}
252+
253+
// GetAnnotatedTag returns a Git tag by its SHA, must be an annotated tag
254+
func (repo *Repository) GetAnnotatedTag(sha string) (*Tag, error) {
255+
id, err := NewIDFromString(sha)
256+
if err != nil {
257+
return nil, err
258+
}
259+
260+
// Tag type must be "tag" (annotated) and not a "commit" (lightweight) tag
261+
if tagType, err := repo.GetTagType(id); err != nil {
262+
return nil, err
263+
} else if ObjectType(tagType) != ObjectTag {
264+
// not an annotated tag
265+
return nil, ErrNotExist{ID: id.String()}
266+
}
267+
268+
tag, err := repo.getTag(id)
269+
if err != nil {
270+
return nil, err
271+
}
272+
return tag, nil
273+
}

0 commit comments

Comments
 (0)