Repository: kopia/kopia
Pull request #1538 (2021-11-26 21:10:19)

policy: revamped policy merge mechanism
Added policy.Definition which allows us to precisely report where
each piece of policy came from.

Fixed a one-off bug with "noParent", which prevented merging of parent
policies one level too soon.

Added a whole bunch of merging helpers and generic reflection-based
test that ensures every single merge is tested.


Patch:
diff --git a/cli/command_policy_show.go b/cli/command_policy_show.go
index cf795a4c..45a0572c 100644
--- a/cli/command_policy_show.go
+++ b/cli/command_policy_show.go
@@ -36,7 +36,7 @@ func (c *commandPolicyShow) run(ctx context.Context, rep repo.Repository) error
 	}
 
 	for _, target := range targets {
-		effective, policies, err := policy.GetEffectivePolicy(ctx, rep, target)
+		effective, definition, _, err := policy.GetEffectivePolicy(ctx, rep, target)
 		if err != nil {
 			return errors.Wrapf(err, "can't get effective policy for %q", target)
 		}
@@ -44,204 +44,144 @@ func (c *commandPolicyShow) run(ctx context.Context, rep repo.Repository) error
 		if c.jo.jsonOutput {
 			c.out.printStdout("%s\n", c.jo.jsonBytes(effective))
 		} else {
-			printPolicy(&c.out, effective, policies)
+			printPolicy(&c.out, effective, definition)
 		}
 	}
 
 	return nil
 }
 
-func getDefinitionPoint(target snapshot.SourceInfo, parents []*policy.Policy, match func(p *policy.Policy) bool) string {
-	for _, p := range parents {
-		if match(p) {
-			if p.Target() == target {
-				return "(defined for this target)"
-			}
-
-			return "inherited from " + p.Target().String()
-		}
-
-		if p.NoParent {
-			break
-		}
-	}
-
-	return "(default)"
-}
-
-func containsString(s []string, v string) bool {
-	for _, item := range s {
-		if item == v {
-			return true
-		}
+func definitionPointToString(target, src snapshot.SourceInfo) string {
+	if src == target {
+		return "(defined for this target)"
 	}
 
-	return false
+	return "inherited from " + src.String()
 }
 
-func printPolicy(out *textOutput, p *policy.Policy, parents []*policy.Policy) {
+func printPolicy(out *textOutput, p *policy.Policy, def *policy.Definition) {
 	out.printStdout("Policy for %v:\n\n", p.Target())
 
-	printRetentionPolicy(out, p, parents)
+	printRetentionPolicy(out, p, def)
 	out.printStdout("\n")
-	printFilesPolicy(out, p, parents)
+	printFilesPolicy(out, p, def)
 	out.printStdout("\n")
-	printErrorHandlingPolicy(out, p, parents)
+	printErrorHandlingPolicy(out, p, def)
 	out.printStdout("\n")
-	printSchedulingPolicy(out, p, parents)
+	printSchedulingPolicy(out, p, def)
 	out.printStdout("\n")
-	printCompressionPolicy(out, p, parents)
+	printCompressionPolicy(out, p, def)
 	out.printStdout("\n")
-	printActions(out, p, parents)
+	printActions(out, p, def)
 	out.printStdout("\n")
-	printLoggingPolicy(out, p, parents)
+	printLoggingPolicy(out, p, def)
 }
 
-func printRetentionPolicy(out *textOutput, p *policy.Policy, parents []*policy.Policy) {
+func printRetentionPolicy(out *textOutput, p *policy.Policy, def *policy.Definition) {
 	out.printStdout("Retention:\n")
 	out.printStdout("  Annual snapshots:  %3v           %v\n",
 		valueOrNotSet(p.RetentionPolicy.KeepAnnual),
-		getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.RetentionPolicy.KeepAnnual != nil
-		}))
+		definitionPointToString(p.Target(), def.RetentionPolicy.KeepAnnual))
 	out.printStdout("  Monthly snapshots: %3v           %v\n",
 		valueOrNotSet(p.RetentionPolicy.KeepMonthly),
-		getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.RetentionPolicy.KeepMonthly != nil
-		}))
+		definitionPointToString(p.Target(), def.RetentionPolicy.KeepMonthly))
 	out.printStdout("  Weekly snapshots:  %3v           %v\n",
 		valueOrNotSet(p.RetentionPolicy.KeepWeekly),
-		getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.RetentionPolicy.KeepWeekly != nil
-		}))
+		definitionPointToString(p.Target(), def.RetentionPolicy.KeepWeekly))
 	out.printStdout("  Daily snapshots:   %3v           %v\n",
 		valueOrNotSet(p.RetentionPolicy.KeepDaily),
-		getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.RetentionPolicy.KeepDaily != nil
-		}))
+		definitionPointToString(p.Target(), def.RetentionPolicy.KeepDaily))
 	out.printStdout("  Hourly snapshots:  %3v           %v\n",
 		valueOrNotSet(p.RetentionPolicy.KeepHourly),
-		getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.RetentionPolicy.KeepHourly != nil
-		}))
+		definitionPointToString(p.Target(), def.RetentionPolicy.KeepHourly))
 	out.printStdout("  Latest snapshots:  %3v           %v\n",
 		valueOrNotSet(p.RetentionPolicy.KeepLatest),
-		getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.RetentionPolicy.KeepLatest != nil
-		}))
+		definitionPointToString(p.Target(), def.RetentionPolicy.KeepLatest))
 }
 
-func printFilesPolicy(out *textOutput, p *policy.Policy, parents []*policy.Policy) {
+func printFilesPolicy(out *textOutput, p *policy.Policy, def *policy.Definition) {
 	out.printStdout("Files policy:\n")
 
 	out.printStdout("  Ignore cache directories:       %5v       %v\n",
 		p.FilesPolicy.IgnoreCacheDirectories.OrDefault(true),
-		getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.FilesPolicy.IgnoreCacheDirectories != nil
-		}))
+		definitionPointToString(p.Target(), def.FilesPolicy.IgnoreCacheDirectories))
 
 	if len(p.FilesPolicy.IgnoreRules) > 0 {
-		out.printStdout("  Ignore rules:\n")
+		out.printStdout("  Ignore rules:                               %v\n",
+			definitionPointToString(p.Target(), def.FilesPolicy.IgnoreRules))
 	} else {
 		out.printStdout("  No ignore rules.\n")
 	}
 
 	for _, rule := range p.FilesPolicy.IgnoreRules {
-		rule := rule
-		out.printStdout("    %-30v %v\n", rule, getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return containsString(pol.FilesPolicy.IgnoreRules, rule)
-		}))
+		out.printStdout("    %-30v\n", rule)
 	}
 
 	if len(p.FilesPolicy.DotIgnoreFiles) > 0 {
-		out.printStdout("  Read ignore rules from files:\n")
+		out.printStdout("  Read ignore rules from files:               %v\n",
+			definitionPointToString(p.Target(), def.FilesPolicy.DotIgnoreFiles))
 	}
 
 	for _, dotFile := range p.FilesPolicy.DotIgnoreFiles {
-		dotFile := dotFile
-		out.printStdout("    %-30v %v\n", dotFile, getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return containsString(pol.FilesPolicy.DotIgnoreFiles, dotFile)
-		}))
+		out.printStdout("    %-30v\n", dotFile)
 	}
 
 	if maxSize := p.FilesPolicy.MaxFileSize; maxSize > 0 {
 		out.printStdout("  Ignore files above: %10v  %v\n",
 			units.BytesStringBase2(maxSize),
-			getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-				return pol.FilesPolicy.MaxFileSize != 0
-			}))
+			definitionPointToString(p.Target(), def.FilesPolicy.MaxFileSize))
 	}
 
 	out.printStdout("  Scan one filesystem only:       %5v       %v\n",
 		p.FilesPolicy.OneFileSystem.OrDefault(false),
-		getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.FilesPolicy.OneFileSystem != nil
-		}))
+		definitionPointToString(p.Target(), def.FilesPolicy.OneFileSystem))
 }
 
-func printErrorHandlingPolicy(out *textOutput, p *policy.Policy, parents []*policy.Policy) {
+func printErrorHandlingPolicy(out *textOutput, p *policy.Policy, def *policy.Definition) {
 	out.printStdout("Error handling policy:\n")
 
 	out.printStdout("  Ignore file read errors:       %5v       %v\n",
 		p.ErrorHandlingPolicy.IgnoreFileErrors.OrDefault(false),
-		getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.ErrorHandlingPolicy.IgnoreFileErrors != nil
-		}))
+		definitionPointToString(p.Target(), def.ErrorHandlingPolicy.IgnoreFileErrors))
 
 	out.printStdout("  Ignore directory read errors:  %5v       %v\n",
 		p.ErrorHandlingPolicy.IgnoreDirectoryErrors.OrDefault(false),
-		getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.ErrorHandlingPolicy.IgnoreDirectoryErrors != nil
-		}))
+		definitionPointToString(p.Target(), def.ErrorHandlingPolicy.IgnoreDirectoryErrors))
 
 	out.printStdout("  Ignore unknown types:          %5v       %v\n",
 		p.ErrorHandlingPolicy.IgnoreUnknownTypes.OrDefault(true),
-		getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.ErrorHandlingPolicy.IgnoreUnknownTypes != nil
-		}))
+		definitionPointToString(p.Target(), def.ErrorHandlingPolicy.IgnoreUnknownTypes))
 }
 
-func printLoggingPolicy(out *textOutput, p *policy.Policy, parents []*policy.Policy) {
+func printLoggingPolicy(out *textOutput, p *policy.Policy, def *policy.Definition) {
 	out.printStdout("Logging details (%v-none, %v-maximum):\n", policy.LogDetailNone, policy.LogDetailMax)
 
 	out.printStdout("  Directory snapshotted:  %5v       %v\n",
 		p.LoggingPolicy.Directories.Snapshotted.OrDefault(policy.LogDetailNone),
-		getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.LoggingPolicy.Directories.Snapshotted != nil
-		}))
+		definitionPointToString(p.Target(), def.LoggingPolicy.Directories.Snapshotted))
 
 	out.printStdout("  Directory ignored:      %5v       %v\n",
 		p.LoggingPolicy.Directories.Ignored.OrDefault(policy.LogDetailNone),
-		getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.LoggingPolicy.Directories.Ignored != nil
-		}))
+		definitionPointToString(p.Target(), def.LoggingPolicy.Directories.Ignored))
 
 	out.printStdout("  Entry snapshotted:      %5v       %v\n",
 		p.LoggingPolicy.Entries.Snapshotted.OrDefault(policy.LogDetailNone),
-		getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.LoggingPolicy.Entries.Snapshotted != nil
-		}))
+		definitionPointToString(p.Target(), def.LoggingPolicy.Entries.Snapshotted))
 
 	out.printStdout("  Entry ignored:          %5v       %v\n",
 		p.LoggingPolicy.Entries.Ignored.OrDefault(policy.LogDetailNone),
-		getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.LoggingPolicy.Entries.Ignored != nil
-		}))
+		definitionPointToString(p.Target(), def.LoggingPolicy.Entries.Ignored))
 
 	out.printStdout("  Entry cache hit         %5v       %v\n",
 		p.LoggingPolicy.Entries.CacheHit.OrDefault(policy.LogDetailNone),
-		getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.LoggingPolicy.Entries.CacheHit != nil
-		}))
+		definitionPointToString(p.Target(), def.LoggingPolicy.Entries.CacheHit))
 
 	out.printStdout("  Entry cache miss        %5v       %v\n",
 		p.LoggingPolicy.Entries.CacheMiss.OrDefault(policy.LogDetailNone),
-		getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.LoggingPolicy.Entries.CacheMiss != nil
-		}))
+		definitionPointToString(p.Target(), def.LoggingPolicy.Entries.CacheMiss))
 }
 
-func printSchedulingPolicy(out *textOutput, p *policy.Policy, parents []*policy.Policy) {
+func printSchedulingPolicy(out *textOutput, p *policy.Policy, def *policy.Definition) {
 	out.printStdout("Scheduling policy:\n")
 
 	any := false
@@ -249,27 +189,17 @@ func printSchedulingPolicy(out *textOutput, p *policy.Policy, parents []*policy.
 	out.printStdout("  Scheduled snapshots:\n")
 
 	if p.SchedulingPolicy.Interval() != 0 {
-		out.printStdout("    Snapshot interval:   %10v  %v\n", p.SchedulingPolicy.Interval(), getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.SchedulingPolicy.Interval() != 0
-		}))
+		out.printStdout("    Snapshot interval:   %10v  %v\n", p.SchedulingPolicy.Interval(),
+			definitionPointToString(p.Target(), def.SchedulingPolicy.IntervalSeconds))
 
 		any = true
 	}
 
 	if len(p.SchedulingPolicy.TimesOfDay) > 0 {
-		out.printStdout("    Snapshot times:\n")
+		out.printStdout("    Snapshot times: %v\n", definitionPointToString(p.Target(), def.SchedulingPolicy.TimesOfDay))
 
 		for _, tod := range p.SchedulingPolicy.TimesOfDay {
-			tod := tod
-			out.printStdout("      %9v                      %v\n", tod, getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-				for _, t := range pol.SchedulingPolicy.TimesOfDay {
-					if t == tod {
-						return true
-					}
-				}
-
-				return false
-			}))
+			out.printStdout("      %9v\n", tod)
 		}
 
 		any = true
@@ -281,17 +211,14 @@ func printSchedulingPolicy(out *textOutput, p *policy.Policy, parents []*policy.
 
 	out.printStdout("  Manual snapshot:           %5v   %v\n",
 		p.SchedulingPolicy.Manual,
-		getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.SchedulingPolicy.Manual
-		}))
+		definitionPointToString(p.Target(), def.SchedulingPolicy.Manual))
 }
 
-func printCompressionPolicy(out *textOutput, p *policy.Policy, parents []*policy.Policy) {
+func printCompressionPolicy(out *textOutput, p *policy.Policy, def *policy.Definition) {
 	if p.CompressionPolicy.CompressorName != "" && p.CompressionPolicy.CompressorName != "none" {
 		out.printStdout("Compression:\n")
-		out.printStdout("  Compressor: %q %v\n", p.CompressionPolicy.CompressorName, getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.CompressionPolicy.CompressorName != ""
-		}))
+		out.printStdout("  Compressor: %q %v\n", p.CompressionPolicy.CompressorName,
+			definitionPointToString(p.Target(), def.CompressionPolicy.CompressorName))
 	} else {
 		out.printStdout("Compression disabled.\n")
 		return
@@ -299,23 +226,19 @@ func printCompressionPolicy(out *textOutput, p *policy.Policy, parents []*policy
 
 	switch {
 	case len(p.CompressionPolicy.OnlyCompress) > 0:
-		out.printStdout("  Only compress files with the following extensions:\n")
+		out.printStdout("  Only compress files with the following extensions: %v\n",
+			definitionPointToString(p.Target(), def.CompressionPolicy.OnlyCompress))
 
 		for _, rule := range p.CompressionPolicy.OnlyCompress {
-			rule := rule
-			out.printStdout("    %-30v %v\n", rule, getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-				return containsString(pol.CompressionPolicy.OnlyCompress, rule)
-			}))
+			out.printStdout("    %-30v\n", rule)
 		}
 
 	case len(p.CompressionPolicy.NeverCompress) > 0:
-		out.printStdout("  Compress all files except the following extensions:\n")
+		out.printStdout("  Compress all files except the following extensions: %v\n",
+			definitionPointToString(p.Target(), def.CompressionPolicy.NeverCompress))
 
 		for _, rule := range p.CompressionPolicy.NeverCompress {
-			rule := rule
-			out.printStdout("    %-30v %v\n", rule, getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-				return containsString(pol.CompressionPolicy.NeverCompress, rule)
-			}))
+			out.printStdout("    %-30v\n", rule)
 		}
 
 	default:
@@ -334,13 +257,12 @@ func printCompressionPolicy(out *textOutput, p *policy.Policy, parents []*policy
 	}
 }
 
-func printActions(out *textOutput, p *policy.Policy, parents []*policy.Policy) {
+func printActions(out *textOutput, p *policy.Policy, def *policy.Definition) {
 	var anyActions bool
 
 	if h := p.Actions.BeforeSnapshotRoot; h != nil {
-		out.printStdout("Run command before snapshot root:  %v\n", getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.Actions.BeforeSnapshotRoot == h
-		}))
+		out.printStdout("Run command before snapshot root:  %v\n",
+			definitionPointToString(p.Target(), def.Actions.BeforeSnapshotRoot))
 
 		printActionCommand(out, h)
 
@@ -348,9 +270,8 @@ func printActions(out *textOutput, p *policy.Policy, parents []*policy.Policy) {
 	}
 
 	if h := p.Actions.AfterSnapshotRoot; h != nil {
-		out.printStdout("Run command after snapshot root:   %v\n", getDefinitionPoint(p.Target(), parents, func(pol *policy.Policy) bool {
-			return pol.Actions.AfterSnapshotRoot == h
-		}))
+		out.printStdout("Run command after snapshot root:   %v\n",
+			definitionPointToString(p.Target(), def.Actions.AfterSnapshotRoot))
 		printActionCommand(out, h)
 
 		anyActions = true
diff --git a/cli/command_repository_create.go b/cli/command_repository_create.go
index 40f0de29..47adf63c 100644
--- a/cli/command_repository_create.go
+++ b/cli/command_repository_create.go
@@ -157,8 +157,8 @@ func (c *commandRepositoryCreate) populateRepository(ctx context.Context, passwo
 			return errors.Wrap(err, "unable to set global policy")
 		}
 
-		printRetentionPolicy(&c.out, policy.DefaultPolicy, nil)
-		printCompressionPolicy(&c.out, policy.DefaultPolicy, nil)
+		printRetentionPolicy(&c.out, policy.DefaultPolicy, &policy.Definition{})
+		printCompressionPolicy(&c.out, policy.DefaultPolicy, &policy.Definition{})
 
 		c.out.printStderr("\nTo find more information about default policy run 'kopia policy get'.\nTo change the policy use 'kopia policy set' command.\n")
 
diff --git a/cli/command_restore.go b/cli/command_restore.go
index ceb1e774..94392aeb 100644
--- a/cli/command_restore.go
+++ b/cli/command_restore.go
@@ -105,6 +105,7 @@ type commandRestore struct {
 	restoreMode                   string
 	restoreParallel               int
 	restoreIgnorePermissionErrors bool
+	restoreWriteFilesAtomically   bool
 	restoreSkipTimes              bool
 	restoreSkipOwners             bool
 	restoreSkipPermissions        bool
@@ -131,6 +132,7 @@ func (c *commandRestore) setup(svc appServices, parent commandParent) {
 	cmd.Flag("skip-permissions", "Skip permissions during restore").BoolVar(&c.restoreSkipPermissions)
 	cmd.Flag("skip-times", "Skip times during restore").BoolVar(&c.restoreSkipTimes)
 	cmd.Flag("ignore-permission-errors", "Ignore permission errors").Default("true").BoolVar(&c.restoreIgnorePermissionErrors)
+	cmd.Flag("write-files-atomically", "Write files atomically to disk, ensuring they are either fully committed, or not written at all, preventing partially written files").Default("false").BoolVar(&c.restoreWriteFilesAtomically)
 	cmd.Flag("ignore-errors", "Ignore all errors").BoolVar(&c.restoreIgnoreErrors)
 	cmd.Flag("skip-existing", "Skip files and symlinks that exist in the output").BoolVar(&c.restoreIncremental)
 	cmd.Flag("shallow", "Shallow restore the directory hierarchy starting at this level (default is to deep restore the entire hierarchy.)").Int32Var(&c.restoreShallowAtDepth)
@@ -214,6 +216,7 @@ func (c *commandRestore) restoreOutput(ctx context.Context) (restore.Output, err
 			OverwriteFiles:         c.restoreOverwriteFiles,
 			OverwriteSymlinks:      c.restoreOverwriteSymlinks,
 			IgnorePermissionErrors: c.restoreIgnorePermissionErrors,
+			WriteFilesAtomically:   c.restoreWriteFilesAtomically,
 			SkipOwners:             c.restoreSkipOwners,
 			SkipPermissions:        c.restoreSkipPermissions,
 			SkipTimes:              c.restoreSkipTimes,
diff --git a/cli/command_snapshot_list.go b/cli/command_snapshot_list.go
index 5848b90a..0809d62e 100644
--- a/cli/command_snapshot_list.go
+++ b/cli/command_snapshot_list.go
@@ -177,7 +177,7 @@ func (c *commandSnapshotList) outputManifestGroups(ctx context.Context, rep repo
 		separator = "\n"
 		anyOutput = true
 
-		pol, _, err := policy.GetEffectivePolicy(ctx, rep, src)
+		pol, _, _, err := policy.GetEffectivePolicy(ctx, rep, src)
 		if err != nil {
 			log(ctx).Errorf("unable to determine effective policy for %v", src)
 		} else {
diff --git a/htmlui/src/BeginRestore.js b/htmlui/src/BeginRestore.js
index 3f65fac8..2e3f582a 100644
--- a/htmlui/src/BeginRestore.js
+++ b/htmlui/src/BeginRestore.js
@@ -23,6 +23,7 @@ export class BeginRestore extends Component {
             overwriteDirectories: false,
             overwriteSymlinks: false,
             ignorePermissionErrors: true,
+            writeFilesAtomically: false,
             restoreDirEntryAtDepth: 1000,
             minSizeForPlaceholder: 0,
             restoreTask: "",
@@ -67,6 +68,7 @@ export class BeginRestore extends Component {
                 overwriteFiles: this.state.overwriteFiles,
                 overwriteDirectories: this.state.overwriteDirectories,
                 overwriteSymlinks: this.state.overwriteSymlinks,
+                writeFilesAtomically: this.state.writeFilesAtomically
             }
         }
 
@@ -123,6 +125,9 @@ export class BeginRestore extends Component {
                 <Row>
                     {RequiredBoolean(this, "Overwrite Symbolic Links", "overwriteSymlinks")}
                 </Row>
+                <Row>
+                    {RequiredBoolean(this, "Write files atomically", "writeFilesAtomically")}
+                </Row>
                 <Row>
                     <Col><hr/></Col>
                 </Row>
diff --git a/internal/server/api_snapshots.go b/internal/server/api_snapshots.go
index c60a9c95..dcccb7fc 100644
--- a/internal/server/api_snapshots.go
+++ b/internal/server/api_snapshots.go
@@ -32,7 +32,7 @@ func (s *Server) handleSnapshotList(ctx context.Context, r *http.Request, body [
 			continue
 		}
 
-		pol, _, err := policy.GetEffectivePolicy(ctx, s.rep, first.Source)
+		pol, _, _, err := policy.GetEffectivePolicy(ctx, s.rep, first.Source)
 		if err == nil {
 			pol.RetentionPolicy.ComputeRetentionReasons(grp)
 		}
diff --git a/internal/server/source_manager.go b/internal/server/source_manager.go
index f55b09e8..1ada6cb6 100644
--- a/internal/server/source_manager.go
+++ b/internal/server/source_manager.go
@@ -329,7 +329,7 @@ func (s *sourceManager) refreshStatus(ctx context.Context) {
 	ctx, cancel := context.WithTimeout(ctx, refreshTimeout)
 	defer cancel()
 
-	pol, _, err := policy.GetEffectivePolicy(ctx, s.server.rep, s.src)
+	pol, _, _, err := policy.GetEffectivePolicy(ctx, s.server.rep, s.src)
 	if err != nil {
 		s.setStatus("FAILED")
 		return
diff --git a/snapshot/policy/actions_policy.go b/snapshot/policy/actions_policy.go
index 14106656..e2e0f6a9 100644
--- a/snapshot/policy/actions_policy.go
+++ b/snapshot/policy/actions_policy.go
@@ -1,5 +1,7 @@
 package policy
 
+import "github.com/kopia/kopia/snapshot"
+
 // ActionsPolicy describes actions to be invoked when taking snapshots.
 type ActionsPolicy struct {
 	// command runs once before and after the folder it's attached to (not inherited).
@@ -11,6 +13,12 @@ type ActionsPolicy struct {
 	AfterSnapshotRoot  *ActionCommand `json:"afterSnapshotRoot,omitempty"`
 }
 
+// ActionsPolicyDefinition specifies which policy definition provided the value of a particular field.
+type ActionsPolicyDefinition struct {
+	BeforeSnapshotRoot snapshot.SourceInfo `json:"beforeSnapshotRoot,omitempty"`
+	AfterSnapshotRoot  snapshot.SourceInfo `json:"afterSnapshotRoot,omitempty"`
+}
+
 // ActionCommand configures a action command.
 type ActionCommand struct {
 	// command + args to run
@@ -25,14 +33,9 @@ type ActionCommand struct {
 }
 
 // Merge applies default values from the provided policy.
-func (p *ActionsPolicy) Merge(src ActionsPolicy) {
-	if p.BeforeSnapshotRoot == nil {
-		p.BeforeSnapshotRoot = src.BeforeSnapshotRoot
-	}
-
-	if p.AfterSnapshotRoot == nil {
-		p.AfterSnapshotRoot = src.AfterSnapshotRoot
-	}
+func (p *ActionsPolicy) Merge(src ActionsPolicy, def *ActionsPolicyDefinition, si snapshot.SourceInfo) {
+	mergeActionCommand(&p.BeforeSnapshotRoot, src.BeforeSnapshotRoot, &def.BeforeSnapshotRoot, si)
+	mergeActionCommand(&p.AfterSnapshotRoot, src.AfterSnapshotRoot, &def.AfterSnapshotRoot, si)
 }
 
 // MergeNonInheritable copies non-inheritable properties from the provided actions policy.
diff --git a/snapshot/policy/compression_policy.go b/snapshot/policy/compression_policy.go
index 202c20ff..e2ac4eea 100644
--- a/snapshot/policy/compression_policy.go
+++ b/snapshot/policy/compression_policy.go
@@ -6,15 +6,27 @@
 
 	"github.com/kopia/kopia/fs"
 	"github.com/kopia/kopia/repo/compression"
+	"github.com/kopia/kopia/snapshot"
 )
 
 // CompressionPolicy specifies compression policy.
 type CompressionPolicy struct {
-	CompressorName compression.Name `json:"compressorName,omitempty"`
-	OnlyCompress   []string         `json:"onlyCompress,omitempty"`
-	NeverCompress  []string         `json:"neverCompress,omitempty"`
-	MinSize        int64            `json:"minSize,omitempty"`
-	MaxSize        int64            `json:"maxSize,omitempty"`
+	CompressorName        compression.Name `json:"compressorName,omitempty"`
+	OnlyCompress          []string         `json:"onlyCompress,omitempty"`
+	NoParentOnlyCompress  bool             `json:"noParentOnlyCompress,omitempty"`
+	NeverCompress         []string         `json:"neverCompress,omitempty"`
+	NoParentNeverCompress bool             `json:"noParentNeverCompress,omitempty"`
+	MinSize               int64            `json:"minSize,omitempty"`
+	MaxSize               int64            `json:"maxSize,omitempty"`
+}
+
+// CompressionPolicyDefinition specifies which policy definition provided the value of a particular field.
+type CompressionPolicyDefinition struct {
+	CompressorName snapshot.SourceInfo `json:"compressorName,omitempty"`
+	OnlyCompress   snapshot.SourceInfo `json:"onlyCompress,omitempty"`
+	NeverCompress  snapshot.SourceInfo `json:"neverCompress,omitempty"`
+	MinSize        snapshot.SourceInfo `json:"minSize,omitempty"`
+	MaxSize        snapshot.SourceInfo `json:"maxSize,omitempty"`
 }
 
 // CompressorForFile returns compression name to be used for compressing a given file according to policy, using attributes such as name or size.
@@ -46,48 +58,19 @@ func (p *CompressionPolicy) CompressorForFile(e fs.File) compression.Name {
 }
 
 // Merge applies default values from the provided policy.
-func (p *CompressionPolicy) Merge(src CompressionPolicy) {
-	if p.CompressorName == "" {
-		p.CompressorName = src.CompressorName
-	}
-
-	if p.MinSize == 0 {
-		p.MinSize = src.MinSize
-	}
+func (p *CompressionPolicy) Merge(src CompressionPolicy, def *CompressionPolicyDefinition, si snapshot.SourceInfo) {
+	mergeCompressionName(&p.CompressorName, src.CompressorName, &def.CompressorName, si)
+	mergeInt64(&p.MinSize, src.MinSize, &def.MinSize, si)
+	mergeInt64(&p.MaxSize, src.MaxSize, &def.MaxSize, si)
 
-	if p.MaxSize == 0 {
-		p.MaxSize = src.MaxSize
-	}
-
-	p.OnlyCompress = mergeStrings(p.OnlyCompress, src.OnlyCompress)
-	p.NeverCompress = mergeStrings(p.NeverCompress, src.NeverCompress)
+	mergeStrings(&p.OnlyCompress, &p.NoParentOnlyCompress, src.OnlyCompress, src.NoParentOnlyCompress, &def.OnlyCompress, si)
+	mergeStrings(&p.NeverCompress, &p.NoParentNeverCompress, src.NeverCompress, src.NoParentNeverCompress, &def.NeverCompress, si)
 }
 
 var defaultCompressionPolicy = CompressionPolicy{
 	CompressorName: "none",
 }
 
-func mergeStrings(s1, s2 []string) []string {
-	merged := map[string]bool{}
-
-	for _, v := range s1 {
-		merged[v] = true
-	}
-
-	for _, v := range s2 {
-		merged[v] = true
-	}
-
-	var result []string
-	for v := range merged {
-		result = append(result, v)
-	}
-
-	sort.Strings(result)
-
-	return result
-}
-
 func isInSortedSlice(s string, slice []string) bool {
 	x := sort.SearchStrings(slice, s)
 	return x < len(slice) && slice[x] == s
diff --git a/snapshot/policy/error_handling_policy.go b/snapshot/policy/error_handling_policy.go
index 57905340..a9ee5777 100644
--- a/snapshot/policy/error_handling_policy.go
+++ b/snapshot/policy/error_handling_policy.go
@@ -1,5 +1,7 @@
 package policy
 
+import "github.com/kopia/kopia/snapshot"
+
 // ErrorHandlingPolicy controls error hadnling behavior when taking snapshots.
 type ErrorHandlingPolicy struct {
 	// IgnoreFileErrors controls whether or not snapshot operation should fail when a file throws an error on being read
@@ -12,11 +14,18 @@ type ErrorHandlingPolicy struct {
 	IgnoreUnknownTypes *OptionalBool `json:"ignoreUnknownTypes,omitempty"`
 }
 
+// ErrorHandlingPolicyDefinition specifies which policy definition provided the value of a particular field.
+type ErrorHandlingPolicyDefinition struct {
+	IgnoreFileErrors      snapshot.SourceInfo `json:"ignoreFileErrors,omitempty"`
+	IgnoreDirectoryErrors snapshot.SourceInfo `json:"ignoreDirectoryErrors,omitempty"`
+	IgnoreUnknownTypes    snapshot.SourceInfo `json:"ignoreUnknownTypes,omitempty"`
+}
+
 // Merge applies default values from the provided policy.
-func (p *ErrorHandlingPolicy) Merge(src ErrorHandlingPolicy) {
-	mergeOptionalBool(&p.IgnoreFileErrors, src.IgnoreFileErrors)
-	mergeOptionalBool(&p.IgnoreDirectoryErrors, src.IgnoreDirectoryErrors)
-	mergeOptionalBool(&p.IgnoreUnknownTypes, src.IgnoreUnknownTypes)
+func (p *ErrorHandlingPolicy) Merge(src ErrorHandlingPolicy, def *ErrorHandlingPolicyDefinition, si snapshot.SourceInfo) {
+	mergeOptionalBool(&p.IgnoreFileErrors, src.IgnoreFileErrors, &def.IgnoreFileErrors, si)
+	mergeOptionalBool(&p.IgnoreDirectoryErrors, src.IgnoreDirectoryErrors, &def.IgnoreDirectoryErrors, si)
+	mergeOptionalBool(&p.IgnoreUnknownTypes, src.IgnoreUnknownTypes, &def.IgnoreUnknownTypes, si)
 }
 
 // defaultErrorHandlingPolicy is the default error handling policy.
diff --git a/snapshot/policy/expire.go b/snapshot/policy/expire.go
index 14c8809d..7bf08645 100644
--- a/snapshot/policy/expire.go
+++ b/snapshot/policy/expire.go
@@ -51,7 +51,7 @@ func getExpiredSnapshots(ctx context.Context, rep repo.Repository, snapshots []*
 func getExpiredSnapshotsForSource(ctx context.Context, rep repo.Repository, snapshots []*snapshot.Manifest) ([]*snapshot.Manifest, error) {
 	src := snapshots[0].Source
 
-	pol, _, err := GetEffectivePolicy(ctx, rep, src)
+	pol, _, _, err := GetEffectivePolicy(ctx, rep, src)
 	if err != nil {
 		return nil, err
 	}
diff --git a/snapshot/policy/files_policy.go b/snapshot/policy/files_policy.go
index efefd06d..0cecd48a 100644
--- a/snapshot/policy/files_policy.go
+++ b/snapshot/policy/files_policy.go
@@ -1,41 +1,38 @@
 package policy
 
+import "github.com/kopia/kopia/snapshot"
+
 // FilesPolicy describes files to be ignored when taking snapshots.
 type FilesPolicy struct {
-	IgnoreRules         []string `json:"ignore,omitempty"`
-	NoParentIgnoreRules bool     `json:"noParentIgnore,omitempty"`
-
-	DotIgnoreFiles         []string `json:"ignoreDotFiles,omitempty"`
-	NoParentDotIgnoreFiles bool     `json:"noParentDotFiles,omitempty"`
-
+	IgnoreRules            []string      `json:"ignore,omitempty"`
+	NoParentIgnoreRules    bool          `json:"noParentIgnore,omitempty"`
+	DotIgnoreFiles         []string      `json:"ignoreDotFiles,omitempty"`
+	NoParentDotIgnoreFiles bool          `json:"noParentDotFiles,omitempty"`
 	IgnoreCacheDirectories *OptionalBool `json:"ignoreCacheDirs,omitempty"`
+	MaxFileSize            int64         `json:"maxFileSize,omitempty"`
+	OneFileSystem          *OptionalBool `json:"oneFileSystem,omitempty"`
+}
 
-	MaxFileSize int64 `json:"maxFileSize,omitempty"`
-
-	OneFileSystem *OptionalBool `json:"oneFileSystem,omitempty"`
+// FilesPolicyDefinition specifies which policy definition provided the value of a particular field.
+type FilesPolicyDefinition struct {
+	IgnoreRules            snapshot.SourceInfo `json:"ignore,omitempty"`
+	NoParentIgnoreRules    snapshot.SourceInfo `json:"noParentIgnore,omitempty"`
+	DotIgnoreFiles         snapshot.SourceInfo `json:"ignoreDotFiles,omitempty"`
+	NoParentDotIgnoreFiles snapshot.SourceInfo `json:"noParentDotFiles,omitempty"`
+	IgnoreCacheDirectories snapshot.SourceInfo `json:"ignoreCacheDirs,omitempty"`
+	MaxFileSize            snapshot.SourceInfo `json:"maxFileSize,omitempty"`
+	OneFileSystem          snapshot.SourceInfo `json:"oneFileSystem,omitempty"`
 }
 
 // Merge applies default values from the provided policy.
-func (p *FilesPolicy) Merge(src FilesPolicy) {
-	if p.MaxFileSize == 0 {
-		p.MaxFileSize = src.MaxFileSize
-	}
-
-	if len(p.IgnoreRules) == 0 {
-		p.IgnoreRules = src.IgnoreRules
-	}
-
-	if len(p.DotIgnoreFiles) == 0 {
-		p.DotIgnoreFiles = src.DotIgnoreFiles
-	}
-
-	if p.IgnoreCacheDirectories == nil {
-		p.IgnoreCacheDirectories = src.IgnoreCacheDirectories
-	}
-
-	if p.OneFileSystem == nil {
-		p.OneFileSystem = src.OneFileSystem
-	}
+func (p *FilesPolicy) Merge(src FilesPolicy, def *FilesPolicyDefinition, si snapshot.SourceInfo) {
+	mergeStringList(&p.IgnoreRules, src.IgnoreRules, &def.IgnoreRules, si)
+	mergeBool(&p.NoParentIgnoreRules, src.NoParentIgnoreRules, &def.NoParentIgnoreRules, si)
+	mergeStringsReplace(&p.DotIgnoreFiles, src.DotIgnoreFiles, &def.DotIgnoreFiles, si)
+	mergeBool(&p.NoParentDotIgnoreFiles, src.NoParentDotIgnoreFiles, &def.NoParentDotIgnoreFiles, si)
+	mergeOptionalBool(&p.IgnoreCacheDirectories, src.IgnoreCacheDirectories, &def.IgnoreCacheDirectories, si)
+	mergeInt64(&p.MaxFileSize, src.MaxFileSize, &def.MaxFileSize, si)
+	mergeOptionalBool(&p.OneFileSystem, src.OneFileSystem, &def.OneFileSystem, si)
 }
 
 // defaultFilesPolicy is the default file ignore policy.
diff --git a/snapshot/policy/log_detail.go b/snapshot/policy/log_detail.go
index a82bbeb1..19455817 100644
--- a/snapshot/policy/log_detail.go
+++ b/snapshot/policy/log_detail.go
@@ -23,11 +23,3 @@ func (l *LogDetail) OrDefault(def LogDetail) LogDetail {
 func NewLogDetail(l LogDetail) *LogDetail {
 	return &l
 }
-
-func mergeLogLevel(target **LogDetail, src *LogDetail) {
-	if *target == nil && src != nil {
-		b := *src
-
-		*target = &b
-	}
-}
diff --git a/snapshot/policy/logging_policy.go b/snapshot/policy/logging_policy.go
index 93c138b5..afa43101 100644
--- a/snapshot/policy/logging_policy.go
+++ b/snapshot/policy/logging_policy.go
@@ -1,15 +1,23 @@
 package policy
 
+import "github.com/kopia/kopia/snapshot"
+
 // DirLoggingPolicy represents the policy for logging directory information when snapshotting.
 type DirLoggingPolicy struct {
 	Snapshotted *LogDetail `json:"snapshotted,omitempty"`
 	Ignored     *LogDetail `json:"ignored,omitempty"`
 }
 
+// DirLoggingPolicyDefinition specifies which policy definition provided the value of a particular field.
+type DirLoggingPolicyDefinition struct {
+	Snapshotted snapshot.SourceInfo `json:"snapshotted,omitempty"`
+	Ignored     snapshot.SourceInfo `json:"ignored,omitempty"`
+}
+
 // Merge merges the provided directory logging policy.
-func (p *DirLoggingPolicy) Merge(src DirLoggingPolicy) {
-	mergeLogLevel(&p.Snapshotted, src.Snapshotted)
-	mergeLogLevel(&p.Ignored, src.Ignored)
+func (p *DirLoggingPolicy) Merge(src DirLoggingPolicy, def *DirLoggingPolicyDefinition, si snapshot.SourceInfo) {
+	mergeLogLevel(&p.Snapshotted, src.Snapshotted, &def.Snapshotted, si)
+	mergeLogLevel(&p.Ignored, src.Ignored, &def.Ignored, si)
 }
 
 // EntryLoggingPolicy represents the policy for logging entry information when snapshotting.
@@ -20,12 +28,20 @@ type EntryLoggingPolicy struct {
 	CacheMiss   *LogDetail `json:"cacheMiss,omitempty"`
 }
 
+// EntryLoggingPolicyDefinition specifies which policy definition provided the value of a particular field.
+type EntryLoggingPolicyDefinition struct {
+	Snapshotted snapshot.SourceInfo `json:"snapshotted,omitempty"`
+	Ignored     snapshot.SourceInfo `json:"ignored,omitempty"`
+	CacheHit    snapshot.SourceInfo `json:"cacheHit,omitempty"`
+	CacheMiss   snapshot.SourceInfo `json:"cacheMiss,omitempty"`
+}
+
 // Merge merges the provided entry logging policy.
-func (p *EntryLoggingPolicy) Merge(src EntryLoggingPolicy) {
-	mergeLogLevel(&p.Snapshotted, src.Snapshotted)
-	mergeLogLevel(&p.Ignored, src.Ignored)
-	mergeLogLevel(&p.CacheHit, src.CacheHit)
-	mergeLogLevel(&p.CacheMiss, src.CacheMiss)
+func (p *EntryLoggingPolicy) Merge(src EntryLoggingPolicy, def *EntryLoggingPolicyDefinition, si snapshot.SourceInfo) {
+	mergeLogLevel(&p.Snapshotted, src.Snapshotted, &def.Snapshotted, si)
+	mergeLogLevel(&p.Ignored, src.Ignored, &def.Ignored, si)
+	mergeLogLevel(&p.CacheHit, src.CacheHit, &def.CacheHit, si)
+	mergeLogLevel(&p.CacheMiss, src.CacheMiss, &def.CacheMiss, si)
 }
 
 // LoggingPolicy describes policy for emitting logs during snapshots.
@@ -34,10 +50,16 @@ type LoggingPolicy struct {
 	Entries     EntryLoggingPolicy `json:"entries,omitempty"`
 }
 
+// LoggingPolicyDefinition specifies which policy definition provided the value of a particular field.
+type LoggingPolicyDefinition struct {
+	Directories DirLoggingPolicyDefinition   `json:"directories,omitempty"`
+	Entries     EntryLoggingPolicyDefinition `json:"entries,omitempty"`
+}
+
 // Merge applies default values from the provided policy.
-func (p *LoggingPolicy) Merge(src LoggingPolicy) {
-	p.Directories.Merge(src.Directories)
-	p.Entries.Merge(src.Entries)
+func (p *LoggingPolicy) Merge(src LoggingPolicy, def *LoggingPolicyDefinition, si snapshot.SourceInfo) {
+	p.Directories.Merge(src.Directories, &def.Directories, si)
+	p.Entries.Merge(src.Entries, &def.Entries, si)
 }
 
 // defaultLoggingPolicy is the default logs policy.
diff --git a/snapshot/policy/optional.go b/snapshot/policy/optional.go
index e9e9d4a6..fd147f71 100644
--- a/snapshot/policy/optional.go
+++ b/snapshot/policy/optional.go
@@ -15,11 +15,3 @@ func (b *OptionalBool) OrDefault(def bool) bool {
 func newOptionalBool(b OptionalBool) *OptionalBool {
 	return &b
 }
-
-func mergeOptionalBool(target **OptionalBool, src *OptionalBool) {
-	if *target == nil && src != nil {
-		v := *src
-
-		*target = &v
-	}
-}
diff --git a/snapshot/policy/policy.go b/snapshot/policy/policy.go
index 903a3bc1..a53d1aa2 100644
--- a/snapshot/policy/policy.go
+++ b/snapshot/policy/policy.go
@@ -27,11 +27,23 @@ type Policy struct {
 	ErrorHandlingPolicy ErrorHandlingPolicy `json:"errorHandling,omitempty"`
 	SchedulingPolicy    SchedulingPolicy    `json:"scheduling,omitempty"`
 	CompressionPolicy   CompressionPolicy   `json:"compression,omitempty"`
-	Actions             ActionsPolicy       `json:"actions"`
-	LoggingPolicy       LoggingPolicy       `json:"logging"`
+	Actions             ActionsPolicy       `json:"actions,omitempty"`
+	LoggingPolicy       LoggingPolicy       `json:"logging,omitempty"`
 	NoParent            bool                `json:"noParent,omitempty"`
 }
 
+// Definition corresponds 1:1 to Policy and each field specifies the snapshot.SourceInfo
+// where a particular policy field was specified.
+type Definition struct {
+	RetentionPolicy     RetentionPolicyDefinition     `json:"retention,omitempty"`
+	FilesPolicy         FilesPolicyDefinition         `json:"files,omitempty"`
+	ErrorHandlingPolicy ErrorHandlingPolicyDefinition `json:"errorHandling,omitempty"`
+	SchedulingPolicy    SchedulingPolicyDefinition    `json:"scheduling,omitempty"`
+	CompressionPolicy   CompressionPolicyDefinition   `json:"compression,omitempty"`
+	Actions             ActionsPolicyDefinition       `json:"actions,omitempty"`
+	LoggingPolicy       LoggingPolicyDefinition       `json:"logging,omitempty"`
+}
+
 func (p *Policy) String() string {
 	var buf bytes.Buffer
 
@@ -59,40 +71,6 @@ func (p *Policy) Target() snapshot.SourceInfo {
 	}
 }
 
-// MergePolicies computes the policy by applying the specified list of policies in order.
-func MergePolicies(policies []*Policy) *Policy {
-	var merged Policy
-
-	for _, p := range policies {
-		if p.NoParent {
-			return &merged
-		}
-
-		merged.RetentionPolicy.Merge(p.RetentionPolicy)
-		merged.FilesPolicy.Merge(p.FilesPolicy)
-		merged.ErrorHandlingPolicy.Merge(p.ErrorHandlingPolicy)
-		merged.SchedulingPolicy.Merge(p.SchedulingPolicy)
-		merged.CompressionPolicy.Merge(p.CompressionPolicy)
-		merged.Actions.Merge(p.Actions)
-		merged.LoggingPolicy.Merge(p.LoggingPolicy)
-	}
-
-	// Merge default expiration policy.
-	merged.RetentionPolicy.Merge(defaultRetentionPolicy)
-	merged.FilesPolicy.Merge(defaultFilesPolicy)
-	merged.ErrorHandlingPolicy.Merge(defaultErrorHandlingPolicy)
-	merged.SchedulingPolicy.Merge(defaultSchedulingPolicy)
-	merged.CompressionPolicy.Merge(defaultCompressionPolicy)
-	merged.Actions.Merge(defaultActionsPolicy)
-	merged.LoggingPolicy.Merge(defaultLoggingPolicy)
-
-	if len(policies) > 0 {
-		merged.Actions.MergeNonInheritable(policies[0].Actions)
-	}
-
-	return &merged
-}
-
 // ValidatePolicy returns error if the given policy is invalid.
 // Currently, only SchedulingPolicy is validated.
 func ValidatePolicy(pol *Policy) error {
diff --git a/snapshot/policy/policy_manager.go b/snapshot/policy/policy_manager.go
index 3cd8a109..dc443d72 100644
--- a/snapshot/policy/policy_manager.go
+++ b/snapshot/policy/policy_manager.go
@@ -40,14 +40,14 @@
 // GetEffectivePolicy calculates effective snapshot policy for a given source by combining the source-specifc policy (if any)
 // with parent policies. The source must contain a path.
 // Returns the effective policies and all source policies that contributed to that (most specific first).
-func GetEffectivePolicy(ctx context.Context, rep repo.Repository, si snapshot.SourceInfo) (effective *Policy, sources []*Policy, e error) {
+func GetEffectivePolicy(ctx context.Context, rep repo.Repository, si snapshot.SourceInfo) (effective *Policy, definition *Definition, sources []*Policy, e error) {
 	var md []*manifest.EntryMetadata
 
 	// Find policies applying to paths all the way up to the root.
 	for tmp := si; len(si.Path) > 0; {
 		manifests, err := rep.FindManifests(ctx, labelsForSource(tmp))
 		if err != nil {
-			return nil, nil, errors.Wrapf(err, "unable to find manifest for source %v", tmp)
+			return nil, nil, nil, errors.Wrapf(err, "unable to find manifest for source %v", tmp)
 		}
 
 		md = append(md, manifests...)
@@ -63,7 +63,7 @@ func GetEffectivePolicy(ctx context.Context, rep repo.Repository, si snapshot.So
 	// Try user@host policy
 	userHostManifests, err := rep.FindManifests(ctx, labelsForSource(snapshot.SourceInfo{Host: si.Host, UserName: si.UserName}))
 	if err != nil {
-		return nil, nil, errors.Wrap(err, "unable to find user@host manifest")
+		return nil, nil, nil, errors.Wrap(err, "unable to find user@host manifest")
 	}
 
 	md = append(md, userHostManifests...)
@@ -71,7 +71,7 @@ func GetEffectivePolicy(ctx context.Context, rep repo.Repository, si snapshot.So
 	// Try host-level policy.
 	hostManifests, err := rep.FindManifests(ctx, labelsForSource(snapshot.SourceInfo{Host: si.Host}))
 	if err != nil {
-		return nil, nil, errors.Wrap(err, "unable to find host-level manifest")
+		return nil, nil, nil, errors.Wrap(err, "unable to find host-level manifest")
 	}
 
 	md = append(md, hostManifests...)
@@ -79,7 +79,7 @@ func GetEffectivePolicy(ctx context.Context, rep repo.Repository, si snapshot.So
 	// Global policy.
 	globalManifests, err := rep.FindManifests(ctx, labelsForSource(GlobalPolicySourceInfo))
 	if err != nil {
-		return nil, nil, errors.Wrap(err, "unable to find global manifest")
+		return nil, nil, nil, errors.Wrap(err, "unable to find global manifest")
 	}
 
 	md = append(md, globalManifests...)
@@ -89,16 +89,16 @@ func GetEffectivePolicy(ctx context.Context, rep repo.Repository, si snapshot.So
 	for _, em := range md {
 		p := &Policy{}
 		if err := loadPolicyFromManifest(ctx, rep, em.ID, p); err != nil {
-			return nil, nil, errors.Wrapf(err, "got unexpected error when loading policy item %v", em.ID)
+			return nil, nil, nil, errors.Wrapf(err, "got unexpected error when loading policy item %v", em.ID)
 		}
 
 		policies = append(policies, p)
 	}
 
-	merged := MergePolicies(policies)
+	merged, def := MergePolicies(policies)
 	merged.Labels = labelsForSource(si)
 
-	return merged, policies, nil
+	return merged, def, policies, nil
 }
 
 // GetDefinedPolicy returns the policy defined on the provided snapshot.SourceInfo or ErrPolicyNotFound if not present.
@@ -229,7 +229,7 @@ func TreeForSource(ctx context.Context, rep repo.Repository, si snapshot.SourceI
 func applicablePoliciesForSource(ctx context.Context, rep repo.Repository, si snapshot.SourceInfo) (map[string]*Policy, error) {
 	result := map[string]*Policy{}
 
-	pol, _, err := GetEffectivePolicy(ctx, rep, si)
+	pol, _, _, err := GetEffectivePolicy(ctx, rep, si)
 	if err != nil {
 		return nil, err
 	}
diff --git a/snapshot/policy/policy_merge.go b/snapshot/policy/policy_merge.go
new file mode 100644
index 00000000..7a451010
--- /dev/null
+++ b/snapshot/policy/policy_merge.go
@@ -0,0 +1,149 @@
+package policy
+
+import (
+	"sort"
+
+	"github.com/kopia/kopia/repo/compression"
+	"github.com/kopia/kopia/snapshot"
+)
+
+// MergePolicies computes the policy by applying the specified list of policies in order from most
+// specific to most general.
+func MergePolicies(policies []*Policy) (*Policy, *Definition) {
+	var (
+		merged Policy
+		def    Definition
+	)
+
+	for _, p := range policies {
+		merged.RetentionPolicy.Merge(p.RetentionPolicy, &def.RetentionPolicy, p.Target())
+		merged.FilesPolicy.Merge(p.FilesPolicy, &def.FilesPolicy, p.Target())
+		merged.ErrorHandlingPolicy.Merge(p.ErrorHandlingPolicy, &def.ErrorHandlingPolicy, p.Target())
+		merged.SchedulingPolicy.Merge(p.SchedulingPolicy, &def.SchedulingPolicy, p.Target())
+		merged.CompressionPolicy.Merge(p.CompressionPolicy, &def.CompressionPolicy, p.Target())
+		merged.Actions.Merge(p.Actions, &def.Actions, p.Target())
+		merged.LoggingPolicy.Merge(p.LoggingPolicy, &def.LoggingPolicy, p.Target())
+
+		if p.NoParent {
+			return &merged, &def
+		}
+	}
+
+	// Merge default expiration policy.
+	merged.RetentionPolicy.Merge(defaultRetentionPolicy, &def.RetentionPolicy, GlobalPolicySourceInfo)
+	merged.FilesPolicy.Merge(defaultFilesPolicy, &def.FilesPolicy, GlobalPolicySourceInfo)
+	merged.ErrorHandlingPolicy.Merge(defaultErrorHandlingPolicy, &def.ErrorHandlingPolicy, GlobalPolicySourceInfo)
+	merged.SchedulingPolicy.Merge(defaultSchedulingPolicy, &def.SchedulingPolicy, GlobalPolicySourceInfo)
+	merged.CompressionPolicy.Merge(defaultCompressionPolicy, &def.CompressionPolicy, GlobalPolicySourceInfo)
+	merged.Actions.Merge(defaultActionsPolicy, &def.Actions, GlobalPolicySourceInfo)
+	merged.LoggingPolicy.Merge(defaultLoggingPolicy, &def.LoggingPolicy, GlobalPolicySourceInfo)
+
+	if len(policies) > 0 {
+		merged.Actions.MergeNonInheritable(policies[0].Actions)
+	}
+
+	return &merged, &def
+}
+
+func mergeOptionalBool(target **OptionalBool, src *OptionalBool, def *snapshot.SourceInfo, si snapshot.SourceInfo) {
+	if *target == nil && src != nil {
+		v := *src
+
+		*target = &v
+		*def = si
+	}
+}
+
+func mergeOptionalInt(target **int, src *int, def *snapshot.SourceInfo, si snapshot.SourceInfo) {
+	if *target == nil && src != nil {
+		v := *src
+
+		*target = &v
+		*def = si
+	}
+}
+
+func mergeStringsReplace(target *[]string, src []string, def *snapshot.SourceInfo, si snapshot.SourceInfo) {
+	if len(*target) == 0 && len(src) > 0 {
+		*target = src
+		*def = si
+	}
+}
+
+func mergeStrings(target *[]string, targetNoParent *bool, src []string, noParent bool, def *snapshot.SourceInfo, si snapshot.SourceInfo) {
+	if *targetNoParent {
+		// merges prevented
+		return
+	}
+
+	merged := map[string]bool{}
+
+	for _, v := range *target {
+		merged[v] = true
+	}
+
+	for _, v := range src {
+		merged[v] = true
+		*def = si
+	}
+
+	var result []string
+	for v := range merged {
+		result = append(result, v)
+	}
+
+	sort.Strings(result)
+
+	*target = result
+
+	if noParent {
+		// prevent future merges.
+		*targetNoParent = noParent
+	}
+}
+
+func mergeCompressionName(target *compression.Name, src compression.Name, def *snapshot.SourceInfo, si snapshot.SourceInfo) {
+	if *target == "" && src != "" {
+		*target = src
+		*def = si
+	}
+}
+
+func mergeInt64(target *int64, src int64, def *snapshot.SourceInfo, si snapshot.SourceInfo) {
+	if *target == 0 && src != 0 {
+		*target = src
+		*def = si
+	}
+}
+
+func mergeBool(target *bool, src bool, def *snapshot.SourceInfo, si snapshot.SourceInfo) {
+	if !*target && src {
+		*target = src
+		*def = si
+	}
+}
+
+func mergeStringList(target *[]string, src []string, def *snapshot.SourceInfo, si snapshot.SourceInfo) {
+	if len(*target) == 0 && len(src) != 0 {
+		*target = src
+		*def = si
+	}
+}
+
+func mergeLogLevel(target **LogDetail, src *LogDetail, def *snapshot.SourceInfo, si snapshot.SourceInfo) {
+	if *target == nil && src != nil {
+		b := *src
+
+		*target = &b
+		*def = si
+	}
+}
+
+func mergeActionCommand(target **ActionCommand, src *ActionCommand, def *snapshot.SourceInfo, si snapshot.SourceInfo) {
+	if *target == nil && src != nil {
+		b := *src
+
+		*target = &b
+		*def = si
+	}
+}
diff --git a/snapshot/policy/policy_tree.go b/snapshot/policy/policy_tree.go
index 88a44ecc..4e3764cb 100644
--- a/snapshot/policy/policy_tree.go
+++ b/snapshot/policy/policy_tree.go
@@ -15,6 +15,9 @@
 	Actions:             defaultActionsPolicy,
 }
 
+// DefaultDefinition provides the Definition for the default policy.
+var DefaultDefinition = &Definition{}
+
 // Tree represents a node in the policy tree, where a policy can be
 // defined. A nil tree is a valid tree with default policy.
 type Tree struct {
diff --git a/snapshot/policy/retention_policy.go b/snapshot/policy/retention_policy.go
index fe0a997c..65e9f501 100644
--- a/snapshot/policy/retention_policy.go
+++ b/snapshot/policy/retention_policy.go
@@ -25,6 +25,16 @@ type RetentionPolicy struct {
 	KeepAnnual  *int `json:"keepAnnual,omitempty"`
 }
 
+// RetentionPolicyDefinition specifies which policy definition provided the value of a particular field.
+type RetentionPolicyDefinition struct {
+	KeepLatest  snapshot.SourceInfo `json:"keepLatest,omitempty"`
+	KeepHourly  snapshot.SourceInfo `json:"keepHourly,omitempty"`
+	KeepDaily   snapshot.SourceInfo `json:"keepDaily,omitempty"`
+	KeepWeekly  snapshot.SourceInfo `json:"keepWeekly,omitempty"`
+	KeepMonthly snapshot.SourceInfo `json:"keepMonthly,omitempty"`
+	KeepAnnual  snapshot.SourceInfo `json:"keepAnnual,omitempty"`
+}
+
 // ComputeRetentionReasons computes the reasons why each snapshot is retained, based on
 // the settings in retention policy and stores them in RetentionReason field.
 func (r *RetentionPolicy) ComputeRetentionReasons(manifests []*snapshot.Manifest) {
@@ -193,28 +203,11 @@ func hoursAgo(base time.Time, n int) time.Time {
 }
 
 // Merge applies default values from the provided policy.
-func (r *RetentionPolicy) Merge(src RetentionPolicy) {
-	if r.KeepLatest == nil {
-		r.KeepLatest = src.KeepLatest
-	}
-
-	if r.KeepHourly == nil {
-		r.KeepHourly = src.KeepHourly
-	}
-
-	if r.KeepDaily == nil {
-		r.KeepDaily = src.KeepDaily
-	}
-
-	if r.KeepWeekly == nil {
-		r.KeepWeekly = src.KeepWeekly
-	}
-
-	if r.KeepMonthly == nil {
-		r.KeepMonthly = src.KeepMonthly
-	}
-
-	if r.KeepAnnual == nil {
-		r.KeepAnnual = src.KeepAnnual
-	}
+func (r *RetentionPolicy) Merge(src RetentionPolicy, def *RetentionPolicyDefinition, si snapshot.SourceInfo) {
+	mergeOptionalInt(&r.KeepLatest, src.KeepLatest, &def.KeepLatest, si)
+	mergeOptionalInt(&r.KeepHourly, src.KeepHourly, &def.KeepHourly, si)
+	mergeOptionalInt(&r.KeepDaily, src.KeepDaily, &def.KeepDaily, si)
+	mergeOptionalInt(&r.KeepWeekly, src.KeepWeekly, &def.KeepWeekly, si)
+	mergeOptionalInt(&r.KeepMonthly, src.KeepMonthly, &def.KeepMonthly, si)
+	mergeOptionalInt(&r.KeepAnnual, src.KeepAnnual, &def.KeepAnnual, si)
 }
diff --git a/snapshot/policy/scheduling_policy.go b/snapshot/policy/scheduling_policy.go
index 75def143..0daf8225 100644
--- a/snapshot/policy/scheduling_policy.go
+++ b/snapshot/policy/scheduling_policy.go
@@ -55,9 +55,17 @@ func SortAndDedupeTimesOfDay(tod []TimeOfDay) []TimeOfDay {
 
 // SchedulingPolicy describes policy for scheduling snapshots.
 type SchedulingPolicy struct {
-	IntervalSeconds int64       `json:"intervalSeconds,omitempty"`
-	TimesOfDay      []TimeOfDay `json:"timeOfDay,omitempty"`
-	Manual          bool        `json:"manual,omitempty"`
+	IntervalSeconds    int64       `json:"intervalSeconds,omitempty"`
+	TimesOfDay         []TimeOfDay `json:"timeOfDay,omitempty"`
+	NoParentTimesOfDay bool        `json:"noParentTimeOfDay,omitempty"`
+	Manual             bool        `json:"manual,omitempty"`
+}
+
+// SchedulingPolicyDefinition specifies which policy definition provided the value of a particular field.
+type SchedulingPolicyDefinition struct {
+	IntervalSeconds snapshot.SourceInfo `json:"intervalSeconds,omitempty"`
+	TimesOfDay      snapshot.SourceInfo `json:"timeOfDay,omitempty"`
+	Manual          snapshot.SourceInfo `json:"manual,omitempty"`
 }
 
 // Interval returns the snapshot interval or zero if not specified.
@@ -111,17 +119,23 @@ func (p *SchedulingPolicy) NextSnapshotTime(previousSnapshotTime, now time.Time)
 }
 
 // Merge applies default values from the provided policy.
-func (p *SchedulingPolicy) Merge(src SchedulingPolicy) {
-	if p.IntervalSeconds == 0 {
-		p.IntervalSeconds = src.IntervalSeconds
-	}
+func (p *SchedulingPolicy) Merge(src SchedulingPolicy, def *SchedulingPolicyDefinition, si snapshot.SourceInfo) {
+	mergeInt64(&p.IntervalSeconds, src.IntervalSeconds, &def.IntervalSeconds, si)
 
-	p.TimesOfDay = SortAndDedupeTimesOfDay(
-		append(append([]TimeOfDay(nil), src.TimesOfDay...), p.TimesOfDay...))
+	if len(src.TimesOfDay) > 0 {
+		def.TimesOfDay = si
 
-	if !p.Manual {
-		p.Manual = src.Manual
+		if !p.NoParentTimesOfDay {
+			p.TimesOfDay = SortAndDedupeTimesOfDay(append(append([]TimeOfDay(nil), src.TimesOfDay...), p.TimesOfDay...))
+		}
 	}
+
+	if src.NoParentTimesOfDay {
+		// prevent future merges
+		p.NoParentTimesOfDay = src.NoParentTimesOfDay
+	}
+
+	mergeBool(&p.Manual, src.Manual, &def.Manual, si)
 }
 
 // IsManualSnapshot returns the SchedulingPolicy manual value from the given policy tree.
diff --git a/snapshot/restore/local_fs_output.go b/snapshot/restore/local_fs_output.go
index ad653891..35adddda 100644
--- a/snapshot/restore/local_fs_output.go
+++ b/snapshot/restore/local_fs_output.go
@@ -14,6 +14,7 @@
 	"github.com/kopia/kopia/fs"
 	"github.com/kopia/kopia/fs/localfs"
 	"github.com/kopia/kopia/internal/atomicfile"
+	"github.com/kopia/kopia/internal/iocopy"
 	"github.com/kopia/kopia/snapshot"
 )
 
@@ -44,6 +45,9 @@ type FilesystemOutput struct {
 	// IgnorePermissionErrors causes restore to ignore errors due to invalid permissions.
 	IgnorePermissionErrors bool `json:"ignorePermissionErrors"`
 
+	// When set to true, first write to a temp file and rename it, to ensure there are no partially written files in case of a crash.
+	WriteFilesAtomically bool `json:"writeFilesAtomically"`
+
 	// SkipOwners when set to true causes restore to skip restoring owner information.
 	SkipOwners bool `json:"skipOwners"`
 
@@ -299,6 +303,29 @@ func (o *FilesystemOutput) createDirectory(ctx context.Context, path string) err
 	}
 }
 
+func write(targetPath string, r fs.Reader) error {
+	f, err := os.OpenFile(targetPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600) //nolint:gosec,gomnd
+	if err != nil {
+		return err //nolint:wrapcheck
+	}
+
+	// ensure we always close f. Note that this does not conflict with the
+	// close below, as close is idempotent.
+	defer f.Close() //nolint:errcheck,gosec
+
+	name := f.Name()
+
+	if err := iocopy.JustCopy(f, r); err != nil {
+		return errors.Wrap(err, "cannot write data to file %q "+name)
+	}
+
+	if err := f.Close(); err != nil {
+		return err //nolint:wrapcheck
+	}
+
+	return nil
+}
+
 func (o *FilesystemOutput) copyFileContent(ctx context.Context, targetPath string, f fs.File) error {
 	switch _, err := os.Stat(targetPath); {
 	case os.IsNotExist(err): // copy file below
@@ -320,8 +347,12 @@ func (o *FilesystemOutput) copyFileContent(ctx context.Context, targetPath strin
 
 	log(ctx).Debugf("copying file contents to: %v", targetPath)
 
-	// nolint:wrapcheck
-	return atomicfile.Write(targetPath, r)
+	if o.WriteFilesAtomically {
+		// nolint:wrapcheck
+		return atomicfile.Write(targetPath, r)
+	}
+
+	return write(atomicfile.MaybePrefixLongFilenameOnWindows(targetPath), r)
 }
 
 func isEmptyDirectory(name string) (bool, error) {


Test patch:
diff --git a/snapshot/policy/error_handling_policy_test.go b/snapshot/policy/error_handling_policy_test.go
index c341879c..7a855613 100644
--- a/snapshot/policy/error_handling_policy_test.go
+++ b/snapshot/policy/error_handling_policy_test.go
@@ -3,6 +3,8 @@
 import (
 	"reflect"
 	"testing"
+
+	"github.com/kopia/kopia/snapshot"
 )
 
 func TestErrorHandlingPolicyMerge(t *testing.T) {
@@ -130,7 +132,8 @@ type args struct {
 			IgnoreFileErrors:      tt.fields.IgnoreFileErrors,
 			IgnoreDirectoryErrors: tt.fields.IgnoreDirectoryErrors,
 		}
-		p.Merge(tt.args.src)
+
+		p.Merge(tt.args.src, &ErrorHandlingPolicyDefinition{}, snapshot.SourceInfo{})
 
 		if !reflect.DeepEqual(*p, tt.expResult) {
 			t.Errorf("Policy after merge was not what was expected\n%v != %v", p, tt.expResult)
diff --git a/snapshot/policy/policy_manager_test.go b/snapshot/policy/policy_manager_test.go
index 7a47237c..fa4bb106 100644
--- a/snapshot/policy/policy_manager_test.go
+++ b/snapshot/policy/policy_manager_test.go
@@ -34,7 +34,7 @@ func TestPolicyManagerInheritanceTest(t *testing.T) {
 		Path:     "/some/path2",
 	}, &Policy{
 		RetentionPolicy: RetentionPolicy{
-			KeepDaily: intPtr(66),
+			KeepMonthly: intPtr(66),
 		},
 	}))
 
@@ -50,6 +50,7 @@ func TestPolicyManagerInheritanceTest(t *testing.T) {
 		sourceInfo    snapshot.SourceInfo
 		wantEffective *Policy
 		wantSources   []snapshot.SourceInfo
+		wantDef       Definition
 	}{
 		{
 			sourceInfo:    GlobalPolicySourceInfo,
@@ -75,7 +76,7 @@ func TestPolicyManagerInheritanceTest(t *testing.T) {
 				Host:     "host-a",
 				Path:     "/some/path",
 			},
-			wantEffective: policyWithLabels(defaultPolicyWithKeepDaily(t, 44), map[string]string{
+			wantEffective: policyWithLabels(policyWithKeepDaily(t, DefaultPolicy, 44), map[string]string{
 				"type":       "policy",
 				"policyType": "path",
 				"hostname":   "host-a",
@@ -85,6 +86,11 @@ func TestPolicyManagerInheritanceTest(t *testing.T) {
 			wantSources: []snapshot.SourceInfo{
 				{Host: "host-a"},
 			},
+			wantDef: Definition{
+				RetentionPolicy: RetentionPolicyDefinition{
+					KeepDaily: snapshot.SourceInfo{Host: "host-a"},
+				},
+			},
 		},
 		{
 			sourceInfo: snapshot.SourceInfo{
@@ -92,7 +98,7 @@ func TestPolicyManagerInheritanceTest(t *testing.T) {
 				Host:     "host-a",
 				Path:     "/some/path2/nested",
 			},
-			wantEffective: policyWithLabels(defaultPolicyWithKeepDaily(t, 66), map[string]string{
+			wantEffective: policyWithLabels(policyWithKeepDaily(t, policyWithKeepMonthly(t, DefaultPolicy, 66), 44), map[string]string{
 				"type":       "policy",
 				"policyType": "path",
 				"hostname":   "host-a",
@@ -103,13 +109,20 @@ func TestPolicyManagerInheritanceTest(t *testing.T) {
 				{UserName: "myuser", Path: "/some/path2", Host: "host-a"},
 				{Host: "host-a"},
 			},
+			wantDef: Definition{
+				RetentionPolicy: RetentionPolicyDefinition{
+					KeepDaily:   snapshot.SourceInfo{Host: "host-a"},
+					KeepMonthly: snapshot.SourceInfo{Host: "host-a", UserName: "myuser", Path: "/some/path2"},
+				},
+			},
 		},
 	}
 
 	for _, tc := range cases {
 		tc := tc
+
 		t.Run(tc.sourceInfo.String(), func(t *testing.T) {
-			pol, src, err := GetEffectivePolicy(ctx, env.RepositoryWriter, tc.sourceInfo)
+			pol, def, src, err := GetEffectivePolicy(ctx, env.RepositoryWriter, tc.sourceInfo)
 			if err != nil {
 				t.Fatalf("err: %v", err)
 			}
@@ -129,6 +142,12 @@ func TestPolicyManagerInheritanceTest(t *testing.T) {
 				t.Errorf("want: %v", tc.wantSources)
 				t.Errorf("invalid sources: %v", diff)
 			}
+
+			if diff := cmp.Diff(*def, tc.wantDef); diff != "" {
+				t.Errorf("got: %v", def)
+				t.Errorf("want: %v", tc.wantDef)
+				t.Errorf("invalid definition: %v", diff)
+			}
 		})
 	}
 }
@@ -154,15 +173,24 @@ func policyWithLabels(p *Policy, l map[string]string) *Policy {
 	return &p2
 }
 
-func defaultPolicyWithKeepDaily(t *testing.T, keepDaily int) *Policy {
+func policyWithKeepDaily(t *testing.T, base *Policy, keepDaily int) *Policy {
 	t.Helper()
 
-	p := clonePolicy(t, DefaultPolicy)
+	p := clonePolicy(t, base)
 	p.RetentionPolicy.KeepDaily = &keepDaily
 
 	return p
 }
 
+func policyWithKeepMonthly(t *testing.T, base *Policy, keepMonthly int) *Policy {
+	t.Helper()
+
+	p := clonePolicy(t, base)
+	p.RetentionPolicy.KeepMonthly = &keepMonthly
+
+	return p
+}
+
 func TestPolicyManagerResolvesConflicts(t *testing.T) {
 	ctx, env := repotesting.NewEnvironment(t, repotesting.FormatNotImportant)
 
diff --git a/snapshot/policy/policy_merge_test.go b/snapshot/policy/policy_merge_test.go
new file mode 100644
index 00000000..e655a475
--- /dev/null
+++ b/snapshot/policy/policy_merge_test.go
@@ -0,0 +1,317 @@
+package policy_test
+
+import (
+	"reflect"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/kopia/kopia/repo/compression"
+	"github.com/kopia/kopia/snapshot"
+	"github.com/kopia/kopia/snapshot/policy"
+)
+
+var omittedDefinitionFields = map[string]bool{
+	"Definition.NoParent":                               true, // special
+	"Definition.Labels":                                 true, // special
+	"ActionsPolicyDefinition.BeforeFolder":              true, // non-inheritable field
+	"ActionsPolicyDefinition.AfterFolder":               true, // non-inheritable field
+	"SchedulingPolicyDefinition.NoParentTimesOfDay":     true, // special
+	"CompressionPolicyDefinition.NoParentOnlyCompress":  true,
+	"CompressionPolicyDefinition.NoParentNeverCompress": true,
+}
+
+func TestPolicyDefinition(t *testing.T) {
+	// verify that each field in the policy struct recursively matches a corresponding field
+	// from the policy.Definition() struct.
+	ensureTypesMatch(t, reflect.TypeOf(policy.Policy{}), reflect.TypeOf(policy.Definition{}))
+}
+
+func ensureTypesMatch(t *testing.T, policyType, definitionType reflect.Type) {
+	t.Helper()
+
+	sourceInfoType := reflect.TypeOf(snapshot.SourceInfo{})
+
+	for i := 0; i < policyType.NumField(); i++ {
+		f := policyType.Field(i)
+
+		dt, ok := definitionType.FieldByName(f.Name)
+		if !ok {
+			require.True(t, omittedDefinitionFields[definitionType.Name()+"."+f.Name], "definition field %q not found in %q", f.Name, definitionType.Name())
+			continue
+		}
+
+		t.Logf("f: %v %v", definitionType.Name(), f.Name)
+
+		if f.Type.Kind() == reflect.Struct {
+			ensureTypesMatch(t, f.Type, dt.Type)
+		} else {
+			require.True(t, sourceInfoType.AssignableTo(dt.Type), "invalid type of %v.%v - %v", definitionType.Name(), dt.Name, dt.Type)
+		}
+
+		require.Equal(t, f.Tag.Get("json"), dt.Tag.Get("json"), dt.Name)
+	}
+}
+
+func TestPolicyMerge(t *testing.T) {
+	testPolicyMerge(t, reflect.TypeOf(policy.Policy{}), reflect.TypeOf(policy.Definition{}), "")
+}
+
+// nolint:thelper
+func testPolicyMerge(t *testing.T, policyType, definitionType reflect.Type, prefix string) {
+	for i := 0; i < policyType.NumField(); i++ {
+		f := policyType.Field(i)
+
+		dt, ok := definitionType.FieldByName(f.Name)
+		if !ok {
+			continue
+		}
+
+		if f.Type.Kind() == reflect.Struct {
+			testPolicyMerge(t, f.Type, dt.Type, prefix+f.Name+".")
+
+			continue
+		}
+
+		t.Run(prefix+f.Name, func(t *testing.T) {
+			testPolicyMergeSingleField(t, prefix+f.Name, f.Type)
+		})
+	}
+}
+
+// nolint:thelper
+func testPolicyMergeSingleField(t *testing.T, fieldName string, typ reflect.Type) {
+	var v0, v1, v2 reflect.Value
+
+	switch typ.String() {
+	case "*int":
+		i1 := 111
+		i2 := 222
+
+		v0 = reflect.ValueOf((*int)(nil))
+		v1 = reflect.ValueOf(&i1)
+		v2 = reflect.ValueOf(&i2)
+
+	case "[]string":
+		s1 := []string{"aa"}
+		s2 := []string{"bb", "cc"}
+
+		v0 = reflect.ValueOf([]string{})
+		v1 = reflect.ValueOf(s1)
+		v2 = reflect.ValueOf(s2)
+
+	case "*policy.OptionalBool":
+		ob1 := policy.OptionalBool(false)
+		ob2 := policy.OptionalBool(true)
+
+		v0 = reflect.ValueOf((*policy.OptionalBool)(nil))
+		v1 = reflect.ValueOf(&ob1)
+		v2 = reflect.ValueOf(&ob2)
+	case "bool":
+		v0 = reflect.ValueOf(false)
+		v1 = reflect.ValueOf(false)
+		v2 = reflect.ValueOf(true)
+	case "int64":
+		v0 = reflect.ValueOf(int64(0))
+		v1 = reflect.ValueOf(int64(1))
+		v2 = reflect.ValueOf(int64(2))
+	case "*policy.LogDetail":
+		ld1 := policy.LogDetail(1)
+		ld2 := policy.LogDetail(2)
+
+		v0 = reflect.ValueOf((*policy.LogDetail)(nil))
+		v1 = reflect.ValueOf(&ld1)
+		v2 = reflect.ValueOf(&ld2)
+	case "*policy.ActionCommand":
+		v0 = reflect.ValueOf((*policy.ActionCommand)(nil))
+		v1 = reflect.ValueOf(&policy.ActionCommand{Command: "foo"})
+		v2 = reflect.ValueOf(&policy.ActionCommand{Command: "bar"})
+	case "[]policy.TimeOfDay":
+		v0 = reflect.ValueOf([]policy.TimeOfDay{})
+		v1 = reflect.ValueOf([]policy.TimeOfDay{{Hour: 10}})
+		v2 = reflect.ValueOf([]policy.TimeOfDay{{Hour: 11}})
+	case "compression.Name":
+		v0 = reflect.ValueOf(compression.Name(""))
+		v1 = reflect.ValueOf(compression.Name("foo"))
+		v2 = reflect.ValueOf(compression.Name("bar"))
+
+	default:
+		t.Fatalf("unhandled case: %v - %v - please update test", fieldName, typ)
+	}
+
+	pol0 := policyWithField(fieldName, v0)
+	pol1 := policyWithField(fieldName, v1)
+	pol2 := policyWithField(fieldName, v2)
+
+	pol0.Labels = map[string]string{
+		"hostname": "host",
+		"username": "user",
+		"path":     "/xx",
+	}
+
+	pol1.Labels = map[string]string{
+		"hostname": "host",
+		"username": "user",
+		"path":     "/xx/aa",
+	}
+
+	pol2.Labels = map[string]string{
+		"hostname": "host",
+		"username": "user",
+		"path":     "/xx/bb",
+	}
+
+	tmp := *policy.DefaultPolicy
+	defaultPolicyMod := &tmp
+
+	disableParentMerging(defaultPolicyMod)
+
+	// merging default policy with no-op policy results in default policy
+	result, _ := policy.MergePolicies([]*policy.Policy{pol0, defaultPolicyMod})
+	require.Equal(t, defaultPolicyMod.String(), result.String())
+
+	result, _ = policy.MergePolicies([]*policy.Policy{pol1, pol0, defaultPolicyMod})
+	require.Equal(t, pol1.String(), result.String())
+
+	result, _ = policy.MergePolicies([]*policy.Policy{pol2, pol1, pol0, defaultPolicyMod})
+	require.Equal(t, pol2.String(), result.String())
+
+	result, _ = policy.MergePolicies([]*policy.Policy{pol2, pol0, defaultPolicyMod})
+	require.Equal(t, pol2.String(), result.String())
+}
+
+func policyWithField(fname string, val reflect.Value) *policy.Policy {
+	pol := *policy.DefaultPolicy
+	p := &pol
+
+	disableParentMerging(p)
+	cur := reflect.ValueOf(p).Elem()
+
+	parts := strings.Split(fname, ".")
+	for _, part := range parts[:len(parts)-1] {
+		cur = cur.FieldByName(part)
+	}
+
+	cur.FieldByName(parts[len(parts)-1]).Set(val)
+
+	return p
+}
+
+func disableParentMerging(p *policy.Policy) {
+	// set flags that disable merging of values with the parent, we will
+	// test those cases separately.
+	p.CompressionPolicy.NoParentNeverCompress = true
+	p.CompressionPolicy.NoParentOnlyCompress = true
+	p.SchedulingPolicy.NoParentTimesOfDay = true
+}
+
+func TestPolicyMergeOnlyCompressIncludingParents(t *testing.T) {
+	p0 := &policy.Policy{
+		CompressionPolicy: policy.CompressionPolicy{
+			OnlyCompress: []string{"a", "c", "e"},
+		},
+	}
+
+	p1 := &policy.Policy{
+		CompressionPolicy: policy.CompressionPolicy{
+			OnlyCompress: []string{"b", "d"},
+		},
+	}
+
+	p2 := &policy.Policy{
+		CompressionPolicy: policy.CompressionPolicy{
+			OnlyCompress: []string{"f", "g"},
+		},
+	}
+
+	result, _ := policy.MergePolicies([]*policy.Policy{p0, p1, p2})
+
+	want := *policy.DefaultPolicy
+	want.CompressionPolicy.OnlyCompress = []string{"a", "b", "c", "d", "e", "f", "g"}
+
+	require.Equal(t, want.String(), result.String())
+
+	p2.NoParent = true
+	result, _ = policy.MergePolicies([]*policy.Policy{p0, p1, p2})
+
+	want = policy.Policy{}
+	want.CompressionPolicy.OnlyCompress = []string{"a", "b", "c", "d", "e", "f", "g"}
+
+	require.Equal(t, want.String(), result.String())
+
+	p1.NoParent = true
+	result, _ = policy.MergePolicies([]*policy.Policy{p0, p1, p2})
+
+	want = policy.Policy{}
+	want.CompressionPolicy.OnlyCompress = []string{"a", "b", "c", "d", "e"}
+
+	require.Equal(t, want.String(), result.String())
+
+	p0.NoParent = true
+	result, _ = policy.MergePolicies([]*policy.Policy{p0, p1, p2})
+
+	want = policy.Policy{}
+	want.CompressionPolicy.OnlyCompress = []string{"a", "c", "e"}
+
+	require.Equal(t, want.String(), result.String())
+}
+
+func TestPolicyMergeTimesOfDayIncludingParents(t *testing.T) {
+	tod0 := policy.TimeOfDay{Hour: 10}
+	tod1 := policy.TimeOfDay{Hour: 11}
+	tod2 := policy.TimeOfDay{Hour: 12}
+	tod3 := policy.TimeOfDay{Hour: 13}
+	tod4 := policy.TimeOfDay{Hour: 14}
+	tod5 := policy.TimeOfDay{Hour: 15}
+	tod6 := policy.TimeOfDay{Hour: 16}
+
+	p0 := &policy.Policy{
+		SchedulingPolicy: policy.SchedulingPolicy{
+			TimesOfDay: []policy.TimeOfDay{tod0, tod2, tod4},
+		},
+	}
+
+	p1 := &policy.Policy{
+		SchedulingPolicy: policy.SchedulingPolicy{
+			TimesOfDay: []policy.TimeOfDay{tod1, tod3},
+		},
+	}
+
+	p2 := &policy.Policy{
+		SchedulingPolicy: policy.SchedulingPolicy{
+			TimesOfDay: []policy.TimeOfDay{tod5, tod6},
+		},
+	}
+
+	result, _ := policy.MergePolicies([]*policy.Policy{p0, p1, p2})
+
+	want := *policy.DefaultPolicy
+	want.SchedulingPolicy.TimesOfDay = []policy.TimeOfDay{tod0, tod1, tod2, tod3, tod4, tod5, tod6}
+
+	require.Equal(t, want.String(), result.String())
+
+	p2.NoParent = true
+	result, _ = policy.MergePolicies([]*policy.Policy{p0, p1, p2})
+
+	want = policy.Policy{}
+	want.SchedulingPolicy.TimesOfDay = []policy.TimeOfDay{tod0, tod1, tod2, tod3, tod4, tod5, tod6}
+
+	require.Equal(t, want.String(), result.String())
+
+	p1.NoParent = true
+	result, _ = policy.MergePolicies([]*policy.Policy{p0, p1, p2})
+
+	want = policy.Policy{}
+	want.SchedulingPolicy.TimesOfDay = []policy.TimeOfDay{tod0, tod1, tod2, tod3, tod4}
+
+	require.Equal(t, want.String(), result.String())
+
+	p0.NoParent = true
+	result, _ = policy.MergePolicies([]*policy.Policy{p0, p1, p2})
+
+	want = policy.Policy{}
+	want.SchedulingPolicy.TimesOfDay = []policy.TimeOfDay{tod0, tod2, tod4}
+
+	require.Equal(t, want.String(), result.String())
+}