diff --git a/integrations/api_repo_file_create_test.go b/integrations/api_repo_file_create_test.go
index 28097179a0..3bb2ecb812 100644
--- a/integrations/api_repo_file_create_test.go
+++ b/integrations/api_repo_file_create_test.go
@@ -146,8 +146,8 @@ func TestAPICreateFile(t *testing.T) {
 		var fileResponse api.FileResponse
 		DecodeJSON(t, resp, &fileResponse)
 		expectedSHA := "a635aa942442ddfdba07468cf9661c08fbdf0ebf"
-		expectedHTMLURL := fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/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)
+		expectedHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/blob/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, expectedHTMLURL, fileResponse.Content.HTMLURL)
 		assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL)
diff --git a/integrations/api_repo_file_update_test.go b/integrations/api_repo_file_update_test.go
index 37438339bb..eab7090df6 100644
--- a/integrations/api_repo_file_update_test.go
+++ b/integrations/api_repo_file_update_test.go
@@ -136,8 +136,8 @@ func TestAPIUpdateFile(t *testing.T) {
 		var fileResponse api.FileResponse
 		DecodeJSON(t, resp, &fileResponse)
 		expectedSHA := "08bd14b2e2852529157324de9c226b3364e76136"
-		expectedHTMLURL := fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/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)
+		expectedHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/blob/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, expectedHTMLURL, fileResponse.Content.HTMLURL)
 		assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL)
@@ -155,8 +155,8 @@ func TestAPIUpdateFile(t *testing.T) {
 		resp = session.MakeRequest(t, req, http.StatusOK)
 		DecodeJSON(t, resp, &fileResponse)
 		expectedSHA = "08bd14b2e2852529157324de9c226b3364e76136"
-		expectedHTMLURL = fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/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)
+		expectedHTMLURL = fmt.Sprintf(setting.AppURL+"user2/repo1/blob/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, expectedHTMLURL, fileResponse.Content.HTMLURL)
 		assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL)
diff --git a/integrations/api_repo_git_tags_test.go b/integrations/api_repo_git_tags_test.go
new file mode 100644
index 0000000000..ae519249e0
--- /dev/null
+++ b/integrations/api_repo_git_tags_test.go
@@ -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)
+}
diff --git a/integrations/api_repo_tags_test.go b/integrations/api_repo_tags_test.go
index 13b446fb96..252037bd4a 100644
--- a/integrations/api_repo_tags_test.go
+++ b/integrations/api_repo_tags_test.go
@@ -6,7 +6,6 @@ package integrations
 
 import (
 	"net/http"
-	"path"
 	"testing"
 
 	"code.gitea.io/gitea/models"
@@ -32,7 +31,7 @@ func TestAPIReposGetTags(t *testing.T) {
 	assert.EqualValues(t, 1, len(tags))
 	assert.Equal(t, "v1.1", tags[0].Name)
 	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, path.Join(setting.AppSubURL, "/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+"api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d", tags[0].Commit.URL)
+	assert.Equal(t, setting.AppURL+"user2/repo1/archive/v1.1.zip", tags[0].ZipballURL)
+	assert.Equal(t, setting.AppURL+"user2/repo1/archive/v1.1.tar.gz", tags[0].TarballURL)
 }
diff --git a/models/repo_tag.go b/models/repo_tag.go
index fa3f19bb29..3864b7a12a 100644
--- a/models/repo_tag.go
+++ b/models/repo_tag.go
@@ -8,7 +8,7 @@ import (
 	"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) {
 	gitRepo, err := git.OpenRepository(path)
 	if err != nil {
diff --git a/modules/git/repo_ref.go b/modules/git/repo_ref.go
index e1ab46e090..95a6c2ae69 100644
--- a/modules/git/repo_ref.go
+++ b/modules/git/repo_ref.go
@@ -31,15 +31,19 @@ func (repo *Repository) GetRefsFiltered(pattern string) ([]*Reference, error) {
 	if err = refsIter.ForEach(func(ref *plumbing.Reference) error {
 		if ref.Name() != plumbing.HEAD && !ref.Name().IsRemote() &&
 			(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{
 				Name:   ref.Name().String(),
 				Object: SHA1(ref.Hash()),
-				Type:   string(ObjectCommit),
+				Type:   refType,
 				repo:   repo,
 			}
-			if ref.Name().IsTag() {
-				r.Type = string(ObjectTag)
-			}
 			refs = append(refs, r)
 		}
 		return nil
diff --git a/modules/git/repo_tag.go b/modules/git/repo_tag.go
index 8c72528933..08d66262c1 100644
--- a/modules/git/repo_tag.go
+++ b/modules/git/repo_tag.go
@@ -6,6 +6,7 @@
 package git
 
 import (
+	"fmt"
 	"strings"
 
 	"github.com/mcuadros/go-version"
@@ -35,34 +36,78 @@ func (repo *Repository) CreateTag(name, revision string) error {
 	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) {
 	t, ok := repo.tagCache.Get(id.String())
 	if ok {
 		log("Hit cache: %s", id)
-		return t.(*Tag), nil
+		tagClone := *t.(*Tag)
+		return &tagClone, nil
 	}
 
-	// Get tag type
-	tp, err := NewCommand("cat-file", "-t", id.String()).RunInDir(repo.Path)
+	// Get tag name
+	name, err := repo.GetTagNameBySHA(id.String())
+	if err != nil {
+		return nil, err
+	}
+
+	tp, err := repo.GetTagType(id)
 	if err != nil {
 		return nil, err
 	}
-	tp = strings.TrimSpace(tp)
 
-	// Tag is a commit.
+	// 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 {
+		commit, err := repo.GetCommit(id.String())
+		if err != nil {
+			return nil, err
+		}
 		tag := &Tag{
-			ID:     id,
-			Object: id,
-			Type:   string(ObjectCommit),
-			repo:   repo,
+			Name:    name,
+			ID:      tagID,
+			Object:  commitID,
+			Type:    string(ObjectCommit),
+			Tagger:  commit.Committer,
+			Message: commit.Message(),
+			repo:    repo,
 		}
 
 		repo.tagCache.Set(id.String(), tag)
 		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)
 	if err != nil {
 		return nil, err
@@ -73,16 +118,57 @@ func (repo *Repository) getTag(id SHA1) (*Tag, error) {
 		return nil, err
 	}
 
+	tag.Name = name
 	tag.ID = id
 	tag.repo = repo
+	tag.Type = tp
 
 	repo.tagCache.Set(id.String(), tag)
 	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.
 func (repo *Repository) GetTag(name string) (*Tag, error) {
-	idStr, err := repo.GetTagCommitID(name)
+	idStr, err := repo.GetTagID(name)
 	if err != nil {
 		return nil, err
 	}
@@ -96,7 +182,6 @@ func (repo *Repository) GetTag(name string) (*Tag, error) {
 	if err != nil {
 		return nil, err
 	}
-	tag.Name = name
 	return tag, nil
 }
 
@@ -108,7 +193,7 @@ func (repo *Repository) GetTagInfos() ([]*Tag, error) {
 		return nil, err
 	}
 
-	tagNames := strings.Split(stdout, "\n")
+	tagNames := strings.Split(strings.TrimRight(stdout, "\n"), "\n")
 	var tags = make([]*Tag, 0, len(tagNames))
 	for _, tagName := range tagNames {
 		tagName = strings.TrimSpace(tagName)
@@ -120,6 +205,7 @@ func (repo *Repository) GetTagInfos() ([]*Tag, error) {
 		if err != nil {
 			return nil, err
 		}
+		tag.Name = tagName
 		tags = append(tags, tag)
 	}
 	sortTagsByTime(tags)
@@ -150,3 +236,38 @@ func (repo *Repository) GetTags() ([]string, error) {
 
 	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
+}
diff --git a/modules/git/repo_tag_test.go b/modules/git/repo_tag_test.go
index ccb2d57ac2..4f727c6c66 100644
--- a/modules/git/repo_tag_test.go
+++ b/modules/git/repo_tag_test.go
@@ -21,8 +21,8 @@ func TestRepository_GetTags(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Len(t, tags, 1)
 	assert.EqualValues(t, "test", tags[0].Name)
-	assert.EqualValues(t, "37991dec2c8e592043f47155ce4808d4580f9123", tags[0].ID.String())
-	assert.EqualValues(t, "commit", tags[0].Type)
+	assert.EqualValues(t, "3ad28a9149a2864384548f3d17ed7f38014c9e8a", tags[0].ID.String())
+	assert.EqualValues(t, "tag", tags[0].Type)
 }
 
 func TestRepository_GetTag(t *testing.T) {
@@ -35,10 +35,78 @@ func TestRepository_GetTag(t *testing.T) {
 	bareRepo1, err := OpenRepository(clonedPath)
 	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.NotNil(t, tag)
-	assert.EqualValues(t, "test", tag.Name)
-	assert.EqualValues(t, "37991dec2c8e592043f47155ce4808d4580f9123", tag.ID.String())
-	assert.EqualValues(t, "commit", tag.Type)
+	assert.EqualValues(t, aTagName, tag.Name)
+	assert.EqualValues(t, aTagID, tag.ID.String())
+	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)
 }
diff --git a/modules/git/tag.go b/modules/git/tag.go
index 500fd27491..c97f574fa6 100644
--- a/modules/git/tag.go
+++ b/modules/git/tag.go
@@ -7,6 +7,7 @@ package git
 import (
 	"bytes"
 	"sort"
+	"strings"
 )
 
 // Tag represents a Git tag.
@@ -59,7 +60,7 @@ l:
 			}
 			nextline += eol + 1
 		case eol == 0:
-			tag.Message = string(data[nextline+1:])
+			tag.Message = strings.TrimRight(string(data[nextline+1:]), "\n")
 			break l
 		default:
 			break l
diff --git a/modules/structs/repo_tag.go b/modules/structs/repo_tag.go
index 6294a8099d..b62395cac4 100644
--- a/modules/structs/repo_tag.go
+++ b/modules/structs/repo_tag.go
@@ -6,11 +6,27 @@ package structs
 
 // Tag represents a repository tag
 type Tag struct {
-	Name   string `json:"name"`
-	Commit struct {
-		SHA string `json:"sha"`
-		URL string `json:"url"`
-	} `json:"commit"`
-	ZipballURL string `json:"zipball_url"`
-	TarballURL string `json:"tarball_url"`
+	Name       string      `json:"name"`
+	ID         string      `json:"id"`
+	Commit     *CommitMeta `json:"commit"`
+	ZipballURL string      `json:"zipball_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"`
 }
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index c1561200cd..2268c1be38 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -751,6 +751,7 @@ func RegisterRoutes(m *macaron.Macaron) {
 						Post(reqToken(), bind(api.CreateStatusOption{}), repo.NewCommitStatus)
 				}, reqRepoReader(models.UnitTypeCode))
 				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("/statuses", repo.GetCommitStatusesByRef)
 				}, reqRepoReader(models.UnitTypeCode))
@@ -762,6 +763,7 @@ func RegisterRoutes(m *macaron.Macaron) {
 					m.Get("/refs/*", repo.GetGitRefs)
 					m.Get("/trees/:sha", context.RepoRef(), repo.GetTree)
 					m.Get("/blobs/:sha", context.RepoRef(), repo.GetBlob)
+					m.Get("/tags/:sha", context.RepoRef(), repo.GetTag)
 				}, reqRepoReader(models.UnitTypeCode))
 				m.Group("/contents", func() {
 					m.Get("/*", repo.GetFileContents)
diff --git a/routers/api/v1/convert/convert.go b/routers/api/v1/convert/convert.go
index ba61c7e46c..a982cb8d37 100644
--- a/routers/api/v1/convert/convert.go
+++ b/routers/api/v1/convert/convert.go
@@ -6,6 +6,7 @@ package convert
 
 import (
 	"fmt"
+	"time"
 
 	"code.gitea.io/gitea/models"
 	"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 {
 	return &api.Branch{
 		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 {
 	return &api.Tag{
-		Name: t.Name,
-		Commit: struct {
-			SHA string `json:"sha"`
-			URL string `json:"url"`
-		}{
-			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"),
+		Name:       t.Name,
+		ID:         t.ID.String(),
+		Commit:     ToCommitMeta(repo, t),
+		ZipballURL: util.URLJoin(repo.HTMLURL(), "archive", t.Name+".zip"),
+		TarballURL: util.URLJoin(repo.HTMLURL(), "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 {
 	authorUsername := ""
 	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)
 	}
 
-	verif := models.ParseCommitWithSignature(c)
-	var signature, payload string
-	if c.Signature != nil {
-		signature = c.Signature.Signature
-		payload = c.Signature.Payload
-	}
-
 	return &api.PayloadCommit{
 		ID:      c.ID.String(),
 		Message: c.Message(),
-		URL:     util.URLJoin(repo.Link(), "commit", c.ID.String()),
+		URL:     util.URLJoin(repo.HTMLURL(), "commit", c.ID.String()),
 		Author: &api.PayloadUser{
 			Name:     c.Author.Name,
 			Email:    c.Author.Email,
@@ -87,13 +76,24 @@ func ToCommit(repo *models.Repository, c *git.Commit) *api.PayloadCommit {
 			Email:    c.Committer.Email,
 			UserName: committerUsername,
 		},
-		Timestamp: c.Author.When,
-		Verification: &api.PayloadCommitVerification{
-			Verified:  verif.Verified,
-			Reason:    verif.Reason,
-			Signature: signature,
-			Payload:   payload,
-		},
+		Timestamp:    c.Author.When,
+		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,
+		Reason:    verif.Reason,
+		Signature: signature,
+		Payload:   payload,
 	}
 }
 
@@ -242,3 +242,45 @@ func ToUser(user *models.User, signed, admin bool) *api.User {
 	}
 	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()),
+	}
+}
diff --git a/routers/api/v1/repo/git_ref.go b/routers/api/v1/repo/git_ref.go
index 2ec8749058..e15f699a1d 100644
--- a/routers/api/v1/repo/git_ref.go
+++ b/routers/api/v1/repo/git_ref.go
@@ -100,8 +100,7 @@ func getGitRefsInternal(ctx *context.APIContext, filter string) {
 			Object: &api.GitObject{
 				SHA:  refs[i].Object.String(),
 				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(),
 			},
 		}
 	}
diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go
index dd1b5aa7c1..ecf580e1b0 100644
--- a/routers/api/v1/repo/tag.go
+++ b/routers/api/v1/repo/tag.go
@@ -7,6 +7,7 @@ package repo
 import (
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/routers/api/v1/convert"
+	"net/http"
 
 	api "code.gitea.io/gitea/modules/structs"
 )
@@ -45,3 +46,47 @@ func ListTags(ctx *context.APIContext) {
 
 	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))
+	}
+}
diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go
index e7df0b8f71..25354b3d66 100644
--- a/routers/api/v1/swagger/repo.go
+++ b/routers/api/v1/swagger/repo.go
@@ -38,11 +38,25 @@ type swaggerResponseBranchList struct {
 
 // TagList
 // swagger:response TagList
-type swaggerReponseTagList struct {
+type swaggerResponseTagList struct {
 	// in: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
 // swagger:response Reference
 type swaggerResponseReference struct {
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 0bd85fbb6e..2b40c89791 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -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}": {
       "get": {
         "produces": [
@@ -6762,6 +6802,57 @@
       },
       "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": {
       "description": "Attachment a generic attachment",
       "type": "object",
@@ -9458,18 +9549,11 @@
       "type": "object",
       "properties": {
         "commit": {
-          "type": "object",
-          "properties": {
-            "sha": {
-              "type": "string",
-              "x-go-name": "SHA"
-            },
-            "url": {
-              "type": "string",
-              "x-go-name": "URL"
-            }
-          },
-          "x-go-name": "Commit"
+          "$ref": "#/definitions/CommitMeta"
+        },
+        "id": {
+          "type": "string",
+          "x-go-name": "ID"
         },
         "name": {
           "type": "string",
@@ -9735,6 +9819,12 @@
     "AccessTokenList": {
       "description": "AccessTokenList represents a list of API access token."
     },
+    "AnnotatedTag": {
+      "description": "AnnotatedTag",
+      "schema": {
+        "$ref": "#/definitions/AnnotatedTag"
+      }
+    },
     "Attachment": {
       "description": "Attachment",
       "schema": {
@@ -10056,6 +10146,12 @@
         }
       }
     },
+    "Tag": {
+      "description": "Tag",
+      "schema": {
+        "$ref": "#/definitions/Tag"
+      }
+    },
     "TagList": {
       "description": "TagList",
       "schema": {