2016-11-03 18:16:01 -04:00
|
|
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
2017-05-30 05:32:01 -04:00
|
|
|
// Copyright 2017 The Gitea Authors. All rights reserved.
|
2022-11-27 13:20:29 -05:00
|
|
|
// SPDX-License-Identifier: MIT
|
2016-11-03 18:16:01 -04:00
|
|
|
|
|
|
|
package git
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2020-12-02 13:36:06 -05:00
|
|
|
"context"
|
2019-05-05 12:25:25 -04:00
|
|
|
"fmt"
|
2021-08-18 09:10:39 -04:00
|
|
|
"io"
|
|
|
|
"net/url"
|
2016-11-03 18:16:01 -04:00
|
|
|
"os"
|
|
|
|
"path"
|
2021-08-24 12:47:09 -04:00
|
|
|
"path/filepath"
|
2019-05-05 12:25:25 -04:00
|
|
|
"strconv"
|
2017-04-07 22:23:39 -04:00
|
|
|
"strings"
|
2016-11-03 18:16:01 -04:00
|
|
|
"time"
|
2021-08-18 09:10:39 -04:00
|
|
|
|
|
|
|
"code.gitea.io/gitea/modules/proxy"
|
2022-03-27 07:54:09 -04:00
|
|
|
"code.gitea.io/gitea/modules/util"
|
2016-11-03 18:16:01 -04:00
|
|
|
)
|
|
|
|
|
2019-10-16 09:42:42 -04:00
|
|
|
// GPGSettings represents the default GPG settings for this repository
|
|
|
|
type GPGSettings struct {
|
|
|
|
Sign bool
|
|
|
|
KeyID string
|
|
|
|
Email string
|
|
|
|
Name string
|
|
|
|
PublicKeyContent string
|
2016-11-03 18:16:01 -04:00
|
|
|
}
|
|
|
|
|
2016-12-22 04:30:52 -05:00
|
|
|
const prettyLogFormat = `--pretty=format:%H`
|
2016-11-03 18:16:01 -04:00
|
|
|
|
2019-11-07 13:09:51 -05:00
|
|
|
// GetAllCommitsCount returns count of all commits in repository
|
|
|
|
func (repo *Repository) GetAllCommitsCount() (int64, error) {
|
2022-01-19 18:26:57 -05:00
|
|
|
return AllCommitsCount(repo.Ctx, repo.Path, false)
|
2019-11-07 13:09:51 -05:00
|
|
|
}
|
|
|
|
|
2021-08-09 14:08:51 -04:00
|
|
|
func (repo *Repository) parsePrettyFormatLogToList(logs []byte) ([]*Commit, error) {
|
|
|
|
var commits []*Commit
|
2016-11-03 18:16:01 -04:00
|
|
|
if len(logs) == 0 {
|
2021-08-09 14:08:51 -04:00
|
|
|
return commits, nil
|
2016-11-03 18:16:01 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
parts := bytes.Split(logs, []byte{'\n'})
|
|
|
|
|
2016-12-22 04:30:52 -05:00
|
|
|
for _, commitID := range parts {
|
|
|
|
commit, err := repo.GetCommit(string(commitID))
|
2016-11-03 18:16:01 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2021-08-09 14:08:51 -04:00
|
|
|
commits = append(commits, commit)
|
2016-11-03 18:16:01 -04:00
|
|
|
}
|
|
|
|
|
2021-08-09 14:08:51 -04:00
|
|
|
return commits, nil
|
2016-11-03 18:16:01 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// IsRepoURLAccessible checks if given repository URL is accessible.
|
2022-01-19 18:26:57 -05:00
|
|
|
func IsRepoURLAccessible(ctx context.Context, url string) bool {
|
2022-10-23 10:44:45 -04:00
|
|
|
_, _, err := NewCommand(ctx, "ls-remote", "-q", "-h").AddDynamicArguments(url, "HEAD").RunStdString(nil)
|
2019-06-12 15:41:28 -04:00
|
|
|
return err == nil
|
2016-11-03 18:16:01 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// InitRepository initializes a new Git repository.
|
2022-01-19 18:26:57 -05:00
|
|
|
func InitRepository(ctx context.Context, repoPath string, bare bool) error {
|
2019-06-12 15:41:28 -04:00
|
|
|
err := os.MkdirAll(repoPath, os.ModePerm)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2016-11-03 18:16:01 -04:00
|
|
|
|
2022-02-06 14:01:47 -05:00
|
|
|
cmd := NewCommand(ctx, "init")
|
2016-11-03 18:16:01 -04:00
|
|
|
if bare {
|
|
|
|
cmd.AddArguments("--bare")
|
|
|
|
}
|
2022-03-31 22:55:30 -04:00
|
|
|
_, _, err = cmd.RunStdString(&RunOpts{Dir: repoPath})
|
2016-11-03 18:16:01 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-06-26 14:15:26 -04:00
|
|
|
// IsEmpty Check if repository is empty.
|
|
|
|
func (repo *Repository) IsEmpty() (bool, error) {
|
2022-01-27 21:51:16 -05:00
|
|
|
var errbuf, output strings.Builder
|
2023-08-17 00:43:39 -04:00
|
|
|
if err := NewCommand(repo.Ctx).AddOptionFormat("--git-dir=%s", repo.Path).AddArguments("rev-list", "-n", "1", "--all").
|
2022-03-31 22:55:30 -04:00
|
|
|
Run(&RunOpts{
|
|
|
|
Dir: repo.Path,
|
|
|
|
Stdout: &output,
|
|
|
|
Stderr: &errbuf,
|
2022-01-27 21:51:16 -05:00
|
|
|
}); err != nil {
|
2023-10-02 11:59:39 -04:00
|
|
|
if (err.Error() == "exit status 1" && strings.TrimSpace(errbuf.String()) == "") || err.Error() == "exit status 129" {
|
2023-10-02 10:05:21 -04:00
|
|
|
// git 2.11 exits with 129 if the repo is empty
|
2022-02-13 11:01:23 -05:00
|
|
|
return true, nil
|
|
|
|
}
|
2022-10-24 15:29:17 -04:00
|
|
|
return true, fmt.Errorf("check empty: %w - %s", err, errbuf.String())
|
2019-06-26 14:15:26 -04:00
|
|
|
}
|
|
|
|
|
2022-02-13 11:01:23 -05:00
|
|
|
return strings.TrimSpace(output.String()) == "", nil
|
2019-06-26 14:15:26 -04:00
|
|
|
}
|
|
|
|
|
2016-12-22 04:30:52 -05:00
|
|
|
// CloneRepoOptions options when clone a repository
|
2016-11-03 18:16:01 -04:00
|
|
|
type CloneRepoOptions struct {
|
2022-03-19 10:16:38 -04:00
|
|
|
Timeout time.Duration
|
|
|
|
Mirror bool
|
|
|
|
Bare bool
|
|
|
|
Quiet bool
|
|
|
|
Branch string
|
|
|
|
Shared bool
|
|
|
|
NoCheckout bool
|
|
|
|
Depth int
|
|
|
|
Filter string
|
|
|
|
SkipTLSVerify bool
|
2016-11-03 18:16:01 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Clone clones original repository to target path.
|
2022-01-19 18:26:57 -05:00
|
|
|
func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {
|
2022-10-23 10:44:45 -04:00
|
|
|
return CloneWithArgs(ctx, globalCommandArgs, from, to, opts)
|
2019-11-26 19:35:52 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// CloneWithArgs original repository to target path.
|
Refactor git command package to improve security and maintainability (#22678)
This PR follows #21535 (and replace #22592)
## Review without space diff
https://github.com/go-gitea/gitea/pull/22678/files?diff=split&w=1
## Purpose of this PR
1. Make git module command completely safe (risky user inputs won't be
passed as argument option anymore)
2. Avoid low-level mistakes like
https://github.com/go-gitea/gitea/pull/22098#discussion_r1045234918
3. Remove deprecated and dirty `CmdArgCheck` function, hide the `CmdArg`
type
4. Simplify code when using git command
## The main idea of this PR
* Move the `git.CmdArg` to the `internal` package, then no other package
except `git` could use it. Then developers could never do
`AddArguments(git.CmdArg(userInput))` any more.
* Introduce `git.ToTrustedCmdArgs`, it's for user-provided and already
trusted arguments. It's only used in a few cases, for example: use git
arguments from config file, help unit test with some arguments.
* Introduce `AddOptionValues` and `AddOptionFormat`, they make code more
clear and simple:
* Before: `AddArguments("-m").AddDynamicArguments(message)`
* After: `AddOptionValues("-m", message)`
* -
* Before: `AddArguments(git.CmdArg(fmt.Sprintf("--author='%s <%s>'",
sig.Name, sig.Email)))`
* After: `AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email)`
## FAQ
### Why these changes were not done in #21535 ?
#21535 is mainly a search&replace, it did its best to not change too
much logic.
Making the framework better needs a lot of changes, so this separate PR
is needed as the second step.
### The naming of `AddOptionXxx`
According to git's manual, the `--xxx` part is called `option`.
### How can it guarantee that `internal.CmdArg` won't be not misused?
Go's specification guarantees that. Trying to access other package's
internal package causes compilation error.
And, `golangci-lint` also denies the git/internal package. Only the
`git/command.go` can use it carefully.
### There is still a `ToTrustedCmdArgs`, will it still allow developers
to make mistakes and pass untrusted arguments?
Generally speaking, no. Because when using `ToTrustedCmdArgs`, the code
will be very complex (see the changes for examples). Then developers and
reviewers can know that something might be unreasonable.
### Why there was a `CmdArgCheck` and why it's removed?
At the moment of #21535, to reduce unnecessary changes, `CmdArgCheck`
was introduced as a hacky patch. Now, almost all code could be written
as `cmd := NewCommand(); cmd.AddXxx(...)`, then there is no need for
`CmdArgCheck` anymore.
### Why many codes for `signArg == ""` is deleted?
Because in the old code, `signArg` could never be empty string, it's
either `-S[key-id]` or `--no-gpg-sign`. So the `signArg == ""` is just
dead code.
---------
Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
2023-02-03 21:30:43 -05:00
|
|
|
func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, opts CloneRepoOptions) (err error) {
|
2016-11-03 18:16:01 -04:00
|
|
|
toDir := path.Dir(to)
|
|
|
|
if err = os.MkdirAll(toDir, os.ModePerm); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-12-02 13:36:06 -05:00
|
|
|
cmd := NewCommandContextNoGlobals(ctx, args...).AddArguments("clone")
|
2022-03-19 10:16:38 -04:00
|
|
|
if opts.SkipTLSVerify {
|
|
|
|
cmd.AddArguments("-c", "http.sslVerify=false")
|
|
|
|
}
|
2016-11-03 18:16:01 -04:00
|
|
|
if opts.Mirror {
|
|
|
|
cmd.AddArguments("--mirror")
|
|
|
|
}
|
|
|
|
if opts.Bare {
|
|
|
|
cmd.AddArguments("--bare")
|
|
|
|
}
|
|
|
|
if opts.Quiet {
|
|
|
|
cmd.AddArguments("--quiet")
|
|
|
|
}
|
2019-05-11 11:29:17 -04:00
|
|
|
if opts.Shared {
|
|
|
|
cmd.AddArguments("-s")
|
|
|
|
}
|
|
|
|
if opts.NoCheckout {
|
|
|
|
cmd.AddArguments("--no-checkout")
|
|
|
|
}
|
2019-11-30 01:54:47 -05:00
|
|
|
if opts.Depth > 0 {
|
2022-10-23 10:44:45 -04:00
|
|
|
cmd.AddArguments("--depth").AddDynamicArguments(strconv.Itoa(opts.Depth))
|
2019-11-30 01:54:47 -05:00
|
|
|
}
|
2022-01-23 16:19:32 -05:00
|
|
|
if opts.Filter != "" {
|
2022-10-23 10:44:45 -04:00
|
|
|
cmd.AddArguments("--filter").AddDynamicArguments(opts.Filter)
|
2022-01-23 16:19:32 -05:00
|
|
|
}
|
2016-11-03 18:16:01 -04:00
|
|
|
if len(opts.Branch) > 0 {
|
2022-10-23 10:44:45 -04:00
|
|
|
cmd.AddArguments("-b").AddDynamicArguments(opts.Branch)
|
2016-11-03 18:16:01 -04:00
|
|
|
}
|
2022-10-23 10:44:45 -04:00
|
|
|
cmd.AddDashesAndList(from, to)
|
2016-11-03 18:16:01 -04:00
|
|
|
|
2022-03-27 07:54:09 -04:00
|
|
|
if strings.Contains(from, "://") && strings.Contains(from, "@") {
|
2022-03-30 22:25:40 -04:00
|
|
|
cmd.SetDescription(fmt.Sprintf("clone branch %s from %s to %s (shared: %t, mirror: %t, depth: %d)", opts.Branch, util.SanitizeCredentialURLs(from), to, opts.Shared, opts.Mirror, opts.Depth))
|
2022-03-27 07:54:09 -04:00
|
|
|
} else {
|
|
|
|
cmd.SetDescription(fmt.Sprintf("clone branch %s from %s to %s (shared: %t, mirror: %t, depth: %d)", opts.Branch, from, to, opts.Shared, opts.Mirror, opts.Depth))
|
|
|
|
}
|
|
|
|
|
2016-11-03 18:16:01 -04:00
|
|
|
if opts.Timeout <= 0 {
|
|
|
|
opts.Timeout = -1
|
|
|
|
}
|
|
|
|
|
2022-01-20 12:46:10 -05:00
|
|
|
envs := os.Environ()
|
2021-08-18 09:10:39 -04:00
|
|
|
u, err := url.Parse(from)
|
2023-02-10 19:39:50 -05:00
|
|
|
if err == nil {
|
|
|
|
envs = proxy.EnvWithProxy(u)
|
2021-08-18 09:10:39 -04:00
|
|
|
}
|
|
|
|
|
2022-01-20 12:46:10 -05:00
|
|
|
stderr := new(bytes.Buffer)
|
2022-03-31 22:55:30 -04:00
|
|
|
if err = cmd.Run(&RunOpts{
|
2021-08-18 09:10:39 -04:00
|
|
|
Timeout: opts.Timeout,
|
|
|
|
Env: envs,
|
|
|
|
Stdout: io.Discard,
|
|
|
|
Stderr: stderr,
|
|
|
|
}); err != nil {
|
|
|
|
return ConcatenateError(err, stderr.String())
|
|
|
|
}
|
|
|
|
return nil
|
2016-11-03 18:16:01 -04:00
|
|
|
}
|
|
|
|
|
2017-05-30 05:32:01 -04:00
|
|
|
// PushOptions options when push to remote
|
|
|
|
type PushOptions struct {
|
2021-06-14 13:20:43 -04:00
|
|
|
Remote string
|
|
|
|
Branch string
|
|
|
|
Force bool
|
|
|
|
Mirror bool
|
|
|
|
Env []string
|
|
|
|
Timeout time.Duration
|
2017-05-30 05:32:01 -04:00
|
|
|
}
|
|
|
|
|
2016-11-03 18:16:01 -04:00
|
|
|
// Push pushs local commits to given remote branch.
|
2021-11-30 15:06:32 -05:00
|
|
|
func Push(ctx context.Context, repoPath string, opts PushOptions) error {
|
2022-02-06 14:01:47 -05:00
|
|
|
cmd := NewCommand(ctx, "push")
|
2017-05-30 05:32:01 -04:00
|
|
|
if opts.Force {
|
|
|
|
cmd.AddArguments("-f")
|
|
|
|
}
|
2021-06-14 13:20:43 -04:00
|
|
|
if opts.Mirror {
|
|
|
|
cmd.AddArguments("--mirror")
|
|
|
|
}
|
2022-10-23 10:44:45 -04:00
|
|
|
remoteBranchArgs := []string{opts.Remote}
|
2021-06-14 13:20:43 -04:00
|
|
|
if len(opts.Branch) > 0 {
|
2022-10-23 10:44:45 -04:00
|
|
|
remoteBranchArgs = append(remoteBranchArgs, opts.Branch)
|
2021-06-14 13:20:43 -04:00
|
|
|
}
|
2022-10-23 10:44:45 -04:00
|
|
|
cmd.AddDashesAndList(remoteBranchArgs...)
|
|
|
|
|
2022-03-27 07:54:09 -04:00
|
|
|
if strings.Contains(opts.Remote, "://") && strings.Contains(opts.Remote, "@") {
|
2022-03-30 22:25:40 -04:00
|
|
|
cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, util.SanitizeCredentialURLs(opts.Remote), opts.Force, opts.Mirror))
|
2022-03-27 07:54:09 -04:00
|
|
|
} else {
|
|
|
|
cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, opts.Remote, opts.Force, opts.Mirror))
|
|
|
|
}
|
2020-03-28 00:13:18 -04:00
|
|
|
|
2023-04-13 19:17:27 -04:00
|
|
|
stdout, stderr, err := cmd.RunStdString(&RunOpts{Env: opts.Env, Timeout: opts.Timeout, Dir: repoPath})
|
2020-03-28 00:13:18 -04:00
|
|
|
if err != nil {
|
2023-04-13 19:17:27 -04:00
|
|
|
if strings.Contains(stderr, "non-fast-forward") {
|
|
|
|
return &ErrPushOutOfDate{StdOut: stdout, StdErr: stderr, Err: err}
|
|
|
|
} else if strings.Contains(stderr, "! [remote rejected]") {
|
|
|
|
err := &ErrPushRejected{StdOut: stdout, StdErr: stderr, Err: err}
|
2020-03-28 00:13:18 -04:00
|
|
|
err.GenerateMessage()
|
|
|
|
return err
|
2023-04-13 19:17:27 -04:00
|
|
|
} else if strings.Contains(stderr, "matches more than one") {
|
|
|
|
return &ErrMoreThanOne{StdOut: stdout, StdErr: stderr, Err: err}
|
2020-03-28 00:13:18 -04:00
|
|
|
}
|
2023-04-13 19:17:27 -04:00
|
|
|
return fmt.Errorf("push failed: %w - %s\n%s", err, stderr, stdout)
|
2020-03-28 00:13:18 -04:00
|
|
|
}
|
|
|
|
|
2023-04-13 19:17:27 -04:00
|
|
|
return nil
|
2016-11-03 18:16:01 -04:00
|
|
|
}
|
|
|
|
|
2017-05-30 05:32:01 -04:00
|
|
|
// GetLatestCommitTime returns time for latest commit in repository (across all branches)
|
2022-01-19 18:26:57 -05:00
|
|
|
func GetLatestCommitTime(ctx context.Context, repoPath string) (time.Time, error) {
|
2022-02-06 14:01:47 -05:00
|
|
|
cmd := NewCommand(ctx, "for-each-ref", "--sort=-committerdate", BranchPrefix, "--count", "1", "--format=%(committerdate)")
|
2022-03-31 22:55:30 -04:00
|
|
|
stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath})
|
2017-05-30 05:32:01 -04:00
|
|
|
if err != nil {
|
|
|
|
return time.Time{}, err
|
|
|
|
}
|
|
|
|
commitTime := strings.TrimSpace(stdout)
|
2017-12-10 21:23:34 -05:00
|
|
|
return time.Parse(GitTimeLayout, commitTime)
|
2017-05-30 05:32:01 -04:00
|
|
|
}
|
2019-05-05 12:25:25 -04:00
|
|
|
|
|
|
|
// DivergeObject represents commit count diverging commits
|
|
|
|
type DivergeObject struct {
|
|
|
|
Ahead int
|
|
|
|
Behind int
|
|
|
|
}
|
|
|
|
|
2023-05-04 01:08:41 -04:00
|
|
|
// GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch
|
|
|
|
func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (do DivergeObject, err error) {
|
|
|
|
cmd := NewCommand(ctx, "rev-list", "--count", "--left-right").
|
|
|
|
AddDynamicArguments(baseBranch + "..." + targetBranch)
|
2022-03-31 22:55:30 -04:00
|
|
|
stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath})
|
2019-05-05 12:25:25 -04:00
|
|
|
if err != nil {
|
2023-05-04 01:08:41 -04:00
|
|
|
return do, err
|
2019-05-05 12:25:25 -04:00
|
|
|
}
|
2023-05-04 01:08:41 -04:00
|
|
|
left, right, found := strings.Cut(strings.Trim(stdout, "\n"), "\t")
|
|
|
|
if !found {
|
|
|
|
return do, fmt.Errorf("git rev-list output is missing a tab: %q", stdout)
|
2019-05-05 12:25:25 -04:00
|
|
|
}
|
|
|
|
|
2023-05-04 01:08:41 -04:00
|
|
|
do.Behind, err = strconv.Atoi(left)
|
|
|
|
if err != nil {
|
|
|
|
return do, err
|
2019-05-05 12:25:25 -04:00
|
|
|
}
|
2023-05-04 01:08:41 -04:00
|
|
|
do.Ahead, err = strconv.Atoi(right)
|
|
|
|
if err != nil {
|
|
|
|
return do, err
|
2019-05-05 12:25:25 -04:00
|
|
|
}
|
2023-05-04 01:08:41 -04:00
|
|
|
return do, nil
|
2019-05-05 12:25:25 -04:00
|
|
|
}
|
2021-08-24 12:47:09 -04:00
|
|
|
|
|
|
|
// CreateBundle create bundle content to the target path
|
|
|
|
func (repo *Repository) CreateBundle(ctx context.Context, commit string, out io.Writer) error {
|
|
|
|
tmp, err := os.MkdirTemp(os.TempDir(), "gitea-bundle")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer os.RemoveAll(tmp)
|
|
|
|
|
2021-09-25 17:29:25 -04:00
|
|
|
env := append(os.Environ(), "GIT_OBJECT_DIRECTORY="+filepath.Join(repo.Path, "objects"))
|
2022-03-31 22:55:30 -04:00
|
|
|
_, _, err = NewCommand(ctx, "init", "--bare").RunStdString(&RunOpts{Dir: tmp, Env: env})
|
2021-09-25 17:29:25 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-10-23 10:44:45 -04:00
|
|
|
_, _, err = NewCommand(ctx, "reset", "--soft").AddDynamicArguments(commit).RunStdString(&RunOpts{Dir: tmp, Env: env})
|
2021-09-25 17:29:25 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2021-08-24 12:47:09 -04:00
|
|
|
}
|
2021-09-25 17:29:25 -04:00
|
|
|
|
2022-03-31 22:55:30 -04:00
|
|
|
_, _, err = NewCommand(ctx, "branch", "-m", "bundle").RunStdString(&RunOpts{Dir: tmp, Env: env})
|
2021-09-25 17:29:25 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
tmpFile := filepath.Join(tmp, "bundle")
|
2022-10-23 10:44:45 -04:00
|
|
|
_, _, err = NewCommand(ctx, "bundle", "create").AddDynamicArguments(tmpFile, "bundle", "HEAD").RunStdString(&RunOpts{Dir: tmp, Env: env})
|
2021-08-24 12:47:09 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
fi, err := os.Open(tmpFile)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer fi.Close()
|
|
|
|
|
|
|
|
_, err = io.Copy(out, fi)
|
|
|
|
return err
|
|
|
|
}
|