diff options
author | Sami Hiltunen <shiltunen@gitlab.com> | 2020-10-15 13:40:26 +0300 |
---|---|---|
committer | Sami Hiltunen <shiltunen@gitlab.com> | 2020-10-21 14:21:12 +0300 |
commit | 1dfda33b2ec81399307a0f062d14061c429a72ab (patch) | |
tree | b2b7461d550d3370280a06edb4d06d1f06521154 /internal/git2go | |
parent | 57424d40acf18995bc2facce7b75f8e6e3f14e75 (diff) |
add commit subcommand to gitaly-git2go
Adds 'commit' subcommand to gitaly-git2go to allow for building
commits without worktrees. Commit is built from a set of actions
and written in to the object database.
Diffstat (limited to 'internal/git2go')
-rw-r--r-- | internal/git2go/command.go | 18 | ||||
-rw-r--r-- | internal/git2go/commit.go | 69 | ||||
-rw-r--r-- | internal/git2go/commit_actions.go | 74 | ||||
-rw-r--r-- | internal/git2go/commit_test.go | 546 | ||||
-rw-r--r-- | internal/git2go/conflicts.go | 4 | ||||
-rw-r--r-- | internal/git2go/executor.go | 11 | ||||
-rw-r--r-- | internal/git2go/gob.go | 70 | ||||
-rw-r--r-- | internal/git2go/gob_test.go | 66 | ||||
-rw-r--r-- | internal/git2go/merge.go | 4 | ||||
-rw-r--r-- | internal/git2go/revert.go | 4 | ||||
-rw-r--r-- | internal/git2go/signature.go | 27 |
11 files changed, 879 insertions, 14 deletions
diff --git a/internal/git2go/command.go b/internal/git2go/command.go index 2409b847c..c65665de1 100644 --- a/internal/git2go/command.go +++ b/internal/git2go/command.go @@ -8,30 +8,32 @@ import ( "fmt" "io" "os/exec" - "path" + "path/filepath" "strings" "gitlab.com/gitlab-org/gitaly/internal/command" "gitlab.com/gitlab-org/gitaly/internal/gitaly/config" ) -func run(ctx context.Context, cfg config.Cfg, subcommand string, arg string) (string, error) { - binary := path.Join(cfg.BinDir, "gitaly-git2go") +func binaryPathFromCfg(cfg config.Cfg) string { + return filepath.Join(cfg.BinDir, "gitaly-git2go") +} +func run(ctx context.Context, binaryPath string, stdin io.Reader, args ...string) (*bytes.Buffer, error) { var stderr, stdout bytes.Buffer - cmd, err := command.New(ctx, exec.Command(binary, subcommand, "-request", arg), nil, &stdout, &stderr) + cmd, err := command.New(ctx, exec.Command(binaryPath, args...), stdin, &stdout, &stderr) if err != nil { - return "", err + return nil, err } if err := cmd.Wait(); err != nil { if _, ok := err.(*exec.ExitError); ok { - return "", fmt.Errorf("%s", stderr.String()) + return nil, fmt.Errorf("%s", stderr.String()) } - return "", err + return nil, err } - return stdout.String(), nil + return &stdout, nil } func serialize(v interface{}) (string, error) { diff --git a/internal/git2go/commit.go b/internal/git2go/commit.go new file mode 100644 index 000000000..ee24862d9 --- /dev/null +++ b/internal/git2go/commit.go @@ -0,0 +1,69 @@ +package git2go + +import ( + "bytes" + "context" + "encoding/gob" + "fmt" +) + +// InvalidArgumentError is returned when an invalid argument is provided. +type InvalidArgumentError string + +func (err InvalidArgumentError) Error() string { return string(err) } + +// FileNotFoundError is returned when an action attempts to operate on a non-existing file. +type FileNotFoundError string + +func (err FileNotFoundError) Error() string { + return fmt.Sprintf("file not found: %q", string(err)) +} + +// FileExistsError is returned when an action attempts to overwrite an existing file. +type FileExistsError string + +func (err FileExistsError) Error() string { + return fmt.Sprintf("file exists: %q", string(err)) +} + +// DirectoryExistsError is returned when an action attempts to overwrite a directory. +type DirectoryExistsError string + +func (err DirectoryExistsError) Error() string { + return fmt.Sprintf("directory exists: %q", string(err)) +} + +// CommitParams contains the information and the steps to build a commit. +type CommitParams struct { + // Repository is the path of the repository to operate on. + Repository string + // Author is the author of the commit. + Author Signature + // Message is message of the commit. + Message string + // Parent is the OID of the commit to use as the parent of this commit. + Parent string + // Actions are the steps to build the commit. + Actions []Action +} + +// Commit builds a commit from the actions, writes it to the object database and +// returns its object id. +func (b Executor) Commit(ctx context.Context, params CommitParams) (string, error) { + input := &bytes.Buffer{} + if err := gob.NewEncoder(input).Encode(params); err != nil { + return "", err + } + + output, err := run(ctx, b.binaryPath, input, "commit") + if err != nil { + return "", err + } + + var result Result + if err := gob.NewDecoder(output).Decode(&result); err != nil { + return "", err + } + + return result.CommitID, result.Error +} diff --git a/internal/git2go/commit_actions.go b/internal/git2go/commit_actions.go new file mode 100644 index 000000000..ced8c7464 --- /dev/null +++ b/internal/git2go/commit_actions.go @@ -0,0 +1,74 @@ +package git2go + +// Action represents an action taken to build a commit. +type Action interface{ action() } + +// isAction is used ensuring type safety for actions. +type isAction struct{} + +func (isAction) action() {} + +// ChangeFileMode sets a file's mode to either regular or executable file. +// FileNotFoundError is returned when attempting to change a non-existent +// file's mode. +type ChangeFileMode struct { + isAction + // Path is the path of the whose mode to change. + Path string + // ExecutableMode indicates whether the file mode should be changed to executable or not. + ExecutableMode bool +} + +// CreateDirectory creates a directory in the given path with a '.gitkeep' file inside. +// FileExistsError is returned if a file already exists at the provided path. +// DirectoryExistsError is returned if a directory already exists at the provided +// path. +type CreateDirectory struct { + isAction + // Path is the path of the directory to create. + Path string +} + +// CreateFile creates a file using the provided path, mode and oid as the blob. +// FileExistsError is returned if a file exists at the given path. +type CreateFile struct { + isAction + // Path is the path of the file to create. + Path string + // ExecutableMode indicates whether the file mode should be executable or not. + ExecutableMode bool + // OID is the id of the object that contains the content of the file. + OID string +} + +// DeleteFile deletes a file or a directory from the provided path. +// FileNotFoundError is returned if the file does not exist. +type DeleteFile struct { + isAction + // Path is the path of the file to delete. + Path string +} + +// MoveFile moves a file or a directory to the new path. +// FileNotFoundError is returned if the file does not exist. +type MoveFile struct { + isAction + // Path is the path of the file to move. + Path string + // NewPath is the new path of the file. + NewPath string + // OID is the id of the object that contains the content of the file. If set, + // the file contents are updated to match the object, otherwise the file keeps + // the existing content. + OID string +} + +// UpdateFile updates a file at the given path to point to the provided +// OID. FileNotFoundError is returned if the file does not exist. +type UpdateFile struct { + isAction + // Path is the path of the file to update. + Path string + // OID is the id of the object that contains the new content of the file. + OID string +} diff --git a/internal/git2go/commit_test.go b/internal/git2go/commit_test.go new file mode 100644 index 000000000..6a1eea7a0 --- /dev/null +++ b/internal/git2go/commit_test.go @@ -0,0 +1,546 @@ +package git2go + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitaly/internal/git" + "gitlab.com/gitlab-org/gitaly/internal/gitaly/config" + "gitlab.com/gitlab-org/gitaly/internal/testhelper" +) + +func TestMain(m *testing.M) { + testhelper.Configure() + testhelper.ConfigureGitalyGit2Go() + os.Exit(m.Run()) +} + +type commit struct { + Parent string + Author Signature + Committer Signature + Message string +} + +func TestExecutor_Commit(t *testing.T) { + const ( + DefaultMode = "100644" + ExecutableMode = "100755" + ) + + type step struct { + actions []Action + error error + treeEntries []testhelper.TreeEntry + } + + ctx, cancel := testhelper.Context() + defer cancel() + + pbRepo, repoPath, clean := testhelper.InitBareRepo(t) + defer clean() + + repo := git.NewRepository(pbRepo) + + originalFile, err := repo.WriteBlob(ctx, "file", bytes.NewBufferString("original")) + require.NoError(t, err) + + updatedFile, err := repo.WriteBlob(ctx, "file", bytes.NewBufferString("updated")) + require.NoError(t, err) + + executor := New(filepath.Join(config.Config.BinDir, "gitaly-git2go")) + + for _, tc := range []struct { + desc string + steps []step + }{ + { + desc: "create directory", + steps: []step{ + { + actions: []Action{ + CreateDirectory{Path: "directory"}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: DefaultMode, Path: "directory/.gitkeep"}, + }, + }, + }, + }, + { + desc: "create directory created duplicate", + steps: []step{ + { + actions: []Action{ + CreateDirectory{Path: "directory"}, + CreateDirectory{Path: "directory"}, + }, + error: DirectoryExistsError("directory"), + }, + }, + }, + { + desc: "create directory existing duplicate", + steps: []step{ + { + actions: []Action{ + CreateDirectory{Path: "directory"}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: DefaultMode, Path: "directory/.gitkeep"}, + }, + }, + { + actions: []Action{ + CreateDirectory{Path: "directory"}, + }, + error: DirectoryExistsError("directory"), + }, + }, + }, + { + desc: "create directory with a files name", + steps: []step{ + { + actions: []Action{ + CreateFile{Path: "file", OID: originalFile}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: DefaultMode, Path: "file", Content: "original"}, + }, + }, + { + actions: []Action{ + CreateDirectory{Path: "file"}, + }, + error: FileExistsError("file"), + }, + }, + }, + { + desc: "create file", + steps: []step{ + { + actions: []Action{ + CreateFile{Path: "file", OID: originalFile}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: DefaultMode, Path: "file", Content: "original"}, + }, + }, + }, + }, + { + desc: "create duplicate file", + steps: []step{ + { + actions: []Action{ + CreateFile{Path: "file", OID: originalFile}, + CreateFile{Path: "file", OID: updatedFile}, + }, + error: FileExistsError("file"), + }, + }, + }, + { + desc: "create file overwrites directory", + steps: []step{ + { + actions: []Action{ + CreateDirectory{Path: "directory"}, + CreateFile{Path: "directory", OID: originalFile}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: DefaultMode, Path: "directory", Content: "original"}, + }, + }, + }, + }, + { + desc: "update created file", + steps: []step{ + { + actions: []Action{ + CreateFile{Path: "file", OID: originalFile}, + UpdateFile{Path: "file", OID: updatedFile}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: DefaultMode, Path: "file", Content: "updated"}, + }, + }, + }, + }, + { + desc: "update existing file", + steps: []step{ + { + actions: []Action{ + CreateFile{Path: "file", OID: originalFile}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: DefaultMode, Path: "file", Content: "original"}, + }, + }, + { + actions: []Action{ + UpdateFile{Path: "file", OID: updatedFile}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: DefaultMode, Path: "file", Content: "updated"}, + }, + }, + }, + }, + { + desc: "update non-existing file", + steps: []step{ + { + actions: []Action{ + UpdateFile{Path: "non-existing", OID: updatedFile}, + }, + error: FileNotFoundError("non-existing"), + }, + }, + }, + { + desc: "move created file", + steps: []step{ + { + actions: []Action{ + CreateFile{Path: "original-file", OID: originalFile}, + MoveFile{Path: "original-file", NewPath: "moved-file", OID: originalFile}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: DefaultMode, Path: "moved-file", Content: "original"}, + }, + }, + }, + }, + { + desc: "moving directory fails", + steps: []step{ + { + actions: []Action{ + CreateDirectory{Path: "directory"}, + MoveFile{Path: "directory", NewPath: "moved-directory"}, + }, + error: FileNotFoundError("directory"), + }, + }, + }, + { + desc: "move file inferring content", + steps: []step{ + { + actions: []Action{ + CreateFile{Path: "original-file", OID: originalFile}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: DefaultMode, Path: "original-file", Content: "original"}, + }, + }, + { + actions: []Action{ + MoveFile{Path: "original-file", NewPath: "moved-file"}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: DefaultMode, Path: "moved-file", Content: "original"}, + }, + }, + }, + }, + { + desc: "move file with non-existing source", + steps: []step{ + { + actions: []Action{ + MoveFile{Path: "non-existing", NewPath: "destination-file"}, + }, + error: FileNotFoundError("non-existing"), + }, + }, + }, + { + desc: "move file with already existing destination file", + steps: []step{ + { + actions: []Action{ + CreateFile{Path: "source-file", OID: originalFile}, + CreateFile{Path: "already-existing", OID: updatedFile}, + MoveFile{Path: "source-file", NewPath: "already-existing"}, + }, + error: FileExistsError("already-existing"), + }, + }, + }, + { + // seems like a bug in the original implementation to allow overwriting a + // directory + desc: "move file with already existing destination directory", + steps: []step{ + { + actions: []Action{ + CreateFile{Path: "file", OID: originalFile}, + CreateDirectory{Path: "already-existing"}, + MoveFile{Path: "file", NewPath: "already-existing"}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: DefaultMode, Path: "already-existing", Content: "original"}, + }, + }, + }, + }, + { + desc: "move file providing content", + steps: []step{ + { + actions: []Action{ + CreateFile{Path: "original-file", OID: originalFile}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: DefaultMode, Path: "original-file", Content: "original"}, + }, + }, + { + actions: []Action{ + MoveFile{Path: "original-file", NewPath: "moved-file", OID: updatedFile}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: DefaultMode, Path: "moved-file", Content: "updated"}, + }, + }, + }, + }, + { + desc: "mark non-existing file executable", + steps: []step{ + { + actions: []Action{ + ChangeFileMode{Path: "non-existing"}, + }, + error: FileNotFoundError("non-existing"), + }, + }, + }, + { + desc: "mark executable file executable", + steps: []step{ + { + actions: []Action{ + CreateFile{Path: "file-1", OID: originalFile}, + ChangeFileMode{Path: "file-1", ExecutableMode: true}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: ExecutableMode, Path: "file-1", Content: "original"}, + }, + }, + { + actions: []Action{ + ChangeFileMode{Path: "file-1", ExecutableMode: true}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: ExecutableMode, Path: "file-1", Content: "original"}, + }, + }, + }, + }, + { + desc: "mark created file executable", + steps: []step{ + { + actions: []Action{ + CreateFile{Path: "file-1", OID: originalFile}, + ChangeFileMode{Path: "file-1", ExecutableMode: true}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: ExecutableMode, Path: "file-1", Content: "original"}, + }, + }, + }, + }, + { + desc: "mark existing file executable", + steps: []step{ + { + actions: []Action{ + CreateFile{Path: "file-1", OID: originalFile}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: DefaultMode, Path: "file-1", Content: "original"}, + }, + }, + { + actions: []Action{ + ChangeFileMode{Path: "file-1", ExecutableMode: true}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: ExecutableMode, Path: "file-1", Content: "original"}, + }, + }, + }, + }, + { + desc: "move non-existing file", + steps: []step{ + { + actions: []Action{ + MoveFile{Path: "non-existing", NewPath: "destination"}, + }, + error: FileNotFoundError("non-existing"), + }, + }, + }, + { + desc: "move doesn't overwrite a file", + steps: []step{ + { + actions: []Action{ + CreateFile{Path: "file-1", OID: originalFile}, + CreateFile{Path: "file-2", OID: updatedFile}, + MoveFile{Path: "file-1", NewPath: "file-2"}, + }, + error: FileExistsError("file-2"), + }, + }, + }, + { + desc: "delete non-existing file", + steps: []step{ + { + actions: []Action{ + DeleteFile{Path: "non-existing"}, + }, + error: FileNotFoundError("non-existing"), + }, + }, + }, + { + desc: "delete created file", + steps: []step{ + { + actions: []Action{ + CreateFile{Path: "file-1", OID: originalFile}, + DeleteFile{Path: "file-1"}, + }, + }, + }, + }, + { + desc: "delete existing file", + steps: []step{ + { + actions: []Action{ + CreateFile{Path: "file-1", OID: originalFile}, + }, + treeEntries: []testhelper.TreeEntry{ + {Mode: DefaultMode, Path: "file-1", Content: "original"}, + }, + }, + { + actions: []Action{ + DeleteFile{Path: "file-1"}, + }, + }, + }, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + author := NewSignature("Author Name", "author.email@example.com", time.Now()) + var parentCommit string + for i, step := range tc.steps { + message := fmt.Sprintf("commit %d", i+1) + commitID, err := executor.Commit(ctx, CommitParams{ + Repository: repoPath, + Author: author, + Message: message, + Parent: parentCommit, + Actions: step.actions, + }) + + if step.error != nil { + require.True(t, errors.Is(err, step.error), "expected: %q, actual: %q", step.error, err) + continue + } else { + require.NoError(t, err) + } + + require.Equal(t, commit{ + Parent: parentCommit, + Author: author, + Committer: author, + Message: message, + }, getCommit(t, ctx, repo, commitID)) + + testhelper.RequireTree(t, repoPath, commitID, step.treeEntries) + parentCommit = commitID + } + }) + } +} + +func getCommit(t testing.TB, ctx context.Context, repo git.Repository, oid string) commit { + t.Helper() + + data, err := repo.ReadObject(ctx, oid) + require.NoError(t, err) + + var commit commit + lines := strings.Split(string(data), "\n") + for i, line := range lines { + if line == "" { + commit.Message = strings.Join(lines[i+1:], "\n") + break + } + + split := strings.SplitN(line, " ", 2) + require.Len(t, split, 2, "invalid commit: %q", data) + + field, value := split[0], split[1] + switch field { + case "parent": + require.Empty(t, commit.Parent, "multi parent parsing not implemented") + commit.Parent = value + case "author": + require.Empty(t, commit.Author, "commit contained multiple authors") + commit.Author = unmarshalSignature(t, value) + case "committer": + require.Empty(t, commit.Committer, "commit contained multiple committers") + commit.Committer = unmarshalSignature(t, value) + default: + } + } + + return commit +} + +func unmarshalSignature(t testing.TB, data string) Signature { + t.Helper() + + // Format: NAME <EMAIL> DATE_UNIX DATE_TIMEZONE + split1 := strings.Split(data, " <") + require.Len(t, split1, 2, "invalid signature: %q", data) + + split2 := strings.Split(split1[1], "> ") + require.Len(t, split2, 2, "invalid signature: %q", data) + + split3 := strings.Split(split2[1], " ") + require.Len(t, split3, 2, "invalid signature: %q", data) + + timestamp, err := strconv.ParseInt(split3[0], 10, 64) + require.NoError(t, err) + + return Signature{ + Name: split1[0], + Email: split2[0], + When: time.Unix(timestamp, 0), + } +} diff --git a/internal/git2go/conflicts.go b/internal/git2go/conflicts.go index c5848c618..67f91808a 100644 --- a/internal/git2go/conflicts.go +++ b/internal/git2go/conflicts.go @@ -86,13 +86,13 @@ func (c ConflictsCommand) Run(ctx context.Context, cfg config.Cfg) (ConflictsRes return ConflictsResult{}, err } - stdout, err := run(ctx, cfg, "conflicts", serialized) + stdout, err := run(ctx, binaryPathFromCfg(cfg), nil, "conflicts", "-request", serialized) if err != nil { return ConflictsResult{}, err } var response ConflictsResult - if err := deserialize(stdout, &response); err != nil { + if err := deserialize(stdout.String(), &response); err != nil { return ConflictsResult{}, err } diff --git a/internal/git2go/executor.go b/internal/git2go/executor.go new file mode 100644 index 000000000..0007ab7e5 --- /dev/null +++ b/internal/git2go/executor.go @@ -0,0 +1,11 @@ +package git2go + +// Executor executes gitaly-git2go. +type Executor struct { + binaryPath string +} + +// New returns a new gitaly-git2go executor using the provided binary. +func New(binaryPath string) Executor { + return Executor{binaryPath: binaryPath} +} diff --git a/internal/git2go/gob.go b/internal/git2go/gob.go new file mode 100644 index 000000000..5f87b95e0 --- /dev/null +++ b/internal/git2go/gob.go @@ -0,0 +1,70 @@ +package git2go + +import ( + "encoding/gob" + "errors" + "reflect" +) + +func init() { + for typee := range registeredTypes { + gob.Register(typee) + } +} + +var registeredTypes = map[interface{}]struct{}{ + ChangeFileMode{}: {}, + CreateDirectory{}: {}, + CreateFile{}: {}, + DeleteFile{}: {}, + MoveFile{}: {}, + UpdateFile{}: {}, + wrapError{}: {}, + DirectoryExistsError(""): {}, + FileExistsError(""): {}, + FileNotFoundError(""): {}, + InvalidArgumentError(""): {}, +} + +// Result is the serialized result. +type Result struct { + // CommitID is the result of the call. + CommitID string + // Error is set if the call errord. + Error error +} + +// wrapError is used to serialize wrapped errors as fmt.wrapError type only has +// private fields and can't be serialized via gob. It's also used to serialize unregistered +// error types by serializing only their error message. +type wrapError struct { + Message string + Err error +} + +func (err wrapError) Error() string { return err.Message } + +func (err wrapError) Unwrap() error { return err.Err } + +// SerializableError returns an error that is Gob serializable. +// Registered types are serialized directly. Unregistered types +// are transformed in to an opaque error using their error message. +// Wrapped errors remain unwrappable. +func SerializableError(err error) error { + if err == nil { + return nil + } + + if unwrappedErr := errors.Unwrap(err); unwrappedErr != nil { + return wrapError{ + Message: err.Error(), + Err: SerializableError(unwrappedErr), + } + } + + if _, ok := registeredTypes[reflect.Zero(reflect.TypeOf(err)).Interface()]; !ok { + return wrapError{Message: err.Error()} + } + + return err +} diff --git a/internal/git2go/gob_test.go b/internal/git2go/gob_test.go new file mode 100644 index 000000000..e18a10961 --- /dev/null +++ b/internal/git2go/gob_test.go @@ -0,0 +1,66 @@ +package git2go + +import ( + "bytes" + "encoding/gob" + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSerializableError(t *testing.T) { + for _, tc := range []struct { + desc string + input error + output error + containsTyped bool + }{ + { + desc: "plain error", + input: errors.New("plain error"), + output: wrapError{Message: "plain error"}, + }, + { + desc: "wrapped plain error", + input: fmt.Errorf("error wrapper: %w", errors.New("plain error")), + output: wrapError{Message: "error wrapper: plain error", Err: wrapError{Message: "plain error"}}, + }, + { + desc: "wrapped typed error", + containsTyped: true, + input: fmt.Errorf("error wrapper: %w", InvalidArgumentError("typed error")), + output: wrapError{Message: "error wrapper: typed error", Err: InvalidArgumentError("typed error")}, + }, + { + desc: "typed wrapper", + containsTyped: true, + input: wrapError{ + Message: "error wrapper: typed error 1: typed error 2", + Err: wrapError{ + Message: "typed error 1: typed error 2", + Err: InvalidArgumentError("typed error 2"), + }, + }, + output: wrapError{ + Message: "error wrapper: typed error 1: typed error 2", + Err: wrapError{ + Message: "typed error 1: typed error 2", + Err: InvalidArgumentError("typed error 2"), + }, + }, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + encoded := &bytes.Buffer{} + require.NoError(t, gob.NewEncoder(encoded).Encode(SerializableError(tc.input))) + var err wrapError + require.NoError(t, gob.NewDecoder(encoded).Decode(&err)) + require.Equal(t, tc.output, err) + + var typedErr InvalidArgumentError + require.Equal(t, tc.containsTyped, errors.As(err, &typedErr)) + }) + } +} diff --git a/internal/git2go/merge.go b/internal/git2go/merge.go index a60ff51c8..90a2db549 100644 --- a/internal/git2go/merge.go +++ b/internal/git2go/merge.go @@ -69,13 +69,13 @@ func (m MergeCommand) Run(ctx context.Context, cfg config.Cfg) (MergeResult, err return MergeResult{}, err } - stdout, err := run(ctx, cfg, "merge", serialized) + stdout, err := run(ctx, binaryPathFromCfg(cfg), nil, "merge", "-request", serialized) if err != nil { return MergeResult{}, err } var response MergeResult - if err := deserialize(stdout, &response); err != nil { + if err := deserialize(stdout.String(), &response); err != nil { return MergeResult{}, err } diff --git a/internal/git2go/revert.go b/internal/git2go/revert.go index ca11169bf..8e6169abc 100644 --- a/internal/git2go/revert.go +++ b/internal/git2go/revert.go @@ -50,13 +50,13 @@ func (r RevertCommand) Run(ctx context.Context, cfg config.Cfg) (RevertResult, e return RevertResult{}, err } - stdout, err := run(ctx, cfg, "revert", serialized) + stdout, err := run(ctx, binaryPathFromCfg(cfg), nil, "revert", "-request", serialized) if err != nil { return RevertResult{}, err } var response RevertResult - if err := deserialize(stdout, &response); err != nil { + if err := deserialize(stdout.String(), &response); err != nil { return RevertResult{}, err } diff --git a/internal/git2go/signature.go b/internal/git2go/signature.go new file mode 100644 index 000000000..79306c051 --- /dev/null +++ b/internal/git2go/signature.go @@ -0,0 +1,27 @@ +package git2go + +import ( + "strings" + "time" +) + +var signatureSanitizer = strings.NewReplacer("\n", "", "<", "", ">", "") + +// Signature represents a commits signature. +type Signature struct { + // Name of the author or the committer. + Name string + // Email of the author or the committer. + Email string + // When is the time of the commit. + When time.Time +} + +// NewSignature creates a new sanitized signature. +func NewSignature(name, email string, when time.Time) Signature { + return Signature{ + Name: signatureSanitizer.Replace(name), + Email: signatureSanitizer.Replace(email), + When: when.Truncate(time.Second), + } +} |