-
Notifications
You must be signed in to change notification settings - Fork 0
/
dump_repository.go
180 lines (152 loc) · 5.54 KB
/
dump_repository.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
package dumper
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/http"
)
const (
DefaultBranhcName = "master"
)
type DumpRepositoryOptions struct {
RepositoryURL string
Destination string
Creds Creds
OnlyDefaultBranch *bool
Output *Output
BranchRestrictions *BranchRestrictions
RepositoryRestrictions RepositoryRestrictions
}
type Creds struct {
Username string
Password string
}
// Output defines where all the output should be written
type Output struct {
GitOutput io.Writer
DumperOutput io.Writer
}
func (opts *DumpRepositoryOptions) Validate() error {
if opts.RepositoryURL == "" {
return errors.New("repository url required")
}
if opts.Destination == "" {
return errors.New("destination required")
}
// opts.Creds.Username is not mandatory as usually people
// use app passwords (bitbucket) or access tokens (github)
if opts.Creds.Password == "" {
return errors.New("username or password required")
}
if opts.OnlyDefaultBranch == nil && opts.BranchRestrictions == nil {
return errors.New("either OnlyDefaultBranch or BranchRestrictions required")
}
if (opts.OnlyDefaultBranch != nil && *opts.OnlyDefaultBranch) && opts.BranchRestrictions != nil {
return errors.New("only one branch restriction can be set, use either OnlyDefaultBranch or BranchRestrictions")
}
if (opts.OnlyDefaultBranch == nil || !(*opts.OnlyDefaultBranch)) && opts.BranchRestrictions != nil {
if err := opts.BranchRestrictions.Validate(); err != nil {
return fmt.Errorf("branch restrictions validate: %w", err)
}
}
return nil
}
// BranchRestrictions has power in case OnlyDefaultBranch is not set
type BranchRestrictions struct {
// SingleBranch indicates that only one branch should be cloned
// if not set, all branches will be cloned
// Depends on a BranchName restriction
SingleBranch bool
BranchName string
}
func (br *BranchRestrictions) Validate() error {
if br.SingleBranch && br.BranchName == "" {
return errors.New("branch name required")
}
if !br.SingleBranch && br.BranchName != "" {
return errors.New("branch name is not required when single branch is not set")
}
return nil
}
type RepositoryRestrictions struct {
IgnoreEmptyRepositories bool
}
// DumpRepository dumps single repository
func (d *Dumper) DumpRepository(opts *DumpRepositoryOptions) (*git.Repository, error) {
if opts == nil {
return nil, errors.New("dump repository: options required")
}
if err := opts.Validate(); err != nil {
return nil, fmt.Errorf("dump repository validate options: %w", err)
}
// TODO: implement fetching branches
// TODO: implement letting user know which repos are > 1Gb
// and leave links for them to do that manually
gitCloneOpts := &git.CloneOptions{
URL: opts.RepositoryURL,
RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,
Progress: os.Stdout,
Auth: &http.BasicAuth{
Username: opts.Creds.Username, // in case of access token, username should be empty
Password: opts.Creds.Password,
},
}
if opts.Output != nil && opts.Output.GitOutput != nil {
gitCloneOpts.Progress = opts.Output.GitOutput
}
switch {
// default repository cloning with default branch
case opts.OnlyDefaultBranch != nil && *opts.OnlyDefaultBranch:
return d.plainClone(opts, gitCloneOpts)
// single branch cloning (target branch clone)
case opts.BranchRestrictions != nil && opts.BranchRestrictions.SingleBranch:
gitCloneOpts.SingleBranch = opts.BranchRestrictions.SingleBranch
gitCloneOpts.ReferenceName = plumbing.NewBranchReferenceName(opts.BranchRestrictions.BranchName)
return d.plainClone(opts, gitCloneOpts)
// all branches clone (mirror clone, bare repository)
case opts.BranchRestrictions != nil && !opts.BranchRestrictions.SingleBranch:
gitCloneOpts.Mirror = true
return d.plainClone(opts, gitCloneOpts)
default:
return nil, errors.New("dump repository: unknown error")
}
}
// plainClone is a helper to clone repository
func (d *Dumper) plainClone(opts *DumpRepositoryOptions, gitCloneOpts *git.CloneOptions) (*git.Repository, error) {
repository, err := git.PlainClone(
filepath.Clean(opts.Destination),
false,
gitCloneOpts,
)
if err != nil {
switch {
case errors.Is(err, transport.ErrEmptyRemoteRepository):
if opts.RepositoryRestrictions.IgnoreEmptyRepositories {
return nil, fmt.Errorf("plain clone repository [%s]: %w", gitCloneOpts.URL, err)
}
// taken from workaround from go-git discussions: https://github.com/jmalloc/grit/pull/80/files
r, err := git.PlainInit(opts.Destination, gitCloneOpts.Mirror)
if err != nil {
_ = os.RemoveAll(opts.Destination)
return nil, fmt.Errorf("plain init repository [%s]: %w", gitCloneOpts.URL, err)
}
if _, err := r.CreateRemote(&config.RemoteConfig{Name: git.DefaultRemoteName, URLs: []string{opts.RepositoryURL}}); err != nil {
_ = os.RemoveAll(opts.Destination)
return nil, fmt.Errorf("plain create remote [%s]: %w", gitCloneOpts.URL, err)
}
if err = r.CreateBranch(&config.Branch{Name: DefaultBranhcName, Remote: git.DefaultRemoteName, Merge: plumbing.Master}); err != nil {
_ = os.RemoveAll(opts.Destination)
return nil, fmt.Errorf("plain create branch [%s]: %w", gitCloneOpts.URL, err)
}
default: // all other kinds of errors
return nil, fmt.Errorf("plain clone repository [%s]: %w", gitCloneOpts.URL, err)
}
}
return repository, nil
}