From 28dc6b77307a858598ea0a64027c8d31cf3b39dd Mon Sep 17 00:00:00 2001 From: michael boulton <61595820+mbfr@users.noreply.github.com> Date: Tue, 18 Aug 2020 17:25:31 +0100 Subject: [PATCH] Add support for creating signed commits (#626) (cherry picked from commit 7d4453198b55ecc2d9e09b64352edecb5db8b6ef) This is only a partial cherry-pick, since signing commits during a rebase is not supported in v0.27. --- .github/workflows/ci.yml | 2 +- commit.go | 63 ++++++++++++++++++++++++++++++++++++++++ git_test.go | 31 ++++++++++++++++++++ rebase.go | 4 ++- rebase_test.go | 55 +++++++++++++++++++++++++---------- 5 files changed, 138 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88e6c57..42636bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: run: | git submodule update --init make build-libgit2-static - go get -tags static github.com/${{ github.repository }}/... + go get -tags static -t github.com/${{ github.repository }}/... go build -tags static github.com/${{ github.repository }}/... - name: Test env: diff --git a/commit.go b/commit.go index 6fdfacf..10bf96a 100644 --- a/commit.go +++ b/commit.go @@ -40,6 +40,69 @@ func (c *Commit) RawMessage() string { return ret } +// RawHeader gets the full raw text of the commit header. +func (c *Commit) RawHeader() string { + ret := C.GoString(C.git_commit_raw_header(c.cast_ptr)) + runtime.KeepAlive(c) + return ret +} + +// ContentToSign returns the content that will be passed to a signing function for this commit +func (c *Commit) ContentToSign() string { + return c.RawHeader() + "\n" + c.RawMessage() +} + +// CommitSigningCallback defines a function type that takes some data to sign and returns (signature, signature_field, error) +type CommitSigningCallback func(string) (signature, signatureField string, err error) + +// WithSignatureUsing creates a new signed commit from this one using the given signing callback +func (c *Commit) WithSignatureUsing(f CommitSigningCallback) (*Oid, error) { + signature, signatureField, err := f(c.ContentToSign()) + if err != nil { + return nil, err + } + + return c.WithSignature(signature, signatureField) +} + +// WithSignature creates a new signed commit from the given signature and signature field +func (c *Commit) WithSignature(signature string, signatureField string) (*Oid, error) { + totalCommit := c.ContentToSign() + + oid := new(Oid) + + var csf *C.char = nil + if signatureField != "" { + csf = C.CString(signatureField) + defer C.free(unsafe.Pointer(csf)) + } + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + cTotalCommit := C.CString(totalCommit) + cSignature := C.CString(signature) + defer C.free(unsafe.Pointer(cTotalCommit)) + defer C.free(unsafe.Pointer(cSignature)) + + ret := C.git_commit_create_with_signature( + oid.toC(), + c.Owner().ptr, + cTotalCommit, + cSignature, + csf, + ) + + runtime.KeepAlive(c) + runtime.KeepAlive(oid) + + if ret < 0 { + return nil, MakeGitError(ret) + } + + return oid, nil +} + func (c *Commit) ExtractSignature() (string, string, error) { var c_signed C.git_buf diff --git a/git_test.go b/git_test.go index 807dcc2..91ade73 100644 --- a/git_test.go +++ b/git_test.go @@ -45,7 +45,16 @@ func createBareTestRepo(t *testing.T) *Repository { return repo } +// commitOpts contains any extra options for creating commits in the seed repo +type commitOpts struct { + CommitSigningCallback +} + func seedTestRepo(t *testing.T, repo *Repository) (*Oid, *Oid) { + return seedTestRepoOpt(t, repo, commitOpts{}) +} + +func seedTestRepoOpt(t *testing.T, repo *Repository, opts commitOpts) (*Oid, *Oid) { loc, err := time.LoadLocation("Europe/Berlin") checkFatal(t, err) sig := &Signature{ @@ -69,6 +78,28 @@ func seedTestRepo(t *testing.T, repo *Repository) (*Oid, *Oid) { commitId, err := repo.CreateCommit("HEAD", sig, sig, message, tree) checkFatal(t, err) + if opts.CommitSigningCallback != nil { + commit, err := repo.LookupCommit(commitId) + checkFatal(t, err) + + signature, signatureField, err := opts.CommitSigningCallback(commit.ContentToSign()) + checkFatal(t, err) + + oid, err := commit.WithSignature(signature, signatureField) + checkFatal(t, err) + newCommit, err := repo.LookupCommit(oid) + checkFatal(t, err) + head, err := repo.Head() + checkFatal(t, err) + _, err = repo.References.Create( + head.Name(), + newCommit.Id(), + true, + "repoint to signed commit", + ) + checkFatal(t, err) + } + return commitId, treeId } diff --git a/rebase.go b/rebase.go index d29e183..6e67669 100644 --- a/rebase.go +++ b/rebase.go @@ -108,7 +108,7 @@ func (ro *RebaseOptions) toC() *C.git_rebase_options { if ro == nil { return nil } - return &C.git_rebase_options{ + opts := &C.git_rebase_options{ version: C.uint(ro.Version), quiet: C.int(ro.Quiet), inmemory: C.int(ro.InMemory), @@ -116,6 +116,8 @@ func (ro *RebaseOptions) toC() *C.git_rebase_options { merge_options: *ro.MergeOptions.toC(), checkout_options: *ro.CheckoutOptions.toC(), } + + return opts } func mapEmptyStringToNull(ref string) *C.char { diff --git a/rebase_test.go b/rebase_test.go index ef4f920..c7deef6 100644 --- a/rebase_test.go +++ b/rebase_test.go @@ -33,12 +33,12 @@ func TestRebaseAbort(t *testing.T) { seedTestRepo(t, repo) // Setup a repo with 2 branches and a different tree - err := setupRepoForRebase(repo, masterCommit, branchName) + err := setupRepoForRebase(repo, masterCommit, branchName, commitOpts{}) checkFatal(t, err) // Create several commits in emile for _, commit := range emileCommits { - _, err = commitSomething(repo, commit, commit) + _, err = commitSomething(repo, commit, commit, commitOpts{}) checkFatal(t, err) } @@ -48,7 +48,7 @@ func TestRebaseAbort(t *testing.T) { assertStringList(t, expectedHistory, actualHistory) // Rebase onto master - rebase, err := performRebaseOnto(repo, "master") + rebase, err := performRebaseOnto(repo, "master", nil) checkFatal(t, err) defer rebase.Free() @@ -94,17 +94,17 @@ func TestRebaseNoConflicts(t *testing.T) { } // Setup a repo with 2 branches and a different tree - err = setupRepoForRebase(repo, masterCommit, branchName) + err = setupRepoForRebase(repo, masterCommit, branchName, commitOpts{}) checkFatal(t, err) // Create several commits in emile for _, commit := range emileCommits { - _, err = commitSomething(repo, commit, commit) + _, err = commitSomething(repo, commit, commit, commitOpts{}) checkFatal(t, err) } // Rebase onto master - rebase, err := performRebaseOnto(repo, "master") + rebase, err := performRebaseOnto(repo, "master", nil) checkFatal(t, err) defer rebase.Free() @@ -130,11 +130,10 @@ func TestRebaseNoConflicts(t *testing.T) { actualHistory, err := commitMsgsList(repo) checkFatal(t, err) assertStringList(t, expectedHistory, actualHistory) - } // Utils -func setupRepoForRebase(repo *Repository, masterCommit, branchName string) error { +func setupRepoForRebase(repo *Repository, masterCommit, branchName string, opts commitOpts) error { // Create a new branch from master err := createBranch(repo, branchName) if err != nil { @@ -142,7 +141,7 @@ func setupRepoForRebase(repo *Repository, masterCommit, branchName string) error } // Create a commit in master - _, err = commitSomething(repo, masterCommit, masterCommit) + _, err = commitSomething(repo, masterCommit, masterCommit, opts) if err != nil { return err } @@ -161,7 +160,7 @@ func setupRepoForRebase(repo *Repository, masterCommit, branchName string) error return nil } -func performRebaseOnto(repo *Repository, branch string) (*Rebase, error) { +func performRebaseOnto(repo *Repository, branch string, opts *RebaseOptions) (*Rebase, error) { master, err := repo.LookupBranch(branch, BranchLocal) if err != nil { return nil, err @@ -175,7 +174,7 @@ func performRebaseOnto(repo *Repository, branch string) (*Rebase, error) { defer onto.Free() // Init rebase - rebase, err := repo.InitRebase(nil, nil, onto, nil) + rebase, err := repo.InitRebase(nil, nil, onto, opts) if err != nil { return nil, err } @@ -276,7 +275,7 @@ func headTree(repo *Repository) (*Tree, error) { return tree, nil } -func commitSomething(repo *Repository, something, content string) (*Oid, error) { +func commitSomething(repo *Repository, something, content string, commitOpts commitOpts) (*Oid, error) { headCommit, err := headCommit(repo) if err != nil { return nil, err @@ -315,14 +314,40 @@ func commitSomething(repo *Repository, something, content string) (*Oid, error) } defer newTree.Free() - if err != nil { - return nil, err - } commit, err := repo.CreateCommit("HEAD", signature(), signature(), "Test rebase, Baby! "+something, newTree, headCommit) if err != nil { return nil, err } + if commitOpts.CommitSigningCallback != nil { + commit, err := repo.LookupCommit(commit) + if err != nil { + return nil, err + } + + oid, err := commit.WithSignatureUsing(commitOpts.CommitSigningCallback) + if err != nil { + return nil, err + } + newCommit, err := repo.LookupCommit(oid) + if err != nil { + return nil, err + } + head, err := repo.Head() + if err != nil { + return nil, err + } + _, err = repo.References.Create( + head.Name(), + newCommit.Id(), + true, + "repoint to signed commit", + ) + if err != nil { + return nil, err + } + } + opts := &CheckoutOpts{ Strategy: CheckoutRemoveUntracked | CheckoutForce, }