From 6392f4691af7db20a2ad81ec19408fa9377a5cd1 Mon Sep 17 00:00:00 2001
From: Lauris BH <lauris@nix.lv>
Date: Tue, 25 Jan 2022 08:33:40 +0200
Subject: [PATCH] API: Return primary language and repository language stats
 API URL (#18396)

---
 models/repo/repo.go            | 24 ++++++++++++++++++++++++
 models/repo_list.go            |  4 ++--
 modules/convert/repository.go  |  9 +++++++++
 modules/structs/repo.go        |  2 ++
 routers/api/v1/repo/repo.go    |  5 +++++
 routers/api/v1/user/repo.go    |  5 +++++
 templates/swagger/v1_json.tmpl |  8 ++++++++
 7 files changed, 55 insertions(+), 2 deletions(-)

diff --git a/models/repo/repo.go b/models/repo/repo.go
index a78d2873154..353d707e609 100644
--- a/models/repo/repo.go
+++ b/models/repo/repo.go
@@ -222,6 +222,30 @@ func (repo *Repository) MustOwner() *user_model.User {
 	return repo.mustOwner(db.DefaultContext)
 }
 
+// LoadAttributes loads attributes of the repository.
+func (repo *Repository) LoadAttributes(ctx context.Context) error {
+	// Load owner
+	if err := repo.GetOwner(ctx); err != nil {
+		return fmt.Errorf("load owner: %w", err)
+	}
+
+	// Load primary language
+	stats := make(LanguageStatList, 0, 1)
+	if err := db.GetEngine(ctx).
+		Where("`repo_id` = ? AND `is_primary` = ? AND `language` != ?", repo.ID, true, "other").
+		Find(&stats); err != nil {
+		return fmt.Errorf("find primary languages: %w", err)
+	}
+	stats.LoadAttributes()
+	for _, st := range stats {
+		if st.RepoID == repo.ID {
+			repo.PrimaryLanguage = st
+			break
+		}
+	}
+	return nil
+}
+
 // FullName returns the repository full name
 func (repo *Repository) FullName() string {
 	return repo.OwnerName + "/" + repo.Name
diff --git a/models/repo_list.go b/models/repo_list.go
index 9cb7a163fc8..290919bb6da 100644
--- a/models/repo_list.go
+++ b/models/repo_list.go
@@ -623,7 +623,7 @@ func FindUserAccessibleRepoIDs(user *user_model.User) ([]int64, error) {
 }
 
 // GetUserRepositories returns a list of repositories of given user.
-func GetUserRepositories(opts *SearchRepoOptions) ([]*repo_model.Repository, int64, error) {
+func GetUserRepositories(opts *SearchRepoOptions) (RepositoryList, int64, error) {
 	if len(opts.OrderBy) == 0 {
 		opts.OrderBy = "updated_unix DESC"
 	}
@@ -646,6 +646,6 @@ func GetUserRepositories(opts *SearchRepoOptions) ([]*repo_model.Repository, int
 	}
 
 	sess = sess.Where(cond).OrderBy(opts.OrderBy.String())
-	repos := make([]*repo_model.Repository, 0, opts.PageSize)
+	repos := make(RepositoryList, 0, opts.PageSize)
 	return repos, count, db.SetSessionPagination(sess, opts).Find(&repos)
 }
diff --git a/modules/convert/repository.go b/modules/convert/repository.go
index a3569255396..1f11fda7ac2 100644
--- a/modules/convert/repository.go
+++ b/modules/convert/repository.go
@@ -125,6 +125,13 @@ func innerToRepo(repo *repo_model.Repository, mode perm.AccessMode, isParent boo
 		}
 	}
 
+	var language string
+	if repo.PrimaryLanguage != nil {
+		language = repo.PrimaryLanguage.Language
+	}
+
+	repoAPIURL := repo.APIURL()
+
 	return &api.Repository{
 		ID:                        repo.ID,
 		Owner:                     ToUserWithAccessMode(repo.Owner, mode),
@@ -144,6 +151,8 @@ func innerToRepo(repo *repo_model.Repository, mode perm.AccessMode, isParent boo
 		CloneURL:                  cloneLink.HTTPS,
 		OriginalURL:               repo.SanitizedOriginalURL(),
 		Website:                   repo.Website,
+		Language:                  language,
+		LanguagesURL:              repoAPIURL + "/languages",
 		Stars:                     repo.NumStars,
 		Forks:                     repo.NumForks,
 		Watchers:                  repo.NumWatches,
diff --git a/modules/structs/repo.go b/modules/structs/repo.go
index 671885f20a6..5a1e99e36be 100644
--- a/modules/structs/repo.go
+++ b/modules/structs/repo.go
@@ -59,6 +59,8 @@ type Repository struct {
 	Parent        *Repository `json:"parent"`
 	Mirror        bool        `json:"mirror"`
 	Size          int         `json:"size"`
+	Language      string      `json:"language"`
+	LanguagesURL  string      `json:"languages_url"`
 	HTMLURL       string      `json:"html_url"`
 	SSHURL        string      `json:"ssh_url"`
 	CloneURL      string      `json:"clone_url"`
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index c2dfc4f193e..7a7fe218e82 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -533,6 +533,11 @@ func Get(ctx *context.APIContext) {
 	//   "200":
 	//     "$ref": "#/responses/Repository"
 
+	if err := ctx.Repo.Repository.LoadAttributes(ctx); err != nil {
+		ctx.Error(http.StatusInternalServerError, "Repository.LoadAttributes", err)
+		return
+	}
+
 	ctx.JSON(http.StatusOK, convert.ToRepo(ctx.Repo.Repository, ctx.Repo.AccessMode))
 }
 
diff --git a/routers/api/v1/user/repo.go b/routers/api/v1/user/repo.go
index 3d5c8418566..109548ec768 100644
--- a/routers/api/v1/user/repo.go
+++ b/routers/api/v1/user/repo.go
@@ -32,6 +32,11 @@ func listUserRepos(ctx *context.APIContext, u *user_model.User, private bool) {
 		return
 	}
 
+	if err := repos.LoadAttributes(); err != nil {
+		ctx.Error(http.StatusInternalServerError, "RepositoryList.LoadAttributes", err)
+		return
+	}
+
 	apiRepos := make([]*api.Repository, 0, len(repos))
 	for i := range repos {
 		access, err := models.AccessLevel(ctx.User, repos[i])
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 768c4c69ee3..497636e7814 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -17188,6 +17188,14 @@
         "internal_tracker": {
           "$ref": "#/definitions/InternalTracker"
         },
+        "language": {
+          "type": "string",
+          "x-go-name": "Language"
+        },
+        "languages_url": {
+          "type": "string",
+          "x-go-name": "LanguagesURL"
+        },
         "mirror": {
           "type": "boolean",
           "x-go-name": "Mirror"