From bcfeb945b751a0b472048fc882fb62b87f73a947 Mon Sep 17 00:00:00 2001 From: Mateusz Hawrus <48822818+nieomylnieja@users.noreply.github.com> Date: Tue, 30 Jul 2024 23:04:32 +0200 Subject: [PATCH] feat: Improve ignore rules (#5) Currently there's no way to define precise ignore rules, like `regex` rule for specific repository and file. This PR refactors these rules in order to make them more flexible. --- README.md | 16 +++++--- internal/config/config.go | 50 +++++++++++++---------- internal/config/config_test.go | 42 +++++++++++++++++++ internal/gitsync/gitsync.go | 73 ++++++++++++++++++++++++++-------- 4 files changed, 138 insertions(+), 43 deletions(-) create mode 100644 internal/config/config_test.go diff --git a/README.md b/README.md index 1d8d92c..9ebe8e2 100644 --- a/README.md +++ b/README.md @@ -115,9 +115,16 @@ The config file is a JSON file which describes the synchronization process. }, // Optional. "ignore": [ + // If neither 'repositoryName' nor 'fileName' is provided, + // the rule will apply globally. + // If both are provided, the rule will apply only to the specific repository file. { + // Optional. Name of the repository to which the ignore rule applies. + "repositoryName": "go-libyear", + // Optional. Name of the file to which the ignore rule applies. + "fileName": "golangci linter config", // Optional. Regular expression used to ignore matching hunks. - "regex": "^\\s*local-prefixes:", + "regex": "^\\s*local-prefixes:" }, { // Optional. Hunk to be ignored is represented with lines header and changes list. @@ -141,14 +148,11 @@ The config file is a JSON file which describes the synchronization process. // Required. URL used to clone the repository. "url": "https://github.com/nieomylnieja/go-libyear.git", // Optional. Default: "origin/main". - "ref": "dev-branch", - // Optional, merged with global 'ignore' section. - // Follows the same format and rules but applies ONLY to the specified repository. - "ignore": [], + "ref": "dev-branch" }, { "name": "sword-to-obsidian", - "url": "https://github.com/nieomylnieja/sword-to-obsidian.git", + "url": "https://github.com/nieomylnieja/sword-to-obsidian.git" } ], // Required. At least one file must be provided. diff --git a/internal/config/config.go b/internal/config/config.go index caba3eb..8c5a75f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -14,11 +14,11 @@ import ( const defaultRef = "origin/main" type Config struct { - StorePath string `json:"storePath,omitempty"` - Root *RepositoryConfig `json:"root"` - Ignore []*IgnoreConfig `json:"ignore,omitempty"` - Repositories []*RepositoryConfig `json:"syncRepositories"` - SyncFiles []*FileConfig `json:"syncFiles"` + StorePath string `json:"storePath,omitempty"` + Root *Repository `json:"root"` + Ignore []*IgnoreRule `json:"ignore,omitempty"` + Repositories []*Repository `json:"syncRepositories"` + SyncFiles []*File `json:"syncFiles"` path string resolvedStorePath string @@ -32,35 +32,36 @@ func (c *Config) GetStorePath() string { return c.resolvedStorePath } -type RepositoryConfig struct { - Name string `json:"name"` - URL string `json:"url"` - Ref string `json:"ref,omitempty"` - Ignore []*IgnoreConfig `json:"ignore,omitempty"` +type Repository struct { + Name string `json:"name"` + URL string `json:"url"` + Ref string `json:"ref,omitempty"` path string defaultRef string } -func (r *RepositoryConfig) GetPath() string { +func (r *Repository) GetPath() string { return r.path } -func (r *RepositoryConfig) GetRef() string { +func (r *Repository) GetRef() string { if r.Ref != "" { return r.Ref } return r.defaultRef } -type FileConfig struct { +type File struct { Name string `json:"name"` Path string `json:"path"` } -type IgnoreConfig struct { - Regex *string `json:"regex,omitempty"` - Hunk *diff.Hunk `json:"hunk,omitempty"` +type IgnoreRule struct { + RepositoryName *string `json:"fileName,omitempty"` + FileName *string `json:"repositoryName,omitempty"` + Regex *string `json:"regex,omitempty"` + Hunk *diff.Hunk `json:"hunk,omitempty"` } func ReadConfig(configPath string) (*Config, error) { @@ -123,9 +124,6 @@ func (c *Config) setDefaults() error { } } c.Root.path = filepath.Join(c.GetStorePath(), c.Root.Name) - if c.Root.Ignore != nil { - return errors.New("root repository cannot have ignore rules") - } if c.Root.Ref == "" { c.Root.defaultRef = defaultRef } @@ -158,10 +156,15 @@ func (c *Config) validate() error { } } } + for _, ignore := range c.Ignore { + if err := ignore.validate(); err != nil { + return errors.Wrap(err, "ignore rule validation failed") + } + } return nil } -func (f *FileConfig) validate() error { +func (f *File) validate() error { if f.Name == "" { return errors.New("file name is required") } @@ -170,3 +173,10 @@ func (f *FileConfig) validate() error { } return nil } + +func (i *IgnoreRule) validate() error { + if i.Regex == nil && i.Hunk == nil { + return errors.New("either 'regex' or 'hunk' needs to be defined") + } + return nil +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..e3e82e3 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,42 @@ +package config + +import ( + "bufio" + "bytes" + "encoding/json" + "os" + "strings" + "testing" +) + +func TestReadmeExample(t *testing.T) { + data, err := os.ReadFile("../../README.md") + if err != nil { + t.Fatal(err, "failed to read README.md") + } + scan := bufio.NewScanner(bytes.NewReader(data)) + readJSON := false + jsonBuilder := bytes.Buffer{} + for scan.Scan() { + line := strings.TrimSpace(scan.Text()) + switch { + case line == "```json5": + readJSON = true + case line == "```": + readJSON = false + case readJSON && !strings.HasPrefix(line, "//"): + jsonBuilder.WriteString(line) + jsonBuilder.WriteString("\n") + } + } + if err = scan.Err(); err != nil { + t.Fatal(err, "failed to scan README.md contents") + } + var config Config + if err = json.Unmarshal(jsonBuilder.Bytes(), &config); err != nil { + t.Fatal(err, "failed to unmarshal JSON config") + } + if err = config.validate(); err != nil { + t.Fatal(err, "config validation failed") + } +} diff --git a/internal/gitsync/gitsync.go b/internal/gitsync/gitsync.go index 3eb2306..cefb516 100644 --- a/internal/gitsync/gitsync.go +++ b/internal/gitsync/gitsync.go @@ -52,7 +52,7 @@ func Run(conf *config.Config, command Command) error { } } } - updatedFiles := make(map[*config.RepositoryConfig][]string, len(conf.Repositories)) + updatedFiles := make(map[*config.Repository][]string, len(conf.Repositories)) for _, file := range conf.SyncFiles { rootFilePath := filepath.Join(conf.GetStorePath(), conf.Root.Name, file.Path) for _, syncedRepo := range conf.Repositories { @@ -90,16 +90,18 @@ func Run(conf *config.Config, command Command) error { func syncRepoFile( conf *config.Config, command Command, - syncedRepo *config.RepositoryConfig, - file *config.FileConfig, + syncedRepo *config.Repository, + file *config.File, rootFilePath string, ) (bool, error) { syncedRepoFilePath := filepath.Join(conf.GetStorePath(), syncedRepo.Name, file.Path) - var regexes []string - for _, ignore := range append(syncedRepo.Ignore, conf.Ignore...) { - if ignore.Regex != nil { - regexes = append(regexes, *ignore.Regex) - } + regexes := make([]string, 0) + for _, ignore := range getIgnoreRules(conf, ignoreRulesQuery{ + RepoName: syncedRepo.Name, + FileName: file.Path, + Regex: true, + }) { + regexes = append(regexes, *ignore.Regex) } args := []string{ "-U", "0", @@ -131,8 +133,12 @@ func syncRepoFile( resultHunks := make([]diff.Hunk, 0, len(unifiedFmt.Hunks)) hunkLoop: for i, hunk := range unifiedFmt.Hunks { - for _, ignore := range append(syncedRepo.Ignore, conf.Ignore...) { - if ignore.Hunk != nil && ignore.Hunk.Equal(hunk) { + for _, ignore := range getIgnoreRules(conf, ignoreRulesQuery{ + RepoName: syncedRepo.Name, + FileName: file.Path, + Hunk: true, + }) { + if ignore.Hunk.Equal(hunk) { continue hunkLoop } } @@ -155,7 +161,11 @@ hunkLoop: case "i": // Copy loop variable. hunk := hunk - syncedRepo.Ignore = append(syncedRepo.Ignore, &config.IgnoreConfig{Hunk: &hunk}) + conf.Ignore = append(conf.Ignore, &config.IgnoreRule{ + RepositoryName: &syncedRepo.Name, + FileName: &file.Name, + Hunk: &hunk, + }) default: fmt.Println("Invalid input. Please enter Y (all), y (yes), n (no), i (ignore), or h (help).") fmt.Print(promptMessage) @@ -207,7 +217,7 @@ type commitDetails struct { Body string } -func commitChanges(root, repo *config.RepositoryConfig, changedFiles []string) (*commitDetails, error) { +func commitChanges(root, repo *config.Repository, changedFiles []string) (*commitDetails, error) { path := repo.GetPath() fmt.Printf("%s: adding changes to the index\n", repo.Name) if _, err := execCmd( @@ -242,7 +252,7 @@ func commitChanges(root, repo *config.RepositoryConfig, changedFiles []string) ( }, nil } -func pushChanges(repo *config.RepositoryConfig) error { +func pushChanges(repo *config.Repository) error { path := repo.GetPath() fmt.Printf("%s: pushing changes to remote\n", repo.Name) if _, err := execCmd( @@ -264,7 +274,7 @@ type ghPullRequest struct { URL string `json:"url"` } -func openPullRequest(repo *config.RepositoryConfig, commit *commitDetails) error { +func openPullRequest(repo *config.Repository, commit *commitDetails) error { ref := repo.GetRef() u, err := url.Parse(repo.URL) if err != nil { @@ -323,7 +333,7 @@ func openPullRequest(repo *config.RepositoryConfig, commit *commitDetails) error return nil } -func cloneRepo(repo *config.RepositoryConfig) error { +func cloneRepo(repo *config.Repository) error { path := repo.GetPath() if info, err := os.Stat(path); err == nil && info.IsDir() { return nil @@ -341,7 +351,7 @@ func cloneRepo(repo *config.RepositoryConfig) error { return nil } -func updateTrackedRef(repo *config.RepositoryConfig) error { +func updateTrackedRef(repo *config.Repository) error { path := repo.GetPath() ref := repo.GetRef() fmt.Printf("%s: updating repository ref (%s)\n", repo.Name, ref) @@ -375,7 +385,7 @@ func updateTrackedRef(repo *config.RepositoryConfig) error { return nil } -func checkoutSyncBranch(repo *config.RepositoryConfig) error { +func checkoutSyncBranch(repo *config.Repository) error { path := repo.GetPath() ref := repo.GetRef() fmt.Printf("%s: checking out %s branch\n", repo.Name, gitsyncUpdateBranch) @@ -413,3 +423,32 @@ func checkDependencies() error { } return nil } + +type ignoreRulesQuery struct { + RepoName string + FileName string + Hunk bool + Regex bool +} + +func getIgnoreRules(conf *config.Config, query ignoreRulesQuery) []*config.IgnoreRule { + if len(conf.Ignore) == 0 { + return nil + } + rules := make([]*config.IgnoreRule, 0) + for _, ignore := range conf.Ignore { + if ignore.RepositoryName != nil && *ignore.RepositoryName != query.RepoName { + continue + } + if ignore.FileName != nil && *ignore.FileName != query.FileName { + continue + } + if query.Hunk && ignore.Hunk != nil { + rules = append(rules, ignore) + } + if query.Regex && ignore.Regex != nil { + rules = append(rules, ignore) + } + } + return rules +}