mirror of https://github.com/go-gitea/gitea.git
Issue time estimate, meaningful time tracking (#23113)
Redesign the time tracker side bar, and add "time estimate" support (in "1d 2m" format) Closes #23112 --------- Co-authored-by: stuzer05 <stuzer05@gmail.com> Co-authored-by: Yarden Shoham <hrsi88@gmail.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>pull/32719/head^2
parent
c5422fae9a
commit
936665bf85
@ -0,0 +1,16 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_23 //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))
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
// Copyright 2024 Gitea. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type timeStrGlobalVarsType struct {
|
||||
units []struct {
|
||||
name string
|
||||
num int64
|
||||
}
|
||||
re *regexp.Regexp
|
||||
}
|
||||
|
||||
// When tracking working time, only hour/minute/second units are accurate and could be used.
|
||||
// For other units like "day", it depends on "how many working hours in a day": 6 or 7 or 8?
|
||||
// So at the moment, we only support hour/minute/second units.
|
||||
// In the future, it could be some configurable options to help users
|
||||
// to convert the working time to different units.
|
||||
|
||||
var timeStrGlobalVars = sync.OnceValue[*timeStrGlobalVarsType](func() *timeStrGlobalVarsType {
|
||||
v := &timeStrGlobalVarsType{}
|
||||
v.re = regexp.MustCompile(`(?i)(\d+)\s*([hms])`)
|
||||
v.units = []struct {
|
||||
name string
|
||||
num int64
|
||||
}{
|
||||
{"h", 60 * 60},
|
||||
{"m", 60},
|
||||
{"s", 1},
|
||||
}
|
||||
return v
|
||||
})
|
||||
|
||||
func TimeEstimateParse(timeStr string) (int64, error) {
|
||||
if timeStr == "" {
|
||||
return 0, nil
|
||||
}
|
||||
var total int64
|
||||
matches := timeStrGlobalVars().re.FindAllStringSubmatchIndex(timeStr, -1)
|
||||
if len(matches) == 0 {
|
||||
return 0, fmt.Errorf("invalid time string: %s", timeStr)
|
||||
}
|
||||
if matches[0][0] != 0 || matches[len(matches)-1][1] != len(timeStr) {
|
||||
return 0, fmt.Errorf("invalid time string: %s", timeStr)
|
||||
}
|
||||
for _, match := range matches {
|
||||
amount, err := strconv.ParseInt(timeStr[match[2]:match[3]], 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid time string: %v", err)
|
||||
}
|
||||
unit := timeStr[match[4]:match[5]]
|
||||
found := false
|
||||
for _, u := range timeStrGlobalVars().units {
|
||||
if strings.ToLower(unit) == u.name {
|
||||
total += amount * u.num
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return 0, fmt.Errorf("invalid time unit: %s", unit)
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func TimeEstimateString(amount int64) string {
|
||||
var timeParts []string
|
||||
for _, u := range timeStrGlobalVars().units {
|
||||
if amount >= u.num {
|
||||
num := amount / u.num
|
||||
amount %= u.num
|
||||
timeParts = append(timeParts, fmt.Sprintf("%d%s", num, u.name))
|
||||
}
|
||||
}
|
||||
return strings.Join(timeParts, " ")
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
// Copyright 2024 Gitea. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestTimeStr(t *testing.T) {
|
||||
t.Run("Parse", func(t *testing.T) {
|
||||
// Test TimeEstimateParse
|
||||
tests := []struct {
|
||||
input string
|
||||
output int64
|
||||
err bool
|
||||
}{
|
||||
{"1h", 3600, false},
|
||||
{"1m", 60, false},
|
||||
{"1s", 1, false},
|
||||
{"1h 1m 1s", 3600 + 60 + 1, false},
|
||||
{"1d1x", 0, true},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.input, func(t *testing.T) {
|
||||
output, err := TimeEstimateParse(test.input)
|
||||
if test.err {
|
||||
assert.NotNil(t, err)
|
||||
} else {
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
assert.Equal(t, test.output, output)
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("String", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
input int64
|
||||
output string
|
||||
}{
|
||||
{3600, "1h"},
|
||||
{60, "1m"},
|
||||
{1, "1s"},
|
||||
{3600 + 1, "1h 1s"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.output, func(t *testing.T) {
|
||||
output := TimeEstimateString(test.input)
|
||||
assert.Equal(t, test.output, output)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue