From b3265379ddde9ef6887d8aaac6cb4c183f49de3a Mon Sep 17 00:00:00 2001 From: John Cai Date: Wed, 11 Mar 2020 15:07:54 -0700 Subject: POC repository transactions --- internal/git/transaction.go | 164 +++++++++++++++++++++++++++++++++++++++ internal/git/transaction_test.go | 49 ++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 internal/git/transaction.go create mode 100644 internal/git/transaction_test.go diff --git a/internal/git/transaction.go b/internal/git/transaction.go new file mode 100644 index 000000000..c98b96403 --- /dev/null +++ b/internal/git/transaction.go @@ -0,0 +1,164 @@ +package git + +import ( + "bytes" + "context" + "encoding/gob" + "errors" + "fmt" + "io" + "io/ioutil" + "os/exec" + + "gitlab.com/gitlab-org/gitaly/internal/helper" + + "gitlab.com/gitlab-org/gitaly/internal/git/repository" + "gitlab.com/gitlab-org/gitaly/internal/helper/text" + "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb" +) + +/* +func init() { + gob.Register(gitalypb.Repository{}) +} + +*/ + +type Transaction struct { + Args []string + repo *gitalypb.Repository +} + +func NewTransaction(repo *gitalypb.Repository, args []string) *Transaction { + return &Transaction{ + Args: args, + repo: repo, + } +} + +func GetCurrentTransactionID(ctx context.Context, repo repository.GitRepo) (string, error) { + getTxID, err := SafeStdinCmd(ctx, repo, nil, SubCmd{ + Name: "rev-parse", + Args: []string{"refs/tx/transaction"}, + }) + if err != nil { + return "", err + } + + txID, err := ioutil.ReadAll(getTxID) + if err != nil { + return "", err + } + + if err = getTxID.Wait(); err != nil { + return "", err + } + + return text.ChompBytes(txID), nil +} + +func GetCurrentTransaction(repo *gitalypb.Repository, transactionID string) (*Transaction, error) { + repoPath, err := helper.GetRepoPath(repo) + if err != nil { + return nil, err + } + + var stdout bytes.Buffer + c := exec.Command("git", "-C", repoPath, "cat-file", "-p", transactionID) + c.Stdout = &stdout + + if err := c.Run(); err != nil { + fmt.Printf("\n\nwhoa %v\n\n", err) + return nil, err + } + + var transaction Transaction + if err := gob.NewDecoder(&stdout).Decode(&transaction); err != nil { + return nil, err + } + + transaction.repo = repo + + return &transaction, nil +} + +func (t *Transaction) Begin(ctx context.Context) error { + // TODO: issue here of creating blobs that may never get used + // we don't want to clutter git's object database with these, even though they should get cleaned up during + // gc + repoPath, err := helper.GetRepoPath(t.repo) + if err != nil { + return err + } + + r, w := io.Pipe() + + var stdout, stderr bytes.Buffer + + c := exec.Command("git", "-C", repoPath, "hash-object", "-w", "--stdin") + c.Stdout = &stdout + c.Stderr = &stderr + c.Stdin = r + + if err := c.Start(); err != nil { + return err + } + + if err = gob.NewEncoder(w).Encode(t); err != nil { + return err + } + w.Close() + + if err := c.Wait(); err != nil { + return err + } + r.Close() + + blobID := text.ChompBytes(stdout.Bytes()) + + lockCmd, err := SafeCmd(ctx, t.repo, nil, SubCmd{ + Name: "update-ref", + Args: []string{"refs/tx/transaction", blobID, "0000000000000000000000000000000000000000"}, + }) + if err != nil { + return err + } + + if err = lockCmd.Wait(); err != nil { + return err + } + + return nil +} + +func (t *Transaction) Commit(ctx context.Context, transactionID string) error { + name := t.Args[0] + var args []string + + if len(t.Args) > 1 { + args = t.Args[1:] + } + + txID, err := GetCurrentTransactionID(ctx, t.repo) + if err != nil { + return err + } + + if txID != transactionID { + return errors.New("you are not allowed to commit this transaction") + } + + c := exec.Command(name, args...) + if err := c.Run(); err != nil { + return err + } + + repoPath, err := helper.GetRepoPath(t.repo) + if err != nil { + return err + } + + c = exec.Command("git", "-C", repoPath, "update-ref", "-d", "refs/tx/transaction") + + return c.Run() +} diff --git a/internal/git/transaction_test.go b/internal/git/transaction_test.go new file mode 100644 index 000000000..792c1b00c --- /dev/null +++ b/internal/git/transaction_test.go @@ -0,0 +1,49 @@ +package git + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/internal/helper/text" + "gitlab.com/gitlab-org/gitaly/internal/testhelper" +) + +func TestTransaction(t *testing.T) { + ctx, cancel := testhelper.Context() + defer cancel() + + repo, testRepoPath, cleanup := testhelper.NewTestRepo(t) + defer cleanup() + + refName, err := text.RandomHex(10) + require.NoError(t, err) + + require.NoError(t, err) + + transaction := NewTransaction(repo, []string{"git", "-C", testRepoPath, "update-ref", fmt.Sprintf("refs/heads/%s", refName), "master"}) + require.NoError(t, transaction.Begin(ctx)) + + transactionID, err := GetCurrentTransactionID(ctx, repo) + require.NoError(t, err) + + tx, err := GetCurrentTransaction(repo, transactionID) + require.NoError(t, err) + require.Equal(t, transaction, tx) + + // try to create a second transaction + refName, err = text.RandomHex(10) + require.NoError(t, err) + + anotherTransaction := NewTransaction(repo, []string{"git", "-C", testRepoPath, "update-ref", fmt.Sprintf("refs/heads/%s", refName), "master"}) + require.Error(t, anotherTransaction.Begin(ctx)) + + // try to commit with an incorrect transaction id + require.Error(t, tx.Commit(ctx, "67d4350058a6f76a8a3d4133aaaaf96bf8a5698b")) + + // commit the transaction + require.NoError(t, tx.Commit(ctx, transactionID)) + + // now another transaction can begin + require.NoError(t, anotherTransaction.Begin(ctx)) +} -- cgit v1.2.3