Merge pull request #2340 from git-lfs/githistory-ref-updater

git/githistory: introduce `*refUpdater` to update references
This commit is contained in:
Taylor Blau 2017-06-21 18:55:26 -06:00 committed by GitHub
commit cdd17d024e
6 changed files with 211 additions and 0 deletions

@ -70,6 +70,8 @@ func rewriteOptions(args []string, opts *githistory.RewriteOptions) (*githistory
Include: include,
Exclude: exclude,
UpdateRefs: opts.UpdateRefs,
BlobFn: opts.BlobFn,
TreeCallbackFn: opts.TreeCallbackFn,
}, nil

@ -6,6 +6,7 @@ import (
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
@ -129,6 +130,20 @@ func AssertCommitTree(t *testing.T, db *odb.ObjectDatabase, sha, tree string) {
assert.Equal(t, decoded, commit.TreeID, "git/odb: expected tree ID: %s (got: %x)", tree, commit.TreeID)
}
// AssertRef asserts that a given refname points at the expected commit.
func AssertRef(t *testing.T, db *odb.ObjectDatabase, ref string, expected []byte) {
root, ok := db.Root()
assert.True(t, ok, "git/odb: expected *odb.ObjectDatabase to have Root()")
cmd := exec.Command("git", "rev-parse", ref)
cmd.Dir = root
out, err := cmd.Output()
assert.Nil(t, err)
assert.Equal(t, hex.EncodeToString(expected), strings.TrimSpace(string(out)))
}
// HexDecode decodes the given ASCII hex-encoded string into []byte's, or fails
// the test immediately if the given "sha" wasn't a valid hex-encoded sequence.
func HexDecode(t *testing.T, sha string) []byte {

@ -0,0 +1,59 @@
package githistory
import (
"encoding/hex"
"fmt"
"github.com/git-lfs/git-lfs/errors"
"github.com/git-lfs/git-lfs/git"
"github.com/git-lfs/git-lfs/git/githistory/log"
)
// refUpdater is a type responsible for moving references from one point in the
// Git object graph to another.
type refUpdater struct {
// CacheFn is a function that returns the SHA1 transformation from an
// original hash to a new one. It specifies a "bool" return value
// signaling whether or not that given "old" SHA1 was migrated.
CacheFn func(old []byte) ([]byte, bool)
// Logger logs the progress of reference updating.
Logger *log.Logger
// Refs is a set of *git.Ref's to migrate.
Refs []*git.Ref
// Root is the given directory on disk in which the repository is
// located.
Root string
}
// UpdateRefs performs the reference update(s) from existing locations (see:
// Refs) to their respective new locations in the graph (see CacheFn).
//
// It creates reflog entries as well as stderr log entries as it progresses
// through the reference updates.
//
// It returns any error encountered, or nil if the reference update(s) was/were
// successful.
func (r *refUpdater) UpdateRefs() error {
list := r.Logger.List("migrate: Updating refs")
defer list.Complete()
for _, ref := range r.Refs {
sha1, err := hex.DecodeString(ref.Sha)
if err != nil {
return errors.Wrapf(err, "could not decode: %q", ref.Sha)
}
to, ok := r.CacheFn(sha1)
if !ok {
continue
}
if err := git.UpdateRefIn(r.Root, ref, to, ""); err != nil {
return err
}
list.Entry(fmt.Sprintf("%s\t%s -> %x", ref, ref.Sha, to))
}
return nil
}

@ -0,0 +1,66 @@
package githistory
import (
"testing"
"github.com/git-lfs/git-lfs/git"
"github.com/stretchr/testify/assert"
)
func TestRefUpdaterMovesRefs(t *testing.T) {
db := DatabaseFromFixture(t, "linear-history-with-tags.git")
root, _ := db.Root()
AssertRef(t, db,
"refs/tags/middle", HexDecode(t, "228afe30855933151f7a88e70d9d88314fd2f191"))
updater := &refUpdater{
CacheFn: func(old []byte) ([]byte, bool) {
return HexDecode(t, "d941e4756add6b06f5bee766fcf669f55419f13f"), true
},
Refs: []*git.Ref{
{
Name: "middle",
Sha: "228afe30855933151f7a88e70d9d88314fd2f191",
Type: git.RefTypeLocalTag,
},
},
Root: root,
}
err := updater.UpdateRefs()
assert.NoError(t, err)
AssertRef(t, db,
"refs/tags/middle", HexDecode(t, "d941e4756add6b06f5bee766fcf669f55419f13f"))
}
func TestRefUpdaterIgnoresUnovedRefs(t *testing.T) {
db := DatabaseFromFixture(t, "linear-history-with-tags.git")
root, _ := db.Root()
AssertRef(t, db,
"refs/tags/middle", HexDecode(t, "228afe30855933151f7a88e70d9d88314fd2f191"))
updater := &refUpdater{
CacheFn: func(old []byte) ([]byte, bool) {
return nil, false
},
Refs: []*git.Ref{
{
Name: "middle",
Sha: "228afe30855933151f7a88e70d9d88314fd2f191",
Type: git.RefTypeLocalTag,
},
},
Root: root,
}
err := updater.UpdateRefs()
assert.NoError(t, err)
AssertRef(t, db,
"refs/tags/middle", HexDecode(t, "228afe30855933151f7a88e70d9d88314fd2f191"))
}

@ -8,6 +8,7 @@ import (
"path/filepath"
"sync"
"github.com/git-lfs/git-lfs/errors"
"github.com/git-lfs/git-lfs/filepathfilter"
"github.com/git-lfs/git-lfs/git"
"github.com/git-lfs/git-lfs/git/githistory/log"
@ -46,6 +47,11 @@ type RewriteOptions struct {
// will be excluded.
Exclude []string
// UpdateRefs specifies whether the Rewriter should move refs from the
// original graph onto the migrated one. If true, the refs will be
// moved, and a reflog entry will be created.
UpdateRefs bool
// BlobFn specifies a function to rewrite blobs.
//
// It is called once per unique, unchanged path. That is to say, if
@ -233,6 +239,26 @@ func (r *Rewriter) Rewrite(opt *RewriteOptions) ([]byte, error) {
tip = rewrittenCommit
}
if opt.UpdateRefs {
refs, err := r.refsToMigrate()
if err != nil {
return nil, errors.Wrap(err, "could not find refs to update")
}
root, _ := r.db.Root()
updater := &refUpdater{
CacheFn: r.uncacheCommit,
Logger: r.l,
Refs: refs,
Root: root,
}
if err := updater.UpdateRefs(); err != nil {
return nil, errors.Wrap(err, "could not update refs")
}
}
r.l.Close()
return tip, err
@ -365,6 +391,15 @@ func (r *Rewriter) commitsToMigrate(opt *RewriteOptions) ([][]byte, error) {
return commits, nil
}
// refsToMigrate returns a list of references to migrate, or an error if loading
// those references failed.
func (r *Rewriter) refsToMigrate() ([]*git.Ref, error) {
if root, ok := r.db.Root(); ok {
return git.AllRefsIn(root)
}
return git.AllRefs()
}
// scannerOpts returns a *git.ScanRefsOptions instance to be given to the
// *git.RevListScanner.
//

@ -300,6 +300,40 @@ func TestHistoryRewriterUseOriginalParentsForPartialMigration(t *testing.T) {
AssertCommitParent(t, db, hex.EncodeToString(tip), expectedParent)
}
func TestHistoryRewriterUpdatesRefs(t *testing.T) {
db := DatabaseFromFixture(t, "linear-history.git")
r := NewRewriter(db)
AssertRef(t, db,
"refs/heads/master", HexDecode(t, "e669b63f829bfb0b91fc52a5bcea53dd7977a0ee"))
tip, err := r.Rewrite(&RewriteOptions{
Include: []string{"refs/heads/master"},
UpdateRefs: true,
BlobFn: func(path string, b *odb.Blob) (*odb.Blob, error) {
suffix := strings.NewReader("_suffix")
return &odb.Blob{
Contents: io.MultiReader(b.Contents, suffix),
Size: b.Size + int64(suffix.Len()),
}, nil
},
})
assert.Nil(t, err)
c1 := hex.EncodeToString(tip)
c2 := "66561fe3ae68651658e18e48053dcfe66a2e9da1"
c3 := "8268d8486c48024a871fa42fc487dbeabd6e3d86"
AssertRef(t, db, "refs/heads/master", tip)
AssertCommitParent(t, db, c1, c2)
AssertCommitParent(t, db, c2, c3)
}
func TestHistoryRewriterReturnsFilter(t *testing.T) {
f := filepathfilter.New([]string{"a"}, []string{"b"})
r := NewRewriter(nil, WithFilter(f))