diff --git a/commands/report.go b/commands/report.go index 115f3eb..16d8301 100644 --- a/commands/report.go +++ b/commands/report.go @@ -27,20 +27,20 @@ func (s *Sort) UnmarshalText(b []byte) error { type Report struct { // Specify the range positionally - Start string `arg:"positional" help:"specify the start of the reporting range (defaults to the beginning of last month)"` - End string `arg:"positional" help:"specify the end of the reporting range (defaults to the end of last month)"` + Start *lib.Date `arg:"positional" help:"specify the start of the reporting range (defaults to the beginning of last month)"` + End *lib.Date `arg:"positional" help:"specify the end of the reporting range (defaults to the end of last month)"` // Specify the range using shorthand - Month string `arg:"-m,--month" help:"shorthand for reporting over an entire month (can be combined with --year, accepted formats: 01, 1, Jan, January, 2019-01)"` - Year int `arg:"-y,--year" help:"shorthand for reporting over an entire year (can be combined with --month)"` - Today bool `arg:"--today" help:"shorthand for reporting on today"` - Yesterday bool `arg:"--yesterday" help:"shorthand for reporting on yesterday"` - ThisWeek bool `arg:"--thisweek" help:"shorthand for reporting on the current week (Sunday-today)"` - LastWeek bool `arg:"--lastweek" help:"shorthand for reporting on last week (Sunday-Saturday)"` - ThisMonth bool `arg:"--thismonth" help:"shorthand for reporting on the current month"` - LastMonth bool `arg:"--lastmonth" help:"shorthand for reporting on last month"` - ThisYear bool `arg:"--thisyear" help:"shorthand for reporting on the current year"` - LastYear bool `arg:"--lastyear" help:"shorthand for reporting on last year"` + Month lib.Month `arg:"-m,--month" help:"shorthand for reporting over an entire month (can be combined with --year, accepted formats: 01, 1, Jan, January, 2019-01)"` + Year int `arg:"-y,--year" help:"shorthand for reporting over an entire year (can be combined with --month)"` + Today bool `arg:"--today" help:"shorthand for reporting on today"` + Yesterday bool `arg:"--yesterday" help:"shorthand for reporting on yesterday"` + ThisWeek bool `arg:"--thisweek" help:"shorthand for reporting on the current week (Sunday-today)"` + LastWeek bool `arg:"--lastweek" help:"shorthand for reporting on last week (Sunday-Saturday)"` + ThisMonth bool `arg:"--thismonth" help:"shorthand for reporting on the current month"` + LastMonth bool `arg:"--lastmonth" help:"shorthand for reporting on last month"` + ThisYear bool `arg:"--thisyear" help:"shorthand for reporting on the current year"` + LastYear bool `arg:"--lastyear" help:"shorthand for reporting on last year"` // Specify the grains to show TaskGrain bool `arg:"--taskgrain" help:"report on the task grain"` @@ -58,7 +58,7 @@ type Report struct { Asc bool `arg:"--asc" help:"sort ascending"` // Output file - OutputFile string `arg:"-o,--outfile" help:"specify the filename to export the report to (supports PDF, HTML, and RST files, if set to '-' then the report is printed to stdout)" default:"-"` + OutputFile lib.Filename `arg:"-o,--outfile" help:"specify the filename to export the report to (supports PDF, HTML, and RST files, if set to '-' then the report is printed to stdout)" default:"-"` } func (s *Report) Run(config *lib.Config) error { diff --git a/commands/sync.go b/commands/sync.go new file mode 100644 index 0000000..b31142b --- /dev/null +++ b/commands/sync.go @@ -0,0 +1,19 @@ +package commands + +import ( + "context" + + "github.com/sumnerevans/tracktime/lib" + "github.com/sumnerevans/tracktime/synchroniser" +) + +type Sync struct { + Month *lib.Month `arg:"positional" help:"the month to synchronize time entries for (accepted formats: 01, 1, Jan, January, 2019-01)" default:"this month"` +} + +func (s *Sync) Run(config *lib.Config) error { + for _, synchroniser := range synchroniser.Synchronisers { + go synchroniser.Sync(context.Background(), nil, nil, *s.Month) + } + return nil +} diff --git a/lib/config.go b/lib/config.go index 97e47e0..1416ce1 100644 --- a/lib/config.go +++ b/lib/config.go @@ -6,16 +6,57 @@ import ( "gopkg.in/yaml.v3" ) +type GitHubSyncConfig struct { + Username string `yaml:"username"` + RootURI string `yaml:"root_uri"` + AccessToken string `yaml:"access_token"` +} + +type GitLabSyncConfig struct { + APIRoot string `yaml:"api_root"` + APIKey string `yaml:"api_key"` +} + +type SourceHutSyncConfig struct { + APIRoot string `yaml:"api_root"` + AccessToken string `yaml:"access_token"` + Username string `yaml:"username"` +} + +type SyncConfig struct { + Enable bool `yaml:"enable"` + GitHub GitHubSyncConfig `yaml:"github"` + GitLab GitLabSyncConfig `yaml:"gitlab"` + SourceHut SourceHutSyncConfig `yaml:"sourcehut"` +} + +type ReportingConfig struct { + FullName string `yaml:"fullname"` + ProjectRates map[string]int `yaml:"project_rates"` + CustomerRates map[string]int `yaml:"customer_rates"` + CustomerAliases map[string]string `yaml:"customer_aliases"` + CustomerAddresses map[string]string `yaml:"customer_addresses"` + DayWorkedMinThreshold int `yaml:"day_worked_min_threshold"` + ReportStatistics bool `yaml:"report_statistics"` + // TODO + // TableFormat string `yaml:"table_format"` +} + type Config struct { - FullName string `yaml:"fullname"` - Directory Filename `yaml:"directory"` + Version string `yaml:"version"` + Directory Filename `yaml:"directory"` + + Reporting ReportingConfig `yaml:"reporting"` + Sync SyncConfig `yaml:"sync"` + + // Editor Editor string `yaml:"editor"` EditorArgs []string `yaml:"editor_args"` } func ReadConfig(f Filename) (*Config, error) { config := Config{ - FullName: "", + Reporting: ReportingConfig{FullName: ""}, Directory: Filename("$HOME/.tracktime"), } configData, err := os.ReadFile(f.Expand()) diff --git a/lib/month.go b/lib/month.go new file mode 100644 index 0000000..533af8d --- /dev/null +++ b/lib/month.go @@ -0,0 +1,29 @@ +package lib + +import ( + "fmt" + "time" +) + +type Month struct { + year int + month time.Month +} + +func ThisMonth() Month { + now := time.Now().Local() + return Month{year: now.Year(), month: now.Month()} +} + +func (d *Month) UnmarshalText(text []byte) error { + now := time.Now().Local() + switch string(text) { + case "this month", "thismonth": + d.year = now.Year() + d.month = now.Month() + default: + // TODO + return fmt.Errorf("Invalid date '%s'.", string(text)) + } + return nil +} diff --git a/synchroniser/github.go b/synchroniser/github.go new file mode 100644 index 0000000..827852b --- /dev/null +++ b/synchroniser/github.go @@ -0,0 +1,62 @@ +package synchroniser + +import ( + "context" + "fmt" + "strings" + + "github.com/sumnerevans/tracktime/lib" +) + +type GitHubSynchroniser struct { + Config lib.GitHubSyncConfig +} + +func (gh *GitHubSynchroniser) Name() string { return "GitHub" } + +func (gh *GitHubSynchroniser) Init(config lib.SyncConfig) { + gh.Config = config.GitHub +} + +func (gh *GitHubSynchroniser) Sync(ctx context.Context, aggregatedTime, syncedTime AggregatedTime, month lib.Month) (AggregatedTime, error) { + return aggregatedTime, nil +} + +func (gh *GitHubSynchroniser) cleanTaskID(taskID string) string { + return strings.TrimPrefix(taskID, "#") +} + +func (gh *GitHubSynchroniser) GetFormattedTaskID(entry *lib.TimeEntry) string { + if (entry.Type != "github" && entry.Type != "gh") || entry.TaskID == "" { + return "" + } + + return fmt.Sprintf("#%s", gh.cleanTaskID(entry.TaskID)) +} + +func (gh *GitHubSynchroniser) GetTaskLink(entry *lib.TimeEntry) string { + var owner, project string + projectParts := strings.Split(entry.Project, "/") + if len(projectParts) == 1 { + if gh.Config.Username == "" { + return "" + } + owner = gh.Config.Username + project = projectParts[0] + } else if len(projectParts) == 2 { + owner = projectParts[0] + project = projectParts[1] + } else { + return "" + } + // Always link to /issues/ because it will redirect to /pull/ if necessary. + return fmt.Sprintf("%s/%s/%s/issues/%s", gh.Config.RootURI, owner, project, gh.cleanTaskID(entry.TaskID)) +} + +func (gh *GitHubSynchroniser) GetTaskDescription(ctx context.Context, entry *lib.TimeEntry) string { + return "" +} + +func init() { + Synchronisers = append(Synchronisers, &GitHubSynchroniser{}) +} diff --git a/synchroniser/syncroniser.go b/synchroniser/syncroniser.go new file mode 100644 index 0000000..0d0e639 --- /dev/null +++ b/synchroniser/syncroniser.go @@ -0,0 +1,27 @@ +package synchroniser + +import ( + "context" + "time" + + "github.com/sumnerevans/tracktime/lib" +) + +type AggregatedTimeKey struct { + Type lib.TimeEntryType + Project string + TaskID string +} + +type AggregatedTime map[AggregatedTimeKey]time.Duration + +type Synchroniser interface { + Init(config lib.SyncConfig) + Name() string + Sync(ctx context.Context, aggregatedTime, syncedTime AggregatedTime, month lib.Month) (AggregatedTime, error) + GetFormattedTaskID(entry *lib.TimeEntry) string + GetTaskLink(entry *lib.TimeEntry) string + GetTaskDescription(ctx context.Context, entry *lib.TimeEntry) string +} + +var Synchronisers []Synchroniser diff --git a/tracktime.go b/tracktime.go index 07d9281..9dd56d1 100644 --- a/tracktime.go +++ b/tracktime.go @@ -12,21 +12,13 @@ import ( "github.com/sumnerevans/tracktime/lib" ) -type sync struct { - Month string `arg:"positional" help:"the month to synchronize time entries for (accepted formats: 01, 1, Jan, January, 2019-01)" default:"this month"` -} - -func (s *sync) Run(config *lib.Config) error { - return nil -} - type args struct { Start *commands.Start `arg:"subcommand" help:"start a new time entry for today"` Stop *commands.Stop `arg:"subcommand" help:"stop the current time entry"` Resume *commands.Resume `arg:"subcommand" help:"resume a time entry from today"` List *commands.List `arg:"subcommand" help:"list the time entries for a date"` Edit *commands.Edit `arg:"subcommand" help:"edit time entries for a date"` - Sync *sync `arg:"subcommand" help:"synchronize time spent on tasks for a month to external services"` + Sync *commands.Sync `arg:"subcommand" help:"synchronize time spent on tasks for a month to external services"` Report *commands.Report `arg:"subcommand" help:"output a report about time spent in a time range"` ConfigFile lib.Filename `arg:"--config" help:"the configuration file to use" default:"$HOME/.config/tracktime/tracktimerc"` }