Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add clone --shared feature #860

Merged
merged 1 commit into from
Oct 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ coverage.txt
profile.out
.tmp/
.git-dist/
.vscode
344 changes: 172 additions & 172 deletions COMPATIBILITY.md

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ type CloneOptions struct {
CABundle []byte
// ProxyOptions provides info required for connecting to a proxy.
ProxyOptions transport.ProxyOptions
// When the repository to clone is on the local machine, instead of
// using hard links, automatically setup .git/objects/info/alternates
// to share the objects with the source repository.
// The resulting repository starts out without any object of its own.
// NOTE: this is a possibly dangerous operation; do not use it unless
// you understand what it does.
//
// [Reference]: https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---shared
Shared bool
enverbisevac marked this conversation as resolved.
Show resolved Hide resolved
}

// Validate validates the fields and sets the default values.
Expand Down
1 change: 1 addition & 0 deletions plumbing/storer/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type EncodedObjectStorer interface {
HasEncodedObject(plumbing.Hash) error
// EncodedObjectSize returns the plaintext size of the encoded object.
EncodedObjectSize(plumbing.Hash) (int64, error)
AddAlternate(remote string) error
}

// DeltaObjectStorer is an EncodedObjectStorer that can return delta
Expand Down
4 changes: 4 additions & 0 deletions plumbing/storer/object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,7 @@ func (o *MockObjectStorage) IterEncodedObjects(t plumbing.ObjectType) (EncodedOb
func (o *MockObjectStorage) Begin() Transaction {
return nil
}

func (o *MockObjectStorage) AddAlternate(remote string) error {
return nil
}
26 changes: 26 additions & 0 deletions repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/internal/path_util"
"github.com/go-git/go-git/v5/internal/revision"
"github.com/go-git/go-git/v5/internal/url"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/cache"
formatcfg "github.com/go-git/go-git/v5/plumbing/format/config"
Expand Down Expand Up @@ -62,6 +63,7 @@ var (
ErrUnableToResolveCommit = errors.New("unable to resolve commit")
ErrPackedObjectsNotSupported = errors.New("packed objects not supported")
ErrSHA256NotSupported = errors.New("go-git was not compiled with SHA256 support")
ErrAlternatePathNotSupported = errors.New("alternate path must use the file scheme")
)

// Repository represents a git repository
Expand Down Expand Up @@ -887,6 +889,30 @@ func (r *Repository) clone(ctx context.Context, o *CloneOptions) error {
return err
}

// When the repository to clone is on the local machine,
// instead of using hard links, automatically setup .git/objects/info/alternates
// to share the objects with the source repository
if o.Shared {
if !url.IsLocalEndpoint(o.URL) {
return ErrAlternatePathNotSupported
}
altpath := o.URL
remoteRepo, err := PlainOpen(o.URL)
if err != nil {
return fmt.Errorf("failed to open remote repository: %w", err)
}
conf, err := remoteRepo.Config()
if err != nil {
return fmt.Errorf("failed to read remote repository configuration: %w", err)
}
if !conf.Core.IsBare {
altpath = path.Join(altpath, GitDirName)
}
if err := r.Storer.AddAlternate(altpath); err != nil {
return fmt.Errorf("failed to add alternate file to git objects dir: %w", err)
}
}
enverbisevac marked this conversation as resolved.
Show resolved Hide resolved

ref, err := r.fetchAndUpdateReferences(ctx, &FetchOptions{
RefSpecs: c.Fetch,
Depth: o.Depth,
Expand Down
96 changes: 96 additions & 0 deletions repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"os/exec"
"os/user"
"path"
"path/filepath"
"regexp"
"strings"
Expand Down Expand Up @@ -791,6 +792,101 @@ func (s *RepositorySuite) TestPlainClone(c *C) {
c.Assert(cfg.Branches["master"].Name, Equals, "master")
}

func (s *RepositorySuite) TestPlainCloneBareAndShared(c *C) {
dir, clean := s.TemporalDir()
defer clean()

remote := s.GetBasicLocalRepositoryURL()

r, err := PlainClone(dir, true, &CloneOptions{
URL: remote,
Shared: true,
})
c.Assert(err, IsNil)

altpath := path.Join(dir, "objects", "info", "alternates")
_, err = os.Stat(altpath)
c.Assert(err, IsNil)

data, err := os.ReadFile(altpath)
c.Assert(err, IsNil)

line := path.Join(remote, GitDirName, "objects") + "\n"
c.Assert(string(data), Equals, line)

cfg, err := r.Config()
c.Assert(err, IsNil)
c.Assert(cfg.Branches, HasLen, 1)
c.Assert(cfg.Branches["master"].Name, Equals, "master")
}

func (s *RepositorySuite) TestPlainCloneShared(c *C) {
dir, clean := s.TemporalDir()
defer clean()

remote := s.GetBasicLocalRepositoryURL()

r, err := PlainClone(dir, false, &CloneOptions{
URL: remote,
Shared: true,
})
c.Assert(err, IsNil)

altpath := path.Join(dir, GitDirName, "objects", "info", "alternates")
_, err = os.Stat(altpath)
c.Assert(err, IsNil)

data, err := os.ReadFile(altpath)
c.Assert(err, IsNil)

line := path.Join(remote, GitDirName, "objects") + "\n"
c.Assert(string(data), Equals, line)

cfg, err := r.Config()
c.Assert(err, IsNil)
c.Assert(cfg.Branches, HasLen, 1)
c.Assert(cfg.Branches["master"].Name, Equals, "master")
}

func (s *RepositorySuite) TestPlainCloneSharedHttpShouldReturnError(c *C) {
dir, clean := s.TemporalDir()
defer clean()

remote := "http://somerepo"

_, err := PlainClone(dir, false, &CloneOptions{
URL: remote,
Shared: true,
})
c.Assert(err, Equals, ErrAlternatePathNotSupported)
}

func (s *RepositorySuite) TestPlainCloneSharedHttpsShouldReturnError(c *C) {
dir, clean := s.TemporalDir()
defer clean()

remote := "https://somerepo"

_, err := PlainClone(dir, false, &CloneOptions{
URL: remote,
Shared: true,
})
c.Assert(err, Equals, ErrAlternatePathNotSupported)
}

func (s *RepositorySuite) TestPlainCloneSharedSSHShouldReturnError(c *C) {
dir, clean := s.TemporalDir()
defer clean()

remote := "ssh://somerepo"

_, err := PlainClone(dir, false, &CloneOptions{
URL: remote,
Shared: true,
})
c.Assert(err, Equals, ErrAlternatePathNotSupported)
}

func (s *RepositorySuite) TestPlainCloneWithRemoteName(c *C) {
dir, clean := s.TemporalDir()
defer clean()
Expand Down
33 changes: 32 additions & 1 deletion storage/filesystem/dotgit/dotgit.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
"fmt"
"io"
"os"
"path"
"path/filepath"
"runtime"
"sort"
"strings"
"time"
Expand Down Expand Up @@ -38,6 +40,7 @@ const (
remotesPath = "remotes"
logsPath = "logs"
worktreesPath = "worktrees"
alternatesPath = "alternates"

tmpPackedRefsPrefix = "._packed-refs"

Expand Down Expand Up @@ -1105,10 +1108,38 @@ func (d *DotGit) Module(name string) (billy.Filesystem, error) {
return d.fs.Chroot(d.fs.Join(modulePath, name))
}

func (d *DotGit) AddAlternate(remote string) error {
altpath := d.fs.Join(objectsPath, infoPath, alternatesPath)

f, err := d.fs.OpenFile(altpath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0640)
if err != nil {
return fmt.Errorf("cannot open file: %w", err)
}
defer f.Close()

// locking in windows throws an error, based on comments
// https://github.com/go-git/go-git/pull/860#issuecomment-1751823044
// do not lock on windows platform.
if runtime.GOOS != "windows" {
if err = f.Lock(); err != nil {
return fmt.Errorf("cannot lock file: %w", err)
}
defer f.Unlock()
}

line := path.Join(remote, objectsPath) + "\n"
enverbisevac marked this conversation as resolved.
Show resolved Hide resolved
_, err = io.WriteString(f, line)
enverbisevac marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return fmt.Errorf("error writing 'alternates' file: %w", err)
}

return nil
}

// Alternates returns DotGit(s) based off paths in objects/info/alternates if
// available. This can be used to checks if it's a shared repository.
func (d *DotGit) Alternates() ([]*DotGit, error) {
altpath := d.fs.Join("objects", "info", "alternates")
altpath := d.fs.Join(objectsPath, infoPath, alternatesPath)
f, err := d.fs.Open(altpath)
if err != nil {
return nil, err
Expand Down
4 changes: 4 additions & 0 deletions storage/filesystem/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,7 @@ func (s *Storage) Filesystem() billy.Filesystem {
func (s *Storage) Init() error {
return s.dir.Initialize()
}

func (s *Storage) AddAlternate(remote string) error {
return s.dir.AddAlternate(remote)
}
4 changes: 4 additions & 0 deletions storage/memory/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,10 @@ func (o *ObjectStorage) DeleteLooseObject(plumbing.Hash) error {
return errNotSupported
}

func (o *ObjectStorage) AddAlternate(remote string) error {
return errNotSupported
}

type TxObjectStorage struct {
Storage *ObjectStorage
Objects map[plumbing.Hash]plumbing.EncodedObject
Expand Down
4 changes: 4 additions & 0 deletions storage/transactional/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,7 @@ func (o *ObjectStorage) Commit() error {
return err
})
}

func (o *ObjectStorage) AddAlternate(remote string) error {
return o.temporal.AddAlternate(remote)
}