Skip to content

Commit 7dff8db

Browse files
committed
fix #76 and support #58: accept author date, strict ISO 8601 format, and try to use git to fetch latest commit date
1 parent e036db7 commit 7dff8db

File tree

21 files changed

+871
-350
lines changed

21 files changed

+871
-350
lines changed

internal/command/github/contribution.go

Lines changed: 19 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"strconv"
77
"strings"
8+
"time"
89

910
"github.com/spf13/cobra"
1011

@@ -13,7 +14,7 @@ import (
1314
"go.octolab.org/toolset/maintainer/internal/config"
1415
"go.octolab.org/toolset/maintainer/internal/model/github/contribution"
1516
"go.octolab.org/toolset/maintainer/internal/pkg/http"
16-
"go.octolab.org/toolset/maintainer/internal/pkg/time"
17+
xtime "go.octolab.org/toolset/maintainer/internal/pkg/time"
1718
"go.octolab.org/toolset/maintainer/internal/pkg/unsafe"
1819
"go.octolab.org/toolset/maintainer/internal/service/github"
1920
)
@@ -66,7 +67,7 @@ func Contribution(cnf *config.Tool) *cobra.Command {
6667
RunE: func(cmd *cobra.Command, args []string) error {
6768
// dependencies and defaults
6869
service := github.New(http.TokenSourcedClient(cmd.Context(), cnf.Token))
69-
construct, date := time.RangeByWeeks, time.Now().UTC()
70+
construct, date := xtime.RangeByWeeks, time.Now().UTC()
7071
zero := unsafe.ReturnBool(cmd.Flags().GetBool("with-zero"))
7172

7273
// input validation: date(year,+month,+week{day})
@@ -80,14 +81,14 @@ func Contribution(cnf *config.Tool) *cobra.Command {
8081
}
8182

8283
switch input := args[0]; len(input) {
83-
case len(time.RFC3339Year):
84-
date, err = time.Parse(time.RFC3339Year, input)
85-
construct = time.RangeByYears
86-
case len(time.RFC3339Month):
87-
date, err = time.Parse(time.RFC3339Month, input)
88-
construct = time.RangeByMonths
89-
case len(time.RFC3339Day):
90-
date, err = time.Parse(time.RFC3339Day, input)
84+
case len(xtime.RFC3339Year):
85+
date, err = time.Parse(xtime.RFC3339Year, input)
86+
construct = xtime.RangeByYears
87+
case len(xtime.RFC3339Month):
88+
date, err = time.Parse(xtime.RFC3339Month, input)
89+
construct = xtime.RangeByMonths
90+
case len(xtime.RFC3339Day):
91+
date, err = time.Parse(xtime.RFC3339Day, input)
9192
default:
9293
err = fmt.Errorf("unsupported format")
9394
}
@@ -170,7 +171,7 @@ func Contribution(cnf *config.Tool) *cobra.Command {
170171
fallthrough
171172
case 1:
172173
if raw[0] != "now" && raw[0] != "" {
173-
date, err = time.Parse(time.RFC3339Day, raw[0])
174+
date, err = time.Parse(xtime.RFC3339Day, raw[0])
174175
}
175176
if err != nil {
176177
return wrap(err)
@@ -181,7 +182,7 @@ func Contribution(cnf *config.Tool) *cobra.Command {
181182
}
182183

183184
// data provisioning
184-
scope := time.RangeByWeeks(date, weeks, half).Shift(-time.Day).ExcludeFuture()
185+
scope := xtime.RangeByWeeks(date, weeks, half).Shift(-xtime.Day).ExcludeFuture()
185186
chm, err := service.ContributionHeatMap(cmd.Context(), scope)
186187
if err != nil {
187188
return err
@@ -209,7 +210,7 @@ func Contribution(cnf *config.Tool) *cobra.Command {
209210
RunE: func(cmd *cobra.Command, args []string) error {
210211
// dependencies and defaults
211212
service := github.New(http.TokenSourcedClient(cmd.Context(), cnf.Token))
212-
date := time.TruncateToYear(time.Now().UTC())
213+
date := xtime.TruncateToYear(time.Now().UTC())
213214

214215
// input validation: date(year)
215216
if len(args) == 1 {
@@ -222,8 +223,8 @@ func Contribution(cnf *config.Tool) *cobra.Command {
222223
}
223224

224225
switch input := args[0]; len(input) {
225-
case len(time.RFC3339Year):
226-
date, err = time.Parse(time.RFC3339Year, input)
226+
case len(xtime.RFC3339Year):
227+
date, err = time.Parse(xtime.RFC3339Year, input)
227228
default:
228229
err = fmt.Errorf("unsupported format")
229230
}
@@ -233,7 +234,7 @@ func Contribution(cnf *config.Tool) *cobra.Command {
233234
}
234235

235236
// data provisioning
236-
scope := time.RangeByYears(date, 0, false).ExcludeFuture()
237+
scope := xtime.RangeByYears(date, 0, false).ExcludeFuture()
237238
chm, err := service.ContributionHeatMap(cmd.Context(), scope)
238239
if err != nil {
239240
return err
@@ -267,85 +268,11 @@ func Contribution(cnf *config.Tool) *cobra.Command {
267268
suggest := cobra.Command{
268269
Use: "suggest",
269270
Args: cobra.MaximumNArgs(1),
270-
RunE: func(cmd *cobra.Command, args []string) error {
271-
// dependencies and defaults
272-
service := github.New(http.TokenSourcedClient(cmd.Context(), cnf.Token))
273-
date, weeks, half := time.TruncateToYear(time.Now().UTC()), 5, false
274-
delta := unsafe.ReturnBool(cmd.Flags().GetBool("delta"))
275-
short := unsafe.ReturnBool(cmd.Flags().GetBool("short"))
276-
target := unsafe.ReturnUint(cmd.Flags().GetUint("target"))
277-
278-
// input validation: date(year,+month,+week{day})[/{+-}weeks]
279-
if len(args) == 1 {
280-
var err error
281-
wrap := func(err error) error {
282-
return fmt.Errorf(
283-
"please provide argument in format YYYY[-mm[-dd]][/[+|-]weeks], e.g., 2006-01: %w",
284-
fmt.Errorf("invalid argument %q: %w", args[0], err),
285-
)
286-
}
287-
288-
input := args[0]
289-
raw := strings.Split(input, "/")
290-
switch len(raw) {
291-
case 2:
292-
weeks, err = strconv.Atoi(raw[1])
293-
if err != nil {
294-
return wrap(err)
295-
}
296-
// +%d and positive %d have the same value, but different semantic
297-
// invariant: len(raw[1]) > 0, because weeks > 0 and invariant(time.RangeByWeeks)
298-
if weeks > 0 && raw[1][0] != '+' {
299-
half = true
300-
}
301-
fallthrough
302-
case 1:
303-
input = raw[0]
304-
default:
305-
return wrap(fmt.Errorf("too many parts"))
306-
}
307-
308-
switch len(input) {
309-
case len(time.RFC3339Year):
310-
date, err = time.Parse(time.RFC3339Year, input)
311-
case len(time.RFC3339Month):
312-
date, err = time.Parse(time.RFC3339Month, input)
313-
case len(time.RFC3339Day):
314-
date, err = time.Parse(time.RFC3339Day, input)
315-
default:
316-
err = fmt.Errorf("unsupported format")
317-
}
318-
if err != nil {
319-
return wrap(err)
320-
}
321-
}
322-
323-
// data provisioning
324-
scope := time.NewRange(
325-
time.RangeByWeeks(date, weeks, half).From(),
326-
time.Now().UTC(),
327-
)
328-
chm, err := service.ContributionHeatMap(cmd.Context(), scope)
329-
if err != nil {
330-
return err
331-
}
332-
333-
value := contribution.Suggest(chm, date, scope.To(), target)
334-
area := time.RangeByWeeks(value.Day, weeks, half).Shift(-time.Day) // start from prev Sunday
335-
data := contribution.HistogramByWeekday(chm.Subset(area), false)
336-
337-
// data presentation
338-
return view.Suggest(cmd, area, data, view.SuggestOption{
339-
Suggest: value,
340-
Current: chm[value.Day],
341-
Delta: delta,
342-
Short: short,
343-
})
344-
},
271+
RunE: exec.Contribution(cnf),
345272
}
346273
suggest.Flags().Bool("delta", false, "shows relatively")
347274
suggest.Flags().Bool("short", false, "shows only date")
348-
suggest.Flags().Int("target", 5, "minimum contributions")
275+
suggest.Flags().Uint("target", 5, "minimum contributions")
349276
cmd.AddCommand(&suggest)
350277

351278
return &cmd
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package exec
2+
3+
import (
4+
"time"
5+
6+
"github.com/spf13/cobra"
7+
8+
"go.octolab.org/toolset/maintainer/internal/command/github/view"
9+
"go.octolab.org/toolset/maintainer/internal/config"
10+
"go.octolab.org/toolset/maintainer/internal/model/github/contribution"
11+
"go.octolab.org/toolset/maintainer/internal/pkg/http"
12+
xtime "go.octolab.org/toolset/maintainer/internal/pkg/time"
13+
"go.octolab.org/toolset/maintainer/internal/pkg/unsafe"
14+
"go.octolab.org/toolset/maintainer/internal/service/github"
15+
)
16+
17+
// configure(cmd) -> setup flags, setup run
18+
19+
func Contribution(cnf *config.Tool) Runner {
20+
return func(cmd *cobra.Command, args []string) error {
21+
// dependencies and defaults
22+
service := github.New(http.TokenSourcedClient(cmd.Context(), cnf.Token))
23+
delta := unsafe.ReturnBool(cmd.Flags().GetBool("delta"))
24+
short := unsafe.ReturnBool(cmd.Flags().GetBool("short"))
25+
target := unsafe.ReturnUint(cmd.Flags().GetUint("target"))
26+
27+
// data provisioning
28+
opts, err := ParseDate(args, FallbackDate(args), 5)
29+
if err != nil {
30+
return err
31+
}
32+
33+
scope := xtime.RangeByWeeks(opts.Value, opts.Weeks, opts.Half).ExpandRight(time.Now().UTC())
34+
chm, err := service.ContributionHeatMap(cmd.Context(), scope)
35+
if err != nil {
36+
return err
37+
}
38+
39+
suggestion := contribution.Suggest(chm, scope.Base(), scope.To(), target)
40+
opts.Value = suggestion.Day
41+
area := contribution.LookupRange(opts)
42+
data := contribution.HistogramByWeekday(chm.Subset(area), false)
43+
44+
// data presentation
45+
return view.Suggest(cmd, area, data, view.SuggestOption{
46+
Suggest: suggestion,
47+
Current: chm[suggestion.Day],
48+
Delta: delta,
49+
Short: short,
50+
})
51+
}
52+
}

internal/command/github/exec/contribution_diff.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,24 @@ package exec
33
import (
44
"fmt"
55
"regexp"
6+
"time"
67

78
"github.com/spf13/cobra"
89

910
"go.octolab.org/toolset/maintainer/internal/command/github/run"
1011
"go.octolab.org/toolset/maintainer/internal/config"
1112
"go.octolab.org/toolset/maintainer/internal/model/github/contribution"
1213
"go.octolab.org/toolset/maintainer/internal/pkg/http"
13-
"go.octolab.org/toolset/maintainer/internal/pkg/time"
14+
xtime "go.octolab.org/toolset/maintainer/internal/pkg/time"
1415
"go.octolab.org/toolset/maintainer/internal/service/github"
1516
)
1617

1718
func ContributionDiff(cnf *config.Tool) Runner {
1819
isYear := regexp.MustCompile(`^\d{4}$`)
19-
wrap := func(err error, input string) error {
20+
wrap := func(err error, arg string) error {
2021
return fmt.Errorf(
2122
"please provide the argument in format YYYY, e.g., 2006: %w",
22-
fmt.Errorf("invalid argument %q: %w", input, err),
23+
fmt.Errorf("invalid argument %q: %w", arg, err),
2324
)
2425
}
2526

@@ -31,7 +32,7 @@ func ContributionDiff(cnf *config.Tool) Runner {
3132

3233
var base run.ContributionSource
3334
if input := args[0]; isYear.MatchString(input) {
34-
year, err := time.Parse(time.RFC3339Year, input)
35+
year, err := time.Parse(xtime.RFC3339Year, input)
3536
if err != nil {
3637
return wrap(err, input)
3738
}
@@ -43,7 +44,7 @@ func ContributionDiff(cnf *config.Tool) Runner {
4344

4445
var head run.ContributionSource
4546
if input := args[1]; isYear.MatchString(input) {
46-
year, err := time.Parse(time.RFC3339Year, input)
47+
year, err := time.Parse(xtime.RFC3339Year, input)
4748
if err != nil {
4849
return wrap(err, input)
4950
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package exec
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
"time"
8+
9+
"github.com/go-git/go-git/v5"
10+
11+
"go.octolab.org/toolset/maintainer/internal/model/github/contribution"
12+
xtime "go.octolab.org/toolset/maintainer/internal/pkg/time"
13+
)
14+
15+
func FallbackDate(args []string) time.Time {
16+
fallback := time.Now().UTC()
17+
if len(args) > 0 {
18+
return fallback
19+
}
20+
21+
repo, err := git.PlainOpenWithOptions("", &git.PlainOpenOptions{DetectDotGit: true})
22+
if err != nil {
23+
return fallback
24+
}
25+
head, err := repo.Head()
26+
if err != nil {
27+
return fallback
28+
}
29+
commit, err := repo.CommitObject(head.Hash())
30+
if err != nil {
31+
return fallback
32+
}
33+
return commit.Author.When.UTC()
34+
}
35+
36+
func ParseDate(
37+
args []string,
38+
defaultDate time.Time,
39+
defaultWeeks int,
40+
) (contribution.DateOptions, error) {
41+
// trick to skip length check
42+
args = append(args, "")
43+
44+
var (
45+
opts contribution.DateOptions
46+
err error
47+
)
48+
var rawDate, rawWeeks string
49+
raw := strings.Split(args[0], "/")
50+
switch len(raw) {
51+
case 2:
52+
rawDate, rawWeeks = raw[0], raw[1]
53+
case 1:
54+
rawDate, rawWeeks = raw[0], ""
55+
default:
56+
return opts, fmt.Errorf("too many parts")
57+
}
58+
59+
var date time.Time
60+
switch len(rawDate) {
61+
case 20, len(time.RFC3339):
62+
date, err = time.Parse(time.RFC3339, rawDate)
63+
case len(xtime.RFC3339Day):
64+
date, err = time.Parse(xtime.RFC3339Day, rawDate)
65+
case len(xtime.RFC3339Month):
66+
date, err = time.Parse(xtime.RFC3339Month, rawDate)
67+
case len(xtime.RFC3339Year):
68+
date, err = time.Parse(xtime.RFC3339Year, rawDate)
69+
case 0:
70+
date = defaultDate
71+
default:
72+
err = fmt.Errorf("unsupported format")
73+
}
74+
if err != nil {
75+
return opts, fmt.Errorf("parse date %q: %w", rawDate, err)
76+
}
77+
opts.Value = date
78+
79+
var weeks = defaultWeeks
80+
if rawWeeks != "" {
81+
weeks, err = strconv.Atoi(rawWeeks)
82+
if err != nil {
83+
return opts, fmt.Errorf("parse weeks %q: %w", rawWeeks, err)
84+
}
85+
// +%d and positive %d have the same value, but different semantic
86+
// invariant: len(rawWeeks) > 0, because weeks > 0
87+
if weeks > 0 && rawWeeks[0] != '+' {
88+
opts.Half = true
89+
}
90+
} else {
91+
opts.Half = true
92+
}
93+
opts.Weeks = weeks
94+
95+
return opts, nil
96+
}

0 commit comments

Comments
 (0)