-
-
Notifications
You must be signed in to change notification settings - Fork 104
/
git_environment.go
374 lines (348 loc) · 12.8 KB
/
git_environment.go
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
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
package test
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/cucumber/messages-go/v10"
"github.com/git-town/git-town/v8/src/config"
"github.com/git-town/git-town/v8/src/git"
"github.com/git-town/git-town/v8/src/stringslice"
"github.com/git-town/git-town/v8/test/helpers"
)
// GitEnvironment is a complete Git environment for a Cucumber scenario.
type GitEnvironment struct {
// Dir defines the local folder in which this GitEnvironment is stored.
// This folder also acts as the HOME directory for tests using this GitEnvironment.
// It contains the global Git configuration to use in this test.
Dir string
// CoworkerRepo is the optional Git repository that is locally checked out at the coworker machine.
CoworkerRepo *Runner `exhaustruct:"optional"`
// DevRepo is the Git repository that is locally checked out at the developer machine.
DevRepo Runner `exhaustruct:"optional"`
// OriginRepo is the Git repository that simulates the origin repo (on GitHub).
// If this value is nil, the current test setup has no origin.
OriginRepo *Runner `exhaustruct:"optional"`
// SubmoduleRepo is the Git repository that simulates an external repo used as a submodule.
// If this value is nil, the current test setup uses no submodules.
SubmoduleRepo *Runner `exhaustruct:"optional"`
// UpstreamRepo is the optional Git repository that contains the upstream for this environment.
UpstreamRepo *Runner `exhaustruct:"optional"`
}
// CloneGitEnvironment provides a GitEnvironment instance in the given directory,
// containing a copy of the given GitEnvironment.
func CloneGitEnvironment(original GitEnvironment, dir string) (GitEnvironment, error) {
err := CopyDirectory(original.Dir, dir)
if err != nil {
return GitEnvironment{}, fmt.Errorf("cannot clone GitEnvironment %q to folder %q: %w", original.Dir, dir, err)
}
binDir := filepath.Join(dir, "bin")
originDir := filepath.Join(dir, "origin")
originRepo := newRunner(originDir, dir, "")
developerDir := filepath.Join(dir, "developer")
devRepo := newRunner(developerDir, dir, binDir)
result := GitEnvironment{
Dir: dir,
DevRepo: devRepo,
OriginRepo: &originRepo,
}
// Since we copied the files from the memoized directory,
// we have to set the "origin" remote to the copied origin repo here.
_, err = result.DevRepo.Run("git", "remote", "remove", config.OriginRemote)
if err != nil {
return GitEnvironment{}, fmt.Errorf("cannot remove remote: %w", err)
}
err = result.DevRepo.AddRemote(config.OriginRemote, result.originRepoPath())
if err != nil {
return GitEnvironment{}, fmt.Errorf("cannot set remote: %w", err)
}
err = result.DevRepo.Fetch()
if err != nil {
return GitEnvironment{}, fmt.Errorf("cannot fetch: %w", err)
}
// and connect the main branches again
err = result.DevRepo.ConnectTrackingBranch("main")
if err != nil {
return GitEnvironment{}, fmt.Errorf("cannot connect tracking branch: %w", err)
}
return result, err
}
// NewStandardGitEnvironment provides a GitEnvironment in the given directory,
// fully populated as a standardized setup for scenarios.
//
// The origin repo has the initial branch checked out.
// Git repos cannot receive pushes of the currently checked out branch
// because that will change files in the current workspace.
// The tests don't use the initial branch.
func NewStandardGitEnvironment(dir string) (GitEnvironment, error) {
// create the folder
// create the GitEnvironment
gitEnv := GitEnvironment{Dir: dir}
// create the origin repo
err := os.MkdirAll(gitEnv.originRepoPath(), 0o744)
if err != nil {
return gitEnv, fmt.Errorf("cannot create directory %q: %w", gitEnv.originRepoPath(), err)
}
// initialize the repo in the folder
originRepo, err := initRunner(gitEnv.originRepoPath(), gitEnv.Dir, gitEnv.binPath())
if err != nil {
return gitEnv, err
}
err = originRepo.RunMany([][]string{
{"git", "commit", "--allow-empty", "-m", "Initial commit"},
{"git", "branch", "main", "initial"},
})
if err != nil {
return gitEnv, fmt.Errorf("cannot initialize origin directory at %q: %w", gitEnv.originRepoPath(), err)
}
gitEnv.OriginRepo = &originRepo
// clone the "developer" repo
gitEnv.DevRepo, err = originRepo.Clone(gitEnv.developerRepoPath())
if err != nil {
return gitEnv, fmt.Errorf("cannot clone developer repo %q from origin %q: %w", gitEnv.originRepoPath(), gitEnv.developerRepoPath(), err)
}
err = gitEnv.initializeWorkspace(&gitEnv.DevRepo)
if err != nil {
return gitEnv, fmt.Errorf("cannot create new standard Git environment: %w", err)
}
err = gitEnv.DevRepo.RemoveUnnecessaryFiles()
if err != nil {
return gitEnv, err
}
err = gitEnv.OriginRepo.RemoveUnnecessaryFiles()
if err != nil {
return gitEnv, err
}
return gitEnv, nil
}
// AddSubmodule adds a submodule repository.
func (env *GitEnvironment) AddSubmoduleRepo() error {
err := os.MkdirAll(env.submoduleRepoPath(), 0o744)
if err != nil {
return fmt.Errorf("cannot create directory %q: %w", env.submoduleRepoPath(), err)
}
submoduleRepo, err := initRunner(env.submoduleRepoPath(), env.Dir, env.binPath())
if err != nil {
return err
}
err = submoduleRepo.RunMany([][]string{
{"git", "config", "--global", "protocol.file.allow", "always"},
{"git", "commit", "--allow-empty", "-m", "Initial commit"},
})
if err != nil {
return fmt.Errorf("cannot initialize submodule directory at %q: %w", env.originRepoPath(), err)
}
env.SubmoduleRepo = &submoduleRepo
return nil
}
// AddUpstream adds an upstream repository.
func (env *GitEnvironment) AddUpstream() error {
repo, err := env.DevRepo.Clone(filepath.Join(env.Dir, "upstream"))
if err != nil {
return fmt.Errorf("cannot clone upstream: %w", err)
}
env.UpstreamRepo = &repo
err = env.DevRepo.AddRemote("upstream", env.UpstreamRepo.WorkingDir())
if err != nil {
return fmt.Errorf("cannot set upstream remote: %w", err)
}
return nil
}
// AddCoworkerRepo adds a coworker repository.
func (env *GitEnvironment) AddCoworkerRepo() error {
coworkerRepo, err := env.OriginRepo.Clone(env.coworkerRepoPath())
if err != nil {
return fmt.Errorf("cannot clone coworker: %w", err)
}
env.CoworkerRepo = &coworkerRepo
return env.initializeWorkspace(env.CoworkerRepo)
}
// binPath provides the full path of the folder containing the test tools for this GitEnvironment.
func (env *GitEnvironment) binPath() string {
return filepath.Join(env.Dir, "bin")
}
// Branches provides a tabular list of all branches in this GitEnvironment.
func (env *GitEnvironment) Branches() (DataTable, error) {
result := DataTable{}
result.AddRow("REPOSITORY", "BRANCHES")
mainBranch := env.DevRepo.Config.MainBranch()
localBranches, err := env.DevRepo.LocalBranchesMainFirst(mainBranch)
if err != nil {
return result, fmt.Errorf("cannot determine the developer repo branches of the GitEnvironment: %w", err)
}
localBranches = stringslice.Remove(localBranches, "initial")
localBranchesJoined := strings.Join(localBranches, ", ")
if env.OriginRepo == nil {
result.AddRow("local", localBranchesJoined)
return result, nil
}
originBranches, err := env.OriginRepo.LocalBranchesMainFirst(mainBranch)
if err != nil {
return result, fmt.Errorf("cannot determine the origin repo branches of the GitEnvironment: %w", err)
}
originBranches = stringslice.Remove(originBranches, "initial")
originBranchesJoined := strings.Join(originBranches, ", ")
if localBranchesJoined == originBranchesJoined {
result.AddRow("local, origin", localBranchesJoined)
} else {
result.AddRow("local", localBranchesJoined)
result.AddRow("origin", originBranchesJoined)
}
return result, nil
}
// CreateCommits creates the commits described by the given Gherkin table in this Git repository.
func (env *GitEnvironment) CreateCommits(commits []git.Commit) error {
for _, commit := range commits {
var err error
for _, location := range commit.Locations {
switch location {
case "coworker":
err = env.CoworkerRepo.CreateCommit(commit)
case "local":
err = env.DevRepo.CreateCommit(commit)
case "local, origin":
err = env.DevRepo.CreateCommit(commit)
if err != nil {
return fmt.Errorf("cannot create local commit: %w", err)
}
err = env.DevRepo.PushBranchToRemote(commit.Branch, config.OriginRemote)
if err != nil {
return fmt.Errorf("cannot push branch %q after creating commit: %w", commit.Branch, err)
}
case "origin":
err = env.OriginRepo.CreateCommit(commit)
case "upstream":
err = env.UpstreamRepo.CreateCommit(commit)
default:
return fmt.Errorf("unknown commit location %q", commit.Locations)
}
}
if err != nil {
return err
}
}
// after setting up the commits, check out the "initial" branch in the origin repo so that we can git-push to it.
if env.OriginRepo != nil {
err := env.OriginRepo.CheckoutBranch("initial")
if err != nil {
return fmt.Errorf("cannot change origin repo back to initial: %w", err)
}
}
return nil
}
// CreateOriginBranch creates a branch with the given name only in the origin directory.
func (env GitEnvironment) CreateOriginBranch(name, parent string) error {
err := env.OriginRepo.CreateBranch(name, parent)
if err != nil {
return fmt.Errorf("cannot create origin branch %q: %w", name, err)
}
return nil
}
// CreateTags creates tags from the given gherkin table.
func (env GitEnvironment) CreateTags(table *messages.PickleStepArgument_PickleTable) error {
columnNames := helpers.TableFields(table)
if columnNames[0] != "NAME" && columnNames[1] != "LOCATION" {
return fmt.Errorf("tag table must have columns NAME and LOCATION")
}
for _, row := range table.Rows[1:] {
name := row.Cells[0].Value
location := row.Cells[1].Value
var err error
switch location {
case "local":
err = env.DevRepo.CreateTag(name)
case "origin":
err = env.OriginRepo.CreateTag(name)
default:
err = fmt.Errorf("tag table LOCATION must be 'local' or 'origin'")
}
if err != nil {
return err
}
}
return nil
}
// CommitTable provides a table for all commits in this Git environment containing only the given fields.
func (env GitEnvironment) CommitTable(fields []string) (DataTable, error) {
builder := NewCommitTableBuilder()
localCommits, err := env.DevRepo.Commits(fields, "main")
if err != nil {
return DataTable{}, fmt.Errorf("cannot determine commits in the developer repo: %w", err)
}
builder.AddMany(localCommits, "local")
if env.CoworkerRepo != nil {
coworkerCommits, err := env.CoworkerRepo.Commits(fields, "main")
if err != nil {
return DataTable{}, fmt.Errorf("cannot determine commits in the coworker repo: %w", err)
}
builder.AddMany(coworkerCommits, "coworker")
}
if env.OriginRepo != nil {
originCommits, err := env.OriginRepo.Commits(fields, "main")
if err != nil {
return DataTable{}, fmt.Errorf("cannot determine commits in the origin repo: %w", err)
}
builder.AddMany(originCommits, config.OriginRemote)
}
if env.UpstreamRepo != nil {
upstreamCommits, err := env.UpstreamRepo.Commits(fields, "main")
if err != nil {
return DataTable{}, fmt.Errorf("cannot determine commits in the origin repo: %w", err)
}
builder.AddMany(upstreamCommits, "upstream")
}
return builder.Table(fields), nil
}
// TagTable provides a table for all tags in this Git environment.
func (env GitEnvironment) TagTable() (DataTable, error) {
builder := NewTagTableBuilder()
localTags, err := env.DevRepo.Tags()
if err != nil {
return DataTable{}, err
}
builder.AddMany(localTags, "local")
if env.OriginRepo != nil {
originTags, err := env.OriginRepo.Tags()
if err != nil {
return DataTable{}, err
}
builder.AddMany(originTags, config.OriginRemote)
}
return builder.Table(), nil
}
func (env GitEnvironment) initializeWorkspace(repo *Runner) error {
err := repo.Config.SetMainBranch("main")
if err != nil {
return err
}
err = repo.Config.SetPerennialBranches([]string{})
if err != nil {
return err
}
return repo.RunMany([][]string{
{"git", "checkout", "main"},
// NOTE: the developer repos receives the initial branch from origin
// but we don't want it here because it isn't used in tests.
{"git", "branch", "-d", "initial"},
})
}
// coworkerRepoPath provides the full path to the Git repository with the given name.
func (env GitEnvironment) coworkerRepoPath() string {
return filepath.Join(env.Dir, "coworker")
}
// developerRepoPath provides the full path to the Git repository with the given name.
func (env GitEnvironment) developerRepoPath() string {
return filepath.Join(env.Dir, "developer")
}
// originRepoPath provides the full path to the Git repository with the given name.
func (env GitEnvironment) originRepoPath() string {
return filepath.Join(env.Dir, config.OriginRemote)
}
// submoduleRepoPath provides the full path to the Git repository with the given name.
func (env GitEnvironment) submoduleRepoPath() string {
return filepath.Join(env.Dir, "submodule")
}
// Remove deletes all files used by this GitEnvironment from disk.
func (env GitEnvironment) Remove() error {
return os.RemoveAll(env.Dir)
}