Skip to content

Commit

Permalink
feat: Improve ignore rules (#5)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nieomylnieja authored Jul 30, 2024
1 parent fd01e5e commit bcfeb94
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 43 deletions.
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
50 changes: 30 additions & 20 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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")
}
Expand All @@ -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
}
42 changes: 42 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
73 changes: 56 additions & 17 deletions internal/gitsync/gitsync.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}

0 comments on commit bcfeb94

Please sign in to comment.