From 2874ab54bccd7a4ebd6b8a51367eb8c1d9b16720 Mon Sep 17 00:00:00 2001
From: Lauris BH <lauris@nix.lv>
Date: Sun, 7 Jun 2020 14:48:41 +0300
Subject: [PATCH] Add language statistics API endpoint (#11737)

* Add language statistics API

* Add tests
---
 integrations/api_repo_languages_test.go | 46 ++++++++++++++
 routers/api/v1/api.go                   |  1 +
 routers/api/v1/repo/language.go         | 84 +++++++++++++++++++++++++
 routers/api/v1/swagger/repo.go          |  7 +++
 templates/swagger/v1_json.tmpl          | 46 ++++++++++++++
 5 files changed, 184 insertions(+)
 create mode 100644 integrations/api_repo_languages_test.go
 create mode 100644 routers/api/v1/repo/language.go

diff --git a/integrations/api_repo_languages_test.go b/integrations/api_repo_languages_test.go
new file mode 100644
index 0000000000..ca92cd4f75
--- /dev/null
+++ b/integrations/api_repo_languages_test.go
@@ -0,0 +1,46 @@
+// Copyright 2020 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"
+	"net/url"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRepoLanguages(t *testing.T) {
+	onGiteaRun(t, func(t *testing.T, u *url.URL) {
+		session := loginUser(t, "user2")
+
+		// Request editor page
+		req := NewRequest(t, "GET", "/user2/repo1/_new/master/")
+		resp := session.MakeRequest(t, req, http.StatusOK)
+
+		doc := NewHTMLParser(t, resp.Body)
+		lastCommit := doc.GetInputValueByName("last_commit")
+		assert.NotEmpty(t, lastCommit)
+
+		// Save new file to master branch
+		req = NewRequestWithValues(t, "POST", "/user2/repo1/_new/master/", map[string]string{
+			"_csrf":         doc.GetCSRF(),
+			"last_commit":   lastCommit,
+			"tree_path":     "test.go",
+			"content":       "package main",
+			"commit_choice": "direct",
+		})
+		session.MakeRequest(t, req, http.StatusFound)
+
+		// Save new file to master branch
+		req = NewRequest(t, "GET", "/api/v1/repos/user2/repo1/languages")
+		resp = session.MakeRequest(t, req, http.StatusOK)
+
+		var languages map[string]int64
+		DecodeJSON(t, resp, &languages)
+
+		assert.InDeltaMapValues(t, map[string]int64{"Go": 12}, languages, 0)
+	})
+}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 1ae4e7a58f..0567c3560c 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -855,6 +855,7 @@ func RegisterRoutes(m *macaron.Macaron) {
 							Delete(reqToken(), repo.DeleteTopic)
 					}, reqAdmin())
 				}, reqAnyRepoReader())
+				m.Get("/languages", reqRepoReader(models.UnitTypeCode), repo.GetLanguages)
 			}, repoAssignment())
 		})
 
diff --git a/routers/api/v1/repo/language.go b/routers/api/v1/repo/language.go
new file mode 100644
index 0000000000..c45911ee66
--- /dev/null
+++ b/routers/api/v1/repo/language.go
@@ -0,0 +1,84 @@
+// Copyright 2020 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 repo
+
+import (
+	"bytes"
+	"net/http"
+	"strconv"
+
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/modules/log"
+)
+
+type languageResponse []*models.LanguageStat
+
+func (l languageResponse) MarshalJSON() ([]byte, error) {
+	var buf bytes.Buffer
+	if _, err := buf.WriteString("{"); err != nil {
+		return nil, err
+	}
+	for i, lang := range l {
+		if i > 0 {
+			if _, err := buf.WriteString(","); err != nil {
+				return nil, err
+			}
+		}
+		if _, err := buf.WriteString(strconv.Quote(lang.Language)); err != nil {
+			return nil, err
+		}
+		if _, err := buf.WriteString(":"); err != nil {
+			return nil, err
+		}
+		if _, err := buf.WriteString(strconv.FormatInt(lang.Size, 10)); err != nil {
+			return nil, err
+		}
+	}
+	if _, err := buf.WriteString("}"); err != nil {
+		return nil, err
+	}
+
+	return buf.Bytes(), nil
+}
+
+// GetLanguages returns languages and number of bytes of code written
+func GetLanguages(ctx *context.APIContext) {
+	// swagger:operation GET /repos/{owner}/{repo}/languages repository repoGetLanguages
+	// ---
+	// summary: Get languages and number of bytes of code written
+	// 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
+	// responses:
+	//   "404":
+	//     "$ref": "#/responses/notFound"
+	//   "200":
+	//     "$ref": "#/responses/LanguageStatistics"
+
+	langs, err := ctx.Repo.Repository.GetLanguageStats()
+	if err != nil {
+		log.Error("GetLanguageStats failed: %v", err)
+		ctx.InternalServerError(err)
+		return
+	}
+
+	resp := make(languageResponse, len(langs))
+	for i, v := range langs {
+		resp[i] = v
+	}
+
+	ctx.JSON(http.StatusOK, resp)
+}
diff --git a/routers/api/v1/swagger/repo.go b/routers/api/v1/swagger/repo.go
index bcbc2b5fa9..bce9e45c37 100644
--- a/routers/api/v1/swagger/repo.go
+++ b/routers/api/v1/swagger/repo.go
@@ -302,3 +302,10 @@ type swaggerTopicNames struct {
 	// in: body
 	Body api.TopicName `json:"body"`
 }
+
+// LanguageStatistics
+// swagger:response LanguageStatistics
+type swaggerLanguageStatistics struct {
+	// in: body
+	Body map[string]int64 `json:"body"`
+}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 456d41b9d4..e91fad693d 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -6049,6 +6049,42 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/languages": {
+      "get": {
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "repository"
+        ],
+        "summary": "Get languages and number of bytes of code written",
+        "operationId": "repoGetLanguages",
+        "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
+          }
+        ],
+        "responses": {
+          "200": {
+            "$ref": "#/responses/LanguageStatistics"
+          },
+          "404": {
+            "$ref": "#/responses/notFound"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/milestones": {
       "get": {
         "produces": [
@@ -14917,6 +14953,16 @@
         }
       }
     },
+    "LanguageStatistics": {
+      "description": "LanguageStatistics",
+      "schema": {
+        "type": "object",
+        "additionalProperties": {
+          "type": "integer",
+          "format": "int64"
+        }
+      }
+    },
     "MarkdownRender": {
       "description": "MarkdownRender is a rendered markdown document",
       "schema": {