Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue time estimate, meaningful time tracking #23113

Open
wants to merge 104 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
1363205
Commit
Feb 24, 2023
d11ba9f
Commit
Feb 24, 2023
c783692
Commit
Feb 24, 2023
a8778f4
Commit
Feb 24, 2023
d944662
Commit
Feb 24, 2023
7ed86ff
Commit
Feb 24, 2023
c248042
Commit
Feb 24, 2023
d75b7ac
Commit
Feb 24, 2023
4be8c50
Commit
Feb 24, 2023
5f3edad
Update services/issue/issue.go
stuzer05 Feb 24, 2023
f7a4c9e
Update services/issue/issue.go
stuzer05 Feb 24, 2023
e187364
Commit
Feb 24, 2023
f33b0a0
Commit
Feb 24, 2023
4e1aed8
Merge branch 'main' into add-issue-planned-time
stuzer05 Feb 24, 2023
2fc2f63
Commit
Feb 24, 2023
b062fc9
Commit
Feb 24, 2023
870bb92
Commit
Feb 24, 2023
d247b0f
Commit
Feb 24, 2023
09c05e8
Commit
Feb 24, 2023
0f5b609
Commit
Feb 24, 2023
11b9719
Commit
Feb 24, 2023
1b7ba41
Commit
Feb 24, 2023
f7427d8
Commit
Feb 24, 2023
1cff1a9
Commit
Feb 24, 2023
e155495
Commit
Feb 24, 2023
f463765
Commit
Feb 24, 2023
7a57044
Commit
Feb 24, 2023
79f507b
Commit
Feb 24, 2023
5c4dc87
Commit
Feb 24, 2023
0ab85af
Commit
Feb 24, 2023
5eea230
Commit
Feb 24, 2023
db3c697
Commit
Feb 24, 2023
775c663
Merge branch 'main' into add-issue-planned-time
stuzer05 Feb 24, 2023
8750870
Commit
Feb 24, 2023
29dd617
Commit
Feb 24, 2023
7be748f
Commit
Feb 24, 2023
b9cdc7c
Commit
Feb 26, 2023
8c0bf88
Merge branch 'main' into add-issue-planned-time
stuzer05 Feb 27, 2023
e20e23b
Commit
stuzer05 Feb 27, 2023
37e8e8d
Commit
stuzer05 Feb 27, 2023
8ef3a47
Commit
stuzer05 Feb 27, 2023
49a176d
Commit
stuzer05 Feb 28, 2023
79e18c6
Commit
stuzer05 Feb 28, 2023
deddce5
Commit
stuzer05 Feb 28, 2023
fb81260
Update models/issues/issue.go
stuzer05 Feb 28, 2023
bf323cf
Commit
stuzer05 Feb 28, 2023
57a3664
Commit
stuzer05 Feb 28, 2023
fd5adc5
Merge branch 'main' into add-issue-planned-time
stuzer05 Feb 28, 2023
879d96f
Merge branch 'main' into add-issue-planned-time
stuzer05 Mar 4, 2023
fc93006
Merge branch 'main' into add-issue-planned-time
stuzer05 Apr 18, 2023
bf4fa11
Merge branch 'main' into add-issue-planned-time
stuzer05 Jun 3, 2023
40e1373
Merge branch 'main' into add-issue-planned-time
stuzer05 Jun 6, 2023
e9afd60
Refactor helper functions
stuzer05 Jun 11, 2023
3924cb0
Merge branch 'go-gitea:main' into add-issue-planned-time
stuzer05 Jun 11, 2023
748bd67
Fix displaying issue estimation
stuzer05 Jun 14, 2023
015ad01
Merge branch 'main' into add-issue-planned-time
stuzer05 Jun 18, 2023
cef496a
Remove unused code
stuzer05 Jun 18, 2023
ffaa4ba
Format
stuzer05 Jun 18, 2023
b211b9e
Merge branch 'main' into add-issue-planned-time
silverwind Jun 18, 2023
598e2d5
Merge branch 'main' into add-issue-planned-time
stuzer05 Jun 19, 2023
3310440
Hide time tracking
stuzer05 Jun 23, 2023
721069d
Merge branch 'main' into add-issue-planned-time
stuzer05 Jun 23, 2023
a45c1e9
Merge branch 'main' into add-issue-planned-time
stuzer05 Jun 24, 2023
db49783
Commit
stuzer05 Jun 24, 2023
e933a89
Delete serviceworker.js
stuzer05 Jun 24, 2023
62094d8
Merge branch 'main' into add-issue-planned-time
stuzer05 Jun 26, 2023
fa662ec
Merge branch 'main' into add-issue-planned-time
stuzer05 Jul 10, 2023
64de74d
Merge branch 'main' into add-issue-planned-time
stuzer05 Sep 11, 2023
349b959
Merge branch 'main' into add-issue-planned-time
stuzer05 Oct 7, 2023
3037d6c
Merge branch 'main' into add-issue-planned-time
stuzer05 Oct 8, 2023
2a9009f
Merge branch 'main' into add-issue-planned-time
stuzer05 Oct 16, 2023
a6fa4c3
Merge branch 'main' into add-issue-planned-time
stuzer05 Jan 15, 2024
a999055
Merge branch 'main' into add-issue-planned-time
stuzer05 Jan 17, 2024
39b8b19
Merge branch 'main' into add-issue-planned-time
stuzer05 Jan 22, 2024
92dc2cd
Merge branch 'main' into add-issue-planned-time
stuzer05 Feb 11, 2024
a737a8c
Merge branch 'main' into add-issue-planned-time
stuzer05 Feb 19, 2024
bb5ca4c
Merge branch 'main' into add-issue-planned-time
stuzer05 Feb 28, 2024
805af19
Merge branch 'main' into add-issue-planned-time
stuzer05 Mar 4, 2024
0c4b2df
Commit
stuzer05 Mar 4, 2024
c272512
Merge branch 'main' into add-issue-planned-time
stuzer05 Mar 4, 2024
09723c5
Merge branch 'main' into add-issue-planned-time
stuzer05 Mar 17, 2024
0820db0
Merge branch 'main' into add-issue-planned-time
stuzer05 Mar 28, 2024
93d09fa
Merge branch 'main' into add-issue-planned-time
stuzer05 Mar 29, 2024
1e22cc2
Merge branch 'main' into add-issue-planned-time
stuzer05 Apr 13, 2024
e92bedc
Commit
stuzer05 Apr 13, 2024
b452e13
Commit
stuzer05 Apr 13, 2024
b97139b
Merge branch 'main' into add-issue-planned-time
stuzer05 Apr 15, 2024
12553d7
Merge branch 'main' into add-issue-planned-time
stuzer05 Apr 22, 2024
0a8fd35
Merge branch 'main' into add-issue-planned-time
stuzer05 Apr 25, 2024
8c92f46
Merge branch 'main' into add-issue-planned-time
stuzer05 Apr 30, 2024
0c44cf7
Commit
stuzer05 Apr 30, 2024
a71b457
Commit
stuzer05 May 2, 2024
52d62fb
Merge branch 'main' into add-issue-planned-time
stuzer05 May 17, 2024
ff60376
Merge branch 'main' into add-issue-planned-time
stuzer05 May 30, 2024
be18b73
Merge branch 'main' into add-issue-planned-time
stuzer05 Jun 17, 2024
84d9dee
Merge branch 'main' into add-issue-planned-time
6543 Jul 5, 2024
823d67f
pass context down
6543 Jul 5, 2024
4ff958b
Merge branch 'main' into add-issue-planned-time
stuzer05 Jul 8, 2024
3499c44
Use RenderedContent
stuzer05 Jul 8, 2024
bf7fcba
Merge branch 'main' into add-issue-planned-time
stuzer05 Aug 1, 2024
392fa48
Merge branch 'main' into add-issue-planned-time
stuzer05 Aug 26, 2024
66b2c4a
Merge branch 'main' into add-issue-planned-time
stuzer05 Oct 9, 2024
10767d7
Merge branch 'main' into add-issue-planned-time
stuzer05 Nov 22, 2024
b24c958
Commit
stuzer05 Nov 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions models/issues/comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ const (
CommentTypePRScheduledToAutoMerge
// 35 pr was un scheduled to auto merge when checks succeed
CommentTypePRUnScheduledToAutoMerge
// 36 Change time estimate
CommentTypeChangeTimeEstimate
)

var commentStrings = []string{
Expand Down Expand Up @@ -169,6 +171,7 @@ var commentStrings = []string{
"change_issue_ref",
"pull_scheduled_merge",
"pull_cancel_scheduled_merge",
"change_time_estimate",
}

func (t CommentType) String() string {
Expand Down Expand Up @@ -244,6 +247,7 @@ type Comment struct {
Milestone *Milestone `xorm:"-"`
TimeID int64
Time *TrackedTime `xorm:"-"`
TimeTracked int64 `xorm:"NOT NULL DEFAULT 0"`
AssigneeID int64
RemovedAssignee bool
Assignee *user_model.User `xorm:"-"`
Expand Down Expand Up @@ -301,6 +305,8 @@ type Comment struct {
NewCommit string `xorm:"-"`
CommitsNum int64 `xorm:"-"`
IsForcePush bool `xorm:"-"`

TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
}

func init() {
Expand Down Expand Up @@ -797,6 +803,7 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment,
OldProjectID: opts.OldProjectID,
ProjectID: opts.ProjectID,
TimeID: opts.TimeID,
TimeTracked: opts.TimeTracked,
RemovedAssignee: opts.RemovedAssignee,
AssigneeID: opts.AssigneeID,
AssigneeTeamID: opts.AssigneeTeamID,
Expand All @@ -819,6 +826,7 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment,
RefIsPull: opts.RefIsPull,
IsForcePush: opts.IsForcePush,
Invalidated: opts.Invalidated,
TimeEstimate: opts.TimeEstimate,
}
if _, err = e.Insert(comment); err != nil {
return nil, err
Expand Down Expand Up @@ -968,6 +976,7 @@ type CreateCommentOptions struct {
OldProjectID int64
ProjectID int64
TimeID int64
TimeTracked int64
AssigneeID int64
AssigneeTeamID int64
RemovedAssignee bool
Expand All @@ -990,6 +999,7 @@ type CreateCommentOptions struct {
RefIsPull bool
IsForcePush bool
Invalidated bool
TimeEstimate int64
}

// GetCommentByID returns the comment by given ID.
Expand Down
121 changes: 121 additions & 0 deletions models/issues/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ package issues
import (
"context"
"fmt"
"math"
"regexp"
"sort"
"strconv"
"strings"

"code.gitea.io/gitea/models/db"
Expand Down Expand Up @@ -146,6 +148,9 @@ type Issue struct {

// For view issue page.
ShowRole RoleDescriptor `xorm:"-"`

// Time estimate
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
}

var (
Expand Down Expand Up @@ -774,6 +779,37 @@ func ChangeIssueTitle(issue *Issue, doer *user_model.User, oldTitle string) (err
return committer.Commit()
}

// ChangeIssueTimeEstimate changes the plan time of this issue, as the given user.
func ChangeIssueTimeEstimate(issue *Issue, doer *user_model.User, timeEstimate int64) (err error) {
ctx, committer, err := db.TxContext(db.DefaultContext)
if err != nil {
return err
}
defer committer.Close()

if err = UpdateIssueCols(ctx, &Issue{ID: issue.ID, TimeEstimate: timeEstimate}, "time_estimate"); err != nil {
return fmt.Errorf("updateIssueCols: %w", err)
}

if err = issue.LoadRepo(ctx); err != nil {
return fmt.Errorf("loadRepo: %w", err)
}

opts := &CreateCommentOptions{
Type: CommentTypeChangeTimeEstimate,
Doer: doer,
Repo: issue.Repo,
Issue: issue,
Content: util.SecToTime(timeEstimate),
TimeEstimate: timeEstimate,
}
if _, err = CreateComment(ctx, opts); err != nil {
return fmt.Errorf("createComment: %w", err)
}

return committer.Commit()
}

// ChangeIssueRef changes the branch of this issue, as the given user.
func ChangeIssueRef(issue *Issue, doer *user_model.User, oldRef string) (err error) {
ctx, committer, err := db.TxContext(db.DefaultContext)
Expand Down Expand Up @@ -2498,3 +2534,88 @@ func DeleteOrphanedIssues(ctx context.Context) error {
func (issue *Issue) HasOriginalAuthor() bool {
return issue.OriginalAuthor != "" && issue.OriginalAuthorID != 0
}

// TimeEstimateFromStr returns time estimate in seconds from formatted string
func (issue *Issue) TimeEstimateFromStr(timeStr string) int64 {
stuzer05 marked this conversation as resolved.
Show resolved Hide resolved
timeTotal := 0

// Time match regex
rOnlyHours := regexp.MustCompile(`^([\d]+)$`)
rWeeks := regexp.MustCompile(`([\d]+)w`)
rDays := regexp.MustCompile(`([\d]+)d`)
rHours := regexp.MustCompile(`([\d]+)h`)
rMinutes := regexp.MustCompile(`([\d]+)m`)
stuzer05 marked this conversation as resolved.
Show resolved Hide resolved

// If single number entered, assume hours
timeStrMatches := rOnlyHours.FindStringSubmatch(timeStr)
if len(timeStrMatches) > 0 {
raw, _ := strconv.Atoi(timeStrMatches[1])
timeTotal += raw * (60 * 60)
} else {
// Find time weeks
timeStrMatches = rWeeks.FindStringSubmatch(timeStr)
if len(timeStrMatches) > 0 {
raw, _ := strconv.Atoi(timeStrMatches[1])
timeTotal += raw * (60 * 60 * 24 * 7)
}

// Find time days
timeStrMatches = rDays.FindStringSubmatch(timeStr)
if len(timeStrMatches) > 0 {
raw, _ := strconv.Atoi(timeStrMatches[1])
timeTotal += raw * (60 * 60 * 24)
}

// Find time hours
timeStrMatches = rHours.FindStringSubmatch(timeStr)
if len(timeStrMatches) > 0 {
raw, _ := strconv.Atoi(timeStrMatches[1])
timeTotal += raw * (60 * 60)
}

// Find time minutes
timeStrMatches = rMinutes.FindStringSubmatch(timeStr)
if len(timeStrMatches) > 0 {
raw, _ := strconv.Atoi(timeStrMatches[1])
timeTotal += raw * (60)
}
}

return int64(timeTotal)
}

// TimeEstimateStr returns formatted time estimate string from seconds (e.g. "2w 4d 12h 5m")
func (issue *Issue) TimeEstimateToStr() string {
var timeParts []string

timeSeconds := float64(issue.TimeEstimate)

// Format weeks
weeks := math.Floor(timeSeconds / (60 * 60 * 24 * 7))
if weeks > 0 {
timeParts = append(timeParts, fmt.Sprintf("%dw", int64(weeks)))
}
timeSeconds -= weeks * (60 * 60 * 24 * 7)

// Format days
days := math.Floor(timeSeconds / (60 * 60 * 24))
if days > 0 {
timeParts = append(timeParts, fmt.Sprintf("%dd", int64(days)))
}
timeSeconds -= days * (60 * 60 * 24)

// Format hours
hours := math.Floor(timeSeconds / (60 * 60))
if hours > 0 {
timeParts = append(timeParts, fmt.Sprintf("%dh", int64(hours)))
}
timeSeconds -= hours * (60 * 60)

// Format minutes
minutes := math.Floor(timeSeconds / (60))
if minutes > 0 {
timeParts = append(timeParts, fmt.Sprintf("%dm", int64(minutes)))
}

return strings.Join(timeParts, " ")
}
12 changes: 6 additions & 6 deletions models/issues/stopwatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,12 +197,12 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss
}

if _, err := CreateComment(ctx, &CreateCommentOptions{
Doer: user,
Issue: issue,
Repo: issue.Repo,
Content: util.SecToTime(timediff),
Type: CommentTypeStopTracking,
TimeID: tt.ID,
Doer: user,
Issue: issue,
Repo: issue.Repo,
Type: CommentTypeStopTracking,
TimeID: tt.ID,
TimeTracked: tt.Time,
}); err != nil {
return err
}
Expand Down
39 changes: 22 additions & 17 deletions models/issues/tracked_time.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,12 @@ func AddTime(user *user_model.User, issue *Issue, amount int64, created time.Tim
}

if _, err := CreateComment(ctx, &CreateCommentOptions{
Issue: issue,
Repo: issue.Repo,
Doer: user,
Content: util.SecToTime(amount),
Type: CommentTypeAddTimeManual,
TimeID: t.ID,
Issue: issue,
Repo: issue.Repo,
Doer: user,
Type: CommentTypeAddTimeManual,
TimeID: t.ID,
TimeTracked: t.Time,
}); err != nil {
return nil, err
}
Expand Down Expand Up @@ -221,7 +221,12 @@ func TotalTimes(options *FindTrackedTimesOptions) (map[*user_model.User]string,
}
return nil, err
}
totalTimes[user] = util.SecToTime(total)

if total < 60 {
totalTimes[user] = util.SecToTimeExact(total, true)
} else {
totalTimes[user] = util.SecToTimeExact(total, false)
}
}
return totalTimes, nil
}
Expand Down Expand Up @@ -251,11 +256,11 @@ func DeleteIssueUserTimes(issue *Issue, user *user_model.User) error {
return err
}
if _, err := CreateComment(ctx, &CreateCommentOptions{
Issue: issue,
Repo: issue.Repo,
Doer: user,
Content: "- " + util.SecToTime(removedTime),
Type: CommentTypeDeleteTimeManual,
Issue: issue,
Repo: issue.Repo,
Doer: user,
TimeTracked: removedTime,
Type: CommentTypeDeleteTimeManual,
}); err != nil {
return err
}
Expand All @@ -280,11 +285,11 @@ func DeleteTime(t *TrackedTime) error {
}

if _, err := CreateComment(ctx, &CreateCommentOptions{
Issue: t.Issue,
Repo: t.Issue.Repo,
Doer: t.User,
Content: "- " + util.SecToTime(t.Time),
Type: CommentTypeDeleteTimeManual,
Issue: t.Issue,
Repo: t.Issue.Repo,
Doer: t.User,
TimeTracked: t.Time,
Type: CommentTypeDeleteTimeManual,
}); err != nil {
return err
}
Expand Down
4 changes: 2 additions & 2 deletions models/issues/tracked_time_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestAddTime(t *testing.T) {
assert.Equal(t, int64(3661), tt.Time)

comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{Type: issues_model.CommentTypeAddTimeManual, PosterID: 3, IssueID: 1})
assert.Equal(t, comment.Content, "1 hour 1 minute")
assert.Equal(t, comment.TimeTracked, int64(3661))
}

func TestGetTrackedTimes(t *testing.T) {
Expand Down Expand Up @@ -87,7 +87,7 @@ func TestTotalTimes(t *testing.T) {
assert.Len(t, total, 1)
for user, time := range total {
assert.Equal(t, int64(1), user.ID)
assert.Equal(t, "6 minutes 40 seconds", time)
assert.Equal(t, "6 minutes", time)
}

total, err = issues_model.TotalTimes(&issues_model.FindTrackedTimesOptions{IssueID: 2})
Expand Down
4 changes: 4 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,10 @@ var migrations = []Migration{

// v244 -> v245
NewMigration("Add NeedApproval to actions tables", v1_20.AddNeedApprovalToActionRun),

// v245 -> v246
NewMigration("Add TimeEstimate to issue table", v1_20.AddTimeEstimateColumnToIssueTable),
NewMigration("Add TimeTracked, TimeEstimate to comment table", v1_20.AddColumnsToCommentTable),
}

// GetCurrentDBVersion returns the current db version
Expand Down
25 changes: 25 additions & 0 deletions models/migrations/v1_20/v245.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package v1_20 //nolint

import (
"xorm.io/xorm"
)

func AddTimeEstimateColumnToIssueTable(x *xorm.Engine) error {
type Issue struct {
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
}

return x.Sync(new(Issue))
}

func AddColumnsToCommentTable(x *xorm.Engine) error {
type Comment struct {
TimeTracked int64 `xorm:"NOT NULL DEFAULT 0"`
TimeEstimate int64 `xorm:"NOT NULL DEFAULT 0"`
}

return x.Sync(new(Comment))
}
2 changes: 2 additions & 0 deletions modules/structs/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type Issue struct {
Attachments []*Attachment `json:"assets"`
Labels []*Label `json:"labels"`
Milestone *Milestone `json:"milestone"`
TimeEstimate int `json:"time_estimate"`
// deprecated
Assignee *User `json:"assignee"`
Assignees []*User `json:"assignees"`
Expand Down Expand Up @@ -108,6 +109,7 @@ type EditIssueOption struct {
// swagger:strfmt date-time
Deadline *time.Time `json:"due_date"`
RemoveDeadline *bool `json:"unset_due_date"`
TimeEstimate *string `json:"time_estimate"`
}

// EditDeadlineOption options for creating a deadline
Expand Down
14 changes: 8 additions & 6 deletions modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,9 +278,10 @@ func NewFuncMap() []template.FuncMap {
}
return dict, nil
},
"Printf": fmt.Sprintf,
"Escape": Escape,
"Sec2Time": util.SecToTime,
"Printf": fmt.Sprintf,
"Escape": Escape,
"Sec2Time": util.SecToTime,
"SecToTimeExact": util.SecToTimeExact,
"ParseDeadline": func(deadline string) []string {
return strings.Split(deadline, "|")
},
Expand Down Expand Up @@ -549,9 +550,10 @@ func NewTextFuncMap() []texttmpl.FuncMap {
}
return dict, nil
},
"Printf": fmt.Sprintf,
"Escape": Escape,
"Sec2Time": util.SecToTime,
"Printf": fmt.Sprintf,
"Escape": Escape,
"Sec2Time": util.SecToTime,
"SecToTimeExact": util.SecToTimeExact,
"ParseDeadline": func(deadline string) []string {
return strings.Split(deadline, "|")
},
Expand Down
Loading