mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
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]>
248 lines
6.9 KiB
Go
248 lines
6.9 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package issues
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/hanzoai/git/models/db"
|
|
"github.com/hanzoai/git/models/repo"
|
|
user_model "github.com/hanzoai/git/models/user"
|
|
"github.com/hanzoai/git/modules/timeutil"
|
|
"github.com/hanzoai/git/modules/util"
|
|
|
|
"github.com/hanzoai/builder"
|
|
)
|
|
|
|
// Stopwatch represents a stopwatch for time tracking.
|
|
type Stopwatch struct {
|
|
ID int64 `xorm:"pk autoincr"`
|
|
IssueID int64 `xorm:"INDEX"`
|
|
UserID int64 `xorm:"INDEX"`
|
|
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
|
}
|
|
|
|
func init() {
|
|
db.RegisterModel(new(Stopwatch))
|
|
}
|
|
|
|
// Seconds returns the amount of time passed since creation, based on local server time
|
|
func (s Stopwatch) Seconds() int64 {
|
|
return int64(timeutil.TimeStampNow() - s.CreatedUnix)
|
|
}
|
|
|
|
func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) {
|
|
sw = new(Stopwatch)
|
|
exists, err = db.GetEngine(ctx).
|
|
Where("user_id = ?", userID).
|
|
And("issue_id = ?", issueID).
|
|
Get(sw)
|
|
return sw, exists, err
|
|
}
|
|
|
|
type UserStopwatch struct {
|
|
UserID int64
|
|
StopWatches []*Stopwatch
|
|
}
|
|
|
|
func GetUIDsAndStopwatch(ctx context.Context) ([]*UserStopwatch, error) {
|
|
sws := []*Stopwatch{}
|
|
if err := db.GetEngine(ctx).Where("issue_id != 0").Find(&sws); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(sws) == 0 {
|
|
return []*UserStopwatch{}, nil
|
|
}
|
|
|
|
lastUserID := int64(-1)
|
|
res := []*UserStopwatch{}
|
|
for _, sw := range sws {
|
|
if lastUserID == sw.UserID {
|
|
lastUserStopwatch := res[len(res)-1]
|
|
lastUserStopwatch.StopWatches = append(lastUserStopwatch.StopWatches, sw)
|
|
} else {
|
|
res = append(res, &UserStopwatch{
|
|
UserID: sw.UserID,
|
|
StopWatches: []*Stopwatch{sw},
|
|
})
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// GetUserStopwatches return list of the user's all stopwatches
|
|
func GetUserStopwatches(ctx context.Context, userID int64, listOptions db.ListOptions) ([]*Stopwatch, error) {
|
|
sws := make([]*Stopwatch, 0, 8)
|
|
sess := db.GetEngine(ctx).Where("stopwatch.user_id = ?", userID)
|
|
if listOptions.Page > 0 {
|
|
db.SetSessionPagination(sess, &listOptions)
|
|
}
|
|
|
|
err := sess.Find(&sws)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return sws, nil
|
|
}
|
|
|
|
// CountUserStopwatches return count of the user's all stopwatches
|
|
func CountUserStopwatches(ctx context.Context, userID int64) (int64, error) {
|
|
return db.GetEngine(ctx).Where("user_id = ?", userID).Count(&Stopwatch{})
|
|
}
|
|
|
|
// StopwatchExists returns true if the stopwatch exists
|
|
func StopwatchExists(ctx context.Context, userID, issueID int64) bool {
|
|
_, exists, _ := getStopwatch(ctx, userID, issueID)
|
|
return exists
|
|
}
|
|
|
|
// HasUserStopwatch returns true if the user has a stopwatch
|
|
func HasUserStopwatch(ctx context.Context, userID int64) (exists bool, sw *Stopwatch, issue *Issue, err error) {
|
|
type stopwatchIssueRepo struct {
|
|
Stopwatch `xorm:"extends"`
|
|
Issue `xorm:"extends"`
|
|
repo.Repository `xorm:"extends"`
|
|
}
|
|
|
|
swIR := new(stopwatchIssueRepo)
|
|
exists, err = db.GetEngine(ctx).
|
|
Table("stopwatch").
|
|
Where("user_id = ?", userID).
|
|
Join("INNER", "issue", "issue.id = stopwatch.issue_id").
|
|
Join("INNER", "repository", "repository.id = issue.repo_id").
|
|
Get(swIR)
|
|
if exists {
|
|
sw = &swIR.Stopwatch
|
|
issue = &swIR.Issue
|
|
issue.Repo = &swIR.Repository
|
|
}
|
|
return exists, sw, issue, err
|
|
}
|
|
|
|
// FinishIssueStopwatch if stopwatch exists, then finish it.
|
|
func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) (ok bool, err error) {
|
|
sw, exists, err := getStopwatch(ctx, user.ID, issue.ID)
|
|
if err != nil {
|
|
return false, err
|
|
} else if !exists {
|
|
return false, nil
|
|
}
|
|
if err = finishIssueStopwatch(ctx, user, issue, sw); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func finishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue, sw *Stopwatch) error {
|
|
// Create tracked time out of the time difference between start date and actual date
|
|
timediff := time.Now().Unix() - int64(sw.CreatedUnix)
|
|
|
|
// Create TrackedTime
|
|
tt := &TrackedTime{
|
|
Created: time.Now(),
|
|
IssueID: issue.ID,
|
|
UserID: user.ID,
|
|
Time: timediff,
|
|
}
|
|
|
|
if err := issue.LoadRepo(ctx); err != nil {
|
|
return err
|
|
}
|
|
if err := db.Insert(ctx, tt); err != nil {
|
|
return err
|
|
}
|
|
if _, err := CreateComment(ctx, &CreateCommentOptions{
|
|
Doer: user,
|
|
Issue: issue,
|
|
Repo: issue.Repo,
|
|
Content: util.SecToHours(timediff),
|
|
Type: CommentTypeStopTracking,
|
|
TimeID: tt.ID,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
_, err := db.DeleteByBean(ctx, sw)
|
|
return err
|
|
}
|
|
|
|
// CreateIssueStopwatch creates a stopwatch if the issue doesn't have the user's stopwatch.
|
|
// It also stops any other stopwatch that might be running for the user.
|
|
func CreateIssueStopwatch(ctx context.Context, user *user_model.User, issue *Issue) (ok bool, err error) {
|
|
{ // if another issue's stopwatch is running: stop it; if this issue has a stopwatch: return an error.
|
|
exists, otherStopWatch, otherIssue, err := HasUserStopwatch(ctx, user.ID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if exists {
|
|
if otherStopWatch.IssueID == issue.ID {
|
|
// don't allow starting stopwatch for the same issue
|
|
return false, nil
|
|
}
|
|
// stop the other issue's stopwatch
|
|
if err = finishIssueStopwatch(ctx, user, otherIssue, otherStopWatch); err != nil {
|
|
return false, err
|
|
}
|
|
}
|
|
}
|
|
|
|
if err = issue.LoadRepo(ctx); err != nil {
|
|
return false, err
|
|
}
|
|
if err = db.Insert(ctx, &Stopwatch{UserID: user.ID, IssueID: issue.ID}); err != nil {
|
|
return false, err
|
|
}
|
|
if _, err = CreateComment(ctx, &CreateCommentOptions{
|
|
Doer: user,
|
|
Issue: issue,
|
|
Repo: issue.Repo,
|
|
Type: CommentTypeStartTracking,
|
|
}); err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// CancelStopwatch removes the given stopwatch and logs it into issue's timeline.
|
|
func CancelStopwatch(ctx context.Context, user *user_model.User, issue *Issue) (ok bool, err error) {
|
|
err = db.WithTx(ctx, func(ctx context.Context) error {
|
|
e := db.GetEngine(ctx)
|
|
sw, exists, err := getStopwatch(ctx, user.ID, issue.ID)
|
|
if err != nil {
|
|
return err
|
|
} else if !exists {
|
|
return nil
|
|
}
|
|
|
|
if err = issue.LoadRepo(ctx); err != nil {
|
|
return err
|
|
}
|
|
if _, err = e.Delete(sw); err != nil {
|
|
return err
|
|
}
|
|
if _, err = CreateComment(ctx, &CreateCommentOptions{
|
|
Doer: user,
|
|
Issue: issue,
|
|
Repo: issue.Repo,
|
|
Type: CommentTypeCancelTracking,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
ok = true
|
|
return nil
|
|
})
|
|
return ok, err
|
|
}
|
|
|
|
// RemoveStopwatchesByRepoID removes all stopwatches for a user in a specific repository
|
|
// this function should be called before removing all the issues of the repository
|
|
func RemoveStopwatchesByRepoID(ctx context.Context, userID, repoID int64) error {
|
|
_, err := db.GetEngine(ctx).
|
|
Where("`stopwatch`.user_id = ?", userID).
|
|
And(builder.In("`stopwatch`.issue_id",
|
|
builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repoID}))).
|
|
Delete(new(Stopwatch))
|
|
return err
|
|
}
|