Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ An rsync-inspired file synchronization tool written in Go.

## Status

CLI parsing and file enumeration are implemented; data transfer is not.
`internal/sync` builds a sorted file list (`sync.Walk`), but nothing calls
it yet — the CLI only echoes parsed flags — and `internal/transport` is
CLI parsing, file enumeration, and filter-rule matching are implemented;
data transfer is not. `internal/sync` builds a sorted file list
(`sync.Walk`) and can filter it (`sync.FilterEntries`), but nothing calls
either yet — the CLI only echoes parsed flags — and `internal/transport` is
still empty.

## Build
Expand Down Expand Up @@ -43,10 +44,12 @@ argument is always the destination.
| `--exclude PATTERN` | | exclude matching files (repeatable) |
| `--include PATTERN` | | include matching files (repeatable) |
| `--filter RULE` | | add a filter rule (repeatable) |
| `--exclude-from FILE` | | read exclude patterns from FILE, one per line (repeatable) |
| `--include-from FILE` | | read include patterns from FILE, one per line (repeatable) |

`--exclude`/`--include`/`--filter` share one ordered rule list — their
relative order on the command line is preserved, matching rsync's
first-match-wins semantics.
All five filter-related flags share one ordered rule list — their relative
order on the command line is preserved, matching rsync's first-match-wins
semantics. See [Filter Rules](#filter-rules) below.

## File Enumeration

Expand All @@ -65,11 +68,31 @@ target). Symlinks are captured via `Lstat`, never followed.
On Windows, `UID`/`GID` are always `0` — there's no POSIX ownership concept
to read, so `0` means "unavailable," not a real value.

## Filter Rules

`sync.CompileRules` turns the ordered `--exclude`/`--include`/`--filter`/
`--exclude-from`/`--include-from` list into ready-to-match rules;
`sync.Included`/`sync.FilterEntries` apply them to `sync.Walk`'s output as
a separate pass, first-match-wins, defaulting to include when nothing
matches.

Pattern syntax: `*` matches within one path segment, `**` crosses segment
boundaries, `?` matches one character. A trailing `/` makes a pattern match
directories only. `--filter` also accepts `merge FILE` to inline another
rule file at that point in the list (one level deep — a merge file that
itself tries to merge another file is an error, not silently ignored).

A pattern anchors to the transfer root — matched once against the full
path, not tried at every depth — if it has a leading `/`, contains any
other `/`, or contains `**`. Only a pattern with none of those (a bare
filename like `*.log`) matches at any depth, against the final path
component only. This matches real rsync's actual anchoring rule.

## Architecture

- `cmd/grsync` — CLI entrypoint.
- `internal/cli` — flag/argument parsing (built on cobra).
- `internal/sync` — file-list generation today; comparison/delta logic later.
- `internal/sync` — file-list generation and filter matching today; comparison/delta logic later.
- `internal/transport` — (placeholder) data movement, local and remote.

Goal: full feature parity with upstream rsync, including protocol/format
Expand Down
49 changes: 31 additions & 18 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,24 @@ import (
// FilterRuleType identifies which kind of rule a FilterRule represents.
type FilterRuleType string

// The three rule kinds grsync's flags can produce. Kept as their own type
// (rather than a bare string) so callers can't accidentally pass an
// arbitrary value through.
// The rule kinds grsync's filter-related flags can produce. Kept as their
// own type (rather than a bare string) so callers can't accidentally pass
// an arbitrary value through.
const (
FilterRuleInclude FilterRuleType = "include"
FilterRuleExclude FilterRuleType = "exclude"
FilterRuleFilter FilterRuleType = "filter"
FilterRuleInclude FilterRuleType = "include"
FilterRuleExclude FilterRuleType = "exclude"
FilterRuleFilter FilterRuleType = "filter"
FilterRuleExcludeFrom FilterRuleType = "exclude-from"
FilterRuleIncludeFrom FilterRuleType = "include-from"
)

// FilterRule is a single --include/--exclude/--filter rule. rsync treats
// these three flags as one ordered, first-match-wins rule list rather than
// three independent lists, so grsync collects them the same way: Type
// records which flag produced the rule, and relative order across *all*
// three flags is preserved in the order the user supplied them.
// FilterRule is a single --include/--exclude/--filter/--exclude-from/
// --include-from occurrence. rsync treats all of these as one ordered,
// first-match-wins rule list rather than independent lists, so grsync
// collects them the same way: Type records which flag produced the rule,
// and relative order across *all* of them is preserved in the order the
// user supplied them. For the two "-from" kinds, Pattern is a file path,
// not a filter pattern — internal/sync reads and expands it.
type FilterRule struct {
Type FilterRuleType
Pattern string
Expand All @@ -49,11 +53,12 @@ type options struct {
filterRules []FilterRule
}

// filterRuleFlag implements pflag.Value. Each of --exclude/--include/--filter
// gets its own instance, fixed to a single FilterRuleType, but all three
// share the same backing slice — so pflag's normal "call Set once per
// occurrence" behavior naturally builds one ordered rule list regardless of
// which of the three flag names was used at each position.
// filterRuleFlag implements pflag.Value. Each of --exclude/--include/
// --filter/--exclude-from/--include-from gets its own instance, fixed to a
// single FilterRuleType, but all of them share the same backing slice — so
// pflag's normal "call Set once per occurrence" behavior naturally builds
// one ordered rule list regardless of which flag name was used at each
// position.
type filterRuleFlag struct {
ruleType FilterRuleType
rules *[]FilterRule
Expand All @@ -67,10 +72,14 @@ func (f *filterRuleFlag) Set(pattern string) error {
}

func (f *filterRuleFlag) Type() string {
if f.ruleType == FilterRuleFilter {
switch f.ruleType {
case FilterRuleFilter:
return "rule"
case FilterRuleExcludeFrom, FilterRuleIncludeFrom:
return "file"
default:
return "pattern"
}
return "pattern"
}

// NewRootCmd builds the root grsync command. It is exported as a
Expand Down Expand Up @@ -106,6 +115,10 @@ func NewRootCmd() *cobra.Command {
"include", "include files matching PATTERN (repeatable, order preserved relative to --exclude/--filter)")
flags.Var(&filterRuleFlag{ruleType: FilterRuleFilter, rules: &opts.filterRules},
"filter", "add a file-filtering RULE (repeatable, order preserved relative to --exclude/--include)")
flags.Var(&filterRuleFlag{ruleType: FilterRuleExcludeFrom, rules: &opts.filterRules},
"exclude-from", "read exclude patterns from FILE, one per line (repeatable, order preserved)")
flags.Var(&filterRuleFlag{ruleType: FilterRuleIncludeFrom, rules: &opts.filterRules},
"include-from", "read include patterns from FILE, one per line (repeatable, order preserved)")

return cmd
}
Expand Down
Loading