From aceb1085c79e4c75268ce794a8ee84631382a403 Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Thu, 12 Dec 2019 05:23:05 +0100
Subject: [PATCH] [API] extend StopWatch (#9196)

* squash api-stopwatch

* fix prepair logic! + add Tests

* fix lint

* more robust time compare

* delete responce 202 -> 204

* change http responce in test too
---
 integrations/api_issue_stopwatch_test.go |  83 +++++++++
 models/fixtures/stopwatch.yml            |   4 +-
 models/issue_stopwatch.go                |  39 ++++
 modules/structs/issue_stopwatch.go       |  19 ++
 routers/api/v1/api.go                    |   3 +
 routers/api/v1/repo/issue.go             | 138 ---------------
 routers/api/v1/repo/issue_stopwatch.go   | 216 +++++++++++++++++++++++
 routers/api/v1/swagger/issue.go          |  14 ++
 templates/swagger/v1_json.tmpl           | 107 ++++++++++-
 9 files changed, 482 insertions(+), 141 deletions(-)
 create mode 100644 integrations/api_issue_stopwatch_test.go
 create mode 100644 modules/structs/issue_stopwatch.go
 create mode 100644 routers/api/v1/repo/issue_stopwatch.go

diff --git a/integrations/api_issue_stopwatch_test.go b/integrations/api_issue_stopwatch_test.go
new file mode 100644
index 0000000000..e0fe00c415
--- /dev/null
+++ b/integrations/api_issue_stopwatch_test.go
@@ -0,0 +1,83 @@
+// Copyright 2019 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"
+	api "code.gitea.io/gitea/modules/structs"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAPIListStopWatches(t *testing.T) {
+	defer prepareTestEnv(t)()
+
+	repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
+	owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)
+
+	session := loginUser(t, owner.Name)
+	token := getTokenForLoggedInUser(t, session)
+	req := NewRequestf(t, "GET", "/api/v1/user/stopwatches?token=%s", token)
+	resp := session.MakeRequest(t, req, http.StatusOK)
+	var apiWatches []*api.StopWatch
+	DecodeJSON(t, resp, &apiWatches)
+	expect := models.AssertExistsAndLoadBean(t, &models.Stopwatch{UserID: owner.ID}).(*models.Stopwatch)
+	expectAPI, _ := expect.APIFormat()
+	assert.Len(t, apiWatches, 1)
+
+	assert.EqualValues(t, expectAPI.IssueIndex, apiWatches[0].IssueIndex)
+	assert.EqualValues(t, expectAPI.Created.Unix(), apiWatches[0].Created.Unix())
+}
+
+func TestAPIStopStopWatches(t *testing.T) {
+	defer prepareTestEnv(t)()
+
+	issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 2}).(*models.Issue)
+	_ = issue.LoadRepo()
+	owner := models.AssertExistsAndLoadBean(t, &models.User{ID: issue.Repo.OwnerID}).(*models.User)
+	user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
+
+	session := loginUser(t, user.Name)
+	token := getTokenForLoggedInUser(t, session)
+
+	req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/issues/%d/stopwatch/stop?token=%s", owner.Name, issue.Repo.Name, issue.Index, token)
+	session.MakeRequest(t, req, http.StatusCreated)
+	session.MakeRequest(t, req, http.StatusConflict)
+}
+
+func TestAPICancelStopWatches(t *testing.T) {
+	defer prepareTestEnv(t)()
+
+	issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1}).(*models.Issue)
+	_ = issue.LoadRepo()
+	owner := models.AssertExistsAndLoadBean(t, &models.User{ID: issue.Repo.OwnerID}).(*models.User)
+	user := models.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User)
+
+	session := loginUser(t, user.Name)
+	token := getTokenForLoggedInUser(t, session)
+
+	req := NewRequestf(t, "DELETE", "/api/v1/repos/%s/%s/issues/%d/stopwatch/delete?token=%s", owner.Name, issue.Repo.Name, issue.Index, token)
+	session.MakeRequest(t, req, http.StatusNoContent)
+	session.MakeRequest(t, req, http.StatusConflict)
+}
+
+func TestAPIStartStopWatches(t *testing.T) {
+	defer prepareTestEnv(t)()
+
+	issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 3}).(*models.Issue)
+	_ = issue.LoadRepo()
+	owner := models.AssertExistsAndLoadBean(t, &models.User{ID: issue.Repo.OwnerID}).(*models.User)
+	user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
+
+	session := loginUser(t, user.Name)
+	token := getTokenForLoggedInUser(t, session)
+
+	req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/issues/%d/stopwatch/start?token=%s", owner.Name, issue.Repo.Name, issue.Index, token)
+	session.MakeRequest(t, req, http.StatusCreated)
+	session.MakeRequest(t, req, http.StatusConflict)
+}
diff --git a/models/fixtures/stopwatch.yml b/models/fixtures/stopwatch.yml
index 397a8214d4..b7919d6fbb 100644
--- a/models/fixtures/stopwatch.yml
+++ b/models/fixtures/stopwatch.yml
@@ -2,10 +2,10 @@
   id: 1
   user_id: 1
   issue_id: 1
-  created_unix: 1500988502
+  created_unix: 1500988001
 
 -
   id: 2
   user_id: 2
   issue_id: 2
-  created_unix: 1500988502
+  created_unix: 1500988002
diff --git a/models/issue_stopwatch.go b/models/issue_stopwatch.go
index d7c3a9f73b..8047f122b5 100644
--- a/models/issue_stopwatch.go
+++ b/models/issue_stopwatch.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"time"
 
+	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/timeutil"
 )
 
@@ -19,6 +20,9 @@ type Stopwatch struct {
 	CreatedUnix timeutil.TimeStamp `xorm:"created"`
 }
 
+// Stopwatches is a List ful of Stopwatch
+type Stopwatches []Stopwatch
+
 func getStopwatch(e Engine, userID, issueID int64) (sw *Stopwatch, exists bool, err error) {
 	sw = new(Stopwatch)
 	exists, err = e.
@@ -28,6 +32,16 @@ func getStopwatch(e Engine, userID, issueID int64) (sw *Stopwatch, exists bool,
 	return
 }
 
+// GetUserStopwatches return list of all stopwatches of a user
+func GetUserStopwatches(userID int64) (sws *Stopwatches, err error) {
+	sws = new(Stopwatches)
+	err = x.Where("stopwatch.user_id = ?", userID).Find(sws)
+	if err != nil {
+		return nil, err
+	}
+	return sws, nil
+}
+
 // StopwatchExists returns true if the stopwatch exists
 func StopwatchExists(userID int64, issueID int64) bool {
 	_, exists, _ := getStopwatch(x, userID, issueID)
@@ -160,3 +174,28 @@ func SecToTime(duration int64) string {
 
 	return hrs
 }
+
+// APIFormat convert Stopwatch type to api.StopWatch type
+func (sw *Stopwatch) APIFormat() (api.StopWatch, error) {
+	issue, err := getIssueByID(x, sw.IssueID)
+	if err != nil {
+		return api.StopWatch{}, err
+	}
+	return api.StopWatch{
+		Created:    sw.CreatedUnix.AsTime(),
+		IssueIndex: issue.Index,
+	}, nil
+}
+
+// APIFormat convert Stopwatches type to api.StopWatches type
+func (sws Stopwatches) APIFormat() (api.StopWatches, error) {
+	result := api.StopWatches(make([]api.StopWatch, 0, len(sws)))
+	for _, sw := range sws {
+		apiSW, err := sw.APIFormat()
+		if err != nil {
+			return nil, err
+		}
+		result = append(result, apiSW)
+	}
+	return result, nil
+}
diff --git a/modules/structs/issue_stopwatch.go b/modules/structs/issue_stopwatch.go
new file mode 100644
index 0000000000..10510e36ef
--- /dev/null
+++ b/modules/structs/issue_stopwatch.go
@@ -0,0 +1,19 @@
+// Copyright 2019 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 structs
+
+import (
+	"time"
+)
+
+// StopWatch represent a running stopwatch
+type StopWatch struct {
+	// swagger:strfmt date-time
+	Created    time.Time `json:"created"`
+	IssueIndex int64     `json:"issue_index"`
+}
+
+// StopWatches represent a list of stopwatches
+type StopWatches []StopWatch
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index cd5fc1f3eb..7526d3f5ef 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -584,6 +584,8 @@ func RegisterRoutes(m *macaron.Macaron) {
 			})
 			m.Get("/times", repo.ListMyTrackedTimes)
 
+			m.Get("/stopwatches", repo.GetStopwatches)
+
 			m.Get("/subscriptions", user.GetMyWatchedRepos)
 
 			m.Get("/teams", org.ListUserTeams)
@@ -691,6 +693,7 @@ func RegisterRoutes(m *macaron.Macaron) {
 						m.Group("/stopwatch", func() {
 							m.Post("/start", reqToken(), repo.StartIssueStopwatch)
 							m.Post("/stop", reqToken(), repo.StopIssueStopwatch)
+							m.Delete("/delete", reqToken(), repo.DeleteIssueStopwatch)
 						})
 						m.Group("/subscriptions", func() {
 							m.Get("", repo.GetIssueSubscribers)
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index 186e66cb8f..6972d447a6 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -598,141 +598,3 @@ func UpdateIssueDeadline(ctx *context.APIContext, form api.EditDeadlineOption) {
 
 	ctx.JSON(201, api.IssueDeadline{Deadline: &deadline})
 }
-
-// StartIssueStopwatch creates a stopwatch for the given issue.
-func StartIssueStopwatch(ctx *context.APIContext) {
-	// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/stopwatch/start issue issueStartStopWatch
-	// ---
-	// summary: Start stopwatch on an issue.
-	// consumes:
-	// - application/json
-	// 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: index
-	//   in: path
-	//   description: index of the issue to create the stopwatch on
-	//   type: integer
-	//   format: int64
-	//   required: true
-	// responses:
-	//   "201":
-	//     "$ref": "#/responses/empty"
-	//   "403":
-	//     description: Not repo writer, user does not have rights to toggle stopwatch
-	//   "404":
-	//     description: Issue not found
-	//   "409":
-	//     description: Cannot start a stopwatch again if it already exists
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
-	if err != nil {
-		if models.IsErrIssueNotExist(err) {
-			ctx.NotFound()
-		} else {
-			ctx.Error(500, "GetIssueByIndex", err)
-		}
-
-		return
-	}
-
-	if !ctx.Repo.CanWrite(models.UnitTypeIssues) {
-		ctx.Status(403)
-		return
-	}
-
-	if !ctx.Repo.CanUseTimetracker(issue, ctx.User) {
-		ctx.Status(403)
-		return
-	}
-
-	if models.StopwatchExists(ctx.User.ID, issue.ID) {
-		ctx.Error(409, "StopwatchExists", "a stopwatch has already been started for this issue")
-		return
-	}
-
-	if err := models.CreateOrStopIssueStopwatch(ctx.User, issue); err != nil {
-		ctx.Error(500, "CreateOrStopIssueStopwatch", err)
-		return
-	}
-
-	ctx.Status(201)
-}
-
-// StopIssueStopwatch stops a stopwatch for the given issue.
-func StopIssueStopwatch(ctx *context.APIContext) {
-	// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/stopwatch/stop issue issueStopWatch
-	// ---
-	// summary: Stop an issue's existing stopwatch.
-	// consumes:
-	// - application/json
-	// 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: index
-	//   in: path
-	//   description: index of the issue to stop the stopwatch on
-	//   type: integer
-	//   format: int64
-	//   required: true
-	// responses:
-	//   "201":
-	//     "$ref": "#/responses/empty"
-	//   "403":
-	//     description: Not repo writer, user does not have rights to toggle stopwatch
-	//   "404":
-	//     description: Issue not found
-	//   "409":
-	//     description:  Cannot stop a non existent stopwatch
-	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
-	if err != nil {
-		if models.IsErrIssueNotExist(err) {
-			ctx.NotFound()
-		} else {
-			ctx.Error(500, "GetIssueByIndex", err)
-		}
-
-		return
-	}
-
-	if !ctx.Repo.CanWrite(models.UnitTypeIssues) {
-		ctx.Status(403)
-		return
-	}
-
-	if !ctx.Repo.CanUseTimetracker(issue, ctx.User) {
-		ctx.Status(403)
-		return
-	}
-
-	if !models.StopwatchExists(ctx.User.ID, issue.ID) {
-		ctx.Error(409, "StopwatchExists", "cannot stop a non existent stopwatch")
-		return
-	}
-
-	if err := models.CreateOrStopIssueStopwatch(ctx.User, issue); err != nil {
-		ctx.Error(500, "CreateOrStopIssueStopwatch", err)
-		return
-	}
-
-	ctx.Status(201)
-}
diff --git a/routers/api/v1/repo/issue_stopwatch.go b/routers/api/v1/repo/issue_stopwatch.go
new file mode 100644
index 0000000000..48b2f6498f
--- /dev/null
+++ b/routers/api/v1/repo/issue_stopwatch.go
@@ -0,0 +1,216 @@
+// Copyright 2019 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 (
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/modules/context"
+)
+
+// StartIssueStopwatch creates a stopwatch for the given issue.
+func StartIssueStopwatch(ctx *context.APIContext) {
+	// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/stopwatch/start issue issueStartStopWatch
+	// ---
+	// summary: Start stopwatch on an issue.
+	// consumes:
+	// - application/json
+	// 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: index
+	//   in: path
+	//   description: index of the issue to create the stopwatch on
+	//   type: integer
+	//   format: int64
+	//   required: true
+	// responses:
+	//   "201":
+	//     "$ref": "#/responses/empty"
+	//   "403":
+	//     description: Not repo writer, user does not have rights to toggle stopwatch
+	//   "404":
+	//     description: Issue not found
+	//   "409":
+	//     description: Cannot start a stopwatch again if it already exists
+	issue, err := prepareIssueStopwatch(ctx, false)
+	if err != nil {
+		return
+	}
+
+	if err := models.CreateOrStopIssueStopwatch(ctx.User, issue); err != nil {
+		ctx.Error(500, "CreateOrStopIssueStopwatch", err)
+		return
+	}
+
+	ctx.Status(201)
+}
+
+// StopIssueStopwatch stops a stopwatch for the given issue.
+func StopIssueStopwatch(ctx *context.APIContext) {
+	// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/stopwatch/stop issue issueStopStopWatch
+	// ---
+	// summary: Stop an issue's existing stopwatch.
+	// consumes:
+	// - application/json
+	// 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: index
+	//   in: path
+	//   description: index of the issue to stop the stopwatch on
+	//   type: integer
+	//   format: int64
+	//   required: true
+	// responses:
+	//   "201":
+	//     "$ref": "#/responses/empty"
+	//   "403":
+	//     description: Not repo writer, user does not have rights to toggle stopwatch
+	//   "404":
+	//     description: Issue not found
+	//   "409":
+	//     description:  Cannot stop a non existent stopwatch
+	issue, err := prepareIssueStopwatch(ctx, true)
+	if err != nil {
+		return
+	}
+
+	if err := models.CreateOrStopIssueStopwatch(ctx.User, issue); err != nil {
+		ctx.Error(500, "CreateOrStopIssueStopwatch", err)
+		return
+	}
+
+	ctx.Status(201)
+}
+
+// DeleteIssueStopwatch delete a specific stopwatch
+func DeleteIssueStopwatch(ctx *context.APIContext) {
+	// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/stopwatch/delete issue issueDeleteStopWatch
+	// ---
+	// summary: Delete an issue's existing stopwatch.
+	// consumes:
+	// - application/json
+	// 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: index
+	//   in: path
+	//   description: index of the issue to stop the stopwatch on
+	//   type: integer
+	//   format: int64
+	//   required: true
+	// responses:
+	//   "204":
+	//     "$ref": "#/responses/empty"
+	//   "403":
+	//     description: Not repo writer, user does not have rights to toggle stopwatch
+	//   "404":
+	//     description: Issue not found
+	//   "409":
+	//     description:  Cannot cancel a non existent stopwatch
+	issue, err := prepareIssueStopwatch(ctx, true)
+	if err != nil {
+		return
+	}
+
+	if err := models.CancelStopwatch(ctx.User, issue); err != nil {
+		ctx.Error(500, "CancelStopwatch", err)
+		return
+	}
+
+	ctx.Status(204)
+}
+
+func prepareIssueStopwatch(ctx *context.APIContext, shouldExist bool) (*models.Issue, error) {
+	issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
+	if err != nil {
+		if models.IsErrIssueNotExist(err) {
+			ctx.NotFound()
+		} else {
+			ctx.Error(500, "GetIssueByIndex", err)
+		}
+
+		return nil, err
+	}
+
+	if !ctx.Repo.CanWrite(models.UnitTypeIssues) {
+		ctx.Status(403)
+		return nil, err
+	}
+
+	if !ctx.Repo.CanUseTimetracker(issue, ctx.User) {
+		ctx.Status(403)
+		return nil, err
+	}
+
+	if models.StopwatchExists(ctx.User.ID, issue.ID) != shouldExist {
+		if shouldExist {
+			ctx.Error(409, "StopwatchExists", "cannot stop/cancel a non existent stopwatch")
+		} else {
+			ctx.Error(409, "StopwatchExists", "cannot start a stopwatch again if it already exists")
+		}
+		return nil, err
+	}
+
+	return issue, nil
+}
+
+// GetStopwatches get all stopwatches
+func GetStopwatches(ctx *context.APIContext) {
+	// swagger:operation GET /user/stopwatches user userGetStopWatches
+	// ---
+	// summary: Get list of all existing stopwatches
+	// consumes:
+	// - application/json
+	// produces:
+	// - application/json
+	// responses:
+	//   "200":
+	//     "$ref": "#/responses/StopWatchList"
+
+	sws, err := models.GetUserStopwatches(ctx.User.ID)
+	if err != nil {
+		ctx.Error(500, "GetUserStopwatches", err)
+		return
+	}
+
+	apiSWs, err := sws.APIFormat()
+	if err != nil {
+		ctx.Error(500, "APIFormat", err)
+		return
+	}
+
+	ctx.JSON(200, apiSWs)
+}
diff --git a/routers/api/v1/swagger/issue.go b/routers/api/v1/swagger/issue.go
index a78c2982fd..68c0a9a38d 100644
--- a/routers/api/v1/swagger/issue.go
+++ b/routers/api/v1/swagger/issue.go
@@ -85,6 +85,20 @@ type swaggerIssueDeadline struct {
 	Body api.IssueDeadline `json:"body"`
 }
 
+// StopWatch
+// swagger:response StopWatch
+type swaggerResponseStopWatch struct {
+	// in:body
+	Body api.StopWatch `json:"body"`
+}
+
+// StopWatchList
+// swagger:response StopWatchList
+type swaggerResponseStopWatchList struct {
+	// in:body
+	Body []api.StopWatch `json:"body"`
+}
+
 // EditReactionOption
 // swagger:response EditReactionOption
 type swaggerEditReactionOption struct {
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 9c8db28817..7ed43b450c 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -3972,6 +3972,59 @@
         }
       }
     },
+    "/repos/{owner}/{repo}/issues/{index}/stopwatch/delete": {
+      "delete": {
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "issue"
+        ],
+        "summary": "Delete an issue's existing stopwatch.",
+        "operationId": "issueDeleteStopWatch",
+        "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": "integer",
+            "format": "int64",
+            "description": "index of the issue to stop the stopwatch on",
+            "name": "index",
+            "in": "path",
+            "required": true
+          }
+        ],
+        "responses": {
+          "204": {
+            "$ref": "#/responses/empty"
+          },
+          "403": {
+            "description": "Not repo writer, user does not have rights to toggle stopwatch"
+          },
+          "404": {
+            "description": "Issue not found"
+          },
+          "409": {
+            "description": "Cannot cancel a non existent stopwatch"
+          }
+        }
+      }
+    },
     "/repos/{owner}/{repo}/issues/{index}/stopwatch/start": {
       "post": {
         "consumes": [
@@ -4037,7 +4090,7 @@
           "issue"
         ],
         "summary": "Stop an issue's existing stopwatch.",
-        "operationId": "issueStopWatch",
+        "operationId": "issueStopStopWatch",
         "parameters": [
           {
             "type": "string",
@@ -7174,6 +7227,26 @@
         }
       }
     },
+    "/user/stopwatches": {
+      "get": {
+        "consumes": [
+          "application/json"
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "tags": [
+          "user"
+        ],
+        "summary": "Get list of all existing stopwatches",
+        "operationId": "userGetStopWatches",
+        "responses": {
+          "200": {
+            "$ref": "#/responses/StopWatchList"
+          }
+        }
+      }
+    },
     "/user/subscriptions": {
       "get": {
         "produces": [
@@ -10808,6 +10881,23 @@
       "type": "string",
       "x-go-package": "code.gitea.io/gitea/modules/structs"
     },
+    "StopWatch": {
+      "description": "StopWatch represent a running stopwatch",
+      "type": "object",
+      "properties": {
+        "created": {
+          "type": "string",
+          "format": "date-time",
+          "x-go-name": "Created"
+        },
+        "issue_index": {
+          "type": "integer",
+          "format": "int64",
+          "x-go-name": "IssueIndex"
+        }
+      },
+      "x-go-package": "code.gitea.io/gitea/modules/structs"
+    },
     "Tag": {
       "description": "Tag represents a repository tag",
       "type": "object",
@@ -11553,6 +11643,21 @@
         }
       }
     },
+    "StopWatch": {
+      "description": "StopWatch",
+      "schema": {
+        "$ref": "#/definitions/StopWatch"
+      }
+    },
+    "StopWatchList": {
+      "description": "StopWatchList",
+      "schema": {
+        "type": "array",
+        "items": {
+          "$ref": "#/definitions/StopWatch"
+        }
+      }
+    },
     "Tag": {
       "description": "Tag",
       "schema": {