2018-11-27 21:52:20 +00:00
|
|
|
// https://github.com/git/git/blob/master/Documentation/gitrepository-layout.txt
|
|
|
|
package dotgit
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
stdioutil "io/ioutil"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"gopkg.in/src-d/go-billy.v4/osfs"
|
|
|
|
"gopkg.in/src-d/go-git.v4/plumbing"
|
2019-04-17 02:04:23 +00:00
|
|
|
"gopkg.in/src-d/go-git.v4/storage"
|
2018-11-27 21:52:20 +00:00
|
|
|
"gopkg.in/src-d/go-git.v4/utils/ioutil"
|
|
|
|
|
|
|
|
"gopkg.in/src-d/go-billy.v4"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
suffix = ".git"
|
|
|
|
packedRefsPath = "packed-refs"
|
|
|
|
configPath = "config"
|
|
|
|
indexPath = "index"
|
|
|
|
shallowPath = "shallow"
|
|
|
|
modulePath = "modules"
|
|
|
|
objectsPath = "objects"
|
|
|
|
packPath = "pack"
|
|
|
|
refsPath = "refs"
|
|
|
|
|
|
|
|
tmpPackedRefsPrefix = "._packed-refs"
|
|
|
|
|
|
|
|
packExt = ".pack"
|
|
|
|
idxExt = ".idx"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
// ErrNotFound is returned by New when the path is not found.
|
|
|
|
ErrNotFound = errors.New("path not found")
|
|
|
|
// ErrIdxNotFound is returned by Idxfile when the idx file is not found
|
|
|
|
ErrIdxNotFound = errors.New("idx file not found")
|
|
|
|
// ErrPackfileNotFound is returned by Packfile when the packfile is not found
|
|
|
|
ErrPackfileNotFound = errors.New("packfile not found")
|
|
|
|
// ErrConfigNotFound is returned by Config when the config is not found
|
|
|
|
ErrConfigNotFound = errors.New("config file not found")
|
|
|
|
// ErrPackedRefsDuplicatedRef is returned when a duplicated reference is
|
|
|
|
// found in the packed-ref file. This is usually the case for corrupted git
|
|
|
|
// repositories.
|
|
|
|
ErrPackedRefsDuplicatedRef = errors.New("duplicated ref found in packed-ref file")
|
|
|
|
// ErrPackedRefsBadFormat is returned when the packed-ref file corrupt.
|
|
|
|
ErrPackedRefsBadFormat = errors.New("malformed packed-ref")
|
|
|
|
// ErrSymRefTargetNotFound is returned when a symbolic reference is
|
|
|
|
// targeting a non-existing object. This usually means the repository
|
|
|
|
// is corrupt.
|
|
|
|
ErrSymRefTargetNotFound = errors.New("symbolic reference target not found")
|
|
|
|
)
|
|
|
|
|
|
|
|
// Options holds configuration for the storage.
|
|
|
|
type Options struct {
|
|
|
|
// ExclusiveAccess means that the filesystem is not modified externally
|
|
|
|
// while the repo is open.
|
|
|
|
ExclusiveAccess bool
|
|
|
|
// KeepDescriptors makes the file descriptors to be reused but they will
|
|
|
|
// need to be manually closed calling Close().
|
|
|
|
KeepDescriptors bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// The DotGit type represents a local git repository on disk. This
|
|
|
|
// type is not zero-value-safe, use the New function to initialize it.
|
|
|
|
type DotGit struct {
|
|
|
|
options Options
|
|
|
|
fs billy.Filesystem
|
|
|
|
|
|
|
|
// incoming object directory information
|
|
|
|
incomingChecked bool
|
|
|
|
incomingDirName string
|
|
|
|
|
|
|
|
objectList []plumbing.Hash
|
|
|
|
objectMap map[plumbing.Hash]struct{}
|
|
|
|
packList []plumbing.Hash
|
|
|
|
packMap map[plumbing.Hash]struct{}
|
|
|
|
|
|
|
|
files map[string]billy.File
|
|
|
|
}
|
|
|
|
|
|
|
|
// New returns a DotGit value ready to be used. The path argument must
|
|
|
|
// be the absolute path of a git repository directory (e.g.
|
|
|
|
// "/foo/bar/.git").
|
|
|
|
func New(fs billy.Filesystem) *DotGit {
|
|
|
|
return NewWithOptions(fs, Options{})
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewWithOptions sets non default configuration options.
|
|
|
|
// See New for complete help.
|
|
|
|
func NewWithOptions(fs billy.Filesystem, o Options) *DotGit {
|
|
|
|
return &DotGit{
|
|
|
|
options: o,
|
|
|
|
fs: fs,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Initialize creates all the folder scaffolding.
|
|
|
|
func (d *DotGit) Initialize() error {
|
|
|
|
mustExists := []string{
|
|
|
|
d.fs.Join("objects", "info"),
|
|
|
|
d.fs.Join("objects", "pack"),
|
|
|
|
d.fs.Join("refs", "heads"),
|
|
|
|
d.fs.Join("refs", "tags"),
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, path := range mustExists {
|
|
|
|
_, err := d.fs.Stat(path)
|
|
|
|
if err == nil {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if !os.IsNotExist(err) {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := d.fs.MkdirAll(path, os.ModeDir|os.ModePerm); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Close closes all opened files.
|
|
|
|
func (d *DotGit) Close() error {
|
|
|
|
var firstError error
|
|
|
|
if d.files != nil {
|
|
|
|
for _, f := range d.files {
|
|
|
|
err := f.Close()
|
|
|
|
if err != nil && firstError == nil {
|
|
|
|
firstError = err
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
d.files = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if firstError != nil {
|
|
|
|
return firstError
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ConfigWriter returns a file pointer for write to the config file
|
|
|
|
func (d *DotGit) ConfigWriter() (billy.File, error) {
|
|
|
|
return d.fs.Create(configPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Config returns a file pointer for read to the config file
|
|
|
|
func (d *DotGit) Config() (billy.File, error) {
|
|
|
|
return d.fs.Open(configPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
// IndexWriter returns a file pointer for write to the index file
|
|
|
|
func (d *DotGit) IndexWriter() (billy.File, error) {
|
|
|
|
return d.fs.Create(indexPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Index returns a file pointer for read to the index file
|
|
|
|
func (d *DotGit) Index() (billy.File, error) {
|
|
|
|
return d.fs.Open(indexPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ShallowWriter returns a file pointer for write to the shallow file
|
|
|
|
func (d *DotGit) ShallowWriter() (billy.File, error) {
|
|
|
|
return d.fs.Create(shallowPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Shallow returns a file pointer for read to the shallow file
|
|
|
|
func (d *DotGit) Shallow() (billy.File, error) {
|
|
|
|
f, err := d.fs.Open(shallowPath)
|
|
|
|
if err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return f, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewObjectPack return a writer for a new packfile, it saves the packfile to
|
|
|
|
// disk and also generates and save the index for the given packfile.
|
|
|
|
func (d *DotGit) NewObjectPack() (*PackWriter, error) {
|
|
|
|
d.cleanPackList()
|
|
|
|
return newPackWrite(d.fs)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ObjectPacks returns the list of availables packfiles
|
|
|
|
func (d *DotGit) ObjectPacks() ([]plumbing.Hash, error) {
|
|
|
|
if !d.options.ExclusiveAccess {
|
|
|
|
return d.objectPacks()
|
|
|
|
}
|
|
|
|
|
|
|
|
err := d.genPackList()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return d.packList, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) objectPacks() ([]plumbing.Hash, error) {
|
|
|
|
packDir := d.fs.Join(objectsPath, packPath)
|
|
|
|
files, err := d.fs.ReadDir(packDir)
|
|
|
|
if err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var packs []plumbing.Hash
|
|
|
|
for _, f := range files {
|
|
|
|
if !strings.HasSuffix(f.Name(), packExt) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
n := f.Name()
|
|
|
|
h := plumbing.NewHash(n[5 : len(n)-5]) //pack-(hash).pack
|
|
|
|
if h.IsZero() {
|
|
|
|
// Ignore files with badly-formatted names.
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
packs = append(packs, h)
|
|
|
|
}
|
|
|
|
|
|
|
|
return packs, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) objectPackPath(hash plumbing.Hash, extension string) string {
|
|
|
|
return d.fs.Join(objectsPath, packPath, fmt.Sprintf("pack-%s.%s", hash.String(), extension))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) objectPackOpen(hash plumbing.Hash, extension string) (billy.File, error) {
|
|
|
|
if d.files == nil {
|
|
|
|
d.files = make(map[string]billy.File)
|
|
|
|
}
|
|
|
|
|
|
|
|
err := d.hasPack(hash)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
path := d.objectPackPath(hash, extension)
|
|
|
|
f, ok := d.files[path]
|
|
|
|
if ok {
|
|
|
|
return f, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
pack, err := d.fs.Open(path)
|
|
|
|
if err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return nil, ErrPackfileNotFound
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if d.options.KeepDescriptors && extension == "pack" {
|
|
|
|
d.files[path] = pack
|
|
|
|
}
|
|
|
|
|
|
|
|
return pack, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ObjectPack returns a fs.File of the given packfile
|
|
|
|
func (d *DotGit) ObjectPack(hash plumbing.Hash) (billy.File, error) {
|
|
|
|
err := d.hasPack(hash)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return d.objectPackOpen(hash, `pack`)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ObjectPackIdx returns a fs.File of the index file for a given packfile
|
|
|
|
func (d *DotGit) ObjectPackIdx(hash plumbing.Hash) (billy.File, error) {
|
|
|
|
err := d.hasPack(hash)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return d.objectPackOpen(hash, `idx`)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) DeleteOldObjectPackAndIndex(hash plumbing.Hash, t time.Time) error {
|
|
|
|
d.cleanPackList()
|
|
|
|
|
|
|
|
path := d.objectPackPath(hash, `pack`)
|
|
|
|
if !t.IsZero() {
|
|
|
|
fi, err := d.fs.Stat(path)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
// too new, skip deletion.
|
|
|
|
if !fi.ModTime().Before(t) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
err := d.fs.Remove(path)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return d.fs.Remove(d.objectPackPath(hash, `idx`))
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewObject return a writer for a new object file.
|
|
|
|
func (d *DotGit) NewObject() (*ObjectWriter, error) {
|
|
|
|
d.cleanObjectList()
|
|
|
|
|
|
|
|
return newObjectWriter(d.fs)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Objects returns a slice with the hashes of objects found under the
|
|
|
|
// .git/objects/ directory.
|
|
|
|
func (d *DotGit) Objects() ([]plumbing.Hash, error) {
|
|
|
|
if d.options.ExclusiveAccess {
|
|
|
|
err := d.genObjectList()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return d.objectList, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var objects []plumbing.Hash
|
|
|
|
err := d.ForEachObjectHash(func(hash plumbing.Hash) error {
|
|
|
|
objects = append(objects, hash)
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return objects, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ForEachObjectHash iterates over the hashes of objects found under the
|
|
|
|
// .git/objects/ directory and executes the provided function.
|
|
|
|
func (d *DotGit) ForEachObjectHash(fun func(plumbing.Hash) error) error {
|
|
|
|
if !d.options.ExclusiveAccess {
|
|
|
|
return d.forEachObjectHash(fun)
|
|
|
|
}
|
|
|
|
|
|
|
|
err := d.genObjectList()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, h := range d.objectList {
|
|
|
|
err := fun(h)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) forEachObjectHash(fun func(plumbing.Hash) error) error {
|
|
|
|
files, err := d.fs.ReadDir(objectsPath)
|
|
|
|
if err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, f := range files {
|
|
|
|
if f.IsDir() && len(f.Name()) == 2 && isHex(f.Name()) {
|
|
|
|
base := f.Name()
|
|
|
|
d, err := d.fs.ReadDir(d.fs.Join(objectsPath, base))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, o := range d {
|
|
|
|
h := plumbing.NewHash(base + o.Name())
|
|
|
|
if h.IsZero() {
|
|
|
|
// Ignore files with badly-formatted names.
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
err = fun(h)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) cleanObjectList() {
|
|
|
|
d.objectMap = nil
|
|
|
|
d.objectList = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) genObjectList() error {
|
|
|
|
if d.objectMap != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
d.objectMap = make(map[plumbing.Hash]struct{})
|
|
|
|
return d.forEachObjectHash(func(h plumbing.Hash) error {
|
|
|
|
d.objectList = append(d.objectList, h)
|
|
|
|
d.objectMap[h] = struct{}{}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) hasObject(h plumbing.Hash) error {
|
|
|
|
if !d.options.ExclusiveAccess {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
err := d.genObjectList()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
_, ok := d.objectMap[h]
|
|
|
|
if !ok {
|
|
|
|
return plumbing.ErrObjectNotFound
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) cleanPackList() {
|
|
|
|
d.packMap = nil
|
|
|
|
d.packList = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) genPackList() error {
|
|
|
|
if d.packMap != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
op, err := d.objectPacks()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
d.packMap = make(map[plumbing.Hash]struct{})
|
|
|
|
d.packList = nil
|
|
|
|
|
|
|
|
for _, h := range op {
|
|
|
|
d.packList = append(d.packList, h)
|
|
|
|
d.packMap[h] = struct{}{}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) hasPack(h plumbing.Hash) error {
|
|
|
|
if !d.options.ExclusiveAccess {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
err := d.genPackList()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
_, ok := d.packMap[h]
|
|
|
|
if !ok {
|
|
|
|
return ErrPackfileNotFound
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) objectPath(h plumbing.Hash) string {
|
|
|
|
hash := h.String()
|
|
|
|
return d.fs.Join(objectsPath, hash[0:2], hash[2:40])
|
|
|
|
}
|
|
|
|
|
|
|
|
// incomingObjectPath is intended to add support for a git pre-receive hook
|
|
|
|
// to be written it adds support for go-git to find objects in an "incoming"
|
|
|
|
// directory, so that the library can be used to write a pre-receive hook
|
|
|
|
// that deals with the incoming objects.
|
|
|
|
//
|
|
|
|
// More on git hooks found here : https://git-scm.com/docs/githooks
|
|
|
|
// More on 'quarantine'/incoming directory here:
|
|
|
|
// https://git-scm.com/docs/git-receive-pack
|
|
|
|
func (d *DotGit) incomingObjectPath(h plumbing.Hash) string {
|
|
|
|
hString := h.String()
|
|
|
|
|
|
|
|
if d.incomingDirName == "" {
|
|
|
|
return d.fs.Join(objectsPath, hString[0:2], hString[2:40])
|
|
|
|
}
|
|
|
|
|
|
|
|
return d.fs.Join(objectsPath, d.incomingDirName, hString[0:2], hString[2:40])
|
|
|
|
}
|
|
|
|
|
|
|
|
// hasIncomingObjects searches for an incoming directory and keeps its name
|
|
|
|
// so it doesn't have to be found each time an object is accessed.
|
|
|
|
func (d *DotGit) hasIncomingObjects() bool {
|
|
|
|
if !d.incomingChecked {
|
|
|
|
directoryContents, err := d.fs.ReadDir(objectsPath)
|
|
|
|
if err == nil {
|
|
|
|
for _, file := range directoryContents {
|
|
|
|
if strings.HasPrefix(file.Name(), "incoming-") && file.IsDir() {
|
|
|
|
d.incomingDirName = file.Name()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
d.incomingChecked = true
|
|
|
|
}
|
|
|
|
|
|
|
|
return d.incomingDirName != ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// Object returns a fs.File pointing the object file, if exists
|
|
|
|
func (d *DotGit) Object(h plumbing.Hash) (billy.File, error) {
|
|
|
|
err := d.hasObject(h)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
obj1, err1 := d.fs.Open(d.objectPath(h))
|
|
|
|
if os.IsNotExist(err1) && d.hasIncomingObjects() {
|
|
|
|
obj2, err2 := d.fs.Open(d.incomingObjectPath(h))
|
|
|
|
if err2 != nil {
|
|
|
|
return obj1, err1
|
|
|
|
}
|
|
|
|
return obj2, err2
|
|
|
|
}
|
|
|
|
return obj1, err1
|
|
|
|
}
|
|
|
|
|
|
|
|
// ObjectStat returns a os.FileInfo pointing the object file, if exists
|
|
|
|
func (d *DotGit) ObjectStat(h plumbing.Hash) (os.FileInfo, error) {
|
|
|
|
err := d.hasObject(h)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
obj1, err1 := d.fs.Stat(d.objectPath(h))
|
|
|
|
if os.IsNotExist(err1) && d.hasIncomingObjects() {
|
|
|
|
obj2, err2 := d.fs.Stat(d.incomingObjectPath(h))
|
|
|
|
if err2 != nil {
|
|
|
|
return obj1, err1
|
|
|
|
}
|
|
|
|
return obj2, err2
|
|
|
|
}
|
|
|
|
return obj1, err1
|
|
|
|
}
|
|
|
|
|
|
|
|
// ObjectDelete removes the object file, if exists
|
|
|
|
func (d *DotGit) ObjectDelete(h plumbing.Hash) error {
|
|
|
|
d.cleanObjectList()
|
|
|
|
|
|
|
|
err1 := d.fs.Remove(d.objectPath(h))
|
|
|
|
if os.IsNotExist(err1) && d.hasIncomingObjects() {
|
|
|
|
err2 := d.fs.Remove(d.incomingObjectPath(h))
|
|
|
|
if err2 != nil {
|
|
|
|
return err1
|
|
|
|
}
|
|
|
|
return err2
|
|
|
|
}
|
|
|
|
return err1
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) readReferenceFrom(rd io.Reader, name string) (ref *plumbing.Reference, err error) {
|
|
|
|
b, err := stdioutil.ReadAll(rd)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
line := strings.TrimSpace(string(b))
|
|
|
|
return plumbing.NewReferenceFromStrings(name, line), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) checkReferenceAndTruncate(f billy.File, old *plumbing.Reference) error {
|
|
|
|
if old == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
ref, err := d.readReferenceFrom(f, old.Name().String())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if ref.Hash() != old.Hash() {
|
2019-04-17 02:04:23 +00:00
|
|
|
return storage.ErrReferenceHasChanged
|
2018-11-27 21:52:20 +00:00
|
|
|
}
|
|
|
|
_, err = f.Seek(0, io.SeekStart)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return f.Truncate(0)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) SetRef(r, old *plumbing.Reference) error {
|
|
|
|
var content string
|
|
|
|
switch r.Type() {
|
|
|
|
case plumbing.SymbolicReference:
|
|
|
|
content = fmt.Sprintf("ref: %s\n", r.Target())
|
|
|
|
case plumbing.HashReference:
|
|
|
|
content = fmt.Sprintln(r.Hash().String())
|
|
|
|
}
|
|
|
|
|
|
|
|
fileName := r.Name().String()
|
|
|
|
|
|
|
|
return d.setRef(fileName, content, old)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Refs scans the git directory collecting references, which it returns.
|
|
|
|
// Symbolic references are resolved and included in the output.
|
|
|
|
func (d *DotGit) Refs() ([]*plumbing.Reference, error) {
|
|
|
|
var refs []*plumbing.Reference
|
|
|
|
var seen = make(map[plumbing.ReferenceName]bool)
|
|
|
|
if err := d.addRefsFromRefDir(&refs, seen); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := d.addRefsFromPackedRefs(&refs, seen); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := d.addRefFromHEAD(&refs); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return refs, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ref returns the reference for a given reference name.
|
|
|
|
func (d *DotGit) Ref(name plumbing.ReferenceName) (*plumbing.Reference, error) {
|
|
|
|
ref, err := d.readReferenceFile(".", name.String())
|
|
|
|
if err == nil {
|
|
|
|
return ref, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return d.packedRef(name)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) findPackedRefsInFile(f billy.File) ([]*plumbing.Reference, error) {
|
|
|
|
s := bufio.NewScanner(f)
|
|
|
|
var refs []*plumbing.Reference
|
|
|
|
for s.Scan() {
|
|
|
|
ref, err := d.processLine(s.Text())
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if ref != nil {
|
|
|
|
refs = append(refs, ref)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return refs, s.Err()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) findPackedRefs() (r []*plumbing.Reference, err error) {
|
|
|
|
f, err := d.fs.Open(packedRefsPath)
|
|
|
|
if err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
defer ioutil.CheckClose(f, &err)
|
|
|
|
return d.findPackedRefsInFile(f)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) packedRef(name plumbing.ReferenceName) (*plumbing.Reference, error) {
|
|
|
|
refs, err := d.findPackedRefs()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, ref := range refs {
|
|
|
|
if ref.Name() == name {
|
|
|
|
return ref, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, plumbing.ErrReferenceNotFound
|
|
|
|
}
|
|
|
|
|
|
|
|
// RemoveRef removes a reference by name.
|
|
|
|
func (d *DotGit) RemoveRef(name plumbing.ReferenceName) error {
|
|
|
|
path := d.fs.Join(".", name.String())
|
|
|
|
_, err := d.fs.Stat(path)
|
|
|
|
if err == nil {
|
|
|
|
err = d.fs.Remove(path)
|
|
|
|
// Drop down to remove it from the packed refs file, too.
|
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil && !os.IsNotExist(err) {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return d.rewritePackedRefsWithoutRef(name)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) addRefsFromPackedRefs(refs *[]*plumbing.Reference, seen map[plumbing.ReferenceName]bool) (err error) {
|
|
|
|
packedRefs, err := d.findPackedRefs()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, ref := range packedRefs {
|
|
|
|
if !seen[ref.Name()] {
|
|
|
|
*refs = append(*refs, ref)
|
|
|
|
seen[ref.Name()] = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) addRefsFromPackedRefsFile(refs *[]*plumbing.Reference, f billy.File, seen map[plumbing.ReferenceName]bool) (err error) {
|
|
|
|
packedRefs, err := d.findPackedRefsInFile(f)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, ref := range packedRefs {
|
|
|
|
if !seen[ref.Name()] {
|
|
|
|
*refs = append(*refs, ref)
|
|
|
|
seen[ref.Name()] = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) openAndLockPackedRefs(doCreate bool) (
|
|
|
|
pr billy.File, err error) {
|
|
|
|
var f billy.File
|
|
|
|
defer func() {
|
|
|
|
if err != nil && f != nil {
|
|
|
|
ioutil.CheckClose(f, &err)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
// File mode is retrieved from a constant defined in the target specific
|
|
|
|
// files (dotgit_rewrite_packed_refs_*). Some modes are not available
|
|
|
|
// in all filesystems.
|
|
|
|
openFlags := d.openAndLockPackedRefsMode()
|
|
|
|
if doCreate {
|
|
|
|
openFlags |= os.O_CREATE
|
|
|
|
}
|
|
|
|
|
|
|
|
// Keep trying to open and lock the file until we're sure the file
|
|
|
|
// didn't change between the open and the lock.
|
|
|
|
for {
|
|
|
|
f, err = d.fs.OpenFile(packedRefsPath, openFlags, 0600)
|
|
|
|
if err != nil {
|
|
|
|
if os.IsNotExist(err) && !doCreate {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
fi, err := d.fs.Stat(packedRefsPath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
mtime := fi.ModTime()
|
|
|
|
|
|
|
|
err = f.Lock()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
fi, err = d.fs.Stat(packedRefsPath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if mtime.Equal(fi.ModTime()) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
// The file has changed since we opened it. Close and retry.
|
|
|
|
err = f.Close()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return f, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) rewritePackedRefsWithoutRef(name plumbing.ReferenceName) (err error) {
|
|
|
|
pr, err := d.openAndLockPackedRefs(false)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if pr == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
defer ioutil.CheckClose(pr, &err)
|
|
|
|
|
|
|
|
// Creating the temp file in the same directory as the target file
|
|
|
|
// improves our chances for rename operation to be atomic.
|
|
|
|
tmp, err := d.fs.TempFile("", tmpPackedRefsPrefix)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
tmpName := tmp.Name()
|
|
|
|
defer func() {
|
|
|
|
ioutil.CheckClose(tmp, &err)
|
|
|
|
_ = d.fs.Remove(tmpName) // don't check err, we might have renamed it
|
|
|
|
}()
|
|
|
|
|
|
|
|
s := bufio.NewScanner(pr)
|
|
|
|
found := false
|
|
|
|
for s.Scan() {
|
|
|
|
line := s.Text()
|
|
|
|
ref, err := d.processLine(line)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if ref != nil && ref.Name() == name {
|
|
|
|
found = true
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if _, err := fmt.Fprintln(tmp, line); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := s.Err(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if !found {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return d.rewritePackedRefsWhileLocked(tmp, pr)
|
|
|
|
}
|
|
|
|
|
|
|
|
// process lines from a packed-refs file
|
|
|
|
func (d *DotGit) processLine(line string) (*plumbing.Reference, error) {
|
|
|
|
if len(line) == 0 {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
switch line[0] {
|
|
|
|
case '#': // comment - ignore
|
|
|
|
return nil, nil
|
|
|
|
case '^': // annotated tag commit of the previous line - ignore
|
|
|
|
return nil, nil
|
|
|
|
default:
|
|
|
|
ws := strings.Split(line, " ") // hash then ref
|
|
|
|
if len(ws) != 2 {
|
|
|
|
return nil, ErrPackedRefsBadFormat
|
|
|
|
}
|
|
|
|
|
|
|
|
return plumbing.NewReferenceFromStrings(ws[1], ws[0]), nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) addRefsFromRefDir(refs *[]*plumbing.Reference, seen map[plumbing.ReferenceName]bool) error {
|
|
|
|
return d.walkReferencesTree(refs, []string{refsPath}, seen)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) walkReferencesTree(refs *[]*plumbing.Reference, relPath []string, seen map[plumbing.ReferenceName]bool) error {
|
|
|
|
files, err := d.fs.ReadDir(d.fs.Join(relPath...))
|
|
|
|
if err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, f := range files {
|
|
|
|
newRelPath := append(append([]string(nil), relPath...), f.Name())
|
|
|
|
if f.IsDir() {
|
|
|
|
if err = d.walkReferencesTree(refs, newRelPath, seen); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
ref, err := d.readReferenceFile(".", strings.Join(newRelPath, "/"))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if ref != nil && !seen[ref.Name()] {
|
|
|
|
*refs = append(*refs, ref)
|
|
|
|
seen[ref.Name()] = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) addRefFromHEAD(refs *[]*plumbing.Reference) error {
|
|
|
|
ref, err := d.readReferenceFile(".", "HEAD")
|
|
|
|
if err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
*refs = append(*refs, ref)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) readReferenceFile(path, name string) (ref *plumbing.Reference, err error) {
|
|
|
|
path = d.fs.Join(path, d.fs.Join(strings.Split(name, "/")...))
|
|
|
|
f, err := d.fs.Open(path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer ioutil.CheckClose(f, &err)
|
|
|
|
|
|
|
|
return d.readReferenceFrom(f, name)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *DotGit) CountLooseRefs() (int, error) {
|
|
|
|
var refs []*plumbing.Reference
|
|
|
|
var seen = make(map[plumbing.ReferenceName]bool)
|
|
|
|
if err := d.addRefsFromRefDir(&refs, seen); err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return len(refs), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// PackRefs packs all loose refs into the packed-refs file.
|
|
|
|
//
|
|
|
|
// This implementation only works under the assumption that the view
|
|
|
|
// of the file system won't be updated during this operation. This
|
|
|
|
// strategy would not work on a general file system though, without
|
|
|
|
// locking each loose reference and checking it again before deleting
|
|
|
|
// the file, because otherwise an updated reference could sneak in and
|
|
|
|
// then be deleted by the packed-refs process. Alternatively, every
|
|
|
|
// ref update could also lock packed-refs, so only one lock is
|
|
|
|
// required during ref-packing. But that would worsen performance in
|
|
|
|
// the common case.
|
|
|
|
//
|
|
|
|
// TODO: add an "all" boolean like the `git pack-refs --all` flag.
|
|
|
|
// When `all` is false, it would only pack refs that have already been
|
|
|
|
// packed, plus all tags.
|
|
|
|
func (d *DotGit) PackRefs() (err error) {
|
|
|
|
// Lock packed-refs, and create it if it doesn't exist yet.
|
|
|
|
f, err := d.openAndLockPackedRefs(true)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer ioutil.CheckClose(f, &err)
|
|
|
|
|
|
|
|
// Gather all refs using addRefsFromRefDir and addRefsFromPackedRefs.
|
|
|
|
var refs []*plumbing.Reference
|
|
|
|
seen := make(map[plumbing.ReferenceName]bool)
|
|
|
|
if err = d.addRefsFromRefDir(&refs, seen); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if len(refs) == 0 {
|
|
|
|
// Nothing to do!
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
numLooseRefs := len(refs)
|
|
|
|
if err = d.addRefsFromPackedRefsFile(&refs, f, seen); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write them all to a new temp packed-refs file.
|
|
|
|
tmp, err := d.fs.TempFile("", tmpPackedRefsPrefix)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
tmpName := tmp.Name()
|
|
|
|
defer func() {
|
|
|
|
ioutil.CheckClose(tmp, &err)
|
|
|
|
_ = d.fs.Remove(tmpName) // don't check err, we might have renamed it
|
|
|
|
}()
|
|
|
|
|
|
|
|
w := bufio.NewWriter(tmp)
|
|
|
|
for _, ref := range refs {
|
|
|
|
_, err = w.WriteString(ref.String() + "\n")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
err = w.Flush()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Rename the temp packed-refs file.
|
|
|
|
err = d.rewritePackedRefsWhileLocked(tmp, f)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Delete all the loose refs, while still holding the packed-refs
|
|
|
|
// lock.
|
|
|
|
for _, ref := range refs[:numLooseRefs] {
|
|
|
|
path := d.fs.Join(".", ref.Name().String())
|
|
|
|
err = d.fs.Remove(path)
|
|
|
|
if err != nil && !os.IsNotExist(err) {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Module return a billy.Filesystem pointing to the module folder
|
|
|
|
func (d *DotGit) Module(name string) (billy.Filesystem, error) {
|
|
|
|
return d.fs.Chroot(d.fs.Join(modulePath, name))
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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")
|
|
|
|
f, err := d.fs.Open(altpath)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
var alternates []*DotGit
|
|
|
|
|
|
|
|
// Read alternate paths line-by-line and create DotGit objects.
|
|
|
|
scanner := bufio.NewScanner(f)
|
|
|
|
for scanner.Scan() {
|
|
|
|
path := scanner.Text()
|
|
|
|
if !filepath.IsAbs(path) {
|
|
|
|
// For relative paths, we can perform an internal conversion to
|
|
|
|
// slash so that they work cross-platform.
|
|
|
|
slashPath := filepath.ToSlash(path)
|
|
|
|
// If the path is not absolute, it must be relative to object
|
|
|
|
// database (.git/objects/info).
|
|
|
|
// https://www.kernel.org/pub/software/scm/git/docs/gitrepository-layout.html
|
|
|
|
// Hence, derive a path relative to DotGit's root.
|
|
|
|
// "../../../reponame/.git/" -> "../../reponame/.git"
|
|
|
|
// Remove the first ../
|
|
|
|
relpath := filepath.Join(strings.Split(slashPath, "/")[1:]...)
|
|
|
|
normalPath := filepath.FromSlash(relpath)
|
|
|
|
path = filepath.Join(d.fs.Root(), normalPath)
|
|
|
|
}
|
|
|
|
fs := osfs.New(filepath.Dir(path))
|
|
|
|
alternates = append(alternates, New(fs))
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = scanner.Err(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return alternates, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fs returns the underlying filesystem of the DotGit folder.
|
|
|
|
func (d *DotGit) Fs() billy.Filesystem {
|
|
|
|
return d.fs
|
|
|
|
}
|
|
|
|
|
|
|
|
func isHex(s string) bool {
|
|
|
|
for _, b := range []byte(s) {
|
|
|
|
if isNum(b) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if isHexAlpha(b) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
func isNum(b byte) bool {
|
|
|
|
return b >= '0' && b <= '9'
|
|
|
|
}
|
|
|
|
|
|
|
|
func isHexAlpha(b byte) bool {
|
|
|
|
return b >= 'a' && b <= 'f' || b >= 'A' && b <= 'F'
|
|
|
|
}
|