code.gitea.io/gitea@v1.19.3/modules/git/repo.go (about) 1 // Copyright 2015 The Gogs Authors. All rights reserved. 2 // Copyright 2017 The Gitea Authors. All rights reserved. 3 // SPDX-License-Identifier: MIT 4 5 package git 6 7 import ( 8 "bytes" 9 "context" 10 "fmt" 11 "io" 12 "net/url" 13 "os" 14 "path" 15 "path/filepath" 16 "strconv" 17 "strings" 18 "time" 19 20 "code.gitea.io/gitea/modules/proxy" 21 "code.gitea.io/gitea/modules/util" 22 ) 23 24 // GPGSettings represents the default GPG settings for this repository 25 type GPGSettings struct { 26 Sign bool 27 KeyID string 28 Email string 29 Name string 30 PublicKeyContent string 31 } 32 33 const prettyLogFormat = `--pretty=format:%H` 34 35 // GetAllCommitsCount returns count of all commits in repository 36 func (repo *Repository) GetAllCommitsCount() (int64, error) { 37 return AllCommitsCount(repo.Ctx, repo.Path, false) 38 } 39 40 func (repo *Repository) parsePrettyFormatLogToList(logs []byte) ([]*Commit, error) { 41 var commits []*Commit 42 if len(logs) == 0 { 43 return commits, nil 44 } 45 46 parts := bytes.Split(logs, []byte{'\n'}) 47 48 for _, commitID := range parts { 49 commit, err := repo.GetCommit(string(commitID)) 50 if err != nil { 51 return nil, err 52 } 53 commits = append(commits, commit) 54 } 55 56 return commits, nil 57 } 58 59 // IsRepoURLAccessible checks if given repository URL is accessible. 60 func IsRepoURLAccessible(ctx context.Context, url string) bool { 61 _, _, err := NewCommand(ctx, "ls-remote", "-q", "-h").AddDynamicArguments(url, "HEAD").RunStdString(nil) 62 return err == nil 63 } 64 65 // InitRepository initializes a new Git repository. 66 func InitRepository(ctx context.Context, repoPath string, bare bool) error { 67 err := os.MkdirAll(repoPath, os.ModePerm) 68 if err != nil { 69 return err 70 } 71 72 cmd := NewCommand(ctx, "init") 73 if bare { 74 cmd.AddArguments("--bare") 75 } 76 _, _, err = cmd.RunStdString(&RunOpts{Dir: repoPath}) 77 return err 78 } 79 80 // IsEmpty Check if repository is empty. 81 func (repo *Repository) IsEmpty() (bool, error) { 82 var errbuf, output strings.Builder 83 if err := NewCommand(repo.Ctx, "show-ref", "--head", "^HEAD$"). 84 Run(&RunOpts{ 85 Dir: repo.Path, 86 Stdout: &output, 87 Stderr: &errbuf, 88 }); err != nil { 89 if err.Error() == "exit status 1" && errbuf.String() == "" { 90 return true, nil 91 } 92 return true, fmt.Errorf("check empty: %w - %s", err, errbuf.String()) 93 } 94 95 return strings.TrimSpace(output.String()) == "", nil 96 } 97 98 // CloneRepoOptions options when clone a repository 99 type CloneRepoOptions struct { 100 Timeout time.Duration 101 Mirror bool 102 Bare bool 103 Quiet bool 104 Branch string 105 Shared bool 106 NoCheckout bool 107 Depth int 108 Filter string 109 SkipTLSVerify bool 110 } 111 112 // Clone clones original repository to target path. 113 func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error { 114 return CloneWithArgs(ctx, globalCommandArgs, from, to, opts) 115 } 116 117 // CloneWithArgs original repository to target path. 118 func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, opts CloneRepoOptions) (err error) { 119 toDir := path.Dir(to) 120 if err = os.MkdirAll(toDir, os.ModePerm); err != nil { 121 return err 122 } 123 124 cmd := NewCommandContextNoGlobals(ctx, args...).AddArguments("clone") 125 if opts.SkipTLSVerify { 126 cmd.AddArguments("-c", "http.sslVerify=false") 127 } 128 if opts.Mirror { 129 cmd.AddArguments("--mirror") 130 } 131 if opts.Bare { 132 cmd.AddArguments("--bare") 133 } 134 if opts.Quiet { 135 cmd.AddArguments("--quiet") 136 } 137 if opts.Shared { 138 cmd.AddArguments("-s") 139 } 140 if opts.NoCheckout { 141 cmd.AddArguments("--no-checkout") 142 } 143 if opts.Depth > 0 { 144 cmd.AddArguments("--depth").AddDynamicArguments(strconv.Itoa(opts.Depth)) 145 } 146 if opts.Filter != "" { 147 cmd.AddArguments("--filter").AddDynamicArguments(opts.Filter) 148 } 149 if len(opts.Branch) > 0 { 150 cmd.AddArguments("-b").AddDynamicArguments(opts.Branch) 151 } 152 cmd.AddDashesAndList(from, to) 153 154 if strings.Contains(from, "://") && strings.Contains(from, "@") { 155 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)) 156 } else { 157 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)) 158 } 159 160 if opts.Timeout <= 0 { 161 opts.Timeout = -1 162 } 163 164 envs := os.Environ() 165 u, err := url.Parse(from) 166 if err == nil { 167 envs = proxy.EnvWithProxy(u) 168 } 169 170 stderr := new(bytes.Buffer) 171 if err = cmd.Run(&RunOpts{ 172 Timeout: opts.Timeout, 173 Env: envs, 174 Stdout: io.Discard, 175 Stderr: stderr, 176 }); err != nil { 177 return ConcatenateError(err, stderr.String()) 178 } 179 return nil 180 } 181 182 // PushOptions options when push to remote 183 type PushOptions struct { 184 Remote string 185 Branch string 186 Force bool 187 Mirror bool 188 Env []string 189 Timeout time.Duration 190 } 191 192 // Push pushs local commits to given remote branch. 193 func Push(ctx context.Context, repoPath string, opts PushOptions) error { 194 cmd := NewCommand(ctx, "push") 195 if opts.Force { 196 cmd.AddArguments("-f") 197 } 198 if opts.Mirror { 199 cmd.AddArguments("--mirror") 200 } 201 remoteBranchArgs := []string{opts.Remote} 202 if len(opts.Branch) > 0 { 203 remoteBranchArgs = append(remoteBranchArgs, opts.Branch) 204 } 205 cmd.AddDashesAndList(remoteBranchArgs...) 206 207 if strings.Contains(opts.Remote, "://") && strings.Contains(opts.Remote, "@") { 208 cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, util.SanitizeCredentialURLs(opts.Remote), opts.Force, opts.Mirror)) 209 } else { 210 cmd.SetDescription(fmt.Sprintf("push branch %s to %s (force: %t, mirror: %t)", opts.Branch, opts.Remote, opts.Force, opts.Mirror)) 211 } 212 var outbuf, errbuf strings.Builder 213 214 if opts.Timeout == 0 { 215 opts.Timeout = -1 216 } 217 218 err := cmd.Run(&RunOpts{ 219 Env: opts.Env, 220 Timeout: opts.Timeout, 221 Dir: repoPath, 222 Stdout: &outbuf, 223 Stderr: &errbuf, 224 }) 225 if err != nil { 226 if strings.Contains(errbuf.String(), "non-fast-forward") { 227 return &ErrPushOutOfDate{ 228 StdOut: outbuf.String(), 229 StdErr: errbuf.String(), 230 Err: err, 231 } 232 } else if strings.Contains(errbuf.String(), "! [remote rejected]") { 233 err := &ErrPushRejected{ 234 StdOut: outbuf.String(), 235 StdErr: errbuf.String(), 236 Err: err, 237 } 238 err.GenerateMessage() 239 return err 240 } else if strings.Contains(errbuf.String(), "matches more than one") { 241 err := &ErrMoreThanOne{ 242 StdOut: outbuf.String(), 243 StdErr: errbuf.String(), 244 Err: err, 245 } 246 return err 247 } 248 } 249 250 if errbuf.Len() > 0 && err != nil { 251 return fmt.Errorf("%w - %s", err, errbuf.String()) 252 } 253 254 return err 255 } 256 257 // GetLatestCommitTime returns time for latest commit in repository (across all branches) 258 func GetLatestCommitTime(ctx context.Context, repoPath string) (time.Time, error) { 259 cmd := NewCommand(ctx, "for-each-ref", "--sort=-committerdate", BranchPrefix, "--count", "1", "--format=%(committerdate)") 260 stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) 261 if err != nil { 262 return time.Time{}, err 263 } 264 commitTime := strings.TrimSpace(stdout) 265 return time.Parse(GitTimeLayout, commitTime) 266 } 267 268 // DivergeObject represents commit count diverging commits 269 type DivergeObject struct { 270 Ahead int 271 Behind int 272 } 273 274 func checkDivergence(ctx context.Context, repoPath, baseBranch, targetBranch string) (int, error) { 275 branches := fmt.Sprintf("%s..%s", baseBranch, targetBranch) 276 cmd := NewCommand(ctx, "rev-list", "--count").AddDynamicArguments(branches) 277 stdout, _, err := cmd.RunStdString(&RunOpts{Dir: repoPath}) 278 if err != nil { 279 return -1, err 280 } 281 outInteger, errInteger := strconv.Atoi(strings.Trim(stdout, "\n")) 282 if errInteger != nil { 283 return -1, errInteger 284 } 285 return outInteger, nil 286 } 287 288 // GetDivergingCommits returns the number of commits a targetBranch is ahead or behind a baseBranch 289 func GetDivergingCommits(ctx context.Context, repoPath, baseBranch, targetBranch string) (DivergeObject, error) { 290 // $(git rev-list --count master..feature) commits ahead of master 291 ahead, errorAhead := checkDivergence(ctx, repoPath, baseBranch, targetBranch) 292 if errorAhead != nil { 293 return DivergeObject{}, errorAhead 294 } 295 296 // $(git rev-list --count feature..master) commits behind master 297 behind, errorBehind := checkDivergence(ctx, repoPath, targetBranch, baseBranch) 298 if errorBehind != nil { 299 return DivergeObject{}, errorBehind 300 } 301 302 return DivergeObject{ahead, behind}, nil 303 } 304 305 // CreateBundle create bundle content to the target path 306 func (repo *Repository) CreateBundle(ctx context.Context, commit string, out io.Writer) error { 307 tmp, err := os.MkdirTemp(os.TempDir(), "gitea-bundle") 308 if err != nil { 309 return err 310 } 311 defer os.RemoveAll(tmp) 312 313 env := append(os.Environ(), "GIT_OBJECT_DIRECTORY="+filepath.Join(repo.Path, "objects")) 314 _, _, err = NewCommand(ctx, "init", "--bare").RunStdString(&RunOpts{Dir: tmp, Env: env}) 315 if err != nil { 316 return err 317 } 318 319 _, _, err = NewCommand(ctx, "reset", "--soft").AddDynamicArguments(commit).RunStdString(&RunOpts{Dir: tmp, Env: env}) 320 if err != nil { 321 return err 322 } 323 324 _, _, err = NewCommand(ctx, "branch", "-m", "bundle").RunStdString(&RunOpts{Dir: tmp, Env: env}) 325 if err != nil { 326 return err 327 } 328 329 tmpFile := filepath.Join(tmp, "bundle") 330 _, _, err = NewCommand(ctx, "bundle", "create").AddDynamicArguments(tmpFile, "bundle", "HEAD").RunStdString(&RunOpts{Dir: tmp, Env: env}) 331 if err != nil { 332 return err 333 } 334 335 fi, err := os.Open(tmpFile) 336 if err != nil { 337 return err 338 } 339 defer fi.Close() 340 341 _, err = io.Copy(out, fi) 342 return err 343 }