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 <lauris@nix.lv>

* 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
This commit is contained in:
Richard Mahn 2019-06-08 10:31:11 -04:00 committed by Lauris BH
parent 23a2ee3510
commit 8de0b0a3f0
16 changed files with 551 additions and 85 deletions

View File

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

View File

@ -136,8 +136,8 @@ func TestAPIUpdateFile(t *testing.T) {
var fileResponse api.FileResponse var fileResponse api.FileResponse
DecodeJSON(t, resp, &fileResponse) DecodeJSON(t, resp, &fileResponse)
expectedSHA := "08bd14b2e2852529157324de9c226b3364e76136" expectedSHA := "08bd14b2e2852529157324de9c226b3364e76136"
expectedHTMLURL := fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/blob/new_branch/update/file%d.txt", fileID) expectedHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/blob/new_branch/update/file%d.txt", fileID)
expectedDownloadURL := fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/raw/branch/new_branch/update/file%d.txt", fileID) expectedDownloadURL := fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/new_branch/update/file%d.txt", fileID)
assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA) assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA)
assert.EqualValues(t, expectedHTMLURL, fileResponse.Content.HTMLURL) assert.EqualValues(t, expectedHTMLURL, fileResponse.Content.HTMLURL)
assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL) assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL)
@ -155,8 +155,8 @@ func TestAPIUpdateFile(t *testing.T) {
resp = session.MakeRequest(t, req, http.StatusOK) resp = session.MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &fileResponse) DecodeJSON(t, resp, &fileResponse)
expectedSHA = "08bd14b2e2852529157324de9c226b3364e76136" expectedSHA = "08bd14b2e2852529157324de9c226b3364e76136"
expectedHTMLURL = fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/blob/master/rename/update/file%d.txt", fileID) expectedHTMLURL = fmt.Sprintf(setting.AppURL+"user2/repo1/blob/master/rename/update/file%d.txt", fileID)
expectedDownloadURL = fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/raw/branch/master/rename/update/file%d.txt", fileID) expectedDownloadURL = fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/master/rename/update/file%d.txt", fileID)
assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA) assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA)
assert.EqualValues(t, expectedHTMLURL, fileResponse.Content.HTMLURL) assert.EqualValues(t, expectedHTMLURL, fileResponse.Content.HTMLURL)
assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL) assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL)

View File

@ -0,0 +1,59 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package integrations
import (
"net/http"
"testing"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
)
func TestAPIGitTags(t *testing.T) {
prepareTestEnv(t)
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
// Login as User2.
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session)
// Set up git config for the tagger
git.NewCommand("config", "user.name", user.Name).RunInDir(repo.RepoPath())
git.NewCommand("config", "user.email", user.Email).RunInDir(repo.RepoPath())
gitRepo, _ := git.OpenRepository(repo.RepoPath())
commit, _ := gitRepo.GetBranchCommit("master")
lTagName := "lightweightTag"
gitRepo.CreateTag(lTagName, commit.ID.String())
aTagName := "annotatedTag"
aTagMessage := "my annotated message"
gitRepo.CreateAnnotatedTag(aTagName, aTagMessage, commit.ID.String())
aTag, _ := gitRepo.GetTag(aTagName)
// SHOULD work for annotated tags
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/tags/%s?token=%s", user.Name, repo.Name, aTag.ID.String(), token)
res := session.MakeRequest(t, req, http.StatusOK)
var tag *api.AnnotatedTag
DecodeJSON(t, res, &tag)
assert.Equal(t, aTagName, tag.Tag)
assert.Equal(t, aTag.ID.String(), tag.SHA)
assert.Equal(t, commit.ID.String(), tag.Object.SHA)
assert.Equal(t, aTagMessage, tag.Message)
assert.Equal(t, user.Name, tag.Tagger.Name)
assert.Equal(t, user.Email, tag.Tagger.Email)
assert.Equal(t, util.URLJoin(repo.APIURL(), "git/tags", aTag.ID.String()), tag.URL)
// Should NOT work for lightweight tags
badReq := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/tags/%s?token=%s", user.Name, repo.Name, commit.ID.String(), token)
session.MakeRequest(t, badReq, http.StatusBadRequest)
}

View File

@ -6,7 +6,6 @@ package integrations
import ( import (
"net/http" "net/http"
"path"
"testing" "testing"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
@ -32,7 +31,7 @@ func TestAPIReposGetTags(t *testing.T) {
assert.EqualValues(t, 1, len(tags)) assert.EqualValues(t, 1, len(tags))
assert.Equal(t, "v1.1", tags[0].Name) assert.Equal(t, "v1.1", tags[0].Name)
assert.Equal(t, "65f1bf27bc3bf70f64657658635e66094edbcb4d", tags[0].Commit.SHA) assert.Equal(t, "65f1bf27bc3bf70f64657658635e66094edbcb4d", tags[0].Commit.SHA)
assert.Equal(t, path.Join(setting.AppSubURL, "/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d"), tags[0].Commit.URL) assert.Equal(t, setting.AppURL+"api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d", tags[0].Commit.URL)
assert.Equal(t, path.Join(setting.AppSubURL, "/user2/repo1/archive/v1.1.zip"), tags[0].ZipballURL) assert.Equal(t, setting.AppURL+"user2/repo1/archive/v1.1.zip", tags[0].ZipballURL)
assert.Equal(t, path.Join(setting.AppSubURL, "/user2/repo1/archive/v1.1.tar.gz"), tags[0].TarballURL) assert.Equal(t, setting.AppURL+"user2/repo1/archive/v1.1.tar.gz", tags[0].TarballURL)
} }

View File

@ -8,7 +8,7 @@ import (
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
) )
// GetTagsByPath returns repo tags by it's path // GetTagsByPath returns repo tags by its path
func GetTagsByPath(path string) ([]*git.Tag, error) { func GetTagsByPath(path string) ([]*git.Tag, error) {
gitRepo, err := git.OpenRepository(path) gitRepo, err := git.OpenRepository(path)
if err != nil { if err != nil {

View File

@ -31,15 +31,19 @@ func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) {
if err = refsIter.ForEach(func(ref *plumbing.Reference) error { if err = refsIter.ForEach(func(ref *plumbing.Reference) error {
if ref.Name() != plumbing.HEAD && !ref.Name().IsRemote() && if ref.Name() != plumbing.HEAD && !ref.Name().IsRemote() &&
(pattern == "" || strings.HasPrefix(ref.Name().String(), pattern)) { (pattern == "" || strings.HasPrefix(ref.Name().String(), pattern)) {
refType := string(ObjectCommit)
if ref.Name().IsTag() {
// tags can be of type `commit` (lightweight) or `tag` (annotated)
if tagType, _ := repo.GetTagType(SHA1(ref.Hash())); err == nil {
refType = tagType
}
}
r := &Reference{ r := &Reference{
Name: ref.Name().String(), Name: ref.Name().String(),
Object: SHA1(ref.Hash()), Object: SHA1(ref.Hash()),
Type: string(ObjectCommit), Type: refType,
repo: repo, repo: repo,
} }
if ref.Name().IsTag() {
r.Type = string(ObjectTag)
}
refs = append(refs, r) refs = append(refs, r)
} }
return nil return nil

View File

@ -6,6 +6,7 @@
package git package git
import ( import (
"fmt"
"strings" "strings"
"github.com/mcuadros/go-version" "github.com/mcuadros/go-version"
@ -35,26 +36,70 @@ func (repo *Repository) CreateTag(name, revision string) error {
return err return err
} }
// CreateAnnotatedTag create one annotated tag in the repository
func (repo *Repository) CreateAnnotatedTag(name, message, revision string) error {
_, err := NewCommand("tag", "-a", "-m", message, name, revision).RunInDir(repo.Path)
return err
}
func (repo *Repository) getTag(id SHA1) (*Tag, error) { func (repo *Repository) getTag(id SHA1) (*Tag, error) {
t, ok := repo.tagCache.Get(id.String()) t, ok := repo.tagCache.Get(id.String())
if ok { if ok {
log("Hit cache: %s", id) log("Hit cache: %s", id)
return t.(*Tag), nil tagClone := *t.(*Tag)
return &tagClone, nil
} }
// Get tag type // Get tag name
tp, err := NewCommand("cat-file", "-t", id.String()).RunInDir(repo.Path) name, err := repo.GetTagNameBySHA(id.String())
if err != nil { if err != nil {
return nil, err return nil, err
} }
tp = strings.TrimSpace(tp)
// Tag is a commit. tp, err := repo.GetTagType(id)
if err != nil {
return nil, err
}
// Get the commit ID and tag ID (may be different for annotated tag) for the returned tag object
commitIDStr, err := repo.GetTagCommitID(name)
if err != nil {
// every tag should have a commit ID so return all errors
return nil, err
}
commitID, err := NewIDFromString(commitIDStr)
if err != nil {
return nil, err
}
// tagID defaults to the commit ID as the tag ID and then tries to get a tag ID (only annotated tags)
tagID := commitID
if tagIDStr, err := repo.GetTagID(name); err != nil {
// if the err is NotExist then we can ignore and just keep tagID as ID (is lightweight tag)
// all other errors we return
if !IsErrNotExist(err) {
return nil, err
}
} else {
tagID, err = NewIDFromString(tagIDStr)
if err != nil {
return nil, err
}
}
// If type is "commit, the tag is a lightweight tag
if ObjectType(tp) == ObjectCommit { if ObjectType(tp) == ObjectCommit {
commit, err := repo.GetCommit(id.String())
if err != nil {
return nil, err
}
tag := &Tag{ tag := &Tag{
ID: id, Name: name,
Object: id, ID: tagID,
Object: commitID,
Type: string(ObjectCommit), Type: string(ObjectCommit),
Tagger: commit.Committer,
Message: commit.Message(),
repo: repo, repo: repo,
} }
@ -62,7 +107,7 @@ func (repo *Repository) getTag(id SHA1) (*Tag, error) {
return tag, nil return tag, nil
} }
// Tag with message. // The tag is an annotated tag with a message.
data, err := NewCommand("cat-file", "-p", id.String()).RunInDirBytes(repo.Path) data, err := NewCommand("cat-file", "-p", id.String()).RunInDirBytes(repo.Path)
if err != nil { if err != nil {
return nil, err return nil, err
@ -73,16 +118,57 @@ func (repo *Repository) getTag(id SHA1) (*Tag, error) {
return nil, err return nil, err
} }
tag.Name = name
tag.ID = id tag.ID = id
tag.repo = repo tag.repo = repo
tag.Type = tp
repo.tagCache.Set(id.String(), tag) repo.tagCache.Set(id.String(), tag)
return tag, nil return tag, nil
} }
// GetTagNameBySHA returns the name of a tag from its tag object SHA or commit SHA
func (repo *Repository) GetTagNameBySHA(sha string) (string, error) {
if len(sha) < 5 {
return "", fmt.Errorf("SHA is too short: %s", sha)
}
stdout, err := NewCommand("show-ref", "--tags", "-d").RunInDir(repo.Path)
if err != nil {
return "", err
}
tagRefs := strings.Split(stdout, "\n")
for _, tagRef := range tagRefs {
if len(strings.TrimSpace(tagRef)) > 0 {
fields := strings.Fields(tagRef)
if strings.HasPrefix(fields[0], sha) && strings.HasPrefix(fields[1], TagPrefix) {
name := fields[1][len(TagPrefix):]
// annotated tags show up twice, their name for commit ID is suffixed with ^{}
name = strings.TrimSuffix(name, "^{}")
return name, nil
}
}
}
return "", ErrNotExist{ID: sha}
}
// GetTagID returns the object ID for a tag (annotated tags have both an object SHA AND a commit SHA)
func (repo *Repository) GetTagID(name string) (string, error) {
stdout, err := NewCommand("show-ref", name).RunInDir(repo.Path)
if err != nil {
return "", err
}
fields := strings.Fields(stdout)
if len(fields) != 2 {
return "", ErrNotExist{ID: name}
}
return fields[0], nil
}
// GetTag returns a Git tag by given name. // GetTag returns a Git tag by given name.
func (repo *Repository) GetTag(name string) (*Tag, error) { func (repo *Repository) GetTag(name string) (*Tag, error) {
idStr, err := repo.GetTagCommitID(name) idStr, err := repo.GetTagID(name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -96,7 +182,6 @@ func (repo *Repository) GetTag(name string) (*Tag, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
tag.Name = name
return tag, nil return tag, nil
} }
@ -108,7 +193,7 @@ func (repo *Repository) GetTagInfos() ([]*Tag, error) {
return nil, err return nil, err
} }
tagNames := strings.Split(stdout, "\n") tagNames := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
var tags = make([]*Tag, 0, len(tagNames)) var tags = make([]*Tag, 0, len(tagNames))
for _, tagName := range tagNames { for _, tagName := range tagNames {
tagName = strings.TrimSpace(tagName) tagName = strings.TrimSpace(tagName)
@ -120,6 +205,7 @@ func (repo *Repository) GetTagInfos() ([]*Tag, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
tag.Name = tagName
tags = append(tags, tag) tags = append(tags, tag)
} }
sortTagsByTime(tags) sortTagsByTime(tags)
@ -150,3 +236,38 @@ func (repo *Repository) GetTags() ([]string, error) {
return tagNames, nil return tagNames, nil
} }
// GetTagType gets the type of the tag, either commit (simple) or tag (annotated)
func (repo *Repository) GetTagType(id SHA1) (string, error) {
// Get tag type
stdout, err := NewCommand("cat-file", "-t", id.String()).RunInDir(repo.Path)
if err != nil {
return "", err
}
if len(stdout) == 0 {
return "", ErrNotExist{ID: id.String()}
}
return strings.TrimSpace(stdout), nil
}
// GetAnnotatedTag returns a Git tag by its SHA, must be an annotated tag
func (repo *Repository) GetAnnotatedTag(sha string) (*Tag, error) {
id, err := NewIDFromString(sha)
if err != nil {
return nil, err
}
// Tag type must be "tag" (annotated) and not a "commit" (lightweight) tag
if tagType, err := repo.GetTagType(id); err != nil {
return nil, err
} else if ObjectType(tagType) != ObjectTag {
// not an annotated tag
return nil, ErrNotExist{ID: id.String()}
}
tag, err := repo.getTag(id)
if err != nil {
return nil, err
}
return tag, nil
}

View File

@ -21,8 +21,8 @@ func TestRepository_GetTags(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, tags, 1) assert.Len(t, tags, 1)
assert.EqualValues(t, "test", tags[0].Name) assert.EqualValues(t, "test", tags[0].Name)
assert.EqualValues(t, "37991dec2c8e592043f47155ce4808d4580f9123", tags[0].ID.String()) assert.EqualValues(t, "3ad28a9149a2864384548f3d17ed7f38014c9e8a", tags[0].ID.String())
assert.EqualValues(t, "commit", tags[0].Type) assert.EqualValues(t, "tag", tags[0].Type)
} }
func TestRepository_GetTag(t *testing.T) { func TestRepository_GetTag(t *testing.T) {
@ -35,10 +35,78 @@ func TestRepository_GetTag(t *testing.T) {
bareRepo1, err := OpenRepository(clonedPath) bareRepo1, err := OpenRepository(clonedPath)
assert.NoError(t, err) assert.NoError(t, err)
tag, err := bareRepo1.GetTag("test") lTagCommitID := "6fbd69e9823458e6c4a2fc5c0f6bc022b2f2acd1"
lTagName := "lightweightTag"
bareRepo1.CreateTag(lTagName, lTagCommitID)
aTagCommitID := "8006ff9adbf0cb94da7dad9e537e53817f9fa5c0"
aTagName := "annotatedTag"
aTagMessage := "my annotated message"
bareRepo1.CreateAnnotatedTag(aTagName, aTagMessage, aTagCommitID)
aTagID, _ := bareRepo1.GetTagID(aTagName)
lTag, err := bareRepo1.GetTag(lTagName)
lTag.repo = nil
assert.NoError(t, err)
assert.NotNil(t, lTag)
assert.EqualValues(t, lTagName, lTag.Name)
assert.EqualValues(t, lTagCommitID, lTag.ID.String())
assert.EqualValues(t, lTagCommitID, lTag.Object.String())
assert.EqualValues(t, "commit", lTag.Type)
aTag, err := bareRepo1.GetTag(aTagName)
assert.NoError(t, err)
assert.NotNil(t, aTag)
assert.EqualValues(t, aTagName, aTag.Name)
assert.EqualValues(t, aTagID, aTag.ID.String())
assert.NotEqual(t, aTagID, aTag.Object.String())
assert.EqualValues(t, aTagCommitID, aTag.Object.String())
assert.EqualValues(t, "tag", aTag.Type)
}
func TestRepository_GetAnnotatedTag(t *testing.T) {
bareRepo1Path := filepath.Join(testReposDir, "repo1_bare")
clonedPath, err := cloneRepo(bareRepo1Path, testReposDir, "repo1_TestRepository_GetTag")
assert.NoError(t, err)
defer os.RemoveAll(clonedPath)
bareRepo1, err := OpenRepository(clonedPath)
assert.NoError(t, err)
lTagCommitID := "6fbd69e9823458e6c4a2fc5c0f6bc022b2f2acd1"
lTagName := "lightweightTag"
bareRepo1.CreateTag(lTagName, lTagCommitID)
aTagCommitID := "8006ff9adbf0cb94da7dad9e537e53817f9fa5c0"
aTagName := "annotatedTag"
aTagMessage := "my annotated message"
bareRepo1.CreateAnnotatedTag(aTagName, aTagMessage, aTagCommitID)
aTagID, _ := bareRepo1.GetTagID(aTagName)
// Try an annotated tag
tag, err := bareRepo1.GetAnnotatedTag(aTagID)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotNil(t, tag) assert.NotNil(t, tag)
assert.EqualValues(t, "test", tag.Name) assert.EqualValues(t, aTagName, tag.Name)
assert.EqualValues(t, "37991dec2c8e592043f47155ce4808d4580f9123", tag.ID.String()) assert.EqualValues(t, aTagID, tag.ID.String())
assert.EqualValues(t, "commit", tag.Type) assert.EqualValues(t, "tag", tag.Type)
// Annotated tag's Commit ID should fail
tag2, err := bareRepo1.GetAnnotatedTag(aTagCommitID)
assert.Error(t, err)
assert.True(t, IsErrNotExist(err))
assert.Nil(t, tag2)
// Annotated tag's name should fail
tag3, err := bareRepo1.GetAnnotatedTag(aTagName)
assert.Error(t, err)
assert.Errorf(t, err, "Length must be 40: %d", len(aTagName))
assert.Nil(t, tag3)
// Lightweight Tag should fail
tag4, err := bareRepo1.GetAnnotatedTag(lTagCommitID)
assert.Error(t, err)
assert.True(t, IsErrNotExist(err))
assert.Nil(t, tag4)
} }

View File

@ -7,6 +7,7 @@ package git
import ( import (
"bytes" "bytes"
"sort" "sort"
"strings"
) )
// Tag represents a Git tag. // Tag represents a Git tag.
@ -59,7 +60,7 @@ l:
} }
nextline += eol + 1 nextline += eol + 1
case eol == 0: case eol == 0:
tag.Message = string(data[nextline+1:]) tag.Message = strings.TrimRight(string(data[nextline+1:]), "\n")
break l break l
default: default:
break l break l

View File

@ -7,10 +7,26 @@ package structs
// Tag represents a repository tag // Tag represents a repository tag
type Tag struct { type Tag struct {
Name string `json:"name"` Name string `json:"name"`
Commit struct { ID string `json:"id"`
SHA string `json:"sha"` Commit *CommitMeta `json:"commit"`
URL string `json:"url"`
} `json:"commit"`
ZipballURL string `json:"zipball_url"` ZipballURL string `json:"zipball_url"`
TarballURL string `json:"tarball_url"` TarballURL string `json:"tarball_url"`
} }
// AnnotatedTag represents an annotated tag
type AnnotatedTag struct {
Tag string `json:"tag"`
SHA string `json:"sha"`
URL string `json:"url"`
Message string `json:"message"`
Tagger *CommitUser `json:"tagger"`
Object *AnnotatedTagObject `json:"object"`
Verification *PayloadCommitVerification `json:"verification"`
}
// AnnotatedTagObject contains meta information of the tag object
type AnnotatedTagObject struct {
Type string `json:"type"`
URL string `json:"url"`
SHA string `json:"sha"`
}

View File

@ -751,6 +751,7 @@ func RegisterRoutes(m *macaron.Macaron) {
Post(reqToken(), bind(api.CreateStatusOption{}), repo.NewCommitStatus) Post(reqToken(), bind(api.CreateStatusOption{}), repo.NewCommitStatus)
}, reqRepoReader(models.UnitTypeCode)) }, reqRepoReader(models.UnitTypeCode))
m.Group("/commits/:ref", func() { m.Group("/commits/:ref", func() {
// TODO: Add m.Get("") for single commit (https://developer.github.com/v3/repos/commits/#get-a-single-commit)
m.Get("/status", repo.GetCombinedCommitStatusByRef) m.Get("/status", repo.GetCombinedCommitStatusByRef)
m.Get("/statuses", repo.GetCommitStatusesByRef) m.Get("/statuses", repo.GetCommitStatusesByRef)
}, reqRepoReader(models.UnitTypeCode)) }, reqRepoReader(models.UnitTypeCode))
@ -762,6 +763,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("/refs/*", repo.GetGitRefs) m.Get("/refs/*", repo.GetGitRefs)
m.Get("/trees/:sha", context.RepoRef(), repo.GetTree) m.Get("/trees/:sha", context.RepoRef(), repo.GetTree)
m.Get("/blobs/:sha", context.RepoRef(), repo.GetBlob) m.Get("/blobs/:sha", context.RepoRef(), repo.GetBlob)
m.Get("/tags/:sha", context.RepoRef(), repo.GetTag)
}, reqRepoReader(models.UnitTypeCode)) }, reqRepoReader(models.UnitTypeCode))
m.Group("/contents", func() { m.Group("/contents", func() {
m.Get("/*", repo.GetFileContents) m.Get("/*", repo.GetFileContents)

View File

@ -6,6 +6,7 @@ package convert
import ( import (
"fmt" "fmt"
"time"
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
@ -26,7 +27,7 @@ func ToEmail(email *models.EmailAddress) *api.Email {
} }
} }
// ToBranch convert a commit and branch to an api.Branch // ToBranch convert a git.Commit and git.Branch to an api.Branch
func ToBranch(repo *models.Repository, b *git.Branch, c *git.Commit) *api.Branch { func ToBranch(repo *models.Repository, b *git.Branch, c *git.Commit) *api.Branch {
return &api.Branch{ return &api.Branch{
Name: b.Name, Name: b.Name,
@ -34,23 +35,18 @@ func ToBranch(repo *models.Repository, b *git.Branch, c *git.Commit) *api.Branch
} }
} }
// ToTag convert a tag to an api.Tag // ToTag convert a git.Tag to an api.Tag
func ToTag(repo *models.Repository, t *git.Tag) *api.Tag { func ToTag(repo *models.Repository, t *git.Tag) *api.Tag {
return &api.Tag{ return &api.Tag{
Name: t.Name, Name: t.Name,
Commit: struct { ID: t.ID.String(),
SHA string `json:"sha"` Commit: ToCommitMeta(repo, t),
URL string `json:"url"` ZipballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".zip"),
}{ TarballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".tar.gz"),
SHA: t.ID.String(),
URL: util.URLJoin(repo.Link(), "commit", t.ID.String()),
},
ZipballURL: util.URLJoin(repo.Link(), "archive", t.Name+".zip"),
TarballURL: util.URLJoin(repo.Link(), "archive", t.Name+".tar.gz"),
} }
} }
// ToCommit convert a commit to api.PayloadCommit // ToCommit convert a git.Commit to api.PayloadCommit
func ToCommit(repo *models.Repository, c *git.Commit) *api.PayloadCommit { func ToCommit(repo *models.Repository, c *git.Commit) *api.PayloadCommit {
authorUsername := "" authorUsername := ""
if author, err := models.GetUserByEmail(c.Author.Email); err == nil { if author, err := models.GetUserByEmail(c.Author.Email); err == nil {
@ -66,17 +62,10 @@ func ToCommit(repo *models.Repository, c *git.Commit) *api.PayloadCommit {
log.Error("GetUserByEmail: %v", err) log.Error("GetUserByEmail: %v", err)
} }
verif := models.ParseCommitWithSignature(c)
var signature, payload string
if c.Signature != nil {
signature = c.Signature.Signature
payload = c.Signature.Payload
}
return &api.PayloadCommit{ return &api.PayloadCommit{
ID: c.ID.String(), ID: c.ID.String(),
Message: c.Message(), Message: c.Message(),
URL: util.URLJoin(repo.Link(), "commit", c.ID.String()), URL: util.URLJoin(repo.HTMLURL(), "commit", c.ID.String()),
Author: &api.PayloadUser{ Author: &api.PayloadUser{
Name: c.Author.Name, Name: c.Author.Name,
Email: c.Author.Email, Email: c.Author.Email,
@ -88,12 +77,23 @@ func ToCommit(repo *models.Repository, c *git.Commit) *api.PayloadCommit {
UserName: committerUsername, UserName: committerUsername,
}, },
Timestamp: c.Author.When, Timestamp: c.Author.When,
Verification: &api.PayloadCommitVerification{ Verification: ToVerification(c),
}
}
// ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification
func ToVerification(c *git.Commit) *api.PayloadCommitVerification {
verif := models.ParseCommitWithSignature(c)
var signature, payload string
if c.Signature != nil {
signature = c.Signature.Signature
payload = c.Signature.Payload
}
return &api.PayloadCommitVerification{
Verified: verif.Verified, Verified: verif.Verified,
Reason: verif.Reason, Reason: verif.Reason,
Signature: signature, Signature: signature,
Payload: payload, Payload: payload,
},
} }
} }
@ -242,3 +242,45 @@ func ToUser(user *models.User, signed, admin bool) *api.User {
} }
return result return result
} }
// ToAnnotatedTag convert git.Tag to api.AnnotatedTag
func ToAnnotatedTag(repo *models.Repository, t *git.Tag, c *git.Commit) *api.AnnotatedTag {
return &api.AnnotatedTag{
Tag: t.Name,
SHA: t.ID.String(),
Object: ToAnnotatedTagObject(repo, c),
Message: t.Message,
URL: util.URLJoin(repo.APIURL(), "git/tags", t.ID.String()),
Tagger: ToCommitUser(t.Tagger),
Verification: ToVerification(c),
}
}
// ToAnnotatedTagObject convert a git.Commit to an api.AnnotatedTagObject
func ToAnnotatedTagObject(repo *models.Repository, commit *git.Commit) *api.AnnotatedTagObject {
return &api.AnnotatedTagObject{
SHA: commit.ID.String(),
Type: string(git.ObjectCommit),
URL: util.URLJoin(repo.APIURL(), "git/commits", commit.ID.String()),
}
}
// ToCommitUser convert a git.Signature to an api.CommitUser
func ToCommitUser(sig *git.Signature) *api.CommitUser {
return &api.CommitUser{
Identity: api.Identity{
Name: sig.Name,
Email: sig.Email,
},
Date: sig.When.UTC().Format(time.RFC3339),
}
}
// ToCommitMeta convert a git.Tag to an api.CommitMeta
func ToCommitMeta(repo *models.Repository, tag *git.Tag) *api.CommitMeta {
return &api.CommitMeta{
SHA: tag.ID.String(),
// TODO: Add the /commits API endpoint and use it here (https://developer.github.com/v3/repos/commits/#get-a-single-commit)
URL: util.URLJoin(repo.APIURL(), "git/commits", tag.ID.String()),
}
}

View File

@ -100,8 +100,7 @@ func getGitRefsInternal(ctx *context.APIContext, filter string) {
Object: &api.GitObject{ Object: &api.GitObject{
SHA: refs[i].Object.String(), SHA: refs[i].Object.String(),
Type: refs[i].Type, Type: refs[i].Type,
// TODO: Add commit/tag info URL URL: ctx.Repo.Repository.APIURL() + "/git/" + refs[i].Type + "s/" + refs[i].Object.String(),
//URL: ctx.Repo.Repository.APIURL() + "/git/" + refs[i].Type + "s/" + refs[i].Object.String(),
}, },
} }
} }

View File

@ -7,6 +7,7 @@ package repo
import ( import (
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/routers/api/v1/convert" "code.gitea.io/gitea/routers/api/v1/convert"
"net/http"
api "code.gitea.io/gitea/modules/structs" api "code.gitea.io/gitea/modules/structs"
) )
@ -45,3 +46,47 @@ func ListTags(ctx *context.APIContext) {
ctx.JSON(200, &apiTags) ctx.JSON(200, &apiTags)
} }
// GetTag get the tag of a repository.
func GetTag(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/git/tags/{sha} repository GetTag
// ---
// summary: Gets the tag of a repository.
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: sha
// in: path
// description: sha of the tag
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/AnnotatedTag"
sha := ctx.Params("sha")
if len(sha) == 0 {
ctx.Error(http.StatusBadRequest, "", "SHA not provided")
return
}
if tag, err := ctx.Repo.GitRepo.GetAnnotatedTag(sha); err != nil {
ctx.Error(http.StatusBadRequest, "GetTag", err)
} else {
commit, err := tag.Commit()
if err != nil {
ctx.Error(http.StatusBadRequest, "GetTag", err)
}
ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx.Repo.Repository, tag, commit))
}
}

View File

@ -38,11 +38,25 @@ type swaggerResponseBranchList struct {
// TagList // TagList
// swagger:response TagList // swagger:response TagList
type swaggerReponseTagList struct { type swaggerResponseTagList struct {
// in:body // in:body
Body []api.Tag `json:"body"` Body []api.Tag `json:"body"`
} }
// Tag
// swagger:response Tag
type swaggerResponseTag struct {
// in:body
Body api.Tag `json:"body"`
}
// AnnotatedTag
// swagger:response AnnotatedTag
type swaggerResponseAnnotatedTag struct {
// in:body
Body api.AnnotatedTag `json:"body"`
}
// Reference // Reference
// swagger:response Reference // swagger:response Reference
type swaggerResponseReference struct { type swaggerResponseReference struct {

View File

@ -2036,6 +2036,46 @@
} }
} }
}, },
"/repos/{owner}/{repo}/git/tags/{sha}": {
"get": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Gets the tag of a repository.",
"operationId": "GetTag",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "string",
"description": "sha of the tag",
"name": "sha",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/AnnotatedTag"
}
}
}
},
"/repos/{owner}/{repo}/git/trees/{sha}": { "/repos/{owner}/{repo}/git/trees/{sha}": {
"get": { "get": {
"produces": [ "produces": [
@ -6762,6 +6802,57 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "x-go-package": "code.gitea.io/gitea/modules/structs"
}, },
"AnnotatedTag": {
"description": "AnnotatedTag represents an annotated tag",
"type": "object",
"properties": {
"message": {
"type": "string",
"x-go-name": "Message"
},
"object": {
"$ref": "#/definitions/AnnotatedTagObject"
},
"sha": {
"type": "string",
"x-go-name": "SHA"
},
"tag": {
"type": "string",
"x-go-name": "Tag"
},
"tagger": {
"$ref": "#/definitions/CommitUser"
},
"url": {
"type": "string",
"x-go-name": "URL"
},
"verification": {
"$ref": "#/definitions/PayloadCommitVerification"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"AnnotatedTagObject": {
"description": "AnnotatedTagObject contains meta information of the tag object",
"type": "object",
"properties": {
"sha": {
"type": "string",
"x-go-name": "SHA"
},
"type": {
"type": "string",
"x-go-name": "Type"
},
"url": {
"type": "string",
"x-go-name": "URL"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"Attachment": { "Attachment": {
"description": "Attachment a generic attachment", "description": "Attachment a generic attachment",
"type": "object", "type": "object",
@ -9458,18 +9549,11 @@
"type": "object", "type": "object",
"properties": { "properties": {
"commit": { "commit": {
"type": "object", "$ref": "#/definitions/CommitMeta"
"properties": {
"sha": {
"type": "string",
"x-go-name": "SHA"
}, },
"url": { "id": {
"type": "string", "type": "string",
"x-go-name": "URL" "x-go-name": "ID"
}
},
"x-go-name": "Commit"
}, },
"name": { "name": {
"type": "string", "type": "string",
@ -9735,6 +9819,12 @@
"AccessTokenList": { "AccessTokenList": {
"description": "AccessTokenList represents a list of API access token." "description": "AccessTokenList represents a list of API access token."
}, },
"AnnotatedTag": {
"description": "AnnotatedTag",
"schema": {
"$ref": "#/definitions/AnnotatedTag"
}
},
"Attachment": { "Attachment": {
"description": "Attachment", "description": "Attachment",
"schema": { "schema": {
@ -10056,6 +10146,12 @@
} }
} }
}, },
"Tag": {
"description": "Tag",
"schema": {
"$ref": "#/definitions/Tag"
}
},
"TagList": { "TagList": {
"description": "TagList", "description": "TagList",
"schema": { "schema": {