Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package kopia for openSUSE:Factory checked in at 2025-12-05 16:51:24 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/kopia (Old) and /work/SRC/openSUSE:Factory/.kopia.new.1939 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "kopia" Fri Dec 5 16:51:24 2025 rev:10 rq:1321061 version:0.22.3 Changes: -------- --- /work/SRC/openSUSE:Factory/kopia/kopia.changes 2025-11-27 15:22:07.722415835 +0100 +++ /work/SRC/openSUSE:Factory/.kopia.new.1939/kopia.changes 2025-12-05 16:52:31.562076332 +0100 @@ -1,0 +2,21 @@ +Thu Dec 04 06:26:40 UTC 2025 - Johannes Kastl <[email protected]> + +- Update to version 0.22.3: + * Defect Fixes + - Fixes regression in dependency used for compression (#5049) + * Snapshots + - New Feature localfs support for passing options (#5044) by + Jarek Kowalski + * CI/CD + - Remove ineffective omitempty tags (#5037) by Julio López + * Dependencies + - build(deps): bump docker/setup-qemu-action in the docker + group (#5054) + - build(deps): bump github.com/klauspost/reedsolomon from + 1.12.5 to 1.12.6 (#5051) + - build(deps): bump the github-actions group with 4 updates + (#5053) + - build(deps): bump github.com/klauspost/compress from 1.18.1 + to 1.18.2 (#5052) + +------------------------------------------------------------------- Old: ---- kopia-0.22.2.obscpio New: ---- kopia-0.22.3.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ kopia.spec ++++++ --- /var/tmp/diff_new_pack.i1JyhY/_old 2025-12-05 16:52:33.958176336 +0100 +++ /var/tmp/diff_new_pack.i1JyhY/_new 2025-12-05 16:52:33.974177004 +0100 @@ -17,7 +17,7 @@ Name: kopia -Version: 0.22.2 +Version: 0.22.3 Release: 0 Summary: Cross-platform backup tool with fast incremental backups License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.i1JyhY/_old 2025-12-05 16:52:34.130183515 +0100 +++ /var/tmp/diff_new_pack.i1JyhY/_new 2025-12-05 16:52:34.146184183 +0100 @@ -3,7 +3,7 @@ <param name="url">https://github.com/kopia/kopia</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v0.22.2</param> + <param name="revision">v0.22.3</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.i1JyhY/_old 2025-12-05 16:52:34.214187021 +0100 +++ /var/tmp/diff_new_pack.i1JyhY/_new 2025-12-05 16:52:34.238188023 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/kopia/kopia</param> - <param name="changesrevision">e456f78fa2d15b102988eee1025a2451eeaa3ebf</param></service></servicedata> + <param name="changesrevision">154bf56899228e5c95fb3176b9c6901bbe4ca97b</param></service></servicedata> (No newline at EOF) ++++++ kopia-0.22.2.obscpio -> kopia-0.22.3.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kopia-0.22.2/cli/app.go new/kopia-0.22.3/cli/app.go --- old/kopia-0.22.2/cli/app.go 2025-11-26 08:05:28.000000000 +0100 +++ new/kopia-0.22.3/cli/app.go 2025-12-02 06:27:24.000000000 +0100 @@ -122,7 +122,6 @@ type App struct { // global flags enableAutomaticMaintenance bool - pf profileFlags progress *cliProgress restoreProgress RestoreProgress initialUpdateCheckDelay time.Duration @@ -295,7 +294,6 @@ c.setupOSSpecificKeychainFlags(c, app) - c.pf.setup(app) c.progress.setup(c, app) c.blob.setup(c, app) @@ -402,11 +400,7 @@ func (c *App) noRepositoryAction(act func(ctx context.Context) error) func(ctx *kingpin.ParseContext) error { return func(kpc *kingpin.ParseContext) error { - return c.runAppWithContext(kpc.SelectedCommand, func(ctx context.Context) error { - return c.pf.withProfiling(func() error { - return act(ctx) - }) - }) + return c.runAppWithContext(kpc.SelectedCommand, act) } } @@ -524,11 +518,7 @@ func (c *App) baseActionWithContext(act func(ctx context.Context) error) func(ctx *kingpin.ParseContext) error { return func(kpc *kingpin.ParseContext) error { - return c.runAppWithContext(kpc.SelectedCommand, func(ctx context.Context) error { - return c.pf.withProfiling(func() error { - return act(ctx) - }) - }) + return c.runAppWithContext(kpc.SelectedCommand, act) } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kopia-0.22.2/cli/observability_flags.go new/kopia-0.22.3/cli/observability_flags.go --- old/kopia-0.22.2/cli/observability_flags.go 2025-11-26 08:05:28.000000000 +0100 +++ new/kopia-0.22.3/cli/observability_flags.go 2025-12-02 06:27:24.000000000 +0100 @@ -43,6 +43,8 @@ } type observabilityFlags struct { + outputDirectory string + dumpAllocatorStats bool enablePProfEndpoint bool metricsListenAddr string @@ -53,10 +55,11 @@ metricsPushUsername string metricsPushPassword string metricsPushFormat string - metricsOutputDir string - outputFilePrefix string + otlpTrace bool + saveMetrics bool + pf profileFlags - otlpTrace bool + outputSubdirectoryName string stopPusher chan struct{} pusherWG sync.WaitGroup @@ -90,16 +93,17 @@ app.Flag("metrics-push-format", "Format to use for push gateway").Envar(svc.EnvName("KOPIA_METRICS_FORMAT")).Hidden().EnumVar(&c.metricsPushFormat, formats...) - app.Flag("metrics-directory", "Directory where the metrics should be saved when kopia exits. A file per process execution will be created in this directory").Hidden().StringVar(&c.metricsOutputDir) + //nolint:lll + app.Flag("diagnostics-output-directory", "Directory where the diagnostics output should be stored saved when kopia exits. Diagnostics data includes among others: metrics, traces, profiles. The output files are stored in a sub-directory for each kopia (process) execution").Hidden().Default(filepath.Join(os.TempDir(), "kopia-diagnostics")).StringVar(&c.outputDirectory) + + app.Flag("metrics-store-on-exit", "Writes metrics to a file in a sub-directory of the directory specified with the --diagnostics-output-directory").Hidden().BoolVar(&c.saveMetrics) + + c.pf.setup(app) app.PreAction(c.initialize) } func (c *observabilityFlags) initialize(ctx *kingpin.ParseContext) error { - if c.metricsOutputDir == "" { - return nil - } - // write to a separate file per command and process execution to avoid // conflicts with previously created files command := "unknown" @@ -107,7 +111,11 @@ command = strings.ReplaceAll(cmd.FullCommand(), " ", "-") } - c.outputFilePrefix = clock.Now().Format("20060102-150405-") + command + c.outputSubdirectoryName = clock.Now().Format("20060102-150405-") + command + + if (c.saveMetrics || c.pf.saveProfiles || c.pf.profileCPU) && c.outputDirectory == "" { + return errors.New("writing diagnostics output requires a non-empty directory name (specified with the '--diagnostics-output-directory' flag)") + } return nil } @@ -121,6 +129,12 @@ defer c.stop(ctx) + if err := c.pf.start(ctx, filepath.Join(c.outputDirectory, c.outputSubdirectoryName)); err != nil { + return errors.Wrap(err, "failed to start profiling") + } + + defer c.pf.stop(ctx) + if spanName != "" { tctx, span := tracer.Start(ctx, spanName, oteltrace.WithSpanKind(oteltrace.SpanKindClient)) ctx = tctx @@ -138,18 +152,26 @@ return err } - if c.metricsOutputDir != "" { - c.metricsOutputDir = filepath.Clean(c.metricsOutputDir) - + if c.saveMetrics { // ensure the metrics output dir can be created - if err := os.MkdirAll(c.metricsOutputDir, DirMode); err != nil { - return errors.Wrapf(err, "could not create metrics output directory: %s", c.metricsOutputDir) + if _, err := mkSubdirectories(c.outputDirectory, c.outputSubdirectoryName); err != nil { + return err } } return c.maybeStartTraceExporter(ctx) } +func mkSubdirectories(directoryNames ...string) (dirName string, err error) { + dirName = filepath.Join(directoryNames...) + + if err := os.MkdirAll(dirName, DirMode); err != nil { + return "", errors.Wrapf(err, "could not create '%q' subdirectory to save diagnostics output", dirName) + } + + return dirName, nil +} + // Starts observability listener when a listener address is specified. func (c *observabilityFlags) maybeStartListener(ctx context.Context) { if c.metricsListenAddr == "" { @@ -262,11 +284,13 @@ } } - if c.metricsOutputDir != "" { - filename := filepath.Join(c.metricsOutputDir, c.outputFilePrefix+".prom") - - if err := prometheus.WriteToTextfile(filename, prometheus.DefaultGatherer); err != nil { - log(ctx).Warnf("unable to write metrics file '%s': %v", filename, err) + if c.saveMetrics { + if metricsDir, err := mkSubdirectories(c.outputDirectory, c.outputSubdirectoryName); err != nil { + log(ctx).Warnf("unable to create metrics output directory '%s': %v", metricsDir, err) + } else { + if err := prometheus.WriteToTextfile(filepath.Join(metricsDir, "kopia-metrics.prom"), prometheus.DefaultGatherer); err != nil { + log(ctx).Warnf("unable to write metrics to file: %v", err) + } } } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kopia-0.22.2/cli/observability_flags_test.go new/kopia-0.22.3/cli/observability_flags_test.go --- old/kopia-0.22.2/cli/observability_flags_test.go 2025-11-26 08:05:28.000000000 +0100 +++ new/kopia-0.22.3/cli/observability_flags_test.go 2025-12-02 06:27:24.000000000 +0100 @@ -97,7 +97,7 @@ tmp2 := testutil.TempDirectory(t) - env.RunAndExpectSuccess(t, "repo", "status", "--metrics-directory", tmp2) + env.RunAndExpectSuccess(t, "repo", "status", "--diagnostics-output-directory", tmp2, "--metrics-store-on-exit") entries, err := os.ReadDir(tmp2) require.NoError(t, err) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kopia-0.22.2/cli/profile.go new/kopia-0.22.3/cli/profile.go --- old/kopia-0.22.2/cli/profile.go 2025-11-26 08:05:28.000000000 +0100 +++ new/kopia-0.22.3/cli/profile.go 2025-12-02 06:27:24.000000000 +0100 @@ -1,46 +1,156 @@ package cli import ( + "context" + "os" + "path/filepath" + "runtime" + "runtime/pprof" + "github.com/alecthomas/kingpin/v2" - "github.com/pkg/profile" + "github.com/pkg/errors" ) +const profDirName = "profiles" + type profileFlags struct { - profileDir string - profileCPU bool - profileMemory int - profileBlocking bool - profileMutex bool + profileGCBeforeSaving bool + profileCPU bool + profileBlockingRate int + profileMemoryRate int + profileMutexFraction int + saveProfiles bool + + outputDirectory string + cpuProfileCloser func() } func (c *profileFlags) setup(app *kingpin.Application) { - app.Flag("profile-dir", "Write profile to the specified directory").Hidden().StringVar(&c.profileDir) + c.profileBlockingRate = -1 + c.profileMemoryRate = -1 + c.profileMutexFraction = -1 + + app.Flag("profile-store-on-exit", "Writes profiling data on exit. It writes a file per profile type (heap, goroutine, threadcreate, block, mutex) in a sub-directory in the directory specified with the --diagnostics-output-directory").Hidden().BoolVar(&c.saveProfiles) //nolint:lll + app.Flag("profile-go-gc-before-dump", "Perform a Go GC before writing out memory profiles").Hidden().BoolVar(&c.profileGCBeforeSaving) + app.Flag("profile-blocking-rate", "Blocking profiling rate, a value of 0 turns off block profiling").Hidden().IntVar(&c.profileBlockingRate) app.Flag("profile-cpu", "Enable CPU profiling").Hidden().BoolVar(&c.profileCPU) - app.Flag("profile-memory", "Enable memory profiling").Hidden().IntVar(&c.profileMemory) - app.Flag("profile-blocking", "Enable block profiling").Hidden().BoolVar(&c.profileBlocking) - app.Flag("profile-mutex", "Enable mutex profiling").Hidden().BoolVar(&c.profileMutex) + app.Flag("profile-memory-rate", "Memory profiling rate").Hidden().IntVar(&c.profileMemoryRate) + app.Flag("profile-mutex-fraction", "Mutex profiling, a value of 0 turns off mutex profiling").Hidden().IntVar(&c.profileMutexFraction) } -// withProfiling runs the given callback with profiling enabled, configured according to command line flags. -func (c *profileFlags) withProfiling(callback func() error) error { - if c.profileDir != "" { - pp := profile.ProfilePath(c.profileDir) - if c.profileMemory > 0 { - defer profile.Start(pp, profile.MemProfileRate(c.profileMemory)).Stop() +func (c *profileFlags) start(ctx context.Context, outputDirectory string) error { + pBlockingRate := c.profileBlockingRate + pMemoryRate := c.profileMemoryRate + pMutexFraction := c.profileMutexFraction + + if c.saveProfiles { + // when saving profiles ensure profiling parameters have sensible values + // unless explicitly modified. + // runtime.MemProfileRate has a default value, no need to reset it. + if pBlockingRate == -1 { + pBlockingRate = 1 } - if c.profileCPU { - defer profile.Start(pp, profile.CPUProfile).Stop() + if pMutexFraction == -1 { + pMutexFraction = 1 } + } - if c.profileBlocking { - defer profile.Start(pp, profile.BlockProfile).Stop() - } + // set profiling parameters if they have been changed from defaults + if pBlockingRate != -1 { + runtime.SetBlockProfileRate(pBlockingRate) + } + + if pMemoryRate != -1 { + runtime.MemProfileRate = pMemoryRate + } + + if pMutexFraction != -1 { + runtime.SetMutexProfileFraction(pMutexFraction) + } + + if !c.profileCPU && !c.saveProfiles { + return nil + } - if c.profileMutex { - defer profile.Start(pp, profile.MutexProfile).Stop() + c.outputDirectory = outputDirectory + + // ensure upfront that the pprof output dir can be created. + profDir, err := mkSubdirectories(c.outputDirectory, profDirName) + if err != nil { + return err + } + + if !c.profileCPU { + return nil + } + + // start CPU profile dumper + f, err := os.Create(filepath.Join(profDir, "cpu.pprof")) //nolint:gosec + if err != nil { + return errors.Wrap(err, "could not create CPU profile output file") + } + + // CPU profile closer + closer := func() { + pprof.StopCPUProfile() + + if err := f.Close(); err != nil { + log(ctx).Warn("error closing CPU profile output file:", err) } } - return callback() + if err := pprof.StartCPUProfile(f); err != nil { + closer() + + return errors.Wrap(err, "could not start CPU profile") + } + + c.cpuProfileCloser = closer + + return nil +} + +func (c *profileFlags) stop(ctx context.Context) { + if c.cpuProfileCloser != nil { + c.cpuProfileCloser() + c.cpuProfileCloser = nil + } + + if !c.saveProfiles { + return + } + + if c.profileGCBeforeSaving { + // update profiles, otherwise they may not include activity after the last GC + runtime.GC() + } + + profDir, err := mkSubdirectories(c.outputDirectory, profDirName) + if err != nil { + log(ctx).Warn("cannot create directory to save profiles:", err) + } + + for _, p := range pprof.Profiles() { + func() { + fname := filepath.Join(profDir, p.Name()+".pprof") + + f, err := os.Create(fname) //nolint:gosec + if err != nil { + log(ctx).Warnf("unable to create profile output file '%s': %v", fname, err) + + return + } + + defer func() { + if err := f.Close(); err != nil { + log(ctx).Warnf("unable to close profile output file '%s': %v", fname, err) + } + }() + + if err := p.WriteTo(f, 0); err != nil { + log(ctx).Warnf("unable to write profile to file '%s': %v", fname, err) + } + }() + } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kopia-0.22.2/fs/localfs/local_fs.go new/kopia-0.22.3/fs/localfs/local_fs.go --- old/kopia-0.22.2/fs/localfs/local_fs.go 2025-11-26 08:05:28.000000000 +0100 +++ new/kopia-0.22.3/fs/localfs/local_fs.go 2025-12-02 06:27:24.000000000 +0100 @@ -13,6 +13,16 @@ const numEntriesToRead = 100 // number of directory entries to read in one shot +// Options contains configuration options for localfs operations. +type Options struct { + // IgnoreUnreadableDirEntries, when true, causes unreadable directory entries + // to be silently skipped during directory iteration instead of causing errors. + IgnoreUnreadableDirEntries bool +} + +// DefaultOptions stores the default options used by localfs functions. +var DefaultOptions = &Options{} + type filesystemEntry struct { name string size int64 @@ -21,7 +31,8 @@ owner fs.OwnerInfo device fs.DeviceInfo - prefix string + prefix string + options *Options } func (e *filesystemEntry) Name() string { @@ -92,6 +103,7 @@ type fileWithMetadata struct { *os.File + options *Options } func (f *fileWithMetadata) Entry() (fs.Entry, error) { @@ -102,7 +114,7 @@ basename, prefix := splitDirPrefix(f.Name()) - return newFilesystemFile(newEntry(basename, fi, prefix)), nil + return newFilesystemFile(newEntry(basename, fi, prefix, f.options)), nil } func (fsf *filesystemFile) Open(_ context.Context) (fs.Reader, error) { @@ -111,7 +123,7 @@ return nil, errors.Wrap(err, "unable to open local file") } - return &fileWithMetadata{f}, nil + return &fileWithMetadata{File: f, options: fsf.options}, nil } func (fsl *filesystemSymlink) Readlink(_ context.Context) (string, error) { @@ -125,7 +137,7 @@ return nil, errors.Wrapf(err, "cannot resolve symlink for '%q'", fsl.fullPath()) } - return NewEntry(target) + return NewEntryWithOptions(target, fsl.options) } func (e *filesystemErrorEntry) ErrorInfo() error { @@ -145,8 +157,15 @@ } // Directory returns fs.Directory for the specified path. +// It uses DefaultOptions for configuration. func Directory(path string) (fs.Directory, error) { - e, err := NewEntry(path) + return DirectoryWithOptions(path, DefaultOptions) +} + +// DirectoryWithOptions returns fs.Directory for the specified path. +// It uses the provided Options for configuration. +func DirectoryWithOptions(path string, options *Options) (fs.Directory, error) { + e, err := NewEntryWithOptions(path, options) if err != nil { return nil, err } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kopia-0.22.2/fs/localfs/local_fs_os.go new/kopia-0.22.3/fs/localfs/local_fs_os.go --- old/kopia-0.22.2/fs/localfs/local_fs_os.go 2025-11-26 08:05:28.000000000 +0100 +++ new/kopia-0.22.3/fs/localfs/local_fs_os.go 2025-12-02 06:27:24.000000000 +0100 @@ -18,6 +18,7 @@ type filesystemDirectoryIterator struct { dirHandle *os.File childPrefix string + options *Options currentIndex int currentBatch []os.DirEntry @@ -45,7 +46,7 @@ n := it.currentIndex it.currentIndex++ - e, err := toDirEntryOrNil(it.currentBatch[n], it.childPrefix) + e, err := toDirEntryOrNil(it.currentBatch[n], it.childPrefix, it.options) if err != nil { // stop iteration return nil, err @@ -74,7 +75,7 @@ childPrefix := fullPath + separatorStr - return &filesystemDirectoryIterator{dirHandle: d, childPrefix: childPrefix}, nil + return &filesystemDirectoryIterator{dirHandle: d, childPrefix: childPrefix, options: fsd.options}, nil } func (fsd *filesystemDirectory) Child(_ context.Context, name string) (fs.Entry, error) { @@ -89,10 +90,10 @@ return nil, errors.Wrap(err, "unable to get child") } - return entryFromDirEntry(name, st, fullPath+separatorStr), nil + return entryFromDirEntry(name, st, fullPath+separatorStr, fsd.options), nil } -func toDirEntryOrNil(dirEntry os.DirEntry, prefix string) (fs.Entry, error) { +func toDirEntryOrNil(dirEntry os.DirEntry, prefix string, options *Options) (fs.Entry, error) { n := dirEntry.Name() fi, err := os.Lstat(prefix + n) @@ -101,15 +102,27 @@ return nil, nil } + if options != nil && options.IgnoreUnreadableDirEntries { + return nil, nil + } + return nil, errors.Wrap(err, "error reading directory") } - return entryFromDirEntry(n, fi, prefix), nil + return entryFromDirEntry(n, fi, prefix, options), nil } // NewEntry returns fs.Entry for the specified path, the result will be one of supported entry types: fs.File, fs.Directory, fs.Symlink // or fs.UnsupportedEntry. +// It uses DefaultOptions for configuration. func NewEntry(path string) (fs.Entry, error) { + return NewEntryWithOptions(path, DefaultOptions) +} + +// NewEntryWithOptions returns fs.Entry for the specified path, the result will be one of supported entry types: fs.File, fs.Directory, fs.Symlink +// or fs.UnsupportedEntry. +// It uses the provided Options for configuration. +func NewEntryWithOptions(path string, options *Options) (fs.Entry, error) { path = filepath.Clean(path) fi, err := os.Lstat(path) @@ -130,42 +143,42 @@ } if path == "/" { - return entryFromDirEntry("/", fi, ""), nil + return entryFromDirEntry("/", fi, "", options), nil } basename, prefix := splitDirPrefix(path) - return entryFromDirEntry(basename, fi, prefix), nil + return entryFromDirEntry(basename, fi, prefix, options), nil } -func entryFromDirEntry(basename string, fi os.FileInfo, prefix string) fs.Entry { +func entryFromDirEntry(basename string, fi os.FileInfo, prefix string, options *Options) fs.Entry { isplaceholder := strings.HasSuffix(basename, ShallowEntrySuffix) maskedmode := fi.Mode() & os.ModeType switch { case maskedmode == os.ModeDir && !isplaceholder: - return newFilesystemDirectory(newEntry(basename, fi, prefix)) + return newFilesystemDirectory(newEntry(basename, fi, prefix, options)) case maskedmode == os.ModeDir && isplaceholder: - return newShallowFilesystemDirectory(newEntry(basename, fi, prefix)) + return newShallowFilesystemDirectory(newEntry(basename, fi, prefix, options)) case maskedmode == os.ModeSymlink && !isplaceholder: - return newFilesystemSymlink(newEntry(basename, fi, prefix)) + return newFilesystemSymlink(newEntry(basename, fi, prefix, options)) case maskedmode == 0 && !isplaceholder: - return newFilesystemFile(newEntry(basename, fi, prefix)) + return newFilesystemFile(newEntry(basename, fi, prefix, options)) case maskedmode == 0 && isplaceholder: - return newShallowFilesystemFile(newEntry(basename, fi, prefix)) + return newShallowFilesystemFile(newEntry(basename, fi, prefix, options)) default: - return newFilesystemErrorEntry(newEntry(basename, fi, prefix), fs.ErrUnknown) + return newFilesystemErrorEntry(newEntry(basename, fi, prefix, options), fs.ErrUnknown) } } var _ os.FileInfo = (*filesystemEntry)(nil) -func newEntry(basename string, fi os.FileInfo, prefix string) filesystemEntry { +func newEntry(basename string, fi os.FileInfo, prefix string, options *Options) filesystemEntry { return filesystemEntry{ TrimShallowSuffix(basename), fi.Size(), @@ -174,5 +187,6 @@ platformSpecificOwnerInfo(fi), platformSpecificDeviceInfo(fi), prefix, + options, } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kopia-0.22.2/fs/localfs/local_fs_test.go new/kopia-0.22.3/fs/localfs/local_fs_test.go --- old/kopia-0.22.2/fs/localfs/local_fs_test.go 2025-11-26 08:05:28.000000000 +0100 +++ new/kopia-0.22.3/fs/localfs/local_fs_test.go 2025-12-02 06:27:24.000000000 +0100 @@ -306,3 +306,277 @@ require.Equal(t, want.prefix, prefix, input) } } + +// getOptionsFromEntry extracts the options pointer from an fs.Entry by type assertion. +// Returns nil if the entry doesn't have options or if type assertion fails. +func getOptionsFromEntry(entry fs.Entry) *Options { + switch e := entry.(type) { + case *filesystemDirectory: + return e.options + case *filesystemFile: + return e.options + case *filesystemSymlink: + return e.options + case *filesystemErrorEntry: + return e.options + default: + return nil + } +} + +func TestOptionsPassedToChildEntries(t *testing.T) { + ctx := testlogging.Context(t) + tmp := testutil.TempDirectory(t) + + // Create a test directory structure + require.NoError(t, os.WriteFile(filepath.Join(tmp, "file1.txt"), []byte{1, 2, 3}, 0o777)) + require.NoError(t, os.WriteFile(filepath.Join(tmp, "file2.txt"), []byte{4, 5, 6}, 0o777)) + subdir := filepath.Join(tmp, "subdir") + require.NoError(t, os.Mkdir(subdir, 0o777)) + require.NoError(t, os.WriteFile(filepath.Join(subdir, "subfile.txt"), []byte{7, 8, 9}, 0o777)) + + // Create custom options + customOptions := &Options{ + IgnoreUnreadableDirEntries: true, + } + + // Create directory with custom options + dir, err := DirectoryWithOptions(tmp, customOptions) + require.NoError(t, err) + + // Verify the directory itself has the correct options + dirOptions := getOptionsFromEntry(dir) + require.NotNil(t, dirOptions, "directory should have options") + require.Equal(t, customOptions, dirOptions, "directory should have the same options pointer") + require.True(t, dirOptions.IgnoreUnreadableDirEntries, "directory options should match") + + // Test that options are passed to children via Child() + childFile, err := dir.Child(ctx, "file1.txt") + require.NoError(t, err) + + childOptions := getOptionsFromEntry(childFile) + require.NotNil(t, childOptions, "child file should have options") + require.Equal(t, customOptions, childOptions, "child file should have the same options pointer") + + // Test that options are passed to subdirectories + childDir, err := dir.Child(ctx, "subdir") + require.NoError(t, err) + + subdirOptions := getOptionsFromEntry(childDir) + require.NotNil(t, subdirOptions, "subdirectory should have options") + require.Equal(t, customOptions, subdirOptions, "subdirectory should have the same options pointer") + + // Test that options are passed to nested children + subdirEntry, ok := childDir.(fs.Directory) + require.True(t, ok, "child directory should be a directory") + + nestedFile, err := subdirEntry.Child(ctx, "subfile.txt") + require.NoError(t, err) + + nestedOptions := getOptionsFromEntry(nestedFile) + require.NotNil(t, nestedOptions, "nested file should have options") + require.Equal(t, customOptions, nestedOptions, "nested file should have the same options pointer") +} + +func TestOptionsPassedThroughIteration(t *testing.T) { + ctx := testlogging.Context(t) + tmp := testutil.TempDirectory(t) + + // Create a test directory structure + require.NoError(t, os.WriteFile(filepath.Join(tmp, "file1.txt"), []byte{1, 2, 3}, 0o777)) + require.NoError(t, os.WriteFile(filepath.Join(tmp, "file2.txt"), []byte{4, 5, 6}, 0o777)) + require.NoError(t, os.Mkdir(filepath.Join(tmp, "subdir"), 0o777)) + + // Create custom options + customOptions := &Options{ + IgnoreUnreadableDirEntries: true, + } + + // Create directory with custom options + dir, err := DirectoryWithOptions(tmp, customOptions) + require.NoError(t, err) + + // Iterate through entries and verify all have the same options pointer + iter, err := dir.Iterate(ctx) + require.NoError(t, err) + + defer iter.Close() + + entryCount := 0 + for { + entry, err := iter.Next(ctx) + if err != nil { + t.Fatalf("iteration error: %v", err) + } + + if entry == nil { + break + } + + entryCount++ + entryOptions := getOptionsFromEntry(entry) + require.NotNil(t, entryOptions, "entry %s should have options", entry.Name()) + require.Equal(t, customOptions, entryOptions, "entry %s should have the same options pointer", entry.Name()) + } + + require.Equal(t, 3, entryCount, "should have found 3 entries") +} + +func TestOptionsPassedThroughSymlinkResolution(t *testing.T) { + ctx := testlogging.Context(t) + tmp := testutil.TempDirectory(t) + + // Create a target file + targetFile := filepath.Join(tmp, "target.txt") + require.NoError(t, os.WriteFile(targetFile, []byte{1, 2, 3}, 0o777)) + + // Create a symlink + symlinkPath := filepath.Join(tmp, "link") + require.NoError(t, os.Symlink(targetFile, symlinkPath)) + + // Create custom options + customOptions := &Options{ + IgnoreUnreadableDirEntries: true, + } + + // Create symlink entry with custom options + symlinkEntry, err := NewEntryWithOptions(symlinkPath, customOptions) + require.NoError(t, err) + + // Verify the symlink has the correct options + symlinkOptions := getOptionsFromEntry(symlinkEntry) + require.NotNil(t, symlinkOptions, "symlink should have options") + require.Equal(t, customOptions, symlinkOptions, "symlink should have the same options pointer") + + // Resolve the symlink and verify the resolved entry has the same options + symlink, ok := symlinkEntry.(fs.Symlink) + require.True(t, ok, "entry should be a symlink") + + resolved, err := symlink.Resolve(ctx) + require.NoError(t, err) + + resolvedOptions := getOptionsFromEntry(resolved) + require.NotNil(t, resolvedOptions, "resolved entry should have options") + require.Equal(t, customOptions, resolvedOptions, "resolved entry should have the same options pointer") +} + +func TestOptionsPassedToNewEntry(t *testing.T) { + tmp := testutil.TempDirectory(t) + + // Create a file + filePath := filepath.Join(tmp, "testfile.txt") + require.NoError(t, os.WriteFile(filePath, []byte{1, 2, 3}, 0o777)) + + // Create custom options + customOptions := &Options{ + IgnoreUnreadableDirEntries: true, + } + + // Create entry with custom options + entry, err := NewEntryWithOptions(filePath, customOptions) + require.NoError(t, err) + + // Verify the entry has the correct options + entryOptions := getOptionsFromEntry(entry) + require.NotNil(t, entryOptions, "entry should have options") + require.Equal(t, customOptions, entryOptions, "entry should have the same options pointer") +} + +func TestOptionsPassedToNestedDirectories(t *testing.T) { + ctx := testlogging.Context(t) + tmp := testutil.TempDirectory(t) + + // Create nested directory structure + level1 := filepath.Join(tmp, "level1") + level2 := filepath.Join(level1, "level2") + level3 := filepath.Join(level2, "level3") + + require.NoError(t, os.MkdirAll(level3, 0o777)) + require.NoError(t, os.WriteFile(filepath.Join(level3, "file.txt"), []byte{1, 2, 3}, 0o777)) + + // Create custom options + customOptions := &Options{ + IgnoreUnreadableDirEntries: true, + } + + // Create root directory with custom options + rootDir, err := DirectoryWithOptions(tmp, customOptions) + require.NoError(t, err) + + // Navigate through nested directories and verify options are passed + currentDir := rootDir + levels := []string{"level1", "level2", "level3"} + + for _, level := range levels { + child, err := currentDir.Child(ctx, level) + require.NoError(t, err) + + childOptions := getOptionsFromEntry(child) + require.NotNil(t, childOptions, "directory %s should have options", level) + require.Equal(t, customOptions, childOptions, "directory %s should have the same options pointer", level) + + var ok bool + + currentDir, ok = child.(fs.Directory) + require.True(t, ok, "child should be a directory") + } + + // Verify the file in the deepest directory has the same options + file, err := currentDir.Child(ctx, "file.txt") + require.NoError(t, err) + + fileOptions := getOptionsFromEntry(file) + require.NotNil(t, fileOptions, "file should have options") + require.Equal(t, customOptions, fileOptions, "file should have the same options pointer") +} + +func TestDefaultOptionsUsedByDefault(t *testing.T) { + tmp := testutil.TempDirectory(t) + + // Create a file + filePath := filepath.Join(tmp, "testfile.txt") + require.NoError(t, os.WriteFile(filePath, []byte{1, 2, 3}, 0o777)) + + // Use default NewEntry (should use DefaultOptions) + entry, err := NewEntry(filePath) + require.NoError(t, err) + + // Verify the entry has DefaultOptions + entryOptions := getOptionsFromEntry(entry) + require.NotNil(t, entryOptions, "entry should have options") + require.Equal(t, DefaultOptions, entryOptions, "entry should have DefaultOptions pointer") +} + +func TestDifferentOptionsInstances(t *testing.T) { + tmp := testutil.TempDirectory(t) + + // Create two different files + filePath1 := filepath.Join(tmp, "testfile1.txt") + filePath2 := filepath.Join(tmp, "testfile2.txt") + + require.NoError(t, os.WriteFile(filePath1, []byte{1, 2, 3}, 0o777)) + require.NoError(t, os.WriteFile(filePath2, []byte{4, 5, 6}, 0o777)) + + // Create two different options instances with same values + options1 := &Options{IgnoreUnreadableDirEntries: true} + options2 := &Options{IgnoreUnreadableDirEntries: false} + + // Create entries with different options instances + entry1, err := NewEntryWithOptions(filePath1, options1) + require.NoError(t, err) + + entry2, err := NewEntryWithOptions(filePath2, options2) + require.NoError(t, err) + + // Verify they have the correct options pointers + entry1Options := getOptionsFromEntry(entry1) + entry2Options := getOptionsFromEntry(entry2) + + require.NotNil(t, entry1Options) + require.NotNil(t, entry2Options) + require.Equal(t, options1, entry1Options, "entry1 should have options1 pointer") + require.Equal(t, options2, entry2Options, "entry2 should have options2 pointer") + require.NotEqual(t, entry1Options, entry2Options, "entries should have different options pointers") + require.True(t, entry1Options.IgnoreUnreadableDirEntries, "entry1 options should have IgnoreUnreadableDirEntries=true") + require.False(t, entry2Options.IgnoreUnreadableDirEntries, "entry2 options should have IgnoreUnreadableDirEntries=false") +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kopia-0.22.2/go.mod new/kopia-0.22.3/go.mod --- old/kopia-0.22.2/go.mod 2025-11-26 08:05:28.000000000 +0100 +++ new/kopia-0.22.3/go.mod 2025-12-02 06:27:24.000000000 +0100 @@ -27,9 +27,9 @@ github.com/gorilla/mux v1.8.1 github.com/hanwen/go-fuse/v2 v2.9.0 github.com/hashicorp/cronexpr v1.1.3 - github.com/klauspost/compress v1.18.1 + github.com/klauspost/compress v1.18.2 github.com/klauspost/pgzip v1.2.6 - github.com/klauspost/reedsolomon v1.12.5 + github.com/klauspost/reedsolomon v1.12.6 github.com/kopia/htmluibuild v0.0.1-0.20251125011029-7f1c3f84f29d github.com/kylelemons/godebug v1.1.0 github.com/mattn/go-colorable v0.1.14 @@ -119,7 +119,7 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect - github.com/klauspost/cpuid/v2 v2.2.11 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/crc32 v1.3.0 // indirect github.com/kr/fs v0.1.0 // indirect github.com/minio/crc64nvme v1.1.0 // indirect diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kopia-0.22.2/go.sum new/kopia-0.22.3/go.sum --- old/kopia-0.22.2/go.sum 2025-11-26 08:05:28.000000000 +0100 +++ new/kopia-0.22.3/go.sum 2025-12-02 06:27:24.000000000 +0100 @@ -170,17 +170,17 @@ github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= -github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= -github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU= -github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM= github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= -github.com/klauspost/reedsolomon v1.12.5 h1:4cJuyH926If33BeDgiZpI5OU0pE+wUHZvMSyNGqN73Y= -github.com/klauspost/reedsolomon v1.12.5/go.mod h1:LkXRjLYGM8K/iQfujYnaPeDmhZLqkrGUyG9p7zs5L68= +github.com/klauspost/reedsolomon v1.12.6 h1:8pqE9aECQG/ZFitiUD1xK/E83zwosBAZtE3UbuZM8TQ= +github.com/klauspost/reedsolomon v1.12.6/go.mod h1:ggJT9lc71Vu+cSOPBlxGvBN6TfAS77qB4fp8vJ05NSA= github.com/kopia/htmluibuild v0.0.1-0.20251125011029-7f1c3f84f29d h1:U3VB/cDMsPW4zB4JRFbVRDzIpPytt889rJUKAG40NPA= github.com/kopia/htmluibuild v0.0.1-0.20251125011029-7f1c3f84f29d/go.mod h1:h53A5JM3t2qiwxqxusBe+PFgGcgZdS+DWCQvG5PTlto= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kopia-0.22.2/repo/format/content_format.go new/kopia-0.22.3/repo/format/content_format.go --- old/kopia-0.22.2/repo/format/content_format.go 2025-11-26 08:05:28.000000000 +0100 +++ new/kopia-0.22.3/repo/format/content_format.go 2025-12-02 06:27:24.000000000 +0100 @@ -63,10 +63,10 @@ // MutableParameters represents parameters of the content manager that can be mutated after the repository // is created. type MutableParameters struct { - Version Version `json:"version,omitempty"` // version number, must be "1", "2" or "3" - MaxPackSize int `json:"maxPackSize,omitempty"` // maximum size of a pack object - IndexVersion int `json:"indexVersion,omitempty"` // force particular index format version (1,2,..) - EpochParameters epoch.Parameters `json:"epochParameters,omitempty"` // epoch manager parameters + Version Version `json:"version,omitempty"` // version number, must be "1", "2" or "3" + MaxPackSize int `json:"maxPackSize,omitempty"` // maximum size of a pack object + IndexVersion int `json:"indexVersion,omitempty"` // force particular index format version (1,2,..) + EpochParameters epoch.Parameters `json:"epochParameters"` // epoch manager parameters } // Validate validates the parameters. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kopia-0.22.2/repo/format/upgrade_lock_intent.go new/kopia-0.22.3/repo/format/upgrade_lock_intent.go --- old/kopia-0.22.2/repo/format/upgrade_lock_intent.go 2025-11-26 08:05:28.000000000 +0100 +++ new/kopia-0.22.3/repo/format/upgrade_lock_intent.go 2025-12-02 06:27:24.000000000 +0100 @@ -13,7 +13,7 @@ // repository. type UpgradeLockIntent struct { OwnerID string `json:"ownerID,omitempty"` - CreationTime time.Time `json:"creationTime,omitempty"` + CreationTime time.Time `json:"creationTime"` AdvanceNoticeDuration time.Duration `json:"advanceNoticeDuration,omitempty"` IODrainTimeout time.Duration `json:"ioDrainTimeout,omitempty"` StatusPollInterval time.Duration `json:"statusPollInterval,omitempty"` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kopia-0.22.2/repo/object/indirect.go new/kopia-0.22.3/repo/object/indirect.go --- old/kopia-0.22.2/repo/object/indirect.go 2025-11-26 08:05:28.000000000 +0100 +++ new/kopia-0.22.3/repo/object/indirect.go 2025-12-02 06:27:24.000000000 +0100 @@ -4,7 +4,7 @@ type IndirectObjectEntry struct { Start int64 `json:"s,omitempty"` Length int64 `json:"l,omitempty"` - Object ID `json:"o,omitempty"` + Object ID `json:"o"` } func (i *IndirectObjectEntry) endOffset() int64 { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kopia-0.22.2/snapshot/manifest.go new/kopia-0.22.3/snapshot/manifest.go --- old/kopia-0.22.2/snapshot/manifest.go 2025-11-26 08:05:28.000000000 +0100 +++ new/kopia-0.22.3/snapshot/manifest.go 2025-12-02 06:27:24.000000000 +0100 @@ -126,7 +126,7 @@ ModTime fs.UTCTimestamp `json:"mtime,omitempty"` UserID uint32 `json:"uid,omitempty"` GroupID uint32 `json:"gid,omitempty"` - ObjectID object.ID `json:"obj,omitempty"` + ObjectID object.ID `json:"obj"` DirSummary *fs.DirectorySummary `json:"summ,omitempty"` } @@ -187,8 +187,8 @@ type StorageStats struct { // amount of new unique data in this snapshot that wasn't there before. // note that this depends on ordering of snapshots. - NewData StorageUsageDetails `json:"newData,omitempty"` - RunningTotal StorageUsageDetails `json:"runningTotal,omitempty"` + NewData StorageUsageDetails `json:"newData"` + RunningTotal StorageUsageDetails `json:"runningTotal"` } // StorageUsageDetails provides details about snapshot storage usage. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/kopia-0.22.2/tests/end_to_end_test/profile_flags_test.go new/kopia-0.22.3/tests/end_to_end_test/profile_flags_test.go --- old/kopia-0.22.2/tests/end_to_end_test/profile_flags_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/kopia-0.22.3/tests/end_to_end_test/profile_flags_test.go 2025-12-02 06:27:24.000000000 +0100 @@ -0,0 +1,47 @@ +package endtoend_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/kopia/kopia/tests/testenv" +) + +func TestProfileFlags(t *testing.T) { + env := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, testenv.NewExeRunner(t)) + + // contents not needed on test failure + diagsDir := t.TempDir() + + env.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", env.RepoDir) + env.RunAndExpectSuccess(t, "repo", "status", + "--diagnostics-output-directory", diagsDir, + "--profile-store-on-exit", + "--profile-cpu", + "--profile-blocking-rate=1", + "--profile-mutex-fraction=1", + "--profile-memory-rate=1", + ) + + // get per-execution directory + entries, err := os.ReadDir(diagsDir) + require.NoError(t, err) + require.NotEmpty(t, entries) + + pprofDir := filepath.Join(diagsDir, entries[0].Name(), "profiles") + + for _, name := range []string{"cpu.pprof", "allocs.pprof", "block.pprof", "goroutine.pprof", "mutex.pprof", "heap.pprof", "threadcreate.pprof"} { + f := filepath.Join(pprofDir, name) + t.Run(f, func(t *testing.T) { + require.FileExists(t, f, "expected profile file") + + info, err := os.Stat(f) + + require.NoError(t, err) + require.NotZero(t, info.Size(), "profile file %s should not be empty", f) + }) + } +} ++++++ kopia.obsinfo ++++++ --- /var/tmp/diff_new_pack.i1JyhY/_old 2025-12-05 16:52:36.502282517 +0100 +++ /var/tmp/diff_new_pack.i1JyhY/_new 2025-12-05 16:52:36.510282852 +0100 @@ -1,5 +1,5 @@ name: kopia -version: 0.22.2 -mtime: 1764140728 -commit: e456f78fa2d15b102988eee1025a2451eeaa3ebf +version: 0.22.3 +mtime: 1764653244 +commit: 154bf56899228e5c95fb3176b9c6901bbe4ca97b ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/kopia/vendor.tar.gz /work/SRC/openSUSE:Factory/.kopia.new.1939/vendor.tar.gz differ: char 133, line 2
