mirror of
https://github.com/lukaszraczylo/git-velocity.git
synced 2026-07-10 06:51:50 +00:00
Initial commit.
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Load reads and parses a configuration file
|
||||
func Load(path string) (*Config, error) {
|
||||
cleanPath := filepath.Clean(path)
|
||||
data, err := os.ReadFile(cleanPath) // #nosec G304 -- path is user-provided config file
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
// Expand environment variables
|
||||
expanded := expandEnvVars(string(data))
|
||||
|
||||
// Start with defaults
|
||||
cfg := DefaultConfig()
|
||||
|
||||
// Parse YAML
|
||||
if err := yaml.Unmarshal([]byte(expanded), cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
|
||||
// Validate configuration
|
||||
if err := Validate(cfg); err != nil {
|
||||
return nil, fmt.Errorf("invalid configuration: %w", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// expandEnvVars replaces ${VAR} patterns with environment variable values
|
||||
func expandEnvVars(input string) string {
|
||||
re := regexp.MustCompile(`\$\{([^}]+)\}`)
|
||||
return re.ReplaceAllStringFunc(input, func(match string) string {
|
||||
// Extract variable name
|
||||
varName := strings.TrimPrefix(strings.TrimSuffix(match, "}"), "${")
|
||||
return os.Getenv(varName)
|
||||
})
|
||||
}
|
||||
|
||||
// parseRelativeDate parses relative date strings like "-90d", "-2w", "-3m"
|
||||
// Returns the parsed time or nil if not a relative format
|
||||
func parseRelativeDate(s string) *time.Time {
|
||||
if !strings.HasPrefix(s, "-") && !strings.HasPrefix(s, "+") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse the number and unit
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
unit := s[len(s)-1]
|
||||
numStr := s[1 : len(s)-1] // Skip the +/- prefix and unit suffix
|
||||
|
||||
num := 0
|
||||
for _, c := range numStr {
|
||||
if c < '0' || c > '9' {
|
||||
return nil
|
||||
}
|
||||
num = num*10 + int(c-'0')
|
||||
}
|
||||
|
||||
if s[0] == '-' {
|
||||
num = -num
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
var result time.Time
|
||||
|
||||
switch unit {
|
||||
case 'd': // days
|
||||
result = now.AddDate(0, 0, num)
|
||||
case 'w': // weeks
|
||||
result = now.AddDate(0, 0, num*7)
|
||||
case 'm': // months
|
||||
result = now.AddDate(0, num, 0)
|
||||
case 'y': // years
|
||||
result = now.AddDate(num, 0, 0)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
// Normalize to start of day
|
||||
result = time.Date(result.Year(), result.Month(), result.Day(), 0, 0, 0, 0, result.Location())
|
||||
return &result
|
||||
}
|
||||
|
||||
// GetParsedDateRange parses and returns the date range with defaults
|
||||
// Supports both absolute dates (2024-01-01) and relative dates (-90d, -2w, -3m, -1y)
|
||||
func (c *Config) GetParsedDateRange() (*ParsedDateRange, error) {
|
||||
result := &ParsedDateRange{}
|
||||
|
||||
if c.DateRange.Start != "" {
|
||||
// Try relative date first
|
||||
if t := parseRelativeDate(c.DateRange.Start); t != nil {
|
||||
result.Start = t
|
||||
} else {
|
||||
// Try absolute date
|
||||
t, err := time.Parse("2006-01-02", c.DateRange.Start)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid start date format (use YYYY-MM-DD or -Nd/-Nw/-Nm/-Ny): %w", err)
|
||||
}
|
||||
result.Start = &t
|
||||
}
|
||||
}
|
||||
|
||||
if c.DateRange.End != "" {
|
||||
// Try relative date first
|
||||
if t := parseRelativeDate(c.DateRange.End); t != nil {
|
||||
// Set end to end of day
|
||||
endOfDay := t.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
|
||||
result.End = &endOfDay
|
||||
} else {
|
||||
// Try absolute date
|
||||
t, err := time.Parse("2006-01-02", c.DateRange.End)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid end date format (use YYYY-MM-DD or -Nd/-Nw/-Nm/-Ny): %w", err)
|
||||
}
|
||||
// Set end to end of day
|
||||
t = t.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
|
||||
result.End = &t
|
||||
}
|
||||
} else {
|
||||
// Default to now
|
||||
now := time.Now()
|
||||
result.End = &now
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetCacheTTL returns the cache TTL as a time.Duration
|
||||
func (c *Config) GetCacheTTL() (time.Duration, error) {
|
||||
if c.Cache.TTL == "" {
|
||||
return 24 * time.Hour, nil
|
||||
}
|
||||
return time.ParseDuration(c.Cache.TTL)
|
||||
}
|
||||
|
||||
// HasGithubToken returns true if token authentication is configured
|
||||
func (c *Config) HasGithubToken() bool {
|
||||
return c.Auth.GithubToken != ""
|
||||
}
|
||||
|
||||
// HasGithubApp returns true if GitHub App authentication is configured
|
||||
func (c *Config) HasGithubApp() bool {
|
||||
return c.Auth.GithubApp != nil &&
|
||||
c.Auth.GithubApp.AppID > 0 &&
|
||||
c.Auth.GithubApp.InstallationID > 0 &&
|
||||
(c.Auth.GithubApp.PrivateKey != "" || c.Auth.GithubApp.PrivateKeyPath != "")
|
||||
}
|
||||
|
||||
// GetGithubAppPrivateKey returns the GitHub App private key content
|
||||
func (c *Config) GetGithubAppPrivateKey() ([]byte, error) {
|
||||
if c.Auth.GithubApp == nil {
|
||||
return nil, fmt.Errorf("GitHub App not configured")
|
||||
}
|
||||
|
||||
if c.Auth.GithubApp.PrivateKey != "" {
|
||||
return []byte(c.Auth.GithubApp.PrivateKey), nil
|
||||
}
|
||||
|
||||
if c.Auth.GithubApp.PrivateKeyPath != "" {
|
||||
cleanPath := filepath.Clean(c.Auth.GithubApp.PrivateKeyPath)
|
||||
return os.ReadFile(cleanPath) // #nosec G304 -- path is user-provided config value
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no private key configured")
|
||||
}
|
||||
|
||||
// GetTeamForUser returns the team configuration for a given username
|
||||
func (c *Config) GetTeamForUser(username string) *TeamConfig {
|
||||
for i := range c.Teams {
|
||||
for _, member := range c.Teams[i].Members {
|
||||
if strings.EqualFold(member, username) {
|
||||
return &c.Teams[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsBot checks if a username matches bot patterns
|
||||
func (c *Config) IsBot(username string) bool {
|
||||
if c.Options.IncludeBots {
|
||||
return false
|
||||
}
|
||||
|
||||
lower := strings.ToLower(username)
|
||||
for _, pattern := range c.Options.BotPatterns {
|
||||
pattern = strings.ToLower(pattern)
|
||||
if matchPattern(lower, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// matchPattern performs simple glob-style pattern matching
|
||||
func matchPattern(s, pattern string) bool {
|
||||
// Handle exact match
|
||||
if !strings.Contains(pattern, "*") {
|
||||
return s == pattern
|
||||
}
|
||||
|
||||
// Handle prefix match (pattern*)
|
||||
if strings.HasSuffix(pattern, "*") && !strings.HasPrefix(pattern, "*") {
|
||||
return strings.HasPrefix(s, strings.TrimSuffix(pattern, "*"))
|
||||
}
|
||||
|
||||
// Handle suffix match (*pattern)
|
||||
if strings.HasPrefix(pattern, "*") && !strings.HasSuffix(pattern, "*") {
|
||||
return strings.HasSuffix(s, strings.TrimPrefix(pattern, "*"))
|
||||
}
|
||||
|
||||
// Handle contains match (*pattern*)
|
||||
if strings.HasPrefix(pattern, "*") && strings.HasSuffix(pattern, "*") {
|
||||
inner := strings.TrimPrefix(strings.TrimSuffix(pattern, "*"), "*")
|
||||
return strings.Contains(s, inner)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetCustomPeriods returns parsed custom periods
|
||||
func (c *Config) GetCustomPeriods() ([]ParsedCustomPeriod, error) {
|
||||
var periods []ParsedCustomPeriod
|
||||
|
||||
for _, cp := range c.CustomPeriods {
|
||||
start, err := time.Parse("2006-01-02", cp.Start)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid start date for period %s: %w", cp.Name, err)
|
||||
}
|
||||
|
||||
end, err := time.Parse("2006-01-02", cp.End)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid end date for period %s: %w", cp.Name, err)
|
||||
}
|
||||
|
||||
// Set end to end of day
|
||||
end = end.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
|
||||
|
||||
periods = append(periods, ParsedCustomPeriod{
|
||||
Name: cp.Name,
|
||||
Start: start,
|
||||
End: end,
|
||||
})
|
||||
}
|
||||
|
||||
return periods, nil
|
||||
}
|
||||
|
||||
// ParsedCustomPeriod represents a parsed custom time period
|
||||
type ParsedCustomPeriod struct {
|
||||
Name string
|
||||
Start time.Time
|
||||
End time.Time
|
||||
}
|
||||
Reference in New Issue
Block a user