Files
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

226 lines
6.3 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"errors"
"net/http"
access_model "github.com/hanzoai/git/models/perm/access"
repo_model "github.com/hanzoai/git/models/repo"
user_model "github.com/hanzoai/git/models/user"
api "github.com/hanzoai/git/modules/structs"
"github.com/hanzoai/git/routers/api/v1/utils"
"github.com/hanzoai/git/services/context"
"github.com/hanzoai/git/services/convert"
)
// getWatchedRepos returns the repos that the user with the specified userID is watching
func getWatchedRepos(ctx *context.APIContext, user *user_model.User, private bool) ([]*api.Repository, int64, error) {
opts := &repo_model.WatchedReposOptions{
ListOptions: utils.GetListOptions(ctx),
WatcherID: user.ID,
IncludePrivate: private,
Actor: user,
}
opts.ApplyPublicOnly(ctx.PublicOnly)
watchedRepos, total, err := repo_model.GetWatchedRepos(ctx, opts)
if err != nil {
return nil, 0, err
}
repos := make([]*api.Repository, len(watchedRepos))
for i, watched := range watchedRepos {
permission, err := access_model.GetIndividualUserRepoPermission(ctx, watched, user)
if err != nil {
return nil, 0, err
}
repos[i] = convert.ToRepo(ctx, watched, permission)
}
return repos, total, nil
}
// GetWatchedRepos returns the repos that the user specified in ctx is watching
func GetWatchedRepos(ctx *context.APIContext) {
// swagger:operation GET /users/{username}/subscriptions user userListSubscriptions
// ---
// summary: List the repositories watched by a user
// produces:
// - application/json
// parameters:
// - name: username
// type: string
// in: path
// description: username of the user whose watched repos are to be listed
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/RepositoryList"
// "404":
// "$ref": "#/responses/notFound"
private := ctx.ContextUser.ID == ctx.Doer.ID
repos, total, err := getWatchedRepos(ctx, ctx.ContextUser, private)
if err != nil {
ctx.APIErrorInternal(err)
}
ctx.SetLinkHeader(total, utils.GetListOptions(ctx).PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &repos)
}
// GetMyWatchedRepos returns the repos that the authenticated user is watching
func GetMyWatchedRepos(ctx *context.APIContext) {
// swagger:operation GET /user/subscriptions user userCurrentListSubscriptions
// ---
// summary: List repositories watched by the authenticated user
// produces:
// - application/json
// parameters:
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/RepositoryList"
repos, total, err := getWatchedRepos(ctx, ctx.Doer, true)
if err != nil {
ctx.APIErrorInternal(err)
}
ctx.SetLinkHeader(total, utils.GetListOptions(ctx).PageSize)
ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, &repos)
}
// IsWatching returns whether the authenticated user is watching the repo
// specified in ctx
func IsWatching(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/subscription repository userCurrentCheckSubscription
// ---
// summary: Check if the current user is watching a repo
// 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:
// "200":
// "$ref": "#/responses/WatchInfo"
// "404":
// description: User is not watching this repo or repo do not exist
if repo_model.IsWatching(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID) {
ctx.JSON(http.StatusOK, api.WatchInfo{
Subscribed: true,
Ignored: false,
Reason: nil,
CreatedAt: ctx.Repo.Repository.CreatedUnix.AsTime(),
URL: subscriptionURL(ctx.Repo.Repository),
RepositoryURL: ctx.Repo.Repository.APIURL(),
})
} else {
ctx.APIErrorNotFound()
}
}
// Watch the repo specified in ctx, as the authenticated user
func Watch(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/subscription repository userCurrentPutSubscription
// ---
// summary: Watch a repo
// 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:
// "200":
// "$ref": "#/responses/WatchInfo"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, true)
if err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.APIError(http.StatusForbidden, err.Error())
} else {
ctx.APIErrorInternal(err)
}
return
}
ctx.JSON(http.StatusOK, api.WatchInfo{
Subscribed: true,
Ignored: false,
Reason: nil,
CreatedAt: ctx.Repo.Repository.CreatedUnix.AsTime(),
URL: subscriptionURL(ctx.Repo.Repository),
RepositoryURL: ctx.Repo.Repository.APIURL(),
})
}
// Unwatch the repo specified in ctx, as the authenticated user
func Unwatch(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/subscription repository userCurrentDeleteSubscription
// ---
// summary: Unwatch a repo
// 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:
// "204":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"
err := repo_model.WatchRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// subscriptionURL returns the URL of the subscription API endpoint of a repo
func subscriptionURL(repo *repo_model.Repository) string {
return repo.APIURL() + "/subscription"
}