File: repoutils.go

package info (click to toggle)
git-sizer 1.5.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 616 kB
  • sloc: sh: 100; makefile: 61
file content (289 lines) | stat: -rw-r--r-- 7,456 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
package testutils

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/github/git-sizer/git"
)

// TestRepo represents a git repository used for tests.
type TestRepo struct {
	Path string
}

// NewTestRepo creates and initializes a test repository in a
// temporary directory constructed using `pattern`. The caller must
// delete the repository by calling `repo.Remove()`.
func NewTestRepo(t *testing.T, bare bool, pattern string) *TestRepo {
	t.Helper()

	path, err := ioutil.TempDir("", pattern)
	require.NoError(t, err)

	repo := TestRepo{Path: path}

	repo.Init(t, bare)

	return &TestRepo{
		Path: path,
	}
}

// Init initializes a git repository at `repo.Path`.
func (repo *TestRepo) Init(t *testing.T, bare bool) {
	t.Helper()

	// Don't use `GitCommand()` because the directory might not
	// exist yet:
	var cmd *exec.Cmd
	if bare {
		//nolint:gosec // `repo.Path` is a path that we created.
		cmd = exec.Command("git", "init", "--bare", repo.Path)
	} else {
		//nolint:gosec // `repo.Path` is a path that we created.
		cmd = exec.Command("git", "init", repo.Path)
	}
	cmd.Env = CleanGitEnv()
	err := cmd.Run()
	require.NoError(t, err)
}

// Remove deletes the test repository at `repo.Path`.
func (repo *TestRepo) Remove(t *testing.T) {
	t.Helper()

	_ = os.RemoveAll(repo.Path)
}

// Clone creates a clone of `repo` at a temporary path constructued
// using `pattern`. The caller is responsible for removing it when
// done by calling `Remove()`.
func (repo *TestRepo) Clone(t *testing.T, pattern string) *TestRepo {
	t.Helper()

	path, err := ioutil.TempDir("", pattern)
	require.NoError(t, err)

	err = repo.GitCommand(
		t, "clone", "--bare", "--mirror", repo.Path, path,
	).Run()
	require.NoError(t, err)

	return &TestRepo{
		Path: path,
	}
}

// Repository returns a `*git.Repository` for `repo`.
func (repo *TestRepo) Repository(t *testing.T) *git.Repository {
	t.Helper()

	r, err := git.NewRepository(repo.Path)
	require.NoError(t, err)
	return r
}

// localEnvVars is a list of the variable names that should be cleared
// to give Git a clean environment.
var localEnvVars = func() map[string]bool {
	m := map[string]bool{
		"HOME":            true,
		"XDG_CONFIG_HOME": true,
	}
	out, err := exec.Command("git", "rev-parse", "--local-env-vars").Output()
	if err != nil {
		return m
	}
	for _, k := range strings.Fields(string(out)) {
		m[k] = true
	}
	return m
}()

// CleanGitEnv returns a clean environment for running `git` commands
// so that they won't be affected by the local environment.
func CleanGitEnv() []string {
	osEnv := os.Environ()
	env := make([]string, 0, len(osEnv)+3)
	for _, e := range osEnv {
		i := strings.IndexByte(e, '=')
		if i == -1 {
			// This shouldn't happen, but if it does,
			// ignore it.
			continue
		}
		k := e[:i]
		if localEnvVars[k] {
			continue
		}
		env = append(env, e)
	}
	return append(
		env,
		fmt.Sprintf("HOME=%s", os.DevNull),
		fmt.Sprintf("XDG_CONFIG_HOME=%s", os.DevNull),
		"GIT_CONFIG_NOSYSTEM=1",
	)
}

// GitCommand creates an `*exec.Cmd` for running `git` in `repo` with
// the specified arguments.
func (repo *TestRepo) GitCommand(t *testing.T, args ...string) *exec.Cmd {
	t.Helper()

	gitArgs := []string{"-C", repo.Path}
	gitArgs = append(gitArgs, args...)

	//nolint:gosec // The args all come from the test code.
	cmd := exec.Command("git", gitArgs...)
	cmd.Env = CleanGitEnv()
	return cmd
}

// UpdateRef updates the reference named `refname` to the value `oid`.
func (repo *TestRepo) UpdateRef(t *testing.T, refname string, oid git.OID) {
	t.Helper()

	var cmd *exec.Cmd

	if oid == git.NullOID {
		cmd = repo.GitCommand(t, "update-ref", "-d", refname)
	} else {
		cmd = repo.GitCommand(t, "update-ref", refname, oid.String())
	}
	require.NoError(t, cmd.Run())
}

// CreateObject creates a new Git object, of the specified type, in
// the repository at `repoPath`. `writer` is a function that generates
// the object contents in `git hash-object` input format.
func (repo *TestRepo) CreateObject(
	t *testing.T, otype git.ObjectType, writer func(io.Writer) error,
) git.OID {
	t.Helper()

	cmd := repo.GitCommand(t, "hash-object", "-w", "-t", string(otype), "--stdin")
	in, err := cmd.StdinPipe()
	require.NoError(t, err)

	out, err := cmd.StdoutPipe()
	require.NoError(t, err)
	cmd.Stderr = os.Stderr

	err = cmd.Start()
	require.NoError(t, err)

	err = writer(in)
	err2 := in.Close()
	if !assert.NoError(t, err) {
		_ = cmd.Wait()
		t.FailNow()
	}
	if !assert.NoError(t, err2) {
		_ = cmd.Wait()
		t.FailNow()
	}

	output, err := io.ReadAll(out)
	err2 = cmd.Wait()
	require.NoError(t, err)
	require.NoError(t, err2)

	oid, err := git.NewOID(string(bytes.TrimSpace(output)))
	require.NoError(t, err)
	return oid
}

// AddFile adds and stages a file in `repo` at path `relativePath`
// with the specified `contents`. This must be run in a non-bare
// repository.
func (repo *TestRepo) AddFile(t *testing.T, relativePath, contents string) {
	t.Helper()

	dirPath := filepath.Dir(relativePath)
	if dirPath != "." {
		require.NoError(
			t,
			os.MkdirAll(filepath.Join(repo.Path, dirPath), 0o777),
			"creating subdir",
		)
	}

	filename := filepath.Join(repo.Path, relativePath)
	f, err := os.Create(filename)
	require.NoErrorf(t, err, "creating file %q", filename)
	_, err = f.WriteString(contents)
	require.NoErrorf(t, err, "writing to file %q", filename)
	require.NoErrorf(t, f.Close(), "closing file %q", filename)

	cmd := repo.GitCommand(t, "add", relativePath)
	require.NoErrorf(t, cmd.Run(), "adding file %q", relativePath)
}

// CreateReferencedOrphan creates a simple new orphan commit and
// points the reference with name `refname` at it. This can be run in
// a bare or non-bare repository.
func (repo *TestRepo) CreateReferencedOrphan(t *testing.T, refname string) {
	t.Helper()

	oid := repo.CreateObject(t, "blob", func(w io.Writer) error {
		_, err := fmt.Fprintf(w, "%s\n", refname)
		return err
	})

	oid = repo.CreateObject(t, "tree", func(w io.Writer) error {
		_, err := fmt.Fprintf(w, "100644 a.txt\x00%s", oid.Bytes())
		return err
	})

	oid = repo.CreateObject(t, "commit", func(w io.Writer) error {
		_, err := fmt.Fprintf(
			w,
			"tree %s\n"+
				"author Example <example@example.com> 1112911993 -0700\n"+
				"committer Example <example@example.com> 1112911993 -0700\n"+
				"\n"+
				"Commit for reference %s\n",
			oid, refname,
		)
		return err
	})

	repo.UpdateRef(t, refname, oid)
}

// AddAuthorInfo adds environment variables to `cmd.Env` that set the
// Git author and committer to known values and set the timestamp to
// `*timestamp`. Then `*timestamp` is moved forward by a minute, so
// that each commit gets a unique timestamp.
func AddAuthorInfo(cmd *exec.Cmd, timestamp *time.Time) {
	cmd.Env = append(cmd.Env,
		"GIT_AUTHOR_NAME=Arthur",
		"GIT_AUTHOR_EMAIL=arthur@example.com",
		fmt.Sprintf("GIT_AUTHOR_DATE=%d -0700", timestamp.Unix()),
		"GIT_COMMITTER_NAME=Constance",
		"GIT_COMMITTER_EMAIL=constance@example.com",
		fmt.Sprintf("GIT_COMMITTER_DATE=%d -0700", timestamp.Unix()),
	)
	*timestamp = timestamp.Add(60 * time.Second)
}

// ConfigAdd adds a key-value pair to the gitconfig in `repo`.
func (repo *TestRepo) ConfigAdd(t *testing.T, key, value string) {
	t.Helper()

	err := repo.GitCommand(t, "config", "--add", key, value).Run()
	require.NoError(t, err)
}