Merge pull request #607 from sinbad/test-enhancements
Add enhanced test setup tools for 'go test' and integration tests
This commit is contained in:
commit
e8a21619a9
70
git/git.go
70
git/git.go
@ -7,10 +7,25 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/github/git-lfs/vendor/_nuts/github.com/rubyist/tracerx"
|
||||
)
|
||||
|
||||
// Some top level information about a commit (only first line of message)
|
||||
type CommitSummary struct {
|
||||
Sha string
|
||||
ShortSha string
|
||||
Parents []string
|
||||
CommitDate time.Time
|
||||
AuthorDate time.Time
|
||||
AuthorName string
|
||||
AuthorEmail string
|
||||
CommitterName string
|
||||
CommitterEmail string
|
||||
Subject string
|
||||
}
|
||||
|
||||
func LsRemote(remote, remoteRef string) (string, error) {
|
||||
if remote == "" {
|
||||
return "", errors.New("remote required")
|
||||
@ -134,3 +149,58 @@ func simpleExec(name string, args ...string) (string, error) {
|
||||
|
||||
return strings.Trim(string(output), " \n"), nil
|
||||
}
|
||||
|
||||
// Parse a Git date formatted in ISO 8601 format (%ci/%ai)
|
||||
func ParseGitDate(str string) (time.Time, error) {
|
||||
|
||||
// Unfortunately Go and Git don't overlap in their builtin date formats
|
||||
// Go's time.RFC1123Z and Git's %cD are ALMOST the same, except that
|
||||
// when the day is < 10 Git outputs a single digit, but Go expects a leading
|
||||
// zero - this is enough to break the parsing. Sigh.
|
||||
|
||||
// Format is for 2 Jan 2006, 15:04:05 -7 UTC as per Go
|
||||
return time.Parse("2006-01-02 15:04:05 -0700", str)
|
||||
}
|
||||
|
||||
// FormatGitDate converts a Go date into a git command line format date
|
||||
func FormatGitDate(tm time.Time) string {
|
||||
// Git format is "Fri Jun 21 20:26:41 2013 +0900" but no zero-leading for day
|
||||
return tm.Format("Mon Jan 2 15:04:05 2006 -0700")
|
||||
}
|
||||
|
||||
// Get summary information about a commit
|
||||
func GetCommitSummary(commit string) (*CommitSummary, error) {
|
||||
cmd := execCommand("git", "show", "-s",
|
||||
`--format=%H|%h|%P|%ai|%ci|%ae|%an|%ce|%cn|%s`, commit)
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to call git show: %v %v", err, string(out))
|
||||
}
|
||||
|
||||
// At most 10 substrings so subject line is not split on anything
|
||||
fields := strings.SplitN(string(out), "|", 10)
|
||||
// Cope with the case where subject is blank
|
||||
if len(fields) >= 9 {
|
||||
ret := &CommitSummary{}
|
||||
// Get SHAs from output, not commit input, so we can support symbolic refs
|
||||
ret.Sha = fields[0]
|
||||
ret.ShortSha = fields[1]
|
||||
ret.Parents = strings.Split(fields[2], " ")
|
||||
// %aD & %cD (RFC2822) matches Go's RFC1123Z format
|
||||
ret.AuthorDate, _ = ParseGitDate(fields[3])
|
||||
ret.CommitDate, _ = ParseGitDate(fields[4])
|
||||
ret.AuthorEmail = fields[5]
|
||||
ret.AuthorName = fields[6]
|
||||
ret.CommitterEmail = fields[7]
|
||||
ret.CommitterName = fields[8]
|
||||
if len(fields) > 9 {
|
||||
ret.Subject = strings.TrimRight(fields[9], "\n")
|
||||
}
|
||||
return ret, nil
|
||||
} else {
|
||||
msg := fmt.Sprintf("Unexpected output from git show: %v", string(out))
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
|
||||
}
|
||||
|
76
lfs/scanner_git_test.go
Normal file
76
lfs/scanner_git_test.go
Normal file
@ -0,0 +1,76 @@
|
||||
package lfs_test // to avoid import cycles
|
||||
|
||||
// This is for doing complete git-level tests using test utils
|
||||
// Needs to be a separate file from scanner_test so that we can use a diff package
|
||||
// which avoids import cycles with testutils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/github/git-lfs/lfs"
|
||||
"github.com/github/git-lfs/test"
|
||||
"github.com/github/git-lfs/vendor/_nuts/github.com/technoweenie/assert"
|
||||
)
|
||||
|
||||
func TestScanUnpushed(t *testing.T) {
|
||||
repo := test.NewRepo(t)
|
||||
repo.Pushd()
|
||||
defer func() {
|
||||
repo.Popd()
|
||||
repo.Cleanup()
|
||||
}()
|
||||
|
||||
inputs := []*test.CommitInput{
|
||||
{ // 0
|
||||
Files: []*test.FileInput{
|
||||
{Filename: "file1.txt", Size: 20},
|
||||
},
|
||||
},
|
||||
{ // 1
|
||||
NewBranch: "branch2",
|
||||
Files: []*test.FileInput{
|
||||
{Filename: "file1.txt", Size: 25},
|
||||
},
|
||||
},
|
||||
{ // 2
|
||||
ParentBranches: []string{"master"}, // back on master
|
||||
Files: []*test.FileInput{
|
||||
{Filename: "file1.txt", Size: 30},
|
||||
},
|
||||
},
|
||||
{ // 3
|
||||
NewBranch: "branch3",
|
||||
Files: []*test.FileInput{
|
||||
{Filename: "file1.txt", Size: 32},
|
||||
},
|
||||
},
|
||||
}
|
||||
repo.AddCommits(inputs)
|
||||
|
||||
// Add a couple of remotes and test state depending on what's pushed
|
||||
repo.AddRemote("origin")
|
||||
repo.AddRemote("upstream")
|
||||
|
||||
pointers, err := ScanUnpushed()
|
||||
assert.Equal(t, nil, err, "Should be no error calling ScanUnpushed")
|
||||
assert.Equal(t, 4, len(pointers), "Should be 4 pointers because none pushed")
|
||||
|
||||
test.RunGitCommand(t, true, "push", "origin", "branch2")
|
||||
// Branch2 will have pushed 2 commits
|
||||
pointers, err = ScanUnpushed()
|
||||
assert.Equal(t, nil, err, "Should be no error calling ScanUnpushed")
|
||||
assert.Equal(t, 2, len(pointers), "Should be 2 pointers")
|
||||
|
||||
test.RunGitCommand(t, true, "push", "upstream", "master")
|
||||
// Master pushes 1 more commit
|
||||
pointers, err = ScanUnpushed()
|
||||
assert.Equal(t, nil, err, "Should be no error calling ScanUnpushed")
|
||||
assert.Equal(t, 1, len(pointers), "Should be 1 pointer")
|
||||
|
||||
test.RunGitCommand(t, true, "push", "origin", "branch3")
|
||||
// All pushed (somewhere)
|
||||
pointers, err = ScanUnpushed()
|
||||
assert.Equal(t, nil, err, "Should be no error calling ScanUnpushed")
|
||||
assert.Equal(t, 0, len(pointers), "Should be 0 pointers unpushed")
|
||||
|
||||
}
|
85
test/cmd/lfstest-testutils.go
Normal file
85
test/cmd/lfstest-testutils.go
Normal file
@ -0,0 +1,85 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/github/git-lfs/test"
|
||||
)
|
||||
|
||||
type TestUtilRepoCallback struct{}
|
||||
|
||||
func (*TestUtilRepoCallback) Fatalf(format string, args ...interface{}) {
|
||||
fmt.Fprintf(os.Stderr, format, args...)
|
||||
os.Exit(4)
|
||||
}
|
||||
func (*TestUtilRepoCallback) Errorf(format string, args ...interface{}) {
|
||||
fmt.Fprintf(os.Stderr, format, args...)
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
commandMap := map[string]func(*test.Repo){
|
||||
"addcommits": AddCommits,
|
||||
}
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintf(os.Stderr, "Command required (e.g. addcommits)\n")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
f, ok := commandMap[os.Args[1]]
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "Unknown command: %v\n", os.Args[1])
|
||||
os.Exit(2)
|
||||
}
|
||||
// Construct test repo context (note: no Cleanup() call since managed outside)
|
||||
// also assume we're in the same folder
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Problem getting working dir: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
// Make sure we're directly inside directory which contains .git
|
||||
// don't want to accidentally end up committing to some other parent git
|
||||
_, err = os.Stat(filepath.Join(wd, ".git"))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "You're in the wrong directory, should be in root of a test repo: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
repo := test.WrapRepo(&TestUtilRepoCallback{}, wd)
|
||||
f(repo)
|
||||
}
|
||||
|
||||
func AddCommits(repo *test.Repo) {
|
||||
// Read stdin as JSON []*test.CommitInput
|
||||
in, err := ioutil.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "addcommits: Unable to read input data: %v\n", err)
|
||||
os.Exit(3)
|
||||
}
|
||||
inputs := make([]*test.CommitInput, 0)
|
||||
err = json.Unmarshal(in, &inputs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "addcommits: Unable to unmarshal JSON: %v\n%v\n", string(in), err)
|
||||
os.Exit(3)
|
||||
}
|
||||
outputs := repo.AddCommits(inputs)
|
||||
|
||||
by, err := json.Marshal(outputs)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "addcommits: Unable to marshal output JSON: %v\n", err)
|
||||
os.Exit(3)
|
||||
}
|
||||
// Write response to stdout
|
||||
_, err = os.Stdout.Write(by)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "addcommits: Error writing JSON to stdout: %v\n", err)
|
||||
os.Exit(3)
|
||||
}
|
||||
os.Stdout.WriteString("\n")
|
||||
|
||||
}
|
@ -84,3 +84,50 @@ begin_test "push object id(s)"
|
||||
grep "(2 of 2 files)" push.log
|
||||
)
|
||||
end_test
|
||||
|
||||
begin_test "push modified files"
|
||||
(
|
||||
set -e
|
||||
|
||||
reponame="$(basename "$0" ".sh")-modified"
|
||||
setup_remote_repo "$reponame"
|
||||
clone_repo "$reponame" "$reponame"
|
||||
|
||||
git lfs track "*.dat"
|
||||
# generate content we'll use
|
||||
content1="filecontent1"
|
||||
content2="filecontent2"
|
||||
content3="filecontent3"
|
||||
oid1=$(printf "$content1" | shasum -a 256 | cut -f 1 -d " ")
|
||||
oid2=$(printf "$content2" | shasum -a 256 | cut -f 1 -d " ")
|
||||
oid3=$(printf "$content3" | shasum -a 256 | cut -f 1 -d " ")
|
||||
oid4=$(printf "$content4" | shasum -a 256 | cut -f 1 -d " ")
|
||||
|
||||
echo "[
|
||||
{
|
||||
\"CommitDate\":\"$(get_date -6m)\",
|
||||
\"Files\":[
|
||||
{\"Filename\":\"file1.dat\",\"Size\":${#content1}, \"Data\":\"$content1\"}]
|
||||
},
|
||||
{
|
||||
\"CommitDate\":\"$(get_date -3m)\",
|
||||
\"Files\":[
|
||||
{\"Filename\":\"file1.dat\",\"Size\":${#content2}, \"Data\":\"$content2\"}]
|
||||
},
|
||||
{
|
||||
\"CommitDate\":\"$(get_date -1m)\",
|
||||
\"Files\":[
|
||||
{\"Filename\":\"file1.dat\",\"Size\":${#content3}, \"Data\":\"$content3\"},
|
||||
{\"Filename\":\"file2.dat\",\"Size\":${#content4}, \"Data\":\"$content4\"}]
|
||||
}
|
||||
]" | lfstest-testutils addcommits
|
||||
|
||||
git push origin master
|
||||
assert_server_object "$reponame" "$oid1"
|
||||
assert_server_object "$reponame" "$oid2"
|
||||
assert_server_object "$reponame" "$oid3"
|
||||
assert_server_object "$reponame" "$oid4"
|
||||
)
|
||||
end_test
|
||||
|
||||
|
||||
|
@ -304,3 +304,41 @@ comparison_to_operator() {
|
||||
echo "???"
|
||||
fi
|
||||
}
|
||||
|
||||
# Get a date string with an offset
|
||||
# Args: One or more date offsets of the form (regex) "[+-]\d+[dmyHM]"
|
||||
# e.g. +1d = 1 day forward from today
|
||||
# -5y = 5 years before today
|
||||
# Example call:
|
||||
# D=$(get_date +1y +1m -5H)
|
||||
# returns date as string in RFC3339 format ccyy-mm-ddThh:MM:ssZ
|
||||
# note returns in UTC time not local time hence Z and not +/-
|
||||
get_date() {
|
||||
# Wrapped because BSD (inc OSX) & GNU 'date' functions are different
|
||||
# on Windows under Git Bash it's GNU
|
||||
if date --version >/dev/null 2>&1 ; then # GNU
|
||||
ARGS=""
|
||||
for var in "$@"
|
||||
do
|
||||
# GNU offsets are more verbose
|
||||
unit=${var: -1}
|
||||
val=${var:0:${#var}-1}
|
||||
case "$unit" in
|
||||
d) unit="days" ;;
|
||||
m) unit="months" ;;
|
||||
y) unit="years" ;;
|
||||
H) unit="hours" ;;
|
||||
M) unit="minutes" ;;
|
||||
esac
|
||||
ARGS="$ARGS $val $unit"
|
||||
done
|
||||
date -d "$ARGS" -u +%Y-%m-%dT%TZ
|
||||
else # BSD
|
||||
ARGS=""
|
||||
for var in "$@"
|
||||
do
|
||||
ARGS="$ARGS -v$var"
|
||||
done
|
||||
date $ARGS -u +%Y-%m-%dT%TZ
|
||||
fi
|
||||
}
|
404
test/testutils.go
Normal file
404
test/testutils.go
Normal file
@ -0,0 +1,404 @@
|
||||
package test
|
||||
|
||||
// Utility functions for more complex go tests
|
||||
// Need to be in a separate test package so they can be imported anywhere
|
||||
// Also can't add _test.go suffix to exclude from main build (import doesn't work)
|
||||
|
||||
// To avoid import cycles, append "_test" to the package statement of any test using
|
||||
// this package and use "import . original/package/name" to get the same visibility
|
||||
// as if the test was in the same package (as usual)
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/github/git-lfs/git"
|
||||
"github.com/github/git-lfs/lfs"
|
||||
)
|
||||
|
||||
type RepoType int
|
||||
|
||||
const (
|
||||
// Normal repo with working copy
|
||||
RepoTypeNormal = RepoType(iota)
|
||||
// Bare repo (no working copy)
|
||||
RepoTypeBare = RepoType(iota)
|
||||
// Repo with working copy but git dir is separate
|
||||
RepoTypeSeparateDir = RepoType(iota)
|
||||
)
|
||||
|
||||
type RepoCreateSettings struct {
|
||||
RepoType RepoType
|
||||
}
|
||||
|
||||
// Callback interface (testing.T compatible)
|
||||
type RepoCallback interface {
|
||||
// Fatalf reports error and fails
|
||||
Fatalf(format string, args ...interface{})
|
||||
// Errorf reports error and continues
|
||||
Errorf(format string, args ...interface{})
|
||||
}
|
||||
type Repo struct {
|
||||
// Path to the repo, working copy if non-bare
|
||||
Path string
|
||||
// Path to the git dir
|
||||
GitDir string
|
||||
// Paths to remotes
|
||||
Remotes map[string]*Repo
|
||||
// Settings used to create this repo
|
||||
Settings *RepoCreateSettings
|
||||
// Previous dir for pushd
|
||||
popDir string
|
||||
// Test callback
|
||||
callback RepoCallback
|
||||
}
|
||||
|
||||
// Change to repo dir but save current dir
|
||||
func (r *Repo) Pushd() {
|
||||
if r.popDir != "" {
|
||||
r.callback.Fatalf("Cannot Pushd twice")
|
||||
}
|
||||
oldwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
r.callback.Fatalf("Can't get cwd %v", err)
|
||||
}
|
||||
err = os.Chdir(r.Path)
|
||||
if err != nil {
|
||||
r.callback.Fatalf("Can't chdir %v", err)
|
||||
}
|
||||
r.popDir = oldwd
|
||||
}
|
||||
|
||||
func (r *Repo) Popd() {
|
||||
if r.popDir != "" {
|
||||
err := os.Chdir(r.popDir)
|
||||
if err != nil {
|
||||
r.callback.Fatalf("Can't chdir %v", err)
|
||||
}
|
||||
r.popDir = ""
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repo) Cleanup() {
|
||||
|
||||
// pop out if necessary
|
||||
r.Popd()
|
||||
|
||||
// Make sure cwd isn't inside a path we're going to delete
|
||||
oldwd, err := os.Getwd()
|
||||
if err == nil {
|
||||
if strings.HasPrefix(oldwd, r.Path) ||
|
||||
strings.HasPrefix(oldwd, r.GitDir) {
|
||||
os.Chdir(os.TempDir())
|
||||
}
|
||||
}
|
||||
|
||||
if r.GitDir != "" {
|
||||
os.RemoveAll(r.GitDir)
|
||||
r.GitDir = ""
|
||||
}
|
||||
if r.Path != "" {
|
||||
os.RemoveAll(r.Path)
|
||||
r.Path = ""
|
||||
}
|
||||
for _, remote := range r.Remotes {
|
||||
remote.Cleanup()
|
||||
}
|
||||
r.Remotes = nil
|
||||
}
|
||||
|
||||
// NewRepo creates a new git repo in a new temp dir
|
||||
func NewRepo(callback RepoCallback) *Repo {
|
||||
return NewCustomRepo(callback, &RepoCreateSettings{RepoType: RepoTypeNormal})
|
||||
}
|
||||
|
||||
// NewCustomRepo creates a new git repo in a new temp dir with more control over settings
|
||||
func NewCustomRepo(callback RepoCallback, settings *RepoCreateSettings) *Repo {
|
||||
ret := &Repo{
|
||||
Settings: settings,
|
||||
Remotes: make(map[string]*Repo),
|
||||
callback: callback}
|
||||
|
||||
path, err := ioutil.TempDir("", "lfsRepo")
|
||||
if err != nil {
|
||||
callback.Fatalf("Can't create temp dir for git repo: %v", err)
|
||||
}
|
||||
ret.Path = path
|
||||
args := []string{"init"}
|
||||
switch settings.RepoType {
|
||||
case RepoTypeBare:
|
||||
args = append(args, "--bare")
|
||||
ret.GitDir = ret.Path
|
||||
case RepoTypeSeparateDir:
|
||||
gitdir, err := ioutil.TempDir("", "lfstestgitdir")
|
||||
if err != nil {
|
||||
ret.Cleanup()
|
||||
callback.Fatalf("Can't create temp dir for git repo: %v", err)
|
||||
}
|
||||
args = append(args, "--separate-dir", gitdir)
|
||||
ret.GitDir = gitdir
|
||||
default:
|
||||
ret.GitDir = filepath.Join(ret.Path, ".git")
|
||||
}
|
||||
args = append(args, path)
|
||||
cmd := exec.Command("git", args...)
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
ret.Cleanup()
|
||||
callback.Fatalf("Unable to create git repo at %v: %v", path, err)
|
||||
}
|
||||
|
||||
// Configure default user/email so not reliant on env
|
||||
ret.Pushd()
|
||||
RunGitCommand(callback, true, "config", "user.name", "Git LFS Tests")
|
||||
RunGitCommand(callback, true, "config", "user.email", "git-lfs@example.com")
|
||||
ret.Popd()
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// WrapRepo creates a new Repo instance for an existing git repo
|
||||
func WrapRepo(c RepoCallback, path string) *Repo {
|
||||
return &Repo{Path: path, callback: c, Settings: &RepoCreateSettings{RepoType: RepoTypeNormal}}
|
||||
}
|
||||
|
||||
// Simplistic fire & forget running of git command - returns combined output
|
||||
func RunGitCommand(callback RepoCallback, failureCheck bool, args ...string) string {
|
||||
outp, err := exec.Command("git", args...).CombinedOutput()
|
||||
if failureCheck && err != nil {
|
||||
callback.Fatalf("Error running git command 'git %v': %v %v", strings.Join(args, " "), err, string(outp))
|
||||
}
|
||||
return string(outp)
|
||||
|
||||
}
|
||||
|
||||
// Input data for a single file in a commit
|
||||
type FileInput struct {
|
||||
// Name of file (required)
|
||||
Filename string
|
||||
// Size of file (required)
|
||||
Size int64
|
||||
// Input data (optional, if provided will be source of data)
|
||||
DataReader io.Reader
|
||||
// Input data (optional, if provided will be source of data)
|
||||
Data string
|
||||
}
|
||||
|
||||
// Input for defining commits for test repo
|
||||
type CommitInput struct {
|
||||
// Date that we should commit on (optional, leave blank for 'now')
|
||||
CommitDate time.Time
|
||||
// List of files to include in this commit
|
||||
Files []*FileInput
|
||||
// List of parent branches (all branches must have been created in a previous NewBranch or be master)
|
||||
// Can be omitted to just use the parent of the previous commit
|
||||
ParentBranches []string
|
||||
// Name of a new branch we should create at this commit (optional - master not required)
|
||||
NewBranch string
|
||||
// Names of any tags we should create at this commit (optional)
|
||||
Tags []string
|
||||
// Name of committer
|
||||
CommitterName string
|
||||
// Email of committer
|
||||
CommitterEmail string
|
||||
}
|
||||
|
||||
// Output struct with details of commits created for test
|
||||
type CommitOutput struct {
|
||||
Sha string
|
||||
Parents []string
|
||||
Files []*lfs.Pointer
|
||||
}
|
||||
|
||||
func commitAtDate(atDate time.Time, committerName, committerEmail, msg string) error {
|
||||
var args []string
|
||||
if committerName != "" && committerEmail != "" {
|
||||
args = append(args, "-c", fmt.Sprintf("user.name=%v", committerName))
|
||||
args = append(args, "-c", fmt.Sprintf("user.email=%v", committerEmail))
|
||||
}
|
||||
args = append(args, "commit", "--allow-empty", "-m", msg)
|
||||
cmd := exec.Command("git", args...)
|
||||
env := os.Environ()
|
||||
// set GIT_COMMITTER_DATE environment var e.g. "Fri Jun 21 20:26:41 2013 +0900"
|
||||
if atDate.IsZero() {
|
||||
env = append(env, "GIT_COMMITTER_DATE=")
|
||||
} else {
|
||||
env = append(env, fmt.Sprintf("GIT_COMMITTER_DATE=%v", git.FormatGitDate(atDate)))
|
||||
}
|
||||
cmd.Env = env
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v %v", err, string(out))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo *Repo) AddCommits(inputs []*CommitInput) []*CommitOutput {
|
||||
if repo.Settings.RepoType == RepoTypeBare {
|
||||
repo.callback.Fatalf("Cannot use AddCommits on a bare repo; clone it & push changes instead")
|
||||
}
|
||||
|
||||
// Change to repo working dir
|
||||
oldwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
repo.callback.Fatalf("Can't get cwd %v", err)
|
||||
}
|
||||
err = os.Chdir(repo.Path)
|
||||
if err != nil {
|
||||
repo.callback.Fatalf("Can't chdir to repo %v", err)
|
||||
}
|
||||
// Used to check whether we need to checkout another commit before
|
||||
lastBranch := "master"
|
||||
outputs := make([]*CommitOutput, 0, len(inputs))
|
||||
|
||||
for i, input := range inputs {
|
||||
output := &CommitOutput{}
|
||||
// first, are we on the correct branch
|
||||
if len(input.ParentBranches) > 0 {
|
||||
if input.ParentBranches[0] != lastBranch {
|
||||
RunGitCommand(repo.callback, true, "checkout", input.ParentBranches[0])
|
||||
lastBranch = input.ParentBranches[0]
|
||||
}
|
||||
}
|
||||
// Is this a merge?
|
||||
if len(input.ParentBranches) > 1 {
|
||||
// Always take the *other* side in a merge so we adopt changes
|
||||
// also don't automatically commit, we'll do that below
|
||||
args := []string{"merge", "--no-ff", "--no-commit", "--strategy-option=theirs"}
|
||||
args = append(args, input.ParentBranches[1:]...)
|
||||
RunGitCommand(repo.callback, false, args...)
|
||||
} else if input.NewBranch != "" {
|
||||
RunGitCommand(repo.callback, true, "checkout", "-b", input.NewBranch)
|
||||
lastBranch = input.NewBranch
|
||||
}
|
||||
// Any files to write?
|
||||
for fi, infile := range input.Files {
|
||||
inputData := infile.DataReader
|
||||
if inputData == nil && infile.Data != "" {
|
||||
inputData = strings.NewReader(infile.Data)
|
||||
}
|
||||
if inputData == nil {
|
||||
// Different data for each file but deterministic
|
||||
inputData = NewPlaceholderDataReader(int64(i*fi), infile.Size)
|
||||
}
|
||||
cleaned, err := lfs.PointerClean(inputData, infile.Filename, infile.Size, nil)
|
||||
if err != nil {
|
||||
repo.callback.Errorf("Error creating pointer file: %v", err)
|
||||
continue
|
||||
}
|
||||
// this only created the temp file, move to final location
|
||||
tmpfile := cleaned.Filename
|
||||
mediafile, err := lfs.LocalMediaPath(cleaned.Oid)
|
||||
if err != nil {
|
||||
repo.callback.Errorf("Unable to get local media path: %v", err)
|
||||
continue
|
||||
}
|
||||
if _, err := os.Stat(mediafile); err != nil {
|
||||
if err := os.Rename(tmpfile, mediafile); err != nil {
|
||||
repo.callback.Errorf("Unable to move %s to %s: %v", tmpfile, mediafile, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
output.Files = append(output.Files, cleaned.Pointer)
|
||||
// Write pointer to local filename for adding (not using clean filter)
|
||||
os.MkdirAll(filepath.Dir(infile.Filename), 0755)
|
||||
f, err := os.Create(infile.Filename)
|
||||
if err != nil {
|
||||
repo.callback.Errorf("Error creating pointer file: %v", err)
|
||||
continue
|
||||
}
|
||||
_, err = cleaned.Pointer.Encode(f)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
repo.callback.Errorf("Error encoding pointer file: %v", err)
|
||||
continue
|
||||
}
|
||||
f.Close() // early close in a loop, don't defer
|
||||
RunGitCommand(repo.callback, true, "add", infile.Filename)
|
||||
|
||||
}
|
||||
// Now commit
|
||||
err = commitAtDate(input.CommitDate, input.CommitterName, input.CommitterEmail,
|
||||
fmt.Sprintf("Test commit %d", i))
|
||||
if err != nil {
|
||||
repo.callback.Fatalf("Error committing: %v", err)
|
||||
}
|
||||
|
||||
commit, err := git.GetCommitSummary("HEAD")
|
||||
if err != nil {
|
||||
repo.callback.Fatalf("Error determining commit SHA: %v", err)
|
||||
}
|
||||
|
||||
// tags
|
||||
for _, tag := range input.Tags {
|
||||
// Use annotated tags, assume full release tags (also tag objects have edge cases)
|
||||
RunGitCommand(repo.callback, true, "tag", "-a", "-m", "Added tag", tag)
|
||||
}
|
||||
|
||||
output.Sha = commit.Sha
|
||||
output.Parents = commit.Parents
|
||||
outputs = append(outputs, output)
|
||||
}
|
||||
|
||||
// Restore cwd
|
||||
err = os.Chdir(oldwd)
|
||||
if err != nil {
|
||||
repo.callback.Fatalf("Can't restore old cwd %v", err)
|
||||
}
|
||||
|
||||
return outputs
|
||||
}
|
||||
|
||||
// Add a new remote (generate a path for it to live in, will be cleaned up)
|
||||
func (r *Repo) AddRemote(name string) *Repo {
|
||||
if _, exists := r.Remotes[name]; exists {
|
||||
r.callback.Fatalf("Remote %v already exists", name)
|
||||
}
|
||||
remote := NewCustomRepo(r.callback, &RepoCreateSettings{RepoTypeBare})
|
||||
r.Remotes[name] = remote
|
||||
RunGitCommand(r.callback, true, "remote", "add", name, remote.Path)
|
||||
return remote
|
||||
}
|
||||
|
||||
// Just a psuedo-random stream of bytes (not cryptographic)
|
||||
// Calls RNG a bit less often than using rand.Source directly
|
||||
type PlaceholderDataReader struct {
|
||||
source rand.Source
|
||||
bytesLeft int64
|
||||
}
|
||||
|
||||
func NewPlaceholderDataReader(seed, size int64) *PlaceholderDataReader {
|
||||
return &PlaceholderDataReader{rand.NewSource(seed), size}
|
||||
}
|
||||
|
||||
func (r *PlaceholderDataReader) Read(p []byte) (int, error) {
|
||||
c := len(p)
|
||||
i := 0
|
||||
for i < c && r.bytesLeft > 0 {
|
||||
// Use all 8 bytes of the 64-bit random number
|
||||
val64 := r.source.Int63()
|
||||
for j := 0; j < 8 && i < c && r.bytesLeft > 0; j++ {
|
||||
// Duplicate this byte 16 times (faster)
|
||||
for k := 0; k < 16 && r.bytesLeft > 0; k++ {
|
||||
p[i] = byte(val64)
|
||||
i++
|
||||
r.bytesLeft--
|
||||
}
|
||||
// Next byte from the 8-byte number
|
||||
val64 = val64 >> 8
|
||||
}
|
||||
}
|
||||
var err error
|
||||
if r.bytesLeft == 0 {
|
||||
err = io.EOF
|
||||
}
|
||||
return i, err
|
||||
}
|
Loading…
Reference in New Issue
Block a user