Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package asdf for openSUSE:Factory checked in at 2026-07-03 16:19:23 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/asdf (Old) and /work/SRC/openSUSE:Factory/.asdf.new.1982 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "asdf" Fri Jul 3 16:19:23 2026 rev:8 rq:1363666 version:0.19.0 Changes: -------- --- /work/SRC/openSUSE:Factory/asdf/asdf.changes 2026-03-16 14:21:05.338654110 +0100 +++ /work/SRC/openSUSE:Factory/.asdf.new.1982/asdf.changes 2026-07-03 16:19:30.187679149 +0200 @@ -1,0 +2,28 @@ +Fri Jul 03 11:21:17 UTC 2026 - Robert Munteanu <[email protected]> + +- Update to version 0.19.0: + * chore: release 0.19.0 (#2261) + * fix: make plugin test command resolve latest version properly + * feat: don't override semver ordering during completion (#2263) + * fix: set ASDF_INSTALL_* environment variables for reshim command + * chore(deps): bump github.com/go-git/go-git/v5 from 5.17.1 to 5.18.0 (#2270) + * fix: convert hyphens to underscores when converting plugin names to generate valid environment variables + * chore: update asdf install action to fix lint workflow (#2264) + * chore(golang-rewrite): remove old Bash implementation code (#1822) + * fix: use os.PathListSeparator (#2195) + * fix: return error from repo.RemoteUrl function + * fix: nushell completions --ignore-errors deprecation (#2157) + * chore(docs): bump @types/node from 25.3.3 to 25.5.0 in /docs in the docs group (#2260) + * chore(deps): bump github.com/go-git/go-git/v5 from 5.16.5 to 5.17.1 (#2259) + * chore(deps): bump dorny/paths-filter from 3 to 4 (#2257) + * chore(deps): bump crazy-max/ghaction-github-pages from 4 to 5 (#2251) + * TrimSpace on the URL returned by RemoteURL() + * docs: update nodejs script for formatting the documentation to be the correct `npm run fmt` from the package, as the `npm run format` does not exist + * docs: remove `The Oh-My-ZSH asdf plugin is yet to be updated, see [ohmyzsh/ohmyzsh#8837](https://github.com/ohmyzsh/ohmyzsh/pull/8837).` from the docs for zsh as it has been [updated](https://github.com/ohmyzsh/ohmyzsh/pull/8837#issuecomment-2174169900) + +------------------------------------------------------------------- +Fri Jul 3 11:16:15 UTC 2026 - Robert Munteanu <[email protected]> + +- Add completion packages + +------------------------------------------------------------------- Old: ---- asdf-0.18.1.obscpio New: ---- asdf-0.19.0.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ asdf.spec ++++++ --- /var/tmp/diff_new_pack.daxAbV/_old 2026-07-03 16:19:33.067779401 +0200 +++ /var/tmp/diff_new_pack.daxAbV/_new 2026-07-03 16:19:33.067779401 +0200 @@ -17,7 +17,7 @@ Name: asdf -Version: 0.18.1 +Version: 0.19.0 Release: 0 Summary: Extendable version manager License: MIT @@ -25,6 +25,9 @@ Source: %{name}-%{version}.tar.gz Source1: vendor.tar.gz BuildRequires: golang(API) >= 1.23 +BuildRequires: bash-completion +BuildRequires: fish +BuildRequires: zsh %description Extendable version manager with support for @@ -33,6 +36,33 @@ Manage multiple runtime versions with a single CLI tool, extendable via plugins. +%package bash-completion +Summary: Bash Completion for %{name} +Requires: %{name} = %{version} +Supplements: (%{name} and bash-completion) +BuildArch: noarch + +%description bash-completion +Bash command line completion support for %{name}. + +%package zsh-completion +Summary: Zsh Completion for %{name} +Requires: %{name} = %{version} +Supplements: (%{name} and zsh) +BuildArch: noarch + +%description zsh-completion +Zsh command line completion support for %{name}. + +%package fish-completion +Summary: Fish Completion for %{name} +Requires: %{name} = %{version} +Supplements: (%{name} and fish) +BuildArch: noarch + +%description fish-completion +Fish command line completion support for %{name}. + %prep %autosetup -a 1 @@ -44,9 +74,27 @@ %install install -D -m 0755 %{name} "%{buildroot}/%{_bindir}/%{name}" +install -d %{buildroot}%{_datadir}/bash-completion/completions +%{buildroot}%{_bindir}/%{name} completion bash > %{buildroot}%{_datarootdir}/bash-completion/completions/%{name} +install -d %{buildroot}%{_datarootdir}/zsh/site-functions +%{buildroot}%{_bindir}/%{name} completion zsh > %{buildroot}%{_datarootdir}/zsh/site-functions/_%{name} +install -d %{buildroot}%{_datadir}/fish/vendor_completions.d +%{buildroot}%{_bindir}/%{name} completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/%{name}.fish %files %doc README.md %license LICENSE %{_bindir}/%{name} +%files bash-completion +%defattr(-,root,root) +%{_datarootdir}/bash-completion/completions/%{name} + +%files zsh-completion +%defattr(-,root,root) +%{_datarootdir}/zsh/site-functions/_%{name} + +%files fish-completion +%defattr(-,root,root) +%{_datadir}/fish/vendor_completions.d/%{name}.fish + ++++++ _service ++++++ --- /var/tmp/diff_new_pack.daxAbV/_old 2026-07-03 16:19:33.127781489 +0200 +++ /var/tmp/diff_new_pack.daxAbV/_new 2026-07-03 16:19:33.131781629 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/asdf-vm/asdf/</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v0.18.1</param> + <param name="revision">v0.19.0</param> <param name="versionformat">@PARENT_TAG@</param> <param name="changesgenerate">enable</param> <param name="versionrewrite-pattern">v(.*)</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.daxAbV/_old 2026-07-03 16:19:33.159782603 +0200 +++ /var/tmp/diff_new_pack.daxAbV/_new 2026-07-03 16:19:33.163782742 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/asdf-vm/asdf/</param> - <param name="changesrevision">6a97697d3d88d6b7ad19c4bd3157679b052ab657</param></service></servicedata> + <param name="changesrevision">b0ae66dc5131aaca03324a1f0eff4d09242c29e1</param></service></servicedata> (No newline at EOF) ++++++ asdf-0.18.1.obscpio -> asdf-0.19.0.obscpio ++++++ ++++ 4612 lines of diff (skipped) ++++++ asdf.obsinfo ++++++ --- /var/tmp/diff_new_pack.daxAbV/_old 2026-07-03 16:19:33.887807945 +0200 +++ /var/tmp/diff_new_pack.daxAbV/_new 2026-07-03 16:19:33.907808641 +0200 @@ -1,5 +1,5 @@ name: asdf -version: 0.18.1 -mtime: 1772590085 -commit: 6a97697d3d88d6b7ad19c4bd3157679b052ab657 +version: 0.19.0 +mtime: 1776997685 +commit: b0ae66dc5131aaca03324a1f0eff4d09242c29e1 ++++++ vendor.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/cloudflare/circl/internal/sha3/xor_unaligned.go new/vendor/github.com/cloudflare/circl/internal/sha3/xor_unaligned.go --- old/vendor/github.com/cloudflare/circl/internal/sha3/xor_unaligned.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/cloudflare/circl/internal/sha3/xor_unaligned.go 2026-04-24 04:28:05.000000000 +0200 @@ -14,14 +14,14 @@ type storageBuf [maxRate / 8]uint64 func (b *storageBuf) asBytes() *[maxRate]byte { - return (*[maxRate]byte)(unsafe.Pointer(b)) + return (*[maxRate]byte)(unsafe.Pointer(b)) //nolint:gosec } // xorInuses unaligned reads and writes to update d.a to contain d.a // XOR buf. func xorIn(d *State, buf []byte) { n := len(buf) - bw := (*[maxRate / 8]uint64)(unsafe.Pointer(&buf[0]))[: n/8 : n/8] + bw := (*[maxRate / 8]uint64)(unsafe.Pointer(&buf[0]))[: n/8 : n/8] //nolint:gosec if n >= 72 { d.a[0] ^= bw[0] d.a[1] ^= bw[1] @@ -56,6 +56,6 @@ } func copyOut(d *State, buf []byte) { - ab := (*[maxRate]uint8)(unsafe.Pointer(&d.a[0])) + ab := (*[maxRate]uint8)(unsafe.Pointer(&d.a[0])) //nolint:gosec copy(buf, ab[:]) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/cloudflare/circl/sign/sign.go new/vendor/github.com/cloudflare/circl/sign/sign.go --- old/vendor/github.com/cloudflare/circl/sign/sign.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/cloudflare/circl/sign/sign.go 2026-04-24 04:28:05.000000000 +0200 @@ -38,6 +38,12 @@ encoding.BinaryMarshaler } +// A private key that retains the seed with which it was generated. +type Seeded interface { + // returns the seed if retained, otherwise nil + Seed() []byte +} + // A Scheme represents a specific instance of a signature scheme. type Scheme interface { // Name of the scheme. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-billy/v5/fs.go new/vendor/github.com/go-git/go-billy/v5/fs.go --- old/vendor/github.com/go-git/go-billy/v5/fs.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/go-git/go-billy/v5/fs.go 2026-04-24 04:28:05.000000000 +0200 @@ -128,12 +128,18 @@ Readlink(link string) (string, error) } -// Change abstract the FileInfo change related operations in a storage-agnostic -// interface as an extension to the Basic interface -type Change interface { +// Chmod abstracts the logic around changing file modes. +type Chmod interface { // Chmod changes the mode of the named file to mode. If the file is a // symbolic link, it changes the mode of the link's target. Chmod(name string, mode os.FileMode) error +} + +// Change abstract the FileInfo change related operations in a storage-agnostic +// interface as an extension to the Basic interface +type Change interface { + Chmod + // Lchown changes the numeric uid and gid of the named file. If the file is // a symbolic link, it changes the uid and gid of the link itself. Lchown(name string, uid, gid int) error diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-billy/v5/helper/chroot/chroot.go new/vendor/github.com/go-git/go-billy/v5/helper/chroot/chroot.go --- old/vendor/github.com/go-git/go-billy/v5/helper/chroot/chroot.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/go-git/go-billy/v5/helper/chroot/chroot.go 2026-04-24 04:28:05.000000000 +0200 @@ -1,6 +1,7 @@ package chroot import ( + "errors" "os" "path/filepath" "strings" @@ -200,6 +201,19 @@ return string(os.PathSeparator) + target, nil } +func (fs *ChrootHelper) Chmod(path string, mode os.FileMode) error { + fullpath, err := fs.underlyingPath(path) + if err != nil { + return err + } + + c, ok := fs.underlying.(billy.Chmod) + if !ok { + return errors.New("underlying fs does not implement billy.Chmod") + } + return c.Chmod(fullpath, mode) +} + func (fs *ChrootHelper) Chroot(path string) (billy.Filesystem, error) { fullpath, err := fs.underlyingPath(path) if err != nil { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-billy/v5/helper/polyfill/polyfill.go new/vendor/github.com/go-git/go-billy/v5/helper/polyfill/polyfill.go --- old/vendor/github.com/go-git/go-billy/v5/helper/polyfill/polyfill.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/go-git/go-billy/v5/helper/polyfill/polyfill.go 2026-04-24 04:28:05.000000000 +0200 @@ -13,7 +13,7 @@ c capabilities } -type capabilities struct{ tempfile, dir, symlink, chroot bool } +type capabilities struct{ tempfile, dir, symlink, chroot, chmod bool } // New creates a new filesystem wrapping up 'fs' the intercepts all the calls // made and errors if fs doesn't implement any of the billy interfaces. @@ -28,6 +28,7 @@ _, h.c.dir = h.Basic.(billy.Dir) _, h.c.symlink = h.Basic.(billy.Symlink) _, h.c.chroot = h.Basic.(billy.Chroot) + _, h.c.chmod = h.Basic.(billy.Chmod) return h } @@ -87,6 +88,14 @@ return h.Basic.(billy.Chroot).Chroot(path) } +func (h *Polyfill) Chmod(path string, mode os.FileMode) error { + if !h.c.chmod { + return billy.ErrNotSupported + } + + return h.Basic.(billy.Chmod).Chmod(path, mode) +} + func (h *Polyfill) Root() string { if !h.c.chroot { return string(filepath.Separator) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-billy/v5/memfs/memory.go new/vendor/github.com/go-git/go-billy/v5/memfs/memory.go --- old/vendor/github.com/go-git/go-billy/v5/memfs/memory.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/go-git/go-billy/v5/memfs/memory.go 2026-04-24 04:28:05.000000000 +0200 @@ -177,6 +177,10 @@ return fs.s.Remove(filename) } +func (fs *Memory) Chmod(path string, mode os.FileMode) error { + return fs.s.Chmod(path, mode) +} + // Falls back to Go's filepath.Join, which works differently depending on the // OS where the code is being executed. func (fs *Memory) Join(elem ...string) string { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-billy/v5/memfs/storage.go new/vendor/github.com/go-git/go-billy/v5/memfs/storage.go --- old/vendor/github.com/go-git/go-billy/v5/memfs/storage.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/go-git/go-billy/v5/memfs/storage.go 2026-04-24 04:28:05.000000000 +0200 @@ -169,6 +169,18 @@ return nil } +func (s *storage) Chmod(path string, mode os.FileMode) error { + path = clean(path) + + f, has := s.Get(path) + if !has { + return os.ErrNotExist + } + + f.mode = mode + return nil +} + func clean(path string) string { return filepath.Clean(filepath.FromSlash(path)) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-billy/v5/osfs/os_bound.go new/vendor/github.com/go-git/go-billy/v5/osfs/os_bound.go --- old/vendor/github.com/go-git/go-billy/v5/osfs/os_bound.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/go-git/go-billy/v5/osfs/os_bound.go 2026-04-24 04:28:05.000000000 +0200 @@ -126,6 +126,14 @@ if err != nil { return nil, err } + + _, err = os.Stat(dir) + if err != nil && os.IsNotExist(err) { + err = os.MkdirAll(dir, defaultDirectoryMode) + if err != nil { + return nil, err + } + } } return tempFile(dir, prefix) @@ -176,6 +184,14 @@ return os.Readlink(link) } +func (fs *BoundOS) Chmod(path string, mode os.FileMode) error { + abspath, err := fs.abs(path) + if err != nil { + return err + } + return os.Chmod(abspath, mode) +} + // Chroot returns a new OS filesystem, with the base dir set to the // result of joining the provided path with the underlying base dir. func (fs *BoundOS) Chroot(path string) (billy.Filesystem, error) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-billy/v5/osfs/os_chroot.go new/vendor/github.com/go-git/go-billy/v5/osfs/os_chroot.go --- old/vendor/github.com/go-git/go-billy/v5/osfs/os_chroot.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/go-git/go-billy/v5/osfs/os_chroot.go 2026-04-24 04:28:05.000000000 +0200 @@ -74,6 +74,10 @@ return os.Remove(filename) } +func (fs *ChrootOS) Chmod(path string, mode os.FileMode) error { + return os.Chmod(path, mode) +} + func (fs *ChrootOS) TempFile(dir, prefix string) (billy.File, error) { if err := fs.createDir(dir + string(os.PathSeparator)); err != nil { return nil, err diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/decoder.go new/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/decoder.go --- old/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/decoder.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/go-git/go-git/v5/plumbing/format/idxfile/decoder.go 2026-04-24 04:28:05.000000000 +0200 @@ -91,8 +91,8 @@ return err } - if v > VersionSupported { - return ErrUnsupportedVersion + if v != VersionSupported { + return fmt.Errorf("%w: v%d", ErrUnsupportedVersion, v) } idx.Version = v @@ -106,6 +106,10 @@ return err } + if k > 0 && n < idx.Fanout[k-1] { + return fmt.Errorf("%w: fanout table is not monotonically non-decreasing at entry %d", ErrMalformedIdxFile, k) + } + idx.Fanout[k] = n idx.FanoutMapping[k] = noMapping } @@ -155,7 +159,7 @@ } func readOffsets(idx *MemoryIndex, r io.Reader) error { - var o64cnt int + var o64cnt int64 for k := 0; k < fanout; k++ { if pos := idx.FanoutMapping[k]; pos != noMapping { if _, err := io.ReadFull(r, idx.Offset32[pos]); err != nil { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-git/v5/plumbing/format/index/decoder.go new/vendor/github.com/go-git/go-git/v5/plumbing/format/index/decoder.go --- old/vendor/github.com/go-git/go-git/v5/plumbing/format/index/decoder.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/go-git/go-git/v5/plumbing/format/index/decoder.go 2026-04-24 04:28:05.000000000 +0200 @@ -4,8 +4,8 @@ "bufio" "bytes" "errors" + "fmt" "io" - "strconv" "time" @@ -26,12 +26,14 @@ ErrInvalidChecksum = errors.New("invalid checksum") // ErrUnknownExtension is returned when an index extension is encountered that is considered mandatory ErrUnknownExtension = errors.New("unknown extension") + // ErrMalformedIndexFile is returned when the index file contents are + // structurally invalid. + ErrMalformedIndexFile = errors.New("index decoder: malformed index file") ) const ( entryHeaderLength = 62 entryExtended = 0x4000 - entryValid = 0x8000 nameMask = 0xfff intentToAddMask = 1 << 13 skipWorkTreeMask = 1 << 14 @@ -140,33 +142,55 @@ e.SkipWorktree = extended&skipWorkTreeMask != 0 } - if err := d.readEntryName(idx, e, flags); err != nil { + nameConsumed, err := d.readEntryName(idx, e, flags) + if err != nil { return nil, err } - return e, d.padEntry(idx, e, read) + return e, d.padEntry(idx, e, read, nameConsumed) } -func (d *Decoder) readEntryName(idx *Index, e *Entry, flags uint16) error { - var name string - var err error - +// readEntryName reads the entry path and sets e.Name. It returns the +// number of bytes consumed from the stream for the name portion. +func (d *Decoder) readEntryName(idx *Index, e *Entry, flags uint16) (int, error) { switch idx.Version { case 2, 3: - len := flags & nameMask - name, err = d.doReadEntryName(len) + nameLen := flags & nameMask + name, consumed, err := d.doReadEntryName(nameLen) + if err != nil { + return 0, err + } + e.Name = name + return consumed, nil case 4: - name, err = d.doReadEntryNameV4() + name, err := d.doReadEntryNameV4() + if err != nil { + return 0, err + } + e.Name = name + return 0, nil // V4 has no padding; consumed count unused default: - return ErrUnsupportedVersion + return 0, ErrUnsupportedVersion } +} - if err != nil { - return err +// doReadEntryName reads the entry path for V2/V3 indexes. It returns the +// name, the number of bytes consumed from the stream, and any error. +// When nameLen equals nameMask (0xFFF), the name was too long to fit in +// the 12-bit field and the real length is found by scanning for the NUL +// terminator — matching C Git's strlen(name) fallback in create_from_disk. +func (d *Decoder) doReadEntryName(nameLen uint16) (string, int, error) { + if nameLen == nameMask { + name, err := binary.ReadUntil(d.r, '\x00') + if err != nil { + return "", 0, err + } + return string(name), len(name) + 1, nil // +1 for the consumed NUL delimiter } - e.Name = name - return nil + name := make([]byte, nameLen) + _, err := io.ReadFull(d.r, name) + return string(name), int(nameLen), err } func (d *Decoder) doReadEntryNameV4() (string, error) { @@ -177,7 +201,14 @@ var base string if d.lastEntry != nil { + if l < 0 || int(l) > len(d.lastEntry.Name) { + return "", fmt.Errorf("%w: invalid V4 entry name strip length %d (previous name length: %d)", + ErrMalformedIndexFile, l, len(d.lastEntry.Name)) + } base = d.lastEntry.Name[:len(d.lastEntry.Name)-int(l)] + } else if l > 0 { + return "", fmt.Errorf("%w: non-zero strip length %d on first V4 entry", + ErrMalformedIndexFile, l) } name, err := binary.ReadUntil(d.r, '\x00') @@ -188,24 +219,23 @@ return base + string(name), nil } -func (d *Decoder) doReadEntryName(len uint16) (string, error) { - name := make([]byte, len) - _, err := io.ReadFull(d.r, name) - - return string(name), err -} - -// Index entries are padded out to the next 8 byte alignment -// for historical reasons related to how C Git read the files. -func (d *Decoder) padEntry(idx *Index, e *Entry, read int) error { +// padEntry discards NUL padding bytes that follow each V2/V3 entry on +// disk. nameConsumed is the number of stream bytes consumed while reading +// the entry name (which may exceed len(e.Name) when a NUL terminator was +// consumed for long names where the 12-bit length field overflowed). +func (d *Decoder) padEntry(idx *Index, e *Entry, read, nameConsumed int) error { if idx.Version == 4 { return nil } entrySize := read + len(e.Name) padLen := 8 - entrySize%8 - _, err := io.CopyN(io.Discard, d.r, int64(padLen)) - return err + padLen -= nameConsumed - len(e.Name) + if padLen > 0 { + _, err := io.CopyN(io.Discard, d.r, int64(padLen)) + return err + } + return nil } func (d *Decoder) readExtensions(idx *Index) error { @@ -312,7 +342,7 @@ } func validateHeader(r io.Reader) (version uint32, err error) { - var s = make([]byte, 4) + s := make([]byte, 4) if _, err := io.ReadFull(r, s); err != nil { return 0, err } @@ -376,24 +406,26 @@ return nil, err } - // An entry can be in an invalidated state and is represented by having a - // negative number in the entry_count field. - if i == -1 { - return nil, nil - } - e.Entries = i trees, err := binary.ReadUntil(d.r, '\n') if err != nil { return nil, err } - i, err = strconv.Atoi(string(trees)) + subtrees, err := strconv.Atoi(string(trees)) if err != nil { return nil, err } - e.Trees = i + e.Trees = subtrees + + // An entry can be in an invalidated state and is represented by having a + // negative number in the entry_count field. In this case, there is no + // object name and the next entry starts immediately after the newline. + if i < 0 { + return nil, nil + } + _, err = io.ReadFull(d.r, e.Hash[:]) if err != nil { return nil, err diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-git/v5/plumbing/format/index/encoder.go new/vendor/github.com/go-git/go-git/v5/plumbing/format/index/encoder.go --- old/vendor/github.com/go-git/go-git/v5/plumbing/format/index/encoder.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/go-git/go-git/v5/plumbing/format/index/encoder.go 2026-04-24 04:28:05.000000000 +0200 @@ -5,9 +5,7 @@ "errors" "fmt" "io" - "path" "sort" - "strings" "time" "github.com/go-git/go-git/v5/plumbing/hash" @@ -160,26 +158,39 @@ } func (e *Encoder) encodeEntryNameV4(entry *Entry) error { - name := entry.Name - l := 0 + // V4 prefix compression: find the longest common prefix between the + // previous entry's name and the current one. The strip length tells + // the decoder how many bytes to remove from the end of the previous + // name, and the suffix is the remainder of the current name. + prefix := 0 if e.lastEntry != nil { - dir := path.Dir(e.lastEntry.Name) + "/" - if strings.HasPrefix(entry.Name, dir) { - l = len(e.lastEntry.Name) - len(dir) - name = strings.TrimPrefix(entry.Name, dir) - } else { - l = len(e.lastEntry.Name) - } + prefix = commonPrefixLen(e.lastEntry.Name, entry.Name) + } + stripLen := 0 + if e.lastEntry != nil { + stripLen = len(e.lastEntry.Name) - prefix } e.lastEntry = entry - err := binary.WriteVariableWidthInt(e.w, int64(l)) - if err != nil { + if err := binary.WriteVariableWidthInt(e.w, int64(stripLen)); err != nil { return err } - return binary.Write(e.w, []byte(name+string('\x00'))) + suffix := entry.Name[prefix:] + return binary.Write(e.w, append([]byte(suffix), '\x00')) +} + +// commonPrefixLen returns the length of the longest common byte prefix +// between a and b. +func commonPrefixLen(a, b string) int { + n := min(len(b), len(a)) + for i := range n { + if a[i] != b[i] { + return i + } + } + return n } func (e *Encoder) encodeRawExtension(signature string, data []byte) error { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-git/v5/plumbing/format/index/index.go new/vendor/github.com/go-git/go-git/v5/plumbing/format/index/index.go --- old/vendor/github.com/go-git/go-git/v5/plumbing/format/index/index.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/go-git/go-git/v5/plumbing/format/index/index.go 2026-04-24 04:28:05.000000000 +0200 @@ -54,6 +54,8 @@ ResolveUndo *ResolveUndo // EndOfIndexEntry represents the 'End of Index Entry' extension EndOfIndexEntry *EndOfIndexEntry + // ModTime is the modification time of the index file + ModTime time.Time } // Add creates a new Entry and returns it. The caller should first check that diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-git/v5/plumbing/transport/http/common.go new/vendor/github.com/go-git/go-git/v5/plumbing/transport/http/common.go --- old/vendor/github.com/go-git/go-git/v5/plumbing/transport/http/common.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/go-git/go-git/v5/plumbing/transport/http/common.go 2026-04-24 04:28:05.000000000 +0200 @@ -7,7 +7,6 @@ "crypto/tls" "crypto/x509" "fmt" - "net" "net/http" "net/url" "reflect" @@ -24,6 +23,33 @@ "github.com/go-git/go-git/v5/utils/ioutil" ) +type contextKey int + +const initialRequestKey contextKey = iota + +// RedirectPolicy controls how the HTTP transport follows redirects. +// +// The values mirror Git's http.followRedirects config: +// "true" follows redirects for all requests, "false" treats redirects as +// errors, and "initial" follows redirects only for the initial +// /info/refs discovery request. The zero value defaults to "initial". +type RedirectPolicy string + +const ( + FollowInitialRedirects RedirectPolicy = "initial" + FollowRedirects RedirectPolicy = "true" + NoFollowRedirects RedirectPolicy = "false" +) + +func withInitialRequest(ctx context.Context) context.Context { + return context.WithValue(ctx, initialRequestKey, true) +} + +func isInitialRequest(req *http.Request) bool { + v, _ := req.Context().Value(initialRequestKey).(bool) + return v +} + // it requires a bytes.Buffer, because we need to know the length func applyHeadersToRequest(req *http.Request, content *bytes.Buffer, host string, requestType string) { req.Header.Add("User-Agent", capability.DefaultAgent()) @@ -54,12 +80,15 @@ s.ApplyAuthToRequest(req) applyHeadersToRequest(req, nil, s.endpoint.Host, serviceName) - res, err := s.client.Do(req.WithContext(ctx)) + res, err := s.client.Do(req.WithContext(withInitialRequest(ctx))) if err != nil { return nil, err } - s.ModifyEndpointIfRedirect(res) + if err := s.ModifyEndpointIfRedirect(res); err != nil { + _ = res.Body.Close() + return nil, err + } defer ioutil.CheckClose(res.Body, &err) if err = NewErr(res); err != nil { @@ -96,6 +125,7 @@ client *http.Client transports *lru.Cache mutex sync.RWMutex + follow RedirectPolicy } // ClientOptions holds user configurable options for the client. @@ -106,6 +136,11 @@ // size, will result in the least recently used transport getting deleted // before the provided transport is added to the cache. CacheMaxEntries int + + // RedirectPolicy controls redirect handling. Supported values are + // "true", "false", and "initial". The zero value defaults to + // "initial", matching Git's http.followRedirects default. + RedirectPolicy RedirectPolicy } var ( @@ -150,12 +185,16 @@ } cl := &client{ client: c, + follow: FollowInitialRedirects, } if opts != nil { if opts.CacheMaxEntries > 0 { cl.transports = lru.New(opts.CacheMaxEntries) } + if opts.RedirectPolicy != "" { + cl.follow = opts.RedirectPolicy + } } return cl } @@ -289,14 +328,9 @@ } } - httpClient = &http.Client{ - Transport: transport, - CheckRedirect: c.client.CheckRedirect, - Jar: c.client.Jar, - Timeout: c.client.Timeout, - } + httpClient = c.cloneHTTPClient(transport) } else { - httpClient = c.client + httpClient = c.cloneHTTPClient(c.client.Transport) } s := &session{ @@ -324,30 +358,122 @@ s.auth.SetAuth(req) } -func (s *session) ModifyEndpointIfRedirect(res *http.Response) { +func (s *session) ModifyEndpointIfRedirect(res *http.Response) error { if res.Request == nil { - return + return nil + } + if s.endpoint == nil { + return fmt.Errorf("http redirect: nil endpoint") } r := res.Request if !strings.HasSuffix(r.URL.Path, infoRefsPath) { - return + return fmt.Errorf("http redirect: target %q does not end with %s", r.URL.Path, infoRefsPath) + } + if r.URL.Scheme != "http" && r.URL.Scheme != "https" { + return fmt.Errorf("http redirect: unsupported scheme %q", r.URL.Scheme) + } + if r.URL.Scheme != s.endpoint.Protocol && + !(s.endpoint.Protocol == "http" && r.URL.Scheme == "https") { + return fmt.Errorf("http redirect: changes scheme from %q to %q", s.endpoint.Protocol, r.URL.Scheme) } - h, p, err := net.SplitHostPort(r.URL.Host) + host := endpointHost(r.URL.Hostname()) + port, err := endpointPort(r.URL.Port()) if err != nil { - h = r.URL.Host + return err } - if p != "" { - port, err := strconv.Atoi(p) - if err == nil { - s.endpoint.Port = port - } + + if host != s.endpoint.Host || effectivePort(r.URL.Scheme, port) != effectivePort(s.endpoint.Protocol, s.endpoint.Port) { + s.endpoint.User = "" + s.endpoint.Password = "" + s.auth = nil } - s.endpoint.Host = h + + s.endpoint.Host = host + s.endpoint.Port = port s.endpoint.Protocol = r.URL.Scheme s.endpoint.Path = r.URL.Path[:len(r.URL.Path)-len(infoRefsPath)] + return nil +} + +func endpointHost(host string) string { + if strings.Contains(host, ":") { + return "[" + host + "]" + } + + return host +} + +func endpointPort(port string) (int, error) { + if port == "" { + return 0, nil + } + + parsed, err := strconv.Atoi(port) + if err != nil { + return 0, fmt.Errorf("http redirect: invalid port %q", port) + } + + return parsed, nil +} + +func effectivePort(scheme string, port int) int { + if port != 0 { + return port + } + + switch strings.ToLower(scheme) { + case "http": + return 80 + case "https": + return 443 + default: + return 0 + } +} + +func (c *client) cloneHTTPClient(transport http.RoundTripper) *http.Client { + return &http.Client{ + Transport: transport, + CheckRedirect: wrapCheckRedirect(c.follow, c.client.CheckRedirect), + Jar: c.client.Jar, + Timeout: c.client.Timeout, + } +} + +func wrapCheckRedirect(policy RedirectPolicy, next func(*http.Request, []*http.Request) error) func(*http.Request, []*http.Request) error { + return func(req *http.Request, via []*http.Request) error { + if err := checkRedirect(req, via, policy); err != nil { + return err + } + if next != nil { + return next(req, via) + } + return nil + } +} + +func checkRedirect(req *http.Request, via []*http.Request, policy RedirectPolicy) error { + switch policy { + case FollowRedirects: + case NoFollowRedirects: + return fmt.Errorf("http redirect: redirects disabled to %s", req.URL) + case "", FollowInitialRedirects: + if !isInitialRequest(req) { + return fmt.Errorf("http redirect: redirect on non-initial request to %s", req.URL) + } + default: + return fmt.Errorf("http redirect: invalid redirect policy %q", policy) + } + if req.URL.Scheme != "http" && req.URL.Scheme != "https" { + return fmt.Errorf("http redirect: unsupported scheme %q", req.URL.Scheme) + } + if len(via) >= 10 { + return fmt.Errorf("http redirect: too many redirects") + } + return nil } func (*session) Close() error { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-git/v5/repository.go new/vendor/github.com/go-git/go-git/v5/repository.go --- old/vendor/github.com/go-git/go-git/v5/repository.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/go-git/go-git/v5/repository.go 2026-04-24 04:28:05.000000000 +0200 @@ -208,6 +208,12 @@ return nil, ErrRepositoryNotExists } + cfg, err := s.Config() + if err != nil { + return nil, err + } + + err = verifyExtensions(s, cfg) if err != nil { return nil, err } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-git/v5/repository_extensions.go new/vendor/github.com/go-git/go-git/v5/repository_extensions.go --- old/vendor/github.com/go-git/go-git/v5/repository_extensions.go 1970-01-01 01:00:00.000000000 +0100 +++ new/vendor/github.com/go-git/go-git/v5/repository_extensions.go 2026-04-24 04:28:05.000000000 +0200 @@ -0,0 +1,121 @@ +package git + +import ( + "errors" + "fmt" + "strings" + + "github.com/go-git/go-git/v5/config" + cfgformat "github.com/go-git/go-git/v5/plumbing/format/config" + "github.com/go-git/go-git/v5/storage" +) + +var ( + // ErrUnsupportedExtensionRepositoryFormatVersion represents when an + // extension being used is not compatible with the repository's + // core.repositoryFormatVersion. + ErrUnsupportedExtensionRepositoryFormatVersion = errors.New("core.repositoryformatversion does not support extension") + + // ErrUnsupportedRepositoryFormatVersion represents when an repository + // is using a format version that is not supported. + ErrUnsupportedRepositoryFormatVersion = errors.New("core.repositoryformatversion not supported") + + // ErrUnknownExtension represents when a repository has an extension + // which is unknown or unsupported by go-git. + ErrUnknownExtension = errors.New("unknown extension") + + // builtinExtensions defines the Git extensions that are supported by + // the core go-git implementation. + // + // Some extensions are storage-specific, those are defined by the Storers + // themselves by implementing the ExtensionChecker interface. + builtinExtensions = map[string]struct{}{ + // noop does not change git’s behavior at all. + // It is useful only for testing format-1 compatibility. + // + // This extension is respected regardless of the + // core.repositoryFormatVersion setting. + "noop": {}, + + // noop-v1 does not change git’s behavior at all. + // It is useful only for testing format-1 compatibility. + "noop-v1": {}, + } + + // Some Git extensions were supported upstream before the introduction + // of repositoryformatversion. These are the only extensions that can be + // enabled while core.repositoryformatversion is unset or set to 0. + extensionsValidForV0 = map[string]struct{}{ + "noop": {}, + "partialClone": {}, + "preciousObjects": {}, + "worktreeConfig": {}, + } +) + +type extension struct { + name string + value string +} + +func extensions(cfg *config.Config) []extension { + if cfg == nil || cfg.Raw == nil { + return nil + } + + if !cfg.Raw.HasSection("extensions") { + return nil + } + + section := cfg.Raw.Section("extensions") + out := make([]extension, 0, len(section.Options)) + for _, opt := range section.Options { + out = append(out, extension{name: strings.ToLower(opt.Key), value: strings.ToLower(opt.Value)}) + } + + return out +} + +func verifyExtensions(st storage.Storer, cfg *config.Config) error { + needed := extensions(cfg) + + switch cfg.Core.RepositoryFormatVersion { + case "", cfgformat.Version_0, cfgformat.Version_1: + default: + return fmt.Errorf("%w: %q", + ErrUnsupportedRepositoryFormatVersion, + cfg.Core.RepositoryFormatVersion) + } + + if len(needed) > 0 { + if cfg.Core.RepositoryFormatVersion == cfgformat.Version_0 || + cfg.Core.RepositoryFormatVersion == "" { + var unsupported []string + for _, ext := range needed { + if _, ok := extensionsValidForV0[ext.name]; !ok { + unsupported = append(unsupported, ext.name) + } + } + if len(unsupported) > 0 { + return fmt.Errorf("%w: %s", + ErrUnsupportedExtensionRepositoryFormatVersion, + strings.Join(unsupported, ", ")) + } + } + + var missing []string + for _, ext := range needed { + if _, ok := builtinExtensions[ext.name]; ok { + continue + } + + missing = append(missing, ext.name) + } + + if len(missing) > 0 { + return fmt.Errorf("%w: %s", ErrUnknownExtension, strings.Join(missing, ", ")) + } + } + + return nil +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers.go new/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers.go --- old/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers.go 2026-04-24 04:28:05.000000000 +0200 @@ -3,6 +3,7 @@ import ( "fmt" "io" + "os" "sync/atomic" "github.com/go-git/go-git/v5/plumbing" @@ -131,20 +132,62 @@ func (w *PackWriter) save() error { base := w.fs.Join(objectsPath, packPath, fmt.Sprintf("pack-%s", w.checksum)) - idx, err := w.fs.Create(fmt.Sprintf("%s.idx", base)) + + // Pack files are content addressable. Each file is checked + // individually — if it already exists on disk, skip creating it. + idxPath := fmt.Sprintf("%s.idx", base) + exists, err := fileExists(w.fs, idxPath) if err != nil { return err } + if !exists { + idx, err := w.fs.Create(idxPath) + if err != nil { + return err + } - if err := w.encodeIdx(idx); err != nil { - return err + if err := w.encodeIdx(idx); err != nil { + _ = idx.Close() + return err + } + + if err := idx.Close(); err != nil { + return err + } + fixPermissions(w.fs, idxPath) } - if err := idx.Close(); err != nil { + packPath := fmt.Sprintf("%s.pack", base) + exists, err = fileExists(w.fs, packPath) + if err != nil { return err } + if !exists { + if err := w.fs.Rename(w.fw.Name(), packPath); err != nil { + return err + } + fixPermissions(w.fs, packPath) + } else { + // Pack already exists, clean up the temp file. + return w.clean() + } - return w.fs.Rename(w.fw.Name(), fmt.Sprintf("%s.pack", base)) + return nil +} + +// fileExists checks whether path already exists as a regular file. +// It returns (true, nil) for an existing regular file, (false, nil) when the +// path does not exist, and (false, err) if the path exists but is not a +// regular file (e.g. a directory or symlink). +func fileExists(fs billy.Filesystem, path string) (bool, error) { + fi, err := fs.Lstat(path) + if err != nil { + return false, nil + } + if !fi.Mode().IsRegular() { + return false, fmt.Errorf("unexpected file type for %q: %s", path, fi.Mode().Type()) + } + return true, nil } func (w *PackWriter) encodeIdx(writer io.Writer) error { @@ -226,7 +269,6 @@ atomic.StoreUint32(&s.blocked, 1) <-s.news } - } func (s *syncedReader) Seek(offset int64, whence int) (int64, error) { @@ -281,5 +323,17 @@ hex := w.Hash().String() file := w.fs.Join(objectsPath, hex[0:2], hex[2:hash.HexSize]) - return w.fs.Rename(w.f.Name(), file) + // Loose objects are content addressable, if they already exist + // we can safely delete the temporary file and short-circuit the + // operation. + if _, err := w.fs.Lstat(file); err == nil || os.IsExist(err) { + return w.fs.Remove(w.f.Name()) + } + + if err := w.fs.Rename(w.f.Name(), file); err != nil { + return err + } + fixPermissions(w.fs, file) + + return nil } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_unix.go new/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_unix.go --- old/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_unix.go 1970-01-01 01:00:00.000000000 +0100 +++ new/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_unix.go 2026-04-24 04:28:05.000000000 +0200 @@ -0,0 +1,29 @@ +//go:build !windows + +package dotgit + +import ( + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-git/v5/utils/trace" +) + +func fixPermissions(fs billy.Filesystem, path string) { + if chmodFS, ok := fs.(billy.Chmod); ok { + if err := chmodFS.Chmod(path, 0o444); err != nil { + trace.General.Printf("failed to chmod %s: %v", path, err) + } + } +} + +func isReadOnly(fs billy.Filesystem, path string) (bool, error) { + fi, err := fs.Stat(path) + if err != nil { + return false, err + } + + if fi.Mode().Perm() == 0o444 { + return true, nil + } + + return false, nil +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_windows.go new/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_windows.go --- old/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_windows.go 1970-01-01 01:00:00.000000000 +0100 +++ new/vendor/github.com/go-git/go-git/v5/storage/filesystem/dotgit/writers_windows.go 2026-04-24 04:28:05.000000000 +0200 @@ -0,0 +1,58 @@ +//go:build windows + +package dotgit + +import ( + "fmt" + "path/filepath" + + "github.com/go-git/go-billy/v5" + "github.com/go-git/go-git/v5/utils/trace" + "golang.org/x/sys/windows" +) + +func fixPermissions(fs billy.Filesystem, path string) { + fullpath := filepath.Join(fs.Root(), path) + p, err := windows.UTF16PtrFromString(fullpath) + if err != nil { + trace.General.Printf("failed to chmod %s: %v", fullpath, err) + return + } + + attrs, err := windows.GetFileAttributes(p) + if err != nil { + trace.General.Printf("failed to chmod %s: %v", fullpath, err) + return + } + + if attrs&windows.FILE_ATTRIBUTE_READONLY != 0 { + return + } + + err = windows.SetFileAttributes(p, + attrs|windows.FILE_ATTRIBUTE_READONLY, + ) + + if err != nil { + trace.General.Printf("failed to chmod %s: %v", fullpath, err) + } +} + +func isReadOnly(fs billy.Filesystem, path string) (bool, error) { + fullpath := filepath.Join(fs.Root(), path) + p, err := windows.UTF16PtrFromString(fullpath) + if err != nil { + return false, fmt.Errorf("%w: %q", err, fullpath) + } + + attrs, err := windows.GetFileAttributes(p) + if err != nil { + return false, fmt.Errorf("%w: %q", err, fullpath) + } + + if attrs&windows.FILE_ATTRIBUTE_READONLY != 0 { + return true, nil + } + + return false, nil +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-git/v5/storage/filesystem/index.go new/vendor/github.com/go-git/go-git/v5/storage/filesystem/index.go --- old/vendor/github.com/go-git/go-git/v5/storage/filesystem/index.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/go-git/go-git/v5/storage/filesystem/index.go 2026-04-24 04:28:05.000000000 +0200 @@ -48,6 +48,11 @@ defer ioutil.CheckClose(f, &err) + fi, statErr := s.dir.Fs().Stat(f.Name()) + if statErr == nil { + idx.ModTime = fi.ModTime() + } + d := index.NewDecoder(f) err = d.Decode(idx) return idx, err diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-git/v5/storage/memory/storage.go new/vendor/github.com/go-git/go-git/v5/storage/memory/storage.go --- old/vendor/github.com/go-git/go-git/v5/storage/memory/storage.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/go-git/go-git/v5/storage/memory/storage.go 2026-04-24 04:28:05.000000000 +0200 @@ -69,7 +69,11 @@ index *index.Index } +// SetIndex stores the given index. +// Note: this method sets idx.ModTime to simulate filesystem storage behavior. func (c *IndexStorage) SetIndex(idx *index.Index) error { + // Set ModTime to enable racy git detection in the metadata optimization. + idx.ModTime = time.Now() c.index = idx return nil } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-git/v5/utils/merkletrie/filesystem/node.go new/vendor/github.com/go-git/go-git/v5/utils/merkletrie/filesystem/node.go --- old/vendor/github.com/go-git/go-git/v5/utils/merkletrie/filesystem/node.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/go-git/go-git/v5/utils/merkletrie/filesystem/node.go 2026-04-24 04:28:05.000000000 +0200 @@ -4,9 +4,11 @@ "io" "os" "path" + "time" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/filemode" + "github.com/go-git/go-git/v5/plumbing/format/index" "github.com/go-git/go-git/v5/utils/merkletrie/noder" "github.com/go-git/go-billy/v5" @@ -16,6 +18,14 @@ ".git": true, } +// Options contains configuration for the filesystem node. +type Options struct { + // Index is used to enable the metadata-first comparison optimization while + // correctly handling the "racy git" condition. If no index is provided, + // the function works without the optimization. + Index *index.Index +} + // The node represents a file or a directory in a billy.Filesystem. It // implements the interface noder.Noder of merkletrie package. // @@ -24,6 +34,8 @@ type node struct { fs billy.Filesystem submodules map[string]plumbing.Hash + idx *index.Index + idxMap map[string]*index.Entry path string hash []byte @@ -31,6 +43,7 @@ isDir bool mode os.FileMode size int64 + modTime time.Time } // NewRootNode returns the root node based on a given billy.Filesystem. @@ -42,7 +55,41 @@ fs billy.Filesystem, submodules map[string]plumbing.Hash, ) noder.Noder { - return &node{fs: fs, submodules: submodules, isDir: true} + return NewRootNodeWithOptions(fs, submodules, Options{}) +} + +// NewRootNodeWithOptions returns the root node based on a given billy.Filesystem +// with options to set an index. Providing an index enables the metadata-first +// comparison optimization while correctly handling the "racy git" condition. If +// no index is provided, the function works without the optimization. +// +// The index's ModTime field is used to detect the racy git condition. When a file's +// mtime equals or is newer than the index ModTime, we must hash the file content +// even if other metadata matches, because the file may have been modified in the +// same second that the index was written. +// +// Reference: https://git-scm.com/docs/racy-git +func NewRootNodeWithOptions( + fs billy.Filesystem, + submodules map[string]plumbing.Hash, + options Options, +) noder.Noder { + var idxMap map[string]*index.Entry + + if options.Index != nil { + idxMap = make(map[string]*index.Entry, len(options.Index.Entries)) + for _, entry := range options.Index.Entries { + idxMap[entry.Name] = entry + } + } + + return &node{ + fs: fs, + submodules: submodules, + idx: options.Index, + idxMap: idxMap, + isDir: true, + } } // Hash the hash of a filesystem is the result of concatenating the computed @@ -133,11 +180,14 @@ node := &node{ fs: n.fs, submodules: n.submodules, + idx: n.idx, + idxMap: n.idxMap, - path: path, - isDir: file.IsDir(), - size: file.Size(), - mode: file.Mode(), + path: path, + isDir: file.IsDir(), + size: file.Size(), + mode: file.Mode(), + modTime: file.ModTime(), } if _, isSubmodule := n.submodules[path]; isSubmodule { @@ -161,6 +211,16 @@ n.hash = append(submoduleHash[:], filemode.Submodule.Bytes()...) return } + + if n.idxMap != nil { + if entry, ok := n.idxMap[n.path]; ok { + if n.metadataMatches(entry) { + n.hash = append(entry.Hash[:], mode.Bytes()...) + return + } + } + } + var hash plumbing.Hash if n.mode&os.ModeSymlink != 0 { hash = n.doCalculateHashForSymlink() @@ -170,6 +230,44 @@ n.hash = append(hash[:], mode.Bytes()...) } +func (n *node) metadataMatches(entry *index.Entry) bool { + if entry == nil { + return false + } + + if uint32(n.size) != entry.Size { + return false + } + + if !n.modTime.IsZero() && !n.modTime.Equal(entry.ModifiedAt) { + return false + } + + mode, err := filemode.NewFromOSFileMode(n.mode) + if err != nil { + return false + } + + if mode != entry.Mode { + return false + } + + if n.idx != nil && !n.idx.ModTime.IsZero() && !n.modTime.IsZero() { + if !n.modTime.Before(n.idx.ModTime) { + return false + } + } + + // If we couldn't perform the racy git check (idx is nil or idx.ModTime is zero), + // we cannot safely rely on metadata alone — force content hashing. + // This can occur with in-memory storage where the index file timestamp is unavailable. + if n.idx == nil || n.idx.ModTime.IsZero() { + return false + } + + return true +} + func (n *node) doCalculateHashForRegular() plumbing.Hash { f, err := n.fs.Open(n.path) if err != nil { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-git/v5/worktree.go new/vendor/github.com/go-git/go-git/v5/worktree.go --- old/vendor/github.com/go-git/go-git/v5/worktree.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/go-git/go-git/v5/worktree.go 2026-04-24 04:28:05.000000000 +0200 @@ -385,7 +385,8 @@ return nil, err } - var removedFiles []string + removedFiles := make([]string, 0, len(changes)) + filesMap := buildFilePathMap(files) for _, ch := range changes { a, err := ch.Action() if err != nil { @@ -407,7 +408,7 @@ } if len(files) > 0 { - contains := inFiles(files, name) + contains := inFiles(filesMap, name) if !contains { continue } @@ -436,15 +437,11 @@ return removedFiles, w.r.Storer.SetIndex(idx) } -func inFiles(files []string, v string) bool { +// inFiles checks if the given file is in the list of files. The incoming filepaths in files should be cleaned before calling this function. +func inFiles(files map[string]struct{}, v string) bool { v = filepath.Clean(v) - for _, s := range files { - if filepath.Clean(s) == v { - return true - } - } - - return false + _, exists := files[v] + return exists } func (w *Worktree) resetWorktree(t *object.Tree, files []string) error { @@ -459,6 +456,7 @@ } b := newIndexBuilder(idx) + filesMap := buildFilePathMap(files) for _, ch := range changes { if err := w.validChange(ch); err != nil { return err @@ -476,7 +474,7 @@ continue } - contains := inFiles(files, file) + contains := inFiles(filesMap, file) if !contains { continue } @@ -1206,3 +1204,16 @@ func (b *indexBuilder) Remove(name string) { delete(b.entries, filepath.ToSlash(name)) } + +// buildFilePathMap creates a map of cleaned file paths for efficient lookup. +// Returns nil if the input slice is empty. +func buildFilePathMap(files []string) map[string]struct{} { + if len(files) == 0 { + return nil + } + filesMap := make(map[string]struct{}, len(files)) + for _, f := range files { + filesMap[filepath.Clean(f)] = struct{}{} + } + return filesMap +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/github.com/go-git/go-git/v5/worktree_status.go new/vendor/github.com/go-git/go-git/v5/worktree_status.go --- old/vendor/github.com/go-git/go-git/v5/worktree_status.go 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/github.com/go-git/go-git/v5/worktree_status.go 2026-04-24 04:28:05.000000000 +0200 @@ -141,7 +141,7 @@ return nil, err } - to := filesystem.NewRootNode(w.Filesystem, submodules) + to := filesystem.NewRootNodeWithOptions(w.Filesystem, submodules, filesystem.Options{Index: idx}) var c merkletrie.Changes if reverse { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vendor/modules.txt new/vendor/modules.txt --- old/vendor/modules.txt 2026-03-04 03:08:05.000000000 +0100 +++ new/vendor/modules.txt 2026-04-24 04:28:05.000000000 +0200 @@ -39,7 +39,7 @@ # github.com/chavacava/garif v0.1.0 ## explicit; go 1.16 github.com/chavacava/garif -# github.com/cloudflare/circl v1.6.1 +# github.com/cloudflare/circl v1.6.3 ## explicit; go 1.22.0 github.com/cloudflare/circl/dh/x25519 github.com/cloudflare/circl/dh/x448 @@ -79,15 +79,15 @@ github.com/go-git/gcfg/scanner github.com/go-git/gcfg/token github.com/go-git/gcfg/types -# github.com/go-git/go-billy/v5 v5.6.2 -## explicit; go 1.21 +# github.com/go-git/go-billy/v5 v5.8.0 +## explicit; go 1.24.0 github.com/go-git/go-billy/v5 github.com/go-git/go-billy/v5/helper/chroot github.com/go-git/go-billy/v5/helper/polyfill github.com/go-git/go-billy/v5/memfs github.com/go-git/go-billy/v5/osfs github.com/go-git/go-billy/v5/util -# github.com/go-git/go-git/v5 v5.16.5 +# github.com/go-git/go-git/v5 v5.18.0 ## explicit; go 1.24.0 github.com/go-git/go-git/v5 github.com/go-git/go-git/v5/config
